Members
(inner) UndoManager
Applications that allow end-user operations can use an UndoManager to record
information on how to undo those operations.
## Undoable Operations
To make an operation undoable an application simply adds the inverse of that
operation to an UndoManager instance using the `add` method:
`undoManager.register(label, operationPromise)`
This means that every undo-able user operation has to have an inverse
operation available. For example a calculator might provide a `subtract`
method as the inverse of the `add` method.
An simple example would look something like this:
```javascript
add: {
value: function (number) {
this.undoManager.register("Add", Promise.resolve([this.subtract, this, number]));
var result = this.total += number;
return result;
}
},
subtract: {
value: function (number) {
this.undoManager.register("Subtract", Promise.resolve([this.add, this, number]));
var result = this.total -= number;
return result;
}
}
```
Of immediate interest is the actual promise added to the undoManager.
`Promise.resolve(["Add", this.subtract, this, number])`
The promise provides the final label (optionally), a reference to the function to call,
the context for the function to be executed in, and any number of arguments
to be passed along when calling the function.
In simple cases such as this the promise for the inverse operation
can be resolved immediately; this is not necessarily always possible in cases
where the operation itself is asynchronous.
## Basic Undoing and Redoing
After performing `calculator.add(42)` the undoManager will have an entry
on how to undo that addition operation. Each operation added to the
undoManager is added on top of a stack. Calling the undoManager's `undo`
method will perform the operation on the top of that stack if
original operationPromise has been resolved.
While performing an undo operation any additions to the undoManager will
instead be placed on the redo stack. Conversely, any additions made while
performing a redo operation will be placed on the undo stack.
When not actively undoing or redoing, the redo stack is cleared whenever a
new operation is added; the only way operations end up on the redo stack is
through undoing an operation.
## Asynchronous Considerations
It is possible for a user invoked operation to take some time to complete or
details of how to undo the operation may not be known until the operation
has completed.
In these cases it is important to remember that the undo stack captures user
intent, which is considered synchronous. This is why the undoManager accepts
promises for the operations but places them on the stack synchronously.
Consider the following example:
```javascript
addRandomNumber: {
var deferredUndo,
self = this;
this.undoManager.register("Add Random", deferredUndo.promise);
return this.randomNumberGeneratorService.next().then(function (rand) {
deferredUndo.resolve(["Add " + rand, self.subtract, self, rand];
var result = self.total = self.total + number;
return result
});
}
```
Here we see that the undo operation for addRandomNumber is added to the
UndoManager before we even know how to undo the operation, indeed it's added
before the operation has even happened.
It is worth noting that the undoManager does not block anything. Users are
still free to call `add`, `subtract`, `addRandomNumber` or any
other APIs exposed by the calculator, whether the `addRandomNumber` has
resolved or not. It's the responsibility of an API provider to handle this
scenario as necessary.
At this point two things can happen:
1) A user could invoke `undo` after the operation promise's resolution.
2) A user could invoke `undo` prior to the operation promise's resolution.
In the first scenario, things move along much like they did in the first case
we described above.
In the second scenario, the undoManager puts the unresolved promise into a
queue of operations to be performed when possible. Subsequent undo and redo
requests are added to this queue.
Whenever a promise is resolved the undoManager runs through this queue in
order, oldest to newest, and attempts to perform the operation specified,
stopping when it encounters an unfulfilled operation promise.
This guarantees that promised operations are added in the order as they were
performed by the user and are executed, not in the order they are fulfilled,
but in the order they are undone or redone.
class UndoManager
extends Target
- Source: