Время прочтения — 10 минут
Как подключить EditorJS в admiral?
Содержание
Всем привет! Напоминаем, что мы разрабатываем Admiral — решение с открытым исходным кодом на React, которое позволяет быстро разрабатывать красивые CRUDы в админ-панелях, но при желании создавать полностью кастомные интерфейсы. От разработчиков для разработчиков с любовью, как говорится.
Сегодня на примере Editor JS мы покажем, как легко и быстро подключить редактор в проект, созданный в Admiral. Это блочный редактор с универсальным выводом JSON, парсинг которого легко настроить на свой вкус. По-умолчанию Editor JS выглядит достаточно ограниченным, но его фишка — в легком расширении функционала с помощью плагинов, которые можно написать самостоятельно.
• Официально поддерживаемые плагины есть здесь.
• Живую демонстрацию смотрите на официальном сайте.
• Demo всего процесса, который я опишу ниже, можно посмотреть тут.
• Официально поддерживаемые плагины есть здесь.
• Живую демонстрацию смотрите на официальном сайте.
• Demo всего процесса, который я опишу ниже, можно посмотреть тут.
Поехали.
Необходимые зависимости
Устанавливаем сам редактор и несколько плагинов:
1
@editorjs/editorjs
2
@editorjs/header
3
@editorjs/image
4
@editorjs/paragraph
Типизация
Если вы будете использовать TS в проекте, потребуется установить описание типов для каждого плагина, однако существуют они не все.
Пакеты с типизацией называются @types/editorjs__image и устанавливаются как yarn add -D @types/editorjs__image или npm i -D @types/editorjs__image
Если пакета с типизацией нет, либо вам просто лень устанавливать дополнительные пакеты, необходимо вручную декларировать пакет. Для этого в корне проекта создайте файл global.d.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
Стили можно создавать в случае необходимости по собственному усмотрению.
Использование компонента выглядит так:
<EditorJSInput
required
imageUploadUrl={apiUrl + '/editor-upload'}
label="Контент"
columnSpan={2}
name="content"
/>
На данном этапе интеграция редактора в админку завершена.
Обработка конечных данных на клиенте
Осталось разобраться, как обрабатывать получаемые из редактора данные.
Конечные данные имеют следующую структуру:
Конечные данные имеют следующую структуру:
{
"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;
Применение компонента:
<EditorJsParser data={editorData} />
В SCSS/CSS файле можно задать любые стили, которые будут применяться к итоговому html.
Подводим итоги
Когда вы установите все необходимые зависимости и добавите несколько простых компонентов в свой проект, легко сможете подключить современный редактор в своё приложение и пользоваться всеми его преимуществами. Задавать любые стили, подключать доступные плагины и расширять функционал. А еще можно написать собственные плагины и внести вклад в развитие аутсорс продукта — попробуйте, это приятно :)
Читайте также
Обзоры
15 декабря 2023
13 минут
В чем суть RFID и NFC? И как их использовать в фудтех индустрии?
Бесконтактные технологии идентификации, используемые в фудтехе
Разработка
9 декабря 2023
13 минут
React Native Splash Screen - поддержка разных тем
Установка Splash Screen в React Native: гайд, кросс-платформенность, темы.
Разработка
15 сентября 2023
14 минут
Family Frontend Meetup #1
Обновления CSS и Next.js, принципы доступности веба, динамическое масштабирование шрифтов и эффективная типизация для redux-thunk
Разработка
5 мая 2023
9 минут
Миграция из Kubernetes в Docker Compose
Наш альтернативный подход по организации процесса развертывания тестового окружения