vant/packages/vant/src/number-keyboard/NumberKeyboard.tsx

306 lines
7.0 KiB
TypeScript

import {
ref,
watch,
computed,
Teleport,
Transition,
defineComponent,
type Slot,
type PropType,
type TeleportProps,
type ExtractPropTypes,
} from 'vue';
// Utils
import {
truthProp,
numericProp,
getZIndexStyle,
makeStringProp,
makeNumericProp,
stopPropagation,
createNamespace,
HAPTICS_FEEDBACK,
type Numeric,
} from '../utils';
// Composables
import { useClickAway } from '@vant/use';
// Components
import NumberKeyboardKey, { KeyType } from './NumberKeyboardKey';
const [name, bem] = createNamespace('number-keyboard');
export type NumberKeyboardTheme = 'default' | 'custom';
type KeyConfig = {
text?: Numeric;
type?: KeyType;
color?: string;
wider?: boolean;
};
const numberKeyboardProps = {
show: Boolean,
title: String,
theme: makeStringProp<NumberKeyboardTheme>('default'),
zIndex: numericProp,
teleport: [String, Object] as PropType<TeleportProps['to']>,
maxlength: makeNumericProp(Infinity),
modelValue: makeStringProp(''),
transition: truthProp,
blurOnClose: truthProp,
showDeleteKey: truthProp,
randomKeyOrder: Boolean,
closeButtonText: String,
deleteButtonText: String,
closeButtonLoading: Boolean,
hideOnClickOutside: truthProp,
safeAreaInsetBottom: truthProp,
extraKey: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
};
export type NumberKeyboardProps = ExtractPropTypes<typeof numberKeyboardProps>;
function shuffle(array: unknown[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
export default defineComponent({
name,
inheritAttrs: false,
props: numberKeyboardProps,
emits: [
'show',
'hide',
'blur',
'input',
'close',
'delete',
'update:modelValue',
],
setup(props, { emit, slots, attrs }) {
const root = ref<HTMLElement>();
const genBasicKeys = () => {
const keys: KeyConfig[] = Array(9)
.fill('')
.map((_, i) => ({ text: i + 1 }));
if (props.randomKeyOrder) {
shuffle(keys);
}
return keys;
};
const genDefaultKeys = (): KeyConfig[] => [
...genBasicKeys(),
{ text: props.extraKey as string, type: 'extra' },
{ text: 0 },
{
text: props.showDeleteKey ? props.deleteButtonText : '',
type: props.showDeleteKey ? 'delete' : '',
},
];
const genCustomKeys = () => {
const keys = genBasicKeys();
const { extraKey } = props;
const extraKeys = Array.isArray(extraKey) ? extraKey : [extraKey];
if (extraKeys.length === 1) {
keys.push(
{ text: 0, wider: true },
{ text: extraKeys[0], type: 'extra' }
);
} else if (extraKeys.length === 2) {
keys.push(
{ text: extraKeys[0], type: 'extra' },
{ text: 0 },
{ text: extraKeys[1], type: 'extra' }
);
}
return keys;
};
const keys = computed(() =>
props.theme === 'custom' ? genCustomKeys() : genDefaultKeys()
);
const onBlur = () => {
if (props.show) {
emit('blur');
}
};
const onClose = () => {
emit('close');
if (props.blurOnClose) {
onBlur();
}
};
const onAnimationEnd = () => emit(props.show ? 'show' : 'hide');
const onPress = (text: string, type: KeyType) => {
if (text === '') {
if (type === 'extra') {
onBlur();
}
return;
}
const value = props.modelValue;
if (type === 'delete') {
emit('delete');
emit('update:modelValue', value.slice(0, value.length - 1));
} else if (type === 'close') {
onClose();
} else if (value.length < props.maxlength) {
emit('input', text);
emit('update:modelValue', value + text);
}
};
const renderTitle = () => {
const { title, theme, closeButtonText } = props;
const leftSlot = slots['title-left'];
const showClose = closeButtonText && theme === 'default';
const showTitle = title || showClose || leftSlot;
if (!showTitle) {
return;
}
return (
<div class={bem('header')}>
{leftSlot && <span class={bem('title-left')}>{leftSlot()}</span>}
{title && <h2 class={bem('title')}>{title}</h2>}
{showClose && (
<button
type="button"
class={[bem('close'), HAPTICS_FEEDBACK]}
onClick={onClose}
>
{closeButtonText}
</button>
)}
</div>
);
};
const renderKeys = () =>
keys.value.map((key) => {
const keySlots: Record<string, Slot | undefined> = {};
if (key.type === 'delete') {
keySlots.default = slots.delete;
}
if (key.type === 'extra') {
keySlots.default = slots['extra-key'];
}
return (
<NumberKeyboardKey
v-slots={keySlots}
key={key.text}
text={key.text}
type={key.type}
wider={key.wider}
color={key.color}
onPress={onPress}
/>
);
});
const renderSidebar = () => {
if (props.theme === 'custom') {
return (
<div class={bem('sidebar')}>
{props.showDeleteKey && (
<NumberKeyboardKey
v-slots={{ delete: slots.delete }}
large
text={props.deleteButtonText}
type="delete"
onPress={onPress}
/>
)}
<NumberKeyboardKey
large
text={props.closeButtonText}
type="close"
color="blue"
loading={props.closeButtonLoading}
onPress={onPress}
/>
</div>
);
}
};
watch(
() => props.show,
(value) => {
if (!props.transition) {
emit(value ? 'show' : 'hide');
}
}
);
if (props.hideOnClickOutside) {
useClickAway(root, onBlur, { eventName: 'touchstart' });
}
return () => {
const Title = renderTitle();
const Content = (
<Transition name={props.transition ? 'van-slide-up' : ''}>
<div
v-show={props.show}
ref={root}
style={getZIndexStyle(props.zIndex)}
class={bem({
unfit: !props.safeAreaInsetBottom,
'with-title': !!Title,
})}
onAnimationend={onAnimationEnd}
onTouchstartPassive={stopPropagation}
{...attrs}
>
{Title}
<div class={bem('body')}>
<div class={bem('keys')}>{renderKeys()}</div>
{renderSidebar()}
</div>
</div>
</Transition>
);
if (props.teleport) {
return <Teleport to={props.teleport}>{Content}</Teleport>;
}
return Content;
};
},
});