import React, { useState, isValidElement, useRef, useMemo } from 'react'; import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ImageStyle, StyleProp, } from 'react-native'; import { isString, isUndefined, omit } from 'lodash'; import { FloatingLayer, ShouldRender, Button, Input, InputInstance, Loading, } from '@xrnjs/ui'; import Checkbox from './Checkbox'; import createStyles from './styles/Select.style'; import { SelectProps, Option, Group } from './interface'; import { Type } from './enum'; import { IconXSearchfor1, IconXRightsmall1, } from '../../icons'; import { useLocale } from '../Locale/locale'; import { FloatingLayerRef } from '../FloatingLayer/interface'; import { useMaxHeight } from '../../utils'; import ErrorBlock from '../ErrorBlock/errorBlock'; import { useTheme } from '../Theme'; import { renderHighLightTextLikeJSX } from '../../core/helpers'; import Image from '../Image'; import { ImageProps } from '../Image/interface'; const isValidString = (v: React.ReactNode) => isString(v) && v !== ''; const SHOW_TIP_LIMIT = 10; const Select = ({ type, visible, options, defaultValue = '', title, showFooter = true, showConfirmButton = false, value, currencySelectProps, showSearch = false, searchInputProps = {}, cancelable = true, customFooter, contentStyle, itemFullContainer = false, showNoMoreTip = true, dataEmptyText, searchEmptyText, onPressOverlay, onConfirm, onCancel, onChange, onSearch, filterOption, onRepeatClick, showTipLimit = SHOW_TIP_LIMIT, loading, testID = '', onDisabledPress, extra, containerHeight, searchEmptyNode, dataEmptyNode, ...resetProps }: SelectProps) => { const locale = useLocale(); const [selectedValues, setSelectedValues] = useState(value ?? defaultValue); const [searchValue, setSearchValue] = useState(''); const searchRef = useRef(null); const floatingLayerRef = useRef(null); const maxHeight = useMaxHeight(); const token = useTheme(); const styles = createStyles(token); const hasFooter = (type === Type.checkbox && showFooter) || !!customFooter; // 有搜索框时,loading默认高度为480,否则为200 const minHeight = showSearch ? 480 : 200; const onChangeFn = (values: string | string[]) => { if (type === Type.radio && !cancelable && isUndefined(values)) { onRepeatClick?.(); } else { setSelectedValues(values); onChange?.(values); } }; const renderBottomTip = () => { // 统计真实的选项数量:如果为分组,累加所有 children;否则直接用列表长度 const isGrouped = Array.isArray(_options) && _options.length > 0 && (_options[0] as Group)?.children instanceof Array; const totalCount = isGrouped ? (_options as Group[]).reduce( (sum, group) => sum + (group?.children?.length || 0), 0 ) : _options?.length || 0; if (showNoMoreTip && totalCount >= showTipLimit) { return ( {locale.Select.noDataText} ); } return null; }; const onConfirmFn = () => { onConfirm?.(selectedValues, searchValue); }; const addLabelPosition = (currentOptions: Option[]) => { return currentOptions?.map?.((option) => { const { labelTextStyle, subLabelTextStyle, contentLabelTextStyle } = option; return { ...option, labelPosition: option.labelPosition || 'left', testID: `${testID}${option.value}`, labelTextStyle: StyleSheet.flatten([styles.label, labelTextStyle]), subLabelTextStyle: StyleSheet.flatten([ styles.subLabel, subLabelTextStyle, ]), contentLabelTextStyle: StyleSheet.flatten([ styles.contenLabel, contentLabelTextStyle, ]), }; }); }; const renderOptions = (currentOptions: Option[]) => { return ( gap={0} style={[styles.checkboxGroupStyle, contentStyle]} checkboxComponentStyle={ itemFullContainer ? styles.itemFullContainer : null } multiple={type === Type.checkbox} defaultValue={defaultValue} value={value} options={addLabelPosition(currentOptions)} onChange={onChangeFn} highlight={searchValue} onDisabledPress={(option) => onDisabledPress?.(option)} /> ); }; const renderRedirectOptions = (currentOptions: Option[]) => currentOptions?.map?.((option) => renderRedirectItem(option)); const renderGroups = (groups: Group[]) => { if (type === Type.redirect) { return ( {groups.map((group, i) => ( {group.label} {renderRedirectOptions(group.children)} ))} {renderBottomTip()} ); } return ( {groups.map((group, i) => ( {group.label} {renderOptions(group.children)} ))} {renderBottomTip()} ); }; // 判断是否是分组数据, 只有分组有children const isGroup = (group: Group[]) => { const firstOpt = group?.[0] as Group; return firstOpt?.children instanceof Array; }; const renderContent = () => { if (loading) { return ( ); } if (options.length === 0) { return ( {isValidElement(dataEmptyNode) ? ( dataEmptyNode ) : ( )} ); } if (_options.length === 0) { return ( {isValidElement(searchEmptyNode) ? ( searchEmptyNode ) : ( )} ); } if (isGroup(_options as Group[])) { // 分组? return renderGroups(_options as Group[]); } return type === Type.redirect ? ( {renderRedirectOptions(_options as Option[])} {renderBottomTip()} ) : ( {renderOptions(_options as Option[])} {renderBottomTip()} ); }; const renderRedirectItem = (option: Option) => { return ( { if (option?.disabled) { onDisabledPress?.(option); return; } onChangeFn(option.value); }} style={styles.itemContainer} > , option.disabled && styles.disabled_img, ])} /> {option.prefixIcon} {renderHighLightTextLikeJSX( option.label, StyleSheet.flatten([ styles.label, option.disabled && styles.labelDisabled, ]), searchValue )} {renderHighLightTextLikeJSX( option.subLabel, StyleSheet.flatten([ styles.subLabel, option.disabled && styles.subLabelDisabled, ]), searchValue )} {renderHighLightTextLikeJSX( option.contentLabel, StyleSheet.flatten([ styles.contenLabel, option.disabled && styles.contentLabelDisabled, ]), searchValue )} ); }; /** * 数据内容是否包含某字符串信息 element元素默认返回true(与之前逻辑保持一致) */ const isContainStr = ({ data, searchStr, }: { data: React.ReactNode; searchStr: string; }) => { if (!data) return false; // 无数据默认不匹配 // 非有效元素 if (!isValidElement(data)) { return String(data) .toLocaleLowerCase() .includes(searchStr.toLocaleLowerCase()); } return true; // 元素默认包含 }; /** * 根据搜索内容筛选选择项 */ const doFilterOptions = ({ optionList, searchStr, }: { optionList: Option[]; searchStr: string; }) => { return optionList.filter((option: Option) => { if (filterOption) { return filterOption(searchStr, option); } return ( isContainStr({ data: option?.label, searchStr }) || isContainStr({ data: option?.subLabel, searchStr }) || isContainStr({ data: option?.contentLabel, searchStr }) ); }); }; /** * 根据搜索内容筛选选择项(分组形式) */ const doFilterGroupOptions = ({ groupList, searchStr, }: { groupList: Group[]; searchStr: string; }) => { const filterGroup: Group[] = []; groupList.forEach((group) => { const optionList = doFilterOptions({ optionList: group?.children, searchStr, }); if (optionList.length > 0) { filterGroup.push({ label: group.label, children: optionList, }); } }); return filterGroup; }; const filterOptions = (searchStr: string) => { if (!searchStr || options?.length < 1) { // 优化,若未输入内容 或 无列表内容,返回全部内容即可 return [...options]; } if (isGroup(options as Group[])) { return doFilterGroupOptions({ groupList: options as Group[], searchStr: searchValue, }); } return doFilterOptions({ optionList: options as Option[], searchStr: searchValue, }); }; const _options = useMemo( () => filterOptions(searchValue) as Option[] | Group[], [options, searchValue, filterOption] ); const onChangeSearch = (data: string) => { if (isUndefined(data)) { data = ''; } setSearchValue(data); visible && onSearch?.(data); }; const onClosed = () => { setSearchValue(''); searchRef.current?.clear(); searchRef.current?.blur(); resetProps?.onClosed?.(); }; const renderHeader = () => { return ( } ref={searchRef} {...searchInputProps} /> ); }; // select 有 search 时,默认为最大值 const floatingLayerHeight = showSearch && !containerHeight ? maxHeight : containerHeight; return ( {customFooter} ) : null } headerExtra={renderHeader()} {...omit(resetProps, 'onClosed')} > {renderContent()} {extra} ); }; export default Select;