source: gs3-extensions/seaweed-debug/trunk/src/UndoMan.js@ 25160

Last change on this file since 25160 was 25160, checked in by sjm84, 12 years ago

Initial cut at a version of seaweed for debugging purposes. Check it out live into the web/ext folder

File size: 13.6 KB
Line 
1/*
2 * file: UndoMan.js
3 *
4 * @BEGINLICENSE
5 * Copyright 2010 Brook Novak (email : [email protected])
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 * @ENDLICENSE
18 */
19
20// @DEPENDS: MVC collections.DoublyLinkedList
21
22bootstrap.provides("UndoMan");
23
24/**
25 * Registers an undoable action to the action repository.
26 * @param {String} name The unique name of the action
27 * @param {Object} action The action object
28 */
29var _registerAction = function() {alert("hit!!!");};
30
31(function(){
32
33 var actionRepository = {},
34 history = new _DoublyLinkedList(),
35
36 /* Always points to the next action to be undoed.
37 Null if there is no undo history.
38 Note that it can be null if there is redo history...*/
39 currentActionNode = null,
40
41 /* Non-zero if executing an action within an action. It represents the action exec depth, typically it would be 0-1, but sometimes 2. */
42 execActionDepth = 0;
43
44 // Setup registor action logic
45 _registerAction = function(name, action) {
46 debug.assert(!actionRepository[name], "Already registered action for " + name);
47 actionRepository[name] = action;
48 };
49
50 /**
51 * @class
52 *
53 * The undo manager singleton subject provides facilities for executing, undoing and redoing
54 * de.actions.UndoableAction's.
55 * <br><br>
56 * Before a action is executed/undon/redone, a "onBeforeAction" event is fired,
57 * where the argument is the action about to be executed/undon/redone.
58 * <br><br>
59 * After a action is executed/undon/redone, a "onAfterAction" event is fired,
60 * where the argument is the action that has been executed/undon/redone.
61 *
62 * @borrows de.mvc.AbstractSubject#addObserver as this.addObserver
63 *
64 * @borrows de.mvc.AbstractSubject#removeObserver as this.removeObserver
65 */
66 de.UndoMan = {
67 /**
68 * Add cap to avoid consuming too much memory... < 0 = unlimited
69 */
70 maxHistoryCount : 100,
71
72 ExecFlag : {
73
74 GROUP : 1, // @REPLACE de.UndoMan.ExecFlag.GROUP 1
75
76 UPDATE_SELECTION : 2, // @REPLACE de.UndoMan.ExecFlag.UPDATE_SELECTION 2
77
78 /**
79 * If provided then the undo manager will not store the undoable operations for undoing/redoing -
80 * It will leave the current operation list in tact after execution and thus the action
81 * will not be undone/redone directly by the undo manager.
82 *
83 * Used for executing actions within an action, or other internal specialized situations.
84 */
85 DONT_STORE : 4 // @REPLACE de.UndoMan.ExecFlag.DONT_STORE 4
86 },
87
88 /**
89 * Exposure of _registerAction internal.
90 * @see _registerAction
91 */
92 registerAction : _registerAction,
93
94 /**
95 *
96 * @param {Number} flags (Optional) NOTE: If currently executing in an action, then the DONT_STORE flag
97 * will be automatically set. This allows actions to be combined into one
98 *
99 * @param {String} actionName
100 *
101 * @return {Object} Action specific result
102 */
103 execute: function(flags, actionName) {
104
105 // Setup arguments
106 var args = Array.prototype.slice.call(arguments);
107
108 // Set default flags
109 if (typeof flags != "number") {
110 flags = de.UndoMan.ExecFlag.UPDATE_SELECTION;
111 actionName = args[0];
112 args.shift();
113 } else args.splice(0, 2);
114
115 // If already executing in a action
116 if (execActionDepth)
117 flags = de.UndoMan.ExecFlag.DONT_STORE;
118
119 // @DEBUG ON
120 // This can be helpful!!
121 if (execActionDepth) {
122 debug.println("WARNING: executing an action within an action - Undo man setting exec flags to DONT_STORE");
123 }
124 // @DEBUG OFF
125
126 if((flags & de.UndoMan.ExecFlag.GROUP) && !currentActionNode)
127 _error("Cannot group action to nothing");
128
129 // Check that the action exists
130 if (!actionRepository[actionName])
131 _error("Unknown action called \"" + actionName + "\"");
132
133 var result,
134 action = actionRepository[actionName],
135 actionData = new _ActionData(actionName, flags, de.selection.getRange(false), de.selection.getRange(true));
136
137 // Apply action filtering on selection
138 if (actionData.selBefore) {
139 var eProps = de.doc.getEditProperties(actionData.selBefore.startNode);
140
141 if (eProps && eProps.afRE) {
142 var reEval = eProps.afRE.test(actionName.toLowerCase() + (actionName == "Format" ? args[0].toLowerCase() : ""));
143 if (reEval != eProps.afInclusive)
144 return;
145 }
146
147 }
148
149 // Notify observers
150 this.fireEvent("BeforeExec", actionData);
151
152 // Safety check: there shouldn't be any operations in the current op list if storing them here
153 debug.assert((flags & de.UndoMan.ExecFlag.DONT_STORE) || !_getOperations());
154
155 // Execute the operation
156 execActionDepth++;
157 try {
158 result = action.exec.apply(actionData, args);
159 } finally {
160 execActionDepth--;
161 }
162
163 // Add to undo history? I.E: Not returning ops
164 if (!(flags & de.UndoMan.ExecFlag.DONT_STORE)) {
165
166 var opList = _getOperations();
167
168 // Did anything occur?
169 if (!opList || opList.length == 0) {
170
171 this.fireEvent("AfterExec", actionData);
172
173 // Restore selection to state before action
174 if (actionData.selBefore)
175 de.selection.setSelection(actionData.selBefore.startNode, actionData.selBefore.startIndex, actionData.selBefore.endNode, actionData.selBefore.endIndex, true);
176
177 return result;
178 }
179
180 // Destroy any redo-history
181 // If the current undo marker is at the very beggining of the
182 // list, then reset the list.
183 if (!currentActionNode)
184 history.clear(); /* Note: If history is already clear then this is ok. */
185 else if (currentActionNode.next)
186 history.chop(currentActionNode);
187
188 // Store the action's operations
189 actionData.opList = opList;
190 history.push(actionData);
191
192 // Update the undo marker
193 currentActionNode = history.tail;
194
195 // Check for max history
196 if (history.length > this.maxHistoryCount && this.maxHistoryCount > -1 && !(action.flags & de.UndoMan.ExecFlag.GROUP))
197 history.removeAtIndex(0);
198
199 }
200
201 // Should update the selection?
202 if (flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
203
204 var selAfter = actionData.selAfter;
205 if (!selAfter)
206 de.selection.clear();
207
208 else de.selection.setSelection(selAfter.startNode, selAfter.startIndex, selAfter.endNode, selAfter.endIndex, true);
209
210 } else {
211
212 // No need to keep the selection snapshots
213 delete actionData["selBefore"];
214
215 if (actionData["selAfter"])
216 delete actionData["selAfter"];
217 }
218
219 // No need to keep order selection range (only used for action exec benifit)
220 delete actionData["selBeforeOrdered"];
221
222 // Notify observers
223 this.fireEvent("AfterExec", actionData);
224
225 return result;
226
227 },
228
229 /**
230 * Undos the last action.
231 */
232 undo: function(){
233
234 do {
235
236 // If the currentActionNode is already before the head, or there is
237 // no history, then return.
238 if (!currentActionNode)
239 return;
240
241 var actionData = currentActionNode.data;
242
243 // Notify observers
244 this.fireEvent("BeforeUndo", actionData);
245
246 try {
247
248 // Undo the operation
249 _undoOperations(actionData.opList);
250
251 // Shift the action node along
252 currentActionNode = currentActionNode.prev;
253
254 } catch (err) {
255
256 // If the undo failed, the all undo/redo history can become out of
257 // sync with the DOM. Therefore lose the history to avoid bugs from
258 // snowballing into something worse.
259 this.clear();
260
261 throw err;
262 }
263
264 // Restore selection
265 if (actionData.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
266 var selBefore = actionData.selBefore;
267 if (!selBefore)
268 de.selection.clear();
269 else de.selection.setSelection(selBefore.startNode, selBefore.startIndex, selBefore.endNode, selBefore.endIndex, true);
270 }
271
272 // Notify observers
273 this.fireEvent("AfterUndo", actionData);
274
275 } while (currentActionNode && (actionData.flags & de.UndoMan.ExecFlag.GROUP));
276
277 },
278
279 /**
280 * Redo's the last undo.
281 */
282 redo: function(){
283
284 var firstRedo = true;
285
286 while(1) {
287
288 if (history.length == 0 || currentActionNode == history.tail)
289 return;
290
291 // Is the undo-pointer back to the very start of the history?
292 var curAction = currentActionNode ? currentActionNode.next : history.head;
293 var actionData = curAction.data;
294
295 // Only continue redoing if the action if grouped
296 if (!firstRedo && !(actionData.flags & de.UndoMan.ExecFlag.GROUP))
297 return;
298
299 currentActionNode = curAction;
300
301 // Notify observers
302 this.fireEvent("BeforeRedo", actionData);
303
304 // Re-execute the operations
305 try {
306 _redoOperations(actionData.opList);
307 } catch (err) {
308
309 // If the undo failed, the all undo/redo history can become out of
310 // sync with the DOM. Therefore lose the history to avoid bugs from
311 // snowballing into something worse.
312 this.clear();
313 throw err;
314 }
315
316 // Set the new selection if it was requested to update the selection
317 // with this action
318 if (actionData.selAfter) {
319 var selAfter = actionData.selAfter;
320 if (!selAfter)
321 de.selection.clear();
322 else de.selection.setSelection(selAfter.startNode, selAfter.startIndex, selAfter.endNode, selAfter.endIndex, true);
323 }
324
325 firstRedo = false;
326
327 // Notify observers
328 this.fireEvent("AfterRedo", actionData);
329
330 }
331
332 },
333
334 /**
335 * Clears all undo/redo history
336 */
337 clear : function() {
338 history.clear();
339 currentActionNode = null;
340 },
341
342 /**
343 * @return {Boolean} True if there is any undo history.
344 */
345 hasUndo : function() {
346 return currentActionNode != null;
347 },
348
349 /**
350 * @return {Boolean} True if there is any redo history.
351 */
352 hasRedo : function() {
353 return history.length > 0 && currentActionNode != history.tail;
354 }
355
356 }; // End undo manager singleton
357
358 // Make undo manager a model
359 _model(de.UndoMan);
360
361})();
362
363
364var _ActionData = function() {
365
366 var cls = function(name, flags, selBefore, selBeforeOrdered) {
367 this.name = name;
368 this.flags = flags;
369 this.selBefore = selBefore;
370 this.selBeforeOrdered = selBeforeOrdered;
371 /* this.opList = null */
372 }
373
374 cls.prototype = {
375
376 /**
377 * @return {Node} The top-most editable section changed by this action. Undefined if there was none.
378 */
379 getEditSection : function() {
380
381 if (this.opList) {
382 // Infer from operations list
383 for (var i in this.opList) {
384 var op = this.opList[i];
385 for (var mem in op) {
386 // Is this a dom node still in the document body?
387 if (_isDOMNode(op[mem]) && _isAncestor(docBody, op[mem])) {
388 var esNode = de.doc.getEditSectionContainer(op[mem]);
389 if (esNode)
390 return esNode;
391 }
392 }
393 }
394 }
395 }
396
397 };
398
399 return cls;
400
401}();
402
Note: See TracBrowser for help on using the repository browser.