refactor: tree-select

pull/4577/head
tangjinzhou 2021-08-21 16:25:55 +08:00
parent b0678d79ee
commit 956ed09885
12 changed files with 433 additions and 302 deletions

15
components/_util/omit.ts Normal file
View File

@ -0,0 +1,15 @@
function omit<T extends object, K extends [...(keyof T)[]]>(
obj: T,
fields: K,
): {
[K2 in Exclude<keyof T, K[number]>]: T[K2];
} {
// eslint-disable-next-line prefer-object-spread
const shallowCopy = Object.assign({}, obj);
for (let i = 0; i < fields.length; i += 1) {
const key = fields[i];
delete shallowCopy[key];
}
return shallowCopy;
}
export default omit;

View File

@ -1,9 +1,9 @@
import type { VNodeChild, App, PropType, Plugin } from 'vue'; import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
import { computed, defineComponent, ref } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import omit from 'omit.js'; import omit from 'omit.js';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import type { SelectProps as RcSelectProps } from '../vc-select'; import type { SelectProps as RcSelectProps } from '../vc-select';
import RcSelect, { Option, OptGroup, BaseProps } from '../vc-select'; import RcSelect, { Option, OptGroup, selectBaseProps } from '../vc-select';
import type { OptionProps as OptionPropsType } from '../vc-select/Option'; import type { OptionProps as OptionPropsType } from '../vc-select/Option';
import getIcons from './utils/iconUtil'; import getIcons from './utils/iconUtil';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
@ -20,36 +20,26 @@ export type OptionType = typeof Option;
export interface LabeledValue { export interface LabeledValue {
key?: string; key?: string;
value: RawValue; value: RawValue;
label: VNodeChild; label: any;
} }
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined; export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
export interface InternalSelectProps<VT> extends Omit<RcSelectProps<VT>, 'mode'> { interface InternalSelectProps<VT> extends Omit<RcSelectProps<VT>, 'mode'> {
suffixIcon?: VNodeChild; suffixIcon?: any;
itemIcon?: VNodeChild; itemIcon?: any;
size?: SizeType; size?: SizeType;
mode?: 'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE'; mode?: 'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE';
bordered?: boolean; bordered?: boolean;
} }
export interface SelectPropsTypes<VT> interface SelectPropsTypes<VT>
extends Omit< extends Omit<InternalSelectProps<VT>, 'inputIcon' | 'mode' | 'getInputElement' | 'backfill'> {
InternalSelectProps<VT>,
'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'class' | 'style'
> {
mode?: 'multiple' | 'tags'; mode?: 'multiple' | 'tags';
} }
export type SelectTypes = SelectPropsTypes<SelectValue>; export type SelectProps = Partial<ExtractPropTypes<SelectPropsTypes<SelectValue>>>;
export const SelectProps = () => ({ export const selectProps = () => ({
...(omit(BaseProps(), [ ...(omit(selectBaseProps(), ['inputIcon', 'mode', 'getInputElement', 'backfill']) as Omit<
'inputIcon', SelectPropsTypes<SelectValue>,
'mode',
'getInputElement',
'backfill',
'class',
'style',
]) as Omit<
ReturnType<typeof BaseProps>,
'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'class' | 'style' 'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'class' | 'style'
>), >),
value: { value: {
@ -58,9 +48,9 @@ export const SelectProps = () => ({
defaultValue: { defaultValue: {
type: [Array, Object, String, Number] as PropType<SelectValue>, type: [Array, Object, String, Number] as PropType<SelectValue>,
}, },
notFoundContent: PropTypes.VNodeChild, notFoundContent: PropTypes.any,
suffixIcon: PropTypes.VNodeChild, suffixIcon: PropTypes.any,
itemIcon: PropTypes.VNodeChild, itemIcon: PropTypes.any,
size: PropTypes.oneOf(tuple('small', 'middle', 'large', 'default')), size: PropTypes.oneOf(tuple('small', 'middle', 'large', 'default')),
mode: PropTypes.oneOf(tuple('multiple', 'tags', 'SECRET_COMBOBOX_MODE_DO_NOT_USE')), mode: PropTypes.oneOf(tuple('multiple', 'tags', 'SECRET_COMBOBOX_MODE_DO_NOT_USE')),
bordered: PropTypes.looseBool.def(true), bordered: PropTypes.looseBool.def(true),
@ -73,7 +63,7 @@ const Select = defineComponent({
Option, Option,
OptGroup, OptGroup,
inheritAttrs: false, inheritAttrs: false,
props: SelectProps(), props: selectProps(),
SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE', SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE',
emits: ['change', 'update:value'], emits: ['change', 'update:value'],
slots: [ slots: [
@ -146,7 +136,7 @@ const Select = defineComponent({
const isMultiple = mode.value === 'multiple' || mode.value === 'tags'; const isMultiple = mode.value === 'multiple' || mode.value === 'tags';
// ===================== Empty ===================== // ===================== Empty =====================
let mergedNotFound: VNodeChild; let mergedNotFound: any;
if (notFoundContent !== undefined) { if (notFoundContent !== undefined) {
mergedNotFound = notFoundContent; mergedNotFound = notFoundContent;
} else if (slots.notFoundContent) { } else if (slots.notFoundContent) {

View File

@ -5,7 +5,7 @@ import classNames from '../_util/classNames';
import pickAttrs from '../_util/pickAttrs'; import pickAttrs from '../_util/pickAttrs';
import { isValidElement } from '../_util/props-util'; import { isValidElement } from '../_util/props-util';
import createRef from '../_util/createRef'; import createRef from '../_util/createRef';
import type { PropType, VNodeChild } from 'vue'; import type { PropType } from 'vue';
import { computed, defineComponent, nextTick, reactive, watch } from 'vue'; import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
import List from '../vc-virtual-list/List'; import List from '../vc-virtual-list/List';
import type { import type {
@ -22,18 +22,18 @@ export interface RefOptionListProps {
onKeyup: (e?: KeyboardEvent) => void; onKeyup: (e?: KeyboardEvent) => void;
scrollTo?: (index: number) => void; scrollTo?: (index: number) => void;
} }
export interface OptionListProps { export interface OptionListProps<OptionType extends object> {
prefixCls: string; prefixCls: string;
id: string; id: string;
options: SelectOptionsType; options: OptionType[];
flattenOptions: FlattenOptionsType<SelectOptionsType>; flattenOptions: FlattenOptionsType<OptionType>;
height: number; height: number;
itemHeight: number; itemHeight: number;
values: Set<RawValueType>; values: Set<RawValueType>;
multiple: boolean; multiple: boolean;
open: boolean; open: boolean;
defaultActiveFirstOption?: boolean; defaultActiveFirstOption?: boolean;
notFoundContent?: VNodeChild; notFoundContent?: any;
menuItemSelectedIcon?: RenderNode; menuItemSelectedIcon?: RenderNode;
childrenAsData: boolean; childrenAsData: boolean;
searchValue: string; searchValue: string;
@ -80,7 +80,7 @@ const OptionListProps = {
* Using virtual list of option display. * Using virtual list of option display.
* Will fallback to dom if use customize render. * Will fallback to dom if use customize render.
*/ */
const OptionList = defineComponent<OptionListProps, { state?: any }>({ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, { state?: any }>({
name: 'OptionList', name: 'OptionList',
inheritAttrs: false, inheritAttrs: false,
slots: ['option'], slots: ['option'],
@ -290,7 +290,7 @@ const OptionList = defineComponent<OptionListProps, { state?: any }>({
virtual, virtual,
onScroll, onScroll,
onMouseenter, onMouseenter,
} = this.$props as OptionListProps; } = this.$props;
const renderOption = $slots.option; const renderOption = $slots.option;
const { activeIndex } = this.state; const { activeIndex } = this.state;
// ========================== Render ========================== // ========================== Render ==========================

View File

@ -47,9 +47,8 @@ import generateSelector from './generate';
import type { DefaultValueType } from './interface/generator'; import type { DefaultValueType } from './interface/generator';
import warningProps from './utils/warningPropsUtil'; import warningProps from './utils/warningPropsUtil';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import omit from 'lodash-es/omit';
const RefSelect = generateSelector<SelectOptionsType>({ const RefSelect = generateSelector<SelectOptionsType[number]>({
prefixCls: 'rc-select', prefixCls: 'rc-select',
components: { components: {
optionList: SelectOptionList as any, optionList: SelectOptionList as any,
@ -64,10 +63,17 @@ const RefSelect = generateSelector<SelectOptionsType>({
fillOptionsWithMissingValue, fillOptionsWithMissingValue,
}); });
export type ExportedSelectProps<ValueType extends DefaultValueType = DefaultValueType> = export type ExportedSelectProps<T extends DefaultValueType = DefaultValueType> = SelectProps<
SelectProps<SelectOptionsType, ValueType>; SelectOptionsType[number],
T
>;
const Select = defineComponent<Omit<ExportedSelectProps, 'children'>>({ const Select = defineComponent({
name: 'Select',
inheritAttrs: false,
Option: Option,
OptGroup: OptGroup,
props: RefSelect.props,
setup(props, { attrs, expose, slots }) { setup(props, { attrs, expose, slots }) {
const selectRef = ref(null); const selectRef = ref(null);
expose({ expose({
@ -91,8 +97,4 @@ const Select = defineComponent<Omit<ExportedSelectProps, 'children'>>({
}; };
}, },
}); });
Select.inheritAttrs = false;
Select.props = omit(RefSelect.props, ['children']);
Select.Option = Option;
Select.OptGroup = OptGroup;
export default Select; export default Select;

View File

@ -57,7 +57,7 @@ export interface SelectorProps {
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean; onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
onSearchSubmit: (searchText: string) => void; onSearchSubmit: (searchText: string) => void;
onSelect: (value: RawValueType, option: { selected: boolean }) => void; onSelect: (value: RawValueType, option: { selected: boolean }) => void;
onInputKeyDown?: EventHandlerNonNull; onInputKeyDown?: (e: KeyboardEvent) => void;
/** /**
* @private get real dom for trigger align. * @private get real dom for trigger align.

View File

@ -11,14 +11,13 @@ import KeyCode from '../_util/KeyCode';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import Selector from './Selector'; import Selector from './Selector';
import SelectTrigger from './SelectTrigger'; import SelectTrigger from './SelectTrigger';
import type { RenderNode, Mode, RenderDOMFunc, OnActiveValue } from './interface'; import type { Mode, RenderDOMFunc, OnActiveValue } from './interface';
import type { import type {
GetLabeledValue, GetLabeledValue,
FilterOptions, FilterOptions,
FilterFunc, FilterFunc,
DefaultValueType, DefaultValueType,
RawValueType, RawValueType,
LabelValueType,
Key, Key,
DisplayLabelValueType, DisplayLabelValueType,
FlattenOptionsType, FlattenOptionsType,
@ -26,7 +25,6 @@ import type {
OnClear, OnClear,
SelectSource, SelectSource,
CustomTagProps, CustomTagProps,
DropdownRender,
} from './interface/generator'; } from './interface/generator';
import { INTERNAL_PROPS_MARK } from './interface/generator'; import { INTERNAL_PROPS_MARK } from './interface/generator';
import type { OptionListProps } from './OptionList'; import type { OptionListProps } from './OptionList';
@ -38,7 +36,7 @@ import { getSeparatedContent } from './utils/valueUtil';
import useSelectTriggerControl from './hooks/useSelectTriggerControl'; import useSelectTriggerControl from './hooks/useSelectTriggerControl';
import useCacheDisplayValue from './hooks/useCacheDisplayValue'; import useCacheDisplayValue from './hooks/useCacheDisplayValue';
import useCacheOptions from './hooks/useCacheOptions'; import useCacheOptions from './hooks/useCacheOptions';
import type { CSSProperties, DefineComponent, VNode, VNodeChild } from 'vue'; import type { CSSProperties, DefineComponent, PropType, VNode, VNodeChild } from 'vue';
import { import {
computed, computed,
defineComponent, defineComponent,
@ -50,8 +48,7 @@ import {
watchEffect, watchEffect,
} from 'vue'; } from 'vue';
import createRef from '../_util/createRef'; import createRef from '../_util/createRef';
import PropTypes, { withUndefined } from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import warning from '../_util/warning'; import warning from '../_util/warning';
import isMobile from '../vc-util/isMobile'; import isMobile from '../vc-util/isMobile';
@ -68,248 +65,177 @@ const DEFAULT_OMIT_PROPS = [
'tabindex', 'tabindex',
]; ];
export const BaseProps = () => ({ export function selectBaseProps<OptionType, ValueType>() {
prefixCls: PropTypes.string, return {
id: PropTypes.string, prefixCls: String,
class: PropTypes.string, id: String,
style: PropTypes.any,
// Options // Options
options: PropTypes.array, options: { type: Array as PropType<OptionType[]> },
mode: PropTypes.string, mode: { type: String as PropType<Mode> },
// Value // Value
value: PropTypes.any, value: { type: [String, Number, Object, Array] as PropType<ValueType> },
defaultValue: PropTypes.any, defaultValue: { type: [String, Number, Object, Array] as PropType<ValueType> },
labelInValue: PropTypes.looseBool, labelInValue: { type: Boolean, default: undefined },
// Search // Search
inputValue: PropTypes.string, inputValue: String,
searchValue: PropTypes.string, searchValue: String,
optionFilterProp: PropTypes.string, optionFilterProp: String,
/** /**
* In Select, `false` means do nothing. * In Select, `false` means do nothing.
* In TreeSelect, `false` will highlight match item. * In TreeSelect, `false` will highlight match item.
* It's by design. * It's by design.
*/ */
filterOption: PropTypes.any, filterOption: {
filterSort: PropTypes.func, type: [Boolean, Function] as PropType<boolean | FilterFunc<OptionType>>,
showSearch: PropTypes.looseBool, default: undefined,
autoClearSearchValue: PropTypes.looseBool, },
onSearch: PropTypes.func, filterSort: {
onClear: PropTypes.func, type: Function as PropType<(optionA: OptionType, optionB: OptionType) => number>,
},
showSearch: { type: Boolean, default: undefined },
autoClearSearchValue: { type: Boolean, default: undefined },
onSearch: { type: Function as PropType<(value: string) => void> },
onClear: { type: Function as PropType<OnClear> },
// Icons // Icons
allowClear: PropTypes.looseBool, allowClear: { type: Boolean, default: undefined },
clearIcon: PropTypes.VNodeChild, clearIcon: PropTypes.any,
showArrow: PropTypes.looseBool, showArrow: { type: Boolean, default: undefined },
inputIcon: PropTypes.VNodeChild, inputIcon: PropTypes.VNodeChild,
removeIcon: PropTypes.VNodeChild, removeIcon: PropTypes.VNodeChild,
menuItemSelectedIcon: PropTypes.VNodeChild, menuItemSelectedIcon: PropTypes.VNodeChild,
// Dropdown // Dropdown
open: PropTypes.looseBool, open: { type: Boolean, default: undefined },
defaultOpen: PropTypes.looseBool, defaultOpen: { type: Boolean, default: undefined },
listHeight: PropTypes.number, listHeight: Number,
listItemHeight: PropTypes.number, listItemHeight: Number,
dropdownStyle: PropTypes.object, dropdownStyle: { type: Function as PropType<CSSProperties> },
dropdownClassName: PropTypes.string, dropdownClassName: String,
dropdownMatchSelectWidth: withUndefined(PropTypes.oneOfType([Boolean, Number])), dropdownMatchSelectWidth: {
virtual: PropTypes.looseBool, type: [Boolean, Number] as PropType<boolean | number>,
dropdownRender: PropTypes.func, default: undefined,
},
virtual: { type: Boolean, default: undefined },
dropdownRender: { type: Function as PropType<(menu: VNode) => any> },
dropdownAlign: PropTypes.any, dropdownAlign: PropTypes.any,
animation: PropTypes.string, animation: String,
transitionName: PropTypes.string, transitionName: String,
getPopupContainer: PropTypes.func, getPopupContainer: { type: Function as PropType<RenderDOMFunc> },
direction: PropTypes.string, direction: String,
// Others // Others
disabled: PropTypes.looseBool, disabled: { type: Boolean, default: undefined },
loading: PropTypes.looseBool, loading: { type: Boolean, default: undefined },
autofocus: PropTypes.looseBool, autofocus: { type: Boolean, default: undefined },
defaultActiveFirstOption: PropTypes.looseBool, defaultActiveFirstOption: { type: Boolean, default: undefined },
notFoundContent: PropTypes.VNodeChild, notFoundContent: PropTypes.any,
placeholder: PropTypes.VNodeChild, placeholder: PropTypes.any,
backfill: PropTypes.looseBool, backfill: { type: Boolean, default: undefined },
getInputElement: PropTypes.func, /** @private Internal usage. Do not use in your production. */
optionLabelProp: PropTypes.string, getInputElement: { type: Function as PropType<() => any> },
maxTagTextLength: PropTypes.number, optionLabelProp: String,
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), maxTagTextLength: Number,
maxTagCount: { type: [String, Number] as PropType<number | 'responsive'> },
maxTagPlaceholder: PropTypes.any, maxTagPlaceholder: PropTypes.any,
tokenSeparators: PropTypes.array, tokenSeparators: { type: Array as PropType<string[]> },
tagRender: PropTypes.func, tagRender: { type: Function as PropType<(props: CustomTagProps) => any> },
showAction: PropTypes.array, showAction: { type: Array as PropType<('focus' | 'click')[]> },
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), tabindex: { type: [Number, String] },
// Events // Events
onKeyup: PropTypes.func, onKeyup: { type: Function as PropType<(e: KeyboardEvent) => void> },
onKeydown: PropTypes.func, onKeydown: { type: Function as PropType<(e: KeyboardEvent) => void> },
onPopupScroll: PropTypes.func, onPopupScroll: { type: Function as PropType<(e: UIEvent) => void> },
onDropdownVisibleChange: PropTypes.func, onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> },
onSelect: PropTypes.func, onSelect: {
onDeselect: PropTypes.func, type: Function as PropType<(value: SingleType<ValueType>, option: OptionType) => void>,
onInputKeyDown: PropTypes.func, },
onClick: PropTypes.func, onDeselect: {
onChange: PropTypes.func, type: Function as PropType<(value: SingleType<ValueType>, option: OptionType) => void>,
onBlur: PropTypes.func, },
onFocus: PropTypes.func, onInputKeyDown: { type: Function as PropType<(e: KeyboardEvent) => void> },
onMousedown: PropTypes.func, onClick: { type: Function as PropType<(e: MouseEvent) => void> },
onMouseenter: PropTypes.func, onChange: {
onMouseleave: PropTypes.func, type: Function as PropType<(value: ValueType, option: OptionType | OptionType[]) => void>,
},
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
onMousedown: { type: Function as PropType<(e: MouseEvent) => void> },
onMouseenter: { type: Function as PropType<(e: MouseEvent) => void> },
onMouseleave: { type: Function as PropType<(e: MouseEvent) => void> },
// Motion // Motion
choiceTransitionName: PropTypes.string, choiceTransitionName: String,
// Internal props // Internal props
/** /**
* Only used in current version for internal event process. * Only used in current version for internal event process.
* Do not use in production environment. * Do not use in production environment.
*/ */
internalProps: PropTypes.object, internalProps: {
children: PropTypes.array, type: Object as PropType<{
});
export interface SelectProps<OptionsType extends object[], ValueType> {
prefixCls?: string;
id?: string;
class?: string;
style?: CSSProperties;
// Options
options?: OptionsType;
children?: any[];
mode?: Mode;
// Value
value?: ValueType;
defaultValue?: ValueType;
labelInValue?: boolean;
// Search
inputValue?: string;
searchValue?: string;
optionFilterProp?: string;
/**
* In Select, `false` means do nothing.
* In TreeSelect, `false` will highlight match item.
* It's by design.
*/
filterOption?: boolean | FilterFunc<OptionsType[number]>;
filterSort?: (optionA: OptionsType[number], optionB: OptionsType[number]) => number;
showSearch?: boolean;
autoClearSearchValue?: boolean;
onSearch?: (value: string) => void;
onClear?: OnClear;
// Icons
allowClear?: boolean;
clearIcon?: VNodeChild;
showArrow?: boolean;
inputIcon?: RenderNode;
removeIcon?: VNodeChild;
menuItemSelectedIcon?: RenderNode;
// Dropdown
open?: boolean;
defaultOpen?: boolean;
listHeight?: number;
listItemHeight?: number;
dropdownStyle?: CSSProperties;
dropdownClassName?: string;
dropdownMatchSelectWidth?: boolean | number;
virtual?: boolean;
dropdownRender?: DropdownRender;
dropdownAlign?: any;
animation?: string;
transitionName?: string;
getPopupContainer?: RenderDOMFunc;
direction?: string;
// Others
disabled?: boolean;
loading?: boolean;
autofocus?: boolean;
defaultActiveFirstOption?: boolean;
notFoundContent?: VNodeChild;
placeholder?: VNodeChild;
backfill?: boolean;
getInputElement?: () => VNodeChild | JSX.Element;
optionLabelProp?: string;
maxTagTextLength?: number;
maxTagCount?: number | 'responsive';
maxTagPlaceholder?: VNodeChild | ((omittedValues: LabelValueType[]) => VNodeChild);
tokenSeparators?: string[];
tagRender?: (props: CustomTagProps) => VNodeChild;
showAction?: ('focus' | 'click')[];
tabindex?: number | string;
// Events
onKeyup?: EventHandlerNonNull;
onKeydown?: EventHandlerNonNull;
onPopupScroll?: EventHandlerNonNull;
onDropdownVisibleChange?: (open: boolean) => void;
onSelect?: (value: SingleType<ValueType>, option: OptionsType[number]) => void;
onDeselect?: (value: SingleType<ValueType>, option: OptionsType[number]) => void;
onInputKeyDown?: EventHandlerNonNull;
onClick?: EventHandlerNonNull;
onChange?: (value: ValueType, option: OptionsType[number] | OptionsType) => void;
onBlur?: EventHandlerNonNull;
onFocus?: EventHandlerNonNull;
onMousedown?: EventHandlerNonNull;
onMouseenter?: EventHandlerNonNull;
onMouseleave?: EventHandlerNonNull;
// Motion
choiceTransitionName?: string;
// Internal props
/**
* Only used in current version for internal event process.
* Do not use in production environment.
*/
internalProps?: {
mark?: string; mark?: string;
onClear?: OnClear; onClear?: OnClear;
skipTriggerChange?: boolean; skipTriggerChange?: boolean;
skipTriggerSelect?: boolean; skipTriggerSelect?: boolean;
onRawSelect?: (value: RawValueType, option: OptionsType[number], source: SelectSource) => void; onRawSelect?: (value: RawValueType, option: OptionType, source: SelectSource) => void;
onRawDeselect?: ( onRawDeselect?: (value: RawValueType, option: OptionType, source: SelectSource) => void;
value: RawValueType, }>,
option: OptionsType[number], default: undefined as {
source: SelectSource, mark?: string;
) => void; onClear?: OnClear;
skipTriggerChange?: boolean;
skipTriggerSelect?: boolean;
onRawSelect?: (value: RawValueType, option: OptionType, source: SelectSource) => void;
onRawDeselect?: (value: RawValueType, option: OptionType, source: SelectSource) => void;
},
},
children: Array,
}; };
} }
export interface GenerateConfig<OptionsType extends object[]> { class Helper<T1, T2> {
SelectBaseProps = selectBaseProps<T1, T2>();
}
type FuncReturnType<T1, T2> = Helper<T1, T2>['SelectBaseProps'];
export type SelectProps<T1, T2> = FuncReturnType<T1, T2>;
export interface GenerateConfig<OptionType extends object> {
prefixCls: string; prefixCls: string;
components: { components: {
optionList: DefineComponent<Omit<OptionListProps, 'options'> & { options?: OptionsType }>; optionList: DefineComponent<
Omit<OptionListProps<OptionType>, 'options'> & { options?: OptionType[] }
>;
}; };
/** Convert jsx tree into `OptionsType` */ /** Convert jsx tree into `OptionType[]` */
convertChildrenToData: (children: VNodeChild | JSX.Element) => OptionsType; convertChildrenToData: (children: VNodeChild | JSX.Element) => OptionType[];
/** Flatten nest options into raw option list */ /** Flatten nest options into raw option list */
flattenOptions: (options: OptionsType, props: any) => FlattenOptionsType<OptionsType>; flattenOptions: (options: OptionType[], props: any) => FlattenOptionsType<OptionType>;
/** Convert single raw value into { label, value } format. Will be called by each value */ /** Convert single raw value into { label, value } format. Will be called by each value */
getLabeledValue: GetLabeledValue<FlattenOptionsType<OptionsType>>; getLabeledValue: GetLabeledValue<FlattenOptionsType<OptionType>>;
filterOptions: FilterOptions<OptionsType>; filterOptions: FilterOptions<OptionType[]>;
findValueOption: // Need still support legacy ts api findValueOption: // Need still support legacy ts api
| ((values: RawValueType[], options: FlattenOptionsType<OptionsType>) => OptionsType) | ((values: RawValueType[], options: FlattenOptionsType<OptionType>) => OptionType[])
// New API add prevValueOptions support // New API add prevValueOptions support
| (( | ((
values: RawValueType[], values: RawValueType[],
options: FlattenOptionsType<OptionsType>, options: FlattenOptionsType<OptionType>,
info?: { prevValueOptions?: OptionsType[] }, info?: { prevValueOptions?: OptionType[][] },
) => OptionsType); ) => OptionType[]);
/** Check if a value is disabled */ /** Check if a value is disabled */
isValueDisabled: (value: RawValueType, options: FlattenOptionsType<OptionsType>) => boolean; isValueDisabled: (value: RawValueType, options: FlattenOptionsType<OptionType>) => boolean;
warningProps?: (props: any) => void; warningProps?: (props: any) => void;
fillOptionsWithMissingValue?: ( fillOptionsWithMissingValue?: (
options: OptionsType, options: OptionType[],
value: DefaultValueType, value: DefaultValueType,
optionLabelProp: string, optionLabelProp: string,
labelInValue: boolean, labelInValue: boolean,
) => OptionsType; ) => OptionType[];
omitDOMProps?: (props: object) => object; omitDOMProps?: (props: object) => object;
} }
type ValueType = DefaultValueType; type ValueType = DefaultValueType;
@ -318,13 +244,13 @@ type ValueType = DefaultValueType;
* Do not use it in your prod env since we may refactor this. * Do not use it in your prod env since we may refactor this.
*/ */
export default function generateSelector< export default function generateSelector<
OptionsType extends { OptionType extends {
value?: RawValueType; value?: RawValueType;
label?: VNodeChild; label?: any;
key?: Key; key?: Key;
disabled?: boolean; disabled?: boolean;
}[], },
>(config: GenerateConfig<OptionsType>) { >(config: GenerateConfig<OptionType>) {
const { const {
prefixCls: defaultPrefixCls, prefixCls: defaultPrefixCls,
components: { optionList: OptionList }, components: { optionList: OptionList },
@ -338,10 +264,12 @@ export default function generateSelector<
fillOptionsWithMissingValue, fillOptionsWithMissingValue,
omitDOMProps, omitDOMProps,
} = config as any; } = config as any;
const Select = defineComponent<SelectProps<OptionsType, ValueType>>({ const Select = defineComponent({
name: 'Select', name: 'Select',
slots: ['option'], slots: ['option'],
setup(props: SelectProps<OptionsType, ValueType>) { inheritAttrs: false,
props: selectBaseProps<OptionType, DefaultValueType>(),
setup(props) {
const useInternalProps = computed( const useInternalProps = computed(
() => props.internalProps && props.internalProps.mark === INTERNAL_PROPS_MARK, () => props.internalProps && props.internalProps.mark === INTERNAL_PROPS_MARK,
); );
@ -440,7 +368,7 @@ export default function generateSelector<
return mergedSearchValue; return mergedSearchValue;
}); });
const mergedOptions = computed((): OptionsType => { const mergedOptions = computed((): OptionType[] => {
let newOptions = props.options; let newOptions = props.options;
if (newOptions === undefined) { if (newOptions === undefined) {
newOptions = convertChildrenToData(props.children); newOptions = convertChildrenToData(props.children);
@ -459,7 +387,7 @@ export default function generateSelector<
); );
} }
return newOptions || ([] as OptionsType); return newOptions || ([] as OptionType[]);
}); });
const mergedFlattenOptions = computed(() => flattenOptions(mergedOptions.value, props)); const mergedFlattenOptions = computed(() => flattenOptions(mergedOptions.value, props));
@ -467,12 +395,12 @@ export default function generateSelector<
const getValueOption = useCacheOptions(mergedFlattenOptions); const getValueOption = useCacheOptions(mergedFlattenOptions);
// Display options for OptionList // Display options for OptionList
const displayOptions = computed<OptionsType>(() => { const displayOptions = computed<OptionType[]>(() => {
if (!mergedSearchValue.value || !mergedShowSearch.value) { if (!mergedSearchValue.value || !mergedShowSearch.value) {
return [...mergedOptions.value] as OptionsType; return [...mergedOptions.value] as OptionType[];
} }
const { optionFilterProp = 'value', mode, filterOption } = props; const { optionFilterProp = 'value', mode, filterOption } = props;
const filteredOptions: OptionsType = filterOptions( const filteredOptions: OptionType[] = filterOptions(
mergedSearchValue.value, mergedSearchValue.value,
mergedOptions.value, mergedOptions.value,
{ {
@ -489,10 +417,10 @@ export default function generateSelector<
value: mergedSearchValue.value, value: mergedSearchValue.value,
label: mergedSearchValue.value, label: mergedSearchValue.value,
key: '__RC_SELECT_TAG_PLACEHOLDER__', key: '__RC_SELECT_TAG_PLACEHOLDER__',
}); } as OptionType);
} }
if (props.filterSort && Array.isArray(filteredOptions)) { if (props.filterSort && Array.isArray(filteredOptions)) {
return ([...filteredOptions] as OptionsType).sort(props.filterSort); return ([...filteredOptions] as OptionType[]).sort(props.filterSort);
} }
return filteredOptions; return filteredOptions;
@ -591,9 +519,9 @@ export default function generateSelector<
return; return;
} }
const newRawValuesOptions = getValueOption(newRawValues); const newRawValuesOptions = getValueOption(newRawValues);
const outValues = toOuterValues<FlattenOptionsType<OptionsType>>(Array.from(newRawValues), { const outValues = toOuterValues<FlattenOptionsType<OptionType>>(Array.from(newRawValues), {
labelInValue: mergedLabelInValue.value, labelInValue: mergedLabelInValue.value,
options: newRawValuesOptions, options: newRawValuesOptions as any,
getLabeledValue, getLabeledValue,
prevValueMap: mergedValueMap.value, prevValueMap: mergedValueMap.value,
optionLabelProp: mergedOptionLabelProp.value, optionLabelProp: mergedOptionLabelProp.value,
@ -872,7 +800,7 @@ export default function generateSelector<
}; };
// KeyUp // KeyUp
const onInternalKeyUp = (event: Event) => { const onInternalKeyUp = (event: KeyboardEvent) => {
if (mergedOpen.value && listRef.value) { if (mergedOpen.value && listRef.value) {
listRef.value.onKeyup(event); listRef.value.onKeyup(event);
} }
@ -1098,7 +1026,6 @@ export default function generateSelector<
} = this as any; } = this as any;
const { const {
prefixCls = defaultPrefixCls, prefixCls = defaultPrefixCls,
class: className,
id, id,
open, open,
@ -1172,7 +1099,7 @@ export default function generateSelector<
internalProps = {}, internalProps = {},
...restProps ...restProps
} = this.$props as SelectProps<OptionsType, ValueType>; } = this.$props; //as SelectProps<OptionType[], ValueType>;
// ============================= Input ============================== // ============================= Input ==============================
// Only works in `combobox` // Only works in `combobox`
@ -1267,7 +1194,7 @@ export default function generateSelector<
} }
// ============================= Render ============================= // ============================= Render =============================
const mergedClassName = classNames(prefixCls, className, { const mergedClassName = classNames(prefixCls, this.$attrs.class, {
[`${prefixCls}-focused`]: mockFocused, [`${prefixCls}-focused`]: mockFocused,
[`${prefixCls}-multiple`]: isMultiple, [`${prefixCls}-multiple`]: isMultiple,
[`${prefixCls}-single`]: !isMultiple, [`${prefixCls}-single`]: !isMultiple,
@ -1282,6 +1209,7 @@ export default function generateSelector<
return ( return (
<div <div
{...this.$attrs}
class={mergedClassName} class={mergedClassName}
{...domProps} {...domProps}
ref="containerRef" ref="containerRef"
@ -1355,6 +1283,5 @@ export default function generateSelector<
); );
}, },
}); });
Select.props = initDefaultProps(BaseProps(), {});
return Select; return Select;
} }

View File

@ -1,17 +1,17 @@
import type { Ref, VNodeChild } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { computed } from 'vue';
import type { RawValueType, FlattenOptionsType, Key } from '../interface/generator'; import type { RawValueType, FlattenOptionsType, Key } from '../interface/generator';
export default function useCacheOptions< export default function useCacheOptions<
OptionsType extends { OptionType extends {
value?: RawValueType; value?: RawValueType;
label?: VNodeChild; label?: any;
key?: Key; key?: Key;
disabled?: boolean; disabled?: boolean;
}[], },
>(options: Ref) { >(options: Ref) {
const optionMap = computed(() => { const optionMap = computed(() => {
const map: Map<RawValueType, FlattenOptionsType<OptionsType>[number]> = new Map(); const map: Map<RawValueType, FlattenOptionsType<OptionType>[number]> = new Map();
options.value.forEach((item: any) => { options.value.forEach((item: any) => {
const { const {
data: { value }, data: { value },
@ -21,7 +21,7 @@ export default function useCacheOptions<
return map; return map;
}); });
const getValueOption = (vals: RawValueType[]): FlattenOptionsType<OptionsType> => const getValueOption = (vals: RawValueType[]) =>
vals.map(value => optionMap.value.get(value)).filter(Boolean); vals.map(value => optionMap.value.get(value)).filter(Boolean);
return getValueOption; return getValueOption;

View File

@ -2,9 +2,9 @@ import type { ExportedSelectProps } from './Select';
import Select from './Select'; import Select from './Select';
import Option from './Option'; import Option from './Option';
import OptGroup from './OptGroup'; import OptGroup from './OptGroup';
import { BaseProps } from './generate'; import { selectBaseProps } from './generate';
export type SelectProps<T = any> = ExportedSelectProps<T>; export type SelectProps<T = any> = ExportedSelectProps<T>;
export { Option, OptGroup, BaseProps }; export { Option, OptGroup, selectBaseProps };
export default Select; export default Select;

View File

@ -56,9 +56,11 @@ export type FilterOptions<OptionsType extends object[]> = (
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean; export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
export type FlattenOptionsType<OptionsType extends object[] = object[]> = { export type FlattenOptionsType<OptionType = object> = {
key: Key; key: Key;
data: OptionsType[number]; data: OptionType;
label?: any;
value?: RawValueType;
/** Used for customize data */ /** Used for customize data */
[name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}[]; }[];

View File

@ -1,6 +1,100 @@
import { GenerateConfig } from '../vc-select/generate'; import generateSelector, { GenerateConfig } from '../vc-select/generate';
import TreeNode from './TreeNode';
import type {
Key,
DefaultValueType,
DataNode,
LabelValueType,
SimpleModeConfig,
RawValueType,
ChangeEventExtra,
LegacyDataNode,
SelectSource,
FlattenDataNode,
FieldNames,
} from './interface';
import {
flattenOptions,
filterOptions,
isValueDisabled,
findValueOption,
addValue,
removeValue,
getRawValueLabeled,
toArray,
fillFieldNames,
} from './utils/valueUtil';
import warningProps from './utils/warningPropsUtil';
import { SelectContext } from './Context';
import useTreeData from './hooks/useTreeData';
import useKeyValueMap from './hooks/useKeyValueMap';
import useKeyValueMapping from './hooks/useKeyValueMapping';
import type { CheckedStrategy } from './utils/strategyUtil';
import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil';
import { fillAdditionalInfo } from './utils/legacyUtil';
import useSelectValues from './hooks/useSelectValues';
import { treeSelectProps, TreeSelectProps } from './props';
import { getLabeledValue } from '../vc-select/utils/valueUtil';
import omit from '../_util/omit';
import { defineComponent } from 'vue';
const OMIT_PROPS: (keyof TreeSelectProps)[] = [
'expandedKeys' as any,
'treeData',
'treeCheckable',
'showCheckedStrategy',
'searchPlaceholder',
'treeLine',
'treeIcon',
'showTreeIcon',
'switcherIcon',
'treeNodeFilterProp',
'filterTreeNode',
'dropdownPopupAlign',
'treeDefaultExpandAll',
'treeCheckStrictly',
'treeExpandedKeys',
'treeLoadedKeys',
'treeMotion',
'onTreeExpand',
'onTreeLoad',
'labelRender',
'loadData',
'treeDataSimpleMode',
'treeNodeLabelProp',
'treeDefaultExpandedKeys',
];
export default function generate(config: { export default function generate(config: {
prefixCls: string; prefixCls: string;
optionList: GenerateConfig<any>['components']['optionList']; optionList: GenerateConfig<DataNode>['components']['optionList'];
}) {} }) {
const { prefixCls, optionList } = config;
const RefSelect = generateSelector<DataNode>({
prefixCls,
components: {
optionList,
},
// Not use generate since we will handle ourself
convertChildrenToData: () => null,
flattenOptions,
// Handle `optionLabelProp` in TreeSelect component
getLabeledValue: getLabeledValue as any,
filterOptions,
isValueDisabled,
findValueOption,
omitDOMProps: (props: TreeSelectProps<any>) => omit(props, OMIT_PROPS),
});
return defineComponent({
props: treeSelectProps(),
slots: [],
name: 'TreeSelect',
setup(props) {
return () => {
return null;
};
},
});
}

View File

@ -1,7 +1,21 @@
import type { ExtractPropTypes, PropType } from 'vue'; import type { ExtractPropTypes, PropType } from 'vue';
import type { DataNode } from '../tree'; import type { DataNode } from '../tree';
import { selectBaseProps } from '../vc-select';
import type { FilterFunc } from '../vc-select/interface/generator';
import omit from '../_util/omit';
import type { Key } from '../_util/type';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import type { FlattenDataNode, RawValueType } from './interface'; import type {
ChangeEventExtra,
DefaultValueType,
FieldNames,
FlattenDataNode,
LabelValueType,
LegacyDataNode,
RawValueType,
SimpleModeConfig,
} from './interface';
import type { CheckedStrategy } from './utils/strategyUtil';
export function optionListProps<OptionsType>() { export function optionListProps<OptionsType>() {
return { return {
@ -33,6 +47,94 @@ export function optionListProps<OptionsType>() {
}; };
} }
export type OptionListProps = Partial< export function treeSelectProps<ValueType = DefaultValueType>() {
Omit<ExtractPropTypes<ReturnType<typeof optionListProps>>, 'options'> & { options: DataNode[] } const selectProps = omit(selectBaseProps<DataNode, ValueType>(), [
'onChange',
'mode',
'menuItemSelectedIcon',
'dropdownAlign',
'backfill',
'getInputElement',
'optionLabelProp',
'tokenSeparators',
'filterOption',
]);
return {
...selectProps,
multiple: { type: Boolean, default: undefined },
showArrow: { type: Boolean, default: undefined },
showSearch: { type: Boolean, default: undefined },
open: { type: Boolean, default: undefined },
defaultOpen: { type: Boolean, default: undefined },
value: { type: [String, Number, Object, Array] as PropType<ValueType> },
defaultValue: { type: [String, Number, Object, Array] as PropType<ValueType> },
disabled: { type: Boolean, default: undefined },
placeholder: PropTypes.any,
/** @deprecated Use `searchValue` instead */
inputValue: String,
searchValue: String,
autoClearSearchValue: { type: Boolean, default: undefined },
maxTagPlaceholder: { type: Function as PropType<(omittedValues: LabelValueType[]) => any> },
fieldNames: { type: Object as PropType<FieldNames> },
loadData: { type: Function as PropType<(dataNode: LegacyDataNode) => Promise<unknown>> },
treeNodeFilterProp: String,
treeNodeLabelProp: String,
treeDataSimpleMode: {
type: [Boolean, Object] as PropType<boolean | SimpleModeConfig>,
default: undefined,
},
treeExpandedKeys: { type: [String, Number] as PropType<Key> },
treeDefaultExpandedKeys: { type: [String, Number] as PropType<Key> },
treeLoadedKeys: { type: [String, Number] as PropType<Key> },
treeCheckable: { type: Boolean, default: undefined },
treeCheckStrictly: { type: Boolean, default: undefined },
showCheckedStrategy: { type: String as PropType<CheckedStrategy> },
treeDefaultExpandAll: { type: Boolean, default: undefined },
treeData: { type: Array as PropType<DataNode[]> },
treeLine: { type: Boolean, default: undefined },
treeIcon: PropTypes.any,
showTreeIcon: { type: Boolean, default: undefined },
switcherIcon: PropTypes.any,
treeMotion: PropTypes.any,
children: PropTypes.any,
filterTreeNode: {
type: [Boolean, Function] as PropType<boolean | FilterFunc<LegacyDataNode>>,
default: undefined,
},
dropdownPopupAlign: PropTypes.any,
// Event
onSearch: { type: Function as PropType<(value: string) => void> },
onChange: {
type: Function as PropType<
(value: ValueType, labelList: any[], extra: ChangeEventExtra) => void
>,
},
onTreeExpand: { type: Function as PropType<(expandedKeys: Key[]) => void> },
onTreeLoad: { type: Function as PropType<(loadedKeys: Key[]) => void> },
onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> },
// Legacy
/** `searchPlaceholder` has been removed since search box has been merged into input box */
searchPlaceholder: PropTypes.any,
/** @private This is not standard API since we only used in `rc-cascader`. Do not use in your production */
labelRender: { type: Function as PropType<(entity: FlattenDataNode) => any> },
};
}
class Helper<T> {
ReturnOptionListProps = optionListProps<T>();
ReturnTreeSelectProps = treeSelectProps<T>();
}
export type OptionListProps = Partial<ExtractPropTypes<Helper<DataNode>['ReturnOptionListProps']>>;
export type TreeSelectProps<T = DefaultValueType> = Partial<
ExtractPropTypes<Helper<T>['ReturnTreeSelectProps']>
>; >;

View File

@ -6,7 +6,6 @@
"ant-design-vue/es/*": ["components/*"] "ant-design-vue/es/*": ["components/*"]
}, },
"strictNullChecks": false, "strictNullChecks": false,
"strict": true,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"experimentalDecorators": true, "experimentalDecorators": true,