refactor: select
parent
ca17b5a928
commit
bb91ce7592
|
@ -1,7 +1,7 @@
|
|||
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> {
|
||||
/** Legacy for check if is a Option Group */
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import type { BaseSelectRef } from '../vc-select2';
|
||||
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select2';
|
||||
import type { BaseOptionType, DefaultOptionType } from '../vc-select2/Select';
|
||||
import type { OptionProps } from '../vc-select2/Option';
|
||||
import type { BaseSelectRef } from '../vc-select';
|
||||
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select';
|
||||
import type { BaseOptionType, DefaultOptionType } from '../vc-select/Select';
|
||||
import type { OptionProps } from '../vc-select/Option';
|
||||
import getIcons from './utils/iconUtil';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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> {
|
||||
/** Legacy for check if is a Option Group */
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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 */
|
||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
|
|
@ -1,22 +1,12 @@
|
|||
import TransBtn from './TransBtn';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
|
||||
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 type { PropType } from 'vue';
|
||||
import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
|
||||
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 { isPlatformMac } from './utils/platformUtil';
|
||||
|
||||
|
@ -28,78 +18,28 @@ export interface RefOptionListProps {
|
|||
|
||||
import type { EventHandler } from '../_util/EventInterface';
|
||||
import omit from '../_util/omit';
|
||||
export interface OptionListProps<OptionType extends object> {
|
||||
prefixCls: string;
|
||||
id: string;
|
||||
options: OptionType[];
|
||||
fieldNames?: FieldNames;
|
||||
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,
|
||||
};
|
||||
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<OptionListProps<SelectOptionsType[number]>, { state?: any }>({
|
||||
const OptionList = defineComponent({
|
||||
name: 'OptionList',
|
||||
inheritAttrs: false,
|
||||
slots: ['option'],
|
||||
setup(props) {
|
||||
const itemPrefixCls = computed(() => `${props.prefixCls}-item`);
|
||||
setup(_, { expose, slots }) {
|
||||
const baseProps = useBaseProps();
|
||||
const props = useSelectProps();
|
||||
const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`);
|
||||
|
||||
const memoFlattenOptions = useMemo(
|
||||
() => props.flattenOptions,
|
||||
[() => props.open, () => props.flattenOptions],
|
||||
[() => baseProps.open, () => props.flattenOptions],
|
||||
next => next[0],
|
||||
);
|
||||
|
||||
|
@ -124,7 +64,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
|||
const current = (index + i * offset + len) % len;
|
||||
|
||||
const { group, data } = memoFlattenOptions.value[current];
|
||||
if (!group && !(data as OptionData).disabled) {
|
||||
if (!group && !data.disabled) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +92,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
|||
// Auto active first item when list length or searchValue changed
|
||||
|
||||
watch(
|
||||
[() => memoFlattenOptions.value.length, () => props.searchValue],
|
||||
[() => memoFlattenOptions.value.length, () => baseProps.searchValue],
|
||||
() => {
|
||||
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
|
||||
|
||||
watch(
|
||||
[() => props.open, () => props.searchValue],
|
||||
[() => baseProps.open, () => baseProps.searchValue],
|
||||
() => {
|
||||
if (!props.multiple && props.open && props.values.size === 1) {
|
||||
const value = Array.from(props.values)[0];
|
||||
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);
|
||||
|
@ -174,7 +114,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
|||
}
|
||||
}
|
||||
// Force trigger scrollbar visible when open
|
||||
if (props.open) {
|
||||
if (baseProps.open) {
|
||||
nextTick(() => {
|
||||
listRef.current?.scrollTo(undefined);
|
||||
});
|
||||
|
@ -186,262 +126,253 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
|||
// ========================== Values ==========================
|
||||
const onSelectValue = (value?: RawValueType) => {
|
||||
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
|
||||
if (!props.multiple) {
|
||||
props.onToggleOpen(false);
|
||||
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 || {}) as OptionData;
|
||||
const { value, label, children } = itemData;
|
||||
const itemData = item.data || {};
|
||||
const { value } = itemData;
|
||||
const { group } = item;
|
||||
const attrs = pickAttrs(itemData, true);
|
||||
const mergedLabel = props.childrenAsData ? children : label;
|
||||
const mergedLabel = getLabel(item);
|
||||
return item ? (
|
||||
<div
|
||||
aria-label={typeof mergedLabel === 'string' ? mergedLabel : undefined}
|
||||
aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
|
||||
{...attrs}
|
||||
key={index}
|
||||
role="option"
|
||||
id={`${props.id}_list_${index}`}
|
||||
aria-selected={props.values.has(value)}
|
||||
role={group ? 'presentation' : 'option'}
|
||||
id={`${baseProps.id}_list_${index}`}
|
||||
aria-selected={props.rawValues.has(value)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
return {
|
||||
memoFlattenOptions,
|
||||
renderItem,
|
||||
listRef,
|
||||
state,
|
||||
onListMouseDown,
|
||||
itemPrefixCls,
|
||||
setActive,
|
||||
onSelectValue,
|
||||
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) {
|
||||
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 (isPlatformMac() && ctrlKey) {
|
||||
if (which === KeyCode.N) {
|
||||
offset = 1;
|
||||
} else if (which === KeyCode.P) {
|
||||
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 (props.open) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
break;
|
||||
if (offset !== 0) {
|
||||
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
|
||||
scrollIntoView(nextActiveIndex);
|
||||
setActive(nextActiveIndex, true);
|
||||
}
|
||||
|
||||
// >>> Close
|
||||
case KeyCode.ESC: {
|
||||
props.onToggleOpen(false);
|
||||
if (props.open) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyup: () => {},
|
||||
|
||||
scrollTo: (index: number) => {
|
||||
scrollIntoView(index);
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
render() {
|
||||
const {
|
||||
renderItem,
|
||||
listRef,
|
||||
onListMouseDown,
|
||||
itemPrefixCls,
|
||||
setActive,
|
||||
onSelectValue,
|
||||
memoFlattenOptions,
|
||||
$slots,
|
||||
} = this as any;
|
||||
const {
|
||||
id,
|
||||
childrenAsData,
|
||||
values,
|
||||
height,
|
||||
itemHeight,
|
||||
menuItemSelectedIcon,
|
||||
notFoundContent,
|
||||
virtual,
|
||||
fieldNames,
|
||||
onScroll,
|
||||
onMouseenter,
|
||||
} = this.$props;
|
||||
const renderOption = $slots.option;
|
||||
const { activeIndex } = this.state;
|
||||
const omitFieldNameList = Object.values(fillFieldNames(fieldNames));
|
||||
// ========================== Render ==========================
|
||||
if (memoFlattenOptions.length === 0) {
|
||||
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`}
|
||||
class={`${itemPrefixCls}-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}
|
||||
height={height}
|
||||
itemHeight={itemHeight}
|
||||
fullHeight={false}
|
||||
onMousedown={onListMouseDown}
|
||||
onScroll={onScroll}
|
||||
virtual={virtual}
|
||||
onMouseenter={onMouseenter}
|
||||
v-slots={{
|
||||
default: ({ group, groupOption, data, label, value }, itemIndex) => {
|
||||
const { key } = data;
|
||||
// Group
|
||||
if (group) {
|
||||
<>
|
||||
<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 class={classNames(itemPrefixCls, `${itemPrefixCls}-group`)}>
|
||||
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
}}
|
||||
></List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
OptionList.props = OptionListProps;
|
||||
|
||||
export default OptionList;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* ref:
|
||||
|
@ -29,76 +29,614 @@
|
|||
* - `combobox` mode not support `optionLabelProp`
|
||||
*/
|
||||
|
||||
import type { OptionsType as SelectOptionsType } from './interface';
|
||||
import SelectOptionList from './OptionList';
|
||||
import Option from './Option';
|
||||
import OptGroup from './OptGroup';
|
||||
import { convertChildrenToData as convertSelectChildrenToData } from './utils/legacyUtil';
|
||||
import {
|
||||
getLabeledValue as getSelectLabeledValue,
|
||||
filterOptions as selectDefaultFilterOptions,
|
||||
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 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 { 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]>({
|
||||
prefixCls: 'rc-select',
|
||||
components: {
|
||||
optionList: SelectOptionList as any,
|
||||
},
|
||||
convertChildrenToData: convertSelectChildrenToData,
|
||||
flattenOptions,
|
||||
getLabeledValue: getSelectLabeledValue,
|
||||
filterOptions: selectDefaultFilterOptions,
|
||||
isValueDisabled: isSelectValueDisabled,
|
||||
findValueOption: findSelectValueOption,
|
||||
warningProps,
|
||||
fillOptionsWithMissingValue,
|
||||
});
|
||||
const OMIT_DOM_PROPS = ['inputValue'];
|
||||
|
||||
export type ExportedSelectProps<T extends DefaultValueType = DefaultValueType> = SelectProps<
|
||||
SelectOptionsType[number],
|
||||
T
|
||||
>;
|
||||
export type OnActiveValue = (
|
||||
active: RawValueType,
|
||||
index: number,
|
||||
info?: { source?: 'keyboard' | 'mouse' },
|
||||
) => void;
|
||||
|
||||
export function selectProps<T>() {
|
||||
return selectBaseProps<SelectOptionsType[number], T>();
|
||||
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;
|
||||
}
|
||||
|
||||
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',
|
||||
inheritAttrs: false,
|
||||
Option,
|
||||
OptGroup,
|
||||
props: RefSelect.props,
|
||||
setup(props, { attrs, expose, slots }) {
|
||||
const selectRef = ref();
|
||||
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: () => {
|
||||
focus() {
|
||||
selectRef.value?.focus();
|
||||
},
|
||||
blur: () => {
|
||||
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 (
|
||||
<RefSelect
|
||||
ref={selectRef}
|
||||
{...(props as any)}
|
||||
<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}
|
||||
children={slots.default?.() || []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
export default Select;
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
import Trigger from '../vc-trigger';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { getSlot } from '../_util/props-util';
|
||||
import classNames from '../_util/classNames';
|
||||
import createRef from '../_util/createRef';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import type { RenderDOMFunc } from './interface';
|
||||
import type { DropdownRender } from './interface/generator';
|
||||
import type { Placement } from './generate';
|
||||
import { computed, ref, defineComponent } from 'vue';
|
||||
import type { VueNode } from '../_util/type';
|
||||
import type { DropdownRender, Placement, RenderDOMFunc } from './BaseSelect';
|
||||
|
||||
const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => {
|
||||
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
|
||||
const adjustX = typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
|
||||
|
||||
const getBuiltInPlacements = (adjustX: number) => {
|
||||
return {
|
||||
bottomLeft: {
|
||||
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 {
|
||||
prefixCls: string;
|
||||
disabled: boolean;
|
||||
|
@ -66,103 +72,122 @@ export interface SelectTriggerProps {
|
|||
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,
|
||||
created() {
|
||||
this.popupRef = createRef();
|
||||
},
|
||||
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`;
|
||||
|
||||
methods: {
|
||||
getPopupElement() {
|
||||
return this.popupRef.current;
|
||||
},
|
||||
},
|
||||
let popupNode = popupElement;
|
||||
if (dropdownRender) {
|
||||
popupNode = dropdownRender({ menuNode: popupElement, props });
|
||||
}
|
||||
|
||||
render() {
|
||||
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`;
|
||||
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
|
||||
|
||||
let popupNode = popupElement;
|
||||
if (dropdownRender) {
|
||||
popupNode = dropdownRender({ menuNode: popupElement, props });
|
||||
}
|
||||
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
|
||||
|
||||
const builtInPlacements = getBuiltInPlacements(dropdownMatchSelectWidth);
|
||||
|
||||
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}
|
||||
prefixCls={dropdownPrefixCls}
|
||||
popupTransitionName={mergedTransitionName}
|
||||
popup={<div ref={this.popupRef}>{popupNode}</div>}
|
||||
popupAlign={dropdownAlign}
|
||||
popupVisible={visible}
|
||||
getPopupContainer={getPopupContainer}
|
||||
popupClassName={classNames(dropdownClassName, {
|
||||
[`${dropdownPrefixCls}-empty`]: empty,
|
||||
})}
|
||||
popupStyle={popupStyle}
|
||||
getTriggerDOMNode={getTriggerDOMNode}
|
||||
>
|
||||
{getSlot(this)[0]}
|
||||
</Trigger>
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
|
|
|
@ -16,7 +16,7 @@ interface InputProps {
|
|||
autofocus: boolean;
|
||||
autocomplete: string;
|
||||
editable: boolean;
|
||||
accessibilityIndex: number;
|
||||
activeDescendantId?: string;
|
||||
value: string;
|
||||
open: boolean;
|
||||
tabindex: number | string;
|
||||
|
@ -45,7 +45,7 @@ const Input = defineComponent({
|
|||
autofocus: PropTypes.looseBool,
|
||||
autocomplete: PropTypes.string,
|
||||
editable: PropTypes.looseBool,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
activeDescendantId: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
open: PropTypes.looseBool,
|
||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
|
@ -86,7 +86,7 @@ const Input = defineComponent({
|
|||
autofocus,
|
||||
autocomplete,
|
||||
editable,
|
||||
accessibilityIndex,
|
||||
activeDescendantId,
|
||||
value,
|
||||
onKeydown,
|
||||
onMousedown,
|
||||
|
@ -131,7 +131,7 @@ const Input = defineComponent({
|
|||
'aria-owns': `${id}_list`,
|
||||
'aria-autocomplete': 'list',
|
||||
'aria-controls': `${id}_list`,
|
||||
'aria-activedescendant': `${id}_list_${accessibilityIndex}`,
|
||||
'aria-activedescendant': activeDescendantId,
|
||||
...attrs,
|
||||
value: editable ? value : '',
|
||||
readonly: !editable,
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
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 Input from './Input';
|
||||
import type { Ref, PropType } from 'vue';
|
||||
|
@ -16,6 +8,8 @@ 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
|
||||
|
@ -24,7 +18,7 @@ type SelectorProps = InnerSelectorProps & {
|
|||
// Tags
|
||||
maxTagCount?: number | 'responsive';
|
||||
maxTagTextLength?: number;
|
||||
maxTagPlaceholder?: VueNode | ((omittedValues: LabelValueType[]) => VueNode);
|
||||
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
||||
tokenSeparators?: string[];
|
||||
tagRender?: (props: CustomTagProps) => VueNode;
|
||||
onToggleOpen: any;
|
||||
|
@ -33,7 +27,7 @@ type SelectorProps = InnerSelectorProps & {
|
|||
choiceTransitionName?: string;
|
||||
|
||||
// Event
|
||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||||
onRemove: (value: DisplayValueType) => void;
|
||||
};
|
||||
|
||||
const props = {
|
||||
|
@ -49,7 +43,7 @@ const props = {
|
|||
showSearch: PropTypes.looseBool,
|
||||
autofocus: PropTypes.looseBool,
|
||||
autocomplete: PropTypes.string,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
activeDescendantId: PropTypes.string,
|
||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
|
||||
removeIcon: PropTypes.any,
|
||||
|
@ -58,12 +52,12 @@ const props = {
|
|||
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
maxTagTextLength: PropTypes.number,
|
||||
maxTagPlaceholder: PropTypes.any.def(
|
||||
() => (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
||||
() => (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`,
|
||||
),
|
||||
tagRender: PropTypes.func,
|
||||
|
||||
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
||||
onSelect: PropTypes.func,
|
||||
onRemove: PropTypes.func,
|
||||
onInputChange: PropTypes.func,
|
||||
onInputPaste: PropTypes.func,
|
||||
onInputKeyDown: PropTypes.func,
|
||||
|
@ -111,6 +105,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
// ===================== Render ======================
|
||||
// >>> Render Selector Node. Includes Item & Rest
|
||||
function defaultRenderSelector(
|
||||
title: VueNode,
|
||||
content: VueNode,
|
||||
itemDisabled: boolean,
|
||||
closable?: boolean,
|
||||
|
@ -122,9 +117,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
||||
})}
|
||||
title={
|
||||
typeof content === 'string' || typeof content === 'number'
|
||||
? content.toString()
|
||||
: undefined
|
||||
typeof title === 'string' || typeof title === 'number' ? title.toString() : undefined
|
||||
}
|
||||
>
|
||||
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
||||
|
@ -143,17 +136,17 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
}
|
||||
|
||||
function customizeRenderSelector(
|
||||
value: DefaultValueType,
|
||||
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({
|
||||
|
@ -162,12 +155,14 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
disabled: itemDisabled,
|
||||
closable,
|
||||
onClose,
|
||||
option,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderItem({ disabled: itemDisabled, label, value }: DisplayLabelValueType) {
|
||||
function renderItem(valueItem: DisplayValueType) {
|
||||
const { disabled: itemDisabled, label, value, option } = valueItem;
|
||||
const closable = !props.disabled && !itemDisabled;
|
||||
|
||||
let displayLabel = label;
|
||||
|
@ -183,24 +178,22 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
}
|
||||
const onClose = (event?: MouseEvent) => {
|
||||
if (event) event.stopPropagation();
|
||||
props.onSelect(value, { selected: false });
|
||||
props.onRemove?.(valueItem);
|
||||
};
|
||||
|
||||
return typeof props.tagRender === 'function'
|
||||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
|
||||
: defaultRenderSelector(displayLabel, itemDisabled, closable, onClose);
|
||||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
|
||||
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
||||
}
|
||||
|
||||
function renderRest(omittedValues: DisplayLabelValueType[]) {
|
||||
const {
|
||||
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
||||
} = props;
|
||||
function renderRest(omittedValues: DisplayValueType[]) {
|
||||
const { maxTagPlaceholder = omittedValues => `+ ${omittedValues.length} ...` } = props;
|
||||
const content =
|
||||
typeof maxTagPlaceholder === 'function'
|
||||
? maxTagPlaceholder(omittedValues)
|
||||
: maxTagPlaceholder;
|
||||
|
||||
return defaultRenderSelector(content, false);
|
||||
return defaultRenderSelector(content, content, false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -214,7 +207,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
disabled,
|
||||
autofocus,
|
||||
autocomplete,
|
||||
accessibilityIndex,
|
||||
activeDescendantId,
|
||||
tabindex,
|
||||
onInputChange,
|
||||
onInputPaste,
|
||||
|
@ -241,7 +234,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
autofocus={autofocus}
|
||||
autocomplete={autocomplete}
|
||||
editable={inputEditable.value}
|
||||
accessibilityIndex={accessibilityIndex}
|
||||
activeDescendantId={activeDescendantId}
|
||||
value={inputValue.value}
|
||||
onKeydown={onInputKeyDown}
|
||||
onMousedown={onInputMouseDown}
|
||||
|
|
|
@ -9,7 +9,6 @@ import type { VueNode } from '../../_util/type';
|
|||
interface SelectorProps extends InnerSelectorProps {
|
||||
inputElement: VueNode;
|
||||
activeValue: string;
|
||||
backfill?: boolean;
|
||||
}
|
||||
const props = {
|
||||
inputElement: PropTypes.any,
|
||||
|
@ -25,7 +24,7 @@ const props = {
|
|||
showSearch: PropTypes.looseBool,
|
||||
autofocus: PropTypes.looseBool,
|
||||
autocomplete: PropTypes.string,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
activeDescendantId: PropTypes.string,
|
||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
activeValue: PropTypes.string,
|
||||
backfill: PropTypes.looseBool,
|
||||
|
@ -64,7 +63,7 @@ const SingleSelector = defineComponent<SelectorProps>({
|
|||
|
||||
// Not show text when closed expect combobox mode
|
||||
const hasTextInput = computed(() =>
|
||||
props.mode !== 'combobox' && !props.open ? false : !!inputValue.value,
|
||||
props.mode !== 'combobox' && !props.open && !props.showSearch ? false : !!inputValue.value,
|
||||
);
|
||||
|
||||
const title = computed(() => {
|
||||
|
@ -74,6 +73,18 @@ const SingleSelector = defineComponent<SelectorProps>({
|
|||
: 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,
|
||||
|
@ -84,9 +95,8 @@ const SingleSelector = defineComponent<SelectorProps>({
|
|||
disabled,
|
||||
autofocus,
|
||||
autocomplete,
|
||||
accessibilityIndex,
|
||||
activeDescendantId,
|
||||
open,
|
||||
placeholder,
|
||||
tabindex,
|
||||
onInputKeyDown,
|
||||
onInputMouseDown,
|
||||
|
@ -126,7 +136,7 @@ const SingleSelector = defineComponent<SelectorProps>({
|
|||
autofocus={autofocus}
|
||||
autocomplete={autocomplete}
|
||||
editable={inputEditable.value}
|
||||
accessibilityIndex={accessibilityIndex}
|
||||
activeDescendantId={activeDescendantId}
|
||||
value={inputValue.value}
|
||||
onKeydown={onInputKeyDown}
|
||||
onMousedown={onInputMouseDown}
|
||||
|
@ -150,9 +160,7 @@ const SingleSelector = defineComponent<SelectorProps>({
|
|||
)}
|
||||
|
||||
{/* Display placeholder */}
|
||||
{!item && !hasTextInput.value && (
|
||||
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
|
||||
)}
|
||||
{renderPlaceholder()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
import KeyCode from '../../_util/KeyCode';
|
||||
import MultipleSelector from './MultipleSelector';
|
||||
import SingleSelector from './SingleSelector';
|
||||
import type { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
|
||||
import type { RenderNode, Mode } from '../interface';
|
||||
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';
|
||||
|
@ -20,22 +20,22 @@ 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;
|
||||
/** Display in the Selector value, it's not same as `value` prop */
|
||||
values: LabelValueType[];
|
||||
multiple: boolean;
|
||||
values: DisplayValueType[];
|
||||
multiple?: boolean;
|
||||
mode: Mode;
|
||||
searchValue: string;
|
||||
activeValue: string;
|
||||
inputElement: VueNode;
|
||||
|
||||
autofocus?: boolean;
|
||||
accessibilityIndex: number;
|
||||
activeDescendantId?: string;
|
||||
tabindex?: number | string;
|
||||
disabled?: boolean;
|
||||
placeholder?: VueNode;
|
||||
|
@ -44,7 +44,7 @@ export interface SelectorProps {
|
|||
// Tags
|
||||
maxTagCount?: number | 'responsive';
|
||||
maxTagTextLength?: number;
|
||||
maxTagPlaceholder?: VueNode | ((omittedValues: LabelValueType[]) => VueNode);
|
||||
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
||||
tagRender?: (props: CustomTagProps) => VueNode;
|
||||
|
||||
/** 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: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
|
||||
onSearchSubmit: (searchText: string) => void;
|
||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||||
onRemove: (value: DisplayValueType) => void;
|
||||
onInputKeyDown?: (e: KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
|
@ -66,6 +66,11 @@ export interface SelectorProps {
|
|||
*/
|
||||
domRef: () => HTMLDivElement;
|
||||
}
|
||||
export interface RefSelectorProps {
|
||||
focus: () => void;
|
||||
blur: () => void;
|
||||
scrollTo?: ScrollTo;
|
||||
}
|
||||
|
||||
const Selector = defineComponent<SelectorProps>({
|
||||
name: 'Selector',
|
||||
|
@ -84,7 +89,7 @@ const Selector = defineComponent<SelectorProps>({
|
|||
inputElement: PropTypes.any,
|
||||
|
||||
autofocus: PropTypes.looseBool,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
activeDescendantId: PropTypes.string,
|
||||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
disabled: PropTypes.looseBool,
|
||||
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: PropTypes.func,
|
||||
onSearchSubmit: PropTypes.func,
|
||||
onSelect: PropTypes.func,
|
||||
onRemove: PropTypes.func,
|
||||
onInputKeyDown: { type: Function as PropType<EventHandler> },
|
||||
|
||||
/**
|
||||
|
@ -115,7 +120,7 @@ const Selector = defineComponent<SelectorProps>({
|
|||
*/
|
||||
domRef: PropTypes.func,
|
||||
} as any,
|
||||
setup(props) {
|
||||
setup(props, { expose }) {
|
||||
const inputRef = createRef();
|
||||
let compositionStatus = false;
|
||||
|
||||
|
@ -139,7 +144,7 @@ const Selector = defineComponent<SelectorProps>({
|
|||
props.onSearchSubmit((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
if (![KeyCode.SHIFT, KeyCode.TAB, KeyCode.BACKSPACE, KeyCode.ESC].includes(which)) {
|
||||
if (isValidateOpenKey(which)) {
|
||||
props.onToggleOpen(true);
|
||||
}
|
||||
};
|
||||
|
@ -227,58 +232,44 @@ const Selector = defineComponent<SelectorProps>({
|
|||
props.onToggleOpen();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
expose({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
},
|
||||
blur: () => {
|
||||
inputRef.current.blur();
|
||||
},
|
||||
onMousedown,
|
||||
onClick,
|
||||
onInputPaste,
|
||||
inputRef,
|
||||
onInternalInputKeyDown,
|
||||
onInternalInputMouseDown,
|
||||
onInputChange,
|
||||
onInputCompositionEnd,
|
||||
onInputCompositionStart,
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
},
|
||||
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;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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 { VueNode } from '../../_util/type';
|
||||
import type { Mode, DisplayValueType } from '../BaseSelect';
|
||||
|
||||
export interface InnerSelectorProps {
|
||||
prefixCls: string;
|
||||
|
@ -13,10 +13,10 @@ export interface InnerSelectorProps {
|
|||
disabled?: boolean;
|
||||
autofocus?: boolean;
|
||||
autocomplete?: string;
|
||||
values: LabelValueType[];
|
||||
values: DisplayValueType[];
|
||||
showSearch?: boolean;
|
||||
searchValue: string;
|
||||
accessibilityIndex: number;
|
||||
activeDescendantId: string;
|
||||
open: boolean;
|
||||
tabindex?: number | string;
|
||||
onInputKeyDown: EventHandler;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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: VueNode | ((props?: any) => VueNode);
|
||||
customizeIcon: RenderNode;
|
||||
customizeIconProps?: any;
|
||||
onMousedown?: (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 Option from './Option';
|
||||
import OptGroup from './OptGroup';
|
||||
import { selectBaseProps } from './generate';
|
||||
import type { ExtractPropTypes } from 'vue';
|
||||
import BaseSelect from './BaseSelect';
|
||||
import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
|
||||
import useBaseProps from './hooks/useBaseProps';
|
||||
|
||||
export type SelectProps<T = any> = Partial<ExtractPropTypes<ExportedSelectProps<T>>>;
|
||||
export { Option, OptGroup, selectBaseProps, selectProps };
|
||||
export { Option, OptGroup, selectProps, BaseSelect, useBaseProps };
|
||||
export type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps };
|
||||
|
||||
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[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
|
@ -13,116 +5,8 @@ export function toArray<T>(value: T | T[]): T[] {
|
|||
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 =
|
||||
typeof window !== 'undefined' && window.document && window.document.documentElement;
|
||||
|
||||
/** Is client side and not jsdom */
|
||||
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 type { VNode } from 'vue';
|
||||
import type { OptionData, OptionGroupData, OptionsType } from '../interface';
|
||||
import type { BaseOptionType, DefaultOptionType } from '../Select';
|
||||
import type { VueNode } from '../../_util/type';
|
||||
|
||||
function convertNodeToOption(node: VNode): OptionData {
|
||||
function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||
node: VNode,
|
||||
): OptionType {
|
||||
const {
|
||||
key,
|
||||
children,
|
||||
|
@ -18,13 +20,16 @@ function convertNodeToOption(node: VNode): OptionData {
|
|||
value: value !== undefined ? value : key,
|
||||
children: child,
|
||||
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 [])
|
||||
.map((node: VNode, index: number): OptionData | OptionGroupData | null => {
|
||||
.map((node: VNode, index: number): OptionType | null => {
|
||||
if (!isValidElement(node) || !node.type) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,8 @@
|
|||
import type { BaseOptionType, DefaultOptionType, RawValueType, FieldNames } from '../Select';
|
||||
import { warning } from '../../vc-util/warning';
|
||||
import { cloneVNode, isVNode } from 'vue';
|
||||
import type {
|
||||
OptionsType as SelectOptionsType,
|
||||
OptionData,
|
||||
OptionGroupData,
|
||||
FlattenOptionData,
|
||||
FieldNames,
|
||||
} from '../interface';
|
||||
import type {
|
||||
LabelValueType,
|
||||
FilterFunc,
|
||||
RawValueType,
|
||||
GetLabeledValue,
|
||||
DefaultValueType,
|
||||
} from '../interface/generator';
|
||||
import type { FlattenOptionData } from '../interface';
|
||||
|
||||
import { toArray } from './commonUtil';
|
||||
import type { VueNode } from '../../_util/type';
|
||||
|
||||
function getKey(data: OptionData | OptionGroupData, index: number) {
|
||||
function getKey(data: BaseOptionType, index: number) {
|
||||
const { key } = data;
|
||||
let value: RawValueType;
|
||||
|
||||
|
@ -35,11 +19,11 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
|
|||
return `rc-index-key-${index}`;
|
||||
}
|
||||
|
||||
export function fillFieldNames(fieldNames?: FieldNames) {
|
||||
export function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean) {
|
||||
const { label, value, options } = fieldNames || {};
|
||||
|
||||
return {
|
||||
label: label || 'label',
|
||||
label: label || (childrenAsData ? 'children' : 'label'),
|
||||
value: value || 'value',
|
||||
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.
|
||||
* Here is simply set `key` to the index if not provided.
|
||||
*/
|
||||
export function flattenOptions(
|
||||
options: SelectOptionsType,
|
||||
{ fieldNames }: { fieldNames?: FieldNames } = {},
|
||||
): FlattenOptionData[] {
|
||||
const flattenList: FlattenOptionData[] = [];
|
||||
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);
|
||||
} = fillFieldNames(fieldNames, false);
|
||||
|
||||
function dig(list: SelectOptionsType, isGroupOption: boolean) {
|
||||
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: data[fieldValue],
|
||||
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,
|
||||
label: grpLabel,
|
||||
});
|
||||
|
||||
dig(data[fieldOptions], true);
|
||||
|
@ -97,7 +86,7 @@ export function flattenOptions(
|
|||
/**
|
||||
* Inject `props` into `option` for legacy usage
|
||||
*/
|
||||
function injectPropsWithOption<T>(option: T): T {
|
||||
export function injectPropsWithOption<T>(option: T): T {
|
||||
const newOption = { ...option };
|
||||
if (!('props' in newOption)) {
|
||||
Object.defineProperty(newOption, 'props', {
|
||||
|
@ -114,154 +103,6 @@ function injectPropsWithOption<T>(option: T): T {
|
|||
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[] {
|
||||
if (!tokens || !tokens.length) {
|
||||
return null;
|
||||
|
@ -285,53 +126,3 @@ export function getSeparatedContent(text: string, tokens: string[]): string[] {
|
|||
const list = separate(text, tokens);
|
||||
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 type { SelectProps } from '..';
|
||||
import { convertChildrenToData } from './legacyUtil';
|
||||
import { toArray } from './commonUtil';
|
||||
import type { RawValueType, LabelValueType } from '../interface/generator';
|
||||
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 {
|
||||
|
@ -25,13 +25,13 @@ function warningProps(props: SelectProps) {
|
|||
optionLabelProp,
|
||||
} = props;
|
||||
|
||||
const multiple = mode === 'multiple' || mode === 'tags';
|
||||
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: 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.',
|
||||
);
|
||||
|
||||
|
@ -67,7 +67,7 @@ function warningProps(props: SelectProps) {
|
|||
);
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
const values = toArray<RawValueType | LabelValueType>(value);
|
||||
const values = toArray<RawValueType | LabelInValueType>(value);
|
||||
warning(
|
||||
!labelInValue ||
|
||||
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 TreeNode from './TreeNode';
|
||||
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';
|
||||
|
|
Loading…
Reference in New Issue