Creating a rich text editor - Part 3 - Entities and decorators
8 mins read
In the second part of the tutorial, we added keyboard shortcuts to change styling of selected text to bold, italic or underline.
In this part, we will use draft-js Entity to add metadata to characters that can be used to add hyperlinks to the selected text besides the basic styling.
In Draft, each and every character can have a metadata, meaning any kind of extra information about a character, besides the character itself, is stored in the editor JSON data. This range of text with metadata is called an Entity. An Entity also has a decorator. A decorator in Draft has two keys,
strategy- A function that iterates over the characters of a block and finds continuous ranges of text having the same entity.component- The range of text is then rendered using theReactcomponent that is provided.
Each Entity has a mutability status which can be read about here.
We will use the good old browser prompt to prompt user to type the link they want to add to the selected text.
The flow will be -
- User selects a range of text
- Presses a key combination (in this case CMD/CTRL + K)
- Browser prompt opens up and asks user to type the link
- User types the link and presses
RETURN - The input link is added to the selected text and this will be designated by an anchor tag appearing for the selected text.
Let us create the link plugin to add links to text.
- Create a file
addLinkPlugin.jsinsrc/plugins. - Add the following code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React from 'react';
import {
RichUtils,
KeyBindingUtil,
EditorState,
} from 'draft-js';
export const linkStrategy = (contentBlock, callback, contentState) => {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
};
export const Link = (props) => {
const { contentState, entityKey } = props;
const { url } = contentState.getEntity(entityKey).getData();
return (
<a
className="link"
href={url}
rel="noopener noreferrer"
target="_blank"
aria-label={url}
>{props.children}</a>
);
};
const addLinkPluginPlugin = {
keyBindingFn(event, { getEditorState }) {
const editorState = getEditorState();
const selection = editorState.getSelection();
// Don't do anything if no text is selected.
if (selection.isCollapsed()) {
return;
}
if (KeyBindingUtil.hasCommandModifier(event) && event.which === 75) {
return 'add-link'
}
},
handleKeyCommand(command, editorState, { getEditorState, setEditorState}) {
if (command !== 'add-link') {
return 'not-handled';
}
let link = window.prompt('Paste the link -');
const selection = editorState.getSelection();
if (!link) {
setEditorState(RichUtils.toggleLink(editorState, selection, null));
return 'handled';
}
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity('LINK', 'MUTABLE', { url: link });
const newEditorState = EditorState.push(editorState, contentWithEntity, 'create-entity');
const entityKey = contentWithEntity.getLastCreatedEntityKey();
setEditorState(RichUtils.toggleLink(newEditorState, selection, entityKey))
return 'handled';
},
decorators: [{
strategy: linkStrategy,
component: Link,
}],
};
export default addLinkPluginPlugin;
Here we are -
- First creating a
strategyas explained earlier to find continuous characters withentitytype ofLINK. It has the signature as defined above. - Then we define a
Linkcomponent that renders ananchortag with the url from the entity data. - Then we are creating the actual plugin.
In this plugin,
- We use the
keyBindingFnto return astringofadd-linkif the key combination matches CMD/CTRL + K and some text is selected.draft-jshas a utility objectKeyBindingUtilwith methodhasCommandModifierto checkCMDpress on OSX orCTRLpress on other device. - Then the
handleKeyCommandmethod checks whether the command isadd-link. If not, it does nothing. Otherwise, it opens the browser prompt, takes in the input url, and applies that url asLINKentity to the selected text.Entityis applied by first creating a new content data usingcontent.createEntitywith entity type ofLINK, mutability of ‘MUTABLE’ and the entity data. If no url is input, it tries to remove any existing link from the selection. - This new content with entity applied is push onto the editor stack and then,
RichUtils.toggleLinkis called which sets the entity on the selected range of text. - Finally, there is
decoratorswhich should be an array of objects with each object having astrategyand acomponent. We set the first object to thestrategyandcomponentdefined earlier.
Now, let us use this newly created plugin in our editor component. Open src/MyEditor.js and update the code to this -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import 'draft-js/dist/Draft.css';
import './MyEditor.css';
import React from 'react';
import { EditorState } from 'draft-js';
import Editor from 'draft-js-plugins-editor';
import basicTextStylePlugin from './plugins/basicTextStylePlugin';
// import the add link plugin
import addLinkPlugin from './plugins/addLinkPlugin';
class MyEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
// add the plugin to the array
this.plugins = [
addLinkPlugin,
basicTextStylePlugin,
];
}
componentDidMount() {
this.focus();
}
onChange = (editorState) => {
this.setState({
editorState,
});
}
focus = () => {
this.editor.focus();
}
render() {
const { editorState } = this.state;
return (
<div className="editor" onClick={this.focus}>
<Editor
editorState={editorState}
onChange={this.onChange}
plugins={this.plugins}
ref={(element) => { this.editor = element; }}
placeholder="Tell your story"
spellCheck
/>
</div>
);
}
}
export default MyEditor;
In the above code, the order of the plugins in the this.plugins array is very important. If the basicTextStylePlugin was before addLinkPlugin, the keyBindingFn method of addLinkPlugin will never be called. That is because if you study the keyBindingFn of basicTextStylePlugin, you will see that it always returns a string by using the getDefaultKeyBinding function available in draft-js. So, the keyBindingFn of addLinkPlugin will never be called. That is why we first use the addLinkPlugin then the other one as keyBindingFn will return a string only if the key combination matches to CMD/CTRL + K. In other cases, nothing is returned and keyBindingFn of basicTextStylePlugin takes over.
Now, type some text, select it and press CMD/CTRL + K, type a link and press ENTER. The selected text should show up as a link. If it does not, read away. I have encountered this bug while using draft-js-plugins-editor where before rendering the Editor its decorator is set to null. If the decorator is null, Draft won’t know how to render a link entity. To prevent this, you should change the onChange method of MyEditor to this -
1
2
3
4
5
6
7
onChange = (editorState) => {
if (editorState.getDecorator() !== null) {
this.setState({
editorState,
});
}
}
After this change, repeat the steps to add a link. It should now show the links as links.
Let’s add some CSS to amke the links more brighter. Change the contents of src/MyEditor.css to this -
1
2
3
4
5
6
7
8
.editor {
width: 600px;
margin: 0 auto;
}
.editor a {
color: #08c;
}

Now we have a working link addition functionality. You can get the source code from this GitHub repo . The code for this tutorial can be run from the part3 tag. After cloning the repo, you can do
git checkout part3
Note
If you are getting a
getEditorStateis not a function error in the console, changehandleKeyCommandmethod in all the plugins from
handleKeyCommand(command, { getEditorState, setEditorState})TO
handleKeyCommand(command, editorState, { getEditorState, setEditorState})and also modify the use of
editorStateinside the method.
List of tutorials in this series –
- Part 1 - Barebones Editor
- Part 2 - Text Styling
- Part 3 - Entities and decorators