2023-06-22 14:56:24 +00:00
|
|
|
import { clamp } from '../../internal/math.js';
|
2023-01-13 20:43:55 +00:00
|
|
|
import { customElement, property, query } from 'lit/decorators.js';
|
|
|
|
import { html } from 'lit';
|
2023-06-22 14:56:24 +00:00
|
|
|
import { LocalizeController } from '../../utilities/localize.js';
|
|
|
|
import { watch } from '../../internal/watch.js';
|
|
|
|
import ShoelaceElement from '../../internal/shoelace-element.js';
|
|
|
|
import SlTreeItem from '../tree-item/tree-item.js';
|
|
|
|
import styles from './tree.styles.js';
|
2022-08-17 20:31:23 +00:00
|
|
|
import type { CSSResultGroup } from 'lit';
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-12-17 16:26:42 +00:00
|
|
|
function syncCheckboxes(changedTreeItem: SlTreeItem, initialSync = false) {
|
|
|
|
function syncParentItem(treeItem: SlTreeItem) {
|
|
|
|
const children = treeItem.getChildrenItems({ includeDisabled: false });
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-12-17 16:26:42 +00:00
|
|
|
if (children.length) {
|
|
|
|
const allChecked = children.every(item => item.selected);
|
2022-07-10 15:23:14 +00:00
|
|
|
const allUnchecked = children.every(item => !item.selected && !item.indeterminate);
|
|
|
|
|
2022-12-17 16:26:42 +00:00
|
|
|
treeItem.selected = allChecked;
|
|
|
|
treeItem.indeterminate = !allChecked && !allUnchecked;
|
|
|
|
}
|
|
|
|
}
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-12-17 16:26:42 +00:00
|
|
|
function syncAncestors(treeItem: SlTreeItem) {
|
|
|
|
const parentItem: SlTreeItem | null = treeItem.parentElement as SlTreeItem;
|
|
|
|
|
|
|
|
if (SlTreeItem.isTreeItem(parentItem)) {
|
|
|
|
syncParentItem(parentItem);
|
2022-07-10 15:23:14 +00:00
|
|
|
syncAncestors(parentItem);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function syncDescendants(treeItem: SlTreeItem) {
|
|
|
|
for (const childItem of treeItem.getChildrenItems()) {
|
2022-12-17 16:26:42 +00:00
|
|
|
childItem.selected = initialSync
|
|
|
|
? treeItem.selected || childItem.selected
|
|
|
|
: !childItem.disabled && treeItem.selected;
|
|
|
|
|
2022-07-10 15:23:14 +00:00
|
|
|
syncDescendants(childItem);
|
|
|
|
}
|
2022-12-17 16:26:42 +00:00
|
|
|
|
|
|
|
if (initialSync) {
|
|
|
|
syncParentItem(treeItem);
|
|
|
|
}
|
2022-07-10 15:23:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
syncDescendants(changedTreeItem);
|
2022-12-17 16:26:42 +00:00
|
|
|
syncAncestors(changedTreeItem);
|
2022-07-10 15:23:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-10-21 13:56:35 +00:00
|
|
|
* @summary Trees allow you to display a hierarchical list of selectable [tree items](/components/tree-item). Items with children can be expanded and collapsed as desired by the user.
|
2023-01-12 15:26:25 +00:00
|
|
|
* @documentation https://shoelace.style/components/tree
|
2022-12-20 18:36:06 +00:00
|
|
|
* @status stable
|
2023-01-12 15:26:25 +00:00
|
|
|
* @since 2.0
|
2022-07-10 15:23:14 +00:00
|
|
|
*
|
2023-02-07 22:44:56 +00:00
|
|
|
* @event {{ selection: SlTreeItem[] }} sl-selection-change - Emitted when a tree item is selected or deselected.
|
2022-07-10 15:23:14 +00:00
|
|
|
*
|
|
|
|
* @slot - The default slot.
|
2022-12-06 16:18:14 +00:00
|
|
|
* @slot expand-icon - The icon to show when the tree item is expanded. Works best with `<sl-icon>`.
|
|
|
|
* @slot collapse-icon - The icon to show when the tree item is collapsed. Works best with `<sl-icon>`.
|
2022-07-10 15:23:14 +00:00
|
|
|
*
|
2022-12-06 16:18:14 +00:00
|
|
|
* @csspart base - The component's base wrapper.
|
2022-07-10 15:23:14 +00:00
|
|
|
*
|
2022-07-26 19:53:24 +00:00
|
|
|
* @cssproperty [--indent-size=var(--sl-spacing-medium)] - The size of the indentation for nested items.
|
|
|
|
* @cssproperty [--indent-guide-color=var(--sl-color-neutral-200)] - The color of the indentation line.
|
2022-12-06 16:18:14 +00:00
|
|
|
* @cssproperty [--indent-guide-offset=0] - The amount of vertical spacing to leave between the top and bottom of the
|
|
|
|
* indentation line's starting position.
|
2022-07-26 19:53:24 +00:00
|
|
|
* @cssproperty [--indent-guide-style=solid] - The style of the indentation line, e.g. solid, dotted, dashed.
|
|
|
|
* @cssproperty [--indent-guide-width=0] - The width of the indentation line.
|
2022-07-10 15:23:14 +00:00
|
|
|
*/
|
|
|
|
@customElement('sl-tree')
|
2022-08-17 15:37:37 +00:00
|
|
|
export default class SlTree extends ShoelaceElement {
|
2022-08-17 20:31:23 +00:00
|
|
|
static styles: CSSResultGroup = styles;
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-08-17 20:22:03 +00:00
|
|
|
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
|
|
|
@query('slot[name=expand-icon]') expandedIconSlot: HTMLSlotElement;
|
|
|
|
@query('slot[name=collapse-icon]') collapsedIconSlot: HTMLSlotElement;
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-12-06 16:18:14 +00:00
|
|
|
/**
|
|
|
|
* The selection behavior of the tree. Single selection allows only one node to be selected at a time. Multiple
|
|
|
|
* displays checkboxes and allows more than one node to be selected. Leaf allows only leaf nodes to be selected.
|
|
|
|
*/
|
2022-07-26 19:53:24 +00:00
|
|
|
@property() selection: 'single' | 'multiple' | 'leaf' = 'single';
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-07-26 19:53:24 +00:00
|
|
|
//
|
|
|
|
// A collection of all the items in the tree, in the order they appear. The collection is live, meaning it is
|
|
|
|
// automatically updated when the underlying document is changed.
|
|
|
|
//
|
2023-07-12 15:19:55 +00:00
|
|
|
private lastFocusedItem: SlTreeItem | null;
|
2022-07-26 19:53:24 +00:00
|
|
|
private readonly localize = new LocalizeController(this);
|
2022-07-10 15:23:14 +00:00
|
|
|
private mutationObserver: MutationObserver;
|
2023-02-28 18:33:34 +00:00
|
|
|
private clickTarget: SlTreeItem | null = null;
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2023-06-13 23:52:12 +00:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.addEventListener('focusin', this.handleFocusIn);
|
|
|
|
this.addEventListener('focusout', this.handleFocusOut);
|
|
|
|
this.addEventListener('sl-lazy-change', this.handleSlotChange);
|
|
|
|
}
|
|
|
|
|
2022-08-17 20:22:03 +00:00
|
|
|
async connectedCallback() {
|
2022-07-10 15:23:14 +00:00
|
|
|
super.connectedCallback();
|
2023-01-03 20:04:07 +00:00
|
|
|
|
2022-07-10 15:23:14 +00:00
|
|
|
this.setAttribute('role', 'tree');
|
|
|
|
this.setAttribute('tabindex', '0');
|
|
|
|
|
2022-08-17 20:22:03 +00:00
|
|
|
await this.updateComplete;
|
2022-12-17 16:26:42 +00:00
|
|
|
|
2022-08-17 20:22:03 +00:00
|
|
|
this.mutationObserver = new MutationObserver(this.handleTreeChanged);
|
|
|
|
this.mutationObserver.observe(this, { childList: true, subtree: true });
|
2022-07-10 15:23:14 +00:00
|
|
|
}
|
|
|
|
|
2022-08-17 20:22:03 +00:00
|
|
|
disconnectedCallback() {
|
2022-07-10 15:23:14 +00:00
|
|
|
super.disconnectedCallback();
|
|
|
|
|
|
|
|
this.mutationObserver.disconnect();
|
|
|
|
}
|
|
|
|
|
2022-08-01 12:41:36 +00:00
|
|
|
// Generates a clone of the expand icon element to use for each tree item
|
2022-08-17 20:22:03 +00:00
|
|
|
private getExpandButtonIcon(status: 'expand' | 'collapse') {
|
|
|
|
const slot = status === 'expand' ? this.expandedIconSlot : this.collapsedIconSlot;
|
2022-08-01 12:41:36 +00:00
|
|
|
const icon = slot.assignedElements({ flatten: true })[0] as HTMLElement;
|
|
|
|
|
|
|
|
// Clone it, remove ids, and slot it
|
|
|
|
if (icon) {
|
|
|
|
const clone = icon.cloneNode(true) as HTMLElement;
|
|
|
|
[clone, ...clone.querySelectorAll('[id]')].forEach(el => el.removeAttribute('id'));
|
|
|
|
clone.setAttribute('data-default', '');
|
|
|
|
clone.slot = `${status}-icon`;
|
|
|
|
|
|
|
|
return clone;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initializes new items by setting the `selectable` property and the expanded/collapsed icons if any
|
|
|
|
private initTreeItem = (item: SlTreeItem) => {
|
|
|
|
item.selectable = this.selection === 'multiple';
|
|
|
|
|
2022-08-17 20:22:03 +00:00
|
|
|
['expand', 'collapse']
|
2022-08-01 12:41:36 +00:00
|
|
|
.filter(status => !!this.querySelector(`[slot="${status}-icon"]`))
|
2022-08-17 20:22:03 +00:00
|
|
|
.forEach((status: 'expand' | 'collapse') => {
|
2022-08-01 12:41:36 +00:00
|
|
|
const existingIcon = item.querySelector(`[slot="${status}-icon"]`);
|
|
|
|
|
2022-08-01 12:48:02 +00:00
|
|
|
if (existingIcon === null) {
|
2022-08-01 12:41:36 +00:00
|
|
|
// No separator exists, add one
|
|
|
|
item.append(this.getExpandButtonIcon(status)!);
|
|
|
|
} else if (existingIcon.hasAttribute('data-default')) {
|
|
|
|
// A default separator exists, replace it
|
|
|
|
existingIcon.replaceWith(this.getExpandButtonIcon(status)!);
|
2022-08-01 12:48:02 +00:00
|
|
|
} else {
|
|
|
|
// The user provided a custom icon, leave it alone
|
2022-08-01 12:41:36 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-06-13 23:52:12 +00:00
|
|
|
private handleTreeChanged = (mutations: MutationRecord[]) => {
|
2022-07-10 15:23:14 +00:00
|
|
|
for (const mutation of mutations) {
|
2022-12-06 16:37:44 +00:00
|
|
|
const addedNodes: SlTreeItem[] = [...mutation.addedNodes].filter(SlTreeItem.isTreeItem) as SlTreeItem[];
|
2023-07-12 15:19:55 +00:00
|
|
|
const removedNodes = [...mutation.removedNodes].filter(SlTreeItem.isTreeItem) as SlTreeItem[];
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-08-01 12:41:36 +00:00
|
|
|
addedNodes.forEach(this.initTreeItem);
|
2023-07-12 15:19:55 +00:00
|
|
|
|
|
|
|
if (this.lastFocusedItem && removedNodes.includes(this.lastFocusedItem)) {
|
|
|
|
this.lastFocusedItem = null;
|
|
|
|
}
|
2022-07-10 15:23:14 +00:00
|
|
|
}
|
2023-06-13 23:52:12 +00:00
|
|
|
};
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private syncTreeItems(selectedItem: SlTreeItem) {
|
2022-11-09 21:53:53 +00:00
|
|
|
const items = this.getAllTreeItems();
|
|
|
|
|
2022-07-10 15:23:14 +00:00
|
|
|
if (this.selection === 'multiple') {
|
|
|
|
syncCheckboxes(selectedItem);
|
|
|
|
} else {
|
2022-11-09 21:53:53 +00:00
|
|
|
for (const item of items) {
|
2022-07-10 15:23:14 +00:00
|
|
|
if (item !== selectedItem) {
|
|
|
|
item.selected = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private selectItem(selectedItem: SlTreeItem) {
|
2022-11-28 16:55:12 +00:00
|
|
|
const previousSelection = [...this.selectedItems];
|
|
|
|
|
2022-07-10 15:23:14 +00:00
|
|
|
if (this.selection === 'multiple') {
|
|
|
|
selectedItem.selected = !selectedItem.selected;
|
|
|
|
if (selectedItem.lazy) {
|
|
|
|
selectedItem.expanded = true;
|
|
|
|
}
|
|
|
|
this.syncTreeItems(selectedItem);
|
|
|
|
} else if (this.selection === 'single' || selectedItem.isLeaf) {
|
2022-08-17 16:33:33 +00:00
|
|
|
selectedItem.expanded = !selectedItem.expanded;
|
2022-07-10 15:23:14 +00:00
|
|
|
selectedItem.selected = true;
|
|
|
|
|
|
|
|
this.syncTreeItems(selectedItem);
|
|
|
|
} else if (this.selection === 'leaf') {
|
|
|
|
selectedItem.expanded = !selectedItem.expanded;
|
|
|
|
}
|
|
|
|
|
2022-11-28 16:55:12 +00:00
|
|
|
const nextSelection = this.selectedItems;
|
|
|
|
|
2022-11-30 13:18:56 +00:00
|
|
|
if (
|
|
|
|
previousSelection.length !== nextSelection.length ||
|
|
|
|
nextSelection.some(item => !previousSelection.includes(item))
|
|
|
|
) {
|
2023-01-03 15:10:14 +00:00
|
|
|
// Wait for the tree items' DOM to update before emitting
|
|
|
|
Promise.all(nextSelection.map(el => el.updateComplete)).then(() => {
|
|
|
|
this.emit('sl-selection-change', { detail: { selection: nextSelection } });
|
|
|
|
});
|
2022-11-28 16:55:12 +00:00
|
|
|
}
|
2022-07-10 15:23:14 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private getAllTreeItems() {
|
2022-11-09 21:53:53 +00:00
|
|
|
return [...this.querySelectorAll<SlTreeItem>('sl-tree-item')];
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private focusItem(item?: SlTreeItem | null) {
|
2022-07-10 15:23:14 +00:00
|
|
|
item?.focus();
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private handleKeyDown(event: KeyboardEvent) {
|
2022-07-26 19:53:24 +00:00
|
|
|
if (!['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Home', 'End', 'Enter', ' '].includes(event.key)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-07-10 15:23:14 +00:00
|
|
|
const items = this.getFocusableItems();
|
2022-07-26 19:53:24 +00:00
|
|
|
const isLtr = this.localize.dir() === 'ltr';
|
|
|
|
const isRtl = this.localize.dir() === 'rtl';
|
2022-07-10 15:23:14 +00:00
|
|
|
|
|
|
|
if (items.length > 0) {
|
|
|
|
event.preventDefault();
|
2022-08-25 13:18:36 +00:00
|
|
|
const activeItemIndex = items.findIndex(item => item.matches(':focus'));
|
2022-07-10 15:23:14 +00:00
|
|
|
const activeItem: SlTreeItem | undefined = items[activeItemIndex];
|
|
|
|
|
|
|
|
const focusItemAt = (index: number) => {
|
|
|
|
const item = items[clamp(index, 0, items.length - 1)];
|
|
|
|
this.focusItem(item);
|
|
|
|
};
|
|
|
|
const toggleExpand = (expanded: boolean) => {
|
|
|
|
activeItem.expanded = expanded;
|
|
|
|
};
|
|
|
|
|
|
|
|
if (event.key === 'ArrowDown') {
|
2022-07-26 19:53:24 +00:00
|
|
|
// Moves focus to the next node that is focusable without opening or closing a node.
|
2022-07-10 15:23:14 +00:00
|
|
|
focusItemAt(activeItemIndex + 1);
|
|
|
|
} else if (event.key === 'ArrowUp') {
|
2022-07-26 19:53:24 +00:00
|
|
|
// Moves focus to the next node that is focusable without opening or closing a node.
|
2022-07-10 15:23:14 +00:00
|
|
|
focusItemAt(activeItemIndex - 1);
|
2022-07-26 19:53:24 +00:00
|
|
|
} else if ((isLtr && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft')) {
|
|
|
|
//
|
|
|
|
// When focus is on a closed node, opens the node; focus does not move.
|
|
|
|
// When focus is on a open node, moves focus to the first child node.
|
|
|
|
// When focus is on an end node (a tree item with no children), does nothing.
|
|
|
|
//
|
2022-07-28 18:01:43 +00:00
|
|
|
if (!activeItem || activeItem.disabled || activeItem.expanded || (activeItem.isLeaf && !activeItem.lazy)) {
|
2022-07-10 15:23:14 +00:00
|
|
|
focusItemAt(activeItemIndex + 1);
|
|
|
|
} else {
|
|
|
|
toggleExpand(true);
|
|
|
|
}
|
2022-07-26 19:53:24 +00:00
|
|
|
} else if ((isLtr && event.key === 'ArrowLeft') || (isRtl && event.key === 'ArrowRight')) {
|
|
|
|
//
|
|
|
|
// When focus is on an open node, closes the node.
|
|
|
|
// When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
|
|
|
|
// When focus is on a closed `tree`, does nothing.
|
|
|
|
//
|
2022-07-28 18:01:43 +00:00
|
|
|
if (!activeItem || activeItem.disabled || activeItem.isLeaf || !activeItem.expanded) {
|
2022-07-10 15:23:14 +00:00
|
|
|
focusItemAt(activeItemIndex - 1);
|
|
|
|
} else {
|
|
|
|
toggleExpand(false);
|
|
|
|
}
|
|
|
|
} else if (event.key === 'Home') {
|
2022-07-26 19:53:24 +00:00
|
|
|
// Moves focus to the first node in the tree without opening or closing a node.
|
2022-07-10 15:23:14 +00:00
|
|
|
focusItemAt(0);
|
|
|
|
} else if (event.key === 'End') {
|
2022-07-26 19:53:24 +00:00
|
|
|
// Moves focus to the last node in the tree that is focusable without opening the node.
|
2022-07-10 15:23:14 +00:00
|
|
|
focusItemAt(items.length - 1);
|
2022-07-26 19:53:24 +00:00
|
|
|
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
|
|
// Selects the focused node.
|
2022-07-28 18:01:43 +00:00
|
|
|
if (!activeItem.disabled) {
|
|
|
|
this.selectItem(activeItem);
|
|
|
|
}
|
2022-07-10 15:23:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private handleClick(event: Event) {
|
2023-02-28 18:33:34 +00:00
|
|
|
const target = event.target as SlTreeItem;
|
2022-07-10 15:23:14 +00:00
|
|
|
const treeItem = target.closest('sl-tree-item')!;
|
2022-08-17 20:22:03 +00:00
|
|
|
const isExpandButton = event
|
|
|
|
.composedPath()
|
|
|
|
.some((el: HTMLElement) => el?.classList?.contains('tree-item__expand-button'));
|
|
|
|
|
2023-02-28 18:33:34 +00:00
|
|
|
//
|
|
|
|
// Don't Do anything if there's no tree item, if it's disabled, or if the click doesn't match the initial target
|
|
|
|
// from mousedown. The latter case prevents the user from starting a click on one item and ending it on another,
|
|
|
|
// causing the parent node to collapse.
|
|
|
|
//
|
|
|
|
// See https://github.com/shoelace-style/shoelace/issues/1082
|
|
|
|
//
|
|
|
|
if (!treeItem || treeItem.disabled || target !== this.clickTarget) {
|
2022-08-17 20:22:03 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2022-08-17 20:22:03 +00:00
|
|
|
if (this.selection === 'multiple' && isExpandButton) {
|
|
|
|
treeItem.expanded = !treeItem.expanded;
|
|
|
|
} else {
|
2022-07-10 15:23:14 +00:00
|
|
|
this.selectItem(treeItem);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-28 18:33:34 +00:00
|
|
|
handleMouseDown(event: MouseEvent) {
|
|
|
|
// Record the click target so we know which item the click initially targeted
|
|
|
|
this.clickTarget = event.target as SlTreeItem;
|
|
|
|
}
|
|
|
|
|
2023-06-13 23:52:12 +00:00
|
|
|
private handleFocusOut = (event: FocusEvent) => {
|
2022-07-26 19:53:24 +00:00
|
|
|
const relatedTarget = event.relatedTarget as HTMLElement;
|
2022-07-10 15:23:14 +00:00
|
|
|
|
|
|
|
// If the element that got the focus is not in the tree
|
|
|
|
if (!relatedTarget || !this.contains(relatedTarget)) {
|
|
|
|
this.tabIndex = 0;
|
|
|
|
}
|
2023-06-13 23:52:12 +00:00
|
|
|
};
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2023-06-13 23:52:12 +00:00
|
|
|
private handleFocusIn = (event: FocusEvent) => {
|
2022-07-26 19:53:24 +00:00
|
|
|
const target = event.target as SlTreeItem;
|
2022-07-10 15:23:14 +00:00
|
|
|
|
|
|
|
// If the tree has been focused, move the focus to the last focused item
|
2022-07-26 19:53:24 +00:00
|
|
|
if (event.target === this) {
|
2022-11-09 21:53:53 +00:00
|
|
|
this.focusItem(this.lastFocusedItem || this.getAllTreeItems()[0]);
|
2022-07-10 15:23:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the target is a tree item, update the tabindex
|
2022-12-06 16:37:44 +00:00
|
|
|
if (SlTreeItem.isTreeItem(target) && !target.disabled) {
|
2022-07-10 15:23:14 +00:00
|
|
|
if (this.lastFocusedItem) {
|
|
|
|
this.lastFocusedItem.tabIndex = -1;
|
|
|
|
}
|
|
|
|
this.lastFocusedItem = target;
|
|
|
|
this.tabIndex = -1;
|
|
|
|
|
|
|
|
target.tabIndex = 0;
|
|
|
|
}
|
2023-06-13 23:52:12 +00:00
|
|
|
};
|
2022-07-10 15:23:14 +00:00
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private handleSlotChange() {
|
2022-11-09 21:53:53 +00:00
|
|
|
const items = this.getAllTreeItems();
|
|
|
|
items.forEach(this.initTreeItem);
|
2022-08-26 14:28:18 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
@watch('selection')
|
|
|
|
async handleSelectionChange() {
|
|
|
|
const isSelectionMultiple = this.selection === 'multiple';
|
|
|
|
const items = this.getAllTreeItems();
|
|
|
|
|
|
|
|
this.setAttribute('aria-multiselectable', isSelectionMultiple ? 'true' : 'false');
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
item.selectable = isSelectionMultiple;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isSelectionMultiple) {
|
|
|
|
await this.updateComplete;
|
|
|
|
|
|
|
|
[...this.querySelectorAll(':scope > sl-tree-item')].forEach((treeItem: SlTreeItem) =>
|
|
|
|
syncCheckboxes(treeItem, true)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-13 19:22:06 +00:00
|
|
|
/** @internal Returns the list of tree items that are selected in the tree. */
|
|
|
|
get selectedItems(): SlTreeItem[] {
|
|
|
|
const items = this.getAllTreeItems();
|
|
|
|
const isSelected = (item: SlTreeItem) => item.selected;
|
|
|
|
|
|
|
|
return items.filter(isSelected);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @internal Gets focusable tree items in the tree. */
|
|
|
|
getFocusableItems() {
|
|
|
|
const items = this.getAllTreeItems();
|
|
|
|
const collapsedItems = new Set();
|
|
|
|
|
|
|
|
return items.filter(item => {
|
|
|
|
// Exclude disabled elements
|
|
|
|
if (item.disabled) return false;
|
|
|
|
|
|
|
|
// Exclude those whose parent is collapsed or loading
|
|
|
|
const parent: SlTreeItem | null | undefined = item.parentElement?.closest('[role=treeitem]');
|
|
|
|
if (parent && (!parent.expanded || parent.loading || collapsedItems.has(parent))) {
|
|
|
|
collapsedItems.add(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
return !collapsedItems.has(item);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-10 15:23:14 +00:00
|
|
|
render() {
|
|
|
|
return html`
|
2023-02-28 18:33:34 +00:00
|
|
|
<div
|
|
|
|
part="base"
|
|
|
|
class="tree"
|
|
|
|
@click=${this.handleClick}
|
|
|
|
@keydown=${this.handleKeyDown}
|
|
|
|
@mousedown=${this.handleMouseDown}
|
|
|
|
>
|
2022-11-09 21:53:53 +00:00
|
|
|
<slot @slotchange=${this.handleSlotChange}></slot>
|
2023-07-12 15:12:15 +00:00
|
|
|
<span hidden aria-hidden="true"><slot name="expand-icon"></slot></span>
|
|
|
|
<span hidden aria-hidden="true"><slot name="collapse-icon"></slot></span>
|
2022-07-10 15:23:14 +00:00
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
'sl-tree': SlTree;
|
|
|
|
}
|
|
|
|
}
|