/** * Created by Alex on 2/27/2015. * */ let util = require('../../util'); import NavigationHandler from "./components/NavigationHandler" import Popup from "./components/Popup" 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.hoverObj = {nodes:{},edges:{}}; this.popup = undefined; this.popupObj = undefined; this.popupTimer = undefined; this.body.functions.getPointer = this.getPointer.bind(this); this.options = {}; this.defaultOptions = { dragNodes:true, dragView: true, zoomView: true, hoverEnabled: false, showNavigationIcons: false, tooltip: { delay: 300, fontColor: '#000000', fontSize: 14, // px fontFace: 'verdana', color: { border: '#666666', background: '#FFFFC6' } }, keyboard: { enabled: false, 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 let fields = ['keyboard','tooltip']; util.selectiveNotDeepExtend(fields,this.options, options); // merge the keyboard options in. util.mergeOptions(this.options, options, 'keyboard'); if (options.tooltip) { util.extend(this.options.tooltip, options.tooltip); if (options.tooltip.color) { this.options.tooltip.color = util.parseColor(options.tooltip.color); } } } 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) { let pointer = this.getPointer(event.center); let previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; let selected = this.selectionHandler.selectOnPoint(pointer); if (selected === true || (previouslySelected == true && selected === false)) { // select or unselect this.body.emitter.emit('select', this.selectionHandler.getSelection()); } this.selectionHandler._generateClickEvent("click",pointer); } /** * handle doubletap event * @private */ onDoubleTap(event) { let pointer = this.getPointer(event.center); this.selectionHandler._generateClickEvent("doubleClick",pointer); } /** * handle long tap event: multi select nodes * @private */ onHold(event) { let pointer = this.getPointer(event.center); let selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); if (selectionChanged === true) { // select or longpress this.body.emitter.emit('select', 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); } // note: drag.pointer is set in onTouch to get the initial touch location let node = this.selectionHandler.getNodeAt(this.drag.pointer); this.drag.dragging = true; this.drag.selection = []; this.drag.translation = util.extend({},this.body.view.translation); // copy the object this.drag.nodeId = undefined; this.body.emitter.emit("dragStart", {nodeIds: this.selectionHandler.getSelection().nodes}); if (node !== undefined && 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); } let 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)) { let object = selection[nodeId]; let 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.options.fixed.x, yFixed: object.options.fixed.y }; object.options.fixed.x = true; object.options.fixed.y = 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"); let pointer = this.getPointer(event.center); let selection = this.drag.selection; if (selection && selection.length && this.options.dragNodes === true) { // calculate delta's and new location let deltaX = pointer.x - this.drag.pointer.x; let deltaY = pointer.y - this.drag.pointer.y; // update position of all selected nodes selection.forEach((selection) => { let node = selection.node; // only move the node if it was not fixed initially if (selection.xFixed === false) { node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); } // only move the node if it was not fixed initially if (selection.yFixed === false) { 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; } let diffX = pointer.x - this.drag.pointer.x; let 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; let selection = this.drag.selection; if (selection && selection.length) { selection.forEach(function (s) { // restore original xFixed and yFixed s.node.options.fixed.x = s.xFixed; s.node.options.fixed.y = 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) { let pointer = this.getPointer(event.center); this.drag.pinched = true; if (this.pinch['scale'] === undefined) { this.pinch.scale = 1; } // TODO: enabled moving while pinching? let 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) { let scaleOld = this.body.view.scale; if (scale < 0.00001) { scale = 0.00001; } if (scale > 10) { scale = 10; } let preScaleDragPointer = undefined; if (this.drag !== undefined) { if (this.drag.dragging === true) { preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); } } // + this.canvas.frame.canvas.clientHeight / 2 let translation = this.body.view.translation; let scaleFrac = scale / scaleOld; let tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; let ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; this.body.view.scale = scale; this.body.view.translation = {x:tx, y:ty}; if (preScaleDragPointer != undefined) { let 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 let 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 !== 0) { // calculate the new scale let scale = this.body.view.scale; let zoom = delta / 10; if (delta < 0) { zoom = zoom / (1 - zoom); } scale *= (1 + zoom); // calculate the pointer location let pointer = this.getPointer({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) { let pointer = this.getPointer({x:event.pageX, y:event.pageY}); let 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) { if (this.popupTimer !== undefined) { clearInterval(this.popupTimer); // stop any running calculationTimer this.popupTimer = undefined; } if (!this.drag.dragging) { this.popupTimer = setTimeout(() => this._checkShowPopup(pointer), this.options.tooltip.delay); } } /** * Adding hover highlights */ if (this.options.hoverEnabled === true) { // removing all hover highlights for (let 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 let obj = this.selectionHandler.getNodeAt(pointer); if (obj == undefined) { obj = this.selectionHandler.getEdgeAt(pointer); } if (obj != undefined) { this.selectionHandler.hoverObject(obj); } // removing all node hover highlights except for the selected one. for (let nodeId in this.hoverObj.nodes) { if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == undefined) { this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); delete this.hoverObj.nodes[nodeId]; } } } this.body.emitter.emit("_requestRedraw"); } } /** * Check if there is an element on the given position in the network * (a node or edge). If so, and if this element has a title, * show a popup window with its title. * * @param {{x:Number, y:Number}} pointer * @private */ _checkShowPopup(pointer) { let x = this.canvas._XconvertDOMtoCanvas(pointer.x); let y = this.canvas._YconvertDOMtoCanvas(pointer.y); let pointerObj = { left: x, top: y, right: x, bottom: y }; let previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; let nodeUnderCursor = false; let popupType = "node"; // check if a node is under the cursor. if (this.popupObj === undefined) { // search the nodes for overlap, select the top one in case of multiple nodes let nodeIndices = this.body.nodeIndices; let nodes = this.body.nodes; let node; let overlappingNodes = []; for (let i = 0; i < nodeIndices.length; i++) { node = nodes[nodeIndices[i]]; if (node.isOverlappingWith(pointerObj) === true) { if (node.getTitle() !== undefined) { overlappingNodes.push(nodeIndices[i]); } } } if (overlappingNodes.length > 0) { // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; // if you hover over a node, the title of the edge is not supposed to be shown. nodeUnderCursor = true; } } if (this.popupObj === undefined && nodeUnderCursor == false) { // search the edges for overlap let edgeIndices = this.body.edgeIndices; let edges = this.body.edges; let edge; let overlappingEdges = []; for (let i = 0; i < edgeIndices.length; i++) { edge = edges[edgeIndices[i]]; if (edge.isOverlappingWith(pointerObj) === true) { if (edge.connected === true && edge.getTitle() !== undefined) { overlappingEdges.push(edgeIndices[i]); } } } if (overlappingEdges.length > 0) { this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; popupType = "edge"; } } if (this.popupObj !== undefined) { // show popup message window if (this.popupObj.id != previousPopupObjId) { if (this.popup === undefined) { this.popup = new Popup(this.frame, this.options.tooltip); } this.popup.popupTargetType = popupType; this.popup.popupTargetId = this.popupObj.id; // adjust a small offset such that the mouse cursor is located in the // bottom left location of the popup, and you can easily move over the // popup area this.popup.setPosition(pointer.x + 3, pointer.y - 5); this.popup.setText(this.popupObj.getTitle()); this.popup.show(); } } else { if (this.popup) { this.popup.hide(); } } } /** * Check if the popup must be hidden, which is the case when the mouse is no * longer hovering on the object * @param {{x:Number, y:Number}} pointer * @private */ _checkHidePopup(pointer) { let x = this.canvas._XconvertDOMtoCanvas(pointer.x); let y = this.canvas._YconvertDOMtoCanvas(pointer.y); let pointerObj = { left: x, top: y, right: x, bottom: y }; let stillOnObj = false; if (this.popup.popupTargetType == 'node') { if (this.body.nodes[this.popup.popupTargetId] !== undefined) { stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. // we initially only check stillOnObj because this is much faster. if (stillOnObj === true) { let overNode = this.selectionHandler.getNodeAt(pointer); stillOnObj = overNode.id == this.popup.popupTargetId; } } } else { if (this.selectionHandler.getNodeAt(pointer) === undefined) { if (this.body.edges[this.popup.popupTargetId] !== undefined) { stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); } } } if (stillOnObj === false) { this.popupObj = undefined; this.popup.hide(); } } } export default InteractionHandler;