diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig
index 44736db1..a6d157c2 100644
--- a/src/TestHTTPServer.zig
+++ b/src/TestHTTPServer.zig
@@ -100,7 +100,10 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
var url_buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&url_buf);
- const unescaped_file_path = try URL.unescape(fba.allocator(), file_path);
+ var unescaped_file_path = try URL.unescape(fba.allocator(), file_path);
+ if (std.mem.indexOfScalarPos(u8, unescaped_file_path, 0, '?')) |pos| {
+ unescaped_file_path = unescaped_file_path[0..pos];
+ }
var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) {
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
else => return err,
@@ -114,7 +117,7 @@ pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
.content_length = stat.size,
.respond_options = .{
.extra_headers = &.{
- .{ .name = "content-type", .value = getContentType(file_path) },
+ .{ .name = "content-type", .value = getContentType(unescaped_file_path) },
},
},
});
diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig
index c29c796e..18a674da 100644
--- a/src/browser/Browser.zig
+++ b/src/browser/Browser.zig
@@ -86,7 +86,6 @@ pub fn closeSession(self: *Browser) void {
if (self.session) |*session| {
session.deinit();
self.session = null;
- self.env.memoryPressureNotification(.critical);
}
}
diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig
index 77cb47b0..fa698403 100644
--- a/src/browser/EventManager.zig
+++ b/src/browser/EventManager.zig
@@ -266,7 +266,11 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
was_handled = true;
event._current_target = target_et;
- try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event});
+ // Inline handlers (e.g. onclick property) follow the same "report,
+ // don't propagate" rule as addEventListener listeners — see Listener.run.
+ ls.toLocal(inline_handler).callWithThis(void, target_et, .{event}) catch |err| {
+ log.warn(.event, "inline handler", .{ .err = err });
+ };
if (event._stop_propagation) {
return;
@@ -388,19 +392,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
event._target = getAdjustedTarget(original_target, current_target);
}
- switch (listener.function) {
- .value => |value| try local.toLocal(value).callWithThis(void, current_target, .{event}),
- .string => |string| {
- const str = try frame.call_arena.dupeZ(u8, string.str());
- try local.eval(str, null);
- },
- .object => |obj_global| {
- const obj = local.toLocal(obj_global);
- if (try obj.getFunction("handleEvent")) |handleEvent| {
- try handleEvent.callWithThis(void, obj, .{event});
- }
- },
- }
+ try listener.run(frame.call_arena, local, event, "listener");
// Restore original target (only if we changed it)
if (event._needs_retargeting) {
diff --git a/src/browser/EventManagerBase.zig b/src/browser/EventManagerBase.zig
index 8e13ecd5..03bb6d15 100644
--- a/src/browser/EventManagerBase.zig
+++ b/src/browser/EventManagerBase.zig
@@ -305,19 +305,7 @@ pub fn dispatchDirect(
event._current_target = target;
- switch (listener.function) {
- .value => |value| try ls.local.toLocal(value).callWithThis(void, target, .{event}),
- .string => |string| {
- const str = try arena.dupeZ(u8, string.str());
- try ls.local.eval(str, null);
- },
- .object => |obj_global| {
- const obj = ls.local.toLocal(obj_global);
- if (try obj.getFunction("handleEvent")) |handleEvent| {
- try handleEvent.callWithThis(void, obj, .{event});
- }
- },
- }
+ try listener.run(arena, &ls.local, event, opts.context);
if (event._stop_immediate_propagation) {
return;
@@ -392,6 +380,46 @@ pub const Listener = struct {
signal: ?*@import("webapi/AbortSignal.zig") = null,
node: std.DoublyLinkedList.Node,
removed: bool = false,
+
+ // Per DOM §2.9 step 4 substep 8 ("Inner invoke"), a listener callback that
+ // throws must have its exception *reported* to the global error handler,
+ // not propagated to the dispatch caller — subsequent listeners on the same
+ // target and the rest of the propagation path must still run.
+ //
+ // Caller must set `event._current_target` before invoking — the function-
+ // listener variant uses it as `this`, matching the spec contract that a
+ // listener sees its current target via both `event.currentTarget` and `this`.
+ pub fn run(
+ self: *const Listener,
+ arena: Allocator,
+ local: *const js.Local,
+ event: *Event,
+ comptime context: []const u8,
+ ) error{OutOfMemory}!void {
+ switch (self.function) {
+ .value => |value| local.toLocal(value).callWithThis(void, event._current_target.?, .{event}) catch |err| {
+ log.warn(.event, context, .{ .err = err });
+ },
+ .string => |string| {
+ const str = try arena.dupeZ(u8, string.str());
+ local.eval(str, null) catch |err| {
+ log.warn(.event, context, .{ .err = err });
+ };
+ },
+ .object => |obj_global| {
+ const obj = local.toLocal(obj_global);
+ const handle_event = obj.getFunction("handleEvent") catch |err| blk: {
+ log.warn(.event, context, .{ .err = err });
+ break :blk null;
+ };
+ if (handle_event) |handleEvent| {
+ handleEvent.callWithThis(void, obj, .{event}) catch |err| {
+ log.warn(.event, context, .{ .err = err });
+ };
+ }
+ },
+ }
+ }
};
pub const Function = union(enum) {
diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig
index 52213bac..af9a1784 100644
--- a/src/browser/Frame.zig
+++ b/src/browser/Frame.zig
@@ -3007,7 +3007,7 @@ pub fn appendNode(self: *Frame, parent: *Node, child: *Node, opts: InsertNodeOpt
}
pub fn appendAllChildren(self: *Frame, parent: *Node, target: *Node) !void {
- self.domChanged();
+ target.bumpDomVersion(self);
const dest_connected = target.isConnected();
// Use firstChild() instead of iterator to handle cases where callbacks
@@ -3022,7 +3022,7 @@ pub fn appendAllChildren(self: *Frame, parent: *Node, target: *Node) !void {
}
pub fn insertAllChildrenBefore(self: *Frame, fragment: *Node, parent: *Node, ref_node: *Node) !void {
- self.domChanged();
+ parent.bumpDomVersion(self);
const dest_connected = parent.isConnected();
// Use firstChild() instead of iterator to handle cases where callbacks
diff --git a/src/browser/Session.zig b/src/browser/Session.zig
index 6c25a1ac..9d6f8c85 100644
--- a/src/browser/Session.zig
+++ b/src/browser/Session.zig
@@ -105,6 +105,12 @@ pub fn deinit(self: *Session) void {
self.removePage();
}
self.cookie_jar.deinit();
+
+ // Force V8 to flush any remaining weak callbacks while
+ // fc_identity_pool is still alive. Identity structs allocated from
+ // this pool back V8 weak-callback parameters; freeing the pool first
+ // would leave dangling pointers that segfault on the next GC.
+ self.browser.env.memoryPressureNotification(.critical);
self.fc_identity_pool.deinit();
self.storage_shed.deinit(self.browser.app.allocator);
diff --git a/src/browser/tests/element/html/script/clone_already_started.html b/src/browser/tests/element/html/script/clone_already_started.html
new file mode 100644
index 00000000..5748459d
--- /dev/null
+++ b/src/browser/tests/element/html/script/clone_already_started.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/events.html b/src/browser/tests/events.html
index 80d41707..eae7f883 100644
--- a/src/browser/tests/events.html
+++ b/src/browser/tests/events.html
@@ -762,3 +762,95 @@
testing.expectEqual(2, calls.length);
}
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/frames/cross_realm_collection.html b/src/browser/tests/frames/cross_realm_collection.html
new file mode 100644
index 00000000..9d14b1b9
--- /dev/null
+++ b/src/browser/tests/frames/cross_realm_collection.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/frames/support/cross_realm_collection.html b/src/browser/tests/frames/support/cross_realm_collection.html
new file mode 100644
index 00000000..f247803c
--- /dev/null
+++ b/src/browser/tests/frames/support/cross_realm_collection.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/browser/tests/node/append_child.html b/src/browser/tests/node/append_child.html
index 151815b9..48bffe91 100644
--- a/src/browser/tests/node/append_child.html
+++ b/src/browser/tests/node/append_child.html
@@ -65,3 +65,32 @@
assertChildren(['a'], d3);
testing.expectEqual(null, b.parentNode);
+
+
+
+
diff --git a/src/browser/tests/node/insert_before.html b/src/browser/tests/node/insert_before.html
index 50dff07c..1018c314 100644
--- a/src/browser/tests/node/insert_before.html
+++ b/src/browser/tests/node/insert_before.html
@@ -39,3 +39,34 @@
assertChildren([], d1);
assertChildren([c1, c2], d2);
+
+
+
+
diff --git a/src/browser/tests/page/meta.html b/src/browser/tests/page/meta.html
index 41e354f1..3c03f403 100644
--- a/src/browser/tests/page/meta.html
+++ b/src/browser/tests/page/meta.html
@@ -42,4 +42,4 @@
-
+
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index 1c5314fb..fdabce23 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -488,8 +488,8 @@ pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, frame: *Frame)
pub fn append(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !void {
try validateDocumentNodes(self, nodes, false);
- frame.domChanged();
const parent = self.asNode();
+ parent.bumpDomVersion(frame);
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| {
@@ -513,8 +513,8 @@ pub fn append(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !v
pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !void {
try validateDocumentNodes(self, nodes, false);
- frame.domChanged();
const parent = self.asNode();
+ parent.bumpDomVersion(frame);
const parent_is_connected = parent.isConnected();
var i = nodes.len;
@@ -736,7 +736,7 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
insert_after = child;
}
- frame.domChanged();
+ parent.bumpDomVersion(frame);
self._write_insertion_point = children_to_insert.getLast();
}
diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig
index 186bc68a..106cb427 100644
--- a/src/browser/webapi/DocumentFragment.zig
+++ b/src/browser/webapi/DocumentFragment.zig
@@ -154,7 +154,7 @@ pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, frame: *Fra
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
- frame.domChanged();
+ parent.bumpDomVersion(frame);
var it = parent.childrenIterator();
while (it.next()) |child| {
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index a5aa19d5..91f47758 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -467,7 +467,7 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void {
const node = self.asNode();
const parent = node._parent orelse return;
- frame.domChanged();
+ parent.bumpDomVersion(frame);
if (html.len > 0) {
const fragment = (try Node.DocumentFragment.init(frame)).asNode();
try frame.parseHtmlAsChildren(fragment, html);
@@ -493,7 +493,7 @@ pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
// fires disconnectedCallback for custom elements, which can mutate
// the child list and dangle any cached next-pointer the iterator
// would otherwise hold.
- frame.domChanged();
+ parent.bumpDomVersion(frame);
while (parent.firstChild()) |child| {
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
}
@@ -879,10 +879,9 @@ pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, frame: *F
}
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
- frame.domChanged();
-
const ref_node = self.asNode();
const parent = ref_node._parent orelse return;
+ parent.bumpDomVersion(frame);
const parent_is_connected = parent.isConnected();
@@ -919,9 +918,9 @@ pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame
}
pub fn remove(self: *Element, frame: *Frame) void {
- frame.domChanged();
const node = self.asNode();
const parent = node._parent orelse return;
+ parent.bumpDomVersion(frame);
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
}
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index fafa32c6..8e8eea21 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -226,7 +226,7 @@ pub fn appendChild(self: *Node, child: *Node, frame: *Frame) !*Node {
try validateNodeInsertion(self, child);
- frame.domChanged();
+ self.bumpDomVersion(frame);
// If the child is currently connected, and if its new parent is connected,
// then we can remove + add a bit more efficiently (we don't have to fully
@@ -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,
@@ -512,11 +517,30 @@ pub fn ownerDocument(self: *const Node, frame: *const Frame) ?*Document {
return frame.document;
}
+// Returns the Frame that owns this node's tree. Used to tie cached state of
+// "live" collections (NodeList, HTMLCollection, etc.) to the right frame's DOM
+// version: cross-realm callers must invalidate based on mutations through the
+// node's owning frame, not the caller's frame.
+//
+// Falls back to `default` when the node has no associated document yet (e.g.,
+// freshly created and detached) or its document has no frame.
pub fn ownerFrame(self: *const Node, default: *Frame) *Frame {
+ if (self._type == .document) {
+ return self._type.document._frame orelse default;
+ }
const doc = self.ownerDocument(default) orelse return default;
return doc._frame orelse default;
}
+// Tells the owning frame that this node's subtree has changed. Use this from
+// mutation paths instead of `frame.domChanged()` so cross-realm mutations
+// (e.g., parent JS mutating an iframe's DOM) bump the iframe's version, not
+// the caller's. Otherwise, live collections that key off the owning frame's
+// version won't see the change and will return stale cached state.
+pub fn bumpDomVersion(self: *const Node, default: *Frame) void {
+ self.ownerFrame(default).domChanged();
+}
+
pub const ResolveURLOpts = struct {
allocator: ?Allocator = null,
};
@@ -548,7 +572,7 @@ pub fn removeChild(self: *Node, child: *Node, frame: *Frame) !*Node {
var it = self.childrenIterator();
while (it.next()) |n| {
if (n == child) {
- frame.domChanged();
+ self.bumpDomVersion(frame);
frame.removeNode(self, child, .{ .will_be_reconnected = false });
return child;
}
@@ -563,7 +587,7 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
// special case: if nodes are the same, ignore the change.
if (new_node == ref_node_) {
- frame.domChanged();
+ self.bumpDomVersion(frame);
if (frame.hasMutationObservers()) {
const parent = new_node._parent.?;
@@ -594,7 +618,7 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
const parent_owner = self.ownerDocument(frame) orelse self.as(Document);
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
- frame.domChanged();
+ self.bumpDomVersion(frame);
const will_be_reconnected = self.isConnected() and !adopting_to_new_document;
if (new_node._parent) |parent| {
frame.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });
@@ -605,6 +629,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,
@@ -1049,7 +1089,7 @@ pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !v
}
}
- frame.domChanged();
+ self.bumpDomVersion(frame);
// Remove all existing children
var it = self.childrenIterator();
diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig
index c1eb3fde..686c69df 100644
--- a/src/browser/webapi/Range.zig
+++ b/src/browser/webapi/Range.zig
@@ -374,7 +374,7 @@ pub fn deleteContents(self: *Range, frame: *Frame) !void {
if (self._proto.getCollapsed()) {
return;
}
- frame.domChanged();
+ self._proto._start_container.bumpDomVersion(frame);
// Simple case: same container
if (self._proto._start_container == self._proto._end_container) {
diff --git a/src/browser/webapi/collections/ChildNodes.zig b/src/browser/webapi/collections/ChildNodes.zig
index 1fa3b180..c94e7e53 100644
--- a/src/browser/webapi/collections/ChildNodes.zig
+++ b/src/browser/webapi/collections/ChildNodes.zig
@@ -34,8 +34,11 @@ _arena: std.mem.Allocator,
_last_index: usize,
_last_length: ?u32,
_last_node: ?*std.DoublyLinkedList.Node,
+// Version observed on `_owning_frame` the last time we refreshed our cache.
+// Compare against `_owning_frame.version` to detect DOM mutations.
_cached_version: usize,
_node: *Node,
+_owning_frame: *Frame,
pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
@@ -45,6 +48,8 @@ pub fn init(node: *Node, frame: *Frame) !*ChildNodes {
const arena = try frame.getArena(.small, "ChildNodes");
errdefer frame.releaseArena(arena);
+ const owning_frame = node.ownerFrame(frame);
+
const self = try arena.create(ChildNodes);
self.* = .{
._node = node,
@@ -52,7 +57,8 @@ pub fn init(node: *Node, frame: *Frame) !*ChildNodes {
._last_index = 0,
._last_node = null,
._last_length = null,
- ._cached_version = frame.version,
+ ._cached_version = owning_frame.version,
+ ._owning_frame = owning_frame,
};
return self;
}
@@ -61,8 +67,8 @@ pub fn deinit(self: *const ChildNodes, page: *Page) void {
page.releaseArena(self._arena);
}
-pub fn length(self: *ChildNodes, frame: *Frame) !u32 {
- if (self.versionCheck(frame)) {
+pub fn length(self: *ChildNodes, _: *Frame) !u32 {
+ if (self.versionCheck()) {
if (self._last_length) |cached_length| {
return cached_length;
}
@@ -75,16 +81,16 @@ pub fn length(self: *ChildNodes, frame: *Frame) !u32 {
return len;
}
-pub fn getAtIndex(self: *ChildNodes, index: usize, frame: *Frame) !?*Node {
- _ = self.versionCheck(frame);
+pub fn getAtIndex(self: *ChildNodes, index: usize, _: *Frame) !?*Node {
+ _ = self.versionCheck();
var current = self._last_index;
var node: ?*std.DoublyLinkedList.Node = null;
- if (index < current) {
+ if (index < current or self._last_node == null) {
current = 0;
node = self.first() orelse return null;
} else {
- node = self._last_node orelse self.first() orelse return null;
+ node = self._last_node;
}
defer self._last_index = current;
@@ -116,8 +122,8 @@ pub fn entries(self: *ChildNodes, frame: *Frame) !*EntryIterator {
return .init(.{ .list = self }, frame);
}
-fn versionCheck(self: *ChildNodes, frame: *Frame) bool {
- const current = frame.version;
+fn versionCheck(self: *ChildNodes) bool {
+ const current = self._owning_frame.version;
if (current == self._cached_version) {
return true;
}
diff --git a/src/browser/webapi/collections/HTMLAllCollection.zig b/src/browser/webapi/collections/HTMLAllCollection.zig
index 835de18c..7e9758b7 100644
--- a/src/browser/webapi/collections/HTMLAllCollection.zig
+++ b/src/browser/webapi/collections/HTMLAllCollection.zig
@@ -32,19 +32,23 @@ _tw: TreeWalker.FullExcludeSelf,
_last_index: usize,
_last_length: ?u32,
_cached_version: usize,
+_owning_frame: *Frame,
pub fn init(root: *Node, frame: *Frame) HTMLAllCollection {
+ const owning_frame = root.ownerFrame(frame);
return .{
._last_index = 0,
._last_length = null,
._tw = TreeWalker.FullExcludeSelf.init(root, .{}),
- ._cached_version = frame.version,
+ ._cached_version = owning_frame.version,
+ ._owning_frame = owning_frame,
};
}
-fn versionCheck(self: *HTMLAllCollection, frame: *const Frame) bool {
- if (self._cached_version != frame.version) {
- self._cached_version = frame.version;
+fn versionCheck(self: *HTMLAllCollection, _: *const Frame) bool {
+ const current = self._owning_frame.version;
+ if (self._cached_version != current) {
+ self._cached_version = current;
self._last_index = 0;
self._last_length = null;
self._tw.reset();
diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig
index 60e2e40c..33962d8e 100644
--- a/src/browser/webapi/collections/node_live.zig
+++ b/src/browser/webapi/collections/node_live.zig
@@ -99,16 +99,19 @@ pub fn NodeLive(comptime mode: Mode) type {
_last_index: usize,
_last_length: ?u32,
_cached_version: usize,
+ _owning_frame: *Frame,
const Self = @This();
pub fn init(root: *Node, filter: Filter, frame: *Frame) Self {
+ const owning_frame = root.ownerFrame(frame);
return .{
._last_index = 0,
._last_length = null,
._filter = filter,
._tw = TW.init(root, .{}),
- ._cached_version = frame.version,
+ ._cached_version = owning_frame.version,
+ ._owning_frame = owning_frame,
};
}
@@ -342,8 +345,8 @@ pub fn NodeLive(comptime mode: Mode) type {
};
}
- fn versionCheck(self: *Self, frame: *const Frame) bool {
- const current = frame.version;
+ fn versionCheck(self: *Self, _: *const Frame) bool {
+ const current = self._owning_frame.version;
if (current == self._cached_version) {
return true;
}
diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig
index 1a45c687..03c83fe4 100644
--- a/src/browser/webapi/element/Attribute.zig
+++ b/src/browser/webapi/element/Attribute.zig
@@ -229,7 +229,7 @@ pub const List = struct {
};
try frame.addElementId(parent, element, entry._value.str());
}
- frame.domChanged();
+ element.asNode().bumpDomVersion(frame);
frame.attributeChange(element, result.normalized, entry._value, old_value);
return entry;
}
@@ -292,7 +292,7 @@ pub const List = struct {
frame.removeElementId(element, entry._value.str());
}
- frame.domChanged();
+ element.asNode().bumpDomVersion(frame);
frame.attributeRemove(element, result.normalized, old_value);
_ = frame._attribute_lookup.remove(@intFromPtr(entry));
self._list.remove(&entry._node);
diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig
index 3ed96f57..ffaba715 100644
--- a/src/browser/webapi/element/Html.zig
+++ b/src/browser/webapi/element/Html.zig
@@ -273,7 +273,7 @@ pub fn setInnerText(self: *HtmlElement, text: []const u8, frame: *Frame) !void {
const parent = self.asElement().asNode();
// Remove all existing children
- frame.domChanged();
+ parent.bumpDomVersion(frame);
var it = parent.childrenIterator();
while (it.next()) |child| {
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
diff --git a/src/browser/webapi/element/html/Option.zig b/src/browser/webapi/element/html/Option.zig
index 9ca53f25..494573cf 100644
--- a/src/browser/webapi/element/html/Option.zig
+++ b/src/browser/webapi/element/html/Option.zig
@@ -80,7 +80,7 @@ pub fn setSelected(self: *Option, selected: bool, frame: *Frame) !void {
// TODO: When setting selected=true, may need to unselect other options
// in the parent