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
master
Alexis Deveria 2010-07-16 15:46:54 +00:00
parent f0ec659f19
commit eb575ef68d
3 changed files with 299 additions and 158 deletions

View File

@ -18,7 +18,7 @@ svgEditor.addExtension("imagelib", function() {
name: 'Demo library (external)', name: 'Demo library (external)',
url: 'http://a.deveria.com/tests/clip-art/', url: 'http://a.deveria.com/tests/clip-art/',
description: 'Demonstration library for SVG-edit on another domain' description: 'Demonstration library for SVG-edit on another domain'
}, }
]; ];
@ -39,11 +39,13 @@ svgEditor.addExtension("imagelib", function() {
switch (char1) { switch (char1) {
case '<': case '<':
svgEditor.loadFromString(response); svgCanvas.importSvgString(response);
break; break;
case 'd': case 'd':
if(response.indexOf('data:') === 0) { 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; break;
} }
// Else fall through // Else fall through

View File

@ -145,6 +145,7 @@
// - invoke a file chooser dialog in 'save' mode // - invoke a file chooser dialog in 'save' mode
// - save the file to location chosen by the user // - save the file to location chosen by the user
Editor.setCustomHandlers = function(opts) { Editor.setCustomHandlers = function(opts) {
Editor.ready(function() {
if(opts.open) { if(opts.open) {
$('#tool_open').show(); $('#tool_open').show();
svgCanvas.open = opts.open; svgCanvas.open = opts.open;
@ -157,6 +158,7 @@
svgCanvas.bind("exported", opts.pngsave); svgCanvas.bind("exported", opts.pngsave);
} }
customHandlers = opts; customHandlers = opts;
});
} }
Editor.randomizeIds = function() { Editor.randomizeIds = function() {
@ -182,6 +184,7 @@
svgEditor.setConfig(urldata); svgEditor.setConfig(urldata);
// FIXME: This is null if Data URL ends with '='.
var src = urldata.source; var src = urldata.source;
var qstr = $.param.querystring(); var qstr = $.param.querystring();
@ -1340,6 +1343,10 @@
var el_name = elem.tagName; var el_name = elem.tagName;
if($(elem).data('gsvg')) {
el_name = 'svg';
}
if(panels[el_name]) { if(panels[el_name]) {
var cur_panel = panels[el_name]; var cur_panel = panels[el_name];
@ -3561,7 +3568,7 @@
if(this.files.length==1) { if(this.files.length==1) {
var reader = new FileReader(); var reader = new FileReader();
reader.onloadend = function(e) { reader.onloadend = function(e) {
svgCanvas.importSvgString(e.target.result); svgCanvas.importSvgString(e.target.result, true);
updateCanvas(); updateCanvas();
}; };
reader.readAsText(this.files[0]); reader.readAsText(this.files[0]);
@ -3815,6 +3822,7 @@
})(); })();
// ?iconsize=s&bkgd_color=555 // ?iconsize=s&bkgd_color=555
// svgEditor.setConfig({ // svgEditor.setConfig({

View File

@ -1249,7 +1249,7 @@ var SelectorManager;
offset += 2/current_zoom; offset += 2/current_zoom;
} }
var bbox = getBBox(selected); 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 // The bbox for a group does not include stroke vals, so we
// get the bbox based on its children. // get the bbox based on its children.
var stroked_bbox = getStrokedBBox(selected.childNodes); var stroked_bbox = getStrokedBBox(selected.childNodes);
@ -2223,6 +2223,18 @@ var getVisibleElements = this.getVisibleElements = function(parent, includeBBox)
return contentElems.reverse(); 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 // Function: copyElem
// Create a clone of an element, updating its ID and its children's IDs when needed // 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; 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); preventClickDefault(new_el);
} }
return new_el; return new_el;
@ -2394,6 +2409,7 @@ var sanitizeSvg = this.sanitizeSvg = function(node) {
// if this element is allowed // if this element is allowed
if (allowedAttrs != undefined) { if (allowedAttrs != undefined) {
var se_attrs = []; var se_attrs = [];
var i = node.attributes.length; var i = node.attributes.length;
@ -2872,6 +2888,7 @@ var remapElement = this.remapElement = function(selected,changes,m) {
changes["x"] = pt1.x; changes["x"] = pt1.x;
changes["y"] = pt1.y; changes["y"] = pt1.y;
break; break;
case "g":
case "text": case "text":
// if it was a translate, then just update x,y // if it was a translate, then just update x,y
if (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 && 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": case "text":
assignAttributes(selected, changes, 1000, true); assignAttributes(selected, changes, 1000, true);
break; break;
case "g":
var gsvg = $(selected).data('gsvg');
if(gsvg) {
assignAttributes(gsvg, changes, 1000, true);
}
break;
case "polyline": case "polyline":
case "polygon": case "polygon":
var len = changes["points"].length; var len = changes["points"].length;
@ -3119,6 +3142,9 @@ var recalculateDimensions = this.recalculateDimensions = function(selected) {
return null; return null;
} }
// Grouped SVG element
var gsvg = $(selected).data('gsvg');
// we know we have some transforms, so set up return variable // we know we have some transforms, so set up return variable
var batchCmd = new BatchCommand("Transform"); var batchCmd = new BatchCommand("Transform");
@ -3170,6 +3196,12 @@ var recalculateDimensions = this.recalculateDimensions = function(selected) {
$.each(changes, function(attr, val) { $.each(changes, function(attr, val) {
changes[attr] = convertToNum(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 // 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 // save the start transform value too
initial["transform"] = start_transform ? start_transform : ""; initial["transform"] = start_transform ? start_transform : "";
// if it's a group, we have special processing to flatten transforms // if it's a regular group, we have special processing to flatten transforms
if (selected.tagName == "g" || selected.tagName == "a") { if ((selected.tagName == "g" && !gsvg) || selected.tagName == "a") {
var box = getBBox(selected), var box = getBBox(selected),
oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2}, 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, newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2,
@ -3770,6 +3802,10 @@ var matrixMultiply = this.matrixMultiply = function() {
// Returns: // Returns:
// A single matrix transform object // A single matrix transform object
var transformListToTransform = this.transformListToTransform = function(tlist, min, max) { 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 min = min == undefined ? 0 : min;
var max = max == undefined ? (tlist.numberOfItems-1) : max; var max = max == undefined ? (tlist.numberOfItems-1) : max;
min = parseInt(min); min = parseInt(min);
@ -3901,6 +3937,7 @@ var addToSelection = this.addToSelection = function(elemsToAdd, showGrips) {
if (elemsToAdd.length == 0) { return; } if (elemsToAdd.length == 0) { return; }
// find the first null in our selectedElements array // find the first null in our selectedElements array
var j = 0; var j = 0;
while (j < selectedElements.length) { while (j < selectedElements.length) {
if (selectedElements[j] == null) { if (selectedElements[j] == null) {
break; break;
@ -4083,15 +4120,38 @@ var getMouseTarget = this.getMouseTarget = function(evt) {
} }
} }
// go up until we hit a child of a layer // Get the desired mouse_target with jQuery selector-fu
while (mouse_target.parentNode.parentNode.tagName == "g") { // 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; 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 // 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 // set the mouse_target to the svgroot like the other browsers
if (mouse_target.nodeName.toLowerCase() == "div") { // if (mouse_target.nodeName.toLowerCase() == "div") {
mouse_target = svgroot; // mouse_target = svgroot;
} // }
return mouse_target; return mouse_target;
}; };
@ -4137,7 +4197,7 @@ var getMouseTarget = this.getMouseTarget = function(evt) {
// if it is a selector grip, then it must be a single element selected, // 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 // 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, var gripid = evt.target.id,
griptype = gripid.substr(0,20); griptype = gripid.substr(0,20);
// rotating // rotating
@ -4582,6 +4642,7 @@ var getMouseTarget = this.getMouseTarget = function(evt) {
} }
var selectedBBox = selectedBBoxes[0]; var selectedBBox = selectedBBoxes[0];
if(selectedBBox) {
// reset selected bbox top-left position // reset selected bbox top-left position
selectedBBox.x = left; selectedBBox.x = left;
selectedBBox.y = top; selectedBBox.y = top;
@ -4593,7 +4654,7 @@ var getMouseTarget = this.getMouseTarget = function(evt) {
if (ty) { if (ty) {
selectedBBox.y += dy; selectedBBox.y += dy;
} }
}
selectorManager.requestSelector(selected).resize(); selectorManager.requestSelector(selected).resize();
break; break;
case "zoom": 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<len; i++) {
if(attrs[i].nodeName == 'id' || attrs[i].nodeName == 'style') {
len--;
}
}
// No significant attributes, so ungroup
if(len <= 0) {
var svg = this.firstChild;
naked_svgs.push(svg);
$(this).replaceWith(svg);
}
});
var output = svgToString(svgcontent, 0); var output = svgToString(svgcontent, 0);
// Rewrap gsvg
if(naked_svgs.length) {
$(naked_svgs).each(function() {
groupSvgElem(this);
});
}
return output; return output;
} }
@ -7481,6 +7569,7 @@ var svgToString = this.svgToString = function(elem, indent) {
out.push(">"); out.push(">");
indent++; indent++;
var bOneLine = false; var bOneLine = false;
for (var i=0; i<childs.length; i++) for (var i=0; i<childs.length; i++)
{ {
var child = childs.item(i); var child = childs.item(i);
@ -7645,6 +7734,94 @@ this.randomizeIds = function() {
} }
} }
// Function: uniquifyElems
// Ensure each element has a unique ID
//
// Parameters:
// g - The parent element of the tree to give unique IDs
var uniquifyElems = this.uniquifyElems = function(g) {
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 <image> or <a> 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 // Function: setSvgString
// This function sets the current drawing as the input SVG XML. // This function sets the current drawing as the input SVG XML.
@ -7699,6 +7876,20 @@ this.setSvgString = function(xmlString) {
canvas.embedImage(val); 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 // convert gradients with userSpaceOnUse to objectBoundingBox
$(svgcontent).find('linearGradient, radialGradient').each(function() { $(svgcontent).find('linearGradient, radialGradient').each(function() {
var grad = this; var grad = this;
@ -7825,6 +8016,8 @@ this.setSvgString = function(xmlString) {
// //
// Parameters: // Parameters:
// xmlString - The SVG as XML text. // xmlString - The SVG as XML text.
// toElements - Boolean indicating whether or not to convert the SVG to a group
// with children
// //
// Returns: // Returns:
// This function returns false if the import was unsuccessful, true otherwise. // This function returns false if the import was unsuccessful, true otherwise.
@ -7836,7 +8029,7 @@ this.setSvgString = function(xmlString) {
// was obtained // was obtained
// * import should happen in top-left of current zoomed viewport // * import should happen in top-left of current zoomed viewport
// * create a new layer for the imported SVG // * create a new layer for the imported SVG
this.importSvgString = function(xmlString) { this.importSvgString = function(xmlString, toElements) {
try { try {
// convert string into XML document // convert string into XML document
var newDoc = Utils.text2xml(xmlString); var newDoc = Utils.text2xml(xmlString);
@ -7848,11 +8041,8 @@ this.importSvgString = function(xmlString) {
// import new svg document into our document // import new svg document into our document
var importedNode = svgdoc.importNode(newDoc.documentElement, true); var importedNode = svgdoc.importNode(newDoc.documentElement, true);
if (current_layer) { var innerw = convertToNum('width', importedNode.getAttribute("width")),
// TODO: properly handle if width/height are not specified or if in percentages innerh = convertToNum('height', importedNode.getAttribute("height")),
// TODO: properly handle if width/height are in units (px, etc)
var innerw = importedNode.getAttribute("width"),
innerh = importedNode.getAttribute("height"),
innervb = importedNode.getAttribute("viewBox"), innervb = importedNode.getAttribute("viewBox"),
// if no explicit viewbox, create one out of the width and height // if no explicit viewbox, create one out of the width and height
vb = innervb ? innervb.split(" ") : [0,0,innerw,innerh]; vb = innervb ? innervb.split(" ") : [0,0,innerw,innerh];
@ -7863,6 +8053,7 @@ this.importSvgString = function(xmlString) {
var canvasw = Number(svgcontent.getAttribute("width")), var canvasw = Number(svgcontent.getAttribute("width")),
canvash = Number(svgcontent.getAttribute("height")); canvash = Number(svgcontent.getAttribute("height"));
// imported content should be 1/3 of the canvas on its largest dimension // imported content should be 1/3 of the canvas on its largest dimension
if (innerh > innerw) { if (innerh > innerw) {
var ts = "scale(" + (canvash/3)/vb[3] + ")"; var ts = "scale(" + (canvash/3)/vb[3] + ")";
} }
@ -7873,6 +8064,21 @@ this.importSvgString = function(xmlString) {
// Hack to make recalculateDimensions understand how to scale // Hack to make recalculateDimensions understand how to scale
ts = "translate(0) " + ts + " translate(0)"; ts = "translate(0) " + ts + " translate(0)";
if(!toElements) {
var elem = $(importedNode).appendTo(current_layer)[0];
groupSvgElem(elem);
clearSelection();
var g = elem.parentNode;
g.setAttribute("transform", ts);
recalculateDimensions(g);
addToSelection([g]);
return;
}
// TODO: Find way to add this in a recalculateDimensions-parsable way // TODO: Find way to add this in a recalculateDimensions-parsable way
// if (vb[0] != 0 || vb[1] != 0) // if (vb[0] != 0 || vb[1] != 0)
// ts = "translate(" + (-vb[0]) + "," + (-vb[1]) + ") " + ts; // ts = "translate(" + (-vb[0]) + "," + (-vb[1]) + ") " + ts;
@ -7884,93 +8090,12 @@ this.importSvgString = function(xmlString) {
if (ts) if (ts)
g.setAttribute("transform", ts); g.setAttribute("transform", ts);
// now ensure each element has a unique ID uniquifyElems(g);
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 <image> or <a> 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 // now give the g itself a new id
g.id = getNextId(); g.id = getNextId();
// manually increment obj_num because our cloned elements are not in the DOM yet
obj_num++;
current_layer.appendChild(g); current_layer.appendChild(g);
}
// change image href vals if possible // change image href vals if possible
// $(svgcontent).find('image').each(function() { // $(svgcontent).find('image').each(function() {
@ -8036,6 +8161,12 @@ var identifyLayers = function() {
if (child && child.nodeType == 1) { if (child && child.nodeType == 1) {
if (child.tagName == "g") { if (child.tagName == "g") {
var name = $("title",child).text(); 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 // store layer and name in global variable
if (name) { if (name) {
layernames.push(name); layernames.push(name);