CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-quill

A modern, powerful rich text editor built for compatibility and extensibility

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

module-system.mddocs/

Module System

Extensible module architecture with core modules for history, keyboard, clipboard, toolbar, and upload functionality. Modules provide specialized functionality and can be configured, extended, or replaced to customize editor behavior.

Capabilities

Base Module Class

Abstract base class that all Quill modules extend, providing common initialization patterns.

/**
 * Base class for all Quill modules
 */
abstract class Module {
  /** Default configuration options for the module */
  static DEFAULTS: Record<string, unknown>;
  
  /** Quill instance this module belongs to */
  quill: Quill;
  
  /** Module configuration options */
  options: Record<string, unknown>;
  
  /**
   * Create module instance
   * @param quill - Quill editor instance
   * @param options - Module configuration options
   */
  constructor(quill: Quill, options?: Record<string, unknown>);
}

Usage Examples:

// Define custom module
class CustomModule extends Module {
  static DEFAULTS = {
    option1: 'default-value',
    option2: true
  };
  
  constructor(quill, options) {
    super(quill, options);
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    this.quill.on('text-change', this.handleTextChange.bind(this));
  }
  
  handleTextChange(delta, oldDelta, source) {
    // Custom logic here
  }
}

// Register and use
Quill.register('modules/custom', CustomModule);

const quill = new Quill('#editor', {
  modules: {
    custom: {
      option1: 'custom-value',
      option2: false
    }
  }
});

History Module

Undo/redo functionality with configurable stack size and change tracking.

class History extends Module {
  static DEFAULTS: {
    /** Delay in milliseconds before creating new history entry */
    delay: number;
    /** Maximum number of changes in history stack */
    maxStack: number;
    /** Only track user changes, not API changes */
    userOnly: boolean;
  };
  
  /** Current history stack */
  stack: {
    undo: HistoryRecord[];
    redo: HistoryRecord[];
  };
  
  /** Timestamp of last recorded change */
  lastRecorded: number;
  
  /** Whether to ignore current change */
  ignoreChange: boolean;
  
  /** Current selection range for restoration */
  currentRange: Range | null;
  
  /**
   * Clear all history
   */
  clear(): void;
  
  /**
   * Force cutoff point in history (start new change group)
   */
  cutoff(): void;
  
  /**
   * Undo last change
   */
  undo(): void;
  
  /**
   * Redo last undone change
   */
  redo(): void;
  
  /**
   * Record change in history
   * @param changeDelta - Delta representing the change
   * @param oldDelta - Previous document state
   * @param source - Source of the change
   */
  record(changeDelta: Delta, oldDelta: Delta): void;
  
  /**
   * Transform history stack against delta
   * @param delta - Delta to transform against
   * @param priority - Transform priority
   */
  transform(delta: Delta): void;
}

interface HistoryRecord {
  delta: Delta;
  range: Range | null;
}

interface HistoryOptions {
  delay?: number;
  maxStack?: number;
  userOnly?: boolean;
}

Usage Examples:

// Configure history module
const quill = new Quill('#editor', {
  modules: {
    history: {
      delay: 2000,      // 2 second delay before new history entry
      maxStack: 200,    // Keep 200 changes in history
      userOnly: true    // Only track user changes
    }
  }
});

// Programmatic history operations
const history = quill.getModule('history');

// Clear history
history.clear();

// Create cutoff point
history.cutoff();

// Undo/redo
history.undo();
history.redo();

// Keyboard shortcuts (automatically bound)
// Ctrl+Z / Cmd+Z: Undo
// Ctrl+Y / Cmd+Shift+Z: Redo

Keyboard Module

Keyboard input handling and customizable key bindings.

class Keyboard extends Module {
  static DEFAULTS: {
    bindings: Record<string, KeyboardBinding | KeyboardBinding[]>;
  };
  
  /** Current keyboard bindings */
  bindings: Record<string, KeyboardBinding[]>;
  
  /** Composition tracker */
  composition: Composition;
  
  /**
   * Add custom key binding
   * @param binding - Key binding configuration
   * @param handler - Handler function
   */
  addBinding(binding: KeyboardBinding, handler: KeyboardHandler): void;
  
  /**
   * Add key binding with shortcut key
   * @param key - Key specification
   * @param handler - Handler function
   */
  addBinding(key: string | KeyboardStatic, handler: KeyboardHandler): void;
  
  /**
   * Add key binding with options
   * @param key - Key specification
   * @param context - Context requirements
   * @param handler - Handler function
   */
  addBinding(key: string | KeyboardStatic, context: ContextObject, handler: KeyboardHandler): void;
  
