import React, { Component } from 'react'; import { is, List, Map } from 'immutable'; import * as Tools from 'jad-tool'; import { Select, Empty, Checkbox, Icon, AutoComplete, Input } from 'antd'; import classNames from 'classnames'; import { throttle } from 'jad-tool'; import { initOptions, onCheckLow2High, onCheckHigh2Low, transform2FlatData, transform2TreeData } from './utils'; const { Option: AutoOption } = AutoComplete; const { Search } = Input; type OptionType = { value: string | number, name?: string, fullName?: (string)[], leaf: number, // 从 0 开始 parent?: OptionType, children?: OptionType[], checked: boolean, indeterminate: boolean, [propName: string]: any, }; type quickOptionsType = { value: string | number, name: string, options: OptionType[], index?: number, checked?: boolean, indeterminate?: boolean, } type State = { options: OptionType[], optionsFlat: OptionType[], quickOptions: quickOptionsType[], selectedValue: (string | number)[], selectedMap: OptionType[], clickedShowMap: { [stateName: string]: string | number }, searchValue: string, }; type PropsKeyValue = { propPValue: string, propValue: string, propName: string, } type SelectProps = { options?: OptionType[], // options 与 optionsFlat二选一 optionsFlat?: OptionType[], value?: string[] | number[], disabled?: boolean, keyValue?: PropsKeyValue, showCheckedStrategy?: 'SHOW_PARENT' | 'SHOW_CHILD' | 'SHOW_ALL', // 默认 SHOW_PARENT showCheckedFormat?: 'ALL' | 'SELF', // 默认 ALL —— 放在 select 里面或者展示里面,去 state.selected Map中的propName showSearch?: boolean, searchProps?: { [propName: string]: any }, showSearchFormat?: 'ALL' | 'SELF', // 默认 ALL —— 放在 search 面板 里面或者展示里面,去 state.selectedMap 中的propName onChange?: (values: string[] | number[]) => void, onCreateQuickOptions?: (options: OptionType[]) => quickOptionsType[], } export class MultiCascaderCard extends Component{ state: State; keyValue = { propPValue: 'pValue', propValue: 'value', propName: 'name', } multiCasacaderSelectRef = null; initState = {} constructor(props: SelectProps) { super(props); if (props['keyValue']) this.keyValue = { propPValue: props['keyValue']['propPValue'] || this.keyValue.propPValue, propValue: props['keyValue']['propValue'] || this.keyValue.propValue, propName: props['keyValue']['propName'] || this.keyValue.propName, } const initValueOption = props['options'] ? props['options'] : (props['optionsFlat'] ? transform2TreeData(props['optionsFlat'], this.keyValue) : []); const initValueOptionFlat = transform2FlatData(initValueOption, this.keyValue.propValue); this.state = { optionsFlat: initValueOptionFlat, options: initValueOption, quickOptions: null, selectedValue: props.value, selectedMap: [], clickedShowMap: {}, // 点击 searchValue: undefined, } // 初始化 快捷选择数据源 this.state.quickOptions = props.onCreateQuickOptions && this.state.optionsFlat ? props.onCreateQuickOptions(this.state.optionsFlat) : null; this.initState = { ...this.state } } componentDidMount() { const { value } = this.props; const { options } = this.state; if (!Tools.isEmptyArray(options) && value) { this.initOptions(value, options); } } UNSAFE_componentWillReceiveProps(nextProps) { const { options, optionsFlat, value } = nextProps; if (!Tools.isEmptyArray(options) && !is(List(options), List(this.props['options']))) { this.initOptions(value, options) } else if (!Tools.isEmptyArray(optionsFlat) && !is(List(optionsFlat), List(this.props['optionsFlat']))) { this.initOptions(value, transform2TreeData(optionsFlat, this.keyValue)) } else if (!is(List(value), List(this.props.value)) && !is(List(value), List(this.state.selectedValue))) { if (!Tools.isEmptyArray(options)) this.resetOptions(value, options) else if (!Tools.isEmptyArray(optionsFlat)) { this.resetOptions(value, transform2TreeData(optionsFlat, this.keyValue)) } } } shouldComponentUpdate(nextProps, nextState) { if ((is(Map(nextProps), Map(this.props)) && is(Map(nextState), Map(this.state)))) { return false } return true } initOptions = (initValue: (string | number)[], initValueOptions: OptionType[]) => { const { selectedMap, options, optionsFlat, selectedValue } = initOptions.bind(this)(initValue, initValueOptions); const quickOptions = this.createQuickOptions(optionsFlat); const { onChange } = this.props; this.setState({ selectedMap, options, optionsFlat, selectedValue, quickOptions, }) onChange && onChange(selectedValue) } resetOptions = (initValue: (string | number)[], initValueOptions: OptionType[]) => { const { selectedMap, options, optionsFlat, selectedValue } = initOptions.bind(this)(initValue, initValueOptions); this.setState({ selectedMap, options, optionsFlat, selectedValue, }, () => { this.resetQuickStatus() }) } /* ------------------------------下拉选择面板------------------------------ */ // 找到根结点 findRoot = (record: OptionType) => { const { propValue } = this.keyValue; function walk(r) { if (r && r['parent'] && r['parent'][propValue]) { return walk(r['parent']) } else if (r && !r['parent']) { return r } } return walk(record) } /** * @param options 最新操作过后的 options * 1. getSelectedMap 获取选中的项,此处可以根据 showCheckedStrategy 定制回填方式 * 2. 取值: * 2.1 SHOW_ALL:所有选中的节点都展示 * 2.2 SHOW_PARENT:当父节点的子节点都选中时,展示父节点(父节点:cheked:true ) —— 此为默认 * 2.3 SHOW_CHILD:展示最底层节点 */ getSelectedMap = (options: OptionType[]) => { const { showCheckedStrategy } = this.props; return options.reduce((prev, current) => { if (showCheckedStrategy === 'SHOW_CHILD') { if (current.checked && !current.children) { return prev.concat(current) } else if ((current.checked || current.indeterminate) && current.children) { return prev.concat(this.getSelectedMap(current.children)) } } else if (showCheckedStrategy === 'SHOW_ALL') { if (current.checked) { if (current.children) return prev.concat(current).concat(this.getSelectedMap(current.children)) else return prev.concat(current) } else if (current.indeterminate) { //后代部分选中 return prev.concat(this.getSelectedMap(current.children)) } } else { if (current.checked) { return prev.concat(current) } else if (current.indeterminate && current.children) { //后代部分选中 return prev.concat(this.getSelectedMap(current.children)) } } return prev }, []) } // 获取 selectedMap 和 替换 原来的 options中新操作过的 rootRecord onCollect = (record: OptionType, options: OptionType[]) => { const { propValue } = this.keyValue; const rootRecord = this.findRoot(record); options.find((item, index) => { if (item[propValue] === rootRecord[propValue]) { options[index] = rootRecord; // 注意:这里修改了 options return true }; }) const selectedMap = this.getSelectedMap(options); return { selectedMap: selectedMap, options: options } } // 选中 onCheck = (checked: boolean, record: OptionType, e?) => { window.event ? window.event.cancelBubble = true : e.stopPropagation(); let { options, optionsFlat } = this.state; let { onChange } = this.props; const { propValue, propName } = this.keyValue; this.onShow(record); // 在checkbox 选择面板时才需要触发 let newRecord = record; let newOptions = options; onCheckLow2High(checked, newRecord); onCheckHigh2Low(checked, newRecord); const collect = this.onCollect(newRecord, newOptions); const newValues = collect.selectedMap.map(item => item[propValue]) const newOptionsFlat = collect.options ? transform2FlatData(collect.options, propName) : optionsFlat; this.setState({ selectedMap: collect.selectedMap, options: collect.options, optionsFlat: newOptionsFlat, selectedValue: newValues, }, () => { // 更新了options、optionsFlat之后要更新 const { searchValue } = this.state; const { showSearch } = this.props; showSearch && this.onSearch(searchValue); this.resetQuickStatus() onChange && onChange(newValues) }) } // 打开子面板 onShow = (record: OptionType) => { if(!record) return console.error('MultiCascaderCard onShow receive OptionType is undefined') const { propValue } = this.keyValue; const newClickedShowMap = {}; function walkParent(r: OptionType) { newClickedShowMap[r['leaf']] = r[propValue] if (r && r['parent'] && r['parent'][propValue]) { walkParent(r['parent']) } } walkParent(record) this.setState({ clickedShowMap: { ...newClickedShowMap, [record['leaf'] + 1]: undefined } }) } renderCheckedBox = () => { const { disabled } = this.props; const { propValue, propName } = this.keyValue; const { options, clickedShowMap } = this.state; const columns = []; function mapColumn(options, clickedShowMap) { if (!options || !options.length) return const column = () columns.unshift(column); } mapColumn.bind(this)(options, clickedShowMap); return
e.preventDefault()}> {columns}
} /* ------------------------------快捷选择展示框------------------------------ */ createQuickOptions = (optionsFlat) => { var { onCreateQuickOptions } = this.props; if (!onCreateQuickOptions) return; const newQuickOptions = onCreateQuickOptions(optionsFlat); return newQuickOptions } resetQuickStatus = () => { const { propName, propValue } = this.keyValue; let { quickOptions, selectedMap } = this.state if (Tools.isEmptyArray(quickOptions)) return null; var selectMapBuff = transform2FlatData(selectedMap); let newQuickOptions = quickOptions.map(item => { let checked = item.options.every(e => selectMapBuff.some(f => f[propValue] === e[propValue])) let indeterminate = checked ? false : item.options.some(e => selectMapBuff.some(f => f[propValue] === e[propValue])); item['checked'] = checked; item['indeterminate'] = indeterminate; return item }) this.setState({ quickOptions: newQuickOptions }) } onQuickSelect(checked, record, index) { let { options, quickOptions, optionsFlat } = this.state; let { onChange } = this.props; const { propName, propValue } = this.keyValue; let newRecord = record; let newOptions = options; let newSelectedMap = []; newRecord['indeterminate'] = false; newRecord['checked'] = checked; if (!newRecord['options'] || !newRecord['options'].length) return console.warn('onQuickSelect record has no options, check!') newRecord['options'].forEach((item) => { onCheckLow2High(checked, item); onCheckHigh2Low(checked, item); const { options, selectedMap } = this.onCollect(item, newOptions); newOptions = options; newSelectedMap = selectedMap; }) const newValues = newSelectedMap.map(item => item[propValue]) const newOptionsFlat = newOptions ? transform2FlatData(newOptions, propName) : optionsFlat; const newQuickOption = quickOptions.map(item => { if (item['value'] === newRecord['value']) { return newRecord } else { return item } }) this.setState({ selectedMap: newSelectedMap, options: newOptions, optionsFlat: newOptionsFlat, selectedValue: newValues, quickOptions: newQuickOption }, () => { // 更新了options、optionsFlat之后要更新 const { searchValue } = this.state; const { showSearch } = this.props; showSearch && this.onSearch(searchValue) onChange && onChange(newValues) }) } /* 快捷选择的数据结构只有最底层级,直接children对应数据最下层children */ renderQuickSelect() { let { quickOptions } = this.state; const { disabled } = this.props if (!quickOptions || Tools.isEmptyArray(quickOptions)) { return null; } return (
快捷选择: { quickOptions.map((item, index) => ( this.onQuickSelect(!item['checked'], item, index)} checked={item['checked']} key={item.value} disabled={disabled} indeterminate={item['indeterminate']} >{item.name} )) }
) } /* ------------------------------select 选中展示框------------------------------ */ onClearAll = () => { const { onChange } = this.props; const { searchValue } = this.state; this.setState({ ...this.initState, selectedValue: [], searchValue }); onChange && onChange([]) } renderSelectedFormat = () => { const { showCheckedFormat, disabled } = this.props; const { selectedMap } = this.state; const { propName, propValue } = this.keyValue; if (showCheckedFormat === 'ALL') { return (selectedMap.map(item => { return {item && item['fullName'] && item['fullName'].join('/') || item[propName]} {!disabled ? this.onCheck(false, item)} /> : } })) } else { return (selectedMap.map(item => { return {item[propName]} {!disabled ? this.onCheck(false, item)} /> : } })) } } renderSelectedList = () => { const { selectedMap } = this.state; const { disabled } = this.props; if (!selectedMap || !selectedMap.length) return null; return (
已选择:
{ this.renderSelectedFormat() } this.onClearAll()}>清空
) } /* ------------------------------search 展示框------------------------------ */ onSearchSelect = (val, option) => { if (val === 'no-data') return if (option && option.props && option.props.record) { this.onCheck(true, option.props.record || {}) setTimeout(() => { const { searchValue } = this.state; this.onSearch(searchValue) }) } } @throttle(300) onSearch(val) { this.setState({ searchValue: val }); } renderSearchDataSource = () => { const { optionsFlat, searchValue } = this.state; const { showSearchFormat } = this.props; const { propName, propValue } = this.keyValue; const searchFlatOption = searchValue ? optionsFlat.filter(item => item[propName].includes(searchValue)) : []; // 内容清空时表示不查询 if (Tools.isEmptyArray(searchFlatOption)) return [] if (showSearchFormat === 'ALL') { const Node = (searchFlatOption.map(item => { const selected = !!item['checked']; return {item['fullName'].map((f, index) => { return ( {index ? ` / ` : null} {!selected ? f.split(searchValue).map((e, i) => { return {i ? {searchValue} : null} {e} }) : f} ) })} })) return Node } else { const Node = (searchFlatOption.map(item => { const selected = !!item['checked']; return {!selected ? item[propName].split(searchValue).map((e, i) => { return {i ? {searchValue} : null} {e} }) : item[propName]} })) return Node } } renderSearch = () => { const { searchProps = {}, disabled } = this.props; const { propName, propValue } = this.keyValue; return } render() { const { showSearch, onCreateQuickOptions } = this.props; return
{this.renderSelectedList()} {onCreateQuickOptions && this.renderQuickSelect()} {showSearch && this.renderSearch()} {this.renderCheckedBox()}
} }