@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
175 lines (174 loc) • 6.8 kB
JavaScript
"use client";
import { FocusScope } from "../FocusScope/FocusScope.mjs";
import { focusScopeItem, useScopeArrowNav } from "../FocusScope/useScopeArrowNav.mjs";
import { styles } from "./style.mjs";
import { TreeContext } from "./context.mjs";
import { conductCheck, flattenVisible, getAllKeys, getAncestorKeys, getSubtreeKeys } from "./utils.mjs";
import TreeNode from "./TreeNode.mjs";
import { memo, useId, useMemo, useRef, useState } from "react";
import { jsx } from "react/jsx-runtime";
import { cx } from "antd-style";
import useMergeState from "use-merge-value";
//#region src/base-ui/Tree/Tree.tsx
const EMPTY = [];
const Tree = memo(({ blockNode = false, checkStrictly = false, checkable = false, checkedKeys: checkedKeysProp, className, classNames = {}, defaultCheckedKeys = EMPTY, defaultExpandAll = false, defaultExpandedKeys = EMPTY, defaultSelectedKeys = EMPTY, disabled = false, expandedKeys: expandedKeysProp, indent = 20, multiple = false, onCheck, onExpand, onRightClick, onSelect, scopeId: scopeIdProp, selectedKeys: selectedKeysProp, showIcon = false, showLine = false, size = "middle", style, styles: customStyles = {}, switcherIcon, titleRender, treeData, vimKeys = false }) => {
const generatedId = useId();
const scopeId = scopeIdProp ?? generatedId;
const [expandedKeys, setExpandedKeys] = useMergeState(defaultExpandAll ? getAllKeys(treeData) : defaultExpandedKeys, { value: expandedKeysProp });
const [selectedKeys, setSelectedKeys] = useMergeState(defaultSelectedKeys, { value: selectedKeysProp });
const [checkedKeys, setCheckedKeys] = useMergeState(defaultCheckedKeys, { value: checkedKeysProp });
const expanded = useMemo(() => new Set(expandedKeys), [expandedKeys]);
const selected = useMemo(() => new Set(selectedKeys), [selectedKeys]);
const flat = useMemo(() => flattenVisible(treeData, expanded), [treeData, expanded]);
const { checked, halfChecked } = useMemo(() => checkStrictly ? {
checked: new Set(checkedKeys),
halfChecked: /* @__PURE__ */ new Set()
} : conductCheck(treeData, checkedKeys), [
checkStrictly,
treeData,
checkedKeys
]);
const [activeKeyState, setActiveKeyState] = useState(null);
const setActiveKey = setActiveKeyState;
const activeKey = activeKeyState && flat.some((row) => row.node.key === activeKeyState) ? activeKeyState : selectedKeys.find((key) => flat.some((row) => row.node.key === key)) ?? flat[0]?.node.key ?? null;
const anchorRef = useRef(null);
const rowOf = (key) => document.querySelector(`[data-focus-scope="${scopeId}"] [data-id="${key}"]`);
const focusRow = (key) => {
setActiveKey(key);
const row = rowOf(key);
if (row) focusScopeItem(scopeId, row);
};
const isDisabled = (node) => disabled || !!node.disabled;
const toggleExpand = (node) => {
const willExpand = !expanded.has(node.key);
const next = willExpand ? [...expandedKeys, node.key] : expandedKeys.filter((key) => key !== node.key);
setExpandedKeys(next);
onExpand?.(next, {
expanded: willExpand,
node
});
};
const toggleSelect = (node, event) => {
if (isDisabled(node) || node.selectable === false) return;
let next;
if (multiple && event?.shiftKey && anchorRef.current) {
const a = flat.findIndex((row) => row.node.key === anchorRef.current);
const b = flat.findIndex((row) => row.node.key === node.key);
next = flat.slice(Math.min(a, b), Math.max(a, b) + 1).filter((row) => !isDisabled(row.node) && row.node.selectable !== false).map((row) => row.node.key);
} else if (multiple && (event?.metaKey || event?.ctrlKey)) {
next = selected.has(node.key) ? selectedKeys.filter((key) => key !== node.key) : [...selectedKeys, node.key];
anchorRef.current = node.key;
} else {
next = [node.key];
anchorRef.current = node.key;
}
setSelectedKeys(next);
setActiveKey(node.key);
onSelect?.(next, {
node,
selected: next.includes(node.key)
});
};
const toggleCheck = (node) => {
if (isDisabled(node) || node.checkable === false) return;
const willCheck = !checked.has(node.key);
const keys = checkStrictly ? [node.key] : getSubtreeKeys(node);
const base = new Set(checkStrictly ? checkedKeys : checked);
keys.forEach((key) => willCheck ? base.add(key) : base.delete(key));
if (!willCheck && !checkStrictly) getAncestorKeys(treeData, node.key).forEach((key) => base.delete(key));
const next = checkStrictly ? [...base] : [...conductCheck(treeData, base).checked];
setCheckedKeys(next);
setActiveKey(node.key);
onCheck?.(next, {
checked: willCheck,
node
});
};
const currentRow = () => {
const key = (document.activeElement?.closest("[role=\"treeitem\"]"))?.dataset.id ?? activeKey;
return flat.find((row) => row.node.key === key);
};
const withRow = (fn) => (event) => {
const row = currentRow();
if (row) fn(row, event);
};
useScopeArrowNav({
extra: {
" ": withRow((row, event) => {
if (checkable && row.node.checkable !== false) toggleCheck(row.node);
else toggleSelect(row.node, event);
}),
"*": withRow((row) => {
const siblings = flat.filter((r) => r.parentKey === row.parentKey && r.hasChildren && !expanded.has(r.node.key));
if (siblings.length === 0) return;
const next = [...expandedKeys, ...siblings.map((r) => r.node.key)];
setExpandedKeys(next);
onExpand?.(next, {
expanded: true,
node: row.node
});
}),
ArrowLeft: withRow((row) => {
if (row.hasChildren && expanded.has(row.node.key)) toggleExpand(row.node);
else if (row.parentKey) focusRow(row.parentKey);
}),
ArrowRight: withRow((row) => {
if (!row.hasChildren) return;
if (expanded.has(row.node.key)) focusRow(flat[row.index + 1].node.key);
else toggleExpand(row.node);
}),
Enter: withRow((row, event) => toggleSelect(row.node, event))
},
itemSelector: "[role=\"treeitem\"]",
onItemFocus: (el) => el.dataset.id && setActiveKey(el.dataset.id),
scopeId,
vimKeys
});
return /* @__PURE__ */ jsx(TreeContext, {
value: {
activeKey,
blockNode,
checkable,
checked,
classNames,
disabled,
expanded,
halfChecked,
indent,
onRightClick: (event, node) => onRightClick?.({
event,
node
}),
selected,
setActiveKey,
showIcon,
showLine,
size,
styles: customStyles,
switcherIcon,
titleRender,
toggleCheck,
toggleExpand,
toggleSelect
},
children: /* @__PURE__ */ jsx(FocusScope, {
className: cx(styles.root, classNames.root, className),
id: scopeId,
role: "tree",
style: {
...customStyles.root,
...style
},
children: treeData.map((node, i) => /* @__PURE__ */ jsx(TreeNode, {
depth: 0,
isLast: i === treeData.length - 1,
node,
trail: []
}, node.key))
})
});
});
Tree.displayName = "Tree";
//#endregion
export { Tree as default };
//# sourceMappingURL=Tree.mjs.map