mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 08:18:59 +00:00
MutationObserver and IntersectionObserver
This commit is contained in:
@@ -5,9 +5,9 @@
|
|||||||
.fingerprint = 0xda130f3af836cea0,
|
.fingerprint = 0xda130f3af836cea0,
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/3aa2b39cb1ab588b85970beef5b374effccf1415.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
|
.hash = "v8-0.0.0-xddH66TeAwDDEs3QkHFlukxqqrRXITzzmmIn2NHISHCn",
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
// .v8 = .{ .path = "../zig-v8-fork" }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const Scheduler = @import("Scheduler.zig");
|
|||||||
const History = @import("webapi/History.zig");
|
const History = @import("webapi/History.zig");
|
||||||
const EventManager = @import("EventManager.zig");
|
const EventManager = @import("EventManager.zig");
|
||||||
const ScriptManager = @import("ScriptManager.zig");
|
const ScriptManager = @import("ScriptManager.zig");
|
||||||
|
|
||||||
const polyfill = @import("polyfill/polyfill.zig");
|
const polyfill = @import("polyfill/polyfill.zig");
|
||||||
|
|
||||||
const Parser = @import("parser/Parser.zig");
|
const Parser = @import("parser/Parser.zig");
|
||||||
@@ -50,6 +51,8 @@ const Window = @import("webapi/Window.zig");
|
|||||||
const Location = @import("webapi/Location.zig");
|
const Location = @import("webapi/Location.zig");
|
||||||
const Document = @import("webapi/Document.zig");
|
const Document = @import("webapi/Document.zig");
|
||||||
const HtmlScript = @import("webapi/Element.zig").Html.Script;
|
const HtmlScript = @import("webapi/Element.zig").Html.Script;
|
||||||
|
const MutationObserver = @import("webapi/MutationObserver.zig");
|
||||||
|
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
||||||
const storage = @import("webapi/storage/storage.zig");
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
|
|
||||||
const timestamp = @import("../datetime.zig").timestamp;
|
const timestamp = @import("../datetime.zig").timestamp;
|
||||||
@@ -89,6 +92,14 @@ _element_class_lists: Element.ClassListLookup = .{},
|
|||||||
|
|
||||||
_script_manager: ScriptManager,
|
_script_manager: ScriptManager,
|
||||||
|
|
||||||
|
// List of active MutationObservers
|
||||||
|
_mutation_observers: std.ArrayList(*MutationObserver) = .{},
|
||||||
|
_mutation_delivery_scheduled: bool = false,
|
||||||
|
|
||||||
|
// List of active IntersectionObservers
|
||||||
|
_intersection_observers: std.ArrayList(*IntersectionObserver) = .{},
|
||||||
|
_intersection_delivery_scheduled: bool = false,
|
||||||
|
|
||||||
_polyfill_loader: polyfill.Loader = .{},
|
_polyfill_loader: polyfill.Loader = .{},
|
||||||
|
|
||||||
// for heap allocations and managing WebAPI objects
|
// for heap allocations and managing WebAPI objects
|
||||||
@@ -190,6 +201,11 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
|||||||
self._notified_network_idle = .init;
|
self._notified_network_idle = .init;
|
||||||
self._notified_network_almost_idle = .init;
|
self._notified_network_almost_idle = .init;
|
||||||
|
|
||||||
|
self._mutation_observers = .{};
|
||||||
|
self._mutation_delivery_scheduled = false;
|
||||||
|
self._intersection_observers = .{};
|
||||||
|
self._intersection_delivery_scheduled = false;
|
||||||
|
|
||||||
try polyfill.preload(self.arena, self.js);
|
try polyfill.preload(self.arena, self.js);
|
||||||
try self.registerBackgroundTasks();
|
try self.registerBackgroundTasks();
|
||||||
}
|
}
|
||||||
@@ -677,6 +693,94 @@ pub fn domChanged(self: *Page) void {
|
|||||||
self.version += 1;
|
self.version += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void {
|
||||||
|
try self._mutation_observers.append(self.arena, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregisterMutationObserver(self: *Page, observer: *MutationObserver) void {
|
||||||
|
for (self._mutation_observers.items, 0..) |obs, i| {
|
||||||
|
if (obs == observer) {
|
||||||
|
_ = self._mutation_observers.swapRemove(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn registerIntersectionObserver(self: *Page, observer: *IntersectionObserver) !void {
|
||||||
|
try self._intersection_observers.append(self.arena, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregisterIntersectionObserver(self: *Page, observer: *IntersectionObserver) void {
|
||||||
|
for (self._intersection_observers.items, 0..) |obs, i| {
|
||||||
|
if (obs == observer) {
|
||||||
|
_ = self._intersection_observers.swapRemove(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkIntersections(self: *Page) !void {
|
||||||
|
for (self._intersection_observers.items) |observer| {
|
||||||
|
try observer.checkIntersections(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scheduleMutationDelivery(self: *Page) !void {
|
||||||
|
// Only queue if not already scheduled
|
||||||
|
if (self._mutation_delivery_scheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self._mutation_delivery_scheduled = true;
|
||||||
|
|
||||||
|
// Queue mutation delivery as a microtask
|
||||||
|
try self.js.queueMutationDelivery();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scheduleIntersectionDelivery(self: *Page) !void {
|
||||||
|
// Only queue if not already scheduled
|
||||||
|
if (self._intersection_delivery_scheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self._intersection_delivery_scheduled = true;
|
||||||
|
|
||||||
|
// Queue intersection delivery as a microtask
|
||||||
|
try self.js.queueIntersectionDelivery();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deliverIntersections(self: *Page) void {
|
||||||
|
if (!self._intersection_delivery_scheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self._intersection_delivery_scheduled = false;
|
||||||
|
|
||||||
|
// Iterate backwards to handle observers that disconnect during their callback
|
||||||
|
var i = self._intersection_observers.items.len;
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
const observer = self._intersection_observers.items[i];
|
||||||
|
observer.deliverEntries(self) catch |err| {
|
||||||
|
log.err(.page, "page.deliverIntersections", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deliverMutations(self: *Page) void {
|
||||||
|
if (!self._mutation_delivery_scheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self._mutation_delivery_scheduled = false;
|
||||||
|
|
||||||
|
// Iterate backwards to handle observers that disconnect during their callback
|
||||||
|
var i = self._mutation_observers.items.len;
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
const observer = self._mutation_observers.items[i];
|
||||||
|
observer.deliverRecords(self) catch |err| {
|
||||||
|
log.err(.page, "page.deliverMutations", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn notifyNetworkIdle(self: *Page) void {
|
fn notifyNetworkIdle(self: *Page) void {
|
||||||
std.debug.assert(self._notified_network_idle == .done);
|
std.debug.assert(self._notified_network_idle == .done);
|
||||||
self._session.browser.notification.dispatch(.page_network_idle, &.{
|
self._session.browser.notification.dispatch(.page_network_idle, &.{
|
||||||
@@ -1106,6 +1210,10 @@ const RemoveNodeOpts = struct {
|
|||||||
will_be_reconnected: bool,
|
will_be_reconnected: bool,
|
||||||
};
|
};
|
||||||
pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts) void {
|
pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts) void {
|
||||||
|
// Capture siblings before removing
|
||||||
|
const previous_sibling = child.previousSibling();
|
||||||
|
const next_sibling = child.nextSibling();
|
||||||
|
|
||||||
const children = parent._children.?;
|
const children = parent._children.?;
|
||||||
switch (children.*) {
|
switch (children.*) {
|
||||||
.one => |n| {
|
.one => |n| {
|
||||||
@@ -1132,6 +1240,11 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
|
|||||||
child._parent = null;
|
child._parent = null;
|
||||||
child._child_link = .{};
|
child._child_link = .{};
|
||||||
|
|
||||||
|
if (self.hasMutationObservers()) {
|
||||||
|
const removed = [_]*Node{child};
|
||||||
|
self.childListChange(parent, &.{}, &removed, previous_sibling, next_sibling);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.will_be_reconnected) {
|
if (opts.will_be_reconnected) {
|
||||||
// We might be removing the node only to re-insert it. If the node will
|
// We might be removing the node only to re-insert it. If the node will
|
||||||
// remain connected, we can skip the expensive process of fully
|
// remain connected, we can skip the expensive process of fully
|
||||||
@@ -1232,10 +1345,30 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
|||||||
}
|
}
|
||||||
child._parent = parent;
|
child._parent = parent;
|
||||||
|
|
||||||
if (comptime from_parser == false) {
|
// Tri-state behavior for mutations:
|
||||||
// When the parser adds the node, nodeIsReady is only called when the
|
// 1. from_parser=true, parse_mode=document -> no mutations (initial document parse)
|
||||||
// nodeComplete() callback is executed.
|
// 2. from_parser=true, parse_mode=fragment -> mutations (innerHTML additions)
|
||||||
try self.nodeIsReady(false, child);
|
// 3. from_parser=false, parse_mode=document -> mutation (js manipulation)
|
||||||
|
// split like this because from_parser can be comptime known.
|
||||||
|
const should_notify = if (comptime from_parser)
|
||||||
|
self._parse_mode == .fragment
|
||||||
|
else
|
||||||
|
true;
|
||||||
|
|
||||||
|
if (should_notify) {
|
||||||
|
if (comptime from_parser == false) {
|
||||||
|
// When the parser adds the node, nodeIsReady is only called when the
|
||||||
|
// nodeComplete() callback is executed.
|
||||||
|
try self.nodeIsReady(false, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify mutation observers about childList change
|
||||||
|
if (self.hasMutationObservers()) {
|
||||||
|
const previous_sibling = child.previousSibling();
|
||||||
|
const next_sibling = child.nextSibling();
|
||||||
|
const added = [_]*Node{child};
|
||||||
|
self.childListChange(parent, &added, &.{}, previous_sibling, next_sibling);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var document_by_id = &self.document._elements_by_id;
|
var document_by_id = &self.document._elements_by_id;
|
||||||
@@ -1276,16 +1409,71 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value: []const u8) void {
|
pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value: []const u8, old_value: ?[]const u8) void {
|
||||||
_ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| {
|
_ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| {
|
||||||
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err });
|
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (self._mutation_observers.items) |observer| {
|
||||||
|
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
||||||
|
log.err(.page, "attributeChange.notifyObserver", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn attributeRemove(self: *Page, element: *Element, name: []const u8) void {
|
pub fn attributeRemove(self: *Page, element: *Element, name: []const u8, old_value: []const u8) void {
|
||||||
_ = Element.Build.call(element, "attributeRemove", .{ element, name, self }) catch |err| {
|
_ = Element.Build.call(element, "attributeRemove", .{ element, name, self }) catch |err| {
|
||||||
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err });
|
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (self._mutation_observers.items) |observer| {
|
||||||
|
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
||||||
|
log.err(.page, "attributeRemove.notifyObserver", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasMutationObservers(self: *const Page) bool {
|
||||||
|
return self._mutation_observers.items.len > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn characterDataChange(
|
||||||
|
self: *Page,
|
||||||
|
target: *Node,
|
||||||
|
old_value: []const u8,
|
||||||
|
) void {
|
||||||
|
// Notify mutation observers
|
||||||
|
for (self._mutation_observers.items) |observer| {
|
||||||
|
observer.notifyCharacterDataChange(target, old_value, self) catch |err| {
|
||||||
|
log.err(.page, "cdataChange.notifyObserver", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn childListChange(
|
||||||
|
self: *Page,
|
||||||
|
target: *Node,
|
||||||
|
added_nodes: []const *Node,
|
||||||
|
removed_nodes: []const *Node,
|
||||||
|
previous_sibling: ?*Node,
|
||||||
|
next_sibling: ?*Node,
|
||||||
|
) void {
|
||||||
|
// Filter out HTML wrapper element during fragment parsing (html5ever quirk)
|
||||||
|
if (self._parse_mode == .fragment and added_nodes.len == 1) {
|
||||||
|
if (added_nodes[0].is(Element.Html.Html) != null) {
|
||||||
|
// This is the temporary HTML wrapper, added by html5ever
|
||||||
|
// that will be unwrapped, see:
|
||||||
|
// https://github.com/servo/html5ever/issues/583
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify mutation observers
|
||||||
|
for (self._mutation_observers.items) |observer| {
|
||||||
|
observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, self) catch |err| {
|
||||||
|
log.err(.page, "childListChange.notifyObserver", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '')
|
// TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '')
|
||||||
@@ -1302,9 +1490,22 @@ pub fn parseHtmlAsChildren(self: *Page, node: *Node, html: []const u8) !void {
|
|||||||
const first = children.one;
|
const first = children.one;
|
||||||
std.debug.assert(first.is(Element.Html.Html) != null);
|
std.debug.assert(first.is(Element.Html.Html) != null);
|
||||||
node._children = first._children;
|
node._children = first._children;
|
||||||
var it = node.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
if (self.hasMutationObservers()) {
|
||||||
child._parent = node;
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
child._parent = node;
|
||||||
|
// Notify mutation observers for each unwrapped child
|
||||||
|
const previous_sibling = child.previousSibling();
|
||||||
|
const next_sibling = child.nextSibling();
|
||||||
|
const added = [_]*Node{child};
|
||||||
|
self.childListChange(node, &added, &.{}, previous_sibling, next_sibling);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
child._parent = node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1907,6 +1907,26 @@ fn zigJsonToJs(isolate: v8.Isolate, v8_context: v8.Context, value: std.json.Valu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Microtasks
|
||||||
|
pub fn queueMutationDelivery(self: *Context) !void {
|
||||||
|
self.isolate.enqueueMicrotask(struct {
|
||||||
|
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||||
|
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||||
|
page.deliverMutations();
|
||||||
|
}
|
||||||
|
}.run, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queueIntersectionDelivery(self: *Context) !void {
|
||||||
|
self.isolate.enqueueMicrotask(struct {
|
||||||
|
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||||
|
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||||
|
page.deliverIntersections();
|
||||||
|
}
|
||||||
|
}.run, self.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// == Misc ==
|
// == Misc ==
|
||||||
// An interface for types that want to have their jsDeinit function to be
|
// An interface for types that want to have their jsDeinit function to be
|
||||||
// called when the call context ends
|
// called when the call context ends
|
||||||
|
|||||||
@@ -46,7 +46,14 @@ pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector
|
|||||||
// If necessary, turn a void context into something we can safely ptrCast
|
// If necessary, turn a void context into something we can safely ptrCast
|
||||||
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
||||||
|
|
||||||
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
|
const channel = v8.InspectorChannel.init(
|
||||||
|
safe_context,
|
||||||
|
InspectorContainer.onInspectorResponse,
|
||||||
|
InspectorContainer.onInspectorEvent,
|
||||||
|
InspectorContainer.onRunMessageLoopOnPause,
|
||||||
|
InspectorContainer.onQuitMessageLoopOnPause,
|
||||||
|
isolate,
|
||||||
|
);
|
||||||
|
|
||||||
const client = v8.InspectorClient.init();
|
const client = v8.InspectorClient.init();
|
||||||
|
|
||||||
@@ -127,6 +134,8 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con
|
|||||||
const NoopInspector = struct {
|
const NoopInspector = struct {
|
||||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
||||||
|
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
|
||||||
|
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
||||||
|
|||||||
@@ -544,4 +544,5 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/URL.zig"),
|
@import("../webapi/URL.zig"),
|
||||||
@import("../webapi/Window.zig"),
|
@import("../webapi/Window.zig"),
|
||||||
@import("../webapi/MutationObserver.zig"),
|
@import("../webapi/MutationObserver.zig"),
|
||||||
|
@import("../webapi/IntersectionObserver.zig"),
|
||||||
});
|
});
|
||||||
|
|||||||
31
src/browser/tests/intersection_observer/basic.html
Normal file
31
src/browser/tests/intersection_observer/basic.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<div id="target" style="width: 100px; height: 100px;">Target Element</div>
|
||||||
|
|
||||||
|
<script id="basic">
|
||||||
|
const target = document.getElementById('target');
|
||||||
|
let callbackCalled = false;
|
||||||
|
let entries = null;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((observerEntries, obs) => {
|
||||||
|
callbackCalled = true;
|
||||||
|
entries = observerEntries;
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(target);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(true, callbackCalled);
|
||||||
|
testing.expectEqual(1, entries.length);
|
||||||
|
|
||||||
|
const entry = entries[0];
|
||||||
|
|
||||||
|
testing.expectEqual(target, entry.target);
|
||||||
|
testing.expectEqual('boolean', typeof entry.isIntersecting);
|
||||||
|
testing.expectEqual('number', typeof entry.intersectionRatio);
|
||||||
|
testing.expectEqual('object', typeof entry.boundingClientRect);
|
||||||
|
testing.expectEqual('object', typeof entry.intersectionRect);
|
||||||
|
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
30
src/browser/tests/intersection_observer/disconnect.html
Normal file
30
src/browser/tests/intersection_observer/disconnect.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<div id="target" style="width: 100px; height: 100px;">Target Element</div>
|
||||||
|
|
||||||
|
<script id="disconnect">
|
||||||
|
const target = document.getElementById('target');
|
||||||
|
let callCount = 0;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(() => {
|
||||||
|
callCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(target);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(1, callCount);
|
||||||
|
|
||||||
|
observer.disconnect();
|
||||||
|
|
||||||
|
// Create a new observer to trigger another check
|
||||||
|
// If disconnect worked, the old observer won't fire
|
||||||
|
const observer2 = new IntersectionObserver(() => {});
|
||||||
|
observer2.observe(target);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
observer2.disconnect();
|
||||||
|
testing.expectEqual(1, callCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<div id="target1" style="width: 100px; height: 100px;">Target 1</div>
|
||||||
|
<div id="target2" style="width: 100px; height: 100px;">Target 2</div>
|
||||||
|
|
||||||
|
<script id="multiple">
|
||||||
|
const target1 = document.getElementById('target1');
|
||||||
|
const target2 = document.getElementById('target2');
|
||||||
|
let entryCount = 0;
|
||||||
|
const seenTargets = new Set();
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
entryCount++;
|
||||||
|
seenTargets.add(entry.target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(target1);
|
||||||
|
observer.observe(target2);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(2, entryCount);
|
||||||
|
testing.expectTrue(seenTargets.has(target1));
|
||||||
|
testing.expectTrue(seenTargets.has(target2));
|
||||||
|
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
30
src/browser/tests/intersection_observer/unobserve.html
Normal file
30
src/browser/tests/intersection_observer/unobserve.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<div id="target1" style="width: 100px; height: 100px;">Target 1</div>
|
||||||
|
<div id="target2" style="width: 100px; height: 100px;">Target 2</div>
|
||||||
|
|
||||||
|
<script id="unobserve">
|
||||||
|
const target1 = document.getElementById('target1');
|
||||||
|
const target2 = document.getElementById('target2');
|
||||||
|
const seenTargets = [];
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
seenTargets.push(entry.target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe target1, unobserve it, then observe target2
|
||||||
|
// We should only get a callback for target2
|
||||||
|
observer.observe(target1);
|
||||||
|
observer.unobserve(target1);
|
||||||
|
observer.observe(target2);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
// Should only see target2, not target1
|
||||||
|
testing.expectEqual(1, seenTargets.length);
|
||||||
|
testing.expectEqual(target2, seenTargets[0]);
|
||||||
|
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
79
src/browser/tests/mutation_observer/character_data.html
Normal file
79
src/browser/tests/mutation_observer/character_data.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id="test-element-1">Initial text</div>
|
||||||
|
<div id="test-element-2">Test</div>
|
||||||
|
<div id="test-element-3">Test</div>
|
||||||
|
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id="character_data">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('test-element-1');
|
||||||
|
const textNode = element.firstChild;
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(textNode, { characterData: true });
|
||||||
|
|
||||||
|
textNode.data = 'Changed text';
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(1, mutations.length, {script_id: 'character_data'});
|
||||||
|
testing.expectEqual('characterData', mutations[0].type);
|
||||||
|
testing.expectEqual(textNode, mutations[0].target);
|
||||||
|
testing.expectEqual(null, mutations[0].oldValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="character_data_old_value">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('test-element-2');
|
||||||
|
const textNode = element.firstChild;
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(textNode, { characterData: true, characterDataOldValue: true });
|
||||||
|
|
||||||
|
textNode.data = 'First change';
|
||||||
|
textNode.data = 'Second change';
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(2, mutations.length, {script_id: 'character_data_old_value'});
|
||||||
|
|
||||||
|
testing.expectEqual('characterData', mutations[0].type);
|
||||||
|
testing.expectEqual(textNode, mutations[0].target);
|
||||||
|
testing.expectEqual('Test', mutations[0].oldValue);
|
||||||
|
|
||||||
|
testing.expectEqual('characterData', mutations[1].type);
|
||||||
|
testing.expectEqual(textNode, mutations[1].target);
|
||||||
|
testing.expectEqual('First change', mutations[1].oldValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="character_data_no_observe">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('test-element-3');
|
||||||
|
const textNode = element.firstChild;
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe for attributes, not characterData
|
||||||
|
observer.observe(element, { attributes: true });
|
||||||
|
|
||||||
|
// Change text node data (should not trigger)
|
||||||
|
textNode.data = 'Changed but not observed';
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(false, callbackCalled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
327
src/browser/tests/mutation_observer/childlist.html
Normal file
327
src/browser/tests/mutation_observer/childlist.html
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id="parent"><div id="child1">Child 1</div></div>
|
||||||
|
<div id="empty-parent"></div>
|
||||||
|
<div id="remove-parent"><div id="only-child">Only</div></div>
|
||||||
|
<div id="text-parent"></div>
|
||||||
|
<div id="middle-parent"><div id="first">First</div><div id="middle">Middle</div><div id="last">Last</div></div>
|
||||||
|
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id="childlist">
|
||||||
|
testing.async(async () => {
|
||||||
|
const parent = document.getElementById('parent');
|
||||||
|
const child1 = document.getElementById('child1');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(parent, { childList: true });
|
||||||
|
|
||||||
|
const child2 = document.createElement('div');
|
||||||
|
child2.textContent = 'Child 2';
|
||||||
|
parent.appendChild(child2);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(1, mutations.length, {script_id: 'childlist'});
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(parent, mutations[0].target);
|
||||||
|
testing.expectEqual(1, mutations[0].addedNodes.length);
|
||||||
|
testing.expectEqual(child2, mutations[0].addedNodes[0]);
|
||||||
|
testing.expectEqual(0, mutations[0].removedNodes.length);
|
||||||
|
testing.expectEqual(child1, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[0].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="childlist_empty_parent">
|
||||||
|
testing.async(async () => {
|
||||||
|
const emptyParent = document.getElementById('empty-parent');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(emptyParent, { childList: true });
|
||||||
|
|
||||||
|
const firstChild = document.createElement('div');
|
||||||
|
firstChild.textContent = 'First child';
|
||||||
|
emptyParent.appendChild(firstChild);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(1, mutations.length, {script_id: 'childlist_empty_parent'});
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(emptyParent, mutations[0].target);
|
||||||
|
testing.expectEqual(1, mutations[0].addedNodes.length);
|
||||||
|
testing.expectEqual(firstChild, mutations[0].addedNodes[0]);
|
||||||
|
testing.expectEqual(0, mutations[0].removedNodes.length);
|
||||||
|
testing.expectEqual(null, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[0].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="childlist_remove_last">
|
||||||
|
testing.async(async () => {
|
||||||
|
const removeParent = document.getElementById('remove-parent');
|
||||||
|
const onlyChild = document.getElementById('only-child');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(removeParent, { childList: true });
|
||||||
|
|
||||||
|
removeParent.removeChild(onlyChild);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(1, mutations.length, {script_id: 'childlist_remove_last'});
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(removeParent, mutations[0].target);
|
||||||
|
testing.expectEqual(0, mutations[0].addedNodes.length);
|
||||||
|
testing.expectEqual(1, mutations[0].removedNodes.length);
|
||||||
|
testing.expectEqual(onlyChild, mutations[0].removedNodes[0]);
|
||||||
|
testing.expectEqual(null, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[0].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="childlist_text_node">
|
||||||
|
testing.async(async () => {
|
||||||
|
const textParent = document.getElementById('text-parent');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(textParent, { childList: true });
|
||||||
|
|
||||||
|
const textNode = document.createTextNode('Hello world');
|
||||||
|
textParent.appendChild(textNode);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(1, mutations.length, {script_id: 'childlist_text_node'});
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(textParent, mutations[0].target);
|
||||||
|
testing.expectEqual(1, mutations[0].addedNodes.length);
|
||||||
|
testing.expectEqual(textNode, mutations[0].addedNodes[0]);
|
||||||
|
testing.expectEqual(null, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[0].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="childlist_remove_middle">
|
||||||
|
testing.async(async () => {
|
||||||
|
const middleParent = document.getElementById('middle-parent');
|
||||||
|
const first = document.getElementById('first');
|
||||||
|
const middle = document.getElementById('middle');
|
||||||
|
const last = document.getElementById('last');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(middleParent, { childList: true });
|
||||||
|
|
||||||
|
middleParent.removeChild(middle);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(1, mutations.length, {script_id: 'childlist_remove_middle'});
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(middleParent, mutations[0].target);
|
||||||
|
testing.expectEqual(0, mutations[0].addedNodes.length);
|
||||||
|
testing.expectEqual(1, mutations[0].removedNodes.length);
|
||||||
|
testing.expectEqual(middle, mutations[0].removedNodes[0]);
|
||||||
|
testing.expectEqual(first, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(last, mutations[0].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="insert-parent"><div id="insert-first">First</div><div id="insert-last">Last</div></div>
|
||||||
|
|
||||||
|
<script id="childlist_insert_before">
|
||||||
|
testing.async(async () => {
|
||||||
|
const insertParent = document.getElementById('insert-parent');
|
||||||
|
const insertFirst = document.getElementById('insert-first');
|
||||||
|
const insertLast = document.getElementById('insert-last');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(insertParent, { childList: true });
|
||||||
|
|
||||||
|
const insertMiddle = document.createElement('div');
|
||||||
|
insertMiddle.textContent = 'Middle';
|
||||||
|
insertParent.insertBefore(insertMiddle, insertLast);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(1, mutations.length, {script_id: 'childlist_insert_before'});
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(insertParent, mutations[0].target);
|
||||||
|
testing.expectEqual(1, mutations[0].addedNodes.length);
|
||||||
|
testing.expectEqual(insertMiddle, mutations[0].addedNodes[0]);
|
||||||
|
testing.expectEqual(0, mutations[0].removedNodes.length);
|
||||||
|
testing.expectEqual(insertFirst, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(insertLast, mutations[0].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="replace-parent"><div id="replace-old">Old</div></div>
|
||||||
|
|
||||||
|
<script id="childlist_replace_child">
|
||||||
|
testing.async(async () => {
|
||||||
|
const replaceParent = document.getElementById('replace-parent');
|
||||||
|
const replaceOld = document.getElementById('replace-old');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(replaceParent, { childList: true });
|
||||||
|
|
||||||
|
const replaceNew = document.createElement('div');
|
||||||
|
replaceNew.textContent = 'New';
|
||||||
|
replaceParent.replaceChild(replaceNew, replaceOld);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
// replaceChild generates two separate mutation records in modern spec:
|
||||||
|
// 1. First record for insertBefore (new node added)
|
||||||
|
// 2. Second record for removeChild (old node removed)
|
||||||
|
testing.expectEqual(2, mutations.length, {script_id: 'childlist_replace_child'});
|
||||||
|
|
||||||
|
// First mutation: insertion of new node
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(replaceParent, mutations[0].target);
|
||||||
|
testing.expectEqual(1, mutations[0].addedNodes.length);
|
||||||
|
testing.expectEqual(replaceNew, mutations[0].addedNodes[0]);
|
||||||
|
testing.expectEqual(0, mutations[0].removedNodes.length);
|
||||||
|
testing.expectEqual(null, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(replaceOld, mutations[0].nextSibling);
|
||||||
|
|
||||||
|
// Second mutation: removal of old node
|
||||||
|
testing.expectEqual('childList', mutations[1].type);
|
||||||
|
testing.expectEqual(replaceParent, mutations[1].target);
|
||||||
|
testing.expectEqual(0, mutations[1].addedNodes.length);
|
||||||
|
testing.expectEqual(1, mutations[1].removedNodes.length);
|
||||||
|
testing.expectEqual(replaceOld, mutations[1].removedNodes[0]);
|
||||||
|
testing.expectEqual(replaceNew, mutations[1].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[1].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="multiple-parent"></div>
|
||||||
|
|
||||||
|
<script id="childlist_multiple_mutations">
|
||||||
|
testing.async(async () => {
|
||||||
|
const multipleParent = document.getElementById('multiple-parent');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(multipleParent, { childList: true });
|
||||||
|
|
||||||
|
const child1 = document.createElement('div');
|
||||||
|
child1.textContent = 'Child 1';
|
||||||
|
multipleParent.appendChild(child1);
|
||||||
|
|
||||||
|
const child2 = document.createElement('div');
|
||||||
|
child2.textContent = 'Child 2';
|
||||||
|
multipleParent.appendChild(child2);
|
||||||
|
|
||||||
|
const child3 = document.createElement('div');
|
||||||
|
child3.textContent = 'Child 3';
|
||||||
|
multipleParent.appendChild(child3);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(3, mutations.length, {script_id: 'childlist_multiple_mutations'});
|
||||||
|
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(child1, mutations[0].addedNodes[0]);
|
||||||
|
testing.expectEqual(null, mutations[0].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[0].nextSibling);
|
||||||
|
|
||||||
|
testing.expectEqual('childList', mutations[1].type);
|
||||||
|
testing.expectEqual(child2, mutations[1].addedNodes[0]);
|
||||||
|
testing.expectEqual(child1, mutations[1].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[1].nextSibling);
|
||||||
|
|
||||||
|
testing.expectEqual('childList', mutations[2].type);
|
||||||
|
testing.expectEqual(child3, mutations[2].addedNodes[0]);
|
||||||
|
testing.expectEqual(child2, mutations[2].previousSibling);
|
||||||
|
testing.expectEqual(null, mutations[2].nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="inner-html-parent"><div>Old 1</div><div>Old 2</div><div>Old 3</div></div>
|
||||||
|
|
||||||
|
<script id="childlist_inner_html">
|
||||||
|
testing.async(async () => {
|
||||||
|
const innerHtmlParent = document.getElementById('inner-html-parent');
|
||||||
|
const oldChildren = Array.from(innerHtmlParent.children);
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(innerHtmlParent, { childList: true });
|
||||||
|
|
||||||
|
// No whitespace between tags to avoid text node mutations
|
||||||
|
innerHtmlParent.innerHTML = '<span>New 1</span><span>New 2</span><span>New 3</span>';
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
// innerHTML triggers mutations for both removals and additions
|
||||||
|
// With tri-state: from_parser=true + parse_mode=fragment -> mutations fire
|
||||||
|
// HTML wrapper element is filtered out, so: 3 removals + 3 additions = 6
|
||||||
|
testing.expectEqual(6, mutations.length, {script_id: 'childlist_inner_html'});
|
||||||
|
|
||||||
|
// First 3: removals
|
||||||
|
testing.expectEqual('childList', mutations[0].type);
|
||||||
|
testing.expectEqual(1, mutations[0].removedNodes.length);
|
||||||
|
testing.expectEqual(oldChildren[0], mutations[0].removedNodes[0]);
|
||||||
|
testing.expectEqual(0, mutations[0].addedNodes.length);
|
||||||
|
|
||||||
|
testing.expectEqual('childList', mutations[1].type);
|
||||||
|
testing.expectEqual(1, mutations[1].removedNodes.length);
|
||||||
|
testing.expectEqual(oldChildren[1], mutations[1].removedNodes[0]);
|
||||||
|
testing.expectEqual(0, mutations[1].addedNodes.length);
|
||||||
|
|
||||||
|
testing.expectEqual('childList', mutations[2].type);
|
||||||
|
testing.expectEqual(1, mutations[2].removedNodes.length);
|
||||||
|
testing.expectEqual(oldChildren[2], mutations[2].removedNodes[0]);
|
||||||
|
testing.expectEqual(0, mutations[2].addedNodes.length);
|
||||||
|
|
||||||
|
// Last 3: additions (unwrapped span elements)
|
||||||
|
testing.expectEqual('childList', mutations[3].type);
|
||||||
|
testing.expectEqual(0, mutations[3].removedNodes.length);
|
||||||
|
testing.expectEqual(1, mutations[3].addedNodes.length);
|
||||||
|
testing.expectEqual('SPAN', mutations[3].addedNodes[0].nodeName);
|
||||||
|
|
||||||
|
testing.expectEqual('childList', mutations[4].type);
|
||||||
|
testing.expectEqual(0, mutations[4].removedNodes.length);
|
||||||
|
testing.expectEqual(1, mutations[4].addedNodes.length);
|
||||||
|
testing.expectEqual('SPAN', mutations[4].addedNodes[0].nodeName);
|
||||||
|
|
||||||
|
testing.expectEqual('childList', mutations[5].type);
|
||||||
|
testing.expectEqual(0, mutations[5].removedNodes.length);
|
||||||
|
testing.expectEqual(1, mutations[5].addedNodes.length);
|
||||||
|
testing.expectEqual('SPAN', mutations[5].addedNodes[0].nodeName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
47
src/browser/tests/mutation_observer/multiple_observers.html
Normal file
47
src/browser/tests/mutation_observer/multiple_observers.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id="target">Test</div>
|
||||||
|
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id="multiple_observers">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('target');
|
||||||
|
|
||||||
|
let records1 = null;
|
||||||
|
let records2 = null;
|
||||||
|
let callbackCount = 0;
|
||||||
|
|
||||||
|
const observer1 = new MutationObserver((records) => {
|
||||||
|
records1 = records;
|
||||||
|
observer1.disconnect();
|
||||||
|
callbackCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const observer2 = new MutationObserver((records) => {
|
||||||
|
records2 = records;
|
||||||
|
observer2.disconnect();
|
||||||
|
callbackCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
observer1.observe(element, { attributes: true });
|
||||||
|
observer2.observe(element, { attributes: true, attributeOldValue: true });
|
||||||
|
|
||||||
|
element.setAttribute('data-foo', 'bar');
|
||||||
|
element.setAttribute('data-foo', 'baz');
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(2, callbackCount);
|
||||||
|
testing.expectEqual(2, records1.length);
|
||||||
|
testing.expectEqual(2, records2.length);
|
||||||
|
|
||||||
|
testing.expectEqual('data-foo', records1[0].attributeName);
|
||||||
|
testing.expectEqual(null, records1[0].oldValue);
|
||||||
|
testing.expectEqual('data-foo', records1[1].attributeName);
|
||||||
|
testing.expectEqual(null, records1[1].oldValue);
|
||||||
|
|
||||||
|
testing.expectEqual('data-foo', records2[0].attributeName);
|
||||||
|
testing.expectEqual(null, records2[0].oldValue);
|
||||||
|
testing.expectEqual('data-foo', records2[1].attributeName);
|
||||||
|
testing.expectEqual('bar', records2[1].oldValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
114
src/browser/tests/mutation_observer/mutation_observer.html
Normal file
114
src/browser/tests/mutation_observer/mutation_observer.html
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id="test-element-1" class="initial">Test</div>
|
||||||
|
<div id="test-element-2">Test</div>
|
||||||
|
<div id="test-element-3">Test</div>
|
||||||
|
<div id="test-element-4">Test</div>
|
||||||
|
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id="mutation_observer">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('test-element-1');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(element, { attributes: true });
|
||||||
|
|
||||||
|
element.setAttribute('data-foo', 'bar');
|
||||||
|
element.setAttribute('class', 'changed');
|
||||||
|
element.removeAttribute('data-foo');
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(3, mutations.length, {script_id: 'mutation_observer'});
|
||||||
|
|
||||||
|
testing.expectEqual('attributes', mutations[0].type);
|
||||||
|
testing.expectEqual(element, mutations[0].target);
|
||||||
|
testing.expectEqual('data-foo', mutations[0].attributeName);
|
||||||
|
testing.expectEqual(null, mutations[0].oldValue);
|
||||||
|
|
||||||
|
testing.expectEqual('attributes', mutations[1].type);
|
||||||
|
testing.expectEqual(element, mutations[1].target);
|
||||||
|
testing.expectEqual('class', mutations[1].attributeName);
|
||||||
|
testing.expectEqual(null, mutations[1].oldValue);
|
||||||
|
|
||||||
|
testing.expectEqual('attributes', mutations[2].type);
|
||||||
|
testing.expectEqual(element, mutations[2].target);
|
||||||
|
testing.expectEqual('data-foo', mutations[2].attributeName);
|
||||||
|
testing.expectEqual(null, mutations[2].oldValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="mutation_observer_old_value">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('test-element-2');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
observer.observe(element, { attributes: true, attributeOldValue: true });
|
||||||
|
|
||||||
|
element.setAttribute('data-test', 'value1');
|
||||||
|
element.setAttribute('data-test', 'value2');
|
||||||
|
element.removeAttribute('data-test');
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(3, mutations.length, {script_id: 'mutation_observer_old_value'});
|
||||||
|
|
||||||
|
testing.expectEqual('data-test', mutations[0].attributeName);
|
||||||
|
testing.expectEqual(null, mutations[0].oldValue);
|
||||||
|
|
||||||
|
testing.expectEqual('data-test', mutations[1].attributeName);
|
||||||
|
testing.expectEqual('value1', mutations[1].oldValue);
|
||||||
|
|
||||||
|
testing.expectEqual('data-test', mutations[2].attributeName);
|
||||||
|
testing.expectEqual('value2', mutations[2].oldValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="mutation_observer_disconnect">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('test-element-3');
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(element, { attributes: true });
|
||||||
|
observer.disconnect();
|
||||||
|
|
||||||
|
element.setAttribute('data-disconnected', 'test');
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(false, callbackCalled, {script_id: 'mutation_observer_disconnect'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="mutation_observer_take_records">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('test-element-4');
|
||||||
|
|
||||||
|
let callbackCalled = false;
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(element, { attributes: true });
|
||||||
|
element.setAttribute('data-take', 'records');
|
||||||
|
|
||||||
|
const taken = observer.takeRecords();
|
||||||
|
testing.expectEqual(1, taken.length);
|
||||||
|
testing.expectEqual('data-take', taken[0].attributeName);
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(false, callbackCalled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id="target">Test</div>
|
||||||
|
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id="mutations_during_callback">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('target');
|
||||||
|
|
||||||
|
let callCount = 0;
|
||||||
|
let firstRecords = null;
|
||||||
|
let secondRecords = null;
|
||||||
|
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
callCount++;
|
||||||
|
if (callCount === 1) {
|
||||||
|
firstRecords = records;
|
||||||
|
element.setAttribute('data-second', 'from-callback');
|
||||||
|
} else if (callCount === 2) {
|
||||||
|
secondRecords = records;
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(element, { attributes: true });
|
||||||
|
|
||||||
|
element.setAttribute('data-first', 'initial');
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
// After first microtask, first callback should have run and triggered second mutation
|
||||||
|
}).then(() => {
|
||||||
|
// After second microtask, second callback should have run
|
||||||
|
testing.expectEqual(2, callCount, {script_id: 'mutations_during_callback'});
|
||||||
|
testing.expectEqual(1, firstRecords.length);
|
||||||
|
testing.expectEqual('data-first', firstRecords[0].attributeName);
|
||||||
|
testing.expectEqual(1, secondRecords.length);
|
||||||
|
testing.expectEqual('data-second', secondRecords[0].attributeName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id="target1">Test1</div>
|
||||||
|
<div id="target2">Test2</div>
|
||||||
|
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id="observe_multiple_targets">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element1 = document.getElementById('target1');
|
||||||
|
const element2 = document.getElementById('target2');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(element1, { attributes: true });
|
||||||
|
observer.observe(element2, { attributes: true });
|
||||||
|
|
||||||
|
element1.setAttribute('data-one', 'value1');
|
||||||
|
element2.setAttribute('data-two', 'value2');
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(2, mutations.length);
|
||||||
|
testing.expectEqual(element1, mutations[0].target);
|
||||||
|
testing.expectEqual('data-one', mutations[0].attributeName);
|
||||||
|
testing.expectEqual(element2, mutations[1].target);
|
||||||
|
testing.expectEqual('data-two', mutations[1].attributeName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<div id="target">Test</div>
|
||||||
|
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id="reobserve_same_target">
|
||||||
|
testing.async(async () => {
|
||||||
|
const element = document.getElementById('target');
|
||||||
|
|
||||||
|
let mutations = null;
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
observer.disconnect();
|
||||||
|
mutations = records;
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(element, { attributes: true, attributeOldValue: false });
|
||||||
|
observer.observe(element, { attributes: true, attributeOldValue: true });
|
||||||
|
|
||||||
|
element.setAttribute('data-foo', 'value1');
|
||||||
|
element.setAttribute('data-foo', 'value2');
|
||||||
|
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
testing.expectEqual(2, mutations.length);
|
||||||
|
testing.expectEqual(null, mutations[0].oldValue);
|
||||||
|
testing.expectEqual('value1', mutations[1].oldValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -61,11 +61,15 @@ pub fn getData(self: *const CData) []const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void {
|
pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void {
|
||||||
|
const old_value = self._data;
|
||||||
|
|
||||||
if (value) |v| {
|
if (value) |v| {
|
||||||
self._data = try page.dupeString(v);
|
self._data = try page.dupeString(v);
|
||||||
} else {
|
} else {
|
||||||
self._data = "";
|
self._data = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page.characterDataChange(self.asNode(), old_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(self: *const CData, writer: *std.io.Writer) !void {
|
pub fn format(self: *const CData, writer: *std.io.Writer) !void {
|
||||||
|
|||||||
342
src/browser/webapi/IntersectionObserver.zig
Normal file
342
src/browser/webapi/IntersectionObserver.zig
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
// 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 Page = @import("../Page.zig");
|
||||||
|
const Element = @import("Element.zig");
|
||||||
|
const DOMRect = @import("DOMRect.zig");
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{
|
||||||
|
IntersectionObserver,
|
||||||
|
IntersectionObserverEntry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntersectionObserver = @This();
|
||||||
|
|
||||||
|
_callback: js.Function,
|
||||||
|
_observing: std.ArrayList(*Element) = .{},
|
||||||
|
_root: ?*Element = null,
|
||||||
|
_root_margin: []const u8 = "0px",
|
||||||
|
_threshold: []const f64 = &.{0.0},
|
||||||
|
_pending_entries: std.ArrayList(*IntersectionObserverEntry) = .{},
|
||||||
|
_previous_states: std.AutoHashMapUnmanaged(*Element, bool) = .{},
|
||||||
|
|
||||||
|
// Shared zero DOMRect to avoid repeated allocations for non-intersecting elements
|
||||||
|
var zero_rect: DOMRect = .{
|
||||||
|
._x = 0.0,
|
||||||
|
._y = 0.0,
|
||||||
|
._width = 0.0,
|
||||||
|
._height = 0.0,
|
||||||
|
._top = 0.0,
|
||||||
|
._right = 0.0,
|
||||||
|
._bottom = 0.0,
|
||||||
|
._left = 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ObserverInit = struct {
|
||||||
|
root: ?*Element = null,
|
||||||
|
rootMargin: ?[]const u8 = null,
|
||||||
|
threshold: []const f64 = &.{0.0},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(callback: js.Function, options: ?ObserverInit, page: *Page) !*IntersectionObserver {
|
||||||
|
const opts = options orelse ObserverInit{};
|
||||||
|
const root_margin = if (opts.rootMargin) |rm| try page.arena.dupe(u8, rm) else "0px";
|
||||||
|
|
||||||
|
return page._factory.create(IntersectionObserver{
|
||||||
|
._callback = callback,
|
||||||
|
._root = opts.root,
|
||||||
|
._root_margin = root_margin,
|
||||||
|
._threshold = try page.arena.dupe(f64, opts.threshold),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
||||||
|
// Check if already observing this target
|
||||||
|
for (self._observing.items) |elem| {
|
||||||
|
if (elem == target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with page if this is our first observation
|
||||||
|
if (self._observing.items.len == 0) {
|
||||||
|
try page.registerIntersectionObserver(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self._observing.append(page.arena, target);
|
||||||
|
|
||||||
|
// Don't initialize previous state yet - let checkIntersection do it
|
||||||
|
// This ensures we get an entry on first observation
|
||||||
|
|
||||||
|
// Check intersection for this new target and schedule delivery
|
||||||
|
try self.checkIntersection(target, page);
|
||||||
|
if (self._pending_entries.items.len > 0) {
|
||||||
|
try page.scheduleIntersectionDelivery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unobserve(self: *IntersectionObserver, target: *Element) void {
|
||||||
|
for (self._observing.items, 0..) |elem, i| {
|
||||||
|
if (elem == target) {
|
||||||
|
_ = self._observing.swapRemove(i);
|
||||||
|
_ = self._previous_states.remove(target);
|
||||||
|
|
||||||
|
// Remove any pending entries for this target
|
||||||
|
var j: usize = 0;
|
||||||
|
while (j < self._pending_entries.items.len) {
|
||||||
|
if (self._pending_entries.items[j]._target == target) {
|
||||||
|
_ = self._pending_entries.swapRemove(j);
|
||||||
|
} else {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
||||||
|
page.unregisterIntersectionObserver(self);
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
|
self._previous_states.clearRetainingCapacity();
|
||||||
|
self._pending_entries.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
||||||
|
const entries = try page.call_arena.dupe(*IntersectionObserverEntry, self._pending_entries.items);
|
||||||
|
self._pending_entries.clearRetainingCapacity();
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculateIntersection(
|
||||||
|
self: *IntersectionObserver,
|
||||||
|
target: *Element,
|
||||||
|
page: *Page,
|
||||||
|
) !IntersectionData {
|
||||||
|
const target_rect = try target.getBoundingClientRect(page);
|
||||||
|
|
||||||
|
// Use root element's rect or viewport (simplified: assume infinite viewport)
|
||||||
|
const root_rect = if (self._root) |root|
|
||||||
|
try root.getBoundingClientRect(page)
|
||||||
|
else
|
||||||
|
// Simplified viewport - assume 1920x1080 for now
|
||||||
|
try page._factory.create(DOMRect{
|
||||||
|
._x = 0.0,
|
||||||
|
._y = 0.0,
|
||||||
|
._width = 1920.0,
|
||||||
|
._height = 1080.0,
|
||||||
|
._top = 0.0,
|
||||||
|
._right = 1920.0,
|
||||||
|
._bottom = 1080.0,
|
||||||
|
._left = 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate intersection rectangle
|
||||||
|
const left = @max(target_rect._left, root_rect._left);
|
||||||
|
const top = @max(target_rect._top, root_rect._top);
|
||||||
|
const right = @min(target_rect._right, root_rect._right);
|
||||||
|
const bottom = @min(target_rect._bottom, root_rect._bottom);
|
||||||
|
|
||||||
|
const is_intersecting = left < right and top < bottom;
|
||||||
|
|
||||||
|
var intersection_rect: ?*DOMRect = null;
|
||||||
|
var intersection_ratio: f64 = 0.0;
|
||||||
|
|
||||||
|
if (is_intersecting) {
|
||||||
|
const width = right - left;
|
||||||
|
const height = bottom - top;
|
||||||
|
const intersection_area = width * height;
|
||||||
|
const target_area = target_rect._width * target_rect._height;
|
||||||
|
|
||||||
|
if (target_area > 0) {
|
||||||
|
intersection_ratio = intersection_area / target_area;
|
||||||
|
}
|
||||||
|
|
||||||
|
intersection_rect = try page._factory.create(DOMRect{
|
||||||
|
._x = left,
|
||||||
|
._y = top,
|
||||||
|
._width = width,
|
||||||
|
._height = height,
|
||||||
|
._top = top,
|
||||||
|
._right = right,
|
||||||
|
._bottom = bottom,
|
||||||
|
._left = left,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No intersection - reuse shared zero rect to avoid allocation
|
||||||
|
intersection_rect = &zero_rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.is_intersecting = is_intersecting,
|
||||||
|
.intersection_ratio = intersection_ratio,
|
||||||
|
.intersection_rect = intersection_rect.?,
|
||||||
|
.bounding_client_rect = target_rect,
|
||||||
|
.root_bounds = root_rect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntersectionData = struct {
|
||||||
|
is_intersecting: bool,
|
||||||
|
intersection_ratio: f64,
|
||||||
|
intersection_rect: *DOMRect,
|
||||||
|
bounding_client_rect: *DOMRect,
|
||||||
|
root_bounds: *DOMRect,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn meetsThreshold(self: *IntersectionObserver, ratio: f64) bool {
|
||||||
|
for (self._threshold) |threshold| {
|
||||||
|
if (ratio >= threshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
||||||
|
const data = try self.calculateIntersection(target, page);
|
||||||
|
const was_intersecting_opt = self._previous_states.get(target);
|
||||||
|
const is_now_intersecting = data.is_intersecting and self.meetsThreshold(data.intersection_ratio);
|
||||||
|
|
||||||
|
// Create entry if:
|
||||||
|
// 1. First time observing this target (was_intersecting_opt == null)
|
||||||
|
// 2. State changed
|
||||||
|
// 3. Currently intersecting
|
||||||
|
const should_report = was_intersecting_opt == null or
|
||||||
|
was_intersecting_opt.? != is_now_intersecting;
|
||||||
|
|
||||||
|
if (should_report) {
|
||||||
|
const entry = try page.arena.create(IntersectionObserverEntry);
|
||||||
|
entry.* = .{
|
||||||
|
._target = target,
|
||||||
|
._time = 0.0, // TODO: Get actual timestamp
|
||||||
|
._bounding_client_rect = data.bounding_client_rect,
|
||||||
|
._intersection_rect = data.intersection_rect,
|
||||||
|
._root_bounds = data.root_bounds,
|
||||||
|
._intersection_ratio = data.intersection_ratio,
|
||||||
|
._is_intersecting = is_now_intersecting,
|
||||||
|
};
|
||||||
|
|
||||||
|
try self._pending_entries.append(page.arena, entry);
|
||||||
|
try self._previous_states.put(page.arena, target, is_now_intersecting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {
|
||||||
|
if (self._observing.items.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self._observing.items) |target| {
|
||||||
|
try self.checkIntersection(target, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self._pending_entries.items.len > 0) {
|
||||||
|
try page.scheduleIntersectionDelivery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
|
||||||
|
if (self._pending_entries.items.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = try self.takeRecords(page);
|
||||||
|
try self._callback.call(void, .{ entries, self });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const IntersectionObserverEntry = struct {
|
||||||
|
_target: *Element,
|
||||||
|
_time: f64,
|
||||||
|
_bounding_client_rect: *DOMRect,
|
||||||
|
_intersection_rect: *DOMRect,
|
||||||
|
_root_bounds: *DOMRect,
|
||||||
|
_intersection_ratio: f64,
|
||||||
|
_is_intersecting: bool,
|
||||||
|
|
||||||
|
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
|
||||||
|
return self._target;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTime(self: *const IntersectionObserverEntry) f64 {
|
||||||
|
return self._time;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getBoundingClientRect(self: *const IntersectionObserverEntry) *DOMRect {
|
||||||
|
return self._bounding_client_rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIntersectionRect(self: *const IntersectionObserverEntry) *DOMRect {
|
||||||
|
return self._intersection_rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getRootBounds(self: *const IntersectionObserverEntry) ?*DOMRect {
|
||||||
|
return self._root_bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIntersectionRatio(self: *const IntersectionObserverEntry) f64 {
|
||||||
|
return self._intersection_ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIsIntersecting(self: *const IntersectionObserverEntry) bool {
|
||||||
|
return self._is_intersecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(IntersectionObserverEntry);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "IntersectionObserverEntry";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});
|
||||||
|
pub const time = bridge.accessor(IntersectionObserverEntry.getTime, null, .{});
|
||||||
|
pub const boundingClientRect = bridge.accessor(IntersectionObserverEntry.getBoundingClientRect, null, .{});
|
||||||
|
pub const intersectionRect = bridge.accessor(IntersectionObserverEntry.getIntersectionRect, null, .{});
|
||||||
|
pub const rootBounds = bridge.accessor(IntersectionObserverEntry.getRootBounds, null, .{});
|
||||||
|
pub const intersectionRatio = bridge.accessor(IntersectionObserverEntry.getIntersectionRatio, null, .{});
|
||||||
|
pub const isIntersecting = bridge.accessor(IntersectionObserverEntry.getIsIntersecting, null, .{});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(IntersectionObserver);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "IntersectionObserver";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(init, .{});
|
||||||
|
|
||||||
|
pub const observe = bridge.function(IntersectionObserver.observe, .{});
|
||||||
|
pub const unobserve = bridge.function(IntersectionObserver.unobserve, .{});
|
||||||
|
pub const disconnect = bridge.function(IntersectionObserver.disconnect, .{});
|
||||||
|
pub const takeRecords = bridge.function(IntersectionObserver.takeRecords, .{});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "WebApi: IntersectionObserver" {
|
||||||
|
try testing.htmlRunner("intersection_observer", .{});
|
||||||
|
}
|
||||||
@@ -16,18 +16,271 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
const Node = @import("Node.zig");
|
||||||
|
const Element = @import("Element.zig");
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{
|
||||||
|
MutationObserver,
|
||||||
|
MutationRecord,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// @ZIGDOM (haha, bet you wish you hadn't opened this file)
|
|
||||||
// puppeteer's startup script creates a MutationObserver, even if it doesn't use
|
|
||||||
// it in simple scripts. This not-even-a-skeleton is required for puppeteer/cdp.js
|
|
||||||
// to run
|
|
||||||
const MutationObserver = @This();
|
const MutationObserver = @This();
|
||||||
|
|
||||||
pub fn init() MutationObserver {
|
_callback: js.Function,
|
||||||
return .{};
|
_observing: std.ArrayList(Observing) = .{},
|
||||||
|
_pending_records: std.ArrayList(*MutationRecord) = .{},
|
||||||
|
|
||||||
|
const Observing = struct {
|
||||||
|
target: *Node,
|
||||||
|
options: ObserveOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ObserveOptions = struct {
|
||||||
|
attributes: bool = false,
|
||||||
|
attributeOldValue: bool = false,
|
||||||
|
childList: bool = false,
|
||||||
|
characterData: bool = false,
|
||||||
|
characterDataOldValue: bool = false,
|
||||||
|
// Future: subtree, attributeFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(callback: js.Function, page: *Page) !*MutationObserver {
|
||||||
|
return page._factory.create(MutationObserver{
|
||||||
|
._callback = callback,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
||||||
|
// Check if already observing this target
|
||||||
|
for (self._observing.items) |*obs| {
|
||||||
|
if (obs.target == target) {
|
||||||
|
obs.options = options;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with page if this is our first observation
|
||||||
|
if (self._observing.items.len == 0) {
|
||||||
|
try page.registerMutationObserver(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self._observing.append(page.arena, .{
|
||||||
|
.target = target,
|
||||||
|
.options = options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
||||||
|
page.unregisterMutationObserver(self);
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
|
self._pending_records.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
||||||
|
const records = try page.call_arena.dupe(*MutationRecord, self._pending_records.items);
|
||||||
|
self._pending_records.clearRetainingCapacity();
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when an attribute changes on any element
|
||||||
|
pub fn notifyAttributeChange(
|
||||||
|
self: *MutationObserver,
|
||||||
|
target: *Element,
|
||||||
|
attribute_name: []const u8,
|
||||||
|
old_value: ?[]const u8,
|
||||||
|
page: *Page,
|
||||||
|
) !void {
|
||||||
|
const target_node = target.asNode();
|
||||||
|
|
||||||
|
for (self._observing.items) |obs| {
|
||||||
|
if (obs.target != target_node) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!obs.options.attributes) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = try page._factory.create(MutationRecord{
|
||||||
|
._type = .attributes,
|
||||||
|
._target = target_node,
|
||||||
|
._attribute_name = try page.arena.dupe(u8, attribute_name),
|
||||||
|
._old_value = if (obs.options.attributeOldValue and old_value != null)
|
||||||
|
try page.arena.dupe(u8, old_value.?)
|
||||||
|
else
|
||||||
|
null,
|
||||||
|
._added_nodes = &.{},
|
||||||
|
._removed_nodes = &.{},
|
||||||
|
._previous_sibling = null,
|
||||||
|
._next_sibling = null,
|
||||||
|
});
|
||||||
|
|
||||||
|
try self._pending_records.append(page.arena, record);
|
||||||
|
|
||||||
|
try page.scheduleMutationDelivery();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when character data changes on a text node
|
||||||
|
pub fn notifyCharacterDataChange(
|
||||||
|
self: *MutationObserver,
|
||||||
|
target: *Node,
|
||||||
|
old_value: ?[]const u8,
|
||||||
|
page: *Page,
|
||||||
|
) !void {
|
||||||
|
for (self._observing.items) |obs| {
|
||||||
|
if (obs.target != target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!obs.options.characterData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = try page._factory.create(MutationRecord{
|
||||||
|
._type = .characterData,
|
||||||
|
._target = target,
|
||||||
|
._attribute_name = null,
|
||||||
|
._old_value = if (obs.options.characterDataOldValue and old_value != null)
|
||||||
|
try page.arena.dupe(u8, old_value.?)
|
||||||
|
else
|
||||||
|
null,
|
||||||
|
._added_nodes = &.{},
|
||||||
|
._removed_nodes = &.{},
|
||||||
|
._previous_sibling = null,
|
||||||
|
._next_sibling = null,
|
||||||
|
});
|
||||||
|
|
||||||
|
try self._pending_records.append(page.arena, record);
|
||||||
|
|
||||||
|
try page.scheduleMutationDelivery();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when children are added or removed from a node
|
||||||
|
pub fn notifyChildListChange(
|
||||||
|
self: *MutationObserver,
|
||||||
|
target: *Node,
|
||||||
|
added_nodes: []const *Node,
|
||||||
|
removed_nodes: []const *Node,
|
||||||
|
previous_sibling: ?*Node,
|
||||||
|
next_sibling: ?*Node,
|
||||||
|
page: *Page,
|
||||||
|
) !void {
|
||||||
|
for (self._observing.items) |obs| {
|
||||||
|
if (obs.target != target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!obs.options.childList) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = try page._factory.create(MutationRecord{
|
||||||
|
._type = .childList,
|
||||||
|
._target = target,
|
||||||
|
._attribute_name = null,
|
||||||
|
._old_value = null,
|
||||||
|
._added_nodes = try page.arena.dupe(*Node, added_nodes),
|
||||||
|
._removed_nodes = try page.arena.dupe(*Node, removed_nodes),
|
||||||
|
._previous_sibling = previous_sibling,
|
||||||
|
._next_sibling = next_sibling,
|
||||||
|
});
|
||||||
|
|
||||||
|
try self._pending_records.append(page.arena, record);
|
||||||
|
|
||||||
|
try page.scheduleMutationDelivery();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
|
||||||
|
if (self._pending_records.items.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take a copy of the records and clear the list before calling callback
|
||||||
|
// This ensures mutations triggered during the callback go into a fresh list
|
||||||
|
const records = try self.takeRecords(page);
|
||||||
|
try self._callback.call(void, .{ records, self });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MutationRecord = struct {
|
||||||
|
_type: Type,
|
||||||
|
_target: *Node,
|
||||||
|
_attribute_name: ?[]const u8,
|
||||||
|
_old_value: ?[]const u8,
|
||||||
|
_added_nodes: []const *Node,
|
||||||
|
_removed_nodes: []const *Node,
|
||||||
|
_previous_sibling: ?*Node,
|
||||||
|
_next_sibling: ?*Node,
|
||||||
|
|
||||||
|
pub const Type = enum {
|
||||||
|
attributes,
|
||||||
|
childList,
|
||||||
|
characterData,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getType(self: *const MutationRecord) []const u8 {
|
||||||
|
return switch (self._type) {
|
||||||
|
.attributes => "attributes",
|
||||||
|
.childList => "childList",
|
||||||
|
.characterData => "characterData",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTarget(self: *const MutationRecord) *Node {
|
||||||
|
return self._target;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getAttributeName(self: *const MutationRecord) ?[]const u8 {
|
||||||
|
return self._attribute_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOldValue(self: *const MutationRecord) ?[]const u8 {
|
||||||
|
return self._old_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getAddedNodes(self: *const MutationRecord) []const *Node {
|
||||||
|
return self._added_nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getRemovedNodes(self: *const MutationRecord) []const *Node {
|
||||||
|
return self._removed_nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPreviousSibling(self: *const MutationRecord) ?*Node {
|
||||||
|
return self._previous_sibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getNextSibling(self: *const MutationRecord) ?*Node {
|
||||||
|
return self._next_sibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(MutationRecord);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "MutationRecord";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
|
||||||
|
pub const target = bridge.accessor(MutationRecord.getTarget, null, .{});
|
||||||
|
pub const attributeName = bridge.accessor(MutationRecord.getAttributeName, null, .{});
|
||||||
|
pub const oldValue = bridge.accessor(MutationRecord.getOldValue, null, .{});
|
||||||
|
pub const addedNodes = bridge.accessor(MutationRecord.getAddedNodes, null, .{});
|
||||||
|
pub const removedNodes = bridge.accessor(MutationRecord.getRemovedNodes, null, .{});
|
||||||
|
pub const previousSibling = bridge.accessor(MutationRecord.getPreviousSibling, null, .{});
|
||||||
|
pub const nextSibling = bridge.accessor(MutationRecord.getNextSibling, null, .{});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(MutationObserver);
|
pub const bridge = js.Bridge(MutationObserver);
|
||||||
|
|
||||||
@@ -38,4 +291,13 @@ pub const JsApi = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const constructor = bridge.constructor(MutationObserver.init, .{});
|
pub const constructor = bridge.constructor(MutationObserver.init, .{});
|
||||||
|
|
||||||
|
pub const observe = bridge.function(MutationObserver.observe, .{});
|
||||||
|
pub const disconnect = bridge.function(MutationObserver.disconnect, .{});
|
||||||
|
pub const takeRecords = bridge.function(MutationObserver.takeRecords, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "WebApi: MutationObserver" {
|
||||||
|
try testing.htmlRunner("mutation_observer", .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ pub const List = struct {
|
|||||||
const is_id = isIdForConnected(result.normalized, element);
|
const is_id = isIdForConnected(result.normalized, element);
|
||||||
|
|
||||||
var entry: *Entry = undefined;
|
var entry: *Entry = undefined;
|
||||||
|
var old_value: ?[]const u8 = null;
|
||||||
if (result.entry) |e| {
|
if (result.entry) |e| {
|
||||||
|
old_value = try page.call_arena.dupe(u8, e._value.str());
|
||||||
if (is_id) {
|
if (is_id) {
|
||||||
_ = page.document._elements_by_id.remove(e._value.str());
|
_ = page.document._elements_by_id.remove(e._value.str());
|
||||||
}
|
}
|
||||||
@@ -174,7 +176,7 @@ pub const List = struct {
|
|||||||
if (is_id) {
|
if (is_id) {
|
||||||
try page.document._elements_by_id.put(page.arena, entry._value.str(), element);
|
try page.document._elements_by_id.put(page.arena, entry._value.str(), element);
|
||||||
}
|
}
|
||||||
page.attributeChange(element, result.normalized, entry._value.str());
|
page.attributeChange(element, result.normalized, entry._value.str(), old_value);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,12 +228,13 @@ pub const List = struct {
|
|||||||
const entry = result.entry orelse return;
|
const entry = result.entry orelse return;
|
||||||
|
|
||||||
const is_id = isIdForConnected(result.normalized, element);
|
const is_id = isIdForConnected(result.normalized, element);
|
||||||
|
const old_value = entry._value.str();
|
||||||
|
|
||||||
if (is_id) {
|
if (is_id) {
|
||||||
_ = page.document._elements_by_id.remove(entry._value.str());
|
_ = page.document._elements_by_id.remove(entry._value.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
page.attributeRemove(element, result.normalized);
|
page.attributeRemove(element, result.normalized, old_value);
|
||||||
_ = page._attribute_lookup.remove(@intFromPtr(entry));
|
_ = page._attribute_lookup.remove(@intFromPtr(entry));
|
||||||
self._list.remove(&entry._node);
|
self._list.remove(&entry._node);
|
||||||
page._factory.destroy(entry);
|
page._factory.destroy(entry);
|
||||||
|
|||||||
@@ -642,6 +642,19 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// debugger events
|
||||||
|
|
||||||
|
|
||||||
|
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {
|
||||||
|
// onRunMessageLoopOnPause is called when a breakpoint is hit.
|
||||||
|
// Until quit pause, we must continue to run a nested message loop
|
||||||
|
// to interact with the the debugger ony (ie. Chrome DevTools).
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {
|
||||||
|
// Quit breakpoint pause.
|
||||||
|
}
|
||||||
|
|
||||||
// This is hacky x 2. First, we create the JSON payload by gluing our
|
// This is hacky x 2. First, we create the JSON payload by gluing our
|
||||||
// session_id onto it. Second, we're much more client/websocket aware than
|
// session_id onto it. Second, we're much more client/websocket aware than
|
||||||
// we should be.
|
// we should be.
|
||||||
|
|||||||
Reference in New Issue
Block a user