import { TransitionGroup } from 'vue'; 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 Option from './Option'; import OptGroup from './OptGroup'; import { hasProp, getPropsData, getValueByProp as getValue, getComponent, getEvents, getOptionProps, getSlot, } from '../_util/props-util'; import getTransitionProps from '../_util/getTransitionProps'; import { cloneElement } from '../_util/vnode'; import BaseMixin from '../_util/BaseMixin'; 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'; import contains from '../vc-util/Dom/contains'; import { isIE, isEdge } from '../_util/env'; const SELECT_EMPTY_VALUE_KEY = 'RC_SELECT_EMPTY_VALUE_KEY'; const noop = () => null; // Where el is the DOM element you'd like to test for visibility function isHidden(node) { return !node || node.offsetParent === 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], inheritAttrs: false, 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, }, 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._focused = false; this._mouseDown = false; this._options = []; this._empty = false; }, data() { const props = getOptionProps(this); const optionsInfo = this.getOptionsInfoFromProps(props); warning( this.__propsSymbol__, 'Replace slots.default with props.children and pass props.__propsSymbol__', ); if (props.tags && typeof props.filterOption !== 'function') { const isDisabledExist = Object.keys(optionsInfo).some(key => optionsInfo[key].disabled); warning( !isDisabledExist, 'Please avoid setting option to disabled in tags mode since user can always type text as tag.', ); } 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, _ariaId: generateUUID(), }; return { ...state, // _mirrorInputValue: state._inputValue, // https://github.com/vueComponent/ant-design-vue/issues/1458 ...this.getDerivedState(props, state), }; }, mounted() { this.$nextTick(() => { // when defaultOpen is true, we should auto focus search input // https://github.com/ant-design/ant-design/issues/14254 if (this.autofocus || this._open) { this.focus(); } // this.setState({ // _ariaId: generateUUID(), // }); }); }, watch: { __propsSymbol__() { Object.assign(this.$data, this.getDerivedState(getOptionProps(this), this.$data)); }, }, updated() { this.$nextTick(() => { this.updateInputWidth(); this.forcePopupAlign(); }); }, beforeUnmount() { this.clearFocusTime(); this.clearBlurTime(); this.clearComboboxTime(); if (this.dropdownContainer) { document.body.removeChild(this.dropdownContainer); this.dropdownContainer = null; } }, methods: { updateInputWidth() { if (isMultipleOrTags(this.$props)) { const inputNode = this.getInputDOMNode(); const mirrorNode = this.getInputMirrorDOMNode(); if (inputNode && inputNode.value && mirrorNode) { inputNode.style.width = ''; inputNode.style.width = `${mirrorNode.clientWidth + 10}px`; } else if (inputNode) { inputNode.style.width = ''; } } }, getDerivedState(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) { return; } if (child.type?.isSelectOptGroup) { this.getOptionsFromChildren(getSlot(child), 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'), disabled: getValue(option, 'disabled'), }; }); 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(e) { const { value: val } = e.target; const { tokenSeparators } = this.$props; if ( isMultipleOrTags(this.$props) && tokenSeparators.length && includesSeparators(val, tokenSeparators) ) { const nextValue = this.getValueByInput(val); if (nextValue !== undefined) { this.fireChange(nextValue); } this.setOpenState(false, { needFocus: 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 // https://github.com/vueComponent/ant-design-vue/issues/1142 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 { disabled, combobox, defaultActiveFirstOption } = this.$props; if (disabled) { return; } const state = this.$data; const isRealOpen = this.getRealOpenState(state); const keyCode = event.keyCode; if (isMultipleOrTags(this.$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 // https://github.com/ant-design/ant-design/issues/14544 if (isRealOpen || !combobox) { event.preventDefault(); } // Hard close popup to avoid lock of non option in combobox mode if (isRealOpen && combobox && defaultActiveFirstOption === false) { this.comboboxTimer = setTimeout(() => { this.setOpenState(false); }); } } else if (keyCode === KeyCode.ESC) { if (state._open) { this.setOpenState(false); event.preventDefault(); event.stopPropagation(); } return; } if (isRealOpen && 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]; let skipTrigger = false; if (isMultipleOrTags(props)) { if (findIndexInValueBySingleValue(value, selectedValue) !== -1) { skipTrigger = true; } else { value = value.concat([selectedValue]); } } else { if ( !isCombobox(props) && lastValue !== undefined && lastValue === selectedValue && selectedValue !== this.$data._backfillValue ) { this.setOpenState(false, { needFocus: true, fireSearch: false }); skipTrigger = true; } else { value = [selectedValue]; this.setOpenState(false, { needFocus: true, fireSearch: false }); } } if (!skipTrigger) { this.fireChange(value); } if (!skipTrigger) { this.fireSelect(selectedValue); 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) { const menuItemDomNode = item.$el; // https://github.com/ant-design/ant-design/issues/20465#issuecomment-569033796 if (!isHidden(menuItemDomNode)) { this.removeSelected(getValuePropValue(item)); } return; } if (domEvent.type === 'click') { this.removeSelected(getValuePropValue(item)); } if (this.autoClearSearchValue) { this.setInputValue(''); } }, onArrowClick(e) { e.stopPropagation(); e.preventDefault(); this.clearBlurTime(); if (!this.disabled) { this.setOpenState(!this.$data._open, { needFocus: !this.$data._open }); } }, onPlaceholderClick() { 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, { needFocus: 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 valueLabel = getLabelFromPropsValue(this.$props.value, value); const defaultValueLabel = getLabelFromPropsValue(this.$props.defaultValue, value); if (valueLabel !== undefined) { defaultLabel = valueLabel; } else if (defaultValueLabel !== undefined) { defaultLabel = defaultValueLabel; } } 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 { disabled } = info; if (disabled) { return; } 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 = { onMousedown: preventDefaultEvent, onClick: this.onPlaceholderClick, ...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) { const target = e.relatedTarget || document.activeElement; // https://github.com/vueComponent/ant-design-vue/issues/999 // https://github.com/vueComponent/ant-design-vue/issues/1223 if ( (isIE || isEdge) && (e.relatedTarget === this.$refs.arrow || (target && this.selectTriggerRef && this.selectTriggerRef.getInnerMenu() && this.selectTriggerRef.getInnerMenu().$el === target) || contains(e.target, target)) ) { e.target.focus(); e.preventDefault(); return; } this.clearBlurTime(); if (this.disabled) { e.preventDefault(); 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 = ''; 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)); }, 200); }, inputFocus(e) { if (this.$props.disabled) { e.preventDefault(); return; } this.clearBlurTime(); // In IE11, onOuterFocus will be trigger twice when focus input // First one: e.target is div // Second one: e.target is input // other browser only trigger second one // https://github.com/ant-design/ant-design/issues/15942 // Here we ignore the first one when e.target is div const inputNode = this.getInputDOMNode(); if (inputNode && e.target === this.rootRef) { return; } if (!isMultipleOrTagsOrCombobox(this.$props) && e.target === inputNode) { 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 = this.$attrs; const defaultInput = ( ); const inputElement = props.getInputElement ? props.getInputElement() : defaultInput; const inputCls = classnames(inputElement.props && inputElement.props.class, { [`${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 return (
{cloneElement(inputElement, { disabled: props.disabled, ...(inputElement.props || {}), disabled: props.disabled, value: inputValue, class: inputCls, ref: this.saveInputRef, // directives: [ // { // name: 'ant-input', // }, // ], onInput: this.onInputChange, onKeydown: chaining( this.onInputKeydown, inputEvents.onKeydown, this.$attrs.onInputKeydown, ), onFocus: chaining(this.inputFocus, inputEvents.onFocus), onBlur: chaining(this.inputBlur, inputEvents.onBlur), })} {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, config = {}) { const { $props: props, $data: state } = this; const { needFocus, fireSearch } = config; 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('', fireSearch); } 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; } }, clearComboboxTime() { if (this.comboboxTimer) { clearTimeout(this.comboboxTimer); this.comboboxTimer = 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('update:value', vls); 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, notFoundContent } = this.$props; const menuItems = []; const childrenKeys = []; let empty = false; 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) ); }); // sort by length value.sort((val1, val2) => { return val1.length - val2.length; }); value.forEach(singleValue => { const key = singleValue; const attrs = { ...UNSELECTABLE_ATTRIBUTE, role: 'option', }; const menuItem = ( {key} ); options.push(menuItem); menuItems.push(menuItem); }); // ref: https://github.com/ant-design/ant-design/issues/14090 if (inputValue && menuItems.every(option => getValuePropValue(option) !== inputValue)) { const p = { ...UNSELECTABLE_ATTRIBUTE, key: inputValue, value: inputValue, role: 'option', style: UNSELECTABLE_STYLE, }; options.unshift({inputValue}); } } if (!options.length && notFoundContent) { empty = true; const p = { ...UNSELECTABLE_ATTRIBUTE, key: 'NOT_FOUND', value: 'NOT_FOUND', disabled: true, role: 'option', style: UNSELECTABLE_STYLE, }; options = [{notFoundContent}]; } return { empty, 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) { return; } const type = child.type; if (type?.isSelectOptGroup) { let label = getComponent(child, 'label'); let key = child.key; if (!key && typeof label === 'string') { key = label; } else if (!label && key) { label = key; } let childChildren = getComponent(child); childChildren = Array.isArray(childChildren) ? childChildren : [childChildren]; // Match option group label if (inputValue && this._filterOption(inputValue, child)) { const innerItems = childChildren.map(subChild => { const childValueSub = getValuePropValue(subChild) || subChild.key; return ( {...getSlot(subChild)} ); }); sel.push( {...innerItems} , ); // Not match } else { const innerItems = this.renderFilterOptionsFromChildren( childChildren, childrenKeys, menuItems, ); if (innerItems.length) { sel.push( {...innerItems} , ); } } return; } warning( typeof type === 'object' && type.isSelectOption, 'the children of `Select` should be `Select.Option` or `Select.OptGroup`, ', ); const childValue = getValuePropValue(child); validateOptionValue(childValue, this.$props); if (this._filterOption(inputValue, child)) { const p = { ...UNSELECTABLE_ATTRIBUTE, key: childValue, value: childValue, ...getPropsData(child), role: 'option', style: UNSELECTABLE_STYLE, class: child?.class, }; const menuItem = {getSlot(child)}; 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 = getComponent(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, , ]; } } 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; } const attrs = { ...UNSELECTABLE_ATTRIBUTE, role: 'presentation', title: toTitle(content), }; 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`; // attrs 放在一起,避免动态title混乱问题,很奇怪的问题 https://github.com/vueComponent/ant-design-vue/issues/588 const attrs = { ...UNSELECTABLE_ATTRIBUTE, role: 'presentation', title: toTitle(title), }; return (
  • {content}
    {disabled ? null : ( { this.removeSelected(singleValue, event); }} class={`${prefixCls}-selection__choice__remove`} > {removeIcon || ×} )}
  • ); }); } if (maxTagPlaceholderEl) { selectedValueNodes.push(maxTagPlaceholderEl); } selectedValueNodes.push( , ); if (isMultipleOrTags(props) && choiceTransitionName) { const transitionProps = getTransitionProps(choiceTransitionName, { tag: 'ul', onAfterLeave: this.onChoiceAnimationLeave, }); innerNode = {selectedValueNodes}; } else { innerNode = ; } } return (
    {this.getPlaceholderElement()} {innerNode}
    ); }, renderArrow(multiple) { // showArrow : Set to true if not multiple by default but keep set value. const { showArrow = !multiple, loading, prefixCls } = this.$props; const inputIcon = getComponent(this, 'inputIcon'); if (!showArrow && !loading) { return null; } // if loading have loading icon 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 = getComponent(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.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() { if (this.getInputDOMNode() && this.getInputDOMNode()) { this.getInputDOMNode().focus(); } // if (this._focused || this.disabled || isMultipleOrTagsOrCombobox(this.$props)) { // e.preventDefault(); // return; // } // this._focused = true; // this.updateFocusClassName(); // this.__emit('focus'); }, selectionRefBlur(e) { if (isMultipleOrTagsOrCombobox(this.$props)) { e.preventDefault(); return; } this.inputBlur(e); }, }, render() { const props = this.$props; const { class: className, style } = this.$attrs; const multiple = isMultipleOrTags(props); // Default set showArrow to true if not set (not set directly in defaultProps to handle multiple case) const { showArrow = true } = props; const state = this.$data; const { disabled, prefixCls, loading } = props; const { _open: open, _inputValue: inputValue, _value: value } = this.$data; if (open) { const filterOptions = this.renderFilterOptions(); this._empty = filterOptions.empty; this._options = filterOptions.options; } const realOpen = this.getRealOpenState(); const empty = this._empty; const options = this._options || []; const selectionProps = { role: 'combobox', 'aria-autocomplete': 'list', 'aria-haspopup': 'true', 'aria-expanded': realOpen, 'aria-controls': this.$data._ariaId, class: `${prefixCls}-selection ${prefixCls}-selection--${multiple ? 'multiple' : 'single'}`, 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 = { [className]: className, [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`]: !showArrow, [`${prefixCls}-loading`]: !!loading, }; return (
    {this.renderTopControlNode()} {this.renderClear()} {this.renderArrow(!!multiple)}
    ); }, }; export { Select }; export default Select;