use crate::accessor::NodeMut;
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DomTokenList {
tokens: Vec<String>,
}
impl DomTokenList {
pub fn from_tokens(tokens: impl IntoIterator<Item = String>) -> Self {
Self {
tokens: tokens.into_iter().collect(),
}
}
pub fn len(&self) -> usize {
self.tokens.len()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
pub fn item(&self, index: usize) -> Option<&str> {
self.tokens.get(index).map(String::as_str)
}
pub fn value(&self) -> String {
self.tokens.join(" ")
}
pub fn contains(&self, token: &str) -> bool {
self.tokens.iter().any(|t| t == token)
}
pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
self.tokens.iter().map(String::as_str)
}
}
pub struct DomTokenListMut<'a, Ext: 'static> {
node: NodeMut<'a, Ext>,
}
impl<'a, Ext: 'static> DomTokenListMut<'a, Ext> {
pub fn new(node: NodeMut<'a, Ext>) -> Self {
Self { node }
}
pub fn add(&mut self, token: &str) -> Result<()> {
self.node.add_class(token)
}
pub fn remove(&mut self, token: &str) -> Result<bool> {
self.node.remove_class(token)
}
pub fn toggle(&mut self, token: &str, force: Option<bool>) -> Result<bool> {
let has = self.contains(token);
match force {
Some(true) => {
if !has {
self.add(token)?;
}
Ok(true)
}
Some(false) => {
if has {
self.remove(token)?;
}
Ok(false)
}
None => {
if has {
self.remove(token)?;
Ok(false)
} else {
self.add(token)?;
Ok(true)
}
}
}
}
pub fn replace(&mut self, old: &str, new: &str) -> Result<bool> {
self.node.replace_class(old, new)
}
pub fn supports(&self, _token: &str) -> bool {
false
}
pub fn contains(&self, token: &str) -> bool {
self.node.as_ref().has_class(token)
}
pub fn len(&self) -> usize {
self.node.dom.class_list(self.node.id).count()
}
pub fn is_empty(&self) -> bool {
self.node.dom.class_list(self.node.id).next().is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Dom;
#[test]
fn snapshot_from_tokens_preserves_input_order() {
let list = DomTokenList::from_tokens(vec!["a".into(), "b".into(), "c".into()]);
assert_eq!(list.len(), 3);
assert!(!list.is_empty());
assert_eq!(list.item(0), Some("a"));
assert_eq!(list.item(1), Some("b"));
assert_eq!(list.item(2), Some("c"));
assert_eq!(list.item(3), None);
}
#[test]
fn snapshot_value_is_space_joined() {
let list = DomTokenList::from_tokens(vec!["foo".into(), "bar".into()]);
assert_eq!(list.value(), "foo bar");
}
#[test]
fn empty_snapshot_value_is_empty_string() {
let list = DomTokenList::from_tokens(Vec::<String>::new());
assert!(list.is_empty());
assert_eq!(list.value(), "");
}
#[test]
fn snapshot_contains_iter() {
let list = DomTokenList::from_tokens(vec!["x".into(), "y".into()]);
assert!(list.contains("x"));
assert!(list.contains("y"));
assert!(!list.contains("z"));
let collected: Vec<&str> = list.iter().collect();
assert_eq!(collected, ["x", "y"]);
}
fn element_with_classes(classes: &[&str]) -> (Dom, crate::node_id::NodeId) {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
for c in classes {
dom.add_class(el, c).unwrap();
}
(dom, el)
}
#[test]
fn add_is_idempotent() {
let (mut dom, el) = element_with_classes(&[]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
list.add("foo").unwrap();
list.add("foo").unwrap();
assert!(list.contains("foo"));
assert_eq!(list.len(), 1);
}
#[test]
fn remove_returns_true_when_present_false_when_absent() {
let (mut dom, el) = element_with_classes(&["foo"]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
assert!(list.remove("foo").unwrap());
assert!(!list.remove("foo").unwrap());
assert!(!list.contains("foo"));
}
#[test]
fn toggle_with_force_true_is_add_idempotent() {
let (mut dom, el) = element_with_classes(&["foo"]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
assert!(list.toggle("foo", Some(true)).unwrap());
assert!(list.contains("foo"));
assert!(list.toggle("bar", Some(true)).unwrap());
assert!(list.contains("bar"));
}
#[test]
fn toggle_with_force_false_is_remove_idempotent() {
let (mut dom, el) = element_with_classes(&["foo"]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
assert!(!list.toggle("foo", Some(false)).unwrap());
assert!(!list.contains("foo"));
assert!(!list.toggle("foo", Some(false)).unwrap());
}
#[test]
fn toggle_with_no_force_flips_state() {
let (mut dom, el) = element_with_classes(&[]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
assert!(list.toggle("foo", None).unwrap());
assert!(list.contains("foo"));
assert!(!list.toggle("foo", None).unwrap());
assert!(!list.contains("foo"));
}
#[test]
fn replace_swaps_existing_token() {
let (mut dom, el) = element_with_classes(&["foo", "bar"]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
assert!(list.replace("foo", "baz").unwrap());
assert!(!list.contains("foo"));
assert!(list.contains("baz"));
assert!(list.contains("bar"));
}
#[test]
fn replace_returns_false_when_old_absent() {
let (mut dom, el) = element_with_classes(&["foo"]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
assert!(!list.replace("missing", "new").unwrap());
assert!(!list.contains("new"));
}
#[test]
fn supports_always_false_for_classlist() {
let (mut dom, el) = element_with_classes(&[]);
let list = DomTokenListMut::new(dom.node_mut(el));
assert!(!list.supports("foo"));
assert!(!list.supports("anything"));
}
#[test]
fn len_and_is_empty_reflect_current_state() {
let (mut dom, el) = element_with_classes(&[]);
let mut list = DomTokenListMut::new(dom.node_mut(el));
assert!(list.is_empty());
assert_eq!(list.len(), 0);
list.add("a").unwrap();
list.add("b").unwrap();
assert!(!list.is_empty());
assert_eq!(list.len(), 2);
}
}