1use crate::dom::Dom;
16use crate::input_type::InputTypeState;
17use crate::node_id::NodeId;
18
19struct GroupKey<'a> {
21 root: NodeId,
22 owner: Option<NodeId>,
23 name: &'a str,
24}
25
26impl<Ext> Dom<Ext> {
27 pub fn in_same_radio_group(&self, a: NodeId, b: NodeId) -> bool {
32 if !self.is_radio(a) || !self.is_radio(b) {
33 return false;
34 }
35 if a == b {
36 return true;
37 }
38 match (self.radio_group_key(a), self.radio_group_key(b)) {
39 (Some(ka), Some(kb)) => {
40 ka.root == kb.root && ka.owner == kb.owner && ka.name == kb.name
41 }
42 _ => false,
43 }
44 }
45
46 pub fn radio_group(&self, id: NodeId) -> Vec<NodeId> {
51 if !self.is_radio(id) {
52 return Vec::new();
53 }
54 let Some(key) = self.radio_group_key(id) else {
55 return vec![id];
56 };
57 let mut out = Vec::new();
58 let mut stack = vec![key.root];
59 while let Some(cur) = stack.pop() {
60 let Some(node) = self.get_node(cur) else {
61 continue;
62 };
63 if cur == id
64 || (self.is_radio(cur)
65 && self.get_attribute(cur, "name") == Some(key.name)
66 && self.form_owner(cur) == key.owner)
67 {
68 out.push(cur);
69 }
70 let first = stack.len();
72 let mut c = node.first_child;
73 while let Some(cid) = c {
74 stack.push(cid);
75 c = self.get_node(cid).and_then(|n| n.next_sibling);
76 }
77 stack[first..].reverse();
78 }
79 out
80 }
81
82 fn is_radio(&self, id: NodeId) -> bool {
83 self.input_type_state(id) == Some(InputTypeState::Radio)
84 }
85
86 fn radio_group_key(&self, id: NodeId) -> Option<GroupKey<'_>> {
89 let name = self.get_attribute(id, "name").filter(|n| !n.is_empty())?;
90 Some(GroupKey {
91 root: self.root_of(id)?,
92 owner: self.form_owner(id),
93 name,
94 })
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use crate::Dom;
101 use crate::node_id::NodeId;
102
103 fn el(dom: &mut Dom, parent: NodeId, tag: &str, attrs: &[(&str, &str)]) -> NodeId {
104 let e = dom.create_element(tag);
105 for (k, v) in attrs {
106 dom.set_attribute(e, k, v).unwrap();
107 }
108 dom.append_child(parent, e).unwrap();
109 e
110 }
111
112 fn radio(dom: &mut Dom, parent: NodeId, attrs: &[(&str, &str)]) -> NodeId {
113 let r = el(dom, parent, "input", &[("type", "radio")]);
114 for (k, v) in attrs {
115 dom.set_attribute(r, k, v).unwrap();
116 }
117 r
118 }
119
120 #[test]
121 fn same_named_radios_in_different_forms_are_different_groups() {
122 let mut dom: Dom = Dom::new();
123 let root = dom.root();
124 let fa = el(&mut dom, root, "form", &[]);
125 let fb = el(&mut dom, root, "form", &[]);
126 let a1 = radio(&mut dom, fa, &[("name", "g")]);
127 let a2 = radio(&mut dom, fa, &[("name", "g")]);
128 let b1 = radio(&mut dom, fb, &[("name", "g")]);
129 let loose = radio(&mut dom, root, &[("name", "g")]);
130 assert_eq!(dom.radio_group(a2), vec![a1, a2]);
131 assert_eq!(dom.radio_group(b1), vec![b1]);
132 assert_eq!(dom.radio_group(loose), vec![loose]);
133 assert!(dom.in_same_radio_group(a1, a2));
134 assert!(!dom.in_same_radio_group(a1, b1));
135 assert!(!dom.in_same_radio_group(a1, loose));
136 }
137
138 #[test]
139 fn a_form_attribute_moves_a_radio_into_that_forms_group() {
140 let mut dom: Dom = Dom::new();
141 let root = dom.root();
142 let before = radio(&mut dom, root, &[("name", "g"), ("form", "f")]);
143 let f = el(&mut dom, root, "form", &[("id", "f")]);
144 let inside = radio(&mut dom, f, &[("name", "g")]);
145 let elsewhere = radio(&mut dom, f, &[("name", "g"), ("form", "nope")]);
146 let unowned = radio(&mut dom, root, &[("name", "g")]);
147 assert_eq!(dom.radio_group(inside), vec![before, inside]);
148 assert_eq!(dom.radio_group(elsewhere), vec![elsewhere, unowned]);
149 }
150
151 #[test]
152 fn names_compare_exactly_and_an_empty_name_is_alone() {
153 let mut dom: Dom = Dom::new();
154 let root = dom.root();
155 let upper = radio(&mut dom, root, &[("name", "G")]);
156 let lower = radio(&mut dom, root, &[("name", "g")]);
157 let empty1 = radio(&mut dom, root, &[("name", "")]);
158 let empty2 = radio(&mut dom, root, &[("name", "")]);
159 let none = radio(&mut dom, root, &[]);
160 assert!(!dom.in_same_radio_group(upper, lower));
161 assert!(!dom.in_same_radio_group(empty1, empty2));
162 assert_eq!(dom.radio_group(empty1), vec![empty1]);
163 assert_eq!(dom.radio_group(none), vec![none]);
164 assert!(dom.in_same_radio_group(none, none));
165 }
166
167 #[test]
168 fn non_radios_and_other_trees_are_not_in_the_group() {
169 let mut dom: Dom = Dom::new();
170 let root = dom.root();
171 let r = radio(&mut dom, root, &[("name", "g")]);
172 let cb = el(
173 &mut dom,
174 root,
175 "input",
176 &[("type", "checkbox"), ("name", "g")],
177 );
178 let upper_type = el(&mut dom, root, "input", &[("type", "RADIO"), ("name", "g")]);
179 let detached = dom.create_element("div");
180 let other_tree = radio(&mut dom, detached, &[("name", "g")]);
181 assert_eq!(dom.radio_group(r), vec![r, upper_type]);
182 assert!(dom.radio_group(cb).is_empty());
183 assert!(!dom.in_same_radio_group(r, cb));
184 assert!(!dom.in_same_radio_group(r, other_tree));
185 assert_eq!(dom.radio_group(other_tree), vec![other_tree]);
186 }
187}