Skip to content

Commit

Permalink
Add basic undo/redo
Browse files Browse the repository at this point in the history
  • Loading branch information
espadrine committed Dec 12, 2017
1 parent 2191ccb commit ed07dc8
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 7 deletions.
62 changes: 55 additions & 7 deletions canop.js
Expand Up @@ -295,6 +295,12 @@ Operation.prototype = {
}
return posChanges;
},
// actionType: actions.*
applyAtomicOperation: function applyAtomicOperation(actionType, key, value, base, local) {
var aop = new AtomicOperation(actionType, key, value, base, local);
this.list.push(aop);
return aop;
},
// Insert a value to the operation. Mutates this.
add: function addOp(path, offset, value, base, local) {
var aop = new AtomicOperation(actions.stringAdd, offset, value, base, local);
Expand Down Expand Up @@ -406,6 +412,8 @@ function Client(params) {
this.canon = new Operation(); // Operation for changes acknowledged by the server.
// Map from event names to array of {func: function(event), options}.
this.listeners = {};
this.undoStack = [];
this.redoStack = [];

this.on('change', function(event) { self.updateData(event); });
this.on('localChange', function(event) { self.updateData(event); });
Expand Down Expand Up @@ -844,19 +852,15 @@ Client.prototype = {
// path: list of keys (string or integer) to the object that receive the
// operation.
act: function(action) {
var aops = this.commitAction(action);
// TODO: localUpdate event.
this.emitChanges('localChange', aops);
this.sendToServer();
this.registerAtomicOperations(this.commitAction(action));
},
// actions: list of [actionType, path, …params].
actAtomically: function(actions) {
actAtomically: function(actions, options) {
var aops = [];
for (var i = 0; i < actions.length; i++) {
aops = aops.concat(this.commitAction(actions[i]));
}
this.emitChanges('localChange', aops);
this.sendToServer();
this.registerAtomicOperations(aops, options);
},
// action: [actions.*, path, …params]
// Return a list of AtomicOperations.
Expand All @@ -875,6 +879,11 @@ Client.prototype = {
}
return aops;
},
// actionType: actions.*
applyAtomicOperation: function(actionType, key, value) {
return this.local.applyAtomicOperation(
actionType, key, value, this.base, this.id);
},
// action: [actions.stringAdd, path, …params]
// Return an AtomicOperation.
stringAdd: function(action) {
Expand All @@ -894,6 +903,45 @@ Client.prototype = {
this.base, this.localId);
},

// History management

registerAtomicOperations: function(aops, options) {
options = options || {};
if (!options.skipHistory) {
// Add to the local history.
this.undoStack.push(new Operation(aops));
}
// TODO: localUpdate event.
this.emitChanges('localChange', aops);
this.sendToServer();
},

undo: function() {
var lastOp = this.undoStack.pop();
if (lastOp !== undefined) {
this.redoStack.push(lastOp.dup());
var op = lastOp.inverse();
var aops = [];
for (var i = 0; i < op.list.length; i++) {
var aop = op.list[i];
aops.push(this.applyAtomicOperation(aop.action, aop.key, aop.value));
}
this.registerAtomicOperations(aops, {skipHistory: true});
}
},

redo: function() {
var op = this.redoStack.pop();
if (op !== undefined) {
var aops = [];
for (var i = 0; i < op.list.length; i++) {
var aop = op.list[i];
aops.push(this.applyAtomicOperation(aop.action, aop.key, aop.value));
}
this.registerAtomicOperations(aops, {skipHistory: true});
}
},

// Send a signal to all other nodes of the network.
// content: JSON-serializable value, sent to other nodes.
signal: function(content) {
Expand Down
29 changes: 29 additions & 0 deletions test/canop.js
Expand Up @@ -140,3 +140,32 @@ star.server.removeClient(star.clients[0]);
sendChange(star, 1);
assert.equal(star.server.clientCount, 1, 'Server clientCount after disconnection');
assert.equal(star.clients[1].clientCount, 1, 'Client 1 clientCount after disconnection');

// Undo
var star = new Star('');
star.clients[0].add([], 0, 'ab');
sendChange(star, 0);
star.clients[0].add([], 2, 'cd');
sendChange(star, 0);
sendChange(star, 1);
// Undo wrong client
star.clients[1].undo();
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.clients[0]), 'abcd', 'Client 0 wrong undo');
assert.equal(String(star.clients[1]), 'abcd', 'Client 1 wrong undo');
assert.equal(String(star.server), 'abcd', 'Client 1 wrong undo');
// Undo right client
star.clients[0].undo();
assert.equal(String(star.clients[0]), 'ab', 'Client 0 undo');
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.server), 'ab', 'Server undo');
assert.equal(String(star.clients[1]), 'ab', 'Client 1 undo');
// Redo
star.clients[0].redo();
assert.equal(String(star.clients[0]), 'abcd', 'Client 0 redo');
sendChange(star, 0);
sendChange(star, 1);
assert.equal(String(star.server), 'abcd', 'Server redo');
assert.equal(String(star.clients[1]), 'abcd', 'Client 1 redo');

0 comments on commit ed07dc8

Please sign in to comment.