mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 14:43: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 {
|
||||
self.high_priority.clearRetainingCapacity();
|
||||
self.low_priority.clearRetainingCapacity();
|
||||
// Our allocator is the page arena, it's been reset. We cannot use
|
||||
// clearAndRetainCapacity, since that space is no longer ours
|
||||
self.high_priority.clearAndFree();
|
||||
self.low_priority.clearAndFree();
|
||||
}
|
||||
|
||||
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 ResizeObserver = @import("resize_observer.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
const IntersectionObserver = @import("intersection_observer.zig");
|
||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
@@ -44,7 +43,6 @@ pub const Interfaces = .{
|
||||
Node.Interfaces,
|
||||
ResizeObserver.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
IntersectionObserver.Interfaces,
|
||||
DOMParser,
|
||||
TreeWalker,
|
||||
NodeIterator,
|
||||
@@ -54,4 +52,5 @@ pub const Interfaces = .{
|
||||
@import("range.zig").Interfaces,
|
||||
@import("Animation.zig"),
|
||||
@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();
|
||||
}
|
||||
|
||||
// debug/helper to print the source of the JS callback
|
||||
pub fn printFunc(self: Function) !void {
|
||||
const context = self.context;
|
||||
pub fn src(self: *const Function) ![]const u8 {
|
||||
const value = self.func.castToFunction().toValue();
|
||||
const src = try js.valueToString(context.call_arena, value, context.isolate, context.v8_context);
|
||||
std.debug.print("{s}\n", .{src});
|
||||
return self.context.valueToString(value, .{});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const Allocator = std.mem.Allocator;
|
||||
const Dump = @import("dump.zig");
|
||||
const State = @import("State.zig");
|
||||
const Mime = @import("mime.zig").Mime;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Renderer = @import("renderer.zig").Renderer;
|
||||
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));
|
||||
try polyfill.preload(self.arena, self.js);
|
||||
|
||||
try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" });
|
||||
// message loop must run only non-test env
|
||||
if (comptime !builtin.is_test) {
|
||||
try self.scheduler.add(self, runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
||||
}
|
||||
try self.registerBackgroundTasks();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Page) void {
|
||||
@@ -160,7 +157,7 @@ pub const Page = struct {
|
||||
self.script_manager.deinit();
|
||||
}
|
||||
|
||||
fn reset(self: *Page) void {
|
||||
fn reset(self: *Page) !void {
|
||||
// Force running the micro task to drain the queue.
|
||||
self.session.browser.env.runMicrotasks();
|
||||
|
||||
@@ -171,18 +168,31 @@ pub const Page = struct {
|
||||
self.load_state = .parsing;
|
||||
self.mode = .{ .pre = {} };
|
||||
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
try self.registerBackgroundTasks();
|
||||
}
|
||||
|
||||
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
||||
self.session.browser.runMicrotasks();
|
||||
return 5;
|
||||
}
|
||||
fn registerBackgroundTasks(self: *Page) !void {
|
||||
if (comptime builtin.is_test) {
|
||||
// HTML test runner manually calls these as necessary
|
||||
return;
|
||||
}
|
||||
|
||||
fn runMessageLoop(ctx: *anyopaque) ?u32 {
|
||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
||||
self.session.browser.runMessageLoop();
|
||||
return 100;
|
||||
try self.scheduler.add(self.session.browser, struct {
|
||||
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
||||
const b: *Browser = @ptrCast(@alignCast(ctx));
|
||||
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 {
|
||||
@@ -529,7 +539,7 @@ pub const Page = struct {
|
||||
if (self.mode != .pre) {
|
||||
// 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.
|
||||
self.reset();
|
||||
try self.reset();
|
||||
}
|
||||
|
||||
log.info(.http, "navigate", .{
|
||||
|
||||
@@ -1,96 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<body></body>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<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;
|
||||
const a1 = document.createElement('div');
|
||||
new IntersectionObserver(entries => {count_a += 1;}).observe(a1);
|
||||
testing.expectEqual(1, count_a);
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(0, count);
|
||||
});
|
||||
}
|
||||
|
||||
// This test is documenting current behavior, not correct behavior.
|
||||
// Currently every time observe is called, the callback is called with all entries.
|
||||
let count_b = 0;
|
||||
let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});
|
||||
const b1 = document.createElement('div');
|
||||
observer_b.observe(b1);
|
||||
testing.expectEqual(1, count_b);
|
||||
const b2 = document.createElement('div');
|
||||
observer_b.observe(b2);
|
||||
testing.expectEqual(2, count_b);
|
||||
{
|
||||
// not connected, but has parent
|
||||
let count = 0;
|
||||
const div1 = document.createElement('div');
|
||||
const div2 = document.createElement('div');
|
||||
new IntersectionObserver((entries) => {
|
||||
console.log(entries[0]);
|
||||
count += 1;
|
||||
}).observe(div1);
|
||||
|
||||
div2.appendChild(div1);
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, count);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=reobserve>
|
||||
let count_bb = 0;
|
||||
let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});
|
||||
const bb1 = document.createElement('div');
|
||||
observer_bb.observe(bb1);
|
||||
testing.expectEqual(1, count_bb)
|
||||
observer_bb.observe(bb1);
|
||||
testing.expectEqual(1, count_bb) // Still 1, not 2
|
||||
{
|
||||
let count = 0;
|
||||
let observer = new IntersectionObserver(entries => {
|
||||
count += entries.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);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, count);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=unobserve>
|
||||
let count_c = 0;
|
||||
let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});
|
||||
const c1 = document.createElement('div');
|
||||
observer_c.observe(c1);
|
||||
testing.expectEqual(1, count_c);
|
||||
observer_c.unobserve(c1);
|
||||
const c2 = document.createElement('div');
|
||||
observer_c.observe(c2);
|
||||
testing.expectEqual(1, count_c);
|
||||
</script>
|
||||
{
|
||||
let count = 0;
|
||||
let observer = new IntersectionObserver(entries => {
|
||||
count += entries.length;
|
||||
});
|
||||
|
||||
<script id=takeRecords>
|
||||
let observer_e = new IntersectionObserver(entries => {});
|
||||
let e1 = document.createElement('div');
|
||||
observer_e.observe(e1);
|
||||
const e2 = document.createElement('div');
|
||||
observer_e.observe(e2);
|
||||
testing.expectEqual(2, observer_e.takeRecords().length);
|
||||
const div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
|
||||
testing.expectEqual(0, count);
|
||||
observer.observe(div1);
|
||||
testing.expectEqual(0, count);
|
||||
observer.observe(div1);
|
||||
observer.observe(div1);
|
||||
testing.expectEqual(0, count);
|
||||
|
||||
observer.unobserve(div1);
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(0, count);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=disconnect>
|
||||
let observer_d = new IntersectionObserver(entries => {});
|
||||
let d1 = document.createElement('div');
|
||||
observer_d.observe(d1);
|
||||
observer_d.disconnect();
|
||||
testing.expectEqual(0, observer_d.takeRecords().length);
|
||||
{
|
||||
let count = 0;
|
||||
let observer = new IntersectionObserver(entries => {
|
||||
count += entries.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 id=entry>
|
||||
let entry;
|
||||
let div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
|
||||
{
|
||||
let entry = null;
|
||||
let observer = new IntersectionObserver(entries => {
|
||||
entry = entries[0];
|
||||
});
|
||||
|
||||
testing.expectEqual(0, entry.boundingClientRect.x);
|
||||
testing.expectEqual(1, entry.intersectionRatio);
|
||||
testing.expectEqual(0, entry.intersectionRect.x);
|
||||
testing.expectEqual(0, entry.intersectionRect.y);
|
||||
testing.expectEqual(1, entry.intersectionRect.width);
|
||||
testing.expectEqual(1, entry.intersectionRect.height);
|
||||
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());
|
||||
let div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(0, entry.boundingClientRect.x);
|
||||
testing.expectEqual(1, entry.intersectionRatio);
|
||||
testing.expectEqual(0, entry.intersectionRect.x);
|
||||
testing.expectEqual(0, entry.intersectionRect.y);
|
||||
testing.expectEqual(1, entry.intersectionRect.width);
|
||||
testing.expectEqual(1, entry.intersectionRect.height);
|
||||
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 id=options>
|
||||
const new_root = document.createElement('span');
|
||||
document.body.appendChild(new_root);
|
||||
<script id=timing>
|
||||
{
|
||||
const capture = [];
|
||||
const observer = new IntersectionObserver(() => {
|
||||
capture.push('callback');
|
||||
});
|
||||
|
||||
let new_entry;
|
||||
const new_observer = new IntersectionObserver(
|
||||
(entries) => { new_entry = entries[0]; },
|
||||
{root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]}
|
||||
);
|
||||
const div = document.createElement('div');
|
||||
capture.push('pre-append');
|
||||
document.body.appendChild(div);
|
||||
capture.push('post-append');
|
||||
|
||||
new_observer.observe(document.createElement('div'));
|
||||
testing.expectEqual(1, new_entry.rootBounds.x);
|
||||
capture.push('pre-observe');
|
||||
observer.observe(div);
|
||||
capture.push('post-observe');
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual([
|
||||
'pre-append',
|
||||
'post-append',
|
||||
'pre-observe',
|
||||
'post-observe',
|
||||
'callback',
|
||||
], capture)
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user