Draft.js или что необходимо знать для быстрого старта: личный гайд разработчика

Draft.js или что необходимо знать для быстрого старта: личный гайд разработчика

Всем привет, меня зовут Евгений, я являюсь frontend-разработчиком компании Creative. Не так давно я решил прокачаться в работе с Draft.js, и выявил одну неочевидную проблему в инструкциях. Если знакомиться с этим редактором по официальной документации, у новичка, вроде меня, остаётся много вопросов. Это происходит из-за отсутствия структурированного и последовательного объяснения принципов, которые используются “под капотом” библиотеки. Эта статья – мой личный гайд с советами и решениями, который я составил для всех, кто только начинает свой путь в мир Wysiwyg-редакторов.

Итак, начнём. Предлагаю по пунктам разобрать следующие проблемы.

  • отсутствие полноценного быстрого старта,
  • особенности реализации собственных структур внутри редактора,
  • отсутствие настраиваемого формирования стейта для редактора с последующим выходом в html структуру.

Отсутствие полноценного старта

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

  • установка библиотеки,
  • импорт необходимых стилей,
  • создание стандартной процедуры общения через value и onChange.

А в качестве компонента мы имеем “голое” текстовое поле, которое не несёт никакого практического применения (рис. 1). Если честно, от готовой библиотеки ожидаешь не такого результата. Но давайте разбираться, что можно с этим сделать.

<p><i>Рисунок 1 – Стандартный компонент Editor</i></p>

Рисунок 1 – Стандартный компонент Editor

Первое, что необходимо понять, – всё управление редактором лежит на плечах непосредственного пользователя библиотеки. Её создатели предоставляют лишь набор объектов и методов, из которых разработчику необходимо построить собственное управление состоянием.

Разберём этот вывод на примере. Создадим полноценное управление тегами и общим состоянием главного объекта. Результат, который мы ожидаем получить, продемонстрирован на рисунке 2.

<i>Рисунок 2 – Управление состоянием редактора</i>
Рисунок 2 – Управление состоянием редактора

В первую очередь, нам нужно создать файл с настройками редактора, где будут храниться элементы стандартного управления, декораторы и, при необходимости, настройки плагинов (рис. 3).

Из названий объектов можно сделать вывод, что здесь будут использоваться два вида управления:

  • блочное (генерирует блоки, которые преобразуются в html-теги),
  • строчное (генерирует внутри блока специальную структуру, которая позволяет в будущем идентифицировать наличие модификации содержимого):
<i>Рисунок 3 – Файл с настройками редактора</i>
Рисунок 3 – Файл с настройками редактора

Далее необходимо создать компонент, который будет отображать непосредственно кнопки для всего, что мы перечислили. Его работа заключается лишь в генерации событий нажатия мыши и отображении пользователю текущего блока, либо модификации. Иными словами, пользователь должен знать, какой блок, либо модификация активирована в данный момент (рис. 4).

<p><i>Рисунок 4 – Создание Button элемента для переключения блоков</i></p>

Рисунок 4 – Создание Button элемента для переключения блоков

Следующий шаг – создание компонентов блочного и строчного управления. В них нужно передать специальные функции, которые и будут заниматься модификацией состояния Editor (рис. 5, рис. 6).

<p><i>Рисунок 5 – Создание строчного управления</i></p>

Рисунок 5 – Создание строчного управления

<p><i>Рисунок 6 – Создание блочного управления</i></p>

Рисунок 6 – Создание блочного управления

Важно обратить внимание на следующие части кода, а именно то, что мы передаем в пропс isActive:

  • в случае со строчным управлением – мы получаем из общего стейта текущий выбранный стиль и отдаём его на отображение.
  • в случае с блочным управлением – с помощью getSelection() мы получаем положение текущего курсора. Он необходим нам определения того, в каком блоке в данный момент мы находимся, а именно – для определения его типа.

