vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

489 lines
14 KiB

/**
* Created by Alex on 2/27/2015.
*
*/
var util = require('../../util');
import { NavigationHandler } from "./components/NavigationHandler"
class InteractionHandler {
constructor(body, canvas, selectionHandler) {
this.body = body;
this.canvas = canvas;
this.selectionHandler = selectionHandler;
this.navigationHandler = new NavigationHandler(body,canvas);
// bind the events from hammer to functions in this object
this.body.eventListeners.onTap = this.onTap.bind(this);
this.body.eventListeners.onTouch = this.onTouch.bind(this);
this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this);
this.body.eventListeners.onHold = this.onHold.bind(this);
this.body.eventListeners.onDragStart = this.onDragStart.bind(this);
this.body.eventListeners.onDrag = this.onDrag.bind(this);
this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this);
this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this);
this.body.eventListeners.onPinch = this.onPinch.bind(this);
this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this);
this.body.eventListeners.onRelease = this.onRelease.bind(this);
this.touchTime = 0;
this.drag = {};
this.pinch = {};
this.pointerPosition = {x:0,y:0};
this.hoverObj = {nodes:{},edges:{}};
this.options = {};
this.defaultOptions = {
dragNodes:true,
dragView: true,
zoomView: true,
hoverEnabled: false,
showNavigationIcons: true,
tooltip: {
delay: 300,
fontColor: 'black',
fontSize: 14, // px
fontFace: 'verdana',
color: {
border: '#666',
background: '#FFFFC6'
}
},
keyboard: {
enabled: true,
speed: {x: 10, y: 10, zoom: 0.02},
bindToWindow: true
}
}
util.extend(this.options,this.defaultOptions);
}
setOptions(options) {
if (options !== undefined) {
// extend all but the values in fields
var fields = ['keyboard'];
util.selectiveNotDeepExtend(fields,this.options, options);
// merge the keyboard options in.
util.mergeOptions(this.options, options,'keyboard');
}
this.navigationHandler.setOptions(this.options);
}
/**
* Get the pointer location from a touch location
* @param {{x: Number, y: Number}} touch
* @return {{x: Number, y: Number}} pointer
* @private
*/
getPointer(touch) {
return {
x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas),
y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas)
};
}
/**
* On start of a touch gesture, store the pointer
* @param event
* @private
*/
onTouch(event) {
if (new Date().valueOf() - this.touchTime > 100) {
this.drag.pointer = this.getPointer(event.center);
this.drag.pinched = false;
this.pinch.scale = this.body.view.scale;
// to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
this.touchTime = new Date().valueOf();
}
}
/**
* handle tap/click event: select/unselect a node
* @private
*/
onTap(event) {
var pointer = this.getPointer(event.center);
var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0;
var selected = this.selectionHandler.selectOnPoint(pointer);
if (selected === true || (previouslySelected == true && selected === false)) { // select or unselect
this.body.emitter.emit('selected', this.selectionHandler.getSelection());
}
this.selectionHandler._generateClickEvent("click",pointer);
}
/**
* handle doubletap event
* @private
*/
onDoubleTap(event) {
var pointer = this.getPointer(event.center);
this.selectionHandler._generateClickEvent("doubleClick",pointer);
}
/**
* handle long tap event: multi select nodes
* @private
*/
onHold(event) {
var pointer = this.getPointer(event.center);
var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer);
if (selectionChanged === true) { // select or longpress
this.body.emitter.emit('selected', this.selectionHandler.getSelection());
}
this.selectionHandler._generateClickEvent("click",pointer);
}
/**
* handle the release of the screen
*
* @private
*/
onRelease(event) {
this.body.emitter.emit("release",event)
}
/**
* This function is called by onDragStart.
* It is separated out because we can then overload it for the datamanipulation system.
*
* @private
*/
onDragStart(event) {
//in case the touch event was triggered on an external div, do the initial touch now.
if (this.drag.pointer === undefined) {
this.onTouch(event);
}
var node = this.selectionHandler.getNodeAt(this.drag.pointer);
// note: drag.pointer is set in onTouch to get the initial touch location
this.drag.dragging = true;
this.drag.selection = [];
this.drag.translation = util.extend({},this.body.view.translation); // copy the object
this.drag.nodeId = null;
this.body.emitter.emit("dragStart", {nodeIds: this.selectionHandler.getSelection().nodes});
if (node != null && this.options.dragNodes === true) {
this.drag.nodeId = node.id;
// select the clicked node if not yet selected
if (node.isSelected() === false) {
this.selectionHandler.unselectAll();
this.selectionHandler.selectObject(node);
}
var selection = this.selectionHandler.selectionObj.nodes;
// create an array with the selected nodes and their original location and status
for (let nodeId in selection) {
if (selection.hasOwnProperty(nodeId)) {
var object = selection[nodeId];
var s = {
id: object.id,
node: object,
// store original x, y, xFixed and yFixed, make the node temporarily Fixed
x: object.x,
y: object.y,
xFixed: object.xFixed,
yFixed: object.yFixed
};
object.xFixed = true;
object.yFixed = true;
this.drag.selection.push(s);
}
}
}
}
/**
* handle drag event
* @private
*/
onDrag(event) {
if (this.drag.pinched === true) {
return;
}
// remove the focus on node if it is focussed on by the focusOnNode
this.body.emitter.emit("unlockNode");
var pointer = this.getPointer(event.center);
var selection = this.drag.selection;
if (selection && selection.length && this.options.dragNodes === true) {
// calculate delta's and new location
var deltaX = pointer.x - this.drag.pointer.x;
var deltaY = pointer.y - this.drag.pointer.y;
// update position of all selected nodes
selection.forEach((selection) => {
var node = selection.node;
if (!selection.xFixed) {
node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX);
}
if (!selection.yFixed) {
node.y = this.canvas._YconvertDOMtoCanvas(this.canvas._YconvertCanvasToDOM(selection.y) + deltaY);
}
});
// start the simulation of the physics
this.body.emitter.emit("startSimulation");
}
else {
// move the network
if (this.options.dragView === true) {
// if the drag was not started properly because the click started outside the network div, start it now.
if (this.drag.pointer === undefined) {
this._handleDragStart(event);
return;
}
var diffX = pointer.x - this.drag.pointer.x;
var diffY = pointer.y - this.drag.pointer.y;
this.body.view.translation = {x:this.drag.translation.x + diffX, y:this.drag.translation.y + diffY};
this.body.emitter.emit("_redraw");
}
}
}
/**
* handle drag start event
* @private
*/
onDragEnd(event) {
this.drag.dragging = false;
var selection = this.drag.selection;
if (selection && selection.length) {
selection.forEach(function (s) {
// restore original xFixed and yFixed
s.node.xFixed = s.xFixed;
s.node.yFixed = s.yFixed;
});
this.body.emitter.emit("startSimulation");
}
else {
this.body.emitter.emit("_requestRedraw");
}
this.body.emitter.emit("dragEnd", {nodeIds: this.selectionHandler.getSelection().nodes});
}
/**
* Handle pinch event
* @param event
* @private
*/
onPinch(event) {
var pointer = this.getPointer(event.center);
this.drag.pinched = true;
if (this.pinch['scale'] === undefined) {
this.pinch.scale = 1;
}
// TODO: enabled moving while pinching?
var scale = this.pinch.scale * event.scale;
this.zoom(scale, pointer)
}
/**
* Zoom the network in or out
* @param {Number} scale a number around 1, and between 0.01 and 10
* @param {{x: Number, y: Number}} pointer Position on screen
* @return {Number} appliedScale scale is limited within the boundaries
* @private
*/
zoom(scale, pointer) {
if (this.options.zoomView === true) {
var scaleOld = this.body.view.scale;
if (scale < 0.00001) {
scale = 0.00001;
}
if (scale > 10) {
scale = 10;
}
var preScaleDragPointer = null;
if (this.drag !== undefined) {
if (this.drag.dragging === true) {
preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer);
}
}
// + this.canvas.frame.canvas.clientHeight / 2
var translation = this.body.view.translation;
var scaleFrac = scale / scaleOld;
var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
this.body.view.scale = scale;
this.body.view.translation = {x:tx, y:ty};
if (preScaleDragPointer != null) {
var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer);
this.drag.pointer.x = postScaleDragPointer.x;
this.drag.pointer.y = postScaleDragPointer.y;
}
this.body.emitter.emit("_requestRedraw");
if (scaleOld < scale) {
this.body.emitter.emit("zoom", {direction: "+"});
}
else {
this.body.emitter.emit("zoom", {direction: "-"});
}
}
}
/**
* Event handler for mouse wheel event, used to zoom the timeline
* See http://adomas.org/javascript-mouse-wheel/
* https://github.com/EightMedia/hammer.js/issues/256
* @param {MouseEvent} event
* @private
*/
onMouseWheel(event) {
// retrieve delta
var delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
delta = event.wheelDelta / 120;
} else if (event.detail) { /* Mozilla case. */
// In Mozilla, sign of delta is different than in IE.
// Also, delta is multiple of 3.
delta = -event.detail / 3;
}
// If delta is nonzero, handle it.
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta) {
// calculate the new scale
var scale = this.body.view.scale;
var zoom = delta / 10;
if (delta < 0) {
zoom = zoom / (1 - zoom);
}
scale *= (1 + zoom);
// calculate the pointer location
var pointer = {x:event.pageX, y:event.pageY};
// apply the new scale
this.zoom(scale, pointer);
}
// Prevent default actions caused by mouse wheel.
event.preventDefault();
}
/**
* Mouse move handler for checking whether the title moves over a node with a title.
* @param {Event} event
* @private
*/
onMouseMove(event) {
// var pointer = {x:event.pageX, y:event.pageY};
// var popupVisible = false;
//
// // check if the previously selected node is still selected
// if (this.popup !== undefined) {
// if (this.popup.hidden === false) {
// this._checkHidePopup(pointer);
// }
//
// // if the popup was not hidden above
// if (this.popup.hidden === false) {
// popupVisible = true;
// this.popup.setPosition(pointer.x + 3, pointer.y - 5)
// this.popup.show();
// }
// }
//
// // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over
// if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) {
// this.canvas.frame.focus();
// }
//
// // start a timeout that will check if the mouse is positioned above an element
// if (popupVisible === false) {
// var me = this;
// var checkShow = function() {
// me._checkShowPopup(pointer);
// };
//
// if (this.popupTimer) {
// clearInterval(this.popupTimer); // stop any running calculationTimer
// }
// if (!this.drag.dragging) {
// this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay);
// }
// }
//
// /**
// * Adding hover highlights
// */
// if (this.options.hoverEnabled === true) {
// // removing all hover highlights
// for (var edgeId in this.hoverObj.edges) {
// if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
// this.hoverObj.edges[edgeId].hover = false;
// delete this.hoverObj.edges[edgeId];
// }
// }
//
// // adding hover highlights
// var obj = this.selectionHandler.getNodeAt(pointer);
// if (obj == null) {
// obj = this.selectionHandler.getEdgeAt(pointer);
// }
// if (obj != null) {
// this._hoverObject(obj);
// }
//
// // removing all node hover highlights except for the selected one.
// for (var nodeId in this.hoverObj.nodes) {
// if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
// if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
// this._blurObject(this.hoverObj.nodes[nodeId]);
// delete this.hoverObj.nodes[nodeId];
// }
// }
// }
// this.body.emitter.emit("_requestRedraw");
// }
}
}
export {InteractionHandler};