use crate::dom::Dom;
use crate::node_id::NodeId;
fn is_disableable_control(tag: &str) -> bool {
matches!(tag, "button" | "input" | "select" | "textarea" | "fieldset")
}
impl<Ext> Dom<Ext> {
pub fn is_actually_disabled(&self, id: NodeId) -> bool {
let Some(tag) = self.get_node(id).and_then(|n| n.tag_name()) else {
return false;
};
match tag {
"option" => {
self.has_attribute(id, "disabled")
|| self.parent_element_id(id).is_some_and(|p| {
self.tag_is(p, "optgroup") && self.has_attribute(p, "disabled")
})
}
"optgroup" => self.has_attribute(id, "disabled"),
t if is_disableable_control(t) => {
self.has_attribute(id, "disabled") || self.in_disabled_fieldset(id)
}
_ => false,
}
}
pub fn is_enabled_control(&self, id: NodeId) -> bool {
let Some(tag) = self.get_node(id).and_then(|n| n.tag_name()) else {
return false;
};
(is_disableable_control(tag) || matches!(tag, "optgroup" | "option"))
&& !self.is_actually_disabled(id)
}
fn in_disabled_fieldset(&self, id: NodeId) -> bool {
let mut via = id;
let mut cur = self.parent_element_id(id);
while let Some(anc) = cur {
if self.tag_is(anc, "fieldset")
&& self.has_attribute(anc, "disabled")
&& self.first_legend_child(anc) != Some(via)
{
return true;
}
via = anc;
cur = self.parent_element_id(anc);
}
false
}
fn first_legend_child(&self, fieldset: NodeId) -> Option<NodeId> {
let mut c = self.get_node(fieldset)?.first_child;
while let Some(cid) = c {
let n = self.get_node(cid)?;
if n.tag_name() == Some("legend") {
return Some(cid);
}
c = n.next_sibling;
}
None
}
pub(crate) fn parent_element_id(&self, id: NodeId) -> Option<NodeId> {
self.get_node(id).and_then(|n| n.parent)
}
pub(crate) fn tag_is(&self, id: NodeId, tag: &str) -> bool {
self.get_node(id).and_then(|n| n.tag_name()) == Some(tag)
}
}
#[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
}
#[test]
fn own_disabled_attribute_disables_a_control_but_not_a_div() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let input = el(&mut dom, root, "input", &[("disabled", "")]);
let div = el(&mut dom, root, "div", &[("disabled", "")]);
let plain = el(&mut dom, root, "button", &[]);
assert!(dom.is_actually_disabled(input));
assert!(!dom.is_actually_disabled(div));
assert!(!dom.is_actually_disabled(plain));
}
#[test]
fn control_inside_disabled_fieldset_is_disabled() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
let div = el(&mut dom, fs, "div", &[]);
let input = el(&mut dom, div, "input", &[]);
let button = el(&mut dom, fs, "button", &[]);
let select = el(&mut dom, fs, "select", &[]);
let textarea = el(&mut dom, fs, "textarea", &[]);
for c in [input, button, select, textarea] {
assert!(dom.is_actually_disabled(c));
}
assert!(!dom.is_actually_disabled(div), "a div is never disabled");
}
#[test]
fn control_in_first_legend_stays_enabled_but_not_in_a_later_one() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
let legend = el(&mut dom, fs, "legend", &[]);
let in_legend = el(&mut dom, legend, "input", &[("type", "checkbox")]);
let legend2 = el(&mut dom, fs, "legend", &[]);
let in_legend2 = el(&mut dom, legend2, "input", &[]);
assert!(!dom.is_actually_disabled(in_legend));
assert!(dom.is_actually_disabled(in_legend2));
}
#[test]
fn nested_legend_that_is_not_a_child_is_no_exemption() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
let div = el(&mut dom, fs, "div", &[]);
let legend = el(&mut dom, div, "legend", &[]);
let input = el(&mut dom, legend, "input", &[]);
assert!(dom.is_actually_disabled(input));
}
#[test]
fn nested_fieldset_in_a_disabled_fieldset_disables_its_content() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let outer = el(&mut dom, root, "fieldset", &[("disabled", "")]);
let inner = el(&mut dom, outer, "fieldset", &[]);
let inner_legend = el(&mut dom, inner, "legend", &[]);
let in_inner_legend = el(&mut dom, inner_legend, "input", &[]);
let input = el(&mut dom, inner, "input", &[]);
assert!(dom.is_actually_disabled(outer));
assert!(dom.is_actually_disabled(inner));
assert!(dom.is_actually_disabled(input));
assert!(dom.is_actually_disabled(in_inner_legend));
}
#[test]
fn inner_disabled_fieldset_and_legend_exempt_fieldset() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let outer = el(&mut dom, root, "fieldset", &[]);
let inner = el(&mut dom, outer, "fieldset", &[("disabled", "")]);
let input = el(&mut dom, inner, "input", &[]);
assert!(!dom.is_actually_disabled(outer));
assert!(dom.is_actually_disabled(input));
let off = el(&mut dom, root, "fieldset", &[("disabled", "")]);
let legend = el(&mut dom, off, "legend", &[]);
let fs_in_legend = el(&mut dom, legend, "fieldset", &[]);
let deep = el(&mut dom, fs_in_legend, "input", &[]);
assert!(!dom.is_actually_disabled(fs_in_legend));
assert!(!dom.is_actually_disabled(deep));
}
#[test]
fn option_and_optgroup_rules() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
let select = el(&mut dom, fs, "select", &[]);
let og = el(&mut dom, select, "optgroup", &[("disabled", "")]);
let in_og = el(&mut dom, og, "option", &[]);
let own = el(&mut dom, select, "option", &[("disabled", "")]);
let plain = el(&mut dom, select, "option", &[]);
assert!(dom.is_actually_disabled(select));
assert!(dom.is_actually_disabled(og));
assert!(dom.is_actually_disabled(in_og));
assert!(dom.is_actually_disabled(own));
assert!(!dom.is_actually_disabled(plain));
}
#[test]
fn disabled_and_enabled_selectors_follow_the_predicate() {
let mut dom: Dom = Dom::new();
let root = dom.root();
let fs = el(&mut dom, root, "fieldset", &[("disabled", "")]);
let legend = el(&mut dom, fs, "legend", &[]);
let in_legend = el(&mut dom, legend, "input", &[]);
let input = el(&mut dom, fs, "input", &[]);
let div = el(&mut dom, root, "div", &[("disabled", "")]);
assert!(dom.matches(fs, ":disabled").unwrap());
assert!(dom.matches(input, ":disabled").unwrap());
assert!(dom.matches(input, "input:disabled").unwrap());
assert!(!dom.matches(input, ":enabled").unwrap());
assert!(!dom.matches(in_legend, ":disabled").unwrap());
assert!(dom.matches(in_legend, ":enabled").unwrap());
assert!(!dom.matches(div, ":disabled").unwrap());
assert!(!dom.matches(div, ":enabled").unwrap());
assert!(!dom.matches(legend, ":enabled").unwrap());
assert_eq!(
dom.query_selector_all_in(root, ":disabled").unwrap(),
vec![fs, input]
);
}
}