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 };