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.

690 lines
17 KiB

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