Use Mobiledoc-Kit to build an auto-linking text editor

Mobiledoc-Kit is a library for building WYSIWYG editors supporting rich content via cards. It was developed by our team at 201 Created for the fine folks at Bustle. To learn more about it, visit Mobiledoc-Kit on github or say hello to our team.

In this post I’ll showcase one of the useful editing hooks that the Mobiledoc API offers: onTextInput. I’ll create a text editor that automatically links URLs as you type.

The finished product: A Mobiledoc editor that aut0-links as you type.

onTextInput is a hook that you can configure on an instance of a Mobiledoc Editor. It requires 3 things: a name, a property to match on which can either be a regular expression(match) or a string (text), and a callback function (run)that Mobiledoc will invoke when the user types some matching text.

The docs for `onTextInput`. = available online.

The callback function, run, gets called with 2 arguments: an instance of the editor and an array that contains the matched segment(s). For instance, here’s how you would configure an editor that quacks every time the word “duck” is typed. (Note that in this and future code snippets I assume an instance of an editor is already available in the editor variable.)

editor.onTextInput({
name: 'quacker',
text: 'duck',
run(editor, matches) { alert('Quack!'); }
});

Not very useful, but helps illustrate the point. You can try it live here.

Here is a regex that will match some text that starts (after a word boundary) with http or https and ends with a space:

/\b(https?:\/\/[^\s]+)\s$/

Take it for a whirl on this regex explorer if you’d like to confirm for yourself that it’ll match.

Here’s how we would register a text input handler that will respond when the user types such a URL:

editor.onTextInput({
name: 'linkify',
match: /\b(https?:\/\/[^\s]+)\s$/,
run(editor, matches) {
// linkify the matched url
}
});

If you add a log statement inside that run callback you should see that it fires at the expected time.

In order to link up the text there are few more Mobiledoc concepts to learn:

  • The Mobiledoc Range. A Range is a logical section of a Mobiledoc document, comprised of a starting Position property called head and an ending Position property called tail. In Mobiledoc a Range can be collapsed, in which case its head will be the same as its tail, or expanded. A Mobiledoc editor instance can have 0 or 1 currently active range at any given time, and it can be accessed through the range property: editor.range.
  • The Mobiledoc Position primitive is a zero-width spot in a Mobiledoc document, identified by which Section of the document it is in, as well as its offset within that section. A Position can be thought of as lying between two characters in a given section (similar to the native text cursor).

The Mobiledoc documentation includes more information on the methods that Mobiledoc.Range provides. For this tutorial we’ll use the move method, which will shift a range to the left or right, and extend, which changes the width of the range by moving its head or tail (depending on the direction of extension).

In order to modify the existing document, Mobiledoc exposes an editor.run method that you can use to apply a set of mutations. Pass a function toeditor.run that it will then call with an instance of a PostEditor — a PostEditor exposes methods that can be used to modify the document, e.g. inserting or deleting text, and modifying the markup applied to a given range. The method we will use in this tutorial is addMarkupToRange, which introduces another Mobiledoc concept, Markup. A Markup is a decoration that can be applied to text, such as to make it strong, emphasized, or, in our case, to make it a link.

To create a markup, use the builder that the editor exposes. For instance, to create a markup that is a link to this post, do:

let linkMarkup = editor.builder.createMarkup(
'a', {href: 'https://medium.com/p/771bdb0b8709'}
);

The last thing to know is that the editor exposes a method selectRange that can be used to set the cursor position.

We can now start putting these pieces together to create an editor that auto-links URLs. Inside the run callback we will need to:

  1. create a logical Range that includes only the URL
  2. create a markup that is a link to that URL
  3. use Editor.run and the PostEditor to apply that markup to the range
  4. Restore the cursor position

Here’s a code snippet that puts the pieces together:

editor.onTextInput({
name: 'linkify',
match: /\b(https?:\/\/[^\s]+)\s$/,
run(editor, matches) {
let url = matches[0];
    // the editor's current range is the cursor position, 1
// character after the URL (to account for the space).
// Keep a reference to this range so that it can be restored later
let currentRange = editor.range;
    // 1. Create a logical range containing the URL.
// Move the range 1 space backward (to skip over the space), and
// then extend it backward the length of the URL:
let linkRange = currentRange.move(-1).extend(-url.length);
    // 2. Create the markup
let linkMarkup = editor.builder.createMarkup('a', {href: url});
    // 3. apply the markup to the range
editor.run((postEditor) => {
postEditor.addMarkupToRange(linkRange, linkMarkup);
});
    // 4. restore the cursor position
editor.selectRange(currentRange);
}
});

And here is an embedded code pen. Feel free to click in to type and try it out for yourself!

I hope this gives you a sense of the power of Mobiledoc, as well as whets your appetite to give it a try. In this tutorial I’ve elided some setup steps. The way you’ll need to import Mobiledoc for use in your application will depend on your build setup and other tooling. Mobiledoc ships in several flavors including global, AMD, commonjs, and ES2015+, as well as has wrappers available for frameworks including React, Ember and others. Find out more at the github repo. In these Codepens I’ve used unpkg to include the global JS as well as the CSS.

It’s obvious but also worth mentioning that the onTextInput hook doesn’t necessarily have to modify the editor’s document, it can instead be used as a way to allow the rest of the editing environment to react to the text that is being typed. For example, you could have an editor that has a side panel (not managed by Mobiledoc) that would automatically look up definitions of words as the user typed them. Here’s a sillier demo that changes the page’s background color in reaction to what the user types: