|
| 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 | +} |
0 commit comments