Spec-Zone.ru › Wagtail 2

Расширение редактора 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» в предложении), а затем автоматически обогащать свою статью информацией о данной акции (ссылка, число, график изменения стоимости).

Панель инструментов редактора может содержать «выбор акции», который отображает список доступных акций, затем вставляет выбор пользователя в качестве текстового токена. Для нашего примера мы просто выберем акцию случайным образом:

../_images/draftail_entity_stock_source.gif

Затем эти токены сохраняются в редактируемом богатом тексте при публикации. Когда новостная статья отображается на сайте, мы вставляем данные о текущем рынке, полученные из API, рядом с каждым токеном:

../_images/draftail_entity_stock_rendering.png

Для достижения этой цели мы начинаем с регистрации функции богатого текста, как и в случае со стилями в строке и блоками:

@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-.
  • Предыдущая Внутреннее устройство редактора богатого текста
  • Следующая Расширение редактора Hallo

Содержание страницы

  • Расширение редактора Draftail
    • Создание новых стилей в строке
    • Создание новых блоков
    • Создание новых сущностей
    • Интеграция виджетов 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API