strokoff

Пишем веб-компонент WYSiWYG редактора. Релиз v.1

веб-компонент wysiwyg редактора

В первой части статьи, я познакомил и рассказал про ход разработки веб-компонента WYSIWYG редактора wc-wysiwyg. В этой части рассмотрим, как изменился компонент с момента публичного тестирования версии 0.9, что вошло в недостающие 0.1, чтобы моя совесть позволяла честно говорить, что это готовый к интеграции веб-компонент, который прошел через публичное тестирование\ревью веб-разработчиками и его в целом не стыдно использовать в проекте

Визуальные изменения

Первое важное изменение, это стилизация кнопок под тот вид, в котором они будут вставлены в редактор, многие не понимают значения тегов и стилевая дифференция помогает пользователям проще отличать, зачем нужен тот или иной тег + всплывющие css хинты не доступны для мобильных пользователей

Большинство из тех, кто попробовал пользоваться редактором постоянно какое-то время, отметили, что sticky позиционирование было бы удобнеее для работы с большими текстами и я в целом согласен с таким мнением, в итоге я полностью убрал логику с InlineDialogElement для редактора и перенес все элементы в header редактора.

Функциональные изменения

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

Таблица emoji

Реализация таблицы эмодзи интересна тем, что список доступных символов насчитывает почти 1000 элементов и нам очень нежелательно на фронте сильно нагружать клиента.

Пробуем в оптимизацию, чтобы показать список emoji нам необходимо показать на клиенте 1000 элементов. Первой мыслью конечно же дальше пользовать функцию el, но если взглянуть на ситуацию поближе, то получается, что в цикле я буду вызывать функцию с деструктуризацией и ифчиками которые в свою очередь еще будут вызывать функции document.createElement в котором я каждой кнопке могу определить event handler для события onpointerup в котором будет функция копирования в буфер — слишком сложно. В итоге 1000 элементов + 1000 слушателей. Ради любопытства я все-таки измерил с помощью window.perfomance скорость сборки элемента диалогового окна и на моем MacBook Pro 13, 2017 —  у которого на борту 2,3 GHz 2‑ядерный процессор Intel Core i5 — скорость получалась в районе ±100мс, что уже вызывает дискомфорт тк такой лаг многие пользователи могут ощутить.

append: [
    ...(()=>{
        const emojiEls:Element[] = [];
        for (let range in emojiRanges) {
            const rangeEl = el('section', {
                props: {
                    innerHTML: _t(range),
                },
            })
            for (let emojiCode = emojiRanges[range][0]; emojiCode < emojiRanges[range][1]; emojiCode++) {
                rangeEl.append(
                    el('button', {
                        props: {
                            innerHTML: `&#${emojiCode};`,
                            //Копируем emoji код в буфер обмена по клику
                            onpointerup: event => {
                                const data = [
                                    new ClipboardItem({ "text/html": new Blob([`&#${emojiCode};`], { type:"text/html" }) }),
                                ];
                                navigator.clipboard.write(data).then(
                                    () => {
                                        this.Dialog.close();
                                    },
                                    (err) => { throw new Error("WC-WYSIWYG: Copy emoji to clipboard failed", err); }
                                );
                            }
                        },
                        attrs: {
                            "data-hint": _t('action'),
                        },
                        styles: {
                            fontSize: '20px'
                        },
                    })
                );
            }
            emojiEls.push(rangeEl);
        }
        return emojiEls
    })()
]

Чтобы сделать рендер еще быстрее мне на ум приходят только строковые шаблоны, мы не будем вызвать el, просто склеим в строку верстку и разово вставим ее в innerHTML нашего диалогового окна.

//Слушатель нажатия на кнопку переехал на элемент выше
onpointerup: event => {
    const target = event.target;
    if(target.tagName === 'BUTTON') {
        const emojiCode = target.innerHTML;
        const data = [
            new ClipboardItem({ "text/html": new Blob([emojiCode], { type:"text/html" }) }),
        ];
        navigator.clipboard.write(data).then(
            () => {
                this.Dialog.close();
            },
            (err) => { throw new Error("WC-WYSIWYG: Copy emoji to clipboard failed", err); }
        );
    }
},
//Строковый рендер списка emoji
innerHTML: (() => {
    let html = '';
    for (let range in emojiRanges) {
        html += '<figure class="wc-wysiwyg_ed"><figcaption>' + _t(range) + '</figcaption>';
        for (let emojiCode = emojiRanges[range][0]; emojiCode < emojiRanges[range][1]; emojiCode++) {
            html += '<button class="wc-wysiwyg_btn -emoji" data-hint="' + _t('action') + '">&#'+emojiCode+';</button>';
        }
        html += '</figure>';
    }
    return html;                        
})()

Кода стало значительно меньше, теперь это просто строки, никаких слушателей у button нет. Слушатель переехал в button.parentElement и теперь он один на все кнопки. Этот код у меня уже выполняется за ±70мс. Как еще ускорить рендер такого списка и войти например в диапазон в 16ms предлагаю обсудить в комментариях

Выбор цвета фона и текста

Одна из самых частых спрашиваемых функций, это возможность покраски текста «изкоробки» через кнопки, а не стили. Я решил далеко не отходить от выбранной цветовой схемы и перенес по аналогии с таблицей emoji, цветовую таблицу из https://materialui.co/colors/ 

Заранее готовые стили

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

Поддержка Vite

Один из фронтенд разработчиков обратился ко мне с просьбой пояснить, как я вел локальную разработку и что нужно, чтобы вести разработку своего форка. Т.к. изначально dev work flow я не предусматривал для разработчиков, точнее в репозитории есть только команды сборки, а так чтобы выполнить популярный `npm run dev` и на локалхосте отлаживать — такого не было. Так родилась команда `npm run vite` которая запустит вам Vite сборщик и предложит перейти на локальный адрес для дальнейшей разработки и поддержки

Оптимизация работы с window.getSelection()

/**
     * Get selection info from editor
     * @returns editorSelection
     */
    getSelection() {
        const windowSelection = window.getSelection();
        const editorSelection = {
            selection: null,
            element: null,
            text: null,
            node: null,
        } as WCWYSIWYGSelection;

        if(windowSelection !== null) {
            editorSelection.selection = windowSelection;
            editorSelection.node = windowSelection.anchorNode;
            editorSelection.element = windowSelection.anchorNode.parentElement;
            let selectionText = windowSelection.toString();
            if(selectionText.length > 0) {
                editorSelection.text = selectionText;
            }
        }
        console.log('getSelection', editorSelection);
        this.EditorSelection = editorSelection;
        return editorSelection;
    }

Обновил работу с текущим выделением в редакторе, теперь выделение сохраняется в инстансе редактора и доступно другим расширениям, Количество вызовов window.getSelection() сократилось до 1 вызова на каждый клик в EditorNode блоке.


Последняя редакция 31 мая, 2023 в 09:05

371
9 мин