import React, { PureComponent, ReactNode } from 'react'; import cls from 'classnames'; import PropTypes from 'prop-types'; import { cssClasses, strings } from '@douyinfe/semi-foundation/cascader/constants'; import isEnterPress from '@douyinfe/semi-foundation/utils/isEnterPress'; import ConfigContext, { ContextValue } from '../configProvider/context'; import LocaleConsumer from '../locale/localeConsumer'; import { IconChevronRight, IconTick } from '@douyinfe/semi-icons'; import { Locale } from '../locale/interface'; import Spin from '../spin'; import Checkbox, { CheckboxEvent } from '../checkbox'; import { BasicCascaderData, BasicEntity, ShowNextType, BasicData, Virtualize } from '@douyinfe/semi-foundation/cascader/foundation'; import { FixedSizeList as List } from 'react-window'; import VirtualRow from './virtualRow'; export interface CascaderData extends BasicCascaderData { label: React.ReactNode } export interface Entity extends BasicEntity { /* children list */ children?: Array; /* treedata */ data: CascaderData; /* parent data */ parent?: Entity } export interface Entities { [idx: string]: Entity } export interface Data extends BasicData { data: CascaderData; searchText: React.ReactNode[] } export interface FilterRenderProps { className: string; inputValue: string; disabled: boolean; data: CascaderData[]; checkStatus: { checked: boolean; halfChecked: boolean }; selected: boolean; onClick: (e: React.MouseEvent) => void; onCheck: (e: React.MouseEvent) => void } export interface CascaderItemProps { activeKeys: Set; selectedKeys: Set; loadedKeys: Set; loadingKeys: Set; onItemClick: (e: React.MouseEvent | React.KeyboardEvent, item: Entity | Data) => void; onItemHover: (e: React.MouseEvent, item: Entity) => void; showNext: ShowNextType; onItemCheckboxClick: (item: Entity | Data) => void; onListScroll: (e: React.UIEvent, ind: number) => void; searchable: boolean; keyword: string; empty: boolean; emptyContent: React.ReactNode; loadData: (selectOptions: CascaderData[]) => Promise; data: Array; separator: string; multiple: boolean; checkedKeys: Set; halfCheckedKeys: Set; filterRender?: (props: FilterRenderProps) => ReactNode; virtualize?: Virtualize; expandIcon?: ReactNode; } const prefixcls = cssClasses.PREFIX_OPTION; export default class Item extends PureComponent { static contextType = ConfigContext; static propTypes = { data: PropTypes.array, emptyContent: PropTypes.node, searchable: PropTypes.bool, onItemClick: PropTypes.func, onItemHover: PropTypes.func, multiple: PropTypes.bool, showNext: PropTypes.oneOf([strings.SHOW_NEXT_BY_CLICK, strings.SHOW_NEXT_BY_HOVER]), checkedKeys: PropTypes.object, halfCheckedKeys: PropTypes.object, onItemCheckboxClick: PropTypes.func, separator: PropTypes.string, keyword: PropTypes.string, virtualize: PropTypes.object, expandIcon: PropTypes.node, }; static defaultProps = { empty: false, }; context: ContextValue; onClick = (e: React.MouseEvent | React.KeyboardEvent, item: Entity | Data) => { const { onItemClick } = this.props; if (item.data.disabled || ('disabled' in item && item.disabled)) { return; } onItemClick(e, item); }; /** * A11y: simulate item click */ handleItemEnterPress = (keyboardEvent: React.KeyboardEvent, item: Entity | Data) => { if (isEnterPress(keyboardEvent)) { this.onClick(keyboardEvent, item); } } onHover = (e: React.MouseEvent, item: Entity) => { const { showNext, onItemHover } = this.props; if (item.data.disabled) { return; } if (showNext === strings.SHOW_NEXT_BY_HOVER) { onItemHover(e, item); } }; onCheckboxChange = (e: CheckboxEvent, item: Entity | Data) => { const { onItemCheckboxClick } = this.props; // Prevent Checkbox's click event bubbling to trigger the li click event e.stopPropagation(); if (e.nativeEvent && typeof e.nativeEvent.stopImmediatePropagation === 'function') { e.nativeEvent.stopImmediatePropagation(); } onItemCheckboxClick(item); }; getItemStatus = (key: string) => { const { activeKeys, selectedKeys, loadedKeys, loadingKeys } = this.props; const state = { active: false, selected: false, loading: false }; if (activeKeys.has(key)) { state.active = true; } if (selectedKeys.has(key)) { state.selected = true; } if (loadingKeys.has(key) && !loadedKeys.has(key)) { state.loading = true; } return state; }; renderIcon = (type: string, haveMarginLeft = false) => { const finalCls = (style: string) => { return style + (haveMarginLeft ? ` ${prefixcls}-icon-left` : ''); }; switch (type) { case 'child': const { expandIcon } = this.props; if (expandIcon) { return expandIcon; } return (); case 'tick': return (); case 'loading': return ; case 'empty': return (); default: return null; } }; highlight = (searchText: React.ReactNode[]) => { const content: React.ReactNode[] = []; const { keyword, separator } = this.props; searchText.forEach((item, idx) => { if (typeof item === 'string' && keyword) { // Keep historical DOM structure (span) and only make match case-insensitive. const lowerItem = item.toLowerCase(); const lowerKeyword = keyword.toLowerCase(); let searchFrom = 0; let keyIndex = 0; while (true) { const matchIndex = lowerItem.indexOf(lowerKeyword, searchFrom); if (matchIndex === -1) { const rest = item.slice(searchFrom); if (rest) { content.push(rest); } break; } const before = item.slice(searchFrom, matchIndex); if (before) { content.push(before); } content.push( {item.slice(matchIndex, matchIndex + keyword.length)} ); searchFrom = matchIndex + keyword.length; keyIndex++; } } else { content.push(item); } if (idx !== searchText.length - 1) { content.push(separator); } }); return content; }; renderFlattenOptionItem = (data: Data, index?: number, style?: any) => { const { multiple, selectedKeys, checkedKeys, halfCheckedKeys, keyword, filterRender, virtualize } = this.props; const { searchText, key, disabled, pathData } = data; const selected = selectedKeys.has(key); const className = cls(prefixcls, { [`${prefixcls}-flatten`]: true && !filterRender, [`${prefixcls}-disabled`]: disabled, [`${prefixcls}-select`]: selected && !multiple, }); const onClick = e => { this.onClick(e, data); }; const onKeyPress = e => this.handleItemEnterPress(e, data); const onCheck = (e: CheckboxEvent) => this.onCheckboxChange(e, data); if (filterRender) { const props = { className, inputValue: keyword, disabled, data: pathData, checkStatus: { checked: checkedKeys.has(data.key), halfChecked: halfCheckedKeys.has(data.key), }, selected, onClick, onCheck }; const item = filterRender(props) as any; const otherProps = virtualize ? { key, style: { ...(item.props.style ?? {}), ...style }, } : { key }; return React.cloneElement(item, otherProps ); } return (
  • {!multiple && this.renderIcon('empty')} {multiple && ( )} {this.highlight(searchText)}
  • ); } renderFlattenOption = (data: Data[]) => { const { virtualize } = this.props; const content = (
      {virtualize ? this.renderVirtualizeList(data) : data.map(item => this.renderFlattenOptionItem(item))}
    ); return content; }; renderVirtualizeList = (visibleOptions: any) => { const { direction } = this.context; const { virtualize } = this.props; return ( {VirtualRow} ); } renderItem(renderData: Array, content: Array = []) { const { multiple, checkedKeys, halfCheckedKeys } = this.props; let showChildItem: Entity; const ind = content.length; content.push(
      this.props.onListScroll(e, ind)}> {renderData.map(item => { const { data, key, parentKey } = item; const { children, label, disabled, isLeaf } = data; const { active, selected, loading } = this.getItemStatus(key); const hasChild = Boolean(children) && children.length; const showExpand = hasChild || (this.props.loadData && !isLeaf); if (active && hasChild) { showChildItem = item; } const className = cls(prefixcls, { [`${prefixcls}-active`]: active && !selected, [`${prefixcls}-select`]: selected && !multiple, [`${prefixcls}-disabled`]: disabled }); const otherAriaProps = parentKey ? { ['aria-owns']: `cascaderItem-${parentKey}` } : {}; return ( ); })}
    ); if (showChildItem) { content.concat(this.renderItem(showChildItem.children, content)); } return content; } renderEmpty() { const { emptyContent } = this.props; if (emptyContent === null) { return null; } return ( {(locale: Locale['Cascader']) => (
      {emptyContent || locale.emptyText}
    )}
    ); } render() { const { data, searchable } = this.props; const { direction } = this.context; const isEmpty = !data || !data.length; let content; const listsCls = cls({ [`${prefixcls}-lists`]: true, [`${prefixcls}-lists-rtl`]: direction === 'rtl', [`${prefixcls}-lists-empty`]: isEmpty, }); if (isEmpty) { content = this.renderEmpty(); } else { content = searchable ? this.renderFlattenOption(data as Data[]) : this.renderItem(data as Entity[]); } return (
    {content}
    ); } }