From eb575ef68d2adb5bca6adbd06578e6f23864615a Mon Sep 17 00:00:00 2001 From: Alexis Deveria Date: Fri, 16 Jul 2010 15:46:54 +0000 Subject: [PATCH] Added proper support for child SVG elements and made image library import SVGs, taking care of more of issue 71. Also fixed Opera 10.60 layer bug git-svn-id: http://svg-edit.googlecode.com/svn/trunk@1633 eee81c28-f429-11dd-99c0-75d572ba1ddd --- editor/extensions/ext-imagelib.js | 8 +- editor/svg-editor.js | 36 ++- editor/svgcanvas.js | 413 ++++++++++++++++++++---------- 3 files changed, 299 insertions(+), 158 deletions(-) diff --git a/editor/extensions/ext-imagelib.js b/editor/extensions/ext-imagelib.js index 074eca5f..c100daa8 100644 --- a/editor/extensions/ext-imagelib.js +++ b/editor/extensions/ext-imagelib.js @@ -18,7 +18,7 @@ svgEditor.addExtension("imagelib", function() { name: 'Demo library (external)', url: 'http://a.deveria.com/tests/clip-art/', description: 'Demonstration library for SVG-edit on another domain' - }, + } ]; @@ -39,11 +39,13 @@ svgEditor.addExtension("imagelib", function() { switch (char1) { case '<': - svgEditor.loadFromString(response); + svgCanvas.importSvgString(response); break; case 'd': if(response.indexOf('data:') === 0) { - svgEditor.loadFromDataURI(response); + var pre = 'data:image/svg+xml;base64,'; + var src = response.substring(pre.length); + svgCanvas.importSvgString(svgCanvas.Utils.decode64(src)); break; } // Else fall through diff --git a/editor/svg-editor.js b/editor/svg-editor.js index 47ea2167..eee37c29 100644 --- a/editor/svg-editor.js +++ b/editor/svg-editor.js @@ -145,18 +145,20 @@ // - invoke a file chooser dialog in 'save' mode // - save the file to location chosen by the user Editor.setCustomHandlers = function(opts) { - if(opts.open) { - $('#tool_open').show(); - svgCanvas.open = opts.open; - } - if(opts.save) { - show_save_warning = false; - svgCanvas.bind("saved", opts.save); - } - if(opts.pngsave) { - svgCanvas.bind("exported", opts.pngsave); - } - customHandlers = opts; + Editor.ready(function() { + if(opts.open) { + $('#tool_open').show(); + svgCanvas.open = opts.open; + } + if(opts.save) { + show_save_warning = false; + svgCanvas.bind("saved", opts.save); + } + if(opts.pngsave) { + svgCanvas.bind("exported", opts.pngsave); + } + customHandlers = opts; + }); } Editor.randomizeIds = function() { @@ -182,9 +184,10 @@ svgEditor.setConfig(urldata); + // FIXME: This is null if Data URL ends with '='. var src = urldata.source; var qstr = $.param.querystring(); - + if(src) { if(src.indexOf("data:") === 0) { // plusses get replaced by spaces, so re-insert @@ -1340,6 +1343,10 @@ var el_name = elem.tagName; + if($(elem).data('gsvg')) { + el_name = 'svg'; + } + if(panels[el_name]) { var cur_panel = panels[el_name]; @@ -3561,7 +3568,7 @@ if(this.files.length==1) { var reader = new FileReader(); reader.onloadend = function(e) { - svgCanvas.importSvgString(e.target.result); + svgCanvas.importSvgString(e.target.result, true); updateCanvas(); }; reader.readAsText(this.files[0]); @@ -3815,6 +3822,7 @@ })(); + // ?iconsize=s&bkgd_color=555 // svgEditor.setConfig({ diff --git a/editor/svgcanvas.js b/editor/svgcanvas.js index 7c3f3cae..95e86777 100644 --- a/editor/svgcanvas.js +++ b/editor/svgcanvas.js @@ -1249,7 +1249,7 @@ var SelectorManager; offset += 2/current_zoom; } var bbox = getBBox(selected); - if(selected.tagName == 'g') { + if(selected.tagName == 'g' && !$(selected).data('gsvg')) { // The bbox for a group does not include stroke vals, so we // get the bbox based on its children. var stroked_bbox = getStrokedBBox(selected.childNodes); @@ -2223,6 +2223,18 @@ var getVisibleElements = this.getVisibleElements = function(parent, includeBBox) return contentElems.reverse(); } +// Function: groupSvgElem +// Wrap an SVG element into a group element, mark the group as 'gsvg' +// +// Parameters: +// elem - SVG element to wrap +var groupSvgElem = this.groupSvgElem = function(elem) { + var g = document.createElementNS(svgns, "g"); + elem.parentNode.replaceChild(g, elem); + $(g).append(elem).data('gsvg', elem)[0].id = getNextId(); +} + + // Function: copyElem // Create a clone of an element, updating its ID and its children's IDs when needed // @@ -2264,7 +2276,10 @@ var copyElem = function(el) { break; } }); - if(new_el.tagName == 'image') { + + if($(el).data('gsvg')) { + $(new_el).data('gsvg', new_el.firstChild); + } else if(new_el.tagName == 'image') { preventClickDefault(new_el); } return new_el; @@ -2388,12 +2403,13 @@ var sanitizeSvg = this.sanitizeSvg = function(node) { var parent = node.parentNode; // can parent ever be null here? I think the root node's parent is the document... if (!doc || !parent) return; - + var allowedAttrs = svgWhiteList[node.nodeName]; var allowedAttrsNS = svgWhiteListNS[node.nodeName]; // if this element is allowed if (allowedAttrs != undefined) { + var se_attrs = []; var i = node.attributes.length; @@ -2872,6 +2888,7 @@ var remapElement = this.remapElement = function(selected,changes,m) { changes["x"] = pt1.x; changes["y"] = pt1.y; break; + case "g": case "text": // if it was a translate, then just update x,y if (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 && @@ -2993,6 +3010,12 @@ var remapElement = this.remapElement = function(selected,changes,m) { case "text": assignAttributes(selected, changes, 1000, true); break; + case "g": + var gsvg = $(selected).data('gsvg'); + if(gsvg) { + assignAttributes(gsvg, changes, 1000, true); + } + break; case "polyline": case "polygon": var len = changes["points"].length; @@ -3119,6 +3142,9 @@ var recalculateDimensions = this.recalculateDimensions = function(selected) { return null; } + // Grouped SVG element + var gsvg = $(selected).data('gsvg'); + // we know we have some transforms, so set up return variable var batchCmd = new BatchCommand("Transform"); @@ -3170,6 +3196,12 @@ var recalculateDimensions = this.recalculateDimensions = function(selected) { $.each(changes, function(attr, val) { changes[attr] = convertToNum(attr, val); }); + } else if(gsvg) { + // GSVG exception + changes = { + x: $(gsvg).attr('x') || 0, + y: $(gsvg).attr('y') || 0 + }; } // if we haven't created an initial array in polygon/polyline/path, then @@ -3183,8 +3215,8 @@ var recalculateDimensions = this.recalculateDimensions = function(selected) { // save the start transform value too initial["transform"] = start_transform ? start_transform : ""; - // if it's a group, we have special processing to flatten transforms - if (selected.tagName == "g" || selected.tagName == "a") { + // if it's a regular group, we have special processing to flatten transforms + if ((selected.tagName == "g" && !gsvg) || selected.tagName == "a") { var box = getBBox(selected), oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2}, newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2, @@ -3770,6 +3802,10 @@ var matrixMultiply = this.matrixMultiply = function() { // Returns: // A single matrix transform object var transformListToTransform = this.transformListToTransform = function(tlist, min, max) { + if(tlist == null) { + // Or should tlist = null have been prevented before this? + return svgroot.createSVGTransformFromMatrix(svgroot.createSVGMatrix()); + } var min = min == undefined ? 0 : min; var max = max == undefined ? (tlist.numberOfItems-1) : max; min = parseInt(min); @@ -3901,6 +3937,7 @@ var addToSelection = this.addToSelection = function(elemsToAdd, showGrips) { if (elemsToAdd.length == 0) { return; } // find the first null in our selectedElements array var j = 0; + while (j < selectedElements.length) { if (selectedElements[j] == null) { break; @@ -4083,15 +4120,38 @@ var getMouseTarget = this.getMouseTarget = function(evt) { } } - // go up until we hit a child of a layer - while (mouse_target.parentNode.parentNode.tagName == "g") { + // Get the desired mouse_target with jQuery selector-fu + // If it's root-like, select the root + if($.inArray(mouse_target, [svgroot, container, svgcontent, current_layer]) !== -1) { + return svgroot; + } + + var $target = $(mouse_target); + + // If it's a selection grip, return the grip parent + if($target.closest('#selectorParentGroup').length) { + // While we could instead have just returned mouse_target, + // this makes it easier to indentify as being a selector grip + return selectorManager.selectorParentGroup; + } + + while (mouse_target.parentNode !== current_layer) { mouse_target = mouse_target.parentNode; } + + return mouse_target; + + +// +// // go up until we hit a child of a layer +// while (mouse_target.parentNode.parentNode.tagName == 'g') { +// mouse_target = mouse_target.parentNode; +// } // Webkit bubbles the mouse event all the way up to the div, so we // set the mouse_target to the svgroot like the other browsers - if (mouse_target.nodeName.toLowerCase() == "div") { - mouse_target = svgroot; - } +// if (mouse_target.nodeName.toLowerCase() == "div") { +// mouse_target = svgroot; +// } return mouse_target; }; @@ -4134,10 +4194,10 @@ var getMouseTarget = this.getMouseTarget = function(evt) { start_x = x; start_y = y; - + // if it is a selector grip, then it must be a single element selected, // set the mouse_target to that and update the mode to rotate/resize - if (mouse_target.parentNode == selectorManager.selectorParentGroup && selectedElements[0] != null) { + if (mouse_target == selectorManager.selectorParentGroup && selectedElements[0] != null) { var gripid = evt.target.id, griptype = gripid.substr(0,20); // rotating @@ -4581,19 +4641,20 @@ var getMouseTarget = this.getMouseTarget = function(evt) { tlist.replaceItem(translateOrigin, N-1); } var selectedBBox = selectedBBoxes[0]; - - // reset selected bbox top-left position - selectedBBox.x = left; - selectedBBox.y = top; - // if this is a translate, adjust the box position - if (tx) { - selectedBBox.x += dx; + if(selectedBBox) { + // reset selected bbox top-left position + selectedBBox.x = left; + selectedBBox.y = top; + + // if this is a translate, adjust the box position + if (tx) { + selectedBBox.x += dx; + } + if (ty) { + selectedBBox.y += dy; + } } - if (ty) { - selectedBBox.y += dy; - } - selectorManager.requestSelector(selected).resize(); break; case "zoom": @@ -7377,7 +7438,34 @@ var svgCanvasToString = this.svgCanvasToString = function() { } }); + var naked_svgs = []; + + // Unwrap gsvg if it has no special attributes (only id and style) + $(svgcontent).find('g:data(gsvg)').each(function() { + var attrs = this.attributes; + var len = attrs.length; + for(var i=0; i"); indent++; var bOneLine = false; + for (var i=0; i or element refers to an element internally? + if(href && + $.inArray(n.nodeName, ["filter", "linearGradient", "pattern", + "radialGradient", "textPath", "use"]) != -1) + { + var refid = href.substr(1); + if (!(refid in ids)) { + // add this id to our map + ids[refid] = {elem:null, attrs:[], hrefs:[]}; + } + ids[refid]["hrefs"].push(n); + } + } + }); + + // in ids, we now have a map of ids, elements and attributes, let's re-identify + for (var oldid in ids) { + var elem = ids[oldid]["elem"]; + if (elem) { + var newid = getNextId(); + // manually increment obj_num because our cloned elements are not in the DOM yet + obj_num++; + + // assign element its new id + elem.id = newid; + + // remap all url() attributes + var attrs = ids[oldid]["attrs"]; + var j = attrs.length; + while (j--) { + var attr = attrs[j]; + attr.ownerElement.setAttribute(attr.name, "url(#" + newid + ")"); + } + + // remap all href attributes + var hreffers = ids[oldid]["hrefs"]; + var k = hreffers.length; + while (k--) { + var hreffer = hreffers[k]; + hreffer.setAttributeNS(xlinkns, "xlink:href", "#"+newid); + } + } + } + + // manually increment obj_num because our cloned elements are not in the DOM yet + obj_num++; +} + // // Function: setSvgString // This function sets the current drawing as the input SVG XML. @@ -7698,6 +7875,20 @@ this.setSvgString = function(xmlString) { // Add to encodableImages if it loads canvas.embedImage(val); }); + + // Wrap child SVGs in group elements + $(svgcontent).find('svg').each(function() { + uniquifyElems(this); + + // Check if it already has a gsvg group + var pa = this.parentNode; + if(pa.children.length === 1 && pa.nodeName === 'g') { + $(pa).data('gsvg', this); + pa.id = pa.id || getNextId(); + } else { + groupSvgElem(this); + } + }); // convert gradients with userSpaceOnUse to objectBoundingBox $(svgcontent).find('linearGradient, radialGradient').each(function() { @@ -7825,6 +8016,8 @@ this.setSvgString = function(xmlString) { // // Parameters: // xmlString - The SVG as XML text. +// toElements - Boolean indicating whether or not to convert the SVG to a group +// with children // // Returns: // This function returns false if the import was unsuccessful, true otherwise. @@ -7836,7 +8029,7 @@ this.setSvgString = function(xmlString) { // was obtained // * import should happen in top-left of current zoomed viewport // * create a new layer for the imported SVG -this.importSvgString = function(xmlString) { +this.importSvgString = function(xmlString, toElements) { try { // convert string into XML document var newDoc = Utils.text2xml(xmlString); @@ -7848,129 +8041,61 @@ this.importSvgString = function(xmlString) { // import new svg document into our document var importedNode = svgdoc.importNode(newDoc.documentElement, true); - if (current_layer) { - // TODO: properly handle if width/height are not specified or if in percentages - // TODO: properly handle if width/height are in units (px, etc) - var innerw = importedNode.getAttribute("width"), - innerh = importedNode.getAttribute("height"), - innervb = importedNode.getAttribute("viewBox"), - // if no explicit viewbox, create one out of the width and height - vb = innervb ? innervb.split(" ") : [0,0,innerw,innerh]; - for (var j = 0; j < 4; ++j) - vb[j] = Number(vb[j]); + var innerw = convertToNum('width', importedNode.getAttribute("width")), + innerh = convertToNum('height', importedNode.getAttribute("height")), + innervb = importedNode.getAttribute("viewBox"), + // if no explicit viewbox, create one out of the width and height + vb = innervb ? innervb.split(" ") : [0,0,innerw,innerh]; + for (var j = 0; j < 4; ++j) + vb[j] = Number(vb[j]); - // TODO: properly handle preserveAspectRatio - var canvasw = Number(svgcontent.getAttribute("width")), - canvash = Number(svgcontent.getAttribute("height")); - // imported content should be 1/3 of the canvas on its largest dimension - if (innerh > innerw) { - var ts = "scale(" + (canvash/3)/vb[3] + ")"; - } - else { - var ts = "scale(" + (canvash/3)/vb[2] + ")"; - } + // TODO: properly handle preserveAspectRatio + var canvasw = Number(svgcontent.getAttribute("width")), + canvash = Number(svgcontent.getAttribute("height")); + // imported content should be 1/3 of the canvas on its largest dimension + + if (innerh > innerw) { + var ts = "scale(" + (canvash/3)/vb[3] + ")"; + } + else { + var ts = "scale(" + (canvash/3)/vb[2] + ")"; + } + + // Hack to make recalculateDimensions understand how to scale + ts = "translate(0) " + ts + " translate(0)"; + + if(!toElements) { + var elem = $(importedNode).appendTo(current_layer)[0]; + groupSvgElem(elem); + clearSelection(); - // Hack to make recalculateDimensions understand how to scale - ts = "translate(0) " + ts + " translate(0)"; + var g = elem.parentNode; - // TODO: Find way to add this in a recalculateDimensions-parsable way + g.setAttribute("transform", ts); + recalculateDimensions(g); + + addToSelection([g]); + + return; + } + + // TODO: Find way to add this in a recalculateDimensions-parsable way // if (vb[0] != 0 || vb[1] != 0) // ts = "translate(" + (-vb[0]) + "," + (-vb[1]) + ") " + ts; - // add all children of the imported to the we create - var g = svgdoc.createElementNS(svgns, "g"); - while (importedNode.hasChildNodes()) - g.appendChild(importedNode.firstChild); - if (ts) - g.setAttribute("transform", ts); - - // now ensure each element has a unique ID - var ids = {}; - walkTree(g, function(n) { - // if it's an element node - if (n.nodeType == 1) { - // and the element has an ID - if (n.id) { - // and we haven't tracked this ID yet - if (!(n.id in ids)) { - // add this id to our map - ids[n.id] = {elem:null, attrs:[], hrefs:[]}; - } - ids[n.id]["elem"] = n; - } - - // now search for all attributes on this element that might refer - // to other elements - $.each(["clip-path", "fill", "filter", "marker-end", "marker-mid", "marker-start", "mask", "stroke"],function(i,attr) { - var attrnode = n.getAttributeNode(attr); - if (attrnode) { - // the incoming file has been sanitized, so we should be able to safely just strip off the leading # - var url = getUrlFromAttr(attrnode.value), - refid = url ? url.substr(1) : null; - if (refid) { - if (!(refid in ids)) { - // add this id to our map - ids[refid] = {elem:null, attrs:[], hrefs:[]}; - } - ids[refid]["attrs"].push(attrnode); - } - } - }); - - // check xlink:href now - var href = n.getAttributeNS(xlinkns,"href"); - // TODO: what if an or element refers to an element internally? - if(href && - $.inArray(n.nodeName, ["filter", "linearGradient", "pattern", - "radialGradient", "textPath", "use"]) != -1) - { - var refid = href.substr(1); - if (!(refid in ids)) { - // add this id to our map - ids[refid] = {elem:null, attrs:[], hrefs:[]}; - } - ids[refid]["hrefs"].push(n); - } - } - }); - - // in ids, we now have a map of ids, elements and attributes, let's re-identify - for (var oldid in ids) { - var elem = ids[oldid]["elem"]; - if (elem) { - var newid = getNextId(); - // manually increment obj_num because our cloned elements are not in the DOM yet - obj_num++; - - // assign element its new id - elem.id = newid; - - // remap all url() attributes - var attrs = ids[oldid]["attrs"]; - var j = attrs.length; - while (j--) { - var attr = attrs[j]; - attr.ownerElement.setAttribute(attr.name, "url(#" + newid + ")"); - } - - // remap all href attributes - var hreffers = ids[oldid]["hrefs"]; - var k = hreffers.length; - while (k--) { - var hreffer = hreffers[k]; - hreffer.setAttributeNS(xlinkns, "xlink:href", "#"+newid); - } - } - } - - // now give the g itself a new id - - g.id = getNextId(); - // manually increment obj_num because our cloned elements are not in the DOM yet - obj_num++; - - current_layer.appendChild(g); - } + // add all children of the imported to the we create + var g = svgdoc.createElementNS(svgns, "g"); + while (importedNode.hasChildNodes()) + g.appendChild(importedNode.firstChild); + if (ts) + g.setAttribute("transform", ts); + + uniquifyElems(g); + + // now give the g itself a new id + g.id = getNextId(); + + current_layer.appendChild(g); // change image href vals if possible // $(svgcontent).find('image').each(function() { @@ -8036,6 +8161,12 @@ var identifyLayers = function() { if (child && child.nodeType == 1) { if (child.tagName == "g") { var name = $("title",child).text(); + + // Hack for Opera 10.60 + if(!name && isOpera && child.querySelectorAll) { + name = $(child.querySelectorAll('title')).text(); + } + // store layer and name in global variable if (name) { layernames.push(name);