/*globals $, svgedit, svgCanvas, jsPDF*/
/*jslint vars: true, eqeq: true, todo: true, bitwise: true, continue: true, forin: true */
/*
* svgcanvas.js
*
* Licensed under the MIT License
*
* Copyright(c) 2010 Alexis Deveria
* Copyright(c) 2010 Pavol Rusnak
* Copyright(c) 2010 Jeff Schiller
*
*/
// Dependencies:
// 1) jQuery
// 2) pathseg.js
// 3) browser.js
// 4) svgtransformlist.js
// 5) math.js
// 6) units.js
// 7) svgutils.js
// 8) sanitize.js
// 9) history.js
// 10) select.js
// 11) draw.js
// 12) path.js
// 13) coords.js
// 14) recalculate.js
(function () {
if (!window.console) {
window.console = {};
window.console.log = function(str) {};
window.console.dir = function(str) {};
}
if (window.opera) {
window.console.log = function(str) { opera.postError(str); };
window.console.dir = function(str) {};
}
}());
// Class: SvgCanvas
// The main SvgCanvas class that manages all SVG-related functions
//
// Parameters:
// container - The container HTML element that should hold the SVG root element
// config - An object that contains configuration data
$.SvgCanvas = function(container, config) {
// Alias Namespace constants
var NS = svgedit.NS;
// Default configuration options
var curConfig = {
show_outside_canvas: true,
selectNew: true,
dimensions: [640, 480]
};
// Update config with new one if given
if (config) {
$.extend(curConfig, config);
}
// Array with width/height of canvas
var dimensions = curConfig.dimensions;
var canvas = this;
// "document" element associated with the container (same as window.document using default svg-editor.js)
// NOTE: This is not actually a SVG document, but a HTML document.
var svgdoc = container.ownerDocument;
// This is a container for the document being edited, not the document itself.
var svgroot = svgdoc.importNode(svgedit.utilities.text2xml(
'').documentElement, true);
container.appendChild(svgroot);
// The actual element that represents the final output SVG element
var svgcontent = svgdoc.createElementNS(NS.SVG, 'svg');
// This function resets the svgcontent element while keeping it in the DOM.
var clearSvgContentElement = canvas.clearSvgContentElement = function() {
while (svgcontent.firstChild) { svgcontent.removeChild(svgcontent.firstChild); }
// TODO: Clear out all other attributes first?
$(svgcontent).attr({
id: 'svgcontent',
width: dimensions[0],
height: dimensions[1],
x: dimensions[0],
y: dimensions[1],
overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden',
xmlns: NS.SVG,
'xmlns:se': NS.SE,
'xmlns:xlink': NS.XLINK
}).appendTo(svgroot);
// TODO: make this string optional and set by the client
var comment = svgdoc.createComment(" Created with SVG-edit - http://svg-edit.googlecode.com/ ");
svgcontent.appendChild(comment);
};
clearSvgContentElement();
// Prefix string for element IDs
var idprefix = 'svg_';
// Function: setIdPrefix
// Changes the ID prefix to the given value
//
// Parameters:
// p - String with the new prefix
canvas.setIdPrefix = function(p) {
idprefix = p;
};
// Current svgedit.draw.Drawing object
// @type {svgedit.draw.Drawing}
canvas.current_drawing_ = new svgedit.draw.Drawing(svgcontent, idprefix);
// Function: getCurrentDrawing
// Returns the current Drawing.
// @return {svgedit.draw.Drawing}
var getCurrentDrawing = canvas.getCurrentDrawing = function() {
return canvas.current_drawing_;
};
// Float displaying the current zoom level (1 = 100%, .5 = 50%, etc)
var current_zoom = 1;
// pointer to current group (for in-group editing)
var current_group = null;
// Object containing data for the currently selected styles
var all_properties = {
shape: {
fill: (curConfig.initFill.color == 'none' ? '' : '#') + curConfig.initFill.color,
fill_paint: null,
fill_opacity: curConfig.initFill.opacity,
stroke: '#' + curConfig.initStroke.color,
stroke_paint: null,
stroke_opacity: curConfig.initStroke.opacity,
stroke_width: curConfig.initStroke.width,
stroke_dasharray: 'none',
stroke_linejoin: 'miter',
stroke_linecap: 'butt',
opacity: curConfig.initOpacity
}
};
all_properties.text = $.extend(true, {}, all_properties.shape);
$.extend(all_properties.text, {
fill: '#000000',
stroke_width: 0,
font_size: 24,
font_family: 'serif'
});
// Current shape style properties
var cur_shape = all_properties.shape;
// Array with all the currently selected elements
// default size of 1 until it needs to grow bigger
var selectedElements = [];
// Function: addSvgElementFromJson
// Create a new SVG element based on the given object keys/values and add it to the current layer
// The element will be ran through cleanupElement before being returned
//
// Parameters:
// data - Object with the following keys/values:
// * element - tag name of the SVG element to create
// * attr - Object with attributes key-values to assign to the new element
// * curStyles - Boolean indicating that current style attributes should be applied first
//
// Returns: The new element
var addSvgElementFromJson = this.addSvgElementFromJson = function(data) {
var shape = svgedit.utilities.getElem(data.attr.id);
// if shape is a path but we need to create a rect/ellipse, then remove the path
var current_layer = getCurrentDrawing().getCurrentLayer();
if (shape && data.element != shape.tagName) {
current_layer.removeChild(shape);
shape = null;
}
if (!shape) {
shape = svgdoc.createElementNS(NS.SVG, data.element);
if (current_layer) {
(current_group || current_layer).appendChild(shape);
}
}
if (data.curStyles) {
svgedit.utilities.assignAttributes(shape, {
'fill': cur_shape.fill,
'stroke': cur_shape.stroke,
'stroke-width': cur_shape.stroke_width,
'stroke-dasharray': cur_shape.stroke_dasharray,
'stroke-linejoin': cur_shape.stroke_linejoin,
'stroke-linecap': cur_shape.stroke_linecap,
'stroke-opacity': cur_shape.stroke_opacity,
'fill-opacity': cur_shape.fill_opacity,
'opacity': cur_shape.opacity / 2,
'style': 'pointer-events:inherit'
}, 100);
}
svgedit.utilities.assignAttributes(shape, data.attr, 100);
svgedit.utilities.cleanupElement(shape);
return shape;
};
// import svgtransformlist.js
var getTransformList = canvas.getTransformList = svgedit.transformlist.getTransformList;
// import from math.js.
var transformPoint = svgedit.math.transformPoint;
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;
var getMatrix = svgedit.math.getMatrix;
// initialize from units.js
// send in an object implementing the ElementContainer interface (see units.js)
svgedit.units.init({
getBaseUnit: function() { return curConfig.baseUnit; },
getElement: svgedit.utilities.getElem,
getHeight: function() { return svgcontent.getAttribute('height')/current_zoom; },
getWidth: function() { return svgcontent.getAttribute('width')/current_zoom; },
getRoundDigits: function() { return save_options.round_digits; }
});
// import from units.js
var convertToNum = canvas.convertToNum = svgedit.units.convertToNum;
// import from svgutils.js
svgedit.utilities.init({
getDOMDocument: function() { return svgdoc; },
getDOMContainer: function() { return container; },
getSVGRoot: function() { return svgroot; },
// TODO: replace this mostly with a way to get the current drawing.
getSelectedElements: function() { return selectedElements; },
getSVGContent: function() { return svgcontent; },
getBaseUnit: function() { return curConfig.baseUnit; },
getSnappingStep: function() { return curConfig.snappingStep; }
});
var findDefs = canvas.findDefs = svgedit.utilities.findDefs;
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 = canvas.getBBox = svgedit.utilities.getBBox;
var getRotationAngle = canvas.getRotationAngle = svgedit.utilities.getRotationAngle;
var getElem = canvas.getElem = svgedit.utilities.getElem;
var getRefElem = canvas.getRefElem = svgedit.utilities.getRefElem;
var assignAttributes = canvas.assignAttributes = svgedit.utilities.assignAttributes;
var cleanupElement = this.cleanupElement = svgedit.utilities.cleanupElement;
// import from coords.js
svgedit.coords.init({
getDrawing: function() { return getCurrentDrawing(); },
getGridSnapping: function() { return curConfig.gridSnapping; }
});
var remapElement = this.remapElement = svgedit.coords.remapElement;
// import from recalculate.js
svgedit.recalculate.init({
getSVGRoot: function() { return svgroot; },
getStartTransform: function() { return startTransform; },
setStartTransform: function(transform) { startTransform = transform; }
});
var recalculateDimensions = this.recalculateDimensions = svgedit.recalculate.recalculateDimensions;
// import from sanitize.js
var nsMap = svgedit.getReverseNS();
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;
var call;
// 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);}
}
if (cmd.elem.tagName === 'use') {
setUseData(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);
}
// This is resolved in later versions of webkit, perhaps we should
// have a featured detection for correct 'use' behavior?
// ——————————
// Remove & Re-add hack for Webkit (issue 775)
//if (cmd.elem.tagName === 'use' && svgedit.browser.isWebkit()) {
// var elem = cmd.elem;
// if (!elem.getAttribute('x') && !elem.getAttribute('y')) {
// var parent = elem.parentNode;
// var sib = elem.nextSibling;
// parent.removeChild(elem);
// parent.insertBefore(elem, sib);
// }
//}
}
}
}
});
var addCommandToHistory = function(cmd) {
canvas.undoMgr.addCommandToHistory(cmd);
};
// import from select.js
svgedit.select.init(curConfig, {
createSVGElement: function(jsonMap) { return canvas.addSvgElementFromJson(jsonMap); },
svgRoot: function() { return svgroot; },
svgContent: function() { return svgcontent; },
currentZoom: function() { return current_zoom; },
// TODO(codedread): Remove when getStrokedBBox() has been put into svgutils.js.
getStrokedBBox: function(elems) { return canvas.getStrokedBBox([elems]); }
});
// this object manages selectors for us
var selectorManager = this.selectorManager = svgedit.select.getSelectorManager();
// Import from path.js
svgedit.path.init({
getCurrentZoom: function() { return current_zoom; },
getSVGRoot: function() { return svgroot; }
});
// Interface strings, usually for title elements
var uiStrings = {
exportNoBlur: "Blurred elements will appear as un-blurred",
exportNoforeignObject: "foreignObject elements will not appear",
exportNoDashArray: "Strokes will appear filled",
exportNoText: "Text may not appear as expected"
};
var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';
var ref_attrs = ['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'];
var elData = $.data;
// Animation element to change the opacity of any newly created element
var opac_ani = document.createElementNS(NS.SVG, 'animate');
$(opac_ani).attr({
attributeName: 'opacity',
begin: 'indefinite',
dur: 1,
fill: 'freeze'
}).appendTo(svgroot);
var restoreRefElems = function(elem) {
// Look for missing reference elements, restore any found
var o, i, l,
attrs = $(elem).attr(ref_attrs);
for (o in attrs) {
var val = attrs[o];
if (val && val.indexOf('url(') === 0) {
var id = svgedit.utilities.getUrlFromAttr(val).substr(1);
var ref = getElem(id);
if (!ref) {
svgedit.utilities.findDefs().appendChild(removedElements[id]);
delete removedElements[id];
}
}
}
var childs = elem.getElementsByTagName('*');
if (childs.length) {
for (i = 0, l = childs.length; i < l; i++) {
restoreRefElems(childs[i]);
}
}
};
(function() {
// TODO For Issue 208: this is a start on a thumbnail
// var svgthumb = svgdoc.createElementNS(NS.SVG, 'use');
// svgthumb.setAttribute('width', '100');
// svgthumb.setAttribute('height', '100');
// svgedit.utilities.setHref(svgthumb, '#svgcontent');
// svgroot.appendChild(svgthumb);
}());
// Object to contain image data for raster images that were found encodable
var encodableImages = {},
// String with image URL of last loadable image
last_good_img_url = curConfig.imgPath + 'logo.png',
// Array with current disabled elements (for in-group editing)
disabled_elems = [],
// Object with save options
save_options = {round_digits: 5},
// Boolean indicating whether or not a draw action has been started
started = false,
// String with an element's initial transform attribute value
startTransform = null,
// String indicating the current editor mode
current_mode = 'select',
// String with the current direction in which an element is being resized
current_resize_mode = 'none',
// Object with IDs for imported files, to see if one was already added
import_ids = {},
// Current text style properties
cur_text = all_properties.text,
// Current general properties
cur_properties = cur_shape,
// Array with selected elements' Bounding box object
// selectedBBoxes = new Array(1),
// The DOM element that was just selected
justSelected = null,
// DOM element for selection rectangle drawn by the user
rubberBox = null,
// Array of current BBoxes, used in getIntersectionList().
curBBoxes = [],
// Object to contain all included extensions
extensions = {},
// Canvas point for the most recent right click
lastClickPoint = null,
// Map of deleted reference elements
removedElements = {};
// Clipboard for cut, copy&pasted elements
canvas.clipBoard = [];
// Should this return an array by default, so extension results aren't overwritten?
var runExtensions = this.runExtensions = function(action, vars, returnArray) {
var result = returnArray ? [] : false;
$.each(extensions, function(name, opts) {
if (opts && action in opts) {
if (returnArray) {
result.push(opts[action](vars));
} else {
result = opts[action](vars);
}
}
});
return result;
};
// Function: addExtension
// Add an extension to the editor
//
// Parameters:
// name - String with the ID of the extension
// ext_func - Function supplied by the extension with its data
this.addExtension = function(name, ext_func) {
var ext;
if (!(name in extensions)) {
// Provide private vars/funcs here. Is there a better way to do this?
if ($.isFunction(ext_func)) {
ext = ext_func($.extend(canvas.getPrivateMethods(), {
svgroot: svgroot,
svgcontent: svgcontent,
nonce: getCurrentDrawing().getNonce(),
selectorManager: selectorManager
}));
} else {
ext = ext_func;
}
extensions[name] = ext;
call('extension_added', ext);
} else {
console.log('Cannot add extension "' + name + '", an extension by that name already exists.');
}
};
// This method rounds the incoming value to the nearest value based on the current_zoom
var round = this.round = function(val) {
return parseInt(val*current_zoom, 10)/current_zoom;
};
// This method sends back an array or a NodeList full of elements that
// intersect the multi-select rubber-band-box on the current_layer only.
//
// We brute-force getIntersectionList for browsers that do not support it (Firefox).
//
// Reference:
// Firefox does not implement getIntersectionList(), see https://bugzilla.mozilla.org/show_bug.cgi?id=501421
var getIntersectionList = this.getIntersectionList = function(rect) {
if (rubberBox == null) { return null; }
var parent = current_group || getCurrentDrawing().getCurrentLayer();
var rubberBBox;
if (!rect) {
rubberBBox = rubberBox.getBBox();
} else {
rubberBBox = svgcontent.createSVGRect(rect.x, rect.y, rect.width, rect.height);
}
var resultList = null;
if (typeof(svgroot.getIntersectionList) == 'function') {
// Offset the bbox of the rubber box by the offset of the svgcontent element.
rubberBBox.x += parseInt(svgcontent.getAttribute('x'), 10);
rubberBBox.y += parseInt(svgcontent.getAttribute('y'), 10);
resultList = svgroot.getIntersectionList(rubberBBox, parent);
}
if (resultList == null || typeof(resultList.item) != 'function') {
resultList = [];
if (!curBBoxes.length) {
// Cache all bboxes
curBBoxes = getVisibleElementsAndBBoxes(parent);
}
var i = curBBoxes.length;
while (i--) {
if (!rubberBBox.width) {continue;}
if (svgedit.math.rectsIntersect(rubberBBox, curBBoxes[i].bbox)) {
resultList.push(curBBoxes[i].elem);
}
}
}
// addToSelection expects an array, but it's ok to pass a NodeList
// because using square-bracket notation is allowed:
// http://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html
return resultList;
};
// TODO(codedread): Migrate this into svgutils.js
// Function: getStrokedBBox
// Get the bounding box for one or more stroked and/or transformed elements
//
// Parameters:
// elems - Array with DOM elements to check
//
// Returns:
// A single bounding box object
getStrokedBBox = this.getStrokedBBox = function(elems) {
if (!elems) {elems = getVisibleElements();}
if (!elems.length) {return false;}
// Make sure the expected BBox is returned if the element is a group
var getCheckedBBox = function(elem) {
// TODO: Fix issue with rotated groups. Currently they work
// fine in FF, but not in other browsers (same problem mentioned
// in Issue 339 comment #2).
var bb = svgedit.utilities.getBBox(elem);
if (!bb) {
return null;
}
var angle = svgedit.utilities.getRotationAngle(elem);
if ((angle && angle % 90) ||
svgedit.math.hasMatrixTransform(svgedit.transformlist.getTransformList(elem))) {
// Accurate way to get BBox of rotated element in Firefox:
// Put element in group and get its BBox
var good_bb = false;
// Get the BBox from the raw path for these elements
var elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon'];
if (elemNames.indexOf(elem.tagName) >= 0) {
bb = good_bb = canvas.convertToPath(elem, true);
} else if (elem.tagName == 'rect') {
// Look for radius
var rx = elem.getAttribute('rx');
var ry = elem.getAttribute('ry');
if (rx || ry) {
bb = good_bb = canvas.convertToPath(elem, true);
}
}
if (!good_bb) {
// Must use clone else FF freaks out
var clone = elem.cloneNode(true);
var g = document.createElementNS(NS.SVG, 'g');
var parent = elem.parentNode;
parent.appendChild(g);
g.appendChild(clone);
bb = svgedit.utilities.bboxToObj(g.getBBox());
parent.removeChild(g);
}
// Old method: Works by giving the rotated BBox,
// this is (unfortunately) what Opera and Safari do
// natively when getting the BBox of the parent group
// var angle = angle * Math.PI / 180.0;
// var rminx = Number.MAX_VALUE, rminy = Number.MAX_VALUE,
// rmaxx = Number.MIN_VALUE, rmaxy = Number.MIN_VALUE;
// var cx = round(bb.x + bb.width/2),
// cy = round(bb.y + bb.height/2);
// var pts = [ [bb.x - cx, bb.y - cy],
// [bb.x + bb.width - cx, bb.y - cy],
// [bb.x + bb.width - cx, bb.y + bb.height - cy],
// [bb.x - cx, bb.y + bb.height - cy] ];
// var j = 4;
// while (j--) {
// var x = pts[j][0],
// y = pts[j][1],
// r = Math.sqrt( x*x + y*y );
// var theta = Math.atan2(y,x) + angle;
// x = round(r * Math.cos(theta) + cx);
// y = round(r * Math.sin(theta) + cy);
//
// // now set the bbox for the shape after it's been rotated
// if (x < rminx) rminx = x;
// if (y < rminy) rminy = y;
// if (x > rmaxx) rmaxx = x;
// if (y > rmaxy) rmaxy = y;
// }
//
// bb.x = rminx;
// bb.y = rminy;
// bb.width = rmaxx - rminx;
// bb.height = rmaxy - rminy;
}
return bb;
};
var full_bb;
$.each(elems, function() {
if (full_bb) {return;}
if (!this.parentNode) {return;}
full_bb = getCheckedBBox(this);
});
// This shouldn't ever happen...
if (full_bb == null) {return null;}
// full_bb doesn't include the stoke, so this does no good!
// if (elems.length == 1) return full_bb;
var max_x = full_bb.x + full_bb.width;
var max_y = full_bb.y + full_bb.height;
var min_x = full_bb.x;
var min_y = full_bb.y;
// FIXME: same re-creation problem with this function as getCheckedBBox() above
var getOffset = function(elem) {
var sw = elem.getAttribute('stroke-width');
var offset = 0;
if (elem.getAttribute('stroke') != 'none' && !isNaN(sw)) {
offset += sw/2;
}
return offset;
};
var bboxes = [];
$.each(elems, function(i, elem) {
var cur_bb = getCheckedBBox(elem);
if (cur_bb) {
var offset = getOffset(elem);
min_x = Math.min(min_x, cur_bb.x - offset);
min_y = Math.min(min_y, cur_bb.y - offset);
bboxes.push(cur_bb);
}
});
full_bb.x = min_x;
full_bb.y = min_y;
$.each(elems, function(i, elem) {
var cur_bb = bboxes[i];
// ensure that elem is really an element node
if (cur_bb && elem.nodeType == 1) {
var offset = getOffset(elem);
max_x = Math.max(max_x, cur_bb.x + cur_bb.width + offset);
max_y = Math.max(max_y, cur_bb.y + cur_bb.height + offset);
}
});
full_bb.width = max_x - min_x;
full_bb.height = max_y - min_y;
return full_bb;
};
// Function: getVisibleElements
// Get all elements that have a BBox (excludes , , etc).
// Note that 0-opacity, off-screen etc elements are still considered "visible"
// for this function
//
// Parameters:
// parent - The parent DOM element to search within
//
// Returns:
// An array with all "visible" elements.
var getVisibleElements = this.getVisibleElements = function(parent) {
if (!parent) {
parent = $(svgcontent).children(); // Prevent layers from being included
}
var contentElems = [];
$(parent).children().each(function(i, elem) {
if (elem.getBBox) {
contentElems.push(elem);
}
});
return contentElems.reverse();
};
// Function: getVisibleElementsAndBBoxes
// Get all elements that have a BBox (excludes , , etc).
// Note that 0-opacity, off-screen etc elements are still considered "visible"
// for this function
//
// Parameters:
// parent - The parent DOM element to search within
//
// Returns:
// An array with objects that include:
// * elem - The element
// * bbox - The element's BBox as retrieved from getStrokedBBox
var getVisibleElementsAndBBoxes = this.getVisibleElementsAndBBoxes = function(parent) {
if (!parent) {
parent = $(svgcontent).children(); // Prevent layers from being included
}
var contentElems = [];
$(parent).children().each(function(i, elem) {
if (elem.getBBox) {
contentElems.push({'elem':elem, 'bbox':getStrokedBBox([elem])});
}
});
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(NS.SVG, '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
//
// Parameters:
// el - DOM element to clone
//
// Returns: The cloned element
var copyElem = function(el) {
// manually create a copy of the element
var new_el = document.createElementNS(el.namespaceURI, el.nodeName);
$.each(el.attributes, function(i, attr) {
if (attr.localName != '-moz-math-font-style') {
new_el.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value);
}
});
// set the copied element's new id
new_el.removeAttribute('id');
new_el.id = getNextId();
// Opera's "d" value needs to be reset for Opera/Win/non-EN
// Also needed for webkit (else does not keep curved segments on clone)
if (svgedit.browser.isWebkit() && el.nodeName == 'path') {
var fixed_d = pathActions.convertPath(el);
new_el.setAttribute('d', fixed_d);
}
// now create copies of all children
$.each(el.childNodes, function(i, child) {
switch(child.nodeType) {
case 1: // element node
new_el.appendChild(copyElem(child));
break;
case 3: // text node
new_el.textContent = child.nodeValue;
break;
default:
break;
}
});
if ($(el).data('gsvg')) {
$(new_el).data('gsvg', new_el.firstChild);
} else if ($(el).data('symbol')) {
var ref = $(el).data('symbol');
$(new_el).data('ref', ref).data('symbol', ref);
} else if (new_el.tagName == 'image') {
preventClickDefault(new_el);
}
return new_el;
};
// Set scope for these functions
var getId, getNextId;
var textActions, pathActions;
(function(c) {
// Object to contain editor event names and callback functions
var events = {};
getId = c.getId = function() { return getCurrentDrawing().getId(); };
getNextId = c.getNextId = function() { return getCurrentDrawing().getNextId(); };
// Function: call
// Run the callback function associated with the given event
//
// Parameters:
// event - String with the event name
// arg - Argument to pass through to the callback function
call = c.call = function(event, arg) {
if (events[event]) {
return events[event](this, arg);
}
};
// Function: bind
// Attaches a callback function to an event
//
// Parameters:
// event - String indicating the name of the event
// f - The callback function to bind to the event
//
// Return:
// The previous event
c.bind = function(event, f) {
var old = events[event];
events[event] = f;
return old;
};
}(canvas));
// Function: canvas.prepareSvg
// Runs the SVG Document through the sanitizer and then updates its paths.
//
// Parameters:
// newDoc - The SVG DOM document
this.prepareSvg = function(newDoc) {
this.sanitizeSvg(newDoc.documentElement);
// convert paths into absolute commands
var i, path, len,
paths = newDoc.getElementsByTagNameNS(NS.SVG, 'path');
for (i = 0, len = paths.length; i < len; ++i) {
path = paths[i];
path.setAttribute('d', pathActions.convertPath(path));
pathActions.fixEnd(path);
}
};
// Function: ffClone
// Hack for Firefox bugs where text element features aren't updated or get
// messed up. See issue 136 and issue 137.
// This function clones the element and re-selects it
// TODO: Test for this bug on load and add it to "support" object instead of
// browser sniffing
//
// Parameters:
// elem - The (text) DOM element to clone
var ffClone = function(elem) {
if (!svgedit.browser.isGecko()) {return elem;}
var clone = elem.cloneNode(true);
elem.parentNode.insertBefore(clone, elem);
elem.parentNode.removeChild(elem);
selectorManager.releaseSelector(elem);
selectedElements[0] = clone;
selectorManager.requestSelector(clone).showGrips(true);
return clone;
};
// this.each is deprecated, if any extension used this it can be recreated by doing this:
// $(canvas.getRootElem()).children().each(...)
// this.each = function(cb) {
// $(svgroot).children().each(cb);
// };
// Function: setRotationAngle
// Removes any old rotations if present, prepends a new rotation at the
// transformed center
//
// Parameters:
// val - The new rotation angle in degrees
// preventUndo - Boolean indicating whether the action should be undoable or not
this.setRotationAngle = function(val, preventUndo) {
// ensure val is the proper type
val = parseFloat(val);
var elem = selectedElements[0];
var oldTransform = elem.getAttribute('transform');
var bbox = svgedit.utilities.getBBox(elem);
var cx = bbox.x+bbox.width/2, cy = bbox.y+bbox.height/2;
var tlist = svgedit.transformlist.getTransformList(elem);
// only remove the real rotational transform if present (i.e. at index=0)
if (tlist.numberOfItems > 0) {
var xform = tlist.getItem(0);
if (xform.type == 4) {
tlist.removeItem(0);
}
}
// find R_nc and insert it
if (val != 0) {
var center = svgedit.math.transformPoint(cx, cy, svgedit.math.transformListToTransform(tlist).matrix);
var R_nc = svgroot.createSVGTransform();
R_nc.setRotate(val, center.x, center.y);
if (tlist.numberOfItems) {
tlist.insertItemBefore(R_nc, 0);
} else {
tlist.appendItem(R_nc);
}
} else if (tlist.numberOfItems == 0) {
elem.removeAttribute('transform');
}
if (!preventUndo) {
// we need to undo it, then redo it so it can be undo-able! :)
// TODO: figure out how to make changes to transform list undo-able cross-browser?
var newTransform = elem.getAttribute('transform');
elem.setAttribute('transform', oldTransform);
changeSelectedAttribute('transform', newTransform, selectedElements);
call('changed', selectedElements);
}
var pointGripContainer = svgedit.utilities.getElem('pathpointgrip_container');
// if (elem.nodeName == 'path' && pointGripContainer) {
// pathActions.setPointContainerTransform(elem.getAttribute('transform'));
// }
var selector = selectorManager.requestSelector(selectedElements[0]);
selector.resize();
selector.updateGripCursors(val);
};
// Function: recalculateAllSelectedDimensions
// Runs recalculateDimensions on the selected elements,
// adding the changes to a single batch command
var recalculateAllSelectedDimensions = this.recalculateAllSelectedDimensions = function() {
var text = (current_resize_mode == 'none' ? 'position' : 'size');
var batchCmd = new svgedit.history.BatchCommand(text);
var i = selectedElements.length;
while (i--) {
var elem = selectedElements[i];
// if (svgedit.utilities.getRotationAngle(elem) && !svgedit.math.hasMatrixTransform(getTransformList(elem))) {continue;}
var cmd = svgedit.recalculate.recalculateDimensions(elem);
if (cmd) {
batchCmd.addSubCommand(cmd);
}
}
if (!batchCmd.isEmpty()) {
addCommandToHistory(batchCmd);
call('changed', selectedElements);
}
};
// this is how we map paths to our preferred relative segment types
var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
'H', 'h', 'V', 'v', 'S', 's', 'T', 't'];
// Debug tool to easily see the current matrix in the browser's console
var logMatrix = function(m) {
console.log([m.a, m.b, m.c, m.d, m.e, m.f]);
};
// Root Current Transformation Matrix in user units
var root_sctm = null;
// Group: Selection
// Function: clearSelection
// Clears the selection. The 'selected' handler is then called.
// Parameters:
// noCall - Optional boolean that when true does not call the "selected" handler
var clearSelection = this.clearSelection = function(noCall) {
if (selectedElements[0] != null) {
var i, elem,
len = selectedElements.length;
for (i = 0; i < len; ++i) {
elem = selectedElements[i];
if (elem == null) {break;}
selectorManager.releaseSelector(elem);
selectedElements[i] = null;
}
// selectedBBoxes[0] = null;
}
if (!noCall) {call('selected', selectedElements);}
};
// TODO: do we need to worry about selectedBBoxes here?
// Function: addToSelection
// Adds a list of elements to the selection. The 'selected' handler is then called.
//
// Parameters:
// elemsToAdd - an array of DOM elements to add to the selection
// showGrips - a boolean flag indicating whether the resize grips should be shown
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;
}
++j;
}
// now add each element consecutively
var i = elemsToAdd.length;
while (i--) {
var elem = elemsToAdd[i];
if (!elem || !svgedit.utilities.getBBox(elem)) {continue;}
if (elem.tagName === 'a' && elem.childNodes.length === 1) {
// Make "a" element's child be the selected element
elem = elem.firstChild;
}
// if it's not already there, add it
if (selectedElements.indexOf(elem) == -1) {
selectedElements[j] = elem;
// only the first selectedBBoxes element is ever used in the codebase these days
// if (j == 0) selectedBBoxes[0] = svgedit.utilities.getBBox(elem);
j++;
var sel = selectorManager.requestSelector(elem);
if (selectedElements.length > 1) {
sel.showGrips(false);
}
}
}
call('selected', selectedElements);
if (showGrips || selectedElements.length == 1) {
selectorManager.requestSelector(selectedElements[0]).showGrips(true);
}
else {
selectorManager.requestSelector(selectedElements[0]).showGrips(false);
}
// make sure the elements are in the correct order
// See: http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition
selectedElements.sort(function(a, b) {
if (a && b && a.compareDocumentPosition) {
return 3 - (b.compareDocumentPosition(a) & 6);
}
if (a == null) {
return 1;
}
});
// Make sure first elements are not null
while (selectedElements[0] == null) {
selectedElements.shift(0);
}
};
// Function: selectOnly()
// Selects only the given elements, shortcut for clearSelection(); addToSelection()
//
// Parameters:
// elems - an array of DOM elements to be selected
var selectOnly = this.selectOnly = function(elems, showGrips) {
clearSelection(true);
addToSelection(elems, showGrips);
};
// TODO: could use slice here to make this faster?
// TODO: should the 'selected' handler
// Function: removeFromSelection
// Removes elements from the selection.
//
// Parameters:
// elemsToRemove - an array of elements to remove from selection
var removeFromSelection = this.removeFromSelection = function(elemsToRemove) {
if (selectedElements[0] == null) { return; }
if (elemsToRemove.length == 0) { return; }
// find every element and remove it from our array copy
var i,
j = 0,
newSelectedItems = [],
len = selectedElements.length;
newSelectedItems.length = len;
for (i = 0; i < len; ++i) {
var elem = selectedElements[i];
if (elem) {
// keep the item
if (elemsToRemove.indexOf(elem) == -1) {
newSelectedItems[j] = elem;
j++;
} else { // remove the item and its selector
selectorManager.releaseSelector(elem);
}
}
}
// the copy becomes the master now
selectedElements = newSelectedItems;
};
// Function: selectAllInCurrentLayer
// Clears the selection, then adds all elements in the current layer to the selection.
this.selectAllInCurrentLayer = function() {
var current_layer = getCurrentDrawing().getCurrentLayer();
if (current_layer) {
current_mode = 'select';
selectOnly($(current_group || current_layer).children());
}
};
// Function: getMouseTarget
// Gets the desired element from a mouse event
//
// Parameters:
// evt - Event object from the mouse event
//
// Returns:
// DOM element we want
var getMouseTarget = this.getMouseTarget = function(evt) {
if (evt == null) {
return null;
}
var mouse_target = evt.target;
// if it was a