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.

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