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.

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