1 /** 2 * Creates a new UndoManager 3 * 4 * @constructor 5 * @param {Integer} [maxStackSize=64] 6 */ 7 function UndoManager(maxStackSize) { 8 this.maxStackSize = maxStackSize || 64; 9 10 var State = { 11 UNDO : "undo", 12 REDO : "redo" 13 }; 14 15 var self = this; 16 var undoStack = new UndoManager.CircularStack(this.maxStackSize); 17 var redoStack = new UndoManager.CircularStack(this.maxStackSize); 18 var undoContext = false; 19 var currentAction = null; 20 var currentState = null; 21 22 var onStateChange = function() { 23 if (self.stateChanged) { 24 self.stateChanged(); 25 } 26 }; 27 28 var callAction = function(action) { 29 currentAction = action; 30 undoContext = true; 31 switch (currentState) { 32 case State.UNDO: 33 action.undo(); 34 break; 35 case State.REDO: 36 action.redo(); 37 break; 38 } 39 undoContext = false; 40 }; 41 42 /** 43 * Register an undo operation. A call to .undo() will cause the undo 44 * function to be executed. If you omit the second argument and the undo 45 * function will cause the registration of another undo operation, then this 46 * operation will be used as the redo function. 47 * 48 * If you provide both arguments, a call to addUndo() during an undo() or 49 * redo() will have no effect. 50 * 51 * 52 * @param {Function} undoFunc The function that should undo the changes. 53 * @param {Function} [redoFunc] The function that should redo the undone 54 * changes. 55 */ 56 this.addUndo = function(undoFunc, redoFunc) { 57 if (undoContext) { 58 /** 59 * If we are currently undoing an action and don't have a redo 60 * function yet, store the undo function to the undo function, which 61 * is in turn the redo function. 62 */ 63 if (currentAction.redo == null && currentState == State.UNDO) { 64 currentAction.redo = undoFunc; 65 } 66 } else { 67 /** 68 * We are not undoing right now. Store the functions as an action. 69 */ 70 var action = { 71 undo : undoFunc, 72 redo : redoFunc 73 }; 74 undoStack.push(action); 75 // clear redo stack 76 redoStack.clear(); 77 78 onStateChange(); 79 } 80 }; 81 82 /** 83 * Undoes the last action. 84 */ 85 this.undo = function() { 86 if (this.canUndo()) { 87 currentState = State.UNDO; 88 var action = undoStack.pop(); 89 callAction(action); 90 91 if (action.redo) { 92 redoStack.push(action); 93 } 94 95 onStateChange(); 96 } 97 }; 98 99 /** 100 * Redoes the last action. 101 */ 102 this.redo = function() { 103 if (this.canRedo()) { 104 currentState = State.REDO; 105 var action = redoStack.pop(); 106 callAction(action); 107 108 if (action.undo) { 109 undoStack.push(action); 110 } 111 112 onStateChange(); 113 } 114 }; 115 116 /** 117 * 118 * @returns {Boolean} true if undo is possible, false otherwise. 119 */ 120 this.canUndo = function() { 121 return !undoStack.isEmpty(); 122 }; 123 124 /** 125 * 126 * @returns {Boolean} true if redo is possible, false otherwise. 127 */ 128 this.canRedo = function() { 129 return !redoStack.isEmpty(); 130 }; 131 132 /** 133 * Resets this instance of the undo manager. 134 */ 135 this.reset = function() { 136 undoStack.clear(); 137 redoStack.clear(); 138 undoContext = false; 139 currentAction = null; 140 currentState = null; 141 142 onStateChange(); 143 }; 144 145 /** 146 * Event that is fired when undo or redo state changes. 147 * 148 * @event 149 */ 150 this.stateChanged = function() { 151 }; 152 } 153 154 /** 155 * Creates a new CircularStack. This is a stack implementation backed by a 156 * circular buffer where the oldest entries automatically are overwritten when 157 * new items are pushed onto the stack and the maximum size has been reached. 158 * 159 * @constructor 160 * @param {Integer} [maxSize=32] 161 */ 162 UndoManager.CircularStack = function(maxSize) { 163 this.maxSize = maxSize || 32; 164 this.buffer = []; 165 this.nextPointer = 0; 166 }; 167 168 /** 169 * Pushes a new item onto the stack. 170 * 171 * @param {Any} item 172 */ 173 UndoManager.CircularStack.prototype.push = function(item) { 174 this.buffer[this.nextPointer] = item; 175 this.nextPointer = (this.nextPointer + 1) % this.maxSize; 176 }; 177 178 /** 179 * Checks whether the stack is empty. 180 * 181 * @returns {Boolean} true if empty, false otherwise. 182 */ 183 UndoManager.CircularStack.prototype.isEmpty = function() { 184 if (this.buffer.length === 0) { 185 return true; 186 } 187 188 var prevPointer = this.getPreviousPointer(); 189 if (prevPointer === null) { 190 return true; 191 } else { 192 return this.buffer[prevPointer] === null; 193 } 194 }; 195 196 /** 197 * Gets the position of the previously inserted item in the buffer. 198 * 199 * @private 200 * @returns {Integer} the previous pointer position or null if no previous 201 * exists. 202 */ 203 UndoManager.CircularStack.prototype.getPreviousPointer = function() { 204 if (this.nextPointer > 0) { 205 return this.nextPointer - 1; 206 } else { 207 if (this.buffer.length < this.maxSize) { 208 return null; 209 } else { 210 return this.maxSize - 1; 211 } 212 } 213 }; 214 215 /** 216 * Clears the stack. 217 */ 218 UndoManager.CircularStack.prototype.clear = function() { 219 this.buffer.length = 0; 220 this.nextPointer = 0; 221 }; 222 223 /** 224 * Returns and removes the top most item of the stack. 225 * 226 * @returns {Any} the last inserted item or null if stack is empty. 227 */ 228 UndoManager.CircularStack.prototype.pop = function() { 229 if (this.isEmpty()) { 230 return null; 231 } 232 233 var previousPointer = this.getPreviousPointer(); 234 var item = this.buffer[previousPointer]; 235 this.buffer[previousPointer] = null; 236 this.nextPointer = previousPointer; 237 238 return item; 239 }; 240 241 /** 242 * Returns but not removes the top most item of the stack. 243 * 244 * @returns {Any} the last inserted item or null if stack is empty. 245 */ 246 UndoManager.CircularStack.prototype.peek = function() { 247 if (this.isEmpty()) { 248 return null; 249 } 250 return this.buffer[this.getPreviousPointer()]; 251 };