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.

724 lines
22 KiB

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