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.

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