0
# History
1
2
The history system provides undo and redo functionality by tracking document changes and maintaining reversible state transitions. It intelligently groups related changes and manages the undo stack.
3
4
## Capabilities
5
6
### History Plugin
7
8
Create and configure the undo/redo system.
9
10
```typescript { .api }
11
/**
12
* Create a history plugin with configurable options
13
*/
14
function history(config?: HistoryOptions): Plugin;
15
16
/**
17
* History plugin configuration options
18
*/
19
interface HistoryOptions {
20
/**
21
* Maximum number of events in the history (default: 100)
22
*/
23
depth?: number;
24
25
/**
26
* Delay in milliseconds for grouping events (default: 500)
27
*/
28
newGroupDelay?: number;
29
}
30
```
31
32
### History Commands
33
34
Commands for navigating through the undo/redo stack.
35
36
```typescript { .api }
37
/**
38
* Undo the last change and scroll to the affected area
39
*/
40
function undo(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;
41
42
/**
43
* Undo the last change without scrolling
44
*/
45
function undoNoScroll(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;
46
47
/**
48
* Redo the last undone change and scroll to the affected area
49
*/
50
function redo(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;
51
52
/**
53
* Redo the last undone change without scrolling
54
*/
55
function redoNoScroll(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;
56
```
57
58
### History State Queries
59
60
Functions to inspect the current history state.
61
62
```typescript { .api }
63
/**
64
* Get the number of undoable events in the history
65
*/
66
function undoDepth(state: EditorState): number;
67
68
/**
69
* Get the number of redoable events in the history
70
*/
71
function redoDepth(state: EditorState): number;
72
```
73
74
### History Control
75
76
Functions to control history behavior and grouping.
77
78
```typescript { .api }
79
/**
80
* Prevent the next steps from being grouped with previous ones
81
*/
82
function closeHistory(tr: Transaction): Transaction;
83
```
84
85
**Usage Examples:**
86
87
```typescript
88
import {
89
history,
90
undo,
91
redo,
92
undoDepth,
93
redoDepth,
94
closeHistory
95
} from "@tiptap/pm/history";
96
import { keymap } from "@tiptap/pm/keymap";
97
98
// Basic history setup
99
const historyPlugin = history({
100
depth: 50, // Keep 50 events
101
newGroupDelay: 1000 // Group events within 1 second
102
});
103
104
// History keymap
105
const historyKeymap = keymap({
106
"Mod-z": undo,
107
"Mod-y": redo,
108
"Mod-Shift-z": redo // Alternative redo binding
109
});
110
111
// Create editor with history
112
const state = EditorState.create({
113
schema: mySchema,
114
plugins: [
115
historyPlugin,
116
historyKeymap
117
]
118
});
119
120
// Check history state
121
function canUndo(state: EditorState): boolean {
122
return undoDepth(state) > 0;
123
}
124
125
function canRedo(state: EditorState): boolean {
126
return redoDepth(state) > 0;
127
}
128
129
// Custom undo/redo with UI updates
130
function createHistoryActions(view: EditorView) {
131
return {
132
undo: () => {
133
if (canUndo(view.state)) {
134
undo(view.state, view.dispatch);
135
updateHistoryButtons(view);
136
}
137
},
138
139
redo: () => {
140
if (canRedo(view.state)) {
141
redo(view.state, view.dispatch);
142
updateHistoryButtons(view);
143
}
144
}
145
};
146
}
147
148
function updateHistoryButtons(view: EditorView) {
149
const undoButton = document.getElementById("undo-btn");
150
const redoButton = document.getElementById("redo-btn");
151
152
undoButton.disabled = !canUndo(view.state);
153
redoButton.disabled = !canRedo(view.state);
154
}
155
```
156
157
## Advanced History Management
158
159
### Manual History Grouping
160
161
Control when history events are grouped together.
162
163
```typescript
164
// Group related operations
165
function performComplexEdit(view: EditorView) {
166
let tr = view.state.tr;
167
168
// Start a new history group
169
tr = closeHistory(tr);
170
171
// Perform multiple related operations
172
tr = tr.insertText("New content at ", 10);
173
tr = tr.addMark(10, 25, view.state.schema.marks.strong.create());
174
tr = tr.insertText(" with formatting", 25);
175
176
// Dispatch as single undoable unit
177
view.dispatch(tr);
178
}
179
180
// Prevent grouping with previous edits
181
function insertTimestamp(view: EditorView) {
182
const tr = closeHistory(view.state.tr);
183
const timestamp = new Date().toLocaleString();
184
185
view.dispatch(
186
tr.insertText(`[${timestamp}] `)
187
);
188
}
189
```
190
191
### Custom History Behavior
192
193
Create specialized history handling for specific operations.
194
195
```typescript
196
// Auto-save with history preservation
197
class AutoSaveManager {
198
private saveTimeout: NodeJS.Timeout | null = null;
199
200
constructor(private view: EditorView, private saveInterval: number = 30000) {
201
this.setupAutoSave();
202
}
203
204
private setupAutoSave() {
205
const plugin = new Plugin({
206
state: {
207
init: () => null,
208
apply: (tr, value) => {
209
if (tr.docChanged) {
210
this.scheduleAutoSave();
211
}
212
return value;
213
}
214
}
215
});
216
217
// Add plugin to existing plugins
218
const newState = this.view.state.reconfigure({
219
plugins: this.view.state.plugins.concat(plugin)
220
});
221
this.view.updateState(newState);
222
}
223
224
private scheduleAutoSave() {
225
if (this.saveTimeout) {
226
clearTimeout(this.saveTimeout);
227
}
228
229
this.saveTimeout = setTimeout(() => {
230
this.performAutoSave();
231
}, this.saveInterval);
232
}
233
234
private async performAutoSave() {
235
try {
236
// Close history group before save
237
const tr = closeHistory(this.view.state.tr);
238
tr.setMeta("auto-save", true);
239
this.view.dispatch(tr);
240
241
// Perform save operation
242
await this.saveDocument();
243
244
// Clear undo history if save successful and history is deep
245
if (undoDepth(this.view.state) > 100) {
246
this.clearOldHistory();
247
}
248
} catch (error) {
249
console.error("Auto-save failed:", error);
250
}
251
}
252
253
private clearOldHistory() {
254
// Create new state with fresh history
255
const historyPlugin = history({ depth: 20 });
256
const newState = this.view.state.reconfigure({
257
plugins: this.view.state.plugins
258
.filter(p => p.spec !== history().spec)
259
.concat(historyPlugin)
260
});
261
this.view.updateState(newState);
262
}
263
264
private async saveDocument(): Promise<void> {
265
// Implement your save logic here
266
const doc = this.view.state.doc.toJSON();
267
// await api.save(doc);
268
}
269
}
270
```
271
272
### History Visualization
273
274
Create UI components that show history state.
275
276
```typescript
277
// History timeline component
278
class HistoryTimeline {
279
private element: HTMLElement;
280
281
constructor(private view: EditorView) {
282
this.element = this.createElement();
283
this.updateTimeline();
284
285
// Listen for state changes
286
this.view.setProps({
287
dispatchTransaction: (tr) => {
288
this.view.updateState(this.view.state.apply(tr));
289
this.updateTimeline();
290
}
291
});
292
}
293
294
private createElement(): HTMLElement {
295
const container = document.createElement("div");
296
container.className = "history-timeline";
297
return container;
298
}
299
300
private updateTimeline() {
301
const undoCount = undoDepth(this.view.state);
302
const redoCount = redoDepth(this.view.state);
303
304
this.element.innerHTML = "";
305
306
// Add undo items
307
for (let i = undoCount - 1; i >= 0; i--) {
308
const item = document.createElement("div");
309
item.className = "history-item undo-item";
310
item.textContent = `Undo ${i + 1}`;
311
item.onclick = () => this.undoToStep(i);
312
this.element.appendChild(item);
313
}
314
315
// Add current state marker
316
const current = document.createElement("div");
317
current.className = "history-item current-item";
318
current.textContent = "Current";
319
this.element.appendChild(current);
320
321
// Add redo items
322
for (let i = 0; i < redoCount; i++) {
323
const item = document.createElement("div");
324
item.className = "history-item redo-item";
325
item.textContent = `Redo ${i + 1}`;
326
item.onclick = () => this.redoToStep(i);
327
this.element.appendChild(item);
328
}
329
}
330
331
private undoToStep(step: number) {
332
for (let i = 0; i <= step; i++) {
333
if (canUndo(this.view.state)) {
334
undo(this.view.state, this.view.dispatch);
335
}
336
}
337
}
338
339
private redoToStep(step: number) {
340
for (let i = 0; i <= step; i++) {
341
if (canRedo(this.view.state)) {
342
redo(this.view.state, this.view.dispatch);
343
}
344
}
345
}
346
347
public getElement(): HTMLElement {
348
return this.element;
349
}
350
}
351
```
352
353
### Collaborative History
354
355
Handle history in collaborative editing scenarios.
356
357
```typescript
358
// History manager for collaborative editing
359
class CollaborativeHistory {
360
constructor(private view: EditorView) {
361
this.setupCollaborativeHistory();
362
}
363
364
private setupCollaborativeHistory() {
365
const plugin = new Plugin({
366
state: {
367
init: () => ({
368
localUndoDepth: 0,
369
remoteChanges: []
370
}),
371
372
apply: (tr, value) => {
373
// Track local vs remote changes
374
const isLocal = !tr.getMeta("remote");
375
const isUndo = tr.getMeta("history$") === "undo";
376
const isRedo = tr.getMeta("history$") === "redo";
377
378
if (isLocal && !isUndo && !isRedo) {
379
// Local change - can be undone
380
return {
381
...value,
382
localUndoDepth: value.localUndoDepth + 1
383
};
384
} else if (tr.getMeta("remote")) {
385
// Remote change - affects undo stack
386
return {
387
...value,
388
remoteChanges: [...value.remoteChanges, tr]
389
};
390
}
391
392
return value;
393
}
394
},
395
396
props: {
397
handleKeyDown: (view, event) => {
398
// Custom undo/redo for collaborative context
399
if (event.key === "z" && (event.metaKey || event.ctrlKey)) {
400
if (event.shiftKey) {
401
return this.collaborativeRedo(view);
402
} else {
403
return this.collaborativeUndo(view);
404
}
405
}
406
return false;
407
}
408
}
409
});
410
411
const newState = this.view.state.reconfigure({
412
plugins: this.view.state.plugins.concat(plugin)
413
});
414
this.view.updateState(newState);
415
}
416
417
private collaborativeUndo(view: EditorView): boolean {
418
// Only undo local changes
419
const state = view.state;
420
const pluginState = this.getPluginState(state);
421
422
if (pluginState.localUndoDepth > 0) {
423
return undo(state, view.dispatch);
424
}
425
426
return false;
427
}
428
429
private collaborativeRedo(view: EditorView): boolean {
430
// Only redo local changes
431
const state = view.state;
432
433
if (redoDepth(state) > 0) {
434
return redo(state, view.dispatch);
435
}
436
437
return false;
438
}
439
440
private getPluginState(state: EditorState) {
441
// Get plugin state helper
442
return state.plugins.find(p => p.spec.key === "collaborativeHistory")?.getState(state);
443
}
444
}
445
```
446
447
## Types
448
449
```typescript { .api }
450
/**
451
* History plugin configuration
452
*/
453
interface HistoryOptions {
454
depth?: number;
455
newGroupDelay?: number;
456
}
457
458
/**
459
* History metadata for transactions
460
*/
461
interface HistoryMeta {
462
"history$"?: "undo" | "redo";
463
addToHistory?: boolean;
464
preserveItems?: number;
465
}
466
```