Last active
January 17, 2021 20:37
-
-
Save jdittrich/b41625a679b28308cf954abdc42a8012 to your computer and use it in GitHub Desktop.
A simple ES6 undo-redo library, consisting of a command stack class and a command class template.
This file contains hidden or 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
/* | |
LICENSE: | |
The MIT License (MIT) | |
Copyright © 2021 Jan Dittrich | |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
WHAT IS THIS? | |
A simple undo-redo library, consisting of a command stack class and a command class template. | |
To use this, you need to create a command stack instance and pass it a store object (e.g. `let myCommandStack = new CommandStack(store)`) | |
and define your commands as classes inheriting from Command (e.g. `let AddNumber = class extends Command {`). | |
Undoable changes to the store are done by passing a new command instance to the command stack instance’s execute method (e.g. `myCommandStack.execute(new AddNumber(`) | |
Inspired by: | |
* kimhogeling [undoredo.js](https://gist.github.com/kimhogeling/b646b432ba974f31c6e4) uses ES5 inheritance; Attaches undo/redo to single entities at least in the example | |
* ArthurClemens/ [Javascript-Undo-Manager](https://github.com/ArthurClemens/Javascript-Undo-Manager) Works rather similar. Does separate the execution of a command from the undo/redo functionality of it. | |
* freegroup/[draw2d](https://github.com/freegroup/draw2d) and its [command-classes](https://github.com/freegroup/draw2d/tree/master/src/command). A bit javaescque but nice to read imho. | |
*/ | |
/** | |
* Class creating a command stack. Can execute commands that change a state object and undo and redo. | |
*/ | |
const CommandStack = class { | |
/** | |
* @param {object|array} initialState - pass the object holding that state. It does not work with pass-by-value types (e.g. strings). | |
*/ | |
constructor(initialState){ | |
this.undoStack = []; | |
this.redoStack = []; | |
this.state = initialState; //state is passed to the commands | |
} | |
/** | |
* Executes a command to change the state and add the command to the undo-stack. | |
* @param {command} - execute this | |
* @returns {this} - for chaining | |
*/ | |
executeCommand(command){ | |
if(!command){throw new Error("called executeCommand without giving a command");} | |
if (!(command instanceof Command)){throw new Error("command must be instance of Command");} | |
command.execute(this.state); | |
this.undoStack.unshift(command); | |
this.redoStack=[]; //every new command flushes redo | |
return this; | |
} | |
/** | |
* undo the most recent command on the command stack and put it to the redo-stack | |
* @returns {this} - for chaining | |
*/ | |
undo(){ | |
let commandToUndo = this.undoStack.shift(); //get and remove first command from undo | |
commandToUndo.undo(this.state); //execute it | |
this.redoStack.unshift(commandToUndo); //add it to redo | |
return this; | |
} | |
/** | |
* Redo most recently undone command and put it on the undo stack again. | |
* @returns {this} - for chaining | |
*/ | |
redo(){ | |
let commandToRedo = this.redoStack.shift(); //get and remove first command from redo | |
commandToRedo.redo(this.state); //execute it | |
this.undoStack.unshift(commandToRedo); //add it to undo | |
return this; | |
} | |
get canUndo(){ | |
return (this.undoStack.length > 0); | |
} | |
get canRedo(){ | |
return (this.redoStack.length > 0) | |
} | |
} | |
/** | |
* Class to create commands. It would not *need* to be a class, technically, as the instances need the | |
* execute/undo/redo commands, but it is probably easier to make this explicit via the class. | |
* */ | |
const Command = class { | |
/** | |
* Create an undo/redoable command | |
* @param {*} payload - passed to each execution/undo/redo | |
* @param {string} commandName - name of command. Not mandatory. | |
*/ | |
constructor(payload, commandName = ''){ | |
this.payload = payload; | |
this.commandName = commandName; | |
} | |
execute(store,payload){ | |
throw new Error("Called execute method of class but this method should be defined by subclass"); | |
} | |
redo(store,payload){ | |
throw new Error("Called execute method of class but this method should be defined by subclass"); | |
} | |
undo(store,payload){ | |
throw new Error("Called undo method of class but this method should be defined by subclass"); | |
} | |
} | |
// If you use this as library, put it in a separate file and uncomment this as the last line: | |
// export {CommandStack, Command} | |
// this is the usage example: | |
//create add number command… it adds a number to a store! | |
let AddNumber = class extends Command { | |
constructor(payload){ | |
super(payload) | |
} | |
execute(store){ | |
store.counter = store.counter + this.payload; | |
} | |
undo(store){ | |
store.counter = store.counter - this.payload; | |
} | |
redo(store){ | |
this.execute(store); | |
} | |
} | |
// is is our store. It can’t be a number, but must be an object that wraps the number, because otherwise | |
// it would get passed-by-value, so each function and assignment would have its own copy, and a change of that | |
// copy would not be reflected in other store-values. | |
let store = {counter:0}; | |
//set up the undo/redo stack and pass it the store it can undo-redo in on | |
let changeNumbersStack = new CommandStack(store); | |
//Now, setup done. Lets do some command-ing | |
console.log(changeNumbersStack.canUndo)// false | |
console.log(changeNumbersStack.canRedo)// false | |
changeNumbersStack.executeCommand(new AddNumber(5)); | |
console.log(store.counter); //5 | |
console.log(changeNumbersStack.canUndo)//now true | |
console.log(changeNumbersStack.canRedo)//still false | |
changeNumbersStack.executeCommand(new AddNumber(2)); | |
console.log(store.counter); //7 | |
changeNumbersStack.undo(); | |
console.log(changeNumbersStack.canRedo)//now true | |
console.log(store.counter); //5 | |
changeNumbersStack.redo(); | |
console.log(changeNumbersStack.canRedo)//false again (we undid the undo) | |
console.log(store.counter); //7 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment