Плагин TinyMCE 6 для вставки блоков в контент

e4ae49d4de20f26a3fbfd195dc697549957b8989
Напишем плагин для вставки в текст информационных сообщений. Такие блоки часто вставляются в документацию или туториал с целью предупредить пользователя или акцентировать его внимание на какой-то особенности.

Подготовка

Начнём с подключения библиотеки TinyMCE и базовой инициализации редактора. Библиотеку можно скачать с официального сайта, подключить из CDN или установить с помощью пакетного менеджера NPM. Кроме того, можно использовать облачную версию. Все способы подключения можно изучить в официальной документации.

В HTML необходимо определить элемент textarea, который будет нашим редактором, а также подключить скрипт TinyMCE и свой скрипт. Результат будет следующим:

<textarea id="editor"></textarea>

<script src="./tinymce.min.js"></script>
<script src="./main.js" type="module"></script>

Теперь создадим файл alerts.js  и напишем в нём минимальный код будущего плагина. Плагин представляем собой функцию обратного вызова, которую можно зарегистрировать в PluginManager. В функцию параметром передается объект текущего редактора. В плагине определим кнопку для панели и команду, которую она будет выполнять.

export const AlertsPlugin = editor => {

    // Выполняемая команда
    editor.addCommand('edit_alert', () => {
        // ...
    });

    // Кнопка с идентификатором 'alerts'
    editor.ui.registry.addToggleButton('alerts', {
        icon    : 'warning',
        text    : 'Alert',
        onAction: () => editor.execCommand('edit_alert'),
    });

};

И в main.js зарегистрируем наш плагин и выполним настройку редактора. Не будем усложнять тестовый пример, просто добавим несколько простых кнопок на тулбар.

import { AlertsPlugin } from './alerts.js';

// Добавим плагин в tinymce с идентификатором 'alerts'
tinymce.PluginManager.add('alerts', AlertsPlugin);

tinymce.init({
    target: document.getElementById('editor'),
    // Укажем идентификатор плагина, чтобы редактор его начал использовать
    plugins: ['code', 'alerts'],
    // Укажем идентификатор кнопки, чтобы редактор её создал
    toolbar : 'code | h2 h3 | bold italic underline | alerts',
    skin    : 'tinymce-5-dark',
    menubar : false,
    branding: false,
    height  : 500,
});

Также, давайте сразу определим внешний вид блока сообщения внутри редактора, потому что обычные дивы никак не выделяются в потоке текста и мы просто не сможем визуально их отличить от обычных параграфов.

Чтобы изменить стилизацию контента в редакторе, нужно подключить к нему таблицу стилей где и описать внешний вид контента. Для этого создадим css-файл, например tinymce-content.css и укажем url-путь к нему в конфигурации редактора. Обратите внимание, что стили, написанные в этом файле, влияют только на окно редактора. Как будет выглядеть этот контент при выводе на сайте, будет зависеть от стилевых таблиц сайта.

tinymce.init({
    // ...
    content_css: './tinymce-content.css',
});

Каким образом стилизовать подобные блоки, полностью зависит от вашей фантазии, мы же для примера ограничимся простыми стилями.

.alert {
    padding: 0 1rem;
    border: 2px solid #ccc;
    border-left-width: 1rem;
}
.alert-info    { border-color: dodgerblue; }
.alert-success { border-color: lawngreen; }
.alert-warning { border-color: chocolate }
.alert-danger  { border-color: crimson }

Это даст нам такое отображение блока в редакторе:

Реализация плагина

Сейчас у нас есть кнопка на панели инструментов которая ничего не делает. Исправим это. Кнопка должна открывать окно настроек для алерта. Данный диалог будет содержать единственую опцию – тип сообщения, и две кнопки - отмена и подтвержение. В случае подтверждения в поле редактора должен будет вставиться новый блок сообщения с выбранным типом, либо измениться тип уже существующего сообщения. Я решил сделать пять типов сообщений: default, info, success, warning, danger.

Для отображения диалога воспользуемся интерфейсом tinymce.WindowManager. Его метод .open() позволяет создать и открыть окно с требуемым набором элементов управления и вода. Оформим это в виде отдельной функции.

const types = [
    { text: 'Простой текст (default)', value: 'default' },
    { text: 'Информация (info)', value: 'info' },
    { text: 'Уведомление (success)', value: 'success' },
    { text: 'Предупреждение (warning)', value: 'warning' },
    { text: 'Важная информация (danger)', value: 'danger' },
];

