import React, { useState, useRef, useEffect, Fragment } from "react"; import { Dropdown } from "../dropdown"; import { Checkbox } from "../checkbox"; import { useDefaultValue, ChangeContext } from "../form"; import { SelectOptionWithGroup } from "./SelectOption"; import { CheckTree } from "../checktree"; import { Button } from "../button"; import { Text } from "../text"; import { useTranslation } from "../i18n"; import { useConfig } from "../_util/config-context"; import { EmptyTip } from "../tips"; import { SearchBox } from "../searchbox"; import { SelectMultipleProps } from "./SelectProps"; import { injectValue } from "../_util/inject-value"; import { Tooltip } from "../tooltip"; import { useDefault } from "../_util/use-default"; import { noop } from "../_util/noop"; import { searchFilter } from "../_util/search-filter"; import { forwardRefWithStatics } from "../_util/forward-ref-with-statics"; import { isMobile } from "../_util/is-mobile"; import { useUnmounted } from "../_util/use-unmounted"; import { getListItems } from "./util"; import { VirtualizedList } from "../list/VirtualizedList"; export const SelectMultiple = forwardRefWithStatics( function SelectMultiple( props: SelectMultipleProps, ref: React.Ref ) { const t = useTranslation(); const { classPrefix } = useConfig(); const { staging = true, value, onChange, options = [], groups, allOption, shouldOptionExcludeFromAll = "disabled", allowEmpty = true, onOpen = noop, onClose = noop, searchable, searchPlaceholder = "", onSearch = noop, filter: customizeFilter, filterRender, autoClearSearchValue = true, defaultSearchValue = "", searchValue, onSearchValueChange, clearable, onScrollBottom = noop, bottomTips, footer, virtual = false, listWidth, listHeight, boxStyle = {}, onReset, ...dropdownProps } = useDefaultValue(props, []); // 移动端 focus 可能产生滚动导致关闭 if (isMobile && searchable) { dropdownProps.closeOnScroll = false; } const vListRef = useRef(null); const [stagingValue, setStagingValue] = useState(value); const InputRef = useRef(null); const [inputValue, setInputValue] = useDefault( searchValue, defaultSearchValue, onSearchValueChange ); const unmountedRef = useUnmounted(); const filter = customizeFilter || ((inputValue: string, { text, value }: SelectOptionWithGroup) => { const optionText = String(typeof text === "string" ? text : value); return !searchable || searchFilter(optionText, inputValue); }); const hasGroup = !!options.find(opt => !!opt.groupKey); function focus() { if (!isMobile && searchable) { setTimeout(() => { if (InputRef.current) { InputRef.current.focus(); } }, 100); // 第一次展开时 Input 还未渲染 } } const handleChange = (value: string[], context: ChangeContext) => { if (staging === false) { onChange(value, context); return; } setStagingValue(value); }; // 「全部」选项和其余选项是数关系 const relations = {}; if (allOption) { for (const option of options) { if ( // 用户可以指定哪些选项从全选逻辑中排除 (typeof shouldOptionExcludeFromAll === "function" && shouldOptionExcludeFromAll(option)) || (shouldOptionExcludeFromAll === "disabled" && option.disabled) ) { // continue } else { relations[option.value] = allOption.value; } } } // 筛选 const filteredOptions = options.filter(options => filter(inputValue, options) ); let { tips } = props; if (!tips && filteredOptions.length === 0) { tips = ; } const items = getListItems({ tips: tips ? injectValue(tips)(filteredOptions) : null, bottomTips: injectValue(bottomTips)(filteredOptions), groups, options: filteredOptions, allOption, }).map(item => { if (item.type === "option" && searchable && filterRender) { return { ...item, text: filterRender(inputValue, item.option) }; } return item; }); if (listWidth) { boxStyle.width = listWidth; boxStyle.minWidth = listWidth; } // 重置数据 const resetData = context => { if (inputValue !== "") { setInputValue("", context); } let defaultData = []; if (onReset && typeof onReset === "function") { const originResetData = onReset(context); // 非受控模式,在reset的时候 可以处置数据 if (originResetData) { defaultData = Array.isArray(originResetData) ? originResetData : defaultData; } else { // 受控场景无需走内置场景 return; } } handleChange(defaultData, context); }; return ( } onOpen={() => { setStagingValue(value); focus(); onOpen(); }} onClose={() => { if (autoClearSearchValue && inputValue !== "") { setTimeout(() => { if (!unmountedRef.current) { setInputValue("", {} as any); } }, 100); } onClose(); }} clickClose={false} clearable={value.length && clearable} boxStyle={boxStyle} onClear={event => { onChange([], { event }); setStagingValue([]); }} > {close => ( x.disabled).map(x => x.value)} > {searchable && ( { setInputValue(value, context); onSearch(value, context); }} onClear={focus} placeholder={searchPlaceholder} onSearch={onSearch} /> )} { if (item.type === "option") { const { option } = item; const content = option.text || option.value; return { ...item, text: ( evt.stopPropagation()} name={option.value} title={ !option.tooltip && typeof content === "string" ? content : undefined } > {content} ), props: { disabled: option.disabled, tooltip: option.tooltip, }, }; } return item; })} virtualizedRef={vListRef} type={hasGroup ? "option-group" : "option"} onScrollBottom={onScrollBottom} listHeight={listHeight} /> {(footer || staging !== false) && ( {footer} {staging !== false && ( <> )} )} )} ); }, { defaultLabelAlign: "middle", } ); SelectMultiple.displayName = "SelectMultiple"; function ValueBrief(props: SelectMultipleProps): JSX.Element { const t = useTranslation(); const { value, allOption, options, button, placeholder = t.pleaseSelect, appearance = props.appearence, // eslint-disable-line react/destructuring-assignment } = props; let buttonPlaceholder: React.ReactNode = placeholder; if (!appearance || appearance === "button" || appearance === "default") { buttonPlaceholder = {placeholder}; } const [selectedOptions, setSelectedOptions] = useState< SelectOptionWithGroup[] >((value || []).map(x => options.find(y => y.value === x) || { value: x })); useEffect(() => { setSelectedOptions(selectedOptions => { return (value || []) .map( v => options.find(option => option.value === v) || selectedOptions.find(option => option.value === v) ) .filter(Boolean); }); }, [options, value]); if (button) { return
{injectValue(button)(selectedOptions)}
; } if (!value || !value.length) { return <>{buttonPlaceholder}; } if ( allOption && value.length === options.length && value.filter(v => options.find(o => o.value === v)).length === options.length ) { return {allOption.text || allOption.value}; } const exceed = selectedOptions.length > 5; return ( ( {o.text || o.value} {index < selectedOptions.length - 1 && ", "} ))} > {selectedOptions.slice(0, 5).map((o, index) => ( {o.text || o.value} {index < selectedOptions.length - 1 && ", "} ))} {exceed && ...} ); } ValueBrief.displayName = "SelectValueBrief";