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. // note: drag.pointer is set in onTouch to get the initial touch location
  144. var node = this.selectionHandler.getNodeAt(this.drag.pointer);
  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.options.fixed.x,
  169. yFixed: object.options.fixed.y
  170. };
  171. object.options.fixed.x = true;
  172. object.options.fixed.y = 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. // only move the node if it was not fixed initially
  198. if (selection.xFixed === false) {
  199. node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX);
  200. }
  201. // only move the node if it was not fixed initially
  202. if (selection.yFixed === false) {
  203. node.y = this.canvas._YconvertDOMtoCanvas(this.canvas._YconvertCanvasToDOM(selection.y) + deltaY);
  204. }
  205. });
  206. // start the simulation of the physics
  207. this.body.emitter.emit("startSimulation");
  208. }
  209. else {
  210. // move the network
  211. if (this.options.dragView === true) {
  212. // if the drag was not started properly because the click started outside the network div, start it now.
  213. if (this.drag.pointer === undefined) {
  214. this._handleDragStart(event);
  215. return;
  216. }
  217. var diffX = pointer.x - this.drag.pointer.x;
  218. var diffY = pointer.y - this.drag.pointer.y;
  219. this.body.view.translation = {x:this.drag.translation.x + diffX, y:this.drag.translation.y + diffY};
  220. this.body.emitter.emit("_redraw");
  221. }
  222. }
  223. }
  224. /**
  225. * handle drag start event
  226. * @private
  227. */
  228. onDragEnd(event) {
  229. this.drag.dragging = false;
  230. var selection = this.drag.selection;
  231. if (selection && selection.length) {
  232. selection.forEach(function (s) {
  233. // restore original xFixed and yFixed
  234. s.node.options.fixed.x = s.xFixed;
  235. s.node.options.fixed.y = s.yFixed;
  236. });
  237. this.body.emitter.emit("startSimulation");
  238. }
  239. else {
  240. this.body.emitter.emit("_requestRedraw");
  241. }
  242. this.body.emitter.emit("dragEnd", {nodeIds: this.selectionHandler.getSelection().nodes});
  243. }
  244. /**
  245. * Handle pinch event
  246. * @param event
  247. * @private
  248. */
  249. onPinch(event) {
  250. var pointer = this.getPointer(event.center);
  251. this.drag.pinched = true;
  252. if (this.pinch['scale'] === undefined) {
  253. this.pinch.scale = 1;
  254. }
  255. // TODO: enabled moving while pinching?
  256. var scale = this.pinch.scale * event.scale;
  257. this.zoom(scale, pointer)
  258. }
  259. /**
  260. * Zoom the network in or out
  261. * @param {Number} scale a number around 1, and between 0.01 and 10
  262. * @param {{x: Number, y: Number}} pointer Position on screen
  263. * @return {Number} appliedScale scale is limited within the boundaries
  264. * @private
  265. */
  266. zoom(scale, pointer) {
  267. if (this.options.zoomView === true) {
  268. var scaleOld = this.body.view.scale;
  269. if (scale < 0.00001) {
  270. scale = 0.00001;
  271. }
  272. if (scale > 10) {
  273. scale = 10;
  274. }
  275. var preScaleDragPointer = null;
  276. if (this.drag !== undefined) {
  277. if (this.drag.dragging === true) {
  278. preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer);
  279. }
  280. }
  281. // + this.canvas.frame.canvas.clientHeight / 2
  282. var translation = this.body.view.translation;
  283. var scaleFrac = scale / scaleOld;
  284. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  285. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  286. this.body.view.scale = scale;
  287. this.body.view.translation = {x:tx, y:ty};
  288. if (preScaleDragPointer != null) {
  289. var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer);
  290. this.drag.pointer.x = postScaleDragPointer.x;
  291. this.drag.pointer.y = postScaleDragPointer.y;
  292. }
  293. this.body.emitter.emit("_requestRedraw");
  294. if (scaleOld < scale) {
  295. this.body.emitter.emit("zoom", {direction: "+"});
  296. }
  297. else {
  298. this.body.emitter.emit("zoom", {direction: "-"});
  299. }
  300. }
  301. }
  302. /**
  303. * Event handler for mouse wheel event, used to zoom the timeline
  304. * See http://adomas.org/javascript-mouse-wheel/
  305. * https://github.com/EightMedia/hammer.js/issues/256
  306. * @param {MouseEvent} event
  307. * @private
  308. */
  309. onMouseWheel(event) {
  310. // retrieve delta
  311. var delta = 0;
  312. if (event.wheelDelta) { /* IE/Opera. */
  313. delta = event.wheelDelta / 120;
  314. } else if (event.detail) { /* Mozilla case. */
  315. // In Mozilla, sign of delta is different than in IE.
  316. // Also, delta is multiple of 3.
  317. delta = -event.detail / 3;
  318. }
  319. // If delta is nonzero, handle it.
  320. // Basically, delta is now positive if wheel was scrolled up,
  321. // and negative, if wheel was scrolled down.
  322. if (delta) {
  323. // calculate the new scale
  324. var scale = this.body.view.scale;
  325. var zoom = delta / 10;
  326. if (delta < 0) {
  327. zoom = zoom / (1 - zoom);
  328. }
  329. scale *= (1 + zoom);
  330. // calculate the pointer location
  331. var pointer = {x:event.pageX, y:event.pageY};
  332. // apply the new scale
  333. this.zoom(scale, pointer);
  334. }
  335. // Prevent default actions caused by mouse wheel.
  336. event.preventDefault();
  337. }
  338. /**
  339. * Mouse move handler for checking whether the title moves over a node with a title.
  340. * @param {Event} event
  341. * @private
  342. */
  343. onMouseMove(event) {
  344. // var pointer = {x:event.pageX, y:event.pageY};
  345. // var popupVisible = false;
  346. //
  347. // // check if the previously selected node is still selected
  348. // if (this.popup !== undefined) {
  349. // if (this.popup.hidden === false) {
  350. // this._checkHidePopup(pointer);
  351. // }
  352. //
  353. // // if the popup was not hidden above
  354. // if (this.popup.hidden === false) {
  355. // popupVisible = true;
  356. // this.popup.setPosition(pointer.x + 3, pointer.y - 5)
  357. // this.popup.show();
  358. // }
  359. // }
  360. //
  361. // // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over
  362. // if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) {
  363. // this.canvas.frame.focus();
  364. // }
  365. //
  366. // // start a timeout that will check if the mouse is positioned above an element
  367. // if (popupVisible === false) {
  368. // var me = this;
  369. // var checkShow = function() {
  370. // me._checkShowPopup(pointer);
  371. // };
  372. //
  373. // if (this.popupTimer) {
  374. // clearInterval(this.popupTimer); // stop any running calculationTimer
  375. // }
  376. // if (!this.drag.dragging) {
  377. // this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay);
  378. // }
  379. // }
  380. //
  381. // /**
  382. // * Adding hover highlights
  383. // */
  384. // if (this.options.hoverEnabled === true) {
  385. // // removing all hover highlights
  386. // for (var edgeId in this.hoverObj.edges) {
  387. // if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  388. // this.hoverObj.edges[edgeId].hover = false;
  389. // delete this.hoverObj.edges[edgeId];
  390. // }
  391. // }
  392. //
  393. // // adding hover highlights
  394. // var obj = this.selectionHandler.getNodeAt(pointer);
  395. // if (obj == null) {
  396. // obj = this.selectionHandler.getEdgeAt(pointer);
  397. // }
  398. // if (obj != null) {
  399. // this._hoverObject(obj);
  400. // }
  401. //
  402. // // removing all node hover highlights except for the selected one.
  403. // for (var nodeId in this.hoverObj.nodes) {
  404. // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  405. // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
  406. // this._blurObject(this.hoverObj.nodes[nodeId]);
  407. // delete this.hoverObj.nodes[nodeId];
  408. // }
  409. // }
  410. // }
  411. // this.body.emitter.emit("_requestRedraw");
  412. // }
  413. }
  414. }
  415. export default InteractionHandler;