Проєктуємо гібридний онлайн WYSIWYG-редактор для React

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Привіт! Мене звати Артем, я Full Stack Developer, працюю зазвичай на фрілансі в сфері професійних видавничих систем. Продовжую серію статей про React для обміну досвідом з колегами. Попередня моя стаття з циклу про веброзробку — «Спрощуємо використання React Bootstrap Forms».

Паралельно з розвитком World Wide Web вдосконалювались інструменти для швидкого та зручного створення контенту в форматі HTML. Одним з простих та поширених рішень для цього є онлайн WYSIWYG-редактори. Про їхні останні досягнення і піде мова в цій статті.

Що таке гібридний редактор

Першими почали використовувати класичні онлайн WYSIWYG-редактори, в яких редагування HTML-розмітки здійснюється в одному спеціальному вікні. Зазвичай в його верхній частині розташовується панель з кнопками, за допомогою яких форматують вміст, розташований нижче.

Класичний онлайн WYSIWYG-редактор TinyMCE

З часом розмір та складність HTML-сторінок збільшувались, до них додавались нові елементи, як-от галереї зображень чи віджети зі вставками з інших сайтів. Вони могли мати власні налаштування. Єдиного вікна для редагування всієї сторінки почало бракувати, через що відносно недавно з’явились блокові онлайн WYSIWYG-редактори.

В них елементи редагуються в окремих послідовних блоках, які розміщуються на сторінці вертикально. Кожен з них має власні індивідуальні налаштування залежно від типу блоку (текст, фото, відео тощо). Їх використання дає можливість вставляти в сторінки складні елементи розмітки, які можуть складатись з декількох простих елементів.

Варто зазначити, що в нових версіях деяких класичних редакторів існує можливість розширювати базову функціональність для користувацьких налаштувань елементів. Але зазвичай зі складними елементами це робити не дуже зручно через обмеження безпосередньо одновіконної архітектури редактора. Натомість архітектура блокових редакторів пристосована для зручної роботи зі складними типами блоків, незалежних один від одного.

Блоковий редактор Gutenberg для WordPress

В блокових редакторах використовуються власні інструменти для форматування простих частин контенту, таких як текст, перелік чи таблиця. Переважно їхні можливості значно скромніші в порівнянні з такими ж інструментами в класичних редакторах, розробники яких мають більше досвіду в цих питаннях. Саме для розв’язання цієї проблеми створили гібридні онлайн WYSIWYG-редактори.

Це ті ж самі блокові редактори, в яких для форматування простих елементів (тексту, переліку, таблиці тощо) використовуються класичні WYSIWYG-редактори, інтегровані у блоки відповідних типів. Таким чином ми отримуємо переваги обох типів редакторів в одному редакторі.

Наприклад, текстовий тип блоку в гібридному редакторі не відрізняється за функціональністю форматування тексту від класичного редактора. За винятком того, що в ньому відсутні решта зайвих можливостей, не властивих даному типу блоку. Для кожного типу блоку можна налаштувати власні кнопки форматування з класичного редактора.

Проєктуємо формат даних

Раніше для створення більшості публікацій вистачало використання трьох базових елементів: абзац, підзаголовок та зображення (<p>, <h2> та <img>). З часом в стандарт HTML додавались нові елементи та ускладнювалась сама розмітка статей. Зараз використання трьох елементів буде замало, особливо при створенні складних блоків, які містять в собі декілька елементів. Наприклад, інтеграція в документ зображення з підписом до нього потребує вже трьох елементів: <figure>, <img> та <figcaption>.

Ще краще взяти за приклад «карусель» — набір зображень у вигляді слайдшоу. Наведу мінімальний фрагмент коду, отриманий з офіційної документації Bootstrap, який необхідно вставити в сторінку.

<div id="carouselExample" class="carousel slide">
  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="..." class="d-block w-100" alt="...">
    </div>
    <div class="carousel-item">
      <img src="..." class="d-block w-100" alt="...">
    </div>
    <div class="carousel-item">
      <img src="..." class="d-block w-100" alt="...">
    </div>
  </div>
  <button class="carousel-control-prev" type="button" 
    data-bs-target="#carouselExample" data-bs-slide="prev">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Previous</span>
  </button>
  <button class="carousel-control-next" type="button"
    data-bs-target="#carouselExample" data-bs-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Next</span>
  </button>
</div>

Це найпростіший приклад «каруселі», а якщо додати в неї бажані індикатори слайдів та підписи до них — код стане ще більшим. Зрозуміло, що зберігати розмітку сторінки з елементами керування каруселі у вигляді кнопок та індикаторів було б неправильно.

Перше, що спадає на думку, це розділити самі дані та їхнє представлення. Адже вигляд одних і тих же елементів на різних сторінках одного сайту однаковий. Тому ми можемо зберігати в БД лише тип конкретного елементу, його дані та мінімальні налаштування за потреби. Ось як при цьому виглядатиме запис про попередню «карусель» у форматі JSON:

{
    "id": 1729028962,
    "type": "image",
    "url": [
        "/images/example1.jpg",
        "/images/example2.jpg",
        "/images/example3.jpg"
    ],
    "size": "small"
}

Погодьтесь, такий запис набагато менший ніж в оригінальній розмітці. Звісно, далеко не всі елементи так вигідно спрощуються при конвертації в JSON. Наведу приклад простих блоків у форматі HTML:

<h2 id="ID1728432192">Підзаголовок ...</h2>
<p id="ID1728432209">Текст ...</p>
<img id="ID1729028963" src="/images/example.jpg" class="small" />

А зараз їхній аналог у форматі JSON:

[
    { "id": 1728432192, "type": "header", "text": "Підзаголовок ..." },
    { "id": 1728432209, "type": "text", "text": "Текст ..." },
    { "id": 1729028963, "type": "image", "url": "/images/example.jpg", size: "small" }
]

Відразу помітно, що використання формату JSON для зберігання простих HTML-елементів є надмірним. І це цілком передбачувано, адже збереження даних сторінки в спеціальному форматі, так само як і використання гібридного редактора, найбільш доцільне для складних елементів.

Наприклад, якщо ми до зображення захочемо додати підпис, то, як я згадував раніше, кількість елементів збільшиться:

<figure id="ID1729028963" class="small">
  <img src="/images/example.jpg" />
  <figcaption>Підпис до зображення ...</figcaption>
</figure>

Водночас у форматі JSON додається лише одна додаткова властивість caption:

{
    "id": 1729028963,
    "type": "image",
    "url": "/images/example.jpg",
    "size": "small",
    "caption": "Підпис до зображення ..."
}

А якщо ми захочемо додати до зображення додаткові поля source (джерело зображення) та author (автор зображення) доцільність використання формату JSON для збереження даних стане більш очевидною.

Як ви вже, мабуть, здогадались, формат для збереження даних містить дві обов’язкові властивості: id з міткою часу, отриманою при створенні самого блоку, та type — назвою типу блоку. Унікальний ідентифікатор блоку, в першу чергу, необхідний в циклі React для виводу в атрибуті key. Але з часом виявилось, що його також зручно використовувати як ідентифікатор HTML-елемента для прямого звернення за посиланням до конкретного блоку на сторінці. Решта властивостей є типовими для кожного типу блоку та розробляється індивідуально, залежно від можливостей самого блоку.

Якщо ви захочете після початку використання формату додати нову необов’язкову властивість — це не викличе додаткових проблем. Але якщо спробуєте внести зміни в наявну структуру формату, необхідно буде також внести зміни в усі збережені документи, що містять дані типи блоку. Тому дуже важливо добре продумати властивості наперед, щоб уникнути зайвого клопоту в майбутньому.

Зверніть увагу, що загальні дані документа (дата, заголовок, опис тощо) зберігаються не в блоках, а окремо на найвищому рівні документа для зручного пошуку в БД. Нижче наведено приклад такого запису для БД, у якому, крім загальних властивостей, масив із блоками зберігається у значенні властивості .blocks.

