or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

document-management.mdevent-system.mdindex.mdposition-tracking.mdshared-data-types.mdsnapshot-system.mdsynchronization.mdtransaction-system.mdundo-redo-system.mdxml-types.md

undo-redo-system.mddocs/

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

```