Skip to content

Instantly share code, notes, and snippets.

@zz85
Last active July 26, 2022 20:18
Show Gist options
  • Save zz85/07661d84d02250b22aac to your computer and use it in GitHub Desktop.
Save zz85/07661d84d02250b22aac to your computer and use it in GitHub Desktop.
Simple Undo / Redo System
// Simple Undo / Redo System
/*
* @author joshua koo / http://joshuakoo.com
*
* There are usually 2 different ways to handle undo / redo
* 1. you snapshot and store the states, so you can load them anytime
* 2. you store the deltas, actions, events or commands between save points.
*
* Method 1 is simplistic. But it works. It's a lot like git.
* It works with the reactive flow.
*
* Method 2 has certain use cases. It might map better to a collaborative
* environment. It is more like hg. It might be a little towards
* operational transformation.
*
* Read more: http://en.wikipedia.org/wiki/Undo
*
* This is a simple implmentation of method 1.
* We store states until states exceed max items, then we abandom some.
* We can undo and redo, but once we create a new action, redo items clears
*/
// State is simply an object
function UndoState(state, description) {
this.state = state;
this.description = description;
}
function UndoManager(max) {
this.MAX_ITEMS = max || 100;
this.clear();
}
UndoManager.prototype.save = function(state) {
var states = this.states;
var next_index = this.index + 1;
var to_remove = states.length - next_index;
states.splice(next_index, to_remove, state);
if (states.length > this.MAX_ITEMS) {
states.shift();
}
this.index = states.length - 1;
};
UndoManager.prototype.clear = function() {
this.states = [];
this.index = -1;
// FIXME: leave default state or always leave one state?
};
UndoManager.prototype.canUndo = function() {
return this.index > 0;
};
UndoManager.prototype.canRedo = function() {
return this.index < this.states.length - 1;
};
UndoManager.prototype.undo = function() {
if (this.canUndo()) {
this.index--;
}
return this.get();
};
UndoManager.prototype.redo = function() {
if (this.canRedo()) {
this.index++;
}
return this.get();
};
UndoManager.prototype.get = function() {
return this.states[this.index];
};
// TODO, get undo list && get redo list
// TODO, implement backing store on localStorage / indexDB to reduce mem usage
// ************
// Usage
// ************
// var manager = new UndoManager(3);
//
// ************
// Tests starts
// ************
var manager = new UndoManager(3);
// Save a state
console.assert(manager.states.length === 0);
manager.save(new UndoState({testing : 1}, 'Set testing to 1'));
console.assert(manager.states.length === 1);
manager.save(new UndoState({testing : 2}, 'Set testing to 2'));
console.assert(manager.states.length === 2);
manager.save(new UndoState({testing : 3}, 'Set testing to 3'));
console.assert(manager.states.length === 3);
manager.save(new UndoState({testing : 4}, 'Set testing to 4'));
console.assert(manager.states.length === 3);
manager.save(new UndoState({testing : 5}, 'Set testing to 5'));
console.assert(manager.states.length === 3);
var at;
at = manager.get();
console.assert(at.state.testing === 5);
console.assert(manager.canRedo() === false);
at = manager.undo();
console.assert(at.state.testing === 4);
console.assert(manager.canUndo() === true);
console.assert(manager.canRedo() === true);
console.assert(manager.index === 1);
at = manager.undo();
console.assert(at.state.testing === 3);
console.assert(manager.canUndo() === false);
console.assert(manager.canRedo() === true);
console.assert(manager.index === 0);
at = manager.undo();
console.assert(at.state.testing === 3);
console.assert(manager.canUndo() === false);
console.assert(manager.canRedo() === true);
console.assert(manager.index === 0);
at = manager.undo();
console.assert(at.state.testing === 3);
console.assert(manager.canUndo() === false);
console.assert(manager.canRedo() === true);
console.assert(manager.index === 0);
at = manager.redo();
console.assert(at.state.testing === 4);
console.assert(manager.canUndo() === true);
console.assert(manager.canRedo() === true);
console.assert(manager.index === 1);
at = manager.redo();
console.assert(at.state.testing === 5);
console.assert(manager.canUndo() === true);
console.assert(manager.canRedo() === false);
console.assert(manager.index === 2);
at = manager.redo();
console.assert(at.state.testing === 5);
console.assert(manager.canUndo() === true);
console.assert(manager.canRedo() === false);
console.assert(manager.index === 2);
at = manager.redo();
console.assert(at.state.testing === 5);
console.assert(manager.canUndo() === true);
console.assert(manager.canRedo() === false);
console.assert(manager.index === 2);
at = manager.undo();
console.assert(at.state.testing === 4);
console.assert(manager.canUndo() === true);
console.assert(manager.canRedo() === true);
console.assert(manager.index === 1);
at = manager.undo();
console.assert(at.state.testing === 3);
console.assert(manager.canUndo() === false);
console.assert(manager.canRedo() === true);
console.assert(manager.index === 0);
manager.save(new UndoState({testing : 'xyz'}, 'Set testing to XYZ'));
at = manager.get();
console.assert(at.state.testing === 'xyz');
console.assert(manager.canUndo() === true);
console.assert(manager.canRedo() === false);
console.assert(manager.index === 1);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment