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.

661 lines
20 KiB

  1. let util = require('../../util');
  2. import NavigationHandler from './components/NavigationHandler'
  3. import Popup from './components/Popup'
  4. class InteractionHandler {
  5. constructor(body, canvas, selectionHandler) {
  6. this.body = body;
  7. this.canvas = canvas;
  8. this.selectionHandler = selectionHandler;
  9. this.navigationHandler = new NavigationHandler(body,canvas);
  10. // bind the events from hammer to functions in this object
  11. this.body.eventListeners.onTap = this.onTap.bind(this);
  12. this.body.eventListeners.onTouch = this.onTouch.bind(this);
  13. this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this);
  14. this.body.eventListeners.onHold = this.onHold.bind(this);
  15. this.body.eventListeners.onDragStart = this.onDragStart.bind(this);
  16. this.body.eventListeners.onDrag = this.onDrag.bind(this);
  17. this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this);
  18. this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this);
  19. this.body.eventListeners.onPinch = this.onPinch.bind(this);
  20. this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this);
  21. this.body.eventListeners.onRelease = this.onRelease.bind(this);
  22. this.body.eventListeners.onContext = this.onContext.bind(this);
  23. this.touchTime = 0;
  24. this.drag = {};
  25. this.pinch = {};
  26. this.hoverObj = {nodes:{},edges:{}};
  27. this.popup = undefined;
  28. this.popupObj = undefined;
  29. this.popupTimer = undefined;
  30. this.body.functions.getPointer = this.getPointer.bind(this);
  31. this.options = {};
  32. this.defaultOptions = {
  33. dragNodes:true,
  34. dragView: true,
  35. zoomView: true,
  36. hoverEnabled: false,
  37. navigationButtons: false,
  38. tooltipDelay: 300,
  39. keyboard: {
  40. enabled: false,
  41. speed: {x: 10, y: 10, zoom: 0.02},
  42. bindToWindow: true
  43. }
  44. }
  45. util.extend(this.options,this.defaultOptions);
  46. }
  47. setOptions(options) {
  48. if (options !== undefined) {
  49. // extend all but the values in fields
  50. let fields = ['keyboard'];
  51. util.selectiveNotDeepExtend(fields,this.options, options);
  52. // merge the keyboard options in.
  53. util.mergeOptions(this.options, options, 'keyboard');
  54. if (options.tooltip) {
  55. util.extend(this.options.tooltip, options.tooltip);
  56. if (options.tooltip.color) {
  57. this.options.tooltip.color = util.parseColor(options.tooltip.color);
  58. }
  59. }
  60. }
  61. this.navigationHandler.setOptions(this.options);
  62. }
  63. /**
  64. * Get the pointer location from a touch location
  65. * @param {{x: Number, y: Number}} touch
  66. * @return {{x: Number, y: Number}} pointer
  67. * @private
  68. */
  69. getPointer(touch) {
  70. return {
  71. x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas),
  72. y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas)
  73. };
  74. }
  75. /**
  76. * On start of a touch gesture, store the pointer
  77. * @param event
  78. * @private
  79. */
  80. onTouch(event) {
  81. if (new Date().valueOf() - this.touchTime > 50) {
  82. this.drag.pointer = this.getPointer(event.center);
  83. this.drag.pinched = false;
  84. this.pinch.scale = this.body.view.scale;
  85. // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
  86. this.touchTime = new Date().valueOf();
  87. }
  88. }
  89. /**
  90. * handle tap/click event: select/unselect a node
  91. * @private
  92. */
  93. onTap(event) {
  94. let pointer = this.getPointer(event.center);
  95. this.checkSelectionChanges(pointer);
  96. this.selectionHandler._generateClickEvent('click',pointer);
  97. }
  98. /**
  99. * handle doubletap event
  100. * @private
  101. */
  102. onDoubleTap(event) {
  103. let pointer = this.getPointer(event.center);
  104. this.selectionHandler._generateClickEvent('doubleClick',pointer);
  105. }
  106. /**
  107. * handle long tap event: multi select nodes
  108. * @private
  109. */
  110. onHold(event) {
  111. let pointer = this.getPointer(event.center);
  112. this.checkSelectionChanges(pointer, true);
  113. this.selectionHandler._generateClickEvent('click',pointer);
  114. this.selectionHandler._generateClickEvent('hold',pointer);
  115. }
  116. /**
  117. * handle the release of the screen
  118. *
  119. * @private
  120. */
  121. onRelease(event) {
  122. if (new Date().valueOf() - this.touchTime > 10) {
  123. let pointer = this.getPointer(event.center);
  124. this.selectionHandler._generateClickEvent('release',pointer);
  125. // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
  126. this.touchTime = new Date().valueOf();
  127. }
  128. }
  129. onContext(event) {
  130. let pointer = this.getPointer({x:event.pageX, y:event.pageY});
  131. this.selectionHandler._generateClickEvent('rightClick',pointer);
  132. }
  133. /**
  134. *
  135. * @param pointer
  136. * @param add
  137. */
  138. checkSelectionChanges(pointer, add = false) {
  139. let previouslySelectedEdgeCount = this.selectionHandler._getSelectedEdgeCount();
  140. let previouslySelectedNodeCount = this.selectionHandler._getSelectedNodeCount();
  141. let previousSelection = this.selectionHandler.getSelection();
  142. let selected;
  143. if (add === true) {
  144. selected = this.selectionHandler.selectAdditionalOnPoint(pointer);
  145. }
  146. else {
  147. selected = this.selectionHandler.selectOnPoint(pointer);
  148. }
  149. let selectedEdges = this.selectionHandler._getSelectedEdgeCount();
  150. let selectedNodes = this.selectionHandler._getSelectedNodeCount();
  151. if (selectedNodes - previouslySelectedNodeCount > 0) { // node was selected
  152. this.selectionHandler._generateClickEvent('selectNode',pointer);
  153. selected = true;
  154. }
  155. else if (selectedNodes - previouslySelectedNodeCount < 0) { // node was deselected
  156. this.selectionHandler._generateClickEvent('deselectNode',pointer, previousSelection);
  157. selected = true;
  158. }
  159. if (selectedEdges - previouslySelectedEdgeCount > 0) { // node was selected
  160. this.selectionHandler._generateClickEvent('selectEdge',pointer);
  161. selected = true;
  162. }
  163. else if (selectedEdges - previouslySelectedEdgeCount < 0) { // node was deselected
  164. this.selectionHandler._generateClickEvent('deselectEdge',pointer, previousSelection);
  165. selected = true;
  166. }
  167. if (selected === true) { // select or unselect
  168. this.selectionHandler._generateClickEvent('select',pointer);
  169. }
  170. }
  171. /**
  172. * This function is called by onDragStart.
  173. * It is separated out because we can then overload it for the datamanipulation system.
  174. *
  175. * @private
  176. */
  177. onDragStart(event) {
  178. //in case the touch event was triggered on an external div, do the initial touch now.
  179. if (this.drag.pointer === undefined) {
  180. this.onTouch(event);
  181. }
  182. // note: drag.pointer is set in onTouch to get the initial touch location
  183. let node = this.selectionHandler.getNodeAt(this.drag.pointer);
  184. this.drag.dragging = true;
  185. this.drag.selection = [];
  186. this.drag.translation = util.extend({},this.body.view.translation); // copy the object
  187. this.drag.nodeId = undefined;
  188. this.selectionHandler._generateClickEvent('dragStart',this.drag.pointer);
  189. if (node !== undefined && this.options.dragNodes === true) {
  190. this.drag.nodeId = node.id;
  191. // select the clicked node if not yet selected
  192. if (node.isSelected() === false) {
  193. this.selectionHandler.unselectAll();
  194. this.selectionHandler.selectObject(node);
  195. }
  196. let selection = this.selectionHandler.selectionObj.nodes;
  197. // create an array with the selected nodes and their original location and status
  198. for (let nodeId in selection) {
  199. if (selection.hasOwnProperty(nodeId)) {
  200. let object = selection[nodeId];
  201. let s = {
  202. id: object.id,
  203. node: object,
  204. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  205. x: object.x,
  206. y: object.y,
  207. xFixed: object.options.fixed.x,
  208. yFixed: object.options.fixed.y
  209. };
  210. object.options.fixed.x = true;
  211. object.options.fixed.y = true;
  212. this.drag.selection.push(s);
  213. }
  214. }
  215. }
  216. }
  217. /**
  218. * handle drag event
  219. * @private
  220. */
  221. onDrag(event) {
  222. if (this.drag.pinched === true) {
  223. return;
  224. }
  225. // remove the focus on node if it is focussed on by the focusOnNode
  226. this.body.emitter.emit('unlockNode');
  227. let pointer = this.getPointer(event.center);
  228. let selection = this.drag.selection;
  229. if (selection && selection.length && this.options.dragNodes === true) {
  230. // calculate delta's and new location
  231. let deltaX = pointer.x - this.drag.pointer.x;
  232. let deltaY = pointer.y - this.drag.pointer.y;
  233. // update position of all selected nodes
  234. selection.forEach((selection) => {
  235. let node = selection.node;
  236. // only move the node if it was not fixed initially
  237. if (selection.xFixed === false) {
  238. node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX);
  239. }
  240. // only move the node if it was not fixed initially
  241. if (selection.yFixed === false) {
  242. node.y = this.canvas._YconvertDOMtoCanvas(this.canvas._YconvertCanvasToDOM(selection.y) + deltaY);
  243. }
  244. });
  245. // start the simulation of the physics
  246. this.body.emitter.emit('startSimulation');
  247. }
  248. else {
  249. // move the network
  250. if (this.options.dragView === true) {
  251. // if the drag was not started properly because the click started outside the network div, start it now.
  252. if (this.drag.pointer === undefined) {
  253. this._handleDragStart(event);
  254. return;
  255. }
  256. let diffX = pointer.x - this.drag.pointer.x;
  257. let diffY = pointer.y - this.drag.pointer.y;
  258. this.body.view.translation = {x:this.drag.translation.x + diffX, y:this.drag.translation.y + diffY};
  259. this.body.emitter.emit('_redraw');
  260. }
  261. }
  262. }
  263. /**
  264. * handle drag start event
  265. * @private
  266. */
  267. onDragEnd(event) {
  268. this.drag.dragging = false;
  269. let selection = this.drag.selection;
  270. if (selection && selection.length) {
  271. selection.forEach(function (s) {
  272. // restore original xFixed and yFixed
  273. s.node.options.fixed.x = s.xFixed;
  274. s.node.options.fixed.y = s.yFixed;
  275. });
  276. this.body.emitter.emit('startSimulation');
  277. }
  278. else {
  279. this.body.emitter.emit('_requestRedraw');
  280. }
  281. this.selectionHandler._generateClickEvent('dragEnd',this.getPointer(event.center));
  282. }
  283. /**
  284. * Handle pinch event
  285. * @param event
  286. * @private
  287. */
  288. onPinch(event) {
  289. let pointer = this.getPointer(event.center);
  290. this.drag.pinched = true;
  291. if (this.pinch['scale'] === undefined) {
  292. this.pinch.scale = 1;
  293. }
  294. // TODO: enabled moving while pinching?
  295. let scale = this.pinch.scale * event.scale;
  296. this.zoom(scale, pointer)
  297. }
  298. /**
  299. * Zoom the network in or out
  300. * @param {Number} scale a number around 1, and between 0.01 and 10
  301. * @param {{x: Number, y: Number}} pointer Position on screen
  302. * @return {Number} appliedScale scale is limited within the boundaries
  303. * @private
  304. */
  305. zoom(scale, pointer) {
  306. if (this.options.zoomView === true) {
  307. let scaleOld = this.body.view.scale;
  308. if (scale < 0.00001) {
  309. scale = 0.00001;
  310. }
  311. if (scale > 10) {
  312. scale = 10;
  313. }
  314. let preScaleDragPointer = undefined;
  315. if (this.drag !== undefined) {
  316. if (this.drag.dragging === true) {
  317. preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer);
  318. }
  319. }
  320. // + this.canvas.frame.canvas.clientHeight / 2
  321. let translation = this.body.view.translation;
  322. let scaleFrac = scale / scaleOld;
  323. let tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  324. let ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  325. this.body.view.scale = scale;
  326. this.body.view.translation = {x:tx, y:ty};
  327. if (preScaleDragPointer != undefined) {
  328. let postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer);
  329. this.drag.pointer.x = postScaleDragPointer.x;
  330. this.drag.pointer.y = postScaleDragPointer.y;
  331. }
  332. this.body.emitter.emit('_requestRedraw');
  333. if (scaleOld < scale) {
  334. this.body.emitter.emit('zoom', {direction: '+'});
  335. }
  336. else {
  337. this.body.emitter.emit('zoom', {direction: '-'});
  338. }
  339. }
  340. }
  341. /**
  342. * Event handler for mouse wheel event, used to zoom the timeline
  343. * See http://adomas.org/javascript-mouse-wheel/
  344. * https://github.com/EightMedia/hammer.js/issues/256
  345. * @param {MouseEvent} event
  346. * @private
  347. */
  348. onMouseWheel(event) {
  349. // retrieve delta
  350. let delta = 0;
  351. if (event.wheelDelta) { /* IE/Opera. */
  352. delta = event.wheelDelta / 120;
  353. } else if (event.detail) { /* Mozilla case. */
  354. // In Mozilla, sign of delta is different than in IE.
  355. // Also, delta is multiple of 3.
  356. delta = -event.detail / 3;
  357. }
  358. // If delta is nonzero, handle it.
  359. // Basically, delta is now positive if wheel was scrolled up,
  360. // and negative, if wheel was scrolled down.
  361. if (delta !== 0) {
  362. // calculate the new scale
  363. let scale = this.body.view.scale;
  364. let zoom = delta / 10;
  365. if (delta < 0) {
  366. zoom = zoom / (1 - zoom);
  367. }
  368. scale *= (1 + zoom);
  369. // calculate the pointer location
  370. let pointer = this.getPointer({x:event.pageX, y:event.pageY});
  371. // apply the new scale
  372. this.zoom(scale, pointer);
  373. }
  374. // Prevent default actions caused by mouse wheel.
  375. event.preventDefault();
  376. }
  377. /**
  378. * Mouse move handler for checking whether the title moves over a node with a title.
  379. * @param {Event} event
  380. * @private
  381. */
  382. onMouseMove(event) {
  383. let pointer = this.getPointer({x:event.pageX, y:event.pageY});
  384. let popupVisible = false;
  385. // check if the previously selected node is still selected
  386. if (this.popup !== undefined) {
  387. if (this.popup.hidden === false) {
  388. this._checkHidePopup(pointer);
  389. }
  390. // if the popup was not hidden above
  391. if (this.popup.hidden === false) {
  392. popupVisible = true;
  393. this.popup.setPosition(pointer.x + 3, pointer.y - 5)
  394. this.popup.show();
  395. }
  396. }
  397. // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over.
  398. if (this.options.keyboard.bindToWindow === false && this.options.keyboard.enabled === true) {
  399. this.canvas.frame.focus();
  400. }
  401. // start a timeout that will check if the mouse is positioned above an element
  402. if (popupVisible === false) {
  403. if (this.popupTimer !== undefined) {
  404. clearInterval(this.popupTimer); // stop any running calculationTimer
  405. this.popupTimer = undefined;
  406. }
  407. if (!this.drag.dragging) {
  408. this.popupTimer = setTimeout(() => this._checkShowPopup(pointer), this.options.tooltipDelay);
  409. }
  410. }
  411. /**
  412. * Adding hover highlights
  413. */
  414. if (this.options.hoverEnabled === true) {
  415. // removing all hover highlights
  416. for (let edgeId in this.hoverObj.edges) {
  417. if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  418. this.hoverObj.edges[edgeId].hover = false;
  419. delete this.hoverObj.edges[edgeId];
  420. }
  421. }
  422. // adding hover highlights
  423. let obj = this.selectionHandler.getNodeAt(pointer);
  424. if (obj === undefined) {
  425. obj = this.selectionHandler.getEdgeAt(pointer);
  426. }
  427. if (obj != undefined) {
  428. this.selectionHandler.hoverObject(obj);
  429. }
  430. // removing all node hover highlights except for the selected one.
  431. for (let nodeId in this.hoverObj.nodes) {
  432. if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  433. if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj === undefined) {
  434. this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]);
  435. delete this.hoverObj.nodes[nodeId];
  436. }
  437. }
  438. }
  439. this.body.emitter.emit('_requestRedraw');
  440. }
  441. }
  442. /**
  443. * Check if there is an element on the given position in the network
  444. * (a node or edge). If so, and if this element has a title,
  445. * show a popup window with its title.
  446. *
  447. * @param {{x:Number, y:Number}} pointer
  448. * @private
  449. */
  450. _checkShowPopup(pointer) {
  451. let x = this.canvas._XconvertDOMtoCanvas(pointer.x);
  452. let y = this.canvas._YconvertDOMtoCanvas(pointer.y);
  453. let pointerObj = {
  454. left: x,
  455. top: y,
  456. right: x,
  457. bottom: y
  458. };
  459. let previousPopupObjId = this.popupObj === undefined ? undefined : this.popupObj.id;
  460. let nodeUnderCursor = false;
  461. let popupType = 'node';
  462. // check if a node is under the cursor.
  463. if (this.popupObj === undefined) {
  464. // search the nodes for overlap, select the top one in case of multiple nodes
  465. let nodeIndices = this.body.nodeIndices;
  466. let nodes = this.body.nodes;
  467. let node;
  468. let overlappingNodes = [];
  469. for (let i = 0; i < nodeIndices.length; i++) {
  470. node = nodes[nodeIndices[i]];
  471. if (node.isOverlappingWith(pointerObj) === true) {
  472. if (node.getTitle() !== undefined) {
  473. overlappingNodes.push(nodeIndices[i]);
  474. }
  475. }
  476. }
  477. if (overlappingNodes.length > 0) {
  478. // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others
  479. this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]];
  480. // if you hover over a node, the title of the edge is not supposed to be shown.
  481. nodeUnderCursor = true;
  482. }
  483. }
  484. if (this.popupObj === undefined && nodeUnderCursor === false) {
  485. // search the edges for overlap
  486. let edgeIndices = this.body.edgeIndices;
  487. let edges = this.body.edges;
  488. let edge;
  489. let overlappingEdges = [];
  490. for (let i = 0; i < edgeIndices.length; i++) {
  491. edge = edges[edgeIndices[i]];
  492. if (edge.isOverlappingWith(pointerObj) === true) {
  493. if (edge.connected === true && edge.getTitle() !== undefined) {
  494. overlappingEdges.push(edgeIndices[i]);
  495. }
  496. }
  497. }
  498. if (overlappingEdges.length > 0) {
  499. this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]];
  500. popupType = 'edge';
  501. }
  502. }
  503. if (this.popupObj !== undefined) {
  504. // show popup message window
  505. if (this.popupObj.id !== previousPopupObjId) {
  506. if (this.popup === undefined) {
  507. this.popup = new Popup(this.canvas.frame);
  508. }
  509. this.popup.popupTargetType = popupType;
  510. this.popup.popupTargetId = this.popupObj.id;
  511. // adjust a small offset such that the mouse cursor is located in the
  512. // bottom left location of the popup, and you can easily move over the
  513. // popup area
  514. this.popup.setPosition(pointer.x + 3, pointer.y - 5);
  515. this.popup.setText(this.popupObj.getTitle());
  516. this.popup.show();
  517. this.body.emitter.emit('showPopup',this.popupObj.id);
  518. }
  519. }
  520. else {
  521. if (this.popup !== undefined) {
  522. this.popup.hide();
  523. this.body.emitter.emit('hidePopup');
  524. }
  525. }
  526. }
  527. /**
  528. * Check if the popup must be hidden, which is the case when the mouse is no
  529. * longer hovering on the object
  530. * @param {{x:Number, y:Number}} pointer
  531. * @private
  532. */
  533. _checkHidePopup(pointer) {
  534. let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
  535. let stillOnObj = false;
  536. if (this.popup.popupTargetType === 'node') {
  537. if (this.body.nodes[this.popup.popupTargetId] !== undefined) {
  538. stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj);
  539. // 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.
  540. // we initially only check stillOnObj because this is much faster.
  541. if (stillOnObj === true) {
  542. let overNode = this.selectionHandler.getNodeAt(pointer);
  543. stillOnObj = overNode.id === this.popup.popupTargetId;
  544. }
  545. }
  546. }
  547. else {
  548. if (this.selectionHandler.getNodeAt(pointer) === undefined) {
  549. if (this.body.edges[this.popup.popupTargetId] !== undefined) {
  550. stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj);
  551. }
  552. }
  553. }
  554. if (stillOnObj === false) {
  555. this.popupObj = undefined;
  556. this.popup.hide();
  557. this.body.emitter.emit('hidePopup');
  558. }
  559. }
  560. }
  561. export default InteractionHandler;