text: 
  /* jquery.cytoscape-edgehandles.js */
  
  /**
   * This file is part of cytoscape.js 2.0.2.
   * 
   * Cytoscape.js is free software: you can redistribute it and/or modify it
   * under the terms of the GNU Lesser General Public License as published by the Free
   * Software Foundation, either version 3 of the License, or (at your option) any
   * later version.
   * 
   * Cytoscape.js is distributed in the hope that it will be useful, but WITHOUT
   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
   * details.
   * 
   * You should have received a copy of the GNU Lesser General Public License along with
   * cytoscape.js. If not, see <http://www.gnu.org/licenses/>.
   */
   
  ;(function($){
  	
  	var defaults = {
  		preview: true,
  		handleSize: 10,
  		handleColor: "#ff0000",
  		handleLineWidth: 1,
  		hoverDelay: 150,
  		enabled: true,
  		lineType: "draw", // can be "straight" or "draw"
  		edgeType: function( sourceNode, targetNode ){
  			return "node"; // can return "flat" for flat edges between nodes or "node" for intermediate node between them
  			// returning null/undefined means an edge can't be added between the two nodes
  		},
  		loopAllowed: function( node ){
  			return false;
  		},
  		nodeParams: function( sourceNode, targetNode ){
  			return {};
  		},
  		edgeParams: function( sourceNode, targetNode ){
  			return {};
  		},
  		start: function( sourceNode ){
  			// fired when edgehandles interaction starts (drag on handle)
  		},
  		complete: function( sourceNode, targetNodes, addedEntities ){
  			// fired when edgehandles is done and entities are added
  		},
  		stop: function( sourceNode ){
  			// fired when edgehandles interaction is stopped (either complete with added edges or incomplete)
  		}
  	};
  	
  	$.fn.cytoscapeEdgehandles = function( params ){
  		var fn = params;
  		
  		var functions = {
  			destroy: function(){
  				var $container = $(this);
  				var data = $container.data("cyedgehandles");
  				
  				if( data == null ){
  					return;
  				}
  				
  				data.unbind();
  				$container.data("cyedgehandles", {});
  				
  				return $container;
  			},
  			
  			option: function(name, value){
  				var $container = $(this);
  				var data = $container.data("cyedgehandles");
  				
  				if( data == null ){
  					return;
  				}
  				
  				var options = data.options;
  				
  				if( value === undefined ){
  					if( typeof name == typeof {} ){
  						var newOpts = name;
  						options = $.extend(true, {}, defaults, newOpts);
  						data.options = options;
  					} else {
  						return options[ name ];
  					}
  				} else {
  					options[ name ] = value;
  				}
  				
  				$container.data("cyedgehandles", data);
  
  				return $container;
  			},
  
  			disable: function(){
  				return functions.option.apply(this, ["enabled", false]);
  			},
  
  			enable: function(){
  				return functions.option.apply(this, ["enabled", true]);
  			},
  				
  			init: function(){
  				var opts = $.extend(true, {}, defaults, params); 
  				var $container = $(this);
  				var cy;
  				var $canvas = $('<canvas></canvas>');
  				var handle;
  				var line, linePoints;
  				var mdownOnHandle = false;
  				var grabbingNode = false;
  				var inForceStart = false;
  				var hx, hy, hr;
  				var hoverTimeout;
  				var drawsClear = true;
  
  				$container.append( $canvas );
  
  				function sizeCanvas(){
  					$canvas
  						.attr('height', $container.height())
  						.attr('width', $container.width())
  						.css({
  							'position': 'absolute',
  							'z-index': '999'
  						})
  					;
  				}
  				sizeCanvas();
  
  				$(window).bind('resize', function(){
  					sizeCanvas();
  				});
  
  
  				var ctx = $canvas[0].getContext("2d"); 
  				
  				// write options to data
  				var data = $container.data("cyedgehandles");
  				if( data == null ){
  					data = {};
  				}
  				data.options = opts;
  				
  				function options(){
  					return $container.data("cyedgehandles").options;
  				}
  
  				function enabled(){
  					return options().enabled;
  				}
  
  				function disabled(){
  					return !enabled();
  				}
  				
  				function clearDraws(){ 
  
  					if( drawsClear ){ return; } // break early to be efficient
  
  					var w = $container.width();
  					var h = $container.height();
  
  					ctx.clearRect( 0, 0, w, h );
  					drawsClear = true;
  				}
  
  				var lastPanningEnabled, lastZoomingEnabled, lastBoxSelectionEnabled;
  				function disableGestures(){
  					lastPanningEnabled = cy.panningEnabled();
  					lastZoomingEnabled = cy.zoomingEnabled();
  					lastBoxSelectionEnabled = cy.boxSelectionEnabled();
  
  					cy
  						.zoomingEnabled(false)
  						.panningEnabled(false)
  						.boxSelectionEnabled(false)
  					;
  				}
  
  				function resetGestures(){
  					cy
  						.zoomingEnabled(lastZoomingEnabled)
  						.panningEnabled(lastPanningEnabled)
  						.boxSelectionEnabled(lastBoxSelectionEnabled)
  					;
  				}
  
  				function resetToDefaultState(){
  //					console.log("resetToDefaultState");
  
  					clearDraws();
  					
  					//setTimeout(function(){
  						cy.nodes()
  							.removeClass("ui-cytoscape-edgehandles-hover")
  							.removeClass("ui-cytoscape-edgehandles-source")
  							.removeClass("ui-cytoscape-edgehandles-target")
  						;
  					//}, 1);
  					
  
  					linePoints = null;
  					
  					resetGestures();
  				}
  				
  				function makePreview( source, target ){
  					makeEdges( true );
  
  					target.trigger('cyedgehandles.addpreview');
  				}
  				
  				function removePreview( source, target ){
  					source.edgesWith(target).filter(".ui-cytoscape-edgehandles-preview").remove();
  					
  					target
  						.neighborhood("node.ui-cytoscape-edgehandles-preview")
  						.closedNeighborhood(".ui-cytoscape-edgehandles-preview")
  						.remove();
  
  					target.trigger('cyedgehandles.removepreview');
  					
  				}
  				
  				function drawHandle(hx, hy, hr){
  					ctx.fillStyle = options().handleColor;
  					ctx.strokeStyle = options().handleColor;
  
  					ctx.beginPath();
  					ctx.arc(hx, hy, hr, 0 , 2*Math.PI);
  					ctx.closePath();
  					ctx.fill();
  
  					drawsClear = false;
  				}
  
  				function drawLine(hx, hy, x, y){
  					ctx.fillStyle = options().handleColor;
  					ctx.strokeStyle = options().handleColor;
  					ctx.lineWidth = options().handleLineWidth;
  
  					// draw line based on type
  					switch( options().lineType ){
  					case "straight":
  
  						ctx.beginPath();
  						ctx.moveTo(hx, hy);
  						ctx.lineTo(x, y);
  						ctx.closePath();
  						ctx.stroke();
  						
  						break;
  					case "draw":
  					default:
  						
  						if( linePoints == null ){
  							linePoints = [ [x, y] ];
  						} else {
  							linePoints.push([ x, y ]);
  						}
  
  						ctx.beginPath();
  						ctx.moveTo(hx, hy);
  
  						for( var i = 0; i < linePoints.length; i++ ){
  							var pt = linePoints[i];
  
  							ctx.lineTo(pt[0], pt[1]);
  						}
  
  						ctx.stroke();
  
  						break;
  					}
  
  					drawsClear = false;
  				}
  
  				function makeEdges( preview, src, tgt ){
  					
  					// console.log("make edges");
  					
  					var source = src ? src : cy.nodes(".ui-cytoscape-edgehandles-source");
  					var targets = tgt ? tgt : cy.nodes(".ui-cytoscape-edgehandles-target");
  					var classes = preview ? "ui-cytoscape-edgehandles-preview" : "";
  					var added = cy.collection();
  					
  					if( source.size() === 0 || targets.size() === 0 ){
  						return; // nothing to do :(
  					}
  					
  					// just remove preview class if we already have the edges
  					if( !src && !tgt ){
  						if( !preview && options().preview ){
  							added = cy.elements(".ui-cytoscape-edgehandles-preview").removeClass("ui-cytoscape-edgehandles-preview");
  							
  							options().complete( source, targets, added );
  							source.trigger('cyedgehandles.complete');	
  							return;
  						} else {
  							// remove old previews
  							cy.elements(".ui-cytoscape-edgehandles-preview").remove();
  						}
  					}
  					
  					for( var i = 0; i < targets.length; i++ ){
  						var target = targets[i];
  						
  						switch( options().edgeType( source, target ) ){
  						case "node":
  							
  							var p1 = source.position();
  							var p2 = target.position();
  							var p = {
  								x: (p1.x + p2.x)/2,
  								y: (p1.y + p2.y)/2
  							};
  												
  							var interNode = cy.add($.extend( true, {
  								group: "nodes",
  								position: p
  							}, options().nodeParams(source, target) )).addClass(classes);
  
  							var source2inter = cy.add($.extend( true, {
  								group: "edges",
  								data: {
  									source: source.id(),
  									target: interNode.id()
  								}
  							}, options().edgeParams(source, target) )).addClass(classes);
  							
  							var inter2target = cy.add($.extend( true, {
  								group: "edges",
  								data: {
  									source: interNode.id(),
  									target: target.id()
  								}
  							}, options().edgeParams(source, target) )).addClass(classes);
  							
  							added = added.add( interNode ).add( source2inter ).add( inter2target );
  							
  							break;
  						
  						case "flat":
  							var edge = cy.add($.extend( true, {
  								group: "edges",
  								data: {
  									source: source.id(),
  									target: target.id()
  								}
  							}, options().edgeParams(source, target) )).addClass(classes);
  						
  							added = added.add( edge );
  						
  							break;
  
  						default:
  							target.removeClass("ui-cytoscape-edgehandles-target");
  							break; // don't add anything
  						}
  					}
  					
  					if( !preview ){
  						options().complete( source, targets, added );
  						source.trigger('cyedgehandles.complete');	
  					}
  				}
  
  				$container.cytoscape(function(e){
  					cy = this;
  					
  					lastPanningEnabled = cy.panningEnabled();
  					lastZoomingEnabled = cy.zoomingEnabled();
  					lastBoxSelectionEnabled = cy.boxSelectionEnabled();
  
  					// console.log('handles on ready')
  
  					var lastActiveId;
  
  					var transformHandler;
  					cy.bind("zoom pan", transformHandler = function(){
  						clearDraws();
  					});
  					
  					var lastMdownHandler;
  
  					var startHandler, hoverHandler, leaveHandler, grabNodeHandler, freeNodeHandler, dragNodeHandler, forceStartHandler, removeHandler;
  					cy.on("mouseover", "node", startHandler = function(e){
  						
  						if( disabled() || mdownOnHandle || grabbingNode || this.hasClass("ui-cytoscape-edgehandles-preview") || inForceStart ){
  							return; // don't override existing handle that's being dragged
  							// also don't trigger when grabbing a node etc
  						} 
  						
  						//console.log("mouseover startHandler %s %o", this.id(), this);
  						
  						if( lastMdownHandler ){
  							$container[0].removeEventListener('mousedown', lastMdownHandler, true);
  						}
  
  						var node = this;
  						var source = this;
  						var p = node.renderedPosition();
  						var h = node.renderedOuterHeight();
  						
  						lastActiveId = node.id();
  
  						// remove old handle
  						clearDraws();
  						
  						hr = options().handleSize/2 * cy.zoom();
  						hx = p.x;
  						hy = p.y - h/2 - hr/2;
  						
  						// add new handle
  						drawHandle(hx, hy, hr);
  
  						node.trigger('cyedgehandles.showhandle');
  						
  
  						function mdownHandler(e){
  							$container[0].removeEventListener('mousedown', mdownHandler, true);
  
  							var x = e.pageX - $container.offset().left;
  							var y = e.pageY - $container.offset().top;
  
  							if( e.button !== 0 ){
  								return; // sorry, no right clicks allowed 
  							}
  							
  							if( Math.abs(x - hx) > hr || Math.abs(y - hy) > hr ){
  								return; // only consider this a proper mousedown if on the handle
  							}
  
  							if( inForceStart ){
  								return; // we don't want this going off if we have the forced start to consider
  							}
  
  							// console.log("mdownHandler %s %o", node.id(), node);
  							
  							mdownOnHandle = true;
  							
  							e.preventDefault();
  							e.stopPropagation();
  							
  							node.addClass("ui-cytoscape-edgehandles-source");
  							node.trigger('cyedgehandles.start');
  							
  							function doneMoving(dmEvent){
  								// console.log("doneMoving %s %o", node.id(), node);
  								
  								if( !mdownOnHandle || inForceStart ){
  									return;
  								}
  								
  								var $this = $(this);
  								mdownOnHandle = false;
  								$(window).unbind("mousemove", moveHandler);
  								
  								makeEdges();
  								resetToDefaultState();
  								
  								options().stop( node );
  								node.trigger('cyedgehandles.stop');
  							}
  							
  							$(window).one("mouseup blur", doneMoving).bind("mousemove", moveHandler);
  							disableGestures();
  							
  							options().start( node );
  
  							return false;
  						}
  						
  						function moveHandler(e){
  							// console.log("mousemove moveHandler %s %o", node.id(), node);
  							
  							var x = e.pageX - $container.offset().left;
  							var y = e.pageY - $container.offset().top;
  
  							clearDraws();
  							drawHandle(hx, hy, hr);
  							drawLine(hx, hy, x, y);
  							
  							return false;
  						}
  
  						$container[0].addEventListener('mousedown', mdownHandler, true);
  						lastMdownHandler = mdownHandler;
  
  						
  					}).on("mouseover touchover", "node", hoverHandler = function(){
  						var node = this;
  						var target = this;
  
  // console.log('mouseover hoverHandler')
  
  						if( disabled() || this.hasClass("ui-cytoscape-edgehandles-preview") ){
  							return; // ignore preview nodes
  						}
  						
  						if( mdownOnHandle ){ // only handle mdown case
  
  							// console.log( 'mouseover hoverHandler %s $o', node.id(), node );
  
  							clearTimeout( hoverTimeout );
  							hoverTimeout = setTimeout(function(){
  								var source = cy.nodes(".ui-cytoscape-edgehandles-source");
  								
  								var isLoop = node.hasClass("ui-cytoscape-edgehandles-source");
  								var loopAllowed = options().loopAllowed( node );
  								
  								if( !isLoop || (isLoop && loopAllowed) ){
  									node.addClass("ui-cytoscape-edgehandles-hover");
  									node.toggleClass("ui-cytoscape-edgehandles-target");
  									
  									if( options().preview ){
  										if( node.hasClass("ui-cytoscape-edgehandles-target") ){
  											makePreview( source, target );
  										} else {
  											removePreview( source, target );
  										}
  									}
  								}
  							}, options().hoverDelay);
  
  							return false;
  						}
  
  					}).on("mouseout", "node", leaveHandler = function(){
  						if( this.hasClass("ui-cytoscape-edgehandles-hover") ){
  							this.removeClass("ui-cytoscape-edgehandles-hover");
  						}
  
  						if( mdownOnHandle ){
  							clearTimeout(hoverTimeout);
  						}
  
  					}).on("drag position", "node", dragNodeHandler = function(){
  						setTimeout(clearDraws, 50);
  
  					}).on("grab", "node", grabHandler = function(){
  						grabbingNode = true;
  
  						setTimeout(function(){
  							clearDraws();
  						}, 5);
  						
  
  					}).on("free", "node", freeNodeHandler = function(){
  						grabbingNode = false;
  
  					}).on("cyedgehandles.forcestart", "node", forceStartHandler = function(){
  						inForceStart = true;
  						clearDraws(); // clear just in case
  
  						var node = this;
  						var source = node;
  
  						lastActiveId = node.id();
  
  						node.trigger('cyedgehandles.start');
  						node.addClass('ui-cytoscape-edgehandles-source');
  
  						var p = node.renderedPosition();
  						var h = node.renderedOuterHeight();
  						var w = node.renderedOuterWidth();
  												
  						var hr = options().handleSize/2 * cy.zoom();
  						var hx = p.x;
  						var hy = p.y - h/2 - hr/2;
  
  						drawHandle(hx, hy, hr);
  
  						node.trigger('cyedgehandles.showhandle');
  
  						// case: down and drag as normal
  						var downHandler = function(e){
  							
  							$container[0].removeEventListener('mousedown', downHandler, true);
  							$container[0].removeEventListener('touchstart', downHandler, true);
  
  							var x = (e.pageX !== undefined ? e.pageX : e.originalEvent.touches[0].pageX) - $container.offset().left;
  							var y = (e.pageY !== undefined ? e.pageY : e.originalEvent.touches[0].pageY) - $container.offset().top;
  							var d = hr/2;
  							var onNode = p.x - w/2 - d <= x && x <= p.x + w/2 + d
  								&& p.y - h/2 - d <= y && y <= p.y + h/2 + d;
  
  							if( onNode ){
  								disableGestures();
  								mdownOnHandle = true; // enable the regular logic for handling going over target nodes
  								
  								var moveHandler = function(me){
  									var x = (me.pageX !== undefined ? me.pageX : me.originalEvent.touches[0].pageX) - $container.offset().left;
  									var y = (me.pageY !== undefined ? me.pageY : me.originalEvent.touches[0].pageY) - $container.offset().top;
  
  									clearDraws();
  									drawHandle(hx, hy, hr);
  									drawLine(hx, hy, x, y);
  								}
  
  								$container[0].addEventListener('mousemove', moveHandler, true);
  								$container[0].addEventListener('touchmove', moveHandler, true);
  
  								$(window).one("mouseup touchend blur", function(){
  									$container[0].removeEventListener('mousemove', moveHandler, true);
  									$container[0].removeEventListener('touchmove', moveHandler, true);
  
  									inForceStart = false; // now we're done so reset the flag
  									mdownOnHandle = false; // we're also no longer down on the node
  
  									makeEdges();
  
  									options().stop( node );
  									node.trigger('cyedgehandles.stop');
  
  									cy.off("tap", "node", tapHandler);
  									node.off("remove", removeBeforeHandler);
  									resetToDefaultState();
  								});
  
  								e.stopPropagation();
  								e.preventDefault();
  								return false;
  							}
  						};
  
  						$container[0].addEventListener('mousedown', downHandler, true);
  						$container[0].addEventListener('touchstart', downHandler, true);
  
  						var removeBeforeHandler;
  						node.one("remove", function(){
  							$container[0].removeEventListener('mousedown', downHandler, true);
  							$container[0].removeEventListener('touchstart', downHandler, true);
  							cy.off("tap", "node", tapHandler);
  						});
  
  						// case: tap a target node
  						var tapHandler;
  						cy.one("tap", "node", tapHandler = function(){
  							var target = this;
  
  							var isLoop = source.id() === target.id();
  							var loopAllowed = options().loopAllowed( target );
  							
  							if( !isLoop || (isLoop && loopAllowed) ){							
  								makeEdges(false, source, target);
  
  								//options().complete( node );
  								//node.trigger('cyedgehandles.complete');	
  							}
  
  							inForceStart = false; // now we're done so reset the flag
  
  							options().stop( node );
  							node.trigger('cyedgehandles.stop');
  
  							$container[0].removeEventListener('mousedown', downHandler, true);
  							$container[0].removeEventListener('touchstart', downHandler, true);
  							node.off("remove", removeBeforeHandler);
  							resetToDefaultState();
  						});
  					
  
  					}).on("remove", "node", removeHandler = function(){
  						var id = this.id();
  
  						if( id === lastActiveId ){
  							setTimeout(function(){
  								resetToDefaultState();
  							}, 5);
  						}
  					});
  				
  
  					data.unbind = function(){
  						cy
  							.off("mouseover", "node", startHandler)
  							.off("mouseover", "node", hoverHandler)
  							.off("mouseout", "node", leaveHandler)
  							.off("drag position", "node", dragNodeHandler)
  							.off("grab", "node", grabNodeHandler)
  							.off("free", "node", freeNodeHandler)
  							.off("cyedgehandles.forcestart", "node", forceStartHandler)
  							.off("remove", "node", removeHandler)
  						;
  						
  						cy.unbind("zoom pan", transformHandler);
  					};
  				});
  				
  				$container.data("cyedgehandles", data);
  			},
  
  			start: function( id ){
  				$container = $(this);
  
  				$container.cytoscape(function(e){
  					var cy = this;
  
  					cy.$("#" + id).trigger('cyedgehandles.forcestart');
  				});
  			}
  		};
  		
  		if( functions[fn] ){
  			return functions[fn].apply(this, Array.prototype.slice.call( arguments, 1 ));
  		} else if( typeof fn == 'object' || !fn ) {
  			return functions.init.apply( this, arguments );
  		} else {
  			$.error("No such function `"+ fn +"` for jquery.cytoscapeEdgeHandles");
  		}
  		
  		return $(this);
  	};
  
  	$.fn.cyEdgehandles = $.fn.cytoscapeEdgehandles;
  	
  })( jQuery );