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,
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}

View File

@ -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,

View File

@ -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 && (

View File

@ -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}

View File

@ -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;
}

View File

@ -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;
}

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,
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);
}
});