Implement new Draftail customisation APIs

- Fixes #5580
- Remove TSLA/TWTR/BTC references and replace with clean energy FSLR / NEE stocks
pull/10633/head
Thibaud Colas 2023-06-12 14:42:53 +01:00 zatwierdzone przez LB (Ben Johnston)
rodzic 4f012d75ec
commit f4ea0156a2
9 zmienionych plików z 423 dodań i 105 usunięć

Wyświetl plik

@ -16,6 +16,7 @@ Changelog
* Optimise queries in collection permission policies using cache on the user object (Sage Abdullah) * 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) * 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) * Auto-select the `StreamField` block when only one block type is declared (Sébastien Corbin)
* Add support for more advanced Draftail customisation APIs (Thibaud Colas)
* Fix: Prevent choosers from failing when initial value is an unrecognised ID, e.g. when moving a page from a location where `parent_page_types` would disallow it (Dan Braghis) * Fix: Prevent choosers from failing when initial value is an unrecognised ID, e.g. when moving a page from a location where `parent_page_types` would disallow it (Dan Braghis)
* Fix: Move comment notifications toggle to the comments side panel (Sage Abdullah) * Fix: Move comment notifications toggle to the comments side panel (Sage Abdullah)
* Fix: Remove comment button on InlinePanel fields (Sage Abdullah) * Fix: Remove comment button on InlinePanel fields (Sage Abdullah)

Wyświetl plik

