Daniel@0: "use strict"; Daniel@0: Daniel@0: App.module("ContextModule", function(ContextModule, App, Backbone, Marionette, $, _, Logger) { Daniel@0: Daniel@0: // Define private variables Daniel@0: var logger = null; Daniel@0: Daniel@0: ContextModule.addInitializer(function(options){ Daniel@0: Daniel@0: // logger = Logger.get("ContextModule.StateBookmark"); Daniel@0: // logger.setLevel(Logger.DEBUG); Daniel@0: Daniel@0: /** Daniel@0: * StateHistory stores a history of a serialized state Daniel@0: * and provides an API to switch between these states Daniel@0: * Daniel@0: * A user simply updates the value of currentSerializedState attribute Daniel@0: * and calls undo(), redo() or reset() to move along the history Daniel@0: * There is no direct access to undo and redo stacks, but it is possible Daniel@0: * to know about them by calling canUndo() and canRedo() Daniel@0: * Daniel@0: * serialize / unserialize deal with the object of the following structure Daniel@0: * { Daniel@0: * undoStack: [{recent}, {less recent}, ... {the oldest}] Daniel@0: * redoStack: [{next}, {the following}, ... {the last}] Daniel@0: * currentSerializedState: {} Daniel@0: * } Daniel@0: * Daniel@0: * attribute compoundChangeDetector is a function that helps decide if the changes between Daniel@0: * the given three serialized states can be treated as a single change. Daniel@0: * This is needed to avoid too many undo states on simple actions like text typing Daniel@0: * compoundChangeDetector returns true if the change between the first, the second Daniel@0: * and the third object can be considered as a single compound change Daniel@0: * (i.e. there is no need to create an extra undo action) Daniel@0: */ Daniel@0: ContextModule.StateHistory = Backbone.Model.extend({ Daniel@0: defaults: { Daniel@0: maxStackSize: 50, Daniel@0: currentSerializedState: undefined, Daniel@0: compoundChangeDetector: undefined Daniel@0: }, Daniel@0: Daniel@0: _NONE: 0, Daniel@0: _UNDO: 1, Daniel@0: _REDO: 2, Daniel@0: _UNSERIALIZE: 3, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.StateBookmark Daniel@0: */ Daniel@0: initialize: function() { Daniel@0: this._undoStack = []; Daniel@0: this._redoStack = []; Daniel@0: this._currentSerializedStateChangeMode = this._NONE; Daniel@0: Daniel@0: this.on("change:maxStackSize", this._trimStacks, this); Daniel@0: this.on("change:currentSerializedState", this._registerNewSerializedState, this); Daniel@0: }, Daniel@0: Daniel@0: _registerNewSerializedState: function() { Daniel@0: var previousSerialisedState = this.previous("currentSerializedState"); Daniel@0: if (this._currentSerializedStateChangeMode == this._UNDO) { Daniel@0: this._redoStack.unshift(previousSerialisedState); Daniel@0: } else if (this._currentSerializedStateChangeMode == this._REDO) { Daniel@0: this._undoStack.unshift(previousSerialisedState); Daniel@0: } else if (this._currentSerializedStateChangeMode == this._UNSERIALIZE) { Daniel@0: } else { Daniel@0: if (!_.isEqual(previousSerialisedState, this.attributes.currentSerializedState)) { Daniel@0: if (this._undoStack.length == 0 Daniel@0: || !_.isFunction(this.attributes.compoundChangeDetector) Daniel@0: || !this.attributes.compoundChangeDetector.call(currentSerializedState, previousSerialisedState, this._undoStack[0])) Daniel@0: { Daniel@0: this._undoStack.unshift(previousSerialisedState); Daniel@0: } Daniel@0: this._redoStack = []; Daniel@0: } Daniel@0: } Daniel@0: this._trimStacks(); Daniel@0: }, Daniel@0: Daniel@0: getCurrentSerializedState: function() { Daniel@0: Daniel@0: }, Daniel@0: Daniel@0: undo: function() { Daniel@0: if (this._undoStack.length) { Daniel@0: this._currentSerializedStateChangeMode = this._UNDO; Daniel@0: this.set("currentSerializedState", this._undoStack.shift()); Daniel@0: this._currentSerializedStateChangeMode = this._NONE; Daniel@0: } else { Daniel@0: throw "Undo was called when undo stack was empty"; Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: redo: function() { Daniel@0: if (this._redoStack.length) { Daniel@0: this._currentSerializedStateChangeMode = this._REDO; Daniel@0: this.set("currentSerializedState", this._redoStack.shift()); Daniel@0: this._currentSerializedStateChangeMode = this._NONE; Daniel@0: } else { Daniel@0: throw "Redo was called when undo stack was empty"; Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: reset: function() { Daniel@0: var stacksWereNotEmpty = this._undoStack.length || this._redoStack.length; Daniel@0: this._undoStack = []; Daniel@0: this._redoStack = []; Daniel@0: if (stacksWereNotEmpty) { Daniel@0: this.trigger("change"); Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: canUndo: function() { Daniel@0: return !!this._undoStack.length; Daniel@0: }, Daniel@0: Daniel@0: canRedo: function() { Daniel@0: return !!this._redoStack.length; Daniel@0: }, Daniel@0: Daniel@0: serialize: function() { Daniel@0: // logger.debug("method called: StateHistory::serialize"); Daniel@0: Daniel@0: var result = { Daniel@0: currentSerializedState: this.attributes.currentSerializedState, Daniel@0: undoStack: _.clone(this._undoStack), Daniel@0: redoStack: _.clone(this._redoStack), Daniel@0: }; Daniel@0: Daniel@0: return result; Daniel@0: }, Daniel@0: Daniel@0: unserialize: function(serializedAttributes) { Daniel@0: this._currentSerializedStateChangeMode = this._UNSERIALIZE; Daniel@0: Daniel@0: var fixedSerializedAttributes = serializedAttributes; Daniel@0: if (!_.isSimpleObject(serializedAttributes)) { Daniel@0: fixedSerializedAttributes = {}; Daniel@0: } Daniel@0: Daniel@0: this._undoStack = _.isArray(fixedSerializedAttributes.undoStack) ? Daniel@0: fixedSerializedAttributes.undoStack : []; Daniel@0: this._redoStack = _.isArray(fixedSerializedAttributes.redoStack) ? Daniel@0: fixedSerializedAttributes.redoStack : []; Daniel@0: this._trimStacks(); Daniel@0: Daniel@0: this.set("currentSerializedState", fixedSerializedAttributes.currentSerializedState); Daniel@0: Daniel@0: this._currentSerializedStateChangeMode = this._NONE; Daniel@0: }, Daniel@0: Daniel@0: _trimStacks: function() { Daniel@0: if (this._undoStack.length > this.attributes.maxStackSize) { Daniel@0: this._undoStack = this._undoStack.slice(0, this.attributes.maxStackSize); Daniel@0: } Daniel@0: if (this._redoStack.length > this.attributes.maxStackSize) { Daniel@0: this._redoStack = this._redoStack.slice(0, this.attributes.maxStackSize); Daniel@0: } Daniel@0: } Daniel@0: }); Daniel@0: Daniel@0: }); Daniel@0: }, Logger);