1use crate::accessor::NodeMut;
31use crate::error::Result;
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct DomTokenList {
44 tokens: Vec<String>,
45}
46
47impl DomTokenList {
48 pub fn from_tokens(tokens: impl IntoIterator<Item = String>) -> Self {
53 Self {
54 tokens: tokens.into_iter().collect(),
55 }
56 }
57
58 pub fn len(&self) -> usize {
60 self.tokens.len()
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.tokens.is_empty()
66 }
67
68 pub fn item(&self, index: usize) -> Option<&str> {
71 self.tokens.get(index).map(String::as_str)
72 }
73
74 pub fn value(&self) -> String {
77 self.tokens.join(" ")
78 }
79
80 pub fn contains(&self, token: &str) -> bool {
82 self.tokens.iter().any(|t| t == token)
83 }
84
85 pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
87 self.tokens.iter().map(String::as_str)
88 }
89}
90
91pub struct DomTokenListMut<'a, Ext: 'static> {
96 node: NodeMut<'a, Ext>,
97}
98
99impl<'a, Ext: 'static> DomTokenListMut<'a, Ext> {
100 pub fn new(node: NodeMut<'a, Ext>) -> Self {
103 Self { node }
104 }
105
106 pub fn add(&mut self, token: &str) -> Result<()> {
109 self.node.add_class(token)
110 }
111
112 pub fn remove(&mut self, token: &str) -> Result<bool> {
116 self.node.remove_class(token)
117 }
118
119 pub fn toggle(&mut self, token: &str, force: Option<bool>) -> Result<bool> {
127 let has = self.contains(token);
128 match force {
129 Some(true) => {
130 if !has {
131 self.add(token)?;
132 }
133 Ok(true)
134 }
135 Some(false) => {
136 if has {
137 self.remove(token)?;
138 }
139 Ok(false)
140 }
141 None => {
142 if has {
143 self.remove(token)?;
144 Ok(false)
145 } else {
146 self.add(token)?;
147 Ok(true)
148 }
149 }
150 }
151 }
152
153 pub fn replace(&mut self, old: &str, new: &str) -> Result<bool> {
157 self.node.replace_class(old, new)
158 }
159
160 pub fn supports(&self, _token: &str) -> bool {
166 false
167 }
168
169 pub fn contains(&self, token: &str) -> bool {
171 self.node.as_ref().has_class(token)
172 }
173
174 pub fn len(&self) -> usize {
176 self.node.dom.class_list(self.node.id).count()
177 }
178
179 pub fn is_empty(&self) -> bool {
181 self.node.dom.class_list(self.node.id).next().is_none()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::Dom;
189
190 #[test]
193 fn snapshot_from_tokens_preserves_input_order() {
194 let list = DomTokenList::from_tokens(vec!["a".into(), "b".into(), "c".into()]);
195 assert_eq!(list.len(), 3);
196 assert!(!list.is_empty());
197 assert_eq!(list.item(0), Some("a"));
198 assert_eq!(list.item(1), Some("b"));
199 assert_eq!(list.item(2), Some("c"));
200 assert_eq!(list.item(3), None);
201 }
202
203 #[test]
204 fn snapshot_value_is_space_joined() {
205 let list = DomTokenList::from_tokens(vec!["foo".into(), "bar".into()]);
206 assert_eq!(list.value(), "foo bar");
207 }
208
209 #[test]
210 fn empty_snapshot_value_is_empty_string() {
211 let list = DomTokenList::from_tokens(Vec::<String>::new());
212 assert!(list.is_empty());
213 assert_eq!(list.value(), "");
214 }
215
216 #[test]
217 fn snapshot_contains_iter() {
218 let list = DomTokenList::from_tokens(vec!["x".into(), "y".into()]);
219 assert!(list.contains("x"));
220 assert!(list.contains("y"));
221 assert!(!list.contains("z"));
222 let collected: Vec<&str> = list.iter().collect();
223 assert_eq!(collected, ["x", "y"]);
224 }
225
226 fn element_with_classes(classes: &[&str]) -> (Dom, crate::node_id::NodeId) {
229 let mut dom: Dom = Dom::new();
230 let el = dom.create_element("div");
231 for c in classes {
232 dom.add_class(el, c).unwrap();
233 }
234 (dom, el)
235 }
236
237 #[test]
238 fn add_is_idempotent() {
239 let (mut dom, el) = element_with_classes(&[]);
240 let mut list = DomTokenListMut::new(dom.node_mut(el));
241 list.add("foo").unwrap();
242 list.add("foo").unwrap();
243 assert!(list.contains("foo"));
244 assert_eq!(list.len(), 1);
245 }
246
247 #[test]
248 fn remove_returns_true_when_present_false_when_absent() {
249 let (mut dom, el) = element_with_classes(&["foo"]);
250 let mut list = DomTokenListMut::new(dom.node_mut(el));
251 assert!(list.remove("foo").unwrap());
252 assert!(!list.remove("foo").unwrap());
253 assert!(!list.contains("foo"));
254 }
255
256 #[test]
257 fn toggle_with_force_true_is_add_idempotent() {
258 let (mut dom, el) = element_with_classes(&["foo"]);
262 let mut list = DomTokenListMut::new(dom.node_mut(el));
263
264 assert!(list.toggle("foo", Some(true)).unwrap());
266 assert!(list.contains("foo"));
267
268 assert!(list.toggle("bar", Some(true)).unwrap());
270 assert!(list.contains("bar"));
271 }
272
273 #[test]
274 fn toggle_with_force_false_is_remove_idempotent() {
275 let (mut dom, el) = element_with_classes(&["foo"]);
276 let mut list = DomTokenListMut::new(dom.node_mut(el));
277
278 assert!(!list.toggle("foo", Some(false)).unwrap());
280 assert!(!list.contains("foo"));
281
282 assert!(!list.toggle("foo", Some(false)).unwrap());
284 }
285
286 #[test]
287 fn toggle_with_no_force_flips_state() {
288 let (mut dom, el) = element_with_classes(&[]);
289 let mut list = DomTokenListMut::new(dom.node_mut(el));
290
291 assert!(list.toggle("foo", None).unwrap());
293 assert!(list.contains("foo"));
294
295 assert!(!list.toggle("foo", None).unwrap());
297 assert!(!list.contains("foo"));
298 }
299
300 #[test]
301 fn replace_swaps_existing_token() {
302 let (mut dom, el) = element_with_classes(&["foo", "bar"]);
303 let mut list = DomTokenListMut::new(dom.node_mut(el));
304 assert!(list.replace("foo", "baz").unwrap());
305 assert!(!list.contains("foo"));
306 assert!(list.contains("baz"));
307 assert!(list.contains("bar"));
308 }
309
310 #[test]
311 fn replace_returns_false_when_old_absent() {
312 let (mut dom, el) = element_with_classes(&["foo"]);
313 let mut list = DomTokenListMut::new(dom.node_mut(el));
314 assert!(!list.replace("missing", "new").unwrap());
315 assert!(!list.contains("new"));
316 }
317
318 #[test]
319 fn supports_always_false_for_classlist() {
320 let (mut dom, el) = element_with_classes(&[]);
321 let list = DomTokenListMut::new(dom.node_mut(el));
322 assert!(!list.supports("foo"));
324 assert!(!list.supports("anything"));
325 }
326
327 #[test]
328 fn len_and_is_empty_reflect_current_state() {
329 let (mut dom, el) = element_with_classes(&[]);
330 let mut list = DomTokenListMut::new(dom.node_mut(el));
331 assert!(list.is_empty());
332 assert_eq!(list.len(), 0);
333
334 list.add("a").unwrap();
335 list.add("b").unwrap();
336 assert!(!list.is_empty());
337 assert_eq!(list.len(), 2);
338 }
339}