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.

591 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._handleTouch = this._handleConnect;
  194. this._handleOnRelease = this._finishConnect;
  195. // redraw to show the unselect
  196. this._redraw();
  197. };
  198. /**
  199. * create the toolbar to edit edges
  200. *
  201. * @private
  202. */
  203. exports._createEditEdgeToolbar = function() {
  204. // clear the toolbar
  205. this._clearManipulatorBar();
  206. this.controlNodesActive = true;
  207. if (this.boundFunction) {
  208. this.off('select', this.boundFunction);
  209. }
  210. this.edgeBeingEdited = this._getSelectedEdge();
  211. this.edgeBeingEdited._enableControlNodes();
  212. var locale = this.constants.locales[this.constants.locale];
  213. this.manipulationDiv.innerHTML = "" +
  214. "<span class='network-manipulationUI back' id='network-manipulate-back'>" +
  215. "<span class='network-manipulationLabel'>" + locale['back'] + " </span></span>" +
  216. "<div class='network-seperatorLine'></div>" +
  217. "<span class='network-manipulationUI none' id='network-manipulate-back'>" +
  218. "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + locale['editEdgeDescription'] + "</span></span>";
  219. // bind the icon
  220. var backButton = document.getElementById("network-manipulate-back");
  221. backButton.onclick = this._createManipulatorBar.bind(this);
  222. // temporarily overload functions
  223. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  224. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  225. this.cachedFunctions["_handleTap"] = this._handleTap;
  226. this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
  227. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  228. this._handleTouch = this._selectControlNode;
  229. this._handleTap = function () {};
  230. this._handleOnDrag = this._controlNodeDrag;
  231. this._handleDragStart = function () {}
  232. this._handleOnRelease = this._releaseControlNode;
  233. // redraw to show the unselect
  234. this._redraw();
  235. };
  236. /**
  237. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  238. * to walk the user through the process.
  239. *
  240. * @private
  241. */
  242. exports._selectControlNode = function(pointer) {
  243. this.edgeBeingEdited.controlNodes.from.unselect();
  244. this.edgeBeingEdited.controlNodes.to.unselect();
  245. this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
  246. if (this.selectedControlNode !== null) {
  247. this.selectedControlNode.select();
  248. this.freezeSimulation = true;
  249. }
  250. this._redraw();
  251. };
  252. /**
  253. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  254. * to walk the user through the process.
  255. *
  256. * @private
  257. */
  258. exports._controlNodeDrag = function(event) {
  259. var pointer = this._getPointer(event.gesture.center);
  260. if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
  261. this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
  262. this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
  263. }
  264. this._redraw();
  265. };
  266. exports._releaseControlNode = function(pointer) {
  267. var newNode = this._getNodeAt(pointer);
  268. if (newNode != null) {
  269. if (this.edgeBeingEdited.controlNodes.from.selected == true) {
  270. this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
  271. this.edgeBeingEdited.controlNodes.from.unselect();
  272. }
  273. if (this.edgeBeingEdited.controlNodes.to.selected == true) {
  274. this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
  275. this.edgeBeingEdited.controlNodes.to.unselect();
  276. }
  277. }
  278. else {
  279. this.edgeBeingEdited._restoreControlNodes();
  280. }
  281. this.freezeSimulation = false;
  282. this._redraw();
  283. };
  284. /**
  285. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  286. * to walk the user through the process.
  287. *
  288. * @private
  289. */
  290. exports._handleConnect = function(pointer) {
  291. if (this._getSelectedNodeCount() == 0) {
  292. var node = this._getNodeAt(pointer);
  293. var supportNodes, targetNode, targetViaNode, connectionEdge;
  294. if (node != null) {
  295. if (node.clusterSize > 1) {
  296. alert(this.constants.locales[this.constants.locale]['createEdgeError'])
  297. }
  298. else {
  299. this._selectObject(node,false);
  300. supportNodes = this.sectors['support']['nodes'];
  301. // create a node the temporary line can look at
  302. supportNodes['targetNode'] = targetNode = new Node({id:'targetNode'},{},{},this.constants);
  303. targetNode.x = node.x;
  304. targetNode.y = node.y;
  305. supportNodes['targetViaNode'] = targetViaNode = new Node({id:'targetViaNode'},{},{},this.constants);
  306. targetViaNode.x = node.x;
  307. targetViaNode.y = node.y;
  308. targetViaNode.parentEdgeId = "connectionEdge";
  309. // create a temporary edge
  310. this.edges['connectionEdge'] = connectionEdge = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants);
  311. connectionEdge.from = node;
  312. connectionEdge.connected = true;
  313. connectionEdge.smooth = true;
  314. connectionEdge.selected = true;
  315. connectionEdge.to = targetNode;
  316. connectionEdge.via = targetViaNode;
  317. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  318. this._handleOnDrag = function(event) {
  319. var pointer = this._getPointer(event.gesture.center);
  320. var supportNodes = this.sectors['support']['nodes'];
  321. supportNodes['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
  322. supportNodes['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  323. supportNodes['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
  324. supportNodes['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  325. };
  326. this.moving = true;
  327. this.start();
  328. }
  329. }
  330. }
  331. };
  332. exports._finishConnect = function(pointer) {
  333. if (this._getSelectedNodeCount() == 1) {
  334. // restore the drag function
  335. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  336. delete this.cachedFunctions["_handleOnDrag"];
  337. // remember the edge id
  338. var connectFromId = this.edges['connectionEdge'].fromId;
  339. // remove the temporary nodes and edge
  340. delete this.edges['connectionEdge'];
  341. delete this.sectors['support']['nodes']['targetNode'];
  342. delete this.sectors['support']['nodes']['targetViaNode'];
  343. var node = this._getNodeAt(pointer);
  344. if (node != null) {
  345. if (node.clusterSize > 1) {
  346. alert(this.constants.locales[this.constants.locale]["createEdgeError"])
  347. }
  348. else {
  349. this._createEdge(connectFromId,node.id);
  350. this._createManipulatorBar();
  351. }
  352. }
  353. this._unselectAll();
  354. }
  355. };
  356. /**
  357. * Adds a node on the specified location
  358. */
  359. exports._addNode = function() {
  360. if (this._selectionIsEmpty() && this.editMode == true) {
  361. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  362. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  363. if (this.triggerFunctions.add) {
  364. if (this.triggerFunctions.add.length == 2) {
  365. var me = this;
  366. this.triggerFunctions.add(defaultData, function(finalizedData) {
  367. me.nodesData.add(finalizedData);
  368. me._createManipulatorBar();
  369. me.moving = true;
  370. me.start();
  371. });
  372. }
  373. else {
  374. throw new Error('The function for add does not support two arguments (data,callback)');
  375. this._createManipulatorBar();
  376. this.moving = true;
  377. this.start();
  378. }
  379. }
  380. else {
  381. this.nodesData.add(defaultData);
  382. this._createManipulatorBar();
  383. this.moving = true;
  384. this.start();
  385. }
  386. }
  387. };
  388. /**
  389. * connect two nodes with a new edge.
  390. *
  391. * @private
  392. */
  393. exports._createEdge = function(sourceNodeId,targetNodeId) {
  394. if (this.editMode == true) {
  395. var defaultData = {from:sourceNodeId, to:targetNodeId};
  396. if (this.triggerFunctions.connect) {
  397. if (this.triggerFunctions.connect.length == 2) {
  398. var me = this;
  399. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  400. me.edgesData.add(finalizedData);
  401. me.moving = true;
  402. me.start();
  403. });
  404. }
  405. else {
  406. throw new Error('The function for connect does not support two arguments (data,callback)');
  407. this.moving = true;
  408. this.start();
  409. }
  410. }
  411. else {
  412. this.edgesData.add(defaultData);
  413. this.moving = true;
  414. this.start();
  415. }
  416. }
  417. };
  418. /**
  419. * connect two nodes with a new edge.
  420. *
  421. * @private
  422. */
  423. exports._editEdge = function(sourceNodeId,targetNodeId) {
  424. if (this.editMode == true) {
  425. var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
  426. if (this.triggerFunctions.editEdge) {
  427. if (this.triggerFunctions.editEdge.length == 2) {
  428. var me = this;
  429. this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
  430. me.edgesData.update(finalizedData);
  431. me.moving = true;
  432. me.start();
  433. });
  434. }
  435. else {
  436. throw new Error('The function for edit does not support two arguments (data, callback)');
  437. this.moving = true;
  438. this.start();
  439. }
  440. }
  441. else {
  442. this.edgesData.update(defaultData);
  443. this.moving = true;
  444. this.start();
  445. }
  446. }
  447. };
  448. /**
  449. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  450. *
  451. * @private
  452. */
  453. exports._editNode = function() {
  454. if (this.triggerFunctions.edit && this.editMode == true) {
  455. var node = this._getSelectedNode();
  456. var data = {id:node.id,
  457. label: node.label,
  458. group: node.options.group,
  459. shape: node.options.shape,
  460. color: {
  461. background:node.options.color.background,
  462. border:node.options.color.border,
  463. highlight: {
  464. background:node.options.color.highlight.background,
  465. border:node.options.color.highlight.border
  466. }
  467. }};
  468. if (this.triggerFunctions.edit.length == 2) {
  469. var me = this;
  470. this.triggerFunctions.edit(data, function (finalizedData) {
  471. me.nodesData.update(finalizedData);
  472. me._createManipulatorBar();
  473. me.moving = true;
  474. me.start();
  475. });
  476. }
  477. else {
  478. throw new Error('The function for edit does not support two arguments (data, callback)');
  479. }
  480. }
  481. else {
  482. throw new Error('No edit function has been bound to this button');
  483. }
  484. };
  485. /**
  486. * delete everything in the selection
  487. *
  488. * @private
  489. */
  490. exports._deleteSelected = function() {
  491. if (!this._selectionIsEmpty() && this.editMode == true) {
  492. if (!this._clusterInSelection()) {
  493. var selectedNodes = this.getSelectedNodes();
  494. var selectedEdges = this.getSelectedEdges();
  495. if (this.triggerFunctions.del) {
  496. var me = this;
  497. var data = {nodes: selectedNodes, edges: selectedEdges};
  498. if (this.triggerFunctions.del.length = 2) {
  499. this.triggerFunctions.del(data, function (finalizedData) {
  500. me.edgesData.remove(finalizedData.edges);
  501. me.nodesData.remove(finalizedData.nodes);
  502. me._unselectAll();
  503. me.moving = true;
  504. me.start();
  505. });
  506. }
  507. else {
  508. throw new Error('The function for delete does not support two arguments (data, callback)')
  509. }
  510. }
  511. else {
  512. this.edgesData.remove(selectedEdges);
  513. this.nodesData.remove(selectedNodes);
  514. this._unselectAll();
  515. this.moving = true;
  516. this.start();
  517. }
  518. }
  519. else {
  520. alert(this.constants.locales[this.constants.locale]["deleteClusterError"]);
  521. }
  522. }
  523. };