diff --git a/packages/vant/src/picker/Picker.tsx b/packages/vant/src/picker/Picker.tsx index fbc28fa3d..a0e606114 100644 --- a/packages/vant/src/picker/Picker.tsx +++ b/packages/vant/src/picker/Picker.tsx @@ -9,7 +9,6 @@ import { // Utils import { - isDef, extend, unitToPx, truthProp, @@ -21,6 +20,12 @@ import { HAPTICS_FEEDBACK, BORDER_UNSET_TOP_BOTTOM, } from '../utils'; +import { + getColumnsType, + findOptionByValue, + formatCascadeColumns, + getFirstEnabledOption, +} from './utils'; // Composables import { useChildren } from '@vant/use'; @@ -34,7 +39,6 @@ import Column, { PICKER_KEY } from './PickerColumn'; import type { PickerColumn, PickerExpose, - PickerOption, PickerFieldNames, PickerToolbarPosition, } from './types'; @@ -76,77 +80,41 @@ export default defineComponent({ const selectedValues = ref(props.modelValue); const currentColumns = ref([]); - const { - text: textKey, - value: valueKey, - children: childrenKey, - } = extend( - { - text: 'text', - value: 'value', - children: 'children', - }, - props.columnsFieldNames - ); - const { children, linkChildren } = useChildren(PICKER_KEY); linkChildren(); + const fields = computed( + (): Required => + extend( + { + text: 'text', + value: 'value', + children: 'children', + }, + props.columnsFieldNames + ) + ); const optionHeight = computed(() => unitToPx(props.optionHeight)); - - const dataType = computed(() => { - const firstColumn = props.columns[0]; - if (Array.isArray(firstColumn)) { - return 'multiple'; - } - if (childrenKey in firstColumn) { - return 'cascade'; - } - return 'default'; - }); - - const findOption = (options: PickerOption[], value: number | string) => - options.find((option) => option[valueKey] === value); - - const formatCascade = () => { - const formatted: PickerColumn[] = []; - - let cursor: PickerOption | undefined = { - [childrenKey]: props.columns, - }; - let columnIndex = 0; - - while (cursor && cursor[childrenKey]) { - const options: PickerOption[] = cursor[childrenKey]; - const value = selectedValues.value[columnIndex]; - - cursor = isDef(value) ? findOption(options, value) : undefined; - - if (!cursor && options.length) { - const firstValue = options[0][valueKey]; - selectedValues.value[columnIndex] = firstValue; - cursor = findOption(options, firstValue); - } - - columnIndex++; - formatted.push(options); - } - - return formatted; - }; + const columnsType = computed(() => + getColumnsType(props.columns, fields.value) + ); const selectedOptions = computed(() => currentColumns.value.map((options, index) => - findOption(options, selectedValues.value[index]) + findOptionByValue(options, selectedValues.value[index], fields.value) ) ); const onChange = (value: number | string, columnIndex: number) => { selectedValues.value[columnIndex] = value; - if (dataType.value === 'cascade') { - currentColumns.value = formatCascade(); + if (columnsType.value === 'cascade') { + currentColumns.value = formatCascadeColumns( + props.columns, + fields.value, + selectedValues + ); } emit('change', { @@ -222,10 +190,9 @@ export default defineComponent({ { + switch (columnsType.value) { + case 'multiple': + return columns as PickerColumn[]; + case 'cascade': + return formatCascadeColumns(columns, fields.value, selectedValues); + default: + return [columns]; + } + }; + watch( () => props.columns, - () => { - const { columns } = props; - - switch (dataType.value) { - case 'multiple': - currentColumns.value = columns; - break; - case 'cascade': - currentColumns.value = formatCascade(); - break; - default: - currentColumns.value = [columns]; - break; - } - + (columns) => { + currentColumns.value = formatColumns(columns); currentColumns.value.forEach((options, index) => { if (selectedValues.value[index] === undefined && options.length) { - selectedValues.value[index] = options[0][valueKey]; + selectedValues.value[index] = + getFirstEnabledOption(options)[fields.value.value]; } }); diff --git a/packages/vant/src/picker/PickerColumn.tsx b/packages/vant/src/picker/PickerColumn.tsx index a9285bbd6..ceafb27f0 100644 --- a/packages/vant/src/picker/PickerColumn.tsx +++ b/packages/vant/src/picker/PickerColumn.tsx @@ -1,4 +1,10 @@ -import { ref, watch, defineComponent, type InjectionKey } from 'vue'; +import { + ref, + watchEffect, + defineComponent, + type PropType, + type InjectionKey, +} from 'vue'; // Utils import { @@ -9,6 +15,7 @@ import { createNamespace, makeRequiredProp, } from '../utils'; +import { getElementTranslateY, findIndexOfEnabledOption } from './utils'; // Composables import { useParent } from '@vant/use'; @@ -16,7 +23,11 @@ import { useTouch } from '../composables/use-touch'; import { useExpose } from '../composables/use-expose'; // Types -import type { PickerOption, PickerColumnProvide } from './types'; +import type { + PickerOption, + PickerFieldNames, + PickerColumnProvide, +} from './types'; const DEFAULT_DURATION = 200; @@ -28,12 +39,6 @@ const MOMENTUM_DISTANCE = 15; const [name, bem] = createNamespace('picker-column'); -function getElementTranslateY(element: Element) { - const { transform } = window.getComputedStyle(element); - const translateY = transform.slice(7, transform.length - 1).split(', ')[5]; - return Number(translateY); -} - export const PICKER_KEY: InjectionKey = Symbol(name); export default defineComponent({ @@ -41,10 +46,9 @@ export default defineComponent({ props: { value: numericProp, - textKey: makeRequiredProp(String), + fields: makeRequiredProp(Object as PropType>), options: makeArrayProp(), readonly: Boolean, - valueKey: makeRequiredProp(String), allowHtml: Boolean, optionHeight: makeRequiredProp(Number), swipeDuration: makeRequiredProp(numericProp), @@ -70,24 +74,12 @@ export default defineComponent({ const baseOffset = () => (props.optionHeight * (+props.visibleOptionNum - 1)) / 2; - const adjustIndex = (index: number) => { - index = clamp(index, 0, count()); - - for (let i = index; i < count(); i++) { - if (!props.options[i].disabled) return i; - } - for (let i = index - 1; i >= 0; i--) { - if (!props.options[i].disabled) return i; - } - return 0; - }; - const updateValueByIndex = (index: number) => { - index = adjustIndex(index); + const enabledIndex = findIndexOfEnabledOption(props.options, index); + const offset = -enabledIndex * props.optionHeight; - const offset = -index * props.optionHeight; const trigger = () => { - const { value } = props.options[index]; + const value = props.options[enabledIndex][props.fields.value]; if (value !== props.value) { emit('change', value); } @@ -214,9 +206,9 @@ export default defineComponent({ }; return props.options.map((option, index) => { - const text = option[props.textKey]; + const text = option[props.fields.text]; const { disabled } = option; - const value: string | number = option[props.valueKey]; + const value: string | number = option[props.fields.value]; const data = { role: 'button', style: optionStyle, @@ -244,16 +236,14 @@ export default defineComponent({ useParent(PICKER_KEY); useExpose({ stopMomentum }); - watch( - () => props.value, - (value) => { - const index = props.options.findIndex( - (option) => option[props.valueKey] === value - ); - const offset = -adjustIndex(index) * props.optionHeight; - currentOffset.value = offset; - } - ); + watchEffect(() => { + const index = props.options.findIndex( + (option) => option[props.fields.value] === props.value + ); + const enabledIndex = findIndexOfEnabledOption(props.options, index); + const offset = -enabledIndex * props.optionHeight; + currentOffset.value = offset; + }); return () => (
!option.disabled) || options[0]; +} + +export function getColumnsType( + columns: PickerColumn | PickerColumn[], + fields: Required +) { + const firstColumn = columns[0]; + if (firstColumn) { + if (Array.isArray(firstColumn)) { + return 'multiple'; + } + if (fields.children in firstColumn) { + return 'cascade'; + } + } + return 'default'; +} + +export function findIndexOfEnabledOption( + options: PickerOption[], + index: number +) { + index = clamp(index, 0, options.length); + + for (let i = index; i < options.length; i++) { + if (!options[i].disabled) return i; + } + for (let i = index - 1; i >= 0; i--) { + if (!options[i].disabled) return i; + } + + return 0; +} + +export function findOptionByValue( + options: PickerOption[], + value: number | string, + fields: Required +) { + const index = options.findIndex((option) => option[fields.value] === value); + const enabledIndex = findIndexOfEnabledOption(options, index); + return options[enabledIndex]; +} + +export function formatCascadeColumns( + columns: PickerColumn | PickerColumn[], + fields: Required, + selectedValues: Ref> +) { + const formatted: PickerColumn[] = []; + + let cursor: PickerOption | undefined = { + [fields.children]: columns, + }; + let columnIndex = 0; + + while (cursor && cursor[fields.children]) { + const options: PickerOption[] = cursor[fields.children]; + const value = selectedValues.value[columnIndex]; + + cursor = isDef(value) + ? findOptionByValue(options, value, fields) + : undefined; + + if (!cursor && options.length) { + const firstValue = getFirstEnabledOption(options)[fields.value]; + selectedValues.value[columnIndex] = firstValue; + cursor = findOptionByValue(options, firstValue, fields); + } + + columnIndex++; + formatted.push(options); + } + + return formatted; +} + +export function getElementTranslateY(element: Element) { + const { transform } = window.getComputedStyle(element); + const translateY = transform.slice(7, transform.length - 1).split(', ')[5]; + return Number(translateY); +}