import PropTypes from '../_util/vue-types' import VcCascader from '../vc-cascader' import arrayTreeFilter from 'array-tree-filter' import classNames from 'classnames' import omit from 'omit.js' import KeyCode from '../_util/KeyCode' import Input from '../input' import Icon from '../icon' import { hasProp, filterEmpty, getOptionProps } from '../_util/props-util' import BaseMixin from '../_util/BaseMixin' const CascaderOptionType = PropTypes.shape({ value: PropTypes.string.isRequired, label: PropTypes.any.isRequired, disabled: PropTypes.bool, children: PropTypes.array, __IS_FILTERED_OPTION: PropTypes.bool, }).loose const CascaderExpandTrigger = PropTypes.oneOf(['click', 'hover']) const ShowSearchType = PropTypes.shape({ filter: PropTypes.func, render: PropTypes.func, sort: PropTypes.func, matchInputWidth: PropTypes.bool, }).loose function noop () {} const CascaderProps = { /** 可选项数据源 */ options: PropTypes.arrayOf(CascaderOptionType).def([]), /** 默认的选中项 */ defaultValue: PropTypes.arrayOf(PropTypes.string), /** 指定选中项 */ value: PropTypes.arrayOf(PropTypes.string), /** 选择完成后的回调 */ // onChange?: (value: string[], selectedOptions?: CascaderOptionType[]) => void; /** 选择后展示的渲染函数 */ displayRender: PropTypes.func, transitionName: PropTypes.string.def('slide-up'), popupStyle: PropTypes.object.def({}), /** 自定义浮层类名 */ popupClassName: PropTypes.string, /** 浮层预设位置:`bottomLeft` `bottomRight` `topLeft` `topRight` */ popupPlacement: PropTypes.oneOf(['bottomLeft', 'bottomRight', 'topLeft', 'topRight']).def('bottomLeft'), /** 输入框占位文本*/ placeholder: PropTypes.string.def('Please select'), /** 输入框大小,可选 `large` `default` `small` */ size: PropTypes.oneOf(['large', 'default', 'small']), /** 禁用*/ disabled: PropTypes.bool.def(false), /** 是否支持清除*/ allowClear: PropTypes.bool.def(true), showSearch: PropTypes.oneOfType([PropTypes.bool, ShowSearchType]), notFoundContent: PropTypes.any.def('Not Found'), loadData: PropTypes.func, /** 次级菜单的展开方式,可选 'click' 和 'hover' */ expandTrigger: CascaderExpandTrigger, /** 当此项为 true 时,点选每级菜单选项值都会发生变化 */ changeOnSelect: PropTypes.bool, /** 浮层可见变化时回调 */ // onPopupVisibleChange?: (popupVisible: boolean) => void; prefixCls: PropTypes.string.def('ant-cascader'), inputPrefixCls: PropTypes.string.def('ant-input'), getPopupContainer: PropTypes.func, popupVisible: PropTypes.bool, } function defaultFilterOption (inputValue, path) { return path.some(option => option.label.indexOf(inputValue) > -1) } function defaultSortFilteredOption (a, b, inputValue) { function callback (elem) { return elem.label.indexOf(inputValue) > -1 } return a.findIndex(callback) - b.findIndex(callback) } const defaultDisplayRender = ({ labels }) => labels.join(' / ') export default { mixins: [BaseMixin], props: CascaderProps, model: { prop: 'value', event: 'change', }, data () { this.cachedOptions = [] const { value, defaultValue, popupVisible, showSearch, options, changeOnSelect, flattenTree } = this return { sValue: value || defaultValue || [], inputValue: '', inputFocused: false, sPopupVisible: popupVisible, flattenOptions: showSearch && flattenTree(options, changeOnSelect), } }, watch: { value (val) { this.setState({ sValue: val || [] }) }, popupVisible (val) { this.setState({ sPopupVisible: val }) }, options (val) { if (this.showSearch) { this.setState({ flattenOptions: this.flattenTree(this.options, this.changeOnSelect) }) } }, }, methods: { highlightKeyword (str, keyword, prefixCls) { return str.split(keyword) .map((node, index) => index === 0 ? node : [ {keyword}, node, ]) }, defaultRenderFilteredOption ({ inputValue, path, prefixCls }) { return path.map(({ label }, index) => { const node = label.indexOf(inputValue) > -1 ? this.highlightKeyword(label, inputValue, prefixCls) : label return index === 0 ? node : [' / ', node] }) }, handleChange (value, selectedOptions) { this.setState({ inputValue: '' }) if (selectedOptions[0].__IS_FILTERED_OPTION) { const unwrappedValue = value[0] const unwrappedSelectedOptions = selectedOptions[0].path this.setValue(unwrappedValue, unwrappedSelectedOptions) return } this.setValue(value, selectedOptions) }, handlePopupVisibleChange (popupVisible) { if (!hasProp(this, 'popupVisible')) { this.setState({ sPopupVisible: popupVisible, inputFocused: popupVisible, inputValue: popupVisible ? this.inputValue : '', }) } this.$emit('popupVisibleChange', popupVisible) }, handleInputBlur () { this.setState({ inputFocused: false, }) }, handleInputClick (e) { const { inputFocused, sPopupVisible } = this // Prevent `Trigger` behaviour. if (inputFocused || sPopupVisible) { e.stopPropagation() e.nativeEvent.stopImmediatePropagation() } }, handleKeyDown (e) { if (e.keyCode === KeyCode.BACKSPACE) { e.stopPropagation() } }, handleInputChange (e) { const inputValue = e.target.value this.setState({ inputValue }) }, setValue (value, selectedOptions) { if (!hasProp(this, 'value')) { this.setState({ sValue: value }) } this.$emit('change', value, selectedOptions) }, getLabel () { const { options, $scopedSlots } = this const displayRender = this.displayRender || $scopedSlots.displayRender || defaultDisplayRender const value = this.sValue const unwrappedValue = Array.isArray(value[0]) ? value[0] : value const selectedOptions = arrayTreeFilter(options, (o, level) => o.value === unwrappedValue[level], ) const labels = selectedOptions.map(o => o.label) return displayRender({ labels, selectedOptions }) }, clearSelection (e) { e.preventDefault() e.stopPropagation() if (!this.inputValue) { this.setValue([]) this.handlePopupVisibleChange(false) } else { this.setState({ inputValue: '' }) } }, flattenTree (options, changeOnSelect, ancestor = []) { let flattenOptions = [] options.forEach((option) => { const path = ancestor.concat(option) if (changeOnSelect || !option.children || !option.children.length) { flattenOptions.push(path) } if (option.children) { flattenOptions = flattenOptions.concat(this.flattenTree(option.children, changeOnSelect, path)) } }) return flattenOptions }, generateFilteredOptions (prefixCls) { const { showSearch, notFoundContent, flattenOptions, inputValue, $scopedSlots } = this const { filter = defaultFilterOption, // render = this.defaultRenderFilteredOption, sort = defaultSortFilteredOption, } = showSearch const render = showSearch.render || $scopedSlots.showSearchRender || this.defaultRenderFilteredOption const filtered = flattenOptions.filter((path) => filter(inputValue, path)) .sort((a, b) => sort(a, b, inputValue)) if (filtered.length > 0) { return filtered.map((path) => { return { __IS_FILTERED_OPTION: true, path, label: render({ inputValue, path, prefixCls }), value: path.map((o) => o.value), disabled: path.some((o) => o.disabled), } }) } return [{ label: notFoundContent, value: 'ANT_CASCADER_NOT_FOUND', disabled: true }] }, focus () { this.$refs.input.focus() }, blur () { this.$refs.input.blur() }, }, render () { const { $slots, sValue: value, sPopupVisible, inputValue } = this const props = getOptionProps(this) const { prefixCls, inputPrefixCls, placeholder, size, disabled, allowClear, showSearch = false, ...otherProps } = props const sizeCls = classNames({ [`${inputPrefixCls}-lg`]: size === 'large', [`${inputPrefixCls}-sm`]: size === 'small', }) const clearIcon = (allowClear && !disabled && value.length > 0) || inputValue ? ( ) : null const arrowCls = classNames({ [`${prefixCls}-picker-arrow`]: true, [`${prefixCls}-picker-arrow-expand`]: sPopupVisible, }) const pickerCls = classNames( `${prefixCls}-picker`, { [`${prefixCls}-picker-with-value`]: inputValue, [`${prefixCls}-picker-disabled`]: disabled, [`${prefixCls}-picker-${size}`]: !!size, }) // Fix bug of https://github.com/facebook/react/pull/5004 // and https://fb.me/react-unknown-prop const tempInputProps = omit(otherProps, [ 'options', 'popupPlacement', 'transitionName', 'displayRender', 'changeOnSelect', 'expandTrigger', 'popupVisible', 'getPopupContainer', 'loadData', 'popupClassName', 'filterOption', 'renderFilteredOption', 'sortFilteredOption', 'notFoundContent', 'defaultValue', ]) let options = this.options if (inputValue) { options = this.generateFilteredOptions(prefixCls) } // Dropdown menu should keep previous status until it is fully closed. if (!sPopupVisible) { options = this.cachedOptions } else { this.cachedOptions = options } const dropdownMenuColumnStyle = {} const isNotFound = (options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND' if (isNotFound) { dropdownMenuColumnStyle.height = 'auto' // Height of one row. } // The default value of `matchInputWidth` is `true` const resultListMatchInputWidth = showSearch.matchInputWidth !== false if (resultListMatchInputWidth && inputValue && this.input) { dropdownMenuColumnStyle.width = this.input.input.offsetWidth } const inputProps = { props: { ...tempInputProps, prefixCls: inputPrefixCls, placeholder: value && value.length > 0 ? undefined : placeholder, value: inputValue, disabled: disabled, readOnly: !showSearch, autoComplete: 'off', }, class: `${prefixCls}-input ${sizeCls}`, ref: 'input', on: { click: showSearch ? this.handleInputClick : noop, blur: showSearch ? this.handleInputBlur : noop, keydown: this.handleKeyDown, change: showSearch ? this.handleInputChange : noop, }, } const children = filterEmpty($slots.default) const input = children.length ? children : ( { showSearch ? {this.getLabel()} : null} { !showSearch ? {this.getLabel()} : null} {clearIcon} ) const cascaderProps = { props: { ...props, options: options, value: value, popupVisible: sPopupVisible, dropdownMenuColumnStyle: dropdownMenuColumnStyle, }, on: { popupVisibleChange: this.handlePopupVisibleChange, change: this.handleChange, }, } return ( {input} ) }, }