0
# Undo/Redo System
1
2
Multi-level undo/redo functionality with scope management and origin tracking. Yjs provides a comprehensive undo/redo system that works seamlessly with collaborative editing scenarios.
3
4
## Capabilities
5
6
### UndoManager Class
7
8
Core class for managing undo/redo operations across one or more shared types.
9
10
```typescript { .api }
11
/**
12
* Manages undo/redo operations for shared types
13
*/
14
class UndoManager {
15
constructor(
16
typeScope: Doc | AbstractType<any> | Array<AbstractType<any>>,
17
options?: UndoManagerOptions
18
);
19
20
/** Document this undo manager operates on */
21
readonly doc: Doc;
22
23
/** Types that are tracked for undo/redo */
24
readonly scope: Array<AbstractType<any> | Doc>;
25
26
/** Stack of undo operations */
27
readonly undoStack: Array<StackItem>;
28
29
/** Stack of redo operations */
30
readonly redoStack: Array<StackItem>;
31
32
/** Whether currently performing undo operation */
33
readonly undoing: boolean;
34
35
/** Whether currently performing redo operation */
36
readonly redoing: boolean;
37
38
/** Set of transaction origins that are tracked */
39
readonly trackedOrigins: Set<any>;
40
41
/** Time window for merging consecutive operations (ms) */
42
readonly captureTimeout: number;
43
}
44
45
interface UndoManagerOptions {
46
/** Time window for merging operations in milliseconds (default: 500) */
47
captureTimeout?: number;
48
49
/** Function to determine if transaction should be captured */
50
captureTransaction?: (transaction: Transaction) => boolean;
51
52
/** Function to filter which items can be deleted during undo */
53
deleteFilter?: (item: Item) => boolean;
54
55
/** Set of origins to track (default: all origins) */
56
trackedOrigins?: Set<any>;
57
58
/** Whether to ignore remote changes (default: true) */
59
ignoreRemoteMapChanges?: boolean;
60
}
61
```
62
63
**Usage Examples:**
64
65
```typescript
66
import * as Y from "yjs";
67
68
const doc = new Y.Doc();
69
const ytext = doc.getText("document");
70
71
// Create undo manager for single type
72
const undoManager = new Y.UndoManager(ytext);
73
74
// Create undo manager for multiple types
75
const yarray = doc.getArray("items");
76
const ymap = doc.getMap("metadata");
77
const multiTypeUndoManager = new Y.UndoManager([ytext, yarray, ymap]);
78
79
// Create undo manager with options
80
const configuredUndoManager = new Y.UndoManager(ytext, {
81
captureTimeout: 1000, // 1 second merge window
82
trackedOrigins: new Set(["user-input", "paste-operation"]),
83
captureTransaction: (transaction) => {
84
// Only capture transactions from specific origins
85
return transaction.origin === "user-input";
86
}
87
});
88
```
89
90
### Undo/Redo Operations
91
92
Core methods for performing undo and redo operations.
93
94
```typescript { .api }
95
/**
96
* Undo the last captured operation
97
* @returns StackItem that was undone, or null if nothing to undo
98
*/
99
undo(): StackItem | null;
100
101
/**
102
* Redo the last undone operation
103
* @returns StackItem that was redone, or null if nothing to redo
104
*/
105
redo(): StackItem | null;
106
107
/**
108
* Check if undo is possible
109
* @returns True if there are operations to undo
110
*/
111
canUndo(): boolean;
112
113
/**
114
* Check if redo is possible
115
* @returns True if there are operations to redo
116
*/
117
canRedo(): boolean;
118
```
119
120
**Usage Examples:**
121
122
```typescript
123
import * as Y from "yjs";
124
125
const doc = new Y.Doc();
126
const ytext = doc.getText("document");
127
const undoManager = new Y.UndoManager(ytext);
128
129
// Make some changes
130
ytext.insert(0, "Hello");
131
ytext.insert(5, " World");
132
133
console.log("Text:", ytext.toString()); // "Hello World"
134
console.log("Can undo:", undoManager.canUndo()); // true
135
136
// Undo last operation
137
const undone = undoManager.undo();
138
console.log("Text after undo:", ytext.toString()); // "Hello"
139
140
// Undo another operation
141
undoManager.undo();
142
console.log("Text after second undo:", ytext.toString()); // ""
143
144
console.log("Can undo:", undoManager.canUndo()); // false
145
console.log("Can redo:", undoManager.canRedo()); // true
146
147
// Redo operations
148
undoManager.redo();
149
console.log("Text after redo:", ytext.toString()); // "Hello"
150
151
undoManager.redo();
152
console.log("Text after second redo:", ytext.toString()); // "Hello World"
153
```
154
155
### Stack Management
156
157
Methods for managing the undo/redo stacks.
158
159
```typescript { .api }
160
/**
161
* Clear undo/redo stacks
162
* @param clearUndoStack - Whether to clear undo stack (default: true)
163
* @param clearRedoStack - Whether to clear redo stack (default: true)
164
*/
165
clear(clearUndoStack?: boolean, clearRedoStack?: boolean): void;
166
167
/**
168
* Stop capturing consecutive operations into current stack item
169
*/
170
stopCapturing(): void;
171
```
172
173
**Usage Examples:**
174
175
```typescript
176
import * as Y from "yjs";
177
178
const doc = new Y.Doc();
179
const ytext = doc.getText("document");
180
const undoManager = new Y.UndoManager(ytext);
181
182
// Make changes
183
ytext.insert(0, "Hello");
184
ytext.insert(5, " World");
185
186
console.log("Undo stack size:", undoManager.undoStack.length);
187
188
// Clear only redo stack
189
undoManager.clear(false, true);
190
191
// Clear all stacks
192
undoManager.clear();
193
console.log("Undo stack size after clear:", undoManager.undoStack.length); // 0
194
195
// Stop capturing to force new stack item
196
ytext.insert(0, "A");
197
ytext.insert(1, "B"); // These might be merged
198
199
undoManager.stopCapturing();
200
ytext.insert(2, "C"); // This will be in separate stack item
201
```
202
203
### Scope Management
204
205
Methods for managing which types are tracked by the undo manager.
206
207
```typescript { .api }
208
/**
209
* Add types to the undo manager scope
210
* @param ytypes - Types or document to add to scope
211
*/
212
addToScope(ytypes: Array<AbstractType<any> | Doc> | AbstractType<any> | Doc): void;
213
214
/**
215
* Add origin to set of tracked origins
216
* @param origin - Origin to start tracking
217
*/
218
addTrackedOrigin(origin: any): void;
219
220
/**
221
* Remove origin from set of tracked origins
222
* @param origin - Origin to stop tracking
223
*/
224
removeTrackedOrigin(origin: any): void;
225
```
226
227
**Usage Examples:**
228
229
```typescript
230
import * as Y from "yjs";
231
232
const doc = new Y.Doc();
233
const ytext = doc.getText("document");
234
const yarray = doc.getArray("items");
235
const undoManager = new Y.UndoManager(ytext);
236
237
// Add another type to scope
238
undoManager.addToScope(yarray);
239
240
// Now changes to both ytext and yarray are tracked
241
ytext.insert(0, "Hello");
242
yarray.push(["item1"]);
243
244
undoManager.undo(); // Undoes both operations
245
246
// Manage tracked origins
247
undoManager.addTrackedOrigin("user-input");
248
undoManager.addTrackedOrigin("paste-operation");
249
250
// Only track specific origins
251
doc.transact(() => {
252
ytext.insert(0, "Tracked ");
253
}, "user-input");
254
255
doc.transact(() => {
256
ytext.insert(0, "Not tracked ");
257
}, "auto-save");
258
259
// Only the "user-input" transaction is available for undo
260
```
261
262
### StackItem Class
263
264
Individual items in the undo/redo stacks representing atomic operations.
265
266
```typescript { .api }
267
/**
268
* Individual undo/redo operation
269
*/
270
class StackItem {
271
constructor(deletions: DeleteSet, insertions: DeleteSet);
272
273
/** Items that were inserted in this operation */
274
readonly insertions: DeleteSet;
275
276
/** Items that were deleted in this operation */
277
readonly deletions: DeleteSet;
278
279
/** Metadata associated with this stack item */
280
readonly meta: Map<any, any>;
281
}
282
```
283
284
**Usage Examples:**
285
286
```typescript
287
import * as Y from "yjs";
288
289
const doc = new Y.Doc();
290
const ytext = doc.getText("document");
291
const undoManager = new Y.UndoManager(ytext);
292
293
// Make changes
294
ytext.insert(0, "Hello World");
295
296
// Examine stack items
297
const stackItem = undoManager.undoStack[0];
298
console.log("Insertions:", stackItem.insertions);
299
console.log("Deletions:", stackItem.deletions);
300
console.log("Metadata:", stackItem.meta);
301
302
// Add metadata to stack items
303
undoManager.on('stack-item-added', (event) => {
304
event.stackItem.meta.set('timestamp', Date.now());
305
event.stackItem.meta.set('userId', 'current-user');
306
});
307
```
308
309
### Advanced Undo Manager Patterns
310
311
**Selective Undo/Redo:**
312
313
```typescript
314
import * as Y from "yjs";
315
316
class SelectiveUndoManager {
317
private undoManager: Y.UndoManager;
318
private operationHistory: Array<{
319
id: string;
320
stackItem: Y.StackItem;
321
description: string;
322
timestamp: number;
323
}> = [];
324
325
constructor(types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
326
this.undoManager = new Y.UndoManager(types);
327
328
// Track all operations
329
this.undoManager.on('stack-item-added', (event) => {
330
this.operationHistory.push({
331
id: `op-${Date.now()}-${Math.random()}`,
332
stackItem: event.stackItem,
333
description: this.getOperationDescription(event.stackItem),
334
timestamp: Date.now()
335
});
336
});
337
}
338
339
getOperationHistory() {
340
return [...this.operationHistory];
341
}
342
343
undoSpecificOperation(operationId: string): boolean {
344
const operation = this.operationHistory.find(op => op.id === operationId);
345
if (!operation) return false;
346
347
// Find operation in stack and undo up to that point
348
const stackIndex = this.undoManager.undoStack.indexOf(operation.stackItem);
349
if (stackIndex === -1) return false;
350
351
// Undo operations in reverse order up to target
352
for (let i = 0; i <= stackIndex; i++) {
353
if (!this.undoManager.canUndo()) break;
354
this.undoManager.undo();
355
}
356
357
return true;
358
}
359
360
private getOperationDescription(stackItem: Y.StackItem): string {
361
// Analyze stack item to generate description
362
return `Operation at ${new Date().toLocaleTimeString()}`;
363
}
364
}
365
```
366
367
**Collaborative Undo:**
368
369
```typescript
370
import * as Y from "yjs";
371
372
class CollaborativeUndoManager {
373
private undoManager: Y.UndoManager;
374
private clientId: number;
375
376
constructor(doc: Y.Doc, types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
377
this.clientId = doc.clientID;
378
379
// Only track local changes
380
this.undoManager = new Y.UndoManager(types, {
381
captureTransaction: (transaction) => {
382
return transaction.local && transaction.origin !== 'undo' && transaction.origin !== 'redo';
383
}
384
});
385
}
386
387
undoLocal(): Y.StackItem | null {
388
// Only undo operations made by this client
389
return this.undoManager.undo();
390
}
391
392
redoLocal(): Y.StackItem | null {
393
// Only redo operations made by this client
394
return this.undoManager.redo();
395
}
396
397
getLocalOperationCount(): number {
398
return this.undoManager.undoStack.length;
399
}
400
}
401
```
402
403
**Undo with Confirmation:**
404
405
```typescript
406
import * as Y from "yjs";
407
408
class ConfirmingUndoManager {
409
private undoManager: Y.UndoManager;
410
private confirmationRequired: boolean = false;
411
412
constructor(types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
413
this.undoManager = new Y.UndoManager(types);
414
}
415
416
setConfirmationRequired(required: boolean) {
417
this.confirmationRequired = required;
418
}
419
420
async undoWithConfirmation(): Promise<Y.StackItem | null> {
421
if (!this.undoManager.canUndo()) return null;
422
423
if (this.confirmationRequired) {
424
const confirmed = await this.showConfirmationDialog("Undo last operation?");
425
if (!confirmed) return null;
426
}
427
428
return this.undoManager.undo();
429
}
430
431
async redoWithConfirmation(): Promise<Y.StackItem | null> {
432
if (!this.undoManager.canRedo()) return null;
433
434
if (this.confirmationRequired) {
435
const confirmed = await this.showConfirmationDialog("Redo last operation?");
436
if (!confirmed) return null;
437
}
438
439
return this.undoManager.redo();
440
}
441
442
private showConfirmationDialog(message: string): Promise<boolean> {
443
// Implementation would show actual confirmation dialog
444
return Promise.resolve(confirm(message));
445
}
446
}
447
```
448
449
**Undo Manager Events:**
450
451
```typescript
452
import * as Y from "yjs";
453
454
const doc = new Y.Doc();
455
const ytext = doc.getText("document");
456
const undoManager = new Y.UndoManager(ytext);
457
458
// Listen for undo manager events
459
undoManager.on('stack-item-added', (event) => {
460
console.log("New operation added to stack:", event.stackItem);
461
});
462
463
undoManager.on('stack-item-popped', (event) => {
464
console.log("Operation removed from stack:", event.stackItem);
465
});
466
467
undoManager.on('stack-cleared', (event) => {
468
console.log("Stacks cleared:", event);
469
});
470
471
// Custom event handling for UI updates
472
class UndoRedoUI {
473
private undoButton: HTMLButtonElement;
474
private redoButton: HTMLButtonElement;
475
476
constructor(undoManager: Y.UndoManager, undoBtn: HTMLButtonElement, redoBtn: HTMLButtonElement) {
477
this.undoButton = undoBtn;
478
this.redoButton = redoBtn;
479
480
this.updateButtons(undoManager);
481
482
undoManager.on('stack-item-added', () => this.updateButtons(undoManager));
483
undoManager.on('stack-item-popped', () => this.updateButtons(undoManager));
484
undoManager.on('stack-cleared', () => this.updateButtons(undoManager));
485
}
486
487
private updateButtons(undoManager: Y.UndoManager) {
488
this.undoButton.disabled = !undoManager.canUndo();
489
this.redoButton.disabled = !undoManager.canRedo();
490
491
this.undoButton.title = `Undo (${undoManager.undoStack.length} operations)`;
492
this.redoButton.title = `Redo (${undoManager.redoStack.length} operations)`;
493
}
494
}
495
```
496
497
### Lifecycle Management
498
499
```typescript { .api }
500
/**
501
* Destroy the undo manager and clean up resources
502
*/
503
destroy(): void;
504
```
505
506
**Usage Examples:**
507
508
```typescript
509
import * as Y from "yjs";
510
511
const doc = new Y.Doc();
512
const ytext = doc.getText("document");
513
const undoManager = new Y.UndoManager(ytext);
514
515
// Use undo manager...
516
517
// Clean up when done
518
undoManager.destroy();
519
520
// Undo manager is no longer functional after destroy
521
console.log("Can undo after destroy:", undoManager.canUndo()); // false
522
```