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.

626 lines
15 KiB

  1. let Node = require("./components/Node");
  2. let util = require('../../util');
  3. class SelectionHandler {
  4. constructor(body, canvas) {
  5. this.body = body;
  6. this.canvas = canvas;
  7. this.selectionObj = {nodes: [], edges: []};
  8. this.options = {};
  9. this.defaultOptions = {
  10. multiselect: false,
  11. selectable: true,
  12. selectConnectedEdges: true
  13. };
  14. util.extend(this.options, this.defaultOptions);
  15. this.body.emitter.on("_dataChanged", () => {
  16. this.updateSelection()
  17. });
  18. }
  19. setOptions(options) {
  20. if (options !== undefined) {
  21. let fields = ['multiselect','selectable','selectConnectedEdges'];
  22. util.selectiveDeepExtend(fields,this.options, options);
  23. }
  24. }
  25. /**
  26. * handles the selection part of the tap;
  27. *
  28. * @param {Object} pointer
  29. * @private
  30. */
  31. selectOnPoint(pointer) {
  32. let selected = false;
  33. if (this.options.selectable === true) {
  34. this.unselectAll();
  35. let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);
  36. if (obj !== undefined) {
  37. selected = this.selectObject(obj);
  38. }
  39. this.body.emitter.emit("_requestRedraw");
  40. }
  41. return selected;
  42. }
  43. selectAdditionalOnPoint(pointer) {
  44. let selectionChanged = false;
  45. if (this.options.selectable === true) {
  46. let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);
  47. if (obj !== undefined) {
  48. selectionChanged = true;
  49. if (obj.isSelected() === true) {
  50. this.deselectObject(obj);
  51. }
  52. else {
  53. this.selectObject(obj);
  54. }
  55. this.body.emitter.emit("_requestRedraw");
  56. }
  57. }
  58. return selectionChanged;
  59. }
  60. _generateClickEvent(eventType, event, pointer, oldSelection) {
  61. let properties = this.getSelection();
  62. properties['pointer'] = {
  63. DOM: {x: pointer.x, y: pointer.y},
  64. canvas: this.canvas.DOMtoCanvas(pointer)
  65. };
  66. properties['event'] = event;
  67. if (oldSelection !== undefined) {
  68. properties['previousSelection'] = oldSelection;
  69. }
  70. this.body.emitter.emit(eventType, properties);
  71. }
  72. selectObject(obj, highlightEdges = this.options.selectConnectedEdges) {
  73. if (obj !== undefined) {
  74. if (obj instanceof Node) {
  75. if (highlightEdges === true) {
  76. this._selectConnectedEdges(obj);
  77. }
  78. }
  79. obj.select();
  80. this._addToSelection(obj);
  81. return true;
  82. }
  83. return false;
  84. }
  85. deselectObject(obj) {
  86. if (obj.isSelected() === true) {
  87. obj.selected = false;
  88. this._removeFromSelection(obj);
  89. }
  90. }
  91. /**
  92. * retrieve all nodes overlapping with given object
  93. * @param {Object} object An object with parameters left, top, right, bottom
  94. * @return {Number[]} An array with id's of the overlapping nodes
  95. * @private
  96. */
  97. _getAllNodesOverlappingWith(object) {
  98. let overlappingNodes = [];
  99. let nodes = this.body.nodes;
  100. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  101. let nodeId = this.body.nodeIndices[i];
  102. if (nodes[nodeId].isOverlappingWith(object)) {
  103. overlappingNodes.push(nodeId);
  104. }
  105. }
  106. return overlappingNodes;
  107. }
  108. /**
  109. * Return a position object in canvasspace from a single point in screenspace
  110. *
  111. * @param pointer
  112. * @returns {{left: number, top: number, right: number, bottom: number}}
  113. * @private
  114. */
  115. _pointerToPositionObject(pointer) {
  116. let canvasPos = this.canvas.DOMtoCanvas(pointer);
  117. return {
  118. left: canvasPos.x - 1,
  119. top: canvasPos.y + 1,
  120. right: canvasPos.x + 1,
  121. bottom: canvasPos.y - 1
  122. };
  123. }
  124. /**
  125. * Get the top node at the a specific point (like a click)
  126. *
  127. * @param {{x: Number, y: Number}} pointer
  128. * @return {Node | undefined} node
  129. * @private
  130. */
  131. getNodeAt(pointer, returnNode = true) {
  132. // we first check if this is an navigation controls element
  133. let positionObject = this._pointerToPositionObject(pointer);
  134. let overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  135. // if there are overlapping nodes, select the last one, this is the
  136. // one which is drawn on top of the others
  137. if (overlappingNodes.length > 0) {
  138. if (returnNode === true) {
  139. return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
  140. }
  141. else {
  142. return overlappingNodes[overlappingNodes.length - 1];
  143. }
  144. }
  145. else {
  146. return undefined;
  147. }
  148. }
  149. /**
  150. * retrieve all edges overlapping with given object, selector is around center
  151. * @param {Object} object An object with parameters left, top, right, bottom
  152. * @return {Number[]} An array with id's of the overlapping nodes
  153. * @private
  154. */
  155. _getEdgesOverlappingWith(object, overlappingEdges) {
  156. let edges = this.body.edges;
  157. for (let i = 0; i < this.body.edgeIndices.length; i++) {
  158. let edgeId = this.body.edgeIndices[i];
  159. if (edges[edgeId].isOverlappingWith(object)) {
  160. overlappingEdges.push(edgeId);
  161. }
  162. }
  163. }
  164. /**
  165. * retrieve all nodes overlapping with given object
  166. * @param {Object} object An object with parameters left, top, right, bottom
  167. * @return {Number[]} An array with id's of the overlapping nodes
  168. * @private
  169. */
  170. _getAllEdgesOverlappingWith(object) {
  171. let overlappingEdges = [];
  172. this._getEdgesOverlappingWith(object,overlappingEdges);
  173. return overlappingEdges;
  174. }
  175. /**
  176. * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call
  177. * getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  178. *
  179. * @param pointer
  180. * @returns {undefined}
  181. * @private
  182. */
  183. getEdgeAt(pointer, returnEdge = true) {
  184. let positionObject = this._pointerToPositionObject(pointer);
  185. let overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  186. if (overlappingEdges.length > 0) {
  187. if (returnEdge === true) {
  188. return this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
  189. }
  190. else {
  191. return overlappingEdges[overlappingEdges.length - 1];
  192. }
  193. }
  194. else {
  195. return undefined;
  196. }
  197. }
  198. /**
  199. * Add object to the selection array.
  200. *
  201. * @param obj
  202. * @private
  203. */
  204. _addToSelection(obj) {
  205. if (obj instanceof Node) {
  206. this.selectionObj.nodes[obj.id] = obj;
  207. }
  208. else {
  209. this.selectionObj.edges[obj.id] = obj;
  210. }
  211. }
  212. /**
  213. * Add object to the selection array.
  214. *
  215. * @param obj
  216. * @private
  217. */
  218. _addToHover(obj) {
  219. if (obj instanceof Node) {
  220. this.hoverObj.nodes[obj.id] = obj;
  221. }
  222. else {
  223. this.hoverObj.edges[obj.id] = obj;
  224. }
  225. }
  226. /**
  227. * Remove a single option from selection.
  228. *
  229. * @param {Object} obj
  230. * @private
  231. */
  232. _removeFromSelection(obj) {
  233. if (obj instanceof Node) {
  234. delete this.selectionObj.nodes[obj.id];
  235. }
  236. else {
  237. delete this.selectionObj.edges[obj.id];
  238. }
  239. }
  240. /**
  241. * Unselect all. The selectionObj is useful for this.
  242. *
  243. * @private
  244. */
  245. unselectAll() {
  246. for(let nodeId in this.selectionObj.nodes) {
  247. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  248. this.selectionObj.nodes[nodeId].unselect();
  249. }
  250. }
  251. for(let edgeId in this.selectionObj.edges) {
  252. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  253. this.selectionObj.edges[edgeId].unselect();
  254. }
  255. }
  256. this.selectionObj = {nodes:{},edges:{}};
  257. }
  258. /**
  259. * return the number of selected nodes
  260. *
  261. * @returns {number}
  262. * @private
  263. */
  264. _getSelectedNodeCount() {
  265. let count = 0;
  266. for (let nodeId in this.selectionObj.nodes) {
  267. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  268. count += 1;
  269. }
  270. }
  271. return count;
  272. }
  273. /**
  274. * return the selected node
  275. *
  276. * @returns {number}
  277. * @private
  278. */
  279. _getSelectedNode() {
  280. for (let nodeId in this.selectionObj.nodes) {
  281. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  282. return this.selectionObj.nodes[nodeId];
  283. }
  284. }
  285. return undefined;
  286. }
  287. /**
  288. * return the selected edge
  289. *
  290. * @returns {number}
  291. * @private
  292. */
  293. _getSelectedEdge() {
  294. for (let edgeId in this.selectionObj.edges) {
  295. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  296. return this.selectionObj.edges[edgeId];
  297. }
  298. }
  299. return undefined;
  300. }
  301. /**
  302. * return the number of selected edges
  303. *
  304. * @returns {number}
  305. * @private
  306. */
  307. _getSelectedEdgeCount() {
  308. let count = 0;
  309. for (let edgeId in this.selectionObj.edges) {
  310. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  311. count += 1;
  312. }
  313. }
  314. return count;
  315. }
  316. /**
  317. * return the number of selected objects.
  318. *
  319. * @returns {number}
  320. * @private
  321. */
  322. _getSelectedObjectCount() {
  323. let count = 0;
  324. for(let nodeId in this.selectionObj.nodes) {
  325. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  326. count += 1;
  327. }
  328. }
  329. for(let edgeId in this.selectionObj.edges) {
  330. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  331. count += 1;
  332. }
  333. }
  334. return count;
  335. }
  336. /**
  337. * Check if anything is selected
  338. *
  339. * @returns {boolean}
  340. * @private
  341. */
  342. _selectionIsEmpty() {
  343. for(let nodeId in this.selectionObj.nodes) {
  344. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  345. return false;
  346. }
  347. }
  348. for(let edgeId in this.selectionObj.edges) {
  349. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  350. return false;
  351. }
  352. }
  353. return true;
  354. }
  355. /**
  356. * check if one of the selected nodes is a cluster.
  357. *
  358. * @returns {boolean}
  359. * @private
  360. */
  361. _clusterInSelection() {
  362. for(let nodeId in this.selectionObj.nodes) {
  363. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  364. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  365. return true;
  366. }
  367. }
  368. }
  369. return false;
  370. }
  371. /**
  372. * select the edges connected to the node that is being selected
  373. *
  374. * @param {Node} node
  375. * @private
  376. */
  377. _selectConnectedEdges(node) {
  378. for (let i = 0; i < node.edges.length; i++) {
  379. let edge = node.edges[i];
  380. edge.select();
  381. this._addToSelection(edge);
  382. }
  383. }
  384. /**
  385. * select the edges connected to the node that is being selected
  386. *
  387. * @param {Node} node
  388. * @private
  389. */
  390. _hoverConnectedEdges(node) {
  391. for (let i = 0; i < node.edges.length; i++) {
  392. let edge = node.edges[i];
  393. edge.hover = true;
  394. this._addToHover(edge);
  395. }
  396. }
  397. /**
  398. * unselect the edges connected to the node that is being selected
  399. *
  400. * @param {Node} node
  401. * @private
  402. */
  403. _unselectConnectedEdges(node) {
  404. for (let i = 0; i < node.edges.length; i++) {
  405. let edge = node.edges[i];
  406. edge.unselect();
  407. this._removeFromSelection(edge);
  408. }
  409. }
  410. /**
  411. * This is called when someone clicks on a node. either select or deselect it.
  412. * If there is an existing selection and we don't want to append to it, clear the existing selection
  413. *
  414. * @param {Node || Edge} object
  415. * @private
  416. */
  417. blurObject(object) {
  418. if (object.hover === true) {
  419. object.hover = false;
  420. this.body.emitter.emit("blurNode",{node:object.id});
  421. }
  422. }
  423. /**
  424. * This is called when someone clicks on a node. either select or deselect it.
  425. * If there is an existing selection and we don't want to append to it, clear the existing selection
  426. *
  427. * @param {Node || Edge} object
  428. * @private
  429. */
  430. hoverObject(object) {
  431. if (object.hover === false) {
  432. object.hover = true;
  433. this._addToHover(object);
  434. if (object instanceof Node) {
  435. this.body.emitter.emit("hoverNode",{node:object.id});
  436. }
  437. }
  438. if (object instanceof Node) {
  439. this._hoverConnectedEdges(object);
  440. }
  441. }
  442. /**
  443. *
  444. * retrieve the currently selected objects
  445. * @return {{nodes: Array.<String>, edges: Array.<String>}} selection
  446. */
  447. getSelection() {
  448. let nodeIds = this.getSelectedNodes();
  449. let edgeIds = this.getSelectedEdges();
  450. return {nodes:nodeIds, edges:edgeIds};
  451. }
  452. /**
  453. *
  454. * retrieve the currently selected nodes
  455. * @return {String[]} selection An array with the ids of the
  456. * selected nodes.
  457. */
  458. getSelectedNodes() {
  459. let idArray = [];
  460. if (this.options.selectable === true) {
  461. for (let nodeId in this.selectionObj.nodes) {
  462. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  463. idArray.push(nodeId);
  464. }
  465. }
  466. }
  467. return idArray;
  468. }
  469. /**
  470. *
  471. * retrieve the currently selected edges
  472. * @return {Array} selection An array with the ids of the
  473. * selected nodes.
  474. */
  475. getSelectedEdges() {
  476. let idArray = [];
  477. if (this.options.selectable === true) {
  478. for (let edgeId in this.selectionObj.edges) {
  479. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  480. idArray.push(edgeId);
  481. }
  482. }
  483. }
  484. return idArray;
  485. }
  486. /**
  487. * select zero or more nodes with the option to highlight edges
  488. * @param {Number[] | String[]} selection An array with the ids of the
  489. * selected nodes.
  490. * @param {boolean} [highlightEdges]
  491. */
  492. selectNodes(selection, highlightEdges = true) {
  493. let i, id;
  494. if (!selection || (selection.length === undefined))
  495. throw 'Selection must be an array with ids';
  496. // first unselect any selected node
  497. this.unselectAll();
  498. for (i = 0; i < selection.length; i++) {
  499. id = selection[i];
  500. let node = this.body.nodes[id];
  501. if (!node) {
  502. throw new RangeError('Node with id "' + id + '" not found');
  503. }
  504. this.selectObject(node,highlightEdges);
  505. }
  506. this.body.emitter.emit('_requestRedraw');
  507. }
  508. /**
  509. * select zero or more edges
  510. * @param {Number[] | String[]} selection An array with the ids of the
  511. * selected nodes.
  512. */
  513. selectEdges(selection) {
  514. let i, id;
  515. if (!selection || (selection.length === undefined))
  516. throw 'Selection must be an array with ids';
  517. // first unselect any selected objects
  518. this.unselectAll();
  519. for (i = 0; i < selection.length; i++) {
  520. id = selection[i];
  521. let edge = this.body.edges[id];
  522. if (!edge) {
  523. throw new RangeError('Edge with id "' + id + '" not found');
  524. }
  525. this.selectObject(edge);
  526. }
  527. this.body.emitter.emit('_requestRedraw');
  528. }
  529. /**
  530. * Validate the selection: remove ids of nodes which no longer exist
  531. * @private
  532. */
  533. updateSelection() {
  534. for (let nodeId in this.selectionObj.nodes) {
  535. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  536. if (!this.body.nodes.hasOwnProperty(nodeId)) {
  537. delete this.selectionObj.nodes[nodeId];
  538. }
  539. }
  540. }
  541. for (let edgeId in this.selectionObj.edges) {
  542. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  543. if (!this.body.edges.hasOwnProperty(edgeId)) {
  544. delete this.selectionObj.edges[edgeId];
  545. }
  546. }
  547. }
  548. }
  549. }
  550. export default SelectionHandler;