import { effect, h } from '@ui-arch/frame'; import { button } from './button.js'; import { addonWrap } from './addons.js'; import type { InputNumberProps } from './types.js'; /*** 数字输入:−/+ 按钮 + 输入框三段式,value 信号双向绑定,min/max/step 超界自动夹取,键盘 ↑↓ 调节,长按连发步进 ***/ export function inputNumber(props: InputNumberProps): HTMLElement { const min = props.min ?? -Infinity; // 无下限缺省 const max = props.max ?? Infinity; // 无上限缺省 const step = props.step ?? 1; // 步长缺省 1 const clamp = (v: number): number => Math.min(max, Math.max(min, v)); // 任何来源的值都夹回合法区间 const emit = (v: number): void => { const n = clamp(v); props.value(n); if (props.onChange) props.onChange(n); }; // 写回信号并回调 const input = h('input', { class: 'ua-input ua-input-number-field', inputmode: 'decimal', disabled: props.disabled, 'aria-label': '数字输入' }) as HTMLInputElement; const sync = (): void => { const text = String(props.value()); if (input.value !== text) input.value = text; }; // 外部变化写回输入框 effect(sync); input.addEventListener('change', () => { const n = parseFloat(input.value); if (Number.isNaN(n)) sync(); else emit(n); }); // 非法输入回滚当前值 input.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; // 卫语句:只拦截上下方向键 e.preventDefault(); emit(props.value() + (e.key === 'ArrowUp' ? step : -step)); // ↑ 加步长 / ↓ 减步长 }); const dec = button({ label: '−', variant: 'ghost', disabled: props.disabled, onClick: () => emit(props.value() - step) }); const inc = button({ label: '+', variant: 'ghost', disabled: props.disabled, onClick: () => emit(props.value() + step) }); dec.setAttribute('aria-label', '减少'); inc.setAttribute('aria-label', '增加'); // —— 长按连发:400ms 延迟后每 100ms 连续步进,松开 / 移出即停(单击行为不受影响) —— let holdTimer = 0; // 长按首步延迟计时器 let repeatTimer = 0; // 连发重复计时器 const stopHold = (): void => { window.clearTimeout(holdTimer); window.clearInterval(repeatTimer); holdTimer = 0; repeatTimer = 0; }; // 停止连发 const hold = (dir: number): void => { // dir:-1 减 / 1 加 stopHold(); // 重复按下先清旧计时 holdTimer = window.setTimeout(() => { repeatTimer = window.setInterval(() => emit(props.value() + dir * step), 100); }, 400); }; dec.addEventListener('pointerdown', () => hold(-1)); inc.addEventListener('pointerdown', () => hold(1)); ['pointerup', 'pointerleave', 'pointercancel'].forEach((ev) => { dec.addEventListener(ev, stopHold); inc.addEventListener(ev, stopHold); }); // 任一中止事件都停 return addonWrap(h('div', { class: 'ua-input-number', role: 'group' }, dec, input, inc) as HTMLDivElement, props.prefix, props.suffix); }