UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

145 lines 4.94 kB
import { ObservableValue } from '@furystack/utils'; import { ListService } from './list-service.js'; /** * Service for managing tree state including expand/collapse, hierarchy navigation, * and flattening the tree into a visible items list for rendering */ export class TreeService extends ListService { treeOptions; expandedNodes = new ObservableValue(new Set()); rootItems = new ObservableValue([]); flattenedNodes = new ObservableValue([]); [Symbol.dispose]() { super[Symbol.dispose](); this.expandedNodes[Symbol.dispose](); this.rootItems[Symbol.dispose](); this.flattenedNodes[Symbol.dispose](); } /** * Checks whether a node is currently expanded */ isExpanded = (item) => this.expandedNodes.getValue().has(item); /** * Expands a node, making its children visible */ expand = (item) => { const expanded = new Set(this.expandedNodes.getValue()); expanded.add(item); this.expandedNodes.setValue(expanded); this.updateFlattenedNodes(); }; /** * Collapses a node, hiding its children */ collapse = (item) => { const expanded = new Set(this.expandedNodes.getValue()); expanded.delete(item); this.expandedNodes.setValue(expanded); this.updateFlattenedNodes(); }; /** * Toggles the expanded state of a node */ toggleExpanded = (item) => { if (this.isExpanded(item)) { this.collapse(item); } else { const children = this.treeOptions.getChildren(item); if (children.length > 0) { this.expand(item); } } }; /** * Finds the parent of a given item in the tree */ getParent(item) { const findParent = (nodes) => { for (const node of nodes) { const children = this.treeOptions.getChildren(node); if (children.includes(item)) { return node; } const found = findParent(children); if (found) return found; } return undefined; }; return findParent(this.rootItems.getValue()); } /** * Flattens the tree based on which nodes are expanded, and syncs the result * to both flattenedNodes and the inherited ListService items */ updateFlattenedNodes() { const expanded = this.expandedNodes.getValue(); const result = []; const flatten = (nodes, level) => { for (const node of nodes) { const children = this.treeOptions.getChildren(node); const hasChildren = children.length > 0; const isExpanded = expanded.has(node); result.push({ item: node, level, hasChildren, isExpanded }); if (hasChildren && isExpanded) { flatten(children, level + 1); } } }; flatten(this.rootItems.getValue(), 0); this.flattenedNodes.setValue(result); this.items.setValue(result.map((n) => n.item)); } /** * Gets the FlattenedTreeNode for a given item */ getNodeInfo(item) { return this.flattenedNodes.getValue().find((n) => n.item === item); } handleKeyDown(ev) { const hasFocus = this.hasFocus.getValue(); const focusedItem = this.focusedItem.getValue(); if (hasFocus && focusedItem) { switch (ev.key) { case 'ArrowRight': { const children = this.treeOptions.getChildren(focusedItem); if (children.length > 0 && !this.isExpanded(focusedItem)) { ev.preventDefault(); this.expand(focusedItem); } return; } case 'ArrowLeft': { if (this.isExpanded(focusedItem)) { ev.preventDefault(); this.collapse(focusedItem); } else { const parent = this.getParent(focusedItem); if (parent) { ev.preventDefault(); this.focusedItem.setValue(parent); } } return; } default: { break; } } } super.handleKeyDown(ev); } handleItemDoubleClick(item) { const children = this.treeOptions.getChildren(item); if (children.length > 0) { this.toggleExpanded(item); } } constructor(treeOptions) { super(treeOptions); this.treeOptions = treeOptions; } } //# sourceMappingURL=tree-service.js.map