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.

489 lines
14 KiB

  1. /**
  2. * Created by Alex on 2/27/2015.
  3. *
  4. */
  5. var util = require('../../util');
  6. import { NavigationHandler } from "./components/NavigationHandler"
  7. class InteractionHandler {
  8. constructor(body, canvas, selectionHandler) {
  9. this.body = body;
  10. this.canvas = canvas;
  11. this.selectionHandler = selectionHandler;
  12. this.navigationHandler = new NavigationHandler(body,canvas);
  13. // bind the events from hammer to functions in this object
  14. this.body.eventListeners.onTap = this.onTap.bind(this);
  15. this.body.eventListeners.onTouch = this.onTouch.bind(this);
  16. this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this);
  17. this.body.eventListeners.onHold = this.onHold.bind(this);
  18. this.body.eventListeners.onDragStart = this.onDragStart.bind(this);
  19. this.body.eventListeners.onDrag = this.onDrag.bind(this);
  20. this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this);
  21. this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this);
  22. this.body.eventListeners.onPinch = this.onPinch.bind(this);
  23. this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this);
  24. this.body.eventListeners.onRelease = this.onRelease.bind(this);
  25. this.touchTime = 0;
  26. this.drag = {};
  27. this.pinch = {};
  28. this.pointerPosition = {x:0,y:0};
  29. this.hoverObj = {nodes:{},edges:{}};
  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: 'black',
  40. fontSize: 14, // px
  41. fontFace: 'verdana',
  42. color: {
  43. border: '#666',
  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. var fields = ['keyboard'];
  59. util.selectiveNotDeepExtend(fields,this.options, options);
  60. // merge the keyboard options in.
  61. util.mergeOptions(this.options, options, 'keyboard');
  62. }
  63. this.navigationHandler.setOptions(this.options);
  64. }
  65. /**
  66. * Get the pointer location from a touch location
  67. * @param {{x: Number, y: Number}} touch
  68. * @return {{x: Number, y: Number}} pointer
  69. * @private
  70. */
  71. getPointer(touch) {
  72. return {
  73. x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas),
  74. y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas)
  75. };
  76. }
  77. /**
  78. * On start of a touch gesture, store the pointer
  79. * @param event
  80. * @private
  81. */
  82. onTouch(event) {
  83. if (new Date().valueOf() - this.touchTime > 100) {
  84. this.drag.pointer = this.getPointer(event.center);
  85. this.drag.pinched = false;
  86. this.pinch.scale = this.body.view.scale;
  87. // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
  88. this.touchTime = new Date().valueOf();
  89. }
  90. }
  91. /**
  92. * handle tap/click event: select/unselect a node
  93. * @private
  94. */
  95. onTap(event) {
  96. var pointer = this.getPointer(event.center);
  97. var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0;
  98. var selected = this.selectionHandler.selectOnPoint(pointer);
  99. if (selected === true || (previouslySelected == true && selected === false)) { // select or unselect
  100. this.body.emitter.emit('select', this.selectionHandler.getSelection());
  101. }
  102. this.selectionHandler._generateClickEvent("click",pointer);
  103. }
  104. /**
  105. * handle doubletap event
  106. * @private
  107. */
  108. onDoubleTap(event) {
  109. var pointer = this.getPointer(event.center);
  110. this.selectionHandler._generateClickEvent("doubleClick",pointer);
  111. }
  112. /**
  113. * handle long tap event: multi select nodes
  114. * @private
  115. */
  116. onHold(event) {
  117. var pointer = this.getPointer(event.center);
  118. var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer);
  119. if (selectionChanged === true) { // select or longpress
  120. this.body.emitter.emit('select', this.selectionHandler.getSelection());
  121. }
  122. this.selectionHandler._generateClickEvent("click",pointer);
  123. }
  124. /**
  125. * handle the release of the screen
  126. *
  127. * @private
  128. */
  129. onRelease(event) {
  130. this.body.emitter.emit("release",event)
  131. }
  132. /**
  133. * This function is called by onDragStart.
  134. * It is separated out because we can then overload it for the datamanipulation system.
  135. *
  136. * @private
  137. */
  138. onDragStart(event) {
  139. //in case the touch event was triggered on an external div, do the initial touch now.
  140. if (this.drag.pointer === undefined) {
  141. this.onTouch(event);
  142. }
  143. var node = this.selectionHandler.getNodeAt(this.drag.pointer);
  144. // note: drag.pointer is set in onTouch to get the initial touch location
  145. this.drag.dragging = true;
  146. this.drag.selection = [];
  147. this.drag.translation = util.extend({},this.body.view.translation); // copy the object
  148. this.drag.nodeId = null;
  149. this.body.emitter.emit("dragStart", {nodeIds: this.selectionHandler.getSelection().nodes});
  150. if (node != null && this.options.dragNodes === true) {
  151. this.drag.nodeId = node.id;
  152. // select the clicked node if not yet selected
  153. if (node.isSelected() === false) {
  154. this.selectionHandler.unselectAll();
  155. this.selectionHandler.selectObject(node);
  156. }
  157. var selection = this.selectionHandler.selectionObj.nodes;
  158. // create an array with the selected nodes and their original location and status
  159. for (let nodeId in selection) {
  160. if (selection.hasOwnProperty(nodeId)) {
  161. var object = selection[nodeId];
  162. var s = {
  163. id: object.id,
  164. node: object,
  165. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  166. x: object.x,
  167. y: object.y,
  168. xFixed: object.xFixed,
  169. yFixed: object.yFixed
  170. };
  171. object.xFixed = true;
  172. object.yFixed = true;
  173. this.drag.selection.push(s);
  174. }
  175. }
  176. }
  177. }
  178. /**
  179. * handle drag event
  180. * @private
  181. */
  182. onDrag(event) {
  183. if (this.drag.pinched === true) {
  184. return;
  185. }
  186. // remove the focus on node if it is focussed on by the focusOnNode
  187. this.body.emitter.emit("unlockNode");
  188. var pointer = this.getPointer(event.center);
  189. var selection = this.drag.selection;
  190. if (selection && selection.length && this.options.dragNodes === true) {
  191. // calculate delta's and new location
  192. var deltaX = pointer.x - this.drag.pointer.x;
  193. var deltaY = pointer.y - this.drag.pointer.y;
  194. // update position of all selected nodes
  195. selection.forEach((selection) => {
  196. var node = selection.node;
  197. if (!selection.xFixed) {
  198. node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX);
  199. }
  200. if (!selection.yFixed) {
  201. node.y = this.canvas._YconvertDOMtoCanvas(this.canvas._YconvertCanvasToDOM(selection.y) + deltaY);
  202. }
  203. });
  204. // start the simulation of the physics
  205. this.body.emitter.emit("startSimulation");
  206. }
  207. else {
  208. // move the network
  209. if (this.options.dragView === true) {
  210. // if the drag was not started properly because the click started outside the network div, start it now.
  211. if (this.drag.pointer === undefined) {
  212. this._handleDragStart(event);
  213. return;
  214. }
  215. var diffX = pointer.x - this.drag.pointer.x;
  216. var diffY = pointer.y - this.drag.pointer.y;
  217. this.body.view.translation = {x:this.drag.translation.x + diffX, y:this.drag.translation.y + diffY};
  218. this.body.emitter.emit("_redraw");
  219. }
  220. }
  221. }
  222. /**
  223. * handle drag start event
  224. * @private
  225. */
  226. onDragEnd(event) {
  227. this.drag.dragging = false;
  228. var selection = this.drag.selection;
  229. if (selection && selection.length) {
  230. selection.forEach(function (s) {
  231. // restore original xFixed and yFixed
  232. s.node.xFixed = s.xFixed;
  233. s.node.yFixed = s.yFixed;
  234. });
  235. this.body.emitter.emit("startSimulation");
  236. }
  237. else {
  238. this.body.emitter.emit("_requestRedraw");
  239. }
  240. this.body.emitter.emit("dragEnd", {nodeIds: this.selectionHandler.getSelection().nodes});
  241. }
  242. /**
  243. * Handle pinch event
  244. * @param event
  245. * @private
  246. */
  247. onPinch(event) {
  248. var pointer = this.getPointer(event.center);
  249. this.drag.pinched = true;
  250. if (this.pinch['scale'] === undefined) {
  251. this.pinch.scale = 1;
  252. }
  253. // TODO: enabled moving while pinching?
  254. var scale = this.pinch.scale * event.scale;
  255. this.zoom(scale, pointer)
  256. }
  257. /**
  258. * Zoom the network in or out
  259. * @param {Number} scale a number around 1, and between 0.01 and 10
  260. * @param {{x: Number, y: Number}} pointer Position on screen
  261. * @return {Number} appliedScale scale is limited within the boundaries
  262. * @private
  263. */
  264. zoom(scale, pointer) {
  265. if (this.options.zoomView === true) {
  266. var scaleOld = this.body.view.scale;
  267. if (scale < 0.00001) {
  268. scale = 0.00001;
  269. }
  270. if (scale > 10) {
  271. scale = 10;
  272. }
  273. var preScaleDragPointer = null;
  274. if (this.drag !== undefined) {
  275. if (this.drag.dragging === true) {
  276. preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer);
  277. }
  278. }
  279. // + this.canvas.frame.canvas.clientHeight / 2
  280. var translation = this.body.view.translation;
  281. var scaleFrac = scale / scaleOld;
  282. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  283. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  284. this.body.view.scale = scale;
  285. this.body.view.translation = {x:tx, y:ty};
  286. if (preScaleDragPointer != null) {
  287. var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer);
  288. this.drag.pointer.x = postScaleDragPointer.x;
  289. this.drag.pointer.y = postScaleDragPointer.y;
  290. }
  291. this.body.emitter.emit("_requestRedraw");
  292. if (scaleOld < scale) {
  293. this.body.emitter.emit("zoom", {direction: "+"});
  294. }
  295. else {
  296. this.body.emitter.emit("zoom", {direction: "-"});
  297. }
  298. }
  299. }
  300. /**
  301. * Event handler for mouse wheel event, used to zoom the timeline
  302. * See http://adomas.org/javascript-mouse-wheel/
  303. * https://github.com/EightMedia/hammer.js/issues/256
  304. * @param {MouseEvent} event
  305. * @private
  306. */
  307. onMouseWheel(event) {
  308. // retrieve delta
  309. var delta = 0;
  310. if (event.wheelDelta) { /* IE/Opera. */
  311. delta = event.wheelDelta / 120;
  312. } else if (event.detail) { /* Mozilla case. */
  313. // In Mozilla, sign of delta is different than in IE.
  314. // Also, delta is multiple of 3.
  315. delta = -event.detail / 3;
  316. }
  317. // If delta is nonzero, handle it.
  318. // Basically, delta is now positive if wheel was scrolled up,
  319. // and negative, if wheel was scrolled down.
  320. if (delta) {
  321. // calculate the new scale
  322. var scale = this.body.view.scale;
  323. var zoom = delta / 10;
  324. if (delta < 0) {
  325. zoom = zoom / (1 - zoom);
  326. }
  327. scale *= (1 + zoom);
  328. // calculate the pointer location
  329. var pointer = {x:event.pageX, y:event.pageY};
  330. // apply the new scale
  331. this.zoom(scale, pointer);
  332. }
  333. // Prevent default actions caused by mouse wheel.
  334. event.preventDefault();
  335. }
  336. /**
  337. * Mouse move handler for checking whether the title moves over a node with a title.
  338. * @param {Event} event
  339. * @private
  340. */
  341. onMouseMove(event) {
  342. // var pointer = {x:event.pageX, y:event.pageY};
  343. // var popupVisible = false;
  344. //
  345. // // check if the previously selected node is still selected
  346. // if (this.popup !== undefined) {
  347. // if (this.popup.hidden === false) {
  348. // this._checkHidePopup(pointer);
  349. // }
  350. //
  351. // // if the popup was not hidden above
  352. // if (this.popup.hidden === false) {
  353. // popupVisible = true;
  354. // this.popup.setPosition(pointer.x + 3, pointer.y - 5)
  355. // this.popup.show();
  356. // }
  357. // }
  358. //
  359. // // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over
  360. // if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) {
  361. // this.canvas.frame.focus();
  362. // }
  363. //
  364. // // start a timeout that will check if the mouse is positioned above an element
  365. // if (popupVisible === false) {
  366. // var me = this;
  367. // var checkShow = function() {
  368. // me._checkShowPopup(pointer);
  369. // };
  370. //
  371. // if (this.popupTimer) {
  372. // clearInterval(this.popupTimer); // stop any running calculationTimer
  373. // }
  374. // if (!this.drag.dragging) {
  375. // this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay);
  376. // }
  377. // }
  378. //
  379. // /**
  380. // * Adding hover highlights
  381. // */
  382. // if (this.options.hoverEnabled === true) {
  383. // // removing all hover highlights
  384. // for (var edgeId in this.hoverObj.edges) {
  385. // if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  386. // this.hoverObj.edges[edgeId].hover = false;
  387. // delete this.hoverObj.edges[edgeId];
  388. // }
  389. // }
  390. //
  391. // // adding hover highlights
  392. // var obj = this.selectionHandler.getNodeAt(pointer);
  393. // if (obj == null) {
  394. // obj = this.selectionHandler.getEdgeAt(pointer);
  395. // }
  396. // if (obj != null) {
  397. // this._hoverObject(obj);
  398. // }
  399. //
  400. // // removing all node hover highlights except for the selected one.
  401. // for (var nodeId in this.hoverObj.nodes) {
  402. // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  403. // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
  404. // this._blurObject(this.hoverObj.nodes[nodeId]);
  405. // delete this.hoverObj.nodes[nodeId];
  406. // }
  407. // }
  408. // }
  409. // this.body.emitter.emit("_requestRedraw");
  410. // }
  411. }
  412. }
  413. export default InteractionHandler;