{
  "_id" : ObjectId("61fae1bc6be8f90a409ecddd"),
  "date" : ISODate("2019-04-16T16:42:00.000+0000"),
  "title": "Заголовок статті ...",
  "slug" : "zaholovok-statti ...",
  "image": {
    "url": "/images/example.jpg",
    "source": "Джерело зображення ...",
    "description": "Опис зображення ..."
  },
  "description": "Опис статті ...",
  "blocks": [...],
  "type" : ObjectId("64be947c69a893de210bd7d5"),
  "category": ObjectId("61fae1bb6be8f90a409ecdd6"),
  "tags" : [
        ObjectId("61fae1bb6be8f90a409ecdc5"),
        ObjectId("61fae1bb6be8f90a409ecdbc"),
        ObjectId("61fae1bb6be8f90a409ecdc6")
  ],
  "user": ObjectId("61fae1ba6be8f90a409ecda8"),
  "status" : true
}

Проєктуємо редактор

Щоб простіше пояснити структуру редактора як компонента React, я створив схему. До неї додані частини, які безпосередньо не відносяться до редактора, але допомагають зрозуміти особливості його використання. Стрілками на схемі позначено напрям імпорту модулів та інших частин редактора.

Оскільки редактор використовує підхід WYSIWYG, візуальні стилі блоків в ньому повинні збігатися зі стилями контенту при під час перегляду на сайті. Тому на схемі спільний файл стилів blocks.css фізично розташований на публічній частині сайту (Front-end), але водночас використовується в частині з обмеженим доступом (адміністративній панелі), де й знаходиться редактор (Back-end).

Хоча загалом можливо інтегрувати будь-який класичний WYSIWYG-редактор, для цього конкретного прикладу я задіяв TinyMCE. Щоб він коректно працював у браузері користувача в публічній частині, потрібно створити символічне посилання на теку tinymce, яка знаходиться в теці з пакунками node_modules. Якщо ви плануєте задіяти в редакторі блок Code для використання фрагментів програмного коду, варто створити також додатково посилання на теку модуля react-highlight з назвою highlight для коректної роботи бібліотеки highlight.js.

Тепер перейдемо до вихідних файлів з теки source, з яких формується наш бандл index.js. Зазвичай спочатку використовується стандартний файл index.js, в який імпортується початковий модуль App, який задіює модуль Router. Маршрутизатор, своєю чергою, використовує необхідну сторінку адміністративної частини сайту (Post, Page, Tag тощо) згідно із заздалегідь прописаними маршрутами.

Вже в конкретному розділі панелі керування зазвичай типово імпортують сторінку Index зі списком контенту сайту відповідного типу (публікації, статичні сторінки, мітки тощо). При виборі зі списку якогось рядка відкривається сторінка з редактором даного контенту Editor. Якщо це редактор публікації Post чи статичної сторінки сайту Page, він, окрім безпосередньо гібридного редактора та звичайної форми для загальних полів, містить в собі головний блок Main, в якому можливо редагувати загальні дані документа в режимі WYSIWYG.

Схема модулів гібридного редактора

Безпосередньо гібридний редактор імпортується у вигляді компонента, який складається з головного модуля Editor, модуля виводу блоку Block, окремих модулів для кожного типу блоку (Header, Text, Image, Video тощо), модуля виведення меню блоку Menu та модуля деревоподібного випадаючого переліку пунктів меню для нього Dropdown. Модулі виводу блоків та меню використовують додатково для своєї роботи відповідні редуктори Blocks та Menu, а головний модуль редактора блоків — додатковий файл стилів editor.css, який доповнює загальний спільний файл стилів blocks.css.

Деякі типи блоків можуть використовувати модуль @tinymce/tinymce-react як обгортку для класичного WYSIWYG-редактора TinyMCE, що й перетворює блоковий редактор в гібридний. Також інколи може використовуватись модуль універсального текстового поля Field, який затребуваний в різних типах блоках для редагування додаткових текстових даних.

