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.

672 lines
21 KiB

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