Last active
February 11, 2025 02:16
-
-
Save Thisisjuke/46d9c0b1b6fbae040931c6897a9fb034 to your computer and use it in GitHub Desktop.
EditorJS component to easily create custom Blocks/Tool with Typescript and React (usable in NextJS)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { Meta, StoryObj } from '@storybook/react' | |
import { useState } from 'react' | |
import type { OutputData } from '@editorjs/editorjs' | |
import Editor from './Editor' | |
const meta: Meta<typeof Editor> = { | |
title: 'Components/Editor', | |
component: Editor, | |
tags: ['autodocs'], | |
} | |
export default meta | |
type Story = StoryObj<typeof Editor> | |
export const Default: Story = { | |
render: (args) => { | |
const [data, setData] = useState<OutputData>() | |
return ( | |
<Editor {...args} data={data} onChange={setData} /> | |
) | |
}, | |
args: { | |
holder: 'editorjs-container', | |
}, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef } from 'react' | |
import type { OutputData, ToolConstructable } from '@editorjs/editorjs' | |
import EditorJS from '@editorjs/editorjs' | |
import QuestionTool from '@/modules/Editor/tools/QuestionTool/TextsQuestionTool' | |
import TitleTool from '@/modules/Editor/tools/TitleTool/TitleTool' | |
import DisplayImageTool from '@/modules/Editor/tools/DisplayImageTool/DisplayImageTool' | |
const tools: { [toolName: string]: ToolConstructable } = { | |
titleTool: TitleTool as unknown as ToolConstructable, | |
displayImageTool: DisplayImageTool as unknown as ToolConstructable, | |
imagesQuestionTool: ImagesQuestionTool as unknown as ToolConstructable, | |
textsQuestionTool: TextsQuestionTool as unknown as ToolConstructable, | |
} | |
export interface EditorBlockProps { | |
data?: OutputData | |
onChange(val: OutputData): void | |
holder: string | |
} | |
const EditorBlock = ({ data, onChange, holder }: EditorBlockProps) => { | |
const ref = useRef<EditorJS>() | |
useEffect(() => { | |
if (!ref.current) { | |
ref.current = new EditorJS({ | |
holder, | |
tools, | |
data, | |
async onChange(api) { | |
const data = await api.saver.save() | |
onChange(data) | |
}, | |
}) | |
} | |
return () => { | |
if (ref.current && ref.current.destroy) { | |
ref.current.destroy() | |
} | |
} | |
}, []) | |
return <div id={holder} className={'!mb-0 bg-gray-100 h-full !p-2 overflow-x-auto'} /> | |
} | |
export default EditorBlock |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ReactDOM from 'react-dom' | |
import type { API, BlockTool, ToolConfig } from '@editorjs/editorjs' | |
import React, { createElement } from 'react' | |
interface CustomToolOptions<TData extends Record<string, any>, TConfig extends Record<string, any>, TOpts extends Record<string, any>> { | |
data: TData | |
config: TConfig | |
api: API | |
readOnly: boolean | |
component: React.ComponentType<{ onDataChange: (newData: TData) => void; readOnly: boolean; data: TData; opts: TOpts }> | |
toolbox: ToolConfig | |
opts?: TOpts | |
} | |
export class CustomTool<TData extends Record<string, any>, TConfig extends Record<string, any>, TOpts extends Record<string, any>> implements BlockTool { | |
private api: API | |
private readonly readOnly: boolean | |
private data: TData | |
private config: TConfig | |
private component: React.ComponentType<{ onDataChange: (newData: TData) => void; readOnly: boolean; data: TData; options?: TOpts }> | |
private toolbox: ToolConfig | |
private readonly CSS = { | |
wrapper: 'custom-tool', | |
} | |
private nodes = { | |
holder: null as HTMLElement | null, | |
} | |
constructor(options: CustomToolOptions<TData, TConfig, TOpts>) { | |
const { data, config, api, readOnly, component, toolbox } = options | |
this.api = api | |
this.readOnly = readOnly | |
this.data = data | |
this.config = config | |
this.component = component as any | |
this.toolbox = toolbox | |
} | |
static get isReadOnlySupported(): boolean { | |
return true | |
} | |
render(): HTMLElement { | |
const rootNode = document.createElement('div') | |
rootNode.setAttribute('class', this.CSS.wrapper) | |
this.nodes.holder = rootNode | |
const onDataChange = (newData: TData) => { | |
this.data = { | |
...newData, | |
} | |
} | |
ReactDOM.render(<this.component onDataChange={onDataChange} readOnly={this.readOnly} data={this.data} />, rootNode) | |
return this.nodes.holder | |
} | |
save(): TData { | |
return this.data | |
} | |
static createTool<TData extends Record<string, any>, TConfig extends Record<string, any>, TOpts extends Record<string, any>>( | |
component: React.ComponentType<{ onDataChange: (newData: TData) => void; readOnly: boolean; data: TData; opts: TOpts }>, | |
toolbox: ToolConfig, | |
opts?: TOpts, | |
): new (options: CustomToolOptions<TData, TConfig, TOpts>) => CustomTool<TData, TConfig, TOpts> { | |
return class extends CustomTool<TData, TConfig, TOpts> { | |
constructor(options: CustomToolOptions<TData, TConfig, TOpts>) { | |
super({ | |
...options, | |
component: (props: any) => createElement(component, { ...props, options: opts }), | |
toolbox, | |
data: { | |
events: [], | |
...options.data, | |
}, | |
}) | |
} | |
static get toolbox() { | |
return toolbox | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
export interface TitleInputProps { | |
data: Record<string, string> | |
onDataChange: (arg: Record<string, string>) => void | |
readOnly?: boolean | |
} | |
export const TitleInput = ({ data, onDataChange, readOnly }: TitleInputProps) => { | |
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { | |
const newData = { | |
title: event.target.value, | |
} | |
onDataChange(newData) | |
} | |
return ( | |
<div className={'flex gap-x-2 w-full'}> | |
<span className={'text-xl underline shrink-0'}>Titre :</span> | |
<textarea | |
className={'bg-transparent border-0 focus:outline-none text-xl grow-1 w-full'} | |
defaultValue={data.title} | |
onChange={handleChange} | |
readOnly={readOnly} | |
/> | |
</div> | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CustomTool } from '@/modules/Editor/tools/GenericTool' | |
import { TitleInput } from '@/modules/Editor/tools/TitleTool/TitleInput' | |
const TitleTool = CustomTool.createTool( | |
// ⬇️ Here is the component that will be used as the custom "tool" / "block" inside EditorJS. | |
TitleInput, | |
{ | |
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m16 10l3-1v10M3 5v7m0 0v7m0-7h8m0-7v7m0 0v7"/></svg>', | |
title: 'Title', | |
}, | |
) | |
export default TitleTool |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@MuhammadAhmed-Developer
TextsQuestionTool example:
QuestionInput interface looks like this:
Added like this in the tool array:
Remember, tool is called inside the EditorJs instance: