Overview
Plugins allow you to extend editor functionality without modifying core code. They can add formatting, handle special content types, integrate with external services, and customize editor behavior.
Plugin benefits:
- Modular architecture - features can be added/removed independently
- Reusable across projects
- Easy to test in isolation
- Community contributions
Plugin Architecture
Plugins integrate with the editor through hooks, commands, and the editor API.
Plugin Interface
All plugins implement a standard interface:
interface Plugin {
name: string;
version?: string;
// Lifecycle methods
install(editor: Editor): void | Promise<void>;
uninstall?(editor: Editor): void | Promise<void>;
// Optional: Plugin configuration
configure?(config: PluginConfig): void;
}
class MyPlugin implements Plugin {
name = 'my-plugin';
version = '1.0.0';
install(editor: Editor) {
// Register hooks, commands, etc.
}
uninstall(editor: Editor) {
// Cleanup
}
}Plugin Lifecycle
Plugins go through a lifecycle:
// 1. Plugin instantiation
const plugin = new MyPlugin();
// 2. Plugin installation (called by editor)
await editor.use(plugin);
// or
await plugin.install(editor);
// 3. Plugin active (handling events, commands, etc.)
// 4. Plugin uninstallation (optional)
await plugin.uninstall?.(editor);
// or
editor.removePlugin(plugin);Creating Plugins
Create a plugin by implementing the Plugin interface and registering hooks or commands.
Basic Plugin Structure
A minimal plugin:
class BasicPlugin implements Plugin {
name = 'basic-plugin';
#editor: Editor | null = null;
install(editor: Editor) {
this.#editor = editor;
// Register event listeners
editor.on('operation', this.#handleOperation.bind(this));
// Register commands
editor.registerCommand('basic:action', this.#handleCommand.bind(this));
}
uninstall(editor: Editor) {
// Remove event listeners
editor.off('operation', this.#handleOperation);
// Unregister commands
editor.unregisterCommand('basic:action');
this.#editor = null;
}
#handleOperation(operation: Operation) {
// Handle operations
}
#handleCommand() {
// Handle command
}
}Using Hooks
Plugins can hook into editor lifecycle and operations:
class HookPlugin implements Plugin {
name = 'hook-plugin';
install(editor: Editor) {
// Hook into operation lifecycle
editor.hooks.beforeOperation.tap((operation) => {
// Modify operation before it's applied
if (operation.type === 'insert') {
// Transform operation
}
});
editor.hooks.afterOperation.tap((operation) => {
// React to operation after it's applied
});
// Hook into rendering
editor.hooks.beforeRender.tap(() => {
// Prepare for render
});
editor.hooks.afterRender.tap(() => {
// Post-render cleanup
});
}
}Adding Commands
Plugins can register commands that can be called programmatically or via keyboard shortcuts:
class CommandPlugin implements Plugin {
name = 'command-plugin';
install(editor: Editor) {
// Register command
editor.registerCommand('format:bold', () => {
const selection = editor.getSelection();
if (selection.isCollapsed) {
// Apply format to current word or selection
editor.toggleFormat('bold', selection);
}
});
// Register keyboard shortcut
editor.registerShortcut('Mod-b', 'format:bold');
}
}Plugin Examples
Real-world plugin examples:
Formatting Plugin
A plugin that adds text formatting:
class FormattingPlugin implements Plugin {
name = 'formatting';
#editor: Editor | null = null;
install(editor: Editor) {
this.#editor = editor;
// Register formatting commands
editor.registerCommand('format:bold', () => this.#toggleBold());
editor.registerCommand('format:italic', () => this.#toggleItalic());
editor.registerCommand('format:underline', () => this.#toggleUnderline());
// Register shortcuts
editor.registerShortcut('Mod-b', 'format:bold');
editor.registerShortcut('Mod-i', 'format:italic');
editor.registerShortcut('Mod-u', 'format:underline');
}
#toggleBold() {
if (!this.#editor) return;
const selection = this.#editor.getSelection();
this.#editor.toggleFormat('bold', selection);
}
#toggleItalic() {
if (!this.#editor) return;
const selection = this.#editor.getSelection();
this.#editor.toggleFormat('italic', selection);
}
#toggleUnderline() {
if (!this.#editor) return;
const selection = this.#editor.getSelection();
this.#editor.toggleFormat('underline', selection);
}
uninstall(editor: Editor) {
editor.unregisterCommand('format:bold');
editor.unregisterCommand('format:italic');
editor.unregisterCommand('format:underline');
this.#editor = null;
}
}Link Plugin
A plugin that handles links:
class LinkPlugin implements Plugin {
name = 'link';
#editor: Editor | null = null;
install(editor: Editor) {
this.#editor = editor;
// Register link command
editor.registerCommand('link:insert', (url: string) => {
const selection = editor.getSelection();
if (selection.isCollapsed) {
// Insert link at cursor
editor.insertLink(url, selection);
} else {
// Wrap selection with link
editor.wrapWithLink(selection, url);
}
});
// Handle link clicks
editor.element.addEventListener('click', (e) => {
const link = (e.target as HTMLElement).closest('a');
if (link && e.ctrlKey) {
// Allow default (open link)
return;
}
if (link) {
e.preventDefault();
// Edit link
this.#editLink(link);
}
});
}
#editLink(link: HTMLElement) {
const url = prompt('Edit URL:', link.getAttribute('href') || '');
if (url !== null) {
link.setAttribute('href', url);
}
}
uninstall(editor: Editor) {
editor.unregisterCommand('link:insert');
this.#editor = null;
}
}Image Plugin
A plugin that handles images:
class ImagePlugin implements Plugin {
name = 'image';
#editor: Editor | null = null;
#uploadUrl: string;
constructor(uploadUrl: string) {
this.#uploadUrl = uploadUrl;
}
install(editor: Editor) {
this.#editor = editor;
// Handle paste
editor.element.addEventListener('paste', async (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (file) {
await this.#handleImagePaste(file);
}
}
}
});
// Register command
editor.registerCommand('image:insert', async (file: File) => {
await this.#uploadAndInsert(file);
});
}
async #handleImagePaste(file: File) {
await this.#uploadAndInsert(file);
}
async #uploadAndInsert(file: File) {
if (!this.#editor) return;
// Upload image
const formData = new FormData();
formData.append('image', file);
const response = await fetch(this.#uploadUrl, {
method: 'POST',
body: formData
});
const { url } = await response.json();
// Insert into editor
const selection = this.#editor.getSelection();
this.#editor.insertImage(url, selection);
}
uninstall(editor: Editor) {
editor.unregisterCommand('image:insert');
this.#editor = null;
}
}Plugin API
Plugins have access to the editor API for manipulating content, selection, and state.
Editor API
Core editor methods:
interface Editor {
// Selection
getSelection(): Selection;
setSelection(selection: Selection): void;
// Content manipulation
insertText(text: string, selection?: Selection): void;
insertNode(node: Node, selection?: Selection): void;
deleteContent(selection: Selection): void;
// Formatting
toggleFormat(format: string, selection: Selection): void;
hasFormat(format: string, selection: Selection): boolean;
// Commands
registerCommand(name: string, handler: Function): void;
unregisterCommand(name: string): void;
executeCommand(name: string, ...args: any[]): void;
// Events
on(event: string, handler: Function): void;
off(event: string, handler: Function): void;
emit(event: string, data?: any): void;
// Hooks
hooks: {
beforeOperation: Hook;
afterOperation: Hook;
beforeRender: Hook;
afterRender: Hook;
};
}Model API
Access to document model:
interface Model {
// Read operations
getNode(path: Path): Node | null;
getText(range?: Range): string;
// Write operations
applyOperation(operation: Operation): void;
// Traversal
walk(callback: (node: Node, path: Path) => void): void;
// Query
findNodes(predicate: (node: Node) => boolean): Node[];
}View API
Access to DOM view:
interface View {
// DOM access
element: HTMLElement;
getNodeElement(node: Node): HTMLElement | null;
// Rendering
render(): void;
updateNode(node: Node): void;
// Selection
getDOMSelection(): Selection | null;
setDOMSelection(selection: Selection): void;
}Best Practices
Key principles for plugin development:
- Keep plugins focused: Each plugin should have a single, well-defined purpose
- Clean up on uninstall: Remove all event listeners, commands, and references
- Use hooks when possible: Prefer hooks over direct API calls for extensibility
- Handle errors gracefully: Don't let plugin errors break the editor
- Document your plugin: Provide clear API documentation and examples
- Test thoroughly: Test with different browsers, IMEs, and edge cases
- Consider performance: Avoid expensive operations in hot paths