Extension Design Guide
Extensions are the primary way to add custom functionality to Barocss Editor. This guide explains how to create extensions step by step.
What You'll Learn
- How extensions work and their role in the editor
- How to create commands and connect them to keybindings
- How to use transactions and operations to modify documents
- How to implement a complete extension
Table of Contents
- What is an Extension?
- Creating Your First Extension
- Commands
- Using Transactions
- Keybindings
- Complete Example: Highlight Extension
What is an Extension?
An Extension is a module that adds new functionality to the editor. Extensions register commands that can be executed by users through keyboard shortcuts or programmatic calls.
Extension Interface
interface Extension {
name: string; // Unique extension identifier
priority?: number; // Execution priority (lower = earlier)
// Lifecycle
onCreate?(editor: Editor): void; // Called when editor is created
onDestroy?(editor: Editor): void; // Called when editor is destroyed
// Command registration
commands?: Command[]; // Commands provided by this extension
// Before hooks (intercept and modify core model changes)
// Only core model changes (Transaction, Selection, Content) are provided as hooks.
// Other changes should use editor.on() events.
onBeforeTransaction?(editor: Editor, transaction: Transaction): Transaction | null | void;
onBeforeSelectionChange?(editor: Editor, selection: SelectionState): SelectionState | null | void;
onBeforeContentChange?(editor: Editor, content: DocumentState): DocumentState | null | void;
// After hooks (notification for core model changes)
// For type safety. Alternatively, you can use editor.on() events.
onTransaction?(editor: Editor, transaction: Transaction): void;
onSelectionChange?(editor: Editor, selection: SelectionState): void;
onContentChange?(editor: Editor, content: DocumentState): void;
}
Note: Only core model changes (Transaction, Selection, Content) are provided as hooks. For other changes (Node, Command, History, etc.), use editor.on() events.
Extension Lifecycle
Creating Your First Extension
Let's create a simple extension that adds a "Insert Hello" command.
Step 1: Define the Extension Class
import { Extension, Editor } from '@barocss/editor-core';
import { transaction, control, insertText } from '@barocss/model';
export class InsertHelloExtension implements Extension {
name = 'insert-hello';
priority = 100;
onCreate(editor: Editor): void {
// Register command
editor.registerCommand({
name: 'insertHello',
execute: async (editor: Editor, payload?: { nodeId?: string }) => {
return await this._executeInsertHello(editor, payload);
},
canExecute: (editor: Editor, payload?: any) => {
// Check if command can be executed
return !!payload?.nodeId || !!editor.selection?.startNodeId;
}
});
}
onDestroy(_editor: Editor): void {
// Cleanup if needed
}
private async _executeInsertHello(
editor: Editor,
payload?: { nodeId?: string }
): Promise<boolean> {
// Get target node ID
const nodeId = payload?.nodeId || editor.selection?.startNodeId;
if (!nodeId) {
return false;
}
// Execute transaction
const result = await transaction(editor, [
...control(nodeId, [
insertText({ text: 'Hello', offset: 0 })
])
]).commit();
return result.success;
}
}
Step 2: Use the Extension
import { Editor } from '@barocss/editor-core';
import { createCoreExtensions } from '@barocss/extensions';
import { InsertHelloExtension } from './insert-hello-extension';
const editor = new Editor({
dataStore,
schema,
extensions: [
...createCoreExtensions(),
new InsertHelloExtension()
]
});
// Execute command programmatically
await editor.executeCommand('insertHello', { nodeId: 'text-1' });
Commands
Commands are executable actions that modify the document. All document changes go through commands.
Command Structure
interface Command {
name: string; // Command identifier
execute: (editor: Editor, payload?: any) => Promise<boolean> | boolean;
canExecute?: (editor: Editor, payload?: any) => boolean;
before?: (editor: Editor, payload?: any) => void;
after?: (editor: Editor, payload?: any) => void;
}
Command Execution Flow
Command Example
editor.registerCommand({
name: 'toggleBold',
execute: async (editor: Editor, payload?: { selection?: ModelSelection }) => {
const selection = payload?.selection || editor.selection;
if (!selection || selection.type !== 'range') {
return false;
}
// Execute transaction to toggle bold mark
const result = await transaction(editor, [
...control(selection.startNodeId, [
toggleMark('bold', [selection.startOffset, selection.endOffset])
])
]).commit();
return result.success;
},
canExecute: (editor: Editor, payload?: any) => {
const selection = payload?.selection || editor.selection;
return !!selection && selection.type === 'range';
}
});
Using Transactions
Commands must use transactions to modify documents. Never directly modify DataStore.
Why Transactions?
- Atomicity: All operations succeed or fail together
- Schema Validation: Operations validated before commit
- Undo/Redo: Transactions recorded for history
- Lock Management: Prevents concurrent conflicts
- Operation Events: Emitted for collaboration
Transaction Pattern
import { transaction, control, insertText, toggleMark } from '@barocss/model';
// Single operation
const result = await transaction(editor, [
insertText({ text: 'Hello', nodeId: 'text-1', offset: 0 })
]).commit();
// Multiple operations
const result = await transaction(editor, [
...control('text-1', [
insertText({ text: 'Hello' }),
toggleMark('bold', [0, 5])
])
]).commit();
if (!result.success) {
console.error('Transaction failed:', result.error);
}
Available Operations
Common operations from @barocss/model:
- Text Operations:
insertText,deleteText,replaceText - Mark Operations:
applyMark,removeMark,toggleMark - Node Operations:
create,update,delete,moveNode - Block Operations:
indentNode,outdentNode,moveBlockUp,moveBlockDown
❌ Wrong Way: Direct DataStore Access
// ❌ NEVER do this
private async _executeBadCommand(editor: Editor): Promise<boolean> {
const dataStore = editor.dataStore;
// Direct modification - bypasses transactions
dataStore.updateNode('node-1', { text: 'Updated' });
// Problems:
// - No transaction guarantee
// - No lock management
// - No history recording
// - No operation events
// - No schema validation
return true;
}
✅ Correct Way: Use Transactions
// ✅ Always use transactions
import { transaction, control, updateNode } from '@barocss/model';
private async _executeGoodCommand(editor: Editor): Promise<boolean> {
const result = await transaction(editor, [
...control('node-1', [
updateNode({ text: 'Updated' })
])
]).commit();
return result.success;
}
Keybindings
Keybindings connect keyboard shortcuts to commands. They can include conditions (when) for context-aware activation.
Registering Keybindings
// In extension onCreate
editor.keybindings.register({
key: 'Mod+b', // Ctrl+B (or Cmd+B on Mac)
command: 'toggleBold',
when: 'editorFocus && editorEditable' // Optional condition
});
Key Format
- Mod:
Ctrlon Windows/Linux,Cmdon Mac - Alt: Alt key
- Shift: Shift key
- Keys:
a-z,0-9,Enter,Backspace,Delete,ArrowUp, etc.
Examples:
Mod+b- Ctrl+B / Cmd+BMod+Shift+h- Ctrl+Shift+H / Cmd+Shift+HAlt+Enter- Alt+EnterEscape- Escape key
When Conditions
Conditions control when keybindings are active (inspired by VS Code):
// Only active when editor is focused and editable
editor.keybindings.register({
key: 'Mod+b',
command: 'toggleBold',
when: 'editorFocus && editorEditable'
});
// Only active when text is selected
editor.keybindings.register({
key: 'Mod+k',
command: 'formatSelection',
when: 'hasSelection'
});
// Multiple conditions
editor.keybindings.register({
key: 'Mod+/',
command: 'toggleComment',
when: 'editorFocus && !readonly'
});
Available Context Variables
editorFocus- Editor has focuseditorEditable- Editor is editablehasSelection- Text is selectedreadonly- Editor is read-only- Custom context via
editor.setContext(key, value)
Complete Example: Highlight Extension
Let's implement a complete Highlight extension that toggles highlight marks on selected text.
Step 1: Define the Extension
import { Extension, Editor, ModelSelection } from '@barocss/editor-core';
import { transaction, control, toggleMark } from '@barocss/model';
export interface HighlightExtensionOptions {
enabled?: boolean;
defaultColor?: string;
keyboardShortcut?: string;
}
export class HighlightExtension implements Extension {
name = 'highlight';
priority = 100;
private _options: HighlightExtensionOptions;
constructor(options: HighlightExtensionOptions = {}) {
this._options = {
enabled: true,
defaultColor: 'yellow',
keyboardShortcut: 'Mod+Shift+h',
...options
};
}
onCreate(editor: Editor): void {
if (!this._options.enabled) return;
// Register command
editor.registerCommand({
name: 'toggleHighlight',
execute: async (editor: Editor, payload?: {
selection?: ModelSelection;
color?: string;
}) => {
return await this._executeToggleHighlight(
editor,
payload?.selection,
payload?.color
);
},
canExecute: (editor: Editor, payload?: { selection?: ModelSelection }) => {
const selection = payload?.selection || editor.selection;
return !!selection && selection.type === 'range';
}
});
// Register keybinding
if (this._options.keyboardShortcut) {
editor.keybindings.register({
key: this._options.keyboardShortcut,
command: 'toggleHighlight',
when: 'editorFocus && editorEditable && hasSelection'
});
}
}
onDestroy(_editor: Editor): void {
// Cleanup if needed
}
private async _executeToggleHighlight(
editor: Editor,
selection?: ModelSelection,
color?: string
): Promise<boolean> {
// Get selection
const currentSelection = selection || editor.selection;
if (!currentSelection || currentSelection.type !== 'range') {
return false;
}
// Validate node
const node = editor.dataStore.getNode(currentSelection.startNodeId);
if (!node || typeof node.text !== 'string') {
return false;
}
// Validate range
const { startOffset, endOffset } = currentSelection;
const text = node.text as string;
if (
startOffset < 0 ||
endOffset > text.length ||
startOffset >= endOffset
) {
return false;
}
// Execute transaction
const result = await transaction(editor, [
...control(currentSelection.startNodeId, [
toggleMark('highlight', [startOffset, endOffset], {
color: color || this._options.defaultColor
})
])
]).commit();
return result.success;
}
}
Step 2: Define Mark Template
import { defineMark, element, data } from '@barocss/dsl';
// Define highlight mark template
defineMark('highlight', element('mark', {
className: 'highlight',
style: {
backgroundColor: (model) => model.attrs?.color || 'yellow',
padding: '2px 0'
}
}, [data('text')]));
Step 3: Use the Extension
import { Editor } from '@barocss/editor-core';
import { createCoreExtensions } from '@barocss/extensions';
import { HighlightExtension } from './highlight-extension';
const editor = new Editor({
dataStore,
schema,
extensions: [
...createCoreExtensions(),
new HighlightExtension({
defaultColor: 'yellow',
keyboardShortcut: 'Mod+Shift+h'
})
]
});
// User can now:
// 1. Select text
// 2. Press Ctrl+Shift+H (or Cmd+Shift+H on Mac)
// 3. Text is highlighted
Extension Best Practices
1. Always Use Transactions
// ✅ Good
const result = await transaction(editor, [/* operations */]).commit();
// ❌ Bad
editor.dataStore.updateNode(id, changes);
2. Validate Inputs
private async _executeCommand(editor: Editor, payload?: any): Promise<boolean> {
// Validate inputs
if (!payload?.nodeId) {
return false;
}
const node = editor.dataStore.getNode(payload.nodeId);
if (!node) {
return false;
}
// Proceed with execution
// ...
}
3. Provide canExecute
editor.registerCommand({
name: 'myCommand',
execute: async (editor, payload) => { /* ... */ },
canExecute: (editor, payload) => {
// Check if command can be executed
return !!payload?.requiredField;
}
});
4. Handle Errors
private async _executeCommand(editor: Editor): Promise<boolean> {
try {
const result = await transaction(editor, [/* operations */]).commit();
if (!result.success) {
console.error('Transaction failed:', result.error);
return false;
}
return true;
} catch (error) {
console.error('Command execution failed:', error);
return false;
}
}
5. Use Extension Options
export interface MyExtensionOptions {
enabled?: boolean;
customOption?: string;
}
export class MyExtension implements Extension {
private _options: MyExtensionOptions;
constructor(options: MyExtensionOptions = {}) {
this._options = {
enabled: true,
customOption: 'default',
...options
};
}
onCreate(editor: Editor): void {
if (!this._options.enabled) return;
// ...
}
}
Extension Checklist
When creating an extension:
- Implement
Extensioninterface - Define
nameand optionalpriority - Register commands in
onCreate - Use transactions for all document modifications
- Provide
canExecutefor commands - Register keybindings (optional)
- Handle errors appropriately
- Clean up in
onDestroyif needed - Test extension thoroughly
Related
- Core Concepts: Decorators - Add visual overlays
- Architecture: Extensions - Extension package details
- Architecture: Editor Core - How editor orchestrates extensions
- Examples: Custom Extensions - More examples