Skip to content

Commit

Permalink
ADM-182 (#105)
Browse files Browse the repository at this point in the history
ADM-182; (Feat) Add Tips & Tricks page and Editor.js
  • Loading branch information
arturvolokhin authored Nov 13, 2023
1 parent d60c464 commit 9f28984
Show file tree
Hide file tree
Showing 15 changed files with 718 additions and 68 deletions.
4 changes: 4 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '@editorjs/header'
declare module '@editorjs/image'
declare module '@editorjs/paragraph'
declare module '@editorjs/nested-list'
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
"@consta/uikit": "^3.18.0",
"@dnd-kit/core": "^5.0.3",
"@dnd-kit/sortable": "^6.0.1",
"@editorjs/editorjs": "2.26.5",
"@editorjs/header": "^2.8.1",
"@editorjs/image": "^2.8.2",
"@editorjs/nested-list": "^1.3.0",
"@editorjs/paragraph": "^2.11.3",
"@tinymce/tinymce-react": "^4.0.0",
"@tippyjs/react": "^4.2.5",
"@types/react-beautiful-dnd": "^13.1.4",
Expand Down
2 changes: 1 addition & 1 deletion pages/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'

export default function ComponentsPage() {
return (
<Page title="Компоненты">
<Page title="Components">
<div style={{ display: 'grid', gridGap: '24px' }}>
<Link to="components/table">
<Button component="span">Table</Button>
Expand Down
39 changes: 39 additions & 0 deletions pages/tips-and-tricks/editor-js.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react'
import { Page, TextInput, Typography } from '../../admiral'
import { EditorJSInput } from '../../src/components/EditorJS'
import PageTopContent from '../../src/components/PageTopContent'

export default function EditorJsPage() {
return (
<Page title="Editor.js field">
<PageTopContent
title="Editor.js implementation"
descr={
<>
<Typography.Paragraph>
Editor.js - popular WYSIWYG editor.
</Typography.Paragraph>
<Typography.Paragraph>
In this example, we show how to create your own field and implement a
third-party WYSIWYG editor
</Typography.Paragraph>
</>
}
link={{
href: 'https://github.com/dev-family/admiral/tree/master/pages/tips-and-tricks/editor-js.tsx',
text: 'Code to implement the page',
}}
/>
<div style={{ marginTop: '24px' }}>
<EditorJSInput
required
imageUploadUrl={'/api/editor-upload'}
label="Editor JS"
columnSpan={2}
name="content"
placeholder="Add something..."
/>
</div>
</Page>
)
}
15 changes: 15 additions & 0 deletions pages/tips-and-tricks/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import { Page, Button } from '../../admiral'
import { Link } from 'react-router-dom'

export default function ComponentsPage() {
return (
<Page title="Tips & Tricks">
<div style={{ display: 'grid', gridGap: '24px' }}>
<Link to="tips-and-tricks/editor-js">
<Button component="span">Editor JS</Button>
</Link>
</div>
</Page>
)
}
49 changes: 49 additions & 0 deletions src/components/EditorJS/EditorJSContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { memo, useCallback } from 'react'
import EditorJS, { EditorConfig, OutputData } from '@editorjs/editorjs'
import { Form, FormItemProps, useForm } from '../../../../admiral'
import '../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: any) => ({ ...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)
11 changes: 11 additions & 0 deletions src/components/EditorJS/EditorJSField/EditorJSField.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.section {
position: relative;
z-index: 2;
padding: var(--control-space-s) calc(var(--control-space-m) / 2);
height: fit-content;
flex-grow: 1;
background: transparent;
border: var(--control-border-width) solid var(--color-control-bg-border-default);
border-radius: var(--control-radius);
cursor: text;
}
23 changes: 23 additions & 0 deletions src/components/EditorJS/EditorJSField/EditorTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Header from '@editorjs/header'
import Image from '@editorjs/image'
import Paragraph from '@editorjs/paragraph'
import NestedList from '@editorjs/nested-list'

export const EDITOR_TOOLS = {
header: { class: Header, inlineToolbar: true },
image: { class: Image, inlineToolbar: true },
list: {
class: NestedList,
inlineToolbar: true,
config: {
defaultStyle: 'ordered',
},
},
paragraph: {
class: Paragraph,
inlineToolbar: true,
config: {
preserveBlank: true,
},
},
}
80 changes: 80 additions & 0 deletions src/components/EditorJS/EditorJSField/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useEffect, useRef } from 'react'
import EditorJS, { EditorConfig, OutputData } from '@editorjs/editorjs'
import styles from './EditorJSField.module.scss'
import '../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 = 247,
onChange,
imageUploadUrl,
imageUploadField,
onImageUpload,
tools,
...rest
}: EditorProps) {
const ref = useRef<EditorJS | null>(null)

useEffect(() => {
if (!ref.current) {
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
Loading

0 comments on commit 9f28984

Please sign in to comment.