Расширение редактора 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 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.
Создание новых блоков
Блоки почти столь же просты, как и стили выделения:
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
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.
По желанию, мы также можем определить стили для блоков с помощью Draftail-block--help-text (Draftail-block--<block type>)-класса CSS.
Это все! Дополнительная сложность заключается в том, что вам может потребоваться написать 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 на клиентскую сторону, чтобы декорировать эти токены ссылками и небольшим графиком.
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/stable/extending/extending_draftail.html