Note: This post comes from SendGrid’s Engineering Team. For more technical engineering posts like this, check out our technical blogroll.
Draft.js is a powerful library that provides APIs for manipulating text styles within a content editable HTML element. Unfortunately, it exposes very little styling support out of the box, requiring one to implement most functionality from scratch.
However, several WYSIWYG editors have been built using Draft.js and some utility packages exist for extending the core library’s functionality. Many of these existing editors would be a fine solution for an application requiring minor modification of a WYSIWYG editor with a toolbar sitting directly above it.
In our case, the designs for the text module style panel within our drag & drop editor necessitated crafting a custom Draft.js implementation which would allow for the editor and style controls to live in distinct locations within the DOM tree.
Below we’ll discuss some of the trials and tribulations our team uncovered while working with Draft.js.
Before diving in, this post will identify and define some key concepts necessary for working with the Draft API, as well as some of the essential user interactions our team had to consider when building out support for the different text editing options provided by our application.
Our hope is that you will come away with a deeper understanding of how to get started with Draft.js while avoiding our mistakes as you move toward implementing a custom Draft implementation of
As described above, Draft provides several APIs for altering the styles of a given text body. But, before we use those APIs, it’s important to understand the different ways that text styles can be manipulated.
As described on the Draft.js website, EditorState is the top-level editor state object. Draft.js utilizes immutable.js so it’s technically a record which comprises:
- The current text content state
- The current selection state
- The fully decorated representation of the contents
- Undo/redo stacks
- The most recent type of change made to the contents
The EditorState object provides several convenience methods for obtaining and manipulating the desired piece of state, and serves as the main entry point for working with the Draft.js API.
Consider the text below. An inline style applies to a certain range of characters within a block. Examples of inline styles include the bold, italic, underline, font size, and font color properties of the selected text (see selection state). However styles like font size and font color map to a multitude of values where bold and italic can either be on or off. This presents an interesting challenge when using the Draft.js RichUtils.toggleInlineStyle () method, as toggling, by design, is intended to be used with boolean values.
To account for the
It seems intuitive that an inline style would only apply to a selected range of text. Yet, there are also times when a user may want to set a style which should apply to all subsequent text typed into the editor, also known as a collapsed selection. For this case, Draft.js defines the concept of an inline style override.
Block styles, on the other hand, apply to paragraphs (i.e. text separated by a new line character) of text. These styles include text headings and text alignment. These styles should apply to either the currently selected (a collapsed selection) paragraph or to
At first glance, it might seem the two are interchangeable until considering that text headings map to the <h1> – <h6> HTML tags where text alignment affords no similar relationship. Yet, this doesn’t quite matter because the Draft.js Editor component provides the blockRenderMap prop which allows us to map a style type to a CSS class name.
It then becomes trivial to map a style type of center to a .center selector defining a text-align: ‘center’ property. This does break down though when we realize the need to align text headings in addition to other block types.
We cannot simply change the block style type to
The function below provides a glimpse of how we achieved this functionality within our own app:
One might think of an entity as having a combination of both inline and block style properties. Entities correspond to a discrete range of characters like an inline style, but also provide a way to annotate these selections with metadata (e.g. like the data property described above) which can represent features and properties of the given selection. Providing the ability to hyperlink text within your editor is the canonical example of when to use an entity.
Entities are often used in combination with decorators which can scan a block of text for the existence of an entity pattern (e.g. the link type or a matching href RegEx) and then wrap them in a custom component which defines how those entities should appear within the editor.
A simple hyperlink may not conjure the need for such an approach, but what if it were desired to show a tooltip containing the link contents after hovering over a link? Or, what if one wanted to underline the link in red when the provided URL was invalid.
In these cases, the decorator system provided by Draft.js exposes a powerful way to annotate entities within the editor with components that encapsulate the logic for their look and feel.
The Draft.js Modifier module provides a static set of methods for manipulating the content and styles of the current Editor State. The methods return a new content state object which can then be pushed on to the Editor State.
The Modifier module comes in handy when needing to build up a new content state on which to apply a new style. In our case, we use it extensively to remove multi-value styles like font size and font color before applying a new value of the respective style.
See the getResetStateForMultiOptionStyle() method below for an example of how this is done.
Controlling focus is tricky, and the Draft.js team calls this out explicitly in their documentation. This is made increasingly more difficult when trying to combine Draft.js with composite inputs like React-Select or React Color.
The core problem is that controls like these often want to control focus themselves, leading one to enter into a tug-of-war for focus between formatting inputs (i.e. a font size dropdown) and the Draft.js text editor itself.
To circumvent this problem, our team decided to replace React-Select within the text styles toolbar with a custom select component which operates wholly via mouse events. This way our team was able to prevent a loss of focus within the editor, eliminating the need to restore focus in the editor after a style selection is made.
This simplification allowed us to remove several redundant functions necessary for managing inline style overrides due to focus bouncing around. If interested, the simple select control developed by our team can be found within the public SendGrid React UI components repository.
Related to focus is the current selection state. When controls are located outside of the current editor container, it is essential to maintain the user’s text selection as they move to the control panel to adjust a given style.
Because of the challenges with
While the team was able to make this approach work, it proved overly complex. Further, it was not possible to match the native browser selection styles completely. Luckily, replacing the focus-hijacking selects with our own simplified version eliminated the need to apply custom styles trickery to the current text selection.
Multi-value & overlapping styles
Working with overlapping styles is not clearly documented. The following example explains why.
Consider a selected string ‘yay’ with multiple font sizes and colors applied before a user attempts to set the font size for the selection to 50px. Before applying the new font size we need a way to turn off the existing font sizes which have already been applied.
If we take a look at the data structure of the selection below, we can see that every character within the selection can and does have multiple styles. This data structure is a simplified version of how Draft keeps track of the inline styles. Draft blocks contain a list of characters, and each character maps to an ordered set of style strings.
Yet, if we were to use the documented EditorState.getCurrentInlineStyle() for the current selection, we would be surprised and disappointed to discover that only a single font size and font color will be returned, yielding:
So how do we extract the existing styles? Unfortunately, our approach requires looping over the characters in the selection to identify unique styles starting with a prefix (in this case _font_size_).
Once acquired, we must iterate over these styles as well to build up a new Draft.js editor state with the existing styles removed. It is important to note that a selection can span multiple blocks, necessitating iteration over the respective blocks as well.
The two functions our team wrote for setting the style of a selection potentially containing multiple styles of the same type are found below.
Iterating through character styles to find currently applied inline styles
Removing styles with the modifier module
Micro-interactions we had to consider
Computer science circles often joke about how developers tend to all think they can write the best text editor only to find out how difficult such a challenge really is. With Draft.js, creating a text editor for the web is definitely an easier task than writing one from scratch.
That said, there are still many
- When changing the font size, font family, or text color of a selection with one or more previous styles of the same type, the entire selection range should take on the new value of the modified style.
- When an inline style override is set, the override style should be shown in the respective style control input.
- Moving the cursor via mouse or keyboard properly clears an override, if set. This allows you to type in the style of the selection after the cursor is moved.
- Block styles like alignment and text heading persist when splitting a block (i.e. adding a new paragraph).
- Left, center, right, and justified alignment work as expected.
- Font size, font family, text color, and background color default to the global email body styles until set. If a reset button is supported for the respective style, resetting the style will revert it to using the global style.
- Changing text color via the picker delegates focus to the editor after each new color selection. This allows the user to type immediately after changing the color.
- When selecting multiple blocks or a range which contains many different inline styles, the active styles will show a value/selection if the style in question is uniformly applied throughout the selection. Block styles which apply in this case include: alignment and text heading style. Inline styles include: bold, underline, italic, text color, font size, and font family. If multiple styles of the same type exists within a selection, a placeholder will be shown in place of an active value. In the case of text color, the selected value in this case will show white and the word ‘AUTO’ if multiple text color styles exist within the selection.
- No font size is shown if a text heading style other than ‘Normal’ is selected.
- Undo (Cmd + Z) and Redo (Cmd + Shift + Z) should work as expected. When changing a style for a selection that contains multiple styles of the same type, undoing the new change should revert immediately to the selection containing the multiple styles of the same type. There should be no intermittent history where all styles have been removed.
Work yet to be done
The Draft.js API offers you a lot of options. Of course, immediately understanding how to make use of these options is both daunting and fraught with peril. It took our team a while to understand how different style permutations can be wrangled with the Draft API, and a lot of our methods for manipulating the Draft.js Editor State made their way into a utilities.js file.
Future work includes editing and breaking out these utility functions into smaller more
Upon completion of this work, the team plans to publish our custom text editor and toolbar components into the public SendGrid components repository. Please let us know if you or your team would be interested in making use of these components. In the meantime, you can test out our Draft.js implementation within the Email
Sign up for a free SendGrid account and tell us what you think. We look forward to your feedback!