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.

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