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.

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