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.

673 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, 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. }
  246. else {
  247. delete this.selectionObj.edges[obj.id];
  248. }
  249. }
  250. /**
  251. * Unselect all. The selectionObj is useful for this.
  252. *
  253. * @private
  254. */
  255. unselectAll() {
  256. for(let nodeId in this.selectionObj.nodes) {
  257. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  258. this.selectionObj.nodes[nodeId].unselect();
  259. }
  260. }
  261. for(let edgeId in this.selectionObj.edges) {
  262. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  263. this.selectionObj.edges[edgeId].unselect();
  264. }
  265. }
  266. this.selectionObj = {nodes:{},edges:{}};
  267. }
  268. /**
  269. * return the number of selected nodes
  270. *
  271. * @returns {number}
  272. * @private
  273. */
  274. _getSelectedNodeCount() {
  275. let count = 0;
  276. for (let nodeId in this.selectionObj.nodes) {
  277. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  278. count += 1;
  279. }
  280. }
  281. return count;
  282. }
  283. /**
  284. * return the selected node
  285. *
  286. * @returns {number}
  287. * @private
  288. */
  289. _getSelectedNode() {
  290. for (let nodeId in this.selectionObj.nodes) {
  291. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  292. return this.selectionObj.nodes[nodeId];
  293. }
  294. }
  295. return undefined;
  296. }
  297. /**
  298. * return the selected edge
  299. *
  300. * @returns {number}
  301. * @private
  302. */
  303. _getSelectedEdge() {
  304. for (let edgeId in this.selectionObj.edges) {
  305. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  306. return this.selectionObj.edges[edgeId];
  307. }
  308. }
  309. return undefined;
  310. }
  311. /**
  312. * return the number of selected edges
  313. *
  314. * @returns {number}
  315. * @private
  316. */
  317. _getSelectedEdgeCount() {
  318. let count = 0;
  319. for (let edgeId in this.selectionObj.edges) {
  320. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  321. count += 1;
  322. }
  323. }
  324. return count;
  325. }
  326. /**
  327. * return the number of selected objects.
  328. *
  329. * @returns {number}
  330. * @private
  331. */
  332. _getSelectedObjectCount() {
  333. let count = 0;
  334. for(let nodeId in this.selectionObj.nodes) {
  335. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  336. count += 1;
  337. }
  338. }
  339. for(let edgeId in this.selectionObj.edges) {
  340. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  341. count += 1;
  342. }
  343. }
  344. return count;
  345. }
  346. /**
  347. * Check if anything is selected
  348. *
  349. * @returns {boolean}
  350. * @private
  351. */
  352. _selectionIsEmpty() {
  353. for(let nodeId in this.selectionObj.nodes) {
  354. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  355. return false;
  356. }
  357. }
  358. for(let edgeId in this.selectionObj.edges) {
  359. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  360. return false;
  361. }
  362. }
  363. return true;
  364. }
  365. /**
  366. * check if one of the selected nodes is a cluster.
  367. *
  368. * @returns {boolean}
  369. * @private
  370. */
  371. _clusterInSelection() {
  372. for(let nodeId in this.selectionObj.nodes) {
  373. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  374. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  375. return true;
  376. }
  377. }
  378. }
  379. return false;
  380. }
  381. /**
  382. * select the edges connected to the node that is being selected
  383. *
  384. * @param {Node} node
  385. * @private
  386. */
  387. _selectConnectedEdges(node) {
  388. for (let i = 0; i < node.edges.length; i++) {
  389. let edge = node.edges[i];
  390. edge.select();
  391. this._addToSelection(edge);
  392. }
  393. }
  394. /**
  395. * select the edges connected to the node that is being selected
  396. *
  397. * @param {Node} node
  398. * @private
  399. */
  400. _hoverConnectedEdges(node) {
  401. for (let i = 0; i < node.edges.length; i++) {
  402. let edge = node.edges[i];
  403. edge.hover = true;
  404. this._addToHover(edge);
  405. }
  406. }
  407. /**
  408. * unselect the edges connected to the node that is being selected
  409. *
  410. * @param {Node} node
  411. * @private
  412. */
  413. _unselectConnectedEdges(node) {
  414. for (let i = 0; i < node.edges.length; i++) {
  415. let edge = node.edges[i];
  416. edge.unselect();
  417. this._removeFromSelection(edge);
  418. }
  419. }
  420. /**
  421. * This is called when someone clicks on a node. either select or deselect it.
  422. * If there is an existing selection and we don't want to append to it, clear the existing selection
  423. *
  424. * @param {Node || Edge} object
  425. * @private
  426. */
  427. blurObject(object) {
  428. if (object.hover === true) {
  429. object.hover = false;
  430. if (object instanceof Node) {
  431. this.body.emitter.emit("blurNode", {node: object.id});
  432. }
  433. else {
  434. this.body.emitter.emit("blurEdge", {edge: object.id});
  435. }
  436. }
  437. }
  438. /**
  439. * This is called when someone clicks on a node. either select or deselect it.
  440. * If there is an existing selection and we don't want to append to it, clear the existing selection
  441. *
  442. * @param {Node || Edge} object
  443. * @private
  444. */
  445. hoverObject(object) {
  446. let hoverChanged = false;
  447. // remove all node hover highlights
  448. for (let nodeId in this.hoverObj.nodes) {
  449. if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  450. if (object === undefined) {
  451. this.blurObject(this.hoverObj.nodes[nodeId]);
  452. hoverChanged = true;
  453. }
  454. else if (object instanceof Node && object.id != nodeId || object instanceof Edge || object === undefined) {
  455. this.blurObject(this.hoverObj.nodes[nodeId]);
  456. hoverChanged = true;
  457. delete this.hoverObj.nodes[nodeId];
  458. }
  459. }
  460. }
  461. // removing all edge hover highlights
  462. for (let edgeId in this.hoverObj.edges) {
  463. if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  464. this.hoverObj.edges[edgeId].hover = false;
  465. delete this.hoverObj.edges[edgeId];
  466. }
  467. }
  468. if (object !== undefined) {
  469. if (object.hover === false) {
  470. object.hover = true;
  471. this._addToHover(object);
  472. hoverChanged = true;
  473. if (object instanceof Node) {
  474. this.body.emitter.emit("hoverNode", {node: object.id});
  475. }
  476. else {
  477. this.body.emitter.emit("hoverEdge", {edge: object.id});
  478. }
  479. }
  480. if (object instanceof Node && this.options.hoverConnectedEdges === true) {
  481. this._hoverConnectedEdges(object);
  482. }
  483. }
  484. if (hoverChanged === true) {
  485. this.body.emitter.emit('_requestRedraw');
  486. }
  487. }
  488. /**
  489. *
  490. * retrieve the currently selected objects
  491. * @return {{nodes: Array.<String>, edges: Array.<String>}} selection
  492. */
  493. getSelection() {
  494. let nodeIds = this.getSelectedNodes();
  495. let edgeIds = this.getSelectedEdges();
  496. return {nodes:nodeIds, edges:edgeIds};
  497. }
  498. /**
  499. *
  500. * retrieve the currently selected nodes
  501. * @return {String[]} selection An array with the ids of the
  502. * selected nodes.
  503. */
  504. getSelectedNodes() {
  505. let idArray = [];
  506. if (this.options.selectable === true) {
  507. for (let nodeId in this.selectionObj.nodes) {
  508. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  509. idArray.push(nodeId);
  510. }
  511. }
  512. }
  513. return idArray;
  514. }
  515. /**
  516. *
  517. * retrieve the currently selected edges
  518. * @return {Array} selection An array with the ids of the
  519. * selected nodes.
  520. */
  521. getSelectedEdges() {
  522. let idArray = [];
  523. if (this.options.selectable === true) {
  524. for (let edgeId in this.selectionObj.edges) {
  525. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  526. idArray.push(edgeId);
  527. }
  528. }
  529. }
  530. return idArray;
  531. }
  532. /**
  533. * select zero or more nodes with the option to highlight edges
  534. * @param {Number[] | String[]} selection An array with the ids of the
  535. * selected nodes.
  536. * @param {boolean} [highlightEdges]
  537. */
  538. selectNodes(selection, highlightEdges = true) {
  539. let i, id;
  540. if (!selection || (selection.length === undefined))
  541. throw 'Selection must be an array with ids';
  542. // first unselect any selected node
  543. this.unselectAll();
  544. for (i = 0; i < selection.length; i++) {
  545. id = selection[i];
  546. let node = this.body.nodes[id];
  547. if (!node) {
  548. throw new RangeError('Node with id "' + id + '" not found');
  549. }
  550. this.selectObject(node,highlightEdges);
  551. }
  552. this.body.emitter.emit('_requestRedraw');
  553. }
  554. /**
  555. * select zero or more edges
  556. * @param {Number[] | String[]} selection An array with the ids of the
  557. * selected nodes.
  558. */
  559. selectEdges(selection) {
  560. let i, id;
  561. if (!selection || (selection.length === undefined))
  562. throw 'Selection must be an array with ids';
  563. // first unselect any selected objects
  564. this.unselectAll();
  565. for (i = 0; i < selection.length; i++) {
  566. id = selection[i];
  567. let edge = this.body.edges[id];
  568. if (!edge) {
  569. throw new RangeError('Edge with id "' + id + '" not found');
  570. }
  571. this.selectObject(edge);
  572. }
  573. this.body.emitter.emit('_requestRedraw');
  574. }
  575. /**
  576. * Validate the selection: remove ids of nodes which no longer exist
  577. * @private
  578. */
  579. updateSelection() {
  580. for (let nodeId in this.selectionObj.nodes) {
  581. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  582. if (!this.body.nodes.hasOwnProperty(nodeId)) {
  583. delete this.selectionObj.nodes[nodeId];
  584. }
  585. }
  586. }
  587. for (let edgeId in this.selectionObj.edges) {
  588. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  589. if (!this.body.edges.hasOwnProperty(edgeId)) {
  590. delete this.selectionObj.edges[edgeId];
  591. }
  592. }
  593. }
  594. }
  595. }
  596. export default SelectionHandler;