use rdom_core::{ListenerOptions, NodeId};
use crate::tui_event::TuiDispatchExt;
use crate::{TuiDom, TuiEvent};
pub fn install(dom: &mut TuiDom) {
let root = dom.root();
dom.add_event_listener(
root,
"beforeinput",
ListenerOptions::default(),
move |ctx| {
if ctx.event.default_prevented() {
return;
}
let Some(target) = ctx.event.target else {
return;
};
if !is_number_input(ctx.dom, target) {
return;
}
let Some(input) = ctx.event.detail.as_input() else {
return;
};
let Some(data) = input.data.as_deref() else {
return; };
if data.is_empty() {
return;
}
if !data.chars().all(is_numeric_char) {
ctx.event.prevent_default();
}
},
)
.expect("number beforeinput filter install");
dom.add_event_listener(root, "keydown", ListenerOptions::default(), move |ctx| {
if ctx.event.default_prevented() {
return;
}
let Some(focused) = ctx.dom.focused() else {
return;
};
if !is_number_input(ctx.dom, focused) {
return;
}
if ctx.dom.node(focused).has_attribute("disabled")
|| ctx.dom.node(focused).has_attribute("readonly")
{
return;
}
let Some(key) = ctx.event.detail.as_keyboard() else {
return;
};
if key.modifiers.ctrl || key.modifiers.shift || key.modifiers.alt || key.modifiers.meta {
return;
}
let direction = match key.key.as_str() {
"ArrowUp" => 1.0,
"ArrowDown" => -1.0,
_ => return,
};
step(ctx.dom, focused, direction);
})
.expect("number arrow stepper install");
}
fn step(dom: &mut TuiDom, input: NodeId, direction: f64) {
let current: f64 = crate::runtime::builtins::input::value(dom, input)
.parse()
.unwrap_or(0.0);
let step_size = parse_step(dom.node(input).get_attribute("step"));
let mut next = current + direction * step_size;
if let Some(min) = parse_bound(dom.node(input).get_attribute("min")) {
next = next.max(min);
}
if let Some(max) = parse_bound(dom.node(input).get_attribute("max")) {
next = next.min(max);
}
let formatted = format_number(next);
crate::runtime::builtins::input::set_value(dom, input, &formatted);
let mut input_ev = TuiEvent::input(rdom_core::InputType::InsertReplacementText, None);
let _ = dom.dispatch_tui_event(input, &mut input_ev);
let mut change_ev = TuiEvent::new("change");
let _ = dom.dispatch_tui_event(input, &mut change_ev);
}
fn is_numeric_char(c: char) -> bool {
c.is_ascii_digit() || matches!(c, '-' | '+' | '.')
}
fn is_number_input(dom: &TuiDom, id: NodeId) -> bool {
dom.node(id).tag_name() == Some("input") && dom.node(id).get_attribute("type") == Some("number")
}
fn parse_step(s: Option<&str>) -> f64 {
match s {
None | Some("any") => 1.0,
Some(v) => v.parse::<f64>().ok().filter(|n| *n > 0.0).unwrap_or(1.0),
}
}
fn parse_bound(s: Option<&str>) -> Option<f64> {
s.and_then(|v| v.parse::<f64>().ok())
}
fn format_number(n: f64) -> String {
if n == n.trunc() && n.is_finite() && n.abs() < 1e16 {
format!("{}", n as i64)
} else {
format!("{}", n)
}
}
#[cfg(test)]
mod tests;