From 4a133d490dfdd1a3c8a0e5f8ca9ae9bd21b5e36b Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Fri, 12 Nov 2010 19:08:29 +0000 Subject: [PATCH] Move undo/redo functionality into history.js. Started unit test file. git-svn-id: http://svg-edit.googlecode.com/svn/trunk@1864 eee81c28-f429-11dd-99c0-75d572ba1ddd --- Makefile | 1 + editor/embedapi.js | 2 +- editor/history.js | 497 +++++++++++++++++++++++++++++++++++ editor/svg-editor.html | 1 + editor/svgcanvas.js | 559 ++++++---------------------------------- editor/svgutils.js | 3 +- test/all_tests.html | 3 +- test/history_test.html | 51 ++++ test/svgutils_test.html | 17 +- test/test1.html | 10 +- 10 files changed, 651 insertions(+), 493 deletions(-) create mode 100644 editor/history.js create mode 100644 test/history_test.html diff --git a/Makefile b/Makefile index 57a668b2..e16c6faf 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ build/$(PACKAGE): --js units.js \ --js svgutils.js \ --js sanitize.js \ + --js history.js \ --js svgcanvas.js \ --js svg-editor.js \ --js locale/locale.js \ diff --git a/editor/embedapi.js b/editor/embedapi.js index 72b53817..40dc92cc 100644 --- a/editor/embedapi.js +++ b/editor/embedapi.js @@ -72,7 +72,7 @@ function embedded_svg_edit(frame){ //Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API //var l=[];for(var i in svgCanvas){if(typeof svgCanvas[i] == "function"){l.push(i)}}; //run in svgedit itself - var functions = ["updateElementFromJson", "embedImage", "fixOperaXML", "clearSelection", "addToSelection", "removeFromSelection", "addNodeToSelection", "open", "save", "getSvgString", "setSvgString", "createLayer", "deleteCurrentLayer", "getNumLayers", "getLayer", "getCurrentLayer", "setCurrentLayer", "renameCurrentLayer", "setCurrentLayerPosition", "getLayerVisibility", "setLayerVisibility", "moveSelectedToLayer", "getLayerOpacity", "setLayerOpacity", "clear", "clearPath", "getNodePoint", "clonePathNode", "deletePathNode", "getResolution", "getImageTitle", "setImageTitle", "setResolution", "setBBoxZoom", "setZoom", "getMode", "setMode", "getStrokeColor", "setStrokeColor", "getFillColor", "setFillColor", "setStrokePaint", "setFillPaint", "getStrokeWidth", "setStrokeWidth", "getStrokeStyle", "setStrokeStyle", "getOpacity", "setOpacity", "getFillOpacity", "setFillOpacity", "getStrokeOpacity", "setStrokeOpacity", "getTransformList", "getBBox", "getRotationAngle", "setRotationAngle", "each", "bind", "setIdPrefix", "getBold", "setBold", "getItalic", "setItalic", "getFontFamily", "setFontFamily", "getFontSize", "setFontSize", "getText", "setTextContent", "setImageURL", "setRectRadius", "setSegType", "quickClone", "beginUndoableChange", "changeSelectedAttributeNoUndo", "finishUndoableChange", "changeSelectedAttribute", "deleteSelectedElements", "groupSelectedElements", "ungroupSelectedElement", "moveToTopSelectedElement", "moveToBottomSelectedElement", "moveSelectedElements", "getStrokedBBox", "getVisibleElements", "cycleElement", "getUndoStackSize", "getRedoStackSize", "getNextUndoCommandText", "getNextRedoCommandText", "undo", "redo", "cloneSelectedElements", "alignSelectedElements", "getZoom", "getVersion", "setIconSize", "setLang", "setCustomHandlers"] + var functions = ["updateElementFromJson", "embedImage", "fixOperaXML", "clearSelection", "addToSelection", "removeFromSelection", "addNodeToSelection", "open", "save", "getSvgString", "setSvgString", "createLayer", "deleteCurrentLayer", "getNumLayers", "getLayer", "getCurrentLayer", "setCurrentLayer", "renameCurrentLayer", "setCurrentLayerPosition", "getLayerVisibility", "setLayerVisibility", "moveSelectedToLayer", "getLayerOpacity", "setLayerOpacity", "clear", "clearPath", "getNodePoint", "clonePathNode", "deletePathNode", "getResolution", "getImageTitle", "setImageTitle", "setResolution", "setBBoxZoom", "setZoom", "getMode", "setMode", "getStrokeColor", "setStrokeColor", "getFillColor", "setFillColor", "setStrokePaint", "setFillPaint", "getStrokeWidth", "setStrokeWidth", "getStrokeStyle", "setStrokeStyle", "getOpacity", "setOpacity", "getFillOpacity", "setFillOpacity", "getStrokeOpacity", "setStrokeOpacity", "getTransformList", "getBBox", "getRotationAngle", "setRotationAngle", "each", "bind", "setIdPrefix", "getBold", "setBold", "getItalic", "setItalic", "getFontFamily", "setFontFamily", "getFontSize", "setFontSize", "getText", "setTextContent", "setImageURL", "setRectRadius", "setSegType", "quickClone", "changeSelectedAttributeNoUndo", "changeSelectedAttribute", "deleteSelectedElements", "groupSelectedElements", "ungroupSelectedElement", "moveToTopSelectedElement", "moveToBottomSelectedElement", "moveSelectedElements", "getStrokedBBox", "getVisibleElements", "cycleElement", "getUndoStackSize", "getRedoStackSize", "getNextUndoCommandText", "getNextRedoCommandText", "undo", "redo", "cloneSelectedElements", "alignSelectedElements", "getZoom", "getVersion", "setIconSize", "setLang", "setCustomHandlers"] //TODO: rewrite the following, it's pretty scary. for(var i = 0; i < functions.length; i++){ diff --git a/editor/history.js b/editor/history.js new file mode 100644 index 00000000..cd48d8d3 --- /dev/null +++ b/editor/history.js @@ -0,0 +1,497 @@ +/** + * Package: svedit.history + * + * Licensed under the Apache License, Version 2 + * + * Copyright(c) 2010 Jeff Schiller + */ + +// Dependencies: +// 1) jQuery +// 2) svgtransformlist.js +// 3) svgutils.js + +(function() { + +if (!window.svgedit) { + window.svgedit = {}; +} + +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 = {}; + +/** + * Interface: svgedit.history.HistoryCommand + * An interface that all command objects must implement. + * + * interface svgedit.history.HistoryCommand { + * void apply(); + * void unapply(); + * Element[] elements(); + * static type(); + * } + */ + +// Class: svgedit.history.MoveElementCommand +// implements svgedit.history.HistoryCommand +// History command for an element that had its DOM position changed +// +// Parameters: +// elem - The DOM element that was moved +// oldNextSibling - The element's next sibling before it was moved +// oldParent - The element's parent before it was moved +// opt_desc - An optional string visible to user related to this change +svgedit.history.MoveElementCommand = function(elem, oldNextSibling, oldParent, opt_desc) { + 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; + +// Function: svgedit.history.MoveElementCommand.apply +// Re-positions the element +svgedit.history.MoveElementCommand.prototype.apply = function() { + this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling); +}; + +// Function: svgedit.history.MoveElementCommand.unapply +// Positions the element back to its original location +svgedit.history.MoveElementCommand.prototype.unapply = function() { + this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling); +}; + +// 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; +}; +svgedit.history.InsertElementCommand.type = function() { return 'svgedit.history.InsertElementCommand'; } +svgedit.history.InsertElementCommand.prototype.type = svgedit.history.InsertElementCommand.type; + +// Function: svgedit.history.InsertElementCommand.apply +// Re-Inserts the new element +svgedit.history.InsertElementCommand.prototype.apply = function() { + this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); +}; + +// Function: svgedit.history.InsertElementCommand.unapply +// Removes the element +svgedit.history.InsertElementCommand.prototype.unapply = function() { + this.parent = this.elem.parentNode; + this.elem = this.elem.parentNode.removeChild(this.elem); +}; + +// 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 +// parent - The DOM element's parent +// text - An optional string visible to user related to this change +svgedit.history.RemoveElementCommand = function(elem, parent, text) { + this.elem = elem; + this.text = text || ("Delete " + elem.tagName); + this.parent = parent; + + // 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: RemoveElementCommand.apply +// Re-removes the new element +svgedit.history.RemoveElementCommand.prototype.apply = function() { + svgedit.transformlist.removeElementFromListMap(this.elem); + + this.parent = this.elem.parentNode; + this.elem = this.parent.removeChild(this.elem); +}; + +// Function: RemoveElementCommand.unapply +// Re-adds the new element +svgedit.history.RemoveElementCommand.prototype.unapply = function() { + svgedit.transformlist.removeElementFromListMap(this.elem); + + this.parent.insertBefore(this.elem, this.elem.nextSibling); +}; + +// 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; + for (var 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.apply +// Performs the stored change action +svgedit.history.ChangeElementCommand.prototype.apply = function() { + var bChangedTransform = false; + for(var 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) { + 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); + } + } + } + return true; +}; + +// Function: svgedit.history.ChangeElementCommand.unapply +// Reverses the stored change action +svgedit.history.ChangeElementCommand.prototype.unapply = function() { + var bChangedTransform = false; + for(var 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); + + 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.apply +// Runs "apply" on all subcommands +svgedit.history.BatchCommand.prototype.apply = function() { + var len = this.stack.length; + for (var i = 0; i < len; ++i) { + this.stack[i].apply(); + } +}; + +// Function: svgedit.history.BatchCommand.unapply +// Runs "unapply" on all subcommands +svgedit.history.BatchCommand.prototype.unapply = function() { + for (var i = this.stack.length-1; i >= 0; i--) { + this.stack[i].unapply(); + } +}; + +// 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; +}; + + +/** + * 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 (see above). + * command is an object fulfilling the HistoryCommand interface (see above). + */ + +// Class: svgedit.history.UndoManager +// Parameters: +// historyEventHandler - an object that conforms to the HistoryEventHandler interface +// (see above) +svgedit.history.UndoManager = function(historyEventHandler) { + this.handler_ = historyEventHandler; + 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].text : ""; +}; + +// 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].text : ""; +}; + +// 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]; + + this.handler_.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, cmd); + + cmd.unapply(); + + this.handler_.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, cmd); + } +}; + +// 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++]; + + this.handler_.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, cmd); + + cmd.apply(); + + this.handler_.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, cmd); + } +}; + +// 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; +}; + + +})(); \ No newline at end of file diff --git a/editor/svg-editor.html b/editor/svg-editor.html index 5fbd0ff7..7ea1294d 100644 --- a/editor/svg-editor.html +++ b/editor/svg-editor.html @@ -24,6 +24,7 @@ + diff --git a/editor/svgcanvas.js b/editor/svgcanvas.js index bea47484..22180f12 100644 --- a/editor/svgcanvas.js +++ b/editor/svgcanvas.js @@ -17,6 +17,7 @@ // 5) units.js // 6) svgutils.js // 7) sanitize.js +// 8) history.js if(!window.console) { window.console = {}; @@ -25,7 +26,7 @@ if(!window.console) { } if(window.opera) { - window.console.log = function(str) {opera.postError(str);}; + window.console.log = function(str) { opera.postError(str); }; window.console.dir = function(str) {}; } @@ -118,6 +119,8 @@ if(config) { $.extend(curConfig, config); } +var canvas = this; + // "document" element associated with the container (same as window.document using default svg-editor.js) var svgdoc = container.ownerDocument; @@ -152,13 +155,13 @@ var getElem = function(id) { }; // import svgtransformlist.js -var getTransformList = this.getTransformList = svgedit.transformlist.getTransformList; +var getTransformList = canvas.getTransformList = svgedit.transformlist.getTransformList; // import from math.js. var transformPoint = svgedit.math.transformPoint; -var matrixMultiply = this.matrixMultiply = svgedit.math.matrixMultiply; -var hasMatrixTransform = this.hasMatrixTransform = svgedit.math.hasMatrixTransform; -var transformListToTransform = this.transformListToTransform = svgedit.math.transformListToTransform; +var matrixMultiply = canvas.matrixMultiply = svgedit.math.matrixMultiply; +var hasMatrixTransform = canvas.hasMatrixTransform = svgedit.math.hasMatrixTransform; +var transformListToTransform = canvas.transformListToTransform = svgedit.math.transformListToTransform; var snapToAngle = svgedit.math.snapToAngle; // initialize from units.js @@ -170,19 +173,73 @@ svgedit.units.init({ getWidth: function() { return svgcontent.getAttribute("width")/current_zoom; } }); // import from units.js -var convertToNum = this.convertToNum = svgedit.units.convertToNum; +var convertToNum = canvas.convertToNum = svgedit.units.convertToNum; // import from svgutils.js -var getUrlFromAttr = this.getUrlFromAttr = svgedit.utilities.getUrlFromAttr; -var getHref = this.getHref = svgedit.utilities.getHref; -var setHref = this.setHref = svgedit.utilities.setHref; +var getUrlFromAttr = canvas.getUrlFromAttr = svgedit.utilities.getUrlFromAttr; +var getHref = canvas.getHref = svgedit.utilities.getHref; +var setHref = canvas.setHref = svgedit.utilities.setHref; var getPathBBox = svgedit.utilities.getPathBBox; -var getBBox = this.getBBox = svgedit.utilities.getBBox; -var getRotationAngle = this.getRotationAngle = svgedit.utilities.getRotationAngle; +var getBBox = canvas.getBBox = svgedit.utilities.getBBox; +var getRotationAngle = canvas.getRotationAngle = svgedit.utilities.getRotationAngle; // import from sanitize.js var nsMap = svgedit.sanitize.getNSMap(); -var sanitizeSvg = this.sanitizeSvg = svgedit.sanitize.sanitizeSvg; +var sanitizeSvg = canvas.sanitizeSvg = svgedit.sanitize.sanitizeSvg; + +// import from history.js +var MoveElementCommand = svgedit.history.MoveElementCommand; +var InsertElementCommand = svgedit.history.InsertElementCommand; +var RemoveElementCommand = svgedit.history.RemoveElementCommand; +var ChangeElementCommand = svgedit.history.ChangeElementCommand; +var BatchCommand = svgedit.history.BatchCommand; +// Implement the svgedit.history.HistoryEventHandler interface. +canvas.undoMgr = new svgedit.history.UndoManager({ + handleHistoryEvent: function(eventType, cmd) { + var EventTypes = svgedit.history.HistoryEventTypes; + // TODO: handle setBlurOffsets. + if (eventType == EventTypes.BEFORE_UNAPPLY || eventType == EventTypes.BEFORE_APPLY) { + canvas.clearSelection(); + } else if (eventType == EventTypes.AFTER_APPLY || eventType == EventTypes.AFTER_UNAPPLY) { + var elems = cmd.elements(); + canvas.pathActions.clear(); + call("changed", elems); + + var cmdType = cmd.type(); + var isApply = eventType == EventTypes.AFTER_APPLY; + if (cmdType == MoveElementCommand.type()) { + var parent = isApply ? cmd.newParent : cmd.oldParent; + if (parent == svgcontent) { + canvas.identifyLayers(); + } + } else if (cmdType == InsertElementCommand.type() || + cmdType == RemoveElementCommand.type()) { + if (cmd.parent == svgcontent) { + canvas.identifyLayers(); + } + if (cmdType == InsertElementCommand.type()) { + if (isApply) restoreRefElems(cmd.elem); + } else { + if (!isApply) restoreRefElems(cmd.elem); + } + } else if (cmdType == ChangeElementCommand.type()) { + // if we are changing layer names, re-identify all layers + if (cmd.elem.tagName == "title" && cmd.elem.parentNode.parentNode == svgcontent) { + canvas.identifyLayers(); + } + var values = isApply ? cmd.newValues : cmd.oldValues; + // If stdDeviation was changed, update the blur. + if (values["stdDeviation"]) { + canvas.setBlurOffsets(cmd.elem.parentNode, values["stdDeviation"]); + } + } + } + } +}); +var addCommandToHistory = function(cmd) { + canvas.undoMgr.addCommandToHistory(cmd); +}; + // Function: snapToGrid // round value to for snapping @@ -202,13 +259,9 @@ var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polylin var ref_attrs = ["clip-path", "fill", "filter", "marker-end", "marker-mid", "marker-start", "mask", "stroke"]; var elData = $.data; - -// TODO: declare the variables and set them as null, then move this setup stuff to -// an initialization function - probably just use clear() -var canvas = this, - - // nonce to uniquify id's - nonce = Math.floor(Math.random()*100001), + +// nonce to uniquify id's +var nonce = Math.floor(Math.random()*100001), // Boolean to indicate whether or not IDs given to elements should be random randomize_ids = false, @@ -272,453 +325,7 @@ var restoreRefElems = function(elem) { } } } -} - - -// Group: Undo/Redo history management - -this.undoCmd = {}; - -// Function: ChangeElementCommand -// 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 -var ChangeElementCommand = this.undoCmd.changeElement = function(elem, attrs, text) { - this.elem = elem; - this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName); - this.newValues = {}; - this.oldValues = attrs; - for (var 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); - } - - // Function: ChangeElementCommand.apply - // Performs the stored change action - this.apply = function() { - var bChangedTransform = false; - for(var 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; } - else if (attr == "stdDeviation") { canvas.setBlurOffsets(this.elem.parentNode, this.newValues[attr]); } - - } - // relocate rotational transform, if necessary - if(!bChangedTransform) { - var angle = svgedit.utilities.getRotationAngle(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); - } - } - } - // if we are changing layer names, re-identify all layers - if (this.elem.tagName == "title" && this.elem.parentNode.parentNode == svgcontent) { - identifyLayers(); - } - return true; - }; - - // Function: ChangeElementCommand.unapply - // Reverses the stored change action - this.unapply = function() { - var bChangedTransform = false; - for(var 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]); - - if (attr == "stdDeviation") canvas.setBlurOffsets(this.elem.parentNode, 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(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); - } - } - } - // if we are changing layer names, re-identify all layers - if (this.elem.tagName == "title" && this.elem.parentNode.parentNode == svgcontent) { - identifyLayers(); - } - - // Remove transformlist to prevent confusion that causes bugs like 575. - svgedit.transformlist.removeElementFromListMap(this.elem); - - return true; - }; - - // Function: ChangeElementCommand.elements - // Returns array with element associated with this command - this.elements = function() { return [this.elem]; } -} - -// Function: InsertElementCommand -// 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 -var InsertElementCommand = this.undoCmd.insertElement = function(elem, text) { - this.elem = elem; - this.text = text || ("Create " + elem.tagName); - this.parent = elem.parentNode; - - // Function: InsertElementCommand.apply - // Re-Inserts the new element - this.apply = function() { - this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); - - restoreRefElems(this.elem); - - if (this.parent == svgcontent) { - identifyLayers(); - } - }; - - // Function: InsertElementCommand.unapply - // Removes the element - this.unapply = function() { - this.parent = this.elem.parentNode; - this.elem = this.elem.parentNode.removeChild(this.elem); - if (this.parent == svgcontent) { - identifyLayers(); - } - }; - - // Function: InsertElementCommand.elements - // Returns array with element associated with this command - this.elements = function() { return [this.elem]; }; -} - -// Function: RemoveElementCommand -// History command for an element removed from the DOM -// -// Parameters: -// elem - The removed DOM element -// parent - The DOM element's parent -// text - An optional string visible to user related to this change -var RemoveElementCommand = this.undoCmd.removeElement = function(elem, parent, text) { - this.elem = elem; - this.text = text || ("Delete " + elem.tagName); - this.parent = parent; - - // Function: RemoveElementCommand.apply - // Re-removes the new element - this.apply = function() { - svgedit.transformlist.removeElementFromListMap(this.elem); - - this.parent = this.elem.parentNode; - this.elem = this.parent.removeChild(this.elem); - if (this.parent == svgcontent) { - identifyLayers(); - } - }; - - // Function: RemoveElementCommand.unapply - // Re-adds the new element - this.unapply = function() { - svgedit.transformlist.removeElementFromListMap(this.elem); - - this.parent.insertBefore(this.elem, this.elem.nextSibling); - - restoreRefElems(this.elem); - - if (this.parent === svgcontent) { - identifyLayers(); - } - }; - - // Function: RemoveElementCommand.elements - // Returns array with element associated with this command - this.elements = function() { return [this.elem]; }; - - // special hack for webkit: remove this element's entry in the svgTransformLists map - svgedit.transformlist.removeElementFromListMap(elem); -} - -// Function: MoveElementCommand -// History command for an element that had its DOM position changed -// -// Parameters: -// elem - The DOM element that was moved -// oldNextSibling - The element's next sibling before it was moved -// oldParent - The element's parent before it was moved -// text - An optional string visible to user related to this change -var MoveElementCommand = this.undoCmd.moveElement = 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; - - // Function: MoveElementCommand.unapply - // Re-positions the element - this.apply = function() { - this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling); - if (this.newParent == svgcontent) { - identifyLayers(); - } - }; - - // Function: MoveElementCommand.unapply - // Positions the element back to its original location - this.unapply = function() { - this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling); - if (this.oldParent == svgcontent) { - identifyLayers(); - } - }; - - // Function: MoveElementCommand.elements - // Returns array with element associated with this command - this.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 - -// Function: BatchCommand -// History command that can contain/execute multiple other commands -// -// Parameters: -// text - An optional string visible to user related to this change -var BatchCommand = this.undoCmd.batch = function(text) { - this.text = text || "Batch Command"; - this.stack = []; - - // Function: BatchCommand.apply - // Runs "apply" on all subcommands - this.apply = function() { - var len = this.stack.length; - for (var i = 0; i < len; ++i) { - this.stack[i].apply(); - } - }; - - // Function: BatchCommand.unapply - // Runs "unapply" on all subcommands - this.unapply = function() { - for (var i = this.stack.length-1; i >= 0; i--) { - this.stack[i].unapply(); - } - }; - - // Function: BatchCommand.elements - // Iterate through all our subcommands and returns all the elements we are changing - this.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: BatchCommand.addSubCommand - // Adds a given command to the history stack - // - // Parameters: - // cmd - The undo command object to add - this.addSubCommand = function(cmd) { this.stack.push(cmd); }; - - // Function: BatchCommand.isEmpty - // Returns a boolean indicating whether or not the batch command is empty - this.isEmpty = function() { return this.stack.length == 0; }; -} - -// Set scope for addCommandToHistory -var addCommandToHistory; - -// Undo/redo stack related functions -(function(c) { - var undoStackPointer = 0, - undoStack = []; - - c.undoMgr = { - // Function: undoMgr.resetUndoStack - // Resets the undo stack, effectively clearing the undo/redo history - resetUndoStack: function() { - undoStack = []; - undoStackPointer = 0; - }, - - // Function: undoMgr.getUndoStackSize - // Returns: - // Integer with the current size of the undo history stack - getUndoStackSize: function() { return undoStackPointer; }, - - // Function: undoMgr.getRedoStackSize - // Returns: - // Integer with the current size of the redo history stack - getRedoStackSize: function() { return undoStack.length - undoStackPointer; }, - - // Function: undoMgr.getNextUndoCommandText - // Returns: - // String associated with the next undo command - getNextUndoCommandText: function() { - if (undoStackPointer > 0) - return undoStack[undoStackPointer-1].text; - return ""; - }, - - // Function: undoMgr.getNextRedoCommandText - // Returns: - // String associated with the next redo command - getNextRedoCommandText: function() { - if (undoStackPointer < undoStack.length) - return undoStack[undoStackPointer].text; - return ""; - }, - - // Function: undoMgr.undo - // Performs an undo step - undo: function() { - if (undoStackPointer > 0) { - c.clearSelection(); - var cmd = undoStack[--undoStackPointer]; - cmd.unapply(); - pathActions.clear(); - call("changed", cmd.elements()); - } - }, - - // Function: undoMgr.redo - // Performs a redo step - redo: function() { - if (undoStackPointer < undoStack.length && undoStack.length > 0) { - c.clearSelection(); - var cmd = undoStack[undoStackPointer++]; - cmd.apply(); - pathActions.clear(); - call("changed", cmd.elements()); - } - } - }; - - // Function: addCommandToHistory - // Adds a command object to the undo history stack - // - // Parameters: - // cmd - The command object to add - addCommandToHistory = c.undoCmd.add = 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 (undoStackPointer < undoStack.length && undoStack.length > 0) { - undoStack = undoStack.splice(0, undoStackPointer); - } - undoStack.push(cmd); - undoStackPointer = undoStack.length; - }; - -}(canvas)); - -(function(c) { - - // New functions for refactoring of Undo/Redo - - // this is the stack that stores the original values, the elements and - // the attribute name for begin/finish - var undoChangeStackPointer = -1; - var undoableChangeStack = []; - - // Function: 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 - c.beginUndoableChange = function(attrName, elems) { - var p = ++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); - } - undoableChangeStack[p] = {'attrName': attrName, - 'oldValues': oldValues, - 'elements': elements}; - }; - - // Function: 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 - c.finishUndoableChange = function() { - var p = undoChangeStackPointer--; - var changeset = undoableChangeStack[p]; - var i = changeset['elements'].length; - var attrName = changeset['attrName']; - var batchCmd = new 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 ChangeElementCommand(elem, changes, attrName)); - } - } - undoableChangeStack[p] = null; - return batchCmd; - }; - -}(canvas)); +}; // Put SelectorManager in this scope var SelectorManager; @@ -1607,8 +1214,8 @@ var getStrokedBBox = this.getStrokedBBox = function(elems) { console.log(elem, e); return null; } + }; - } var full_bb; $.each(elems, function() { if(full_bb) return; @@ -1804,7 +1411,7 @@ var getId, getNextId, call; // arg - Argument to pass through to the callback function call = c.call = function(event, arg) { if (events[event]) { - return events[event](this,arg); + return events[event](this, arg); } }; @@ -3598,7 +3205,7 @@ var getMouseTarget = this.getMouseTarget = function(evt) { case "rotate": started = true; // we are starting an undoable change (a drag-rotation) - canvas.beginUndoableChange("transform", selectedElements); + canvas.undoMgr.beginUndoableChange("transform", selectedElements); break; default: // This could occur in an extension @@ -4224,7 +3831,7 @@ var getMouseTarget = this.getMouseTarget = function(evt) { keep = true; element = null; current_mode = "select"; - var batchCmd = canvas.finishUndoableChange(); + var batchCmd = canvas.undoMgr.finishUndoableChange(); if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); } @@ -4793,7 +4400,7 @@ var textActions = canvas.textActions = function() { // Group: Path edit functions // Functions relating to editing path elements -var pathActions = this.pathActions = function() { +var pathActions = canvas.pathActions = function() { var subpath = false; var pathData = {}; @@ -7683,7 +7290,7 @@ this.importSvgString = function(xmlString) { // Function: identifyLayers // Updates layer system -var identifyLayers = function() { +var identifyLayers = canvas.identifyLayers = function() { all_layers = []; leaveContext(); var numchildren = svgcontent.childNodes.length; @@ -8947,7 +8554,7 @@ this.getBlur = function(elem) { } function finishChange() { - var bCmd = canvas.finishUndoableChange(); + var bCmd = canvas.undoMgr.finishUndoableChange(); cur_command.addSubCommand(bCmd); addCommandToHistory(cur_command); cur_command = null; @@ -9043,7 +8650,7 @@ this.getBlur = function(elem) { } cur_command = batchCmd; - canvas.beginUndoableChange("stdDeviation", [filter?filter.firstChild:null]); + canvas.undoMgr.beginUndoableChange("stdDeviation", [filter?filter.firstChild:null]); if(complete) { canvas.setBlurNoUndo(val); finishChange(); @@ -9564,12 +9171,12 @@ var changeSelectedAttributeNoUndo = function(attr, newValue, elems) { // elems - The DOM elements to apply the change to var changeSelectedAttribute = this.changeSelectedAttribute = function(attr, val, elems) { var elems = elems || selectedElements; - canvas.beginUndoableChange(attr, elems); + canvas.undoMgr.beginUndoableChange(attr, elems); var i = elems.length; changeSelectedAttributeNoUndo(attr, val, elems); - var batchCmd = canvas.finishUndoableChange(); + var batchCmd = canvas.undoMgr.finishUndoableChange(); if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); } diff --git a/editor/svgutils.js b/editor/svgutils.js index ecb48ec4..c1351d11 100644 --- a/editor/svgutils.js +++ b/editor/svgutils.js @@ -175,8 +175,7 @@ svgedit.utilities.convertToXMLReferences = function(input) { var c = input.charCodeAt(n); if (c < 128) { output += input[n]; - } - else if(c > 127) { + } else if(c > 127) { output += ("&#" + c + ";"); } } diff --git a/test/all_tests.html b/test/all_tests.html index 97fa6b53..13f648f8 100644 --- a/test/all_tests.html +++ b/test/all_tests.html @@ -7,8 +7,9 @@

All SVG-edit Tests

This file frames all SVG-edit test pages. This should only include tests known to work. These tests are known to pass 100% in the following: Firefox 3.6, Chrome 7, IE9 Preview 6 (1.9.8006.6000), Opera 10.63. If a test is broken in this page, it is possible that YOU broke it. Please do not submit code that breaks any of these tests.

- + + + + + + + + + +

Unit Tests for history.js

+

+

+
    +
+ + diff --git a/test/svgutils_test.html b/test/svgutils_test.html index e4165228..925565a2 100644 --- a/test/svgutils_test.html +++ b/test/svgutils_test.html @@ -1,7 +1,7 @@ - - - + + + @@ -80,7 +80,6 @@ var convert = svgedit.utilities.convertToXMLReferences; equals(convert('ABC'), 'ABC'); // equals(convert('ÀBC'), 'ÀBC'); -// alert(convert('ÿBC', true)); }); test('Test svgedit.utilities.bboxToObj() function', function() { @@ -101,6 +100,16 @@ equals(obj.height, 4); }); + + test("Test getUrlFromAttr", function() { + expect(4); + + equal(svgedit.utilities.getUrlFromAttr("url(#foo)"), "#foo"); + equal(svgedit.utilities.getUrlFromAttr("url(somefile.svg#foo)"), "somefile.svg#foo"); + equal(svgedit.utilities.getUrlFromAttr("url('#foo')"), "#foo"); + equal(svgedit.utilities.getUrlFromAttr('url("#foo")'), "#foo"); + }); + }); diff --git a/test/test1.html b/test/test1.html index 45c1ff59..a82e58dc 100644 --- a/test/test1.html +++ b/test/test1.html @@ -11,6 +11,7 @@ +