import React, { useRef, useEffect, useState, useCallback, useMemo, } from "react"; import classNames from "classnames"; import { useDefaultValue } from "../form"; import { Popover } from "../popover"; import { Input } from "../input"; import { Icon } from "../icon"; import { Tag } from "../tag"; import { CascaderMultipleProps, CascaderBoxProps, CascaderData, CascaderOption, } from "./CascaderProps"; import { CascaderMenuBox } from "./CascadeMenu"; import { CascaderBox, getOptions } from "./CascaderBox"; import { CascaderSearch } from "./CascaderSearch"; import { TagSelectInput } from "../tagselect/TagSelectInput"; import { useDefault } from "../_util/use-default"; import { useConfig } from "../_util/config-context"; import { useTranslation } from "../i18n"; import { noop } from "../_util/noop"; import { mergeEventProps } from "../_util/merge-event-props"; import { forwardRefWithStatics } from "../_util/forward-ref-with-statics"; import { mergeRefs } from "../util"; import { defaultAllValue, getRelations, separator, } from "./CascadeMenu/CascaderMultiMenuLists"; import { getAllIds, getRelationMap, useValueMode, } from "../checktree/CheckTree"; const defaultValueRender = (options: CascaderOption[]) => { const leaf = options[options.length - 1]; return leaf?.label || leaf?.value || "-"; }; function isEqual(a: string[], b: string[]) { return a.length === b.length && a.every((str, i) => str === b[i]); } export const CascaderMultiple = forwardRefWithStatics( function CascaderMultiple( props: CascaderMultipleProps & { data: CascaderData }, ref: React.Ref ) { const t = useTranslation(); const { classPrefix } = useConfig(); const { data, value: rawValue, onChange: rawOnChange, onLoad, disabled, style, className, placeholder = t.pleaseSelect, defaultOpen = false, open, onOpenChange = noop, placement = "bottom-start", placementOffset = 5, closeOnScroll = true, escapeWithReference, popupContainer, overlayClassName, overlayStyle, clearable, searchable, filter, type, expandTrigger, all, valueRender = defaultValueRender, valueMode = "onlyLeaf", showMode = "onlyLeaf", tips, bottomTips, onScrollBottom = noop, onSearch = noop, } = useDefaultValue(props, []); const [hover, setHover] = useState(false); const scheduleUpdateRef = useRef<() => void>(null); useEffect(() => { if (scheduleUpdateRef.current) { scheduleUpdateRef.current(); } }, [rawValue]); const [isOpened, setIsOpened] = useDefault(open, defaultOpen, onOpenChange); const searchInputRef = useRef(null); const [searchValue, setSearchValue] = useState(""); const [fullSearchValue, setFullSearchValue] = useState(""); const [wrapperWidth, setWrapperWidth] = useState(0); const Box = type === "menu" ? CascaderMenuBox : CascaderBox; /** * ----------- * 值类型转换 * ----------- */ const { parentMap, childrenMap } = useMemo(() => { const { relations } = getRelations(data, type === "menu" ? all : false); return getRelationMap(relations); }, [all, data, type]); // 拼接多级值 const [joinedValue, joinedOnChange] = useMemo(() => { const joinedValue = rawValue.map(list => list.join(separator)); const joinedOnChange = (value: string[], context) => { rawOnChange( value.map(i => i.split(separator)), { event: context?.event, options: value.map(i => getOptions(data, i.split(separator))), } ); }; return [joinedValue, joinedOnChange]; }, [data, rawOnChange, rawValue]); // 只包含叶节点模式的拼接后的 value/onChange const [onlyLeafJoinedValue, onlyLeafJoinedOnChange] = useValueMode( joinedValue, joinedOnChange, parentMap, childrenMap, valueMode ); // 只包含叶节点模式的 value/onChange const [onlyLeafValue, onlyLeafOnChange] = useMemo(() => { const onlyLeafValue = onlyLeafJoinedValue.map(i => i.split(separator)); const onlyLeafOnChange = (value: string[][], ...args) => { onlyLeafJoinedOnChange( value.map(i => i.join(separator)), ...args ); }; return [onlyLeafValue, onlyLeafOnChange]; }, [onlyLeafJoinedOnChange, onlyLeafJoinedValue]); const isSelected = useCallback( (value: string[]) => !!onlyLeafValue.find(v => isEqual(v, value)), [onlyLeafValue] ); const onSelect: CascaderBoxProps["onSelect"] = (value, context) => { const next = [...onlyLeafValue, value]; onlyLeafOnChange(next, { ...context, options: next.map(v => getOptions(data, v)), }); }; const onDeselect: CascaderBoxProps["onDeselect"] = (value, context) => { const next = onlyLeafValue.filter(v => !isEqual(v, value)); onlyLeafOnChange(next, { ...context, options: next.map(v => getOptions(data, v)), }); }; // showMode 转换渲染节点 const tags = useMemo(() => { // 全选父节点 + 叶节点 const allValue = getAllIds(onlyLeafJoinedValue, parentMap, childrenMap); if (showMode === "parentFirst") { const allSet = new Set(allValue); const invalidSet = new Set(); allValue.forEach(id => { // 父节点此刻被选中则该节点无效 if (parentMap.has(id) && allSet.has(parentMap.get(id))) { invalidSet.add(id); } }); // 转换后的 valueList const showValueList = allValue .filter(id => !invalidSet.has(id)) .map(i => i.split(separator)); const optionsList = showValueList .map(v => getOptions(data, v)) .filter(Boolean); return optionsList.map(options => { const key = options.map(i => i.value).join(separator); // all 配置比较特殊,无法在 getOptions 中找到 if (key === ((all as any)?.value || defaultAllValue)) { options[0].label = (all as any)?.label || t.selectAllText; } return ( { event.stopPropagation(); if (disabled) { return; } const next = onlyLeafValue.filter(value => { let curKey = value.join(separator); let result = true; while (curKey) { if (curKey === key) { result = false; break; } curKey = parentMap.get(curKey); } return result; }); onlyLeafOnChange(next, { event, options: next.map(v => getOptions(data, v)), }); }} > {valueRender(options)} ); }); } // if (showMode === "onlyLeaf") // 转换后的 valueList const showValueList = allValue .filter(i => !childrenMap.has(i)) .map(i => i.split(separator)); const optionsList = showValueList .map(v => getOptions(data, v)) .filter(Boolean); return optionsList.map(options => { const key = options.map(i => i.value).join(separator); return ( { event.stopPropagation(); if (disabled) { return; } const next = onlyLeafValue.filter( value => value.join(separator) !== key ); onlyLeafOnChange(next, { event, options: next.map(v => getOptions(data, v)), }); }} > {valueRender(options)} ); }); }, [ onlyLeafJoinedValue, parentMap, childrenMap, showMode, data, all, valueRender, t.selectAllText, disabled, onlyLeafValue, onlyLeafOnChange, ]); return ( { setSearchValue(""); setFullSearchValue(""); }} placement={placement || "bottom-start"} placementOffset={placementOffset} closeOnScroll={closeOnScroll} escapeWithReference={escapeWithReference} popupContainer={popupContainer} overlayClassName={overlayClassName} overlayStyle={overlayStyle} overlay={({ scheduleUpdate }) => { scheduleUpdateRef.current = scheduleUpdate; if (searchValue) { return ( { if (selected) { onSelect(value, context); } else { onDeselect(value, context); } }} tips={tips} bottomTips={bottomTips} onScrollBottom={onScrollBottom} onSearch={onSearch} /> ); } return ( setIsOpened(false)} changeOnSelect={false} scheduleUpdate={scheduleUpdate} isSelected={isSelected} onDeselect={onDeselect} expandTrigger={expandTrigger} tips={tips} bottomTips={bottomTips} onScrollBottom={onScrollBottom} /> ); }} >
ref && setWrapperWidth(ref.clientWidth))} {...mergeEventProps(props, { onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), })} >
{ if (disabled) { return; } if (searchable && searchInputRef.current) { event.stopPropagation(); searchInputRef.current.focus(); setIsOpened(true); } }} >
{tags} {searchable && ( { setSearchValue(value); }} onInput={event => { setFullSearchValue(event.currentTarget.value); }} /> )}
{!fullSearchValue && ( )}
{clearable && !disabled && hover && onlyLeafValue.length ? ( { event.stopPropagation(); onlyLeafOnChange([], { event, options: [] }); }} /> ) : ( )}
); }, { defaultLabelAlign: "middle", } ); CascaderMultiple.displayName = "CascaderMultiple";