diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 187fd50f1..77d527ea2 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -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.window.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.window.document.?))) { + return &.{}; + } + const heap_ptr = try state.call_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(&.{ diff --git a/src/browser/dom/intersection_observer.zig b/src/browser/dom/intersection_observer.zig index b9aad9715..35ad8bbc8 100644 --- a/src/browser/dom/intersection_observer.zig +++ b/src/browser/dom/intersection_observer.zig @@ -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( diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index 7f2747e14..14bfe136f 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -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