import React from 'react'; import Overlay from '../../components/Overlay'; import Checkbox from '../../components/Checkbox'; import PopOver from '../../components/PopOver'; import {RootCloseWrapper} from 'react-overlays'; import {Icon} from '../../components/icons'; import { autobind, flattenTree, filterTree, string2regExp, getTreeAncestors, getTreeParent } from '../../utils/helper'; import { FormOptionsControl, OptionsControl, OptionsControlProps } from '../Form/Options'; import {Option, Options} from '../../components/Select'; import {findDOMNode} from 'react-dom'; import {ResultBox, Spinner} from '../../components'; import xor from 'lodash/xor'; import union from 'lodash/union'; /** * Nested Select * 文档:https://baidu.gitee.io/amis/docs/components/form/nested-select */ export interface NestedSelectControlSchema extends FormOptionsControl { type: 'nested-select'; } export interface NestedSelectProps extends OptionsControlProps { cascade?: boolean; noResultsText?: string; withChildren?: boolean; } export interface NestedSelectState { isOpened?: boolean; isFocused?: boolean; inputValue?: string; stack: Array>; } export default class NestedSelectControl extends React.Component< NestedSelectProps, NestedSelectState > { static defaultProps: Partial = { cascade: false, withChildren: false, searchPromptText: 'Select.searchPromptText', noResultsText: 'noResult', checkAll: true, checkAllLabel: '全选' }; target: any; input: HTMLInputElement; state: NestedSelectState = { isOpened: false, isFocused: false, inputValue: '', stack: [this.props.options] }; @autobind domRef(ref: any) { this.target = ref; } componentDidUpdate(prevProps: NestedSelectProps) { if (prevProps.options !== this.props.options) { this.setState({ stack: [this.props.options] }); } } @autobind handleOutClick(e: React.MouseEvent) { const {options} = this.props; e.defaultPrevented || this.setState({ isOpened: true }); } @autobind close() { this.setState({ isOpened: false }); } removeItem(index: number, e?: React.MouseEvent) { let { onChange, selectedOptions, joinValues, valueField, extractValue, delimiter, value } = this.props; e && e.stopPropagation(); selectedOptions.splice(index, 1); if (joinValues) { value = (selectedOptions as Options) .map(item => item[valueField || 'value']) .join(delimiter || ','); } else if (extractValue) { value = (selectedOptions as Options).map( item => item[valueField || 'value'] ); } onChange(value); } @autobind renderValue(item: Option, key?: any) { const {classnames: cx, labelField, options} = this.props; const ancestors = getTreeAncestors(options, item, true); return ( {`${ ancestors ? ancestors .map(item => `${item[labelField || 'label']}`) .join(' / ') : item[labelField || 'label'] }`} ); } @autobind handleOptionClick(option: Option) { const { multiple, onChange, joinValues, extractValue, valueField } = this.props; if (multiple) { return; } onChange( joinValues ? option[valueField || 'value'] : extractValue ? option[valueField || 'value'] : option ); !multiple && this.close(); } @autobind handleCheck(option: Option | Options, index?: number) { const { onChange, selectedOptions, joinValues, delimiter, extractValue, withChildren, cascade, options } = this.props; const {stack} = this.state; let valueField = this.props.valueField || 'value'; if ( !Array.isArray(option) && option.children && option.children.length && typeof index === 'number' ) { if (stack[index]) { stack.splice(index + 1, 1, option.children); } else { stack.push(option.children); } } const items = selectedOptions; let value: any[]; // 三种情况: // 1.全选,option为数组 // 2.单个选中,且有children // 3.单个选中,没有children if (Array.isArray(option)) { option = withChildren ? flattenTree(option) : option; value = items.length === option.length ? [] : (option as Options); } else if (Array.isArray(option.children)) { if (cascade) { value = xor(items, [option]); } else if (withChildren) { option = flattenTree([option]); const isEvery = (option as Options).every(opt => !!~items.indexOf(opt)); value = (isEvery ? xor : union)(items, option as any); } else { value = items.filter(item => !~flattenTree([option]).indexOf(item)); !~items.indexOf(option) && value.push(option); } } else { value = xor(items, [option]); } if (!cascade) { let toCheck = option; while (true) { const parent = getTreeParent(options, toCheck as any); if (parent?.value) { // 如果所有孩子节点都勾选了,应该自动勾选父级。 if (parent.children.every((child: any) => ~value.indexOf(child))) { parent.children.forEach((child: any) => { const index = value.indexOf(child); if (~index && !withChildren) { value.splice(index, 1); } }); value.push(parent); toCheck = parent; continue; } } break; } } onChange( joinValues ? value.map(item => item[valueField as string]).join(delimiter) : extractValue ? value.map(item => item[valueField as string]) : value ); } allChecked(options: Options): boolean { const {selectedOptions, withChildren} = this.props; return options.every(option => { if (withChildren && option.children) { return this.allChecked(option.children); } return selectedOptions.some(item => item === option); }); } partialChecked(options: Options): boolean { return options.some(option => { const childrenPartialChecked = option.children && this.partialChecked(option.children); return ( childrenPartialChecked || this.props.selectedOptions.some(item => item === option) ); }); } reload() { const reload = this.props.reloadOptions; reload && reload(); } @autobind onFocus(e: any) { this.props.disabled || this.state.isOpened || this.setState({ isFocused: true }); } @autobind onBlur(e: any) { this.setState({ isFocused: false }); } @autobind getTarget() { if (!this.target) { this.target = findDOMNode(this) as HTMLElement; } return this.target as HTMLElement; } @autobind handleKeyPress(e: React.KeyboardEvent) { if (e.key === ' ') { this.handleOutClick(e as any); e.preventDefault(); } } @autobind handleInputKeyDown(event: React.KeyboardEvent) { const inputValue = this.state.inputValue; const {multiple, selectedOptions} = this.props; if ( event.key === 'Backspace' && !inputValue && selectedOptions.length && multiple ) { this.removeItem(selectedOptions.length - 1); } } @autobind handleInputChange(inputValue: string) { const {options, labelField, valueField} = this.props; const regexp = string2regExp(inputValue); let filtedOptions = inputValue && this.state.isOpened ? filterTree( options, option => regexp.test(option[labelField || 'label']) || regexp.test(option[valueField || 'value']) || !!(option.children && option.children.length), 1, true ) : options.concat(); this.setState({ inputValue, stack: [filtedOptions] }); } @autobind handleResultChange(value: Array