rsuite
Version:
A suite of react components
305 lines (300 loc) • 9.45 kB
JavaScript
'use client';
import _extends from "@babel/runtime/helpers/esm/extends";
import React, { useEffect, useMemo } from 'react';
import isNil from 'lodash/isNil';
import CheckTreeNode from "./CheckTreeNode.js";
import IndentLine from "../Tree/IndentLine.js";
import SearchBox from "../internals/SearchBox/index.js";
import Box from "../internals/Box/index.js";
import useTreeSearch from "../Tree/hooks/useTreeSearch.js";
import useFocusTree from "../Tree/hooks/useFocusTree.js";
import useVirtualizedTreeData from "../Tree/hooks/useVirtualizedTreeData.js";
import useTreeCheckState from "./hooks/useTreeCheckState.js";
import useTreeNodeProps from "./hooks/useTreeNodeProps.js";
import { forwardRef } from "../internals/utils/index.js";
import { List, AutoSizer, defaultItemSize } from "../internals/Windowing/index.js";
import { useStyles, useCustom, useEventCallback } from "../internals/hooks/index.js";
import { getPathTowardsItem, getKeyParentMap } from "../internals/Tree/utils/index.js";
import { onMenuKeyDown } from "../internals/Picker/index.js";
import { TreeView } from "../internals/Tree/index.js";
import { hasGrandchild, isEveryFirstLevelNodeUncheckable, getFormattedTree, isNodeUncheckable } from "./utils.js";
import { hasVisibleChildren, getActiveItem, isExpand } from "../Tree/utils/index.js";
import { useTreeContextProps } from "../internals/Tree/TreeProvider.js";
/**
* Props for the CheckTreeView component.
*/
const CheckTreeView = forwardRef((props, ref) => {
const {
as,
className,
classPrefix = 'check-tree',
cascade = true,
data = [],
disabledItemValues = [],
expandItemValues = [],
height = 360,
locale: overrideLocale,
listProps,
listRef,
style,
searchKeyword,
showIndentLine,
searchable,
searchInputRef,
uncheckableItemValues = [],
loadingNodeValues = [],
flattenedNodes = {},
searchBy,
onChange,
onSearch,
onSelect,
onSelectItem,
onScroll,
onExpand,
onFocusItem,
...rest
} = props;
const {
getLocale
} = useCustom();
const {
searchPlaceholder,
noResultsText
} = getLocale('Combobox', overrideLocale);
const {
childrenKey,
valueKey,
virtualized,
scrollShadow
} = useTreeContextProps();
const {
prefix,
merge,
withPrefix
} = useStyles(classPrefix);
const {
getCheckedValues
} = useTreeCheckState({
cascade,
flattenedNodes,
uncheckableItemValues,
disabledItemValues
});
const handleSearchCallback = (value, _data, event) => {
onSearch === null || onSearch === void 0 || onSearch(value, event);
};
const {
filteredData,
keyword,
setFilteredData,
handleSearch
} = useTreeSearch({
callback: handleSearchCallback,
data,
searchKeyword,
searchBy
});
const {
focusItemValue,
setFocusItemValue,
onTreeKeydown,
saveTreeNodeRef
} = useFocusTree({
filteredData,
disabledItemValues,
expandItemValues,
searchKeyword: keyword,
flattenedNodes,
onFocused: onFocusItem,
onExpand
});
const transformation = useVirtualizedTreeData(flattenedNodes, filteredData, {
cascade,
expandItemValues,
searchKeyword: keyword,
disabledItemValues
});
/**
* Get formatted nodes for render tree
* @params render - renderNode function. only used when virtualized setting false
*/
const getFormattedNodes = render => {
if (virtualized) {
return transformation().filter(item => item.visible);
}
return getFormattedTree(flattenedNodes, filteredData, {
childrenKey,
cascade,
disabledItemValues,
valueKey
}).map(node => render === null || render === void 0 ? void 0 : render(node, 1)).filter(item => item);
};
const getTreeNodeProps = useTreeNodeProps({
uncheckableItemValues,
disabledItemValues,
loadingNodeValues,
focusItemValue,
flattenedNodes,
keyword
});
useEffect(() => {
setFilteredData(data, keyword);
}, [data, keyword, setFilteredData]);
// TODO-Doma
// Replace `getKeyParentMap` with `getParentMap`
const itemParentMap = useMemo(() => getKeyParentMap(data, node => node[valueKey], node => node[childrenKey]), [childrenKey, data, valueKey]);
const handleSelect = useEventCallback((node, event) => {
const currentNode = node.refKey ? flattenedNodes[node.refKey] : null;
if (!node || !currentNode) {
return;
}
const checkedValues = getCheckedValues(node, !currentNode.check);
const path = getPathTowardsItem(node, item => itemParentMap.get(item[valueKey]));
setFocusItemValue(node[valueKey]);
onChange === null || onChange === void 0 || onChange(checkedValues, event);
onSelect === null || onSelect === void 0 || onSelect(node, checkedValues, event);
onSelectItem === null || onSelectItem === void 0 || onSelectItem(node, path);
});
const selectActiveItem = event => {
if (isNil(focusItemValue)) return;
const activeItem = getActiveItem(focusItemValue, flattenedNodes, valueKey);
if (!isNodeUncheckable(activeItem, {
uncheckableItemValues,
valueKey
}) && activeItem !== null) {
handleSelect(activeItem, event);
}
};
const handleTreeKeyDown = useEventCallback(event => {
onTreeKeydown(event);
onMenuKeyDown(event, {
enter: selectActiveItem
});
});
const renderNode = (node, layer) => {
const {
visible,
refKey,
parent
} = node;
// when searching, all nodes should be expand
const expanded = isExpand(keyword, expandItemValues.includes(node[valueKey]));
if (!visible) {
return null;
}
const children = node[childrenKey];
const hasChildren = keyword ? hasVisibleChildren(node, childrenKey) : Boolean(children);
const treeNodeProps = {
// The spread operator does not copy non-enumerable properties,
// so we need to copy the `parent` property manually.
...getTreeNodeProps({
...node,
parent
}),
layer,
expanded,
hasChildren,
onSelect: handleSelect,
onExpand
};
if (hasChildren) {
layer += 1;
const childClassName = merge(prefix('node-children'), {
[prefix('node-expanded')]: expanded
});
const nodes = children || [];
return /*#__PURE__*/React.createElement("div", {
className: childClassName,
key: node[valueKey]
}, /*#__PURE__*/React.createElement(CheckTreeNode, _extends({}, treeNodeProps, {
treeItemRef: ref => saveTreeNodeRef(ref, refKey)
})), /*#__PURE__*/React.createElement("div", {
className: prefix('group'),
role: "group"
}, nodes.map(child => renderNode(child, layer)), showIndentLine && /*#__PURE__*/React.createElement(IndentLine, null)));
}
return /*#__PURE__*/React.createElement(CheckTreeNode, _extends({
key: node[valueKey],
treeItemRef: ref => saveTreeNodeRef(ref, refKey)
}, treeNodeProps));
};
const renderVirtualListNode = ({
index,
style,
data
}) => {
const node = data[index];
const {
layer,
refKey,
visible,
hasChildren,
parent
} = node;
const expanded = isExpand(keyword, expandItemValues.includes(node[valueKey]));
const treeNodeProps = {
// The spread operator does not copy non-enumerable properties,
// so we need to copy the `parent` property manually.
...getTreeNodeProps({
...node,
parent
}),
onSelect: handleSelect,
onExpand,
expanded,
layer,
hasChildren
};
return visible && /*#__PURE__*/React.createElement(CheckTreeNode, _extends({
style: style,
ref: ref => saveTreeNodeRef(ref, refKey)
}, treeNodeProps));
};
const classes = merge(className, withPrefix({
'without-children': !hasGrandchild(data, childrenKey),
virtualized
}));
const formattedNodes = getFormattedNodes(renderNode);
const treeNodesClass = merge(prefix('root'), {
[prefix('all-uncheckable')]: isEveryFirstLevelNodeUncheckable(flattenedNodes, uncheckableItemValues, valueKey)
});
return /*#__PURE__*/React.createElement(Box, {
as: as,
ref: ref,
className: classes,
style: style
}, searchable ? /*#__PURE__*/React.createElement(SearchBox, {
placeholder: searchPlaceholder,
onChange: handleSearch,
value: keyword,
inputRef: searchInputRef
}) : null, keyword && formattedNodes.length === 0 ? /*#__PURE__*/React.createElement("div", {
className: prefix('empty')
}, noResultsText) : null, /*#__PURE__*/React.createElement(TreeView, _extends({}, rest, {
multiselectable: true,
treeRootClassName: treeNodesClass,
className: prefix('view'),
onScroll: onScroll,
onKeyDown: handleTreeKeyDown,
height: height
}), virtualized ? /*#__PURE__*/React.createElement(AutoSizer, {
defaultHeight: height,
style: {
width: 'auto',
height: 'auto'
},
className: prefix('virt-auto-sizer')
}, ({
height
}) => /*#__PURE__*/React.createElement(List, _extends({
ref: listRef,
height: height,
itemSize: defaultItemSize,
itemCount: formattedNodes.length,
itemData: formattedNodes,
className: prefix('virt-list'),
scrollShadow: scrollShadow
}, listProps), renderVirtualListNode)) : formattedNodes));
});
CheckTreeView.displayName = 'CheckTreeView';
export default CheckTreeView;