Зверніть увагу на те, що деколи можуть повторюватись назви модулів. В таких випадках дуже важливо звертати увагу на контекст — шлях до цього файлу, де вказано, в якій теці він знаходиться і, відповідно, до якої частини редактора він належить. Наприклад, модулі з назвою Editor.js трапляються мінімум тричі в проєкті, якщо не враховувати кожен розділ адмінки окремо.

/src/pages/Post/Editor.js
/src/components/Editor.js
/src/components/Editor/Editor.js

У першому випадку йдеться про редактор публікацій як про сторінку адміністративної частини сайту, де їх можна створювати, редагувати чи видаляти. В другому випадку — саме компонент гібридного редактора, який використовується на сторінці редактора публікацій. А в третьому випадку він є обгорткою для класичного WYSIWYG-редактора TinyMCE, яка використовується в деяких блоках гібридного редактора.

Приклад реалізації

В одній зі своїх попередніх статей я згадував Bootstrap як чудове рішення для швидкого створення прототипів сайтів. Тому в цьому проєкті він теж буде задіяний, але як модуль react-bootstrap. Ви, звісно, можете замінити його на будь-який інший інструмент, який вам більше подобається.

Зазвичай для редагування блоків в редакторах використовують додаткову велику бічну панель, в якій присутні багато різноманітних налаштувань для кожного типу блока. Але практика показала, що такий підхід надлишковий для створення публікацій чи звичайних сторінок сайту. В більшості випадків на сайті використовуються уніфіковані елементи та стилі, через що потреба в їх індивідуальних налаштуваннях відпадає.

Наприклад, немає ніякого сенсу налаштовувати різні назви, розміри чи кольори шрифтів для кожного підзаголовка статті індивідуально, інакше контент буде не читабельним. Водночас для конструкторів односторінкових сайтів такий підхід може бути корисним, адже кожен елемент в ньому може бути унікальним з погляду дизайну. Але це вже зовсім інший випадок, для якого необхідний складніший редактор з багатьма додатковими функціями.

Саме через простоту налаштувань блоків я вирішив обмежитись редагуванням їхніх даних в тілі самого блоку та налаштуванням через персональне меню. Воно має базову функціональність, однакову для всіх блоків, окрім перших двох статичних. За його допомогою можливо додавати нові блоки після наявного, переміщати їх у вертикальній площині та видаляти. Окрім цього, кожен тип блоку може додавати власні пункти меню з персональною функціональністю та видаляти стандартні.

Приклад головного блоку гібридного редактора

Так виглядає типовий простий головний блок Main, адаптований для редагування статичної сторінки на сайті. На ній ви можете редагувати тільки заголовок, фонове зображення, його джерело та опис. А вже для редагування статей в нього можна додати поля на зразок дати публікації, категорії чи автора.

На наступному зображенні ви можете побачити відразу декілька блоків для роботи з текстом (вступ, підзаголовок, текст, таблиця, перелік). Через те, що перший блок є статичним та обов’язковим, з його меню можливо лише додати наступний блок.

Приклад текстових блоків гібридного редактора

На прикладі редагування тексту блоку таблиця видно додаткове меню WYSIWYG-редактора TinyMCE. Це типовий зразок використання класичного редактора в середині блокового. Окрім блоків з прикладу вище, він також зустрічається в таких блоках, як цитата та адреса. Зверніть увагу, що в цьому випадку окрім стандартної для TinyMCE верхньої панелі форматування тексту присутня додаткова нижня панель для редагування самої таблиці, яка використовується лише для даного типу блоку.

Приклад блоку з зображенням гібридного редактора

Блок зображення додає саме зображення та необов’язкові додаткові дані внизу блоку, як-от опис зображення, джерело та автора в його лівому та правому кутах відповідно. В меню цього та декількох інших блоків з’являється додатковий пункт розмір з типовими значеннями. Якщо при завантаженні з накопичувача обрати лише один файл, то він вставиться як звичайне одинарне зображення, наведене в прикладі вище. Але якщо обрати відразу декілька файлів, блок виглядатиме як «карусель» зображень з додатковими функціями керування переглядом, як у прикладі нижче.

