6 * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
9 * This file contains the code for creating and manipulating anchors.
11 * Copyright (c) 2010 - 2013 Simon Porritt (simon.porritt@gmail.com)
14 * http://github.com/sporritt/jsplumb
15 * http://code.google.com/p/jsplumb
17 * Dual licensed under the MIT and GPL2 licenses.
22 // manages anchors for all elements.
24 jsPlumb.AnchorManager = function(params) {
25 var _amEndpoints = {},
26 continuousAnchors = {},
27 continuousAnchorLocations = {},
28 userDefinedContinuousAnchorLocations = {},
29 continuousAnchorOrientations = {},
30 Orientation = { HORIZONTAL : "horizontal", VERTICAL : "vertical", DIAGONAL : "diagonal", IDENTITY:"identity" },
31 connectionsByElementId = {},
34 jsPlumbInstance = params.jsPlumbInstance,
35 jpcl = jsPlumb.CurrentLibrary,
36 floatingConnections = {},
37 // TODO this functions uses a crude method of determining orientation between two elements.
38 // 'diagonal' should be chosen when the angle of the line between the two centers is around
39 // one of 45, 135, 225 and 315 degrees. maybe +- 15 degrees.
40 // used by AnchorManager.redraw
41 calculateOrientation = function(sourceId, targetId, sd, td, sourceAnchor, targetAnchor) {
43 if (sourceId === targetId) return {
44 orientation:Orientation.IDENTITY,
48 var theta = Math.atan2((td.centery - sd.centery) , (td.centerx - sd.centerx)),
49 theta2 = Math.atan2((sd.centery - td.centery) , (sd.centerx - td.centerx)),
50 h = ((sd.left <= td.left && sd.right >= td.left) || (sd.left <= td.right && sd.right >= td.right) ||
51 (sd.left <= td.left && sd.right >= td.right) || (td.left <= sd.left && td.right >= sd.right)),
52 v = ((sd.top <= td.top && sd.bottom >= td.top) || (sd.top <= td.bottom && sd.bottom >= td.bottom) ||
53 (sd.top <= td.top && sd.bottom >= td.bottom) || (td.top <= sd.top && td.bottom >= sd.bottom)),
54 possiblyTranslateEdges = function(edges) {
55 // this function checks to see if either anchor is Continuous, and if so, runs the suggested edge
56 // through the anchor: Continuous anchors can say which faces they support, and they get to choose
57 // whether a certain face is honoured, or, if not, which face to replace it with. the behaviour when
58 // choosing an alternate face is to try for the opposite face first, then the next one clockwise, and then
59 // the opposite of that one.
61 sourceAnchor.isContinuous ? sourceAnchor.verifyEdge(edges[0]) : edges[0],
62 targetAnchor.isContinuous ? targetAnchor.verifyEdge(edges[1]) : edges[1]
66 orientation:Orientation.DIAGONAL,
72 if (td.left > sd.left && td.top > sd.top)
73 out.a = ["right", "top"];
74 else if (td.left > sd.left && sd.top > td.top)
75 out.a = [ "top", "left"];
76 else if (td.left < sd.left && td.top < sd.top)
77 out.a = [ "top", "right"];
78 else if (td.left < sd.left && td.top > sd.top)
79 out.a = ["left", "top" ];
82 out.orientation = Orientation.HORIZONTAL;
83 out.a = sd.top < td.top ? ["bottom", "top"] : ["top", "bottom"];
86 out.orientation = Orientation.VERTICAL;
87 out.a = sd.left < td.left ? ["right", "left"] : ["left", "right"];
90 out.a = possiblyTranslateEdges(out.a);
93 // used by placeAnchors function
94 placeAnchorsOnLine = function(desc, elementDimensions, elementPosition,
95 connections, horizontal, otherMultiplier, reverse) {
96 var a = [], step = elementDimensions[horizontal ? 0 : 1] / (connections.length + 1);
98 for (var i = 0; i < connections.length; i++) {
99 var val = (i + 1) * step, other = otherMultiplier * elementDimensions[horizontal ? 1 : 0];
101 val = elementDimensions[horizontal ? 0 : 1] - val;
103 var dx = (horizontal ? val : other), x = elementPosition[0] + dx, xp = dx / elementDimensions[0],
104 dy = (horizontal ? other : val), y = elementPosition[1] + dy, yp = dy / elementDimensions[1];
106 a.push([ x, y, xp, yp, connections[i][1], connections[i][2] ]);
111 // used by edgeSortFunctions
112 currySort = function(reverseAngles) {
113 return function(a,b) {
116 /*if (a[0][0] < b[0][0])
119 r = a[0][1] > b[0][1];*/
120 r = a[0][0] < b[0][0];
123 /*if (a[0][0] > b[0][0])
126 r =a[0][1] > b[0][1];
128 r = a[0][0] > b[0][0];
130 return r === false ? -1 : 1;
133 // used by edgeSortFunctions
134 leftSort = function(a,b) {
135 // first get adjusted values
136 var p1 = a[0][0] < 0 ? -Math.PI - a[0][0] : Math.PI - a[0][0],
137 p2 = b[0][0] < 0 ? -Math.PI - b[0][0] : Math.PI - b[0][0];
138 if (p1 > p2) return 1;
139 else return a[0][1] > b[0][1] ? 1 : -1;
141 // used by placeAnchors
142 edgeSortFunctions = {
143 "top":function(a, b) { return a[0] > b[0] ? 1 : -1; },
144 "right":currySort(true),
145 "bottom":currySort(true),
148 // used by placeAnchors
149 _sortHelper = function(_array, _fn) { return _array.sort(_fn); },
150 // used by AnchorManager.redraw
151 placeAnchors = function(elementId, _anchorLists) {
152 var cd = jsPlumbInstance.getCachedData(elementId), sS = cd.s, sO = cd.o,
153 placeSomeAnchors = function(desc, elementDimensions, elementPosition, unsortedConnections, isHorizontal, otherMultiplier, orientation) {
154 if (unsortedConnections.length > 0) {
155 var sc = _sortHelper(unsortedConnections, edgeSortFunctions[desc]), // puts them in order based on the target element's pos on screen
156 reverse = desc === "right" || desc === "top",
157 anchors = placeAnchorsOnLine(desc, elementDimensions,
159 isHorizontal, otherMultiplier, reverse );
161 // takes a computed anchor position and adjusts it for parent offset and scroll, then stores it.
162 var _setAnchorLocation = function(endpoint, anchorPos) {
163 var a = jsPlumbInstance.adjustForParentOffsetAndScroll([anchorPos[0], anchorPos[1]], endpoint.canvas);
164 continuousAnchorLocations[endpoint.id] = [ a[0], a[1], anchorPos[2], anchorPos[3] ];
165 continuousAnchorOrientations[endpoint.id] = orientation;
168 for (var i = 0; i < anchors.length; i++) {
169 var c = anchors[i][4], weAreSource = c.endpoints[0].elementId === elementId, weAreTarget = c.endpoints[1].elementId === elementId;
171 _setAnchorLocation(c.endpoints[0], anchors[i]);
172 else if (weAreTarget)
173 _setAnchorLocation(c.endpoints[1], anchors[i]);
178 placeSomeAnchors("bottom", sS, [sO.left,sO.top], _anchorLists.bottom, true, 1, [0,1]);
179 placeSomeAnchors("top", sS, [sO.left,sO.top], _anchorLists.top, true, 0, [0,-1]);
180 placeSomeAnchors("left", sS, [sO.left,sO.top], _anchorLists.left, false, 0, [-1,0]);
181 placeSomeAnchors("right", sS, [sO.left,sO.top], _anchorLists.right, false, 1, [1,0]);
184 this.reset = function() {
186 connectionsByElementId = {};
189 this.addFloatingConnection = function(key, conn) {
190 floatingConnections[key] = conn;
192 this.removeFloatingConnection = function(key) {
193 delete floatingConnections[key];
195 this.newConnection = function(conn) {
196 var sourceId = conn.sourceId, targetId = conn.targetId,
198 doRegisterTarget = true,
199 registerConnection = function(otherIndex, otherEndpoint, otherAnchor, elId, c) {
200 if ((sourceId == targetId) && otherAnchor.isContinuous){
201 // remove the target endpoint's canvas. we dont need it.
202 jpcl.removeElement(ep[1].canvas);
203 doRegisterTarget = false;
205 jsPlumbUtil.addToList(connectionsByElementId, elId, [c, otherEndpoint, otherAnchor.constructor == jsPlumb.DynamicAnchor]);
208 registerConnection(0, ep[0], ep[0].anchor, targetId, conn);
209 if (doRegisterTarget)
210 registerConnection(1, ep[1], ep[1].anchor, sourceId, conn);
212 var removeEndpointFromAnchorLists = function(endpoint) {
213 (function(list, eId) {
214 if (list) { // transient anchors dont get entries in this list.
215 var f = function(e) { return e[4] == eId; };
216 jsPlumbUtil.removeWithFunction(list.top, f);
217 jsPlumbUtil.removeWithFunction(list.left, f);
218 jsPlumbUtil.removeWithFunction(list.bottom, f);
219 jsPlumbUtil.removeWithFunction(list.right, f);
221 })(anchorLists[endpoint.elementId], endpoint.id);
223 this.connectionDetached = function(connInfo) {
224 var connection = connInfo.connection || connInfo,
225 sourceId = connInfo.sourceId,
226 targetId = connInfo.targetId,
227 ep = connection.endpoints,
228 removeConnection = function(otherIndex, otherEndpoint, otherAnchor, elId, c) {
229 if (otherAnchor != null && otherAnchor.constructor == jsPlumb.FloatingAnchor) {
233 jsPlumbUtil.removeWithFunction(connectionsByElementId[elId], function(_c) {
234 return _c[0].id == c.id;
239 removeConnection(1, ep[1], ep[1].anchor, sourceId, connection);
240 removeConnection(0, ep[0], ep[0].anchor, targetId, connection);
242 // remove from anchorLists
243 removeEndpointFromAnchorLists(connection.endpoints[0]);
244 removeEndpointFromAnchorLists(connection.endpoints[1]);
246 self.redraw(connection.sourceId);
247 self.redraw(connection.targetId);
249 this.add = function(endpoint, elementId) {
250 jsPlumbUtil.addToList(_amEndpoints, elementId, endpoint);
252 this.changeId = function(oldId, newId) {
253 connectionsByElementId[newId] = connectionsByElementId[oldId];
254 _amEndpoints[newId] = _amEndpoints[oldId];
255 delete connectionsByElementId[oldId];
256 delete _amEndpoints[oldId];
258 this.getConnectionsFor = function(elementId) {
259 return connectionsByElementId[elementId] || [];
261 this.getEndpointsFor = function(elementId) {
262 return _amEndpoints[elementId] || [];
264 this.deleteEndpoint = function(endpoint) {
265 jsPlumbUtil.removeWithFunction(_amEndpoints[endpoint.elementId], function(e) {
266 return e.id == endpoint.id;
268 removeEndpointFromAnchorLists(endpoint);
270 this.clearFor = function(elementId) {
271 delete _amEndpoints[elementId];
272 _amEndpoints[elementId] = [];
274 // updates the given anchor list by either updating an existing anchor's info, or adding it. this function
275 // also removes the anchor from its previous list, if the edge it is on has changed.
276 // all connections found along the way (those that are connected to one of the faces this function
277 // operates on) are added to the connsToPaint list, as are their endpoints. in this way we know to repaint
278 // them wthout having to calculate anything else about them.
279 var _updateAnchorList = function(lists, theta, order, conn, aBoolean, otherElId, idx, reverse, edgeId, elId, connsToPaint, endpointsToPaint) {
280 // first try to find the exact match, but keep track of the first index of a matching element id along the way.s
282 firstMatchingElIdx = -1,
283 endpoint = conn.endpoints[idx],
284 endpointId = endpoint.id,
286 values = [ [ theta, order ], conn, aBoolean, otherElId, endpointId ],
287 listToAddTo = lists[edgeId],
288 listToRemoveFrom = endpoint._continuousAnchorEdge ? lists[endpoint._continuousAnchorEdge] : null;
290 if (listToRemoveFrom) {
291 var rIdx = jsPlumbUtil.findWithFunction(listToRemoveFrom, function(e) { return e[4] == endpointId; });
293 listToRemoveFrom.splice(rIdx, 1);
294 // get all connections from this list
295 for (var i = 0; i < listToRemoveFrom.length; i++) {
296 jsPlumbUtil.addWithFunction(connsToPaint, listToRemoveFrom[i][1], function(c) { return c.id == listToRemoveFrom[i][1].id; });
297 jsPlumbUtil.addWithFunction(endpointsToPaint, listToRemoveFrom[i][1].endpoints[idx], function(e) { return e.id == listToRemoveFrom[i][1].endpoints[idx].id; });
298 jsPlumbUtil.addWithFunction(endpointsToPaint, listToRemoveFrom[i][1].endpoints[oIdx], function(e) { return e.id == listToRemoveFrom[i][1].endpoints[oIdx].id; });
303 for (i = 0; i < listToAddTo.length; i++) {
304 if (params.idx == 1 && listToAddTo[i][3] === otherElId && firstMatchingElIdx == -1)
305 firstMatchingElIdx = i;
306 jsPlumbUtil.addWithFunction(connsToPaint, listToAddTo[i][1], function(c) { return c.id == listToAddTo[i][1].id; });
307 jsPlumbUtil.addWithFunction(endpointsToPaint, listToAddTo[i][1].endpoints[idx], function(e) { return e.id == listToAddTo[i][1].endpoints[idx].id; });
308 jsPlumbUtil.addWithFunction(endpointsToPaint, listToAddTo[i][1].endpoints[oIdx], function(e) { return e.id == listToAddTo[i][1].endpoints[oIdx].id; });
310 if (exactIdx != -1) {
311 listToAddTo[exactIdx] = values;
314 var insertIdx = reverse ? firstMatchingElIdx != -1 ? firstMatchingElIdx : 0 : listToAddTo.length; // of course we will get this from having looked through the array shortly.
315 listToAddTo.splice(insertIdx, 0, values);
318 // store this for next time.
319 endpoint._continuousAnchorEdge = edgeId;
323 // find the entry in an endpoint's list for this connection and update its target endpoint
324 // with the current target in the connection.
327 this.updateOtherEndpoint = function(elId, oldTargetId, newTargetId, connection) {
328 var sIndex = jsPlumbUtil.findWithFunction(connectionsByElementId[elId], function(i) {
329 return i[0].id === connection.id;
331 tIndex = jsPlumbUtil.findWithFunction(connectionsByElementId[oldTargetId], function(i) {
332 return i[0].id === connection.id;
335 // update or add data for source
337 connectionsByElementId[elId][sIndex][0] = connection;
338 connectionsByElementId[elId][sIndex][1] = connection.endpoints[1];
339 connectionsByElementId[elId][sIndex][2] = connection.endpoints[1].anchor.constructor == jsPlumb.DynamicAnchor;
342 // remove entry for previous target (if there)
345 connectionsByElementId[oldTargetId].splice(tIndex, 1);
346 // add entry for new target
347 jsPlumbUtil.addToList(connectionsByElementId, newTargetId, [connection, connection.endpoints[0], connection.endpoints[0].anchor.constructor == jsPlumb.DynamicAnchor]);
352 // notification that the connection given has changed source from the originalId to the newId.
354 // 1. removing the connection from the list of connections stored for the originalId
355 // 2. updating the source information for the target of the connection
356 // 3. re-registering the connection in connectionsByElementId with the newId
358 this.sourceChanged = function(originalId, newId, connection) {
359 // remove the entry that points from the old source to the target
360 jsPlumbUtil.removeWithFunction(connectionsByElementId[originalId], function(info) {
361 return info[0].id === connection.id;
363 // find entry for target and update it
364 var tIdx = jsPlumbUtil.findWithFunction(connectionsByElementId[connection.targetId], function(i) {
365 return i[0].id === connection.id;
368 connectionsByElementId[connection.targetId][tIdx][0] = connection;
369 connectionsByElementId[connection.targetId][tIdx][1] = connection.endpoints[0];
370 connectionsByElementId[connection.targetId][tIdx][2] = connection.endpoints[0].anchor.constructor == jsPlumb.DynamicAnchor;
372 // add entry for new source
373 jsPlumbUtil.addToList(connectionsByElementId, newId, [connection, connection.endpoints[1], connection.endpoints[1].anchor.constructor == jsPlumb.DynamicAnchor]);
377 // moves the given endpoint from `currentId` to `element`.
380 // 1. changing the key in _amEndpoints under which the endpoint is stored
381 // 2. changing the source or target values in all of the endpoint's connections
382 // 3. changing the array in connectionsByElementId in which the endpoint's connections
383 // are stored (done by either sourceChanged or updateOtherEndpoint)
385 this.rehomeEndpoint = function(ep, currentId, element) {
386 var eps = _amEndpoints[currentId] || [],
387 elementId = jsPlumbInstance.getId(element);
389 if (elementId !== currentId) {
390 var idx = jsPlumbUtil.indexOf(eps, ep);
392 var _ep = eps.splice(idx, 1)[0];
393 self.add(_ep, elementId);
397 for (var i = 0; i < ep.connections.length; i++) {
398 if (ep.connections[i].sourceId == currentId) {
399 ep.connections[i].sourceId = ep.elementId;
400 ep.connections[i].source = ep.element;
401 self.sourceChanged(currentId, ep.elementId, ep.connections[i]);
403 else if(ep.connections[i].targetId == currentId) {
404 ep.connections[i].targetId = ep.elementId;
405 ep.connections[i].target = ep.element;
406 self.updateOtherEndpoint(ep.connections[i].sourceId, currentId, ep.elementId, ep.connections[i]);
411 this.redraw = function(elementId, ui, timestamp, offsetToUI, clearEdits, doNotRecalcEndpoint) {
413 if (!jsPlumbInstance.isSuspendDrawing()) {
414 // get all the endpoints for this element
415 var ep = _amEndpoints[elementId] || [],
416 endpointConnections = connectionsByElementId[elementId] || [],
417 connectionsToPaint = [],
418 endpointsToPaint = [],
419 anchorsToUpdate = [];
421 timestamp = timestamp || jsPlumbInstance.timestamp();
422 // offsetToUI are values that would have been calculated in the dragManager when registering
423 // an endpoint for an element that had a parent (somewhere in the hierarchy) that had been
424 // registered as draggable.
425 offsetToUI = offsetToUI || {left:0, top:0};
428 left:ui.left + offsetToUI.left,
429 top:ui.top + offsetToUI.top
433 // valid for one paint cycle.
434 var myOffset = jsPlumbInstance.updateOffset( { elId : elementId, offset : ui, recalc : false, timestamp : timestamp }),
435 orientationCache = {};
437 // actually, first we should compute the orientation of this element to all other elements to which
438 // this element is connected with a continuous anchor (whether both ends of the connection have
439 // a continuous anchor or just one)
441 for (var i = 0; i < endpointConnections.length; i++) {
442 var conn = endpointConnections[i][0],
443 sourceId = conn.sourceId,
444 targetId = conn.targetId,
445 sourceContinuous = conn.endpoints[0].anchor.isContinuous,
446 targetContinuous = conn.endpoints[1].anchor.isContinuous;
448 if (sourceContinuous || targetContinuous) {
449 var oKey = sourceId + "_" + targetId,
450 oKey2 = targetId + "_" + sourceId,
451 o = orientationCache[oKey],
452 oIdx = conn.sourceId == elementId ? 1 : 0;
454 if (sourceContinuous && !anchorLists[sourceId]) anchorLists[sourceId] = { top:[], right:[], bottom:[], left:[] };
455 if (targetContinuous && !anchorLists[targetId]) anchorLists[targetId] = { top:[], right:[], bottom:[], left:[] };
457 if (elementId != targetId) jsPlumbInstance.updateOffset( { elId : targetId, timestamp : timestamp });
458 if (elementId != sourceId) jsPlumbInstance.updateOffset( { elId : sourceId, timestamp : timestamp });
460 var td = jsPlumbInstance.getCachedData(targetId),
461 sd = jsPlumbInstance.getCachedData(sourceId);
463 if (targetId == sourceId && (sourceContinuous || targetContinuous)) {
464 // here we may want to improve this by somehow determining the face we'd like
465 // to put the connector on. ideally, when drawing, the face should be calculated
466 // by determining which face is closest to the point at which the mouse button
467 // was released. for now, we're putting it on the top face.
469 anchorLists[sourceId],
475 0, false, "top", sourceId, connectionsToPaint, endpointsToPaint);
479 o = calculateOrientation(sourceId, targetId, sd.o, td.o, conn.endpoints[0].anchor, conn.endpoints[1].anchor);
480 orientationCache[oKey] = o;
481 // this would be a performance enhancement, but the computed angles need to be clamped to
482 //the (-PI/2 -> PI/2) range in order for the sorting to work properly.
483 /* orientationCache[oKey2] = {
484 orientation:o.orientation,
486 theta:o.theta + Math.PI,
487 theta2:o.theta2 + Math.PI
490 if (sourceContinuous) _updateAnchorList(anchorLists[sourceId], o.theta, 0, conn, false, targetId, 0, false, o.a[0], sourceId, connectionsToPaint, endpointsToPaint);
491 if (targetContinuous) _updateAnchorList(anchorLists[targetId], o.theta2, -1, conn, true, sourceId, 1, true, o.a[1], targetId, connectionsToPaint, endpointsToPaint);
494 if (sourceContinuous) jsPlumbUtil.addWithFunction(anchorsToUpdate, sourceId, function(a) { return a === sourceId; });
495 if (targetContinuous) jsPlumbUtil.addWithFunction(anchorsToUpdate, targetId, function(a) { return a === targetId; });
496 jsPlumbUtil.addWithFunction(connectionsToPaint, conn, function(c) { return c.id == conn.id; });
497 if ((sourceContinuous && oIdx === 0) || (targetContinuous && oIdx === 1))
498 jsPlumbUtil.addWithFunction(endpointsToPaint, conn.endpoints[oIdx], function(e) { return e.id == conn.endpoints[oIdx].id; });
501 // place Endpoints whose anchors are continuous but have no Connections
502 for (i = 0; i < ep.length; i++) {
503 if (ep[i].connections.length === 0 && ep[i].anchor.isContinuous) {
504 if (!anchorLists[elementId]) anchorLists[elementId] = { top:[], right:[], bottom:[], left:[] };
505 _updateAnchorList(anchorLists[elementId], -Math.PI / 2, 0, {endpoints:[ep[i], ep[i]], paint:function(){}}, false, elementId, 0, false, "top", elementId, connectionsToPaint, endpointsToPaint);
506 jsPlumbUtil.addWithFunction(anchorsToUpdate, elementId, function(a) { return a === elementId; });
509 // now place all the continuous anchors we need to;
510 for (i = 0; i < anchorsToUpdate.length; i++) {
511 placeAnchors(anchorsToUpdate[i], anchorLists[anchorsToUpdate[i]]);
514 // now that continuous anchors have been placed, paint all the endpoints for this element
515 // TODO performance: add the endpoint ids to a temp array, and then when iterating in the next
516 // loop, check that we didn't just paint that endpoint. we can probably shave off a few more milliseconds this way.
517 for (i = 0; i < ep.length; i++) {
518 ep[i].paint( { timestamp : timestamp, offset : myOffset, dimensions : myOffset.s, recalc:doNotRecalcEndpoint !== true });
520 // ... and any other endpoints we came across as a result of the continuous anchors.
521 for (i = 0; i < endpointsToPaint.length; i++) {
522 var cd = jsPlumbInstance.getCachedData(endpointsToPaint[i].elementId);
523 // dont use timestamp for this endpoint, as it is not for the current element and we may
524 // have needed to recalculate anchor position due to the element for the endpoint moving.
525 //endpointsToPaint[i].paint( { timestamp : null, offset : cd, dimensions : cd.s });
527 endpointsToPaint[i].paint( { timestamp : timestamp, offset : cd, dimensions : cd.s });
530 // paint all the standard and "dynamic connections", which are connections whose other anchor is
531 // static and therefore does need to be recomputed; we make sure that happens only one time.
533 // TODO we could have compiled a list of these in the first pass through connections; might save some time.
534 for (i = 0; i < endpointConnections.length; i++) {
535 var otherEndpoint = endpointConnections[i][1];
536 if (otherEndpoint.anchor.constructor == jsPlumb.DynamicAnchor) {
537 otherEndpoint.paint({ elementWithPrecedence:elementId, timestamp:timestamp });
538 jsPlumbUtil.addWithFunction(connectionsToPaint, endpointConnections[i][0], function(c) { return c.id == endpointConnections[i][0].id; });
539 // all the connections for the other endpoint now need to be repainted
540 for (var k = 0; k < otherEndpoint.connections.length; k++) {
541 if (otherEndpoint.connections[k] !== endpointConnections[i][0])
542 jsPlumbUtil.addWithFunction(connectionsToPaint, otherEndpoint.connections[k], function(c) { return c.id == otherEndpoint.connections[k].id; });
544 } else if (otherEndpoint.anchor.constructor == jsPlumb.Anchor) {
545 jsPlumbUtil.addWithFunction(connectionsToPaint, endpointConnections[i][0], function(c) { return c.id == endpointConnections[i][0].id; });
548 // paint current floating connection for this element, if there is one.
549 var fc = floatingConnections[elementId];
551 fc.paint({timestamp:timestamp, recalc:false, elId:elementId});
553 // paint all the connections
554 for (i = 0; i < connectionsToPaint.length; i++) {
555 // if not a connection between the two elements in question dont use the timestamp.
556 var ts =timestamp;// ((connectionsToPaint[i].sourceId == sourceId && connectionsToPaint[i].targetId == targetId) ||
557 //(connectionsToPaint[i].sourceId == targetId && connectionsToPaint[i].targetId == sourceId)) ? timestamp : null;
558 connectionsToPaint[i].paint({elId:elementId, timestamp:ts, recalc:false, clearEdits:clearEdits});
563 var ContinuousAnchor = function(anchorParams) {
564 jsPlumbUtil.EventGenerator.apply(this);
565 this.type = "Continuous";
566 this.isDynamic = true;
567 this.isContinuous = true;
568 var faces = anchorParams.faces || ["top", "right", "bottom", "left"],
569 clockwise = !(anchorParams.clockwise === false),
570 availableFaces = { },
571 opposites = { "top":"bottom", "right":"left","left":"right","bottom":"top" },
572 clockwiseOptions = { "top":"right", "right":"bottom","left":"top","bottom":"left" },
573 antiClockwiseOptions = { "top":"left", "right":"top","left":"bottom","bottom":"right" },
574 secondBest = clockwise ? clockwiseOptions : antiClockwiseOptions,
575 lastChoice = clockwise ? antiClockwiseOptions : clockwiseOptions,
576 cssClass = anchorParams.cssClass || "";
578 for (var i = 0; i < faces.length; i++) { availableFaces[faces[i]] = true; }
580 // if the given edge is supported, returns it. otherwise looks for a substitute that _is_
581 // supported. if none supported we also return the request edge.
582 this.verifyEdge = function(edge) {
583 if (availableFaces[edge]) return edge;
584 else if (availableFaces[opposites[edge]]) return opposites[edge];
585 else if (availableFaces[secondBest[edge]]) return secondBest[edge];
586 else if (availableFaces[lastChoice[edge]]) return lastChoice[edge];
587 return edge; // we have to give them something.
590 this.compute = function(params) {
591 return userDefinedContinuousAnchorLocations[params.element.id] || continuousAnchorLocations[params.element.id] || [0,0];
593 this.getCurrentLocation = function(params) {
594 return userDefinedContinuousAnchorLocations[params.element.id] || continuousAnchorLocations[params.element.id] || [0,0];
596 this.getOrientation = function(endpoint) {
597 return continuousAnchorOrientations[endpoint.id] || [0,0];
599 this.clearUserDefinedLocation = function() {
600 delete userDefinedContinuousAnchorLocations[anchorParams.elementId];
602 this.setUserDefinedLocation = function(loc) {
603 userDefinedContinuousAnchorLocations[anchorParams.elementId] = loc;
605 this.getCssClass = function() { return cssClass; };
606 this.setCssClass = function(c) { cssClass = c; };
609 // continuous anchors
610 jsPlumbInstance.continuousAnchorFactory = {
611 get:function(params) {
612 var existing = continuousAnchors[params.elementId];
614 existing = new ContinuousAnchor(params);
615 continuousAnchors[params.elementId] = existing;
619 clear:function(elementId) {
620 delete continuousAnchors[elementId];
626 * Anchors model a position on some element at which an Endpoint may be located. They began as a first class citizen of jsPlumb, ie. a user
627 * was required to create these themselves, but over time this has been replaced by the concept of referring to them either by name (eg. "TopMiddle"),
628 * or by an array describing their coordinates (eg. [ 0, 0.5, 0, -1 ], which is the same as "TopMiddle"). jsPlumb now handles all of the
629 * creation of Anchors without user intervention.
631 jsPlumb.Anchor = function(params) {
632 this.x = params.x || 0;
633 this.y = params.y || 0;
634 this.elementId = params.elementId;
635 this.cssClass = params.cssClass || "";
636 this.userDefinedLocation = null;
637 this.orientation = params.orientation || [ 0, 0 ];
639 jsPlumbUtil.EventGenerator.apply(this);
641 var jsPlumbInstance = params.jsPlumbInstance;//,
642 //lastTimestamp = null;//, lastReturnValue = null;
644 this.lastReturnValue = null;
645 this.offsets = params.offsets || [ 0, 0 ];
646 this.timestamp = null;
647 this.compute = function(params) {
649 var xy = params.xy, wh = params.wh, element = params.element, timestamp = params.timestamp;
651 if(params.clearUserDefinedLocation)
652 this.userDefinedLocation = null;
654 if (timestamp && timestamp === self.timestamp)
655 return this.lastReturnValue;
657 if (this.userDefinedLocation != null) {
658 this.lastReturnValue = this.userDefinedLocation;
662 this.lastReturnValue = [ xy[0] + (this.x * wh[0]) + this.offsets[0], xy[1] + (this.y * wh[1]) + this.offsets[1] ];
663 // adjust loc if there is an offsetParent
664 this.lastReturnValue = jsPlumbInstance.adjustForParentOffsetAndScroll(this.lastReturnValue, element.canvas);
667 this.timestamp = timestamp;
668 return this.lastReturnValue;
671 this.getCurrentLocation = function(params) {
672 return (this.lastReturnValue == null || (params.timestamp != null && this.timestamp != params.timestamp)) ? this.compute(params) : this.lastReturnValue;
675 jsPlumbUtil.extend(jsPlumb.Anchor, jsPlumbUtil.EventGenerator, {
676 equals : function(anchor) {
677 if (!anchor) return false;
678 var ao = anchor.getOrientation(),
679 o = this.getOrientation();
680 return this.x == anchor.x && this.y == anchor.y && this.offsets[0] == anchor.offsets[0] && this.offsets[1] == anchor.offsets[1] && o[0] == ao[0] && o[1] == ao[1];
682 getUserDefinedLocation : function() {
683 return this.userDefinedLocation;
685 setUserDefinedLocation : function(l) {
686 this.userDefinedLocation = l;
688 clearUserDefinedLocation : function() {
689 this.userDefinedLocation = null;
691 getOrientation : function(_endpoint) { return this.orientation; },
692 getCssClass : function() { return this.cssClass; }
696 * An Anchor that floats. its orientation is computed dynamically from
697 * its position relative to the anchor it is floating relative to. It is used when creating
698 * a connection through drag and drop.
700 * TODO FloatingAnchor could totally be refactored to extend Anchor just slightly.
702 jsPlumb.FloatingAnchor = function(params) {
704 jsPlumb.Anchor.apply(this, arguments);
706 // this is the anchor that this floating anchor is referenced to for
707 // purposes of calculating the orientation.
708 var ref = params.reference,
709 jpcl = jsPlumb.CurrentLibrary,
710 jsPlumbInstance = params.jsPlumbInstance,
711 // the canvas this refers to.
712 refCanvas = params.referenceCanvas,
713 size = jpcl.getSize(jpcl.getElementObject(refCanvas)),
714 // these are used to store the current relative position of our
715 // anchor wrt the reference anchor. they only indicate
716 // direction, so have a value of 1 or -1 (or, very rarely, 0). these
717 // values are written by the compute method, and read
718 // by the getOrientation method.
720 // temporary member used to store an orientation when the floating
721 // anchor is hovering over another anchor.
725 // clear from parent. we want floating anchor orientation to always be computed.
726 this.orientation = null;
728 // set these to 0 each; they are used by certain types of connectors in the loopback case,
729 // when the connector is trying to clear the element it is on. but for floating anchor it's not
731 this.x = 0; this.y = 0;
733 this.isFloating = true;
735 this.compute = function(params) {
736 var xy = params.xy, element = params.element,
737 result = [ xy[0] + (size[0] / 2), xy[1] + (size[1] / 2) ]; // return origin of the element. we may wish to improve this so that any object can be the drag proxy.
739 // adjust loc if there is an offsetParent
740 result = jsPlumbInstance.adjustForParentOffsetAndScroll(result, element.canvas);
742 _lastResult = result;
746 this.getOrientation = function(_endpoint) {
747 if (orientation) return orientation;
749 var o = ref.getOrientation(_endpoint);
750 // here we take into account the orientation of the other
751 // anchor: if it declares zero for some direction, we declare zero too. this might not be the most awesome. perhaps we can come
752 // up with a better way. it's just so that the line we draw looks like it makes sense. maybe this wont make sense.
753 return [ Math.abs(o[0]) * xDir * -1,
754 Math.abs(o[1]) * yDir * -1 ];
759 * notification the endpoint associated with this anchor is hovering
760 * over another anchor; we want to assume that anchor's orientation
761 * for the duration of the hover.
763 this.over = function(anchor, endpoint) {
764 orientation = anchor.getOrientation(endpoint);
768 * notification the endpoint associated with this anchor is no
769 * longer hovering over another anchor; we should resume calculating
770 * orientation as we normally do.
772 this.out = function() { orientation = null; };
774 this.getCurrentLocation = function(params) { return _lastResult == null ? this.compute(params) : _lastResult; };
776 jsPlumbUtil.extend(jsPlumb.FloatingAnchor, jsPlumb.Anchor);
778 var _convertAnchor = function(anchor, jsPlumbInstance, elementId) {
779 return anchor.constructor == jsPlumb.Anchor ? anchor: jsPlumbInstance.makeAnchor(anchor, elementId, jsPlumbInstance);
783 * A DynamicAnchor is an Anchor that contains a list of other Anchors, which it cycles
784 * through at compute time to find the one that is located closest to
785 * the center of the target element, and returns that Anchor's compute
786 * method result. this causes endpoints to follow each other with
787 * respect to the orientation of their target elements, which is a useful
788 * feature for some applications.
791 jsPlumb.DynamicAnchor = function(params) {
792 jsPlumb.Anchor.apply(this, arguments);
794 this.isSelective = true;
795 this.isDynamic = true;
797 this.elementId = params.elementId;
798 this.jsPlumbInstance = params.jsPlumbInstance;
800 for (var i = 0; i < params.anchors.length; i++)
801 this.anchors[i] = _convertAnchor(params.anchors[i], this.jsPlumbInstance, this.elementId);
802 this.addAnchor = function(anchor) { this.anchors.push(_convertAnchor(anchor, this.jsPlumbInstance, this.elementId)); };
803 this.getAnchors = function() { return this.anchors; };
805 var _curAnchor = this.anchors.length > 0 ? this.anchors[0] : null,
806 _curIndex = this.anchors.length > 0 ? 0 : -1,
807 _lastAnchor = _curAnchor,
810 // helper method to calculate the distance between the centers of the two elements.
811 _distance = function(anchor, cx, cy, xy, wh) {
812 var ax = xy[0] + (anchor.x * wh[0]), ay = xy[1] + (anchor.y * wh[1]),
813 acx = xy[0] + (wh[0] / 2), acy = xy[1] + (wh[1] / 2);
814 return (Math.sqrt(Math.pow(cx - ax, 2) + Math.pow(cy - ay, 2)) +
815 Math.sqrt(Math.pow(acx - ax, 2) + Math.pow(acy - ay, 2)));
817 // default method uses distance between element centers. you can provide your own method in the dynamic anchor
818 // constructor (and also to jsPlumb.makeDynamicAnchor). the arguments to it are four arrays:
819 // xy - xy loc of the anchor's element
820 // wh - anchor's element's dimensions
821 // txy - xy loc of the element of the other anchor in the connection
822 // twh - dimensions of the element of the other anchor in the connection.
823 // anchors - the list of selectable anchors
824 _anchorSelector = params.selector || function(xy, wh, txy, twh, anchors) {
825 var cx = txy[0] + (twh[0] / 2), cy = txy[1] + (twh[1] / 2);
826 var minIdx = -1, minDist = Infinity;
827 for ( var i = 0; i < anchors.length; i++) {
828 var d = _distance(anchors[i], cx, cy, xy, wh);
834 return anchors[minIdx];
837 this.compute = function(params) {
838 var xy = params.xy, wh = params.wh, timestamp = params.timestamp, txy = params.txy, twh = params.twh;
840 if(params.clearUserDefinedLocation)
841 userDefinedLocation = null;
843 this.timestamp = timestamp;
845 var udl = self.getUserDefinedLocation();
850 // if anchor is locked or an opposite element was not given, we
851 // maintain our state. anchor will be locked
852 // if it is the source of a drag and drop.
853 if (this.locked || txy == null || twh == null)
854 return _curAnchor.compute(params);
856 params.timestamp = null; // otherwise clear this, i think. we want the anchor to compute.
858 _curAnchor = _anchorSelector(xy, wh, txy, twh, this.anchors);
859 this.x = _curAnchor.x;
860 this.y = _curAnchor.y;
862 if (_curAnchor != _lastAnchor)
863 this.fire("anchorChanged", _curAnchor);
865 _lastAnchor = _curAnchor;
867 return _curAnchor.compute(params);
870 this.getCurrentLocation = function(params) {
871 return this.getUserDefinedLocation() || (_curAnchor != null ? _curAnchor.getCurrentLocation(params) : null);
874 this.getOrientation = function(_endpoint) { return _curAnchor != null ? _curAnchor.getOrientation(_endpoint) : [ 0, 0 ]; };
875 this.over = function(anchor, endpoint) { if (_curAnchor != null) _curAnchor.over(anchor, endpoint); };
876 this.out = function() { if (_curAnchor != null) _curAnchor.out(); };
878 this.getCssClass = function() { return (_curAnchor && _curAnchor.getCssClass()) || ""; };
880 jsPlumbUtil.extend(jsPlumb.DynamicAnchor, jsPlumb.Anchor);
882 // -------- basic anchors ------------------
883 var _curryAnchor = function(x, y, ox, oy, type, fnInit) {
884 jsPlumb.Anchors[type] = function(params) {
885 var a = params.jsPlumbInstance.makeAnchor([ x, y, ox, oy, 0, 0 ], params.elementId, params.jsPlumbInstance);
887 if (fnInit) fnInit(a, params);
892 _curryAnchor(0.5, 0, 0,-1, "TopCenter");
893 _curryAnchor(0.5, 1, 0, 1, "BottomCenter");
894 _curryAnchor(0, 0.5, -1, 0, "LeftMiddle");
895 _curryAnchor(1, 0.5, 1, 0, "RightMiddle");
896 // from 1.4.2: Top, Right, Bottom, Left
897 _curryAnchor(0.5, 0, 0,-1, "Top");
898 _curryAnchor(0.5, 1, 0, 1, "Bottom");
899 _curryAnchor(0, 0.5, -1, 0, "Left");
900 _curryAnchor(1, 0.5, 1, 0, "Right");
901 _curryAnchor(0.5, 0.5, 0, 0, "Center");
902 _curryAnchor(1, 0, 0,-1, "TopRight");
903 _curryAnchor(1, 1, 0, 1, "BottomRight");
904 _curryAnchor(0, 0, 0, -1, "TopLeft");
905 _curryAnchor(0, 1, 0, 1, "BottomLeft");
907 // ------- dynamic anchors -------------------
909 // default dynamic anchors chooses from Top, Right, Bottom, Left
910 jsPlumb.Defaults.DynamicAnchors = function(params) {
911 return params.jsPlumbInstance.makeAnchors(["TopCenter", "RightMiddle", "BottomCenter", "LeftMiddle"], params.elementId, params.jsPlumbInstance);
914 // default dynamic anchors bound to name 'AutoDefault'
915 jsPlumb.Anchors.AutoDefault = function(params) {
916 var a = params.jsPlumbInstance.makeDynamicAnchor(jsPlumb.Defaults.DynamicAnchors(params));
917 a.type = "AutoDefault";
921 // ------- continuous anchors -------------------
923 var _curryContinuousAnchor = function(type, faces) {
924 jsPlumb.Anchors[type] = function(params) {
925 var a = params.jsPlumbInstance.makeAnchor(["Continuous", { faces:faces }], params.elementId, params.jsPlumbInstance);
931 jsPlumb.Anchors.Continuous = function(params) {
932 return params.jsPlumbInstance.continuousAnchorFactory.get(params);
935 _curryContinuousAnchor("ContinuousLeft", ["left"]);
936 _curryContinuousAnchor("ContinuousTop", ["top"]);
937 _curryContinuousAnchor("ContinuousBottom", ["bottom"]);
938 _curryContinuousAnchor("ContinuousRight", ["right"]);
940 // ------- position assign anchors -------------------
942 // this anchor type lets you assign the position at connection time.
943 _curryAnchor(0, 0, 0, 0, "Assign", function(anchor, params) {
944 // find what to use as the "position finder". the user may have supplied a String which represents
945 // the id of a position finder in jsPlumb.AnchorPositionFinders, or the user may have supplied the
946 // position finder as a function. we find out what to use and then set it on the anchor.
947 var pf = params.position || "Fixed";
948 anchor.positionFinder = pf.constructor == String ? params.jsPlumbInstance.AnchorPositionFinders[pf] : pf;
949 // always set the constructor params; the position finder might need them later (the Grid one does,
951 anchor.constructorParams = params;
954 // these are the default anchor positions finders, which are used by the makeTarget function. supplying
955 // a position finder argument to that function allows you to specify where the resulting anchor will
957 jsPlumbInstance.prototype.AnchorPositionFinders = {
958 "Fixed": function(dp, ep, es, params) {
959 return [ (dp.left - ep.left) / es[0], (dp.top - ep.top) / es[1] ];
961 "Grid":function(dp, ep, es, params) {
962 var dx = dp.left - ep.left, dy = dp.top - ep.top,
963 gx = es[0] / (params.grid[0]), gy = es[1] / (params.grid[1]),
964 mx = Math.floor(dx / gx), my = Math.floor(dy / gy);
965 return [ ((mx * gx) + (gx / 2)) / es[0], ((my * gy) + (gy / 2)) / es[1] ];
969 // ------- perimeter anchors -------------------
971 jsPlumb.Anchors.Perimeter = function(params) {
972 params = params || {};
973 var anchorCount = params.anchorCount || 60,
974 shape = params.shape;
976 if (!shape) throw new Error("no shape supplied to Perimeter Anchor type");
978 var _circle = function() {
979 var r = 0.5, step = Math.PI * 2 / anchorCount, current = 0, a = [];
980 for (var i = 0; i < anchorCount; i++) {
981 var x = r + (r * Math.sin(current)),
982 y = r + (r * Math.cos(current));
983 a.push( [ x, y, 0, 0 ] );
988 _path = function(segments) {
989 var anchorsPerFace = anchorCount / segments.length, a = [],
990 _computeFace = function(x1, y1, x2, y2, fractionalLength) {
991 anchorsPerFace = anchorCount * fractionalLength;
992 var dx = (x2 - x1) / anchorsPerFace, dy = (y2 - y1) / anchorsPerFace;
993 for (var i = 0; i < anchorsPerFace; i++) {
1003 for (var i = 0; i < segments.length; i++)
1004 _computeFace.apply(null, segments[i]);
1008 _shape = function(faces) {
1010 for (var i = 0; i < faces.length; i++) {
1011 s.push([faces[i][0], faces[i][1], faces[i][2], faces[i][3], 1 / faces.length]);
1015 _rectangle = function() {
1017 [ 0, 0, 1, 0 ], [ 1, 0, 1, 1 ], [ 1, 1, 0, 1 ], [ 0, 1, 0, 0 ]
1024 "Diamond":function() {
1026 [ 0.5, 0, 1, 0.5 ], [ 1, 0.5, 0.5, 1 ], [ 0.5, 1, 0, 0.5 ], [ 0, 0.5, 0.5, 0 ]
1029 "Rectangle":_rectangle,
1030 "Square":_rectangle,
1031 "Triangle":function() {
1033 [ 0.5, 0, 1, 1 ], [ 1, 1, 0, 1 ], [ 0, 1, 0.5, 0]
1036 "Path":function(params) {
1037 var points = params.points, p = [], tl = 0;
1038 for (var i = 0; i < points.length - 1; i++) {
1039 var l = Math.sqrt(Math.pow(points[i][2] - points[i][0]) + Math.pow(points[i][3] - points[i][1]));
1041 p.push([points[i][0], points[i][1], points[i+1][0], points[i+1][1], l]);
1043 for (var j = 0; j < p.length; j++) {
1044 p[j][4] = p[j][4] / tl;
1049 _rotate = function(points, amountInDegrees) {
1050 var o = [], theta = amountInDegrees / 180 * Math.PI ;
1051 for (var i = 0; i < points.length; i++) {
1052 var _x = points[i][0] - 0.5,
1053 _y = points[i][1] - 0.5;
1056 0.5 + ((_x * Math.cos(theta)) - (_y * Math.sin(theta))),
1057 0.5 + ((_x * Math.sin(theta)) + (_y * Math.cos(theta))),
1065 if (!_shapes[shape]) throw new Error("Shape [" + shape + "] is unknown by Perimeter Anchor type");
1067 var da = _shapes[shape](params);
1068 if (params.rotation) da = _rotate(da, params.rotation);
1069 var a = params.jsPlumbInstance.makeDynamicAnchor(da);
1070 a.type = "Perimeter";