diff --git a/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.collisioncheck-1.1.js b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.collisioncheck-1.1.js
new file mode 100644
index 0000000..e01f549
--- /dev/null
+++ b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.collisioncheck-1.1.js
@@ -0,0 +1,56 @@
+/*
+* Collision Check Plugin v1.1
+* Copyright (c) Constantin Groß, 48design.de
+* v1.2 rewrite with thanks to Daniel
+*
+* @requires jQuery v1.3.2
+* @description Checks single or groups of objects (divs, images or any other block element) for collission / overlapping
+* @returns an object collection with all colliding / overlapping html objects
+*
+* Dual licensed under the MIT and GPL licenses:
+* http://www.opensource.org/licenses/mit-license.php
+* http://www.gnu.org/licenses/gpl.html
+*
+*/
+(function($) {
+ $.fn.collidesWith = function(elements) {
+ var rects = this;
+ var checkWith = $(elements);
+ var c = $([]);
+
+ if (!rects || !checkWith) { return false; }
+
+ rects.each(function() {
+ var rect = $(this);
+
+ // define minimum and maximum coordinates
+ var rectOff = rect.offset();
+ var rectMinX = rectOff.left;
+ var rectMinY = rectOff.top;
+ var rectMaxX = rectMinX + rect.outerWidth();
+ var rectMaxY = rectMinY + rect.outerHeight();
+
+ checkWith.not(rect).each(function() {
+ var otherRect = $(this);
+ var otherRectOff = otherRect.offset();
+ var otherRectMinX = otherRectOff.left;
+ var otherRectMinY = otherRectOff.top;
+ var otherRectMaxX = otherRectMinX + otherRect.outerWidth();
+ var otherRectMaxY = otherRectMinY + otherRect.outerHeight();
+
+ // check for intersection
+ if ( rectMinX >= otherRectMaxX ||
+ rectMaxX <= otherRectMinX ||
+ rectMinY >= otherRectMaxY ||
+ rectMaxY <= otherRectMinY ) {
+ return true; // no intersection, continue each-loop
+ } else {
+ // intersection found, add only once
+ if(c.length == c.not(this).length) { c.push(this); }
+ }
+ });
+ });
+ // return collection
+ return c;
+ }
+})(jQuery);
\ No newline at end of file
diff --git a/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.color.js b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.color.js
new file mode 100644
index 0000000..1e5c2da
--- /dev/null
+++ b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.color.js
@@ -0,0 +1,664 @@
+/*
+ * jQuery Color Animations v@VERSION
+ * http://jquery.org/
+ *
+ * Copyright 2011 John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Date: @DATE
+ */
+
+(function( jQuery, undefined ){
+ var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color outlineColor".split(" "),
+
+ // plusequals test for += 100 -= 100
+ rplusequals = /^([\-+])=\s*(\d+\.?\d*)/,
+ // a set of RE's that can match strings and generate color tuples.
+ stringParsers = [{
+ re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
+ parse: function( execResult ) {
+ return [
+ execResult[ 1 ],
+ execResult[ 2 ],
+ execResult[ 3 ],
+ execResult[ 4 ]
+ ];
+ }
+ }, {
+ re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
+ parse: function( execResult ) {
+ return [
+ 2.55 * execResult[1],
+ 2.55 * execResult[2],
+ 2.55 * execResult[3],
+ execResult[ 4 ]
+ ];
+ }
+ }, {
+ re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
+ parse: function( execResult ) {
+ return [
+ parseInt( execResult[ 1 ], 16 ),
+ parseInt( execResult[ 2 ], 16 ),
+ parseInt( execResult[ 3 ], 16 )
+ ];
+ }
+ }, {
+ re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,
+ parse: function( execResult ) {
+ return [
+ parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ),
+ parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ),
+ parseInt( execResult[ 3 ] + execResult[ 3 ], 16 )
+ ];
+ }
+ }, {
+ re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
+ space: "hsla",
+ parse: function( execResult ) {
+ return [
+ execResult[1],
+ execResult[2] / 100,
+ execResult[3] / 100,
+ execResult[4]
+ ];
+ }
+ }],
+
+ // jQuery.Color( )
+ color = jQuery.Color = function( color, green, blue, alpha ) {
+ return new jQuery.Color.fn.parse( color, green, blue, alpha );
+ },
+ spaces = {
+ rgba: {
+ cache: "_rgba",
+ props: {
+ red: {
+ idx: 0,
+ type: "byte",
+ empty: true
+ },
+ green: {
+ idx: 1,
+ type: "byte",
+ empty: true
+ },
+ blue: {
+ idx: 2,
+ type: "byte",
+ empty: true
+ },
+ alpha: {
+ idx: 3,
+ type: "percent",
+ def: 1
+ }
+ }
+ },
+ hsla: {
+ cache: "_hsla",
+ props: {
+ hue: {
+ idx: 0,
+ type: "degrees",
+ empty: true
+ },
+ saturation: {
+ idx: 1,
+ type: "percent",
+ empty: true
+ },
+ lightness: {
+ idx: 2,
+ type: "percent",
+ empty: true
+ }
+ }
+ }
+ },
+ propTypes = {
+ "byte": {
+ floor: true,
+ min: 0,
+ max: 255
+ },
+ "percent": {
+ min: 0,
+ max: 1
+ },
+ "degrees": {
+ mod: 360,
+ floor: true
+ }
+ },
+ rgbaspace = spaces.rgba.props,
+ support = color.support = {},
+
+ // colors = jQuery.Color.names
+ colors,
+
+ // local aliases of functions called often
+ each = jQuery.each;
+
+ spaces.hsla.props.alpha = rgbaspace.alpha;
+
+ function clamp( value, prop, alwaysAllowEmpty ) {
+ var type = propTypes[ prop.type ] || {},
+ allowEmpty = prop.empty || alwaysAllowEmpty;
+
+ if ( allowEmpty && value == null ) {
+ return null;
+ }
+ if ( prop.def && value == null ) {
+ return prop.def;
+ }
+ if ( type.floor ) {
+ value = ~~value;
+ } else {
+ value = parseFloat( value );
+ }
+ if ( value == null || isNaN( value ) ) {
+ return prop.def;
+ }
+ if ( type.mod ) {
+ value = value % type.mod;
+ // -10 -> 350
+ return value < 0 ? type.mod + value : value;
+ }
+
+ // for now all property types without mod have min and max
+ return type.min > value ? type.min : type.max < value ? type.max : value;
+ }
+
+ function stringParse( string ) {
+ var inst = color(),
+ rgba = inst._rgba = [];
+
+ string = string.toLowerCase();
+
+ each( stringParsers, function( i, parser ) {
+ var match = parser.re.exec( string ),
+ values = match && parser.parse( match ),
+ parsed,
+ spaceName = parser.space || "rgba",
+ cache = spaces[ spaceName ].cache;
+
+
+ if ( values ) {
+ parsed = inst[ spaceName ]( values );
+
+ // if this was an rgba parse the assignment might happen twice
+ // oh well....
+ inst[ cache ] = parsed[ cache ];
+ rgba = inst._rgba = parsed._rgba;
+
+ // exit each( stringParsers ) here because we matched
+ return false;
+ }
+ });
+
+ // Found a stringParser that handled it
+ if ( rgba.length !== 0 ) {
+
+ // if this came from a parsed string, force "transparent" when alpha is 0
+ // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0)
+ if ( Math.max.apply( Math, rgba ) === 0 ) {
+ jQuery.extend( rgba, colors.transparent );
+ }
+ return inst;
+ }
+
+ // named colors / default - filter back through parse function
+ if ( string = colors[ string ] ) {
+ return string;
+ }
+ }
+
+ color.fn = color.prototype = {
+ constructor: color,
+ parse: function( red, green, blue, alpha ) {
+ if ( red === undefined ) {
+ this._rgba = [ null, null, null, null ];
+ return this;
+ }
+ if ( red instanceof jQuery || red.nodeType ) {
+ red = red instanceof jQuery ? red.css( green ) : jQuery( red ).css( green );
+ green = undefined;
+ }
+
+ var inst = this,
+ type = jQuery.type( red ),
+ rgba = this._rgba = [],
+ source;
+
+ // more than 1 argument specified - assume ( red, green, blue, alpha )
+ if ( green !== undefined ) {
+ red = [ red, green, blue, alpha ];
+ type = "array";
+ }
+
+ if ( type === "string" ) {
+ return this.parse( stringParse( red ) || colors._default );
+ }
+
+ if ( type === "array" ) {
+ each( rgbaspace, function( key, prop ) {
+ rgba[ prop.idx ] = clamp( red[ prop.idx ], prop );
+ });
+ return this;
+ }
+
+ if ( type === "object" ) {
+ if ( red instanceof color ) {
+ each( spaces, function( spaceName, space ) {
+ if ( red[ space.cache ] ) {
+ inst[ space.cache ] = red[ space.cache ].slice();
+ }
+ });
+ } else {
+ each( spaces, function( spaceName, space ) {
+ each( space.props, function( key, prop ) {
+ var cache = space.cache;
+
+ // if the cache doesn't exist, and we know how to convert
+ if ( !inst[ cache ] && space.to ) {
+
+ // if the value was null, we don't need to copy it
+ // if the key was alpha, we don't need to copy it either
+ if ( red[ key ] == null || key === "alpha") {
+ return;
+ }
+ inst[ cache ] = space.to( inst._rgba );
+ }
+
+ // this is the only case where we allow nulls for ALL properties.
+ // call clamp with alwaysAllowEmpty
+ inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true );
+ });
+ });
+ }
+ return this;
+ }
+ },
+ is: function( compare ) {
+ var is = color( compare ),
+ same = true,
+ myself = this;
+
+ each( spaces, function( _, space ) {
+ var isCache = is[ space.cache ],
+ localCache;
+ if (isCache) {
+ localCache = myself[ space.cache ] || space.to && space.to( myself._rgba ) || [];
+ each( space.props, function( _, prop ) {
+ if ( isCache[ prop.idx ] != null ) {
+ same = ( isCache[ prop.idx ] == localCache[ prop.idx ] );
+ return same;
+ }
+ });
+ }
+ return same;
+ });
+ return same;
+ },
+ _space: function() {
+ var used = [],
+ inst = this;
+ each( spaces, function( spaceName, space ) {
+ if ( inst[ space.cache ] ) {
+ used.push( spaceName );
+ }
+ });
+ return used.pop();
+ },
+ transition: function( other, distance ) {
+ var end = color( other ),
+ spaceName = end._space(),
+ space = spaces[ spaceName ],
+ start = this[ space.cache ] || space.to( this._rgba ),
+ result = start.slice();
+
+ end = end[ space.cache ];
+ each( space.props, function( key, prop ) {
+ var index = prop.idx,
+ startValue = start[ index ],
+ endValue = end[ index ],
+ type = propTypes[ prop.type ] || {};
+
+ // if null, don't override start value
+ if ( endValue === null ) {
+ return;
+ }
+ // if null - use end
+ if ( startValue === null ) {
+ result[ index ] = endValue;
+ } else {
+ if ( type.mod ) {
+ if ( endValue - startValue > type.mod / 2 ) {
+ startValue += type.mod;
+ } else if ( startValue - endValue > type.mod / 2 ) {
+ startValue -= type.mod;
+ }
+ }
+ result[ prop.idx ] = clamp( ( endValue - startValue ) * distance + startValue, prop );
+ }
+ });
+ return this[ spaceName ]( result );
+ },
+ blend: function( opaque ) {
+ // if we are already opaque - return ourself
+ if ( this._rgba[ 3 ] === 1 ) {
+ return this;
+ }
+
+ var rgb = this._rgba.slice(),
+ a = rgb.pop(),
+ blend = color( opaque )._rgba;
+
+ return color( jQuery.map( rgb, function( v, i ) {
+ return ( 1 - a ) * blend[ i ] + a * v;
+ }));
+ },
+ toRgbaString: function() {
+ var prefix = "rgba(",
+ rgba = jQuery.map( this._rgba, function( v, i ) {
+ return v == null ? ( i > 2 ? 1 : 0 ) : v;
+ });
+
+ if ( rgba[ 3 ] === 1 ) {
+ rgba.pop();
+ prefix = "rgb(";
+ }
+
+ return prefix + rgba.join(",") + ")";
+ },
+ toHslaString: function() {
+ var prefix = "hsla(",
+ hsla = jQuery.map( this.hsla(), function( v, i ) {
+ if ( v == null ) {
+ v = i > 2 ? 1 : 0;
+ }
+
+ // catch 1 and 2
+ if ( i && i < 3 ) {
+ v = Math.round( v * 100 ) + "%";
+ }
+ return v;
+ });
+
+ if ( hsla[ 3 ] == 1 ) {
+ hsla.pop();
+ prefix = "hsl(";
+ }
+ return prefix + hsla.join(",") + ")";
+ },
+ toHexString: function( includeAlpha ) {
+ var rgba = this._rgba.slice(),
+ alpha = rgba.pop();
+
+ if ( includeAlpha ) {
+ rgba.push( ~~( alpha * 255 ) );
+ }
+
+ return "#" + jQuery.map( rgba, function( v, i ) {
+
+ // default to 0 when nulls exist
+ v = ( v || 0 ).toString( 16 );
+ return v.length == 1 ? "0" + v : v;
+ }).join("");
+ },
+ toString: function() {
+ return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString();
+ }
+ };
+ color.fn.parse.prototype = color.fn;
+
+ // hsla conversions adapted from:
+ // http://www.google.com/codesearch/p#OAMlx_jo-ck/src/third_party/WebKit/Source/WebCore/inspector/front-end/Color.js&d=7&l=193
+
+ function hue2rgb( p, q, h ) {
+ h = ( h + 1 ) % 1;
+ if ( h * 6 < 1 ) {
+ return p + (q - p) * 6 * h;
+ }
+ if ( h * 2 < 1) {
+ return q;
+ }
+ if ( h * 3 < 2 ) {
+ return p + (q - p) * ((2/3) - h) * 6;
+ }
+ return p;
+ }
+
+ spaces.hsla.to = function ( rgba ) {
+ if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) {
+ return [ null, null, null, rgba[ 3 ] ];
+ }
+ var r = rgba[ 0 ] / 255,
+ g = rgba[ 1 ] / 255,
+ b = rgba[ 2 ] / 255,
+ a = rgba[ 3 ],
+ max = Math.max( r, g, b ),
+ min = Math.min( r, g, b ),
+ diff = max - min,
+ add = max + min,
+ l = add * 0.5,
+ h, s;
+
+ if ( min === max ) {
+ h = 0;
+ } else if ( r === max ) {
+ h = ( 60 * ( g - b ) / diff ) + 360;
+ } else if ( g === max ) {
+ h = ( 60 * ( b - r ) / diff ) + 120;
+ } else {
+ h = ( 60 * ( r - g ) / diff ) + 240;
+ }
+
+ if ( l === 0 || l === 1 ) {
+ s = l;
+ } else if ( l <= 0.5 ) {
+ s = diff / add;
+ } else {
+ s = diff / ( 2 - add );
+ }
+ return [ Math.round(h) % 360, s, l, a == null ? 1 : a ];
+ };
+
+ spaces.hsla.from = function ( hsla ) {
+ if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) {
+ return [ null, null, null, hsla[ 3 ] ];
+ }
+ var h = hsla[ 0 ] / 360,
+ s = hsla[ 1 ],
+ l = hsla[ 2 ],
+ a = hsla[ 3 ],
+ q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s,
+ p = 2 * l - q,
+ r, g, b;
+
+ return [
+ Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ),
+ Math.round( hue2rgb( p, q, h ) * 255 ),
+ Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ),
+ a
+ ];
+ };
+
+
+ each( spaces, function( spaceName, space ) {
+ var props = space.props,
+ cache = space.cache,
+ to = space.to,
+ from = space.from;
+
+ // makes rgba() and hsla()
+ color.fn[ spaceName ] = function( value ) {
+
+ // generate a cache for this space if it doesn't exist
+ if ( to && !this[ cache ] ) {
+ this[ cache ] = to( this._rgba );
+ }
+ if ( value === undefined ) {
+ return this[ cache ].slice();
+ }
+
+ var type = jQuery.type( value ),
+ arr = ( type === "array" || type === "object" ) ? value : arguments,
+ local = this[ cache ].slice(),
+ ret;
+
+ each( props, function( key, prop ) {
+ var val = arr[ type === "object" ? key : prop.idx ];
+ if ( val == null ) {
+ val = local[ prop.idx ];
+ }
+ local[ prop.idx ] = clamp( val, prop );
+ });
+
+ if ( from ) {
+ ret = color( from( local ) );
+ ret[ cache ] = local;
+ return ret;
+ } else {
+ return color( local );
+ }
+ };
+
+ // makes red() green() blue() alpha() hue() saturation() lightness()
+ each( props, function( key, prop ) {
+ // alpha is included in more than one space
+ if ( color.fn[ key ] ) {
+ return;
+ }
+ color.fn[ key ] = function( value ) {
+ var vtype = jQuery.type( value ),
+ fn = ( key === 'alpha' ? ( this._hsla ? 'hsla' : 'rgba' ) : spaceName ),
+ local = this[ fn ](),
+ cur = local[ prop.idx ],
+ match;
+
+ if ( vtype === "undefined" ) {
+ return cur;
+ }
+
+ if ( vtype === "function" ) {
+ value = value.call( this, cur );
+ vtype = jQuery.type( value );
+ }
+ if ( value == null && prop.empty ) {
+ return this;
+ }
+ if ( vtype === "string" ) {
+ match = rplusequals.exec( value );
+ if ( match ) {
+ value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 );
+ }
+ }
+ local[ prop.idx ] = value;
+ return this[ fn ]( local );
+ };
+ });
+ });
+
+ // add .fx.step functions
+ each( stepHooks, function( i, hook ) {
+ jQuery.cssHooks[ hook ] = {
+ set: function( elem, value ) {
+ var parsed;
+
+ if ( jQuery.type( value ) !== 'string' || ( parsed = stringParse( value ) ) )
+ {
+ value = color( parsed || value );
+ if ( !support.rgba && value._rgba[ 3 ] !== 1 ) {
+ var backgroundColor,
+ curElem = hook === "backgroundColor" ? elem.parentNode : elem;
+ do {
+ backgroundColor = jQuery.curCSS( curElem, "backgroundColor" );
+ } while (
+ ( backgroundColor === "" || backgroundColor === "transparent" ) &&
+ ( curElem = curElem.parentNode ) &&
+ curElem.style
+ );
+
+ value = value.blend( backgroundColor && backgroundColor !== "transparent" ?
+ backgroundColor :
+ "_default" );
+ }
+
+ value = value.toRgbaString();
+ }
+ elem.style[ hook ] = value;
+ }
+ };
+ jQuery.fx.step[ hook ] = function( fx ) {
+ if ( !fx.colorInit ) {
+ fx.start = color( fx.elem, hook );
+ fx.end = color( fx.end );
+ fx.colorInit = true;
+ }
+ jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) );
+ };
+ });
+
+ // detect rgba support
+ jQuery(function() {
+ var div = document.createElement( "div" ),
+ div_style = div.style;
+
+ div_style.cssText = "background-color:rgba(1,1,1,.5)";
+ support.rgba = div_style.backgroundColor.indexOf( "rgba" ) > -1;
+ });
+
+ // Some named colors to work with
+ // From Interface by Stefan Petre
+ // http://interface.eyecon.ro/
+ colors = jQuery.Color.names = {
+ aqua: "#00ffff",
+ azure: "#f0ffff",
+ beige: "#f5f5dc",
+ black: "#000000",
+ blue: "#0000ff",
+ brown: "#a52a2a",
+ cyan: "#00ffff",
+ darkblue: "#00008b",
+ darkcyan: "#008b8b",
+ darkgrey: "#a9a9a9",
+ darkgreen: "#006400",
+ darkkhaki: "#bdb76b",
+ darkmagenta: "#8b008b",
+ darkolivegreen: "#556b2f",
+ darkorange: "#ff8c00",
+ darkorchid: "#9932cc",
+ darkred: "#8b0000",
+ darksalmon: "#e9967a",
+ darkviolet: "#9400d3",
+ fuchsia: "#ff00ff",
+ gold: "#ffd700",
+ green: "#008000",
+ indigo: "#4b0082",
+ khaki: "#f0e68c",
+ lightblue: "#add8e6",
+ lightcyan: "#e0ffff",
+ lightgreen: "#90ee90",
+ lightgrey: "#d3d3d3",
+ lightpink: "#ffb6c1",
+ lightyellow: "#ffffe0",
+ lime: "#00ff00",
+ magenta: "#ff00ff",
+ maroon: "#800000",
+ navy: "#000080",
+ olive: "#808000",
+ orange: "#ffa500",
+ pink: "#ffc0cb",
+ purple: "#800080",
+ violet: "#800080",
+ red: "#ff0000",
+ silver: "#c0c0c0",
+ white: "#ffffff",
+ yellow: "#ffff00",
+ transparent: [ null, null, null, 0 ],
+ _default: "#ffffff"
+ };
+})( jQuery );
diff --git a/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.jsPlumb-1.3.8-all.js b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.jsPlumb-1.3.8-all.js
new file mode 100644
index 0000000..0e5c873
--- /dev/null
+++ b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.jsPlumb-1.3.8-all.js
@@ -0,0 +1,8761 @@
+/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.3.8
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the jsPlumb core code.
+ *
+ * Copyright (c) 2010 - 2012 Simon Porritt (simon.porritt@gmail.com)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+
+;(function() {
+
+ /**
+ * Class:jsPlumb
+ * The jsPlumb engine, registered as a static object in the window. This object contains all of the methods you will use to
+ * create and maintain Connections and Endpoints.
+ */
+
+ var canvasAvailable = !!document.createElement('canvas').getContext,
+ svgAvailable = !!window.SVGAngle || document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"),
+ // http://stackoverflow.com/questions/654112/how-do-you-detect-support-for-vml-or-svg-in-a-browser
+ vmlAvailable = function() {
+ if(vmlAvailable.vml == undefined) {
+ var a = document.body.appendChild(document.createElement('div'));
+ a.innerHTML = '';
+ var b = a.firstChild;
+ b.style.behavior = "url(#default#VML)";
+ vmlAvailable.vml = b ? typeof b.adj == "object": true;
+ a.parentNode.removeChild(a);
+ }
+ return vmlAvailable.vml;
+ };
+
+ var _findWithFunction = function(a, f) {
+ if (a)
+ for (var i = 0; i < a.length; i++) if (f(a[i])) return i;
+ return -1;
+ },
+ _indexOf = function(l, v) {
+ return _findWithFunction(l, function(_v) { return _v == v; });
+ },
+ _removeWithFunction = function(a, f) {
+ var idx = _findWithFunction(a, f);
+ if (idx > -1) a.splice(idx, 1);
+ return idx != -1;
+ },
+ _remove = function(l, v) {
+ var idx = _indexOf(l, v);
+ if (idx > -1) l.splice(idx, 1);
+ return idx != -1;
+ },
+ // TODO support insert index
+ _addWithFunction = function(list, item, hashFunction) {
+ if (_findWithFunction(list, hashFunction) == -1) list.push(item);
+ },
+ _addToList = function(map, key, value) {
+ var l = map[key];
+ if (l == null) {
+ l = [], map[key] = l;
+ }
+ l.push(value);
+ return l;
+ },
+ /**
+ an isArray function that even works across iframes...see here:
+
+ http://tobyho.com/2011/01/28/checking-types-in-javascript/
+
+ i was originally using "a.constructor == Array" as a test.
+ */
+ _isArray = function(a) {
+ return Object.prototype.toString.call(a) === "[object Array]";
+ },
+ _isString = function(s) {
+ return typeof s === "string";
+ },
+ _isObject = function(o) {
+ return Object.prototype.toString.call(o) === "[object Object]";
+ };
+
+ // for those browsers that dont have it. they still don't have it! but at least they won't crash.
+ if (!window.console)
+ window.console = { time:function(){}, timeEnd:function(){}, group:function(){}, groupEnd:function(){}, log:function(){} };
+
+ var _connectionBeingDragged = null,
+ _getAttribute = function(el, attName) { return jsPlumb.CurrentLibrary.getAttribute(_getElementObject(el), attName); },
+ _setAttribute = function(el, attName, attValue) { jsPlumb.CurrentLibrary.setAttribute(_getElementObject(el), attName, attValue); },
+ _addClass = function(el, clazz) { jsPlumb.CurrentLibrary.addClass(_getElementObject(el), clazz); },
+ _hasClass = function(el, clazz) { return jsPlumb.CurrentLibrary.hasClass(_getElementObject(el), clazz); },
+ _removeClass = function(el, clazz) { jsPlumb.CurrentLibrary.removeClass(_getElementObject(el), clazz); },
+ _getElementObject = function(el) { return jsPlumb.CurrentLibrary.getElementObject(el); },
+ _getOffset = function(el) { return jsPlumb.CurrentLibrary.getOffset(_getElementObject(el)); },
+ _getSize = function(el) { return jsPlumb.CurrentLibrary.getSize(_getElementObject(el)); },
+ _logEnabled = true,
+ _log = function() {
+ if (_logEnabled && typeof console != "undefined") {
+ try {
+ var msg = arguments[arguments.length - 1];
+ console.log(msg);
+ }
+ catch (e) {}
+ }
+ },
+ _group = function(g) { if (_logEnabled && typeof console != "undefined") console.group(g); },
+ _groupEnd = function(g) { if (_logEnabled && typeof console != "undefined") console.groupEnd(g); },
+ _time = function(t) { if (_logEnabled && typeof console != "undefined") console.time(t); },
+ _timeEnd = function(t) { if (_logEnabled && typeof console != "undefined") console.timeEnd(t); };
+
+ /**
+ * EventGenerator
+ * Superclass for objects that generate events - jsPlumb extends this, as does jsPlumbUIComponent, which all the UI elements extend.
+ */
+ EventGenerator = function() {
+ var _listeners = {}, self = this;
+
+ // this is a list of events that should re-throw any errors that occur during their dispatch. as of 1.3.0 this is private to
+ // jsPlumb, but it seems feasible that people might want to manipulate this list. the thinking is that we don't want event
+ // listeners to bring down jsPlumb - or do we. i can't make up my mind about this, but i know i want to hear about it if the "ready"
+ // event fails, because then my page has most likely not initialised. so i have this halfway-house solution. it will be interesting
+ // to hear what other people think.
+ var eventsToDieOn = [ "ready" ];
+
+ /*
+ * Binds a listener to an event.
+ *
+ * Parameters:
+ * event - name of the event to bind to.
+ * listener - function to execute.
+ */
+ this.bind = function(event, listener) {
+ _addToList(_listeners, event, listener);
+ };
+ /*
+ * Fires an update for the given event.
+ *
+ * Parameters:
+ * event - event to fire
+ * value - value to pass to the event listener(s).
+ * originalEvent - the original event from the browser
+ */
+ this.fire = function(event, value, originalEvent) {
+ if (_listeners[event]) {
+ for ( var i = 0; i < _listeners[event].length; i++) {
+ // doing it this way rather than catching and then possibly re-throwing means that an error propagated by this
+ // method will have the whole call stack available in the debugger.
+ //if (_findIndex(eventsToDieOn, event) != -1)
+ if (_findWithFunction(eventsToDieOn, function(e) { return e === event}) != -1)
+ _listeners[event][i](value, originalEvent);
+ else {
+ // for events we don't want to die on, catch and log.
+ try {
+ _listeners[event][i](value, originalEvent);
+ } catch (e) {
+ _log("jsPlumb: fire failed for event " + event + " : " + e);
+ }
+ }
+ }
+ }
+ };
+ /*
+ * Clears either all listeners, or listeners for some specific event.
+ *
+ * Parameters:
+ * event - optional. constrains the clear to just listeners for this event.
+ */
+ this.clearListeners = function(event) {
+ if (event)
+ delete _listeners[event];
+ else {
+ delete _listeners;
+ _listeners = {};
+ }
+ };
+
+ this.getListener = function(forEvent) {
+ return _listeners[forEvent];
+ };
+ },
+
+ /**
+ * creates a timestamp, using milliseconds since 1970, but as a string.
+ */
+ _timestamp = function() { return "" + (new Date()).getTime(); },
+
+ /*
+ * Class:jsPlumbUIComponent
+ * Abstract superclass for UI components Endpoint and Connection. Provides the abstraction of paintStyle/hoverPaintStyle,
+ * and also extends EventGenerator to provide the bind and fire methods.
+ */
+ jsPlumbUIComponent = function(params) {
+ var self = this, a = arguments, _hover = false, parameters = params.parameters || {}, idPrefix = self.idPrefix,
+ id = idPrefix + (new Date()).getTime();
+ self._jsPlumb = params["_jsPlumb"];
+ self.getId = function() { return id; };
+ self.tooltip = params.tooltip;
+ self.hoverClass = params.hoverClass;
+
+ // all components can generate events
+ EventGenerator.apply(this);
+ // 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();
+ self.constructor.apply(o, a);
+ return o;
+ };
+
+ this.getParameter = function(name) { return parameters[name]; },
+ this.getParameters = function() {
+ return parameters;
+ },
+ this.setParameter = function(name, value) { parameters[name] = value; },
+ this.setParameters = function(p) { parameters = p; },
+ this.overlayPlacements = [],
+ this.paintStyle = null,
+ this.hoverPaintStyle = null;
+
+ // user can supply a beforeDetach callback, which will be executed before a detach
+ // is performed; returning false prevents the detach.
+ var beforeDetach = params.beforeDetach;
+ this.isDetachAllowed = function(connection) {
+ var r = self._jsPlumb.checkCondition("beforeDetach", connection );
+ if (beforeDetach) {
+ try {
+ r = beforeDetach(connection);
+ }
+ catch (e) { _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.
+ var beforeDrop = params.beforeDrop;
+ this.isDropAllowed = function(sourceId, targetId, scope) {
+ var r = self._jsPlumb.checkCondition("beforeDrop", { sourceId:sourceId, targetId:targetId, scope:scope });
+ if (beforeDrop) {
+ try {
+ r = beforeDrop({ sourceId:sourceId, targetId:targetId, scope:scope });
+ }
+ catch (e) { _log("jsPlumb: beforeDrop callback failed", e); }
+ }
+ return r;
+ };
+
+ // helper method to update the hover style whenever it, or paintStyle, changes.
+ // we use paintStyle as the foundation and merge hoverPaintStyle over the
+ // top.
+ var _updateHoverStyle = function() {
+ if (self.paintStyle && self.hoverPaintStyle) {
+ var mergedHoverStyle = {};
+ jsPlumb.extend(mergedHoverStyle, self.paintStyle);
+ jsPlumb.extend(mergedHoverStyle, self.hoverPaintStyle);
+ delete self["hoverPaintStyle"];
+ // we want the fillStyle of paintStyle to override a gradient, if possible.
+ if (mergedHoverStyle.gradient && self.paintStyle.fillStyle)
+ delete mergedHoverStyle["gradient"];
+ self.hoverPaintStyle = mergedHoverStyle;
+ }
+ };
+
+ /*
+ * Sets the paint style and then repaints the element.
+ *
+ * Parameters:
+ * style - Style to use.
+ */
+ this.setPaintStyle = function(style, doNotRepaint) {
+ self.paintStyle = style;
+ self.paintStyleInUse = self.paintStyle;
+ _updateHoverStyle();
+ if (!doNotRepaint) self.repaint();
+ };
+
+ /*
+ * Sets the paint style to use when the mouse is hovering over the element. This is null by default.
+ * The hover paint style is applied as extensions to the paintStyle; it does not entirely replace
+ * it. This is because people will most likely want to change just one thing when hovering, say the
+ * color for example, but leave the rest of the appearance the same.
+ *
+ * Parameters:
+ * style - Style to use when the mouse is hovering.
+ * doNotRepaint - if true, the component will not be repainted. useful when setting things up initially.
+ */
+ this.setHoverPaintStyle = function(style, doNotRepaint) {
+ self.hoverPaintStyle = style;
+ _updateHoverStyle();
+ if (!doNotRepaint) self.repaint();
+ };
+
+ /*
+ * sets/unsets the hover state of this element.
+ *
+ * Parameters:
+ * hover - hover state boolean
+ * ignoreAttachedElements - if true, does not notify any attached elements of the change in hover state. used mostly to avoid infinite loops.
+ */
+ this.setHover = function(hover, ignoreAttachedElements, timestamp) {
+ // while dragging, we ignore these events. this keeps the UI from flashing and
+ // swishing and whatevering.
+ if (!self._jsPlumb.currentlyDragging && !self._jsPlumb.isHoverSuspended()) {
+
+ _hover = hover;
+ if (self.hoverClass != null && self.canvas != null) {
+ if (hover)
+ jpcl.addClass(self.canvas, self.hoverClass);
+ else
+ jpcl.removeClass(self.canvas, self.hoverClass);
+ }
+ if (self.hoverPaintStyle != null) {
+ self.paintStyleInUse = hover ? self.hoverPaintStyle : self.paintStyle;
+ timestamp = timestamp || _timestamp();
+ self.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 (self.getAttachedElements && !ignoreAttachedElements)
+ _updateAttachedElements(hover, _timestamp(), self);
+ }
+ };
+
+ this.isHover = function() { return _hover; };
+
+ var jpcl = jsPlumb.CurrentLibrary,
+ events = [ "click", "dblclick", "mouseenter", "mouseout", "mousemove", "mousedown", "mouseup", "contextmenu" ],
+ eventFilters = { "mouseout":"mouseexit" },
+ bindOne = function(o, c, evt) {
+ var filteredEvent = eventFilters[evt] || evt;
+ jpcl.bind(o, evt, function(ee) {
+ c.fire(filteredEvent, c, ee);
+ });
+ },
+ unbindOne = function(o, evt) {
+ var filteredEvent = eventFilters[evt] || evt;
+ jpcl.unbind(o, evt);
+ };
+
+ this.attachListeners = function(o, c) {
+ for (var i = 0; i < events.length; i++) {
+ bindOne(o, c, events[i]);
+ }
+ };
+
+ var _updateAttachedElements = function(state, timestamp, sourceElement) {
+ var affectedElements = self.getAttachedElements(); // implemented in subclasses
+ if (affectedElements) {
+ for (var i = 0; i < affectedElements.length; i++) {
+ if (!sourceElement || sourceElement != affectedElements[i])
+ affectedElements[i].setHover(state, true, timestamp); // tell the attached elements not to inform their own attached elements.
+ }
+ }
+ };
+
+ this.reattachListenersForElement = function(o) {
+ if (arguments.length > 1) {
+ for (var i = 0; i < events.length; i++)
+ unbindOne(o, events[i]);
+ for (var i = 1; i < arguments.length; i++)
+ self.attachListeners(o, arguments[i]);
+ }
+ };
+ },
+
+ overlayCapableJsPlumbUIComponent = function(params) {
+ jsPlumbUIComponent.apply(this, arguments);
+ var self = this;
+ /*
+ * Property: overlays
+ * List of Overlays for this component.
+ */
+ this.overlays = [];
+
+ var processOverlay = function(o) {
+ var _newOverlay = null;
+ if (_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:self, _jsPlumb:self._jsPlumb}, o[1]);
+ if (o.length == 3) jsPlumb.extend(p, o[2]);
+ _newOverlay = new jsPlumb.Overlays[self._jsPlumb.getRenderMode()][type](p);
+ if (p.events) {
+ for (var evt in p.events) {
+ _newOverlay.bind(evt, p.events[evt]);
+ }
+ }
+ } else if (o.constructor == String) {
+ _newOverlay = new jsPlumb.Overlays[self._jsPlumb.getRenderMode()][o]({component:self, _jsPlumb:self._jsPlumb});
+ } else {
+ _newOverlay = o;
+ }
+
+ self.overlays.push(_newOverlay);
+ },
+ calculateOverlaysToAdd = function(params) {
+ var defaultKeys = self.defaultOverlayKeys || [],
+ o = params.overlays,
+ checkKey = function(k) {
+ return self._jsPlumb.Defaults[k] || jsPlumb.Defaults[k] || [];
+ };
+
+ if (!o) o = [];
+
+ for (var i = 0; i < defaultKeys.length; i++)
+ o.unshift.apply(o, checkKey(defaultKeys[i]));
+
+ return o;
+ }
+
+ var _overlays = calculateOverlaysToAdd(params);//params.overlays || self._jsPlumb.Defaults.Overlays;
+ if (_overlays) {
+ for (var i = 0; i < _overlays.length; i++) {
+ processOverlay(_overlays[i]);
+ }
+ }
+
+ // overlay finder helper method
+ var _getOverlayIndex = function(id) {
+ var idx = -1;
+ for (var i = 0; i < self.overlays.length; i++) {
+ if (id === self.overlays[i].id) {
+ idx = i;
+ break;
+ }
+ }
+ return idx;
+ };
+
+ /*
+ * Function: addOverlay
+ * Adds an Overlay to the Connection.
+ *
+ * Parameters:
+ * overlay - Overlay to add.
+ */
+ this.addOverlay = function(overlay) {
+ processOverlay(overlay);
+ self.repaint();
+ };
+
+ /*
+ * Function: getOverlay
+ * Gets an overlay, by ID. Note: by ID. You would pass an 'id' parameter
+ * in to the Overlay's constructor arguments, and then use that to retrieve
+ * it via this method.
+ */
+ this.getOverlay = function(id) {
+ var idx = _getOverlayIndex(id);
+ return idx >= 0 ? self.overlays[idx] : null;
+ };
+
+ /*
+ * Function:getOverlays
+ * Gets all the overlays for this component.
+ */
+ this.getOverlays = function() {
+ return self.overlays;
+ };
+
+ /*
+ * Function: hideOverlay
+ * Hides the overlay specified by the given id.
+ */
+ this.hideOverlay = function(id) {
+ var o = self.getOverlay(id);
+ if (o) o.hide();
+ };
+
+ this.hideOverlays = function() {
+ for (var i = 0; i < self.overlays.length; i++)
+ self.overlays[i].hide();
+ };
+
+ /*
+ * Function: showOverlay
+ * Shows the overlay specified by the given id.
+ */
+ this.showOverlay = function(id) {
+ var o = self.getOverlay(id);
+ if (o) o.show();
+ };
+
+ this.showOverlays = function() {
+ for (var i = 0; i < self.overlays.length; i++)
+ self.overlays[i].show();
+ };
+
+ /**
+ * Function: removeAllOverlays
+ * Removes all overlays from the Connection, and then repaints.
+ */
+ this.removeAllOverlays = function() {
+ self.overlays.splice(0, self.overlays.length);
+ self.repaint();
+ };
+
+ /**
+ * Function:removeOverlay
+ * Removes an overlay by ID. Note: by ID. this is a string you set in the overlay spec.
+ * Parameters:
+ * overlayId - id of the overlay to remove.
+ */
+ this.removeOverlay = function(overlayId) {
+ var idx = _getOverlayIndex(overlayId);
+ if (idx != -1) {
+ var o = self.overlays[idx];
+ o.cleanup();
+ self.overlays.splice(idx, 1);
+ }
+ };
+
+ /**
+ * Function:removeOverlays
+ * Removes a set of overlays by ID. Note: by ID. this is a string you set in the overlay spec.
+ * Parameters:
+ * overlayIds - this function takes an arbitrary number of arguments, each of which is a single overlay id.
+ */
+ this.removeOverlays = function() {
+ for (var i = 0; i < arguments.length; i++)
+ self.removeOverlay(arguments[i]);
+ };
+
+ // this is a shortcut helper method to let people add a label as
+ // overlay.
+ var _internalLabelOverlayId = "__label",
+ _makeLabelOverlay = function(params) {
+
+ var _params = {
+ cssClass:params.cssClass,
+ labelStyle : this.labelStyle,
+ id:_internalLabelOverlayId,
+ component:self,
+ _jsPlumb:self._jsPlumb
+ },
+ mergedParams = jsPlumb.extend(_params, params);
+
+ return new jsPlumb.Overlays[self._jsPlumb.getRenderMode()].Label( mergedParams );
+ };
+ if (params.label) {
+ var loc = params.labelLocation || self.defaultLabelLocation || 0.5,
+ labelStyle = params.labelStyle || self._jsPlumb.Defaults.LabelStyle || jsPlumb.Defaults.LabelStyle;
+ this.overlays.push(_makeLabelOverlay({
+ label:params.label,
+ location:loc,
+ labelStyle:labelStyle
+ }));
+ }
+
+ /*
+ * Function: setLabel
+ * Sets the Connection's label.
+ *
+ * Parameters:
+ * l - label to set. May be a String, a Function that returns a String, or a params object containing { "label", "labelStyle", "location", "cssClass" }
+ */
+ this.setLabel = function(l) {
+ var lo = self.getOverlay(_internalLabelOverlayId);
+ if (!lo) {
+ var params = l.constructor == String || l.constructor == Function ? { label:l } : l;
+ lo = _makeLabelOverlay(params);
+ this.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);
+ }
+ }
+
+ self.repaint();
+ };
+
+ /*
+ Function:getLabel
+ Returns the label text for this component (or a function if you are labelling with a function).
+ This does not return the overlay itself; this is a convenience method which is a pair with
+ setLabel; together they allow you to add and access a Label Overlay without having to create the
+ Overlay object itself. For access to the underlying label overlay that jsPlumb has created,
+ use getLabelOverlay.
+ */
+ this.getLabel = function() {
+ var lo = self.getOverlay(_internalLabelOverlayId);
+ return lo != null ? lo.getLabel() : null;
+ };
+
+ /*
+ Function:getLabelOverlay
+ Returns the underlying internal label overlay, which will exist if you specified a label on
+ a connect or addEndpoint call, or have called setLabel at any stage.
+ */
+ this.getLabelOverlay = function() {
+ return self.getOverlay(_internalLabelOverlayId);
+ }
+ },
+
+ _bindListeners = function(obj, _self, _hoverFunction) {
+ obj.bind("click", function(ep, e) { _self.fire("click", _self, e); });
+ obj.bind("dblclick", function(ep, e) { _self.fire("dblclick", _self, e); });
+ obj.bind("contextmenu", function(ep, e) { _self.fire("contextmenu", _self, e); });
+ obj.bind("mouseenter", function(ep, e) {
+ if (!_self.isHover()) {
+ _hoverFunction(true);
+ _self.fire("mouseenter", _self, e);
+ }
+ });
+ obj.bind("mouseexit", function(ep, e) {
+ if (_self.isHover()) {
+ _hoverFunction(false);
+ _self.fire("mouseexit", _self, e);
+ }
+ });
+ };
+
+ var _jsPlumbInstanceIndex = 0,
+ getInstanceIndex = function() {
+ var i = _jsPlumbInstanceIndex + 1;
+ _jsPlumbInstanceIndex++;
+ return i;
+ };
+
+ var jsPlumbInstance = function(_defaults) {
+
+ /*
+ * Property: Defaults
+ *
+ * These are the default settings for jsPlumb. They are what will be used if you do not supply specific pieces of information
+ * to the various API calls. A convenient way to implement your own look and feel can be to override these defaults
+ * by including a script somewhere after the jsPlumb include, but before you make any calls to jsPlumb.
+ *
+ * Properties:
+ * - *Anchor* The default anchor to use for all connections (both source and target). Default is "BottomCenter".
+ * - *Anchors* The default anchors to use ([source, target]) for all connections. Defaults are ["BottomCenter", "BottomCenter"].
+ * - *ConnectionsDetachable* Whether or not connections are detachable by default (using the mouse). Defults to true.
+ * - *ConnectionOverlays* The default overlay definitions for Connections. Defaults to an empty list.
+ * - *Connector* The default connector definition to use for all connections. Default is "Bezier".
+ * - *Container* Optional selector or element id that instructs jsPlumb to append elements it creates to a specific element.
+ * - *DragOptions* The default drag options to pass in to connect, makeTarget and addEndpoint calls. Default is empty.
+ * - *DropOptions* The default drop options to pass in to connect, makeTarget and addEndpoint calls. Default is empty.
+ * - *Endpoint* The default endpoint definition to use for all connections (both source and target). Default is "Dot".
+ * - *EndpointOverlays* The default overlay definitions for Endpoints. Defaults to an empty list.
+ * - *Endpoints* The default endpoint definitions ([ source, target ]) to use for all connections. Defaults are ["Dot", "Dot"].
+ * - *EndpointStyle* The default style definition to use for all endpoints. Default is fillStyle:"#456".
+ * - *EndpointStyles* The default style definitions ([ source, target ]) to use for all endpoints. Defaults are empty.
+ * - *EndpointHoverStyle* The default hover style definition to use for all endpoints. Default is null.
+ * - *EndpointHoverStyles* The default hover style definitions ([ source, target ]) to use for all endpoints. Defaults are null.
+ * - *HoverPaintStyle* The default hover style definition to use for all connections. Defaults are null.
+ * - *LabelStyle* The default style to use for label overlays on connections.
+ * - *LogEnabled* Whether or not the jsPlumb log is enabled. defaults to false.
+ * - *Overlays* The default overlay definitions (for both Connections and Endpoint). Defaults to an empty list.
+ * - *MaxConnections* The default maximum number of connections for an Endpoint. Defaults to 1.
+ * - *PaintStyle* The default paint style for a connection. Default is line width of 8 pixels, with color "#456".
+ * - *RenderMode* What mode to use to paint with. If you're on IE<9, you don't really get to choose this. You'll just get VML. Otherwise, the jsPlumb default is to use SVG.
+ * - *Scope* The default "scope" to use for connections. Scope lets you assign connections to different categories.
+ */
+ this.Defaults = {
+ Anchor : "BottomCenter",
+ Anchors : [ null, null ],
+ ConnectionsDetachable : true,
+ ConnectionOverlays : [ ],
+ Connector : "Bezier",
+ Container : null,
+ 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" },
+ //Reattach:false,
+ RenderMode : "svg",
+ Scope : "jsPlumb_DefaultScope"
+ };
+ if (_defaults) jsPlumb.extend(this.Defaults, _defaults);
+
+ this.logEnabled = this.Defaults.LogEnabled;
+
+ EventGenerator.apply(this);
+ var _currentInstance = this,
+ _instanceIndex = getInstanceIndex(),
+ _bb = _currentInstance.bind,
+ _initialDefaults = {};
+
+ 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]);
+ };
+
+ /*
+ Function: importDefaults
+ Imports all the given defaults into this instance of jsPlumb.
+ */
+ _currentInstance.importDefaults = function(d) {
+ for (var i in d) {
+ _currentInstance.Defaults[i] = d[i];
+ }
+ };
+
+ /*
+ Function:restoreDefaults
+ Restores the default settings to "factory" values.
+ */
+ _currentInstance.restoreDefaults = function() {
+ _currentInstance.Defaults = jsPlumb.extend({}, _initialDefaults);
+ };
+
+ var log = null,
+ repaintFunction = function() {
+ jsPlumb.repaintEverything();
+ },
+ automaticRepaint = true,
+ repaintEverything = function() {
+ if (automaticRepaint)
+ repaintFunction();
+ },
+ resizeTimer = null,
+ initialized = false,
+ connectionsByScope = {},
+ /**
+ * 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 = {},
+ canvasList = [],
+ sizes = [],
+ //listeners = {}, // a map: keys are event types, values are lists of listeners.
+ DEFAULT_SCOPE = this.Defaults.Scope,
+ renderMode = null, // will be set in init()
+
+ /**
+ * helper method to add an item to a list, creating the list if it does
+ * not yet exist.
+ */
+ _addToList = function(map, key, value) {
+ var l = map[key];
+ if (l == null) {
+ l = [];
+ map[key] = l;
+ }
+ l.push(value);
+ return l;
+ },
+
+ /**
+ * 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 document body.
+ *
+ */
+ _appendElement = function(el, parent) {
+ if (_currentInstance.Defaults.Container)
+ jsPlumb.CurrentLibrary.appendElement(el, _currentInstance.Defaults.Container);
+ else if (!parent)
+ document.body.appendChild(el);
+ else
+ jsPlumb.CurrentLibrary.appendElement(el, parent);
+ },
+
+ _curIdStamp = 1,
+ _idstamp = function() { return "" + _curIdStamp++; },
+
+ /**
+ * 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;
+ },
+
+ _suspendDrawing = false,
+ /*
+ sets whether or not to suspend drawing. you should use this if you need to connect a whole load of things in one go.
+ it will save you a lot of time.
+ */
+ _setSuspendDrawing = function(val, repaintAfterwards) {
+ _suspendDrawing = val;
+ if (repaintAfterwards) _currentInstance.repaintEverything();
+ },
+
+ /**
+ * 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.
+ */
+ _draw = function(element, ui, timestamp) {
+ if (!_suspendDrawing) {
+ var id = _getAttribute(element, "id"),
+ repaintEls = _currentInstance.dragManager.getElementsForDraggable(id);
+
+ if (timestamp == null) timestamp = _timestamp();
+
+ _currentInstance.anchorManager.redraw(id, ui, timestamp);
+
+ if (repaintEls) {
+ for (var i in repaintEls) {
+ _currentInstance.anchorManager.redraw(repaintEls[i].id, ui, timestamp, repaintEls[i].offset);
+ }
+ }
+ }
+ },
+
+ /**
+ * 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;
+ if (_isArray(element)) {
+ retVal = [];
+ for ( var i = 0; i < element.length; i++) {
+ var el = _getElementObject(element[i]), id = _getAttribute(el, "id");
+ retVal.push(fn(el, id)); // append return values to what we will return
+ }
+ } else {
+ var el = _getElementObject(element), id = _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.
+ */
+ _initDraggableIfNecessary = function(element, isDraggable, dragOptions) {
+ 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] = _wrap(options[startEvent], function() {
+ _currentInstance.setHoverSuspended(true);
+ });
+
+ options[dragEvent] = _wrap(options[dragEvent], function() {
+ var ui = jpcl.getUIPosition(arguments);
+ _draw(element, ui);
+ _addClass(element, "jsPlumb_dragged");
+ });
+ options[stopEvent] = _wrap(options[stopEvent], function() {
+ var ui = jpcl.getUIPosition(arguments);
+ _draw(element, ui);
+ _removeClass(element, "jsPlumb_dragged");
+ _currentInstance.setHoverSuspended(false);
+ });
+ draggableStates[_getId(element)] = true;
+ var draggable = draggableStates[_getId(element)];
+ options.disabled = draggable == null ? false : !draggable;
+ jpcl.initDraggable(element, options, false);
+ _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 && _p.source.endpoint) _p.sourceEndpoint = _p.source;
+ if (_p.source && _p.target.endpoint) _p.targetEndpoint = _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()) {
+ _log(_currentInstance, "could not add connection; source endpoint is full");
+ return;
+ }
+
+ // target:
+ if (_p.targetEndpoint && _p.targetEndpoint.isFull()) {
+ _log(_currentInstance, "could not add connection; target endpoint is full");
+ return;
+ }
+
+ // 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; i < _p.sourceEndpoint.connectorOverlays.length; i++) {
+ _p.overlays.push(_p.sourceEndpoint.connectorOverlays[i]);
+ }
+ }
+
+ // tooltip. params.tooltip takes precedence, then sourceEndpoint.connectorTooltip.
+ _p.tooltip = params.tooltip;
+ if (!_p.tooltip && _p.sourceEndpoint && _p.sourceEndpoint.connectorTooltip)
+ _p.tooltip = _p.sourceEndpoint.connectorTooltip;
+
+ // 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.
+ if (_p.target && !_p.target.endpoint && !_p.targetEndpoint && !_p.newConnection) {
+ var tid = _getId(_p.target),
+ tep =_targetEndpointDefinitions[tid],
+ existingUniqueEndpoint = _targetEndpoints[tid];
+
+ if (tep) {
+ // check for max connections??
+ var newEndpoint = existingUniqueEndpoint != null ? existingUniqueEndpoint : _currentInstance.addEndpoint(_p.target, tep);
+ if (_targetEndpointsUnique[tid]) _targetEndpoints[tid] = newEndpoint;
+ _p.targetEndpoint = newEndpoint;
+ newEndpoint._makeTargetCreator = true;
+ }
+ }
+
+ // same thing, but for source.
+ if (_p.source && !_p.source.endpoint && !_p.sourceEndpoint && !_p.newConnection) {
+ var tid = _getId(_p.source),
+ tep = _sourceEndpointDefinitions[tid],
+ existingUniqueEndpoint = _sourceEndpoints[tid];
+
+ if (tep) {
+
+ var newEndpoint = existingUniqueEndpoint != null ? existingUniqueEndpoint : _currentInstance.addEndpoint(_p.source, tep);
+ if (_sourceEndpointsUnique[tid]) _sourceEndpoints[tid] = newEndpoint;
+ _p.sourceEndpoint = newEndpoint;
+ }
+ }
+
+ return _p;
+ },
+
+ _newConnection = function(params) {
+ var connectionFunc = _currentInstance.Defaults.ConnectionType || _currentInstance.getDefaultConnectionType(),
+ endpointFunc = _currentInstance.Defaults.EndpointType || 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;
+ var con = new connectionFunc(params);
+ con.id = "con_" + _idstamp();
+ _eventFireProxy("click", "click", con);
+ _eventFireProxy("dblclick", "dblclick", con);
+ _eventFireProxy("contextmenu", "contextmenu", con);
+ return con;
+ },
+
+ /**
+ * adds the connection to the backing model, fires an event if necessary and then redraws
+ */
+ _finaliseConnection = function(jpc, params, originalEvent) {
+ params = params || {};
+ // add to list of connections (by scope).
+ if (!jpc.suspendedEndpoint)
+ _addToList(connectionsByScope, jpc.scope, jpc);
+ // fire an event
+ if (!params.doNotFireConnectionEvent && params.fireEvent !== false) {
+ _currentInstance.fire("jsPlumbConnection", {
+ connection:jpc,
+ source : jpc.source, target : jpc.target,
+ sourceId : jpc.sourceId, targetId : jpc.targetId,
+ sourceEndpoint : jpc.endpoints[0], targetEndpoint : jpc.endpoints[1]
+ }, originalEvent);
+ }
+ // 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.
+ _currentInstance.anchorManager.newConnection(jpc);
+ // force a paint
+ _draw(jpc.source);
+ },
+
+ _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 || Endpoint;
+ params.parent = _getParentFromParams(params);
+ params["_jsPlumb"] = _currentInstance;
+ var ep = new endpointFunc(params);
+ ep.id = "ep_" + _idstamp();
+ _eventFireProxy("click", "endpointClick", ep);
+ _eventFireProxy("dblclick", "endpointDblClick", ep);
+ _eventFireProxy("contextmenu", "contextmenu", ep);
+ 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; i < endpoints.length; i++) {
+ for ( var j = 0; j < endpoints[i].connections.length; 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]);
+ }
+ }
+ },
+ /**
+ * perform an operation on all elements.
+ */
+ _operationOnAll = function(func) {
+ for ( var elId in endpointsByElement) {
+ _operation(elId, func);
+ }
+ },
+
+ /**
+ * helper to remove an element from the DOM.
+ */
+ _removeElement = function(element, parent) {
+ if (element != null && element.parentNode != null) {
+ element.parentNode.removeChild(element);
+ }
+ },
+ /**
+ * helper to remove a list of elements from the DOM.
+ */
+ _removeElements = function(elements, parent) {
+ for ( var i = 0; i < elements.length; i++)
+ _removeElement(elements[i], parent);
+ },
+ /**
+ * Sets whether or not the given element(s) should be draggable,
+ * regardless of what a particular plumb command may request.
+ *
+ * @param element
+ * May be a string, a element objects, or a list of
+ * strings/elements.
+ * @param draggable
+ * Whether or not the given element(s) should be draggable.
+ */
+ _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 id = _getAttribute(el, "id");
+ _operation(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 === 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).
+ *
+ * @param el
+ * 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.
+ *
+ * @param elId
+ * Id of the element in question
+ */
+ _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;
+ if (!recalc) {
+ if (timestamp && timestamp === offsetTimestamps[elId])
+ return offsets[elId];
+ }
+ if (recalc || !offset) { // if forced repaint or no offset
+ // available, we recalculate.
+ // get the current size and offset, and store them
+ var s = _getElementObject(elId);
+ if (s != null) {
+ sizes[elId] = _getSize(s);
+ offsets[elId] = _getOffset(s);
+ offsetTimestamps[elId] = timestamp;
+ }
+ } else {
+ offsets[elId] = offset;
+ if (sizes[elId] == null) {
+ var s = _getElementObject(elId);
+ if (s != null)
+ sizes[elId] = _getSize(s);
+ }
+ }
+
+ 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 offsets[elId];
+ },
+
+ // TODO comparison performance
+ _getCachedData = function(elId) {
+ var o = offsets[elId];
+ if (!o) o = _updateOffset({elId:elId});
+ 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) {
+ var ele = _getElementObject(element);
+ var id = _getAttribute(ele, "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();
+ _setAttribute(ele, "id", id);
+ }
+ return id;
+ },
+
+ /**
+ * wraps one function with another, creating a placeholder for the
+ * wrapped function if it was null. this is used to wrap the various
+ * drag/drop event functions - to allow jsPlumb to be notified of
+ * important lifecycle events without imposing itself on the user's
+ * drag/drop functionality. TODO: determine whether or not we should
+ * support an error handler concept, if one of the functions fails.
+ *
+ * @param wrappedFunction original function to wrap; may be null.
+ * @param newFunction function to wrap the original with.
+ * @param returnOnThisValue Optional. Indicates that the wrappedFunction should
+ * not be executed if the newFunction returns a value matching 'returnOnThisValue'.
+ * note that this is a simple comparison and only works for primitives right now.
+ */
+ _wrap = function(wrappedFunction, newFunction, returnOnThisValue) {
+ wrappedFunction = wrappedFunction || function() { };
+ newFunction = newFunction || function() { };
+ return function() {
+ var r = null;
+ try {
+ r = newFunction.apply(this, arguments);
+ } catch (e) {
+ _log(_currentInstance, "jsPlumb function failed : " + e);
+ }
+ if (returnOnThisValue == null || (r !== returnOnThisValue)) {
+ try {
+ wrappedFunction.apply(this, arguments);
+ } catch (e) {
+ _log(_currentInstance, "wrapped function failed : " + e);
+ }
+ }
+ return r;
+ };
+ };
+
+ /*
+ * Property: connectorClass
+ * The CSS class to set on Connection elements. This value is a String and can have multiple classes; the entire String is appended as-is.
+ */
+ this.connectorClass = "_jsPlumb_connector";
+
+ /*
+ * Property: endpointClass
+ * The CSS class to set on Endpoint elements. This value is a String and can have multiple classes; the entire String is appended as-is.
+ */
+ this.endpointClass = "_jsPlumb_endpoint";
+
+ /*
+ * Property: overlayClass
+ * The CSS class to set on an Overlay that is an HTML element. This value is a String and can have multiple classes; the entire String is appended as-is.
+ */
+ this.overlayClass = "_jsPlumb_overlay";
+
+ this.Anchors = {};
+
+ this.Connectors = {
+ "canvas":{},
+ "svg":{},
+ "vml":{}
+ };
+
+ this.Endpoints = {
+ "canvas":{},
+ "svg":{},
+ "vml":{}
+ };
+
+ this.Overlays = {
+ "canvas":{},
+ "svg":{},
+ "vml":{}
+ };
+
+// ************************ PLACEHOLDER DOC ENTRIES FOR NATURAL DOCS *****************************************
+ /*
+ * Function: bind
+ * Bind to an event on jsPlumb.
+ *
+ * Parameters:
+ * event - the event to bind. Available events on jsPlumb are:
+ * - *jsPlumbConnection* : notification that a new Connection was established. jsPlumb passes the new Connection to the callback.
+ * - *jsPlumbConnectionDetached* : notification that a Connection was detached. jsPlumb passes the detached Connection to the callback.
+ * - *click* : notification that a Connection was clicked. jsPlumb passes the Connection that was clicked to the callback.
+ * - *dblclick* : notification that a Connection was double clicked. jsPlumb passes the Connection that was double clicked to the callback.
+ * - *endpointClick* : notification that an Endpoint was clicked. jsPlumb passes the Endpoint that was clicked to the callback.
+ * - *endpointDblClick* : notification that an Endpoint was double clicked. jsPlumb passes the Endpoint that was double clicked to the callback.
+ *
+ * callback - function to callback. This function will be passed the Connection/Endpoint that caused the event, and also the original event.
+ */
+
+ /*
+ * Function: clearListeners
+ * Clears either all listeners, or listeners for some specific event.
+ *
+ * Parameters:
+ * event - optional. constrains the clear to just listeners for this event.
+ */
+
+// *************** END OF PLACEHOLDER DOC ENTRIES FOR NATURAL DOCS ***********************************************************
+
+
+// --------------------------- jsPLumbInstance public API ---------------------------------------------------------
+
+ /*
+ Function: addClass
+
+ Helper method to abstract out differences in setting css classes on the different renderer types.
+ */
+ this.addClass = function(el, clazz) {
+ return jsPlumb.CurrentLibrary.addClass(el, clazz);
+ };
+
+ /*
+ Function: removeClass
+
+ Helper method to abstract out differences in setting css classes on the different renderer types.
+ */
+ this.removeClass = function(el, clazz) {
+ return jsPlumb.CurrentLibrary.removeClass(el, clazz);
+ };
+
+ /*
+ Function: hasClass
+
+ Helper method to abstract out differences in testing for css classes on the different renderer types.
+ */
+ this.hasClass = function(el, clazz) {
+ return jsPlumb.CurrentLibrary.hasClass(el, clazz);
+ };
+
+ /*
+ Function: addEndpoint
+
+ Adds an to a given element or elements.
+
+ Parameters:
+
+ el - Element to add the endpoint to. Either an element id, a selector representing some element(s), or an array of either of these.
+ params - Object containing Endpoint constructor arguments. For more information, see .
+ referenceParams - Object containing more Endpoint constructor arguments; it will be merged with params by jsPlumb. You would use this if you had some
+ shared parameters that you wanted to reuse when you added Endpoints to a number of elements. The allowed values in
+ this object are anything that 'params' can contain. See .
+
+ Returns:
+ The newly created , if el referred to a single element. Otherwise, an array of newly created s.
+
+ See Also:
+
+ */
+ 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 = el.length && el.constructor != String ? el : [ el ];
+
+ for (var i = 0; i < inputs.length; i++) {
+ var _el = _getElementObject(inputs[i]), id = _getId(_el);
+ p.source = _el;
+ _updateOffset({ elId : id });
+ var e = _newEndpoint(p);
+ if (p.parentAnchor) e.parentAnchor = p.parentAnchor;
+ _addToList(endpointsByElement, id, e);
+ var myOffset = offsets[id], myWH = sizes[id];
+ var anchorLoc = e.anchor.compute( { xy : [ myOffset.left, myOffset.top ], wh : myWH, element : e });
+ e.paint({ anchorLoc : anchorLoc });
+ results.push(e);
+ _currentInstance.dragManager.endpointAdded(_el);
+ }
+
+ return results.length == 1 ? results[0] : results;
+ };
+
+ /*
+ Function: addEndpoints
+ Adds a list of s to a given element or elements.
+
+ Parameters:
+ target - element to add the Endpoint to. Either an element id, a selector representing some element(s), or an array of either of these.
+ endpoints - List of objects containing Endpoint constructor arguments. one Endpoint is created for each entry in this list. See 's constructor documentation.
+ referenceParams - Object containing more Endpoint constructor arguments; it will be merged with params by jsPlumb. You would use this if you had some shared parameters that you wanted to reuse when you added Endpoints to a number of elements.
+
+ Returns:
+ List of newly created s, one for each entry in the 'endpoints' argument.
+
+ See Also:
+
+ */
+ this.addEndpoints = function(el, endpoints, referenceParams) {
+ var results = [];
+ for ( var i = 0; i < endpoints.length; i++) {
+ var e = _currentInstance.addEndpoint(el, endpoints[i], referenceParams);
+ if (_isArray(e))
+ Array.prototype.push.apply(results, e);
+ else results.push(e);
+ }
+ return results;
+ };
+
+ /*
+ Function: animate
+ This is a wrapper around the supporting library's animate function; it injects a call to jsPlumb in the 'step' function (creating
+ the 'step' function if necessary). This only supports the two-arg version of the animate call in jQuery, the one that takes an 'options' object as
+ the second arg. MooTools has only one method, a two arg one. Which is handy. YUI has a one-arg method, so jsPlumb merges 'properties' and 'options' together for YUI.
+
+ Parameters:
+ el - Element to animate. Either an id, or a selector representing the element.
+ properties - The 'properties' argument you want passed to the library's animate call.
+ options - The 'options' argument you want passed to the library's animate call.
+
+ Returns:
+ void
+ */
+ this.animate = function(el, properties, options) {
+ var ele = _getElementObject(el), id = _getAttribute(el, "id");
+ options = options || {};
+ var stepFunction = jsPlumb.CurrentLibrary.dragEvents['step'];
+ var completeFunction = jsPlumb.CurrentLibrary.dragEvents['complete'];
+ options[stepFunction] = _wrap(options[stepFunction], function() {
+ _currentInstance.repaint(id);
+ });
+
+ // onComplete repaints, just to make sure everything looks good at the end of the animation.
+ options[completeFunction] = _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);
+ var r = true;
+ if (l && l.length > 0) {
+ try {
+ for (var i = 0 ; i < l.length; i++) {
+ r = r && l[i](value);
+ }
+ }
+ catch (e) {
+ _log(_currentInstance, "cannot check condition [" + conditionName + "]" + e);
+ }
+ }
+ return r;
+ };
+
+ /*
+ Function: connect
+ Establishes a between two elements (or s, which are themselves registered to elements).
+
+ Parameters:
+ params - Object containing constructor arguments for the Connection. See 's constructor documentation.
+ referenceParams - Optional object containing more constructor arguments for the Connection. Typically you would pass in data that a lot of
+ Connections are sharing here, such as connector style etc, and then use the main params for data specific to this Connection.
+
+ Returns:
+ The newly created .
+ */
+ this.connect = function(params, referenceParams) {
+ // prepare a final set of parameters to create connection with
+ var _p = _prepareConnectionParams(params, referenceParams);
+ // 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) {
+ // a connect call will delete its created endpoints on detach, unless otherwise specified.
+ // this is because the endpoints belong to this connection only, and are no use to
+ // anyone else, so they hang around like a bad smell.
+ if (_p.deleteEndpointsOnDetach == null)
+ _p.deleteEndpointsOnDetach = true;
+
+ // create the connection. it is not yet registered
+ var jpc = _newConnection(_p);
+ // now add it the model, fire an event, and redraw
+ _finaliseConnection(jpc, _p);
+ return jpc;
+ }
+ };
+
+ /*
+ Function: deleteEndpoint
+ Deletes an Endpoint and removes all Connections it has (which removes the Connections from the other Endpoints involved too)
+
+ Parameters:
+ object - either an object (such as from an addEndpoint call), or a String UUID.
+
+ Returns:
+ void
+ */
+ this.deleteEndpoint = function(object) {
+ var endpoint = (typeof object == "string") ? endpointsByUUID[object] : object;
+ if (endpoint) {
+ var uuid = endpoint.getUuid();
+ if (uuid) endpointsByUUID[uuid] = null;
+ endpoint.detachAll();
+ _removeElements(endpoint.endpoint.getDisplayElements());
+ _currentInstance.anchorManager.deleteEndpoint(endpoint);
+ for (var e in endpointsByElement) {
+ var endpoints = endpointsByElement[e];
+ if (endpoints) {
+ var newEndpoints = [];
+ for (var i = 0; i < endpoints.length; i++)
+ if (endpoints[i] != endpoint) newEndpoints.push(endpoints[i]);
+
+ endpointsByElement[e] = newEndpoints;
+ }
+ }
+ _currentInstance.dragManager.endpointDeleted(endpoint);
+ }
+ };
+
+ /*
+ Function: deleteEveryEndpoint
+ Deletes every , and their associated s, in this instance of jsPlumb. Does not unregister any event listeners (this is the only difference
+between this method and jsPlumb.reset).
+
+ Returns:
+ void
+ */
+ this.deleteEveryEndpoint = function() {
+ for ( var id in endpointsByElement) {
+ var endpoints = endpointsByElement[id];
+ if (endpoints && endpoints.length) {
+ for ( var i = 0; i < endpoints.length; i++) {
+ _currentInstance.deleteEndpoint(endpoints[i]);
+ }
+ }
+ }
+ delete endpointsByElement;
+ endpointsByElement = {};
+ delete endpointsByUUID;
+ endpointsByUUID = {};
+ };
+
+ 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("jsPlumbConnectionDetached", params, originalEvent);
+ _currentInstance.anchorManager.connectionDetached(params);
+ },
+ /**
+ fires an event to indicate an existing connection is being dragged.
+ */
+ fireConnectionDraggingEvent = function(jpc) {
+ _currentInstance.fire("connectionDrag", jpc);
+ },
+ fireConnectionDragStopEvent = function(jpc) {
+ _currentInstance.fire("connectionDragStop", jpc);
+ };
+
+
+ /*
+ Function: detach
+ Detaches and then removes a . From 1.3.5 this method has been altered to remove support for
+ specifying Connections by various parameters; you can now pass in a Connection as the first argument and
+ an optional parameters object as a second argument. If you need the functionality this method provided
+ before 1.3.5 then you should use the getConnections method to get the list of Connections to detach, and
+ then iterate through them, calling this for each one.
+
+ Parameters:
+ connection - the to detach
+ params - optional parameters to the detach call. valid values here are
+ fireEvent : defaults to false; indicates you want jsPlumb to fire a connection
+ detached event. The thinking behind this is that if you made a programmatic
+ call to detach an event, you probably don't need the callback.
+ forceDetach : defaults to false. allows you to override any beforeDetach listeners that may be registered.
+
+ Returns:
+ true if successful, false if not.
+ */
+ 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,
+ connection = firstArgIsConnection ? arguments[0] : params.connection;
+
+ if (connection) {
+ if (forceDetach || (connection.isDetachAllowed(connection)
+ && connection.endpoints[0].isDetachAllowed(connection)
+ && connection.endpoints[1].isDetachAllowed(connection))) {
+ if (forceDetach || _currentInstance.checkCondition("beforeDetach", connection))
+ connection.endpoints[0].detach(connection, false, true, fireEvent); // TODO check this param iscorrect for endpoint's detach method
+ }
+ }
+ 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(_p.source),
+ targetId = _getId(_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);
+ }
+ }
+ });
+ }
+ }
+ };
+
+ /*
+ Function: detachAllConnections
+ Removes all an element's Connections.
+
+ Parameters:
+ el - either the id of the element, or a selector for the element.
+ params - optional parameters. alowed values:
+ fireEvent : defaults to true, whether or not to fire the detach event.
+
+ Returns:
+ void
+ */
+ this.detachAllConnections = function(el, params) {
+ params = params || {};
+ el = _getElementObject(el);
+ var id = _getAttribute(el, "id"),
+ endpoints = endpointsByElement[id];
+ if (endpoints && endpoints.length) {
+ for ( var i = 0; i < endpoints.length; i++) {
+ endpoints[i].detachAll(params.fireEvent);
+ }
+ }
+ };
+
+ /*
+ Function: detachEveryConnection
+ Remove all Connections from all elements, but leaves Endpoints in place.
+
+ Parameters:
+ params - optional params object containing:
+ fireEvent : whether or not to fire detach events. defaults to true.
+
+
+ Returns:
+ void
+
+ See Also:
+
+ */
+ this.detachEveryConnection = function(params) {
+ params = params || {};
+ for ( var id in endpointsByElement) {
+ var endpoints = endpointsByElement[id];
+ if (endpoints && endpoints.length) {
+ for ( var i = 0; i < endpoints.length; i++) {
+ endpoints[i].detachAll(params.fireEvent);
+ }
+ }
+ }
+ delete connectionsByScope;
+ connectionsByScope = {};
+ };
+
+
+ /*
+ Function: draggable
+ Initialises the draggability of some element or elements. You should use this instead of your
+ library's draggable method so that jsPlumb can setup the appropriate callbacks. Your
+ underlying library's drag method is always called from this method.
+
+ Parameters:
+ el - either an element id, a list of element ids, or a selector.
+ options - options to pass through to the underlying library
+
+ Returns:
+ void
+ */
+ this.draggable = function(el, options) {
+ if (typeof el == 'object' && el.length) {
+ for ( var i = 0; i < el.length; i++) {
+ var ele = _getElementObject(el[i]);
+ if (ele) _initDraggableIfNecessary(ele, true, options);
+ }
+ }
+ 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 ( var i = 0; i < el._nodes.length; i++) {
+ var ele = _getElementObject(el._nodes[i]);
+ if (ele) _initDraggableIfNecessary(ele, true, options);
+ }
+ }
+ else {
+ var ele = _getElementObject(el);
+ if (ele) _initDraggableIfNecessary(ele, true, options);
+ }
+ };
+
+ /*
+ Function: extend
+ Wraps the underlying library's extend functionality.
+
+ Parameters:
+ o1 - object to extend
+ o2 - object to extend o1 with
+
+ Returns:
+ o1, extended with all properties from o2.
+ */
+ this.extend = function(o1, o2) {
+ return jsPlumb.CurrentLibrary.extend(o1, o2);
+ };
+
+ /*
+ * Function: getDefaultEndpointType
+ * Returns the default Endpoint type. Used when someone wants to subclass Endpoint and have jsPlumb return instances of their subclass.
+ * you would make a call like this in your class's constructor:
+ * jsPlumb.getDefaultEndpointType().apply(this, arguments);
+ *
+ * Returns:
+ * the default Endpoint function used by jsPlumb.
+ */
+ this.getDefaultEndpointType = function() {
+ return Endpoint;
+ };
+
+ /*
+ * Function: getDefaultConnectionType
+ * Returns the default Connection type. Used when someone wants to subclass Connection and have jsPlumb return instances of their subclass.
+ * you would make a call like this in your class's constructor:
+ * jsPlumb.getDefaultConnectionType().apply(this, arguments);
+ *
+ * Returns:
+ * the default Connection function used by jsPlumb.
+ */
+ this.getDefaultConnectionType = function() {
+ return Connection;
+ };
+
+ // helpers for select/selectEndpoints
+ var _setOperation = function(list, func, args, selector) {
+ for (var i = 0; i < list.length; i++) {
+ list[i][func].apply(list[i], args);
+ }
+ return selector(list);
+ },
+ _getOperation = function(list, func, args) {
+ var out = [];
+ for (var i = 0; i < list.length; 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);
+ };
+ };
+
+ /*
+ * Function: getConnections
+ * Gets all or a subset of connections currently managed by this jsPlumb instance. If only one scope is passed in to this method,
+ * the result will be a list of connections having that scope (passing in no scope at all will result in jsPlumb assuming you want the
+ * default scope). If multiple scopes are passed in, the return value will be a map of { scope -> [ connection... ] }.
+ *
+ * Parameters
+ * scope - if the only argument to getConnections is a string, jsPlumb will treat that string as a scope filter, and return a list
+ * of connections that are in the given scope. use '*' for all scopes.
+ * options - if the argument is a JS object, you can specify a finer-grained filter:
+ *
+ * - *scope* may be a string specifying a single scope, or an array of strings, specifying multiple scopes.
+ * - *source* either a string representing an element id, or a selector. constrains the result to connections having this source.
+ * - *target* either a string representing an element id, or a selector. constrains the result to connections having this target.
+ * flat - return results in a flat array (don't return an object whose keys are scopes and whose values are lists per scope).
+ *
+ */
+ this.getConnections = function(options, flat) {
+ if (!options) {
+ options = {};
+ } else if (options.constructor == String) {
+ options = { "scope": options };
+ }
+ var prepareList = function(input) {
+ var r = [];
+ if (input) {
+ if (typeof input == 'string') {
+ if (input === "*") return input;
+ r.push(input);
+ }
+ else
+ r = input;
+ }
+ return r;
+ },
+ scope = options.scope || _currentInstance.getDefaultScope(),
+ scopes = prepareList(scope),
+ sources = prepareList(options.source),
+ targets = prepareList(options.target),
+ filter = function(list, value) {
+ if (list === "*") return true;
+ return list.length > 0 ? _indexOf(list, value) != -1 : true;
+ },
+ 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;
+ }
+ ss.push(obj);
+ } else results.push(obj);
+ };
+ for ( var i in connectionsByScope) {
+ if (filter(scopes, i)) {
+ for ( var j = 0; j < connectionsByScope[i].length; j++) {
+ var c = connectionsByScope[i][j];
+ if (filter(sources, c.sourceId) && filter(targets, c.targetId))
+ _addOne(i, c);
+ }
+ }
+ }
+ return results;
+ };
+
+ var _makeConnectionSelectHandler = function(list) {
+ //var
+ return {
+ // setters
+ setHover:setter(list, "setHover", _makeConnectionSelectHandler),
+ removeAllOverlays:setter(list, "removeAllOverlays", _makeConnectionSelectHandler),
+ setLabel:setter(list, "setLabel", _makeConnectionSelectHandler),
+ addOverlay:setter(list, "addOverlay", _makeConnectionSelectHandler),
+ removeOverlay:setter(list, "removeOverlay", _makeConnectionSelectHandler),
+ removeOverlays:setter(list, "removeOverlays", _makeConnectionSelectHandler),
+ showOverlay:setter(list, "showOverlay", _makeConnectionSelectHandler),
+ hideOverlay:setter(list, "hideOverlay", _makeConnectionSelectHandler),
+ showOverlays:setter(list, "showOverlays", _makeConnectionSelectHandler),
+ hideOverlays:setter(list, "hideOverlays", _makeConnectionSelectHandler),
+ setPaintStyle:setter(list, "setPaintStyle", _makeConnectionSelectHandler),
+ setHoverPaintStyle:setter(list, "setHoverPaintStyle", _makeConnectionSelectHandler),
+ setDetachable:setter(list, "setDetachable", _makeConnectionSelectHandler),
+ setConnector:setter(list, "setConnector", _makeConnectionSelectHandler),
+ setParameter:setter(list, "setParameter", _makeConnectionSelectHandler),
+ setParameters:setter(list, "setParameters", _makeConnectionSelectHandler),
+
+ detach:function() {
+ for (var i = 0; i < list.length; i++)
+ _currentInstance.detach(list[i]);
+ },
+
+ // getters
+ getLabel:getter(list, "getLabel"),
+ getOverlay:getter(list, "getOverlay"),
+ isHover:getter(list, "isHover"),
+ isDetachable:getter(list, "isDetachable"),
+ getParameter:getter(list, "getParameter"),
+ getParameters:getter(list, "getParameters"),
+
+ // util
+ length:list.length,
+ each:function(f) {
+ for (var i = 0; i < list.length; i++) {
+ f(list[i]);
+ }
+ return _makeConnectionSelectHandler(list);
+ },
+ get:function(idx) {
+ return list[idx];
+ }
+
+ };
+ };
+
+ this.select = function(params) {
+ params = params || {};
+ params.scope = params.scope || "*";
+ var c = _currentInstance.getConnections(params, true);
+ return _makeConnectionSelectHandler(c);
+ };
+
+ /*
+ * Function: getAllConnections
+ * Gets all connections, as a map of { scope -> [ connection... ] }.
+ */
+ this.getAllConnections = function() {
+ return connectionsByScope;
+ };
+
+ /*
+ * Function: getDefaultScope
+ * Gets the default scope for connections and endpoints. a scope defines a type of endpoint/connection; supplying a
+ * scope to an endpoint or connection allows you to support different
+ * types of connections in the same UI. but if you're only interested in
+ * one type of connection, you don't need to supply a scope. this method
+ * will probably be used by very few people; it's good for testing
+ * though.
+ */
+ this.getDefaultScope = function() {
+ return DEFAULT_SCOPE;
+ };
+
+ /*
+ Function: getEndpoint
+ Gets an Endpoint by UUID
+
+ Parameters:
+ uuid - the UUID for the Endpoint
+
+ Returns:
+ Endpoint with the given UUID, null if nothing found.
+ */
+ this.getEndpoint = _getEndpoint;
+
+ /**
+ * Function:getEndpoints
+ * Gets the list of Endpoints for a given selector, or element id.
+ * @param el
+ * @return
+ */
+ this.getEndpoints = function(el) {
+ return endpointsByElement[_getId(el)];
+ };
+
+ /*
+ * 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(spec) {
+ return jsPlumb.CurrentLibrary.getSelector(spec);
+ };
+
+ 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; };
+
+ this.isCanvasAvailable = function() { return canvasAvailable; };
+ this.isSVGAvailable = function() { return svgAvailable; };
+ this.isVMLAvailable = vmlAvailable;
+
+ /*
+ Function: hide
+ Sets an element's connections to be hidden.
+
+ Parameters:
+ el - either the id of the element, or a selector for the element.
+ changeEndpoints - whether not to also hide endpoints on the element. by default this is false.
+
+ Returns:
+ void
+ */
+ this.hide = function(el, changeEndpoints) {
+ _setVisible(el, "none", changeEndpoints);
+ };
+
+ // exposed for other objects to use to get a unique id.
+ this.idstamp = _idstamp;
+
+ /**
+ * 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)
+ * @return
+ */
+ this.init = function() {
+ if (!initialized) {
+ _currentInstance.setRenderMode(_currentInstance.Defaults.RenderMode); // calling the method forces the capability logic to be run.
+
+ var bindOne = function(event) {
+ jsPlumb.CurrentLibrary.bind(document, event, function(e) {
+ if (!_currentInstance.currentlyDragging && renderMode == jsPlumb.CANVAS) {
+ // try connections first
+ for (var scope in connectionsByScope) {
+ var c = connectionsByScope[scope];
+ for (var i = 0; i < c.length; i++) {
+ var t = c[i].connector[event](e);
+ if (t) return;
+ }
+ }
+ for (var el in endpointsByElement) {
+ var ee = endpointsByElement[el];
+ for (var i = 0; i < ee.length; i++) {
+ if (ee[i].endpoint[event](e)) return;
+ }
+ }
+ }
+ });
+ };
+ bindOne("click");bindOne("dblclick");bindOne("mousemove");bindOne("mousedown");bindOne("mouseup");bindOne("contextmenu");
+
+ initialized = true;
+ _currentInstance.fire("ready");
+ }
+ };
+
+ this.log = log;
+ this.jsPlumbUIComponent = jsPlumbUIComponent;
+ this.EventGenerator = EventGenerator;
+
+ /*
+ * Creates an anchor with the given params.
+ *
+ *
+ * Returns: The newly created Anchor.
+ */
+ this.makeAnchor = function() {
+ 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 = jsPlumb.Anchors[arguments[0]]({elementId:elementId, jsPlumbInstance:_currentInstance});
+ }
+ // is it an array? it will be one of:
+ // an array of [name, params] - this defines a single anchor
+ // an array of arrays - this defines some dynamic anchors
+ // an array of numbers - this defines a single anchor.
+ else if (_isArray(specimen)) {
+ if (_isArray(specimen[0]) || _isString(specimen[0])) {
+ if (specimen.length == 2 && _isString(specimen[0]) && _isObject(specimen[1])) {
+ var pp = jsPlumb.extend({elementId:elementId, jsPlumbInstance:_currentInstance}, specimen[1]);
+ newAnchor = jsPlumb.Anchors[specimen[0]](pp);
+ }
+ else
+ newAnchor = new DynamicAnchor(specimen, null, elementId);
+ }
+ 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
+ };
+ newAnchor = new Anchor(anchorParams);
+ newAnchor.clone = function() { return new 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; i < types.length; i++) {
+ if (typeof types[i] == "string")
+ r.push(jsPlumb.Anchors[types[i]]({elementId:elementId, jsPlumbInstance:jsPlumbInstance}));
+ else if (_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 DynamicAnchor(anchors, anchorSelector);
+ };
+
+ /**
+ * Function: makeTarget
+ * Makes some DOM element a Connection target, allowing you to drag connections to it
+ * without having to register any Endpoints on it first. When a Connection is established,
+ * the endpoint spec that was passed in to this method is used to create a suitable
+ * Endpoint (the default will be used if you do not provide one).
+ *
+ * Parameters:
+ * el - string id or element selector for the element to make a target.
+ * params - JS object containing parameters:
+ * endpoint optional. specification of an endpoint to create when a connection is created.
+ * scope optional. scope for the drop zone.
+ * dropOptions optional. same stuff as you would pass to dropOptions of an Endpoint definition.
+ * deleteEndpointsOnDetach optional, defaults to true. whether or not to delete
+ * any Endpoints created by a connection to this target if
+ * the connection is subsequently detached. this will not
+ * remove Endpoints that have had more Connections attached
+ * to them after they were created.
+ *
+ *
+ */
+ 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;
+ };
+ this.makeTarget = function(el, params, referenceParams) {
+
+ var p = jsPlumb.extend({_jsPlumb:_currentInstance}, referenceParams);
+ jsPlumb.extend(p, params);
+ _setEndpointPaintStylesAndAnchor(p, 1);
+ var jpcl = jsPlumb.CurrentLibrary,
+ targetScope = p.scope || _currentInstance.Defaults.Scope,
+ deleteEndpointsOnDetach = !(p.deleteEndpointsOnDetach === false),
+ maxConnections = p.maxConnections || -1,
+ _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.
+ var elid = _getId(_el);
+ _targetEndpointDefinitions[elid] = p;
+ _targetEndpointsUnique[elid] = p.uniqueEndpoint,
+ _targetMaxConnections[elid] = maxConnections,
+ proxyComponent = new jsPlumbUIComponent(p);
+
+ var dropOptions = jsPlumb.extend({}, p.dropOptions || {}),
+ _drop = function() {
+
+ var originalEvent = jsPlumb.CurrentLibrary.getDropEvent(arguments),
+ targetCount = _currentInstance.select({target:elid}).length;
+
+ if (_targetMaxConnections[elid] > 0 && targetCount >= _targetMaxConnections[elid]){
+ console.log("target element " + elid + " is full.");
+ return false;
+ }
+
+ _currentInstance.currentlyDragging = false;
+ var draggable = _getElementObject(jpcl.getDragObject(arguments)),
+ id = _getAttribute(draggable, "dragId"),
+ // restore the original scope if necessary (issue 57)
+ scope = _getAttribute(draggable, "originalScope"),
+ jpc = floatingConnections[id],
+ source = jpc.endpoints[0],
+ _endpoint = p.endpoint ? jsPlumb.extend({}, p.endpoint) : {};
+
+ // unlock the source anchor to allow it to refresh its position if necessary
+ source.anchor.locked = false;
+
+ if (scope) jpcl.setDragScope(draggable, scope);
+
+ // check if drop is allowed here.
+ //var _continue = jpc.isDropAllowed(jpc.sourceId, _getId(_el), jpc.scope);
+ var _continue = proxyComponent.isDropAllowed(jpc.sourceId, _getId(_el), jpc.scope);
+
+ // regardless of whether the connection is ok, reconfigure the existing connection to
+ // point at the current info. we need this to be correct for the detach event that will follow.
+ // clear the source endpoint from the list to detach. we will detach this connection at this
+ // point, but we want to keep the source endpoint. the target is a floating endpoint and should
+ // be removed. TODO need to figure out whether this code can result in endpoints kicking around
+ // when they shouldnt be. like is this a full detach of a connection? can it be?
+ if (jpc.endpointsToDeleteOnDetach) {
+ if (source === jpc.endpointsToDeleteOnDetach[0])
+ jpc.endpointsToDeleteOnDetach[0] = null;
+ else if (source === jpc.endpointsToDeleteOnDetach[1])
+ jpc.endpointsToDeleteOnDetach[1] = 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.targetId = jpc.suspendedEndpoint.elementId;
+ jpc.target = jpcl.getElementObject(jpc.suspendedEndpoint.elementId);
+ jpc.endpoints[1] = jpc.suspendedEndpoint;
+ }
+
+ if (_continue) {
+
+ // detach this connection from the source.
+ source.detach(jpc, false, true, false);//source.endpointWillMoveAfterConnection);
+
+ // make a new Endpoint for the target
+ //var newEndpoint = _currentInstance.addEndpoint(_el, _endpoint);
+
+ var newEndpoint = _targetEndpoints[elid] || _currentInstance.addEndpoint(_el, p);
+ if (p.uniqueEndpoint) _targetEndpoints[elid] = newEndpoint; // may of course just store what it just pulled out. that's ok.
+ newEndpoint._makeTargetCreator = 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),
+ elPosition = jpcl.getOffset(_el),
+ elSize = jpcl.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.
+ }
+ var c = _currentInstance.connect({
+ source:source,
+ target:newEndpoint,
+ scope:scope,
+ previousConnection:jpc,
+ container:jpc.parent,
+ deleteEndpointsOnDetach:deleteEndpointsOnDetach,
+ // 'endpointWillMoveAfterConnection' is set by the makeSource function, and it indicates that the
+ // given endpoint will actually transfer from the element it is currently attached to to some other
+ // element after a connection has been established. in that case, we do not want to fire the
+ // connection event, since it will have the wrong data in it; makeSource will do it for us.
+ // this is controlled by the 'parent' parameter on a makeSource call.
+ doNotFireConnectionEvent:source.endpointWillMoveAfterConnection
+ });
+
+ // delete the original target endpoint. but only want to do this if the endpoint was created
+ // automatically and has no other connections.
+ if (jpc.endpoints[1]._makeTargetCreator && jpc.endpoints[1].connections.length < 2)
+ _currentInstance.deleteEndpoint(jpc.endpoints[1]);
+
+ if (deleteEndpointsOnDetach)
+ c.endpointsToDeleteOnDetach = [ source, newEndpoint ];
+
+ c.repaint();
+ }
+ // 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?
+ if (jpc.suspendedEndpoint) {
+ if (source.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.
+ }
+
+ }
+ };
+
+ var dropEvent = jpcl.dragEvents['drop'];
+ dropOptions["scope"] = dropOptions["scope"] || targetScope;
+ dropOptions[dropEvent] = _wrap(dropOptions[dropEvent], _drop);
+
+ jpcl.initDroppable(_el, dropOptions, true);
+ };
+
+ el = _convertYUICollection(el);
+
+ var inputs = el.length && el.constructor != String ? el : [ el ];
+
+ for (var i = 0; i < inputs.length; i++) {
+ _doOne(_getElementObject(inputs[i]));
+ }
+ };
+
+ /**
+ * helper method to make a list of elements drop targets.
+ * @param els
+ * @param params
+ * @param referenceParams
+ * @return
+ */
+ this.makeTargets = function(els, params, referenceParams) {
+ for ( var i = 0; i < els.length; i++) {
+ _currentInstance.makeTarget(els[i], params, referenceParams);
+ }
+ };
+
+ /**
+ * Function: makeSource
+ * Makes some DOM element a Connection source, allowing you to drag connections from it
+ * without having to register any Endpoints on it first. When a Connection is established,
+ * the endpoint spec that was passed in to this method is used to create a suitable
+ * Endpoint (the default will be used if you do not provide one).
+ *
+ * Parameters:
+ * el - string id or element selector for the element to make a source.
+ * params - JS object containing parameters:
+ * endpoint optional. specification of an endpoint to create when a connection is created.
+ * parent optional. the element to add Endpoints to when a Connection is established. if you omit this,
+ * Endpoints will be added to 'el'.
+ * scope optional. scope for the connections dragged from this element.
+ * dragOptions optional. same stuff as you would pass to dragOptions of an Endpoint definition.
+ * deleteEndpointsOnDetach optional, defaults to false. whether or not to delete
+ * any Endpoints created by a connection from this source if
+ * the connection is subsequently detached. this will not
+ * remove Endpoints that have had more Connections attached
+ * to them after they were created.
+ *
+ *
+ */
+ var _sourceEndpointDefinitions = {},
+ _sourceEndpoints = {},
+ _sourceEndpointsUnique = {};
+
+ this.makeSource = function(el, params, referenceParams) {
+ var p = jsPlumb.extend({}, referenceParams);
+ jsPlumb.extend(p, params);
+ _setEndpointPaintStylesAndAnchor(p, 0);
+ var jpcl = jsPlumb.CurrentLibrary,
+ _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.
+ var elid = _getId(_el),
+ parent = p.parent,
+ idToRegisterAgainst = parent != null ? _currentInstance.getId(jpcl.getElementObject(parent)) : elid;
+
+ _sourceEndpointDefinitions[idToRegisterAgainst] = p;
+ _sourceEndpointsUnique[idToRegisterAgainst] = p.uniqueEndpoint;
+
+ var stopEvent = jpcl.dragEvents["stop"],
+ dragEvent = jpcl.dragEvents["drag"],
+ dragOptions = jsPlumb.extend({ }, p.dragOptions || {}),
+ existingDrag = dragOptions.drag,
+ existingStop = dragOptions.stop,
+ ep = null,
+ endpointAddedButNoDragYet = false;
+
+ // set scope if its not set in dragOptions but was passed in in params
+ dragOptions["scope"] = dragOptions["scope"] || p.scope;
+
+ dragOptions[dragEvent] = _wrap(dragOptions[dragEvent], function() {
+ if (existingDrag) existingDrag.apply(this, arguments);
+ endpointAddedButNoDragYet = false;
+ });
+
+ dragOptions[stopEvent] = _wrap(dragOptions[stopEvent], function() {
+ if (existingStop) existingStop.apply(this, arguments);
+
+ //_currentlyDown = false;
+ _currentInstance.currentlyDragging = false;
+
+ if (ep.connections.length == 0)
+ _currentInstance.deleteEndpoint(ep);
+ else {
+
+ 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];
+
+ ep.anchor = _currentInstance.makeAnchor(anchorDef, elid, _currentInstance);
+
+ if (p.parent) {
+ var parent = jpcl.getElementObject(p.parent);
+ if (parent) {
+ var currentId = ep.elementId;
+
+ ep.setElement(parent);
+ ep.endpointWillMoveAfterConnection = false;
+ _currentInstance.anchorManager.rehomeEndpoint(currentId, parent);
+ oldConnection.previousConnection = null;
+ // remove from connectionsByScope
+ _removeWithFunction(connectionsByScope[oldConnection.scope], function(c) {
+ return c.id === oldConnection.id;
+ });
+ _currentInstance.anchorManager.connectionDetached({
+ sourceId:oldConnection.sourceId,
+ targetId:oldConnection.targetId,
+ connection:oldConnection
+ });
+ _finaliseConnection(oldConnection);
+ }
+ }
+
+ ep.repaint();
+ _currentInstance.repaint(ep.elementId);
+ _currentInstance.repaint(oldConnection.targetId);
+
+ }
+ });
+ // when the user presses the mouse, add an Endpoint
+ var mouseDownListener = function(e) {
+ // make sure we have the latest offset for this div
+ var myOffsetInfo = _updateOffset({elId:elid});
+
+ var x = ((e.pageX || e.page.x) - myOffsetInfo.left) / myOffsetInfo.width,
+ y = ((e.pageY || e.page.y) - 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 = jsPlumb.CurrentLibrary.getElementObject(p.parent),
+ pId = _getId(pEl);
+ myOffsetInfo = _updateOffset({elId:pId});
+ 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;
+ if (potentialParent)
+ tempEndpointParams.container = potentialParent;
+ else
+ tempEndpointParams.container = jsPlumb.CurrentLibrary.getParent(p.parent);
+ }
+
+ 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 ? jpcl.getElementObject(p.parent) : null;
+
+ 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) {
+ _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);
+ };
+
+ el = _convertYUICollection(el);
+
+ var inputs = el.length && el.constructor != String ? el : [ el ];
+
+ for (var i = 0; i < inputs.length; i++) {
+ _doOne(_getElementObject(inputs[i]));
+ }
+ };
+
+ /**
+ * helper method to make a list of elements connection sources.
+ * @param els
+ * @param params
+ * @param referenceParams
+ * @return
+ */
+ this.makeSources = function(els, params, referenceParams) {
+ for ( var i = 0; i < els.length; i++) {
+ _currentInstance.makeSource(els[i], params, referenceParams);
+ }
+ };
+
+ /*
+ Function: ready
+ Helper method to bind a function to jsPlumb's ready event.
+ */
+ this.ready = function(fn) {
+ _currentInstance.bind("ready", fn);
+ },
+
+ /*
+ Function: repaint
+ Repaints an element and its connections. This method gets new sizes for the elements before painting anything.
+
+ Parameters:
+ el - either the id of the element or a selector representing the element.
+
+ Returns:
+ void
+
+ See Also:
+
+ */
+ this.repaint = function(el) {
+ var _processElement = function(el) { _draw(_getElementObject(el)); };
+ // support both lists...
+ if (typeof el == 'object')
+ for ( var i = 0; i < el.length; i++) _processElement(el[i]);
+ else // ...and single strings.
+ _processElement(el);
+ };
+
+ /*
+ Function: repaintEverything
+ Repaints all connections.
+
+ Returns:
+ void
+
+ See Also:
+
+ */
+ this.repaintEverything = function() {
+ for ( var elId in endpointsByElement) {
+ _draw(_getElementObject(elId), null, null);
+ }
+ };
+
+ /*
+ Function: removeAllEndpoints
+ Removes all Endpoints associated with a given element. Also removes all Connections associated with each Endpoint it removes.
+
+ Parameters:
+ el - either an element id, or a selector for an element.
+
+ Returns:
+ void
+
+ See Also:
+
+ */
+ this.removeAllEndpoints = function(el) {
+ var elId = _getAttribute(el, "id"),
+ ebe = endpointsByElement[elId];
+ if (ebe) {
+ for ( var i = 0; i < ebe.length; i++)
+ _currentInstance.deleteEndpoint(ebe[i]);
+ }
+ endpointsByElement[elId] = [];
+ };
+
+ /*
+ Removes every Endpoint in this instance of jsPlumb.
+ @deprecated use deleteEveryEndpoint instead
+ */
+ this.removeEveryEndpoint = this.deleteEveryEndpoint;
+
+ /*
+ Removes the given Endpoint from the given element.
+ @deprecated Use jsPlumb.deleteEndpoint instead (and note you dont need to supply the element. it's irrelevant).
+ */
+ this.removeEndpoint = function(el, endpoint) {
+ _currentInstance.deleteEndpoint(endpoint);
+ };
+
+ var _registeredListeners = {},
+ _unbindRegisteredListeners = function() {
+ for (var i in _registeredListeners) {
+ for (var j = 0; j < _registeredListeners[i].length; 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);
+ _addToList(_registeredListeners, type, {el:el, event:type, listener:listener});
+ };
+
+ /*
+ Function:reset
+ Removes all endpoints and connections and clears the listener list. To keep listeners call jsPlumb.deleteEveryEndpoint instead of this.
+ */
+ this.reset = function() {
+ _currentInstance.deleteEveryEndpoint();
+ _currentInstance.clearListeners();
+ _targetEndpointDefinitions = {};
+ _targetEndpoints = {};
+ _targetEndpointsUnique = {};
+ _targetMaxConnections = {};
+ _sourceEndpointDefinitions = {};
+ _sourceEndpoints = {};
+ _sourceEndpointsUnique = {};
+ _unbindRegisteredListeners();
+ _currentInstance.anchorManager.reset();
+ _currentInstance.dragManager.reset();
+ };
+
+ /*
+ Function: setAutomaticRepaint
+ Sets/unsets automatic repaint on window resize.
+
+ Parameters:
+ value - whether or not to automatically repaint when the window is resized.
+
+ Returns: void
+ *
+ this.setAutomaticRepaint = function(value) {
+ automaticRepaint = value;
+ };*/
+
+ /*
+ * Function: setDefaultScope
+ * Sets the default scope for Connections and Endpoints. A scope defines a type of Endpoint/Connection; supplying a
+ * scope to an Endpoint or Connection allows you to support different
+ * types of Connections in the same UI. If you're only interested in
+ * one type of Connection, you don't need to supply a scope. This method
+ * will probably be used by very few people; it just instructs jsPlumb
+ * to use a different key for the default scope.
+ *
+ * Parameters:
+ * scope - scope to set as default.
+ */
+ this.setDefaultScope = function(scope) {
+ DEFAULT_SCOPE = scope;
+ };
+
+ /*
+ * Function: setDraggable
+ * Sets whether or not a given element is
+ * draggable, regardless of what any jsPlumb command may request.
+ *
+ * Parameters:
+ * el - either the id for the element, or a selector representing the element.
+ *
+ * Returns:
+ * void
+ */
+ this.setDraggable = _setDraggable;
+
+ /*
+ * Function: setId
+ * Changes the id of some element, adjusting all connections and endpoints
+ *
+ * Parameters:
+ * el - a selector, a DOM element, or a string.
+ * newId - string.
+ */
+ this.setId = function(el, newId, doNotSetAttribute) {
+
+ var id = el.constructor == String ? el : _currentInstance.getId(el),
+ sConns = _currentInstance.getConnections({source:id, scope:'*'}, true),
+ tConns = _currentInstance.getConnections({target:id, scope:'*'}, true);
+
+ newId = "" + newId;
+
+ if (!doNotSetAttribute) {
+ el = jsPlumb.CurrentLibrary.getElementObject(id);
+ jsPlumb.CurrentLibrary.setAttribute(el, "id", newId);
+ }
+
+ el = jsPlumb.CurrentLibrary.getElementObject(newId);
+
+
+ endpointsByElement[newId] = endpointsByElement[id] || [];
+ for (var i = 0; i < endpointsByElement[newId].length; i++) {
+ endpointsByElement[newId][i].elementId = newId;
+ endpointsByElement[newId][i].element = el;
+ endpointsByElement[newId][i].anchor.elementId = newId;
+ }
+ delete endpointsByElement[id];
+
+ _currentInstance.anchorManager.changeId(id, newId);
+
+ var _conns = function(list, epIdx, type) {
+ for (var i = 0; i < list.length; i++) {
+ list[i].endpoints[epIdx].elementId = newId;
+ list[i].endpoints[epIdx].element = el;
+ list[i][type + "Id"] = newId;
+ list[i][type] = el;
+ }
+ };
+ _conns(sConns, 0, "source");
+ _conns(tConns, 1, "target");
+ };
+
+ /*
+ * Function: setIdChanged
+ * Notify jsPlumb that the element with oldId has had its id changed to newId.
+ */
+ this.setIdChanged = function(oldId, newId) {
+ _currentInstance.setId(oldId, newId, true);
+ };
+
+ this.setDebugLog = function(debugLog) {
+ log = debugLog;
+ };
+
+ /*
+ * Function: setRepaintFunction
+ * Sets the function to fire when the window size has changed and a repaint was fired.
+ *
+ * Parameters:
+ * f - Function to execute.
+ *
+ * Returns: void
+ */
+ this.setRepaintFunction = function(f) {
+ repaintFunction = f;
+ };
+
+ /*
+ * Function: setSuspendDrawing
+ * Suspends drawing operations. This can be used when you have a lot of connections to make or endpoints to register;
+ * it will save you a lot of time.
+ */
+ this.setSuspendDrawing = _setSuspendDrawing;
+
+ /*
+ * Constant for use with the setRenderMode method
+ */
+ this.CANVAS = "canvas";
+
+ /*
+ * Constant for use with the setRenderMode method
+ */
+ this.SVG = "svg";
+
+ this.VML = "vml";
+
+ /*
+ * Function: setRenderMode
+ * Sets render mode: jsPlumb.CANVAS, jsPlumb.SVG or jsPlumb.VML. 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.
+ *
+ * Returns:
+ * the render mode that jsPlumb set, which of course may be different from that requested.
+ */
+ this.setRenderMode = function(mode) {
+ if (mode)
+ mode = mode.toLowerCase();
+ else
+ return;
+ if (mode !== jsPlumb.CANVAS && mode !== jsPlumb.SVG && mode !== jsPlumb.VML) throw new Error("render mode must be one of jsPlumb.CANVAS, jsPlumb.SVG or jsPlumb.VML");
+ // now test we actually have the capability to do this.
+ if (mode === jsPlumb.CANVAS && canvasAvailable)
+ renderMode = jsPlumb.CANVAS;
+ else if (mode === jsPlumb.SVG && svgAvailable)
+ renderMode = jsPlumb.SVG;
+ else if (vmlAvailable())
+ renderMode = jsPlumb.VML;
+
+ return renderMode;
+ };
+
+ this.getRenderMode = function() { return renderMode; };
+
+ /*
+ * Function: show
+ * Sets an element's connections to be visible.
+ *
+ * Parameters:
+ * el - either the id of the element, or a selector for the element.
+ * changeEndpoints - whether or not to also change the visible state of the endpoints on the element. this also has a bearing on
+ * other connections on those endpoints: if their other endpoint is also visible, the connections are made visible.
+ *
+ * Returns:
+ * void
+ */
+ this.show = function(el, changeEndpoints) {
+ _setVisible(el, "block", changeEndpoints);
+ };
+
+ /*
+ * Function: sizeCanvas
+ * Helper to size a canvas. You would typically use
+ * this when writing your own Connector or Endpoint implementation.
+ *
+ * Parameters:
+ * x - [int] x position for the Canvas origin
+ * y - [int] y position for the Canvas origin
+ * w - [int] width of the canvas
+ * h - [int] height of the canvas
+ *
+ * Returns:
+ * void
+ */
+ this.sizeCanvas = function(canvas, x, y, w, h) {
+ if (canvas) {
+ canvas.style.height = h + "px";
+ canvas.height = h;
+ canvas.style.width = w + "px";
+ canvas.width = w;
+ canvas.style.left = x + "px";
+ canvas.style.top = y + "px";
+ }
+ };
+
+ /**
+ * 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 = connectionsByScope[scope];
+ return c ? c.length : 0;
+ },
+ //findIndex : _findIndex,
+ getId : _getId,
+ makeAnchor:self.makeAnchor,
+ makeDynamicAnchor:self.makeDynamicAnchor
+ };
+ };
+
+ /**
+ * Toggles visibility of an element's connections. kept for backwards
+ * compatibility
+ */
+ this.toggle = _toggleVisible;
+
+ /*
+ * Function: toggleVisible
+ * Toggles visibility of an element's Connections.
+ *
+ * Parameters:
+ * el - either the element's id, or a selector representing the element.
+ * changeEndpoints - whether or not to also toggle the endpoints on the element.
+ *
+ * Returns:
+ * void, but should be updated to return the current state
+ */
+ // TODO: update this method to return the current state.
+ this.toggleVisible = _toggleVisible;
+
+ /*
+ * Function: toggleDraggable
+ * Toggles draggability (sic?) of an element's Connections.
+ *
+ * Parameters:
+ * el - either the element's id, or a selector representing the element.
+ *
+ * Returns:
+ * The current draggable state.
+ */
+ this.toggleDraggable = _toggleDraggable;
+
+ /*
+ * Function: unload
+ * Unloads jsPlumb, deleting all storage. You should call this from an onunload attribute on the element.
+ *
+ * Returns:
+ * void
+ */
+ this.unload = function() {
+ // this used to do something, but it turns out that what it did was nothing.
+ // now it exists only for backwards compatibility.
+ };
+
+ /*
+ * Helper method to wrap an existing function with one of
+ * your own. This is used by the various implementations to wrap event
+ * callbacks for drag/drop etc; it allows jsPlumb to be transparent in
+ * its handling of these things. If a user supplies their own event
+ * callback, for anything, it will always be called.
+ */
+ this.wrap = _wrap;
+ this.addListener = this.bind;
+
+ var 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),
+ 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;
+
+ };
+
+ /**
+ * Anchors model a position on some element at which an Endpoint may be located. They began as a first class citizen of jsPlumb, ie. a user
+ * was required to create these themselves, but over time this has been replaced by the concept of referring to them either by name (eg. "TopMiddle"),
+ * or by an array describing their coordinates (eg. [ 0, 0.5, 0, -1 ], which is the same as "TopMiddle"). jsPlumb now handles all of the
+ * creation of Anchors without user intervention.
+ */
+ var Anchor = function(params) {
+ var self = this;
+ this.x = params.x || 0;
+ this.y = params.y || 0;
+ this.elementId = params.elementId;
+ var orientation = params.orientation || [ 0, 0 ];
+ var lastTimestamp = null, lastReturnValue = null;
+ this.offsets = params.offsets || [ 0, 0 ];
+ self.timestamp = null;
+ this.compute = function(params) {
+ var xy = params.xy, wh = params.wh, element = params.element, timestamp = params.timestamp;
+
+ if (timestamp && timestamp === self.timestamp)
+ return lastReturnValue;
+
+ lastReturnValue = [ xy[0] + (self.x * wh[0]) + self.offsets[0], xy[1] + (self.y * wh[1]) + self.offsets[1] ];
+
+ // adjust loc if there is an offsetParent
+ lastReturnValue = adjustForParentOffsetAndScroll(lastReturnValue, element.canvas);
+
+ self.timestamp = timestamp;
+ return lastReturnValue;
+ };
+
+ this.getOrientation = function(_endpoint) { return orientation; };
+
+ this.equals = function(anchor) {
+ if (!anchor) return false;
+ var ao = anchor.getOrientation();
+ var o = this.getOrientation();
+ return this.x == anchor.x && this.y == anchor.y
+ && this.offsets[0] == anchor.offsets[0]
+ && this.offsets[1] == anchor.offsets[1]
+ && o[0] == ao[0] && o[1] == ao[1];
+ };
+
+ this.getCurrentLocation = function() { return lastReturnValue; };
+ };
+
+ /**
+ * An Anchor that floats. its orientation is computed dynamically from
+ * its position relative to the anchor it is floating relative to. It is used when creating
+ * a connection through drag and drop.
+ *
+ * TODO FloatingAnchor could totally be refactored to extend Anchor just slightly.
+ */
+ var FloatingAnchor = function(params) {
+
+ // this is the anchor that this floating anchor is referenced to for
+ // purposes of calculating the orientation.
+ var ref = params.reference,
+ // the canvas this refers to.
+ refCanvas = params.referenceCanvas,
+ size = _getSize(_getElementObject(refCanvas)),
+
+ // these are used to store the current relative position of our
+ // anchor wrt the reference anchor. they only indicate
+ // direction, so have a value of 1 or -1 (or, very rarely, 0). these
+ // values are written by the compute method, and read
+ // by the getOrientation method.
+ xDir = 0, yDir = 0,
+ // temporary member used to store an orientation when the floating
+ // anchor is hovering over another anchor.
+ orientation = null,
+ _lastResult = null;
+
+ // set these to 0 each; they are used by certain types of connectors in the loopback case,
+ // when the connector is trying to clear the element it is on. but for floating anchor it's not
+ // very important.
+ this.x = 0; this.y = 0;
+
+ this.isFloating = true;
+
+ this.compute = function(params) {
+ var xy = params.xy, element = params.element,
+ result = [ xy[0] + (size[0] / 2), xy[1] + (size[1] / 2) ]; // return origin of the element. we may wish to improve this so that any object can be the drag proxy.
+
+ // adjust loc if there is an offsetParent
+ result = adjustForParentOffsetAndScroll(result, element.canvas);
+
+ _lastResult = result;
+ return result;
+ };
+
+ this.getOrientation = function(_endpoint) {
+ if (orientation) return orientation;
+ else {
+ var o = ref.getOrientation(_endpoint);
+ // here we take into account the orientation of the other
+ // anchor: if it declares zero for some direction, we declare zero too. this might not be the most awesome. perhaps we can come
+ // up with a better way. it's just so that the line we draw looks like it makes sense. maybe this wont make sense.
+ return [ Math.abs(o[0]) * xDir * -1,
+ Math.abs(o[1]) * yDir * -1 ];
+ }
+ };
+
+ /**
+ * notification the endpoint associated with this anchor is hovering
+ * over another anchor; we want to assume that anchor's orientation
+ * for the duration of the hover.
+ */
+ this.over = function(anchor) {
+ orientation = anchor.getOrientation();
+ };
+
+ /**
+ * notification the endpoint associated with this anchor is no
+ * longer hovering over another anchor; we should resume calculating
+ * orientation as we normally do.
+ */
+ this.out = function() { orientation = null; };
+
+ this.getCurrentLocation = function() { return _lastResult; };
+ };
+
+ /*
+ * A DynamicAnchor is an Anchor that contains a list of other Anchors, which it cycles
+ * through at compute time to find the one that is located closest to
+ * the center of the target element, and returns that Anchor's compute
+ * method result. this causes endpoints to follow each other with
+ * respect to the orientation of their target elements, which is a useful
+ * feature for some applications.
+ *
+ */
+ var DynamicAnchor = function(anchors, anchorSelector, elementId) {
+ this.isSelective = true;
+ this.isDynamic = true;
+ var _anchors = [], self = this,
+ _convert = function(anchor) {
+ return anchor.constructor == Anchor ? anchor: _currentInstance.makeAnchor(anchor, elementId, _currentInstance);
+ };
+ for (var i = 0; i < anchors.length; i++)
+ _anchors[i] = _convert(anchors[i]);
+ this.addAnchor = function(anchor) { _anchors.push(_convert(anchor)); };
+ this.getAnchors = function() { return _anchors; };
+ this.locked = false;
+ var _curAnchor = _anchors.length > 0 ? _anchors[0] : null,
+ _curIndex = _anchors.length > 0 ? 0 : -1,
+ self = this,
+
+ // helper method to calculate the distance between the centers of the two elements.
+ _distance = function(anchor, cx, cy, xy, wh) {
+ var ax = xy[0] + (anchor.x * wh[0]), ay = xy[1] + (anchor.y * wh[1]);
+ return Math.sqrt(Math.pow(cx - ax, 2) + Math.pow(cy - ay, 2));
+ },
+
+ // default method uses distance between element centers. you can provide your own method in the dynamic anchor
+ // constructor (and also to jsPlumb.makeDynamicAnchor). the arguments to it are four arrays:
+ // xy - xy loc of the anchor's element
+ // wh - anchor's element's dimensions
+ // txy - xy loc of the element of the other anchor in the connection
+ // twh - dimensions of the element of the other anchor in the connection.
+ // anchors - the list of selectable anchors
+ _anchorSelector = anchorSelector || function(xy, wh, txy, twh, anchors) {
+ var cx = txy[0] + (twh[0] / 2), cy = txy[1] + (twh[1] / 2);
+ var minIdx = -1, minDist = Infinity;
+ for ( var i = 0; i < anchors.length; i++) {
+ var d = _distance(anchors[i], cx, cy, xy, wh);
+ if (d < minDist) {
+ minIdx = i + 0;
+ minDist = d;
+ }
+ }
+ return anchors[minIdx];
+ };
+
+ this.compute = function(params) {
+ var xy = params.xy, wh = params.wh, timestamp = params.timestamp, txy = params.txy, twh = params.twh;
+ // if anchor is locked or an opposite element was not given, we
+ // maintain our state. anchor will be locked
+ // if it is the source of a drag and drop.
+ if (self.locked || txy == null || twh == null)
+ return _curAnchor.compute(params);
+ else
+ params.timestamp = null; // otherwise clear this, i think. we want the anchor to compute.
+
+ _curAnchor = _anchorSelector(xy, wh, txy, twh, _anchors);
+ self.x = _curAnchor.x;
+ self.y = _curAnchor.y;
+
+ return _curAnchor.compute(params);
+ };
+
+ this.getCurrentLocation = function() {
+ return _curAnchor != null ? _curAnchor.getCurrentLocation() : null;
+ };
+
+ this.getOrientation = function(_endpoint) { return _curAnchor != null ? _curAnchor.getOrientation(_endpoint) : [ 0, 0 ]; };
+ this.over = function(anchor) { if (_curAnchor != null) _curAnchor.over(anchor); };
+ this.out = function() { if (_curAnchor != null) _curAnchor.out(); };
+ };
+
+ /*
+ manages anchors for all elements.
+ */
+ // "continuous" anchors: anchors that pick their location based on how many connections the given element has.
+ // this requires looking at a lot more elements than normal - anything that has had a Continuous anchor applied has
+ // to be recalculated. so this manager is used as a reference point. the first time, with a new timestamp, that
+ // a continuous anchor is asked to compute, it calls this guy. or maybe, even, this guy gets called somewhere else
+ // and compute only ever returns pre-computed values. either way, this is the central point, and we want it to
+ // be called as few times as possible.
+ var continuousAnchors = {},
+ continuousAnchorLocations = {},
+ continuousAnchorOrientations = {},
+ Orientation = { HORIZONTAL : "horizontal", VERTICAL : "vertical", DIAGONAL : "diagonal", IDENTITY:"identity" },
+
+ // TODO this functions uses a crude method of determining orientation between two elements.
+ // 'diagonal' should be chosen when the angle of the line between the two centers is around
+ // one of 45, 135, 225 and 315 degrees. maybe +- 15 degrees.
+ calculateOrientation = function(sourceId, targetId, sd, td) {
+
+ if (sourceId === targetId) return {
+ orientation:Orientation.IDENTITY,
+ a:["top", "top"]
+ };
+
+ var theta = Math.atan2((td.centery - sd.centery) , (td.centerx - sd.centerx)),
+ theta2 = Math.atan2((sd.centery - td.centery) , (sd.centerx - td.centerx)),
+ h = ((sd.left <= td.left && sd.right >= td.left) || (sd.left <= td.right && sd.right >= td.right) ||
+ (sd.left <= td.left && sd.right >= td.right) || (td.left <= sd.left && td.right >= sd.right)),
+ v = ((sd.top <= td.top && sd.bottom >= td.top) || (sd.top <= td.bottom && sd.bottom >= td.bottom) ||
+ (sd.top <= td.top && sd.bottom >= td.bottom) || (td.top <= sd.top && td.bottom >= sd.bottom));
+
+ if (! (h || v)) {
+ var a = null, rls = false, rrs = false, sortValue = null;
+ if (td.left > sd.left && td.top > sd.top)
+ a = ["right", "top"];
+ else if (td.left > sd.left && sd.top > td.top)
+ a = [ "top", "left"];
+ else if (td.left < sd.left && td.top < sd.top)
+ a = [ "top", "right"];
+ else if (td.left < sd.left && td.top > sd.top)
+ a = ["left", "top" ];
+
+ return { orientation:Orientation.DIAGONAL, a:a, theta:theta, theta2:theta2 };
+ }
+ else if (h) return {
+ orientation:Orientation.HORIZONTAL,
+ a:sd.top < td.top ? ["bottom", "top"] : ["top", "bottom"],
+ theta:theta, theta2:theta2
+ }
+ else return {
+ orientation:Orientation.VERTICAL,
+ a:sd.left < td.left ? ["right", "left"] : ["left", "right"],
+ theta:theta, theta2:theta2
+ }
+ },
+ placeAnchorsOnLine = function(desc, elementDimensions, elementPosition,
+ connections, horizontal, otherMultiplier, reverse) {
+ var a = [], step = elementDimensions[horizontal ? 0 : 1] / (connections.length + 1);
+
+ for (var i = 0; i < connections.length; i++) {
+ var val = (i + 1) * step, other = otherMultiplier * elementDimensions[horizontal ? 1 : 0];
+ if (reverse)
+ val = elementDimensions[horizontal ? 0 : 1] - val;
+
+ var dx = (horizontal ? val : other), x = elementPosition[0] + dx, xp = dx / elementDimensions[0],
+ dy = (horizontal ? other : val), y = elementPosition[1] + dy, yp = dy / elementDimensions[1];
+
+ a.push([ x, y, xp, yp, connections[i][1], connections[i][2] ]);
+ }
+
+ return a;
+ },
+ standardEdgeSort = function(a, b) { return a[0] > b[0] ? 1 : -1 },
+ currySort = function(reverseAngles) {
+ return function(a,b) {
+ var r = true;
+ if (reverseAngles) {
+ if (a[0][0] < b[0][0])
+ r = true;
+ else
+ r = a[0][1] > b[0][1];
+ }
+ else {
+ if (a[0][0] > b[0][0])
+ r= true;
+ else
+ r =a[0][1] > b[0][1];
+ }
+ return r === false ? -1 : 1;
+ };
+ },
+ leftSort = function(a,b) {
+ // first get adjusted values
+ var p1 = a[0][0] < 0 ? -Math.PI - a[0][0] : Math.PI - a[0][0],
+ p2 = b[0][0] < 0 ? -Math.PI - b[0][0] : Math.PI - b[0][0];
+ if (p1 > p2) return 1;
+ else return a[0][1] > b[0][1] ? 1 : -1;
+ },
+ edgeSortFunctions = {
+ "top":standardEdgeSort,
+ "right":currySort(true),
+ "bottom":currySort(true),
+ "left":leftSort
+ },
+ _sortHelper = function(_array, _fn) {
+ return _array.sort(_fn);
+ },
+ placeAnchors = function(elementId, _anchorLists) {
+ var sS = sizes[elementId], sO = offsets[elementId],
+ placeSomeAnchors = function(desc, elementDimensions, elementPosition, unsortedConnections, isHorizontal, otherMultiplier, orientation) {
+ if (unsortedConnections.length > 0) {
+ var sc = _sortHelper(unsortedConnections, edgeSortFunctions[desc]), // puts them in order based on the target element's pos on screen
+ reverse = desc === "right" || desc === "top",
+ anchors = placeAnchorsOnLine(desc, elementDimensions,
+ elementPosition, sc,
+ isHorizontal, otherMultiplier, reverse );
+
+ // takes a computed anchor position and adjusts it for parent offset and scroll, then stores it.
+ var _setAnchorLocation = function(endpoint, anchorPos) {
+ var a = adjustForParentOffsetAndScroll([anchorPos[0], anchorPos[1]], endpoint.canvas);
+ continuousAnchorLocations[endpoint.id] = [ a[0], a[1], anchorPos[2], anchorPos[3] ];
+ continuousAnchorOrientations[endpoint.id] = orientation;
+ };
+
+ for (var i = 0; i < anchors.length; i++) {
+ var c = anchors[i][4], weAreSource = c.endpoints[0].elementId === elementId, weAreTarget = c.endpoints[1].elementId === elementId;
+ if (weAreSource)
+ _setAnchorLocation(c.endpoints[0], anchors[i]);
+ else if (weAreTarget)
+ _setAnchorLocation(c.endpoints[1], anchors[i]);
+ }
+ }
+ };
+
+ placeSomeAnchors("bottom", sS, [sO.left,sO.top], _anchorLists.bottom, true, 1, [0,1]);
+ placeSomeAnchors("top", sS, [sO.left,sO.top], _anchorLists.top, true, 0, [0,-1]);
+ placeSomeAnchors("left", sS, [sO.left,sO.top], _anchorLists.left, false, 0, [-1,0]);
+ placeSomeAnchors("right", sS, [sO.left,sO.top], _anchorLists.right, false, 1, [1,0]);
+ },
+ AnchorManager = function() {
+ var _amEndpoints = {},
+ connectionsByElementId = {},
+ self = this,
+ anchorLists = {};
+
+ this.reset = function() {
+ _amEndpoints = {};
+ connectionsByElementId = {};
+ anchorLists = {};
+ };
+ this.newConnection = function(conn) {
+ var sourceId = conn.sourceId, targetId = conn.targetId,
+ ep = conn.endpoints,
+ doRegisterTarget = true,
+ registerConnection = function(otherIndex, otherEndpoint, otherAnchor, elId, c) {
+
+ if ((sourceId == targetId) && otherAnchor.isContinuous){
+ // remove the target endpoint's canvas. we dont need it.
+ jsPlumb.CurrentLibrary.removeElement(ep[1].canvas);
+ doRegisterTarget = false;
+ }
+ _addToList(connectionsByElementId, elId, [c, otherEndpoint, otherAnchor.constructor == DynamicAnchor]);
+ };
+
+ registerConnection(0, ep[0], ep[0].anchor, targetId, conn);
+ if (doRegisterTarget)
+ registerConnection(1, ep[1], ep[1].anchor, sourceId, conn);
+ };
+ this.connectionDetached = function(connInfo) {
+ var connection = connInfo.connection || connInfo;
+ var sourceId = connection.sourceId,
+ targetId = connection.targetId,
+ ep = connection.endpoints,
+ removeConnection = function(otherIndex, otherEndpoint, otherAnchor, elId, c) {
+ if (otherAnchor.constructor == FloatingAnchor) {
+ // no-op
+ }
+ else {
+ _removeWithFunction(connectionsByElementId[elId], function(_c) {
+ return _c[0].id == c.id;
+ });
+ }
+ };
+
+ removeConnection(1, ep[1], ep[1].anchor, sourceId, connection);
+ removeConnection(0, ep[0], ep[0].anchor, targetId, connection);
+
+ // remove from anchorLists
+ var sEl = connection.sourceId,
+ tEl = connection.targetId,
+ sE = connection.endpoints[0].id,
+ tE = connection.endpoints[1].id,
+ _remove = function(list, eId) {
+ if (list) { // transient anchors dont get entries in this list.
+ var f = function(e) { return e[4] == eId; };
+ _removeWithFunction(list["top"], f);
+ _removeWithFunction(list["left"], f);
+ _removeWithFunction(list["bottom"], f);
+ _removeWithFunction(list["right"], f);
+ }
+ };
+
+ _remove(anchorLists[sEl], sE);
+ _remove(anchorLists[tEl], tE);
+ self.redraw(sEl);
+ self.redraw(tEl);
+ };
+ this.add = function(endpoint, elementId) {
+ _addToList(_amEndpoints, elementId, endpoint);
+ };
+ this.changeId = function(oldId, newId) {
+ connectionsByElementId[newId] = connectionsByElementId[oldId];
+ _amEndpoints[newId] = _amEndpoints[oldId];
+ delete connectionsByElementId[oldId];
+ delete _amEndpoints[oldId];
+ };
+ this.getConnectionsFor = function(elementId) {
+ return connectionsByElementId[elementId] || [];
+ };
+ this.getEndpointsFor = function(elementId) {
+ return _amEndpoints[elementId] || [];
+ };
+ this.deleteEndpoint = function(endpoint) {
+ _removeWithFunction(_amEndpoints[endpoint.elementId], function(e) {
+ return e.id == endpoint.id;
+ });
+ };
+ this.clearFor = function(elementId) {
+ delete _amEndpoints[elementId];
+ _amEndpoints[elementId] = [];
+ };
+ // updates the given anchor list by either updating an existing anchor's info, or adding it. this function
+ // also removes the anchor from its previous list, if the edge it is on has changed.
+ // all connections found along the way (those that are connected to one of the faces this function
+ // operates on) are added to the connsToPaint list, as are their endpoints. in this way we know to repaint
+ // them wthout having to calculate anything else about them.
+ var _updateAnchorList = function(lists, theta, order, conn, aBoolean, otherElId, idx, reverse, edgeId, elId, connsToPaint, endpointsToPaint) {
+ // first try to find the exact match, but keep track of the first index of a matching element id along the way.s
+ var exactIdx = -1,
+ firstMatchingElIdx = -1,
+ endpoint = conn.endpoints[idx],
+ endpointId = endpoint.id,
+ oIdx = [1,0][idx],
+ values = [ [ theta, order ], conn, aBoolean, otherElId, endpointId ],
+ listToAddTo = lists[edgeId],
+ listToRemoveFrom = endpoint._continuousAnchorEdge ? lists[endpoint._continuousAnchorEdge] : null;
+
+ if (listToRemoveFrom) {
+ var rIdx = _findWithFunction(listToRemoveFrom, function(e) { return e[4] == endpointId });
+ if (rIdx != -1) {
+ listToRemoveFrom.splice(rIdx, 1);
+ // get all connections from this list
+ for (var i = 0; i < listToRemoveFrom.length; i++) {
+ _addWithFunction(connsToPaint, listToRemoveFrom[i][1], function(c) { return c.id == listToRemoveFrom[i][1].id });
+ _addWithFunction(endpointsToPaint, listToRemoveFrom[i][1].endpoints[idx], function(e) { return e.id == listToRemoveFrom[i][1].endpoints[idx].id });
+ }
+ }
+ }
+
+ for (var i = 0; i < listToAddTo.length; i++) {
+ if (idx == 1 && listToAddTo[i][3] === otherElId && firstMatchingElIdx == -1)
+ firstMatchingElIdx = i;
+ _addWithFunction(connsToPaint, listToAddTo[i][1], function(c) { return c.id == listToAddTo[i][1].id });
+ _addWithFunction(endpointsToPaint, listToAddTo[i][1].endpoints[idx], function(e) { return e.id == listToAddTo[i][1].endpoints[idx].id });
+ }
+ if (exactIdx != -1) {
+ listToAddTo[exactIdx] = values;
+ }
+ else {
+ var insertIdx = reverse ? firstMatchingElIdx != -1 ? firstMatchingElIdx : 0 : listToAddTo.length; // of course we will get this from having looked through the array shortly.
+ listToAddTo.splice(insertIdx, 0, values);
+ }
+
+ // store this for next time.
+ endpoint._continuousAnchorEdge = edgeId;
+ };
+ this.redraw = function(elementId, ui, timestamp, offsetToUI) {
+ // get all the endpoints for this element
+ var ep = _amEndpoints[elementId] || [],
+ endpointConnections = connectionsByElementId[elementId] || [],
+ connectionsToPaint = [],
+ endpointsToPaint = [],
+ anchorsToUpdate = [];
+
+ timestamp = timestamp || _timestamp();
+ // offsetToUI are values that would have been calculated in the dragManager when registering
+ // an endpoint for an element that had a parent (somewhere in the hierarchy) that had been
+ // registered as draggable.
+ offsetToUI = offsetToUI || {left:0, top:0};
+ if (ui) {
+ ui = {
+ left:ui.left + offsetToUI.left,
+ top:ui.top + offsetToUI.top
+ }
+ }
+
+ _updateOffset( { elId : elementId, offset : ui, recalc : false, timestamp : timestamp });
+ // valid for one paint cycle.
+ var myOffset = offsets[elementId],
+ myWH = sizes[elementId],
+ orientationCache = {};
+
+ // actually, first we should compute the orientation of this element to all other elements to which
+ // this element is connected with a continuous anchor (whether both ends of the connection have
+ // a continuous anchor or just one)
+ //for (var i = 0; i < continuousAnchorConnections.length; i++) {
+ for (var i = 0; i < endpointConnections.length; i++) {
+ var conn = endpointConnections[i][0],
+ sourceId = conn.sourceId,
+ targetId = conn.targetId,
+ sourceContinuous = conn.endpoints[0].anchor.isContinuous,
+ targetContinuous = conn.endpoints[1].anchor.isContinuous;
+
+ if (sourceContinuous || targetContinuous) {
+ var oKey = sourceId + "_" + targetId,
+ oKey2 = targetId + "_" + sourceId,
+ o = orientationCache[oKey],
+ oIdx = conn.sourceId == elementId ? 1 : 0;
+
+ if (sourceContinuous && !anchorLists[sourceId]) anchorLists[sourceId] = { top:[], right:[], bottom:[], left:[] };
+ if (targetContinuous && !anchorLists[targetId]) anchorLists[targetId] = { top:[], right:[], bottom:[], left:[] };
+
+ if (elementId != targetId) _updateOffset( { elId : targetId, timestamp : timestamp });
+ if (elementId != sourceId) _updateOffset( { elId : sourceId, timestamp : timestamp });
+
+ var td = _getCachedData(targetId),
+ sd = _getCachedData(sourceId);
+
+ if (targetId == sourceId && (sourceContinuous || targetContinuous)) {
+ // here we may want to improve this by somehow determining the face we'd like
+ // to put the connector on. ideally, when drawing, the face should be calculated
+ // by determining which face is closest to the point at which the mouse button
+ // was released. for now, we're putting it on the top face.
+ _updateAnchorList(anchorLists[sourceId], -Math.PI / 2, 0, conn, false, targetId, 0, false, "top", sourceId, connectionsToPaint, endpointsToPaint)
+ }
+ else {
+ if (!o) {
+ o = calculateOrientation(sourceId, targetId, sd.o, td.o);
+ orientationCache[oKey] = o;
+ // this would be a performance enhancement, but the computed angles need to be clamped to
+ //the (-PI/2 -> PI/2) range in order for the sorting to work properly.
+ /* orientationCache[oKey2] = {
+ orientation:o.orientation,
+ a:[o.a[1], o.a[0]],
+ theta:o.theta + Math.PI,
+ theta2:o.theta2 + Math.PI
+ };*/
+ }
+ if (sourceContinuous) _updateAnchorList(anchorLists[sourceId], o.theta, 0, conn, false, targetId, 0, false, o.a[0], sourceId, connectionsToPaint, endpointsToPaint);
+ if (targetContinuous) _updateAnchorList(anchorLists[targetId], o.theta2, -1, conn, true, sourceId, 1, true, o.a[1], targetId, connectionsToPaint, endpointsToPaint);
+ }
+
+ if (sourceContinuous) _addWithFunction(anchorsToUpdate, sourceId, function(a) { return a === sourceId; });
+ if (targetContinuous) _addWithFunction(anchorsToUpdate, targetId, function(a) { return a === targetId; });
+ _addWithFunction(connectionsToPaint, conn, function(c) { return c.id == conn.id; });
+ if ((sourceContinuous && oIdx == 0) || (targetContinuous && oIdx == 1))
+ _addWithFunction(endpointsToPaint, conn.endpoints[oIdx], function(e) { return e.id == conn.endpoints[oIdx].id; });
+ }
+ }
+
+ // now place all the continuous anchors we need to;
+ for (var i = 0; i < anchorsToUpdate.length; i++) {
+ placeAnchors(anchorsToUpdate[i], anchorLists[anchorsToUpdate[i]]);
+ }
+
+ // now that continuous anchors have been placed, paint all the endpoints for this element
+ // TODO performance: add the endpoint ids to a temp array, and then when iterating in the next
+ // loop, check that we didn't just paint that endpoint. we can probably shave off a few more milliseconds this way.
+ for (var i = 0; i < ep.length; i++) {
+ ep[i].paint( { timestamp : timestamp, offset : myOffset, dimensions : myWH });
+ }
+ // ... and any other endpoints we came across as a result of the continuous anchors.
+ for (var i = 0; i < endpointsToPaint.length; i++) {
+ endpointsToPaint[i].paint( { timestamp : timestamp, offset : myOffset, dimensions : myWH });
+ }
+
+ // paint all the standard and "dynamic connections", which are connections whose other anchor is
+ // static and therefore does need to be recomputed; we make sure that happens only one time.
+
+ // TODO we could have compiled a list of these in the first pass through connections; might save some time.
+ for (var i = 0; i < endpointConnections.length; i++) {
+ var otherEndpoint = endpointConnections[i][1];
+ if (otherEndpoint.anchor.constructor == DynamicAnchor) {
+ otherEndpoint.paint({ elementWithPrecedence:elementId });
+ _addWithFunction(connectionsToPaint, endpointConnections[i][0], function(c) { return c.id == endpointConnections[i][0].id; });
+ // all the connections for the other endpoint now need to be repainted
+ for (var k = 0; k < otherEndpoint.connections.length; k++) {
+ if (otherEndpoint.connections[k] !== endpointConnections[i][0])
+ _addWithFunction(connectionsToPaint, otherEndpoint.connections[k], function(c) { return c.id == otherEndpoint.connections[k].id; });
+ }
+ } else if (otherEndpoint.anchor.constructor == Anchor) {
+ _addWithFunction(connectionsToPaint, endpointConnections[i][0], function(c) { return c.id == endpointConnections[i][0].id; });
+ }
+ }
+ // paint current floating connection for this element, if there is one.
+ var fc = floatingConnections[elementId];
+ if (fc)
+ fc.paint({timestamp:timestamp, recalc:false, elId:elementId});
+
+ // paint all the connections
+ for (var i = 0; i < connectionsToPaint.length; i++) {
+ connectionsToPaint[i].paint({elId:elementId, timestamp:timestamp, recalc:false});
+ }
+ };
+ this.rehomeEndpoint = function(currentId, element) {
+ var eps = _amEndpoints[currentId] || [], //,
+ elementId = _currentInstance.getId(element);
+ for (var i = 0; i < eps.length; i++) {
+ self.add(eps[i], elementId);
+ }
+ eps.splice(0, eps.length);
+ };
+ };
+ _currentInstance.anchorManager = new AnchorManager();
+ _currentInstance.continuousAnchorFactory = {
+ get:function(params) {
+ var existing = continuousAnchors[params.elementId];
+ if (!existing) {
+ existing = {
+ type:"Continuous",
+ compute : function(params) {
+ return continuousAnchorLocations[params.element.id] || [0,0];
+ },
+ getCurrentLocation : function(endpoint) {
+ return continuousAnchorLocations[endpoint.id] || [0,0];
+ },
+ getOrientation : function(endpoint) {
+ return continuousAnchorOrientations[endpoint.id] || [0,0];
+ },
+ isDynamic : true,
+ isContinuous : true
+ };
+ continuousAnchors[params.elementId] = existing;
+ }
+ return existing;
+ }
+ };
+
+ /**
+ Manages dragging for some instance of jsPlumb.
+
+ */
+ var DragManager = function() {
+
+ var _draggables = {}, _dlist = [], _delements = {}, _elementsWithEndpoints = {};
+
+ /**
+ register some element as draggable. right now the drag init stuff is done elsewhere, and it is
+ possible that will continue to be the case.
+ */
+ this.register = function(el) {
+ el = jsPlumb.CurrentLibrary.getElementObject(el);
+ var id = _currentInstance.getId(el),
+ domEl = jsPlumb.CurrentLibrary.getDOMElement(el);
+ if (!_draggables[id]) {
+ _draggables[id] = el;
+ _dlist.push(el);
+ _delements[id] = {};
+ }
+
+ // look for child elements that have endpoints and register them against this draggable.
+ var _oneLevel = function(p) {
+ var pEl = jsPlumb.CurrentLibrary.getElementObject(p),
+ pOff = jsPlumb.CurrentLibrary.getOffset(pEl);
+
+ for (var i = 0; i < p.childNodes.length; i++) {
+ if (p.childNodes[i].nodeType != 3) {
+ var cEl = jsPlumb.CurrentLibrary.getElementObject(p.childNodes[i]),
+ cid = _currentInstance.getId(cEl, null, true);
+ if (cid && _elementsWithEndpoints[cid] && _elementsWithEndpoints[cid] > 0) {
+ var cOff = jsPlumb.CurrentLibrary.getOffset(cEl);
+ _delements[id][cid] = {
+ id:cid,
+ offset:{
+ left:cOff.left - pOff.left,
+ top:cOff.top - pOff.top
+ }
+ };
+ }
+ }
+ }
+ };
+
+ _oneLevel(domEl);
+ };
+
+ /**
+ notification that an endpoint was added to the given el. we go up from that el's parent
+ node, looking for a parent that has been registered as a draggable. if we find one, we add this
+ el to that parent's list of elements to update on drag (if it is not there already)
+ */
+ this.endpointAdded = function(el) {
+ var jpcl = jsPlumb.CurrentLibrary, b = document.body, id = _currentInstance.getId(el), c = jpcl.getDOMElement(el),
+ p = c.parentNode, done = p == b;
+
+ _elementsWithEndpoints[id] = _elementsWithEndpoints[id] ? _elementsWithEndpoints[id] + 1 : 1;
+
+ while (p != b) {
+ var pid = _currentInstance.getId(p);
+ if (_draggables[pid]) {
+ var idx = -1, pEl = jpcl.getElementObject(p), pLoc = jsPlumb.CurrentLibrary.getOffset(pEl);
+
+ if (_delements[pid][id] == null) {
+ var cLoc = jsPlumb.CurrentLibrary.getOffset(el);
+ _delements[pid][id] = {
+ id:id,
+ offset:{
+ left:cLoc.left - pLoc.left,
+ top:cLoc.top - pLoc.top
+ }
+ };
+ }
+ break;
+ }
+ p = p.parentNode;
+ }
+ };
+
+ this.endpointDeleted = function(endpoint) {
+ if (_elementsWithEndpoints[endpoint.elementId]) {
+ _elementsWithEndpoints[endpoint.elementId]--;
+ if (_elementsWithEndpoints[endpoint.elementId] <= 0) {
+ for (var i in _delements) {
+ delete _delements[i][endpoint.elementId];
+ }
+ }
+ }
+ };
+
+ this.getElementsForDraggable = function(id) {
+ return _delements[id];
+ };
+
+ this.reset = function() {
+ _draggables = {};
+ _dlist = [];
+ _delements = {};
+ _elementsWithEndpoints = {};
+ };
+
+ };
+ _currentInstance.dragManager = new DragManager();
+
+
+
+ /*
+ * Class: Connection
+ * The connecting line between two Endpoints.
+ */
+ /*
+ * Function: Connection
+ * Connection constructor.
+ *
+ * Parameters:
+ * source - either an element id, a selector for an element, or an Endpoint.
+ * target - either an element id, a selector for an element, or an Endpoint
+ * scope - scope descriptor for this connection. optional.
+ * container - optional id or selector instructing jsPlumb where to attach all the elements it creates for this connection. you should read the documentation for a full discussion of this.
+ * endpoint - Optional. Endpoint definition to use for both ends of the connection.
+ * endpoints - Optional. Array of two Endpoint definitions, one for each end of the Connection. This and 'endpoint' are mutually exclusive parameters.
+ * endpointStyle - Optional. Endpoint style definition to use for both ends of the Connection.
+ * endpointStyles - Optional. Array of two Endpoint style definitions, one for each end of the Connection. This and 'endpoint' are mutually exclusive parameters.
+ * paintStyle - Parameters defining the appearance of the Connection. Optional; jsPlumb will use the defaults if you supply nothing here.
+ * hoverPaintStyle - Parameters defining the appearance of the Connection when the mouse is hovering over it. Optional; jsPlumb will use the defaults if you supply nothing here (note that the default hoverPaintStyle is null).
+ * overlays - Optional array of Overlay definitions to appear on this Connection.
+ * drawEndpoints - if false, instructs jsPlumb to not draw the endpoints for this Connection. Be careful with this: it only really works when you tell jsPlumb to attach elements to the document body. Read the documentation for a full discussion of this.
+ */
+ var Connection = function(params) {
+ var self = this, visible = true;
+ self.idPrefix = "_jsplumb_c_";
+ self.defaultLabelLocation = 0.5;
+ self.defaultOverlayKeys = ["Overlays", "ConnectionOverlays"];
+ this.parent = params.parent;
+ overlayCapableJsPlumbUIComponent.apply(this, arguments);
+ // ************** get the source and target and register the connection. *******************
+
+ /**
+ Function:isVisible
+ Returns whether or not the Connection is currently visible.
+ */
+ this.isVisible = function() { return visible; };
+ /**
+ Function: setVisible
+ Sets whether or not the Connection should be visible.
+
+ Parameters:
+ visible - boolean indicating desired visible state.
+ */
+ this.setVisible = function(v) {
+ visible = v;
+ self[v ? "showOverlays" : "hideOverlays"]();
+ if (self.connector && self.connector.canvas) self.connector.canvas.style.display = v ? "block" : "none";
+ };
+
+ /**
+ Property: source
+ The source element for this Connection.
+ */
+ this.source = _getElementObject(params.source);
+ /**
+ Property:target
+ The target element for this Connection.
+ */
+ this.target = _getElementObject(params.target);
+ // sourceEndpoint and targetEndpoint override source/target, if they are present. but
+ // source is not overridden if the Endpoint has declared it is not the final target of a connection;
+ // instead we use the source that the Endpoint declares will be the final source element.
+ if (params.sourceEndpoint) {
+ this.source = params.sourceEndpoint.endpointWillMoveTo || params.sourceEndpoint.getElement();
+ }
+ if (params.targetEndpoint) this.target = params.targetEndpoint.getElement();
+
+ // if a new connection is the result of moving some existing connection, params.previousConnection
+ // will have that Connection in it. listeners for the jsPlumbConnection event can look for that
+ // member and take action if they need to.
+ self.previousConnection = params.previousConnection;
+
+ var _cost = params.cost;
+ self.getCost = function() { return _cost; };
+ self.setCost = function(c) { _cost = c; };
+
+ var _bidirectional = params.bidirectional === false ? false : true;
+ self.isBidirectional = function() { return _bidirectional; };
+
+ /*
+ * Property: sourceId
+ * Id of the source element in the connection.
+ */
+ this.sourceId = _getAttribute(this.source, "id");
+ /*
+ * Property: targetId
+ * Id of the target element in the connection.
+ */
+ this.targetId = _getAttribute(this.target, "id");
+
+ /**
+ * implementation of abstract method in EventGenerator
+ * @return list of attached elements. in our case, a list of Endpoints.
+ */
+ this.getAttachedElements = function() {
+ return self.endpoints;
+ };
+
+ /*
+ * Property: scope
+ * Optional scope descriptor for the connection.
+ */
+ this.scope = params.scope; // scope may have been passed in to the connect call. if it wasn't, we will pull it from the source endpoint, after having initialised the endpoints.
+ /*
+ * Property: endpoints
+ * Array of [source, target] Endpoint objects.
+ */
+ this.endpoints = [];
+ this.endpointStyles = [];
+ // wrapped the main function to return null if no input given. this lets us cascade defaults properly.
+ var _makeAnchor = function(anchorParams, elementId) {
+ if (anchorParams)
+ return _currentInstance.makeAnchor(anchorParams, elementId, _currentInstance);
+ },
+ prepareEndpoint = function(existing, index, params, element, elementId, connectorPaintStyle, connectorHoverPaintStyle) {
+ if (existing) {
+ self.endpoints[index] = existing;
+ existing.addConnection(self);
+ } else {
+ if (!params.endpoints) params.endpoints = [ null, null ];
+ var ep = params.endpoints[index]
+ || params.endpoint
+ || _currentInstance.Defaults.Endpoints[index]
+ || jsPlumb.Defaults.Endpoints[index]
+ || _currentInstance.Defaults.Endpoint
+ || jsPlumb.Defaults.Endpoint;
+
+ if (!params.endpointStyles) params.endpointStyles = [ null, null ];
+ if (!params.endpointHoverStyles) params.endpointHoverStyles = [ null, null ];
+ var es = params.endpointStyles[index] || params.endpointStyle || _currentInstance.Defaults.EndpointStyles[index] || jsPlumb.Defaults.EndpointStyles[index] || _currentInstance.Defaults.EndpointStyle || jsPlumb.Defaults.EndpointStyle;
+ // Endpoints derive their fillStyle from the connector's strokeStyle, if no fillStyle was specified.
+ if (es.fillStyle == null && connectorPaintStyle != null)
+ es.fillStyle = connectorPaintStyle.strokeStyle;
+
+ // TODO: decide if the endpoint should derive the connection's outline width and color. currently it does:
+ //*
+ if (es.outlineColor == null && connectorPaintStyle != null)
+ es.outlineColor = connectorPaintStyle.outlineColor;
+ if (es.outlineWidth == null && connectorPaintStyle != null)
+ es.outlineWidth = connectorPaintStyle.outlineWidth;
+ //*/
+
+ var ehs = params.endpointHoverStyles[index] || params.endpointHoverStyle || _currentInstance.Defaults.EndpointHoverStyles[index] || jsPlumb.Defaults.EndpointHoverStyles[index] || _currentInstance.Defaults.EndpointHoverStyle || jsPlumb.Defaults.EndpointHoverStyle;
+ // endpoint hover fill style is derived from connector's hover stroke style. TODO: do we want to do this by default? for sure?
+ if (connectorHoverPaintStyle != null) {
+ if (ehs == null) ehs = {};
+ if (ehs.fillStyle == null) {
+ ehs.fillStyle = connectorHoverPaintStyle.strokeStyle;
+ }
+ }
+ var a = params.anchors ? params.anchors[index] :
+ params.anchor ? params.anchor :
+ _makeAnchor(_currentInstance.Defaults.Anchors[index], elementId) ||
+ _makeAnchor(jsPlumb.Defaults.Anchors[index], elementId) ||
+ _makeAnchor(_currentInstance.Defaults.Anchor, elementId) ||
+ _makeAnchor(jsPlumb.Defaults.Anchor, elementId),
+ u = params.uuids ? params.uuids[index] : null,
+ e = _newEndpoint({
+ paintStyle : es,
+ hoverPaintStyle:ehs,
+ endpoint : ep,
+ connections : [ self ],
+ uuid : u,
+ anchor : a,
+ source : element,
+ scope : params.scope,
+ container:params.container,
+ reattach:params.reattach,
+ detachable:params.detachable
+ });
+ self.endpoints[index] = e;
+
+
+ if (params.drawEndpoints === false) e.setVisible(false, true, true);
+
+ return e;
+ }
+ };
+
+ var eS = prepareEndpoint(params.sourceEndpoint,
+ 0,
+ params,
+ self.source,
+ self.sourceId,
+ params.paintStyle,
+ params.hoverPaintStyle);
+ if (eS) _addToList(endpointsByElement, this.sourceId, eS);
+
+ // if there were no endpoints supplied and the source element is the target element, we will reuse the source
+ // endpoint that was just created.
+ var existingTargetEndpoint = ((self.sourceId == self.targetId) && params.targetEndpoint == null) ? eS : params.targetEndpoint,
+ eT = prepareEndpoint(existingTargetEndpoint,
+ 1,
+ params,
+ self.target,
+ self.targetId,
+ params.paintStyle,
+ params.hoverPaintStyle);
+ if (eT) _addToList(endpointsByElement, this.targetId, eT);
+ // if scope not set, set it to be the scope for the source endpoint.
+ if (!this.scope) this.scope = this.endpoints[0].scope;
+
+ // if delete endpoints on detach, keep a record of just exactly which endpoints they are.
+ if (params.deleteEndpointsOnDetach)
+ self.endpointsToDeleteOnDetach = [eS, eT];
+
+ var _detachable = _currentInstance.Defaults.ConnectionsDetachable;
+ if (params.detachable === false) _detachable = false;
+ if(self.endpoints[0].connectionsDetachable === false) _detachable = false;
+ if(self.endpoints[1].connectionsDetachable === false) _detachable = false;
+
+ // inherit connectin cost if it was set on source endpoint
+ if (_cost == null) _cost = self.endpoints[0].getConnectionCost();
+ // inherit bidirectional flag if set no source endpoint
+ if (params.bidirectional == null) _bidirectional = self.endpoints[0].areConnectionsBidirectional();
+
+ /*
+ Function: isDetachable
+ Returns whether or not this connection can be detached from its target/source endpoint. by default this
+ is false; use it in conjunction with the 'reattach' parameter.
+ */
+ this.isDetachable = function() {
+ return _detachable === true;
+ };
+
+ /*
+ Function: setDetachable
+ Sets whether or not this connection is detachable.
+ */
+ this.setDetachable = function(detachable) {
+ _detachable = detachable === true;
+ };
+
+ // merge all the parameters objects into the connection. parameters set
+ // on the connection take precedence; then target endpoint params, then
+ // finally source endpoint params.
+ // TODO jsPlumb.extend could be made to take more than two args, and it would
+ // apply the second through nth args in order.
+ var _p = jsPlumb.extend({}, this.endpoints[0].getParameters());
+ jsPlumb.extend(_p, this.endpoints[1].getParameters());
+ jsPlumb.extend(_p, self.getParameters());
+ self.setParameters(_p);
+
+ // override setHover to pass it down to the underlying connector
+ var _sh = self.setHover;
+
+ self.setHover = function() {
+ self.connector.setHover.apply(self.connector, arguments);
+ _sh.apply(self, arguments);
+ };
+
+ var _internalHover = function(state) {
+ if (_connectionBeingDragged == null) {
+ self.setHover(state, false);
+ }
+ };
+
+ /*
+ * Function: setConnector
+ * Sets the Connection's connector (eg "Bezier", "Flowchart", etc). You pass a Connector definition into this method - the same
+ * thing that you would set as the 'connector' property on a jsPlumb.connect call.
+ *
+ * Parameters:
+ * connector - Connector definition
+ */
+ this.setConnector = function(connector, doNotRepaint) {
+ if (self.connector != null) _removeElements(self.connector.getDisplayElements(), self.parent);
+ var connectorArgs = {
+ _jsPlumb:self._jsPlumb,
+ parent:params.parent,
+ cssClass:params.cssClass,
+ container:params.container,
+ tooltip:self.tooltip
+ };
+ if (_isString(connector))
+ this.connector = new jsPlumb.Connectors[renderMode][connector](connectorArgs); // lets you use a string as shorthand.
+ else if (_isArray(connector))
+ this.connector = new jsPlumb.Connectors[renderMode][connector[0]](jsPlumb.extend(connector[1], connectorArgs));
+ self.canvas = self.connector.canvas;
+ // binds mouse listeners to the current connector.
+ _bindListeners(self.connector, self, _internalHover);
+ if (!doNotRepaint) self.repaint();
+ };
+ /*
+ * Property: connector
+ * The underlying Connector for this Connection (eg. a Bezier connector, straight line connector, flowchart connector etc)
+ */
+
+ self.setConnector(this.endpoints[0].connector ||
+ this.endpoints[1].connector ||
+ params.connector ||
+ _currentInstance.Defaults.Connector ||
+ jsPlumb.Defaults.Connector, true);
+
+ this.setPaintStyle(this.endpoints[0].connectorStyle ||
+ this.endpoints[1].connectorStyle ||
+ params.paintStyle ||
+ _currentInstance.Defaults.PaintStyle ||
+ jsPlumb.Defaults.PaintStyle, true);
+
+ this.setHoverPaintStyle(this.endpoints[0].connectorHoverStyle ||
+ this.endpoints[1].connectorHoverStyle ||
+ params.hoverPaintStyle ||
+ _currentInstance.Defaults.HoverPaintStyle ||
+ jsPlumb.Defaults.HoverPaintStyle, true);
+
+ this.paintStyleInUse = this.paintStyle;
+
+
+ this.moveParent = function(newParent) {
+ var jpcl = jsPlumb.CurrentLibrary, curParent = jpcl.getParent(self.connector.canvas);
+ jpcl.removeElement(self.connector.canvas, curParent);
+ jpcl.appendElement(self.connector.canvas, newParent);
+ if (self.connector.bgCanvas) {
+ jpcl.removeElement(self.connector.bgCanvas, curParent);
+ jpcl.appendElement(self.connector.bgCanvas, newParent);
+ }
+ // this only applies for DOMOverlays
+ for (var i = 0; i < self.overlays.length; i++) {
+ if (self.overlays[i].isAppendedAtTopLevel) {
+ jpcl.removeElement(self.overlays[i].canvas, curParent);
+ jpcl.appendElement(self.overlays[i].canvas, newParent);
+ if (self.overlays[i].reattachListeners)
+ self.overlays[i].reattachListeners(self.connector);
+ }
+ }
+ if (self.connector.reattachListeners) // this is for SVG/VML; change an element's parent and you have to reinit its listeners.
+ self.connector.reattachListeners(); // the Canvas implementation doesn't have to care about this
+ };
+
+// ***************************** PLACEHOLDERS FOR NATURAL DOCS *************************************************
+ /*
+ * Function: bind
+ * Bind to an event on the Connection.
+ *
+ * Parameters:
+ * event - the event to bind. Available events on a Connection are:
+ * - *click* : notification that a Connection was clicked.
+ * - *dblclick* : notification that a Connection was double clicked.
+ * - *mouseenter* : notification that the mouse is over a Connection.
+ * - *mouseexit* : notification that the mouse exited a Connection.
+ *
+ * callback - function to callback. This function will be passed the Connection that caused the event, and also the original event.
+ */
+
+ /*
+ * Function: setPaintStyle
+ * Sets the Connection's paint style and then repaints the Connection.
+ *
+ * Parameters:
+ * style - Style to use.
+ */
+
+ /*
+ * Function: setHoverPaintStyle
+ * Sets the paint style to use when the mouse is hovering over the Connection. This is null by default.
+ * The hover paint style is applied as extensions to the paintStyle; it does not entirely replace
+ * it. This is because people will most likely want to change just one thing when hovering, say the
+ * color for example, but leave the rest of the appearance the same.
+ *
+ * Parameters:
+ * style - Style to use when the mouse is hovering.
+ * doNotRepaint - if true, the Connection will not be repainted. useful when setting things up initially.
+ */
+
+ /*
+ * Function: setHover
+ * Sets/unsets the hover state of this Connection.
+ *
+ * Parameters:
+ * hover - hover state boolean
+ * ignoreAttachedElements - if true, does not notify any attached elements of the change in hover state. used mostly to avoid infinite loops.
+ */
+
+// ***************************** END OF PLACEHOLDERS FOR NATURAL DOCS *************************************************
+
+ _updateOffset( { elId : this.sourceId });
+ _updateOffset( { elId : this.targetId });
+
+ // paint the endpoints
+ var myOffset = offsets[this.sourceId], myWH = sizes[this.sourceId],
+ otherOffset = offsets[this.targetId],
+ otherWH = sizes[this.targetId],
+ initialTimestamp = _timestamp(),
+ anchorLoc = this.endpoints[0].anchor.compute( {
+ xy : [ myOffset.left, myOffset.top ], wh : myWH, element : this.endpoints[0],
+ elementId:this.endpoints[0].elementId,
+ txy : [ otherOffset.left, otherOffset.top ], twh : otherWH, tElement : this.endpoints[1],
+ timestamp:initialTimestamp
+ });
+
+ this.endpoints[0].paint( { anchorLoc : anchorLoc, timestamp:initialTimestamp });
+
+ anchorLoc = this.endpoints[1].anchor.compute( {
+ xy : [ otherOffset.left, otherOffset.top ], wh : otherWH, element : this.endpoints[1],
+ elementId:this.endpoints[1].elementId,
+ txy : [ myOffset.left, myOffset.top ], twh : myWH, tElement : this.endpoints[0],
+ timestamp:initialTimestamp
+ });
+ this.endpoints[1].paint({ anchorLoc : anchorLoc, timestamp:initialTimestamp });
+
+ /*
+ * Paints the Connection. Not exposed for public usage.
+ *
+ * Parameters:
+ * elId - Id of the element that is in motion.
+ * ui - current library's event system ui object (present if we came from a drag to get here).
+ * recalc - whether or not to recalculate all anchors etc before painting.
+ * timestamp - timestamp of this paint. If the Connection was last painted with the same timestamp, it does not paint again.
+ */
+ this.paint = function(params) {
+ params = params || {};
+ var elId = params.elId, ui = params.ui, recalc = params.recalc, timestamp = params.timestamp,
+ // if the moving object is not the source we must transpose the two references.
+ swap = false,
+ tId = swap ? this.sourceId : this.targetId, sId = swap ? this.targetId : this.sourceId,
+ tIdx = swap ? 0 : 1, sIdx = swap ? 1 : 0;
+
+ var sourceInfo = _updateOffset( { elId : elId, offset : ui, recalc : recalc, timestamp : timestamp }),
+ targetInfo = _updateOffset( { elId : tId, timestamp : timestamp }); // update the target if this is a forced repaint. otherwise, only the source has been moved.
+
+ var sE = this.endpoints[sIdx], tE = this.endpoints[tIdx],
+ sAnchorP = sE.anchor.getCurrentLocation(sE),
+ tAnchorP = tE.anchor.getCurrentLocation(tE);
+
+ /* paint overlays*/
+ var maxSize = 0;
+ for ( var i = 0; i < self.overlays.length; i++) {
+ var o = self.overlays[i];
+ if (o.isVisible()) maxSize = Math.max(maxSize, o.computeMaxSize(self.connector));
+ }
+
+ var dim = this.connector.compute(sAnchorP, tAnchorP,
+ this.endpoints[sIdx], this.endpoints[tIdx],
+ this.endpoints[sIdx].anchor, this.endpoints[tIdx].anchor,
+ self.paintStyleInUse.lineWidth, maxSize,
+ sourceInfo,
+ targetInfo);
+
+ self.connector.paint(dim, self.paintStyleInUse);
+
+ /* paint overlays*/
+ for ( var i = 0; i < self.overlays.length; i++) {
+ var o = self.overlays[i];
+ if (o.isVisible) self.overlayPlacements[i] = o.draw(self.connector, self.paintStyleInUse, dim);
+ }
+ };
+
+ /*
+ * Function: repaint
+ * Repaints the Connection.
+ */
+ this.repaint = function(params) {
+ params = params || {};
+ var recalc = !(params.recalc === false);
+ this.paint({ elId : this.sourceId, recalc : recalc, timestamp:params.timestamp });
+ };
+
+ };
+
+// ENDPOINT HELPER FUNCTIONS
+ var _makeConnectionDragHandler = function(placeholder) {
+ var stopped = false;
+ return {
+
+ drag : function() {
+ if (stopped) {
+ stopped = false;
+ return true;
+ }
+ var _ui = jsPlumb.CurrentLibrary.getUIPosition(arguments),
+ el = placeholder.element;
+ if (el) {
+ jsPlumb.CurrentLibrary.setOffset(el, _ui);
+ _draw(_getElementObject(el), _ui);
+ }
+ },
+ stopDrag : function() {
+ stopped = true;
+ }
+ };
+ };
+
+ var _makeFloatingEndpoint = function(paintStyle, referenceAnchor, endpoint, referenceCanvas, sourceElement) {
+ var floatingAnchor = new FloatingAnchor( { reference : referenceAnchor, referenceCanvas : referenceCanvas });
+
+ //setting the scope here should not be the way to fix that mootools issue. it should be fixed by not
+ // adding the floating endpoint as a droppable. that makes more sense anyway!
+
+ return _newEndpoint({ paintStyle : paintStyle, endpoint : endpoint, anchor : floatingAnchor, source : sourceElement, scope:"__floating" });
+ };
+
+ /**
+ * creates a placeholder div for dragging purposes, adds it to the DOM, and pre-computes its offset. then returns
+ * both the element id and a selector for the element.
+ */
+ var _makeDraggablePlaceholder = function(placeholder, parent) {
+ var n = document.createElement("div");
+ n.style.position = "absolute";
+ var placeholderDragElement = _getElementObject(n);
+ _appendElement(n, parent);
+ var id = _getId(placeholderDragElement);
+ _updateOffset( { elId : id });
+ // create and assign an id, and initialize the offset.
+ placeholder.id = id;
+ placeholder.element = placeholderDragElement;
+ };
+
+ /*
+ * Class: Endpoint
+ *
+ * Models an endpoint. Can have 1 to 'maxConnections' Connections emanating from it (set maxConnections to -1
+ * to allow unlimited). Typically, if you use 'jsPlumb.connect' to programmatically connect two elements, you won't
+ * actually deal with the underlying Endpoint objects. But if you wish to support drag and drop Connections, one of the ways you
+ * do so is by creating and registering Endpoints using 'jsPlumb.addEndpoint', and marking these Endpoints as 'source' and/or
+ * 'target' Endpoints for Connections.
+ *
+ *
+ */
+
+ /*
+ * Function: Endpoint
+ *
+ * Endpoint constructor.
+ *
+ * Parameters:
+ * anchor - definition of the Anchor for the endpoint. You can include one or more Anchor definitions here; if you include more than one, jsPlumb creates a 'dynamic' Anchor, ie. an Anchor which changes position relative to the other elements in a Connection. Each Anchor definition can be either a string nominating one of the basic Anchors provided by jsPlumb (eg. "TopCenter"), or a four element array that designates the Anchor's location and orientation (eg, and this is equivalent to TopCenter, [ 0.5, 0, 0, -1 ]). To provide more than one Anchor definition just put them all in an array. You can mix string definitions with array definitions.
+ * endpoint - optional Endpoint definition. This takes the form of either a string nominating one of the basic Endpoints provided by jsPlumb (eg. "Rectangle"), or an array containing [name,params] for those cases where you don't wish to use the default values, eg. [ "Rectangle", { width:5, height:10 } ].
+ * enabled - optional, defaults to true. Indicates whether or not the Endpoint should be enabled for mouse events (drag/drop).
+ * paintStyle - endpoint style, a js object. may be null.
+ * hoverPaintStyle - style to use when the mouse is hovering over the Endpoint. A js object. may be null; defaults to null.
+ * source - element the Endpoint is attached to, of type String (an element id) or element selector. Required.
+ * canvas - canvas element to use. may be, and most often is, null.
+ * container - optional id or selector instructing jsPlumb where to attach the element it creates for this endpoint. you should read the documentation for a full discussion of this.
+ * connections - optional list of Connections to configure the Endpoint with.
+ * isSource - boolean. indicates the endpoint can act as a source of new connections. Optional; defaults to false.
+ * maxConnections - integer; defaults to 1. a value of -1 means no upper limit.
+ * dragOptions - if isSource is set to true, you can supply arguments for the underlying library's drag method. Optional; defaults to null.
+ * connectorStyle - if isSource is set to true, this is the paint style for Connections from this Endpoint. Optional; defaults to null.
+ * connectorHoverStyle - if isSource is set to true, this is the hover paint style for Connections from this Endpoint. Optional; defaults to null.
+ * connector - optional Connector type to use. Like 'endpoint', this may be either a single string nominating a known Connector type (eg. "Bezier", "Straight"), or an array containing [name, params], eg. [ "Bezier", { curviness:160 } ].
+ * connectorOverlays - optional array of Overlay definitions that will be applied to any Connection from this Endpoint.
+ * isTarget - boolean. indicates the endpoint can act as a target of new connections. Optional; defaults to false.
+ * dropOptions - if isTarget is set to true, you can supply arguments for the underlying library's drop method with this parameter. Optional; defaults to null.
+ * reattach - optional boolean that determines whether or not the Connections reattach after they have been dragged off an Endpoint and left floating. defaults to false: Connections dropped in this way will just be deleted.
+ */
+ var Endpoint = function(params) {
+ var self = this;
+ self.idPrefix = "_jsplumb_e_";
+ self.defaultLabelLocation = [ 0.5, 0.5 ];
+ self.defaultOverlayKeys = ["Overlays", "EndpointOverlays"];
+ this.parent = params.parent;
+ overlayCapableJsPlumbUIComponent.apply(this, arguments);
+ params = params || {};
+
+// ***************************** PLACEHOLDERS FOR NATURAL DOCS *************************************************
+ /*
+ * Function: bind
+ * Bind to an event on the Endpoint.
+ *
+ * Parameters:
+ * event - the event to bind. Available events on an Endpoint are:
+ * - *click* : notification that a Endpoint was clicked.
+ * - *dblclick* : notification that a Endpoint was double clicked.
+ * - *mouseenter* : notification that the mouse is over a Endpoint.
+ * - *mouseexit* : notification that the mouse exited a Endpoint.
+ *
+ * callback - function to callback. This function will be passed the Endpoint that caused the event, and also the original event.
+ */
+
+ /*
+ * Function: setPaintStyle
+ * Sets the Endpoint's paint style and then repaints the Endpoint.
+ *
+ * Parameters:
+ * style - Style to use.
+ */
+
+ /*
+ * Function: setHoverPaintStyle
+ * Sets the paint style to use when the mouse is hovering over the Endpoint. This is null by default.
+ * The hover paint style is applied as extensions to the paintStyle; it does not entirely replace
+ * it. This is because people will most likely want to change just one thing when hovering, say the
+ * color for example, but leave the rest of the appearance the same.
+ *
+ * Parameters:
+ * style - Style to use when the mouse is hovering.
+ * doNotRepaint - if true, the Endpoint will not be repainted. useful when setting things up initially.
+ */
+
+ /*
+ * Function: setHover
+ * Sets/unsets the hover state of this Endpoint.
+ *
+ * Parameters:
+ * hover - hover state boolean
+ * ignoreAttachedElements - if true, does not notify any attached elements of the change in hover state. used mostly to avoid infinite loops.
+ */
+
+// ***************************** END OF PLACEHOLDERS FOR NATURAL DOCS *************************************************
+
+ var visible = true, __enabled = !(params.enabled === false);
+ /*
+ Function: isVisible
+ Returns whether or not the Endpoint is currently visible.
+ */
+ this.isVisible = function() { return visible; };
+ /*
+ Function: setVisible
+ Sets whether or not the Endpoint is currently visible.
+
+ Parameters:
+ visible - whether or not the Endpoint should be visible.
+ doNotChangeConnections - Instructs jsPlumb to not pass the visible state on to any attached Connections. defaults to false.
+ doNotNotifyOtherEndpoint - Instructs jsPlumb to not pass the visible state on to Endpoints at the other end of any attached Connections. defaults to false.
+ */
+ this.setVisible = function(v, doNotChangeConnections, doNotNotifyOtherEndpoint) {
+ visible = v;
+ if (self.canvas) self.canvas.style.display = v ? "block" : "none";
+ self[v ? "showOverlays" : "hideOverlays"]();
+ if (!doNotChangeConnections) {
+ for (var i = 0; i < self.connections.length; i++) {
+ self.connections[i].setVisible(v);
+ if (!doNotNotifyOtherEndpoint) {
+ var oIdx = self === self.connections[i].endpoints[0] ? 1 : 0;
+ // only change the other endpoint if this is its only connection.
+ if (self.connections[i].endpoints[oIdx].connections.length == 1) self.connections[i].endpoints[oIdx].setVisible(v, true, true);
+ }
+ }
+ }
+ };
+
+ /*
+ Function: isEnabled
+ Returns whether or not the Endpoint is enabled for drag/drop connections.
+ */
+ this.isEnabled = function() { return __enabled; };
+
+ /*
+ Function: setEnabled
+ Sets whether or not the Endpoint is enabled for drag/drop connections.
+ */
+ this.setEnabled = function(e) { __enabled = e; };
+
+ var _element = params.source, _uuid = params.uuid, floatingEndpoint = null, inPlaceCopy = null;
+ if (_uuid) endpointsByUUID[_uuid] = self;
+ var _elementId = _getAttribute(_element, "id");
+ this.elementId = _elementId;
+ this.element = _element;
+
+ var _connectionCost = params.connectionCost;
+ this.getConnectionCost = function() { return _connectionCost; };
+ this.setConnectionCost = function(c) {
+ _connectionCost = c;
+ };
+
+ var _connectionsBidirectional = params.connectionsBidirectional === false ? false : true;
+ this.areConnectionsBidirectional = function() { return _connectionsBidirectional; };
+ this.setConnectionsBidirectional = function(b) { _connectionsBidirectional = b; };
+
+ self.anchor = params.anchor ? _currentInstance.makeAnchor(params.anchor, _elementId, _currentInstance) : params.anchors ? _currentInstance.makeAnchor(params.anchors, _elementId, _currentInstance) : _currentInstance.makeAnchor("TopCenter", _elementId, _currentInstance);
+
+ // ANCHOR MANAGER
+ if (!params._transient) // in place copies, for example, are transient. they will never need to be retrieved during a paint cycle, because they dont move, and then they are deleted.
+ _currentInstance.anchorManager.add(self, _elementId);
+
+ var _endpoint = params.endpoint || _currentInstance.Defaults.Endpoint || jsPlumb.Defaults.Endpoint || "Dot",
+ endpointArgs = {
+ _jsPlumb:self._jsPlumb,
+ parent:params.parent,
+ container:params.container,
+ tooltip:params.tooltip,
+ connectorTooltip:params.connectorTooltip,
+ endpoint:self
+ };
+ if (_isString(_endpoint))
+ _endpoint = new jsPlumb.Endpoints[renderMode][_endpoint](endpointArgs);
+ else if (_isArray(_endpoint)) {
+ endpointArgs = jsPlumb.extend(_endpoint[1], endpointArgs);
+ _endpoint = new jsPlumb.Endpoints[renderMode][_endpoint[0]](endpointArgs);
+ }
+ else {
+ _endpoint = _endpoint.clone();
+ }
+
+ // assign a clone function using a copy of endpointArgs. this is used when a drag starts: the endpoint that was dragged is cloned,
+ // and the clone is left in its place while the original one goes off on a magical journey.
+ // the copy is to get around a closure problem, in which endpointArgs ends up getting shared by
+ // the whole world.
+ var argsForClone = jsPlumb.extend({}, endpointArgs);
+ _endpoint.clone = function() {
+ var o = new Object();
+ _endpoint.constructor.apply(o, [argsForClone]);
+ return o;
+ };
+
+ self.endpoint = _endpoint;
+ self.type = self.endpoint.type;
+ // override setHover to pass it down to the underlying endpoint
+ var _sh = self.setHover;
+ self.setHover = function() {
+ self.endpoint.setHover.apply(self.endpoint, arguments);
+ _sh.apply(self, arguments);
+ };
+ // endpoint delegates to first connection for hover, if there is one.
+ var internalHover = function(state) {
+ if (self.connections.length > 0)
+ self.connections[0].setHover(state, false);
+ else
+ self.setHover(state);
+ };
+
+ // bind listeners from endpoint to self, with the internal hover function defined above.
+ _bindListeners(self.endpoint, self, internalHover);
+
+ this.setPaintStyle(params.paintStyle ||
+ params.style ||
+ _currentInstance.Defaults.EndpointStyle ||
+ jsPlumb.Defaults.EndpointStyle, true);
+ this.setHoverPaintStyle(params.hoverPaintStyle ||
+ _currentInstance.Defaults.EndpointHoverStyle ||
+ jsPlumb.Defaults.EndpointHoverStyle, true);
+ this.paintStyleInUse = this.paintStyle;
+ this.connectorStyle = params.connectorStyle;
+ this.connectorHoverStyle = params.connectorHoverStyle;
+ this.connectorOverlays = params.connectorOverlays;
+ this.connector = params.connector;
+ this.connectorTooltip = params.connectorTooltip;
+ this.isSource = params.isSource || false;
+ this.isTarget = params.isTarget || false;
+
+ var _maxConnections = params.maxConnections || _currentInstance.Defaults.MaxConnections; // maximum number of connections this endpoint can be the source of.
+
+ this.getAttachedElements = function() {
+ return self.connections;
+ };
+
+ /*
+ * Property: canvas
+ * The Endpoint's Canvas.
+ */
+ this.canvas = this.endpoint.canvas;
+ /*
+ * Property: connections
+ * List of Connections this Endpoint is attached to.
+ */
+ this.connections = params.connections || [];
+ /*
+ * Property: scope
+ * Scope descriptor for this Endpoint.
+ */
+ this.scope = params.scope || DEFAULT_SCOPE;
+ this.timestamp = null;
+ self.isReattach = params.reattach || false;
+ self.connectionsDetachable = _currentInstance.Defaults.ConnectionsDetachable;
+ if (params.connectionsDetachable === false || params.detachable === false)
+ self.connectionsDetachable = false;
+ var dragAllowedWhenFull = params.dragAllowedWhenFull || true;
+
+ this.computeAnchor = function(params) {
+ return self.anchor.compute(params);
+ };
+ /*
+ * Function: addConnection
+ * Adds a Connection to this Endpoint.
+ *
+ * Parameters:
+ * connection - the Connection to add.
+ */
+ this.addConnection = function(connection) {
+ self.connections.push(connection);
+ };
+ /*
+ * Function: detach
+ * Detaches the given Connection from this Endpoint.
+ *
+ * Parameters:
+ * connection - the Connection to detach.
+ * ignoreTarget - optional; tells the Endpoint to not notify the Connection target that the Connection was detached. The default behaviour is to notify the target.
+ */
+ this.detach = function(connection, ignoreTarget, forceDetach, fireEvent, originalEvent) {
+ var idx = _findWithFunction(self.connections, function(c) { return c.id == connection.id}),
+ actuallyDetached = false;
+ fireEvent = (fireEvent !== false);
+ if (idx >= 0) {
+ // 1. does the connection have a before detach (note this also checks jsPlumb's bound
+ // detach handlers; but then Endpoint's check will, too, hmm.)
+ if (forceDetach || connection._forceDetach || connection.isDetachable() || connection.isDetachAllowed(connection)) {
+ // get the target endpoint
+ var t = connection.endpoints[0] == self ? connection.endpoints[1] : connection.endpoints[0];
+ // it would be nice to check with both endpoints that it is ok to detach. but
+ // for this we'll have to get a bit fancier: right now if you use the same beforeDetach
+ // interceptor for two endpoints (which is kind of common, because it's part of the
+ // endpoint definition), then it gets fired twice. so in fact we need to loop through
+ // each beforeDetach and see if it returns false, at which point we exit. but if it
+ // returns true, we have to check the next one. however we need to track which ones
+ // have already been run, and not run them again.
+ if (forceDetach || connection._forceDetach || (self.isDetachAllowed(connection) /*&& t.isDetachAllowed(connection)*/)) {
+
+ self.connections.splice(idx, 1);
+
+ // this avoids a circular loop
+ if (!ignoreTarget) {
+
+ t.detach(connection, true, forceDetach);
+ // check connection to see if we want to delete the endpoints associated with it.
+ // we only detach those that have just this connection; this scenario is most
+ // likely if we got to this bit of code because it is set by the methods that
+ // create their own endpoints, like .connect or .makeTarget. the user is
+ // not likely to have interacted with those endpoints.
+ if (connection.endpointsToDeleteOnDetach){
+ for (var i = 0; i < connection.endpointsToDeleteOnDetach.length; i++) {
+ var cde = connection.endpointsToDeleteOnDetach[i];
+ if (cde && cde.connections.length == 0)
+ _currentInstance.deleteEndpoint(cde);
+ }
+ }
+ }
+ _removeElements(connection.connector.getDisplayElements(), connection.parent);
+ _removeWithFunction(connectionsByScope[connection.scope], function(c) {
+ return c.id == connection.id;
+ });
+ actuallyDetached = true;
+ var doFireEvent = (!ignoreTarget && fireEvent)
+ fireDetachEvent(connection, doFireEvent, originalEvent);
+ }
+ }
+ }
+ return actuallyDetached;
+ };
+
+ /*
+ * Function: detachAll
+ * Detaches all Connections this Endpoint has.
+ *
+ * Parameters:
+ * fireEvent - whether or not to fire the detach event. defaults to false.
+ */
+ this.detachAll = function(fireEvent, originalEvent) {
+ while (self.connections.length > 0) {
+ self.detach(self.connections[0], false, true, fireEvent, originalEvent);
+ }
+ };
+ /*
+ * Function: detachFrom
+ * Removes any connections from this Endpoint that are connected to the given target endpoint.
+ *
+ * Parameters:
+ * targetEndpoint - Endpoint from which to detach all Connections from this Endpoint.
+ * fireEvent - whether or not to fire the detach event. defaults to false.
+ */
+ this.detachFrom = function(targetEndpoint, fireEvent, originalEvent) {
+ var c = [];
+ for ( var i = 0; i < self.connections.length; i++) {
+ if (self.connections[i].endpoints[1] == targetEndpoint
+ || self.connections[i].endpoints[0] == targetEndpoint) {
+ c.push(self.connections[i]);
+ }
+ }
+ for ( var i = 0; i < c.length; i++) {
+ if (self.detach(c[i], false, true, fireEvent, originalEvent))
+ c[i].setHover(false, false);
+ }
+ };
+ /*
+ * Function: detachFromConnection
+ * Detach this Endpoint from the Connection, but leave the Connection alive. Used when dragging.
+ *
+ * Parameters:
+ * connection - Connection to detach from.
+ */
+ this.detachFromConnection = function(connection) {
+ var idx = _findWithFunction(self.connections, function(c) { return c.id == connection.id});
+ if (idx >= 0) {
+ self.connections.splice(idx, 1);
+ }
+ };
+
+ /*
+ * Function: getElement
+ * Returns the DOM element this Endpoint is attached to.
+ */
+ this.getElement = function() {
+ return _element;
+ };
+
+ /*
+ * Function: setElement
+ * Sets the DOM element this Endpoint is attached to.
+ */
+ this.setElement = function(el) {
+
+ // TODO possibly have this object take charge of moving the UI components into the appropriate
+ // parent. this is used only by makeSource right now, and that function takes care of
+ // moving the UI bits and pieces. however it would s
+ var parentId = _getId(el);
+ // remove the endpoint from the list for the current endpoint's element
+ _removeWithFunction(endpointsByElement[self.elementId], function(e) {
+ return e.id == self.id;
+ });
+ _element = _getElementObject(el);
+ _elementId = _getId(_element);
+ self.elementId = _elementId;
+ // need to get the new parent now
+ var newParentElement = _getParentFromParams({source:parentId}),
+ curParent = jpcl.getParent(self.canvas);
+ jpcl.removeElement(self.canvas, curParent);
+ jpcl.appendElement(self.canvas, newParentElement);
+
+ // now move connection(s)...i would expect there to be only one but we will iterate.
+ for (var i = 0; i < self.connections.length; i++) {
+ self.connections[i].moveParent(newParentElement);
+ self.connections[i].sourceId = _elementId;
+ self.connections[i].source = _element;
+ }
+ _addToList(endpointsByElement, parentId, self);
+ //_currentInstance.repaint(parentId);
+
+ };
+
+ /*
+ * Function: getUuid
+ * Returns the UUID for this Endpoint, if there is one. Otherwise returns null.
+ */
+ this.getUuid = function() {
+ return _uuid;
+ };
+ /**
+ * private but must be exposed.
+ */
+ this.makeInPlaceCopy = function() {
+ var loc = self.anchor.getCurrentLocation(self),
+ o = self.anchor.getOrientation(self),
+ inPlaceAnchor = {
+ compute:function() { return [ loc[0], loc[1] ]},
+ getCurrentLocation : function() { return [ loc[0], loc[1] ]},
+ getOrientation:function() { return o; }
+ };
+
+ return _newEndpoint( {
+ anchor : inPlaceAnchor,
+ source : _element,
+ paintStyle : this.paintStyle,
+ endpoint : _endpoint,
+ _transient:true,
+ scope:self.scope
+ });
+ };
+ /*
+ * Function: isConnectedTo
+ * Returns whether or not this endpoint is connected to the given Endpoint.
+ *
+ * Parameters:
+ * endpoint - Endpoint to test.
+ */
+ this.isConnectedTo = function(endpoint) {
+ var found = false;
+ if (endpoint) {
+ for ( var i = 0; i < self.connections.length; i++) {
+ if (self.connections[i].endpoints[1] == endpoint) {
+ found = true;
+ break;
+ }
+ }
+ }
+ return found;
+ };
+
+ /**
+ * private but needs to be exposed.
+ */
+ this.isFloating = function() {
+ return floatingEndpoint != null;
+ };
+
+ /**
+ * returns a connection from the pool; used when dragging starts. just gets the head of the array if it can.
+ */
+ this.connectorSelector = function() {
+ var candidate = self.connections[0];
+ if (self.isTarget && candidate) return candidate;
+ else {
+ return (self.connections.length < _maxConnections) || _maxConnections == -1 ? null : candidate;
+ }
+ };
+
+ /*
+ * Function: isFull
+ * Returns whether or not the Endpoint can accept any more Connections.
+ */
+ this.isFull = function() {
+ return !(self.isFloating() || _maxConnections < 1 || self.connections.length < _maxConnections);
+ };
+ /*
+ * Function: setDragAllowedWhenFull
+ * Sets whether or not connections can be dragged from this Endpoint once it is full. You would use this in a UI in
+ * which you're going to provide some other way of breaking connections, if you need to break them at all. This property
+ * is by default true; use it in conjunction with the 'reattach' option on a connect call.
+ *
+ * Parameters:
+ * allowed - whether drag is allowed or not when the Endpoint is full.
+ */
+ this.setDragAllowedWhenFull = function(allowed) {
+ dragAllowedWhenFull = allowed;
+ };
+ /*
+ * Function: setStyle
+ * Sets the paint style of the Endpoint. This is a JS object of the same form you supply to a jsPlumb.addEndpoint or jsPlumb.connect call.
+ * TODO move setStyle into EventGenerator, remove it from here. is Connection's method currently setPaintStyle ? wire that one up to
+ * setStyle and deprecate it if so.
+ *
+ * Parameters:
+ * style - Style object to set, for example {fillStyle:"blue"}.
+ *
+ * @deprecated use setPaintStyle instead.
+ */
+ this.setStyle = self.setPaintStyle;
+
+ /**
+ * a deep equals check. everything must match, including the anchor,
+ * styles, everything. TODO: finish Endpoint.equals
+ */
+ this.equals = function(endpoint) {
+ return this.anchor.equals(endpoint.anchor);
+ };
+
+ // a helper function that tries to find a connection to the given element, and returns it if so. if elementWithPrecedence is null,
+ // or no connection to it is found, we return the first connection in our list.
+ var findConnectionToUseForDynamicAnchor = function(elementWithPrecedence) {
+ var idx = 0;
+ if (elementWithPrecedence != null) {
+ for (var i = 0; i < self.connections.length; i++) {
+ if (self.connections[i].sourceId == elementWithPrecedence || self.connections[i].targetId == elementWithPrecedence) {
+ idx = i;
+ break;
+ }
+ }
+ }
+
+ return self.connections[idx];
+ };
+
+ /*
+ * Function: paint
+ * Paints the Endpoint, recalculating offset and anchor positions if necessary. This does NOT paint
+ * any of the Endpoint's connections.
+ *
+ * Parameters:
+ * timestamp - optional timestamp advising the Endpoint of the current paint time; if it has painted already once for this timestamp, it will not paint again.
+ * canvas - optional Canvas to paint on. Only used internally by jsPlumb in certain obscure situations.
+ * connectorPaintStyle - paint style of the Connector attached to this Endpoint. Used to get a fillStyle if nothing else was supplied.
+ */
+ this.paint = function(params) {
+ params = params || {};
+ var timestamp = params.timestamp,
+ recalc = !(params.recalc === false);
+ if (!timestamp || self.timestamp !== timestamp) {
+ _updateOffset({ elId:_elementId, timestamp:timestamp, recalc:recalc });
+ var xy = params.offset || offsets[_elementId];
+ if(xy) {
+ var ap = params.anchorPoint,connectorPaintStyle = params.connectorPaintStyle;
+ if (ap == null) {
+ var wh = params.dimensions || sizes[_elementId];
+ if (xy == null || wh == null) {
+ _updateOffset( { elId : _elementId, timestamp : timestamp });
+ xy = offsets[_elementId];
+ wh = sizes[_elementId];
+ }
+ var anchorParams = { xy : [ xy.left, xy.top ], wh : wh, element : self, timestamp : timestamp };
+ if (recalc && self.anchor.isDynamic && self.connections.length > 0) {
+ var c = findConnectionToUseForDynamicAnchor(params.elementWithPrecedence),
+ oIdx = c.endpoints[0] == self ? 1 : 0,
+ oId = oIdx == 0 ? c.sourceId : c.targetId,
+ oOffset = offsets[oId], oWH = sizes[oId];
+ anchorParams.txy = [ oOffset.left, oOffset.top ];
+ anchorParams.twh = oWH;
+ anchorParams.tElement = c.endpoints[oIdx];
+ }
+ ap = self.anchor.compute(anchorParams);
+ }
+
+ var d = _endpoint.compute(ap, self.anchor.getOrientation(_endpoint), self.paintStyleInUse, connectorPaintStyle || self.paintStyleInUse);
+ _endpoint.paint(d, self.paintStyleInUse, self.anchor);
+ self.timestamp = timestamp;
+
+
+ /* paint overlays*/
+ for ( var i = 0; i < self.overlays.length; i++) {
+ var o = self.overlays[i];
+ if (o.isVisible) self.overlayPlacements[i] = o.draw(self.endpoint, self.paintStyleInUse, d);
+ }
+ }
+ }
+ };
+
+ this.repaint = this.paint;
+
+ /**
+ * @deprecated
+ */
+ this.removeConnection = this.detach; // backwards compatibility
+
+ // is this a connection source? we make it draggable and have the
+ // drag listener maintain a connection with a floating endpoint.
+ if (jsPlumb.CurrentLibrary.isDragSupported(_element)) {
+ var placeholderInfo = { id:null, element:null },
+ jpc = null,
+ existingJpc = false,
+ existingJpcParams = null,
+ _dragHandler = _makeConnectionDragHandler(placeholderInfo);
+
+ var start = function() {
+ // drag might have started on an endpoint that is not actually a source, but which has
+ // one or more connections.
+ jpc = self.connectorSelector();
+ var _continue = true;
+ // if not enabled, return
+ if (!self.isEnabled()) _continue = false;
+ // if no connection and we're not a source, return.
+ if (jpc == null && !params.isSource) _continue = false;
+ // otherwise if we're full and not allowed to drag, also return false.
+ if (params.isSource && self.isFull() && !dragAllowedWhenFull) _continue = false;
+ // if the connection was setup as not detachable or one of its endpoints
+ // was setup as connectionsDetachable = false, or Defaults.ConnectionsDetachable
+ // is set to false...
+ if (jpc != null && !jpc.isDetachable()) _continue = false;
+
+ if (_continue === false) {
+ // this is for mootools and yui. returning false from this causes jquery to stop drag.
+ // the events are wrapped in both mootools and yui anyway, but i don't think returning
+ // false from the start callback would stop a drag.
+ if (jsPlumb.CurrentLibrary.stopDrag) jsPlumb.CurrentLibrary.stopDrag();
+ _dragHandler.stopDrag();
+ return false;
+ }
+
+ // if we're not full but there was a connection, make it null. we'll create a new one.
+ if (jpc && !self.isFull() && params.isSource) jpc = null;
+
+ _updateOffset( { elId : _elementId });
+ inPlaceCopy = self.makeInPlaceCopy();
+ inPlaceCopy.paint();
+
+ _makeDraggablePlaceholder(placeholderInfo, self.parent);
+
+ // set the offset of this div to be where 'inPlaceCopy' is, to start with.
+ // TODO merge this code with the code in both Anchor and FloatingAnchor, because it
+ // does the same stuff.
+ var ipcoel = _getElementObject(inPlaceCopy.canvas),
+ ipco = jsPlumb.CurrentLibrary.getOffset(ipcoel),
+ po = adjustForParentOffsetAndScroll([ipco.left, ipco.top], inPlaceCopy.canvas);
+ jsPlumb.CurrentLibrary.setOffset(placeholderInfo.element, {left:po[0], top:po[1]});
+
+ // when using makeSource and a parent, we first draw the source anchor on the source element, then
+ // move it to the parent. note that this happens after drawing the placeholder for the
+ // first time.
+ if (self.parentAnchor) self.anchor = _currentInstance.makeAnchor(self.parentAnchor, self.elementId, _currentInstance);
+
+
+ // store the id of the dragging div and the source element. the drop function will pick these up.
+ _setAttribute(_getElementObject(self.canvas), "dragId", placeholderInfo.id);
+ _setAttribute(_getElementObject(self.canvas), "elId", _elementId);
+ // create a floating anchor
+ floatingEndpoint = _makeFloatingEndpoint(self.paintStyle, self.anchor, _endpoint, self.canvas, placeholderInfo.element);
+
+ if (jpc == null) {
+ self.anchor.locked = true;
+ self.setHover(false, false);
+ // TODO the hover call above does not reset any target endpoint's hover
+ // states.
+ // create a connection. one end is this endpoint, the other is a floating endpoint.
+ jpc = _newConnection({
+ sourceEndpoint : self,
+ targetEndpoint : floatingEndpoint,
+ source : self.endpointWillMoveTo || _getElementObject(_element), // for makeSource with parent option. ensure source element is represented correctly.
+ target : placeholderInfo.element,
+ anchors : [ self.anchor, floatingEndpoint.anchor ],
+ paintStyle : params.connectorStyle, // this can be null. Connection will use the default.
+ hoverPaintStyle:params.connectorHoverStyle,
+ connector : params.connector, // this can also be null. Connection will use the default.
+ overlays : params.connectorOverlays
+ });
+
+ } else {
+ existingJpc = true;
+ jpc.connector.setHover(false, false);
+ // if existing connection, allow to be dropped back on the source endpoint (issue 51).
+ _initDropTarget(_getElementObject(inPlaceCopy.canvas), false, true);
+ // new anchor idx
+ var anchorIdx = jpc.endpoints[0].id == self.id ? 0 : 1;
+ jpc.floatingAnchorIndex = anchorIdx; // save our anchor index as the connection's floating index.
+ self.detachFromConnection(jpc); // detach from the connection while dragging is occurring.
+
+ // store the original scope (issue 57)
+ var c = _getElementObject(self.canvas),
+ dragScope = jsPlumb.CurrentLibrary.getDragScope(c);
+ _setAttribute(c, "originalScope", dragScope);
+ // now we want to get this endpoint's DROP scope, and set it for now: we can only be dropped on drop zones
+ // that have our drop scope (issue 57).
+ var dropScope = jsPlumb.CurrentLibrary.getDropScope(c);
+ jsPlumb.CurrentLibrary.setDragScope(c, dropScope);
+
+ // now we replace ourselves with the temporary div we created above:
+ if (anchorIdx == 0) {
+ existingJpcParams = [ jpc.source, jpc.sourceId, i, dragScope ];
+ jpc.source = placeholderInfo.element;
+ jpc.sourceId = placeholderInfo.id;
+ } else {
+ existingJpcParams = [ jpc.target, jpc.targetId, i, dragScope ];
+ jpc.target = placeholderInfo.element;
+ jpc.targetId = placeholderInfo.id;
+ }
+
+ // lock the other endpoint; if it is dynamic it will not move while the drag is occurring.
+ jpc.endpoints[anchorIdx == 0 ? 1 : 0].anchor.locked = true;
+ // store the original endpoint and assign the new floating endpoint for the drag.
+ jpc.suspendedEndpoint = jpc.endpoints[anchorIdx];
+ jpc.suspendedEndpoint.setHover(false);
+ jpc.endpoints[anchorIdx] = floatingEndpoint;
+
+ // fire an event that informs that a connection is being dragged
+ fireConnectionDraggingEvent(jpc);
+
+ }
+ // register it and register connection on it.
+ floatingConnections[placeholderInfo.id] = jpc;
+ floatingEndpoint.addConnection(jpc);
+ // only register for the target endpoint; we will not be dragging the source at any time
+ // before this connection is either discarded or made into a permanent connection.
+ _addToList(endpointsByElement, placeholderInfo.id, floatingEndpoint);
+ // tell jsplumb about it
+ _currentInstance.currentlyDragging = true;
+ };
+
+ var jpcl = jsPlumb.CurrentLibrary,
+ dragOptions = params.dragOptions || {},
+ defaultOpts = jsPlumb.extend( {}, jpcl.defaultDragOptions),
+ startEvent = jpcl.dragEvents["start"],
+ stopEvent = jpcl.dragEvents["stop"],
+ dragEvent = jpcl.dragEvents["drag"];
+
+ dragOptions = jsPlumb.extend(defaultOpts, dragOptions);
+ dragOptions.scope = dragOptions.scope || self.scope;
+ dragOptions[startEvent] = _wrap(dragOptions[startEvent], start);
+ // extracted drag handler function so can be used by makeSource
+ dragOptions[dragEvent] = _wrap(dragOptions[dragEvent], _dragHandler.drag);
+ dragOptions[stopEvent] = _wrap(dragOptions[stopEvent],
+ function() {
+ var originalEvent = jpcl.getDropEvent(arguments);
+ _currentInstance.currentlyDragging = false;
+ _removeWithFunction(endpointsByElement[placeholderInfo.id], function(e) {
+ return e.id == floatingEndpoint.id;
+ });
+ _removeElements( [ placeholderInfo.element[0], floatingEndpoint.canvas ], _element); // TODO: clean up the connection canvas (if the user aborted)
+ _removeElement(inPlaceCopy.canvas, _element);
+ _currentInstance.anchorManager.clearFor(placeholderInfo.id);
+ var idx = jpc.floatingAnchorIndex == null ? 1 : jpc.floatingAnchorIndex;
+ jpc.endpoints[idx == 0 ? 1 : 0].anchor.locked = false;
+ if (jpc.endpoints[idx] == floatingEndpoint) {
+ // if the connection was an existing one:
+ if (existingJpc && jpc.suspendedEndpoint) {
+ // fix for issue35, thanks Sylvain Gizard: when firing the detach event make sure the
+ // floating endpoint has been replaced.
+ if (idx == 0) {
+ jpc.source = existingJpcParams[0];
+ jpc.sourceId = existingJpcParams[1];
+ } else {
+ jpc.target = existingJpcParams[0];
+ jpc.targetId = existingJpcParams[1];
+ }
+
+ // restore the original scope (issue 57)
+ jsPlumb.CurrentLibrary.setDragScope(existingJpcParams[2], existingJpcParams[3]);
+ jpc.endpoints[idx] = jpc.suspendedEndpoint;
+ if (self.isReattach || jpc._forceDetach || !jpc.endpoints[idx == 0 ? 1 : 0].detach(jpc, false, false, true, originalEvent)) {
+ jpc.setHover(false);
+ jpc.floatingAnchorIndex = null;
+ jpc.suspendedEndpoint.addConnection(jpc);
+ _currentInstance.repaint(existingJpcParams[1]);
+ }
+ jpc._forceDetach = null;
+ } else {
+ // TODO this looks suspiciously kind of like an Endpoint.detach call too.
+ // i wonder if this one should post an event though. maybe this is good like this.
+ _removeElements(jpc.connector.getDisplayElements(), self.parent);
+ self.detachFromConnection(jpc);
+ }
+ }
+ self.anchor.locked = false;
+ self.paint({recalc:false});
+ jpc.setHover(false, false);
+
+ fireConnectionDragStopEvent(jpc);
+
+ jpc = null;
+ inPlaceCopy = null;
+ delete endpointsByElement[floatingEndpoint.elementId];
+ floatingEndpoint.anchor = null;
+ floatingEndpoint = null;
+ _currentInstance.currentlyDragging = false;
+
+
+ });
+
+ var i = _getElementObject(self.canvas);
+ jsPlumb.CurrentLibrary.initDraggable(i, dragOptions, true);
+ }
+
+ // pulled this out into a function so we can reuse it for the inPlaceCopy canvas; you can now drop detached connections
+ // back onto the endpoint you detached it from.
+ var _initDropTarget = function(canvas, forceInit, isTransient, endpoint) {
+ if ((params.isTarget || forceInit) && jsPlumb.CurrentLibrary.isDropSupported(_element)) {
+ var dropOptions = params.dropOptions || _currentInstance.Defaults.DropOptions || jsPlumb.Defaults.DropOptions;
+ dropOptions = jsPlumb.extend( {}, dropOptions);
+ dropOptions.scope = dropOptions.scope || self.scope;
+ var dropEvent = jsPlumb.CurrentLibrary.dragEvents['drop'],
+ overEvent = jsPlumb.CurrentLibrary.dragEvents['over'],
+ outEvent = jsPlumb.CurrentLibrary.dragEvents['out'],
+ drop = function() {
+
+ var originalEvent = jsPlumb.CurrentLibrary.getDropEvent(arguments),
+ draggable = _getElementObject(jsPlumb.CurrentLibrary.getDragObject(arguments)),
+ id = _getAttribute(draggable, "dragId"),
+ elId = _getAttribute(draggable, "elId"),
+ scope = _getAttribute(draggable, "originalScope"),
+ jpc = floatingConnections[id];
+
+ if (jpc != null) {
+ var idx = jpc.floatingAnchorIndex == null ? 1 : jpc.floatingAnchorIndex, oidx = idx == 0 ? 1 : 0;
+
+ // restore the original scope if necessary (issue 57)
+ if (scope) jsPlumb.CurrentLibrary.setDragScope(draggable, scope);
+
+ var endpointEnabled = endpoint != null ? endpoint.isEnabled() : true;
+
+ if (!self.isFull() && !(idx == 0 && !self.isSource) && !(idx == 1 && !self.isTarget) && endpointEnabled) {
+
+ var _doContinue = true;
+
+ // the second check here is for the case that the user is dropping it back
+ // where it came from.
+ if (jpc.suspendedEndpoint && jpc.suspendedEndpoint.id != self.id) {
+ if (idx == 0) {
+ jpc.source = jpc.suspendedEndpoint.element;
+ jpc.sourceId = jpc.suspendedEndpoint.elementId;
+ } else {
+ jpc.target = jpc.suspendedEndpoint.element;
+ jpc.targetId = jpc.suspendedEndpoint.elementId;
+ }
+
+ if (!jpc.isDetachAllowed(jpc) || !jpc.endpoints[idx].isDetachAllowed(jpc) || !jpc.suspendedEndpoint.isDetachAllowed(jpc) || !_currentInstance.checkCondition("beforeDetach", jpc))
+ _doContinue = false;
+ }
+
+ // these have to be set before testing for beforeDrop.
+ if (idx == 0) {
+ jpc.source = self.element;
+ jpc.sourceId = self.elementId;
+ } else {
+ jpc.target = self.element;
+ jpc.targetId = self.elementId;
+ }
+
+
+ // now check beforeDrop. this will be available only on Endpoints that are setup to
+ // have a beforeDrop condition (although, secretly, under the hood all Endpoints and
+ // the Connection have them, because they are on jsPlumbUIComponent. shhh!), because
+ // it only makes sense to have it on a target endpoint.
+ _doContinue = _doContinue && self.isDropAllowed(jpc.sourceId, jpc.targetId, jpc.scope);
+
+ if (_doContinue) {
+ // remove this jpc from the current endpoint
+ jpc.endpoints[idx].detachFromConnection(jpc);
+ if (jpc.suspendedEndpoint) jpc.suspendedEndpoint.detachFromConnection(jpc);
+ jpc.endpoints[idx] = self;
+ self.addConnection(jpc);
+
+ // copy our parameters in to the connection:
+ var params = self.getParameters();
+ for (var aParam in params)
+ jpc.setParameter(aParam, params[aParam]);
+
+ if (!jpc.suspendedEndpoint) {
+ //_addToList(connectionsByScope, jpc.scope, jpc);
+ _initDraggableIfNecessary(self.element, params.draggable, {});
+ }
+ else {
+ var suspendedElement = jpc.suspendedEndpoint.getElement(), suspendedElementId = jpc.suspendedEndpoint.elementId;
+ // fire a detach event
+ fireDetachEvent({
+ source : idx == 0 ? suspendedElement : jpc.source,
+ target : idx == 1 ? suspendedElement : jpc.target,
+ sourceId : idx == 0 ? suspendedElementId : jpc.sourceId,
+ targetId : idx == 1 ? suspendedElementId : jpc.targetId,
+ sourceEndpoint : idx == 0 ? jpc.suspendedEndpoint : jpc.endpoints[0],
+ targetEndpoint : idx == 1 ? jpc.suspendedEndpoint : jpc.endpoints[1],
+ connection : jpc
+ }, true, originalEvent);
+ }
+
+ // finalise will inform the anchor manager and also add to
+ // connectionsByScope if necessary.
+ _finaliseConnection(jpc, null, originalEvent);
+ }
+ else {
+ // otherwise just put it back on the endpoint it was on before the drag.
+ if (jpc.suspendedEndpoint) {
+ // self.detachFrom(jpc);
+ jpc.endpoints[idx] = jpc.suspendedEndpoint;
+ jpc.setHover(false);
+ jpc._forceDetach = true;
+ if (idx == 0) {
+ jpc.source = jpc.suspendedEndpoint.element;
+ jpc.sourceId = jpc.suspendedEndpoint.elementId;
+ } else {
+ jpc.target = jpc.suspendedEndpoint.element;
+ jpc.targetId = jpc.suspendedEndpoint.elementId;;
+ }
+ jpc.suspendedEndpoint.addConnection(jpc);
+
+ jpc.endpoints[0].repaint();
+ jpc.repaint();
+ _currentInstance.repaint(jpc.source.elementId);
+ jpc._forceDetach = false;
+ }
+ }
+
+ jpc.floatingAnchorIndex = null;
+ }
+ _currentInstance.currentlyDragging = false;
+ delete floatingConnections[id];
+ }
+ };
+
+ dropOptions[dropEvent] = _wrap(dropOptions[dropEvent], drop);
+ dropOptions[overEvent] = _wrap(dropOptions[overEvent], function() {
+ if (self.isTarget) {
+ var draggable = jsPlumb.CurrentLibrary.getDragObject(arguments),
+ id = _getAttribute( _getElementObject(draggable), "dragId"),
+ jpc = floatingConnections[id];
+ if (jpc != null) {
+ var idx = jpc.floatingAnchorIndex == null ? 1 : jpc.floatingAnchorIndex;
+ jpc.endpoints[idx].anchor.over(self.anchor);
+ }
+ }
+ });
+ dropOptions[outEvent] = _wrap(dropOptions[outEvent], function() {
+ if (self.isTarget) {
+ var draggable = jsPlumb.CurrentLibrary.getDragObject(arguments),
+ id = _getAttribute( _getElementObject(draggable), "dragId"),
+ jpc = floatingConnections[id];
+ if (jpc != null) {
+ var idx = jpc.floatingAnchorIndex == null ? 1 : jpc.floatingAnchorIndex;
+ jpc.endpoints[idx].anchor.out();
+ }
+ }
+ });
+ jsPlumb.CurrentLibrary.initDroppable(canvas, dropOptions, true, isTransient);
+ }
+ };
+
+ // initialise the endpoint's canvas as a drop target. this will be ignored if the endpoint is not a target or drag is not supported.
+ _initDropTarget(_getElementObject(self.canvas), true, !(params._transient || self.anchor.isFloating), self);
+
+ return self;
+ };
+ };
+
+ var jsPlumb = window.jsPlumb = new jsPlumbInstance();
+ jsPlumb.getInstance = function(_defaults) {
+ var j = new jsPlumbInstance(_defaults);
+ j.init();
+ return j;
+ };
+ jsPlumb.util = {
+ convertStyle : function(s, ignoreAlpha) {
+ // TODO: jsPlumb should support a separate 'opacity' style member.
+ if ("transparent" === s) return s;
+ var o = s,
+ pad = function(n) { return n.length == 1 ? "0" + n : n; },
+ hex = function(k) { return pad(Number(k).toString(16)); },
+ pattern = /(rgb[a]?\()(.*)(\))/;
+ if (s.match(pattern)) {
+ var parts = s.match(pattern)[2].split(",");
+ o = "#" + hex(parts[0]) + hex(parts[1]) + hex(parts[2]);
+ if (!ignoreAlpha && parts.length == 4)
+ o = o + hex(parts[3]);
+ }
+ return o;
+ },
+ gradient : function(p1, p2) {
+ p1 = _isArray(p1) ? p1 : [p1.x, p1.y];
+ p2 = _isArray(p2) ? p2 : [p2.x, p2.y];
+ return (p2[1] - p1[1]) / (p2[0] - p1[0]);
+ },
+ normal : function(p1, p2) {
+ return -1 / jsPlumb.util.gradient(p1,p2);
+ },
+ segment : function(p1, p2) {
+ p1 = _isArray(p1) ? p1 : [p1.x, p1.y];
+ p2 = _isArray(p2) ? p2 : [p2.x, p2.y];
+ if (p2[0] > p1[0]) {
+ return (p2[1] > p1[1]) ? 2 : 1;
+ }
+ else {
+ return (p2[1] > p1[1]) ? 3 : 4;
+ }
+ },
+ intersects : function(r1, r2) {
+ var x1 = r1.x, x2 = r1.x + r1.w, y1 = r1.y, y2 = r1.y + r1.h,
+ a1 = r2.x, a2 = r2.x + r2.w, b1 = r2.y, b2 = r2.y + r2.h;
+
+ return ( (x1 <= a1 && a1 <= x2) && (y1 <= b1 && b1 <= y2) ) ||
+ ( (x1 <= a2 && a2 <= x2) && (y1 <= b1 && b1 <= y2) ) ||
+ ( (x1 <= a1 && a1 <= x2) && (y1 <= b2 && b2 <= y2) ) ||
+ ( (x1 <= a2 && a1 <= x2) && (y1 <= b2 && b2 <= y2) ) ||
+
+ ( (a1 <= x1 && x1 <= a2) && (b1 <= y1 && y1 <= b2) ) ||
+ ( (a1 <= x2 && x2 <= a2) && (b1 <= y1 && y1 <= b2) ) ||
+ ( (a1 <= x1 && x1 <= a2) && (b1 <= y2 && y2 <= b2) ) ||
+ ( (a1 <= x2 && x1 <= a2) && (b1 <= y2 && y2 <= b2) );
+ },
+ segmentMultipliers : [null, [1, -1], [1, 1], [-1, 1], [-1, -1] ],
+ inverseSegmentMultipliers : [null, [-1, -1], [-1, 1], [1, 1], [1, -1] ],
+ pointOnLine : function(fromPoint, toPoint, distance) {
+ var m = jsPlumb.util.gradient(fromPoint, toPoint),
+ s = jsPlumb.util.segment(fromPoint, toPoint),
+ segmentMultiplier = distance > 0 ? jsPlumb.util.segmentMultipliers[s] : jsPlumb.util.inverseSegmentMultipliers[s],
+ theta = Math.atan(m),
+ y = Math.abs(distance * Math.sin(theta)) * segmentMultiplier[1],
+ x = Math.abs(distance * Math.cos(theta)) * segmentMultiplier[0];
+ return { x:fromPoint.x + x, y:fromPoint.y + y };
+ },
+ /**
+ * calculates a perpendicular to the line fromPoint->toPoint, that passes through toPoint and is 'length' long.
+ * @param fromPoint
+ * @param toPoint
+ * @param length
+ */
+ perpendicularLineTo : function(fromPoint, toPoint, length) {
+ var m = jsPlumb.util.gradient(fromPoint, toPoint),
+ theta2 = Math.atan(-1 / m),
+ y = length / 2 * Math.sin(theta2),
+ x = length / 2 * Math.cos(theta2);
+ return [{x:toPoint.x + x, y:toPoint.y + y}, {x:toPoint.x - x, y:toPoint.y - y}];
+ }
+ };
+
+ var _curryAnchor = function(x, y, ox, oy, type, fnInit) {
+ return function(params) {
+ params = params || {};
+ //var a = jsPlumb.makeAnchor([ x, y, ox, oy, 0, 0 ], params.elementId, params.jsPlumbInstance);
+ var a = params.jsPlumbInstance.makeAnchor([ x, y, ox, oy, 0, 0 ], params.elementId, params.jsPlumbInstance);
+ a.type = type;
+ if (fnInit) fnInit(a, params);
+ return a;
+ };
+ };
+ jsPlumb.Anchors["TopCenter"] = _curryAnchor(0.5, 0, 0,-1, "TopCenter");
+ jsPlumb.Anchors["BottomCenter"] = _curryAnchor(0.5, 1, 0, 1, "BottomCenter");
+ jsPlumb.Anchors["LeftMiddle"] = _curryAnchor(0, 0.5, -1, 0, "LeftMiddle");
+ jsPlumb.Anchors["RightMiddle"] = _curryAnchor(1, 0.5, 1, 0, "RightMiddle");
+ jsPlumb.Anchors["Center"] = _curryAnchor(0.5, 0.5, 0, 0, "Center");
+ jsPlumb.Anchors["TopRight"] = _curryAnchor(1, 0, 0,-1, "TopRight");
+ jsPlumb.Anchors["BottomRight"] = _curryAnchor(1, 1, 0, 1, "BottomRight");
+ jsPlumb.Anchors["TopLeft"] = _curryAnchor(0, 0, 0, -1, "TopLeft");
+ jsPlumb.Anchors["BottomLeft"] = _curryAnchor(0, 1, 0, 1, "BottomLeft");
+
+ // TODO test that this does not break with the current instance idea
+ jsPlumb.Defaults.DynamicAnchors = function(params) {
+ return params.jsPlumbInstance.makeAnchors(["TopCenter", "RightMiddle", "BottomCenter", "LeftMiddle"], params.elementId, params.jsPlumbInstance);
+ };
+ jsPlumb.Anchors["AutoDefault"] = function(params) {
+ var a = params.jsPlumbInstance.makeDynamicAnchor(jsPlumb.Defaults.DynamicAnchors(params));
+ a.type = "AutoDefault";
+ return a;
+ };
+
+ jsPlumb.Anchors["Assign"] = _curryAnchor(0,0,0,0,"Assign", function(anchor, params) {
+ // find what to use as the "position finder". the user may have supplied a String which represents
+ // the id of a position finder in jsPlumb.AnchorPositionFinders, or the user may have supplied the
+ // position finder as a function. we find out what to use and then set it on the anchor.
+ var pf = params.position || "Fixed";
+ anchor.positionFinder = pf.constructor == String ? params.jsPlumbInstance.AnchorPositionFinders[pf] : pf;
+ // always set the constructor params; the position finder might need them later (the Grid one does,
+ // for example)
+ anchor.constructorParams = params;
+ });
+
+ // Continuous anchor is just curried through to the 'get' method of the continuous anchor
+ // factory.
+ jsPlumb.Anchors["Continuous"] = function(params) {
+ return params.jsPlumbInstance.continuousAnchorFactory.get(params);
+ };
+
+ // these are the default anchor positions finders, which are used by the makeTarget function. supply
+ // a position finder argument to that function allows you to specify where the resulting anchor will
+ // be located
+ jsPlumb.AnchorPositionFinders = {
+ "Fixed": function(dp, ep, es, params) {
+ return [ (dp.left - ep.left) / es[0], (dp.top - ep.top) / es[1] ];
+ },
+ "Grid":function(dp, ep, es, params) {
+ var dx = dp.left - ep.left, dy = dp.top - ep.top,
+ gx = es[0] / (params.grid[0]), gy = es[1] / (params.grid[1]),
+ mx = Math.floor(dx / gx), my = Math.floor(dy / gy);
+ return [ ((mx * gx) + (gx / 2)) / es[0], ((my * gy) + (gy / 2)) / es[1] ];
+ }
+ };
+})();
+/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.3.8
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the default Connectors, Endpoint and Overlay definitions.
+ *
+ * Copyright (c) 2010 - 2012 Simon Porritt (http://jsplumb.org)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+
+(function() {
+
+ /**
+ *
+ * Helper class to consume unused mouse events by components that are DOM elements and
+ * are used by all of the different rendering modes.
+ *
+ */
+ jsPlumb.DOMElementComponent = function(params) {
+ jsPlumb.jsPlumbUIComponent.apply(this, arguments);
+ // when render mode is canvas, these functions may be called by the canvas mouse handler.
+ // this component is safe to pipe this stuff to /dev/null.
+ this.mousemove =
+ this.dblclick =
+ this.click =
+ this.mousedown =
+ this.mouseup = function(e) { };
+ };
+
+ /**
+ * Class: Connectors.Straight
+ * The Straight connector draws a simple straight line between the two anchor points. It does not have any constructor parameters.
+ */
+ jsPlumb.Connectors.Straight = function() {
+ this.type = "Straight";
+ var self = this,
+ currentPoints = null,
+ _m, _m2, _b, _dx, _dy, _theta, _theta2, _sx, _sy, _tx, _ty, _segment, _length;
+
+ /**
+ * Computes the new size and position of the canvas.
+ */
+ this.compute = function(sourcePos, targetPos, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor, lineWidth, minWidth) {
+ var w = Math.abs(sourcePos[0] - targetPos[0]),
+ h = Math.abs(sourcePos[1] - targetPos[1]),
+ // these are padding to ensure the whole connector line appears
+ xo = 0.45 * w, yo = 0.45 * h;
+ // these are padding to ensure the whole connector line appears
+ w *= 1.9; h *=1.9;
+
+ var x = Math.min(sourcePos[0], targetPos[0]) - xo;
+ var y = Math.min(sourcePos[1], targetPos[1]) - yo;
+
+ // minimum size is 2 * line Width if minWidth was not given.
+ var calculatedMinWidth = Math.max(2 * lineWidth, minWidth);
+
+ if (w < calculatedMinWidth) {
+ w = calculatedMinWidth;
+ x = sourcePos[0] + ((targetPos[0] - sourcePos[0]) / 2) - (calculatedMinWidth / 2);
+ xo = (w - Math.abs(sourcePos[0]-targetPos[0])) / 2;
+ }
+ if (h < calculatedMinWidth) {
+ h = calculatedMinWidth;
+ y = sourcePos[1] + ((targetPos[1] - sourcePos[1]) / 2) - (calculatedMinWidth / 2);
+ yo = (h - Math.abs(sourcePos[1]-targetPos[1])) / 2;
+ }
+
+ _sx = sourcePos[0] < targetPos[0] ? xo : w-xo;
+ _sy = sourcePos[1] < targetPos[1] ? yo:h-yo;
+ _tx = sourcePos[0] < targetPos[0] ? w-xo : xo;
+ _ty = sourcePos[1] < targetPos[1] ? h-yo : yo;
+ currentPoints = [ x, y, w, h, _sx, _sy, _tx, _ty ];
+ _dx = _tx - _sx, _dy = _ty - _sy;
+ //_m = _dy / _dx, _m2 = -1 / _m;
+ _m = jsPlumb.util.gradient({x:_sx, y:_sy}, {x:_tx, y:_ty}), _m2 = -1 / _m;
+ _b = -1 * ((_m * _sx) - _sy);
+ _theta = Math.atan(_m); _theta2 = Math.atan(_m2);
+ //_segment = jsPlumb.util.segment({x:_sx, y:_sy}, {x:_tx, y:_ty});
+ _length = Math.sqrt((_dx * _dx) + (_dy * _dy));
+
+ return currentPoints;
+ };
+
+
+ /**
+ * returns the point on the connector's path that is 'location' along the length of the path, where 'location' is a decimal from
+ * 0 to 1 inclusive. for the straight line connector this is simple maths. for Bezier, not so much.
+ */
+ this.pointOnPath = function(location) {
+ if (location == 0)
+ return { x:_sx, y:_sy };
+ else if (location == 1)
+ return { x:_tx, y:_ty };
+ else
+ return jsPlumb.util.pointOnLine({x:_sx, y:_sy}, {x:_tx, y:_ty}, location * _length);
+ };
+
+ /**
+ * returns the gradient of the connector at the given point - which for us is constant.
+ */
+ this.gradientAtPoint = function(location) {
+ return _m;
+ };
+
+ /**
+ * returns the point on the connector's path that is 'distance' along the length of the path from 'location', where
+ * 'location' is a decimal from 0 to 1 inclusive, and 'distance' is a number of pixels.
+ * this hands off to jsPlumb.util to do the maths, supplying two points and the distance.
+ */
+ this.pointAlongPathFrom = function(location, distance) {
+ var p = self.pointOnPath(location),
+ farAwayPoint = location == 1 ? {
+ x:_sx + ((_tx - _sx) * 10),
+ y:_sy + ((_sy - _ty) * 10)
+ } : {x:_tx, y:_ty };
+
+ return jsPlumb.util.pointOnLine(p, farAwayPoint, distance);
+ };
+ };
+
+
+ /**
+ * Class:Connectors.Bezier
+ * This Connector draws a Bezier curve with two control points. You can provide a 'curviness' value which gets applied to jsPlumb's
+ * internal voodoo machine and ends up generating locations for the two control points. See the constructor documentation below.
+ */
+ /**
+ * Function:Constructor
+ *
+ * Parameters:
+ * curviness - How 'curvy' you want the curve to be! This is a directive for the placement of control points, not endpoints of the curve, so your curve does not
+ * actually touch the given point, but it has the tendency to lean towards it. The larger this value, the greater the curve is pulled from a straight line.
+ * Optional; defaults to 150.
+ * stub - optional value for a distance to travel from the connector's endpoint before beginning the Bezier curve. defaults to 0.
+ *
+ */
+ jsPlumb.Connectors.Bezier = function(params) {
+ var self = this;
+ params = params || {};
+ this.majorAnchor = params.curviness || 150;
+ this.minorAnchor = 10;
+ var currentPoints = null;
+ this.type = "Bezier";
+
+ this._findControlPoint = function(point, sourceAnchorPosition, targetAnchorPosition, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor) {
+ // determine if the two anchors are perpendicular to each other in their orientation. we swap the control
+ // points around if so (code could be tightened up)
+ var soo = sourceAnchor.getOrientation(sourceEndpoint),
+ too = targetAnchor.getOrientation(targetEndpoint),
+ perpendicular = soo[0] != too[0] || soo[1] == too[1],
+ p = [],
+ ma = self.majorAnchor, mi = self.minorAnchor;
+
+ if (!perpendicular) {
+ if (soo[0] == 0) // X
+ p.push(sourceAnchorPosition[0] < targetAnchorPosition[0] ? point[0] + mi : point[0] - mi);
+ else p.push(point[0] - (ma * soo[0]));
+
+ if (soo[1] == 0) // Y
+ p.push(sourceAnchorPosition[1] < targetAnchorPosition[1] ? point[1] + mi : point[1] - mi);
+ else p.push(point[1] + (ma * too[1]));
+ }
+ else {
+ if (too[0] == 0) // X
+ p.push(targetAnchorPosition[0] < sourceAnchorPosition[0] ? point[0] + mi : point[0] - mi);
+ else p.push(point[0] + (ma * too[0]));
+
+ if (too[1] == 0) // Y
+ p.push(targetAnchorPosition[1] < sourceAnchorPosition[1] ? point[1] + mi : point[1] - mi);
+ else p.push(point[1] + (ma * soo[1]));
+ }
+
+ return p;
+ };
+
+ var _CP, _CP2, _sx, _tx, _ty, _sx, _sy, _canvasX, _canvasY, _w, _h, _sStubX, _sStubY, _tStubX, _tStubY;
+
+ this.compute = function(sourcePos, targetPos, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor, lineWidth, minWidth) {
+ lineWidth = lineWidth || 0;
+ _w = Math.abs(sourcePos[0] - targetPos[0]) + lineWidth;
+ _h = Math.abs(sourcePos[1] - targetPos[1]) + lineWidth;
+ _canvasX = Math.min(sourcePos[0], targetPos[0])-(lineWidth/2);
+ _canvasY = Math.min(sourcePos[1], targetPos[1])-(lineWidth/2);
+ _sx = sourcePos[0] < targetPos[0] ? _w - (lineWidth/2): (lineWidth/2);
+ _sy = sourcePos[1] < targetPos[1] ? _h - (lineWidth/2) : (lineWidth/2);
+ _tx = sourcePos[0] < targetPos[0] ? (lineWidth/2) : _w - (lineWidth/2);
+ _ty = sourcePos[1] < targetPos[1] ? (lineWidth/2) : _h - (lineWidth/2);
+
+ _CP = self._findControlPoint([_sx,_sy], sourcePos, targetPos, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor);
+ _CP2 = self._findControlPoint([_tx,_ty], targetPos, sourcePos, sourceEndpoint, targetEndpoint, targetAnchor, sourceAnchor);
+ var minx1 = Math.min(_sx,_tx), minx2 = Math.min(_CP[0], _CP2[0]), minx = Math.min(minx1,minx2),
+ maxx1 = Math.max(_sx,_tx), maxx2 = Math.max(_CP[0], _CP2[0]), maxx = Math.max(maxx1,maxx2);
+
+ if (maxx > _w) _w = maxx;
+ if (minx < 0) {
+ _canvasX += minx; var ox = Math.abs(minx);
+ _w += ox; _CP[0] += ox; _sx += ox; _tx +=ox; _CP2[0] += ox;
+ }
+
+ var miny1 = Math.min(_sy,_ty), miny2 = Math.min(_CP[1], _CP2[1]), miny = Math.min(miny1,miny2),
+ maxy1 = Math.max(_sy,_ty), maxy2 = Math.max(_CP[1], _CP2[1]), maxy = Math.max(maxy1,maxy2);
+
+ if (maxy > _h) _h = maxy;
+ if (miny < 0) {
+ _canvasY += miny; var oy = Math.abs(miny);
+ _h += oy; _CP[1] += oy; _sy += oy; _ty +=oy; _CP2[1] += oy;
+ }
+
+ if (minWidth && _w < minWidth) {
+ var posAdjust = (minWidth - _w) / 2;
+ _w = minWidth;
+ _canvasX -= posAdjust; _sx = _sx + posAdjust ; _tx = _tx + posAdjust; _CP[0] = _CP[0] + posAdjust; _CP2[0] = _CP2[0] + posAdjust;
+ }
+
+ if (minWidth && _h < minWidth) {
+ var posAdjust = (minWidth - _h) / 2;
+ _h = minWidth;
+ _canvasY -= posAdjust; _sy = _sy + posAdjust ; _ty = _ty + posAdjust; _CP[1] = _CP[1] + posAdjust; _CP2[1] = _CP2[1] + posAdjust;
+ }
+
+ currentPoints = [_canvasX, _canvasY, _w, _h,
+ _sx, _sy, _tx, _ty,
+ _CP[0], _CP[1], _CP2[0], _CP2[1] ];
+
+ return currentPoints;
+ };
+
+ var _makeCurve = function() {
+ return [
+ { x:_sx, y:_sy },
+ { x:_CP[0], y:_CP[1] },
+ { x:_CP2[0], y:_CP2[1] },
+ { x:_tx, y:_ty }
+ ];
+ };
+
+ /**
+ * returns the point on the connector's path that is 'location' along the length of the path, where 'location' is a decimal from
+ * 0 to 1 inclusive. for the straight line connector this is simple maths. for Bezier, not so much.
+ */
+ this.pointOnPath = function(location) {
+ return jsBezier.pointOnCurve(_makeCurve(), location);
+ };
+
+ /**
+ * returns the gradient of the connector at the given point.
+ */
+ this.gradientAtPoint = function(location) {
+ return jsBezier.gradientAtPoint(_makeCurve(), location);
+ };
+
+ /**
+ * for Bezier curves this method is a little tricky, cos calculating path distance algebraically is notoriously difficult.
+ * this method is iterative, jumping forward .05% of the path at a time and summing the distance between this point and the previous
+ * one, until the sum reaches 'distance'. the method may turn out to be computationally expensive; we'll see.
+ * another drawback of this method is that if the connector gets quite long, .05% of the length of it is not necessarily smaller
+ * than the desired distance, in which case the loop returns immediately and the arrow is mis-shapen. so a better strategy might be to
+ * calculate the step as a function of distance/distance between endpoints.
+ */
+ this.pointAlongPathFrom = function(location, distance) {
+ return jsBezier.pointAlongCurveFrom(_makeCurve(), location, distance);
+ };
+ };
+
+
+ /**
+ * Class: Connectors.Flowchart
+ * Provides 'flowchart' connectors, consisting of vertical and horizontal line segments.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ * stub - minimum length for the stub at each end of the connector. defaults to 30 pixels.
+ */
+ jsPlumb.Connectors.Flowchart = function(params) {
+ this.type = "Flowchart";
+ params = params || {};
+ var self = this,
+ minStubLength = params.stub || params.minStubLength /* bwds compat. */ || 30,
+ segments = [],
+ totalLength = 0,
+ segmentProportions = [],
+ segmentProportionalLengths = [],
+ points = [],
+ swapX, swapY,
+ maxX = 0, maxY = 0,
+ /**
+ * recalculates the points at which the segments begin and end, proportional to the total length travelled
+ * by all the segments that constitute the connector. we use this to help with pointOnPath calculations.
+ */
+ updateSegmentProportions = function(startX, startY, endX, endY) {
+ var curLoc = 0;
+ for (var i = 0; i < segments.length; i++) {
+ segmentProportionalLengths[i] = segments[i][5] / totalLength;
+ segmentProportions[i] = [curLoc, (curLoc += (segments[i][5] / totalLength)) ];
+ }
+ },
+ appendSegmentsToPoints = function() {
+ points.push(segments.length);
+ for (var i = 0; i < segments.length; i++) {
+ points.push(segments[i][0]);
+ points.push(segments[i][1]);
+ }
+ },
+ /**
+ * helper method to add a segment.
+ */
+ addSegment = function(x, y, sx, sy, tx, ty) {
+ var lx = segments.length == 0 ? sx : segments[segments.length - 1][0],
+ ly = segments.length == 0 ? sy : segments[segments.length - 1][1],
+ m = x == lx ? Infinity : 0,
+ l = Math.abs(x == lx ? y - ly : x - lx);
+ segments.push([x, y, lx, ly, m, l]);
+ totalLength += l;
+
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ },
+ /**
+ * returns [segment, proportion of travel in segment, segment index] for the segment
+ * that contains the point which is 'location' distance along the entire path, where
+ * 'location' is a decimal between 0 and 1 inclusive. in this connector type, paths
+ * are made up of a list of segments, each of which contributes some fraction to
+ * the total length.
+ */
+ findSegmentForLocation = function(location) {
+ var idx = segmentProportions.length - 1, inSegmentProportion = 1;
+ for (var i = 0; i < segmentProportions.length; i++) {
+ if (segmentProportions[i][1] >= location) {
+ idx = i;
+ inSegmentProportion = (location - segmentProportions[i][0]) / segmentProportionalLengths[i];
+ break;
+ }
+ }
+ return { segment:segments[idx], proportion:inSegmentProportion, index:idx };
+ };
+
+ this.compute = function(sourcePos, targetPos, sourceEndpoint, targetEndpoint,
+ sourceAnchor, targetAnchor, lineWidth, minWidth, sourceInfo, targetInfo) {
+
+ segments = [];
+ segmentProportions = [];
+ totalLength = 0;
+ segmentProportionalLengths = [];
+ maxX = maxY = 0;
+
+ swapX = targetPos[0] < sourcePos[0];
+ swapY = targetPos[1] < sourcePos[1];
+
+ var lw = lineWidth || 1,
+ offx = (lw / 2) + (minStubLength * 2),
+ offy = (lw / 2) + (minStubLength * 2),
+ so = sourceAnchor.orientation || sourceAnchor.getOrientation(sourceEndpoint),
+ to = targetAnchor.orientation || targetAnchor.getOrientation(targetEndpoint),
+ x = swapX ? targetPos[0] : sourcePos[0],
+ y = swapY ? targetPos[1] : sourcePos[1],
+ w = Math.abs(targetPos[0] - sourcePos[0]) + 2*offx,
+ h = Math.abs(targetPos[1] - sourcePos[1]) + 2*offy;
+
+ // if either anchor does not have an orientation set, we derive one from their relative
+ // positions. we fix the axis to be the one in which the two elements are further apart, and
+ // point each anchor at the other element. this is also used when dragging a new connection.
+ if (so[0] == 0 && so[1] == 0 || to[0] == 0 && to[1] == 0) {
+ var index = w > h ? 0 : 1, oIndex = [1,0][index];
+ so = []; to = [];
+ so[index] = sourcePos[index] > targetPos[index] ? -1 : 1;
+ to[index] = sourcePos[index] > targetPos[index] ? 1 : -1;
+ so[oIndex] = 0;
+ to[oIndex] = 0;
+ }
+
+ if (w < minWidth) {
+ offx += (minWidth - w) / 2;
+ w = minWidth;
+ }
+ if (h < minWidth) {
+ offy += (minWidth - h) / 2;
+ h = minWidth;
+ }
+
+ var sx = swapX ? w-offx : offx,
+ sy = swapY ? h-offy : offy,
+ tx = swapX ? offx : w-offx ,
+ ty = swapY ? offy : h-offy,
+ startStubX = sx + (so[0] * minStubLength),
+ startStubY = sy + (so[1] * minStubLength),
+ endStubX = tx + (to[0] * minStubLength),
+ endStubY = ty + (to[1] * minStubLength),
+ isXGreaterThanStubTimes2 = Math.abs(sx - tx) > 2 * minStubLength,
+ isYGreaterThanStubTimes2 = Math.abs(sy - ty) > 2 * minStubLength,
+ midx = startStubX + ((endStubX - startStubX) / 2),
+ midy = startStubY + ((endStubY - startStubY) / 2),
+ oProduct = ((so[0] * to[0]) + (so[1] * to[1])),
+ opposite = oProduct == -1,
+ perpendicular = oProduct == 0,
+ orthogonal = oProduct == 1;
+
+ x -= offx; y -= offy;
+ points = [x, y, w, h, sx, sy, tx, ty];
+ var extraPoints = [];
+
+ addSegment(startStubX, startStubY, sx, sy, tx, ty);
+
+ var sourceAxis = so[0] == 0 ? "y" : "x",
+ anchorOrientation = opposite ? "opposite" : orthogonal ? "orthogonal" : "perpendicular",
+ segment = jsPlumb.util.segment([sx, sy], [tx, ty]),
+ flipSourceSegments = so[sourceAxis == "x" ? 0 : 1] == -1,
+ flipSegments = {
+ "x":[null, 4, 3, 2, 1],
+ "y":[null, 2, 1, 4, 3]
+ }
+
+ if (flipSourceSegments)
+ segment = flipSegments[sourceAxis][segment];
+
+ var findClearedLine = function(start, mult, anchorPos, dimension) {
+ return start + (mult * (( 1 - anchorPos) * dimension) + minStubLength);
+ //mx = so[0] == 0 ? startStubX + ((1 - sourceAnchor.x) * sourceInfo.width) + minStubLength : startStubX,
+ },
+
+ lineCalculators = {
+ oppositex : function() {
+ if (sourceEndpoint.elementId == targetEndpoint.elementId) {
+ var _y = startStubY + ((1 - sourceAnchor.y) * sourceInfo.height) + minStubLength;
+ return [ [ startStubX, _y ], [ endStubX, _y ]];
+ }
+ else if (isXGreaterThanStubTimes2 && (segment == 1 || segment == 2)) {
+ return [[ midx, sy ], [ midx, ty ]];
+ }
+ else {
+ return [[ startStubX, midy ], [endStubX, midy ]];
+ }
+ },
+ orthogonalx : function() {
+ if (segment == 1 || segment == 2) {
+ return [ [ endStubX, startStubY ]];
+ }
+ else {
+ return [ [ startStubX, endStubY ]];
+ }
+ },
+ perpendicularx : function() {
+ var my = (ty + sy) / 2;
+ if ((segment == 1 && to[1] == 1) || (segment == 2 && to[1] == -1)) {
+ if (Math.abs(tx - sx) > minStubLength)
+ return [ [endStubX, startStubY ]];
+ else
+ return [ [startStubX, startStubY ], [ startStubX, my ], [ endStubX, my ]];
+ }
+ else if ((segment == 3 && to[1] == -1) || (segment == 4 && to[1] == 1)) {
+ return [ [ startStubX, my ], [ endStubX, my ]];
+ }
+ else if ((segment == 3 && to[1] == 1) || (segment == 4 && to[1] == -1)) {
+ return [ [ startStubX, endStubY ]];
+ }
+ else if ((segment == 1 && to[1] == -1) || (segment == 2 && to[1] == 1)) {
+ if (Math.abs(tx - sx) > minStubLength)
+ return [ [ midx, startStubY ], [ midx, endStubY ]];
+ else
+ return [ [ startStubX, endStubY ]];
+ }
+ },
+ oppositey : function() {
+ if (sourceEndpoint.elementId == targetEndpoint.elementId) {
+ var _x = startStubX + ((1 - sourceAnchor.x) * sourceInfo.width) + minStubLength;
+ return [ [ _x, startStubY ], [ _x, endStubY ]];
+ }
+ else if (isYGreaterThanStubTimes2 && (segment == 2 || segment == 3)) {
+ return [[ sx, midy ], [ tx, midy ]];
+ }
+ else {
+ return [[ midx, startStubY ], [midx, endStubY ]];
+ }
+ },
+ orthogonaly : function() {
+ if (segment == 2 || segment == 3) {
+ return [ [ startStubX, endStubY ]];
+ }
+ else {
+ return [ [ endStubX, startStubY ]];
+ }
+ },
+ perpendiculary : function() {
+ var mx = (tx + sx) / 2;
+ if ((segment == 2 && to[0] == -1) || (segment == 3 && to[0] == 1)) {
+ if (Math.abs(tx - sx) > minStubLength)
+ return [ [startStubX, endStubY ]];
+ else
+ return [ [startStubX, midy ], [ endStubX, midy ]];
+ }
+ else if ((segment == 1 && to[0] == -1) || (segment == 4 && to[0] == 1)) {
+ var mx = (tx + sx) / 2;
+ return [ [ mx, startStubY ], [ mx, endStubY ]];
+ }
+ else if ((segment == 1 && to[0] == 1) || (segment == 4 && to[0] == -1)) {
+ return [ [ endStubX, startStubY ]];
+ }
+ else if ((segment == 2 && to[0] == 1) || (segment == 3 && to[0] == -1)) {
+ if (Math.abs(ty - sy) > minStubLength)
+ return [ [ startStubX, midy ], [ endStubX, midy ]];
+ else
+ return [ [ endStubX, startStubY ]];
+ }
+ }
+ };
+
+ var p = lineCalculators[anchorOrientation + sourceAxis]();
+ if (p) {
+ for (var i = 0; i < p.length; i++) {
+ addSegment(p[i][0], p[i][1], sx, sy, tx, ty);
+ }
+ }
+
+
+ addSegment(endStubX, endStubY, sx, sy, tx, ty);
+ addSegment(tx, ty, sx, sy, tx, ty);
+
+ appendSegmentsToPoints();
+ updateSegmentProportions(sx, sy, tx, ty);
+
+ // adjust the max values of the canvas if we have a value that is larger than what we previously set.
+ //
+ if (maxY > points[3]) points[3] = maxY + (lineWidth * 2);
+ if (maxX > points[2]) points[2] = maxX + (lineWidth * 2);
+
+ return points;
+ };
+
+ /**
+ * returns the point on the connector's path that is 'location' along the length of the path, where 'location' is a decimal from
+ * 0 to 1 inclusive. for this connector we must first figure out which segment the given point lies in, and then compute the x,y position
+ * from our knowledge of the segment's start and end points.
+ */
+ this.pointOnPath = function(location) {
+ return self.pointAlongPathFrom(location, 0);
+ };
+
+ /**
+ * returns the gradient of the connector at the given point; the gradient will be either 0 or Infinity, depending on the direction of the
+ * segment the point falls in. segment gradients are calculated in the compute method.
+ */
+ this.gradientAtPoint = function(location) {
+ return segments[findSegmentForLocation(location)["index"]][4];
+ };
+
+ /**
+ * returns the point on the connector's path that is 'distance' along the length of the path from 'location', where
+ * 'location' is a decimal from 0 to 1 inclusive, and 'distance' is a number of pixels. when you consider this concept from the point of view
+ * of this connector, it starts to become clear that there's a problem with the overlay paint code: given that this connector makes several
+ * 90 degree turns, it's entirely possible that an arrow overlay could be forced to paint itself around a corner, which would look stupid. this is
+ * because jsPlumb uses this method (and pointOnPath) so determine the locations of the various points that go to make up an overlay. a better
+ * solution would probably be to just use pointOnPath along with gradientAtPoint, and draw the overlay so that its axis ran along
+ * a tangent to the connector. for straight line connectors this would obviously mean the overlay was painted directly on the connector, since a
+ * tangent to a straight line is the line itself, which is what we want; for this connector, and for beziers, the results would probably be better. an additional
+ * advantage is, of course, that there's less computation involved doing it that way.
+ */
+ this.pointAlongPathFrom = function(location, distance) {
+ var s = findSegmentForLocation(location), seg = s.segment, p = s.proportion, sl = segments[s.index][5], m = segments[s.index][4];
+ var e = {
+ x : m == Infinity ? seg[2] : seg[2] > seg[0] ? seg[0] + ((1 - p) * sl) - distance : seg[2] + (p * sl) + distance,
+ y : m == 0 ? seg[3] : seg[3] > seg[1] ? seg[1] + ((1 - p) * sl) - distance : seg[3] + (p * sl) + distance,
+ segmentInfo : s
+ };
+
+ return e;
+ };
+ };
+
+ // ********************************* END OF CONNECTOR TYPES *******************************************************************
+
+ // ********************************* ENDPOINT TYPES *******************************************************************
+
+ /**
+ * Class: Endpoints.Dot
+ * A round endpoint, with default radius 10 pixels.
+ */
+
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * radius - radius of the endpoint. defaults to 10 pixels.
+ */
+ jsPlumb.Endpoints.Dot = function(params) {
+ this.type = "Dot";
+ var self = this;
+ params = params || {};
+ this.radius = params.radius || 10;
+ this.defaultOffset = 0.5 * this.radius;
+ this.defaultInnerRadius = this.radius / 3;
+
+ this.compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ var r = endpointStyle.radius || self.radius,
+ x = anchorPoint[0] - r,
+ y = anchorPoint[1] - r;
+ return [ x, y, r * 2, r * 2, r ];
+ };
+ };
+
+ /**
+ * Class: Endpoints.Rectangle
+ * A Rectangular Endpoint, with default size 20x20.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * width - width of the endpoint. defaults to 20 pixels.
+ * height - height of the endpoint. defaults to 20 pixels.
+ */
+ jsPlumb.Endpoints.Rectangle = function(params) {
+ this.type = "Rectangle";
+ var self = this;
+ params = params || {};
+ this.width = params.width || 20;
+ this.height = params.height || 20;
+
+ this.compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ var width = endpointStyle.width || self.width,
+ height = endpointStyle.height || self.height,
+ x = anchorPoint[0] - (width/2),
+ y = anchorPoint[1] - (height/2);
+ return [ x, y, width, height];
+ };
+ };
+
+
+ var DOMElementEndpoint = function(params) {
+ jsPlumb.DOMElementComponent.apply(this, arguments);
+ var self = this;
+
+ var displayElements = [ ];
+ this.getDisplayElements = function() {
+ return displayElements;
+ };
+
+ this.appendDisplayElement = function(el) {
+ displayElements.push(el);
+ };
+ };
+ /**
+ * Class: Endpoints.Image
+ * Draws an image as the Endpoint.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * src - location of the image to use.
+ */
+ jsPlumb.Endpoints.Image = function(params) {
+
+ this.type = "Image";
+ DOMElementEndpoint.apply(this, arguments);
+
+ var self = this,
+ initialized = false,
+ widthToUse = params.width,
+ heightToUse = params.height,
+ _onload = null,
+ _endpoint = params.endpoint;
+
+ this.img = new Image();
+ self.ready = false;
+
+ this.img.onload = function() {
+ self.ready = true;
+ widthToUse = widthToUse || self.img.width;
+ heightToUse = heightToUse || self.img.height;
+ if (_onload) {
+ _onload(self);
+ }
+ };
+
+ /*
+ Function: setImage
+ Sets the Image to use in this Endpoint.
+
+ Parameters:
+ img - may be a URL or an Image object
+ onload - optional; a callback to execute once the image has loaded.
+ */
+ _endpoint.setImage = function(img, onload) {
+ var s = img.constructor == String ? img : img.src;
+ _onload = onload;
+ self.img.src = img;
+
+ if (self.canvas != null)
+ self.canvas.setAttribute("src", img);
+ };
+
+ _endpoint.setImage(params.src || params.url, params.onload);
+
+ this.compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ self.anchorPoint = anchorPoint;
+ if (self.ready) return [anchorPoint[0] - widthToUse / 2, anchorPoint[1] - heightToUse / 2,
+ widthToUse, heightToUse];
+ else return [0,0,0,0];
+ };
+
+ self.canvas = document.createElement("img"), initialized = false;
+ self.canvas.style["margin"] = 0;
+ self.canvas.style["padding"] = 0;
+ self.canvas.style["outline"] = 0;
+ self.canvas.style["position"] = "absolute";
+ var clazz = params.cssClass ? " " + params.cssClass : "";
+ self.canvas.className = jsPlumb.endpointClass + clazz;
+ if (widthToUse) self.canvas.setAttribute("width", widthToUse);
+ if (heightToUse) self.canvas.setAttribute("height", heightToUse);
+ jsPlumb.appendElement(self.canvas, params.parent);
+ self.attachListeners(self.canvas, self);
+
+ var actuallyPaint = function(d, style, anchor) {
+ if (!initialized) {
+ self.canvas.setAttribute("src", self.img.src);
+ self.appendDisplayElement(self.canvas);
+ initialized = true;
+ }
+ var x = self.anchorPoint[0] - (widthToUse / 2),
+ y = self.anchorPoint[1] - (heightToUse / 2);
+ jsPlumb.sizeCanvas(self.canvas, x, y, widthToUse, heightToUse);
+ };
+
+ this.paint = function(d, style, anchor) {
+ if (self.ready) {
+ actuallyPaint(d, style, anchor);
+ }
+ else {
+ window.setTimeout(function() {
+ self.paint(d, style, anchor);
+ }, 200);
+ }
+ };
+ };
+
+ /**
+ * Class: Endpoints.Blank
+ * An Endpoint that paints nothing (visible) on the screen. Supports cssClass and hoverClass parameters like all Endpoints.
+ */
+ jsPlumb.Endpoints.Blank = function(params) {
+ var self = this;
+ this.type = "Blank";
+ DOMElementEndpoint.apply(this, arguments);
+ this.compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ return [anchorPoint[0], anchorPoint[1],10,0];
+ };
+
+ self.canvas = document.createElement("div");
+ self.canvas.style.display = "block";
+ self.canvas.style.width = "1px";
+ self.canvas.style.height = "1px";
+ self.canvas.style.background = "transparent";
+ self.canvas.style.position = "absolute";
+ self.canvas.className = self._jsPlumb.endpointClass;
+ jsPlumb.appendElement(self.canvas, params.parent);
+
+ this.paint = function(d, style, anchor) {
+ jsPlumb.sizeCanvas(self.canvas, d[0], d[1], d[2], d[3]);
+ };
+ };
+
+ /**
+ * Class: Endpoints.Triangle
+ * A triangular Endpoint.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * width - width of the triangle's base. defaults to 55 pixels.
+ * height - height of the triangle from base to apex. defaults to 55 pixels.
+ */
+ jsPlumb.Endpoints.Triangle = function(params) {
+ this.type = "Triangle";
+ params = params || { };
+ params.width = params.width || 55;
+ params.height = params.height || 55;
+ this.width = params.width;
+ this.height = params.height;
+ this.compute = function(anchorPoint, orientation, endpointStyle, connectorPaintStyle) {
+ var width = endpointStyle.width || self.width,
+ height = endpointStyle.height || self.height,
+ x = anchorPoint[0] - (width/2),
+ y = anchorPoint[1] - (height/2);
+ return [ x, y, width, height ];
+ };
+ };
+// ********************************* END OF ENDPOINT TYPES *******************************************************************
+
+
+// ********************************* OVERLAY DEFINITIONS ***********************************************************************
+
+ var AbstractOverlay = function(params) {
+ var visible = true, self = this;
+ this.isAppendedAtTopLevel = true;
+ this.component = params.component;
+ this.loc = params.location == null ? 0.5 : params.location;
+ this.endpointLoc = params.endpointLocation == null ? [ 0.5, 0.5] : params.endpointLocation;
+ this.setVisible = function(val) {
+ visible = val;
+ self.component.repaint();
+ };
+ this.isVisible = function() { return visible; };
+ this.hide = function() { self.setVisible(false); };
+ this.show = function() { self.setVisible(true); };
+
+ this.incrementLocation = function(amount) {
+ self.loc += amount;
+ self.component.repaint();
+ };
+ this.setLocation = function(l) {
+ self.loc = l;
+ self.component.repaint();
+ };
+ this.getLocation = function() {
+ return self.loc;
+ };
+ };
+
+
+ /**
+ * Class: Overlays.Arrow
+ *
+ * An arrow overlay, defined by four points: the head, the two sides of the tail, and a 'foldback' point at some distance along the length
+ * of the arrow that lines from each tail point converge into. The foldback point is defined using a decimal that indicates some fraction
+ * of the length of the arrow and has a default value of 0.623. A foldback point value of 1 would mean that the arrow had a straight line
+ * across the tail.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ *
+ * length - distance in pixels from head to tail baseline. default 20.
+ * width - width in pixels of the tail baseline. default 20.
+ * fillStyle - style to use when filling the arrow. defaults to "black".
+ * strokeStyle - style to use when stroking the arrow. defaults to null, which means the arrow is not stroked.
+ * lineWidth - line width to use when stroking the arrow. defaults to 1, but only used if strokeStyle is not null.
+ * foldback - distance (as a decimal from 0 to 1 inclusive) along the length of the arrow marking the point the tail points should fold back to. defaults to 0.623.
+ * location - distance (as a decimal from 0 to 1 inclusive) marking where the arrow should sit on the connector. defaults to 0.5.
+ * direction - indicates the direction the arrow points in. valid values are -1 and 1; 1 is default.
+ */
+ jsPlumb.Overlays.Arrow = function(params) {
+ this.type = "Arrow";
+ AbstractOverlay.apply(this, arguments);
+ this.isAppendedAtTopLevel = false;
+ params = params || {};
+ var self = this;
+
+ this.length = params.length || 20;
+ this.width = params.width || 20;
+ this.id = params.id;
+ var direction = (params.direction || 1) < 0 ? -1 : 1,
+ paintStyle = params.paintStyle || { lineWidth:1 },
+ // how far along the arrow the lines folding back in come to. default is 62.3%.
+ foldback = params.foldback || 0.623;
+
+
+ this.computeMaxSize = function() { return self.width * 1.5; };
+
+ this.cleanup = function() { }; // nothing to clean up for Arrows
+
+ this.draw = function(connector, currentConnectionPaintStyle, connectorDimensions) {
+
+ var hxy, mid, txy, tail, cxy;
+ if (connector.pointAlongPathFrom) {
+
+ if (self.loc == 1) {
+ hxy = connector.pointOnPath(self.loc);
+ mid = connector.pointAlongPathFrom(self.loc, -1);
+ txy = jsPlumb.util.pointOnLine(hxy, mid, self.length);
+ }
+ else if (self.loc == 0) {
+ txy = connector.pointOnPath(self.loc);
+ mid = connector.pointAlongPathFrom(self.loc, 1);
+ hxy = jsPlumb.util.pointOnLine(txy, mid, self.length);
+ }
+ else {
+ hxy = connector.pointAlongPathFrom(self.loc, direction * self.length / 2),
+ mid = connector.pointOnPath(self.loc),
+ txy = jsPlumb.util.pointOnLine(hxy, mid, self.length);
+ }
+
+ tail = jsPlumb.util.perpendicularLineTo(hxy, txy, self.width);
+ cxy = jsPlumb.util.pointOnLine(hxy, txy, foldback * self.length);
+
+ var minx = Math.min(hxy.x, tail[0].x, tail[1].x),
+ maxx = Math.max(hxy.x, tail[0].x, tail[1].x),
+ miny = Math.min(hxy.y, tail[0].y, tail[1].y),
+ maxy = Math.max(hxy.y, tail[0].y, tail[1].y);
+
+ var d = { hxy:hxy, tail:tail, cxy:cxy },
+ strokeStyle = paintStyle.strokeStyle || currentConnectionPaintStyle.strokeStyle,
+ fillStyle = paintStyle.fillStyle || currentConnectionPaintStyle.strokeStyle,
+ lineWidth = paintStyle.lineWidth || currentConnectionPaintStyle.lineWidth;
+
+ self.paint(connector, d, lineWidth, strokeStyle, fillStyle, connectorDimensions);
+
+ return [ minx, maxx, miny, maxy];
+ }
+ else return [0,0,0,0];
+ };
+ };
+
+ /**
+ * Class: Overlays.PlainArrow
+ *
+ * A basic arrow. This is in fact just one instance of the more generic case in which the tail folds back on itself to some
+ * point along the length of the arrow: in this case, that foldback point is the full length of the arrow. so it just does
+ * a 'call' to Arrow with foldback set appropriately.
+ */
+ /**
+ * Function: Constructor
+ * See for allowed parameters for this overlay.
+ */
+ jsPlumb.Overlays.PlainArrow = function(params) {
+ params = params || {};
+ var p = jsPlumb.extend(params, {foldback:1});
+ jsPlumb.Overlays.Arrow.call(this, p);
+ this.type = "PlainArrow";
+ };
+
+ /**
+ * Class: Overlays.Diamond
+ *
+ * A diamond. Like PlainArrow, this is a concrete case of the more generic case of the tail points converging on some point...it just
+ * happens that in this case, that point is greater than the length of the the arrow.
+ *
+ * this could probably do with some help with positioning...due to the way it reuses the Arrow paint code, what Arrow thinks is the
+ * center is actually 1/4 of the way along for this guy. but we don't have any knowledge of pixels at this point, so we're kind of
+ * stuck when it comes to helping out the Arrow class. possibly we could pass in a 'transpose' parameter or something. the value
+ * would be -l/4 in this case - move along one quarter of the total length.
+ */
+ /**
+ * Function: Constructor
+ * See for allowed parameters for this overlay.
+ */
+ jsPlumb.Overlays.Diamond = function(params) {
+ params = params || {};
+ var l = params.length || 40,
+ p = jsPlumb.extend(params, {length:l/2, foldback:2});
+ jsPlumb.Overlays.Arrow.call(this, p);
+ this.type = "Diamond";
+ };
+
+
+
+ /**
+ * Class: Overlays.Label
+ * A Label overlay. For all different renderer types (SVG/Canvas/VML), jsPlumb draws a Label overlay as a styled DIV. Version 1.3.0 of jsPlumb
+ * introduced the ability to set css classes on the label; this is now the preferred way for you to style a label. The 'labelStyle' parameter
+ * is still supported in 1.3.0 but its usage is deprecated. Under the hood, jsPlumb just turns that object into a bunch of CSS directive that it
+ * puts on the Label's 'style' attribute, so the end result is the same.
+ */
+ /**
+ * Function: Constructor
+ *
+ * Parameters:
+ * cssClass - optional css class string to append to css class. This string is appended "as-is", so you can of course have multiple classes
+ * defined. This parameter is preferred to using labelStyle, borderWidth and borderStyle.
+ * label - the label to paint. May be a string or a function that returns a string. Nothing will be painted if your label is null or your
+ * label function returns null. empty strings _will_ be painted.
+ * location - distance (as a decimal from 0 to 1 inclusive) marking where the label should sit on the connector. defaults to 0.5.
+ *
+ */
+ jsPlumb.Overlays.Label = function(params) {
+ this.type = "Label";
+ jsPlumb.DOMElementComponent.apply(this, arguments);
+ AbstractOverlay.apply(this, arguments);
+ this.labelStyle = params.labelStyle || jsPlumb.Defaults.LabelStyle;
+ this.id = params.id;
+ this.cachedDimensions = null; // setting on 'this' rather than using closures uses a lot less memory. just don't monkey with it!
+ var label = params.label || "",
+ self = this,
+ initialised = false,
+ div = document.createElement("div"),
+ labelText = null;
+ div.style["position"] = "absolute";
+
+ var clazz = params["_jsPlumb"].overlayClass + " " +
+ (self.labelStyle.cssClass ? self.labelStyle.cssClass :
+ params.cssClass ? params.cssClass : "");
+
+ div.className = clazz;
+
+ jsPlumb.appendElement(div, params.component.parent);
+ jsPlumb.getId(div);
+ self.attachListeners(div, self);
+ self.canvas = div;
+
+ //override setVisible
+ var osv = self.setVisible;
+ self.setVisible = function(state) {
+ osv(state); // call superclass
+ div.style.display = state ? "block" : "none";
+ };
+
+ this.getElement = function() {
+ return div;
+ };
+
+ this.cleanup = function() {
+ if (div != null) jsPlumb.CurrentLibrary.removeElement(div);
+ };
+
+ /*
+ * Function: setLabel
+ * sets the label's, um, label. you would think i'd call this function
+ * 'setText', but you can pass either a Function or a String to this, so
+ * it makes more sense as 'setLabel'.
+ */
+ this.setLabel = function(l) {
+ label = l;
+ labelText = null;
+ self.component.repaint();
+ };
+
+ this.getLabel = function() {
+ return label;
+ };
+
+ this.paint = function(component, d, componentDimensions) {
+ if (!initialised) {
+ component.appendDisplayElement(div);
+ self.attachListeners(div, component);
+ initialised = true;
+ }
+ div.style.left = (componentDimensions[0] + d.minx) + "px";
+ div.style.top = (componentDimensions[1] + d.miny) + "px";
+ };
+
+ this.getTextDimensions = function() {
+ if (typeof label == "function") {
+ var lt = label(self);
+ div.innerHTML = lt.replace(/\r\n/g, "
");
+ }
+ else {
+ if (labelText == null) {
+ labelText = label;
+ div.innerHTML = labelText.replace(/\r\n/g, "
");
+ }
+ }
+ var de = jsPlumb.CurrentLibrary.getElementObject(div),
+ s = jsPlumb.CurrentLibrary.getSize(de);
+ return {width:s[0], height:s[1]};
+ };
+
+ this.computeMaxSize = function(connector) {
+ var td = self.getTextDimensions(connector);
+ return td.width ? Math.max(td.width, td.height) * 1.5 : 0;
+ };
+
+ this.draw = function(component, currentConnectionPaintStyle, componentDimensions) {
+ var td = self.getTextDimensions(component);
+ if (td.width != null) {
+ var cxy = {x:0,y:0};
+ if (component.pointOnPath)
+ cxy = component.pointOnPath(self.loc); // a connection
+ else {
+ var locToUse = self.loc.constructor == Array ? self.loc : self.endpointLoc;
+ cxy = { x:locToUse[0] * componentDimensions[2],
+ y:locToUse[1] * componentDimensions[3] };
+ }
+
+ minx = cxy.x - (td.width / 2),
+ miny = cxy.y - (td.height / 2);
+
+ self.paint(component, {
+ minx:minx,
+ miny:miny,
+ td:td,
+ cxy:cxy
+ }, componentDimensions);
+
+ return [minx, minx+td.width, miny, miny+td.height];
+ }
+ else return [0,0,0,0];
+ };
+
+ this.reattachListeners = function(connector) {
+ if (div) {
+ self.reattachListenersForElement(div, self, connector);
+ }
+ };
+ };
+
+ // this is really just a test overlay, so its undocumented and doesnt take any parameters. but i was loth to delete it.
+ jsPlumb.Overlays.GuideLines = function() {
+ var self = this;
+ self.length = 50;
+ self.lineWidth = 5;
+ this.type = "GuideLines";
+ AbstractOverlay.apply(this, arguments);
+ jsPlumb.jsPlumbUIComponent.apply(this, arguments);
+ this.draw = function(connector, currentConnectionPaintStyle, connectorDimensions) {
+
+ var head = connector.pointAlongPathFrom(self.loc, self.length / 2),
+ mid = connector.pointOnPath(self.loc),
+ tail = jsPlumb.util.pointOnLine(head, mid, self.length),
+ tailLine = jsPlumb.util.perpendicularLineTo(head, tail, 40),
+ headLine = jsPlumb.util.perpendicularLineTo(tail, head, 20);
+
+ self.paint(connector, [head, tail, tailLine, headLine], self.lineWidth, "red", null, connectorDimensions);
+
+ return [Math.min(head.x, tail.x), Math.min(head.y, tail.y), Math.max(head.x, tail.x), Math.max(head.y,tail.y)];
+ };
+
+ this.computeMaxSize = function() { return 50; };
+
+ this.cleanup = function() { }; // nothing to clean up for GuideLines
+ };
+
+ // ********************************* END OF OVERLAY DEFINITIONS ***********************************************************************
+
+ // ********************************* OVERLAY CANVAS RENDERERS***********************************************************************
+
+ // ********************************* END OF OVERLAY CANVAS RENDERERS ***********************************************************************
+})();/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.3.8
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the state machine connectors.
+ *
+ * Thanks to Brainstorm Mobile Solutions for supporting the development of these.
+ *
+ * Copyright (c) 2010 - 2012 Simon Porritt (simon.porritt@gmail.com)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+
+;(function() {
+
+ var Line = function(x1, y1, x2, y2) {
+
+ this.m = (y2 - y1) / (x2 - x1);
+ this.b = -1 * ((this.m * x1) - y1);
+
+ this.rectIntersect = function(x,y,w,h) {
+ var results = [];
+
+ // try top face
+ // the equation of the top face is y = (0 * x) + b; y = b.
+ var xInt = (y - this.b) / this.m;
+ // test that the X value is in the line's range.
+ if (xInt >= x && xInt <= (x + w)) results.push([ xInt, (this.m * xInt) + this.b ]);
+
+ // try right face
+ var yInt = (this.m * (x + w)) + this.b;
+ if (yInt >= y && yInt <= (y + h)) results.push([ (yInt - this.b) / this.m, yInt ]);
+
+ // bottom face
+ var xInt = ((y + h) - this.b) / this.m;
+ // test that the X value is in the line's range.
+ if (xInt >= x && xInt <= (x + w)) results.push([ xInt, (this.m * xInt) + this.b ]);
+
+ // try left face
+ var yInt = (this.m * x) + this.b;
+ if (yInt >= y && yInt <= (y + h)) results.push([ (yInt - this.b) / this.m, yInt ]);
+
+ if (results.length == 2) {
+ var midx = (results[0][0] + results[1][0]) / 2, midy = (results[0][1] + results[1][1]) / 2;
+ results.push([ midx,midy ]);
+ // now calculate the segment inside the rectangle where the midpoint lies.
+ var xseg = midx <= x + (w / 2) ? -1 : 1,
+ yseg = midy <= y + (h / 2) ? -1 : 1;
+ results.push([xseg, yseg]);
+ return results;
+ }
+
+ return null;
+
+ };
+ },
+ _segment = function(x1, y1, x2, y2) {
+ if (x1 <= x2 && y2 <= y1) return 1;
+ else if (x1 <= x2 && y1 <= y2) return 2;
+ else if (x2 <= x1 && y2 >= y1) return 3;
+ return 4;
+ },
+
+ // the control point we will use depends on the faces to which each end of the connection is assigned, specifically whether or not the
+ // two faces are parallel or perpendicular. if they are parallel then the control point lies on the midpoint of the axis in which they
+ // are parellel and varies only in the other axis; this variation is proportional to the distance that the anchor points lie from the
+ // center of that face. if the two faces are perpendicular then the control point is at some distance from both the midpoints; the amount and
+ // direction are dependent on the orientation of the two elements. 'seg', passed in to this method, tells you which segment the target element
+ // lies in with respect to the source: 1 is top right, 2 is bottom right, 3 is bottom left, 4 is top left.
+ //
+ // sourcePos and targetPos are arrays of info about where on the source and target each anchor is located. their contents are:
+ //
+ // 0 - absolute x
+ // 1 - absolute y
+ // 2 - proportional x in element (0 is left edge, 1 is right edge)
+ // 3 - proportional y in element (0 is top edge, 1 is bottom edge)
+ //
+ _findControlPoint = function(midx, midy, segment, sourceEdge, targetEdge, dx, dy, distance, proximityLimit) {
+
+ // TODO (maybe)
+ // - if anchor pos is 0.5, make the control point take into account the relative position of the elements.
+ if (distance <= proximityLimit) return [midx, midy];
+
+ if (segment == 1) {
+ if (sourceEdge[3] <= 0 && targetEdge[3] >= 1) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
+ else if (sourceEdge[2] >= 1 && targetEdge[2] <= 0) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
+ else return [ midx + (-1 * dx) , midy + (-1 * dy) ];
+ }
+ else if (segment == 2) {
+ if (sourceEdge[3] >= 1 && targetEdge[3] <= 0) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
+ else if (sourceEdge[2] >= 1 && targetEdge[2] <= 0) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
+ else return [ midx + (1 * dx) , midy + (-1 * dy) ];
+ }
+ else if (segment == 3) {
+ if (sourceEdge[3] >= 1 && targetEdge[3] <= 0) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
+ else if (sourceEdge[2] <= 0 && targetEdge[2] >= 1) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
+ else return [ midx + (-1 * dx) , midy + (-1 * dy) ];
+ }
+ else if (segment == 4) {
+ if (sourceEdge[3] <= 0 && targetEdge[3] >= 1) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
+ else if (sourceEdge[2] <= 0 && targetEdge[2] >= 1) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
+ else return [ midx + (1 * dx) , midy + (-1 * dy) ];
+ }
+ };
+
+ /*
+ Function: StateMachine constructor
+
+ Allowed parameters:
+ curviness - measure of how "curvy" the connectors will be. this is translated as the distance that the
+ Bezier curve's control point is from the midpoint of the straight line connecting the two
+ endpoints, and does not mean that the connector is this wide. The Bezier curve never reaches
+ its control points; they act as gravitational masses. defaults to 10.
+ margin - distance from element to start and end connectors, in pixels. defaults to 5.
+ proximityLimit - sets the distance beneath which the elements are consider too close together to bother with fancy
+ curves. by default this is 80 pixels.
+ loopbackRadius - the radius of a loopback connector. optional; defaults to 25.
+ */
+ jsPlumb.Connectors.StateMachine = function(params) {
+ var self = this,
+ currentPoints = null,
+ _sx, _sy, _tx, _ty, _controlPoint = [],
+ curviness = params.curviness || 10,
+ margin = params.margin || 5,
+ proximityLimit = params.proximityLimit || 80,
+ clockwise = params.orientation && params.orientation == "clockwise",
+ loopbackRadius = params.loopbackRadius || 25,
+ isLoopback = false;
+
+ this.type = "StateMachine";
+ params = params || {};
+
+ this.compute = function(sourcePos, targetPos, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor, lineWidth, minWidth) {
+
+ var w = Math.abs(sourcePos[0] - targetPos[0]),
+ h = Math.abs(sourcePos[1] - targetPos[1]),
+ // these are padding to ensure the whole connector line appears
+ xo = 0.45 * w, yo = 0.45 * h;
+ // these are padding to ensure the whole connector line appears
+ w *= 1.9; h *= 1.9;
+ //ensure at least one pixel width
+ lineWidth = lineWidth || 1;
+ var x = Math.min(sourcePos[0], targetPos[0]) - xo,
+ y = Math.min(sourcePos[1], targetPos[1]) - yo;
+
+ if (sourceEndpoint.elementId != targetEndpoint.elementId) {
+
+ isLoopback = false;
+
+ _sx = sourcePos[0] < targetPos[0] ? xo : w-xo;
+ _sy = sourcePos[1] < targetPos[1] ? yo:h-yo;
+ _tx = sourcePos[0] < targetPos[0] ? w-xo : xo;
+ _ty = sourcePos[1] < targetPos[1] ? h-yo : yo;
+
+ // now adjust for the margin
+ if (sourcePos[2] == 0) _sx -= margin;
+ if (sourcePos[2] == 1) _sx += margin;
+ if (sourcePos[3] == 0) _sy -= margin;
+ if (sourcePos[3] == 1) _sy += margin;
+ if (targetPos[2] == 0) _tx -= margin;
+ if (targetPos[2] == 1) _tx += margin;
+ if (targetPos[3] == 0) _ty -= margin;
+ if (targetPos[3] == 1) _ty += margin;
+
+ //
+ // these connectors are quadratic bezier curves, having a single control point. if both anchors
+ // are located at 0.5 on their respective faces, the control point is set to the midpoint and you
+ // get a straight line. this is also the case if the two anchors are within 'proximityLimit', since
+ // it seems to make good aesthetic sense to do that. outside of that, the control point is positioned
+ // at 'curviness' pixels away along the normal to the straight line connecting the two anchors.
+ //
+ // there may be two improvements to this. firstly, we might actually support the notion of avoiding nodes
+ // in the UI, or at least making a good effort at doing so. if a connection would pass underneath some node,
+ // for example, we might increase the distance the control point is away from the midpoint in a bid to
+ // steer it around that node. this will work within limits, but i think those limits would also be the likely
+ // limits for, once again, aesthetic good sense in the layout of a chart using these connectors.
+ //
+ // the second possible change is actually two possible changes: firstly, it is possible we should gradually
+ // decrease the 'curviness' as the distance between the anchors decreases; start tailing it off to 0 at some
+ // point (which should be configurable). secondly, we might slightly increase the 'curviness' for connectors
+ // with respect to how far their anchor is from the center of its respective face. this could either look cool,
+ // or stupid, and may indeed work only in a way that is so subtle as to have been a waste of time.
+ //
+
+ var _midx = (_sx + _tx) / 2, _midy = (_sy + _ty) / 2,
+ m2 = (-1 * _midx) / _midy, theta2 = Math.atan(m2),
+ dy = (m2 == Infinity || m2 == -Infinity) ? 0 : Math.abs(curviness / 2 * Math.sin(theta2)),
+ dx = (m2 == Infinity || m2 == -Infinity) ? 0 : Math.abs(curviness / 2 * Math.cos(theta2)),
+ segment = _segment(_sx, _sy, _tx, _ty),
+ distance = Math.sqrt(Math.pow(_tx - _sx, 2) + Math.pow(_ty - _sy, 2));
+
+ // calculate the control point. this code will be where we'll put in a rudimentary element avoidance scheme; it
+ // will work by extending the control point to force the curve to be, um, curvier.
+ _controlPoint = _findControlPoint(_midx,
+ _midy,
+ segment,
+ sourcePos,
+ targetPos,
+ curviness, curviness,
+ distance,
+ proximityLimit);
+
+
+ var requiredWidth = Math.max(Math.abs(_controlPoint[0] - _sx) * 3, Math.abs(_controlPoint[0] - _tx) * 3, Math.abs(_tx-_sx), 2 * lineWidth, minWidth),
+ requiredHeight = Math.max(Math.abs(_controlPoint[1] - _sy) * 3, Math.abs(_controlPoint[1] - _ty) * 3, Math.abs(_ty-_sy), 2 * lineWidth, minWidth);
+
+ if (w < requiredWidth) {
+ var dw = requiredWidth - w;
+ x -= (dw / 2);
+ _sx += (dw / 2);
+ _tx += (dw / 2);
+ w = requiredWidth;
+ _controlPoint[0] += (dw / 2);
+ }
+
+ if (h < requiredHeight) {
+ var dh = requiredHeight - h;
+ y -= (dh / 2);
+ _sy += (dh / 2);
+ _ty += (dh / 2);
+ h = requiredHeight;
+ _controlPoint[1] += (dh / 2);
+ }
+ currentPoints = [ x, y, w, h, _sx, _sy, _tx, _ty, _controlPoint[0], _controlPoint[1] ];
+ }
+ else {
+ isLoopback = true;
+ // a loopback connector. draw an arc from one anchor to the other.
+ // i guess we'll do this the same way as the others. just the control point will be a fair distance away.
+ var x1 = sourcePos[0], x2 = sourcePos[0], y1 = sourcePos[1] - margin, y2 = sourcePos[1] - margin,
+ cx = x1, cy = y1 - loopbackRadius;
+
+ // canvas sizing stuff, to ensure the whole painted area is visible.
+ w = ((2 * lineWidth) + (4 * loopbackRadius)), h = ((2 * lineWidth) + (4 * loopbackRadius));
+ x = cx - loopbackRadius - lineWidth - loopbackRadius, y = cy - loopbackRadius - lineWidth - loopbackRadius;
+ currentPoints = [ x, y, w, h, cx-x, cy-y, loopbackRadius, clockwise, x1-x, y1-y, x2-x, y2-y];
+ }
+
+ return currentPoints;
+ };
+
+ var _makeCurve = function() {
+ return [
+ { x:_tx, y:_ty },
+ { x:_controlPoint[0], y:_controlPoint[1] },
+ { x:_controlPoint[0] + 1, y:_controlPoint[1] + 1},
+ { x:_sx, y:_sy }
+ ];
+ };
+
+ /**
+ * returns the point on the connector's path that is 'location' along the length of the path, where 'location' is a decimal from
+ * 0 to 1 inclusive. for the straight line connector this is simple maths. for Bezier, not so much.
+ */
+ this.pointOnPath = function(location) {
+ if (isLoopback) {
+
+ if (location > 0 && location < 1) location = 1- location;
+
+// current points are [ x, y, width, height, center x, center y, radius, clockwise, startx, starty, endx, endy ]
+ // so the path length is the circumference of the circle
+ //var len = 2 * Math.PI * currentPoints[6],
+ // map 'location' to an angle. 0 is PI/2 when the connector is on the top face; if we
+ // support other faces it will have to be calculated for each one. 1 is also PI/2.
+ // 0.5 is -PI/2.
+ var startAngle = (location * 2 * Math.PI) + (Math.PI / 2),
+ startX = currentPoints[4] + (currentPoints[6] * Math.cos(startAngle)),
+ startY = currentPoints[5] + (currentPoints[6] * Math.sin(startAngle));
+
+ return {x:startX, y:startY};
+
+ }
+ else return jsBezier.pointOnCurve(_makeCurve(), location);
+ };
+
+ /**
+ * returns the gradient of the connector at the given point.
+ */
+ this.gradientAtPoint = function(location) {
+ if (isLoopback)
+ return Math.atan(location * 2 * Math.PI);
+ else
+ return jsBezier.gradientAtPoint(_makeCurve(), location);
+ };
+
+ /**
+ * for Bezier curves this method is a little tricky, cos calculating path distance algebraically is notoriously difficult.
+ * this method is iterative, jumping forward .05% of the path at a time and summing the distance between this point and the previous
+ * one, until the sum reaches 'distance'. the method may turn out to be computationally expensive; we'll see.
+ * another drawback of this method is that if the connector gets quite long, .05% of the length of it is not necessarily smaller
+ * than the desired distance, in which case the loop returns immediately and the arrow is mis-shapen. so a better strategy might be to
+ * calculate the step as a function of distance/distance between endpoints.
+ */
+ this.pointAlongPathFrom = function(location, distance) {
+ if (isLoopback) {
+
+ if (location > 0 && location < 1) location = 1- location;
+
+ var circumference = 2 * Math.PI * currentPoints[6],
+ arcSpan = distance / circumference * 2 * Math.PI,
+ startAngle = (location * 2 * Math.PI) - arcSpan + (Math.PI / 2),
+
+ startX = currentPoints[4] + (currentPoints[6] * Math.cos(startAngle)),
+ startY = currentPoints[5] + (currentPoints[6] * Math.sin(startAngle));
+
+ return {x:startX, y:startY};
+ }
+ return jsBezier.pointAlongCurveFrom(_makeCurve(), location, distance);
+ };
+
+ };
+
+ /*
+ * Canvas state machine renderer.
+ */
+ jsPlumb.Connectors.canvas.StateMachine = function(params) {
+ params = params || {};
+ var self = this, drawGuideline = params.drawGuideline || true, avoidSelector = params.avoidSelector;
+ jsPlumb.Connectors.StateMachine.apply(this, arguments);
+ jsPlumb.CanvasConnector.apply(this, arguments);
+
+
+ this._paint = function(dimensions) {
+
+ if (dimensions.length == 10) {
+ self.ctx.beginPath();
+ self.ctx.moveTo(dimensions[4], dimensions[5]);
+ self.ctx.quadraticCurveTo(dimensions[8], dimensions[9], dimensions[6], dimensions[7]);
+ self.ctx.stroke();
+
+ /*/ draw the guideline
+ if (drawGuideline) {
+ self.ctx.save();
+ self.ctx.beginPath();
+ self.ctx.strokeStyle = "silver";
+ self.ctx.lineWidth = 1;
+ self.ctx.moveTo(dimensions[4], dimensions[5]);
+ self.ctx.lineTo(dimensions[6], dimensions[7]);
+ self.ctx.stroke();
+ self.ctx.restore();
+ }
+ //*/
+ }
+ else {
+ // a loopback connector
+ self.ctx.save();
+ self.ctx.beginPath();
+ var startAngle = 0, // Starting point on circle
+ endAngle = 2 * Math.PI, // End point on circle
+ clockwise = dimensions[7]; // clockwise or anticlockwise
+ self.ctx.arc(dimensions[4],dimensions[5],dimensions[6],0, endAngle, clockwise);
+ self.ctx.stroke();
+ self.ctx.closePath();
+ self.ctx.restore();
+ }
+ };
+
+ this.createGradient = function(dim, ctx) {
+ return ctx.createLinearGradient(dim[4], dim[5], dim[6], dim[7]);
+ };
+ };
+
+ /*
+ * SVG State Machine renderer
+ */
+ jsPlumb.Connectors.svg.StateMachine = function() {
+ var self = this;
+ jsPlumb.Connectors.StateMachine.apply(this, arguments);
+ jsPlumb.SvgConnector.apply(this, arguments);
+ this.getPath = function(d) {
+
+ if (d.length == 10)
+ return "M " + d[4] + " " + d[5] + " C " + d[8] + " " + d[9] + " " + d[8] + " " + d[9] + " " + d[6] + " " + d[7];
+ else {
+ // loopback
+ return "M" + (d[8] + 4) + " " + d[9] + " A " + d[6] + " " + d[6] + " 0 1,0 " + (d[8]-4) + " " + d[9];
+ }
+ };
+ };
+
+ /*
+ * VML state machine renderer
+ */
+ jsPlumb.Connectors.vml.StateMachine = function() {
+ jsPlumb.Connectors.StateMachine.apply(this, arguments);
+ jsPlumb.VmlConnector.apply(this, arguments);
+ var _conv = jsPlumb.vml.convertValue;
+ this.getPath = function(d) {
+ if (d.length == 10) {
+ return "m" + _conv(d[4]) + "," + _conv(d[5]) +
+ " c" + _conv(d[8]) + "," + _conv(d[9]) + "," + _conv(d[8]) + "," + _conv(d[9]) + "," + _conv(d[6]) + "," + _conv(d[7]) + " e";
+ }
+ else {
+ // loopback
+ var left = _conv(d[8] - d[6]),
+ top = _conv(d[9] - (2 * d[6])),
+ right = left + _conv(2 * d[6]),
+ bottom = top + _conv(2 * d[6]),
+ posString = left + "," + top + "," + right + "," + bottom;
+
+ var o = "ar " + posString + "," + _conv(d[8]) + ","
+ + _conv(d[9]) + "," + _conv(d[8]) + "," + _conv(d[9]) + " e";
+
+ return o;
+ }
+ };
+ };
+
+})();
+
+/*
+ // now for a rudimentary avoidance scheme. TODO: how to set this in a cross-library way?
+ // if (avoidSelector) {
+ // var testLine = new Line(sourcePos[0] + _sx,sourcePos[1] + _sy,sourcePos[0] + _tx,sourcePos[1] + _ty);
+ // var sel = jsPlumb.getSelector(avoidSelector);
+ // for (var i = 0; i < sel.length; i++) {
+ // var id = jsPlumb.getId(sel[i]);
+ // if (id != sourceEndpoint.elementId && id != targetEndpoint.elementId) {
+ // o = jsPlumb.getOffset(id), s = jsPlumb.getSize(id);
+//
+// if (o && s) {
+// var collision = testLine.rectIntersect(o.left,o.top,s[0],s[1]);
+// if (collision) {
+ // set the control point to be a certain distance from the midpoint of the two points that
+ // the line crosses on the rectangle.
+ // TODO where will this 75 number come from?
+ // _controlX = collision[2][0] + (75 * collision[3][0]);
+ // / _controlY = collision[2][1] + (75 * collision[3][1]);
+// }
+// }
+ // }
+ // }
+ //}
+ *//*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.3.8
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the VML renderers.
+ *
+ * Copyright (c) 2010 - 2012 Simon Porritt (http://jsplumb.org)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+
+;(function() {
+
+ // http://ajaxian.com/archives/the-vml-changes-in-ie-8
+ // http://www.nczonline.net/blog/2010/01/19/internet-explorer-8-document-and-browser-modes/
+ // http://www.louisremi.com/2009/03/30/changes-in-vml-for-ie8-or-what-feature-can-the-ie-dev-team-break-for-you-today/
+
+ var vmlAttributeMap = {
+ "stroke-linejoin":"joinstyle",
+ "joinstyle":"joinstyle",
+ "endcap":"endcap",
+ "miterlimit":"miterlimit"
+ },
+ jsPlumbStylesheet = null;
+
+ if (document.createStyleSheet) {
+
+ var ruleClasses = [
+ ".jsplumb_vml", "jsplumb\\:textbox", "jsplumb\\:oval", "jsplumb\\:rect",
+ "jsplumb\\:stroke", "jsplumb\\:shape", "jsplumb\\:group"
+ ],
+ rule = "behavior:url(#default#VML);position:absolute;";
+
+ jsPlumbStylesheet = document.createStyleSheet();
+
+ for (var i = 0; i < ruleClasses.length; i++)
+ jsPlumbStylesheet.addRule(ruleClasses[i], rule);
+
+ // in this page it is also mentioned that IE requires the extra arg to the namespace
+ // http://www.louisremi.com/2009/03/30/changes-in-vml-for-ie8-or-what-feature-can-the-ie-dev-team-break-for-you-today/
+ // but someone commented saying they didn't need it, and it seems jsPlumb doesnt need it either.
+ // var iev = document.documentMode;
+ //if (!iev || iev < 8)
+ document.namespaces.add("jsplumb", "urn:schemas-microsoft-com:vml");
+ //else
+ // document.namespaces.add("jsplumb", "urn:schemas-microsoft-com:vml", "#default#VML");
+ }
+
+ jsPlumb.vml = {};
+
+ var scale = 1000,
+
+ _groupMap = {},
+ _getGroup = function(container, connectorClass) {
+ var id = jsPlumb.getId(container),
+ g = _groupMap[id];
+ if(!g) {
+ g = _node("group", [0,0,scale, scale], {"class":connectorClass});
+ //g.style.position=absolute;
+ //g["coordsize"] = "1000,1000";
+ g.style.backgroundColor="red";
+ _groupMap[id] = g;
+ jsPlumb.appendElement(g, container); // todo if this gets reinstated, remember to use the current jsplumb instance.
+ //document.body.appendChild(g);
+ }
+ return g;
+ },
+ _atts = function(o, atts) {
+ for (var i in atts) {
+ // IE8 fix: setattribute does not work after an element has been added to the dom!
+ // http://www.louisremi.com/2009/03/30/changes-in-vml-for-ie8-or-what-feature-can-the-ie-dev-team-break-for-you-today/
+ //o.setAttribute(i, atts[i]);
+
+ /*There is an additional problem when accessing VML elements by using get/setAttribute. The simple solution is following:
+
+ if (document.documentMode==8) {
+ ele.opacity=1;
+ } else {
+ ele.setAttribute(‘opacity’,1);
+ }
+ */
+
+ o[i] = atts[i];
+ }
+ },
+ _node = function(name, d, atts, parent, _jsPlumb) {
+ atts = atts || {};
+ var o = document.createElement("jsplumb:" + name);
+ _jsPlumb.appendElement(o, parent);
+ o.className = (atts["class"] ? atts["class"] + " " : "") + "jsplumb_vml";
+ _pos(o, d);
+ _atts(o, atts);
+ return o;
+ },
+ _pos = function(o,d) {
+ o.style.left = d[0] + "px";
+ o.style.top = d[1] + "px";
+ o.style.width= d[2] + "px";
+ o.style.height= d[3] + "px";
+ o.style.position = "absolute";
+ },
+ _conv = jsPlumb.vml.convertValue = function(v) {
+ return Math.floor(v * scale);
+ },
+ // tests if the given style is "transparent" and then sets the appropriate opacity node to 0 if so,
+ // or 1 if not. TODO in the future, support variable opacity.
+ _maybeSetOpacity = function(styleToWrite, styleToCheck, type, component) {
+ if ("transparent" === styleToCheck)
+ component.setOpacity(type, "0.0");
+ else
+ component.setOpacity(type, "1.0");
+ },
+ _applyStyles = function(node, style, component, _jsPlumb) {
+ var styleToWrite = {};
+ if (style.strokeStyle) {
+ styleToWrite["stroked"] = "true";
+ var strokeColor = jsPlumb.util.convertStyle(style.strokeStyle, true);
+ styleToWrite["strokecolor"] = strokeColor;
+ _maybeSetOpacity(styleToWrite, strokeColor, "stroke", component);
+ styleToWrite["strokeweight"] = style.lineWidth + "px";
+ }
+ else styleToWrite["stroked"] = "false";
+
+ if (style.fillStyle) {
+ styleToWrite["filled"] = "true";
+ var fillColor = jsPlumb.util.convertStyle(style.fillStyle, true);
+ styleToWrite["fillcolor"] = fillColor;
+ _maybeSetOpacity(styleToWrite, fillColor, "fill", component);
+ }
+ else styleToWrite["filled"] = "false";
+
+ if(style["dashstyle"]) {
+ if (component.strokeNode == null) {
+ component.strokeNode = _node("stroke", [0,0,0,0], { dashstyle:style["dashstyle"] }, node, _jsPlumb);
+ }
+ else
+ component.strokeNode.dashstyle = style["dashstyle"];
+ }
+ else if (style["stroke-dasharray"] && style["lineWidth"]) {
+ var sep = style["stroke-dasharray"].indexOf(",") == -1 ? " " : ",",
+ parts = style["stroke-dasharray"].split(sep),
+ styleToUse = "";
+ for(var i = 0; i < parts.length; i++) {
+ styleToUse += (Math.floor(parts[i] / style.lineWidth) + sep);
+ }
+ if (component.strokeNode == null) {
+ component.strokeNode = _node("stroke", [0,0,0,0], { dashstyle:styleToUse }, node, _jsPlumb);
+ //node.appendChild(component.strokeNode);
+ }
+ else
+ component.strokeNode.dashstyle = styleToUse;
+ }
+
+ _atts(node, styleToWrite);
+ },
+ /*
+ * Base class for Vml endpoints and connectors. Extends jsPlumbUIComponent.
+ */
+ VmlComponent = function() {
+ var self = this;
+ jsPlumb.jsPlumbUIComponent.apply(this, arguments);
+ this.opacityNodes = {
+ "stroke":null,
+ "fill":null
+ };
+ this.initOpacityNodes = function(vml) {
+ self.opacityNodes["stroke"] = _node("stroke", [0,0,1,1], {opacity:"0.0"}, vml, self._jsPlumb);
+ self.opacityNodes["fill"] = _node("fill", [0,0,1,1], {opacity:"0.0"}, vml, self._jsPlumb);
+ };
+ this.setOpacity = function(type, value) {
+ var node = self.opacityNodes[type];
+ if (node) node["opacity"] = "" + value;
+ };
+ var displayElements = [ ];
+ this.getDisplayElements = function() {
+ return displayElements;
+ };
+
+ this.appendDisplayElement = function(el, doNotAppendToCanvas) {
+ if (!doNotAppendToCanvas) self.canvas.parentNode.appendChild(el);
+ displayElements.push(el);
+ };
+ },
+ /*
+ * Base class for Vml connectors. extends VmlComponent.
+ */
+ VmlConnector = jsPlumb.VmlConnector = function(params) {
+ var self = this;
+ self.strokeNode = null;
+ self.canvas = null;
+ VmlComponent.apply(this, arguments);
+ var clazz = self._jsPlumb.connectorClass + (params.cssClass ? (" " + params.cssClass) : "");
+ this.paint = function(d, style, anchor) {
+ if (style != null) {
+ var path = self.getPath(d), p = { "path":path };
+
+ //*
+ if (style.outlineColor) {
+ var outlineWidth = style.outlineWidth || 1,
+ outlineStrokeWidth = style.lineWidth + (2 * outlineWidth),
+ outlineStyle = {
+ strokeStyle : jsPlumb.util.convertStyle(style.outlineColor),
+ lineWidth : outlineStrokeWidth
+ };
+ for (var aa in vmlAttributeMap) outlineStyle[aa] = style[aa];
+
+ if (self.bgCanvas == null) {
+ p["class"] = clazz;
+ p["coordsize"] = (d[2] * scale) + "," + (d[3] * scale);
+ self.bgCanvas = _node("shape", d, p, params.parent, self._jsPlumb);
+ _pos(self.bgCanvas, d);
+ self.appendDisplayElement(self.bgCanvas, true);
+ self.attachListeners(self.bgCanvas, self);
+ self.initOpacityNodes(self.bgCanvas, ["stroke"]);
+ }
+ else {
+ p["coordsize"] = (d[2] * scale) + "," + (d[3] * scale);
+ _pos(self.bgCanvas, d);
+ _atts(self.bgCanvas, p);
+ }
+
+ _applyStyles(self.bgCanvas, outlineStyle, self);
+ }
+ //*/
+
+ if (self.canvas == null) {
+ p["class"] = clazz;
+ p["coordsize"] = (d[2] * scale) + "," + (d[3] * scale);
+ if (self.tooltip) p["label"] = self.tooltip;
+ self.canvas = _node("shape", d, p, params.parent, self._jsPlumb);
+ //var group = _getGroup(params.parent); // test of append everything to a group
+ //group.appendChild(self.canvas); // sort of works but not exactly;
+ //params["_jsPlumb"].appendElement(self.canvas, params.parent); //before introduction of groups
+
+ self.appendDisplayElement(self.canvas, true);
+ self.attachListeners(self.canvas, self);
+ self.initOpacityNodes(self.canvas, ["stroke"]);
+ }
+ else {
+ p["coordsize"] = (d[2] * scale) + "," + (d[3] * scale);
+ _pos(self.canvas, d);
+ _atts(self.canvas, p);
+ }
+
+ _applyStyles(self.canvas, style, self, self._jsPlumb);
+ }
+ };
+
+ //self.appendDisplayElement(self.canvas);
+
+ this.reattachListeners = function() {
+ if (self.canvas) self.reattachListenersForElement(self.canvas, self);
+ };
+ },
+ /*
+ *
+ * Base class for Vml Endpoints. extends VmlComponent.
+ *
+ */
+ VmlEndpoint = function(params) {
+ VmlComponent.apply(this, arguments);
+ var vml = null, self = this, opacityStrokeNode = null, opacityFillNode = null;
+ self.canvas = document.createElement("div");
+ self.canvas.style["position"] = "absolute";
+
+ var clazz = self._jsPlumb.endpointClass + (params.cssClass ? (" " + params.cssClass) : "");
+
+ //var group = _getGroup(params.parent);
+ //group.appendChild(self.canvas);
+ params["_jsPlumb"].appendElement(self.canvas, params.parent);
+
+ if (self.tooltip) self.canvas.setAttribute("label", self.tooltip);
+
+ this.paint = function(d, style, anchor) {
+ var p = { };
+
+ jsPlumb.sizeCanvas(self.canvas, d[0], d[1], d[2], d[3]);
+ if (vml == null) {
+ p["class"] = clazz;
+ vml = self.getVml([0,0, d[2], d[3]], p, anchor, self.canvas, self._jsPlumb);
+ self.attachListeners(vml, self);
+
+ self.appendDisplayElement(vml, true);
+ self.appendDisplayElement(self.canvas, true);
+
+ self.initOpacityNodes(vml, ["fill"]);
+ }
+ else {
+ _pos(vml, [0,0, d[2], d[3]]);
+ _atts(vml, p);
+ }
+
+ _applyStyles(vml, style, self);
+ };
+
+ this.reattachListeners = function() {
+ if (vml) self.reattachListenersForElement(vml, self);
+ };
+ };
+
+ jsPlumb.Connectors.vml.Bezier = function() {
+ jsPlumb.Connectors.Bezier.apply(this, arguments);
+ VmlConnector.apply(this, arguments);
+ this.getPath = function(d) {
+ return "m" + _conv(d[4]) + "," + _conv(d[5]) +
+ " c" + _conv(d[8]) + "," + _conv(d[9]) + "," + _conv(d[10]) + "," + _conv(d[11]) + "," + _conv(d[6]) + "," + _conv(d[7]) + " e";
+ };
+ };
+
+ jsPlumb.Connectors.vml.Straight = function() {
+ jsPlumb.Connectors.Straight.apply(this, arguments);
+ VmlConnector.apply(this, arguments);
+ this.getPath = function(d) {
+ return "m" + _conv(d[4]) + "," + _conv(d[5]) + " l" + _conv(d[6]) + "," + _conv(d[7]) + " e";
+ };
+ };
+
+ jsPlumb.Connectors.vml.Flowchart = function() {
+ jsPlumb.Connectors.Flowchart.apply(this, arguments);
+ VmlConnector.apply(this, arguments);
+ this.getPath = function(dimensions) {
+ var p = "m " + _conv(dimensions[4]) + "," + _conv(dimensions[5]) + " l";
+ // loop through extra points
+ for (var i = 0; i < dimensions[8]; i++) {
+ p = p + " " + _conv(dimensions[9 + (i*2)]) + "," + _conv(dimensions[10 + (i*2)]);
+ }
+ // finally draw a line to the end
+ p = p + " " + _conv(dimensions[6]) + "," + _conv(dimensions[7]) + " e";
+ return p;
+ };
+ };
+
+ jsPlumb.Endpoints.vml.Dot = function() {
+ jsPlumb.Endpoints.Dot.apply(this, arguments);
+ VmlEndpoint.apply(this, arguments);
+ this.getVml = function(d, atts, anchor, parent, _jsPlumb) { return _node("oval", d, atts, parent, _jsPlumb); };
+ };
+
+ jsPlumb.Endpoints.vml.Rectangle = function() {
+ jsPlumb.Endpoints.Rectangle.apply(this, arguments);
+ VmlEndpoint.apply(this, arguments);
+ this.getVml = function(d, atts, anchor, parent, _jsPlumb) { return _node("rect", d, atts, parent, _jsPlumb); };
+ };
+
+ /*
+ * VML Image Endpoint is the same as the default image endpoint.
+ */
+ jsPlumb.Endpoints.vml.Image = jsPlumb.Endpoints.Image;
+
+ /**
+ * placeholder for Blank endpoint in vml renderer.
+ */
+ jsPlumb.Endpoints.vml.Blank = jsPlumb.Endpoints.Blank;
+
+ /**
+ * VML Label renderer. uses the default label renderer (which adds an element to the DOM)
+ */
+ jsPlumb.Overlays.vml.Label = jsPlumb.Overlays.Label;
+
+ var AbstractVmlArrowOverlay = function(superclass, originalArgs) {
+ superclass.apply(this, originalArgs);
+ VmlComponent.apply(this, arguments);
+ var self = this, path = null;
+ self.canvas = null;
+ var getPath = function(d, connectorDimensions) {
+ return "m " + _conv(d.hxy.x) + "," + _conv(d.hxy.y) +
+ " l " + _conv(d.tail[0].x) + "," + _conv(d.tail[0].y) +
+ " " + _conv(d.cxy.x) + "," + _conv(d.cxy.y) +
+ " " + _conv(d.tail[1].x) + "," + _conv(d.tail[1].y) +
+ " x e";
+ };
+ this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle, connectorDimensions) {
+ var p = {};
+ if (strokeStyle) {
+ p["stroked"] = "true";
+ p["strokecolor"] = jsPlumb.util.convertStyle(strokeStyle, true);
+ }
+ if (lineWidth) p["strokeweight"] = lineWidth + "px";
+ if (fillStyle) {
+ p["filled"] = "true";
+ p["fillcolor"] = fillStyle;
+ }
+ var xmin = Math.min(d.hxy.x, d.tail[0].x, d.tail[1].x, d.cxy.x),
+ ymin = Math.min(d.hxy.y, d.tail[0].y, d.tail[1].y, d.cxy.y),
+ xmax = Math.max(d.hxy.x, d.tail[0].x, d.tail[1].x, d.cxy.x),
+ ymax = Math.max(d.hxy.y, d.tail[0].y, d.tail[1].y, d.cxy.y),
+ w = Math.abs(xmax - xmin),
+ h = Math.abs(ymax - ymin),
+ dim = [xmin, ymin, w, h];
+
+ // for VML, we create overlays using shapes that have the same dimensions and
+ // coordsize as their connector - overlays calculate themselves relative to the
+ // connector (it's how it's been done since the original canvas implementation, because
+ // for canvas that makes sense).
+ p["path"] = getPath(d, connectorDimensions);
+ p["coordsize"] = (connectorDimensions[2] * scale) + "," + (connectorDimensions[3] * scale);
+
+ dim[0] = connectorDimensions[0];
+ dim[1] = connectorDimensions[1];
+ dim[2] = connectorDimensions[2];
+ dim[3] = connectorDimensions[3];
+
+ if (self.canvas == null) {
+ //p["class"] = jsPlumb.overlayClass; // TODO currentInstance?
+ self.canvas = _node("shape", dim, p, connector.canvas.parentNode, connector._jsPlumb);
+ connector.appendDisplayElement(self.canvas, true);
+ self.attachListeners(self.canvas, connector);
+ }
+ else {
+ _pos(self.canvas, dim);
+ _atts(self.canvas, p);
+ }
+ };
+
+ this.reattachListeners = function() {
+ if (self.canvas) self.reattachListenersForElement(self.canvas, self);
+ };
+ };
+
+ jsPlumb.Overlays.vml.Arrow = function() {
+ AbstractVmlArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]);
+ };
+
+ jsPlumb.Overlays.vml.PlainArrow = function() {
+ AbstractVmlArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]);
+ };
+
+ jsPlumb.Overlays.vml.Diamond = function() {
+ AbstractVmlArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]);
+ };
+})();/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.3.8
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the SVG renderers.
+ *
+ * Copyright (c) 2010 - 2012 Simon Porritt (http://jsplumb.org)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+
+/**
+ * SVG support for jsPlumb.
+ *
+ * things to investigate:
+ *
+ * gradients: https://developer.mozilla.org/en/svg_in_html_introduction
+ * css:http://tutorials.jenkov.com/svg/svg-and-css.html
+ * text on a path: http://www.w3.org/TR/SVG/text.html#TextOnAPath
+ * pointer events: https://developer.mozilla.org/en/css/pointer-events
+ *
+ * IE9 hover jquery: http://forum.jquery.com/topic/1-6-2-broke-svg-hover-events
+ *
+ */
+;(function() {
+
+ var svgAttributeMap = {
+ "joinstyle":"stroke-linejoin",
+ "stroke-linejoin":"stroke-linejoin",
+ "stroke-dashoffset":"stroke-dashoffset",
+ "stroke-linecap":"stroke-linecap"
+ },
+ STROKE_DASHARRAY = "stroke-dasharray",
+ DASHSTYLE = "dashstyle",
+ LINEAR_GRADIENT = "linearGradient",
+ RADIAL_GRADIENT = "radialGradient",
+ FILL = "fill",
+ STOP = "stop",
+ STROKE = "stroke",
+ STROKE_WIDTH = "stroke-width",
+ STYLE = "style",
+ NONE = "none",
+ JSPLUMB_GRADIENT = "jsplumb_gradient_",
+ LINE_WIDTH = "lineWidth",
+ ns = {
+ svg:"http://www.w3.org/2000/svg",
+ xhtml:"http://www.w3.org/1999/xhtml"
+ },
+ _attr = function(node, attributes) {
+ for (var i in attributes)
+ node.setAttribute(i, "" + attributes[i]);
+ },
+ _node = function(name, attributes) {
+ var n = document.createElementNS(ns.svg, name);
+ attributes = attributes || {};
+ attributes["version"] = "1.1";
+ attributes["xmlns"] = ns.xhtml;
+ _attr(n, attributes);
+ return n;
+ },
+ _pos = function(d) { return "position:absolute;left:" + d[0] + "px;top:" + d[1] + "px"; },
+ _clearGradient = function(parent) {
+ for (var i = 0; i < parent.childNodes.length; i++) {
+ if (parent.childNodes[i].tagName == LINEAR_GRADIENT || parent.childNodes[i].tagName == RADIAL_GRADIENT)
+ parent.removeChild(parent.childNodes[i]);
+ }
+ },
+ _updateGradient = function(parent, node, style, dimensions, uiComponent) {
+ var id = JSPLUMB_GRADIENT + uiComponent._jsPlumb.idstamp();
+ // first clear out any existing gradient
+ _clearGradient(parent);
+ // this checks for an 'offset' property in the gradient, and in the absence of it, assumes
+ // we want a linear gradient. if it's there, we create a radial gradient.
+ // it is possible that a more explicit means of defining the gradient type would be
+ // better. relying on 'offset' means that we can never have a radial gradient that uses
+ // some default offset, for instance.
+ if (!style.gradient.offset) {
+ var g = _node(LINEAR_GRADIENT, {id:id});
+ parent.appendChild(g);
+ }
+ else {
+ var g = _node(RADIAL_GRADIENT, {
+ id:id
+ });
+ parent.appendChild(g);
+ }
+
+ // the svg radial gradient seems to treat stops in the reverse
+ // order to how canvas does it. so we want to keep all the maths the same, but
+ // iterate the actual style declarations in reverse order, if the x indexes are not in order.
+ for (var i = 0; i < style.gradient.stops.length; i++) {
+ // Straight Connectors and Bezier connectors act slightly differently; this code is a bit of a kludge. but next version of
+ // jsplumb will be replacing both Straight and Bezier to be generic instances of 'Connector', which has a list of segments.
+ // so, not too concerned about leaving this in for now.
+ var styleToUse = i;
+ if (dimensions.length == 8)
+ styleToUse = dimensions[4] < dimensions[6] ? i: style.gradient.stops.length - 1 - i;
+ else
+ styleToUse = dimensions[4] < dimensions[6] ? style.gradient.stops.length - 1 - i : i;
+ var stopColor = jsPlumb.util.convertStyle(style.gradient.stops[styleToUse][1], true);
+ var s = _node(STOP, {"offset":Math.floor(style.gradient.stops[i][0] * 100) + "%", "stop-color":stopColor});
+ g.appendChild(s);
+ }
+ var applyGradientTo = style.strokeStyle ? STROKE : FILL;
+ node.setAttribute(STYLE, applyGradientTo + ":url(#" + id + ")");
+ },
+ _applyStyles = function(parent, node, style, dimensions, uiComponent) {
+
+ if (style.gradient) {
+ _updateGradient(parent, node, style, dimensions, uiComponent);
+ }
+ else {
+ // make sure we clear any existing gradient
+ _clearGradient(parent);
+ node.setAttribute(STYLE, "");
+ }
+
+ node.setAttribute(FILL, style.fillStyle ? jsPlumb.util.convertStyle(style.fillStyle, true) : NONE);
+ node.setAttribute(STROKE, style.strokeStyle ? jsPlumb.util.convertStyle(style.strokeStyle, true) : NONE);
+ if (style.lineWidth) {
+ node.setAttribute(STROKE_WIDTH, style.lineWidth);
+ }
+
+ // in SVG there is a stroke-dasharray attribute we can set, and its syntax looks like
+ // the syntax in VML but is actually kind of nasty: values are given in the pixel
+ // coordinate space, whereas in VML they are multiples of the width of the stroked
+ // line, which makes a lot more sense. for that reason, jsPlumb is supporting both
+ // the native svg 'stroke-dasharray' attribute, and also the 'dashstyle' concept from
+ // VML, which will be the preferred method. the code below this converts a dashstyle
+ // attribute given in terms of stroke width into a pixel representation, by using the
+ // stroke's lineWidth.
+ if (style[DASHSTYLE] && style[LINE_WIDTH] && !style[STROKE_DASHARRAY]) {
+ var sep = style[DASHSTYLE].indexOf(",") == -1 ? " " : ",",
+ parts = style[DASHSTYLE].split(sep),
+ styleToUse = "";
+ parts.forEach(function(p) {
+ styleToUse += (Math.floor(p * style.lineWidth) + sep);
+ });
+ node.setAttribute(STROKE_DASHARRAY, styleToUse);
+ }
+ else if(style[STROKE_DASHARRAY]) {
+ node.setAttribute(STROKE_DASHARRAY, style[STROKE_DASHARRAY]);
+ }
+
+ // extra attributes such as join type, dash offset.
+ for (var i in svgAttributeMap) {
+ if (style[i]) {
+ node.setAttribute(svgAttributeMap[i], style[i]);
+ }
+ }
+ },
+ _decodeFont = function(f) {
+ var r = /([0-9].)(p[xt])\s(.*)/;
+ var bits = f.match(r);
+ return {size:bits[1] + bits[2], font:bits[3]};
+ },
+ _classManip = function(el, add, clazz) {
+ var classesToAddOrRemove = clazz.split(" "),
+ className = el.className,
+ curClasses = className.baseVal.split(" ");
+
+ for (var i = 0; i < classesToAddOrRemove.length; i++) {
+ if (add) {
+ if (curClasses.indexOf(classesToAddOrRemove[i]) == -1)
+ curClasses.push(classesToAddOrRemove[i]);
+ }
+ else {
+ var idx = curClasses.indexOf(classesToAddOrRemove[i]);
+ if (idx != -1)
+ curClasses.splice(idx, 1);
+ }
+ }
+
+ el.className.baseVal = curClasses.join(" ");
+ },
+ _addClass = function(el, clazz) {
+ _classManip(el, true, clazz);
+ },
+ _removeClass = function(el, clazz) {
+ _classManip(el, false, clazz);
+ };
+
+ /**
+ utility methods for other objects to use.
+ */
+ jsPlumb.util.svg = {
+ addClass:_addClass,
+ removeClass:_removeClass
+ };
+
+ /*
+ * Base class for SVG components.
+ */
+ //var SvgComponent = function(cssClass, originalArgs, pointerEventsSpec) {
+ var SvgComponent = function(params) {
+ var self = this,
+ pointerEventsSpec = params.pointerEventsSpec || "all";
+ jsPlumb.jsPlumbUIComponent.apply(this, params.originalArgs);
+ self.canvas = null, self.path = null, self.svg = null;
+
+ var clazz = params.cssClass + " " + (params.originalArgs[0].cssClass || ""),
+ svgParams = {
+ "style":"",
+ "width":0,
+ "height":0,
+ "pointer-events":pointerEventsSpec,
+ "position":"absolute"
+ };
+ if (self.tooltip) svgParams["title"] = self.tooltip;
+ self.svg = _node("svg", svgParams);
+ if (params.useDivWrapper) {
+ self.canvas = document.createElement("div");
+ self.canvas.style["position"] = "absolute";
+ jsPlumb.sizeCanvas(self.canvas,0,0,1,1);
+ self.canvas.className = clazz;
+ if (self.tooltip) self.canvas.setAttribute("title", self.tooltip);
+ }
+ else {
+ _attr(self.svg, { "class":clazz });
+ self.canvas = self.svg;
+ }
+
+ params._jsPlumb.appendElement(self.canvas, params.originalArgs[0]["parent"]);
+ if (params.useDivWrapper) self.canvas.appendChild(self.svg);
+
+ // TODO this displayElement stuff is common between all components, across all
+ // renderers. would be best moved to jsPlumbUIComponent.
+ var displayElements = [ self.canvas ];
+ this.getDisplayElements = function() {
+ return displayElements;
+ };
+
+ this.appendDisplayElement = function(el) {
+ displayElements.push(el);
+ };
+
+ this.paint = function(d, style, anchor) {
+ if (style != null) {
+ var x = d[0], y = d[1];
+ if (params.useDivWrapper) {
+ jsPlumb.sizeCanvas(self.canvas, d[0], d[1], d[2], d[3]);
+ x = 0, y = 0;
+ }
+ _attr(self.svg, {
+ "style":_pos([x, y, d[2], d[3]]),
+ "width": d[2],
+ "height": d[3]
+ });
+ self._paint.apply(this, arguments);
+ }
+ };
+ };
+
+ /*
+ * Base class for SVG connectors.
+ */
+ var SvgConnector = jsPlumb.SvgConnector = function(params) {
+ var self = this;
+ SvgComponent.apply(this, [ {
+ cssClass:params["_jsPlumb"].connectorClass,
+ originalArgs:arguments,
+ pointerEventsSpec:"none",
+ tooltip:params.tooltip,
+ _jsPlumb:params["_jsPlumb"]
+ } ]);
+ this._paint = function(d, style) {
+ var p = self.getPath(d), a = { "d":p }, outlineStyle = null;
+ a["pointer-events"] = "all";
+
+ // outline style. actually means drawing an svg object underneath the main one.
+ if (style.outlineColor) {
+ var outlineWidth = style.outlineWidth || 1,
+ outlineStrokeWidth = style.lineWidth + (2 * outlineWidth),
+ outlineStyle = jsPlumb.CurrentLibrary.extend({}, style);
+ outlineStyle.strokeStyle = jsPlumb.util.convertStyle(style.outlineColor);
+ outlineStyle.lineWidth = outlineStrokeWidth;
+
+ if (self.bgPath == null) {
+ self.bgPath = _node("path", a);
+ self.svg.appendChild(self.bgPath);
+ self.attachListeners(self.bgPath, self);
+ }
+ else {
+ _attr(self.bgPath, a);
+ }
+
+ _applyStyles(self.svg, self.bgPath, outlineStyle, d, self);
+ }
+
+
+ // test - see below
+ // a["clip-path"]= "url(#testClip)";
+
+ if (self.path == null) {
+ self.path = _node("path", a);
+ self.svg.appendChild(self.path);
+ self.attachListeners(self.path, self);
+
+ /*
+ this is a test of a clip path. i'm looking into using one of these to animate a jsplumb connection.
+ you could do this by walking along the line, stepping along a little at a time, and setting the clip
+ path to extend as far as that point.
+
+ self.clip = _node("clipPath", {id:"testClip", clipPathUnits:"objectBoundingBox"});
+ self.svg.appendChild(self.clip);
+ self.clip.appendChild(_node("rect", {
+ x:"0",y:"0",width:"0.5",height:"1"
+ }));
+ */
+ }
+ else {
+ _attr(self.path, a);
+ }
+
+ _applyStyles(self.svg, self.path, style, d, self);
+ };
+
+ this.reattachListeners = function() {
+ if (self.bgPath) self.reattachListenersForElement(self.bgPath, self);
+ if (self.path) self.reattachListenersForElement(self.path, self);
+ };
+
+ };
+
+ /*
+ * SVG Bezier Connector
+ */
+ jsPlumb.Connectors.svg.Bezier = function(params) {
+ jsPlumb.Connectors.Bezier.apply(this, arguments);
+ SvgConnector.apply(this, arguments);
+ this.getPath = function(d) {
+ var _p = "M " + d[4] + " " + d[5];
+ _p += (" C " + d[8] + " " + d[9] + " " + d[10] + " " + d[11] + " " + d[6] + " " + d[7]);
+ return _p;
+ };
+ };
+
+ /*
+ * SVG straight line Connector
+ */
+ jsPlumb.Connectors.svg.Straight = function(params) {
+ jsPlumb.Connectors.Straight.apply(this, arguments);
+ SvgConnector.apply(this, arguments);
+ this.getPath = function(d) { return "M " + d[4] + " " + d[5] + " L " + d[6] + " " + d[7]; };
+ };
+
+ jsPlumb.Connectors.svg.Flowchart = function() {
+ var self = this;
+ jsPlumb.Connectors.Flowchart.apply(this, arguments);
+ SvgConnector.apply(this, arguments);
+ this.getPath = function(dimensions) {
+ var p = "M " + dimensions[4] + "," + dimensions[5];
+ // loop through extra points
+ for (var i = 0; i < dimensions[8]; i++) {
+ p = p + " L " + dimensions[9 + (i*2)] + " " + dimensions[10 + (i*2)];
+ }
+ // finally draw a line to the end
+ p = p + " " + dimensions[6] + "," + dimensions[7];
+ return p;
+ };
+ };
+
+ /*
+ * Base class for SVG endpoints.
+ */
+ var SvgEndpoint = function(params) {
+ var self = this;
+ SvgComponent.apply(this, [ {
+ cssClass:params["_jsPlumb"].endpointClass,
+ originalArgs:arguments,
+ pointerEventsSpec:"all",
+ useDivWrapper:true,
+ _jsPlumb:params["_jsPlumb"]
+ } ]);
+ this._paint = function(d, style) {
+ var s = jsPlumb.extend({}, style);
+ if (s.outlineColor) {
+ s.strokeWidth = s.outlineWidth;
+ s.strokeStyle = jsPlumb.util.convertStyle(s.outlineColor, true);
+ }
+
+ if (self.node == null) {
+ self.node = self.makeNode(d, s);
+ self.svg.appendChild(self.node);
+ self.attachListeners(self.node, self);
+ }
+ _applyStyles(self.svg, self.node, s, d, self);
+ _pos(self.node, d);
+ };
+
+ this.reattachListeners = function() {
+ if (self.node) self.reattachListenersForElement(self.node, self);
+ };
+ };
+
+ /*
+ * SVG Dot Endpoint
+ */
+ jsPlumb.Endpoints.svg.Dot = function() {
+ jsPlumb.Endpoints.Dot.apply(this, arguments);
+ SvgEndpoint.apply(this, arguments);
+ this.makeNode = function(d, style) {
+ return _node("circle", {
+ "cx" : d[2] / 2,
+ "cy" : d[3] / 2,
+ "r" : d[2] / 2
+ });
+ };
+ };
+
+ /*
+ * SVG Rectangle Endpoint
+ */
+ jsPlumb.Endpoints.svg.Rectangle = function() {
+ jsPlumb.Endpoints.Rectangle.apply(this, arguments);
+ SvgEndpoint.apply(this, arguments);
+ this.makeNode = function(d, style) {
+ return _node("rect", {
+ "width":d[2],
+ "height":d[3]
+ });
+ };
+ };
+
+ /*
+ * SVG Image Endpoint is the default image endpoint.
+ */
+ jsPlumb.Endpoints.svg.Image = jsPlumb.Endpoints.Image;
+ /*
+ * Blank endpoint in svg renderer is the default Blank endpoint.
+ */
+ jsPlumb.Endpoints.svg.Blank = jsPlumb.Endpoints.Blank;
+ /*
+ * Label endpoint in svg renderer is the default Label endpoint.
+ */
+ jsPlumb.Overlays.svg.Label = jsPlumb.Overlays.Label;
+
+ var AbstractSvgArrowOverlay = function(superclass, originalArgs) {
+ superclass.apply(this, originalArgs);
+ jsPlumb.jsPlumbUIComponent.apply(this, originalArgs);
+ this.isAppendedAtTopLevel = false;
+ var self = this, path =null;
+ this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle) {
+ if (path == null) {
+ path = _node("path");
+ connector.svg.appendChild(path);
+ self.attachListeners(path, connector);
+ self.attachListeners(path, self);
+ }
+ var clazz = originalArgs && (originalArgs.length == 1) ? (originalArgs[0].cssClass || "") : "";
+
+ _attr(path, {
+ "d" : makePath(d),
+ "class" : clazz,
+ stroke : strokeStyle ? strokeStyle : null,
+ fill : fillStyle ? fillStyle : null
+ });
+ };
+ var makePath = function(d) {
+ return "M" + d.hxy.x + "," + d.hxy.y +
+ " L" + d.tail[0].x + "," + d.tail[0].y +
+ " L" + d.cxy.x + "," + d.cxy.y +
+ " L" + d.tail[1].x + "," + d.tail[1].y +
+ " L" + d.hxy.x + "," + d.hxy.y;
+ };
+ this.reattachListeners = function() {
+ if (path) self.reattachListenersForElement(path, self);
+ };
+ };
+
+ jsPlumb.Overlays.svg.Arrow = function() {
+ AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]);
+ };
+
+ jsPlumb.Overlays.svg.PlainArrow = function() {
+ AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]);
+ };
+
+ jsPlumb.Overlays.svg.Diamond = function() {
+ AbstractSvgArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]);
+ };
+
+ // a test
+ jsPlumb.Overlays.svg.GuideLines = function() {
+ var path = null, self = this, path2 = null, p1_1, p1_2;
+ jsPlumb.Overlays.GuideLines.apply(this, arguments);
+ this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle) {
+ if (path == null) {
+ path = _node("path");
+ connector.svg.appendChild(path);
+ self.attachListeners(path, connector);
+ self.attachListeners(path, self);
+
+ p1_1 = _node("path");
+ connector.svg.appendChild(p1_1);
+ self.attachListeners(p1_1, connector);
+ self.attachListeners(p1_1, self);
+
+ p1_2 = _node("path");
+ connector.svg.appendChild(p1_2);
+ self.attachListeners(p1_2, connector);
+ self.attachListeners(p1_2, self);
+
+ }
+
+ _attr(path, {
+ "d" : makePath(d[0], d[1]),
+ stroke : "red",
+ fill : null
+ });
+
+ _attr(p1_1, {
+ "d" : makePath(d[2][0], d[2][1]),
+ stroke : "blue",
+ fill : null
+ });
+
+ _attr(p1_2, {
+ "d" : makePath(d[3][0], d[3][1]),
+ stroke : "green",
+ fill : null
+ });
+ };
+
+ var makePath = function(d1, d2) {
+ return "M " + d1.x + "," + d1.y +
+ " L" + d2.x + "," + d2.y;
+ };
+
+ };
+})();/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.3.8
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the HTML5 canvas renderers.
+ *
+ * Copyright (c) 2010 - 2012 Simon Porritt (http://jsplumb.org)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+
+;(function() {
+
+// ********************************* CANVAS RENDERERS FOR CONNECTORS AND ENDPOINTS *******************************************************************
+
+ // TODO refactor to renderer common script. put a ref to jsPlumb.sizeCanvas in there too.
+ var _connectionBeingDragged = null,
+ _hasClass = function(el, clazz) { return jsPlumb.CurrentLibrary.hasClass(_getElementObject(el), clazz); },
+ _getElementObject = function(el) { return jsPlumb.CurrentLibrary.getElementObject(el); },
+ _getOffset = function(el) { return jsPlumb.CurrentLibrary.getOffset(_getElementObject(el)); },
+ _pageXY = function(el) { return jsPlumb.CurrentLibrary.getPageXY(el); },
+ _clientXY = function(el) { return jsPlumb.CurrentLibrary.getClientXY(el); };
+
+ /*
+ * Class:CanvasMouseAdapter
+ * Provides support for mouse events on canvases.
+ */
+ var CanvasMouseAdapter = function() {
+ var self = this;
+ self.overlayPlacements = [];
+ jsPlumb.jsPlumbUIComponent.apply(this, arguments);
+ jsPlumb.EventGenerator.apply(this, arguments);
+ /**
+ * returns whether or not the given event is ojver a painted area of the canvas.
+ */
+ this._over = function(e) {
+ var o = _getOffset(_getElementObject(self.canvas)),
+ pageXY = _pageXY(e),
+ x = pageXY[0] - o.left, y = pageXY[1] - o.top;
+ if (x > 0 && y > 0 && x < self.canvas.width && y < self.canvas.height) {
+ // first check overlays
+ for ( var i = 0; i < self.overlayPlacements.length; i++) {
+ var p = self.overlayPlacements[i];
+ if (p && (p[0] <= x && p[1] >= x && p[2] <= y && p[3] >= y))
+ return true;
+ }
+
+ // then the canvas
+ var d = self.canvas.getContext("2d").getImageData(parseInt(x), parseInt(y), 1, 1);
+ return d.data[0] != 0 || d.data[1] != 0 || d.data[2] != 0 || d.data[3] != 0;
+ }
+ return false;
+ };
+
+ var _mouseover = false, _mouseDown = false, _posWhenMouseDown = null, _mouseWasDown = false,
+ _nullSafeHasClass = function(el, clazz) {
+ return el != null && _hasClass(el, clazz);
+ };
+ this.mousemove = function(e) {
+ var pageXY = _pageXY(e), clientXY = _clientXY(e),
+ ee = document.elementFromPoint(clientXY[0], clientXY[1]),
+ eventSourceWasOverlay = _nullSafeHasClass(ee, "_jsPlumb_overlay");
+ var _continue = _connectionBeingDragged == null && (_nullSafeHasClass(ee, "_jsPlumb_endpoint") || _nullSafeHasClass(ee, "_jsPlumb_connector"));
+ if (!_mouseover && _continue && self._over(e)) {
+ _mouseover = true;
+ self.fire("mouseenter", self, e);
+ return true;
+ }
+ // TODO here there is a remote chance that the overlay the mouse moved onto
+ // is actually not an overlay for the current component. a more thorough check would
+ // be to ensure the overlay belonged to the current component.
+ else if (_mouseover && (!self._over(e) || !_continue) && !eventSourceWasOverlay) {
+ _mouseover = false;
+ self.fire("mouseexit", self, e);
+ }
+ self.fire("mousemove", self, e);
+ };
+
+ this.click = function(e) {
+ if (_mouseover && self._over(e) && !_mouseWasDown)
+ self.fire("click", self, e);
+ _mouseWasDown = false;
+ };
+
+ this.dblclick = function(e) {
+ if (_mouseover && self._over(e) && !_mouseWasDown)
+ self.fire("dblclick", self, e);
+ _mouseWasDown = false;
+ };
+
+ this.mousedown = function(e) {
+ if(self._over(e) && !_mouseDown) {
+ _mouseDown = true;
+ _posWhenMouseDown = _getOffset(_getElementObject(self.canvas));
+ self.fire("mousedown", self, e);
+ }
+ };
+
+ this.mouseup = function(e) {
+ _mouseDown = false;
+ self.fire("mouseup", self, e);
+ };
+
+ this.contextmenu = function(e) {
+ if (_mouseover && self._over(e) && !_mouseWasDown)
+ self.fire("contextmenu", self, e);
+ _mouseWasDown = false;
+ };
+ };
+
+ var _newCanvas = function(params) {
+ var canvas = document.createElement("canvas");
+ params["_jsPlumb"].appendElement(canvas, params.parent);
+ canvas.style.position = "absolute";
+ if (params["class"]) canvas.className = params["class"];
+ // set an id. if no id on the element and if uuid was supplied it
+ // will be used, otherwise we'll create one.
+ params["_jsPlumb"].getId(canvas, params.uuid);
+ if (params.tooltip) canvas.setAttribute("title", params.tooltip);
+
+ return canvas;
+ };
+
+ var CanvasComponent = function(params) {
+ CanvasMouseAdapter.apply(this, arguments);
+
+ var displayElements = [ ];
+ this.getDisplayElements = function() { return displayElements; };
+ this.appendDisplayElement = function(el) { displayElements.push(el); };
+ }
+
+ /**
+ * Class:CanvasConnector
+ * Superclass for Canvas Connector renderers.
+ */
+ var CanvasConnector = jsPlumb.CanvasConnector = function(params) {
+
+ CanvasComponent.apply(this, arguments);
+
+ var _paintOneStyle = function(dim, aStyle) {
+ self.ctx.save();
+ jsPlumb.extend(self.ctx, aStyle);
+ if (aStyle.gradient) {
+ var g = self.createGradient(dim, self.ctx);
+ for ( var i = 0; i < aStyle.gradient.stops.length; i++)
+ g.addColorStop(aStyle.gradient.stops[i][0], aStyle.gradient.stops[i][1]);
+ self.ctx.strokeStyle = g;
+ }
+ self._paint(dim);
+ self.ctx.restore();
+ };
+
+ var self = this,
+ clazz = self._jsPlumb.connectorClass + " " + (params.cssClass || "");
+ self.canvas = _newCanvas({
+ "class":clazz,
+ _jsPlumb:self._jsPlumb,
+ parent:params.parent,
+ tooltip:params.tooltip
+ });
+ self.ctx = self.canvas.getContext("2d");
+
+ self.appendDisplayElement(self.canvas);
+
+ self.paint = function(dim, style) {
+ if (style != null) {
+ jsPlumb.sizeCanvas(self.canvas, dim[0], dim[1], dim[2], dim[3]);
+ if (style.outlineColor != null) {
+ var outlineWidth = style.outlineWidth || 1,
+ outlineStrokeWidth = style.lineWidth + (2 * outlineWidth),
+ outlineStyle = {
+ strokeStyle:style.outlineColor,
+ lineWidth:outlineStrokeWidth
+ };
+ _paintOneStyle(dim, outlineStyle);
+ }
+ _paintOneStyle(dim, style);
+ }
+ };
+ };
+
+ /**
+ * Class:CanvasEndpoint
+ * Superclass for Canvas Endpoint renderers.
+ */
+ var CanvasEndpoint = function(params) {
+ var self = this;
+ CanvasComponent.apply(this, arguments);
+ var clazz = self._jsPlumb.endpointClass + " " + (params.cssClass || ""),
+ canvasParams = {
+ "class":clazz,
+ _jsPlumb:self._jsPlumb,
+ parent:params.parent,
+ tooltip:self.tooltip
+ };
+ self.canvas = _newCanvas(canvasParams);
+ self.ctx = self.canvas.getContext("2d");
+
+ self.appendDisplayElement(self.canvas);
+
+ this.paint = function(d, style, anchor) {
+ jsPlumb.sizeCanvas(self.canvas, d[0], d[1], d[2], d[3]);
+ if (style.outlineColor != null) {
+ var outlineWidth = style.outlineWidth || 1,
+ outlineStrokeWidth = style.lineWidth + (2 * outlineWidth);
+ var outlineStyle = {
+ strokeStyle:style.outlineColor,
+ lineWidth:outlineStrokeWidth
+ };
+ }
+
+ self._paint.apply(this, arguments);
+ };
+ };
+
+ jsPlumb.Endpoints.canvas.Dot = function(params) {
+ jsPlumb.Endpoints.Dot.apply(this, arguments);
+ CanvasEndpoint.apply(this, arguments);
+ var self = this,
+ parseValue = function(value) {
+ try { return parseInt(value); }
+ catch(e) {
+ if (value.substring(value.length - 1) == '%')
+ return parseInt(value.substring(0, value - 1));
+ }
+ },
+ calculateAdjustments = function(gradient) {
+ var offsetAdjustment = self.defaultOffset, innerRadius = self.defaultInnerRadius;
+ gradient.offset && (offsetAdjustment = parseValue(gradient.offset));
+ gradient.innerRadius && (innerRadius = parseValue(gradient.innerRadius));
+ return [offsetAdjustment, innerRadius];
+ };
+ this._paint = function(d, style, anchor) {
+ if (style != null) {
+ var ctx = self.canvas.getContext('2d'), orientation = anchor.getOrientation(self);
+ jsPlumb.extend(ctx, style);
+ if (style.gradient) {
+ var adjustments = calculateAdjustments(style.gradient),
+ yAdjust = orientation[1] == 1 ? adjustments[0] * -1 : adjustments[0],
+ xAdjust = orientation[0] == 1 ? adjustments[0] * -1: adjustments[0],
+ g = ctx.createRadialGradient(d[4], d[4], d[4], d[4] + xAdjust, d[4] + yAdjust, adjustments[1]);
+ for (var i = 0; i < style.gradient.stops.length; i++)
+ g.addColorStop(style.gradient.stops[i][0], style.gradient.stops[i][1]);
+ ctx.fillStyle = g;
+ }
+ ctx.beginPath();
+ ctx.arc(d[4], d[4], d[4], 0, Math.PI*2, true);
+ ctx.closePath();
+ if (style.fillStyle || style.gradient) ctx.fill();
+ if (style.strokeStyle) ctx.stroke();
+ }
+ };
+ };
+
+ jsPlumb.Endpoints.canvas.Rectangle = function(params) {
+
+ var self = this;
+ jsPlumb.Endpoints.Rectangle.apply(this, arguments);
+ CanvasEndpoint.apply(this, arguments);
+
+ this._paint = function(d, style, anchor) {
+
+ var ctx = self.canvas.getContext("2d"), orientation = anchor.getOrientation(self);
+ jsPlumb.extend(ctx, style);
+
+ /* canvas gradient */
+ if (style.gradient) {
+ // first figure out which direction to run the gradient in (it depends on the orientation of the anchors)
+ var y1 = orientation[1] == 1 ? d[3] : orientation[1] == 0 ? d[3] / 2 : 0;
+ var y2 = orientation[1] == -1 ? d[3] : orientation[1] == 0 ? d[3] / 2 : 0;
+ var x1 = orientation[0] == 1 ? d[2] : orientation[0] == 0 ? d[2] / 2 : 0;
+ var x2 = orientation[0] == -1 ? d[2] : orientation[0] == 0 ? d[2] / 2 : 0;
+ var g = ctx.createLinearGradient(x1,y1,x2,y2);
+ for (var i = 0; i < style.gradient.stops.length; i++)
+ g.addColorStop(style.gradient.stops[i][0], style.gradient.stops[i][1]);
+ ctx.fillStyle = g;
+ }
+
+ ctx.beginPath();
+ ctx.rect(0, 0, d[2], d[3]);
+ ctx.closePath();
+ if (style.fillStyle || style.gradient) ctx.fill();
+ if (style.strokeStyle) ctx.stroke();
+ };
+ };
+
+ jsPlumb.Endpoints.canvas.Triangle = function(params) {
+
+ var self = this;
+ jsPlumb.Endpoints.Triangle.apply(this, arguments);
+ CanvasEndpoint.apply(this, arguments);
+
+ this._paint = function(d, style, anchor)
+ {
+ var width = d[2], height = d[3], x = d[0], y = d[1],
+ ctx = self.canvas.getContext('2d'),
+ offsetX = 0, offsetY = 0, angle = 0,
+ orientation = anchor.getOrientation(self);
+
+ if( orientation[0] == 1 ) {
+ offsetX = width;
+ offsetY = height;
+ angle = 180;
+ }
+ if( orientation[1] == -1 ) {
+ offsetX = width;
+ angle = 90;
+ }
+ if( orientation[1] == 1 ) {
+ offsetY = height;
+ angle = -90;
+ }
+
+ ctx.fillStyle = style.fillStyle;
+
+ ctx.translate(offsetX, offsetY);
+ ctx.rotate(angle * Math.PI/180);
+
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(width/2, height/2);
+ ctx.lineTo(0, height);
+ ctx.closePath();
+ if (style.fillStyle || style.gradient) ctx.fill();
+ if (style.strokeStyle) ctx.stroke();
+ };
+ };
+
+ /*
+ * Canvas Image Endpoint: uses the default version, which creates an
tag.
+ */
+ jsPlumb.Endpoints.canvas.Image = jsPlumb.Endpoints.Image;
+
+ /*
+ * Blank endpoint in all renderers is just the default Blank endpoint.
+ */
+ jsPlumb.Endpoints.canvas.Blank = jsPlumb.Endpoints.Blank;
+
+ /*
+ * Canvas Bezier Connector. Draws a Bezier curve onto a Canvas element.
+ */
+ jsPlumb.Connectors.canvas.Bezier = function() {
+ var self = this;
+ jsPlumb.Connectors.Bezier.apply(this, arguments);
+ CanvasConnector.apply(this, arguments);
+ this._paint = function(dimensions) {
+ self.ctx.beginPath();
+ self.ctx.moveTo(dimensions[4], dimensions[5]);
+ self.ctx.bezierCurveTo(dimensions[8], dimensions[9], dimensions[10], dimensions[11], dimensions[6], dimensions[7]);
+ self.ctx.stroke();
+ };
+
+ // TODO i doubt this handles the case that source and target are swapped.
+ this.createGradient = function(dim, ctx, swap) {
+ return /*(swap) ? self.ctx.createLinearGradient(dim[4], dim[5], dim[6], dim[7]) : */self.ctx.createLinearGradient(dim[6], dim[7], dim[4], dim[5]);
+ };
+ };
+
+ /*
+ * Canvas straight line Connector. Draws a straight line onto a Canvas element.
+ */
+ jsPlumb.Connectors.canvas.Straight = function() {
+ var self = this;
+ jsPlumb.Connectors.Straight.apply(this, arguments);
+ CanvasConnector.apply(this, arguments);
+ this._paint = function(dimensions) {
+ self.ctx.beginPath();
+ self.ctx.moveTo(dimensions[4], dimensions[5]);
+ self.ctx.lineTo(dimensions[6], dimensions[7]);
+ self.ctx.stroke();
+ };
+
+ // TODO this does not handle the case that src and target are swapped.
+ this.createGradient = function(dim, ctx) {
+ return ctx.createLinearGradient(dim[4], dim[5], dim[6], dim[7]);
+ };
+ };
+
+ jsPlumb.Connectors.canvas.Flowchart = function() {
+ var self = this;
+ jsPlumb.Connectors.Flowchart.apply(this, arguments);
+ CanvasConnector.apply(this, arguments);
+ this._paint = function(dimensions) {
+ self.ctx.beginPath();
+ self.ctx.moveTo(dimensions[4], dimensions[5]);
+ // loop through extra points
+ for (var i = 0; i < dimensions[8]; i++) {
+ self.ctx.lineTo(dimensions[9 + (i*2)], dimensions[10 + (i*2)]);
+ }
+ // finally draw a line to the end
+ self.ctx.lineTo(dimensions[6], dimensions[7]);
+ self.ctx.stroke();
+ };
+
+ this.createGradient = function(dim, ctx) {
+ return ctx.createLinearGradient(dim[4], dim[5], dim[6], dim[7]);
+ };
+ };
+
+// ********************************* END OF CANVAS RENDERERS *******************************************************************
+
+ jsPlumb.Overlays.canvas.Label = jsPlumb.Overlays.Label;
+
+ /**
+ * a placeholder right now, really just exists to mirror the fact that there are SVG and VML versions of this.
+ */
+ var CanvasOverlay = function() {
+ jsPlumb.jsPlumbUIComponent.apply(this, arguments);
+ };
+
+ var AbstractCanvasArrowOverlay = function(superclass, originalArgs) {
+ superclass.apply(this, originalArgs);
+ CanvasOverlay.apply(this, arguments);
+ this.paint = function(connector, d, lineWidth, strokeStyle, fillStyle) {
+ var ctx = connector.ctx;
+
+ ctx.lineWidth = lineWidth;
+ ctx.beginPath();
+ ctx.moveTo(d.hxy.x, d.hxy.y);
+ ctx.lineTo(d.tail[0].x, d.tail[0].y);
+ ctx.lineTo(d.cxy.x, d.cxy.y);
+ ctx.lineTo(d.tail[1].x, d.tail[1].y);
+ ctx.lineTo(d.hxy.x, d.hxy.y);
+ ctx.closePath();
+
+ if (strokeStyle) {
+ ctx.strokeStyle = strokeStyle;
+ ctx.stroke();
+ }
+ if (fillStyle) {
+ ctx.fillStyle = fillStyle;
+ ctx.fill();
+ }
+ };
+ };
+
+ jsPlumb.Overlays.canvas.Arrow = function() {
+ AbstractCanvasArrowOverlay.apply(this, [jsPlumb.Overlays.Arrow, arguments]);
+ };
+
+ jsPlumb.Overlays.canvas.PlainArrow = function() {
+ AbstractCanvasArrowOverlay.apply(this, [jsPlumb.Overlays.PlainArrow, arguments]);
+ };
+
+ jsPlumb.Overlays.canvas.Diamond = function() {
+ AbstractCanvasArrowOverlay.apply(this, [jsPlumb.Overlays.Diamond, arguments]);
+ };
+})();/*
+ * jsPlumb
+ *
+ * Title:jsPlumb 1.3.8
+ *
+ * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
+ * elements, or VML.
+ *
+ * This file contains the jQuery adapter.
+ *
+ * Copyright (c) 2010 - 2012 Simon Porritt (http://jsplumb.org)
+ *
+ * http://jsplumb.org
+ * http://github.com/sporritt/jsplumb
+ * http://code.google.com/p/jsplumb
+ *
+ * Dual licensed under the MIT and GPL2 licenses.
+ */
+/*
+ * the library specific functions, such as find offset, get id, get attribute, extend etc.
+ * the full list is:
+ *
+ * addClass adds a class to the given element
+ * animate calls the underlying library's animate functionality
+ * appendElement appends a child element to a parent element.
+ * bind binds some event to an element
+ * dragEvents a dictionary of event names
+ * extend extend some js object with another. probably not overly necessary; jsPlumb could just do this internally.
+ * getAttribute gets some attribute from an element
+ * getDragObject gets the object that is being dragged, by extracting it from the arguments passed to a drag callback
+ * getDragScope gets the drag scope for a given element.
+ * getDropScope gets the drop scope for a given element.
+ * getElementObject turns an id or dom element into an element object of the underlying library's type.
+ * getOffset gets an element's offset
+ * getPageXY gets the page event's xy location.
+ * getParent gets the parent of some element.
+ * getScrollLeft gets an element's scroll left. TODO: is this actually used? will it be?
+ * getScrollTop gets an element's scroll top. TODO: is this actually used? will it be?
+ * getSize gets an element's size.
+ * getUIPosition gets the position of some element that is currently being dragged, by extracting it from the arguments passed to a drag callback.
+ * hasClass returns whether or not the given element has the given class.
+ * initDraggable initializes an element to be draggable
+ * initDroppable initializes an element to be droppable
+ * isDragSupported returns whether or not drag is supported for some element.
+ * isDropSupported returns whether or not drop is supported for some element.
+ * removeClass removes a class from a given element.
+ * removeElement removes some element completely from the DOM.
+ * setAttribute sets an attribute on some element.
+ * setDraggable sets whether or not some element should be draggable.
+ * setDragScope sets the drag scope for a given element.
+ * setOffset sets the offset of some element.
+ * trigger triggers some event on an element.
+ * unbind unbinds some listener from some element.
+ */
+(function($) {
+
+ //var getBoundingClientRectSupported = "getBoundingClientRect" in document.documentElement;
+
+ jsPlumb.CurrentLibrary = {
+
+ /**
+ * adds the given class to the element object.
+ */
+ addClass : function(el, clazz) {
+ el = jsPlumb.CurrentLibrary.getElementObject(el);
+ try {
+ if (el[0].className.constructor == SVGAnimatedString) {
+ jsPlumb.util.svg.addClass(el[0], clazz);
+ }
+ }
+ catch (e) {
+ // SVGAnimatedString not supported; no problem.
+ }
+ el.addClass(clazz);
+ },
+
+ /**
+ * animates the given element.
+ */
+ animate : function(el, properties, options) {
+ el.animate(properties, options);
+ },
+
+ /**
+ * appends the given child to the given parent.
+ */
+ appendElement : function(child, parent) {
+ jsPlumb.CurrentLibrary.getElementObject(parent).append(child);
+ },
+
+ /**
+ * executes ana ajax call.
+ */
+ ajax : function(params) {
+ params = params || {};
+ params.type = params.type || "get";
+ $.ajax(params);
+ },
+
+ /**
+ * event binding wrapper. it just so happens that jQuery uses 'bind' also. yui3, for example,
+ * uses 'on'.
+ */
+ bind : function(el, event, callback) {
+ el = jsPlumb.CurrentLibrary.getElementObject(el);
+ el.bind(event, callback);
+ },
+
+ /**
+ * mapping of drag events for jQuery
+ */
+ dragEvents : {
+ 'start':'start', 'stop':'stop', 'drag':'drag', 'step':'step',
+ 'over':'over', 'out':'out', 'drop':'drop', 'complete':'complete'
+ },
+
+ /**
+ * wrapper around the library's 'extend' functionality (which it hopefully has.
+ * otherwise you'll have to do it yourself). perhaps jsPlumb could do this for you
+ * instead. it's not like its hard.
+ */
+ extend : function(o1, o2) {
+ return $.extend(o1, o2);
+ },
+
+ /**
+ * gets the named attribute from the given element object.
+ */
+ getAttribute : function(el, attName) {
+ return el.attr(attName);
+ },
+
+ getClientXY : function(eventObject) {
+ return [eventObject.clientX, eventObject.clientY];
+ },
+
+ getDocumentElement : function() { return document; },
+
+ /**
+ * takes the args passed to an event function and returns you an object representing that which is being dragged.
+ */
+ getDragObject : function(eventArgs) {
+ return eventArgs[1].draggable;
+ },
+
+ getDragScope : function(el) {
+ return el.draggable("option", "scope");
+ },
+
+ getDropEvent : function(args) {
+ return args[0];
+ },
+
+ getDropScope : function(el) {
+ return el.droppable("option", "scope");
+ },
+
+ /**
+ * gets a DOM element from the given input, which might be a string (in which case we just do document.getElementById),
+ * a selector (in which case we return el[0]), or a DOM element already (we assume this if it's not either of the other
+ * two cases). this is the opposite of getElementObject below.
+ */
+ getDOMElement : function(el) {
+ if (typeof(el) == "string") return document.getElementById(el);
+ else if (el.context) return el[0];
+ else return el;
+ },
+
+ /**
+ * gets an "element object" from the given input. this means an object that is used by the
+ * underlying library on which jsPlumb is running. 'el' may already be one of these objects,
+ * in which case it is returned as-is. otherwise, 'el' is a String, the library's lookup
+ * function is used to find the element, using the given String as the element's id.
+ *
+ */
+ getElementObject : function(el) {
+ return typeof(el) == "string" ? $("#" + el) : $(el);
+ },
+
+ /**
+ * gets the offset for the element object. this should return a js object like this:
+ *
+ * { left:xxx, top: xxx }
+ */
+ getOffset : function(el) {
+ return el.offset();
+ },
+
+ getPageXY : function(eventObject) {
+ return [eventObject.pageX, eventObject.pageY];
+ },
+
+ getParent : function(el) {
+ return jsPlumb.CurrentLibrary.getElementObject(el).parent();
+ },
+
+ getScrollLeft : function(el) {
+ return el.scrollLeft();
+ },
+
+ getScrollTop : function(el) {
+ return el.scrollTop();
+ },
+
+ getSelector : function(spec) {
+ return $(spec);
+ },
+
+ /**
+ * gets the size for the element object, in an array : [ width, height ].
+ */
+ getSize : function(el) {
+ return [el.outerWidth(), el.outerHeight()];
+ },
+
+ getTagName : function(el) {
+ var e = jsPlumb.CurrentLibrary.getElementObject(el);
+ return e.length > 0 ? e[0].tagName : null;
+ },
+
+ /**
+ * takes the args passed to an event function and returns you an object that gives the
+ * position of the object being moved, as a js object with the same params as the result of
+ * getOffset, ie: { left: xxx, top: xxx }.
+ *
+ * different libraries have different signatures for their event callbacks.
+ * see getDragObject as well
+ */
+ getUIPosition : function(eventArgs) {
+
+ // this code is a workaround for the case that the element being dragged has a margin set on it. jquery UI passes
+ // in the wrong offset if the element has a margin (it doesn't take the margin into account). the getBoundingClientRect
+ // method, which is in pretty much all browsers now, reports the right numbers. but it introduces a noticeable lag, which
+ // i don't like.
+
+ /*if ( getBoundingClientRectSupported ) {
+ var r = eventArgs[1].helper[0].getBoundingClientRect();
+ return { left : r.left, top: r.top };
+ } else {*/
+ if (eventArgs.length == 1) {
+ ret = { left: eventArgs[0].pageX, top:eventArgs[0].pageY };
+ }
+ else {
+ var ui = eventArgs[1], _offset = ui.offset;
+ ret = _offset || ui.absolutePosition;
+ }
+ return ret;
+ },
+
+ hasClass : function(el, clazz) {
+ return el.hasClass(clazz);
+ },
+
+ /**
+ * initialises the given element to be draggable.
+ */
+ initDraggable : function(el, options) {
+ options = options || {};
+ // remove helper directive if present.
+ options.helper = null;
+ options['scope'] = options['scope'] || jsPlumb.Defaults.Scope;
+ el.draggable(options);
+ },
+
+ /**
+ * initialises the given element to be droppable.
+ */
+ initDroppable : function(el, options) {
+ options['scope'] = options['scope'] || jsPlumb.Defaults.Scope;
+ el.droppable(options);
+ },
+
+ isAlreadyDraggable : function(el) {
+ el = jsPlumb.CurrentLibrary.getElementObject(el);
+ return el.hasClass("ui-draggable");
+ },
+
+ /**
+ * returns whether or not drag is supported (by the library, not whether or not it is disabled) for the given element.
+ */
+ isDragSupported : function(el, options) {
+ return el.draggable;
+ },
+
+ /**
+ * returns whether or not drop is supported (by the library, not whether or not it is disabled) for the given element.
+ */
+ isDropSupported : function(el, options) {
+ return el.droppable;
+ },
+
+ /**
+ * removes the given class from the element object.
+ */
+ removeClass : function(el, clazz) {
+ el = jsPlumb.CurrentLibrary.getElementObject(el);
+ try {
+ if (el[0].className.constructor == SVGAnimatedString) {
+ jsPlumb.util.svg.removeClass(el[0], clazz);
+ }
+ }
+ catch (e) {
+ // SVGAnimatedString not supported; no problem.
+ }
+ el.removeClass(clazz);
+ },
+
+ removeElement : function(element, parent) {
+ jsPlumb.CurrentLibrary.getElementObject(element).remove();
+ },
+
+ /**
+ * sets the named attribute on the given element object.
+ */
+ setAttribute : function(el, attName, attValue) {
+ el.attr(attName, attValue);
+ },
+
+ /**
+ * sets the draggable state for the given element
+ */
+ setDraggable : function(el, draggable) {
+ el.draggable("option", "disabled", !draggable);
+ },
+
+ /**
+ * sets the drag scope. probably time for a setDragOption method (roll this and the one above together)
+ * @param el
+ * @param scope
+ */
+ setDragScope : function(el, scope) {
+ el.draggable("option", "scope", scope);
+ },
+
+ setOffset : function(el, o) {
+ jsPlumb.CurrentLibrary.getElementObject(el).offset(o);
+ },
+
+ /**
+ * note that jquery ignores the name of the event you wanted to trigger, and figures it out for itself.
+ * the other libraries do not. yui, in fact, cannot even pass an original event. we have to pull out stuff
+ * from the originalEvent to put in an options object for YUI.
+ * @param el
+ * @param event
+ * @param originalEvent
+ */
+ trigger : function(el, event, originalEvent) {
+ //originalEvent.stopPropagation();
+ //jsPlumb.CurrentLibrary.getElementObject(el).trigger(originalEvent);
+ var h = jQuery._data(jsPlumb.CurrentLibrary.getElementObject(el)[0], "handle");
+ h(originalEvent);
+ //originalEvent.stopPropagation();
+ },
+
+ /**
+ * event unbinding wrapper. it just so happens that jQuery uses 'unbind' also. yui3, for example,
+ * uses..something else.
+ */
+ unbind : function(el, event, callback) {
+ el = jsPlumb.CurrentLibrary.getElementObject(el);
+ el.unbind(event, callback);
+ }
+ };
+
+ $(document).ready(jsPlumb.init);
+
+})(jQuery);
+
+(function(){if("undefined"==typeof Math.sgn)Math.sgn=function(a){return 0==a?0:0n?n=l:lb.location)b.location=0;return t(a,b.location)},nearestPointOnCurve:function(a,b){var f=u(a,b);return{point:r(b,b.length-1,f.location,null,null),location:f.location}},pointOnCurve:p,pointAlongCurveFrom:function(a,b,f){return s(a,b,f).point},perpendicularToCurveAt:function(a,b,f,d){b=s(a,b,null==d?0:d);a=t(a,b.location);d=Math.atan(-1/a);a=f/2*Math.sin(d);f=f/2*Math.cos(d);return[{x:b.point.x+f,y:b.point.y+a},{x:b.point.x-f,y:b.point.y-a}]}}})();
\ No newline at end of file
diff --git a/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.scrollTo-1.4.2.js b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.scrollTo-1.4.2.js
new file mode 100644
index 0000000..753e62c
--- /dev/null
+++ b/beautifulmind/mindmap/static/mindmap/js/vendor/jquery.scrollTo-1.4.2.js
@@ -0,0 +1,215 @@
+/**
+ * jQuery.ScrollTo
+ * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
+ * Dual licensed under MIT and GPL.
+ * Date: 5/25/2009
+ *
+ * @projectDescription Easy element scrolling using jQuery.
+ * http://flesler.blogspot.com/2007/10/jqueryscrollto.html
+ * Works with jQuery +1.2.6. Tested on FF 2/3, IE 6/7/8, Opera 9.5/6, Safari 3, Chrome 1 on WinXP.
+ *
+ * @author Ariel Flesler
+ * @version 1.4.2
+ *
+ * @id jQuery.scrollTo
+ * @id jQuery.fn.scrollTo
+ * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements.
+ * The different options for target are:
+ * - A number position (will be applied to all axes).
+ * - A string position ('44', '100px', '+=90', etc ) will be applied to all axes
+ * - A jQuery/DOM element ( logically, child of the element to scroll )
+ * - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc )
+ * - A hash { top:x, left:y }, x and y can be any kind of number/string like above.
+* - A percentage of the container's dimension/s, for example: 50% to go to the middle.
+ * - The string 'max' for go-to-end.
+ * @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead.
+ * @param {Object,Function} settings Optional set of settings or the onAfter callback.
+ * @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'.
+ * @option {Number} duration The OVERALL length of the animation.
+ * @option {String} easing The easing method for the animation.
+ * @option {Boolean} margin If true, the margin of the target element will be deducted from the final position.
+ * @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }.
+ * @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes.
+ * @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends.
+ * @option {Function} onAfter Function to be called after the scrolling ends.
+ * @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends.
+ * @return {jQuery} Returns the same jQuery object, for chaining.
+ *
+ * @desc Scroll to a fixed position
+ * @example $('div').scrollTo( 340 );
+ *
+ * @desc Scroll relatively to the actual position
+ * @example $('div').scrollTo( '+=340px', { axis:'y' } );
+ *
+ * @dec Scroll using a selector (relative to the scrolled element)
+ * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } );
+ *
+ * @ Scroll to a DOM element (same for jQuery object)
+ * @example var second_child = document.getElementById('container').firstChild.nextSibling;
+ * $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){
+ * alert('scrolled!!');
+ * }});
+ *
+ * @desc Scroll on both axes, to different values
+ * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } );
+ */
+;(function( $ ){
+
+ var $scrollTo = $.scrollTo = function( target, duration, settings ){
+ $(window).scrollTo( target, duration, settings );
+ };
+
+ $scrollTo.defaults = {
+ axis:'xy',
+ duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 1
+ };
+
+ // Returns the element that needs to be animated to scroll the window.
+ // Kept for backwards compatibility (specially for localScroll & serialScroll)
+ $scrollTo.window = function( scope ){
+ return $(window)._scrollable();
+ };
+
+ // Hack, hack, hack :)
+ // Returns the real elements to scroll (supports window/iframes, documents and regular nodes)
+ $.fn._scrollable = function(){
+ return this.map(function(){
+ var elem = this,
+ isWin = !elem.nodeName || $.inArray( elem.nodeName.toLowerCase(), ['iframe','#document','html','body'] ) != -1;
+
+ if( !isWin )
+ return elem;
+
+ var doc = (elem.contentWindow || elem).document || elem.ownerDocument || elem;
+
+ return $.browser.safari || doc.compatMode == 'BackCompat' ?
+ doc.body :
+ doc.documentElement;
+ });
+ };
+
+ $.fn.scrollTo = function( target, duration, settings ){
+ if( typeof duration == 'object' ){
+ settings = duration;
+ duration = 0;
+ }
+ if( typeof settings == 'function' )
+ settings = { onAfter:settings };
+
+ if( target == 'max' )
+ target = 9e9;
+
+ settings = $.extend( {}, $scrollTo.defaults, settings );
+ // Speed is still recognized for backwards compatibility
+ duration = duration || settings.speed || settings.duration;
+ // Make sure the settings are given right
+ settings.queue = settings.queue && settings.axis.length > 1;
+
+ if( settings.queue )
+ // Let's keep the overall duration
+ duration /= 2;
+ settings.offset = both( settings.offset );
+ settings.over = both( settings.over );
+
+ return this._scrollable().each(function(){
+ var elem = this,
+ $elem = $(elem),
+ targ = target, toff, attr = {},
+ win = $elem.is('html,body');
+
+ switch( typeof targ ){
+ // A number will pass the regex
+ case 'number':
+ case 'string':
+ if( /^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(targ) ){
+ targ = both( targ );
+ // We are done
+ break;
+ }
+ // Relative selector, no break!
+ targ = $(targ,this);
+ case 'object':
+ // DOMElement / jQuery
+ if( targ.is || targ.style )
+ // Get the real position of the target
+ toff = (targ = $(targ)).offset();
+ }
+ $.each( settings.axis.split(''), function( i, axis ){
+ var Pos = axis == 'x' ? 'Left' : 'Top',
+ pos = Pos.toLowerCase(),
+ key = 'scroll' + Pos,
+ old = elem[key],
+ max = $scrollTo.max(elem, axis);
+
+ if( toff ){// jQuery / DOMElement
+ attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] );
+
+ // If it's a dom element, reduce the margin
+ if( settings.margin ){
+ attr[key] -= parseInt(targ.css('margin'+Pos)) || 0;
+ attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0;
+ }
+
+ attr[key] += settings.offset[pos] || 0;
+
+ if( settings.over[pos] )
+ // Scroll to a fraction of its width/height
+ attr[key] += targ[axis=='x'?'width':'height']() * settings.over[pos];
+ }else{
+ var val = targ[pos];
+ // Handle percentage values
+ attr[key] = val.slice && val.slice(-1) == '%' ?
+ parseFloat(val) / 100 * max
+ : val;
+ }
+
+ // Number or 'number'
+ if( /^\d+$/.test(attr[key]) )
+ // Check the limits
+ attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max );
+
+ // Queueing axes
+ if( !i && settings.queue ){
+ // Don't waste time animating, if there's no need.
+ if( old != attr[key] )
+ // Intermediate animation
+ animate( settings.onAfterFirst );
+ // Don't animate this axis again in the next iteration.
+ delete attr[key];
+ }
+ });
+
+ animate( settings.onAfter );
+
+ function animate( callback ){
+ $elem.animate( attr, duration, settings.easing, callback && function(){
+ callback.call(this, target, settings);
+ });
+ };
+
+ }).end();
+ };
+
+ // Max scrolling position, works on quirks mode
+ // It only fails (not too badly) on IE, quirks mode.
+ $scrollTo.max = function( elem, axis ){
+ var Dim = axis == 'x' ? 'Width' : 'Height',
+ scroll = 'scroll'+Dim;
+
+ if( !$(elem).is('html,body') )
+ return elem[scroll] - $(elem)[Dim.toLowerCase()]();
+
+ var size = 'client' + Dim,
+ html = elem.ownerDocument.documentElement,
+ body = elem.ownerDocument.body;
+
+ return Math.max( html[scroll], body[scroll] )
+ - Math.min( html[size] , body[size] );
+
+ };
+
+ function both( val ){
+ return typeof val == 'object' ? val : { top:val, left:val };
+ };
+
+})( jQuery );
\ No newline at end of file
diff --git a/beautifulmind/mindmap/static/mindmap/js/vendor/sockjs-0.3.1.js b/beautifulmind/mindmap/static/mindmap/js/vendor/sockjs-0.3.1.js
new file mode 100644
index 0000000..ef0043a
--- /dev/null
+++ b/beautifulmind/mindmap/static/mindmap/js/vendor/sockjs-0.3.1.js
@@ -0,0 +1,2314 @@
+/* SockJS client, version 0.3.1, http://sockjs.org, MIT License
+
+Copyright (c) 2011-2012 VMware, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+// JSON2 by Douglas Crockford (minified).
+var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c 1) {
+ this._listeners[eventType] = arr.slice(0, idx).concat( arr.slice(idx+1) );
+ } else {
+ delete this._listeners[eventType];
+ }
+ return;
+ }
+ return;
+};
+
+REventTarget.prototype.dispatchEvent = function (event) {
+ var t = event.type;
+ var args = Array.prototype.slice.call(arguments, 0);
+ if (this['on'+t]) {
+ this['on'+t].apply(this, args);
+ }
+ if (this._listeners && t in this._listeners) {
+ for(var i=0; i < this._listeners[t].length; i++) {
+ this._listeners[t][i].apply(this, args);
+ }
+ }
+};
+// [*] End of lib/reventtarget.js
+
+
+// [*] Including lib/simpleevent.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var SimpleEvent = function(type, obj) {
+ this.type = type;
+ if (typeof obj !== 'undefined') {
+ for(var k in obj) {
+ if (!obj.hasOwnProperty(k)) continue;
+ this[k] = obj[k];
+ }
+ }
+};
+
+SimpleEvent.prototype.toString = function() {
+ var r = [];
+ for(var k in this) {
+ if (!this.hasOwnProperty(k)) continue;
+ var v = this[k];
+ if (typeof v === 'function') v = '[function]';
+ r.push(k + '=' + v);
+ }
+ return 'SimpleEvent(' + r.join(', ') + ')';
+};
+// [*] End of lib/simpleevent.js
+
+
+// [*] Including lib/eventemitter.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var EventEmitter = function(events) {
+ this.events = events || [];
+};
+EventEmitter.prototype.emit = function(type) {
+ var that = this;
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (!that.nuked && that['on'+type]) {
+ that['on'+type].apply(that, args);
+ }
+ if (utils.arrIndexOf(that.events, type) === -1) {
+ utils.log('Event ' + JSON.stringify(type) +
+ ' not listed ' + JSON.stringify(that.events) +
+ ' in ' + that);
+ }
+};
+
+EventEmitter.prototype.nuke = function(type) {
+ var that = this;
+ that.nuked = true;
+ for(var i=0; i= 3000 && code <= 4999);
+};
+
+// See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/
+// and RFC 2988.
+utils.countRTO = function (rtt) {
+ var rto;
+ if (rtt > 100) {
+ rto = 3 * rtt; // rto > 300msec
+ } else {
+ rto = rtt + 200; // 200msec < rto <= 300msec
+ }
+ return rto;
+}
+
+utils.log = function() {
+ if (_window.console && console.log && console.log.apply) {
+ console.log.apply(console, arguments);
+ }
+};
+
+utils.bind = function(fun, that) {
+ if (fun.bind) {
+ return fun.bind(that);
+ } else {
+ return function() {
+ return fun.apply(that, arguments);
+ };
+ }
+};
+
+utils.flatUrl = function(url) {
+ return url.indexOf('?') === -1 && url.indexOf('#') === -1;
+};
+
+utils.amendUrl = function(url) {
+ var dl = _document.location;
+ if (!url) {
+ throw new Error('Wrong url for SockJS');
+ }
+ if (!utils.flatUrl(url)) {
+ throw new Error('Only basic urls are supported in SockJS');
+ }
+
+ // '//abc' --> 'http://abc'
+ if (url.indexOf('//') === 0) {
+ url = dl.protocol + url;
+ }
+ // '/abc' --> 'http://localhost:80/abc'
+ if (url.indexOf('/') === 0) {
+ url = dl.protocol + '//' + dl.host + url;
+ }
+ // strip trailing slashes
+ url = url.replace(/[/]+$/,'');
+ return url;
+};
+
+// IE doesn't support [].indexOf.
+utils.arrIndexOf = function(arr, obj){
+ for(var i=0; i < arr.length; i++){
+ if(arr[i] === obj){
+ return i;
+ }
+ }
+ return -1;
+};
+
+utils.arrSkip = function(arr, obj) {
+ var idx = utils.arrIndexOf(arr, obj);
+ if (idx === -1) {
+ return arr.slice();
+ } else {
+ var dst = arr.slice(0, idx);
+ return dst.concat(arr.slice(idx+1));
+ }
+};
+
+// Via: https://gist.github.com/1133122/2121c601c5549155483f50be3da5305e83b8c5df
+utils.isArray = Array.isArray || function(value) {
+ return {}.toString.call(value).indexOf('Array') >= 0
+};
+
+utils.delay = function(t, fun) {
+ if(typeof t === 'function') {
+ fun = t;
+ t = 0;
+ }
+ return setTimeout(fun, t);
+};
+
+
+// Chars worth escaping, as defined by Douglas Crockford:
+// https://github.com/douglascrockford/JSON-js/blob/47a9882cddeb1e8529e07af9736218075372b8ac/json2.js#L196
+var json_escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ json_lookup = {
+"\u0000":"\\u0000","\u0001":"\\u0001","\u0002":"\\u0002","\u0003":"\\u0003",
+"\u0004":"\\u0004","\u0005":"\\u0005","\u0006":"\\u0006","\u0007":"\\u0007",
+"\b":"\\b","\t":"\\t","\n":"\\n","\u000b":"\\u000b","\f":"\\f","\r":"\\r",
+"\u000e":"\\u000e","\u000f":"\\u000f","\u0010":"\\u0010","\u0011":"\\u0011",
+"\u0012":"\\u0012","\u0013":"\\u0013","\u0014":"\\u0014","\u0015":"\\u0015",
+"\u0016":"\\u0016","\u0017":"\\u0017","\u0018":"\\u0018","\u0019":"\\u0019",
+"\u001a":"\\u001a","\u001b":"\\u001b","\u001c":"\\u001c","\u001d":"\\u001d",
+"\u001e":"\\u001e","\u001f":"\\u001f","\"":"\\\"","\\":"\\\\",
+"\u007f":"\\u007f","\u0080":"\\u0080","\u0081":"\\u0081","\u0082":"\\u0082",
+"\u0083":"\\u0083","\u0084":"\\u0084","\u0085":"\\u0085","\u0086":"\\u0086",
+"\u0087":"\\u0087","\u0088":"\\u0088","\u0089":"\\u0089","\u008a":"\\u008a",
+"\u008b":"\\u008b","\u008c":"\\u008c","\u008d":"\\u008d","\u008e":"\\u008e",
+"\u008f":"\\u008f","\u0090":"\\u0090","\u0091":"\\u0091","\u0092":"\\u0092",
+"\u0093":"\\u0093","\u0094":"\\u0094","\u0095":"\\u0095","\u0096":"\\u0096",
+"\u0097":"\\u0097","\u0098":"\\u0098","\u0099":"\\u0099","\u009a":"\\u009a",
+"\u009b":"\\u009b","\u009c":"\\u009c","\u009d":"\\u009d","\u009e":"\\u009e",
+"\u009f":"\\u009f","\u00ad":"\\u00ad","\u0600":"\\u0600","\u0601":"\\u0601",
+"\u0602":"\\u0602","\u0603":"\\u0603","\u0604":"\\u0604","\u070f":"\\u070f",
+"\u17b4":"\\u17b4","\u17b5":"\\u17b5","\u200c":"\\u200c","\u200d":"\\u200d",
+"\u200e":"\\u200e","\u200f":"\\u200f","\u2028":"\\u2028","\u2029":"\\u2029",
+"\u202a":"\\u202a","\u202b":"\\u202b","\u202c":"\\u202c","\u202d":"\\u202d",
+"\u202e":"\\u202e","\u202f":"\\u202f","\u2060":"\\u2060","\u2061":"\\u2061",
+"\u2062":"\\u2062","\u2063":"\\u2063","\u2064":"\\u2064","\u2065":"\\u2065",
+"\u2066":"\\u2066","\u2067":"\\u2067","\u2068":"\\u2068","\u2069":"\\u2069",
+"\u206a":"\\u206a","\u206b":"\\u206b","\u206c":"\\u206c","\u206d":"\\u206d",
+"\u206e":"\\u206e","\u206f":"\\u206f","\ufeff":"\\ufeff","\ufff0":"\\ufff0",
+"\ufff1":"\\ufff1","\ufff2":"\\ufff2","\ufff3":"\\ufff3","\ufff4":"\\ufff4",
+"\ufff5":"\\ufff5","\ufff6":"\\ufff6","\ufff7":"\\ufff7","\ufff8":"\\ufff8",
+"\ufff9":"\\ufff9","\ufffa":"\\ufffa","\ufffb":"\\ufffb","\ufffc":"\\ufffc",
+"\ufffd":"\\ufffd","\ufffe":"\\ufffe","\uffff":"\\uffff"};
+
+// Some extra characters that Chrome gets wrong, and substitutes with
+// something else on the wire.
+var extra_escapable = /[\x00-\x1f\ud800-\udfff\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0f80-\u0f83\u0f93\u0f9d\u0fa2\u0fa7\u0fac\u0fb9\u1939-\u193a\u1a17\u1b6b\u1cda-\u1cdb\u1dc0-\u1dcf\u1dfc\u1dfe\u1f71\u1f73\u1f75\u1f77\u1f79\u1f7b\u1f7d\u1fbb\u1fbe\u1fc9\u1fcb\u1fd3\u1fdb\u1fe3\u1feb\u1fee-\u1fef\u1ff9\u1ffb\u1ffd\u2000-\u2001\u20d0-\u20d1\u20d4-\u20d7\u20e7-\u20e9\u2126\u212a-\u212b\u2329-\u232a\u2adc\u302b-\u302c\uaab2-\uaab3\uf900-\ufa0d\ufa10\ufa12\ufa15-\ufa1e\ufa20\ufa22\ufa25-\ufa26\ufa2a-\ufa2d\ufa30-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4e\ufff0-\uffff]/g,
+ extra_lookup;
+
+// JSON Quote string. Use native implementation when possible.
+var JSONQuote = (JSON && JSON.stringify) || function(string) {
+ json_escapable.lastIndex = 0;
+ if (json_escapable.test(string)) {
+ string = string.replace(json_escapable, function(a) {
+ return json_lookup[a];
+ });
+ }
+ return '"' + string + '"';
+};
+
+// This may be quite slow, so let's delay until user actually uses bad
+// characters.
+var unroll_lookup = function(escapable) {
+ var i;
+ var unrolled = {}
+ var c = []
+ for(i=0; i<65536; i++) {
+ c.push( String.fromCharCode(i) );
+ }
+ escapable.lastIndex = 0;
+ c.join('').replace(escapable, function (a) {
+ unrolled[ a ] = '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ return '';
+ });
+ escapable.lastIndex = 0;
+ return unrolled;
+};
+
+// Quote string, also taking care of unicode characters that browsers
+// often break. Especially, take care of unicode surrogates:
+// http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates
+utils.quote = function(string) {
+ var quoted = JSONQuote(string);
+
+ // In most cases this should be very fast and good enough.
+ extra_escapable.lastIndex = 0;
+ if(!extra_escapable.test(quoted)) {
+ return quoted;
+ }
+
+ if(!extra_lookup) extra_lookup = unroll_lookup(extra_escapable);
+
+ return quoted.replace(extra_escapable, function(a) {
+ return extra_lookup[a];
+ });
+}
+
+var _all_protocols = ['websocket',
+ 'xdr-streaming',
+ 'xhr-streaming',
+ 'iframe-eventsource',
+ 'iframe-htmlfile',
+ 'xdr-polling',
+ 'xhr-polling',
+ 'iframe-xhr-polling',
+ 'jsonp-polling'];
+
+utils.probeProtocols = function() {
+ var probed = {};
+ for(var i=0; i<_all_protocols.length; i++) {
+ var protocol = _all_protocols[i];
+ // User can have a typo in protocol name.
+ probed[protocol] = SockJS[protocol] &&
+ SockJS[protocol].enabled();
+ }
+ return probed;
+};
+
+utils.detectProtocols = function(probed, protocols_whitelist, info) {
+ var pe = {},
+ protocols = [];
+ if (!protocols_whitelist) protocols_whitelist = _all_protocols;
+ for(var i=0; i 0) {
+ maybe_push(protos);
+ }
+ }
+ }
+
+ // 1. Websocket
+ if (info.websocket !== false) {
+ maybe_push(['websocket']);
+ }
+
+ // 2. Streaming
+ if (pe['xhr-streaming'] && !info.null_origin) {
+ protocols.push('xhr-streaming');
+ } else {
+ if (pe['xdr-streaming'] && !info.cookie_needed && !info.null_origin) {
+ protocols.push('xdr-streaming');
+ } else {
+ maybe_push(['iframe-eventsource',
+ 'iframe-htmlfile']);
+ }
+ }
+
+ // 3. Polling
+ if (pe['xhr-polling'] && !info.null_origin) {
+ protocols.push('xhr-polling');
+ } else {
+ if (pe['xdr-polling'] && !info.cookie_needed && !info.null_origin) {
+ protocols.push('xdr-polling');
+ } else {
+ maybe_push(['iframe-xhr-polling',
+ 'jsonp-polling']);
+ }
+ }
+ return protocols;
+}
+// [*] End of lib/utils.js
+
+
+// [*] Including lib/dom.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+// May be used by htmlfile jsonp and transports.
+var MPrefix = '_sockjs_global';
+utils.createHook = function() {
+ var window_id = 'a' + utils.random_string(8);
+ if (!(MPrefix in _window)) {
+ var map = {};
+ _window[MPrefix] = function(window_id) {
+ if (!(window_id in map)) {
+ map[window_id] = {
+ id: window_id,
+ del: function() {delete map[window_id];}
+ };
+ }
+ return map[window_id];
+ }
+ }
+ return _window[MPrefix](window_id);
+};
+
+
+
+utils.attachMessage = function(listener) {
+ utils.attachEvent('message', listener);
+};
+utils.attachEvent = function(event, listener) {
+ if (typeof _window.addEventListener !== 'undefined') {
+ _window.addEventListener(event, listener, false);
+ } else {
+ // IE quirks.
+ // According to: http://stevesouders.com/misc/test-postmessage.php
+ // the message gets delivered only to 'document', not 'window'.
+ _document.attachEvent("on" + event, listener);
+ // I get 'window' for ie8.
+ _window.attachEvent("on" + event, listener);
+ }
+};
+
+utils.detachMessage = function(listener) {
+ utils.detachEvent('message', listener);
+};
+utils.detachEvent = function(event, listener) {
+ if (typeof _window.addEventListener !== 'undefined') {
+ _window.removeEventListener(event, listener, false);
+ } else {
+ _document.detachEvent("on" + event, listener);
+ _window.detachEvent("on" + event, listener);
+ }
+};
+
+
+var on_unload = {};
+// Things registered after beforeunload are to be called immediately.
+var after_unload = false;
+
+var trigger_unload_callbacks = function() {
+ for(var ref in on_unload) {
+ on_unload[ref]();
+ delete on_unload[ref];
+ };
+};
+
+var unload_triggered = function() {
+ if(after_unload) return;
+ after_unload = true;
+ trigger_unload_callbacks();
+};
+
+// Onbeforeunload alone is not reliable. We could use only 'unload'
+// but it's not working in opera within an iframe. Let's use both.
+utils.attachEvent('beforeunload', unload_triggered);
+utils.attachEvent('unload', unload_triggered);
+
+utils.unload_add = function(listener) {
+ var ref = utils.random_string(8);
+ on_unload[ref] = listener;
+ if (after_unload) {
+ utils.delay(trigger_unload_callbacks);
+ }
+ return ref;
+};
+utils.unload_del = function(ref) {
+ if (ref in on_unload)
+ delete on_unload[ref];
+};
+
+
+utils.createIframe = function (iframe_url, error_callback) {
+ var iframe = _document.createElement('iframe');
+ var tref, unload_ref;
+ var unattach = function() {
+ clearTimeout(tref);
+ // Explorer had problems with that.
+ try {iframe.onload = null;} catch (x) {}
+ iframe.onerror = null;
+ };
+ var cleanup = function() {
+ if (iframe) {
+ unattach();
+ // This timeout makes chrome fire onbeforeunload event
+ // within iframe. Without the timeout it goes straight to
+ // onunload.
+ setTimeout(function() {
+ if(iframe) {
+ iframe.parentNode.removeChild(iframe);
+ }
+ iframe = null;
+ }, 0);
+ utils.unload_del(unload_ref);
+ }
+ };
+ var onerror = function(r) {
+ if (iframe) {
+ cleanup();
+ error_callback(r);
+ }
+ };
+ var post = function(msg, origin) {
+ try {
+ // When the iframe is not loaded, IE raises an exception
+ // on 'contentWindow'.
+ if (iframe && iframe.contentWindow) {
+ iframe.contentWindow.postMessage(msg, origin);
+ }
+ } catch (x) {};
+ };
+
+ iframe.src = iframe_url;
+ iframe.style.display = 'none';
+ iframe.style.position = 'absolute';
+ iframe.onerror = function(){onerror('onerror');};
+ iframe.onload = function() {
+ // `onload` is triggered before scripts on the iframe are
+ // executed. Give it few seconds to actually load stuff.
+ clearTimeout(tref);
+ tref = setTimeout(function(){onerror('onload timeout');}, 2000);
+ };
+ _document.body.appendChild(iframe);
+ tref = setTimeout(function(){onerror('timeout');}, 15000);
+ unload_ref = utils.unload_add(cleanup);
+ return {
+ post: post,
+ cleanup: cleanup,
+ loaded: unattach
+ };
+};
+
+utils.createHtmlfile = function (iframe_url, error_callback) {
+ var doc = new ActiveXObject('htmlfile');
+ var tref, unload_ref;
+ var iframe;
+ var unattach = function() {
+ clearTimeout(tref);
+ };
+ var cleanup = function() {
+ if (doc) {
+ unattach();
+ utils.unload_del(unload_ref);
+ iframe.parentNode.removeChild(iframe);
+ iframe = doc = null;
+ CollectGarbage();
+ }
+ };
+ var onerror = function(r) {
+ if (doc) {
+ cleanup();
+ error_callback(r);
+ }
+ };
+ var post = function(msg, origin) {
+ try {
+ // When the iframe is not loaded, IE raises an exception
+ // on 'contentWindow'.
+ if (iframe && iframe.contentWindow) {
+ iframe.contentWindow.postMessage(msg, origin);
+ }
+ } catch (x) {};
+ };
+
+ doc.open();
+ doc.write('' +
+ 'document.domain="' + document.domain + '";' +
+ '');
+ doc.close();
+ doc.parentWindow[WPrefix] = _window[WPrefix];
+ var c = doc.createElement('div');
+ doc.body.appendChild(c);
+ iframe = doc.createElement('iframe');
+ c.appendChild(iframe);
+ iframe.src = iframe_url;
+ tref = setTimeout(function(){onerror('timeout');}, 15000);
+ unload_ref = utils.unload_add(cleanup);
+ return {
+ post: post,
+ cleanup: cleanup,
+ loaded: unattach
+ };
+};
+// [*] End of lib/dom.js
+
+
+// [*] Including lib/dom2.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var AbstractXHRObject = function(){};
+AbstractXHRObject.prototype = new EventEmitter(['chunk', 'finish']);
+
+AbstractXHRObject.prototype._start = function(method, url, payload, opts) {
+ var that = this;
+
+ try {
+ that.xhr = new XMLHttpRequest();
+ } catch(x) {};
+
+ if (!that.xhr) {
+ try {
+ that.xhr = new _window.ActiveXObject('Microsoft.XMLHTTP');
+ } catch(x) {};
+ }
+ if (_window.ActiveXObject || _window.XDomainRequest) {
+ // IE8 caches even POSTs
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date);
+ }
+
+ // Explorer tends to keep connection open, even after the
+ // tab gets closed: http://bugs.jquery.com/ticket/5280
+ that.unload_ref = utils.unload_add(function(){that._cleanup(true);});
+ try {
+ that.xhr.open(method, url, true);
+ } catch(e) {
+ // IE raises an exception on wrong port.
+ that.emit('finish', 0, '');
+ that._cleanup();
+ return;
+ };
+
+ if (!opts || !opts.no_credentials) {
+ // Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest :
+ // "This never affects same-site requests."
+ that.xhr.withCredentials = 'true';
+ }
+ if (opts && opts.headers) {
+ for(var key in opts.headers) {
+ that.xhr.setRequestHeader(key, opts.headers[key]);
+ }
+ }
+
+ that.xhr.onreadystatechange = function() {
+ if (that.xhr) {
+ var x = that.xhr;
+ switch (x.readyState) {
+ case 3:
+ // IE doesn't like peeking into responseText or status
+ // on Microsoft.XMLHTTP and readystate=3
+ try {
+ var status = x.status;
+ var text = x.responseText;
+ } catch (x) {};
+ // IE does return readystate == 3 for 404 answers.
+ if (text && text.length > 0) {
+ that.emit('chunk', status, text);
+ }
+ break;
+ case 4:
+ that.emit('finish', x.status, x.responseText);
+ that._cleanup(false);
+ break;
+ }
+ }
+ };
+ that.xhr.send(payload);
+};
+
+AbstractXHRObject.prototype._cleanup = function(abort) {
+ var that = this;
+ if (!that.xhr) return;
+ utils.unload_del(that.unload_ref);
+
+ // IE needs this field to be a function
+ that.xhr.onreadystatechange = function(){};
+
+ if (abort) {
+ try {
+ that.xhr.abort();
+ } catch(x) {};
+ }
+ that.unload_ref = that.xhr = null;
+};
+
+AbstractXHRObject.prototype.close = function() {
+ var that = this;
+ that.nuke();
+ that._cleanup(true);
+};
+
+var XHRCorsObject = utils.XHRCorsObject = function() {
+ var that = this, args = arguments;
+ utils.delay(function(){that._start.apply(that, args);});
+};
+XHRCorsObject.prototype = new AbstractXHRObject();
+
+var XHRLocalObject = utils.XHRLocalObject = function(method, url, payload) {
+ var that = this;
+ utils.delay(function(){
+ that._start(method, url, payload, {
+ no_credentials: true
+ });
+ });
+};
+XHRLocalObject.prototype = new AbstractXHRObject();
+
+
+
+// References:
+// http://ajaxian.com/archives/100-line-ajax-wrapper
+// http://msdn.microsoft.com/en-us/library/cc288060(v=VS.85).aspx
+var XDRObject = utils.XDRObject = function(method, url, payload) {
+ var that = this;
+ utils.delay(function(){that._start(method, url, payload);});
+};
+XDRObject.prototype = new EventEmitter(['chunk', 'finish']);
+XDRObject.prototype._start = function(method, url, payload) {
+ var that = this;
+ var xdr = new XDomainRequest();
+ // IE caches even POSTs
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date);
+
+ var onerror = xdr.ontimeout = xdr.onerror = function() {
+ that.emit('finish', 0, '');
+ that._cleanup(false);
+ };
+ xdr.onprogress = function() {
+ that.emit('chunk', 200, xdr.responseText);
+ };
+ xdr.onload = function() {
+ that.emit('finish', 200, xdr.responseText);
+ that._cleanup(false);
+ };
+ that.xdr = xdr;
+ that.unload_ref = utils.unload_add(function(){that._cleanup(true);});
+ try {
+ // Fails with AccessDenied if port number is bogus
+ that.xdr.open(method, url);
+ that.xdr.send(payload);
+ } catch(x) {
+ onerror();
+ }
+};
+
+XDRObject.prototype._cleanup = function(abort) {
+ var that = this;
+ if (!that.xdr) return;
+ utils.unload_del(that.unload_ref);
+
+ that.xdr.ontimeout = that.xdr.onerror = that.xdr.onprogress =
+ that.xdr.onload = null;
+ if (abort) {
+ try {
+ that.xdr.abort();
+ } catch(x) {};
+ }
+ that.unload_ref = that.xdr = null;
+};
+
+XDRObject.prototype.close = function() {
+ var that = this;
+ that.nuke();
+ that._cleanup(true);
+};
+
+// 1. Is natively via XHR
+// 2. Is natively via XDR
+// 3. Nope, but postMessage is there so it should work via the Iframe.
+// 4. Nope, sorry.
+utils.isXHRCorsCapable = function() {
+ if (_window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()) {
+ return 1;
+ }
+ // XDomainRequest doesn't work if page is served from file://
+ if (_window.XDomainRequest && _document.domain) {
+ return 2;
+ }
+ if (IframeTransport.enabled()) {
+ return 3;
+ }
+ return 4;
+};
+// [*] End of lib/dom2.js
+
+
+// [*] Including lib/sockjs.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var SockJS = function(url, dep_protocols_whitelist, options) {
+ var that = this, protocols_whitelist;
+ that._options = {devel: false, debug: false, protocols_whitelist: [],
+ info: undefined, rtt: undefined};
+ if (options) {
+ utils.objectExtend(that._options, options);
+ }
+ that._base_url = utils.amendUrl(url);
+ that._server = that._options.server || utils.random_number_string(1000);
+ if (that._options.protocols_whitelist &&
+ that._options.protocols_whitelist.length) {
+ protocols_whitelist = that._options.protocols_whitelist;
+ } else {
+ // Deprecated API
+ if (typeof dep_protocols_whitelist === 'string' &&
+ dep_protocols_whitelist.length > 0) {
+ protocols_whitelist = [dep_protocols_whitelist];
+ } else if (utils.isArray(dep_protocols_whitelist)) {
+ protocols_whitelist = dep_protocols_whitelist
+ } else {
+ protocols_whitelist = null;
+ }
+ if (protocols_whitelist) {
+ that._debug('Deprecated API: Use "protocols_whitelist" option ' +
+ 'instead of supplying protocol list as a second ' +
+ 'parameter to SockJS constructor.');
+ }
+ }
+ that._protocols = [];
+ that.protocol = null;
+ that.readyState = SockJS.CONNECTING;
+ that._ir = createInfoReceiver(that._base_url);
+ that._ir.onfinish = function(info, rtt) {
+ that._ir = null;
+ if (info) {
+ if (that._options.info) {
+ // Override if user supplies the option
+ info = utils.objectExtend(info, that._options.info);
+ }
+ if (that._options.rtt) {
+ rtt = that._options.rtt;
+ }
+ that._applyInfo(info, rtt, protocols_whitelist);
+ that._didClose();
+ } else {
+ that._didClose(1002, 'Can\'t connect to server', true);
+ }
+ };
+};
+// Inheritance
+SockJS.prototype = new REventTarget();
+
+SockJS.version = "0.3.1";
+
+SockJS.CONNECTING = 0;
+SockJS.OPEN = 1;
+SockJS.CLOSING = 2;
+SockJS.CLOSED = 3;
+
+SockJS.prototype._debug = function() {
+ if (this._options.debug)
+ utils.log.apply(utils, arguments);
+};
+
+SockJS.prototype._dispatchOpen = function() {
+ var that = this;
+ if (that.readyState === SockJS.CONNECTING) {
+ if (that._transport_tref) {
+ clearTimeout(that._transport_tref);
+ that._transport_tref = null;
+ }
+ that.readyState = SockJS.OPEN;
+ that.dispatchEvent(new SimpleEvent("open"));
+ } else {
+ // The server might have been restarted, and lost track of our
+ // connection.
+ that._didClose(1006, "Server lost session");
+ }
+};
+
+SockJS.prototype._dispatchMessage = function(data) {
+ var that = this;
+ if (that.readyState !== SockJS.OPEN)
+ return;
+ that.dispatchEvent(new SimpleEvent("message", {data: data}));
+};
+
+SockJS.prototype._dispatchHeartbeat = function(data) {
+ var that = this;
+ if (that.readyState !== SockJS.OPEN)
+ return;
+ that.dispatchEvent(new SimpleEvent('heartbeat', {}));
+};
+
+SockJS.prototype._didClose = function(code, reason, force) {
+ var that = this;
+ if (that.readyState !== SockJS.CONNECTING &&
+ that.readyState !== SockJS.OPEN &&
+ that.readyState !== SockJS.CLOSING)
+ throw new Error('INVALID_STATE_ERR');
+ if (that._ir) {
+ that._ir.nuke();
+ that._ir = null;
+ }
+
+ if (that._transport) {
+ that._transport.doCleanup();
+ that._transport = null;
+ }
+
+ var close_event = new SimpleEvent("close", {
+ code: code,
+ reason: reason,
+ wasClean: utils.userSetCode(code)});
+
+ if (!utils.userSetCode(code) &&
+ that.readyState === SockJS.CONNECTING && !force) {
+ if (that._try_next_protocol(close_event)) {
+ return;
+ }
+ close_event = new SimpleEvent("close", {code: 2000,
+ reason: "All transports failed",
+ wasClean: false,
+ last_event: close_event});
+ }
+ that.readyState = SockJS.CLOSED;
+
+ utils.delay(function() {
+ that.dispatchEvent(close_event);
+ });
+};
+
+SockJS.prototype._didMessage = function(data) {
+ var that = this;
+ var type = data.slice(0, 1);
+ switch(type) {
+ case 'o':
+ that._dispatchOpen();
+ break;
+ case 'a':
+ var payload = JSON.parse(data.slice(1) || '[]');
+ for(var i=0; i < payload.length; i++){
+ that._dispatchMessage(payload[i]);
+ }
+ break;
+ case 'm':
+ var payload = JSON.parse(data.slice(1) || 'null');
+ that._dispatchMessage(payload);
+ break;
+ case 'c':
+ var payload = JSON.parse(data.slice(1) || '[]');
+ that._didClose(payload[0], payload[1]);
+ break;
+ case 'h':
+ that._dispatchHeartbeat();
+ break;
+ }
+};
+
+SockJS.prototype._try_next_protocol = function(close_event) {
+ var that = this;
+ if (that.protocol) {
+ that._debug('Closed transport:', that.protocol, ''+close_event);
+ that.protocol = null;
+ }
+ if (that._transport_tref) {
+ clearTimeout(that._transport_tref);
+ that._transport_tref = null;
+ }
+
+ while(1) {
+ var protocol = that.protocol = that._protocols.shift();
+ if (!protocol) {
+ return false;
+ }
+ // Some protocols require access to `body`, what if were in
+ // the `head`?
+ if (SockJS[protocol] &&
+ SockJS[protocol].need_body === true &&
+ (!_document.body ||
+ (typeof _document.readyState !== 'undefined'
+ && _document.readyState !== 'complete'))) {
+ that._protocols.unshift(protocol);
+ that.protocol = 'waiting-for-load';
+ utils.attachEvent('load', function(){
+ that._try_next_protocol();
+ });
+ return true;
+ }
+
+ if (!SockJS[protocol] ||
+ !SockJS[protocol].enabled(that._options)) {
+ that._debug('Skipping transport:', protocol);
+ } else {
+ var roundTrips = SockJS[protocol].roundTrips || 1;
+ var to = ((that._options.rto || 0) * roundTrips) || 5000;
+ that._transport_tref = utils.delay(to, function() {
+ if (that.readyState === SockJS.CONNECTING) {
+ // I can't understand how it is possible to run
+ // this timer, when the state is CLOSED, but
+ // apparently in IE everythin is possible.
+ that._didClose(2007, "Transport timeouted");
+ }
+ });
+
+ var connid = utils.random_string(8);
+ var trans_url = that._base_url + '/' + that._server + '/' + connid;
+ that._debug('Opening transport:', protocol, ' url:'+trans_url,
+ ' RTO:'+that._options.rto);
+ that._transport = new SockJS[protocol](that, trans_url,
+ that._base_url);
+ return true;
+ }
+ }
+};
+
+SockJS.prototype.close = function(code, reason) {
+ var that = this;
+ if (code && !utils.userSetCode(code))
+ throw new Error("INVALID_ACCESS_ERR");
+ if(that.readyState !== SockJS.CONNECTING &&
+ that.readyState !== SockJS.OPEN) {
+ return false;
+ }
+ that.readyState = SockJS.CLOSING;
+ that._didClose(code || 1000, reason || "Normal closure");
+ return true;
+};
+
+SockJS.prototype.send = function(data) {
+ var that = this;
+ if (that.readyState === SockJS.CONNECTING)
+ throw new Error('INVALID_STATE_ERR');
+ if (that.readyState === SockJS.OPEN) {
+ that._transport.doSend(utils.quote('' + data));
+ }
+ return true;
+};
+
+SockJS.prototype._applyInfo = function(info, rtt, protocols_whitelist) {
+ var that = this;
+ that._options.info = info;
+ that._options.rtt = rtt;
+ that._options.rto = utils.countRTO(rtt);
+ that._options.info.null_origin = !_document.domain;
+ var probed = utils.probeProtocols();
+ that._protocols = utils.detectProtocols(probed, protocols_whitelist, info);
+};
+// [*] End of lib/sockjs.js
+
+
+// [*] Including lib/trans-websocket.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var WebSocketTransport = SockJS.websocket = function(ri, trans_url) {
+ var that = this;
+ var url = trans_url + '/websocket';
+ if (url.slice(0, 5) === 'https') {
+ url = 'wss' + url.slice(5);
+ } else {
+ url = 'ws' + url.slice(4);
+ }
+ that.ri = ri;
+ that.url = url;
+ var Constructor = _window.WebSocket || _window.MozWebSocket;
+
+ that.ws = new Constructor(that.url);
+ that.ws.onmessage = function(e) {
+ that.ri._didMessage(e.data);
+ };
+ // Firefox has an interesting bug. If a websocket connection is
+ // created after onbeforeunload, it stays alive even when user
+ // navigates away from the page. In such situation let's lie -
+ // let's not open the ws connection at all. See:
+ // https://github.com/sockjs/sockjs-client/issues/28
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=696085
+ that.unload_ref = utils.unload_add(function(){that.ws.close()});
+ that.ws.onclose = function() {
+ that.ri._didMessage(utils.closeFrame(1006, "WebSocket connection broken"));
+ };
+};
+
+WebSocketTransport.prototype.doSend = function(data) {
+ this.ws.send('[' + data + ']');
+};
+
+WebSocketTransport.prototype.doCleanup = function() {
+ var that = this;
+ var ws = that.ws;
+ if (ws) {
+ ws.onmessage = ws.onclose = null;
+ ws.close();
+ utils.unload_del(that.unload_ref);
+ that.unload_ref = that.ri = that.ws = null;
+ }
+};
+
+WebSocketTransport.enabled = function() {
+ return !!(_window.WebSocket || _window.MozWebSocket);
+};
+
+// In theory, ws should require 1 round trip. But in chrome, this is
+// not very stable over SSL. Most likely a ws connection requires a
+// separate SSL connection, in which case 2 round trips are an
+// absolute minumum.
+WebSocketTransport.roundTrips = 2;
+// [*] End of lib/trans-websocket.js
+
+
+// [*] Including lib/trans-sender.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var BufferedSender = function() {};
+BufferedSender.prototype.send_constructor = function(sender) {
+ var that = this;
+ that.send_buffer = [];
+ that.sender = sender;
+};
+BufferedSender.prototype.doSend = function(message) {
+ var that = this;
+ that.send_buffer.push(message);
+ if (!that.send_stop) {
+ that.send_schedule();
+ }
+};
+
+// For polling transports in a situation when in the message callback,
+// new message is being send. If the sending connection was started
+// before receiving one, it is possible to saturate the network and
+// timeout due to the lack of receiving socket. To avoid that we delay
+// sending messages by some small time, in order to let receiving
+// connection be started beforehand. This is only a halfmeasure and
+// does not fix the big problem, but it does make the tests go more
+// stable on slow networks.
+BufferedSender.prototype.send_schedule_wait = function() {
+ var that = this;
+ var tref;
+ that.send_stop = function() {
+ that.send_stop = null;
+ clearTimeout(tref);
+ };
+ tref = utils.delay(25, function() {
+ that.send_stop = null;
+ that.send_schedule();
+ });
+};
+
+BufferedSender.prototype.send_schedule = function() {
+ var that = this;
+ if (that.send_buffer.length > 0) {
+ var payload = '[' + that.send_buffer.join(',') + ']';
+ that.send_stop = that.sender(that.trans_url,
+ payload,
+ function() {
+ that.send_stop = null;
+ that.send_schedule_wait();
+ });
+ that.send_buffer = [];
+ }
+};
+
+BufferedSender.prototype.send_destructor = function() {
+ var that = this;
+ if (that._send_stop) {
+ that._send_stop();
+ }
+ that._send_stop = null;
+};
+
+var jsonPGenericSender = function(url, payload, callback) {
+ var that = this;
+
+ if (!('_send_form' in that)) {
+ var form = that._send_form = _document.createElement('form');
+ var area = that._send_area = _document.createElement('textarea');
+ area.name = 'd';
+ form.style.display = 'none';
+ form.style.position = 'absolute';
+ form.method = 'POST';
+ form.enctype = 'application/x-www-form-urlencoded';
+ form.acceptCharset = "UTF-8";
+ form.appendChild(area);
+ _document.body.appendChild(form);
+ }
+ var form = that._send_form;
+ var area = that._send_area;
+ var id = 'a' + utils.random_string(8);
+ form.target = id;
+ form.action = url + '/jsonp_send?i=' + id;
+
+ var iframe;
+ try {
+ // ie6 dynamic iframes with target="" support (thanks Chris Lambacher)
+ iframe = _document.createElement('