CSS and Next.js updates, web accessibility principles, dynamic font scaling and efficient typing for redux-thunk
15 minutes
Tools
Admiral
Dec 19, 2023
10 minutes
declare module '@editorjs/header'
declare module '@editorjs/image'
declare module '@editorjs/paragraph'
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)
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<EditorJS | null>(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"
}
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 <div className={styles.content}>{parse(data)}</div>;
};
export default EditorJsParser;
<EditorJsParser data={editorData} />