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.

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