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.

774 lines
20 KiB

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