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.

595 lines
20 KiB

  1. var util = require('../../util');
  2. var Node = require('../Node');
  3. var Edge = require('../Edge');
  4. /**
  5. * clears the toolbar div element of children
  6. *
  7. * @private
  8. */
  9. exports._clearManipulatorBar = function() {
  10. while (this.manipulationDiv.hasChildNodes()) {
  11. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  12. }
  13. this._manipulationReleaseOverload = function () {};
  14. delete this.sectors['support']['nodes']['targetNode'];
  15. delete this.sectors['support']['nodes']['targetViaNode'];
  16. this.controlNodesActive = false;
  17. };
  18. /**
  19. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  20. * these functions to their original functionality, we saved them in this.cachedFunctions.
  21. * This function restores these functions to their original function.
  22. *
  23. * @private
  24. */
  25. exports._restoreOverloadedFunctions = function() {
  26. for (var functionName in this.cachedFunctions) {
  27. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  28. this[functionName] = this.cachedFunctions[functionName];
  29. }
  30. }
  31. };
  32. /**
  33. * Enable or disable edit-mode.
  34. *
  35. * @private
  36. */
  37. exports._toggleEditMode = function() {
  38. this.editMode = !this.editMode;
  39. var toolbar = document.getElementById("network-manipulationDiv");
  40. var closeDiv = document.getElementById("network-manipulation-closeDiv");
  41. var editModeDiv = document.getElementById("network-manipulation-editMode");
  42. if (this.editMode == true) {
  43. toolbar.style.display="block";
  44. closeDiv.style.display="block";
  45. editModeDiv.style.display="none";
  46. closeDiv.onclick = this._toggleEditMode.bind(this);
  47. }
  48. else {
  49. toolbar.style.display="none";
  50. closeDiv.style.display="none";
  51. editModeDiv.style.display="block";
  52. closeDiv.onclick = null;
  53. }
  54. this._createManipulatorBar()
  55. };
  56. /**
  57. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  58. *
  59. * @private
  60. */
  61. exports._createManipulatorBar = function() {
  62. // remove bound functions
  63. if (this.boundFunction) {
  64. this.off('select', this.boundFunction);
  65. }
  66. var locale = this.constants.locales[this.constants.locale];
  67. if (this.edgeBeingEdited !== undefined) {
  68. this.edgeBeingEdited._disableControlNodes();
  69. this.edgeBeingEdited = undefined;
  70. this.selectedControlNode = null;
  71. this.controlNodesActive = false;
  72. }
  73. // restore overloaded functions
  74. this._restoreOverloadedFunctions();
  75. // resume calculation
  76. this.freezeSimulation = false;
  77. // reset global variables
  78. this.blockConnectingEdgeSelection = false;
  79. this.forceAppendSelection = false;
  80. if (this.editMode == true) {
  81. while (this.manipulationDiv.hasChildNodes()) {
  82. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  83. }
  84. // add the icons to the manipulator div
  85. this.manipulationDiv.innerHTML = "" +
  86. "<span class='network-manipulationUI add' id='network-manipulate-addNode'>" +
  87. "<span class='network-manipulationLabel'>"+locale['addNode'] +"</span></span>" +
  88. "<div class='network-seperatorLine'></div>" +
  89. "<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" +
  90. "<span class='network-manipulationLabel'>"+locale['addEdge'] +"</span></span>";
  91. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  92. this.manipulationDiv.innerHTML += "" +
  93. "<div class='network-seperatorLine'></div>" +
  94. "<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" +
  95. "<span class='network-manipulationLabel'>"+locale['editNode'] +"</span></span>";
  96. }
  97. else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
  98. this.manipulationDiv.innerHTML += "" +
  99. "<div class='network-seperatorLine'></div>" +
  100. "<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" +
  101. "<span class='network-manipulationLabel'>"+locale['editEdge'] +"</span></span>";
  102. }
  103. if (this._selectionIsEmpty() == false) {
  104. this.manipulationDiv.innerHTML += "" +
  105. "<div class='network-seperatorLine'></div>" +
  106. "<span class='network-manipulationUI delete' id='network-manipulate-delete'>" +
  107. "<span class='network-manipulationLabel'>"+locale['del'] +"</span></span>";
  108. }
  109. // bind the icons
  110. var addNodeButton = document.getElementById("network-manipulate-addNode");
  111. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  112. var addEdgeButton = document.getElementById("network-manipulate-connectNode");
  113. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  114. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  115. var editButton = document.getElementById("network-manipulate-editNode");
  116. editButton.onclick = this._editNode.bind(this);
  117. }
  118. else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
  119. var editButton = document.getElementById("network-manipulate-editEdge");
  120. editButton.onclick = this._createEditEdgeToolbar.bind(this);
  121. }
  122. if (this._selectionIsEmpty() == false) {
  123. var deleteButton = document.getElementById("network-manipulate-delete");
  124. deleteButton.onclick = this._deleteSelected.bind(this);
  125. }
  126. var closeDiv = document.getElementById("network-manipulation-closeDiv");
  127. closeDiv.onclick = this._toggleEditMode.bind(this);
  128. this.boundFunction = this._createManipulatorBar.bind(this);
  129. this.on('select', this.boundFunction);
  130. }
  131. else {
  132. this.editModeDiv.innerHTML = "" +
  133. "<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" +
  134. "<span class='network-manipulationLabel'>" + locale['edit'] + "</span></span>";
  135. var editModeButton = document.getElementById("network-manipulate-editModeButton");
  136. editModeButton.onclick = this._toggleEditMode.bind(this);
  137. }
  138. };
  139. /**
  140. * Create the toolbar for adding Nodes
  141. *
  142. * @private
  143. */
  144. exports._createAddNodeToolbar = function() {
  145. // clear the toolbar
  146. this._clearManipulatorBar();
  147. if (this.boundFunction) {
  148. this.off('select', this.boundFunction);
  149. }
  150. var locale = this.constants.locales[this.constants.locale];
  151. // create the toolbar contents
  152. this.manipulationDiv.innerHTML = "" +
  153. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  154. "<span class='network-manipulationLabel'>" + locale['back'] + " </span></span>" +
  155. "<div class='network-seperatorLine'></div>" +
  156. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  157. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + locale['addDescription'] + "</span></span>";
  158. // bind the icon
  159. var backButton = document.getElementById("network-manipulate-back");
  160. backButton.onclick = this._createManipulatorBar.bind(this);
  161. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  162. this.boundFunction = this._addNode.bind(this);
  163. this.on('select', this.boundFunction);
  164. };
  165. /**
  166. * create the toolbar to connect nodes
  167. *
  168. * @private
  169. */
  170. exports._createAddEdgeToolbar = function() {
  171. // clear the toolbar
  172. this._clearManipulatorBar();
  173. this._unselectAll(true);
  174. this.freezeSimulation = true;
  175. var locale = this.constants.locales[this.constants.locale];
  176. if (this.boundFunction) {
  177. this.off('select', this.boundFunction);
  178. }
  179. this._unselectAll();
  180. this.forceAppendSelection = false;
  181. this.blockConnectingEdgeSelection = true;
  182. this.manipulationDiv.innerHTML = "" +
  183. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  184. "<span class='network-manipulationLabel'>" + locale['back'] + " </span></span>" +
  185. "<div class='network-seperatorLine'></div>" +
  186. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  187. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + locale['edgeDescription'] + "</span></span>";
  188. // bind the icon
  189. var backButton = document.getElementById("network-manipulate-back");
  190. backButton.onclick = this._createManipulatorBar.bind(this);
  191. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  192. this.boundFunction = this._handleConnect.bind(this);
  193. this.on('select', this.boundFunction);
  194. // temporarily overload functions
  195. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  196. this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload;
  197. this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
  198. this.cachedFunctions["_handleDragEnd"] = this._handleDragEnd;
  199. this._handleTouch = this._handleConnect;
  200. this._manipulationReleaseOverload = function () {};
  201. this._handleDragStart = function () {};
  202. this._handleDragEnd = this._finishConnect;
  203. // redraw to show the unselect
  204. this._redraw();
  205. };
  206. /**
  207. * create the toolbar to edit edges
  208. *
  209. * @private
  210. */
  211. exports._createEditEdgeToolbar = function() {
  212. // clear the toolbar
  213. this._clearManipulatorBar();
  214. this.controlNodesActive = true;
  215. if (this.boundFunction) {
  216. this.off('select', this.boundFunction);
  217. }
  218. this.edgeBeingEdited = this._getSelectedEdge();
  219. this.edgeBeingEdited._enableControlNodes();
  220. var locale = this.constants.locales[this.constants.locale];
  221. this.manipulationDiv.innerHTML = "" +
  222. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  223. "<span class='network-manipulationLabel'>" + locale['back'] + " </span></span>" +
  224. "<div class='network-seperatorLine'></div>" +
  225. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  226. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + locale['editEdgeDescription'] + "</span></span>";
  227. // bind the icon
  228. var backButton = document.getElementById("network-manipulate-back");
  229. backButton.onclick = this._createManipulatorBar.bind(this);
  230. // temporarily overload functions
  231. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  232. this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload;
  233. this.cachedFunctions["_handleTap"] = this._handleTap;
  234. this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
  235. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  236. this._handleTouch = this._selectControlNode;
  237. this._handleTap = function () {};
  238. this._handleOnDrag = this._controlNodeDrag;
  239. this._handleDragStart = function () {}
  240. this._manipulationReleaseOverload = this._releaseControlNode;
  241. // redraw to show the unselect
  242. this._redraw();
  243. };
  244. /**
  245. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  246. * to walk the user through the process.
  247. *
  248. * @private
  249. */
  250. exports._selectControlNode = function(pointer) {
  251. this.edgeBeingEdited.controlNodes.from.unselect();
  252. this.edgeBeingEdited.controlNodes.to.unselect();
  253. this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
  254. if (this.selectedControlNode !== null) {
  255. this.selectedControlNode.select();
  256. this.freezeSimulation = true;
  257. }
  258. this._redraw();
  259. };
  260. /**
  261. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  262. * to walk the user through the process.
  263. *
  264. * @private
  265. */
  266. exports._controlNodeDrag = function(event) {
  267. var pointer = this._getPointer(event.gesture.center);
  268. if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
  269. this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
  270. this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
  271. }
  272. this._redraw();
  273. };
  274. exports._releaseControlNode = function(pointer) {
  275. var newNode = this._getNodeAt(pointer);
  276. if (newNode != null) {
  277. if (this.edgeBeingEdited.controlNodes.from.selected == true) {
  278. this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
  279. this.edgeBeingEdited.controlNodes.from.unselect();
  280. }
  281. if (this.edgeBeingEdited.controlNodes.to.selected == true) {
  282. this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
  283. this.edgeBeingEdited.controlNodes.to.unselect();
  284. }
  285. }
  286. else {
  287. this.edgeBeingEdited._restoreControlNodes();
  288. }
  289. this.freezeSimulation = false;
  290. this._redraw();
  291. };
  292. /**
  293. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  294. * to walk the user through the process.
  295. *
  296. * @private
  297. */
  298. exports._handleConnect = function(pointer) {
  299. if (this._getSelectedNodeCount() == 0) {
  300. var node = this._getNodeAt(pointer);
  301. if (node != null) {
  302. if (node.clusterSize > 1) {
  303. alert(this.constants.locales[this.constants.locale]['createEdgeError'])
  304. }
  305. else {
  306. this._selectObject(node,false);
  307. var supportNodes = this.sectors['support']['nodes'];
  308. // create a node the temporary line can look at
  309. supportNodes['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  310. var targetNode = supportNodes['targetNode'];
  311. targetNode.x = node.x;
  312. targetNode.y = node.y;
  313. // create a temporary edge
  314. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants);
  315. var connectionEdge = this.edges['connectionEdge'];
  316. connectionEdge.from = node;
  317. connectionEdge.connected = true;
  318. connectionEdge.options.smoothCurves = {enabled: true,
  319. dynamic: false,
  320. type: "continuous",
  321. roundness: 0.5
  322. };
  323. connectionEdge.selected = true;
  324. connectionEdge.to = targetNode;
  325. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  326. this._handleOnDrag = function(event) {
  327. var pointer = this._getPointer(event.gesture.center);
  328. var connectionEdge = this.edges['connectionEdge'];
  329. connectionEdge.to.x = this._XconvertDOMtoCanvas(pointer.x);
  330. connectionEdge.to.y = this._YconvertDOMtoCanvas(pointer.y);
  331. };
  332. this.moving = true;
  333. this.start();
  334. }
  335. }
  336. }
  337. };
  338. exports._finishConnect = function(event) {
  339. if (this._getSelectedNodeCount() == 1) {
  340. var pointer = this._getPointer(event.gesture.center);
  341. // restore the drag function
  342. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  343. delete this.cachedFunctions["_handleOnDrag"];
  344. // remember the edge id
  345. var connectFromId = this.edges['connectionEdge'].fromId;
  346. // remove the temporary nodes and edge
  347. delete this.edges['connectionEdge'];
  348. delete this.sectors['support']['nodes']['targetNode'];
  349. delete this.sectors['support']['nodes']['targetViaNode'];
  350. var node = this._getNodeAt(pointer);
  351. if (node != null) {
  352. if (node.clusterSize > 1) {
  353. alert(this.constants.locales[this.constants.locale]["createEdgeError"])
  354. }
  355. else {
  356. this._createEdge(connectFromId,node.id);
  357. this._createManipulatorBar();
  358. }
  359. }
  360. this._unselectAll();
  361. }
  362. };
  363. /**
  364. * Adds a node on the specified location
  365. */
  366. exports._addNode = function() {
  367. if (this._selectionIsEmpty() && this.editMode == true) {
  368. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  369. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  370. if (this.triggerFunctions.add) {
  371. if (this.triggerFunctions.add.length == 2) {
  372. var me = this;
  373. this.triggerFunctions.add(defaultData, function(finalizedData) {
  374. me.nodesData.add(finalizedData);
  375. me._createManipulatorBar();
  376. me.moving = true;
  377. me.start();
  378. });
  379. }
  380. else {
  381. throw new Error('The function for add does not support two arguments (data,callback)');
  382. this._createManipulatorBar();
  383. this.moving = true;
  384. this.start();
  385. }
  386. }
  387. else {
  388. this.nodesData.add(defaultData);
  389. this._createManipulatorBar();
  390. this.moving = true;
  391. this.start();
  392. }
  393. }
  394. };
  395. /**
  396. * connect two nodes with a new edge.
  397. *
  398. * @private
  399. */
  400. exports._createEdge = function(sourceNodeId,targetNodeId) {
  401. if (this.editMode == true) {
  402. var defaultData = {from:sourceNodeId, to:targetNodeId};
  403. if (this.triggerFunctions.connect) {
  404. if (this.triggerFunctions.connect.length == 2) {
  405. var me = this;
  406. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  407. me.edgesData.add(finalizedData);
  408. me.moving = true;
  409. me.start();
  410. });
  411. }
  412. else {
  413. throw new Error('The function for connect does not support two arguments (data,callback)');
  414. this.moving = true;
  415. this.start();
  416. }
  417. }
  418. else {
  419. this.edgesData.add(defaultData);
  420. this.moving = true;
  421. this.start();
  422. }
  423. }
  424. };
  425. /**
  426. * connect two nodes with a new edge.
  427. *
  428. * @private
  429. */
  430. exports._editEdge = function(sourceNodeId,targetNodeId) {
  431. if (this.editMode == true) {
  432. var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
  433. if (this.triggerFunctions.editEdge) {
  434. if (this.triggerFunctions.editEdge.length == 2) {
  435. var me = this;
  436. this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
  437. me.edgesData.update(finalizedData);
  438. me.moving = true;
  439. me.start();
  440. });
  441. }
  442. else {
  443. throw new Error('The function for edit does not support two arguments (data, callback)');
  444. this.moving = true;
  445. this.start();
  446. }
  447. }
  448. else {
  449. this.edgesData.update(defaultData);
  450. this.moving = true;
  451. this.start();
  452. }
  453. }
  454. };
  455. /**
  456. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  457. *
  458. * @private
  459. */
  460. exports._editNode = function() {
  461. if (this.triggerFunctions.edit && this.editMode == true) {
  462. var node = this._getSelectedNode();
  463. var data = {id:node.id,
  464. label: node.label,
  465. group: node.options.group,
  466. shape: node.options.shape,
  467. color: {
  468. background:node.options.color.background,
  469. border:node.options.color.border,
  470. highlight: {
  471. background:node.options.color.highlight.background,
  472. border:node.options.color.highlight.border
  473. }
  474. }};
  475. if (this.triggerFunctions.edit.length == 2) {
  476. var me = this;
  477. this.triggerFunctions.edit(data, function (finalizedData) {
  478. me.nodesData.update(finalizedData);
  479. me._createManipulatorBar();
  480. me.moving = true;
  481. me.start();
  482. });
  483. }
  484. else {
  485. throw new Error('The function for edit does not support two arguments (data, callback)');
  486. }
  487. }
  488. else {
  489. throw new Error('No edit function has been bound to this button');
  490. }
  491. };
  492. /**
  493. * delete everything in the selection
  494. *
  495. * @private
  496. */
  497. exports._deleteSelected = function() {
  498. if (!this._selectionIsEmpty() && this.editMode == true) {
  499. if (!this._clusterInSelection()) {
  500. var selectedNodes = this.getSelectedNodes();
  501. var selectedEdges = this.getSelectedEdges();
  502. if (this.triggerFunctions.del) {
  503. var me = this;
  504. var data = {nodes: selectedNodes, edges: selectedEdges};
  505. if (this.triggerFunctions.del.length = 2) {
  506. this.triggerFunctions.del(data, function (finalizedData) {
  507. me.edgesData.remove(finalizedData.edges);
  508. me.nodesData.remove(finalizedData.nodes);
  509. me._unselectAll();
  510. me.moving = true;
  511. me.start();
  512. });
  513. }
  514. else {
  515. throw new Error('The function for delete does not support two arguments (data, callback)')
  516. }
  517. }
  518. else {
  519. this.edgesData.remove(selectedEdges);
  520. this.nodesData.remove(selectedNodes);
  521. this._unselectAll();
  522. this.moving = true;
  523. this.start();
  524. }
  525. }
  526. else {
  527. alert(this.constants.locales[this.constants.locale]["deleteClusterError"]);
  528. }
  529. }
  530. };