Skip to content

Instantly share code, notes, and snippets.

@Thisisjuke
Last active April 26, 2024 13:33
Show Gist options
  • Save Thisisjuke/46d9c0b1b6fbae040931c6897a9fb034 to your computer and use it in GitHub Desktop.
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)
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',
},
}
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
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
}
}
}
}
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>
)
}
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
@Thisisjuke
Copy link
Author

Thisisjuke commented Jul 10, 2023

Custom EditorJS Tool

This is a custom React component that creates an instance of EditorJS with custom tools. It allows you to add and edit content using a variety of tools.

Introduction

Preview

image

Recommended folder structure:

image

Limitations and Usage Information

Please note the following limitations and usage information regarding the custom EditorJS component:

  • Not compatible with Next.js Server-Side Rendering (SSR): This custom component is not compatible with Next.js Server-Side Rendering (SSR) due to the way EditorJS interacts with the DOM. EditorJS relies on the browser environment to function properly, which is not available during SSR. Therefore, if you are using Next.js with SSR, you should only render this component on the client-side. SEE THE NEXT.JS SECTION AT THE END OF THIS COMMENT.

  • Usage of JavaScript class instead of functional components: Currently, EditorJS does not support functional components as custom tools. Therefore, the CustomTool class provided in this implementation uses JavaScript class syntax. This is necessary to ensure compatibility with EditorJS. If you need to create custom tools, make sure to define them as classes.

Installation

To use this custom component, you need to have the following dependencies installed:

  • React
  • EditorJS

You can install them using npm or yarn:

npm install react @editorjs/editorjs

or

yarn add react @editorjs/editorjs

Usage

To use the custom EditorJS component, follow these steps:

  1. Import the necessary dependencies and tools:
import React from 'react';
import EditorJS from '@editorjs/editorjs';
import { CustomTool } from './CustomTool';
import TitleTool from './TitleTool';

Note: Import other custom tools if needed

  1. Create an instance of the EditorJS component:
const MyEditor = () => {
  const editorRef = React.useRef(null);

  React.useEffect(() => {
    if (!editorRef.current) {
      editorRef.current = new EditorJS({
        // Specify the holder element where the editor should be rendered
        holder: 'editor-container',
        // Add custom tools to the `tools` option
        tools: {
          // Use the custom tool
          title: TitleTool,
          // Add other custom tools if needed
        },
        // Other EditorJS configuration options
      });
    }

    return () => {
      if (editorRef.current && editorRef.current.destroy) {
        editorRef.current.destroy();
      }
    };
  }, []);

  return <div id="editor-container" />;
};
  1. Use the MyEditor component in your application:
Copy code
function App() {
  return (
    <div>
      <h1>My Application</h1>
      <MyEditor />
    </div>
  );
}

CustomTool.tsx

The CustomTool class in CustomTool.tsx is a utility class used to create custom tools for EditorJS. It provides a convenient way to define and render custom blocks within the editor.

Class: CustomTool

Constructor:

  • options: An object containing the following properties:
  • data: The initial data for the custom tool.
  • config: Configuration options for the custom tool.
  • api: The EditorJS API object.
  • readOnly: A boolean indicating whether the editor is in read-only mode.
  • component: The React component to render for the custom tool.
  • toolbox: The configuration for the custom tool in the editor's toolbox.
  • opts (optional): Additional options for the custom tool.

Methods:

  • render(): Renders the custom tool and returns the root element.
  • save(): Returns the current data of the custom tool.

Static Methods:

  • createTool(): Creates a new custom tool class based on the provided component, toolbox configuration, and additional options.

Example usage:

import { CustomTool } from './CustomTool';

// Define a custom React component
const MyCustomComponent = ({ onDataChange, readOnly, data, opts }) => {
  // Implement the custom component logic
  // ...

  return <div>{/* Custom component JSX */}</div>;
};

// Create a new custom tool
const MyCustomTool = CustomTool.createTool(MyCustomComponent, {
  icon: '<svg>...</svg>',
  title: 'My Custom Tool',
});

// Use the custom tool in EditorJS
const editor = new EditorJS({
  // ...
  tools: {
    customTool: MyCustomTool,
    // Add other tools if needed
  },
});

TitleTool.tsx

The TitleTool component in TitleTool.tsx is an example of a custom tool implementation using the CustomTool class.

Usage

To use the TitleTool, follow these steps:

  1. Import the necessary dependencies:
import { CustomTool } from './CustomTool';
import { TitleInput } from './TitleInput';

Create a new instance of the CustomTool using the createTool() static method:

const TitleTool = CustomTool.createTool(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',
});
Use the TitleTool in your EditorJS instance:
jsx
Copy code
const editor = new EditorJS({
  // ...
  tools: {
    title: TitleTool,
    // Add other tools if needed
  },
});

EditorBlock.tsx

The EditorBlock component in EditorBlock.tsx is a wrapper component that integrates the EditorJS instance into your React application.

