mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-18 18:08:10 +00:00
Element.slot, Element.assignedSlot and slotchange event
This commit is contained in:
@@ -53,7 +53,6 @@ const DocumentFragment = @import("webapi/DocumentFragment.zig");
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
const Performance = @import("webapi/Performance.zig");
|
||||
const Screen = @import("webapi/Screen.zig");
|
||||
const HtmlScript = @import("webapi/Element.zig").Html.Script;
|
||||
const MutationObserver = @import("webapi/MutationObserver.zig");
|
||||
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
||||
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
||||
@@ -95,6 +94,7 @@ _element_styles: Element.StyleLookup = .{},
|
||||
_element_datasets: Element.DatasetLookup = .{},
|
||||
_element_class_lists: Element.ClassListLookup = .{},
|
||||
_element_shadow_roots: Element.ShadowRootLookup = .{},
|
||||
_element_assigned_slots: Element.AssignedSlotLookup = .{},
|
||||
|
||||
_script_manager: ScriptManager,
|
||||
|
||||
@@ -108,6 +108,10 @@ _intersection_observers: std.ArrayList(*IntersectionObserver) = .{},
|
||||
_intersection_check_scheduled: bool = false,
|
||||
_intersection_delivery_scheduled: bool = false,
|
||||
|
||||
// Slots that need slotchange events to be fired
|
||||
_slots_pending_slotchange: std.AutoHashMapUnmanaged(*Element.Html.Slot, void) = .{},
|
||||
_slotchange_delivery_scheduled: bool = false,
|
||||
|
||||
// Lookup for customized built-in elements. Maps element pointer to definition.
|
||||
_customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{},
|
||||
_customized_builtin_connected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{},
|
||||
@@ -242,6 +246,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self._element_datasets = .{};
|
||||
self._element_class_lists = .{};
|
||||
self._element_shadow_roots = .{};
|
||||
self._element_assigned_slots = .{};
|
||||
self._notified_network_idle = .init;
|
||||
self._notified_network_almost_idle = .init;
|
||||
|
||||
@@ -251,6 +256,8 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self._intersection_observers = .{};
|
||||
self._intersection_check_scheduled = false;
|
||||
self._intersection_delivery_scheduled = false;
|
||||
self._slots_pending_slotchange = .{};
|
||||
self._slotchange_delivery_scheduled = false;
|
||||
self._customized_builtin_definitions = .{};
|
||||
self._customized_builtin_connected_callback_invoked = .{};
|
||||
self._customized_builtin_disconnected_callback_invoked = .{};
|
||||
@@ -770,7 +777,7 @@ pub fn tick(self: *Page) void {
|
||||
self.js.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void {
|
||||
pub fn scriptAddedCallback(self: *Page, script: *Element.Html.Script) !void {
|
||||
self._script_manager.addFromElement(script, "parsing") catch |err| {
|
||||
log.err(.page, "page.scriptAddedCallback", .{
|
||||
.err = err,
|
||||
@@ -889,6 +896,14 @@ pub fn scheduleIntersectionDelivery(self: *Page) !void {
|
||||
try self.js.queueIntersectionDelivery();
|
||||
}
|
||||
|
||||
pub fn scheduleSlotchangeDelivery(self: *Page) !void {
|
||||
if (self._slotchange_delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
self._slotchange_delivery_scheduled = true;
|
||||
try self.js.queueSlotchangeDelivery();
|
||||
}
|
||||
|
||||
pub fn performScheduledIntersectionChecks(self: *Page) void {
|
||||
if (!self._intersection_check_scheduled) {
|
||||
return;
|
||||
@@ -945,6 +960,42 @@ pub fn deliverMutations(self: *Page) void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deliverSlotchangeEvents(self: *Page) void {
|
||||
if (!self._slotchange_delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
self._slotchange_delivery_scheduled = false;
|
||||
|
||||
// we need to collect the pending slots, and then clear it and THEN exeute
|
||||
// the slot change. We do this in case the slotchange event itself schedules
|
||||
// more slot changes (which should only be executed on the next microtask)
|
||||
const pending = self._slots_pending_slotchange.count();
|
||||
|
||||
var i: usize = 0;
|
||||
var slots = self.call_arena.alloc(*Element.Html.Slot, pending) catch |err| {
|
||||
log.err(.page, "deliverSlotchange.append", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
var it = self._slots_pending_slotchange.keyIterator();
|
||||
while (it.next()) |slot| {
|
||||
slots[i] = slot.*;
|
||||
i += 1;
|
||||
}
|
||||
self._slots_pending_slotchange.clearRetainingCapacity();
|
||||
|
||||
for (slots) |slot| {
|
||||
const event = Event.init("slotchange", .{ .bubbles = true }, self) catch |err| {
|
||||
log.err(.page, "deliverSlotchange.init", .{ .err = err });
|
||||
continue;
|
||||
};
|
||||
const target = slot.asNode().asEventTarget();
|
||||
_ = target.dispatchEvent(event, self) catch |err| {
|
||||
log.err(.page, "deliverSlotchange.dispatch", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn notifyNetworkIdle(self: *Page) void {
|
||||
std.debug.assert(self._notified_network_idle == .done);
|
||||
self._session.browser.notification.dispatch(.page_network_idle, &.{
|
||||
@@ -1564,6 +1615,28 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
|
||||
child._parent = null;
|
||||
child._child_link = .{};
|
||||
|
||||
// Handle slot assignment removal before mutation observers
|
||||
if (child.is(Element)) |el| {
|
||||
// Check if the parent was a shadow host
|
||||
if (parent.is(Element)) |parent_el| {
|
||||
if (self._element_shadow_roots.get(parent_el)) |shadow_root| {
|
||||
// Signal slot changes for any affected slots
|
||||
const slot_name = el.getAttributeSafe("slot") orelse "";
|
||||
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(shadow_root.asNode(), .{});
|
||||
while (tw.next()) |slot_el| {
|
||||
if (slot_el.is(Element.Html.Slot)) |slot| {
|
||||
if (std.mem.eql(u8, slot.getName(), slot_name)) {
|
||||
self.signalSlotChange(slot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove from assigned slot lookup
|
||||
_ = self._element_assigned_slots.remove(el);
|
||||
}
|
||||
|
||||
if (self.hasMutationObservers()) {
|
||||
const removed = [_]*Node{child};
|
||||
self.childListChange(parent, &.{}, &removed, previous_sibling, next_sibling);
|
||||
@@ -1733,6 +1806,12 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
||||
return;
|
||||
}
|
||||
|
||||
// Update slot assignments for the inserted child if parent is a shadow host
|
||||
// This needs to happen even if the element isn't connected to the document
|
||||
if (child.is(Element)) |el| {
|
||||
self.updateElementAssignedSlot(el);
|
||||
}
|
||||
|
||||
if (opts.child_already_connected and !opts.adopting_to_new_document) {
|
||||
// The child is already connected in the same document, we don't have to reconnect it
|
||||
return;
|
||||
@@ -1779,6 +1858,16 @@ pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value:
|
||||
log.err(.page, "attributeChange.notifyObserver", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// Handle slot assignment changes
|
||||
if (std.mem.eql(u8, name, "slot")) {
|
||||
self.updateSlotAssignments(element);
|
||||
} else if (std.mem.eql(u8, name, "name")) {
|
||||
// Check if this is a slot element
|
||||
if (element.is(Element.Html.Slot)) |slot| {
|
||||
self.signalSlotChange(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attributeRemove(self: *Page, element: *Element, name: []const u8, old_value: []const u8) void {
|
||||
@@ -1793,6 +1882,88 @@ pub fn attributeRemove(self: *Page, element: *Element, name: []const u8, old_val
|
||||
log.err(.page, "attributeRemove.notifyObserver", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// Handle slot assignment changes
|
||||
if (std.mem.eql(u8, name, "slot")) {
|
||||
self.updateSlotAssignments(element);
|
||||
} else if (std.mem.eql(u8, name, "name")) {
|
||||
// Check if this is a slot element
|
||||
if (element.is(Element.Html.Slot)) |slot| {
|
||||
self.signalSlotChange(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn signalSlotChange(self: *Page, slot: *Element.Html.Slot) void {
|
||||
self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| {
|
||||
log.err(.page, "signalSlotChange.put", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
self.scheduleSlotchangeDelivery() catch |err| {
|
||||
log.err(.page, "signalSlotChange.schedule", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn updateSlotAssignments(self: *Page, element: *Element) void {
|
||||
// Find all slots in the shadow root that might be affected
|
||||
const parent = element.asNode()._parent orelse return;
|
||||
|
||||
// Check if parent is a shadow host
|
||||
const parent_el = parent.is(Element) orelse return;
|
||||
_ = self._element_shadow_roots.get(parent_el) orelse return;
|
||||
|
||||
// Signal change for the old slot (if any)
|
||||
if (self._element_assigned_slots.get(element)) |old_slot| {
|
||||
self.signalSlotChange(old_slot);
|
||||
}
|
||||
|
||||
// Update the assignedSlot lookup to the new slot
|
||||
self.updateElementAssignedSlot(element);
|
||||
|
||||
// Signal change for the new slot (if any)
|
||||
if (self._element_assigned_slots.get(element)) |new_slot| {
|
||||
self.signalSlotChange(new_slot);
|
||||
}
|
||||
}
|
||||
|
||||
fn updateElementAssignedSlot(self: *Page, element: *Element) void {
|
||||
// Remove old assignment
|
||||
_ = self._element_assigned_slots.remove(element);
|
||||
|
||||
// Find the new assigned slot
|
||||
const parent = element.asNode()._parent orelse return;
|
||||
const parent_el = parent.is(Element) orelse return;
|
||||
const shadow_root = self._element_shadow_roots.get(parent_el) orelse return;
|
||||
|
||||
const slot_name = element.getAttributeSafe("slot") orelse "";
|
||||
|
||||
// Recursively search through the shadow root for a matching slot
|
||||
if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| {
|
||||
self._element_assigned_slots.put(self.arena, element, slot) catch |err| {
|
||||
log.err(.page, "updateElementAssignedSlot.put", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn findMatchingSlot(node: *Node, slot_name: []const u8) ?*Element.Html.Slot {
|
||||
// Check if this node is a matching slot
|
||||
if (node.is(Element)) |el| {
|
||||
if (el.is(Element.Html.Slot)) |slot| {
|
||||
if (std.mem.eql(u8, slot.getName(), slot_name)) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search children
|
||||
var it = node.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (findMatchingSlot(child, slot_name)) |slot| {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn hasMutationObservers(self: *const Page) bool {
|
||||
|
||||
@@ -2007,6 +2007,15 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
|
||||
}.run, self.page);
|
||||
}
|
||||
|
||||
pub fn queueSlotchangeDelivery(self: *Context) !void {
|
||||
self.isolate.enqueueMicrotask(struct {
|
||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||
const page: *Page = @ptrCast(@alignCast(data.?));
|
||||
page.deliverSlotchangeEvents();
|
||||
}
|
||||
}.run, self.page);
|
||||
}
|
||||
|
||||
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
||||
self.isolate.enqueueMicrotaskFunc(cb.func.castToFunction());
|
||||
}
|
||||
|
||||
@@ -382,3 +382,137 @@
|
||||
testing.expectTrue(flattened[2] === span);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#assignedSlot_returns_null_when_not_assigned">
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
testing.expectEqual(null, div.assignedSlot);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#assignedSlot_property">
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
slot.name = 'header';
|
||||
shadow.appendChild(slot);
|
||||
|
||||
const h1 = document.createElement('h1');
|
||||
h1.setAttribute('slot', 'header');
|
||||
host.appendChild(h1);
|
||||
|
||||
testing.expectEqual(slot, h1.assignedSlot);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#slotchange_on_insertion">
|
||||
{
|
||||
let calls = 0;
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
slot.name = 'content';
|
||||
shadow.appendChild(slot);
|
||||
|
||||
slot.addEventListener('slotchange', (e) => {
|
||||
const nodes = slot.assignedNodes();
|
||||
testing.expectEqual(1, nodes.length);
|
||||
calls += 1;
|
||||
});
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('slot', 'content');
|
||||
host.appendChild(div);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, calls);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#slotchange_on_attribute_change">
|
||||
{
|
||||
let calls = 0;
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
slot.name = 'content';
|
||||
shadow.appendChild(slot);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('slot', 'content');
|
||||
host.appendChild(div);
|
||||
|
||||
slot.addEventListener('slotchange', (e) => {
|
||||
const nodes = slot.assignedNodes();
|
||||
testing.expectEqual(0, nodes.length);
|
||||
calls += 1;
|
||||
});
|
||||
|
||||
div.setAttribute('slot', 'other');
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, calls);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#slotchange_on_removal">
|
||||
{
|
||||
let calls = 0;
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
slot.name = 'content';
|
||||
shadow.appendChild(slot);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('slot', 'content');
|
||||
host.appendChild(div);
|
||||
|
||||
slot.addEventListener('slotchange', (e) => {
|
||||
const nodes = slot.assignedNodes();
|
||||
testing.expectEqual(0, nodes.length);
|
||||
calls += 1;
|
||||
});
|
||||
|
||||
div.remove();
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, calls);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#slotchange_with_slot_property">
|
||||
{
|
||||
let calls = 0;
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
slot.name = 'content';
|
||||
shadow.appendChild(slot);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.slot = 'content';
|
||||
host.appendChild(div);
|
||||
|
||||
slot.addEventListener('slotchange', (e) => {
|
||||
const nodes = slot.assignedNodes();
|
||||
testing.expectEqual(0, nodes.length);
|
||||
calls += 1;
|
||||
});
|
||||
|
||||
div.slot = 'other';
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(1, calls);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -45,6 +45,7 @@ pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
|
||||
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
|
||||
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
|
||||
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
|
||||
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
|
||||
|
||||
pub const Namespace = enum(u8) {
|
||||
html,
|
||||
@@ -400,6 +401,14 @@ pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
|
||||
return self.setAttributeSafe("id", value, page);
|
||||
}
|
||||
|
||||
pub fn getSlot(self: *const Element) []const u8 {
|
||||
return self.getAttributeSafe("slot") orelse "";
|
||||
}
|
||||
|
||||
pub fn setSlot(self: *Element, value: []const u8, page: *Page) !void {
|
||||
return self.setAttributeSafe("slot", value, page);
|
||||
}
|
||||
|
||||
pub fn getDir(self: *const Element) []const u8 {
|
||||
return self.getAttributeSafe("dir") orelse "";
|
||||
}
|
||||
@@ -481,6 +490,10 @@ pub fn getShadowRoot(self: *Element, page: *Page) ?*ShadowRoot {
|
||||
return shadow_root;
|
||||
}
|
||||
|
||||
pub fn getAssignedSlot(self: *Element, page: *Page) ?*Html.Slot {
|
||||
return page._element_assigned_slots.get(self);
|
||||
}
|
||||
|
||||
pub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowRoot {
|
||||
if (page._element_shadow_roots.get(self)) |_| {
|
||||
return error.AlreadyHasShadowRoot;
|
||||
@@ -1242,6 +1255,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
|
||||
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
|
||||
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
|
||||
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
|
||||
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
|
||||
pub const classList = bridge.accessor(Element.getClassList, null, .{});
|
||||
@@ -1259,6 +1273,7 @@ pub const JsApi = struct {
|
||||
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
|
||||
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
|
||||
pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
|
||||
pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
|
||||
pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
|
||||
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true });
|
||||
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true });
|
||||
|
||||
@@ -62,6 +62,7 @@ fn collectAssignedNodes(self: *Slot, comptime elements: bool, coll: CollectionTy
|
||||
const allocator = page.call_arena;
|
||||
|
||||
const host = shadow_root.getHost();
|
||||
const initial_count = coll.items.len;
|
||||
var it = host.asNode().childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (!isAssignedToSlot(child, slot_name)) {
|
||||
@@ -87,6 +88,20 @@ fn collectAssignedNodes(self: *Slot, comptime elements: bool, coll: CollectionTy
|
||||
try coll.append(allocator, child);
|
||||
}
|
||||
}
|
||||
|
||||
// If flatten is true and no assigned nodes were found, return fallback content
|
||||
if (opts.flatten and coll.items.len == initial_count) {
|
||||
var child_it = self.asNode().childrenIterator();
|
||||
while (child_it.next()) |child| {
|
||||
if (comptime elements) {
|
||||
if (child.is(Element)) |el| {
|
||||
try coll.append(allocator, el);
|
||||
}
|
||||
} else {
|
||||
try coll.append(allocator, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assign(self: *Slot, nodes: []const *Node) void {
|
||||
|
||||
Reference in New Issue
Block a user