diff --git a/packages/vant/src/picker/Picker.tsx b/packages/vant/src/picker/Picker.tsx index 922db050e..f864c9e7c 100644 --- a/packages/vant/src/picker/Picker.tsx +++ b/packages/vant/src/picker/Picker.tsx @@ -9,6 +9,7 @@ import { // Utils import { + isDef, extend, unitToPx, truthProp, @@ -32,12 +33,11 @@ import Column, { PICKER_KEY } from './PickerColumn'; // Types import type { PickerColumn, - PickerOption, PickerExpose, PickerFieldNames, - PickerObjectColumn, PickerToolbarPosition, } from './types'; +import { PickerOption } from '.'; const [name, bem, t] = createNamespace('picker'); @@ -55,7 +55,8 @@ export const pickerSharedProps = { }; const pickerProps = extend({}, pickerSharedProps, { - columns: makeArrayProp(), + columns: makeArrayProp(), + modelValue: makeArrayProp(), defaultIndex: makeNumericProp(0), toolbarPosition: makeStringProp('top'), columnsFieldNames: Object as PropType, @@ -68,20 +69,21 @@ export default defineComponent({ props: pickerProps, - emits: ['confirm', 'cancel', 'change'], + emits: ['confirm', 'cancel', 'change', 'update:modelValue'], setup(props, { emit, slots }) { const hasOptions = ref(false); - const formattedColumns = ref([]); + const selectedValues = ref(props.modelValue); + const currentColumns = ref([]); const { text: textKey, - values: valuesKey, + value: valueKey, children: childrenKey, } = extend( { text: 'text', - values: 'values', + value: 'value', children: 'children', }, props.columnsFieldNames @@ -95,186 +97,78 @@ export default defineComponent({ const dataType = computed(() => { const firstColumn = props.columns[0]; - if (typeof firstColumn === 'object') { - if (childrenKey in firstColumn) { - return 'cascade'; - } - if (valuesKey in firstColumn) { - return 'object'; - } + if (Array.isArray(firstColumn)) { + return 'multiple'; } - return 'plain'; + 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: PickerObjectColumn[] = []; + const formatted: PickerColumn[] = []; - let cursor: PickerObjectColumn = { + let cursor: PickerOption | undefined = { [childrenKey]: props.columns, }; + let columnIndex = 0; while (cursor && cursor[childrenKey]) { - const children = cursor[childrenKey]; - let defaultIndex = cursor.defaultIndex ?? +props.defaultIndex; + const options: PickerOption[] = cursor[childrenKey]; + const value = selectedValues.value[columnIndex]; - while (children[defaultIndex] && children[defaultIndex].disabled) { - if (defaultIndex < children.length - 1) { - defaultIndex++; - } else { - defaultIndex = 0; - break; - } + cursor = isDef(value) ? findOption(options, value) : undefined; + + if (!cursor && options.length) { + const firstValue = options[0][valueKey]; + selectedValues.value[columnIndex] = firstValue; + cursor = findOption(options, firstValue); } - formatted.push({ - [valuesKey]: cursor[childrenKey], - className: cursor.className, - defaultIndex, - }); - - cursor = children[defaultIndex]; - } - - formattedColumns.value = formatted; - }; - - const format = () => { - const { columns } = props; - - if (dataType.value === 'plain') { - formattedColumns.value = [{ [valuesKey]: columns }]; - } else if (dataType.value === 'cascade') { - formatCascade(); - } else { - formattedColumns.value = columns as PickerObjectColumn[]; - } - - hasOptions.value = formattedColumns.value.some( - (item) => item[valuesKey] && item[valuesKey].length !== 0 - ); - }; - - // get indexes of all columns - const getIndexes = () => children.map((child) => child.state.index); - - // set options of column by index - const setColumnValues = (index: number, options: PickerOption[]) => { - const column = children[index]; - if (column) { - column.setOptions(options); - hasOptions.value = true; - } - }; - - const onCascadeChange = (columnIndex: number) => { - let cursor: PickerObjectColumn = { - [childrenKey]: props.columns, - }; - const indexes = getIndexes(); - - for (let i = 0; i <= columnIndex; i++) { - cursor = cursor[childrenKey][indexes[i]]; - } - - while (cursor && cursor[childrenKey]) { columnIndex++; - setColumnValues(columnIndex, cursor[childrenKey]); - cursor = cursor[childrenKey][cursor.defaultIndex || 0]; + formatted.push(options); } + + return formatted; }; - // get column instance by index - const getChild = (index: number) => children[index]; + const selectedOptions = computed(() => + currentColumns.value.map((options, index) => + findOption(options, selectedValues.value[index]) + ) + ); - // get column value by index - const getColumnValue = (index: number) => { - const column = getChild(index); - if (column) { - return column.getValue(); - } - }; + const onChange = (value: number | string, columnIndex: number) => { + selectedValues.value[columnIndex] = value; - // set column value by index - const setColumnValue = (index: number, value: string) => { - const column = getChild(index); - if (column) { - column.setValue(value); - if (dataType.value === 'cascade') { - onCascadeChange(index); - } - } - }; - - // get column option index by column index - const getColumnIndex = (index: number) => { - const column = getChild(index); - if (column) { - return column.state.index; - } - }; - - // set column option index by column index - const setColumnIndex = (columnIndex: number, optionIndex: number) => { - const column = getChild(columnIndex); - if (column) { - column.setIndex(optionIndex); - if (dataType.value === 'cascade') { - onCascadeChange(columnIndex); - } - } - }; - - // get options of column by index - const getColumnValues = (index: number) => { - const column = getChild(index); - if (column) { - return column.state.options; - } - }; - - // get values of all columns - const getValues = () => children.map((child) => child.getValue()); - - // set values of all columns - const setValues = (values: string[]) => { - values.forEach((value, index) => { - setColumnValue(index, value); - }); - }; - - // set indexes of all columns - const setIndexes = (indexes: number[]) => { - indexes.forEach((optionIndex, columnIndex) => { - setColumnIndex(columnIndex, optionIndex); - }); - }; - - const emitAction = (event: 'confirm' | 'cancel') => { - if (dataType.value === 'plain') { - emit(event, getColumnValue(0), getColumnIndex(0)); - } else { - emit(event, getValues(), getIndexes()); - } - }; - - const onChange = (columnIndex: number) => { if (dataType.value === 'cascade') { - onCascadeChange(columnIndex); + currentColumns.value = formatCascade(); } - if (dataType.value === 'plain') { - emit('change', getColumnValue(0), getColumnIndex(0)); - } else { - emit('change', getValues(), columnIndex); - } + emit('change', { + columnIndex, + selectedValues: selectedValues.value, + selectedOptions: selectedOptions.value, + }); }; const confirm = () => { children.forEach((child) => child.stopMomentum()); - emitAction('confirm'); + emit('confirm', { + selectedValues: selectedValues.value, + selectedOptions: selectedOptions.value, + }); }; - const cancel = () => emitAction('cancel'); + const cancel = () => + emit('cancel', { + selectedValues: selectedValues.value, + selectedOptions: selectedOptions.value, + }); const renderTitle = () => { if (slots.title) { @@ -324,19 +218,19 @@ export default defineComponent({ }; const renderColumnItems = () => - formattedColumns.value.map((item, columnIndex) => ( + currentColumns.value.map((options, columnIndex) => ( onChange(columnIndex)} + onChange={(value: number | string) => onChange(value, columnIndex)} /> )); @@ -371,22 +265,51 @@ export default defineComponent({ ); }; - watch(() => props.columns, format, { immediate: true }); + watch( + () => props.columns, + () => { + const { columns } = props; - useExpose({ - confirm, - getValues, - setValues, - getIndexes, - setIndexes, - getColumnIndex, - setColumnIndex, - getColumnValue, - setColumnValue, - getColumnValues, - setColumnValues, + switch (dataType.value) { + case 'multiple': + currentColumns.value = columns; + break; + case 'cascade': + currentColumns.value = formatCascade(); + break; + default: + currentColumns.value = [columns]; + break; + } + + currentColumns.value.forEach((options, index) => { + if (selectedValues.value[index] === undefined && options.length) { + selectedValues.value[index] = options[0][valueKey]; + } + }); + + hasOptions.value = currentColumns.value.some( + (options) => !!options.length + ); + }, + { immediate: true } + ); + + watch( + () => props.modelValue, + (value) => { + selectedValues.value = value; + } + ); + + watch(selectedValues, () => { + if (selectedValues.value !== props.modelValue) { + emit('update:modelValue', selectedValues.value); + } }); + useExpose({ confirm }); + return () => (
{props.toolbarPosition === 'top' ? renderToolbar() : null} diff --git a/packages/vant/src/picker/PickerColumn.tsx b/packages/vant/src/picker/PickerColumn.tsx index 2768c8dd7..bed35b2f0 100644 --- a/packages/vant/src/picker/PickerColumn.tsx +++ b/packages/vant/src/picker/PickerColumn.tsx @@ -1,14 +1,10 @@ -import { ref, watch, reactive, defineComponent, type InjectionKey } from 'vue'; +import { ref, reactive, defineComponent, type InjectionKey, watch } from 'vue'; // Utils -import { deepClone } from '../utils/deep-clone'; import { clamp, - isObject, - unknownProp, numericProp, makeArrayProp, - makeNumberProp, preventDefault, createNamespace, makeRequiredProp, @@ -40,21 +36,18 @@ function getElementTranslateY(element: Element) { export const PICKER_KEY: InjectionKey = Symbol(name); -const isOptionDisabled = (option: PickerOption) => - isObject(option) && option.disabled; - export default defineComponent({ name, props: { + value: numericProp, textKey: makeRequiredProp(String), + options: makeArrayProp(), readonly: Boolean, + valueKey: makeRequiredProp(String), allowHtml: Boolean, - className: unknownProp, itemHeight: makeRequiredProp(Number), - defaultIndex: makeNumberProp(0), swipeDuration: makeRequiredProp(numericProp), - initialOptions: makeArrayProp(), visibleItemCount: makeRequiredProp(numericProp), }, @@ -70,15 +63,13 @@ export default defineComponent({ const wrapper = ref(); const state = reactive({ - index: props.defaultIndex, offset: 0, duration: 0, - options: deepClone(props.initialOptions), }); const touch = useTouch(); - const count = () => state.options.length; + const count = () => props.options.length; const baseOffset = () => (props.itemHeight * (+props.visibleItemCount - 1)) / 2; @@ -87,24 +78,22 @@ export default defineComponent({ index = clamp(index, 0, count()); for (let i = index; i < count(); i++) { - if (!isOptionDisabled(state.options[i])) return i; + if (!props.options[i].disabled) return i; } for (let i = index - 1; i >= 0; i--) { - if (!isOptionDisabled(state.options[i])) return i; + if (!props.options[i].disabled) return i; } + return 0; }; - const setIndex = (index: number, emitChange?: boolean) => { - index = adjustIndex(index) || 0; + const updateValueByIndex = (index: number) => { + index = adjustIndex(index); const offset = -index * props.itemHeight; const trigger = () => { - if (index !== state.index) { - state.index = index; - - if (emitChange) { - emit('change', index); - } + const { value } = props.options[index]; + if (value !== props.value) { + emit('change', value); } }; @@ -118,13 +107,6 @@ export default defineComponent({ state.offset = offset; }; - const setOptions = (options: PickerOption[]) => { - if (JSON.stringify(options) !== JSON.stringify(state.options)) { - state.options = deepClone(options); - setIndex(props.defaultIndex); - } - }; - const onClickItem = (index: number) => { if (moving || props.readonly) { return; @@ -132,14 +114,7 @@ export default defineComponent({ transitionEndTrigger = null; state.duration = DEFAULT_DURATION; - setIndex(index, true); - }; - - const getOptionText = (option: PickerOption) => { - if (isObject(option) && props.textKey in option) { - return option[props.textKey]; - } - return option; + updateValueByIndex(index); }; const getIndexByOffset = (offset: number) => @@ -153,7 +128,7 @@ export default defineComponent({ const index = getIndexByOffset(distance); state.duration = +props.swipeDuration; - setIndex(index, true); + updateValueByIndex(index); }; const stopMomentum = () => { @@ -230,10 +205,10 @@ export default defineComponent({ const index = getIndexByOffset(state.offset); state.duration = DEFAULT_DURATION; - setIndex(index, true); + updateValueByIndex(index); // compatible with desktop scenario - // use setTimeout to skip the click event Emitted after touchstart + // use setTimeout to skip the click event emitted after touchstart setTimeout(() => { moving = false; }, 0); @@ -244,17 +219,17 @@ export default defineComponent({ height: `${props.itemHeight}px`, }; - return state.options.map((option, index: number) => { - const text = getOptionText(option); - const disabled = isOptionDisabled(option); - + return props.options.map((option, index) => { + const text = option[props.textKey]; + const { disabled } = option; + const value: string | number = option[props.valueKey]; const data = { role: 'button', style: optionStyle, tabindex: disabled ? -1 : 0, class: bem('item', { disabled, - selected: index === state.index, + selected: value === props.value, }), onClick: () => onClickItem(index), }; @@ -272,39 +247,23 @@ export default defineComponent({ }); }; - const setValue = (value: string) => { - const { options } = state; - for (let i = 0; i < options.length; i++) { - if (getOptionText(options[i]) === value) { - return setIndex(i); - } - } - }; - - const getValue = (): PickerOption => state.options[state.index]; - - setIndex(state.index); - useParent(PICKER_KEY); - useExpose({ - state, - setIndex, - getValue, - setValue, - setOptions, - stopMomentum, - }); - - watch(() => props.initialOptions, setOptions); + useExpose({ stopMomentum }); watch( - () => props.defaultIndex, - (value) => setIndex(value) + () => props.value, + (value) => { + const index = props.options.findIndex( + (option) => option[props.valueKey] === value + ); + const offset = -adjustIndex(index) * props.itemHeight; + state.offset = offset; + } ); return () => (
{ - Toast(`Value: ${value}, Index: ${index}`); + const columns = [ + { text: 'Delaware', value: 'Delaware' }, + { text: 'Florida', value: 'Florida' }, + { text: 'Georqia', value: 'Georqia' }, + { text: 'Indiana', value: 'Indiana' }, + { text: 'Maine', value: 'Maine' }, + ]; + const onConfirm = (option, index) => { + Toast(`Value: ${option.value}, Index: ${index}`); }; - const onChange = (value, index) => { - Toast(`Value: ${value}, Index: ${index}`); + const onChange = (option, index) => { + Toast(`Value: ${option.value}, Index: ${index}`); }; const onCancel = () => Toast('Cancel'); @@ -55,12 +60,6 @@ export default { }; ``` -### Default Index - -```html - -``` - ### Multiple Columns ```html @@ -71,14 +70,18 @@ export default { export default { setup() { const columns = [ - { - values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], - defaultIndex: 2, - }, - { - values: ['Morning', 'Afternoon', 'Evening'], - defaultIndex: 1, - }, + [ + { text: 'Monday', value: 'Monday' }, + { text: 'Tuesday', value: 'Tuesday' }, + { text: 'Wednesday', value: 'Wednesday' }, + { text: 'Thursday', value: 'Thursday' }, + { text: 'Friday', value: 'Friday' }, + ], + [ + { text: 'Morning', value: 'Morning' }, + { text: 'Afternoon', value: 'Afternoon' }, + { text: 'Evening', value: 'Evening' }, + ], ]; return { columns }; diff --git a/packages/vant/src/picker/README.zh-CN.md b/packages/vant/src/picker/README.zh-CN.md index dca100c1e..5f59f2277 100644 --- a/packages/vant/src/picker/README.zh-CN.md +++ b/packages/vant/src/picker/README.zh-CN.md @@ -43,13 +43,21 @@ import { Toast } from 'vant'; export default { setup() { - const columns = ['杭州', '宁波', '温州', '绍兴', '湖州', '嘉兴', '金华']; - - const onConfirm = (value, index) => { - Toast(`当前值: ${value}, 当前索引: ${index}`); + const columns = [ + { text: '杭州', value: 'Hangzhou' }, + { text: '宁波', value: 'Ningbo' }, + { text: '温州', value: 'Wenzhou' }, + { text: '绍兴', value: 'Shaoxing' }, + { text: '湖州', value: 'Huzhou' }, + { text: '嘉兴', value: 'Jiaxing' }, + { text: '金华', value: 'Jinhua' }, + { text: '衢州', value: 'Quzhou' }, + ]; + const onConfirm = (option, index) => { + Toast(`当前值: ${option.value}, 当前索引: ${index}`); }; - const onChange = (value, index) => { - Toast(`当前值: ${value}, 当前索引: ${index}`); + const onChange = (option, index) => { + Toast(`当前值: ${option.value}, 当前索引: ${index}`); }; const onCancel = () => Toast('取消'); @@ -63,17 +71,9 @@ export default { }; ``` -### 默认选中项 - -单列选择时,可以通过 `default-index` 属性设置初始选中项的索引。 - -```html - -``` - ### 多列选择 -`columns` 属性可以通过对象数组的形式配置多列选择,对象中可以配置选项数据、初始选中项等,详细格式见[下方表格](#/zh-CN/picker#column-shu-ju-jie-gou)。 +`columns` 属性可以通过二维数组的形式配置多列选择。 ```html @@ -84,15 +84,19 @@ export default { setup() { const columns = [ // 第一列 - { - values: ['周一', '周二', '周三', '周四', '周五'], - defaultIndex: 2, - }, + [ + { text: '周一', value: 'Monday' }, + { text: '周二', value: 'Tuesday' }, + { text: '周三', value: 'Wednesday' }, + { text: '周四', value: 'Thursday' }, + { text: '周五', value: 'Friday' }, + ], // 第二列 - { - values: ['上午', '下午', '晚上'], - defaultIndex: 1, - }, + [ + { text: '上午', value: 'Morning' }, + { text: '下午', value: 'Afternoon' }, + { text: '晚上', value: 'Evening' }, + ], ]; return { columns }; diff --git a/packages/vant/src/picker/demo/data.ts b/packages/vant/src/picker/demo/data.ts index cd9b0b803..1b2a57dc6 100644 --- a/packages/vant/src/picker/demo/data.ts +++ b/packages/vant/src/picker/demo/data.ts @@ -1,23 +1,51 @@ -export const dateColumns = { +export const basicColumns = { 'zh-CN': [ - { - values: ['周一', '周二', '周三', '周四', '周五'], - defaultIndex: 2, - }, - { - values: ['上午', '下午', '晚上'], - defaultIndex: 1, - }, + { text: '杭州', value: 'Hangzhou' }, + { text: '宁波', value: 'Ningbo' }, + { text: '温州', value: 'Wenzhou' }, + { text: '绍兴', value: 'Shaoxing' }, + { text: '湖州', value: 'Huzhou' }, + { text: '嘉兴', value: 'Jiaxing' }, + { text: '金华', value: 'Jinhua' }, + { text: '衢州', value: 'Quzhou' }, ], 'en-US': [ - { - values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], - defaultIndex: 2, - }, - { - values: ['Morning', 'Afternoon', 'Evening'], - defaultIndex: 1, - }, + { text: 'Delaware', value: 'Delaware' }, + { text: 'Florida', value: 'Florida' }, + { text: 'Georqia', value: 'Georqia' }, + { text: 'Indiana', value: 'Indiana' }, + { text: 'Maine', value: 'Maine' }, + ], +}; + +export const dateColumns = { + 'zh-CN': [ + [ + { text: '周一', value: 'Monday' }, + { text: '周二', value: 'Tuesday' }, + { text: '周三', value: 'Wednesday' }, + { text: '周四', value: 'Thursday' }, + { text: '周五', value: 'Friday' }, + ], + [ + { text: '上午', value: 'Morning' }, + { text: '下午', value: 'Afternoon' }, + { text: '晚上', value: 'Evening' }, + ], + ], + 'en-US': [ + [ + { text: 'Monday', value: 'Monday' }, + { text: 'Tuesday', value: 'Tuesday' }, + { text: 'Wednesday', value: 'Wednesday' }, + { text: 'Thursday', value: 'Thursday' }, + { text: 'Friday', value: 'Friday' }, + ], + [ + { text: 'Morning', value: 'Morning' }, + { text: 'Afternoon', value: 'Afternoon' }, + { text: 'Evening', value: 'Evening' }, + ], ], }; @@ -25,27 +53,45 @@ export const cascadeColumns = { 'zh-CN': [ { text: '浙江', + value: 'Zhejiang', children: [ { text: '杭州', - children: [{ text: '西湖区' }, { text: '余杭区' }], + value: 'Hangzhou', + children: [ + { text: '西湖区', value: 'Xihu' }, + { text: '余杭区', value: 'Yuhang' }, + ], }, { text: '温州', - children: [{ text: '鹿城区' }, { text: '瓯海区' }], + value: 'Wenzhou', + children: [ + { text: '鹿城区', value: 'Lucheng' }, + { text: '瓯海区', value: 'Ouhai' }, + ], }, ], }, { text: '福建', + value: 'Fujian', children: [ { text: '福州', - children: [{ text: '鼓楼区' }, { text: '台江区' }], + value: 'Fuzhou', + children: [ + { text: '鼓楼区', value: 'Gulou' }, + { text: '台江区', value: 'Taijiang' }, + ], }, { text: '厦门', - children: [{ text: '思明区' }, { text: '海沧区' }], + value: 'Xiamen', + children: [ + { text: '思明区', value: 'Siming' }, + { text: '海沧区', value: 'Haicang' }, + ], }, ], }, @@ -53,27 +99,45 @@ export const cascadeColumns = { 'en-US': [ { text: 'Zhejiang', + value: 'Zhejiang', children: [ { text: 'Hangzhou', - children: [{ text: 'Xihu' }, { text: 'Yuhang' }], + value: 'Hangzhou', + children: [ + { text: 'Xihu', value: 'Xihu' }, + { text: 'Yuhang', value: 'Yuhang' }, + ], }, { text: 'Wenzhou', - children: [{ text: 'Lucheng' }, { text: 'Ouhai' }], + value: 'Wenzhou', + children: [ + { text: 'Lucheng', value: 'Lucheng' }, + { text: 'Ouhai', value: 'Ouhai' }, + ], }, ], }, { text: 'Fujian', + value: 'Fujian', children: [ { text: 'Fuzhou', - children: [{ text: 'Gulou' }, { text: 'Taijiang' }], + value: 'Fuzhou', + children: [ + { text: 'Gulou', value: 'Gulou' }, + { text: 'Taijiang', value: 'Taijiang' }, + ], }, { text: 'Xiamen', - children: [{ text: 'Siming' }, { text: 'Haicang' }], + value: 'Xiamen', + children: [ + { text: 'Siming', value: 'Siming' }, + { text: 'Haicang', value: 'Haicang' }, + ], }, ], }, diff --git a/packages/vant/src/picker/demo/index.vue b/packages/vant/src/picker/demo/index.vue index b90e41ee1..91a78e7ba 100644 --- a/packages/vant/src/picker/demo/index.vue +++ b/packages/vant/src/picker/demo/index.vue @@ -1,11 +1,17 @@