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 theReact
component 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.js
insrc/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
strategy
as explained earlier to find continuous characters withentity
type ofLINK
. It has the signature as defined above. - Then we define a
Link
component that renders ananchor
tag with the url from the entity data. - Then we are creating the actual plugin.
In this plugin,
- We use the
keyBindingFn
to return astring
ofadd-link
if the key combination matches CMD/CTRL + K and some text is selected.draft-js
has a utility objectKeyBindingUtil
with methodhasCommandModifier
to checkCMD
press on OSX orCTRL
press on other device. - Then the
handleKeyCommand
method 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 asLINK
entity to the selected text.Entity
is applied by first creating a new content data usingcontent.createEntity
with 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.toggleLink
is called which sets the entity on the selected range of text. - Finally, there is
decorators
which should be an array of objects with each object having astrategy
and acomponent
. We set the first object to thestrategy
andcomponent
defined 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
getEditorState
is not a function error in the console, changehandleKeyCommand
method in all the plugins from
handleKeyCommand(command, { getEditorState, setEditorState})
TO
handleKeyCommand(command, editorState, { getEditorState, setEditorState})
and also modify the use of
editorState
inside the method.
List of tutorials in this series –
- Part 1 - Barebones Editor
- Part 2 - Text Styling
- Part 3 - Entities and decorators