Skip to content

Commit 1a7dbd5

Browse files
committed
Dispatch slotchange event
The first time a `slotchange` event is registered, we setup a SlotChangeMonitor on the page. This uses a global (ugh) MutationEvent to detect slot changes. We could improve the perfomance of this by installing a MutationEvent per custom element, but a global is obviously a lot easier. Our MutationEvent currently fired _during_ the changes. This is problematic (in general, but specifically for slotchange). You can image something like: ``` slot.addEventListener('slotchange', () => { // do something with slot.assignedNodes() }); ``` But, if we dispatch the `slotchange` during the MutationEvent, assignedNodes will return old nodes. So, our SlotChangeMonitor uses the page scheduler to schedule dispatches on the next tick.
1 parent db166b4 commit 1a7dbd5

File tree

7 files changed

+345
-20
lines changed

7 files changed

+345
-20
lines changed

src/browser/SlotChangeMonitor.zig

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
const std = @import("std");
2+
3+
const log = @import("../log.zig");
4+
const parser = @import("netsurf.zig");
5+
const collection = @import("dom/html_collection.zig");
6+
7+
const Page = @import("page.zig").Page;
8+
9+
const SlotChangeMonitor = @This();
10+
11+
page: *Page,
12+
event_node: parser.EventNode,
13+
slots_changed: std.ArrayList(*parser.Slot),
14+
15+
// Monitors the document in order to trigger slotchange events.
16+
pub fn init(page: *Page) !*SlotChangeMonitor {
17+
// on the heap, we need a stable address for event_node
18+
const self = try page.arena.create(SlotChangeMonitor);
19+
self.* = .{
20+
.page = page,
21+
.slots_changed = .empty,
22+
.event_node = .{ .func = mutationCallback },
23+
};
24+
const root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document));
25+
26+
_ = try parser.eventTargetAddEventListener(
27+
parser.toEventTarget(parser.Node, root),
28+
"DOMNodeInserted",
29+
&self.event_node,
30+
false,
31+
);
32+
33+
_ = try parser.eventTargetAddEventListener(
34+
parser.toEventTarget(parser.Node, root),
35+
"DOMNodeRemoved",
36+
&self.event_node,
37+
false,
38+
);
39+
40+
_ = try parser.eventTargetAddEventListener(
41+
parser.toEventTarget(parser.Node, root),
42+
"DOMAttrModified",
43+
&self.event_node,
44+
false,
45+
);
46+
47+
return self;
48+
}
49+
50+
// Given a element, finds its slot, if any.
51+
pub fn findSlot(element: *parser.Element, page: *const Page) !?*parser.Slot {
52+
const target_name = (try parser.elementGetAttribute(element, "slot")) orelse return null;
53+
return findNamedSlot(element, target_name, page);
54+
}
55+
56+
// Given an element and a name, find the slo, if any. This is only useful for
57+
// MutationEvents where findSlot is unreliable because parser.elementGetAttribute(element, "slot")
58+
// could return the new or old value.
59+
fn findNamedSlot(element: *parser.Element, target_name: []const u8, page: *const Page) !?*parser.Slot {
60+
// I believe elements need to be added as direct descendents of the host,
61+
// so we don't need to go find the host, we just grab the parent.
62+
const host = parser.nodeParentNode(@ptrCast(element)) orelse return null;
63+
const state = page.getNodeState(host) orelse return null;
64+
const shadow_root = state.shadow_root orelse return null;
65+
66+
// if we're here, we found a host, now find the slot
67+
var nodes = collection.HTMLCollectionByTagName(
68+
@ptrCast(@alignCast(shadow_root.proto)),
69+
"slot",
70+
.{ .include_root = false },
71+
);
72+
for (0..1000) |i| {
73+
const n = (try nodes.item(@intCast(i))) orelse return null;
74+
const slot_name = (try parser.elementGetAttribute(@ptrCast(n), "name")) orelse "";
75+
if (std.mem.eql(u8, target_name, slot_name)) {
76+
return @ptrCast(n);
77+
}
78+
}
79+
return null;
80+
}
81+
82+
// Event callback from the mutation event, signaling either the addition of
83+
// a node, removal of a node, or a change in attribute
84+
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
85+
const mutation_event = parser.eventToMutationEvent(event);
86+
const self: *SlotChangeMonitor = @fieldParentPtr("event_node", en);
87+
self._mutationCallback(mutation_event) catch |err| {
88+
log.err(.web_api, "slot change callback", .{ .err = err });
89+
};
90+
}
91+
92+
fn _mutationCallback(self: *SlotChangeMonitor, event: *parser.MutationEvent) !void {
93+
const event_type = parser.eventType(@ptrCast(event));
94+
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
95+
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
96+
return self.nodeAddedOrRemoved(@ptrCast(event_target));
97+
}
98+
99+
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
100+
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
101+
return self.nodeAddedOrRemoved(@ptrCast(event_target));
102+
}
103+
104+
if (std.mem.eql(u8, event_type, "DOMAttrModified")) {
105+
const attribute_name = try parser.mutationEventAttributeName(event);
106+
if (std.mem.eql(u8, attribute_name, "slot") == false) {
107+
return;
108+
}
109+
110+
const new_value = parser.mutationEventNewValue(event);
111+
const prev_value = parser.mutationEventPrevValue(event);
112+
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
113+
return self.nodeAttributeChanged(@ptrCast(event_target), new_value, prev_value);
114+
}
115+
}
116+
117+
// A node was removed or added. If it's an element, and if it has a slot attribute
118+
// then we'll dispatch a slotchange event.
119+
fn nodeAddedOrRemoved(self: *SlotChangeMonitor, node: *parser.Node) !void {
120+
if (parser.nodeType(node) != .element) {
121+
return;
122+
}
123+
const el: *parser.Element = @ptrCast(node);
124+
if (try findSlot(el, self.page)) |slot| {
125+
return self.scheduleSlotChange(slot);
126+
}
127+
}
128+
129+
// An attribute was modified. If the attribute is "slot", then we'll trigger 1
130+
// slotchange for the old slot (if there was one) and 1 slotchange for the new
131+
// one (if there is one)
132+
fn nodeAttributeChanged(self: *SlotChangeMonitor, node: *parser.Node, new_value: ?[]const u8, prev_value: ?[]const u8) !void {
133+
if (parser.nodeType(node) != .element) {
134+
return;
135+
}
136+
137+
const el: *parser.Element = @ptrCast(node);
138+
if (try findNamedSlot(el, prev_value orelse "", self.page)) |slot| {
139+
try self.scheduleSlotChange(slot);
140+
}
141+
142+
if (try findNamedSlot(el, new_value orelse "", self.page)) |slot| {
143+
try self.scheduleSlotChange(slot);
144+
}
145+
}
146+
147+
// OK. Our MutationEvent is not a MutationObserver - it's an older, deprecated
148+
// API. It gets dispatched in the middle of the change. While I'm sure it has
149+
// some rules, from our point of view, it fires too early. DOMAttrModified fires
150+
// before the attribute is actually updated and DOMNodeRemoved before the node
151+
// is actually removed. This is a problem if the callback will call
152+
// `slot.assignedNodes`, since that won't return the new state.
153+
// So, we use the page schedule to schedule the dispatching of the slotchange
154+
// event.
155+
fn scheduleSlotChange(self: *SlotChangeMonitor, slot: *parser.Slot) !void {
156+
for (self.slots_changed.items) |changed| {
157+
if (slot == changed) {
158+
return;
159+
}
160+
}
161+
162+
try self.slots_changed.append(self.page.arena, slot);
163+
if (self.slots_changed.items.len == 1) {
164+
// first item added, schedule the callback
165+
try self.page.scheduler.add(self, scheduleCallback, 0, .{ .name = "slot change" });
166+
}
167+
}
168+
169+
// Callback from the schedule. Time to dispatch the slotchange event
170+
fn scheduleCallback(ctx: *anyopaque) ?u32 {
171+
var self: *SlotChangeMonitor = @ptrCast(@alignCast(ctx));
172+
self._scheduleCallback() catch |err| {
173+
log.err(.app, "slot change schedule", .{ .err = err });
174+
};
175+
return null;
176+
}
177+
178+
fn _scheduleCallback(self: *SlotChangeMonitor) !void {
179+
for (self.slots_changed.items) |slot| {
180+
const event = try parser.eventCreate();
181+
defer parser.eventDestroy(event);
182+
try parser.eventInit(event, "slotchange", .{});
183+
_ = try parser.eventTargetDispatchEvent(
184+
parser.toEventTarget(parser.Element, @ptrCast(@alignCast(slot))),
185+
event,
186+
);
187+
}
188+
self.slots_changed.clearRetainingCapacity();
189+
}

