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.

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