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));
},
_inputValue(val) {
this.$data._mirrorInputValue = val;
},
},
updated() {
this.$nextTick(() => {
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 = '';
}
}
this.forcePopupAlign();
});
},
beforeUnmount() {
this.clearFocusTime();
this.clearBlurTime();
this.clearComboboxTime();
if (this.dropdownContainer) {
document.body.removeChild(this.dropdownContainer);
this.dropdownContainer = null;
}
},
methods: {
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, composing } = e.target;
const { _inputValue = '' } = this.$data;
if (e.isComposing || composing || _inputValue === val) {
this.setState({
_mirrorInputValue: val,
});
return;
}
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._mirrorInputValue) {
hidden = true;
}
const value = state._value;
if (value.length) {
hidden = true;
}
if (
!state._mirrorInputValue &&
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, _mirrorInputValue } = this.$data;
const attrs = this.$attrs;
const defaultInput = ;
const inputElement = props.getInputElement ? props.getInputElement() : defaultInput;
const inputCls = classnames(inputElement.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),
})}
{_mirrorInputValue}
);
},
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 = (
);
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();
}
}
if (!options.length && notFoundContent) {
empty = true;
const p = {
...UNSELECTABLE_ATTRIBUTE,
key: 'NOT_FOUND',
value: 'NOT_FOUND',
disabled: true,
role: 'option',
style: UNSELECTABLE_STYLE,
};
options = [];
}
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 (
);
});
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 = ;
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,
{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;
}
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(
{this._getInputElement()}
,
);
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;