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.

1169 lines
38 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 variables
  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 > 0 && 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. addNodeMode() {
  195. // when using the gui, enable edit mode if it wasnt already.
  196. if (this.editMode !== true) {
  197. this.enableEditMode();
  198. }
  199. // restore the state of any bound functions or events, remove control nodes, restore physics
  200. this._clean();
  201. this.inMode = 'addNode';
  202. if (this.guiEnabled === true) {
  203. let locale = this.options.locales[this.options.locale];
  204. this.manipulationDOM = {};
  205. this._createBackButton(locale);
  206. this._createSeperator();
  207. this._createDescription(locale['addDescription'] || this.options.locales['en']['addDescription']);
  208. // bind the close button
  209. this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this));
  210. }
  211. this._temporaryBindEvent('click', this._performAddNode.bind(this));
  212. }
  213. /**
  214. * call the bound function to handle the editing of the node. The node has to be selected.
  215. */
  216. editNode() {
  217. // when using the gui, enable edit mode if it wasnt already.
  218. if (this.editMode !== true) {
  219. this.enableEditMode();
  220. }
  221. // restore the state of any bound functions or events, remove control nodes, restore physics
  222. this._clean();
  223. let node = this.selectionHandler._getSelectedNode();
  224. if (node !== undefined) {
  225. this.inMode = 'editNode';
  226. if (typeof this.options.editNode === 'function') {
  227. if (node.isCluster !== true) {
  228. let data = util.deepExtend({}, node.options, false);
  229. data.x = node.x;
  230. data.y = node.y;
  231. if (this.options.editNode.length === 2) {
  232. this.options.editNode(data, (finalizedData) => {
  233. if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'editNode') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) {
  234. this.body.data.nodes.getDataSet().update(finalizedData);
  235. }
  236. this.showManipulatorToolbar();
  237. });
  238. }
  239. else {
  240. throw new Error('The function for edit does not support two arguments (data, callback)');
  241. }
  242. }
  243. else {
  244. alert(this.options.locales[this.options.locale]['editClusterError'] || this.options.locales['en']['editClusterError']);
  245. }
  246. }
  247. else {
  248. throw new Error('No function has been configured to handle the editing of nodes.');
  249. }
  250. }
  251. else {
  252. this.showManipulatorToolbar();
  253. }
  254. }
  255. /**
  256. * create the toolbar to connect nodes
  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',this._dragStartEdge.bind(this));
  281. this._temporaryBindUI('onHold', () => {});
  282. }
  283. /**
  284. * create the toolbar to edit edges
  285. */
  286. editEdgeMode() {
  287. // when using the gui, enable edit mode if it wasn't already.
  288. if (this.editMode !== true) {
  289. this.enableEditMode();
  290. }
  291. // restore the state of any bound functions or events, remove control nodes, restore physics
  292. this._clean();
  293. this.inMode = 'editEdge';
  294. if (typeof this.options.editEdge === 'object' && typeof this.options.editEdge.editWithoutDrag === "function") {
  295. this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0];
  296. if (this.edgeBeingEditedId !== undefined) {
  297. var edge = this.body.edges[this.edgeBeingEditedId];
  298. this._performEditEdge(edge.from, edge.to);
  299. return;
  300. }
  301. }
  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', () => {}); // 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. else {
  347. this.showManipulatorToolbar();
  348. }
  349. }
  350. /**
  351. * delete everything in the selection
  352. */
  353. deleteSelected() {
  354. // when using the gui, enable edit mode if it wasnt already.
  355. if (this.editMode !== true) {
  356. this.enableEditMode();
  357. }
  358. // restore the state of any bound functions or events, remove control nodes, restore physics
  359. this._clean();
  360. this.inMode = 'delete';
  361. let selectedNodes = this.selectionHandler.getSelectedNodes();
  362. let selectedEdges = this.selectionHandler.getSelectedEdges();
  363. let deleteFunction = undefined;
  364. if (selectedNodes.length > 0) {
  365. for (let i = 0; i < selectedNodes.length; i++) {
  366. if (this.body.nodes[selectedNodes[i]].isCluster === true) {
  367. alert(this.options.locales[this.options.locale]['deleteClusterError'] || this.options.locales['en']['deleteClusterError']);
  368. return;
  369. }
  370. }
  371. if (typeof this.options.deleteNode === 'function') {
  372. deleteFunction = this.options.deleteNode;
  373. }
  374. }
  375. else if (selectedEdges.length > 0) {
  376. if (typeof this.options.deleteEdge === 'function') {
  377. deleteFunction = this.options.deleteEdge;
  378. }
  379. }
  380. if (typeof deleteFunction === 'function') {
  381. let data = {nodes: selectedNodes, edges: selectedEdges};
  382. if (deleteFunction.length === 2) {
  383. deleteFunction(data, (finalizedData) => {
  384. if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'delete') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) {
  385. this.body.data.edges.getDataSet().remove(finalizedData.edges);
  386. this.body.data.nodes.getDataSet().remove(finalizedData.nodes);
  387. this.body.emitter.emit('startSimulation');
  388. this.showManipulatorToolbar();
  389. }
  390. else {
  391. this.body.emitter.emit('startSimulation');
  392. this.showManipulatorToolbar();
  393. }
  394. });
  395. }
  396. else {
  397. throw new Error('The function for delete does not support two arguments (data, callback)')
  398. }
  399. }
  400. else {
  401. this.body.data.edges.getDataSet().remove(selectedEdges);
  402. this.body.data.nodes.getDataSet().remove(selectedNodes);
  403. this.body.emitter.emit('startSimulation');
  404. this.showManipulatorToolbar();
  405. }
  406. }
  407. //********************************************** PRIVATE ***************************************//
  408. /**
  409. * draw or remove the DOM
  410. * @private
  411. */
  412. _setup() {
  413. if (this.options.enabled === true) {
  414. // Enable the GUI
  415. this.guiEnabled = true;
  416. this._createWrappers();
  417. if (this.editMode === false) {
  418. this._createEditButton();
  419. }
  420. else {
  421. this.showManipulatorToolbar();
  422. }
  423. }
  424. else {
  425. this._removeManipulationDOM();
  426. // disable the gui
  427. this.guiEnabled = false;
  428. }
  429. }
  430. /**
  431. * create the div overlays that contain the DOM
  432. * @private
  433. */
  434. _createWrappers() {
  435. // load the manipulator HTML elements. All styling done in css.
  436. if (this.manipulationDiv === undefined) {
  437. this.manipulationDiv = document.createElement('div');
  438. this.manipulationDiv.className = 'vis-manipulation';
  439. if (this.editMode === true) {
  440. this.manipulationDiv.style.display = 'block';
  441. }
  442. else {
  443. this.manipulationDiv.style.display = 'none';
  444. }
  445. this.canvas.frame.appendChild(this.manipulationDiv);
  446. }
  447. // container for the edit button.
  448. if (this.editModeDiv === undefined) {
  449. this.editModeDiv = document.createElement('div');
  450. this.editModeDiv.className = 'vis-edit-mode';
  451. if (this.editMode === true) {
  452. this.editModeDiv.style.display = 'none';
  453. }
  454. else {
  455. this.editModeDiv.style.display = 'block';
  456. }
  457. this.canvas.frame.appendChild(this.editModeDiv);
  458. }
  459. // container for the close div button
  460. if (this.closeDiv === undefined) {
  461. this.closeDiv = document.createElement('div');
  462. this.closeDiv.className = 'vis-close';
  463. this.closeDiv.style.display = this.manipulationDiv.style.display;
  464. this.canvas.frame.appendChild(this.closeDiv);
  465. }
  466. }
  467. /**
  468. * generate a new target node. Used for creating new edges and editing edges
  469. * @param x
  470. * @param y
  471. * @returns {*}
  472. * @private
  473. */
  474. _getNewTargetNode(x,y) {
  475. let controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle);
  476. controlNodeStyle.id = 'targetNode' + util.randomUUID();
  477. controlNodeStyle.hidden = false;
  478. controlNodeStyle.physics = false;
  479. controlNodeStyle.x = x;
  480. controlNodeStyle.y = y;
  481. // we have to define the bounding box in order for the nodes to be drawn immediately
  482. let node = this.body.functions.createNode(controlNodeStyle);
  483. node.shape.boundingBox = {left: x, right:x, top:y, bottom:y};
  484. return node;
  485. }
  486. /**
  487. * Create the edit button
  488. */
  489. _createEditButton() {
  490. // restore everything to it's original state (if applicable)
  491. this._clean();
  492. // reset the manipulationDOM
  493. this.manipulationDOM = {};
  494. // empty the editModeDiv
  495. util.recursiveDOMDelete(this.editModeDiv);
  496. // create the contents for the editMode button
  497. let locale = this.options.locales[this.options.locale];
  498. let button = this._createButton('editMode', 'vis-button vis-edit vis-edit-mode', locale['edit'] || this.options.locales['en']['edit']);
  499. this.editModeDiv.appendChild(button);
  500. // bind a hammer listener to the button, calling the function toggleEditMode.
  501. this._bindHammerToDiv(button, this.toggleEditMode.bind(this));
  502. }
  503. /**
  504. * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed.
  505. * @private
  506. */
  507. _clean() {
  508. // not in mode
  509. this.inMode = false;
  510. // _clean the divs
  511. if (this.guiEnabled === true) {
  512. util.recursiveDOMDelete(this.editModeDiv);
  513. util.recursiveDOMDelete(this.manipulationDiv);
  514. // removes all the bindings and overloads
  515. this._cleanManipulatorHammers();
  516. }
  517. // remove temporary nodes and edges
  518. this._cleanupTemporaryNodesAndEdges();
  519. // restore overloaded UI functions
  520. this._unbindTemporaryUIs();
  521. // remove the temporaryEventFunctions
  522. this._unbindTemporaryEvents();
  523. // restore the physics if required
  524. this.body.emitter.emit('restorePhysics');
  525. }
  526. /**
  527. * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up.
  528. * @private
  529. */
  530. _cleanManipulatorHammers() {
  531. // _clean hammer bindings
  532. if (this.manipulationHammers.length != 0) {
  533. for (let i = 0; i < this.manipulationHammers.length; i++) {
  534. this.manipulationHammers[i].destroy();
  535. }
  536. this.manipulationHammers = [];
  537. }
  538. }
  539. /**
  540. * Remove all DOM elements created by this module.
  541. * @private
  542. */
  543. _removeManipulationDOM() {
  544. // removes all the bindings and overloads
  545. this._clean();
  546. // empty the manipulation divs
  547. util.recursiveDOMDelete(this.manipulationDiv);
  548. util.recursiveDOMDelete(this.editModeDiv);
  549. util.recursiveDOMDelete(this.closeDiv);
  550. // remove the manipulation divs
  551. if (this.manipulationDiv) {this.canvas.frame.removeChild(this.manipulationDiv);}
  552. if (this.editModeDiv) {this.canvas.frame.removeChild(this.editModeDiv);}
  553. if (this.closeDiv) {this.canvas.frame.removeChild(this.closeDiv);}
  554. // set the references to undefined
  555. this.manipulationDiv = undefined;
  556. this.editModeDiv = undefined;
  557. this.closeDiv = undefined;
  558. }
  559. /**
  560. * create a seperator line. the index is to differentiate in the manipulation dom
  561. * @param index
  562. * @private
  563. */
  564. _createSeperator(index = 1) {
  565. this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div');
  566. this.manipulationDOM['seperatorLineDiv' + index].className = 'vis-separator-line';
  567. this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]);
  568. }
  569. // ---------------------- DOM functions for buttons --------------------------//
  570. _createAddNodeButton(locale) {
  571. let button = this._createButton('addNode', 'vis-button vis-add', locale['addNode'] || this.options.locales['en']['addNode']);
  572. this.manipulationDiv.appendChild(button);
  573. this._bindHammerToDiv(button, this.addNodeMode.bind(this));
  574. }
  575. _createAddEdgeButton(locale) {
  576. let button = this._createButton('addEdge', 'vis-button vis-connect', locale['addEdge'] || this.options.locales['en']['addEdge']);
  577. this.manipulationDiv.appendChild(button);
  578. this._bindHammerToDiv(button, this.addEdgeMode.bind(this));
  579. }
  580. _createEditNodeButton(locale) {
  581. let button = this._createButton('editNode', 'vis-button vis-edit', locale['editNode'] || this.options.locales['en']['editNode']);
  582. this.manipulationDiv.appendChild(button);
  583. this._bindHammerToDiv(button, this.editNode.bind(this));
  584. }
  585. _createEditEdgeButton(locale) {
  586. let button = this._createButton('editEdge', 'vis-button vis-edit', locale['editEdge'] || this.options.locales['en']['editEdge']);
  587. this.manipulationDiv.appendChild(button);
  588. this._bindHammerToDiv(button, this.editEdgeMode.bind(this));
  589. }
  590. _createDeleteButton(locale) {
  591. var deleteBtnClass;
  592. if (this.options.rtl) {
  593. deleteBtnClass = 'vis-button vis-delete-rtl';
  594. } else {
  595. deleteBtnClass = 'vis-button vis-delete';
  596. }
  597. let button = this._createButton('delete', deleteBtnClass, locale['del'] || this.options.locales['en']['del']);
  598. this.manipulationDiv.appendChild(button);
  599. this._bindHammerToDiv(button, this.deleteSelected.bind(this));
  600. }
  601. _createBackButton(locale) {
  602. let button = this._createButton('back', 'vis-button vis-back', locale['back'] || this.options.locales['en']['back']);
  603. this.manipulationDiv.appendChild(button);
  604. this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this));
  605. }
  606. _createButton(id, className, label, labelClassName = 'vis-label') {
  607. this.manipulationDOM[id+'Div'] = document.createElement('div');
  608. this.manipulationDOM[id+'Div'].className = className;
  609. this.manipulationDOM[id+'Label'] = document.createElement('div');
  610. this.manipulationDOM[id+'Label'].className = labelClassName;
  611. this.manipulationDOM[id+'Label'].innerHTML = label;
  612. this.manipulationDOM[id+'Div'].appendChild(this.manipulationDOM[id+'Label']);
  613. return this.manipulationDOM[id+'Div'];
  614. }
  615. _createDescription(label) {
  616. this.manipulationDiv.appendChild(
  617. this._createButton('description', 'vis-button vis-none', label)
  618. );
  619. }
  620. // -------------------------- End of DOM functions for buttons ------------------------------//
  621. /**
  622. * this binds an event until cleanup by the clean functions.
  623. * @param event
  624. * @param newFunction
  625. * @private
  626. */
  627. _temporaryBindEvent(event, newFunction) {
  628. this.temporaryEventFunctions.push({event:event, boundFunction:newFunction});
  629. this.body.emitter.on(event, newFunction);
  630. }
  631. /**
  632. * this overrides an UI function until cleanup by the clean function
  633. * @param UIfunctionName
  634. * @param newFunction
  635. * @private
  636. */
  637. _temporaryBindUI(UIfunctionName, newFunction) {
  638. if (this.body.eventListeners[UIfunctionName] !== undefined) {
  639. this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName];
  640. this.body.eventListeners[UIfunctionName] = newFunction;
  641. }
  642. else {
  643. throw new Error('This UI function does not exist. Typo? You tried: ' + UIfunctionName + ' possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners)));
  644. }
  645. }
  646. /**
  647. * Restore the overridden UI functions to their original state.
  648. *
  649. * @private
  650. */
  651. _unbindTemporaryUIs() {
  652. for (let functionName in this.temporaryUIFunctions) {
  653. if (this.temporaryUIFunctions.hasOwnProperty(functionName)) {
  654. this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName];
  655. delete this.temporaryUIFunctions[functionName];
  656. }
  657. }
  658. this.temporaryUIFunctions = {};
  659. }
  660. /**
  661. * Unbind the events created by _temporaryBindEvent
  662. * @private
  663. */
  664. _unbindTemporaryEvents() {
  665. for (let i = 0; i < this.temporaryEventFunctions.length; i++) {
  666. let eventName = this.temporaryEventFunctions[i].event;
  667. let boundFunction = this.temporaryEventFunctions[i].boundFunction;
  668. this.body.emitter.off(eventName, boundFunction);
  669. }
  670. this.temporaryEventFunctions = [];
  671. }
  672. /**
  673. * Bind an hammer instance to a DOM element.
  674. * @param domElement
  675. * @param funct
  676. */
  677. _bindHammerToDiv(domElement, boundFunction) {
  678. let hammer = new Hammer(domElement, {});
  679. hammerUtil.onTouch(hammer, boundFunction);
  680. this.manipulationHammers.push(hammer);
  681. }
  682. /**
  683. * Neatly clean up temporary edges and nodes
  684. * @private
  685. */
  686. _cleanupTemporaryNodesAndEdges() {
  687. // _clean temporary edges
  688. for (let i = 0; i < this.temporaryIds.edges.length; i++) {
  689. this.body.edges[this.temporaryIds.edges[i]].disconnect();
  690. delete this.body.edges[this.temporaryIds.edges[i]];
  691. let indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]);
  692. if (indexTempEdge !== -1) {this.body.edgeIndices.splice(indexTempEdge,1);}
  693. }
  694. // _clean temporary nodes
  695. for (let i = 0; i < this.temporaryIds.nodes.length; i++) {
  696. delete this.body.nodes[this.temporaryIds.nodes[i]];
  697. let indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]);
  698. if (indexTempNode !== -1) {this.body.nodeIndices.splice(indexTempNode,1);}
  699. }
  700. this.temporaryIds = {nodes: [], edges: []};
  701. }
  702. // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------//
  703. /**
  704. * the touch is used to get the position of the initial click
  705. * @param event
  706. * @private
  707. */
  708. _controlNodeTouch(event) {
  709. this.selectionHandler.unselectAll();
  710. this.lastTouch = this.body.functions.getPointer(event.center);
  711. this.lastTouch.translation = util.extend({},this.body.view.translation); // copy the object
  712. }
  713. /**
  714. * the drag start is used to mark one of the control nodes as selected.
  715. * @param event
  716. * @private
  717. */
  718. _controlNodeDragStart(event) { // eslint-disable-line no-unused-vars
  719. let pointer = this.lastTouch;
  720. let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
  721. let from = this.body.nodes[this.temporaryIds.nodes[0]];
  722. let to = this.body.nodes[this.temporaryIds.nodes[1]];
  723. let edge = this.body.edges[this.edgeBeingEditedId];
  724. this.selectedControlNode = undefined;
  725. let fromSelect = from.isOverlappingWith(pointerObj);
  726. let toSelect = to.isOverlappingWith(pointerObj);
  727. if (fromSelect === true) {
  728. this.selectedControlNode = from;
  729. edge.edgeType.from = from;
  730. }
  731. else if (toSelect === true) {
  732. this.selectedControlNode = to;
  733. edge.edgeType.to = to;
  734. }
  735. // we use the selection to find the node that is being dragged. We explicitly select it here.
  736. if (this.selectedControlNode !== undefined) {
  737. this.selectionHandler.selectObject(this.selectedControlNode)
  738. }
  739. this.body.emitter.emit('_redraw');
  740. }
  741. /**
  742. * dragging the control nodes or the canvas
  743. * @param event
  744. * @private
  745. */
  746. _controlNodeDrag(event) {
  747. this.body.emitter.emit('disablePhysics');
  748. let pointer = this.body.functions.getPointer(event.center);
  749. let pos = this.canvas.DOMtoCanvas(pointer);
  750. if (this.selectedControlNode !== undefined) {
  751. this.selectedControlNode.x = pos.x;
  752. this.selectedControlNode.y = pos.y;
  753. }
  754. else {
  755. // if the drag was not started properly because the click started outside the network div, start it now.
  756. let diffX = pointer.x - this.lastTouch.x;
  757. let diffY = pointer.y - this.lastTouch.y;
  758. this.body.view.translation = {x:this.lastTouch.translation.x + diffX, y:this.lastTouch.translation.y + diffY};
  759. }
  760. this.body.emitter.emit('_redraw');
  761. }
  762. /**
  763. * connecting or restoring the control nodes.
  764. * @param event
  765. * @private
  766. */
  767. _controlNodeDragEnd(event) {
  768. let pointer = this.body.functions.getPointer(event.center);
  769. let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
  770. let edge = this.body.edges[this.edgeBeingEditedId];
  771. // if the node that was dragged is not a control node, return
  772. if (this.selectedControlNode === undefined) {
  773. return;
  774. }
  775. // we use the selection to find the node that is being dragged. We explicitly DEselect the control node here.
  776. this.selectionHandler.unselectAll();
  777. let overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
  778. let node = undefined;
  779. for (let i = overlappingNodeIds.length-1; i >= 0; i--) {
  780. if (overlappingNodeIds[i] !== this.selectedControlNode.id) {
  781. node = this.body.nodes[overlappingNodeIds[i]];
  782. break;
  783. }
  784. }
  785. // perform the connection
  786. if (node !== undefined && this.selectedControlNode !== undefined) {
  787. if (node.isCluster === true) {
  788. alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError'])
  789. }
  790. else {
  791. let from = this.body.nodes[this.temporaryIds.nodes[0]];
  792. if (this.selectedControlNode.id === from.id) {
  793. this._performEditEdge(node.id, edge.to.id);
  794. }
  795. else {
  796. this._performEditEdge(edge.from.id, node.id);
  797. }
  798. }
  799. }
  800. else {
  801. edge.updateEdgeType();
  802. this.body.emitter.emit('restorePhysics');
  803. }
  804. this.body.emitter.emit('_redraw');
  805. }
  806. // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------//
  807. // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------//
  808. /**
  809. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  810. * to walk the user through the process.
  811. *
  812. * @private
  813. */
  814. _handleConnect(event) {
  815. // check to avoid double fireing of this function.
  816. if (new Date().valueOf() - this.touchTime > 100) {
  817. this.lastTouch = this.body.functions.getPointer(event.center);
  818. this.lastTouch.translation = util.extend({},this.body.view.translation); // copy the object
  819. let pointer = this.lastTouch;
  820. let node = this.selectionHandler.getNodeAt(pointer);
  821. if (node !== undefined) {
  822. if (node.isCluster === true) {
  823. alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError'])
  824. }
  825. else {
  826. // create a node the temporary line can look at
  827. let targetNode = this._getNewTargetNode(node.x,node.y);
  828. this.body.nodes[targetNode.id] = targetNode;
  829. this.body.nodeIndices.push(targetNode.id);
  830. // create a temporary edge
  831. let connectionEdge = this.body.functions.createEdge({
  832. id: 'connectionEdge' + util.randomUUID(),
  833. from: node.id,
  834. to: targetNode.id,
  835. physics: false,
  836. smooth: {
  837. enabled: true,
  838. type: 'continuous',
  839. roundness: 0.5
  840. }
  841. });
  842. this.body.edges[connectionEdge.id] = connectionEdge;
  843. this.body.edgeIndices.push(connectionEdge.id);
  844. this.temporaryIds.nodes.push(targetNode.id);
  845. this.temporaryIds.edges.push(connectionEdge.id);
  846. }
  847. }
  848. this.touchTime = new Date().valueOf();
  849. }
  850. }
  851. _dragControlNode(event) {
  852. let pointer = this.body.functions.getPointer(event.center);
  853. if (this.temporaryIds.nodes[0] !== undefined) {
  854. let targetNode = this.body.nodes[this.temporaryIds.nodes[0]]; // there is only one temp node in the add edge mode.
  855. targetNode.x = this.canvas._XconvertDOMtoCanvas(pointer.x);
  856. targetNode.y = this.canvas._YconvertDOMtoCanvas(pointer.y);
  857. this.body.emitter.emit('_redraw');
  858. }
  859. else {
  860. let diffX = pointer.x - this.lastTouch.x;
  861. let diffY = pointer.y - this.lastTouch.y;
  862. this.body.view.translation = {x:this.lastTouch.translation.x + diffX, y:this.lastTouch.translation.y + diffY};
  863. }
  864. }
  865. /**
  866. * Connect the new edge to the target if one exists, otherwise remove temp line
  867. * @param event
  868. * @private
  869. */
  870. _finishConnect(event) {
  871. let pointer = this.body.functions.getPointer(event.center);
  872. let pointerObj = this.selectionHandler._pointerToPositionObject(pointer);
  873. // remember the edge id
  874. let connectFromId = undefined;
  875. if (this.temporaryIds.edges[0] !== undefined) {
  876. connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId;
  877. }
  878. // get the overlapping node but NOT the temporary node;
  879. let overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj);
  880. let node = undefined;
  881. for (let i = overlappingNodeIds.length-1; i >= 0; i--) {
  882. // if the node id is NOT a temporary node, accept the node.
  883. if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) === -1) {
  884. node = this.body.nodes[overlappingNodeIds[i]];
  885. break;
  886. }
  887. }
  888. // clean temporary nodes and edges.
  889. this._cleanupTemporaryNodesAndEdges();
  890. // perform the connection
  891. if (node !== undefined) {
  892. if (node.isCluster === true) {
  893. alert(this.options.locales[this.options.locale]['createEdgeError'] || this.options.locales['en']['createEdgeError']);
  894. }
  895. else {
  896. if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) {
  897. this._performAddEdge(connectFromId, node.id);
  898. }
  899. }
  900. }
  901. // No need to do _generateclickevent('dragEnd') here, the regular dragEnd event fires.
  902. this.body.emitter.emit('_redraw');
  903. }
  904. _dragStartEdge(event) {
  905. let pointer = this.lastTouch;
  906. this.selectionHandler._generateClickEvent('dragStart', event, pointer, undefined, true);
  907. }
  908. // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------//
  909. // ------------------------------ Performing all the actual data manipulation ------------------------//
  910. /**
  911. * Adds a node on the specified location
  912. */
  913. _performAddNode(clickData) {
  914. let defaultData = {
  915. id: util.randomUUID(),
  916. x: clickData.pointer.canvas.x,
  917. y: clickData.pointer.canvas.y,
  918. label: 'new'
  919. };
  920. if (typeof this.options.addNode === 'function') {
  921. if (this.options.addNode.length === 2) {
  922. this.options.addNode(defaultData, (finalizedData) => {
  923. if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'addNode') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback
  924. this.body.data.nodes.getDataSet().add(finalizedData);
  925. this.showManipulatorToolbar();
  926. }
  927. });
  928. }
  929. else {
  930. this.showManipulatorToolbar();
  931. throw new Error('The function for add does not support two arguments (data,callback)');
  932. }
  933. }
  934. else {
  935. this.body.data.nodes.getDataSet().add(defaultData);
  936. this.showManipulatorToolbar();
  937. }
  938. }
  939. /**
  940. * connect two nodes with a new edge.
  941. *
  942. * @private
  943. */
  944. _performAddEdge(sourceNodeId, targetNodeId) {
  945. let defaultData = {from: sourceNodeId, to: targetNodeId};
  946. if (typeof this.options.addEdge === 'function') {
  947. if (this.options.addEdge.length === 2) {
  948. this.options.addEdge(defaultData, (finalizedData) => {
  949. if (finalizedData !== null && finalizedData !== undefined && this.inMode === 'addEdge') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback
  950. this.body.data.edges.getDataSet().add(finalizedData);
  951. this.selectionHandler.unselectAll();
  952. this.showManipulatorToolbar();
  953. }
  954. });
  955. }
  956. else {
  957. throw new Error('The function for connect does not support two arguments (data,callback)');
  958. }
  959. }
  960. else {
  961. this.body.data.edges.getDataSet().add(defaultData);
  962. this.selectionHandler.unselectAll();
  963. this.showManipulatorToolbar();
  964. }
  965. }
  966. /**
  967. * connect two nodes with a new edge.
  968. *
  969. * @private
  970. */
  971. _performEditEdge(sourceNodeId, targetNodeId) {
  972. let defaultData = {id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId, label: this.body.data.edges._data[this.edgeBeingEditedId].label };
  973. let eeFunct = this.options.editEdge;
  974. if (typeof eeFunct === 'object') {
  975. eeFunct = eeFunct.editWithoutDrag;
  976. }
  977. if (typeof eeFunct === 'function') {
  978. if (eeFunct.length === 2) {
  979. eeFunct(defaultData, (finalizedData) => {
  980. if (finalizedData === null || finalizedData === undefined || this.inMode !== 'editEdge') { // if for whatever reason the mode has changes (due to dataset change) disregard the callback) {
  981. this.body.edges[defaultData.id].updateEdgeType();
  982. this.body.emitter.emit('_redraw');
  983. this.showManipulatorToolbar();
  984. }
  985. else {
  986. this.body.data.edges.getDataSet().update(finalizedData);
  987. this.selectionHandler.unselectAll();
  988. this.showManipulatorToolbar();
  989. }
  990. });
  991. }
  992. else {
  993. throw new Error('The function for edit does not support two arguments (data, callback)');
  994. }
  995. }
  996. else {
  997. this.body.data.edges.getDataSet().update(defaultData);
  998. this.selectionHandler.unselectAll();
  999. this.showManipulatorToolbar();
  1000. }
  1001. }
  1002. }
  1003. export default ManipulationSystem;