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.

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