--- /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 default Connectors, Endpoint and Overlay definitions.
+ *
+ * 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.
+ */
+
+;(function() {
+
+ /**
+ *
+ * Helper class to consume unused mouse events by components that are DOM elements and
+ * are used by all of the different rendering modes.
+ *
+ */
+ jsPlumb.DOMElementComponent = jsPlumbUtil.extend(jsPlumb.jsPlumbUIComponent, function(params) {
+ // when render mode is canvas, these functions may be called by the canvas mouse handler.
+ // this component is safe to pipe this stuff to /dev/null.
+ this.mousemove =
+ this.dblclick =
+ this.click =
+ this.mousedown =
+ this.mouseup = function(e) { };
+ });
+
+ jsPlumb.Segments = {
+
+ /*
+ * Class: AbstractSegment
+ * A Connector is made up of 1..N Segments, each of which has a Type, such as 'Straight', 'Arc',
+ * 'Bezier'. This is new from 1.4.2, and gives us a lot more flexibility when drawing connections: things such
+ * as rounded corners for flowchart connectors, for example, or a straight line stub for Bezier connections, are
+ * much easier to do now.
+ *
+ * A Segment is responsible for providing coordinates for painting it, and also must be able to report its length.
+ *
+ */
+ AbstractSegment : function(params) {
+ this.params = params;
+
+ /**
+ * Function: findClosestPointOnPath
+ * Finds the closest point on this segment to the given [x, y],
+ * returning both the x and y of the point plus its distance from
+ * the supplied point, and its location along the length of the
+ * path inscribed by the segment. This implementation returns
+ * Infinity for distance and null values for everything else;
+ * subclasses are expected to override.
+ */
+ this.findClosestPointOnPath = function(x, y) {
+ return {
+ d:Infinity,
+ x:null,
+ y:null,
+ l:null
+ };
+ };
+
+ this.getBounds = function() {
+ return {
+ minX:Math.min(params.x1, params.x2),
+ minY:Math.min(params.y1, params.y2),
+ maxX:Math.max(params.x1, params.x2),
+ maxY:Math.max(params.y1, params.y2)
+ };
+ };
+ },
+ Straight : function(params) {
+ var _super = jsPlumb.Segments.AbstractSegment.apply(this, arguments),
+ length, m, m2, x1, x2, y1, y2,
+ _recalc = function() {
+ length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+ m = jsPlumbGeom.gradient({x:x1, y:y1}, {x:x2, y:y2});
+ m2 = -1 / m;
+ };
+
+ this.type = "Straight";
+
+ this.getLength = function() { return length; };
+ this.getGradient = function() { return m; };
+
+ this.getCoordinates = function() {
+ return { x1:x1,y1:y1,x2:x2,y2:y2 };
+ };
+ this.setCoordinates = function(coords) {
+ x1 = coords.x1; y1 = coords.y1; x2 = coords.x2; y2 = coords.y2;
+ _recalc();
+ };
+ this.setCoordinates({x1:params.x1, y1:params.y1, x2:params.x2, y2:params.y2});
+
+ this.getBounds = function() {
+ return {
+ minX:Math.min(x1, x2),
+ minY:Math.min(y1, y2),
+ maxX:Math.max(x1, x2),
+ maxY:Math.max(y1, y2)
+ };
+ };
+
+ /**
+ * returns the point on the segment's path that is 'location' along the length of the path, where 'location' is a decimal from
+ * 0 to 1 inclusive. for the straight line segment this is simple maths.
+ */
+ this.pointOnPath = function(location, absolute) {
+ if (location === 0 && !absolute)
+ return { x:x1, y:y1 };
+ else if (location == 1 && !absolute)
+ return { x:x2, y:y2 };
+ else {
+ var l = absolute ? location > 0 ? location : length + location : location * length;
+ return jsPlumbGeom.pointOnLine({x:x1, y:y1}, {x:x2, y:y2}, l);
+ }
+ };
+
+ /**
+ * returns the gradient of the segment at the given point - which for us is constant.
+ */
+ this.gradientAtPoint = function(_) {
+ return m;
+ };
+
+ /**
+ * returns the point on the segment's path that is 'distance' along the length of the path from 'location', where
+ * 'location' is a decimal from 0 to 1 inclusive, and 'distance' is a number of pixels.
+ * this hands off to jsPlumbUtil to do the maths, supplying two points and the distance.
+ */
+ this.pointAlongPathFrom = function(location, distance, absolute) {
+ var p = this.pointOnPath(location, absolute),
+ farAwayPoint = distance <= 0 ? {x:x1, y:y1} : {x:x2, y:y2 };
+
+ /*
+ location == 1 ? {
+ x:x1 + ((x2 - x1) * 10),
+ y:y1 + ((y1 - y2) * 10)
+ } :
+ */
+
+ if (distance <= 0 && Math.abs(distance) > 1) distance *= -1;
+
+ return jsPlumbGeom.pointOnLine(p, farAwayPoint, distance);
+ };
+
+ // is c between a and b?
+ var within = function(a,b,c) {
+ return c >= Math.min(a,b) && c <= Math.max(a,b);
+ };
+ // find which of a and b is closest to c
+ var closest = function(a,b,c) {
+ return Math.abs(c - a) < Math.abs(c - b) ? a : b;
+ };
+
+ /**
+ Function: findClosestPointOnPath
+ Finds the closest point on this segment to [x,y]. See
+ notes on this method in AbstractSegment.
+ */
+ this.findClosestPointOnPath = function(x, y) {
+ var out = {
+ d:Infinity,
+ x:null,
+ y:null,
+ l:null,
+ x1:x1,
+ x2:x2,
+ y1:y1,
+ y2:y2
+ };
+
+ if (m === 0) {
+ out.y = y1;
+ out.x = within(x1, x2, x) ? x : closest(x1, x2, x);
+ }
+ else if (m == Infinity || m == -Infinity) {
+ out.x = x1;
+ out.y = within(y1, y2, y) ? y : closest(y1, y2, y);
+ }
+ else {
+ // closest point lies on normal from given point to this line.
+ var b = y1 - (m * x1),
+ b2 = y - (m2 * x),
+ // y1 = m.x1 + b and y1 = m2.x1 + b2
+ // so m.x1 + b = m2.x1 + b2
+ // x1(m - m2) = b2 - b
+ // x1 = (b2 - b) / (m - m2)
+ _x1 = (b2 -b) / (m - m2),
+ _y1 = (m * _x1) + b;
+
+ out.x = within(x1,x2,_x1) ? _x1 : closest(x1,x2,_x1);//_x1;
+ out.y = within(y1,y2,_y1) ? _y1 : closest(y1,y2,_y1);//_y1;
+ }
+
+ var fractionInSegment = jsPlumbGeom.lineLength([ out.x, out.y ], [ x1, y1 ]);
+ out.d = jsPlumbGeom.lineLength([x,y], [out.x, out.y]);
+ out.l = fractionInSegment / length;
+ return out;
+ };
+ },
+
+ /*
+ Arc Segment. You need to supply:
+
+ r - radius
+ cx - center x for the arc
+ cy - center y for the arc
+ ac - whether the arc is anticlockwise or not. default is clockwise.
+
+ and then either:
+
+ startAngle - startAngle for the arc.
+ endAngle - endAngle for the arc.
+
+ or:
+
+ x1 - x for start point
+ y1 - y for start point
+ x2 - x for end point
+ y2 - y for end point
+
+ */
+ Arc : function(params) {
+ var _super = jsPlumb.Segments.AbstractSegment.apply(this, arguments),
+ _calcAngle = function(_x, _y) {
+ return jsPlumbGeom.theta([params.cx, params.cy], [_x, _y]);
+ },
+ _calcAngleForLocation = function(segment, location) {
+ if (segment.anticlockwise) {
+ var sa = segment.startAngle < segment.endAngle ? segment.startAngle + TWO_PI : segment.startAngle,
+ s = Math.abs(sa - segment.endAngle);
+ return sa - (s * location);
+ }
+ else {
+ var ea = segment.endAngle < segment.startAngle ? segment.endAngle + TWO_PI : segment.endAngle,
+ ss = Math.abs (ea - segment.startAngle);
+
+ return segment.startAngle + (ss * location);
+ }
+ },
+ TWO_PI = 2 * Math.PI;
+
+ this.radius = params.r;
+ this.anticlockwise = params.ac;
+ this.type = "Arc";
+
+ if (params.startAngle && params.endAngle) {
+ this.startAngle = params.startAngle;
+ this.endAngle = params.endAngle;
+ this.x1 = params.cx + (this.radius * Math.cos(params.startAngle));
+ this.y1 = params.cy + (this.radius * Math.sin(params.startAngle));
+ this.x2 = params.cx + (this.radius * Math.cos(params.endAngle));
+ this.y2 = params.cy + (this.radius * Math.sin(params.endAngle));
+ }
+ else {
+ this.startAngle = _calcAngle(params.x1, params.y1);
+ this.endAngle = _calcAngle(params.x2, params.y2);
+ this.x1 = params.x1;
+ this.y1 = params.y1;
+ this.x2 = params.x2;
+ this.y2 = params.y2;
+ }
+
+ if (this.endAngle < 0) this.endAngle += TWO_PI;
+ if (this.startAngle < 0) this.startAngle += TWO_PI;
+
+ // segment is used by vml
+ this.segment = jsPlumbGeom.quadrant([this.x1, this.y1], [this.x2, this.y2]);
+
+ // we now have startAngle and endAngle as positive numbers, meaning the
+ // absolute difference (|d|) between them is the sweep (s) of this arc, unless the
+ // arc is 'anticlockwise' in which case 's' is given by 2PI - |d|.
+
+ var ea = this.endAngle < this.startAngle ? this.endAngle + TWO_PI : this.endAngle;
+ this.sweep = Math.abs (ea - this.startAngle);
+ if (this.anticlockwise) this.sweep = TWO_PI - this.sweep;
+ var circumference = 2 * Math.PI * this.radius,
+ frac = this.sweep / TWO_PI,
+ length = circumference * frac;
+
+ this.getLength = function() {
+ return length;
+ };
+
+ this.getBounds = function() {
+ return {
+ minX:params.cx - params.r,
+ maxX:params.cx + params.r,
+ minY:params.cy - params.r,
+ maxY:params.cy + params.r
+ };
+ };
+
+ var VERY_SMALL_VALUE = 0.0000000001,
+ gentleRound = function(n) {
+ var f = Math.floor(n), r = Math.ceil(n);
+ if (n - f < VERY_SMALL_VALUE)
+ return f;
+ else if (r - n < VERY_SMALL_VALUE)
+ return r;
+ return n;
+ };
+
+ /**
+ * returns the point on the segment's path that is 'location' along the length of the path, where 'location' is a decimal from
+ * 0 to 1 inclusive.
+ */
+ this.pointOnPath = function(location, absolute) {
+
+ if (location === 0) {
+ return { x:this.x1, y:this.y1, theta:this.startAngle };
+ }
+ else if (location == 1) {
+ return { x:this.x2, y:this.y2, theta:this.endAngle };
+ }
+
+ if (absolute) {
+ location = location / length;
+ }
+
+ var angle = _calcAngleForLocation(this, location),
+ _x = params.cx + (params.r * Math.cos(angle)),
+ _y = params.cy + (params.r * Math.sin(angle));
+
+ return { x:gentleRound(_x), y:gentleRound(_y), theta:angle };
+ };
+
+ /**
+ * returns the gradient of the segment at the given point.
+ */
+ this.gradientAtPoint = function(location, absolute) {
+ var p = this.pointOnPath(location, absolute);
+ var m = jsPlumbGeom.normal( [ params.cx, params.cy ], [p.x, p.y ] );
+ if (!this.anticlockwise && (m == Infinity || m == -Infinity)) m *= -1;
+ return m;
+ };
+
+ this.pointAlongPathFrom = function(location, distance, absolute) {
+ var p = this.pointOnPath(location, absolute),
+ arcSpan = distance / circumference * 2 * Math.PI,
+ dir = this.anticlockwise ? -1 : 1,
+ startAngle = p.theta + (dir * arcSpan),
+ startX = params.cx + (this.radius * Math.cos(startAngle)),
+ startY = params.cy + (this.radius * Math.sin(startAngle));
+
+ return {x:startX, y:startY};
+ };
+ },
+
+ Bezier : function(params) {
+ var _super = jsPlumb.Segments.AbstractSegment.apply(this, arguments),
+ curve = [
+ { x:params.x1, y:params.y1},
+ { x:params.cp1x, y:params.cp1y },
+ { x:params.cp2x, y:params.cp2y },
+ { x:params.x2, y:params.y2 }
+ ],
+ // although this is not a strictly rigorous determination of bounds
+ // of a bezier curve, it works for the types of curves that this segment
+ // type produces.
+ bounds = {
+ minX:Math.min(params.x1, params.x2, params.cp1x, params.cp2x),
+ minY:Math.min(params.y1, params.y2, params.cp1y, params.cp2y),
+ maxX:Math.max(params.x1, params.x2, params.cp1x, params.cp2x),
+ maxY:Math.max(params.y1, params.y2, params.cp1y, params.cp2y)
+ };
+
+ this.type = "Bezier";
+
+ var _translateLocation = function(_curve, location, absolute) {
+ if (absolute)
+ location = jsBezier.locationAlongCurveFrom(_curve, location > 0 ? 0 : 1, location);
+
+ return location;
+ };
+
+ /**
+ * returns the point on the segment's path that is 'location' along the length of the path, where 'location' is a decimal from
+ * 0 to 1 inclusive.
+ */
+ this.pointOnPath = function(location, absolute) {
+ location = _translateLocation(curve, location, absolute);
+ return jsBezier.pointOnCurve(curve, location);
+ };
+
+ /**
+ * returns the gradient of the segment at the given point.
+ */
+ this.gradientAtPoint = function(location, absolute) {
+ location = _translateLocation(curve, location, absolute);
+ return jsBezier.gradientAtPoint(curve, location);
+ };
+
+ this.pointAlongPathFrom = function(location, distance, absolute) {
+ location = _translateLocation(curve, location, absolute);
+ return jsBezier.pointAlongCurveFrom(curve, location, distance);
+ };
+
+ this.getLength = function() {
+ return jsBezier.getLength(curve);
+ };
+
+ this.getBounds = function() {
+ return bounds;
+ };
+ }
+ };
+
+ /*
+ Class: AbstractComponent
+ Superclass for AbstractConnector and AbstractEndpoint.
+ */
+ var AbstractComponent = function() {
+ this.resetBounds = function() {
+ this.bounds = { minX:Infinity, minY:Infinity, maxX:-Infinity, maxY:-Infinity };
+ };
+ this.resetBounds();
+ };
+
+ /*
+ * Class: AbstractConnector
+ * Superclass for all Connectors; here is where Segments are managed. This is exposed on jsPlumb just so it
+ * can be accessed from other files. You should not try to instantiate one of these directly.
+ *
+ * When this class is asked for a pointOnPath, or gradient etc, it must first figure out which segment to dispatch
+ * that request to. This is done by keeping track of the total connector length as segments are added, and also
+ * their cumulative ratios to the total length. Then when the right segment is found it is a simple case of dispatching
+ * the request to it (and adjusting 'location' so that it is relative to the beginning of that segment.)
+ */
+ jsPlumb.Connectors.AbstractConnector = function(params) {
+
+ AbstractComponent.apply(this, arguments);
+
+ var //self = this,
+ segments = [],
+ editing = false,
+ totalLength = 0,
+ segmentProportions = [],
+ segmentProportionalLengths = [],
+ stub = params.stub || 0,
+ sourceStub = jsPlumbUtil.isArray(stub) ? stub[0] : stub,
+ targetStub = jsPlumbUtil.isArray(stub) ? stub[1] : stub,
+ gap = params.gap || 0,
+ sourceGap = jsPlumbUtil.isArray(gap) ? gap[0] : gap,
+ targetGap = jsPlumbUtil.isArray(gap) ? gap[1] : gap,
+ userProvidedSegments = null,
+ edited = false,
+ paintInfo = null;
+
+ // subclasses should override.
+ this.isEditable = function() { return false; };
+ this.setEdited = function(ed) { edited = ed; };
+
+ // to be overridden by subclasses.
+ this.getPath = function() { };
+ this.setPath = function(path) { };
+
+ /**
+ * Function: findSegmentForPoint
+ * Returns the segment that is closest to the given [x,y],
+ * null if nothing found. This function returns a JS
+ * object with:
+ *
+ * d - distance from segment
+ * l - proportional location in segment
+ * x - x point on the segment
+ * y - y point on the segment
+ * s - the segment itself.
+ */
+ this.findSegmentForPoint = function(x, y) {
+ var out = { d:Infinity, s:null, x:null, y:null, l:null };
+ for (var i = 0; i < segments.length; i++) {
+ var _s = segments[i].findClosestPointOnPath(x, y);
+ if (_s.d < out.d) {
+ out.d = _s.d;
+ out.l = _s.l;
+ out.x = _s.x;
+ out.y = _s.y;
+ out.s = segments[i];
+ out.x1 = _s.x1;
+ out.x2 = _s.x2;
+ out.y1 = _s.y1;
+ out.y2 = _s.y2;
+ out.index = i;
+ }
+ }
+
+ return out;
+ };
+
+ var _updateSegmentProportions = function() {
+ var curLoc = 0;
+ for (var i = 0; i < segments.length; i++) {
+ var sl = segments[i].getLength();
+ segmentProportionalLengths[i] = sl / totalLength;
+ segmentProportions[i] = [curLoc, (curLoc += (sl / totalLength)) ];
+ }
+ },
+
+ /**
+ * returns [segment, proportion of travel in segment, segment index] for the segment
+ * that contains the point which is 'location' distance along the entire path, where
+ * 'location' is a decimal between 0 and 1 inclusive. in this connector type, paths
+ * are made up of a list of segments, each of which contributes some fraction to
+ * the total length.
+ * From 1.3.10 this also supports the 'absolute' property, which lets us specify a location
+ * as the absolute distance in pixels, rather than a proportion of the total path.
+ */
+ _findSegmentForLocation = function(location, absolute) {
+ if (absolute) {
+ location = location > 0 ? location / totalLength : (totalLength + location) / totalLength;
+ }
+
+ var idx = segmentProportions.length - 1, inSegmentProportion = 1;
+ //if (location < 1) {
+ for (var i = 0; i < segmentProportions.length; i++) {
+ if (segmentProportions[i][1] >= location) {
+ idx = i;
+ // todo is this correct for all connector path types?
+ inSegmentProportion = location == 1 ? 1 : location === 0 ? 0 : (location - segmentProportions[i][0]) / segmentProportionalLengths[i];
+ break;
+ }
+ }
+ //}
+ return { segment:segments[idx], proportion:inSegmentProportion, index:idx };
+ },
+ _addSegment = function(conn, type, params) {
+ if (params.x1 == params.x2 && params.y1 == params.y2) return;
+ var s = new jsPlumb.Segments[type](params);
+ segments.push(s);
+ totalLength += s.getLength();
+ conn.updateBounds(s);
+ },
+ _clearSegments = function() {
+ totalLength = 0;
+ segments.splice(0, segments.length);
+ segmentProportions.splice(0, segmentProportions.length);
+ segmentProportionalLengths.splice(0, segmentProportionalLengths.length);
+ };
+
+ this.setSegments = function(_segs) {
+ userProvidedSegments = [];
+ totalLength = 0;
+ for (var i = 0; i < _segs.length; i++) {
+ userProvidedSegments.push(_segs[i]);
+ totalLength += _segs[i].getLength();
+ }
+ };
+
+ var _prepareCompute = function(params) {
+ this.lineWidth = params.lineWidth;
+ var segment = jsPlumbGeom.quadrant(params.sourcePos, params.targetPos),
+ swapX = params.targetPos[0] < params.sourcePos[0],
+ swapY = params.targetPos[1] < params.sourcePos[1],
+ lw = params.lineWidth || 1,
+ so = params.sourceEndpoint.anchor.getOrientation(params.sourceEndpoint),
+ to = params.targetEndpoint.anchor.getOrientation(params.targetEndpoint),
+ x = swapX ? params.targetPos[0] : params.sourcePos[0],
+ y = swapY ? params.targetPos[1] : params.sourcePos[1],
+ w = Math.abs(params.targetPos[0] - params.sourcePos[0]),
+ h = Math.abs(params.targetPos[1] - params.sourcePos[1]);
+
+ // if either anchor does not have an orientation set, we derive one from their relative
+ // positions. we fix the axis to be the one in which the two elements are further apart, and
+ // point each anchor at the other element. this is also used when dragging a new connection.
+ if (so[0] === 0 && so[1] === 0 || to[0] === 0 && to[1] === 0) {
+ var index = w > h ? 0 : 1, oIndex = [1,0][index];
+ so = []; to = [];
+ so[index] = params.sourcePos[index] > params.targetPos[index] ? -1 : 1;
+ to[index] = params.sourcePos[index] > params.targetPos[index] ? 1 : -1;
+ so[oIndex] = 0; to[oIndex] = 0;
+ }
+
+ var sx = swapX ? w + (sourceGap * so[0]) : sourceGap * so[0],
+ sy = swapY ? h + (sourceGap * so[1]) : sourceGap * so[1],
+ tx = swapX ? targetGap * to[0] : w + (targetGap * to[0]),
+ ty = swapY ? targetGap * to[1] : h + (targetGap * to[1]),
+ oProduct = ((so[0] * to[0]) + (so[1] * to[1]));
+
+ var result = {
+ sx:sx, sy:sy, tx:tx, ty:ty, lw:lw,
+ xSpan:Math.abs(tx - sx),
+ ySpan:Math.abs(ty - sy),
+ mx:(sx + tx) / 2,
+ my:(sy + ty) / 2,
+ so:so, to:to, x:x, y:y, w:w, h:h,
+ segment : segment,
+ startStubX : sx + (so[0] * sourceStub),
+ startStubY : sy + (so[1] * sourceStub),
+ endStubX : tx + (to[0] * targetStub),
+ endStubY : ty + (to[1] * targetStub),
+ isXGreaterThanStubTimes2 : Math.abs(sx - tx) > (sourceStub + targetStub),
+ isYGreaterThanStubTimes2 : Math.abs(sy - ty) > (sourceStub + targetStub),
+ opposite:oProduct == -1,
+ perpendicular:oProduct === 0,
+ orthogonal:oProduct == 1,
+ sourceAxis : so[0] === 0 ? "y" : "x",
+ points:[x, y, w, h, sx, sy, tx, ty ]
+ };
+ result.anchorOrientation = result.opposite ? "opposite" : result.orthogonal ? "orthogonal" : "perpendicular";
+ return result;
+ };
+
+ this.getSegments = function() { return segments; };
+
+ this.updateBounds = function(segment) {
+ var segBounds = segment.getBounds();
+ this.bounds.minX = Math.min(this.bounds.minX, segBounds.minX);
+ this.bounds.maxX = Math.max(this.bounds.maxX, segBounds.maxX);
+ this.bounds.minY = Math.min(this.bounds.minY, segBounds.minY);
+ this.bounds.maxY = Math.max(this.bounds.maxY, segBounds.maxY);
+ };
+
+ var dumpSegmentsToConsole = function() {
+ console.log("SEGMENTS:");
+ for (var i = 0; i < segments.length; i++) {
+ console.log(segments[i].type, segments[i].getLength(), segmentProportions[i]);
+ }
+ };
+
+ this.pointOnPath = function(location, absolute) {
+ var seg = _findSegmentForLocation(location, absolute);
+ return seg.segment && seg.segment.pointOnPath(seg.proportion, absolute) || [0,0];
+ };
+
+ this.gradientAtPoint = function(location) {
+ var seg = _findSegmentForLocation(location, absolute);
+ return seg.segment && seg.segment.gradientAtPoint(seg.proportion, absolute) || 0;
+ };
+
+ this.pointAlongPathFrom = function(location, distance, absolute) {
+ var seg = _findSegmentForLocation(location, absolute);
+ // TODO what happens if this crosses to the next segment?
+ return seg.segment && seg.segment.pointAlongPathFrom(seg.proportion, distance, false) || [0,0];
+ };
+
+ this.compute = function(params) {
+ if (!edited)
+ paintInfo = _prepareCompute.call(this, params);
+
+ _clearSegments();
+ this._compute(paintInfo, params);
+ this.x = paintInfo.points[0];
+ this.y = paintInfo.points[1];
+ this.w = paintInfo.points[2];
+ this.h = paintInfo.points[3];
+ this.segment = paintInfo.segment;
+ _updateSegmentProportions();
+ };
+
+ return {
+ addSegment:_addSegment,
+ prepareCompute:_prepareCompute,
+ sourceStub:sourceStub,
+ targetStub:targetStub,
+ maxStub:Math.max(sourceStub, targetStub),
+ sourceGap:sourceGap,
+ targetGap:targetGap,
+ maxGap:Math.max(sourceGap, targetGap)
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Connectors.AbstractConnector, AbstractComponent);
+
+ /**
+ * Class: Connectors.Straight
+ * The Straight connector draws a simple straight line between the two anchor points. It does not have any constructor parameters.
+ */
+ var Straight = jsPlumb.Connectors.Straight = function() {
+ this.type = "Straight";
+ var _super = jsPlumb.Connectors.AbstractConnector.apply(this, arguments);
+
+ this._compute = function(paintInfo, _) {
+ _super.addSegment(this, "Straight", {x1:paintInfo.sx, y1:paintInfo.sy, x2:paintInfo.startStubX, y2:paintInfo.startStubY});
+ _super.addSegment(this, "Straight", {x1:paintInfo.startStubX, y1:paintInfo.startStubY, x2:paintInfo.endStubX, y2:paintInfo.endStubY});
+ _super.addSegment(this, "Straight", {x1:paintInfo.endStubX, y1:paintInfo.endStubY, x2:paintInfo.tx, y2:paintInfo.ty});
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Connectors.Straight, jsPlumb.Connectors.AbstractConnector);
+ jsPlumb.registerConnectorType(Straight, "Straight");
+
+ /**
+ * Class:Connectors.Bezier
+ * This Connector draws a Bezier curve with two control points. You can provide a 'curviness' value which gets applied to jsPlumb's
+ * internal voodoo machine and ends up generating locations for the two control points. See the constructor documentation below.
+ */
+ /**
+ * Function:Constructor
+ *
+ * Parameters:
+ * curviness - How 'curvy' you want the curve to be! This is a directive for the placement of control points, not endpoints of the curve, so your curve does not
+ * actually touch the given point, but it has the tendency to lean towards it. The larger this value, the greater the curve is pulled from a straight line.
+ * Optional; defaults to 150.
+ * stub - optional value for a distance to travel from the connector's endpoint before beginning the Bezier curve. defaults to 0.
+ *
+ */
+ var Bezier = function(params) {
+ params = params || {};
+
+ var _super = jsPlumb.Connectors.AbstractConnector.apply(this, arguments),
+ stub = params.stub || 50,
+ majorAnchor = params.curviness || 150,
+ minorAnchor = 10;
+
+ this.type = "Bezier";
+ this.getCurviness = function() { return majorAnchor; };
+
+ this._findControlPoint = function(point, sourceAnchorPosition, targetAnchorPosition, sourceEndpoint, targetEndpoint) {
+ // determine if the two anchors are perpendicular to each other in their orientation. we swap the control
+ // points around if so (code could be tightened up)
+ var soo = sourceEndpoint.anchor.getOrientation(sourceEndpoint),
+ too = targetEndpoint.anchor.getOrientation(targetEndpoint),
+ perpendicular = soo[0] != too[0] || soo[1] == too[1],
+ p = [];
+
+ if (!perpendicular) {
+ if (soo[0] === 0) // X
+ p.push(sourceAnchorPosition[0] < targetAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
+ else p.push(point[0] - (majorAnchor * soo[0]));
+
+ if (soo[1] === 0) // Y
+ p.push(sourceAnchorPosition[1] < targetAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
+ else p.push(point[1] + (majorAnchor * too[1]));
+ }
+ else {
+ if (too[0] === 0) // X
+ p.push(targetAnchorPosition[0] < sourceAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
+ else p.push(point[0] + (majorAnchor * too[0]));
+
+ if (too[1] === 0) // Y
+ p.push(targetAnchorPosition[1] < sourceAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
+ else p.push(point[1] + (majorAnchor * soo[1]));
+ }
+
+ return p;
+ };
+
+ this._compute = function(paintInfo, p) {
+ var sp = p.sourcePos,
+ tp = p.targetPos,
+ _w = Math.abs(sp[0] - tp[0]),
+ _h = Math.abs(sp[1] - tp[1]),
+ _sx = sp[0] < tp[0] ? _w : 0,
+ _sy = sp[1] < tp[1] ? _h : 0,
+ _tx = sp[0] < tp[0] ? 0 : _w,
+ _ty = sp[1] < tp[1] ? 0 : _h,
+ _CP = this._findControlPoint([_sx, _sy], sp, tp, p.sourceEndpoint, p.targetEndpoint),
+ _CP2 = this._findControlPoint([_tx, _ty], tp, sp, p.targetEndpoint, p.sourceEndpoint);
+
+ _super.addSegment(this, "Bezier", {
+ x1:_sx, y1:_sy, x2:_tx, y2:_ty,
+ cp1x:_CP[0], cp1y:_CP[1], cp2x:_CP2[0], cp2y:_CP2[1]
+ });
+ };
+ };
+ jsPlumbUtil.extend(Bezier, jsPlumb.Connectors.AbstractConnector);
+ jsPlumb.registerConnectorType(Bezier, "Bezier");
+
+ // ********************************* END OF CONNECTOR TYPES *******************************************************************
+
+ // ********************************* ENDPOINT TYPES *******************************************************************
+
+ jsPlumb.Endpoints.AbstractEndpoint = function(params) {
+ AbstractComponent.apply(this, arguments);
+ var compute = this.compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ var out = this._compute.apply(this, arguments);
+ this.x = out[0];
+ this.y = out[1];
+ this.w = out[2];
+ this.h = out[3];
+ this.bounds.minX = this.x;
+ this.bounds.minY = this.y;
+ this.bounds.maxX = this.x + this.w;
+ this.bounds.maxY = this.y + this.h;
+ return out;
+ };
+ return {
+ compute:compute,
+ cssClass:params.cssClass
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Endpoints.AbstractEndpoint, AbstractComponent);
+
+ /**
+ * Class: Endpoints.Dot
+ * A round endpoint, with default radius 10 pixels.
+ */
+
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * radius - radius of the endpoint. defaults to 10 pixels.
+ */
+ jsPlumb.Endpoints.Dot = function(params) {
+ this.type = "Dot";
+ var _super = jsPlumb.Endpoints.AbstractEndpoint.apply(this, arguments);
+ params = params || {};
+ this.radius = params.radius || 10;
+ this.defaultOffset = 0.5 * this.radius;
+ this.defaultInnerRadius = this.radius / 3;
+
+ this._compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ this.radius = endpointStyle.radius || this.radius;
+ var x = anchorPoint[0] - this.radius,
+ y = anchorPoint[1] - this.radius,
+ w = this.radius * 2,
+ h = this.radius * 2;
+
+ if (endpointStyle.strokeStyle) {
+ var lw = endpointStyle.lineWidth || 1;
+ x -= lw;
+ y -= lw;
+ w += (lw * 2);
+ h += (lw * 2);
+ }
+ return [ x, y, w, h, this.radius ];
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Endpoints.Dot, jsPlumb.Endpoints.AbstractEndpoint);
+
+ /**
+ * Class: Endpoints.Rectangle
+ * A Rectangular Endpoint, with default size 20x20.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * width - width of the endpoint. defaults to 20 pixels.
+ * height - height of the endpoint. defaults to 20 pixels.
+ */
+ jsPlumb.Endpoints.Rectangle = function(params) {
+ this.type = "Rectangle";
+ var _super = jsPlumb.Endpoints.AbstractEndpoint.apply(this, arguments);
+ params = params || {};
+ this.width = params.width || 20;
+ this.height = params.height || 20;
+
+ this._compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ var width = endpointStyle.width || this.width,
+ height = endpointStyle.height || this.height,
+ x = anchorPoint[0] - (width/2),
+ y = anchorPoint[1] - (height/2);
+
+ return [ x, y, width, height];
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Endpoints.Rectangle, jsPlumb.Endpoints.AbstractEndpoint);
+
+
+ var DOMElementEndpoint = function(params) {
+ jsPlumb.DOMElementComponent.apply(this, arguments);
+ this._jsPlumb.displayElements = [ ];
+ };
+ jsPlumbUtil.extend(DOMElementEndpoint, jsPlumb.DOMElementComponent, {
+ // jsPlumb.Endpoints.AbstractEndpoint
+ getDisplayElements : function() {
+ return this._jsPlumb.displayElements;
+ },
+ appendDisplayElement : function(el) {
+ this._jsPlumb.displayElements.push(el);
+ }
+ });
+
+ /**
+ * Class: Endpoints.Image
+ * Draws an image as the Endpoint.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * src - location of the image to use.
+
+ TODO: multiple references to self. not sure quite how to get rid of them entirely. perhaps self = null in the cleanup
+ function will suffice
+
+ TODO this class still leaks memory.
+
+ */
+ jsPlumb.Endpoints.Image = function(params) {
+
+ this.type = "Image";
+ DOMElementEndpoint.apply(this, arguments);
+ jsPlumb.Endpoints.AbstractEndpoint.apply(this, arguments);
+
+ var _onload = params.onload,
+ src = params.src || params.url,
+ parent = params.parent,
+ clazz = params.cssClass ? " " + params.cssClass : "";
+
+ this._jsPlumb.img = new Image();
+ this._jsPlumb.ready = false;
+ this._jsPlumb.initialized = false;
+ this._jsPlumb.deleted = false;
+ this._jsPlumb.widthToUse = params.width;
+ this._jsPlumb.heightToUse = params.height;
+ this._jsPlumb.endpoint = params.endpoint;
+
+ this._jsPlumb.img.onload = function() {
+ // check we weren't actually discarded before use (in fact mostly happens in tests)
+ if (this._jsPlumb != null) {
+ this._jsPlumb.ready = true;
+ this._jsPlumb.widthToUse = this._jsPlumb.widthToUse || this._jsPlumb.img.width;
+ this._jsPlumb.heightToUse = this._jsPlumb.heightToUse || this._jsPlumb.img.height;
+ if (_onload) {
+ _onload(this);
+ }
+ }
+ }.bind(this);
+
+ /*
+ Function: setImage
+ Sets the Image to use in this Endpoint.
+
+ Parameters:
+ img - may be a URL or an Image object
+ onload - optional; a callback to execute once the image has loaded.
+ */
+ this._jsPlumb.endpoint.setImage = function(_img, onload) {
+ var s = _img.constructor == String ? _img : _img.src;
+ _onload = onload;
+ this._jsPlumb.img.src = s;
+
+ if (this.canvas != null)
+ this.canvas.setAttribute("src", this._jsPlumb.img.src);
+ }.bind(this);
+
+ this._jsPlumb.endpoint.setImage(src, _onload);
+ /*
+ var s = src.constructor == String ? src : src.src;
+ //_onload = onload;
+ this._jsPlumb.img.src = src;
+
+ if (this.canvas != null)
+ this.canvas.setAttribute("src", this._jsPlumb.img.src);
+ // }.bind(this);
+
+ //this._jsPlumb.endpoint.setImage(src, _onload);*/
+
+ this._compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ this.anchorPoint = anchorPoint;
+ if (this._jsPlumb.ready) return [anchorPoint[0] - this._jsPlumb.widthToUse / 2, anchorPoint[1] - this._jsPlumb.heightToUse / 2,
+ this._jsPlumb.widthToUse, this._jsPlumb.heightToUse];
+ else return [0,0,0,0];
+ };
+
+ this.canvas = document.createElement("img");
+ this.canvas.style.margin = 0;
+ this.canvas.style.padding = 0;
+ this.canvas.style.outline = 0;
+ this.canvas.style.position = "absolute";
+ this.canvas.className = this._jsPlumb.instance.endpointClass + clazz;
+ if (this._jsPlumb.widthToUse) this.canvas.setAttribute("width", this._jsPlumb.widthToUse);
+ if (this._jsPlumb.heightToUse) this.canvas.setAttribute("height", this._jsPlumb.heightToUse);
+ this._jsPlumb.instance.appendElement(this.canvas, parent);
+ this.attachListeners(this.canvas, this);
+
+ this.actuallyPaint = function(d, style, anchor) {
+ if (!this._jsPlumb.deleted) {
+ if (!this._jsPlumb.initialized) {
+ this.canvas.setAttribute("src", this._jsPlumb.img.src);
+ this.appendDisplayElement(this.canvas);
+ this._jsPlumb.initialized = true;
+ }
+ var x = this.anchorPoint[0] - (this._jsPlumb.widthToUse / 2),
+ y = this.anchorPoint[1] - (this._jsPlumb.heightToUse / 2);
+ jsPlumbUtil.sizeElement(this.canvas, x, y, this._jsPlumb.widthToUse, this._jsPlumb.heightToUse);
+ }
+ };
+
+ this.paint = function(style, anchor) {
+ if (this._jsPlumb != null) { // may have been deleted
+ if (this._jsPlumb.ready) {
+ this.actuallyPaint(style, anchor);
+ }
+ else {
+ window.setTimeout(function() {
+ this.paint(style, anchor);
+ }.bind(this), 200);
+ }
+ }
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Endpoints.Image, [ DOMElementEndpoint, jsPlumb.Endpoints.AbstractEndpoint ], {
+ cleanup : function() {
+ this._jsPlumb.deleted = true;
+ jsPlumbUtil.removeElement(this.canvas);
+ this.canvas = null;
+ }
+ });
+
+ /*
+ * Class: Endpoints.Blank
+ * An Endpoint that paints nothing (visible) on the screen. Supports cssClass and hoverClass parameters like all Endpoints.
+ */
+ jsPlumb.Endpoints.Blank = function(params) {
+ var _super = jsPlumb.Endpoints.AbstractEndpoint.apply(this, arguments);
+ this.type = "Blank";
+ DOMElementEndpoint.apply(this, arguments);
+ this._compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ return [anchorPoint[0], anchorPoint[1],10,0];
+ };
+
+ this.canvas = document.createElement("div");
+ this.canvas.style.display = "block";
+ this.canvas.style.width = "1px";
+ this.canvas.style.height = "1px";
+ this.canvas.style.background = "transparent";
+ this.canvas.style.position = "absolute";
+ this.canvas.className = this._jsPlumb.endpointClass;
+ jsPlumb.appendElement(this.canvas, params.parent);
+
+ this.paint = function(style, anchor) {
+ jsPlumbUtil.sizeElement(this.canvas, this.x, this.y, this.w, this.h);
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Endpoints.Blank, [jsPlumb.Endpoints.AbstractEndpoint, DOMElementEndpoint], {
+ cleanup:function() {
+ if (this.canvas && this.canvas.parentNode) {
+ this.canvas.parentNode.removeChild(this.canvas);
+ }
+ }
+ });
+
+ /*
+ * Class: Endpoints.Triangle
+ * A triangular Endpoint.
+ */
+ /*
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * width - width of the triangle's base. defaults to 55 pixels.
+ * height - height of the triangle from base to apex. defaults to 55 pixels.
+ */
+ jsPlumb.Endpoints.Triangle = function(params) {
+ this.type = "Triangle";
+ var _super = jsPlumb.Endpoints.AbstractEndpoint.apply(this, arguments);
+ params = params || { };
+ params.width = params.width || 55;
+ params.height = params.height || 55;
+ this.width = params.width;
+ this.height = params.height;
+ this._compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ var width = endpointStyle.width || self.width,
+ height = endpointStyle.height || self.height,
+ x = anchorPoint[0] - (width/2),
+ y = anchorPoint[1] - (height/2);
+ return [ x, y, width, height ];
+ };
+ };
+// ********************************* END OF ENDPOINT TYPES *******************************************************************
+
+
+// ********************************* OVERLAY DEFINITIONS ***********************************************************************
+
+ var AbstractOverlay = jsPlumb.Overlays.AbstractOverlay = function(params) {
+ this.visible = true;
+ this.isAppendedAtTopLevel = true;
+ this.component = params.component;
+ this.loc = params.location == null ? 0.5 : params.location;
+ this.endpointLoc = params.endpointLocation == null ? [ 0.5, 0.5] : params.endpointLocation;
+ };
+ AbstractOverlay.prototype = {
+ cleanup:function() {
+ this.component = null;
+ this.canvas = null;
+ this.endpointLoc = null;
+ },
+ setVisible : function(val) {
+ this.visible = val;
+ // TODO this is only actually necessary for canvas. so, the Canvas overlay should
+ // override setVisible and call this.
+ //this.component.repaint();
+ },
+ isVisible : function() { return this.visible; },
+ hide : function() { this.setVisible(false); },
+ show : function() { this.setVisible(true); },
+ incrementLocation : function(amount) {
+ this.loc += amount;
+ this.component.repaint();
+ },
+ setLocation : function(l) {
+ this.loc = l;
+ this.component.repaint();
+ },
+ getLocation : function() {
+ return this.loc;
+ }
+ };
+
+
+ /*
+ * Class: Overlays.Arrow
+ *
+ * An arrow overlay, defined by four points: the head, the two sides of the tail, and a 'foldback' point at some distance along the length
+ * of the arrow that lines from each tail point converge into. The foldback point is defined using a decimal that indicates some fraction
+ * of the length of the arrow and has a default value of 0.623. A foldback point value of 1 would mean that the arrow had a straight line
+ * across the tail.
+ */
+ /*
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * length - distance in pixels from head to tail baseline. default 20.
+ * width - width in pixels of the tail baseline. default 20.
+ * fillStyle - style to use when filling the arrow. defaults to "black".
+ * strokeStyle - style to use when stroking the arrow. defaults to null, which means the arrow is not stroked.
+ * lineWidth - line width to use when stroking the arrow. defaults to 1, but only used if strokeStyle is not null.
+ * foldback - distance (as a decimal from 0 to 1 inclusive) along the length of the arrow marking the point the tail points should fold back to. defaults to 0.623.
+ * location - distance (as a decimal from 0 to 1 inclusive) marking where the arrow should sit on the connector. defaults to 0.5.
+ * direction - indicates the direction the arrow points in. valid values are -1 and 1; 1 is default.
+ */
+ jsPlumb.Overlays.Arrow = function(params) {
+ this.type = "Arrow";
+ AbstractOverlay.apply(this, arguments);
+ this.isAppendedAtTopLevel = false;
+ params = params || {};
+ var _ju = jsPlumbUtil, _jg = jsPlumbGeom;
+
+ this.length = params.length || 20;
+ this.width = params.width || 20;
+ this.id = params.id;
+ var direction = (params.direction || 1) < 0 ? -1 : 1,
+ paintStyle = params.paintStyle || { lineWidth:1 },
+ // how far along the arrow the lines folding back in come to. default is 62.3%.
+ foldback = params.foldback || 0.623;
+
+ this.computeMaxSize = function() { return self.width * 1.5; };
+ //this.cleanup = function() { }; // nothing to clean up for Arrows
+ this.draw = function(component, currentConnectionPaintStyle) {
+
+ var hxy, mid, txy, tail, cxy;
+ if (component.pointAlongPathFrom) {
+
+ if (_ju.isString(this.loc) || this.loc > 1 || this.loc < 0) {
+ var l = parseInt(this.loc, 10),
+ fromLoc = this.loc < 0 ? 1 : 0;
+ hxy = component.pointAlongPathFrom(fromLoc, l, false);
+ mid = component.pointAlongPathFrom(fromLoc, l - (direction * this.length / 2), false);
+ txy = _jg.pointOnLine(hxy, mid, this.length);
+ }
+ else if (this.loc == 1) {
+ hxy = component.pointOnPath(this.loc);
+ mid = component.pointAlongPathFrom(this.loc, -(this.length));
+ txy = _jg.pointOnLine(hxy, mid, this.length);
+
+ if (direction == -1) {
+ var _ = txy;
+ txy = hxy;
+ hxy = _;
+ }
+ }
+ else if (this.loc === 0) {
+ txy = component.pointOnPath(this.loc);
+ mid = component.pointAlongPathFrom(this.loc, this.length);
+ hxy = _jg.pointOnLine(txy, mid, this.length);
+ if (direction == -1) {
+ var __ = txy;
+ txy = hxy;
+ hxy = __;
+ }
+ }
+ else {
+ hxy = component.pointAlongPathFrom(this.loc, direction * this.length / 2);
+ mid = component.pointOnPath(this.loc);
+ txy = _jg.pointOnLine(hxy, mid, this.length);
+ }
+
+ tail = _jg.perpendicularLineTo(hxy, txy, this.width);
+ cxy = _jg.pointOnLine(hxy, txy, foldback * this.length);
+
+ var d = { hxy:hxy, tail:tail, cxy:cxy },
+ strokeStyle = paintStyle.strokeStyle || currentConnectionPaintStyle.strokeStyle,
+ fillStyle = paintStyle.fillStyle || currentConnectionPaintStyle.strokeStyle,
+ lineWidth = paintStyle.lineWidth || currentConnectionPaintStyle.lineWidth,
+ info = {
+ component:component,
+ d:d,
+ lineWidth:lineWidth,
+ strokeStyle:strokeStyle,
+ fillStyle:fillStyle,
+ minX:Math.min(hxy.x, tail[0].x, tail[1].x),
+ maxX:Math.max(hxy.x, tail[0].x, tail[1].x),
+ minY:Math.min(hxy.y, tail[0].y, tail[1].y),
+ maxY:Math.max(hxy.y, tail[0].y, tail[1].y)
+ };
+
+ return info;
+ }
+ else return {component:component, minX:0,maxX:0,minY:0,maxY:0};
+ };
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.Arrow, AbstractOverlay);
+
+ /*
+ * Class: Overlays.PlainArrow
+ *
+ * A basic arrow. This is in fact just one instance of the more generic case in which the tail folds back on itself to some
+ * point along the length of the arrow: in this case, that foldback point is the full length of the arrow. so it just does
+ * a 'call' to Arrow with foldback set appropriately.
+ */
+ /*
+ * Function: Constructor
+ * See <Overlays.Arrow> for allowed parameters for this overlay.
+ */
+ jsPlumb.Overlays.PlainArrow = function(params) {
+ params = params || {};
+ var p = jsPlumb.extend(params, {foldback:1});
+ jsPlumb.Overlays.Arrow.call(this, p);
+ this.type = "PlainArrow";
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.PlainArrow, jsPlumb.Overlays.Arrow);
+
+ /*
+ * Class: Overlays.Diamond
+ *
+ * A diamond. Like PlainArrow, this is a concrete case of the more generic case of the tail points converging on some point...it just
+ * happens that in this case, that point is greater than the length of the the arrow.
+ *
+ * this could probably do with some help with positioning...due to the way it reuses the Arrow paint code, what Arrow thinks is the
+ * center is actually 1/4 of the way along for this guy. but we don't have any knowledge of pixels at this point, so we're kind of
+ * stuck when it comes to helping out the Arrow class. possibly we could pass in a 'transpose' parameter or something. the value
+ * would be -l/4 in this case - move along one quarter of the total length.
+ */
+ /*
+ * Function: Constructor
+ * See <Overlays.Arrow> for allowed parameters for this overlay.
+ */
+ jsPlumb.Overlays.Diamond = function(params) {
+ params = params || {};
+ var l = params.length || 40,
+ p = jsPlumb.extend(params, {length:l/2, foldback:2});
+ jsPlumb.Overlays.Arrow.call(this, p);
+ this.type = "Diamond";
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.Diamond, jsPlumb.Overlays.Arrow);
+
+ var _getDimensions = function(component) {
+ if (component._jsPlumb.cachedDimensions == null)
+ component._jsPlumb.cachedDimensions = component.getDimensions();
+ return component._jsPlumb.cachedDimensions;
+ };
+
+ // abstract superclass for overlays that add an element to the DOM.
+ var AbstractDOMOverlay = function(params) {
+ jsPlumb.DOMElementComponent.apply(this, arguments);
+ AbstractOverlay.apply(this, arguments);
+
+ var jpcl = jsPlumb.CurrentLibrary;
+ this.id = params.id;
+ this._jsPlumb.div = null;
+ this._jsPlumb.initialised = false;
+ this._jsPlumb.component = params.component;
+ this._jsPlumb.cachedDimensions = null;
+ this._jsPlumb.create = params.create;
+
+ this.getElement = function() {
+ if (this._jsPlumb.div == null) {
+ var div = this._jsPlumb.div = jpcl.getDOMElement(this._jsPlumb.create(this._jsPlumb.component));
+ div.style.position = "absolute";
+ var clazz = params._jsPlumb.overlayClass + " " +
+ (this.cssClass ? this.cssClass :
+ params.cssClass ? params.cssClass : "");
+ div.className = clazz;
+ this._jsPlumb.instance.appendElement(div, this._jsPlumb.component.parent);
+ this._jsPlumb.instance.getId(div);
+ this.attachListeners(div, this);
+ this.canvas = div;
+ }
+ return this._jsPlumb.div;
+ };
+
+ this.draw = function(component, currentConnectionPaintStyle, absolutePosition) {
+ var td = _getDimensions(this);
+ if (td != null && td.length == 2) {
+ var cxy = { x:0,y:0 };
+
+ // absolutePosition would have been set by a call to connection.setAbsoluteOverlayPosition.
+ if (absolutePosition) {
+ cxy = { x:absolutePosition[0], y:absolutePosition[1] };
+ }
+ else if (component.pointOnPath) {
+ var loc = this.loc, absolute = false;
+ if (jsPlumbUtil.isString(this.loc) || this.loc < 0 || this.loc > 1) {
+ loc = parseInt(this.loc, 10);
+ absolute = true;
+ }
+ cxy = component.pointOnPath(loc, absolute); // a connection
+ }
+ else {
+ var locToUse = this.loc.constructor == Array ? this.loc : this.endpointLoc;
+ cxy = { x:locToUse[0] * component.w,
+ y:locToUse[1] * component.h };
+ }
+
+ var minx = cxy.x - (td[0] / 2),
+ miny = cxy.y - (td[1] / 2);
+
+ return {
+ component:component,
+ d:{ minx:minx, miny:miny, td:td, cxy:cxy },
+ minX:minx,
+ maxX:minx + td[0],
+ minY:miny,
+ maxY:miny + td[1]
+ };
+ }
+ else return {minX:0,maxX:0,minY:0,maxY:0};
+ };
+ };
+ jsPlumbUtil.extend(AbstractDOMOverlay, [jsPlumb.DOMElementComponent, AbstractOverlay], {
+ getDimensions : function() {
+ return jsPlumb.CurrentLibrary.getSize(jsPlumb.CurrentLibrary.getElementObject(this.getElement()));
+ },
+ setVisible : function(state) {
+ this._jsPlumb.div.style.display = state ? "block" : "none";
+ },
+ /*
+ * Function: clearCachedDimensions
+ * Clears the cached dimensions for the label. As a performance enhancement, label dimensions are
+ * cached from 1.3.12 onwards. The cache is cleared when you change the label text, of course, but
+ * there are other reasons why the text dimensions might change - if you make a change through CSS, for
+ * example, you might change the font size. in that case you should explicitly call this method.
+ */
+ clearCachedDimensions : function() {
+ this._jsPlumb.cachedDimensions = null;
+ },
+ cleanup : function() {
+ if (this._jsPlumb.div != null)
+ jsPlumb.CurrentLibrary.removeElement(this._jsPlumb.div);
+ },
+ computeMaxSize : function() {
+ var td = _getDimensions(this);
+ return Math.max(td[0], td[1]);
+ },
+ reattachListeners : function(connector) {
+ if (this._jsPlumb.div) {
+ this.reattachListenersForElement(this._jsPlumb.div, this, connector);
+ }
+ },
+ paint : function(p, containerExtents) {
+ if (!this._jsPlumb.initialised) {
+ this.getElement();
+ p.component.appendDisplayElement(this._jsPlumb.div);
+ this.attachListeners(this._jsPlumb.div, p.component);
+ this._jsPlumb.initialised = true;
+ }
+ this._jsPlumb.div.style.left = (p.component.x + p.d.minx) + "px";
+ this._jsPlumb.div.style.top = (p.component.y + p.d.miny) + "px";
+ }
+ });
+
+ /*
+ * Class: Overlays.Custom
+ * A Custom overlay. You supply a 'create' function which returns some DOM element, and jsPlumb positions it.
+ * The 'create' function is passed a Connection or Endpoint.
+ */
+ /*
+ * Function: Constructor
+ *
+ * Parameters:
+ * create - function for jsPlumb to call that returns a DOM element.
+ * location - distance (as a decimal from 0 to 1 inclusive) marking where the label should sit on the connector. defaults to 0.5.
+ * id - optional id to use for later retrieval of this overlay.
+ *
+ */
+ jsPlumb.Overlays.Custom = function(params) {
+ this.type = "Custom";
+ AbstractDOMOverlay.apply(this, arguments);
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.Custom, AbstractDOMOverlay);
+
+ jsPlumb.Overlays.GuideLines = function() {
+ var self = this;
+ self.length = 50;
+ self.lineWidth = 5;
+ this.type = "GuideLines";
+ AbstractOverlay.apply(this, arguments);
+ jsPlumb.jsPlumbUIComponent.apply(this, arguments);
+ this.draw = function(connector, currentConnectionPaintStyle) {
+
+ var head = connector.pointAlongPathFrom(self.loc, self.length / 2),
+ mid = connector.pointOnPath(self.loc),
+ tail = jsPlumbGeom.pointOnLine(head, mid, self.length),
+ tailLine = jsPlumbGeom.perpendicularLineTo(head, tail, 40),
+ headLine = jsPlumbGeom.perpendicularLineTo(tail, head, 20);
+
+ return {
+ connector:connector,
+ head:head,
+ tail:tail,
+ headLine:headLine,
+ tailLine:tailLine,
+ minX:Math.min(head.x, tail.x, headLine[0].x, headLine[1].x),
+ minY:Math.min(head.y, tail.y, headLine[0].y, headLine[1].y),
+ maxX:Math.max(head.x, tail.x, headLine[0].x, headLine[1].x),
+ maxY:Math.max(head.y, tail.y, headLine[0].y, headLine[1].y)
+ };
+ };
+
+ // this.cleanup = function() { }; // nothing to clean up for GuideLines
+ };
+
+ /*
+ * Class: Overlays.Label
+
+ */
+ /*
+ * Function: Constructor
+ *
+ * Parameters:
+ * cssClass - optional css class string to append to css class. This string is appended "as-is", so you can of course have multiple classes
+ * defined. This parameter is preferred to using labelStyle, borderWidth and borderStyle.
+ * label - the label to paint. May be a string or a function that returns a string. Nothing will be painted if your label is null or your
+ * label function returns null. empty strings _will_ be painted.
+ * location - distance (as a decimal from 0 to 1 inclusive) marking where the label should sit on the connector. defaults to 0.5.
+ * id - optional id to use for later retrieval of this overlay.
+ *
+ *
+ */
+ jsPlumb.Overlays.Label = function(params) {
+ this.labelStyle = params.labelStyle;
+
+ var labelWidth = null, labelHeight = null, labelText = null, labelPadding = null;
+ this.cssClass = this.labelStyle != null ? this.labelStyle.cssClass : null;
+ var p = jsPlumb.extend({
+ create : function() {
+ return document.createElement("div");
+ }}, params);
+ jsPlumb.Overlays.Custom.call(this, p);
+ this.type = "Label";
+ this.label = params.label || "";
+ this.labelText = null;
+ if (this.labelStyle) {
+ var el = this.getElement();
+ this.labelStyle.font = this.labelStyle.font || "12px sans-serif";
+ el.style.font = this.labelStyle.font;
+ el.style.color = this.labelStyle.color || "black";
+ if (this.labelStyle.fillStyle) el.style.background = this.labelStyle.fillStyle;
+ if (this.labelStyle.borderWidth > 0) {
+ var dStyle = this.labelStyle.borderStyle ? this.labelStyle.borderStyle : "black";
+ el.style.border = this.labelStyle.borderWidth + "px solid " + dStyle;
+ }
+ if (this.labelStyle.padding) el.style.padding = this.labelStyle.padding;
+ }
+
+ };
+ jsPlumbUtil.extend(jsPlumb.Overlays.Label, jsPlumb.Overlays.Custom, {
+ cleanup:function() {
+ this.div = null;
+ this.label = null;
+ this.labelText = null;
+ this.cssClass = null;
+ this.labelStyle = null;
+ },
+ getLabel : function() {
+ return this.label;
+ },
+ /*
+ * Function: setLabel
+ * sets the label's, um, label. you would think i'd call this function
+ * 'setText', but you can pass either a Function or a String to this, so
+ * it makes more sense as 'setLabel'. This uses innerHTML on the label div, so keep
+ * that in mind if you need escaped HTML.
+ */
+ setLabel : function(l) {
+ this.label = l;
+ this.labelText = null;
+ this.clearCachedDimensions();
+ this.update();
+ this.component.repaint();
+ },
+ getDimensions : function() {
+ this.update();
+ return AbstractDOMOverlay.prototype.getDimensions.apply(this, arguments);
+ },
+ update : function() {
+ if (typeof this.label == "function") {
+ var lt = this.label(this);
+ this.getElement().innerHTML = lt.replace(/\r\n/g, "<br/>");
+ }
+ else {
+ if (this.labelText == null) {
+ this.labelText = this.label;
+ this.getElement().innerHTML = this.labelText.replace(/\r\n/g, "<br/>");
+ }
+ }
+ }
+ });
+
+ // ********************************* END OF OVERLAY DEFINITIONS ***********************************************************************
+
+})();
\ No newline at end of file