3 var makeConnector = function(_jsPlumb, renderMode, connectorName, connectorArgs) {
4 if (!_jsPlumb.Defaults.DoNotThrowErrors && jsPlumb.Connectors[renderMode][connectorName] == null)
5 throw { msg:"jsPlumb: unknown connector type '" + connectorName + "'" };
7 return new jsPlumb.Connectors[renderMode][connectorName](connectorArgs);
9 _makeAnchor = function(anchorParams, elementId, _jsPlumb) {
10 return (anchorParams) ? _jsPlumb.makeAnchor(anchorParams, elementId, _jsPlumb) : null;
12 prepareEndpoint = function(_jsPlumb, _newEndpoint, conn, existing, index, params, element, elementId, connectorPaintStyle, connectorHoverPaintStyle) {
15 conn.endpoints[index] = existing;
16 existing.addConnection(conn);
18 if (!params.endpoints) params.endpoints = [ null, null ];
19 var ep = params.endpoints[index] || params.endpoint || _jsPlumb.Defaults.Endpoints[index] || jsPlumb.Defaults.Endpoints[index] || _jsPlumb.Defaults.Endpoint || jsPlumb.Defaults.Endpoint;
20 if (!params.endpointStyles) params.endpointStyles = [ null, null ];
21 if (!params.endpointHoverStyles) params.endpointHoverStyles = [ null, null ];
22 var es = params.endpointStyles[index] || params.endpointStyle || _jsPlumb.Defaults.EndpointStyles[index] || jsPlumb.Defaults.EndpointStyles[index] || _jsPlumb.Defaults.EndpointStyle || jsPlumb.Defaults.EndpointStyle;
23 // Endpoints derive their fillStyle from the connector's strokeStyle, if no fillStyle was specified.
24 if (es.fillStyle == null && connectorPaintStyle != null)
25 es.fillStyle = connectorPaintStyle.strokeStyle;
27 // TODO: decide if the endpoint should derive the connection's outline width and color. currently it does:
29 if (es.outlineColor == null && connectorPaintStyle != null)
30 es.outlineColor = connectorPaintStyle.outlineColor;
31 if (es.outlineWidth == null && connectorPaintStyle != null)
32 es.outlineWidth = connectorPaintStyle.outlineWidth;
35 var ehs = params.endpointHoverStyles[index] || params.endpointHoverStyle || _jsPlumb.Defaults.EndpointHoverStyles[index] || jsPlumb.Defaults.EndpointHoverStyles[index] || _jsPlumb.Defaults.EndpointHoverStyle || jsPlumb.Defaults.EndpointHoverStyle;
36 // endpoint hover fill style is derived from connector's hover stroke style. TODO: do we want to do this by default? for sure?
37 if (connectorHoverPaintStyle != null) {
38 if (ehs == null) ehs = {};
39 if (ehs.fillStyle == null) {
40 ehs.fillStyle = connectorHoverPaintStyle.strokeStyle;
43 var a = params.anchors ? params.anchors[index] :
44 params.anchor ? params.anchor :
45 _makeAnchor(_jsPlumb.Defaults.Anchors[index], elementId, _jsPlumb) ||
46 _makeAnchor(jsPlumb.Defaults.Anchors[index], elementId,_jsPlumb) ||
47 _makeAnchor(_jsPlumb.Defaults.Anchor, elementId,_jsPlumb) ||
48 _makeAnchor(jsPlumb.Defaults.Anchor, elementId, _jsPlumb),
49 u = params.uuids ? params.uuids[index] : null;
51 paintStyle : es, hoverPaintStyle:ehs, endpoint : ep, connections : [ conn ],
52 uuid : u, anchor : a, source : element, scope : params.scope, container:params.container,
53 reattach:params.reattach || _jsPlumb.Defaults.ReattachConnections,
54 detachable:params.detachable || _jsPlumb.Defaults.ConnectionsDetachable
56 conn.endpoints[index] = e;
58 if (params.drawEndpoints === false) e.setVisible(false, true, true);
64 jsPlumb.Connection = function(params) {
65 var _newConnection = params.newConnection,
66 _newEndpoint = params.newEndpoint,
67 jpcl = jsPlumb.CurrentLibrary,
68 _att = jpcl.getAttribute,
69 _gel = jpcl.getElementObject,
70 _dom = jpcl.getDOMElement,
72 _getOffset = jpcl.getOffset;
74 this.connector = null;
75 this.idPrefix = "_jsplumb_c_";
76 this.defaultLabelLocation = 0.5;
77 this.defaultOverlayKeys = ["Overlays", "ConnectionOverlays"];
78 this.parent = params.parent;
79 // if a new connection is the result of moving some existing connection, params.previousConnection
80 // will have that Connection in it. listeners for the jsPlumbConnection event can look for that
81 // member and take action if they need to.
82 this.previousConnection = params.previousConnection;
83 this.source = _dom(params.source);
84 this.target = _dom(params.target);
85 // sourceEndpoint and targetEndpoint override source/target, if they are present. but
86 // source is not overridden if the Endpoint has declared it is not the final target of a connection;
87 // instead we use the source that the Endpoint declares will be the final source element.
88 if (params.sourceEndpoint) this.source = params.sourceEndpoint.endpointWillMoveTo || params.sourceEndpoint.getElement();
89 if (params.targetEndpoint) this.target = params.targetEndpoint.getElement();
91 OverlayCapableJsPlumbUIComponent.apply(this, arguments);
93 this.sourceId = this._jsPlumb.instance.getId(this.source);
94 this.targetId = this._jsPlumb.instance.getId(this.target);
95 this.scope = params.scope; // scope may have been passed in to the connect call. if it wasn't, we will pull it from the source endpoint, after having initialised the endpoints.
97 this.endpointStyles = [];
99 var _jsPlumb = this._jsPlumb.instance;
100 this._jsPlumb.visible = true;
101 this._jsPlumb.editable = params.editable === true;
102 this._jsPlumb.params = {
103 parent:params.parent,
104 cssClass:params.cssClass,
105 container:params.container,
106 "pointer-events":params["pointer-events"],
107 editorParams:params.editorParams
109 this._jsPlumb.lastPaintedAt = null;
110 this.getDefaultType = function() {
114 detachable:this._jsPlumb.instance.Defaults.ConnectionsDetachable,
115 rettach:this._jsPlumb.instance.Defaults.ReattachConnections,
116 paintStyle:this._jsPlumb.instance.Defaults.PaintStyle || jsPlumb.Defaults.PaintStyle,
117 connector:this._jsPlumb.instance.Defaults.Connector || jsPlumb.Defaults.Connector,
118 hoverPaintStyle:this._jsPlumb.instance.Defaults.HoverPaintStyle || jsPlumb.Defaults.HoverPaintStyle,
119 overlays:this._jsPlumb.instance.Defaults.ConnectorOverlays || jsPlumb.Defaults.ConnectorOverlays
123 // INITIALISATION CODE
125 // wrapped the main function to return null if no input given. this lets us cascade defaults properly.
127 var eS = prepareEndpoint(_jsPlumb, _newEndpoint, this, params.sourceEndpoint, 0, params, this.source, this.sourceId, params.paintStyle, params.hoverPaintStyle);
128 if (eS) _ju.addToList(params.endpointsByElement, this.sourceId, eS);
129 var eT = prepareEndpoint(_jsPlumb, _newEndpoint, this, params.targetEndpoint, 1, params, this.target, this.targetId, params.paintStyle, params.hoverPaintStyle);
130 if (eT) _ju.addToList(params.endpointsByElement, this.targetId, eT);
131 // if scope not set, set it to be the scope for the source endpoint.
132 if (!this.scope) this.scope = this.endpoints[0].scope;
134 // if explicitly told to (or not to) delete endpoints on detach, override endpoint's preferences
135 if (params.deleteEndpointsOnDetach != null) {
136 this.endpoints[0]._deleteOnDetach = params.deleteEndpointsOnDetach;
137 this.endpoints[1]._deleteOnDetach = params.deleteEndpointsOnDetach;
140 // otherwise, unless the endpoints say otherwise, mark them for deletion.
141 if (!this.endpoints[0]._doNotDeleteOnDetach) this.endpoints[0]._deleteOnDetach = true;
142 if (!this.endpoints[1]._doNotDeleteOnDetach) this.endpoints[1]._deleteOnDetach = true;
145 // TODO these could surely be refactored into some method that tries them one at a time until something exists
146 this.setConnector(this.endpoints[0].connector ||
147 this.endpoints[1].connector ||
149 _jsPlumb.Defaults.Connector ||
150 jsPlumb.Defaults.Connector, true);
153 this.connector.setPath(params.path);
155 this.setPaintStyle(this.endpoints[0].connectorStyle ||
156 this.endpoints[1].connectorStyle ||
158 _jsPlumb.Defaults.PaintStyle ||
159 jsPlumb.Defaults.PaintStyle, true);
161 this.setHoverPaintStyle(this.endpoints[0].connectorHoverStyle ||
162 this.endpoints[1].connectorHoverStyle ||
163 params.hoverPaintStyle ||
164 _jsPlumb.Defaults.HoverPaintStyle ||
165 jsPlumb.Defaults.HoverPaintStyle, true);
167 this._jsPlumb.paintStyleInUse = this.getPaintStyle();
169 var _suspendedAt = _jsPlumb.getSuspendedAt();
170 _jsPlumb.updateOffset( { elId : this.sourceId, timestamp:_suspendedAt });
171 _jsPlumb.updateOffset( { elId : this.targetId, timestamp:_suspendedAt });
174 if(!_jsPlumb.isSuspendDrawing()) {
175 // paint the endpoints
176 var myInfo = _jsPlumb.getCachedData(this.sourceId),
177 myOffset = myInfo.o, myWH = myInfo.s,
178 otherInfo = _jsPlumb.getCachedData(this.targetId),
179 otherOffset = otherInfo.o,
180 otherWH = otherInfo.s,
181 initialTimestamp = _suspendedAt || _jsPlumb.timestamp(),
182 anchorLoc = this.endpoints[0].anchor.compute( {
183 xy : [ myOffset.left, myOffset.top ], wh : myWH, element : this.endpoints[0],
184 elementId:this.endpoints[0].elementId,
185 txy : [ otherOffset.left, otherOffset.top ], twh : otherWH, tElement : this.endpoints[1],
186 timestamp:initialTimestamp
189 this.endpoints[0].paint( { anchorLoc : anchorLoc, timestamp:initialTimestamp });
191 anchorLoc = this.endpoints[1].anchor.compute( {
192 xy : [ otherOffset.left, otherOffset.top ], wh : otherWH, element : this.endpoints[1],
193 elementId:this.endpoints[1].elementId,
194 txy : [ myOffset.left, myOffset.top ], twh : myWH, tElement : this.endpoints[0],
195 timestamp:initialTimestamp
197 this.endpoints[1].paint({ anchorLoc : anchorLoc, timestamp:initialTimestamp });
201 // END INITIALISATION CODE
204 this._jsPlumb.detachable = _jsPlumb.Defaults.ConnectionsDetachable;
205 if (params.detachable === false) this._jsPlumb.detachable = false;
206 if(this.endpoints[0].connectionsDetachable === false) this._jsPlumb.detachable = false;
207 if(this.endpoints[1].connectionsDetachable === false) this._jsPlumb.detachable = false;
209 this._jsPlumb.reattach = params.reattach || this.endpoints[0].reattachConnections || this.endpoints[1].reattachConnections || _jsPlumb.Defaults.ReattachConnections;
210 // COST + DIRECTIONALITY
211 // if cost not supplied, try to inherit from source endpoint
212 this._jsPlumb.cost = params.cost || this.endpoints[0].getConnectionCost();
213 this._jsPlumb.directed = params.directed;
214 // inherit directed flag if set no source endpoint
215 if (params.directed == null) this._jsPlumb.directed = this.endpoints[0].areConnectionsDirected();
216 // END COST + DIRECTIONALITY
219 // merge all the parameters objects into the connection. parameters set
220 // on the connection take precedence; then source endpoint params, then
221 // finally target endpoint params.
222 // TODO jsPlumb.extend could be made to take more than two args, and it would
223 // apply the second through nth args in order.
224 var _p = jsPlumb.extend({}, this.endpoints[1].getParameters());
225 jsPlumb.extend(_p, this.endpoints[0].getParameters());
226 jsPlumb.extend(_p, this.getParameters());
227 this.setParameters(_p);
232 // the very last thing we do is apply types, if there are any.
233 var _types = [params.type, this.endpoints[0].connectionType, this.endpoints[1].connectionType ].join(" ");
234 if (/[a-zA-Z]/.test(_types))
235 this.addType(_types, params.data, true);
241 jsPlumbUtil.extend(jsPlumb.Connection, OverlayCapableJsPlumbUIComponent, {
242 applyType : function(t, doNotRepaint) {
243 if (t.detachable != null) this.setDetachable(t.detachable);
244 if (t.reattach != null) this.setReattach(t.reattach);
245 if (t.scope) this.scope = t.scope;
246 //editable = t.editable; // TODO
247 this.setConnector(t.connector, doNotRepaint);
249 getTypeDescriptor : function() { return "connection"; },
250 getAttachedElements : function() {
251 return this.endpoints;
253 addClass : function(c, informEndpoints) {
254 if (informEndpoints) {
255 this.endpoints[0].addClass(c);
256 this.endpoints[1].addClass(c);
257 if (this.suspendedEndpoint) this.suspendedEndpoint.addClass(c);
259 if (this.connector) {
260 this.connector.addClass(c);
263 removeClass : function(c, informEndpoints) {
264 if (informEndpoints) {
265 this.endpoints[0].removeClass(c);
266 this.endpoints[1].removeClass(c);
267 if (this.suspendedEndpoint) this.suspendedEndpoint.removeClass(c);
269 if (this.connector) {
270 this.connector.removeClass(c);
273 isVisible : function() { return this._jsPlumb.visible; },
274 setVisible : function(v) {
275 this._jsPlumb.visible = v;
276 //this[v ? "showOverlays" : "hideOverlays"]();
278 this.connector.setVisible(v);
282 /* TODO move to connecto editors; it should put these on the prototype.
284 setEditable : function(e) {
285 if (this.connector && this.connector.isEditable())
286 this._jsPlumb.editable = e;
288 return this._jsPlumb.editable;
290 isEditable : function() { return this._jsPlumb.editable; },
291 editStarted : function() {
292 this.setSuspendEvents(true);
293 this.fire("editStarted", {
294 path:this.connector.getPath()
296 this._jsPlumb.instance.setHoverSuspended(true);
298 editCompleted : function() {
299 this.fire("editCompleted", {
300 path:this.connector.getPath()
302 this.setSuspendEvents(false);
303 this.setHover(false);
304 this._jsPlumb.instance.setHoverSuspended(false);
306 editCanceled : function() {
307 this.fire("editCanceled", {
308 path:this.connector.getPath()
310 this.setHover(false);
311 this._jsPlumb.instance.setHoverSuspended(false);
317 //this.endpointsToDeleteOnDetach = null;
318 this.endpoints = null;
321 if (this.connector != null) {
322 this.connector.cleanup();
323 this.connector.destroy();
325 this.connector = null;
327 isDetachable : function() {
328 return this._jsPlumb.detachable === true;
330 setDetachable : function(detachable) {
331 this._jsPlumb.detachable = detachable === true;
333 isReattach : function() {
334 return this._jsPlumb.reattach === true;
336 setReattach : function(reattach) {
337 this._jsPlumb.reattach = reattach === true;
339 setHover : function(state) {
340 if (this.connector && this._jsPlumb && !this._jsPlumb.instance.isConnectionBeingDragged()) {
341 this.connector.setHover(state);
342 jsPlumb.CurrentLibrary[state ? "addClass" : "removeClass"](this.source, this._jsPlumb.instance.hoverSourceClass);
343 jsPlumb.CurrentLibrary[state ? "addClass" : "removeClass"](this.target, this._jsPlumb.instance.hoverTargetClass);
346 getCost : function() { return this._jsPlumb.cost; },
347 setCost : function(c) { this._jsPlumb.cost = c; },
348 isDirected : function() { return this._jsPlumb.directed === true; },
350 // changes the parent element of this connection to newParent. not exposed for the public API.
352 // TODO ensure moveParent method still works (the overlay stuff in particular)
353 moveParent : function(newParent) {
354 var jpcl = jsPlumb.CurrentLibrary, curParent = jpcl.getParent(this.connector.canvas);
355 if (this.connector.bgCanvas) {
356 jpcl.removeElement(this.connector.bgCanvas);
357 jpcl.appendElement(this.connector.bgCanvas, newParent);
359 jpcl.removeElement(this.connector.canvas);
360 jpcl.appendElement(this.connector.canvas, newParent);
361 // this only applies for DOMOverlays
362 for (var i = 0; i < this._jsPlumb.overlays.length; i++) {
363 if (this._jsPlumb.overlays[i].isAppendedAtTopLevel) {
364 jpcl.removeElement(this._jsPlumb.overlays[i].canvas);
365 jpcl.appendElement(this._jsPlumb.overlays[i].canvas, newParent);
366 if (this._jsPlumb.overlays[i].reattachListeners)
367 this._jsPlumb.overlays[i].reattachListeners(this.connector);
370 if (this.connector.reattachListeners) // this is for SVG/VML; change an element's parent and you have to reinit its listeners.
371 this.connector.reattachListeners(); // the Canvas implementation doesn't have to care about this
373 getConnector : function() { return this.connector; },
374 setConnector : function(connectorSpec, doNotRepaint) {
375 var _ju = jsPlumbUtil;
376 if (this.connector != null) {
377 this.connector.cleanup();
378 this.connector.destroy();
381 var connectorArgs = {
382 _jsPlumb:this._jsPlumb.instance,
383 parent:this._jsPlumb.params.parent,
384 cssClass:this._jsPlumb.params.cssClass,
385 container:this._jsPlumb.params.container,
386 "pointer-events":this._jsPlumb.params["pointer-events"]
388 renderMode = this._jsPlumb.instance.getRenderMode();
390 if (_ju.isString(connectorSpec))
391 this.connector = makeConnector(this._jsPlumb.instance, renderMode, connectorSpec, connectorArgs); // lets you use a string as shorthand.
392 else if (_ju.isArray(connectorSpec)) {
393 if (connectorSpec.length == 1)
394 this.connector = makeConnector(this._jsPlumb.instance, renderMode, connectorSpec[0], connectorArgs);
396 this.connector = makeConnector(this._jsPlumb.instance, renderMode, connectorSpec[0], _ju.merge(connectorSpec[1], connectorArgs));
398 // binds mouse listeners to the current connector.
399 this.bindListeners(this.connector, this, function(state) {
400 this.setHover(state, false);
403 this.canvas = this.connector.canvas;
405 if (this._jsPlumb.editable && jsPlumb.ConnectorEditors != null && jsPlumb.ConnectorEditors[this.connector.type] && this.connector.isEditable()) {
406 new jsPlumb.ConnectorEditors[this.connector.type]({
407 connector:this.connector,
409 params:this._jsPlumb.params.editorParams || { }
413 this._jsPlumb.editable = false;
416 if (!doNotRepaint) this.repaint();
418 paint : function(params) {
420 if (!this._jsPlumb.instance.isSuspendDrawing() && this._jsPlumb.visible) {
422 params = params || {};
423 var elId = params.elId, ui = params.ui, recalc = params.recalc, timestamp = params.timestamp,
424 // if the moving object is not the source we must transpose the two references.
426 tId = swap ? this.sourceId : this.targetId, sId = swap ? this.targetId : this.sourceId,
427 tIdx = swap ? 0 : 1, sIdx = swap ? 1 : 0;
429 if (timestamp == null || timestamp != this._jsPlumb.lastPaintedAt) {
430 var sourceInfo = this._jsPlumb.instance.updateOffset( { elId : sId, offset : ui, recalc : recalc, timestamp : timestamp }).o,
431 targetInfo = this._jsPlumb.instance.updateOffset( { elId : tId, timestamp : timestamp }).o, // update the target if this is a forced repaint. otherwise, only the source has been moved.
432 sE = this.endpoints[sIdx], tE = this.endpoints[tIdx];
434 if (params.clearEdits) {
435 this._jsPlumb.overlayPositions = null;
436 sE.anchor.clearUserDefinedLocation();
437 tE.anchor.clearUserDefinedLocation();
438 this.connector.setEdited(false);
441 var sAnchorP = sE.anchor.getCurrentLocation({xy:[sourceInfo.left,sourceInfo.top], wh:[sourceInfo.width, sourceInfo.height], element:sE, timestamp:timestamp}),
442 tAnchorP = tE.anchor.getCurrentLocation({xy:[targetInfo.left,targetInfo.top], wh:[targetInfo.width, targetInfo.height], element:tE, timestamp:timestamp});
444 this.connector.resetBounds();
446 this.connector.compute({
449 sourceEndpoint:this.endpoints[sIdx],
450 targetEndpoint:this.endpoints[tIdx],
451 lineWidth:this._jsPlumb.paintStyleInUse.lineWidth,
452 sourceInfo:sourceInfo,
453 targetInfo:targetInfo,
454 clearEdits:params.clearEdits === true
457 var overlayExtents = { minX:Infinity, minY:Infinity, maxX:-Infinity, maxY:-Infinity };
459 // compute overlays. we do this first so we can get their placements, and adjust the
460 // container if needs be (if an overlay would be clipped)
461 for ( var i = 0; i < this._jsPlumb.overlays.length; i++) {
462 var o = this._jsPlumb.overlays[i];
464 this._jsPlumb.overlayPlacements[i] = o.draw(this.connector, this._jsPlumb.paintStyleInUse, this.getAbsoluteOverlayPosition(o));
465 overlayExtents.minX = Math.min(overlayExtents.minX, this._jsPlumb.overlayPlacements[i].minX);
466 overlayExtents.maxX = Math.max(overlayExtents.maxX, this._jsPlumb.overlayPlacements[i].maxX);
467 overlayExtents.minY = Math.min(overlayExtents.minY, this._jsPlumb.overlayPlacements[i].minY);
468 overlayExtents.maxY = Math.max(overlayExtents.maxY, this._jsPlumb.overlayPlacements[i].maxY);
472 var lineWidth = parseFloat(this._jsPlumb.paintStyleInUse.lineWidth || 1) / 2,
473 outlineWidth = parseFloat(this._jsPlumb.paintStyleInUse.lineWidth || 0),
475 xmin : Math.min(this.connector.bounds.minX - (lineWidth + outlineWidth), overlayExtents.minX),
476 ymin : Math.min(this.connector.bounds.minY - (lineWidth + outlineWidth), overlayExtents.minY),
477 xmax : Math.max(this.connector.bounds.maxX + (lineWidth + outlineWidth), overlayExtents.maxX),
478 ymax : Math.max(this.connector.bounds.maxY + (lineWidth + outlineWidth), overlayExtents.maxY)
481 // paint the connector.
482 this.connector.paint(this._jsPlumb.paintStyleInUse, null, extents);
483 // and then the overlays
484 for ( var j = 0; j < this._jsPlumb.overlays.length; j++) {
485 var p = this._jsPlumb.overlays[j];
487 p.paint(this._jsPlumb.overlayPlacements[j], extents);
491 this._jsPlumb.lastPaintedAt = timestamp;
496 * Repaints the Connection. No parameters exposed to public API.
498 repaint : function(params) {
499 params = params || {};
500 this.paint({ elId : this.sourceId, recalc : !(params.recalc === false), timestamp:params.timestamp, clearEdits:params.clearEdits });
503 }); // END Connection class