mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Revert "Merge pull request #2394 from lightpanda-io/custom_element_reactions"
This reverts commit5c7097b6a0, reversing changes made to235f1cd9bc.
This commit is contained in:
@@ -1,129 +0,0 @@
|
||||
// Copyright (C) 2023-2026 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/>.
|
||||
|
||||
// Implements the spec's "custom element reactions" mechanism: callbacks
|
||||
// (connectedCallback, disconnectedCallback, adoptedCallback,
|
||||
// attributeChangedCallback) are enqueued during DOM mutation and invoked at
|
||||
// the outer algorithm boundary, not synchronously mid-mutation.
|
||||
//
|
||||
// The "stack of element queues" is collapsed to a single flat ArrayList plus
|
||||
// per-scope checkpoint indices: push() captures items.len, popAndInvoke()
|
||||
// drains items[checkpoint..] and truncates. Nested scopes work naturally —
|
||||
// inside a callback, a new scope captures its own checkpoint past the current
|
||||
// length, drains its own range, and the outer iteration continues from where
|
||||
// it left off.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const Frame = @import("Frame.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const Document = @import("webapi/Document.zig");
|
||||
const Custom = @import("webapi/element/html/Custom.zig");
|
||||
|
||||
const String = lp.String;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
queue: std.ArrayList(Reaction) = .empty,
|
||||
// Number of currently-open scopes (push() that hasn't been pop'd). Every
|
||||
// enqueue must happen inside a scope — that's the leak-detection invariant.
|
||||
// Checked in debug at enqueue time so leaks surface where the bug is, not
|
||||
// later at some unrelated boundary.
|
||||
active_scopes: u32 = 0,
|
||||
|
||||
/// Open a new reactions scope. Returns a checkpoint to be passed to popAndInvoke.
|
||||
pub fn push(self: *Self) usize {
|
||||
self.active_scopes += 1;
|
||||
return self.queue.items.len;
|
||||
}
|
||||
|
||||
/// Drain reactions queued at indices >= checkpoint, then truncate. Reactions
|
||||
/// enqueued within a nested scope drain at that scope's pop, before this loop
|
||||
/// sees them.
|
||||
pub fn popAndInvoke(self: *Self, checkpoint: usize, frame: *Frame) void {
|
||||
for (self.queue.items[checkpoint..]) |reaction| {
|
||||
Custom.fireReaction(reaction, frame);
|
||||
}
|
||||
self.queue.items.len = checkpoint;
|
||||
self.active_scopes -= 1;
|
||||
}
|
||||
|
||||
inline fn assertScopeActive(self: *const Self) void {
|
||||
lp.assert(self.active_scopes > 0, "ce_reactions enqueue without active scope", .{});
|
||||
}
|
||||
|
||||
pub fn enqueueConnected(self: *Self, element: *Element) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .connected = element });
|
||||
}
|
||||
|
||||
pub fn enqueueDisconnected(self: *Self, element: *Element) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .disconnected = element });
|
||||
}
|
||||
|
||||
pub fn enqueueAdopted(self: *Self, element: *Element, old_document: *Document, new_document: *Document) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .adopted = .{
|
||||
.element = element,
|
||||
.old_document = old_document,
|
||||
.new_document = new_document,
|
||||
} });
|
||||
}
|
||||
|
||||
pub fn enqueueAttributeChanged(
|
||||
self: *Self,
|
||||
element: *Element,
|
||||
name: String,
|
||||
old_value: ?String,
|
||||
new_value: ?String,
|
||||
namespace: ?String,
|
||||
) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .attribute_changed = .{
|
||||
.name = name,
|
||||
.element = element,
|
||||
.old_value = old_value,
|
||||
.new_value = new_value,
|
||||
.namespace = namespace,
|
||||
} });
|
||||
}
|
||||
|
||||
pub const Reaction = union(enum) {
|
||||
connected: *Element,
|
||||
disconnected: *Element,
|
||||
adopted: Adopted,
|
||||
attribute_changed: AttributeChanged,
|
||||
|
||||
pub const Adopted = struct {
|
||||
element: *Element,
|
||||
old_document: *Document,
|
||||
new_document: *Document,
|
||||
};
|
||||
|
||||
pub const AttributeChanged = struct {
|
||||
element: *Element,
|
||||
name: String,
|
||||
old_value: ?String,
|
||||
new_value: ?String,
|
||||
namespace: ?String,
|
||||
};
|
||||
};
|
||||
@@ -32,8 +32,6 @@ const StyleManager = @import("StyleManager.zig");
|
||||
const Parser = @import("parser/Parser.zig");
|
||||
const h5e = @import("parser/html5ever.zig");
|
||||
|
||||
const CustomElementReactions = @import("CustomElementReactions.zig");
|
||||
|
||||
const URL = @import("URL.zig");
|
||||
const Blob = @import("webapi/Blob.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
@@ -190,12 +188,6 @@ _upgrading_element: ?*Node = null,
|
||||
// List of custom elements that were created before their definition was registered
|
||||
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
|
||||
|
||||
// Pending custom-element reactions (connected/disconnected/adopted/attribute
|
||||
// changed). Reactions are enqueued during DOM mutation and drained at the
|
||||
// outer algorithm boundary — set up by the JS bridge for [CEReactions]
|
||||
// methods and by the parser pump on each yield.
|
||||
_ce_reactions: CustomElementReactions,
|
||||
|
||||
// for heap allocations and managing WebAPI objects
|
||||
_factory: *Factory,
|
||||
|
||||
@@ -295,7 +287,6 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void {
|
||||
._type = if (parent == null) .root else .frame,
|
||||
._style_manager = undefined,
|
||||
._script_manager = undefined,
|
||||
._ce_reactions = .{ .allocator = arena },
|
||||
._event_manager = EventManager.init(arena, self),
|
||||
};
|
||||
self._to_load = &self._to_load_1;
|
||||
@@ -1861,7 +1852,7 @@ pub fn adoptNodeTree(self: *Frame, node: *Node, old_owner: *Document, new_owner:
|
||||
|
||||
// Per spec, adopted steps run on each element after its document is set.
|
||||
if (node.is(Element)) |el| {
|
||||
Element.Html.Custom.enqueueAdoptedCallbackOnElement(el, old_owner, new_owner, self);
|
||||
Element.Html.Custom.invokeAdoptedCallbackOnElement(el, old_owner, new_owner, self);
|
||||
}
|
||||
|
||||
var it = node.childrenIterator();
|
||||
@@ -2597,7 +2588,7 @@ pub fn createElementNS(self: *Frame, namespace: Element.Namespace, name: []const
|
||||
if (element._attributes) |attributes| {
|
||||
var it = attributes.iterator();
|
||||
while (it.next()) |attr| {
|
||||
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(
|
||||
element,
|
||||
attr._name,
|
||||
null, // old_value is null for initial attributes
|
||||
@@ -2990,7 +2981,7 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt
|
||||
self.removeElementIdWithMaps(id_maps.?, id);
|
||||
}
|
||||
|
||||
Element.Html.Custom.enqueueDisconnectedCallbackOnElement(el, self);
|
||||
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
||||
|
||||
// If a <style> element is being removed, remove its sheet from the list
|
||||
if (el.is(Element.Html.Style)) |style| {
|
||||
@@ -3013,8 +3004,11 @@ pub fn appendAllChildren(self: *Frame, parent: *Node, target: *Node) !void {
|
||||
self.domChanged();
|
||||
const dest_connected = target.isConnected();
|
||||
|
||||
var it = parent.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
// Use firstChild() instead of iterator to handle cases where callbacks
|
||||
// (like custom element connectedCallback) modify the parent during iteration.
|
||||
// The iterator captures "next" pointers that can become stale.
|
||||
while (parent.firstChild()) |child| {
|
||||
// Check if child was connected BEFORE removing it from parent
|
||||
const child_was_connected = child.isConnected();
|
||||
self.removeNode(parent, child, .{ .will_be_reconnected = dest_connected });
|
||||
try self.appendNode(target, child, .{ .child_already_connected = child_was_connected });
|
||||
@@ -3025,16 +3019,31 @@ pub fn insertAllChildrenBefore(self: *Frame, fragment: *Node, parent: *Node, ref
|
||||
self.domChanged();
|
||||
const dest_connected = parent.isConnected();
|
||||
|
||||
var it = fragment.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
// Use firstChild() instead of iterator to handle cases where callbacks
|
||||
// (like custom element connectedCallback) modify the fragment during iteration.
|
||||
// The iterator captures "next" pointers that can become stale.
|
||||
while (fragment.firstChild()) |child| {
|
||||
// Check if child was connected BEFORE removing it from fragment
|
||||
const child_was_connected = child.isConnected();
|
||||
self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });
|
||||
try self.insertNodeRelative(
|
||||
parent,
|
||||
child,
|
||||
.{ .before = ref_node },
|
||||
.{ .child_already_connected = child_was_connected },
|
||||
);
|
||||
// A callback fired by a previous iteration's insert (e.g. a custom
|
||||
// element's connectedCallback) may have detached ref_node from
|
||||
// parent. In that case, fall back to append so the remaining
|
||||
// children still land in `parent` in source order.
|
||||
if (ref_node._parent == parent) {
|
||||
try self.insertNodeRelative(
|
||||
parent,
|
||||
child,
|
||||
.{ .before = ref_node },
|
||||
.{ .child_already_connected = child_was_connected },
|
||||
);
|
||||
} else {
|
||||
try self.appendNode(
|
||||
parent,
|
||||
child,
|
||||
.{ .child_already_connected = child_was_connected },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3158,7 +3167,7 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||
try self.addElementId(parent, el, id);
|
||||
}
|
||||
try Element.Html.Custom.enqueueConnectedCallbackOnElement(true, el, self);
|
||||
try Element.Html.Custom.invokeConnectedCallbackOnElement(true, el, self);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -3204,7 +3213,7 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
}
|
||||
|
||||
if (should_invoke_connected) {
|
||||
try Element.Html.Custom.enqueueConnectedCallbackOnElement(false, el, self);
|
||||
try Element.Html.Custom.invokeConnectedCallbackOnElement(false, el, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3214,7 +3223,7 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str
|
||||
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
|
||||
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(element, name, old_value, value, null, self);
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, null, self);
|
||||
|
||||
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
@@ -3240,7 +3249,7 @@ pub fn attributeRemove(self: *Frame, element: *Element, name: String, old_value:
|
||||
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
|
||||
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(element, name, old_value, null, null, self);
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, null, self);
|
||||
|
||||
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
|
||||
@@ -749,13 +749,6 @@ pub const Script = struct {
|
||||
try_catch.init(local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
// Custom-element reactions: the script body is a JS-execution
|
||||
// boundary. Open a scope so any reactions it queues (or that were
|
||||
// queued by the parser since the previous boundary) drain at the
|
||||
// end of the script, before the parser resumes.
|
||||
const ce_checkpoint = frame._ce_reactions.push();
|
||||
defer frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
|
||||
|
||||
const success = blk: {
|
||||
const content = self.source.content();
|
||||
switch (fe.kind) {
|
||||
|
||||
@@ -569,7 +569,6 @@ pub const Function = struct {
|
||||
null_as_undefined: bool = false,
|
||||
cache: ?Caching = null,
|
||||
embedded_receiver: bool = false,
|
||||
ce_reactions: bool = false,
|
||||
|
||||
// We support two ways to cache a value directly into a v8::Object. The
|
||||
// difference between the two is like the difference between a Map
|
||||
@@ -622,26 +621,6 @@ pub const Function = struct {
|
||||
caller.initWithContext(ctx, v8_context);
|
||||
defer caller.deinit();
|
||||
|
||||
// [CEReactions] entry: open a reactions scope so any custom-element
|
||||
// callbacks queued by DOM mutation inside `func` fire after it
|
||||
// returns, never mid-algorithm.
|
||||
var ce_checkpoint: usize = undefined;
|
||||
const ce_frame: ?*Frame = if (comptime opts.ce_reactions) switch (ctx.global) {
|
||||
.frame => |frame| frame,
|
||||
.worker => null,
|
||||
} else null;
|
||||
|
||||
if (comptime opts.ce_reactions) {
|
||||
if (ce_frame) |frame| {
|
||||
ce_checkpoint = frame._ce_reactions.push();
|
||||
}
|
||||
}
|
||||
defer if (comptime opts.ce_reactions) {
|
||||
if (ce_frame) |frame| {
|
||||
frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
|
||||
}
|
||||
};
|
||||
|
||||
const js_value = _call(T, &caller.local, info, func, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), &caller.local, err, info, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
|
||||
@@ -130,18 +130,6 @@ pub const Constructor = struct {
|
||||
}
|
||||
defer caller.deinit();
|
||||
|
||||
// Constructors are a JS-execution boundary, just like
|
||||
// [CEReactions] methods. Open a reactions scope so any
|
||||
// callbacks queued by the user's constructor body (or
|
||||
// by attribute_changed reactions queued before invocation)
|
||||
// drain at the constructor's exit, not later.
|
||||
const ce_frame: ?*Frame = switch (caller.local.ctx.global) {
|
||||
.frame => |frame| frame,
|
||||
.worker => null,
|
||||
};
|
||||
const ce_checkpoint: usize = if (ce_frame) |frame| frame._ce_reactions.push() else 0;
|
||||
defer if (ce_frame) |frame| frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
|
||||
|
||||
caller.constructor(T, func, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.new_target = opts.new_target,
|
||||
|
||||
@@ -346,8 +346,6 @@ fn parseErrorCallback(ctx: *anyopaque, err: h5e.StringSlice) callconv(.c) void {
|
||||
|
||||
fn popCallback(ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._popCallback(getNode(node_ref)) catch |err| {
|
||||
self.err = .{ .err = err, .source = .pop };
|
||||
};
|
||||
@@ -370,8 +368,6 @@ fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualNa
|
||||
|
||||
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {
|
||||
self.err = .{ .err = err, .source = .create_element };
|
||||
return null;
|
||||
@@ -394,8 +390,6 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName,
|
||||
|
||||
fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*anyopaque {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
return self._createCommentCallback(str.slice()) catch |err| {
|
||||
self.err = .{ .err = err, .source = .create_comment };
|
||||
return null;
|
||||
@@ -414,8 +408,6 @@ fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {
|
||||
|
||||
fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h5e.StringSlice) callconv(.c) ?*anyopaque {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
return self._createProcessingInstruction(target.slice(), data.slice()) catch |err| {
|
||||
self.err = .{ .err = err, .source = .create_processing_instruction };
|
||||
return null;
|
||||
@@ -434,8 +426,6 @@ fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const
|
||||
|
||||
fn appendDoctypeToDocument(ctx: *anyopaque, name: h5e.StringSlice, public_id: h5e.StringSlice, system_id: h5e.StringSlice) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._appendDoctypeToDocument(name.slice(), public_id.slice(), system_id.slice()) catch |err| {
|
||||
self.err = .{ .err = err, .source = .append_doctype_to_document };
|
||||
};
|
||||
@@ -458,8 +448,6 @@ fn _appendDoctypeToDocument(self: *Parser, name: []const u8, public_id: []const
|
||||
|
||||
fn addAttrsIfMissingCallback(ctx: *anyopaque, target_ref: *anyopaque, attributes: h5e.AttributeIterator) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._addAttrsIfMissingCallback(getNode(target_ref), attributes) catch |err| {
|
||||
self.err = .{ .err = err, .source = .add_attrs_if_missing };
|
||||
};
|
||||
@@ -509,8 +497,6 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
|
||||
|
||||
fn appendCallback(ctx: *anyopaque, parent_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._appendCallback(getNode(parent_ref), node_or_text) catch |err| {
|
||||
self.err = .{ .err = err, .source = .append };
|
||||
};
|
||||
@@ -543,8 +529,6 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
|
||||
|
||||
fn removeFromParentCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._removeFromParentCallback(getNode(target_ref)) catch |err| {
|
||||
self.err = .{ .err = err, .source = .remove_from_parent };
|
||||
};
|
||||
@@ -561,8 +545,6 @@ fn _removeFromParentCallback(self: *Parser, node: *Node) !void {
|
||||
|
||||
fn reparentChildrenCallback(ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._reparentChildrenCallback(getNode(node_ref), getNode(new_parent_ref)) catch |err| {
|
||||
self.err = .{ .err = err, .source = .reparent_children };
|
||||
};
|
||||
@@ -577,8 +559,6 @@ fn _reparentChildrenCallback(self: *Parser, node: *Node, new_parent: *Node) !voi
|
||||
|
||||
fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._appendBeforeSiblingCallback(getNode(sibling_ref), node_or_text) catch |err| {
|
||||
self.err = .{ .err = err, .source = .append_before_sibling };
|
||||
};
|
||||
@@ -607,8 +587,6 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e
|
||||
|
||||
fn appendBasedOnParentNodeCallback(ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
|
||||
self._appendBasedOnParentNodeCallback(getNode(element_ref), getNode(prev_element_ref), node_or_text) catch |err| {
|
||||
self.err = .{ .err = err, .source = .append_based_on_parent_node };
|
||||
};
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
<body>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!-- Custom-element reactions queue during outerHTML's algorithm and fire
|
||||
at the boundary, not mid-loop. By the time connectedCallback runs,
|
||||
insertAllChildrenBefore has already inserted every fragment child
|
||||
and setOuterHTML has detached the original `target`, so the callback
|
||||
sees the post-state — its check observes target already detached and
|
||||
leaves the DOM untouched. -->
|
||||
<script id="outerHTML_callback_observes_post_state">
|
||||
<!-- Regression: Frame.insertAllChildrenBefore (used by Element.outerHTML
|
||||
setter) loops over fragment children and inserts each one .before
|
||||
ref_node. _insertNodeRelative invokes connectedCallback for custom
|
||||
elements, and that callback can detach ref_node from parent. The
|
||||
next iteration must not panic on `ref_node._parent.? == parent`. -->
|
||||
<script id="outerHTML_callback_detaches_ref_node">
|
||||
{
|
||||
let targetParentAtCallback = undefined;
|
||||
class DetachTarget extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const target = document.getElementById('outer_target');
|
||||
targetParentAtCallback = target && target.parentNode;
|
||||
if (target && target.parentNode) {
|
||||
target.parentNode.removeChild(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define('detach-target-1', DetachTarget);
|
||||
@@ -26,48 +26,60 @@
|
||||
target.id = 'outer_target';
|
||||
wrap.appendChild(target);
|
||||
|
||||
// Two siblings: the first one's connectedCallback removes `target`
|
||||
// (which is the ref_node passed to insertAllChildrenBefore). The
|
||||
// second one then tries to insert before a parentless ref_node.
|
||||
target.outerHTML = '<detach-target-1></detach-target-1><b id="second">x</b>';
|
||||
|
||||
// Both replacement children landed; the original target was removed
|
||||
// before the queued connectedCallback ran.
|
||||
// We don't strictly care about the final shape, just that we got
|
||||
// here without crashing. Sanity-check the first child landed.
|
||||
testing.expectTrue(wrap.querySelector('detach-target-1') !== null);
|
||||
testing.expectTrue(wrap.querySelector('#second') !== null);
|
||||
testing.expectEqual(null, targetParentAtCallback);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- innerHTML removes existing children, then parses and inserts the
|
||||
new content. With queued reactions, the disconnectedCallbacks for
|
||||
the removed elements fire after the whole algorithm completes — so
|
||||
they observe `host` already populated with the new content. -->
|
||||
<script id="innerHTML_disconnected_observes_post_state">
|
||||
<!-- Regression: Element.setInnerHTML removes existing children using
|
||||
`childrenIterator()`. removeNode fires disconnectedCallback for
|
||||
custom elements, and that callback can mutate the host's child
|
||||
list, leaving the iterator advancing through a node that no longer
|
||||
belongs to the parent. -->
|
||||
<script id="innerHTML_disconnected_mutates_siblings">
|
||||
{
|
||||
let host;
|
||||
let lastSeenChildCount = -1;
|
||||
let lastSeenFirstTag = null;
|
||||
class SiblingObserver extends HTMLElement {
|
||||
let removeNextOnDisconnect = false;
|
||||
class SiblingZapper extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
lastSeenChildCount = host.children.length;
|
||||
lastSeenFirstTag = host.children.length > 0 ? host.children[0].tagName : null;
|
||||
if (!removeNextOnDisconnect) return;
|
||||
removeNextOnDisconnect = false;
|
||||
// Reach into the parent (captured via closure) to remove a
|
||||
// sibling that the outer setInnerHTML loop is about to visit.
|
||||
// The element being removed here is what the iterator's
|
||||
// current node still points its `next` pointer at.
|
||||
if (host.children.length > 0) {
|
||||
host.removeChild(host.children[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define('sibling-zapper-2', SiblingObserver);
|
||||
customElements.define('sibling-zapper-2', SiblingZapper);
|
||||
|
||||
host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
|
||||
host.appendChild(document.createElement('sibling-zapper-2'));
|
||||
host.appendChild(document.createElement('sibling-zapper-2'));
|
||||
host.appendChild(document.createElement('span'));
|
||||
const a = document.createElement('sibling-zapper-2');
|
||||
const b = document.createElement('sibling-zapper-2');
|
||||
const c = document.createElement('span');
|
||||
host.appendChild(a);
|
||||
host.appendChild(b);
|
||||
host.appendChild(c);
|
||||
|
||||
removeNextOnDisconnect = true;
|
||||
|
||||
// Triggers the removal loop in setInnerHTML. Removing `a` fires its
|
||||
// disconnectedCallback, which removes `b` (the iterator's next
|
||||
// target). The loop must not visit a node whose `_parent` is null.
|
||||
host.innerHTML = '<i>after</i>';
|
||||
|
||||
// The disconnectedCallback fires after innerHTML completes; by then
|
||||
// the new <i> is already in place.
|
||||
testing.expectEqual(1, host.children.length);
|
||||
testing.expectEqual('I', host.children[0].tagName);
|
||||
testing.expectEqual(1, lastSeenChildCount);
|
||||
testing.expectEqual('I', lastSeenFirstTag);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -31,19 +31,18 @@
|
||||
|
||||
<div id=d3></div>
|
||||
<script id=appendChild_fragment_mutation>
|
||||
// Custom-element reactions are queued during the appendChild algorithm
|
||||
// and fire after it completes. The connectedCallback below sees the
|
||||
// post-move DOM state — both fragment children are already in d3 — so
|
||||
// its check `b.parentNode === fragment` is false and it leaves b alone.
|
||||
// Test that appendChild with DocumentFragment handles synchronous callbacks
|
||||
// (like custom element connectedCallback) that modify the fragment during iteration.
|
||||
// This reproduces a bug where the iterator captures "next" node pointers
|
||||
// before processing, but callbacks can remove those nodes from the fragment.
|
||||
const d3 = $('#d3');
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Create custom element whose connectedCallback modifies the fragment
|
||||
let bElement = null;
|
||||
let parentSeenAtCallback = null;
|
||||
class ModifyingElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
// Callback fires after the move; record what we observe.
|
||||
parentSeenAtCallback = bElement && bElement.parentNode;
|
||||
// When this element is connected, remove 'b' from the fragment
|
||||
if (bElement && bElement.parentNode === fragment) {
|
||||
fragment.removeChild(bElement);
|
||||
}
|
||||
@@ -59,37 +58,39 @@
|
||||
fragment.appendChild(a);
|
||||
fragment.appendChild(b);
|
||||
|
||||
// This should not crash - appendChild should handle the modification gracefully
|
||||
d3.appendChild(fragment);
|
||||
|
||||
// Both moved atomically. Callback observed b already in d3.
|
||||
assertChildren(['a', 'b'], d3);
|
||||
testing.expectEqual(d3, parentSeenAtCallback);
|
||||
// 'a' should be in d3, 'b' was removed by connectedCallback and is now detached
|
||||
assertChildren(['a'], d3);
|
||||
testing.expectEqual(null, b.parentNode);
|
||||
</script>
|
||||
|
||||
<div id=d4></div>
|
||||
<div id=d4_stash></div>
|
||||
<script id=appendChild_disconnect_callback_observes_post_move>
|
||||
// Custom-element reactions queue and drain at the algorithm boundary, so
|
||||
// disconnectedCallback fires after the whole move completes. By then the
|
||||
// element is already in its new (disconnected) parent — the callback
|
||||
// observes the post-move state, never a parentless intermediate.
|
||||
<script id=appendChild_disconnect_callback_reparents>
|
||||
// Moving a connected child into a disconnected target makes
|
||||
// will_be_reconnected=false, so disconnectedCallback fires synchronously
|
||||
// inside removeNode. The callback re-parents the child; appendChild
|
||||
// respects that placement instead of overriding it.
|
||||
const d4 = $('#d4');
|
||||
const stash = $('#d4_stash');
|
||||
|
||||
let parentSeenAtDisconnect = undefined;
|
||||
class ObserveOnDisconnect extends HTMLElement {
|
||||
class ReparentOnDisconnect extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
parentSeenAtDisconnect = this.parentNode;
|
||||
if (this.parentNode === null) {
|
||||
stash.appendChild(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define('observe-on-disconnect', ObserveOnDisconnect);
|
||||
customElements.define('reparent-on-disconnect', ReparentOnDisconnect);
|
||||
|
||||
const ood = document.createElement('observe-on-disconnect');
|
||||
ood.id = 'ood';
|
||||
d4.appendChild(ood);
|
||||
const rpd = document.createElement('reparent-on-disconnect');
|
||||
rpd.id = 'rpd';
|
||||
d4.appendChild(rpd);
|
||||
|
||||
const detached = document.createElement('div');
|
||||
detached.appendChild(ood);
|
||||
detached.appendChild(rpd);
|
||||
|
||||
testing.expectEqual(detached, ood.parentNode);
|
||||
testing.expectEqual(detached, parentSeenAtDisconnect);
|
||||
testing.expectEqual(stash, rpd.parentNode);
|
||||
</script>
|
||||
|
||||
@@ -42,18 +42,21 @@
|
||||
|
||||
<div id=d3></div>
|
||||
<div id=d3_stash></div>
|
||||
<script id=insertBefore_disconnect_callback_observes_post_move>
|
||||
// disconnectedCallback fires after insertBefore completes, so the
|
||||
// callback observes the element already placed in its new parent.
|
||||
<script id=insertBefore_disconnect_callback_reparents>
|
||||
// Same disconnectedCallback re-parenting pattern as in append_child.html,
|
||||
// exercised through insertBefore. insertBefore respects the callback's
|
||||
// placement instead of overriding it.
|
||||
const d3 = $('#d3');
|
||||
const stash = $('#d3_stash');
|
||||
|
||||
let parentSeenAtDisconnect = undefined;
|
||||
class IBObserveOnDisconnect extends HTMLElement {
|
||||
class IBReparentOnDisconnect extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
parentSeenAtDisconnect = this.parentNode;
|
||||
if (this.parentNode === null) {
|
||||
stash.appendChild(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define('ib-reparent-on-disconnect', IBObserveOnDisconnect);
|
||||
customElements.define('ib-reparent-on-disconnect', IBReparentOnDisconnect);
|
||||
|
||||
const moving = document.createElement('ib-reparent-on-disconnect');
|
||||
moving.id = 'ib_moving';
|
||||
@@ -65,6 +68,5 @@
|
||||
|
||||
detached.insertBefore(moving, ref);
|
||||
|
||||
testing.expectEqual(detached, moving.parentNode);
|
||||
testing.expectEqual(detached, parentSeenAtDisconnect);
|
||||
testing.expectEqual(stash, moving.parentNode);
|
||||
</script>
|
||||
|
||||
@@ -423,19 +423,19 @@ pub const JsApi = struct {
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const data = bridge.accessor(CData.getData, CData._setData, .{ .ce_reactions = true });
|
||||
pub const data = bridge.accessor(CData.getData, CData._setData, .{});
|
||||
pub const length = bridge.accessor(CData.getLength, null, .{});
|
||||
|
||||
pub const appendData = bridge.function(CData.appendData, .{ .ce_reactions = true });
|
||||
pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const appendData = bridge.function(CData.appendData, .{});
|
||||
pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true });
|
||||
pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true });
|
||||
pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true });
|
||||
pub const substringData = bridge.function(CData.substringData, .{ .dom_exception = true });
|
||||
|
||||
pub const remove = bridge.function(CData.remove, .{ .ce_reactions = true });
|
||||
pub const before = bridge.function(CData.before, .{ .ce_reactions = true });
|
||||
pub const after = bridge.function(CData.after, .{ .ce_reactions = true });
|
||||
pub const replaceWith = bridge.function(CData.replaceWith, .{ .ce_reactions = true });
|
||||
pub const remove = bridge.function(CData.remove, .{});
|
||||
pub const before = bridge.function(CData.before, .{});
|
||||
pub const after = bridge.function(CData.after, .{});
|
||||
pub const replaceWith = bridge.function(CData.replaceWith, .{});
|
||||
|
||||
pub const nextElementSibling = bridge.accessor(CData.nextElementSibling, null, .{});
|
||||
pub const previousElementSibling = bridge.accessor(CData.previousElementSibling, null, .{});
|
||||
|
||||
@@ -190,20 +190,17 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
|
||||
return error.CustomElementUpgradeFailed;
|
||||
};
|
||||
|
||||
// Enqueue attributeChangedCallback for existing observed attributes
|
||||
const element = custom.asElement();
|
||||
var attr_it = element.attributeIterator();
|
||||
// Invoke attributeChangedCallback for existing observed attributes
|
||||
var attr_it = custom.asElement().attributeIterator();
|
||||
while (attr_it.next()) |attr| {
|
||||
const name = attr._name;
|
||||
if (definition.isAttributeObserved(name)) {
|
||||
Custom.enqueueAttributeChangedCallbackOnElement(element, name, null, attr._value, null, frame);
|
||||
custom.invokeAttributeChangedCallback(name, null, attr._value, null, frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.isConnected()) {
|
||||
Custom.enqueueConnectedCallbackOnElement(false, element, frame) catch |err| {
|
||||
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
|
||||
};
|
||||
custom.invokeConnectedCallback(frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(DOMParser.init, .{});
|
||||
pub const parseFromString = bridge.function(DOMParser.parseFromString, .{ .ce_reactions = true });
|
||||
pub const parseFromString = bridge.function(DOMParser.parseFromString, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -1162,15 +1162,15 @@ pub const JsApi = struct {
|
||||
pub const getSelection = bridge.function(Document.getSelection, .{});
|
||||
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
|
||||
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
|
||||
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const append = bridge.function(Document.append, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
|
||||
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true });
|
||||
pub const append = bridge.function(Document.append, .{ .dom_exception = true });
|
||||
pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true });
|
||||
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true });
|
||||
pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});
|
||||
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
|
||||
pub const write = bridge.function(Document.write, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const writeln = bridge.function(Document.writeln, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const write = bridge.function(Document.write, .{ .dom_exception = true });
|
||||
pub const writeln = bridge.function(Document.writeln, .{ .dom_exception = true });
|
||||
pub const open = bridge.function(Document.open, .{ .dom_exception = true });
|
||||
pub const close = bridge.function(Document.close, .{ .dom_exception = true });
|
||||
pub const doctype = bridge.accessor(Document.getDocType, null, .{});
|
||||
|
||||
@@ -239,10 +239,10 @@ pub const JsApi = struct {
|
||||
pub const childElementCount = bridge.accessor(DocumentFragment.getChildElementCount, null, .{});
|
||||
pub const firstElementChild = bridge.accessor(DocumentFragment.firstElementChild, null, .{});
|
||||
pub const lastElementChild = bridge.accessor(DocumentFragment.lastElementChild, null, .{});
|
||||
pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{ .ce_reactions = true });
|
||||
pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true });
|
||||
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true });
|
||||
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true });
|
||||
pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{});
|
||||
|
||||
fn _innerHTML(self: *DocumentFragment, frame: *Frame) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
|
||||
|
||||
@@ -474,7 +474,11 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
try frame.insertAllChildrenBefore(fragment, parent, node);
|
||||
}
|
||||
|
||||
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
|
||||
// A custom element callback fired during insertAllChildrenBefore may
|
||||
// have already detached `node`; only remove it if it's still here.
|
||||
if (node._parent == parent) {
|
||||
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
@@ -485,16 +489,21 @@ pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void
|
||||
pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
const parent = self.asNode();
|
||||
|
||||
// Remove all existing children. Drain via firstChild(): removeNode
|
||||
// fires disconnectedCallback for custom elements, which can mutate
|
||||
// the child list and dangle any cached next-pointer the iterator
|
||||
// would otherwise hold.
|
||||
frame.domChanged();
|
||||
var it = parent.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
while (parent.firstChild()) |child| {
|
||||
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
|
||||
}
|
||||
|
||||
// Fast path: skip parsing if html is empty
|
||||
if (html.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and add new children
|
||||
try frame.parseHtmlAsChildren(parent, html);
|
||||
}
|
||||
|
||||
@@ -1734,21 +1743,21 @@ pub const JsApi = struct {
|
||||
}
|
||||
pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});
|
||||
|
||||
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{ .ce_reactions = true });
|
||||
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{});
|
||||
fn _innerText(self: *Element, frame: *const Frame) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
|
||||
try self.getInnerText(&buf.writer);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{ .ce_reactions = true });
|
||||
pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{});
|
||||
fn _outerHTML(self: *Element, frame: *Frame) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
|
||||
try self.getOuterHTML(&buf.writer, frame);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{ .ce_reactions = true });
|
||||
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
|
||||
fn _innerHTML(self: *Element, frame: *Frame) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
|
||||
try self.getInnerHTML(&buf.writer, frame);
|
||||
@@ -1757,24 +1766,24 @@ pub const JsApi = struct {
|
||||
|
||||
pub const prefix = bridge.accessor(Element._prefix, null, .{});
|
||||
|
||||
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true });
|
||||
fn _setAttribute(self: *Element, name: String, value: js.Value, frame: *Frame) !void {
|
||||
return self.setAttribute(name, .wrap(try value.toStringSlice()), frame);
|
||||
}
|
||||
|
||||
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true });
|
||||
fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, frame: *Frame) !void {
|
||||
return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), frame);
|
||||
}
|
||||
|
||||
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
|
||||
pub const id = bridge.accessor(Element.getId, Element.setId, .{ .ce_reactions = true });
|
||||
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{ .ce_reactions = true });
|
||||
pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{ .ce_reactions = true });
|
||||
pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{ .ce_reactions = true });
|
||||
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{ .ce_reactions = true });
|
||||
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{ .ce_reactions = true });
|
||||
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{ .ce_reactions = true });
|
||||
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
|
||||
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
|
||||
pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{});
|
||||
pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{});
|
||||
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, Element.setClassList, .{});
|
||||
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
|
||||
pub const style = bridge.accessor(Element.getOrCreateStyle, null, .{});
|
||||
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
|
||||
@@ -1783,17 +1792,17 @@ pub const JsApi = struct {
|
||||
pub const getAttribute = bridge.function(Element.getAttribute, .{});
|
||||
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
|
||||
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
|
||||
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{ .ce_reactions = true });
|
||||
pub const removeAttribute = bridge.function(Element.removeAttribute, .{ .ce_reactions = true });
|
||||
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
|
||||
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
|
||||
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
|
||||
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
|
||||
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true, .ce_reactions = true });
|
||||
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, .ce_reactions = true });
|
||||
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true });
|
||||
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true });
|
||||
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true });
|
||||
|
||||
const ShadowRootInit = struct {
|
||||
mode: []const u8,
|
||||
@@ -1801,13 +1810,13 @@ pub const JsApi = struct {
|
||||
fn _attachShadow(self: *Element, init: ShadowRootInit, frame: *Frame) !*ShadowRoot {
|
||||
return self.attachShadow(init.mode, frame);
|
||||
}
|
||||
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const remove = bridge.function(Element.remove, .{ .ce_reactions = true });
|
||||
pub const append = bridge.function(Element.append, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const before = bridge.function(Element.before, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const after = bridge.function(Element.after, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true });
|
||||
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true });
|
||||
pub const remove = bridge.function(Element.remove, .{});
|
||||
pub const append = bridge.function(Element.append, .{ .dom_exception = true });
|
||||
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true });
|
||||
pub const before = bridge.function(Element.before, .{ .dom_exception = true });
|
||||
pub const after = bridge.function(Element.after, .{ .dom_exception = true });
|
||||
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
|
||||
pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
|
||||
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
|
||||
|
||||
@@ -253,6 +253,11 @@ pub fn appendChild(self: *Node, child: *Node, frame: *Frame) !*Node {
|
||||
try frame.adoptNodeTree(child, child_owner.?, parent_owner);
|
||||
}
|
||||
|
||||
// A custom element callback can re-parent the node. If it does, we're done
|
||||
if (child._parent != null) {
|
||||
return child;
|
||||
}
|
||||
|
||||
try frame.appendNode(self, child, .{
|
||||
.child_already_connected = child_connected,
|
||||
.adopting_to_new_document = adopting_to_new_document,
|
||||
@@ -615,6 +620,22 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
|
||||
try frame.adoptNodeTree(new_node, child_owner.?, parent_owner);
|
||||
}
|
||||
|
||||
// See Node.appendChild: a callback above (disconnectedCallback or
|
||||
// adoptedCallback) can re-parent new_node. Let that placement stand.
|
||||
if (new_node._parent != null) {
|
||||
return new_node;
|
||||
}
|
||||
|
||||
// The same callback could also have detached ref_node from self. Fall
|
||||
// back to append so new_node still lands in self.
|
||||
if (ref_node._parent != self) {
|
||||
try frame.appendNode(self, new_node, .{
|
||||
.child_already_connected = child_already_connected,
|
||||
.adopting_to_new_document = adopting_to_new_document,
|
||||
});
|
||||
return new_node;
|
||||
}
|
||||
|
||||
try frame.insertNodeRelative(
|
||||
self,
|
||||
new_node,
|
||||
@@ -637,8 +658,11 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, frame: *Fra
|
||||
|
||||
_ = try self.insertBefore(new_child, old_child, frame);
|
||||
|
||||
// Special case: if we replace a node by itself, insertBefore was a noop.
|
||||
if (new_child != old_child) {
|
||||
// Special case: if we replace a node by itself, we don't remove it.
|
||||
// insertBefore is an noop in this case.
|
||||
// Re-check parent after insertBefore since callbacks (e.g. connectedCallback)
|
||||
// could have already removed old_child from self.
|
||||
if (new_child != old_child and old_child._parent == self) {
|
||||
frame.removeNode(self, old_child, .{ .will_be_reconnected = false });
|
||||
}
|
||||
|
||||
@@ -1139,7 +1163,7 @@ pub const JsApi = struct {
|
||||
}.wrap, null, .{});
|
||||
pub const nodeType = bridge.accessor(Node.getNodeType, null, .{});
|
||||
|
||||
pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{ .ce_reactions = true });
|
||||
pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{});
|
||||
fn _textContext(self: *Node, frame: *const Frame) !?[]const u8 {
|
||||
// cdata and attributes can return value directly, avoiding the copy
|
||||
switch (self._type) {
|
||||
@@ -1161,19 +1185,19 @@ pub const JsApi = struct {
|
||||
pub const previousSibling = bridge.accessor(Node.previousSibling, null, .{});
|
||||
pub const parentNode = bridge.accessor(Node.parentNode, null, .{});
|
||||
pub const parentElement = bridge.accessor(Node.parentElement, null, .{});
|
||||
pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true });
|
||||
pub const childNodes = bridge.accessor(Node.childNodes, null, .{ .cache = .{ .private = "child_nodes" } });
|
||||
pub const isConnected = bridge.accessor(Node.isConnected, null, .{});
|
||||
pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{});
|
||||
pub const hasChildNodes = bridge.function(Node.hasChildNodes, .{});
|
||||
pub const isSameNode = bridge.function(Node.isSameNode, .{});
|
||||
pub const contains = bridge.function(Node.contains, .{});
|
||||
pub const removeChild = bridge.function(Node.removeChild, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const nodeValue = bridge.accessor(Node.getNodeValue, Node.setNodeValue, .{ .ce_reactions = true });
|
||||
pub const insertBefore = bridge.function(Node.insertBefore, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const replaceChild = bridge.function(Node.replaceChild, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const normalize = bridge.function(Node.normalize, .{ .ce_reactions = true });
|
||||
pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const removeChild = bridge.function(Node.removeChild, .{ .dom_exception = true });
|
||||
pub const nodeValue = bridge.accessor(Node.getNodeValue, Node.setNodeValue, .{});
|
||||
pub const insertBefore = bridge.function(Node.insertBefore, .{ .dom_exception = true });
|
||||
pub const replaceChild = bridge.function(Node.replaceChild, .{ .dom_exception = true });
|
||||
pub const normalize = bridge.function(Node.normalize, .{});
|
||||
pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true });
|
||||
pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{});
|
||||
pub const getRootNode = bridge.function(Node.getRootNode, .{});
|
||||
pub const isEqualNode = bridge.function(Node.isEqualNode, .{});
|
||||
|
||||
@@ -311,11 +311,11 @@ pub const JsApi = struct {
|
||||
}
|
||||
|
||||
pub const contains = bridge.function(DOMTokenList.contains, .{ .dom_exception = true });
|
||||
pub const add = bridge.function(DOMTokenList.add, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const remove = bridge.function(DOMTokenList.remove, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const toggle = bridge.function(DOMTokenList.toggle, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const replace = bridge.function(DOMTokenList.replace, .{ .dom_exception = true, .ce_reactions = true });
|
||||
pub const value = bridge.accessor(DOMTokenList.getValue, DOMTokenList.setValue, .{ .ce_reactions = true });
|
||||
pub const add = bridge.function(DOMTokenList.add, .{ .dom_exception = true });
|
||||
pub const remove = bridge.function(DOMTokenList.remove, .{ .dom_exception = true });
|
||||
pub const toggle = bridge.function(DOMTokenList.toggle, .{ .dom_exception = true });
|
||||
pub const replace = bridge.function(DOMTokenList.replace, .{ .dom_exception = true });
|
||||
pub const value = bridge.accessor(DOMTokenList.getValue, DOMTokenList.setValue, .{});
|
||||
pub const toString = bridge.function(DOMTokenList.getValue, .{});
|
||||
pub const keys = bridge.function(DOMTokenList.keys, .{});
|
||||
pub const values = bridge.function(DOMTokenList.values, .{});
|
||||
|
||||
@@ -104,7 +104,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const name = bridge.accessor(Attribute.getName, null, .{});
|
||||
pub const localName = bridge.accessor(Attribute.getName, null, .{});
|
||||
pub const value = bridge.accessor(Attribute.getValue, Attribute.setValue, .{ .ce_reactions = true });
|
||||
pub const value = bridge.accessor(Attribute.getValue, Attribute.setValue, .{});
|
||||
pub const namespaceURI = bridge.accessor(Attribute.getNamespaceURI, null, .{});
|
||||
pub const ownerElement = bridge.accessor(Attribute.getOwnerElement, null, .{});
|
||||
};
|
||||
@@ -538,8 +538,8 @@ pub const NamedNodeMap = struct {
|
||||
pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, null, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true });
|
||||
pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
|
||||
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{ .ce_reactions = true });
|
||||
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{ .ce_reactions = true });
|
||||
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{});
|
||||
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{});
|
||||
pub const item = bridge.function(_item, .{});
|
||||
fn _item(self: *const NamedNodeMap, index: i32, frame: *Frame) !?*Attribute {
|
||||
// the bridge.indexed handles this, so if we want
|
||||
|
||||
@@ -27,7 +27,6 @@ const Element = @import("../../Element.zig");
|
||||
const Document = @import("../../Document.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const CustomElementDefinition = @import("../../CustomElementDefinition.zig");
|
||||
const Reaction = @import("../../../CustomElementReactions.zig").Reaction;
|
||||
|
||||
const log = lp.log;
|
||||
const String = lp.String;
|
||||
@@ -46,18 +45,53 @@ pub fn asNode(self: *Custom) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
// Reactions are queued via enqueue* and fired via fireReaction at the outer
|
||||
// CEReactions boundary (set up by the JS bridge, the parser pump, etc.).
|
||||
//
|
||||
// Dedup happens at enqueue time: the connected/disconnected flags flip when
|
||||
// we queue a reaction so that a redundant enqueue (already-in-this-state)
|
||||
// is dropped, and a remove+re-insert in the same scope queues both reactions
|
||||
// in order. Fire-time is unconditional.
|
||||
pub fn invokeConnectedCallback(self: *Custom, frame: *Frame) void {
|
||||
// Only invoke if we haven't already called it while connected
|
||||
if (self._connected_callback_invoked) {
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn enqueueConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, frame: *Frame) error{OutOfMemory}!void {
|
||||
self._connected_callback_invoked = true;
|
||||
self._disconnected_callback_invoked = false;
|
||||
self.invokeCallback("connectedCallback", .{}, frame);
|
||||
}
|
||||
|
||||
pub fn invokeDisconnectedCallback(self: *Custom, frame: *Frame) void {
|
||||
// Only invoke if we haven't already called it while disconnected
|
||||
if (self._disconnected_callback_invoked) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._disconnected_callback_invoked = true;
|
||||
self._connected_callback_invoked = false;
|
||||
self.invokeCallback("disconnectedCallback", .{}, frame);
|
||||
}
|
||||
|
||||
pub fn invokeAttributeChangedCallback(self: *Custom, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
|
||||
const definition = self._definition orelse return;
|
||||
if (!definition.isAttributeObserved(name)) {
|
||||
return;
|
||||
}
|
||||
self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value, namespace }, frame);
|
||||
}
|
||||
|
||||
pub fn invokeAdoptedCallback(self: *Custom, old_document: *Document, new_document: *Document, frame: *Frame) void {
|
||||
self.invokeCallback("adoptedCallback", .{ old_document, new_document }, frame);
|
||||
}
|
||||
|
||||
pub fn invokeAdoptedCallbackOnElement(element: *Element, old_document: *Document, new_document: *Document, frame: *Frame) void {
|
||||
if (element.is(Custom)) |custom| {
|
||||
custom.invokeAdoptedCallback(old_document, new_document, frame);
|
||||
return;
|
||||
}
|
||||
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
invokeCallbackOnElement(element, definition, "adoptedCallback", .{ old_document, new_document }, frame);
|
||||
}
|
||||
|
||||
pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, frame: *Frame) !void {
|
||||
// Autonomous custom element
|
||||
if (element.is(Custom)) |custom| {
|
||||
// Upgrade if a definition exists but isn't yet attached
|
||||
// If the element is undefined, check if a definition now exists and upgrade
|
||||
if (custom._definition == null) {
|
||||
const name = custom._tag_name.str();
|
||||
if (frame.window._custom_elements._definitions.get(name)) |definition| {
|
||||
@@ -65,31 +99,30 @@ pub fn enqueueConnectedCallbackOnElement(comptime from_parser: bool, element: *E
|
||||
CustomElementRegistry.upgradeCustomElement(custom, definition, frame) catch {};
|
||||
return;
|
||||
}
|
||||
// Element is undefined and no definition exists yet — nothing to queue.
|
||||
return;
|
||||
}
|
||||
|
||||
// Dedup: skip if already queued/fired while connected.
|
||||
if (custom._connected_callback_invoked) return;
|
||||
custom._connected_callback_invoked = true;
|
||||
custom._disconnected_callback_invoked = false;
|
||||
try frame._ce_reactions.enqueueConnected(element);
|
||||
if (comptime from_parser) {
|
||||
// From parser, we know the element is brand new
|
||||
custom._connected_callback_invoked = true;
|
||||
custom.invokeCallback("connectedCallback", .{}, frame);
|
||||
} else {
|
||||
custom.invokeConnectedCallback(frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Customized built-in element - check if it actually has a definition first
|
||||
if (frame.getCustomizedBuiltInDefinition(element) == null) {
|
||||
return;
|
||||
}
|
||||
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
|
||||
if (comptime from_parser) {
|
||||
// From parser, we know the element is brand new; skip the dedup check.
|
||||
// From parser, we know the element is brand new, skip the tracking check
|
||||
try frame._customized_builtin_connected_callback_invoked.put(
|
||||
frame.arena,
|
||||
element,
|
||||
{},
|
||||
);
|
||||
} else {
|
||||
// Not from parser, check if we've already invoked while connected
|
||||
const gop = try frame._customized_builtin_connected_callback_invoked.getOrPut(
|
||||
frame.arena,
|
||||
element,
|
||||
@@ -101,95 +134,43 @@ pub fn enqueueConnectedCallbackOnElement(comptime from_parser: bool, element: *E
|
||||
}
|
||||
|
||||
_ = frame._customized_builtin_disconnected_callback_invoked.remove(element);
|
||||
try frame._ce_reactions.enqueueConnected(element);
|
||||
invokeCallbackOnElement(element, definition, "connectedCallback", .{}, frame);
|
||||
}
|
||||
|
||||
pub fn enqueueDisconnectedCallbackOnElement(element: *Element, frame: *Frame) void {
|
||||
pub fn invokeDisconnectedCallbackOnElement(element: *Element, frame: *Frame) void {
|
||||
// Autonomous custom element
|
||||
if (element.is(Custom)) |custom| {
|
||||
if (custom._definition == null) return;
|
||||
if (custom._disconnected_callback_invoked) return;
|
||||
custom._disconnected_callback_invoked = true;
|
||||
custom._connected_callback_invoked = false;
|
||||
frame._ce_reactions.enqueueDisconnected(element) catch |err| {
|
||||
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
|
||||
};
|
||||
custom.invokeDisconnectedCallback(frame);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.getCustomizedBuiltInDefinition(element) == null) {
|
||||
return;
|
||||
}
|
||||
// Customized built-in element - check if it actually has a definition first
|
||||
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
|
||||
// Check if we've already invoked disconnectedCallback while disconnected
|
||||
const gop = frame._customized_builtin_disconnected_callback_invoked.getOrPut(
|
||||
frame.arena,
|
||||
element,
|
||||
) catch return;
|
||||
if (gop.found_existing) return;
|
||||
gop.value_ptr.* = {};
|
||||
|
||||
_ = frame._customized_builtin_connected_callback_invoked.remove(element);
|
||||
|
||||
frame._ce_reactions.enqueueDisconnected(element) catch |err| {
|
||||
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
|
||||
};
|
||||
invokeCallbackOnElement(element, definition, "disconnectedCallback", .{}, frame);
|
||||
}
|
||||
|
||||
pub fn enqueueAdoptedCallbackOnElement(element: *Element, old_document: *Document, new_document: *Document, frame: *Frame) void {
|
||||
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
|
||||
// Autonomous custom element
|
||||
if (element.is(Custom)) |custom| {
|
||||
if (custom._definition == null) return;
|
||||
} else {
|
||||
if (frame.getCustomizedBuiltInDefinition(element) == null) return;
|
||||
custom.invokeAttributeChangedCallback(name, old_value, new_value, namespace, frame);
|
||||
return;
|
||||
}
|
||||
frame._ce_reactions.enqueueAdopted(element, old_document, new_document) catch |err| {
|
||||
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn enqueueAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
|
||||
if (element.is(Custom)) |custom| {
|
||||
const definition = custom._definition orelse return;
|
||||
if (!definition.isAttributeObserved(name)) return;
|
||||
} else {
|
||||
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
if (!definition.isAttributeObserved(name)) return;
|
||||
}
|
||||
frame._ce_reactions.enqueueAttributeChanged(element, name, old_value, new_value, namespace) catch |err| {
|
||||
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// Called by CustomElementReactions.popAndInvoke for each queued reaction.
|
||||
// Filtering already happened at enqueue time, so just fire unconditionally.
|
||||
pub fn fireReaction(reaction: Reaction, frame: *Frame) void {
|
||||
switch (reaction) {
|
||||
.connected => |el| {
|
||||
if (el.is(Custom)) |custom| {
|
||||
custom.invokeCallback("connectedCallback", .{}, frame);
|
||||
} else if (frame.getCustomizedBuiltInDefinition(el)) |definition| {
|
||||
invokeCallbackOnElement(el, definition, "connectedCallback", .{}, frame);
|
||||
}
|
||||
},
|
||||
.disconnected => |el| {
|
||||
if (el.is(Custom)) |custom| {
|
||||
custom.invokeCallback("disconnectedCallback", .{}, frame);
|
||||
} else if (frame.getCustomizedBuiltInDefinition(el)) |definition| {
|
||||
invokeCallbackOnElement(el, definition, "disconnectedCallback", .{}, frame);
|
||||
}
|
||||
},
|
||||
.adopted => |a| {
|
||||
if (a.element.is(Custom)) |custom| {
|
||||
custom.invokeCallback("adoptedCallback", .{ a.old_document, a.new_document }, frame);
|
||||
} else if (frame.getCustomizedBuiltInDefinition(a.element)) |definition| {
|
||||
invokeCallbackOnElement(a.element, definition, "adoptedCallback", .{ a.old_document, a.new_document }, frame);
|
||||
}
|
||||
},
|
||||
.attribute_changed => |a| {
|
||||
if (a.element.is(Custom)) |custom| {
|
||||
custom.invokeCallback("attributeChangedCallback", .{ a.name, a.old_value, a.new_value, a.namespace }, frame);
|
||||
} else if (frame.getCustomizedBuiltInDefinition(a.element)) |definition| {
|
||||
invokeCallbackOnElement(a.element, definition, "attributeChangedCallback", .{ a.name, a.old_value, a.new_value, a.namespace }, frame);
|
||||
}
|
||||
},
|
||||
}
|
||||
// Customized built-in element - check if attribute is observed
|
||||
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
if (!definition.isAttributeObserved(name)) return;
|
||||
invokeCallbackOnElement(element, definition, "attributeChangedCallback", .{ name, old_value, new_value, namespace }, frame);
|
||||
}
|
||||
|
||||
fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, frame: *Frame) void {
|
||||
|
||||
Reference in New Issue
Block a user