4 // create the drag handler for a connection
5 var _makeConnectionDragHandler = function(placeholder, _jsPlumb) {
13 var _ui = jsPlumb.CurrentLibrary.getUIPosition(arguments, _jsPlumb.getZoom());
15 if (placeholder.element) {
16 jsPlumb.CurrentLibrary.setOffset(placeholder.element, _ui);
17 _jsPlumb.repaint(placeholder.element, _ui);
20 stopDrag : function() {
26 // creates a placeholder div for dragging purposes, adds it to the DOM, and pre-computes its offset.
27 var _makeDraggablePlaceholder = function(placeholder, parent, _jsPlumb) {
28 var n = document.createElement("div");
29 n.style.position = "absolute";
30 var placeholderDragElement = jsPlumb.CurrentLibrary.getElementObject(n);
31 jsPlumb.CurrentLibrary.appendElement(n, parent);
32 var id = _jsPlumb.getId(n);
33 _jsPlumb.updateOffset( { elId : id });
34 // create and assign an id, and initialize the offset.
36 placeholder.element = n;
39 // create a floating endpoint (for drag connections)
40 var _makeFloatingEndpoint = function(paintStyle, referenceAnchor, endpoint, referenceCanvas, sourceElement, _jsPlumb, _newEndpoint) {
41 var floatingAnchor = new jsPlumb.FloatingAnchor( { reference : referenceAnchor, referenceCanvas : referenceCanvas, jsPlumbInstance:_jsPlumb });
42 //setting the scope here should not be the way to fix that mootools issue. it should be fixed by not
43 // adding the floating endpoint as a droppable. that makes more sense anyway!
44 return _newEndpoint({ paintStyle : paintStyle, endpoint : endpoint, anchor : floatingAnchor, source : sourceElement, scope:"__floating" });
47 var typeParameters = [ "connectorStyle", "connectorHoverStyle", "connectorOverlays",
48 "connector", "connectionType", "connectorClass", "connectorHoverClass" ];
50 // a helper function that tries to find a connection to the given element, and returns it if so. if elementWithPrecedence is null,
51 // or no connection to it is found, we return the first connection in our list.
52 var findConnectionToUseForDynamicAnchor = function(ep, elementWithPrecedence) {
54 if (elementWithPrecedence != null) {
55 for (var i = 0; i < ep.connections.length; i++) {
56 if (ep.connections[i].sourceId == elementWithPrecedence || ep.connections[i].targetId == elementWithPrecedence) {
63 return ep.connections[idx];
66 var findConnectionIndex = function(conn, ep) {
67 return jsPlumbUtil.findWithFunction(ep.connections, function(c) { return c.id == conn.id; });
70 jsPlumb.Endpoint = function(params) {
71 var _jsPlumb = params._jsPlumb,
72 jpcl = jsPlumb.CurrentLibrary,
73 _att = jsPlumbAdapter.getAttribute,
74 _gel = jpcl.getElementObject,
75 _dom = jpcl.getDOMElement,
77 _newConnection = params.newConnection,
78 _newEndpoint = params.newEndpoint,
79 _finaliseConnection = params.finaliseConnection,
80 _fireDetachEvent = params.fireDetachEvent,
81 _fireMoveEvent = params.fireMoveEvent,
82 floatingConnections = params.floatingConnections;
84 this.idPrefix = "_jsplumb_e_";
85 this.defaultLabelLocation = [ 0.5, 0.5 ];
86 this.defaultOverlayKeys = ["Overlays", "EndpointOverlays"];
87 this.parent = params.parent;
88 OverlayCapableJsPlumbUIComponent.apply(this, arguments);
92 this.getDefaultType = function() {
96 maxConnections:this._jsPlumb.instance.Defaults.MaxConnections,
97 paintStyle:this._jsPlumb.instance.Defaults.EndpointStyle || jsPlumb.Defaults.EndpointStyle,
98 endpoint:this._jsPlumb.instance.Defaults.Endpoint || jsPlumb.Defaults.Endpoint,
99 hoverPaintStyle:this._jsPlumb.instance.Defaults.EndpointHoverStyle || jsPlumb.Defaults.EndpointHoverStyle,
100 overlays:this._jsPlumb.instance.Defaults.EndpointOverlays || jsPlumb.Defaults.EndpointOverlays,
101 connectorStyle:params.connectorStyle,
102 connectorHoverStyle:params.connectorHoverStyle,
103 connectorClass:params.connectorClass,
104 connectorHoverClass:params.connectorHoverClass,
105 connectorOverlays:params.connectorOverlays,
106 connector:params.connector,
107 connectorTooltip:params.connectorTooltip
113 this._jsPlumb.enabled = !(params.enabled === false);
114 this._jsPlumb.visible = true;
115 this.element = _dom(params.source);
116 this._jsPlumb.uuid = params.uuid;
117 this._jsPlumb.floatingEndpoint = null;
118 var inPlaceCopy = null;
119 if (this._jsPlumb.uuid) params.endpointsByUUID[this._jsPlumb.uuid] = this;
120 this.elementId = params.elementId;
122 this._jsPlumb.connectionCost = params.connectionCost;
123 this._jsPlumb.connectionsDirected = params.connectionsDirected;
124 this._jsPlumb.currentAnchorClass = "";
125 this._jsPlumb.events = {};
127 var _updateAnchorClass = function() {
128 jpcl.removeClass(this.element, _jsPlumb.endpointAnchorClassPrefix + "_" + this._jsPlumb.currentAnchorClass);
129 this.removeClass(_jsPlumb.endpointAnchorClassPrefix + "_" + this._jsPlumb.currentAnchorClass);
130 this._jsPlumb.currentAnchorClass = this.anchor.getCssClass();
131 this.addClass(_jsPlumb.endpointAnchorClassPrefix + "_" + this._jsPlumb.currentAnchorClass);
132 jpcl.addClass(this.element, _jsPlumb.endpointAnchorClassPrefix + "_" + this._jsPlumb.currentAnchorClass);
135 this.setAnchor = function(anchorParams, doNotRepaint) {
136 this._jsPlumb.instance.continuousAnchorFactory.clear(this.elementId);
137 this.anchor = this._jsPlumb.instance.makeAnchor(anchorParams, this.elementId, _jsPlumb);
138 _updateAnchorClass();
139 this.anchor.bind("anchorChanged", function(currentAnchor) {
140 this.fire("anchorChanged", {endpoint:this, anchor:currentAnchor});
141 _updateAnchorClass();
144 this._jsPlumb.instance.repaint(this.elementId);
148 var anchorParamsToUse = params.anchor ? params.anchor : params.anchors ? params.anchors : (_jsPlumb.Defaults.Anchor || "Top");
149 this.setAnchor(anchorParamsToUse, true);
151 // endpoint delegates to first connection for hover, if there is one.
152 var internalHover = function(state) {
153 if (this.connections.length > 0)
154 this.connections[0].setHover(state, false);
156 this.setHover(state);
160 if (!params._transient) // in place copies, for example, are transient. they will never need to be retrieved during a paint cycle, because they dont move, and then they are deleted.
161 this._jsPlumb.instance.anchorManager.add(this, this.elementId);
163 this.setEndpoint = function(ep) {
165 if (this.endpoint != null) {
166 this.endpoint.cleanup();
167 this.endpoint.destroy();
170 var _e = function(t, p) {
171 var rm = _jsPlumb.getRenderMode();
172 if (jsPlumb.Endpoints[rm][t]) return new jsPlumb.Endpoints[rm][t](p);
173 if (!_jsPlumb.Defaults.DoNotThrowErrors)
174 throw { msg:"jsPlumb: unknown endpoint type '" + t + "'" };
178 _jsPlumb:this._jsPlumb.instance,
179 cssClass:params.cssClass,
180 parent:params.parent,
181 container:params.container,
182 tooltip:params.tooltip,
183 connectorTooltip:params.connectorTooltip,
186 if (_ju.isString(ep))
187 this.endpoint = _e(ep, endpointArgs);
188 else if (_ju.isArray(ep)) {
189 endpointArgs = _ju.merge(ep[1], endpointArgs);
190 this.endpoint = _e(ep[0], endpointArgs);
193 this.endpoint = ep.clone();
196 // assign a clone function using a copy of endpointArgs. this is used when a drag starts: the endpoint that was dragged is cloned,
197 // and the clone is left in its place while the original one goes off on a magical journey.
198 // the copy is to get around a closure problem, in which endpointArgs ends up getting shared by
200 var argsForClone = jsPlumb.extend({}, endpointArgs);
201 this.endpoint.clone = function() {
202 // TODO this, and the code above, can be refactored to be more dry.
203 if (_ju.isString(ep))
204 return _e(ep, endpointArgs);
205 else if (_ju.isArray(ep)) {
206 endpointArgs = _ju.merge(ep[1], endpointArgs);
207 return _e(ep[0], endpointArgs);
211 this.type = this.endpoint.type;
212 // bind listeners from endpoint to self, with the internal hover function defined above.
213 this.bindListeners(this.endpoint, this, internalHover);
216 this.setEndpoint(params.endpoint || _jsPlumb.Defaults.Endpoint || jsPlumb.Defaults.Endpoint || "Dot");
217 this.setPaintStyle(params.paintStyle || params.style || _jsPlumb.Defaults.EndpointStyle || jsPlumb.Defaults.EndpointStyle, true);
218 this.setHoverPaintStyle(params.hoverPaintStyle || _jsPlumb.Defaults.EndpointHoverStyle || jsPlumb.Defaults.EndpointHoverStyle, true);
219 this._jsPlumb.paintStyleInUse = this.getPaintStyle();
221 _ju.copyValues(typeParameters, params, this);
223 this.isSource = params.isSource || false;
224 this.isTarget = params.isTarget || false;
225 this._jsPlumb.maxConnections = params.maxConnections || _jsPlumb.Defaults.MaxConnections; // maximum number of connections this endpoint can be the source of.
226 this.canvas = this.endpoint.canvas;
227 // add anchor class (need to do this on construction because we set anchor first)
228 this.addClass(_jsPlumb.endpointAnchorClassPrefix + "_" + this._jsPlumb.currentAnchorClass);
229 jpcl.addClass(this.element, _jsPlumb.endpointAnchorClassPrefix + "_" + this._jsPlumb.currentAnchorClass);
230 this.connections = params.connections || [];
231 this.connectorPointerEvents = params["connector-pointer-events"];
233 this.scope = params.scope || _jsPlumb.getDefaultScope();
234 this.timestamp = null;
235 this.reattachConnections = params.reattach || _jsPlumb.Defaults.ReattachConnections;
236 this.connectionsDetachable = _jsPlumb.Defaults.ConnectionsDetachable;
237 if (params.connectionsDetachable === false || params.detachable === false)
238 this.connectionsDetachable = false;
239 this.dragAllowedWhenFull = params.dragAllowedWhenFull || true;
241 if (params.onMaxConnections)
242 this.bind("maxConnections", params.onMaxConnections);
245 // add a connection. not part of public API.
247 this.addConnection = function(connection) {
248 this.connections.push(connection);
249 this[(this.connections.length > 0 ? "add" : "remove") + "Class"](_jsPlumb.endpointConnectedClass);
250 this[(this.isFull() ? "add" : "remove") + "Class"](_jsPlumb.endpointFullClass);
253 this.detachFromConnection = function(connection, idx) {
254 idx = idx == null ? findConnectionIndex(connection, this) : idx;
256 this.connections.splice(idx, 1);
257 this[(this.connections.length > 0 ? "add" : "remove") + "Class"](_jsPlumb.endpointConnectedClass);
258 this[(this.isFull() ? "add" : "remove") + "Class"](_jsPlumb.endpointFullClass);
262 this.detach = function(connection, ignoreTarget, forceDetach, fireEvent, originalEvent, endpointBeingDeleted, connectionIndex) {
264 var idx = connectionIndex == null ? findConnectionIndex(connection, this) : connectionIndex,
265 actuallyDetached = false;
266 fireEvent = (fireEvent !== false);
269 if (forceDetach || connection._forceDetach || (connection.isDetachable() && connection.isDetachAllowed(connection) && this.isDetachAllowed(connection) )) {
271 //connection.setHover(false);
273 _jsPlumb.deleteObject({
274 connection:connection,
275 fireEvent:(!ignoreTarget && fireEvent),
276 originalEvent:originalEvent
278 actuallyDetached = true;
281 return actuallyDetached;
284 this.detachAll = function(fireEvent, originalEvent) {
285 while (this.connections.length > 0) {
286 // TODO this could pass the index in to the detach method to save some time (index will always be zero in this while loop)
287 // TODO now it defaults to fireEvent true. will that mess with things?
288 this.detach(this.connections[0], false, true, fireEvent !== false, originalEvent, this, 0);
292 this.detachFrom = function(targetEndpoint, fireEvent, originalEvent) {
294 for ( var i = 0; i < this.connections.length; i++) {
295 if (this.connections[i].endpoints[1] == targetEndpoint || this.connections[i].endpoints[0] == targetEndpoint) {
296 c.push(this.connections[i]);
299 for ( var j = 0; j < c.length; j++) {
300 this.detach(c[j], false, true, fireEvent, originalEvent);
305 this.getElement = function() {
309 // container not supported in 1.5.5; you cannot change the container once it is set.
310 // it might come back int a future release.
311 this.setElement = function(el/*, container*/) {
312 var parentId = this._jsPlumb.instance.getId(el),
313 curId = this.elementId;
314 // remove the endpoint from the list for the current endpoint's element
315 _ju.removeWithFunction(params.endpointsByElement[this.elementId], function(e) {
316 return e.id == this.id;
318 this.element = _dom(el);
319 this.elementId = _jsPlumb.getId(this.element);
320 _jsPlumb.anchorManager.rehomeEndpoint(this, curId, this.element);
321 _jsPlumb.dragManager.endpointAdded(this.element);
322 _ju.addToList(params.endpointsByElement, parentId, this);
327 * private but must be exposed.
329 this.makeInPlaceCopy = function() {
330 var loc = this.anchor.getCurrentLocation({element:this}),
331 o = this.anchor.getOrientation(this),
332 acc = this.anchor.getCssClass(),
335 compute:function() { return [ loc[0], loc[1] ]; },
336 getCurrentLocation : function() { return [ loc[0], loc[1] ]; },
337 getOrientation:function() { return o; },
338 getCssClass:function() { return acc; }
341 return _newEndpoint( {
342 anchor : inPlaceAnchor,
343 source : this.element,
344 paintStyle : this.getPaintStyle(),
345 endpoint : params.hideOnDrag ? "Blank" : this.endpoint,
353 * private but needs to be exposed.
355 this.isFloating = function() {
356 return this.anchor != null && this.anchor.isFloating;
360 * returns a connection from the pool; used when dragging starts. just gets the head of the array if it can.
362 this.connectorSelector = function() {
363 var candidate = this.connections[0];
364 if (this.isTarget && candidate) return candidate;
366 return (this.connections.length < this._jsPlumb.maxConnections) || this._jsPlumb.maxConnections == -1 ? null : candidate;
370 this.setStyle = this.setPaintStyle;
372 this.paint = function(params) {
373 params = params || {};
374 var timestamp = params.timestamp, recalc = !(params.recalc === false);
375 if (!timestamp || this.timestamp !== timestamp) {
377 // TODO check: is this is a safe performance enhancement?
378 var info = _jsPlumb.updateOffset({ elId:this.elementId, timestamp:timestamp/*, recalc:recalc*/ });
380 var xy = params.offset ? params.offset.o : info.o;
382 var ap = params.anchorPoint,connectorPaintStyle = params.connectorPaintStyle;
384 var wh = params.dimensions || info.s,
385 anchorParams = { xy : [ xy.left, xy.top ], wh : wh, element : this, timestamp : timestamp };
386 if (recalc && this.anchor.isDynamic && this.connections.length > 0) {
387 var c = findConnectionToUseForDynamicAnchor(this, params.elementWithPrecedence),
388 oIdx = c.endpoints[0] == this ? 1 : 0,
389 oId = oIdx === 0 ? c.sourceId : c.targetId,
390 oInfo = _jsPlumb.getCachedData(oId),
391 oOffset = oInfo.o, oWH = oInfo.s;
392 anchorParams.txy = [ oOffset.left, oOffset.top ];
393 anchorParams.twh = oWH;
394 anchorParams.tElement = c.endpoints[oIdx];
396 ap = this.anchor.compute(anchorParams);
399 this.endpoint.compute(ap, this.anchor.getOrientation(this), this._jsPlumb.paintStyleInUse, connectorPaintStyle || this.paintStyleInUse);
400 this.endpoint.paint(this._jsPlumb.paintStyleInUse, this.anchor);
401 this.timestamp = timestamp;
404 for ( var i = 0; i < this._jsPlumb.overlays.length; i++) {
405 var o = this._jsPlumb.overlays[i];
407 this._jsPlumb.overlayPlacements[i] = o.draw(this.endpoint, this._jsPlumb.paintStyleInUse);
408 o.paint(this._jsPlumb.overlayPlacements[i]);
415 this.repaint = this.paint;
417 var draggingInitialised = false;
418 this.initDraggable = function() {
419 // is this a connection source? we make it draggable and have the
420 // drag listener maintain a connection with a floating endpoint.
421 if (!draggingInitialised && jpcl.isDragSupported(this.element)) {
422 var placeholderInfo = { id:null, element:null },
425 existingJpcParams = null,
426 _dragHandler = _makeConnectionDragHandler(placeholderInfo, _jsPlumb);
428 var start = function() {
429 // drag might have started on an endpoint that is not actually a source, but which has
430 // one or more connections.
431 jpc = this.connectorSelector();
432 var _continue = true;
433 // if not enabled, return
434 if (!this.isEnabled()) _continue = false;
435 // if no connection and we're not a source, return.
436 if (jpc == null && !this.isSource) _continue = false;
437 // otherwise if we're full and not allowed to drag, also return false.
438 if (this.isSource && this.isFull() && !this.dragAllowedWhenFull) _continue = false;
439 // if the connection was setup as not detachable or one of its endpoints
440 // was setup as connectionsDetachable = false, or Defaults.ConnectionsDetachable
441 // is set to false...
442 if (jpc != null && !jpc.isDetachable()) _continue = false;
444 if (_continue === false) {
445 // this is for mootools and yui. returning false from this causes jquery to stop drag.
446 // the events are wrapped in both mootools and yui anyway, but i don't think returning
447 // false from the start callback would stop a drag.
448 if (jpcl.stopDrag) jpcl.stopDrag();
449 _dragHandler.stopDrag();
453 // clear hover for all connections for this endpoint before continuing.
454 for (var i = 0; i < this.connections.length; i++)
455 this.connections[i].setHover(false);
457 this.addClass("endpointDrag");
458 _jsPlumb.setConnectionBeingDragged(true);
460 // if we're not full but there was a connection, make it null. we'll create a new one.
461 if (jpc && !this.isFull() && this.isSource) jpc = null;
463 _jsPlumb.updateOffset( { elId : this.elementId });
464 inPlaceCopy = this.makeInPlaceCopy();
465 inPlaceCopy.referenceEndpoint = this;
468 _makeDraggablePlaceholder(placeholderInfo, this.parent, _jsPlumb);
470 // set the offset of this div to be where 'inPlaceCopy' is, to start with.
471 // TODO merge this code with the code in both Anchor and FloatingAnchor, because it
472 // does the same stuff.
473 var ipcoel = _gel(inPlaceCopy.canvas),
474 ipco = jsPlumb.CurrentLibrary.getOffset(ipcoel, _jsPlumb),
475 po = _jsPlumb.adjustForParentOffsetAndScroll([ipco.left, ipco.top], inPlaceCopy.canvas),
476 canvasElement = _gel(this.canvas);
478 jpcl.setOffset(placeholderInfo.element, {left:po[0], top:po[1]});
480 // when using makeSource and a parent, we first draw the source anchor on the source element, then
481 // move it to the parent. note that this happens after drawing the placeholder for the
483 if (this.parentAnchor) this.anchor = _jsPlumb.makeAnchor(this.parentAnchor, this.elementId, _jsPlumb);
485 // store the id of the dragging div and the source element. the drop function will pick these up.
486 _jsPlumb.setAttribute(this.canvas, "dragId", placeholderInfo.id);
487 _jsPlumb.setAttribute(this.canvas, "elId", this.elementId);
489 this._jsPlumb.floatingEndpoint = _makeFloatingEndpoint(this.getPaintStyle(), this.anchor, this.endpoint, this.canvas, placeholderInfo.element, _jsPlumb, _newEndpoint);
490 // TODO we should not know about DOM here. make the library adapter do this (or the
492 this.canvas.style.visibility = "hidden";
495 this.anchor.locked = true;
496 this.setHover(false, false);
497 // create a connection. one end is this endpoint, the other is a floating endpoint.
498 jpc = _newConnection({
499 sourceEndpoint : this,
500 targetEndpoint : this._jsPlumb.floatingEndpoint,
501 source : this.endpointWillMoveTo || this.element, // for makeSource with parent option. ensure source element is represented correctly.
502 target : placeholderInfo.element,
503 anchors : [ this.anchor, this._jsPlumb.floatingEndpoint.anchor ],
504 paintStyle : params.connectorStyle, // this can be null. Connection will use the default.
505 hoverPaintStyle:params.connectorHoverStyle,
506 connector : params.connector, // this can also be null. Connection will use the default.
507 overlays : params.connectorOverlays,
508 type:this.connectionType,
509 cssClass:this.connectorClass,
510 hoverClass:this.connectorHoverClass
512 jpc.pending = true; // mark this connection as not having been established.
513 jpc.addClass(_jsPlumb.draggingClass);
514 this._jsPlumb.floatingEndpoint.addClass(_jsPlumb.draggingClass);
515 // fire an event that informs that a connection is being dragged
516 _jsPlumb.fire("connectionDrag", jpc);
521 // if existing connection, allow to be dropped back on the source endpoint (issue 51).
522 _initDropTarget(ipcoel, false, true);
524 var anchorIdx = jpc.endpoints[0].id == this.id ? 0 : 1;
525 jpc.floatingAnchorIndex = anchorIdx; // save our anchor index as the connection's floating index.
526 this.detachFromConnection(jpc); // detach from the connection while dragging is occurring.
528 // store the original scope (issue 57)
529 var dragScope = jsPlumb.CurrentLibrary.getDragScope(canvasElement);
530 _jsPlumb.setAttribute(this.canvas, "originalScope", dragScope);
531 // now we want to get this endpoint's DROP scope, and set it for now: we can only be dropped on drop zones
532 // that have our drop scope (issue 57).
533 var dropScope = jpcl.getDropScope(canvasElement);
534 jpcl.setDragScope(canvasElement, dropScope);
536 // fire an event that informs that a connection is being dragged. we do this before
537 // replacing the original target with the floating element info.
538 _jsPlumb.fire("connectionDrag", jpc);
540 // now we replace ourselves with the temporary div we created above:
541 if (anchorIdx === 0) {
542 existingJpcParams = [ jpc.source, jpc.sourceId, canvasElement, dragScope ];
543 jpc.source = placeholderInfo.element;
544 jpc.sourceId = placeholderInfo.id;
546 existingJpcParams = [ jpc.target, jpc.targetId, canvasElement, dragScope ];
547 jpc.target = placeholderInfo.element;
548 jpc.targetId = placeholderInfo.id;
551 // lock the other endpoint; if it is dynamic it will not move while the drag is occurring.
552 jpc.endpoints[anchorIdx === 0 ? 1 : 0].anchor.locked = true;
553 // store the original endpoint and assign the new floating endpoint for the drag.
554 jpc.suspendedEndpoint = jpc.endpoints[anchorIdx];
556 // PROVIDE THE SUSPENDED ELEMENT, BE IT A SOURCE OR TARGET (ISSUE 39)
557 jpc.suspendedElement = jpc.endpoints[anchorIdx].getElement();
558 jpc.suspendedElementId = jpc.endpoints[anchorIdx].elementId;
559 jpc.suspendedElementType = anchorIdx === 0 ? "source" : "target";
561 jpc.suspendedEndpoint.setHover(false);
562 this._jsPlumb.floatingEndpoint.referenceEndpoint = jpc.suspendedEndpoint;
563 jpc.endpoints[anchorIdx] = this._jsPlumb.floatingEndpoint;
565 jpc.addClass(_jsPlumb.draggingClass);
566 this._jsPlumb.floatingEndpoint.addClass(_jsPlumb.draggingClass);
569 // register it and register connection on it.
570 floatingConnections[placeholderInfo.id] = jpc;
571 _jsPlumb.anchorManager.addFloatingConnection(placeholderInfo.id, jpc);
572 // only register for the target endpoint; we will not be dragging the source at any time
573 // before this connection is either discarded or made into a permanent connection.
574 _ju.addToList(params.endpointsByElement, placeholderInfo.id, this._jsPlumb.floatingEndpoint);
575 // tell jsplumb about it
576 _jsPlumb.currentlyDragging = true;
579 var dragOptions = params.dragOptions || {},
580 defaultOpts = jsPlumb.extend( {}, jpcl.defaultDragOptions),
581 startEvent = jpcl.dragEvents.start,
582 stopEvent = jpcl.dragEvents.stop,
583 dragEvent = jpcl.dragEvents.drag;
585 dragOptions = jsPlumb.extend(defaultOpts, dragOptions);
586 dragOptions.scope = dragOptions.scope || this.scope;
587 dragOptions[startEvent] = _ju.wrap(dragOptions[startEvent], start, false);
588 // extracted drag handler function so can be used by makeSource
589 dragOptions[dragEvent] = _ju.wrap(dragOptions[dragEvent], _dragHandler.drag);
590 dragOptions[stopEvent] = _ju.wrap(dragOptions[stopEvent],
593 _jsPlumb.setConnectionBeingDragged(false);
594 // if no endpoints, jpc already cleaned up.
595 if (jpc.endpoints != null) {
596 // get the actual drop event (decode from library args to stop function)
597 var originalEvent = jpcl.getDropEvent(arguments);
598 // unlock the other endpoint (if it is dynamic, it would have been locked at drag start)
599 var idx = jpc.floatingAnchorIndex == null ? 1 : jpc.floatingAnchorIndex;
600 jpc.endpoints[idx === 0 ? 1 : 0].anchor.locked = false;
601 // WHY does this need to happen? i suppose because the connection might not get
602 // deleted. TODO: i dont want to know about css classes inside jsplumb, ideally.
603 jpc.removeClass(_jsPlumb.draggingClass);
605 // if we have the floating endpoint then the connection has not been dropped
606 // on another endpoint. If it is a new connection we throw it away. If it is an
607 // existing connection we check to see if we should reattach it, throwing it away
609 if (jpc.endpoints[idx] == this._jsPlumb.floatingEndpoint) {
610 // 6a. if the connection was an existing one...
611 if (existingJpc && jpc.suspendedEndpoint) {
612 // fix for issue35, thanks Sylvain Gizard: when firing the detach event make sure the
613 // floating endpoint has been replaced.
615 jpc.source = existingJpcParams[0];
616 jpc.sourceId = existingJpcParams[1];
618 jpc.target = existingJpcParams[0];
619 jpc.targetId = existingJpcParams[1];
622 // restore the original scope (issue 57)
623 jpcl.setDragScope(existingJpcParams[2], existingJpcParams[3]);
624 jpc.endpoints[idx] = jpc.suspendedEndpoint;
625 // IF the connection should be reattached, or the other endpoint refuses detach, then
626 // reset the connection to its original state
627 if (jpc.isReattach() || jpc._forceReattach || jpc._forceDetach || !jpc.endpoints[idx === 0 ? 1 : 0].detach(jpc, false, false, true, originalEvent)) {
629 jpc.floatingAnchorIndex = null;
630 jpc._forceDetach = null;
631 jpc._forceReattach = null;
632 this._jsPlumb.floatingEndpoint.detachFromConnection(jpc);
633 jpc.suspendedEndpoint.addConnection(jpc);
634 _jsPlumb.repaint(existingJpcParams[1]);
640 // remove the element associated with the floating endpoint
641 // (and its associated floating endpoint and visual artefacts)
642 _jsPlumb.remove(placeholderInfo.element, false);
643 // remove the inplace copy
644 _jsPlumb.remove(inPlaceCopy.canvas, false);
646 // makeTargets sets this flag, to tell us we have been replaced and should delete ourself.
647 if (this.deleteAfterDragStop) {
648 _jsPlumb.deleteObject({endpoint:this});
652 this._jsPlumb.floatingEndpoint = null;
653 // repaint this endpoint.
654 // make our canvas visible (TODO: hand off to library; we should not know about DOM)
655 this.canvas.style.visibility = "visible";
657 this.anchor.locked = false;
658 this.paint({recalc:false});
662 // although the connection is no longer valid, there are use cases where this is useful.
663 _jsPlumb.fire("connectionDragStop", jpc, originalEvent);
665 // tell jsplumb that dragging is finished.
666 _jsPlumb.currentlyDragging = false;
672 var i = _gel(this.canvas);
673 jpcl.initDraggable(i, dragOptions, true, _jsPlumb);
675 draggingInitialised = true;
679 // if marked as source or target at create time, init the dragging.
680 if (this.isSource || this.isTarget)
681 this.initDraggable();
683 // pulled this out into a function so we can reuse it for the inPlaceCopy canvas; you can now drop detached connections
684 // back onto the endpoint you detached it from.
685 var _initDropTarget = function(canvas, forceInit, isTransient, endpoint) {
686 if ((this.isTarget || forceInit) && jpcl.isDropSupported(this.element)) {
687 var dropOptions = params.dropOptions || _jsPlumb.Defaults.DropOptions || jsPlumb.Defaults.DropOptions;
688 dropOptions = jsPlumb.extend( {}, dropOptions);
689 dropOptions.scope = dropOptions.scope || this.scope;
690 var dropEvent = jpcl.dragEvents.drop,
691 overEvent = jpcl.dragEvents.over,
692 outEvent = jpcl.dragEvents.out,
695 this.removeClass(_jsPlumb.endpointDropAllowedClass);
696 this.removeClass(_jsPlumb.endpointDropForbiddenClass);
698 var originalEvent = jpcl.getDropEvent(arguments),
699 draggable = _gel(jpcl.getDragObject(arguments)),
700 id = _jsPlumb.getAttribute(draggable, "dragId"),
701 elId = _jsPlumb.getAttribute(draggable, "elId"),
702 scope = _jsPlumb.getAttribute(draggable, "originalScope"),
703 jpc = floatingConnections[id];
705 // if this is a drop back where the connection came from, mark it force rettach and
706 // return; the stop handler will reattach. without firing an event.
707 var redrop = jpc.suspendedEndpoint && (jpc.suspendedEndpoint.id == this.id ||
708 this.referenceEndpoint && jpc.suspendedEndpoint.id == this.referenceEndpoint.id) ;
710 jpc._forceReattach = true;
715 var idx = jpc.floatingAnchorIndex == null ? 1 : jpc.floatingAnchorIndex, oidx = idx === 0 ? 1 : 0;
717 // restore the original scope if necessary (issue 57)
718 if (scope) jsPlumb.CurrentLibrary.setDragScope(draggable, scope);
720 var endpointEnabled = endpoint != null ? endpoint.isEnabled() : true;
723 this.fire("maxConnections", {
726 maxConnections:this._jsPlumb.maxConnections
730 if (!this.isFull() && !(idx === 0 && !this.isSource) && !(idx == 1 && !this.isTarget) && endpointEnabled) {
731 var _doContinue = true;
733 // the second check here is for the case that the user is dropping it back
734 // where it came from.
735 if (jpc.suspendedEndpoint && jpc.suspendedEndpoint.id != this.id) {
737 jpc.source = jpc.suspendedEndpoint.element;
738 jpc.sourceId = jpc.suspendedEndpoint.elementId;
740 jpc.target = jpc.suspendedEndpoint.element;
741 jpc.targetId = jpc.suspendedEndpoint.elementId;
744 if (!jpc.isDetachAllowed(jpc) || !jpc.endpoints[idx].isDetachAllowed(jpc) || !jpc.suspendedEndpoint.isDetachAllowed(jpc) || !_jsPlumb.checkCondition("beforeDetach", jpc))
748 // these have to be set before testing for beforeDrop.
750 jpc.source = this.element;
751 jpc.sourceId = this.elementId;
753 jpc.target = this.element;
754 jpc.targetId = this.elementId;
757 // ------------ wrap the execution path in a function so we can support asynchronous beforeDrop
759 // we want to execute this regardless.
760 var commonFunction = function() {
761 jpc.floatingAnchorIndex = null;
764 var continueFunction = function() {
767 // remove this jpc from the current endpoint
768 jpc.endpoints[idx].detachFromConnection(jpc);
769 if (jpc.suspendedEndpoint) jpc.suspendedEndpoint.detachFromConnection(jpc);
770 jpc.endpoints[idx] = this;
771 this.addConnection(jpc);
773 // copy our parameters in to the connection:
774 var params = this.getParameters();
775 for (var aParam in params)
776 jpc.setParameter(aParam, params[aParam]);
778 if (!jpc.suspendedEndpoint) {
779 // if not an existing connection and
780 if (params.draggable)
781 jsPlumb.CurrentLibrary.initDraggable(this.element, dragOptions, true, _jsPlumb);
784 var suspendedElement = jpc.suspendedEndpoint.getElement(), suspendedElementId = jpc.suspendedEndpoint.elementId;
787 originalSourceId:idx === 0 ? suspendedElementId : jpc.sourceId,
788 newSourceId:idx === 0 ? this.elementId : jpc.sourceId,
789 originalTargetId:idx == 1 ? suspendedElementId : jpc.targetId,
790 newTargetId:idx == 1 ? this.elementId : jpc.targetId,
791 originalSourceEndpoint:idx === 0 ? jpc.suspendedEndpoint : jpc.endpoints[0],
792 newSourceEndpoint:idx === 0 ? this : jpc.endpoints[0],
793 originalTargetEndpoint:idx == 1 ? jpc.suspendedEndpoint : jpc.endpoints[1],
794 newTargetEndpoint:idx == 1 ? this : jpc.endpoints[1],
797 /* var suspendedElement = jpc.suspendedEndpoint.getElement(), suspendedElementId = jpc.suspendedEndpoint.elementId;
798 // fire a detach event
800 source : idx === 0 ? suspendedElement : jpc.source,
801 target : idx == 1 ? suspendedElement : jpc.target,
802 sourceId : idx === 0 ? suspendedElementId : jpc.sourceId,
803 targetId : idx == 1 ? suspendedElementId : jpc.targetId,
804 sourceEndpoint : idx === 0 ? jpc.suspendedEndpoint : jpc.endpoints[0],
805 targetEndpoint : idx == 1 ? jpc.suspendedEndpoint : jpc.endpoints[1],
807 }, true, originalEvent);*/
810 // TODO this is like the makeTarget drop code.
812 _jsPlumb.anchorManager.updateOtherEndpoint(jpc.sourceId, jpc.suspendedElementId, jpc.targetId, jpc);
814 _jsPlumb.anchorManager.sourceChanged(jpc.suspendedEndpoint.elementId, jpc.sourceId, jpc);
816 // finalise will inform the anchor manager and also add to
817 // connectionsByScope if necessary.
818 // TODO if this is not set to true, then dragging a connection's target to a new
819 // target causes the connection to be forgotten. however if it IS set to true, then
820 // the opposite happens: dragging by source causes the connection to get forgotten
821 // about and then if you delete it jsplumb breaks.
822 _finaliseConnection(jpc, null, originalEvent/*, true*/);
827 var dontContinueFunction = function() {
828 // otherwise just put it back on the endpoint it was on before the drag.
829 if (jpc.suspendedEndpoint) {
830 jpc.endpoints[idx] = jpc.suspendedEndpoint;
832 jpc._forceDetach = true;
834 jpc.source = jpc.suspendedEndpoint.element;
835 jpc.sourceId = jpc.suspendedEndpoint.elementId;
837 jpc.target = jpc.suspendedEndpoint.element;
838 jpc.targetId = jpc.suspendedEndpoint.elementId;
840 jpc.suspendedEndpoint.addConnection(jpc);
842 jpc.endpoints[0].repaint();
844 _jsPlumb.repaint(jpc.sourceId);
845 jpc._forceDetach = false;
851 // --------------------------------------
852 // now check beforeDrop. this will be available only on Endpoints that are setup to
853 // have a beforeDrop condition (although, secretly, under the hood all Endpoints and
854 // the Connection have them, because they are on jsPlumbUIComponent. shhh!), because
855 // it only makes sense to have it on a target endpoint.
856 _doContinue = _doContinue && this.isDropAllowed(jpc.sourceId, jpc.targetId, jpc.scope, jpc, this);
862 dontContinueFunction();
865 _jsPlumb.currentlyDragging = false;
869 dropOptions[dropEvent] = _ju.wrap(dropOptions[dropEvent], drop);
870 dropOptions[overEvent] = _ju.wrap(dropOptions[overEvent], function() {
871 var draggable = jpcl.getDragObject(arguments),
872 id = _jsPlumb.getAttribute(draggable, "dragId"),
873 _jpc = floatingConnections[id];
876 var idx = _jpc.floatingAnchorIndex == null ? 1 : _jpc.floatingAnchorIndex;
877 // here we should fire the 'over' event if we are a target and this is a new connection,
878 // or we are the same as the floating endpoint.
879 var _cont = (this.isTarget && _jpc.floatingAnchorIndex !== 0) || (_jpc.suspendedEndpoint && this.referenceEndpoint && this.referenceEndpoint.id == _jpc.suspendedEndpoint.id);
881 var bb = _jsPlumb.checkCondition("checkDropAllowed", {
882 sourceEndpoint:_jpc.endpoints[idx],
886 this[(bb ? "add" : "remove") + "Class"](_jsPlumb.endpointDropAllowedClass);
887 this[(bb ? "remove" : "add") + "Class"](_jsPlumb.endpointDropForbiddenClass);
888 _jpc.endpoints[idx].anchor.over(this.anchor, this);
893 dropOptions[outEvent] = _ju.wrap(dropOptions[outEvent], function() {
894 var draggable = jpcl.getDragObject(arguments),
895 id = _jsPlumb.getAttribute( draggable, "dragId"),
896 _jpc = floatingConnections[id];
899 var idx = _jpc.floatingAnchorIndex == null ? 1 : _jpc.floatingAnchorIndex;
900 var _cont = (this.isTarget && _jpc.floatingAnchorIndex !== 0) || (_jpc.suspendedEndpoint && this.referenceEndpoint && this.referenceEndpoint.id == _jpc.suspendedEndpoint.id);
902 this.removeClass(_jsPlumb.endpointDropAllowedClass);
903 this.removeClass(_jsPlumb.endpointDropForbiddenClass);
904 _jpc.endpoints[idx].anchor.out();
908 jpcl.initDroppable(canvas, dropOptions, true, isTransient);
912 // initialise the endpoint's canvas as a drop target. this will be ignored if the endpoint is not a target or drag is not supported.
913 _initDropTarget(_gel(this.canvas), true, !(params._transient || this.anchor.isFloating), this);
915 // finally, set type if it was provided
917 this.addType(params.type, params.data, _jsPlumb.isSuspendDrawing());
922 jsPlumbUtil.extend(jsPlumb.Endpoint, OverlayCapableJsPlumbUIComponent, {
923 getTypeDescriptor : function() { return "endpoint"; },
924 isVisible : function() { return this._jsPlumb.visible; },
925 setVisible : function(v, doNotChangeConnections, doNotNotifyOtherEndpoint) {
926 this._jsPlumb.visible = v;
927 if (this.canvas) this.canvas.style.display = v ? "block" : "none";
928 this[v ? "showOverlays" : "hideOverlays"]();
929 if (!doNotChangeConnections) {
930 for (var i = 0; i < this.connections.length; i++) {
931 this.connections[i].setVisible(v);
932 if (!doNotNotifyOtherEndpoint) {
933 var oIdx = this === this.connections[i].endpoints[0] ? 1 : 0;
934 // only change the other endpoint if this is its only connection.
935 if (this.connections[i].endpoints[oIdx].connections.length == 1) this.connections[i].endpoints[oIdx].setVisible(v, true, true);
940 getAttachedElements : function() {
941 return this.connections;
943 applyType : function(t, doNotRepaint) {
944 if (t.maxConnections != null) this._jsPlumb.maxConnections = t.maxConnections;
945 if (t.scope) this.scope = t.scope;
946 jsPlumbUtil.copyValues(typeParameters, t, this);
948 this.anchor = this._jsPlumb.instance.makeAnchor(t.anchor);
951 isEnabled : function() { return this._jsPlumb.enabled; },
952 setEnabled : function(e) { this._jsPlumb.enabled = e; },
953 cleanup : function() {
954 jsPlumb.CurrentLibrary.removeClass(this.element, this._jsPlumb.instance.endpointAnchorClassPrefix + "_" + this._jsPlumb.currentAnchorClass);
956 this.endpoint.cleanup();
957 this.endpoint.destroy();
958 this.endpoint = null;
960 var i = jsPlumb.CurrentLibrary.getElementObject(this.canvas);
961 jsPlumb.CurrentLibrary.destroyDraggable(i);
962 jsPlumb.CurrentLibrary.destroyDroppable(i);
964 setHover : function(h) {
965 if (this.endpoint && this._jsPlumb && !this._jsPlumb.instance.isConnectionBeingDragged())
966 this.endpoint.setHover(h);
968 isFull : function() {
969 return !(this.isFloating() || this._jsPlumb.maxConnections < 1 || this.connections.length < this._jsPlumb.maxConnections);
971 getConnectionCost : function() { return this._jsPlumb.connectionCost; },
972 setConnectionCost : function(c) {
973 this._jsPlumb.connectionCost = c;
975 areConnectionsDirected : function() { return this._jsPlumb.connectionsDirected; },
976 setConnectionsDirected : function(b) { this._jsPlumb.connectionsDirected = b; },
977 setElementId : function(_elId) {
978 this.elementId = _elId;
979 this.anchor.elementId = _elId;
981 setReferenceElement : function(_el) {
982 this.element = jsPlumb.CurrentLibrary.getDOMElement(_el);
984 setDragAllowedWhenFull : function(allowed) {
985 this.dragAllowedWhenFull = allowed;
987 equals : function(endpoint) {
988 return this.anchor.equals(endpoint.anchor);
990 getUuid : function() {
991 return this._jsPlumb.uuid;
993 computeAnchor : function(params) {
994 return this.anchor.compute(params);