diff --git a/src/browser/tests/custom_elements/upgrade.html b/src/browser/tests/custom_elements/upgrade.html
index 1b917023..44f37bd6 100644
--- a/src/browser/tests/custom_elements/upgrade.html
+++ b/src/browser/tests/custom_elements/upgrade.html
@@ -92,4 +92,63 @@
document.body.appendChild(container);
testing.expectEqual(1, connectedCalled);
}
+
+{
+ let constructorCalled = 0;
+ let connectedCalled = 0;
+
+ class ManualUpgrade extends HTMLElement {
+ constructor() {
+ super();
+ constructorCalled++;
+ this.manuallyUpgraded = true;
+ }
+
+ connectedCallback() {
+ connectedCalled++;
+ }
+ }
+
+ customElements.define('manual-upgrade', ManualUpgrade);
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ testing.expectEqual(2, constructorCalled);
+ testing.expectEqual(0, connectedCalled);
+
+ customElements.upgrade(container);
+
+ testing.expectEqual(2, constructorCalled);
+ testing.expectEqual(0, connectedCalled);
+
+ const m1 = container.querySelector('#m1');
+ const m2 = container.querySelector('#m2');
+ testing.expectEqual(true, m1.manuallyUpgraded);
+ testing.expectEqual(true, m2.manuallyUpgraded);
+
+ document.body.appendChild(container);
+ testing.expectEqual(2, connectedCalled);
+}
+
+{
+ let alreadyUpgradedCalled = 0;
+
+ class AlreadyUpgraded extends HTMLElement {
+ constructor() {
+ super();
+ alreadyUpgradedCalled++;
+ }
+ }
+
+ const elem = document.createElement('div');
+ elem.innerHTML = '';
+ document.body.appendChild(elem);
+
+ customElements.define('already-upgraded', AlreadyUpgraded);
+ testing.expectEqual(1, alreadyUpgradedCalled);
+
+ customElements.upgrade(elem);
+ testing.expectEqual(1, alreadyUpgradedCalled);
+}
diff --git a/src/browser/tests/element/attributes.html b/src/browser/tests/element/attributes.html
index c1ce0767..5fb92883 100644
--- a/src/browser/tests/element/attributes.html
+++ b/src/browser/tests/element/attributes.html
@@ -107,30 +107,24 @@
{
const el1 = $('#attr1');
- // Toggle non-existent attribute (should add it and return true)
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
testing.expectEqual(true, el1.toggleAttribute('toggle-test'));
testing.expectEqual(true, el1.hasAttribute('toggle-test'));
testing.expectEqual('', el1.getAttribute('toggle-test'));
- // Toggle existing attribute (should remove it and return false)
testing.expectEqual(false, el1.toggleAttribute('toggle-test'));
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
- // Toggle with force=true when attribute doesn't exist (should add and return true)
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
testing.expectEqual(true, el1.toggleAttribute('toggle-test', true));
testing.expectEqual(true, el1.hasAttribute('toggle-test'));
- // Toggle with force=true when attribute exists (should keep and return true)
testing.expectEqual(true, el1.toggleAttribute('toggle-test', true));
testing.expectEqual(true, el1.hasAttribute('toggle-test'));
- // Toggle with force=false when attribute exists (should remove and return false)
testing.expectEqual(false, el1.toggleAttribute('toggle-test', false));
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
- // Toggle with force=false when attribute doesn't exist (should keep removed and return false)
testing.expectEqual(false, el1.toggleAttribute('toggle-test', false));
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
}
diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig
index 62213754..2028bda8 100644
--- a/src/browser/webapi/CustomElementRegistry.zig
+++ b/src/browser/webapi/CustomElementRegistry.zig
@@ -21,6 +21,7 @@ const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Element = @import("Element.zig");
+const Node = @import("Node.zig");
const CustomElementDefinition = @import("CustomElementDefinition.zig");
const CustomElementRegistry = @This();
@@ -88,24 +89,11 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
continue;
}
- custom._definition = definition;
-
- const node = custom.asNode();
- const prev_upgrading = page._upgrading_element;
- page._upgrading_element = node;
- defer page._upgrading_element = prev_upgrading;
-
- var result: js.Function.Result = undefined;
- _ = definition.constructor.newInstance(&result) catch |err| {
- log.warn(.js, "custom element upgrade", .{ .name = name, .err = err });
+ upgradeCustomElement(custom, definition, page) catch {
_ = page._undefined_custom_elements.swapRemove(idx);
continue;
};
- if (node.isConnected()) {
- custom.invokeConnectedCallback(page);
- }
-
_ = page._undefined_custom_elements.swapRemove(idx);
}
}
@@ -115,6 +103,55 @@ pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function {
return definition.constructor;
}
+pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void {
+ try upgradeNode(self, root, page);
+}
+
+fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
+ if (node.is(Element)) |element| {
+ try upgradeElement(self, element, page);
+ }
+
+ var it = node.childrenIterator();
+ while (it.next()) |child| {
+ try upgradeNode(self, child, page);
+ }
+}
+
+fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) !void {
+ const Custom = @import("element/html/Custom.zig");
+
+ const custom = element.is(Custom) orelse {
+ return Custom.checkAndAttachBuiltIn(element, page);
+ };
+
+ if (custom._definition != null) return;
+
+ const name = custom._tag_name.str();
+ const definition = self._definitions.get(name) orelse return;
+
+ try upgradeCustomElement(custom, definition, page);
+}
+
+fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: *CustomElementDefinition, page: *Page) !void {
+ custom._definition = definition;
+
+ const node = custom.asNode();
+ const prev_upgrading = page._upgrading_element;
+ page._upgrading_element = node;
+ defer page._upgrading_element = prev_upgrading;
+
+ var result: js.Function.Result = undefined;
+ _ = definition.constructor.newInstance(&result) catch |err| {
+ log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err });
+ return error.CustomElementUpgradeFailed;
+ };
+
+ if (node.isConnected()) {
+ custom.invokeConnectedCallback(page);
+ }
+}
+
fn validateName(name: []const u8) !void {
if (name.len == 0) {
return error.InvalidCustomElementName;
@@ -166,6 +203,7 @@ pub const JsApi = struct {
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true });
pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });
+ pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{});
};
const testing = @import("../../testing.zig");