diff --git a/backbone.memento.js b/backbone.memento.js index 6662229..894457f 100644 --- a/backbone.memento.js +++ b/backbone.memento.js @@ -19,25 +19,42 @@ Backbone.Memento = (function(Backbone, _){ var serializer = new Serializer(structure, config); var mementoStack = new MementoStack(structure, config); + var dirtyState; var restoreState = function (previousState, restoreConfig){ - if (!previousState){ return; } + if (!previousState){ return false; } serializer.deserialize(previousState, restoreConfig); + return true; }; this.store = function(){ + dirtyState = null; var currentState = serializer.serialize(); mementoStack.push(currentState); }; this.restore = function(restoreConfig){ - var previousState = mementoStack.pop(); - restoreState(previousState, restoreConfig); + if (mementoStack.atTop()) { + dirtyState = serializer.serialize(); + } + var previousState = mementoStack.previousElement(); + return restoreState(previousState, restoreConfig); + }; + + this.undo = this.restore; + + this.redo = function(restoreConfig){ + var nextState = mementoStack.nextElement(); + if (!nextState) { + nextState = dirtyState; + dirtyState = null; + } + return restoreState(nextState, restoreConfig); }; this.restart = function(restoreConfig){ var previousState = mementoStack.rewind(); - restoreState(previousState, restoreConfig); + return restoreState(previousState, restoreConfig); }; }; @@ -132,20 +149,37 @@ Backbone.Memento = (function(Backbone, _){ // ---------------------------- var MementoStack = function(structure, config){ var attributeStack; + var nextAttributeIndex; function initialize(){ attributeStack = []; + nextAttributeIndex = -1; + } + + this.atTop = function(){ + return (nextAttributeIndex == attributeStack.length - 1); } this.push = function(attrs){ + // Truncate the array to remove the attributes after the next restore point + attributeStack.length = nextAttributeIndex + 1; attributeStack.push(attrs); + nextAttributeIndex++; } - this.pop = function(restoreConfig){ - var oldAttrs = attributeStack.pop(); + this.previousElement = function(restoreConfig){ + if (nextAttributeIndex == -1) return; + var oldAttrs = attributeStack[nextAttributeIndex]; + nextAttributeIndex--; return oldAttrs; } + this.nextElement = function(restoreConfig){ + if (this.atTop()) return; + nextAttributeIndex++; + return attributeStack[nextAttributeIndex + 1]; + } + this.rewind = function(){ var oldAttrs = attributeStack[0]; initialize(); diff --git a/spec/javascripts/redo.spec.js b/spec/javascripts/redo.spec.js new file mode 100644 index 0000000..1502721 --- /dev/null +++ b/spec/javascripts/redo.spec.js @@ -0,0 +1,119 @@ +describe("redo", function(){ + beforeEach(function(){ + this.model = new AModel(); + }); + + describe("when undoing, then redoing the last unstored change", function(){ + beforeEach(function(){ + this.model.set({foo: "bar"}); + this.model.store(); + this.model.set({foo: "what?"}); + this.model.restore(); + }); + + it("should reset the model to the last change", function(){ + expect(this.model.get("foo")).toBe("bar"); + this.model.redo(); + expect(this.model.get("foo")).toBe("what?"); + }); + }); + + describe("when undoing then redoing multiple unstored changes", function(){ + beforeEach(function(){ + this.model.set({foo: "foo 1"}); + this.model.set({bar: "bar 1"}); + this.model.store(); + this.model.set({foo: "foo 2"}); + this.model.set({bar: "bar 2"}); + this.model.restore(); + }); + + it("should reset the model to all the unstored changes", function(){ + expect(this.model.get("foo")).toBe("foo 1"); + expect(this.model.get("bar")).toBe("bar 1"); + this.model.redo(); + expect(this.model.get("foo")).toBe("foo 2"); + expect(this.model.get("foo")).toBe("foo 2"); + }); + }); + + describe("when undoing then redoing a change applied without storing first", function(){ + beforeEach(function(){ + this.model.set({foo: "foo 1"}); + this.model.store(); + this.model.set({foo: "foo 2"}); + this.model.restore(); + }); + + it("should reset the model to all the unstored changes", function(){ + expect(this.model.get("foo")).toBe("foo 1"); + this.model.set({foo: "foo 3"}); // This should be ignored by the redo() + this.model.redo(); + expect(this.model.get("foo")).toBe("foo 2"); + }); + }); + + describe("when redoing and no more mementos exist", function(){ + beforeEach(function(){ + this.model.set({foo: "bar"}); + this.model.redo(); + }); + + it("should not redo anything", function(){ + expect(this.model.get("foo")).toBe("bar"); + }); + }); + + describe("when undoing once and redoing twice", function(){ + beforeEach(function(){ + this.model.set({foo: "bar"}); + this.model.store(); + this.model.set({foo: "what?"}); + this.model.restore(); + }); + + it("should not restore anything past the first one", function(){ + expect(this.model.get("foo")).toBe("bar"); + this.model.redo(); + expect(this.model.get("foo")).toBe("what?"); + this.model.redo(); + expect(this.model.get("foo")).toBe("what?"); + }); + }); + + describe("when undoing twice and redoing twice", function(){ + beforeEach(function(){ + this.model.set({foo: "bar"}); + this.model.store(); + this.model.set({foo: "i dont know"}); + this.model.store(); + this.model.set({foo: "third"}); + this.model.restore(); + }); + + it("should reapply the last change", function(){ + expect(this.model.get("foo")).toBe("i dont know"); + this.model.restore(); + expect(this.model.get("foo")).toBe("bar"); + this.model.redo(); + expect(this.model.get("foo")).toBe("i dont know"); + this.model.redo(); + expect(this.model.get("foo")).toBe("third"); + }); + }); + + describe("when adding a new attributes, undoing, then redoing", function(){ + beforeEach(function(){ + this.model.set({foo: "bar"}); + this.model.store(); + this.model.set({bar: "baz"}); + this.model.restore(); + }); + + it("should readd the new attribute", function(){ + expect(this.model.get("bar")).toBeUndefined(); + this.model.redo(); + expect(this.model.get("bar")).toBe("baz"); + }); + }); +});