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.

1137 lines
36 KiB

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