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.
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 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
. ARange
is a logical section of a Mobiledoc document, comprised of a startingPosition
property calledhead
and an endingPosition
property calledtail
. In Mobiledoc aRange
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 therange
property:editor.range
. - The Mobiledoc
Position
primitive is a zero-width spot in a Mobiledoc document, identified by whichSection
of the document it is in, as well as itsoffset
within that section. APosition
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:
- create a logical Range that includes only the URL
- create a markup that is a link to that URL
- use
Editor.run
and thePostEditor
to apply that markup to the range - 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: