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, "click", ListenerOptions::default(), move |ctx| {
if ctx.event.default_prevented() {
return;
}
let Some(target) = ctx.event.target else {
return;
};
let Some(button) = closest_form_button(ctx.dom, target) else {
return;
};
if ctx.dom.node(button).has_attribute("disabled") {
return;
}
let Some(form) = enclosing_form(ctx.dom, button) else {
return;
};
match button_action(ctx.dom, button) {
ButtonAction::Submit => {
let prevented = fire_submit(ctx.dom, form, Some(button));
if !prevented
&& is_dialog_form(ctx.dom, form)
&& let Some(dialog) =
crate::runtime::builtins::dialog::enclosing_dialog(ctx.dom, form)
{
let rv = ctx
.dom
.node(button)
.get_attribute("value")
.unwrap_or("")
.to_string();
crate::runtime::builtins::dialog::close(ctx.dom, dialog, &rv);
}
}
ButtonAction::Reset => {
let _ = fire_reset(ctx.dom, form);
}
ButtonAction::Button => {} }
})
.expect("form click listener 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;
};
let Some(key) = ctx.event.detail.as_keyboard() else {
return;
};
let no_mods = !key.modifiers.ctrl
&& !key.modifiers.shift
&& !key.modifiers.alt
&& !key.modifiers.meta;
if !no_mods || key.key != "Enter" {
return;
}
if !is_single_line_text_input(ctx.dom, focused) {
return;
}
let Some(form) = enclosing_form(ctx.dom, focused) else {
return;
};
if count_text_inputs(ctx.dom, form) != 1 {
return;
}
let prevented = fire_submit(ctx.dom, form, None);
if !prevented
&& is_dialog_form(ctx.dom, form)
&& let Some(dialog) = crate::runtime::builtins::dialog::enclosing_dialog(ctx.dom, form)
{
crate::runtime::builtins::dialog::close(ctx.dom, dialog, "");
}
})
.expect("form implicit-enter submit listener install");
}
pub fn collect(dom: &TuiDom, form: NodeId) -> Vec<(String, String)> {
let mut out = Vec::new();
walk_collect(dom, form, &mut out);
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ButtonAction {
Submit,
Reset,
Button,
}
fn button_action(dom: &TuiDom, id: NodeId) -> ButtonAction {
let node = dom.node(id);
let ty = node.get_attribute("type");
match (node.tag_name(), ty) {
(Some("button"), None)
| (Some("button"), Some("submit"))
| (Some("input"), Some("submit")) => ButtonAction::Submit,
(Some("button"), Some("reset")) | (Some("input"), Some("reset")) => ButtonAction::Reset,
_ => ButtonAction::Button,
}
}
fn closest_form_button(dom: &TuiDom, id: NodeId) -> Option<NodeId> {
let mut cur = Some(id);
while let Some(n) = cur {
let node = dom.node(n);
match node.tag_name() {
Some("button") => return Some(n),
Some("input") => {
if matches!(
node.get_attribute("type"),
Some("submit") | Some("reset") | Some("button")
) {
return Some(n);
}
}
_ => {}
}
cur = node.parent_node().map(|p| p.id());
}
None
}
fn enclosing_form(dom: &TuiDom, id: NodeId) -> Option<NodeId> {
let mut cur = Some(id);
while let Some(n) = cur {
if dom.node(n).tag_name() == Some("form") {
return Some(n);
}
cur = dom.node(n).parent_node().map(|p| p.id());
}
None
}
pub fn elements(dom: &TuiDom, form: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
walk_elements(dom, form, form, &mut out);
out
}
fn walk_elements(dom: &TuiDom, form: NodeId, id: NodeId, out: &mut Vec<NodeId>) {
if id != form && is_form_control(dom, id) {
out.push(id);
}
for child in dom.node(id).child_nodes() {
walk_elements(dom, form, child.id(), out);
}
}
fn is_form_control(dom: &TuiDom, id: NodeId) -> bool {
matches!(
dom.node(id).tag_name(),
Some("button" | "fieldset" | "input" | "object" | "output" | "select" | "textarea")
)
}
pub(crate) fn fire_submit(dom: &mut TuiDom, form: NodeId, submitter: Option<NodeId>) -> bool {
let mut ev = TuiEvent::new("submit");
ev.event.detail =
rdom_core::EventDetail::Submit(Box::new(rdom_core::SubmitDetail { submitter }));
let _ = dom.dispatch_tui_event(form, &mut ev);
ev.event.default_prevented()
}
fn fire_reset(dom: &mut TuiDom, form: NodeId) -> bool {
let mut ev = TuiEvent::new("reset");
let _ = dom.dispatch_tui_event(form, &mut ev);
ev.event.default_prevented()
}
fn is_dialog_form(dom: &TuiDom, form: NodeId) -> bool {
matches!(
dom.node(form)
.get_attribute("method")
.map(|s| s.to_ascii_lowercase()),
Some(ref m) if m == "dialog"
)
}
fn is_single_line_text_input(dom: &TuiDom, id: NodeId) -> bool {
if dom.node(id).tag_name() != Some("input") {
return false;
}
matches!(
dom.node(id).get_attribute("type"),
None | Some("text")
| Some("password")
| Some("email")
| Some("url")
| Some("tel")
| Some("search")
| Some("number")
)
}
fn count_text_inputs(dom: &TuiDom, form: NodeId) -> usize {
fn walk(dom: &TuiDom, id: NodeId, count: &mut usize) {
if is_single_line_text_input(dom, id) {
*count += 1;
}
for child in dom.node(id).child_nodes() {
walk(dom, child.id(), count);
}
}
let mut n = 0;
walk(dom, form, &mut n);
n
}
fn walk_collect(dom: &TuiDom, id: NodeId, out: &mut Vec<(String, String)>) {
let node = dom.node(id);
if !node.has_attribute("disabled") {
let name = node.get_attribute("name").unwrap_or("").to_string();
if !name.is_empty() {
#[allow(clippy::collapsible_match, clippy::collapsible_if)]
match (node.tag_name(), node.get_attribute("type")) {
(Some("input"), Some("checkbox")) | (Some("input"), Some("radio")) => {
if node.has_attribute("checked") {
let value = node.get_attribute("value").unwrap_or("on").to_string();
out.push((name, value));
}
}
(Some("input"), Some("submit"))
| (Some("input"), Some("reset"))
| (Some("input"), Some("button"))
| (Some("input"), Some("hidden")) => {
if let Some(value) = node.get_attribute("value") {
out.push((name, value.to_string()));
}
}
(Some("input"), _) => {
out.push((name, crate::runtime::builtins::input::value(dom, id)));
}
(Some("textarea"), _) => {
let mut text = String::new();
for child in node.child_nodes() {
if child.node_type() == rdom_core::NodeType::Text
&& let Some(s) = child.node_value()
{
text.push_str(s);
}
}
out.push((name, text));
}
(Some("select"), _) => {
let selected = crate::runtime::builtins::select::selected_options(dom, id);
for opt in selected {
let value = crate::runtime::builtins::select::option_value(dom, opt);
out.push((name.clone(), value));
}
}
_ => {}
}
}
}
for child in node.child_nodes() {
walk_collect(dom, child.id(), out);
}
}
#[cfg(test)]
mod tests;