mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
More pseudo-seletors
:invalid, :valid and :indeterminate implementation Fix Element.closest with :scope pseudo-selector.
This commit is contained in:
@@ -376,3 +376,67 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<form id="form-validity-test">
|
||||||
|
<input id="vi-required-empty" type="text" required>
|
||||||
|
<input id="vi-optional" type="text">
|
||||||
|
<input id="vi-hidden-required" type="hidden" required>
|
||||||
|
<fieldset id="vi-fieldset">
|
||||||
|
<input id="vi-nested-required" type="text" required>
|
||||||
|
<select id="vi-select-required" required>
|
||||||
|
<option value="">Pick one</option>
|
||||||
|
<option value="a">A</option>
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<input id="vi-checkbox" type="checkbox">
|
||||||
|
|
||||||
|
<script id=invalidPseudo>
|
||||||
|
{
|
||||||
|
// Inputs with required + empty value are :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-required-empty').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-required-empty').matches(':valid'));
|
||||||
|
|
||||||
|
// Inputs without required are :valid
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-optional').matches(':invalid'));
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-optional').matches(':valid'));
|
||||||
|
|
||||||
|
// hidden inputs are not candidates for constraint validation
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-hidden-required').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-hidden-required').matches(':valid'));
|
||||||
|
|
||||||
|
// select with required and empty selected value is :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-select-required').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-select-required').matches(':valid'));
|
||||||
|
|
||||||
|
// fieldset containing invalid controls is :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('vi-fieldset').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('vi-fieldset').matches(':valid'));
|
||||||
|
|
||||||
|
// form containing invalid controls is :invalid
|
||||||
|
testing.expectEqual(true, document.getElementById('form-validity-test').matches(':invalid'));
|
||||||
|
testing.expectEqual(false, document.getElementById('form-validity-test').matches(':valid'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=validAfterValueSet>
|
||||||
|
{
|
||||||
|
// After setting a value, a required input becomes :valid
|
||||||
|
const input = document.getElementById('vi-required-empty');
|
||||||
|
input.value = 'hello';
|
||||||
|
testing.expectEqual(false, input.matches(':invalid'));
|
||||||
|
testing.expectEqual(true, input.matches(':valid'));
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=indeterminatePseudo>
|
||||||
|
{
|
||||||
|
const cb = document.getElementById('vi-checkbox');
|
||||||
|
testing.expectEqual(false, cb.matches(':indeterminate'));
|
||||||
|
cb.indeterminate = true;
|
||||||
|
testing.expectEqual(true, cb.matches(':indeterminate'));
|
||||||
|
cb.indeterminate = false;
|
||||||
|
testing.expectEqual(false, cb.matches(':indeterminate'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -952,7 +952,7 @@ pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element {
|
|||||||
|
|
||||||
var current: ?*Element = self;
|
var current: ?*Element = self;
|
||||||
while (current) |el| {
|
while (current) |el| {
|
||||||
if (try el.matches(selector, page)) {
|
if (try Selector.matchesWithScope(el, selector, self, page)) {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -531,11 +531,45 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *N
|
|||||||
.enabled => {
|
.enabled => {
|
||||||
return el.getAttributeSafe(comptime .wrap("disabled")) == null;
|
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
|
// Form validation
|
||||||
.valid => return false,
|
.valid => {
|
||||||
.invalid => return false,
|
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 => {
|
.required => {
|
||||||
return el.getAttributeSafe(comptime .wrap("required")) != null;
|
return el.getAttributeSafe(comptime .wrap("required")) != null;
|
||||||
},
|
},
|
||||||
@@ -684,6 +718,26 @@ fn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, scope: *
|
|||||||
return false;
|
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 {
|
fn isFirstChild(el: *Node.Element) bool {
|
||||||
const node = el.asNode();
|
const node = el.asNode();
|
||||||
var sibling = node.previousSibling();
|
var sibling = node.previousSibling();
|
||||||
|
|||||||
@@ -92,6 +92,24 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool {
|
|||||||
return false;
|
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 {
|
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;
|
if (class_name.len == 0 or class_name.len > class_attr.len) return false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user