63 lines
3.5 KiB
TypeScript
63 lines
3.5 KiB
TypeScript
import { effect, h } from '@ui-arch/frame';
|
|
import type { PinInputProps } from './types.js';
|
|
|
|
/*** 验证码输入:N 格单字符数字输入,输入自动进格、退格回删、方向键移格、粘贴自动分发,value 信号双向绑定 ***/
|
|
export function pinInput(props: PinInputProps): HTMLDivElement {
|
|
const length = props.length ?? 6; // 格数缺省 6
|
|
const sanitize = (s: string): string => String(s).replace(/\D/g, '').slice(0, length); // 只留数字并截到格数
|
|
const inputs: HTMLInputElement[] = []; // 格子引用(格子建一次不重渲染,焦点天然不丢)
|
|
const focusAt = (i: number): void => inputs[Math.max(0, Math.min(length - 1, i))]?.focus(); // 目标格钳制到首尾
|
|
|
|
/*** 写回单一来源:拼接各格内容赋回外部信号(同值短路防 effect 环),并回调当前拼接串 ***/
|
|
const commit = (): void => {
|
|
const next = inputs.map((el) => el.value).join('');
|
|
if (props.value() !== next) props.value(next);
|
|
if (props.onChange) props.onChange(next);
|
|
};
|
|
|
|
/*** 单格构造:输入进格 / 退格回删 / 方向键移格 / 粘贴分发,非数字键一律拦下 ***/
|
|
const cell = (i: number): HTMLInputElement => h('input', {
|
|
class: 'ua-input ua-pin-cell',
|
|
type: props.mask ? 'password' : 'text',
|
|
inputmode: 'numeric',
|
|
autocomplete: props.mask ? 'one-time-code' : 'off',
|
|
maxlength: 1,
|
|
'aria-label': `第 ${i + 1} 位`,
|
|
onFocus: () => inputs[i].select(), // 聚焦全选:直接敲键即覆盖旧值
|
|
onInput: () => {
|
|
inputs[i].value = sanitize(inputs[i].value).slice(-1); // 单格只留最后敲入的一位
|
|
if (inputs[i].value && i < length - 1) focusAt(i + 1); // 敲完进下一格(末格不进)
|
|
commit();
|
|
},
|
|
onKeydown: (e: KeyboardEvent) => {
|
|
if (e.key === 'Backspace') { e.preventDefault(); if (inputs[i].value) { inputs[i].value = ''; } else if (i > 0) { inputs[i - 1].value = ''; focusAt(i - 1); } commit(); return; } // 本格有值删本格,空格回删上一格并左移
|
|
if (e.key === 'ArrowLeft') { e.preventDefault(); focusAt(i - 1); return; }
|
|
if (e.key === 'ArrowRight') { e.preventDefault(); focusAt(i + 1); return; }
|
|
if (e.key.length === 1 && !/\d/.test(e.key) && !e.ctrlKey && !e.metaKey) e.preventDefault(); // 只放行数字与控制键(粘贴由 onPaste 接管)
|
|
},
|
|
onPaste: (e: ClipboardEvent) => {
|
|
const digits = sanitize(e.clipboardData?.getData('text') ?? '');
|
|
if (!digits) return;
|
|
e.preventDefault();
|
|
const from = digits.length >= length ? 0 : i; // 整段验证码从首格铺起,零散片段从当前格续
|
|
for (let k = 0; k < digits.length && from + k < length; k++) inputs[from + k].value = digits[k]; // 逐格分发
|
|
focusAt(Math.min(length - 1, from + digits.length)); // 落到末个空格,接着手输也顺手
|
|
commit();
|
|
},
|
|
}) as HTMLInputElement;
|
|
|
|
const root = h('div', { class: 'ua-pin-input', role: 'group' }, ...Array.from({ length }, (_, i) => cell(i))) as HTMLDivElement;
|
|
root.querySelectorAll('input').forEach((el) => inputs.push(el as HTMLInputElement));
|
|
|
|
effect(() => { // 外部信号 → 格子回写(同值不写避免打断输入法),禁用态支持布尔或信号
|
|
const v = sanitize(props.value());
|
|
const dis = typeof props.disabled === 'function' ? props.disabled() : !!props.disabled;
|
|
inputs.forEach((el, k) => {
|
|
const ch = v[k] ?? '';
|
|
if (el.value !== ch) el.value = ch;
|
|
el.disabled = dis;
|
|
});
|
|
});
|
|
return root;
|
|
}
|