  /**
   * Listen for keyboard events
   * @param event - Keyboard event
   */
  listen(): void;
  
  /**
   * Handle keyboard event
   * @param event - Keyboard event
   */
  handleKeyboard(event: KeyboardEvent): void;
}

interface KeyboardBinding {
  key: string | number;
  altKey?: boolean;
  ctrlKey?: boolean;
  metaKey?: boolean;
  shiftKey?: boolean;
  shortKey?: boolean; // Ctrl on PC, Cmd on Mac
  handler: KeyboardHandler;
  context?: ContextObject;
}

interface KeyboardStatic {
  key: string | number;
  altKey?: boolean;
  ctrlKey?: boolean;
  metaKey?: boolean;
  shiftKey?: boolean;
  shortKey?: boolean;
}

interface ContextObject {
  collapsed?: boolean;
  empty?: boolean;
  format?: Record<string, any>;
  list?: boolean;
  offset?: number;
  prefix?: RegExp | string;
  suffix?: RegExp | string;
}

interface Context {
  collapsed: boolean;
  empty: boolean;
  format: Record<string, any>;
  line: Blot;
  list: boolean;
  offset: number;
  prefix: string;
  suffix: string;
  event: KeyboardEvent;
}

type KeyboardHandler = (range: Range, context: Context) => boolean | void;

Usage Examples:

// Get keyboard module
const keyboard = quill.getModule('keyboard');

// Add custom key binding
keyboard.addBinding({
  key: 'Enter',
  shiftKey: true
}, (range, context) => {
  // Custom Shift+Enter behavior
  quill.insertText(range.index, '\n');
  return false; // Prevent default
});

// Add binding with context
keyboard.addBinding({
  key: 'Tab'
}, {
  format: ['code-block']
}, (range, context) => {
  // Custom Tab behavior in code blocks
  quill.insertText(range.index, '  '); // Insert 2 spaces
  return false;
});

// Add shortcut key (Ctrl/Cmd)
keyboard.addBinding({
  key: 'B',
  shortKey: true
}, (range, context) => {
  // Custom Ctrl+B / Cmd+B behavior
  const format = quill.getFormat(range);
  quill.format('bold', !format.bold);
});

