use crate::dom::Dom;
use crate::input_type::InputTypeState;
use crate::node_id::NodeId;
struct GroupKey<'a> {
root: NodeId,
owner: Option<NodeId>,
name: &'a str,
}
impl<Ext> Dom<Ext> {
pub fn in_same_radio_group(&self, a: NodeId, b: NodeId) -> bool {
if !self.is_radio(a) || !self.is_radio(b) {
return false;
}
if a == b {
return true;
}
match (self.radio_group_key(a), self.radio_group_key(b)) {
(Some(ka), Some(kb)) => {
ka.root == kb.root && ka.owner == kb.owner && ka.name == kb.name
}
_ => false,
}
}
pub fn radio_group(&self, id: NodeId) -> Vec<NodeId> {
if !self.is_radio(id) {
return Vec::new();
}
let Some(key) = self.radio_group_key(id) else {
return vec![id];
};
let mut out = Vec::new();
let mut stack = vec![key.root];
while let Some(cur) = stack.pop() {
let Some(node) = self.get_node(cur) else {
continue;
};
if cur == id
|| (self.is_radio(cur)
&& self.get_attribute(cur, "name") == Some(key.name)
&& self.form_owner(cur) == key.owner)
{
out.push(cur);
}
let first = stack.len();
let mut c = node.first_child;
while let Some(cid) = c {
stack.push(cid);
c = self.get_node(cid).and_then(|n| n.next_sibling);
}
stack[first..].reverse();
}
out
}
fn is_radio(&self, id: NodeId) -> bool {
self.input_type_state(id) == Some(InputTypeState::Radio)
}
fn radio_group_key(&self, id: NodeId) -> Option<GroupKey<'_>> {
let name = self.get_attribute(id, "name").filter(|n| !n.is_empty())?;
Some(GroupKey {
root: self.root_of(id)?,
owner: self.form_owner(id),
name,
})
}
}
#[cfg(test)]
mod tests {
use crate::Dom;
use crate::node_id::NodeId;
fn el(dom: &mut Dom, parent: NodeId, tag: &str, attrs: &[(&str, &str)]) -> NodeId {
let e = dom.create_element(tag);
for (k, v) in attrs {
dom.set_attribute(e, k, v).unwrap();
}
dom.append_child(parent, e).unwrap();
e
}
fn radio(dom: &mut Dom, parent: NodeId, attrs: &[(&str, &str)]) -> NodeId {
let r = el(dom, parent, "input", &[("type", "radio")]);
for (k, v) in attrs {
dom.set_attribute(r, k, v).unwrap();
}
r
}
#[test]
fn same_named_radios_in_different_forms_are_different_groups() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let fa = el(&mut dom, root, "form", &[]);
let fb = el(&mut dom, root, "form", &[]);
let a1 = radio(&mut dom, fa, &[("name", "g")]);
let a2 = radio(&mut dom, fa, &[("name", "g")]);
let b1 = radio(&mut dom, fb, &[("name", "g")]);
let loose = radio(&mut dom, root, &[("name", "g")]);
assert_eq!(dom.radio_group(a2), vec![a1, a2]);
assert_eq!(dom.radio_group(b1), vec![b1]);
assert_eq!(dom.radio_group(loose), vec![loose]);
assert!(dom.in_same_radio_group(a1, a2));
assert!(!dom.in_same_radio_group(a1, b1));
assert!(!dom.in_same_radio_group(a1, loose));
}
#[test]
fn a_form_attribute_moves_a_radio_into_that_forms_group() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let before = radio(&mut dom, root, &[("name", "g"), ("form", "f")]);
let f = el(&mut dom, root, "form", &[("id", "f")]);
let inside = radio(&mut dom, f, &[("name", "g")]);
let elsewhere = radio(&mut dom, f, &[("name", "g"), ("form", "nope")]);
let unowned = radio(&mut dom, root, &[("name", "g")]);
assert_eq!(dom.radio_group(inside), vec![before, inside]);
assert_eq!(dom.radio_group(elsewhere), vec![elsewhere, unowned]);
}
#[test]
fn names_compare_exactly_and_an_empty_name_is_alone() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let upper = radio(&mut dom, root, &[("name", "G")]);
let lower = radio(&mut dom, root, &[("name", "g")]);
let empty1 = radio(&mut dom, root, &[("name", "")]);
let empty2 = radio(&mut dom, root, &[("name", "")]);
let none = radio(&mut dom, root, &[]);
assert!(!dom.in_same_radio_group(upper, lower));
assert!(!dom.in_same_radio_group(empty1, empty2));
assert_eq!(dom.radio_group(empty1), vec![empty1]);
assert_eq!(dom.radio_group(none), vec![none]);
assert!(dom.in_same_radio_group(none, none));
}
#[test]
fn non_radios_and_other_trees_are_not_in_the_group() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let r = radio(&mut dom, root, &[("name", "g")]);
let cb = el(
&mut dom,
root,
"input",
&[("type", "checkbox"), ("name", "g")],
);
let upper_type = el(&mut dom, root, "input", &[("type", "RADIO"), ("name", "g")]);
let detached = dom.create_element("div");
let other_tree = radio(&mut dom, detached, &[("name", "g")]);
assert_eq!(dom.radio_group(r), vec![r, upper_type]);
assert!(dom.radio_group(cb).is_empty());
assert!(!dom.in_same_radio_group(r, cb));
assert!(!dom.in_same_radio_group(r, other_tree));
assert_eq!(dom.radio_group(other_tree), vec![other_tree]);
}
}