From 833a33678cea50792b3afdba578b24f2914d9af3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 28 Nov 2025 13:04:42 +0800 Subject: [PATCH] call AttributeChangedCallback on upgrade --- src/browser/ScriptManager.zig | 6 ++-- .../custom_elements/attribute_changed.html | 13 ++++---- .../tests/custom_elements/upgrade.html | 30 +++++++++++++++++++ src/browser/webapi/CustomElementRegistry.zig | 18 ++++++++--- src/browser/webapi/Element.zig | 4 +-- src/browser/webapi/Window.zig | 14 +++++++++ src/browser/webapi/net/Fetch.zig | 5 ++++ src/browser/webapi/net/XMLHttpRequest.zig | 3 +- 8 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index f037713f..a1df242e 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -215,14 +215,12 @@ pub fn addFromElement(self: *ScriptManager, script_element: *Element.Html.Script .url = remote_url orelse page.url, .mode = blk: { if (source == .@"inline") { - // inline modules are deferred, all other inline scripts have a - // normal execution flow - break :blk if (kind == .module) .@"defer" else .normal; + break :blk .normal; } if (element.getAttributeSafe("async") != null) { break :blk .async; } - if (element.getAttributeSafe("defer") != null) { + if (kind == .module or element.getAttributeSafe("defer") != null) { break :blk .@"defer"; } break :blk .normal; diff --git a/src/browser/tests/custom_elements/attribute_changed.html b/src/browser/tests/custom_elements/attribute_changed.html index 24a8a48d..f94de7f3 100644 --- a/src/browser/tests/custom_elements/attribute_changed.html +++ b/src/browser/tests/custom_elements/attribute_changed.html @@ -122,13 +122,16 @@ testing.expectEqual(0, callbackCalls.length); customElements.define('upgrade-attr-element', UpgradeAttrElement); - testing.expectEqual(0, callbackCalls.length); - - el.setAttribute('existing', 'after-upgrade'); testing.expectEqual(1, callbackCalls.length); testing.expectEqual('existing', callbackCalls[0].name); - testing.expectEqual('before-upgrade', callbackCalls[0].oldValue); - testing.expectEqual('after-upgrade', callbackCalls[0].newValue); + testing.expectEqual(null, callbackCalls[0].oldValue); + testing.expectEqual('before-upgrade', callbackCalls[0].newValue); + + el.setAttribute('existing', 'after-upgrade'); + testing.expectEqual(2, callbackCalls.length); + testing.expectEqual('existing', callbackCalls[1].name); + testing.expectEqual('before-upgrade', callbackCalls[1].oldValue); + testing.expectEqual('after-upgrade', callbackCalls[1].newValue); } { diff --git a/src/browser/tests/custom_elements/upgrade.html b/src/browser/tests/custom_elements/upgrade.html index 44f37bd6..73f1367c 100644 --- a/src/browser/tests/custom_elements/upgrade.html +++ b/src/browser/tests/custom_elements/upgrade.html @@ -151,4 +151,34 @@ customElements.upgrade(elem); testing.expectEqual(1, alreadyUpgradedCalled); } + +{ + let attributeChangedCalls = []; + + class UpgradeWithAttrs extends HTMLElement { + static get observedAttributes() { + return ['data-foo', 'data-bar']; + } + + attributeChangedCallback(name, oldValue, newValue) { + attributeChangedCalls.push({ name, oldValue, newValue }); + } + } + + const container = document.createElement('div'); + container.innerHTML = ''; + document.body.appendChild(container); + + testing.expectEqual(0, attributeChangedCalls.length); + + customElements.define('upgrade-with-attrs', UpgradeWithAttrs); + + testing.expectEqual(2, attributeChangedCalls.length); + testing.expectEqual('data-foo', attributeChangedCalls[0].name); + testing.expectEqual(null, attributeChangedCalls[0].oldValue); + testing.expectEqual('hello', attributeChangedCalls[0].newValue); + testing.expectEqual('data-bar', attributeChangedCalls[1].name); + testing.expectEqual(null, attributeChangedCalls[1].oldValue); + testing.expectEqual('world', attributeChangedCalls[1].newValue); +} diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 9c295170..318b80e6 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -18,10 +18,13 @@ const std = @import("std"); 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 Element = @import("Element.zig"); +const Custom = @import("element/html/Custom.zig"); const CustomElementDefinition = @import("CustomElementDefinition.zig"); const CustomElementRegistry = @This(); @@ -119,8 +122,6 @@ fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void { } 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); }; @@ -133,7 +134,7 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) try upgradeCustomElement(custom, definition, page); } -fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: *CustomElementDefinition, page: *Page) !void { +fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, page: *Page) !void { custom._definition = definition; // Reset callback flags since this is a fresh upgrade @@ -151,6 +152,15 @@ fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: return error.CustomElementUpgradeFailed; }; + // Invoke attributeChangedCallback for existing observed attributes + var attr_it = custom.asElement().attributeIterator(); + while (attr_it.next()) |attr| { + const name = attr._name.str(); + if (definition.isAttributeObserved(name)) { + custom.invokeAttributeChangedCallback(name, null, attr._value.str(), page); + } + } + if (node.isConnected()) { custom.invokeConnectedCallback(page); } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index c688d6a8..36215d47 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -272,9 +272,9 @@ pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void { return self.setAttributeSafe("class", value, page); } -pub fn attributeIterator(self: *Element) Attribute.Iterator { +pub fn attributeIterator(self: *Element) Attribute.InnerIterator { const attributes = self._attributes orelse return .{}; - return attributes.iterator(self); + return attributes.iterator(); } pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]const u8 { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 16562980..f025d1ed 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -54,6 +54,7 @@ _history: History, _storage_bucket: *storage.Bucket, _on_load: ?js.Function = null, _on_error: ?js.Function = null, // TODO: invoke on error? +_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error _location: *Location, _timer_id: u30 = 0, _timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{}, @@ -143,6 +144,18 @@ pub fn setOnError(self: *Window, cb_: ?js.Function) !void { } } +pub fn getOnUnhandledRejection(self: *const Window) ?js.Function { + return self._on_unhandled_rejection; +} + +pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void { + if (cb_) |cb| { + self._on_unhandled_rejection = cb; + } else { + self._on_unhandled_rejection = null; + } +} + pub fn fetch(_: *const Window, input: Fetch.Input, page: *Page) !js.Promise { return Fetch.init(input, page); } @@ -390,6 +403,7 @@ pub const JsApi = struct { pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" }); pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{}); pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{}); + pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{}); pub const fetch = bridge.function(Window.fetch, .{}); pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{}); pub const setTimeout = bridge.function(Window.setTimeout, .{}); diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index d3589ee2..6fd4c4fd 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -29,6 +29,7 @@ const Request = @import("Request.zig"); const Response = @import("Response.zig"); const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; const Fetch = @This(); @@ -54,6 +55,10 @@ pub fn init(input: Input, page: *Page) !js.Promise { const http_client = page._session.browser.http_client; const headers = try http_client.newHeaders(); + if (comptime IS_DEBUG) { + log.debug(.http, "fetch", .{ .url = request._url }); + } + try http_client.request(.{ .ctx = fetch, .url = request._url, diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 6239ddc4..3bb219e2 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -19,8 +19,6 @@ const std = @import("std"); const js = @import("../../js/js.zig"); -const IS_DEBUG = @import("builtin").mode == .Debug; - const log = @import("../../../log.zig"); const Http = @import("../../../http/Http.zig"); @@ -32,6 +30,7 @@ const EventTarget = @import("../EventTarget.zig"); const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig"); const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; const XMLHttpRequest = @This(); _page: *Page,