40 lines
3.3 KiB
TypeScript
40 lines
3.3 KiB
TypeScript
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 超界自动夹取,precision 小数位归一,键盘 ↑↓ 调节,长按连发步进 ***/
|
||
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 quantize = (n: number): number => (props.precision === undefined ? n : Number(n.toFixed(props.precision))); // precision 归一(顺带消除步进浮点尾差)
|
||
const emit = (v: number): void => { const n = quantize(clamp(v)); props.value(n); if (props.onChange) props.onChange(n); }; // 写回信号并回调
|
||
const input = h('input', { class: `ua-input ua-input-number-field${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, inputmode: 'decimal', disabled: props.disabled, 'aria-label': '数字输入' }) as HTMLInputElement;
|
||
const sync = (): void => { const text = props.precision === undefined ? String(props.value()) : props.value().toFixed(props.precision); if (input.value !== text) input.value = text; }; // 外部变化写回输入框(precision 定长显示)
|
||
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);
|
||
}
|