Последним шагом станет назначение на кнопки соответствующих функций, которые приготовили разработчики в специальных утилитах (рис. 7).

<p><i>Рисунок 7 – Функции переключения состояния</i></p>

Рисунок 7 – Функции переключения состояния

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

Особенности реализации собственных структур

В данном редакторе имеются два подхода к созданию собственных структур:

  • создание декораторов,
  • создание собственного объекта.

Разберём их на примере создания ссылок в тексте и полностью настраиваемого компонента изображения. Для себя я определил следующую структуру, определённую на рисунке 8.

<i>Рисунок 8 – Структура создания собственных компонентов</i>
Рисунок 8 – Структура создания собственных компонентов

Всё, что должен принимать главный компонент кастомного управления, – это состояние Editor и метод onChange.

Сами компоненты делятся на два вида:

  • компонент, отвечающий за добавление кастомной структуры,
  • компонент, отвечающий непосредственно за отображение структуры в самом редакторе с последующим, динамическим изменением.

Суть декоратора заключается в том, что мы определяем правило, по которому данные объекты будут идентифицироваться в контексте редактора.

В случае с ссылкой, мы идём по объекту и ищем ключи старта и финиша нашей сущности, которая имеет тип “Link” (рис. 9).

<p><i>Рисунок 9 – Создание стратегии поиска ссылок в редакторе</i></p>

Рисунок 9 – Создание стратегии поиска ссылок в редакторе

Далее мы создаем декоратор и отдаём функции при создании главного стейта редактора. За отображение должен отвечать компонент ссылки (рис. 10). С помощью этого же декоратора компонент получает следующие пропсы:

  • contentState (общий стейт объекта, преобразованный в читаемую форму, с помощью которого возможно найти любой блок по ключу или сделать мутацию),
  • entityKey (ключ, с помощью которого возможно найти объект, в который мы положим url, введённый с инпута при создании),
  • children (в данном случае это текст, который будет выделен и заключён в ссылку).
<p><i>Рисунок 10 – Создание компонента для декоратора</i></p>

Рисунок 10 – Создание компонента для декоратора

Переходим к созданию сущности добавления и редактирования ссылки:

  • создаём инпут для ввода ссылки,
  • создаём функции для добавления и удаления ссылки (рис. 11, рис. 12),
  • получаем текущий курсор, проверяем на выделение, получаем стартовый ключ выделения, определяем, является ли выделение объектом с типом “Link”. На основе этого производим редактирование, просмотр и любое возможное действие с ссылкой (рис. 13).
<i>Рисунок 11 – Функция добавления ссылки в стейт Editor</i>
Рисунок 11 – Функция добавления ссылки в стейт Editor
<p><i>Рисунок 12 – Переменные, необходимые для идентификации и редактирования ссылки</i></p>

Рисунок 12 – Переменные, необходимые для идентификации и редактирования ссылки

Разберём, что происходит на функциях создания и удаления сущности по пунктам:

  • создаём объект, который будет положен в поле data у объекта, которое мы забрали в компоненте декоратора,
  • с помощью contentState создаём новую сущность ссылки,
  • далее получаем последний ключ, созданный в предыдущем пункте,
  • устанавливаем новый contentState для главного инстанса объекта, и получаем новый EditorState,
  • назначаем специальной утилите определения ссылок текущий возвращённый EditorState и отдаём его функции onChange.

Так как мы воспользовались утилитой, функция удаления выглядит крайне просто, всё, что необходимо сделать:

  • определить, что выделенный текст является ссылкой,
  • удалить ссылку с помощью утилиты, отдав текущий state и текущее выделение.

Таким образом с помощью декоратора было создано кастомное редактирование ссылок.

