This page predates the current package split. Use the current package guides for checked installation, public imports, and onboarding examples. The detailed examples below have not all been revalidated.
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: ModelSelection): ModelSelection | 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: MaybeSelection): 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 — real bug we fixed in ChecklistExtension
private async _executeBadCommand(editor: Editor): Promise<boolean> {
const dataStore = editor.dataStore;
// Direct modification — bypasses transactions
dataStore.setAttrs(nodeId, { checked: !currentChecked });
// Problems:
// - No undo/redo support (cannot Ctrl+Z)
// - No lock management
// - No history recording
// - No operation events for collaboration
// - No schema validation
return true;
}
✅ Correct Way: Use Transactions
// ✅ Always use transactions — correct pattern from ChecklistExtension
import { transaction, control } from '@barocss/model';
private async _executeGoodCommand(editor: Editor): Promise<boolean> {
const ops = [
...control(nodeId, [
{ type: 'setAttrs', payload: { attrs: { checked: !currentChecked } } }
])
];
const result = await transaction(editor, ops).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