X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;f=portal%2Fstatic%2Funbound_reservation_static%2Fsrc%2FjsPlumb.js;fp=portal%2Fstatic%2Funbound_reservation_static%2Fsrc%2FjsPlumb.js;h=c464b47dd8cdc35ea11c3920e565d7a0aa9f8ce9;hb=729a9dbb380b51a217194ba2a4e5978186fe50b0;hp=0000000000000000000000000000000000000000;hpb=c4bd5da6e2630eddf1262aa8d808dbb48b097d53;p=unfold.git diff --git a/portal/static/unbound_reservation_static/src/jsPlumb.js b/portal/static/unbound_reservation_static/src/jsPlumb.js new file mode 100644 index 00000000..c464b47d --- /dev/null +++ b/portal/static/unbound_reservation_static/src/jsPlumb.js @@ -0,0 +1,2997 @@ +/** + * @module jsPlumb + * @description Provides a way to visually connect elements on an HTML page, using either SVG, Canvas + * elements, or VML. + * + * - [Demo Site](http://jsplumb.org) + * - [GitHub](http://github.com/sporritt/jsplumb) + * + * Dual licensed under the MIT and GPL2 licenses. + * + * Copyright (c) 2010 - 2013 Simon Porritt (simon.porritt@gmail.com) + */ +;(function() { + + var _ju = jsPlumbUtil, + _addClass = function(el, clazz) { jsPlumb.CurrentLibrary.addClass(_gel(el), clazz); }, + _hasClass = function(el, clazz) { return jsPlumb.CurrentLibrary.hasClass(_gel(el), clazz); }, + _removeClass = function(el, clazz) { jsPlumb.CurrentLibrary.removeClass(_gel(el), clazz); }, + _gel = function(el) { return jsPlumb.CurrentLibrary.getElementObject(el); }, + _dom = function(el) { return jsPlumb.CurrentLibrary.getDOMElement(el); }, + _getOffset = function(el, _instance) { + var o = jsPlumb.CurrentLibrary.getOffset(_gel(el)); + if (_instance != null) { + var z = _instance.getZoom(); + return {left:o.left / z, top:o.top / z }; + } + else + return o; + }, + _getSize = function(el) { + return jsPlumb.CurrentLibrary.getSize(_gel(el)); + }, + + /** + * creates a timestamp, using milliseconds since 1970, but as a string. + */ + _timestamp = function() { return "" + (new Date()).getTime(); }, + + // helper method to update the hover style whenever it, or paintStyle, changes. + // we use paintStyle as the foundation and merge hoverPaintStyle over the + // top. + _updateHoverStyle = function(component) { + if (component._jsPlumb.paintStyle && component._jsPlumb.hoverPaintStyle) { + var mergedHoverStyle = {}; + jsPlumb.extend(mergedHoverStyle, component._jsPlumb.paintStyle); + jsPlumb.extend(mergedHoverStyle, component._jsPlumb.hoverPaintStyle); + delete component._jsPlumb.hoverPaintStyle; + // we want the fillStyle of paintStyle to override a gradient, if possible. + if (mergedHoverStyle.gradient && component._jsPlumb.paintStyle.fillStyle) + delete mergedHoverStyle.gradient; + component._jsPlumb.hoverPaintStyle = mergedHoverStyle; + } + }, + events = [ "click", "dblclick", "mouseenter", "mouseout", "mousemove", "mousedown", "mouseup", "contextmenu" ], + eventFilters = { "mouseout":"mouseexit" }, + _updateAttachedElements = function(component, state, timestamp, sourceElement) { + var affectedElements = component.getAttachedElements(); + if (affectedElements) { + for (var i = 0, j = affectedElements.length; i < j; i++) { + if (!sourceElement || sourceElement != affectedElements[i]) + affectedElements[i].setHover(state, true, timestamp); // tell the attached elements not to inform their own attached elements. + } + } + }, + _splitType = function(t) { return t == null ? null : t.split(" "); }, + _applyTypes = function(component, params, doNotRepaint) { + if (component.getDefaultType) { + var td = component.getTypeDescriptor(); + + var o = _ju.merge({}, component.getDefaultType()); + for (var i = 0, j = component._jsPlumb.types.length; i < j; i++) + o = _ju.merge(o, component._jsPlumb.instance.getType(component._jsPlumb.types[i], td)); + + if (params) { + o = _ju.populate(o, params); + } + + component.applyType(o, doNotRepaint); + if (!doNotRepaint) component.repaint(); + } + }, + +// ------------------------------ BEGIN jsPlumbUIComponent -------------------------------------------- + + jsPlumbUIComponent = window.jsPlumbUIComponent = function(params) { + + jsPlumbUtil.EventGenerator.apply(this, arguments); + + var self = this, + a = arguments, + idPrefix = self.idPrefix, + id = idPrefix + (new Date()).getTime(), + jpcl = jsPlumb.CurrentLibrary; + + this._jsPlumb = { + instance: params._jsPlumb, + parameters:params.parameters || {}, + paintStyle:null, + hoverPaintStyle:null, + paintStyleInUse:null, + hover:false, + beforeDetach:params.beforeDetach, + beforeDrop:params.beforeDrop, + overlayPlacements : [], + hoverClass: params.hoverClass || params._jsPlumb.Defaults.HoverClass || jsPlumb.Defaults.HoverClass, + types:[] + }; + + this.getId = function() { return id; }; + + // all components can generate events + + if (params.events) { + for (var i in params.events) + self.bind(i, params.events[i]); + } + + // all components get this clone function. + // TODO issue 116 showed a problem with this - it seems 'a' that is in + // the clone function's scope is shared by all invocations of it, the classic + // JS closure problem. for now, jsPlumb does a version of this inline where + // it used to call clone. but it would be nice to find some time to look + // further at this. + this.clone = function() { + var o = {};//new Object(); + this.constructor.apply(o, a); + return o; + }.bind(this); + + // user can supply a beforeDetach callback, which will be executed before a detach + // is performed; returning false prevents the detach. + this.isDetachAllowed = function(connection) { + var r = true; + if (this._jsPlumb.beforeDetach) { + try { + r = this._jsPlumb.beforeDetach(connection); + } + catch (e) { _ju.log("jsPlumb: beforeDetach callback failed", e); } + } + return r; + }; + + // user can supply a beforeDrop callback, which will be executed before a dropped + // connection is confirmed. user can return false to reject connection. + this.isDropAllowed = function(sourceId, targetId, scope, connection, dropEndpoint) { + var r = this._jsPlumb.instance.checkCondition("beforeDrop", { + sourceId:sourceId, + targetId:targetId, + scope:scope, + connection:connection, + dropEndpoint:dropEndpoint + }); + if (this._jsPlumb.beforeDrop) { + try { + r = this._jsPlumb.beforeDrop({ + sourceId:sourceId, + targetId:targetId, + scope:scope, + connection:connection, + dropEndpoint:dropEndpoint + }); + } + catch (e) { _ju.log("jsPlumb: beforeDrop callback failed", e); } + } + return r; + }; + + var boundListeners = [], + bindAListener = function(obj, type, fn) { + boundListeners.push([obj, type, fn]); + obj.bind(type, fn); + }, + domListeners = [], + bindOne = function(o, c, evt) { + var filteredEvent = eventFilters[evt] || evt, + fn = function(ee) { + c.fire(filteredEvent, c, ee); + }; + domListeners.push([o, evt, fn]); + jpcl.bind(o, evt, fn); + }, + unbindOne = function(o, evt, fn) { + var filteredEvent = eventFilters[evt] || evt; + jpcl.unbind(o, evt, fn); + }; + + this.bindListeners = function(obj, _self, _hoverFunction) { + bindAListener(obj, "click", function(ep, e) { _self.fire("click", _self, e); }); + bindAListener(obj, "dblclick", function(ep, e) { _self.fire("dblclick", _self, e); }); + bindAListener(obj, "contextmenu", function(ep, e) { _self.fire("contextmenu", _self, e); }); + bindAListener(obj, "mouseenter", function(ep, e) { + if (!_self.isHover()) { + _hoverFunction(true); + _self.fire("mouseenter", _self, e); + } + }); + bindAListener(obj, "mouseexit", function(ep, e) { + if (_self.isHover()) { + _hoverFunction(false); + _self.fire("mouseexit", _self, e); + } + }); + bindAListener(obj, "mousedown", function(ep, e) { _self.fire("mousedown", _self, e); }); + bindAListener(obj, "mouseup", function(ep, e) { _self.fire("mouseup", _self, e); }); + }; + + this.unbindListeners = function() { + for (var i = 0; i < boundListeners.length; i++) { + var o = boundListeners[i]; + o[0].unbind(o[1], o[2]); + } + boundListeners = null; + }; + + this.attachListeners = function(o, c) { + for (var i = 0, j = events.length; i < j; i++) { + bindOne(o, c, events[i]); + } + }; + this.detachListeners = function() { + for (var i = 0; i < domListeners.length; i++) { + unbindOne(domListeners[i][0], domListeners[i][1], domListeners[i][2]); + } + domListeners = null; + }; + + this.reattachListenersForElement = function(o) { + if (arguments.length > 1) { + for (var i = 0, j = events.length; i < j; i++) + unbindOne(o, events[i]); + for (i = 1, j = arguments.length; i < j; i++) + this.attachListeners(o, arguments[i]); + } + }; + }; + + jsPlumbUtil.extend(jsPlumbUIComponent, jsPlumbUtil.EventGenerator, { + + getParameter : function(name) { + return this._jsPlumb.parameters[name]; + }, + + setParameter : function(name, value) { + this._jsPlumb.parameters[name] = value; + }, + + getParameters : function() { + return this._jsPlumb.parameters; + }, + + setParameters : function(p) { + this._jsPlumb.parameters = p; + }, + + addClass : function(clazz) { + if (this.canvas != null) + _addClass(this.canvas, clazz); + }, + + removeClass : function(clazz) { + if (this.canvas != null) + _removeClass(this.canvas, clazz); + }, + + setType : function(typeId, params, doNotRepaint) { + this._jsPlumb.types = _splitType(typeId) || []; + _applyTypes(this, params, doNotRepaint); + }, + + getType : function() { + return this._jsPlumb.types; + }, + + reapplyTypes : function(params, doNotRepaint) { + _applyTypes(this, params, doNotRepaint); + }, + + hasType : function(typeId) { + return jsPlumbUtil.indexOf(this._jsPlumb.types, typeId) != -1; + }, + + addType : function(typeId, params, doNotRepaint) { + var t = _splitType(typeId), _cont = false; + if (t != null) { + for (var i = 0, j = t.length; i < j; i++) { + if (!this.hasType(t[i])) { + this._jsPlumb.types.push(t[i]); + _cont = true; + } + } + if (_cont) _applyTypes(this, params, doNotRepaint); + } + }, + + removeType : function(typeId, doNotRepaint) { + var t = _splitType(typeId), _cont = false, _one = function(tt) { + var idx = _ju.indexOf(this._jsPlumb.types, tt); + if (idx != -1) { + this._jsPlumb.types.splice(idx, 1); + return true; + } + return false; + }.bind(this); + + if (t != null) { + for (var i = 0,j = t.length; i < j; i++) { + _cont = _one(t[i]) || _cont; + } + if (_cont) _applyTypes(this, null, doNotRepaint); + } + }, + + toggleType : function(typeId, params, doNotRepaint) { + var t = _splitType(typeId); + if (t != null) { + for (var i = 0, j = t.length; i < j; i++) { + var idx = jsPlumbUtil.indexOf(this._jsPlumb.types, t[i]); + if (idx != -1) + this._jsPlumb.types.splice(idx, 1); + else + this._jsPlumb.types.push(t[i]); + } + + _applyTypes(this, params, doNotRepaint); + } + }, + applyType : function(t, doNotRepaint) { + this.setPaintStyle(t.paintStyle, doNotRepaint); + this.setHoverPaintStyle(t.hoverPaintStyle, doNotRepaint); + if (t.parameters){ + for (var i in t.parameters) + this.setParameter(i, t.parameters[i]); + } + }, + setPaintStyle : function(style, doNotRepaint) { +// this._jsPlumb.paintStyle = jsPlumb.extend({}, style); +// TODO figure out if we want components to clone paintStyle so as not to share it. + this._jsPlumb.paintStyle = style; + this._jsPlumb.paintStyleInUse = this._jsPlumb.paintStyle; + _updateHoverStyle(this); + if (!doNotRepaint) this.repaint(); + }, + getPaintStyle : function() { + return this._jsPlumb.paintStyle; + }, + setHoverPaintStyle : function(style, doNotRepaint) { + //this._jsPlumb.hoverPaintStyle = jsPlumb.extend({}, style); +// TODO figure out if we want components to clone paintStyle so as not to share it. + this._jsPlumb.hoverPaintStyle = style; + _updateHoverStyle(this); + if (!doNotRepaint) this.repaint(); + }, + getHoverPaintStyle : function() { + return this._jsPlumb.hoverPaintStyle; + }, + cleanup:function() { + this.unbindListeners(); + this.detachListeners(); + }, + destroy:function() { + this.cleanupListeners(); + this.clone = null; + this._jsPlumb = null; + }, + + isHover : function() { return this._jsPlumb.hover; }, + + setHover : function(hover, ignoreAttachedElements, timestamp) { + var jpcl = jsPlumb.CurrentLibrary; + // while dragging, we ignore these events. this keeps the UI from flashing and + // swishing and whatevering. + if (this._jsPlumb && !this._jsPlumb.instance.currentlyDragging && !this._jsPlumb.instance.isHoverSuspended()) { + + this._jsPlumb.hover = hover; + + if (this.canvas != null) { + if (this._jsPlumb.instance.hoverClass != null) { + jpcl[hover ? "addClass" : "removeClass"](this.canvas, this._jsPlumb.instance.hoverClass); + } + } + if (this._jsPlumb.hoverPaintStyle != null) { + this._jsPlumb.paintStyleInUse = hover ? this._jsPlumb.hoverPaintStyle : this._jsPlumb.paintStyle; + if (!this._jsPlumb.instance.isSuspendDrawing()) { + timestamp = timestamp || _timestamp(); + this.repaint({timestamp:timestamp, recalc:false}); + } + } + // get the list of other affected elements, if supported by this component. + // for a connection, its the endpoints. for an endpoint, its the connections! surprise. + if (this.getAttachedElements && !ignoreAttachedElements) + _updateAttachedElements(this, hover, _timestamp(), this); + } + } + }); + +// ------------------------------ END jsPlumbUIComponent -------------------------------------------- + +// ------------------------------ BEGIN OverlayCapablejsPlumbUIComponent -------------------------------------------- + + var _internalLabelOverlayId = "__label", + // helper to get the index of some overlay + _getOverlayIndex = function(component, id) { + var idx = -1; + for (var i = 0, j = component._jsPlumb.overlays.length; i < j; i++) { + if (id === component._jsPlumb.overlays[i].id) { + idx = i; + break; + } + } + return idx; + }, + // this is a shortcut helper method to let people add a label as + // overlay. + _makeLabelOverlay = function(component, params) { + + var _params = { + cssClass:params.cssClass, + labelStyle : component.labelStyle, + id:_internalLabelOverlayId, + component:component, + _jsPlumb:component._jsPlumb.instance // TODO not necessary, since the instance can be accessed through the component. + }, + mergedParams = jsPlumb.extend(_params, params); + + return new jsPlumb.Overlays[component._jsPlumb.instance.getRenderMode()].Label( mergedParams ); + }, + _processOverlay = function(component, o) { + var _newOverlay = null; + if (_ju.isArray(o)) { // this is for the shorthand ["Arrow", { width:50 }] syntax + // there's also a three arg version: + // ["Arrow", { width:50 }, {location:0.7}] + // which merges the 3rd arg into the 2nd. + var type = o[0], + // make a copy of the object so as not to mess up anyone else's reference... + p = jsPlumb.extend({component:component, _jsPlumb:component._jsPlumb.instance}, o[1]); + if (o.length == 3) jsPlumb.extend(p, o[2]); + _newOverlay = new jsPlumb.Overlays[component._jsPlumb.instance.getRenderMode()][type](p); + } else if (o.constructor == String) { + _newOverlay = new jsPlumb.Overlays[component._jsPlumb.instance.getRenderMode()][o]({component:component, _jsPlumb:component._jsPlumb.instance}); + } else { + _newOverlay = o; + } + + component._jsPlumb.overlays.push(_newOverlay); + }, + _calculateOverlaysToAdd = function(component, params) { + var defaultKeys = component.defaultOverlayKeys || [], o = params.overlays, + checkKey = function(k) { + return component._jsPlumb.instance.Defaults[k] || jsPlumb.Defaults[k] || []; + }; + + if (!o) o = []; + + for (var i = 0, j = defaultKeys.length; i < j; i++) + o.unshift.apply(o, checkKey(defaultKeys[i])); + + return o; + }, + OverlayCapableJsPlumbUIComponent = window.OverlayCapableJsPlumbUIComponent = function(params) { + + jsPlumbUIComponent.apply(this, arguments); + this._jsPlumb.overlays = []; + + var _overlays = _calculateOverlaysToAdd(this, params); + if (_overlays) { + for (var i = 0, j = _overlays.length; i < j; i++) { + _processOverlay(this, _overlays[i]); + } + } + + if (params.label) { + var loc = params.labelLocation || this.defaultLabelLocation || 0.5, + labelStyle = params.labelStyle || this._jsPlumb.instance.Defaults.LabelStyle || jsPlumb.Defaults.LabelStyle; + + this._jsPlumb.overlays.push(_makeLabelOverlay(this, { + label:params.label, + location:loc, + labelStyle:labelStyle + })); + } + }; + + jsPlumbUtil.extend(OverlayCapableJsPlumbUIComponent, jsPlumbUIComponent, { + applyType : function(t, doNotRepaint) { + this.removeAllOverlays(doNotRepaint); + if (t.overlays) { + for (var i = 0, j = t.overlays.length; i < j; i++) + this.addOverlay(t.overlays[i], true); + } + }, + setHover : function(hover, ignoreAttachedElements, timestamp) { + if (this._jsPlumb && !this._jsPlumb.instance.isConnectionBeingDragged()) { + for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++) { + this._jsPlumb.overlays[i][hover ? "addClass":"removeClass"](this._jsPlumb.instance.hoverClass); + } + } + }, + addOverlay : function(overlay, doNotRepaint) { + _processOverlay(this, overlay); + if (!doNotRepaint) this.repaint(); + }, + getOverlay : function(id) { + var idx = _getOverlayIndex(this, id); + return idx >= 0 ? this._jsPlumb.overlays[idx] : null; + }, + getOverlays : function() { + return this._jsPlumb.overlays; + }, + hideOverlay : function(id) { + var o = this.getOverlay(id); + if (o) o.hide(); + }, + hideOverlays : function() { + for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++) + this._jsPlumb.overlays[i].hide(); + }, + showOverlay : function(id) { + var o = this.getOverlay(id); + if (o) o.show(); + }, + showOverlays : function() { + for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++) + this._jsPlumb.overlays[i].show(); + }, + removeAllOverlays : function(doNotRepaint) { + for (var i = 0, j = this._jsPlumb.overlays.length; i < j; i++) { + if (this._jsPlumb.overlays[i].cleanup) this._jsPlumb.overlays[i].cleanup(); + } + + this._jsPlumb.overlays.splice(0, this._jsPlumb.overlays.length); + this._jsPlumb.overlayPositions = null; + if (!doNotRepaint) + this.repaint(); + }, + removeOverlay : function(overlayId) { + var idx = _getOverlayIndex(this, overlayId); + if (idx != -1) { + var o = this._jsPlumb.overlays[idx]; + if (o.cleanup) o.cleanup(); + this._jsPlumb.overlays.splice(idx, 1); + this._jsPlumb.overlayPositions && delete this._jsPlumb.overlayPositions[overlayId]; + } + }, + removeOverlays : function() { + for (var i = 0, j = arguments.length; i < j; i++) + this.removeOverlay(arguments[i]); + }, + getLabel : function() { + var lo = this.getOverlay(_internalLabelOverlayId); + return lo != null ? lo.getLabel() : null; + }, + getLabelOverlay : function() { + return this.getOverlay(_internalLabelOverlayId); + }, + setLabel : function(l) { + var lo = this.getOverlay(_internalLabelOverlayId); + if (!lo) { + var params = l.constructor == String || l.constructor == Function ? { label:l } : l; + lo = _makeLabelOverlay(this, params); + this._jsPlumb.overlays.push(lo); + } + else { + if (l.constructor == String || l.constructor == Function) lo.setLabel(l); + else { + if (l.label) lo.setLabel(l.label); + if (l.location) lo.setLocation(l.location); + } + } + + if (!this._jsPlumb.instance.isSuspendDrawing()) + this.repaint(); + }, + cleanup:function() { + for (var i = 0; i < this._jsPlumb.overlays.length; i++) { + this._jsPlumb.overlays[i].cleanup(); + this._jsPlumb.overlays[i].destroy(); + } + this._jsPlumb.overlays.splice(0); + this._jsPlumb.overlayPositions = null; + }, + setVisible:function(v) { + this[v ? "showOverlays" : "hideOverlays"](); + }, + setAbsoluteOverlayPosition:function(overlay, xy) { + this._jsPlumb.overlayPositions = this._jsPlumb.overlayPositions || {}; + this._jsPlumb.overlayPositions[overlay.id] = xy; + }, + getAbsoluteOverlayPosition:function(overlay) { + return this._jsPlumb.overlayPositions ? this._jsPlumb.overlayPositions[overlay.id] : null; + } + }); + +// ------------------------------ END OverlayCapablejsPlumbUIComponent -------------------------------------------- + + var _jsPlumbInstanceIndex = 0, + getInstanceIndex = function() { + var i = _jsPlumbInstanceIndex + 1; + _jsPlumbInstanceIndex++; + return i; + }; + + var jsPlumbInstance = window.jsPlumbInstance = function(_defaults) { + + this.Defaults = { + Anchor : "BottomCenter", + Anchors : [ null, null ], + ConnectionsDetachable : true, + ConnectionOverlays : [ ], + Connector : "Bezier", + Container : null, + DoNotThrowErrors:false, + DragOptions : { }, + DropOptions : { }, + Endpoint : "Dot", + EndpointOverlays : [ ], + Endpoints : [ null, null ], + EndpointStyle : { fillStyle : "#456" }, + EndpointStyles : [ null, null ], + EndpointHoverStyle : null, + EndpointHoverStyles : [ null, null ], + HoverPaintStyle : null, + LabelStyle : { color : "black" }, + LogEnabled : false, + Overlays : [ ], + MaxConnections : 1, + PaintStyle : { lineWidth : 8, strokeStyle : "#456" }, + ReattachConnections:false, + RenderMode : "svg", + Scope : "jsPlumb_DefaultScope" + }; + if (_defaults) jsPlumb.extend(this.Defaults, _defaults); + + this.logEnabled = this.Defaults.LogEnabled; + this._connectionTypes = {}; + this._endpointTypes = {}; + + jsPlumbUtil.EventGenerator.apply(this); + + var _currentInstance = this, + _instanceIndex = getInstanceIndex(), + _bb = _currentInstance.bind, + _initialDefaults = {}, + _zoom = 1, + _info = function(el) { + var _el = _dom(el); + return { el:_el, id:(jsPlumbUtil.isString(el) && _el == null) ? el : _getId(_el) }; + }; + + this.getInstanceIndex = function() { return _instanceIndex; }; + + this.setZoom = function(z, repaintEverything) { + _zoom = z; + if (repaintEverything) _currentInstance.repaintEverything(); + }; + this.getZoom = function() { return _zoom; }; + + for (var i in this.Defaults) + _initialDefaults[i] = this.Defaults[i]; + + this.bind = function(event, fn) { + if ("ready" === event && initialized) fn(); + else _bb.apply(_currentInstance,[event, fn]); + }; + + _currentInstance.importDefaults = function(d) { + for (var i in d) { + _currentInstance.Defaults[i] = d[i]; + } + return _currentInstance; + }; + + _currentInstance.restoreDefaults = function() { + _currentInstance.Defaults = jsPlumb.extend({}, _initialDefaults); + return _currentInstance; + }; + + var log = null, + resizeTimer = null, + initialized = false, + // TODO remove from window scope + connections = [], + // map of element id -> endpoint lists. an element can have an arbitrary + // number of endpoints on it, and not all of them have to be connected + // to anything. + endpointsByElement = {}, + endpointsByUUID = {}, + offsets = {}, + offsetTimestamps = {}, + floatingConnections = {}, + draggableStates = {}, + connectionBeingDragged = false, + sizes = [], + _suspendDrawing = false, + _suspendedAt = null, + DEFAULT_SCOPE = this.Defaults.Scope, + renderMode = null, // will be set in init() + _curIdStamp = 1, + _idstamp = function() { return "" + _curIdStamp++; }, + + // + // appends an element to some other element, which is calculated as follows: + // + // 1. if _currentInstance.Defaults.Container exists, use that element. + // 2. if the 'parent' parameter exists, use that. + // 3. otherwise just use the root element (for DOM usage, the document body). + // + // + _appendElement = function(el, parent) { + if (_currentInstance.Defaults.Container) + jsPlumb.CurrentLibrary.appendElement(el, _currentInstance.Defaults.Container); + else if (!parent) + jsPlumbAdapter.appendToRoot(el); + else + jsPlumb.CurrentLibrary.appendElement(el, parent); + }, + + // + // YUI, for some reason, put the result of a Y.all call into an object that contains + // a '_nodes' array, instead of handing back an array-like object like the other + // libraries do. + // + _convertYUICollection = function(c) { + return c._nodes ? c._nodes : c; + }, + + // + // Draws an endpoint and its connections. this is the main entry point into drawing connections as well + // as endpoints, since jsPlumb is endpoint-centric under the hood. + // + // @param element element to draw (of type library specific element object) + // @param ui UI object from current library's event system. optional. + // @param timestamp timestamp for this paint cycle. used to speed things up a little by cutting down the amount of offset calculations we do. + // @param clearEdits defaults to false; indicates that mouse edits for connectors should be cleared + /// + _draw = function(element, ui, timestamp, clearEdits) { + + // TODO is it correct to filter by headless at this top level? how would a headless adapter ever repaint? + if (!jsPlumbAdapter.headless && !_suspendDrawing) { + var id = _getId(element), + repaintEls = _currentInstance.dragManager.getElementsForDraggable(id); + + if (timestamp == null) timestamp = _timestamp(); + + // update the offset of everything _before_ we try to draw anything. + var o = _updateOffset( { elId : id, offset : ui, recalc : false, timestamp : timestamp }); + + if (repaintEls) { + for (var i in repaintEls) { + // TODO this seems to cause a lag, but we provide the offset, so in theory it + // should not. is the timestamp failing? + _updateOffset( { + elId : repaintEls[i].id, + offset : { + left:o.o.left + repaintEls[i].offset.left, + top:o.o.top + repaintEls[i].offset.top + }, + recalc : false, + timestamp : timestamp + }); + } + } + + + _currentInstance.anchorManager.redraw(id, ui, timestamp, null, clearEdits); + + if (repaintEls) { + for (var j in repaintEls) { + _currentInstance.anchorManager.redraw(repaintEls[j].id, ui, timestamp, repaintEls[j].offset, clearEdits, true); + } + } + } + }, + + // + // executes the given function against the given element if the first + // argument is an object, or the list of elements, if the first argument + // is a list. the function passed in takes (element, elementId) as + // arguments. + // + _elementProxy = function(element, fn) { + var retVal = null, el, id; + if (_ju.isArray(element)) { + retVal = []; + for ( var i = 0, j = element.length; i < j; i++) { + el = _gel(element[i]); + id = _currentInstance.getAttribute(el, "id"); + retVal.push(fn(el, id)); // append return values to what we will return + } + } else { + el = _gel(element); + id = _currentInstance.getAttribute(el, "id"); + retVal = fn(el, id); + } + return retVal; + }, + + // + // gets an Endpoint by uuid. + // + _getEndpoint = function(uuid) { return endpointsByUUID[uuid]; }, + + /** + * inits a draggable if it's not already initialised. + * TODO: somehow abstract this to the adapter, because the concept of "draggable" has no + * place on the server. + */ + _initDraggableIfNecessary = function(element, isDraggable, dragOptions) { + // TODO move to DragManager? + if (!jsPlumbAdapter.headless) { + var _draggable = isDraggable == null ? false : isDraggable, jpcl = jsPlumb.CurrentLibrary; + if (_draggable) { + if (jpcl.isDragSupported(element) && !jpcl.isAlreadyDraggable(element)) { + var options = dragOptions || _currentInstance.Defaults.DragOptions || jsPlumb.Defaults.DragOptions; + options = jsPlumb.extend( {}, options); // make a copy. + var dragEvent = jpcl.dragEvents.drag, + stopEvent = jpcl.dragEvents.stop, + startEvent = jpcl.dragEvents.start; + + options[startEvent] = _ju.wrap(options[startEvent], function() { + _currentInstance.setHoverSuspended(true); + _currentInstance.select({source:element}).addClass(_currentInstance.elementDraggingClass + " " + _currentInstance.sourceElementDraggingClass, true); + _currentInstance.select({target:element}).addClass(_currentInstance.elementDraggingClass + " " + _currentInstance.targetElementDraggingClass, true); + _currentInstance.setConnectionBeingDragged(true); + }); + + options[dragEvent] = _ju.wrap(options[dragEvent], function() { + var ui = jpcl.getUIPosition(arguments, _currentInstance.getZoom()); + _draw(element, ui, null, true); + _addClass(element, "jsPlumb_dragged"); + }); + options[stopEvent] = _ju.wrap(options[stopEvent], function() { + var ui = jpcl.getUIPosition(arguments, _currentInstance.getZoom()); + _draw(element, ui); + _removeClass(element, "jsPlumb_dragged"); + _currentInstance.setHoverSuspended(false); + _currentInstance.select({source:element}).removeClass(_currentInstance.elementDraggingClass + " " + _currentInstance.sourceElementDraggingClass, true); + _currentInstance.select({target:element}).removeClass(_currentInstance.elementDraggingClass + " " + _currentInstance.targetElementDraggingClass, true); + _currentInstance.setConnectionBeingDragged(false); + _currentInstance.dragManager.dragEnded(element); + }); + var elId = _getId(element); // need ID + draggableStates[elId] = true; + var draggable = draggableStates[elId]; + options.disabled = draggable == null ? false : !draggable; + jpcl.initDraggable(element, options, false, _currentInstance); + _currentInstance.dragManager.register(element); + } + } + } + }, + + /* + * prepares a final params object that can be passed to _newConnection, taking into account defaults, events, etc. + */ + _prepareConnectionParams = function(params, referenceParams) { + var _p = jsPlumb.extend( { }, params); + if (referenceParams) jsPlumb.extend(_p, referenceParams); + + // hotwire endpoints passed as source or target to sourceEndpoint/targetEndpoint, respectively. + if (_p.source) { + if (_p.source.endpoint) + _p.sourceEndpoint = _p.source; + else + _p.source = _dom(_p.source); + } + if (_p.target) { + if (_p.target.endpoint) + _p.targetEndpoint = _p.target; + else + _p.target = _dom(_p.target); + } + + // test for endpoint uuids to connect + if (params.uuids) { + _p.sourceEndpoint = _getEndpoint(params.uuids[0]); + _p.targetEndpoint = _getEndpoint(params.uuids[1]); + } + + // now ensure that if we do have Endpoints already, they're not full. + // source: + if (_p.sourceEndpoint && _p.sourceEndpoint.isFull()) { + _ju.log(_currentInstance, "could not add connection; source endpoint is full"); + return; + } + + // target: + if (_p.targetEndpoint && _p.targetEndpoint.isFull()) { + _ju.log(_currentInstance, "could not add connection; target endpoint is full"); + return; + } + + // if source endpoint mandates connection type and nothing specified in our params, use it. + if (!_p.type && _p.sourceEndpoint) + _p.type = _p.sourceEndpoint.connectionType; + + // copy in any connectorOverlays that were specified on the source endpoint. + // it doesnt copy target endpoint overlays. i'm not sure if we want it to or not. + if (_p.sourceEndpoint && _p.sourceEndpoint.connectorOverlays) { + _p.overlays = _p.overlays || []; + for (var i = 0, j = _p.sourceEndpoint.connectorOverlays.length; i < j; i++) { + _p.overlays.push(_p.sourceEndpoint.connectorOverlays[i]); + } + } + + // pointer events + if (!_p["pointer-events"] && _p.sourceEndpoint && _p.sourceEndpoint.connectorPointerEvents) + _p["pointer-events"] = _p.sourceEndpoint.connectorPointerEvents; + + // if there's a target specified (which of course there should be), and there is no + // target endpoint specified, and 'newConnection' was not set to true, then we check to + // see if a prior call to makeTarget has provided us with the specs for the target endpoint, and + // we use those if so. additionally, if the makeTarget call was specified with 'uniqueEndpoint' set + // to true, then if that target endpoint has already been created, we re-use it. + + var tid, tep, existingUniqueEndpoint, newEndpoint; + + // TODO: this code can be refactored to be a little dry. + if (_p.target && !_p.target.endpoint && !_p.targetEndpoint && !_p.newConnection) { + tid = _getId(_p.target); + tep =_targetEndpointDefinitions[tid]; + existingUniqueEndpoint = _targetEndpoints[tid]; + + if (tep) { + // if target not enabled, return. + if (!_targetsEnabled[tid]) return; + + // TODO this is dubious. i think it is there so that the endpoint can subsequently + // be dragged (ie it kicks off the draggable registration). but it is dubious. + tep.isTarget = true; + + // check for max connections?? + newEndpoint = existingUniqueEndpoint != null ? existingUniqueEndpoint : _currentInstance.addEndpoint(_p.target, tep); + if (_targetEndpointsUnique[tid]) _targetEndpoints[tid] = newEndpoint; + _p.targetEndpoint = newEndpoint; + // TODO test options to makeTarget to see if we should do this? + newEndpoint._doNotDeleteOnDetach = false; // reset. + newEndpoint._deleteOnDetach = true; + } + } + + // same thing, but for source. + if (_p.source && !_p.source.endpoint && !_p.sourceEndpoint && !_p.newConnection) { + tid = _getId(_p.source); + tep = _sourceEndpointDefinitions[tid]; + existingUniqueEndpoint = _sourceEndpoints[tid]; + + if (tep) { + // if source not enabled, return. + if (!_sourcesEnabled[tid]) return; + + // TODO this is dubious. i think it is there so that the endpoint can subsequently + // be dragged (ie it kicks off the draggable registration). but it is dubious. + //tep.isSource = true; + + newEndpoint = existingUniqueEndpoint != null ? existingUniqueEndpoint : _currentInstance.addEndpoint(_p.source, tep); + if (_sourceEndpointsUnique[tid]) _sourceEndpoints[tid] = newEndpoint; + _p.sourceEndpoint = newEndpoint; + // TODO test options to makeSource to see if we should do this? + newEndpoint._doNotDeleteOnDetach = false; // reset. + newEndpoint._deleteOnDetach = true; + } + } + + return _p; + }, + + _newConnection = function(params) { + var connectionFunc = _currentInstance.Defaults.ConnectionType || _currentInstance.getDefaultConnectionType(), + endpointFunc = _currentInstance.Defaults.EndpointType || jsPlumb.Endpoint, + parent = jsPlumb.CurrentLibrary.getParent; + + if (params.container) + params.parent = params.container; + else { + if (params.sourceEndpoint) + params.parent = params.sourceEndpoint.parent; + else if (params.source.constructor == endpointFunc) + params.parent = params.source.parent; + else params.parent = parent(params.source); + } + + params._jsPlumb = _currentInstance; + params.newConnection = _newConnection; + params.newEndpoint = _newEndpoint; + params.endpointsByUUID = endpointsByUUID; + params.endpointsByElement = endpointsByElement; + params.finaliseConnection = _finaliseConnection; + var con = new connectionFunc(params); + con.id = "con_" + _idstamp(); + _eventFireProxy("click", "click", con); + _eventFireProxy("dblclick", "dblclick", con); + _eventFireProxy("contextmenu", "contextmenu", con); + + // if the connection is draggable, then maybe we need to tell the target endpoint to init the + // dragging code. it won't run again if it already configured to be draggable. + if (con.isDetachable()) { + con.endpoints[0].initDraggable(); + con.endpoints[1].initDraggable(); + } + + return con; + }, + + // + // adds the connection to the backing model, fires an event if necessary and then redraws + // + _finaliseConnection = function(jpc, params, originalEvent, doInformAnchorManager) { + params = params || {}; + // add to list of connections (by scope). + if (!jpc.suspendedEndpoint) + connections.push(jpc); + + // always inform the anchor manager + // except that if jpc has a suspended endpoint it's not true to say the + // connection is new; it has just (possibly) moved. the question is whether + // to make that call here or in the anchor manager. i think perhaps here. + if (jpc.suspendedEndpoint == null || doInformAnchorManager) + _currentInstance.anchorManager.newConnection(jpc); + + // force a paint + _draw(jpc.source); + + // fire an event + if (!params.doNotFireConnectionEvent && params.fireEvent !== false) { + + var eventArgs = { + connection:jpc, + source : jpc.source, target : jpc.target, + sourceId : jpc.sourceId, targetId : jpc.targetId, + sourceEndpoint : jpc.endpoints[0], targetEndpoint : jpc.endpoints[1] + }; + + _currentInstance.fire("connection", eventArgs, originalEvent); + } + }, + + _eventFireProxy = function(event, proxyEvent, obj) { + obj.bind(event, function(originalObject, originalEvent) { + _currentInstance.fire(proxyEvent, obj, originalEvent); + }); + }, + + /* + * for the given endpoint params, returns an appropriate parent element for the UI elements that will be added. + * this function is used by _newEndpoint (directly below), and also in the makeSource function in jsPlumb. + * + * the logic is to first look for a "container" member of params, and pass that back if found. otherwise we + * handoff to the 'getParent' function in the current library. + */ + _getParentFromParams = function(params) { + if (params.container) + return params.container; + else { + var tag = jsPlumb.CurrentLibrary.getTagName(params.source), + p = jsPlumb.CurrentLibrary.getParent(params.source); + if (tag && tag.toLowerCase() === "td") + return jsPlumb.CurrentLibrary.getParent(p); + else return p; + } + }, + + /* + factory method to prepare a new endpoint. this should always be used instead of creating Endpoints + manually, since this method attaches event listeners and an id. + */ + _newEndpoint = function(params) { + var endpointFunc = _currentInstance.Defaults.EndpointType || jsPlumb.Endpoint; + var _p = jsPlumb.extend({}, params); + _p.parent = _getParentFromParams(_p); + _p._jsPlumb = _currentInstance; + _p.newConnection = _newConnection; + _p.newEndpoint = _newEndpoint; + _p.endpointsByUUID = endpointsByUUID; + _p.endpointsByElement = endpointsByElement; + _p.finaliseConnection = _finaliseConnection; + _p.fireDetachEvent = fireDetachEvent; + _p.fireMoveEvent = fireMoveEvent; + _p.floatingConnections = floatingConnections; + _p.getParentFromParams = _getParentFromParams; + _p.elementId = _getId(_p.source); + var ep = new endpointFunc(_p); + ep.id = "ep_" + _idstamp(); + _eventFireProxy("click", "endpointClick", ep); + _eventFireProxy("dblclick", "endpointDblClick", ep); + _eventFireProxy("contextmenu", "contextmenu", ep); + if (!jsPlumbAdapter.headless) + _currentInstance.dragManager.endpointAdded(_p.source); + return ep; + }, + + /* + * performs the given function operation on all the connections found + * for the given element id; this means we find all the endpoints for + * the given element, and then for each endpoint find the connectors + * connected to it. then we pass each connection in to the given + * function. + */ + _operation = function(elId, func, endpointFunc) { + var endpoints = endpointsByElement[elId]; + if (endpoints && endpoints.length) { + for ( var i = 0, ii = endpoints.length; i < ii; i++) { + for ( var j = 0, jj = endpoints[i].connections.length; j < jj; j++) { + var retVal = func(endpoints[i].connections[j]); + // if the function passed in returns true, we exit. + // most functions return false. + if (retVal) return; + } + if (endpointFunc) endpointFunc(endpoints[i]); + } + } + }, + + _setDraggable = function(element, draggable) { + return _elementProxy(element, function(el, id) { + draggableStates[id] = draggable; + if (jsPlumb.CurrentLibrary.isDragSupported(el)) { + jsPlumb.CurrentLibrary.setDraggable(el, draggable); + } + }); + }, + /* + * private method to do the business of hiding/showing. + * + * @param el + * either Id of the element in question or a library specific + * object for the element. + * @param state + * String specifying a value for the css 'display' property + * ('block' or 'none'). + */ + _setVisible = function(el, state, alsoChangeEndpoints) { + state = state === "block"; + var endpointFunc = null; + if (alsoChangeEndpoints) { + if (state) endpointFunc = function(ep) { + ep.setVisible(true, true, true); + }; + else endpointFunc = function(ep) { + ep.setVisible(false, true, true); + }; + } + var info = _info(el); + _operation(info.id, function(jpc) { + if (state && alsoChangeEndpoints) { + // this test is necessary because this functionality is new, and i wanted to maintain backwards compatibility. + // this block will only set a connection to be visible if the other endpoint in the connection is also visible. + var oidx = jpc.sourceId === info.id ? 1 : 0; + if (jpc.endpoints[oidx].isVisible()) jpc.setVisible(true); + } + else // the default behaviour for show, and what always happens for hide, is to just set the visibility without getting clever. + jpc.setVisible(state); + }, endpointFunc); + }, + /* + * toggles the draggable state of the given element(s). + * el is either an id, or an element object, or a list of ids/element objects. + */ + _toggleDraggable = function(el) { + return _elementProxy(el, function(el, elId) { + var state = draggableStates[elId] == null ? false : draggableStates[elId]; + state = !state; + draggableStates[elId] = state; + jsPlumb.CurrentLibrary.setDraggable(el, state); + return state; + }); + }, + /** + * private method to do the business of toggling hiding/showing. + */ + _toggleVisible = function(elId, changeEndpoints) { + var endpointFunc = null; + if (changeEndpoints) { + endpointFunc = function(ep) { + var state = ep.isVisible(); + ep.setVisible(!state); + }; + } + _operation(elId, function(jpc) { + var state = jpc.isVisible(); + jpc.setVisible(!state); + }, endpointFunc); + // todo this should call _elementProxy, and pass in the + // _operation(elId, f) call as a function. cos _toggleDraggable does + // that. + }, + /** + * updates the offset and size for a given element, and stores the + * values. if 'offset' is not null we use that (it would have been + * passed in from a drag call) because it's faster; but if it is null, + * or if 'recalc' is true in order to force a recalculation, we get the current values. + */ + _updateOffset = function(params) { + var timestamp = params.timestamp, recalc = params.recalc, offset = params.offset, elId = params.elId, s; + if (_suspendDrawing && !timestamp) timestamp = _suspendedAt; + if (!recalc) { + if (timestamp && timestamp === offsetTimestamps[elId]) { + return {o:params.offset || offsets[elId], s:sizes[elId]}; + } + } + if (recalc || !offset) { // if forced repaint or no offset available, we recalculate. + // get the current size and offset, and store them + s = _gel(elId); + if (s != null) { + sizes[elId] = _getSize(s); + offsets[elId] = _getOffset(s, _currentInstance); + offsetTimestamps[elId] = timestamp; + } + } else { + offsets[elId] = offset; + if (sizes[elId] == null) { + s = _gel(elId); + if (s != null) sizes[elId] = _getSize(s); + } + offsetTimestamps[elId] = timestamp; + } + + if(offsets[elId] && !offsets[elId].right) { + offsets[elId].right = offsets[elId].left + sizes[elId][0]; + offsets[elId].bottom = offsets[elId].top + sizes[elId][1]; + offsets[elId].width = sizes[elId][0]; + offsets[elId].height = sizes[elId][1]; + offsets[elId].centerx = offsets[elId].left + (offsets[elId].width / 2); + offsets[elId].centery = offsets[elId].top + (offsets[elId].height / 2); + } + return {o:offsets[elId], s:sizes[elId]}; + }, + + // TODO comparison performance + _getCachedData = function(elId) { + var o = offsets[elId]; + if (!o) + return _updateOffset({elId:elId}); + else + return {o:o, s:sizes[elId]}; + }, + + /** + * gets an id for the given element, creating and setting one if + * necessary. the id is of the form + * + * jsPlumb__ + * + * where "index in instance" is a monotonically increasing integer that starts at 0, + * for each instance. this method is used not only to assign ids to elements that do not + * have them but also to connections and endpoints. + */ + _getId = function(element, uuid, doNotCreateIfNotFound) { + if (jsPlumbUtil.isString(element)) return element; + if (element == null) return null; + var id = jsPlumbAdapter.getAttribute(element, "id"); + if (!id || id === "undefined") { + // check if fixed uuid parameter is given + if (arguments.length == 2 && arguments[1] !== undefined) + id = uuid; + else if (arguments.length == 1 || (arguments.length == 3 && !arguments[2])) + id = "jsPlumb_" + _instanceIndex + "_" + _idstamp(); + + if (!doNotCreateIfNotFound) jsPlumbAdapter.setAttribute(element, "id", id); + } + return id; + }; + + this.setConnectionBeingDragged = function(v) { + connectionBeingDragged = v; + }; + this.isConnectionBeingDragged = function() { + return connectionBeingDragged; + }; + + this.connectorClass = "_jsPlumb_connector"; + this.hoverClass = "_jsPlumb_hover"; + this.endpointClass = "_jsPlumb_endpoint"; + this.endpointConnectedClass = "_jsPlumb_endpoint_connected"; + this.endpointFullClass = "_jsPlumb_endpoint_full"; + this.endpointDropAllowedClass = "_jsPlumb_endpoint_drop_allowed"; + this.endpointDropForbiddenClass = "_jsPlumb_endpoint_drop_forbidden"; + this.overlayClass = "_jsPlumb_overlay"; + this.draggingClass = "_jsPlumb_dragging"; + this.elementDraggingClass = "_jsPlumb_element_dragging"; + this.sourceElementDraggingClass = "_jsPlumb_source_element_dragging"; + this.targetElementDraggingClass = "_jsPlumb_target_element_dragging"; + this.endpointAnchorClassPrefix = "_jsPlumb_endpoint_anchor"; + this.hoverSourceClass = "_jsPlumb_source_hover"; + this.hoverTargetClass = "_jsPlumb_target_hover"; + this.dragSelectClass = "_jsPlumb_drag_select"; + + this.Anchors = {}; + this.Connectors = { "canvas":{}, "svg":{}, "vml":{} }; + this.Endpoints = { "canvas":{}, "svg":{}, "vml":{} }; + this.Overlays = { "canvas":{}, "svg":{}, "vml":{}}; + this.ConnectorRenderers = {}; + this.SVG = "svg"; + this.CANVAS = "canvas"; + this.VML = "vml"; + + +// --------------------------- jsPLumbInstance public API --------------------------------------------------------- + + + this.addEndpoint = function(el, params, referenceParams) { + referenceParams = referenceParams || {}; + var p = jsPlumb.extend({}, referenceParams); + jsPlumb.extend(p, params); + p.endpoint = p.endpoint || _currentInstance.Defaults.Endpoint || jsPlumb.Defaults.Endpoint; + p.paintStyle = p.paintStyle || _currentInstance.Defaults.EndpointStyle || jsPlumb.Defaults.EndpointStyle; + // YUI wrapper + el = _convertYUICollection(el); + + var results = [], + inputs = (_ju.isArray(el) || (el.length != null && !_ju.isString(el))) ? el : [ el ]; + + for (var i = 0, j = inputs.length; i < j; i++) { + var _el = _dom(inputs[i]), id = _getId(_el); + p.source = _el; + + _updateOffset({ elId : id, timestamp:_suspendedAt }); + var e = _newEndpoint(p); + if (p.parentAnchor) e.parentAnchor = p.parentAnchor; + _ju.addToList(endpointsByElement, id, e); + var myOffset = offsets[id], + myWH = sizes[id], + anchorLoc = e.anchor.compute( { xy : [ myOffset.left, myOffset.top ], wh : myWH, element : e, timestamp:_suspendedAt }), + endpointPaintParams = { anchorLoc : anchorLoc, timestamp:_suspendedAt }; + + if (_suspendDrawing) endpointPaintParams.recalc = false; + if (!_suspendDrawing) e.paint(endpointPaintParams); + + results.push(e); + e._doNotDeleteOnDetach = true; // mark this as being added via addEndpoint. + } + + return results.length == 1 ? results[0] : results; + }; + + + this.addEndpoints = function(el, endpoints, referenceParams) { + var results = []; + for ( var i = 0, j = endpoints.length; i < j; i++) { + var e = _currentInstance.addEndpoint(el, endpoints[i], referenceParams); + if (_ju.isArray(e)) + Array.prototype.push.apply(results, e); + else results.push(e); + } + return results; + }; + + this.animate = function(el, properties, options) { + options = options || {}; + var ele = _gel(el), + id = _getId(el), + stepFunction = jsPlumb.CurrentLibrary.dragEvents.step, + completeFunction = jsPlumb.CurrentLibrary.dragEvents.complete; + + options[stepFunction] = _ju.wrap(options[stepFunction], function() { + _currentInstance.repaint(id); + }); + + // onComplete repaints, just to make sure everything looks good at the end of the animation. + options[completeFunction] = _ju.wrap(options[completeFunction], function() { + _currentInstance.repaint(id); + }); + + jsPlumb.CurrentLibrary.animate(ele, properties, options); + }; + + /** + * checks for a listener for the given condition, executing it if found, passing in the given value. + * condition listeners would have been attached using "bind" (which is, you could argue, now overloaded, since + * firing click events etc is a bit different to what this does). i thought about adding a "bindCondition" + * or something, but decided against it, for the sake of simplicity. jsPlumb will never fire one of these + * condition events anyway. + */ + this.checkCondition = function(conditionName, value) { + var l = _currentInstance.getListener(conditionName), + r = true; + + if (l && l.length > 0) { + try { + for (var i = 0, j = l.length; i < j; i++) { + r = r && l[i](value); + } + } + catch (e) { + _ju.log(_currentInstance, "cannot check condition [" + conditionName + "]" + e); + } + } + return r; + }; + + /** + * checks a condition asynchronously: fires the event handler and passes the handler + * a 'proceed' function and a 'stop' function. The handler MUST execute one or other + * of these once it has made up its mind. + * + * Note that although this reads the listener list for the given condition, it + * does not loop through and hit each listener, because that, with asynchronous + * callbacks, would be messy. so it uses only the first listener registered. + */ + this.checkASyncCondition = function(conditionName, value, proceed, stop) { + var l = _currentInstance.getListener(conditionName); + + if (l && l.length > 0) { + try { + l[0](value, proceed, stop); + } + catch (e) { + _ju.log(_currentInstance, "cannot asynchronously check condition [" + conditionName + "]" + e); + } + } + }; + + + this.connect = function(params, referenceParams) { + // prepare a final set of parameters to create connection with + var _p = _prepareConnectionParams(params, referenceParams), jpc; + // TODO probably a nicer return value if the connection was not made. _prepareConnectionParams + // will return null (and log something) if either endpoint was full. what would be nicer is to + // create a dedicated 'error' object. + if (_p) { + // create the connection. it is not yet registered + jpc = _newConnection(_p); + // now add it the model, fire an event, and redraw + _finaliseConnection(jpc, _p); + } + return jpc; + }; + + this.deleteEndpoint = function(object, doNotRepaintAfterwards) { + var _is = _currentInstance.setSuspendDrawing(true); + var endpoint = (typeof object == "string") ? endpointsByUUID[object] : object; + if (endpoint) { + _currentInstance.deleteObject({ + endpoint:endpoint + }); + } + if(!_is) _currentInstance.setSuspendDrawing(false, doNotRepaintAfterwards); + return _currentInstance; + }; + + this.deleteEveryEndpoint = function() { + var _is = _currentInstance.setSuspendDrawing(true); + for ( var id in endpointsByElement) { + var endpoints = endpointsByElement[id]; + if (endpoints && endpoints.length) { + for ( var i = 0, j = endpoints.length; i < j; i++) { + _currentInstance.deleteEndpoint(endpoints[i], true); + } + } + } + endpointsByElement = {}; + endpointsByUUID = {}; + _currentInstance.anchorManager.reset(); + _currentInstance.dragManager.reset(); + if(!_is) _currentInstance.setSuspendDrawing(false); + return _currentInstance; + }; + + var fireDetachEvent = function(jpc, doFireEvent, originalEvent) { + // may have been given a connection, or in special cases, an object + var connType = _currentInstance.Defaults.ConnectionType || _currentInstance.getDefaultConnectionType(), + argIsConnection = jpc.constructor == connType, + params = argIsConnection ? { + connection:jpc, + source : jpc.source, target : jpc.target, + sourceId : jpc.sourceId, targetId : jpc.targetId, + sourceEndpoint : jpc.endpoints[0], targetEndpoint : jpc.endpoints[1] + } : jpc; + + if (doFireEvent) + _currentInstance.fire("connectionDetached", params, originalEvent); + + _currentInstance.anchorManager.connectionDetached(params); + }; + + var fireMoveEvent = function(params, evt) { + _currentInstance.fire("connectionMoved", params, evt); + }; + + this.unregisterEndpoint = function(endpoint) { + if (endpoint._jsPlumb.uuid) endpointsByUUID[endpoint._jsPlumb.uuid] = null; + _currentInstance.anchorManager.deleteEndpoint(endpoint); + // TODO at least replace this with a removeWithFunction call. + for (var e in endpointsByElement) { + var endpoints = endpointsByElement[e]; + if (endpoints) { + var newEndpoints = []; + for (var i = 0, j = endpoints.length; i < j; i++) + if (endpoints[i] != endpoint) newEndpoints.push(endpoints[i]); + + endpointsByElement[e] = newEndpoints; + } + if(endpointsByElement[e].length <1){ + delete endpointsByElement[e]; + } + } + }; + + this.detach = function() { + + if (arguments.length === 0) return; + var connType = _currentInstance.Defaults.ConnectionType || _currentInstance.getDefaultConnectionType(), + firstArgIsConnection = arguments[0].constructor == connType, + params = arguments.length == 2 ? firstArgIsConnection ? (arguments[1] || {}) : arguments[0] : arguments[0], + fireEvent = (params.fireEvent !== false), + forceDetach = params.forceDetach, + conn = firstArgIsConnection ? arguments[0] : params.connection; + + if (conn) { + if (forceDetach || jsPlumbUtil.functionChain(true, false, [ + [ conn.endpoints[0], "isDetachAllowed", [ conn ] ], + [ conn.endpoints[1], "isDetachAllowed", [ conn ] ], + [ conn, "isDetachAllowed", [ conn ] ], + [ _currentInstance, "checkCondition", [ "beforeDetach", conn ] ] ])) { + + conn.endpoints[0].detach(conn, false, true, fireEvent); + } + } + else { + var _p = jsPlumb.extend( {}, params); // a backwards compatibility hack: source should be thought of as 'params' in this case. + // test for endpoint uuids to detach + if (_p.uuids) { + _getEndpoint(_p.uuids[0]).detachFrom(_getEndpoint(_p.uuids[1]), fireEvent); + } else if (_p.sourceEndpoint && _p.targetEndpoint) { + _p.sourceEndpoint.detachFrom(_p.targetEndpoint); + } else { + var sourceId = _getId(_dom(_p.source)), + targetId = _getId(_dom(_p.target)); + _operation(sourceId, function(jpc) { + if ((jpc.sourceId == sourceId && jpc.targetId == targetId) || (jpc.targetId == sourceId && jpc.sourceId == targetId)) { + if (_currentInstance.checkCondition("beforeDetach", jpc)) { + jpc.endpoints[0].detach(jpc, false, true, fireEvent); + } + } + }); + } + } + }; + + this.detachAllConnections = function(el, params) { + params = params || {}; + el = _dom(el); + var id = _getId(el), + endpoints = endpointsByElement[id]; + if (endpoints && endpoints.length) { + for ( var i = 0, j = endpoints.length; i < j; i++) { + endpoints[i].detachAll(params.fireEvent !== false); + } + } + return _currentInstance; + }; + + this.detachEveryConnection = function(params) { + params = params || {}; + _currentInstance.doWhileSuspended(function() { + for ( var id in endpointsByElement) { + var endpoints = endpointsByElement[id]; + if (endpoints && endpoints.length) { + for ( var i = 0, j = endpoints.length; i < j; i++) { + endpoints[i].detachAll(params.fireEvent !== false); + } + } + } + connections.splice(0); + }); + return _currentInstance; + }; + + /// not public. but of course its exposed. how to change this. + this.deleteObject = function(params) { + var result = { + endpoints : {}, + connections : {}, + endpointCount:0, + connectionCount:0 + }, + fireEvent = params.fireEvent !== false, + deleteAttachedObjects = params.deleteAttachedObjects !== false; + + var unravelConnection = function(connection) { + if(connection != null && result.connections[connection.id] == null) { + if (connection._jsPlumb != null) connection.setHover(false); + result.connections[connection.id] = connection; + result.connectionCount++; + if (deleteAttachedObjects) { + for (var j = 0; j < connection.endpoints.length; j++) { + if (connection.endpoints[j]._deleteOnDetach) + unravelEndpoint(connection.endpoints[j]); + } + } + } + }; + var unravelEndpoint = function(endpoint) { + if(endpoint != null && result.endpoints[endpoint.id] == null) { + if (endpoint._jsPlumb != null) endpoint.setHover(false); + result.endpoints[endpoint.id] = endpoint; + result.endpointCount++; + + if (deleteAttachedObjects) { + for (var i = 0; i < endpoint.connections.length; i++) { + var c = endpoint.connections[i]; + unravelConnection(c); + } + } + } + }; + + if (params.connection) + unravelConnection(params.connection); + else unravelEndpoint(params.endpoint); + + // loop through connections + for (var i in result.connections) { + var c = result.connections[i]; + c.endpoints[0].detachFromConnection(c); + c.endpoints[1].detachFromConnection(c); + //_currentInstance.unregisterConnection(c); + jsPlumbUtil.removeWithFunction(connections, function(_c) { + return c.id == _c.id; + }); + fireDetachEvent(c, fireEvent, params.originalEvent); + c.cleanup(); + c.destroy(); + } + + // loop through endpoints + for (var j in result.endpoints) { + var e = result.endpoints[j]; + _currentInstance.unregisterEndpoint(e); + // FIRE some endpoint deleted event? + e.cleanup(); + e.destroy(); + } + + return result; + }; + + this.draggable = function(el, options) { + var i,j,ele; + // allows for array or jquery/mootools selector + if (typeof el == 'object' && el.length) { + for (i = 0, j = el.length; i < j; i++) { + ele = _dom(el[i]); + if (ele) _initDraggableIfNecessary(ele, true, options); + } + } + // allows for YUI selector + else if (el._nodes) { // TODO this is YUI specific; really the logic should be forced + // into the library adapters (for jquery and mootools aswell) + for (i = 0, j = el._nodes.length; i < j; i++) { + ele = _dom(el._nodes[i]); + if (ele) _initDraggableIfNecessary(ele, true, options); + } + } + else { + ele = _dom(el); + if (ele) _initDraggableIfNecessary(ele, true, options); + } + return _currentInstance; + }; + + + // just a library-agnostic wrapper. + this.extend = function(o1, o2) { + return jsPlumb.CurrentLibrary.extend(o1, o2); + }; + + // helpers for select/selectEndpoints + var _setOperation = function(list, func, args, selector) { + for (var i = 0, j = list.length; i < j; i++) { + list[i][func].apply(list[i], args); + } + return selector(list); + }, + _getOperation = function(list, func, args) { + var out = []; + for (var i = 0, j = list.length; i < j; i++) { + out.push([ list[i][func].apply(list[i], args), list[i] ]); + } + return out; + }, + setter = function(list, func, selector) { + return function() { + return _setOperation(list, func, arguments, selector); + }; + }, + getter = function(list, func) { + return function() { + return _getOperation(list, func, arguments); + }; + }, + prepareList = function(input, doNotGetIds) { + var r = []; + if (input) { + if (typeof input == 'string') { + if (input === "*") return input; + r.push(input); + } + else { + input = _gel(input); + if (doNotGetIds) r = input; + else { + for (var i = 0, j = input.length; i < j; i++) + r.push(_info(input[i]).id); + } + } + } + return r; + }, + filterList = function(list, value, missingIsFalse) { + if (list === "*") return true; + return list.length > 0 ? jsPlumbUtil.indexOf(list, value) != -1 : !missingIsFalse; + }; + + // get some connections, specifying source/target/scope + this.getConnections = function(options, flat) { + if (!options) { + options = {}; + } else if (options.constructor == String) { + options = { "scope": options }; + } + var scope = options.scope || _currentInstance.getDefaultScope(), + scopes = prepareList(scope, true), + sources = prepareList(options.source), + targets = prepareList(options.target), + results = (!flat && scopes.length > 1) ? {} : [], + _addOne = function(scope, obj) { + if (!flat && scopes.length > 1) { + var ss = results[scope]; + if (ss == null) { + ss = results[scope] = []; + } + ss.push(obj); + } else results.push(obj); + }; + + for ( var j = 0, jj = connections.length; j < jj; j++) { + var c = connections[j]; + if (filterList(scopes, c.scope) && filterList(sources, c.sourceId) && filterList(targets, c.targetId)) + _addOne(c.scope, c); + } + + return results; + }; + + var _curryEach = function(list, executor) { + return function(f) { + for (var i = 0, ii = list.length; i < ii; i++) { + f(list[i]); + } + return executor(list); + }; + }, + _curryGet = function(list) { + return function(idx) { + return list[idx]; + }; + }; + + var _makeCommonSelectHandler = function(list, executor) { + var out = { + length:list.length, + each:_curryEach(list, executor), + get:_curryGet(list) + }, + setters = ["setHover", "removeAllOverlays", "setLabel", "addClass", "addOverlay", "removeOverlay", + "removeOverlays", "showOverlay", "hideOverlay", "showOverlays", "hideOverlays", "setPaintStyle", + "setHoverPaintStyle", "setSuspendEvents", "setParameter", "setParameters", "setVisible", + "repaint", "addType", "toggleType", "removeType", "removeClass", "setType", "bind", "unbind" ], + + getters = ["getLabel", "getOverlay", "isHover", "getParameter", "getParameters", "getPaintStyle", + "getHoverPaintStyle", "isVisible", "hasType", "getType", "isSuspendEvents" ], + i, ii; + + for (i = 0, ii = setters.length; i < ii; i++) + out[setters[i]] = setter(list, setters[i], executor); + + for (i = 0, ii = getters.length; i < ii; i++) + out[getters[i]] = getter(list, getters[i]); + + return out; + }; + + var _makeConnectionSelectHandler = function(list) { + var common = _makeCommonSelectHandler(list, _makeConnectionSelectHandler); + return jsPlumb.CurrentLibrary.extend(common, { + // setters + setDetachable:setter(list, "setDetachable", _makeConnectionSelectHandler), + setReattach:setter(list, "setReattach", _makeConnectionSelectHandler), + setConnector:setter(list, "setConnector", _makeConnectionSelectHandler), + detach:function() { + for (var i = 0, ii = list.length; i < ii; i++) + _currentInstance.detach(list[i]); + }, + // getters + isDetachable:getter(list, "isDetachable"), + isReattach:getter(list, "isReattach") + }); + }; + + var _makeEndpointSelectHandler = function(list) { + var common = _makeCommonSelectHandler(list, _makeEndpointSelectHandler); + return jsPlumb.CurrentLibrary.extend(common, { + setEnabled:setter(list, "setEnabled", _makeEndpointSelectHandler), + setAnchor:setter(list, "setAnchor", _makeEndpointSelectHandler), + isEnabled:getter(list, "isEnabled"), + detachAll:function() { + for (var i = 0, ii = list.length; i < ii; i++) + list[i].detachAll(); + }, + "remove":function() { + for (var i = 0, ii = list.length; i < ii; i++) + _currentInstance.deleteObject({endpoint:list[i]}); + } + }); + }; + + + this.select = function(params) { + params = params || {}; + params.scope = params.scope || "*"; + return _makeConnectionSelectHandler(params.connections || _currentInstance.getConnections(params, true)); + }; + + this.selectEndpoints = function(params) { + params = params || {}; + params.scope = params.scope || "*"; + var noElementFilters = !params.element && !params.source && !params.target, + elements = noElementFilters ? "*" : prepareList(params.element), + sources = noElementFilters ? "*" : prepareList(params.source), + targets = noElementFilters ? "*" : prepareList(params.target), + scopes = prepareList(params.scope, true); + + var ep = []; + + for (var el in endpointsByElement) { + var either = filterList(elements, el, true), + source = filterList(sources, el, true), + sourceMatchExact = sources != "*", + target = filterList(targets, el, true), + targetMatchExact = targets != "*"; + + // 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. + if ( either || source || target ) { + inner: + for (var i = 0, ii = endpointsByElement[el].length; i < ii; i++) { + var _ep = endpointsByElement[el][i]; + if (filterList(scopes, _ep.scope, true)) { + + var noMatchSource = (sourceMatchExact && sources.length > 0 && !_ep.isSource), + noMatchTarget = (targetMatchExact && targets.length > 0 && !_ep.isTarget); + + if (noMatchSource || noMatchTarget) + continue inner; + + ep.push(_ep); + } + } + } + } + + return _makeEndpointSelectHandler(ep); + }; + + // get all connections managed by the instance of jsplumb. + this.getAllConnections = function() { return connections; }; + this.getDefaultScope = function() { return DEFAULT_SCOPE; }; + // get an endpoint by uuid. + this.getEndpoint = _getEndpoint; + // get endpoints for some element. + this.getEndpoints = function(el) { return endpointsByElement[_info(el).id]; }; + // gets the default endpoint type. used when subclassing. see wiki. + this.getDefaultEndpointType = function() { return jsPlumb.Endpoint; }; + // gets the default connection type. used when subclassing. see wiki. + this.getDefaultConnectionType = function() { return jsPlumb.Connection; }; + /* + * Gets an element's id, creating one if necessary. really only exposed + * for the lib-specific functionality to access; would be better to pass + * the current instance into the lib-specific code (even though this is + * a static call. i just don't want to expose it to the public API). + */ + this.getId = _getId; + this.getOffset = function(id) { + var o = offsets[id]; + return _updateOffset({elId:id}); + }; + + this.getSelector = function() { + return jsPlumb.CurrentLibrary.getSelector.apply(null, arguments); + }; + + // get the size of the element with the given id, perhaps from cache. + this.getSize = function(id) { + var s = sizes[id]; + if (!s) _updateOffset({elId:id}); + return sizes[id]; + }; + + this.appendElement = _appendElement; + + var _hoverSuspended = false; + this.isHoverSuspended = function() { return _hoverSuspended; }; + this.setHoverSuspended = function(s) { _hoverSuspended = s; }; + + var _isAvailable = function(m) { + return function() { + return jsPlumbAdapter.isRenderModeAvailable(m); + }; + }; + this.isCanvasAvailable = _isAvailable("canvas"); + this.isSVGAvailable = _isAvailable("svg"); + this.isVMLAvailable = _isAvailable("vml"); + + // set an element's connections to be hidden + this.hide = function(el, changeEndpoints) { + _setVisible(el, "none", changeEndpoints); + return _currentInstance; + }; + + // exposed for other objects to use to get a unique id. + this.idstamp = _idstamp; + + this.connectorsInitialized = false; + var connectorTypes = [], rendererTypes = ["canvas", "svg", "vml"]; + this.registerConnectorType = function(connector, name) { + connectorTypes.push([connector, name]); + }; + + /** + * callback from the current library to tell us to prepare ourselves (attach + * mouse listeners etc; can't do that until the library has provided a bind method) + */ + this.init = function() { + var _oneType = function(renderer, name, fn) { + jsPlumb.Connectors[renderer][name] = function() { + fn.apply(this, arguments); + jsPlumb.ConnectorRenderers[renderer].apply(this, arguments); + }; + jsPlumbUtil.extend(jsPlumb.Connectors[renderer][name], [ fn, jsPlumb.ConnectorRenderers[renderer]]); + }; + + if (!jsPlumb.connectorsInitialized) { + for (var i = 0; i < connectorTypes.length; i++) { + for (var j = 0; j < rendererTypes.length; j++) { + _oneType(rendererTypes[j], connectorTypes[i][1], connectorTypes[i][0]); + } + + } + jsPlumb.connectorsInitialized = true; + } + + if (!initialized) { + _currentInstance.anchorManager = new jsPlumb.AnchorManager({jsPlumbInstance:_currentInstance}); + _currentInstance.setRenderMode(_currentInstance.Defaults.RenderMode); // calling the method forces the capability logic to be run. + initialized = true; + _currentInstance.fire("ready", _currentInstance); + } + }.bind(this); + + this.log = log; + this.jsPlumbUIComponent = jsPlumbUIComponent; + + /* + * Creates an anchor with the given params. + * + * + * Returns: The newly created Anchor. + * Throws: an error if a named anchor was not found. + */ + this.makeAnchor = function() { + var pp, _a = function(t, p) { + if (jsPlumb.Anchors[t]) return new jsPlumb.Anchors[t](p); + if (!_currentInstance.Defaults.DoNotThrowErrors) + throw { msg:"jsPlumb: unknown anchor type '" + t + "'" }; + }; + if (arguments.length === 0) return null; + var specimen = arguments[0], elementId = arguments[1], jsPlumbInstance = arguments[2], newAnchor = null; + // if it appears to be an anchor already... + if (specimen.compute && specimen.getOrientation) return specimen; //TODO hazy here about whether it should be added or is already added somehow. + // is it the name of an anchor type? + else if (typeof specimen == "string") { + newAnchor = _a(arguments[0], {elementId:elementId, jsPlumbInstance:_currentInstance}); + } + // is it an array? it will be one of: + // an array of [spec, params] - this defines a single anchor, which may be dynamic, but has parameters. + // an array of arrays - this defines some dynamic anchors + // an array of numbers - this defines a single anchor. + else if (_ju.isArray(specimen)) { + if (_ju.isArray(specimen[0]) || _ju.isString(specimen[0])) { + // if [spec, params] format + if (specimen.length == 2 && _ju.isObject(specimen[1])) { + // if first arg is a string, its a named anchor with params + if (_ju.isString(specimen[0])) { + pp = jsPlumb.extend({elementId:elementId, jsPlumbInstance:_currentInstance}, specimen[1]); + newAnchor = _a(specimen[0], pp); + } + // otherwise first arg is array, second is params. we treat as a dynamic anchor, which is fine + // even if the first arg has only one entry. you could argue all anchors should be implicitly dynamic in fact. + else { + pp = jsPlumb.extend({elementId:elementId, jsPlumbInstance:_currentInstance, anchors:specimen[0]}, specimen[1]); + newAnchor = new jsPlumb.DynamicAnchor(pp); + } + } + else + newAnchor = new jsPlumb.DynamicAnchor({anchors:specimen, selector:null, elementId:elementId, jsPlumbInstance:jsPlumbInstance}); + + } + else { + var anchorParams = { + x:specimen[0], y:specimen[1], + orientation : (specimen.length >= 4) ? [ specimen[2], specimen[3] ] : [0,0], + offsets : (specimen.length >= 6) ? [ specimen[4], specimen[5] ] : [ 0, 0 ], + elementId:elementId, + jsPlumbInstance:jsPlumbInstance, + cssClass:specimen.length == 7 ? specimen[6] : null + }; + newAnchor = new jsPlumb.Anchor(anchorParams); + newAnchor.clone = function() { return new jsPlumb.Anchor(anchorParams); }; + } + } + + if (!newAnchor.id) newAnchor.id = "anchor_" + _idstamp(); + return newAnchor; + }; + + /** + * makes a list of anchors from the given list of types or coords, eg + * ["TopCenter", "RightMiddle", "BottomCenter", [0, 1, -1, -1] ] + */ + this.makeAnchors = function(types, elementId, jsPlumbInstance) { + var r = []; + for ( var i = 0, ii = types.length; i < ii; i++) { + if (typeof types[i] == "string") + r.push(jsPlumb.Anchors[types[i]]({elementId:elementId, jsPlumbInstance:jsPlumbInstance})); + else if (_ju.isArray(types[i])) + r.push(_currentInstance.makeAnchor(types[i], elementId, jsPlumbInstance)); + } + return r; + }; + + /** + * Makes a dynamic anchor from the given list of anchors (which may be in shorthand notation as strings or dimension arrays, or Anchor + * objects themselves) and the given, optional, anchorSelector function (jsPlumb uses a default if this is not provided; most people will + * not need to provide this - i think). + */ + this.makeDynamicAnchor = function(anchors, anchorSelector) { + return new jsPlumb.DynamicAnchor({anchors:anchors, selector:anchorSelector, elementId:null, jsPlumbInstance:_currentInstance}); + }; + +// --------------------- makeSource/makeTarget ---------------------------------------------- + + var _targetEndpointDefinitions = {}, + _targetEndpoints = {}, + _targetEndpointsUnique = {}, + _targetMaxConnections = {}, + _setEndpointPaintStylesAndAnchor = function(ep, epIndex) { + ep.paintStyle = ep.paintStyle || + _currentInstance.Defaults.EndpointStyles[epIndex] || + _currentInstance.Defaults.EndpointStyle || + jsPlumb.Defaults.EndpointStyles[epIndex] || + jsPlumb.Defaults.EndpointStyle; + ep.hoverPaintStyle = ep.hoverPaintStyle || + _currentInstance.Defaults.EndpointHoverStyles[epIndex] || + _currentInstance.Defaults.EndpointHoverStyle || + jsPlumb.Defaults.EndpointHoverStyles[epIndex] || + jsPlumb.Defaults.EndpointHoverStyle; + + ep.anchor = ep.anchor || + _currentInstance.Defaults.Anchors[epIndex] || + _currentInstance.Defaults.Anchor || + jsPlumb.Defaults.Anchors[epIndex] || + jsPlumb.Defaults.Anchor; + + ep.endpoint = ep.endpoint || + _currentInstance.Defaults.Endpoints[epIndex] || + _currentInstance.Defaults.Endpoint || + jsPlumb.Defaults.Endpoints[epIndex] || + jsPlumb.Defaults.Endpoint; + }, + // TODO put all the source stuff inside one parent, keyed by id. + _sourceEndpointDefinitions = {}, + _sourceEndpoints = {}, + _sourceEndpointsUnique = {}, + _sourcesEnabled = {}, + _sourceTriggers = {}, + _sourceMaxConnections = {}, + _targetsEnabled = {}, + selectorFilter = function(evt, _el, selector) { + var t = evt.target || evt.srcElement, ok = false, + sel = _currentInstance.getSelector(_el, selector); + for (var j = 0; j < sel.length; j++) { + if (sel[j] == t) { + ok = true; + break; + } + } + return ok; + }; + + // see API docs + this.makeTarget = function(el, params, referenceParams) { + + // put jsplumb ref into params without altering the params passed in + var p = jsPlumb.extend({_jsPlumb:_currentInstance}, referenceParams); + jsPlumb.extend(p, params); + + // calculate appropriate paint styles and anchor from the params given + _setEndpointPaintStylesAndAnchor(p, 1); + + var jpcl = jsPlumb.CurrentLibrary, + targetScope = p.scope || _currentInstance.Defaults.Scope, + deleteEndpointsOnDetach = !(p.deleteEndpointsOnDetach === false), + maxConnections = p.maxConnections || -1, + onMaxConnections = p.onMaxConnections, + + _doOne = function(el) { + + // get the element's id and store the endpoint definition for it. jsPlumb.connect calls will look for one of these, + // and use the endpoint definition if found. + // decode the info for this element (id and element) + var elInfo = _info(el), + elid = elInfo.id, + proxyComponent = new jsPlumbUIComponent(p), + dropOptions = jsPlumb.extend({}, p.dropOptions || {}); + + // store the definitions keyed against the element id. + _targetEndpointDefinitions[elid] = p; + _targetEndpointsUnique[elid] = p.uniqueEndpoint; + _targetMaxConnections[elid] = maxConnections; + _targetsEnabled[elid] = true; + + var _drop = function() { + _currentInstance.currentlyDragging = false; + var originalEvent = jsPlumb.CurrentLibrary.getDropEvent(arguments), + targetCount = _currentInstance.select({target:elid}).length, + draggable = _gel(jpcl.getDragObject(arguments)), + id = _currentInstance.getAttribute(draggable, "dragId"), + scope = _currentInstance.getAttribute(draggable, "originalScope"), + jpc = floatingConnections[id], + idx = jpc.endpoints[0].isFloating() ? 0 : 1, + // this is not necessarily correct. if the source is being dragged, + // then the source endpoint is actually the currently suspended endpoint. + source = jpc.endpoints[0], + _endpoint = p.endpoint ? jsPlumb.extend({}, p.endpoint) : {}; + + if (!_targetsEnabled[elid] || _targetMaxConnections[elid] > 0 && targetCount >= _targetMaxConnections[elid]){ + if (onMaxConnections) { + // TODO here we still have the id of the floating element, not the + // actual target. + onMaxConnections({ + element:elInfo.el, + connection:jpc + }, originalEvent); + } + return false; + } + + // unlock the source anchor to allow it to refresh its position if necessary + source.anchor.locked = false; + + // restore the original scope if necessary (issue 57) + if (scope) jpcl.setDragScope(draggable, scope); + + // if no suspendedEndpoint and not pending, it is likely there was a drop on two + // elements that are on top of each other. abort. + if (jpc.suspendedEndpoint == null && !jpc.pending) + return false; + + // check if drop is allowed here. + // if the source is being dragged then in fact + // the source and target ids to pass into the drop interceptor are + // source - elid + // target - jpc's targetId + // + // otherwise the ids are + // source - jpc.sourceId + // target - elid + // + var _continue = proxyComponent.isDropAllowed(idx === 0 ? elid : jpc.sourceId, idx === 0 ? jpc.targetId : elid, jpc.scope, jpc, null); + + // reinstate any suspended endpoint; this just puts the connection back into + // a state in which it will report sensible values if someone asks it about + // its target. we're going to throw this connection away shortly so it doesnt matter + // if we manipulate it a bit. + if (jpc.suspendedEndpoint) { + jpc[idx ? "targetId" : "sourceId"] = jpc.suspendedEndpoint.elementId; + jpc[idx ? "target" : "source"] = jpc.suspendedEndpoint.element; + jpc.endpoints[idx] = jpc.suspendedEndpoint; + } + + if (_continue) { + + // make a new Endpoint for the target, or get it from the cache if uniqueEndpoint + // is set. + var _el = jpcl.getElementObject(elInfo.el), + newEndpoint = _targetEndpoints[elid]; + + // if no cached endpoint, or there was one but it has been cleaned up + // (ie. detached), then create a new one. + if (newEndpoint == null || newEndpoint._jsPlumb == null) + newEndpoint = _currentInstance.addEndpoint(_el, p); + + if (p.uniqueEndpoint) _targetEndpoints[elid] = newEndpoint; // may of course just store what it just pulled out. that's ok. + // TODO test options to makeTarget to see if we should do this? + newEndpoint._doNotDeleteOnDetach = false; // reset. + newEndpoint._deleteOnDetach = true; + + // if the anchor has a 'positionFinder' set, then delegate to that function to find + // out where to locate the anchor. + if (newEndpoint.anchor.positionFinder != null) { + var dropPosition = jpcl.getUIPosition(arguments, _currentInstance.getZoom()), + elPosition = _getOffset(_el, _currentInstance), + elSize = _getSize(_el), + ap = newEndpoint.anchor.positionFinder(dropPosition, elPosition, elSize, newEndpoint.anchor.constructorParams); + newEndpoint.anchor.x = ap[0]; + newEndpoint.anchor.y = ap[1]; + // now figure an orientation for it..kind of hard to know what to do actually. probably the best thing i can do is to + // support specifying an orientation in the anchor's spec. if one is not supplied then i will make the orientation + // be what will cause the most natural link to the source: it will be pointing at the source, but it needs to be + // specified in one axis only, and so how to make that choice? i think i will use whichever axis is the one in which + // the target is furthest away from the source. + } + + // change the target endpoint and target element information. really this should be + // done on a method on connection + jpc[idx ? "target" : "source"] = newEndpoint.element; + jpc[idx ? "targetId" : "sourceId"] = newEndpoint.elementId; + jpc.endpoints[idx].detachFromConnection(jpc); + if (jpc.endpoints[idx]._deleteOnDetach) + jpc.endpoints[idx].deleteAfterDragStop = true; // tell this endpoint to delet itself after drag stop. + // set new endpoint, and configure the settings for endpoints to delete on detach + newEndpoint.addConnection(jpc); + jpc.endpoints[idx] = newEndpoint; + jpc.deleteEndpointsOnDetach = deleteEndpointsOnDetach; + + // inform the anchor manager to update its target endpoint for this connection. + // TODO refactor to make this a single method. + if (idx == 1) + _currentInstance.anchorManager.updateOtherEndpoint(jpc.sourceId, jpc.suspendedElementId, jpc.targetId, jpc); + else + _currentInstance.anchorManager.sourceChanged(jpc.suspendedEndpoint.elementId, jpc.sourceId, jpc); + + _finaliseConnection(jpc, null, originalEvent); + jpc.pending = false; + + } + // if not allowed to drop... + else { + // TODO this code is identical (pretty much) to what happens when a connection + // dragged from a normal endpoint is in this situation. refactor. + // is this an existing connection, and will we reattach? + // TODO also this assumes the source needs to detach - is that always valid? + if (jpc.suspendedEndpoint) { + if (jpc.isReattach()) { + jpc.setHover(false); + jpc.floatingAnchorIndex = null; + jpc.suspendedEndpoint.addConnection(jpc); + _currentInstance.repaint(source.elementId); + } + else + source.detach(jpc, false, true, true, originalEvent); // otherwise, detach the connection and tell everyone about it. + } + + } + }; + + // wrap drop events as needed and initialise droppable + var dropEvent = jpcl.dragEvents.drop; + dropOptions.scope = dropOptions.scope || targetScope; + dropOptions[dropEvent] = _ju.wrap(dropOptions[dropEvent], _drop); + jpcl.initDroppable(_gel(elInfo.el), dropOptions, true); + }; + + // YUI collection fix + el = _convertYUICollection(el); + // make an array if only given one element + var inputs = el.length && el.constructor != String ? el : [ el ]; + + // register each one in the list. + for (var i = 0, ii = inputs.length; i < ii; i++) { + _doOne(inputs[i]); + } + + return _currentInstance; + }; + + // see api docs + this.unmakeTarget = function(el, doNotClearArrays) { + var info = _info(el); + + jsPlumb.CurrentLibrary.destroyDroppable(info.el); + // TODO this is not an exhaustive unmake of a target, since it does not remove the droppable stuff from + // the element. the effect will be to prevent it from behaving as a target, but it's not completely purged. + if (!doNotClearArrays) { + delete _targetEndpointDefinitions[info.id]; + delete _targetEndpointsUnique[info.id]; + delete _targetMaxConnections[info.id]; + delete _targetsEnabled[info.id]; + } + + return _currentInstance; + }; + + // see api docs + this.makeSource = function(el, params, referenceParams) { + var p = jsPlumb.extend({}, referenceParams); + jsPlumb.extend(p, params); + _setEndpointPaintStylesAndAnchor(p, 0); + var jpcl = jsPlumb.CurrentLibrary, + maxConnections = p.maxConnections || -1, + onMaxConnections = p.onMaxConnections, + _doOne = function(elInfo) { + // get the element's id and store the endpoint definition for it. jsPlumb.connect calls will look for one of these, + // and use the endpoint definition if found. + var elid = elInfo.id, + _el = _gel(elInfo.el), + parentElement = function() { + return p.parent == null ? null : p.parent === "parent" ? elInfo.el.parentNode : _dom(p.parent); + }, + idToRegisterAgainst = p.parent != null ? _currentInstance.getId(parentElement()) : elid; + + _sourceEndpointDefinitions[idToRegisterAgainst] = p; + _sourceEndpointsUnique[idToRegisterAgainst] = p.uniqueEndpoint; + _sourcesEnabled[idToRegisterAgainst] = true; + + var stopEvent = jpcl.dragEvents.stop, + dragEvent = jpcl.dragEvents.drag, + dragOptions = jsPlumb.extend({ }, p.dragOptions || {}), + existingDrag = dragOptions.drag, + existingStop = dragOptions.stop, + ep = null, + endpointAddedButNoDragYet = false; + + _sourceMaxConnections[idToRegisterAgainst] = maxConnections; + + // set scope if its not set in dragOptions but was passed in in params + dragOptions.scope = dragOptions.scope || p.scope; + + dragOptions[dragEvent] = _ju.wrap(dragOptions[dragEvent], function() { + if (existingDrag) existingDrag.apply(this, arguments); + endpointAddedButNoDragYet = false; + }); + + dragOptions[stopEvent] = _ju.wrap(dragOptions[stopEvent], function() { + + if (existingStop) existingStop.apply(this, arguments); + _currentInstance.currentlyDragging = false; + if (ep._jsPlumb != null) { // if not cleaned up... + + jpcl.unbind(ep.canvas, "mousedown"); + + // reset the anchor to the anchor that was initially provided. the one we were using to drag + // the connection was just a placeholder that was located at the place the user pressed the + // mouse button to initiate the drag. + var anchorDef = p.anchor || _currentInstance.Defaults.Anchor, + oldAnchor = ep.anchor, + oldConnection = ep.connections[0], + newAnchor = _currentInstance.makeAnchor(anchorDef, elid, _currentInstance), + _el = ep.element; + + // if the anchor has a 'positionFinder' set, then delegate to that function to find + // out where to locate the anchor. issue 117. + if (newAnchor.positionFinder != null) { + var elPosition = _getOffset(_el, _currentInstance), + elSize = _getSize(_el), + dropPosition = { left:elPosition.left + (oldAnchor.x * elSize[0]), top:elPosition.top + (oldAnchor.y * elSize[1]) }, + ap = newAnchor.positionFinder(dropPosition, elPosition, elSize, newAnchor.constructorParams); + + newAnchor.x = ap[0]; + newAnchor.y = ap[1]; + } + + ep.setAnchor(newAnchor, true); + + if (p.parent) { + var parent = parentElement(); + if (parent) { + var potentialParent = p.container || _currentInstance.Defaults.Container || jsPlumb.Defaults.Container; + ep.setElement(parent, potentialParent); + } + } + + ep.repaint(); + _currentInstance.repaint(ep.elementId); + _currentInstance.repaint(oldConnection.targetId); + } + }); + // when the user presses the mouse, add an Endpoint, if we are enabled. + var mouseDownListener = function(e) { + + // if disabled, return. + if (!_sourcesEnabled[idToRegisterAgainst]) return; + + // if a filter was given, run it, and return if it says no. + if (p.filter) { + var evt = jpcl.getOriginalEvent(e), + r = jsPlumbUtil.isString(p.filter) ? selectorFilter(evt, _el, p.filter) : p.filter(evt, _el); + + if (r === false) return; + } + + // if maxConnections reached + var sourceCount = _currentInstance.select({source:idToRegisterAgainst}).length; + if (_sourceMaxConnections[idToRegisterAgainst] >= 0 && sourceCount >= _sourceMaxConnections[idToRegisterAgainst]) { + if (onMaxConnections) { + onMaxConnections({ + element:_el, + maxConnections:maxConnections + }, e); + } + return false; + } + + // make sure we have the latest offset for this div + var myOffsetInfo = _updateOffset({elId:elid}).o, + z = _currentInstance.getZoom(), + x = ( ((e.pageX || e.page.x) / z) - myOffsetInfo.left) / myOffsetInfo.width, + y = ( ((e.pageY || e.page.y) / z) - myOffsetInfo.top) / myOffsetInfo.height, + parentX = x, + parentY = y; + + // if there is a parent, the endpoint will actually be added to it now, rather than the div + // that was the source. in that case, we have to adjust the anchor position so it refers to + // the parent. + if (p.parent) { + var pEl = parentElement(), pId = _getId(pEl); + myOffsetInfo = _updateOffset({elId:pId}).o; + parentX = ((e.pageX || e.page.x) - myOffsetInfo.left) / myOffsetInfo.width; + parentY = ((e.pageY || e.page.y) - myOffsetInfo.top) / myOffsetInfo.height; + } + + // we need to override the anchor in here, and force 'isSource', but we don't want to mess with + // the params passed in, because after a connection is established we're going to reset the endpoint + // to have the anchor we were given. + var tempEndpointParams = {}; + jsPlumb.extend(tempEndpointParams, p); + tempEndpointParams.isSource = true; + tempEndpointParams.anchor = [x,y,0,0]; + tempEndpointParams.parentAnchor = [ parentX, parentY, 0, 0 ]; + tempEndpointParams.dragOptions = dragOptions; + // if a parent was given we need to turn that into a "container" argument. this is, by default, + // the parent of the element we will move to, so parent of p.parent in this case. however, if + // the user has specified a 'container' on the endpoint definition or on + // the defaults, we should use that. + if (p.parent) { + var potentialParent = tempEndpointParams.container || _currentInstance.Defaults.Container || jsPlumb.Defaults.Container; + if (potentialParent) + tempEndpointParams.container = potentialParent; + else + tempEndpointParams.container = jsPlumb.CurrentLibrary.getParent(parentElement()); + } + + ep = _currentInstance.addEndpoint(elid, tempEndpointParams); + + endpointAddedButNoDragYet = true; + // we set this to prevent connections from firing attach events before this function has had a chance + // to move the endpoint. + ep.endpointWillMoveAfterConnection = p.parent != null; + ep.endpointWillMoveTo = p.parent ? parentElement() : null; + + // TODO test options to makeSource to see if we should do this? + ep._doNotDeleteOnDetach = false; // reset. + ep._deleteOnDetach = true; + + var _delTempEndpoint = function() { + // this mouseup event is fired only if no dragging occurred, by jquery and yui, but for mootools + // it is fired even if dragging has occurred, in which case we would blow away a perfectly + // legitimate endpoint, were it not for this check. the flag is set after adding an + // endpoint and cleared in a drag listener we set in the dragOptions above. + if(endpointAddedButNoDragYet) { + endpointAddedButNoDragYet = false; + _currentInstance.deleteEndpoint(ep); + } + }; + + _currentInstance.registerListener(ep.canvas, "mouseup", _delTempEndpoint); + _currentInstance.registerListener(_el, "mouseup", _delTempEndpoint); + + // and then trigger its mousedown event, which will kick off a drag, which will start dragging + // a new connection from this endpoint. + jpcl.trigger(ep.canvas, "mousedown", e); + + }; + + // register this on jsPlumb so that it can be cleared by a reset. + _currentInstance.registerListener(_el, "mousedown", mouseDownListener); + _sourceTriggers[elid] = mouseDownListener; + + // lastly, if a filter was provided, set it as a dragFilter on the element, + // to prevent the element drag function from kicking in when we want to + // drag a new connection + if (p.filter && jsPlumbUtil.isString(p.filter)) { + jpcl.setDragFilter(_el, p.filter); + } + }; + + el = _convertYUICollection(el); + + var inputs = el.length && el.constructor != String ? el : [ el ]; + + for (var i = 0, ii = inputs.length; i < ii; i++) { + _doOne(_info(inputs[i])); + } + + return _currentInstance; + }; + + // see api docs + this.unmakeSource = function(el, doNotClearArrays) { + var info = _info(el), + mouseDownListener = _sourceTriggers[info.id]; + + if (mouseDownListener) + _currentInstance.unregisterListener(info.el, "mousedown", mouseDownListener); + + if (!doNotClearArrays) { + delete _sourceEndpointDefinitions[info.id]; + delete _sourceEndpointsUnique[info.id]; + delete _sourcesEnabled[info.id]; + delete _sourceTriggers[info.id]; + delete _sourceMaxConnections[info.id]; + } + + return _currentInstance; + }; + + // see api docs + this.unmakeEverySource = function() { + for (var i in _sourcesEnabled) + _currentInstance.unmakeSource(i, true); + + _sourceEndpointDefinitions = {}; + _sourceEndpointsUnique = {}; + _sourcesEnabled = {}; + _sourceTriggers = {}; + }; + + // see api docs + this.unmakeEveryTarget = function() { + for (var i in _targetsEnabled) + _currentInstance.unmakeTarget(i, true); + + _targetEndpointDefinitions = {}; + _targetEndpointsUnique = {}; + _targetMaxConnections = {}; + _targetsEnabled = {}; + + return _currentInstance; + }; + + // does the work of setting a source enabled or disabled. + var _setEnabled = function(type, el, state, toggle) { + var a = type == "source" ? _sourcesEnabled : _targetsEnabled; + el = _convertYUICollection(el); + + if (_ju.isString(el)) a[el] = toggle ? !a[el] : state; + else if (el.length) { + for (var i = 0, ii = el.length; i < ii; i++) { + var info = _info(el[i]); + a[info.id] = toggle ? !a[info.id] : state; + } + } + return _currentInstance; + }; + + this.toggleSourceEnabled = function(el) { + _setEnabled("source", el, null, true); + return _currentInstance.isSourceEnabled(el); + }; + + this.setSourceEnabled = function(el, state) { return _setEnabled("source", el, state); }; + this.isSource = function(el) { return _sourcesEnabled[_info(el).id] != null; }; + this.isSourceEnabled = function(el) { return _sourcesEnabled[_info(el).id] === true; }; + + this.toggleTargetEnabled = function(el) { + _setEnabled("target", el, null, true); + return _currentInstance.isTargetEnabled(el); + }; + + this.isTarget = function(el) { return _targetsEnabled[_info(el).id] != null; }; + this.isTargetEnabled = function(el) { return _targetsEnabled[_info(el).id] === true; }; + this.setTargetEnabled = function(el, state) { return _setEnabled("target", el, state); }; + +// --------------------- end makeSource/makeTarget ---------------------------------------------- + + this.ready = function(fn) { + _currentInstance.bind("ready", fn); + }; + + // repaint some element's endpoints and connections + this.repaint = function(el, ui, timestamp) { + // support both lists... + if (typeof el == 'object' && el.length) + for ( var i = 0, ii = el.length; i < ii; i++) { + _draw(el[i], ui, timestamp); + } + else // ...and single strings. + _draw(el, ui, timestamp); + + return _currentInstance; + }; + + // repaint every endpoint and connection. + this.repaintEverything = function(clearEdits) { + // TODO this timestamp causes continuous anchors to not repaint properly. + // fix this. do not just take out the timestamp. it runs a lot faster with + // the timestamp included. + //var timestamp = null; + var timestamp = _timestamp(); + for ( var elId in endpointsByElement) { + _draw(elId, null, timestamp, clearEdits); + } + return _currentInstance; + }; + + + this.removeAllEndpoints = function(el, recurse) { + var _one = function(_el) { + var info = _info(_el), + ebe = endpointsByElement[info.id], + i, ii; + + if (ebe) { + for ( i = 0, ii = ebe.length; i < ii; i++) + _currentInstance.deleteEndpoint(ebe[i]); + } + delete endpointsByElement[info.id]; + + if (recurse) { + if (info.el && info.el.nodeType != 3 && info.el.nodeType != 8 ) { + for ( i = 0, ii = info.el.childNodes.length; i < ii; i++) { + _one(info.el.childNodes[i]); + } + } + } + + }; + _one(el); + return _currentInstance; + }; + + /** + * Remove the given element, including cleaning up all endpoints registered for it. + * This is exposed in the public API but also used internally by jsPlumb when removing the + * element associated with a connection drag. + */ + this.remove = function(el, doNotRepaint) { + var info = _info(el); + _currentInstance.doWhileSuspended(function() { + _currentInstance.removeAllEndpoints(info.id, true); + _currentInstance.dragManager.elementRemoved(info.id); + delete floatingConnections[info.id]; + _currentInstance.anchorManager.clearFor(info.id); + _currentInstance.anchorManager.removeFloatingConnection(info.id); + }, doNotRepaint === false); + if(info.el) jsPlumb.CurrentLibrary.removeElement(info.el); + }; + + var _registeredListeners = {}, + _unbindRegisteredListeners = function() { + for (var i in _registeredListeners) { + for (var j = 0, jj = _registeredListeners[i].length; j < jj; j++) { + var info = _registeredListeners[i][j]; + jsPlumb.CurrentLibrary.unbind(info.el, info.event, info.listener); + } + } + _registeredListeners = {}; + }; + + // internal register listener method. gives us a hook to clean things up + // with if the user calls jsPlumb.reset. + this.registerListener = function(el, type, listener) { + jsPlumb.CurrentLibrary.bind(el, type, listener); + jsPlumbUtil.addToList(_registeredListeners, type, {el:el, event:type, listener:listener}); + }; + + this.unregisterListener = function(el, type, listener) { + jsPlumb.CurrentLibrary.unbind(el, type, listener); + jsPlumbUtil.removeWithFunction(_registeredListeners, function(rl) { + return rl.type == type && rl.listener == listener; + }); + }; + + this.reset = function() { + _currentInstance.deleteEveryEndpoint(); + _currentInstance.unbind(); + _targetEndpointDefinitions = {}; + _targetEndpoints = {}; + _targetEndpointsUnique = {}; + _targetMaxConnections = {}; + _sourceEndpointDefinitions = {}; + _sourceEndpoints = {}; + _sourceEndpointsUnique = {}; + _sourceMaxConnections = {}; + connections.splice(0); + _unbindRegisteredListeners(); + _currentInstance.anchorManager.reset(); + if (!jsPlumbAdapter.headless) + _currentInstance.dragManager.reset(); + }; + + + this.setDefaultScope = function(scope) { + DEFAULT_SCOPE = scope; + return _currentInstance; + }; + + // sets whether or not some element should be currently draggable. + this.setDraggable = _setDraggable; + + // sets the id of some element, changing whatever we need to to keep track. + this.setId = function(el, newId, doNotSetAttribute) { + // + var id; + + if (jsPlumbUtil.isString(el)) { + id = el; + } + else { + el = _dom(el); + id = _currentInstance.getId(el); + } + + var sConns = _currentInstance.getConnections({source:id, scope:'*'}, true), + tConns = _currentInstance.getConnections({target:id, scope:'*'}, true); + + newId = "" + newId; + + if (!doNotSetAttribute) { + el = _dom(id); + jsPlumbAdapter.setAttribute(el, "id", newId); + } + else + el = _dom(newId); + + endpointsByElement[newId] = endpointsByElement[id] || []; + for (var i = 0, ii = endpointsByElement[newId].length; i < ii; i++) { + endpointsByElement[newId][i].setElementId(newId); + endpointsByElement[newId][i].setReferenceElement(el); + } + delete endpointsByElement[id]; + + _currentInstance.anchorManager.changeId(id, newId); + if (!jsPlumbAdapter.headless) + _currentInstance.dragManager.changeId(id, newId); + + var _conns = function(list, epIdx, type) { + for (var i = 0, ii = list.length; i < ii; i++) { + list[i].endpoints[epIdx].setElementId(newId); + list[i].endpoints[epIdx].setReferenceElement(el); + list[i][type + "Id"] = newId; + list[i][type] = el; + } + }; + _conns(sConns, 0, "source"); + _conns(tConns, 1, "target"); + + _currentInstance.repaint(newId); + }; + + this.setDebugLog = function(debugLog) { + log = debugLog; + }; + + this.setSuspendDrawing = function(val, repaintAfterwards) { + var curVal = _suspendDrawing; + _suspendDrawing = val; + if (val) _suspendedAt = new Date().getTime(); else _suspendedAt = null; + if (repaintAfterwards) _currentInstance.repaintEverything(); + return curVal; + }; + + // returns whether or not drawing is currently suspended. + this.isSuspendDrawing = function() { + return _suspendDrawing; + }; + + // return timestamp for when drawing was suspended. + this.getSuspendedAt = function() { return _suspendedAt; }; + + /** + * @doc function + * @name jsPlumb.class:doWhileSuspended + * @param {function} fn Function to run while suspended. + * @param {boolean} doNotRepaintAfterwards If true, jsPlumb won't run a full repaint. Otherwise it will. + * @description Suspends drawing, runs the given function, then re-enables drawing (and repaints, unless you tell it not to) + */ + this.doWhileSuspended = function(fn, doNotRepaintAfterwards) { + var _wasSuspended = _currentInstance.isSuspendDrawing(); + if (!_wasSuspended) + _currentInstance.setSuspendDrawing(true); + try { + fn(); + } + catch (e) { + _ju.log("Function run while suspended failed", e); + } + if (!_wasSuspended) + _currentInstance.setSuspendDrawing(false, !doNotRepaintAfterwards); + }; + + this.updateOffset = _updateOffset; + this.getOffset = function(elId) { return offsets[elId]; }; + this.getSize = function(elId) { return sizes[elId]; }; + this.getCachedData = _getCachedData; + this.timestamp = _timestamp; + + + + /** + * @doc function + * @name jsPlumb.class:setRenderMode + * @param {string} mode One of `jsPlumb.SVG, `jsPlumb.VML` or `jsPlumb.CANVAS`. + * @description Sets render mode. jsPlumb will fall back to VML if it determines that + * what you asked for is not supported (and that VML is). If you asked for VML but the browser does + * not support it, jsPlumb uses SVG. + * @return {string} The render mode that jsPlumb set, which of course may be different from that requested. + */ + this.setRenderMode = function(mode) { + renderMode = jsPlumbAdapter.setRenderMode(mode); + var i, ii; + // only add this if the renderer is canvas; we dont want these listeners registered on te + // entire document otherwise. + if (renderMode == jsPlumb.CANVAS) { + var bindOne = function(event) { + jsPlumb.CurrentLibrary.bind(document, event, function(e) { + if (!_currentInstance.currentlyDragging && renderMode == jsPlumb.CANVAS) { + // try connections first + for (i = 0, ii = connections.length; i < ii; i++ ) { + var t = connections[i].getConnector()[event](e); + if (t) return; + } + for (var el in endpointsByElement) { + var ee = endpointsByElement[el]; + for ( i = 0, ii = ee.length; i < ii; i++ ) { + if (ee[i].endpoint[event] && ee[i].endpoint[event](e)) return; + } + } + } + }); + }; + bindOne("click");bindOne("dblclick");bindOne("mousemove");bindOne("mousedown");bindOne("mouseup");bindOne("contextmenu"); + } + + return renderMode; + }; + + /** + * @doc function + * @name jsPlumb.class:getRenderMode + * @description Gets the current render mode for this instance of jsPlumb. + * @return {string} The current render mode - "canvas", "svg" or "vml". + */ + this.getRenderMode = function() { return renderMode; }; + + this.show = function(el, changeEndpoints) { + _setVisible(el, "block", changeEndpoints); + return _currentInstance; + }; + + /** + * gets some test hooks. nothing writable. + */ + this.getTestHarness = function() { + return { + endpointsByElement : endpointsByElement, + endpointCount : function(elId) { + var e = endpointsByElement[elId]; + return e ? e.length : 0; + }, + connectionCount : function(scope) { + scope = scope || DEFAULT_SCOPE; + var c = _currentInstance.getConnections({scope:scope}); + return c ? c.length : 0; + }, + getId : _getId, + makeAnchor:self.makeAnchor, + makeDynamicAnchor:self.makeDynamicAnchor + }; + }; + + + // TODO: update this method to return the current state. + this.toggleVisible = _toggleVisible; + this.toggleDraggable = _toggleDraggable; + this.addListener = this.bind; + + /* + helper method to take an xy location and adjust it for the parent's offset and scroll. + */ + this.adjustForParentOffsetAndScroll = function(xy, el) { + + var offsetParent = null, result = xy; + if (el.tagName.toLowerCase() === "svg" && el.parentNode) { + offsetParent = el.parentNode; + } + else if (el.offsetParent) { + offsetParent = el.offsetParent; + } + if (offsetParent != null) { + var po = offsetParent.tagName.toLowerCase() === "body" ? {left:0,top:0} : _getOffset(offsetParent, _currentInstance), + so = offsetParent.tagName.toLowerCase() === "body" ? {left:0,top:0} : {left:offsetParent.scrollLeft, top:offsetParent.scrollTop}; + + // i thought it might be cool to do this: + // lastReturnValue[0] = lastReturnValue[0] - offsetParent.offsetLeft + offsetParent.scrollLeft; + // lastReturnValue[1] = lastReturnValue[1] - offsetParent.offsetTop + offsetParent.scrollTop; + // but i think it ignores margins. my reasoning was that it's quicker to not hand off to some underlying + // library. + + result[0] = xy[0] - po.left + so.left; + result[1] = xy[1] - po.top + so.top; + } + + return result; + + }; + + if (!jsPlumbAdapter.headless) { + _currentInstance.dragManager = jsPlumbAdapter.getDragManager(_currentInstance); + _currentInstance.recalculateOffsets = _currentInstance.dragManager.updateOffsets; + } + + }; + + jsPlumbUtil.extend(jsPlumbInstance, jsPlumbUtil.EventGenerator, { + setAttribute : function(el, a, v) { + jsPlumbAdapter.setAttribute(el, a, v); + }, + getAttribute : function(el, a) { + return jsPlumbAdapter.getAttribute(jsPlumb.CurrentLibrary.getDOMElement(el), a); + }, + registerConnectionType : function(id, type) { + this._connectionTypes[id] = jsPlumb.extend({}, type); + }, + registerConnectionTypes : function(types) { + for (var i in types) + this._connectionTypes[i] = jsPlumb.extend({}, types[i]); + }, + registerEndpointType : function(id, type) { + this._endpointTypes[id] = jsPlumb.extend({}, type); + }, + registerEndpointTypes : function(types) { + for (var i in types) + this._endpointTypes[i] = jsPlumb.extend({}, types[i]); + }, + getType : function(id, typeDescriptor) { + return typeDescriptor === "connection" ? this._connectionTypes[id] : this._endpointTypes[id]; + }, + setIdChanged : function(oldId, newId) { + this.setId(oldId, newId, true); + }, + // set parent: change the parent for some node and update all the registrations we need to. + setParent : function(el, newParent) { + var jpcl = jsPlumb.CurrentLibrary, + _el = jpcl.getElementObject(el), + _dom = jpcl.getDOMElement(_el), + _id = this.getId(_dom), + _pel = jpcl.getElementObject(newParent), + _pdom = jpcl.getDOMElement(_pel), + _pid = this.getId(_pdom); + + _dom.parentNode.removeChild(_dom); + _pdom.appendChild(_dom); + this.dragManager.setParent(_el, _id, _pel, _pid); + } + }); + +// --------------------- static instance + AMD registration ------------------------------------------- + +// create static instance and assign to window if window exists. + var jsPlumb = new jsPlumbInstance(); + // register on window if defined (lets us run on server) + if (typeof window != 'undefined') window.jsPlumb = jsPlumb; + // add 'getInstance' method to static instance + /** + * @name jsPlumb.getInstance + * @param {object} [_defaults] Optional default settings for the new instance. + * @desc Gets a new instance of jsPlumb. + */ + jsPlumb.getInstance = function(_defaults) { + var j = new jsPlumbInstance(_defaults); + j.init(); + return j; + }; +// maybe register static instance as an AMD module, and getInstance method too. + if ( typeof define === "function") { + define( "jsplumb", [], function () { return jsPlumb; } ); + define( "jsplumbinstance", [], function () { return jsPlumb.getInstance(); } ); + } + // CommonJS + if (typeof exports !== 'undefined') { + exports.jsPlumb = jsPlumb; + } + + +// --------------------- end static instance + AMD registration ------------------------------------------- + +})();