0
# Command System
1
2
The command system in @tiptap/core provides a powerful and flexible way to execute editor actions. Commands can be executed individually, chained together for complex operations, or validated before execution.
3
4
## Capabilities
5
6
### CommandManager
7
8
The CommandManager class handles command execution, chaining, and validation within the editor context.
9
10
```typescript { .api }
11
/**
12
* Manages command execution and validation for the editor
13
*/
14
class CommandManager {
15
/**
16
* Create a new CommandManager instance
17
* @param props - Configuration including editor and state
18
*/
19
constructor(props: {
20
editor: Editor;
21
state: EditorState
22
});
23
24
/** Direct access to individual commands */
25
readonly commands: SingleCommands;
26
27
/**
28
* Create a command chain for sequential execution
29
* @returns ChainedCommands instance for chaining operations
30
*/
31
chain(): ChainedCommands;
32
33
/**
34
* Create command validation interface
35
* @returns CanCommands instance for testing executability
36
*/
37
can(): CanCommands;
38
39
/**
40
* Create a custom command chain with specific transaction and dispatch behavior
41
* @param startTr - Optional starting transaction
42
* @param shouldDispatch - Whether commands should be dispatched immediately
43
* @returns ChainedCommands instance
44
*/
45
createChain(
46
startTr?: Transaction,
47
shouldDispatch?: boolean
48
): ChainedCommands;
49
50
/**
51
* Create a custom command validation instance
52
* @param startTr - Optional starting transaction
53
* @returns CanCommands instance
54
*/
55
createCan(startTr?: Transaction): CanCommands;
56
57
/**
58
* Build command properties for command execution
59
* @param tr - Transaction to build props for
60
* @param shouldDispatch - Whether to include dispatch function
61
* @returns CommandProps object
62
*/
63
buildProps(
64
tr: Transaction,
65
shouldDispatch?: boolean
66
): CommandProps;
67
}
68
```
69
70
### Single Commands
71
72
Interface for executing individual commands that return boolean success values.
73
74
```typescript { .api }
75
/**
76
* Interface for executing individual commands
77
*/
78
interface SingleCommands {
79
[commandName: string]: (attributes?: Record<string, any>) => boolean;
80
81
// Core commands available by default
82
insertContent(value: Content, options?: InsertContentOptions): boolean;
83
deleteSelection(): boolean;
84
deleteRange(range: { from: number; to: number }): boolean;
85
enter(): boolean;
86
focus(position?: FocusPosition, options?: { scrollIntoView?: boolean }): boolean;
87
blur(): boolean;
88
selectAll(): boolean;
89
selectTextblockStart(): boolean;
90
selectTextblockEnd(): boolean;
91
selectNodeForward(): boolean;
92
selectNodeBackward(): boolean;
93
selectParentNode(): boolean;
94
95
// Mark commands
96
setMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
97
toggleMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
98
unsetMark(typeOrName: string | MarkType, options?: { extendEmptyMarkRange?: boolean }): boolean;
99
100
// Node commands
101
setNode(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
102
toggleNode(typeOrName: string | NodeType, toggleTypeOrName?: string | NodeType, attributes?: Record<string, any>): boolean;
103
wrapIn(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
104
lift(typeOrName?: string | NodeType, attributes?: Record<string, any>): boolean;
105
liftEmptyBlock(): boolean;
106
splitBlock(options?: { keepMarks?: boolean }): boolean;
107
joinBackward(): boolean;
108
joinForward(): boolean;
109
110
// Custom commands added by extensions
111
[extensionCommand: string]: (attributes?: Record<string, any>) => boolean;
112
}
113
114
interface InsertContentOptions {
115
parseOptions?: ParseOptions;
116
updateSelection?: boolean;
117
}
118
119
type Content =
120
| string
121
| JSONContent
122
| JSONContent[]
123
| ProseMirrorNode
124
| ProseMirrorNode[]
125
| ProseMirrorFragment;
126
```
127
128
**Usage Examples:**
129
130
```typescript
131
// Execute individual commands
132
const success = editor.commands.insertContent('Hello World!');
133
134
if (editor.commands.focus()) {
135
editor.commands.selectAll();
136
}
137
138
// Mark commands
139
editor.commands.setMark('bold');
140
editor.commands.toggleMark('italic');
141
editor.commands.unsetMark('link');
142
143
// Node commands
144
editor.commands.setNode('heading', { level: 1 });
145
editor.commands.wrapIn('blockquote');
146
editor.commands.lift('listItem');
147
148
// Insert complex content
149
editor.commands.insertContent({
150
type: 'paragraph',
151
content: [
152
{
153
type: 'text',
154
text: 'Bold text',
155
marks: [{ type: 'bold' }]
156
}
157
]
158
});
159
160
// Delete and select commands
161
editor.commands.deleteSelection();
162
editor.commands.selectTextblockStart();
163
editor.commands.selectParentNode();
164
```
165
166
### Chained Commands
167
168
Interface for chaining multiple commands together for sequential execution.
169
170
```typescript { .api }
171
/**
172
* Interface for chaining commands together
173
*/
174
interface ChainedCommands {
175
[commandName: string]: (attributes?: Record<string, any>) => ChainedCommands;
176
177
/**
178
* Execute the entire command chain
179
* @returns Whether all commands in the chain succeeded
180
*/
181
run(): boolean;
182
183
// All single commands are available for chaining
184
insertContent(value: Content, options?: InsertContentOptions): ChainedCommands;
185
deleteSelection(): ChainedCommands;
186
deleteRange(range: { from: number; to: number }): ChainedCommands;
187
enter(): ChainedCommands;
188
focus(position?: FocusPosition, options?: { scrollIntoView?: boolean }): ChainedCommands;
189
blur(): ChainedCommands;
190
selectAll(): ChainedCommands;
191
192
setMark(typeOrName: string | MarkType, attributes?: Record<string, any>): ChainedCommands;
193
toggleMark(typeOrName: string | MarkType, attributes?: Record<string, any>): ChainedCommands;
194
unsetMark(typeOrName: string | MarkType, options?: { extendEmptyMarkRange?: boolean }): ChainedCommands;
195
196
setNode(typeOrName: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
197
toggleNode(typeOrName: string | NodeType, toggleTypeOrName?: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
198
wrapIn(typeOrName: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
199
lift(typeOrName?: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
200
201
// Custom commands added by extensions
202
[extensionCommand: string]: (attributes?: Record<string, any>) => ChainedCommands;
203
}
204
```
205
206
**Usage Examples:**
207
208
```typescript
209
// Basic command chaining
210
editor
211
.chain()
212
.focus()
213
.selectAll()
214
.deleteSelection()
215
.insertContent('New content')
216
.run();
217
218
// Complex formatting chain
219
editor
220
.chain()
221
.focus()
222
.toggleMark('bold')
223
.toggleMark('italic')
224
.insertContent('Bold and italic text')
225
.setMark('link', { href: 'https://example.com' })
226
.insertContent(' with a link')
227
.run();
228
229
// Node manipulation chain
230
editor
231
.chain()
232
.focus()
233
.selectAll()
234
.wrapIn('blockquote')
235
.setNode('heading', { level: 2 })
236
.insertContent('Quoted heading')
237
.run();
238
239
// Conditional chaining
240
const success = editor
241
.chain()
242
.focus()
243
.deleteSelection() // Only if something is selected
244
.insertContent('Replacement text')
245
.run();
246
247
if (success) {
248
console.log('Chain executed successfully');
249
}
250
251
// Chain with custom commands (from extensions)
252
editor
253
.chain()
254
.focus()
255
.setFontSize(16)
256
.setTextAlign('center')
257
.insertTable({ rows: 3, cols: 3 })
258
.run();
259
```
260
261
### Command Validation
262
263
Interface for testing whether commands can be executed without actually running them.
264
265
```typescript { .api }
266
/**
267
* Interface for validating command executability
268
*/
269
interface CanCommands {
270
[commandName: string]: (attributes?: Record<string, any>) => boolean;
271
272
// Core command validation
273
insertContent(value: Content, options?: InsertContentOptions): boolean;
274
deleteSelection(): boolean;
275
deleteRange(range: { from: number; to: number }): boolean;
276
enter(): boolean;
277
focus(position?: FocusPosition): boolean;
278
blur(): boolean;
279
selectAll(): boolean;
280
281
setMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
282
toggleMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
283
unsetMark(typeOrName: string | MarkType): boolean;
284
285
setNode(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
286
toggleNode(typeOrName: string | NodeType, toggleTypeOrName?: string | NodeType): boolean;
287
wrapIn(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
288
lift(typeOrName?: string | NodeType): boolean;
289
290
// Custom commands added by extensions
291
[extensionCommand: string]: (attributes?: Record<string, any>) => boolean;
292
}
293
```
294
295
**Usage Examples:**
296
297
```typescript
298
// Check if commands can be executed
299
if (editor.can().toggleMark('bold')) {
300
editor.commands.toggleMark('bold');
301
}
302
303
if (editor.can().wrapIn('blockquote')) {
304
editor.commands.wrapIn('blockquote');
305
}
306
307
// Conditional UI updates
308
function BoldButton() {
309
const canToggleBold = editor.can().toggleMark('bold');
310
const isBold = editor.isActive('bold');
311
312
return (
313
<button
314
disabled={!canToggleBold}
315
className={isBold ? 'active' : ''}
316
onClick={() => editor.commands.toggleMark('bold')}
317
>
318
Bold
319
</button>
320
);
321
}
322
323
// Check complex operations
324
const canInsertTable = editor.can().insertTable?.({ rows: 3, cols: 3 });
325
const canSetHeading = editor.can().setNode('heading', { level: 1 });
326
327
// Multiple validations
328
const formattingActions = [
329
{ name: 'bold', can: editor.can().toggleMark('bold') },
330
{ name: 'italic', can: editor.can().toggleMark('italic') },
331
{ name: 'code', can: editor.can().toggleMark('code') },
332
{ name: 'link', can: editor.can().setMark('link', { href: '' }) }
333
];
334
335
const availableActions = formattingActions.filter(action => action.can);
336
```
337
338
### Command Properties
339
340
The properties passed to command functions when they are executed.
341
342
```typescript { .api }
343
/**
344
* Properties passed to command functions during execution
345
*/
346
interface CommandProps {
347
/** The editor instance */
348
editor: Editor;
349
350
/** Current transaction (may be modified by commands) */
351
tr: Transaction;
352
353
/** Access to all single commands */
354
commands: SingleCommands;
355
356
/** Access to command validation */
357
can: CanCommands;
358
359
/** Create a new command chain */
360
chain: () => ChainedCommands;
361
362
/** Current editor state */
363
state: EditorState;
364
365
/** ProseMirror editor view */
366
view: EditorView;
367
368
/** Dispatch function (undefined in dry-run mode) */
369
dispatch: ((tr: Transaction) => void) | undefined;
370
}
371
372
/**
373
* Command function signature
374
*/
375
type CommandFunction = (props: CommandProps) => boolean;
376
```
377
378
### Creating Custom Commands
379
380
How to create custom commands in extensions.
381
382
```typescript { .api }
383
/**
384
* Commands configuration for extensions
385
*/
386
interface Commands {
387
[commandName: string]: (...args: any[]) => CommandFunction;
388
}
389
```
390
391
**Usage Examples:**
392
393
```typescript
394
import { Extension } from '@tiptap/core';
395
396
// Extension with custom commands
397
const CustomCommands = Extension.create({
398
name: 'customCommands',
399
400
addCommands() {
401
return {
402
// Simple command
403
insertDate: () => ({ commands }) => {
404
const date = new Date().toLocaleDateString();
405
return commands.insertContent(date);
406
},
407
408
// Command with parameters
409
insertHeading: (level: number, text: string) => ({ commands, chain }) => {
410
return chain()
411
.setNode('heading', { level })
412
.insertContent(text)
413
.run();
414
},
415
416
// Complex command using transaction
417
duplicateLine: () => ({ tr, state, dispatch }) => {
418
const { from, to } = state.selection;
419
const line = state.doc.textBetween(from, to);
420
421
if (!line) return false;
422
423
tr.insertText(`\n${line}`, to);
424
425
if (dispatch) {
426
dispatch(tr);
427
}
428
429
return true;
430
},
431
432
// Command that checks state
433
toggleHighlight: (color: string = 'yellow') => ({ commands, editor }) => {
434
const isActive = editor.isActive('highlight', { color });
435
436
if (isActive) {
437
return commands.unsetMark('highlight');
438
}
439
440
return commands.setMark('highlight', { color });
441
},
442
443
// Command with validation
444
wrapInCallout: (type: string = 'info') => ({ commands, can }) => {
445
// Only wrap if we can and aren't already in a callout
446
if (!can().wrapIn('callout') || editor.isActive('callout')) {
447
return false;
448
}
449
450
return commands.wrapIn('callout', { type });
451
}
452
};
453
}
454
});
455
456
// Using custom commands
457
editor.commands.insertDate();
458
editor.commands.insertHeading(1, 'Chapter Title');
459
editor.commands.duplicateLine();
460
461
// Chain custom commands
462
editor
463
.chain()
464
.focus()
465
.insertHeading(2, 'Section')
466
.insertDate()
467
.toggleHighlight('blue')
468
.run();
469
470
// Validate custom commands
471
if (editor.can().wrapInCallout('warning')) {
472
editor.commands.wrapInCallout('warning');
473
}
474
```
475
476
### Command Execution Flow
477
478
Understanding how commands are processed and executed.
479
480
```typescript { .api }
481
/**
482
* Command execution involves several phases:
483
* 1. Command function is called with CommandProps
484
* 2. Command modifies the transaction (tr)
485
* 3. Command returns boolean success value
486
* 4. Transaction is dispatched (if dispatch is provided)
487
* 5. Editor state is updated
488
*/
489
490
// Example command implementation
491
const exampleCommand = (text: string) => ({ tr, dispatch, state }) => {
492
// 1. Validate the command can run
493
if (!text || text.trim().length === 0) {
494
return false;
495
}
496
497
// 2. Modify the transaction
498
const { from } = state.selection;
499
tr.insertText(text, from);
500
501
// 3. Dispatch if available (not in validation mode)
502
if (dispatch) {
503
dispatch(tr);
504
}
505
506
// 4. Return success
507
return true;
508
};
509
510
// Command chaining execution
511
// Each command in a chain operates on the same transaction
512
// The transaction is only dispatched when .run() is called
513
editor
514
.chain()
515
.command1() // Modifies tr
516
.command2() // Modifies same tr
517
.command3() // Modifies same tr
518
.run(); // Dispatches tr with all modifications
519
```