You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ant-design-vue/components/vc-select/Select.jsx

1593 lines
47 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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: <Option value={value} key={value}>{value}</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 (
<div {...p}>
{placeholder}
</div>
)
}
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 = <input id={attrs.id} autoComplete='off' />
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 (
<div class={`${props.prefixCls}-search__field__wrap`} onClick={this.inputClick}>
{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,
),
},
})}
<span
{...{ directives: [{
name: 'ant-ref',
value: this.saveInputMirrorRef,
}] }}
// ref='inputMirrorRef'
class={`${props.prefixCls}-search__field__mirror`}
>
{inputValue}&nbsp;
</span>
</div>
)
},
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 = (
<MenuItem
style={UNSELECTABLE_STYLE}
{...{ attrs: UNSELECTABLE_ATTRIBUTE }}
value={key}
key={key}
role='option'
>
{key}
</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(
<MenuItem {...p}>
{inputValue}
</MenuItem>
)
}
}
}
if (!options.length && notFoundContent) {
const p = {
attrs: UNSELECTABLE_ATTRIBUTE,
key: 'NOT_FOUND',
props: {
value: 'NOT_FOUND',
disabled: true,
role: 'option',
},
style: UNSELECTABLE_STYLE,
}
options = [
<MenuItem {...p}>
{notFoundContent}
</MenuItem>,
]
}
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 (
<MenuItem key={childValueSub} value={childValueSub} {...subChild.data}>
{subChild.componentOptions.children}
</MenuItem>
)
},
)
sel.push(
<MenuItemGroup key={key} title={label} class ={getClass(child)}>
{innerItems}
</MenuItemGroup>,
)
// Not match
} else {
const innerItems = this.renderFilterOptionsFromChildren(
childChildren,
childrenKeys,
menuItems,
)
if (innerItems.length) {
sel.push(
<MenuItemGroup key={key} title={label} {...child.data}>
{innerItems}
</MenuItemGroup>
)
}
}
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 = (
<MenuItem {...p}>{child.componentOptions.children}</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 = (
<div
key='value'
class={`${prefixCls}-selection-selected-value`}
title={toTitle(title || label)}
style={{
display: showSelectedValue ? 'block' : 'none',
opacity,
}}
>
{label}
</div>
)
}
if (!showSearch) {
innerNode = [selectedValue]
} else {
innerNode = [
selectedValue,
<div
class={`${prefixCls}-search ${prefixCls}-search--inline`}
key='input'
style={{
display: open ? 'block' : 'none',
}}
>
{this._getInputElement()}
</div>,
]
}
} 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 = (<li
style={UNSELECTABLE_STYLE}
{...{ attrs: UNSELECTABLE_ATTRIBUTE }}
role='presentation'
onMousedown={preventDefaultEvent}
class={`${prefixCls}-selection__choice ${prefixCls}-selection__choice__disabled`}
key='maxTagPlaceholder'
title={toTitle(content)}
>
<div class={`${prefixCls}-selection__choice__content`}>{content}</div>
</li>)
}
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 (
<li
style={UNSELECTABLE_STYLE}
{...{ attrs: UNSELECTABLE_ATTRIBUTE }}
onMousedown={preventDefaultEvent}
class={choiceClassName}
role='presentation'
key={singleValue || SELECT_EMPTY_VALUE_KEY}
title={toTitle(title)}
>
<div class={`${prefixCls}-selection__choice__content`}>
{content}
</div>
{disabled ? null : (
<span
onClick={(event) => {
this.removeSelected(singleValue, event)
}}
class={`${prefixCls}-selection__choice__remove`}
>
{removeIcon || <i class={`${prefixCls}-selection__choice__remove-icon`}>×</i>}
</span>)}
</li>
)
})
}
if (maxTagPlaceholderEl) {
selectedValueNodes.push(maxTagPlaceholderEl)
}
selectedValueNodes.push(
<li
class={`${prefixCls}-search ${prefixCls}-search--inline`}
key='__input'
>
{this._getInputElement()}
</li>
)
if (isMultipleOrTags(props) && choiceTransitionName) {
const transitionProps = getTransitionProps(choiceTransitionName, {
tag: 'ul',
afterLeave: this.onChoiceAnimationLeave,
})
innerNode = (
<transition-group
{...transitionProps}
>
{selectedValueNodes}
</transition-group>
)
} else {
innerNode = <ul>{selectedValueNodes}</ul>
}
}
return (
<div
class={className}
{...{ directives: [{
name: 'ant-ref',
value: this.saveTopCtrlRef,
}] }}
onClick={this.topCtrlContainerClick}
>
{this.getPlaceholderElement()}
{innerNode}
</div>
)
},
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 ? (
<i class={`${prefixCls}-arrow-loading`} />
) : (
<i class={`${prefixCls}-arrow-icon`} />
)
return (
<span
key='arrow'
class={`${prefixCls}-arrow`}
style={UNSELECTABLE_STYLE}
{...{ attrs: UNSELECTABLE_ATTRIBUTE }}
onClick={this.onArrowClick}
>
{inputIcon || defaultIcon}
</span>
)
},
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 = (
<span
key='clear'
class={`${prefixCls}-selection__clear`}
onMousedown={preventDefaultEvent}
style={UNSELECTABLE_STYLE}
{...{ attrs: UNSELECTABLE_ATTRIBUTE }}
onClick={this.onClearSelection}
>{clearIcon || <i class={`${prefixCls}-selection__clear-icon`}>×</i>}</span>
)
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 (
<SelectTrigger
dropdownAlign={props.dropdownAlign}
dropdownClassName={props.dropdownClassName}
dropdownMatchSelectWidth={props.dropdownMatchSelectWidth}
defaultActiveFirstOption={props.defaultActiveFirstOption}
dropdownMenuStyle={props.dropdownMenuStyle}
transitionName={props.transitionName}
animation={props.animation}
prefixCls={props.prefixCls}
dropdownStyle={props.dropdownStyle}
combobox={props.combobox}
showSearch={props.showSearch}
options={options}
multiple={multiple}
disabled={disabled}
visible={realOpen}
inputValue={inputValue}
value={value}
backfillValue={state._backfillValue}
firstActiveValue={props.firstActiveValue}
onDropdownVisibleChange={this.onDropdownVisibleChange}
getPopupContainer={props.getPopupContainer}
onMenuSelect={this.onMenuSelect}
onMenuDeselect={this.onMenuDeselect}
onPopupScroll={popupScroll}
onPopupFocus={this.onPopupFocus}
onMouseenter={mouseenter}
onMouseleave={mouseleave}
showAction={props.showAction}
menuItemSelectedIcon={getComponentFromProp(this, 'menuItemSelectedIcon')}
{...{ directives: [{
name: 'ant-ref',
value: this.saveSelectTriggerRef,
}] }}
dropdownRender={props.dropdownRender}
ariaId={this.ariaId}
>
<div
{...{ directives: [{
name: 'ant-ref',
value: this.saveRootRef,
}] }}
style={getStyle(this)}
class={classnames(rootCls)}
onMousedown={this.markMouseDown}
onMouseup={this.markMouseLeave}
onMouseout={this.markMouseLeave}
// tabindex='-1'
// onBlur={this.onOuterBlur}
// onFocus={this.onOuterFocus}
>
<div {...selectionProps}>
{ctrlNode}
{this.renderClear()}
{this.renderArrow(!!multiple)}
</div>
</div>
</SelectTrigger>
)
},
}
export { Select }
export default proxyComponent(Select)