mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user