On a colorful draft.js

November 17, 2021

Draft.js is an incredibly powerful framework for building rich text editors in React. I’ve had an absolute blast recently writing a rich text editor to suite our customer’s unique requirements - something I never thought I’d say 😂

While Draft.js provides a powerful (and immutable 😍) API to build rich text features, it itself doesn’t ship with many features out of the box. I’ve come to appreciate this design decision though and have consistently found several good solutions to common rich text features like text alignment or font size.

This post is going to be mostly a play-by-play of how I went about implementing a color selector in Draft.js

Block styles vs Inline styles

Similar to HTML, Draft.js has two “levels” at which styles can be applied; block and inline. The editor consists of a set of blocks and applying styles at the block level would mean that the entire heading, paragraph or list item would need to be the same color.

Applying styles inline gives us more flexibility but we need to be careful about how we apply the styles since they are just sets of strings applied to text ranges within blocks.

For instance, given the text “Hello, world” with the text “world” in bold, Draft.js might store the following styling information.

"inlineStyleRanges": [
    {
        "offset": 6,
        "length": 5,
        "style": "BOLD"
    }
],

This might seem daunting to manipulate but lucky for us Draft.js abstracts this away by providing us with a powerful API to toggle styles for the current user selection.

Selecting color

When the user clicks a button to select a color, say #FFD600, we can simply add this as an inline style to the current selection using rich utils.

const nextState = RichUtils.toggleInlineStyle(editorState, "COLOR_#FFD600")

The problem with this is that the user could select the same text and click multiple color buttons. This’ll just add every selected color to the inline style set for the selection instead of replacing the previous one. We don’t have this issue with bold or italic formatting since “boldness” is binary where as there are several colors that could be applied to the same selection.

To deal with the overlapping color issue, we need to first remove all of the existing text colors from the selection before adding the new color. This can be accomplished cleanly by reducing the set of inline styles + the new inline style into the new editor state.

const applyColor = (color: string, editorState: EditorState) => {
  // ['BOLD', 'COLOR_#333', ... ]
  const currentStyles = editorState.getCurrentInlineStyle().toJS()

  // Toggle off any existing colors, toggle on new color
  const nextEditorState = [...currentStyles, color].reduce(
    (state: EditorState, style: string) =>
      style.startsWith("COLOR_")
        ? RichUtils.toggleInlineStyle(state, style)
        : state,
    editorState
  )

  return nextEditorState
}

Rendering color

As with everything in Draft.js, there are a couple ways to apply custom styles to text within the editor. You can either extend the style map or provide a custom style function.

Custom Style Map

The style map is a fairly trivial way to extend the editor styles. If you have a limited number of colors that you’d like your users to pick from, this might be the right approach as any color that’s not recognized will just have the default text color.

const customStyleMap = {
    'COLOR_#FFD600': {
        color: '#FFD600'
    },
    ...
}

Custom Style Function

For our implementation, we opted to use a custom style function that can extract and apply the color from any inline style that begins with the prefix COLOR_. This is because we had a requirement to allow users to paste content from other rich editors and preserve colors coming in even if we didn’t allow them to pick the colors natively.

As with selecting the color, applying it is simply a case of reducing the current inline style into an object with a color property. If the current style does not include any colors, we return an empty object which Draft.js happily accepts and extends with built-in styles.

const customStyleFn = (style: DraftInlineStyle) => {
  const styles = style.toJS() // ['BOLD', 'COLOR_#FF600', ...]

  return styles.reduce(
    (styleMap: Record<string, string>, styleName: string) =>
      styleName.startsWith("COLOR_")
        ? { color: styleName.split("COLOR_")[1] } // Strip the prefix
        : styleMap,
    {}
  )
}

Hope this post helps any future Draft.js adventurers :)


Profile picture

Personal blog of Aquib Master (Keeb)