Spec-Zone.ru › Wagtail 3

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

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

../_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 на клиентскую сторону, чтобы добавить к этим токенам ссылки и небольшой график изменения.

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

Spec-Zone.ru

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