Editor.js uses a modular tool architecture where each content type (paragraphs, headers, images, etc.) is implemented as a separate plugin. This guide covers the interfaces and patterns for developing Block Tools, Inline Tools, and Block Tunes.
Block Tools create standalone content blocks that occupy their own line in the editor.
interface BlockTool {
/**
* Render the block's HTML element
* @returns HTML element representing the block
*/
render(): HTMLElement;
/**
* Extract data from the block's HTML element
* @param block - HTML element created by render()
* @returns Block data to be saved
*/
save(block: HTMLElement): BlockToolData;
/**
* Sanitization rules for the block content
*/
sanitize?: SanitizerConfig;
/**
* Create block settings UI (optional)
* @returns HTML element or menu configuration for settings
*/
renderSettings?(): HTMLElement | MenuConfig;
/**
* Validate block data (optional)
* @param blockData - Data to validate
* @returns true if data is valid
*/
validate?(blockData: BlockToolData): boolean;
/**
* Merge with another block of same type (optional)
* @param blockData - Data from block being merged
*/
merge?(blockData: BlockToolData): void;
/**
* Handle paste events (optional)
* @param event - Paste event details
*/
onPaste?(event: PasteEvent): void;
/**
* Cleanup resources (optional)
*/
destroy?(): void;
// Lifecycle hooks
rendered?(): void;
updated?(): void;
removed?(): void;
moved?(event: MoveEvent): void;
}
interface BlockToolConstructorOptions<D extends object = any, C extends object = any> {
data: BlockToolData<D>;
block: BlockAPI;
api: API;
config: ToolConfig<C>;
readOnly: boolean;
}
interface BlockToolConstructable {
toolbox?: ToolboxConfig;
pasteConfig?: PasteConfig | false;
conversionConfig?: ConversionConfig;
isReadOnlySupported?: boolean;
new(config: BlockToolConstructorOptions): BlockTool;
}Usage Examples:
// Simple paragraph tool implementation
class SimpleParagraph implements BlockTool {
private wrapper: HTMLElement;
private data: { text: string };
constructor({ data }: BlockToolConstructorOptions) {
this.data = data || { text: '' };
}
render(): HTMLElement {
this.wrapper = document.createElement('div');
this.wrapper.contentEditable = 'true';
this.wrapper.innerHTML = this.data.text;
return this.wrapper;
}
save(): BlockToolData {
return {
text: this.wrapper.innerHTML
};
}
validate(data: BlockToolData): boolean {
return typeof data.text === 'string';
}
// Static configuration
static get toolbox(): ToolboxConfig {
return {
icon: '<svg>...</svg>',
title: 'Paragraph'
};
}
static get sanitize(): SanitizerConfig {
return {
text: {
b: true,
i: true,
u: true
}
};
}
}
// Register tool with editor
const editor = new EditorJS({
tools: {
paragraph: SimpleParagraph
}
});Inline Tools provide formatting options for selected text within blocks.
interface InlineTool {
/**
* Render tool button/interface
* @returns HTML element or menu configuration
*/
render(): HTMLElement | MenuConfig;
/**
* Keyboard shortcut for the tool (optional)
*/
shortcut?: string;
/**
* Apply tool formatting to selection (deprecated)
* @param range - Current selection range
*/
surround?(range: Range | null): void;
/**
* Check if tool is currently active (deprecated)
* @param selection - Current selection
* @returns true if tool is active
*/
checkState?(selection: Selection): boolean;
/**
* Render additional actions UI (deprecated)
* @returns HTML element with actions
*/
renderActions?(): HTMLElement;
/**
* Cleanup when toolbar closes (deprecated)
*/
clear?(): void;
}
interface InlineToolConstructorOptions {
api: API;
config?: ToolConfig;
}
interface InlineToolConstructable {
new(config: InlineToolConstructorOptions): InlineTool;
}Usage Examples:
// Bold text inline tool
class BoldTool implements InlineTool {
private button: HTMLElement;
private api: API;
constructor({ api }: InlineToolConstructorOptions) {
this.api = api;
}
render(): HTMLElement {
this.button = document.createElement('button');
this.button.innerHTML = '<b>B</b>';
this.button.onclick = () => {
document.execCommand('bold');
};
return this.button;
}
checkState(): boolean {
return document.queryCommandState('bold');
}
// Keyboard shortcut
static get shortcut(): string {
return 'CMD+B';
}
}
// Register inline tool
const editor = new EditorJS({
tools: {
bold: {
class: BoldTool,
shortcut: 'CMD+B'
}
}
});Block Tunes provide additional settings and modifications for blocks.
interface BlockTune {
/**
* Render tune interface
* @returns HTML element or menu configuration
*/
render(): HTMLElement | MenuConfig;
/**
* Apply tune modifications (optional)
* @param wrapper - Block wrapper element
*/
wrap?(wrapper: HTMLElement): HTMLElement;
/**
* Save tune data (optional)
* @returns Tune configuration data
*/
save?(): BlockTuneData;
}
interface BlockTuneConstructable {
/**
* Whether tune supports read-only mode
*/
isTune: true;
new(options: {
api: API;
data: BlockTuneData;
block: BlockAPI;
config: ToolConfig;
}): BlockTune;
}Usage Examples:
// Text alignment block tune
class TextAlignmentTune implements BlockTune {
private api: API;
private block: BlockAPI;
private data: { alignment: string };
constructor({ api, data, block }) {
this.api = api;
this.block = block;
this.data = data || { alignment: 'left' };
}
render(): MenuConfig {
return {
icon: '<svg>...</svg>',
title: 'Alignment',
children: [
{
icon: '<svg>...</svg>',
title: 'Left',
onActivate: () => this.setAlignment('left')
},
{
icon: '<svg>...</svg>',
title: 'Center',
onActivate: () => this.setAlignment('center')
},
{
icon: '<svg>...</svg>',
title: 'Right',
onActivate: () => this.setAlignment('right')
}
]
};
}
private setAlignment(alignment: string) {
this.data.alignment = alignment;
this.block.holder.style.textAlign = alignment;
}
wrap(wrapper: HTMLElement): HTMLElement {
wrapper.style.textAlign = this.data.alignment;
return wrapper;
}
save(): BlockTuneData {
return this.data;
}
static get isTune(): true {
return true;
}
}
// Register block tune
const editor = new EditorJS({
tunes: ['textAlignment'],
tools: {
textAlignment: TextAlignmentTune
}
});interface ToolboxConfig {
icon?: string;
title?: string;
data?: BlockToolData;
}Usage Examples:
class CustomTool implements BlockTool {
static get toolbox(): ToolboxConfig {
return {
icon: `<svg width="17" height="15" viewBox="0 0 336 276">
<path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-67 49v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/>
</svg>`,
title: 'Custom Block'
};
}
}interface PasteConfig {
patterns?: { [key: string]: RegExp };
tags?: string[];
files?: {
mimeTypes?: string[];
extensions?: string[];
};
}Usage Examples:
class LinkTool implements BlockTool {
static get pasteConfig(): PasteConfig {
return {
patterns: {
url: /^https?:\/\/.+$/
},
tags: ['A']
};
}
onPaste(event: PasteEvent): void {
if (event.type === 'pattern') {
const url = event.detail.data;
// Handle URL paste
}
}
}interface ConversionConfig {
import?: string | ((data: any) => BlockToolData);
export?: string | ((data: BlockToolData) => any);
}Usage Examples:
class HeaderTool implements BlockTool {
static get conversionConfig(): ConversionConfig {
return {
export: 'text', // Export text property
import: 'text' // Import to text property
};
}
}
class ParagraphTool implements BlockTool {
static get conversionConfig(): ConversionConfig {
return {
export: (data: BlockToolData) => data.text,
import: (text: string) => ({ text })
};
}
}class ValidatedTool implements BlockTool {
validate(data: BlockToolData): boolean {
// Required fields check
if (!data.title || !data.content) {
return false;
}
// Type validation
if (typeof data.title !== 'string' || typeof data.content !== 'string') {
return false;
}
// Length validation
if (data.title.length > 100 || data.content.length > 1000) {
return false;
}
return true;
}
}class MergeableTool implements BlockTool {
merge(blockData: BlockToolData): void {
// Merge content from another block
this.data.content += ' ' + blockData.content;
// Update UI
this.wrapper.innerHTML = this.data.content;
}
}class SettingsTool implements BlockTool {
renderSettings(): MenuConfig {
return {
icon: '<svg>...</svg>',
title: 'Settings',
children: [
{
icon: '<svg>...</svg>',
title: 'Style 1',
isActive: this.data.style === 'style1',
onActivate: () => this.setStyle('style1')
},
{
icon: '<svg>...</svg>',
title: 'Style 2',
isActive: this.data.style === 'style2',
onActivate: () => this.setStyle('style2')
}
]
};
}
private setStyle(style: string) {
this.data.style = style;
this.wrapper.className = `block-${style}`;
}
}interface BlockToolData<T extends object = any> extends T {}
interface ToolConfig<T extends object = any> extends T {}
interface SanitizerConfig {
[tagName: string]: boolean | { [attrName: string]: boolean | string };
}
interface MenuConfig {
icon?: string;
title?: string;
children?: MenuConfigItem[];
onActivate?: () => void;
isActive?: boolean;
closeOnActivate?: boolean;
}
interface MenuConfigItem {
icon?: string;
title?: string;
onActivate?: () => void;
isActive?: boolean;
closeOnActivate?: boolean;
}
interface PasteEvent {
type: 'pattern' | 'tag' | 'file';
detail: PasteEventDetail;
}
interface MoveEvent {
fromIndex: number;
toIndex: number;
}
interface BlockTuneData {
[key: string]: any;
}