contenteditable-visualizer
v0.1.4
Published
SDK for visualizing and tracking contenteditable events, ranges, and DOM changes
Maintainers
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-visualizerQuick 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 tooptions(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 overlaylogEvents(boolean, default:true) - Enable event loggingsnapshots(boolean, default:true) - Enable snapshot management functionalitypanel(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 configurationboolean- Enable/disable panelFloatingPanelConfigobject:position('top-right' | 'top-left' | 'bottom-right' | 'bottom-left', default:'bottom-right') - Panel positiontheme('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 resizingtoggleButtonSize(number, default:48) - Toggle button size in pixelspanelWidth(number, default:500) - Initial panel width in pixelspanelHeight(number, default:600) - Initial panel height in pixelspanelMinWidth(number, default:300) - Minimum panel width in pixelspanelMinHeight(number, default:200) - Minimum panel height in pixelspanelMaxWidth(number | string, default:'90vw') - Maximum panel widthpanelMaxHeight(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). Usedocument.bodyfor 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 logssnapshots- All stored snapshotsenvironment- 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. IncludesgetTargetRanges()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 IDtimestamp- When the snapshot was captured (ISO 8601 string)trigger- What triggered the snapshot ('manual','auto', or custom string)triggerDetail- Optional description of the triggerenvironment- Environment information:os- Operating system nameosVersion- OS versionbrowser- Browser namebrowserVersion- Browser versiondevice- Device typeisMobile- Whether running on mobile device
eventLogs- All events captured up to that pointdomBefore- DOM state before (if captured)domAfter- DOM state afterranges- Selection and composition ranges at snapshot timedomChangeResult- 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 bestartContainer/endContainerin 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
throttleSelectionoption. - Log Limits: Event logs are limited to 1000 by default. Set
maxLogsto 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: relativeor usecontainer: document.bodyoption - Check that
visualize: trueis set - Verify the element is actually contenteditable
Events not logging
- Check that
logEvents: trueis 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: trueis set - Verify IndexedDB is available in your browser
- Check browser console for errors
Performance issues
- Reduce
maxLogsto limit memory usage - Increase
throttleSelectiondelay - 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:
Initialization (
initialize)- Called when
registerPlugin()is invoked - Receives editor instance and visualizer instance
- Sets up internal references
- Called when
Attachment (
attach)- Called when visualizer is attached (or immediately if already attached)
- Set up event listeners, observers, etc.
- Start monitoring editor state
Detachment (
detach)- Called when visualizer is detached
- Clean up event listeners, observers, etc.
- Stop monitoring (but keep plugin registered)
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
Error Handling
protected onAttach(): void { try { // Your attachment logic } catch (error) { console.error(`[${this.metadata.id}] Failed to attach:`, error); } }Resource Cleanup
- Always clean up event listeners in
onDetach() - Clear timers, observers, and subscriptions
- Release references to prevent memory leaks
- Always clean up event listeners in
State Management
- Keep plugin state separate from editor state
- Limit history/event arrays to prevent memory issues
- Use
getState()andgetEvents()for snapshot integration
Plugin Options
- Use TypeScript interfaces for type-safe options
- Provide sensible defaults
- Validate options in constructor or
onInitialize()
Visualizer Integration
- Access visualizer methods when needed (e.g.,
visualizer.captureSnapshot()) - Don't modify visualizer state directly
- Use visualizer's error handling if available
- Access visualizer methods when needed (e.g.,
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