// Add binding with prefix matching
keyboard.addBinding({
  key: ' '
}, {
  prefix: /^(#{1,6})$/
}, (range, context) => {
  // Convert # prefix to headers
  const level = context.prefix.length;
  quill.formatLine(range.index - level, 1, 'header', level);
  quill.deleteText(range.index - level, level);
});

Clipboard Module

Clipboard operations with paste filtering and content conversion.

class Clipboard extends Module {
  static DEFAULTS: {
    matchers: ClipboardMatcher[];
  };
  
  /** Content matchers for filtering pasted content */
  matchers: ClipboardMatcher[];
  
  /**
   * Add content matcher for filtering pastes
   * @param selector - CSS selector or node name to match
   * @param matcher - Function to process matched content
   */
  addMatcher(selector: string, matcher: ClipboardMatcherFunction): void;
  
  /**
   * Add content matcher with priority
   * @param selector - CSS selector or node name
   * @param priority - Matcher priority (higher runs first)
   * @param matcher - Function to process matched content
   */
  addMatcher(selector: string, priority: number, matcher: ClipboardMatcherFunction): void;
  
  /**
   * Paste HTML content directly (bypasses matchers)
   * @param html - HTML string to paste
   * @param source - Source of the paste operation
   */
  dangerouslyPasteHTML(html: string, source?: EmitterSource): void;
  
  /**
   * Paste HTML at specific index
   * @param index - Position to paste at
   * @param html - HTML string to paste
   * @param source - Source of the paste operation
   */
  dangerouslyPasteHTML(index: number, html: string, source?: EmitterSource): void;
  
  /**
   * Convert clipboard content to Delta
   * @param clipboard - Clipboard data with html and/or text
   * @returns Delta representing the clipboard content
   */
  convert(clipboard: { html?: string; text?: string }): Delta;
  
  /**
   * Handle paste event
   * @param event - Paste event
   */
  onPaste(event: ClipboardEvent): void;
  
  /**
   * Handle copy/cut events
   * @param event - Copy or cut event
   */
  onCopy(event: ClipboardEvent): void;
}

interface ClipboardMatcher {
  selector: string;
  priority: number;
  matcher: ClipboardMatcherFunction;
}

type ClipboardMatcherFunction = (node: Node, delta: Delta, scroll: Scroll) => Delta;

Usage Examples:

// Get clipboard module
const clipboard = quill.getModule('clipboard');

// Add matcher to handle pasted images
clipboard.addMatcher('IMG', (node, delta) => {
  const image = node as HTMLImageElement;
  return new Delta().insert({ image: image.src });
});

// Add matcher for custom elements
clipboard.addMatcher('.custom-element', (node, delta) => {
  const text = node.textContent || '';
  return new Delta().insert(text, { 'custom-format': true });
});

// Add high-priority matcher
clipboard.addMatcher('STRONG', 10, (node, delta) => {
  // Higher priority than default bold matcher
  const text = node.textContent || '';
  return new Delta().insert(text, { bold: true, 'custom-bold': true });
});

// Paste HTML directly
clipboard.dangerouslyPasteHTML('<p><strong>Bold text</strong></p>');

// Paste at specific position
clipboard.dangerouslyPasteHTML(10, '<em>Italic text</em>');

// Convert HTML to Delta
const delta = clipboard.convert({
  html: '<p>Hello <strong>world</strong></p>',
  text: 'Hello world'
});

Toolbar Module

Toolbar UI with customizable controls and handlers.

class Toolbar extends Module {
  static DEFAULTS: {
    /** Toolbar container selector or element */
    container?: string | HTMLElement | ToolbarConfig;
    /** Custom format handlers */
    handlers?: Record<string, ToolbarHandler>;
  };
  
  /** Toolbar container element */
  container: HTMLElement;
  
  /** Array of [format, element] control pairs */
  controls: [string, HTMLElement][];
  
  /** Format handler functions */
  handlers: Record<string, ToolbarHandler>;
  
  /**
   * Add custom format handler
   * @param format - Format name
   * @param handler - Handler function
   */
  addHandler(format: string, handler: ToolbarHandler): void;
  
  /**
   * Attach toolbar to input element
   * @param input - Input element to attach
   */
  attach(input: HTMLElement): void;
  
  /**
   * Update toolbar state based on current selection
   * @param range - Current selection range
   */
  update(range: Range | null): void;
  
  /**
   * Update control state
   * @param format - Format name
   * @param value - Current format value
   */
  updateControl(format: string, value: any): void;
}

type ToolbarConfig = (string | Record<string, any>)[];

type ToolbarHandler = (value: any) => void;

Usage Examples:

// Configure toolbar
const quill = new Quill('#editor', {
  modules: {
    toolbar: [
      ['bold', 'italic', 'underline'],
      [{ 'header': [1, 2, 3, false] }],
      [{ 'list': 'ordered'}, { 'list': 'bullet' }],
      ['link', 'image'],
      ['clean']
    ]
  }
});

// Get toolbar module
const toolbar = quill.getModule('toolbar');

// Add custom handler
toolbar.addHandler('image', () => {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = 'image/*';
  input.onchange = () => {
    const file = input.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const range = quill.getSelection();
        quill.insertEmbed(range.index, 'image', e.target.result);
      };
      reader.readAsDataURL(file);
    }
  };
  input.click();
});

// Custom format handler
toolbar.addHandler('mention', (value) => {
  if (value) {
    const range = quill.getSelection();
    quill.insertText(range.index, `@${value} `);
    quill.setSelection(range.index + value.length + 2);
  }
});

// HTML toolbar configuration
const quill2 = new Quill('#editor2', {
  modules: {
    toolbar: {
      container: '#toolbar',
      handlers: {
        'custom-button': customButtonHandler
      }
    }
  }
});

Uploader Module

File upload handling with customizable upload logic.

class Uploader extends Module {
  static DEFAULTS: {
    /** Allowed MIME types */
    mimetypes?: string[];
    /** Handler function for upload */
    handler?: UploaderHandler;
  };
  
  /**
   * Upload files
   * @param range - Current selection range
   * @param files - Files to upload
   */
  upload(range: Range, files: File[] | FileList): void;
  
  /**
   * Handle file upload
   * @param file - File to upload
   * @param handler - Upload completion handler
   */
  handler(file: File, handler: (url: string) => void): void;
}

type UploaderHandler = (file: File, callback: (url: string) => void) => void;

Usage Examples:

// Configure uploader
const quill = new Quill('#editor', {
  modules: {
    uploader: {
      mimetypes: ['image/png', 'image/jpeg'],
      handler: (file, callback) => {
        // Custom upload logic
        const formData = new FormData();
        formData.append('image', file);
        
        fetch('/upload', {
          method: 'POST',
          body: formData
        })
        .then(response => response.json())
        .then(data => {
          callback(data.url);
        })
        .catch(err => {
          console.error('Upload failed:', err);
        });
      }
    }
  }
});

