Files
vant/packages/vant/src/picker/Picker.tsx
T

294 lines
7.6 KiB
TypeScript

import {
ref,
watch,
computed,
defineComponent,
type PropType,
type ExtractPropTypes,
} from 'vue';
// Utils
import {
extend,
unitToPx,
truthProp,
makeArrayProp,
preventDefault,
makeStringProp,
makeNumericProp,
createNamespace,
HAPTICS_FEEDBACK,
BORDER_UNSET_TOP_BOTTOM,
} from '../utils';
import {
isOptionExist,
isValuesEqual,
getColumnsType,
findOptionByValue,
formatCascadeColumns,
getFirstEnabledOption,
assignDefaultFields,
} 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,
PickerExpose,
PickerOption,
PickerFieldNames,
PickerToolbarPosition,
} from './types';
const [name, bem, t] = createNamespace('picker');
export const pickerSharedProps = {
title: String,
loading: Boolean,
readonly: Boolean,
allowHtml: Boolean,
optionHeight: makeNumericProp(44),
showToolbar: truthProp,
swipeDuration: makeNumericProp(1000),
visibleOptionNum: makeNumericProp(6),
cancelButtonText: String,
confirmButtonText: String,
};
const pickerProps = extend({}, pickerSharedProps, {
columns: makeArrayProp<PickerOption | PickerColumn>(),
modelValue: makeArrayProp<number | string>(),
toolbarPosition: makeStringProp<PickerToolbarPosition>('top'),
columnsFieldNames: Object as PropType<PickerFieldNames>,
});
export type PickerProps = ExtractPropTypes<typeof pickerProps>;
export default defineComponent({
name,
props: pickerProps,
emits: ['confirm', 'cancel', 'change', 'update:modelValue'],
setup(props, { emit, slots }) {
const selectedValues = ref(props.modelValue);
const { children, linkChildren } = useChildren(PICKER_KEY);
linkChildren();
const fields = computed(() => assignDefaultFields(props.columnsFieldNames));
const optionHeight = computed(() => unitToPx(props.optionHeight));
const columnsType = computed(() =>
getColumnsType(props.columns, fields.value)
);
const currentColumns = computed<PickerColumn[]>(() => {
const { columns } = props;
switch (columnsType.value) {
case 'multiple':
return columns as PickerColumn[];
case 'cascade':
return formatCascadeColumns(columns, fields.value, selectedValues);
default:
return [columns];
}
});
const hasOptions = computed(() =>
currentColumns.value.some((options) => options.length)
);
const selectedOptions = computed(() =>
currentColumns.value.map((options, index) =>
findOptionByValue(options, selectedValues.value[index], fields.value)
)
);
const onChange = (value: number | string, columnIndex: number) => {
selectedValues.value[columnIndex] = value;
if (columnsType.value === 'cascade') {
// reset values after cascading
selectedValues.value.forEach((value, index) => {
const options = currentColumns.value[index];
if (!isOptionExist(options, value, fields.value)) {
selectedValues.value[index] = options.length
? options[0][fields.value.value]
: undefined;
}
});
}
emit('change', {
columnIndex,
selectedValues: selectedValues.value,
selectedOptions: selectedOptions.value,
});
};
const confirm = () => {
children.forEach((child) => child.stopMomentum());
emit('confirm', {
selectedValues: selectedValues.value,
selectedOptions: selectedOptions.value,
});
};
const cancel = () =>
emit('cancel', {
selectedValues: selectedValues.value,
selectedOptions: selectedOptions.value,
});
const renderTitle = () => {
if (slots.title) {
return slots.title();
}
if (props.title) {
return <div class={[bem('title'), 'van-ellipsis']}>{props.title}</div>;
}
};
const renderCancel = () => {
const text = props.cancelButtonText || t('cancel');
return (
<button
type="button"
class={[bem('cancel'), HAPTICS_FEEDBACK]}
onClick={cancel}
>
{slots.cancel ? slots.cancel() : text}
</button>
);
};
const renderConfirm = () => {
const text = props.confirmButtonText || t('confirm');
return (
<button
type="button"
class={[bem('confirm'), HAPTICS_FEEDBACK]}
onClick={confirm}
>
{slots.confirm ? slots.confirm() : text}
</button>
);
};
const renderToolbar = () => {
if (props.showToolbar) {
return (
<div class={bem('toolbar')}>
{slots.toolbar
? slots.toolbar()
: [renderCancel(), renderTitle(), renderConfirm()]}
</div>
);
}
};
const renderColumnItems = () =>
currentColumns.value.map((options, columnIndex) => (
<Column
v-slots={{ option: slots.option }}
value={selectedValues.value[columnIndex]}
fields={fields.value}
options={options}
readonly={props.readonly}
allowHtml={props.allowHtml}
optionHeight={optionHeight.value}
swipeDuration={props.swipeDuration}
visibleOptionNum={props.visibleOptionNum}
onChange={(value: number | string) => onChange(value, columnIndex)}
/>
));
const renderMask = (wrapHeight: number) => {
if (hasOptions.value) {
const frameStyle = { height: `${optionHeight.value}px` };
const maskStyle = {
backgroundSize: `100% ${(wrapHeight - optionHeight.value) / 2}px`,
};
return [
<div class={bem('mask')} style={maskStyle} />,
<div
class={[BORDER_UNSET_TOP_BOTTOM, bem('frame')]}
style={frameStyle}
/>,
];
}
};
const renderColumns = () => {
const wrapHeight = optionHeight.value * +props.visibleOptionNum;
const columnsStyle = { height: `${wrapHeight}px` };
return (
<div
class={bem('columns')}
style={columnsStyle}
onTouchmove={preventDefault}
>
{renderColumnItems()}
{renderMask(wrapHeight)}
</div>
);
};
watch(
currentColumns,
(columns) => {
columns.forEach((options, index) => {
if (
options.length &&
!isOptionExist(options, selectedValues.value[index], fields.value)
) {
selectedValues.value[index] =
getFirstEnabledOption(options)[fields.value.value];
}
});
},
{ immediate: true }
);
watch(
() => props.modelValue,
(newValues) => {
if (!isValuesEqual(newValues, selectedValues.value)) {
selectedValues.value = newValues;
}
},
{ deep: true }
);
watch(
selectedValues,
(newValues) => {
if (!isValuesEqual(newValues, props.modelValue)) {
emit('update:modelValue', selectedValues.value);
}
},
{ deep: true }
);
useExpose<PickerExpose>({ confirm });
return () => (
<div class={bem()}>
{props.toolbarPosition === 'top' ? renderToolbar() : null}
{props.loading ? <Loading class={bem('loading')} /> : null}
{slots['columns-top']?.()}
{renderColumns()}
{slots['columns-bottom']?.()}
{props.toolbarPosition === 'bottom' ? renderToolbar() : null}
</div>
);
},
});