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.

634 lines
19 KiB

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