src/browser/dom/element.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ pub const Element = struct {
136136
return try parser.elementSetAttribute(self, "slot", slot);
137137
}
138138

139+
pub fn get_assignedSlot(self: *parser.Element, page: *const Page) !?*parser.Slot {
140+
return @import("../SlotChangeMonitor.zig").findSlot(self, page);
141+
}
142+
139143
pub fn get_classList(self: *parser.Element) !*parser.TokenList {
140144
return try parser.tokenListCreate(self, "class");
141145
}

src/browser/dom/event_target.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ pub const EventTarget = struct {
100100
page: *Page,
101101
) !void {
102102
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
103+
if (std.mem.eql(u8, typ, "slotchange")) {
104+
try page.registerSlotChangeMonitor();
105+
}
103106
}
104107

105108
const RemoveEventListenerOpts = union(enum) {

src/browser/netsurf.zig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,14 @@ pub fn mutationEventPrevValue(evt: *MutationEvent) ?[]const u8 {
581581
return strToData(s.?);
582582
}
583583

584+
pub fn mutationEventNewValue(evt: *MutationEvent) ?[]const u8 {
585+
var s: ?*String = null;
586+
const err = c._dom_mutation_event_get_new_value(evt, &s);
587+
std.debug.assert(err == c.DOM_NO_ERR);
588+
if (s == null) return null;
589+
return strToData(s.?);
590+
}
591+
584592
pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node {
585593
var n: NodeExternal = undefined;
586594
const err = c._dom_mutation_event_get_related_node(evt, &n);

src/browser/page.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const Walker = @import("dom/walker.zig").WalkerDepthFirst;
3232
const Scheduler = @import("Scheduler.zig");
3333
const Http = @import("../http/Http.zig");
3434
const ScriptManager = @import("ScriptManager.zig");
35+
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
3536
const HTMLDocument = @import("html/document.zig").HTMLDocument;
3637

3738
const URL = @import("../url.zig").URL;
@@ -90,6 +91,10 @@ pub const Page = struct {
9091

9192
load_state: LoadState = .parsing,
9293

94+
// expensive, adds a a global MutationObserver, so we only do it if there's
95+
// an "slotchange" event registered
96+
slot_change_monitor: ?*SlotChangeMonitor = null,
97+
9398
notified_network_idle: IdleNotification = .init,
9499
notified_network_almost_idle: IdleNotification = .init,
95100

@@ -1117,6 +1122,13 @@ pub const Page = struct {
11171122
}
11181123
return null;
11191124
}
1125+
1126+
pub fn registerSlotChangeMonitor(self: *Page) !void {
1127+
if (self.slot_change_monitor != null) {
1128+
return;
1129+
}
1130+
self.slot_change_monitor = try SlotChangeMonitor.init(self);
1131+
}
11201132
};
11211133

11221134
pub const NavigateReason = enum {

0 commit comments

Comments
 (0)