import { ref, watch, computed, PropType, defineComponent, ExtractPropTypes, } from 'vue'; // Utils import { extend, unitToPx, truthProp, preventDefault, createNamespace, BORDER_UNSET_TOP_BOTTOM, } from '../utils'; // Composables import { useChildren } from '@vant/use'; import { useExpose } from '../composables/use-expose'; // Components import { Loading } from '../loading'; import Column, { PICKER_KEY } from './PickerColumn'; // Types import type { PickerColumn, PickerOption, PickerExpose, PickerFieldNames, PickerObjectColumn, PickerToolbarPosition, } from './types'; const [name, bem, t] = createNamespace('picker'); export const pickerProps = { title: String, loading: Boolean, readonly: Boolean, allowHtml: Boolean, showToolbar: truthProp, cancelButtonText: String, confirmButtonText: String, itemHeight: { type: [Number, String], default: 44, }, visibleItemCount: { type: [Number, String], default: 6, }, swipeDuration: { type: [Number, String], default: 1000, }, }; export type PickerProps = ExtractPropTypes; export default defineComponent({ name, props: extend({}, pickerProps, { // @deprecated // should be removed in next major version valueKey: String, columnsFieldNames: Object as PropType, columns: { type: Array as PropType, default: () => [], }, defaultIndex: { type: [Number, String], default: 0, }, toolbarPosition: { type: String as PropType, default: 'top', }, }), emits: ['confirm', 'cancel', 'change'], setup(props, { emit, slots }) { if (process.env.NODE_ENV !== 'production') { if (slots.default) { console.warn( '[Vant] Picker: "default" slot is deprecated, please use "toolbar" slot instead.' ); } if (props.valueKey) { console.warn( '[Vant] Picker: "valueKey" prop is deprecated, please use "columnsFieldNames" prop instead.' ); } } const formattedColumns = ref([]); const { text: textKey, values: valuesKey, children: childrenKey } = extend( { // compatible with valueKey prop text: props.valueKey || 'text', values: 'values', children: 'children', }, props.columnsFieldNames ); const { children, linkChildren } = useChildren(PICKER_KEY); linkChildren(); const itemHeight = computed(() => unitToPx(props.itemHeight)); const dataType = computed(() => { const firstColumn = props.columns[0]; if (typeof firstColumn === 'object') { if (childrenKey in firstColumn) { return 'cascade'; } if (valuesKey in firstColumn) { return 'object'; } } return 'plain'; }); const formatCascade = () => { const formatted: PickerObjectColumn[] = []; let cursor: PickerObjectColumn = { [childrenKey]: props.columns, }; while (cursor && cursor[childrenKey]) { const children = cursor[childrenKey]; let defaultIndex = cursor.defaultIndex ?? +props.defaultIndex; while (children[defaultIndex] && children[defaultIndex].disabled) { if (defaultIndex < children.length - 1) { defaultIndex++; } else { defaultIndex = 0; break; } } 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[]; } }; // 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); } }; 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]; } }; // get column instance by index const getChild = (index: number) => children[index]; // get column value by index const getColumnValue = (index: number) => { const column = getChild(index); if (column) { return column.getValue(); } }; // 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); } if (dataType.value === 'plain') { emit('change', getColumnValue(0), getColumnIndex(0)); } else { emit('change', getValues(), columnIndex); } }; const confirm = () => { children.forEach((child) => child.stopMomentum()); emitAction('confirm'); }; const cancel = () => emitAction('cancel'); const renderTitle = () => { if (slots.title) { return slots.title(); } if (props.title) { return
{props.title}
; } }; const renderCancel = () => { const text = props.cancelButtonText || t('cancel'); return ( ); }; const renderConfirm = () => { const text = props.confirmButtonText || t('confirm'); return ( ); }; const renderToolbar = () => { if (props.showToolbar) { // default slot is deprecated // should be removed in next major version const slot = slots.toolbar || slots.default; return (
{slot ? slot() : [renderCancel(), renderTitle(), renderConfirm()]}
); } }; const renderColumnItems = () => formattedColumns.value.map((item, columnIndex) => ( onChange(columnIndex)} /> )); const renderColumns = () => { const wrapHeight = itemHeight.value * +props.visibleItemCount; const frameStyle = { height: `${itemHeight.value}px` }; const columnsStyle = { height: `${wrapHeight}px` }; const maskStyle = { backgroundSize: `100% ${(wrapHeight - itemHeight.value) / 2}px`, }; return (
{renderColumnItems()}
); }; watch(() => props.columns, format, { immediate: true }); useExpose({ confirm, getValues, setValues, getIndexes, setIndexes, getColumnIndex, setColumnIndex, getColumnValue, setColumnValue, getColumnValues, setColumnValues, }); return () => (
{props.toolbarPosition === 'top' ? renderToolbar() : null} {props.loading ? : null} {slots['columns-top']?.()} {renderColumns()} {slots['columns-bottom']?.()} {props.toolbarPosition === 'bottom' ? renderToolbar() : null}
); }, });