import KeyCode from '../_util/KeyCode' import PropTypes from '../_util/vue-types' import classnames from 'classnames' import classes from 'component-classes' import { Item as MenuItem, ItemGroup as MenuItemGroup } from '../vc-menu' import warning from 'warning' import Vue from 'vue' import Option from './Option' import { hasProp, getSlotOptions, getPropsData, getValueByProp as getValue, getComponentFromProp, getEvents, getClass, getStyle, getAttrs, getOptionProps } from '../_util/props-util' import getTransitionProps from '../_util/getTransitionProps' import { cloneElement } from '../_util/vnode' import BaseMixin from '../_util/BaseMixin' import proxyComponent from '../_util/proxyComponent' import antRefDirective from '../_util/antRefDirective' Vue.use(antRefDirective) import { getPropValue, getValuePropValue, isCombobox, isMultipleOrTags, isMultipleOrTagsOrCombobox, isSingleMode, toArray, getMapKey, findIndexInValueBySingleValue, getLabelFromPropsValue, UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE, preventDefaultEvent, findFirstMenuItem, includesSeparators, splitBySeparators, defaultFilterFn, validateOptionValue, saveRef, toTitle, } from './util' import SelectTrigger from './SelectTrigger' import { SelectPropTypes } from './PropTypes' function noop () {} function chaining (...fns) { return function (...args) { // eslint-disable-line // eslint-disable-line for (let i = 0; i < fns.length; i++) { if (fns[i] && typeof fns[i] === 'function') { fns[i].apply(this, args) } } } } const Select = { inheritAttrs: false, name: 'Select', mixins: [BaseMixin], props: { ...SelectPropTypes, prefixCls: SelectPropTypes.prefixCls.def('rc-select'), defaultOpen: PropTypes.bool.def(false), labelInValue: SelectPropTypes.labelInValue.def(false), defaultActiveFirstOption: SelectPropTypes.defaultActiveFirstOption.def(true), showSearch: SelectPropTypes.showSearch.def(true), allowClear: SelectPropTypes.allowClear.def(false), placeholder: SelectPropTypes.placeholder.def(''), showArrow: SelectPropTypes.showArrow.def(true), dropdownMatchSelectWidth: PropTypes.bool.def(true), dropdownStyle: SelectPropTypes.dropdownStyle.def({}), dropdownMenuStyle: PropTypes.object.def({}), optionFilterProp: SelectPropTypes.optionFilterProp.def('value'), optionLabelProp: SelectPropTypes.optionLabelProp.def('value'), notFoundContent: PropTypes.any.def('Not Found'), backfill: PropTypes.bool.def(false), showAction: SelectPropTypes.showAction.def(['click']), combobox: PropTypes.bool.def(false), tokenSeparators: PropTypes.arrayOf(PropTypes.string).def([]), autoClearSearchValue: PropTypes.bool.def(true), // onChange: noop, // onFocus: noop, // onBlur: noop, // onSelect: noop, // onSearch: noop, // onDeselect: noop, // onInputKeydown: noop, }, model: { prop: 'value', event: 'change', }, created () { this.saveInputRef = saveRef(this, 'inputRef') this.saveInputMirrorRef = saveRef(this, 'inputMirrorRef') this.saveTopCtrlRef = saveRef(this, 'topCtrlRef') this.saveSelectTriggerRef = saveRef(this, 'selectTriggerRef') this.saveRootRef = saveRef(this, 'rootRef') this.saveSelectionRef = saveRef(this, 'selectionRef') }, data () { const props = getOptionProps(this) const optionsInfo = this.getOptionsInfoFromProps(props) warning( this.__propsSymbol__, 'Replace slots.default with props.children and pass props.__propsSymbol__' ) return { _value: this.getValueFromProps(props, true), // true: use default value _inputValue: props.combobox ? this.getInputValueForCombobox( props, optionsInfo, true, // use default value ) : '', _open: props.defaultOpen, _optionsInfo: optionsInfo, // a flag for aviod redundant getOptionsInfoFromProps call _skipBuildOptionsInfo: true, } }, beforeMount () { const state = this.getDerivedStateFromProps(getOptionProps(this), this.$data) Object.assign(this.$data, state) }, mounted () { this.$nextTick(() => { this.autoFocus && this.focus() }) }, watch: { __propsSymbol__ () { Object.assign(this.$data, this.getDerivedStateFromProps(getOptionProps(this), this.$data)) }, // value (val) { // let sValue = toArray(val) // if (this.labelInValue) { // sValue.forEach(v => { // v.key = v.key !== undefined ? v.key : v.value // }) // } else { // sValue = sValue.map(v => { // return { // key: v, // } // }) // } // this.sValue = sValue // this.initLabelAndTitleMap(sValue) // sValue.forEach((val) => { // const key = val.key // let { label, title } = val // label = label === undefined ? this.labelMap.get(key) : label // title = title === undefined ? this.titleMap.get(key) : title // this.labelMap.set(key, label === undefined ? key : label) // this.titleMap.set(key, title) // }) // if (this.combobox) { // this.setState({ // _inputValue: sValue.length ? this.labelMap.get((sValue[0].key)) : '', // }) // } // }, // combobox (val) { // if (val) { // this.setState({ // _inputValue: this.sValue.length ? this.labelMap.get((this.sValue[0].key)) : '', // }) // } // }, }, updated () { this.$nextTick(() => { if (isMultipleOrTags(this.$props)) { const inputNode = this.getInputDOMNode() const mirrorNode = this.getInputMirrorDOMNode() if (inputNode.value) { inputNode.style.width = '' inputNode.style.width = `${mirrorNode.clientWidth + 10}px` } else { inputNode.style.width = '' } } this.forcePopupAlign() }) }, beforeDestroy () { this.clearFocusTime() this.clearBlurTime() if (this.dropdownContainer) { document.body.removeChild(this.dropdownContainer) this.dropdownContainer = null } }, methods: { getDerivedStateFromProps (nextProps, prevState) { const optionsInfo = prevState._skipBuildOptionsInfo ? prevState._optionsInfo : this.getOptionsInfoFromProps(nextProps, prevState) const newState = { _optionsInfo: optionsInfo, _skipBuildOptionsInfo: false, } if ('open' in nextProps) { newState._open = nextProps.open } if ('value' in nextProps) { const value = this.getValueFromProps(nextProps) newState._value = value if (nextProps.combobox) { newState._inputValue = this.getInputValueForCombobox( nextProps, optionsInfo, ) } } return newState }, // initLabelAndTitleMap (sValue) { // // 保留已选中的label and title // const labelArr = [] // const titleArr = [] // const values = sValue || this.sValue // values.forEach((val) => { // const key = val.key // let { label, title } = val // label = label === undefined ? this.labelMap.get(key) : label // title = title === undefined ? this.titleMap.get(key) : title // title = typeof title === 'string' ? title.trim() : title // labelArr.push([key, label === undefined ? key : label]) // titleArr.push([key, title]) // }) // this.labelMap = new Map(labelArr) // this.titleMap = new Map(titleArr) // this.updateLabelAndTitleMap(this.$props.children) // }, // updateLabelAndTitleMap (children = []) { // children.forEach(child => { // if (!child.data || child.data.slot !== undefined) { // return // } // if (getSlotOptions(child).isSelectOptGroup) { // this.updateLabelAndTitleMap(child.componentOptions.children) // } else { // const key = getValuePropValue(child) // this.titleMap.set(key, getValue(child, 'title')) // this.labelMap.set(key, this.getLabelFromOption(child)) // } // }) // }, onInputChange (event) { const { tokenSeparators } = this.$props const val = event.target.value if ( isMultipleOrTags(this.$props) && tokenSeparators.length && includesSeparators(val, tokenSeparators) ) { const nextValue = this.getValueByInput(val) if (nextValue !== undefined) { this.fireChange(nextValue) } this.setOpenState(false, true) this.setInputValue('', false) return } this.setInputValue(val) this.setState({ _open: true, }) if (isCombobox(this.$props)) { this.fireChange([val]) } }, onDropdownVisibleChange (open) { if (open && !this._focused) { this.clearBlurTime() this.timeoutFocus() this._focused = true this.updateFocusClassName() } this.setOpenState(open) }, // combobox ignore onKeyDown (event) { const props = this.$props if (props.disabled) { return } const keyCode = event.keyCode if (this.$data._open && !this.getInputDOMNode()) { this.onInputKeydown(event) } else if (keyCode === KeyCode.ENTER || keyCode === KeyCode.DOWN) { // vue state是同步更新,onKeyDown在onMenuSelect后会再次调用,单选时不在调用setOpenState if (keyCode === KeyCode.ENTER && !isMultipleOrTags(props)) { this.maybeFocus(true) } else { this.setOpenState(true) } event.preventDefault() } }, onInputKeydown (event) { const props = this.$props if (props.disabled) { return } const state = this.$data const keyCode = event.keyCode if ( isMultipleOrTags(props) && !event.target.value && keyCode === KeyCode.BACKSPACE ) { event.preventDefault() const { _value: value } = state if (value.length) { this.removeSelected(value[value.length - 1]) } return } if (keyCode === KeyCode.DOWN) { if (!state._open) { this.openIfHasChildren() event.preventDefault() event.stopPropagation() return } } else if (keyCode === KeyCode.ENTER && state._open) { // Aviod trigger form submit when select item // https://github.com/ant-design/ant-design/issues/10861 event.preventDefault() } else if (keyCode === KeyCode.ESC) { if (state._open) { this.setOpenState(false) event.preventDefault() event.stopPropagation() } return } if (state._open) { const menu = this.selectTriggerRef.getInnerMenu() if (menu && menu.onKeyDown(event, this.handleBackfill)) { event.preventDefault() event.stopPropagation() } } }, onMenuSelect ({ item }) { if (!item) { return } let value = this.$data._value const props = this.$props const selectedValue = getValuePropValue(item) const lastValue = value[value.length - 1] this.fireSelect(selectedValue) if (isMultipleOrTags(props)) { if (findIndexInValueBySingleValue(value, selectedValue) !== -1) { return } value = value.concat([selectedValue]) } else { if (lastValue && lastValue === selectedValue && selectedValue !== this.$data._backfillValue) { this.setOpenState(false, true) return } value = [selectedValue] this.setOpenState(false, true) } this.fireChange(value) let inputValue if (isCombobox(props)) { inputValue = getPropValue(item, props.optionLabelProp) } else { inputValue = '' } if (props.autoClearSearchValue) { this.setInputValue(inputValue, false) } }, onMenuDeselect ({ item, domEvent }) { if (domEvent.type === 'click') { this.removeSelected(getValuePropValue(item)) } if (this.autoClearSearchValue) { this.setInputValue('', false) } }, onArrowClick (e) { e.stopPropagation() e.preventDefault() if (!this.disabled) { this.setOpenState(!this.$data._open, !this.$data._open) } }, onPlaceholderClick (e) { // if (this.openStatus) { // e.stopPropagation() // } if (this.getInputDOMNode()) { this.getInputDOMNode().focus() } }, // onOuterFocus (e) { // if (this.disabled) { // e.preventDefault() // return // } // this.clearBlurTime() // if ( // !isMultipleOrTagsOrCombobox(this.$props) && // e.target === this.getInputDOMNode() // ) { // return // } // if (this._focused) { // return // } // this._focused = true // this.updateFocusClassName() // this.timeoutFocus() // }, onPopupFocus () { // fix ie scrollbar, focus element again this.maybeFocus(true, true) }, // onOuterBlur (e) { // if (this.disabled) { // e.preventDefault() // return // } // this.blurTimer = setTimeout(() => { // this._focused = false // this.updateFocusClassName() // const props = this.$props // let { sValue } = this // const { inputValue } = this // if ( // isSingleMode(props) && // props.showSearch && // inputValue && // props.defaultActiveFirstOption // ) { // const options = this._options || [] // if (options.length) { // const firstOption = findFirstMenuItem(options) // if (firstOption) { // sValue = [ // { // key: firstOption.key, // label: this.getLabelFromOption(firstOption), // }, // ] // this.fireChange(sValue) // } // } // } else if (isMultipleOrTags(props) && inputValue) { // this.inputValue = this.getInputDOMNode().value = '' // } // this.$emit('blur', this.getVLForOnChange(sValue)) // this.setOpenState(false) // }, 10) // }, onClearSelection (event) { const props = this.$props const state = this.$data if (props.disabled) { return } const { _inputValue: inputValue, _value: value } = state event.stopPropagation() if (inputValue || value.length) { if (value.length) { this.fireChange([]) } this.setOpenState(false, true) if (inputValue) { this.setInputValue('') } } }, onChoiceAnimationLeave () { this.forcePopupAlign() }, getOptionsFromChildren (children = [], options = []) { children.forEach(child => { if (!child.data || child.data.slot !== undefined) { return } if (getSlotOptions(child).isSelectOptGroup) { this.getOptionsFromChildren(child.componentOptions.children, options) } else { options.push(child) } }) return options }, getInputValueForCombobox (props, optionsInfo, useDefaultValue) { let value = [] if ('value' in props && !useDefaultValue) { value = toArray(props.value) } if ('defaultValue' in props && useDefaultValue) { value = toArray(props.defaultValue) } if (value.length) { value = value[0] } else { return '' } let label = value if (props.labelInValue) { label = value.label } else if (optionsInfo[getMapKey(value)]) { label = optionsInfo[getMapKey(value)].label } if (label === undefined) { label = '' } return label }, getLabelFromOption (props, option) { return getPropValue(option, props.optionLabelProp) }, getOptionsInfoFromProps (props, preState) { const options = this.getOptionsFromChildren(this.$props.children) const optionsInfo = {} options.forEach((option) => { const singleValue = getValuePropValue(option) optionsInfo[getMapKey(singleValue)] = { option, value: singleValue, label: this.getLabelFromOption(props, option), title: getValue(option, 'title'), } }) if (preState) { // keep option info in pre state value. const oldOptionsInfo = preState._optionsInfo const value = preState._value value.forEach(v => { const key = getMapKey(v) if (!optionsInfo[key] && oldOptionsInfo[key] !== undefined) { optionsInfo[key] = oldOptionsInfo[key] } }) } return optionsInfo }, getValueFromProps (props, useDefaultValue) { let value = [] if ('value' in props && !useDefaultValue) { value = toArray(props.value) } if ('defaultValue' in props && useDefaultValue) { value = toArray(props.defaultValue) } if (props.labelInValue) { value = value.map((v) => { return v.key }) } return value }, getOptionInfoBySingleValue (value, optionsInfo) { let info optionsInfo = optionsInfo || this.$data._optionsInfo if (optionsInfo[getMapKey(value)]) { info = optionsInfo[getMapKey(value)] } if (info) { return info } let defaultLabel = value if (this.$props.labelInValue) { const label = getLabelFromPropsValue(this.$props.value, value) if (label !== undefined) { defaultLabel = label } } const defaultInfo = { option: , value, label: defaultLabel, } return defaultInfo }, getOptionBySingleValue (value) { const { option } = this.getOptionInfoBySingleValue(value) return option }, getOptionsBySingleValue (values) { return values.map(value => { return this.getOptionBySingleValue(value) }) }, // getSingleOptionByValueKey (key) { // return this.getOptionsFromChildren({ // key, // label: key, // }, this.$props.children) // }, // getOptionsByValue (value) { // if (value === undefined) { // return undefined // } // if (value.length === 0) { // return [] // } // return this.getOptionsFromChildren(value, this.$props.children) // }, // getLabelBySingleValue (children = [], value) { // if (value === undefined) { // return null // } // let label = null // children.forEach(child => { // if (!child.data || child.data.slot !== undefined) { // return // } // if (getSlotOptions(child).isSelectOptGroup) { // const maybe = this.getLabelBySingleValue(child.componentOptions.children, value) // if (maybe !== null) { // label = maybe // } // } else if (getValuePropValue(child) === value) { // label = this.getLabelFromOption(child) // } // }) // return label // }, getValueByLabel (label) { if (label === undefined) { return null } let value = null Object.keys(this.$data._optionsInfo).forEach(key => { const info = this.$data._optionsInfo[key] if (toArray(info.label).join('') === label) { value = info.value } }) return value }, // getValueByLabel (children = [], label) { // if (label === undefined) { // return null // } // let value = null // children.forEach(child => { // if (!child.data || child.data.slot !== undefined) { // return // } // if (getSlotOptions(child).isSelectOptGroup) { // const maybe = this.getValueByLabel(child.componentOptions.children, label) // if (maybe !== null) { // value = maybe // } // } else if (toArray(this.getLabelFromOption(child)).join('') === label) { // value = getValuePropValue(child) // } // }) // return value // }, // getLabelFromOption (child) { // let label = getPropValue(child, this.optionLabelProp) // if (Array.isArray(label) && label.length === 1 && !label[0].tag) { // label = label[0].text // } // return label // }, getVLBySingleValue (value) { if (this.$props.labelInValue) { return { key: value, label: this.getLabelBySingleValue(value), } } return value }, // getLabelFromProps (value) { // return this.getLabelByValue(this.$props.children || [], value) // }, getVLForOnChange (vls_) { let vls = vls_ if (vls !== undefined) { if (!this.labelInValue) { vls = vls.map(v => v) } else { vls = vls.map(vl => ({ key: vl, label: this.getLabelBySingleValue(vl), })) } return isMultipleOrTags(this.$props) ? vls : vls[0] } return vls }, getLabelBySingleValue (value, optionsInfo) { const { label } = this.getOptionInfoBySingleValue(value, optionsInfo) return label }, // getLabelByValue (children, value) { // const label = this.getLabelBySingleValue(children, value) // if (label === null) { // return value // } // return label // }, getDropdownContainer () { if (!this.dropdownContainer) { this.dropdownContainer = document.createElement('div') document.body.appendChild(this.dropdownContainer) } return this.dropdownContainer }, getPlaceholderElement () { const { $props: props, $data: state } = this let hidden = false if (state._inputValue) { hidden = true } if (state._value.length) { hidden = true } if (isCombobox(props) && state._value.length === 1 && !state._value[0]) { hidden = false } const placeholder = props.placeholder if (placeholder) { const p = { on: { mousedown: preventDefaultEvent, click: this.onPlaceholderClick, }, attrs: UNSELECTABLE_ATTRIBUTE, style: { display: hidden ? 'none' : 'block', ...UNSELECTABLE_STYLE, }, class: `${props.prefixCls}-selection__placeholder`, } return (