/* 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 );