wpt, shadowdom: Improve shadowdom

This was driven by various WPT tests in the /shadow-dom/ category. There are
4 distinct changes.

1. Template hooks into "cloned" to include the _content. This change required
   passing `deep: bool` which is why TextArea and Input are also changed (they
   ignore that new parameter)

2. Node.getRootNode and Node.ownerDocument will now traverse through the
   ShadowRoot to find the root/document

3. DOM events won't gain the Window when triggered from within a ShadowRoot

4. attachShadow now takes a full option, not just a string mode. This also
   touched a few different places since it's called internally too.
This commit is contained in:
Karl Seguin
2026-06-10 15:28:11 +08:00
parent 6bead7becd
commit caeb359f75
17 changed files with 467 additions and 42 deletions

View File

@@ -176,12 +176,21 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
const activation_state = try ActivationState.create(event, target, frame);
var path_len: usize = 0;
var node_path_len: usize = 0;
var path_buffer: [128]*EventTarget = undefined;
// Defer runs even on early return - ensures event phase is reset
// and default actions execute (unless prevented)
defer {
event._event_phase = .none;
event._current_target = null;
event._stop_propagation = false;
event._stop_immediate_propagation = false;
if (event._needs_retargeting and node_path_len > 0) {
const adjusted = getAdjustedTarget(event._dispatch_target, path_buffer[node_path_len - 1]);
event._target = if (rootIsShadowRoot(adjusted)) null else adjusted;
}
// Handle checkbox/radio activation rollback or commit
if (activation_state) |state| {
state.restore(event, frame);
@@ -201,9 +210,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
}
}
var path_len: usize = 0;
var path_buffer: [128]*EventTarget = undefined;
var node: ?*Node = target;
while (node) |n| {
if (path_len >= path_buffer.len) break;
@@ -227,11 +233,18 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
node = n._parent;
}
node_path_len = path_len;
// Even though the window isn't part of the DOM, most events propagate
// through it in the capture phase (unless we stopped at a shadow boundary)
// The only explicit exception is "load"
if (event._type_string.eql(comptime .wrap("load")) == false) {
if (path_len < path_buffer.len) {
// through it in the capture phase. It only participates when the tree's
// root is the document (not for detached trees, and not when propagation
// stopped at a shadow boundary). The only explicit exception is "load".
if (event._type_string.eql(comptime .wrap("load")) == false and path_len < path_buffer.len) {
const root_is_document = path_len > 0 and switch (path_buffer[path_len - 1]._type) {
.node => |n| n._type == .document,
else => false,
};
if (root_is_document) {
path_buffer[path_len] = frame.window.asEventTarget();
path_len += 1;
}
@@ -454,6 +467,22 @@ fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarge
return original_target;
}
// Whether the target's tree root (without crossing shadow boundaries) is a
// shadow root. Used for the spec's post-dispatch "clear targets" step.
fn rootIsShadowRoot(target_: ?*EventTarget) bool {
const ShadowRoot = @import("webapi/ShadowRoot.zig");
const target = target_ orelse return false;
var current: *Node = switch (target._type) {
.node => |n| n,
else => return false,
};
while (current._parent) |p| {
current = p;
}
return current.is(ShadowRoot) != null;
}
// Check if ancestor is an ancestor of (or the same as) node
// WITHOUT crossing shadow boundaries (just regular DOM tree)
fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {

View File

@@ -253,6 +253,9 @@ pub fn dispatchDirect(
ls.deinit();
}
// Per spec, currentTarget is only set while listeners are being invoked
defer event._current_target = null;
// Call the property handler (e.g., onmessage) if present
if (getFunction(handler, &ls.local)) |func| {
event._current_target = target;

View File

@@ -775,7 +775,7 @@ fn testMarkdownShadow(light: []const u8, shadow: []const u8, expected: []const u
try frame.parseHtmlAsChildren(host.asNode(), light);
}
const sr = try host.attachShadow(comptime .wrap("open"), frame);
const sr = try host.attachShadow(.{ .mode = .open }, frame);
try frame.parseHtmlAsChildren(sr.asNode(), shadow);
var aw: std.Io.Writer.Allocating = .init(testing.allocator);

View File

@@ -579,14 +579,19 @@ fn attachDeclarativeShadowCallback(ctx: *anyopaque, host_ref: *anyopaque, templa
fn _attachDeclarativeShadowCallback(self: *Parser, host_node: *Node, template_node: *Node, mode_is_open: bool) !u8 {
// guaranteed by html5ever
const host = host_node.as(Element);
const mode: lp.String = if (mode_is_open) comptime .wrap("open") else comptime .wrap("closed");
const shadow = host.attachShadow(mode, self.frame) catch |err| switch (err) {
// Expected per-spec fall-backs (host can't host a shadow, or already
// has one): keep the <template> in the light DOM instead.
const template_el = template_node.as(Element);
const shadow = host.attachShadow(.{
.declarative = true,
.mode = if (mode_is_open) .open else .closed,
.delegates_focus = template_el.hasAttributeSafe(.wrap("shadowrootdelegatesfocus")),
.clonable = template_el.hasAttributeSafe(.wrap("shadowrootclonable")),
.serializable = template_el.hasAttributeSafe(.wrap("shadowrootserializable")),
}, self.frame) catch |err| switch (err) {
error.NotSupported => return 0,
else => return err,
};
const template = template_node.as(Element).is(Element.Html.Template) orelse return 0;
const template = template_el.is(Element.Html.Template) orelse return 0;
template._content = shadow.asDocumentFragment();
return 1;
}

View File

@@ -228,3 +228,25 @@
testing.expectEqual('<p>hello, world</p>', out.innerHTML);
</script>
<script id=cloneNode_copies_content>
{
const template = $('#basic');
// Per spec, template content is only copied on a deep clone.
const shallow = template.cloneNode(false);
testing.expectEqual(0, shallow.content.childNodes.length);
const deep = template.cloneNode(true);
testing.expectEqual('Hello Template', deep.content.querySelector('h1').textContent);
testing.expectTrue(deep.content.querySelector('h1') !== template.content.querySelector('h1'));
// Cloning an ancestor must also clone nested template content.
const wrapper = document.createElement('div');
const inner = document.createElement('template');
inner.innerHTML = '<span id="in-template">x</span>';
wrapper.appendChild(inner);
const wrapperClone = wrapper.cloneNode(true);
testing.expectEqual('x', wrapperClone.querySelector('template').content.querySelector('#in-template').textContent);
}
</script>

View File

@@ -122,3 +122,96 @@
testing.expectError('NotSupportedError', () => sd.attachShadow({ mode: 'open' }));
}
</script>
<script id=attachShadow_options_reflection>
{
const host = document.createElement('div');
const shadow = host.attachShadow({
mode: 'open',
delegatesFocus: true,
slotAssignment: 'manual',
clonable: true,
serializable: true,
});
testing.expectEqual(true, shadow.delegatesFocus);
testing.expectEqual('manual', shadow.slotAssignment);
testing.expectEqual(true, shadow.clonable);
testing.expectEqual(true, shadow.serializable);
const defaults = document.createElement('div').attachShadow({ mode: 'open' });
testing.expectEqual(false, defaults.delegatesFocus);
testing.expectEqual('named', defaults.slotAssignment);
testing.expectEqual(false, defaults.clonable);
testing.expectEqual(false, defaults.serializable);
}
</script>
<script id=attachShadow_declarative_reattach>
{
const wrapper = document.createElement('div');
wrapper.setHTMLUnsafe('<div><template shadowrootmode="open" shadowrootdelegatesfocus><span>declared</span></template></div>');
const host = wrapper.firstElementChild;
const declarative = host.shadowRoot;
testing.expectEqual(true, declarative.delegatesFocus);
testing.expectEqual('declared', declarative.firstElementChild.textContent);
// Mismatched mode throws.
let caught = null;
try { host.attachShadow({ mode: 'closed' }); } catch (e) { caught = e.name; }
testing.expectEqual('NotSupportedError', caught);
// Matching mode returns the same root, emptied.
const reattached = host.attachShadow({ mode: 'open' });
testing.expectEqual(declarative, reattached);
testing.expectEqual(0, reattached.childNodes.length);
// No longer declarative: a second attachShadow now throws.
caught = null;
try { host.attachShadow({ mode: 'open' }); } catch (e) { caught = e.name; }
testing.expectEqual('NotSupportedError', caught);
}
</script>
<script id=clonable_shadow_cloned_with_host>
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open', clonable: true });
shadow.innerHTML = '<input><div><span></span></div>';
// Even a shallow host clone deep-clones a clonable shadow root.
const shallow = host.cloneNode(false);
testing.expectEqual(true, shallow.shadowRoot.clonable);
testing.expectEqual(2, shallow.shadowRoot.children.length);
testing.expectEqual('SPAN', shallow.shadowRoot.children[1].firstElementChild.tagName);
// Non-clonable shadow roots are not cloned.
const plainHost = document.createElement('div');
plainHost.attachShadow({ mode: 'open' });
testing.expectEqual(null, plainHost.cloneNode(true).shadowRoot);
}
</script>
<script id=template_shadowroot_reflection>
{
const t = document.createElement('template');
testing.expectEqual('', t.shadowRootMode);
testing.expectEqual(false, t.shadowRootDelegatesFocus);
t.shadowRootMode = 'open';
testing.expectEqual('open', t.shadowRootMode);
testing.expectEqual('open', t.getAttribute('shadowrootmode'));
// Limited to known values: invalid reads back as "".
t.shadowRootMode = 'blah';
testing.expectEqual('', t.shadowRootMode);
testing.expectEqual('blah', t.getAttribute('shadowrootmode'));
t.shadowRootDelegatesFocus = true;
testing.expectEqual(true, t.hasAttribute('shadowrootdelegatesfocus'));
t.shadowRootClonable = true;
testing.expectEqual(true, t.shadowRootClonable);
t.shadowRootClonable = false;
testing.expectEqual(false, t.hasAttribute('shadowrootclonable'));
testing.expectEqual(false, t.shadowRootSerializable);
}
</script>

View File

@@ -281,3 +281,48 @@
host.remove();
}
</script>
<script id="composedPath_detached_tree_excludes_window">
{
// A detached tree's path ends at its root: no document, no window.
const root = document.createElement('div');
const target = document.createElement('span');
root.appendChild(target);
let capturedPath = null;
root.addEventListener('my-event', (e) => {
capturedPath = e.composedPath();
});
target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true }));
testing.expectEqual(2, capturedPath.length);
testing.expectEqual(target, capturedPath[0]);
testing.expectEqual(root, capturedPath[1]);
}
</script>
<script id="post_dispatch_state">
{
const host = document.createElement('div');
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
const target = document.createElement('span');
shadow.appendChild(target);
shadow.addEventListener('my-event', () => {});
// composed: crossing the boundary retargets to the host post-dispatch.
const composed = new Event('my-event', { bubbles: true, composed: true });
target.dispatchEvent(composed);
testing.expectEqual(host, composed.target);
testing.expectEqual(null, composed.currentTarget);
testing.expectEqual(0, composed.eventPhase);
testing.expectEqual(0, composed.composedPath().length);
// non-composed: the event never left the shadow tree, targets are cleared.
const scoped = new Event('my-event', { bubbles: true, composed: false });
target.dispatchEvent(scoped);
testing.expectEqual(null, scoped.target);
host.remove();
}
</script>

