From 02d9b64fece39cfe7869ec2003171caaeab7b402 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 27 Feb 2015 09:47:54 +0100 Subject: [PATCH] mv js vendor libs into vendor folder --- .../js/vendor/jquery.collisioncheck-1.1.js | 56 + .../static/mindmap/js/vendor/jquery.color.js | 664 ++ .../js/vendor/jquery.jsPlumb-1.3.8-all.js | 8761 +++++++++++++++ .../js/vendor/jquery.scrollTo-1.4.2.js | 215 + .../static/mindmap/js/vendor/sockjs-0.3.1.js | 2314 ++++ .../bootstrap/css/bootstrap-responsive.css | 808 ++ .../js/vendor/bootstrap/css/bootstrap.css | 4592 ++++++++ .../img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../bootstrap/img/glyphicons-halflings.png | Bin 0 -> 13826 bytes .../bootstrap/jquery-ui-1.8.20.custom.js | 1854 ++++ .../js/vendor/bootstrap/js/bootstrap.js | 1824 ++++ beautifulmind/static/js/vendor/html5shiv.js | 287 + .../static/js/vendor/jquery-1.7.2.js | 9404 +++++++++++++++++ .../js/vendor/jquery-ui-1.8.20.custom.js | 1587 +++ .../static/js/vendor/jquery.json-2.3.js | 193 + 15 files changed, 32559 insertions(+) create mode 100644 beautifulmind/mindmap/static/mindmap/js/vendor/jquery.collisioncheck-1.1.js create mode 100644 beautifulmind/mindmap/static/mindmap/js/vendor/jquery.color.js create mode 100644 beautifulmind/mindmap/static/mindmap/js/vendor/jquery.jsPlumb-1.3.8-all.js create mode 100644 beautifulmind/mindmap/static/mindmap/js/vendor/jquery.scrollTo-1.4.2.js create mode 100644 beautifulmind/mindmap/static/mindmap/js/vendor/sockjs-0.3.1.js create mode 100644 beautifulmind/static/js/vendor/bootstrap/css/bootstrap-responsive.css create mode 100644 beautifulmind/static/js/vendor/bootstrap/css/bootstrap.css create mode 100644 beautifulmind/static/js/vendor/bootstrap/img/glyphicons-halflings-white.png create mode 100644 beautifulmind/static/js/vendor/bootstrap/img/glyphicons-halflings.png create mode 100644 beautifulmind/static/js/vendor/bootstrap/jquery-ui-1.8.20.custom.js create mode 100644 beautifulmind/static/js/vendor/bootstrap/js/bootstrap.js create mode 100644 beautifulmind/static/js/vendor/html5shiv.js create mode 100644 beautifulmind/static/js/vendor/jquery-1.7.2.js create mode 100644 beautifulmind/static/js/vendor/jquery-ui-1.8.20.custom.js create mode 100644 beautifulmind/static/js/vendor/jquery.json-2.3.js 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('