Simple undo manager to provide undo and redo actions in JavaScript applications.
npm install undo-manager
Actions (typing a character, moving an object) are structured as command pairs: one command for destruction (undo) and one for creation (redo). Each pair is added to the undo stack:
const undoManager = new UndoManager();
undoManager.add({
undo: function() {
// ...
},
redo: function() {
// ...
}
});
To make an action undoable, you'd add an undo/redo command pair to the undo manager:
const undoManager = new UndoManager();
const people = {};
function addPerson(id, name) {
people[id] = name;
};
function removePerson(id) {
delete people[id];
};
function createPerson(id, name) {
// first creation
addPerson(id, name);
// make undoable
undoManager.add({
undo: () => removePerson(id),
redo: () => addPerson(id, name)
});
}
createPerson(101, "John");
createPerson(102, "Mary");
console.log(people); // logs: {101: "John", 102: "Mary"}
undoManager.undo();
console.log(people); // logs: {101: "John"}
undoManager.undo();
console.log(people); // logs: {}
undoManager.redo();
console.log(people); // logs: {101: "John"}
TL;DR UI that relies on undo manager state - for example hasUndo
and hasRedo
- needs to be updated using the callback function provided with setCallback
. This ensures that all internal state has been resolved before the UI is repainted.
Let's say we have an update function that conditionally disables the undo and redo buttons:
function updateUI() {
btn_undo.disabled = !undoManager.hasUndo();
btn_redo.disabled = !undoManager.hasRedo();
}
You might be inclined to call the update in the undo/redo command pair:
// wrong approach, don't copy
const undoManager = new UndoManager();
const states = [];
function updateState(newState) {
states.push(newState);
updateUI();
undoManager.add({
undo: function () {
states.pop();
updateUI(); // <= this will lead to inconsistent UI state
},
redo: function () {
states.push(newState);
updateUI(); // <= this will lead to inconsistent UI state
}
});
}
Instead, pass the update function to setCallback
:
// recommended approach
const undoManager = new UndoManager();
undoManager.setCallback(updateUI);
const states = [];
function updateState(newState) {
states.push(newState);
updateUI();
undoManager.add({
undo: function () {
states.pop();
},
redo: function () {
states.push(newState);
}
});
}
Adds an undo/redo command pair to the stack.
function createPerson(id, name) {
// first creation
addPerson(id, name);
// make undoable
undoManager.add({
undo: () => removePerson(id),
redo: () => addPerson(id, name)
});
}
Optionally add a groupId
to identify related command pairs. Undo and redo actions will then be performed on all adjacent command pairs with that group id.
undoManager.add({
groupId: 'auth',
undo: () => removePerson(id),
redo: () => addPerson(id, name)
});
Performs the undo action.
undoManager.undo();
If a groupId
was set, the undo action will be performed on all adjacent command pairs with that group id.
Performs the redo action.
undoManager.redo();
If a groupId
was set, the redo action will be performed on all adjacent command pairs with that group id.
Clears all stored states.
undoManager.clear();
Set the maximum number of undo steps. Default: 0 (unlimited).
undoManager.setLimit(limit);
Tests if any undo actions exist.
const hasUndo = undoManager.hasUndo();
Tests if any redo actions exist.
const hasRedo = undoManager.hasRedo();
Get notified on changes. Pass a function to be called on undo and redo actions.
undoManager.setCallback(myCallback);
Returns the index of the actions list.
const index = undoManager.getIndex();
Returns the list of queued commands, optionally filtered by group id.
const commands = undoManager.getCommands();
const commands = undoManager.getCommands(groupId);
npm install undo-manager
const UndoManager = require('undo-manager')
If you only need a single instance of UndoManager throughout your application, it may be wise to create a module that exports a singleton:
// undoManager.js
const undoManager = require('undo-manager'); // require the lib from node_modules
let singleton = undefined;
if (!singleton) {
singleton = new undoManager();
}
module.exports = singleton;
Then in your app:
// app.js
const undoManager = require('undoManager');
undoManager.add(...);
undoManager.undo();
If you are using RequireJS, you need to use the shim
config parameter.
Assuming require.js
and domReady.js
are located in js/extern
, the index.html
load call would be:
<script src="js/extern/require.js" data-main="js/demo"></script>
And demo.js
would look like this:
requirejs.config({
baseUrl: "js",
paths: {
domReady: "extern/domReady",
app: "../demo",
undomanager: "../../js/undomanager",
circledrawer: "circledrawer"
},
shim: {
"undomanager": {
exports: "UndoManager"
},
"circledrawer": {
exports: "CircleDrawer"
}
}
});
require(["domReady", "undomanager", "circledrawer"], function(domReady, UndoManager, CircleDrawer) {
"use strict";
let undoManager,
circleDrawer,
btnUndo,
btnRedo,
btnClear;
undoManager = new UndoManager();
circleDrawer = new CircleDrawer("view", undoManager);
// etcetera
});