当前位置:网站首页>How to design a message box through draftjs

How to design a message box through draftjs

2022-06-12 21:20:00 Omni image cloud low code

draftjs brief introduction

draftjs Is used for react Rich text editor framework for , It doesn't work out of the box , But it provides a lot of tools for developing rich text API. Based on this , Developers can build customized rich text editors .draftjs There are several important concepts :EditorState、Entity、SelectionState、CompositeDecorator.

EditorState

EditorState Is the top-level state object of the editor . It is an immutable data , Express Draft The whole state of the editor , Include :

  • Current text content status (ContentState)
  • Current selection status (SelectionState)
  • The decorator of the content (Decorator)
  • revoke / Redo stack
  • The latest type of changes made to the content (EditorChangeType)

draftjs Based on immutability (immutable) data , Therefore, changes to the editor need to generate a new EditorState Object passed into the editor , To achieve data update .

Entity

Entity Used to describe text with metadata , So that a piece of text can carry any type of data , More functions are provided , link 、 The content mentioned and embedded can be through Entity To achieve .

Entity Structure

{
    type: 'string', 
    //  Express Entity The type of ; eg:'LINK', 'TOKEN', 'PHOTO', 'IMAGE'
    mutability: 'MUTABLE' | 'IMMUTABLE' | 'SEGMENTED', 
    //  This attribute represents the behavior of using the text range of this entity object annotation when editing the text range in the editor .
    data: 'object', 
    // Entity Metadata ;  Used to store what you want to store in the Entity Any information in 
}

among Mutability The meanings of the three values of this attribute are :

  • Immutable: this Entity As a whole , Once deleted, it will be deleted as a whole , Cannot change text ;
  • Mutable:Entity The text in the editor can be modified freely , For example, link text ;
  • Segmented: On Immutable similar , The difference is that part of the text can be deleted ;

SelectionState

SelectionState Represents the selection range in the editor . There are two options : Anchor point ( The starting point ) And focus ( End ).

  • Anchor location === Focus position , No text selected ;
  • Anchor location > Focus position , Select text from right to left ;
  • Anchor location < Focus position , Select text from left to right ;

CompositeDecorator

Decorator The concept is based on scanning a given ContentBlock The content of , Locate the matching location according to the defined policy , Then use the specified React Components render them .

Implement a message box

First, identify the needs :

  1. Limited length , provisional 200 A word ;
  2. mention (@) Time highlight , When user input @ The symbol will be followed by @ The text after the symbol highlights ;
  3. Insert link ;

First implement a basic editor :

import React from 'react'
import { Editor, EditorState } from 'draft-js';
import 'draft-js/dist/Draft.css';

import './App.css';

function MyEditor() {
  const [editorState, setEditorState] = React.useState(
    () => EditorState.createEmpty(),
  );

  const handleEditorChange = (newEditorState) => {
    setEditorState(newEditorState);
  }

  return (
    <div className='box'>
      <Editor editorState={editorState} onChange={handleEditorChange} />
      <button className='btn'> Submit </button>
    </div>
  );
}

export default MyEditor;

You can see that a text box with a toolbar does not appear , Instead, an editable area is generated , Next, we will give him unique functions .

Need one : Limit message length

There are two forms of input to the editor : Keyboard entry and pasting , General input Input box we can use maxLength To limit ,draftjs No such attribute , But it provides handleBeforeInput and handlePastedText These two methods .

handleBeforeInput

handleBeforeInput?: ( chars: string, //  Input content  editorState: EditorState, //  The text content status of the editor  eventTimeStamp: number, ) => 'handled' | 'not-handled'

When handleBeforeInput return handled The default behavior of input will be blocked ,handlePastedText Empathy .

handlePastedText

handlePastedText?: ( text: string, html?: string, editorState: EditorState, ) => 'handled' | 'not-handled'

Next, modify our code :

const MAX_LENGTH = 200;

function MyEditor() {
  const [editorState, setEditorState] = React.useState(
    () => EditorState.createEmpty(),
  );

  const handleEditorChange = (newEditorState) => {
    setEditorState(newEditorState);
  }

  const handleBeforeInput = (_, editorState) => {
    //  Gets the text content status of the editor 
    const currentContent = editorState.getCurrentContent(); 
    //  Gets the length of the editor text ,getPlainText Returns the text content of the current editor , String type 
    const currentContentLength = currentContent.getPlainText('').length;
    if (currentContentLength > MAX_LENGTH - 1) {
     //  Block input when the current text length is greater than the maximum length , On the contrary, it is allowed to enter 
      return 'handled';
    }

    return 'not-handled';
  }

  return (
    <div className='box'>
      <Editor editorState={editorState} onChange={handleEditorChange} handleBeforeInput={handleBeforeInput} />
      <button className='btn'> Submit </button>
    </div>
  );
}

There may be a doubt here :MAX_LENGTH Why subtract one ?

as a result of handleBeforeInput Trigger before input , therefore getPlainText Returns the content before the editor content changes . Length of previous content + Length of input < Maximum length , Because it's keyboard input , Therefore, the length of the input content is always 1. It's not over yet. , There are also cases where text content is selected and then input is not handled . And that's where it comes in SelectionState 了 .

add to getLengthOfSelectedText function :

  const getLengthOfSelectedText = () => {
    //  Get the selection status of the editor 
    const currentSelection = editorState.getSelection();
    //  Return to the selection status , The offset of anchor and focus is the same ( no choice ) And anchor and focus block_key Return... When the same true
    const isCollapsed = currentSelection.isCollapsed();
    let length = 0;
    if (!isCollapsed) {
      const currentContent = editorState.getCurrentContent();
      //  Get the starting position of the selection range block_key
      const startKey = currentSelection.getStartKey();
      //  Get the end position of the selection range block_key
      const endKey = currentSelection.getEndKey();
      if (startKey === endKey) {
        //  The selection range is in the same block, Then choose the length = End offset - Start offset 
        length += currentSelection.getEndOffset() - currentSelection.getStartOffset();
      } else {
        const startBlockTextLength = currentContent.getBlockForKey(startKey).getLength();
        //  start block The selected length  =  start block The length of - Start offset 
        const startSelectedTextLength = startBlockTextLength -                                   currentSelection.getStartOffset();
        //  The end is at the end block Offset in 
        const endSelectedTextLength = currentSelection.getEndOffset();
        // getKeyAfter Returns the specified key Of block Back one block Of key
        const keyAfterEnd = currentContent.getKeyAfter(endKey);
        let currentKey = startKey;
        //  Start of accumulation block To end block In the middle of the block The selected length 
        while (currentKey && currentKey !== keyAfterEnd) {
          if (currentKey === startKey) {
            length += startSelectedTextLength + 1;
          } else if (currentKey === endKey) {
            length += endSelectedTextLength;
          } else {
            length += currentContent.getBlockForKey(currentKey).getLength() + 1;
          }

          currentKey = currentContent.getKeyAfter(currentKey);
        }
      }
    }

    return length;
  };

This method is a little long , It's about draftjs Several api and block The concept of , A little more complicated , But the use is simple , Is to get the selected length . Now let's transform handleBeforeInput:

    const handleBeforeInput = (_, editorState) => {
    const currentContent = editorState.getCurrentContent();
    const currentContentLength = currentContent.getPlainText('').length;
    //  Actual length  =  The length of the current content - Select the length ( The length replaced )
    if (currentContentLength - getLengthOfSelectedText() > MAX_LENGTH - 1) {
      return 'handled';
    }

    return 'not-handled';
  }

Follow the gourd , Now let's add handlePastedText, In case of pasting , There are more pastedText( Pasted text ) Parameters .

  const handlePastedText = (pastedText) => {
    const currentContent = editorState.getCurrentContent();
    const currentContentLength = currentContent.getPlainText('').length;
    const selectedTextLength = getLengthOfSelectedText();
    if (currentContentLength + pastedText.length - selectedTextLength > maxLength - 1) {
      return 'handled';
    }

    return 'not-handled';
  };

In order to have a better use experience , You can add a current content length in the lower right corner of the editor / Prompt for maximum length . modified handleEditorChange Method , Use the current text length as state Store it .

  const handleEditorChange = (newEditorState) => {
    const currentContent = newEditorState.getCurrentContent();
    const currentContentLength = currentContent.getPlainText('').length;
    setLength(currentContentLength);
    setEditorState(newEditorState);
  }

Adjust the style , Look at the effect :

So far, we have completed the first requirement .

Demand two : mention (@) Time highlight

Generally speaking, there will be @ The text behind the symbol changes color to show the difference , We can use a regular expression to match @ The symbol and the following text , Then replace it with our custom one ReactNode, You can achieve highlighting , That's exactly what it is. Decorator Where it comes in handy .

We just need to create one CompositeDecorator example , Passed in when the editor is initialized createEmpty Medium will do .

  const HANDLE_REGEX = /@[\w]+/g;
  const compositeDecorator = new CompositeDecorator([
    {
      strategy: (contentBlock, callback) => {
        //  Every time the editor change Will trigger this function , Get the content text .
        const text = contentBlock.getText();
        let matchArr, start;
        while ((matchArr = HANDLE_REGEX.exec(text)) !== null) {
          //  Get the starting position and offset of the matching value ,callback Then it will be decorator Of component Replace 
          start = matchArr.index;
          callback(start, start + matchArr[0].length);
        }
      },
      component: (props) => {
        return (
          <span className='mention' data-offset-key={props.offsetKey} >
            {props.children}
          </span>
        );
      },
    },
  ]);

  const [editorState, setEditorState] = React.useState(
    () => EditorState.createEmpty(compositeDecorator),
  );

Look at the effect :

Demand 3 : Insert link

Link display text , Mouse in prompt url. Plain text can no longer describe this information , And that's where it comes in Entity. add to insertEntity function :

  const insertEntity = (entityData) => {
    let contentState = editorState.getCurrentContent();
      //  Create entities 
    contentState = contentState.createEntity('LINK', 'IMMUTABLE', entityData);
    const entityKey = contentState.getLastCreatedEntityKey();
    let selection = editorState.getSelection();
      //  Determine whether to replace or insert 
    if (selection.isCollapsed()) {
      contentState = Modifier.insertText(
        contentState, selection, entityData.name + ' ', undefined, entityKey,
      );
    } else {
      contentState = Modifier.replaceText(
        contentState, selection, entityData.name + ' ', undefined, entityKey,
      );
    }

    let end;
      //  Gets the range of entities displayed in the editor , The purpose is to make the cursor stay at the tail of the entity after inserting the entity 
    contentState.getFirstBlock().findEntityRanges(
      (character) => character.getEntity() === entityKey,
      (_, _end) => {
        end = _end;
      });

    let newEditorState = EditorState.set(editorState, { currentContent: contentState });
    selection = selection.merge({
      anchorOffset: end,
      focusOffset: end,
    });
    newEditorState = EditorState.forceSelection(newEditorState, selection);
    handleEditorChange(newEditorState);
  };

Look at the effect :

complete !

Complete code

Due to the large space occupied by the complete code , For full code, please pay attention to official account. “ Full image cloud low code ”, reply “ Message box complete code ” Can get .

Quotes

draftjs: https://draftjs.org/

official account : Full image cloud low code
GitHub:https://github.com/quanxiang-...

原网站

版权声明
本文为[Omni image cloud low code]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202281411474241.html