mirror of
https://gitee.com/ant-design-vue/ant-design-vue.git
synced 2024-11-30 02:57:50 +08:00
feat: select support fieldNames
This commit is contained in:
parent
3ce5046dd5
commit
80f9b9e8ac
@ -13,9 +13,12 @@ import type {
|
||||
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';
|
||||
|
||||
export interface RefOptionListProps {
|
||||
onKeydown: (e?: KeyboardEvent) => void;
|
||||
@ -24,10 +27,12 @@ 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;
|
||||
@ -40,6 +45,7 @@ export interface OptionListProps<OptionType extends object> {
|
||||
childrenAsData: boolean;
|
||||
searchValue: string;
|
||||
virtual: boolean;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
|
||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||||
onToggleOpen: (open?: boolean) => void;
|
||||
@ -55,6 +61,7 @@ const OptionListProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
fieldNames: PropTypes.object,
|
||||
flattenOptions: PropTypes.array,
|
||||
height: PropTypes.number,
|
||||
itemHeight: PropTypes.number,
|
||||
@ -67,6 +74,7 @@ const OptionListProps = {
|
||||
childrenAsData: PropTypes.looseBool,
|
||||
searchValue: PropTypes.string,
|
||||
virtual: PropTypes.looseBool,
|
||||
direction: PropTypes.string,
|
||||
|
||||
onSelect: PropTypes.func,
|
||||
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
||||
@ -153,15 +161,17 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||
// Auto scroll to item position in single mode
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
[() => props.open, () => props.searchValue],
|
||||
() => {
|
||||
if (!props.multiple && props.open && props.values.size === 1) {
|
||||
const value = Array.from(props.values)[0];
|
||||
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
|
||||
setActive(index);
|
||||
nextTick(() => {
|
||||
scrollIntoView(index);
|
||||
});
|
||||
if (index !== -1) {
|
||||
setActive(index);
|
||||
nextTick(() => {
|
||||
scrollIntoView(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Force trigger scrollbar visible when open
|
||||
if (props.open) {
|
||||
@ -216,9 +226,11 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||
setActive,
|
||||
onSelectValue,
|
||||
onKeydown: (event: KeyboardEvent) => {
|
||||
const { which } = event;
|
||||
const { which, ctrlKey } = event;
|
||||
switch (which) {
|
||||
// >>> Arrow keys
|
||||
// >>> Arrow keys & ctrl + n/p on Mac
|
||||
case KeyCode.N:
|
||||
case KeyCode.P:
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN: {
|
||||
let offset = 0;
|
||||
@ -226,6 +238,12 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||
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) {
|
||||
@ -290,11 +308,13 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||
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) {
|
||||
return (
|
||||
@ -326,8 +346,8 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||
onScroll={onScroll}
|
||||
virtual={virtual}
|
||||
onMouseenter={onMouseenter}
|
||||
children={({ group, groupOption, data }, itemIndex) => {
|
||||
const { label, key } = data;
|
||||
children={({ group, groupOption, data, label, value }, itemIndex) => {
|
||||
const { key } = data;
|
||||
// Group
|
||||
if (group) {
|
||||
return (
|
||||
@ -337,17 +357,8 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
disabled,
|
||||
value,
|
||||
title,
|
||||
children,
|
||||
style,
|
||||
class: cls,
|
||||
className,
|
||||
...otherProps
|
||||
} = data;
|
||||
|
||||
const { disabled, title, children, style, class: cls, className, ...otherProps } = data;
|
||||
const passedProps = omit(otherProps, omitFieldNameList);
|
||||
// Option
|
||||
const selected = values.has(value);
|
||||
|
||||
@ -376,7 +387,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
|
||||
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
{...passedProps}
|
||||
aria-selected={selected}
|
||||
class={optionClassName}
|
||||
title={optionTitle}
|
||||
|
@ -7,6 +7,7 @@ import type { CSSProperties, VNodeChild } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import type { RenderDOMFunc } from './interface';
|
||||
import type { DropdownRender } from './interface/generator';
|
||||
import type { Placement } from './generate';
|
||||
|
||||
const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => {
|
||||
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
|
||||
@ -55,6 +56,7 @@ export interface SelectTriggerProps {
|
||||
animation?: string;
|
||||
transitionName?: string;
|
||||
containerWidth: number;
|
||||
placement?: Placement;
|
||||
dropdownStyle: CSSProperties;
|
||||
dropdownClassName: string;
|
||||
direction: string;
|
||||
@ -88,12 +90,13 @@ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
||||
popupElement,
|
||||
dropdownClassName,
|
||||
dropdownStyle,
|
||||
direction = 'ltr',
|
||||
placement,
|
||||
dropdownMatchSelectWidth,
|
||||
containerWidth,
|
||||
dropdownRender,
|
||||
animation,
|
||||
transitionName,
|
||||
direction,
|
||||
getPopupContainer,
|
||||
getTriggerDOMNode,
|
||||
} = props as SelectTriggerProps;
|
||||
@ -120,7 +123,7 @@ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
||||
{...props}
|
||||
showAction={[]}
|
||||
hideAction={[]}
|
||||
popupPlacement={direction === 'rtl' ? 'bottomRight' : 'bottomLeft'}
|
||||
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
||||
builtinPlacements={builtInPlacements}
|
||||
prefixCls={dropdownPrefixCls}
|
||||
popupTransitionName={mergedTransitionName}
|
||||
@ -146,6 +149,7 @@ SelectTrigger.props = {
|
||||
disabled: PropTypes.looseBool,
|
||||
dropdownClassName: PropTypes.string,
|
||||
dropdownStyle: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
empty: PropTypes.looseBool,
|
||||
prefixCls: PropTypes.string,
|
||||
popupClassName: PropTypes.string,
|
||||
|
@ -121,6 +121,11 @@ const SelectSelector = defineComponent<SelectorProps>({
|
||||
class={classNames(`${selectionPrefixCls.value}-item`, {
|
||||
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
||||
})}
|
||||
title={
|
||||
typeof content === 'string' || typeof content === 'number'
|
||||
? content.toString()
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
||||
{closable && (
|
||||
|
@ -11,7 +11,7 @@ import KeyCode from '../_util/KeyCode';
|
||||
import classNames from '../_util/classNames';
|
||||
import Selector from './Selector';
|
||||
import SelectTrigger from './SelectTrigger';
|
||||
import type { Mode, RenderDOMFunc, OnActiveValue } from './interface';
|
||||
import type { Mode, RenderDOMFunc, OnActiveValue, FieldNames } from './interface';
|
||||
import type {
|
||||
GetLabeledValue,
|
||||
FilterOptions,
|
||||
@ -67,6 +67,8 @@ const DEFAULT_OMIT_PROPS = [
|
||||
'tabindex',
|
||||
];
|
||||
|
||||
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight';
|
||||
|
||||
export function selectBaseProps<OptionType, ValueType>() {
|
||||
return {
|
||||
prefixCls: String,
|
||||
@ -87,6 +89,7 @@ export function selectBaseProps<OptionType, ValueType>() {
|
||||
},
|
||||
labelInValue: { type: Boolean, default: undefined },
|
||||
|
||||
fieldNames: { type: Object as PropType<FieldNames> },
|
||||
// Search
|
||||
inputValue: String,
|
||||
searchValue: String,
|
||||
@ -127,13 +130,16 @@ export function selectBaseProps<OptionType, ValueType>() {
|
||||
type: [Boolean, Number] as PropType<boolean | number>,
|
||||
default: undefined,
|
||||
},
|
||||
placement: {
|
||||
type: String as PropType<Placement>,
|
||||
},
|
||||
virtual: { type: Boolean, default: undefined },
|
||||
dropdownRender: { type: Function as PropType<(menu: VNode) => any> },
|
||||
dropdownAlign: PropTypes.any,
|
||||
animation: String,
|
||||
transitionName: String,
|
||||
getPopupContainer: { type: Function as PropType<RenderDOMFunc> },
|
||||
direction: String,
|
||||
direction: { type: String as PropType<'ltr' | 'rtl'> },
|
||||
|
||||
// Others
|
||||
disabled: { type: Boolean, default: undefined },
|
||||
@ -237,7 +243,7 @@ export interface GenerateConfig<OptionType extends object> {
|
||||
| ((
|
||||
values: RawValueType[],
|
||||
options: FlattenOptionsType<OptionType>,
|
||||
info?: { prevValueOptions?: OptionType[][] },
|
||||
info?: { prevValueOptions?: OptionType[][]; props?: any },
|
||||
) => OptionType[]);
|
||||
/** Check if a value is disabled */
|
||||
isValueDisabled: (value: RawValueType, options: FlattenOptionsType<OptionType>) => boolean;
|
||||
@ -487,7 +493,7 @@ export default function generateSelector<
|
||||
|
||||
const triggerSelect = (newValue: RawValueType, isSelect: boolean, source: SelectSource) => {
|
||||
const newValueOption = getValueOption([newValue]);
|
||||
const outOption = findValueOption([newValue], newValueOption)[0];
|
||||
const outOption = findValueOption([newValue], newValueOption, { props })[0];
|
||||
const { internalProps = {} } = props;
|
||||
if (!internalProps.skipTriggerSelect) {
|
||||
// Skip trigger `onSelect` or `onDeselect` if configured
|
||||
@ -549,6 +555,7 @@ export default function generateSelector<
|
||||
) {
|
||||
const outOptions = findValueOption(newRawValues, newRawValuesOptions, {
|
||||
prevValueOptions: prevValueOptions.value,
|
||||
props,
|
||||
});
|
||||
|
||||
// We will cache option in case it removed by ajax
|
||||
@ -1008,6 +1015,7 @@ export default function generateSelector<
|
||||
backfill,
|
||||
getInputElement,
|
||||
getPopupContainer,
|
||||
placement,
|
||||
|
||||
// Dropdown
|
||||
listHeight = 200,
|
||||
@ -1022,6 +1030,7 @@ export default function generateSelector<
|
||||
dropdownAlign,
|
||||
showAction,
|
||||
direction,
|
||||
fieldNames,
|
||||
|
||||
// Tags
|
||||
tokenSeparators,
|
||||
@ -1062,6 +1071,7 @@ export default function generateSelector<
|
||||
open={mergedOpen.value}
|
||||
childrenAsData={!options}
|
||||
options={displayOptions.value}
|
||||
fieldNames={fieldNames}
|
||||
flattenOptions={displayFlattenOptions.value}
|
||||
multiple={isMultiple.value}
|
||||
values={rawValues.value}
|
||||
@ -1077,6 +1087,7 @@ export default function generateSelector<
|
||||
menuItemSelectedIcon={menuItemSelectedIcon}
|
||||
virtual={virtual !== false && dropdownMatchSelectWidth !== false}
|
||||
onMouseenter={onPopupMouseEnter}
|
||||
direction={direction}
|
||||
v-slots={slots}
|
||||
/>
|
||||
);
|
||||
@ -1193,6 +1204,7 @@ export default function generateSelector<
|
||||
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
|
||||
dropdownRender={dropdownRender as any}
|
||||
dropdownAlign={dropdownAlign}
|
||||
placement={placement}
|
||||
getPopupContainer={getPopupContainer}
|
||||
empty={!mergedOptions.value.length}
|
||||
getTriggerDOMNode={() => selectorDomRef.current}
|
||||
|
@ -12,17 +12,15 @@ export default function useCacheOptions<
|
||||
>(options: Ref) {
|
||||
const optionMap = computed(() => {
|
||||
const map: Map<RawValueType, FlattenOptionsType<OptionType>[number]> = new Map();
|
||||
options.value.forEach((item: any) => {
|
||||
const {
|
||||
data: { value },
|
||||
} = item;
|
||||
options.value.forEach(item => {
|
||||
const { value } = item;
|
||||
map.set(value, item);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const getValueOption = (vals: RawValueType[]) =>
|
||||
vals.map(value => optionMap.value.get(value)).filter(Boolean);
|
||||
const getValueOption = (valueList: RawValueType[]) =>
|
||||
valueList.map(value => optionMap.value.get(value)).filter(Boolean);
|
||||
|
||||
return getValueOption;
|
||||
}
|
||||
|
@ -8,6 +8,13 @@ export type RenderNode = VNodeChild | ((props: any) => VNodeChild);
|
||||
export type Mode = 'multiple' | 'tags' | 'combobox';
|
||||
|
||||
// ======================== Option ========================
|
||||
|
||||
export interface FieldNames {
|
||||
value?: string;
|
||||
label?: string;
|
||||
options?: string;
|
||||
}
|
||||
|
||||
export type OnActiveValue = (
|
||||
active: RawValueType,
|
||||
index: number,
|
||||
@ -49,4 +56,6 @@ export interface FlattenOptionData {
|
||||
groupOption?: boolean;
|
||||
key: string | number;
|
||||
data: OptionData | OptionGroupData;
|
||||
label?: any;
|
||||
value?: RawValueType;
|
||||
}
|
||||
|
4
components/vc-select/utils/platformUtil.ts
Normal file
4
components/vc-select/utils/platformUtil.ts
Normal file
@ -0,0 +1,4 @@
|
||||
/* istanbul ignore file */
|
||||
export function isPlatformMac(): boolean {
|
||||
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
|
||||
}
|
@ -6,6 +6,7 @@ import type {
|
||||
OptionData,
|
||||
OptionGroupData,
|
||||
FlattenOptionData,
|
||||
FieldNames,
|
||||
} from '../interface';
|
||||
import type {
|
||||
LabelValueType,
|
||||
@ -34,22 +35,45 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
|
||||
return `rc-index-key-${index}`;
|
||||
}
|
||||
|
||||
export function fillFieldNames(fieldNames?: FieldNames) {
|
||||
const { label, value, options } = fieldNames || {};
|
||||
|
||||
return {
|
||||
label: label || '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(options: SelectOptionsType): FlattenOptionData[] {
|
||||
export function flattenOptions(
|
||||
options: SelectOptionsType,
|
||||
{ fieldNames }: { fieldNames?: FieldNames } = {},
|
||||
): FlattenOptionData[] {
|
||||
const flattenList: FlattenOptionData[] = [];
|
||||
|
||||
const {
|
||||
label: fieldLabel,
|
||||
value: fieldValue,
|
||||
options: fieldOptions,
|
||||
} = fillFieldNames(fieldNames);
|
||||
|
||||
function dig(list: SelectOptionsType, isGroupOption: boolean) {
|
||||
list.forEach(data => {
|
||||
if (isGroupOption || !('options' in data)) {
|
||||
const label = data[fieldLabel];
|
||||
|
||||
if (isGroupOption || !(fieldOptions in data)) {
|
||||
// Option
|
||||
flattenList.push({
|
||||
key: getKey(data, flattenList.length),
|
||||
groupOption: isGroupOption,
|
||||
data,
|
||||
label,
|
||||
value: data[fieldValue],
|
||||
});
|
||||
} else {
|
||||
// Option Group
|
||||
@ -57,9 +81,10 @@ export function flattenOptions(options: SelectOptionsType): FlattenOptionData[]
|
||||
key: getKey(data, flattenList.length),
|
||||
group: true,
|
||||
data,
|
||||
label,
|
||||
});
|
||||
|
||||
dig(data.options, true);
|
||||
dig(data[fieldOptions], true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -96,11 +121,10 @@ export function findValueOption(
|
||||
): OptionData[] {
|
||||
const optionMap: Map<RawValueType, OptionData> = new Map();
|
||||
|
||||
options.forEach(flattenItem => {
|
||||
if (!flattenItem.group) {
|
||||
const data = flattenItem.data as OptionData;
|
||||
options.forEach(({ data, group, value }) => {
|
||||
if (!group) {
|
||||
// Check if match
|
||||
optionMap.set(data.value, data);
|
||||
optionMap.set(value, data as OptionData);
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user