mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-02 10:47:15 -04:00
WebAPI: much better shadowdom slot support
This adds more comprehensive and correct support for slot assignment. The
changes can be broken down into 3 buckets:
1 - Slot.assign is now implemented, and general correctness around finding/
assigning slots (e.g. Text.assignedSlot)
2 - Event dispatching for "composed" (propagating through the shadowdom) better,
specifically around the composedPath and slots
3 - The slotchange event is fires at the right time (snapshot before mutation
fire after mutation).
A number of WPT tests now pass, and there should be real-world impact for
slot-heavy frameworks (e.g. lit)
This commit is contained in:
@@ -210,6 +210,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
}
|
||||
}
|
||||
|
||||
const target_root = target.getRootNode(.{});
|
||||
var node: ?*Node = target;
|
||||
while (node) |n| {
|
||||
if (path_len >= path_buffer.len) break;
|
||||
@@ -220,8 +221,8 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
if (n.is(ShadowRoot)) |shadow| {
|
||||
event._needs_retargeting = true;
|
||||
|
||||
// If event is not composed, stop at shadow boundary
|
||||
if (!event._composed) {
|
||||
// A non-composed event stops at its own tree's root.
|
||||
if (!event._composed and n == target_root) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -230,6 +231,13 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
continue;
|
||||
}
|
||||
|
||||
// an assigned slottable's event-path parent is its assigned slot,
|
||||
// routing the event into the slot's shadow tree
|
||||
if (frame._assigned_slots.get(n)) |slot| {
|
||||
node = slot.asNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
}
|
||||
|
||||
@@ -305,6 +313,34 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
event._event_phase = .bubbling_phase;
|
||||
for (path[1..]) |current_target| {
|
||||
if (event._stop_propagation) break;
|
||||
|
||||
// Inline handlers (e.g. shadowRoot.onslotchange, div.onclick) are
|
||||
// regular non-capture listeners and also fire on ancestors.
|
||||
if (self.getInlineHandler(current_target, event)) |inline_handler| {
|
||||
was_handled = true;
|
||||
event._current_target = current_target;
|
||||
|
||||
const original_target = event._target;
|
||||
if (event._needs_retargeting) {
|
||||
event._target = getAdjustedTarget(original_target, current_target);
|
||||
}
|
||||
|
||||
ls.toLocal(inline_handler).callWithThis(void, current_target, .{event}) catch |err| {
|
||||
log.warn(.event, "inline handler", .{ .err = err });
|
||||
};
|
||||
|
||||
if (event._needs_retargeting) {
|
||||
event._target = original_target;
|
||||
}
|
||||
|
||||
if (event._stop_propagation) {
|
||||
break;
|
||||
}
|
||||
if (event._stop_immediate_propagation) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.base.getListeners(current_target, event._type_string)) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, &was_handled, &ls.local, comptime .init(false, opts));
|
||||
}
|
||||
@@ -420,6 +456,12 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
|
||||
const global_event_handlers = @import("webapi/global_event_handlers.zig");
|
||||
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;
|
||||
|
||||
// Non-element targets (e.g. ShadowRoot.onslotchange) only ever set their
|
||||
// handler via the property, so the lookup alone covers them.
|
||||
if (self.frame._event_target_attr_listeners.get(.{ .target = target, .handler = handler_type })) |cached| {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Look up the inline handler for this target
|
||||
const html_element = switch (target._type) {
|
||||
.node => |n| n.is(Element.Html) orelse return null,
|
||||
@@ -432,9 +474,8 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
|
||||
};
|
||||
}
|
||||
|
||||
// Computes the adjusted target for shadow DOM event retargeting
|
||||
// Returns the lowest shadow-including ancestor of original_target that is
|
||||
// also an ancestor-or-self of current_target
|
||||
// DOM spec "retarget": walk original_target out of shadow trees until the
|
||||
// node is visible from current_target's tree.
|
||||
fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget {
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
|
||||
@@ -447,24 +488,32 @@ fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarge
|
||||
else => return original_target,
|
||||
};
|
||||
|
||||
// Walk up from original target, checking if we can reach current target
|
||||
var node: ?*Node = orig_node;
|
||||
while (node) |n| {
|
||||
// Check if current_target is an ancestor of n (or n itself)
|
||||
if (isAncestorOrSelf(curr_node, n)) {
|
||||
return n.asEventTarget();
|
||||
var node = orig_node;
|
||||
while (true) {
|
||||
const root = node.getRootNode(.{});
|
||||
const shadow = root.is(ShadowRoot) orelse return node.asEventTarget();
|
||||
if (isShadowIncludingInclusiveAncestor(root, curr_node)) {
|
||||
return node.asEventTarget();
|
||||
}
|
||||
node = shadow._host.asNode();
|
||||
}
|
||||
}
|
||||
|
||||
// Cross shadow boundary if needed
|
||||
if (n.is(ShadowRoot)) |shadow| {
|
||||
node = shadow._host.asNode();
|
||||
fn isShadowIncludingInclusiveAncestor(ancestor: *Node, node: *Node) bool {
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
|
||||
var n: ?*Node = node;
|
||||
while (n) |cur| {
|
||||
if (cur == ancestor) {
|
||||
return true;
|
||||
}
|
||||
if (cur.is(ShadowRoot)) |shadow| {
|
||||
n = shadow._host.asNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
n = cur._parent;
|
||||
}
|
||||
|
||||
return original_target;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether the target's tree root (without crossing shadow boundaries) is a
|
||||
|
||||
@@ -41,6 +41,7 @@ const Node = @import("webapi/Node.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const CData = @import("webapi/CData.zig");
|
||||
const Text = @import("webapi/cdata/Text.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const HtmlElement = @import("webapi/element/Html.zig");
|
||||
const Window = @import("webapi/Window.zig");
|
||||
@@ -121,10 +122,14 @@ _element_class_lists: Element.ClassListLookup = .empty,
|
||||
_element_rel_lists: Element.RelListLookup = .empty,
|
||||
_element_shadow_roots: Element.ShadowRootLookup = .empty,
|
||||
_node_owner_documents: Node.OwnerDocumentLookup = .empty,
|
||||
_element_assigned_slots: Element.AssignedSlotLookup = .empty,
|
||||
_element_scroll_positions: Element.ScrollPositionLookup = .empty,
|
||||
_element_namespace_uris: Element.NamespaceUriLookup = .empty,
|
||||
|
||||
// Same as above, but for Nodes (slot assigments apply to both Element AND
|
||||
// Text nodes)
|
||||
_assigned_slots: Node.AssignedSlotLookup = .empty,
|
||||
_manual_slot_assignments: Node.AssignedSlotLookup = .empty,
|
||||
|
||||
/// Lazily-created inline event listeners (or listeners provided as attributes).
|
||||
/// Avoids bloating all elements with extra function fields for rare usage.
|
||||
///
|
||||
@@ -178,9 +183,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,
|
||||
// Slots that need slotchange events to be fired, in signal order. Delivered
|
||||
// by deliverMutations because there is specific timing with for these events
|
||||
// with respect to mutations
|
||||
_slots_pending_slotchange: std.AutoArrayHashMapUnmanaged(*Element.Html.Slot, void) = .{},
|
||||
|
||||
// Lookup for customized built-in elements. Maps element pointer to definition.
|
||||
_customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{},
|
||||
@@ -1867,14 +1873,6 @@ pub fn scheduleIntersectionDelivery(self: *Frame) !void {
|
||||
try self.js.queueIntersectionDelivery();
|
||||
}
|
||||
|
||||
pub fn scheduleSlotchangeDelivery(self: *Frame) !void {
|
||||
if (self._slotchange_delivery_scheduled) {
|
||||
return;
|
||||
}
|
||||
self._slotchange_delivery_scheduled = true;
|
||||
try self.js.queueSlotchangeDelivery();
|
||||
}
|
||||
|
||||
pub fn scheduleCustomElementBackupDrain(self: *Frame) !void {
|
||||
try self.js.queueCustomElementBackupDrain();
|
||||
}
|
||||
@@ -1924,39 +1922,38 @@ pub fn deliverMutations(self: *Frame) void {
|
||||
return;
|
||||
}
|
||||
|
||||
// snapshot the pending slots to deliver. We'll deliver these AFTER the mutation
|
||||
// but new pending slots that land during mutation should only be delivered
|
||||
// on the microtask tick.
|
||||
const slots = self.call_arena.dupe(*Element.Html.Slot, self._slots_pending_slotchange.keys()) catch |err| blk: {
|
||||
log.err(.frame, "deliverMutations.slots", .{ .err = err, .type = self._type, .url = self.url });
|
||||
break :blk &.{};
|
||||
};
|
||||
self._slots_pending_slotchange.clearRetainingCapacity();
|
||||
|
||||
// We only deliver notifications for observers that have records BEFORE
|
||||
// we started the delivery. So we need to snapshot this. Any observers which
|
||||
// get records during this phase will only be processed on the next microtask tick.
|
||||
var notify: std.ArrayList(*MutationObserver) = .empty;
|
||||
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||
if (observer._pending_records.items.len == 0) {
|
||||
continue;
|
||||
}
|
||||
notify.append(self.call_arena, observer) catch |err| {
|
||||
log.err(.frame, "deliverMutations.notify", .{ .err = err, .type = self._type, .url = self.url });
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
for (notify.items) |observer| {
|
||||
observer.deliverRecords(self) catch |err| {
|
||||
log.err(.frame, "frame.deliverMutations", .{ .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deliverSlotchangeEvents(self: *Frame) 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(.frame, "deliverSlotchange.append", .{ .err = err, .type = self._type, .url = self.url });
|
||||
return;
|
||||
};
|
||||
|
||||
var it = self._slots_pending_slotchange.keyIterator();
|
||||
while (it.next()) |slot| {
|
||||
slots[i] = slot.*;
|
||||
i += 1;
|
||||
}
|
||||
self._slots_pending_slotchange.clearRetainingCapacity();
|
||||
|
||||
// slotchange events fire after the observer callbacks (spec step order)
|
||||
for (slots) |slot| {
|
||||
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self._page) catch |err| {
|
||||
log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = self._type, .url = self.url });
|
||||
@@ -3095,27 +3092,7 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt
|
||||
self.updateRangesForNodeRemoval(parent, child, idx);
|
||||
}
|
||||
|
||||
// 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(comptime .wrap("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);
|
||||
}
|
||||
self.slotRemovalSteps(parent, child);
|
||||
|
||||
if (self.hasMutationObservers()) {
|
||||
const removed = [_]*Node{child};
|
||||
@@ -3261,6 +3238,16 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
}
|
||||
}
|
||||
|
||||
if (self._element_shadow_roots.count() != 0) {
|
||||
// html5ever wraps fragment parses in a temporary <html> element that
|
||||
// gets unwrapped later; it must not take part in slot assignment.
|
||||
const is_fragment_wrapper = from_parser and
|
||||
self._parse_mode == .fragment and child.is(Element.Html.Html) != null;
|
||||
if (!is_fragment_wrapper) {
|
||||
self.slotInsertionSteps(parent, child);
|
||||
}
|
||||
}
|
||||
|
||||
const parent_is_connected = parent.isConnected();
|
||||
|
||||
// Tri-state behavior for mutations:
|
||||
@@ -3315,12 +3302,6 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
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.
|
||||
// On cross-document adoption the child has already fired
|
||||
@@ -3377,11 +3358,12 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str
|
||||
|
||||
// Handle slot assignment changes
|
||||
if (name.eql(comptime .wrap("slot"))) {
|
||||
self.updateSlotAssignments(element);
|
||||
const old = if (old_value) |o| o.str() else "";
|
||||
self.slottableNameChange(element.asNode(), old, value.str());
|
||||
} else if (name.eql(comptime .wrap("name"))) {
|
||||
// Check if this is a slot element
|
||||
if (element.is(Element.Html.Slot)) |slot| {
|
||||
self.signalSlotChange(slot);
|
||||
const old = if (old_value) |o| o.str() else "";
|
||||
self.slotNameChange(slot, old, value.str());
|
||||
}
|
||||
} else if (name.eql(comptime .wrap("popover"))) {
|
||||
const old = if (old_value) |o| o.str() else null;
|
||||
@@ -3406,11 +3388,10 @@ pub fn attributeRemove(self: *Frame, element: *Element, name: String, old_value:
|
||||
|
||||
// Handle slot assignment changes
|
||||
if (name.eql(comptime .wrap("slot"))) {
|
||||
self.updateSlotAssignments(element);
|
||||
self.slottableNameChange(element.asNode(), old_value.str(), "");
|
||||
} else if (name.eql(comptime .wrap("name"))) {
|
||||
// Check if this is a slot element
|
||||
if (element.is(Element.Html.Slot)) |slot| {
|
||||
self.signalSlotChange(slot);
|
||||
self.slotNameChange(slot, old_value.str(), "");
|
||||
}
|
||||
} else if (name.eql(comptime .wrap("popover"))) {
|
||||
popover.attributeChanged(element, old_value.str(), null, self);
|
||||
@@ -3422,66 +3403,56 @@ fn signalSlotChange(self: *Frame, slot: *Element.Html.Slot) void {
|
||||
log.err(.frame, "signalSlotChange.put", .{ .err = err, .type = self._type, .url = self.url });
|
||||
return;
|
||||
};
|
||||
self.scheduleSlotchangeDelivery() catch |err| {
|
||||
self.scheduleMutationDelivery() catch |err| {
|
||||
log.err(.frame, "signalSlotChange.schedule", .{ .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
}
|
||||
|
||||
fn updateSlotAssignments(self: *Frame, 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);
|
||||
}
|
||||
pub fn isSlottable(node: *Node) bool {
|
||||
return switch (node._type) {
|
||||
.element => true,
|
||||
.cdata => node.is(Text) != null,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn updateElementAssignedSlot(self: *Frame, element: *Element) void {
|
||||
// Remove old assignment
|
||||
_ = self._element_assigned_slots.remove(element);
|
||||
pub fn findSlotForSlottable(self: *Frame, slottable: *Node, comptime open_only: bool) ?*Element.Html.Slot {
|
||||
const parent = slottable.parentElement() orelse return null;
|
||||
|
||||
// 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 shadow_root = self._element_shadow_roots.get(parent) orelse return null;
|
||||
|
||||
const slot_name = element.getAttributeSafe(comptime .wrap("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(.frame, "updateElementAssignedSlot.put", .{ .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
if (open_only and shadow_root._mode != .open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shadow_node = shadow_root.asNode();
|
||||
|
||||
if (shadow_root._slot_assignment == .manual) {
|
||||
const slot = self._manual_slot_assignments.get(slottable) orelse return null;
|
||||
if (slot.asNode().getRootNode(.{}) != shadow_node) {
|
||||
return null;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
const slottable_name = blk: {
|
||||
const el = slottable.is(Element) orelse break :blk "";
|
||||
break :blk el.getAttributeSafe(comptime .wrap("slot")) orelse "";
|
||||
};
|
||||
return findNamedSlot(shadow_node, slottable_name);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// First slot, in tree order, with the given name.
|
||||
fn findNamedSlot(node: *Node, slot_name: []const u8) ?*Element.Html.Slot {
|
||||
if (node.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| {
|
||||
if (findNamedSlot(child, slot_name)) |slot| {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
@@ -3489,6 +3460,175 @@ fn findMatchingSlot(node: *Node, slot_name: []const u8) ?*Element.Html.Slot {
|
||||
return null;
|
||||
}
|
||||
|
||||
// DOM spec "assign slottables": recompute a slot's assigned nodes, signaling
|
||||
// a slot change when the assignment actually changed.
|
||||
pub fn assignSlottables(self: *Frame, slot: *Element.Html.Slot) void {
|
||||
self._assignSlottables(slot) catch |err| {
|
||||
log.err(.frame, "assignSlottables", .{ .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
}
|
||||
|
||||
fn _assignSlottables(self: *Frame, slot: *Element.Html.Slot) !void {
|
||||
var slottables: std.ArrayList(*Node) = .empty;
|
||||
if (slot.asNode().getRootNode(.{}).is(ShadowRoot)) |shadow_root| {
|
||||
const host = shadow_root.getHost();
|
||||
if (shadow_root._slot_assignment == .manual) {
|
||||
// manual assignment preserves the assign(...) order, not tree order
|
||||
for (slot._manually_assigned.items) |node| {
|
||||
if (node._parent == host.asNode()) {
|
||||
try slottables.append(self.call_arena, node);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var it = host.asNode().childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (!isSlottable(child)) {
|
||||
continue;
|
||||
}
|
||||
if (self.findSlotForSlottable(child, false) == slot) {
|
||||
try slottables.append(self.call_arena, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const old = slot._assigned.items;
|
||||
const changed = blk: {
|
||||
if (old.len != slottables.items.len) {
|
||||
break :blk true;
|
||||
}
|
||||
for (old, slottables.items) |a, b| {
|
||||
if (a != b) break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
};
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.signalSlotChange(slot);
|
||||
|
||||
for (old) |node| {
|
||||
if (self._assigned_slots.get(node) == slot) {
|
||||
_ = self._assigned_slots.remove(node);
|
||||
}
|
||||
}
|
||||
slot._assigned.clearRetainingCapacity();
|
||||
try slot._assigned.appendSlice(self.arena, slottables.items);
|
||||
for (slottables.items) |node| {
|
||||
try self._assigned_slots.put(self.arena, node, slot);
|
||||
}
|
||||
}
|
||||
|
||||
// DOM spec "assign a slot"
|
||||
fn assignASlot(self: *Frame, slottable: *Node) void {
|
||||
const slot = self.findSlotForSlottable(slottable, false) orelse return;
|
||||
self.assignSlottables(slot);
|
||||
}
|
||||
|
||||
// DOM spec "assign slottables for a tree"
|
||||
pub fn assignSlottablesForTree(self: *Frame, root: *Node) void {
|
||||
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(root, .{});
|
||||
while (tw.next()) |el| {
|
||||
if (el.is(Element.Html.Slot)) |slot| {
|
||||
self.assignSlottables(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn subtreeHasSlot(node: *Node) bool {
|
||||
if (node.is(Element) == null) {
|
||||
return false;
|
||||
}
|
||||
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{});
|
||||
while (tw.next()) |el| {
|
||||
if (el.is(Element.Html.Slot) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// DOM spec insertion steps that affect slot assignment.
|
||||
fn slotInsertionSteps(self: *Frame, parent: *Node, child: *Node) void {
|
||||
// The new child may be a slottable to assign in the parent's shadow tree.
|
||||
if (parent.is(Element)) |parent_el| {
|
||||
if (self._element_shadow_roots.get(parent_el) != null and isSlottable(child)) {
|
||||
self.assignASlot(child);
|
||||
}
|
||||
}
|
||||
|
||||
// New fallback content in a slot that currently renders its fallback.
|
||||
if (parent.is(Element.Html.Slot)) |parent_slot| {
|
||||
if (parent_slot._assigned.items.len == 0 and parent.getRootNode(.{}).is(ShadowRoot) != null) {
|
||||
self.signalSlotChange(parent_slot);
|
||||
}
|
||||
}
|
||||
|
||||
// A subtree containing slots was inserted into a shadow tree.
|
||||
if (subtreeHasSlot(child)) {
|
||||
const root = child.getRootNode(.{});
|
||||
if (root.is(ShadowRoot) != null) {
|
||||
self.assignSlottablesForTree(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOM spec removing steps that affect slot assignment. Runs after child has
|
||||
// been unlinked from parent.
|
||||
fn slotRemovalSteps(self: *Frame, parent: *Node, child: *Node) void {
|
||||
if (self._element_shadow_roots.count() == 0) {
|
||||
// shortcut
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._assigned_slots.get(child)) |slot| {
|
||||
self.assignSlottables(slot);
|
||||
}
|
||||
|
||||
// Fallback content was removed from a slot that renders its fallback.
|
||||
if (parent.is(Element.Html.Slot)) |parent_slot| {
|
||||
if (parent_slot._assigned.items.len == 0 and parent.getRootNode(.{}).is(ShadowRoot) != null) {
|
||||
self.signalSlotChange(parent_slot);
|
||||
}
|
||||
}
|
||||
|
||||
// A subtree containing slots was removed: update assignments in the old
|
||||
// tree, and clear assignments held by slots in the detached subtree.
|
||||
if (subtreeHasSlot(child)) {
|
||||
const root = parent.getRootNode(.{});
|
||||
if (root.is(ShadowRoot) != null) {
|
||||
self.assignSlottablesForTree(root);
|
||||
}
|
||||
self.assignSlottablesForTree(child);
|
||||
}
|
||||
}
|
||||
|
||||
// DOM spec attribute change steps for the `slot` attribute on a slottable.
|
||||
fn slottableNameChange(self: *Frame, slottable: *Node, old_value: []const u8, value: []const u8) void {
|
||||
if (std.mem.eql(u8, old_value, value)) {
|
||||
return;
|
||||
}
|
||||
if (self._element_shadow_roots.count() == 0) {
|
||||
return;
|
||||
}
|
||||
if (self._assigned_slots.get(slottable)) |old_slot| {
|
||||
self.assignSlottables(old_slot);
|
||||
}
|
||||
self.assignASlot(slottable);
|
||||
}
|
||||
|
||||
// HTML spec attribute change steps for the `name` attribute on a slot.
|
||||
fn slotNameChange(self: *Frame, slot: *Element.Html.Slot, old_value: []const u8, value: []const u8) void {
|
||||
if (std.mem.eql(u8, old_value, value)) {
|
||||
return;
|
||||
}
|
||||
const root = slot.asNode().getRootNode(.{});
|
||||
if (root.is(ShadowRoot) != null) {
|
||||
self.assignSlottablesForTree(root);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hasMutationObservers(self: *const Frame) bool {
|
||||
return self._mutation_observers.first != null;
|
||||
}
|
||||
@@ -3605,6 +3745,21 @@ fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, allow_d
|
||||
self._parse_mode = .fragment;
|
||||
defer self._parse_mode = previous_parse_mode;
|
||||
|
||||
// The html5ever wrapper-unwrap below rebinds children without going
|
||||
// through the insertion path, so recompute slot assignments for any
|
||||
// shadow tree this fragment landed in (idempotent; signals only on diff).
|
||||
defer if (self._element_shadow_roots.count() != 0) {
|
||||
const root = node.getRootNode(.{});
|
||||
if (root.is(ShadowRoot) != null) {
|
||||
self.assignSlottablesForTree(root);
|
||||
}
|
||||
if (node.is(Element)) |el| {
|
||||
if (self._element_shadow_roots.get(el)) |shadow_root| {
|
||||
self.assignSlottablesForTree(shadow_root.asNode());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var parser = Parser.init(self.call_arena, node, self, .{ .allow_declarative_shadow = allow_declarative_shadow });
|
||||
parser.parseFragment(html);
|
||||
|
||||
|
||||
@@ -774,6 +774,10 @@ pub const Script = struct {
|
||||
|
||||
const local = &ls.local;
|
||||
|
||||
// Per spec, trigger any microtasks BEFORE execution in case the parser
|
||||
// (e.g. via event callbacks) queued anything.
|
||||
local.runMicrotasks();
|
||||
|
||||
// Handle importmap special case here: the content is a JSON containing imports.
|
||||
// Multiple <script type="importmap"> elements merge with first-wins semantics.
|
||||
if (fe.kind == .importmap) {
|
||||
|
||||
@@ -1063,17 +1063,6 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
|
||||
}.run);
|
||||
}
|
||||
|
||||
pub fn queueSlotchangeDelivery(self: *Context) !void {
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
switch (ctx.global) {
|
||||
.frame => |frame| frame.deliverSlotchangeEvents(),
|
||||
.worker => unreachable,
|
||||
}
|
||||
}
|
||||
}.run);
|
||||
}
|
||||
|
||||
pub fn queueCustomElementBackupDrain(self: *Context) !void {
|
||||
self.enqueueMicrotask(struct {
|
||||
fn run(ctx: *Context) void {
|
||||
|
||||
@@ -516,3 +516,150 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#no_slotchange_when_assignment_unchanged">
|
||||
{
|
||||
// Renaming a detached slot or one whose assignment doesn't change must
|
||||
// not fire slotchange.
|
||||
let calls = 0;
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
slot.addEventListener('slotchange', () => { calls += 1; });
|
||||
slot.name = 'detached-rename'; // detached: no signal
|
||||
shadow.appendChild(slot); // host has no children: no signal
|
||||
slot.name = 'still-nothing-assigned'; // assignment ([]) unchanged: no signal
|
||||
|
||||
testing.onload(() => {
|
||||
testing.expectEqual(0, calls);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#slotchange_bubbles_to_shadow_root">
|
||||
{
|
||||
let listener_calls = 0;
|
||||
let handler_calls = 0;
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
shadow.appendChild(slot);
|
||||
|
||||
shadow.addEventListener('slotchange', (e) => {
|
||||
testing.expectTrue(e.target === slot);
|
||||
listener_calls += 1;
|
||||
});
|
||||
shadow.onslotchange = (e) => {
|
||||
testing.expectTrue(e.target === slot);
|
||||
handler_calls += 1;
|
||||
};
|
||||
|
||||
host.appendChild(document.createElement('span'));
|
||||
|
||||
testing.onload(() => {
|
||||
testing.expectEqual(1, listener_calls);
|
||||
testing.expectEqual(1, handler_calls);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#assignedSlot_null_for_closed_shadow_root">
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'closed' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
shadow.appendChild(slot);
|
||||
|
||||
const span = document.createElement('span');
|
||||
host.appendChild(span);
|
||||
|
||||
// assigned (slot sees it), but hidden from the outside
|
||||
testing.expectEqual(1, slot.assignedNodes().length);
|
||||
testing.expectEqual(null, span.assignedSlot);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#text_assignedSlot">
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
shadow.appendChild(slot);
|
||||
|
||||
const text = document.createTextNode('hello');
|
||||
host.appendChild(text);
|
||||
|
||||
testing.expectEqual(slot, text.assignedSlot);
|
||||
testing.expectEqual(null, document.createTextNode('detached').assignedSlot);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#manual_assignment">
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open', slotAssignment: 'manual' });
|
||||
|
||||
const s1 = document.createElement('slot');
|
||||
const s2 = document.createElement('slot');
|
||||
shadow.appendChild(s1);
|
||||
shadow.appendChild(s2);
|
||||
|
||||
const c1 = document.createElement('div');
|
||||
const c2 = document.createElement('div');
|
||||
const c3 = document.createElement('div');
|
||||
c1.setAttribute('slot', 'ignored-in-manual-mode');
|
||||
host.appendChild(c1);
|
||||
host.appendChild(c2);
|
||||
host.appendChild(c3);
|
||||
|
||||
// slot attribute is ignored in manual mode
|
||||
testing.expectEqual(0, s1.assignedNodes().length);
|
||||
|
||||
// assign() order is preserved, not tree order
|
||||
s1.assign(c2, c3, c1);
|
||||
testing.expectEqual(3, s1.assignedNodes().length);
|
||||
testing.expectTrue(s1.assignedNodes()[0] === c2);
|
||||
testing.expectTrue(s1.assignedNodes()[1] === c3);
|
||||
testing.expectTrue(s1.assignedNodes()[2] === c1);
|
||||
testing.expectEqual(s1, c2.assignedSlot);
|
||||
|
||||
// re-assigning a node moves it to the new slot
|
||||
s2.assign(c2);
|
||||
testing.expectEqual(2, s1.assignedNodes().length);
|
||||
testing.expectTrue(s1.assignedNodes()[0] === c3);
|
||||
testing.expectEqual(1, s2.assignedNodes().length);
|
||||
testing.expectEqual(s2, c2.assignedSlot);
|
||||
|
||||
// an array is not allowed: assign is variadic
|
||||
testing.expectError('TypeError', () => { s1.assign([c1, c2]); });
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="Slot#flatten_uses_fallback_of_nested_slot">
|
||||
{
|
||||
// outer host content assigned to outer slot; outer slot's tree contains
|
||||
// an inner shadow host whose slot has fallback content
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const slot = document.createElement('slot');
|
||||
shadow.appendChild(slot);
|
||||
|
||||
// fallback of the slot is itself a slot (not in a shadow tree of its own,
|
||||
// it shares this shadow tree) with its own fallback
|
||||
const innerSlot = document.createElement('slot');
|
||||
innerSlot.name = 'inner';
|
||||
const fallback = document.createElement('em');
|
||||
innerSlot.appendChild(fallback);
|
||||
slot.appendChild(innerSlot);
|
||||
|
||||
// host has no children: flatten resolves the fallback chain
|
||||
const flat = slot.assignedNodes({ flatten: true });
|
||||
testing.expectEqual(1, flat.length);
|
||||
testing.expectTrue(flat[0] === fallback);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -49,7 +49,6 @@ pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
|
||||
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
|
||||
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
|
||||
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
|
||||
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
|
||||
pub const NamespaceUriLookup = std.AutoHashMapUnmanaged(*Element, []const u8);
|
||||
|
||||
pub const ScrollPosition = struct {
|
||||
@@ -748,7 +747,8 @@ pub fn getShadowRoot(self: *Element, frame: *Frame) ?*ShadowRoot {
|
||||
}
|
||||
|
||||
pub fn getAssignedSlot(self: *Element, frame: *Frame) ?*Html.Slot {
|
||||
return frame._element_assigned_slots.get(self);
|
||||
// Hidden by a closed shadow tree
|
||||
return frame.findSlotForSlottable(self.asNode(), true);
|
||||
}
|
||||
|
||||
// Whether this element may host a shadow root
|
||||
|
||||
@@ -292,6 +292,12 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
|
||||
// Track closed shadow boundaries (position in path and host position)
|
||||
var closed_shadow_boundary: ?struct { shadow_end: usize, host_start: usize } = null;
|
||||
|
||||
const frame_ = switch (exec.js.global) {
|
||||
.frame => |frame| frame,
|
||||
else => null,
|
||||
};
|
||||
|
||||
const target_root = target_node.getRootNode(.{});
|
||||
var node: ?*Node = target_node;
|
||||
while (node) |n| {
|
||||
if (path_len >= path_buffer.len) {
|
||||
@@ -305,8 +311,7 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
|
||||
if (n._type.document_fragment._type == .shadow_root) {
|
||||
const shadow = n._type.document_fragment._type.shadow_root;
|
||||
|
||||
// If event is not composed, stop at shadow boundary
|
||||
if (!self._composed) {
|
||||
if (!self._composed and n == target_root) {
|
||||
stopped_at_shadow_boundary = true;
|
||||
break;
|
||||
}
|
||||
@@ -327,6 +332,15 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
// an assigned slottable's event-path parent is its assigned slot,
|
||||
// routing the event into the slot's shadow tree
|
||||
if (frame_) |frame| {
|
||||
if (frame._assigned_slots.get(n)) |slot| {
|
||||
node = slot.asNode();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ const String = lp.String;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const LinkedList = std.DoublyLinkedList;
|
||||
|
||||
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Node, *Element.Html.Slot);
|
||||
|
||||
const Node = @This();
|
||||
|
||||
_type: Type,
|
||||
|
||||
@@ -110,6 +110,18 @@ pub fn setHTMLUnsafe(self: *ShadowRoot, html: []const u8, frame: *Frame) !void {
|
||||
return self.asDocumentFragment().setHTMLUnsafe(html, frame);
|
||||
}
|
||||
|
||||
pub fn getOnSlotChange(self: *ShadowRoot, frame: *Frame) ?js.Function.Global {
|
||||
return frame._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = .onslotchange });
|
||||
}
|
||||
|
||||
pub fn setOnSlotChange(self: *ShadowRoot, callback: ?js.Function.Global, frame: *Frame) !void {
|
||||
if (callback) |cb| {
|
||||
try frame._event_target_attr_listeners.put(frame.arena, .{ .target = self.asEventTarget(), .handler = .onslotchange }, cb);
|
||||
} else {
|
||||
_ = frame._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = .onslotchange });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getElementById(self: *ShadowRoot, id: []const u8, frame: *Frame) ?*Element {
|
||||
if (id.len == 0) {
|
||||
return null;
|
||||
@@ -182,6 +194,7 @@ pub const JsApi = struct {
|
||||
}
|
||||
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
|
||||
pub const setHTMLUnsafe = bridge.function(ShadowRoot.setHTMLUnsafe, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const onslotchange = bridge.accessor(ShadowRoot.getOnSlotChange, ShadowRoot.setOnSlotChange, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const CData = @import("../CData.zig");
|
||||
const Slot = @import("../element/html/Slot.zig");
|
||||
|
||||
const Text = @This();
|
||||
|
||||
@@ -33,6 +34,10 @@ pub fn getWholeText(self: *Text) []const u8 {
|
||||
return self._proto._data.str();
|
||||
}
|
||||
|
||||
pub fn getAssignedSlot(self: *Text, frame: *Frame) ?*Slot {
|
||||
return frame.findSlotForSlottable(self._proto.asNode(), true);
|
||||
}
|
||||
|
||||
pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text {
|
||||
const data = self._proto._data.str();
|
||||
|
||||
@@ -77,5 +82,6 @@ pub const JsApi = struct {
|
||||
|
||||
pub const constructor = bridge.constructor(Text.init, .{});
|
||||
pub const wholeText = bridge.accessor(Text.getWholeText, null, .{});
|
||||
pub const assignedSlot = bridge.accessor(Text.getAssignedSlot, null, .{});
|
||||
pub const splitText = bridge.function(Text.splitText, .{ .dom_exception = true });
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -7,11 +6,15 @@ const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const ShadowRoot = @import("../../ShadowRoot.zig");
|
||||
|
||||
const log = lp.log;
|
||||
|
||||
const Slot = @This();
|
||||
|
||||
_proto: *HtmlElement,
|
||||
// DOM spec "assigned nodes". Maintained by Frame.assignSlottables; always
|
||||
// empty while the slot isn't in a shadow tree.
|
||||
_assigned: std.ArrayList(*Node) = .empty,
|
||||
// DOM spec "manually assigned nodes", set via assign(). Only consulted when
|
||||
// the shadow root was attached with slotAssignment: "manual".
|
||||
_manually_assigned: std.ArrayList(*Node) = .empty,
|
||||
|
||||
pub fn asElement(self: *Slot) *Element {
|
||||
return self._proto._proto;
|
||||
@@ -39,15 +42,26 @@ const AssignedNodesOptions = struct {
|
||||
|
||||
pub fn assignedNodes(self: *Slot, opts_: ?AssignedNodesOptions, frame: *Frame) ![]const *Node {
|
||||
const opts = opts_ orelse AssignedNodesOptions{};
|
||||
if (!opts.flatten) {
|
||||
return self._assigned.items;
|
||||
}
|
||||
var nodes: std.ArrayList(*Node) = .empty;
|
||||
try self.collectAssignedNodes(false, &nodes, opts, frame);
|
||||
try self.collectFlattened(false, &nodes, frame);
|
||||
return nodes.items;
|
||||
}
|
||||
|
||||
pub fn assignedElements(self: *Slot, opts_: ?AssignedNodesOptions, frame: *Frame) ![]const *Element {
|
||||
const opts = opts_ orelse AssignedNodesOptions{};
|
||||
var elements: std.ArrayList(*Element) = .empty;
|
||||
try self.collectAssignedNodes(true, &elements, opts, frame);
|
||||
const opts = opts_ orelse AssignedNodesOptions{};
|
||||
if (!opts.flatten) {
|
||||
for (self._assigned.items) |node| {
|
||||
if (node.is(Element)) |el| {
|
||||
try elements.append(frame.call_arena, el);
|
||||
}
|
||||
}
|
||||
return elements.items;
|
||||
}
|
||||
try self.collectFlattened(true, &elements, frame);
|
||||
return elements.items;
|
||||
}
|
||||
|
||||
@@ -55,97 +69,91 @@ fn CollectionType(comptime elements: bool) type {
|
||||
return if (elements) *std.ArrayList(*Element) else *std.ArrayList(*Node);
|
||||
}
|
||||
|
||||
fn collectAssignedNodes(self: *Slot, comptime elements: bool, coll: CollectionType(elements), opts: AssignedNodesOptions, frame: *Frame) !void {
|
||||
// Find the shadow root this slot belongs to
|
||||
const shadow_root = self.findShadowRoot() orelse return;
|
||||
// DOM spec "find flattened slottables"
|
||||
fn collectFlattened(self: *Slot, comptime elements: bool, coll: CollectionType(elements), frame: *Frame) error{OutOfMemory}!void {
|
||||
if (self.asNode().getRootNode(.{}).is(ShadowRoot) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slot_name = self.getName();
|
||||
const allocator = frame.call_arena;
|
||||
if (self._assigned.items.len > 0) {
|
||||
for (self._assigned.items) |node| {
|
||||
try appendFlattened(elements, coll, node, frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const host = shadow_root.getHost();
|
||||
const initial_count = coll.items.len;
|
||||
var it = host.asNode().childrenIterator();
|
||||
// no assigned nodes; flatten the slot's fallback content
|
||||
var it = self.asNode().childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (!isAssignedToSlot(child, slot_name)) {
|
||||
if (!Frame.isSlottable(child)) {
|
||||
continue;
|
||||
}
|
||||
try appendFlattened(elements, coll, child, frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.flatten) {
|
||||
if (child.is(Slot)) |child_slot| {
|
||||
// Only flatten if the child slot is actually in a shadow tree
|
||||
if (child_slot.findShadowRoot()) |_| {
|
||||
try child_slot.collectAssignedNodes(elements, coll, opts, frame);
|
||||
continue;
|
||||
fn appendFlattened(comptime elements: bool, coll: CollectionType(elements), node: *Node, frame: *Frame) error{OutOfMemory}!void {
|
||||
if (node.is(Slot)) |nested| {
|
||||
// a slottable (or fallback child) that is itself a slot in a shadow
|
||||
// tree flattens to its own flattened slottables
|
||||
if (nested.asNode().getRootNode(.{}).is(ShadowRoot) != null) {
|
||||
return nested.collectFlattened(elements, coll, frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime elements) {
|
||||
if (node.is(Element)) |el| {
|
||||
try coll.append(frame.call_arena, el);
|
||||
}
|
||||
} else {
|
||||
try coll.append(frame.call_arena, node);
|
||||
}
|
||||
}
|
||||
|
||||
// DOM spec HTMLSlotElement.assign(...nodes). Takes js.Value so the bridge
|
||||
// always treats the parameter as variadic: per WebIDL it's a rest parameter
|
||||
// of (Element or Text), so passing an array must throw a TypeError.
|
||||
pub fn assign(self: *Slot, values: []const js.Value, frame: *Frame) !void {
|
||||
const nodes = try frame.call_arena.alloc(*Node, values.len);
|
||||
for (values, nodes) |value, *entry| {
|
||||
const node = value.toZig(*Node) catch return error.TypeError;
|
||||
if (!Frame.isSlottable(node)) {
|
||||
return error.TypeError;
|
||||
}
|
||||
entry.* = node;
|
||||
}
|
||||
|
||||
for (self._manually_assigned.items) |node| {
|
||||
_ = frame._manual_slot_assignments.remove(node);
|
||||
}
|
||||
self._manually_assigned.clearRetainingCapacity();
|
||||
|
||||
for (nodes) |node| {
|
||||
const gop = try frame._manual_slot_assignments.getOrPut(frame.arena, node);
|
||||
if (gop.found_existing) {
|
||||
const other = gop.value_ptr.*;
|
||||
if (other == self) {
|
||||
// duplicate within `nodes`; an ordered set keeps the first position
|
||||
continue;
|
||||
}
|
||||
// steal the node from the slot it was previously assigned to
|
||||
for (other._manually_assigned.items, 0..) |n, i| {
|
||||
if (n == node) {
|
||||
_ = other._manually_assigned.orderedRemove(i);
|
||||
break;
|
||||
}
|
||||
// Otherwise, treat it as a regular element and fall through
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime elements) {
|
||||
if (child.is(Element)) |el| {
|
||||
try coll.append(allocator, el);
|
||||
}
|
||||
} else {
|
||||
try coll.append(allocator, child);
|
||||
}
|
||||
gop.value_ptr.* = self;
|
||||
try self._manually_assigned.append(frame.arena, node);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
const root = self.asNode().getRootNode(.{});
|
||||
if (root.is(ShadowRoot) != null) {
|
||||
frame.assignSlottablesForTree(root);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assign(self: *Slot, nodes: []const *Node) void {
|
||||
// Imperative slot assignment API
|
||||
// This would require storing manually assigned nodes
|
||||
// For now, this is a placeholder for the API
|
||||
_ = self;
|
||||
_ = nodes;
|
||||
|
||||
// let's see if this is ever actually used
|
||||
log.warn(.not_implemented, "Slot.assign", .{});
|
||||
}
|
||||
|
||||
fn findShadowRoot(self: *Slot) ?*ShadowRoot {
|
||||
// Walk up the parent chain to find the shadow root
|
||||
var parent = self.asNode()._parent;
|
||||
while (parent) |p| {
|
||||
if (p.is(ShadowRoot)) |shadow_root| {
|
||||
return shadow_root;
|
||||
}
|
||||
parent = p._parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn isAssignedToSlot(node: *Node, slot_name: []const u8) bool {
|
||||
// Check if a node should be assigned to a slot with the given name
|
||||
if (node.is(Element)) |element| {
|
||||
// Get the slot attribute from the element
|
||||
const node_slot = element.getAttributeSafe(comptime .wrap("slot")) orelse "";
|
||||
|
||||
// Match if:
|
||||
// - Both are empty (default slot)
|
||||
// - They match exactly
|
||||
return std.mem.eql(u8, node_slot, slot_name);
|
||||
}
|
||||
|
||||
// Text nodes, comments, etc. are only assigned to the default slot
|
||||
// (when they have no preceding/following element siblings with slot attributes)
|
||||
// For simplicity, text nodes go to default slot if slot_name is empty
|
||||
return slot_name.len == 0;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Slot);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user