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.

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