123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- /**
- * 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
- *
- */
- ;(function() {
-
- var svgAttributeMap = {
- "stroke-linejoin":"stroke-linejoin",
- "joinstyle":"stroke-linejoin",
- "stroke-dashoffset":"stroke-dashoffset"
- };
- var 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["xmnls"] = ns.xhtml;
- _attr(n, attributes);
- return n;
- },
- _pos = function(d) { return "position:absolute;left:" + d[0] + "px;top:" + d[1] + "px"; },
- _convertStyle = function(s, ignoreAlpha) {
- var o = s,
- pad = function(n) { return n.length == 1 ? "0" + n : n; },
- hex = function(k) { return pad(Number(k).toString(16)); },
- pattern = /(rgb[a]?\()(.*)(\))/;
- if (s.match(pattern)) {
- var parts = s.match(pattern)[2].split(",");
- o = "#" + hex(parts[0]) + hex(parts[1]) + hex(parts[2]);
- if (!ignoreAlpha && parts.length == 4)
- o = o + hex(parts[3]);
- }
-
- return o;
- },
- _clearGradient = function(parent) {
- for (var i = 0; i < parent.childNodes.length; i++) {
- if (parent.childNodes[i].tagName == "linearGradient" || parent.childNodes[i].tagName == "radialGradient")
- parent.removeChild(parent.childNodes[i]);
- }
- },
- _updateGradient = function(parent, node, style, dimensions) {
- var id = "jsplumb_gradient_" + (new Date()).getTime();
- // 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("linearGradient", {id:id});
- parent.appendChild(g);
- }
- else {
- var g = _node("radialGradient", {
- 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 = _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) {
-
- if (style.gradient) {
- _updateGradient(parent, node, style, dimensions);
- }
- else {
- // make sure we clear any existing gradient
- _clearGradient(parent);
- node.setAttribute("style", "");
- }
-
- node.setAttribute("fill", style.fillStyle ? _convertStyle(style.fillStyle, true) : "none");
- node.setAttribute("stroke", style.strokeStyle ? _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["stroke-dasharray"]) {
- node.setAttribute("stroke-dasharray", style["stroke-dasharray"]);
- }
- if (style["dashstyle"] && style["lineWidth"]) {
- 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);
- }
-
- // 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]};
- };
-
- /*
- * Base class for SVG components.
- */
- var SvgComponent = function(cssClass, originalArgs, pointerEventsSpec) {
- var self = this;
- pointerEventsSpec = pointerEventsSpec || "all";
- jsPlumb.jsPlumbUIComponent.apply(this, originalArgs);
- self.canvas = null, self.path = null, self.svg = null;
-
- this.setHover = function() { };
-
- self.canvas = document.createElement("div");
- self.canvas.style["position"] = "absolute";
- jsPlumb.sizeCanvas(self.canvas,0,0,1,1);
-
- var clazz = cssClass + " " + (originalArgs[0].cssClass || "");
-
- self.svg = _node("svg", {
- "style":"",
- "width":0,
- "height":0,
- "pointer-events":pointerEventsSpec,
- "class": clazz
- });
-
- jsPlumb.appendElement(self.canvas, originalArgs[0]["parent"]);
- 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) {
- jsPlumb.sizeCanvas(self.canvas, d[0], d[1], d[2], d[3]);
- _attr(self.svg, {
- "style":_pos([0,0,d[2], d[3]]),
- "width": d[2],
- "height": d[3]
- });
- self._paint.apply(this, arguments);
- }
- };
- };
-
- /*
- * Base class for SVG connectors.
- */
- var SvgConnector = function(params) {
- var self = this;
- SvgComponent.apply(this, [ params["_jsPlumb"].connectorClass, arguments, "none" ]);
- 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 = {
- strokeStyle:_convertStyle(style.outlineColor),
- 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);
- }
-
-
- if (self.path == null) {
- self.path = _node("path", a);
- self.svg.appendChild(self.path);
- self.attachListeners(self.path, self);
- }
- else {
- _attr(self.path, a);
- }
-
- _applyStyles(self.svg, self.path, style, d);
- };
- };
- /*
- * SVG Bezier Connector
- */
- jsPlumb.Connectors.svg.Bezier = function(params) {
- jsPlumb.Connectors.Bezier.apply(this, arguments);
- SvgConnector.apply(this, arguments);
- this.getPath = function(d) { return "M " + d[4] + " " + d[5] + " C " + d[8] + " " + d[9] + " " + d[10] + " " + d[11] + " " + d[6] + " " + d[7]; };
- };
-
- /*
- * 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, [ params["_jsPlumb"].endpointClass, arguments, "all" ]);
- this._paint = function(d, style) {
- var s = jsPlumb.extend({}, style);
- if (s.outlineColor) {
- s.strokeWidth = s.outlineWidth;
- s.strokeStyle = _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);
- _pos(self.node, d);
- };
- };
-
- /*
- * 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);
- 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);
- }
-
- _attr(path, {
- "d" : makePath(d),
- 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;
- };
- };
-
- 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]);
- };
- })();
|