mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 17:55:59 -04:00
Merge pull request #2744 from lightpanda-io/document_frame
webapi: Use document._frame rather than executing frame
This commit is contained in:
@@ -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, .{});
|
||||
|
||||
@@ -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;
|
||||
|
||||
33
src/browser/tests/frames/cross_realm_focus.html
Normal file
33
src/browser/tests/frames/cross_realm_focus.html
Normal 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>
|
||||
73
src/browser/tests/frames/cross_realm_identity.html
Normal file
73
src/browser/tests/frames/cross_realm_identity.html
Normal 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>
|
||||
6
src/browser/tests/frames/support/cross_realm_focus.html
Normal file
6
src/browser/tests/frames/support/cross_realm_focus.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<head></head>
|
||||
<body>
|
||||
<input id="fin">
|
||||
<input id="fin2">
|
||||
</body>
|
||||
13
src/browser/tests/frames/support/cross_realm_identity.html
Normal file
13
src/browser/tests/frames/support/cross_realm_identity.html
Normal 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>
|
||||
62
src/browser/tests/shadowroot/form_association_scoping.html
Normal file
62
src/browser/tests/shadowroot/form_association_scoping.html
Normal 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>
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
}
|
||||
|
||||
@@ -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, .{});
|
||||
}
|
||||
|
||||
@@ -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, .{});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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..]);
|
||||
|
||||
Reference in New Issue
Block a user