refactor: select
parent
ca17b5a928
commit
bb91ce7592
|
|
@ -1,7 +1,7 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
import type { FunctionalComponent } from 'vue';
|
||||||
import type { OptionGroupData } from '../vc-select/interface';
|
import type { DefaultOptionType } from '../select';
|
||||||
|
|
||||||
export type OptGroupProps = Omit<OptionGroupData, 'options'>;
|
export type OptGroupProps = Omit<DefaultOptionType, 'options'>;
|
||||||
|
|
||||||
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
|
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
|
||||||
/** Legacy for check if is a Option Group */
|
/** Legacy for check if is a Option Group */
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
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 type { BaseSelectRef } from '../vc-select2';
|
import type { BaseSelectRef } from '../vc-select';
|
||||||
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select2';
|
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select';
|
||||||
import type { BaseOptionType, DefaultOptionType } from '../vc-select2/Select';
|
import type { BaseOptionType, DefaultOptionType } from '../vc-select/Select';
|
||||||
import type { OptionProps } from '../vc-select2/Option';
|
import type { OptionProps } from '../vc-select/Option';
|
||||||
import getIcons from './utils/iconUtil';
|
import getIcons from './utils/iconUtil';
|
||||||
import PropTypes from '../_util/vue-types';
|
import PropTypes from '../_util/vue-types';
|
||||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
import type { FunctionalComponent } from 'vue';
|
||||||
|
|
||||||
import type { OptionGroupData } from './interface';
|
import type { DefaultOptionType } from './Select';
|
||||||
|
|
||||||
export type OptGroupProps = Omit<OptionGroupData, 'options'>;
|
export type OptGroupProps = Omit<DefaultOptionType, 'options'>;
|
||||||
|
|
||||||
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
|
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
|
||||||
/** Legacy for check if is a Option Group */
|
/** Legacy for check if is a Option Group */
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
import type { FunctionalComponent } from 'vue';
|
||||||
|
|
||||||
import type { OptionCoreData } from './interface';
|
import type { DefaultOptionType } from './Select';
|
||||||
|
|
||||||
export interface OptionProps extends Omit<OptionCoreData, 'label'> {
|
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
|
||||||
/** Save for customize data */
|
/** Save for customize data */
|
||||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,12 @@
|
||||||
import TransBtn from './TransBtn';
|
import TransBtn from './TransBtn';
|
||||||
import PropTypes from '../_util/vue-types';
|
|
||||||
import KeyCode from '../_util/KeyCode';
|
import KeyCode from '../_util/KeyCode';
|
||||||
import classNames from '../_util/classNames';
|
import classNames from '../_util/classNames';
|
||||||
import pickAttrs from '../_util/pickAttrs';
|
import pickAttrs from '../_util/pickAttrs';
|
||||||
import { isValidElement } from '../_util/props-util';
|
import { isValidElement } from '../_util/props-util';
|
||||||
import createRef from '../_util/createRef';
|
import createRef from '../_util/createRef';
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
|
import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
|
||||||
import List from '../vc-virtual-list';
|
import List from '../vc-virtual-list';
|
||||||
import type {
|
|
||||||
OptionsType as SelectOptionsType,
|
|
||||||
OptionData,
|
|
||||||
RenderNode,
|
|
||||||
OnActiveValue,
|
|
||||||
FieldNames,
|
|
||||||
} from './interface';
|
|
||||||
import type { RawValueType, FlattenOptionsType } from './interface/generator';
|
|
||||||
import { fillFieldNames } from './utils/valueUtil';
|
|
||||||
import useMemo from '../_util/hooks/useMemo';
|
import useMemo from '../_util/hooks/useMemo';
|
||||||
import { isPlatformMac } from './utils/platformUtil';
|
import { isPlatformMac } from './utils/platformUtil';
|
||||||
|
|
||||||
|
|
@ -28,78 +18,28 @@ export interface RefOptionListProps {
|
||||||
|
|
||||||
import type { EventHandler } from '../_util/EventInterface';
|
import type { EventHandler } from '../_util/EventInterface';
|
||||||
import omit from '../_util/omit';
|
import omit from '../_util/omit';
|
||||||
export interface OptionListProps<OptionType extends object> {
|
import useBaseProps from './hooks/useBaseProps';
|
||||||
prefixCls: string;
|
import type { RawValueType } from './Select';
|
||||||
id: string;
|
import useSelectProps from './SelectContext';
|
||||||
options: OptionType[];
|
// export interface OptionListProps<OptionsType extends object[]> {
|
||||||
fieldNames?: FieldNames;
|
export type OptionListProps = Record<string, never>;
|
||||||
flattenOptions: FlattenOptionsType<OptionType>;
|
|
||||||
height: number;
|
|
||||||
itemHeight: number;
|
|
||||||
values: Set<RawValueType>;
|
|
||||||
multiple: boolean;
|
|
||||||
open: boolean;
|
|
||||||
defaultActiveFirstOption?: boolean;
|
|
||||||
notFoundContent?: any;
|
|
||||||
menuItemSelectedIcon?: RenderNode;
|
|
||||||
childrenAsData: boolean;
|
|
||||||
searchValue: string;
|
|
||||||
virtual: boolean;
|
|
||||||
direction?: 'ltr' | 'rtl';
|
|
||||||
|
|
||||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
|
||||||
onToggleOpen: (open?: boolean) => void;
|
|
||||||
/** Tell Select that some value is now active to make accessibility work */
|
|
||||||
onActiveValue: OnActiveValue;
|
|
||||||
onScroll: EventHandler;
|
|
||||||
|
|
||||||
/** Tell Select that mouse enter the popup to force re-render */
|
|
||||||
onMouseenter?: EventHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OptionListProps = {
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
id: PropTypes.string,
|
|
||||||
options: PropTypes.array,
|
|
||||||
fieldNames: PropTypes.object,
|
|
||||||
flattenOptions: PropTypes.array,
|
|
||||||
height: PropTypes.number,
|
|
||||||
itemHeight: PropTypes.number,
|
|
||||||
values: PropTypes.any,
|
|
||||||
multiple: PropTypes.looseBool,
|
|
||||||
open: PropTypes.looseBool,
|
|
||||||
defaultActiveFirstOption: PropTypes.looseBool,
|
|
||||||
notFoundContent: PropTypes.any,
|
|
||||||
menuItemSelectedIcon: PropTypes.any,
|
|
||||||
childrenAsData: PropTypes.looseBool,
|
|
||||||
searchValue: PropTypes.string,
|
|
||||||
virtual: PropTypes.looseBool,
|
|
||||||
direction: PropTypes.string,
|
|
||||||
|
|
||||||
onSelect: PropTypes.func,
|
|
||||||
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
|
||||||
/** Tell Select that some value is now active to make accessibility work */
|
|
||||||
onActiveValue: PropTypes.func,
|
|
||||||
onScroll: PropTypes.func,
|
|
||||||
|
|
||||||
/** Tell Select that mouse enter the popup to force re-render */
|
|
||||||
onMouseenter: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Using virtual list of option display.
|
* Using virtual list of option display.
|
||||||
* Will fallback to dom if use customize render.
|
* Will fallback to dom if use customize render.
|
||||||
*/
|
*/
|
||||||
const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, { state?: any }>({
|
const OptionList = defineComponent({
|
||||||
name: 'OptionList',
|
name: 'OptionList',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
slots: ['option'],
|
slots: ['option'],
|
||||||
setup(props) {
|
setup(_, { expose, slots }) {
|
||||||
const itemPrefixCls = computed(() => `${props.prefixCls}-item`);
|
const baseProps = useBaseProps();
|
||||||
|
const props = useSelectProps();
|
||||||
|
const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`);
|
||||||
|
|
||||||
const memoFlattenOptions = useMemo(
|
const memoFlattenOptions = useMemo(
|
||||||
() => props.flattenOptions,
|
() => props.flattenOptions,
|
||||||
[() => props.open, () => props.flattenOptions],
|
[() => baseProps.open, () => props.flattenOptions],
|
||||||
next => next[0],
|
next => next[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -124,7 +64,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||||
const current = (index + i * offset + len) % len;
|
const current = (index + i * offset + len) % len;
|
||||||
|
|
||||||
const { group, data } = memoFlattenOptions.value[current];
|
const { group, data } = memoFlattenOptions.value[current];
|
||||||
if (!group && !(data as OptionData).disabled) {
|
if (!group && !data.disabled) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +92,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||||
// Auto active first item when list length or searchValue changed
|
// Auto active first item when list length or searchValue changed
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[() => memoFlattenOptions.value.length, () => props.searchValue],
|
[() => memoFlattenOptions.value.length, () => baseProps.searchValue],
|
||||||
() => {
|
() => {
|
||||||
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
||||||
},
|
},
|
||||||
|
|
@ -161,10 +101,10 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||||
// Auto scroll to item position in single mode
|
// Auto scroll to item position in single mode
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[() => props.open, () => props.searchValue],
|
[() => baseProps.open, () => baseProps.searchValue],
|
||||||
() => {
|
() => {
|
||||||
if (!props.multiple && props.open && props.values.size === 1) {
|
if (!baseProps.multiple && baseProps.open && props.rawValues.size === 1) {
|
||||||
const value = Array.from(props.values)[0];
|
const value = Array.from(props.rawValues)[0];
|
||||||
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
|
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
setActive(index);
|
setActive(index);
|
||||||
|
|
@ -174,7 +114,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Force trigger scrollbar visible when open
|
// Force trigger scrollbar visible when open
|
||||||
if (props.open) {
|
if (baseProps.open) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
listRef.current?.scrollTo(undefined);
|
listRef.current?.scrollTo(undefined);
|
||||||
});
|
});
|
||||||
|
|
@ -186,262 +126,253 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||||
// ========================== Values ==========================
|
// ========================== Values ==========================
|
||||||
const onSelectValue = (value?: RawValueType) => {
|
const onSelectValue = (value?: RawValueType) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
props.onSelect(value, { selected: !props.values.has(value) });
|
props.onSelect(value, { selected: !props.rawValues.has(value) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single mode should always close by select
|
// Single mode should always close by select
|
||||||
if (!props.multiple) {
|
if (!baseProps.multiple) {
|
||||||
props.onToggleOpen(false);
|
baseProps.toggleOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const getLabel = (item: Record<string, any>) => item.label;
|
||||||
function renderItem(index: number) {
|
function renderItem(index: number) {
|
||||||
const item = memoFlattenOptions.value[index];
|
const item = memoFlattenOptions.value[index];
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
const itemData = (item.data || {}) as OptionData;
|
const itemData = item.data || {};
|
||||||
const { value, label, children } = itemData;
|
const { value } = itemData;
|
||||||
|
const { group } = item;
|
||||||
const attrs = pickAttrs(itemData, true);
|
const attrs = pickAttrs(itemData, true);
|
||||||
const mergedLabel = props.childrenAsData ? children : label;
|
const mergedLabel = getLabel(item);
|
||||||
return item ? (
|
return item ? (
|
||||||
<div
|
<div
|
||||||
aria-label={typeof mergedLabel === 'string' ? mergedLabel : undefined}
|
aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
|
||||||
{...attrs}
|
{...attrs}
|
||||||
key={index}
|
key={index}
|
||||||
role="option"
|
role={group ? 'presentation' : 'option'}
|
||||||
id={`${props.id}_list_${index}`}
|
id={`${baseProps.id}_list_${index}`}
|
||||||
aria-selected={props.values.has(value)}
|
aria-selected={props.rawValues.has(value)}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
return {
|
const onKeydown = (event: KeyboardEvent) => {
|
||||||
memoFlattenOptions,
|
const { which, ctrlKey } = event;
|
||||||
renderItem,
|
switch (which) {
|
||||||
listRef,
|
// >>> Arrow keys & ctrl + n/p on Mac
|
||||||
state,
|
case KeyCode.N:
|
||||||
onListMouseDown,
|
case KeyCode.P:
|
||||||
itemPrefixCls,
|
case KeyCode.UP:
|
||||||
setActive,
|
case KeyCode.DOWN: {
|
||||||
onSelectValue,
|
let offset = 0;
|
||||||
onKeydown: (event: KeyboardEvent) => {
|
if (which === KeyCode.UP) {
|
||||||
const { which, ctrlKey } = event;
|
offset = -1;
|
||||||
switch (which) {
|
} else if (which === KeyCode.DOWN) {
|
||||||
// >>> Arrow keys & ctrl + n/p on Mac
|
offset = 1;
|
||||||
case KeyCode.N:
|
} else if (isPlatformMac() && ctrlKey) {
|
||||||
case KeyCode.P:
|
if (which === KeyCode.N) {
|
||||||
case KeyCode.UP:
|
|
||||||
case KeyCode.DOWN: {
|
|
||||||
let offset = 0;
|
|
||||||
if (which === KeyCode.UP) {
|
|
||||||
offset = -1;
|
|
||||||
} else if (which === KeyCode.DOWN) {
|
|
||||||
offset = 1;
|
offset = 1;
|
||||||
} else if (isPlatformMac() && ctrlKey) {
|
} else if (which === KeyCode.P) {
|
||||||
if (which === KeyCode.N) {
|
offset = -1;
|
||||||
offset = 1;
|
|
||||||
} else if (which === KeyCode.P) {
|
|
||||||
offset = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset !== 0) {
|
|
||||||
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
|
|
||||||
scrollIntoView(nextActiveIndex);
|
|
||||||
setActive(nextActiveIndex, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// >>> Select
|
if (offset !== 0) {
|
||||||
case KeyCode.ENTER: {
|
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
|
||||||
// value
|
scrollIntoView(nextActiveIndex);
|
||||||
const item = memoFlattenOptions.value[state.activeIndex];
|
setActive(nextActiveIndex, true);
|
||||||
if (item && !item.data.disabled) {
|
|
||||||
onSelectValue(item.data.value);
|
|
||||||
} else {
|
|
||||||
onSelectValue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.open) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// >>> Close
|
break;
|
||||||
case KeyCode.ESC: {
|
}
|
||||||
props.onToggleOpen(false);
|
|
||||||
if (props.open) {
|
// >>> Select
|
||||||
event.stopPropagation();
|
case KeyCode.ENTER: {
|
||||||
}
|
// value
|
||||||
|
const item = memoFlattenOptions.value[state.activeIndex];
|
||||||
|
if (item && !item.data.disabled) {
|
||||||
|
onSelectValue(item.data.value);
|
||||||
|
} else {
|
||||||
|
onSelectValue(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseProps.open) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>> Close
|
||||||
|
case KeyCode.ESC: {
|
||||||
|
baseProps.toggleOpen(false);
|
||||||
|
if (baseProps.open) {
|
||||||
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onKeyup: () => {},
|
|
||||||
|
|
||||||
scrollTo: (index: number) => {
|
|
||||||
scrollIntoView(index);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
const onKeyup = () => {};
|
||||||
render() {
|
|
||||||
const {
|
const scrollTo = (index: number) => {
|
||||||
renderItem,
|
scrollIntoView(index);
|
||||||
listRef,
|
};
|
||||||
onListMouseDown,
|
expose({
|
||||||
itemPrefixCls,
|
onKeydown,
|
||||||
setActive,
|
onKeyup,
|
||||||
onSelectValue,
|
scrollTo,
|
||||||
memoFlattenOptions,
|
});
|
||||||
$slots,
|
return () => {
|
||||||
} = this as any;
|
// const {
|
||||||
const {
|
// renderItem,
|
||||||
id,
|
// listRef,
|
||||||
childrenAsData,
|
// onListMouseDown,
|
||||||
values,
|
// itemPrefixCls,
|
||||||
height,
|
// setActive,
|
||||||
itemHeight,
|
// onSelectValue,
|
||||||
menuItemSelectedIcon,
|
// memoFlattenOptions,
|
||||||
notFoundContent,
|
// $slots,
|
||||||
virtual,
|
// } = this as any;
|
||||||
fieldNames,
|
const { id, notFoundContent, onPopupScroll } = baseProps;
|
||||||
onScroll,
|
const { menuItemSelectedIcon, rawValues, fieldNames, virtual, listHeight, listItemHeight } =
|
||||||
onMouseenter,
|
props;
|
||||||
} = this.$props;
|
|
||||||
const renderOption = $slots.option;
|
const renderOption = slots.option;
|
||||||
const { activeIndex } = this.state;
|
const { activeIndex } = state;
|
||||||
const omitFieldNameList = Object.values(fillFieldNames(fieldNames));
|
const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]);
|
||||||
// ========================== Render ==========================
|
// ========================== Render ==========================
|
||||||
if (memoFlattenOptions.length === 0) {
|
if (memoFlattenOptions.value.length === 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="listbox"
|
||||||
|
id={`${id}_list`}
|
||||||
|
class={`${itemPrefixCls.value}-empty`}
|
||||||
|
onMousedown={onListMouseDown}
|
||||||
|
>
|
||||||
|
{notFoundContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
role="listbox"
|
<div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}>
|
||||||
id={`${id}_list`}
|
{renderItem(activeIndex - 1)}
|
||||||
class={`${itemPrefixCls}-empty`}
|
{renderItem(activeIndex)}
|
||||||
onMousedown={onListMouseDown}
|
{renderItem(activeIndex + 1)}
|
||||||
>
|
</div>
|
||||||
{notFoundContent}
|
<List
|
||||||
</div>
|
itemKey="key"
|
||||||
);
|
ref={listRef}
|
||||||
}
|
data={memoFlattenOptions.value}
|
||||||
return (
|
height={listHeight}
|
||||||
<>
|
itemHeight={listItemHeight}
|
||||||
<div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}>
|
fullHeight={false}
|
||||||
{renderItem(activeIndex - 1)}
|
onMousedown={onListMouseDown}
|
||||||
{renderItem(activeIndex)}
|
onScroll={onPopupScroll}
|
||||||
{renderItem(activeIndex + 1)}
|
virtual={virtual}
|
||||||
</div>
|
v-slots={{
|
||||||
<List
|
default: (item, itemIndex) => {
|
||||||
itemKey="key"
|
const { group, groupOption, data, label, value } = item;
|
||||||
ref={listRef}
|
const { key } = data;
|
||||||
data={memoFlattenOptions}
|
// Group
|
||||||
height={height}
|
if (group) {
|
||||||
itemHeight={itemHeight}
|
return (
|
||||||
fullHeight={false}
|
<div class={classNames(itemPrefixCls.value, `${itemPrefixCls.value}-group`)}>
|
||||||
onMousedown={onListMouseDown}
|
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
||||||
onScroll={onScroll}
|
</div>
|
||||||
virtual={virtual}
|
);
|
||||||
onMouseenter={onMouseenter}
|
}
|
||||||
v-slots={{
|
|
||||||
default: ({ group, groupOption, data, label, value }, itemIndex) => {
|
const {
|
||||||
const { key } = data;
|
disabled,
|
||||||
// Group
|
title,
|
||||||
if (group) {
|
children,
|
||||||
|
style,
|
||||||
|
class: cls,
|
||||||
|
className,
|
||||||
|
...otherProps
|
||||||
|
} = data;
|
||||||
|
const passedProps = omit(otherProps, omitFieldNameList);
|
||||||
|
// Option
|
||||||
|
const selected = rawValues.has(value);
|
||||||
|
|
||||||
|
const optionPrefixCls = `${itemPrefixCls.value}-option`;
|
||||||
|
const optionClassName = classNames(
|
||||||
|
itemPrefixCls.value,
|
||||||
|
optionPrefixCls,
|
||||||
|
cls,
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
[`${optionPrefixCls}-grouped`]: groupOption,
|
||||||
|
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
||||||
|
[`${optionPrefixCls}-disabled`]: disabled,
|
||||||
|
[`${optionPrefixCls}-selected`]: selected,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedLabel = getLabel(item);
|
||||||
|
|
||||||
|
const iconVisible =
|
||||||
|
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
|
||||||
|
|
||||||
|
const content = mergedLabel || value;
|
||||||
|
// https://github.com/ant-design/ant-design/issues/26717
|
||||||
|
let optionTitle =
|
||||||
|
typeof content === 'string' || typeof content === 'number'
|
||||||
|
? content.toString()
|
||||||
|
: undefined;
|
||||||
|
if (title !== undefined) {
|
||||||
|
optionTitle = title;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classNames(itemPrefixCls, `${itemPrefixCls}-group`)}>
|
<div
|
||||||
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
{...passedProps}
|
||||||
|
aria-selected={selected}
|
||||||
|
class={optionClassName}
|
||||||
|
title={optionTitle}
|
||||||
|
onMousemove={e => {
|
||||||
|
if (otherProps.onMousemove) {
|
||||||
|
otherProps.onMousemove(e);
|
||||||
|
}
|
||||||
|
if (activeIndex === itemIndex || disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActive(itemIndex);
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
if (!disabled) {
|
||||||
|
onSelectValue(value);
|
||||||
|
}
|
||||||
|
if (otherProps.onClick) {
|
||||||
|
otherProps.onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<div class={`${optionPrefixCls}-content`}>
|
||||||
|
{renderOption ? renderOption(data) : content}
|
||||||
|
</div>
|
||||||
|
{isValidElement(menuItemSelectedIcon) || selected}
|
||||||
|
{iconVisible && (
|
||||||
|
<TransBtn
|
||||||
|
class={`${itemPrefixCls.value}-option-state`}
|
||||||
|
customizeIcon={menuItemSelectedIcon}
|
||||||
|
customizeIconProps={{ isSelected: selected }}
|
||||||
|
>
|
||||||
|
{selected ? '✓' : null}
|
||||||
|
</TransBtn>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
}}
|
||||||
const {
|
></List>
|
||||||
disabled,
|
</>
|
||||||
title,
|
);
|
||||||
children,
|
};
|
||||||
style,
|
|
||||||
class: cls,
|
|
||||||
className,
|
|
||||||
...otherProps
|
|
||||||
} = data;
|
|
||||||
const passedProps = omit(otherProps, omitFieldNameList);
|
|
||||||
// Option
|
|
||||||
const selected = values.has(value);
|
|
||||||
|
|
||||||
const optionPrefixCls = `${itemPrefixCls}-option`;
|
|
||||||
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, cls, className, {
|
|
||||||
[`${optionPrefixCls}-grouped`]: groupOption,
|
|
||||||
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
|
||||||
[`${optionPrefixCls}-disabled`]: disabled,
|
|
||||||
[`${optionPrefixCls}-selected`]: selected,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mergedLabel = childrenAsData ? children : label;
|
|
||||||
|
|
||||||
const iconVisible =
|
|
||||||
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
|
|
||||||
|
|
||||||
const content = mergedLabel || value;
|
|
||||||
// https://github.com/ant-design/ant-design/issues/26717
|
|
||||||
let optionTitle =
|
|
||||||
typeof content === 'string' || typeof content === 'number'
|
|
||||||
? content.toString()
|
|
||||||
: undefined;
|
|
||||||
if (title !== undefined) {
|
|
||||||
optionTitle = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...passedProps}
|
|
||||||
aria-selected={selected}
|
|
||||||
class={optionClassName}
|
|
||||||
title={optionTitle}
|
|
||||||
onMousemove={e => {
|
|
||||||
if (otherProps.onMousemove) {
|
|
||||||
otherProps.onMousemove(e);
|
|
||||||
}
|
|
||||||
if (activeIndex === itemIndex || disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setActive(itemIndex);
|
|
||||||
}}
|
|
||||||
onClick={e => {
|
|
||||||
if (!disabled) {
|
|
||||||
onSelectValue(value);
|
|
||||||
}
|
|
||||||
if (otherProps.onClick) {
|
|
||||||
otherProps.onClick(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div class={`${optionPrefixCls}-content`}>
|
|
||||||
{renderOption ? renderOption(data) : content}
|
|
||||||
</div>
|
|
||||||
{isValidElement(menuItemSelectedIcon) || selected}
|
|
||||||
{iconVisible && (
|
|
||||||
<TransBtn
|
|
||||||
class={`${itemPrefixCls}-option-state`}
|
|
||||||
customizeIcon={menuItemSelectedIcon}
|
|
||||||
customizeIconProps={{ isSelected: selected }}
|
|
||||||
>
|
|
||||||
{selected ? '✓' : null}
|
|
||||||
</TransBtn>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
></List>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
OptionList.props = OptionListProps;
|
|
||||||
|
|
||||||
export default OptionList;
|
export default OptionList;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* To match accessibility requirement, we always provide an input in the component.
|
* To match accessibility requirement, we always provide an input in the component.
|
||||||
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
|
* Other element will not set `tabindex` to avoid `onBlur` sequence problem.
|
||||||
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||||
*
|
*
|
||||||
* ref:
|
* ref:
|
||||||
|
|
@ -29,76 +29,614 @@
|
||||||
* - `combobox` mode not support `optionLabelProp`
|
* - `combobox` mode not support `optionLabelProp`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { OptionsType as SelectOptionsType } from './interface';
|
import BaseSelect, { baseSelectPropsWithoutPrivate, isMultiple } from './BaseSelect';
|
||||||
import SelectOptionList from './OptionList';
|
import type { DisplayValueType, BaseSelectRef, BaseSelectProps } from './BaseSelect';
|
||||||
import Option from './Option';
|
import OptionList from './OptionList';
|
||||||
import OptGroup from './OptGroup';
|
import useOptions from './hooks/useOptions';
|
||||||
import { convertChildrenToData as convertSelectChildrenToData } from './utils/legacyUtil';
|
import type { SelectContextProps } from './SelectContext';
|
||||||
import {
|
import { useProvideSelectProps } from './SelectContext';
|
||||||
getLabeledValue as getSelectLabeledValue,
|
import useId from './hooks/useId';
|
||||||
filterOptions as selectDefaultFilterOptions,
|
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
|
||||||
isValueDisabled as isSelectValueDisabled,
|
|
||||||
findValueOption as findSelectValueOption,
|
|
||||||
flattenOptions,
|
|
||||||
fillOptionsWithMissingValue,
|
|
||||||
} from './utils/valueUtil';
|
|
||||||
import type { SelectProps } from './generate';
|
|
||||||
import generateSelector, { selectBaseProps } from './generate';
|
|
||||||
import type { DefaultValueType } from './interface/generator';
|
|
||||||
import warningProps from './utils/warningPropsUtil';
|
import warningProps from './utils/warningPropsUtil';
|
||||||
import { defineComponent, ref } from 'vue';
|
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 RefSelect = generateSelector<SelectOptionsType[number]>({
|
const OMIT_DOM_PROPS = ['inputValue'];
|
||||||
prefixCls: 'rc-select',
|
|
||||||
components: {
|
|
||||||
optionList: SelectOptionList as any,
|
|
||||||
},
|
|
||||||
convertChildrenToData: convertSelectChildrenToData,
|
|
||||||
flattenOptions,
|
|
||||||
getLabeledValue: getSelectLabeledValue,
|
|
||||||
filterOptions: selectDefaultFilterOptions,
|
|
||||||
isValueDisabled: isSelectValueDisabled,
|
|
||||||
findValueOption: findSelectValueOption,
|
|
||||||
warningProps,
|
|
||||||
fillOptionsWithMissingValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ExportedSelectProps<T extends DefaultValueType = DefaultValueType> = SelectProps<
|
export type OnActiveValue = (
|
||||||
SelectOptionsType[number],
|
active: RawValueType,
|
||||||
T
|
index: number,
|
||||||
>;
|
info?: { source?: 'keyboard' | 'mouse' },
|
||||||
|
) => void;
|
||||||
|
|
||||||
export function selectProps<T>() {
|
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
|
||||||
return selectBaseProps<SelectOptionsType[number], T>();
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Select = defineComponent({
|
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',
|
name: 'Select',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
Option,
|
props: initDefaultProps(selectProps(), {
|
||||||
OptGroup,
|
prefixCls: 'vc-select',
|
||||||
props: RefSelect.props,
|
autoClearSearchValue: true,
|
||||||
setup(props, { attrs, expose, slots }) {
|
listHeight: 200,
|
||||||
const selectRef = ref();
|
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({
|
expose({
|
||||||
focus: () => {
|
focus() {
|
||||||
selectRef.value?.focus();
|
selectRef.value?.focus();
|
||||||
},
|
},
|
||||||
blur: () => {
|
blur() {
|
||||||
selectRef.value?.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 () => {
|
||||||
return (
|
return (
|
||||||
<RefSelect
|
<BaseSelect
|
||||||
ref={selectRef}
|
{...pickProps.value}
|
||||||
{...(props as any)}
|
|
||||||
{...attrs}
|
{...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}
|
v-slots={slots}
|
||||||
children={slots.default?.() || []}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export default Select;
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
import Trigger from '../vc-trigger';
|
import Trigger from '../vc-trigger';
|
||||||
import PropTypes from '../_util/vue-types';
|
import PropTypes from '../_util/vue-types';
|
||||||
import { getSlot } from '../_util/props-util';
|
|
||||||
import classNames from '../_util/classNames';
|
import classNames from '../_util/classNames';
|
||||||
import createRef from '../_util/createRef';
|
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
import { computed, ref, defineComponent } from 'vue';
|
||||||
import type { RenderDOMFunc } from './interface';
|
|
||||||
import type { DropdownRender } from './interface/generator';
|
|
||||||
import type { Placement } from './generate';
|
|
||||||
import type { VueNode } from '../_util/type';
|
import type { VueNode } from '../_util/type';
|
||||||
|
import type { DropdownRender, Placement, RenderDOMFunc } from './BaseSelect';
|
||||||
|
|
||||||
const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => {
|
const getBuiltInPlacements = (adjustX: number) => {
|
||||||
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
|
|
||||||
const adjustX = typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bottomLeft: {
|
bottomLeft: {
|
||||||
points: ['tl', 'bl'],
|
points: ['tl', 'bl'],
|
||||||
|
|
@ -49,6 +42,19 @@ const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAdjustX = (
|
||||||
|
adjustXDependencies: Pick<SelectTriggerProps, 'autoAdjustOverflow' | 'dropdownMatchSelectWidth'>,
|
||||||
|
) => {
|
||||||
|
const { autoAdjustOverflow, dropdownMatchSelectWidth } = adjustXDependencies;
|
||||||
|
if (!!autoAdjustOverflow) return 1;
|
||||||
|
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
|
||||||
|
return typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
|
||||||
|
};
|
||||||
|
export interface RefTriggerProps {
|
||||||
|
getPopupElement: () => HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SelectTriggerProps {
|
export interface SelectTriggerProps {
|
||||||
prefixCls: string;
|
prefixCls: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
@ -66,103 +72,122 @@ export interface SelectTriggerProps {
|
||||||
getPopupContainer?: RenderDOMFunc;
|
getPopupContainer?: RenderDOMFunc;
|
||||||
dropdownAlign: object;
|
dropdownAlign: object;
|
||||||
empty: boolean;
|
empty: boolean;
|
||||||
|
autoAdjustOverflow?: boolean;
|
||||||
getTriggerDOMNode: () => any;
|
getTriggerDOMNode: () => any;
|
||||||
|
onPopupVisibleChange?: (visible: boolean) => void;
|
||||||
|
|
||||||
|
onPopupMouseEnter: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
||||||
name: 'SelectTrigger',
|
name: 'SelectTrigger',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
created() {
|
props: {
|
||||||
this.popupRef = createRef();
|
dropdownAlign: PropTypes.object,
|
||||||
},
|
visible: PropTypes.looseBool,
|
||||||
|
disabled: PropTypes.looseBool,
|
||||||
|
dropdownClassName: PropTypes.string,
|
||||||
|
dropdownStyle: PropTypes.object,
|
||||||
|
placement: PropTypes.string,
|
||||||
|
empty: PropTypes.looseBool,
|
||||||
|
autoAdjustOverflow: PropTypes.looseBool,
|
||||||
|
prefixCls: PropTypes.string,
|
||||||
|
popupClassName: PropTypes.string,
|
||||||
|
animation: PropTypes.string,
|
||||||
|
transitionName: PropTypes.string,
|
||||||
|
getPopupContainer: PropTypes.func,
|
||||||
|
dropdownRender: PropTypes.func,
|
||||||
|
containerWidth: PropTypes.number,
|
||||||
|
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
|
||||||
|
popupElement: PropTypes.any,
|
||||||
|
direction: PropTypes.string,
|
||||||
|
getTriggerDOMNode: PropTypes.func,
|
||||||
|
onPopupVisibleChange: PropTypes.func,
|
||||||
|
onPopupMouseEnter: PropTypes.func,
|
||||||
|
} as any,
|
||||||
|
setup(props, { slots, attrs, expose }) {
|
||||||
|
const builtInPlacements = computed(() => {
|
||||||
|
const { autoAdjustOverflow, dropdownMatchSelectWidth } = props;
|
||||||
|
return getBuiltInPlacements(
|
||||||
|
getAdjustX({
|
||||||
|
autoAdjustOverflow,
|
||||||
|
dropdownMatchSelectWidth,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const popupRef = ref();
|
||||||
|
expose({
|
||||||
|
getPopupElement: () => {
|
||||||
|
return popupRef.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
const { empty = false, ...restProps } = { ...props, ...attrs };
|
||||||
|
const {
|
||||||
|
visible,
|
||||||
|
dropdownAlign,
|
||||||
|
prefixCls,
|
||||||
|
popupElement,
|
||||||
|
dropdownClassName,
|
||||||
|
dropdownStyle,
|
||||||
|
direction = 'ltr',
|
||||||
|
placement,
|
||||||
|
dropdownMatchSelectWidth,
|
||||||
|
containerWidth,
|
||||||
|
dropdownRender,
|
||||||
|
animation,
|
||||||
|
transitionName,
|
||||||
|
getPopupContainer,
|
||||||
|
getTriggerDOMNode,
|
||||||
|
onPopupVisibleChange,
|
||||||
|
onPopupMouseEnter,
|
||||||
|
} = restProps as SelectTriggerProps;
|
||||||
|
const dropdownPrefixCls = `${prefixCls}-dropdown`;
|
||||||
|
|
||||||
methods: {
|
let popupNode = popupElement;
|
||||||
getPopupElement() {
|
if (dropdownRender) {
|
||||||
return this.popupRef.current;
|
popupNode = dropdownRender({ menuNode: popupElement, props });
|
||||||
},
|
}
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
|
||||||
const { empty = false, ...props } = { ...this.$props, ...this.$attrs };
|
|
||||||
const {
|
|
||||||
visible,
|
|
||||||
dropdownAlign,
|
|
||||||
prefixCls,
|
|
||||||
popupElement,
|
|
||||||
dropdownClassName,
|
|
||||||
dropdownStyle,
|
|
||||||
direction = 'ltr',
|
|
||||||
placement,
|
|
||||||
dropdownMatchSelectWidth,
|
|
||||||
containerWidth,
|
|
||||||
dropdownRender,
|
|
||||||
animation,
|
|
||||||
transitionName,
|
|
||||||
getPopupContainer,
|
|
||||||
getTriggerDOMNode,
|
|
||||||
} = props as SelectTriggerProps;
|
|
||||||
const dropdownPrefixCls = `${prefixCls}-dropdown`;
|
|
||||||
|
|
||||||
let popupNode = popupElement;
|
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
|
||||||
if (dropdownRender) {
|
|
||||||
popupNode = dropdownRender({ menuNode: popupElement, props });
|
|
||||||
}
|
|
||||||
|
|
||||||
const builtInPlacements = getBuiltInPlacements(dropdownMatchSelectWidth);
|
if (typeof dropdownMatchSelectWidth === 'number') {
|
||||||
|
popupStyle.width = `${dropdownMatchSelectWidth}px`;
|
||||||
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
|
} else if (dropdownMatchSelectWidth) {
|
||||||
|
popupStyle.width = `${containerWidth}px`;
|
||||||
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
|
}
|
||||||
|
return (
|
||||||
if (typeof dropdownMatchSelectWidth === 'number') {
|
<Trigger
|
||||||
popupStyle.width = `${dropdownMatchSelectWidth}px`;
|
{...props}
|
||||||
} else if (dropdownMatchSelectWidth) {
|
showAction={[]}
|
||||||
popupStyle.width = `${containerWidth}px`;
|
hideAction={[]}
|
||||||
}
|
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
||||||
return (
|
builtinPlacements={builtInPlacements.value}
|
||||||
<Trigger
|
prefixCls={dropdownPrefixCls}
|
||||||
{...props}
|
popupTransitionName={mergedTransitionName}
|
||||||
showAction={[]}
|
popupAlign={dropdownAlign}
|
||||||
hideAction={[]}
|
popupVisible={visible}
|
||||||
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
getPopupContainer={getPopupContainer}
|
||||||
builtinPlacements={builtInPlacements}
|
popupClassName={classNames(dropdownClassName, {
|
||||||
prefixCls={dropdownPrefixCls}
|
[`${dropdownPrefixCls}-empty`]: empty,
|
||||||
popupTransitionName={mergedTransitionName}
|
})}
|
||||||
popup={<div ref={this.popupRef}>{popupNode}</div>}
|
popupStyle={popupStyle}
|
||||||
popupAlign={dropdownAlign}
|
getTriggerDOMNode={getTriggerDOMNode}
|
||||||
popupVisible={visible}
|
onPopupVisibleChange={onPopupVisibleChange}
|
||||||
getPopupContainer={getPopupContainer}
|
v-slots={{
|
||||||
popupClassName={classNames(dropdownClassName, {
|
default: slots.default,
|
||||||
[`${dropdownPrefixCls}-empty`]: empty,
|
popup: () => (
|
||||||
})}
|
<div ref={popupRef} onMouseenter={onPopupMouseEnter}>
|
||||||
popupStyle={popupStyle}
|
{popupNode}
|
||||||
getTriggerDOMNode={getTriggerDOMNode}
|
</div>
|
||||||
>
|
),
|
||||||
{getSlot(this)[0]}
|
}}
|
||||||
</Trigger>
|
></Trigger>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
SelectTrigger.props = {
|
|
||||||
dropdownAlign: PropTypes.object,
|
|
||||||
visible: PropTypes.looseBool,
|
|
||||||
disabled: PropTypes.looseBool,
|
|
||||||
dropdownClassName: PropTypes.string,
|
|
||||||
dropdownStyle: PropTypes.object,
|
|
||||||
placement: PropTypes.string,
|
|
||||||
empty: PropTypes.looseBool,
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
popupClassName: PropTypes.string,
|
|
||||||
animation: PropTypes.string,
|
|
||||||
transitionName: PropTypes.string,
|
|
||||||
getPopupContainer: PropTypes.func,
|
|
||||||
dropdownRender: PropTypes.func,
|
|
||||||
containerWidth: PropTypes.number,
|
|
||||||
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
|
|
||||||
popupElement: PropTypes.any,
|
|
||||||
direction: PropTypes.string,
|
|
||||||
getTriggerDOMNode: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectTrigger;
|
export default SelectTrigger;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ interface InputProps {
|
||||||
autofocus: boolean;
|
autofocus: boolean;
|
||||||
autocomplete: string;
|
autocomplete: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
accessibilityIndex: number;
|
activeDescendantId?: string;
|
||||||
value: string;
|
value: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
tabindex: number | string;
|
tabindex: number | string;
|
||||||
|
|
@ -45,7 +45,7 @@ const Input = defineComponent({
|
||||||
autofocus: PropTypes.looseBool,
|
autofocus: PropTypes.looseBool,
|
||||||
autocomplete: PropTypes.string,
|
autocomplete: PropTypes.string,
|
||||||
editable: PropTypes.looseBool,
|
editable: PropTypes.looseBool,
|
||||||
accessibilityIndex: PropTypes.number,
|
activeDescendantId: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
open: PropTypes.looseBool,
|
open: PropTypes.looseBool,
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
|
@ -86,7 +86,7 @@ const Input = defineComponent({
|
||||||
autofocus,
|
autofocus,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
editable,
|
editable,
|
||||||
accessibilityIndex,
|
activeDescendantId,
|
||||||
value,
|
value,
|
||||||
onKeydown,
|
onKeydown,
|
||||||
onMousedown,
|
onMousedown,
|
||||||
|
|
@ -131,7 +131,7 @@ const Input = defineComponent({
|
||||||
'aria-owns': `${id}_list`,
|
'aria-owns': `${id}_list`,
|
||||||
'aria-autocomplete': 'list',
|
'aria-autocomplete': 'list',
|
||||||
'aria-controls': `${id}_list`,
|
'aria-controls': `${id}_list`,
|
||||||
'aria-activedescendant': `${id}_list_${accessibilityIndex}`,
|
'aria-activedescendant': activeDescendantId,
|
||||||
...attrs,
|
...attrs,
|
||||||
value: editable ? value : '',
|
value: editable ? value : '',
|
||||||
readonly: !editable,
|
readonly: !editable,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,4 @@
|
||||||
import TransBtn from '../TransBtn';
|
import TransBtn from '../TransBtn';
|
||||||
import type {
|
|
||||||
LabelValueType,
|
|
||||||
RawValueType,
|
|
||||||
CustomTagProps,
|
|
||||||
DefaultValueType,
|
|
||||||
DisplayLabelValueType,
|
|
||||||
} from '../interface/generator';
|
|
||||||
import type { RenderNode } from '../interface';
|
|
||||||
import type { InnerSelectorProps } from './interface';
|
import type { InnerSelectorProps } from './interface';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
import type { Ref, PropType } from 'vue';
|
import type { Ref, PropType } from 'vue';
|
||||||
|
|
@ -16,6 +8,8 @@ import pickAttrs from '../../_util/pickAttrs';
|
||||||
import PropTypes from '../../_util/vue-types';
|
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 { BaseOptionType } from '../Select';
|
||||||
|
|
||||||
type SelectorProps = InnerSelectorProps & {
|
type SelectorProps = InnerSelectorProps & {
|
||||||
// Icon
|
// Icon
|
||||||
|
|
@ -24,7 +18,7 @@ type SelectorProps = InnerSelectorProps & {
|
||||||
// Tags
|
// Tags
|
||||||
maxTagCount?: number | 'responsive';
|
maxTagCount?: number | 'responsive';
|
||||||
maxTagTextLength?: number;
|
maxTagTextLength?: number;
|
||||||
maxTagPlaceholder?: VueNode | ((omittedValues: LabelValueType[]) => VueNode);
|
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
||||||
tokenSeparators?: string[];
|
tokenSeparators?: string[];
|
||||||
tagRender?: (props: CustomTagProps) => VueNode;
|
tagRender?: (props: CustomTagProps) => VueNode;
|
||||||
onToggleOpen: any;
|
onToggleOpen: any;
|
||||||
|
|
@ -33,7 +27,7 @@ type SelectorProps = InnerSelectorProps & {
|
||||||
choiceTransitionName?: string;
|
choiceTransitionName?: string;
|
||||||
|
|
||||||
// Event
|
// Event
|
||||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
onRemove: (value: DisplayValueType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
|
|
@ -49,7 +43,7 @@ const props = {
|
||||||
showSearch: PropTypes.looseBool,
|
showSearch: PropTypes.looseBool,
|
||||||
autofocus: PropTypes.looseBool,
|
autofocus: PropTypes.looseBool,
|
||||||
autocomplete: PropTypes.string,
|
autocomplete: PropTypes.string,
|
||||||
accessibilityIndex: PropTypes.number,
|
activeDescendantId: PropTypes.string,
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
|
||||||
removeIcon: PropTypes.any,
|
removeIcon: PropTypes.any,
|
||||||
|
|
@ -58,12 +52,12 @@ const props = {
|
||||||
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
maxTagTextLength: PropTypes.number,
|
maxTagTextLength: PropTypes.number,
|
||||||
maxTagPlaceholder: PropTypes.any.def(
|
maxTagPlaceholder: PropTypes.any.def(
|
||||||
() => (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
() => (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`,
|
||||||
),
|
),
|
||||||
tagRender: PropTypes.func,
|
tagRender: PropTypes.func,
|
||||||
|
|
||||||
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
||||||
onSelect: PropTypes.func,
|
onRemove: PropTypes.func,
|
||||||
onInputChange: PropTypes.func,
|
onInputChange: PropTypes.func,
|
||||||
onInputPaste: PropTypes.func,
|
onInputPaste: PropTypes.func,
|
||||||
onInputKeyDown: PropTypes.func,
|
onInputKeyDown: PropTypes.func,
|
||||||
|
|
@ -111,6 +105,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
// ===================== Render ======================
|
// ===================== Render ======================
|
||||||
// >>> Render Selector Node. Includes Item & Rest
|
// >>> Render Selector Node. Includes Item & Rest
|
||||||
function defaultRenderSelector(
|
function defaultRenderSelector(
|
||||||
|
title: VueNode,
|
||||||
content: VueNode,
|
content: VueNode,
|
||||||
itemDisabled: boolean,
|
itemDisabled: boolean,
|
||||||
closable?: boolean,
|
closable?: boolean,
|
||||||
|
|
@ -122,9 +117,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
||||||
})}
|
})}
|
||||||
title={
|
title={
|
||||||
typeof content === 'string' || typeof content === 'number'
|
typeof title === 'string' || typeof title === 'number' ? title.toString() : undefined
|
||||||
? content.toString()
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
||||||
|
|
@ -143,17 +136,17 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
}
|
}
|
||||||
|
|
||||||
function customizeRenderSelector(
|
function customizeRenderSelector(
|
||||||
value: DefaultValueType,
|
value: RawValueType,
|
||||||
content: VueNode,
|
content: VueNode,
|
||||||
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({
|
||||||
|
|
@ -162,12 +155,14 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
disabled: itemDisabled,
|
disabled: itemDisabled,
|
||||||
closable,
|
closable,
|
||||||
onClose,
|
onClose,
|
||||||
|
option,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItem({ disabled: itemDisabled, label, value }: DisplayLabelValueType) {
|
function renderItem(valueItem: DisplayValueType) {
|
||||||
|
const { disabled: itemDisabled, label, value, option } = valueItem;
|
||||||
const closable = !props.disabled && !itemDisabled;
|
const closable = !props.disabled && !itemDisabled;
|
||||||
|
|
||||||
let displayLabel = label;
|
let displayLabel = label;
|
||||||
|
|
@ -183,24 +178,22 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
}
|
}
|
||||||
const onClose = (event?: MouseEvent) => {
|
const onClose = (event?: MouseEvent) => {
|
||||||
if (event) event.stopPropagation();
|
if (event) event.stopPropagation();
|
||||||
props.onSelect(value, { selected: false });
|
props.onRemove?.(valueItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
return typeof props.tagRender === 'function'
|
return typeof props.tagRender === 'function'
|
||||||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
|
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
|
||||||
: defaultRenderSelector(displayLabel, itemDisabled, closable, onClose);
|
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRest(omittedValues: DisplayLabelValueType[]) {
|
function renderRest(omittedValues: DisplayValueType[]) {
|
||||||
const {
|
const { maxTagPlaceholder = omittedValues => `+ ${omittedValues.length} ...` } = props;
|
||||||
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
|
||||||
} = props;
|
|
||||||
const content =
|
const content =
|
||||||
typeof maxTagPlaceholder === 'function'
|
typeof maxTagPlaceholder === 'function'
|
||||||
? maxTagPlaceholder(omittedValues)
|
? maxTagPlaceholder(omittedValues)
|
||||||
: maxTagPlaceholder;
|
: maxTagPlaceholder;
|
||||||
|
|
||||||
return defaultRenderSelector(content, false);
|
return defaultRenderSelector(content, content, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -214,7 +207,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
disabled,
|
disabled,
|
||||||
autofocus,
|
autofocus,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
accessibilityIndex,
|
activeDescendantId,
|
||||||
tabindex,
|
tabindex,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onInputPaste,
|
onInputPaste,
|
||||||
|
|
@ -241,7 +234,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||||
autofocus={autofocus}
|
autofocus={autofocus}
|
||||||
autocomplete={autocomplete}
|
autocomplete={autocomplete}
|
||||||
editable={inputEditable.value}
|
editable={inputEditable.value}
|
||||||
accessibilityIndex={accessibilityIndex}
|
activeDescendantId={activeDescendantId}
|
||||||
value={inputValue.value}
|
value={inputValue.value}
|
||||||
onKeydown={onInputKeyDown}
|
onKeydown={onInputKeyDown}
|
||||||
onMousedown={onInputMouseDown}
|
onMousedown={onInputMouseDown}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import type { VueNode } from '../../_util/type';
|
||||||
interface SelectorProps extends InnerSelectorProps {
|
interface SelectorProps extends InnerSelectorProps {
|
||||||
inputElement: VueNode;
|
inputElement: VueNode;
|
||||||
activeValue: string;
|
activeValue: string;
|
||||||
backfill?: boolean;
|
|
||||||
}
|
}
|
||||||
const props = {
|
const props = {
|
||||||
inputElement: PropTypes.any,
|
inputElement: PropTypes.any,
|
||||||
|
|
@ -25,7 +24,7 @@ const props = {
|
||||||
showSearch: PropTypes.looseBool,
|
showSearch: PropTypes.looseBool,
|
||||||
autofocus: PropTypes.looseBool,
|
autofocus: PropTypes.looseBool,
|
||||||
autocomplete: PropTypes.string,
|
autocomplete: PropTypes.string,
|
||||||
accessibilityIndex: PropTypes.number,
|
activeDescendantId: PropTypes.string,
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
activeValue: PropTypes.string,
|
activeValue: PropTypes.string,
|
||||||
backfill: PropTypes.looseBool,
|
backfill: PropTypes.looseBool,
|
||||||
|
|
@ -64,7 +63,7 @@ const SingleSelector = defineComponent<SelectorProps>({
|
||||||
|
|
||||||
// Not show text when closed expect combobox mode
|
// Not show text when closed expect combobox mode
|
||||||
const hasTextInput = computed(() =>
|
const hasTextInput = computed(() =>
|
||||||
props.mode !== 'combobox' && !props.open ? false : !!inputValue.value,
|
props.mode !== 'combobox' && !props.open && !props.showSearch ? false : !!inputValue.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
|
|
@ -74,6 +73,18 @@ const SingleSelector = defineComponent<SelectorProps>({
|
||||||
: undefined;
|
: undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renderPlaceholder = () => {
|
||||||
|
if (props.values[0]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const hiddenStyle = hasTextInput.value ? { visibility: 'hidden' as const } : undefined;
|
||||||
|
return (
|
||||||
|
<span class={`${props.prefixCls}-selection-placeholder`} style={hiddenStyle}>
|
||||||
|
{props.placeholder}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const {
|
const {
|
||||||
inputElement,
|
inputElement,
|
||||||
|
|
@ -84,9 +95,8 @@ const SingleSelector = defineComponent<SelectorProps>({
|
||||||
disabled,
|
disabled,
|
||||||
autofocus,
|
autofocus,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
accessibilityIndex,
|
activeDescendantId,
|
||||||
open,
|
open,
|
||||||
placeholder,
|
|
||||||
tabindex,
|
tabindex,
|
||||||
onInputKeyDown,
|
onInputKeyDown,
|
||||||
onInputMouseDown,
|
onInputMouseDown,
|
||||||
|
|
@ -126,7 +136,7 @@ const SingleSelector = defineComponent<SelectorProps>({
|
||||||
autofocus={autofocus}
|
autofocus={autofocus}
|
||||||
autocomplete={autocomplete}
|
autocomplete={autocomplete}
|
||||||
editable={inputEditable.value}
|
editable={inputEditable.value}
|
||||||
accessibilityIndex={accessibilityIndex}
|
activeDescendantId={activeDescendantId}
|
||||||
value={inputValue.value}
|
value={inputValue.value}
|
||||||
onKeydown={onInputKeyDown}
|
onKeydown={onInputKeyDown}
|
||||||
onMousedown={onInputMouseDown}
|
onMousedown={onInputMouseDown}
|
||||||
|
|
@ -150,9 +160,7 @@ const SingleSelector = defineComponent<SelectorProps>({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Display placeholder */}
|
{/* Display placeholder */}
|
||||||
{!item && !hasTextInput.value && (
|
{renderPlaceholder()}
|
||||||
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
import KeyCode from '../../_util/KeyCode';
|
import KeyCode from '../../_util/KeyCode';
|
||||||
import MultipleSelector from './MultipleSelector';
|
import MultipleSelector from './MultipleSelector';
|
||||||
import SingleSelector from './SingleSelector';
|
import SingleSelector from './SingleSelector';
|
||||||
import type { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
|
import type { CustomTagProps, DisplayValueType, Mode, RenderNode } from '../BaseSelect';
|
||||||
import type { RenderNode, Mode } from '../interface';
|
import { isValidateOpenKey } from '../utils/keyUtil';
|
||||||
import useLock from '../hooks/useLock';
|
import useLock from '../hooks/useLock';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
|
@ -20,22 +20,22 @@ import createRef from '../../_util/createRef';
|
||||||
import PropTypes from '../../_util/vue-types';
|
import PropTypes from '../../_util/vue-types';
|
||||||
import type { VueNode } from '../../_util/type';
|
import type { VueNode } from '../../_util/type';
|
||||||
import type { EventHandler } from '../../_util/EventInterface';
|
import type { EventHandler } from '../../_util/EventInterface';
|
||||||
|
import type { ScrollTo } from '../../vc-virtual-list/List';
|
||||||
|
|
||||||
export interface SelectorProps {
|
export interface SelectorProps {
|
||||||
id: string;
|
id: string;
|
||||||
prefixCls: string;
|
prefixCls: string;
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
/** Display in the Selector value, it's not same as `value` prop */
|
values: DisplayValueType[];
|
||||||
values: LabelValueType[];
|
multiple?: boolean;
|
||||||
multiple: boolean;
|
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
activeValue: string;
|
activeValue: string;
|
||||||
inputElement: VueNode;
|
inputElement: VueNode;
|
||||||
|
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
accessibilityIndex: number;
|
activeDescendantId?: string;
|
||||||
tabindex?: number | string;
|
tabindex?: number | string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: VueNode;
|
placeholder?: VueNode;
|
||||||
|
|
@ -44,7 +44,7 @@ export interface SelectorProps {
|
||||||
// Tags
|
// Tags
|
||||||
maxTagCount?: number | 'responsive';
|
maxTagCount?: number | 'responsive';
|
||||||
maxTagTextLength?: number;
|
maxTagTextLength?: number;
|
||||||
maxTagPlaceholder?: VueNode | ((omittedValues: LabelValueType[]) => VueNode);
|
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
||||||
tagRender?: (props: CustomTagProps) => VueNode;
|
tagRender?: (props: CustomTagProps) => VueNode;
|
||||||
|
|
||||||
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
||||||
|
|
@ -57,7 +57,7 @@ export interface SelectorProps {
|
||||||
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
||||||
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
|
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
|
||||||
onSearchSubmit: (searchText: string) => void;
|
onSearchSubmit: (searchText: string) => void;
|
||||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
onRemove: (value: DisplayValueType) => void;
|
||||||
onInputKeyDown?: (e: KeyboardEvent) => void;
|
onInputKeyDown?: (e: KeyboardEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -66,6 +66,11 @@ export interface SelectorProps {
|
||||||
*/
|
*/
|
||||||
domRef: () => HTMLDivElement;
|
domRef: () => HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
export interface RefSelectorProps {
|
||||||
|
focus: () => void;
|
||||||
|
blur: () => void;
|
||||||
|
scrollTo?: ScrollTo;
|
||||||
|
}
|
||||||
|
|
||||||
const Selector = defineComponent<SelectorProps>({
|
const Selector = defineComponent<SelectorProps>({
|
||||||
name: 'Selector',
|
name: 'Selector',
|
||||||
|
|
@ -84,7 +89,7 @@ const Selector = defineComponent<SelectorProps>({
|
||||||
inputElement: PropTypes.any,
|
inputElement: PropTypes.any,
|
||||||
|
|
||||||
autofocus: PropTypes.looseBool,
|
autofocus: PropTypes.looseBool,
|
||||||
accessibilityIndex: PropTypes.number,
|
activeDescendantId: PropTypes.string,
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
disabled: PropTypes.looseBool,
|
disabled: PropTypes.looseBool,
|
||||||
placeholder: PropTypes.any,
|
placeholder: PropTypes.any,
|
||||||
|
|
@ -106,7 +111,7 @@ const Selector = defineComponent<SelectorProps>({
|
||||||
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
||||||
onSearch: PropTypes.func,
|
onSearch: PropTypes.func,
|
||||||
onSearchSubmit: PropTypes.func,
|
onSearchSubmit: PropTypes.func,
|
||||||
onSelect: PropTypes.func,
|
onRemove: PropTypes.func,
|
||||||
onInputKeyDown: { type: Function as PropType<EventHandler> },
|
onInputKeyDown: { type: Function as PropType<EventHandler> },
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,7 +120,7 @@ const Selector = defineComponent<SelectorProps>({
|
||||||
*/
|
*/
|
||||||
domRef: PropTypes.func,
|
domRef: PropTypes.func,
|
||||||
} as any,
|
} as any,
|
||||||
setup(props) {
|
setup(props, { expose }) {
|
||||||
const inputRef = createRef();
|
const inputRef = createRef();
|
||||||
let compositionStatus = false;
|
let compositionStatus = false;
|
||||||
|
|
||||||
|
|
@ -139,7 +144,7 @@ const Selector = defineComponent<SelectorProps>({
|
||||||
props.onSearchSubmit((event.target as HTMLInputElement).value);
|
props.onSearchSubmit((event.target as HTMLInputElement).value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (![KeyCode.SHIFT, KeyCode.TAB, KeyCode.BACKSPACE, KeyCode.ESC].includes(which)) {
|
if (isValidateOpenKey(which)) {
|
||||||
props.onToggleOpen(true);
|
props.onToggleOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -227,58 +232,44 @@ const Selector = defineComponent<SelectorProps>({
|
||||||
props.onToggleOpen();
|
props.onToggleOpen();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
expose({
|
||||||
return {
|
|
||||||
focus: () => {
|
focus: () => {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
},
|
},
|
||||||
blur: () => {
|
blur: () => {
|
||||||
inputRef.current.blur();
|
inputRef.current.blur();
|
||||||
},
|
},
|
||||||
onMousedown,
|
});
|
||||||
onClick,
|
|
||||||
onInputPaste,
|
return () => {
|
||||||
inputRef,
|
const { prefixCls, domRef, mode } = props as SelectorProps;
|
||||||
onInternalInputKeyDown,
|
const sharedProps = {
|
||||||
onInternalInputMouseDown,
|
inputRef,
|
||||||
onInputChange,
|
onInputKeyDown: onInternalInputKeyDown,
|
||||||
onInputCompositionEnd,
|
onInputMouseDown: onInternalInputMouseDown,
|
||||||
onInputCompositionStart,
|
onInputChange,
|
||||||
|
onInputPaste,
|
||||||
|
onInputCompositionStart,
|
||||||
|
onInputCompositionEnd,
|
||||||
|
};
|
||||||
|
const selectNode =
|
||||||
|
mode === 'multiple' || mode === 'tags' ? (
|
||||||
|
<MultipleSelector {...props} {...sharedProps} />
|
||||||
|
) : (
|
||||||
|
<SingleSelector {...props} {...sharedProps} />
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={domRef}
|
||||||
|
class={`${prefixCls}-selector`}
|
||||||
|
onClick={onClick}
|
||||||
|
onMousedown={onMousedown}
|
||||||
|
>
|
||||||
|
{selectNode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render() {
|
|
||||||
const { prefixCls, domRef, multiple } = this.$props as SelectorProps;
|
|
||||||
const {
|
|
||||||
onMousedown,
|
|
||||||
onClick,
|
|
||||||
inputRef,
|
|
||||||
onInputPaste,
|
|
||||||
onInternalInputKeyDown,
|
|
||||||
onInternalInputMouseDown,
|
|
||||||
onInputChange,
|
|
||||||
onInputCompositionStart,
|
|
||||||
onInputCompositionEnd,
|
|
||||||
} = this as any;
|
|
||||||
const sharedProps = {
|
|
||||||
inputRef,
|
|
||||||
onInputKeyDown: onInternalInputKeyDown,
|
|
||||||
onInputMouseDown: onInternalInputMouseDown,
|
|
||||||
onInputChange,
|
|
||||||
onInputPaste,
|
|
||||||
onInputCompositionStart,
|
|
||||||
onInputCompositionEnd,
|
|
||||||
};
|
|
||||||
const selectNode = multiple ? (
|
|
||||||
<MultipleSelector {...this.$props} {...sharedProps} />
|
|
||||||
) : (
|
|
||||||
<SingleSelector {...this.$props} {...sharedProps} />
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div ref={domRef} class={`${prefixCls}-selector`} onClick={onClick} onMousedown={onMousedown}>
|
|
||||||
{selectNode}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Selector;
|
export default Selector;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { RefObject } from '../../_util/createRef';
|
import type { RefObject } from '../../_util/createRef';
|
||||||
import type { Mode } from '../interface';
|
|
||||||
import type { LabelValueType } from '../interface/generator';
|
|
||||||
import type { EventHandler } from '../../_util/EventInterface';
|
import type { EventHandler } from '../../_util/EventInterface';
|
||||||
import type { VueNode } from '../../_util/type';
|
import type { VueNode } from '../../_util/type';
|
||||||
|
import type { Mode, DisplayValueType } from '../BaseSelect';
|
||||||
|
|
||||||
export interface InnerSelectorProps {
|
export interface InnerSelectorProps {
|
||||||
prefixCls: string;
|
prefixCls: string;
|
||||||
|
|
@ -13,10 +13,10 @@ export interface InnerSelectorProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
autocomplete?: string;
|
autocomplete?: string;
|
||||||
values: LabelValueType[];
|
values: DisplayValueType[];
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
accessibilityIndex: number;
|
activeDescendantId: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
tabindex?: number | string;
|
tabindex?: number | string;
|
||||||
onInputKeyDown: EventHandler;
|
onInputKeyDown: EventHandler;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
import type { FunctionalComponent } from 'vue';
|
||||||
import type { VueNode } from '../_util/type';
|
import type { VueNode } from '../_util/type';
|
||||||
import PropTypes from '../_util/vue-types';
|
import PropTypes from '../_util/vue-types';
|
||||||
|
import type { RenderNode } from './BaseSelect';
|
||||||
|
|
||||||
export interface TransBtnProps {
|
export interface TransBtnProps {
|
||||||
class: string;
|
class: string;
|
||||||
customizeIcon: VueNode | ((props?: any) => VueNode);
|
customizeIcon: RenderNode;
|
||||||
customizeIconProps?: any;
|
customizeIconProps?: any;
|
||||||
onMousedown?: (payload: MouseEvent) => void;
|
onMousedown?: (payload: MouseEvent) => void;
|
||||||
onClick?: (payload: MouseEvent) => void;
|
onClick?: (payload: MouseEvent) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,345 +0,0 @@
|
||||||
@select-prefix: ~'rc-select';
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-without-border() {
|
|
||||||
.@{select-prefix}-selection-search-input {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background: rgba(255, 0, 0, 0.2);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix} {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 12px;
|
|
||||||
width: 100px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&-disabled {
|
|
||||||
&,
|
|
||||||
& input {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-selector {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-show-arrow&-loading {
|
|
||||||
.@{select-prefix}-arrow {
|
|
||||||
&-icon::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 100%;
|
|
||||||
border: 2px solid #999;
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
transform: none;
|
|
||||||
margin-top: 4px;
|
|
||||||
|
|
||||||
animation: rcSelectLoadingIcon 0.5s infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== Selector ===============
|
|
||||||
.@{select-prefix}-selection-placeholder {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== Search ===============
|
|
||||||
.@{select-prefix}-selection-search-input {
|
|
||||||
appearance: none;
|
|
||||||
|
|
||||||
&::-webkit-search-cancel-button {
|
|
||||||
display: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------- Single ----------------
|
|
||||||
&-single {
|
|
||||||
.@{select-prefix}-selector {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.@{select-prefix}-selection-search {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-selection-item,
|
|
||||||
.@{select-prefix}-selection-placeholder {
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
left: 3px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not customize
|
|
||||||
&:not(.@{select-prefix}-customize-input) {
|
|
||||||
.@{select-prefix}-selector {
|
|
||||||
padding: 1px;
|
|
||||||
border: 1px solid #000;
|
|
||||||
|
|
||||||
.search-input-without-border();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------- Multiple ---------------
|
|
||||||
&-multiple .@{select-prefix}-selector {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 1px;
|
|
||||||
border: 1px solid #000;
|
|
||||||
|
|
||||||
.@{select-prefix}-selection-item {
|
|
||||||
flex: none;
|
|
||||||
background: #bbb;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-right: 2px;
|
|
||||||
padding: 0 8px;
|
|
||||||
|
|
||||||
&-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-selection-search {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&-input,
|
|
||||||
&-mirror {
|
|
||||||
padding: 1px;
|
|
||||||
font-family: system-ui;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-mirror {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 999;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: none;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-without-border();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================ Icons ================
|
|
||||||
&-allow-clear {
|
|
||||||
&.@{select-prefix}-multiple .@{select-prefix}-selector {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-clear {
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-show-arrow {
|
|
||||||
&.@{select-prefix}-multiple .@{select-prefix}-selector {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-arrow {
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
right: 5px;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
&-icon::after {
|
|
||||||
content: '';
|
|
||||||
border: 5px solid transparent;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
display: inline-block;
|
|
||||||
border-top-color: #999;
|
|
||||||
transform: translateY(5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============== Focused ===============
|
|
||||||
&-focused {
|
|
||||||
.@{select-prefix}-selector {
|
|
||||||
border-color: blue !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== Dropdown ===============
|
|
||||||
&-dropdown {
|
|
||||||
border: 1px solid green;
|
|
||||||
min-height: 100px;
|
|
||||||
position: absolute;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
&-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============== Option ================
|
|
||||||
&-item {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.5;
|
|
||||||
padding: 4px 16px;
|
|
||||||
|
|
||||||
// >>> Group
|
|
||||||
&-group {
|
|
||||||
color: #999;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// >>> Option
|
|
||||||
&-option {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&-grouped {
|
|
||||||
padding-left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-item-option-state {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 4px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------- Active -------
|
|
||||||
&-active {
|
|
||||||
background: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------ Disabled ------
|
|
||||||
&-disabled {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// >>> Empty
|
|
||||||
&-empty {
|
|
||||||
text-align: center;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-selection__choice-zoom {
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-selection__choice-zoom-appear {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.5);
|
|
||||||
|
|
||||||
&&-active {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.@{select-prefix}-selection__choice-zoom-leave {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
|
|
||||||
&&-active {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.effect() {
|
|
||||||
animation-duration: 0.3s;
|
|
||||||
animation-fill-mode: both;
|
|
||||||
transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{select-prefix}-dropdown {
|
|
||||||
&-slide-up-enter,
|
|
||||||
&-slide-up-appear {
|
|
||||||
.effect();
|
|
||||||
opacity: 0;
|
|
||||||
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-slide-up-leave {
|
|
||||||
.effect();
|
|
||||||
opacity: 1;
|
|
||||||
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft,
|
|
||||||
&-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft {
|
|
||||||
animation-name: rcSelectDropdownSlideUpIn;
|
|
||||||
animation-play-state: running;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft {
|
|
||||||
animation-name: rcSelectDropdownSlideUpOut;
|
|
||||||
animation-play-state: running;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-slide-up-enter&-slide-up-enter-active&-placement-topLeft,
|
|
||||||
&-slide-up-appear&-slide-up-appear-active&-placement-topLeft {
|
|
||||||
animation-name: rcSelectDropdownSlideDownIn;
|
|
||||||
animation-play-state: running;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-slide-up-leave&-slide-up-leave-active&-placement-topLeft {
|
|
||||||
animation-name: rcSelectDropdownSlideDownOut;
|
|
||||||
animation-play-state: running;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rcSelectDropdownSlideUpIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform-origin: 0% 0%;
|
|
||||||
transform: scaleY(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform-origin: 0% 0%;
|
|
||||||
transform: scaleY(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes rcSelectDropdownSlideUpOut {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
transform-origin: 0% 0%;
|
|
||||||
transform: scaleY(1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
transform-origin: 0% 0%;
|
|
||||||
transform: scaleY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rcSelectLoadingIcon {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,36 +0,0 @@
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import type { DisplayLabelValueType } from '../interface/generator';
|
|
||||||
|
|
||||||
export default function useCacheDisplayValue(
|
|
||||||
values: Ref<DisplayLabelValueType[]>,
|
|
||||||
): ComputedRef<DisplayLabelValueType[]> {
|
|
||||||
let prevValues = [...values.value];
|
|
||||||
|
|
||||||
const mergedValues = computed(() => {
|
|
||||||
// Create value - label map
|
|
||||||
const valueLabels = new Map();
|
|
||||||
prevValues.forEach(({ value, label }) => {
|
|
||||||
if (value !== label) {
|
|
||||||
valueLabels.set(value, label);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const resultValues = values.value.map(item => {
|
|
||||||
const cacheLabel = valueLabels.get(item.value);
|
|
||||||
if (item.isCacheable && cacheLabel) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
label: cacheLabel,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
|
|
||||||
prevValues = resultValues;
|
|
||||||
return resultValues;
|
|
||||||
});
|
|
||||||
|
|
||||||
return mergedValues;
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import type { RawValueType, FlattenOptionsType, Key } from '../interface/generator';
|
|
||||||
|
|
||||||
export default function useCacheOptions<
|
|
||||||
OptionType extends {
|
|
||||||
value?: RawValueType;
|
|
||||||
label?: any;
|
|
||||||
key?: Key;
|
|
||||||
disabled?: boolean;
|
|
||||||
},
|
|
||||||
>(options: Ref) {
|
|
||||||
const optionMap = computed(() => {
|
|
||||||
const map: Map<RawValueType, FlattenOptionsType<OptionType>[number]> = new Map();
|
|
||||||
options.value.forEach(item => {
|
|
||||||
const { value } = item;
|
|
||||||
map.set(value, item);
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getValueOption = (valueList: RawValueType[]) =>
|
|
||||||
valueList.map(value => optionMap.value.get(value)).filter(Boolean);
|
|
||||||
|
|
||||||
return getValueOption;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import type { ExportedSelectProps } from './Select';
|
import type { SelectProps } from './Select';
|
||||||
import Select, { selectProps } from './Select';
|
import Select, { selectProps } from './Select';
|
||||||
import Option from './Option';
|
import Option from './Option';
|
||||||
import OptGroup from './OptGroup';
|
import OptGroup from './OptGroup';
|
||||||
import { selectBaseProps } from './generate';
|
import BaseSelect from './BaseSelect';
|
||||||
import type { ExtractPropTypes } from 'vue';
|
import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
|
||||||
|
import useBaseProps from './hooks/useBaseProps';
|
||||||
|
|
||||||
export type SelectProps<T = any> = Partial<ExtractPropTypes<ExportedSelectProps<T>>>;
|
export { Option, OptGroup, selectProps, BaseSelect, useBaseProps };
|
||||||
export { Option, OptGroup, selectBaseProps, selectProps };
|
export type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps };
|
||||||
|
|
||||||
export default Select;
|
export default Select;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,3 @@
|
||||||
import type {
|
|
||||||
RawValueType,
|
|
||||||
GetLabeledValue,
|
|
||||||
LabelValueType,
|
|
||||||
DefaultValueType,
|
|
||||||
FlattenOptionsType,
|
|
||||||
} from '../interface/generator';
|
|
||||||
|
|
||||||
export function toArray<T>(value: T | T[]): T[] {
|
export function toArray<T>(value: T | T[]): T[] {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -13,116 +5,8 @@ export function toArray<T>(value: T | T[]): T[] {
|
||||||
return value !== undefined ? [value] : [];
|
return value !== undefined ? [value] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert outer props value into internal value
|
|
||||||
*/
|
|
||||||
export function toInnerValue(
|
|
||||||
value: DefaultValueType,
|
|
||||||
{ labelInValue, combobox }: { labelInValue: boolean; combobox: boolean },
|
|
||||||
): [RawValueType[], Map<RawValueType, LabelValueType>] {
|
|
||||||
const valueMap = new Map<RawValueType, LabelValueType>();
|
|
||||||
if (value === undefined || (value === '' && combobox)) {
|
|
||||||
return [[], valueMap];
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = Array.isArray(value) ? value : [value];
|
|
||||||
|
|
||||||
let rawValues = values as RawValueType[];
|
|
||||||
|
|
||||||
if (labelInValue) {
|
|
||||||
rawValues = (values as LabelValueType[])
|
|
||||||
.filter(item => item !== null)
|
|
||||||
.map((itemValue: LabelValueType) => {
|
|
||||||
const { key, value: val } = itemValue;
|
|
||||||
const finalVal = val !== undefined ? val : key;
|
|
||||||
valueMap.set(finalVal, itemValue);
|
|
||||||
return finalVal;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [rawValues, valueMap];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert internal value into out event value
|
|
||||||
*/
|
|
||||||
export function toOuterValues<FOT extends FlattenOptionsType>(
|
|
||||||
valueList: RawValueType[],
|
|
||||||
{
|
|
||||||
optionLabelProp,
|
|
||||||
labelInValue,
|
|
||||||
prevValueMap,
|
|
||||||
options,
|
|
||||||
getLabeledValue,
|
|
||||||
}: {
|
|
||||||
optionLabelProp: string;
|
|
||||||
labelInValue: boolean;
|
|
||||||
getLabeledValue: GetLabeledValue<FOT>;
|
|
||||||
options: FOT;
|
|
||||||
prevValueMap: Map<RawValueType, LabelValueType>;
|
|
||||||
},
|
|
||||||
): RawValueType[] | LabelValueType[] | DefaultValueType {
|
|
||||||
let values: DefaultValueType = valueList;
|
|
||||||
|
|
||||||
if (labelInValue) {
|
|
||||||
values = values.map(val =>
|
|
||||||
getLabeledValue(val, {
|
|
||||||
options,
|
|
||||||
prevValueMap,
|
|
||||||
labelInValue,
|
|
||||||
optionLabelProp,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeLastEnabledValue<
|
|
||||||
T extends { disabled?: boolean },
|
|
||||||
P extends RawValueType | object,
|
|
||||||
>(measureValues: T[], values: P[]): { values: P[]; removedValue: P } {
|
|
||||||
const newValues = [...values];
|
|
||||||
|
|
||||||
let removeIndex: number;
|
|
||||||
for (removeIndex = measureValues.length - 1; removeIndex >= 0; removeIndex -= 1) {
|
|
||||||
if (!measureValues[removeIndex].disabled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let removedValue = null;
|
|
||||||
|
|
||||||
if (removeIndex !== -1) {
|
|
||||||
removedValue = newValues[removeIndex];
|
|
||||||
newValues.splice(removeIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
values: newValues,
|
|
||||||
removedValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isClient =
|
export const isClient =
|
||||||
typeof window !== 'undefined' && window.document && window.document.documentElement;
|
typeof window !== 'undefined' && window.document && window.document.documentElement;
|
||||||
|
|
||||||
/** Is client side and not jsdom */
|
/** Is client side and not jsdom */
|
||||||
export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient;
|
export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient;
|
||||||
|
|
||||||
let uuid = 0;
|
|
||||||
/** Get unique id for accessibility usage */
|
|
||||||
export function getUUID(): number | string {
|
|
||||||
let retId: string | number;
|
|
||||||
|
|
||||||
// Test never reach
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (isBrowserClient) {
|
|
||||||
retId = uuid;
|
|
||||||
uuid += 1;
|
|
||||||
} else {
|
|
||||||
retId = 'TEST_OR_SSR';
|
|
||||||
}
|
|
||||||
|
|
||||||
return retId;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { flattenChildren, isValidElement } from '../../_util/props-util';
|
import { flattenChildren, isValidElement } from '../../_util/props-util';
|
||||||
import type { VNode } from 'vue';
|
import type { VNode } from 'vue';
|
||||||
import type { OptionData, OptionGroupData, OptionsType } from '../interface';
|
import type { BaseOptionType, DefaultOptionType } from '../Select';
|
||||||
import type { VueNode } from '../../_util/type';
|
import type { VueNode } from '../../_util/type';
|
||||||
|
|
||||||
function convertNodeToOption(node: VNode): OptionData {
|
function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||||
|
node: VNode,
|
||||||
|
): OptionType {
|
||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
children,
|
children,
|
||||||
|
|
@ -18,13 +20,16 @@ function convertNodeToOption(node: VNode): OptionData {
|
||||||
value: value !== undefined ? value : key,
|
value: value !== undefined ? value : key,
|
||||||
children: child,
|
children: child,
|
||||||
disabled: disabled || disabled === '', // support <a-select-option disabled />
|
disabled: disabled || disabled === '', // support <a-select-option disabled />
|
||||||
...(restProps as Omit<typeof restProps, 'key'>),
|
...(restProps as any),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertChildrenToData(nodes: VueNode, optionOnly = false): OptionsType {
|
export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||||
|
nodes: VueNode[],
|
||||||
|
optionOnly = false,
|
||||||
|
): OptionType[] {
|
||||||
const dd = flattenChildren(nodes as [])
|
const dd = flattenChildren(nodes as [])
|
||||||
.map((node: VNode, index: number): OptionData | OptionGroupData | null => {
|
.map((node: VNode, index: number): OptionType | null => {
|
||||||
if (!isValidElement(node) || !node.type) {
|
if (!isValidElement(node) || !node.type) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,8 @@
|
||||||
|
import type { BaseOptionType, DefaultOptionType, RawValueType, FieldNames } from '../Select';
|
||||||
import { warning } from '../../vc-util/warning';
|
import { warning } from '../../vc-util/warning';
|
||||||
import { cloneVNode, isVNode } from 'vue';
|
import type { FlattenOptionData } from '../interface';
|
||||||
import type {
|
|
||||||
OptionsType as SelectOptionsType,
|
|
||||||
OptionData,
|
|
||||||
OptionGroupData,
|
|
||||||
FlattenOptionData,
|
|
||||||
FieldNames,
|
|
||||||
} from '../interface';
|
|
||||||
import type {
|
|
||||||
LabelValueType,
|
|
||||||
FilterFunc,
|
|
||||||
RawValueType,
|
|
||||||
GetLabeledValue,
|
|
||||||
DefaultValueType,
|
|
||||||
} from '../interface/generator';
|
|
||||||
|
|
||||||
import { toArray } from './commonUtil';
|
function getKey(data: BaseOptionType, index: number) {
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
|
|
||||||
function getKey(data: OptionData | OptionGroupData, index: number) {
|
|
||||||
const { key } = data;
|
const { key } = data;
|
||||||
let value: RawValueType;
|
let value: RawValueType;
|
||||||
|
|
||||||
|
|
@ -35,11 +19,11 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
|
||||||
return `rc-index-key-${index}`;
|
return `rc-index-key-${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillFieldNames(fieldNames?: FieldNames) {
|
export function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean) {
|
||||||
const { label, value, options } = fieldNames || {};
|
const { label, value, options } = fieldNames || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: label || 'label',
|
label: label || (childrenAsData ? 'children' : 'label'),
|
||||||
value: value || 'value',
|
value: value || 'value',
|
||||||
options: options || 'options',
|
options: options || 'options',
|
||||||
};
|
};
|
||||||
|
|
@ -50,38 +34,43 @@ export function fillFieldNames(fieldNames?: FieldNames) {
|
||||||
* We use `optionOnly` here is aim to avoid user use nested option group.
|
* We use `optionOnly` here is aim to avoid user use nested option group.
|
||||||
* Here is simply set `key` to the index if not provided.
|
* Here is simply set `key` to the index if not provided.
|
||||||
*/
|
*/
|
||||||
export function flattenOptions(
|
export function flattenOptions<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||||
options: SelectOptionsType,
|
options: OptionType[],
|
||||||
{ fieldNames }: { fieldNames?: FieldNames } = {},
|
{ fieldNames, childrenAsData }: { fieldNames?: FieldNames; childrenAsData?: boolean } = {},
|
||||||
): FlattenOptionData[] {
|
): FlattenOptionData<OptionType>[] {
|
||||||
const flattenList: FlattenOptionData[] = [];
|
const flattenList: FlattenOptionData<OptionType>[] = [];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
label: fieldLabel,
|
label: fieldLabel,
|
||||||
value: fieldValue,
|
value: fieldValue,
|
||||||
options: fieldOptions,
|
options: fieldOptions,
|
||||||
} = fillFieldNames(fieldNames);
|
} = fillFieldNames(fieldNames, false);
|
||||||
|
|
||||||
function dig(list: SelectOptionsType, isGroupOption: boolean) {
|
function dig(list: OptionType[], isGroupOption: boolean) {
|
||||||
list.forEach(data => {
|
list.forEach(data => {
|
||||||
const label = data[fieldLabel];
|
const label = data[fieldLabel];
|
||||||
|
|
||||||
if (isGroupOption || !(fieldOptions in data)) {
|
if (isGroupOption || !(fieldOptions in data)) {
|
||||||
|
const value = data[fieldValue];
|
||||||
// Option
|
// Option
|
||||||
flattenList.push({
|
flattenList.push({
|
||||||
key: getKey(data, flattenList.length),
|
key: getKey(data, flattenList.length),
|
||||||
groupOption: isGroupOption,
|
groupOption: isGroupOption,
|
||||||
data,
|
data,
|
||||||
label,
|
label,
|
||||||
value: data[fieldValue],
|
value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
let grpLabel = label;
|
||||||
|
if (grpLabel === undefined && childrenAsData) {
|
||||||
|
grpLabel = data.label;
|
||||||
|
}
|
||||||
// Option Group
|
// Option Group
|
||||||
flattenList.push({
|
flattenList.push({
|
||||||
key: getKey(data, flattenList.length),
|
key: getKey(data, flattenList.length),
|
||||||
group: true,
|
group: true,
|
||||||
data,
|
data,
|
||||||
label,
|
label: grpLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
dig(data[fieldOptions], true);
|
dig(data[fieldOptions], true);
|
||||||
|
|
@ -97,7 +86,7 @@ export function flattenOptions(
|
||||||
/**
|
/**
|
||||||
* Inject `props` into `option` for legacy usage
|
* Inject `props` into `option` for legacy usage
|
||||||
*/
|
*/
|
||||||
function injectPropsWithOption<T>(option: T): T {
|
export function injectPropsWithOption<T>(option: T): T {
|
||||||
const newOption = { ...option };
|
const newOption = { ...option };
|
||||||
if (!('props' in newOption)) {
|
if (!('props' in newOption)) {
|
||||||
Object.defineProperty(newOption, 'props', {
|
Object.defineProperty(newOption, 'props', {
|
||||||
|
|
@ -114,154 +103,6 @@ function injectPropsWithOption<T>(option: T): T {
|
||||||
return newOption;
|
return newOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findValueOption(
|
|
||||||
values: RawValueType[],
|
|
||||||
options: FlattenOptionData[],
|
|
||||||
{ prevValueOptions = [] }: { prevValueOptions?: OptionData[] } = {},
|
|
||||||
): OptionData[] {
|
|
||||||
const optionMap: Map<RawValueType, OptionData> = new Map();
|
|
||||||
|
|
||||||
options.forEach(({ data, group, value }) => {
|
|
||||||
if (!group) {
|
|
||||||
// Check if match
|
|
||||||
optionMap.set(value, data as OptionData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return values.map(val => {
|
|
||||||
let option = optionMap.get(val);
|
|
||||||
|
|
||||||
// Fallback to try to find prev options
|
|
||||||
if (!option) {
|
|
||||||
option = {
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
...prevValueOptions.find(opt => opt._INTERNAL_OPTION_VALUE_ === val),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return injectPropsWithOption(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getLabeledValue: GetLabeledValue<FlattenOptionData[]> = (
|
|
||||||
value,
|
|
||||||
{ options, prevValueMap, labelInValue, optionLabelProp },
|
|
||||||
) => {
|
|
||||||
const item = findValueOption([value], options)[0];
|
|
||||||
const result: LabelValueType = {
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevValItem: LabelValueType = labelInValue ? prevValueMap.get(value) : undefined;
|
|
||||||
|
|
||||||
if (prevValItem && typeof prevValItem === 'object' && 'label' in prevValItem) {
|
|
||||||
result.label = prevValItem.label;
|
|
||||||
|
|
||||||
if (
|
|
||||||
item &&
|
|
||||||
typeof prevValItem.label === 'string' &&
|
|
||||||
typeof item[optionLabelProp] === 'string' &&
|
|
||||||
prevValItem.label.trim() !== item[optionLabelProp].trim()
|
|
||||||
) {
|
|
||||||
warning(false, '`label` of `value` is not same as `label` in Select options.');
|
|
||||||
}
|
|
||||||
} else if (item && optionLabelProp in item) {
|
|
||||||
if (Array.isArray(item[optionLabelProp])) {
|
|
||||||
result.label = isVNode(item[optionLabelProp][0])
|
|
||||||
? cloneVNode(item[optionLabelProp][0])
|
|
||||||
: item[optionLabelProp];
|
|
||||||
} else {
|
|
||||||
result.label = item[optionLabelProp];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.label = value;
|
|
||||||
result.isCacheable = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for motion control
|
|
||||||
result.key = result.value;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
function toRawString(content: VueNode): string {
|
|
||||||
return toArray(content)
|
|
||||||
.map(item => {
|
|
||||||
if (isVNode(item)) {
|
|
||||||
return item?.el?.innerText || item?.el?.wholeText;
|
|
||||||
} else {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Filter single option if match the search text */
|
|
||||||
function getFilterFunction(optionFilterProp: string) {
|
|
||||||
return (searchValue: string, option: OptionData | OptionGroupData) => {
|
|
||||||
const lowerSearchText = searchValue.toLowerCase();
|
|
||||||
|
|
||||||
// Group label search
|
|
||||||
if ('options' in option) {
|
|
||||||
return toRawString(option.label).toLowerCase().includes(lowerSearchText);
|
|
||||||
}
|
|
||||||
// Option value search
|
|
||||||
const rawValue = option[optionFilterProp];
|
|
||||||
const value = toRawString(rawValue).toLowerCase();
|
|
||||||
return value.includes(lowerSearchText);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Filter options and return a new options by the search text */
|
|
||||||
export function filterOptions(
|
|
||||||
searchValue: string,
|
|
||||||
options: SelectOptionsType,
|
|
||||||
{
|
|
||||||
optionFilterProp,
|
|
||||||
filterOption,
|
|
||||||
}: { optionFilterProp: string; filterOption: boolean | FilterFunc<SelectOptionsType[number]> },
|
|
||||||
) {
|
|
||||||
const filteredOptions: SelectOptionsType = [];
|
|
||||||
let filterFunc: FilterFunc<SelectOptionsType[number]>;
|
|
||||||
|
|
||||||
if (filterOption === false) {
|
|
||||||
return [...options];
|
|
||||||
}
|
|
||||||
if (typeof filterOption === 'function') {
|
|
||||||
filterFunc = filterOption;
|
|
||||||
} else {
|
|
||||||
filterFunc = getFilterFunction(optionFilterProp);
|
|
||||||
}
|
|
||||||
|
|
||||||
options.forEach(item => {
|
|
||||||
// Group should check child options
|
|
||||||
if ('options' in item) {
|
|
||||||
// Check group first
|
|
||||||
const matchGroup = filterFunc(searchValue, item);
|
|
||||||
if (matchGroup) {
|
|
||||||
filteredOptions.push(item);
|
|
||||||
} else {
|
|
||||||
// Check option
|
|
||||||
const subOptions = item.options.filter(subItem => filterFunc(searchValue, subItem));
|
|
||||||
if (subOptions.length) {
|
|
||||||
filteredOptions.push({
|
|
||||||
...item,
|
|
||||||
options: subOptions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterFunc(searchValue, injectPropsWithOption(item))) {
|
|
||||||
filteredOptions.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSeparatedContent(text: string, tokens: string[]): string[] {
|
export function getSeparatedContent(text: string, tokens: string[]): string[] {
|
||||||
if (!tokens || !tokens.length) {
|
if (!tokens || !tokens.length) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -285,53 +126,3 @@ export function getSeparatedContent(text: string, tokens: string[]): string[] {
|
||||||
const list = separate(text, tokens);
|
const list = separate(text, tokens);
|
||||||
return match ? list : null;
|
return match ? list : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValueDisabled(value: RawValueType, options: FlattenOptionData[]): boolean {
|
|
||||||
const option = findValueOption([value], options)[0];
|
|
||||||
return option.disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `tags` mode should fill un-list item into the option list
|
|
||||||
*/
|
|
||||||
export function fillOptionsWithMissingValue(
|
|
||||||
options: SelectOptionsType,
|
|
||||||
value: DefaultValueType,
|
|
||||||
optionLabelProp: string,
|
|
||||||
labelInValue: boolean,
|
|
||||||
): SelectOptionsType {
|
|
||||||
const values = toArray<RawValueType | LabelValueType>(value).slice().sort();
|
|
||||||
const cloneOptions = [...options];
|
|
||||||
|
|
||||||
// Convert options value to set
|
|
||||||
const optionValues = new Set<RawValueType>();
|
|
||||||
options.forEach(opt => {
|
|
||||||
if (opt.options) {
|
|
||||||
opt.options.forEach((subOpt: OptionData) => {
|
|
||||||
optionValues.add(subOpt.value);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
optionValues.add((opt as OptionData).value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fill missing value
|
|
||||||
values.forEach(item => {
|
|
||||||
const val: RawValueType = labelInValue
|
|
||||||
? (item as LabelValueType).value
|
|
||||||
: (item as RawValueType);
|
|
||||||
|
|
||||||
if (!optionValues.has(val)) {
|
|
||||||
cloneOptions.push(
|
|
||||||
labelInValue
|
|
||||||
? {
|
|
||||||
[optionLabelProp]: (item as LabelValueType).label,
|
|
||||||
value: val,
|
|
||||||
}
|
|
||||||
: { value: val },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return cloneOptions;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import warning, { noteOnce } from '../../vc-util/warning';
|
import warning, { noteOnce } from '../../vc-util/warning';
|
||||||
import type { SelectProps } from '..';
|
|
||||||
import { convertChildrenToData } from './legacyUtil';
|
import { convertChildrenToData } from './legacyUtil';
|
||||||
import { toArray } from './commonUtil';
|
import { toArray } from './commonUtil';
|
||||||
import type { RawValueType, LabelValueType } from '../interface/generator';
|
|
||||||
import { isValidElement } from '../../_util/props-util';
|
import { isValidElement } from '../../_util/props-util';
|
||||||
import type { VNode } from 'vue';
|
import type { VNode } from 'vue';
|
||||||
|
import type { RawValueType, LabelInValueType, SelectProps } from '../Select';
|
||||||
|
import { isMultiple } from '../BaseSelect';
|
||||||
|
|
||||||
function warningProps(props: SelectProps) {
|
function warningProps(props: SelectProps) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -25,13 +25,13 @@ function warningProps(props: SelectProps) {
|
||||||
optionLabelProp,
|
optionLabelProp,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const multiple = mode === 'multiple' || mode === 'tags';
|
const multiple = isMultiple(mode);
|
||||||
const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox';
|
const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox';
|
||||||
const mergedOptions = options || convertChildrenToData(children);
|
const mergedOptions = options || convertChildrenToData(children);
|
||||||
|
|
||||||
// `tags` should not set option as disabled
|
// `tags` should not set option as disabled
|
||||||
warning(
|
warning(
|
||||||
mode !== 'tags' || mergedOptions.every((opt: any) => !opt.disabled),
|
mode !== 'tags' || mergedOptions.every((opt: { disabled?: boolean }) => !opt.disabled),
|
||||||
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
|
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ function warningProps(props: SelectProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
const values = toArray<RawValueType | LabelValueType>(value);
|
const values = toArray<RawValueType | LabelInValueType>(value);
|
||||||
warning(
|
warning(
|
||||||
!labelInValue ||
|
!labelInValue ||
|
||||||
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),
|
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
|
||||||
|
|
||||||
import type { DefaultOptionType } from './Select';
|
|
||||||
|
|
||||||
export type OptGroupProps = Omit<DefaultOptionType, 'options'>;
|
|
||||||
|
|
||||||
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
|
|
||||||
/** Legacy for check if is a Option Group */
|
|
||||||
isSelectOptGroup: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OptGroup: OptionGroupFC = () => null;
|
|
||||||
OptGroup.isSelectOptGroup = true;
|
|
||||||
OptGroup.displayName = 'ASelectOptGroup';
|
|
||||||
export default OptGroup;
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
|
||||||
|
|
||||||
import type { DefaultOptionType } from './Select';
|
|
||||||
|
|
||||||
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
|
|
||||||
/** Save for customize data */
|
|
||||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptionFC extends FunctionalComponent<OptionProps> {
|
|
||||||
/** Legacy for check if is a Option Group */
|
|
||||||
isSelectOption: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Option: OptionFC = () => null;
|
|
||||||
Option.isSelectOption = true;
|
|
||||||
Option.displayName = 'ASelectOption';
|
|
||||||
export default Option;
|
|
||||||
|
|
@ -1,378 +0,0 @@
|
||||||
import TransBtn from './TransBtn';
|
|
||||||
|
|
||||||
import KeyCode from '../_util/KeyCode';
|
|
||||||
import classNames from '../_util/classNames';
|
|
||||||
import pickAttrs from '../_util/pickAttrs';
|
|
||||||
import { isValidElement } from '../_util/props-util';
|
|
||||||
import createRef from '../_util/createRef';
|
|
||||||
import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
|
|
||||||
import List from '../vc-virtual-list';
|
|
||||||
import useMemo from '../_util/hooks/useMemo';
|
|
||||||
import { isPlatformMac } from './utils/platformUtil';
|
|
||||||
|
|
||||||
export interface RefOptionListProps {
|
|
||||||
onKeydown: (e?: KeyboardEvent) => void;
|
|
||||||
onKeyup: (e?: KeyboardEvent) => void;
|
|
||||||
scrollTo?: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
import type { EventHandler } from '../_util/EventInterface';
|
|
||||||
import omit from '../_util/omit';
|
|
||||||
import useBaseProps from './hooks/useBaseProps';
|
|
||||||
import type { RawValueType } from './Select';
|
|
||||||
import useSelectProps from './SelectContext';
|
|
||||||
// export interface OptionListProps<OptionsType extends object[]> {
|
|
||||||
export type OptionListProps = Record<string, never>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Using virtual list of option display.
|
|
||||||
* Will fallback to dom if use customize render.
|
|
||||||
*/
|
|
||||||
const OptionList = defineComponent({
|
|
||||||
name: 'OptionList',
|
|
||||||
inheritAttrs: false,
|
|
||||||
slots: ['option'],
|
|
||||||
setup(_, { expose, slots }) {
|
|
||||||
const baseProps = useBaseProps();
|
|
||||||
const props = useSelectProps();
|
|
||||||
const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`);
|
|
||||||
|
|
||||||
const memoFlattenOptions = useMemo(
|
|
||||||
() => props.flattenOptions,
|
|
||||||
[() => baseProps.open, () => props.flattenOptions],
|
|
||||||
next => next[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
// =========================== List ===========================
|
|
||||||
const listRef = createRef();
|
|
||||||
|
|
||||||
const onListMouseDown: EventHandler = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollIntoView = (index: number) => {
|
|
||||||
if (listRef.current) {
|
|
||||||
listRef.current.scrollTo({ index });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========================== Active ==========================
|
|
||||||
const getEnabledActiveIndex = (index: number, offset = 1) => {
|
|
||||||
const len = memoFlattenOptions.value.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < len; i += 1) {
|
|
||||||
const current = (index + i * offset + len) % len;
|
|
||||||
|
|
||||||
const { group, data } = memoFlattenOptions.value[current];
|
|
||||||
if (!group && !data.disabled) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
const state = reactive({
|
|
||||||
activeIndex: getEnabledActiveIndex(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
const setActive = (index: number, fromKeyboard = false) => {
|
|
||||||
state.activeIndex = index;
|
|
||||||
const info = { source: fromKeyboard ? ('keyboard' as const) : ('mouse' as const) };
|
|
||||||
|
|
||||||
// Trigger active event
|
|
||||||
const flattenItem = memoFlattenOptions.value[index];
|
|
||||||
if (!flattenItem) {
|
|
||||||
props.onActiveValue(null, -1, info);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onActiveValue(flattenItem.data.value, index, info);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto active first item when list length or searchValue changed
|
|
||||||
|
|
||||||
watch(
|
|
||||||
[() => memoFlattenOptions.value.length, () => baseProps.searchValue],
|
|
||||||
() => {
|
|
||||||
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
// Auto scroll to item position in single mode
|
|
||||||
|
|
||||||
watch(
|
|
||||||
[() => baseProps.open, () => baseProps.searchValue],
|
|
||||||
() => {
|
|
||||||
if (!baseProps.multiple && baseProps.open && props.rawValues.size === 1) {
|
|
||||||
const value = Array.from(props.rawValues)[0];
|
|
||||||
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
|
|
||||||
if (index !== -1) {
|
|
||||||
setActive(index);
|
|
||||||
nextTick(() => {
|
|
||||||
scrollIntoView(index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Force trigger scrollbar visible when open
|
|
||||||
if (baseProps.open) {
|
|
||||||
nextTick(() => {
|
|
||||||
listRef.current?.scrollTo(undefined);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true, flush: 'post' },
|
|
||||||
);
|
|
||||||
|
|
||||||
// ========================== Values ==========================
|
|
||||||
const onSelectValue = (value?: RawValueType) => {
|
|
||||||
if (value !== undefined) {
|
|
||||||
props.onSelect(value, { selected: !props.rawValues.has(value) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single mode should always close by select
|
|
||||||
if (!baseProps.multiple) {
|
|
||||||
baseProps.toggleOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getLabel = (item: Record<string, any>) => item.label;
|
|
||||||
function renderItem(index: number) {
|
|
||||||
const item = memoFlattenOptions.value[index];
|
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
const itemData = item.data || {};
|
|
||||||
const { value } = itemData;
|
|
||||||
const { group } = item;
|
|
||||||
const attrs = pickAttrs(itemData, true);
|
|
||||||
const mergedLabel = getLabel(item);
|
|
||||||
return item ? (
|
|
||||||
<div
|
|
||||||
aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
|
|
||||||
{...attrs}
|
|
||||||
key={index}
|
|
||||||
role={group ? 'presentation' : 'option'}
|
|
||||||
id={`${baseProps.id}_list_${index}`}
|
|
||||||
aria-selected={props.rawValues.has(value)}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
const onKeydown = (event: KeyboardEvent) => {
|
|
||||||
const { which, ctrlKey } = event;
|
|
||||||
switch (which) {
|
|
||||||
// >>> Arrow keys & ctrl + n/p on Mac
|
|
||||||
case KeyCode.N:
|
|
||||||
case KeyCode.P:
|
|
||||||
case KeyCode.UP:
|
|
||||||
case KeyCode.DOWN: {
|
|
||||||
let offset = 0;
|
|
||||||
if (which === KeyCode.UP) {
|
|
||||||
offset = -1;
|
|
||||||
} else if (which === KeyCode.DOWN) {
|
|
||||||
offset = 1;
|
|
||||||
} else if (isPlatformMac() && ctrlKey) {
|
|
||||||
if (which === KeyCode.N) {
|
|
||||||
offset = 1;
|
|
||||||
} else if (which === KeyCode.P) {
|
|
||||||
offset = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offset !== 0) {
|
|
||||||
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
|
|
||||||
scrollIntoView(nextActiveIndex);
|
|
||||||
setActive(nextActiveIndex, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// >>> Select
|
|
||||||
case KeyCode.ENTER: {
|
|
||||||
// value
|
|
||||||
const item = memoFlattenOptions.value[state.activeIndex];
|
|
||||||
if (item && !item.data.disabled) {
|
|
||||||
onSelectValue(item.data.value);
|
|
||||||
} else {
|
|
||||||
onSelectValue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseProps.open) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// >>> Close
|
|
||||||
case KeyCode.ESC: {
|
|
||||||
baseProps.toggleOpen(false);
|
|
||||||
if (baseProps.open) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onKeyup = () => {};
|
|
||||||
|
|
||||||
const scrollTo = (index: number) => {
|
|
||||||
scrollIntoView(index);
|
|
||||||
};
|
|
||||||
expose({
|
|
||||||
onKeydown,
|
|
||||||
onKeyup,
|
|
||||||
scrollTo,
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
// const {
|
|
||||||
// renderItem,
|
|
||||||
// listRef,
|
|
||||||
// onListMouseDown,
|
|
||||||
// itemPrefixCls,
|
|
||||||
// setActive,
|
|
||||||
// onSelectValue,
|
|
||||||
// memoFlattenOptions,
|
|
||||||
// $slots,
|
|
||||||
// } = this as any;
|
|
||||||
const { id, notFoundContent, onPopupScroll } = baseProps;
|
|
||||||
const { menuItemSelectedIcon, rawValues, fieldNames, virtual, listHeight, listItemHeight } =
|
|
||||||
props;
|
|
||||||
|
|
||||||
const renderOption = slots.option;
|
|
||||||
const { activeIndex } = state;
|
|
||||||
const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]);
|
|
||||||
// ========================== Render ==========================
|
|
||||||
if (memoFlattenOptions.value.length === 0) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="listbox"
|
|
||||||
id={`${id}_list`}
|
|
||||||
class={`${itemPrefixCls.value}-empty`}
|
|
||||||
onMousedown={onListMouseDown}
|
|
||||||
>
|
|
||||||
{notFoundContent}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}>
|
|
||||||
{renderItem(activeIndex - 1)}
|
|
||||||
{renderItem(activeIndex)}
|
|
||||||
{renderItem(activeIndex + 1)}
|
|
||||||
</div>
|
|
||||||
<List
|
|
||||||
itemKey="key"
|
|
||||||
ref={listRef}
|
|
||||||
data={memoFlattenOptions.value}
|
|
||||||
height={listHeight}
|
|
||||||
itemHeight={listItemHeight}
|
|
||||||
fullHeight={false}
|
|
||||||
onMousedown={onListMouseDown}
|
|
||||||
onScroll={onPopupScroll}
|
|
||||||
virtual={virtual}
|
|
||||||
v-slots={{
|
|
||||||
default: (item, itemIndex) => {
|
|
||||||
const { group, groupOption, data, label, value } = item;
|
|
||||||
const { key } = data;
|
|
||||||
// Group
|
|
||||||
if (group) {
|
|
||||||
return (
|
|
||||||
<div class={classNames(itemPrefixCls.value, `${itemPrefixCls.value}-group`)}>
|
|
||||||
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
disabled,
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
style,
|
|
||||||
class: cls,
|
|
||||||
className,
|
|
||||||
...otherProps
|
|
||||||
} = data;
|
|
||||||
const passedProps = omit(otherProps, omitFieldNameList);
|
|
||||||
// Option
|
|
||||||
const selected = rawValues.has(value);
|
|
||||||
|
|
||||||
const optionPrefixCls = `${itemPrefixCls.value}-option`;
|
|
||||||
const optionClassName = classNames(
|
|
||||||
itemPrefixCls.value,
|
|
||||||
optionPrefixCls,
|
|
||||||
cls,
|
|
||||||
className,
|
|
||||||
{
|
|
||||||
[`${optionPrefixCls}-grouped`]: groupOption,
|
|
||||||
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
|
||||||
[`${optionPrefixCls}-disabled`]: disabled,
|
|
||||||
[`${optionPrefixCls}-selected`]: selected,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mergedLabel = getLabel(item);
|
|
||||||
|
|
||||||
const iconVisible =
|
|
||||||
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
|
|
||||||
|
|
||||||
const content = mergedLabel || value;
|
|
||||||
// https://github.com/ant-design/ant-design/issues/26717
|
|
||||||
let optionTitle =
|
|
||||||
typeof content === 'string' || typeof content === 'number'
|
|
||||||
? content.toString()
|
|
||||||
: undefined;
|
|
||||||
if (title !== undefined) {
|
|
||||||
optionTitle = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...passedProps}
|
|
||||||
aria-selected={selected}
|
|
||||||
class={optionClassName}
|
|
||||||
title={optionTitle}
|
|
||||||
onMousemove={e => {
|
|
||||||
if (otherProps.onMousemove) {
|
|
||||||
otherProps.onMousemove(e);
|
|
||||||
}
|
|
||||||
if (activeIndex === itemIndex || disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setActive(itemIndex);
|
|
||||||
}}
|
|
||||||
onClick={e => {
|
|
||||||
if (!disabled) {
|
|
||||||
onSelectValue(value);
|
|
||||||
}
|
|
||||||
if (otherProps.onClick) {
|
|
||||||
otherProps.onClick(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div class={`${optionPrefixCls}-content`}>
|
|
||||||
{renderOption ? renderOption(data) : content}
|
|
||||||
</div>
|
|
||||||
{isValidElement(menuItemSelectedIcon) || selected}
|
|
||||||
{iconVisible && (
|
|
||||||
<TransBtn
|
|
||||||
class={`${itemPrefixCls.value}-option-state`}
|
|
||||||
customizeIcon={menuItemSelectedIcon}
|
|
||||||
customizeIconProps={{ isSelected: selected }}
|
|
||||||
>
|
|
||||||
{selected ? '✓' : null}
|
|
||||||
</TransBtn>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
></List>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default OptionList;
|
|
||||||
|
|
@ -1,642 +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, 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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
import Trigger from '../vc-trigger';
|
|
||||||
import PropTypes from '../_util/vue-types';
|
|
||||||
import classNames from '../_util/classNames';
|
|
||||||
import type { CSSProperties } from 'vue';
|
|
||||||
import { computed, ref, defineComponent } from 'vue';
|
|
||||||
import type { VueNode } from '../_util/type';
|
|
||||||
import type { DropdownRender, Placement, RenderDOMFunc } from './BaseSelect';
|
|
||||||
|
|
||||||
const getBuiltInPlacements = (adjustX: number) => {
|
|
||||||
return {
|
|
||||||
bottomLeft: {
|
|
||||||
points: ['tl', 'bl'],
|
|
||||||
offset: [0, 4],
|
|
||||||
overflow: {
|
|
||||||
adjustX,
|
|
||||||
adjustY: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bottomRight: {
|
|
||||||
points: ['tr', 'br'],
|
|
||||||
offset: [0, 4],
|
|
||||||
overflow: {
|
|
||||||
adjustX,
|
|
||||||
adjustY: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
topLeft: {
|
|
||||||
points: ['bl', 'tl'],
|
|
||||||
offset: [0, -4],
|
|
||||||
overflow: {
|
|
||||||
adjustX,
|
|
||||||
adjustY: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
topRight: {
|
|
||||||
points: ['br', 'tr'],
|
|
||||||
offset: [0, -4],
|
|
||||||
overflow: {
|
|
||||||
adjustX,
|
|
||||||
adjustY: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAdjustX = (
|
|
||||||
adjustXDependencies: Pick<SelectTriggerProps, 'autoAdjustOverflow' | 'dropdownMatchSelectWidth'>,
|
|
||||||
) => {
|
|
||||||
const { autoAdjustOverflow, dropdownMatchSelectWidth } = adjustXDependencies;
|
|
||||||
if (!!autoAdjustOverflow) return 1;
|
|
||||||
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
|
|
||||||
return typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
|
|
||||||
};
|
|
||||||
export interface RefTriggerProps {
|
|
||||||
getPopupElement: () => HTMLDivElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectTriggerProps {
|
|
||||||
prefixCls: string;
|
|
||||||
disabled: boolean;
|
|
||||||
visible: boolean;
|
|
||||||
popupElement: VueNode;
|
|
||||||
animation?: string;
|
|
||||||
transitionName?: string;
|
|
||||||
containerWidth: number;
|
|
||||||
placement?: Placement;
|
|
||||||
dropdownStyle: CSSProperties;
|
|
||||||
dropdownClassName: string;
|
|
||||||
direction: string;
|
|
||||||
dropdownMatchSelectWidth?: boolean | number;
|
|
||||||
dropdownRender?: DropdownRender;
|
|
||||||
getPopupContainer?: RenderDOMFunc;
|
|
||||||
dropdownAlign: object;
|
|
||||||
empty: boolean;
|
|
||||||
autoAdjustOverflow?: boolean;
|
|
||||||
getTriggerDOMNode: () => any;
|
|
||||||
onPopupVisibleChange?: (visible: boolean) => void;
|
|
||||||
|
|
||||||
onPopupMouseEnter: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
|
||||||
name: 'SelectTrigger',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
dropdownAlign: PropTypes.object,
|
|
||||||
visible: PropTypes.looseBool,
|
|
||||||
disabled: PropTypes.looseBool,
|
|
||||||
dropdownClassName: PropTypes.string,
|
|
||||||
dropdownStyle: PropTypes.object,
|
|
||||||
placement: PropTypes.string,
|
|
||||||
empty: PropTypes.looseBool,
|
|
||||||
autoAdjustOverflow: PropTypes.looseBool,
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
popupClassName: PropTypes.string,
|
|
||||||
animation: PropTypes.string,
|
|
||||||
transitionName: PropTypes.string,
|
|
||||||
getPopupContainer: PropTypes.func,
|
|
||||||
dropdownRender: PropTypes.func,
|
|
||||||
containerWidth: PropTypes.number,
|
|
||||||
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
|
|
||||||
popupElement: PropTypes.any,
|
|
||||||
direction: PropTypes.string,
|
|
||||||
getTriggerDOMNode: PropTypes.func,
|
|
||||||
onPopupVisibleChange: PropTypes.func,
|
|
||||||
onPopupMouseEnter: PropTypes.func,
|
|
||||||
} as any,
|
|
||||||
setup(props, { slots, attrs, expose }) {
|
|
||||||
const builtInPlacements = computed(() => {
|
|
||||||
const { autoAdjustOverflow, dropdownMatchSelectWidth } = props;
|
|
||||||
return getBuiltInPlacements(
|
|
||||||
getAdjustX({
|
|
||||||
autoAdjustOverflow,
|
|
||||||
dropdownMatchSelectWidth,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const popupRef = ref();
|
|
||||||
expose({
|
|
||||||
getPopupElement: () => {
|
|
||||||
return popupRef.value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
const { empty = false, ...restProps } = { ...props, ...attrs };
|
|
||||||
const {
|
|
||||||
visible,
|
|
||||||
dropdownAlign,
|
|
||||||
prefixCls,
|
|
||||||
popupElement,
|
|
||||||
dropdownClassName,
|
|
||||||
dropdownStyle,
|
|
||||||
direction = 'ltr',
|
|
||||||
placement,
|
|
||||||
dropdownMatchSelectWidth,
|
|
||||||
containerWidth,
|
|
||||||
dropdownRender,
|
|
||||||
animation,
|
|
||||||
transitionName,
|
|
||||||
getPopupContainer,
|
|
||||||
getTriggerDOMNode,
|
|
||||||
onPopupVisibleChange,
|
|
||||||
onPopupMouseEnter,
|
|
||||||
} = restProps as SelectTriggerProps;
|
|
||||||
const dropdownPrefixCls = `${prefixCls}-dropdown`;
|
|
||||||
|
|
||||||
let popupNode = popupElement;
|
|
||||||
if (dropdownRender) {
|
|
||||||
popupNode = dropdownRender({ menuNode: popupElement, props });
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
|
|
||||||
|
|
||||||
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
|
|
||||||
|
|
||||||
if (typeof dropdownMatchSelectWidth === 'number') {
|
|
||||||
popupStyle.width = `${dropdownMatchSelectWidth}px`;
|
|
||||||
} else if (dropdownMatchSelectWidth) {
|
|
||||||
popupStyle.width = `${containerWidth}px`;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Trigger
|
|
||||||
{...props}
|
|
||||||
showAction={[]}
|
|
||||||
hideAction={[]}
|
|
||||||
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
|
||||||
builtinPlacements={builtInPlacements.value}
|
|
||||||
prefixCls={dropdownPrefixCls}
|
|
||||||
popupTransitionName={mergedTransitionName}
|
|
||||||
popupAlign={dropdownAlign}
|
|
||||||
popupVisible={visible}
|
|
||||||
getPopupContainer={getPopupContainer}
|
|
||||||
popupClassName={classNames(dropdownClassName, {
|
|
||||||
[`${dropdownPrefixCls}-empty`]: empty,
|
|
||||||
})}
|
|
||||||
popupStyle={popupStyle}
|
|
||||||
getTriggerDOMNode={getTriggerDOMNode}
|
|
||||||
onPopupVisibleChange={onPopupVisibleChange}
|
|
||||||
v-slots={{
|
|
||||||
default: slots.default,
|
|
||||||
popup: () => (
|
|
||||||
<div ref={popupRef} onMouseenter={onPopupMouseEnter}>
|
|
||||||
{popupNode}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
></Trigger>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default SelectTrigger;
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
import { cloneElement } from '../../_util/vnode';
|
|
||||||
import type { VNode } from 'vue';
|
|
||||||
import { defineComponent, getCurrentInstance, inject, onMounted, withDirectives } from 'vue';
|
|
||||||
import PropTypes from '../../_util/vue-types';
|
|
||||||
import type { RefObject } from '../../_util/createRef';
|
|
||||||
import antInput from '../../_util/antInputDirective';
|
|
||||||
import classNames from '../../_util/classNames';
|
|
||||||
import type { EventHandler } from '../../_util/EventInterface';
|
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
|
|
||||||
interface InputProps {
|
|
||||||
prefixCls: string;
|
|
||||||
id: string;
|
|
||||||
inputElement: VueNode;
|
|
||||||
disabled: boolean;
|
|
||||||
autofocus: boolean;
|
|
||||||
autocomplete: string;
|
|
||||||
editable: boolean;
|
|
||||||
activeDescendantId?: string;
|
|
||||||
value: string;
|
|
||||||
open: boolean;
|
|
||||||
tabindex: number | string;
|
|
||||||
/** Pass accessibility props to input */
|
|
||||||
attrs: object;
|
|
||||||
inputRef: RefObject;
|
|
||||||
onKeydown: EventHandler;
|
|
||||||
onMousedown: EventHandler;
|
|
||||||
onChange: EventHandler;
|
|
||||||
onPaste: EventHandler;
|
|
||||||
onCompositionstart: EventHandler;
|
|
||||||
onCompositionend: EventHandler;
|
|
||||||
onFocus: EventHandler;
|
|
||||||
onBlur: EventHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Input = defineComponent({
|
|
||||||
name: 'Input',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
inputRef: PropTypes.any,
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
id: PropTypes.string,
|
|
||||||
inputElement: PropTypes.any,
|
|
||||||
disabled: PropTypes.looseBool,
|
|
||||||
autofocus: PropTypes.looseBool,
|
|
||||||
autocomplete: PropTypes.string,
|
|
||||||
editable: PropTypes.looseBool,
|
|
||||||
activeDescendantId: PropTypes.string,
|
|
||||||
value: PropTypes.string,
|
|
||||||
open: PropTypes.looseBool,
|
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
/** Pass accessibility props to input */
|
|
||||||
attrs: PropTypes.object,
|
|
||||||
onKeydown: PropTypes.func,
|
|
||||||
onMousedown: PropTypes.func,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onPaste: PropTypes.func,
|
|
||||||
onCompositionstart: PropTypes.func,
|
|
||||||
onCompositionend: PropTypes.func,
|
|
||||||
onFocus: PropTypes.func,
|
|
||||||
onBlur: PropTypes.func,
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
onMounted(() => {
|
|
||||||
const ins = getCurrentInstance();
|
|
||||||
if (props.autofocus) {
|
|
||||||
if (ins.vnode && ins.vnode.el) {
|
|
||||||
ins.vnode.el.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
blurTimeout: null,
|
|
||||||
VCSelectContainerEvent: inject('VCSelectContainerEvent') as any,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
prefixCls,
|
|
||||||
id,
|
|
||||||
inputElement,
|
|
||||||
disabled,
|
|
||||||
tabindex,
|
|
||||||
autofocus,
|
|
||||||
autocomplete,
|
|
||||||
editable,
|
|
||||||
activeDescendantId,
|
|
||||||
value,
|
|
||||||
onKeydown,
|
|
||||||
onMousedown,
|
|
||||||
onChange,
|
|
||||||
onPaste,
|
|
||||||
onCompositionstart,
|
|
||||||
onCompositionend,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
open,
|
|
||||||
inputRef,
|
|
||||||
attrs,
|
|
||||||
} = this.$props as InputProps;
|
|
||||||
let inputNode: any = inputElement || withDirectives((<input />) as VNode, [[antInput]]);
|
|
||||||
|
|
||||||
const inputProps = inputNode.props || {};
|
|
||||||
const {
|
|
||||||
onKeydown: onOriginKeyDown,
|
|
||||||
onInput: onOriginInput,
|
|
||||||
onFocus: onOriginFocus,
|
|
||||||
onBlur: onOriginBlur,
|
|
||||||
onMousedown: onOriginMouseDown,
|
|
||||||
onCompositionstart: onOriginCompositionStart,
|
|
||||||
onCompositionend: onOriginCompositionEnd,
|
|
||||||
style,
|
|
||||||
} = inputProps;
|
|
||||||
inputNode = cloneElement(
|
|
||||||
inputNode,
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
ref: inputRef,
|
|
||||||
disabled,
|
|
||||||
tabindex,
|
|
||||||
autocomplete: autocomplete || 'off',
|
|
||||||
autofocus,
|
|
||||||
class: classNames(`${prefixCls}-selection-search-input`, inputNode?.props?.className),
|
|
||||||
style: { ...style, opacity: editable ? null : 0 },
|
|
||||||
role: 'combobox',
|
|
||||||
'aria-expanded': open,
|
|
||||||
'aria-haspopup': 'listbox',
|
|
||||||
'aria-owns': `${id}_list`,
|
|
||||||
'aria-autocomplete': 'list',
|
|
||||||
'aria-controls': `${id}_list`,
|
|
||||||
'aria-activedescendant': activeDescendantId,
|
|
||||||
...attrs,
|
|
||||||
value: editable ? value : '',
|
|
||||||
readonly: !editable,
|
|
||||||
unselectable: !editable ? 'on' : null,
|
|
||||||
onKeydown: (event: KeyboardEvent) => {
|
|
||||||
onKeydown(event);
|
|
||||||
if (onOriginKeyDown) {
|
|
||||||
onOriginKeyDown(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMousedown: (event: MouseEvent) => {
|
|
||||||
onMousedown(event);
|
|
||||||
if (onOriginMouseDown) {
|
|
||||||
onOriginMouseDown(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onInput: (event: Event) => {
|
|
||||||
onChange(event);
|
|
||||||
if (onOriginInput) {
|
|
||||||
onOriginInput(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCompositionstart(event: CompositionEvent) {
|
|
||||||
onCompositionstart(event);
|
|
||||||
if (onOriginCompositionStart) {
|
|
||||||
onOriginCompositionStart(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCompositionend(event: CompositionEvent) {
|
|
||||||
onCompositionend(event);
|
|
||||||
if (onOriginCompositionEnd) {
|
|
||||||
onOriginCompositionEnd(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPaste,
|
|
||||||
onFocus: (...args: any[]) => {
|
|
||||||
clearTimeout(this.blurTimeout);
|
|
||||||
onOriginFocus && onOriginFocus(args[0]);
|
|
||||||
onFocus && onFocus(args[0]);
|
|
||||||
this.VCSelectContainerEvent?.focus(args[0]);
|
|
||||||
},
|
|
||||||
onBlur: (...args: any[]) => {
|
|
||||||
this.blurTimeout = setTimeout(() => {
|
|
||||||
onOriginBlur && onOriginBlur(args[0]);
|
|
||||||
onBlur && onBlur(args[0]);
|
|
||||||
this.VCSelectContainerEvent?.blur(args[0]);
|
|
||||||
}, 200);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
inputNode.type === 'textarea' ? {} : { type: 'search' },
|
|
||||||
),
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
) as VNode;
|
|
||||||
return inputNode;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Input.props = {
|
|
||||||
// inputRef: PropTypes.any,
|
|
||||||
// prefixCls: PropTypes.string,
|
|
||||||
// id: PropTypes.string,
|
|
||||||
// inputElement: PropTypes.any,
|
|
||||||
// disabled: PropTypes.looseBool,
|
|
||||||
// autofocus: PropTypes.looseBool,
|
|
||||||
// autocomplete: PropTypes.string,
|
|
||||||
// editable: PropTypes.looseBool,
|
|
||||||
// accessibilityIndex: PropTypes.number,
|
|
||||||
// value: PropTypes.string,
|
|
||||||
// open: PropTypes.looseBool,
|
|
||||||
// tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
// /** Pass accessibility props to input */
|
|
||||||
// attrs: PropTypes.object,
|
|
||||||
// onKeydown: PropTypes.func,
|
|
||||||
// onMousedown: PropTypes.func,
|
|
||||||
// onChange: PropTypes.func,
|
|
||||||
// onPaste: PropTypes.func,
|
|
||||||
// onCompositionstart: PropTypes.func,
|
|
||||||
// onCompositionend: PropTypes.func,
|
|
||||||
// onFocus: PropTypes.func,
|
|
||||||
// onBlur: PropTypes.func,
|
|
||||||
// };
|
|
||||||
|
|
||||||
export default Input;
|
|
||||||
|
|
@ -1,283 +0,0 @@
|
||||||
import TransBtn from '../TransBtn';
|
|
||||||
import type { InnerSelectorProps } from './interface';
|
|
||||||
import Input from './Input';
|
|
||||||
import type { Ref, PropType } from 'vue';
|
|
||||||
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
|
|
||||||
import classNames from '../../_util/classNames';
|
|
||||||
import pickAttrs from '../../_util/pickAttrs';
|
|
||||||
import PropTypes from '../../_util/vue-types';
|
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
import Overflow from '../../vc-overflow';
|
|
||||||
import type { DisplayValueType, RenderNode, CustomTagProps, RawValueType } from '../BaseSelect';
|
|
||||||
import type { BaseOptionType } from '../Select';
|
|
||||||
|
|
||||||
type SelectorProps = InnerSelectorProps & {
|
|
||||||
// Icon
|
|
||||||
removeIcon?: RenderNode;
|
|
||||||
|
|
||||||
// Tags
|
|
||||||
maxTagCount?: number | 'responsive';
|
|
||||||
maxTagTextLength?: number;
|
|
||||||
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
|
||||||
tokenSeparators?: string[];
|
|
||||||
tagRender?: (props: CustomTagProps) => VueNode;
|
|
||||||
onToggleOpen: any;
|
|
||||||
|
|
||||||
// Motion
|
|
||||||
choiceTransitionName?: string;
|
|
||||||
|
|
||||||
// Event
|
|
||||||
onRemove: (value: DisplayValueType) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
id: PropTypes.string,
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
values: PropTypes.array,
|
|
||||||
open: PropTypes.looseBool,
|
|
||||||
searchValue: PropTypes.string,
|
|
||||||
inputRef: PropTypes.any,
|
|
||||||
placeholder: PropTypes.any,
|
|
||||||
disabled: PropTypes.looseBool,
|
|
||||||
mode: PropTypes.string,
|
|
||||||
showSearch: PropTypes.looseBool,
|
|
||||||
autofocus: PropTypes.looseBool,
|
|
||||||
autocomplete: PropTypes.string,
|
|
||||||
activeDescendantId: PropTypes.string,
|
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
|
|
||||||
removeIcon: PropTypes.any,
|
|
||||||
choiceTransitionName: PropTypes.string,
|
|
||||||
|
|
||||||
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
maxTagTextLength: PropTypes.number,
|
|
||||||
maxTagPlaceholder: PropTypes.any.def(
|
|
||||||
() => (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`,
|
|
||||||
),
|
|
||||||
tagRender: PropTypes.func,
|
|
||||||
|
|
||||||
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
|
||||||
onRemove: PropTypes.func,
|
|
||||||
onInputChange: PropTypes.func,
|
|
||||||
onInputPaste: PropTypes.func,
|
|
||||||
onInputKeyDown: PropTypes.func,
|
|
||||||
onInputMouseDown: PropTypes.func,
|
|
||||||
onInputCompositionStart: PropTypes.func,
|
|
||||||
onInputCompositionEnd: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPreventMouseDown = (event: MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const SelectSelector = defineComponent<SelectorProps>({
|
|
||||||
name: 'MultipleSelectSelector',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: props as any,
|
|
||||||
setup(props) {
|
|
||||||
const measureRef = ref();
|
|
||||||
const inputWidth = ref(0);
|
|
||||||
const focused = ref(false);
|
|
||||||
|
|
||||||
const selectionPrefixCls = computed(() => `${props.prefixCls}-selection`);
|
|
||||||
|
|
||||||
// ===================== Search ======================
|
|
||||||
const inputValue = computed(() =>
|
|
||||||
props.open || props.mode === 'tags' ? props.searchValue : '',
|
|
||||||
);
|
|
||||||
const inputEditable: Ref<boolean> = computed(
|
|
||||||
() =>
|
|
||||||
props.mode === 'tags' || ((props.showSearch && (props.open || focused.value)) as boolean),
|
|
||||||
);
|
|
||||||
|
|
||||||
// We measure width and set to the input immediately
|
|
||||||
onMounted(() => {
|
|
||||||
watch(
|
|
||||||
inputValue,
|
|
||||||
() => {
|
|
||||||
inputWidth.value = measureRef.value.scrollWidth;
|
|
||||||
},
|
|
||||||
{ flush: 'post', immediate: true },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===================== Render ======================
|
|
||||||
// >>> Render Selector Node. Includes Item & Rest
|
|
||||||
function defaultRenderSelector(
|
|
||||||
title: VueNode,
|
|
||||||
content: VueNode,
|
|
||||||
itemDisabled: boolean,
|
|
||||||
closable?: boolean,
|
|
||||||
onClose?: (e: MouseEvent) => void,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
class={classNames(`${selectionPrefixCls.value}-item`, {
|
|
||||||
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
|
||||||
})}
|
|
||||||
title={
|
|
||||||
typeof title === 'string' || typeof title === 'number' ? title.toString() : undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
|
||||||
{closable && (
|
|
||||||
<TransBtn
|
|
||||||
class={`${selectionPrefixCls.value}-item-remove`}
|
|
||||||
onMousedown={onPreventMouseDown}
|
|
||||||
onClick={onClose}
|
|
||||||
customizeIcon={props.removeIcon}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</TransBtn>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function customizeRenderSelector(
|
|
||||||
value: RawValueType,
|
|
||||||
content: VueNode,
|
|
||||||
itemDisabled: boolean,
|
|
||||||
closable: boolean,
|
|
||||||
onClose: (e: MouseEvent) => void,
|
|
||||||
option: BaseOptionType,
|
|
||||||
) {
|
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
|
||||||
onPreventMouseDown(e);
|
|
||||||
props.onToggleOpen(!open);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<span onMousedown={onMouseDown}>
|
|
||||||
{props.tagRender({
|
|
||||||
label: content,
|
|
||||||
value,
|
|
||||||
disabled: itemDisabled,
|
|
||||||
closable,
|
|
||||||
onClose,
|
|
||||||
option,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItem(valueItem: DisplayValueType) {
|
|
||||||
const { disabled: itemDisabled, label, value, option } = valueItem;
|
|
||||||
const closable = !props.disabled && !itemDisabled;
|
|
||||||
|
|
||||||
let displayLabel = label;
|
|
||||||
|
|
||||||
if (typeof props.maxTagTextLength === 'number') {
|
|
||||||
if (typeof label === 'string' || typeof label === 'number') {
|
|
||||||
const strLabel = String(displayLabel);
|
|
||||||
|
|
||||||
if (strLabel.length > props.maxTagTextLength) {
|
|
||||||
displayLabel = `${strLabel.slice(0, props.maxTagTextLength)}...`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const onClose = (event?: MouseEvent) => {
|
|
||||||
if (event) event.stopPropagation();
|
|
||||||
props.onRemove?.(valueItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
return typeof props.tagRender === 'function'
|
|
||||||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
|
|
||||||
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRest(omittedValues: DisplayValueType[]) {
|
|
||||||
const { maxTagPlaceholder = omittedValues => `+ ${omittedValues.length} ...` } = props;
|
|
||||||
const content =
|
|
||||||
typeof maxTagPlaceholder === 'function'
|
|
||||||
? maxTagPlaceholder(omittedValues)
|
|
||||||
: maxTagPlaceholder;
|
|
||||||
|
|
||||||
return defaultRenderSelector(content, content, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
prefixCls,
|
|
||||||
values,
|
|
||||||
open,
|
|
||||||
inputRef,
|
|
||||||
placeholder,
|
|
||||||
disabled,
|
|
||||||
autofocus,
|
|
||||||
autocomplete,
|
|
||||||
activeDescendantId,
|
|
||||||
tabindex,
|
|
||||||
onInputChange,
|
|
||||||
onInputPaste,
|
|
||||||
onInputKeyDown,
|
|
||||||
onInputMouseDown,
|
|
||||||
onInputCompositionStart,
|
|
||||||
onInputCompositionEnd,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
// >>> Input Node
|
|
||||||
const inputNode = (
|
|
||||||
<div
|
|
||||||
class={`${selectionPrefixCls.value}-search`}
|
|
||||||
style={{ width: inputWidth.value + 'px' }}
|
|
||||||
key="input"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
inputRef={inputRef}
|
|
||||||
open={open}
|
|
||||||
prefixCls={prefixCls}
|
|
||||||
id={id}
|
|
||||||
inputElement={null}
|
|
||||||
disabled={disabled}
|
|
||||||
autofocus={autofocus}
|
|
||||||
autocomplete={autocomplete}
|
|
||||||
editable={inputEditable.value}
|
|
||||||
activeDescendantId={activeDescendantId}
|
|
||||||
value={inputValue.value}
|
|
||||||
onKeydown={onInputKeyDown}
|
|
||||||
onMousedown={onInputMouseDown}
|
|
||||||
onChange={onInputChange}
|
|
||||||
onPaste={onInputPaste}
|
|
||||||
onCompositionstart={onInputCompositionStart}
|
|
||||||
onCompositionend={onInputCompositionEnd}
|
|
||||||
tabindex={tabindex}
|
|
||||||
attrs={pickAttrs(props, true)}
|
|
||||||
onFocus={() => (focused.value = true)}
|
|
||||||
onBlur={() => (focused.value = false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Measure Node */}
|
|
||||||
<span ref={measureRef} class={`${selectionPrefixCls.value}-search-mirror`} aria-hidden>
|
|
||||||
{inputValue.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// >>> Selections
|
|
||||||
const selectionNode = (
|
|
||||||
<Overflow
|
|
||||||
prefixCls={`${selectionPrefixCls.value}-overflow`}
|
|
||||||
data={values}
|
|
||||||
renderItem={renderItem}
|
|
||||||
renderRest={renderRest}
|
|
||||||
suffix={inputNode}
|
|
||||||
itemKey="key"
|
|
||||||
maxCount={props.maxTagCount}
|
|
||||||
key="overflow"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{selectionNode}
|
|
||||||
{!values.length && !inputValue.value && (
|
|
||||||
<span class={`${selectionPrefixCls.value}-placeholder`}>{placeholder}</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default SelectSelector;
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
import pickAttrs from '../../_util/pickAttrs';
|
|
||||||
import Input from './Input';
|
|
||||||
import type { InnerSelectorProps } from './interface';
|
|
||||||
import { Fragment, computed, defineComponent, ref, watch } from 'vue';
|
|
||||||
import PropTypes from '../../_util/vue-types';
|
|
||||||
import { useInjectTreeSelectContext } from '../../vc-tree-select/Context';
|
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
|
|
||||||
interface SelectorProps extends InnerSelectorProps {
|
|
||||||
inputElement: VueNode;
|
|
||||||
activeValue: string;
|
|
||||||
}
|
|
||||||
const props = {
|
|
||||||
inputElement: PropTypes.any,
|
|
||||||
id: PropTypes.string,
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
values: PropTypes.array,
|
|
||||||
open: PropTypes.looseBool,
|
|
||||||
searchValue: PropTypes.string,
|
|
||||||
inputRef: PropTypes.any,
|
|
||||||
placeholder: PropTypes.any,
|
|
||||||
disabled: PropTypes.looseBool,
|
|
||||||
mode: PropTypes.string,
|
|
||||||
showSearch: PropTypes.looseBool,
|
|
||||||
autofocus: PropTypes.looseBool,
|
|
||||||
autocomplete: PropTypes.string,
|
|
||||||
activeDescendantId: PropTypes.string,
|
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
activeValue: PropTypes.string,
|
|
||||||
backfill: PropTypes.looseBool,
|
|
||||||
onInputChange: PropTypes.func,
|
|
||||||
onInputPaste: PropTypes.func,
|
|
||||||
onInputKeyDown: PropTypes.func,
|
|
||||||
onInputMouseDown: PropTypes.func,
|
|
||||||
onInputCompositionStart: PropTypes.func,
|
|
||||||
onInputCompositionEnd: PropTypes.func,
|
|
||||||
};
|
|
||||||
const SingleSelector = defineComponent<SelectorProps>({
|
|
||||||
name: 'SingleSelector',
|
|
||||||
setup(props) {
|
|
||||||
const inputChanged = ref(false);
|
|
||||||
|
|
||||||
const combobox = computed(() => props.mode === 'combobox');
|
|
||||||
const inputEditable = computed(() => combobox.value || props.showSearch);
|
|
||||||
|
|
||||||
const inputValue = computed(() => {
|
|
||||||
let inputValue: string = props.searchValue || '';
|
|
||||||
if (combobox.value && props.activeValue && !inputChanged.value) {
|
|
||||||
inputValue = props.activeValue;
|
|
||||||
}
|
|
||||||
return inputValue;
|
|
||||||
});
|
|
||||||
const treeSelectContext = useInjectTreeSelectContext();
|
|
||||||
watch(
|
|
||||||
[combobox, () => props.activeValue],
|
|
||||||
() => {
|
|
||||||
if (combobox.value) {
|
|
||||||
inputChanged.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Not show text when closed expect combobox mode
|
|
||||||
const hasTextInput = computed(() =>
|
|
||||||
props.mode !== 'combobox' && !props.open && !props.showSearch ? false : !!inputValue.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
const title = computed(() => {
|
|
||||||
const item = props.values[0];
|
|
||||||
return item && (typeof item.label === 'string' || typeof item.label === 'number')
|
|
||||||
? item.label.toString()
|
|
||||||
: undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderPlaceholder = () => {
|
|
||||||
if (props.values[0]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const hiddenStyle = hasTextInput.value ? { visibility: 'hidden' as const } : undefined;
|
|
||||||
return (
|
|
||||||
<span class={`${props.prefixCls}-selection-placeholder`} style={hiddenStyle}>
|
|
||||||
{props.placeholder}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const {
|
|
||||||
inputElement,
|
|
||||||
prefixCls,
|
|
||||||
id,
|
|
||||||
values,
|
|
||||||
inputRef,
|
|
||||||
disabled,
|
|
||||||
autofocus,
|
|
||||||
autocomplete,
|
|
||||||
activeDescendantId,
|
|
||||||
open,
|
|
||||||
tabindex,
|
|
||||||
onInputKeyDown,
|
|
||||||
onInputMouseDown,
|
|
||||||
onInputChange,
|
|
||||||
onInputPaste,
|
|
||||||
onInputCompositionStart,
|
|
||||||
onInputCompositionEnd,
|
|
||||||
} = props;
|
|
||||||
const item = values[0];
|
|
||||||
let titleNode = null;
|
|
||||||
// custom tree-select title by slot
|
|
||||||
if (item && treeSelectContext.value.slots) {
|
|
||||||
titleNode =
|
|
||||||
treeSelectContext.value.slots[item?.option?.data?.slots?.title] ||
|
|
||||||
treeSelectContext.value.slots.title ||
|
|
||||||
item.label;
|
|
||||||
if (typeof titleNode === 'function') {
|
|
||||||
titleNode = titleNode(item.option?.data || {});
|
|
||||||
}
|
|
||||||
// else if (treeSelectContext.value.slots.titleRender) {
|
|
||||||
// // 因历史 title 是覆盖逻辑,新增 titleRender,所有的 title 都走一遍 titleRender
|
|
||||||
// titleNode = treeSelectContext.value.slots.titleRender(item.option?.data || {});
|
|
||||||
// }
|
|
||||||
} else {
|
|
||||||
titleNode = item?.label;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span class={`${prefixCls}-selection-search`}>
|
|
||||||
<Input
|
|
||||||
inputRef={inputRef}
|
|
||||||
prefixCls={prefixCls}
|
|
||||||
id={id}
|
|
||||||
open={open}
|
|
||||||
inputElement={inputElement}
|
|
||||||
disabled={disabled}
|
|
||||||
autofocus={autofocus}
|
|
||||||
autocomplete={autocomplete}
|
|
||||||
editable={inputEditable.value}
|
|
||||||
activeDescendantId={activeDescendantId}
|
|
||||||
value={inputValue.value}
|
|
||||||
onKeydown={onInputKeyDown}
|
|
||||||
onMousedown={onInputMouseDown}
|
|
||||||
onChange={e => {
|
|
||||||
inputChanged.value = true;
|
|
||||||
onInputChange(e as any);
|
|
||||||
}}
|
|
||||||
onPaste={onInputPaste}
|
|
||||||
onCompositionstart={onInputCompositionStart}
|
|
||||||
onCompositionend={onInputCompositionEnd}
|
|
||||||
tabindex={tabindex}
|
|
||||||
attrs={pickAttrs(props, true)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Display value */}
|
|
||||||
{!combobox.value && item && !hasTextInput.value && (
|
|
||||||
<span class={`${prefixCls}-selection-item`} title={title.value}>
|
|
||||||
<Fragment key={item.key || item.value}>{titleNode}</Fragment>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Display placeholder */}
|
|
||||||
{renderPlaceholder()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
SingleSelector.props = props;
|
|
||||||
SingleSelector.inheritAttrs = false;
|
|
||||||
|
|
||||||
export default SingleSelector;
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
/**
|
|
||||||
* Cursor rule:
|
|
||||||
* 1. Only `showSearch` enabled
|
|
||||||
* 2. Only `open` is `true`
|
|
||||||
* 3. When typing, set `open` to `true` which hit rule of 2
|
|
||||||
*
|
|
||||||
* Accessibility:
|
|
||||||
* - https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
import KeyCode from '../../_util/KeyCode';
|
|
||||||
import MultipleSelector from './MultipleSelector';
|
|
||||||
import SingleSelector from './SingleSelector';
|
|
||||||
import type { CustomTagProps, DisplayValueType, Mode, RenderNode } from '../BaseSelect';
|
|
||||||
import { isValidateOpenKey } from '../utils/keyUtil';
|
|
||||||
import useLock from '../hooks/useLock';
|
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import createRef from '../../_util/createRef';
|
|
||||||
import PropTypes from '../../_util/vue-types';
|
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
import type { EventHandler } from '../../_util/EventInterface';
|
|
||||||
import type { ScrollTo } from '../../vc-virtual-list/List';
|
|
||||||
|
|
||||||
export interface SelectorProps {
|
|
||||||
id: string;
|
|
||||||
prefixCls: string;
|
|
||||||
showSearch?: boolean;
|
|
||||||
open: boolean;
|
|
||||||
values: DisplayValueType[];
|
|
||||||
multiple?: boolean;
|
|
||||||
mode: Mode;
|
|
||||||
searchValue: string;
|
|
||||||
activeValue: string;
|
|
||||||
inputElement: VueNode;
|
|
||||||
|
|
||||||
autofocus?: boolean;
|
|
||||||
activeDescendantId?: string;
|
|
||||||
tabindex?: number | string;
|
|
||||||
disabled?: boolean;
|
|
||||||
placeholder?: VueNode;
|
|
||||||
removeIcon?: RenderNode;
|
|
||||||
|
|
||||||
// Tags
|
|
||||||
maxTagCount?: number | 'responsive';
|
|
||||||
maxTagTextLength?: number;
|
|
||||||
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
|
||||||
tagRender?: (props: CustomTagProps) => VueNode;
|
|
||||||
|
|
||||||
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
|
||||||
tokenWithEnter?: boolean;
|
|
||||||
|
|
||||||
// Motion
|
|
||||||
choiceTransitionName?: string;
|
|
||||||
|
|
||||||
onToggleOpen: (open?: boolean) => void | any;
|
|
||||||
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
|
||||||
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
|
|
||||||
onSearchSubmit: (searchText: string) => void;
|
|
||||||
onRemove: (value: DisplayValueType) => void;
|
|
||||||
onInputKeyDown?: (e: KeyboardEvent) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private get real dom for trigger align.
|
|
||||||
* This may be removed after React provides replacement of `findDOMNode`
|
|
||||||
*/
|
|
||||||
domRef: () => HTMLDivElement;
|
|
||||||
}
|
|
||||||
export interface RefSelectorProps {
|
|
||||||
focus: () => void;
|
|
||||||
blur: () => void;
|
|
||||||
scrollTo?: ScrollTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Selector = defineComponent<SelectorProps>({
|
|
||||||
name: 'Selector',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
id: PropTypes.string,
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
showSearch: PropTypes.looseBool,
|
|
||||||
open: PropTypes.looseBool,
|
|
||||||
/** Display in the Selector value, it's not same as `value` prop */
|
|
||||||
values: PropTypes.array,
|
|
||||||
multiple: PropTypes.looseBool,
|
|
||||||
mode: PropTypes.string,
|
|
||||||
searchValue: PropTypes.string,
|
|
||||||
activeValue: PropTypes.string,
|
|
||||||
inputElement: PropTypes.any,
|
|
||||||
|
|
||||||
autofocus: PropTypes.looseBool,
|
|
||||||
activeDescendantId: PropTypes.string,
|
|
||||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
disabled: PropTypes.looseBool,
|
|
||||||
placeholder: PropTypes.any,
|
|
||||||
removeIcon: PropTypes.any,
|
|
||||||
|
|
||||||
// Tags
|
|
||||||
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
maxTagTextLength: PropTypes.number,
|
|
||||||
maxTagPlaceholder: PropTypes.any,
|
|
||||||
tagRender: PropTypes.func,
|
|
||||||
|
|
||||||
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
|
||||||
tokenWithEnter: PropTypes.looseBool,
|
|
||||||
|
|
||||||
// Motion
|
|
||||||
choiceTransitionName: PropTypes.string,
|
|
||||||
|
|
||||||
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
|
||||||
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
|
||||||
onSearch: PropTypes.func,
|
|
||||||
onSearchSubmit: PropTypes.func,
|
|
||||||
onRemove: PropTypes.func,
|
|
||||||
onInputKeyDown: { type: Function as PropType<EventHandler> },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private get real dom for trigger align.
|
|
||||||
* This may be removed after React provides replacement of `findDOMNode`
|
|
||||||
*/
|
|
||||||
domRef: PropTypes.func,
|
|
||||||
} as any,
|
|
||||||
setup(props, { expose }) {
|
|
||||||
const inputRef = createRef();
|
|
||||||
let compositionStatus = false;
|
|
||||||
|
|
||||||
// ====================== Input ======================
|
|
||||||
const [getInputMouseDown, setInputMouseDown] = useLock(0);
|
|
||||||
|
|
||||||
const onInternalInputKeyDown = (event: KeyboardEvent) => {
|
|
||||||
const { which } = event;
|
|
||||||
|
|
||||||
if (which === KeyCode.UP || which === KeyCode.DOWN) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.onInputKeyDown) {
|
|
||||||
props.onInputKeyDown(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (which === KeyCode.ENTER && props.mode === 'tags' && !compositionStatus && !props.open) {
|
|
||||||
// When menu isn't open, OptionList won't trigger a value change
|
|
||||||
// So when enter is pressed, the tag's input value should be emitted here to let selector know
|
|
||||||
props.onSearchSubmit((event.target as HTMLInputElement).value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidateOpenKey(which)) {
|
|
||||||
props.onToggleOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We can not use `findDOMNode` sine it will get warning,
|
|
||||||
* have to use timer to check if is input element.
|
|
||||||
*/
|
|
||||||
const onInternalInputMouseDown = () => {
|
|
||||||
setInputMouseDown(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// When paste come, ignore next onChange
|
|
||||||
let pastedText = null;
|
|
||||||
|
|
||||||
const triggerOnSearch = (value: string) => {
|
|
||||||
if (props.onSearch(value, true, compositionStatus) !== false) {
|
|
||||||
props.onToggleOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInputCompositionStart = () => {
|
|
||||||
compositionStatus = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInputCompositionEnd = (e: InputEvent) => {
|
|
||||||
compositionStatus = false;
|
|
||||||
// Trigger search again to support `tokenSeparators` with typewriting
|
|
||||||
if (props.mode !== 'combobox') {
|
|
||||||
triggerOnSearch((e.target as HTMLInputElement).value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInputChange = (event: { target: { value: any } }) => {
|
|
||||||
let {
|
|
||||||
target: { value },
|
|
||||||
} = event;
|
|
||||||
|
|
||||||
// Pasted text should replace back to origin content
|
|
||||||
if (props.tokenWithEnter && pastedText && /[\r\n]/.test(pastedText)) {
|
|
||||||
// CRLF will be treated as a single space for input element
|
|
||||||
const replacedText = pastedText
|
|
||||||
.replace(/[\r\n]+$/, '')
|
|
||||||
.replace(/\r\n/g, ' ')
|
|
||||||
.replace(/[\r\n]/g, ' ');
|
|
||||||
value = value.replace(replacedText, pastedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
pastedText = null;
|
|
||||||
|
|
||||||
triggerOnSearch(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInputPaste = (e: ClipboardEvent) => {
|
|
||||||
const { clipboardData } = e;
|
|
||||||
const value = clipboardData.getData('text');
|
|
||||||
|
|
||||||
pastedText = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClick = ({ target }) => {
|
|
||||||
if (target !== inputRef.current) {
|
|
||||||
// Should focus input if click the selector
|
|
||||||
const isIE = (document.body.style as any).msTouchAction !== undefined;
|
|
||||||
if (isIE) {
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current.focus();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
inputRef.current.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMousedown = (event: MouseEvent) => {
|
|
||||||
const inputMouseDown = getInputMouseDown();
|
|
||||||
if (event.target !== inputRef.current && !inputMouseDown) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((props.mode !== 'combobox' && (!props.showSearch || !inputMouseDown)) || !props.open) {
|
|
||||||
if (props.open) {
|
|
||||||
props.onSearch('', true, false);
|
|
||||||
}
|
|
||||||
props.onToggleOpen();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
expose({
|
|
||||||
focus: () => {
|
|
||||||
inputRef.current.focus();
|
|
||||||
},
|
|
||||||
blur: () => {
|
|
||||||
inputRef.current.blur();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const { prefixCls, domRef, mode } = props as SelectorProps;
|
|
||||||
const sharedProps = {
|
|
||||||
inputRef,
|
|
||||||
onInputKeyDown: onInternalInputKeyDown,
|
|
||||||
onInputMouseDown: onInternalInputMouseDown,
|
|
||||||
onInputChange,
|
|
||||||
onInputPaste,
|
|
||||||
onInputCompositionStart,
|
|
||||||
onInputCompositionEnd,
|
|
||||||
};
|
|
||||||
const selectNode =
|
|
||||||
mode === 'multiple' || mode === 'tags' ? (
|
|
||||||
<MultipleSelector {...props} {...sharedProps} />
|
|
||||||
) : (
|
|
||||||
<SingleSelector {...props} {...sharedProps} />
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={domRef}
|
|
||||||
class={`${prefixCls}-selector`}
|
|
||||||
onClick={onClick}
|
|
||||||
onMousedown={onMousedown}
|
|
||||||
>
|
|
||||||
{selectNode}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Selector;
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import type { RefObject } from '../../_util/createRef';
|
|
||||||
|
|
||||||
import type { EventHandler } from '../../_util/EventInterface';
|
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
import type { Mode, DisplayValueType } from '../BaseSelect';
|
|
||||||
|
|
||||||
export interface InnerSelectorProps {
|
|
||||||
prefixCls: string;
|
|
||||||
id: string;
|
|
||||||
mode: Mode;
|
|
||||||
inputRef: RefObject;
|
|
||||||
placeholder?: VueNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
autofocus?: boolean;
|
|
||||||
autocomplete?: string;
|
|
||||||
values: DisplayValueType[];
|
|
||||||
showSearch?: boolean;
|
|
||||||
searchValue: string;
|
|
||||||
activeDescendantId: string;
|
|
||||||
open: boolean;
|
|
||||||
tabindex?: number | string;
|
|
||||||
onInputKeyDown: EventHandler;
|
|
||||||
onInputMouseDown: EventHandler;
|
|
||||||
onInputChange: EventHandler;
|
|
||||||
onInputPaste: EventHandler;
|
|
||||||
onInputCompositionStart: EventHandler;
|
|
||||||
onInputCompositionEnd: EventHandler;
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
|
||||||
import type { VueNode } from '../_util/type';
|
|
||||||
import PropTypes from '../_util/vue-types';
|
|
||||||
import type { RenderNode } from './BaseSelect';
|
|
||||||
|
|
||||||
export interface TransBtnProps {
|
|
||||||
class: string;
|
|
||||||
customizeIcon: RenderNode;
|
|
||||||
customizeIconProps?: any;
|
|
||||||
onMousedown?: (payload: MouseEvent) => void;
|
|
||||||
onClick?: (payload: MouseEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransBtnType extends FunctionalComponent<TransBtnProps> {
|
|
||||||
displayName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransBtn: TransBtnType = (props, { slots }) => {
|
|
||||||
const { class: className, customizeIcon, customizeIconProps, onMousedown, onClick } = props;
|
|
||||||
let icon: VueNode;
|
|
||||||
|
|
||||||
if (typeof customizeIcon === 'function') {
|
|
||||||
icon = customizeIcon(customizeIconProps);
|
|
||||||
} else {
|
|
||||||
icon = customizeIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
class={className}
|
|
||||||
onMousedown={event => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (onMousedown) {
|
|
||||||
onMousedown(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
userSelect: 'none',
|
|
||||||
WebkitUserSelect: 'none',
|
|
||||||
}}
|
|
||||||
unselectable="on"
|
|
||||||
onClick={onClick}
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
{icon !== undefined ? (
|
|
||||||
icon
|
|
||||||
) : (
|
|
||||||
<span class={className.split(/\s+/).map((cls: any) => `${cls}-icon`)}>
|
|
||||||
{slots.default?.()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TransBtn.inheritAttrs = false;
|
|
||||||
TransBtn.displayName = 'TransBtn';
|
|
||||||
TransBtn.props = {
|
|
||||||
class: PropTypes.string,
|
|
||||||
customizeIcon: PropTypes.any,
|
|
||||||
customizeIconProps: PropTypes.any,
|
|
||||||
onMousedown: PropTypes.func,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransBtn;
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Similar with `useLock`, but this hook will always execute last value.
|
|
||||||
* When set to `true`, it will keep `true` for a short time even if `false` is set.
|
|
||||||
*/
|
|
||||||
export default function useDelayReset(
|
|
||||||
timeout = 10,
|
|
||||||
): [Ref<Boolean>, (val: boolean, callback?: () => void) => void, () => void] {
|
|
||||||
const bool = ref(false);
|
|
||||||
let delay: any;
|
|
||||||
|
|
||||||
const cancelLatest = () => {
|
|
||||||
clearTimeout(delay);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
cancelLatest();
|
|
||||||
});
|
|
||||||
const delaySetBool = (value: boolean, callback: () => void) => {
|
|
||||||
cancelLatest();
|
|
||||||
delay = setTimeout(() => {
|
|
||||||
bool.value = value;
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}, timeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [bool, delaySetBool, cancelLatest];
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { onBeforeUpdate } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Locker return cached mark.
|
|
||||||
* If set to `true`, will return `true` in a short time even if set `false`.
|
|
||||||
* If set to `false` and then set to `true`, will change to `true`.
|
|
||||||
* And after time duration, it will back to `null` automatically.
|
|
||||||
*/
|
|
||||||
export default function useLock(duration = 250): [() => boolean | null, (lock: boolean) => void] {
|
|
||||||
let lock: boolean | null = null;
|
|
||||||
let timeout: any;
|
|
||||||
|
|
||||||
onBeforeUpdate(() => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
});
|
|
||||||
|
|
||||||
function doLock(locked: boolean) {
|
|
||||||
if (locked || lock === null) {
|
|
||||||
lock = locked;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
lock = null;
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [() => lock, doLock];
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { onBeforeUnmount, onMounted } from 'vue';
|
|
||||||
|
|
||||||
export default function useSelectTriggerControl(
|
|
||||||
refs: Ref[],
|
|
||||||
open: Ref<boolean>,
|
|
||||||
triggerOpen: (open: boolean) => void,
|
|
||||||
) {
|
|
||||||
function onGlobalMouseDown(event: MouseEvent) {
|
|
||||||
let target = event.target as HTMLElement;
|
|
||||||
|
|
||||||
if (target.shadowRoot && event.composed) {
|
|
||||||
target = (event.composedPath()[0] || target) as HTMLElement;
|
|
||||||
}
|
|
||||||
const elements = [refs[0]?.value, refs[1]?.value?.getPopupElement()];
|
|
||||||
if (
|
|
||||||
open.value &&
|
|
||||||
elements.every(element => element && !element.contains(target) && element !== target)
|
|
||||||
) {
|
|
||||||
// Should trigger close
|
|
||||||
triggerOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('mousedown', onGlobalMouseDown);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener('mousedown', onGlobalMouseDown);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import type { SelectProps } from './Select';
|
|
||||||
import Select, { selectProps } from './Select';
|
|
||||||
import Option from './Option';
|
|
||||||
import OptGroup from './OptGroup';
|
|
||||||
import BaseSelect from './BaseSelect';
|
|
||||||
import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
|
|
||||||
import useBaseProps from './hooks/useBaseProps';
|
|
||||||
|
|
||||||
export { Option, OptGroup, selectProps, BaseSelect, useBaseProps };
|
|
||||||
export type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps };
|
|
||||||
|
|
||||||
export default Select;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export function toArray<T>(value: T | T[]): T[] {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return value !== undefined ? [value] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isClient =
|
|
||||||
typeof window !== 'undefined' && window.document && window.document.documentElement;
|
|
||||||
|
|
||||||
/** Is client side and not jsdom */
|
|
||||||
export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient;
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import { flattenChildren, isValidElement } from '../../_util/props-util';
|
|
||||||
import type { VNode } from 'vue';
|
|
||||||
import type { BaseOptionType, DefaultOptionType } from '../Select';
|
|
||||||
import type { VueNode } from '../../_util/type';
|
|
||||||
|
|
||||||
function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
|
|
||||||
node: VNode,
|
|
||||||
): OptionType {
|
|
||||||
const {
|
|
||||||
key,
|
|
||||||
children,
|
|
||||||
props: { value, disabled, ...restProps },
|
|
||||||
} = node as Omit<VNode, 'key'> & {
|
|
||||||
children: { default?: () => any };
|
|
||||||
key: string | number;
|
|
||||||
};
|
|
||||||
const child = children && children.default ? children.default() : undefined;
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value: value !== undefined ? value : key,
|
|
||||||
children: child,
|
|
||||||
disabled: disabled || disabled === '', // support <a-select-option disabled />
|
|
||||||
...(restProps as any),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
|
|
||||||
nodes: VueNode[],
|
|
||||||
optionOnly = false,
|
|
||||||
): OptionType[] {
|
|
||||||
const dd = flattenChildren(nodes as [])
|
|
||||||
.map((node: VNode, index: number): OptionType | null => {
|
|
||||||
if (!isValidElement(node) || !node.type) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
type: { isSelectOptGroup },
|
|
||||||
key,
|
|
||||||
children,
|
|
||||||
props,
|
|
||||||
} = node as VNode & {
|
|
||||||
type: { isSelectOptGroup?: boolean };
|
|
||||||
children: { default?: () => any; label?: () => any };
|
|
||||||
};
|
|
||||||
|
|
||||||
if (optionOnly || !isSelectOptGroup) {
|
|
||||||
return convertNodeToOption(node);
|
|
||||||
}
|
|
||||||
const child = children && children.default ? children.default() : undefined;
|
|
||||||
const label = props?.label || children.label?.() || key;
|
|
||||||
return {
|
|
||||||
key: `__RC_SELECT_GRP__${key === null ? index : String(key)}__`,
|
|
||||||
...props,
|
|
||||||
label,
|
|
||||||
options: convertChildrenToData(child || []),
|
|
||||||
} as any;
|
|
||||||
})
|
|
||||||
.filter(data => data);
|
|
||||||
return dd;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
/* istanbul ignore file */
|
|
||||||
export function isPlatformMac(): boolean {
|
|
||||||
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
import type { BaseOptionType, DefaultOptionType, RawValueType, FieldNames } from '../Select';
|
|
||||||
import { warning } from '../../vc-util/warning';
|
|
||||||
import type { FlattenOptionData } from '../interface';
|
|
||||||
|
|
||||||
function getKey(data: BaseOptionType, index: number) {
|
|
||||||
const { key } = data;
|
|
||||||
let value: RawValueType;
|
|
||||||
|
|
||||||
if ('value' in data) {
|
|
||||||
({ value } = data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key !== null && key !== undefined) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
if (value !== undefined) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return `rc-index-key-${index}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean) {
|
|
||||||
const { label, value, options } = fieldNames || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: label || (childrenAsData ? 'children' : 'label'),
|
|
||||||
value: value || 'value',
|
|
||||||
options: options || 'options',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flat options into flatten list.
|
|
||||||
* We use `optionOnly` here is aim to avoid user use nested option group.
|
|
||||||
* Here is simply set `key` to the index if not provided.
|
|
||||||
*/
|
|
||||||
export function flattenOptions<OptionType extends BaseOptionType = DefaultOptionType>(
|
|
||||||
options: OptionType[],
|
|
||||||
{ fieldNames, childrenAsData }: { fieldNames?: FieldNames; childrenAsData?: boolean } = {},
|
|
||||||
): FlattenOptionData<OptionType>[] {
|
|
||||||
const flattenList: FlattenOptionData<OptionType>[] = [];
|
|
||||||
|
|
||||||
const {
|
|
||||||
label: fieldLabel,
|
|
||||||
value: fieldValue,
|
|
||||||
options: fieldOptions,
|
|
||||||
} = fillFieldNames(fieldNames, false);
|
|
||||||
|
|
||||||
function dig(list: OptionType[], isGroupOption: boolean) {
|
|
||||||
list.forEach(data => {
|
|
||||||
const label = data[fieldLabel];
|
|
||||||
|
|
||||||
if (isGroupOption || !(fieldOptions in data)) {
|
|
||||||
const value = data[fieldValue];
|
|
||||||
// Option
|
|
||||||
flattenList.push({
|
|
||||||
key: getKey(data, flattenList.length),
|
|
||||||
groupOption: isGroupOption,
|
|
||||||
data,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let grpLabel = label;
|
|
||||||
if (grpLabel === undefined && childrenAsData) {
|
|
||||||
grpLabel = data.label;
|
|
||||||
}
|
|
||||||
// Option Group
|
|
||||||
flattenList.push({
|
|
||||||
key: getKey(data, flattenList.length),
|
|
||||||
group: true,
|
|
||||||
data,
|
|
||||||
label: grpLabel,
|
|
||||||
});
|
|
||||||
|
|
||||||
dig(data[fieldOptions], true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dig(options, false);
|
|
||||||
|
|
||||||
return flattenList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject `props` into `option` for legacy usage
|
|
||||||
*/
|
|
||||||
export function injectPropsWithOption<T>(option: T): T {
|
|
||||||
const newOption = { ...option };
|
|
||||||
if (!('props' in newOption)) {
|
|
||||||
Object.defineProperty(newOption, 'props', {
|
|
||||||
get() {
|
|
||||||
warning(
|
|
||||||
false,
|
|
||||||
'Return type is option instead of Option instance. Please read value directly instead of reading from `props`.',
|
|
||||||
);
|
|
||||||
return newOption;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newOption;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSeparatedContent(text: string, tokens: string[]): string[] {
|
|
||||||
if (!tokens || !tokens.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let match = false;
|
|
||||||
|
|
||||||
function separate(str: string, [token, ...restTokens]: string[]) {
|
|
||||||
if (!token) {
|
|
||||||
return [str];
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = str.split(token);
|
|
||||||
match = match || list.length > 1;
|
|
||||||
|
|
||||||
return list
|
|
||||||
.reduce((prevList, unitStr) => [...prevList, ...separate(unitStr, restTokens)], [])
|
|
||||||
.filter(unit => unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = separate(text, tokens);
|
|
||||||
return match ? list : null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import warning, { noteOnce } from '../../vc-util/warning';
|
|
||||||
import { convertChildrenToData } from './legacyUtil';
|
|
||||||
import { toArray } from './commonUtil';
|
|
||||||
import { isValidElement } from '../../_util/props-util';
|
|
||||||
import type { VNode } from 'vue';
|
|
||||||
import type { RawValueType, LabelInValueType, SelectProps } from '../Select';
|
|
||||||
import { isMultiple } from '../BaseSelect';
|
|
||||||
|
|
||||||
function warningProps(props: SelectProps) {
|
|
||||||
const {
|
|
||||||
mode,
|
|
||||||
options,
|
|
||||||
children,
|
|
||||||
backfill,
|
|
||||||
allowClear,
|
|
||||||
placeholder,
|
|
||||||
getInputElement,
|
|
||||||
showSearch,
|
|
||||||
onSearch,
|
|
||||||
defaultOpen,
|
|
||||||
autofocus,
|
|
||||||
labelInValue,
|
|
||||||
value,
|
|
||||||
inputValue,
|
|
||||||
optionLabelProp,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const multiple = isMultiple(mode);
|
|
||||||
const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox';
|
|
||||||
const mergedOptions = options || convertChildrenToData(children);
|
|
||||||
|
|
||||||
// `tags` should not set option as disabled
|
|
||||||
warning(
|
|
||||||
mode !== 'tags' || mergedOptions.every((opt: { disabled?: boolean }) => !opt.disabled),
|
|
||||||
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// `combobox` should not use `optionLabelProp`
|
|
||||||
warning(
|
|
||||||
mode !== 'combobox' || !optionLabelProp,
|
|
||||||
'`combobox` mode not support `optionLabelProp`. Please set `value` on Option directly.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only `combobox` support `backfill`
|
|
||||||
warning(mode === 'combobox' || !backfill, '`backfill` only works with `combobox` mode.');
|
|
||||||
|
|
||||||
// Only `combobox` support `getInputElement`
|
|
||||||
warning(
|
|
||||||
mode === 'combobox' || !getInputElement,
|
|
||||||
'`getInputElement` only work with `combobox` mode.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Customize `getInputElement` should not use `allowClear` & `placeholder`
|
|
||||||
noteOnce(
|
|
||||||
mode !== 'combobox' || !getInputElement || !allowClear || !placeholder,
|
|
||||||
'Customize `getInputElement` should customize clear and placeholder logic instead of configuring `allowClear` and `placeholder`.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// `onSearch` should use in `combobox` or `showSearch`
|
|
||||||
if (onSearch && !mergedShowSearch && mode !== 'combobox' && mode !== 'tags') {
|
|
||||||
warning(false, '`onSearch` should work with `showSearch` instead of use alone.');
|
|
||||||
}
|
|
||||||
|
|
||||||
noteOnce(
|
|
||||||
!defaultOpen || autofocus,
|
|
||||||
'`defaultOpen` makes Select open without focus which means it will not close by click outside. You can set `autofocus` if needed.',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
const values = toArray<RawValueType | LabelInValueType>(value);
|
|
||||||
warning(
|
|
||||||
!labelInValue ||
|
|
||||||
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),
|
|
||||||
'`value` should in shape of `{ value: string | number, label?: any }` when you set `labelInValue` to `true`',
|
|
||||||
);
|
|
||||||
|
|
||||||
warning(
|
|
||||||
!multiple || Array.isArray(value),
|
|
||||||
'`value` should be array when `mode` is `multiple` or `tags`',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Syntactic sugar should use correct children type
|
|
||||||
if (children) {
|
|
||||||
let invalidateChildType = null;
|
|
||||||
children.some((node: any) => {
|
|
||||||
if (!isValidElement(node) || !node.type) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type } = node as { type: { isSelectOption?: boolean; isSelectOptGroup?: boolean } };
|
|
||||||
|
|
||||||
if (type.isSelectOption) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (type.isSelectOptGroup) {
|
|
||||||
const childs = node.children?.default() || [];
|
|
||||||
const allChildrenValid = childs.every((subNode: VNode) => {
|
|
||||||
if (
|
|
||||||
!isValidElement(subNode) ||
|
|
||||||
!node.type ||
|
|
||||||
(subNode.type as { isSelectOption?: boolean }).isSelectOption
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
invalidateChildType = subNode.type;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allChildrenValid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
invalidateChildType = type;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (invalidateChildType) {
|
|
||||||
warning(
|
|
||||||
false,
|
|
||||||
`\`children\` should be \`Select.Option\` or \`Select.OptGroup\` instead of \`${
|
|
||||||
invalidateChildType.displayName || invalidateChildType.name || invalidateChildType
|
|
||||||
}\`.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
warning(
|
|
||||||
inputValue === undefined,
|
|
||||||
'`inputValue` is deprecated, please use `searchValue` instead.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default warningProps;
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// base rc-tree-select@4.6.1
|
||||||
import TreeSelect from './TreeSelect';
|
import TreeSelect from './TreeSelect';
|
||||||
import TreeNode from './TreeNode';
|
import TreeNode from './TreeNode';
|
||||||
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';
|
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue