ant-design-vue/components/vc-select/Select.jsx
2020-10-10 18:16:28 +08:00

1605 lines
49 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { TransitionGroup, withDirectives } from 'vue';
import KeyCode from '../_util/KeyCode';
import PropTypes from '../_util/vue-types';
import classnames from '../_util/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,
findDOMNode,
} 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';
import isValid from '../_util/isValid';
import { getDataAndAriaProps } from '../_util/util';
import antInput from '../_util/antInputDirective';
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.looseBool.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.looseBool.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.looseBool.def(false),
showAction: SelectPropTypes.showAction.def(['click']),
combobox: PropTypes.looseBool.def(false),
tokenSeparators: PropTypes.arrayOf(PropTypes.string).def([]),
autoClearSearchValue: PropTypes.looseBool.def(true),
tabindex: PropTypes.any.def(0),
dropdownRender: PropTypes.func.def(({ menuNode }) => menuNode),
// 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 = findDOMNode(item);
// https://github.com/ant-design/ant-design/issues/20465#issuecomment-569033796
if (!isHidden(menuItemDomNode)) {
this.removeSelected(getValuePropValue(item));
}
return;
}
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: (
<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 { 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 && !isValid(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 <div {...p}>{placeholder}</div>;
}
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 &&
findDOMNode(this.selectTriggerRef.getInnerMenu()) === target) ||
contains(e.target, target))
) {
e.target.focus();
e.preventDefault();
return;
}
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 = (
<input {...(attrs.id !== undefined ? { id: attrs.id } : {})} autocomplete="off" />
);
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 (
<div class={`${props.prefixCls}-search__field__wrap`} onClick={this.inputClick}>
{cloneElement(withDirectives(inputElement, [[antInput]]), {
disabled: props.disabled,
...(inputElement.props || {}),
disabled: props.disabled,
value: inputValue,
class: inputCls,
ref: this.saveInputRef,
onInput: this.onInputChange,
onKeydown: chaining(
this.onInputKeydown,
inputEvents.onKeydown,
this.$attrs.onInputKeydown,
),
onFocus: chaining(this.inputFocus, inputEvents.onFocus),
onBlur: chaining(this.inputBlur, inputEvents.onBlur),
})}
<span ref={this.saveInputMirrorRef} 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, 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 = (
<MenuItem style={UNSELECTABLE_STYLE} {...attrs} value={key} key={key}>
{key}
</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(<MenuItem {...p}>{inputValue}</MenuItem>);
}
}
if (!options.length && notFoundContent) {
empty = true;
const p = {
...UNSELECTABLE_ATTRIBUTE,
key: 'NOT_FOUND',
value: 'NOT_FOUND',
disabled: true,
role: 'option',
style: UNSELECTABLE_STYLE,
};
options = [<MenuItem {...p}>{notFoundContent}</MenuItem>];
}
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 (
<MenuItem key={childValueSub} value={childValueSub} {...subChild.props}>
{...getSlot(subChild)}
</MenuItem>
);
});
sel.push(
<MenuItemGroup key={key} title={label} class={child.props && child.props.class}>
{...innerItems}
</MenuItemGroup>,
);
// Not match
} else {
const innerItems = this.renderFilterOptionsFromChildren(
childChildren,
childrenKeys,
menuItems,
);
if (innerItems.length) {
sel.push(
<MenuItemGroup key={key} title={label} {...child.props}>
{...innerItems}
</MenuItemGroup>,
);
}
}
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 = <MenuItem {...p}>{getSlot(child)}</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 = (
<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;
}
const attrs = {
...UNSELECTABLE_ATTRIBUTE,
role: 'presentation',
title: toTitle(content),
};
maxTagPlaceholderEl = (
<li
style={UNSELECTABLE_STYLE}
{...attrs}
onMousedown={preventDefaultEvent}
class={`${prefixCls}-selection__choice ${prefixCls}-selection__choice__disabled`}
key="maxTagPlaceholder"
>
<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`;
// attrs 放在一起避免动态title混乱问题很奇怪的问题 https://github.com/vueComponent/ant-design-vue/issues/588
const attrs = {
...UNSELECTABLE_ATTRIBUTE,
role: 'presentation',
title: toTitle(title),
};
return (
<li
style={UNSELECTABLE_STYLE}
{...attrs}
onMousedown={preventDefaultEvent}
class={choiceClassName}
key={singleValue || SELECT_EMPTY_VALUE_KEY}
>
<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',
onAfterLeave: this.onChoiceAnimationLeave,
});
innerNode = <TransitionGroup {...transitionProps}>{selectedValueNodes}</TransitionGroup>;
} else {
innerNode = <ul>{selectedValueNodes}</ul>;
}
}
return (
<div class={className} ref={this.saveTopCtrlRef} onClick={this.topCtrlContainerClick}>
{this.getPlaceholderElement()}
{innerNode}
</div>
);
},
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 ? (
<i class={`${prefixCls}-arrow-loading`} />
) : (
<i class={`${prefixCls}-arrow-icon`} />
);
return (
<span
key="arrow"
class={`${prefixCls}-arrow`}
style={UNSELECTABLE_STYLE}
{...UNSELECTABLE_ATTRIBUTE}
onClick={this.onArrowClick}
ref="arrow"
>
{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 = getComponent(this, 'clearIcon');
const clear = (
<span
key="clear"
class={`${prefixCls}-selection__clear`}
onMousedown={preventDefaultEvent}
style={UNSELECTABLE_STYLE}
{...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.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) {
this.inputFocus(e);
},
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 (
<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}
empty={empty}
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={this.$attrs.onPopupScroll}
onPopupFocus={this.onPopupFocus}
onMouseenter={this.$attrs.onMouseenter}
onMouseleave={this.$attrs.onMouseleave}
showAction={props.showAction}
menuItemSelectedIcon={getComponent(this, 'menuItemSelectedIcon')}
ref={this.saveSelectTriggerRef}
dropdownRender={props.dropdownRender}
ariaId={this.$data._ariaId}
>
<div
{...getDataAndAriaProps(this.$attrs)}
ref={chaining(this.saveRootRef, this.saveSelectionRef)}
style={style}
class={classnames(rootCls)}
onMousedown={this.markMouseDown}
onMouseup={this.markMouseLeave}
onMouseout={this.markMouseLeave}
tabindex={props.disabled ? -1 : props.tabindex}
onBlur={this.selectionRefBlur}
onFocus={this.selectionRefFocus}
onClick={this.selectionRefClick}
onKeydown={isMultipleOrTagsOrCombobox(props) ? noop : this.onKeyDown}
>
<div {...selectionProps}>
{this.renderTopControlNode()}
{this.renderClear()}
{this.renderArrow(!!multiple)}
</div>
</div>
</SelectTrigger>
);
},
};
export { Select };
export default Select;