- Anyone following Elon Musk’s $TSLA should - also look into $BTC. + Anyone following NextEra technology $NEE should + also look into $FSLR.
``` @@ -329,17 +323,205 @@ To fully complete the demo, we can add a bit of JavaScript to the front-end in o ```javascript document.querySelectorAll('[data-stock]').forEach((elt) => { - const link = document.createElement('a'); - link.href = `https://finance.yahoo.com/quote/${elt.dataset.stock}`; - link.innerHTML = `${elt.innerHTML}`; + const link = document.createElement('a'); + link.href = `https://finance.yahoo.com/quote/${elt.dataset.stock}`; + link.innerHTML = `${elt.innerHTML}`; - elt.innerHTML = ''; - elt.appendChild(link); + elt.innerHTML = ''; + elt.appendChild(link); }); ``` Custom block entities can also be created (have a look at the separate [Draftail documentation](https://www.draftail.org/docs/blocks)), but these are not detailed here since [StreamField](streamfield_topic) is the go-to way to create block-level rich text in Wagtail. +(extending_the_draftail_editor_advanced)= + +## Other editor extensions + +Draftail has additional APIs for more complex customisations: + +- **Controls** – To add arbitrary UI elements to editor toolbars. +- **Decorators** – For arbitrary text decorations / highlighting. +- **Plugins** – For direct access to all Draft.js APIs. + +### Custom toolbar controls + +To add an arbitrary new UI element to editor toolbars, Draftail comes with a [controls API](https://www.draftail.org/docs/arbitrary-controls). Controls can be arbitrary React components, which can get and set the editor state. Note controls update on _every keystroke_ in the editor – make sure they render fast! + +Here is an example with a simple sentence counter – first, registering the editor feature in a `wagtail_hooks.py`: + +```python +from wagtail.admin.rich_text.editors.draftail.features import ControlFeature + + +@hooks.register('register_rich_text_features') +def register_sentences_counter(features): + feature_name = 'sentences' + features.default_features.append(feature_name) + + features.register_editor_plugin( + 'draftail', + feature_name, + ControlFeature({ + 'type': feature_name, + }, + js=['draftail_sentences.js'], + ), + ) +``` + +Then, `draftail_sentences.js` declares a React component that will be rendered in the "meta" bottom toolbar of the editor: + +```javascript +const countSentences = (str) => + str ? (str.match(/[.?!…]+./g) || []).length + 1 : 0; + +const SentenceCounter = ({ getEditorState }) => { + const editorState = getEditorState(); + const content = editorState.getCurrentContent(); + const text = content.getPlainText(); + + return window.React.createElement('div', { + className: 'w-inline-block w-tabular-nums w-help-text w-mr-4', + }, `Sentences: ${countSentences(text)}`); +} + +window.draftail.registerPlugin({ + type: 'sentences', + meta: SentenceCounter, +}, 'controls'); +``` + +### Text decorators + +The [decorators API](https://www.draftail.org/docs/decorators) is how Draftail / Draft.js supports highlighting text with special formatting in the editor. It uses the [CompositeDecorator](https://draftjs.org/docs/advanced-topics-decorators/#compositedecorator) API, with each entry having a `strategy` function to determine what text to target, and a `component` function to render the decoration. + +There are two important considerations when using this API: + +- Order matters: only one decorator can render per character in the editor. This includes any entities that are rendered as decorations. +- For performance reasons, Draft.js only re-renders decorators that are on the currently-focused line of text. + +Here is an example with highlighting of problematic punctuation – first, registering the editor feature in a `wagtail_hooks.py`: + +```python +from wagtail.admin.rich_text.editors.draftail.features import DecoratorFeature + + +@hooks.register('register_rich_text_features') +def register_punctuation_highlighter(features): + feature_name = 'punctuation' + features.default_features.append(feature_name) + + features.register_editor_plugin( + 'draftail', + feature_name, + DecoratorFeature({ + 'type': feature_name, + }, + js=['draftail_punctuation.js'], + ), + ) +``` + +Then, `draftail_punctuation.js` defines the strategy and the highlighting component: + +```javascript +const PUNCTUATION = /(\.\.\.|!!|\?!)/g; + +const punctuationStrategy = (block, callback) => { + const text = block.getText(); + let matches; + while ((matches = PUNCTUATION.exec(text)) !== null) { + callback(matches.index, matches.index + matches[0].length); + } +}; + +const errorHighlight = { + color: 'var(--w-color-text-error)', + outline: '1px solid currentColor', +} + +const PunctuationHighlighter = ({ children }) => ( + window.React.createElement('span', { style: errorHighlight, title: 'refer to our styleguide' }, children) +); + +window.draftail.registerPlugin({ + type: 'punctuation', + strategy: punctuationStrategy, + component: PunctuationHighlighter, +}, 'decorators'); +``` + +### Arbitrary plugins + +```{warning} +This is an advanced feature. Please carefully consider whether you really need this. +``` + +Draftail supports plugins following the [Draft.js Plugins](https://www.draft-js-plugins.com/) architecture. Such plugins are the most advanced and powerful type of extension for the editor, offering customisation capabilities equal to what would be possible with a custom Draft.js editor. + +A common scenario where this API can help is to add bespoke copy-paste processing. Here is a simple example, automatically converting URL anchor hash references to links. First, let’s register the extension in Python: + +```python +@hooks.register('register_rich_text_features') +def register_anchorify(features): + feature_name = 'anchorify' + features.default_features.append(feature_name) + + features.register_editor_plugin( + 'draftail', + feature_name, + PluginFeature({ + 'type': feature_name, + }, + js=['draftail_anchorify.js'], + ), + ) +``` + +Then, in `draftail_anchorify.js`: + +```javascript +const anchorifyPlugin = { + type: 'anchorify', + + handlePastedText(text, html, editorState, { setEditorState }) { + let nextState = editorState; + + if (text.match(/^#[a-zA-Z0-9_-]+$/ig)) { + const selection = nextState.getSelection(); + let content = nextState.getCurrentContent(); + content = content.createEntity("LINK", "MUTABLE", { url: text }); + const entityKey = content.getLastCreatedEntityKey(); + + if (selection.isCollapsed()) { + content = window.DraftJS.Modifier.insertText( + content, + selection, + text, + undefined, + entityKey, + ) + nextState = window.DraftJS.EditorState.push( + nextState, + content, + "insert-fragment", + ); + } else { + nextState = window.DraftJS.RichUtils.toggleLink(nextState, selection, entityKey); + } + + setEditorState(nextState); + return "handled"; + } + + return "not-handled"; + }, +}; + +window.draftail.registerPlugin(anchorifyPlugin, 'plugins'); +``` + ## Integration of the Draftail widgets To further customise how the Draftail widgets are integrated into the UI, there are additional extension points for CSS and JS: diff --git a/docs/releases/5.1.md b/docs/releases/5.1.md index cd41609810..c9de65ed38 100644 --- a/docs/releases/5.1.md +++ b/docs/releases/5.1.md @@ -40,6 +40,7 @@ Thank you to Damilola for his work, and to Google for sponsoring this project. * Optimise queries in collection permission policies using cache on the user object (Sage Abdullah) * Phone numbers entered via a link chooser will now have any spaces stripped out, ensuring a valid `href="tel:..."` attribute (Sahil Jangra) * Auto-select the `StreamField` block when only one block type is declared (Sébastien Corbin) + * Add support for more [advanced Draftail customisation APIs](extending_the_draftail_editor_advanced) (Thibaud Colas) ### Bug fixes diff --git a/wagtail/admin/rich_text/editors/draftail/features.py b/wagtail/admin/rich_text/editors/draftail/features.py index 7bfa8894c5..50ae8cb3ca 100644 --- 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"