/* * jsPlumb * * Title:jsPlumb 1.3.6 * * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas * elements, or VML. * * This file contains the SVG renderers. * * Copyright (c) 2010 - 2012 Simon Porritt (http://jsplumb.org) * * http://jsplumb.org * http://github.com/sporritt/jsplumb * http://code.google.com/p/jsplumb * * Dual licensed under the MIT and GPL2 licenses. */ /** * SVG support for jsPlumb. * * things to investigate: * * gradients: https://developer.mozilla.org/en/svg_in_html_introduction * css:http://tutorials.jenkov.com/svg/svg-and-css.html * text on a path: http://www.w3.org/TR/SVG/text.html#TextOnAPath * pointer events: https://developer.mozilla.org/en/css/pointer-events * * IE9 hover jquery: http://forum.jquery.com/topic/1-6-2-broke-svg-hover-events * */ ;(function() { var svgAttributeMap = { "joinstyle":"stroke-linejoin", "stroke-linejoin":"stroke-linejoin", "stroke-dashoffset":"stroke-dashoffset", "stroke-linecap":"stroke-linecap" }, STROKE_DASHARRAY = "stroke-dasharray", DASHSTYLE = "dashstyle", LINEAR_GRADIENT = "linearGradient", RADIAL_GRADIENT = "radialGradient", FILL = "fill", STOP = "stop", STROKE = "stroke", STROKE_WIDTH = "stroke-width", STYLE = "style", NONE = "none", JSPLUMB_GRADIENT = "jsplumb_gradient_", LINE_WIDTH = "lineWidth", ns = { svg:"http://www.w3.org/2000/svg", xhtml:"http://www.w3.org/1999/xhtml" }, _attr = function(node, attributes) { for (var i in attributes) node.setAttribute(i, "" + attributes[i]); }, _node = function(name, attributes) { var n = document.createElementNS(ns.svg, name); attributes = attributes || {}; attributes["version"] = "1.1"; attributes["xmlns"] = ns.xhtml; _attr(n, attributes); return n; }, _pos = function(d) { return "position:absolute;left:" + d[0] + "px;top:" + d[1] + "px"; }, _clearGradient = function(parent) { for (var i = 0; i < parent.childNodes.length; i++) { if (parent.childNodes[i].tagName == LINEAR_GRADIENT || parent.childNodes[i].tagName == RADIAL_GRADIENT) parent.removeChild(parent.childNodes[i]); } }, _updateGradient = function(parent, node, style, dimensions, uiComponent) { var id = JSPLUMB_GRADIENT + uiComponent._jsPlumb.idstamp(); // first clear out any existing gradient _clearGradient(parent); // this checks for an 'offset' property in the gradient, and in the absence of it, assumes // we want a linear gradient. if it's there, we create a radial gradient. // it is possible that a more explicit means of defining the gradient type would be // better. relying on 'offset' means that we can never have a radial gradient that uses // some default offset, for instance. if (!style.gradient.offset) { var g = _node(LINEAR_GRADIENT, {id:id}); parent.appendChild(g); } else { var g = _node(RADIAL_GRADIENT, { id:id }); parent.appendChild(g); } // the svg radial gradient seems to treat stops in the reverse // order to how canvas does it. so we want to keep all the maths the same, but // iterate the actual style declarations in reverse order, if the x indexes are not in order. for (var i = 0; i < style.gradient.stops.length; i++) { // Straight Connectors and Bezier connectors act slightly differently; this code is a bit of a kludge. but next version of // jsplumb will be replacing both Straight and Bezier to be generic instances of 'Connector', which has a list of segments. // so, not too concerned about leaving this in for now. var styleToUse = i; if (dimensions.length == 8) styleToUse = dimensions[4] < dimensions[6] ? i: style.gradient.stops.length - 1 - i; else styleToUse = dimensions[4] < dimensions[6] ? style.gradient.stops.length - 1 - i : i; var stopColor = jsPlumb.util.convertStyle(style.gradient.stops[styleToUse][1], true); var s = _node(STOP, {"offset":Math.floor(style.gradient.stops[i][0] * 100) + "%", "stop-color":stopColor}); g.appendChild(s); } var applyGradientTo = style.strokeStyle ? STROKE : FILL; node.setAttribute(STYLE, applyGradientTo + ":url(#" + id + ")"); }, _applyStyles = function(parent, node, style, dimensions, uiComponent) { if (style.gradient) { _updateGradient(parent, node, style, dimensions, uiComponent); } else { // make sure we clear any existing gradient _clearGradient(parent); node.setAttribute(STYLE, ""); } node.setAttribute(FILL, style.fillStyle ? jsPlumb.util.convertStyle(style.fillStyle, true) : NONE); node.setAttribute(STROKE, style.strokeStyle ? jsPlumb.util.convertStyle(style.strokeStyle, true) : NONE); if (style.lineWidth) { node.setAttribute(STROKE_WIDTH, style.lineWidth); } // in SVG there is a stroke-dasharray attribute we can set, and its syntax looks like // the syntax in VML but is actually kind of nasty: values are given in the pixel // coordinate space, whereas in VML they are multiples of the width of the stroked // line, which makes a lot more sense. for that reason, jsPlumb is supporting both // the native svg 'stroke-dasharray' attribute, and also the 'dashstyle' concept from // VML, which will be the preferred method. the code below this converts a dashstyle // attribute given in terms of stroke width into a pixel representation, by using the // stroke's lineWidth. if (style[DASHSTYLE] && style[LINE_WIDTH] && !style[STROKE_DASHARRAY]) { var sep = style[DASHSTYLE].indexOf(",") == -1 ? " " : ",", parts = style[DASHSTYLE].split(sep), styleToUse = ""; parts.forEach(function(p) { styleToUse += (Math.floor(p * style.lineWidth) + sep); }); node.setAttribute(STROKE_DASHARRAY, styleToUse); } else if(style[STROKE_DASHARRAY]) { node.setAttribute(STROKE_DASHARRAY, style[STROKE_DASHARRAY]); } // extra attributes such as join type, dash offset. for (var i in svgAttributeMap) { if (style[i]) { node.setAttribute(svgAttributeMap[i], style[i]); } } }, _decodeFont = function(f) { var r = /([0-9].)(p[xt])\s(.*)/; var bits = f.match(r); return {size:bits[1] + bits[2], font:bits[3]}; }, _classManip = function(el, add, clazz) { var classesToAddOrRemove = clazz.split(" "), className = el.className, curClasses = className.baseVal.split(" "); for (var i = 0; i < classesToAddOrRemove.length; i++) { if (add) { if (curClasses.indexOf(classesToAddOrRemove[i]) == -1) curClasses.push(classesToAddOrRemove[i]); } else { var idx = curClasses.indexOf(classesToAddOrRemove[i]); if (idx != -1) curClasses.splice(idx, 1); } } el.className.baseVal = curClasses.join(" "); }, _addClass = function(el, clazz) { _classManip(el, true, clazz); }, _removeClass = function(el, clazz) { _classManip(el, false, clazz); }; /** utility methods for other objects to use. */ jsPlumb.util.svg = { addClass:_addClass, removeClass:_removeClass }; /* * Base class for SVG components. */ //var SvgComponent = function(cssClass, originalArgs, pointerEventsSpec) { var SvgComponent = function(params) { var self = this, pointerEventsSpec = params.pointerEventsSpec || "all"; jsPlumb.jsPlumbUIComponent.apply(this, params.originalArgs); self.canvas = null, self.path = null, self.svg = null; var clazz = params.cssClass + " " + (params.originalArgs[0].cssClass || ""), svgParams = { "style":"", "width":0, "height":0, "pointer-events":pointerEventsSpec, "position":"absolute" }; if (self.tooltip) svgParams["title"] = self.tooltip; self.svg = _node("svg", svgParams); if (params.useDivWrapper) { self.canvas = document.createElement("div"); self.canvas.style["position"] = "absolute"; jsPlumb.sizeCanvas(self.canvas,0,0,1,1); self.canvas.className = clazz; if (self.tooltip) self.canvas.setAttribute("title", self.tooltip); } else { _attr(self.svg, { "class":clazz }); self.canvas = self.svg; } params._jsPlumb.appendElement(self.canvas, params.originalArgs[0]["parent"]); if (params.useDivWrapper) self.canvas.appendChild(self.svg); // TODO this displayElement stuff is common between all components, across all // renderers. would be best moved to jsPlumbUIComponent. var displayElements = [ self.canvas ]; this.getDisplayElements = function() { return displayElements; }; this.appendDisplayElement = function(el) { displayElements.push(el); }; this.paint = function(d, style, anchor) { if (style != null) { var x = d[0], y = d[1]; if (params.useDivWrapper) { jsPlumb.sizeCanvas(self.canvas, d[0], d[1], d[2], d[3]); x = 0, y = 0; } _attr(self.svg, { "style":_pos([x, y, d[2], d[3]]), "width": d[2], "height": d[3] }); self._paint.apply(this, arguments); } }; }; /* * Base class for SVG connectors. */ var SvgConnector = jsPlumb.SvgConnector = function(params) { var self = this; SvgComponent.apply(this, [ { cssClass:params["_jsPlumb"].connectorClass, originalArgs:arguments, pointerEventsSpec:"none", tooltip:params.tooltip, _jsPlumb:params["_jsPlumb"] } ]); this._paint = function(d, style) { var p = self.getPath(d), a = { "d":p }, outlineStyle = null; a["pointer-events"] = "all"; // outline style. actually means drawing an svg object underneath the main one. if (style.outlineColor) { var outlineWidth = style.outlineWidth || 1, outlineStrokeWidth = style.lineWidth + (2 * outlineWidth), outlineStyle = jsPlumb.CurrentLibrary.extend({}, style); outlineStyle.strokeStyle = jsPlumb.util.convertStyle(style.outlineColor); outlineStyle.lineWidth = outlineStrokeWidth; if (self.bgPath == null) { self.bgPath = _node("path", a); self.svg.appendChild(self.bgPath); self.attachListeners(self.bgPath, self); } else { _attr(self.bgPath, a); } _applyStyles(self.svg, self.bgPath, outlineStyle, d, self); } // test - see below // a["clip-path"]= "url(#testClip)"; if (self.path == null) { self.path = _node("path", a); self.svg.appendChild(self.path); self.attachListeners(self.path, self); /* this is a test of a clip path. i'm looking into using one of these to animate a jsplumb connection. you could do this by walking along the line, stepping along a little at a time, and setting the clip path to extend as far as that point. self.clip = _node("clipPath", {id:"testClip", clipPathUnits:"objectBoundingBox"}); self.svg.appendChild(self.clip); self.clip.appendChild(_node("rect", { x:"0",y:"0",width:"0.5",height:"1" })); */ } else { _attr(self.path, a); } _applyStyles(self.svg, self.path, style, d, self); }; this.reattachListeners = function() { if (self.bgPath) self.reattachListenersForElement(self.bgPath, self); if (self.path) self.reattachListenersForElement(self.path, self); }; }; /* * SVG Bezier Connector */ jsPlumb.Connectors.svg.Bezier = function(params) { jsPlumb.Connectors.Bezier.apply(this, arguments); SvgConnector.apply(this, arguments); this.getPath = function(d) { var _p = "M " + d[4] + " " + d[5]; _p += (" C " + d[8] + " " + d[9] + " " + d[10] + " " + d[11] + " " + d[6] + " " + d[7]); return _p; }; }; /* * SVG straight line Connector */ jsPlumb.Connectors.svg.Straight = function(params) { jsPlumb.Connectors.Straight.apply(this, arguments); SvgConnector.apply(this, arguments); this.getPath = function(d) { return "M " + d[4] + " " + d[5] + " L " + d[6] + " " + d[7]; }; }; jsPlumb.Connectors.svg.Flowchart = function() { var self = this; jsPlumb.Connectors.Flowchart.apply(this, arguments); SvgConnector.apply(this, arguments); this.getPath = function(dimensions) { var p = "M " + dimensions[4] + "," + dimensions[5]; // loop through extra points for (var i = 0; i < dimensions[8]; i++) { p = p + " L " + dimensions[9 + (i*2)] + " " + dimensions[10 + (i*2)]; } // finally draw a line to the end p = p + " " + dimensions[6] + "," + dimensions[7]; return p; }; }; /* * Base class for SVG endpoints. */ var SvgEndpoint = function(params) { var self = this; SvgComponent.apply(this, [ { cssClass:params["_jsPlumb"].endpointClass, originalArgs:arguments, pointerEventsSpec:"all", useDivWrapper:true, _jsPlumb:params["_jsPlumb"] } ]); this._paint = function(d, style) { var s = jsPlumb.extend({}, style); if (s.outlineColor) { s.strokeWidth = s.outlineWidth; s.strokeStyle = jsPlumb.util.convertStyle(s.outlineColor, true); } if (self.node == null) { self.node = self.makeNode(d, s); self.svg.appendChild(self.node); self.attachListeners(self.node, self); } _applyStyles(self.svg, self.node, s, d, self); _pos(self.node, d); }; this.reattachListeners = function() { if (self.node) self.reattachListenersForElement(self.node, self); }; }; /* * SVG Dot Endpoint */ jsPlumb.Endpoints.svg.Dot = function() { jsPlumb.Endpoints.Dot.apply(this, arguments); SvgEndpoint.apply(this, arguments); this.makeNode = function(d, style) { return _node("circle", { "cx" : d[2] / 2, "cy" : d[3] / 2, "r" : d[2] / 2 }); }; }; /* * SVG Rectangle Endpoint */ jsPlumb.Endpoints.svg.Rectangle = function() { jsPlumb.Endpoints.Rectangle.apply(this, arguments); SvgEndpoint.apply(this, arguments); this.makeNode = function(d, style) { return _node("rect", { "width":d[2], "height":d[3] }); }; }; /* * SVG Image Endpoint is the default image endpoint. */ jsPlumb.Endpoints.svg.Image = jsPlumb.Endpoints.Image; /* * Blank endpoint in svg renderer is the default Blank endpoint. */ jsPlumb.Endpoints.svg.Blank = jsPlumb.Endpoints.Blank; /* * Label endpoint in svg renderer is the default Label endpoint. */ jsPlumb.Overlays.svg.Label = jsPlumb.Overlays.Label; var AbstractSvgArrowOverlay = function(superclass, originalArgs) { superclass.apply(this, originalArgs); jsPlumb.jsPlumbUIComponent.apply(this, originalArgs); this.isAppendedAtTopLevel = false; var self = this, path =null; this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle) { if (path == null) { path = _node("path"); connector.svg.appendChild(path); self.attachListeners(path, connector); self.attachListeners(path, self); } var clazz = originalArgs && (originalArgs.length == 1) ? (originalArgs[0].cssClass || "") : ""; _attr(path, { "d" : makePath(d), "class" : clazz, stroke : strokeStyle ? strokeStyle : null, fill : fillStyle ? fillStyle : null }); }; var makePath = function(d) { return "M" + d.hxy.x + "," + d.hxy.y + " L" + d.tail[0].x + "," + d.tail[0].y + " L" + d.cxy.x + "," + d.cxy.y + " L" + d.tail[1].x + "," + d.tail[1].y + " L" + d.hxy.x + "," + d.hxy.y; }; this.reattachListeners = function() { if (path) self.reattachListenersForElement(path, self); }; }; jsPlumb.Overlays.svg.Arrow = function() { AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]); }; jsPlumb.Overlays.svg.PlainArrow = function() { AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]); }; jsPlumb.Overlays.svg.Diamond = function() { AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]); }; // a test jsPlumb.Overlays.svg.GuideLines = function() { var path = null, self = this, path2 = null, p1_1, p1_2; jsPlumb.Overlays.GuideLines.apply(this, arguments); this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle) { if (path == null) { path = _node("path"); connector.svg.appendChild(path); self.attachListeners(path, connector); self.attachListeners(path, self); p1_1 = _node("path"); connector.svg.appendChild(p1_1); self.attachListeners(p1_1, connector); self.attachListeners(p1_1, self); p1_2 = _node("path"); connector.svg.appendChild(p1_2); self.attachListeners(p1_2, connector); self.attachListeners(p1_2, self); } _attr(path, { "d" : makePath(d[0], d[1]), stroke : "red", fill : null }); _attr(p1_1, { "d" : makePath(d[2][0], d[2][1]), stroke : "blue", fill : null }); _attr(p1_2, { "d" : makePath(d[3][0], d[3][1]), stroke : "green", fill : null }); }; var makePath = function(d1, d2) { return "M " + d1.x + "," + d1.y + " L" + d2.x + "," + d2.y; }; }; })();