npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

contenteditable-visualizer

v0.1.4

Published

SDK for visualizing and tracking contenteditable events, ranges, and DOM changes

Readme

contenteditable-visualizer

SDK for visualizing and tracking contenteditable events, ranges, and DOM changes. Works with any editor (ProseMirror, Slate.js, Editor.js, Lexical, etc.) without React dependencies.

Features

  • 🎯 Range Visualization - Visualize selection, composition, beforeinput, and input ranges with SVG overlays
  • 📊 DOM Change Tracking - Track text node changes (added, deleted, modified, moved) in real-time
  • 📝 Event Logging - Capture all contenteditable events with detailed information (beforeinput, input, composition events, selection changes)
  • 📸 Snapshot Management - Capture and store snapshots with IndexedDB for debugging and analysis
  • 🤖 AI Prompt Generation - Automatically generate structured prompts from snapshots for AI analysis
  • 🎨 Floating UI Panel - Built-in event viewer and snapshot history viewer with toggle button and resizing support
  • 🔍 Invisible Character Visualization - Visualize zero-width spaces, line feeds, tabs, and other invisible characters
  • 📍 Boundary Markers - Visual indicators when selection is at text node or element boundaries
  • 🔌 Framework Agnostic - Pure TypeScript/DOM API, no React or other framework dependencies
  • 🔌 Plugin System - Extensible plugin system for editor-specific integrations (ProseMirror, Slate.js, etc.)
  • Performance Optimized - Throttled selection changes, configurable log limits, efficient DOM tracking
  • 🎨 Customizable - Custom color schemes, panel sizing, container options, and error handling callbacks

Installation

npm install contenteditable-visualizer
# or
pnpm add contenteditable-visualizer
# or
yarn add contenteditable-visualizer

Quick Start

import { createVisualizer } from 'contenteditable-visualizer';

const editorElement = document.querySelector('[contenteditable]');
const visualizer = createVisualizer(editorElement, {
  visualize: true,
  logEvents: true,
  snapshots: true,
  panel: true,
});

// Capture a snapshot manually
await visualizer.captureSnapshot('manual', 'User triggered snapshot');

// Get event logs
const events = visualizer.getEventLogs();

// Export all data
const data = await visualizer.exportData();
console.log(data);

// Clean up when done
visualizer.destroy();

API Reference

createVisualizer(element, options?)

Creates a new visualizer instance and attaches it to the specified element.

Parameters:

  • element (HTMLElement) - The contenteditable element to attach the visualizer to
  • options (ContentEditableVisualizerOptions, optional) - Configuration options

Returns: ContentEditableVisualizer instance

Throws: Error if element is not a valid HTMLElement

Options

Core Options

  • visualize (boolean, default: true) - Enable range visualization overlay
  • logEvents (boolean, default: true) - Enable event logging
  • snapshots (boolean, default: true) - Enable snapshot management functionality
  • panel (boolean, default: true) - Show floating UI panel with event viewer and snapshot history

Panel Options

  • panel (boolean | FloatingPanelConfig, default: true) - Show floating panel or panel configuration
    • boolean - Enable/disable panel
    • FloatingPanelConfig object:
      • position ('top-right' | 'top-left' | 'bottom-right' | 'bottom-left', default: 'bottom-right') - Panel position
      • theme ('light' | 'dark' | 'auto', default: 'auto') - Panel theme (auto detects system preference)
      • container (HTMLElement, optional) - Container to append the panel to (default: document.body)
      • resizable (boolean, default: true) - Enable panel resizing
      • toggleButtonSize (number, default: 48) - Toggle button size in pixels
      • panelWidth (number, default: 500) - Initial panel width in pixels
      • panelHeight (number, default: 600) - Initial panel height in pixels
      • panelMinWidth (number, default: 300) - Minimum panel width in pixels
      • panelMinHeight (number, default: 200) - Minimum panel height in pixels
      • panelMaxWidth (number | string, default: '90vw') - Maximum panel width
      • panelMaxHeight (number | string, default: '90vh') - Maximum panel height

Snapshot Options

  • autoSnapshot (boolean, default: false) - Automatically capture snapshots on input events

Performance Options

  • maxLogs (number, default: 1000) - Maximum number of event logs to keep (0 = unlimited)
  • throttleSelection (number, default: 100) - Throttle delay for selectionchange events in milliseconds

Customization Options

  • container (HTMLElement, optional) - Container for the visualization overlay (default: element itself). Use document.body for fixed positioning.
  • colors (VisualizerColorScheme, optional) - Custom color scheme for visualizations
    {
      selection?: { fill?: string; stroke?: string };
      composition?: { fill?: string; stroke?: string };
      beforeinput?: { fill?: string; stroke?: string };
      input?: { fill?: string; stroke?: string };
      deleted?: { fill?: string; stroke?: string };
      added?: { fill?: string; stroke?: string };
    }
  • sizes (VisualizerSizeOptions, optional) - Custom size options (for future use)
  • onError ((error: Error, context: string) => void, optional) - Error callback function

Methods

Event Logging

getEventLogs(): EventLog[]

Get all logged events.

Returns: Array of event logs

clearEventLogs(): void

Clear all event logs.

onEvent(callback: (log: EventLog) => void): () => void

Register a callback for new events. The callback is called whenever a new event is logged.

Parameters:

  • callback - Function to call when a new event is logged

Returns: Unsubscribe function to remove the callback

Example:

const unsubscribe = visualizer.onEvent((log) => {
  console.log('New event:', log);
});

// Later, to unsubscribe:
unsubscribe();

Snapshot Management

captureSnapshot(trigger?, triggerDetail?): Promise<number>

Manually capture a snapshot of the current editor state.

Parameters:

  • trigger (SnapshotTrigger, optional) - Trigger type (e.g., 'manual', 'auto', 'custom')
  • triggerDetail (string, optional) - Description of what triggered the snapshot

Returns: Promise that resolves to the snapshot ID

Throws: Error if snapshots are not enabled

Example:

const snapshotId = await visualizer.captureSnapshot('manual', 'User clicked save button');
console.log('Snapshot ID:', snapshotId);
getSnapshots(): Promise<Snapshot[]>

Get all stored snapshots.

Returns: Promise that resolves to an array of snapshots

getSnapshot(id: number): Promise<Snapshot | null>

Get a specific snapshot by ID.

Parameters:

  • id - The snapshot ID

Returns: Promise that resolves to the snapshot or null if not found

deleteSnapshot(id: number): Promise<void>

Delete a snapshot by ID.

Parameters:

  • id - The snapshot ID to delete

Throws: Error if deletion fails

clearSnapshots(): Promise<void>

Clear all stored snapshots.

Throws: Error if clearing fails

Visualization

showVisualization(enabled: boolean): void

Enable or disable visualization dynamically.

Parameters:

  • enabled - Whether to enable visualization

Example:

// Disable visualization
visualizer.showVisualization(false);

// Re-enable visualization
visualizer.showVisualization(true);

Data Export

exportData(): Promise<ExportData>

Export all events and snapshots as JSON.

Returns: Promise that resolves to export data object containing:

  • events - Serialized event logs
  • snapshots - All stored snapshots
  • environment - Environment information (OS, browser, device)

Example:

const data = await visualizer.exportData();
const json = JSON.stringify(data, null, 2);

