Conflict-free Replicated Data Type (CRDT) framework for real-time collaborative applications
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.
Core class for managing undo/redo operations across one or more shared types.
/**
* Manages undo/redo operations for shared types
*/
class UndoManager {
constructor(
typeScope: Doc | AbstractType<any> | Array<AbstractType<any>>,
options?: UndoManagerOptions
);
/** Document this undo manager operates on */
readonly doc: Doc;
/** Types that are tracked for undo/redo */
readonly scope: Array<AbstractType<any> | Doc>;
/** Stack of undo operations */
readonly undoStack: Array<StackItem>;
/** Stack of redo operations */
readonly redoStack: Array<StackItem>;
/** Whether currently performing undo operation */
readonly undoing: boolean;
/** Whether currently performing redo operation */
readonly redoing: boolean;
/** Set of transaction origins that are tracked */
readonly trackedOrigins: Set<any>;
/** Time window for merging consecutive operations (ms) */
readonly captureTimeout: number;
}
interface UndoManagerOptions {
/** Time window for merging operations in milliseconds (default: 500) */
captureTimeout?: number;
/** Function to determine if transaction should be captured */
captureTransaction?: (transaction: Transaction) => boolean;
/** Function to filter which items can be deleted during undo */
deleteFilter?: (item: Item) => boolean;
/** Set of origins to track (default: all origins) */
trackedOrigins?: Set<any>;
/** Whether to ignore remote changes (default: true) */
ignoreRemoteMapChanges?: boolean;
}Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
// Create undo manager for single type
const undoManager = new Y.UndoManager(ytext);
// Create undo manager for multiple types
const yarray = doc.getArray("items");
const ymap = doc.getMap("metadata");
const multiTypeUndoManager = new Y.UndoManager([ytext, yarray, ymap]);
// Create undo manager with options
const configuredUndoManager = new Y.UndoManager(ytext, {
captureTimeout: 1000, // 1 second merge window
trackedOrigins: new Set(["user-input", "paste-operation"]),
captureTransaction: (transaction) => {
// Only capture transactions from specific origins
return transaction.origin === "user-input";
}
});Core methods for performing undo and redo operations.
/**
* Undo the last captured operation
* @returns StackItem that was undone, or null if nothing to undo
*/
undo(): StackItem | null;
/**
* Redo the last undone operation
* @returns StackItem that was redone, or null if nothing to redo
*/
redo(): StackItem | null;
/**
* Check if undo is possible
* @returns True if there are operations to undo
*/
canUndo(): boolean;
/**
* Check if redo is possible
* @returns True if there are operations to redo
*/
canRedo(): boolean;Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);
// Make some changes
ytext.insert(0, "Hello");
ytext.insert(5, " World");
console.log("Text:", ytext.toString()); // "Hello World"
console.log("Can undo:", undoManager.canUndo()); // true
// Undo last operation
const undone = undoManager.undo();
console.log("Text after undo:", ytext.toString()); // "Hello"
// Undo another operation
undoManager.undo();
console.log("Text after second undo:", ytext.toString()); // ""
console.log("Can undo:", undoManager.canUndo()); // false
console.log("Can redo:", undoManager.canRedo()); // true
// Redo operations
undoManager.redo();
console.log("Text after redo:", ytext.toString()); // "Hello"
undoManager.redo();
console.log("Text after second redo:", ytext.toString()); // "Hello World"Methods for managing the undo/redo stacks.
/**
* Clear undo/redo stacks
* @param clearUndoStack - Whether to clear undo stack (default: true)
* @param clearRedoStack - Whether to clear redo stack (default: true)
*/
clear(clearUndoStack?: boolean, clearRedoStack?: boolean): void;
/**
* Stop capturing consecutive operations into current stack item
*/
stopCapturing(): void;Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);
// Make changes
ytext.insert(0, "Hello");
ytext.insert(5, " World");
console.log("Undo stack size:", undoManager.undoStack.length);
// Clear only redo stack
undoManager.clear(false, true);
// Clear all stacks
undoManager.clear();
console.log("Undo stack size after clear:", undoManager.undoStack.length); // 0
// Stop capturing to force new stack item
ytext.insert(0, "A");
ytext.insert(1, "B"); // These might be merged
undoManager.stopCapturing();
ytext.insert(2, "C"); // This will be in separate stack itemMethods for managing which types are tracked by the undo manager.
/**
* Add types to the undo manager scope
* @param ytypes - Types or document to add to scope
*/
addToScope(ytypes: Array<AbstractType<any> | Doc> | AbstractType<any> | Doc): void;
/**
* Add origin to set of tracked origins
* @param origin - Origin to start tracking
*/
addTrackedOrigin(origin: any): void;
/**
* Remove origin from set of tracked origins
* @param origin - Origin to stop tracking
*/
removeTrackedOrigin(origin: any): void;Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
const yarray = doc.getArray("items");
const undoManager = new Y.UndoManager(ytext);
// Add another type to scope
undoManager.addToScope(yarray);
// Now changes to both ytext and yarray are tracked
ytext.insert(0, "Hello");
yarray.push(["item1"]);
undoManager.undo(); // Undoes both operations
// Manage tracked origins
undoManager.addTrackedOrigin("user-input");
undoManager.addTrackedOrigin("paste-operation");
// Only track specific origins
doc.transact(() => {
ytext.insert(0, "Tracked ");
}, "user-input");
doc.transact(() => {
ytext.insert(0, "Not tracked ");
}, "auto-save");
// Only the "user-input" transaction is available for undoIndividual items in the undo/redo stacks representing atomic operations.
/**
* Individual undo/redo operation
*/
class StackItem {
constructor(deletions: DeleteSet, insertions: DeleteSet);
/** Items that were inserted in this operation */
readonly insertions: DeleteSet;
/** Items that were deleted in this operation */
readonly deletions: DeleteSet;
/** Metadata associated with this stack item */
readonly meta: Map<any, any>;
}Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);
// Make changes
ytext.insert(0, "Hello World");
// Examine stack items
const stackItem = undoManager.undoStack[0];
console.log("Insertions:", stackItem.insertions);
console.log("Deletions:", stackItem.deletions);
console.log("Metadata:", stackItem.meta);
// Add metadata to stack items
undoManager.on('stack-item-added', (event) => {
event.stackItem.meta.set('timestamp', Date.now());
event.stackItem.meta.set('userId', 'current-user');
});Selective Undo/Redo:
import * as Y from "yjs";
class SelectiveUndoManager {
private undoManager: Y.UndoManager;
private operationHistory: Array<{
id: string;
stackItem: Y.StackItem;
description: string;
timestamp: number;
}> = [];
constructor(types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
this.undoManager = new Y.UndoManager(types);
// Track all operations
this.undoManager.on('stack-item-added', (event) => {
this.operationHistory.push({
id: `op-${Date.now()}-${Math.random()}`,
stackItem: event.stackItem,
description: this.getOperationDescription(event.stackItem),
timestamp: Date.now()
});
});
}
getOperationHistory() {
return [...this.operationHistory];
}
undoSpecificOperation(operationId: string): boolean {
const operation = this.operationHistory.find(op => op.id === operationId);
if (!operation) return false;
// Find operation in stack and undo up to that point
const stackIndex = this.undoManager.undoStack.indexOf(operation.stackItem);
if (stackIndex === -1) return false;
// Undo operations in reverse order up to target
for (let i = 0; i <= stackIndex; i++) {
if (!this.undoManager.canUndo()) break;
this.undoManager.undo();
}
return true;
}
private getOperationDescription(stackItem: Y.StackItem): string {
// Analyze stack item to generate description
return `Operation at ${new Date().toLocaleTimeString()}`;
}
}Collaborative Undo:
import * as Y from "yjs";
class CollaborativeUndoManager {
private undoManager: Y.UndoManager;
private clientId: number;
constructor(doc: Y.Doc, types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
this.clientId = doc.clientID;
// Only track local changes
this.undoManager = new Y.UndoManager(types, {
captureTransaction: (transaction) => {
return transaction.local && transaction.origin !== 'undo' && transaction.origin !== 'redo';
}
});
}
undoLocal(): Y.StackItem | null {
// Only undo operations made by this client
return this.undoManager.undo();
}
redoLocal(): Y.StackItem | null {
// Only redo operations made by this client
return this.undoManager.redo();
}
getLocalOperationCount(): number {
return this.undoManager.undoStack.length;
}
}Undo with Confirmation:
import * as Y from "yjs";
class ConfirmingUndoManager {
private undoManager: Y.UndoManager;
private confirmationRequired: boolean = false;
constructor(types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
this.undoManager = new Y.UndoManager(types);
}
setConfirmationRequired(required: boolean) {
this.confirmationRequired = required;
}
async undoWithConfirmation(): Promise<Y.StackItem | null> {
if (!this.undoManager.canUndo()) return null;
if (this.confirmationRequired) {
const confirmed = await this.showConfirmationDialog("Undo last operation?");
if (!confirmed) return null;
}
return this.undoManager.undo();
}
async redoWithConfirmation(): Promise<Y.StackItem | null> {
if (!this.undoManager.canRedo()) return null;
if (this.confirmationRequired) {
const confirmed = await this.showConfirmationDialog("Redo last operation?");
if (!confirmed) return null;
}
return this.undoManager.redo();
}
private showConfirmationDialog(message: string): Promise<boolean> {
// Implementation would show actual confirmation dialog
return Promise.resolve(confirm(message));
}
}Undo Manager Events:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);
// Listen for undo manager events
undoManager.on('stack-item-added', (event) => {
console.log("New operation added to stack:", event.stackItem);
});
undoManager.on('stack-item-popped', (event) => {
console.log("Operation removed from stack:", event.stackItem);
});
undoManager.on('stack-cleared', (event) => {
console.log("Stacks cleared:", event);
});
// Custom event handling for UI updates
class UndoRedoUI {
private undoButton: HTMLButtonElement;
private redoButton: HTMLButtonElement;
constructor(undoManager: Y.UndoManager, undoBtn: HTMLButtonElement, redoBtn: HTMLButtonElement) {
this.undoButton = undoBtn;
this.redoButton = redoBtn;
this.updateButtons(undoManager);
undoManager.on('stack-item-added', () => this.updateButtons(undoManager));
undoManager.on('stack-item-popped', () => this.updateButtons(undoManager));
undoManager.on('stack-cleared', () => this.updateButtons(undoManager));
}
private updateButtons(undoManager: Y.UndoManager) {
this.undoButton.disabled = !undoManager.canUndo();
this.redoButton.disabled = !undoManager.canRedo();
this.undoButton.title = `Undo (${undoManager.undoStack.length} operations)`;
this.redoButton.title = `Redo (${undoManager.redoStack.length} operations)`;
}
}/**
* Destroy the undo manager and clean up resources
*/
destroy(): void;Usage Examples:
import * as Y from "yjs";
const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);
// Use undo manager...
// Clean up when done
undoManager.destroy();
// Undo manager is no longer functional after destroy
console.log("Can undo after destroy:", undoManager.canUndo()); // falseInstall with Tessl CLI
npx tessl i tessl/npm-yjs