@ -15,8 +15,19 @@ Object {
"bottomToolbar": [Function], "bottomToolbar": [Function],
"commandToolbar": [Function], "commandToolbar": [Function],
"commands": true, "commands": true,
"controls": Array [], "controls": Array [
"decorators": Array [], Object {
"meta": [Function],
"type": "sentences",
},
],
"decorators": Array [
Object {
"component": [Function],
"strategy": [Function],
"type": "punctuation",
},
],
"editorState": null, "editorState": null,
"enableHorizontalRule": Object { "enableHorizontalRule": Object {
"description": "Horizontal line", "description": "Horizontal line",
@ -43,7 +54,12 @@ Object {
"onFocus": null, "onFocus": null,
"onSave": [Function], "onSave": [Function],
"placeholder": "Write something or type / to insert a block", "placeholder": "Write something or type / to insert a block",
"plugins": Array [], "plugins": Array [
Object {
"handlePastedText": [Function],
"type": "anchorify",
},
],
"rawContentState": null, "rawContentState": null,
"readOnly": false, "readOnly": false,
"showRedoControl": false, "showRedoControl": false,

Wyświetl plik

@ -90,11 +90,21 @@ const onSetToolbar = (choice, callback) => {
/** /**
* Registry for client-side code of Draftail plugins. * Registry for client-side code of Draftail plugins.
*/ */
const PLUGINS = {}; const PLUGINS = {
entityTypes: {},
plugins: {},
controls: {},
decorators: {},
};
const registerPlugin = (plugin) => { /**
PLUGINS[plugin.type] = plugin; * Client-side editor-specific equivalent to register_editor_plugin.
return PLUGINS; * `optionName` defaults to entityTypes for backwards-compatibility with
* previous function signature only allowing registering entities.
*/
const registerPlugin = (type, optionName = 'entityTypes') => {
PLUGINS[optionName][type.type] = type;
return PLUGINS[optionName];
}; };
/** /**
@ -157,15 +167,28 @@ const initEditor = (selector, originalOptions, currentScript) => {
const blockTypes = newOptions.blockTypes || []; const blockTypes = newOptions.blockTypes || [];
const inlineStyles = newOptions.inlineStyles || []; const inlineStyles = newOptions.inlineStyles || [];
let controls = newOptions.controls || []; let controls = newOptions.controls || [];
let decorators = newOptions.decorators || [];
let plugins = newOptions.plugins || [];
const commands = newOptions.commands || true; const commands = newOptions.commands || true;
let entityTypes = newOptions.entityTypes || []; let entityTypes = newOptions.entityTypes || [];
entityTypes = entityTypes.map(wrapWagtailIcon).map((type) => { entityTypes = entityTypes
const plugin = PLUGINS[type.type]; .map(wrapWagtailIcon)
// Override the properties defined in the JS plugin: Python should be the source of truth. // Override the properties defined in the JS plugin: Python should be the source of truth.
return { ...plugin, ...type }; .map((type) => ({ ...PLUGINS.entityTypes[type.type], ...type }));
});
controls = controls.map((type) => ({
...PLUGINS.controls[type.type],
...type,
}));
decorators = decorators.map((type) => ({
...PLUGINS.decorators[type.type],
...type,
}));
plugins = plugins.map((type) => ({
...PLUGINS.plugins[type.type],
...type,
}));
// Only initialise the character count / max length on fields explicitly requiring it. // Only initialise the character count / max length on fields explicitly requiring it.
if (field.hasAttribute('maxlength')) { if (field.hasAttribute('maxlength')) {
@ -228,6 +251,8 @@ const initEditor = (selector, originalOptions, currentScript) => {
inlineStyles: inlineStyles.map(wrapWagtailIcon), inlineStyles: inlineStyles.map(wrapWagtailIcon),
entityTypes, entityTypes,
controls, controls,
decorators,
plugins,
commands, commands,
enableHorizontalRule, enableHorizontalRule,
}; };

Wyświetl plik

@ -41,14 +41,45 @@ describe('Draftail', () => {
document.body.innerHTML = '<input id="test" value="null" />'; document.body.innerHTML = '<input id="test" value="null" />';
const field = document.querySelector('#test'); const field = document.querySelector('#test');
draftail.registerPlugin({ draftail.registerPlugin(
type: 'IMAGE', {
source: () => {}, type: 'IMAGE',
block: () => {}, 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', { draftail.initEditor('#test', {
entityTypes: [{ type: 'IMAGE' }], entityTypes: [{ type: 'IMAGE' }],
controls: [{ type: 'sentences' }],
decorators: [{ type: 'punctuation' }],
plugins: [{ type: 'anchorify' }],
enableHorizontalRule: true, enableHorizontalRule: true,
}); });
@ -153,14 +184,30 @@ describe('Draftail', () => {
describe('#registerPlugin', () => { describe('#registerPlugin', () => {
it('works', () => { 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 = { const plugin = {
type: 'TEST', type: 'TEST_ENTITY',
source: null, source: null,
decorator: null, decorator: null,
}; };
expect(draftail.registerPlugin(plugin)).toMatchObject({ expect(draftail.registerPlugin(plugin)).toMatchObject({
TEST: plugin, TEST_ENTITY: plugin,
}); });
}); });
}); });

Wyświetl plik

@ -17,7 +17,7 @@ window.Draftail = Draftail;
window.draftail = draftail; window.draftail = draftail;
// Plugins for the built-in entities. // Plugins for the built-in entities.
const plugins = [ const entityTypes = [
{ {
type: 'DOCUMENT', type: 'DOCUMENT',
source: draftail.DocumentModalWorkflowSource, source: draftail.DocumentModalWorkflowSource,
@ -41,4 +41,4 @@ const plugins = [
}, },
]; ];
plugins.forEach(draftail.registerPlugin); entityTypes.forEach((type) => draftail.registerPlugin(type, 'entityTypes'));

Wyświetl plik

@ -1,21 +1,49 @@
require('./draftail'); require('./draftail');
describe('draftail', () => { describe('draftail', () => {
it('exposes module as global', () => { it('exposes a stable API', () => {
expect(window.draftail).toBeDefined(); 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', () => { it('exposes package as global', () => {
expect(window.Draftail).toBeDefined(); expect(window.Draftail).toBeDefined();
}); });
it('has defaults registered', () => { it('has default entities registered', () => {
expect(Object.keys(window.draftail.registerPlugin({}))).toEqual([ expect(
'DOCUMENT', Object.keys(window.draftail.registerPlugin({}, 'entityTypes')),
'LINK', ).toEqual(['DOCUMENT', 'LINK', 'IMAGE', 'EMBED', 'undefined']);
'IMAGE',
'EMBED',
'undefined',
]);
}); });
}); });

Wyświetl plik

@ -2,15 +2,17 @@
# Extending the Draftail Editor # Extending the Draftail Editor
Wagtails rich text editor is built with [Draftail](https://www.draftail.org/), and its functionality can be extended through plugins. Wagtails rich text editor is built with [Draftail](https://www.draftail.org/), which supports different types of extensions.
Plugins come in three types: ## Formatting extensions
- Inline styles – To format a portion of a line, for example `bold`, `italic` or `monospace`. Draftail supports three types of formatting:
- Blocks – To indicate the structure of the content, for example, `blockquote`, `ol`.
- Entities – To enter additional data/metadata, for example, `link` (with a URL) or `image` (with a file).
All of these plugins are created with a similar baseline, which we can demonstrate with one of the simplest examples – a custom feature for an inline style of `mark`. Place the following in a `wagtail_hooks.py` file in any installed app: - **Inline styles** – To format a portion of a line, for example `bold`, `italic` or `monospace`. Text can have as many inline styles as needed – for example bold _and_ italic at the same time.
- **Blocks** – To indicate the structure of the content, for example, `blockquote`, `ol`. Any given text can only be of one block type.
- **Entities** – To enter additional data/metadata, for example, `link` (with a URL) or `image` (with a file). Text can only have one entity applied at a time.
All of these extensions are created with a similar baseline, which we can demonstrate with one of the simplest examples – a custom feature for an inline style of `mark`. Place the following in a `wagtail_hooks.py` file in any installed app:
```python ```python
import wagtail.admin.rich_text.editors.draftail.features as draftail_features import wagtail.admin.rich_text.editors.draftail.features as draftail_features
@ -68,13 +70,13 @@ For detailed configuration options, head over to the [Draftail documentation](ht
- To display the control in the toolbar, combine `icon`, `label` and `description`. - To display the control in the toolbar, combine `icon`, `label` and `description`.
- The controls `icon` can be a string to use an icon font with CSS classes, say `'icon': 'fas fa-user',`. It can also be an array of strings, to use SVG paths, or SVG symbol references for example `'icon': ['M100 100 H 900 V 900 H 100 Z'],`. The paths need to be set for a 1024x1024 viewbox. - The controls `icon` can be a string to use an icon font with CSS classes, say `'icon': 'fas fa-user',`. It can also be an array of strings, to use SVG paths, or SVG symbol references for example `'icon': ['M100 100 H 900 V 900 H 100 Z'],`. The paths need to be set for a 1024x1024 viewbox.
## Creating new inline styles ### Creating new inline styles
In addition to the initial example, inline styles take a `style` property to define what CSS rules will be applied to text in the editor. Be sure to read the [Draftail documentation](https://www.draftail.org/docs/formatting-options) on inline styles. In addition to the initial example, inline styles take a `style` property to define what CSS rules will be applied to text in the editor. Be sure to read the [Draftail documentation](https://www.draftail.org/docs/formatting-options) on inline styles.
Finally, the DB to/from conversion uses an `InlineStyleElementHandler` to map from a given tag (`<mark>` in the example above) to a Draftail type, and the inverse mapping is done with [Draft.js exporter configuration](https://github.com/springload/draftjs_exporter) of the `style_map`. Finally, the DB to/from conversion uses an `InlineStyleElementHandler` to map from a given tag (`<mark>` in the example above) to a Draftail type, and the inverse mapping is done with [Draft.js exporter configuration](https://github.com/springload/draftjs_exporter) of the `style_map`.
## Creating new blocks ### Creating new blocks
Blocks are nearly as simple as inline styles: Blocks are nearly as simple as inline styles:
@ -119,7 +121,7 @@ Optionally, we can also define styles for the blocks with the `Draftail-block--h
Thats it! The extra complexity is that you may need to write CSS to style the blocks in the editor. Thats it! The extra complexity is that you may need to write CSS to style the blocks in the editor.
## Creating new entities ### Creating new entities
```{warning} ```{warning}
This is an advanced feature. Please carefully consider whether you really need this. This is an advanced feature. Please carefully consider whether you really need this.
@ -147,7 +149,7 @@ To go further, please look at the [Draftail documentation](https://www.draftail.
Here is a detailed example to showcase how those tools are used in the context of Wagtail. Here is a detailed example to showcase how those tools are used in the context of Wagtail.
For the sake of our example, we can imagine a news team working at a financial newspaper. For the sake of our example, we can imagine a news team working at a financial newspaper.
They want to write articles about the stock market, refer to specific stocks anywhere inside of their content (for example "$TSLA" tokens in a sentence), and then have their article automatically enriched with the stocks information (a link, a number, a sparkline). They want to write articles about the stock market, refer to specific stocks anywhere inside of their content (for example "$NEE" tokens in a sentence), and then have their article automatically enriched with the stocks information (a link, a number, a sparkline).
The editor toolbar could contain a "stock chooser" that displays a list of available stocks, then inserts the users selection as a textual token. For our example, we will just pick a stock at random: The editor toolbar could contain a "stock chooser" that displays a list of available stocks, then inserts the users selection as a textual token. For our example, we will just pick a stock at random:
@ -228,55 +230,47 @@ Note how they both do similar conversions, but use different APIs. `to_database_
The next step is to add JavaScript to define how the entities are created (the `source`), and how they are displayed (the `decorator`). Within `stock.js`, we define the source component: The next step is to add JavaScript to define how the entities are created (the `source`), and how they are displayed (the `decorator`). Within `stock.js`, we define the source component:
```javascript ```javascript
const React = window.React;
const Modifier = window.DraftJS.Modifier;
const EditorState = window.DraftJS.EditorState;
const DEMO_STOCKS = ['AMD', 'AAPL', 'TWTR', 'TSLA', 'BTC'];
// Not a real React component – just creates the entities as soon as it is rendered. // Not a real React component – just creates the entities as soon as it is rendered.
class StockSource extends React.Component { class StockSource extends window.React.Component {
componentDidMount() { componentDidMount() {
const { editorState, entityType, onComplete } = this.props; const { editorState, entityType, onComplete } = this.props;
const content = editorState.getCurrentContent(); const content = editorState.getCurrentContent();
const selection = editorState.getSelection(); const selection = editorState.getSelection();
const randomStock = const demoStocks = ['AMD', 'AAPL', 'NEE', 'FSLR'];
DEMO_STOCKS[Math.floor(Math.random() * DEMO_STOCKS.length)]; const randomStock = demoStocks[Math.floor(Math.random() * demoStocks.length)];
// Uses the Draft.js API to create a new entity with the right data. // Uses the Draft.js API to create a new entity with the right data.
const contentWithEntity = content.createEntity( const contentWithEntity = content.createEntity(
entityType.type, entityType.type,
'IMMUTABLE', 'IMMUTABLE',
{ { stock: randomStock },
stock: randomStock, );
}, const entityKey = contentWithEntity.getLastCreatedEntityKey();
);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
// We also add some text for the entity to be activated on. // We also add some text for the entity to be activated on.
const text = `$${randomStock}`; const text = `$${randomStock}`;
const newContent = Modifier.replaceText( const newContent = window.DraftJS.Modifier.replaceText(
content, content,
selection, selection,
text, text,
null, null,
entityKey, entityKey,
); );
const nextState = EditorState.push( const nextState = window.DraftJS.EditorState.push(
editorState, editorState,
newContent, newContent,
'insert-characters', 'insert-characters',
); );
onComplete(nextState); onComplete(nextState);
} }
render() { render() {
return null; return null;
} }
} }
``` ```
@ -287,19 +281,19 @@ We then create the decorator component:
```javascript ```javascript
const Stock = (props) => { const Stock = (props) => {
const { entityKey, contentState } = props; const { entityKey, contentState } = props;
const data = contentState.getEntity(entityKey).getData(); const data = contentState.getEntity(entityKey).getData();
return React.createElement( return window.React.createElement(
'a', 'a',
{ {
role: 'button', role: 'button',
onMouseUp: () => { onMouseUp: () => {
window.open(`https://finance.yahoo.com/quote/${data.stock}`); window.open(`https://finance.yahoo.com/quote/${data.stock}`);
}, },
}, },
props.children, props.children,
); );
}; };
``` ```
@ -310,18 +304,18 @@ Finally, we register the JS components of our plugin:
```javascript ```javascript
// Register the plugin directly on script execution so the editor loads it when initialising. // Register the plugin directly on script execution so the editor loads it when initialising.
window.draftail.registerPlugin({ window.draftail.registerPlugin({
type: 'STOCK', type: 'STOCK',
source: StockSource, source: StockSource,
decorator: Stock, decorator: Stock,
}); }, 'entityTypes');
``` ```
And thats it! All of this setup will finally produce the following HTML on the sites front-end: And thats it! All of this setup will finally produce the following HTML on the sites front-end:
```html ```html
<p> <p>
Anyone following Elon Musks <span data-stock="TSLA">$TSLA</span> should Anyone following NextEra technology <span data-stock="NEE">$NEE</span> should
also look into <span data-stock="BTC">$BTC</span>. also look into <span data-stock="FSLR">$FSLR</span>.
</p> </p>
``` ```
@ -329,17 +323,205 @@ To fully complete the demo, we can add a bit of JavaScript to the front-end in o
```javascript ```javascript
document.querySelectorAll('[data-stock]').forEach((elt) => { document.querySelectorAll('[data-stock]').forEach((elt) => {
const link = document.createElement('a'); const link = document.createElement('a');
link.href = `https://finance.yahoo.com/quote/${elt.dataset.stock}`; link.href = `https://finance.yahoo.com/quote/${elt.dataset.stock}`;
link.innerHTML = `${elt.innerHTML}<svg width="50" height="20" stroke-width="2" stroke="blue" fill="rgba(0, 0, 255, .2)"><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4" fill="none"></path><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4 V 20 L 4 20 Z" stroke="none"></path></svg>`; link.innerHTML = `${elt.innerHTML}<svg width="50" height="20" stroke-width="2" stroke="blue" fill="rgba(0, 0, 255, .2)"><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4" fill="none"></path><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4 V 20 L 4 20 Z" stroke="none"></path></svg>`;
elt.innerHTML = ''; elt.innerHTML = '';
elt.appendChild(link); 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. 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, lets 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 ## 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: To further customise how the Draftail widgets are integrated into the UI, there are additional extension points for CSS and JS:

Wyświetl plik

@ -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) * 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) * 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) * 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 ### Bug fixes

Wyświetl plik

@ -74,3 +74,21 @@ class InlineStyleFeature(ListFeature):
"""A feature which is listed in the inlineStyles list of the options""" """A feature which is listed in the inlineStyles list of the options"""
option_name = "inlineStyles" 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"