1use crate::dom::Dom;
9use crate::error::{DomError, Result};
10use crate::node::NodeData;
11use crate::node_id::NodeId;
12use crate::observer::Mutation;
13
14impl<Ext: 'static> Dom<Ext> {
15 pub fn set_attribute(&mut self, id: NodeId, key: &str, value: &str) -> Result<()> {
18 let old_full = match &mut self.node_mut_or_err(id)?.data {
19 NodeData::Element { attrs, .. } => attrs.insert(key.to_string(), value.to_string()),
20 other => {
21 return Err(DomError::WrongNodeType {
22 expected: "Element",
23 got: node_type_of(other),
24 });
25 }
26 };
27 if key == "id" {
28 if let Some(prev) = &old_full {
29 self.indexes.unregister_id(id, prev);
30 }
31 self.indexes.register_id(id, value);
32 }
33 if key == "class" {
40 self.sync_class_list_from_attribute_value(id, value);
41 }
42 self.fire_mutation(Mutation::AttributeChanged {
43 id,
44 name: key.to_string(),
45 old: old_full,
46 new: Some(value.to_string()),
47 });
48 Ok(())
49 }
50
51 fn sync_class_list_from_attribute_value(&mut self, id: NodeId, value: &str) {
61 let new_tokens: std::collections::BTreeSet<String> =
62 value.split_whitespace().map(String::from).collect();
63 let old_tokens: std::collections::BTreeSet<String> = match self.get_node(id) {
64 Some(node) => match &node.data {
65 NodeData::Element { classes, .. } => classes.clone(),
66 _ => return,
67 },
68 None => return,
69 };
70 if new_tokens == old_tokens {
71 return;
72 }
73 let added: Vec<String> = new_tokens.difference(&old_tokens).cloned().collect();
74 let removed: Vec<String> = old_tokens.difference(&new_tokens).cloned().collect();
75
76 for cls in &removed {
77 self.indexes.unregister_class(id, cls);
78 }
79 for cls in &added {
80 self.indexes.register_class(id, cls);
81 }
82 if let Some(node) = self.node_mut_or_err(id).ok()
83 && let NodeData::Element { classes, .. } = &mut node.data
84 {
85 *classes = new_tokens;
86 }
87 self.fire_mutation(Mutation::ClassChanged { id, added, removed });
88 }
89
90 fn sync_class_attribute_from_class_list(&mut self, id: NodeId) {
99 let new_attr: String = match self.get_node(id) {
100 Some(node) => match &node.data {
101 NodeData::Element { classes, .. } => {
102 classes.iter().cloned().collect::<Vec<_>>().join(" ")
103 }
104 _ => return,
105 },
106 None => return,
107 };
108 if let Some(node) = self.node_mut_or_err(id).ok()
115 && let NodeData::Element { attrs, .. } = &mut node.data
116 {
117 if new_attr.is_empty() {
118 attrs.remove("class");
119 } else {
120 attrs.insert("class".to_string(), new_attr);
121 }
122 }
123 }
124
125 pub fn get_attribute(&self, id: NodeId, key: &str) -> Option<&str> {
126 match &self.get_node(id)?.data {
127 NodeData::Element { attrs, .. } => attrs.get(key).map(String::as_str),
128 _ => None,
129 }
130 }
131
132 pub fn remove_attribute(&mut self, id: NodeId, key: &str) -> Result<bool> {
133 let removed = match &mut self.node_mut_or_err(id)?.data {
134 NodeData::Element { attrs, .. } => attrs.remove(key),
135 other => {
136 return Err(DomError::WrongNodeType {
137 expected: "Element",
138 got: node_type_of(other),
139 });
140 }
141 };
142 if key == "id"
143 && let Some(prev) = &removed
144 {
145 self.indexes.unregister_id(id, prev);
146 }
147 if removed.is_some() {
148 self.fire_mutation(Mutation::AttributeChanged {
149 id,
150 name: key.to_string(),
151 old: removed.clone(),
152 new: None,
153 });
154 }
155 Ok(removed.is_some())
156 }
157
158 pub fn has_attribute(&self, id: NodeId, key: &str) -> bool {
159 matches!(
160 self.get_node(id).map(|n| &n.data),
161 Some(NodeData::Element { attrs, .. }) if attrs.contains_key(key)
162 )
163 }
164
165 pub fn toggle_attribute(&mut self, id: NodeId, key: &str) -> Result<bool> {
168 let (was_present, prev_value) = match &mut self.node_mut_or_err(id)?.data {
169 NodeData::Element { attrs, .. } => {
170 if let Some(prev) = attrs.remove(key) {
171 (true, Some(prev))
172 } else {
173 attrs.insert(key.to_string(), String::new());
174 (false, None)
175 }
176 }
177 other => {
178 return Err(DomError::WrongNodeType {
179 expected: "Element",
180 got: node_type_of(other),
181 });
182 }
183 };
184 if key == "id"
185 && let Some(prev) = &prev_value
186 {
187 self.indexes.unregister_id(id, prev);
188 }
189 let (old, new) = if was_present {
191 (prev_value.clone(), None)
192 } else {
193 (None, Some(String::new()))
194 };
195 self.fire_mutation(Mutation::AttributeChanged {
196 id,
197 name: key.to_string(),
198 old,
199 new,
200 });
201 Ok(!was_present)
202 }
203
204 pub fn attributes(&self, id: NodeId) -> impl Iterator<Item = (&str, &str)> {
206 let slot = self.get_node(id);
207
208 match slot.map(|n| &n.data) {
209 Some(NodeData::Element { attrs, .. }) => {
210 Box::new(attrs.iter().map(|(k, v)| (k.as_str(), v.as_str())))
211 as Box<dyn Iterator<Item = (&str, &str)>>
212 }
213 _ => Box::new(std::iter::empty()) as Box<dyn Iterator<Item = (&str, &str)>>,
214 }
215 }
216
217 pub fn set_id(&mut self, id: NodeId, value: &str) -> Result<()> {
219 self.set_attribute(id, "id", value)
220 }
221
222 pub fn id_attr(&self, id: NodeId) -> Option<&str> {
223 self.get_attribute(id, "id")
224 }
225
226 pub fn add_class(&mut self, id: NodeId, class: &str) -> Result<()> {
229 let inserted = match &mut self.node_mut_or_err(id)?.data {
230 NodeData::Element { classes, .. } => classes.insert(class.to_string()),
231 other => {
232 return Err(DomError::WrongNodeType {
233 expected: "Element",
234 got: node_type_of(other),
235 });
236 }
237 };
238 if inserted {
239 self.indexes.register_class(id, class);
240 self.sync_class_attribute_from_class_list(id);
241 self.fire_mutation(Mutation::ClassChanged {
242 id,
243 added: vec![class.to_string()],
244 removed: vec![],
245 });
246 }
247 Ok(())
248 }
249
250 pub fn remove_class(&mut self, id: NodeId, class: &str) -> Result<bool> {
251 let removed = match &mut self.node_mut_or_err(id)?.data {
252 NodeData::Element { classes, .. } => classes.remove(class),
253 other => {
254 return Err(DomError::WrongNodeType {
255 expected: "Element",
256 got: node_type_of(other),
257 });
258 }
259 };
260 if removed {
261 self.indexes.unregister_class(id, class);
262 self.sync_class_attribute_from_class_list(id);
263 self.fire_mutation(Mutation::ClassChanged {
264 id,
265 added: vec![],
266 removed: vec![class.to_string()],
267 });
268 }
269 Ok(removed)
270 }
271
272 pub fn toggle_class(&mut self, id: NodeId, class: &str) -> Result<bool> {
273 let (removed, added) = match &mut self.node_mut_or_err(id)?.data {
274 NodeData::Element { classes, .. } => {
275 if classes.remove(class) {
276 (true, false)
277 } else {
278 classes.insert(class.to_string());
279 (false, true)
280 }
281 }
282 other => {
283 return Err(DomError::WrongNodeType {
284 expected: "Element",
285 got: node_type_of(other),
286 });
287 }
288 };
289 if removed {
290 self.indexes.unregister_class(id, class);
291 self.sync_class_attribute_from_class_list(id);
292 self.fire_mutation(Mutation::ClassChanged {
293 id,
294 added: vec![],
295 removed: vec![class.to_string()],
296 });
297 } else if added {
298 self.indexes.register_class(id, class);
299 self.sync_class_attribute_from_class_list(id);
300 self.fire_mutation(Mutation::ClassChanged {
301 id,
302 added: vec![class.to_string()],
303 removed: vec![],
304 });
305 }
306 Ok(added)
307 }
308
309 pub fn has_class(&self, id: NodeId, class: &str) -> bool {
310 matches!(
311 self.get_node(id).map(|n| &n.data),
312 Some(NodeData::Element { classes, .. }) if classes.contains(class)
313 )
314 }
315
316 pub fn replace_class(&mut self, id: NodeId, old: &str, new: &str) -> Result<bool> {
317 let swapped = match &mut self.node_mut_or_err(id)?.data {
318 NodeData::Element { classes, .. } => {
319 if classes.remove(old) {
320 classes.insert(new.to_string());
321 true
322 } else {
323 false
324 }
325 }
326 other => {
327 return Err(DomError::WrongNodeType {
328 expected: "Element",
329 got: node_type_of(other),
330 });
331 }
332 };
333 if swapped {
334 self.indexes.unregister_class(id, old);
335 self.indexes.register_class(id, new);
336 self.sync_class_attribute_from_class_list(id);
337 self.fire_mutation(Mutation::ClassChanged {
338 id,
339 added: vec![new.to_string()],
340 removed: vec![old.to_string()],
341 });
342 }
343 Ok(swapped)
344 }
345
346 pub fn class_list(&self, id: NodeId) -> impl Iterator<Item = &str> {
348 let slot = self.get_node(id);
349
350 match slot.map(|n| &n.data) {
351 Some(NodeData::Element { classes, .. }) => {
352 Box::new(classes.iter().map(String::as_str)) as Box<dyn Iterator<Item = &str>>
353 }
354 _ => Box::new(std::iter::empty()) as Box<dyn Iterator<Item = &str>>,
355 }
356 }
357}
358
359fn node_type_of<Ext>(data: &NodeData<Ext>) -> crate::node::NodeType {
360 use crate::node::NodeType;
361 match data {
362 NodeData::Element { .. } => NodeType::Element,
363 NodeData::Text { .. } => NodeType::Text,
364 NodeData::Comment { .. } => NodeType::Comment,
365 NodeData::Fragment => NodeType::Fragment,
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
376 fn set_get_remove_attribute() {
377 let mut dom: Dom = Dom::new();
378 let el = dom.create_element("div");
379 assert!(!dom.has_attribute(el, "role"));
380
381 dom.set_attribute(el, "role", "banner").unwrap();
382 assert_eq!(dom.get_attribute(el, "role"), Some("banner"));
383 assert!(dom.has_attribute(el, "role"));
384
385 assert!(dom.remove_attribute(el, "role").unwrap());
386 assert!(!dom.has_attribute(el, "role"));
387 assert!(!dom.remove_attribute(el, "role").unwrap());
388 }
389
390 #[test]
391 fn set_attribute_overwrites() {
392 let mut dom: Dom = Dom::new();
393 let el = dom.create_element("div");
394 dom.set_attribute(el, "role", "banner").unwrap();
395 dom.set_attribute(el, "role", "navigation").unwrap();
396 assert_eq!(dom.get_attribute(el, "role"), Some("navigation"));
397 }
398
399 #[test]
400 fn toggle_attribute_flips_presence() {
401 let mut dom: Dom = Dom::new();
402 let el = dom.create_element("input");
403 assert!(dom.toggle_attribute(el, "disabled").unwrap()); assert!(dom.has_attribute(el, "disabled"));
405 assert_eq!(dom.get_attribute(el, "disabled"), Some(""));
406
407 assert!(!dom.toggle_attribute(el, "disabled").unwrap()); assert!(!dom.has_attribute(el, "disabled"));
409 }
410
411 #[test]
412 fn attributes_iterate_in_alpha_order() {
413 let mut dom: Dom = Dom::new();
414 let el = dom.create_element("div");
415 dom.set_attribute(el, "z", "1").unwrap();
416 dom.set_attribute(el, "a", "2").unwrap();
417 dom.set_attribute(el, "m", "3").unwrap();
418 let names: Vec<&str> = dom.attributes(el).map(|(k, _)| k).collect();
419 assert_eq!(names, vec!["a", "m", "z"]);
420 }
421
422 #[test]
423 fn attribute_on_non_element_errors() {
424 let mut dom: Dom = Dom::new();
425 let t = dom.create_text_node("hi");
426 assert!(matches!(
427 dom.set_attribute(t, "role", "banner").unwrap_err(),
428 DomError::WrongNodeType { .. }
429 ));
430 assert!(dom.get_attribute(t, "anything").is_none());
432 }
433
434 #[test]
435 fn id_sugar() {
436 let mut dom: Dom = Dom::new();
437 let el = dom.create_element("div");
438 dom.set_id(el, "hero").unwrap();
439 assert_eq!(dom.id_attr(el), Some("hero"));
440 }
441
442 #[test]
445 fn add_remove_has_class() {
446 let mut dom: Dom = Dom::new();
447 let el = dom.create_element("div");
448 dom.add_class(el, "active").unwrap();
449 assert!(dom.has_class(el, "active"));
450 assert!(dom.remove_class(el, "active").unwrap());
451 assert!(!dom.has_class(el, "active"));
452 assert!(!dom.remove_class(el, "active").unwrap());
453 }
454
455 #[test]
456 fn add_class_is_idempotent() {
457 let mut dom: Dom = Dom::new();
458 let el = dom.create_element("div");
459 dom.add_class(el, "active").unwrap();
460 dom.add_class(el, "active").unwrap();
461 let list: Vec<&str> = dom.class_list(el).collect();
462 assert_eq!(list, vec!["active"]);
463 }
464
465 #[test]
466 fn toggle_class() {
467 let mut dom: Dom = Dom::new();
468 let el = dom.create_element("div");
469 assert!(dom.toggle_class(el, "on").unwrap()); assert!(!dom.toggle_class(el, "on").unwrap()); }
472
473 #[test]
474 fn replace_class_swaps() {
475 let mut dom: Dom = Dom::new();
476 let el = dom.create_element("div");
477 dom.add_class(el, "old").unwrap();
478 assert!(dom.replace_class(el, "old", "new").unwrap());
479 assert!(!dom.has_class(el, "old"));
480 assert!(dom.has_class(el, "new"));
481 }
482
483 #[test]
484 fn replace_class_returns_false_when_old_missing() {
485 let mut dom: Dom = Dom::new();
486 let el = dom.create_element("div");
487 assert!(!dom.replace_class(el, "never-there", "new").unwrap());
488 assert!(!dom.has_class(el, "new")); }
490
491 #[test]
492 fn class_list_alpha_order() {
493 let mut dom: Dom = Dom::new();
494 let el = dom.create_element("div");
495 dom.add_class(el, "zeta").unwrap();
496 dom.add_class(el, "alpha").unwrap();
497 dom.add_class(el, "mu").unwrap();
498 let list: Vec<&str> = dom.class_list(el).collect();
499 assert_eq!(list, vec!["alpha", "mu", "zeta"]);
500 }
501
502 #[test]
503 fn class_on_non_element_errors() {
504 let mut dom: Dom = Dom::new();
505 let t = dom.create_text_node("hi");
506 assert!(matches!(
507 dom.add_class(t, "x").unwrap_err(),
508 DomError::WrongNodeType { .. }
509 ));
510 assert!(!dom.has_class(t, "anything"));
511 }
512
513 #[test]
516 fn set_attribute_class_syncs_class_list() {
517 let mut dom: Dom = Dom::new();
525 let el = dom.create_element("div");
526
527 dom.set_attribute(el, "class", "alpha beta").unwrap();
528
529 let tokens: Vec<&str> = dom.class_list(el).collect();
531 assert!(tokens.contains(&"alpha"));
532 assert!(tokens.contains(&"beta"));
533 assert_eq!(tokens.len(), 2);
534
535 assert!(dom.has_class(el, "alpha"));
537 assert!(dom.has_class(el, "beta"));
538 assert!(!dom.has_class(el, "gamma"));
539 }
540
541 #[test]
542 fn set_attribute_class_replaces_existing_classes() {
543 let mut dom: Dom = Dom::new();
546 let el = dom.create_element("div");
547 dom.add_class(el, "old").unwrap();
548 assert!(dom.has_class(el, "old"));
549
550 dom.set_attribute(el, "class", "fresh").unwrap();
551
552 assert!(!dom.has_class(el, "old"), "old token cleared");
553 assert!(dom.has_class(el, "fresh"), "new token present");
554 }
555
556 #[test]
557 fn set_attribute_class_empty_clears_class_list() {
558 let mut dom: Dom = Dom::new();
559 let el = dom.create_element("div");
560 dom.add_class(el, "x").unwrap();
561 dom.add_class(el, "y").unwrap();
562 assert_eq!(dom.class_list(el).count(), 2);
563
564 dom.set_attribute(el, "class", "").unwrap();
565
566 assert_eq!(dom.class_list(el).count(), 0);
567 }
568
569 #[test]
570 fn add_class_syncs_class_attribute() {
571 let mut dom: Dom = Dom::new();
575 let el = dom.create_element("div");
576
577 dom.add_class(el, "foo").unwrap();
578
579 let attr = dom.get_attribute(el, "class");
580 assert_eq!(attr, Some("foo"), "add_class wrote the attribute as well");
581 }
582
583 #[test]
584 fn remove_class_syncs_class_attribute() {
585 let mut dom: Dom = Dom::new();
586 let el = dom.create_element("div");
587 dom.add_class(el, "a").unwrap();
588 dom.add_class(el, "b").unwrap();
589 assert!(
590 dom.get_attribute(el, "class").unwrap().contains('a')
591 && dom.get_attribute(el, "class").unwrap().contains('b')
592 );
593
594 dom.remove_class(el, "a").unwrap();
595
596 let attr = dom.get_attribute(el, "class").unwrap_or("");
597 assert!(!attr.contains('a'), "removed token gone from attribute");
598 assert!(attr.contains('b'), "remaining token still in attribute");
599 }
600
601 #[test]
602 fn set_attribute_then_class_selector_via_index_round_trips() {
603 let mut dom: Dom = Dom::new();
609 let root = dom.root();
610 let el = dom.create_element("div");
611 dom.append_child(root, el).unwrap();
612
613 dom.set_attribute(el, "class", "hero").unwrap();
614
615 let matches = dom.get_elements_by_class_name(root, "hero");
616 assert!(
617 matches.contains(&el),
618 "el is in the indexed match set for .hero (got {matches:?})"
619 );
620 }
621}