Files
vant/packages/vant/src/picker/Picker.tsx
T
neverlandandGitHub 1afe960f30 feat: add new PickerGroup component (#11005)
* feat: add PickerGroup component

* chore: remove log

* chore: en doc

* chore: add snapshot

* docs: update
2022-09-04 09:39:37 +08:00

288 lines
7.7 KiB
TypeScript

import {
ref,
watch,
computed,
defineComponent,
type PropType,
type ExtractPropTypes,
} from 'vue';
// Utils
import {
pick,
extend,
unitToPx,
truthProp,
isSameValue,
makeArrayProp,
preventDefault,
makeStringProp,
makeNumericProp,
BORDER_UNSET_TOP_BOTTOM,
type Numeric,
} from '../utils';
import {
bem,
name,
isOptionExist,
getColumnsType,
findOptionByValue,
assignDefaultFields,
formatCascadeColumns,
getFirstEnabledOption,
} from './utils';
// Composables
import { useChildren, useEventListener, useParent } from '@vant/use';
import { useExpose } from '../composables/use-expose';
// Components
import { Loading } from '../loading';
import Column, { PICKER_KEY } from './PickerColumn';
import Toolbar, {
pickerToolbarPropKeys,
pickerToolbarProps,
pickerToolbarSlots,
} from './PickerToolbar';
// Types
import type {
PickerColumn,
PickerExpose,
PickerOption,
PickerFieldNames,
PickerToolbarPosition,
} from './types';
import { PICKER_GROUP_KEY } from '../picker-group/PickerGroup';
export const pickerSharedProps = extend(
{
loading: Boolean,
readonly: Boolean,
allowHtml: Boolean,
optionHeight: makeNumericProp(44),
showToolbar: truthProp,
swipeDuration: makeNumericProp(1000),
visibleOptionNum: makeNumericProp(6),
},
pickerToolbarProps
);
const pickerProps = extend({}, pickerSharedProps, {
columns: makeArrayProp<PickerOption | PickerColumn>(),
modelValue: makeArrayProp<Numeric>(),
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', 'clickOption', 'update:modelValue'],
setup(props, { emit, slots }) {
const columnsRef = ref<HTMLElement>();
const selectedValues = ref(props.modelValue.slice(0));
const { parent } = useParent(PICKER_GROUP_KEY);
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 setValue = (index: number, value: Numeric) => {
if (selectedValues.value[index] !== value) {
const newValues = selectedValues.value.slice(0);
newValues[index] = value;
selectedValues.value = newValues;
}
};
const getEventParams = () => ({
selectedValues: selectedValues.value.slice(0),
selectedOptions: selectedOptions.value,
});
const onChange = (value: Numeric, columnIndex: number) => {
setValue(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)) {
setValue(
index,
options.length ? options[0][fields.value.value] : undefined
);
}
});
}
emit('change', extend({ columnIndex }, getEventParams()));
};
const onClickOption = (currentOption: PickerOption, columnIndex: number) =>
emit(
'clickOption',
extend({ columnIndex, currentOption }, getEventParams())
);
const confirm = () => {
children.forEach((child) => child.stopMomentum());
const params = getEventParams();
emit('confirm', params);
return params;
};
const cancel = () => emit('cancel', getEventParams());
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: Numeric) => onChange(value, columnIndex)}
onClickOption={(option: PickerOption) =>
onClickOption(option, 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 ref={columnsRef} class={bem('columns')} style={columnsStyle}>
{renderColumnItems()}
{renderMask(wrapHeight)}
</div>
);
};
const renderToolbar = () => {
if (props.showToolbar && !parent) {
return (
<Toolbar
v-slots={pick(slots, pickerToolbarSlots)}
{...pick(props, pickerToolbarPropKeys)}
onConfirm={confirm}
onCancel={cancel}
/>
);
}
};
watch(
currentColumns,
(columns) => {
columns.forEach((options, index) => {
if (
options.length &&
!isOptionExist(options, selectedValues.value[index], fields.value)
) {
setValue(
index,
getFirstEnabledOption(options)![fields.value.value]
);
}
});
},
{ immediate: true }
);
watch(
() => props.modelValue,
(newValues) => {
if (!isSameValue(newValues, selectedValues.value)) {
selectedValues.value = newValues.slice(0);
}
},
{ deep: true }
);
watch(
selectedValues,
(newValues) => {
if (!isSameValue(newValues, props.modelValue)) {
emit('update:modelValue', newValues.slice(0));
}
},
{ immediate: true }
);
// useEventListener will set passive to `false` to eliminate the warning of Chrome
useEventListener('touchmove', preventDefault, {
target: columnsRef,
});
const getSelectedOptions = () => selectedOptions.value;
useExpose<PickerExpose>({ confirm, getSelectedOptions });
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>
);
},
});