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.

801 lines
20 KiB

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