import React, { ChangeEvent, Component, createRef, MouseEvent, ReactNode, KeyboardEvent, } from 'react' import classNames from 'classnames' import _ from 'lodash' import { MoreSelectBaseProps, MoreSelectGroupDataItem, MoreSelectDataItem } from './types' import { Popover } from '../popover' import { Flex } from '../flex' import SVGRemove from '../../svg/remove.svg' import SVGCloseCircle from '../../svg/close-circle.svg' import { renderListFilterDefault, renderListFilterPinYin } from './render_list_filter' import { Input } from '../input' import { Loading } from '../loading' import { getLocale } from '@gm-pc/locales' import { ListBase } from '../list' import { findDOMNode } from 'react-dom' import { ConfigConsumer, ConfigProvider, ConfigProviderProps } from '../config_provider' interface MoreSelectBaseState { searchValue: string loading: boolean /* keyboard 默认第一个位置 */ willActiveIndex: number | null } // @todo keydown item disabled // 目前全键盘还没有这种场景,暂时不管 class MoreSelectBase extends Component< MoreSelectBaseProps, MoreSelectBaseState > { static renderListFilterDefault = renderListFilterDefault static renderListFilterPinYin = renderListFilterPinYin readonly state: MoreSelectBaseState = { searchValue: '', loading: false, willActiveIndex: this.props.isKeyboard ? 0 : null, } private _isUnmounted = false private _baseRef = createRef() private _selectionRef = createRef() private _popoverRef = createRef() private _inputRef = createRef() private _filterData: MoreSelectGroupDataItem[] | undefined constructor(props: MoreSelectBaseProps) { super(props) if (props.selected?.length) { this._getFilterData() const flatList = this._getFlatFilterData() this.state.willActiveIndex = flatList.findIndex( (v) => v.value === props.selected[0].value ) } } componentWillUnmount() { this._isUnmounted = false } public apiDoFocus = (): void => { // 唤起 popover,input autoFocus 会自动聚焦,但是这种方式本质是显示 UI // this.popoverRef.current.apiDoSetActive(true) // focus更符合直觉 // eslint-disable-next-line react/no-find-dom-node ;(findDOMNode(this._selectionRef.current) as HTMLDivElement).focus() } public apiDoSelectWillActive = (): void => { const { selected = [], onSelect = _.noop, multiple } = this.props const { willActiveIndex } = this.state const flatList = this._getFlatFilterData() // 没有做过键盘操作啥也不做 if (!_.isNil(willActiveIndex) && willActiveIndex < flatList.length) { if (multiple) { onSelect(_.uniqBy([...selected, flatList[willActiveIndex]], (item) => item.value)) } else { onSelect([flatList[willActiveIndex]]) } } } private _getFlatFilterData = (): MoreSelectDataItem[] => { return _.flatMap(this._filterData, (v) => v.children) } private _handleSelect = (values: V[]): void => { const { onSelect = _.noop, data = [], multiple, selected = [] } = this.props const items: MoreSelectDataItem[] = [] data.forEach((group) => { group.children.forEach((child) => { if (values.includes(child.value)) { items.push(child) } }) }) selected.forEach((item) => { let flag = true // 判断当前已选择的选项中是否存在不在当前data里面的,解决onSearch异步,true则表示都不在data里面 data.forEach((group) => { flag = flag && group.children.every((v) => v.value !== item.value) }) if (flag) { items.push(item) } }) onSelect(items) if (!multiple) { // 单选后关闭 // 要异步 window.setTimeout(() => { if (!this._isUnmounted && this._popoverRef.current) { this._popoverRef.current.apiDoSetActive(false) } }, 0) } } public _handleChange = ( event: ChangeEvent, isInitSearch?: boolean ): void => { const searchValue = event.target.value this.setState({ searchValue }) this._debounceDoSearch(searchValue) setTimeout(() => { // eslint-disable-next-line no-unused-expressions isInitSearch && this._inputRef.current?.select() if (this.props.searchOnActive) { localStorage.setItem('_GM-PC_MORESELECT_SEARCHVALUE', this.state.searchValue) } }, 100) } private _doSearch = (query: string): void => { const { onSearch, data = [] } = this.props if (!this._isUnmounted && onSearch) { const result = onSearch(query, data) if (!result) { return } this.setState({ loading: true }) Promise.resolve(result).finally(() => { this.setState({ loading: false }) }) } } private _debounceDoSearch = _.debounce(this._doSearch, this.props.delay) private _handleClear = (clearItem: MoreSelectDataItem, event: MouseEvent): void => { event.stopPropagation() const { onSelect = _.noop, selected = [] } = this.props const willSelected = selected.filter((item) => item.value !== clearItem.value) onSelect(willSelected) } private _handlePopupKeyDown = (event: KeyboardEvent): void => { const { onKeyDown } = this.props let willActiveIndex = this.state.willActiveIndex as number if (!onKeyDown) { // 没有事件的不用拦截 return } // 不是上下方向键不用拦截 if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { onKeyDown(event) return } const flatList = this._getFlatFilterData() // 没有过滤数据,不用拦截 if (!flatList.length) { onKeyDown(event) return } if (event.key === 'ArrowUp') { willActiveIndex-- } else if (event.key === 'ArrowDown') { willActiveIndex++ } // 修正 if (willActiveIndex < 0) { willActiveIndex = flatList.length - 1 } else if (willActiveIndex > flatList.length - 1) { willActiveIndex = 0 } this.setState({ willActiveIndex }) } private _getFilterData = () => { const { data = [], renderListFilter, renderListFilterType } = this.props const { searchValue } = this.state let filterData: MoreSelectGroupDataItem[] if (renderListFilter) { filterData = renderListFilter(data, searchValue) } else if (renderListFilterType === 'pinyin') { filterData = renderListFilterPinYin(data, searchValue) } else { filterData = renderListFilterDefault(data, searchValue) } this._filterData = filterData return filterData } private _renderEmpty = (): ReactNode => { const { renderEmpty } = this.props if (typeof renderEmpty === 'function') { const result = renderEmpty(this.state.searchValue) if (result !== undefined) { return result } } return ( {getLocale('没有数据')} ) } private _renderList = (config: ConfigProviderProps): ReactNode => { const { selected = [], multiple, isGroupList, renderListItem, searchPlaceholder, listHeight, popupClassName, renderCustomizedBottom, } = this.props const { loading, searchValue, willActiveIndex } = this.state const filterData = this._getFilterData() return (
{loading && ( )} {!loading && !filterData.length && this._renderEmpty()} {!loading && !!filterData.length && ( v.value)} data={filterData} multiple={multiple} isGroupList={isGroupList} className='gm-border-0' renderItem={renderListItem} onSelect={this._handleSelect} isScrollTo willActiveIndex={willActiveIndex!} style={{ height: listHeight }} /> )}
{!loading && !!filterData.length && renderCustomizedBottom && renderCustomizedBottom(this._popoverRef)}
) } private _handleMoreSelectClick = () => { const { onClick, selected } = this.props if (typeof onClick === 'function') { return onClick(selected) } } private _handlePopoverVisibleChange = (active: boolean) => { if (active && this.props.searchOnActive) { const searchValue = localStorage.getItem('_GM-PC_MORESELECT_SEARCHVALUE') if (searchValue) { this.setState({ searchValue }) setTimeout(() => { // eslint-disable-next-line no-unused-expressions this._inputRef.current?.select() this._debounceDoSearch(searchValue) }, 0) } } } render() { const { isInPopup, disabled, disabledClose, selected = [], multiple, placeholder, renderSelected, className, style, popoverType, children, } = this.props return ( {(config) => (
this._renderList(config)} disabled={disabled} isInPopup={isInPopup} onVisibleChange={this._handlePopoverVisibleChange} > {children ?? ( {selected.length !== 0 ? ( selected.map((item) => ( {item.text}
} > {renderSelected!(item)}
{multiple ? ( ) : ( !disabledClose && ( // 是否不限时清除按钮,仅单选可用 ) )} )) ) : ( // 加多个   避免对齐问题,有文本才有对齐
{placeholder} 
)} )} )} ) } } export default MoreSelectBase