cytoscape
Version:
Graph theory (a.k.a. network) library for analysis and visualisation
1,784 lines (1,355 loc) • 59.6 kB
JavaScript
import * as is from '../../../is';
import * as util from '../../../util';
import * as math from '../../../math';
var BRp = {};
/* global document, window, ResizeObserver, MutationObserver */
BRp.registerBinding = function( target, event, handler, useCapture ){ // eslint-disable-line no-unused-vars
var args = Array.prototype.slice.apply( arguments, [1] ); // copy
var b = this.binder( target );
return b.on.apply( b, args );
};
BRp.binder = function( tgt ){
var r = this;
var tgtIsDom = tgt === window || tgt === document || tgt === document.body || is.domElement( tgt );
if( r.supportsPassiveEvents == null ){
// from https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
var supportsPassive = false;
try {
var opts = Object.defineProperty( {}, 'passive', {
get: function(){
supportsPassive = true;
return true;
}
} );
window.addEventListener( 'test', null, opts );
} catch( err ){
// not supported
}
r.supportsPassiveEvents = supportsPassive;
}
var on = function( event, handler, useCapture ){
var args = Array.prototype.slice.call( arguments );
if( tgtIsDom && r.supportsPassiveEvents ){ // replace useCapture w/ opts obj
args[2] = {
capture: useCapture != null ? useCapture : false,
passive: false,
once: false
};
}
r.bindings.push({
target: tgt,
args: args
});
( tgt.addEventListener || tgt.on ).apply( tgt, args );
return this;
};
return {
on: on,
addEventListener: on,
addListener: on,
bind: on
};
};
BRp.nodeIsDraggable = function( node ){
return (
node
&& node.isNode()
&& !node.locked()
&& node.grabbable()
);
};
BRp.nodeIsGrabbable = function( node ){
return (
this.nodeIsDraggable( node )
&& node.interactive()
);
};
BRp.load = function(){
var r = this;
var isSelected = ele => ele.selected();
var triggerEvents = function( target, names, e, position ){
if( target == null ){
target = r.cy;
}
for( var i = 0; i < names.length; i++ ){
var name = names[ i ];
target.emit({
originalEvent: e,
type: name,
position
});
}
};
var isMultSelKeyDown = function( e ){
return e.shiftKey || e.metaKey || e.ctrlKey; // maybe e.altKey
};
var allowPanningPassthrough = function( down, downs ){
var allowPassthrough = true;
if( r.cy.hasCompoundNodes() && down && down.pannable() ){
// a grabbable compound node below the ele => no passthrough panning
for( var i = 0; downs && i < downs.length; i++ ){
var down = downs[i];
//if any parent node in event hierarchy isn't pannable, reject passthrough
if( down.isNode() && down.isParent() && !down.pannable() ){
allowPassthrough = false;
break;
}
}
} else {
allowPassthrough = true;
}
return allowPassthrough;
};
var setGrabbed = function( ele ){
ele[0]._private.grabbed = true;
};
var setFreed = function( ele ){
ele[0]._private.grabbed = false;
};
var setInDragLayer = function( ele ){
ele[0]._private.rscratch.inDragLayer = true;
};
var setOutDragLayer = function( ele ){
ele[0]._private.rscratch.inDragLayer = false;
};
var setGrabTarget = function( ele ){
ele[0]._private.rscratch.isGrabTarget = true;
};
var removeGrabTarget = function( ele ){
ele[0]._private.rscratch.isGrabTarget = false;
};
var addToDragList = function( ele, opts ){
var list = opts.addToList;
var listHasEle = list.has(ele);
if( !listHasEle && ele.grabbable() && !ele.locked() ){
list.merge( ele );
setGrabbed( ele );
}
};
// helper function to determine which child nodes and inner edges
// of a compound node to be dragged as well as the grabbed and selected nodes
var addDescendantsToDrag = function( node, opts ){
if( !node.cy().hasCompoundNodes() ){
return;
}
if( opts.inDragLayer == null && opts.addToList == null ){ return; } // nothing to do
var innerNodes = node.descendants();
if( opts.inDragLayer ){
innerNodes.forEach( setInDragLayer );
innerNodes.connectedEdges().forEach( setInDragLayer );
}
if( opts.addToList ){
addToDragList(innerNodes, opts);
}
};
// adds the given nodes and its neighbourhood to the drag layer
var addNodesToDrag = function( nodes, opts ){
opts = opts || {};
var hasCompoundNodes = nodes.cy().hasCompoundNodes();
if( opts.inDragLayer ){
nodes.forEach( setInDragLayer );
nodes.neighborhood().stdFilter(function( ele ){
return !hasCompoundNodes || ele.isEdge();
}).forEach( setInDragLayer );
}
if( opts.addToList ){
nodes.forEach(function( ele ){
addToDragList( ele, opts );
});
}
addDescendantsToDrag( nodes, opts ); // always add to drag
// also add nodes and edges related to the topmost ancestor
updateAncestorsInDragLayer( nodes, {
inDragLayer: opts.inDragLayer
} );
r.updateCachedGrabbedEles();
};
var addNodeToDrag = addNodesToDrag;
var freeDraggedElements = function( grabbedEles ){
if( !grabbedEles ){ return; }
// just go over all elements rather than doing a bunch of (possibly expensive) traversals
r.getCachedZSortedEles().forEach(function( ele ){
setFreed( ele );
setOutDragLayer( ele );
removeGrabTarget( ele );
});
r.updateCachedGrabbedEles();
};
// helper function to determine which ancestor nodes and edges should go
// to the drag layer (or should be removed from drag layer).
var updateAncestorsInDragLayer = function( node, opts ){
if( opts.inDragLayer == null && opts.addToList == null ){ return; } // nothing to do
if( !node.cy().hasCompoundNodes() ){
return;
}
// find top-level parent
var parent = node.ancestors().orphans();
// no parent node: no nodes to add to the drag layer
if( parent.same( node ) ){
return;
}
var nodes = parent.descendants().spawnSelf()
.merge( parent )
.unmerge( node )
.unmerge( node.descendants() )
;
var edges = nodes.connectedEdges();
if( opts.inDragLayer ){
edges.forEach( setInDragLayer );
nodes.forEach( setInDragLayer );
}
if( opts.addToList ){
nodes.forEach(function( ele ){
addToDragList( ele, opts );
});
}
};
var blurActiveDomElement = function(){
if( document.activeElement != null && document.activeElement.blur != null ){
document.activeElement.blur();
}
};
var haveMutationsApi = typeof MutationObserver !== 'undefined';
var haveResizeObserverApi = typeof ResizeObserver !== 'undefined';
// watch for when the cy container is removed from the dom
if( haveMutationsApi ){
r.removeObserver = new MutationObserver( function( mutns ){ // eslint-disable-line no-undef
for( var i = 0; i < mutns.length; i++ ){
var mutn = mutns[ i ];
var rNodes = mutn.removedNodes;
if( rNodes ){ for( var j = 0; j < rNodes.length; j++ ){
var rNode = rNodes[ j ];
if( rNode === r.container ){
r.destroy();
break;
}
} }
}
} );
if( r.container.parentNode ){
r.removeObserver.observe( r.container.parentNode, { childList: true } );
}
} else {
r.registerBinding( r.container, 'DOMNodeRemoved', function( e ){ // eslint-disable-line no-unused-vars
r.destroy();
} );
}
var onResize = util.debounce( function(){
r.cy.resize();
}, 100 );
if( haveMutationsApi ){
r.styleObserver = new MutationObserver( onResize ); // eslint-disable-line no-undef
r.styleObserver.observe( r.container, { attributes: true } );
}
// auto resize
r.registerBinding( window, 'resize', onResize ); // eslint-disable-line no-undef
if( haveResizeObserverApi ){
r.resizeObserver = new ResizeObserver(onResize); // eslint-disable-line no-undef
r.resizeObserver.observe( r.container );
}
var forEachUp = function( domEle, fn ){
while( domEle != null ){
fn( domEle );
domEle = domEle.parentNode;
}
};
var invalidateCoords = function(){
r.invalidateContainerClientCoordsCache();
};
forEachUp( r.container, function( domEle ){
r.registerBinding( domEle, 'transitionend', invalidateCoords );
r.registerBinding( domEle, 'animationend', invalidateCoords );
r.registerBinding( domEle, 'scroll', invalidateCoords );
} );
// stop right click menu from appearing on cy
r.registerBinding( r.container, 'contextmenu', function( e ){
e.preventDefault();
} );
var inBoxSelection = function(){
return r.selection[4] !== 0;
};
var eventInContainer = function( e ){
// save cycles if mouse events aren't to be captured
var containerPageCoords = r.findContainerClientCoords();
var x = containerPageCoords[0];
var y = containerPageCoords[1];
var width = containerPageCoords[2];
var height = containerPageCoords[3];
var positions = e.touches ? e.touches : [ e ];
var atLeastOnePosInside = false;
for( var i = 0; i < positions.length; i++ ){
var p = positions[i];
if( x <= p.clientX && p.clientX <= x + width
&& y <= p.clientY && p.clientY <= y + height
){
atLeastOnePosInside = true;
break;
}
}
if( !atLeastOnePosInside ){ return false; }
var container = r.container;
var target = e.target;
var tParent = target.parentNode;
var containerIsTarget = false;
while( tParent ){
if( tParent === container ){
containerIsTarget = true;
break;
}
tParent = tParent.parentNode;
}
if( !containerIsTarget ){ return false; } // if target is outisde cy container, then this event is not for us
return true;
};
// Primary key
r.registerBinding( r.container, 'mousedown', function mousedownHandler( e ){
if( !eventInContainer(e) ){ return; }
e.preventDefault();
blurActiveDomElement();
r.hoverData.capture = true;
r.hoverData.which = e.which;
var cy = r.cy;
var gpos = [ e.clientX, e.clientY ];
var pos = r.projectIntoViewport( gpos[0], gpos[1] );
var select = r.selection;
var nears = r.findNearestElements( pos[0], pos[1], true, false );
var near = nears[0];
var draggedElements = r.dragData.possibleDragElements;
r.hoverData.mdownPos = pos;
r.hoverData.mdownGPos = gpos;
var checkForTaphold = function(){
r.hoverData.tapholdCancelled = false;
clearTimeout( r.hoverData.tapholdTimeout );
r.hoverData.tapholdTimeout = setTimeout( function(){
if( r.hoverData.tapholdCancelled ){
return;
} else {
var ele = r.hoverData.down;
if( ele ){
ele.emit( {
originalEvent: e,
type: 'taphold',
position: { x: pos[0], y: pos[1] }
} );
} else {
cy.emit( {
originalEvent: e,
type: 'taphold',
position: { x: pos[0], y: pos[1] }
} );
}
}
}, r.tapholdDuration );
};
// Right click button
if( e.which == 3 ){
r.hoverData.cxtStarted = true;
var cxtEvt = {
originalEvent: e,
type: 'cxttapstart',
position: { x: pos[0], y: pos[1] }
};
if( near ){
near.activate();
near.emit( cxtEvt );
r.hoverData.down = near;
} else {
cy.emit( cxtEvt );
}
r.hoverData.downTime = (new Date()).getTime();
r.hoverData.cxtDragged = false;
// Primary button
} else if( e.which == 1 ){
if( near ){
near.activate();
}
// Element dragging
{
// If something is under the cursor and it is draggable, prepare to grab it
if( near != null ){
if( r.nodeIsGrabbable( near ) ){
var makeEvent = function( type ){
return {
originalEvent: e,
type: type,
position: { x: pos[0], y: pos[1] }
};
};
var triggerGrab = function( ele ){
ele.emit( makeEvent('grab') );
};
setGrabTarget( near );
if( !near.selected() ){
draggedElements = r.dragData.possibleDragElements = cy.collection();
addNodeToDrag( near, { addToList: draggedElements } );
near.emit( makeEvent('grabon') ).emit( makeEvent('grab') );
} else {
draggedElements = r.dragData.possibleDragElements = cy.collection();
var selectedNodes = cy.$( function( ele ){ return ele.isNode() && ele.selected() && r.nodeIsGrabbable( ele ); } );
addNodesToDrag( selectedNodes, { addToList: draggedElements } );
near.emit( makeEvent('grabon') );
selectedNodes.forEach( triggerGrab );
}
r.redrawHint( 'eles', true );
r.redrawHint( 'drag', true );
}
}
r.hoverData.down = near;
r.hoverData.downs = nears;
r.hoverData.downTime = (new Date()).getTime();
}
triggerEvents( near, [ 'mousedown', 'tapstart', 'vmousedown' ], e, { x: pos[0], y: pos[1] } );
if( near == null ){
select[4] = 1;
r.data.bgActivePosistion = {
x: pos[0],
y: pos[1]
};
r.redrawHint( 'select', true );
r.redraw();
} else if( near.pannable() ){
select[4] = 1; // for future pan
}
checkForTaphold();
}
// Initialize selection box coordinates
select[0] = select[2] = pos[0];
select[1] = select[3] = pos[1];
}, false );
r.registerBinding( window, 'mousemove', function mousemoveHandler( e ){ // eslint-disable-line no-undef
var capture = r.hoverData.capture;
if( !capture && !eventInContainer(e) ){ return; }
var preventDefault = false;
var cy = r.cy;
var zoom = cy.zoom();
var gpos = [ e.clientX, e.clientY ];
var pos = r.projectIntoViewport( gpos[0], gpos[1] );
var mdownPos = r.hoverData.mdownPos;
var mdownGPos = r.hoverData.mdownGPos;
var select = r.selection;
var near = null;
if( !r.hoverData.draggingEles && !r.hoverData.dragging && !r.hoverData.selecting ){
near = r.findNearestElement( pos[0], pos[1], true, false );
}
var last = r.hoverData.last;
var down = r.hoverData.down;
var disp = [ pos[0] - select[2], pos[1] - select[3] ];
var draggedElements = r.dragData.possibleDragElements;
var isOverThresholdDrag;
if( mdownGPos ){
var dx = gpos[0] - mdownGPos[0];
var dx2 = dx * dx;
var dy = gpos[1] - mdownGPos[1];
var dy2 = dy * dy;
var dist2 = dx2 + dy2;
r.hoverData.isOverThresholdDrag = isOverThresholdDrag = dist2 >= r.desktopTapThreshold2;
}
var multSelKeyDown = isMultSelKeyDown( e );
if (isOverThresholdDrag) {
r.hoverData.tapholdCancelled = true;
}
var updateDragDelta = function(){
var dragDelta = r.hoverData.dragDelta = r.hoverData.dragDelta || [];
if( dragDelta.length === 0 ){
dragDelta.push( disp[0] );
dragDelta.push( disp[1] );
} else {
dragDelta[0] += disp[0];
dragDelta[1] += disp[1];
}
};
preventDefault = true;
triggerEvents( near, [ 'mousemove', 'vmousemove', 'tapdrag' ], e, { x: pos[0], y: pos[1] } );
var goIntoBoxMode = function(){
r.data.bgActivePosistion = undefined;
if( !r.hoverData.selecting ){
cy.emit( ( {
originalEvent: e,
type: 'boxstart',
position: { x: pos[0], y: pos[1] }
} ) );
}
select[4] = 1;
r.hoverData.selecting = true;
r.redrawHint( 'select', true );
r.redraw();
};
// trigger context drag if rmouse down
if( r.hoverData.which === 3 ){
// but only if over threshold
if( isOverThresholdDrag ){
var cxtEvt = ( {
originalEvent: e,
type: 'cxtdrag',
position: { x: pos[0], y: pos[1] }
} );
if( down ){
down.emit( cxtEvt );
} else {
cy.emit( cxtEvt );
}
r.hoverData.cxtDragged = true;
if( !r.hoverData.cxtOver || near !== r.hoverData.cxtOver ){
if( r.hoverData.cxtOver ){
r.hoverData.cxtOver.emit( ( {
originalEvent: e,
type: 'cxtdragout',
position: { x: pos[0], y: pos[1] }
} ) );
}
r.hoverData.cxtOver = near;
if( near ){
near.emit( ( {
originalEvent: e,
type: 'cxtdragover',
position: { x: pos[0], y: pos[1] }
} ) );
}
}
}
// Check if we are drag panning the entire graph
} else if( r.hoverData.dragging ){
preventDefault = true;
if( cy.panningEnabled() && cy.userPanningEnabled() ){
var deltaP;
if( r.hoverData.justStartedPan ){
var mdPos = r.hoverData.mdownPos;
deltaP = {
x: ( pos[0] - mdPos[0] ) * zoom,
y: ( pos[1] - mdPos[1] ) * zoom
};
r.hoverData.justStartedPan = false;
} else {
deltaP = {
x: disp[0] * zoom,
y: disp[1] * zoom
};
}
cy.panBy( deltaP );
cy.emit('dragpan');
r.hoverData.dragged = true;
}
// Needs reproject due to pan changing viewport
pos = r.projectIntoViewport( e.clientX, e.clientY );
// Checks primary button down & out of time & mouse not moved much
} else if(
select[4] == 1 && (down == null || down.pannable())
){
if( isOverThresholdDrag ){
if( !r.hoverData.dragging && cy.boxSelectionEnabled() && ( multSelKeyDown || !cy.panningEnabled() || !cy.userPanningEnabled() ) ){
goIntoBoxMode();
} else if( !r.hoverData.selecting && cy.panningEnabled() && cy.userPanningEnabled() ){
var allowPassthrough = allowPanningPassthrough( down, r.hoverData.downs );
if( allowPassthrough ){
r.hoverData.dragging = true;
r.hoverData.justStartedPan = true;
select[4] = 0;
r.data.bgActivePosistion = math.array2point( mdownPos );
r.redrawHint( 'select', true );
r.redraw();
}
}
if( down && down.pannable() && down.active() ){ down.unactivate(); }
}
} else {
if( down && down.pannable() && down.active() ){ down.unactivate(); }
if( ( !down || !down.grabbed() ) && near != last ){
if( last ){
triggerEvents( last, [ 'mouseout', 'tapdragout' ], e, { x: pos[0], y: pos[1] } );
}
if( near ){
triggerEvents( near, [ 'mouseover', 'tapdragover' ], e, { x: pos[0], y: pos[1] } );
}
r.hoverData.last = near;
}
if( down ){
if( isOverThresholdDrag ){ // then we can take action
if( cy.boxSelectionEnabled() && multSelKeyDown ){ // then selection overrides
if( down && down.grabbed() ){
freeDraggedElements( draggedElements );
down.emit('freeon');
draggedElements.emit('free');
if( r.dragData.didDrag ){
down.emit('dragfreeon');
draggedElements.emit('dragfree');
}
}
goIntoBoxMode();
} else if( down && down.grabbed() && r.nodeIsDraggable( down ) ){ // drag node
var justStartedDrag = !r.dragData.didDrag;
if( justStartedDrag ){
r.redrawHint( 'eles', true );
}
r.dragData.didDrag = true; // indicate that we actually did drag the node
// now, add the elements to the drag layer if not done already
if( !r.hoverData.draggingEles ){
addNodesToDrag( draggedElements, { inDragLayer: true } );
}
let totalShift = { x: 0, y: 0 };
if( is.number( disp[0] ) && is.number( disp[1] ) ){
totalShift.x += disp[0];
totalShift.y += disp[1];
if( justStartedDrag ){
var dragDelta = r.hoverData.dragDelta;
if( dragDelta && is.number( dragDelta[0] ) && is.number( dragDelta[1] ) ){
totalShift.x += dragDelta[0];
totalShift.y += dragDelta[1];
}
}
}
r.hoverData.draggingEles = true;
( draggedElements
.silentShift( totalShift )
.emit('position drag')
);
r.redrawHint( 'drag', true );
r.redraw();
}
} else { // otherwise save drag delta for when we actually start dragging so the relative grab pos is constant
updateDragDelta();
}
}
// prevent the dragging from triggering text selection on the page
preventDefault = true;
}
select[2] = pos[0]; select[3] = pos[1];
if( preventDefault ){
if( e.stopPropagation ) e.stopPropagation();
if( e.preventDefault ) e.preventDefault();
return false;
}
}, false );
let clickTimeout, didDoubleClick, prevClickTimeStamp;
r.registerBinding( window, 'mouseup', function mouseupHandler( e ){ // eslint-disable-line no-undef
var capture = r.hoverData.capture;
if( !capture ){ return; }
r.hoverData.capture = false;
var cy = r.cy; var pos = r.projectIntoViewport( e.clientX, e.clientY ); var select = r.selection;
var near = r.findNearestElement( pos[0], pos[1], true, false );
var draggedElements = r.dragData.possibleDragElements; var down = r.hoverData.down;
var multSelKeyDown = isMultSelKeyDown( e );
if( r.data.bgActivePosistion ){
r.redrawHint( 'select', true );
r.redraw();
}
r.hoverData.tapholdCancelled = true;
r.data.bgActivePosistion = undefined; // not active bg now
if( down ){
down.unactivate();
}
if( r.hoverData.which === 3 ){
var cxtEvt = ( {
originalEvent: e,
type: 'cxttapend',
position: { x: pos[0], y: pos[1] }
} );
if( down ){
down.emit( cxtEvt );
} else {
cy.emit( cxtEvt );
}
if( !r.hoverData.cxtDragged ){
var cxtTap = ( {
originalEvent: e,
type: 'cxttap',
position: { x: pos[0], y: pos[1] }
} );
if( down ){
down.emit( cxtTap );
} else {
cy.emit( cxtTap );
}
}
r.hoverData.cxtDragged = false;
r.hoverData.which = null;
} else if( r.hoverData.which === 1 ){
triggerEvents( near, [ 'mouseup', 'tapend', 'vmouseup' ], e, { x: pos[0], y: pos[1] } );
if (
!r.dragData.didDrag && // didn't move a node around
!r.hoverData.dragged && // didn't pan
!r.hoverData.selecting && // not box selection
!r.hoverData.isOverThresholdDrag // didn't move too much
) {
triggerEvents(down, ["click", "tap", "vclick"], e, { x: pos[0], y: pos[1] });
didDoubleClick = false;
if (e.timeStamp - prevClickTimeStamp <= cy.multiClickDebounceTime()) {
clickTimeout && clearTimeout(clickTimeout);
didDoubleClick = true;
prevClickTimeStamp = null;
triggerEvents(down, ["dblclick", "dbltap", "vdblclick"], e, { x: pos[0], y: pos[1] });
} else {
clickTimeout = setTimeout(() => {
if (didDoubleClick) return;
triggerEvents(down, ["oneclick", "onetap", "voneclick"], e, { x: pos[0], y: pos[1] });
}, cy.multiClickDebounceTime());
prevClickTimeStamp = e.timeStamp;
}
}
// Deselect all elements if nothing is currently under the mouse cursor and we aren't dragging something
if( (down == null) // not mousedown on node
&& !r.dragData.didDrag // didn't move the node around
&& !r.hoverData.selecting // not box selection
&& !r.hoverData.dragged // didn't pan
&& !isMultSelKeyDown( e )
){
cy.$(isSelected).unselect(['tapunselect']);
if( draggedElements.length > 0 ){
r.redrawHint( 'eles', true );
}
r.dragData.possibleDragElements = draggedElements = cy.collection();
}
// Single selection
if( near == down && !r.dragData.didDrag && !r.hoverData.selecting ){
if( near != null && near._private.selectable ){
if( r.hoverData.dragging ){
// if panning, don't change selection state
} else if( cy.selectionType() === 'additive' || multSelKeyDown ){
if( near.selected() ){
near.unselect(['tapunselect']);
} else {
near.select(['tapselect']);
}
} else {
if( !multSelKeyDown ){
cy.$(isSelected).unmerge( near ).unselect(['tapunselect']);
near.select(['tapselect']);
}
}
r.redrawHint( 'eles', true );
}
}
if( r.hoverData.selecting ){
var box = cy.collection( r.getAllInBox( select[0], select[1], select[2], select[3] ) );
r.redrawHint( 'select', true );
if( box.length > 0 ){
r.redrawHint( 'eles', true );
}
cy.emit({
type: 'boxend',
originalEvent: e,
position: { x: pos[0], y: pos[1] }
});
var eleWouldBeSelected = function( ele ){ return ele.selectable() && !ele.selected(); };
if( cy.selectionType() === 'additive' ){
box
.emit('box')
.stdFilter( eleWouldBeSelected )
.select()
.emit('boxselect')
;
} else {
if( !multSelKeyDown ){
cy.$(isSelected).unmerge(box).unselect();
}
box
.emit('box')
.stdFilter( eleWouldBeSelected )
.select()
.emit('boxselect')
;
}
// always need redraw in case eles unselectable
r.redraw();
}
// Cancel drag pan
if( r.hoverData.dragging ){
r.hoverData.dragging = false;
r.redrawHint( 'select', true );
r.redrawHint( 'eles', true );
r.redraw();
}
if( !select[4] ) {
r.redrawHint('drag', true);
r.redrawHint('eles', true);
var downWasGrabbed = down && down.grabbed();
freeDraggedElements( draggedElements );
if( downWasGrabbed ){
down.emit('freeon');
draggedElements.emit('free');
if( r.dragData.didDrag ){
down.emit('dragfreeon');
draggedElements.emit('dragfree');
}
}
}
} // else not right mouse
select[4] = 0; r.hoverData.down = null;
r.hoverData.cxtStarted = false;
r.hoverData.draggingEles = false;
r.hoverData.selecting = false;
r.hoverData.isOverThresholdDrag = false;
r.dragData.didDrag = false;
r.hoverData.dragged = false;
r.hoverData.dragDelta = [];
r.hoverData.mdownPos = null;
r.hoverData.mdownGPos = null;
}, false );
var wheelHandler = function( e ){
if( r.scrollingPage ){ return; } // while scrolling, ignore wheel-to-zoom
var cy = r.cy;
var zoom = cy.zoom();
var pan = cy.pan();
var pos = r.projectIntoViewport( e.clientX, e.clientY );
var rpos = [ pos[0] * zoom + pan.x,
pos[1] * zoom + pan.y ];
if( r.hoverData.draggingEles || r.hoverData.dragging || r.hoverData.cxtStarted || inBoxSelection() ){ // if pan dragging or cxt dragging, wheel movements make no zoom
e.preventDefault();
return;
}
if( cy.panningEnabled() && cy.userPanningEnabled() && cy.zoomingEnabled() && cy.userZoomingEnabled() ){
e.preventDefault();
r.data.wheelZooming = true;
clearTimeout( r.data.wheelTimeout );
r.data.wheelTimeout = setTimeout( function(){
r.data.wheelZooming = false;
r.redrawHint( 'eles', true );
r.redraw();
}, 150 );
var diff;
if( e.deltaY != null ){
diff = e.deltaY / -250;
} else if( e.wheelDeltaY != null ){
diff = e.wheelDeltaY / 1000;
} else {
diff = e.wheelDelta / 1000;
}
diff = diff * r.wheelSensitivity;
var needsWheelFix = e.deltaMode === 1;
if( needsWheelFix ){ // fixes slow wheel events on ff/linux and ff/windows
diff *= 33;
}
var newZoom = cy.zoom() * Math.pow( 10, diff );
if( e.type === 'gesturechange' ){
newZoom = r.gestureStartZoom * e.scale;
}
cy.zoom( {
level: newZoom,
renderedPosition: { x: rpos[0], y: rpos[1] }
} );
cy.emit(e.type === 'gesturechange' ? 'pinchzoom' : 'scrollzoom');
}
};
// Functions to help with whether mouse wheel should trigger zooming
// --
r.registerBinding( r.container, 'wheel', wheelHandler, true );
// disable nonstandard wheel events
// r.registerBinding(r.container, 'mousewheel', wheelHandler, true);
// r.registerBinding(r.container, 'DOMMouseScroll', wheelHandler, true);
// r.registerBinding(r.container, 'MozMousePixelScroll', wheelHandler, true); // older firefox
r.registerBinding( window, 'scroll', function scrollHandler( e ){ // eslint-disable-line no-unused-vars
r.scrollingPage = true;
clearTimeout( r.scrollingPageTimeout );
r.scrollingPageTimeout = setTimeout( function(){
r.scrollingPage = false;
}, 250 );
}, true );
// desktop safari pinch to zoom start
r.registerBinding( r.container, 'gesturestart', function gestureStartHandler(e){
r.gestureStartZoom = r.cy.zoom();
if( !r.hasTouchStarted ){ // don't affect touch devices like iphone
e.preventDefault();
}
}, true );
r.registerBinding( r.container, 'gesturechange', function(e){
if( !r.hasTouchStarted ){ // don't affect touch devices like iphone
wheelHandler(e);
}
}, true );
// Functions to help with handling mouseout/mouseover on the Cytoscape container
// Handle mouseout on Cytoscape container
r.registerBinding( r.container, 'mouseout', function mouseOutHandler( e ){
var pos = r.projectIntoViewport( e.clientX, e.clientY );
r.cy.emit( ( {
originalEvent: e,
type: 'mouseout',
position: { x: pos[0], y: pos[1] }
} ) );
}, false );
r.registerBinding( r.container, 'mouseover', function mouseOverHandler( e ){
var pos = r.projectIntoViewport( e.clientX, e.clientY );
r.cy.emit( ( {
originalEvent: e,
type: 'mouseover',
position: { x: pos[0], y: pos[1] }
} ) );
}, false );
var f1x1, f1y1, f2x1, f2y1; // starting points for pinch-to-zoom
var distance1, distance1Sq; // initial distance between finger 1 and finger 2 for pinch-to-zoom
var center1, modelCenter1; // center point on start pinch to zoom
var offsetLeft, offsetTop;
var containerWidth, containerHeight;
var twoFingersStartInside;
var distance = function( x1, y1, x2, y2 ){
return Math.sqrt( (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) );
};
var distanceSq = function( x1, y1, x2, y2 ){
return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
};
var touchstartHandler;
r.registerBinding( r.container, 'touchstart', touchstartHandler = function( e ){
r.hasTouchStarted = true;
if( !eventInContainer(e) ){ return; }
blurActiveDomElement();
r.touchData.capture = true;
r.data.bgActivePosistion = undefined;
var cy = r.cy;
var now = r.touchData.now;
var earlier = r.touchData.earlier;
if( e.touches[0] ){ var pos = r.projectIntoViewport( e.touches[0].clientX, e.touches[0].clientY ); now[0] = pos[0]; now[1] = pos[1]; }
if( e.touches[1] ){ var pos = r.projectIntoViewport( e.touches[1].clientX, e.touches[1].clientY ); now[2] = pos[0]; now[3] = pos[1]; }
if( e.touches[2] ){ var pos = r.projectIntoViewport( e.touches[2].clientX, e.touches[2].clientY ); now[4] = pos[0]; now[5] = pos[1]; }
// record starting points for pinch-to-zoom
if( e.touches[1] ){
r.touchData.singleTouchMoved = true;
freeDraggedElements( r.dragData.touchDragEles );
var offsets = r.findContainerClientCoords();
offsetLeft = offsets[0];
offsetTop = offsets[1];
containerWidth = offsets[2];
containerHeight = offsets[3];
f1x1 = e.touches[0].clientX - offsetLeft;
f1y1 = e.touches[0].clientY - offsetTop;
f2x1 = e.touches[1].clientX - offsetLeft;
f2y1 = e.touches[1].clientY - offsetTop;
twoFingersStartInside =
0 <= f1x1 && f1x1 <= containerWidth
&& 0 <= f2x1 && f2x1 <= containerWidth
&& 0 <= f1y1 && f1y1 <= containerHeight
&& 0 <= f2y1 && f2y1 <= containerHeight
;
var pan = cy.pan();
var zoom = cy.zoom();
distance1 = distance( f1x1, f1y1, f2x1, f2y1 );
distance1Sq = distanceSq( f1x1, f1y1, f2x1, f2y1 );
center1 = [ (f1x1 + f2x1) / 2, (f1y1 + f2y1) / 2 ];
modelCenter1 = [
(center1[0] - pan.x) / zoom,
(center1[1] - pan.y) / zoom
];
// consider context tap
var cxtDistThreshold = 200;
var cxtDistThresholdSq = cxtDistThreshold * cxtDistThreshold;
if( distance1Sq < cxtDistThresholdSq && !e.touches[2] ){
var near1 = r.findNearestElement( now[0], now[1], true, true );
var near2 = r.findNearestElement( now[2], now[3], true, true );
if( near1 && near1.isNode() ){
near1.activate().emit( ( {
originalEvent: e,
type: 'cxttapstart',
position: { x: now[0], y: now[1] }
} ) );
r.touchData.start = near1;
} else if( near2 && near2.isNode() ){
near2.activate().emit( ( {
originalEvent: e,
type: 'cxttapstart',
position: { x: now[0], y: now[1] }
} ) );
r.touchData.start = near2;
} else {
cy.emit( ( {
originalEvent: e,
type: 'cxttapstart',
position: { x: now[0], y: now[1] }
} ) );
}
if( r.touchData.start ){ r.touchData.start._private.grabbed = false; }
r.touchData.cxt = true;
r.touchData.cxtDragged = false;
r.data.bgActivePosistion = undefined;
r.redraw();
return;
}
}
if( e.touches[2] ){
// ignore
// safari on ios pans the page otherwise (normally you should be able to preventdefault on touchmove...)
if( cy.boxSelectionEnabled() ){
e.preventDefault();
}
} else if( e.touches[1] ){
// ignore
} else if( e.touches[0] ){
var nears = r.findNearestElements( now[0], now[1], true, true );
var near = nears[0];
if( near != null ){
near.activate();
r.touchData.start = near;
r.touchData.starts = nears;
if( r.nodeIsGrabbable( near ) ){
var draggedEles = r.dragData.touchDragEles = cy.collection();
var selectedNodes = null;
r.redrawHint( 'eles', true );
r.redrawHint( 'drag', true );
if( near.selected() ){
// reset drag elements, since near will be added again
selectedNodes = cy.$( function( ele ){
return ele.selected() && r.nodeIsGrabbable( ele );
} );
addNodesToDrag( selectedNodes, { addToList: draggedEles } );
} else {
addNodeToDrag( near, { addToList: draggedEles } );
}
setGrabTarget( near );
var makeEvent = function( type ){
return ( {
originalEvent: e,
type: type,
position: { x: now[0], y: now[1] }
} );
};
near.emit( makeEvent('grabon') );
if( selectedNodes ){
selectedNodes.forEach(function( n ){ n.emit( makeEvent('grab') ); });
} else {
near.emit( makeEvent('grab') );
}
}
}
triggerEvents( near, [ 'touchstart', 'tapstart', 'vmousedown' ], e, { x: now[0], y: now[1] } );
if( near == null ){
r.data.bgActivePosistion = {
x: pos[0],
y: pos[1]
};
r.redrawHint( 'select', true );
r.redraw();
}
// Tap, taphold
// -----
r.touchData.singleTouchMoved = false;
r.touchData.singleTouchStartTime = +new Date();
clearTimeout( r.touchData.tapholdTimeout );
r.touchData.tapholdTimeout = setTimeout( function(){
if(
r.touchData.singleTouchMoved === false
&& !r.pinching // if pinching, then taphold unselect shouldn't take effect
&& !r.touchData.selecting // box selection shouldn't allow taphold through
){
triggerEvents( r.touchData.start, [ 'taphold' ], e, { x: now[0], y: now[1] } );
}
}, r.tapholdDuration );
}
if( e.touches.length >= 1 ){
var sPos = r.touchData.startPosition = [];
for( var i = 0; i < now.length; i++ ){
sPos[i] = earlier[i] = now[i];
}
var touch0 = e.touches[0];
r.touchData.startGPosition = [ touch0.clientX, touch0.clientY ];
}
}, false );
var touchmoveHandler;
r.registerBinding(window, 'touchmove', touchmoveHandler = function(e) { // eslint-disable-line no-undef
var capture = r.touchData.capture;
if( !capture && !eventInContainer(e) ){ return; }
var select = r.selection;
var cy = r.cy;
var now = r.touchData.now;
var earlier = r.touchData.earlier;
var zoom = cy.zoom();
if( e.touches[0] ){ var pos = r.projectIntoViewport( e.touches[0].clientX, e.touches[0].clientY ); now[0] = pos[0]; now[1] = pos[1]; }
if( e.touches[1] ){ var pos = r.projectIntoViewport( e.touches[1].clientX, e.touches[1].clientY ); now[2] = pos[0]; now[3] = pos[1]; }
if( e.touches[2] ){ var pos = r.projectIntoViewport( e.touches[2].clientX, e.touches[2].clientY ); now[4] = pos[0]; now[5] = pos[1]; }
var startGPos = r.touchData.startGPosition;
var isOverThresholdDrag;
if( capture && e.touches[0] && startGPos ){
var disp = []; for (var j=0;j<now.length;j++) { disp[j] = now[j] - earlier[j]; }
var dx = e.touches[0].clientX - startGPos[0];
var dx2 = dx * dx;
var dy = e.touches[0].clientY - startGPos[1];
var dy2 = dy * dy;
var dist2 = dx2 + dy2;
isOverThresholdDrag = dist2 >= r.touchTapThreshold2;
}
// context swipe cancelling
if( capture && r.touchData.cxt ){
e.preventDefault();
var f1x2 = e.touches[0].clientX - offsetLeft, f1y2 = e.touches[0].clientY - offsetTop;
var f2x2 = e.touches[1].clientX - offsetLeft, f2y2 = e.touches[1].clientY - offsetTop;
// var distance2 = distance( f1x2, f1y2, f2x2, f2y2 );
var distance2Sq = distanceSq( f1x2, f1y2, f2x2, f2y2 );
var factorSq = distance2Sq / distance1Sq;
var distThreshold = 150;
var distThresholdSq = distThreshold * distThreshold;
var factorThreshold = 1.5;
var factorThresholdSq = factorThreshold * factorThreshold;
// cancel ctx gestures if the distance b/t the fingers increases
if( factorSq >= factorThresholdSq || distance2Sq >= distThresholdSq ){
r.touchData.cxt = false;
r.data.bgActivePosistion = undefined;
r.redrawHint( 'select', true );
var cxtEvt = ( {
originalEvent: e,
type: 'cxttapend',
position: { x: now[0], y: now[1] }
} );
if( r.touchData.start ){
r.touchData.start
.unactivate()
.emit( cxtEvt )
;
r.touchData.start = null;
} else {
cy.emit( cxtEvt );
}
}
}
// context swipe
if( capture && r.touchData.cxt ){
var cxtEvt = ( {
originalEvent: e,
type: 'cxtdrag',
position: { x: now[0], y: now[1] }
} );
r.data.bgActivePosistion = undefined;
r.redrawHint( 'select', true );
if( r.touchData.start ){
r.touchData.start.emit( cxtEvt );
} else {
cy.emit( cxtEvt );
}
if( r.touchData.start ){ r.touchData.start._private.grabbed = false; }
r.touchData.cxtDragged = true;
var near = r.findNearestElement( now[0], now[1], true, true );
if( !r.touchData.cxtOver || near !== r.touchData.cxtOver ){
if( r.touchData.cxtOver ){
r.touchData.cxtOver.emit( ( {
originalEvent: e,
type: 'cxtdragout',
position: { x: now[0], y: now[1] }
} ) );
}
r.touchData.cxtOver = near;
if( near ){
near.emit( ( {
originalEvent: e,
type: 'cxtdragover',
position: { x: now[0], y: now[1] }
} ) );
}
}
// box selection
} else if( capture && e.touches[2] && cy.boxSelectionEnabled() ){
e.preventDefault();
r.data.bgActivePosistion = undefined;
this.lastThreeTouch = +new Date();
if( !r.touchData.selecting ){
cy.emit({
originalEvent: e,
type: 'boxstart',
position: { x: now[0], y: now[1] }
});
}
r.touchData.selecting = true;
r.touchData.didSelect = true;
select[4] = 1;
if( !select || select.length === 0 || select[0] === undefined ){
select[0] = (now[0] + now[2] + now[4]) / 3;
select[1] = (now[1] + now[3] + now[5]) / 3;
select[2] = (now[0] + now[2] + now[4]) / 3 + 1;
select[3] = (now[1] + now[3] + now[5]) / 3 + 1;
} else {
select[2] = (now[0] + now[2] + now[4]) / 3;
select[3] = (now[1] + now[3] + now[5]) / 3;
}
r.redrawHint( 'select', true );
r.redraw();
// pinch to zoom
} else if(
capture && e.touches[1]
&& !r.touchData.didSelect // don't allow box selection to degrade to pinch-to-zoom
&& cy.zoomingEnabled() && cy.panningEnabled() && cy.userZoomingEnabled() && cy.userPanningEnabled()
){ // two fingers => pinch to zoom
e.preventDefault();
r.data.bgActivePosistion = undefined;
r.redrawHint( 'select', true );
var draggedEles = r.dragData.touchDragEles;
if( draggedEles ){
r.redrawHint( 'drag', true );
for( var i = 0; i < draggedEles.length; i++ ){
var de_p = draggedEles[i]._private;
de_p.grabbed = false;
de_p.rscratch.inDragLayer = false;
}
}
let start = r.touchData.start;
// (x2, y2) for fingers 1 and 2
var f1x2 = e.touches[0].clientX - offsetLeft, f1y2 = e.touches[0].clientY - offsetTop;
var f2x2 = e.touches[1].clientX - offsetLeft, f2y2 = e.touches[1].clientY - offsetTop;
var distance2 = distance( f1x2, f1y2, f2x2, f2y2 );
// var distance2Sq = distanceSq( f1x2, f1y2, f2x2, f2y2 );
// var factor = Math.sqrt( distance2Sq ) / Math.sqrt( distance1Sq );
var factor = distance2 / distance1;
if( twoFingersStartInside ){
// delta finger1
var df1x = f1x2 - f1x1;
var df1y = f1y2 - f1y1;
// delta finger 2
var df2x = f2x2 - f2x1;
var df2y = f2y2 - f2y1;
// translation is the normalised vector of the two fingers movement
// i.e. so pinching cancels out and moving together pans
var tx = (df1x + df2x) / 2;
var ty = (df1y + df2y) / 2;
// now calculate the zoom
var zoom1 = cy.zoom();
var zoom2 = zoom1 * factor;
var pan1 = cy.pan();
// the model center point converted to the current rendered pos
var ctrx = modelCenter1[0] * zoom1 + pan1.x;
var ctry = modelCenter1[1] * zoom1 + pan1.y;
var pan2 = {
x: -zoom2 / zoom1 * (ctrx - pan1.x - tx) + ctrx,
y: -zoom2 / zoom1 * (ctry - pan1.y - ty) + ctry
};
// remove dragged eles
if( start && start.active() ){
var draggedEles = r.dragData.touchDragEles;
freeDraggedElements( draggedEles );
r.redrawHint( 'drag', true );
r.redrawHint( 'eles', true );
start
.unactivate()
.emit('freeon')
;
draggedEles.emit('free');
if( r.dragData.didDrag ){
start.emit('dragfreeon');
draggedEles.emit('dragfree');
}
}
cy.viewport( {
zoom: zoom2,
pan: pan2,
cancelOnFailedZoom: true
} );
cy.emit('pinchzoom');
distance1 = distance2;
f1x1 = f1x2;
f1y1 = f1y2;
f2x1 = f2x2;
f2y1 = f2y2;
r.pinching = true;
}
// Re-project
if( e.touches[0] ){ var pos = r.projectIntoViewport( e.touches[0].clientX, e.touches[0].clientY ); now[0] = pos[0]; now[1] = pos[1]; }
if( e.touches[1] ){ var pos = r.projectIntoViewport( e.touches[1].clientX, e.touches[1].clientY ); now[2] = pos[0]; now[3] = pos[1]; }
if( e.touches[2] ){ var pos = r.projectIntoViewport( e.touches[2].clientX, e.touches[2].clientY ); now[4] = pos[0]; now[5] = pos[1]; }
} else if(
e.touches[0]
&& !r.touchData.didSelect // don't allow box selection to degrade to single finger events like panning
){
var start = r.touchData.start;
var last = r.touchData.last;
var near;
if( !r.hoverData.draggingEles && !r.swipePanning ){
near = r.findNearestElement( now[0], now[1], true, true );
}
if( capture && start != null ){
e.preventDefault();
}
// dragging nodes
if( capture && start != null && r.nodeIsDraggable( start ) ){
if( isOverThresholdDrag ){ // then dragging can happen
var draggedEles = r.dragData.touchDragEles;
var justStartedDrag = !r.dragData.didDrag;
if( justStartedDrag ){
addNodesToDrag( draggedEles , { inDragLayer: true } );
}
r.dragData.didDrag = true;
var totalShift = { x: 0, y: 0 };
if( is.number( disp[0] ) && is.number( disp[1] ) ){
totalShift.x += disp[0];
totalShift.y += disp[1];
if( justStartedDrag ){
r.redrawHint( 'eles', true );
var dragDelta = r.touchData.dragDelta;
if( dragDelta && is.number( dragDelta[0] ) && is.number( dragDelta[1] ) ){
totalShift.x += dragDelta[0];
totalShift.y += dragDelta[1];
}
}
}
r.hoverData.draggingEles = true;
( draggedEles
.silentShift( totalShift )
.emit('position drag')
);
r.redrawHint( 'drag', true );
if(
r.touchData.startPosition[0] == earlier[0]
&& r.touchData.startPosition[1] == earlier[1]
){
r.redrawHint( 'eles', true );
}
r.redraw();
} else { // otherise keep track of drag delta for later
var dragDelta = r.touchData.dragDelta = r.touchData.dragDelta || [];
if( dragDelta.length === 0 ){
dragDelta.push( disp[0] );
dragDelta.push( disp[1] );
} else {
dragDelta[0] += disp[0];
dragDelta[1] += disp[1];
}
}
}
// touchmove
{
triggerEvents( (start || near), [ 'touchmove', 'tapdrag', 'vmousemove' ], e, { x: now[0], y: now[1] } );
if( ( !start || !start.grabbed() ) && near != last ){
if( last ){ last.emit( ( { originalEvent: e, type: 'tapdragout', position: { x: now[0], y: now[1] } } ) ); }
if( near ){ near.emit( ( { originalEvent: e, type: 'tapdragover', position: { x: now[0], y: now[1] } } ) ); }
}
r.touchData.last = near;
}
// check to cancel taphold
if( capture ){
for( var i = 0; i < now.length; i++ ){
if( now[ i ]
&& r.touchData.startPosition[ i ]
&& isOverThresholdDrag ){
r.touchData.singleTouchMoved = true;
}
}
}
// panning
if(
capture
&& ( start == null || start.pannable() )
&& cy.panningEnabled() && cy.userPanningEnabled()
){
var allowPassthrough = allowPanningPassthrough( start, r.touchData.starts );
if( allowPassthrough ){
e.preventDefault();
if( !r.data.bgActivePosistion ){
r.data.bgActivePosistion = math.array2point( r.touchData.startPosition );
}
if( r.swipePanning ){
cy.panBy( {
x: disp[0] * zoom,
y: disp[1] * zoom
} );
cy.emit('dragpan');
} else if( isOverThresholdDrag ){
r.swipePanning = true;
cy.panBy( {
x: dx * zoom,
y: dy * zoom
} );
cy.emit('dragpan');
if( start ){
start.unactivate();
r.redrawHint( 'select', true );
r.touchData.start = null;
}
}
}
// Re-project
var pos = r.projectIntoViewport( e.touches[0].clientX, e.touches[0].clientY );
now[0] = pos[0]; now[1] = pos[1];
}
}
for( var j = 0; j < now.length; j++ ){ earlier[ j ] = now[ j ]; }
// the active bg indicator should be removed w