// Get uploader module
const uploader = quill.getModule('uploader');

// Manually trigger upload
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = () => {
  const range = quill.getSelection();
  uploader.upload(range, fileInput.files);
};

Input Module

Input method editor (IME) and text input handling.

class Input extends Module {
  /**
   * Handle text input events
   * @param event - Input event
   */
  onInput(event: InputEvent): void;
  
  /**
   * Handle composition start
   * @param event - Composition event
   */
  onCompositionStart(event: CompositionEvent): void;
  
  /**
   * Handle composition update
   * @param event - Composition event
   */
  onCompositionUpdate(event: CompositionEvent): void;
  
  /**
   * Handle composition end
   * @param event - Composition event
   */
  onCompositionEnd(event: CompositionEvent): void;
}

Syntax Module

Syntax highlighting for code blocks using highlight.js.

class Syntax extends Module {
  static DEFAULTS: {
    /** Highlight.js instance */
    hljs?: any;
    /** Highlight interval in milliseconds */
    interval?: number;
    /** Languages to support */
    languages?: string[];
  };
  
  /** Highlight.js instance */
  hljs: any;
  
  /** Highlight timer */
  timer: number | null;
  
  /**
   * Highlight code blocks
   */
  highlight(): void;
  
  /**
   * Initialize syntax highlighting
   */
  init(): void;
}

Usage Examples:

// Configure syntax highlighting
const quill = new Quill('#editor', {
  modules: {
    syntax: {
      hljs: window.hljs, // Include highlight.js library
      languages: ['javascript', 'python', 'java', 'css']
    },
    toolbar: [
      [{ 'code-block': 'Code' }]
    ]
  }
});

// Code blocks will be automatically highlighted

Custom Module Creation

Create custom modules to extend Quill functionality.

class CustomModule extends Module {
  static DEFAULTS = {
    // Default options
  };
  
  constructor(quill, options) {
    super(quill, options);
    // Initialize module
  }
}

// Register module
Quill.register('modules/custom', CustomModule);

Usage Examples:

// Word count module
class WordCount extends Module {
  static DEFAULTS = {
    container: null,
    unit: 'word'
  };
  
  constructor(quill, options) {
    super(quill, options);
    this.container = document.querySelector(options.container);
    this.quill.on('text-change', this.update.bind(this));
    this.update(); // Initial count
  }
  
  calculate() {
    const text = this.quill.getText();
    if (this.options.unit === 'word') {
      return text.split(/\s+/).filter(word => word.length > 0).length;
    } else {
      return text.length;
    }
  }
  
  update() {
    const count = this.calculate();
    if (this.container) {
      this.container.textContent = `${count} ${this.options.unit}s`;
    }
  }
}

// Register and use
Quill.register('modules/wordCount', WordCount);

const quill = new Quill('#editor', {
  modules: {
    wordCount: {
      container: '#word-count',
      unit: 'word'
    }
  }
});

Table Module

Advanced table management module providing comprehensive table manipulation functionality including insertion, deletion, and structural modifications.

Core Table Operations

class Table extends Module {
  static register(): void;
  
  // Table structure manipulation
  insertTable(rows: number, columns: number): void;
  deleteTable(): void;
  balanceTables(): void;
  
  // Row operations
  insertRowAbove(): void;
  insertRowBelow(): void;
  deleteRow(): void;
  
  // Column operations
  insertColumnLeft(): void;
  insertColumnRight(): void;
  deleteColumn(): void;
  
  // Table state and utilities
  getTable(range?: Range): [TableContainer | null, TableRow | null, TableCell | null, number];
}

Table Module Options

interface TableOptions {
  // Currently no specific options for Table module
}

Usage Examples:

import Quill from 'quill';

// Enable table module
const quill = new Quill('#editor', {
  modules: {
    table: true
  }
});

// Get table module instance
const tableModule = quill.getModule('table');

// Insert a 3x4 table
tableModule.insertTable(3, 4);

// Table manipulation within existing table
// (requires cursor to be in a table)
tableModule.insertRowAbove();
tableModule.insertColumnRight();
tableModule.deleteRow();
tableModule.deleteColumn();

// Remove entire table
tableModule.deleteTable();

// Balance all table cells (ensure consistent structure)
tableModule.balanceTables();

The Table module automatically registers all table-related formats (TableContainer, TableBody, TableRow, TableCell) and provides a complete API for programmatic table manipulation.

Install with Tessl CLI

npx tessl i tessl/npm-quill

docs

delta-operations.md

editor-core.md

formatting-system.md

index.md

module-system.md

registry-system.md

theme-system.md

tile.json