Props

  • data (optional): The initial data for the editor.
  • onChange: A callback function that is triggered when the editor content changes. It receives the updated editor data as an argument.
  • holder: The ID of the container element where the editor should be rendered.

Usage

To use the EditorBlock, follow these steps:

  1. Import the necessary dependencies:
import React from 'react';
import EditorJS from '@editorjs/editorjs';
import { CustomTool } from './CustomTool';
import TitleTool from './TitleTool';

// Import other custom tools if needed

Create an instance of the EditorJS component inside the EditorBlock component:

const EditorBlock = ({ data, onChange, holder }) => {
  const ref = React.useRef();

  React.useEffect(() => {
    if (!ref.current) {
      ref.current = new EditorJS({
        holder,
        tools: {
          title: TitleTool,
          // Add other tools if needed
        },
        data,
        async onChange(api) {
          const data = await api.saver.save();
          onChange(data);
        },
        // Other EditorJS configuration options
      });
    }

    return () => {
      if (ref.current && ref.current.destroy) {
        ref.current.destroy();
      }
    };
  }, []);

  return <div id={holder} />;
};

Use the EditorBlock component in your application:

function App() {
  const handleEditorChange = (data) => {
    // Handle the editor data change
    console.log(data);
  };

  return (
    <div>
      <h1>My Application</h1>
      <EditorBlock data={initialData} onChange={handleEditorChange} holder="editor-container" />
    </div>
  );
}

That's it! You can now use the custom EditorJS component with your custom tools in your React application.

How to Use in Next.js

You have to create another component that will be only be displayed client side, using Next.js noSSR mod ssr: false.

import { useState } from 'react'
import dynamic from 'next/dynamic'

const EditorModule = dynamic(() => import('@/modules/quiz/components/question-builder/Editor/Editor'), {
    ssr: false,
})

export const MyCustomEditor = () => {
    const [content, setContent] = useState<any>()

    return (
        <EditorModule data={content} onChange={setContent} holder={'editorjs-container'} />
    )
}

Then import it normally in your page or in another component.

@MuhammadAhmed-Developer

Custom EditorJS Tool

This is a custom React component that creates an instance of EditorJS with custom tools. It allows you to add and edit content using a variety of tools.

Introduction

Preview

image

Recommended folder structure:

image

Limitations and Usage Information

Please note the following limitations and usage information regarding the custom EditorJS component:

  • Not compatible with Next.js Server-Side Rendering (SSR): This custom component is not compatible with Next.js Server-Side Rendering (SSR) due to the way EditorJS interacts with the DOM. EditorJS relies on the browser environment to function properly, which is not available during SSR. Therefore, if you are using Next.js with SSR, you should only render this component on the client-side. SEE THE NEXT.JS SECTION AT THE END OF THIS COMMENT.
  • Usage of JavaScript class instead of functional components: Currently, EditorJS does not support functional components as custom tools. Therefore, the CustomTool class provided in this implementation uses JavaScript class syntax. This is necessary to ensure compatibility with EditorJS. If you need to create custom tools, make sure to define them as classes.

Installation

To use this custom component, you need to have the following dependencies installed:

  • React
  • EditorJS

You can install them using npm or yarn:

npm install react @editorjs/editorjs

or

yarn add react @editorjs/editorjs

Usage

To use the custom EditorJS component, follow these steps:

  1. Import the necessary dependencies and tools:
import React from 'react';
import EditorJS from '@editorjs/editorjs';
import { CustomTool } from './CustomTool';
import TitleTool from './TitleTool';

Note: Import other custom tools if needed

  1. Create an instance of the EditorJS component:
const MyEditor = () => {
  const editorRef = React.useRef(null);

  React.useEffect(() => {
    if (!editorRef.current) {
      editorRef.current = new EditorJS({
        // Specify the holder element where the editor should be rendered
        holder: 'editor-container',
        // Add custom tools to the `tools` option
        tools: {
          // Use the custom tool
          title: TitleTool,
          // Add other custom tools if needed
        },
        // Other EditorJS configuration options
      });
    }

    return () => {
      if (editorRef.current && editorRef.current.destroy) {
        editorRef.current.destroy();
      }
    };
  }, []);

  return <div id="editor-container" />;
};
  1. Use the MyEditor component in your application:
Copy code
function App() {
  return (
    <div>
      <h1>My Application</h1>
      <MyEditor />
    </div>
  );
}

CustomTool.tsx

The CustomTool class in CustomTool.tsx is a utility class used to create custom tools for EditorJS. It provides a convenient way to define and render custom blocks within the editor.

Class: CustomTool

Constructor:

  • options: An object containing the following properties:
  • data: The initial data for the custom tool.
  • config: Configuration options for the custom tool.
  • api: The EditorJS API object.
  • readOnly: A boolean indicating whether the editor is in read-only mode.
  • component: The React component to render for the custom tool.
  • toolbox: The configuration for the custom tool in the editor's toolbox.
  • opts (optional): Additional options for the custom tool.

