mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Merge pull request #1130 from lightpanda-io/intersection_observer
Rework IntersectionObserver
This commit is contained in:
@@ -37,8 +37,10 @@ pub fn init(allocator: Allocator) Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *Scheduler) void {
|
pub fn reset(self: *Scheduler) void {
|
||||||
self.high_priority.clearRetainingCapacity();
|
// Our allocator is the page arena, it's been reset. We cannot use
|
||||||
self.low_priority.clearRetainingCapacity();
|
// clearAndRetainCapacity, since that space is no longer ours
|
||||||
|
self.high_priority.clearAndFree();
|
||||||
|
self.low_priority.clearAndFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddOpts = struct {
|
const AddOpts = struct {
|
||||||
|
|||||||
329
src/browser/dom/IntersectionObserver.zig
Normal file
329
src/browser/dom/IntersectionObserver.zig
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const Node = @import("node.zig").Node;
|
||||||
|
const Element = @import("element.zig").Element;
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
IntersectionObserver,
|
||||||
|
Entry,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This implementation attempts to be as less wrong as possible. Since we don't
|
||||||
|
// render, or know how things are positioned, our best guess isn't very good.
|
||||||
|
const IntersectionObserver = @This();
|
||||||
|
page: *Page,
|
||||||
|
root: *parser.Node,
|
||||||
|
callback: js.Function,
|
||||||
|
event_node: parser.EventNode,
|
||||||
|
observed_entries: std.ArrayList(Entry),
|
||||||
|
pending_elements: std.ArrayList(*parser.Element),
|
||||||
|
ready_elements: std.ArrayList(*parser.Element),
|
||||||
|
|
||||||
|
pub fn constructor(callback: js.Function, opts_: ?IntersectionObserverOptions, page: *Page) !*IntersectionObserver {
|
||||||
|
const opts = opts_ orelse IntersectionObserverOptions{};
|
||||||
|
|
||||||
|
const self = try page.arena.create(IntersectionObserver);
|
||||||
|
self.* = .{
|
||||||
|
.page = page,
|
||||||
|
.callback = callback,
|
||||||
|
.ready_elements = .{},
|
||||||
|
.observed_entries = .{},
|
||||||
|
.pending_elements = .{},
|
||||||
|
.event_node = .{ .func = mutationCallback },
|
||||||
|
.root = opts.root orelse parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Node, self.root),
|
||||||
|
"DOMNodeInserted",
|
||||||
|
&self.event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Node, self.root),
|
||||||
|
"DOMNodeRemoved",
|
||||||
|
&self.event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _disconnect(self: *IntersectionObserver) !void {
|
||||||
|
// We don't free as it is on an arena
|
||||||
|
self.ready_elements = .{};
|
||||||
|
self.observed_entries = .{};
|
||||||
|
self.pending_elements = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element, page: *Page) !void {
|
||||||
|
for (self.observed_entries.items) |*observer| {
|
||||||
|
if (observer.target == target_element) {
|
||||||
|
return; // Already observed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.isPending(target_element)) {
|
||||||
|
return; // Already pending
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.ready_elements.items) |element| {
|
||||||
|
if (element == target_element) {
|
||||||
|
return; // Already primed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can never fire callbacks synchronously. Code like React expects any
|
||||||
|
// callback to fire in the future (e.g. via microtasks).
|
||||||
|
try self.ready_elements.append(self.page.arena, target_element);
|
||||||
|
if (self.ready_elements.items.len == 1) {
|
||||||
|
// this is our first ready entry, schedule a callback
|
||||||
|
try page.scheduler.add(self, processReady, 0, .{
|
||||||
|
.name = "intersection ready",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||||
|
if (self.removeObserved(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.ready_elements.items, 0..) |el, index| {
|
||||||
|
if (el == target) {
|
||||||
|
_ = self.ready_elements.swapRemove(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.pending_elements.items, 0..) |el, index| {
|
||||||
|
if (el == target) {
|
||||||
|
_ = self.pending_elements.swapRemove(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _takeRecords(self: *IntersectionObserver) []Entry {
|
||||||
|
return self.observed_entries.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processReady(ctx: *anyopaque) ?u32 {
|
||||||
|
const self: *IntersectionObserver = @ptrCast(@alignCast(ctx));
|
||||||
|
self._processReady() catch |err| {
|
||||||
|
log.err(.web_api, "intersection ready", .{ .err = err });
|
||||||
|
};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _processReady(self: *IntersectionObserver) !void {
|
||||||
|
defer self.ready_elements.clearRetainingCapacity();
|
||||||
|
for (self.ready_elements.items) |element| {
|
||||||
|
// IntersectionObserver probably doesn't work like what your intuition
|
||||||
|
// thinks. As long as a node has a parent, even if that parent isn't
|
||||||
|
// connected and even if the two nodes don't intersect, it'll fire the
|
||||||
|
// callback once.
|
||||||
|
if (try Node.get_parentNode(@ptrCast(element)) == null) {
|
||||||
|
if (!self.isPending(element)) {
|
||||||
|
try self.pending_elements.append(self.page.arena, element);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try self.forceObserve(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isPending(self: *IntersectionObserver, element: *parser.Element) bool {
|
||||||
|
for (self.pending_elements.items) |el| {
|
||||||
|
if (el == element) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
|
||||||
|
const mutation_event = parser.eventToMutationEvent(event);
|
||||||
|
const self: *IntersectionObserver = @fieldParentPtr("event_node", en);
|
||||||
|
self._mutationCallback(mutation_event) catch |err| {
|
||||||
|
log.err(.web_api, "mutation callback", .{ .err = err, .source = "intersection observer" });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _mutationCallback(self: *IntersectionObserver, event: *parser.MutationEvent) !void {
|
||||||
|
const event_type = parser.eventType(@ptrCast(event));
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
|
||||||
|
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
||||||
|
if (parser.nodeType(node) != .element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el: *parser.Element = @ptrCast(node);
|
||||||
|
if (self.removePending(el)) {
|
||||||
|
// It was pending (because it wasn't in the root), but now it is
|
||||||
|
// we should observe it.
|
||||||
|
try self.forceObserve(el);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
|
||||||
|
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
||||||
|
if (parser.nodeType(node) != .element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el: *parser.Element = @ptrCast(node);
|
||||||
|
if (self.removeObserved(el)) {
|
||||||
|
// It _was_ observed, it no longer is in our root, but if it was
|
||||||
|
// to get re-added, it should be observed again (I think), so
|
||||||
|
// we add it to our pending list
|
||||||
|
try self.pending_elements.append(self.page.arena, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// impossible event type
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists to skip the checks made _observe when called from a DOMNodeInserted
|
||||||
|
// event. In such events, the event handler has alread done the necessary
|
||||||
|
// checks.
|
||||||
|
fn forceObserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||||
|
try self.observed_entries.append(self.page.arena, .{
|
||||||
|
.page = self.page,
|
||||||
|
.root = self.root,
|
||||||
|
.target = target,
|
||||||
|
});
|
||||||
|
|
||||||
|
var result: js.Function.Result = undefined;
|
||||||
|
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
||||||
|
log.debug(.user_script, "callback error", .{
|
||||||
|
.err = result.exception,
|
||||||
|
.stack = result.stack,
|
||||||
|
.source = "intersection observer",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn removeObserved(self: *IntersectionObserver, target: *parser.Element) bool {
|
||||||
|
for (self.observed_entries.items, 0..) |*observer, index| {
|
||||||
|
if (observer.target == target) {
|
||||||
|
_ = self.observed_entries.swapRemove(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn removePending(self: *IntersectionObserver, target: *parser.Element) bool {
|
||||||
|
for (self.pending_elements.items, 0..) |el, index| {
|
||||||
|
if (el == target) {
|
||||||
|
_ = self.pending_elements.swapRemove(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntersectionObserverOptions = struct {
|
||||||
|
root: ?*parser.Node = null, // Element or Document
|
||||||
|
rootMargin: ?[]const u8 = "0px 0px 0px 0px",
|
||||||
|
threshold: ?Threshold = .{ .single = 0.0 },
|
||||||
|
|
||||||
|
const Threshold = union(enum) {
|
||||||
|
single: f32,
|
||||||
|
list: []const f32,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Entry
|
||||||
|
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||||
|
pub const Entry = struct {
|
||||||
|
page: *Page,
|
||||||
|
root: *parser.Node,
|
||||||
|
target: *parser.Element,
|
||||||
|
|
||||||
|
// 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 Entry) !Element.DOMRect {
|
||||||
|
return Element._getBoundingClientRect(self.target, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||||
|
pub fn get_intersectionRatio(_: *const Entry) f32 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a DOMRectReadOnly representing the target's visible area.
|
||||||
|
pub fn get_intersectionRect(self: *const Entry) !Element.DOMRect {
|
||||||
|
return Element._getBoundingClientRect(self.target, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Boolean value which is true if the target element intersects with the
|
||||||
|
// intersection observer's root. If this is true, then, the
|
||||||
|
// Entry describes a transition into a state of
|
||||||
|
// intersection; if it's false, then you know the transition is from
|
||||||
|
// intersecting to not-intersecting.
|
||||||
|
pub fn get_isIntersecting(_: *const Entry) bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a DOMRectReadOnly for the intersection observer's root.
|
||||||
|
pub fn get_rootBounds(self: *const Entry) !Element.DOMRect {
|
||||||
|
const root = self.root;
|
||||||
|
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
||||||
|
return self.page.renderer.boundingRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const root_type = parser.nodeType(root);
|
||||||
|
|
||||||
|
var element: *parser.Element = undefined;
|
||||||
|
switch (root_type) {
|
||||||
|
.element => element = parser.nodeToElement(root),
|
||||||
|
.document => {
|
||||||
|
const doc = parser.nodeToDocument(root);
|
||||||
|
element = (try parser.documentGetDocumentElement(doc)).?;
|
||||||
|
},
|
||||||
|
else => return error.InvalidState,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Element._getBoundingClientRect(element, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Element whose intersection with the root changed.
|
||||||
|
pub fn get_target(self: *const Entry) *parser.Element {
|
||||||
|
return self.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: pub fn get_time(self: *const Entry)
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: DOM.IntersectionObserver" {
|
||||||
|
try testing.htmlRunner("dom/intersection_observer.html");
|
||||||
|
}
|
||||||
@@ -25,7 +25,6 @@ const NodeList = @import("nodelist.zig");
|
|||||||
const Node = @import("node.zig");
|
const Node = @import("node.zig");
|
||||||
const ResizeObserver = @import("resize_observer.zig");
|
const ResizeObserver = @import("resize_observer.zig");
|
||||||
const MutationObserver = @import("mutation_observer.zig");
|
const MutationObserver = @import("mutation_observer.zig");
|
||||||
const IntersectionObserver = @import("intersection_observer.zig");
|
|
||||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
const DOMParser = @import("dom_parser.zig").DOMParser;
|
||||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||||
@@ -44,7 +43,6 @@ pub const Interfaces = .{
|
|||||||
Node.Interfaces,
|
Node.Interfaces,
|
||||||
ResizeObserver.Interfaces,
|
ResizeObserver.Interfaces,
|
||||||
MutationObserver.Interfaces,
|
MutationObserver.Interfaces,
|
||||||
IntersectionObserver.Interfaces,
|
|
||||||
DOMParser,
|
DOMParser,
|
||||||
TreeWalker,
|
TreeWalker,
|
||||||
NodeIterator,
|
NodeIterator,
|
||||||
@@ -54,4 +52,5 @@ pub const Interfaces = .{
|
|||||||
@import("range.zig").Interfaces,
|
@import("range.zig").Interfaces,
|
||||||
@import("Animation.zig"),
|
@import("Animation.zig"),
|
||||||
@import("MessageChannel.zig").Interfaces,
|
@import("MessageChannel.zig").Interfaces,
|
||||||
|
@import("IntersectionObserver.zig").Interfaces,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
IntersectionObserver,
|
|
||||||
IntersectionObserverEntry,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is supposed to listen to change between the root and observation targets.
|
|
||||||
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
|
|
||||||
// As such, there are no changes to intersections between the root and any target.
|
|
||||||
// Instead we keep a list of all entries that are being observed.
|
|
||||||
// The callback is called with all entries everytime a new entry is added(observed).
|
|
||||||
// Potentially we should also call the callback at a regular interval.
|
|
||||||
// The returned Entries are phony, they always indicate full intersection.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
|
||||||
pub const IntersectionObserver = struct {
|
|
||||||
page: *Page,
|
|
||||||
callback: js.Function,
|
|
||||||
options: IntersectionObserverOptions,
|
|
||||||
|
|
||||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
|
||||||
|
|
||||||
// new IntersectionObserver(callback)
|
|
||||||
// new IntersectionObserver(callback, options) [not supported yet]
|
|
||||||
pub fn constructor(callback: js.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
|
|
||||||
var options = IntersectionObserverOptions{
|
|
||||||
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
|
||||||
.rootMargin = "0px 0px 0px 0px",
|
|
||||||
.threshold = .{ .single = 0.0 },
|
|
||||||
};
|
|
||||||
if (options_) |*o| {
|
|
||||||
if (o.root) |root| {
|
|
||||||
options.root = root;
|
|
||||||
} // Other properties are not used due to the way we render
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.page = page,
|
|
||||||
.callback = callback,
|
|
||||||
.options = options,
|
|
||||||
.observed_entries = .{},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
|
||||||
self.observed_entries = .{}; // We don't free as it is on an arena
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
|
|
||||||
for (self.observed_entries.items) |*observer| {
|
|
||||||
if (observer.target == target_element) {
|
|
||||||
return; // Already observed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.observed_entries.append(self.page.arena, .{
|
|
||||||
.page = self.page,
|
|
||||||
.target = target_element,
|
|
||||||
.options = &self.options,
|
|
||||||
});
|
|
||||||
|
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "intersection observer",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
|
||||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
|
||||||
if (observer.target == target) {
|
|
||||||
_ = self.observed_entries.swapRemove(index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
|
|
||||||
return self.observed_entries.items;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const IntersectionObserverOptions = struct {
|
|
||||||
root: ?*parser.Node, // Element or Document
|
|
||||||
rootMargin: ?[]const u8,
|
|
||||||
threshold: ?Threshold,
|
|
||||||
|
|
||||||
const Threshold = union(enum) {
|
|
||||||
single: f32,
|
|
||||||
list: []const f32,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
|
||||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
|
||||||
pub const IntersectionObserverEntry = struct {
|
|
||||||
page: *Page,
|
|
||||||
target: *parser.Element,
|
|
||||||
options: *IntersectionObserverOptions,
|
|
||||||
|
|
||||||
// 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 Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
|
||||||
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
|
||||||
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
return Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
|
||||||
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
const root = self.options.root.?;
|
|
||||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
|
||||||
return self.page.renderer.boundingRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root_type = parser.nodeType(root);
|
|
||||||
|
|
||||||
var element: *parser.Element = undefined;
|
|
||||||
switch (root_type) {
|
|
||||||
.element => element = parser.nodeToElement(root),
|
|
||||||
.document => {
|
|
||||||
const doc = parser.nodeToDocument(root);
|
|
||||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
|
||||||
},
|
|
||||||
else => return error.InvalidState,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Element._getBoundingClientRect(element, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Element whose intersection with the root changed.
|
|
||||||
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
|
|
||||||
return self.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser: DOM.IntersectionObserver" {
|
|
||||||
try testing.htmlRunner("dom/intersection_observer.html");
|
|
||||||
}
|
|
||||||
@@ -138,10 +138,7 @@ fn getThis(self: *const Function) v8.Object {
|
|||||||
return self.this orelse self.context.v8_context.getGlobal();
|
return self.this orelse self.context.v8_context.getGlobal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// debug/helper to print the source of the JS callback
|
pub fn src(self: *const Function) ![]const u8 {
|
||||||
pub fn printFunc(self: Function) !void {
|
|
||||||
const context = self.context;
|
|
||||||
const value = self.func.castToFunction().toValue();
|
const value = self.func.castToFunction().toValue();
|
||||||
const src = try js.valueToString(context.call_arena, value, context.isolate, context.v8_context);
|
return self.context.valueToString(value, .{});
|
||||||
std.debug.print("{s}\n", .{src});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const Dump = @import("dump.zig");
|
const Dump = @import("dump.zig");
|
||||||
const State = @import("State.zig");
|
const State = @import("State.zig");
|
||||||
const Mime = @import("mime.zig").Mime;
|
const Mime = @import("mime.zig").Mime;
|
||||||
|
const Browser = @import("browser.zig").Browser;
|
||||||
const Session = @import("session.zig").Session;
|
const Session = @import("session.zig").Session;
|
||||||
const Renderer = @import("renderer.zig").Renderer;
|
const Renderer = @import("renderer.zig").Renderer;
|
||||||
const Window = @import("html/window.zig").Window;
|
const Window = @import("html/window.zig").Window;
|
||||||
@@ -146,11 +147,7 @@ pub const Page = struct {
|
|||||||
self.js = try session.executor.createContext(self, true, js.GlobalMissingCallback.init(&self.polyfill_loader));
|
self.js = try session.executor.createContext(self, true, js.GlobalMissingCallback.init(&self.polyfill_loader));
|
||||||
try polyfill.preload(self.arena, self.js);
|
try polyfill.preload(self.arena, self.js);
|
||||||
|
|
||||||
try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" });
|
try self.registerBackgroundTasks();
|
||||||
// message loop must run only non-test env
|
|
||||||
if (comptime !builtin.is_test) {
|
|
||||||
try self.scheduler.add(self, runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Page) void {
|
pub fn deinit(self: *Page) void {
|
||||||
@@ -160,7 +157,7 @@ pub const Page = struct {
|
|||||||
self.script_manager.deinit();
|
self.script_manager.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(self: *Page) void {
|
fn reset(self: *Page) !void {
|
||||||
// Force running the micro task to drain the queue.
|
// Force running the micro task to drain the queue.
|
||||||
self.session.browser.env.runMicrotasks();
|
self.session.browser.env.runMicrotasks();
|
||||||
|
|
||||||
@@ -171,18 +168,31 @@ pub const Page = struct {
|
|||||||
self.load_state = .parsing;
|
self.load_state = .parsing;
|
||||||
self.mode = .{ .pre = {} };
|
self.mode = .{ .pre = {} };
|
||||||
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
|
|
||||||
|
try self.registerBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
fn registerBackgroundTasks(self: *Page) !void {
|
||||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
if (comptime builtin.is_test) {
|
||||||
self.session.browser.runMicrotasks();
|
// HTML test runner manually calls these as necessary
|
||||||
return 5;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runMessageLoop(ctx: *anyopaque) ?u32 {
|
try self.scheduler.add(self.session.browser, struct {
|
||||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
||||||
self.session.browser.runMessageLoop();
|
const b: *Browser = @ptrCast(@alignCast(ctx));
|
||||||
return 100;
|
b.runMicrotasks();
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}.runMicrotasks, 5, .{ .name = "page.microtasks" });
|
||||||
|
|
||||||
|
try self.scheduler.add(self.session.browser, struct {
|
||||||
|
fn runMessageLoop(ctx: *anyopaque) ?u32 {
|
||||||
|
const b: *Browser = @ptrCast(@alignCast(ctx));
|
||||||
|
b.runMessageLoop();
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
}.runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DumpOpts = struct {
|
pub const DumpOpts = struct {
|
||||||
@@ -529,7 +539,7 @@ pub const Page = struct {
|
|||||||
if (self.mode != .pre) {
|
if (self.mode != .pre) {
|
||||||
// it's possible for navigate to be called multiple times on the
|
// it's possible for navigate to be called multiple times on the
|
||||||
// same page (via CDP). We want to reset the page between each call.
|
// same page (via CDP). We want to reset the page between each call.
|
||||||
self.reset();
|
try self.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(.http, "navigate", .{
|
log.info(.http, "navigate", .{
|
||||||
|
|||||||
@@ -1,96 +1,163 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<body></body>
|
<body></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
<script id=intersectionObserver>
|
<script id=intersectionObserver>
|
||||||
// doesn't crash (I guess)
|
{
|
||||||
new IntersectionObserver(() => {}).observe(document.documentElement);
|
// never attached
|
||||||
|
let count = 0;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
new IntersectionObserver((entries) => {
|
||||||
|
count += 1;
|
||||||
|
}).observe(div);
|
||||||
|
|
||||||
let count_a = 0;
|
testing.eventually(() => {
|
||||||
const a1 = document.createElement('div');
|
testing.expectEqual(0, count);
|
||||||
new IntersectionObserver(entries => {count_a += 1;}).observe(a1);
|
});
|
||||||
testing.expectEqual(1, count_a);
|
}
|
||||||
|
|
||||||
// This test is documenting current behavior, not correct behavior.
|
{
|
||||||
// Currently every time observe is called, the callback is called with all entries.
|
// not connected, but has parent
|
||||||
let count_b = 0;
|
let count = 0;
|
||||||
let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});
|
const div1 = document.createElement('div');
|
||||||
const b1 = document.createElement('div');
|
const div2 = document.createElement('div');
|
||||||
observer_b.observe(b1);
|
new IntersectionObserver((entries) => {
|
||||||
testing.expectEqual(1, count_b);
|
console.log(entries[0]);
|
||||||
const b2 = document.createElement('div');
|
count += 1;
|
||||||
observer_b.observe(b2);
|
}).observe(div1);
|
||||||
testing.expectEqual(2, count_b);
|
|
||||||
|
div2.appendChild(div1);
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(1, count);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=reobserve>
|
<script id=reobserve>
|
||||||
let count_bb = 0;
|
{
|
||||||
let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});
|
let count = 0;
|
||||||
const bb1 = document.createElement('div');
|
let observer = new IntersectionObserver(entries => {
|
||||||
observer_bb.observe(bb1);
|
count += entries.length;
|
||||||
testing.expectEqual(1, count_bb)
|
});
|
||||||
observer_bb.observe(bb1);
|
|
||||||
testing.expectEqual(1, count_bb) // Still 1, not 2
|
const div1 = document.createElement('div');
|
||||||
|
document.body.appendChild(div1);
|
||||||
|
|
||||||
|
// cannot fire synchronously, must be on the next tick
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
observer.observe(div1);
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
observer.observe(div1);
|
||||||
|
observer.observe(div1);
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(1, count);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=unobserve>
|
<script id=unobserve>
|
||||||
let count_c = 0;
|
{
|
||||||
let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});
|
let count = 0;
|
||||||
const c1 = document.createElement('div');
|
let observer = new IntersectionObserver(entries => {
|
||||||
observer_c.observe(c1);
|
count += entries.length;
|
||||||
testing.expectEqual(1, count_c);
|
});
|
||||||
observer_c.unobserve(c1);
|
|
||||||
const c2 = document.createElement('div');
|
|
||||||
observer_c.observe(c2);
|
|
||||||
testing.expectEqual(1, count_c);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script id=takeRecords>
|
const div1 = document.createElement('div');
|
||||||
let observer_e = new IntersectionObserver(entries => {});
|
document.body.appendChild(div1);
|
||||||
let e1 = document.createElement('div');
|
|
||||||
observer_e.observe(e1);
|
testing.expectEqual(0, count);
|
||||||
const e2 = document.createElement('div');
|
observer.observe(div1);
|
||||||
observer_e.observe(e2);
|
testing.expectEqual(0, count);
|
||||||
testing.expectEqual(2, observer_e.takeRecords().length);
|
observer.observe(div1);
|
||||||
|
observer.observe(div1);
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
|
||||||
|
observer.unobserve(div1);
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=disconnect>
|
<script id=disconnect>
|
||||||
let observer_d = new IntersectionObserver(entries => {});
|
{
|
||||||
let d1 = document.createElement('div');
|
let count = 0;
|
||||||
observer_d.observe(d1);
|
let observer = new IntersectionObserver(entries => {
|
||||||
observer_d.disconnect();
|
count += entries.length;
|
||||||
testing.expectEqual(0, observer_d.takeRecords().length);
|
});
|
||||||
|
|
||||||
|
const div1 = document.createElement('div');
|
||||||
|
document.body.appendChild(div1);
|
||||||
|
|
||||||
|
// cannot fire synchronously, must be on the next tick
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
observer.observe(div1);
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
observer.observe(div1);
|
||||||
|
observer.observe(div1);
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
observer.disconnect();
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(0, count);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=entry>
|
<script id=entry>
|
||||||
let entry;
|
{
|
||||||
let div1 = document.createElement('div');
|
let entry = null;
|
||||||
document.body.appendChild(div1);
|
let observer = new IntersectionObserver(entries => {
|
||||||
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
|
entry = entries[0];
|
||||||
|
});
|
||||||
|
|
||||||
testing.expectEqual(0, entry.boundingClientRect.x);
|
let div1 = document.createElement('div');
|
||||||
testing.expectEqual(1, entry.intersectionRatio);
|
document.body.appendChild(div1);
|
||||||
testing.expectEqual(0, entry.intersectionRect.x);
|
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
|
||||||
testing.expectEqual(0, entry.intersectionRect.y);
|
|
||||||
testing.expectEqual(1, entry.intersectionRect.width);
|
testing.eventually(() => {
|
||||||
testing.expectEqual(1, entry.intersectionRect.height);
|
testing.expectEqual(0, entry.boundingClientRect.x);
|
||||||
testing.expectEqual(true, entry.isIntersecting);
|
testing.expectEqual(1, entry.intersectionRatio);
|
||||||
testing.expectEqual(0, entry.rootBounds.x);
|
testing.expectEqual(0, entry.intersectionRect.x);
|
||||||
testing.expectEqual(0, entry.rootBounds.y);
|
testing.expectEqual(0, entry.intersectionRect.y);
|
||||||
testing.expectEqual(1, entry.rootBounds.width);
|
testing.expectEqual(1, entry.intersectionRect.width);
|
||||||
testing.expectEqual(1, entry.rootBounds.height);
|
testing.expectEqual(1, entry.intersectionRect.height);
|
||||||
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
|
testing.expectEqual(true, entry.isIntersecting);
|
||||||
|
testing.expectEqual(0, entry.rootBounds.x);
|
||||||
|
testing.expectEqual(0, entry.rootBounds.y);
|
||||||
|
testing.expectEqual(1, entry.rootBounds.width);
|
||||||
|
testing.expectEqual(1, entry.rootBounds.height);
|
||||||
|
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=options>
|
<script id=timing>
|
||||||
const new_root = document.createElement('span');
|
{
|
||||||
document.body.appendChild(new_root);
|
const capture = [];
|
||||||
|
const observer = new IntersectionObserver(() => {
|
||||||
|
capture.push('callback');
|
||||||
|
});
|
||||||
|
|
||||||
let new_entry;
|
const div = document.createElement('div');
|
||||||
const new_observer = new IntersectionObserver(
|
capture.push('pre-append');
|
||||||
(entries) => { new_entry = entries[0]; },
|
document.body.appendChild(div);
|
||||||
{root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]}
|
capture.push('post-append');
|
||||||
);
|
|
||||||
|
|
||||||
new_observer.observe(document.createElement('div'));
|
capture.push('pre-observe');
|
||||||
testing.expectEqual(1, new_entry.rootBounds.x);
|
observer.observe(div);
|
||||||
|
capture.push('post-observe');
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual([
|
||||||
|
'pre-append',
|
||||||
|
'post-append',
|
||||||
|
'pre-observe',
|
||||||
|
'post-observe',
|
||||||
|
'callback',
|
||||||
|
], capture)
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user