Overview
Hooks provide a way for plugins to tap into the editor's lifecycle and modify behavior at specific points. They enable a plugin architecture where functionality can be extended without modifying core code.
The hook system is inspired by webpack's tapable system and Vue's plugin system. It provides both synchronous and asynchronous hooks, allowing for flexible plugin integration.
Synchronous Hooks
Basic SyncHook
Synchronous hooks execute callbacks in sequence. Each callback receives the same arguments:
export class SyncHook {
#callbacks = [];
tap(fn) {
this.#callbacks.push(fn);
}
call(...args) {
this.#callbacks.forEach(callback => {
callback(...args);
});
}
untap(fn) {
const index = this.#callbacks.indexOf(fn);
if (index > -1) {
this.#callbacks.splice(index, 1);
}
}
}
// Usage
const hooks = {
beforeOperation: new SyncHook(),
afterOperation: new SyncHook(),
render: new SyncHook()
};
// Register callbacks
hooks.beforeOperation.tap((operation) => {
console.log('Before operation:', operation);
});
hooks.beforeOperation.tap((operation) => {
// Can modify operation
if (operation.type === 'insertText' && operation.text.length > 100) {
operation.text = operation.text.substring(0, 100);
}
});
// Call hook
hooks.beforeOperation.call(operation);Waterfall Hook
Waterfall hooks pass return values to the next callback:
export class SyncWaterfallHook {
#callbacks = [];
tap(fn) {
this.#callbacks.push(fn);
}
call(initialValue) {
return this.#callbacks.reduce((value, callback) => {
return callback(value);
}, initialValue);
}
}
// Usage: Transform document through multiple plugins
const transformHook = new SyncWaterfallHook();
transformHook.tap((doc) => {
// Plugin 1: Normalize whitespace
return normalizeWhitespace(doc);
});
transformHook.tap((doc) => {
// Plugin 2: Validate structure
return validateStructure(doc);
});
const transformed = transformHook.call(originalDocument);Asynchronous Hooks
Parallel Hook
Asynchronous hooks allow parallel execution:
export class AsyncParallelHook {
#callbacks = [];
tapPromise(fn) {
this.#callbacks.push(fn);
}
async promise() {
// Execute all callbacks in parallel
await Promise.all(this.#callbacks.map(callback => callback()));
}
}
// Usage
const hooks = {
initAsync: new AsyncParallelHook()
};
// Multiple plugins can initialize in parallel
hooks.initAsync.tapPromise(async () => {
await loadUserPreferences();
});
hooks.initAsync.tapPromise(async () => {
await loadDocumentHistory();
});
// Both run in parallel
await hooks.initAsync.promise();Series Hook
Series hooks execute sequentially:
export class AsyncSeriesHook {
#callbacks = [];
tapPromise(fn) {
this.#callbacks.push(fn);
}
async promise() {
// Execute callbacks sequentially
for (const callback of this.#callbacks) {
await callback();
}
}
}
// Usage
const hooks = {
saveAsync: new AsyncSeriesHook()
};
// Save operations must be sequential
hooks.saveAsync.tapPromise(async () => {
await saveToLocalStorage();
});
hooks.saveAsync.tapPromise(async () => {
await syncToServer();
});
// Run sequentially
await hooks.saveAsync.promise();Hook Lifecycle Management
Hook Definition
Define a complete hook lifecycle for editor operations:
// Editor hooks definition
const hooks = {
// Initialization
init: new SyncHook(),
initAsync: new AsyncParallelHook(),
ready: new SyncHook(),
// Operations
beforeOperation: new SyncHook(),
operation: new SyncHook(),
afterOperation: new SyncHook(),
// Rendering
beforeRender: new SyncHook(),
render: new SyncHook(),
afterRender: new SyncHook(),
// Selection
selectionChange: new SyncHook(),
// Cleanup
destroy: new SyncHook()
};
class Editor {
constructor() {
this.#hooks = hooks;
}
applyOperation(operation) {
// Before operation hooks
this.#hooks.beforeOperation.call(operation);
// Apply operation
this.#doApplyOperation(operation);
// Operation hooks
this.#hooks.operation.call(operation);
// After operation hooks
this.#hooks.afterOperation.call(operation);
// Trigger render
this.render();
}
render() {
const doc = this.getDocument();
this.#hooks.beforeRender.call(doc);
this.#doRender(doc);
this.#hooks.render.call(doc);
this.#hooks.afterRender.call(doc);
}
}Hook Integration
Plugins can integrate with hooks:
class HistoryPlugin {
apply(editor) {
// Hook into operations
editor.hooks.beforeOperation.tap((operation) => {
// Save state before operation
this.#saveState(editor.getDocument());
});
editor.hooks.afterOperation.tap((operation) => {
// Update history after operation
this.#updateHistory(operation);
});
}
}
class ValidationPlugin {
apply(editor) {
// Hook into rendering
editor.hooks.beforeRender.tap((doc) => {
// Validate document before rendering
const errors = this.#validate(doc);
if (errors.length > 0) {
console.warn('Validation errors:', errors);
}
});
}
}Advanced Patterns
Advanced hook patterns for complex scenarios:
// Conditional hook execution
class ConditionalHook extends SyncHook {
call(...args) {
this.#callbacks.forEach(callback => {
if (callback.condition(...args)) {
callback.fn(...args);
}
});
}
}
// Hook with priority
class PriorityHook extends SyncHook {
tap(fn, priority = 0) {
this.#callbacks.push({ fn, priority });
this.#callbacks.sort((a, b) => b.priority - a.priority);
}
call(...args) {
this.#callbacks.forEach(({ fn }) => {
fn(...args);
});
}
}
// Hook with context
class ContextHook extends SyncHook {
call(context, ...args) {
this.#callbacks.forEach(callback => {
callback.call(context, ...args);
});
}
}Related Pages
Editor Architecture
Overview of editor architecture patterns
Asynchronous Initialization
Promise-based initialization and lifecycle
Event System
Event system architecture and patterns
Performance Optimization
Optimization strategies for large documents
Rendering Pipeline
Rendering pipeline and update scheduling