Files
UI/packages/skin/ui/src/inputNumber.ts
T
2026-10-26 11:08:35 +08:00

39 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}