source: other-projects/nz-flag-design/trunk/design-2d/Original editor.method.ac/method-draw/src/history.js@ 29468

Last change on this file since 29468 was 29468, checked in by sjs49, 9 years ago

Initial commit for editor.method.ac for flag design

  • Property svn:executable set to *
File size: 19.8 KB
Line 
1/**
2 * Package: svedit.history
3 *
4 * Licensed under the Apache License, Version 2
5 *
6 * Copyright(c) 2010 Jeff Schiller
7 */
8
9// Dependencies:
10// 1) jQuery
11// 2) svgtransformlist.js
12// 3) svgutils.js
13
14var svgedit = svgedit || {};
15
16(function() {
17
18if (!svgedit.history) {
19 svgedit.history = {};
20}
21
22// Group: Undo/Redo history management
23
24
25svgedit.history.HistoryEventTypes = {
26 BEFORE_APPLY: 'before_apply',
27 AFTER_APPLY: 'after_apply',
28 BEFORE_UNAPPLY: 'before_unapply',
29 AFTER_UNAPPLY: 'after_unapply'
30};
31
32var removedElements = {};
33
34/**
35 * Interface: svgedit.history.HistoryCommand
36 * An interface that all command objects must implement.
37 *
38 * interface svgedit.history.HistoryCommand {
39 * void apply(svgedit.history.HistoryEventHandler);
40 * void unapply(svgedit.history.HistoryEventHandler);
41 * Element[] elements();
42 * String getText();
43 *
44 * static String type();
45 * }
46 *
47 * Interface: svgedit.history.HistoryEventHandler
48 * An interface for objects that will handle history events.
49 *
50 * interface svgedit.history.HistoryEventHandler {
51 * void handleHistoryEvent(eventType, command);
52 * }
53 *
54 * eventType is a string conforming to one of the HistoryEvent types.
55 * command is an object fulfilling the HistoryCommand interface.
56 */
57
58// Class: svgedit.history.MoveElementCommand
59// implements svgedit.history.HistoryCommand
60// History command for an element that had its DOM position changed
61//
62// Parameters:
63// elem - The DOM element that was moved
64// oldNextSibling - The element's next sibling before it was moved
65// oldParent - The element's parent before it was moved
66// text - An optional string visible to user related to this change
67svgedit.history.MoveElementCommand = function(elem, oldNextSibling, oldParent, text) {
68 this.elem = elem;
69 this.text = text ? ("Move " + elem.tagName + " to " + text) : ("Move " + elem.tagName);
70 this.oldNextSibling = oldNextSibling;
71 this.oldParent = oldParent;
72 this.newNextSibling = elem.nextSibling;
73 this.newParent = elem.parentNode;
74};
75svgedit.history.MoveElementCommand.type = function() { return 'svgedit.history.MoveElementCommand'; }
76svgedit.history.MoveElementCommand.prototype.type = svgedit.history.MoveElementCommand.type;
77
78// Function: svgedit.history.MoveElementCommand.getText
79svgedit.history.MoveElementCommand.prototype.getText = function() {
80 return this.text;
81};
82
83// Function: svgedit.history.MoveElementCommand.apply
84// Re-positions the element
85svgedit.history.MoveElementCommand.prototype.apply = function(handler) {
86 // TODO(codedread): Refactor this common event code into a base HistoryCommand class.
87 if (handler) {
88 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
89 }
90
91 this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling);
92
93 if (handler) {
94 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
95 }
96};
97
98// Function: svgedit.history.MoveElementCommand.unapply
99// Positions the element back to its original location
100svgedit.history.MoveElementCommand.prototype.unapply = function(handler) {
101 if (handler) {
102 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
103 }
104
105 this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling);
106
107 if (handler) {
108 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
109 }
110};
111
112// Function: svgedit.history.MoveElementCommand.elements
113// Returns array with element associated with this command
114svgedit.history.MoveElementCommand.prototype.elements = function() {
115 return [this.elem];
116};
117
118
119// Class: svgedit.history.InsertElementCommand
120// implements svgedit.history.HistoryCommand
121// History command for an element that was added to the DOM
122//
123// Parameters:
124// elem - The newly added DOM element
125// text - An optional string visible to user related to this change
126svgedit.history.InsertElementCommand = function(elem, text) {
127 this.elem = elem;
128 this.text = text || ("Create " + elem.tagName);
129 this.parent = elem.parentNode;
130 this.nextSibling = this.elem.nextSibling;
131};
132svgedit.history.InsertElementCommand.type = function() { return 'svgedit.history.InsertElementCommand'; }
133svgedit.history.InsertElementCommand.prototype.type = svgedit.history.InsertElementCommand.type;
134
135// Function: svgedit.history.InsertElementCommand.getText
136svgedit.history.InsertElementCommand.prototype.getText = function() {
137 return this.text;
138};
139
140// Function: svgedit.history.InsertElementCommand.apply
141// Re-Inserts the new element
142svgedit.history.InsertElementCommand.prototype.apply = function(handler) {
143 if (handler) {
144 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
145 }
146
147 this.elem = this.parent.insertBefore(this.elem, this.nextSibling);
148
149 if (handler) {
150 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
151 }
152};
153
154// Function: svgedit.history.InsertElementCommand.unapply
155// Removes the element
156svgedit.history.InsertElementCommand.prototype.unapply = function(handler) {
157 if (handler) {
158 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
159 }
160
161 this.parent = this.elem.parentNode;
162 this.elem = this.elem.parentNode.removeChild(this.elem);
163
164 if (handler) {
165 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
166 }
167};
168
169// Function: svgedit.history.InsertElementCommand.elements
170// Returns array with element associated with this command
171svgedit.history.InsertElementCommand.prototype.elements = function() {
172 return [this.elem];
173};
174
175
176// Class: svgedit.history.RemoveElementCommand
177// implements svgedit.history.HistoryCommand
178// History command for an element removed from the DOM
179//
180// Parameters:
181// elem - The removed DOM element
182// oldNextSibling - the DOM element's nextSibling when it was in the DOM
183// oldParent - The DOM element's parent
184// text - An optional string visible to user related to this change
185svgedit.history.RemoveElementCommand = function(elem, oldNextSibling, oldParent, text) {
186 this.elem = elem;
187 this.text = text || ("Delete " + elem.tagName);
188 this.nextSibling = oldNextSibling;
189 this.parent = oldParent;
190
191 // special hack for webkit: remove this element's entry in the svgTransformLists map
192 svgedit.transformlist.removeElementFromListMap(elem);
193};
194svgedit.history.RemoveElementCommand.type = function() { return 'svgedit.history.RemoveElementCommand'; }
195svgedit.history.RemoveElementCommand.prototype.type = svgedit.history.RemoveElementCommand.type;
196
197// Function: svgedit.history.RemoveElementCommand.getText
198svgedit.history.RemoveElementCommand.prototype.getText = function() {
199 return this.text;
200};
201
202// Function: RemoveElementCommand.apply
203// Re-removes the new element
204svgedit.history.RemoveElementCommand.prototype.apply = function(handler) {
205 if (handler) {
206 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
207 }
208
209 svgedit.transformlist.removeElementFromListMap(this.elem);
210 this.parent = this.elem.parentNode;
211 this.elem = this.parent.removeChild(this.elem);
212
213 if (handler) {
214 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
215 }
216};
217
218// Function: RemoveElementCommand.unapply
219// Re-adds the new element
220svgedit.history.RemoveElementCommand.prototype.unapply = function(handler) {
221 if (handler) {
222 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
223 }
224
225 svgedit.transformlist.removeElementFromListMap(this.elem);
226 if(this.nextSibling == null) {
227 if(window.console) console.log('Error: reference element was lost');
228 }
229 this.parent.insertBefore(this.elem, this.nextSibling);
230
231
232 if (handler) {
233 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
234 }
235};
236
237// Function: RemoveElementCommand.elements
238// Returns array with element associated with this command
239svgedit.history.RemoveElementCommand.prototype.elements = function() {
240 return [this.elem];
241};
242
243
244// Class: svgedit.history.ChangeElementCommand
245// implements svgedit.history.HistoryCommand
246// History command to make a change to an element.
247// Usually an attribute change, but can also be textcontent.
248//
249// Parameters:
250// elem - The DOM element that was changed
251// attrs - An object with the attributes to be changed and the values they had *before* the change
252// text - An optional string visible to user related to this change
253svgedit.history.ChangeElementCommand = function(elem, attrs, text) {
254 this.elem = elem;
255 this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName);
256 this.newValues = {};
257 this.oldValues = attrs;
258 for (var attr in attrs) {
259 if (attr == "#text") this.newValues[attr] = elem.textContent;
260 else if (attr == "#href") this.newValues[attr] = svgedit.utilities.getHref(elem);
261 else this.newValues[attr] = elem.getAttribute(attr);
262 }
263};
264svgedit.history.ChangeElementCommand.type = function() { return 'svgedit.history.ChangeElementCommand'; }
265svgedit.history.ChangeElementCommand.prototype.type = svgedit.history.ChangeElementCommand.type;
266
267// Function: svgedit.history.ChangeElementCommand.getText
268svgedit.history.ChangeElementCommand.prototype.getText = function() {
269 return this.text;
270};
271
272// Function: svgedit.history.ChangeElementCommand.apply
273// Performs the stored change action
274svgedit.history.ChangeElementCommand.prototype.apply = function(handler) {
275 if (handler) {
276 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
277 }
278
279 var bChangedTransform = false;
280 for(var attr in this.newValues ) {
281 if (this.newValues[attr]) {
282 if (attr == "#text") this.elem.textContent = this.newValues[attr];
283 else if (attr == "#href") svgedit.utilities.setHref(this.elem, this.newValues[attr])
284 else this.elem.setAttribute(attr, this.newValues[attr]);
285 }
286 else {
287 if (attr == "#text") {
288 this.elem.textContent = "";
289 }
290 else {
291 this.elem.setAttribute(attr, "");
292 this.elem.removeAttribute(attr);
293 }
294 }
295
296 if (attr == "transform") { bChangedTransform = true; }
297 }
298
299 // relocate rotational transform, if necessary
300 if(!bChangedTransform) {
301 var angle = svgedit.utilities.getRotationAngle(this.elem);
302 if (angle) {
303 var bbox = elem.getBBox();
304 var cx = bbox.x + bbox.width/2,
305 cy = bbox.y + bbox.height/2;
306 var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
307 if (rotate != elem.getAttribute("transform")) {
308 elem.setAttribute("transform", rotate);
309 }
310 }
311 }
312
313 if (handler) {
314 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
315 }
316
317 return true;
318};
319
320// Function: svgedit.history.ChangeElementCommand.unapply
321// Reverses the stored change action
322svgedit.history.ChangeElementCommand.prototype.unapply = function(handler) {
323 if (handler) {
324 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
325 }
326
327 var bChangedTransform = false;
328 for(var attr in this.oldValues ) {
329 if (this.oldValues[attr]) {
330 if (attr == "#text") this.elem.textContent = this.oldValues[attr];
331 else if (attr == "#href") svgedit.utilities.setHref(this.elem, this.oldValues[attr]);
332 else this.elem.setAttribute(attr, this.oldValues[attr]);
333 }
334 else {
335 if (attr == "#text") {
336 this.elem.textContent = "";
337 }
338 else this.elem.removeAttribute(attr);
339 }
340 if (attr == "transform") { bChangedTransform = true; }
341 }
342 // relocate rotational transform, if necessary
343 if(!bChangedTransform) {
344 var angle = svgedit.utilities.getRotationAngle(this.elem);
345 if (angle) {
346 var bbox = this.elem.getBBox();
347 var cx = bbox.x + bbox.width/2,
348 cy = bbox.y + bbox.height/2;
349 var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
350 if (rotate != this.elem.getAttribute("transform")) {
351 this.elem.setAttribute("transform", rotate);
352 }
353 }
354 }
355
356 // Remove transformlist to prevent confusion that causes bugs like 575.
357 svgedit.transformlist.removeElementFromListMap(this.elem);
358
359 if (handler) {
360 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
361 }
362
363 return true;
364};
365
366// Function: ChangeElementCommand.elements
367// Returns array with element associated with this command
368svgedit.history.ChangeElementCommand.prototype.elements = function() {
369 return [this.elem];
370};
371
372
373// TODO: create a 'typing' command object that tracks changes in text
374// if a new Typing command is created and the top command on the stack is also a Typing
375// and they both affect the same element, then collapse the two commands into one
376
377
378// Class: svgedit.history.BatchCommand
379// implements svgedit.history.HistoryCommand
380// History command that can contain/execute multiple other commands
381//
382// Parameters:
383// text - An optional string visible to user related to this change
384svgedit.history.BatchCommand = function(text) {
385 this.text = text || "Batch Command";
386 this.stack = [];
387};
388svgedit.history.BatchCommand.type = function() { return 'svgedit.history.BatchCommand'; }
389svgedit.history.BatchCommand.prototype.type = svgedit.history.BatchCommand.type;
390
391// Function: svgedit.history.BatchCommand.getText
392svgedit.history.BatchCommand.prototype.getText = function() {
393 return this.text;
394};
395
396// Function: svgedit.history.BatchCommand.apply
397// Runs "apply" on all subcommands
398svgedit.history.BatchCommand.prototype.apply = function(handler) {
399 if (handler) {
400 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
401 }
402
403 var len = this.stack.length;
404 for (var i = 0; i < len; ++i) {
405 this.stack[i].apply(handler);
406 }
407
408 if (handler) {
409 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
410 }
411};
412
413// Function: svgedit.history.BatchCommand.unapply
414// Runs "unapply" on all subcommands
415svgedit.history.BatchCommand.prototype.unapply = function(handler) {
416 if (handler) {
417 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
418 }
419
420 for (var i = this.stack.length-1; i >= 0; i--) {
421 this.stack[i].unapply(handler);
422 }
423
424 if (handler) {
425 handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
426 }
427};
428
429// Function: svgedit.history.BatchCommand.elements
430// Iterate through all our subcommands and returns all the elements we are changing
431svgedit.history.BatchCommand.prototype.elements = function() {
432 var elems = [];
433 var cmd = this.stack.length;
434 while (cmd--) {
435 var thisElems = this.stack[cmd].elements();
436 var elem = thisElems.length;
437 while (elem--) {
438 if (elems.indexOf(thisElems[elem]) == -1) elems.push(thisElems[elem]);
439 }
440 }
441 return elems;
442};
443
444// Function: svgedit.history.BatchCommand.addSubCommand
445// Adds a given command to the history stack
446//
447// Parameters:
448// cmd - The undo command object to add
449svgedit.history.BatchCommand.prototype.addSubCommand = function(cmd) {
450 this.stack.push(cmd);
451};
452
453// Function: svgedit.history.BatchCommand.isEmpty
454// Returns a boolean indicating whether or not the batch command is empty
455svgedit.history.BatchCommand.prototype.isEmpty = function() {
456 return this.stack.length == 0;
457};
458
459
460// Class: svgedit.history.UndoManager
461// Parameters:
462// historyEventHandler - an object that conforms to the HistoryEventHandler interface
463// (see above)
464svgedit.history.UndoManager = function(historyEventHandler) {
465 this.handler_ = historyEventHandler || null;
466 this.undoStackPointer = 0;
467 this.undoStack = [];
468
469 // this is the stack that stores the original values, the elements and
470 // the attribute name for begin/finish
471 this.undoChangeStackPointer = -1;
472 this.undoableChangeStack = [];
473};
474
475// Function: svgedit.history.UndoManager.resetUndoStack
476// Resets the undo stack, effectively clearing the undo/redo history
477svgedit.history.UndoManager.prototype.resetUndoStack = function() {
478 this.undoStack = [];
479 this.undoStackPointer = 0;
480};
481
482// Function: svgedit.history.UndoManager.getUndoStackSize
483// Returns:
484// Integer with the current size of the undo history stack
485svgedit.history.UndoManager.prototype.getUndoStackSize = function() {
486 return this.undoStackPointer;
487};
488
489// Function: svgedit.history.UndoManager.getRedoStackSize
490// Returns:
491// Integer with the current size of the redo history stack
492svgedit.history.UndoManager.prototype.getRedoStackSize = function() {
493 return this.undoStack.length - this.undoStackPointer;
494};
495
496// Function: svgedit.history.UndoManager.getNextUndoCommandText
497// Returns:
498// String associated with the next undo command
499svgedit.history.UndoManager.prototype.getNextUndoCommandText = function() {
500 return this.undoStackPointer > 0 ? this.undoStack[this.undoStackPointer-1].getText() : "";
501};
502
503// Function: svgedit.history.UndoManager.getNextRedoCommandText
504// Returns:
505// String associated with the next redo command
506svgedit.history.UndoManager.prototype.getNextRedoCommandText = function() {
507 return this.undoStackPointer < this.undoStack.length ? this.undoStack[this.undoStackPointer].getText() : "";
508};
509
510// Function: svgedit.history.UndoManager.undo
511// Performs an undo step
512svgedit.history.UndoManager.prototype.undo = function() {
513 if (this.undoStackPointer > 0) {
514 var cmd = this.undoStack[--this.undoStackPointer];
515 cmd.unapply(this.handler_);
516 }
517};
518
519// Function: svgedit.history.UndoManager.redo
520// Performs a redo step
521svgedit.history.UndoManager.prototype.redo = function() {
522 if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
523 var cmd = this.undoStack[this.undoStackPointer++];
524 cmd.apply(this.handler_);
525 }
526};
527
528// Function: svgedit.history.UndoManager.addCommandToHistory
529// Adds a command object to the undo history stack
530//
531// Parameters:
532// cmd - The command object to add
533svgedit.history.UndoManager.prototype.addCommandToHistory = function(cmd) {
534 // FIXME: we MUST compress consecutive text changes to the same element
535 // (right now each keystroke is saved as a separate command that includes the
536 // entire text contents of the text element)
537 // TODO: consider limiting the history that we store here (need to do some slicing)
538
539 // if our stack pointer is not at the end, then we have to remove
540 // all commands after the pointer and insert the new command
541 if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
542 this.undoStack = this.undoStack.splice(0, this.undoStackPointer);
543 }
544 this.undoStack.push(cmd);
545 this.undoStackPointer = this.undoStack.length;
546};
547
548
549// Function: svgedit.history.UndoManager.beginUndoableChange
550// This function tells the canvas to remember the old values of the
551// attrName attribute for each element sent in. The elements and values
552// are stored on a stack, so the next call to finishUndoableChange() will
553// pop the elements and old values off the stack, gets the current values
554// from the DOM and uses all of these to construct the undo-able command.
555//
556// Parameters:
557// attrName - The name of the attribute being changed
558// elems - Array of DOM elements being changed
559svgedit.history.UndoManager.prototype.beginUndoableChange = function(attrName, elems) {
560 var p = ++this.undoChangeStackPointer;
561 var i = elems.length;
562 var oldValues = new Array(i), elements = new Array(i);
563 while (i--) {
564 var elem = elems[i];
565 if (elem == null) continue;
566 elements[i] = elem;
567 oldValues[i] = elem.getAttribute(attrName);
568 }
569 this.undoableChangeStack[p] = {'attrName': attrName,
570 'oldValues': oldValues,
571 'elements': elements};
572};
573
574// Function: svgedit.history.UndoManager.finishUndoableChange
575// This function returns a BatchCommand object which summarizes the
576// change since beginUndoableChange was called. The command can then
577// be added to the command history
578//
579// Returns:
580// Batch command object with resulting changes
581svgedit.history.UndoManager.prototype.finishUndoableChange = function() {
582 var p = this.undoChangeStackPointer--;
583 var changeset = this.undoableChangeStack[p];
584 var i = changeset['elements'].length;
585 var attrName = changeset['attrName'];
586 var batchCmd = new svgedit.history.BatchCommand("Change " + attrName);
587 while (i--) {
588 var elem = changeset['elements'][i];
589 if (elem == null) continue;
590 var changes = {};
591 changes[attrName] = changeset['oldValues'][i];
592 if (changes[attrName] != elem.getAttribute(attrName)) {
593 batchCmd.addSubCommand(new svgedit.history.ChangeElementCommand(elem, changes, attrName));
594 }
595 }
596 this.undoableChangeStack[p] = null;
597 return batchCmd;
598};
599
600
601})();
Note: See TracBrowser for help on using the repository browser.