Building a Rich Text Editor with React and Draft.js, Pt. 2.2: Embedding Links

Siobhan Mahoney

This post is part of the second installment of a multi-part series about building a Rich Text Editor with React and Draft.js. For an introduction to the Draft.js framework with setup instructions, checkout Building a Rich Text Editor with Draft.js, Part 1: Basic Set Up. Links to additional posts in this series can be found below. The full codebase for my ongoing Draft.js project is available on GitHub. Feel free to checkout the deployed demo here.

In this post, I will review how to build both a key command and button to embed hyperlinks within your text editor using tools provided by the Draft.js API and out-of-the-box plugins offered by DraftJS Plugins.

DraftJS Plugins can be installed with:

yarn add draft-js-plugins-editor

For reference, I’ll be integrating these features into the React basic text editor I built for the demo discussed in the first post in this series, Building a Rich Text Editor with Draft.js, Part 1: Basic Set Up, the code for which is available on CodeSandbox.


I. Draft.js API Quick Reference

We’ll follow a similar process for creating a Hyperlink plugin. Before we do, however, let’s review the building blocks from the Draft.js API we’ll be working with:

ContentState

As discussed in the first post of this series, the EditorState is a single immutable object that represents a complete snapshot of the state of the editor. The EditorState consists of a stack of ContentState objects, each of which represents a subsequent change in EditorState.

Each instance ofContentState consists of a snapshot of the EditorState and methods for accessing the various pieces that make up the EditorState. It is an immutable record that represents:

  • The full contents of an editor, text, blocks, inline styles, and entity ranges.
  • Two editor selection states, beforeSelection and afterSelection, that represent the selection states before and after the rendering of contents

ContentBlock

The contents of ContentState are structured as an OrderedMap of ContentBlocks, immutable record that represents the full state of a single block of editor content, including:

  • Plain text contents
  • Type, such as paragraph, header, list item
  • Entity, inline style, and depth information

New ContentBlock objects may be created directly using the constructor.

Entity

An entity is a range of text with given set of metadata. The entity API allows for creating, editing, and retrieving entity objects by annotating ranges of text with metadata.

Entity properties include:

  • decorator: an object with keys strategy, which points to a function that iterates over the characters within a block and finds continuous ranges of text with the given entity, and component, which identifies the React component rendering the entity’s character range.
  • mutability: defines whether the text can be changed or not without having an impact on the entity itself via one of 3 values: MUTABLE, IMMUTABLE, SEGMENT (see the Draft.js documentation for more details on these options)

II. Building Link Key Command

To begin, create a plugins directory to house custom plugin modules, and create file titled addLinkPlugin.js and import the following:

import React from 'react';
import {
RichUtils,
KeyBindingUtil,
EditorState,
} from 'draft-js'

Next, define the linkStrategy by calling the findEntityRanges callback on contentBlock, will find the continuous range of characters with which to associate the LINK entity:

export const linkStrategy = (contentBlock, callback, contentState) => {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
};

We’ll then create a Link component, which renders an anchor tag with the url as identified in the entity data:

export const Link = (props) => {
const { contentState, entityKey } = props;
const { url } = contentState.getEntity(entityKey).getData();
return (
<a
className="link"
href={url}
rel="noopener noreferrer"
target="_blank"
aria-label={url}
>{props.children}</a>
);
};

Next, we’ll create the actual plugin.

Similar to the highlight plugin created in my previous tutorial, we’ll create keyBindingFn that returns 'add-link' if characters are selected and the key combination matches CMD (Mac) / CTRL (windows) + K using Draft.js’ utility object, KeyBindingUtil with method hasCommandModifier:

const addLinkPluginPlugin = {
keyBindingFn(event, { getEditorState }) {
const editorState = getEditorState()
const selection = editorState.getSelection();
if (selection.isCollapsed()) {
return;
}
if (KeyBindingUtil.hasCommandModifier(event) && event.which === 75) {
return 'add-link'
}
}
}

Next, create method handleKeyCommand, which, after checking if the command return is 'add-link' it will:

  • Prompt user to enter url with window.prompt
  • Applies that url as the LINK entity to the selected text by using content.createEntity() to create new content data using entity type LINK, 'MUTABLE' mutability, and the entity data passed in as arguments
  • If URL text is not entered, any existing LINK entity will be removed from selected characters
  • Create newEditorState by pushing contentWithEntity and the command 'create-entity' to the current EditorState
  • Set entity to the selected range of characters using RichUtils.toggleLink
  • Define decorators, an array of objects defining relevant strategy and component with linkStrategy and Link, respectively.
handleKeyCommand(command, editorState, { getEditorState, setEditorState}) {
if (command !== 'add-link') {
return 'not-handled';
}
let link = window.prompt('Paste the link -');
const selection = editorState.getSelection();
if (!link) {
setEditorState(RichUtils.toggleLink(editorState, selection, null));
return 'handled';
}
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity('LINK', 'MUTABLE', { url: link });
const newEditorState = EditorState.push(editorState, contentWithEntity, 'create-entity');
const entityKey = contentWithEntity.getLastCreatedEntityKey();
setEditorState(RichUtils.toggleLink(newEditorState, selection, entityKey))
return 'handled';
},
decorators: [{
strategy: linkStrategy,
component: Link,
}],
};

The end result will be the following:

Now that the plugin has been created, let’s import it into our PageContainer component:

import React from "react"
import { EditorState, RichUtils } from "draft-js"
import Editor from "draft-js-plugins-editor"
import addLinkPlugin from './plugins/addLinkPlugin'

Update the PageContainer constructor method to define this.plugins and add addLinkPlugin:

Next, update PageContainer's constructor method by creating athis.plugins array, which will hold addLinkPlugin:

class PageContainer extends React.Component {
constructor(props) {
super(props)
this.state = {
editorState: EditorState.createEmpty(),
}
this.plugins = [
addLinkPlugin,
];
}

Finally, update the Editor component by passing it plugins as a prop:

<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={this.plugins}
/>

And your new command is up and running!

III. Building Link UI Button

The process for building a button for embedding links is similar to building the key command.

First, we’ll add the onAddLink() function to PageContainer , which will be passed as a callback function to the button’s onClick event handler:

onAddLink = () => {
const editorState = this.state.editorState;
const selection = editorState.getSelection();
const link = window.prompt('Paste the link -')
if (!link) {
this.onChange(RichUtils.toggleLink(editorState, selection, null));
return 'handled';
}
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity('LINK', 'MUTABLE', { url: link });
const newEditorState = EditorState.push(editorState, contentWithEntity, 'create-entity');
const entityKey = contentWithEntity.getLastCreatedEntityKey();
this.onChange(RichUtils.toggleLink(newEditorState, selection, entityKey))
}
  • Prompt user to enter url with window.prompt
  • Applies that url as the LINK entity (which we defined in addLinkPlugin.js) to the selected text by using content.createEntity() to create new content data using entity type LINK, 'MUTABLE' mutability, and the entity data passed in as arguments
  • If URL text is not entered, any existing LINK entity will be removed from selected characters
  • Create newEditorState by pushing contentWithEntity and the command 'create-entity' to the current EditorState
  • Set entity to the selected range of characters using RichUtils.toggleLink

And, there you go! An editor that embeds links via both a key command and button:


Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade