From 80f9b9e8ac5933830b66d5c78a7aeeab919e8e72 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 3 Nov 2021 16:22:02 +0800 Subject: [PATCH] feat: select support fieldNames --- components/vc-select/OptionList.tsx | 53 +++++++++++-------- components/vc-select/SelectTrigger.tsx | 8 ++- .../vc-select/Selector/MultipleSelector.tsx | 5 ++ components/vc-select/generate.tsx | 20 +++++-- components/vc-select/hooks/useCacheOptions.ts | 10 ++-- components/vc-select/interface/index.ts | 9 ++++ components/vc-select/utils/platformUtil.ts | 4 ++ components/vc-select/utils/valueUtil.ts | 38 ++++++++++--- 8 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 components/vc-select/utils/platformUtil.ts diff --git a/components/vc-select/OptionList.tsx b/components/vc-select/OptionList.tsx index fba95effc..28a2c54cc 100644 --- a/components/vc-select/OptionList.tsx +++ b/components/vc-select/OptionList.tsx @@ -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 { prefixCls: string; id: string; options: OptionType[]; + fieldNames?: FieldNames; flattenOptions: FlattenOptionsType; height: number; itemHeight: number; @@ -40,6 +45,7 @@ export interface OptionListProps { 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, { // 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, { 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, { 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, { 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, { 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, { ); } - 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, { return (
{ // 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({ popupElement, dropdownClassName, dropdownStyle, + direction = 'ltr', + placement, dropdownMatchSelectWidth, containerWidth, dropdownRender, animation, transitionName, - direction, getPopupContainer, getTriggerDOMNode, } = props as SelectTriggerProps; @@ -120,7 +123,7 @@ const SelectTrigger = defineComponent({ {...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, diff --git a/components/vc-select/Selector/MultipleSelector.tsx b/components/vc-select/Selector/MultipleSelector.tsx index 950378854..54e32cb47 100644 --- a/components/vc-select/Selector/MultipleSelector.tsx +++ b/components/vc-select/Selector/MultipleSelector.tsx @@ -121,6 +121,11 @@ const SelectSelector = defineComponent({ class={classNames(`${selectionPrefixCls.value}-item`, { [`${selectionPrefixCls.value}-item-disabled`]: itemDisabled, })} + title={ + typeof content === 'string' || typeof content === 'number' + ? content.toString() + : undefined + } > {content} {closable && ( diff --git a/components/vc-select/generate.tsx b/components/vc-select/generate.tsx index 42fa768bd..baaa426ec 100644 --- a/components/vc-select/generate.tsx +++ b/components/vc-select/generate.tsx @@ -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() { return { prefixCls: String, @@ -87,6 +89,7 @@ export function selectBaseProps() { }, labelInValue: { type: Boolean, default: undefined }, + fieldNames: { type: Object as PropType }, // Search inputValue: String, searchValue: String, @@ -127,13 +130,16 @@ export function selectBaseProps() { type: [Boolean, Number] as PropType, default: undefined, }, + placement: { + type: String as PropType, + }, 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 }, - direction: String, + direction: { type: String as PropType<'ltr' | 'rtl'> }, // Others disabled: { type: Boolean, default: undefined }, @@ -237,7 +243,7 @@ export interface GenerateConfig { | (( values: RawValueType[], options: FlattenOptionsType, - info?: { prevValueOptions?: OptionType[][] }, + info?: { prevValueOptions?: OptionType[][]; props?: any }, ) => OptionType[]); /** Check if a value is disabled */ isValueDisabled: (value: RawValueType, options: FlattenOptionsType) => 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} diff --git a/components/vc-select/hooks/useCacheOptions.ts b/components/vc-select/hooks/useCacheOptions.ts index d96a2e578..1daf8908a 100644 --- a/components/vc-select/hooks/useCacheOptions.ts +++ b/components/vc-select/hooks/useCacheOptions.ts @@ -12,17 +12,15 @@ export default function useCacheOptions< >(options: Ref) { const optionMap = computed(() => { const map: Map[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; } diff --git a/components/vc-select/interface/index.ts b/components/vc-select/interface/index.ts index 8c70ef44b..80e6ec569 100644 --- a/components/vc-select/interface/index.ts +++ b/components/vc-select/interface/index.ts @@ -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; } diff --git a/components/vc-select/utils/platformUtil.ts b/components/vc-select/utils/platformUtil.ts new file mode 100644 index 000000000..f6bdcc68b --- /dev/null +++ b/components/vc-select/utils/platformUtil.ts @@ -0,0 +1,4 @@ +/* istanbul ignore file */ +export function isPlatformMac(): boolean { + return /(mac\sos|macintosh)/i.test(navigator.appVersion); +} diff --git a/components/vc-select/utils/valueUtil.ts b/components/vc-select/utils/valueUtil.ts index 38fe6c950..0fcf436b7 100644 --- a/components/vc-select/utils/valueUtil.ts +++ b/components/vc-select/utils/valueUtil.ts @@ -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 = 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); } });