// Download as file
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `visualizer-export-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);

Lifecycle

detach(): void

Detach event listeners and clean up resources. The visualizer can be reattached later by calling methods that require attachment.

Note: This does not remove DOM elements. Use destroy() for complete cleanup.

destroy(): void

Completely remove the visualizer and all UI elements. This:

  • Removes all event listeners
  • Destroys the range visualizer
  • Destroys the floating panel
  • Removes the overlay element
  • Cleans up ResizeObserver and scroll handlers

Example:

// When unmounting or cleaning up
visualizer.destroy();

Usage Examples

Basic Usage

import { createVisualizer } from 'contenteditable-visualizer';

const editorElement = document.getElementById('editor');
const visualizer = createVisualizer(editorElement);

Custom Configuration

const visualizer = createVisualizer(editorElement, {
  visualize: true,
  logEvents: true,
  snapshots: true,
  panel: {
    position: 'top-right',
    theme: 'dark',
    resizable: true,
    panelWidth: 600,
    panelHeight: 700,
  },
  autoSnapshot: false,
  maxLogs: 500,
  throttleSelection: 150,
  colors: {
    selection: {
      fill: 'rgba(59, 130, 246, 0.2)',
      stroke: 'rgba(59, 130, 246, 0.8)',
    },
    input: {
      fill: 'rgba(16, 185, 129, 0.2)',
      stroke: 'rgba(16, 185, 129, 0.8)',
    },
  },
  onError: (error, context) => {
    console.error(`Visualizer error in ${context}:`, error);
  },
});

Without Floating Panel

const visualizer = createVisualizer(editorElement, {
  panel: false,
});

// Use your own UI to display events
visualizer.onEvent((log) => {
  console.log('New event:', log);
  // Update your custom UI
});

Fixed Overlay Container

// Attach overlay to document.body for fixed positioning
const visualizer = createVisualizer(editorElement, {
  container: document.body,
});

Integration with ProseMirror

import { EditorView } from 'prosemirror-view';
import { createVisualizer } from 'contenteditable-visualizer';

const view = new EditorView(dom, {
  state,
  // ... other options
});

// Attach visualizer to the editor's DOM
const visualizer = createVisualizer(view.dom, {
  visualize: true,
  logEvents: true,
  container: document.body, // Use fixed positioning for ProseMirror
});

// Register a ProseMirror plugin (from separate package)
// import { ProseMirrorPlugin } from '@contenteditable/prosemirror';
// const plugin = new ProseMirrorPlugin();
// visualizer.registerPlugin(plugin, view);

Integration with Slate.js

import { createEditor } from 'slate';
import { createVisualizer } from 'contenteditable-visualizer';

const editor = createEditor();
const editorElement = document.querySelector('[data-slate-editor]');

const visualizer = createVisualizer(editorElement, {
  visualize: true,
  logEvents: true,
});

// Register a Slate plugin (from separate package)
// import { SlatePlugin } from '@contenteditable/slate';
// const plugin = new SlatePlugin();
// visualizer.registerPlugin(plugin, editor);

Integration with Editor.js

import EditorJS from '@editorjs/editorjs';
import { createVisualizer } from 'contenteditable-visualizer';

const editor = new EditorJS({
  holder: 'editorjs',
});

editor.isReady.then(() => {
  const contentEditable = document.querySelector('[contenteditable]');
  const visualizer = createVisualizer(contentEditable, {
    visualize: true,
    logEvents: true,
  });
});

Event Monitoring

const visualizer = createVisualizer(editorElement);

// Monitor all events
visualizer.onEvent((log) => {
  switch (log.type) {
    case 'beforeinput':
      console.log('Before input:', log.event.inputType);
      break;
    case 'input':
      console.log('Input:', log.event.inputType);
      break;
    case 'compositionstart':
      console.log('IME composition started');
      break;
    case 'selectionchange':
      console.log('Selection changed');
      break;
  }
});

Snapshot Workflow

const visualizer = createVisualizer(editorElement, {
  snapshots: true,
  autoSnapshot: false, // Manual snapshots only
});

// Capture snapshot on specific condition
if (shouldCaptureSnapshot) {
  const id = await visualizer.captureSnapshot('custom', 'Important state change');
  console.log('Captured snapshot:', id);
}

// Get all snapshots
const snapshots = await visualizer.getSnapshots();
console.log('Total snapshots:', snapshots.length);

// Export for analysis
const data = await visualizer.exportData();

Event Types

The visualizer captures the following events:

  • beforeinput - Fired before input is processed. Includes getTargetRanges() information.
  • input - Fired after input is processed. Includes DOM change detection results.
  • compositionstart - IME composition started (for languages like Japanese, Chinese, Korean).
  • compositionupdate - IME composition updated.
  • compositionend - IME composition ended.
  • selectionchange - Selection changed (throttled for performance).

Snapshot Structure

Each snapshot includes:

  • id - Unique snapshot ID
  • timestamp - When the snapshot was captured (ISO 8601 string)
  • trigger - What triggered the snapshot ('manual', 'auto', or custom string)
  • triggerDetail - Optional description of the trigger
  • environment - Environment information:
    • os - Operating system name
    • osVersion - OS version
    • browser - Browser name
    • browserVersion - Browser version
    • device - Device type
    • isMobile - Whether running on mobile device
  • eventLogs - All events captured up to that point
  • domBefore - DOM state before (if captured)
  • domAfter - DOM state after
  • ranges - Selection and composition ranges at snapshot time
  • domChangeResult - Detected DOM changes (if available)

Type Definitions

EventLog

type EventLog = {
  type: 'beforeinput' | 'input' | 'compositionstart' | 'compositionupdate' | 'compositionend' | 'selectionchange';
  timestamp: number;
  event: Event; // Original event object
  range: Range | null; // Selection range at event time
  // Additional properties depending on event type
};

Snapshot

type Snapshot = {
  id: number;
  timestamp: string;
  trigger: string;
  triggerDetail?: string;
  environment: {
    os: string;
    osVersion: string;
    browser: string;
    browserVersion: string;
    device: string;
    isMobile: boolean;
  };
  eventLogs: EventLog[];
  domBefore?: any;
  domAfter: any;
  ranges?: any;
  domChangeResult?: DomChangeResult;
};

ExportData

type ExportData = {
  events: any[];
  snapshots: Snapshot[];
  environment: {
    os: string;
    osVersion: string;
    browser: string;
    browserVersion: string;
    device: string;
    isMobile: boolean;
  };
};

Browser Support

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)
  • Mobile browsers (iOS Safari, Chrome Mobile)

Note: Requires modern browser features:

  • IndexedDB (for snapshot storage)
  • ResizeObserver (for editor resize handling)
  • getTargetRanges() API (for beforeinput events)

Browser-Specific Behavior

ContentEditable behavior, especially for contenteditable="false" elements, may vary between browsers:

  • Chrome/Edge: Generally consistent behavior. contenteditable="false" elements can be startContainer/endContainer in Range API.
  • Firefox: Similar to Chrome, but may handle edge cases differently.
  • Safari: May have different behavior, especially on iOS devices.
  • Mobile browsers: Additional variations due to different input methods and touch handling.

Important: The SDK captures environment information (OS, browser, device) in snapshots to help identify browser-specific issues. Always test your implementation across different browsers and devices.

Performance Considerations

  • Selection Change Throttling: Selection change events are throttled by default (100ms) to avoid performance issues. Adjust with throttleSelection option.
  • Log Limits: Event logs are limited to 1000 by default. Set maxLogs to 0 for unlimited (not recommended for long sessions).
  • DOM Tracking: Text node tracking uses efficient TreeWalker API and WeakMap for memory management.
  • Overlay Rendering: SVG overlay is efficiently updated only when ranges change.

Advanced Features

Invisible Character Visualization

The visualizer automatically detects and visualizes invisible characters in the selection:

  • ZWNBSP (Zero-Width Non-Breaking Space, \uFEFF) - Red diamond
  • LF (Line Feed, \n) - Blue diamond
  • CR (Carriage Return, \r) - Cyan diamond
  • TAB (Tab, \t) - Purple diamond
  • ZWSP (Zero-Width Space, \u200B) - Pink diamond
  • ZWNJ (Zero-Width Non-Joiner, \u200C) - Rose diamond
  • ZWJ (Zero-Width Joiner, \u200D) - Fuchsia diamond

Each invisible character is marked with a colored diamond at the top of its bounding box, with a dashed line extending downward.

Boundary Markers

When a selection (collapsed or non-collapsed) is at a text node or element boundary, visual markers are displayed:

  • Start boundary: Orange triangle pointing downward (above the text)
  • End boundary: Orange triangle pointing upward (below the text)

This helps identify when the cursor or selection is at the edge of a text node or element.

AI Prompt Generation

Snapshots automatically include AI prompts that can be used for debugging and analysis. The prompt includes:

  • HTML structure of the editor
  • Event logs leading up to the snapshot
  • DOM changes detected
  • Range information
  • Environment details

Access the AI prompt from the snapshot detail view in the floating panel, or via the aiPrompt field in the snapshot object.

Example AI Prompt:

# ContentEditable Event Analysis Request

## Environment Information
- OS: macOS 14.0
- Browser: Chrome 120.0
- Device: Desktop
- Mobile: No

## Snapshot Information
- **Trigger**: \`auto\`
- **Description**: Auto capture (on input event)
- **Detail**: User typed 'hello'
- **Timestamp**: 2024-01-15T10:30:45.123Z

## Event Logs
\`\`\`
[0] selectionchange (t=1705315845123)
  parent: P #editor
  node: #text
  offset: start=5, end=5
  selection: "hello|"

[1] beforeinput (t=1705315845125, Δ=2ms)
  type: insertText
  parent: P #editor
  node: #text
  offset: start=5, end=5
  data: " "
  selection: "hello |"

[2] input (t=1705315845127, Δ=2ms)
  type: insertText
  parent: P #editor
  node: #text
  offset: start=6, end=6
  data: " "
  selection: "hello |"
\`\`\`

## Range Information
\`\`\`
Selection Range:
  startContainer: #text
  startOffset: 6
  endContainer: #text
  endOffset: 6
  collapsed: true

Input Range:
  startContainer: #text
  startOffset: 6
  endContainer: #text
  endOffset: 6
  collapsed: true
\`\`\`

## DOM Change Results
\`\`\`
Added:
  - Text node: " " (offset: 5)

Modified:
  - Text node: "hello" → "hello " (offset: 0)
\`\`\`

## DOM Structure

### Before (beforeinput point)
\`\`\`html
<p id="editor">hello</p>
\`\`\`

### After (current state)
\`\`\`html
<p id="editor">hello </p>
\`\`\`

## Analysis Request

### 1. Event Flow Analysis
- Analyze event occurrence order and time intervals
- Relationship between Selection, Composition, BeforeInput, and Input events
- Track Range position changes between events

### 2. DOM Change Analysis
- Track Before DOM → After DOM changes
- Text node addition/deletion/modification patterns
- Timing and content of browser DOM changes

### 3. Range Analysis
- Compare Ranges at each point: Selection, Composition, BeforeInput, Input
- Range position changes (offset, container)
- Consistency between Range and actual DOM changes

### 4. Problem Diagnosis
- Analyze root causes if abnormal behavior exists
- Mismatch points between browser behavior and expected behavior
- Areas in editor implementation that need improvement

### 5. Solution Proposal
- Propose specific solutions
- Code-level improvement suggestions (if possible)
- Consider browser-specific issues

## Notes

### Terminology
- **Selection Range**: Text range selected by the user
- **Composition Range**: Range during IME input (Japanese, Chinese, Korean, etc.)
- **BeforeInput Range**: Range at beforeinput event point
- **Input Range**: Range at input event point
- **DOM Change Result**: Text node level change tracking results

### Event Sequence
Typical input event flow:
1. \`selectionchange\` - Selection area change
2. \`compositionstart\` (for IME input) - IME input start
3. \`compositionupdate\` (for IME input) - IME input update
4. \`beforeinput\` - Before input (before browser DOM change)
5. \`input\` - After input (after browser DOM change)
6. \`compositionend\` (for IME input) - IME input end
\`\`\`

Usage:

// Capture a snapshot
const snapshotId = await visualizer.captureSnapshot('manual', 'Debugging issue');

// Get the snapshot with AI prompt
const snapshot = await visualizer.getSnapshot(snapshotId);
if (snapshot?.aiPrompt) {
  // Copy to clipboard or send to AI service
  await navigator.clipboard.writeText(snapshot.aiPrompt);
  
  // Or use with AI API
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      model: 'gpt-4',
      messages: [
        {
          role: 'system',
          content: 'You are an expert in contenteditable behavior and browser input events.',
        },
        {
          role: 'user',
          content: snapshot.aiPrompt,
        },
      ],
    }),
  });
}

Panel Resizing

The floating panel supports drag-to-resize functionality:

  • Drag the resize handle (bottom-right corner) to adjust panel size
  • Minimum and maximum sizes are configurable via FloatingPanelConfig
  • Panel size is maintained across sessions

Troubleshooting

Overlay not visible

  • Ensure the editor element has position: relative or use container: document.body option
  • Check that visualize: true is set
  • Verify the element is actually contenteditable

Events not logging

  • Check that logEvents: true is set
  • Verify the element is receiving events (check browser console)
  • Ensure the element is properly attached to the DOM

Snapshots not saving

  • Check that snapshots: true is set
  • Verify IndexedDB is available in your browser
  • Check browser console for errors

Performance issues

  • Reduce maxLogs to limit memory usage
  • Increase throttleSelection delay
  • Disable visualization if not needed: visualize: false
  • Disable floating panel if not needed: panel: false

Plugin System

The SDK includes an extensible plugin system for editor-specific integrations. Editor-specific plugins are provided as separate packages (e.g., @contenteditable-visualizer/prosemirror, @contenteditable-visualizer/slate).

📖 For detailed plugin documentation, see PLUGINS.md

Overview

Plugins allow you to monitor editor-specific state and events beyond the standard DOM events. They integrate seamlessly with the visualizer's lifecycle and can provide additional context for debugging and analysis.

Key Features:

  • Monitor editor-specific state changes (e.g., ProseMirror transactions, Slate operations)
  • Track editor events that aren't captured by standard DOM events
  • Provide editor state snapshots for AI prompt generation
  • Integrate with visualizer lifecycle (attach/detach/destroy)
  • Access visualizer instance for advanced integrations

Plugin Interface

All plugins must implement the VisualizerPlugin interface:

interface VisualizerPlugin {
  readonly metadata: PluginMetadata;
  initialize(editor: any, visualizer: ContentEditableVisualizer): void;
  attach(): void;
  detach(): void;
  getState?(): any;
  getEvents?(): any[];
  destroy(): void;
}

Plugin Metadata:

interface PluginMetadata {
  id: string;              // Unique plugin identifier
  name: string;            // Human-readable plugin name
  version: string;         // Plugin version
  editor: string;          // Editor framework (e.g., 'prosemirror', 'slate')
  description?: string;    // Optional description
}

BasePlugin Class

The BasePlugin class provides a foundation for creating plugins with built-in lifecycle management:

import { BasePlugin } from 'contenteditable-visualizer';
import type { PluginMetadata, PluginOptions } from 'contenteditable-visualizer';

class MyCustomPlugin extends BasePlugin {
  readonly metadata: PluginMetadata = {
    id: 'my-plugin',
    name: 'My Custom Plugin',
    version: '1.0.0',
    editor: 'my-editor',
    description: 'Custom plugin for my editor',
  };

  constructor(options: PluginOptions = {}) {
    super(options);
  }

  // Called when plugin is initialized with editor and visualizer
  protected onInitialize(): void {
    // Access this.editor and this.visualizer here
  }

  // Called when plugin is attached (visualizer is active)
  protected onAttach(): void {
    // Set up event listeners, observers, etc.
  }

  // Called when plugin is detached (visualizer is paused)
  protected onDetach(): void {
    // Clean up event listeners, observers, etc.
  }

  // Called when plugin is destroyed
  protected onDestroy(): void {
    // Final cleanup
  }

  // Optional: Return current editor state
  getState(): any {
    return {
      // Editor-specific state
    };
  }

  // Optional: Return editor events since last snapshot
  getEvents(): any[] {
    return [
      // Array of editor events
    ];
  }
}

Plugin Lifecycle

Plugins follow a specific lifecycle that aligns with the visualizer:

  1. Initialization (initialize)

    • Called when registerPlugin() is invoked
    • Receives editor instance and visualizer instance
    • Sets up internal references
  2. Attachment (attach)

    • Called when visualizer is attached (or immediately if already attached)
    • Set up event listeners, observers, etc.
    • Start monitoring editor state
  3. Detachment (detach)

    • Called when visualizer is detached
    • Clean up event listeners, observers, etc.
    • Stop monitoring (but keep plugin registered)
  4. Destruction (destroy)

    • Called when plugin is unregistered or visualizer is destroyed
    • Final cleanup of all resources
    • Plugin cannot be reused after destruction

Creating a Custom Plugin

Here's a complete example of a custom plugin:

import { BasePlugin } from 'contenteditable-visualizer';
import type { PluginMetadata, PluginOptions } from 'contenteditable-visualizer';
import type { ContentEditableVisualizer } from 'contenteditable-visualizer';

interface MyEditor {
  on(event: string, handler: Function): void;
  off(event: string, handler: Function): void;
  getState(): any;
  getHistory(): any[];
}

interface MyPluginOptions extends PluginOptions {
  config?: {
    trackHistory?: boolean;
    maxHistorySize?: number;
  };
}

class MyEditorPlugin extends BasePlugin {
  readonly metadata: PluginMetadata = {
    id: 'my-editor',
    name: 'My Editor Plugin',
    version: '1.0.0',
    editor: 'my-editor',
    description: 'Monitors My Editor state and events',
  };

  private editor: MyEditor | null = null;
  private visualizer: ContentEditableVisualizer | null = null;
  private eventHistory: any[] = [];
  private handlers: Map<string, Function> = new Map();

  constructor(options: MyPluginOptions = {}) {
    super(options);
  }

  protected onInitialize(): void {
    this.editor = this.editor as MyEditor;
    this.visualizer = this.visualizer;
    
    // Initialize based on options
    const config = this.options.config || {};
    // ... setup based on config
  }

  protected onAttach(): void {
    if (!this.editor) return;

    // Set up event listeners
    const changeHandler = (event: any) => {
      this.eventHistory.push({
        timestamp: Date.now(),
        type: 'change',
        data: event,
      });
      
      // Keep history size limited
      const maxSize = this.options.config?.maxHistorySize ?? 100;
      if (this.eventHistory.length > maxSize) {
        this.eventHistory.shift();
      }
    };

    this.editor.on('change', changeHandler);
    this.handlers.set('change', changeHandler);
  }

  protected onDetach(): void {
    if (!this.editor) return;

    // Remove event listeners
    this.handlers.forEach((handler, event) => {
      this.editor!.off(event, handler);
    });
    this.handlers.clear();
  }

  protected onDestroy(): void {
    this.onDetach();
    this.eventHistory = [];
    this.editor = null;
    this.visualizer = null;
  }

  getState(): any {
    if (!this.editor) return null;
    
    return {
      editorState: this.editor.getState(),
      eventCount: this.eventHistory.length,
    };
  }

  getEvents(): any[] {
    return this.eventHistory.slice();
  }
}

// Usage
const plugin = new MyEditorPlugin({
  enabled: true,
  config: {
    trackHistory: true,
    maxHistorySize: 50,
  },
});

visualizer.registerPlugin(plugin, myEditorInstance);

// Later, get plugin state
const state = plugin.getState();
const events = plugin.getEvents();

// Or access via visualizer
const retrievedPlugin = visualizer.getPlugin('my-editor');
if (retrievedPlugin) {
  const pluginState = retrievedPlugin.getState?.();
}

Plugin Registration

Plugins are registered with the visualizer instance:

// Register a plugin
visualizer.registerPlugin(plugin, editorInstance);

// Get a specific plugin
const plugin = visualizer.getPlugin('plugin-id');

// Get all registered plugins
const plugins = visualizer.getPlugins();

// Unregister a plugin
visualizer.unregisterPlugin('plugin-id');

Important Notes:

  • Plugins are automatically attached when the visualizer is attached
  • Plugins are automatically detached when the visualizer is detached
  • Plugins are automatically destroyed when the visualizer is destroyed
  • You can manually unregister plugins before visualizer destruction

Best Practices

  1. Error Handling

    protected onAttach(): void {
      try {
        // Your attachment logic
      } catch (error) {
        console.error(`[${this.metadata.id}] Failed to attach:`, error);
      }
    }
  2. Resource Cleanup

    • Always clean up event listeners in onDetach()
    • Clear timers, observers, and subscriptions
    • Release references to prevent memory leaks
  3. State Management

    • Keep plugin state separate from editor state
    • Limit history/event arrays to prevent memory issues
    • Use getState() and getEvents() for snapshot integration
  4. Plugin Options

    • Use TypeScript interfaces for type-safe options
    • Provide sensible defaults
    • Validate options in constructor or onInitialize()
  5. Visualizer Integration

    • Access visualizer methods when needed (e.g., visualizer.captureSnapshot())
    • Don't modify visualizer state directly
    • Use visualizer's error handling if available

Plugin Options

Plugins can accept configuration options:

interface PluginOptions {
  enabled?: boolean;           // Enable/disable plugin (default: true)
  config?: Record<string, any>; // Plugin-specific configuration
}

// Usage
const plugin = new MyPlugin({
  enabled: true,
  config: {
    trackHistory: true,
    maxSize: 100,
  },
});

Accessing Visualizer from Plugin

Plugins have access to the visualizer instance:

protected onAttach(): void {
  if (!this.visualizer) return;
  
  // Access visualizer methods
  const logs = this.visualizer.getEventLogs();
  const snapshots = await this.visualizer.getSnapshots();
  
  // Trigger snapshots programmatically
  await this.visualizer.captureSnapshot('plugin', 'Plugin triggered snapshot');
}

Editor-Specific Plugins

Editor-specific plugins are distributed as separate packages:

  • @contenteditable/prosemirror - ProseMirror integration
  • @contenteditable/slate - Slate.js integration
  • @contenteditable/lexical - Lexical integration
  • @contenteditable/editorjs - Editor.js integration

These packages provide pre-built plugins with editor-specific monitoring capabilities.

License

MIT