tree.js 16 KB

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