const openDialog = editor => {
    editor.windowManager.open({
        // Заголовок диалога
        title: 'Информационное сообщение',
        // Содержимое диалога
        body: {
            type: 'panel',
            // Список элементов
            items: [
                {
                    type : 'listbox',      // Выпадающий список
                    name : 'type',         // Имя для доступа из кода
                    label: 'Тип сообщения',// Подпись
                    items: types,          // Список значений в списке
                },
            ],
        },
        // Набор кнопок в диалоге
        buttons: [
            {
                type: 'cancel',
                name: 'cancel',
                text: 'Cancel',
            },
            {
                type   : 'submit',
                name   : 'save',
                text   : 'Save',
                primary: true,
            },
        ],
    });
};

И поместим вызов функции в команду edit_alert

export const AlertsPlugin = editor => {

    // Выполняемая команда
    editor.addCommand('edit_alert', () => {
        // Открываем диалог настроек блока сообщения
        openDialog(editor);
    });

    // ...
};

Чтобы блок появился или обновился в редакторе, нужно добавить обработчик кнопки Save в диалоговом окне настроек. Функция обратного вызова принимает первым параметром объект Api, позволяющий получить введённые в диалоге данные. В данном случае нас интересует поле data.type, содержащее тип сообщения.

const openDialog = editor => {
    editor.windowManager.open({
        // ...
        // Обработчик сохранения
        onSubmit: api => {
            const data = api.getData();
            insertAlertBlock(editor, data.type);
            api.close();
        },
    });
};

Функция вставки/обновления довольна проста. Нужно получить узел под курсором и определить, являемся ли мы внутри существующего блока сообщения или находимся в тексте. В первом случае мы просто обновим css-класс у блока, а во втором – вставим в позицию курсора новый блок. Если мы создаем новый блок сообщения, то дополнительно проверим, есть в редакторе выделенный текст. Если таковой присутствует, то мы поместим его во внутрь созданного сообщения.

const getSelectedAlertBlock = editor =>
    editor.selection.getNode().closest('.alert');

const getSelectedContent = editor => {
    // Получаем выделенный текст
    const selectedContent = editor.selection.getContent().trim();
    return selectedContent.length && selectedContent.startsWith('<')
           ? selectedContent
           : `<p>${selectedContent}</p>`;
};

const insertAlertBlock = (editor, type) => {
    const node = getSelectedAlertBlock(editor);
    if (node) {
        // Если курсор внутри блока, то просто меняем его тип
        node.className = `alert alert-${type}`;
    } else {
        // Если курсор вне блока, то вставляем новый
        editor.insertContent(`<div class="alert alert-${type}">${getSelectedContent(editor)}</div>`);
    }
};

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

const openDialog = editor => {
    const alert = editor.selection.getNode().closest('.alert');
    let currentType = 'default';
    let matches = /alert-(default|info|success|warning|danger)/.exec(alert?.className);
    if (alert && matches) currentType = matches[1];

    editor.windowManager.open({
        // ...
        // Данные для передачи в диалог
        initialData: {
            type: currentType,
        },
    });
};

Остался последний важный момент. Если мы создадим блок последним в документе, то после него не получится вставить курсор и продолжить писать статью. Я, честно говоря, не смог разобраться как правильным образом разрешить данную ситуацию и буду рад подсказкам. Но так как проблему всё равно нужно решить, то немножко поговнокодим =).

Решение будет заключаться в следующем. Повесим слушателя на нажатие клавиши ECS или сочетания CTRL+ENTER. Так как слушатель вешается на весь редактор, то следует проверить, находится ли курсор внутри блока сообщения и только в этом случае выполнять следующие действия. Основная логика заключается в том, чтобы выяснить, является ли блок последним в документе. Если да, то нужно сгенерировать пустой параграф и вставить его после блока, а если нет, то просто установить курсор в следующий элемент.

export const AlertsPlugin = editor => {
    // ...
    editor.on('keydown', function (e) {
        if (e.keyCode === 27 || (e.keyCode === 13 && e.ctrlKey)) {

            const alertBlock = editor.selection ? editor.selection.getNode().closest('.alert') : null;
            if (alertBlock) {
                e.preventDefault();
                const container = alertBlock.parentNode;
                const isLast = alertBlock === container.lastChild;
                let nextElement = alertBlock.nextElementSibling;

                if (isLast) {
                    nextElement = editor.dom.create('p');
                    nextElement.innerHTML = '<br data-mce-bogus>';
                    editor.dom.insertAfter(nextElement, alertBlock);
                }

                const rng = editor.dom.createRng();
                rng.setStart(nextElement, 0);
                rng.setEnd(nextElement, 0);
                editor.selection.setRng(rng);
            }
        }
    });
};

В результате мы получили возможность вставлять в текст вот такие блоки.

Пример редактора с новым плагином можно посмотреть здесь – https://delphinpro.ru/examples/tinymce/alerts-plugin.

Полный код размещен в репозитории на гитхабе – https://github.com/delphinpro/mce-alert-plugin

Вы можете оставить комментарий:

Поддерживается разметка markdown

Комментарий будет опубликован после модерации