Introduction

Draft.js is a framework by Facebook designed to develop text editors in React.js. The source code of the project has already scored more than 22,000 stars on github.

Specific requirements for the created editor, of course, will require writing own logic, but a large toolkit of the library will help to do this without violating the concept and approach. Here are examples of text editors developed with Draft.js from Awesome Draft.js. The article describes key features of the library. Examples of the code were taken from publicly available Draft.js documentation, as well as from articles devoted to Draft.js.

Framework description

The main component of Draft.js is the Editor with mandatory EditorState and onChange parameters.

EditorState is an immutable object that contains the editor’s full state: text, cursor position, and history of changes. Any change to the editor will create a new editorState object using onChange. onChange compares the new and old editor states and applies the updated state.

Example:

import {Editor, EditorState} from ‘draft-js’;

export default class DraftEditor extends Component {
constructor() {
super();
this.state = {
editorState: EditorState.createEmpty()
};
this.onChange = (editorState) => {
console.log('editorState ==>', editorState.toJS());

this.setState({ editorState });
}
}
render() {
return (
<div id=”content”>
<h1>Draft.js Editor</h1>
<div className=”editor”>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
/>
</div>
</div>
);}}

Adding basic functionality using Rich Text

RichUtils module provides a set of functions for embedded and custom blocks out of the box.
The module contains standard commands, for example, Cmd+B – bold, Cmd+I – italic and Cmd+U – underscore. And we can also add our own commands with their own styles using the handleKeyCommand function to apply or delete the desired style.

Example of handleKeyCommand definition:

handleKeyCommand = (command) => {
const newState = RichUtils.handleKeyCommand(this.state.editorState, command)
if (newState) {
this.onChange(newState);
return 'handled';
}
return 'not-handled';
}

We pass the command, for example, boldor underline as an argument that will be passed to
RichUtils. handleKeyCommand – a function that accepts commands. If the updated EditorState value is returned as a result, we pass it to the onChange method, which will update the editor:

<Editor
editorState={this.state.editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.onChange}/>

RichUtils buttons

To toggle styles, use the toggleInlineStyle function imported from RichUtils. The function parameters pass the editor state and style type.

onItalicClick = () => {
this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'ITALIC'))
}

Toggle button:

<button onClick={this.onItalicClick}></button>

Adding links – Entities and Decorators in Draft.js

Entities are entities that contain additional information about sections of text.
To create an Entity, the createEntity function is used, which takes two required arguments (type and mutability) and one optional argument (additional data).

Below is presented the function of adding a link:

setLink() {
const urlValue = prompt('Введите ссылку', '');
const { editorState } = this.state;
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK',
'SEGMENTED',
{ url: urlValue }
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const newEditorState = EditorState.set(editorState, {currentContent: contentStateWithEntity});
this.setState({
editorState: RichUtils.toggleLink(
newEditorState,
newEditorState.getSelection(),
entityKey
)
}, () => {
setTimeout(() => this.focus(), 0);
});
}

Below is the decorator, which, using the strategy and data of the entity, represents our link in the text and makes it possible to go to the address:

const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link
}
]);
this.state = {
inlineToolbar: { show: false },
editorState: EditorState.createEmpty(decorator)
};

When creating an instance of the CompositeDecorator class, pass an array of objects with the strategy and component properties as an argument to the constructor.

Strategy – will compare and check entities for compliance with the requirements. If the requirements match, then this section will be wrapped with a component, which, for example, will render the link to us.

function findLinkEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(
(character) => {const entityKey = character.getEntity();
return (
entityKey !== null && contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback);}
const Link = (
props) => {
const { url } = props.contentState.getEntity(props.entityKey).getData();
return (<a href={url} title={url} className="ed-link"> {props.children} </a>);
};

Custom block

Draft.js provides the possibility to customize blocks, i.e. editor strings according to your requirements. Up to adding images, audio and media files both in a standard way and by dragging and dropping.

constructor() {
this.getEditorState = () => this.state.editorState;
this.blockRendererFn = customBlockRenderer(
this.onChange,
this.getEditorState
);}
const customBlockRenderer = (setEditorState, getEditorState) => (contentBlock) => {
const type =
contentBlock.getType();
switch (type) {
case 'SLIDER':
return {
component: EditorSlider,
props: {
getEditorState,
setEditorState,
} }
default: return null;}};
const RenderMap = new Map({
SLIDER: {
element: 'div',
}
}).merge(DefaultDraftBlockRenderMap);

To create a custom block, it is required to create a separate component and inform the editor that this block needs to be rendered and specify its type in advance. To do this, define the CustomBlockRenderer function, which includes two arguments: this is the new state of the editorState editor and its current state. After that, define RenderMap and specify in it the rules according to which the editor will wrap the elements with our custom block as a wrapper.

render() {
<Editor
editorState={editorState}
onChange={this.onChange}
handleKeyCommand={this.handleKeyCommand}
customStyleMap={customStyleMap}
handleDroppedFiles={this.handleDroppedFiles} // <-- [4]
handleReturn={this.handleReturn} // <-- [3]
blockRenderMap={RenderMap} // <-- [1]
blockRendererFn={this.blockRendererFn} // <-- [1]
ref="editor"
/>);
}

RenderMap and blockRenderFn shall be passed in props to the Editor. We also need to define the functions: handleReturn and handleDroppedFiles.

HandleReturn – will be responsible for pressing Enter. Depending on what kind of behavior we want to observe when pressing this key, such logic should be specified in this function.

HandleDroppedFiles – handles the drag and drop event. We can manage the received files using it.

handleDroppedFiles(selection, files) {
const filteredFiles = files
.filter(file => (file.type.indexOf('image/') === 0)); // <-- [1]

if (!filteredFiles.length) {
return 'not_handled'; // <-- [2]
}

this.onChange(addNewBlockAt( // <-- [3]
this.state.editorState,
selection.getAnchorKey(),
'SLIDER',
new Map({ slides: _map(
filteredFiles,
file => ({ url: urlCreator.createObjectURL(file) }) // <-- [4]
)})
));
return 'handled';
}

Using the example above, we check the files for an image and if it is an image, we update the editor using the addNewBlockAt function with the following parameters: current state, selection – a text fragment, block type, and data for this block. Otherwise, we will not respond.

The updateData function that updates the custom block data is shown below:

updateData(data) {
const editorState = this.props.blockProps.getEditorState();
const content = editorState.getCurrentContent();

const selection = new SelectionState({
anchorKey: this.props.block.key,
anchorOffset: 0,
focusKey: this.props.block.key,
focusOffset: this.props.block.getLength()
});

const newContentState = Modifier.mergeBlockData(content, selection, data);
const newEditorState = EditorState.push(editorState, newContentState);

setTimeout(() => this.props.blockProps.setEditorState(newEditorState));
}

Result

The article presents the main tools of the Draft.js framework. With their help, you can perform key actions for working with text in the browser. In all other non-standard tasks, you can write your own solution implementation, but it is important to use the utility methods and functions of the framework itself. Otherwise, the likelihood of losing the productivity and flexibility of working with text increases.

Example

On one of the projects, we encountered the following problem – the user began to randomly click on the lines of the editor and simultaneously enter characters. In this case, the editor terminated with a critical error.

Since the editor did not have time to analyze the position of the cursor and characters, it took an incorrect state, as a result of which work on the browser page was blocked.

It was decided to override the handleBeforeInput method. The method is called before entering characters. After overriding the method, we added a check for the correctness of the editor state before entering a character.

As a result, the bug was fixed, but the performance got worse. When the text was changed, all editor lines were updated without exception, even those ones that were not affected by the changes. It was necessary to improve the solution by adding the shouldComponentUpdate method to the “string” component with a number of conditions that prevent a meaningless update.