Как подключить EditorJS в Admiral?

Всем привет! Напоминаем, что мы разрабатываем Admiral — решение с открытым исходным кодом на React, которое позволяет быстро разрабатывать красивые CRUDы в админ-панелях, но при желании создавать полностью кастомные интерфейсы. От разработчиков для разработчиков с любовью, как говорится.

Сегодня на примере Editor JS мы покажем, как легко и быстро подключить редактор в проект, созданный в Admiral. Это блочный редактор с универсальным выводом JSON, парсинг которого легко настроить на свой вкус. По-умолчанию Editor JS выглядит достаточно ограниченным, но его фишка — в легком расширении функционала с помощью плагинов, которые можно написать самостоятельно.

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

Поехали.

Необходимые зависимости

Устанавливаем сам редактор и несколько плагинов:

  1. @editorjs/editorjs
  2. @editorjs/header
  3. @editorjs/image
  4. @editorjs/paragraph

Типизация

Если вы будете использовать TS в проекте, потребуется установить описание типов для каждого плагина, однако существуют они не все.

Если пакета с типизацией нет, либо вам просто лень устанавливать дополнительные пакеты, необходимо вручную декларировать пакет. Для этого в корне проекта создайте файл global.d.ts со следующим содержимым:

declare module '@editorjs/header' declare module '@editorjs/image' declare module '@editorjs/paragraph'

Интеграция в проект с Admiral

Осталось добавить адаптированные под admiral компоненты с интеграцией Editor JS.

Файловая структура компонента выглядит так:

Контейнер над самим редактором для взаимодействия с хуком useForm(), который используем внутри admiral — EditorJSContainer:

import React, { memo, useCallback } from 'react' import EditorJS, { EditorConfig, OutputData } from '@editorjs/editorjs' import { Form, FormItemProps, useForm } from '@devfamily/admiral' import '@/assets/editor.scss' import EditorJSInput from '../EditorJSField' type UploadResponseFormat = { success: 1 | 0; file: { url: string } } interface EditorProps extends Omit<EditorConfig, 'onChange' | 'holder'> { isFetching: boolean value: OutputData holder?: string imageUploadUrl?: string imageUploadField?: string onImageUpload?: (file: Blob) => Promise<UploadResponseFormat> onChange: (value: OutputData) => void } type Props = Partial<Omit<EditorProps, 'value'≶> & FormItemProps & { name: string } function EditorJSContainer({ name, label, required, columnSpan, ...rest }: Props) { const { values, errors, isFetching, setValues } = useForm() const value = values[name] const error = errors[name]?.[0] const onChange = (value: OutputData) => { setValues((values) => ({ ...values, [name]: value })) } // prevent reopen when close picker by clicking on label const onLabelClick = useCallback((e) => { e?.preventDefault() }, []) return ( <Form.Item label={label} required={required} error={error} columnSpan={columnSpan} onLabelClick={onLabelClick} > <EditorJSInput value={value} onChange={onChange} isFetching={isFetching} {...rest} /> </Form.Item> ) } export default memo(EditorJSContainer)

Компонент, внутри которого инициализируется и настраивается сам редактор — EditorJSField.

import React, { memo, useCallback, useEffect, useRef } from 'react'; import EditorJS, { EditorConfig, OutputData } from '@editorjs/editorjs'; import { Form, FormItemProps, useForm } from '@devfamily/admiral'; import styles from './editorJsInput.module.scss'; import '@/assets/editor.scss'; import { EDITOR_TOOLS } from './EditorTools'; const defaultHolder = 'editorjs-container'; type UploadResponseFormat = { success: 1 | 0; file: { url: string } }; interface EditorProps extends Omit<EditorConfig, 'onChange' | 'holder'> { isFetching: boolean; value: OutputData; onChange: (value: OutputData) => void; onImageUpload?: (file: Blob) => Promise<UploadResponseFormat> holder?: string; imageUploadUrl?: string; imageUploadField?: string; } function EditorJSField({ isFetching, value, holder = defaultHolder, minHeight = 300, onChange, imageUploadUrl, imageUploadField, onImageUpload, tools, ...rest }: EditorProps) { const ref = useRef(null); useEffect(() => { if (!ref.current && !isFetching) { const editor = new EditorJS({ holder, tools: tools ?? { ...EDITOR_TOOLS, image: { ...EDITOR_TOOLS.image, config: { endpoints: { byFile: imageUploadUrl, }, field: imageUploadField, uploader: { uploadByFile: onImageUpload, }, }, }, }, data: value, minHeight, async onChange(api) { const data = await api.saver.save(); onChange(data); }, ...rest, }); ref.current = editor; } return () => { ref.current?.destroy(); ref.current = null; }; }, [isFetching]); return ( <section className={styles.section}> <div id={holder} /> </section> ) } export default EditorJSField

Стили можно создавать в случае необходимости по собственному усмотрению.

Использование компонента выглядит так:

На данном этапе интеграция редактора в админку завершена.

Обработка конечных данных на клиенте

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

Конечные данные имеют следующую структуру:

{ "time": 1550476186479, "blocks": [ { "id": "oUq2g_tl8y", "type": "header", "data": { "text": "Editor.js", "level": 2 } }, { "id": "zbGZFPM-iI", "type": "paragraph", "data": { "text": "Hey. Meet the new Editor.." } }, { "id": "XV87kJS_H1", "type": "list", "data": { "style": "unordered", "items": [ "It is a block-styled editor", "It returns clean data output in JSON", "Designed to be extendable and pluggable with a simple API" ] } }, ], "version": "2.8.1" }

Вы можете парсить данные по собственному усмотрению. Мы используем библиотеку html-react-parser. Вот, как выглядит тогда компонент (в наиболее простом варианте):

import parse from 'html-react-parser'; import styles from './EditorJSParser.module.scss'; type TBlock = { id: string; type: string; data: T }; export type EditorJSData = { blocks: TBlock[]; time: string; version: string; }; type EditorJsProps = { data: EditorJSData }; const EditorJsParser = ({ data }: EditorJsProps) => { return {parse(data)}</div> }; export default EditorJsParser;

Применение компонента:

В SCSS/CSS файле можно задать любые стили, которые будут применяться к итоговому html.

Подводим итоги

Когда вы установите все необходимые зависимости и добавите несколько простых компонентов в свой проект, легко сможете подключить современный редактор в своё приложение и пользоваться всеми его преимуществами. Задавать любые стили, подключать доступные плагины и расширять функционал. А еще можно написать собственные плагины и внести вклад в развитие аутсорс продукта — попробуйте, это приятно :)

0
Комментарии
-3 комментариев
Раскрывать всегда