diff --git a/src/browser/tests/document/query_selector_all.html b/src/browser/tests/document/query_selector_all.html index aa56b285..c98de73c 100644 --- a/src/browser/tests/document/query_selector_all.html +++ b/src/browser/tests/document/query_selector_all.html @@ -376,3 +376,67 @@ } +
+ + + +
+ + +
+
+ + + + + + + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 5682b01d..ec533d9c 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -958,7 +958,7 @@ pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element { var current: ?*Element = self; while (current) |el| { - if (try el.matches(selector, page)) { + if (try Selector.matchesWithScope(el, selector, self, page)) { return el; } diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index ec2d90d3..3ed295c4 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -531,11 +531,45 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *N .enabled => { return el.getAttributeSafe(comptime .wrap("disabled")) == null; }, - .indeterminate => return false, + .indeterminate => { + const input = el.is(Node.Element.Html.Input) orelse return false; + return switch (input._input_type) { + .checkbox => input.getIndeterminate(), + else => false, + }; + }, // Form validation - .valid => return false, - .invalid => return false, + .valid => { + if (el.is(Node.Element.Html.Input)) |input| { + return switch (input._input_type) { + .hidden, .submit, .reset, .button => false, + else => !input.getRequired() or input.getValue().len > 0, + }; + } + if (el.is(Node.Element.Html.Select)) |select| { + return !select.getRequired() or select.getValue(page).len > 0; + } + if (el.is(Node.Element.Html.Form) != null or el.is(Node.Element.Html.FieldSet) != null) { + return !hasInvalidDescendant(node, page); + } + return false; + }, + .invalid => { + if (el.is(Node.Element.Html.Input)) |input| { + return switch (input._input_type) { + .hidden, .submit, .reset, .button => false, + else => input.getRequired() and input.getValue().len == 0, + }; + } + if (el.is(Node.Element.Html.Select)) |select| { + return select.getRequired() and select.getValue(page).len == 0; + } + if (el.is(Node.Element.Html.Form) != null or el.is(Node.Element.Html.FieldSet) != null) { + return hasInvalidDescendant(node, page); + } + return false; + }, .required => { return el.getAttributeSafe(comptime .wrap("required")) != null; }, @@ -684,6 +718,26 @@ fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, scope: * return false; } +fn hasInvalidDescendant(parent: *Node, page: *Page) bool { + var child = parent.firstChild(); + while (child) |c| { + if (c.is(Node.Element)) |child_el| { + if (child_el.is(Node.Element.Html.Input)) |input| { + const invalid = switch (input._input_type) { + .hidden, .submit, .reset, .button => false, + else => input.getRequired() and input.getValue().len == 0, + }; + if (invalid) return true; + } else if (child_el.is(Node.Element.Html.Select)) |select| { + if (select.getRequired() and select.getValue(page).len == 0) return true; + } + } + if (hasInvalidDescendant(c, page)) return true; + child = c.nextSibling(); + } + return false; +} + fn isFirstChild(el: *Node.Element) bool { const node = el.asNode(); var sibling = node.previousSibling(); diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 0ac9793a..4a236fc3 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -92,6 +92,24 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool { return false; } +// Like matches, but allows the caller to specify a scope node distinct from el. +// Used by closest() so that :scope always refers to the original context element. +pub fn matchesWithScope(el: *Node.Element, input: []const u8, scope: *Node.Element, page: *Page) !bool { + if (input.len == 0) { + return error.SyntaxError; + } + + const arena = page.call_arena; + const selectors = try Parser.parseList(arena, input, page); + + for (selectors) |selector| { + if (List.matches(el.asNode(), selector, scope.asNode(), page)) { + return true; + } + } + return false; +} + pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bool { if (class_name.len == 0 or class_name.len > class_attr.len) return false;