From 3c010f0e73a9503b4574b0b52b6dd444ae56f6b8 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 22 Nov 2025 12:25:12 +0800 Subject: [PATCH] tweak custom element callbacks --- src/browser/Page.zig | 18 +++++++--- src/browser/webapi/CustomElementRegistry.zig | 4 +++ src/browser/webapi/element/html/Custom.zig | 36 ++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 8c75f2bc..19dd7f78 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -107,6 +107,8 @@ _intersection_delivery_scheduled: bool = false, // Lookup for customized built-in elements. Maps element pointer to definition. _customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{}, +_customized_builtin_connected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{}, +_customized_builtin_disconnected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{}, // This is set when an element is being upgraded (constructor is called). // The constructor can access this to get the element being upgraded. @@ -223,6 +225,8 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._intersection_observers = .{}; self._intersection_delivery_scheduled = false; self._customized_builtin_definitions = .{}; + self._customized_builtin_connected_callback_invoked = .{}; + self._customized_builtin_disconnected_callback_invoked = .{}; self._undefined_custom_elements = .{}; try self.registerBackgroundTasks(); @@ -1380,13 +1384,14 @@ pub fn appendNode(self: *Page, parent: *Node, child: *Node, opts: InsertNodeOpts pub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void { self.domChanged(); - const is_connected = parent.isConnected(); const dest_connected = target.isConnected(); var it = parent.childrenIterator(); while (it.next()) |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 = is_connected }); + try self.appendNode(target, child, .{ .child_already_connected = child_was_connected }); } } @@ -1500,14 +1505,19 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod // 1. A disconnected child became connected (parent.isConnected() == true) // 2. Child is being added to a shadow tree (parent_in_shadow == true) // In both cases, we need to update ID maps and invoke callbacks + + // Only invoke connectedCallback if the root child is transitioning from + // disconnected to connected. When that happens, all descendants should also + // get connectedCallback invoked (they're becoming connected as a group). + const should_invoke_connected = parent_is_connected and !opts.child_already_connected; + var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{}); while (tw.next()) |el| { if (el.getAttributeSafe("id")) |id| { try self.addElementId(el.asNode()._parent.?, el, id); } - // Only invoke connected callback if actually connected to document - if (parent_is_connected) { + if (should_invoke_connected) { Element.Html.Custom.invokeConnectedCallbackOnElement(el, self); } } diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 2028bda8..c97fab0d 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -136,6 +136,10 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: *CustomElementDefinition, page: *Page) !void { custom._definition = definition; + // Reset callback flags since this is a fresh upgrade + custom._connected_callback_invoked = false; + custom._disconnected_callback_invoked = false; + const node = custom.asNode(); const prev_upgrading = page._upgrading_element; page._upgrading_element = node; diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig index 31e46bcf..50e8518e 100644 --- a/src/browser/webapi/element/html/Custom.zig +++ b/src/browser/webapi/element/html/Custom.zig @@ -32,6 +32,8 @@ const Custom = @This(); _proto: *HtmlElement, _tag_name: String, _definition: ?*CustomElementDefinition, +_connected_callback_invoked: bool = false, +_disconnected_callback_invoked: bool = false, pub fn asElement(self: *Custom) *Element { return self._proto._proto; @@ -41,10 +43,20 @@ pub fn asNode(self: *Custom) *Node { } pub fn invokeConnectedCallback(self: *Custom, page: *Page) void { + // Only invoke if we haven't already called it while connected + if (self._connected_callback_invoked) return; + + self._connected_callback_invoked = true; + self._disconnected_callback_invoked = false; self.invokeCallback("connectedCallback", .{}, page); } pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) 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", .{}, page); } @@ -63,6 +75,16 @@ pub fn invokeConnectedCallbackOnElement(element: *Element, page: *Page) void { } // Customized built-in element + // Check if we've already invoked connectedCallback while connected + if (page._customized_builtin_connected_callback_invoked.contains(element)) return; + + page._customized_builtin_connected_callback_invoked.put( + page.arena, + element, + {}, + ) catch return; + _ = page._customized_builtin_disconnected_callback_invoked.remove(element); + invokeCallbackOnElement(element, "connectedCallback", .{}, page); } @@ -74,6 +96,16 @@ pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void } // Customized built-in element + // Check if we've already invoked disconnectedCallback while disconnected + if (page._customized_builtin_disconnected_callback_invoked.contains(element)) return; + + page._customized_builtin_disconnected_callback_invoked.put( + page.arena, + element, + {}, + ) catch return; + _ = page._customized_builtin_connected_callback_invoked.remove(element); + invokeCallbackOnElement(element, "disconnectedCallback", .{}, page); } @@ -119,6 +151,10 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void { // Attach the definition try page.setCustomizedBuiltInDefinition(element, definition); + // Reset callback flags since this is a fresh upgrade + _ = page._customized_builtin_connected_callback_invoked.remove(element); + _ = page._customized_builtin_disconnected_callback_invoked.remove(element); + // Invoke constructor const prev_upgrading = page._upgrading_element; const node = element.asNode();