Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/ArthurClemens/JavaScript-Undo-Manager

Simple JavaScript undo and redo independent of other libraries
https://github.com/ArthurClemens/JavaScript-Undo-Manager

Last synced: 18 days ago
JSON representation

Simple JavaScript undo and redo independent of other libraries

Awesome Lists containing this project

README

        

# Undo Manager

Simple undo manager to provide undo and redo actions in JavaScript applications.

- [Demos](#demos)
- [Installation](#installation)
- [Example](#example)
- [Updating the UI](#updating-the-ui)
- [Methods](#methods)
- [add](#add)
- [undo](#undo)
- [redo](#redo)
- [clear](#clear)
- [setLimit](#setlimit)
- [hasUndo](#hasundo)
- [hasRedo](#hasredo)
- [setCallback](#setcallback)
- [getIndex](#getindex)
- [getCommands](#getcommands)
- [Use with CommonJS](#use-with-commonjs)
- [Use with RequireJS](#use-with-requirejs)

## Demos

* [CodeSandbox with RGB color slider](https://codesandbox.io/s/undo-manager-color-sliders-z4myoj)
* [Undo Manager with canvas drawing](https://arthurclemens.github.io/JavaScript-Undo-Manager/)
* [JSBin demo, also with canvas](http://jsbin.com/tidibi/edit?js,output)

## Installation

```
npm install undo-manager
```

## Example

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:

```js
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:

```js
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"}
```

## Updating the UI

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:

```js
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:

```js
// 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`:

```js
// 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);
}
});
}
```

## Methods

### add

Adds an undo/redo command pair to the stack.

```js
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.

```js
undoManager.add({
groupId: 'auth',
undo: () => removePerson(id),
redo: () => addPerson(id, name)
});
```

### undo

Performs the undo action.

```js
undoManager.undo();
```

If a `groupId` was set, the undo action will be performed on all adjacent command pairs with that group id.

### redo

Performs the redo action.

```js
undoManager.redo();
```

If a `groupId` was set, the redo action will be performed on all adjacent command pairs with that group id.

### clear

Clears all stored states.

```js
undoManager.clear();
```

### setLimit

Set the maximum number of undo steps. Default: 0 (unlimited).

```js
undoManager.setLimit(limit);
```

### hasUndo

Tests if any undo actions exist.

```js
const hasUndo = undoManager.hasUndo();
```

### hasRedo

Tests if any redo actions exist.

```js
const hasRedo = undoManager.hasRedo();
```

### setCallback

Get notified on changes. Pass a function to be called on undo and redo actions.

```js
undoManager.setCallback(myCallback);
```

### getIndex

Returns the index of the actions list.

```js
const index = undoManager.getIndex();
```

### getCommands

Returns the list of queued commands, optionally filtered by group id.

```js
const commands = undoManager.getCommands();
const commands = undoManager.getCommands(groupId);
```

## Use with CommonJS

```bash
npm install undo-manager
```

```js
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:

```js
// 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:

```js
// app.js
const undoManager = require('undoManager');

undoManager.add(...);
undoManager.undo();
```

## Use with RequireJS

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:

```html

```

And `demo.js` would look like this:

```js
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
});
```