jsPlumb-renderers-svg-1.3.5-RC1.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /*
  2. * jsPlumb
  3. *
  4. * Title:jsPlumb 1.3.5
  5. *
  6. * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
  7. * elements, or VML.
  8. *
  9. * This file contains the SVG renderers.
  10. *
  11. * Copyright (c) 2010 - 2012 Simon Porritt (http://jsplumb.org)
  12. *
  13. * http://jsplumb.org
  14. * http://github.com/sporritt/jsplumb
  15. * http://code.google.com/p/jsplumb
  16. *
  17. * Dual licensed under the MIT and GPL2 licenses.
  18. */
  19. /**
  20. * SVG support for jsPlumb.
  21. *
  22. * things to investigate:
  23. *
  24. * gradients: https://developer.mozilla.org/en/svg_in_html_introduction
  25. * css:http://tutorials.jenkov.com/svg/svg-and-css.html
  26. * text on a path: http://www.w3.org/TR/SVG/text.html#TextOnAPath
  27. * pointer events: https://developer.mozilla.org/en/css/pointer-events
  28. *
  29. * IE9 hover jquery: http://forum.jquery.com/topic/1-6-2-broke-svg-hover-events
  30. *
  31. */
  32. ;(function() {
  33. var svgAttributeMap = {
  34. "joinstyle":"stroke-linejoin",
  35. "stroke-linejoin":"stroke-linejoin",
  36. "stroke-dashoffset":"stroke-dashoffset",
  37. "stroke-linecap":"stroke-linecap"
  38. },
  39. STROKE_DASHARRAY = "stroke-dasharray",
  40. DASHSTYLE = "dashstyle",
  41. LINEAR_GRADIENT = "linearGradient",
  42. RADIAL_GRADIENT = "radialGradient",
  43. FILL = "fill",
  44. STOP = "stop",
  45. STROKE = "stroke",
  46. STROKE_WIDTH = "stroke-width",
  47. STYLE = "style",
  48. NONE = "none",
  49. JSPLUMB_GRADIENT = "jsplumb_gradient_",
  50. LINE_WIDTH = "lineWidth",
  51. ns = {
  52. svg:"http://www.w3.org/2000/svg",
  53. xhtml:"http://www.w3.org/1999/xhtml"
  54. },
  55. _attr = function(node, attributes) {
  56. for (var i in attributes)
  57. node.setAttribute(i, "" + attributes[i]);
  58. },
  59. _node = function(name, attributes) {
  60. var n = document.createElementNS(ns.svg, name);
  61. attributes = attributes || {};
  62. attributes["version"] = "1.1";
  63. attributes["xmnls"] = ns.xhtml;
  64. _attr(n, attributes);
  65. return n;
  66. },
  67. _pos = function(d) { return "position:absolute;left:" + d[0] + "px;top:" + d[1] + "px"; },
  68. _clearGradient = function(parent) {
  69. for (var i = 0; i < parent.childNodes.length; i++) {
  70. if (parent.childNodes[i].tagName == LINEAR_GRADIENT || parent.childNodes[i].tagName == RADIAL_GRADIENT)
  71. parent.removeChild(parent.childNodes[i]);
  72. }
  73. },
  74. _updateGradient = function(parent, node, style, dimensions, uiComponent) {
  75. var id = JSPLUMB_GRADIENT + uiComponent._jsPlumb.idstamp();
  76. // first clear out any existing gradient
  77. _clearGradient(parent);
  78. // this checks for an 'offset' property in the gradient, and in the absence of it, assumes
  79. // we want a linear gradient. if it's there, we create a radial gradient.
  80. // it is possible that a more explicit means of defining the gradient type would be
  81. // better. relying on 'offset' means that we can never have a radial gradient that uses
  82. // some default offset, for instance.
  83. if (!style.gradient.offset) {
  84. var g = _node(LINEAR_GRADIENT, {id:id});
  85. parent.appendChild(g);
  86. }
  87. else {
  88. var g = _node(RADIAL_GRADIENT, {
  89. id:id
  90. });
  91. parent.appendChild(g);
  92. }
  93. // the svg radial gradient seems to treat stops in the reverse
  94. // order to how canvas does it. so we want to keep all the maths the same, but
  95. // iterate the actual style declarations in reverse order, if the x indexes are not in order.
  96. for (var i = 0; i < style.gradient.stops.length; i++) {
  97. // Straight Connectors and Bezier connectors act slightly differently; this code is a bit of a kludge. but next version of
  98. // jsplumb will be replacing both Straight and Bezier to be generic instances of 'Connector', which has a list of segments.
  99. // so, not too concerned about leaving this in for now.
  100. var styleToUse = i;
  101. if (dimensions.length == 8)
  102. styleToUse = dimensions[4] < dimensions[6] ? i: style.gradient.stops.length - 1 - i;
  103. else
  104. styleToUse = dimensions[4] < dimensions[6] ? style.gradient.stops.length - 1 - i : i;
  105. var stopColor = jsPlumb.util.convertStyle(style.gradient.stops[styleToUse][1], true);
  106. var s = _node(STOP, {"offset":Math.floor(style.gradient.stops[i][0] * 100) + "%", "stop-color":stopColor});
  107. g.appendChild(s);
  108. }
  109. var applyGradientTo = style.strokeStyle ? STROKE : FILL;
  110. node.setAttribute(STYLE, applyGradientTo + ":url(#" + id + ")");
  111. },
  112. _applyStyles = function(parent, node, style, dimensions, uiComponent) {
  113. if (style.gradient) {
  114. _updateGradient(parent, node, style, dimensions, uiComponent);
  115. }
  116. else {
  117. // make sure we clear any existing gradient
  118. _clearGradient(parent);
  119. node.setAttribute(STYLE, "");
  120. }
  121. node.setAttribute(FILL, style.fillStyle ? jsPlumb.util.convertStyle(style.fillStyle, true) : NONE);
  122. node.setAttribute(STROKE, style.strokeStyle ? jsPlumb.util.convertStyle(style.strokeStyle, true) : NONE);
  123. if (style.lineWidth) {
  124. node.setAttribute(STROKE_WIDTH, style.lineWidth);
  125. }
  126. // in SVG there is a stroke-dasharray attribute we can set, and its syntax looks like
  127. // the syntax in VML but is actually kind of nasty: values are given in the pixel
  128. // coordinate space, whereas in VML they are multiples of the width of the stroked
  129. // line, which makes a lot more sense. for that reason, jsPlumb is supporting both
  130. // the native svg 'stroke-dasharray' attribute, and also the 'dashstyle' concept from
  131. // VML, which will be the preferred method. the code below this converts a dashstyle
  132. // attribute given in terms of stroke width into a pixel representation, by using the
  133. // stroke's lineWidth.
  134. if (style[DASHSTYLE] && style[LINE_WIDTH] && !style[STROKE_DASHARRAY]) {
  135. var sep = style[DASHSTYLE].indexOf(",") == -1 ? " " : ",",
  136. parts = style[DASHSTYLE].split(sep),
  137. styleToUse = "";
  138. parts.forEach(function(p) {
  139. styleToUse += (Math.floor(p * style.lineWidth) + sep);
  140. });
  141. node.setAttribute(STROKE_DASHARRAY, styleToUse);
  142. }
  143. else if(style[STROKE_DASHARRAY]) {
  144. node.setAttribute(STROKE_DASHARRAY, style[STROKE_DASHARRAY]);
  145. }
  146. // extra attributes such as join type, dash offset.
  147. for (var i in svgAttributeMap) {
  148. if (style[i]) {
  149. node.setAttribute(svgAttributeMap[i], style[i]);
  150. }
  151. }
  152. },
  153. _decodeFont = function(f) {
  154. var r = /([0-9].)(p[xt])\s(.*)/;
  155. var bits = f.match(r);
  156. return {size:bits[1] + bits[2], font:bits[3]};
  157. },
  158. _classManip = function(el, add, clazz) {
  159. var classesToAddOrRemove = clazz.split(" "),
  160. className = el.className,
  161. curClasses = className.baseVal.split(" ");
  162. for (var i = 0; i < classesToAddOrRemove.length; i++) {
  163. if (add) {
  164. if (curClasses.indexOf(classesToAddOrRemove[i]) == -1)
  165. curClasses.push(classesToAddOrRemove[i]);
  166. }
  167. else {
  168. var idx = curClasses.indexOf(classesToAddOrRemove[i]);
  169. if (idx != -1)
  170. curClasses.splice(idx, 1);
  171. }
  172. }
  173. el.className.baseVal = curClasses.join(" ");
  174. },
  175. _addClass = function(el, clazz) {
  176. _classManip(el, true, clazz);
  177. },
  178. _removeClass = function(el, clazz) {
  179. _classManip(el, false, clazz);
  180. };
  181. /**
  182. utility methods for other objects to use.
  183. */
  184. jsPlumb.util.svg = {
  185. addClass:_addClass,
  186. removeClass:_removeClass
  187. };
  188. /*
  189. * Base class for SVG components.
  190. */
  191. //var SvgComponent = function(cssClass, originalArgs, pointerEventsSpec) {
  192. var SvgComponent = function(params) {
  193. var self = this,
  194. pointerEventsSpec = params.pointerEventsSpec || "all";
  195. jsPlumb.jsPlumbUIComponent.apply(this, params.originalArgs);
  196. self.canvas = null, self.path = null, self.svg = null;
  197. var clazz = params.cssClass + " " + (params.originalArgs[0].cssClass || ""),
  198. svgParams = {
  199. "style":"",
  200. "width":0,
  201. "height":0,
  202. "pointer-events":pointerEventsSpec,
  203. "position":"absolute"
  204. };
  205. if (self.tooltip) svgParams["title"] = self.tooltip;
  206. self.svg = _node("svg", svgParams);
  207. if (params.useDivWrapper) {
  208. self.canvas = document.createElement("div");
  209. self.canvas.style["position"] = "absolute";
  210. jsPlumb.sizeCanvas(self.canvas,0,0,1,1);
  211. self.canvas.className = clazz;
  212. if (self.tooltip) self.canvas.setAttribute("title", self.tooltip);
  213. }
  214. else {
  215. _attr(self.svg, { "class":clazz });
  216. self.canvas = self.svg;
  217. }
  218. params._jsPlumb.appendElement(self.canvas, params.originalArgs[0]["parent"]);
  219. if (params.useDivWrapper) self.canvas.appendChild(self.svg);
  220. // TODO this displayElement stuff is common between all components, across all
  221. // renderers. would be best moved to jsPlumbUIComponent.
  222. var displayElements = [ self.canvas ];
  223. this.getDisplayElements = function() {
  224. return displayElements;
  225. };
  226. this.appendDisplayElement = function(el) {
  227. displayElements.push(el);
  228. };
  229. this.paint = function(d, style, anchor) {
  230. if (style != null) {
  231. var x = d[0], y = d[1];
  232. if (params.useDivWrapper) {
  233. jsPlumb.sizeCanvas(self.canvas, d[0], d[1], d[2], d[3]);
  234. x = 0, y = 0;
  235. }
  236. _attr(self.svg, {
  237. "style":_pos([x, y, d[2], d[3]]),
  238. "width": d[2],
  239. "height": d[3]
  240. });
  241. self._paint.apply(this, arguments);
  242. }
  243. };
  244. };
  245. /*
  246. * Base class for SVG connectors.
  247. */
  248. var SvgConnector = jsPlumb.SvgConnector = function(params) {
  249. var self = this;
  250. SvgComponent.apply(this, [ {
  251. cssClass:params["_jsPlumb"].connectorClass,
  252. originalArgs:arguments,
  253. pointerEventsSpec:"none",
  254. tooltip:params.tooltip,
  255. _jsPlumb:params["_jsPlumb"]
  256. } ]);
  257. this._paint = function(d, style) {
  258. var p = self.getPath(d), a = { "d":p }, outlineStyle = null;
  259. a["pointer-events"] = "all";
  260. // outline style. actually means drawing an svg object underneath the main one.
  261. if (style.outlineColor) {
  262. var outlineWidth = style.outlineWidth || 1,
  263. outlineStrokeWidth = style.lineWidth + (2 * outlineWidth),
  264. outlineStyle = jsPlumb.CurrentLibrary.extend({}, style);
  265. outlineStyle.strokeStyle = jsPlumb.util.convertStyle(style.outlineColor);
  266. outlineStyle.lineWidth = outlineStrokeWidth;
  267. if (self.bgPath == null) {
  268. self.bgPath = _node("path", a);
  269. self.svg.appendChild(self.bgPath);
  270. self.attachListeners(self.bgPath, self);
  271. }
  272. else {
  273. _attr(self.bgPath, a);
  274. }
  275. _applyStyles(self.svg, self.bgPath, outlineStyle, d, self);
  276. }
  277. // test - see below
  278. // a["clip-path"]= "url(#testClip)";
  279. if (self.path == null) {
  280. self.path = _node("path", a);
  281. self.svg.appendChild(self.path);
  282. self.attachListeners(self.path, self);
  283. /*
  284. this is a test of a clip path. i'm looking into using one of these to animate a jsplumb connection.
  285. you could do this by walking along the line, stepping along a little at a time, and setting the clip
  286. path to extend as far as that point.
  287. self.clip = _node("clipPath", {id:"testClip", clipPathUnits:"objectBoundingBox"});
  288. self.svg.appendChild(self.clip);
  289. self.clip.appendChild(_node("rect", {
  290. x:"0",y:"0",width:"0.5",height:"1"
  291. }));
  292. */
  293. }
  294. else {
  295. _attr(self.path, a);
  296. }
  297. _applyStyles(self.svg, self.path, style, d, self);
  298. };
  299. this.reattachListeners = function() {
  300. if (self.bgPath) self.reattachListenersForElement(self.bgPath, self);
  301. if (self.path) self.reattachListenersForElement(self.path, self);
  302. };
  303. };
  304. /*
  305. * SVG Bezier Connector
  306. */
  307. jsPlumb.Connectors.svg.Bezier = function(params) {
  308. jsPlumb.Connectors.Bezier.apply(this, arguments);
  309. SvgConnector.apply(this, arguments);
  310. this.getPath = function(d) { return "M " + d[4] + " " + d[5] + " C " + d[8] + " " + d[9] + " " + d[10] + " " + d[11] + " " + d[6] + " " + d[7]; };
  311. };
  312. /*
  313. * SVG straight line Connector
  314. */
  315. jsPlumb.Connectors.svg.Straight = function(params) {
  316. jsPlumb.Connectors.Straight.apply(this, arguments);
  317. SvgConnector.apply(this, arguments);
  318. this.getPath = function(d) { return "M " + d[4] + " " + d[5] + " L " + d[6] + " " + d[7]; };
  319. };
  320. jsPlumb.Connectors.svg.Flowchart = function() {
  321. var self = this;
  322. jsPlumb.Connectors.Flowchart.apply(this, arguments);
  323. SvgConnector.apply(this, arguments);
  324. this.getPath = function(dimensions) {
  325. var p = "M " + dimensions[4] + "," + dimensions[5];
  326. // loop through extra points
  327. for (var i = 0; i < dimensions[8]; i++) {
  328. p = p + " L " + dimensions[9 + (i*2)] + " " + dimensions[10 + (i*2)];
  329. }
  330. // finally draw a line to the end
  331. p = p + " " + dimensions[6] + "," + dimensions[7];
  332. return p;
  333. };
  334. };
  335. /*
  336. * Base class for SVG endpoints.
  337. */
  338. var SvgEndpoint = function(params) {
  339. var self = this;
  340. SvgComponent.apply(this, [ {
  341. cssClass:params["_jsPlumb"].endpointClass,
  342. originalArgs:arguments,
  343. pointerEventsSpec:"all",
  344. useDivWrapper:true,
  345. _jsPlumb:params["_jsPlumb"]
  346. } ]);
  347. this._paint = function(d, style) {
  348. var s = jsPlumb.extend({}, style);
  349. if (s.outlineColor) {
  350. s.strokeWidth = s.outlineWidth;
  351. s.strokeStyle = jsPlumb.util.convertStyle(s.outlineColor, true);
  352. }
  353. if (self.node == null) {
  354. self.node = self.makeNode(d, s);
  355. self.svg.appendChild(self.node);
  356. self.attachListeners(self.node, self);
  357. }
  358. _applyStyles(self.svg, self.node, s, d, self);
  359. _pos(self.node, d);
  360. };
  361. this.reattachListeners = function() {
  362. if (self.node) self.reattachListenersForElement(self.node, self);
  363. };
  364. };
  365. /*
  366. * SVG Dot Endpoint
  367. */
  368. jsPlumb.Endpoints.svg.Dot = function() {
  369. jsPlumb.Endpoints.Dot.apply(this, arguments);
  370. SvgEndpoint.apply(this, arguments);
  371. this.makeNode = function(d, style) {
  372. return _node("circle", {
  373. "cx" : d[2] / 2,
  374. "cy" : d[3] / 2,
  375. "r" : d[2] / 2
  376. });
  377. };
  378. };
  379. /*
  380. * SVG Rectangle Endpoint
  381. */
  382. jsPlumb.Endpoints.svg.Rectangle = function() {
  383. jsPlumb.Endpoints.Rectangle.apply(this, arguments);
  384. SvgEndpoint.apply(this, arguments);
  385. this.makeNode = function(d, style) {
  386. return _node("rect", {
  387. "width":d[2],
  388. "height":d[3]
  389. });
  390. };
  391. };
  392. /*
  393. * SVG Image Endpoint is the default image endpoint.
  394. */
  395. jsPlumb.Endpoints.svg.Image = jsPlumb.Endpoints.Image;
  396. /*
  397. * Blank endpoint in svg renderer is the default Blank endpoint.
  398. */
  399. jsPlumb.Endpoints.svg.Blank = jsPlumb.Endpoints.Blank;
  400. /*
  401. * Label endpoint in svg renderer is the default Label endpoint.
  402. */
  403. jsPlumb.Overlays.svg.Label = jsPlumb.Overlays.Label;
  404. var AbstractSvgArrowOverlay = function(superclass, originalArgs) {
  405. superclass.apply(this, originalArgs);
  406. jsPlumb.jsPlumbUIComponent.apply(this, originalArgs);
  407. this.isAppendedAtTopLevel = false;
  408. var self = this, path =null;
  409. this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle) {
  410. if (path == null) {
  411. path = _node("path");
  412. connector.svg.appendChild(path);
  413. self.attachListeners(path, connector);
  414. self.attachListeners(path, self);
  415. }
  416. var clazz = originalArgs && (originalArgs.length == 1) ? (originalArgs[0].cssClass || "") : "";
  417. _attr(path, {
  418. "d" : makePath(d),
  419. "class" : clazz,
  420. stroke : strokeStyle ? strokeStyle : null,
  421. fill : fillStyle ? fillStyle : null
  422. });
  423. };
  424. var makePath = function(d) {
  425. return "M" + d.hxy.x + "," + d.hxy.y +
  426. " L" + d.tail[0].x + "," + d.tail[0].y +
  427. " L" + d.cxy.x + "," + d.cxy.y +
  428. " L" + d.tail[1].x + "," + d.tail[1].y +
  429. " L" + d.hxy.x + "," + d.hxy.y;
  430. };
  431. this.reattachListeners = function() {
  432. if (path) self.reattachListenersForElement(path, self);
  433. };
  434. };
  435. jsPlumb.Overlays.svg.Arrow = function() {
  436. AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]);
  437. };
  438. jsPlumb.Overlays.svg.PlainArrow = function() {
  439. AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]);
  440. };
  441. jsPlumb.Overlays.svg.Diamond = function() {
  442. AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]);
  443. };
  444. // a test
  445. jsPlumb.Overlays.svg.GuideLines = function() {
  446. var path = null, self = this, path2 = null, p1_1, p1_2;
  447. jsPlumb.Overlays.GuideLines.apply(this, arguments);
  448. this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle) {
  449. if (path == null) {
  450. path = _node("path");
  451. connector.svg.appendChild(path);
  452. self.attachListeners(path, connector);
  453. self.attachListeners(path, self);
  454. p1_1 = _node("path");
  455. connector.svg.appendChild(p1_1);
  456. self.attachListeners(p1_1, connector);
  457. self.attachListeners(p1_1, self);
  458. p1_2 = _node("path");
  459. connector.svg.appendChild(p1_2);
  460. self.attachListeners(p1_2, connector);
  461. self.attachListeners(p1_2, self);
  462. }
  463. _attr(path, {
  464. "d" : makePath(d[0], d[1]),
  465. stroke : "red",
  466. fill : null
  467. });
  468. _attr(p1_1, {
  469. "d" : makePath(d[2][0], d[2][1]),
  470. stroke : "blue",
  471. fill : null
  472. });
  473. _attr(p1_2, {
  474. "d" : makePath(d[3][0], d[3][1]),
  475. stroke : "green",
  476. fill : null
  477. });
  478. };
  479. var makePath = function(d1, d2) {
  480. return "M " + d1.x + "," + d1.y +
  481. " L" + d2.x + "," + d2.y;
  482. };
  483. };
  484. })();