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.

1096 lines
34 KiB

  1. let util = require('../../util');
  2. let Hammer = require('../../module/hammer');
  3. let hammerUtil = require('../../hammerUtil');
  4. let locales = require('../locales');
  5. /**
  6. * clears the toolbar div element of children
  7. *
  8. * @private
  9. */
  10. class ManipulationSystem {
  11. constructor(body, canvas, selectionHandler) {
  12. this.body = body;
  13. this.canvas = canvas;
  14. this.selectionHandler = selectionHandler;
  15. this.editMode = false;
  16. this.manipulationDiv = undefined;
  17. this.editModeDiv = undefined;
  18. this.closeDiv = undefined;
  19. this.manipulationHammers = [];
  20. this.temporaryUIFunctions = {};
  21. this.temporaryEventFunctions = [];
  22. this.touchTime = 0;
  23. this.temporaryIds = {nodes: [], edges:[]};
  24. this.guiEnabled = false;
  25. this.selectedControlNode = undefined;
  26. this.options = {};
  27. this.defaultOptions = {
  28. enabled: false,
  29. initiallyActive: false,
  30. locale: 'en',
  31. locales: locales,
  32. functionality:{
  33. addNode: true,
  34. addEdge: true,
  35. editNode: true,
  36. editEdge: true,
  37. deleteNode: true,
  38. deleteEdge: true
  39. },
  40. handlerFunctions: {
  41. addNode: undefined,
  42. addEdge: undefined,
  43. editNode: undefined,
  44. editEdge: undefined,
  45. deleteNode: undefined,
  46. deleteEdge: undefined
  47. },
  48. controlNodeStyle:{
  49. shape:'dot',
  50. size:6,
  51. color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968', border: '#3c3c3c'}},
  52. borderWidth: 2,
  53. borderWidthSelected: 2
  54. }
  55. }
  56. util.extend(this.options, this.defaultOptions);
  57. }
  58. /**
  59. * Set the Options
  60. * @param options
  61. */
  62. setOptions(options) {
  63. if (options !== undefined) {
  64. if (typeof options === 'boolean') {
  65. this.options.enabled = options;
  66. }
  67. else {
  68. this.options.enabled = true;
  69. util.deepExtend(this.options, options);
  70. }
  71. if (this.options.initiallyActive === true) {
  72. this.editMode = true;
  73. }
  74. this._setup();
  75. }
  76. }
  77. /**
  78. * Enable or disable edit-mode. Draws the DOM required and cleans up after itself.
  79. *
  80. * @private
  81. */
  82. toggleEditMode() {
  83. if (this.editMode === true) {
  84. this.disableEditMode();
  85. }
  86. else {
  87. this.enableEditMode();
  88. }
  89. }
  90. enableEditMode() {
  91. this.editMode = true;
  92. this._clean();
  93. if (this.guiEnabled === true) {
  94. this.manipulationDiv.style.display = 'block';
  95. this.closeDiv.style.display = 'block';
  96. this.editModeDiv.style.display = 'none';
  97. this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this));
  98. this.showManipulatorToolbar();
  99. }
  100. }
  101. disableEditMode() {
  102. this.editMode = false;
  103. this._clean();
  104. if (this.guiEnabled === true) {
  105. this.manipulationDiv.style.display = 'none';
  106. this.closeDiv.style.display = 'none';
  107. this.editModeDiv.style.display = 'block';
  108. this._createEditButton();
  109. }
  110. }
  111. /**
  112. * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  113. *
  114. * @private
  115. */
  116. showManipulatorToolbar() {
  117. // restore the state of any bound functions or events, remove control nodes, restore physics
  118. this._clean();
  119. // reset global letiables
  120. this.manipulationDOM = {};
  121. // if the gui is enabled, draw all elements.
  122. if (this.guiEnabled === true) {
  123. let selectedNodeCount = this.selectionHandler._getSelectedNodeCount();
  124. let selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount();
  125. let selectedTotalCount = selectedNodeCount + selectedEdgeCount;
  126. let locale = this.options.locales[this.options.locale];
  127. let needSeperator = false;
  128. if (this.options.functionality.addNode === true) {
  129. this._createAddNodeButton(locale);
  130. needSeperator = true;
  131. }
  132. if (this.options.functionality.addEdge === true) {
  133. if (needSeperator === true) {
  134. this._createSeperator(1);
  135. } else {
  136. needSeperator = true;
  137. }
  138. this._createAddEdgeButton(locale);
  139. }
  140. if (selectedNodeCount === 1 && typeof this.options.handlerFunctions.editNode === 'function' && this.options.functionality.editNode === true) {
  141. if (needSeperator === true) {
  142. this._createSeperator(2);
  143. } else {
  144. needSeperator = true;
  145. }
  146. this._createEditNodeButton(locale);
  147. }
  148. else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.functionality.editEdge === true) {
  149. if (needSeperator === true) {
  150. this._createSeperator(3);
  151. } else {
  152. needSeperator = true;
  153. }
  154. this._createEditEdgeButton(locale);
  155. }
  156. // remove buttons
  157. if (selectedTotalCount !== 0) {
  158. if (selectedNodeCount === 1 && this.options.functionality.deleteNode === true) {
  159. if (needSeperator === true) {
  160. this._createSeperator(4);
  161. }
  162. this._createDeleteButton(locale);
  163. }
  164. else if (selectedNodeCount === 0 && this.options.functionality.deleteEdge === true) {
  165. if (needSeperator === true) {
  166. this._createSeperator(4);
  167. }
  168. this._createDeleteButton(locale);
  169. }
  170. }
  171. // bind the close button
  172. this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this));
  173. // refresh this bar based on what has been selected
  174. this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this));
  175. }
  176. // redraw to show any possible changes
  177. this.body.emitter.emit('_redraw');
  178. }
  179. /**
  180. * Create the toolbar for adding Nodes
  181. *
  182. * @private
  183. */
  184. addNodeMode() {
  185. // when using the gui, enable edit mode if it wasnt already.
  186. if (this.editMode !== true) {
  187. this.enableEditMode();
  188. }
  189. // restore the state of any bound functions or events, remove control nodes, restore physics
  190. this._clean();
  191. if (this.guiEnabled === true) {
  192. let locale = this.options.locales[this.options.locale];
  193. this.manipulationDOM = {};
  194. this._createBackButton(locale);
  195. this._createSeperator();
  196. this._createDescription(locale['addDescription'])
  197. // bind the close button
  198. this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this));
  199. }
  200. this._temporaryBindEvent('click', this._performAddNode.bind(this));
  201. }
  202. /**
  203. * call the bound function to handle the editing of the node. The node has to be selected.
  204. *
  205. * @private
  206. */
  207. editNodeMode() {
  208. // when using the gui, enable edit mode if it wasnt already.
  209. if (this.editMode !== true) {
  210. this.enableEditMode();
  211. }
  212. // restore the state of any bound functions or events, remove control nodes, restore physics
  213. this._clean();
  214. if (typeof this.options.handlerFunctions.editNode === 'function') {
  215. let node = this.selectionHandler._getSelectedNode();
  216. if (node.isCluster !== true) {
  217. let data = util.deepExtend({}, node.options, true);
  218. data.x = node.x;
  219. data.y = node.y;
  220. if (this.options.handlerFunctions.editNode.length === 2) {
  221. this.options.handlerFunctions.editNode(data, (finalizedData) => {
  222. this.body.data.nodes.update(finalizedData);
  223. this.showManipulatorToolbar();
  224. });
  225. }
  226. else {
  227. throw new Error('The function for edit does not support two arguments (data, callback)');
  228. }
  229. }
  230. else {
  231. alert(this.options.locales[this.options.locale]['editClusterError']);
  232. }
  233. }
  234. else {
  235. throw new Error('No function has been configured to handle the editing of nodes.');
  236. }
  237. }
  238. /**
  239. * create the toolbar to connect nodes
  240. *
  241. * @private
  242. */
  243. addEdgeMode() {
  244. // when using the gui, enable edit mode if it wasnt already.
  245. if (this.editMode !== true) {
  246. this.enableEditMode();
  247. }
  248. // restore the state of any bound functions or events, remove control nodes, restore physics
  249. this._clean();
  250. if (this.guiEnabled === true) {
  251. let locale = this.options.locales[this.options.locale];
  252. this.manipulationDOM = {};
  253. this._createBackButton(locale);
  254. this._createSeperator();
  255. this._createDescription(locale['edgeDescription']);
  256. // bind the close button
  257. this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this));
  258. }
  259. // temporarily overload functions
  260. this._temporaryBindUI('onTouch', this._handleConnect.bind(this));
  261. this._temporaryBindUI('onDragEnd', this._finishConnect.bind(this));
  262. this._temporaryBindUI('onDrag', this._dragControlNode.bind(this));
  263. this._temporaryBindUI('onRelease', this._finishConnect.bind(this));
  264. this._temporaryBindUI('onDragStart', () => {});
  265. this._temporaryBindUI('onHold', () => {});
  266. }
  267. /**
  268. * create the toolbar to edit edges
  269. *
  270. * @private
  271. */
  272. editEdgeMode() {
  273. // when using the gui, enable edit mode if it wasnt already.
  274. if (this.editMode !== true) {
  275. this.enableEditMode();
  276. }
  277. // restore the state of any bound functions or events, remove control nodes, restore physics
  278. this._clean();
  279. if (this.guiEnabled === true) {
  280. let locale = this.options.locales[this.options.locale];
  281. this.manipulationDOM = {};
  282. this._createBackButton(locale);
  283. this._createSeperator();
  284. this._createDescription(locale['editEdgeDescription']);
  285. // bind the close button
  286. this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this));
  287. }
  288. this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0];
  289. let edge = this.body.edges[this.edgeBeingEditedId];
  290. // create control nodes
  291. let controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y);
  292. let controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y);
  293. this.temporaryIds.nodes.push(controlNodeFrom.id);
  294. this.temporaryIds.nodes.push(controlNodeTo.id);
  295. this.body.nodes[controlNodeFrom.id] = controlNodeFrom;
  296. this.body.nodeIndices.push(controlNodeFrom.id);
  297. this.body.nodes[controlNodeTo.id] = controlNodeTo;
  298. this.body.nodeIndices.push(controlNodeTo.id);
  299. // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI
  300. this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this)); // used to get the position
  301. this._temporaryBindUI('onTap', () => {}); // disabled
  302. this._temporaryBindUI('onHold', () => {}); // disabled
  303. this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this));// used to select control node
  304. this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this)); // used to drag control node
  305. this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes
  306. this._temporaryBindUI('onMouseMove', () => {}); // disabled
  307. // create function to position control nodes correctly on movement
  308. // automatically cleaned up because we use the temporary bind
  309. this._temporaryBindEvent('beforeDrawing', (ctx) => {
  310. let positions = edge.edgeType.findBorderPositions(ctx);
  311. if (controlNodeFrom.selected === false) {
  312. controlNodeFrom.x = positions.from.x;
  313. controlNodeFrom.y = positions.from.y;
  314. }
  315. if (controlNodeTo.selected === false) {
  316. controlNodeTo.x = positions.to.x;
  317. controlNodeTo.y = positions.to.y;
  318. }
  319. });
  320. this.body.emitter.emit('_redraw');
  321. }
  322. /**
  323. * delete everything in the selection
  324. *
  325. * @private
  326. */
  327. deleteSelected() {
  328. // when using the gui, enable edit mode if it wasnt already.
  329. if (this.editMode !== true) {
  330. this.enableEditMode();
  331. }
  332. // restore the state of any bound functions or events, remove control nodes, restore physics
  333. this._clean();
  334. let selectedNodes = this.selectionHandler.getSelectedNodes();
  335. let selectedEdges = this.selectionHandler.getSelectedEdges();
  336. let deleteFunction = undefined;
  337. if (selectedNodes.length > 0) {
  338. for (let i = 0; i < selectedNodes.length; i++) {
  339. if (this.body.nodes[selectedNodes[i]].isCluster === true) {
  340. alert(this.options.locales[this.options.locale]['deleteClusterError']);
  341. return;
  342. }
  343. }
  344. if (typeof this.options.handlerFunctions.deleteNode === 'function') {
  345. deleteFunction = this.options.handlerFunctions.deleteNode;
  346. }
  347. }
  348. else if (selectedEdges.length > 0) {
  349. if (typeof this.options.handlerFunctions.deleteEdge === 'function') {
  350. deleteFunction = this.options.handlerFunctions.deleteEdge;
  351. }
  352. }
  353. if (typeof deleteFunction === 'function') {
  354. let data = {nodes: selectedNodes, edges: selectedEdges};
  355. if (deleteFunction.length === 2) {
  356. deleteFunction(data, (finalizedData) => {
  357. if (finalizedData !== null && finalizedData !== undefined) {
  358. this.body.data.edges.remove(finalizedData.edges);
  359. this.body.data.nodes.remove(finalizedData.nodes);
  360. this.body.emitter.emit('startSimulation');
  361. }
  362. });
  363. }
  364. else {
  365. throw new Error('The function for delete does not support two arguments (data, callback)')
  366. }
  367. }
  368. else {
  369. this.body.data.edges.remove(selectedEdges);
  370. this.body.data.nodes.remove(selectedNodes);
  371. this.body.emitter.emit('startSimulation');
  372. }
  373. }
  374. //********************************************** PRIVATE ***************************************//
  375. /**
  376. * draw or remove the DOM
  377. * @private
  378. */
  379. _setup() {
  380. if (this.options.enabled === true) {
  381. // Enable the GUI
  382. this.guiEnabled = true;
  383. this._createWrappers();
  384. if (this.editMode === false) {
  385. this._createEditButton();
  386. }
  387. else {
  388. this.showManipulatorToolbar();
  389. }
  390. }
  391. else {
  392. this._removeManipulationDOM();
  393. // disable the gui
  394. this.guiEnabled = false;
  395. }
  396. }
  397. /**
  398. * create the div overlays that contain the DOM
  399. * @private
  400. */
  401. _createWrappers() {
  402. // load the manipulator HTML elements. All styling done in css.
  403. if (this.manipulationDiv === undefined) {
  404. this.manipulationDiv = document.createElement('div');
  405. this.manipulationDiv.className = 'vis-manipulation';
  406. if (this.editMode === true) {
  407. this.manipulationDiv.style.display = 'block';
  408. }
  409. else {
  410. this.manipulationDiv.style.display = 'none';
  411. }
  412. this.canvas.frame.appendChild(this.manipulationDiv);
  413. }
  414. // container for the edit button.
  415. if (this.editModeDiv === undefined) {
  416. this.editModeDiv = document.createElement('div');
  417. this.editModeDiv.className = 'vis-edit-mode';
  418. if (this.editMode === true) {
  419. this.editModeDiv.style.display = 'none';
  420. }
  421. else {
  422. this.editModeDiv.style.display = 'block';
  423. }
  424. this.canvas.frame.appendChild(this.editModeDiv);
  425. }
  426. // container for the close div button
  427. if (this.closeDiv === undefined) {
  428. this.closeDiv = document.createElement('div');
  429. this.closeDiv.className = 'vis-close';
  430. this.closeDiv.style.display = this.manipulationDiv.style.display;
  431. this.canvas.frame.appendChild(this.closeDiv);
  432. }
  433. }
  434. /**
  435. * generate a new target node. Used for creating new edges and editing edges
  436. * @param x
  437. * @param y
  438. * @returns {*}
  439. * @private
  440. */
  441. _getNewTargetNode(x,y) {
  442. let controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle);
  443. controlNodeStyle.id = 'targetNode' + util.randomUUID();
  444. controlNodeStyle.hidden = false;
  445. controlNodeStyle.physics = false;
  446. controlNodeStyle.x = x;
  447. controlNodeStyle.y = y;
  448. return this.body.functions.createNode(controlNodeStyle);
  449. }
  450. /**
  451. * Create the edit button
  452. */
  453. _createEditButton() {
  454. // restore everything to it's original state (if applicable)
  455. this._clean();
  456. // reset the manipulationDOM
  457. this.manipulationDOM = {};
  458. // empty the editModeDiv
  459. util.recursiveDOMDelete(this.editModeDiv);
  460. // create the contents for the editMode button
  461. let locale = this.options.locales[this.options.locale];
  462. let button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit']);
  463. this.editModeDiv.appendChild(button);
  464. // bind a hammer listener to the button, calling the function toggleEditMode.
  465. this._bindHammerToDiv(button, this.toggleEditMode.bind(this));
  466. }
  467. /**
  468. * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed.
  469. * @private
  470. */
  471. _clean() {
  472. // _clean the divs
  473. if (this.guiEnabled === true) {
  474. util.recursiveDOMDelete(this.editModeDiv);
  475. util.recursiveDOMDelete(this.manipulationDiv);
  476. // removes all the bindings and overloads
  477. this._cleanManipulatorHammers();
  478. }
  479. // remove temporary nodes and edges
  480. this._cleanupTemporaryNodesAndEdges();
  481. // restore overloaded UI functions
  482. this._unbindTemporaryUIs();
  483. // remove the temporaryEventFunctions
  484. this._unbindTemporaryEvents();
  485. // restore the physics if required
  486. this.body.emitter.emit('restorePhysics');
  487. }
  488. /**
  489. * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up.
  490. * @private
  491. */
  492. _cleanManipulatorHammers() {
  493. // _clean hammer bindings
  494. if (this.manipulationHammers.length != 0) {
  495. for (let i = 0; i < this.manipulationHammers.length; i++) {
  496. this.manipulationHammers[i].destroy();
  497. }
  498. this.manipulationHammers = [];
  499. }
  500. }
  501. /**
  502. * Remove all DOM elements created by this module.
  503. * @private
  504. */
  505. _removeManipulationDOM() {
  506. // removes all the bindings and overloads
  507. this._clean();
  508. // empty the manipulation divs
  509. util.recursiveDOMDelete(this.manipulationDiv);
  510. util.recursiveDOMDelete(this.editModeDiv);
  511. util.recursiveDOMDelete(this.closeDiv);
  512. // remove the manipulation divs
  513. this.canvas.frame.removeChild(this.manipulationDiv);
  514. this.canvas.frame.removeChild(this.editModeDiv);
  515. this.canvas.frame.removeChild(this.closeDiv);
  516. // set the references to undefined
  517. this.manipulationDiv = undefined;
  518. this.editModeDiv = undefined;
  519. this.closeDiv = undefined;
  520. }
  521. /**
  522. * create a seperator line. the index is to differentiate in the manipulation dom
  523. * @param index
  524. * @private
  525. */
  526. _createSeperator(index = 1) {
  527. this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div');
  528. this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line';
  529. this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]);
  530. }
  531. // ---------------------- DOM functions for buttons --------------------------//
  532. _createAddNodeButton(locale) {
  533. let button = this._createButton('addNode', 'vis-button vis-add', locale['addNode']);
  534. this.manipulationDiv.appendChild(button);
  535. this._bindHammerToDiv(button, this.addNodeMode.bind(this));
  536. }
  537. _createAddEdgeButton(locale) {
  538. let button = this._createButton('addEdge', 'vis-button vis-connect', locale['addEdge']);
  539. this.manipulationDiv.appendChild(button);
  540. this._bindHammerToDiv(button, this.addEdgeMode.bind(this));
  541. }
  542. _createEditNodeButton(locale) {
  543. let button = this._createButton('editNodeMode', 'vis-button vis-edit', locale['editNodeMode']);
  544. this.manipulationDiv.appendChild(button);
  545. this._bindHammerToDiv(button, this.editNodeMode.bind(this));
  546. }
  547. _createEditEdgeButton(locale) {
  548. let button = this._createButton('editEdge', 'vis-button vis-edit', locale['editEdge']);
  549. this.manipulationDiv.appendChild(button);
  550. this._bindHammerToDiv(button, this.editEdgeMode.bind(this));
  551. }
  552. _createDeleteButton(locale) {
  553. let button = this._createButton('delete', 'vis-button vis-delete', locale['del']);
  554. this.manipulationDiv.appendChild(button);
  555. this._bindHammerToDiv(button, this.deleteSelected.bind(this));
  556. }
  557. _createBackButton(locale) {
  558. let button = this._createButton('back', 'vis-button vis-back', locale['back']);
  559. this.manipulationDiv.appendChild(button);
  560. this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this));
  561. }
  562. _createButton(id, className, label, labelClassName = 'vis-label') {
  563. this.manipulationDOM[id+'Div'] = document.createElement('div');
  564. this.manipulationDOM[id+'Div'].className = className;
  565. this.manipulationDOM[id+'Label'] = document.createElement('div');
  566. this.manipulationDOM[id+'Label'].className = labelClassName;
  567. this.manipulationDOM[id+'Label'].innerHTML = label;
  568. this.manipulationDOM[id+'Div'].appendChild(this.manipulationDOM[id+'Label']);
  569. return this.manipulationDOM[id+'Div'];
  570. }
  571. _createDescription(label) {
  572. this.manipulationDiv.appendChild(
  573. this._createButton('description', 'vis-button vis-none', label)
  574. );
  575. }
  576. // -------------------------- End of DOM functions for buttons ------------------------------//
  577. /**
  578. * this binds an event until cleanup by the clean functions.
  579. * @param event
  580. * @param newFunction
  581. * @private
  582. */
  583. _temporaryBindEvent(event, newFunction) {
  584. this.temporaryEventFunctions.push({event:event, boundFunction:newFunction});
  585. this.body.emitter.on(event, newFunction);
  586. }
  587. /**
  588. * this overrides an UI function until cleanup by the clean function
  589. * @param UIfunctionName
  590. * @param newFunction
  591. * @private
  592. */
  593. _temporaryBindUI(UIfunctionName, newFunction) {
  594. if (this.body.eventListeners[UIfunctionName] !== undefined) {
  595. this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName];
  596. this.body.eventListeners[UIfunctionName] = newFunction;
  597. }
  598. else {
  599. throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners)));
  600. }
  601. }
  602. /**
  603. * Restore the overridden UI functions to their original state.
  604. *
  605. * @private
  606. */
  607. _unbindTemporaryUIs() {
  608. for (let functionName in this.temporaryUIFunctions) {
  609. if (this.temporaryUIFunctions.hasOwnProperty(functionName)) {
  610. this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName];
  611. delete this.temporaryUIFunctions[functionName];
  612. }
  613. }
  614. this.temporaryUIFunctions = {};
  615. }
  616. /**
  617. * Unbind the events created by _temporaryBindEvent
  618. * @private
  619. */
  620. _unbindTemporaryEvents() {
  621. for (let i = 0; i < this.temporaryEventFunctions.length; i++) {
  622. let eventName = this.temporaryEventFunctions[i].event;
  623. let boundFunction = this.temporaryEventFunctions[i].boundFunction;
  624. this.body.emitter.off(eventName, boundFunction);
  625. }
  626. this.temporaryEventFunctions = [];
  627. }
  628. /**
  629. * Bind an hammer instance to a DOM element.
  630. * @param domElement
  631. * @param funct
  632. */
  633. _bindHammerToDiv(domElement, boundFunction) {
  634. let hammer = new Hammer(domElement, {});
  635. hammerUtil.onTouch(hammer, boundFunction);
  636. this.manipulationHammers.push(hammer);
  637. }
  638. /**
  639. * Neatly clean up temporary edges and nodes
  640. * @private
  641. */
  642. _cleanupTemporaryNodesAndEdges() {
  643. // _clean temporary edges
  644. for (let i = 0; i < this.temporaryIds.edges.length; i++) {
  645. this.body.edges[this.temporaryIds.edges[i]].disconnect();
  646. delete this.body.edges[this.temporaryIds.edges[i]];
  647. let indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]);
  648. if (indexTempEdge !== -1) {this.body.edgeIndices.splice(indexTempEdge,1);}
  649. }
  650. // _clean temporary nodes
  651. for (let i = 0; i < this.temporaryIds.nodes.length; i++) {
  652. delete this.body.nodes[this.temporaryIds.nodes[i]];
  653. let indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]);
  654. if (indexTempNode !== -1) {this.body.nodeIndices.splice(indexTempNode,1);}
  655. }
  656. this.temporaryIds = {nodes: [], edges: []};
  657. }
  658. // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------//
  659. /**
  660. * the touch is used to get the position of the initial click
  661. * @param event
  662. * @private
  663. */
  664. _controlNodeTouch(event) {
  665. this.selectionHandler.unselectAll();
  666. this.lastTouch = this.body.functions.getPointer(event.center);
  667. this.lastTouch.translation = util.extend({},this.body.view.translation); // copy the object
  668. }
  669. /**
  670. * the drag start is used to mark one of the control nodes as selected.
  671. * @param event
  672. * @private
  673. */
  674. _controlNodeDragStart(event) {
  675. let pointer = this.lastTouch;
  676. let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
  677. let from = this.body.nodes[this.temporaryIds.nodes[0]];
  678. let to = this.body.nodes[this.temporaryIds.nodes[1]];
  679. let edge = this.body.edges[this.edgeBeingEditedId];
  680. this.selectedControlNode = undefined;
  681. let fromSelect = from.isOverlappingWith(pointerObj);
  682. let toSelect = to.isOverlappingWith(pointerObj);
  683. if (fromSelect === true) {
  684. this.selectedControlNode = from;
  685. edge.edgeType.from = from;
  686. }
  687. else if (toSelect === true) {
  688. this.selectedControlNode = to;
  689. edge.edgeType.to = to;
  690. }
  691. this.body.emitter.emit('_redraw');
  692. }
  693. /**
  694. * dragging the control nodes or the canvas
  695. * @param event
  696. * @private
  697. */
  698. _controlNodeDrag(event) {
  699. this.body.emitter.emit('disablePhysics');
  700. let pointer = this.body.functions.getPointer(event.center);
  701. let pos = this.canvas.DOMtoCanvas(pointer);
  702. if (this.selectedControlNode !== undefined) {
  703. this.selectedControlNode.x = pos.x;
  704. this.selectedControlNode.y = pos.y;
  705. }
  706. else {
  707. // if the drag was not started properly because the click started outside the network div, start it now.
  708. let diffX = pointer.x - this.lastTouch.x;
  709. let diffY = pointer.y - this.lastTouch.y;
  710. this.body.view.translation = {x:this.lastTouch.translation.x + diffX, y:this.lastTouch.translation.y + diffY};
  711. }
  712. this.body.emitter.emit('_redraw');
  713. }
  714. /**
  715. * connecting or restoring the control nodes.
  716. * @param event
  717. * @private
  718. */
  719. _controlNodeDragEnd(event) {
  720. let pointer = this.body.functions.getPointer(event.center);
  721. let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
  722. let edge = this.body.edges[this.edgeBeingEditedId];
  723. let overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
  724. let node = undefined;
  725. for (let i = overlappingNodeIds.length-1; i >= 0; i--) {
  726. if (overlappingNodeIds[i] !== this.selectedControlNode.id) {
  727. node = this.body.nodes[overlappingNodeIds[i]];
  728. break;
  729. }
  730. }
  731. // perform the connection
  732. if (node !== undefined && this.selectedControlNode !== undefined) {
  733. if (node.isCluster === true) {
  734. alert(this.options.locales[this.options.locale]['createEdgeError'])
  735. }
  736. else {
  737. let from = this.body.nodes[this.temporaryIds.nodes[0]];
  738. if (this.selectedControlNode.id === from.id) {
  739. this._performEditEdge(node.id, edge.to.id);
  740. }
  741. else {
  742. this._performEditEdge(edge.from.id, node.id);
  743. }
  744. }
  745. }
  746. else {
  747. edge.updateEdgeType();
  748. this.body.emitter.emit('restorePhysics');
  749. }
  750. this.body.emitter.emit('_redraw');
  751. }
  752. // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------//
  753. // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------//
  754. /**
  755. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  756. * to walk the user through the process.
  757. *
  758. * @private
  759. */
  760. _handleConnect(event) {
  761. // check to avoid double fireing of this function.
  762. if (new Date().valueOf() - this.touchTime > 100) {
  763. this.lastTouch = this.body.functions.getPointer(event.center);
  764. this.lastTouch.translation = util.extend({},this.body.view.translation); // copy the object
  765. let pointer = this.lastTouch;
  766. let node = this.selectionHandler.getNodeAt(pointer);
  767. if (node !== undefined) {
  768. if (node.isCluster === true) {
  769. alert(this.options.locales[this.options.locale]['createEdgeError'])
  770. }
  771. else {
  772. // create a node the temporary line can look at
  773. let targetNode = this._getNewTargetNode(node.x,node.y);
  774. this.body.nodes[targetNode.id] = targetNode;
  775. this.body.nodeIndices.push(targetNode.id);
  776. // create a temporary edge
  777. let connectionEdge = this.body.functions.createEdge({
  778. id: 'connectionEdge' + util.randomUUID(),
  779. from: node.id,
  780. to: targetNode.id,
  781. physics: false,
  782. smooth: {
  783. enabled: true,
  784. dynamic: false,
  785. type: 'continuous',
  786. roundness: 0.5
  787. }
  788. });
  789. this.body.edges[connectionEdge.id] = connectionEdge;
  790. this.body.edgeIndices.push(connectionEdge.id);
  791. this.temporaryIds.nodes.push(targetNode.id);
  792. this.temporaryIds.edges.push(connectionEdge.id);
  793. }
  794. }
  795. this.touchTime = new Date().valueOf();
  796. }
  797. }
  798. _dragControlNode(event) {
  799. let pointer = this.body.functions.getPointer(event.center);
  800. if (this.temporaryIds.nodes[0] !== undefined) {
  801. let targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode.
  802. targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x);
  803. targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y);
  804. this.body.emitter.emit('_redraw');
  805. }
  806. else {
  807. let diffX = pointer.x - this.lastTouch.x;
  808. let diffY = pointer.y - this.lastTouch.y;
  809. this.body.view.translation = {x:this.lastTouch.translation.x + diffX, y:this.lastTouch.translation.y + diffY};
  810. }
  811. }
  812. /**
  813. * Connect the new edge to the target if one exists, otherwise remove temp line
  814. * @param event
  815. * @private
  816. */
  817. _finishConnect(event) {
  818. let pointer = this.body.functions.getPointer(event.center);
  819. let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
  820. // remember the edge id
  821. let connectFromId = undefined;
  822. if (this.temporaryIds.edges[0] !== undefined) {
  823. connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId;
  824. }
  825. // get the overlapping node but NOT the temporary node;
  826. let overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
  827. let node = undefined;
  828. for (let i = overlappingNodeIds.length-1; i >= 0; i--) {
  829. // if the node id is NOT a temporary node, accept the node.
  830. if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) {
  831. node = this.body.nodes[overlappingNodeIds[i]];
  832. break;
  833. }
  834. }
  835. // clean temporary nodes and edges.
  836. this._cleanupTemporaryNodesAndEdges();
  837. // perform the connection
  838. if (node !== undefined) {
  839. if (node.isCluster === true) {
  840. alert(this.options.locales[this.options.locale]['createEdgeError']);
  841. }
  842. else {
  843. if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) {
  844. this._performCreateEdge(connectFromId, node.id);
  845. }
  846. }
  847. }
  848. this.body.emitter.emit('_redraw');
  849. }
  850. // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------//
  851. // ------------------------------ Performing all the actual data manipulation ------------------------//
  852. /**
  853. * Adds a node on the specified location
  854. */
  855. _performAddNode(clickData) {
  856. let defaultData = {
  857. id: util.randomUUID(),
  858. x: clickData.pointer.canvas.x,
  859. y: clickData.pointer.canvas.y,
  860. label: 'new'
  861. };
  862. if (typeof this.options.handlerFunctions.addNode === 'function') {
  863. if (this.options.handlerFunctions.addNode.length === 2) {
  864. this.options.handlerFunctions.addNode(defaultData, (finalizedData) => {
  865. if (finalizedData !== null && finalizedData !== undefined) {
  866. this.body.data.nodes.add(finalizedData);
  867. this.showManipulatorToolbar();
  868. }
  869. });
  870. }
  871. else {
  872. throw new Error('The function for add does not support two arguments (data,callback)');
  873. this.showManipulatorToolbar();
  874. }
  875. }
  876. else {
  877. this.body.data.nodes.add(defaultData);
  878. this.showManipulatorToolbar();
  879. }
  880. }
  881. /**
  882. * connect two nodes with a new edge.
  883. *
  884. * @private
  885. */
  886. _performCreateEdge(sourceNodeId, targetNodeId) {
  887. let defaultData = {from: sourceNodeId, to: targetNodeId};
  888. if (this.options.handlerFunctions.addEdge) {
  889. if (this.options.handlerFunctions.addEdge.length === 2) {
  890. this.options.handlerFunctions.addEdge(defaultData, (finalizedData) => {
  891. if (finalizedData !== null && finalizedData !== undefined) {
  892. this.body.data.edges.add(finalizedData);
  893. this.selectionHandler.unselectAll();
  894. this.showManipulatorToolbar();
  895. }
  896. });
  897. }
  898. else {
  899. throw new Error('The function for connect does not support two arguments (data,callback)');
  900. }
  901. }
  902. else {
  903. this.body.data.edges.add(defaultData);
  904. this.selectionHandler.unselectAll();
  905. this.showManipulatorToolbar();
  906. }
  907. }
  908. /**
  909. * connect two nodes with a new edge.
  910. *
  911. * @private
  912. */
  913. _performEditEdge(sourceNodeId, targetNodeId) {
  914. let defaultData = {id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId};
  915. if (this.options.handlerFunctions.editEdge) {
  916. if (this.options.handlerFunctions.editEdge.length === 2) {
  917. this.options.handlerFunctions.editEdge(defaultData, (finalizedData) => {
  918. if (finalizedData === null || finalizedData === undefined) {
  919. this.body.edges[defaultData.id].updateEdgeType();
  920. this.body.emitter.emit('_redraw');
  921. }
  922. else {
  923. this.body.data.edges.update(finalizedData);
  924. this.selectionHandler.unselectAll();
  925. this.showManipulatorToolbar();
  926. }
  927. });
  928. }
  929. else {
  930. throw new Error('The function for edit does not support two arguments (data, callback)');
  931. }
  932. }
  933. else {
  934. this.body.data.edges.update(defaultData);
  935. this.selectionHandler.unselectAll();
  936. this.showManipulatorToolbar();
  937. }
  938. }
  939. }
  940. export default ManipulationSystem;