@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
JavaScript
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 }));
}