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.

586 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['add'] +"</span></span>" +
  84. "<div class='network-seperatorLine'></div>" +
  85. "<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" +
  86. "<span class='network-manipulationLabel'>"+locale['link'] +"</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['linkDescription'] + "</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. if (node != null) {
  294. if (node.clusterSize > 1) {
  295. alert(this.constants.locales[this.constants.locale]['createEdgeError'])
  296. }
  297. else {
  298. this._selectObject(node,false);
  299. // create a node the temporary line can look at
  300. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  301. this.sectors['support']['nodes']['targetNode'].x = node.x;
  302. this.sectors['support']['nodes']['targetNode'].y = node.y;
  303. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  304. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  305. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  306. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  307. // create a temporary edge
  308. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  309. this.edges['connectionEdge'].from = node;
  310. this.edges['connectionEdge'].connected = true;
  311. this.edges['connectionEdge'].smooth = true;
  312. this.edges['connectionEdge'].selected = true;
  313. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  314. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  315. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  316. this._handleOnDrag = function(event) {
  317. var pointer = this._getPointer(event.gesture.center);
  318. this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
  319. this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  320. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
  321. this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  322. };
  323. this.moving = true;
  324. this.start();
  325. }
  326. }
  327. }
  328. };
  329. exports._finishConnect = function(pointer) {
  330. if (this._getSelectedNodeCount() == 1) {
  331. // restore the drag function
  332. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  333. delete this.cachedFunctions["_handleOnDrag"];
  334. // remember the edge id
  335. var connectFromId = this.edges['connectionEdge'].fromId;
  336. // remove the temporary nodes and edge
  337. delete this.edges['connectionEdge'];
  338. delete this.sectors['support']['nodes']['targetNode'];
  339. delete this.sectors['support']['nodes']['targetViaNode'];
  340. var node = this._getNodeAt(pointer);
  341. if (node != null) {
  342. if (node.clusterSize > 1) {
  343. alert(this.constants.locales[this.constants.locale]["createEdgeError"])
  344. }
  345. else {
  346. this._createEdge(connectFromId,node.id);
  347. this._createManipulatorBar();
  348. }
  349. }
  350. this._unselectAll();
  351. }
  352. };
  353. /**
  354. * Adds a node on the specified location
  355. */
  356. exports._addNode = function() {
  357. if (this._selectionIsEmpty() && this.editMode == true) {
  358. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  359. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  360. if (this.triggerFunctions.add) {
  361. if (this.triggerFunctions.add.length == 2) {
  362. var me = this;
  363. this.triggerFunctions.add(defaultData, function(finalizedData) {
  364. me.nodesData.add(finalizedData);
  365. me._createManipulatorBar();
  366. me.moving = true;
  367. me.start();
  368. });
  369. }
  370. else {
  371. throw new Error('The function for add does not support two arguments (data,callback)');
  372. this._createManipulatorBar();
  373. this.moving = true;
  374. this.start();
  375. }
  376. }
  377. else {
  378. this.nodesData.add(defaultData);
  379. this._createManipulatorBar();
  380. this.moving = true;
  381. this.start();
  382. }
  383. }
  384. };
  385. /**
  386. * connect two nodes with a new edge.
  387. *
  388. * @private
  389. */
  390. exports._createEdge = function(sourceNodeId,targetNodeId) {
  391. if (this.editMode == true) {
  392. var defaultData = {from:sourceNodeId, to:targetNodeId};
  393. if (this.triggerFunctions.connect) {
  394. if (this.triggerFunctions.connect.length == 2) {
  395. var me = this;
  396. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  397. me.edgesData.add(finalizedData);
  398. me.moving = true;
  399. me.start();
  400. });
  401. }
  402. else {
  403. throw new Error('The function for connect does not support two arguments (data,callback)');
  404. this.moving = true;
  405. this.start();
  406. }
  407. }
  408. else {
  409. this.edgesData.add(defaultData);
  410. this.moving = true;
  411. this.start();
  412. }
  413. }
  414. };
  415. /**
  416. * connect two nodes with a new edge.
  417. *
  418. * @private
  419. */
  420. exports._editEdge = function(sourceNodeId,targetNodeId) {
  421. if (this.editMode == true) {
  422. var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
  423. if (this.triggerFunctions.editEdge) {
  424. if (this.triggerFunctions.editEdge.length == 2) {
  425. var me = this;
  426. this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
  427. me.edgesData.update(finalizedData);
  428. me.moving = true;
  429. me.start();
  430. });
  431. }
  432. else {
  433. throw new Error('The function for edit does not support two arguments (data, callback)');
  434. this.moving = true;
  435. this.start();
  436. }
  437. }
  438. else {
  439. this.edgesData.update(defaultData);
  440. this.moving = true;
  441. this.start();
  442. }
  443. }
  444. };
  445. /**
  446. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  447. *
  448. * @private
  449. */
  450. exports._editNode = function() {
  451. if (this.triggerFunctions.edit && this.editMode == true) {
  452. var node = this._getSelectedNode();
  453. var data = {id:node.id,
  454. label: node.label,
  455. group: node.options.group,
  456. shape: node.options.shape,
  457. color: {
  458. background:node.options.color.background,
  459. border:node.options.color.border,
  460. highlight: {
  461. background:node.options.color.highlight.background,
  462. border:node.options.color.highlight.border
  463. }
  464. }};
  465. if (this.triggerFunctions.edit.length == 2) {
  466. var me = this;
  467. this.triggerFunctions.edit(data, function (finalizedData) {
  468. me.nodesData.update(finalizedData);
  469. me._createManipulatorBar();
  470. me.moving = true;
  471. me.start();
  472. });
  473. }
  474. else {
  475. throw new Error('The function for edit does not support two arguments (data, callback)');
  476. }
  477. }
  478. else {
  479. throw new Error('No edit function has been bound to this button');
  480. }
  481. };
  482. /**
  483. * delete everything in the selection
  484. *
  485. * @private
  486. */
  487. exports._deleteSelected = function() {
  488. if (!this._selectionIsEmpty() && this.editMode == true) {
  489. if (!this._clusterInSelection()) {
  490. var selectedNodes = this.getSelectedNodes();
  491. var selectedEdges = this.getSelectedEdges();
  492. if (this.triggerFunctions.del) {
  493. var me = this;
  494. var data = {nodes: selectedNodes, edges: selectedEdges};
  495. if (this.triggerFunctions.del.length = 2) {
  496. this.triggerFunctions.del(data, function (finalizedData) {
  497. me.edgesData.remove(finalizedData.edges);
  498. me.nodesData.remove(finalizedData.nodes);
  499. me._unselectAll();
  500. me.moving = true;
  501. me.start();
  502. });
  503. }
  504. else {
  505. throw new Error('The function for delete does not support two arguments (data, callback)')
  506. }
  507. }
  508. else {
  509. this.edgesData.remove(selectedEdges);
  510. this.nodesData.remove(selectedNodes);
  511. this._unselectAll();
  512. this.moving = true;
  513. this.start();
  514. }
  515. }
  516. else {
  517. alert(this.constants.locales[this.constants.locale]["deleteClusterError"]);
  518. }
  519. }
  520. };