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.

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