diff --git a/src/browser/tests/custom_elements/constructor.html b/src/browser/tests/custom_elements/constructor.html index 0241b242..c99639a6 100644 --- a/src/browser/tests/custom_elements/constructor.html +++ b/src/browser/tests/custom_elements/constructor.html @@ -72,3 +72,59 @@ testing.expectEqual(2, calls); } + +
+ + + + + + diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index f804b08e..004ee916 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -195,8 +195,9 @@ pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node { var child_it = node.childrenIterator(); while (child_it.next()) |child| { - const cloned_child = try child.cloneNode(true, page); - try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected }); + if (try child.cloneNodeForAppending(true, page)) |cloned_child| { + try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected }); + } } } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index ef2386da..7a6598a3 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1328,20 +1328,12 @@ pub fn clone(self: *Element, deep: bool, page: *Page) !*Node { if (deep) { var child_it = self.asNode().childrenIterator(); while (child_it.next()) |child| { - const cloned_child = try child.cloneNode(true, page); - if (cloned_child._parent != null) { - // This is almost always false, the only case where a cloned - // node would already have a parent is with a custom element - // that has a constructor (which is called during cloning) which - // inserts it somewhere. In that case, whatever parent was set - // in the constructor should not be changed. - continue; + if (try child.cloneNodeForAppending(true, page)) |cloned_child| { + // We pass `true` to `child_already_connected` as a hacky optimization + // We _know_ this child isn't connected (Because the parent isn't connected) + // setting this to `true` skips all connection checks. + try page.appendNode(node, cloned_child, .{ .child_already_connected = true }); } - - // We pass `true` to `child_already_connected` as a hacky optimization - // We _know_ this child isn't connected (Because the parent isn't connected) - // setting this to `true` skips all connection checks. - try page.appendNode(node, cloned_child, .{ .child_already_connected = true }); } } diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 4b351b6f..e93cc1f7 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -751,6 +751,29 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node { } } +/// Clone a node for the purpose of appending to a parent. +/// Returns null if the cloned node was already attached somewhere by a custom element +/// constructor, indicating that the constructor's decision should be respected. +/// +/// This helper is used when iterating over children to clone them. The typical pattern is: +/// while (child_it.next()) |child| { +/// if (try child.cloneNodeForAppending(true, page)) |cloned| { +/// try page.appendNode(parent, cloned, opts); +/// } +/// } +/// +/// The only case where a cloned node would already have a parent is when a custom element +/// constructor (which runs during cloning per the HTML spec) explicitly attaches the element +/// somewhere. In that case, we respect the constructor's decision and return null to signal +/// that the cloned node should not be appended to our intended parent. +pub fn cloneNodeForAppending(self: *Node, deep: bool, page: *Page) CloneError!?*Node { + const cloned = try self.cloneNode(deep, page); + if (cloned._parent != null) { + return null; + } + return cloned; +} + pub fn compareDocumentPosition(self: *Node, other: *Node) u16 { const DISCONNECTED: u16 = 0x01; const PRECEDING: u16 = 0x02; diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 840fa227..21a3ce12 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -446,8 +446,9 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment { var offset = self._proto._start_offset; while (offset < self._proto._end_offset) : (offset += 1) { if (self._proto._start_container.getChildAt(offset)) |child| { - const cloned = try child.cloneNode(true, page); - _ = try fragment.asNode().appendChild(cloned, page); + if (try child.cloneNodeForAppending(true, page)) |cloned| { + _ = try fragment.asNode().appendChild(cloned, page); + } } } } @@ -468,9 +469,11 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment { if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) { var current = self._proto._start_container.nextSibling(); while (current != null and current != self._proto._end_container) { - const cloned = try current.?.cloneNode(true, page); - _ = try fragment.asNode().appendChild(cloned, page); - current = current.?.nextSibling(); + const next = current.?.nextSibling(); + if (try current.?.cloneNodeForAppending(true, page)) |cloned| { + _ = try fragment.asNode().appendChild(cloned, page); + } + current = next; } }