6 * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
9 * This file contains the SVG renderers.
11 * Copyright (c) 2010 - 2013 Simon Porritt (http://jsplumb.org)
14 * http://github.com/sporritt/jsplumb
15 * http://code.google.com/p/jsplumb
17 * Dual licensed under the MIT and GPL2 licenses.
21 * SVG support for jsPlumb.
23 * things to investigate:
25 * gradients: https://developer.mozilla.org/en/svg_in_html_introduction
26 * css:http://tutorials.jenkov.com/svg/svg-and-css.html
27 * text on a path: http://www.w3.org/TR/SVG/text.html#TextOnAPath
28 * pointer events: https://developer.mozilla.org/en/css/pointer-events
30 * IE9 hover jquery: http://forum.jquery.com/topic/1-6-2-broke-svg-hover-events
35 // ************************** SVG utility methods ********************************************
37 var svgAttributeMap = {
38 "joinstyle":"stroke-linejoin",
39 "stroke-linejoin":"stroke-linejoin",
40 "stroke-dashoffset":"stroke-dashoffset",
41 "stroke-linecap":"stroke-linecap"
43 STROKE_DASHARRAY = "stroke-dasharray",
44 DASHSTYLE = "dashstyle",
45 LINEAR_GRADIENT = "linearGradient",
46 RADIAL_GRADIENT = "radialGradient",
50 STROKE_WIDTH = "stroke-width",
53 JSPLUMB_GRADIENT = "jsplumb_gradient_",
54 LINE_WIDTH = "lineWidth",
56 svg:"http://www.w3.org/2000/svg",
57 xhtml:"http://www.w3.org/1999/xhtml"
59 _attr = function(node, attributes) {
60 for (var i in attributes)
61 node.setAttribute(i, "" + attributes[i]);
63 _node = function(name, attributes) {
64 var n = document.createElementNS(ns.svg, name);
65 attributes = attributes || {};
66 attributes.version = "1.1";
67 attributes.xmlns = ns.xhtml;
71 _pos = function(d) { return "position:absolute;left:" + d[0] + "px;top:" + d[1] + "px"; },
72 _clearGradient = function(parent) {
73 for (var i = 0; i < parent.childNodes.length; i++) {
74 if (parent.childNodes[i].tagName == LINEAR_GRADIENT || parent.childNodes[i].tagName == RADIAL_GRADIENT)
75 parent.removeChild(parent.childNodes[i]);
78 _updateGradient = function(parent, node, style, dimensions, uiComponent) {
79 var id = JSPLUMB_GRADIENT + uiComponent._jsPlumb.instance.idstamp();
80 // first clear out any existing gradient
81 _clearGradient(parent);
82 // this checks for an 'offset' property in the gradient, and in the absence of it, assumes
83 // we want a linear gradient. if it's there, we create a radial gradient.
84 // it is possible that a more explicit means of defining the gradient type would be
85 // better. relying on 'offset' means that we can never have a radial gradient that uses
86 // some default offset, for instance.
87 // issue 244 suggested the 'gradientUnits' attribute; without this, straight/flowchart connectors with gradients would
88 // not show gradients when the line was perfectly horizontal or vertical.
90 if (!style.gradient.offset) {
91 g = _node(LINEAR_GRADIENT, {id:id, gradientUnits:"userSpaceOnUse"});
94 g = _node(RADIAL_GRADIENT, {
99 parent.appendChild(g);
101 // the svg radial gradient seems to treat stops in the reverse
102 // order to how canvas does it. so we want to keep all the maths the same, but
103 // iterate the actual style declarations in reverse order, if the x indexes are not in order.
104 for (var i = 0; i < style.gradient.stops.length; i++) {
105 var styleToUse = uiComponent.segment == 1 || uiComponent.segment == 2 ? i: style.gradient.stops.length - 1 - i,
106 stopColor = jsPlumbUtil.convertStyle(style.gradient.stops[styleToUse][1], true),
107 s = _node(STOP, {"offset":Math.floor(style.gradient.stops[i][0] * 100) + "%", "stop-color":stopColor});
111 var applyGradientTo = style.strokeStyle ? STROKE : FILL;
112 //document.location.toString()
113 //node.setAttribute(STYLE, applyGradientTo + ":url(#" + id + ")");
114 node.setAttribute(STYLE, applyGradientTo + ":url(" + document.location.toString() + "#" + id + ")");
116 _applyStyles = function(parent, node, style, dimensions, uiComponent) {
118 if (style.gradient) {
119 _updateGradient(parent, node, style, dimensions, uiComponent);
122 // make sure we clear any existing gradient
123 _clearGradient(parent);
124 node.setAttribute(STYLE, "");
127 node.setAttribute(FILL, style.fillStyle ? jsPlumbUtil.convertStyle(style.fillStyle, true) : NONE);
128 node.setAttribute(STROKE, style.strokeStyle ? jsPlumbUtil.convertStyle(style.strokeStyle, true) : NONE);
129 if (style.lineWidth) {
130 node.setAttribute(STROKE_WIDTH, style.lineWidth);
133 // in SVG there is a stroke-dasharray attribute we can set, and its syntax looks like
134 // the syntax in VML but is actually kind of nasty: values are given in the pixel
135 // coordinate space, whereas in VML they are multiples of the width of the stroked
136 // line, which makes a lot more sense. for that reason, jsPlumb is supporting both
137 // the native svg 'stroke-dasharray' attribute, and also the 'dashstyle' concept from
138 // VML, which will be the preferred method. the code below this converts a dashstyle
139 // attribute given in terms of stroke width into a pixel representation, by using the
140 // stroke's lineWidth.
141 if (style[DASHSTYLE] && style[LINE_WIDTH] && !style[STROKE_DASHARRAY]) {
142 var sep = style[DASHSTYLE].indexOf(",") == -1 ? " " : ",",
143 parts = style[DASHSTYLE].split(sep),
145 parts.forEach(function(p) {
146 styleToUse += (Math.floor(p * style.lineWidth) + sep);
148 node.setAttribute(STROKE_DASHARRAY, styleToUse);
150 else if(style[STROKE_DASHARRAY]) {
151 node.setAttribute(STROKE_DASHARRAY, style[STROKE_DASHARRAY]);
154 // extra attributes such as join type, dash offset.
155 for (var i in svgAttributeMap) {
157 node.setAttribute(svgAttributeMap[i], style[i]);
161 _decodeFont = function(f) {
162 var r = /([0-9].)(p[xt])\s(.*)/,
165 return {size:bits[1] + bits[2], font:bits[3]};
167 _classManip = function(el, add, clazz) {
168 var classesToAddOrRemove = clazz.split(" "),
169 className = el.className,
170 curClasses = className.baseVal.split(" ");
172 for (var i = 0; i < classesToAddOrRemove.length; i++) {
174 if (curClasses.indexOf(classesToAddOrRemove[i]) == -1)
175 curClasses.push(classesToAddOrRemove[i]);
178 var idx = curClasses.indexOf(classesToAddOrRemove[i]);
180 curClasses.splice(idx, 1);
184 el.className.baseVal = curClasses.join(" ");
186 _addClass = function(el, clazz) { _classManip(el, true, clazz); },
187 _removeClass = function(el, clazz) { _classManip(el, false, clazz); },
188 _appendAtIndex = function(svg, path, idx) {
189 if (svg.childNodes.length > idx) {
190 svg.insertBefore(path, svg.childNodes[idx]);
192 else svg.appendChild(path);
196 utility methods for other objects to use.
200 removeClass:_removeClass,
206 // ************************** / SVG utility methods ********************************************
209 * Base class for SVG components.
211 var SvgComponent = function(params) {
212 var pointerEventsSpec = params.pointerEventsSpec || "all", renderer = {};
214 jsPlumb.jsPlumbUIComponent.apply(this, params.originalArgs);
215 this.canvas = null;this.path = null;this.svg = null;
217 var clazz = params.cssClass + " " + (params.originalArgs[0].cssClass || ""),
222 "pointer-events":pointerEventsSpec,
223 "position":"absolute"
225 this.svg = _node("svg", svgParams);
226 if (params.useDivWrapper) {
227 this.canvas = document.createElement("div");
228 this.canvas.style.position = "absolute";
229 jsPlumbUtil.sizeElement(this.canvas,0,0,1,1);
230 this.canvas.className = clazz;
233 _attr(this.svg, { "class":clazz });
234 this.canvas = this.svg;
237 params._jsPlumb.appendElement(this.canvas, params.originalArgs[0].parent);
238 if (params.useDivWrapper) this.canvas.appendChild(this.svg);
240 // TODO this displayElement stuff is common between all components, across all
241 // renderers. would be best moved to jsPlumbUIComponent.
242 var displayElements = [ this.canvas ];
243 this.getDisplayElements = function() {
244 return displayElements;
247 this.appendDisplayElement = function(el) {
248 displayElements.push(el);
251 this.paint = function(style, anchor, extents) {
254 var xy = [ this.x, this.y ], wh = [ this.w, this.h ], p;
255 if (extents != null) {
256 if (extents.xmin < 0) xy[0] += extents.xmin;
257 if (extents.ymin < 0) xy[1] += extents.ymin;
258 wh[0] = extents.xmax + ((extents.xmin < 0) ? -extents.xmin : 0);
259 wh[1] = extents.ymax + ((extents.ymin < 0) ? -extents.ymin : 0);
262 if (params.useDivWrapper) {
263 jsPlumbUtil.sizeElement(this.canvas, xy[0], xy[1], wh[0], wh[1]);
264 xy[0] = 0; xy[1] = 0;
268 p = _pos([ xy[0], xy[1] ]);
270 renderer.paint.apply(this, arguments);
284 jsPlumbUtil.extend(SvgComponent, jsPlumb.jsPlumbUIComponent, {
286 jsPlumbUtil.removeElement(this.canvas);
291 setVisible:function(v) {
293 this.canvas.style.display = v ? "block" : "none";
296 this.bgCanvas.style.display = v ? "block" : "none";
302 * Base class for SVG connectors.
304 var SvgConnector = jsPlumb.ConnectorRenderers.svg = function(params) {
306 _super = SvgComponent.apply(this, [ {
307 cssClass:params._jsPlumb.connectorClass,
308 originalArgs:arguments,
309 pointerEventsSpec:"none",
310 _jsPlumb:params._jsPlumb
313 /*this.pointOnPath = function(location, absolute) {
314 if (!self.path) return [0,0];
315 var p = absolute ? location : location * self.path.getTotalLength();
316 return self.path.getPointAtLength(p);
319 _super.renderer.paint = function(style, anchor, extents) {
321 var segments = self.getSegments(), p = "", offset = [0,0];
322 if (extents.xmin < 0) offset[0] = -extents.xmin;
323 if (extents.ymin < 0) offset[1] = -extents.ymin;
325 // create path from segments.
326 for (var i = 0; i < segments.length; i++) {
327 p += jsPlumb.Segments.svg.SegmentRenderer.getPath(segments[i]);
333 transform:"translate(" + offset[0] + "," + offset[1] + ")",
334 "pointer-events":params["pointer-events"] || "visibleStroke"
337 d = [self.x,self.y,self.w,self.h];
339 // outline style. actually means drawing an svg object underneath the main one.
340 if (style.outlineColor) {
341 var outlineWidth = style.outlineWidth || 1,
342 outlineStrokeWidth = style.lineWidth + (2 * outlineWidth);
343 outlineStyle = jsPlumb.CurrentLibrary.extend({}, style);
344 outlineStyle.strokeStyle = jsPlumbUtil.convertStyle(style.outlineColor);
345 outlineStyle.lineWidth = outlineStrokeWidth;
347 if (self.bgPath == null) {
348 self.bgPath = _node("path", a);
349 _appendAtIndex(self.svg, self.bgPath, 0);
350 self.attachListeners(self.bgPath, self);
353 _attr(self.bgPath, a);
356 _applyStyles(self.svg, self.bgPath, outlineStyle, d, self);
359 if (self.path == null) {
360 self.path = _node("path", a);
361 _appendAtIndex(self.svg, self.path, style.outlineColor ? 1 : 0);
362 self.attachListeners(self.path, self);
368 _applyStyles(self.svg, self.path, style, d, self);
371 this.reattachListeners = function() {
372 if (this.bgPath) this.reattachListenersForElement(this.bgPath, this);
373 if (this.path) this.reattachListenersForElement(this.path, this);
376 jsPlumbUtil.extend(jsPlumb.ConnectorRenderers.svg, SvgComponent);
378 // ******************************* svg segment renderer *****************************************************
380 jsPlumb.Segments.svg = {
382 getPath : function(segment) {
384 "Straight":function() {
385 var d = segment.getCoordinates();
386 return "M " + d.x1 + " " + d.y1 + " L " + d.x2 + " " + d.y2;
388 "Bezier":function() {
389 var d = segment.params;
390 return "M " + d.x1 + " " + d.y1 +
391 " C " + d.cp1x + " " + d.cp1y + " " + d.cp2x + " " + d.cp2y + " " + d.x2 + " " + d.y2;
394 var d = segment.params,
395 laf = segment.sweep > Math.PI ? 1 : 0,
396 sf = segment.anticlockwise ? 0 : 1;
398 return "M" + segment.x1 + " " + segment.y1 + " A " + segment.radius + " " + d.r + " 0 " + laf + "," + sf + " " + segment.x2 + " " + segment.y2;
405 // ******************************* /svg segments *****************************************************
408 * Base class for SVG endpoints.
410 var SvgEndpoint = window.SvgEndpoint = function(params) {
411 var _super = SvgComponent.apply(this, [ {
412 cssClass:params._jsPlumb.endpointClass,
413 originalArgs:arguments,
414 pointerEventsSpec:"all",
416 _jsPlumb:params._jsPlumb
419 _super.renderer.paint = function(style) {
420 var s = jsPlumb.extend({}, style);
421 if (s.outlineColor) {
422 s.strokeWidth = s.outlineWidth;
423 s.strokeStyle = jsPlumbUtil.convertStyle(s.outlineColor, true);
426 if (this.node == null) {
427 this.node = this.makeNode(s);
428 this.svg.appendChild(this.node);
429 this.attachListeners(this.node, this);
431 else if (this.updateNode != null) {
432 this.updateNode(this.node);
434 _applyStyles(this.svg, this.node, s, [ this.x, this.y, this.w, this.h ], this);
435 _pos(this.node, [ this.x, this.y ]);
439 jsPlumbUtil.extend(SvgEndpoint, SvgComponent, {
440 reattachListeners : function() {
441 if (this.node) this.reattachListenersForElement(this.node, this);
448 jsPlumb.Endpoints.svg.Dot = function() {
449 jsPlumb.Endpoints.Dot.apply(this, arguments);
450 SvgEndpoint.apply(this, arguments);
451 this.makeNode = function(style) {
452 return _node("circle", {
458 this.updateNode = function(node) {
466 jsPlumbUtil.extend(jsPlumb.Endpoints.svg.Dot, [jsPlumb.Endpoints.Dot, SvgEndpoint]);
469 * SVG Rectangle Endpoint
471 jsPlumb.Endpoints.svg.Rectangle = function() {
472 jsPlumb.Endpoints.Rectangle.apply(this, arguments);
473 SvgEndpoint.apply(this, arguments);
474 this.makeNode = function(style) {
475 return _node("rect", {
480 this.updateNode = function(node) {
487 jsPlumbUtil.extend(jsPlumb.Endpoints.svg.Rectangle, [jsPlumb.Endpoints.Rectangle, SvgEndpoint]);
490 * SVG Image Endpoint is the default image endpoint.
492 jsPlumb.Endpoints.svg.Image = jsPlumb.Endpoints.Image;
494 * Blank endpoint in svg renderer is the default Blank endpoint.
496 jsPlumb.Endpoints.svg.Blank = jsPlumb.Endpoints.Blank;
498 * Label overlay in svg renderer is the default Label overlay.
500 jsPlumb.Overlays.svg.Label = jsPlumb.Overlays.Label;
502 * Custom overlay in svg renderer is the default Custom overlay.
504 jsPlumb.Overlays.svg.Custom = jsPlumb.Overlays.Custom;
506 var AbstractSvgArrowOverlay = function(superclass, originalArgs) {
507 superclass.apply(this, originalArgs);
508 jsPlumb.jsPlumbUIComponent.apply(this, originalArgs);
509 this.isAppendedAtTopLevel = false;
512 this.paint = function(params, containerExtents) {
513 // only draws on connections, not endpoints.
514 if (params.component.svg && containerExtents) {
515 if (this.path == null) {
516 this.path = _node("path", {
517 "pointer-events":"all"
519 params.component.svg.appendChild(this.path);
521 this.attachListeners(this.path, params.component);
522 this.attachListeners(this.path, this);
524 var clazz = originalArgs && (originalArgs.length == 1) ? (originalArgs[0].cssClass || "") : "",
527 if (containerExtents.xmin < 0) offset[0] = -containerExtents.xmin;
528 if (containerExtents.ymin < 0) offset[1] = -containerExtents.ymin;
531 "d" : makePath(params.d),
533 stroke : params.strokeStyle ? params.strokeStyle : null,
534 fill : params.fillStyle ? params.fillStyle : null,
535 transform : "translate(" + offset[0] + "," + offset[1] + ")"
539 var makePath = function(d) {
540 return "M" + d.hxy.x + "," + d.hxy.y +
541 " L" + d.tail[0].x + "," + d.tail[0].y +
542 " L" + d.cxy.x + "," + d.cxy.y +
543 " L" + d.tail[1].x + "," + d.tail[1].y +
544 " L" + d.hxy.x + "," + d.hxy.y;
546 this.reattachListeners = function() {
547 if (this.path) this.reattachListenersForElement(this.path, this);
550 jsPlumbUtil.extend(AbstractSvgArrowOverlay, [jsPlumb.jsPlumbUIComponent, jsPlumb.Overlays.AbstractOverlay], {
551 cleanup : function() {
552 if (this.path != null) jsPlumb.CurrentLibrary.removeElement(this.path);
554 setVisible:function(v) {
555 if(this.path != null) (this.path.style.display = (v ? "block" : "none"));
559 jsPlumb.Overlays.svg.Arrow = function() {
560 AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]);
562 jsPlumbUtil.extend(jsPlumb.Overlays.svg.Arrow, [ jsPlumb.Overlays.Arrow, AbstractSvgArrowOverlay ]);
564 jsPlumb.Overlays.svg.PlainArrow = function() {
565 AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]);
567 jsPlumbUtil.extend(jsPlumb.Overlays.svg.PlainArrow, [ jsPlumb.Overlays.PlainArrow, AbstractSvgArrowOverlay ]);
569 jsPlumb.Overlays.svg.Diamond = function() {
570 AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]);
572 jsPlumbUtil.extend(jsPlumb.Overlays.svg.Diamond, [ jsPlumb.Overlays.Diamond, AbstractSvgArrowOverlay ]);
575 jsPlumb.Overlays.svg.GuideLines = function() {
576 var path = null, self = this, p1_1, p1_2;
577 jsPlumb.Overlays.GuideLines.apply(this, arguments);
578 this.paint = function(params, containerExtents) {
580 path = _node("path");
581 params.connector.svg.appendChild(path);
582 self.attachListeners(path, params.connector);
583 self.attachListeners(path, self);
585 p1_1 = _node("path");
586 params.connector.svg.appendChild(p1_1);
587 self.attachListeners(p1_1, params.connector);
588 self.attachListeners(p1_1, self);
590 p1_2 = _node("path");
591 params.connector.svg.appendChild(p1_2);
592 self.attachListeners(p1_2, params.connector);
593 self.attachListeners(p1_2, self);
597 if (containerExtents.xmin < 0) offset[0] = -containerExtents.xmin;
598 if (containerExtents.ymin < 0) offset[1] = -containerExtents.ymin;
601 "d" : makePath(params.head, params.tail),
604 transform:"translate(" + offset[0] + "," + offset[1] + ")"
608 "d" : makePath(params.tailLine[0], params.tailLine[1]),
611 transform:"translate(" + offset[0] + "," + offset[1] + ")"
615 "d" : makePath(params.headLine[0], params.headLine[1]),
618 transform:"translate(" + offset[0] + "," + offset[1] + ")"
622 var makePath = function(d1, d2) {
623 return "M " + d1.x + "," + d1.y +
624 " L" + d2.x + "," + d2.y;
627 jsPlumbUtil.extend(jsPlumb.Overlays.svg.GuideLines, jsPlumb.Overlays.GuideLines);