Overview
The event system is the communication layer between the editor core and plugins. It allows plugins to listen to and react to editor state changes, user interactions, and lifecycle events.
A well-designed event system provides type safety, efficient event propagation, and flexible event handling patterns.
Event Bubbling and Capturing
Bubbling Implementation
Implement event bubbling similar to DOM events, allowing parent handlers to process events before children:
class EventEmitter {
#listeners = new Map();
on(event, handler, options) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(handler);
}
emit(event, data, bubbles = true) {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.forEach(handler => {
handler(data);
});
}
// Bubble up to parent
if (bubbles && this.#parent) {
this.#parent.emit(event, data, true);
}
}
}
// Usage: Node-level events bubble to document
class DocumentNode {
constructor(parent) {
this.#parent = parent;
this.#emitter = new EventEmitter();
}
emit(event, data) {
// Emit on this node
this.#emitter.emit(event, data, false);
// Bubble to parent
if (this.#parent) {
this.#parent.emit(event, data);
}
}
}
// Handler on document receives all node events
document.on('textInsert', (data) => {
console.log('Text inserted anywhere in document:', data);
});Capture Phase
Support capture phase for events that need to be handled before bubbling:
class EventEmitter {
#captureListeners = new Map();
#bubbleListeners = new Map();
on(event, handler, options = {}) {
const listeners = options.capture
? this.#captureListeners
: this.#bubbleListeners;
if (!listeners.has(event)) {
listeners.set(event, new Set());
}
listeners.get(event).add(handler);
}
emit(event, data) {
// Capture phase: parent to child
this.#emitCapture(event, data);
// Target phase: current node
this.#emitTarget(event, data);
// Bubble phase: child to parent
this.#emitBubble(event, data);
}
#emitCapture(event, data) {
// Emit to capture listeners
const handlers = this.#captureListeners.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
// Continue to children
this.#children.forEach(child => {
child.#emitCapture(event, data);
});
}
#emitBubble(event, data) {
// Emit to bubble listeners
const handlers = this.#bubbleListeners.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
// Continue to parent
if (this.#parent) {
this.#parent.#emitBubble(event, data);
}
}
}Event Delegation Pattern
Delegation Benefits
Event delegation reduces memory usage and improves performance:
- Single listener on container instead of many on individual nodes
- Works with dynamically added nodes
- Reduces event listener overhead
- Simplifies event management
Delegation Implementation
Use event delegation to handle events efficiently:
class Editor {
constructor(container) {
this.#container = container;
// Single listener on container
this.#container.addEventListener('click', (e) => {
this.#handleClick(e);
});
this.#container.addEventListener('input', (e) => {
this.#handleInput(e);
});
}
#handleClick(e) {
const target = e.target;
// Find the model node for this DOM element
const node = this.#findNodeForElement(target);
if (!node) return;
// Dispatch to appropriate handler based on node type
switch (node.type) {
case 'link':
this.#handleLinkClick(node, e);
break;
case 'image':
this.#handleImageClick(node, e);
break;
}
}
#handleInput(e) {
// Convert DOM input to model operation
const operation = this.#domToOperation(e);
this.applyOperation(operation);
}
#findNodeForElement(element) {
// Walk up DOM tree to find node marker
let current = element;
while (current && current !== this.#container) {
const nodeId = current.getAttribute('data-node-id');
if (nodeId) {
return this.#getNodeById(nodeId);
}
current = current.parentElement;
}
return null;
}
}Custom Event System
Typed Events
Implement a custom event system with typed events:
// Define event types
const EditorEvents = {
OPERATION: 'operation',
SELECTION_CHANGE: 'selectionChange',
DOCUMENT_CHANGE: 'documentChange',
ERROR: 'error'
};
// Typed event emitter
class TypedEventEmitter {
#listeners = new Map();
on(event, handler) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(handler);
}
off(event, handler) {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.delete(handler);
}
}
emit(event, data) {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
}
// Usage
const editor = new TypedEventEmitter();
editor.on(EditorEvents.OPERATION, (data) => {
console.log('Operation:', data.operation);
});
editor.on(EditorEvents.ERROR, (data) => {
console.error('Error:', data.error);
});
editor.emit(EditorEvents.OPERATION, {
operation: { type: 'insertText', text: 'Hello' }
});Event Priority
Support event priority for ordered event handling:
class PriorityEventEmitter {
#listeners = new Map();
on(event, handler, priority = 0) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
const listeners = this.#listeners.get(event);
listeners.push({ handler, priority });
// Sort by priority (higher priority first)
listeners.sort((a, b) => b.priority - a.priority);
}
emit(event, data) {
const listeners = this.#listeners.get(event);
if (listeners) {
listeners.forEach(({ handler }) => {
handler(data);
});
}
}
}
// Usage
const emitter = new PriorityEventEmitter();
// High priority handler (runs first)
emitter.on('operation', (data) => {
console.log('High priority:', data);
}, 100);
// Normal priority handler
emitter.on('operation', (data) => {
console.log('Normal priority:', data);
}, 0);
// Low priority handler (runs last)
emitter.on('operation', (data) => {
console.log('Low priority:', data);
}, -100);Event Lifecycle
Complete event lifecycle management:
class EventLifecycle {
#preHandlers = new Map();
#handlers = new Map();
#postHandlers = new Map();
on(event, handler, phase = 'main') {
const map = phase === 'pre'
? this.#preHandlers
: phase === 'post'
? this.#postHandlers
: this.#handlers;
if (!map.has(event)) {
map.set(event, new Set());
}
map.get(event).add(handler);
}
emit(event, data) {
// Pre-handlers
this.#callHandlers(this.#preHandlers, event, data);
// Main handlers
const result = this.#callHandlers(this.#handlers, event, data);
// Post-handlers
this.#callHandlers(this.#postHandlers, event, data);
return result;
}
#callHandlers(map, event, data) {
const handlers = map.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
// Prevent default behavior
preventDefault(event) {
event.defaultPrevented = true;
}
// Stop propagation
stopPropagation(event) {
event.propagationStopped = true;
}
}Related Pages
Editor Architecture
Overview of editor architecture patterns
Asynchronous Initialization
Promise-based initialization and lifecycle
Hook System
Understanding hook system implementation
Performance Optimization
Optimization strategies for large documents
Rendering Pipeline
Rendering pipeline and update scheduling