View File

@@ -90,3 +90,19 @@
host.remove();
}
</script>
<script id="ownerDocument_in_shadow_of_other_document">
{
const doc = document.implementation.createHTMLDocument('Test');
const host = doc.createElement('div');
doc.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
const child = doc.createElement('span');
shadow.appendChild(child);
// Nodes in a shadow tree belong to the host's document, not the
// frame's main document.
testing.expectEqual(doc, child.ownerDocument);
testing.expectEqual(doc, shadow.ownerDocument);
}
</script>

View File

@@ -119,7 +119,8 @@
window.reportError(err);
testing.expectEqual(window, evt.target);
testing.expectEqual(window, evt.currentTarget);
// currentTarget is only set while listeners are invoked; null post-dispatch
testing.expectEqual(null, evt.currentTarget);
}
</script>

View File

@@ -763,10 +763,7 @@ fn isValidShadowHost(self: *const Element) bool {
};
}
pub fn attachShadow(self: *Element, mode_str: String, frame: *Frame) !*ShadowRoot {
if (frame._element_shadow_roots.get(self)) |_| {
return error.NotSupported;
}
pub fn attachShadow(self: *Element, opts: ShadowRoot.AttachOptions, frame: *Frame) !*ShadowRoot {
if (!self.isValidShadowHost()) {
return error.NotSupported;
}
@@ -780,13 +777,20 @@ pub fn attachShadow(self: *Element, mode_str: String, frame: *Frame) !*ShadowRoo
}
}
}
const mode: ShadowRoot.Mode = blk: {
if (mode_str.eql(comptime .wrap("open"))) break :blk .open;
if (mode_str.eql(comptime .wrap("closed"))) break :blk .closed;
return error.InvalidArgument;
};
const shadow_root = try ShadowRoot.init(self, mode, frame);
if (frame._element_shadow_roots.get(self)) |existing| {
// Imperative attachShadow over a declarative shadow root with a matching
// mode empties it and returns the same root. The parser
// (opts.declarative) never replaces an existing root.
if (opts.declarative or !existing._declarative or existing._mode != opts.mode) {
return error.NotSupported;
}
try existing.asNode().replaceChildren(&.{}, frame);
existing._declarative = false;
return existing;
}
const shadow_root = try ShadowRoot.init(self, opts, frame);
try frame._element_shadow_roots.put(frame.arena, self, shadow_root);
return shadow_root;
}
@@ -1459,10 +1463,33 @@ pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node {
const node = try frame.createElementNS(self._namespace, tag_name, self._attributes);
// Allow element-specific types to copy their runtime state
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), frame }) catch |err| {
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), deep, frame }) catch |err| {
log.err(.dom, "element.clone.failed", .{ .err = err });
};
// Per spec, a clonable shadow root is cloned along with its host — its
// children always deep-cloned, even when the host clone is shallow.
if (frame._element_shadow_roots.get(self)) |shadow| {
if (shadow._clonable) {
const cloned_shadow = node.as(Element).attachShadow(.{
.mode = shadow._mode,
.clonable = true,
.delegates_focus = shadow._delegates_focus,
.slot_assignment = shadow._slot_assignment,
.serializable = shadow._serializable,
.declarative = shadow._declarative,
}, frame) catch return error.CloneError;
const cloned_shadow_node = cloned_shadow.asNode();
var shadow_child_it = shadow.asNode().childrenIterator();
while (shadow_child_it.next()) |child| {
if (try child.cloneNodeForAppending(true, frame)) |cloned_child| {
try frame.appendNode(cloned_shadow_node, cloned_child, .{ .child_already_connected = true });
}
}
}
}
if (deep) {
var child_it = self.asNode().childrenIterator();
while (child_it.next()) |child| {
@@ -1915,9 +1942,30 @@ pub const JsApi = struct {
const ShadowRootInit = struct {
mode: String,
delegatesFocus: bool = false,
slotAssignment: ?String = null,
clonable: bool = false,
serializable: bool = false,
};
fn _attachShadow(self: *Element, init: ShadowRootInit, frame: *Frame) !*ShadowRoot {
return self.attachShadow(init.mode, frame);
const mode: ShadowRoot.Mode = blk: {
if (init.mode.eql(comptime .wrap("open"))) break :blk .open;
if (init.mode.eql(comptime .wrap("closed"))) break :blk .closed;
return error.InvalidArgument;
};
const slot_assignment: ShadowRoot.SlotAssignment = blk: {
const sa = init.slotAssignment orelse break :blk .named;
if (sa.eql(comptime .wrap("named"))) break :blk .named;
if (sa.eql(comptime .wrap("manual"))) break :blk .manual;
return error.InvalidArgument;
};
return self.attachShadow(.{
.mode = mode,
.delegates_focus = init.delegatesFocus,
.slot_assignment = slot_assignment,
.clonable = init.clonable,
.serializable = init.serializable,
}, frame);
}
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true, .ce_reactions = true });

View File

@@ -330,15 +330,23 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
node = n._parent;
}
// Add window at the end (unless we stopped at shadow boundary)
if (!stopped_at_shadow_boundary) {
if (path_len < path_buffer.len) {
switch (exec.js.global) {
.worker => {},
.frame => |frame| {
path_buffer[path_len] = frame.window.asEventTarget();
path_len += 1;
},
// Add window at the end. It only participates when propagation did not stop
// at a shadow boundary...
if (stopped_at_shadow_boundary == false) {
// ... AND when the tree's root is a document
const root_is_document = path_len > 0 and switch (path_buffer[path_len - 1]._type) {
.node => |n| n._type == .document,
else => false,
};
if (root_is_document) {
if (path_len < path_buffer.len) {
switch (exec.js.global) {
.worker => {},
.frame => |frame| {
path_buffer[path_len] = frame.window.asEventTarget();
path_len += 1;
},
}
}
}
}

View File

@@ -479,7 +479,7 @@ pub fn getRootNode(self: *Node, opts_: ?GetRootNodeOpts) *Node {
// If composed is true, traverse through shadow boundaries
if (opts.composed) {
while (true) {
const shadow_root = @constCast(root).is(ShadowRoot) orelse break;
const shadow_root = root.is(ShadowRoot) orelse break;
root = shadow_root.getHost().asNode();
while (root._parent) |parent| {
root = parent;
@@ -525,6 +525,16 @@ pub fn ownerDocument(self: *const Node, frame: *const Frame) ?*Document {
return current._type.document;
}
// A shadow tree's root is a parent-less ShadowRoot fragment; its owner
// is the host's owner document.
// can't use current.is(ShadowRoot) without @constCast on `current`
if (current._type == .document_fragment) {
const df = current._type.document_fragment;
if (df._type == .shadow_root) {
return df._type.shadow_root._host.asNode().ownerDocument(frame);
}
}
// Otherwise, this is a detached node. Check if it has a specific owner
// document registered (for nodes created via non-main documents).
if (frame._node_owner_documents.get(@constCast(self))) |owner| {

View File

@@ -31,18 +31,42 @@ pub const Mode = enum {
closed,
};
pub const SlotAssignment = enum {
named,
manual,
};
pub const AttachOptions = struct {
mode: Mode,
delegates_focus: bool = false,
slot_assignment: SlotAssignment = .named,
clonable: bool = false,
serializable: bool = false,
declarative: bool = false,
};
_proto: *DocumentFragment,
_mode: Mode,
_host: *Element,
_delegates_focus: bool,
_slot_assignment: SlotAssignment,
_clonable: bool,
_serializable: bool,
_declarative: bool,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
_adopted_style_sheets: ?js.Object.Global = null,
pub fn init(host: *Element, mode: Mode, frame: *Frame) !*ShadowRoot {
pub fn init(host: *Element, opts: AttachOptions, frame: *Frame) !*ShadowRoot {
return frame._factory.documentFragment(ShadowRoot{
._proto = undefined,
._mode = mode,
._mode = opts.mode,
._host = host,
._delegates_focus = opts.delegates_focus,
._slot_assignment = opts.slot_assignment,
._clonable = opts.clonable,
._serializable = opts.serializable,
._declarative = opts.declarative,
});
}
@@ -66,6 +90,22 @@ pub fn getHost(self: *const ShadowRoot) *Element {
return self._host;
}
pub fn getDelegatesFocus(self: *const ShadowRoot) bool {
return self._delegates_focus;
}
pub fn getSlotAssignment(self: *const ShadowRoot) []const u8 {
return @tagName(self._slot_assignment);
}
pub fn getClonable(self: *const ShadowRoot) bool {
return self._clonable;
}
pub fn getSerializable(self: *const ShadowRoot) bool {
return self._serializable;
}
pub fn setHTMLUnsafe(self: *ShadowRoot, html: []const u8, frame: *Frame) !void {
return self.asDocumentFragment().setHTMLUnsafe(html, frame);
}
@@ -125,6 +165,10 @@ pub const JsApi = struct {
pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});
pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});
pub const delegatesFocus = bridge.accessor(ShadowRoot.getDelegatesFocus, null, .{});
pub const slotAssignment = bridge.accessor(ShadowRoot.getSlotAssignment, null, .{});
pub const clonable = bridge.accessor(ShadowRoot.getClonable, null, .{});
pub const serializable = bridge.accessor(ShadowRoot.getSerializable, null, .{});
pub const getElementById = bridge.function(_getElementById, .{});
fn _getElementById(self: *ShadowRoot, value_: ?js.Value, frame: *Frame) !?*Element {
const value = value_ orelse return null;

View File

@@ -1517,7 +1517,8 @@ pub const Build = struct {
}
}
pub fn cloned(source_element: *Element, cloned_element: *Element, _: *Frame) !void {
pub fn cloned(source_element: *Element, cloned_element: *Element, deep: bool, _: *Frame) !void {
_ = deep;
const source = source_element.as(Input);
const clone = cloned_element.as(Input);

View File

@@ -154,7 +154,8 @@ pub const Build = struct {
// Per the HTML spec, the "already started" flag must be propagated to the
// clone so that re-inserting a cloned <script> doesn't run it again.
pub fn cloned(source_element: *Element, cloned_element: *Element, _: *Frame) !void {
pub fn cloned(source_element: *Element, cloned_element: *Element, deep: bool, _: *Frame) !void {
_ = deep;
const source = source_element.as(Script);
const clone = cloned_element.as(Script);
clone._executed = source._executed;

View File

@@ -1,12 +1,35 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../../js/js.zig");
const Frame = @import("../../../Frame.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const DocumentFragment = @import("../../DocumentFragment.zig");
const HtmlElement = @import("../Html.zig");
const String = lp.String;
const Template = @This();
_proto: *HtmlElement,
@@ -15,6 +38,10 @@ _content: *DocumentFragment,
pub fn asElement(self: *Template) *Element {
return self._proto._proto;
}
pub fn asConstElement(self: *const Template) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Template) *Node {
return self.asElement().asNode();
}
@@ -27,6 +54,36 @@ pub fn setInnerHTML(self: *Template, html: []const u8, frame: *Frame) !void {
return self._content.setInnerHTML(html, frame);
}
pub fn getShadowRootMode(self: *const Template) []const u8 {
const value = self.asConstElement().getAttributeSafe(.wrap("shadowrootmode")) orelse return "";
if (std.ascii.eqlIgnoreCase(value, "open")) {
return "open";
}
if (std.ascii.eqlIgnoreCase(value, "closed")) {
return "closed";
}
return "";
}
pub fn setShadowRootMode(self: *Template, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(.wrap("shadowrootmode"), .wrap(value), frame);
}
fn getBoolAttribute(self: *const Template, name: String) bool {
return self.asConstElement().getAttributeSafe(name) != null;
}
fn setBoolAttribute(self: *Template, name: String, value: bool, frame: *Frame) !void {
if (value) {
try self.asElement().setAttributeSafe(name, .wrap(""), frame);
} else {
try self.asElement().removeAttribute(name, frame);
}
}
pub fn getOuterHTML(self: *Template, writer: *std.Io.Writer, frame: *Frame) !void {
const dump = @import("../../../dump.zig");
const el = self.asElement();
@@ -50,6 +107,29 @@ pub const JsApi = struct {
pub const content = bridge.accessor(Template.getContent, null, .{});
pub const innerHTML = bridge.accessor(_getInnerHTML, Template.setInnerHTML, .{ .ce_reactions = true });
pub const outerHTML = bridge.accessor(_getOuterHTML, null, .{});
pub const shadowRootMode = bridge.accessor(Template.getShadowRootMode, Template.setShadowRootMode, .{ .ce_reactions = true });
pub const shadowRootDelegatesFocus = bridge.accessor(_getShadowRootDelegatesFocus, _setShadowRootDelegatesFocus, .{ .ce_reactions = true });
pub const shadowRootClonable = bridge.accessor(_getShadowRootClonable, _setShadowRootClonable, .{ .ce_reactions = true });
pub const shadowRootSerializable = bridge.accessor(_getShadowRootSerializable, _setShadowRootSerializable, .{ .ce_reactions = true });
fn _getShadowRootDelegatesFocus(self: *const Template) bool {
return self.getBoolAttribute(.wrap("shadowrootdelegatesfocus"));
}
fn _setShadowRootDelegatesFocus(self: *Template, value: bool, frame: *Frame) !void {
try self.setBoolAttribute(.wrap("shadowrootdelegatesfocus"), value, frame);
}
fn _getShadowRootClonable(self: *const Template) bool {
return self.getBoolAttribute(.wrap("shadowrootclonable"));
}
fn _setShadowRootClonable(self: *Template, value: bool, frame: *Frame) !void {
try self.setBoolAttribute(.wrap("shadowrootclonable"), value, frame);
}
fn _getShadowRootSerializable(self: *const Template) bool {
return self.getBoolAttribute(.wrap("shadowrootserializable"));
}
fn _setShadowRootSerializable(self: *Template, value: bool, frame: *Frame) !void {
try self.setBoolAttribute(.wrap("shadowrootserializable"), value, frame);
}
fn _getInnerHTML(self: *Template, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
@@ -70,6 +150,24 @@ pub const Build = struct {
// Create the template content DocumentFragment
self._content = try DocumentFragment.init(frame);
}
// Per the HTML spec's cloning steps for <template>, a deep clone must
// also copy the content fragment (the element itself has no childNodes,
// so the generic deep-clone loop won't do it).
pub fn cloned(source_element: *Element, cloned_element: *Element, deep: bool, frame: *Frame) !void {
if (!deep) {
return;
}
const source = source_element.as(Template);
const clone = cloned_element.as(Template);
const clone_content = clone._content.asNode();
var child_it = source._content.asNode().childrenIterator();
while (child_it.next()) |child| {
if (try child.cloneNodeForAppending(true, frame)) |cloned_child| {
try frame.appendNode(clone_content, cloned_child, .{ .child_already_connected = true });
}
}
}
};
const testing = @import("../../../../testing.zig");

View File

@@ -421,7 +421,8 @@ pub const JsApi = struct {
};
pub const Build = struct {
pub fn cloned(source_element: *Element, cloned_element: *Element, _: *Frame) !void {
pub fn cloned(source_element: *Element, cloned_element: *Element, deep: bool, _: *Frame) !void {
_ = deep;
const source = source_element.as(TextArea);
const clone = cloned_element.as(TextArea);
clone._value = source._value;