Merge pull request #2744 from lightpanda-io/document_frame

webapi: Use document._frame rather than executing frame
This commit is contained in:
Karl Seguin
2026-06-17 16:44:15 +08:00
committed by GitHub
24 changed files with 285 additions and 79 deletions

View File

@@ -637,7 +637,7 @@ const ActivationState = struct {
// Walk from the root of the tree containing this element
// This handles both document-attached and orphaned elements
const root = elem.asNode().getRootNode(null);
const root = elem.asNode().getRootNode(.{});
const TreeWalker = @import("webapi/TreeWalker.zig");
var walker = TreeWalker.Full.init(root, .{});

View File

@@ -1684,24 +1684,17 @@ pub fn removeElementIdWithMaps(self: *Frame, id_maps: ElementIdMaps, id: []const
}
pub fn getElementByIdFromNode(self: *Frame, node: *Node, id: []const u8) ?*Element {
if (node.isConnected() or node.isInShadowTree()) {
var current = node;
while (true) {
if (current.is(ShadowRoot)) |shadow_root| {
return shadow_root.getElementById(id, self);
}
const parent = current._parent orelse {
if (current._type == .document) {
return current._type.document.getElementById(id, self);
}
if (IS_DEBUG) {
std.debug.assert(false);
}
return null;
};
current = parent;
}
// The id map lives on the node's root: a Document, or a ShadowRoot for
// shadow DOM. Walk to the root once and consult the matching map.
const root = node.getRootNode(.{});
if (root._type == .document) {
return root._type.document.getElementById(id, self);
}
if (root.is(ShadowRoot)) |shadow_root| {
return shadow_root.getElementById(id, self);
}
// Detached subtree (root is neither a Document nor a ShadowRoot): no id map
// exists, so scan it.
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{});
while (tw.next()) |el| {
const element_id = el.getAttributeSafe(comptime .wrap("id")) orelse continue;

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<head></head>
<body>
<script src="../testing.js"></script>
<!--
focus()/blur() and the :focus selector act on the element's own document. Parent
JS focusing an element inside an iframe must update the iframe document's
activeElement, not the parent's. Open in Firefox to cross-check.
(Firefox additionally sets the *parent* document.activeElement to the <iframe>
element; we don't model that, so this file deliberately doesn't assert it.)
-->
<iframe id="idf" src="support/cross_realm_focus.html"></iframe>
<script id="focus_targets_own_document">
testing.onload(() => {
const idoc = document.getElementById('idf').contentDocument;
const inp = idoc.getElementById('fin');
inp.focus();
// focus() updates the iframe document's activeElement, and :focus matches
// against that document — not the parent's.
testing.expectEqual(inp, idoc.activeElement);
testing.expectEqual(inp, idoc.querySelector('input:focus'));
inp.blur();
// blur() clears it; activeElement falls back to the iframe body.
testing.expectFalse(idoc.activeElement === inp);
testing.expectEqual(idoc.body, idoc.activeElement);
});
</script>
</body>

View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<head></head>
<body>
<script src="../testing.js"></script>
<!--
Document-identity, association and URL-resolution APIs must reflect the frame
the object belongs to, not the realm doing the reading. Parent JS reaching into
iframe.contentDocument used to resolve some of these against the *parent's*
frame (wrong document URL / id map / base). These guard that regression.
Open this file in Firefox to cross-check: every assertion below holds in a
spec-compliant engine too.
-->
<iframe id="idf" src="support/cross_realm_identity.html"></iframe>
<script id="document_identity">
testing.onload(() => {
const f = document.getElementById('idf');
const idoc = f.contentDocument;
const iwin = f.contentWindow;
// URL / defaultView reflect the iframe's own document, not the parent.
testing.expectTrue(idoc.URL.endsWith('frames/support/cross_realm_identity.html'));
testing.expectFalse(idoc.URL === document.URL);
testing.expectEqual(iwin, idoc.defaultView);
testing.expectFalse(idoc.defaultView === window);
// Same host, so domain matches the parent's host (regression: must not read
// the parent frame's url, but the value coincides here).
testing.expectEqual(document.domain, idoc.domain);
});
</script>
<script id="form_and_label_associations">
testing.onload(() => {
const idoc = document.getElementById('idf').contentDocument;
const input = idoc.getElementById('i1');
const form = idoc.getElementById('f');
const label = idoc.getElementById('lbl');
// form= / for= resolve the id in the iframe's document, not the parent's.
testing.expectEqual(form, input.form);
testing.expectEqual(input, label.control);
});
</script>
<script id="named_collections">
testing.onload(() => {
const idoc = document.getElementById('idf').contentDocument;
const input = idoc.getElementById('i1');
// HTMLCollection named getter (by id) against the iframe document.
testing.expectEqual(input, idoc.getElementsByTagName('input').i1);
// document.all named getter against the iframe document.
testing.expectEqual(idoc.getElementById('f'), idoc.all.f);
});
</script>
<script id="url_resolution">
testing.onload(() => {
const idoc = document.getElementById('idf').contentDocument;
// URLs resolve against the iframe document's base (<base href="based/">),
// not the parent frame's.
testing.expectTrue(idoc.getElementById('f').action.endsWith('/support/based/act'));
testing.expectTrue(idoc.getElementById('btn1').formAction.endsWith('/support/based/bact'));
testing.expectTrue(idoc.getElementById('q1').cite.endsWith('/support/based/c'));
testing.expectTrue(idoc.getElementById('src1').src.endsWith('/support/based/s'));
// <base>.href resolves against the document address, not against base() itself.
testing.expectTrue(idoc.querySelector('base').href.endsWith('/support/based/'));
});
</script>
</body>

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<head></head>
<body>
<input id="fin">
<input id="fin2">
</body>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<head>
<!-- Shifts the document base; URL/action/cite/src resolution must use this. -->
<base href="based/">
</head>
<body>
<form id="f" action="act"></form>
<input id="i1" form="f" name="byname">
<label id="lbl" for="i1">label</label>
<q id="q1" cite="c">quote</q>
<input id="src1" src="s">
<button id="btn1" form="f" formaction="bact">b</button>
</body>

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body>
<!--
form= / for= resolve the id in the element's own tree (the shadow root), not the
document — even when the document has elements with the same id. This guards the
switch from a document-scoped lookup to the tree-scoped getElementByIdFromNode.
Open in Firefox to cross-check: tree-scoped association is spec behaviour.
-->
<!-- Document-tree decoys sharing the shadow ids. -->
<form id="f"></form>
<input id="i" form="f">
<form id="ff"></form>
<script id="association_resolves_inside_shadow_tree">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<form id="f"></form>' +
'<input id="i" form="f">' +
'<label for="i">L</label>';
document.body.appendChild(host);
const shadowForm = shadow.getElementById('f');
const shadowInput = shadow.getElementById('i');
const shadowLabel = shadow.querySelector('label');
// form= / for= resolve to the shadow-tree elements, not the document ones.
testing.expectEqual(shadowForm, shadowInput.form);
testing.expectFalse(shadowInput.form === document.getElementById('f'));
testing.expectEqual(shadowInput, shadowLabel.control);
host.remove();
}
</script>
<script id="association_does_not_cross_shadow_boundary">
{
// A shadow control whose form= has no match inside the shadow tree must NOT
// escape to a document form with that id — the owner is null.
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<input id="i2" form="ff">';
document.body.appendChild(host);
const shadowInput = shadow.getElementById('i2');
testing.expectEqual(null, shadowInput.form);
host.remove();
}
</script>
<script id="light_dom_association_unchanged">
{
// The document-tree control still resolves against the document tree.
testing.expectEqual(document.getElementById('f'), document.getElementById('i').form);
}
</script>
</body>

View File

@@ -132,9 +132,10 @@ pub fn getLocation(self: *const Document) ?*Location {
return doc_frame.window._location;
}
pub fn setLocation(self: *Document, url: [:0]const u8, frame: *Frame) !void {
pub fn setLocation(self: *Document, url: [:0]const u8) !void {
if (self._type != .html) return;
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._frame });
const frame = self._frame orelse return;
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = frame });
}
pub fn getContentType(self: *const Document) []const u8 {
@@ -145,8 +146,8 @@ pub fn getContentType(self: *const Document) []const u8 {
};
}
pub fn getDomain(_: *const Document, frame: *const Frame) []const u8 {
return URL.getHostname(frame.url);
pub fn getDomain(self: *const Document, frame: *const Frame) []const u8 {
return URL.getHostname((self._frame orelse frame).url);
}
const CreateElementOptions = struct {
@@ -1227,7 +1228,8 @@ pub const JsApi = struct {
pub const hidden = bridge.property(false, .{ .template = false, .readonly = true });
pub const visibilityState = bridge.property("visible", .{ .template = false, .readonly = true });
pub const defaultView = bridge.accessor(struct {
fn defaultView(_: *const Document, frame: *Frame) *@import("Window.zig") {
fn defaultView(self: *const Document) ?*@import("Window.zig") {
const frame = self._frame orelse return null;
return frame.window;
}
}.defaultView, null, .{});

View File

@@ -990,8 +990,9 @@ pub fn focus(self: *Element, frame: *Frame) !void {
const FocusEvent = @import("event/FocusEvent.zig");
const new_target = self.asEventTarget();
const old_active = frame.document._active_element;
frame.document._active_element = self;
const doc = self.asNode().ownerDocument(frame) orelse frame.document;
const old_active = doc._active_element;
doc._active_element = self;
if (old_active) |old| {
if (old == self) {
@@ -1021,9 +1022,10 @@ pub fn focus(self: *Element, frame: *Frame) !void {
}
pub fn blur(self: *Element, frame: *Frame) !void {
if (frame.document._active_element != self) return;
const doc = self.asNode().ownerDocument(frame) orelse frame.document;
if (doc._active_element != self) return;
frame.document._active_element = null;
doc._active_element = null;
const FocusEvent = @import("event/FocusEvent.zig");
const old_target = self.asEventTarget();

View File

@@ -470,9 +470,7 @@ pub fn isConnected(self: *const Node) bool {
const GetRootNodeOpts = struct {
composed: bool = false,
};
pub fn getRootNode(self: *Node, opts_: ?GetRootNodeOpts) *Node {
const opts = opts_ orelse GetRootNodeOpts{};
pub fn getRootNode(self: *Node, opts: GetRootNodeOpts) *Node {
var root = self;
while (root._parent) |parent| {
root = parent;
@@ -1229,16 +1227,29 @@ pub const JsApi = struct {
pub const normalize = bridge.function(Node.normalize, .{ .ce_reactions = true });
pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true, .ce_reactions = true });
pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{});
pub const getRootNode = bridge.function(Node.getRootNode, .{});
pub const getRootNode = bridge.function(_getRootNode, .{});
// The `options` argument is optional in JS; default it before calling the
// (non-optional) Node.getRootNode.
fn _getRootNode(self: *Node, opts: ?GetRootNodeOpts) *Node {
return self.getRootNode(opts orelse .{});
}
pub const isEqualNode = bridge.function(Node.isEqualNode, .{});
pub const lookupNamespaceURI = bridge.function(Node.lookupNamespaceURI, .{});
pub const lookupPrefix = bridge.function(Node.lookupPrefix, .{});
pub const isDefaultNamespace = bridge.function(Node.isDefaultNamespace, .{});
fn _baseURI(_: *Node, frame: *const Frame) []const u8 {
return frame.base();
}
pub const baseURI = bridge.accessor(_baseURI, null, .{});
fn _baseURI(self: *Node, frame: *const Frame) []const u8 {
const doc = if (self._type == .document)
self._type.document
else
self.ownerDocument(frame) orelse return frame.base();
if (doc._frame) |doc_frame| {
return doc_frame.base();
}
return doc.getURL(frame);
}
};
pub const Build = struct {

View File

@@ -60,8 +60,8 @@ pub fn setStart(self: *Range, node: *Node, offset: u32) !void {
self._proto._start_offset = offset;
// If start is now after end, or nodes are in different trees, collapse to start
const end_root = self._proto._end_container.getRootNode(null);
const start_root = node.getRootNode(null);
const end_root = self._proto._end_container.getRootNode(.{});
const start_root = node.getRootNode(.{});
if (end_root != start_root or self._proto.isStartAfterEnd()) {
self._proto._end_container = self._proto._start_container;
self._proto._end_offset = self._proto._start_offset;
@@ -82,8 +82,8 @@ pub fn setEnd(self: *Range, node: *Node, offset: u32) !void {
self._proto._end_offset = offset;
// If end is now before start, or nodes are in different trees, collapse to end
const start_root = self._proto._start_container.getRootNode(null);
const end_root = node.getRootNode(null);
const start_root = self._proto._start_container.getRootNode(.{});
const end_root = node.getRootNode(.{});
if (start_root != end_root or self._proto.isStartAfterEnd()) {
self._proto._start_container = self._proto._end_container;
self._proto._start_offset = self._proto._end_offset;
@@ -154,8 +154,8 @@ pub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *co
}
// If the two ranges' root is different, throw WrongDocumentError
const this_root = self._proto._start_container.getRootNode(null);
const source_root = source_range._proto._start_container.getRootNode(null);
const this_root = self._proto._start_container.getRootNode(.{});
const source_root = source_range._proto._start_container.getRootNode(.{});
if (this_root != source_root) {
return error.WrongDocument;
}
@@ -198,8 +198,8 @@ pub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *co
pub fn comparePoint(self: *const Range, node: *Node, offset: u32) !i16 {
// Check if node is in a different tree than the range
const node_root = node.getRootNode(null);
const start_root = self._proto._start_container.getRootNode(null);
const node_root = node.getRootNode(.{});
const start_root = self._proto._start_container.getRootNode(.{});
if (node_root != start_root) {
return error.WrongDocument;
}
@@ -236,8 +236,8 @@ pub fn comparePoint(self: *const Range, node: *Node, offset: u32) !i16 {
pub fn isPointInRange(self: *const Range, node: *Node, offset: u32) !bool {
// If node's root is different from the context object's root, return false
const node_root = node.getRootNode(null);
const start_root = self._proto._start_container.getRootNode(null);
const node_root = node.getRootNode(.{});
const start_root = self._proto._start_container.getRootNode(.{});
if (node_root != start_root) {
return false;
}
@@ -275,8 +275,8 @@ pub fn isPointInRange(self: *const Range, node: *Node, offset: u32) !bool {
pub fn intersectsNode(self: *const Range, node: *Node) bool {
// If node's root is different from the context object's root, return false
const node_root = node.getRootNode(null);
const start_root = self._proto._start_container.getRootNode(null);
const node_root = node.getRootNode(.{});
const start_root = self._proto._start_container.getRootNode(.{});
if (node_root != start_root) {
return false;
}

View File

@@ -100,8 +100,7 @@ pub fn getAtIndex(self: *HTMLAllCollection, index: usize, frame: *const Frame) ?
}
pub fn getByName(self: *HTMLAllCollection, name: []const u8, frame: *Frame) ?*Element {
// First, try fast ID lookup using the document's element map
if (frame.document._elements_by_id.get(name)) |el| {
if (frame.getElementByIdFromNode(self._tw._root, name)) |el| {
return el;
}

View File

@@ -179,7 +179,7 @@ pub fn NodeLive(comptime mode: Mode) type {
}
pub fn getByName(self: *Self, name: []const u8, frame: *Frame) ?*Element {
if (frame.document.getElementById(name, frame)) |element| {
if (frame.getElementByIdFromNode(self._tw._root, name)) |element| {
const node = element.asNode();
if (self._tw.contains(node) and self.matches(node)) {
return element;

View File

@@ -31,7 +31,8 @@ pub fn getHref(self: *Base, frame: *Frame) ![]const u8 {
if (href.len == 0) {
return "";
}
return URL.resolve(frame.call_arena, frame.url, href, .{});
const owner = element.asConstNode().ownerFrame(frame);
return URL.resolve(frame.call_arena, owner.url, href, .{});
}
pub fn setHref(self: *Base, value: []const u8, frame: *Frame) !void {

View File

@@ -100,7 +100,9 @@ pub fn getForm(self: *Button, frame: *Frame) ?*Form {
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (frame.document.getElementById(form_id, frame)) |form_element| {
// form= resolves in the control's own tree (shadow root or document),
// not the calling realm's. getElementByIdFromNode walks to that root.
if (frame.getElementByIdFromNode(element.asNode(), form_id)) |form_element| {
return form_element.is(Form);
}
// form attribute present but invalid - no form owner
@@ -135,9 +137,10 @@ pub fn getLabels(self: *Button, frame: *Frame) !js.Array {
pub fn getFormAction(self: *Button, frame: *Frame) ![]const u8 {
const element = self.asElement();
const action = element.getAttributeSafe(comptime .wrap("formaction")) orelse return frame.url;
const owner_url = element.asNode().ownerFrame(frame).url;
const action = element.getAttributeSafe(comptime .wrap("formaction")) orelse return owner_url;
if (action.len == 0) {
return frame.url;
return owner_url;
}
return element.asNode().resolveURL(action, frame, .{});
}

View File

@@ -106,7 +106,7 @@ pub fn getElements(self: *Form, frame: *Frame) !*collections.HTMLFormControlsCol
pub fn iterator(self: *Form, frame: *Frame) collections.NodeLive(.form) {
const form_id = self.asElement().getAttributeSafe(comptime .wrap("id"));
const root = if (form_id != null)
self.asNode().getRootNode(null) // Has ID: walk entire document to find form=ID controls
self.asNode().getRootNode(.{}) // Has ID: walk entire document to find form=ID controls
else
self.asNode(); // No ID: walk only form subtree (no external controls possible)
@@ -115,9 +115,10 @@ pub fn iterator(self: *Form, frame: *Frame) collections.NodeLive(.form) {
pub fn getAction(self: *Form, frame: *Frame) ![]const u8 {
const element = self.asElement();
const action = element.getAttributeSafe(comptime .wrap("action")) orelse return frame.url;
const owner_url = element.asNode().ownerFrame(frame).url;
const action = element.getAttributeSafe(comptime .wrap("action")) orelse return owner_url;
if (action.len == 0) {
return frame.url;
return owner_url;
}
return element.asNode().resolveURL(action, frame, .{});
}

View File

@@ -305,7 +305,7 @@ pub fn getValueForJS(self: *const Input, frame: *Frame) ![]const u8 {
return try std.fmt.allocPrint(frame.call_arena, "C:\\fakepath\\{s}", .{fl._files[0]._name});
}
pub fn getValidationMessage(self: *const Input, frame: *Frame) []const u8 {
pub fn getValidationMessage(self: *Input, frame: *Frame) []const u8 {
if (!self.getWillValidate()) return "";
if (self._custom_validity) |msg| return msg;
if (self.suffersValueMissing(frame)) return "Please fill out this field.";
@@ -349,7 +349,7 @@ pub fn hasCustomValidity(self: *const Input) bool {
return self._custom_validity != null;
}
pub fn suffersValueMissing(self: *const Input, frame: *Frame) bool {
pub fn suffersValueMissing(self: *Input, frame: *Frame) bool {
if (!self.getWillValidate()) return false;
if (!self.getRequired()) return false;
return switch (self._input_type) {
@@ -462,7 +462,7 @@ fn numericRangeBreach(self: *const Input, comptime kind: enum { underflow, overf
};
}
fn radioGroupHasChecked(self: *const Input, frame: *Frame) bool {
fn radioGroupHasChecked(self: *Input, frame: *Frame) bool {
if (self._checked) return true;
var iter = self.radioGroupIterator() orelse return false;
const my_form = self.getForm(frame);
@@ -501,14 +501,14 @@ fn radioGroupIterator(self: *const Input) ?RadioGroupIterator {
const element = self.asConstElement();
const name = element.getAttributeSafe(comptime .wrap("name")) orelse return null;
if (name.len == 0) return null;
const root = @constCast(element.asConstNode()).getRootNode(null);
const root = @constCast(element.asConstNode()).getRootNode(.{});
return .{
.walker = TreeWalker.Full.init(root, .{}),
.name = name,
};
}
fn sameFormOwner(self_form: ?*const Form, other: *const Input, frame: *Frame) bool {
fn sameFormOwner(self_form: ?*Form, other: *Input, frame: *Frame) bool {
const other_form = other.getForm(frame);
// Check if same form context
@@ -659,8 +659,7 @@ pub fn setSize(self: *Input, size: i32, frame: *Frame) !void {
pub fn getSrc(self: *const Input, frame: *Frame) ![]const u8 {
const src = self.asConstElement().getAttributeSafe(comptime .wrap("src")) orelse return "";
// If attribute is explicitly set (even if empty), resolve it against the base URL
return @import("../../URL.zig").resolve(frame.call_arena, frame.base(), src, .{});
return self.asConstElement().asConstNode().resolveURL(src, frame, .{});
}
pub fn setSrc(self: *Input, src: []const u8, frame: *Frame) !void {
@@ -890,12 +889,16 @@ pub fn getLabels(self: *Input, frame: *Frame) !js.Array {
return @import("Label.zig").getControlLabels(self.asElement(), frame);
}
pub fn getForm(self: *const Input, frame: *Frame) ?*Form {
const element = self.asConstElement();
pub fn getForm(self: *Input, frame: *Frame) ?*Form {
const element = self.asElement();
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (frame.document.getElementById(form_id, frame)) |form_element| {
// form= resolves in the control's own tree (shadow root or document),
// not the calling realm's. @constCast: getElementByIdFromNode wants a
// mutable *Node but doesn't mutate it (same idiom as radioGroupIterator);
// keeping getForm const avoids a wide validity-path const cascade.
if (frame.getElementByIdFromNode(element.asNode(), form_id)) |form_element| {
return form_element.is(Form);
}
// form attribute present but invalid - no form owner
@@ -920,9 +923,10 @@ pub fn getForm(self: *const Input, frame: *Frame) ?*Form {
pub fn getFormAction(self: *Input, frame: *Frame) ![]const u8 {
const element = self.asElement();
const action = element.getAttributeSafe(comptime .wrap("formaction")) orelse return frame.url;
const owner_url = element.asNode().ownerFrame(frame).url;
const action = element.getAttributeSafe(comptime .wrap("formaction")) orelse return owner_url;
if (action.len == 0) {
return frame.url;
return owner_url;
}
return element.asNode().resolveURL(action, frame, .{});
}

View File

@@ -27,7 +27,7 @@ pub fn setHtmlFor(self: *Label, value: []const u8, frame: *Frame) !void {
pub fn getControl(self: *Label, frame: *Frame) ?*Element {
if (self.asElement().getAttributeSafe(comptime .wrap("for"))) |id| {
const el = frame.document.getElementById(id, frame) orelse return null;
const el = frame.getElementByIdFromNode(self.asElement().asNode(), id) orelse return null;
if (!isLabelable(el)) {
return null;
}

View File

@@ -24,8 +24,7 @@ pub fn asNode(self: *Quote) *Node {
pub fn getCite(self: *Quote, frame: *Frame) ![]const u8 {
const attr = self.asElement().getAttributeSafe(comptime .wrap("cite")) orelse return "";
if (attr.len == 0) return "";
const URL = @import("../../URL.zig");
return URL.resolve(frame.call_arena, frame.base(), attr, .{});
return self.asNode().resolveURL(attr, frame, .{});
}
pub fn setCite(self: *Quote, value: []const u8, frame: *Frame) !void {

View File

@@ -231,7 +231,7 @@ pub fn getForm(self: *Select, frame: *Frame) ?*Form {
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (frame.document.getElementById(form_id, frame)) |form_element| {
if (frame.getElementByIdFromNode(element.asNode(), form_id)) |form_element| {
return form_element.is(Form);
}
// form attribute present but invalid - no form owner

View File

@@ -95,7 +95,8 @@ pub fn getSheet(self: *Style, frame: *Frame) !?*CSSStyleSheet {
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), frame);
self._sheet = sheet;
const sheets = try frame.document.getStyleSheets(frame);
const owner = self.asNode().ownerDocument(frame) orelse frame.document;
const sheets = try owner.getStyleSheets(frame);
try sheets.add(sheet, frame);
return sheet;

View File

@@ -292,7 +292,7 @@ pub fn getForm(self: *TextArea, frame: *Frame) ?*Form {
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (frame.document.getElementById(form_id, frame)) |form_element| {
if (frame.getElementByIdFromNode(element.asNode(), form_id)) |form_element| {
return form_element.is(Form);
}
// form attribute present but invalid - no form owner

View File

@@ -193,7 +193,7 @@ pub fn removeFromOpen(el: *Element, frame: *Frame) void {
// the explicitly IDL-set element or the `popovertarget` IDREF content attribute.
pub fn invokerTarget(invoker: *Node, explicit: ?*Element, frame: *Frame) ?*Element {
if (explicit) |target| {
if (target.asNode().getRootNode(null) == invoker.getRootNode(null)) {
if (target.asNode().getRootNode(.{}) == invoker.getRootNode(.{})) {
// The invoker and target must share the same root
return target;
}
@@ -201,7 +201,7 @@ pub fn invokerTarget(invoker: *Node, explicit: ?*Element, frame: *Frame) ?*Eleme
}
const el = invoker.is(Element) orelse return null;
const id = el.getAttributeSafe(.wrap("popovertarget")) orelse return null;
return frame.document.getElementById(id, frame);
return frame.getElementByIdFromNode(el.asNode(), id);
}
pub fn runInvokerActivation(invoker: *HtmlElement, explicit: ?*Element, frame: *Frame) !void {

View File

@@ -601,11 +601,13 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *N
.hover => return false,
.active => return false,
.focus => {
const active = frame.document._active_element orelse return false;
const doc = node.ownerDocument(frame) orelse return false;
const active = doc._active_element orelse return false;
return active == el;
},
.focus_within => {
const active = frame.document._active_element orelse return false;
const doc = node.ownerDocument(frame) orelse return false;
const active = doc._active_element orelse return false;
return node.contains(active.asNode());
},
.focus_visible => return false,
@@ -619,7 +621,8 @@ fn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *N
},
.target => {
const element_id = el.getAttributeSafe(comptime .wrap("id")) orelse return false;
const location = frame.document.getLocation() orelse return false;
const doc = node.ownerDocument(frame) orelse return false;
const location = doc.getLocation() orelse return false;
const hash = location.getHash();
if (hash.len <= 1) return false;
return std.mem.eql(u8, element_id, hash[1..]);