feat: select support fieldNames

This commit is contained in:
tangjinzhou 2021-11-03 16:22:02 +08:00
parent 3ce5046dd5
commit 80f9b9e8ac
8 changed files with 107 additions and 40 deletions

View File

@ -13,9 +13,12 @@ import type {
OptionData, OptionData,
RenderNode, RenderNode,
OnActiveValue, OnActiveValue,
FieldNames,
} from './interface'; } from './interface';
import type { RawValueType, FlattenOptionsType } from './interface/generator'; import type { RawValueType, FlattenOptionsType } from './interface/generator';
import { fillFieldNames } from './utils/valueUtil';
import useMemo from '../_util/hooks/useMemo'; import useMemo from '../_util/hooks/useMemo';
import { isPlatformMac } from './utils/platformUtil';
export interface RefOptionListProps { export interface RefOptionListProps {
onKeydown: (e?: KeyboardEvent) => void; onKeydown: (e?: KeyboardEvent) => void;
@ -24,10 +27,12 @@ export interface RefOptionListProps {
} }
import type { EventHandler } from '../_util/EventInterface'; import type { EventHandler } from '../_util/EventInterface';
import omit from '../_util/omit';
export interface OptionListProps<OptionType extends object> { export interface OptionListProps<OptionType extends object> {
prefixCls: string; prefixCls: string;
id: string; id: string;
options: OptionType[]; options: OptionType[];
fieldNames?: FieldNames;
flattenOptions: FlattenOptionsType<OptionType>; flattenOptions: FlattenOptionsType<OptionType>;
height: number; height: number;
itemHeight: number; itemHeight: number;
@ -40,6 +45,7 @@ export interface OptionListProps<OptionType extends object> {
childrenAsData: boolean; childrenAsData: boolean;
searchValue: string; searchValue: string;
virtual: boolean; virtual: boolean;
direction?: 'ltr' | 'rtl';
onSelect: (value: RawValueType, option: { selected: boolean }) => void; onSelect: (value: RawValueType, option: { selected: boolean }) => void;
onToggleOpen: (open?: boolean) => void; onToggleOpen: (open?: boolean) => void;
@ -55,6 +61,7 @@ const OptionListProps = {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
id: PropTypes.string, id: PropTypes.string,
options: PropTypes.array, options: PropTypes.array,
fieldNames: PropTypes.object,
flattenOptions: PropTypes.array, flattenOptions: PropTypes.array,
height: PropTypes.number, height: PropTypes.number,
itemHeight: PropTypes.number, itemHeight: PropTypes.number,
@ -67,6 +74,7 @@ const OptionListProps = {
childrenAsData: PropTypes.looseBool, childrenAsData: PropTypes.looseBool,
searchValue: PropTypes.string, searchValue: PropTypes.string,
virtual: PropTypes.looseBool, virtual: PropTypes.looseBool,
direction: PropTypes.string,
onSelect: PropTypes.func, onSelect: PropTypes.func,
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> }, 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 // Auto scroll to item position in single mode
watch( watch(
() => props.open, [() => props.open, () => props.searchValue],
() => { () => {
if (!props.multiple && props.open && props.values.size === 1) { if (!props.multiple && props.open && props.values.size === 1) {
const value = Array.from(props.values)[0]; const value = Array.from(props.values)[0];
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value); const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
setActive(index); if (index !== -1) {
nextTick(() => { setActive(index);
scrollIntoView(index); nextTick(() => {
}); scrollIntoView(index);
});
}
} }
// Force trigger scrollbar visible when open // Force trigger scrollbar visible when open
if (props.open) { if (props.open) {
@ -216,9 +226,11 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
setActive, setActive,
onSelectValue, onSelectValue,
onKeydown: (event: KeyboardEvent) => { onKeydown: (event: KeyboardEvent) => {
const { which } = event; const { which, ctrlKey } = event;
switch (which) { switch (which) {
// >>> Arrow keys // >>> Arrow keys & ctrl + n/p on Mac
case KeyCode.N:
case KeyCode.P:
case KeyCode.UP: case KeyCode.UP:
case KeyCode.DOWN: { case KeyCode.DOWN: {
let offset = 0; let offset = 0;
@ -226,6 +238,12 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
offset = -1; offset = -1;
} else if (which === KeyCode.DOWN) { } else if (which === KeyCode.DOWN) {
offset = 1; offset = 1;
} else if (isPlatformMac() && ctrlKey) {
if (which === KeyCode.N) {
offset = 1;
} else if (which === KeyCode.P) {
offset = -1;
}
} }
if (offset !== 0) { if (offset !== 0) {
@ -290,11 +308,13 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
menuItemSelectedIcon, menuItemSelectedIcon,
notFoundContent, notFoundContent,
virtual, virtual,
fieldNames,
onScroll, onScroll,
onMouseenter, onMouseenter,
} = this.$props; } = this.$props;
const renderOption = $slots.option; const renderOption = $slots.option;
const { activeIndex } = this.state; const { activeIndex } = this.state;
const omitFieldNameList = Object.values(fillFieldNames(fieldNames));
// ========================== Render ========================== // ========================== Render ==========================
if (memoFlattenOptions.length === 0) { if (memoFlattenOptions.length === 0) {
return ( return (
@ -326,8 +346,8 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
onScroll={onScroll} onScroll={onScroll}
virtual={virtual} virtual={virtual}
onMouseenter={onMouseenter} onMouseenter={onMouseenter}
children={({ group, groupOption, data }, itemIndex) => { children={({ group, groupOption, data, label, value }, itemIndex) => {
const { label, key } = data; const { key } = data;
// Group // Group
if (group) { if (group) {
return ( return (
@ -337,17 +357,8 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
); );
} }
const { const { disabled, title, children, style, class: cls, className, ...otherProps } = data;
disabled, const passedProps = omit(otherProps, omitFieldNameList);
value,
title,
children,
style,
class: cls,
className,
...otherProps
} = data;
// Option // Option
const selected = values.has(value); const selected = values.has(value);
@ -376,7 +387,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
return ( return (
<div <div
{...otherProps} {...passedProps}
aria-selected={selected} aria-selected={selected}
class={optionClassName} class={optionClassName}
title={optionTitle} title={optionTitle}

View File

@ -7,6 +7,7 @@ import type { CSSProperties, VNodeChild } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { RenderDOMFunc } from './interface'; import type { RenderDOMFunc } from './interface';
import type { DropdownRender } from './interface/generator'; import type { DropdownRender } from './interface/generator';
import type { Placement } from './generate';
const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => { const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => {
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided // Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
@ -55,6 +56,7 @@ export interface SelectTriggerProps {
animation?: string; animation?: string;
transitionName?: string; transitionName?: string;
containerWidth: number; containerWidth: number;
placement?: Placement;
dropdownStyle: CSSProperties; dropdownStyle: CSSProperties;
dropdownClassName: string; dropdownClassName: string;
direction: string; direction: string;
@ -88,12 +90,13 @@ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
popupElement, popupElement,
dropdownClassName, dropdownClassName,
dropdownStyle, dropdownStyle,
direction = 'ltr',
placement,
dropdownMatchSelectWidth, dropdownMatchSelectWidth,
containerWidth, containerWidth,
dropdownRender, dropdownRender,
animation, animation,
transitionName, transitionName,
direction,
getPopupContainer, getPopupContainer,
getTriggerDOMNode, getTriggerDOMNode,
} = props as SelectTriggerProps; } = props as SelectTriggerProps;
@ -120,7 +123,7 @@ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
{...props} {...props}
showAction={[]} showAction={[]}
hideAction={[]} hideAction={[]}
popupPlacement={direction === 'rtl' ? 'bottomRight' : 'bottomLeft'} popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
builtinPlacements={builtInPlacements} builtinPlacements={builtInPlacements}
prefixCls={dropdownPrefixCls} prefixCls={dropdownPrefixCls}
popupTransitionName={mergedTransitionName} popupTransitionName={mergedTransitionName}
@ -146,6 +149,7 @@ SelectTrigger.props = {
disabled: PropTypes.looseBool, disabled: PropTypes.looseBool,
dropdownClassName: PropTypes.string, dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object, dropdownStyle: PropTypes.object,
placement: PropTypes.string,
empty: PropTypes.looseBool, empty: PropTypes.looseBool,
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
popupClassName: PropTypes.string, popupClassName: PropTypes.string,

View File

@ -121,6 +121,11 @@ const SelectSelector = defineComponent<SelectorProps>({
class={classNames(`${selectionPrefixCls.value}-item`, { class={classNames(`${selectionPrefixCls.value}-item`, {
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled, [`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
})} })}
title={
typeof content === 'string' || typeof content === 'number'
? content.toString()
: undefined
}
> >
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span> <span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
{closable && ( {closable && (

View File

@ -11,7 +11,7 @@ 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 { Mode, RenderDOMFunc, OnActiveValue } from './interface'; import type { Mode, RenderDOMFunc, OnActiveValue, FieldNames } from './interface';
import type { import type {
GetLabeledValue, GetLabeledValue,
FilterOptions, FilterOptions,
@ -67,6 +67,8 @@ const DEFAULT_OMIT_PROPS = [
'tabindex', 'tabindex',
]; ];
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight';
export function selectBaseProps<OptionType, ValueType>() { export function selectBaseProps<OptionType, ValueType>() {
return { return {
prefixCls: String, prefixCls: String,
@ -87,6 +89,7 @@ export function selectBaseProps<OptionType, ValueType>() {
}, },
labelInValue: { type: Boolean, default: undefined }, labelInValue: { type: Boolean, default: undefined },
fieldNames: { type: Object as PropType<FieldNames> },
// Search // Search
inputValue: String, inputValue: String,
searchValue: String, searchValue: String,
@ -127,13 +130,16 @@ export function selectBaseProps<OptionType, ValueType>() {
type: [Boolean, Number] as PropType<boolean | number>, type: [Boolean, Number] as PropType<boolean | number>,
default: undefined, default: undefined,
}, },
placement: {
type: String as PropType<Placement>,
},
virtual: { type: Boolean, default: undefined }, virtual: { type: Boolean, default: undefined },
dropdownRender: { type: Function as PropType<(menu: VNode) => any> }, dropdownRender: { type: Function as PropType<(menu: VNode) => any> },
dropdownAlign: PropTypes.any, dropdownAlign: PropTypes.any,
animation: String, animation: String,
transitionName: String, transitionName: String,
getPopupContainer: { type: Function as PropType<RenderDOMFunc> }, getPopupContainer: { type: Function as PropType<RenderDOMFunc> },
direction: String, direction: { type: String as PropType<'ltr' | 'rtl'> },
// Others // Others
disabled: { type: Boolean, default: undefined }, disabled: { type: Boolean, default: undefined },
@ -237,7 +243,7 @@ export interface GenerateConfig<OptionType extends object> {
| (( | ((
values: RawValueType[], values: RawValueType[],
options: FlattenOptionsType<OptionType>, options: FlattenOptionsType<OptionType>,
info?: { prevValueOptions?: OptionType[][] }, info?: { prevValueOptions?: OptionType[][]; props?: any },
) => OptionType[]); ) => OptionType[]);
/** Check if a value is disabled */ /** Check if a value is disabled */
isValueDisabled: (value: RawValueType, options: FlattenOptionsType<OptionType>) => boolean; isValueDisabled: (value: RawValueType, options: FlattenOptionsType<OptionType>) => boolean;
@ -487,7 +493,7 @@ export default function generateSelector<
const triggerSelect = (newValue: RawValueType, isSelect: boolean, source: SelectSource) => { const triggerSelect = (newValue: RawValueType, isSelect: boolean, source: SelectSource) => {
const newValueOption = getValueOption([newValue]); const newValueOption = getValueOption([newValue]);
const outOption = findValueOption([newValue], newValueOption)[0]; const outOption = findValueOption([newValue], newValueOption, { props })[0];
const { internalProps = {} } = props; const { internalProps = {} } = props;
if (!internalProps.skipTriggerSelect) { if (!internalProps.skipTriggerSelect) {
// Skip trigger `onSelect` or `onDeselect` if configured // Skip trigger `onSelect` or `onDeselect` if configured
@ -549,6 +555,7 @@ export default function generateSelector<
) { ) {
const outOptions = findValueOption(newRawValues, newRawValuesOptions, { const outOptions = findValueOption(newRawValues, newRawValuesOptions, {
prevValueOptions: prevValueOptions.value, prevValueOptions: prevValueOptions.value,
props,
}); });
// We will cache option in case it removed by ajax // We will cache option in case it removed by ajax
@ -1008,6 +1015,7 @@ export default function generateSelector<
backfill, backfill,
getInputElement, getInputElement,
getPopupContainer, getPopupContainer,
placement,
// Dropdown // Dropdown
listHeight = 200, listHeight = 200,
@ -1022,6 +1030,7 @@ export default function generateSelector<
dropdownAlign, dropdownAlign,
showAction, showAction,
direction, direction,
fieldNames,
// Tags // Tags
tokenSeparators, tokenSeparators,
@ -1062,6 +1071,7 @@ export default function generateSelector<
open={mergedOpen.value} open={mergedOpen.value}
childrenAsData={!options} childrenAsData={!options}
options={displayOptions.value} options={displayOptions.value}
fieldNames={fieldNames}
flattenOptions={displayFlattenOptions.value} flattenOptions={displayFlattenOptions.value}
multiple={isMultiple.value} multiple={isMultiple.value}
values={rawValues.value} values={rawValues.value}
@ -1077,6 +1087,7 @@ export default function generateSelector<
menuItemSelectedIcon={menuItemSelectedIcon} menuItemSelectedIcon={menuItemSelectedIcon}
virtual={virtual !== false && dropdownMatchSelectWidth !== false} virtual={virtual !== false && dropdownMatchSelectWidth !== false}
onMouseenter={onPopupMouseEnter} onMouseenter={onPopupMouseEnter}
direction={direction}
v-slots={slots} v-slots={slots}
/> />
); );
@ -1193,6 +1204,7 @@ export default function generateSelector<
dropdownMatchSelectWidth={dropdownMatchSelectWidth} dropdownMatchSelectWidth={dropdownMatchSelectWidth}
dropdownRender={dropdownRender as any} dropdownRender={dropdownRender as any}
dropdownAlign={dropdownAlign} dropdownAlign={dropdownAlign}
placement={placement}
getPopupContainer={getPopupContainer} getPopupContainer={getPopupContainer}
empty={!mergedOptions.value.length} empty={!mergedOptions.value.length}
getTriggerDOMNode={() => selectorDomRef.current} getTriggerDOMNode={() => selectorDomRef.current}

View File

@ -12,17 +12,15 @@ export default function useCacheOptions<
>(options: Ref) { >(options: Ref) {
const optionMap = computed(() => { const optionMap = computed(() => {
const map: Map<RawValueType, FlattenOptionsType<OptionType>[number]> = new Map(); const map: Map<RawValueType, FlattenOptionsType<OptionType>[number]> = new Map();
options.value.forEach((item: any) => { options.value.forEach(item => {
const { const { value } = item;
data: { value },
} = item;
map.set(value, item); map.set(value, item);
}); });
return map; return map;
}); });
const getValueOption = (vals: RawValueType[]) => const getValueOption = (valueList: RawValueType[]) =>
vals.map(value => optionMap.value.get(value)).filter(Boolean); valueList.map(value => optionMap.value.get(value)).filter(Boolean);
return getValueOption; return getValueOption;
} }

View File

@ -8,6 +8,13 @@ export type RenderNode = VNodeChild | ((props: any) => VNodeChild);
export type Mode = 'multiple' | 'tags' | 'combobox'; export type Mode = 'multiple' | 'tags' | 'combobox';
// ======================== Option ======================== // ======================== Option ========================
export interface FieldNames {
value?: string;
label?: string;
options?: string;
}
export type OnActiveValue = ( export type OnActiveValue = (
active: RawValueType, active: RawValueType,
index: number, index: number,
@ -49,4 +56,6 @@ export interface FlattenOptionData {
groupOption?: boolean; groupOption?: boolean;
key: string | number; key: string | number;
data: OptionData | OptionGroupData; data: OptionData | OptionGroupData;
label?: any;
value?: RawValueType;
} }

View File

@ -0,0 +1,4 @@
/* istanbul ignore file */
export function isPlatformMac(): boolean {
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
}

View File

@ -6,6 +6,7 @@ import type {
OptionData, OptionData,
OptionGroupData, OptionGroupData,
FlattenOptionData, FlattenOptionData,
FieldNames,
} from '../interface'; } from '../interface';
import type { import type {
LabelValueType, LabelValueType,
@ -34,22 +35,45 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
return `rc-index-key-${index}`; 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. * Flat options into flatten list.
* We use `optionOnly` here is aim to avoid user use nested option group. * We use `optionOnly` here is aim to avoid user use nested option group.
* Here is simply set `key` to the index if not provided. * Here is simply set `key` to the index if not provided.
*/ */
export function flattenOptions(options: SelectOptionsType): FlattenOptionData[] { export function flattenOptions(
options: SelectOptionsType,
{ fieldNames }: { fieldNames?: FieldNames } = {},
): FlattenOptionData[] {
const flattenList: FlattenOptionData[] = []; const flattenList: FlattenOptionData[] = [];
const {
label: fieldLabel,
value: fieldValue,
options: fieldOptions,
} = fillFieldNames(fieldNames);
function dig(list: SelectOptionsType, isGroupOption: boolean) { function dig(list: SelectOptionsType, isGroupOption: boolean) {
list.forEach(data => { list.forEach(data => {
if (isGroupOption || !('options' in data)) { const label = data[fieldLabel];
if (isGroupOption || !(fieldOptions in data)) {
// Option // Option
flattenList.push({ flattenList.push({
key: getKey(data, flattenList.length), key: getKey(data, flattenList.length),
groupOption: isGroupOption, groupOption: isGroupOption,
data, data,
label,
value: data[fieldValue],
}); });
} else { } else {
// Option Group // Option Group
@ -57,9 +81,10 @@ export function flattenOptions(options: SelectOptionsType): FlattenOptionData[]
key: getKey(data, flattenList.length), key: getKey(data, flattenList.length),
group: true, group: true,
data, data,
label,
}); });
dig(data.options, true); dig(data[fieldOptions], true);
} }
}); });
} }
@ -96,11 +121,10 @@ export function findValueOption(
): OptionData[] { ): OptionData[] {
const optionMap: Map<RawValueType, OptionData> = new Map(); const optionMap: Map<RawValueType, OptionData> = new Map();
options.forEach(flattenItem => { options.forEach(({ data, group, value }) => {
if (!flattenItem.group) { if (!group) {
const data = flattenItem.data as OptionData;
// Check if match // Check if match
optionMap.set(data.value, data); optionMap.set(value, data as OptionData);
} }
}); });