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 :
- Limited length , provisional 200 A word ;
- mention (@) Time highlight , When user input @ The symbol will be followed by @ The text after the symbol highlights ;
- 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-...