Note: This was originally posted on the 22nd of July 2015, with the caveat that "the code in this page will not run in Firefox, due to the underlying issues discussed here.", which discussed the fact that Firefox does not have offsetLeft
and offsetTop
properties on SVG elements.
Fast forward a year and a bit and the demo does not work anywhere now, because of the situation discussed here.. No browser has offsetLeft
or offsetTop
properties any longer. The upshot is that jsPlumb can no longer connect SVG elements in the same way as it does standard DOM elements. Consider the following markup and code snippet:
<div style="position:relative;width:100%">
<svg width="150" height="150" fill="red" id="red" x="0" y="0">
<rect x="0" y="0" width="150" height="150"/>
</svg>
<svg width="150" height="150" fill="blue" style="position:absolute;left:250px" id="blue" x="200" y="200" >
<rect x="0" y="0" width="150" height="150"/>
</svg>
</div>
jsPlumb cannot connect these two svg
elements. To connect the svg
elements, we need to wrap them in divs
:
<div style="position:relative;width:100%;height:200px;" id="demo1">
<div id="red" style="position:absolute;left:0;top:0;">
<svg width="150" height="150" fill="red">
<rect x="0" y="0" width="150" height="150"/>
</svg>
</div>
<div style="position:absolute;left:250px;top:0;" id="blue">
<svg width="150" height="150" fill="blue">
<rect x="0" y="0" width="150" height="150"/>
</svg>
</div>
</div>
var j = jsPlumb.getInstance({
Container:document.getElementById("demo1")
});
j.connect({
source:"red",
target:"blue",
anchor:"Continuous",
connector:"Straight",
paintStyle:{ lineWidth:2, strokeStyle:"aliceblue" }
});
The result is this:
One of the key things about this is that the svg
elements are at position [0,0] of their parent elements, which are "standard" DOM elements. This is currently a requirement if you wish to connect SVG elements.
You cannot connect two rect
elements (or any SVG shapes) directly. This is because the shapes inside an SVG element
do not have the same positioning information as all other DOM elements. So now consider you have this markup:
<div style="position:relative;width:100%">
<div id="red2" style="position:absolute;">
<svg width="100%" height="200" fill="red" x="0" y="0" style="border:1px solid gray;">
<rect x="10" y="10" width="50" height="50" id="rect1"/>
<circle cx="220" cy="130" r="20"/>
<rect x="290" y="60" width="100" height="50" id="rect2"/>
</svg>
</div>
</div>
which produces:
and you want to connect the two red rectangles. The key here is that both rect
elements share the same svg
parent;
this happens in quite a few libraries that render to SVG (the one that comes to mind immediately, which spurred this
post, is Raphael. Highcharts is also a well known library that does this. But there are probably a million or more,
and that may or may not be an exaggeration).
The solution here is that you need to override the methods that jsPlumb uses to determine the location and size of
things. First let's define handlers for getting the offset and size for a few known SVG shapes:
var offsetCalculators = {
"CIRCLE":function(el) {
var cx = parseInt(el.getAttribute("cx"), 10),
cy = parseInt(el.getAttribute("cy"), 10),
r = parseInt(el.getAttribute("r"), 10);
return {
left: (cx - r),
top:(cy - r)
};
},
"ELLIPSE":function(el) {
var cx = parseInt(el.getAttribute("cx"), 10),
cy = parseInt(el.getAttribute("cy"), 10),
rx = parseInt(el.getAttribute("rx"), 10),
ry = parseInt(el.getAttribute("ry"), 10);
return {
left: (cx - rx),
top:(cy - ry)
};
},
"RECT":function(el) {
var x = parseInt(el.getAttribute("x"), 10),
y = parseInt(el.getAttribute("y"), 10);
return {
left:x,
top:y
};
}
};
// custom size calculators for SVG shapes.
var sizeCalculators = {
"CIRCLE":function(el) {
var r = parseInt(el.getAttribute("r"), 10);
return [ r * 2, r * 2 ];
},
"ELLIPSE":function(el) {
var rx = parseInt(el.getAttribute("rx"), 10),
ry = parseInt(el.getAttribute("ry"), 10);
return [ rx * 2, ry * 2 ];
},
"RECT":function(el) {
var w = parseInt(el.getAttribute("width"), 10),
h = parseInt(el.getAttribute("height"), 10);
return [ w, h ];
}
};
Now we can override the methods we need to in jsPlumb, handing off control to these handlers when necessary:
// store original jsPlumb prototype methods for getOffset and size.
var originalOffset = jsPlumbInstance.prototype.getOffset;
var originalSize = jsPlumbInstance.prototype.getSize;
jsPlumbInstance.prototype.getOffset = function(el) {
var tn = el.tagName.toUpperCase();
if (offsetCalculators[tn]) {
// here we rely on the fact that the SVG element is at 0,0 of its parent div. If it was not, we'd also want to adjust
// the offset for each shape by the offset of its parent.
// note that a previous version of this blog post used to take into account the offset of the SVG's parent, but
// more recent versions of jsPlumb are strict about the necessity of working inside a "container" element, so this is
// not a consideration any longer.
return offsetCalculators[tn](el);
}
else
return originalOffset.apply(this, [el]);
};
jsPlumbInstance.prototype.getSize = function(el) {
var tn = el.tagName.toUpperCase();
if (sizeCalculators[tn]) {
return sizeCalculators[tn](el);
}
else
return originalSize.apply(this, [el]);
};
And then tell jsPlumb to connect these two rectangles:
var j2 = jsPlumb.getInstance({
Container:document.getElementById("demo2")
});
j2.connect({
source:"rect1",
target:"rect2",
anchor:"Continuous",
connector:"Bezier",
paintStyle:{ lineWidth:2, strokeStyle:"#456" },
endpoint:["Dot", { radius:2 }]
});
The result is this:
The original version of this post demonstrated a way to connect shapes drawn by Raphael. Unfortunately that no longer
works. We're hoping to revisit this whole post at some point in the future.