ant-design-vue
Version:
An enterprise-class UI design language and Vue-based implementation
542 lines (541 loc) • 22.1 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
import _typeof from "@babel/runtime/helpers/esm/typeof";
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue";
/**
* To match accessibility requirement, we always provide an input in the component.
* Other element will not set `tabindex` to avoid `onBlur` sequence problem.
* For focused select, we set `aria-live="polite"` to update the accessibility content.
*
* ref:
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
*
* New api:
* - listHeight
* - listItemHeight
* - component
*
* Remove deprecated api:
* - multiple
* - tags
* - combobox
* - firstActiveValue
* - dropdownMenuStyle
* - openClassName (Not list in api)
*
* Update:
* - `backfill` only support `combobox` mode
* - `combobox` mode not support `labelInValue` since it's meaningless
* - `getInputElement` only support `combobox` mode
* - `onChange` return OptionData instead of ReactNode
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
* - `combobox` mode not support `optionLabelProp`
*/
import BaseSelect, { baseSelectPropsWithoutPrivate, isMultiple } from './BaseSelect';
import OptionList from './OptionList';
import useOptions from './hooks/useOptions';
import { useProvideSelectProps } from './SelectContext';
import useId from './hooks/useId';
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
import warningProps from './utils/warningPropsUtil';
import { toArray } from './utils/commonUtil';
import useFilterOptions from './hooks/useFilterOptions';
import useCache from './hooks/useCache';
import { computed, defineComponent, ref, shallowRef, toRef, watchEffect } from 'vue';
import PropTypes from '../_util/vue-types';
import { initDefaultProps } from '../_util/props-util';
import useMergedState from '../_util/hooks/useMergedState';
import useState from '../_util/hooks/useState';
import { toReactive } from '../_util/toReactive';
import omit from '../_util/omit';
var OMIT_DOM_PROPS = ['inputValue'];
export function selectProps() {
return _objectSpread(_objectSpread({}, baseSelectPropsWithoutPrivate()), {}, {
prefixCls: String,
id: String,
backfill: {
type: Boolean,
default: undefined
},
// >>> Field Names
fieldNames: Object,
// >>> Search
/** @deprecated Use `searchValue` instead */
inputValue: String,
searchValue: String,
onSearch: Function,
autoClearSearchValue: {
type: Boolean,
default: undefined
},
// >>> Select
onSelect: Function,
onDeselect: Function,
// >>> Options
/**
* In Select, `false` means do nothing.
* In TreeSelect, `false` will highlight match item.
* It's by design.
*/
filterOption: {
type: [Boolean, Function],
default: undefined
},
filterSort: Function,
optionFilterProp: String,
optionLabelProp: String,
options: Array,
defaultActiveFirstOption: {
type: Boolean,
default: undefined
},
virtual: {
type: Boolean,
default: undefined
},
listHeight: Number,
listItemHeight: Number,
// >>> Icon
menuItemSelectedIcon: PropTypes.any,
mode: String,
labelInValue: {
type: Boolean,
default: undefined
},
value: PropTypes.any,
defaultValue: PropTypes.any,
onChange: Function,
children: Array
});
}
function isRawValue(value) {
return !value || _typeof(value) !== 'object';
}
export default defineComponent({
compatConfig: {
MODE: 3
},
name: 'Select',
inheritAttrs: false,
props: initDefaultProps(selectProps(), {
prefixCls: 'vc-select',
autoClearSearchValue: true,
listHeight: 200,
listItemHeight: 20,
dropdownMatchSelectWidth: true
}),
setup: function setup(props, _ref) {
var expose = _ref.expose,
attrs = _ref.attrs,
slots = _ref.slots;
var mergedId = useId(toRef(props, 'id'));
var multiple = computed(function () {
return isMultiple(props.mode);
});
var childrenAsData = computed(function () {
return !!(!props.options && props.children);
});
var mergedFilterOption = computed(function () {
if (props.filterOption === undefined && props.mode === 'combobox') {
return false;
}
return props.filterOption;
});
// ========================= FieldNames =========================
var mergedFieldNames = computed(function () {
return fillFieldNames(props.fieldNames, childrenAsData.value);
});
// =========================== Search ===========================
var _useMergedState = useMergedState('', {
value: computed(function () {
return props.searchValue !== undefined ? props.searchValue : props.inputValue;
}),
postState: function postState(search) {
return search || '';
}
}),
_useMergedState2 = _slicedToArray(_useMergedState, 2),
mergedSearchValue = _useMergedState2[0],
setSearchValue = _useMergedState2[1];
// =========================== Option ===========================
var parsedOptions = useOptions(toRef(props, 'options'), toRef(props, 'children'), mergedFieldNames);
var valueOptions = parsedOptions.valueOptions,
labelOptions = parsedOptions.labelOptions,
mergedOptions = parsedOptions.options;
// ========================= Wrap Value =========================
var convert2LabelValues = function convert2LabelValues(draftValues) {
// Convert to array
var valueList = toArray(draftValues);
// Convert to labelInValue type
return valueList.map(function (val) {
var rawValue;
var rawLabel;
var rawKey;
var rawDisabled;
// Fill label & value
if (isRawValue(val)) {
rawValue = val;
} else {
var _val$value;
rawKey = val.key;
rawLabel = val.label;
rawValue = (_val$value = val.value) !== null && _val$value !== void 0 ? _val$value : rawKey;
}
var option = valueOptions.value.get(rawValue);
if (option) {
var _option$key;
// Fill missing props
if (rawLabel === undefined) rawLabel = option === null || option === void 0 ? void 0 : option[props.optionLabelProp || mergedFieldNames.value.label];
if (rawKey === undefined) rawKey = (_option$key = option === null || option === void 0 ? void 0 : option.key) !== null && _option$key !== void 0 ? _option$key : rawValue;
rawDisabled = option === null || option === void 0 ? void 0 : option.disabled;
// Warning if label not same as provided
// if (process.env.NODE_ENV !== 'production' && !isRawValue(val)) {
// const optionLabel = option?.[mergedFieldNames.value.label];
// if (optionLabel !== undefined && optionLabel !== rawLabel) {
// warning(false, '`label` of `value` is not same as `label` in Select options.');
// }
// }
}
return {
label: rawLabel,
value: rawValue,
key: rawKey,
disabled: rawDisabled,
option: option
};
});
};
// =========================== Values ===========================
var _useMergedState3 = useMergedState(props.defaultValue, {
value: toRef(props, 'value')
}),
_useMergedState4 = _slicedToArray(_useMergedState3, 2),
internalValue = _useMergedState4[0],
setInternalValue = _useMergedState4[1];
// Merged value with LabelValueType
var rawLabeledValues = computed(function () {
var _values$;
var values = convert2LabelValues(internalValue.value);
// combobox no need save value when it's empty
if (props.mode === 'combobox' && !((_values$ = values[0]) !== null && _values$ !== void 0 && _values$.value)) {
return [];
}
return values;
});
// Fill label with cache to avoid option remove
var _useCache = useCache(rawLabeledValues, valueOptions),
_useCache2 = _slicedToArray(_useCache, 2),
mergedValues = _useCache2[0],
getMixedOption = _useCache2[1];
var displayValues = computed(function () {
// `null` need show as placeholder instead
// https://github.com/ant-design/ant-design/issues/25057
if (!props.mode && mergedValues.value.length === 1) {
var firstValue = mergedValues.value[0];
if (firstValue.value === null && (firstValue.label === null || firstValue.label === undefined)) {
return [];
}
}
return mergedValues.value.map(function (item) {
var _ref2;
return _objectSpread(_objectSpread({}, item), {}, {
label: (_ref2 = typeof item.label === 'function' ? item.label() : item.label) !== null && _ref2 !== void 0 ? _ref2 : item.value
});
});
});
/** Convert `displayValues` to raw value type set */
var rawValues = computed(function () {
return new Set(mergedValues.value.map(function (val) {
return val.value;
}));
});
watchEffect(function () {
if (props.mode === 'combobox') {
var _mergedValues$value$;
var strValue = (_mergedValues$value$ = mergedValues.value[0]) === null || _mergedValues$value$ === void 0 ? void 0 : _mergedValues$value$.value;
if (strValue !== undefined && strValue !== null) {
setSearchValue(String(strValue));
}
}
}, {
flush: 'post'
});
// ======================= Display Option =======================
// Create a placeholder item if not exist in `options`
var createTagOption = function createTagOption(val, label) {
var _ref3;
var mergedLabel = label !== null && label !== void 0 ? label : val;
return _ref3 = {}, _defineProperty(_ref3, mergedFieldNames.value.value, val), _defineProperty(_ref3, mergedFieldNames.value.label, mergedLabel), _ref3;
};
// Fill tag as option if mode is `tags`
var filledTagOptions = shallowRef();
watchEffect(function () {
if (props.mode !== 'tags') {
filledTagOptions.value = mergedOptions.value;
return;
}
// >>> Tag mode
var cloneOptions = mergedOptions.value.slice();
// Check if value exist in options (include new patch item)
var existOptions = function existOptions(val) {
return valueOptions.value.has(val);
};
// Fill current value as option
_toConsumableArray(mergedValues.value).sort(function (a, b) {
return a.value < b.value ? -1 : 1;
}).forEach(function (item) {
var val = item.value;
if (!existOptions(val)) {
cloneOptions.push(createTagOption(val, item.label));
}
});
filledTagOptions.value = cloneOptions;
});
var filteredOptions = useFilterOptions(filledTagOptions, mergedFieldNames, mergedSearchValue, mergedFilterOption, toRef(props, 'optionFilterProp'));
// Fill options with search value if needed
var filledSearchOptions = computed(function () {
if (props.mode !== 'tags' || !mergedSearchValue.value || filteredOptions.value.some(function (item) {
return item[props.optionFilterProp || 'value'] === mergedSearchValue.value;
})) {
return filteredOptions.value;
}
// Fill search value as option
return [createTagOption(mergedSearchValue.value)].concat(_toConsumableArray(filteredOptions.value));
});
var orderedFilteredOptions = computed(function () {
if (!props.filterSort) {
return filledSearchOptions.value;
}
return _toConsumableArray(filledSearchOptions.value).sort(function (a, b) {
return props.filterSort(a, b);
});
});
var displayOptions = computed(function () {
return flattenOptions(orderedFilteredOptions.value, {
fieldNames: mergedFieldNames.value,
childrenAsData: childrenAsData.value
});
});
// =========================== Change ===========================
var triggerChange = function triggerChange(values) {
var labeledValues = convert2LabelValues(values);
setInternalValue(labeledValues);
if (props.onChange && (
// Trigger event only when value changed
labeledValues.length !== mergedValues.value.length || labeledValues.some(function (newVal, index) {
var _mergedValues$value$i;
return ((_mergedValues$value$i = mergedValues.value[index]) === null || _mergedValues$value$i === void 0 ? void 0 : _mergedValues$value$i.value) !== (newVal === null || newVal === void 0 ? void 0 : newVal.value);
}))) {
var returnValues = props.labelInValue ? labeledValues.map(function (v) {
return _objectSpread(_objectSpread({}, v), {}, {
originLabel: v.label,
label: typeof v.label === 'function' ? v.label() : v.label
});
}) : labeledValues.map(function (v) {
return v.value;
});
var returnOptions = labeledValues.map(function (v) {
return injectPropsWithOption(getMixedOption(v.value));
});
props.onChange(
// Value
multiple.value ? returnValues : returnValues[0],
// Option
multiple.value ? returnOptions : returnOptions[0]);
}
};
// ======================= Accessibility ========================
var _useState = useState(null),
_useState2 = _slicedToArray(_useState, 2),
activeValue = _useState2[0],
setActiveValue = _useState2[1];
var _useState3 = useState(0),
_useState4 = _slicedToArray(_useState3, 2),
accessibilityIndex = _useState4[0],
setAccessibilityIndex = _useState4[1];
var mergedDefaultActiveFirstOption = computed(function () {
return props.defaultActiveFirstOption !== undefined ? props.defaultActiveFirstOption : props.mode !== 'combobox';
});
var onActiveValue = function onActiveValue(active, index) {
var _ref4 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
_ref4$source = _ref4.source,
source = _ref4$source === void 0 ? 'keyboard' : _ref4$source;
setAccessibilityIndex(index);
if (props.backfill && props.mode === 'combobox' && active !== null && source === 'keyboard') {
setActiveValue(String(active));
}
};
// ========================= OptionList =========================
var triggerSelect = function triggerSelect(val, selected) {
var getSelectEnt = function getSelectEnt() {
var _option$key2;
var option = getMixedOption(val);
var originLabel = option === null || option === void 0 ? void 0 : option[mergedFieldNames.value.label];
return [props.labelInValue ? {
label: typeof originLabel === 'function' ? originLabel() : originLabel,
originLabel: originLabel,
value: val,
key: (_option$key2 = option === null || option === void 0 ? void 0 : option.key) !== null && _option$key2 !== void 0 ? _option$key2 : val
} : val, injectPropsWithOption(option)];
};
if (selected && props.onSelect) {
var _getSelectEnt = getSelectEnt(),
_getSelectEnt2 = _slicedToArray(_getSelectEnt, 2),
wrappedValue = _getSelectEnt2[0],
option = _getSelectEnt2[1];
props.onSelect(wrappedValue, option);
} else if (!selected && props.onDeselect) {
var _getSelectEnt3 = getSelectEnt(),
_getSelectEnt4 = _slicedToArray(_getSelectEnt3, 2),
_wrappedValue = _getSelectEnt4[0],
_option = _getSelectEnt4[1];
props.onDeselect(_wrappedValue, _option);
}
};
// Used for OptionList selection
var onInternalSelect = function onInternalSelect(val, info) {
var cloneValues;
// Single mode always trigger select only with option list
var mergedSelect = multiple.value ? info.selected : true;
if (mergedSelect) {
cloneValues = multiple.value ? [].concat(_toConsumableArray(mergedValues.value), [val]) : [val];
} else {
cloneValues = mergedValues.value.filter(function (v) {
return v.value !== val;
});
}
triggerChange(cloneValues);
triggerSelect(val, mergedSelect);
// Clean search value if single or configured
if (props.mode === 'combobox') {
// setSearchValue(String(val));
setActiveValue('');
} else if (!multiple.value || props.autoClearSearchValue) {
setSearchValue('');
setActiveValue('');
}
};
// ======================= Display Change =======================
// BaseSelect display values change
var onDisplayValuesChange = function onDisplayValuesChange(nextValues, info) {
triggerChange(nextValues);
if (info.type === 'remove' || info.type === 'clear') {
info.values.forEach(function (item) {
triggerSelect(item.value, false);
});
}
};
// =========================== Search ===========================
var onInternalSearch = function onInternalSearch(searchText, info) {
setSearchValue(searchText);
setActiveValue(null);
// [Submit] Tag mode should flush input
if (info.source === 'submit') {
var formatted = (searchText || '').trim();
// prevent empty tags from appearing when you click the Enter button
if (formatted) {
var newRawValues = Array.from(new Set([].concat(_toConsumableArray(rawValues.value), [formatted])));
triggerChange(newRawValues);
triggerSelect(formatted, true);
setSearchValue('');
}
return;
}
if (info.source !== 'blur') {
var _props$onSearch;
if (props.mode === 'combobox') {
triggerChange(searchText);
}
(_props$onSearch = props.onSearch) === null || _props$onSearch === void 0 ? void 0 : _props$onSearch.call(props, searchText);
}
};
var onInternalSearchSplit = function onInternalSearchSplit(words) {
var patchValues = words;
if (props.mode !== 'tags') {
patchValues = words.map(function (word) {
var opt = labelOptions.value.get(word);
return opt === null || opt === void 0 ? void 0 : opt.value;
}).filter(function (val) {
return val !== undefined;
});
}
var newRawValues = Array.from(new Set([].concat(_toConsumableArray(rawValues.value), _toConsumableArray(patchValues))));
triggerChange(newRawValues);
newRawValues.forEach(function (newRawValue) {
triggerSelect(newRawValue, true);
});
};
var realVirtual = computed(function () {
return props.virtual !== false && props.dropdownMatchSelectWidth !== false;
});
useProvideSelectProps(toReactive(_objectSpread(_objectSpread({}, parsedOptions), {}, {
flattenOptions: displayOptions,
onActiveValue: onActiveValue,
defaultActiveFirstOption: mergedDefaultActiveFirstOption,
onSelect: onInternalSelect,
menuItemSelectedIcon: toRef(props, 'menuItemSelectedIcon'),
rawValues: rawValues,
fieldNames: mergedFieldNames,
virtual: realVirtual,
listHeight: toRef(props, 'listHeight'),
listItemHeight: toRef(props, 'listItemHeight'),
childrenAsData: childrenAsData
})));
// ========================== Warning ===========================
if (process.env.NODE_ENV !== 'production') {
watchEffect(function () {
warningProps(props);
}, {
flush: 'post'
});
}
var selectRef = ref();
expose({
focus: function focus() {
var _selectRef$value;
(_selectRef$value = selectRef.value) === null || _selectRef$value === void 0 ? void 0 : _selectRef$value.focus();
},
blur: function blur() {
var _selectRef$value2;
(_selectRef$value2 = selectRef.value) === null || _selectRef$value2 === void 0 ? void 0 : _selectRef$value2.blur();
},
scrollTo: function scrollTo(arg) {
var _selectRef$value3;
(_selectRef$value3 = selectRef.value) === null || _selectRef$value3 === void 0 ? void 0 : _selectRef$value3.scrollTo(arg);
}
});
var pickProps = computed(function () {
return omit(props, ['id', 'mode', 'prefixCls', 'backfill', 'fieldNames',
// Search
'inputValue', 'searchValue', 'onSearch', 'autoClearSearchValue',
// Select
'onSelect', 'onDeselect', 'dropdownMatchSelectWidth',
// Options
'filterOption', 'filterSort', 'optionFilterProp', 'optionLabelProp', 'options', 'children', 'defaultActiveFirstOption', 'menuItemSelectedIcon', 'virtual', 'listHeight', 'listItemHeight',
// Value
'value', 'defaultValue', 'labelInValue', 'onChange']);
});
return function () {
return _createVNode(BaseSelect, _objectSpread(_objectSpread(_objectSpread({}, pickProps.value), attrs), {}, {
"id": mergedId,
"prefixCls": props.prefixCls,
"ref": selectRef,
"omitDomProps": OMIT_DOM_PROPS,
"mode": props.mode,
"displayValues": displayValues.value,
"onDisplayValuesChange": onDisplayValuesChange,
"searchValue": mergedSearchValue.value,
"onSearch": onInternalSearch,
"onSearchSplit": onInternalSearchSplit,
"dropdownMatchSelectWidth": props.dropdownMatchSelectWidth,
"OptionList": OptionList,
"emptyOptions": !displayOptions.value.length,
"activeValue": activeValue.value,
"activeDescendantId": "".concat(mergedId, "_list_").concat(accessibilityIndex.value)
}), slots);
};
}
});