Draft.js или что необходимо знать для быстрого старта: личный гайд разработчика
Всем привет, меня зовут Евгений, я являюсь frontend-разработчиком компании Creative. Не так давно я решил прокачаться в работе с Draft.js, и выявил одну неочевидную проблему в инструкциях. Если знакомиться с этим редактором по официальной документации, у новичка, вроде меня, остаётся много вопросов. Это происходит из-за отсутствия структурированного и последовательного объяснения принципов, которые используются “под капотом” библиотеки. Эта статья – мой личный гайд с советами и решениями, который я составил для всех, кто только начинает свой путь в мир Wysiwyg-редакторов.
Итак, начнём. Предлагаю по пунктам разобрать следующие проблемы.
- отсутствие полноценного быстрого старта,
- особенности реализации собственных структур внутри редактора,
- отсутствие настраиваемого формирования стейта для редактора с последующим выходом в html структуру.
Отсутствие полноценного старта
Если обратиться к официальной документации, то всё, что мы получим в качестве быстрого старта, это:
- установка библиотеки,
- импорт необходимых стилей,
- создание стандартной процедуры общения через value и onChange.
А в качестве компонента мы имеем “голое” текстовое поле, которое не несёт никакого практического применения (рис. 1). Если честно, от готовой библиотеки ожидаешь не такого результата. Но давайте разбираться, что можно с этим сделать.
Рисунок 1 – Стандартный компонент Editor
Первое, что необходимо понять, – всё управление редактором лежит на плечах непосредственного пользователя библиотеки. Её создатели предоставляют лишь набор объектов и методов, из которых разработчику необходимо построить собственное управление состоянием.
Разберём этот вывод на примере. Создадим полноценное управление тегами и общим состоянием главного объекта. Результат, который мы ожидаем получить, продемонстрирован на рисунке 2.
В первую очередь, нам нужно создать файл с настройками редактора, где будут храниться элементы стандартного управления, декораторы и, при необходимости, настройки плагинов (рис. 3).
Из названий объектов можно сделать вывод, что здесь будут использоваться два вида управления:
- блочное (генерирует блоки, которые преобразуются в html-теги),
- строчное (генерирует внутри блока специальную структуру, которая позволяет в будущем идентифицировать наличие модификации содержимого):
Далее необходимо создать компонент, который будет отображать непосредственно кнопки для всего, что мы перечислили. Его работа заключается лишь в генерации событий нажатия мыши и отображении пользователю текущего блока, либо модификации. Иными словами, пользователь должен знать, какой блок, либо модификация активирована в данный момент (рис. 4).
Рисунок 4 – Создание Button элемента для переключения блоков
Следующий шаг – создание компонентов блочного и строчного управления. В них нужно передать специальные функции, которые и будут заниматься модификацией состояния Editor (рис. 5, рис. 6).
Рисунок 5 – Создание строчного управления
Рисунок 6 – Создание блочного управления
Важно обратить внимание на следующие части кода, а именно то, что мы передаем в пропс isActive:
- в случае со строчным управлением – мы получаем из общего стейта текущий выбранный стиль и отдаём его на отображение.
- в случае с блочным управлением – с помощью getSelection() мы получаем положение текущего курсора. Он необходим нам определения того, в каком блоке в данный момент мы находимся, а именно – для определения его типа.
Последним шагом станет назначение на кнопки соответствующих функций, которые приготовили разработчики в специальных утилитах (рис. 7).
Рисунок 7 – Функции переключения состояния
После всех этих действий мы получим редактор с полноценным управлением, который можно назвать «быстрым стартом», так как он решает минимальные потребности пользователя.
Особенности реализации собственных структур
В данном редакторе имеются два подхода к созданию собственных структур:
- создание декораторов,
- создание собственного объекта.
Разберём их на примере создания ссылок в тексте и полностью настраиваемого компонента изображения. Для себя я определил следующую структуру, определённую на рисунке 8.
Всё, что должен принимать главный компонент кастомного управления, – это состояние Editor и метод onChange.
Сами компоненты делятся на два вида:
- компонент, отвечающий за добавление кастомной структуры,
- компонент, отвечающий непосредственно за отображение структуры в самом редакторе с последующим, динамическим изменением.
Суть декоратора заключается в том, что мы определяем правило, по которому данные объекты будут идентифицироваться в контексте редактора.
В случае с ссылкой, мы идём по объекту и ищем ключи старта и финиша нашей сущности, которая имеет тип “Link” (рис. 9).
Рисунок 9 – Создание стратегии поиска ссылок в редакторе
Далее мы создаем декоратор и отдаём функции при создании главного стейта редактора. За отображение должен отвечать компонент ссылки (рис. 10). С помощью этого же декоратора компонент получает следующие пропсы:
- contentState (общий стейт объекта, преобразованный в читаемую форму, с помощью которого возможно найти любой блок по ключу или сделать мутацию),
- entityKey (ключ, с помощью которого возможно найти объект, в который мы положим url, введённый с инпута при создании),
- children (в данном случае это текст, который будет выделен и заключён в ссылку).
Рисунок 10 – Создание компонента для декоратора
Переходим к созданию сущности добавления и редактирования ссылки:
- создаём инпут для ввода ссылки,
- создаём функции для добавления и удаления ссылки (рис. 11, рис. 12),
- получаем текущий курсор, проверяем на выделение, получаем стартовый ключ выделения, определяем, является ли выделение объектом с типом “Link”. На основе этого производим редактирование, просмотр и любое возможное действие с ссылкой (рис. 13).
Рисунок 12 – Переменные, необходимые для идентификации и редактирования ссылки
Разберём, что происходит на функциях создания и удаления сущности по пунктам:
- создаём объект, который будет положен в поле data у объекта, которое мы забрали в компоненте декоратора,
- с помощью contentState создаём новую сущность ссылки,
- далее получаем последний ключ, созданный в предыдущем пункте,
- устанавливаем новый contentState для главного инстанса объекта, и получаем новый EditorState,
- назначаем специальной утилите определения ссылок текущий возвращённый EditorState и отдаём его функции onChange.
Так как мы воспользовались утилитой, функция удаления выглядит крайне просто, всё, что необходимо сделать:
- определить, что выделенный текст является ссылкой,
- удалить ссылку с помощью утилиты, отдав текущий state и текущее выделение.
Таким образом с помощью декоратора было создано кастомное редактирование ссылок.
Перейдем к следующему типу создания кастомных блоков, а именно – к созданию собственных объектов. Компонент Editor принимает функцию blockRendererFn(), позволяющую настраивать любые блоки под собственные компоненты (рис. 13). В данном случае, функция позволяет передавать любые пропсы в компоненты, что упрощает редактирование, а также решает важную проблему фокуса своего компонента внутри редактора. Всё, что необходимо сделать, – это объявить новый блок и отдать ему компонент отображения, который мы ожидаем увидеть в редакторе.
Рисунок 13 – Функция определения кастомных блоков
Пропсы, которые мы передаем, возможно забрать через объект blockProps в компоненте отображения, который необходимо отдельно типизировать (рис. 14).
Рисунок 14 – Типизация пропсов компонента, к которым он имеет доступ
В утилитах также имеется специальная функция, позволяющая вставлять «atomic» блоки в contentState (рис. 15, рис. 16). Именно с помощью неё осуществляется вставка кастомных сущностей.
Рисунок 15 – Функция для вставки «atomic» блоков в редактор
Рисунок 16 – Вставка редактируемых изображений в редактор
Последнее, о чём необходимо поговорить в данном разделе, – это удаление «atomic» блоков. Здесь также имеется специальная функция, которую можно обернуть в собственную – для удобства использования(рис.17).
На рисунке 18 изображён пример кастомной функции, в которой выбирается действие – в зависимости от того, на какую кнопку компонента изображения было совершено нажатие. В данном случае – это удаление и редактирование с мутацией объекта data.
Рисунок 18 – Реагирование на изменение состояния компонента и немедленная мутация объекта
Формирование EditorState с помощью html и обратная процедура с полным настраиваем блоков в функции конвертации
Всё, что мы получаем на выходе, – это объект, который будет содержать все сущности редактора, из которых пользователю необходимо написать парсер в удобную для него структуру.
Что может пойти не так? Проблема заключается в том, что обычный пользователь ожидает получить готовый html, и поэтому базовая функция преобразования должна уже существовать в библиотеке. Для себя я выделил библиотеку «draft-convert». Она умеет преобразовывать как вход, так и выход в редактор и, важно заметить, позволяет делать это с учётом всей своей кастомной структуры. Разберём это на примере сущностей, которые были созданы в предыдущем разделе (рис. 19).
Рисунок 19 – Пример конвертации из html в ContentState
Суть в том, чтобы использовать «HtmlElement» и все его свойства. На основе этого нам нужно создать сущности редактора и параллельно сформировать данные, необходимые для создания View этой сущности. Делаем мы это, чтобы забрать объект data и распарсить в пропсы компонента.
Важно заметить: если компонент создавался через «atomic» блок, как изображение, то необходимо прописать это в структуре «htmlToBlock».
Функция обратного преобразования имеет противоположный алгоритм: мы имеем объект с данными, который формируется при создании сущности, и на его основе создаём html (риc. 20).
Рисунок 20 – Пример конвертации из ContentState в html
Подведём итоги. В этой статье я постарался в быстрой форме рассказать все основы, которые потребуются первичному пользователю библиотеки Draft.js. Теперь у вас будет гораздо меньше проблем при знакомстве с этим редактором, и уже в первый час работы вы сможете получить ожидаемые результаты. Буду рад продолжить с вами дискуссию в комментариях!