rsuite
Version:
A suite of react components
321 lines (319 loc) • 9.7 kB
JavaScript
'use client';
import _extends from "@babel/runtime/helpers/esm/extends";
import React, { useMemo } from 'react';
import classNames from 'classnames';
import CheckTreeView from "../CheckTree/CheckTreeView.js";
import useTreeValue from "../CheckTree/hooks/useTreeValue.js";
import useFlattenTree from "../Tree/hooks/useFlattenTree.js";
import useTreeWithChildren from "../Tree/hooks/useTreeWithChildren.js";
import useExpandTree from "../Tree/hooks/useExpandTree.js";
import useFocusState from "./hooks/useFocusState.js";
import isNil from 'lodash/isNil';
import pick from 'lodash/pick';
import isFunction from 'lodash/isFunction';
import { useStyles, useCustom, useEventCallback } from "../internals/hooks/index.js";
import { forwardRef, createChainedFunction, mergeRefs } from "../internals/utils/index.js";
import { PickerToggle, onMenuKeyDown, PickerPopup, SelectedElement, PickerToggleTrigger, useToggleKeyDownEvent, usePickerRef, triggerPropKeys } from "../internals/Picker/index.js";
import { getSelectedItems } from "../CheckTree/utils.js";
import { TreeProvider, useTreeImperativeHandle } from "../internals/Tree/TreeProvider.js";
/**
* The `CheckTreePicker` component is used for selecting multiple options which are organized in a tree structure.
*
* @see https://rsuitejs.com/components/check-tree-picker
*/
const CheckTreePicker = forwardRef((props, ref) => {
const {
propsWithDefaults
} = useCustom('CheckTreePicker', props);
const {
as,
id,
appearance = 'default',
block,
cleanable = true,
countable = true,
cascade = true,
className,
classPrefix = 'picker',
childrenKey = 'children',
disabled,
data = [],
defaultValue = [],
defaultExpandAll = false,
disabledItemValues = [],
expandItemValues: controlledExpandItemValues,
defaultExpandItemValues = [],
placeholder,
popupClassName,
popupStyle,
popupAutoWidth = true,
placement = 'bottomStart',
treeHeight = 320,
toggleAs,
searchBy,
searchKeyword,
showIndentLine,
searchable = true,
style,
size,
valueKey = 'value',
value: controlledValue,
virtualized = false,
uncheckableItemValues = [],
locale,
labelKey = 'label',
listProps,
getChildren,
renderExtraFooter,
onEnter,
onChange,
onClean,
onExit,
onSearch,
onSelect,
onSelectItem,
onScroll,
onExpand,
renderValue,
renderTree,
renderTreeIcon,
renderTreeNode,
onCascadeChange,
...rest
} = propsWithDefaults;
const {
trigger,
root,
target,
overlay,
list,
searchInput,
treeView
} = usePickerRef(ref);
const {
prefix
} = useStyles(classPrefix);
const [value, setValue] = useTreeValue(controlledValue, {
defaultValue,
uncheckableItemValues
});
const itemDataKeys = {
childrenKey,
labelKey,
valueKey
};
const {
treeData,
loadingNodeValues,
appendChild
} = useTreeWithChildren(data, itemDataKeys);
const {
expandItemValues,
handleExpandTreeNode
} = useExpandTree(data, {
...itemDataKeys,
defaultExpandAll,
defaultExpandItemValues,
controlledExpandItemValues,
onExpand,
getChildren,
appendChild
});
const flattenedNodes = useFlattenTree(treeData, {
...itemDataKeys,
uncheckableItemValues,
disabledItemValues,
multiple: true,
cascade,
value
});
const selectedNodes = getSelectedItems(flattenedNodes, value);
const {
register,
focusFirstNode
} = useTreeImperativeHandle();
const {
focusItemValue,
setFocusItemValue,
active,
triggerProps
} = useFocusState({
target,
onEnter,
onExit
});
const handleClean = useEventCallback(event => {
const target = event.target;
// exclude searchbox
if (target.matches('input[role="searchbox"]') || disabled || !cleanable) {
return;
}
setFocusItemValue(null);
setValue([]);
onChange === null || onChange === void 0 || onChange([], event);
onCascadeChange === null || onCascadeChange === void 0 || onCascadeChange([], event);
});
const handleTreeKeyDown = useEventCallback(event => {
onMenuKeyDown(event, {
del: handleClean,
down: () => focusFirstNode()
});
});
const onPickerKeydown = useToggleKeyDownEvent({
toggle: !focusItemValue || !active,
trigger,
target,
overlay,
searchInput,
active,
onExit: handleClean,
onMenuKeyDown: handleTreeKeyDown,
...rest
});
// transform the parent node value to the leaf node value
const handleTransValue2Children = useEventCallback(nextSelectedNodes => {
return nextSelectedNodes.map(node => {
const currentNode = node.refKey ? flattenedNodes[node.refKey] : null;
if (currentNode && currentNode[childrenKey] && currentNode[childrenKey].length) {
const childNodes = currentNode[childrenKey].filter(child => {
const childValue = child[valueKey];
return !disabledItemValues.includes(childValue) && !uncheckableItemValues.includes(childValue);
});
return handleTransValue2Children(childNodes);
}
return node;
}).flat();
});
const handleChangeCascade = useEventCallback((nextValue, event) => {
if (!cascade) {
onCascadeChange === null || onCascadeChange === void 0 || onCascadeChange(nextValue, event);
} else {
const nextSelectedNodes = getSelectedItems(flattenedNodes, nextValue);
const childrenNodes = handleTransValue2Children(nextSelectedNodes);
const childrenValue = childrenNodes.map(node => node[valueKey]);
onCascadeChange === null || onCascadeChange === void 0 || onCascadeChange(childrenValue, event);
}
});
const handleChange = useEventCallback((nextValue, event) => {
setValue(nextValue);
onChange === null || onChange === void 0 || onChange(nextValue, event);
handleChangeCascade(nextValue, event);
});
const treeContext = useMemo(() => ({
register,
props: {
labelKey,
valueKey,
childrenKey,
virtualized,
renderTreeIcon,
renderTreeNode
}
}), [childrenKey, labelKey, valueKey, virtualized, register, renderTreeIcon, renderTreeNode]);
const checkTreeView = /*#__PURE__*/React.createElement(TreeProvider, {
value: treeContext
}, /*#__PURE__*/React.createElement(CheckTreeView, {
ref: treeView,
disabledItemValues: disabledItemValues,
expandItemValues: expandItemValues,
uncheckableItemValues: uncheckableItemValues,
cascade: cascade,
data: treeData,
height: treeHeight,
showIndentLine: showIndentLine,
listProps: listProps,
listRef: list,
locale: locale,
searchBy: searchBy,
searchable: searchable,
searchKeyword: searchKeyword,
searchInputRef: searchInput,
onScroll: onScroll,
onSelect: onSelect,
onSelectItem: onSelectItem,
onExpand: handleExpandTreeNode,
onSearch: onSearch,
onChange: handleChange,
onFocusItem: setFocusItemValue,
value: value,
loadingNodeValues: loadingNodeValues,
flattenedNodes: flattenedNodes
}));
const renderTreeView = (positionProps, speakerRef) => {
const {
className
} = positionProps;
const classes = classNames(className, popupClassName, prefix('check-tree-menu'));
return /*#__PURE__*/React.createElement(PickerPopup, {
ref: mergeRefs(overlay, speakerRef),
autoWidth: popupAutoWidth,
className: classes,
style: popupStyle,
onKeyDown: onPickerKeydown,
target: trigger
}, renderTree ? renderTree(checkTreeView) : checkTreeView, renderExtraFooter === null || renderExtraFooter === void 0 ? void 0 : renderExtraFooter());
};
/**
* 1.Have a value and the value is valid.
* 2.Regardless of whether the value is valid, as long as renderValue is set, it is judged to have a value.
*/
let hasValidValue = selectedNodes.length > 0 || value.length > 0 && isFunction(renderValue);
let selectedElement = placeholder;
if (hasValidValue) {
selectedElement = /*#__PURE__*/React.createElement(SelectedElement, {
selectedItems: selectedNodes,
countable: countable,
valueKey: valueKey,
labelKey: labelKey,
childrenKey: childrenKey,
prefix: prefix,
cascade: cascade,
locale: locale,
badgeSize: size
});
if (isFunction(renderValue)) {
selectedElement = renderValue(value, selectedNodes, selectedElement);
if (isNil(selectedElement)) {
hasValidValue = false;
}
}
}
return /*#__PURE__*/React.createElement(PickerToggleTrigger, {
as: as,
id: id,
pickerType: "check-tree",
block: block,
disabled: disabled,
appearance: appearance,
popupType: "tree",
multiple: true,
triggerProps: {
...pick(props, triggerPropKeys),
...triggerProps
},
ref: trigger,
placement: placement,
speaker: renderTreeView,
rootRef: root,
style: style,
classPrefix: classPrefix,
className: className
}, /*#__PURE__*/React.createElement(PickerToggle, _extends({
ref: target,
appearance: appearance,
onKeyDown: onPickerKeydown,
onClean: createChainedFunction(handleClean, onClean),
cleanable: cleanable && !disabled,
countable: countable,
disabled: disabled,
as: toggleAs,
hasValue: hasValidValue,
active: active,
placement: placement,
inputValue: value,
focusItemValue: focusItemValue,
size: size
}, rest), selectedElement || (locale === null || locale === void 0 ? void 0 : locale.placeholder)));
});
CheckTreePicker.displayName = 'CheckTreePicker';
export default CheckTreePicker;