Hey it’s part 5 of the series! I hope you are enjoying playing with Draft.js so far.
Today, I want to talk a bit about inserting content into the editor.
Some examples of when you may want to do this:
- (simple) emoji picker: Maybe you want to let people click an emoji icon and have it automatically insert that emoji character
- (simple) mentioning: Maybe you want to have an icon you click that inserts the
@
mention trigger character - (more complicated) markdown: Maybe you want to be able to insert markdown characters under certain circumstances, or wrap text in markdown characters (eg maybe highlighting a word and hitting cmd/ctrl b wraps it in
**
instead of actually bolding) - (more complicated) find-and-replace functionality
Let’s look at a simple example first 🙂
Here is what the finished product will look like
Simple example: Emoji Insertion
⁉️ For this example we’ll just be inserting the emoji characters into our editor when you click on them. If you are doing some real-life emoji work with your editor, you may want to look at something like This Draft.Js emoji plugin which converts emoji into their own custom Entity and provides a lot more flexibility for styling in-editor, as well as swapping out emoji assets for non-native assets if you want to.
The Modifier Module
We’re going to be using Draft.js’s Modifier to insert content into our editor.
What we want to do is insert the emoji at whatever location the user’s caret is currently at (their current “selection”) . We also want to replace content, if they have a range of text selected.
For this we’ll use Modifier
’s method replaceText
which takes:
- The editor’s current
ContentState
- The text to be replaced, as indicated by
SelectionState
(this provides a range so the editor can say “ok I need to replace the content that currently exists between location X and location Y”. You could provide any range you like, provided content exists in that range, but for this simple case we just want to insert text where the selection already is, so no need to specify anything custom) - The text to be inserted.
What it returns is a new instance of ContentState
that contains this change.
However, we still need to get this change into our EditorState
and we can do this by using EditorState.push
EditorState.push
takes
- The editor state to apply changes to
- The content state to apply (so we want the result of
Modifier.replaceText
here) - What kind of change you’re making, also known as an EditorChangeType. We’re doing
insert-characters
EditorState.push
returns a new instance of EditorState
.
So with this in mind I wrote this small insertCharacter
helper function:
function insertCharacter(characterToInsert, editorState) {
const currentContent = editorState.getCurrentContent(),
currentSelection = editorState.getSelection();
const newContent = Modifier.replaceText(
currentContent,
currentSelection,
characterToInsert
);
const newEditorState = EditorState.push(editorState, newContent, 'insert-characters');
return newEditorState;
}
It takes the character you want to insert, as well as the current editorState
. It applies the modifications needed, then returns the new editorState
.
We’d then need to apply this.setState
with the new editorState to ensure we are actually using it, so the code that calls it would be something like:
const newEditorState = insertCharacter('💖', this.state.editorState);
this.setState({
editorState: newEditorState
});
For my example I wanted to make some little emoji buttons, so I added this to my render method:
<div className="emoji-picker">
<h2 className="toolbar-title">Insert Emoji:</h2>
<button
className="emoji"
onMouseDown={(e) => e.preventDefault()}
onClick={this.onEmojiClick}
data-emoji="🎊">
<span role="img" aria-label="confetti">🎊</span>
</button>
<button
className="emoji"
onMouseDown={(e) => e.preventDefault()}
onClick={this.onEmojiClick}
data-emoji="💖">
<span role="img" aria-label="sparkle heart">💖</span>
</button>
<button
className="emoji"
onMouseDown={(e) => e.preventDefault()}
onClick={this.onEmojiClick}
data-emoji="🌼">
<span role="img" aria-label="yellow flower">🌼</span>
</button>
</div>
Then I defined onEmojiClick
like so -
onEmojiClick (e) {
let emoji = e.currentTarget.getAttribute('data-emoji');
this.setState({editorState: insertCharacter(emoji, this.state.editorState)});
}
In other words, exactly what we talked about 🙂
There’s one other little gotcha here. You may notice that if you click the button when the editor doesn’t have focus, an emoji is inserted but the editor remains unfocused, which is likely not your desired behaviour.
If you like, you can ensure focus by using EditorState.forceSelection
and update your insertCharacter
method slightly:
function insertCharacter(characterToInsert, editorState) {
const currentContent = editorState.getCurrentContent(),
currentSelection = editorState.getSelection();
const newContent = Modifier.replaceText(
currentContent,
currentSelection,
characterToInsert
);
const newEditorState = EditorState.push(editorState, newContent, 'insert-characters');
return EditorState.forceSelection(newEditorState, newContent.getSelectionAfter());
}
This will make sure that after you insert the character, the selected state will be immediately after that character. (Official forceSelection documentation here)
So there’s our little intro into making modifications to your editor’s content. I will follow up in my next post with some trickier content replacement 🙂 (I would have done it all in one but I think shorter articles are a bit easier for people to consume, plus it makes it easier for me to publish updates more frequently 🙃)
Thanks (as always) for reading 💕
Top comments (9)
Hi Rose, suggestion for a follow-up article would be using some of the draftjs plugins.
I'm fairly new to react, and tried to follow some of the documentation (github.com/draft-js-plugins/draft-...) and was more confused than ever before.
Thanks again for these articles, they're pure gold.
Absolutely I'd love to dive into that! I've been a bit out of commission recently but as soon as I have free time again I'll try to add some more to this series :)
Hi Im having a problem with my decorator instance. Anytime the decorator takes place based on some regex like mention, if a press the spacebar to create another text writing a single char, if i want to erase that char by pressing backspace the cursor moves to the front of the text without erasing the text. But if i type a second character after and perform the backspace to erase the chars after the decorator block it erases
Hey Rose, awesome article! Are you planning on continuing with the series? If so, it would be great if you wrote about entities. Your explanations are awesome!
I'm so glad you're finding them valuable :) a few people have commented with requests for various draftjs topics and many of them are great suggestions. I do want to continue the series but they are pretty labour intensive so I need to both have the time and the inclination, and those two things need to line up perfectly 😅 so yes to more posts (I hope/think.) But it may not be for quite some time 🙁 comments like yours do inspire me to want to continue so thanks for asking. I'll definitely prioritize a draftjs topic for my next post!
Thank you for putting so much effort. I will wait for your next post then!
Hi, great series. It would be cool to do a post on code highlighting via prism or the like.
Great idea!
Great tutorial.
A post on handling sanitizing on paste would be great, even more so a post about custom types, entities and decorators.