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.

800 lines
20 KiB

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