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