Расширение редактора Draftail
Редактор богатого текста Wagtail построен с помощью Draftail, и его функциональность может быть расширена с помощью плагинов.
Плагины бывают трех типов:
- Стили в строке – для форматирования части строки, например
bold,italic,monospace. - Блоки – для указания структуры контента, например
blockquote,ol. - Сущности – для ввода дополнительных данных/метаданных, например
link(с URL),image(с файлом).
Все эти плагины созданы на основе одного шаблона, который мы можем продемонстрировать на одном из простейших примеров — пользовательского элемента для стиля в строке mark. Поместите следующее в файл wagtail_hooks.py в любой установленной приложении:
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import InlineStyleElementHandler
from wagtail.core import hooks
# 1. Use the register_rich_text_features hook.
@hooks.register('register_rich_text_features')
def register_mark_feature(features):
"""
Registering the `mark` feature, which uses the `MARK` Draft.js inline style type,
and is stored as HTML with a `<mark>` tag.
"""
feature_name = 'mark'
type_ = 'MARK'
tag = 'mark'
# 2. Configure how Draftail handles the feature in its toolbar.
control = {
'type': type_,
'label': '☆',
'description': 'Mark',
# This isn’t even required – Draftail has predefined styles for MARK.
# 'style': {'textDecoration': 'line-through'},
}
# 3. Call register_editor_plugin to register the configuration for Draftail.
features.register_editor_plugin(
'draftail', feature_name, draftail_features.InlineStyleFeature(control)
)
# 4.configure the content transform from the DB to the editor and back.
db_conversion = {
'from_database_format': {tag: InlineStyleElementHandler(type_)},
'to_database_format': {'style_map': {type_: tag}},
}
# 5. Call register_converter_rule to register the content transformation conversion.
features.register_converter_rule('contentstate', feature_name, db_conversion)
# 6. (optional) Add the feature to the default features list to make it available
# on rich text fields that do not specify an explicit 'features' list
features.default_features.append('mark')
Эти шаги всегда будут одинаковыми для всех плагинов Draftail. Важные части заключаются в следующем:
- Последовательно используйте тип Draft.js или имена функций Wagtail там, где это уместно.
- Предоставьте достаточно информации Draftail, чтобы он знал, как создать кнопку для функции и как ее отобразить (подробнее об этом позже).
- Настройте преобразование для использования правильного HTML-элемента (так как они хранятся в базе данных).
Для подробных параметров настройки перейдите к документации Draftail, чтобы увидеть все детали. Вот некоторые моменты, заслуживающие внимания в отношении элементов управления:
type— единственная обязательная часть информации.- Чтобы отобразить элемент управления на панели инструментов, объедините
icon,labelиdescription. iconэлементов управления может быть строкой для использования иконки шрифта с классами CSS, например'icon': 'fas fa-user',. Он также может быть массивом строк для использования SVG-путей или ссылок на SVG-символы, например'icon': ['M100 100 H 900 V 900 H 100 Z'],. Пути должны быть настроены для области просмотра 1024x1024.
Создание новых стилей в строке
В дополнение к исходному примеру, стили в строке принимают свойство style для определения правил CSS, которые будут применяться к тексту в редакторе. Обязательно прочитайте документацию Draftail по стилям в строке.
Наконец, преобразование базы данных в/из использует InlineStyleElementHandler для сопоставления заданного тега (<mark> в примере выше) с типом Draftail, а обратное сопоставление выполняется с помощью настройки экспортера Draft.js style_map.
Создание новых блоков
Блоки почти так же просты, как стили в строке:
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
@hooks.register('register_rich_text_features')
def register_help_text_feature(features):
"""
Registering the `help-text` feature, which uses the `help-text` Draft.js block type,
and is stored as HTML with a `<div class="help-text">` tag.
"""
feature_name = 'help-text'
type_ = 'help-text'
control = {
'type': type_,
'label': '?',
'description': 'Help text',
# Optionally, we can tell Draftail what element to use when displaying those blocks in the editor.
'element': 'div',
}
features.register_editor_plugin(
'draftail', feature_name, draftail_features.BlockFeature(control, css={'all': ['help-text.css']})
)
features.register_converter_rule('contentstate', feature_name, {
'from_database_format': {'div[class=help-text]': BlockElementHandler(type_)},
'to_database_format': {'block_map': {type_: {'element': 'div', 'props': {'class': 'help-text'}}}},
})
Вот основные отличия:
- Мы можем настроить
element, чтобы указать Draftail, как отображать эти блоки в редакторе. - Мы регистрируем плагин с помощью
BlockFeature. - Мы настраиваем преобразование с помощью
BlockElementHandlerиblock_map.
По желанию мы также можем определить стили для блоков с помощью класса CSS Draftail-block--help-text (Draftail-block--<block type>).
Вот и все! Дополнительная сложность заключается в том, что вам может потребоваться написать CSS для форматирования блоков в редакторе.
Создание новых сущностей
Предупреждение
Это расширенная функция. Пожалуйста, тщательно подумайте, действительно ли вам это нужно.
Сущности — это не просто кнопки форматирования на панели инструментов. Обычно они должны быть намного более универсальными, общаясь с API или запрашивая дополнительный ввод пользователя. Таким образом,
- Вам, скорее всего, понадобится большой объем JavaScript, часть из которого будет написана на React.
- API очень низкоуровневый. Вам, скорее всего, потребуются знания Draft.js.
- Пользовательские интерфейсы в редакторе богатого текста могут быть хрупкими. Будьте готовы тратить время на тестирование в нескольких браузерах.
Хорошей новостью является то, что такой низкоуровневый API позволит сторонним плагинам Wagtail вносить инновации в функции редактирования богатого текста, предлагая новые виды опыта. Но на данный момент рассмотрите возможность реализации вашего пользовательского интерфейса с помощью StreamField вместо этого, у которого есть проверенный временем API, предназначенный для разработчиков Django.
Вот основные требования для создания новой функции сущности:
- Как и для стилей в строке и блоков, зарегистрируйте плагин редактора.
- Плагин редактора должен определить
source: компонент React, отвечающий за создание новых экземпляров сущностей в редакторе с помощью API Draft.js. - Плагин редактора также нуждается в
decorator(для сущностей в строке) илиblock(для блочных сущностей): компоненте React, отвечающем за отображение экземпляров сущностей внутри редактора. - Как и для стилей в строке и блоков, настройте преобразование базы данных в/из.
- Преобразование обычно более сложное, так как сущности содержат данные, которые необходимо сериализовать в HTML.
Для написания компонентов React Wagtail предоставляет свои зависимости React, Draft.js и Draftail в качестве глобальных переменных. Подробнее об этом читайте в Расширение компонентов клиентской части. Для более глубокого погружения обратитесь к документации Draftail, а также к документации экспортера Draft.js.
Вот подробный пример, демонстрирующий, как эти инструменты используются в контексте Wagtail. Для нашего примера мы можем представить себе новостную команду, работающую в финансовой газете. Они хотят писать статьи о фондовом рынке, ссылаться на конкретные акции где угодно в своём контенте (например, токены «$TSLA» в предложении), а затем автоматически обогащать свою статью информацией о данной акции (ссылка, число, график изменения стоимости).
Панель инструментов редактора может содержать «выбор акции», который отображает список доступных акций, затем вставляет выбор пользователя в качестве текстового токена. Для нашего примера мы просто выберем акцию случайным образом:
Затем эти токены сохраняются в редактируемом богатом тексте при публикации. Когда новостная статья отображается на сайте, мы вставляем данные о текущем рынке, полученные из API, рядом с каждым токеном:
Для достижения этой цели мы начинаем с регистрации функции богатого текста, как и в случае со стилями в строке и блоками:
@hooks.register('register_rich_text_features')
def register_stock_feature(features):
features.default_features.append('stock')
"""
Registering the `stock` feature, which uses the `STOCK` Draft.js entity type,
and is stored as HTML with a `<span data-stock>` tag.
"""
feature_name = 'stock'
type_ = 'STOCK'
control = {
'type': type_,
'label': '$',
'description': 'Stock',
}
features.register_editor_plugin(
'draftail', feature_name, draftail_features.EntityFeature(
control,
js=['stock.js'],
css={'all': ['stock.css']}
)
)
features.register_converter_rule('contentstate', feature_name, {
# Note here that the conversion is more complicated than for blocks and inline styles.
'from_database_format': {'span[data-stock]': StockEntityElementHandler(type_)},
'to_database_format': {'entity_decorators': {type_: stock_entity_decorator}},
})
Ключевые аргументы js и css в EntityFeature могут использоваться для указания дополнительных файлов JS и CSS для загрузки, когда активна эта функция. Оба являются необязательными. Их значения добавляются в объект Media, дополнительная документация по этим объектам доступна в документации Django Form Assets.
Поскольку сущности содержат данные, преобразование в/из формат базы данных более сложное. Мы должны создать два обработчика:
from draftjs_exporter.dom import DOM
from wagtail.admin.rich_text.converters.html_to_contentstate import InlineEntityElementHandler
def stock_entity_decorator(props):
"""
Draft.js ContentState to database HTML.
Converts the STOCK entities into a span tag.
"""
return DOM.create_element('span', {
'data-stock': props['stock'],
}, props['children'])
class StockEntityElementHandler(InlineEntityElementHandler):
"""
Database HTML to Draft.js ContentState.
Converts the span tag into a STOCK entity, with the right data.
"""
mutability = 'IMMUTABLE'
def get_attribute_data(self, attrs):
"""
Take the ``stock`` value from the ``data-stock`` HTML attribute.
"""
return {
'stock': attrs['data-stock'],
}
Обратите внимание, как оба они выполняют похожие преобразования, но используют разные API. to_database_format создан с помощью API компонентов экспортера Draft.js, в то время как from_database_format использует API Wagtail.
Следующим шагом является добавление JavaScript для определения того, как создаются сущности (source), и как они отображаются (decorator). Внутри stock.js, мы определяем компонент источника:
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.
class StockSource extends React.Component {
componentDidMount() {
const { editorState, entityType, onComplete } = this.props;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const randomStock = DEMO_STOCKS[Math.floor(Math.random() * DEMO_STOCKS.length)];
// Uses the Draft.js API to create a new entity with the right data.
const contentWithEntity = content.createEntity(entityType.type, 'IMMUTABLE', {
stock: randomStock,
});
const entityKey = contentWithEntity.getLastCreatedEntityKey();
// We also add some text for the entity to be activated on.
const text = `$${randomStock}`;
const newContent = Modifier.replaceText(content, selection, text, null, entityKey);
const nextState = EditorState.push(editorState, newContent, 'insert-characters');
onComplete(nextState);
}
render() {
return null;
}
}
Этот компонент источника использует данные и обратные вызовы, предоставленные Draftail. Он также использует зависимости из глобальных переменных — см. Расширение компонентов клиентской части.
Затем мы создаём компонент декоратора:
const Stock = (props) => {
const { entityKey, contentState } = props;
const data = contentState.getEntity(entityKey).getData();
return React.createElement('a', {
role: 'button',
onMouseUp: () => {
window.open(`https://finance.yahoo.com/quote/${data.stock}`);
},
}, props.children);
};
Это простой компонент React. Он не использует JSX, так как мы не хотим использовать этап сборки для нашего JavaScript.
Наконец, мы регистрируем JS-компоненты нашего плагина:
window.draftail.registerPlugin({
type: 'STOCK',
source: StockSource,
decorator: Stock,
});
И вот это всё! Вся эта настройка в конечном итоге генерирует следующий HTML на клиентской стороне сайта:
<p>
Anyone following Elon Musk’s <span data-stock="TSLA">$TSLA</span> should also look into <span data-stock="BTC">$BTC</span>.
</p>
Для полного завершения демонстрации мы можем добавить немного JavaScript на клиентскую сторону, чтобы оформить эти токены ссылками и небольшим графиком изменения стоимости.
[].slice.call(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}<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.appendChild(link);
});
Также можно создавать пользовательские блочные сущности (посмотрите отдельную документацию Draftail), но здесь это не рассматривается, так как StreamField является оптимальным способом создания блочных редактируемых богатых текстов в Wagtail.
Интеграция виджетов Draftail
Для дальнейшей настройки интеграции виджетов Draftail в пользовательский интерфейс существуют дополнительные точки расширения для CSS и JS:
- В JavaScript используйте селектор атрибутов
[data-draftail-input]для выбора поля ввода, содержащего данные, и[data-draftail-editor-wrapper]для элемента, который оборачивает редактор. - Экземпляр редактора связан с полем ввода для императивного доступа. Используйте
document.querySelector('[data-draftail-input]').draftailEditor. - В CSS используйте классы, начинающиеся с
Draftail-.
© 2014-present Torchbox Ltd and individual contributors.
All rights are reserved.
Licensed under the BSD License.
https://docs.wagtail.org/en/v2.16.3/extending/extending_draftail.html