Methods:

  • render(): Renders the custom tool and returns the root element.
  • save(): Returns the current data of the custom tool.

Static Methods:

  • createTool(): Creates a new custom tool class based on the provided component, toolbox configuration, and additional options.

Example usage:

import { CustomTool } from './CustomTool';

// Define a custom React component
const MyCustomComponent = ({ onDataChange, readOnly, data, opts }) => {
  // Implement the custom component logic
  // ...

  return <div>{/* Custom component JSX */}</div>;
};

// Create a new custom tool
const MyCustomTool = CustomTool.createTool(MyCustomComponent, {
  icon: '<svg>...</svg>',
  title: 'My Custom Tool',
});

// Use the custom tool in EditorJS
const editor = new EditorJS({
  // ...
  tools: {
    customTool: MyCustomTool,
    // Add other tools if needed
  },
});

TitleTool.tsx

The TitleTool component in TitleTool.tsx is an example of a custom tool implementation using the CustomTool class.

Usage

To use the TitleTool, follow these steps:

  1. Import the necessary dependencies:
import { CustomTool } from './CustomTool';
import { TitleInput } from './TitleInput';

Create a new instance of the CustomTool using the createTool() static method:

const TitleTool = CustomTool.createTool(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',
});
Use the TitleTool in your EditorJS instance:
jsx
Copy code
const editor = new EditorJS({
  // ...
  tools: {
    title: TitleTool,
    // Add other tools if needed
  },
});

EditorBlock.tsx

The EditorBlock component in EditorBlock.tsx is a wrapper component that integrates the EditorJS instance into your React application.

Props

  • data (optional): The initial data for the editor.
  • onChange: A callback function that is triggered when the editor content changes. It receives the updated editor data as an argument.
  • holder: The ID of the container element where the editor should be rendered.

Usage

To use the EditorBlock, follow these steps:

  1. Import the necessary dependencies:
import React from 'react';
import EditorJS from '@editorjs/editorjs';
import { CustomTool } from './CustomTool';
import TitleTool from './TitleTool';

// Import other custom tools if needed

Create an instance of the EditorJS component inside the EditorBlock component:

const EditorBlock = ({ data, onChange, holder }) => {
  const ref = React.useRef();

  React.useEffect(() => {
    if (!ref.current) {
      ref.current = new EditorJS({
        holder,
        tools: {
          title: TitleTool,
          // Add other tools if needed
        },
        data,
        async onChange(api) {
          const data = await api.saver.save();
          onChange(data);
        },
        // Other EditorJS configuration options
      });
    }

    return () => {
      if (ref.current && ref.current.destroy) {
        ref.current.destroy();
      }
    };
  }, []);

  return <div id={holder} />;
};

Use the EditorBlock component in your application:

function App() {
  const handleEditorChange = (data) => {
    // Handle the editor data change
    console.log(data);
  };

  return (
    <div>
      <h1>My Application</h1>
      <EditorBlock data={initialData} onChange={handleEditorChange} holder="editor-container" />
    </div>
  );
}

That's it! You can now use the custom EditorJS component with your custom tools in your React application.

How to Use in Next.js

You have to create another component that will be only be displayed client side, using Next.js noSSR mod ssr: false.

import { useState } from 'react'
import dynamic from 'next/dynamic'

const EditorModule = dynamic(() => import('@/modules/quiz/components/question-builder/Editor/Editor'), {
    ssr: false,
})

export const MyCustomEditor = () => {
    const [content, setContent] = useState<any>()

    return (
        <EditorModule data={content} onChange={setContent} holder={'editorjs-container'} />
    )
}

Then import it normally in your page or in another component.

Thank you! Can you send one other tool code just like Ask Question because I am facing issue to add other custom tool

@Thisisjuke
Copy link
Author

@MuhammadAhmed-Developer

TextsQuestionTool example:

import { QuestionInput } from '@/components/inputs/QuestionInput'
import { CustomTool } from '@/components/tools/GenericTool'

const TextsQuestionTool = CustomTool.createTool(
    QuestionInput,
    {
        icon: '(?)',
        title: 'Ask Question',
    },
    {
        type: 'texts',
    },
)

export default TextsQuestionTool

QuestionInput interface looks like this:

export interface QuestionInputProps {
    data: QuestionToolData
    onDataChange: (questionData: QuestionToolData) => void
    readOnly?: boolean
    options?: {
        type: 'images' | 'texts'
        [key: string]: unknown
    }
}

Added like this in the tool array:

import type { OutputData, ToolConstructable } from '@editorjs/editorjs'
import EditorJS from '@editorjs/editorjs'

const tools: { [toolName: string]: ToolConstructable } = {
    titleTool: TitleTool as unknown as ToolConstructable,
    textsQuestionTool: TextsQuestionTool as unknown as ToolConstructable,
}

Remember, tool is called inside the EditorJs instance:

ref.current = new EditorJS({
    holder,
    tools, // <== here
    data,
    async onChange(api) {
        const data = await api.saver.save()
        onChange(data)
    },
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment