Repository: wagtail/wagtail

Issue:
Feature request: add support for more customisations of Draftail
Previously at #4315 / #4436. Related: #4445, #4223.

At the moment, rich text extensions can only customise the following parts of Draftail:

- Blocks
- Inline styles
- Entities (e.g. links, images, embeds)
- Horizontal rules being enabled or not

Here are things that are not customisable and would be very valuable to make customisable:

```js
// Displayed when the editor is empty. Hidden if the user changes styling.
placeholder: "Write here",
// Show undo control in the toolbar.
showUndoControl: true
// Show redo control in the toolbar.
showRedoControl: true,
// List of active decorators.
decorators: [],
// Additional React components to render in the toolbar.
controls: [],
// List of plugins of the draft-js-plugins architecture.
plugins: [],
// Max level of nesting for list items. 0 = no nesting. Maximum = 10.
maxListNesting: 4,
```

And things that I think would be valuable for _some_ extensions, but probably not that valuable to make customisable just yet, except to allow for experimentation:

```
// Called when the editor receives focus.
onFocus: null,
// Called when the editor loses focus.
onBlur: null,
// Optionally override the default Draftail toolbar, removing or replacing it.
topToolbar: Toolbar,
// Optionally add a custom toolbar underneath the editor, e.g. for metrics.
bottomToolbar: null,
```

---

Additionally, for those customisations that would be done with Wagtail’s rich text features, I think it’ll be important that a single rich text feature can map to multiple Draftail extensions – for example, a `code-block` rich text feature could configure both a block type for code (as is supported already) and a decorator that would do syntax highlighting. I think this might be possible already in the rich text features API, but the Draftail integration of this might not support it.

## Implementation

I think the options described above fall in different categories:

- Things that map well to "rich text features" or "rich text formatting" because they determine the formatting available in the editor in a widget-agnostic way. For example an "emoji" feature that maps to a plugin and a control, and we would likely want to enable either site-wide, or per-field
- Things that would ideally map to field or widget attributes, because they aren’t widget-agnostic. For example providing a custom placeholder string for some fields, or hiding the undo/redo controls site-wide.
- It might also make sense to have customisation APIs that are client-side only – for more advanced customisations that we might not want to have APIs for, but where it would still be valuable to reuse existing code.

As a way to assess API designs, here are types of editor extensions that I would like those new APIs to support:

- Indent/dedent buttons. This is the only operation currently doable with the keyboard only, thus unusable on touch screens with virtual keyboards that generally don’t have a "tab" key.
- Paste from Word button – content import features 
- Clear formatting button – Resets the editor to plain-text
- Character replacements – smart quotes, convert `-->` to `→` like Google Docs
- Slash-commands – interact with the editor, and Wagtail, by typing CLI-like commands with autocomplete
- Emojis – emoji picker, copy-paste support
- Content linting – validation rules, highlighted words
- Content metrics – live-updating measures of the content’s readability, SEO-keywords-ratio, sentiment analysis.
- Single-line input – the editor constrained to a single block, with no block-level formats in the toolbar. Pasting causes the content to be added in a single line.
Feature request: add support for more customisations of Draftail
Previously at #4315 / #4436. Related: #4445, #4223.

At the moment, rich text extensions can only customise the following parts of Draftail:

- Blocks
- Inline styles
- Entities (e.g. links, images, embeds)
- Horizontal rules being enabled or not

Here are things that are not customisable and would be very valuable to make customisable:

```js
// Displayed when the editor is empty. Hidden if the user changes styling.
placeholder: "Write here",
// Show undo control in the toolbar.
showUndoControl: true
// Show redo control in the toolbar.
showRedoControl: true,
// List of active decorators.
decorators: [],
// Additional React components to render in the toolbar.
controls: [],
// List of plugins of the draft-js-plugins architecture.
plugins: [],
// Max level of nesting for list items. 0 = no nesting. Maximum = 10.
maxListNesting: 4,
```

And things that I think would be valuable for _some_ extensions, but probably not that valuable to make customisable just yet, except to allow for experimentation:

```
// Called when the editor receives focus.
onFocus: null,
// Called when the editor loses focus.
onBlur: null,
// Optionally override the default Draftail toolbar, removing or replacing it.
topToolbar: Toolbar,
// Optionally add a custom toolbar underneath the editor, e.g. for metrics.
bottomToolbar: null,
```

---

Additionally, for those customisations that would be done with Wagtail’s rich text features, I think it’ll be important that a single rich text feature can map to multiple Draftail extensions – for example, a `code-block` rich text feature could configure both a block type for code (as is supported already) and a decorator that would do syntax highlighting. I think this might be possible already in the rich text features API, but the Draftail integration of this might not support it.

## Implementation

I think the options described above fall in different categories:

- Things that map well to "rich text features" or "rich text formatting" because they determine the formatting available in the editor in a widget-agnostic way. For example an "emoji" feature that maps to a plugin and a control, and we would likely want to enable either site-wide, or per-field
- Things that would ideally map to field or widget attributes, because they aren’t widget-agnostic. For example providing a custom placeholder string for some fields, or hiding the undo/redo controls site-wide.
- It might also make sense to have customisation APIs that are client-side only – for more advanced customisations that we might not want to have APIs for, but where it would still be valuable to reuse existing code.

As a way to assess API designs, here are types of editor extensions that I would like those new APIs to support:

- Indent/dedent buttons. This is the only operation currently doable with the keyboard only, thus unusable on touch screens with virtual keyboards that generally don’t have a "tab" key.
- Paste from Word button – content import features 
- Clear formatting button – Resets the editor to plain-text
- Character replacements – smart quotes, convert `-->` to `→` like Google Docs
- Slash-commands – interact with the editor, and Wagtail, by typing CLI-like commands with autocomplete
- Emojis – emoji picker, copy-paste support
- Content linting – validation rules, highlighted words
- Content metrics – live-updating measures of the content’s readability, SEO-keywords-ratio, sentiment analysis.
- Single-line input – the editor constrained to a single block, with no block-level formats in the toolbar. Pasting causes the content to be added in a single line.


Discussion:
For anyone looking into this, the temporary workaround for this is to monkey patch / override Draftail’s `initEditor` to implement your own API to register further customisations of Draftail. 

Here is a good minimal example from @v9Chris: https://github.com/vixdigital/wagtail-readinglevel/blob/master/wagtailreadinglevel/static/api-monkeypatch.js

```js
// Implement the new APIs.

const DECORATORS = [];
const CONTROLS = [];

const registerDecorator = (decorator) => {
    DECORATORS.push(decorator);
    return DECORATORS;
};

const registerControl = (control) => {
    CONTROLS.push(control);
    return CONTROLS;
};

// Override the existing initEditor to hook the new APIs into it.
// This works in Wagtail 2.0 but will definitely break in a future release.
const initEditor = window.draftail.initEditor;

const initEditorOverride = (selector, options, currentScript) => {
    const overrides = {
        decorators: DECORATORS,
        controls: CONTROLS,
    };

    const newOptions = Object.assign({}, options, overrides);

    return initEditor(selector, newOptions, currentScript);
};

window.draftail.registerControl = registerControl;
window.draftail.registerDecorator = registerDecorator;
window.draftail.initEditor = initEditorOverride;
```

This is obviously not a documented / official API, so if you’re in need of this please comment on this issue to make sure we’re aware of your use case. **At some stage this implementation-specific workaround will likely go away, and we likely won’t provide any migration path or breaking change notice for this** – except for a heads-up in this issue.
I’m interested in this and have used the same approach as above to inject some functionality into the draftail instances. In my case I wanted to replace ASCII style quote-characters with [guillemets](https://en.wikipedia.org/wiki/Guillemet), which can be solved by a relatively simple draft-js plugin. Unfortunately there is no way to add draft-js plugins through the Wagtail API.
Thanks @kmohrf, that’s good to know. Do you have any code to share for reference? There aren’t that many people publicly talking about customizations like this, so any real-world implementation is appreciated.

For this type of character replacement – it’s something I would like to support out of the box eventually (https://github.com/springload/draftail/issues/113). As you say though, it’s also desirable to be able to add with draft-js-plugins.
I’ve published the code [as a gist](https://gist.github.com/kmohrf/b0e070b1f3e2e32f7577ed1bf3bfa863) for reference :)
Hi, any update on this?
I would love to have an "add emoji" button!
It looks like (as predicted), the monkeypatch suggested by @thibaudcolas [above](#issuecomment-574693649) may no longer be working with current versions of wagtail, as per this issue from the wagtail-readinglevel package (that the above example was taken from).

https://github.com/vixdigital/wagtail-readinglevel/issues/28


This example from @alxbridge updates the approach for more recent wagtail versions
https://github.com/vixdigital/wagtail-readinglevel/pull/32
Clever! This approach seems to rely on defining those extensions as entities, whose JS files will then be loaded as configured.  This seems to then register an entity server-side, and a `control` client-side using the API monkeypatch. I was surprised this doesn’t result in two buttons in the editor toolbar, but I guess this might be working since [Draftail v1.4.0](https://github.com/springload/draftail/blob/main/CHANGELOG.md#v140) ("Fix empty buttons appearing when providing custom formats without a defined label or icon"). I believe we’re only using that release in Wagtail starting with Wagtail v2.13 – in older versions, this would result in two buttons in the toolbar, one of which would either break the editor when clicked, or do nothing.

So – seems pretty good to me, with the caveat that this is again relying on implementation details of existing APIs rather than something we intend to support.
[Sharing more info of our use case](https://wagtailcms.slack.com/archives/C80KTJKKM/p1644313904861229) of Draftail customisation.

One of these is using a slightly modified version of the word count:

- Our contents are dual language (Chinese and English), and in Chinese, one characters equals to one word (so we need to extend the default counting via space between words)
- In addition to that we also have Total Word counts! (Excluding some fields that we don't need to count) We placed it near the save button below, neat placement I would say. This was requested by our editors, since we need the total word counts for freelance writers.

![image](https://user-images.githubusercontent.com/47908628/158938469-bae0fd19-9a9c-4374-acae-add4ce1531c1.png)

---

Oh, and also, we managed to do a quote sign replacement! (from `''` and `""`,  to `‘’` and `“”`).  
_Not included in this example to keep the sharing simple for now_

I was hoping we would get to this as part of the [Page editor 2022](https://github.com/wagtail/wagtail/discussions/7739) project but this is looking very unlikely, so I thought I’d add notes here of what this would look like if implemented properly in Wagtail core. The implementation shouldn’t be that complex, it’s just a matter of changing rich text features to affect more options of the editor.

Here are the kinds of changes I would expect, if someone wants to give this a go.

## Server-side

There’s really not that much needed. In [`wagtail/admin/rich_text/editors/draftail/features.py`](https://github.com/wagtail/wagtail/blob/main/wagtail/admin/rich_text/editors/draftail/features.py#L62), add:

```python
class DecoratorFeature(ListFeature):
    """A feature which is listed in the decorators list of the options"""

    option_name = "decorators"


class ControlFeature(ListFeature):
    """A feature which is listed in the controls list of the options"""

    option_name = "controls"


class PluginFeature(ListFeature):
    """A feature which is listed in the plugins list of the options"""

    option_name = "plugins"
```

Then implementers can declare features in `wagtail_hooks.py` like:

```python
type_ = "reading-level-smog"
features.register_editor_plugin(
        "draftail",
        type_,
        draftail_features.ControlFeature(
            {
                "type": type_,
            },
            js=['reading-level-smog.js']
        ),
    )
```

I can’t think why we’d want to do much more than this. Perhaps allow a feature to register multiple things – for example a control and a decorator at the same time. That wouldn’t be much more work than the above though (10 lines of code?). And could be something people add themselves outside of core if they need it. For example, here is a feature that would register multiple things:

```python
class MyFancyFeature(Feature):
    def construct_options(self, options):
        if "controls" not in options:
            options["controls"] = []
        options["controls"].append({ "type": "my-fancy-control" })
        if "decorators" not in options:
            options["decorators"] = []
        options["decorators"].append({ "type": "my-fancy-decorator" })

features.register_editor_plugin(
        "draftail",
        "fancy",
        MyFancyFeature(),
    )
```


## Client-side

We’d need to change [`client/src/components/Draftail/index.js`](https://github.com/wagtail/wagtail/blob/main/client/src/components/Draftail/index.js#L106) so the client-side implementation of features can be registered for more than entities:

```js
/**
 * Registries for client-side code of Draftail plugins.
 */
const ENTITIES = {};
const DECORATORS = {};
const CONTROLS = {};

const registerEntity = (type) => {
  ENTITIES[type.type] = type;
  return ENTITIES;
};
const registerDecorators = (type) => {
  DECORATORS[type.type] = type;
  return DECORATORS;
};
const registerControls = (type) => {
  CONTROLS[type.type] = type;
  return CONTROLS;
};
// For backwards compatibility.
const registerPlugin = registerEntity;
```

And then use those registries:

```diff
-const controls = newOptions.controls || [];
+let controls = newOptions.controls || [];
let entityTypes = newOptions.entityTypes || [];

entityTypes = entityTypes.map(wrapWagtailIcon).map((type) => {
-    const plugin = PLUGINS[type.type];
+    const registeredType = ENTITIES[type.type];

    // Override the properties defined in the JS plugin: Python should be the source of truth.
-    return { ...plugin, ...type };
+    return { ...registeredType, ...type };
});

+controls = controls.map((type) => ({ ...CONTROLS[type.type], ...type }));
```

And then do the same thing for `decorators` and `plugins`. Usage in JS file like `reading-level-smog.js` from the above example would be like this:

```js
const SmogIndex = ({ getEditorState }) => {
    const text = getEditorState().getCurrentContent().getPlainText();
    const stats = CalculateSmogIndex(text);

    return `SMOG Index: ${stats ? stats.smogIndex : 'N/A'}`;
};

window.draftail.registerControl({
    type: 'reading-level-smog',
    meta: SmogIndex
});
```

With the "multiple things registered" example above, it would just be two calls:

```js
window.draftail.registerControl({
    type: 'my-fancy-control',
    // meta: […]
});

window.draftail.registerDecorator({
    type: 'my-fancy-decorator',
    // strategy: […]
    // component: […]
});
```

Again there could be a potentially nicer API to declare multiple things at once, but really that’s about all that’s needed.
For anyone looking into this, the temporary workaround for this is to monkey patch / override Draftail’s `initEditor` to implement your own API to register further customisations of Draftail. 

Here is a good minimal example from @v9Chris: https://github.com/vixdigital/wagtail-readinglevel/blob/master/wagtailreadinglevel/static/api-monkeypatch.js

```js
// Implement the new APIs.

const DECORATORS = [];
const CONTROLS = [];

const registerDecorator = (decorator) => {
    DECORATORS.push(decorator);
    return DECORATORS;
};

const registerControl = (control) => {
    CONTROLS.push(control);
    return CONTROLS;
};

// Override the existing initEditor to hook the new APIs into it.
// This works in Wagtail 2.0 but will definitely break in a future release.
const initEditor = window.draftail.initEditor;

const initEditorOverride = (selector, options, currentScript) => {
    const overrides = {
        decorators: DECORATORS,
        controls: CONTROLS,
    };

    const newOptions = Object.assign({}, options, overrides);

    return initEditor(selector, newOptions, currentScript);
};

window.draftail.registerControl = registerControl;
window.draftail.registerDecorator = registerDecorator;
window.draftail.initEditor = initEditorOverride;
```

This is obviously not a documented / official API, so if you’re in need of this please comment on this issue to make sure we’re aware of your use case. **At some stage this implementation-specific workaround will likely go away, and we likely won’t provide any migration path or breaking change notice for this** – except for a heads-up in this issue.
I’m interested in this and have used the same approach as above to inject some functionality into the draftail instances. In my case I wanted to replace ASCII style quote-characters with [guillemets](https://en.wikipedia.org/wiki/Guillemet), which can be solved by a relatively simple draft-js plugin. Unfortunately there is no way to add draft-js plugins through the Wagtail API.
Thanks @kmohrf, that’s good to know. Do you have any code to share for reference? There aren’t that many people publicly talking about customizations like this, so any real-world implementation is appreciated.

For this type of character replacement – it’s something I would like to support out of the box eventually (https://github.com/springload/draftail/issues/113). As you say though, it’s also desirable to be able to add with draft-js-plugins.
I’ve published the code [as a gist](https://gist.github.com/kmohrf/b0e070b1f3e2e32f7577ed1bf3bfa863) for reference :)
Hi, any update on this?
I would love to have an "add emoji" button!
It looks like (as predicted), the monkeypatch suggested by @thibaudcolas [above](#issuecomment-574693649) may no longer be working with current versions of wagtail, as per this issue from the wagtail-readinglevel package (that the above example was taken from).

https://github.com/vixdigital/wagtail-readinglevel/issues/28


This example from @alxbridge updates the approach for more recent wagtail versions
https://github.com/vixdigital/wagtail-readinglevel/pull/32
Clever! This approach seems to rely on defining those extensions as entities, whose JS files will then be loaded as configured.  This seems to then register an entity server-side, and a `control` client-side using the API monkeypatch. I was surprised this doesn’t result in two buttons in the editor toolbar, but I guess this might be working since [Draftail v1.4.0](https://github.com/springload/draftail/blob/main/CHANGELOG.md#v140) ("Fix empty buttons appearing when providing custom formats without a defined label or icon"). I believe we’re only using that release in Wagtail starting with Wagtail v2.13 – in older versions, this would result in two buttons in the toolbar, one of which would either break the editor when clicked, or do nothing.

So – seems pretty good to me, with the caveat that this is again relying on implementation details of existing APIs rather than something we intend to support.
[Sharing more info of our use case](https://wagtailcms.slack.com/archives/C80KTJKKM/p1644313904861229) of Draftail customisation.

One of these is using a slightly modified version of the word count:

- Our contents are dual language (Chinese and English), and in Chinese, one characters equals to one word (so we need to extend the default counting via space between words)
- In addition to that we also have Total Word counts! (Excluding some fields that we don't need to count) We placed it near the save button below, neat placement I would say. This was requested by our editors, since we need the total word counts for freelance writers.

![image](https://user-images.githubusercontent.com/47908628/158938469-bae0fd19-9a9c-4374-acae-add4ce1531c1.png)

---

Oh, and also, we managed to do a quote sign replacement! (from `''` and `""`,  to `‘’` and `“”`).  
_Not included in this example to keep the sharing simple for now_

I was hoping we would get to this as part of the [Page editor 2022](https://github.com/wagtail/wagtail/discussions/7739) project but this is looking very unlikely, so I thought I’d add notes here of what this would look like if implemented properly in Wagtail core. The implementation shouldn’t be that complex, it’s just a matter of changing rich text features to affect more options of the editor.

Here are the kinds of changes I would expect, if someone wants to give this a go.

## Server-side

There’s really not that much needed. In [`wagtail/admin/rich_text/editors/draftail/features.py`](https://github.com/wagtail/wagtail/blob/main/wagtail/admin/rich_text/editors/draftail/features.py#L62), add:

```python
class DecoratorFeature(ListFeature):
    """A feature which is listed in the decorators list of the options"""

    option_name = "decorators"


class ControlFeature(ListFeature):
    """A feature which is listed in the controls list of the options"""

    option_name = "controls"


class PluginFeature(ListFeature):
    """A feature which is listed in the plugins list of the options"""

    option_name = "plugins"
```

Then implementers can declare features in `wagtail_hooks.py` like:

```python
type_ = "reading-level-smog"
features.register_editor_plugin(
        "draftail",
        type_,
        draftail_features.ControlFeature(
            {
                "type": type_,
            },
            js=['reading-level-smog.js']
        ),
    )
```

I can’t think why we’d want to do much more than this. Perhaps allow a feature to register multiple things – for example a control and a decorator at the same time. That wouldn’t be much more work than the above though (10 lines of code?). And could be something people add themselves outside of core if they need it. For example, here is a feature that would register multiple things:

```python
class MyFancyFeature(Feature):
    def construct_options(self, options):
        if "controls" not in options:
            options["controls"] = []
        options["controls"].append({ "type": "my-fancy-control" })
        if "decorators" not in options:
            options["decorators"] = []
        options["decorators"].append({ "type": "my-fancy-decorator" })

features.register_editor_plugin(
        "draftail",
        "fancy",
        MyFancyFeature(),
    )
```


## Client-side

We’d need to change [`client/src/components/Draftail/index.js`](https://github.com/wagtail/wagtail/blob/main/client/src/components/Draftail/index.js#L106) so the client-side implementation of features can be registered for more than entities:

```js
/**
 * Registries for client-side code of Draftail plugins.
 */
const ENTITIES = {};
const DECORATORS = {};
const CONTROLS = {};

const registerEntity = (type) => {
  ENTITIES[type.type] = type;
  return ENTITIES;
};
const registerDecorators = (type) => {
  DECORATORS[type.type] = type;
  return DECORATORS;
};
const registerControls = (type) => {
  CONTROLS[type.type] = type;
  return CONTROLS;
};
// For backwards compatibility.
const registerPlugin = registerEntity;
```

And then use those registries:

```diff
-const controls = newOptions.controls || [];
+let controls = newOptions.controls || [];
let entityTypes = newOptions.entityTypes || [];

entityTypes = entityTypes.map(wrapWagtailIcon).map((type) => {
-    const plugin = PLUGINS[type.type];
+    const registeredType = ENTITIES[type.type];

    // Override the properties defined in the JS plugin: Python should be the source of truth.
-    return { ...plugin, ...type };
+    return { ...registeredType, ...type };
});

+controls = controls.map((type) => ({ ...CONTROLS[type.type], ...type }));
```

And then do the same thing for `decorators` and `plugins`. Usage in JS file like `reading-level-smog.js` from the above example would be like this:

```js
const SmogIndex = ({ getEditorState }) => {
    const text = getEditorState().getCurrentContent().getPlainText();
    const stats = CalculateSmogIndex(text);

    return `SMOG Index: ${stats ? stats.smogIndex : 'N/A'}`;
};

window.draftail.registerControl({
    type: 'reading-level-smog',
    meta: SmogIndex
});
```

With the "multiple things registered" example above, it would just be two calls:

```js
window.draftail.registerControl({
    type: 'my-fancy-control',
    // meta: […]
});

window.draftail.registerDecorator({
    type: 'my-fancy-decorator',
    // strategy: […]
    // component: […]
});
```

Again there could be a potentially nicer API to declare multiple things at once, but really that’s about all that’s needed.

Resolving patch:
diff --git a/wagtail/admin/rich_text/editors/draftail/features.py b/wagtail/admin/rich_text/editors/draftail/features.py
--- a/wagtail/admin/rich_text/editors/draftail/features.py
+++ b/wagtail/admin/rich_text/editors/draftail/features.py
@@ -74,3 +74,21 @@ class InlineStyleFeature(ListFeature):
     """A feature which is listed in the inlineStyles list of the options"""
 
     option_name = "inlineStyles"
+
+
+class DecoratorFeature(ListFeature):
+    """A feature which is listed in the decorators list of the options"""
+
+    option_name = "decorators"
+
+
+class ControlFeature(ListFeature):
+    """A feature which is listed in the controls list of the options"""
+
+    option_name = "controls"
+
+
+class PluginFeature(ListFeature):
+    """A feature which is listed in the plugins list of the options"""
+
+    option_name = "plugins"


Test patch:
diff --git a/client/src/components/Draftail/__snapshots__/index.test.js.snap b/client/src/components/Draftail/__snapshots__/index.test.js.snap
--- a/client/src/components/Draftail/__snapshots__/index.test.js.snap
+++ b/client/src/components/Draftail/__snapshots__/index.test.js.snap
@@ -15,8 +15,19 @@ Object {
   "bottomToolbar": [Function],
   "commandToolbar": [Function],
   "commands": true,
-  "controls": Array [],
-  "decorators": Array [],
+  "controls": Array [
+    Object {
+      "meta": [Function],
+      "type": "sentences",
+    },
+  ],
+  "decorators": Array [
+    Object {
+      "component": [Function],
+      "strategy": [Function],
+      "type": "punctuation",
+    },
+  ],
   "editorState": null,
   "enableHorizontalRule": Object {
     "description": "Horizontal line",
@@ -43,7 +54,12 @@ Object {
   "onFocus": null,
   "onSave": [Function],
   "placeholder": "Write something or type ‘/’ to insert a block",
-  "plugins": Array [],
+  "plugins": Array [
+    Object {
+      "handlePastedText": [Function],
+      "type": "anchorify",
+    },
+  ],
   "rawContentState": null,
   "readOnly": false,
   "showRedoControl": false,
diff --git a/client/src/components/Draftail/index.test.js b/client/src/components/Draftail/index.test.js
--- a/client/src/components/Draftail/index.test.js
+++ b/client/src/components/Draftail/index.test.js
@@ -41,14 +41,45 @@ describe('Draftail', () => {
       document.body.innerHTML = '<input id="test" value="null" />';
       const field = document.querySelector('#test');
 
-      draftail.registerPlugin({
-        type: 'IMAGE',
-        source: () => {},
-        block: () => {},
-      });
+      draftail.registerPlugin(
+        {
+          type: 'IMAGE',
+          source: () => {},
+          block: () => null,
+        },
+        'entityTypes',
+      );
+
+      draftail.registerPlugin(
+        {
+          type: 'sentences',
+          meta: () => null,
+        },
+        'controls',
+      );
+
+      draftail.registerPlugin(
+        {
+          type: 'punctuation',
+          strategy: () => {},
+          component: () => null,
+        },
+        'decorators',
+      );
+
+      draftail.registerPlugin(
+        {
+          type: 'anchorify',
+          handlePastedText: () => 'not-handled',
+        },
+        'plugins',
+      );
 
       draftail.initEditor('#test', {
         entityTypes: [{ type: 'IMAGE' }],
+        controls: [{ type: 'sentences' }],
+        decorators: [{ type: 'punctuation' }],
+        plugins: [{ type: 'anchorify' }],
         enableHorizontalRule: true,
       });
 
@@ -153,14 +184,30 @@ describe('Draftail', () => {
 
   describe('#registerPlugin', () => {
     it('works', () => {
+      const plugin = { type: 'TEST' };
+      expect(draftail.registerPlugin(plugin, 'entityTypes')).toMatchObject({
+        TEST: plugin,
+      });
+      expect(draftail.registerPlugin(plugin, 'controls')).toMatchObject({
+        TEST: plugin,
+      });
+      expect(draftail.registerPlugin(plugin, 'decorators')).toMatchObject({
+        TEST: plugin,
+      });
+      expect(draftail.registerPlugin(plugin, 'plugins')).toMatchObject({
+        TEST: plugin,
+      });
+    });
+
+    it('supports legacy entityTypes registration', () => {
       const plugin = {
-        type: 'TEST',
+        type: 'TEST_ENTITY',
         source: null,
         decorator: null,
       };
 
       expect(draftail.registerPlugin(plugin)).toMatchObject({
-        TEST: plugin,
+        TEST_ENTITY: plugin,
       });
     });
   });
diff --git a/client/src/entrypoints/admin/draftail.test.js b/client/src/entrypoints/admin/draftail.test.js
--- a/client/src/entrypoints/admin/draftail.test.js
+++ b/client/src/entrypoints/admin/draftail.test.js
@@ -1,21 +1,49 @@
 require('./draftail');
 
 describe('draftail', () => {
-  it('exposes module as global', () => {
-    expect(window.draftail).toBeDefined();
+  it('exposes a stable API', () => {
+    expect(window.draftail).toMatchInlineSnapshot(`
+      Object {
+        "DocumentModalWorkflowSource": [Function],
+        "DraftUtils": Object {
+          "addHorizontalRuleRemovingSelection": [Function],
+          "addLineBreak": [Function],
+          "applyMarkdownStyle": [Function],
+          "getCommandPalettePrompt": [Function],
+          "getEntitySelection": [Function],
+          "getEntityTypeStrategy": [Function],
+          "getSelectedBlock": [Function],
+          "getSelectionEntity": [Function],
+          "handleDeleteAtomic": [Function],
+          "handleHardNewline": [Function],
+          "handleNewLine": [Function],
+          "insertNewUnstyledBlock": [Function],
+          "removeBlock": [Function],
+          "removeBlockEntity": [Function],
+          "removeCommandPalettePrompt": [Function],
+          "resetBlockWithType": [Function],
+          "updateBlockEntity": [Function],
+        },
+        "EmbedModalWorkflowSource": [Function],
+        "ImageModalWorkflowSource": [Function],
+        "LinkModalWorkflowSource": [Function],
+        "ModalWorkflowSource": [Function],
+        "Tooltip": [Function],
+        "TooltipEntity": [Function],
+        "initEditor": [Function],
+        "registerPlugin": [Function],
+        "splitState": [Function],
+      }
+    `);
   });
 
   it('exposes package as global', () => {
     expect(window.Draftail).toBeDefined();
   });
 
-  it('has defaults registered', () => {
-    expect(Object.keys(window.draftail.registerPlugin({}))).toEqual([
-      'DOCUMENT',
-      'LINK',
-      'IMAGE',
-      'EMBED',
-      'undefined',
-    ]);
+  it('has default entities registered', () => {
+    expect(
+      Object.keys(window.draftail.registerPlugin({}, 'entityTypes')),
+    ).toEqual(['DOCUMENT', 'LINK', 'IMAGE', 'EMBED', 'undefined']);
   });
 });