--- /dev/null
+/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.5.5
+ *
+ * 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 - 2013 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() {
+
+// ************************** SVG utility methods ********************************************
+
+ 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.instance.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.
+ // issue 244 suggested the 'gradientUnits' attribute; without this, straight/flowchart connectors with gradients would
+ // not show gradients when the line was perfectly horizontal or vertical.
+ var g;
+ if (!style.gradient.offset) {
+ g = _node(LINEAR_GRADIENT, {id:id, gradientUnits:"userSpaceOnUse"});
+ }
+ else {
+ 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++) {
+ var styleToUse = uiComponent.segment == 1 || uiComponent.segment == 2 ? i: style.gradient.stops.length - 1 - i,
+ stopColor = jsPlumbUtil.convertStyle(style.gradient.stops[styleToUse][1], true),
+ s = _node(STOP, {"offset":Math.floor(style.gradient.stops[i][0] * 100) + "%", "stop-color":stopColor});
+
+ g.appendChild(s);
+ }
+ var applyGradientTo = style.strokeStyle ? STROKE : FILL;
+ //document.location.toString()
+ //node.setAttribute(STYLE, applyGradientTo + ":url(#" + id + ")");
+ node.setAttribute(STYLE, applyGradientTo + ":url(" + document.location.toString() + "#" + 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 ? jsPlumbUtil.convertStyle(style.fillStyle, true) : NONE);
+ node.setAttribute(STROKE, style.strokeStyle ? jsPlumbUtil.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(.*)/,
+ 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); },
+ _appendAtIndex = function(svg, path, idx) {
+ if (svg.childNodes.length > idx) {
+ svg.insertBefore(path, svg.childNodes[idx]);
+ }
+ else svg.appendChild(path);
+ };
+
+ /**
+ utility methods for other objects to use.
+ */
+ jsPlumbUtil.svg = {
+ addClass:_addClass,
+ removeClass:_removeClass,
+ node:_node,
+ attr:_attr,
+ pos:_pos
+ };
+
+ // ************************** / SVG utility methods ********************************************
+
+ /*
+ * Base class for SVG components.
+ */
+ var SvgComponent = function(params) {
+ var pointerEventsSpec = params.pointerEventsSpec || "all", renderer = {};
+
+ jsPlumb.jsPlumbUIComponent.apply(this, params.originalArgs);
+ this.canvas = null;this.path = null;this.svg = null;
+
+ var clazz = params.cssClass + " " + (params.originalArgs[0].cssClass || ""),
+ svgParams = {
+ "style":"",
+ "width":0,
+ "height":0,
+ "pointer-events":pointerEventsSpec,
+ "position":"absolute"
+ };
+ this.svg = _node("svg", svgParams);
+ if (params.useDivWrapper) {
+ this.canvas = document.createElement("div");
+ this.canvas.style.position = "absolute";
+ jsPlumbUtil.sizeElement(this.canvas,0,0,1,1);
+ this.canvas.className = clazz;
+ }
+ else {
+ _attr(this.svg, { "class":clazz });
+ this.canvas = this.svg;
+ }
+
+ params._jsPlumb.appendElement(this.canvas, params.originalArgs[0].parent);
+ if (params.useDivWrapper) this.canvas.appendChild(this.svg);
+
+ // TODO this displayElement stuff is common between all components, across all
+ // renderers. would be best moved to jsPlumbUIComponent.
+ var displayElements = [ this.canvas ];
+ this.getDisplayElements = function() {
+ return displayElements;
+ };
+
+ this.appendDisplayElement = function(el) {
+ displayElements.push(el);
+ };
+
+ this.paint = function(style, anchor, extents) {
+ if (style != null) {
+
+ var xy = [ this.x, this.y ], wh = [ this.w, this.h ], p;
+ if (extents != null) {
+ if (extents.xmin < 0) xy[0] += extents.xmin;
+ if (extents.ymin < 0) xy[1] += extents.ymin;
+ wh[0] = extents.xmax + ((extents.xmin < 0) ? -extents.xmin : 0);
+ wh[1] = extents.ymax + ((extents.ymin < 0) ? -extents.ymin : 0);
+ }
+
+ if (params.useDivWrapper) {
+ jsPlumbUtil.sizeElement(this.canvas, xy[0], xy[1], wh[0], wh[1]);
+ xy[0] = 0; xy[1] = 0;
+ p = _pos([ 0, 0 ]);
+ }
+ else
+ p = _pos([ xy[0], xy[1] ]);
+
+ renderer.paint.apply(this, arguments);
+
+ _attr(this.svg, {
+ "style":p,
+ "width": wh[0],
+ "height": wh[1]
+ });
+ }
+ };
+
+ return {
+ renderer:renderer
+ };
+ };
+ jsPlumbUtil.extend(SvgComponent, jsPlumb.jsPlumbUIComponent, {
+ cleanup:function() {
+ jsPlumbUtil.removeElement(this.canvas);
+ this.svg = null;
+ this.canvas = null;
+ this.path = null;
+ },
+ setVisible:function(v) {
+ if (this.canvas) {
+ this.canvas.style.display = v ? "block" : "none";
+ }
+ if (this.bgCanvas) {
+ this.bgCanvas.style.display = v ? "block" : "none";
+ }
+ }
+ });
+
+ /*
+ * Base class for SVG connectors.
+ */
+ var SvgConnector = jsPlumb.ConnectorRenderers.svg = function(params) {
+ var self = this,
+ _super = SvgComponent.apply(this, [ {
+ cssClass:params._jsPlumb.connectorClass,
+ originalArgs:arguments,
+ pointerEventsSpec:"none",
+ _jsPlumb:params._jsPlumb
+ } ]);
+
+ /*this.pointOnPath = function(location, absolute) {
+ if (!self.path) return [0,0];
+ var p = absolute ? location : location * self.path.getTotalLength();
+ return self.path.getPointAtLength(p);
+ };*/
+
+ _super.renderer.paint = function(style, anchor, extents) {
+
+ var segments = self.getSegments(), p = "", offset = [0,0];
+ if (extents.xmin < 0) offset[0] = -extents.xmin;
+ if (extents.ymin < 0) offset[1] = -extents.ymin;
+
+ // create path from segments.
+ for (var i = 0; i < segments.length; i++) {
+ p += jsPlumb.Segments.svg.SegmentRenderer.getPath(segments[i]);
+ p += " ";
+ }
+
+ var a = {
+ d:p,
+ transform:"translate(" + offset[0] + "," + offset[1] + ")",
+ "pointer-events":params["pointer-events"] || "visibleStroke"
+ },
+ outlineStyle = null,
+ d = [self.x,self.y,self.w,self.h];
+
+ // 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 = jsPlumbUtil.convertStyle(style.outlineColor);
+ outlineStyle.lineWidth = outlineStrokeWidth;
+
+ if (self.bgPath == null) {
+ self.bgPath = _node("path", a);
+ _appendAtIndex(self.svg, self.bgPath, 0);
+ self.attachListeners(self.bgPath, self);
+ }
+ else {
+ _attr(self.bgPath, a);
+ }
+
+ _applyStyles(self.svg, self.bgPath, outlineStyle, d, self);
+ }
+
+ if (self.path == null) {
+ self.path = _node("path", a);
+ _appendAtIndex(self.svg, self.path, style.outlineColor ? 1 : 0);
+ self.attachListeners(self.path, self);
+ }
+ else {
+ _attr(self.path, a);
+ }
+
+ _applyStyles(self.svg, self.path, style, d, self);
+ };
+
+ this.reattachListeners = function() {
+ if (this.bgPath) this.reattachListenersForElement(this.bgPath, this);
+ if (this.path) this.reattachListenersForElement(this.path, this);
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.ConnectorRenderers.svg, SvgComponent);
+
+// ******************************* svg segment renderer *****************************************************
+
+ jsPlumb.Segments.svg = {
+ SegmentRenderer : {
+ getPath : function(segment) {
+ return ({
+ "Straight":function() {
+ var d = segment.getCoordinates();
+ return "M " + d.x1 + " " + d.y1 + " L " + d.x2 + " " + d.y2;
+ },
+ "Bezier":function() {
+ var d = segment.params;
+ return "M " + d.x1 + " " + d.y1 +
+ " C " + d.cp1x + " " + d.cp1y + " " + d.cp2x + " " + d.cp2y + " " + d.x2 + " " + d.y2;
+ },
+ "Arc":function() {
+ var d = segment.params,
+ laf = segment.sweep > Math.PI ? 1 : 0,
+ sf = segment.anticlockwise ? 0 : 1;
+
+ return "M" + segment.x1 + " " + segment.y1 + " A " + segment.radius + " " + d.r + " 0 " + laf + "," + sf + " " + segment.x2 + " " + segment.y2;
+ }
+ })[segment.type]();
+ }
+ }
+ };
+
+// ******************************* /svg segments *****************************************************
+
+ /*
+ * Base class for SVG endpoints.
+ */
+ var SvgEndpoint = window.SvgEndpoint = function(params) {
+ var _super = SvgComponent.apply(this, [ {
+ cssClass:params._jsPlumb.endpointClass,
+ originalArgs:arguments,
+ pointerEventsSpec:"all",
+ useDivWrapper:true,
+ _jsPlumb:params._jsPlumb
+ } ]);
+
+ _super.renderer.paint = function(style) {
+ var s = jsPlumb.extend({}, style);
+ if (s.outlineColor) {
+ s.strokeWidth = s.outlineWidth;
+ s.strokeStyle = jsPlumbUtil.convertStyle(s.outlineColor, true);
+ }
+
+ if (this.node == null) {
+ this.node = this.makeNode(s);
+ this.svg.appendChild(this.node);
+ this.attachListeners(this.node, this);
+ }
+ else if (this.updateNode != null) {
+ this.updateNode(this.node);
+ }
+ _applyStyles(this.svg, this.node, s, [ this.x, this.y, this.w, this.h ], this);
+ _pos(this.node, [ this.x, this.y ]);
+ }.bind(this);
+
+ };
+ jsPlumbUtil.extend(SvgEndpoint, SvgComponent, {
+ reattachListeners : function() {
+ if (this.node) this.reattachListenersForElement(this.node, this);
+ }
+ });
+
+ /*
+ * SVG Dot Endpoint
+ */
+ jsPlumb.Endpoints.svg.Dot = function() {
+ jsPlumb.Endpoints.Dot.apply(this, arguments);
+ SvgEndpoint.apply(this, arguments);
+ this.makeNode = function(style) {
+ return _node("circle", {
+ "cx" : this.w / 2,
+ "cy" : this.h / 2,
+ "r" : this.radius
+ });
+ };
+ this.updateNode = function(node) {
+ _attr(node, {
+ "cx":this.w / 2,
+ "cy":this.h / 2,
+ "r":this.radius
+ });
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Endpoints.svg.Dot, [jsPlumb.Endpoints.Dot, SvgEndpoint]);
+
+ /*
+ * SVG Rectangle Endpoint
+ */
+ jsPlumb.Endpoints.svg.Rectangle = function() {
+ jsPlumb.Endpoints.Rectangle.apply(this, arguments);
+ SvgEndpoint.apply(this, arguments);
+ this.makeNode = function(style) {
+ return _node("rect", {
+ "width" : this.w,
+ "height" : this.h
+ });
+ };
+ this.updateNode = function(node) {
+ _attr(node, {
+ "width":this.w,
+ "height":this.h
+ });
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Endpoints.svg.Rectangle, [jsPlumb.Endpoints.Rectangle, SvgEndpoint]);
+
+ /*
+ * 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 overlay in svg renderer is the default Label overlay.
+ */
+ jsPlumb.Overlays.svg.Label = jsPlumb.Overlays.Label;
+ /*
+ * Custom overlay in svg renderer is the default Custom overlay.
+ */
+ jsPlumb.Overlays.svg.Custom = jsPlumb.Overlays.Custom;
+
+ var AbstractSvgArrowOverlay = function(superclass, originalArgs) {
+ superclass.apply(this, originalArgs);
+ jsPlumb.jsPlumbUIComponent.apply(this, originalArgs);
+ this.isAppendedAtTopLevel = false;
+ var self = this;
+ this.path = null;
+ this.paint = function(params, containerExtents) {
+ // only draws on connections, not endpoints.
+ if (params.component.svg && containerExtents) {
+ if (this.path == null) {
+ this.path = _node("path", {
+ "pointer-events":"all"
+ });
+ params.component.svg.appendChild(this.path);
+
+ this.attachListeners(this.path, params.component);
+ this.attachListeners(this.path, this);
+ }
+ var clazz = originalArgs && (originalArgs.length == 1) ? (originalArgs[0].cssClass || "") : "",
+ offset = [0,0];
+
+ if (containerExtents.xmin < 0) offset[0] = -containerExtents.xmin;
+ if (containerExtents.ymin < 0) offset[1] = -containerExtents.ymin;
+
+ _attr(this.path, {
+ "d" : makePath(params.d),
+ "class" : clazz,
+ stroke : params.strokeStyle ? params.strokeStyle : null,
+ fill : params.fillStyle ? params.fillStyle : null,
+ transform : "translate(" + offset[0] + "," + offset[1] + ")"
+ });
+ }
+ };
+ 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 (this.path) this.reattachListenersForElement(this.path, this);
+ };
+ };
+ jsPlumbUtil.extend(AbstractSvgArrowOverlay, [jsPlumb.jsPlumbUIComponent, jsPlumb.Overlays.AbstractOverlay], {
+ cleanup : function() {
+ if (this.path != null) jsPlumb.CurrentLibrary.removeElement(this.path);
+ },
+ setVisible:function(v) {
+ if(this.path != null) (this.path.style.display = (v ? "block" : "none"));
+ }
+ });
+
+ jsPlumb.Overlays.svg.Arrow = function() {
+ AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]);
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.svg.Arrow, [ jsPlumb.Overlays.Arrow, AbstractSvgArrowOverlay ]);
+
+ jsPlumb.Overlays.svg.PlainArrow = function() {
+ AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]);
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.svg.PlainArrow, [ jsPlumb.Overlays.PlainArrow, AbstractSvgArrowOverlay ]);
+
+ jsPlumb.Overlays.svg.Diamond = function() {
+ AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]);
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.svg.Diamond, [ jsPlumb.Overlays.Diamond, AbstractSvgArrowOverlay ]);
+
+ // a test
+ jsPlumb.Overlays.svg.GuideLines = function() {
+ var path = null, self = this, p1_1, p1_2;
+ jsPlumb.Overlays.GuideLines.apply(this, arguments);
+ this.paint = function(params, containerExtents) {
+ if (path == null) {
+ path = _node("path");
+ params.connector.svg.appendChild(path);
+ self.attachListeners(path, params.connector);
+ self.attachListeners(path, self);
+
+ p1_1 = _node("path");
+ params.connector.svg.appendChild(p1_1);
+ self.attachListeners(p1_1, params.connector);
+ self.attachListeners(p1_1, self);
+
+ p1_2 = _node("path");
+ params.connector.svg.appendChild(p1_2);
+ self.attachListeners(p1_2, params.connector);
+ self.attachListeners(p1_2, self);
+ }
+
+ var offset =[0,0];
+ if (containerExtents.xmin < 0) offset[0] = -containerExtents.xmin;
+ if (containerExtents.ymin < 0) offset[1] = -containerExtents.ymin;
+
+ _attr(path, {
+ "d" : makePath(params.head, params.tail),
+ stroke : "red",
+ fill : null,
+ transform:"translate(" + offset[0] + "," + offset[1] + ")"
+ });
+
+ _attr(p1_1, {
+ "d" : makePath(params.tailLine[0], params.tailLine[1]),
+ stroke : "blue",
+ fill : null,
+ transform:"translate(" + offset[0] + "," + offset[1] + ")"
+ });
+
+ _attr(p1_2, {
+ "d" : makePath(params.headLine[0], params.headLine[1]),
+ stroke : "green",
+ fill : null,
+ transform:"translate(" + offset[0] + "," + offset[1] + ")"
+ });
+ };
+
+ var makePath = function(d1, d2) {
+ return "M " + d1.x + "," + d1.y +
+ " L" + d2.x + "," + d2.y;
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.svg.GuideLines, jsPlumb.Overlays.GuideLines);
+})();
\ No newline at end of file