or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

block-management.mdcaret-navigation.mdconfiguration.mdeditor-core.mdevent-system.mdindex.mdtool-development.md
tile.json

tool-development.mddocs/

Tool Development

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.

Capabilities

Block Tools

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

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

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
  }
});

Tool Configuration

Toolbox Configuration

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'
    };
  }
}

Paste Configuration

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
    }
  }
}

Conversion Configuration

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 })
    };
  }
}

Advanced Patterns

Data Validation

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;
  }
}

Block Merging

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;
  }
}

Settings UI

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}`;
  }
}

Types

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;
}