mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
handle detached elements
This commit is contained in:
@@ -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(&.{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 this’s 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 {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" },
|
||||
}, .{});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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} }, .{});
|
||||
|
||||
Reference in New Issue
Block a user