123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- /*globals svgedit*/
- /*jslint vars: true, eqeq: true, continue: true, forin: true*/
- /**
- * Package: svedit.history
- *
- * Licensed under the MIT License
- *
- * Copyright(c) 2010 Jeff Schiller
- */
- // Dependencies:
- // 1) jQuery
- // 2) svgtransformlist.js
- // 3) svgutils.js
- (function() {'use strict';
- if (!svgedit.history) {
- svgedit.history = {};
- }
- // Group: Undo/Redo history management
- svgedit.history.HistoryEventTypes = {
- BEFORE_APPLY: 'before_apply',
- AFTER_APPLY: 'after_apply',
- BEFORE_UNAPPLY: 'before_unapply',
- AFTER_UNAPPLY: 'after_unapply'
- };
- var removedElements = {};
- /**
- * An interface that all command objects must implement.
- * @typedef svgedit.history.HistoryCommand
- * @type {object}
- * void apply(svgedit.history.HistoryEventHandler);
- * void unapply(svgedit.history.HistoryEventHandler);
- * Element[] elements();
- * String getText();
- *
- * static String type();
- * }
- *
- * Interface: svgedit.history.HistoryEventHandler
- * An interface for objects that will handle history events.
- *
- * interface svgedit.history.HistoryEventHandler {
- * void handleHistoryEvent(eventType, command);
- * }
- *
- * eventType is a string conforming to one of the HistoryEvent types.
- * command is an object fulfilling the HistoryCommand interface.
- */
- /**
- * @class svgedit.history.MoveElementCommand
- * @implements svgedit.history.HistoryCommand
- * History command for an element that had its DOM position changed
- * @param {Element} elem - The DOM element that was moved
- * @param {Element} oldNextSibling - The element's next sibling before it was moved
- * @param {Element} oldParent - The element's parent before it was moved
- * @param {string} [text] - An optional string visible to user related to this change
- */
- svgedit.history.MoveElementCommand = function(elem, oldNextSibling, oldParent, text) {
- this.elem = elem;
- this.text = text ? ("Move " + elem.tagName + " to " + text) : ("Move " + elem.tagName);
- this.oldNextSibling = oldNextSibling;
- this.oldParent = oldParent;
- this.newNextSibling = elem.nextSibling;
- this.newParent = elem.parentNode;
- };
- svgedit.history.MoveElementCommand.type = function() { return 'svgedit.history.MoveElementCommand'; };
- svgedit.history.MoveElementCommand.prototype.type = svgedit.history.MoveElementCommand.type;
- svgedit.history.MoveElementCommand.prototype.getText = function() {
- return this.text;
- };
- /**
- * Re-positions the element
- * @param {handleHistoryEvent: function}
- */
- svgedit.history.MoveElementCommand.prototype.apply = function(handler) {
- // TODO(codedread): Refactor this common event code into a base HistoryCommand class.
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
- }
- this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling);
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
- }
- };
- /**
- * Positions the element back to its original location
- * @param {handleHistoryEvent: function}
- */
- svgedit.history.MoveElementCommand.prototype.unapply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
- }
- this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling);
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
- }
- };
- // Function: svgedit.history.MoveElementCommand.elements
- // Returns array with element associated with this command
- svgedit.history.MoveElementCommand.prototype.elements = function() {
- return [this.elem];
- };
- // Class: svgedit.history.InsertElementCommand
- // implements svgedit.history.HistoryCommand
- // History command for an element that was added to the DOM
- //
- // Parameters:
- // elem - The newly added DOM element
- // text - An optional string visible to user related to this change
- svgedit.history.InsertElementCommand = function(elem, text) {
- this.elem = elem;
- this.text = text || ("Create " + elem.tagName);
- this.parent = elem.parentNode;
- this.nextSibling = this.elem.nextSibling;
- };
- svgedit.history.InsertElementCommand.type = function() { return 'svgedit.history.InsertElementCommand'; };
- svgedit.history.InsertElementCommand.prototype.type = svgedit.history.InsertElementCommand.type;
- // Function: svgedit.history.InsertElementCommand.getText
- svgedit.history.InsertElementCommand.prototype.getText = function() {
- return this.text;
- };
- // Function: svgedit.history.InsertElementCommand.apply
- // Re-Inserts the new element
- svgedit.history.InsertElementCommand.prototype.apply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
- }
- this.elem = this.parent.insertBefore(this.elem, this.nextSibling);
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
- }
- };
- // Function: svgedit.history.InsertElementCommand.unapply
- // Removes the element
- svgedit.history.InsertElementCommand.prototype.unapply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
- }
- this.parent = this.elem.parentNode;
- this.elem = this.elem.parentNode.removeChild(this.elem);
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
- }
- };
- // Function: svgedit.history.InsertElementCommand.elements
- // Returns array with element associated with this command
- svgedit.history.InsertElementCommand.prototype.elements = function() {
- return [this.elem];
- };
- // Class: svgedit.history.RemoveElementCommand
- // implements svgedit.history.HistoryCommand
- // History command for an element removed from the DOM
- //
- // Parameters:
- // elem - The removed DOM element
- // oldNextSibling - the DOM element's nextSibling when it was in the DOM
- // oldParent - The DOM element's parent
- // text - An optional string visible to user related to this change
- svgedit.history.RemoveElementCommand = function(elem, oldNextSibling, oldParent, text) {
- this.elem = elem;
- this.text = text || ("Delete " + elem.tagName);
- this.nextSibling = oldNextSibling;
- this.parent = oldParent;
- // special hack for webkit: remove this element's entry in the svgTransformLists map
- svgedit.transformlist.removeElementFromListMap(elem);
- };
- svgedit.history.RemoveElementCommand.type = function() { return 'svgedit.history.RemoveElementCommand'; };
- svgedit.history.RemoveElementCommand.prototype.type = svgedit.history.RemoveElementCommand.type;
- // Function: svgedit.history.RemoveElementCommand.getText
- svgedit.history.RemoveElementCommand.prototype.getText = function() {
- return this.text;
- };
- // Function: RemoveElementCommand.apply
- // Re-removes the new element
- svgedit.history.RemoveElementCommand.prototype.apply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
- }
- svgedit.transformlist.removeElementFromListMap(this.elem);
- this.parent = this.elem.parentNode;
- this.elem = this.parent.removeChild(this.elem);
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
- }
- };
- // Function: RemoveElementCommand.unapply
- // Re-adds the new element
- svgedit.history.RemoveElementCommand.prototype.unapply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
- }
- svgedit.transformlist.removeElementFromListMap(this.elem);
- if (this.nextSibling == null) {
- if (window.console) {
- console.log('Error: reference element was lost');
- }
- }
- this.parent.insertBefore(this.elem, this.nextSibling);
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
- }
- };
- // Function: RemoveElementCommand.elements
- // Returns array with element associated with this command
- svgedit.history.RemoveElementCommand.prototype.elements = function() {
- return [this.elem];
- };
- // Class: svgedit.history.ChangeElementCommand
- // implements svgedit.history.HistoryCommand
- // History command to make a change to an element.
- // Usually an attribute change, but can also be textcontent.
- //
- // Parameters:
- // elem - The DOM element that was changed
- // attrs - An object with the attributes to be changed and the values they had *before* the change
- // text - An optional string visible to user related to this change
- svgedit.history.ChangeElementCommand = function(elem, attrs, text) {
- this.elem = elem;
- this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName);
- this.newValues = {};
- this.oldValues = attrs;
- var attr;
- for (attr in attrs) {
- if (attr == "#text") {this.newValues[attr] = elem.textContent;}
- else if (attr == "#href") {this.newValues[attr] = svgedit.utilities.getHref(elem);}
- else {this.newValues[attr] = elem.getAttribute(attr);}
- }
- };
- svgedit.history.ChangeElementCommand.type = function() { return 'svgedit.history.ChangeElementCommand'; };
- svgedit.history.ChangeElementCommand.prototype.type = svgedit.history.ChangeElementCommand.type;
- // Function: svgedit.history.ChangeElementCommand.getText
- svgedit.history.ChangeElementCommand.prototype.getText = function() {
- return this.text;
- };
- // Function: svgedit.history.ChangeElementCommand.apply
- // Performs the stored change action
- svgedit.history.ChangeElementCommand.prototype.apply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
- }
- var bChangedTransform = false;
- var attr;
- for (attr in this.newValues ) {
- if (this.newValues[attr]) {
- if (attr == "#text") {this.elem.textContent = this.newValues[attr];}
- else if (attr == "#href") {svgedit.utilities.setHref(this.elem, this.newValues[attr]);}
- else {this.elem.setAttribute(attr, this.newValues[attr]);}
- }
- else {
- if (attr == "#text") {
- this.elem.textContent = "";
- }
- else {
- this.elem.setAttribute(attr, "");
- this.elem.removeAttribute(attr);
- }
- }
- if (attr == "transform") { bChangedTransform = true; }
- }
- // relocate rotational transform, if necessary
- if (!bChangedTransform) {
- var angle = svgedit.utilities.getRotationAngle(this.elem);
- if (angle) {
- // TODO: These instances of elem either need to be declared as global
- // (which would not be good for conflicts) or declare/use this.elem
- var bbox = elem.getBBox();
- var cx = bbox.x + bbox.width/2,
- cy = bbox.y + bbox.height/2;
- var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
- if (rotate != elem.getAttribute("transform")) {
- elem.setAttribute("transform", rotate);
- }
- }
- }
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
- }
- return true;
- };
- // Function: svgedit.history.ChangeElementCommand.unapply
- // Reverses the stored change action
- svgedit.history.ChangeElementCommand.prototype.unapply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
- }
- var bChangedTransform = false;
- var attr;
- for (attr in this.oldValues ) {
- if (this.oldValues[attr]) {
- if (attr == "#text") {this.elem.textContent = this.oldValues[attr];}
- else if (attr == "#href") {svgedit.utilities.setHref(this.elem, this.oldValues[attr]);}
- else {
- this.elem.setAttribute(attr, this.oldValues[attr]);
- }
- }
- else {
- if (attr == "#text") {
- this.elem.textContent = "";
- }
- else {this.elem.removeAttribute(attr);}
- }
- if (attr == "transform") { bChangedTransform = true; }
- }
- // relocate rotational transform, if necessary
- if (!bChangedTransform) {
- var angle = svgedit.utilities.getRotationAngle(this.elem);
- if (angle) {
- var bbox = elem.getBBox();
- var cx = bbox.x + bbox.width/2,
- cy = bbox.y + bbox.height/2;
- var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
- if (rotate != elem.getAttribute("transform")) {
- elem.setAttribute("transform", rotate);
- }
- }
- }
- // Remove transformlist to prevent confusion that causes bugs like 575.
- svgedit.transformlist.removeElementFromListMap(this.elem);
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
- }
- return true;
- };
- // Function: ChangeElementCommand.elements
- // Returns array with element associated with this command
- svgedit.history.ChangeElementCommand.prototype.elements = function() {
- return [this.elem];
- };
- // TODO: create a 'typing' command object that tracks changes in text
- // if a new Typing command is created and the top command on the stack is also a Typing
- // and they both affect the same element, then collapse the two commands into one
- // Class: svgedit.history.BatchCommand
- // implements svgedit.history.HistoryCommand
- // History command that can contain/execute multiple other commands
- //
- // Parameters:
- // text - An optional string visible to user related to this change
- svgedit.history.BatchCommand = function(text) {
- this.text = text || "Batch Command";
- this.stack = [];
- };
- svgedit.history.BatchCommand.type = function() { return 'svgedit.history.BatchCommand'; };
- svgedit.history.BatchCommand.prototype.type = svgedit.history.BatchCommand.type;
- // Function: svgedit.history.BatchCommand.getText
- svgedit.history.BatchCommand.prototype.getText = function() {
- return this.text;
- };
- // Function: svgedit.history.BatchCommand.apply
- // Runs "apply" on all subcommands
- svgedit.history.BatchCommand.prototype.apply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
- }
- var i,
- len = this.stack.length;
- for (i = 0; i < len; ++i) {
- this.stack[i].apply(handler);
- }
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
- }
- };
- // Function: svgedit.history.BatchCommand.unapply
- // Runs "unapply" on all subcommands
- svgedit.history.BatchCommand.prototype.unapply = function(handler) {
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
- }
- var i;
- for (i = this.stack.length-1; i >= 0; i--) {
- this.stack[i].unapply(handler);
- }
- if (handler) {
- handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
- }
- };
- // Function: svgedit.history.BatchCommand.elements
- // Iterate through all our subcommands and returns all the elements we are changing
- svgedit.history.BatchCommand.prototype.elements = function() {
- var elems = [];
- var cmd = this.stack.length;
- while (cmd--) {
- var thisElems = this.stack[cmd].elements();
- var elem = thisElems.length;
- while (elem--) {
- if (elems.indexOf(thisElems[elem]) == -1) {elems.push(thisElems[elem]);}
- }
- }
- return elems;
- };
- // Function: svgedit.history.BatchCommand.addSubCommand
- // Adds a given command to the history stack
- //
- // Parameters:
- // cmd - The undo command object to add
- svgedit.history.BatchCommand.prototype.addSubCommand = function(cmd) {
- this.stack.push(cmd);
- };
- // Function: svgedit.history.BatchCommand.isEmpty
- // Returns a boolean indicating whether or not the batch command is empty
- svgedit.history.BatchCommand.prototype.isEmpty = function() {
- return this.stack.length === 0;
- };
- // Class: svgedit.history.UndoManager
- // Parameters:
- // historyEventHandler - an object that conforms to the HistoryEventHandler interface
- // (see above)
- svgedit.history.UndoManager = function(historyEventHandler) {
- this.handler_ = historyEventHandler || null;
- this.undoStackPointer = 0;
- this.undoStack = [];
- // this is the stack that stores the original values, the elements and
- // the attribute name for begin/finish
- this.undoChangeStackPointer = -1;
- this.undoableChangeStack = [];
- };
- // Function: svgedit.history.UndoManager.resetUndoStack
- // Resets the undo stack, effectively clearing the undo/redo history
- svgedit.history.UndoManager.prototype.resetUndoStack = function() {
- this.undoStack = [];
- this.undoStackPointer = 0;
- };
- // Function: svgedit.history.UndoManager.getUndoStackSize
- // Returns:
- // Integer with the current size of the undo history stack
- svgedit.history.UndoManager.prototype.getUndoStackSize = function() {
- return this.undoStackPointer;
- };
- // Function: svgedit.history.UndoManager.getRedoStackSize
- // Returns:
- // Integer with the current size of the redo history stack
- svgedit.history.UndoManager.prototype.getRedoStackSize = function() {
- return this.undoStack.length - this.undoStackPointer;
- };
- // Function: svgedit.history.UndoManager.getNextUndoCommandText
- // Returns:
- // String associated with the next undo command
- svgedit.history.UndoManager.prototype.getNextUndoCommandText = function() {
- return this.undoStackPointer > 0 ? this.undoStack[this.undoStackPointer-1].getText() : "";
- };
- // Function: svgedit.history.UndoManager.getNextRedoCommandText
- // Returns:
- // String associated with the next redo command
- svgedit.history.UndoManager.prototype.getNextRedoCommandText = function() {
- return this.undoStackPointer < this.undoStack.length ? this.undoStack[this.undoStackPointer].getText() : "";
- };
- // Function: svgedit.history.UndoManager.undo
- // Performs an undo step
- svgedit.history.UndoManager.prototype.undo = function() {
- if (this.undoStackPointer > 0) {
- var cmd = this.undoStack[--this.undoStackPointer];
- cmd.unapply(this.handler_);
- }
- };
- // Function: svgedit.history.UndoManager.redo
- // Performs a redo step
- svgedit.history.UndoManager.prototype.redo = function() {
- if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
- var cmd = this.undoStack[this.undoStackPointer++];
- cmd.apply(this.handler_);
- }
- };
- // Function: svgedit.history.UndoManager.addCommandToHistory
- // Adds a command object to the undo history stack
- //
- // Parameters:
- // cmd - The command object to add
- svgedit.history.UndoManager.prototype.addCommandToHistory = function(cmd) {
- // FIXME: we MUST compress consecutive text changes to the same element
- // (right now each keystroke is saved as a separate command that includes the
- // entire text contents of the text element)
- // TODO: consider limiting the history that we store here (need to do some slicing)
- // if our stack pointer is not at the end, then we have to remove
- // all commands after the pointer and insert the new command
- if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
- this.undoStack = this.undoStack.splice(0, this.undoStackPointer);
- }
- this.undoStack.push(cmd);
- this.undoStackPointer = this.undoStack.length;
- };
- // Function: svgedit.history.UndoManager.beginUndoableChange
- // This function tells the canvas to remember the old values of the
- // attrName attribute for each element sent in. The elements and values
- // are stored on a stack, so the next call to finishUndoableChange() will
- // pop the elements and old values off the stack, gets the current values
- // from the DOM and uses all of these to construct the undo-able command.
- //
- // Parameters:
- // attrName - The name of the attribute being changed
- // elems - Array of DOM elements being changed
- svgedit.history.UndoManager.prototype.beginUndoableChange = function(attrName, elems) {
- var p = ++this.undoChangeStackPointer;
- var i = elems.length;
- var oldValues = new Array(i), elements = new Array(i);
- while (i--) {
- var elem = elems[i];
- if (elem == null) {continue;}
- elements[i] = elem;
- oldValues[i] = elem.getAttribute(attrName);
- }
- this.undoableChangeStack[p] = {
- 'attrName': attrName,
- 'oldValues': oldValues,
- 'elements': elements
- };
- };
- // Function: svgedit.history.UndoManager.finishUndoableChange
- // This function returns a BatchCommand object which summarizes the
- // change since beginUndoableChange was called. The command can then
- // be added to the command history
- //
- // Returns:
- // Batch command object with resulting changes
- svgedit.history.UndoManager.prototype.finishUndoableChange = function() {
- var p = this.undoChangeStackPointer--;
- var changeset = this.undoableChangeStack[p];
- var i = changeset.elements.length;
- var attrName = changeset.attrName;
- var batchCmd = new svgedit.history.BatchCommand("Change " + attrName);
- while (i--) {
- var elem = changeset.elements[i];
- if (elem == null) {continue;}
- var changes = {};
- changes[attrName] = changeset.oldValues[i];
- if (changes[attrName] != elem.getAttribute(attrName)) {
- batchCmd.addSubCommand(new svgedit.history.ChangeElementCommand(elem, changes, attrName));
- }
- }
- this.undoableChangeStack[p] = null;
- return batchCmd;
- };
- }());
|