UNPKG

@adaptabletools/adaptable

Version:

Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

394 lines (393 loc) 17.3 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, useMemo, useState } from 'react'; import { ChevronRightIcon, ChevronDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, } from 'lucide-react'; import { TreeExpandState, TreeSelectionState } from '../../InfiniteTable'; import { GRID_FILTER_COMBBOX_ADJUSTMENTS_CLASSNAME, MultiCombobox, } from '../../Combobox'; import { ComboboxChip } from '../../ui/combobox'; import { toDisplayValueDefault, toDisplayValueFromOptionTree } from '../treeUtils'; import { cn } from '../../../lib/utils'; import { CheckBox } from '../../CheckBox'; export { toDisplayValueDefault, toDisplayValueFromOptionTree }; const encodePath = (p) => JSON.stringify(p); const decodePath = (s) => JSON.parse(s); function normalizePaths(value) { if (!Array.isArray(value)) { return []; } return value.map((v) => (Array.isArray(v) ? v : [v])); } function flattenOptions(options, primaryKey, labelField, parentPath = [], result = []) { for (const option of options) { const id = option[primaryKey]; const path = [...parentPath, id]; const children = option.children; const hasChildren = Array.isArray(children) && children.length > 0; result.push({ path, pathKey: encodePath(path), label: option[labelField] != null ? String(option[labelField]) : String(id), depth: parentPath.length, hasChildren, }); if (hasChildren) { flattenOptions(children, primaryKey, labelField, path, result); } } return result; } function TreeItemLabel(props) { const { node, isExpanded, onToggleExpand, selected, reserveExpandSpace } = props; const handleChevronMouseDown = (e) => { e.preventDefault(); e.stopPropagation(); }; const handleChevronClick = (e) => { e.preventDefault(); e.stopPropagation(); onToggleExpand(node.path); }; const indent = node.depth > 0 ? `calc(${node.depth} * var(--ab-tree-indent-size))` : 0; const iconProps = { className: 'twa:size-4', }; return (_jsxs(_Fragment, { children: [node.hasChildren ? (_jsx("button", { type: "button", tabIndex: -1, "aria-label": isExpanded ? 'Collapse' : 'Expand', "data-name": "expand-collapse-icon", onMouseDown: handleChevronMouseDown, onClick: handleChevronClick, style: { order: -1, marginLeft: indent }, className: cn('twa:bg-transparent twa:text-foreground', 'twa:inline-flex twa:shrink-0 twa:items-center twa:justify-center twa:rounded', 'twa:cursor-pointer'), children: isExpanded ? _jsx(ChevronDownIcon, { ...iconProps }) : _jsx(ChevronRightIcon, { ...iconProps }) })) : reserveExpandSpace ? (_jsx("span", { "aria-hidden": "true", style: { order: -1, marginLeft: indent }, className: "twa:inline-block twa:size-4 twa:shrink-0" })) : null, _jsx(CheckBox, { checked: selected, checkSquareClassName: cn(selected === true && 'twa:ring-1 twa:ring-accent-foreground') }), _jsx("span", { className: "twa:truncate twa:min-w-0 twa:flex-1", children: node.label })] })); } export function TreeDropdown(props) { const { isLoading = false } = props; const labelField = (props.labelField ?? 'label'); const primaryKey = (props.primaryKey ?? 'id'); const allNodes = useMemo(() => { return flattenOptions(props.items, primaryKey, labelField); }, [props.items, primaryKey, labelField]); const nodeByKey = useMemo(() => { const map = new Map(); for (const n of allNodes) { map.set(n.pathKey, n); } return map; }, [allNodes]); const treePaths = useMemo(() => allNodes.map((n) => n.path), [allNodes]); const [internalSelection, setInternalSelection] = useState(() => ({ defaultSelection: false, selectedPaths: normalizePaths(props.value ?? props.defaultValue), deselectedPaths: [], })); const [lastPropsValue, setLastPropsValue] = useState(props.value); if (props.value !== undefined && props.value !== lastPropsValue) { setLastPropsValue(props.value); setInternalSelection({ defaultSelection: false, selectedPaths: normalizePaths(props.value), deselectedPaths: [], }); } const selectionState = useMemo(() => { return new TreeSelectionState(internalSelection, { treePaths, strictCheckPaths: false, }); }, [internalSelection, treePaths]); const [internalExpand, setInternalExpand] = useState(() => ({ defaultExpanded: false, expandedPaths: [], collapsedPaths: [], })); const expandState = useMemo(() => { return new TreeExpandState(internalExpand); }, [internalExpand]); const toggleExpand = useCallback((path) => { const next = new TreeExpandState(internalExpand); next.setNodeExpanded(path, !next.isNodeExpanded(path)); setInternalExpand(next.getState()); }, [internalExpand]); const branchNodes = useMemo(() => allNodes.filter((n) => n.hasChildren), [allNodes]); const anyBranchExpanded = useMemo(() => { for (const n of branchNodes) { if (expandState.isNodeExpanded(n.path)) { return true; } } return false; }, [branchNodes, expandState]); const toggleExpandAll = useCallback(() => { const next = new TreeExpandState(internalExpand); if (anyBranchExpanded) { next.collapseAll(); } else { next.expandAll(); } setInternalExpand(next.getState()); }, [internalExpand, anyBranchExpanded]); const [searchQuery, setSearchQuery] = useState(''); const matchAncestorKeys = useMemo(() => { if (!searchQuery) { return null; } const q = searchQuery.toLowerCase(); const keys = new Set(); for (const node of allNodes) { if (node.label.toLowerCase().includes(q)) { for (let i = 1; i <= node.path.length; i++) { keys.add(encodePath(node.path.slice(0, i))); } } } return keys; }, [allNodes, searchQuery]); const expandPathsForQuery = useCallback((query) => { if (!query) { return; } const q = query.toLowerCase(); const next = new TreeExpandState(internalExpand); let mutated = false; for (let i = 0; i < allNodes.length; i++) { const node = allNodes[i]; if (!node.label.toLowerCase().includes(q)) { continue; } for (let j = 1; j < node.path.length; j++) { const ancPath = node.path.slice(0, j); if (!next.isNodeExpanded(ancPath)) { next.setNodeExpanded(ancPath, true); mutated = true; } } if (node.hasChildren && !next.isNodeExpanded(node.path)) { next.setNodeExpanded(node.path, true); mutated = true; } for (let j = i + 1; j < allNodes.length; j++) { const desc = allNodes[j]; if (desc.depth <= node.depth) { break; } if (desc.hasChildren && !next.isNodeExpanded(desc.path)) { next.setNodeExpanded(desc.path, true); mutated = true; } } } if (mutated) { setInternalExpand(next.getState()); } }, [allNodes, internalExpand]); const handleSearchInputChange = useCallback((value) => { setSearchQuery(value); expandPathsForQuery(value); }, [expandPathsForQuery]); const reserveExpandSpace = branchNodes.length > 0; const items = useMemo(() => { const visible = []; for (const node of allNodes) { let ancestorsExpanded = true; for (let i = 1; i < node.path.length; i++) { if (!expandState.isNodeExpanded(node.path.slice(0, i))) { ancestorsExpanded = false; break; } } if (!ancestorsExpanded) { continue; } const isExpanded = expandState.isNodeExpanded(node.path); const selected = selectionState.isNodeSelected(node.path); visible.push({ value: node.pathKey, textLabel: node.label, label: (_jsx(TreeItemLabel, { node: node, isExpanded: isExpanded, onToggleExpand: toggleExpand, selected: selected, reserveExpandSpace: reserveExpandSpace })), path: node.path, depth: node.depth, hasChildren: node.hasChildren, isLeaf: !node.hasChildren, }); } return visible; }, [allNodes, expandState, toggleExpand, selectionState, reserveExpandSpace]); const value = useMemo(() => { const result = []; for (const item of items) { if (selectionState.isNodeSelected(item.path) === true) { result.push(item.value); } } return result; }, [items, selectionState]); const valueSet = useMemo(() => new Set(value), [value]); const { onValueChange: onValueChangeFromProps } = props; const onValueChange = useCallback((nextValues) => { const next = new TreeSelectionState(internalSelection, { treePaths, strictCheckPaths: false, }); if (nextValues.length === 0) { next.deselectAll(); } else { const nextSet = new Set(nextValues); for (const key of nextSet) { if (!valueSet.has(key)) { next.setNodeSelection(decodePath(key), true); } } for (const key of valueSet) { if (!nextSet.has(key)) { next.setNodeSelection(decodePath(key), false); } } } setInternalSelection(next.getState()); onValueChangeFromProps?.(next.getSelectedLeafNodePaths()); }, [internalSelection, treePaths, valueSet, onValueChangeFromProps]); const filter = useCallback((item, query) => { const q = query.toLowerCase(); if (item.textLabel.toLowerCase().includes(q)) { return true; } for (let i = 1; i < item.path.length; i++) { const anc = nodeByKey.get(encodePath(item.path.slice(0, i))); if (anc && anc.label.toLowerCase().includes(q)) { return true; } } if (matchAncestorKeys?.has(encodePath(item.path))) { return true; } return false; }, [nodeByKey, matchAncestorKeys]); const selectedLeafPaths = useMemo(() => { return selectionState.getSelectedLeafNodePaths(); }, [selectionState]); const { toDisplayValue, showClear } = props; const chipLabelForPath = useCallback((path) => { if (toDisplayValue) { return toDisplayValue([path]); } return path .map((segment, idx) => { const anc = nodeByKey.get(encodePath(path.slice(0, idx + 1))); return anc ? anc.label : String(segment); }) .join('-'); }, [nodeByKey, toDisplayValue]); const deselectLeafPath = useCallback((path) => { const next = new TreeSelectionState(internalSelection, { treePaths, strictCheckPaths: false, }); next.setNodeSelection(path, false); setInternalSelection(next.getState()); onValueChangeFromProps?.(next.getSelectedLeafNodePaths()); }, [internalSelection, treePaths, onValueChangeFromProps]); const showRemoveChip = showClear !== false; const { renderSelectedValues } = props; const renderInputValues = useCallback(() => { if (renderSelectedValues) { return renderSelectedValues({ selectedLeafPaths, getLabelForPath: chipLabelForPath, }); } if (selectedLeafPaths.length === 0) { return null; } return (_jsx(_Fragment, { children: selectedLeafPaths.map((path) => { const key = encodePath(path); const labelText = chipLabelForPath(path); return (_jsx(ComboboxChip, { showRemove: showRemoveChip, onRemove: () => deselectLeafPath(path), className: 'twa:overflow-hidden', "aria-label": labelText, children: labelText }, key)); }) })); }, [renderSelectedValues, selectedLeafPaths, chipLabelForPath, showRemoveChip, deselectLeafPath]); const allLeafPaths = useMemo(() => allNodes.filter((n) => !n.hasChildren).map((n) => n.path), [allNodes]); const selectAllChecked = useMemo(() => { if (allLeafPaths.length === 0) { return false; } let selected = 0; for (const p of allLeafPaths) { if (selectionState.isNodeSelected(p) === true) { selected++; } } if (selected === 0) { return false; } if (selected === allLeafPaths.length) { return true; } return null; }, [allLeafPaths, selectionState]); const onSelectAllChange = useCallback((checked) => { const next = new TreeSelectionState(internalSelection, { treePaths, strictCheckPaths: false, }); if (checked) { next.selectAll(); } else { next.deselectAll(); } setInternalSelection(next.getState()); onValueChangeFromProps?.(next.getSelectedLeafNodePaths()); }, [internalSelection, treePaths, onValueChangeFromProps]); const renderSearchInputTrailing = useCallback(() => { if (branchNodes.length === 0) { return null; } const title = anyBranchExpanded ? 'Collapse all' : 'Expand all'; return (_jsx("button", { type: "button", "data-name": "tree-expand-all-toggle", title: title, "aria-label": title, onMouseDown: (e) => { e.preventDefault(); }, onClick: (e) => { e.preventDefault(); toggleExpandAll(); }, className: cn('ab-NewTreeDropdown-expand-all', 'twa:inline-flex twa:shrink-0 twa:items-center twa:justify-center', 'twa:bg-background', 'twa:size-6 twa:rounded-md twa:text-muted-foreground', 'twa:hover:text-foreground twa:hover:bg-ring/50 twa:cursor-pointer'), children: anyBranchExpanded ? (_jsx(ChevronsDownUpIcon, { className: "twa:size-4" })) : (_jsx(ChevronsUpDownIcon, { className: "twa:size-4" })) })); }, [branchNodes.length, anyBranchExpanded, toggleExpandAll]); const comboboxProps = { isLoading, items, value, onValueChange, searchable: 'menulist', virtualized: true, showSelectAllCheckbox: true, selectAllCheckboxValue: selectAllChecked, onSelectAllCheckboxChange: onSelectAllChange, renderCheckboxIndicator: () => null, resizable: props.resizable, placeholder: props.placeholder, showClear: showRemoveChip, 'data-name': props['data-name'] ?? 'Select Values', renderItemLabel: (defaultLabel) => { return defaultLabel; }, filter, inputValue: searchQuery, onInputValueChange: handleSearchInputChange, onOpenChange: (open) => { if (!open) { setSearchQuery(''); } props.onOpenChange?.(open); }, renderInputValues, renderSearchInputTrailing, className: cn('ab-NewTreeDropdown', props.className), }; return (_jsx(MultiCombobox, { ...comboboxProps })); } export function GridFilterTreeDropdown(props) { const { showSelectedCount = false, placeholder = 'Select...', className, ...rest } = props; const renderSelectedValues = useCallback(({ selectedLeafPaths, getLabelForPath }) => { let children; if (selectedLeafPaths.length === 0) { children = (_jsx("span", { "data-name": "placeholder", className: "twa:text-muted-foreground", children: placeholder })); } else { children = selectedLeafPaths.map((path) => getLabelForPath(path)).join(', '); } return (_jsxs("div", { className: "twa:text-ellipsis twa:overflow-hidden twa:whitespace-nowrap twa:flex-1000", "data-slot": "combobox-selected-values", children: [showSelectedCount && selectedLeafPaths.length > 0 && (_jsxs("span", { "data-name": "multiple-values-count", className: "twa:mr-0.5", children: ["(", selectedLeafPaths.length, ")"] })), children] })); }, [placeholder, showSelectedCount]); const mergedClassName = cn(GRID_FILTER_COMBBOX_ADJUSTMENTS_CLASSNAME, className); return (_jsx(TreeDropdown, { ...rest, placeholder: placeholder, className: mergedClassName, showClear: rest.showClear ?? false, resizable: rest.resizable ?? true, renderSelectedValues: renderSelectedValues })); }