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 OptGroup from './OptGroup'
import { hasProp, getSlotOptions, getPropsData, getValueByProp as getValue, getComponentFromProp, getEvents, getClass, getStyle, getAttrs, getOptionProps, getSlots } 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 ref from 'vue-ref'
import SelectTrigger from './SelectTrigger'
import {
defaultFilterFn,
findFirstMenuItem,
findIndexInValueBySingleValue,
generateUUID,
getLabelFromPropsValue,
getMapKey,
getPropValue,
getValuePropValue,
includesSeparators,
isCombobox,
isMultipleOrTags,
isMultipleOrTagsOrCombobox,
isSingleMode,
preventDefaultEvent,
saveRef,
splitBySeparators,
toArray,
toTitle,
UNSELECTABLE_ATTRIBUTE,
UNSELECTABLE_STYLE,
validateOptionValue,
} from './util'
import { SelectPropTypes } from './PropTypes'
Vue.use(ref, { name: 'ant-ref' })
const SELECT_EMPTY_VALUE_KEY = 'RC_SELECT_EMPTY_VALUE_KEY'
const noop = () => null
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(chaining, args)
}
}
}
}
const Select = {
inheritAttrs: false,
Option,
OptGroup,
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),
tabIndex: PropTypes.any.def(0),
dropdownRender: PropTypes.func.def(menu => menu),
// 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')
this.ariaId = generateUUID()
this._focused = false
this._mouseDown = false
this._options = []
},
data () {
const props = getOptionProps(this)
const optionsInfo = this.getOptionsInfoFromProps(props)
warning(
this.__propsSymbol__,
'Replace slots.default with props.children and pass props.__propsSymbol__'
)
const state = {
_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,
_backfillValue: '',
// a flag for aviod redundant getOptionsInfoFromProps call
_skipBuildOptionsInfo: true,
}
return {
...state,
...this.getDerivedStateFromProps(props, state),
}
},
mounted () {
this.$nextTick(() => {
this.autoFocus && this.focus()
})
},
watch: {
__propsSymbol__ () {
Object.assign(this.$data, this.getDerivedStateFromProps(getOptionProps(this), this.$data))
},
},
updated () {
this.$nextTick(() => {
if (isMultipleOrTags(this.$props)) {
const inputNode = this.getInputDOMNode()
const mirrorNode = this.getInputMirrorDOMNode()
if (inputNode.value && inputNode.value && mirrorNode) {
inputNode.style.width = ''
inputNode.style.width = `${mirrorNode.clientWidth + 10}px`
} else if (inputNode) {
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
},
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
if (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
},
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 { _open: open } = this.$data
const { disabled } = this.$props
if (disabled) {
return
}
const keyCode = event.keyCode
if (open && !this.getInputDOMNode()) {
this.onInputKeydown(event)
} else if (
keyCode === KeyCode.ENTER ||
keyCode === KeyCode.DOWN
) {
// vue state是同步更新,onKeyDown在onMenuSelect后会再次调用,单选时不在调用setOpenState
if (keyCode === KeyCode.ENTER && !isMultipleOrTags(this.$props)) {
this.maybeFocus(true)
} else if (!open) {
this.setOpenState(true)
}
event.preventDefault()
} else if (keyCode === KeyCode.SPACE) {
// Not block space if popup is shown
if (!open) {
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 (this.getRealOpenState(state) && this.selectTriggerRef) {
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 !== undefined &&
lastValue === selectedValue &&
selectedValue !== this.$data._backfillValue) {
this.setOpenState(false, true)
return
}
value = [selectedValue]
this.setOpenState(false, true)
}
this.fireChange(value)
const inputValue = isCombobox(props) ? getPropValue(item, props.optionLabelProp) : ''
if (props.autoClearSearchValue) {
this.setInputValue(inputValue, false)
}
},
onMenuDeselect ({ item, domEvent }) {
if (domEvent.type === 'keydown' && domEvent.keyCode === KeyCode.ENTER) {
this.removeSelected(getValuePropValue(item))
return
}
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.getInputDOMNode() && this.getInputDOMNode()) {
this.getInputDOMNode().focus()
}
},
onPopupFocus () {
// fix ie scrollbar, focus element again
this.maybeFocus(true, true)
},
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()
},
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)
})
},
getValueByLabel (label) {
if (label === undefined) {
return null
}
let value = null
Object.keys(this.$data._optionsInfo).forEach(key => {
const info = this.$data._optionsInfo[key]
const oldLable = toArray(info.label)
if (oldLable && oldLable.join('') === label) {
value = info.value
}
})
return value
},
getVLBySingleValue (value) {
if (this.$props.labelInValue) {
return {
key: value,
label: this.getLabelBySingleValue(value),
}
}
return value
},
getVLForOnChange (vlsS) {
let vls = vlsS
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
},
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
}
const value = state._value
if (value.length) {
hidden = true
}
if (isCombobox(props) && value.length === 1 && (state._value && !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 (
{placeholder}
)
}
return null
},
inputClick (e) {
if (this.$data._open) {
this.clearBlurTime()
e.stopPropagation()
} else {
this._focused = false
}
},
inputBlur (e) {
this.clearBlurTime()
if (this.disabled) {
return
}
this.blurTimer = setTimeout(() => {
this._focused = false
this.updateFocusClassName()
const props = this.$props
let { _value: value } = this.$data
const { _inputValue: inputValue } = this.$data
if (
isSingleMode(props) &&
props.showSearch &&
inputValue &&
props.defaultActiveFirstOption
) {
const options = this._options || []
if (options.length) {
const firstOption = findFirstMenuItem(options)
if (firstOption) {
value = [getValuePropValue(firstOption)]
this.fireChange(value)
}
}
} else if (isMultipleOrTags(props) && inputValue) {
if (this._mouseDown) {
// need update dropmenu when not blur
this.setInputValue('')
} else {
// why not use setState?
this.$data._inputValue = ''
this.$nextTick(() => {
if (this.getInputDOMNode && this.getInputDOMNode()) {
this.getInputDOMNode().value = ''
}
})
}
const tmpValue = this.getValueByInput(inputValue)
if (tmpValue !== undefined) {
value = tmpValue
this.fireChange(value)
}
}
// if click the rest space of Select in multiple mode
if (isMultipleOrTags(props) && this._mouseDown) {
this.maybeFocus(true, true)
this._mouseDown = false
return
}
this.setOpenState(false)
this.$emit('blur', this.getVLForOnChange(value))
}, 10)
},
inputFocus (e) {
if (this.$props.disabled) {
e.preventDefault()
return
}
this.clearBlurTime()
if (
!isMultipleOrTagsOrCombobox(this.$props) &&
e.target === this.getInputDOMNode()
) {
return
}
if (this._focused) {
return
}
this._focused = true
this.updateFocusClassName()
// only effect multiple or tag mode
if (!isMultipleOrTags(this.$props) || !this._mouseDown) {
this.timeoutFocus()
}
},
_getInputElement () {
const props = this.$props
const { _inputValue: inputValue } = this.$data
const attrs = getAttrs(this)
const defaultInput =
const inputElement = props.getInputElement
? props.getInputElement()
: defaultInput
const inputCls = classnames(getClass(inputElement), {
[`${props.prefixCls}-search__field`]: true,
})
const inputEvents = getEvents(inputElement)
// https://github.com/ant-design/ant-design/issues/4992#issuecomment-281542159
// Add space to the end of the inputValue as the width measurement tolerance
inputElement.data = inputElement.data || {}
return (
{cloneElement(inputElement, {
props: {
disabled: props.disabled,
value: inputValue,
},
attrs: {
...(inputElement.data.attrs || {}),
disabled: props.disabled,
value: inputValue,
},
domProps: {
value: inputValue,
},
class: inputCls,
directives: [{
name: 'ant-ref',
value: this.saveInputRef,
}],
on: {
input: this.onInputChange,
keydown: chaining(
this.onInputKeydown,
inputEvents.keydown,
this.$listeners.inputKeydown
),
focus: chaining(
this.inputFocus,
inputEvents.focus,
),
blur: chaining(
this.inputBlur,
inputEvents.blur,
),
},
})}
{inputValue}
)
},
getInputDOMNode () {
return this.topCtrlRef
? this.topCtrlRef.querySelector('input,textarea,div[contentEditable]')
: this.inputRef
},
getInputMirrorDOMNode () {
return this.inputMirrorRef
},
getPopupDOMNode () {
if (this.selectTriggerRef) {
return this.selectTriggerRef.getPopupDOMNode()
}
},
getPopupMenuComponent () {
if (this.selectTriggerRef) {
return this.selectTriggerRef.getInnerMenu()
}
},
setOpenState (open, needFocus) {
const { $props: props, $data: state } = this
if (state._open === open) {
this.maybeFocus(open, !!needFocus)
return
}
this.__emit('dropdownVisibleChange', open)
const nextState = {
_open: open,
_backfillValue: '',
}
// clear search input value when open is false in singleMode.
if (!open && isSingleMode(props) && props.showSearch) {
this.setInputValue('', false)
}
if (!open) {
this.maybeFocus(open, !!needFocus)
}
this.setState(nextState, () => {
if (open) {
this.maybeFocus(open, !!needFocus)
}
})
},
setInputValue (inputValue, fireSearch = true) {
if (inputValue !== this.$data._inputValue) {
this.setState({
_inputValue: inputValue,
}, this.forcePopupAlign)
if (fireSearch) {
this.$emit('search', inputValue)
}
}
},
getValueByInput (str) {
const { multiple, tokenSeparators } = this.$props
let nextValue = this.$data._value
let hasNewValue = false
splitBySeparators(str, tokenSeparators).forEach(label => {
const selectedValue = [label]
if (multiple) {
const value = this.getValueByLabel(label)
if (value && findIndexInValueBySingleValue(nextValue, value) === -1) {
nextValue = nextValue.concat(value)
hasNewValue = true
this.fireSelect(value)
}
} else if (findIndexInValueBySingleValue(nextValue, label) === -1) {
nextValue = nextValue.concat(selectedValue)
hasNewValue = true
this.fireSelect(label)
}
})
return hasNewValue ? nextValue : undefined
},
getRealOpenState (state) {
const { open: _open } = this.$props
if (typeof _open === 'boolean') {
return _open
}
let open = (state || this.$data)._open
const options = this._options || []
if (isMultipleOrTagsOrCombobox(this.$props) || !this.$props.showSearch) {
if (open && !options.length) {
open = false
}
}
return open
},
focus () {
if (isSingleMode(this.$props) && this.selectionRef) {
this.selectionRef.focus()
} else if (this.getInputDOMNode()) {
this.getInputDOMNode().focus()
}
},
blur () {
if (isSingleMode(this.$props) && this.selectionRef) {
this.selectionRef.blur()
} else if (this.getInputDOMNode()) {
this.getInputDOMNode().blur()
}
},
markMouseDown () {
this._mouseDown = true
},
markMouseLeave () {
this._mouseDown = false
},
handleBackfill (item) {
if (!this.backfill || !(isSingleMode(this.$props) || isCombobox(this.$props))) {
return
}
const key = getValuePropValue(item)
if (isCombobox(this.$props)) {
this.setInputValue(key, false)
}
this.setState({
_value: [key],
_backfillValue: key,
})
},
_filterOption (input, child, defaultFilter = defaultFilterFn) {
const { _value: value, _backfillValue: backfillValue } = this.$data
const lastValue = value[value.length - 1]
if (!input || (lastValue && lastValue === backfillValue)) {
return true
}
let filterFn = this.$props.filterOption
if (hasProp(this, 'filterOption')) {
if (filterFn === true) {
filterFn = defaultFilter.bind(this)
}
} else {
filterFn = defaultFilter.bind(this)
}
if (!filterFn) {
return true
} else if (typeof filterFn === 'function') {
return filterFn.call(this, input, child)
} else if (getValue(child, 'disabled')) {
return false
}
return true
},
timeoutFocus () {
if (this.focusTimer) {
this.clearFocusTime()
}
this.focusTimer = window.setTimeout(() => {
// this._focused = true
// this.updateFocusClassName()
this.$emit('focus')
}, 10)
},
clearFocusTime () {
if (this.focusTimer) {
clearTimeout(this.focusTimer)
this.focusTimer = null
}
},
clearBlurTime () {
if (this.blurTimer) {
clearTimeout(this.blurTimer)
this.blurTimer = null
}
},
updateFocusClassName () {
const { rootRef, prefixCls } = this
// avoid setState and its side effect
if (this._focused) {
classes(rootRef).add(`${prefixCls}-focused`)
} else {
classes(rootRef).remove(`${prefixCls}-focused`)
}
},
maybeFocus (open, needFocus) {
if (needFocus || open) {
const input = this.getInputDOMNode()
const { activeElement } = document
if (input && (open || isMultipleOrTagsOrCombobox(this.$props))) {
if (activeElement !== input) {
input.focus()
this._focused = true
}
} else if (activeElement !== this.selectionRef && this.selectionRef) {
this.selectionRef.focus()
this._focused = true
}
}
},
removeSelected (selectedKey, e) {
const props = this.$props
if (props.disabled || this.isChildDisabled(selectedKey)) {
return
}
// Do not trigger Trigger popup
if (e && e.stopPropagation) {
e.stopPropagation()
}
const oldValue = this.$data._value
const value = oldValue.filter(singleValue => {
return singleValue !== selectedKey
})
const canMultiple = isMultipleOrTags(props)
if (canMultiple) {
let event = selectedKey
if (props.labelInValue) {
event = {
key: selectedKey,
label: this.getLabelBySingleValue(selectedKey),
}
}
this.$emit('deselect', event, this.getOptionBySingleValue(selectedKey))
}
this.fireChange(value)
},
openIfHasChildren () {
const { $props } = this
if (($props.children && $props.children.length) || isSingleMode($props)) {
this.setOpenState(true)
}
},
fireSelect (value) {
this.$emit('select', this.getVLBySingleValue(value), this.getOptionBySingleValue(value))
},
fireChange (value) {
if (!hasProp(this, 'value')) {
this.setState({
_value: value,
}, this.forcePopupAlign)
}
const vls = this.getVLForOnChange(value)
const options = this.getOptionsBySingleValue(value)
this._valueOptions = options
this.$emit('change', vls, isMultipleOrTags(this.$props) ? options : options[0])
},
isChildDisabled (key) {
return (this.$props.children || []).some(child => {
const childValue = getValuePropValue(child)
return childValue === key && getValue(child, 'disabled')
})
},
forcePopupAlign () {
if (!this.$data._open) {
return
}
if (this.selectTriggerRef && this.selectTriggerRef.triggerRef) {
this.selectTriggerRef.triggerRef.forcePopupAlign()
}
},
renderFilterOptions () {
const { _inputValue: inputValue } = this.$data
const { children, tags, filterOption, notFoundContent } = this.$props
const menuItems = []
const childrenKeys = []
let options = this.renderFilterOptionsFromChildren(
children,
childrenKeys,
menuItems,
)
if (tags) {
// tags value must be string
let value = this.$data._value
value = value.filter(singleValue => {
return (
childrenKeys.indexOf(singleValue) === -1 &&
(!inputValue || String(singleValue).indexOf(String(inputValue)) > -1)
)
})
value.forEach(singleValue => {
const key = singleValue
const menuItem = (
)
options.push(menuItem)
menuItems.push(menuItem)
})
if (inputValue) {
const notFindInputItem = menuItems.every(option => {
// this.filterOption return true has two meaning,
// 1, some one exists after filtering
// 2, filterOption is set to false
// condition 2 does not mean the option has same value with inputValue
const filterFn = () => getValuePropValue(option) === inputValue
if (filterOption !== false) {
return !this._filterOption(
inputValue,
option,
filterFn
)
}
return !filterFn()
})
if (notFindInputItem) {
const p = {
attrs: UNSELECTABLE_ATTRIBUTE,
key: inputValue,
props: {
value: inputValue,
role: 'option',
},
style: UNSELECTABLE_STYLE,
}
options.unshift(
)
}
}
}
if (!options.length && notFoundContent) {
const p = {
attrs: UNSELECTABLE_ATTRIBUTE,
key: 'NOT_FOUND',
props: {
value: 'NOT_FOUND',
disabled: true,
role: 'option',
},
style: UNSELECTABLE_STYLE,
}
options = [
,
]
}
return options
},
renderFilterOptionsFromChildren (children = [], childrenKeys, menuItems) {
const sel = []
const props = this.$props
const { _inputValue: inputValue } = this.$data
const tags = props.tags
children.forEach(child => {
if (!child.data || child.data.slot !== undefined) {
return
}
if (getSlotOptions(child).isSelectOptGroup) {
let label = getComponentFromProp(child, 'label')
let key = child.key
if (!key && typeof label === 'string') {
key = label
} else if (!label && key) {
label = key
}
const childChildren = getSlots(child).default
// Match option group label
if (inputValue && this._filterOption(inputValue, child)) {
const innerItems = childChildren.map(
(subChild) => {
const childValueSub = getValuePropValue(subChild) || subChild.key
return (
)
},
)
sel.push(
{innerItems}
,
)
// Not match
} else {
const innerItems = this.renderFilterOptionsFromChildren(
childChildren,
childrenKeys,
menuItems,
)
if (innerItems.length) {
sel.push(
{innerItems}
)
}
}
return
}
warning(
getSlotOptions(child).isSelectOption,
'the children of `Select` should be `Select.Option` or `Select.OptGroup`, ' +
`instead of \`${getSlotOptions(child).name ||
getSlotOptions(child)}\`.`
)
const childValue = getValuePropValue(child)
validateOptionValue(childValue, this.$props)
if (this._filterOption(inputValue, child)) {
const p = {
attrs: {
...UNSELECTABLE_ATTRIBUTE,
...getAttrs(child),
},
key: childValue,
props: {
value: childValue,
...getPropsData(child),
role: 'option',
},
style: UNSELECTABLE_STYLE,
on: getEvents(child),
class: getClass(child),
}
const menuItem = (
)
sel.push(menuItem)
menuItems.push(menuItem)
}
if (tags) {
childrenKeys.push(childValue)
}
})
return sel
},
renderTopControlNode () {
const { $props: props } = this
const { _value: value, _inputValue: inputValue, _open: open } = this.$data
const {
choiceTransitionName,
prefixCls,
maxTagTextLength,
maxTagCount,
maxTagPlaceholder,
showSearch,
} = props
const removeIcon = getComponentFromProp(this, 'removeIcon')
const className = `${prefixCls}-selection__rendered`
// search input is inside topControlNode in single, multiple & combobox. 2016/04/13
let innerNode = null
if (isSingleMode(props)) {
let selectedValue = null
if (value.length) {
let showSelectedValue = false
let opacity = 1
if (!showSearch) {
showSelectedValue = true
} else if (open) {
showSelectedValue = !inputValue
if (showSelectedValue) {
opacity = 0.4
}
} else {
showSelectedValue = true
}
const singleValue = value[0]
const { label, title } = this.getOptionInfoBySingleValue(singleValue)
selectedValue = (
{label}
)
}
if (!showSearch) {
innerNode = [selectedValue]
} else {
innerNode = [
selectedValue,
{this._getInputElement()}
,
]
}
} else {
let selectedValueNodes = []
let limitedCountValue = value
let maxTagPlaceholderEl
if (maxTagCount !== undefined && value.length > maxTagCount) {
limitedCountValue = limitedCountValue.slice(0, maxTagCount)
const omittedValues = this.getVLForOnChange(value.slice(maxTagCount, value.length))
let content = `+ ${value.length - maxTagCount} ...`
if (maxTagPlaceholder) {
content = typeof maxTagPlaceholder === 'function'
? maxTagPlaceholder(omittedValues)
: maxTagPlaceholder
}
maxTagPlaceholderEl = (
{content}
)
}
if (isMultipleOrTags(props)) {
selectedValueNodes = limitedCountValue.map(singleValue => {
const info = this.getOptionInfoBySingleValue(singleValue)
let content = info.label
const title = info.title || content
if (
maxTagTextLength &&
typeof content === 'string' &&
content.length > maxTagTextLength
) {
content = `${content.slice(0, maxTagTextLength)}...`
}
const disabled = this.isChildDisabled(singleValue)
const choiceClassName = disabled
? `${prefixCls}-selection__choice ${prefixCls}-selection__choice__disabled`
: `${prefixCls}-selection__choice`
return (
{content}
{disabled ? null : (
{
this.removeSelected(singleValue, event)
}}
class={`${prefixCls}-selection__choice__remove`}
>
{removeIcon || ×}
)}
)
})
}
if (maxTagPlaceholderEl) {
selectedValueNodes.push(maxTagPlaceholderEl)
}
selectedValueNodes.push(
{this._getInputElement()}
)
if (isMultipleOrTags(props) && choiceTransitionName) {
const transitionProps = getTransitionProps(choiceTransitionName, {
tag: 'ul',
afterLeave: this.onChoiceAnimationLeave,
})
innerNode = (
{selectedValueNodes}
)
} else {
innerNode =
}
}
return (
{this.getPlaceholderElement()}
{innerNode}
)
},
renderArrow (multiple) {
const { showArrow, loading, prefixCls } = this.$props
const inputIcon = getComponentFromProp(this, 'inputIcon')
if (!showArrow) {
return null
}
// if loading have loading icon
if (multiple && !loading) {
return null
}
const defaultIcon = loading ? (
) : (
)
return (
{inputIcon || defaultIcon}
)
},
topCtrlContainerClick (e) {
if (this.$data._open && !isSingleMode(this.$props)) {
e.stopPropagation()
}
},
renderClear () {
const { prefixCls, allowClear } = this.$props
const { _value: value, _inputValue: inputValue } = this.$data
const clearIcon = getComponentFromProp(this, 'clearIcon')
const clear = (
{clearIcon || ×}
)
if (!allowClear) {
return null
}
if (isCombobox(this.$props)) {
if (inputValue) {
return clear
}
return null
}
if (inputValue || value.length) {
return clear
}
return null
},
selectionRefClick (e) {
e.stopPropagation()
if (!this.disabled) {
const input = this.getInputDOMNode()
if (this._focused && this.$data._open) {
this._focused = false
this.setOpenState(false, false)
input && input.blur()
} else {
this.clearBlurTime()
this._focused = true
this.setOpenState(true, true)
input && input.focus()
}
}
},
selectionRefFocus (e) {
if (this._focused || this.disabled) {
return
}
this._focused = true
this.updateFocusClassName()
this.$emit('focus')
},
selectionRefBlur (e) {
this._focused = false
this.updateFocusClassName()
this.$emit('blur')
},
},
render () {
const props = this.$props
const multiple = isMultipleOrTags(props)
const state = this.$data
const { disabled, prefixCls } = props
const ctrlNode = this.renderTopControlNode()
const { _open: open, _inputValue: inputValue, _value: value } = this.$data
if (open) {
this._options = this.renderFilterOptions()
}
const realOpen = this.getRealOpenState()
const options = this._options || []
const { $listeners } = this
const { mouseenter = noop, mouseleave = noop, popupScroll = noop } = $listeners
const selectionProps = {
props: {},
attrs: {
role: 'combobox',
'aria-autocomplete': 'list',
'aria-haspopup': 'true',
'aria-expanded': realOpen,
'aria-controls': this.ariaId,
},
on: {
click: this.selectionRefClick,
},
class: `${prefixCls}-selection ${prefixCls}-selection--${multiple ? 'multiple' : 'single'}`,
directives: [{
name: 'ant-ref',
value: this.saveSelectionRef,
}],
key: 'selection',
}
if (!isMultipleOrTagsOrCombobox(props)) {
selectionProps.on.keydown = this.onKeyDown
selectionProps.on.focus = this.selectionRefFocus
selectionProps.on.blur = this.selectionRefBlur
selectionProps.attrs.tabIndex = props.disabled ? -1 : props.tabIndex
}
const rootCls = {
[prefixCls]: true,
[`${prefixCls}-open`]: open,
[`${prefixCls}-focused`]: open || !!this._focused,
[`${prefixCls}-combobox`]: isCombobox(props),
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-enabled`]: !disabled,
[`${prefixCls}-allow-clear`]: !!props.allowClear,
[`${prefixCls}-no-arrow`]: !props.showArrow,
}
return (
{ctrlNode}
{this.renderClear()}
{this.renderArrow(!!multiple)}
)
},
}
export { Select }
export default proxyComponent(Select)