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.

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