import React, { Component } from 'react'; import { is, List, Map } from 'immutable'; import * as Tools from 'jad-tool'; import { Select, Empty, Checkbox, Icon } from 'antd'; import classNames from 'classnames'; import { throttle } from 'jad-tool'; import { initOptions, onCheckLow2High, onCheckHigh2Low, transform2FlatData, transform2TreeData } from './utils'; type OptionType = { value: string | number, name?: string, fullName?: (string)[], leaf: number, // 从 0 开始 parent?: OptionType, children?: OptionType[], checked: boolean, indeterminate: boolean, }; type State = { options: OptionType[], optionsFlat: OptionType[], 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[], // 最简单的二维数组:需要[propPValue]/[propValue]/[propName] value?: string[] | number[], disabled?: boolean, keyValue?: PropsKeyValue, onChange?: (values: string[] | number[]) => void, showCheckedStrategy?: 'SHOW_PARENT' | 'SHOW_CHILD' | 'SHOW_ALL', // 默认 SHOW_PARENT showCheckedFormat?: 'ALL' | 'SELF', // 默认 ALL —— 放在 select 里面或者展示里面,去 state.selected Map中的propName showSearchFormat?: 'ALL' | 'SELF', // 默认 ALL —— 放在 select search 面板 里面或者展示里面,去 state.selectedMap 中的propName selectProps?: { [propsName: string]: any } // select 中除了排除掉上述的其他属性,以及labelInValue } export class MultiCascaderSelect extends Component{ state: State; keyValue = { propPValue: 'pValue', propValue: 'value', propName: 'name', } multiCasacaderSelectRef = null; 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, selectedValue: props.value, selectedMap: [], clickedShowMap: {}, // 点击 searchValue: '', } } componentDidMount() { const { value } = this.props; const { options } = this.state; if (!Tools.isEmptyArray(options)) { 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))) { if (!Tools.isEmptyArray(options)) this.initOptions(value, options) else if (!Tools.isEmptyArray(optionsFlat)) this.initOptions(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 { onChange } = this.props; this.setState({ selectedMap, options, optionsFlat, selectedValue, }) onChange && onChange(selectedValue) } /* ------------------------------下拉选择面板------------------------------ */ // 找到根结点 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, searchValue } = this.state; let { onChange } = this.props; const { propValue, propName } = this.keyValue; !searchValue && 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, }) onChange && onChange(newValues) } // 打开子面板 onShow = (record: OptionType) => { if(!record) return console.error('MultiCascaderSelect 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 } }) } renderCheckedDropdown = () => { 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}
} /* ------------------------------下拉查询面板------------------------------ */ onSearchSelect = (record: OptionType) => { const { selectedValue } = this.state; const { propValue } = this.keyValue; if (!selectedValue.includes(record[propValue])) { this.onCheck(true, record) } } getSearchFiteredOptions(optionsFlat: OptionType[]) { const { showSearchFormat } = this.props; const { searchValue } = this.state; const { propName } = this.keyValue; if (showSearchFormat === 'ALL') { return optionsFlat.filter(item => (searchValue && item['fullName'].join('').includes(searchValue))) } else { return optionsFlat.filter(item => (searchValue && item[propName].includes(searchValue))) } } renderSearchDropdown() { const { showSearchFormat } = this.props; const { propValue, propName } = this.keyValue; const { optionsFlat, searchValue } = this.state; const filteredOptions = this.getSearchFiteredOptions(optionsFlat) || []; if (!searchValue) return if (Tools.isEmptyArray(filteredOptions)) return if (showSearchFormat === 'ALL') { const Node = filteredOptions.map(obj => { const selected = obj['checked']; return
false : (e) => this.onSearchSelect(obj)} className={classNames('jad-multi-cascader-select-search-item', selected ? 'item-selected' : '')} > {obj['fullName'].map((f, index) => { return ( {index ? ` / ` : null} {!selected ? f.split(searchValue).map((e, i) => { return {i ? {searchValue} : null} {e} }) : {f}} ) })}
}) return
e.preventDefault()} className={'jad-multi-cascader-select-search-drop'}>{Node}
} else { const Node = filteredOptions.map(obj => { const selected = obj['checked']; return
false : (e) => this.onSearchSelect(obj)} className={classNames('jad-multi-cascader-select-search-item', selected ? 'item-selected' : '')} > {!selected ? obj[propName].split(searchValue).map((f, index) => { return ( {index ? {searchValue} : null} {f} ) }) : obj[propName]}
}) return
e.preventDefault()} className={'jad-multi-cascader-select-search-drop'}>{Node}
} } renderDropdown = (menu) => { const { searchValue } = this.state; if (!searchValue) return this.renderCheckedDropdown() else return this.renderSearchDropdown.bind(this)() } /* ------------------------------select 选中展示框------------------------------ */ onSelectChange = (values, option) => { const { options, optionsFlat, onChange } = this.props; const { propName } = this.keyValue; if (Tools.isEmptyArray(values)) { // 清空,恢复初始props this.setState({ selectedValue: [], selectedMap: [], options: options ? options : transform2TreeData(optionsFlat, this.keyValue), optionsFlat: optionsFlat ? optionsFlat : transform2FlatData(options, propName), }) onChange && onChange([]) } } onDeselect = (value: { key: string | number, label: string }) => { if (!value || !value['key']) return console.warn('MultiCascaderSelect onDeselect value has some problems') const { selectedMap } = this.state; const { propValue } = this.keyValue; const record = selectedMap.find(item => item[propValue] === value['key']) this.onCheck(false, record) } @throttle(300) onSearch(value) { this.setState({ searchValue: value }) } onBlur = () => { this.setState({ searchValue: '' }) } getCheckedValueFormat = () => { const { showCheckedFormat } = this.props; const { selectedMap } = this.state; const { propValue, propName } = this.keyValue; if (showCheckedFormat === 'ALL') { return selectedMap.map(item => ({ key: item && item[propValue], label: item && item['fullName'] && item['fullName'].join('/') || item[propName] })) } else { return selectedMap.map(item => ({ key: item && item[propValue], label: item && item[propName] })) } } render() { const { disabled, selectProps } = this.props; return