Draft.JS. Editor with hashtags and mentions. Part 2

Aleksandr Nikiforov
Beauty Coder
Published in
4 min readAug 1, 2017

Hey, #beautycoders. Last time we created a simple editor that shows us a list of suggestions for mentions and hashtags. In this article we will see how to use different events in the Draft.JS to control our list and how to change the EditorState to insert a suggestion.

Draft.JS. Editor with hashtags and mentions. Part 1.

To control different Editor events Draft.JS provides two type of handlers: Cancelable handlers and Key Handlers.

Cancelable handlers
These prop functions are provided to allow custom event handling for a small set of useful events. By returning ‘handled’ from your handler, you indicate that the event is handled and the Draft core should do nothing more with it. By returning ‘not-handled’, you defer to Draft to handle the event.

Key Handlers
These prop functions expose common useful key events. Example: At Facebook, these are used to provide keyboard interaction for mention results in inputs.

We need those handlers to control our list. What are we trying to achieve? We want to be able to use the arrow keys to change a selection in the list, *Esc* button to close the list and we need to be able to insert the suggestion by pressing *Tab* or *Return*.

Let’s add events for the arrows first. The idea is the following: If we press the “Up” button, we decrease the selection number in the autocompleteState, if we press the “Down” button we increase the selection number, then we call onAutocompleteChange method.


this.onUpArrow = (e) => {
this.onArrow(e, this.props.onUpArrow, -1);
};
this.onDownArrow = (e) => {
this.onArrow(e, this.props.onDownArrow, 1);
};
this.onArrow = (e, originalHandler, nudgeAmount) => {
const {
onAutocompleteChange
} = this.props;
let autocompleteState = this.getAutocompleteState(false);
if (!autocompleteState) {
if (originalHandler) {
originalHandler(e);
}
return;
}
e.preventDefault();
autocompleteState.selectedIndex += nudgeAmount;
this.autocompleteState = autocompleteState;
if (onAutocompleteChange) {
onAutocompleteChange(autocompleteState);
}
};

If the user doesn’t want to use suggestions, he has to be able to hide them. To do that, we need to implement a handler for the escape button. All we need to do is to set to null the autocompleteState property.


this.onEscape = (e) => {
const {
onEscape,
onAutocompleteChange
} = this.props;
if (!this.getAutocompleteState(false)) {
if (onEscape) {
onEscape(e);
}
return;
}
e.preventDefault();
this.autocompleteState = null;
if (onAutocompleteChange) {
onAutocompleteChange(null);
}
};

The last thing we want to do is to insert our suggestion when we press the Tab or the Return keys. We will use two events: onTab and handleReturn respectively, but in the beginning we have to implement a method to insert our text to Editor.

So, what exactly are we going to do? First, we have to get the information about the insertion: trigger, selectedIndex, start position and end position of the inserted text. We already have the first two.

So the first thing we need do is to get insertState.

getInsertState(selectedIndex, trigger) {
const {
editorState
} = this.props;
const currentSelectionState = editorState.getSelection();
const end = currentSelectionState.getAnchorOffset();
const anchorKey = currentSelectionState.getAnchorKey();
const currentContent = editorState.getCurrentContent();
const currentBlock = currentContent.getBlockForKey(anchorKey);
const blockText = currentBlock.getText();
const start = blockText.substring(0, end).lastIndexOf(trigger);
return {
editorState, //Current Editor State
start, //Start position
end, //End position
trigger, //Trigger
selectedIndex //Selected position in array.
}
}

To get the end of the replacement text we used a SelecetionState object.

SelectionState is an Immutable Record that represents a selection range in the editor.

In our case we will use it to get the position of the cursor.

To get the start position we use ContentBlock, and found the position for the last entry of the trigger.

ContentBlock is an Immutable Record that represents the full state of a single block of editor content

Now we need to insert text into editor.

commitSelection(e) {
const {
onAutocompleteChange
} = this.props;
let autocompleteState = this.getAutocompleteState(false);
//Check if editor in autocomplete mode.
if (!autocompleteState) {
return false;
}
e.preventDefault();

//Insert text
this.onMentionSelect();

//Close suggestion list.
this.autocompleteState = null;
if (onAutocompleteChange) {
onAutocompleteChange(null);
}
return true;
};

onMentionSelect() {
let autocompleteState = this.getAutocompleteState(false);
const {
editorState
} = this.props;
//Getring insertState.
const insertState =
this.getInsertState(autocompleteState.selectedIndex,
autocompleteState.trigger);
const {
onInsert
} = this.props;
//Pass insert state to parent object.
const newEditorState = onInsert(insertState);
const {
onChange
} = this.props;
//Update EditorState.
onChange(newEditorState);
};

To insert text into the Editor we have to change our current EditorState, but we don’t want to just insert the text, we want to make this text Immutable. If you are planning to use it for hashtags, then it’s not necessary, but it’s necessary for mentions as they represent a real user and can not be changed.

We are creating an Entity for a suggestion text with the Immutable property and then creating a new content using Modifier .

const addSuggestion = ({editorState, start, end, trigger, text}) => {
const entityKey = Entity.create(‘MENTION’, ‘IMMUTABLE’,
http://google.com");
const currentSelectionState = editorState.getSelection();
const mentionTextSelection = currentSelectionState.merge({
anchorOffset: start,
focusOffset: end
});
let insertingContent = Modifier.replaceText(
editorState.getCurrentContent(),
mentionTextSelection,
text, [‘link’, ‘BOLD’],
entityKey
);
const blockKey = mentionTextSelection.getAnchorKey();
const blockSize = editorState.getCurrentContent()
.getBlockForKey(blockKey).getLength();
if (blockSize === end) {
insertingContent = Modifier.insertText(
insertingContent,
insertingContent.getSelectionAfter(),
‘ ‘
);
}
const newEditorState = EditorState.push(
editorState,
insertingContent,
‘insert-mention’
);
return EditorState.forceSelection(newEditorState,
insertingContent.getSelectionAfter());
};

Now, when we have already implemented these methods, we can call them in onTab and handleReturn events.

this.onTab = (e) => {
this.commitSelection(e)
};
this.handleReturn = (e) => {
return this.commitSelection(e);
}

You can find the full working example with the source code on my GitHub.
And you can see the result here

--

--