Skip to content

Commit 926a761

Browse files
committed
feat: initial version of the blocknote extension API
1 parent 95a72b7 commit 926a761

File tree

2 files changed

+418
-18
lines changed

2 files changed

+418
-18
lines changed

adr/2025_06_13-extensions.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# BlockNote Extension API
2+
3+
## Status
4+
5+
Proposed
6+
7+
## Context
8+
9+
BlockNote needs a flexible and powerful extension system to allow developers to add custom functionality to the editor. The extension system should be type-safe, easy to use, and provide hooks into various editor lifecycle events.
10+
11+
## Decision
12+
13+
We will implement an extension system based on a class/interface-based approach that provides:
14+
15+
1. Type-safe extension development
16+
2. Lifecycle hooks for editor events
17+
3. State management capabilities
18+
4. Plugin integration
19+
5. Keyboard shortcut handling
20+
21+
## Implementation Details
22+
23+
### Extension Definition
24+
25+
Extensions can be implemented in two ways:
26+
27+
1. **Class-based approach** - Extending the abstract `BlockNoteExtension` class
28+
2. **Object-based approach** - Implementing the `BlockNoteExtension` interface
29+
30+
### Core Properties
31+
32+
- `key`: Unique identifier for the extension
33+
- `store`: Optional state management using @tanstack/store
34+
- `priority`: Optional number to determine extension execution order
35+
- `plugins`: Optional array of ProseMirror plugins
36+
- `keyboardShortcuts`: Optional record of keyboard shortcut handlers
37+
38+
### Lifecycle Hooks
39+
40+
Extensions can implement the following lifecycle hooks:
41+
42+
#### `onCreate`
43+
44+
- Called during editor initialization
45+
- View is not yet mounted
46+
- Use for initial setup
47+
48+
#### `onMount`
49+
50+
- Called when editor is mounted
51+
- View is available
52+
- Use for DOM-related setup
53+
54+
#### `onUnmount`
55+
56+
- Called when editor is unmounted
57+
- View will be unavailable after execution
58+
- Use for cleanup
59+
60+
### Event Handlers
61+
62+
#### `onChange`
63+
64+
- Triggered when editor content changes
65+
- Provides access to change information via `getChanges()`
66+
67+
#### `onSelectionChange`
68+
69+
- Triggered when editor selection changes
70+
- Provides access to current selection via `getSelection()`
71+
72+
#### `onBeforeChange`
73+
74+
- Called before changes are applied
75+
- Can cancel changes by returning `false`
76+
- Provides access to changes and transaction
77+
78+
#### `onTransaction`
79+
80+
- Called when a ProseMirror transaction is applied
81+
- Provides access to the transaction object
82+
83+
### Example Implementation
84+
85+
```typescript
86+
// Class-based approach
87+
class MyExtension extends BlockNoteExtension<{ abc: number[] }> {
88+
public key = "my-extension";
89+
public store = new Store({ abc: [1, 2, 3] });
90+
91+
constructor(options: { myCustomOption: string }) {
92+
super();
93+
}
94+
}
95+
96+
// Object-based approach
97+
function myExtension(options: { myCustomOption: string }): BlockNoteExtension<{ state: number }> {
98+
const myState = new Store({ state: 0 });
99+
return {
100+
key: "my-extension",
101+
store: myState,
102+
onMount(context) {
103+
context.editor.extensions.myExtension = this;
104+
myState.setState({ state: 1 });
105+
},
106+
};
107+
}
108+
```
109+
110+
## API Reference
111+
112+
### Types
113+
114+
```typescript
115+
type Schema = [BlockSchema, InlineContentSchema, StyleSchema];
116+
117+
interface BlockNoteExtension<State, BSchema extends Schema = Schema> {
118+
key: string;
119+
store?: Store<State>;
120+
priority?: number;
121+
plugins?: Plugin[];
122+
keyboardShortcuts?: Record<string, (context: BlockNoteExtensionContext<BSchema>) => boolean>;
123+
onCreate?: (context: BlockNoteExtensionContext<BSchema>) => void;
124+
onMount?: (context: BlockNoteExtensionContext<BSchema>) => void;
125+
onUnmount?: (context: BlockNoteExtensionContext<BSchema>) => void;
126+
onChange?: (context: BlockNoteExtensionContext<BSchema> & { getChanges: () => BlocksChanged }) => void;
127+
onSelectionChange?: (context: BlockNoteExtensionContext<BSchema> & { getSelection: () => Selection<any, any, any> | undefined }) => void;
128+
onBeforeChange?: (context: BlockNoteExtensionContext<BSchema> & { getChanges: () => BlocksChanged; tr: Transaction }) => boolean | void;
129+
onTransaction?: (context: BlockNoteExtensionContext<BSchema> & { tr: Transaction }) => void;
130+
}
131+
132+
interface BlockNoteExtensionContext<BSchema extends Schema> {
133+
editor: BlockNoteEditor<BSchema[0], BSchema[1], BSchema[2]>;
134+
}
135+
```
136+
137+
### Properties
138+
139+
#### `key`
140+
141+
- **Type**: `string`
142+
- **Required**: Yes
143+
- **Description**: Unique identifier for the extension. Must be unique across all extensions.
144+
145+
#### `store`
146+
147+
- **Type**: `Store<State>`
148+
- **Required**: No
149+
- **Description**: State management store using @tanstack/store. Used to maintain extension state.
150+
151+
#### `priority`
152+
153+
- **Type**: `number`
154+
- **Required**: No
155+
- **Description**: Determines the order in which extensions are applied. Higher numbers are applied first.
156+
157+
#### `plugins`
158+
159+
- **Type**: `Plugin[]`
160+
- **Required**: No
161+
- **Description**: Array of ProseMirror plugins to be added to the editor.
162+
163+
#### `keyboardShortcuts`
164+
165+
- **Type**: `Record<string, (context: BlockNoteExtensionContext<BSchema>) => boolean>`
166+
- **Required**: No
167+
- **Description**: Map of keyboard shortcuts to handler functions. Return `true` to prevent other extensions from handling the shortcut.
168+
169+
### Methods
170+
171+
#### `onCreate(context: BlockNoteExtensionContext<BSchema>)`
172+
173+
- **Called**: During editor initialization
174+
- **Context**: Editor instance available, view not mounted
175+
- **Use Case**: Initial setup, configuration
176+
177+
#### `onMount(context: BlockNoteExtensionContext<BSchema>)`
178+
179+
- **Called**: When editor is mounted
180+
- **Context**: Editor instance and view available
181+
- **Use Case**: DOM-related setup, event listeners
182+
183+
#### `onUnmount(context: BlockNoteExtensionContext<BSchema>)`
184+
185+
- **Called**: When editor is unmounted
186+
- **Context**: Editor instance available, view will be removed
187+
- **Use Case**: Cleanup, removing event listeners
188+
189+
#### `onChange(context: BlockNoteExtensionContext<BSchema> & { getChanges: () => BlocksChanged })`
190+
191+
- **Called**: When editor content changes
192+
- **Context**: Editor instance and change information
193+
- **Use Case**: Reacting to content changes
194+
195+
#### `onSelectionChange(context: BlockNoteExtensionContext<BSchema> & { getSelection: () => Selection<any, any, any> | undefined })`
196+
197+
- **Called**: When editor selection changes
198+
- **Context**: Editor instance and selection information
199+
- **Use Case**: Reacting to selection changes
200+
201+
#### `onBeforeChange(context: BlockNoteExtensionContext<BSchema> & { getChanges: () => BlocksChanged; tr: Transaction })`
202+
203+
- **Called**: Before changes are applied
204+
- **Context**: Editor instance, changes, and transaction
205+
- **Return**: `boolean | void` - Return `false` to cancel the change
206+
- **Use Case**: Validating or modifying changes before they're applied
207+
208+
#### `onTransaction(context: BlockNoteExtensionContext<BSchema> & { tr: Transaction })`
209+
210+
- **Called**: When a ProseMirror transaction is applied
211+
- **Context**: Editor instance and transaction
212+
- **Use Case**: Reacting to or modifying transactions
213+
214+
### Usage Examples
215+
216+
#### Basic Extension
217+
218+
```typescript
219+
const basicExtension: BlockNoteExtension<{ count: number }> = {
220+
key: "basic",
221+
store: new Store({ count: 0 }),
222+
onMount(context) {
223+
console.log("Editor mounted");
224+
}
225+
};
226+
```
227+
228+
#### Extension with Keyboard Shortcuts
229+
230+
```typescript
231+
const shortcutExtension: BlockNoteExtension<{}> = {
232+
key: "shortcuts",
233+
keyboardShortcuts: {
234+
"Mod-b": (context) => {
235+
// Handle bold shortcut
236+
return true; // Prevent other extensions from handling
237+
}
238+
}
239+
};
240+
```
241+
242+
#### Extension with State Management
243+
244+
```typescript
245+
class StatefulExtension extends BlockNoteExtension<{ items: string[] }> {
246+
key = "stateful";
247+
store = new Store({ items: [] });
248+
249+
onMount(context) {
250+
this.store.setState({ items: ["new item"] });
251+
}
252+
}
253+
```

0 commit comments

Comments
 (0)