Перейдем к следующему типу создания кастомных блоков, а именно – к созданию собственных объектов. Компонент Editor принимает функцию blockRendererFn(), позволяющую настраивать любые блоки под собственные компоненты (рис. 13). В данном случае, функция позволяет передавать любые пропсы в компоненты, что упрощает редактирование, а также решает важную проблему фокуса своего компонента внутри редактора. Всё, что необходимо сделать, – это объявить новый блок и отдать ему компонент отображения, который мы ожидаем увидеть в редакторе.

<p><i>Рисунок 13 – Функция определения кастомных блоков</i></p>

Рисунок 13 – Функция определения кастомных блоков

Пропсы, которые мы передаем, возможно забрать через объект blockProps в компоненте отображения, который необходимо отдельно типизировать (рис. 14).

<p><i>Рисунок 14 – Типизация пропсов компонента, к которым он имеет доступ</i></p>

Рисунок 14 – Типизация пропсов компонента, к которым он имеет доступ

В утилитах также имеется специальная функция, позволяющая вставлять «atomic» блоки в contentState (рис. 15, рис. 16). Именно с помощью неё осуществляется вставка кастомных сущностей.

<p><i>Рисунок 15 – Функция для вставки «atomic» блоков в редактор</i></p>

Рисунок 15 – Функция для вставки «atomic» блоков в редактор

<p><i>Рисунок 16 – Вставка редактируемых изображений в редактор</i></p>

Рисунок 16 – Вставка редактируемых изображений в редактор

Последнее, о чём необходимо поговорить в данном разделе, – это удаление «atomic» блоков. Здесь также имеется специальная функция, которую можно обернуть в собственную – для удобства использования(рис.17).

Рисунок 17 – Функция удаления «atomic» блока
Рисунок 17 – Функция удаления «atomic» блока

На рисунке 18 изображён пример кастомной функции, в которой выбирается действие – в зависимости от того, на какую кнопку компонента изображения было совершено нажатие. В данном случае – это удаление и редактирование с мутацией объекта data.

<p><i>Рисунок 18 – Реагирование на изменение состояния компонента и немедленная мутация объекта</i></p>

Рисунок 18 – Реагирование на изменение состояния компонента и немедленная мутация объекта

Формирование EditorState с помощью html и обратная процедура с полным настраиваем блоков в функции конвертации

Всё, что мы получаем на выходе, – это объект, который будет содержать все сущности редактора, из которых пользователю необходимо написать парсер в удобную для него структуру.

Что может пойти не так? Проблема заключается в том, что обычный пользователь ожидает получить готовый html, и поэтому базовая функция преобразования должна уже существовать в библиотеке. Для себя я выделил библиотеку «draft-convert». Она умеет преобразовывать как вход, так и выход в редактор и, важно заметить, позволяет делать это с учётом всей своей кастомной структуры. Разберём это на примере сущностей, которые были созданы в предыдущем разделе (рис. 19).

<p><i>Рисунок 19 – Пример конвертации из html в ContentState</i></p>

Рисунок 19 – Пример конвертации из html в ContentState

Суть в том, чтобы использовать «HtmlElement» и все его свойства. На основе этого нам нужно создать сущности редактора и параллельно сформировать данные, необходимые для создания View этой сущности. Делаем мы это, чтобы забрать объект data и распарсить в пропсы компонента.

Важно заметить: если компонент создавался через «atomic» блок, как изображение, то необходимо прописать это в структуре «htmlToBlock».

Функция обратного преобразования имеет противоположный алгоритм: мы имеем объект с данными, который формируется при создании сущности, и на его основе создаём html (риc. 20).

<p><i>Рисунок 20 – Пример конвертации из ContentState в html</i></p>

Рисунок 20 – Пример конвертации из ContentState в html

Подведём итоги. В этой статье я постарался в быстрой форме рассказать все основы, которые потребуются первичному пользователю библиотеки Draft.js. Теперь у вас будет гораздо меньше проблем при знакомстве с этим редактором, и уже в первый час работы вы сможете получить ожидаемые результаты. Буду рад продолжить с вами дискуссию в комментариях!

99