3 * @description Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
6 * - [Demo Site](http://jsplumb.org)
7 * - [GitHub](http://github.com/sporritt/jsplumb)
9 * Dual licensed under the MIT and GPL2 licenses.
11 * Copyright (c) 2010 - 2013 Simon Porritt (simon.porritt@gmail.com)
15 var _ju = jsPlumbUtil,
16 _addClass = function(el, clazz) { jsPlumb.CurrentLibrary.addClass(_gel(el), clazz); },
17 _hasClass = function(el, clazz) { return jsPlumb.CurrentLibrary.hasClass(_gel(el), clazz); },
18 _removeClass = function(el, clazz) { jsPlumb.CurrentLibrary.removeClass(_gel(el), clazz); },
19 _gel = function(el) { return jsPlumb.CurrentLibrary.getElementObject(el); },
20 _dom = function(el) { return jsPlumb.CurrentLibrary.getDOMElement(el); },
21 _getOffset = function(el, _instance) {
22 var o = jsPlumb.CurrentLibrary.getOffset(_gel(el));
23 if (_instance != null) {
24 var z = _instance.getZoom();
25 return {left:o.left / z, top:o.top / z };
30 _getSize = function(el) {
31 return jsPlumb.CurrentLibrary.getSize(_gel(el));
35 * creates a timestamp, using milliseconds since 1970, but as a string.
37 _timestamp = function() { return "" + (new Date()).getTime(); },
39 // helper method to update the hover style whenever it, or paintStyle, changes.
40 // we use paintStyle as the foundation and merge hoverPaintStyle over the
42 _updateHoverStyle = function(component) {
43 if (component._jsPlumb.paintStyle && component._jsPlumb.hoverPaintStyle) {
44 var mergedHoverStyle = {};
45 jsPlumb.extend(mergedHoverStyle, component._jsPlumb.paintStyle);
46 jsPlumb.extend(mergedHoverStyle, component._jsPlumb.hoverPaintStyle);
47 delete component._jsPlumb.hoverPaintStyle;
48 // we want the fillStyle of paintStyle to override a gradient, if possible.
49 if (mergedHoverStyle.gradient && component._jsPlumb.paintStyle.fillStyle)
50 delete mergedHoverStyle.gradient;
51 component._jsPlumb.hoverPaintStyle = mergedHoverStyle;
54 events = [ "click", "dblclick", "mouseenter", "mouseout", "mousemove", "mousedown", "mouseup", "contextmenu" ],
55 eventFilters = { "mouseout":"mouseexit" },
56 _updateAttachedElements = function(component, state, timestamp, sourceElement) {
57 var affectedElements = component.getAttachedElements();
58 if (affectedElements) {
59 for (var i = 0, j = affectedElements.length; i < j; i++) {
60 if (!sourceElement || sourceElement != affectedElements[i])
61 affectedElements[i].setHover(state, true, timestamp); // tell the attached elements not to inform their own attached elements.
65 _splitType = function(t) { return t == null ? null : t.split(" "); },
66 _applyTypes = function(component, params, doNotRepaint) {
67 if (component.getDefaultType) {
68 var td = component.getTypeDescriptor();
70 var o = _ju.merge({}, component.getDefaultType());
71 for (var i = 0, j = component._jsPlumb.types.length; i < j; i++)
72 o = _ju.merge(o, component._jsPlumb.instance.getType(component._jsPlumb.types[i], td));
75 o = _ju.populate(o, params);
78 component.applyType(o, doNotRepaint);
79 if (!doNotRepaint) component.repaint();
83 // ------------------------------ BEGIN jsPlumbUIComponent --------------------------------------------
85 jsPlumbUIComponent = window.jsPlumbUIComponent = function(params) {
87 jsPlumbUtil.EventGenerator.apply(this, arguments);
91 idPrefix = self.idPrefix,
92 id = idPrefix + (new Date()).getTime(),
93 jpcl = jsPlumb.CurrentLibrary;
96 instance: params._jsPlumb,
97 parameters:params.parameters || {},
100 paintStyleInUse:null,
102 beforeDetach:params.beforeDetach,
103 beforeDrop:params.beforeDrop,
104 overlayPlacements : [],
105 hoverClass: params.hoverClass || params._jsPlumb.Defaults.HoverClass || jsPlumb.Defaults.HoverClass,
109 this.getId = function() { return id; };
111 // all components can generate events
114 for (var i in params.events)
115 self.bind(i, params.events[i]);
118 // all components get this clone function.
119 // TODO issue 116 showed a problem with this - it seems 'a' that is in
120 // the clone function's scope is shared by all invocations of it, the classic
121 // JS closure problem. for now, jsPlumb does a version of this inline where
122 // it used to call clone. but it would be nice to find some time to look
124 this.clone = function() {
125 var o = {};//new Object();
126 this.constructor.apply(o, a);
130 // user can supply a beforeDetach callback, which will be executed before a detach
131 // is performed; returning false prevents the detach.
132 this.isDetachAllowed = function(connection) {
134 if (this._jsPlumb.beforeDetach) {
136 r = this._jsPlumb.beforeDetach(connection);
138 catch (e) { _ju.log("jsPlumb: beforeDetach callback failed", e); }
143 // user can supply a beforeDrop callback, which will be executed before a dropped
144 // connection is confirmed. user can return false to reject connection.
145 this.isDropAllowed = function(sourceId, targetId, scope, connection, dropEndpoint) {
146 var r = this._jsPlumb.instance.checkCondition("beforeDrop", {
150 connection:connection,
151 dropEndpoint:dropEndpoint
153 if (this._jsPlumb.beforeDrop) {
155 r = this._jsPlumb.beforeDrop({
159 connection:connection,
160 dropEndpoint:dropEndpoint
163 catch (e) { _ju.log("jsPlumb: beforeDrop callback failed", e); }
168 var boundListeners = [],
169 bindAListener = function(obj, type, fn) {
170 boundListeners.push([obj, type, fn]);
174 bindOne = function(o, c, evt) {
175 var filteredEvent = eventFilters[evt] || evt,
177 c.fire(filteredEvent, c, ee);
179 domListeners.push([o, evt, fn]);
180 jpcl.bind(o, evt, fn);
182 unbindOne = function(o, evt, fn) {
183 var filteredEvent = eventFilters[evt] || evt;
184 jpcl.unbind(o, evt, fn);
187 this.bindListeners = function(obj, _self, _hoverFunction) {
188 bindAListener(obj, "click", function(ep, e) { _self.fire("click", _self, e); });
189 bindAListener(obj, "dblclick", function(ep, e) { _self.fire("dblclick", _self, e); });
190 bindAListener(obj, "contextmenu", function(ep, e) { _self.fire("contextmenu", _self, e); });
191 bindAListener(obj, "mouseenter", function(ep, e) {
192 if (!_self.isHover()) {
193 _hoverFunction(true);
194 _self.fire("mouseenter", _self, e);
197 bindAListener(obj, "mouseexit", function(ep, e) {
198 if (_self.isHover()) {
199 _hoverFunction(false);
200 _self.fire("mouseexit", _self, e);
203 bindAListener(obj, "mousedown", function(ep, e) { _self.fire("mousedown", _self, e); });
204 bindAListener(obj, "mouseup", function(ep, e) { _self.fire("mouseup", _self, e); });
207 this.unbindListeners = function() {
208 for (var i = 0; i < boundListeners.length; i++) {
209 var o = boundListeners[i];
210 o[0].unbind(o[1], o[2]);
212 boundListeners = null;
215 this.attachListeners = function(o, c) {
216 for (var i = 0, j = events.length; i < j; i++) {
217 bindOne(o, c, events[i]);
220 this.detachListeners = function() {
221 for (var i = 0; i < domListeners.length; i++) {
222 unbindOne(domListeners[i][0], domListeners[i][1], domListeners[i][2]);
227 this.reattachListenersForElement = function(o) {
228 if (arguments.length > 1) {
229 for (var i = 0, j = events.length; i < j; i++)
230 unbindOne(o, events[i]);
231 for (i = 1, j = arguments.length; i < j; i++)
232 this.attachListeners(o, arguments[i]);
237 jsPlumbUtil.extend(jsPlumbUIComponent, jsPlumbUtil.EventGenerator, {
239 getParameter : function(name) {
240 return this._jsPlumb.parameters[name];
243 setParameter : function(name, value) {
244 this._jsPlumb.parameters[name] = value;
247 getParameters : function() {
248 return this._jsPlumb.parameters;
251 setParameters : function(p) {
252 this._jsPlumb.parameters = p;
255 addClass : function(clazz) {
256 if (this.canvas != null)
257 _addClass(this.canvas, clazz);
260 removeClass : function(clazz) {
261 if (this.canvas != null)
262 _removeClass(this.canvas, clazz);
265 setType : function(typeId, params, doNotRepaint) {
266 this._jsPlumb.types = _splitType(typeId) || [];
267 _applyTypes(this, params, doNotRepaint);
270 getType : function() {
271 return this._jsPlumb.types;
274 reapplyTypes : function(params, doNotRepaint) {
275 _applyTypes(this, params, doNotRepaint);
278 hasType : function(typeId) {
279 return jsPlumbUtil.indexOf(this._jsPlumb.types, typeId) != -1;
282 addType : function(typeId, params, doNotRepaint) {
283 var t = _splitType(typeId), _cont = false;
285 for (var i = 0, j = t.length; i < j; i++) {
286 if (!this.hasType(t[i])) {
287 this._jsPlumb.types.push(t[i]);
291 if (_cont) _applyTypes(this, params, doNotRepaint);
295 removeType : function(typeId, doNotRepaint) {
296 var t = _splitType(typeId), _cont = false, _one = function(tt) {
297 var idx = _ju.indexOf(this._jsPlumb.types, tt);
299 this._jsPlumb.types.splice(idx, 1);
306 for (var i = 0,j = t.length; i < j; i++) {
307 _cont = _one(t[i]) || _cont;
309 if (_cont) _applyTypes(this, null, doNotRepaint);
313 toggleType : function(typeId, params, doNotRepaint) {
314 var t = _splitType(typeId);
316 for (var i = 0, j = t.length; i < j; i++) {
317 var idx = jsPlumbUtil.indexOf(this._jsPlumb.types, t[i]);
319 this._jsPlumb.types.splice(idx, 1);
321 this._jsPlumb.types.push(t[i]);
324 _applyTypes(this, params, doNotRepaint);
327 applyType : function(t, doNotRepaint) {
328 this.setPaintStyle(t.paintStyle, doNotRepaint);
329 this.setHoverPaintStyle(t.hoverPaintStyle, doNotRepaint);
331 for (var i in t.parameters)
332 this.setParameter(i, t.parameters[i]);
335 setPaintStyle : function(style, doNotRepaint) {
336 // this._jsPlumb.paintStyle = jsPlumb.extend({}, style);
337 // TODO figure out if we want components to clone paintStyle so as not to share it.
338 this._jsPlumb.paintStyle = style;
339 this._jsPlumb.paintStyleInUse = this._jsPlumb.paintStyle;
340 _updateHoverStyle(this);
341 if (!doNotRepaint) this.repaint();
343 getPaintStyle : function() {
344 return this._jsPlumb.paintStyle;
346 setHoverPaintStyle : function(style, doNotRepaint) {
347 //this._jsPlumb.hoverPaintStyle = jsPlumb.extend({}, style);
348 // TODO figure out if we want components to clone paintStyle so as not to share it.
349 this._jsPlumb.hoverPaintStyle = style;
350 _updateHoverStyle(this);
351 if (!doNotRepaint) this.repaint();
353 getHoverPaintStyle : function() {
354 return this._jsPlumb.hoverPaintStyle;
357 this.unbindListeners();
358 this.detachListeners();
361 this.cleanupListeners();
363 this._jsPlumb = null;
366 isHover : function() { return this._jsPlumb.hover; },
368 setHover : function(hover, ignoreAttachedElements, timestamp) {
369 var jpcl = jsPlumb.CurrentLibrary;
370 // while dragging, we ignore these events. this keeps the UI from flashing and
371 // swishing and whatevering.
372 if (this._jsPlumb && !this._jsPlumb.instance.currentlyDragging && !this._jsPlumb.instance.isHoverSuspended()) {
374 this._jsPlumb.hover = hover;
376 if (this.canvas != null) {
377 if (this._jsPlumb.instance.hoverClass != null) {
378 jpcl[hover ? "addClass" : "removeClass"](this.canvas, this._jsPlumb.instance.hoverClass);
381 if (this._jsPlumb.hoverPaintStyle != null) {
382 this._jsPlumb.paintStyleInUse = hover ? this._jsPlumb.hoverPaintStyle : this._jsPlumb.paintStyle;
383 if (!this._jsPlumb.instance.isSuspendDrawing()) {
384 timestamp = timestamp || _timestamp();
385 this.repaint({timestamp:timestamp, recalc:false});
388 // get the list of other affected elements, if supported by this component.
389 // for a connection, its the endpoints. for an endpoint, its the connections! surprise.
390 if (this.getAttachedElements && !ignoreAttachedElements)
391 _updateAttachedElements(this, hover, _timestamp(), this);
396 // ------------------------------ END jsPlumbUIComponent --------------------------------------------
398 // ------------------------------ BEGIN OverlayCapablejsPlumbUIComponent --------------------------------------------
400 var _internalLabelOverlayId = "__label",
401 // helper to get the index of some overlay
402 _getOverlayIndex = function(component, id) {
404 for (var i = 0, j = component._jsPlumb.overlays.length; i < j; i++) {
405 if (id === component._jsPlumb.overlays[i].id) {
412 // this is a shortcut helper method to let people add a label as
414 _makeLabelOverlay = function(component, params) {
417 cssClass:params.cssClass,
418 labelStyle : component.labelStyle,
419 id:_internalLabelOverlayId,
421 _jsPlumb:component._jsPlumb.instance // TODO not necessary, since the instance can be accessed through the component.
423 mergedParams = jsPlumb.extend(_params, params);
425 return new jsPlumb.Overlays[component._jsPlumb.instance.getRenderMode()].Label( mergedParams );
427 _processOverlay = function(component, o) {
428 var _newOverlay = null;
429 if (_ju.isArray(o)) { // this is for the shorthand ["Arrow", { width:50 }] syntax
430 // there's also a three arg version:
431 // ["Arrow", { width:50 }, {location:0.7}]
432 // which merges the 3rd arg into the 2nd.
434 // make a copy of the object so as not to mess up anyone else's reference...
435 p = jsPlumb.extend({component:component, _jsPlumb:component._jsPlumb.instance}, o[1]);
436 if (o.length == 3) jsPlumb.extend(p, o[2]);
437 _newOverlay = new jsPlumb.Overlays[component._jsPlumb.instance.getRenderMode()][type](p);
438 } else if (o.constructor == String) {
439 _newOverlay = new jsPlumb.Overlays[component._jsPlumb.instance.getRenderMode()][o]({component:component, _jsPlumb:component._jsPlumb.instance});
444 component._jsPlumb.overlays.push(_newOverlay);
446 _calculateOverlaysToAdd = function(component, params) {
447 var defaultKeys = component.defaultOverlayKeys || [], o = params.overlays,
448 checkKey = function(k) {
449 return component._jsPlumb.instance.Defaults[k] || jsPlumb.Defaults[k] || [];
454 for (var i = 0, j = defaultKeys.length; i < j; i++)
455 o.unshift.apply(o, checkKey(defaultKeys[i]));
459 OverlayCapableJsPlumbUIComponent = window.OverlayCapableJsPlumbUIComponent = function(params) {
461 jsPlumbUIComponent.apply(this, arguments);
462 this._jsPlumb.overlays = [];
464 var _overlays = _calculateOverlaysToAdd(this, params);
466 for (var i = 0, j = _overlays.length; i < j; i++) {
467 _processOverlay(this, _overlays[i]);
472 var loc = params.labelLocation || this.defaultLabelLocation || 0.5,
473 labelStyle = params.labelStyle || this._jsPlumb.instance.Defaults.LabelStyle || jsPlumb.Defaults.LabelStyle;
475 this._jsPlumb.overlays.push(_makeLabelOverlay(this, {
478 labelStyle:labelStyle
483 jsPlumbUtil.extend(OverlayCapableJsPlumbUIComponent, jsPlumbUIComponent, {
484 applyType : function(t, doNotRepaint) {
485 this.removeAllOverlays(doNotRepaint);
487 for (var i = 0, j = t.overlays.length; i < j; i++)
488 this.addOverlay(t.overlays[i], true);
491 setHover : function(hover, ignoreAttachedElements, timestamp) {
492 if (this._jsPlumb && !this._jsPlumb.instance.isConnectionBeingDragged()) {
493 for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++) {
494 this._jsPlumb.overlays[i][hover ? "addClass":"removeClass"](this._jsPlumb.instance.hoverClass);
498 addOverlay : function(overlay, doNotRepaint) {
499 _processOverlay(this, overlay);
500 if (!doNotRepaint) this.repaint();
502 getOverlay : function(id) {
503 var idx = _getOverlayIndex(this, id);
504 return idx >= 0 ? this._jsPlumb.overlays[idx] : null;
506 getOverlays : function() {
507 return this._jsPlumb.overlays;
509 hideOverlay : function(id) {
510 var o = this.getOverlay(id);
513 hideOverlays : function() {
514 for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++)
515 this._jsPlumb.overlays[i].hide();
517 showOverlay : function(id) {
518 var o = this.getOverlay(id);
521 showOverlays : function() {
522 for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++)
523 this._jsPlumb.overlays[i].show();
525 removeAllOverlays : function(doNotRepaint) {
526 for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++) {
527 if (this._jsPlumb.overlays[i].cleanup) this._jsPlumb.overlays[i].cleanup();
530 this._jsPlumb.overlays.splice(0, this._jsPlumb.overlays.length);
531 this._jsPlumb.overlayPositions = null;
535 removeOverlay : function(overlayId) {
536 var idx = _getOverlayIndex(this, overlayId);
538 var o = this._jsPlumb.overlays[idx];
539 if (o.cleanup) o.cleanup();
540 this._jsPlumb.overlays.splice(idx, 1);
541 this._jsPlumb.overlayPositions && delete this._jsPlumb.overlayPositions[overlayId];
544 removeOverlays : function() {
545 for (var i = 0, j = arguments.length; i < j; i++)
546 this.removeOverlay(arguments[i]);
548 getLabel : function() {
549 var lo = this.getOverlay(_internalLabelOverlayId);
550 return lo != null ? lo.getLabel() : null;
552 getLabelOverlay : function() {
553 return this.getOverlay(_internalLabelOverlayId);
555 setLabel : function(l) {
556 var lo = this.getOverlay(_internalLabelOverlayId);
558 var params = l.constructor == String || l.constructor == Function ? { label:l } : l;
559 lo = _makeLabelOverlay(this, params);
560 this._jsPlumb.overlays.push(lo);
563 if (l.constructor == String || l.constructor == Function) lo.setLabel(l);
565 if (l.label) lo.setLabel(l.label);
566 if (l.location) lo.setLocation(l.location);
570 if (!this._jsPlumb.instance.isSuspendDrawing())
574 for (var i = 0; i < this._jsPlumb.overlays.length; i++) {
575 this._jsPlumb.overlays[i].cleanup();
576 this._jsPlumb.overlays[i].destroy();
578 this._jsPlumb.overlays.splice(0);
579 this._jsPlumb.overlayPositions = null;
581 setVisible:function(v) {
582 this[v ? "showOverlays" : "hideOverlays"]();
584 setAbsoluteOverlayPosition:function(overlay, xy) {
585 this._jsPlumb.overlayPositions = this._jsPlumb.overlayPositions || {};
586 this._jsPlumb.overlayPositions[overlay.id] = xy;
588 getAbsoluteOverlayPosition:function(overlay) {
589 return this._jsPlumb.overlayPositions ? this._jsPlumb.overlayPositions[overlay.id] : null;
593 // ------------------------------ END OverlayCapablejsPlumbUIComponent --------------------------------------------
595 var _jsPlumbInstanceIndex = 0,
596 getInstanceIndex = function() {
597 var i = _jsPlumbInstanceIndex + 1;
598 _jsPlumbInstanceIndex++;
602 var jsPlumbInstance = window.jsPlumbInstance = function(_defaults) {
605 Anchor : "BottomCenter",
606 Anchors : [ null, null ],
607 ConnectionsDetachable : true,
608 ConnectionOverlays : [ ],
609 Connector : "Bezier",
611 DoNotThrowErrors:false,
615 EndpointOverlays : [ ],
616 Endpoints : [ null, null ],
617 EndpointStyle : { fillStyle : "#456" },
618 EndpointStyles : [ null, null ],
619 EndpointHoverStyle : null,
620 EndpointHoverStyles : [ null, null ],
621 HoverPaintStyle : null,
622 LabelStyle : { color : "black" },
626 PaintStyle : { lineWidth : 8, strokeStyle : "#456" },
627 ReattachConnections:false,
629 Scope : "jsPlumb_DefaultScope"
631 if (_defaults) jsPlumb.extend(this.Defaults, _defaults);
633 this.logEnabled = this.Defaults.LogEnabled;
634 this._connectionTypes = {};
635 this._endpointTypes = {};
637 jsPlumbUtil.EventGenerator.apply(this);
639 var _currentInstance = this,
640 _instanceIndex = getInstanceIndex(),
641 _bb = _currentInstance.bind,
642 _initialDefaults = {},
644 _info = function(el) {
646 return { el:_el, id:(jsPlumbUtil.isString(el) && _el == null) ? el : _getId(_el) };
649 this.getInstanceIndex = function() { return _instanceIndex; };
651 this.setZoom = function(z, repaintEverything) {
653 if (repaintEverything) _currentInstance.repaintEverything();
655 this.getZoom = function() { return _zoom; };
657 for (var i in this.Defaults)
658 _initialDefaults[i] = this.Defaults[i];
660 this.bind = function(event, fn) {
661 if ("ready" === event && initialized) fn();
662 else _bb.apply(_currentInstance,[event, fn]);
665 _currentInstance.importDefaults = function(d) {
667 _currentInstance.Defaults[i] = d[i];
669 return _currentInstance;
672 _currentInstance.restoreDefaults = function() {
673 _currentInstance.Defaults = jsPlumb.extend({}, _initialDefaults);
674 return _currentInstance;
680 // TODO remove from window scope
682 // map of element id -> endpoint lists. an element can have an arbitrary
683 // number of endpoints on it, and not all of them have to be connected
685 endpointsByElement = {},
686 endpointsByUUID = {},
688 offsetTimestamps = {},
689 floatingConnections = {},
690 draggableStates = {},
691 connectionBeingDragged = false,
693 _suspendDrawing = false,
695 DEFAULT_SCOPE = this.Defaults.Scope,
696 renderMode = null, // will be set in init()
698 _idstamp = function() { return "" + _curIdStamp++; },
701 // appends an element to some other element, which is calculated as follows:
703 // 1. if _currentInstance.Defaults.Container exists, use that element.
704 // 2. if the 'parent' parameter exists, use that.
705 // 3. otherwise just use the root element (for DOM usage, the document body).
708 _appendElement = function(el, parent) {
709 if (_currentInstance.Defaults.Container)
710 jsPlumb.CurrentLibrary.appendElement(el, _currentInstance.Defaults.Container);
712 jsPlumbAdapter.appendToRoot(el);
714 jsPlumb.CurrentLibrary.appendElement(el, parent);
718 // YUI, for some reason, put the result of a Y.all call into an object that contains
719 // a '_nodes' array, instead of handing back an array-like object like the other
722 _convertYUICollection = function(c) {
723 return c._nodes ? c._nodes : c;
727 // Draws an endpoint and its connections. this is the main entry point into drawing connections as well
728 // as endpoints, since jsPlumb is endpoint-centric under the hood.
730 // @param element element to draw (of type library specific element object)
731 // @param ui UI object from current library's event system. optional.
732 // @param timestamp timestamp for this paint cycle. used to speed things up a little by cutting down the amount of offset calculations we do.
733 // @param clearEdits defaults to false; indicates that mouse edits for connectors should be cleared
735 _draw = function(element, ui, timestamp, clearEdits) {
737 // TODO is it correct to filter by headless at this top level? how would a headless adapter ever repaint?
738 if (!jsPlumbAdapter.headless && !_suspendDrawing) {
739 var id = _getId(element),
740 repaintEls = _currentInstance.dragManager.getElementsForDraggable(id);
742 if (timestamp == null) timestamp = _timestamp();
744 // update the offset of everything _before_ we try to draw anything.
745 var o = _updateOffset( { elId : id, offset : ui, recalc : false, timestamp : timestamp });
748 for (var i in repaintEls) {
749 // TODO this seems to cause a lag, but we provide the offset, so in theory it
750 // should not. is the timestamp failing?
752 elId : repaintEls[i].id,
754 left:o.o.left + repaintEls[i].offset.left,
755 top:o.o.top + repaintEls[i].offset.top
758 timestamp : timestamp
764 _currentInstance.anchorManager.redraw(id, ui, timestamp, null, clearEdits);
767 for (var j in repaintEls) {
768 _currentInstance.anchorManager.redraw(repaintEls[j].id, ui, timestamp, repaintEls[j].offset, clearEdits, true);
775 // executes the given function against the given element if the first
776 // argument is an object, or the list of elements, if the first argument
777 // is a list. the function passed in takes (element, elementId) as
780 _elementProxy = function(element, fn) {
781 var retVal = null, el, id;
782 if (_ju.isArray(element)) {
784 for ( var i = 0, j = element.length; i < j; i++) {
785 el = _gel(element[i]);
786 id = _currentInstance.getAttribute(el, "id");
787 retVal.push(fn(el, id)); // append return values to what we will return
791 id = _currentInstance.getAttribute(el, "id");
798 // gets an Endpoint by uuid.
800 _getEndpoint = function(uuid) { return endpointsByUUID[uuid]; },
803 * inits a draggable if it's not already initialised.
804 * TODO: somehow abstract this to the adapter, because the concept of "draggable" has no
805 * place on the server.
807 _initDraggableIfNecessary = function(element, isDraggable, dragOptions) {
808 // TODO move to DragManager?
809 if (!jsPlumbAdapter.headless) {
810 var _draggable = isDraggable == null ? false : isDraggable, jpcl = jsPlumb.CurrentLibrary;
812 if (jpcl.isDragSupported(element) && !jpcl.isAlreadyDraggable(element)) {
813 var options = dragOptions || _currentInstance.Defaults.DragOptions || jsPlumb.Defaults.DragOptions;
814 options = jsPlumb.extend( {}, options); // make a copy.
815 var dragEvent = jpcl.dragEvents.drag,
816 stopEvent = jpcl.dragEvents.stop,
817 startEvent = jpcl.dragEvents.start;
819 options[startEvent] = _ju.wrap(options[startEvent], function() {
820 _currentInstance.setHoverSuspended(true);
821 _currentInstance.select({source:element}).addClass(_currentInstance.elementDraggingClass + " " + _currentInstance.sourceElementDraggingClass, true);
822 _currentInstance.select({target:element}).addClass(_currentInstance.elementDraggingClass + " " + _currentInstance.targetElementDraggingClass, true);
823 _currentInstance.setConnectionBeingDragged(true);
826 options[dragEvent] = _ju.wrap(options[dragEvent], function() {
827 var ui = jpcl.getUIPosition(arguments, _currentInstance.getZoom());
828 _draw(element, ui, null, true);
829 _addClass(element, "jsPlumb_dragged");
831 options[stopEvent] = _ju.wrap(options[stopEvent], function() {
832 var ui = jpcl.getUIPosition(arguments, _currentInstance.getZoom());
834 _removeClass(element, "jsPlumb_dragged");
835 _currentInstance.setHoverSuspended(false);
836 _currentInstance.select({source:element}).removeClass(_currentInstance.elementDraggingClass + " " + _currentInstance.sourceElementDraggingClass, true);
837 _currentInstance.select({target:element}).removeClass(_currentInstance.elementDraggingClass + " " + _currentInstance.targetElementDraggingClass, true);
838 _currentInstance.setConnectionBeingDragged(false);
839 _currentInstance.dragManager.dragEnded(element);
841 var elId = _getId(element); // need ID
842 draggableStates[elId] = true;
843 var draggable = draggableStates[elId];
844 options.disabled = draggable == null ? false : !draggable;
845 jpcl.initDraggable(element, options, false, _currentInstance);
846 _currentInstance.dragManager.register(element);
853 * prepares a final params object that can be passed to _newConnection, taking into account defaults, events, etc.
855 _prepareConnectionParams = function(params, referenceParams) {
856 var _p = jsPlumb.extend( { }, params);
857 if (referenceParams) jsPlumb.extend(_p, referenceParams);
859 // hotwire endpoints passed as source or target to sourceEndpoint/targetEndpoint, respectively.
861 if (_p.source.endpoint)
862 _p.sourceEndpoint = _p.source;
864 _p.source = _dom(_p.source);
867 if (_p.target.endpoint)
868 _p.targetEndpoint = _p.target;
870 _p.target = _dom(_p.target);
873 // test for endpoint uuids to connect
875 _p.sourceEndpoint = _getEndpoint(params.uuids[0]);
876 _p.targetEndpoint = _getEndpoint(params.uuids[1]);
879 // now ensure that if we do have Endpoints already, they're not full.
881 if (_p.sourceEndpoint && _p.sourceEndpoint.isFull()) {
882 _ju.log(_currentInstance, "could not add connection; source endpoint is full");
887 if (_p.targetEndpoint && _p.targetEndpoint.isFull()) {
888 _ju.log(_currentInstance, "could not add connection; target endpoint is full");
892 // if source endpoint mandates connection type and nothing specified in our params, use it.
893 if (!_p.type && _p.sourceEndpoint)
894 _p.type = _p.sourceEndpoint.connectionType;
896 // copy in any connectorOverlays that were specified on the source endpoint.
897 // it doesnt copy target endpoint overlays. i'm not sure if we want it to or not.
898 if (_p.sourceEndpoint && _p.sourceEndpoint.connectorOverlays) {
899 _p.overlays = _p.overlays || [];
900 for (var i = 0, j = _p.sourceEndpoint.connectorOverlays.length; i < j; i++) {
901 _p.overlays.push(_p.sourceEndpoint.connectorOverlays[i]);
906 if (!_p["pointer-events"] && _p.sourceEndpoint && _p.sourceEndpoint.connectorPointerEvents)
907 _p["pointer-events"] = _p.sourceEndpoint.connectorPointerEvents;
909 // if there's a target specified (which of course there should be), and there is no
910 // target endpoint specified, and 'newConnection' was not set to true, then we check to
911 // see if a prior call to makeTarget has provided us with the specs for the target endpoint, and
912 // we use those if so. additionally, if the makeTarget call was specified with 'uniqueEndpoint' set
913 // to true, then if that target endpoint has already been created, we re-use it.
915 var tid, tep, existingUniqueEndpoint, newEndpoint;
917 // TODO: this code can be refactored to be a little dry.
918 if (_p.target && !_p.target.endpoint && !_p.targetEndpoint && !_p.newConnection) {
919 tid = _getId(_p.target);
920 tep =_targetEndpointDefinitions[tid];
921 existingUniqueEndpoint = _targetEndpoints[tid];
924 // if target not enabled, return.
925 if (!_targetsEnabled[tid]) return;
927 // TODO this is dubious. i think it is there so that the endpoint can subsequently
928 // be dragged (ie it kicks off the draggable registration). but it is dubious.
931 // check for max connections??
932 newEndpoint = existingUniqueEndpoint != null ? existingUniqueEndpoint : _currentInstance.addEndpoint(_p.target, tep);
933 if (_targetEndpointsUnique[tid]) _targetEndpoints[tid] = newEndpoint;
934 _p.targetEndpoint = newEndpoint;
935 // TODO test options to makeTarget to see if we should do this?
936 newEndpoint._doNotDeleteOnDetach = false; // reset.
937 newEndpoint._deleteOnDetach = true;
941 // same thing, but for source.
942 if (_p.source && !_p.source.endpoint && !_p.sourceEndpoint && !_p.newConnection) {
943 tid = _getId(_p.source);
944 tep = _sourceEndpointDefinitions[tid];
945 existingUniqueEndpoint = _sourceEndpoints[tid];
948 // if source not enabled, return.
949 if (!_sourcesEnabled[tid]) return;
951 // TODO this is dubious. i think it is there so that the endpoint can subsequently
952 // be dragged (ie it kicks off the draggable registration). but it is dubious.
953 //tep.isSource = true;
955 newEndpoint = existingUniqueEndpoint != null ? existingUniqueEndpoint : _currentInstance.addEndpoint(_p.source, tep);
956 if (_sourceEndpointsUnique[tid]) _sourceEndpoints[tid] = newEndpoint;
957 _p.sourceEndpoint = newEndpoint;
958 // TODO test options to makeSource to see if we should do this?
959 newEndpoint._doNotDeleteOnDetach = false; // reset.
960 newEndpoint._deleteOnDetach = true;
967 _newConnection = function(params) {
968 var connectionFunc = _currentInstance.Defaults.ConnectionType || _currentInstance.getDefaultConnectionType(),
969 endpointFunc = _currentInstance.Defaults.EndpointType || jsPlumb.Endpoint,
970 parent = jsPlumb.CurrentLibrary.getParent;
972 if (params.container)
973 params.parent = params.container;
975 if (params.sourceEndpoint)
976 params.parent = params.sourceEndpoint.parent;
977 else if (params.source.constructor == endpointFunc)
978 params.parent = params.source.parent;
979 else params.parent = parent(params.source);
982 params._jsPlumb = _currentInstance;
983 params.newConnection = _newConnection;
984 params.newEndpoint = _newEndpoint;
985 params.endpointsByUUID = endpointsByUUID;
986 params.endpointsByElement = endpointsByElement;
987 params.finaliseConnection = _finaliseConnection;
988 var con = new connectionFunc(params);
989 con.id = "con_" + _idstamp();
990 _eventFireProxy("click", "click", con);
991 _eventFireProxy("dblclick", "dblclick", con);
992 _eventFireProxy("contextmenu", "contextmenu", con);
994 // if the connection is draggable, then maybe we need to tell the target endpoint to init the
995 // dragging code. it won't run again if it already configured to be draggable.
996 if (con.isDetachable()) {
997 con.endpoints[0].initDraggable();
998 con.endpoints[1].initDraggable();
1005 // adds the connection to the backing model, fires an event if necessary and then redraws
1007 _finaliseConnection = function(jpc, params, originalEvent, doInformAnchorManager) {
1008 params = params || {};
1009 // add to list of connections (by scope).
1010 if (!jpc.suspendedEndpoint)
1011 connections.push(jpc);
1013 // always inform the anchor manager
1014 // except that if jpc has a suspended endpoint it's not true to say the
1015 // connection is new; it has just (possibly) moved. the question is whether
1016 // to make that call here or in the anchor manager. i think perhaps here.
1017 if (jpc.suspendedEndpoint == null || doInformAnchorManager)
1018 _currentInstance.anchorManager.newConnection(jpc);
1024 if (!params.doNotFireConnectionEvent && params.fireEvent !== false) {
1028 source : jpc.source, target : jpc.target,
1029 sourceId : jpc.sourceId, targetId : jpc.targetId,
1030 sourceEndpoint : jpc.endpoints[0], targetEndpoint : jpc.endpoints[1]
1033 _currentInstance.fire("connection", eventArgs, originalEvent);
1037 _eventFireProxy = function(event, proxyEvent, obj) {
1038 obj.bind(event, function(originalObject, originalEvent) {
1039 _currentInstance.fire(proxyEvent, obj, originalEvent);
1044 * for the given endpoint params, returns an appropriate parent element for the UI elements that will be added.
1045 * this function is used by _newEndpoint (directly below), and also in the makeSource function in jsPlumb.
1047 * the logic is to first look for a "container" member of params, and pass that back if found. otherwise we
1048 * handoff to the 'getParent' function in the current library.
1050 _getParentFromParams = function(params) {
1051 if (params.container)
1052 return params.container;
1054 var tag = jsPlumb.CurrentLibrary.getTagName(params.source),
1055 p = jsPlumb.CurrentLibrary.getParent(params.source);
1056 if (tag && tag.toLowerCase() === "td")
1057 return jsPlumb.CurrentLibrary.getParent(p);
1063 factory method to prepare a new endpoint. this should always be used instead of creating Endpoints
1064 manually, since this method attaches event listeners and an id.
1066 _newEndpoint = function(params) {
1067 var endpointFunc = _currentInstance.Defaults.EndpointType || jsPlumb.Endpoint;
1068 var _p = jsPlumb.extend({}, params);
1069 _p.parent = _getParentFromParams(_p);
1070 _p._jsPlumb = _currentInstance;
1071 _p.newConnection = _newConnection;
1072 _p.newEndpoint = _newEndpoint;
1073 _p.endpointsByUUID = endpointsByUUID;
1074 _p.endpointsByElement = endpointsByElement;
1075 _p.finaliseConnection = _finaliseConnection;
1076 _p.fireDetachEvent = fireDetachEvent;
1077 _p.fireMoveEvent = fireMoveEvent;
1078 _p.floatingConnections = floatingConnections;
1079 _p.getParentFromParams = _getParentFromParams;
1080 _p.elementId = _getId(_p.source);
1081 var ep = new endpointFunc(_p);
1082 ep.id = "ep_" + _idstamp();
1083 _eventFireProxy("click", "endpointClick", ep);
1084 _eventFireProxy("dblclick", "endpointDblClick", ep);
1085 _eventFireProxy("contextmenu", "contextmenu", ep);
1086 if (!jsPlumbAdapter.headless)
1087 _currentInstance.dragManager.endpointAdded(_p.source);
1092 * performs the given function operation on all the connections found
1093 * for the given element id; this means we find all the endpoints for
1094 * the given element, and then for each endpoint find the connectors
1095 * connected to it. then we pass each connection in to the given
1098 _operation = function(elId, func, endpointFunc) {
1099 var endpoints = endpointsByElement[elId];
1100 if (endpoints && endpoints.length) {
1101 for ( var i = 0, ii = endpoints.length; i < ii; i++) {
1102 for ( var j = 0, jj = endpoints[i].connections.length; j < jj; j++) {
1103 var retVal = func(endpoints[i].connections[j]);
1104 // if the function passed in returns true, we exit.
1105 // most functions return false.
1108 if (endpointFunc) endpointFunc(endpoints[i]);
1113 _setDraggable = function(element, draggable) {
1114 return _elementProxy(element, function(el, id) {
1115 draggableStates[id] = draggable;
1116 if (jsPlumb.CurrentLibrary.isDragSupported(el)) {
1117 jsPlumb.CurrentLibrary.setDraggable(el, draggable);
1122 * private method to do the business of hiding/showing.
1125 * either Id of the element in question or a library specific
1126 * object for the element.
1128 * String specifying a value for the css 'display' property
1129 * ('block' or 'none').
1131 _setVisible = function(el, state, alsoChangeEndpoints) {
1132 state = state === "block";
1133 var endpointFunc = null;
1134 if (alsoChangeEndpoints) {
1135 if (state) endpointFunc = function(ep) {
1136 ep.setVisible(true, true, true);
1138 else endpointFunc = function(ep) {
1139 ep.setVisible(false, true, true);
1142 var info = _info(el);
1143 _operation(info.id, function(jpc) {
1144 if (state && alsoChangeEndpoints) {
1145 // this test is necessary because this functionality is new, and i wanted to maintain backwards compatibility.
1146 // this block will only set a connection to be visible if the other endpoint in the connection is also visible.
1147 var oidx = jpc.sourceId === info.id ? 1 : 0;
1148 if (jpc.endpoints[oidx].isVisible()) jpc.setVisible(true);
1150 else // the default behaviour for show, and what always happens for hide, is to just set the visibility without getting clever.
1151 jpc.setVisible(state);
1155 * toggles the draggable state of the given element(s).
1156 * el is either an id, or an element object, or a list of ids/element objects.
1158 _toggleDraggable = function(el) {
1159 return _elementProxy(el, function(el, elId) {
1160 var state = draggableStates[elId] == null ? false : draggableStates[elId];
1162 draggableStates[elId] = state;
1163 jsPlumb.CurrentLibrary.setDraggable(el, state);
1168 * private method to do the business of toggling hiding/showing.
1170 _toggleVisible = function(elId, changeEndpoints) {
1171 var endpointFunc = null;
1172 if (changeEndpoints) {
1173 endpointFunc = function(ep) {
1174 var state = ep.isVisible();
1175 ep.setVisible(!state);
1178 _operation(elId, function(jpc) {
1179 var state = jpc.isVisible();
1180 jpc.setVisible(!state);
1182 // todo this should call _elementProxy, and pass in the
1183 // _operation(elId, f) call as a function. cos _toggleDraggable does
1187 * updates the offset and size for a given element, and stores the
1188 * values. if 'offset' is not null we use that (it would have been
1189 * passed in from a drag call) because it's faster; but if it is null,
1190 * or if 'recalc' is true in order to force a recalculation, we get the current values.
1192 _updateOffset = function(params) {
1193 var timestamp = params.timestamp, recalc = params.recalc, offset = params.offset, elId = params.elId, s;
1194 if (_suspendDrawing && !timestamp) timestamp = _suspendedAt;
1196 if (timestamp && timestamp === offsetTimestamps[elId]) {
1197 return {o:params.offset || offsets[elId], s:sizes[elId]};
1200 if (recalc || !offset) { // if forced repaint or no offset available, we recalculate.
1201 // get the current size and offset, and store them
1204 sizes[elId] = _getSize(s);
1205 offsets[elId] = _getOffset(s, _currentInstance);
1206 offsetTimestamps[elId] = timestamp;
1209 offsets[elId] = offset;
1210 if (sizes[elId] == null) {
1212 if (s != null) sizes[elId] = _getSize(s);
1214 offsetTimestamps[elId] = timestamp;
1217 if(offsets[elId] && !offsets[elId].right) {
1218 offsets[elId].right = offsets[elId].left + sizes[elId][0];
1219 offsets[elId].bottom = offsets[elId].top + sizes[elId][1];
1220 offsets[elId].width = sizes[elId][0];
1221 offsets[elId].height = sizes[elId][1];
1222 offsets[elId].centerx = offsets[elId].left + (offsets[elId].width / 2);
1223 offsets[elId].centery = offsets[elId].top + (offsets[elId].height / 2);
1225 return {o:offsets[elId], s:sizes[elId]};
1228 // TODO comparison performance
1229 _getCachedData = function(elId) {
1230 var o = offsets[elId];
1232 return _updateOffset({elId:elId});
1234 return {o:o, s:sizes[elId]};
1238 * gets an id for the given element, creating and setting one if
1239 * necessary. the id is of the form
1241 * jsPlumb_<instance index>_<index in instance>
1243 * where "index in instance" is a monotonically increasing integer that starts at 0,
1244 * for each instance. this method is used not only to assign ids to elements that do not
1245 * have them but also to connections and endpoints.
1247 _getId = function(element, uuid, doNotCreateIfNotFound) {
1248 if (jsPlumbUtil.isString(element)) return element;
1249 if (element == null) return null;
1250 var id = jsPlumbAdapter.getAttribute(element, "id");
1251 if (!id || id === "undefined") {
1252 // check if fixed uuid parameter is given
1253 if (arguments.length == 2 && arguments[1] !== undefined)
1255 else if (arguments.length == 1 || (arguments.length == 3 && !arguments[2]))
1256 id = "jsPlumb_" + _instanceIndex + "_" + _idstamp();
1258 if (!doNotCreateIfNotFound) jsPlumbAdapter.setAttribute(element, "id", id);
1263 this.setConnectionBeingDragged = function(v) {
1264 connectionBeingDragged = v;
1266 this.isConnectionBeingDragged = function() {
1267 return connectionBeingDragged;
1270 this.connectorClass = "_jsPlumb_connector";
1271 this.hoverClass = "_jsPlumb_hover";
1272 this.endpointClass = "_jsPlumb_endpoint";
1273 this.endpointConnectedClass = "_jsPlumb_endpoint_connected";
1274 this.endpointFullClass = "_jsPlumb_endpoint_full";
1275 this.endpointDropAllowedClass = "_jsPlumb_endpoint_drop_allowed";
1276 this.endpointDropForbiddenClass = "_jsPlumb_endpoint_drop_forbidden";
1277 this.overlayClass = "_jsPlumb_overlay";
1278 this.draggingClass = "_jsPlumb_dragging";
1279 this.elementDraggingClass = "_jsPlumb_element_dragging";
1280 this.sourceElementDraggingClass = "_jsPlumb_source_element_dragging";
1281 this.targetElementDraggingClass = "_jsPlumb_target_element_dragging";
1282 this.endpointAnchorClassPrefix = "_jsPlumb_endpoint_anchor";
1283 this.hoverSourceClass = "_jsPlumb_source_hover";
1284 this.hoverTargetClass = "_jsPlumb_target_hover";
1285 this.dragSelectClass = "_jsPlumb_drag_select";
1288 this.Connectors = { "canvas":{}, "svg":{}, "vml":{} };
1289 this.Endpoints = { "canvas":{}, "svg":{}, "vml":{} };
1290 this.Overlays = { "canvas":{}, "svg":{}, "vml":{}};
1291 this.ConnectorRenderers = {};
1293 this.CANVAS = "canvas";
1297 // --------------------------- jsPLumbInstance public API ---------------------------------------------------------
1300 this.addEndpoint = function(el, params, referenceParams) {
1301 referenceParams = referenceParams || {};
1302 var p = jsPlumb.extend({}, referenceParams);
1303 jsPlumb.extend(p, params);
1304 p.endpoint = p.endpoint || _currentInstance.Defaults.Endpoint || jsPlumb.Defaults.Endpoint;
1305 p.paintStyle = p.paintStyle || _currentInstance.Defaults.EndpointStyle || jsPlumb.Defaults.EndpointStyle;
1307 el = _convertYUICollection(el);
1310 inputs = (_ju.isArray(el) || (el.length != null && !_ju.isString(el))) ? el : [ el ];
1312 for (var i = 0, j = inputs.length; i < j; i++) {
1313 var _el = _dom(inputs[i]), id = _getId(_el);
1316 _updateOffset({ elId : id, timestamp:_suspendedAt });
1317 var e = _newEndpoint(p);
1318 if (p.parentAnchor) e.parentAnchor = p.parentAnchor;
1319 _ju.addToList(endpointsByElement, id, e);
1320 var myOffset = offsets[id],
1322 anchorLoc = e.anchor.compute( { xy : [ myOffset.left, myOffset.top ], wh : myWH, element : e, timestamp:_suspendedAt }),
1323 endpointPaintParams = { anchorLoc : anchorLoc, timestamp:_suspendedAt };
1325 if (_suspendDrawing) endpointPaintParams.recalc = false;
1326 if (!_suspendDrawing) e.paint(endpointPaintParams);
1329 e._doNotDeleteOnDetach = true; // mark this as being added via addEndpoint.
1332 return results.length == 1 ? results[0] : results;
1336 this.addEndpoints = function(el, endpoints, referenceParams) {
1338 for ( var i = 0, j = endpoints.length; i < j; i++) {
1339 var e = _currentInstance.addEndpoint(el, endpoints[i], referenceParams);
1341 Array.prototype.push.apply(results, e);
1342 else results.push(e);
1347 this.animate = function(el, properties, options) {
1348 options = options || {};
1351 stepFunction = jsPlumb.CurrentLibrary.dragEvents.step,
1352 completeFunction = jsPlumb.CurrentLibrary.dragEvents.complete;
1354 options[stepFunction] = _ju.wrap(options[stepFunction], function() {
1355 _currentInstance.repaint(id);
1358 // onComplete repaints, just to make sure everything looks good at the end of the animation.
1359 options[completeFunction] = _ju.wrap(options[completeFunction], function() {
1360 _currentInstance.repaint(id);
1363 jsPlumb.CurrentLibrary.animate(ele, properties, options);
1367 * checks for a listener for the given condition, executing it if found, passing in the given value.
1368 * condition listeners would have been attached using "bind" (which is, you could argue, now overloaded, since
1369 * firing click events etc is a bit different to what this does). i thought about adding a "bindCondition"
1370 * or something, but decided against it, for the sake of simplicity. jsPlumb will never fire one of these
1371 * condition events anyway.
1373 this.checkCondition = function(conditionName, value) {
1374 var l = _currentInstance.getListener(conditionName),
1377 if (l && l.length > 0) {
1379 for (var i = 0, j = l.length; i < j; i++) {
1380 r = r && l[i](value);
1384 _ju.log(_currentInstance, "cannot check condition [" + conditionName + "]" + e);
1391 * checks a condition asynchronously: fires the event handler and passes the handler
1392 * a 'proceed' function and a 'stop' function. The handler MUST execute one or other
1393 * of these once it has made up its mind.
1395 * Note that although this reads the listener list for the given condition, it
1396 * does not loop through and hit each listener, because that, with asynchronous
1397 * callbacks, would be messy. so it uses only the first listener registered.
1399 this.checkASyncCondition = function(conditionName, value, proceed, stop) {
1400 var l = _currentInstance.getListener(conditionName);
1402 if (l && l.length > 0) {
1404 l[0](value, proceed, stop);
1407 _ju.log(_currentInstance, "cannot asynchronously check condition [" + conditionName + "]" + e);
1413 this.connect = function(params, referenceParams) {
1414 // prepare a final set of parameters to create connection with
1415 var _p = _prepareConnectionParams(params, referenceParams), jpc;
1416 // TODO probably a nicer return value if the connection was not made. _prepareConnectionParams
1417 // will return null (and log something) if either endpoint was full. what would be nicer is to
1418 // create a dedicated 'error' object.
1420 // create the connection. it is not yet registered
1421 jpc = _newConnection(_p);
1422 // now add it the model, fire an event, and redraw
1423 _finaliseConnection(jpc, _p);
1428 this.deleteEndpoint = function(object, doNotRepaintAfterwards) {
1429 var _is = _currentInstance.setSuspendDrawing(true);
1430 var endpoint = (typeof object == "string") ? endpointsByUUID[object] : object;
1432 _currentInstance.deleteObject({
1436 if(!_is) _currentInstance.setSuspendDrawing(false, doNotRepaintAfterwards);
1437 return _currentInstance;
1440 this.deleteEveryEndpoint = function() {
1441 var _is = _currentInstance.setSuspendDrawing(true);
1442 for ( var id in endpointsByElement) {
1443 var endpoints = endpointsByElement[id];
1444 if (endpoints && endpoints.length) {
1445 for ( var i = 0, j = endpoints.length; i < j; i++) {
1446 _currentInstance.deleteEndpoint(endpoints[i], true);
1450 endpointsByElement = {};
1451 endpointsByUUID = {};
1452 _currentInstance.anchorManager.reset();
1453 _currentInstance.dragManager.reset();
1454 if(!_is) _currentInstance.setSuspendDrawing(false);
1455 return _currentInstance;
1458 var fireDetachEvent = function(jpc, doFireEvent, originalEvent) {
1459 // may have been given a connection, or in special cases, an object
1460 var connType = _currentInstance.Defaults.ConnectionType || _currentInstance.getDefaultConnectionType(),
1461 argIsConnection = jpc.constructor == connType,
1462 params = argIsConnection ? {
1464 source : jpc.source, target : jpc.target,
1465 sourceId : jpc.sourceId, targetId : jpc.targetId,
1466 sourceEndpoint : jpc.endpoints[0], targetEndpoint : jpc.endpoints[1]
1470 _currentInstance.fire("connectionDetached", params, originalEvent);
1472 _currentInstance.anchorManager.connectionDetached(params);
1475 var fireMoveEvent = function(params, evt) {
1476 _currentInstance.fire("connectionMoved", params, evt);
1479 this.unregisterEndpoint = function(endpoint) {
1480 if (endpoint._jsPlumb.uuid) endpointsByUUID[endpoint._jsPlumb.uuid] = null;
1481 _currentInstance.anchorManager.deleteEndpoint(endpoint);
1482 // TODO at least replace this with a removeWithFunction call.
1483 for (var e in endpointsByElement) {
1484 var endpoints = endpointsByElement[e];
1486 var newEndpoints = [];
1487 for (var i = 0, j = endpoints.length; i < j; i++)
1488 if (endpoints[i] != endpoint) newEndpoints.push(endpoints[i]);
1490 endpointsByElement[e] = newEndpoints;
1492 if(endpointsByElement[e].length <1){
1493 delete endpointsByElement[e];
1498 this.detach = function() {
1500 if (arguments.length === 0) return;
1501 var connType = _currentInstance.Defaults.ConnectionType || _currentInstance.getDefaultConnectionType(),
1502 firstArgIsConnection = arguments[0].constructor == connType,
1503 params = arguments.length == 2 ? firstArgIsConnection ? (arguments[1] || {}) : arguments[0] : arguments[0],
1504 fireEvent = (params.fireEvent !== false),
1505 forceDetach = params.forceDetach,
1506 conn = firstArgIsConnection ? arguments[0] : params.connection;
1509 if (forceDetach || jsPlumbUtil.functionChain(true, false, [
1510 [ conn.endpoints[0], "isDetachAllowed", [ conn ] ],
1511 [ conn.endpoints[1], "isDetachAllowed", [ conn ] ],
1512 [ conn, "isDetachAllowed", [ conn ] ],
1513 [ _currentInstance, "checkCondition", [ "beforeDetach", conn ] ] ])) {
1515 conn.endpoints[0].detach(conn, false, true, fireEvent);
1519 var _p = jsPlumb.extend( {}, params); // a backwards compatibility hack: source should be thought of as 'params' in this case.
1520 // test for endpoint uuids to detach
1522 _getEndpoint(_p.uuids[0]).detachFrom(_getEndpoint(_p.uuids[1]), fireEvent);
1523 } else if (_p.sourceEndpoint && _p.targetEndpoint) {
1524 _p.sourceEndpoint.detachFrom(_p.targetEndpoint);
1526 var sourceId = _getId(_dom(_p.source)),
1527 targetId = _getId(_dom(_p.target));
1528 _operation(sourceId, function(jpc) {
1529 if ((jpc.sourceId == sourceId && jpc.targetId == targetId) || (jpc.targetId == sourceId && jpc.sourceId == targetId)) {
1530 if (_currentInstance.checkCondition("beforeDetach", jpc)) {
1531 jpc.endpoints[0].detach(jpc, false, true, fireEvent);
1539 this.detachAllConnections = function(el, params) {
1540 params = params || {};
1542 var id = _getId(el),
1543 endpoints = endpointsByElement[id];
1544 if (endpoints && endpoints.length) {
1545 for ( var i = 0, j = endpoints.length; i < j; i++) {
1546 endpoints[i].detachAll(params.fireEvent !== false);
1549 return _currentInstance;
1552 this.detachEveryConnection = function(params) {
1553 params = params || {};
1554 _currentInstance.doWhileSuspended(function() {
1555 for ( var id in endpointsByElement) {
1556 var endpoints = endpointsByElement[id];
1557 if (endpoints && endpoints.length) {
1558 for ( var i = 0, j = endpoints.length; i < j; i++) {
1559 endpoints[i].detachAll(params.fireEvent !== false);
1563 connections.splice(0);
1565 return _currentInstance;
1568 /// not public. but of course its exposed. how to change this.
1569 this.deleteObject = function(params) {
1576 fireEvent = params.fireEvent !== false,
1577 deleteAttachedObjects = params.deleteAttachedObjects !== false;
1579 var unravelConnection = function(connection) {
1580 if(connection != null && result.connections[connection.id] == null) {
1581 if (connection._jsPlumb != null) connection.setHover(false);
1582 result.connections[connection.id] = connection;
1583 result.connectionCount++;
1584 if (deleteAttachedObjects) {
1585 for (var j = 0; j < connection.endpoints.length; j++) {
1586 if (connection.endpoints[j]._deleteOnDetach)
1587 unravelEndpoint(connection.endpoints[j]);
1592 var unravelEndpoint = function(endpoint) {
1593 if(endpoint != null && result.endpoints[endpoint.id] == null) {
1594 if (endpoint._jsPlumb != null) endpoint.setHover(false);
1595 result.endpoints[endpoint.id] = endpoint;
1596 result.endpointCount++;
1598 if (deleteAttachedObjects) {
1599 for (var i = 0; i < endpoint.connections.length; i++) {
1600 var c = endpoint.connections[i];
1601 unravelConnection(c);
1607 if (params.connection)
1608 unravelConnection(params.connection);
1609 else unravelEndpoint(params.endpoint);
1611 // loop through connections
1612 for (var i in result.connections) {
1613 var c = result.connections[i];
1614 c.endpoints[0].detachFromConnection(c);
1615 c.endpoints[1].detachFromConnection(c);
1616 //_currentInstance.unregisterConnection(c);
1617 jsPlumbUtil.removeWithFunction(connections, function(_c) {
1618 return c.id == _c.id;
1620 fireDetachEvent(c, fireEvent, params.originalEvent);
1625 // loop through endpoints
1626 for (var j in result.endpoints) {
1627 var e = result.endpoints[j];
1628 _currentInstance.unregisterEndpoint(e);
1629 // FIRE some endpoint deleted event?
1637 this.draggable = function(el, options) {
1639 // allows for array or jquery/mootools selector
1640 if (typeof el == 'object' && el.length) {
1641 for (i = 0, j = el.length; i < j; i++) {
1643 if (ele) _initDraggableIfNecessary(ele, true, options);
1646 // allows for YUI selector
1647 else if (el._nodes) { // TODO this is YUI specific; really the logic should be forced
1648 // into the library adapters (for jquery and mootools aswell)
1649 for (i = 0, j = el._nodes.length; i < j; i++) {
1650 ele = _dom(el._nodes[i]);
1651 if (ele) _initDraggableIfNecessary(ele, true, options);
1656 if (ele) _initDraggableIfNecessary(ele, true, options);
1658 return _currentInstance;
1662 // just a library-agnostic wrapper.
1663 this.extend = function(o1, o2) {
1664 return jsPlumb.CurrentLibrary.extend(o1, o2);
1667 // helpers for select/selectEndpoints
1668 var _setOperation = function(list, func, args, selector) {
1669 for (var i = 0, j = list.length; i < j; i++) {
1670 list[i][func].apply(list[i], args);
1672 return selector(list);
1674 _getOperation = function(list, func, args) {
1676 for (var i = 0, j = list.length; i < j; i++) {
1677 out.push([ list[i][func].apply(list[i], args), list[i] ]);
1681 setter = function(list, func, selector) {
1683 return _setOperation(list, func, arguments, selector);
1686 getter = function(list, func) {
1688 return _getOperation(list, func, arguments);
1691 prepareList = function(input, doNotGetIds) {
1694 if (typeof input == 'string') {
1695 if (input === "*") return input;
1699 input = _gel(input);
1700 if (doNotGetIds) r = input;
1702 for (var i = 0, j = input.length; i < j; i++)
1703 r.push(_info(input[i]).id);
1709 filterList = function(list, value, missingIsFalse) {
1710 if (list === "*") return true;
1711 return list.length > 0 ? jsPlumbUtil.indexOf(list, value) != -1 : !missingIsFalse;
1714 // get some connections, specifying source/target/scope
1715 this.getConnections = function(options, flat) {
1718 } else if (options.constructor == String) {
1719 options = { "scope": options };
1721 var scope = options.scope || _currentInstance.getDefaultScope(),
1722 scopes = prepareList(scope, true),
1723 sources = prepareList(options.source),
1724 targets = prepareList(options.target),
1725 results = (!flat && scopes.length > 1) ? {} : [],
1726 _addOne = function(scope, obj) {
1727 if (!flat && scopes.length > 1) {
1728 var ss = results[scope];
1730 ss = results[scope] = [];
1733 } else results.push(obj);
1736 for ( var j = 0, jj = connections.length; j < jj; j++) {
1737 var c = connections[j];
1738 if (filterList(scopes, c.scope) && filterList(sources, c.sourceId) && filterList(targets, c.targetId))
1739 _addOne(c.scope, c);
1745 var _curryEach = function(list, executor) {
1746 return function(f) {
1747 for (var i = 0, ii = list.length; i < ii; i++) {
1750 return executor(list);
1753 _curryGet = function(list) {
1754 return function(idx) {
1759 var _makeCommonSelectHandler = function(list, executor) {
1762 each:_curryEach(list, executor),
1765 setters = ["setHover", "removeAllOverlays", "setLabel", "addClass", "addOverlay", "removeOverlay",
1766 "removeOverlays", "showOverlay", "hideOverlay", "showOverlays", "hideOverlays", "setPaintStyle",
1767 "setHoverPaintStyle", "setSuspendEvents", "setParameter", "setParameters", "setVisible",
1768 "repaint", "addType", "toggleType", "removeType", "removeClass", "setType", "bind", "unbind" ],
1770 getters = ["getLabel", "getOverlay", "isHover", "getParameter", "getParameters", "getPaintStyle",
1771 "getHoverPaintStyle", "isVisible", "hasType", "getType", "isSuspendEvents" ],
1774 for (i = 0, ii = setters.length; i < ii; i++)
1775 out[setters[i]] = setter(list, setters[i], executor);
1777 for (i = 0, ii = getters.length; i < ii; i++)
1778 out[getters[i]] = getter(list, getters[i]);
1783 var _makeConnectionSelectHandler = function(list) {
1784 var common = _makeCommonSelectHandler(list, _makeConnectionSelectHandler);
1785 return jsPlumb.CurrentLibrary.extend(common, {
1787 setDetachable:setter(list, "setDetachable", _makeConnectionSelectHandler),
1788 setReattach:setter(list, "setReattach", _makeConnectionSelectHandler),
1789 setConnector:setter(list, "setConnector", _makeConnectionSelectHandler),
1791 for (var i = 0, ii = list.length; i < ii; i++)
1792 _currentInstance.detach(list[i]);
1795 isDetachable:getter(list, "isDetachable"),
1796 isReattach:getter(list, "isReattach")
1800 var _makeEndpointSelectHandler = function(list) {
1801 var common = _makeCommonSelectHandler(list, _makeEndpointSelectHandler);
1802 return jsPlumb.CurrentLibrary.extend(common, {
1803 setEnabled:setter(list, "setEnabled", _makeEndpointSelectHandler),
1804 setAnchor:setter(list, "setAnchor", _makeEndpointSelectHandler),
1805 isEnabled:getter(list, "isEnabled"),
1806 detachAll:function() {
1807 for (var i = 0, ii = list.length; i < ii; i++)
1808 list[i].detachAll();
1810 "remove":function() {
1811 for (var i = 0, ii = list.length; i < ii; i++)
1812 _currentInstance.deleteObject({endpoint:list[i]});
1818 this.select = function(params) {
1819 params = params || {};
1820 params.scope = params.scope || "*";
1821 return _makeConnectionSelectHandler(params.connections || _currentInstance.getConnections(params, true));
1824 this.selectEndpoints = function(params) {
1825 params = params || {};
1826 params.scope = params.scope || "*";
1827 var noElementFilters = !params.element && !params.source && !params.target,
1828 elements = noElementFilters ? "*" : prepareList(params.element),
1829 sources = noElementFilters ? "*" : prepareList(params.source),
1830 targets = noElementFilters ? "*" : prepareList(params.target),
1831 scopes = prepareList(params.scope, true);
1835 for (var el in endpointsByElement) {
1836 var either = filterList(elements, el, true),
1837 source = filterList(sources, el, true),
1838 sourceMatchExact = sources != "*",
1839 target = filterList(targets, el, true),
1840 targetMatchExact = targets != "*";
1842 // if they requested 'either' then just match scope. otherwise if they requested 'source' (not as a wildcard) then we have to match only endpoints that have isSource set to to true, and the same thing with isTarget.
1843 if ( either || source || target ) {
1845 for (var i = 0, ii = endpointsByElement[el].length; i < ii; i++) {
1846 var _ep = endpointsByElement[el][i];
1847 if (filterList(scopes, _ep.scope, true)) {
1849 var noMatchSource = (sourceMatchExact && sources.length > 0 && !_ep.isSource),
1850 noMatchTarget = (targetMatchExact && targets.length > 0 && !_ep.isTarget);
1852 if (noMatchSource || noMatchTarget)
1861 return _makeEndpointSelectHandler(ep);
1864 // get all connections managed by the instance of jsplumb.
1865 this.getAllConnections = function() { return connections; };
1866 this.getDefaultScope = function() { return DEFAULT_SCOPE; };
1867 // get an endpoint by uuid.
1868 this.getEndpoint = _getEndpoint;
1869 // get endpoints for some element.
1870 this.getEndpoints = function(el) { return endpointsByElement[_info(el).id]; };
1871 // gets the default endpoint type. used when subclassing. see wiki.
1872 this.getDefaultEndpointType = function() { return jsPlumb.Endpoint; };
1873 // gets the default connection type. used when subclassing. see wiki.
1874 this.getDefaultConnectionType = function() { return jsPlumb.Connection; };
1876 * Gets an element's id, creating one if necessary. really only exposed
1877 * for the lib-specific functionality to access; would be better to pass
1878 * the current instance into the lib-specific code (even though this is
1879 * a static call. i just don't want to expose it to the public API).
1881 this.getId = _getId;
1882 this.getOffset = function(id) {
1883 var o = offsets[id];
1884 return _updateOffset({elId:id});
1887 this.getSelector = function() {
1888 return jsPlumb.CurrentLibrary.getSelector.apply(null, arguments);
1891 // get the size of the element with the given id, perhaps from cache.
1892 this.getSize = function(id) {
1894 if (!s) _updateOffset({elId:id});
1898 this.appendElement = _appendElement;
1900 var _hoverSuspended = false;
1901 this.isHoverSuspended = function() { return _hoverSuspended; };
1902 this.setHoverSuspended = function(s) { _hoverSuspended = s; };
1904 var _isAvailable = function(m) {
1906 return jsPlumbAdapter.isRenderModeAvailable(m);
1909 this.isCanvasAvailable = _isAvailable("canvas");
1910 this.isSVGAvailable = _isAvailable("svg");
1911 this.isVMLAvailable = _isAvailable("vml");
1913 // set an element's connections to be hidden
1914 this.hide = function(el, changeEndpoints) {
1915 _setVisible(el, "none", changeEndpoints);
1916 return _currentInstance;
1919 // exposed for other objects to use to get a unique id.
1920 this.idstamp = _idstamp;
1922 this.connectorsInitialized = false;
1923 var connectorTypes = [], rendererTypes = ["canvas", "svg", "vml"];
1924 this.registerConnectorType = function(connector, name) {
1925 connectorTypes.push([connector, name]);
1929 * callback from the current library to tell us to prepare ourselves (attach
1930 * mouse listeners etc; can't do that until the library has provided a bind method)
1932 this.init = function() {
1933 var _oneType = function(renderer, name, fn) {
1934 jsPlumb.Connectors[renderer][name] = function() {
1935 fn.apply(this, arguments);
1936 jsPlumb.ConnectorRenderers[renderer].apply(this, arguments);
1938 jsPlumbUtil.extend(jsPlumb.Connectors[renderer][name], [ fn, jsPlumb.ConnectorRenderers[renderer]]);
1941 if (!jsPlumb.connectorsInitialized) {
1942 for (var i = 0; i < connectorTypes.length; i++) {
1943 for (var j = 0; j < rendererTypes.length; j++) {
1944 _oneType(rendererTypes[j], connectorTypes[i][1], connectorTypes[i][0]);
1948 jsPlumb.connectorsInitialized = true;
1952 _currentInstance.anchorManager = new jsPlumb.AnchorManager({jsPlumbInstance:_currentInstance});
1953 _currentInstance.setRenderMode(_currentInstance.Defaults.RenderMode); // calling the method forces the capability logic to be run.
1955 _currentInstance.fire("ready", _currentInstance);
1960 this.jsPlumbUIComponent = jsPlumbUIComponent;
1963 * Creates an anchor with the given params.
1966 * Returns: The newly created Anchor.
1967 * Throws: an error if a named anchor was not found.
1969 this.makeAnchor = function() {
1970 var pp, _a = function(t, p) {
1971 if (jsPlumb.Anchors[t]) return new jsPlumb.Anchors[t](p);
1972 if (!_currentInstance.Defaults.DoNotThrowErrors)
1973 throw { msg:"jsPlumb: unknown anchor type '" + t + "'" };
1975 if (arguments.length === 0) return null;
1976 var specimen = arguments[0], elementId = arguments[1], jsPlumbInstance = arguments[2], newAnchor = null;
1977 // if it appears to be an anchor already...
1978 if (specimen.compute && specimen.getOrientation) return specimen; //TODO hazy here about whether it should be added or is already added somehow.
1979 // is it the name of an anchor type?
1980 else if (typeof specimen == "string") {
1981 newAnchor = _a(arguments[0], {elementId:elementId, jsPlumbInstance:_currentInstance});
1983 // is it an array? it will be one of:
1984 // an array of [spec, params] - this defines a single anchor, which may be dynamic, but has parameters.
1985 // an array of arrays - this defines some dynamic anchors
1986 // an array of numbers - this defines a single anchor.
1987 else if (_ju.isArray(specimen)) {
1988 if (_ju.isArray(specimen[0]) || _ju.isString(specimen[0])) {
1989 // if [spec, params] format
1990 if (specimen.length == 2 && _ju.isObject(specimen[1])) {
1991 // if first arg is a string, its a named anchor with params
1992 if (_ju.isString(specimen[0])) {
1993 pp = jsPlumb.extend({elementId:elementId, jsPlumbInstance:_currentInstance}, specimen[1]);
1994 newAnchor = _a(specimen[0], pp);
1996 // otherwise first arg is array, second is params. we treat as a dynamic anchor, which is fine
1997 // even if the first arg has only one entry. you could argue all anchors should be implicitly dynamic in fact.
1999 pp = jsPlumb.extend({elementId:elementId, jsPlumbInstance:_currentInstance, anchors:specimen[0]}, specimen[1]);
2000 newAnchor = new jsPlumb.DynamicAnchor(pp);
2004 newAnchor = new jsPlumb.DynamicAnchor({anchors:specimen, selector:null, elementId:elementId, jsPlumbInstance:jsPlumbInstance});
2008 var anchorParams = {
2009 x:specimen[0], y:specimen[1],
2010 orientation : (specimen.length >= 4) ? [ specimen[2], specimen[3] ] : [0,0],
2011 offsets : (specimen.length >= 6) ? [ specimen[4], specimen[5] ] : [ 0, 0 ],
2012 elementId:elementId,
2013 jsPlumbInstance:jsPlumbInstance,
2014 cssClass:specimen.length == 7 ? specimen[6] : null
2016 newAnchor = new jsPlumb.Anchor(anchorParams);
2017 newAnchor.clone = function() { return new jsPlumb.Anchor(anchorParams); };
2021 if (!newAnchor.id) newAnchor.id = "anchor_" + _idstamp();
2026 * makes a list of anchors from the given list of types or coords, eg
2027 * ["TopCenter", "RightMiddle", "BottomCenter", [0, 1, -1, -1] ]
2029 this.makeAnchors = function(types, elementId, jsPlumbInstance) {
2031 for ( var i = 0, ii = types.length; i < ii; i++) {
2032 if (typeof types[i] == "string")
2033 r.push(jsPlumb.Anchors[types[i]]({elementId:elementId, jsPlumbInstance:jsPlumbInstance}));
2034 else if (_ju.isArray(types[i]))
2035 r.push(_currentInstance.makeAnchor(types[i], elementId, jsPlumbInstance));
2041 * Makes a dynamic anchor from the given list of anchors (which may be in shorthand notation as strings or dimension arrays, or Anchor
2042 * objects themselves) and the given, optional, anchorSelector function (jsPlumb uses a default if this is not provided; most people will
2043 * not need to provide this - i think).
2045 this.makeDynamicAnchor = function(anchors, anchorSelector) {
2046 return new jsPlumb.DynamicAnchor({anchors:anchors, selector:anchorSelector, elementId:null, jsPlumbInstance:_currentInstance});
2049 // --------------------- makeSource/makeTarget ----------------------------------------------
2051 var _targetEndpointDefinitions = {},
2052 _targetEndpoints = {},
2053 _targetEndpointsUnique = {},
2054 _targetMaxConnections = {},
2055 _setEndpointPaintStylesAndAnchor = function(ep, epIndex) {
2056 ep.paintStyle = ep.paintStyle ||
2057 _currentInstance.Defaults.EndpointStyles[epIndex] ||
2058 _currentInstance.Defaults.EndpointStyle ||
2059 jsPlumb.Defaults.EndpointStyles[epIndex] ||
2060 jsPlumb.Defaults.EndpointStyle;
2061 ep.hoverPaintStyle = ep.hoverPaintStyle ||
2062 _currentInstance.Defaults.EndpointHoverStyles[epIndex] ||
2063 _currentInstance.Defaults.EndpointHoverStyle ||
2064 jsPlumb.Defaults.EndpointHoverStyles[epIndex] ||
2065 jsPlumb.Defaults.EndpointHoverStyle;
2067 ep.anchor = ep.anchor ||
2068 _currentInstance.Defaults.Anchors[epIndex] ||
2069 _currentInstance.Defaults.Anchor ||
2070 jsPlumb.Defaults.Anchors[epIndex] ||
2071 jsPlumb.Defaults.Anchor;
2073 ep.endpoint = ep.endpoint ||
2074 _currentInstance.Defaults.Endpoints[epIndex] ||
2075 _currentInstance.Defaults.Endpoint ||
2076 jsPlumb.Defaults.Endpoints[epIndex] ||
2077 jsPlumb.Defaults.Endpoint;
2079 // TODO put all the source stuff inside one parent, keyed by id.
2080 _sourceEndpointDefinitions = {},
2081 _sourceEndpoints = {},
2082 _sourceEndpointsUnique = {},
2083 _sourcesEnabled = {},
2084 _sourceTriggers = {},
2085 _sourceMaxConnections = {},
2086 _targetsEnabled = {},
2087 selectorFilter = function(evt, _el, selector) {
2088 var t = evt.target || evt.srcElement, ok = false,
2089 sel = _currentInstance.getSelector(_el, selector);
2090 for (var j = 0; j < sel.length; j++) {
2100 this.makeTarget = function(el, params, referenceParams) {
2102 // put jsplumb ref into params without altering the params passed in
2103 var p = jsPlumb.extend({_jsPlumb:_currentInstance}, referenceParams);
2104 jsPlumb.extend(p, params);
2106 // calculate appropriate paint styles and anchor from the params given
2107 _setEndpointPaintStylesAndAnchor(p, 1);
2109 var jpcl = jsPlumb.CurrentLibrary,
2110 targetScope = p.scope || _currentInstance.Defaults.Scope,
2111 deleteEndpointsOnDetach = !(p.deleteEndpointsOnDetach === false),
2112 maxConnections = p.maxConnections || -1,
2113 onMaxConnections = p.onMaxConnections,
2115 _doOne = function(el) {
2117 // get the element's id and store the endpoint definition for it. jsPlumb.connect calls will look for one of these,
2118 // and use the endpoint definition if found.
2119 // decode the info for this element (id and element)
2120 var elInfo = _info(el),
2122 proxyComponent = new jsPlumbUIComponent(p),
2123 dropOptions = jsPlumb.extend({}, p.dropOptions || {});
2125 // store the definitions keyed against the element id.
2126 _targetEndpointDefinitions[elid] = p;
2127 _targetEndpointsUnique[elid] = p.uniqueEndpoint;
2128 _targetMaxConnections[elid] = maxConnections;
2129 _targetsEnabled[elid] = true;
2131 var _drop = function() {
2132 _currentInstance.currentlyDragging = false;
2133 var originalEvent = jsPlumb.CurrentLibrary.getDropEvent(arguments),
2134 targetCount = _currentInstance.select({target:elid}).length,
2135 draggable = _gel(jpcl.getDragObject(arguments)),
2136 id = _currentInstance.getAttribute(draggable, "dragId"),
2137 scope = _currentInstance.getAttribute(draggable, "originalScope"),
2138 jpc = floatingConnections[id],
2139 idx = jpc.endpoints[0].isFloating() ? 0 : 1,
2140 // this is not necessarily correct. if the source is being dragged,
2141 // then the source endpoint is actually the currently suspended endpoint.
2142 source = jpc.endpoints[0],
2143 _endpoint = p.endpoint ? jsPlumb.extend({}, p.endpoint) : {};
2145 if (!_targetsEnabled[elid] || _targetMaxConnections[elid] > 0 && targetCount >= _targetMaxConnections[elid]){
2146 if (onMaxConnections) {
2147 // TODO here we still have the id of the floating element, not the
2157 // unlock the source anchor to allow it to refresh its position if necessary
2158 source.anchor.locked = false;
2160 // restore the original scope if necessary (issue 57)
2161 if (scope) jpcl.setDragScope(draggable, scope);
2163 // if no suspendedEndpoint and not pending, it is likely there was a drop on two
2164 // elements that are on top of each other. abort.
2165 if (jpc.suspendedEndpoint == null && !jpc.pending)
2168 // check if drop is allowed here.
2169 // if the source is being dragged then in fact
2170 // the source and target ids to pass into the drop interceptor are
2172 // target - jpc's targetId
2174 // otherwise the ids are
2175 // source - jpc.sourceId
2178 var _continue = proxyComponent.isDropAllowed(idx === 0 ? elid : jpc.sourceId, idx === 0 ? jpc.targetId : elid, jpc.scope, jpc, null);
2180 // reinstate any suspended endpoint; this just puts the connection back into
2181 // a state in which it will report sensible values if someone asks it about
2182 // its target. we're going to throw this connection away shortly so it doesnt matter
2183 // if we manipulate it a bit.
2184 if (jpc.suspendedEndpoint) {
2185 jpc[idx ? "targetId" : "sourceId"] = jpc.suspendedEndpoint.elementId;
2186 jpc[idx ? "target" : "source"] = jpc.suspendedEndpoint.element;
2187 jpc.endpoints[idx] = jpc.suspendedEndpoint;
2192 // make a new Endpoint for the target, or get it from the cache if uniqueEndpoint
2194 var _el = jpcl.getElementObject(elInfo.el),
2195 newEndpoint = _targetEndpoints[elid];
2197 // if no cached endpoint, or there was one but it has been cleaned up
2198 // (ie. detached), then create a new one.
2199 if (newEndpoint == null || newEndpoint._jsPlumb == null)
2200 newEndpoint = _currentInstance.addEndpoint(_el, p);
2202 if (p.uniqueEndpoint) _targetEndpoints[elid] = newEndpoint; // may of course just store what it just pulled out. that's ok.
2203 // TODO test options to makeTarget to see if we should do this?
2204 newEndpoint._doNotDeleteOnDetach = false; // reset.
2205 newEndpoint._deleteOnDetach = true;
2207 // if the anchor has a 'positionFinder' set, then delegate to that function to find
2208 // out where to locate the anchor.
2209 if (newEndpoint.anchor.positionFinder != null) {
2210 var dropPosition = jpcl.getUIPosition(arguments, _currentInstance.getZoom()),
2211 elPosition = _getOffset(_el, _currentInstance),
2212 elSize = _getSize(_el),
2213 ap = newEndpoint.anchor.positionFinder(dropPosition, elPosition, elSize, newEndpoint.anchor.constructorParams);
2214 newEndpoint.anchor.x = ap[0];
2215 newEndpoint.anchor.y = ap[1];
2216 // now figure an orientation for it..kind of hard to know what to do actually. probably the best thing i can do is to
2217 // support specifying an orientation in the anchor's spec. if one is not supplied then i will make the orientation
2218 // be what will cause the most natural link to the source: it will be pointing at the source, but it needs to be
2219 // specified in one axis only, and so how to make that choice? i think i will use whichever axis is the one in which
2220 // the target is furthest away from the source.
2223 // change the target endpoint and target element information. really this should be
2224 // done on a method on connection
2225 jpc[idx ? "target" : "source"] = newEndpoint.element;
2226 jpc[idx ? "targetId" : "sourceId"] = newEndpoint.elementId;
2227 jpc.endpoints[idx].detachFromConnection(jpc);
2228 if (jpc.endpoints[idx]._deleteOnDetach)
2229 jpc.endpoints[idx].deleteAfterDragStop = true; // tell this endpoint to delet itself after drag stop.
2230 // set new endpoint, and configure the settings for endpoints to delete on detach
2231 newEndpoint.addConnection(jpc);
2232 jpc.endpoints[idx] = newEndpoint;
2233 jpc.deleteEndpointsOnDetach = deleteEndpointsOnDetach;
2235 // inform the anchor manager to update its target endpoint for this connection.
2236 // TODO refactor to make this a single method.
2238 _currentInstance.anchorManager.updateOtherEndpoint(jpc.sourceId, jpc.suspendedElementId, jpc.targetId, jpc);
2240 _currentInstance.anchorManager.sourceChanged(jpc.suspendedEndpoint.elementId, jpc.sourceId, jpc);
2242 _finaliseConnection(jpc, null, originalEvent);
2243 jpc.pending = false;
2246 // if not allowed to drop...
2248 // TODO this code is identical (pretty much) to what happens when a connection
2249 // dragged from a normal endpoint is in this situation. refactor.
2250 // is this an existing connection, and will we reattach?
2251 // TODO also this assumes the source needs to detach - is that always valid?
2252 if (jpc.suspendedEndpoint) {
2253 if (jpc.isReattach()) {
2254 jpc.setHover(false);
2255 jpc.floatingAnchorIndex = null;
2256 jpc.suspendedEndpoint.addConnection(jpc);
2257 _currentInstance.repaint(source.elementId);
2260 source.detach(jpc, false, true, true, originalEvent); // otherwise, detach the connection and tell everyone about it.
2266 // wrap drop events as needed and initialise droppable
2267 var dropEvent = jpcl.dragEvents.drop;
2268 dropOptions.scope = dropOptions.scope || targetScope;
2269 dropOptions[dropEvent] = _ju.wrap(dropOptions[dropEvent], _drop);
2270 jpcl.initDroppable(_gel(elInfo.el), dropOptions, true);
2273 // YUI collection fix
2274 el = _convertYUICollection(el);
2275 // make an array if only given one element
2276 var inputs = el.length && el.constructor != String ? el : [ el ];
2278 // register each one in the list.
2279 for (var i = 0, ii = inputs.length; i < ii; i++) {
2283 return _currentInstance;
2287 this.unmakeTarget = function(el, doNotClearArrays) {
2288 var info = _info(el);
2290 jsPlumb.CurrentLibrary.destroyDroppable(info.el);
2291 // TODO this is not an exhaustive unmake of a target, since it does not remove the droppable stuff from
2292 // the element. the effect will be to prevent it from behaving as a target, but it's not completely purged.
2293 if (!doNotClearArrays) {
2294 delete _targetEndpointDefinitions[info.id];
2295 delete _targetEndpointsUnique[info.id];
2296 delete _targetMaxConnections[info.id];
2297 delete _targetsEnabled[info.id];
2300 return _currentInstance;
2304 this.makeSource = function(el, params, referenceParams) {
2305 var p = jsPlumb.extend({}, referenceParams);
2306 jsPlumb.extend(p, params);
2307 _setEndpointPaintStylesAndAnchor(p, 0);
2308 var jpcl = jsPlumb.CurrentLibrary,
2309 maxConnections = p.maxConnections || -1,
2310 onMaxConnections = p.onMaxConnections,
2311 _doOne = function(elInfo) {
2312 // get the element's id and store the endpoint definition for it. jsPlumb.connect calls will look for one of these,
2313 // and use the endpoint definition if found.
2314 var elid = elInfo.id,
2315 _el = _gel(elInfo.el),
2316 parentElement = function() {
2317 return p.parent == null ? null : p.parent === "parent" ? elInfo.el.parentNode : _dom(p.parent);
2319 idToRegisterAgainst = p.parent != null ? _currentInstance.getId(parentElement()) : elid;
2321 _sourceEndpointDefinitions[idToRegisterAgainst] = p;
2322 _sourceEndpointsUnique[idToRegisterAgainst] = p.uniqueEndpoint;
2323 _sourcesEnabled[idToRegisterAgainst] = true;
2325 var stopEvent = jpcl.dragEvents.stop,
2326 dragEvent = jpcl.dragEvents.drag,
2327 dragOptions = jsPlumb.extend({ }, p.dragOptions || {}),
2328 existingDrag = dragOptions.drag,
2329 existingStop = dragOptions.stop,
2331 endpointAddedButNoDragYet = false;
2333 _sourceMaxConnections[idToRegisterAgainst] = maxConnections;
2335 // set scope if its not set in dragOptions but was passed in in params
2336 dragOptions.scope = dragOptions.scope || p.scope;
2338 dragOptions[dragEvent] = _ju.wrap(dragOptions[dragEvent], function() {
2339 if (existingDrag) existingDrag.apply(this, arguments);
2340 endpointAddedButNoDragYet = false;
2343 dragOptions[stopEvent] = _ju.wrap(dragOptions[stopEvent], function() {
2345 if (existingStop) existingStop.apply(this, arguments);
2346 _currentInstance.currentlyDragging = false;
2347 if (ep._jsPlumb != null) { // if not cleaned up...
2349 jpcl.unbind(ep.canvas, "mousedown");
2351 // reset the anchor to the anchor that was initially provided. the one we were using to drag
2352 // the connection was just a placeholder that was located at the place the user pressed the
2353 // mouse button to initiate the drag.
2354 var anchorDef = p.anchor || _currentInstance.Defaults.Anchor,
2355 oldAnchor = ep.anchor,
2356 oldConnection = ep.connections[0],
2357 newAnchor = _currentInstance.makeAnchor(anchorDef, elid, _currentInstance),
2360 // if the anchor has a 'positionFinder' set, then delegate to that function to find
2361 // out where to locate the anchor. issue 117.
2362 if (newAnchor.positionFinder != null) {
2363 var elPosition = _getOffset(_el, _currentInstance),
2364 elSize = _getSize(_el),
2365 dropPosition = { left:elPosition.left + (oldAnchor.x * elSize[0]), top:elPosition.top + (oldAnchor.y * elSize[1]) },
2366 ap = newAnchor.positionFinder(dropPosition, elPosition, elSize, newAnchor.constructorParams);
2368 newAnchor.x = ap[0];
2369 newAnchor.y = ap[1];
2372 ep.setAnchor(newAnchor, true);
2375 var parent = parentElement();
2377 var potentialParent = p.container || _currentInstance.Defaults.Container || jsPlumb.Defaults.Container;
2378 ep.setElement(parent, potentialParent);
2383 _currentInstance.repaint(ep.elementId);
2384 _currentInstance.repaint(oldConnection.targetId);
2387 // when the user presses the mouse, add an Endpoint, if we are enabled.
2388 var mouseDownListener = function(e) {
2390 // if disabled, return.
2391 if (!_sourcesEnabled[idToRegisterAgainst]) return;
2393 // if a filter was given, run it, and return if it says no.
2395 var evt = jpcl.getOriginalEvent(e),
2396 r = jsPlumbUtil.isString(p.filter) ? selectorFilter(evt, _el, p.filter) : p.filter(evt, _el);
2398 if (r === false) return;
2401 // if maxConnections reached
2402 var sourceCount = _currentInstance.select({source:idToRegisterAgainst}).length;
2403 if (_sourceMaxConnections[idToRegisterAgainst] >= 0 && sourceCount >= _sourceMaxConnections[idToRegisterAgainst]) {
2404 if (onMaxConnections) {
2407 maxConnections:maxConnections
2413 // make sure we have the latest offset for this div
2414 var myOffsetInfo = _updateOffset({elId:elid}).o,
2415 z = _currentInstance.getZoom(),
2416 x = ( ((e.pageX || e.page.x) / z) - myOffsetInfo.left) / myOffsetInfo.width,
2417 y = ( ((e.pageY || e.page.y) / z) - myOffsetInfo.top) / myOffsetInfo.height,
2421 // if there is a parent, the endpoint will actually be added to it now, rather than the div
2422 // that was the source. in that case, we have to adjust the anchor position so it refers to
2425 var pEl = parentElement(), pId = _getId(pEl);
2426 myOffsetInfo = _updateOffset({elId:pId}).o;
2427 parentX = ((e.pageX || e.page.x) - myOffsetInfo.left) / myOffsetInfo.width;
2428 parentY = ((e.pageY || e.page.y) - myOffsetInfo.top) / myOffsetInfo.height;
2431 // we need to override the anchor in here, and force 'isSource', but we don't want to mess with
2432 // the params passed in, because after a connection is established we're going to reset the endpoint
2433 // to have the anchor we were given.
2434 var tempEndpointParams = {};
2435 jsPlumb.extend(tempEndpointParams, p);
2436 tempEndpointParams.isSource = true;
2437 tempEndpointParams.anchor = [x,y,0,0];
2438 tempEndpointParams.parentAnchor = [ parentX, parentY, 0, 0 ];
2439 tempEndpointParams.dragOptions = dragOptions;
2440 // if a parent was given we need to turn that into a "container" argument. this is, by default,
2441 // the parent of the element we will move to, so parent of p.parent in this case. however, if
2442 // the user has specified a 'container' on the endpoint definition or on
2443 // the defaults, we should use that.
2445 var potentialParent = tempEndpointParams.container || _currentInstance.Defaults.Container || jsPlumb.Defaults.Container;
2446 if (potentialParent)
2447 tempEndpointParams.container = potentialParent;
2449 tempEndpointParams.container = jsPlumb.CurrentLibrary.getParent(parentElement());
2452 ep = _currentInstance.addEndpoint(elid, tempEndpointParams);
2454 endpointAddedButNoDragYet = true;
2455 // we set this to prevent connections from firing attach events before this function has had a chance
2456 // to move the endpoint.
2457 ep.endpointWillMoveAfterConnection = p.parent != null;
2458 ep.endpointWillMoveTo = p.parent ? parentElement() : null;
2460 // TODO test options to makeSource to see if we should do this?
2461 ep._doNotDeleteOnDetach = false; // reset.
2462 ep._deleteOnDetach = true;
2464 var _delTempEndpoint = function() {
2465 // this mouseup event is fired only if no dragging occurred, by jquery and yui, but for mootools
2466 // it is fired even if dragging has occurred, in which case we would blow away a perfectly
2467 // legitimate endpoint, were it not for this check. the flag is set after adding an
2468 // endpoint and cleared in a drag listener we set in the dragOptions above.
2469 if(endpointAddedButNoDragYet) {
2470 endpointAddedButNoDragYet = false;
2471 _currentInstance.deleteEndpoint(ep);
2475 _currentInstance.registerListener(ep.canvas, "mouseup", _delTempEndpoint);
2476 _currentInstance.registerListener(_el, "mouseup", _delTempEndpoint);
2478 // and then trigger its mousedown event, which will kick off a drag, which will start dragging
2479 // a new connection from this endpoint.
2480 jpcl.trigger(ep.canvas, "mousedown", e);
2484 // register this on jsPlumb so that it can be cleared by a reset.
2485 _currentInstance.registerListener(_el, "mousedown", mouseDownListener);
2486 _sourceTriggers[elid] = mouseDownListener;
2488 // lastly, if a filter was provided, set it as a dragFilter on the element,
2489 // to prevent the element drag function from kicking in when we want to
2490 // drag a new connection
2491 if (p.filter && jsPlumbUtil.isString(p.filter)) {
2492 jpcl.setDragFilter(_el, p.filter);
2496 el = _convertYUICollection(el);
2498 var inputs = el.length && el.constructor != String ? el : [ el ];
2500 for (var i = 0, ii = inputs.length; i < ii; i++) {
2501 _doOne(_info(inputs[i]));
2504 return _currentInstance;
2508 this.unmakeSource = function(el, doNotClearArrays) {
2509 var info = _info(el),
2510 mouseDownListener = _sourceTriggers[info.id];
2512 if (mouseDownListener)
2513 _currentInstance.unregisterListener(info.el, "mousedown", mouseDownListener);
2515 if (!doNotClearArrays) {
2516 delete _sourceEndpointDefinitions[info.id];
2517 delete _sourceEndpointsUnique[info.id];
2518 delete _sourcesEnabled[info.id];
2519 delete _sourceTriggers[info.id];
2520 delete _sourceMaxConnections[info.id];
2523 return _currentInstance;
2527 this.unmakeEverySource = function() {
2528 for (var i in _sourcesEnabled)
2529 _currentInstance.unmakeSource(i, true);
2531 _sourceEndpointDefinitions = {};
2532 _sourceEndpointsUnique = {};
2533 _sourcesEnabled = {};
2534 _sourceTriggers = {};
2538 this.unmakeEveryTarget = function() {
2539 for (var i in _targetsEnabled)
2540 _currentInstance.unmakeTarget(i, true);
2542 _targetEndpointDefinitions = {};
2543 _targetEndpointsUnique = {};
2544 _targetMaxConnections = {};
2545 _targetsEnabled = {};
2547 return _currentInstance;
2550 // does the work of setting a source enabled or disabled.
2551 var _setEnabled = function(type, el, state, toggle) {
2552 var a = type == "source" ? _sourcesEnabled : _targetsEnabled;
2553 el = _convertYUICollection(el);
2555 if (_ju.isString(el)) a[el] = toggle ? !a[el] : state;
2556 else if (el.length) {
2557 for (var i = 0, ii = el.length; i < ii; i++) {
2558 var info = _info(el[i]);
2559 a[info.id] = toggle ? !a[info.id] : state;
2562 return _currentInstance;
2565 this.toggleSourceEnabled = function(el) {
2566 _setEnabled("source", el, null, true);
2567 return _currentInstance.isSourceEnabled(el);
2570 this.setSourceEnabled = function(el, state) { return _setEnabled("source", el, state); };
2571 this.isSource = function(el) { return _sourcesEnabled[_info(el).id] != null; };
2572 this.isSourceEnabled = function(el) { return _sourcesEnabled[_info(el).id] === true; };
2574 this.toggleTargetEnabled = function(el) {
2575 _setEnabled("target", el, null, true);
2576 return _currentInstance.isTargetEnabled(el);
2579 this.isTarget = function(el) { return _targetsEnabled[_info(el).id] != null; };
2580 this.isTargetEnabled = function(el) { return _targetsEnabled[_info(el).id] === true; };
2581 this.setTargetEnabled = function(el, state) { return _setEnabled("target", el, state); };
2583 // --------------------- end makeSource/makeTarget ----------------------------------------------
2585 this.ready = function(fn) {
2586 _currentInstance.bind("ready", fn);
2589 // repaint some element's endpoints and connections
2590 this.repaint = function(el, ui, timestamp) {
2591 // support both lists...
2592 if (typeof el == 'object' && el.length)
2593 for ( var i = 0, ii = el.length; i < ii; i++) {
2594 _draw(el[i], ui, timestamp);
2596 else // ...and single strings.
2597 _draw(el, ui, timestamp);
2599 return _currentInstance;
2602 // repaint every endpoint and connection.
2603 this.repaintEverything = function(clearEdits) {
2604 // TODO this timestamp causes continuous anchors to not repaint properly.
2605 // fix this. do not just take out the timestamp. it runs a lot faster with
2606 // the timestamp included.
2607 //var timestamp = null;
2608 var timestamp = _timestamp();
2609 for ( var elId in endpointsByElement) {
2610 _draw(elId, null, timestamp, clearEdits);
2612 return _currentInstance;
2616 this.removeAllEndpoints = function(el, recurse) {
2617 var _one = function(_el) {
2618 var info = _info(_el),
2619 ebe = endpointsByElement[info.id],
2623 for ( i = 0, ii = ebe.length; i < ii; i++)
2624 _currentInstance.deleteEndpoint(ebe[i]);
2626 delete endpointsByElement[info.id];
2629 if (info.el && info.el.nodeType != 3 && info.el.nodeType != 8 ) {
2630 for ( i = 0, ii = info.el.childNodes.length; i < ii; i++) {
2631 _one(info.el.childNodes[i]);
2638 return _currentInstance;
2642 * Remove the given element, including cleaning up all endpoints registered for it.
2643 * This is exposed in the public API but also used internally by jsPlumb when removing the
2644 * element associated with a connection drag.
2646 this.remove = function(el, doNotRepaint) {
2647 var info = _info(el);
2648 _currentInstance.doWhileSuspended(function() {
2649 _currentInstance.removeAllEndpoints(info.id, true);
2650 _currentInstance.dragManager.elementRemoved(info.id);
2651 delete floatingConnections[info.id];
2652 _currentInstance.anchorManager.clearFor(info.id);
2653 _currentInstance.anchorManager.removeFloatingConnection(info.id);
2654 }, doNotRepaint === false);
2655 if(info.el) jsPlumb.CurrentLibrary.removeElement(info.el);
2658 var _registeredListeners = {},
2659 _unbindRegisteredListeners = function() {
2660 for (var i in _registeredListeners) {
2661 for (var j = 0, jj = _registeredListeners[i].length; j < jj; j++) {
2662 var info = _registeredListeners[i][j];
2663 jsPlumb.CurrentLibrary.unbind(info.el, info.event, info.listener);
2666 _registeredListeners = {};
2669 // internal register listener method. gives us a hook to clean things up
2670 // with if the user calls jsPlumb.reset.
2671 this.registerListener = function(el, type, listener) {
2672 jsPlumb.CurrentLibrary.bind(el, type, listener);
2673 jsPlumbUtil.addToList(_registeredListeners, type, {el:el, event:type, listener:listener});
2676 this.unregisterListener = function(el, type, listener) {
2677 jsPlumb.CurrentLibrary.unbind(el, type, listener);
2678 jsPlumbUtil.removeWithFunction(_registeredListeners, function(rl) {
2679 return rl.type == type && rl.listener == listener;
2683 this.reset = function() {
2684 _currentInstance.deleteEveryEndpoint();
2685 _currentInstance.unbind();
2686 _targetEndpointDefinitions = {};
2687 _targetEndpoints = {};
2688 _targetEndpointsUnique = {};
2689 _targetMaxConnections = {};
2690 _sourceEndpointDefinitions = {};
2691 _sourceEndpoints = {};
2692 _sourceEndpointsUnique = {};
2693 _sourceMaxConnections = {};
2694 connections.splice(0);
2695 _unbindRegisteredListeners();
2696 _currentInstance.anchorManager.reset();
2697 if (!jsPlumbAdapter.headless)
2698 _currentInstance.dragManager.reset();
2702 this.setDefaultScope = function(scope) {
2703 DEFAULT_SCOPE = scope;
2704 return _currentInstance;
2707 // sets whether or not some element should be currently draggable.
2708 this.setDraggable = _setDraggable;
2710 // sets the id of some element, changing whatever we need to to keep track.
2711 this.setId = function(el, newId, doNotSetAttribute) {
2715 if (jsPlumbUtil.isString(el)) {
2720 id = _currentInstance.getId(el);
2723 var sConns = _currentInstance.getConnections({source:id, scope:'*'}, true),
2724 tConns = _currentInstance.getConnections({target:id, scope:'*'}, true);
2728 if (!doNotSetAttribute) {
2730 jsPlumbAdapter.setAttribute(el, "id", newId);
2735 endpointsByElement[newId] = endpointsByElement[id] || [];
2736 for (var i = 0, ii = endpointsByElement[newId].length; i < ii; i++) {
2737 endpointsByElement[newId][i].setElementId(newId);
2738 endpointsByElement[newId][i].setReferenceElement(el);
2740 delete endpointsByElement[id];
2742 _currentInstance.anchorManager.changeId(id, newId);
2743 if (!jsPlumbAdapter.headless)
2744 _currentInstance.dragManager.changeId(id, newId);
2746 var _conns = function(list, epIdx, type) {
2747 for (var i = 0, ii = list.length; i < ii; i++) {
2748 list[i].endpoints[epIdx].setElementId(newId);
2749 list[i].endpoints[epIdx].setReferenceElement(el);
2750 list[i][type + "Id"] = newId;
2754 _conns(sConns, 0, "source");
2755 _conns(tConns, 1, "target");
2757 _currentInstance.repaint(newId);
2760 this.setDebugLog = function(debugLog) {
2764 this.setSuspendDrawing = function(val, repaintAfterwards) {
2765 var curVal = _suspendDrawing;
2766 _suspendDrawing = val;
2767 if (val) _suspendedAt = new Date().getTime(); else _suspendedAt = null;
2768 if (repaintAfterwards) _currentInstance.repaintEverything();
2772 // returns whether or not drawing is currently suspended.
2773 this.isSuspendDrawing = function() {
2774 return _suspendDrawing;
2777 // return timestamp for when drawing was suspended.
2778 this.getSuspendedAt = function() { return _suspendedAt; };
2782 * @name jsPlumb.class:doWhileSuspended
2783 * @param {function} fn Function to run while suspended.
2784 * @param {boolean} doNotRepaintAfterwards If true, jsPlumb won't run a full repaint. Otherwise it will.
2785 * @description Suspends drawing, runs the given function, then re-enables drawing (and repaints, unless you tell it not to)
2787 this.doWhileSuspended = function(fn, doNotRepaintAfterwards) {
2788 var _wasSuspended = _currentInstance.isSuspendDrawing();
2790 _currentInstance.setSuspendDrawing(true);
2795 _ju.log("Function run while suspended failed", e);
2798 _currentInstance.setSuspendDrawing(false, !doNotRepaintAfterwards);
2801 this.updateOffset = _updateOffset;
2802 this.getOffset = function(elId) { return offsets[elId]; };
2803 this.getSize = function(elId) { return sizes[elId]; };
2804 this.getCachedData = _getCachedData;
2805 this.timestamp = _timestamp;
2811 * @name jsPlumb.class:setRenderMode
2812 * @param {string} mode One of `jsPlumb.SVG, `jsPlumb.VML` or `jsPlumb.CANVAS`.
2813 * @description Sets render mode. jsPlumb will fall back to VML if it determines that
2814 * what you asked for is not supported (and that VML is). If you asked for VML but the browser does
2815 * not support it, jsPlumb uses SVG.
2816 * @return {string} The render mode that jsPlumb set, which of course may be different from that requested.
2818 this.setRenderMode = function(mode) {
2819 renderMode = jsPlumbAdapter.setRenderMode(mode);
2821 // only add this if the renderer is canvas; we dont want these listeners registered on te
2822 // entire document otherwise.
2823 if (renderMode == jsPlumb.CANVAS) {
2824 var bindOne = function(event) {
2825 jsPlumb.CurrentLibrary.bind(document, event, function(e) {
2826 if (!_currentInstance.currentlyDragging && renderMode == jsPlumb.CANVAS) {
2827 // try connections first
2828 for (i = 0, ii = connections.length; i < ii; i++ ) {
2829 var t = connections[i].getConnector()[event](e);
2832 for (var el in endpointsByElement) {
2833 var ee = endpointsByElement[el];
2834 for ( i = 0, ii = ee.length; i < ii; i++ ) {
2835 if (ee[i].endpoint[event] && ee[i].endpoint[event](e)) return;
2841 bindOne("click");bindOne("dblclick");bindOne("mousemove");bindOne("mousedown");bindOne("mouseup");bindOne("contextmenu");
2849 * @name jsPlumb.class:getRenderMode
2850 * @description Gets the current render mode for this instance of jsPlumb.
2851 * @return {string} The current render mode - "canvas", "svg" or "vml".
2853 this.getRenderMode = function() { return renderMode; };
2855 this.show = function(el, changeEndpoints) {
2856 _setVisible(el, "block", changeEndpoints);
2857 return _currentInstance;
2861 * gets some test hooks. nothing writable.
2863 this.getTestHarness = function() {
2865 endpointsByElement : endpointsByElement,
2866 endpointCount : function(elId) {
2867 var e = endpointsByElement[elId];
2868 return e ? e.length : 0;
2870 connectionCount : function(scope) {
2871 scope = scope || DEFAULT_SCOPE;
2872 var c = _currentInstance.getConnections({scope:scope});
2873 return c ? c.length : 0;
2876 makeAnchor:self.makeAnchor,
2877 makeDynamicAnchor:self.makeDynamicAnchor
2882 // TODO: update this method to return the current state.
2883 this.toggleVisible = _toggleVisible;
2884 this.toggleDraggable = _toggleDraggable;
2885 this.addListener = this.bind;
2888 helper method to take an xy location and adjust it for the parent's offset and scroll.
2890 this.adjustForParentOffsetAndScroll = function(xy, el) {
2892 var offsetParent = null, result = xy;
2893 if (el.tagName.toLowerCase() === "svg" && el.parentNode) {
2894 offsetParent = el.parentNode;
2896 else if (el.offsetParent) {
2897 offsetParent = el.offsetParent;
2899 if (offsetParent != null) {
2900 var po = offsetParent.tagName.toLowerCase() === "body" ? {left:0,top:0} : _getOffset(offsetParent, _currentInstance),
2901 so = offsetParent.tagName.toLowerCase() === "body" ? {left:0,top:0} : {left:offsetParent.scrollLeft, top:offsetParent.scrollTop};
2903 // i thought it might be cool to do this:
2904 // lastReturnValue[0] = lastReturnValue[0] - offsetParent.offsetLeft + offsetParent.scrollLeft;
2905 // lastReturnValue[1] = lastReturnValue[1] - offsetParent.offsetTop + offsetParent.scrollTop;
2906 // but i think it ignores margins. my reasoning was that it's quicker to not hand off to some underlying
2909 result[0] = xy[0] - po.left + so.left;
2910 result[1] = xy[1] - po.top + so.top;
2917 if (!jsPlumbAdapter.headless) {
2918 _currentInstance.dragManager = jsPlumbAdapter.getDragManager(_currentInstance);
2919 _currentInstance.recalculateOffsets = _currentInstance.dragManager.updateOffsets;
2924 jsPlumbUtil.extend(jsPlumbInstance, jsPlumbUtil.EventGenerator, {
2925 setAttribute : function(el, a, v) {
2926 jsPlumbAdapter.setAttribute(el, a, v);
2928 getAttribute : function(el, a) {
2929 return jsPlumbAdapter.getAttribute(jsPlumb.CurrentLibrary.getDOMElement(el), a);
2931 registerConnectionType : function(id, type) {
2932 this._connectionTypes[id] = jsPlumb.extend({}, type);
2934 registerConnectionTypes : function(types) {
2935 for (var i in types)
2936 this._connectionTypes[i] = jsPlumb.extend({}, types[i]);
2938 registerEndpointType : function(id, type) {
2939 this._endpointTypes[id] = jsPlumb.extend({}, type);
2941 registerEndpointTypes : function(types) {
2942 for (var i in types)
2943 this._endpointTypes[i] = jsPlumb.extend({}, types[i]);
2945 getType : function(id, typeDescriptor) {
2946 return typeDescriptor === "connection" ? this._connectionTypes[id] : this._endpointTypes[id];
2948 setIdChanged : function(oldId, newId) {
2949 this.setId(oldId, newId, true);
2951 // set parent: change the parent for some node and update all the registrations we need to.
2952 setParent : function(el, newParent) {
2953 var jpcl = jsPlumb.CurrentLibrary,
2954 _el = jpcl.getElementObject(el),
2955 _dom = jpcl.getDOMElement(_el),
2956 _id = this.getId(_dom),
2957 _pel = jpcl.getElementObject(newParent),
2958 _pdom = jpcl.getDOMElement(_pel),
2959 _pid = this.getId(_pdom);
2961 _dom.parentNode.removeChild(_dom);
2962 _pdom.appendChild(_dom);
2963 this.dragManager.setParent(_el, _id, _pel, _pid);
2967 // --------------------- static instance + AMD registration -------------------------------------------
2969 // create static instance and assign to window if window exists.
2970 var jsPlumb = new jsPlumbInstance();
2971 // register on window if defined (lets us run on server)
2972 if (typeof window != 'undefined') window.jsPlumb = jsPlumb;
2973 // add 'getInstance' method to static instance
2975 * @name jsPlumb.getInstance
2976 * @param {object} [_defaults] Optional default settings for the new instance.
2977 * @desc Gets a new instance of jsPlumb.
2979 jsPlumb.getInstance = function(_defaults) {
2980 var j = new jsPlumbInstance(_defaults);
2984 // maybe register static instance as an AMD module, and getInstance method too.
2985 if ( typeof define === "function") {
2986 define( "jsplumb", [], function () { return jsPlumb; } );
2987 define( "jsplumbinstance", [], function () { return jsPlumb.getInstance(); } );
2990 if (typeof exports !== 'undefined') {
2991 exports.jsPlumb = jsPlumb;
2995 // --------------------- end static instance + AMD registration -------------------------------------------