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)',
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

View File

@ -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({

View File

@ -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<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);
// Rewrap gsvg
if(naked_svgs.length) {
$(naked_svgs).each(function() {
groupSvgElem(this);
});
}
return output;
}
@ -7481,6 +7569,7 @@ var svgToString = this.svgToString = function(elem, indent) {
out.push(">");
indent++;
var bOneLine = false;
for (var i=0; i<childs.length; 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
// 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 <svg> to the <g> 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 <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
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 <svg> to the <g> 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);