/* 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 . */ ;(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 = $(''); 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 );