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.

725 lines
22 KiB

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