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.

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