calcite-tree.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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 { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client/index.js';
  7. import { f as focusElement, n as nodeListToArray, l as getRootNode } from './dom.js';
  8. import { T as TreeSelectionMode } from './interfaces2.js';
  9. function isTreeItem(element) {
  10. return element === null || element === void 0 ? void 0 : element.matches("calcite-tree-item");
  11. }
  12. function getEnabledSiblingItem(el, direction) {
  13. const directionProp = direction === "down" ? "nextElementSibling" : "previousElementSibling";
  14. let currentEl = el;
  15. let enabledEl = null;
  16. while (isTreeItem(currentEl)) {
  17. if (!currentEl.disabled) {
  18. enabledEl = currentEl;
  19. break;
  20. }
  21. currentEl = currentEl[directionProp];
  22. }
  23. return enabledEl;
  24. }
  25. const treeCss = "@keyframes in{0%{opacity:0}100%{opacity:1}}@keyframes in-down{0%{opacity:0;transform:translate3D(0, -5px, 0)}100%{opacity:1;transform:translate3D(0, 0, 0)}}@keyframes in-up{0%{opacity:0;transform:translate3D(0, 5px, 0)}100%{opacity:1;transform:translate3D(0, 0, 0)}}@keyframes in-scale{0%{opacity:0;transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;transform:scale3D(1, 1, 1)}}:root{--calcite-animation-timing:calc(150ms * var(--calcite-internal-duration-factor));--calcite-internal-duration-factor:var(--calcite-duration-factor, 1);--calcite-internal-animation-timing-fast:calc(100ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-medium:calc(200ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-slow:calc(300ms * var(--calcite-internal-duration-factor))}.calcite-animate{opacity:0;animation-fill-mode:both;animation-duration:var(--calcite-animation-timing)}.calcite-animate__in{animation-name:in}.calcite-animate__in-down{animation-name:in-down}.calcite-animate__in-up{animation-name:in-up}.calcite-animate__in-scale{animation-name:in-scale}@media (prefers-reduced-motion: reduce){:root{--calcite-internal-duration-factor:0.01}}:root{--calcite-floating-ui-transition:var(--calcite-animation-timing)}:host([hidden]){display:none}:host{display:block}:host(:focus){outline:2px solid transparent;outline-offset:2px}";
  26. const Tree = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
  27. constructor() {
  28. super();
  29. this.__registerHost();
  30. this.__attachShadow();
  31. this.calciteTreeSelect = createEvent(this, "calciteTreeSelect", 6);
  32. //--------------------------------------------------------------------------
  33. //
  34. // Properties
  35. //
  36. //--------------------------------------------------------------------------
  37. /** Displays indentation guide lines. */
  38. this.lines = false;
  39. /**
  40. * Display input
  41. *
  42. * @deprecated Use `selectionMode="ancestors"` for checkbox input.
  43. */
  44. this.inputEnabled = false;
  45. /** Specifies the size of the component. */
  46. this.scale = "m";
  47. /**
  48. * Customize how the component's selection works.
  49. *
  50. * @default "single"
  51. * @see [TreeSelectionMode](https://github.com/Esri/calcite-components/blob/master/src/components/tree/interfaces.ts#L5)
  52. */
  53. this.selectionMode = TreeSelectionMode.Single;
  54. }
  55. //--------------------------------------------------------------------------
  56. //
  57. // Lifecycle
  58. //
  59. //--------------------------------------------------------------------------
  60. componentWillRender() {
  61. var _a;
  62. const parent = (_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.closest("calcite-tree");
  63. this.lines = parent ? parent.lines : this.lines;
  64. this.scale = parent ? parent.scale : this.scale;
  65. this.selectionMode = parent ? parent.selectionMode : this.selectionMode;
  66. this.child = !!parent;
  67. }
  68. render() {
  69. return (h(Host, { "aria-multiselectable": this.child
  70. ? undefined
  71. : (this.selectionMode === TreeSelectionMode.Multi ||
  72. this.selectionMode === TreeSelectionMode.MultiChildren).toString(), role: !this.child ? "tree" : undefined, tabIndex: this.getRootTabIndex() }, h("slot", null)));
  73. }
  74. //--------------------------------------------------------------------------
  75. //
  76. // Event Listeners
  77. //
  78. //--------------------------------------------------------------------------
  79. onFocus() {
  80. if (!this.child) {
  81. const focusTarget = this.el.querySelector("calcite-tree-item[selected]:not([disabled])") || this.el.querySelector("calcite-tree-item:not([disabled])");
  82. focusElement(focusTarget);
  83. }
  84. }
  85. onFocusIn(event) {
  86. const focusedFromRootOrOutsideTree = event.relatedTarget === this.el || !this.el.contains(event.relatedTarget);
  87. if (focusedFromRootOrOutsideTree) {
  88. // gives user the ability to tab into external elements (modifying tabindex property will not work in firefox)
  89. this.el.removeAttribute("tabindex");
  90. }
  91. }
  92. onFocusOut(event) {
  93. const willFocusOutsideTree = !this.el.contains(event.relatedTarget);
  94. if (willFocusOutsideTree) {
  95. this.el.tabIndex = this.getRootTabIndex();
  96. }
  97. }
  98. onClick(event) {
  99. const target = event.target;
  100. const childItems = nodeListToArray(target.querySelectorAll("calcite-tree-item"));
  101. if (this.child) {
  102. return;
  103. }
  104. if (!this.child) {
  105. event.preventDefault();
  106. event.stopPropagation();
  107. }
  108. if (this.selectionMode === TreeSelectionMode.Ancestors && !this.child) {
  109. this.updateAncestorTree(event);
  110. return;
  111. }
  112. const isNoneSelectionMode = this.selectionMode === TreeSelectionMode.None;
  113. const shouldSelect = this.selectionMode !== null &&
  114. (!target.hasChildren ||
  115. (target.hasChildren &&
  116. (this.selectionMode === TreeSelectionMode.Children ||
  117. this.selectionMode === TreeSelectionMode.MultiChildren)));
  118. const shouldModifyToCurrentSelection = !isNoneSelectionMode &&
  119. event.detail.modifyCurrentSelection &&
  120. (this.selectionMode === TreeSelectionMode.Multi ||
  121. this.selectionMode === TreeSelectionMode.MultiChildren);
  122. const shouldSelectChildren = this.selectionMode === TreeSelectionMode.MultiChildren ||
  123. this.selectionMode === TreeSelectionMode.Children;
  124. const shouldClearCurrentSelection = !shouldModifyToCurrentSelection &&
  125. (((this.selectionMode === TreeSelectionMode.Single ||
  126. this.selectionMode === TreeSelectionMode.Multi) &&
  127. childItems.length <= 0) ||
  128. this.selectionMode === TreeSelectionMode.Children ||
  129. this.selectionMode === TreeSelectionMode.MultiChildren);
  130. const shouldExpandTarget = this.selectionMode === TreeSelectionMode.Children ||
  131. this.selectionMode === TreeSelectionMode.MultiChildren;
  132. if (!this.child) {
  133. const targetItems = [];
  134. if (shouldSelect) {
  135. targetItems.push(target);
  136. }
  137. if (shouldSelectChildren) {
  138. childItems.forEach((treeItem) => {
  139. targetItems.push(treeItem);
  140. });
  141. }
  142. if (shouldClearCurrentSelection) {
  143. const selectedItems = nodeListToArray(this.el.querySelectorAll("calcite-tree-item[selected]"));
  144. selectedItems.forEach((treeItem) => {
  145. if (!targetItems.includes(treeItem)) {
  146. treeItem.selected = false;
  147. }
  148. });
  149. }
  150. if (shouldExpandTarget && !event.detail.forceToggle) {
  151. target.expanded = true;
  152. }
  153. if (shouldModifyToCurrentSelection) {
  154. window.getSelection().removeAllRanges();
  155. }
  156. if ((shouldModifyToCurrentSelection && target.selected) ||
  157. (shouldSelectChildren && event.detail.forceToggle)) {
  158. targetItems.forEach((treeItem) => {
  159. if (!treeItem.disabled) {
  160. treeItem.selected = false;
  161. }
  162. });
  163. }
  164. else if (!isNoneSelectionMode) {
  165. targetItems.forEach((treeItem) => {
  166. if (!treeItem.disabled) {
  167. treeItem.selected = true;
  168. }
  169. });
  170. }
  171. }
  172. const selected = isNoneSelectionMode
  173. ? [target]
  174. : nodeListToArray(this.el.querySelectorAll("calcite-tree-item")).filter((i) => i.selected);
  175. this.calciteTreeSelect.emit({ selected });
  176. event.stopPropagation();
  177. }
  178. keyDownHandler(event) {
  179. var _a;
  180. const root = this.el.closest("calcite-tree:not([child])");
  181. const target = event.target;
  182. if (!(root === this.el && target.tagName === "CALCITE-TREE-ITEM" && this.el.contains(target))) {
  183. return;
  184. }
  185. if (event.key === "ArrowDown") {
  186. const next = getEnabledSiblingItem(target.nextElementSibling, "down");
  187. if (next) {
  188. next.focus();
  189. event.preventDefault();
  190. }
  191. return;
  192. }
  193. if (event.key === "ArrowUp") {
  194. const previous = getEnabledSiblingItem(target.previousElementSibling, "up");
  195. if (previous) {
  196. previous.focus();
  197. event.preventDefault();
  198. }
  199. }
  200. if (event.key === "ArrowLeft" && !target.disabled) {
  201. // When focus is on an open node, closes the node.
  202. if (target.hasChildren && target.expanded) {
  203. target.expanded = false;
  204. event.preventDefault();
  205. return;
  206. }
  207. // When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
  208. const parentItem = target.parentElement.closest("calcite-tree-item");
  209. if (parentItem && (!target.hasChildren || target.expanded === false)) {
  210. parentItem.focus();
  211. event.preventDefault();
  212. return;
  213. }
  214. // When focus is on a root node that is also either an end node or a closed node, does nothing.
  215. return;
  216. }
  217. if (event.key === "ArrowRight" && !target.disabled) {
  218. if (target.hasChildren) {
  219. if (target.expanded && getRootNode(this.el).activeElement === target) {
  220. // When focus is on an open node, moves focus to the first child node.
  221. (_a = getEnabledSiblingItem(target.querySelector("calcite-tree-item"), "down")) === null || _a === void 0 ? void 0 : _a.focus();
  222. event.preventDefault();
  223. }
  224. else {
  225. // When focus is on a closed node, opens the node; focus does not move.
  226. target.expanded = true;
  227. event.preventDefault();
  228. }
  229. }
  230. return;
  231. }
  232. }
  233. updateAncestorTree(event) {
  234. const item = event.target;
  235. if (item.disabled) {
  236. return;
  237. }
  238. const ancestors = [];
  239. let parent = item.parentElement.closest("calcite-tree-item");
  240. while (parent) {
  241. ancestors.push(parent);
  242. parent = parent.parentElement.closest("calcite-tree-item");
  243. }
  244. const childItems = Array.from(item.querySelectorAll("calcite-tree-item:not([disabled])"));
  245. const childItemsWithNoChildren = childItems.filter((child) => !child.hasChildren);
  246. const childItemsWithChildren = childItems.filter((child) => child.hasChildren);
  247. const futureSelected = item.hasChildren
  248. ? !(item.selected || item.indeterminate)
  249. : !item.selected;
  250. childItemsWithNoChildren.forEach((el) => {
  251. el.selected = futureSelected;
  252. el.indeterminate = false;
  253. });
  254. function updateItemState(childItems, item) {
  255. const selected = childItems.filter((child) => child.selected);
  256. const unselected = childItems.filter((child) => !child.selected);
  257. item.selected = selected.length === childItems.length;
  258. item.indeterminate = selected.length > 0 && unselected.length > 0;
  259. }
  260. childItemsWithChildren.forEach((el) => {
  261. const directChildItems = Array.from(el.querySelectorAll(":scope > calcite-tree > calcite-tree-item"));
  262. updateItemState(directChildItems, el);
  263. });
  264. if (item.hasChildren) {
  265. updateItemState(childItems, item);
  266. }
  267. else {
  268. item.selected = futureSelected;
  269. item.indeterminate = false;
  270. }
  271. ancestors.forEach((ancestor) => {
  272. const descendants = nodeListToArray(ancestor.querySelectorAll("calcite-tree-item"));
  273. const activeDescendants = descendants.filter((el) => el.selected);
  274. if (activeDescendants.length === 0) {
  275. ancestor.selected = false;
  276. ancestor.indeterminate = false;
  277. return;
  278. }
  279. const indeterminate = activeDescendants.length < descendants.length;
  280. ancestor.indeterminate = indeterminate;
  281. ancestor.selected = !indeterminate;
  282. });
  283. this.calciteTreeSelect.emit({
  284. selected: nodeListToArray(this.el.querySelectorAll("calcite-tree-item")).filter((i) => i.selected)
  285. });
  286. }
  287. // --------------------------------------------------------------------------
  288. //
  289. // Private Methods
  290. //
  291. //--------------------------------------------------------------------------
  292. getRootTabIndex() {
  293. return !this.child ? 0 : -1;
  294. }
  295. get el() { return this; }
  296. static get style() { return treeCss; }
  297. }, [1, "calcite-tree", {
  298. "lines": [1540],
  299. "inputEnabled": [4, "input-enabled"],
  300. "child": [1540],
  301. "scale": [1537],
  302. "selectionMode": [1537, "selection-mode"]
  303. }, [[0, "focus", "onFocus"], [0, "focusin", "onFocusIn"], [0, "focusout", "onFocusOut"], [0, "calciteInternalTreeItemSelect", "onClick"], [0, "keydown", "keyDownHandler"]]]);
  304. function defineCustomElement$1() {
  305. if (typeof customElements === "undefined") {
  306. return;
  307. }
  308. const components = ["calcite-tree"];
  309. components.forEach(tagName => { switch (tagName) {
  310. case "calcite-tree":
  311. if (!customElements.get(tagName)) {
  312. customElements.define(tagName, Tree);
  313. }
  314. break;
  315. } });
  316. }
  317. defineCustomElement$1();
  318. const CalciteTree = Tree;
  319. const defineCustomElement = defineCustomElement$1;
  320. export { CalciteTree, defineCustomElement };