Rich text editing on the web: Formatting text and keyboard shortcuts in Draft.js
Tags: javascript, react, tutorial, draftjs
Published: 2019-10-24 01:44:56

☀️ Hello! If you are here, know nothing about Draft.js, and you are just jumping in, I recommend you check out [Rich text editing on the web: Getting started with Draft.js](https://dev.to/rose/rich-text-editing-on-the-web-getting-started-with-draft-js-2f68), as we’ll be picking up where we left off 🙂 

- - - -

This post is going to get more of the basics set up so that we can be free to start exploring more fun topics next time.

Here is what we’ll look at adding:

* Adding in all the inline style buttons we didn’t cover in the last post (code, underline, strikethrough)
* Keyboard shortcuts to apply inline styles
* Adding block-level styles (e.g. blockquote, unordered lists)
* Keyboard shortcuts to apply block-level styles
* [You can see the final built-out version of this tutorial here](https://www.rosey.dev/demos/draft-article-2/)

It’s a lot of stuff but we’ll move a little quicker, since a lot of what we’ll be doing builds directly on what we did last time.

🖍 _**small note:** Although this entire tutorial is done in a single file, it could be very easily abstracted out into smaller components, and if you are building this in earnest, I would recommend doing so (ie perhaps StyleButton components and a Toolbar component). For the purpose of the tutorial, I decided to keep everything in one single component in the hopes that it would make things easier to work with for a tutorial instead of having to jump between files._

- - - -

## Adding the rest of the inline styles

As noted in the previous post, Draft.js supports bold,  italic, strikethrough, underline, and code out-of-the-box. We already added bold and italic; I quickly popped in the rest. The logic is the exact same. If you notice the code looks a bit different, it’s because I moved the button rendering into its own method and defined a little array of all the styles, just to hopefully make it a bit cleaner.

So in `App.js` there is a new 	`renderInlineStyle` button, and `render` looks a bit different as well:

```javascript
renderInlineStyleButton(value, style) {
    return (
      <input
        type="button"
        key={style}
        value={value}
        data-style={style}
        onMouseDown={this.toggleInlineStyle}
      />
    );
  }

  render() {
    const inlineStyleButtons = [
      {
        value: 'Bold',
        style: 'BOLD'
      },

      {
        value: 'Italic',
        style: 'ITALIC'
      },

      {
        value: 'Underline',
        style: 'UNDERLINE'
      },

      {
        value: 'Strikethrough',
        style: 'STRIKETHROUGH'
      },

      {
        value: 'Code',
        style: 'CODE'
      }
    ];

    return (
      <div className="my-little-app">
        <h1>Playing with Draft!</h1>
        <div className="inline-style-options">
          Inline Styles:
          {inlineStyleButtons.map((button) => {
            return this.renderInlineStyleButton(button.value, button.style);
          })}
        </div>
        <div className="draft-editor-wrapper">
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange}
          />
        </div>
      </div>
    );
  }
```

Since we already covered this in the last post and this was just a bit of additional housekeeping, I’ll quickly move on to the next item:

## Adding keyboard shortcuts to apply inline styles
`RichUtils`, which is what we used for toggling inline styles with our buttons, also has a method for handling keyboard events 🙌

The  Editor component takes a prop, `handleKeyCommand`. If we define a `handleKeyCommand` and pass it in as a prop, the Editor will call this method whenever it detects the user entering a keyboard command.

From that method, we can get `RichUtils` to do the heavy lifting for us, just like we did last time.

Our `handleKeyCommand` should look like this:

```javascript
handleKeyCommand(command) {
    // inline formatting key commands handles bold, italic, code, underline
    const editorState = RichUtils.handleKeyCommand(this.state.editorState, command);

    if (editorState) {
      this.setState({editorState});
      return 'handled';
    }

    return 'not-handled';
  }
```

Just like with `toggleInlineStyle`, `RichUtils.handleKeyCommand` returns a new instance of `editorState` that we need to set on our state.

`RichUtils.handleKeyCommand` takes two arguments: The current `editorState`, and the key command entered.

The key commands are not standard JavaScript keyboard events, they are draft-specific “command” strings. There are some that exist out-of-the-box, and you can define your own as well.

If you were to add a `console.log(command)` in this method and then do the keyboard shortcut for `bold` (either cmd-b or ctrl-b depending on your OS), the console would log out the string `bold` . If you hit the `backspace` key, the console would log out `backspace`.

You want to make sure that if you are not doing anything with the key command you return the string `not-handled` . This ensures that if there’s any default Draft.js behaviour associated with that command, Draft.js knows that it’s OK to do its thing. If you return `handled` that will override any default behaviour.

## So this is great but some inline styles don’t have a key command: What about strikethrough?
As you may have noticed from the comment in the code example, there’s no key command defined for strikethrough by default.

Luckily it is very easy to define something custom.

Right now when rendering your editor component it should look something like this:

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

We want to pass in another function, `keyBindingFn` - this is what we can use to define some of our own key commands.

This function, when called, will be passed a keyboard event object. We can check it to see if a certain key is being pressed, and if the key is one that we want to associate with a command, we can return that command value as a string. That string will then make its way to the already defined `handleKeyCommand`

In this case we want to add a shortcut for **strikethrough**.

I’m going to copy Gmail’s pattern and make the shortcut for strikethrough cmd+shift+x (or ctrl+shift+x)

So we’ll detect this set of keys and then return the string `'strikethrough'` if detected.  

We also want to make sure we don’t break all the built-in key commands, so if we _don’t_ detect strikethrough, we want to make sure Draft.js still parses it and detects default commands. We’ll do this by importing a function, `getDefaultKeyBinding` and calling it whenever we don’t detect a strikethrough shortcut.

SO.

Our import from `draft-js` is going to look like this now:

```javascript
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, KeyBindingUtil } from 'draft-js';
```

We’ve added two items here: `getDefaultKeyBinding` which I already mentioned, and `KeyBindingUtil` which provides some handy helper methods that we’ll make use of.

Now we need to write out our key binding function.  It will look like this:

```javascript
function keyBindingFunction(event) {
  if (KeyBindingUtil.hasCommandModifier(event) && event.shiftKey && event.key === 'x') {
    return 'strikethrough';
  }

  return getDefaultKeyBinding(event);
}
```

As I said before, it first checks the event for the cmd-or-ctrl key using `KeyBindingUtil.hasCommandModifier`. Then it checks if the shiftKey is employed, and finally checks if the key itself is the letter `x`. If all 3 cases are `true` then it returns the string `strikethrough`. Otherwise it lets Draft.js parse the event using `getDefaultKeyBinding` and returns that value.

Now we can also pass this function into our editor -

```javascript
<Editor
  editorState={this.state.editorState}
  onChange={this.onChange}
  handleKeyCommand={this.handleKeyCommand}
  keyBindingFn={keyBindingFunction}
/>
```

The final piece to get the strikethrough shortcut working is to update our `handleKeyCommand` function to check for our custom `strikethrough` command, and then tell it what to do if it encounters this command.

Our updated `handleKeyCommand` looks like this:

```javascript
handleKeyCommand(command) {
    // inline formatting key commands handles bold, italic, code, underline
    var editorState = RichUtils.handleKeyCommand(this.state.editorState, command);
    
    // If RichUtils.handleKeyCommand didn't find anything, check for our custom strikethrough command and call `RichUtils.toggleInlineStyle` if we find it.
    if (!editorState && command === 'strikethrough') {
      editorState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH');
    }

    if (editorState) {
      this.setState({editorState});
      return 'handled';
    }

    return 'not-handled';
  }
```

And that’s that. We now have keyboard shortcuts defined for all our inline styles 👍

## Moving on to the block level styles, like block quote and ordered list
Adding in support for block level styles is very similar to what we just did for inline styles. `RichUtils` has a `toggleBlockType` method that takes `editorState` as its first argument and a string representing a block type as the second argument. It returns a new instance of `editorState`. So as you can see, _very_ similar to how `toggleInlineStyle` works.

#### Default block types supported are:
* header-one
* header-two
* header-three
* header-four
* header-five
* header-six
* blockquote
* code-block
* atomic
* unordered-list-item
* ordered-list-item

So, for example, if we wanted to toggle a blockquote, we’d do something like:

```javascript
const editorState = RichUtils.toggleBlockType(this.state.editorState, 'blockquote');
this.setState({editorState});
```

Since the logic here is so similar to the inline style buttons, instead of showing all the individual steps taken to add this in, I’m instead going to provide you with what my App.js now looks like as a whole, including the new block-level buttons:

```javascript
import React from 'react';
import './App.css';
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, KeyBindingUtil } from 'draft-js';

function keyBindingFunction(event) {
  if (KeyBindingUtil.hasCommandModifier(event) && event.shiftKey && event.key === 'x') {
    return 'strikethrough';
  }

  return getDefaultKeyBinding(event);
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState: EditorState.createEmpty()
    };

    this.onChange = this.onChange.bind(this);
    this.handleKeyCommand = this.handleKeyCommand.bind(this);
    this.toggleInlineStyle = this.toggleInlineStyle.bind(this);
    this.toggleBlockType = this.toggleBlockType.bind(this);
  }

  onChange (editorState) {
    this.setState({editorState});
  }

  handleKeyCommand(command) {
    // inline formatting key commands handles bold, italic, code, underline
    var editorState = RichUtils.handleKeyCommand(this.state.editorState, command);

    if (!editorState && command === 'strikethrough') {
      editorState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH');
    }

    if (editorState) {
      this.setState({editorState});
      return 'handled';
    }

    return 'not-handled';
  }

  toggleInlineStyle (event) {
    event.preventDefault();

    let style = event.currentTarget.getAttribute('data-style');
    this.setState({
      editorState: RichUtils.toggleInlineStyle(this.state.editorState, style)
    });
  }

  toggleBlockType (event) {
    event.preventDefault();

    let block = event.currentTarget.getAttribute('data-block');
    this.setState({
      editorState: RichUtils.toggleBlockType(this.state.editorState, block)
    });
  }

  renderBlockButton(value, block) {
    return (
      <input
        type="button"
        key={block}
        value={value}
        data-block={block}
        onMouseDown={this.toggleBlockType}
      />
    );
  }

  renderInlineStyleButton(value, style) {
    return (
      <input
        type="button"
        key={style}
        value={value}
        data-style={style}
        onMouseDown={this.toggleInlineStyle}
      />
    );
  }

  render() {
    const inlineStyleButtons = [
      {
        value: 'Bold',
        style: 'BOLD'
      },

      {
        value: 'Italic',
        style: 'ITALIC'
      },

      {
        value: 'Underline',
        style: 'UNDERLINE'
      },

      {
        value: 'Strikethrough',
        style: 'STRIKETHROUGH'
      },

      {
        value: 'Code',
        style: 'CODE'
      }
    ];

    const blockTypeButtons = [
      {
        value: 'Heading One',
        block: 'header-one'
      },

      {
        value: 'Heading Two',
        block: 'header-two'
      },

      {
        value: 'Heading Three',
        block: 'header-three'
      },

      {
        value: 'Blockquote',
        block: 'blockquote'
      },

      {
        value: 'Unordered List',
        block: 'unordered-list-item'
      },

      {
        value: 'Ordered List',
        block: 'ordered-list-item'
      }
    ];

    return (
      <div className="my-little-app">
        <h1>Playing with Draft!</h1>
        <div className="inline-style-options">
          Inline Styles:
          {inlineStyleButtons.map((button) => {
            return this.renderInlineStyleButton(button.value, button.style);
          })}
        </div>

        <div className="block-style-options">
          Block Types:
          {blockTypeButtons.map((button) => {
            return this.renderBlockButton(button.value, button.block);
          })}
        </div>
        <div className="draft-editor-wrapper">
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange}
            handleKeyCommand={this.handleKeyCommand}
            keyBindingFn={keyBindingFunction}
          />
        </div>
      </div>
    );
  }
}

export default App;
```

Wow, this post is getting long! Better wrap up quick 🙂 

The final TODO is adding in more custom keyboard shortcuts for these block level items. `RichUtils` doesn’t have anything built in, so we need to do the same thing as what we did for `strikethrough`. Again, I’ll copy the shortcuts that Gmail uses for numbered list, bulleted list, and blockquote. Maybe you can add in your own shortcuts for headings as a project of your own!

Here is the final code that we’re wrapping up with, which includes these new shortcuts:

```javascript
import React from 'react';
import './App.css';
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, KeyBindingUtil } from 'draft-js';

function keyBindingFunction(event) {
  if (KeyBindingUtil.hasCommandModifier(event) && event.shiftKey && event.key === 'x') {
    return 'strikethrough';
  }

  if (KeyBindingUtil.hasCommandModifier(event) && event.shiftKey && event.key === '7') {
    return 'ordered-list';
  }

  if (KeyBindingUtil.hasCommandModifier(event) && event.shiftKey && event.key === '8') {
    return 'unordered-list';
  }

  if (KeyBindingUtil.hasCommandModifier(event) && event.shiftKey && event.key === '9') {
    return 'blockquote';
  }

  return getDefaultKeyBinding(event);
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState: EditorState.createEmpty()
    };

    this.onChange = this.onChange.bind(this);
    this.handleKeyCommand = this.handleKeyCommand.bind(this);
    this.toggleInlineStyle = this.toggleInlineStyle.bind(this);
    this.toggleBlockType = this.toggleBlockType.bind(this);
  }

  onChange (editorState) {
    this.setState({editorState});
  }

  handleKeyCommand(command) {
    // inline formatting key commands handles bold, italic, code, underline
    var editorState = RichUtils.handleKeyCommand(this.state.editorState, command);

    if (!editorState && command === 'strikethrough') {
      editorState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH');
    }

    if (!editorState && command === 'blockquote') {
      editorState = RichUtils.toggleBlockType(this.state.editorState, 'blockquote');
    }

    if (!editorState && command === 'ordered-list') {
      editorState = RichUtils.toggleBlockType(this.state.editorState, 'ordered-list-item');
    }

    if (!editorState && command === 'unordered-list') {
      editorState = RichUtils.toggleBlockType(this.state.editorState, 'unordered-list-item');
    }

    if (editorState) {
      this.setState({editorState});
      return 'handled';
    }

    return 'not-handled';
  }

  toggleInlineStyle (event) {
    event.preventDefault();

    let style = event.currentTarget.getAttribute('data-style');
    this.setState({
      editorState: RichUtils.toggleInlineStyle(this.state.editorState, style)
    });
  }

  toggleBlockType (event) {
    event.preventDefault();

    let block = event.currentTarget.getAttribute('data-block');
    this.setState({
      editorState: RichUtils.toggleBlockType(this.state.editorState, block)
    });
  }

  renderBlockButton(value, block) {
    return (
      <input
        type="button"
        key={block}
        value={value}
        data-block={block}
        onMouseDown={this.toggleBlockType}
      />
    );
  }

  renderInlineStyleButton(value, style) {
    return (
      <input
        type="button"
        key={style}
        value={value}
        data-style={style}
        onMouseDown={this.toggleInlineStyle}
      />
    );
  }

  render() {
    const inlineStyleButtons = [
      {
        value: 'Bold',
        style: 'BOLD'
      },

      {
        value: 'Italic',
        style: 'ITALIC'
      },

      {
        value: 'Underline',
        style: 'UNDERLINE'
      },

      {
        value: 'Strikethrough',
        style: 'STRIKETHROUGH'
      },

      {
        value: 'Code',
        style: 'CODE'
      }
    ];

    const blockTypeButtons = [
      {
        value: 'Heading One',
        block: 'header-one'
      },

      {
        value: 'Heading Two',
        block: 'header-two'
      },

      {
        value: 'Heading Three',
        block: 'header-three'
      },

      {
        value: 'Blockquote',
        block: 'blockquote'
      },

      {
        value: 'Unordered List',
        block: 'unordered-list-item'
      },

      {
        value: 'Ordered List',
        block: 'ordered-list-item'
      }
    ];

    return (
      <div className="my-little-app">
        <h1>Playing with Draft!</h1>
        <div className="inline-style-options">
          Inline Styles:
          {inlineStyleButtons.map((button) => {
            return this.renderInlineStyleButton(button.value, button.style);
          })}
        </div>

        <div className="block-style-options">
          Block Types:
          {blockTypeButtons.map((button) => {
            return this.renderBlockButton(button.value, button.block);
          })}
        </div>
        <div className="draft-editor-wrapper">
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange}
            handleKeyCommand={this.handleKeyCommand}
            keyBindingFn={keyBindingFunction}
          />
        </div>
      </div>
    );
  }
}

export default App;
```

And that’s it 👏

You now have a reasonably functioning little visual editor. You could pretty easily make this quite nice with some CSS to style things a little nicer than the ugly defaults we have in this demo, and you could also abstract out/tidy up some of the javascript if you so desire.

There’s still more to learn but you have the framework in place.  I’m excited to move on to some more advanced topics next time!

Thanks so much for reading ❤️ If you're finding this helpful, OR if you are finding it too confusing and feel I should slow down or take more time to explain certain things, let me know in the comments. 🧸

[You can see the final built-out version of this tutorial here](https://www.rosey.dev/demos/draft-article-2/)