Расширение редактора 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.
По желанию, мы также можем определить стили для блоков с помощью класса 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 на клиентскую сторону, чтобы добавить к этим токенам ссылки и небольшой график изменения.
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/v3.0.3/extending/extending_draftail.html