tree.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.82
  5. */
  6. import { Component, Element, Prop, Host, Event, Listen, h } from "@stencil/core";
  7. import { focusElement, nodeListToArray } from "../../utils/dom";
  8. import { TreeSelectionMode } from "./interfaces";
  9. /**
  10. * @slot - A slot for calcite-tree-item elements.
  11. */
  12. export class Tree {
  13. constructor() {
  14. //--------------------------------------------------------------------------
  15. //
  16. // Properties
  17. //
  18. //--------------------------------------------------------------------------
  19. /** Display indentation guide lines. */
  20. this.lines = false;
  21. /** Display input
  22. * @deprecated Use "ancestors" selection-mode for checkbox input.
  23. */
  24. this.inputEnabled = false;
  25. /** Specify the scale of the tree. */
  26. this.scale = "m";
  27. /** Customize how tree selection works.
  28. * @default "single"
  29. * @see [TreeSelectionMode](https://github.com/Esri/calcite-components/blob/master/src/components/tree/interfaces.ts#L5)
  30. */
  31. this.selectionMode = TreeSelectionMode.Single;
  32. }
  33. //--------------------------------------------------------------------------
  34. //
  35. // Lifecycle
  36. //
  37. //--------------------------------------------------------------------------
  38. componentWillRender() {
  39. const parent = this.el.parentElement.closest("calcite-tree");
  40. this.lines = parent ? parent.lines : this.lines;
  41. this.scale = parent ? parent.scale : this.scale;
  42. this.selectionMode = parent ? parent.selectionMode : this.selectionMode;
  43. this.child = !!parent;
  44. }
  45. render() {
  46. return (h(Host, { "aria-multiselectable": this.child
  47. ? undefined
  48. : (this.selectionMode === TreeSelectionMode.Multi ||
  49. this.selectionMode === TreeSelectionMode.MultiChildren).toString(), role: !this.child ? "tree" : undefined, tabIndex: this.getRootTabIndex() },
  50. h("slot", null)));
  51. }
  52. //--------------------------------------------------------------------------
  53. //
  54. // Event Listeners
  55. //
  56. //--------------------------------------------------------------------------
  57. onFocus() {
  58. if (!this.child) {
  59. const focusTarget = this.el.querySelector("calcite-tree-item[selected]") ||
  60. this.el.querySelector("calcite-tree-item");
  61. focusElement(focusTarget);
  62. }
  63. }
  64. onFocusIn(event) {
  65. const focusedFromRootOrOutsideTree = event.relatedTarget === this.el || !this.el.contains(event.relatedTarget);
  66. if (focusedFromRootOrOutsideTree) {
  67. // gives user the ability to tab into external elements (modifying tabindex property will not work in firefox)
  68. this.el.removeAttribute("tabindex");
  69. }
  70. }
  71. onFocusOut(event) {
  72. const willFocusOutsideTree = !this.el.contains(event.relatedTarget);
  73. if (willFocusOutsideTree) {
  74. this.el.tabIndex = this.getRootTabIndex();
  75. }
  76. }
  77. onClick(e) {
  78. const target = e.target;
  79. const childItems = nodeListToArray(target.querySelectorAll("calcite-tree-item"));
  80. if (this.child) {
  81. return;
  82. }
  83. if (!this.child) {
  84. e.preventDefault();
  85. e.stopPropagation();
  86. }
  87. if (this.selectionMode === TreeSelectionMode.Ancestors && !this.child) {
  88. this.updateAncestorTree(e);
  89. return;
  90. }
  91. const shouldSelect = this.selectionMode !== null &&
  92. (!target.hasChildren ||
  93. (target.hasChildren &&
  94. (this.selectionMode === TreeSelectionMode.Children ||
  95. this.selectionMode === TreeSelectionMode.MultiChildren)));
  96. const shouldModifyToCurrentSelection = e.detail.modifyCurrentSelection &&
  97. (this.selectionMode === TreeSelectionMode.Multi ||
  98. this.selectionMode === TreeSelectionMode.MultiChildren);
  99. const shouldSelectChildren = this.selectionMode === TreeSelectionMode.MultiChildren ||
  100. this.selectionMode === TreeSelectionMode.Children;
  101. const shouldClearCurrentSelection = !shouldModifyToCurrentSelection &&
  102. (((this.selectionMode === TreeSelectionMode.Single ||
  103. this.selectionMode === TreeSelectionMode.Multi) &&
  104. childItems.length <= 0) ||
  105. this.selectionMode === TreeSelectionMode.Children ||
  106. this.selectionMode === TreeSelectionMode.MultiChildren);
  107. const shouldExpandTarget = this.selectionMode === TreeSelectionMode.Children ||
  108. this.selectionMode === TreeSelectionMode.MultiChildren;
  109. if (!this.child) {
  110. const targetItems = [];
  111. if (shouldSelect) {
  112. targetItems.push(target);
  113. }
  114. if (shouldSelectChildren) {
  115. childItems.forEach((treeItem) => {
  116. targetItems.push(treeItem);
  117. });
  118. }
  119. if (shouldClearCurrentSelection) {
  120. const selectedItems = nodeListToArray(this.el.querySelectorAll("calcite-tree-item[selected]"));
  121. selectedItems.forEach((treeItem) => {
  122. if (!targetItems.includes(treeItem)) {
  123. treeItem.selected = false;
  124. }
  125. });
  126. }
  127. if (shouldExpandTarget && !e.detail.forceToggle) {
  128. target.expanded = true;
  129. }
  130. if (shouldModifyToCurrentSelection) {
  131. window.getSelection().removeAllRanges();
  132. }
  133. if ((shouldModifyToCurrentSelection && target.selected) ||
  134. (shouldSelectChildren && e.detail.forceToggle)) {
  135. targetItems.forEach((treeItem) => {
  136. treeItem.selected = false;
  137. });
  138. }
  139. else {
  140. targetItems.forEach((treeItem) => {
  141. treeItem.selected = true;
  142. });
  143. }
  144. }
  145. this.calciteTreeSelect.emit({
  146. selected: nodeListToArray(this.el.querySelectorAll("calcite-tree-item")).filter((i) => i.selected)
  147. });
  148. }
  149. keyDownHandler(event) {
  150. var _a;
  151. const root = this.el.closest("calcite-tree:not([child])");
  152. const target = event.target;
  153. if (root === this.el && target.tagName === "CALCITE-TREE-ITEM" && this.el.contains(target)) {
  154. switch (event.key) {
  155. case "ArrowDown":
  156. const next = target.nextElementSibling;
  157. if (next && next.matches("calcite-tree-item")) {
  158. next.focus();
  159. event.preventDefault();
  160. }
  161. break;
  162. case "ArrowLeft":
  163. // When focus is on an open node, closes the node.
  164. if (target.hasChildren && target.expanded) {
  165. target.expanded = false;
  166. event.preventDefault();
  167. break;
  168. }
  169. // When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
  170. const parentItem = target.parentElement.closest("calcite-tree-item");
  171. if (parentItem && (!target.hasChildren || target.expanded === false)) {
  172. parentItem.focus();
  173. event.preventDefault();
  174. break;
  175. }
  176. // When focus is on a root node that is also either an end node or a closed node, does nothing.
  177. break;
  178. case "ArrowRight":
  179. if (!target.hasChildren) {
  180. break;
  181. }
  182. if (target.expanded && document.activeElement === target) {
  183. // When focus is on an open node, moves focus to the first child node.
  184. (_a = target.querySelector("calcite-tree-item")) === null || _a === void 0 ? void 0 : _a.focus();
  185. event.preventDefault();
  186. }
  187. else {
  188. // When focus is on a closed node, opens the node; focus does not move.
  189. target.expanded = true;
  190. event.preventDefault();
  191. }
  192. // When focus is on an end node, does nothing.
  193. break;
  194. case "ArrowUp":
  195. const previous = target.previousElementSibling;
  196. if (previous && previous.matches("calcite-tree-item")) {
  197. previous.focus();
  198. event.preventDefault();
  199. }
  200. break;
  201. }
  202. }
  203. }
  204. updateAncestorTree(e) {
  205. const item = e.target;
  206. const children = item.querySelectorAll("calcite-tree-item");
  207. const ancestors = [];
  208. let parent = item.parentElement.closest("calcite-tree-item");
  209. while (parent) {
  210. ancestors.push(parent);
  211. parent = parent.parentElement.closest("calcite-tree-item");
  212. }
  213. item.selected = !item.selected;
  214. item.indeterminate = false;
  215. if (children === null || children === void 0 ? void 0 : children.length) {
  216. children.forEach((el) => {
  217. el.selected = item.selected;
  218. el.indeterminate = false;
  219. });
  220. }
  221. if (ancestors) {
  222. ancestors.forEach((ancestor) => {
  223. const descendants = nodeListToArray(ancestor.querySelectorAll("calcite-tree-item"));
  224. const activeDescendants = descendants.filter((el) => el.selected);
  225. if (activeDescendants.length === 0) {
  226. ancestor.selected = false;
  227. ancestor.indeterminate = false;
  228. return;
  229. }
  230. const indeterminate = activeDescendants.length < descendants.length;
  231. ancestor.indeterminate = indeterminate;
  232. ancestor.selected = !indeterminate;
  233. });
  234. }
  235. this.calciteTreeSelect.emit({
  236. selected: nodeListToArray(this.el.querySelectorAll("calcite-tree-item")).filter((i) => i.selected)
  237. });
  238. }
  239. // --------------------------------------------------------------------------
  240. //
  241. // Private Methods
  242. //
  243. //--------------------------------------------------------------------------
  244. getRootTabIndex() {
  245. return !this.child ? 0 : -1;
  246. }
  247. static get is() { return "calcite-tree"; }
  248. static get encapsulation() { return "shadow"; }
  249. static get originalStyleUrls() { return {
  250. "$": ["tree.scss"]
  251. }; }
  252. static get styleUrls() { return {
  253. "$": ["tree.css"]
  254. }; }
  255. static get properties() { return {
  256. "lines": {
  257. "type": "boolean",
  258. "mutable": true,
  259. "complexType": {
  260. "original": "boolean",
  261. "resolved": "boolean",
  262. "references": {}
  263. },
  264. "required": false,
  265. "optional": false,
  266. "docs": {
  267. "tags": [],
  268. "text": "Display indentation guide lines."
  269. },
  270. "attribute": "lines",
  271. "reflect": true,
  272. "defaultValue": "false"
  273. },
  274. "inputEnabled": {
  275. "type": "boolean",
  276. "mutable": false,
  277. "complexType": {
  278. "original": "boolean",
  279. "resolved": "boolean",
  280. "references": {}
  281. },
  282. "required": false,
  283. "optional": false,
  284. "docs": {
  285. "tags": [{
  286. "name": "deprecated",
  287. "text": "Use \"ancestors\" selection-mode for checkbox input."
  288. }],
  289. "text": "Display input"
  290. },
  291. "attribute": "input-enabled",
  292. "reflect": false,
  293. "defaultValue": "false"
  294. },
  295. "child": {
  296. "type": "boolean",
  297. "mutable": true,
  298. "complexType": {
  299. "original": "boolean",
  300. "resolved": "boolean",
  301. "references": {}
  302. },
  303. "required": false,
  304. "optional": false,
  305. "docs": {
  306. "tags": [{
  307. "name": "internal",
  308. "text": "If this tree is nested within another tree, set to false."
  309. }],
  310. "text": ""
  311. },
  312. "attribute": "child",
  313. "reflect": true
  314. },
  315. "scale": {
  316. "type": "string",
  317. "mutable": true,
  318. "complexType": {
  319. "original": "Scale",
  320. "resolved": "\"l\" | \"m\" | \"s\"",
  321. "references": {
  322. "Scale": {
  323. "location": "import",
  324. "path": "../interfaces"
  325. }
  326. }
  327. },
  328. "required": false,
  329. "optional": false,
  330. "docs": {
  331. "tags": [],
  332. "text": "Specify the scale of the tree."
  333. },
  334. "attribute": "scale",
  335. "reflect": true,
  336. "defaultValue": "\"m\""
  337. },
  338. "selectionMode": {
  339. "type": "string",
  340. "mutable": true,
  341. "complexType": {
  342. "original": "TreeSelectionMode",
  343. "resolved": "TreeSelectionMode.Ancestors | TreeSelectionMode.Children | TreeSelectionMode.Multi | TreeSelectionMode.MultiChildren | TreeSelectionMode.Single",
  344. "references": {
  345. "TreeSelectionMode": {
  346. "location": "import",
  347. "path": "./interfaces"
  348. }
  349. }
  350. },
  351. "required": false,
  352. "optional": false,
  353. "docs": {
  354. "tags": [{
  355. "name": "default",
  356. "text": "\"single\""
  357. }, {
  358. "name": "see",
  359. "text": "[TreeSelectionMode](https://github.com/Esri/calcite-components/blob/master/src/components/tree/interfaces.ts#L5)"
  360. }],
  361. "text": "Customize how tree selection works."
  362. },
  363. "attribute": "selection-mode",
  364. "reflect": true,
  365. "defaultValue": "TreeSelectionMode.Single"
  366. }
  367. }; }
  368. static get events() { return [{
  369. "method": "calciteTreeSelect",
  370. "name": "calciteTreeSelect",
  371. "bubbles": true,
  372. "cancelable": true,
  373. "composed": true,
  374. "docs": {
  375. "tags": [{
  376. "name": "see",
  377. "text": "[TreeSelectDetail](https://github.com/Esri/calcite-components/blob/master/src/components/tree/interfaces.ts#L1)"
  378. }],
  379. "text": "Emits when the user selects/deselects tree items. An object including an array of selected items will be passed in the event's \"detail\" property."
  380. },
  381. "complexType": {
  382. "original": "TreeSelectDetail",
  383. "resolved": "TreeSelectDetail",
  384. "references": {
  385. "TreeSelectDetail": {
  386. "location": "import",
  387. "path": "./interfaces"
  388. }
  389. }
  390. }
  391. }]; }
  392. static get elementRef() { return "el"; }
  393. static get listeners() { return [{
  394. "name": "focus",
  395. "method": "onFocus",
  396. "target": undefined,
  397. "capture": false,
  398. "passive": false
  399. }, {
  400. "name": "focusin",
  401. "method": "onFocusIn",
  402. "target": undefined,
  403. "capture": false,
  404. "passive": false
  405. }, {
  406. "name": "focusout",
  407. "method": "onFocusOut",
  408. "target": undefined,
  409. "capture": false,
  410. "passive": false
  411. }, {
  412. "name": "calciteTreeItemSelect",
  413. "method": "onClick",
  414. "target": undefined,
  415. "capture": false,
  416. "passive": false
  417. }, {
  418. "name": "keydown",
  419. "method": "keyDownHandler",
  420. "target": undefined,
  421. "capture": false,
  422. "passive": false
  423. }]; }
  424. }