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.

1023 lines
32 KiB

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