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.

663 lines
16 KiB

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