Last active
July 26, 2022 20:18
-
-
Save zz85/07661d84d02250b22aac to your computer and use it in GitHub Desktop.
Simple Undo / Redo System
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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