handle detached elements

This commit is contained in:
sjorsdonkers
2025-05-16 11:10:54 +02:00
committed by Sjors
parent 333c377bc7
commit 216f6cc8e8
8 changed files with 104 additions and 27 deletions

View File

@@ -365,14 +365,28 @@ pub const Element = struct {
return Node.replaceChildren(parser.elementToNode(self), nodes);
}
// A DOMRect object providing information about the size of an element and its position relative to the viewport.
// Returns a 0 DOMRect object if the element is eventually detached from the main window
pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect {
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
if (root != parser.documentToNode(parser.documentHTMLToDocument(state.document.?))) {
return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 };
}
return state.renderer.getRect(self);
}
// returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
// We do not render so just always return the element's rect.
pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![1]DOMRect {
return [_]DOMRect{try state.renderer.getRect(self)};
// Returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
// We do not render so it only always return the element's bounding rect.
// Returns an empty array if the element is eventually detached from the main window
pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![]DOMRect {
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
if (root != parser.documentToNode(parser.documentHTMLToDocument(state.document.?))) {
return &.{};
}
const heap_ptr = try state.arena.create(DOMRect);
heap_ptr.* = try state.renderer.getRect(self);
return heap_ptr[0..1];
}
pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 {
@@ -568,6 +582,26 @@ test "Browser.DOM.Element" {
.{ "document.getElementById('para').clientWidth", "2" },
.{ "document.getElementById('para').clientHeight", "1" },
.{ "let r4 = document.createElement('div').getBoundingClientRect()", null },
.{ "r4.x", "0" },
.{ "r4.y", "0" },
.{ "r4.width", "0" },
.{ "r4.height", "0" },
// Test setup causes WrongDocument or HierarchyRequest error unlike in chrome/firefox
// .{ // An element of another document, even if created from the main document, is not rendered.
// \\ let div5 = document.createElement('div');
// \\ const newDoc = document.implementation.createHTMLDocument("New Document");
// \\ newDoc.body.appendChild(div5);
// \\ let r5 = div5.getBoundingClientRect();
// ,
// null,
// },
// .{ "r5.x", "0" },
// .{ "r5.y", "0" },
// .{ "r5.width", "0" },
// .{ "r5.height", "0" },
}, .{});
try runner.testCases(&.{

View File

@@ -121,7 +121,7 @@ pub const IntersectionObserverEntry = struct {
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
return self.state.renderer.getRect(self.target);
return Element._getBoundingClientRect(self.target, self.state);
}
// Returns the ratio of the intersectionRect to the boundingClientRect.
@@ -131,7 +131,7 @@ pub const IntersectionObserverEntry = struct {
// Returns a DOMRectReadOnly representing the target's visible area.
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
return self.state.renderer.getRect(self.target);
return Element._getBoundingClientRect(self.target, self.state);
}
// A Boolean value which is true if the target element intersects with the intersection observer's root. If this is true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, then you know the transition is from intersecting to not-intersecting.
@@ -158,7 +158,7 @@ pub const IntersectionObserverEntry = struct {
else => return error.InvalidState,
}
return try self.state.renderer.getRect(element);
return Element._getBoundingClientRect(element, self.state);
}
// The Element whose intersection with the root changed.
@@ -244,7 +244,9 @@ test "Browser.DOM.IntersectionObserver" {
// Entry
try runner.testCases(&.{
.{ "let entry;", "undefined" },
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(document.createElement('div'));", "undefined" },
.{ "let div1 = document.createElement('div')", null },
.{ "document.body.appendChild(div1);", null },
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);", null },
.{ "entry.boundingClientRect.x;", "0" },
.{ "entry.intersectionRatio;", "1" },
.{ "entry.intersectionRect.x;", "0" },
@@ -261,7 +263,8 @@ test "Browser.DOM.IntersectionObserver" {
// Options
try runner.testCases(&.{
.{ "const new_root = document.createElement('span');", "undefined" },
.{ "const new_root = document.createElement('span');", null },
.{ "document.body.appendChild(new_root);", null },
.{ "let new_entry;", "undefined" },
.{
\\ const new_observer = new IntersectionObserver(

View File

@@ -41,6 +41,8 @@ const Walker = @import("walker.zig").WalkerDepthFirst;
const HTML = @import("../html/html.zig");
const HTMLElem = @import("../html/elements.zig");
const log = std.log.scoped(.node);
// Node interfaces
pub const Interfaces = .{
Attr,
@@ -262,13 +264,15 @@ pub const Node = struct {
return try parser.nodeContains(self, other);
}
pub fn _getRootNode(self: *parser.Node) !?HTMLElem.Union {
// TODO return thiss shadow-including root if options["composed"] is true
const res = try parser.nodeOwnerDocument(self);
if (res == null) {
return null;
}
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
// Returns itself or ancestor object inheriting from Node.
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
// - An Element inside a shadow DOM will return the associated ShadowRoot.
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
if (options) |options_| if (options_.composed) {
log.warn("getRootNode composed is not implemented yet", .{});
};
return try Node.toInterface(try parser.nodeGetRootNode(self));
}
pub fn _hasChildNodes(self: *parser.Node) !bool {

View File

@@ -321,9 +321,15 @@ test "Browser.HTML.Document" {
}, .{});
try runner.testCases(&.{
.{ "document.elementFromPoint(0.5, 0.5)", "null" },
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Should these be document?
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
.{ "document.createElement('div').getClientRects()", null },
.{
\\ let div1 = document.createElement('div');
\\ document.body.appendChild(div1);
\\ div1.getClientRects();
,
null,
},
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
.{ "elems.length", "1" },
@@ -331,9 +337,14 @@ test "Browser.HTML.Document" {
}, .{});
try runner.testCases(&.{
.{ "let a = document.createElement('a')", null },
.{ "a.href = \"https://lightpanda.io\"", null },
.{ "a.getClientRects()", null }, // Note this will be placed after the div of previous test
.{
\\ let a = document.createElement('a');
\\ a.href = "https://lightpanda.io";
\\ document.body.appendChild(a);
\\ a.getClientRects();
, // Note this will be placed after the div of previous test
null,
},
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
.{ "a_again", "[object HTMLAnchorElement]" },
.{ "a_again.href", "https://lightpanda.io" },

View File

@@ -307,8 +307,20 @@ test "Browser.HTML.Window" {
try runner.testCases(&.{
.{ "innerHeight", "1" },
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
.{ "document.createElement('div').getClientRects()", null },
.{ "document.createElement('div').getClientRects()", null },
.{
\\ let div1 = document.createElement('div');
\\ document.body.appendChild(div1);
\\ div1.getClientRects();
,
null,
},
.{
\\ let div2 = document.createElement('div');
\\ document.body.appendChild(div2);
\\ div2.getClientRects();
,
null,
},
.{ "innerHeight", "1" },
.{ "innerWidth", "2" },
}, .{});

View File

@@ -1151,6 +1151,17 @@ pub fn nodeGetChildNodes(node: *Node) !*NodeList {
return nlist.?;
}
pub fn nodeGetRootNode(node: *Node) !*Node {
var root = node;
while (true) {
const parent = try nodeParentNode(root);
if (parent) |parent_| {
root = parent_;
} else break;
}
return root;
}
pub fn nodeAppendChild(node: *Node, child: *Node) !*Node {
var res: ?*Node = undefined;
const err = nodeVtable(node).dom_node_append_child.?(node, child, &res);

View File

@@ -50,6 +50,8 @@ const FlatRenderer = struct {
};
}
// The DOMRect is always relative to the viewport, not the document the element belongs to.
// Element that are not part of the main document, either detached or in a shadow DOM should not call this function.
pub fn getRect(self: *FlatRenderer, e: *parser.Element) !Element.DOMRect {
var elements = &self.elements;
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));

View File

@@ -22,7 +22,7 @@ const Node = @import("../Node.zig");
const css = @import("../../browser/dom/css.zig");
const parser = @import("../../browser/netsurf.zig");
const dom_node = @import("../../browser/dom/node.zig");
const DOMRect = @import("../../browser/dom/element.zig").Element.DOMRect;
const Element = @import("../../browser/dom/element.zig").Element;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -253,7 +253,7 @@ fn describeNode(cmd: anytype) !void {
// We are assuming the start/endpoint is not repeated.
const Quad = [8]f64;
fn rectToQuad(rect: DOMRect) Quad {
fn rectToQuad(rect: Element.DOMRect) Quad {
return Quad{
rect.x,
rect.y,
@@ -271,7 +271,7 @@ fn scrollIntoViewIfNeeded(cmd: anytype) !void {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
objectId: ?[]const u8 = null,
rect: ?DOMRect = null,
rect: ?Element.DOMRect = null,
})) orelse return error.InvalidParams;
// Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null
@@ -327,7 +327,7 @@ fn getContentQuads(cmd: anytype) !void {
// Elements like SVGElement may have multiple quads.
const element = parser.nodeToElement(node._node);
const rect = try bc.session.page.?.state.renderer.getRect(element);
const rect = try Element._getBoundingClientRect(element, &bc.session.page.?.state);
const quad = rectToQuad(rect);
return cmd.sendResult(.{ .quads = &.{quad} }, .{});