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.

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