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.

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