refactor: select
parent
1c508d61fb
commit
d027f286e3
|
@ -31,11 +31,6 @@ The label of the selected item will be packed as an object for passing to the on
|
||||||
import type { SelectProps } from 'ant-design-vue';
|
import type { SelectProps } from 'ant-design-vue';
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
|
|
||||||
interface Value {
|
|
||||||
value?: string;
|
|
||||||
label?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const options = ref<SelectProps['options']>([
|
const options = ref<SelectProps['options']>([
|
||||||
|
@ -48,11 +43,11 @@ export default defineComponent({
|
||||||
label: 'Lucy (101)',
|
label: 'Lucy (101)',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const handleChange = (value: Value) => {
|
const handleChange: SelectProps['onChange'] = value => {
|
||||||
console.log(value); // { key: "lucy", label: "Lucy (101)" }
|
console.log(value); // { key: "lucy", label: "Lucy (101)" }
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
value: ref<Value>({ value: 'lucy' }),
|
value: ref({ value: 'lucy' }),
|
||||||
options,
|
options,
|
||||||
handleChange,
|
handleChange,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,10 +10,13 @@ title:
|
||||||
|
|
||||||
ไฝฟ็จ `optionLabelProp` ๆๅฎๅๅกซๅฐ้ๆฉๆก็ `Option` ๅฑๆงใ
|
ไฝฟ็จ `optionLabelProp` ๆๅฎๅๅกซๅฐ้ๆฉๆก็ `Option` ๅฑๆงใ
|
||||||
|
|
||||||
|
ๆ่
ไฝฟ็จ `tagRender` ๆๆงฝ่ชๅฎไนๆธฒๆ่็น
|
||||||
|
|
||||||
## en-US
|
## en-US
|
||||||
|
|
||||||
Spacified the prop name of Option which will be rendered in select box.
|
Spacified the prop name of Option which will be rendered in select box.
|
||||||
|
|
||||||
|
or use `tagRender` slot for custom rendering of tags.
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -23,7 +26,7 @@ Spacified the prop name of Option which will be rendered in select box.
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="select one country"
|
placeholder="select one country"
|
||||||
option-label-prop="label"
|
option-label-prop="children"
|
||||||
>
|
>
|
||||||
<a-select-option value="china" label="China">
|
<a-select-option value="china" label="China">
|
||||||
<span role="img" aria-label="China">๐จ๐ณ</span>
|
<span role="img" aria-label="China">๐จ๐ณ</span>
|
||||||
|
@ -58,6 +61,29 @@ Spacified the prop name of Option which will be rendered in select box.
|
||||||
</a-select>
|
</a-select>
|
||||||
<span>Note: v-slot:option support from v2.2.5</span>
|
<span>Note: v-slot:option support from v2.2.5</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a-space direction="vertical" style="width: 100%">
|
||||||
|
<a-select
|
||||||
|
v-model:value="value"
|
||||||
|
mode="multiple"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="select one country"
|
||||||
|
:options="options"
|
||||||
|
>
|
||||||
|
<template #option="{ value: val, label, icon }">
|
||||||
|
<span role="img" :aria-label="val">{{ icon }}</span>
|
||||||
|
{{ label }}
|
||||||
|
</template>
|
||||||
|
<template #tagRender="{ value: val, label, closable, onClose, option }">
|
||||||
|
<a-tag :closable="closable" style="margin-right: 3px" @close="onClose">
|
||||||
|
{{ label }}
|
||||||
|
<span role="img" :aria-label="val">{{ option.icon }}</span>
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
<span>Note: v-slot:tagRender support from v3.0</span>
|
||||||
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, watch } from 'vue';
|
import { defineComponent, ref, watch } from 'vue';
|
||||||
|
@ -91,7 +117,6 @@ export default defineComponent({
|
||||||
watch(value, val => {
|
watch(value, val => {
|
||||||
console.log(`selected:`, val);
|
console.log(`selected:`, val);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
|
|
|
@ -19,7 +19,7 @@ The height of the input field for the select defaults to 32px. If size is set to
|
||||||
<template>
|
<template>
|
||||||
<a-radio-group v-model:value="size">
|
<a-radio-group v-model:value="size">
|
||||||
<a-radio-button value="large">Large</a-radio-button>
|
<a-radio-button value="large">Large</a-radio-button>
|
||||||
<a-radio-button value="default">Default</a-radio-button>
|
<a-radio-button value="middle">Middle</a-radio-button>
|
||||||
<a-radio-button value="small">Small</a-radio-button>
|
<a-radio-button value="small">Small</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<br />
|
<br />
|
||||||
|
@ -51,6 +51,7 @@ The height of the input field for the select defaults to 32px. If size is set to
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { SizeType } from 'ant-design-vue/es/config-provider';
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -60,7 +61,7 @@ export default defineComponent({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
popupScroll,
|
popupScroll,
|
||||||
size: ref('default'),
|
size: ref<SizeType>('middle'),
|
||||||
value1: ref('a1'),
|
value1: ref('a1'),
|
||||||
value2: ref(['a1', 'b2']),
|
value2: ref(['a1', 'b2']),
|
||||||
value3: ref(['a1', 'b2']),
|
value3: ref(['a1', 'b2']),
|
||||||
|
|
|
@ -55,6 +55,7 @@ Select component to select value from options.
|
||||||
| clearIcon | The custom clear icon | VNode \| slot | - | |
|
| clearIcon | The custom clear icon | VNode \| slot | - | |
|
||||||
| menuItemSelectedIcon | The custom menuItemSelected icon | VNode \| slot | - | |
|
| menuItemSelectedIcon | The custom menuItemSelected icon | VNode \| slot | - | |
|
||||||
| tokenSeparators | Separator used to tokenize on tag/multiple mode | string\[] | | |
|
| tokenSeparators | Separator used to tokenize on tag/multiple mode | string\[] | | |
|
||||||
|
| tagRender | Customize tag render, only applies when `mode` is set to `multiple` or `tags` | slot \| (props) => any | - | |
|
||||||
| value(v-model) | Current selected option. | string\|number\|string\[]\|number\[] | - | |
|
| value(v-model) | Current selected option. | string\|number\|string\[]\|number\[] | - | |
|
||||||
| options | Data of the selectOption, manual construction work is no longer needed if this property has been set | array<{value, label, [disabled, key, title]}> | \[] | |
|
| options | Data of the selectOption, manual construction work is no longer needed if this property has been set | array<{value, label, [disabled, key, title]}> | \[] | |
|
||||||
| option | custom render option by slot | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
|
| option | custom render option by slot | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
|
import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, ref } from 'vue';
|
||||||
import classNames from '../_util/classNames';
|
import classNames from '../_util/classNames';
|
||||||
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select';
|
import type { BaseSelectRef } from '../vc-select2';
|
||||||
import type { OptionProps as OptionPropsType } from '../vc-select/Option';
|
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select2';
|
||||||
|
import type { BaseOptionType, DefaultOptionType } from '../vc-select2/Select';
|
||||||
|
import type { OptionProps } from '../vc-select2/Option';
|
||||||
import getIcons from './utils/iconUtil';
|
import getIcons from './utils/iconUtil';
|
||||||
import PropTypes from '../_util/vue-types';
|
import PropTypes from '../_util/vue-types';
|
||||||
import { tuple } from '../_util/type';
|
|
||||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||||
import omit from '../_util/omit';
|
import omit from '../_util/omit';
|
||||||
import { useInjectFormItemContext } from '../form/FormItemContext';
|
import { useInjectFormItemContext } from '../form/FormItemContext';
|
||||||
import { getTransitionName } from '../_util/transition';
|
import { getTransitionName } from '../_util/transition';
|
||||||
|
import type { SizeType } from '../config-provider';
|
||||||
|
import { initDefaultProps } from '../_util/props-util';
|
||||||
|
|
||||||
type RawValue = string | number;
|
type RawValue = string | number;
|
||||||
|
|
||||||
export type OptionProps = OptionPropsType;
|
|
||||||
|
|
||||||
export type OptionType = typeof Option;
|
export type OptionType = typeof Option;
|
||||||
|
export type { OptionProps, BaseSelectRef as RefSelectProps, BaseOptionType, DefaultOptionType };
|
||||||
|
|
||||||
export interface LabeledValue {
|
export interface LabeledValue {
|
||||||
key?: string;
|
key?: string;
|
||||||
value: RawValue;
|
value: RawValue;
|
||||||
label: any;
|
label?: any;
|
||||||
}
|
}
|
||||||
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
|
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
|
||||||
|
|
||||||
|
@ -35,23 +37,28 @@ export const selectProps = () => ({
|
||||||
notFoundContent: PropTypes.any,
|
notFoundContent: PropTypes.any,
|
||||||
suffixIcon: PropTypes.any,
|
suffixIcon: PropTypes.any,
|
||||||
itemIcon: PropTypes.any,
|
itemIcon: PropTypes.any,
|
||||||
size: PropTypes.oneOf(tuple('small', 'middle', 'large', 'default')),
|
size: String as PropType<SizeType>,
|
||||||
mode: PropTypes.oneOf(tuple('multiple', 'tags', 'SECRET_COMBOBOX_MODE_DO_NOT_USE')),
|
mode: String as PropType<'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE'>,
|
||||||
bordered: PropTypes.looseBool.def(true),
|
bordered: { type: Boolean, default: true },
|
||||||
transitionName: PropTypes.string,
|
transitionName: String,
|
||||||
choiceTransitionName: PropTypes.string.def(''),
|
choiceTransitionName: { type: String, default: '' },
|
||||||
|
'onUpdate:value': Function as PropType<(val: SelectValue) => void>,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
||||||
|
|
||||||
|
const SECRET_COMBOBOX_MODE_DO_NOT_USE = 'SECRET_COMBOBOX_MODE_DO_NOT_USE';
|
||||||
const Select = defineComponent({
|
const Select = defineComponent({
|
||||||
name: 'ASelect',
|
name: 'ASelect',
|
||||||
Option,
|
Option,
|
||||||
OptGroup,
|
OptGroup,
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: selectProps(),
|
props: initDefaultProps(selectProps(), {
|
||||||
SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE',
|
listHeight: 256,
|
||||||
emits: ['change', 'update:value', 'blur'],
|
listItemHeight: 24,
|
||||||
|
}),
|
||||||
|
SECRET_COMBOBOX_MODE_DO_NOT_USE,
|
||||||
|
// emits: ['change', 'update:value', 'blur'],
|
||||||
slots: [
|
slots: [
|
||||||
'notFoundContent',
|
'notFoundContent',
|
||||||
'suffixIcon',
|
'suffixIcon',
|
||||||
|
@ -61,20 +68,21 @@ const Select = defineComponent({
|
||||||
'dropdownRender',
|
'dropdownRender',
|
||||||
'option',
|
'option',
|
||||||
'placeholder',
|
'placeholder',
|
||||||
|
'tagRender',
|
||||||
],
|
],
|
||||||
setup(props, { attrs, emit, slots, expose }) {
|
setup(props, { attrs, emit, slots, expose }) {
|
||||||
const selectRef = ref();
|
const selectRef = ref<BaseSelectRef>();
|
||||||
const formItemContext = useInjectFormItemContext();
|
const formItemContext = useInjectFormItemContext();
|
||||||
const focus = () => {
|
const focus = () => {
|
||||||
if (selectRef.value) {
|
selectRef.value?.focus();
|
||||||
selectRef.value.focus();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const blur = () => {
|
const blur = () => {
|
||||||
if (selectRef.value) {
|
selectRef.value?.blur();
|
||||||
selectRef.value.blur();
|
};
|
||||||
}
|
|
||||||
|
const scrollTo: BaseSelectRef['scrollTo'] = arg => {
|
||||||
|
selectRef.value?.scrollTo(arg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mode = computed(() => {
|
const mode = computed(() => {
|
||||||
|
@ -84,7 +92,7 @@ const Select = defineComponent({
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === Select.SECRET_COMBOBOX_MODE_DO_NOT_USE) {
|
if (mode === SECRET_COMBOBOX_MODE_DO_NOT_USE) {
|
||||||
return 'combobox';
|
return 'combobox';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,19 +111,21 @@ const Select = defineComponent({
|
||||||
[`${prefixCls.value}-borderless`]: !props.bordered,
|
[`${prefixCls.value}-borderless`]: !props.bordered,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const triggerChange = (...args: any[]) => {
|
const triggerChange: SelectProps['onChange'] = (...args) => {
|
||||||
emit('update:value', args[0]);
|
emit('update:value', args[0]);
|
||||||
emit('change', ...args);
|
emit('change', ...args);
|
||||||
formItemContext.onFieldChange();
|
formItemContext.onFieldChange();
|
||||||
};
|
};
|
||||||
const handleBlur = (e: InputEvent) => {
|
const handleBlur: SelectProps['onBlur'] = e => {
|
||||||
emit('blur', e);
|
emit('blur', e);
|
||||||
formItemContext.onFieldBlur();
|
formItemContext.onFieldBlur();
|
||||||
};
|
};
|
||||||
expose({
|
expose({
|
||||||
blur,
|
blur,
|
||||||
focus,
|
focus,
|
||||||
|
scrollTo,
|
||||||
});
|
});
|
||||||
|
const isMultiple = computed(() => mode.value === 'multiple' || mode.value === 'tags');
|
||||||
return () => {
|
return () => {
|
||||||
const {
|
const {
|
||||||
notFoundContent,
|
notFoundContent,
|
||||||
|
@ -131,8 +141,6 @@ const Select = defineComponent({
|
||||||
|
|
||||||
const { renderEmpty, getPopupContainer: getContextPopupContainer } = configProvider;
|
const { renderEmpty, getPopupContainer: getContextPopupContainer } = configProvider;
|
||||||
|
|
||||||
const isMultiple = mode.value === 'multiple' || mode.value === 'tags';
|
|
||||||
|
|
||||||
// ===================== Empty =====================
|
// ===================== Empty =====================
|
||||||
let mergedNotFound: any;
|
let mergedNotFound: any;
|
||||||
if (notFoundContent !== undefined) {
|
if (notFoundContent !== undefined) {
|
||||||
|
@ -149,7 +157,7 @@ const Select = defineComponent({
|
||||||
const { suffixIcon, itemIcon, removeIcon, clearIcon } = getIcons(
|
const { suffixIcon, itemIcon, removeIcon, clearIcon } = getIcons(
|
||||||
{
|
{
|
||||||
...props,
|
...props,
|
||||||
multiple: isMultiple,
|
multiple: isMultiple.value,
|
||||||
prefixCls: prefixCls.value,
|
prefixCls: prefixCls.value,
|
||||||
},
|
},
|
||||||
slots,
|
slots,
|
||||||
|
@ -195,9 +203,9 @@ const Select = defineComponent({
|
||||||
dropdownRender={selectProps.dropdownRender || slots.dropdownRender}
|
dropdownRender={selectProps.dropdownRender || slots.dropdownRender}
|
||||||
v-slots={{ option: slots.option }}
|
v-slots={{ option: slots.option }}
|
||||||
transitionName={transitionName.value}
|
transitionName={transitionName.value}
|
||||||
>
|
children={slots.default?.()}
|
||||||
{slots.default?.()}
|
tagRender={props.tagRender || slots.tagRender}
|
||||||
</RcSelect>
|
></RcSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,6 +56,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
|
||||||
| clearIcon | ่ชๅฎไน็ๅค้ๆกๆธ
็ฉบๅพๆ | VNode \| slot | - | |
|
| clearIcon | ่ชๅฎไน็ๅค้ๆกๆธ
็ฉบๅพๆ | VNode \| slot | - | |
|
||||||
| menuItemSelectedIcon | ่ชๅฎไนๅฝๅ้ไธญ็ๆก็ฎๅพๆ | VNode \| slot | - | |
|
| menuItemSelectedIcon | ่ชๅฎไนๅฝๅ้ไธญ็ๆก็ฎๅพๆ | VNode \| slot | - | |
|
||||||
| tokenSeparators | ๅจ tags ๅ multiple ๆจกๅผไธ่ชๅจๅ่ฏ็ๅ้็ฌฆ | string\[] | | |
|
| tokenSeparators | ๅจ tags ๅ multiple ๆจกๅผไธ่ชๅจๅ่ฏ็ๅ้็ฌฆ | string\[] | | |
|
||||||
|
| tagRender | ่ชๅฎไน tag ๅ
ๅฎน render๏ผไป
ๅจ `mode` ไธบ `multiple` ๆ `tags` ๆถ็ๆ | slot \| (props) => any | - | 3.0 |
|
||||||
| value(v-model) | ๆๅฎๅฝๅ้ไธญ็ๆก็ฎ | string\|string\[]\|number\|number\[] | - | |
|
| value(v-model) | ๆๅฎๅฝๅ้ไธญ็ๆก็ฎ | string\|string\[]\|number\|number\[] | - | |
|
||||||
| options | options ๆฐๆฎ๏ผๅฆๆ่ฎพ็ฝฎๅไธ้่ฆๆๅจๆ้ selectOption ่็น | array<{value, label, [disabled, key, title]}> | \[] | |
|
| options | options ๆฐๆฎ๏ผๅฆๆ่ฎพ็ฝฎๅไธ้่ฆๆๅจๆ้ selectOption ่็น | array<{value, label, [disabled, key, title]}> | \[] | |
|
||||||
| option | ้่ฟ option ๆๆงฝ๏ผ่ชๅฎไน่็น | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
|
| option | ้่ฟ option ๆๆงฝ๏ผ่ชๅฎไน่็น | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
@import '../../style/themes/index';
|
@import '../../style/themes/index';
|
||||||
@import '../../style/mixins/index';
|
@import '../../style/mixins/index';
|
||||||
@import '../../input/style/mixin';
|
@import '../../input/style/mixin';
|
||||||
|
|
||||||
@import './single';
|
@import './single';
|
||||||
@import './multiple';
|
@import './multiple';
|
||||||
|
|
||||||
|
@ -59,6 +58,7 @@
|
||||||
|
|
||||||
&::-webkit-search-cancel-button {
|
&::-webkit-search-cancel-button {
|
||||||
display: none;
|
display: none;
|
||||||
|
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,7 @@
|
||||||
&-selection-item {
|
&-selection-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-weight: normal;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@
|
||||||
&-arrow {
|
&-arrow {
|
||||||
.iconfont-mixin();
|
.iconfont-mixin();
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 53%;
|
top: 50%;
|
||||||
right: @control-padding-horizontal - 1px;
|
right: @control-padding-horizontal - 1px;
|
||||||
width: @font-size-sm;
|
width: @font-size-sm;
|
||||||
height: @font-size-sm;
|
height: @font-size-sm;
|
||||||
|
@ -167,9 +168,11 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: color 0.3s ease, opacity 0.15s ease;
|
transition: color 0.3s ease, opacity 0.15s ease;
|
||||||
text-rendering: auto;
|
text-rendering: auto;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
}
|
}
|
||||||
|
@ -286,6 +289,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-disabled {
|
&-disabled {
|
||||||
|
&.@{select-prefix-cls}-item-option-selected {
|
||||||
|
background-color: @select-multiple-disabled-background;
|
||||||
|
}
|
||||||
color: @disabled-color;
|
color: @disabled-color;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import './index';
|
@import (reference) '../../style/themes/index';
|
||||||
|
@select-prefix-cls: ~'@{ant-prefix}-select';
|
||||||
|
|
||||||
@select-overflow-prefix-cls: ~'@{select-prefix-cls}-selection-overflow';
|
@select-overflow-prefix-cls: ~'@{select-prefix-cls}-selection-overflow';
|
||||||
@select-multiple-item-border-width: 1px;
|
@select-multiple-item-border-width: 1px;
|
||||||
|
@ -128,8 +129,6 @@
|
||||||
.@{select-prefix-cls}-selection-search {
|
.@{select-prefix-cls}-selection-search {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-top: @select-multiple-item-spacing-half;
|
|
||||||
margin-bottom: @select-multiple-item-spacing-half;
|
|
||||||
margin-inline-start: @input-padding-horizontal-base - @input-padding-vertical-base;
|
margin-inline-start: @input-padding-horizontal-base - @input-padding-vertical-base;
|
||||||
|
|
||||||
&-input,
|
&-input,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import './index';
|
@import (reference) '../../style/themes/index';
|
||||||
|
@select-prefix-cls: ~'@{ant-prefix}-select';
|
||||||
|
|
||||||
@selection-item-padding: ceil(@font-size-base * 1.25);
|
@selection-item-padding: ceil(@font-size-base * 1.25);
|
||||||
|
|
||||||
|
@ -39,14 +40,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{select-prefix-cls}-selection-placeholder {
|
.@{select-prefix-cls}-selection-placeholder {
|
||||||
|
transition: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For common baseline align
|
// For common baseline align
|
||||||
&::after,
|
&::after,
|
||||||
// For '' value baseline align
|
/* For '' value baseline align */
|
||||||
.@{select-prefix-cls}-selection-item::after,
|
.@{select-prefix-cls}-selection-item::after,
|
||||||
// For undefined value baseline align
|
/* For undefined value baseline align */
|
||||||
.@{select-prefix-cls}-selection-placeholder::after {
|
.@{select-prefix-cls}-selection-placeholder::after {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
@ -1,162 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 type {
|
|
||||||
DisplayValueType,
|
|
||||||
RenderNode,
|
|
||||||
BaseSelectRef,
|
|
||||||
BaseSelectPropsWithoutPrivate,
|
|
||||||
BaseSelectProps,
|
|
||||||
} from './BaseSelect';
|
|
||||||
import OptionList from './OptionList';
|
|
||||||
import Option from './Option';
|
|
||||||
import OptGroup from './OptGroup';
|
|
||||||
import useOptions from './hooks/useOptions';
|
|
||||||
import SelectContext from './SelectContext';
|
|
||||||
import useId from './hooks/useId';
|
|
||||||
import useRefFunc from './hooks/useRefFunc';
|
|
||||||
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 type { Key } from '../_util/type';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import type { ExtractPropTypes, PropType } from 'vue';
|
|
||||||
import PropTypes from '../_util/vue-types';
|
|
||||||
|
|
||||||
const OMIT_DOM_PROPS = ['inputValue'];
|
|
||||||
|
|
||||||
export type OnActiveValue = (
|
|
||||||
active: RawValueType,
|
|
||||||
index: number,
|
|
||||||
info?: { source?: 'keyboard' | 'mouse' },
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
|
|
||||||
|
|
||||||
export type RawValueType = string | number;
|
|
||||||
export interface LabelInValueType {
|
|
||||||
label: any;
|
|
||||||
value: RawValueType;
|
|
||||||
/** @deprecated `key` is useless since it should always same as `value` */
|
|
||||||
key?: Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DraftValueType =
|
|
||||||
| RawValueType
|
|
||||||
| LabelInValueType
|
|
||||||
| DisplayValueType
|
|
||||||
| (RawValueType | LabelInValueType | DisplayValueType)[];
|
|
||||||
|
|
||||||
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
|
||||||
|
|
||||||
export interface FieldNames {
|
|
||||||
value?: string;
|
|
||||||
label?: string;
|
|
||||||
options?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseOptionType {
|
|
||||||
disabled?: boolean;
|
|
||||||
[name: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DefaultOptionType extends BaseOptionType {
|
|
||||||
label: any;
|
|
||||||
value?: string | number | null;
|
|
||||||
children?: Omit<DefaultOptionType, 'children'>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SelectHandler<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType> =
|
|
||||||
| ((value: RawValueType | LabelInValueType, option: OptionType) => void)
|
|
||||||
| ((value: ValueType, option: OptionType) => void);
|
|
||||||
|
|
||||||
export function selectProps<
|
|
||||||
ValueType = any,
|
|
||||||
OptionType extends BaseOptionType = DefaultOptionType,
|
|
||||||
>() {
|
|
||||||
return {
|
|
||||||
...baseSelectPropsWithoutPrivate(),
|
|
||||||
prefixCls: String,
|
|
||||||
id: String,
|
|
||||||
|
|
||||||
backfill: { type: Boolean, default: undefined },
|
|
||||||
|
|
||||||
// >>> Field Names
|
|
||||||
fieldNames: Object as PropType<FieldNames>,
|
|
||||||
|
|
||||||
// >>> Search
|
|
||||||
/** @deprecated Use `searchValue` instead */
|
|
||||||
inputValue: String,
|
|
||||||
searchValue: String,
|
|
||||||
onSearch: Function as PropType<(value: string) => void>,
|
|
||||||
autoClearSearchValue: { type: Boolean, default: undefined },
|
|
||||||
|
|
||||||
// >>> Select
|
|
||||||
onSelect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
|
||||||
onDeselect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
|
||||||
|
|
||||||
// >>> Options
|
|
||||||
/**
|
|
||||||
* In Select, `false` means do nothing.
|
|
||||||
* In TreeSelect, `false` will highlight match item.
|
|
||||||
* It's by design.
|
|
||||||
*/
|
|
||||||
filterOption: {
|
|
||||||
type: [Boolean, Function] as PropType<boolean | FilterFunc<OptionType>>,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
filterSort: Function as PropType<(optionA: OptionType, optionB: OptionType) => number>,
|
|
||||||
optionFilterProp: String,
|
|
||||||
optionLabelProp: String,
|
|
||||||
children: PropTypes.any,
|
|
||||||
options: Array as PropType<OptionType[]>,
|
|
||||||
defaultActiveFirstOption: { type: Boolean, default: undefined },
|
|
||||||
virtual: { type: Boolean, default: undefined },
|
|
||||||
listHeight: Number,
|
|
||||||
listItemHeight: Number,
|
|
||||||
|
|
||||||
// >>> Icon
|
|
||||||
menuItemSelectedIcon: PropTypes.any,
|
|
||||||
|
|
||||||
mode: String as PropType<'combobox' | 'multiple' | 'tags'>,
|
|
||||||
labelInValue: { type: Boolean, default: undefined },
|
|
||||||
value: PropTypes.any,
|
|
||||||
defaultValue: PropTypes.any,
|
|
||||||
onChange: Function as PropType<(value: ValueType, option: OptionType | OptionType[]) => void>,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
|
||||||
|
|
||||||
export default defineComponent({});
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,73 +0,0 @@
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
|
|
||||||
export type SelectSource = 'option' | 'selection' | 'input';
|
|
||||||
|
|
||||||
export const INTERNAL_PROPS_MARK = 'RC_SELECT_INTERNAL_PROPS_MARK';
|
|
||||||
|
|
||||||
// =================================== Shared Type ===================================
|
|
||||||
export type Key = string | number;
|
|
||||||
|
|
||||||
export type RawValueType = string | number | null;
|
|
||||||
|
|
||||||
export interface LabelValueType extends Record<string, any> {
|
|
||||||
key?: Key;
|
|
||||||
value?: RawValueType;
|
|
||||||
label?: any;
|
|
||||||
isCacheable?: boolean;
|
|
||||||
}
|
|
||||||
export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[];
|
|
||||||
|
|
||||||
export interface DisplayLabelValueType extends LabelValueType {
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SingleType<MixType> = MixType extends (infer Single)[] ? Single : MixType;
|
|
||||||
|
|
||||||
export type OnClear = () => any;
|
|
||||||
|
|
||||||
export type CustomTagProps = {
|
|
||||||
label: any;
|
|
||||||
value: DefaultValueType;
|
|
||||||
disabled: boolean;
|
|
||||||
onClose: (event?: MouseEvent) => void;
|
|
||||||
closable: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================================== Generator ====================================
|
|
||||||
export type GetLabeledValue<FOT extends FlattenOptionsType> = (
|
|
||||||
value: RawValueType,
|
|
||||||
config: {
|
|
||||||
options: FOT;
|
|
||||||
prevValueMap: Map<RawValueType, LabelValueType>;
|
|
||||||
labelInValue: boolean;
|
|
||||||
optionLabelProp: string;
|
|
||||||
},
|
|
||||||
) => LabelValueType;
|
|
||||||
|
|
||||||
export type FilterOptions<OptionsType extends object[]> = (
|
|
||||||
searchValue: string,
|
|
||||||
options: OptionsType,
|
|
||||||
/** Component props, since Select & TreeSelect use different prop name, use any here */
|
|
||||||
config: {
|
|
||||||
optionFilterProp: string;
|
|
||||||
filterOption: boolean | FilterFunc<OptionsType[number]>;
|
|
||||||
},
|
|
||||||
) => OptionsType;
|
|
||||||
|
|
||||||
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
|
||||||
|
|
||||||
export type FlattenOptionsType<OptionType = object> = {
|
|
||||||
key: Key;
|
|
||||||
data: OptionType;
|
|
||||||
label?: any;
|
|
||||||
value?: RawValueType;
|
|
||||||
/** Used for customize data */
|
|
||||||
[name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export type DropdownObject = {
|
|
||||||
menuNode?: VueNode;
|
|
||||||
props?: Record<string, any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DropdownRender = (opt?: DropdownObject) => VueNode;
|
|
|
@ -1,62 +0,0 @@
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
import type { VNode, CSSProperties } from 'vue';
|
|
||||||
import type { Key, RawValueType } from './generator';
|
|
||||||
|
|
||||||
export type RenderDOMFunc = (props: any) => HTMLElement;
|
|
||||||
|
|
||||||
export type RenderNode = VueNode | ((props: any) => VueNode);
|
|
||||||
|
|
||||||
export type Mode = 'multiple' | 'tags' | 'combobox';
|
|
||||||
|
|
||||||
// ======================== Option ========================
|
|
||||||
|
|
||||||
export interface FieldNames {
|
|
||||||
value?: string;
|
|
||||||
label?: string;
|
|
||||||
options?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OnActiveValue = (
|
|
||||||
active: RawValueType,
|
|
||||||
index: number,
|
|
||||||
info?: { source?: 'keyboard' | 'mouse' },
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export interface OptionCoreData {
|
|
||||||
key?: Key;
|
|
||||||
disabled?: boolean;
|
|
||||||
value?: Key;
|
|
||||||
title?: string;
|
|
||||||
class?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
label?: VueNode;
|
|
||||||
/** @deprecated Only works when use `children` as option data */
|
|
||||||
children?: VNode[] | JSX.Element[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptionData extends OptionCoreData {
|
|
||||||
/** Save for customize data */
|
|
||||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptionGroupData {
|
|
||||||
key?: Key;
|
|
||||||
label?: VueNode;
|
|
||||||
options: OptionData[];
|
|
||||||
class?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
|
|
||||||
/** Save for customize data */
|
|
||||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OptionsType = (OptionData | OptionGroupData)[];
|
|
||||||
|
|
||||||
export interface FlattenOptionData {
|
|
||||||
group?: boolean;
|
|
||||||
groupOption?: boolean;
|
|
||||||
key: string | number;
|
|
||||||
data: OptionData | OptionGroupData;
|
|
||||||
label?: any;
|
|
||||||
value?: RawValueType;
|
|
||||||
}
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
provide,
|
||||||
ref,
|
ref,
|
||||||
toRefs,
|
toRefs,
|
||||||
watch,
|
watch,
|
||||||
|
@ -36,6 +37,7 @@ import { toReactive } from '../_util/toReactive';
|
||||||
import classNames from '../_util/classNames';
|
import classNames from '../_util/classNames';
|
||||||
import OptionList from './OptionList';
|
import OptionList from './OptionList';
|
||||||
import createRef from '../_util/createRef';
|
import createRef from '../_util/createRef';
|
||||||
|
import type { BaseOptionType } from './Select';
|
||||||
|
|
||||||
const DEFAULT_OMIT_PROPS = [
|
const DEFAULT_OMIT_PROPS = [
|
||||||
'value',
|
'value',
|
||||||
|
@ -50,6 +52,8 @@ const DEFAULT_OMIT_PROPS = [
|
||||||
'onInputKeyDown',
|
'onInputKeyDown',
|
||||||
'onPopupScroll',
|
'onPopupScroll',
|
||||||
'tabindex',
|
'tabindex',
|
||||||
|
'OptionList',
|
||||||
|
'notFoundContent',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type RenderNode = VueNode | ((props: any) => VueNode);
|
export type RenderNode = VueNode | ((props: any) => VueNode);
|
||||||
|
@ -74,20 +78,22 @@ export type CustomTagProps = {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onClose: (event?: MouseEvent) => void;
|
onClose: (event?: MouseEvent) => void;
|
||||||
closable: boolean;
|
closable: boolean;
|
||||||
|
option: BaseOptionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DisplayValueType {
|
export interface DisplayValueType {
|
||||||
key?: Key;
|
key?: Key;
|
||||||
value?: RawValueType;
|
value?: RawValueType;
|
||||||
label?: any;
|
label?: any;
|
||||||
disabled: boolean;
|
disabled?: boolean;
|
||||||
|
option?: BaseOptionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseSelectRef {
|
export type BaseSelectRef = {
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
blur: () => void;
|
blur: () => void;
|
||||||
scrollTo: ScrollTo;
|
scrollTo: ScrollTo;
|
||||||
}
|
};
|
||||||
|
|
||||||
const baseSelectPrivateProps = () => {
|
const baseSelectPrivateProps = () => {
|
||||||
return {
|
return {
|
||||||
|
@ -251,7 +257,7 @@ export default defineComponent({
|
||||||
name: 'BaseSelect',
|
name: 'BaseSelect',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: initDefaultProps(baseSelectProps(), { showAction: [], notFoundContent: 'Not Found' }),
|
props: initDefaultProps(baseSelectProps(), { showAction: [], notFoundContent: 'Not Found' }),
|
||||||
setup(props, { attrs, expose }) {
|
setup(props, { attrs, expose, slots }) {
|
||||||
const multiple = computed(() => isMultiple(props.mode));
|
const multiple = computed(() => isMultiple(props.mode));
|
||||||
|
|
||||||
const mergedShowSearch = computed(() =>
|
const mergedShowSearch = computed(() =>
|
||||||
|
@ -533,7 +539,10 @@ export default defineComponent({
|
||||||
props.onBlur(...args);
|
props.onBlur(...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
provide('VCSelectContainerEvent', {
|
||||||
|
focus: onContainerFocus,
|
||||||
|
blur: onContainerBlur,
|
||||||
|
});
|
||||||
const activeTimeoutIds: any[] = [];
|
const activeTimeoutIds: any[] = [];
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -743,7 +752,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================== OptionList ===========================
|
// =========================== OptionList ===========================
|
||||||
const optionList = <OptionList ref={listRef} />;
|
const optionList = <OptionList ref={listRef} v-slots={{ option: slots.option }} />;
|
||||||
|
|
||||||
// ============================= Select =============================
|
// ============================= Select =============================
|
||||||
const mergedClassName = classNames(prefixCls, attrs.class, {
|
const mergedClassName = classNames(prefixCls, attrs.class, {
|
||||||
|
@ -822,14 +831,14 @@ export default defineComponent({
|
||||||
} else {
|
} else {
|
||||||
renderNode = (
|
renderNode = (
|
||||||
<div
|
<div
|
||||||
class={mergedClassName}
|
|
||||||
{...domProps}
|
{...domProps}
|
||||||
|
class={mergedClassName}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onMousedown={onInternalMouseDown}
|
onMousedown={onInternalMouseDown}
|
||||||
onKeydown={onInternalKeyDown}
|
onKeydown={onInternalKeyDown}
|
||||||
onKeyup={onInternalKeyUp}
|
onKeyup={onInternalKeyUp}
|
||||||
onFocus={onContainerFocus}
|
// onFocus={onContainerFocus}
|
||||||
onBlur={onContainerBlur}
|
// onBlur={onContainerBlur}
|
||||||
>
|
>
|
||||||
{mockFocused && !mergedOpen.value && (
|
{mockFocused && !mergedOpen.value && (
|
||||||
<span
|
<span
|
|
@ -278,7 +278,7 @@ const OptionList = defineComponent({
|
||||||
// Group
|
// Group
|
||||||
if (group) {
|
if (group) {
|
||||||
return (
|
return (
|
||||||
<div class={classNames(itemPrefixCls, `${itemPrefixCls.value}-group`)}>
|
<div class={classNames(itemPrefixCls.value, `${itemPrefixCls.value}-group`)}>
|
||||||
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -298,12 +298,18 @@ const OptionList = defineComponent({
|
||||||
const selected = rawValues.has(value);
|
const selected = rawValues.has(value);
|
||||||
|
|
||||||
const optionPrefixCls = `${itemPrefixCls.value}-option`;
|
const optionPrefixCls = `${itemPrefixCls.value}-option`;
|
||||||
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, cls, className, {
|
const optionClassName = classNames(
|
||||||
|
itemPrefixCls.value,
|
||||||
|
optionPrefixCls,
|
||||||
|
cls,
|
||||||
|
className,
|
||||||
|
{
|
||||||
[`${optionPrefixCls}-grouped`]: groupOption,
|
[`${optionPrefixCls}-grouped`]: groupOption,
|
||||||
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
||||||
[`${optionPrefixCls}-disabled`]: disabled,
|
[`${optionPrefixCls}-disabled`]: disabled,
|
||||||
[`${optionPrefixCls}-selected`]: selected,
|
[`${optionPrefixCls}-selected`]: selected,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const mergedLabel = getLabel(item);
|
const mergedLabel = getLabel(item);
|
||||||
|
|
|
@ -0,0 +1,642 @@
|
||||||
|
/**
|
||||||
|
* 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 type { DisplayValueType, BaseSelectRef, BaseSelectProps } from './BaseSelect';
|
||||||
|
import OptionList from './OptionList';
|
||||||
|
import useOptions from './hooks/useOptions';
|
||||||
|
import type { SelectContextProps } from './SelectContext';
|
||||||
|
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 type { Key, VueNode } from '../_util/type';
|
||||||
|
import { computed, defineComponent, ref, toRef, watchEffect } from 'vue';
|
||||||
|
import type { ExtractPropTypes, PropType } 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';
|
||||||
|
|
||||||
|
const OMIT_DOM_PROPS = ['inputValue'];
|
||||||
|
|
||||||
|
export type OnActiveValue = (
|
||||||
|
active: RawValueType,
|
||||||
|
index: number,
|
||||||
|
info?: { source?: 'keyboard' | 'mouse' },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
|
||||||
|
|
||||||
|
export type RawValueType = string | number;
|
||||||
|
export interface LabelInValueType {
|
||||||
|
label: any;
|
||||||
|
value: RawValueType;
|
||||||
|
/** @deprecated `key` is useless since it should always same as `value` */
|
||||||
|
key?: Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DraftValueType =
|
||||||
|
| RawValueType
|
||||||
|
| LabelInValueType
|
||||||
|
| DisplayValueType
|
||||||
|
| (RawValueType | LabelInValueType | DisplayValueType)[];
|
||||||
|
|
||||||
|
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||||
|
|
||||||
|
export interface FieldNames {
|
||||||
|
value?: string;
|
||||||
|
label?: string;
|
||||||
|
options?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseOptionType {
|
||||||
|
disabled?: boolean;
|
||||||
|
[name: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultOptionType extends BaseOptionType {
|
||||||
|
label?: any;
|
||||||
|
value?: string | number | null;
|
||||||
|
children?: Omit<DefaultOptionType, 'children'>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectHandler<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType> =
|
||||||
|
| ((value: RawValueType | LabelInValueType, option: OptionType) => void)
|
||||||
|
| ((value: ValueType, option: OptionType) => void);
|
||||||
|
|
||||||
|
export function selectProps<
|
||||||
|
ValueType = any,
|
||||||
|
OptionType extends BaseOptionType = DefaultOptionType,
|
||||||
|
>() {
|
||||||
|
return {
|
||||||
|
...baseSelectPropsWithoutPrivate(),
|
||||||
|
prefixCls: String,
|
||||||
|
id: String,
|
||||||
|
|
||||||
|
backfill: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
|
// >>> Field Names
|
||||||
|
fieldNames: Object as PropType<FieldNames>,
|
||||||
|
|
||||||
|
// >>> Search
|
||||||
|
/** @deprecated Use `searchValue` instead */
|
||||||
|
inputValue: String,
|
||||||
|
searchValue: String,
|
||||||
|
onSearch: Function as PropType<(value: string) => void>,
|
||||||
|
autoClearSearchValue: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
|
// >>> Select
|
||||||
|
onSelect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||||
|
onDeselect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||||
|
|
||||||
|
// >>> Options
|
||||||
|
/**
|
||||||
|
* In Select, `false` means do nothing.
|
||||||
|
* In TreeSelect, `false` will highlight match item.
|
||||||
|
* It's by design.
|
||||||
|
*/
|
||||||
|
filterOption: {
|
||||||
|
type: [Boolean, Function] as PropType<boolean | FilterFunc<OptionType>>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
filterSort: Function as PropType<(optionA: OptionType, optionB: OptionType) => number>,
|
||||||
|
optionFilterProp: String,
|
||||||
|
optionLabelProp: String,
|
||||||
|
options: Array as PropType<OptionType[]>,
|
||||||
|
defaultActiveFirstOption: { type: Boolean, default: undefined },
|
||||||
|
virtual: { type: Boolean, default: undefined },
|
||||||
|
listHeight: Number,
|
||||||
|
listItemHeight: Number,
|
||||||
|
|
||||||
|
// >>> Icon
|
||||||
|
menuItemSelectedIcon: PropTypes.any,
|
||||||
|
|
||||||
|
mode: String as PropType<'combobox' | 'multiple' | 'tags'>,
|
||||||
|
labelInValue: { type: Boolean, default: undefined },
|
||||||
|
value: PropTypes.any,
|
||||||
|
defaultValue: PropTypes.any,
|
||||||
|
onChange: Function as PropType<(value: ValueType, option: OptionType | OptionType[]) => void>,
|
||||||
|
children: Array as PropType<VueNode[]>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
||||||
|
|
||||||
|
function isRawValue(value: DraftValueType): value is RawValueType {
|
||||||
|
return !value || typeof value !== 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Select',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: initDefaultProps(selectProps(), {
|
||||||
|
prefixCls: 'vc-select',
|
||||||
|
autoClearSearchValue: true,
|
||||||
|
listHeight: 200,
|
||||||
|
listItemHeight: 20,
|
||||||
|
}),
|
||||||
|
setup(props, { expose, attrs, slots }) {
|
||||||
|
const mergedId = useId(toRef(props, 'id'));
|
||||||
|
const multiple = computed(() => isMultiple(props.mode));
|
||||||
|
const childrenAsData = computed(() => !!(!props.options && props.children));
|
||||||
|
|
||||||
|
const mergedFilterOption = computed(() => {
|
||||||
|
if (props.filterOption === undefined && props.mode === 'combobox') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return props.filterOption;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================= FieldNames =========================
|
||||||
|
const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames, childrenAsData.value));
|
||||||
|
|
||||||
|
// =========================== Search ===========================
|
||||||
|
const [mergedSearchValue, setSearchValue] = useMergedState('', {
|
||||||
|
value: computed(() =>
|
||||||
|
props.searchValue !== undefined ? props.searchValue : props.inputValue,
|
||||||
|
),
|
||||||
|
postState: search => search || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================== Option ===========================
|
||||||
|
const parsedOptions = useOptions(
|
||||||
|
toRef(props, 'options'),
|
||||||
|
toRef(props, 'children'),
|
||||||
|
mergedFieldNames,
|
||||||
|
);
|
||||||
|
const { valueOptions, labelOptions, options: mergedOptions } = parsedOptions;
|
||||||
|
|
||||||
|
// ========================= Wrap Value =========================
|
||||||
|
const convert2LabelValues = (draftValues: DraftValueType) => {
|
||||||
|
// Convert to array
|
||||||
|
const valueList = toArray(draftValues);
|
||||||
|
|
||||||
|
// Convert to labelInValue type
|
||||||
|
return valueList.map(val => {
|
||||||
|
let rawValue: RawValueType;
|
||||||
|
let rawLabel: any;
|
||||||
|
let rawKey: Key;
|
||||||
|
let rawDisabled: boolean | undefined;
|
||||||
|
|
||||||
|
// Fill label & value
|
||||||
|
if (isRawValue(val)) {
|
||||||
|
rawValue = val;
|
||||||
|
} else {
|
||||||
|
rawKey = val.key;
|
||||||
|
rawLabel = val.label;
|
||||||
|
rawValue = val.value ?? rawKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = valueOptions.value.get(rawValue);
|
||||||
|
if (option) {
|
||||||
|
// Fill missing props
|
||||||
|
if (rawLabel === undefined)
|
||||||
|
rawLabel = option?.[props.optionLabelProp || mergedFieldNames.value.label];
|
||||||
|
if (rawKey === undefined) rawKey = option?.key ?? rawValue;
|
||||||
|
rawDisabled = 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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================== Values ===========================
|
||||||
|
const [internalValue, setInternalValue] = useMergedState(props.defaultValue, {
|
||||||
|
value: toRef(props, 'value'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merged value with LabelValueType
|
||||||
|
const rawLabeledValues = computed(() => {
|
||||||
|
const values = convert2LabelValues(internalValue.value);
|
||||||
|
|
||||||
|
// combobox no need save value when it's empty
|
||||||
|
if (props.mode === 'combobox' && !values[0]?.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fill label with cache to avoid option remove
|
||||||
|
const [mergedValues, getMixedOption] = useCache(rawLabeledValues, valueOptions);
|
||||||
|
|
||||||
|
const displayValues = computed(() => {
|
||||||
|
// `null` need show as placeholder instead
|
||||||
|
// https://github.com/ant-design/ant-design/issues/25057
|
||||||
|
if (!props.mode && mergedValues.value.length === 1) {
|
||||||
|
const firstValue = mergedValues.value[0];
|
||||||
|
if (
|
||||||
|
firstValue.value === null &&
|
||||||
|
(firstValue.label === null || firstValue.label === undefined)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedValues.value.map(item => ({
|
||||||
|
...item,
|
||||||
|
label: item.label ?? item.value,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Convert `displayValues` to raw value type set */
|
||||||
|
const rawValues = computed(() => new Set(mergedValues.value.map(val => val.value)));
|
||||||
|
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
if (props.mode === 'combobox') {
|
||||||
|
const strValue = mergedValues.value[0]?.value;
|
||||||
|
|
||||||
|
if (strValue !== undefined && strValue !== null) {
|
||||||
|
setSearchValue(String(strValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ======================= Display Option =======================
|
||||||
|
// Create a placeholder item if not exist in `options`
|
||||||
|
const createTagOption = (val: RawValueType, label?: any) => {
|
||||||
|
const mergedLabel = label ?? val;
|
||||||
|
return {
|
||||||
|
[mergedFieldNames.value.value]: val,
|
||||||
|
[mergedFieldNames.value.label]: mergedLabel,
|
||||||
|
} as DefaultOptionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill tag as option if mode is `tags`
|
||||||
|
const filledTagOptions = computed(() => {
|
||||||
|
if (props.mode !== 'tags') {
|
||||||
|
return mergedOptions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>> Tag mode
|
||||||
|
const cloneOptions = [...mergedOptions.value];
|
||||||
|
|
||||||
|
// Check if value exist in options (include new patch item)
|
||||||
|
const existOptions = (val: RawValueType) => valueOptions.value.has(val);
|
||||||
|
|
||||||
|
// Fill current value as option
|
||||||
|
[...mergedValues.value]
|
||||||
|
.sort((a, b) => (a.value < b.value ? -1 : 1))
|
||||||
|
.forEach(item => {
|
||||||
|
const val = item.value;
|
||||||
|
|
||||||
|
if (!existOptions(val)) {
|
||||||
|
cloneOptions.push(createTagOption(val, item.label));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cloneOptions;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredOptions = useFilterOptions(
|
||||||
|
filledTagOptions,
|
||||||
|
mergedFieldNames,
|
||||||
|
mergedSearchValue,
|
||||||
|
mergedFilterOption,
|
||||||
|
toRef(props, 'optionFilterProp'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fill options with search value if needed
|
||||||
|
const filledSearchOptions = computed(() => {
|
||||||
|
if (
|
||||||
|
props.mode !== 'tags' ||
|
||||||
|
!mergedSearchValue.value ||
|
||||||
|
filteredOptions.value.some(
|
||||||
|
item => item[props.optionFilterProp || 'value'] === mergedSearchValue.value,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return filteredOptions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill search value as option
|
||||||
|
return [createTagOption(mergedSearchValue.value), ...filteredOptions.value];
|
||||||
|
});
|
||||||
|
|
||||||
|
const orderedFilteredOptions = computed(() => {
|
||||||
|
if (!props.filterSort) {
|
||||||
|
return filledSearchOptions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...filledSearchOptions.value].sort((a, b) => props.filterSort(a, b));
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayOptions = computed(() =>
|
||||||
|
flattenOptions(orderedFilteredOptions.value, {
|
||||||
|
fieldNames: mergedFieldNames.value,
|
||||||
|
childrenAsData: childrenAsData.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================== Change ===========================
|
||||||
|
const triggerChange = (values: DraftValueType) => {
|
||||||
|
const labeledValues = convert2LabelValues(values);
|
||||||
|
setInternalValue(labeledValues);
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.onChange &&
|
||||||
|
// Trigger event only when value changed
|
||||||
|
(labeledValues.length !== mergedValues.value.length ||
|
||||||
|
labeledValues.some((newVal, index) => mergedValues.value[index]?.value !== newVal?.value))
|
||||||
|
) {
|
||||||
|
const returnValues = props.labelInValue ? labeledValues : labeledValues.map(v => v.value);
|
||||||
|
const returnOptions = labeledValues.map(v =>
|
||||||
|
injectPropsWithOption(getMixedOption(v.value)),
|
||||||
|
);
|
||||||
|
|
||||||
|
props.onChange(
|
||||||
|
// Value
|
||||||
|
multiple.value ? returnValues : returnValues[0],
|
||||||
|
// Option
|
||||||
|
multiple.value ? returnOptions : returnOptions[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================= Accessibility ========================
|
||||||
|
const [activeValue, setActiveValue] = useState<string>(null);
|
||||||
|
const [accessibilityIndex, setAccessibilityIndex] = useState(0);
|
||||||
|
const mergedDefaultActiveFirstOption = computed(() =>
|
||||||
|
props.defaultActiveFirstOption !== undefined
|
||||||
|
? props.defaultActiveFirstOption
|
||||||
|
: props.mode !== 'combobox',
|
||||||
|
);
|
||||||
|
|
||||||
|
const onActiveValue: OnActiveValue = (active, index, { source = 'keyboard' } = {}) => {
|
||||||
|
setAccessibilityIndex(index);
|
||||||
|
|
||||||
|
if (props.backfill && props.mode === 'combobox' && active !== null && source === 'keyboard') {
|
||||||
|
setActiveValue(String(active));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================= OptionList =========================
|
||||||
|
const triggerSelect = (val: RawValueType, selected: boolean) => {
|
||||||
|
const getSelectEnt = (): [RawValueType | LabelInValueType, DefaultOptionType] => {
|
||||||
|
const option = getMixedOption(val);
|
||||||
|
return [
|
||||||
|
props.labelInValue
|
||||||
|
? {
|
||||||
|
label: option?.[mergedFieldNames.value.label],
|
||||||
|
value: val,
|
||||||
|
key: option.key ?? val,
|
||||||
|
}
|
||||||
|
: val,
|
||||||
|
injectPropsWithOption(option),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selected && props.onSelect) {
|
||||||
|
const [wrappedValue, option] = getSelectEnt();
|
||||||
|
props.onSelect(wrappedValue, option);
|
||||||
|
} else if (!selected && props.onDeselect) {
|
||||||
|
const [wrappedValue, option] = getSelectEnt();
|
||||||
|
props.onDeselect(wrappedValue, option);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Used for OptionList selection
|
||||||
|
const onInternalSelect = (val, info) => {
|
||||||
|
let cloneValues: (RawValueType | DisplayValueType)[];
|
||||||
|
|
||||||
|
// Single mode always trigger select only with option list
|
||||||
|
const mergedSelect = multiple.value ? info.selected : true;
|
||||||
|
|
||||||
|
if (mergedSelect) {
|
||||||
|
cloneValues = multiple.value ? [...mergedValues.value, val] : [val];
|
||||||
|
} else {
|
||||||
|
cloneValues = mergedValues.value.filter(v => 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
|
||||||
|
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (nextValues, info) => {
|
||||||
|
triggerChange(nextValues);
|
||||||
|
|
||||||
|
if (info.type === 'remove' || info.type === 'clear') {
|
||||||
|
info.values.forEach(item => {
|
||||||
|
triggerSelect(item.value, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================== Search ===========================
|
||||||
|
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
|
||||||
|
setSearchValue(searchText);
|
||||||
|
setActiveValue(null);
|
||||||
|
|
||||||
|
// [Submit] Tag mode should flush input
|
||||||
|
if (info.source === 'submit') {
|
||||||
|
const formatted = (searchText || '').trim();
|
||||||
|
// prevent empty tags from appearing when you click the Enter button
|
||||||
|
if (formatted) {
|
||||||
|
const newRawValues = Array.from(new Set<RawValueType>([...rawValues.value, formatted]));
|
||||||
|
triggerChange(newRawValues);
|
||||||
|
triggerSelect(formatted, true);
|
||||||
|
setSearchValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.source !== 'blur') {
|
||||||
|
if (props.mode === 'combobox') {
|
||||||
|
triggerChange(searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onSearch?.(searchText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInternalSearchSplit: BaseSelectProps['onSearchSplit'] = words => {
|
||||||
|
let patchValues: RawValueType[] = words;
|
||||||
|
|
||||||
|
if (props.mode !== 'tags') {
|
||||||
|
patchValues = words
|
||||||
|
.map(word => {
|
||||||
|
const opt = labelOptions.value.get(word);
|
||||||
|
return opt?.value;
|
||||||
|
})
|
||||||
|
.filter(val => val !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRawValues = Array.from(new Set<RawValueType>([...rawValues.value, ...patchValues]));
|
||||||
|
triggerChange(newRawValues);
|
||||||
|
newRawValues.forEach(newRawValue => {
|
||||||
|
triggerSelect(newRawValue, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const realVirtual = computed(
|
||||||
|
() => props.virtual !== false && props.dropdownMatchSelectWidth !== false,
|
||||||
|
);
|
||||||
|
useProvideSelectProps(
|
||||||
|
toReactive({
|
||||||
|
...parsedOptions,
|
||||||
|
flattenOptions: displayOptions,
|
||||||
|
onActiveValue,
|
||||||
|
defaultActiveFirstOption: mergedDefaultActiveFirstOption,
|
||||||
|
onSelect: onInternalSelect,
|
||||||
|
menuItemSelectedIcon: toRef(props, 'menuItemSelectedIcon'),
|
||||||
|
rawValues,
|
||||||
|
fieldNames: mergedFieldNames,
|
||||||
|
virtual: realVirtual,
|
||||||
|
listHeight: toRef(props, 'listHeight'),
|
||||||
|
listItemHeight: toRef(props, 'listItemHeight'),
|
||||||
|
childrenAsData,
|
||||||
|
} as unknown as SelectContextProps),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================== Warning ===========================
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
warningProps(props);
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const selectRef = ref<BaseSelectRef>();
|
||||||
|
expose({
|
||||||
|
focus() {
|
||||||
|
selectRef.value?.focus();
|
||||||
|
},
|
||||||
|
blur() {
|
||||||
|
selectRef.value?.blur();
|
||||||
|
},
|
||||||
|
scrollTo(arg) {
|
||||||
|
selectRef.value?.scrollTo(arg);
|
||||||
|
},
|
||||||
|
} as BaseSelectRef);
|
||||||
|
const pickProps = computed(() => {
|
||||||
|
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 () => {
|
||||||
|
return (
|
||||||
|
<BaseSelect
|
||||||
|
{...pickProps.value}
|
||||||
|
{...attrs}
|
||||||
|
// >>> MISC
|
||||||
|
id={mergedId}
|
||||||
|
prefixCls={props.prefixCls}
|
||||||
|
ref={selectRef}
|
||||||
|
omitDomProps={OMIT_DOM_PROPS}
|
||||||
|
mode={props.mode}
|
||||||
|
// >>> Values
|
||||||
|
displayValues={displayValues.value}
|
||||||
|
onDisplayValuesChange={onDisplayValuesChange}
|
||||||
|
// >>> Search
|
||||||
|
searchValue={mergedSearchValue.value}
|
||||||
|
onSearch={onInternalSearch}
|
||||||
|
onSearchSplit={onInternalSearchSplit}
|
||||||
|
dropdownMatchSelectWidth={props.dropdownMatchSelectWidth}
|
||||||
|
// >>> OptionList
|
||||||
|
OptionList={OptionList}
|
||||||
|
emptyOptions={!displayOptions.value.length}
|
||||||
|
// >>> Accessibility
|
||||||
|
activeValue={activeValue.value}
|
||||||
|
activeDescendantId={`${mergedId}_list_${accessibilityIndex.value}`}
|
||||||
|
v-slots={slots}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -9,6 +9,7 @@ import PropTypes from '../../_util/vue-types';
|
||||||
import type { VueNode } from '../../_util/type';
|
import type { VueNode } from '../../_util/type';
|
||||||
import Overflow from '../../vc-overflow';
|
import Overflow from '../../vc-overflow';
|
||||||
import type { DisplayValueType, RenderNode, CustomTagProps, RawValueType } from '../BaseSelect';
|
import type { DisplayValueType, RenderNode, CustomTagProps, RawValueType } from '../BaseSelect';
|
||||||
|
import type { BaseOptionType } from '../Select';
|
||||||
|
|
||||||
type SelectorProps = InnerSelectorProps & {
|
type SelectorProps = InnerSelectorProps & {
|
||||||
// Icon
|
// Icon
|
||||||
|
@ -140,12 +141,12 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
itemDisabled: boolean,
|
itemDisabled: boolean,
|
||||||
closable: boolean,
|
closable: boolean,
|
||||||
onClose: (e: MouseEvent) => void,
|
onClose: (e: MouseEvent) => void,
|
||||||
|
option: BaseOptionType,
|
||||||
) {
|
) {
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
onPreventMouseDown(e);
|
onPreventMouseDown(e);
|
||||||
props.onToggleOpen(!open);
|
props.onToggleOpen(!open);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span onMousedown={onMouseDown}>
|
<span onMousedown={onMouseDown}>
|
||||||
{props.tagRender({
|
{props.tagRender({
|
||||||
|
@ -154,13 +155,14 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
disabled: itemDisabled,
|
disabled: itemDisabled,
|
||||||
closable,
|
closable,
|
||||||
onClose,
|
onClose,
|
||||||
|
option,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItem(valueItem: DisplayValueType) {
|
function renderItem(valueItem: DisplayValueType) {
|
||||||
const { disabled: itemDisabled, label, value } = valueItem;
|
const { disabled: itemDisabled, label, value, option } = valueItem;
|
||||||
const closable = !props.disabled && !itemDisabled;
|
const closable = !props.disabled && !itemDisabled;
|
||||||
|
|
||||||
let displayLabel = label;
|
let displayLabel = label;
|
||||||
|
@ -180,7 +182,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
};
|
};
|
||||||
|
|
||||||
return typeof props.tagRender === 'function'
|
return typeof props.tagRender === 'function'
|
||||||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
|
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
|
||||||
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { computed } from 'vue';
|
import { shallowRef, watchEffect } from 'vue';
|
||||||
import type { FieldNames, RawValueType } from '../Select';
|
import type { FieldNames, RawValueType } from '../Select';
|
||||||
import { convertChildrenToData } from '../utils/legacyUtil';
|
import { convertChildrenToData } from '../utils/legacyUtil';
|
||||||
|
|
||||||
|
@ -12,35 +12,40 @@ export default function useOptions<OptionType>(
|
||||||
children: Ref<any>,
|
children: Ref<any>,
|
||||||
fieldNames: Ref<FieldNames>,
|
fieldNames: Ref<FieldNames>,
|
||||||
) {
|
) {
|
||||||
return computed(() => {
|
const mergedOptions = shallowRef();
|
||||||
let mergedOptions = options.value;
|
const valueOptions = shallowRef();
|
||||||
|
const labelOptions = shallowRef();
|
||||||
|
watchEffect(() => {
|
||||||
|
let newOptions = options.value;
|
||||||
const childrenAsData = !options.value;
|
const childrenAsData = !options.value;
|
||||||
|
|
||||||
if (childrenAsData) {
|
if (childrenAsData) {
|
||||||
mergedOptions = convertChildrenToData(children.value);
|
newOptions = convertChildrenToData(children.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueOptions = new Map<RawValueType, OptionType>();
|
const newValueOptions = new Map<RawValueType, OptionType>();
|
||||||
const labelOptions = new Map<any, OptionType>();
|
const newLabelOptions = new Map<any, OptionType>();
|
||||||
|
|
||||||
function dig(optionList: OptionType[], isChildren = false) {
|
function dig(optionList: OptionType[], isChildren = false) {
|
||||||
// for loop to speed up collection speed
|
// for loop to speed up collection speed
|
||||||
for (let i = 0; i < optionList.length; i += 1) {
|
for (let i = 0; i < optionList.length; i += 1) {
|
||||||
const option = optionList[i];
|
const option = optionList[i];
|
||||||
if (!option[fieldNames.value.options] || isChildren) {
|
if (!option[fieldNames.value.options] || isChildren) {
|
||||||
valueOptions.set(option[fieldNames.value.value], option);
|
newValueOptions.set(option[fieldNames.value.value], option);
|
||||||
labelOptions.set(option[fieldNames.value.label], option);
|
newLabelOptions.set(option[fieldNames.value.label], option);
|
||||||
} else {
|
} else {
|
||||||
dig(option[fieldNames.value.options], true);
|
dig(option[fieldNames.value.options], true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dig(mergedOptions);
|
dig(newOptions);
|
||||||
|
mergedOptions.value = newOptions;
|
||||||
|
valueOptions.value = newValueOptions;
|
||||||
|
labelOptions.value = newLabelOptions;
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
options: mergedOptions,
|
options: mergedOptions,
|
||||||
valueOptions,
|
valueOptions,
|
||||||
labelOptions,
|
labelOptions,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
}
|
}
|
|
@ -25,7 +25,7 @@ function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionTy
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
|
export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||||
nodes: VueNode,
|
nodes: VueNode[],
|
||||||
optionOnly = false,
|
optionOnly = false,
|
||||||
): OptionType[] {
|
): OptionType[] {
|
||||||
const dd = flattenChildren(nodes as [])
|
const dd = flattenChildren(nodes as [])
|
Loadingโฆ
Reference in New Issue