Skip to content

Commit 4cf7000

Browse files
committed
handle detached elements
1 parent 6ac9bcd commit 4cf7000

File tree

8 files changed

+104
-27
lines changed

8 files changed

+104
-27
lines changed

src/browser/dom/element.zig

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,14 +365,28 @@ pub const Element = struct {
365365
return Node.replaceChildren(parser.elementToNode(self), nodes);
366366
}
367367

368+
// A DOMRect object providing information about the size of an element and its position relative to the viewport.
369+
// Returns a 0 DOMRect object if the element is eventually detached from the main window
368370
pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect {
371+
// 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.
372+
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
373+
if (root != parser.documentToNode(parser.documentHTMLToDocument(state.document.?))) {
374+
return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 };
375+
}
369376
return state.renderer.getRect(self);
370377
}
371378

372-
// returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
373-
// We do not render so just always return the element's rect.
374-
pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![1]DOMRect {
375-
return [_]DOMRect{try state.renderer.getRect(self)};
379+
// Returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
380+
// We do not render so it only always return the element's bounding rect.
381+
// Returns an empty array if the element is eventually detached from the main window
382+
pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![]DOMRect {
383+
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
384+
if (root != parser.documentToNode(parser.documentHTMLToDocument(state.document.?))) {
385+
return &.{};
386+
}
387+
const heap_ptr = try state.arena.create(DOMRect);
388+
heap_ptr.* = try state.renderer.getRect(self);
389+
return heap_ptr[0..1];
376390
}
377391

378392
pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 {
@@ -568,6 +582,26 @@ test "Browser.DOM.Element" {
568582

569583
.{ "document.getElementById('para').clientWidth", "2" },
570584
.{ "document.getElementById('para').clientHeight", "1" },
585+
586+
.{ "let r4 = document.createElement('div').getBoundingClientRect()", null },
587+
.{ "r4.x", "0" },
588+
.{ "r4.y", "0" },
589+
.{ "r4.width", "0" },
590+
.{ "r4.height", "0" },
591+
592+
// Test setup causes WrongDocument or HierarchyRequest error unlike in chrome/firefox
593+
// .{ // An element of another document, even if created from the main document, is not rendered.
594+
// \\ let div5 = document.createElement('div');
595+
// \\ const newDoc = document.implementation.createHTMLDocument("New Document");
596+
// \\ newDoc.body.appendChild(div5);
597+
// \\ let r5 = div5.getBoundingClientRect();
598+
// ,
599+
// null,
600+
// },
601+
// .{ "r5.x", "0" },
602+
// .{ "r5.y", "0" },
603+
// .{ "r5.width", "0" },
604+
// .{ "r5.height", "0" },
571605
}, .{});
572606

573607
try runner.testCases(&.{

src/browser/dom/intersection_observer.zig

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ pub const IntersectionObserverEntry = struct {
121121

122122
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
123123
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
124-
return self.state.renderer.getRect(self.target);
124+
return Element._getBoundingClientRect(self.target, self.state);
125125
}
126126

127127
// Returns the ratio of the intersectionRect to the boundingClientRect.
@@ -131,7 +131,7 @@ pub const IntersectionObserverEntry = struct {
131131

132132
// Returns a DOMRectReadOnly representing the target's visible area.
133133
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
134-
return self.state.renderer.getRect(self.target);
134+
return Element._getBoundingClientRect(self.target, self.state);
135135
}
136136

137137
// 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 {
158158
else => return error.InvalidState,
159159
}
160160

161-
return try self.state.renderer.getRect(element);
161+
return Element._getBoundingClientRect(element, self.state);
162162
}
163163

164164
// The Element whose intersection with the root changed.
@@ -244,7 +244,9 @@ test "Browser.DOM.IntersectionObserver" {
244244
// Entry
245245
try runner.testCases(&.{
246246
.{ "let entry;", "undefined" },
247-
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(document.createElement('div'));", "undefined" },
247+
.{ "let div1 = document.createElement('div')", null },
248+
.{ "document.body.appendChild(div1);", null },
249+
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);", null },
248250
.{ "entry.boundingClientRect.x;", "0" },
249251
.{ "entry.intersectionRatio;", "1" },
250252
.{ "entry.intersectionRect.x;", "0" },
@@ -261,7 +263,8 @@ test "Browser.DOM.IntersectionObserver" {
261263

262264
// Options
263265
try runner.testCases(&.{
264-
.{ "const new_root = document.createElement('span');", "undefined" },
266+
.{ "const new_root = document.createElement('span');", null },
267+
.{ "document.body.appendChild(new_root);", null },
265268
.{ "let new_entry;", "undefined" },
266269
.{
267270
\\ const new_observer = new IntersectionObserver(

src/browser/dom/node.zig

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const Walker = @import("walker.zig").WalkerDepthFirst;
4141
const HTML = @import("../html/html.zig");
4242
const HTMLElem = @import("../html/elements.zig");
4343

44+
const log = std.log.scoped(.node);
45+
4446
// Node interfaces
4547
pub const Interfaces = .{
4648
Attr,
@@ -262,13 +264,15 @@ pub const Node = struct {
262264
return try parser.nodeContains(self, other);
263265
}
264266

265-
pub fn _getRootNode(self: *parser.Node) !?HTMLElem.Union {
266-
// TODO return this’s shadow-including root if options["composed"] is true
267-
const res = try parser.nodeOwnerDocument(self);
268-
if (res == null) {
269-
return null;
270-
}
271-
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
267+
// Returns itself or ancestor object inheriting from Node.
268+
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
269+
// - An Element inside a shadow DOM will return the associated ShadowRoot.
270+
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
271+
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
272+
if (options) |options_| if (options_.composed) {
273+
log.warn("getRootNode composed is not implemented yet", .{});
274+
};
275+
return try Node.toInterface(try parser.nodeGetRootNode(self));
272276
}
273277

274278
pub fn _hasChildNodes(self: *parser.Node) !bool {

src/browser/html/document.zig

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,19 +292,30 @@ test "Browser.HTML.Document" {
292292
}, .{});
293293

294294
try runner.testCases(&.{
295-
.{ "document.elementFromPoint(0.5, 0.5)", "null" },
295+
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Should these be document?
296296
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
297-
.{ "document.createElement('div').getClientRects()", null },
297+
.{
298+
\\ let div1 = document.createElement('div');
299+
\\ document.body.appendChild(div1);
300+
\\ div1.getClientRects();
301+
,
302+
null,
303+
},
298304
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
299305
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
300306
.{ "elems.length", "1" },
301307
.{ "elems[0]", "[object HTMLDivElement]" },
302308
}, .{});
303309

304310
try runner.testCases(&.{
305-
.{ "let a = document.createElement('a')", null },
306-
.{ "a.href = \"https://lightpanda.io\"", null },
307-
.{ "a.getClientRects()", null }, // Note this will be placed after the div of previous test
311+
.{
312+
\\ let a = document.createElement('a');
313+
\\ a.href = "https://lightpanda.io";
314+
\\ document.body.appendChild(a);
315+
\\ a.getClientRects();
316+
, // Note this will be placed after the div of previous test
317+
null,
318+
},
308319
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
309320
.{ "a_again", "[object HTMLAnchorElement]" },
310321
.{ "a_again.href", "https://lightpanda.io" },

src/browser/html/window.zig

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,20 @@ test "Browser.HTML.Window" {
307307
try runner.testCases(&.{
308308
.{ "innerHeight", "1" },
309309
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
310-
.{ "document.createElement('div').getClientRects()", null },
311-
.{ "document.createElement('div').getClientRects()", null },
310+
.{
311+
\\ let div1 = document.createElement('div');
312+
\\ document.body.appendChild(div1);
313+
\\ div1.getClientRects();
314+
,
315+
null,
316+
},
317+
.{
318+
\\ let div2 = document.createElement('div');
319+
\\ document.body.appendChild(div2);
320+
\\ div2.getClientRects();
321+
,
322+
null,
323+
},
312324
.{ "innerHeight", "1" },
313325
.{ "innerWidth", "2" },
314326
}, .{});

src/browser/netsurf.zig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,17 @@ pub fn nodeGetChildNodes(node: *Node) !*NodeList {
11511151
return nlist.?;
11521152
}
11531153

1154+
pub fn nodeGetRootNode(node: *Node) !*Node {
1155+
var root = node;
1156+
while (true) {
1157+
const parent = try nodeParentNode(root);
1158+
if (parent) |parent_| {
1159+
root = parent_;
1160+
} else break;
1161+
}
1162+
return root;
1163+
}
1164+
11541165
pub fn nodeAppendChild(node: *Node, child: *Node) !*Node {
11551166
var res: ?*Node = undefined;
11561167
const err = nodeVtable(node).dom_node_append_child.?(node, child, &res);

src/browser/renderer.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const FlatRenderer = struct {
5050
};
5151
}
5252

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

src/cdp/domains/dom.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const Node = @import("../Node.zig");
2222
const css = @import("../../browser/dom/css.zig");
2323
const parser = @import("../../browser/netsurf.zig");
2424
const dom_node = @import("../../browser/dom/node.zig");
25-
const DOMRect = @import("../../browser/dom/element.zig").Element.DOMRect;
25+
const Element = @import("../../browser/dom/element.zig").Element;
2626

2727
pub fn processMessage(cmd: anytype) !void {
2828
const action = std.meta.stringToEnum(enum {
@@ -253,7 +253,7 @@ fn describeNode(cmd: anytype) !void {
253253
// We are assuming the start/endpoint is not repeated.
254254
const Quad = [8]f64;
255255

256-
fn rectToQuad(rect: DOMRect) Quad {
256+
fn rectToQuad(rect: Element.DOMRect) Quad {
257257
return Quad{
258258
rect.x,
259259
rect.y,
@@ -271,7 +271,7 @@ fn scrollIntoViewIfNeeded(cmd: anytype) !void {
271271
nodeId: ?Node.Id = null,
272272
backendNodeId: ?u32 = null,
273273
objectId: ?[]const u8 = null,
274-
rect: ?DOMRect = null,
274+
rect: ?Element.DOMRect = null,
275275
})) orelse return error.InvalidParams;
276276
// Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null
277277

@@ -327,7 +327,7 @@ fn getContentQuads(cmd: anytype) !void {
327327
// Elements like SVGElement may have multiple quads.
328328

329329
const element = parser.nodeToElement(node._node);
330-
const rect = try bc.session.page.?.state.renderer.getRect(element);
330+
const rect = try Element._getBoundingClientRect(element, &bc.session.page.?.state);
331331
const quad = rectToQuad(rect);
332332

333333
return cmd.sendResult(.{ .quads = &.{quad} }, .{});

0 commit comments

Comments
 (0)