Приклад блоку з зображеннями гібридного редактора

Функціональність блоків відео (YouTube), Фейсбук, Твіттер, мапа (Google), звук (SoundCloud) настільки прості, що не бачу сенсу зупинятись на них детальніше. В блок достатньо вставити фрагмент коду HTML, взятого з відповідного сервісу, та він автоматично інтегрується в сторінку. Додатково можна використати деякі налаштування за їх наявності.

Єдине, на що варто звернути увагу — блокові та гібридні редактори краще за класичні, пристосовані для використання сервісу Mash-up з технології Web 2.0. Особливо коли ці сервіси мають багато додаткових налаштувань саме для їхньої інтеграції в користувацький сайт. Але, на жаль, таких сервісів не так багато, як хотілось би.

Приклад блоку з програмним кодом гібридного редактора

Натомість блок код дуже цікавий для програмістів функціональністю підсвітки коду. Для його реалізації використовується бібліотека highlight.js через модуль react-highlight, про що я згадував раніше. Для редагування коду достатньо клікнути на елемент з кодом, тоді він перемкнеться в режим редагування без підсвітки. Після внесення змін достатньо клікнути за межами елемента редагування коду й він автоматично повернеться в режим перегляду з підсвіткою коду.

Мова програмного коду визначається автоматично за допомогою алгоритмів самої бібліотеки. Відповідно й підсвітка обирається автоматично для кожної мови. Але деколи автовизначення може помилятись, особливо якщо фрагмент коду невеликий. Тому мову можна обрати вручну з наперед підібраного та відсортованого для зручності переліку мов.

До речі, як приклад фрагмента коду в ілюстрації використовується реальний програмний код з модуля найпростішого блоку підзаголовок. Якщо комусь потрібно вставляти більше одного рівня підзаголовка (<h3>, <h4>, <h5>), цей блок можна легко модернізувати, додавши пункт меню з переліком рівнів вкладеності підзаголовків. Та адаптувати вивід самого елемента за допомогою динамічного створення.

Демоверсія на GitHub

Скільки б я не описував переваги редактора, найефективніший спосіб це перевірити — ознайомитись з ним, практично користуючись деякий час. І це легко зробити на безплатному сервісі GitHub Pages, де я розмістив робочу онлайн демоверсію гібридного WYSIWYG-редактора для React з детальним описом його блоків та мінімальними обмеженнями за функціональністю. З того ж GitHub можна завантажити його початковий код для повнофункціонального використання на власних проєктах.

Реалізація редактора у вигляді автономного універсального модуля, який необхідно лише приєднати та користуватись, дуже спрощує його інтеграцію в робочий проєкт. Водночас такий підхід ускладнює його код та обмежує можливості адаптації до проєкту, в якому він використовується. Через це код редактора зі статті поданий у вигляді простого шаблону, який можна використати як основу для створення власного редактора.

Першочергово він буде корисним професійним інтернет-виданням та іншим проєктам з великою кількістю статичних сторінок. Розробники можуть максимально адаптувати його для своїх потреб та додати власні блоки. Звісно, його також можна використовувати й на невеликих сайтах, але це може виявитись занадто трудомістким в порівнянні з готовими рішеннями. Відповідно, не доцільним. Хіба що хтось з ентузіастів захоче поекспериментувати для загального розвитку чи просто з цікавості.

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

чи не буде проблем з API/json з додаванням кількох «важких» зображень (більше 5 мб, кожне)?

Самі зображення у форматі JSON не зберігаються, тільки посилання на них, аналогічно до формату HTML. Для зберігання на стороні сервера окремо використовую API на Node.js — проблем з розміром чи кількістю файлів не виявив. По ньому на ДОУ є інша моя стаття Автоматизуємо використання адаптивних зображень для вебсайтів за допомогою Node.js

Підписатись на коментарі