ShadowRoot

This commit is contained in:
Karl Seguin
2025-11-20 19:41:14 +08:00
parent bd3da38fc8
commit afaf105cb0
20 changed files with 1417 additions and 30 deletions

View File

@@ -121,6 +121,16 @@ pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {
return child_ptr;
}
pub fn documentFragment(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
child_ptr._proto = try self.node(Node.DocumentFragment{
._proto = undefined,
._type = unionInit(Node.DocumentFragment.Type, child_ptr),
});
return child_ptr;
}
pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;

View File

@@ -50,6 +50,8 @@ const Element = @import("webapi/Element.zig");
const Window = @import("webapi/Window.zig");
const Location = @import("webapi/Location.zig");
const Document = @import("webapi/Document.zig");
const DocumentFragment = @import("webapi/DocumentFragment.zig");
const ShadowRoot = @import("webapi/ShadowRoot.zig");
const Performance = @import("webapi/Performance.zig");
const HtmlScript = @import("webapi/Element.zig").Html.Script;
const MutationObserver = @import("webapi/MutationObserver.zig");
@@ -91,6 +93,7 @@ _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attri
_element_styles: Element.StyleLookup = .{},
_element_datasets: Element.DatasetLookup = .{},
_element_class_lists: Element.ClassListLookup = .{},
_element_shadow_roots: Element.ShadowRootLookup = .{},
_script_manager: ScriptManager,
@@ -211,6 +214,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._element_styles = .{};
self._element_datasets = .{};
self._element_class_lists = .{};
self._element_shadow_roots = .{};
self._notified_network_idle = .init;
self._notified_network_almost_idle = .init;
@@ -714,6 +718,39 @@ pub fn domChanged(self: *Page) void {
self.version += 1;
}
pub fn getElementIdMap(page: *Page, node: *Node) *std.StringHashMapUnmanaged(*Element) {
if (node.is(ShadowRoot)) |shadow_root| {
return &shadow_root._elements_by_id;
}
var parent = node._parent;
while (parent) |n| {
if (n.is(ShadowRoot)) |shadow_root| {
return &shadow_root._elements_by_id;
}
parent = n._parent;
}
return &page.document._elements_by_id;
}
pub fn addElementId(self: *Page, parent: *Node, element: *Element, id: []const u8) !void {
var id_map = self.getElementIdMap(parent);
const gop = try id_map.getOrPut(self.arena, id);
if (!gop.found_existing) {
gop.value_ptr.* = element;
}
}
pub fn removeElementId(self: *Page, element: *Element, id: []const u8) void {
var id_map = self.getElementIdMap(element.asNode());
_ = id_map.remove(id);
}
pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Element {
const id_map = self.getElementIdMap(node);
return id_map.get(id);
}
pub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void {
try self._mutation_observers.append(self.arena, observer);
}
@@ -1314,12 +1351,11 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
// The child was connected and now it no longer is. We need to "disconnect"
// it and all of its descendants. For now "disconnect" just means updating
// document._elements_by_id and invoking disconnectedCallback for custom elements
var elements_by_id = &self.document._elements_by_id;
// the ID map and invoking disconnectedCallback for custom elements
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
while (tw.next()) |el| {
if (el.getAttributeSafe("id")) |id| {
_ = elements_by_id.remove(id);
self.removeElementId(el, id);
}
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
@@ -1427,15 +1463,10 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
}
}
var document_by_id = &self.document._elements_by_id;
if (comptime from_parser) {
if (child.is(Element)) |el| {
if (el.getAttributeSafe("id")) |id| {
const gop = try document_by_id.getOrPut(self.arena, id);
if (!gop.found_existing) {
gop.value_ptr.* = el;
}
try self.addElementId(parent, el, id);
}
}
return;
@@ -1446,24 +1477,27 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
return;
}
if (parent.isConnected() == false) {
// The parent isn't connected, we don't have to connect the child
const parent_in_shadow = parent.is(ShadowRoot) != null or parent.isInShadowTree();
const parent_is_connected = parent.isConnected();
if (!parent_in_shadow and !parent_is_connected) {
return;
}
// If we're here, it means that a disconnected child became connected. We
// need to connect it (and all of its descendants)
// If we're here, it means either:
// 1. A disconnected child became connected (parent.isConnected() == true)
// 2. Child is being added to a shadow tree (parent_in_shadow == true)
// In both cases, we need to update ID maps and invoke callbacks
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
while (tw.next()) |el| {
if (el.getAttributeSafe("id")) |id| {
const gop = try document_by_id.getOrPut(self.arena, id);
if (!gop.found_existing) {
gop.value_ptr.* = el;
}
try self.addElementId(el.asNode()._parent.?, el, id);
}
Element.Html.Custom.invokeConnectedCallbackOnElement(el, self);
// Only invoke connected callback if actually connected to document
if (parent_is_connected) {
Element.Html.Custom.invokeConnectedCallbackOnElement(el, self);
}
}
}

View File

@@ -483,6 +483,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/KeyValueList.zig"),
@import("../webapi/DocumentFragment.zig"),
@import("../webapi/DocumentType.zig"),
@import("../webapi/ShadowRoot.zig"),
@import("../webapi/DOMException.zig"),
@import("../webapi/DOMImplementation.zig"),
@import("../webapi/DOMTreeWalker.zig"),

View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<div id="outer" class="container">
<div id="middle" class="wrapper">
<div id="inner" class="box">
<p id="paragraph" class="text">Content</p>
</div>
</div>
</div>
<script id=matchesSelf>
{
const para = document.getElementById('paragraph');
testing.expectEqual(para, para.closest('p'));
testing.expectEqual(para, para.closest('.text'));
testing.expectEqual(para, para.closest('#paragraph'));
testing.expectEqual(para, para.closest('p.text'));
testing.expectEqual(para, para.closest('p#paragraph.text'));
}
</script>
<script id=findsAncestors>
{
const para = document.getElementById('paragraph');
const inner = document.getElementById('inner');
testing.expectEqual(inner, para.closest('.box'));
testing.expectEqual(inner, para.closest('#inner'));
testing.expectEqual(inner, para.closest('div.box'));
const middle = document.getElementById('middle');
testing.expectEqual(middle, para.closest('.wrapper'));
testing.expectEqual(middle, para.closest('#middle'));
const outer = document.getElementById('outer');
testing.expectEqual(outer, para.closest('.container'));
testing.expectEqual(outer, para.closest('#outer'));
}
</script>
<script id=returnsNull>
{
const para = document.getElementById('paragraph');
testing.expectEqual(null, para.closest('.nonexistent'));
testing.expectEqual(null, para.closest('#missing'));
testing.expectEqual(null, para.closest('table'));
testing.expectEqual(null, para.closest('span'));
}
</script>
<script id=complexSelectors>
{
const para = document.getElementById('paragraph');
testing.expectEqual(document.getElementById('inner'), para.closest('div.box'));
testing.expectEqual(document.getElementById('middle'), para.closest('div.wrapper'));
testing.expectEqual(document.getElementById('outer'), para.closest('div.container'));
const inner = document.getElementById('inner');
inner.classList.add('active');
testing.expectEqual(inner, para.closest('.box.active'));
}
</script>
<script id=attributeSelectors>
{
const para = document.getElementById('paragraph');
const inner = document.getElementById('inner');
inner.setAttribute('data-test', 'value');
testing.expectEqual(inner, para.closest('[data-test]'));
testing.expectEqual(inner, para.closest('[data-test="value"]'));
testing.expectEqual(null, para.closest('[data-other]'));
}
</script>
<script id=errorHandling>
{
const para = document.getElementById('paragraph');
let caught = false;
try {
para.closest('');
} catch (e) {
caught = true;
testing.expectEqual('SyntaxError', e.name);
}
testing.expectEqual(true, caught);
}
</script>

View File

@@ -0,0 +1,156 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="host1"></div>
<div id="host2"></div>
<div id="host3"></div>
<!-- <script id="attachShadow_open">
{
const host = $('#host1');
const shadow = host.attachShadow({ mode: 'open' });
testing.expectEqual('open', shadow.mode);
testing.expectEqual(host, shadow.host);
testing.expectEqual(shadow, host.shadowRoot);
}
</script>
<script id="attachShadow_closed">
{
const host = $('#host2');
const shadow = host.attachShadow({ mode: 'closed' });
testing.expectEqual('closed', shadow.mode);
testing.expectEqual(host, shadow.host);
testing.expectEqual(null, host.shadowRoot);
}
</script>
<script id="innerHTML">
{
const host = $('#host3');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Hello</p><span>World</span>';
testing.expectEqual('<p>Hello</p><span>World</span>', shadow.innerHTML);
const p = shadow.querySelector('p');
testing.expectEqual('Hello', p.textContent);
const span = shadow.querySelector('span');
testing.expectEqual('World', span.textContent);
}
</script>
<script id="querySelector">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div class="test"><p id="para">Text</p></div>';
const para = shadow.querySelector('#para');
testing.expectEqual('Text', para.textContent);
const div = shadow.querySelector('.test');
testing.expectEqual('DIV', div.tagName);
testing.expectEqual(null, shadow.querySelector('.nonexistent'));
}
</script>
<script id="querySelectorAll">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>One</p><p>Two</p><p>Three</p>';
const paras = shadow.querySelectorAll('p');
testing.expectEqual(3, paras.length);
testing.expectEqual('One', paras[0].textContent);
testing.expectEqual('Two', paras[1].textContent);
testing.expectEqual('Three', paras[2].textContent);
}
</script>
<script id="children">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div>A</div><span>B</span><p>C</p>';
const children = shadow.children;
testing.expectEqual(3, children.length);
testing.expectEqual('DIV', children[0].tagName);
testing.expectEqual('SPAN', children[1].tagName);
testing.expectEqual('P', children[2].tagName);
}
</script>
<script id="firstLastChild">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div>First</div><span>Last</span>';
testing.expectEqual('DIV', shadow.firstElementChild.tagName);
testing.expectEqual('First', shadow.firstElementChild.textContent);
testing.expectEqual('SPAN', shadow.lastElementChild.tagName);
testing.expectEqual('Last', shadow.lastElementChild.textContent);
}
</script>
<script id="appendChild">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
const p = document.createElement('p');
p.textContent = 'Test';
shadow.appendChild(p);
testing.expectEqual(1, shadow.childElementCount);
testing.expectEqual(p, shadow.firstElementChild);
testing.expectEqual('Test', shadow.firstElementChild.textContent);
}
</script>
<script id="append_prepend">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.append('text1');
testing.expectEqual('text1', shadow.innerHTML);
shadow.prepend('text0');
testing.expectEqual('text0text1', shadow.innerHTML);
}
</script>
<script id="replaceChildren">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div>Old</div>';
testing.expectEqual(1, shadow.childElementCount);
shadow.replaceChildren('New content');
testing.expectEqual('New content', shadow.innerHTML);
}
</script> -->
<script id="getElementById">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="test">Content</div>';
const el = shadow.getElementById('test');
testing.expectEqual('Content', el.textContent);
testing.expectEqual(null, shadow.getElementById('nonexistent'));
}
</script>

View File

@@ -0,0 +1,138 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="shadow_in_constructor">
{
class ShadowElement extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
this.shadow.innerHTML = '<p>Shadow content</p>';
}
}
customElements.define('shadow-element', ShadowElement);
const el = document.createElement('shadow-element');
testing.expectEqual('open', el.shadowRoot.mode);
testing.expectEqual('<p>Shadow content</p>', el.shadowRoot.innerHTML);
}
</script>
<script id="shadow_in_connectedCallback">
{
let connectedCount = 0;
class ConnectedShadowElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
connectedCount++;
this.shadowRoot.innerHTML = `<div>Connected ${connectedCount}</div>`;
}
}
customElements.define('connected-shadow-element', ConnectedShadowElement);
const el = document.createElement('connected-shadow-element');
testing.expectEqual('', el.shadowRoot.innerHTML);
document.body.appendChild(el);
testing.expectEqual('<div>Connected 1</div>', el.shadowRoot.innerHTML);
el.remove();
document.body.appendChild(el);
testing.expectEqual('<div>Connected 2</div>', el.shadowRoot.innerHTML);
}
</script>
<script id="closed_shadow">
{
class ClosedShadowElement extends HTMLElement {
constructor() {
super();
this._shadow = this.attachShadow({ mode: 'closed' });
this._shadow.innerHTML = '<span>Private</span>';
}
getContent() {
return this._shadow.innerHTML;
}
}
customElements.define('closed-shadow-element', ClosedShadowElement);
const el = document.createElement('closed-shadow-element');
testing.expectEqual(null, el.shadowRoot);
testing.expectEqual('<span>Private</span>', el.getContent());
}
</script>
<script id="multiple_custom_elements_with_shadows">
{
class ComponentA extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<div class="a">Component A</div>';
}
}
class ComponentB extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<div class="b">Component B</div>';
}
}
customElements.define('component-a', ComponentA);
customElements.define('component-b', ComponentB);
const a = document.createElement('component-a');
const b = document.createElement('component-b');
testing.expectEqual('Component A', a.shadowRoot.querySelector('.a').textContent);
testing.expectEqual('Component B', b.shadowRoot.querySelector('.b').textContent);
testing.expectEqual(null, a.shadowRoot.querySelector('.b'));
testing.expectEqual(null, b.shadowRoot.querySelector('.a'));
}
</script>
<script id="nested_custom_elements">
{
class InnerElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<p>Nested</p>';
}
}
class OuterElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
addInner() {
const inner = document.createElement('inner-element');
this.shadowRoot.appendChild(inner);
return inner;
}
}
customElements.define('inner-element', InnerElement);
customElements.define('outer-element', OuterElement);
const outer = document.createElement('outer-element');
const inner = outer.addInner();
testing.expectEqual('INNER-ELEMENT', inner.tagName);
testing.expectEqual('<p>Nested</p>', inner.shadowRoot.innerHTML);
}
</script>

View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="parentNode_shadow_boundary">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="child">Content</div>';
document.body.appendChild(host);
const child = shadow.getElementById('child');
// In actual browsers, parentNode DOES expose the shadow root
testing.expectEqual(shadow, child.parentNode);
host.remove();
}
</script>
<script id="parentElement_shadow_boundary">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="child">Content</div>';
document.body.appendChild(host);
const child = shadow.getElementById('child');
// parentElement should also not expose shadow root
testing.expectEqual(null, child.parentElement);
host.remove();
}
</script>
<script id="getRootNode">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="child">Content</div>';
document.body.appendChild(host);
const child = shadow.getElementById('child');
// getRootNode should return the shadow root
const root = child.getRootNode();
testing.expectEqual(shadow, root);
// Host's getRootNode should return document
const hostRoot = host.getRootNode();
testing.expectEqual(document, hostRoot);
host.remove();
}
</script>
<script id="contains_crosses_shadow">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="child">Content</div>';
document.body.appendChild(host);
const child = shadow.getElementById('child');
// Shadow root contains its children
testing.expectEqual(true, shadow.contains(child));
// In actual browsers, contains() does NOT cross shadow boundaries
testing.expectEqual(false, host.contains(child));
testing.expectEqual(false, document.body.contains(child));
testing.expectEqual(false, document.contains(child));
host.remove();
}
</script>
<script id="closest_shadow_boundary">
{
const host = document.createElement('div');
host.className = 'host-class';
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div class="inner"><p id="para">Text</p></div>';
document.body.appendChild(host);
const para = shadow.getElementById('para');
// closest should find elements within shadow tree
const inner = para.closest('.inner');
testing.expectEqual('DIV', inner.tagName);
// closest should NOT cross shadow boundary to find host
const foundHost = para.closest('.host-class');
testing.expectEqual(null, foundHost);
host.remove();
}
</script>
<script id="nextSibling_in_shadow">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="first">First</div><div id="second">Second</div>';
document.body.appendChild(host);
const first = shadow.getElementById('first');
const second = shadow.getElementById('second');
// Sibling traversal should work within shadow tree
testing.expectEqual(second, first.nextSibling);
testing.expectEqual(first, second.previousSibling);
host.remove();
}
</script>
<script id="childNodes_of_shadow">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div>A</div><div>B</div>';
document.body.appendChild(host);
// Shadow root should expose its children
const children = shadow.childNodes;
testing.expectEqual(2, children.length);
// Host should have no child nodes (shadow tree is separate)
testing.expectEqual(0, host.childNodes.length);
host.remove();
}
</script>

View File

@@ -0,0 +1,186 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body>
<div id="container"></div>
</body>
<script id="double_attach_error">
{
const el = document.createElement('div');
el.attachShadow({ mode: 'open' });
let threw = false;
try {
el.attachShadow({ mode: 'open' });
} catch (e) {
threw = true;
}
testing.expectEqual(true, threw);
}
</script>
<script id="shadow_isolation_querySelector">
{
const host = document.createElement('div');
host.id = 'test-host';
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p id="shadow-para">In shadow</p>';
document.body.appendChild(host);
const fromDocument = document.querySelector('#shadow-para');
testing.expectEqual(null, fromDocument);
const fromShadow = shadow.querySelector('#shadow-para');
testing.expectEqual('In shadow', fromShadow.textContent);
host.remove();
}
</script>
<script id="shadow_isolation_getElementById">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="shadow-div">Shadow</div>';
document.body.appendChild(host);
const fromDocument = document.getElementById('shadow-div');
testing.expectEqual(null, fromDocument);
const fromShadow = shadow.getElementById('shadow-div');
testing.expectEqual('Shadow', fromShadow.textContent);
host.remove();
}
</script>
<script id="moving_shadow_host">
{
const container = $('#container');
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<span>Content</span>';
container.appendChild(host);
testing.expectEqual('<span>Content</span>', shadow.innerHTML);
testing.expectEqual(container, host.parentElement);
document.body.appendChild(host);
testing.expectEqual('<span>Content</span>', shadow.innerHTML);
testing.expectEqual(document.body, host.parentElement);
host.remove();
testing.expectEqual('<span>Content</span>', shadow.innerHTML);
testing.expectEqual(null, host.parentElement);
}
</script>
<script id="shadow_persists_after_disconnect">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Persistent</p>';
document.body.appendChild(host);
const shadowRef = host.shadowRoot;
host.remove();
testing.expectEqual(shadowRef, host.shadowRoot);
testing.expectEqual('<p>Persistent</p>', host.shadowRoot.innerHTML);
document.body.appendChild(host);
testing.expectEqual(shadowRef, host.shadowRoot);
testing.expectEqual('<p>Persistent</p>', host.shadowRoot.innerHTML);
host.remove();
}
</script>
<script id="invalid_mode">
{
const el = document.createElement('div');
let threw = false;
try {
el.attachShadow({ mode: 'invalid' });
} catch (e) {
threw = true;
}
testing.expectEqual(true, threw);
}
</script>
<script id="shadow_with_style">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<style>p { color: red; }</style><p>Styled</p>';
const para = shadow.querySelector('p');
testing.expectEqual('Styled', para.textContent);
const style = shadow.querySelector('style');
testing.expectEqual('p { color: red; }', style.textContent);
}
</script>
<script id="innerHTML_clears_existing">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
const p = document.createElement('p');
p.textContent = 'First';
shadow.appendChild(p);
testing.expectEqual(1, shadow.childElementCount);
shadow.innerHTML = '<span>Second</span>';
testing.expectEqual(1, shadow.childElementCount);
testing.expectEqual('SPAN', shadow.firstElementChild.tagName);
testing.expectEqual('Second', shadow.firstElementChild.textContent);
}
</script>
<script id="empty_innerHTML">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Content</p>';
testing.expectEqual(1, shadow.childElementCount);
shadow.innerHTML = '';
testing.expectEqual(0, shadow.childElementCount);
testing.expectEqual('', shadow.innerHTML);
}
</script>
<script id="querySelectorAll_isolation">
{
const host1 = document.createElement('div');
const shadow1 = host1.attachShadow({ mode: 'open' });
shadow1.innerHTML = '<p class="test">Shadow 1</p>';
const host2 = document.createElement('div');
const shadow2 = host2.attachShadow({ mode: 'open' });
shadow2.innerHTML = '<p class="test">Shadow 2</p>';
document.body.appendChild(host1);
document.body.appendChild(host2);
const fromDoc = document.querySelectorAll('.test');
testing.expectEqual(0, fromDoc.length);
const fromShadow1 = shadow1.querySelectorAll('.test');
testing.expectEqual(1, fromShadow1.length);
testing.expectEqual('Shadow 1', fromShadow1[0].textContent);
const fromShadow2 = shadow2.querySelectorAll('.test');
testing.expectEqual(1, fromShadow2.length);
testing.expectEqual('Shadow 2', fromShadow2[0].textContent);
host1.remove();
host2.remove();
}
</script>

View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="event_bubbling_through_shadow">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<button id="btn">Click</button>';
document.body.appendChild(host);
const button = shadow.getElementById('btn');
let insideCalled = false;
let shadowCalled = false;
let hostCalled = false;
let documentCalled = false;
let insideTarget = null;
let hostTarget = null;
button.addEventListener('click', (e) => {
insideCalled = true;
insideTarget = e.target;
});
shadow.addEventListener('click', (e) => {
shadowCalled = true;
});
host.addEventListener('click', (e) => {
hostCalled = true;
hostTarget = e.target;
});
document.addEventListener('click', (e) => {
documentCalled = true;
});
const event = new Event('click', { bubbles: true });
button.dispatchEvent(event);
testing.expectEqual(true, insideCalled);
testing.expectEqual(true, shadowCalled);
// Without composed:true, event should NOT escape shadow tree
testing.expectEqual(false, hostCalled);
testing.expectEqual(false, documentCalled);
host.remove();
}
</script>
<script id="event_composed_escapes_shadow">
// @ZIGDOM TODO
testing.expectEqual(true, true);
// {
// const host = document.createElement('div');
// const shadow = host.attachShadow({ mode: 'open' });
// shadow.innerHTML = '<button id="btn">Click</button>';
// document.body.appendChild(host);
// const button = shadow.getElementById('btn');
// let hostCalled = false;
// let hostTarget = null;
// host.addEventListener('click', (e) => {
// hostCalled = true;
// hostTarget = e.target;
// });
// // With composed:true, event SHOULD escape shadow tree
// const event = new Event('click', { bubbles: true, composed: true });
// button.dispatchEvent(event);
// testing.expectEqual(true, hostCalled);
// // Event target should be retargeted to host when outside shadow tree
// testing.expectEqual(host, hostTarget);
// host.remove();
// }
</script>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body>
<div id="collision-test">Document</div>
</body>
<script id="id_collision_document_first">
{
// Document tree element exists with id="collision-test"
// Create shadow tree with same ID
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="collision-test">Shadow</div>';
document.body.appendChild(host);
// document.getElementById should find the document-tree element
const fromDoc = document.getElementById('collision-test');
testing.expectEqual('Document', fromDoc.textContent);
// shadow.getElementById should find the shadow-tree element
const fromShadow = shadow.getElementById('collision-test');
testing.expectEqual('Shadow', fromShadow.textContent);
host.remove();
}
</script>
<script id="id_collision_shadow_first">
{
// Create shadow tree with ID first
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="unique-id">Shadow</div>';
document.body.appendChild(host);
// Now add document element with same ID
const docEl = document.createElement('div');
docEl.id = 'unique-id';
docEl.textContent = 'Document';
document.body.appendChild(docEl);
// document.getElementById should find the document-tree element
const fromDoc = document.getElementById('unique-id');
testing.expectEqual('Document', fromDoc.textContent);
// shadow.getElementById should find the shadow-tree element
const fromShadow = shadow.getElementById('unique-id');
testing.expectEqual('Shadow', fromShadow.textContent);
host.remove();
docEl.remove();
}
</script>

View File

@@ -0,0 +1,167 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="set_id_after_append">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
const div = document.createElement('div');
div.textContent = 'Content';
shadow.appendChild(div);
document.body.appendChild(host);
// Set ID after element is in connected shadow tree
div.id = 'dynamic-id';
const found = shadow.getElementById('dynamic-id');
testing.expectEqual('Content', found.textContent);
const fromDoc = document.getElementById('dynamic-id');
testing.expectEqual(null, fromDoc);
host.remove();
}
</script>
<script id="change_id">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="old-id">Text</div>';
document.body.appendChild(host);
const el = shadow.getElementById('old-id');
testing.expectEqual('Text', el.textContent);
// Change the ID
el.id = 'new-id';
testing.expectEqual(null, shadow.getElementById('old-id'));
const found = shadow.getElementById('new-id');
testing.expectEqual('Text', found.textContent);
host.remove();
}
</script>
<script id="remove_id">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="removable">Text</div>';
document.body.appendChild(host);
testing.expectEqual('Text', shadow.getElementById('removable').textContent);
const el = shadow.getElementById('removable');
el.removeAttribute('id');
testing.expectEqual(null, shadow.getElementById('removable'));
host.remove();
}
</script>
<script id="multiple_shadow_roots_same_id">
{
// Create three shadow roots with same ID
const host1 = document.createElement('div');
const shadow1 = host1.attachShadow({ mode: 'open' });
shadow1.innerHTML = '<div id="shared">Shadow 1</div>';
const host2 = document.createElement('div');
const shadow2 = host2.attachShadow({ mode: 'open' });
shadow2.innerHTML = '<div id="shared">Shadow 2</div>';
const host3 = document.createElement('div');
const shadow3 = host3.attachShadow({ mode: 'open' });
shadow3.innerHTML = '<div id="shared">Shadow 3</div>';
document.body.appendChild(host1);
document.body.appendChild(host2);
document.body.appendChild(host3);
// Each shadow root should find its own element
testing.expectEqual('Shadow 1', shadow1.getElementById('shared').textContent);
testing.expectEqual('Shadow 2', shadow2.getElementById('shared').textContent);
testing.expectEqual('Shadow 3', shadow3.getElementById('shared').textContent);
// querySelector should also be isolated
testing.expectEqual('Shadow 1', shadow1.querySelector('#shared').textContent);
testing.expectEqual('Shadow 2', shadow2.querySelector('#shared').textContent);
testing.expectEqual('Shadow 3', shadow3.querySelector('#shared').textContent);
// Document should not find any of them
testing.expectEqual(null, document.getElementById('shared'));
host1.remove();
host2.remove();
host3.remove();
}
</script>
<script id="multiple_shadows_with_document_collision">
{
// Create document element with ID
const docEl = document.createElement('div');
docEl.id = 'collision';
docEl.textContent = 'Document';
document.body.appendChild(docEl);
// Create two shadow roots with same ID
const host1 = document.createElement('div');
const shadow1 = host1.attachShadow({ mode: 'open' });
shadow1.innerHTML = '<div id="collision">Shadow 1</div>';
const host2 = document.createElement('div');
const shadow2 = host2.attachShadow({ mode: 'open' });
shadow2.innerHTML = '<div id="collision">Shadow 2</div>';
document.body.appendChild(host1);
document.body.appendChild(host2);
// Document should find document element
testing.expectEqual('Document', document.getElementById('collision').textContent);
testing.expectEqual('Document', document.querySelector('#collision').textContent);
// Each shadow should find its own
testing.expectEqual('Shadow 1', shadow1.getElementById('collision').textContent);
testing.expectEqual('Shadow 1', shadow1.querySelector('#collision').textContent);
testing.expectEqual('Shadow 2', shadow2.getElementById('collision').textContent);
testing.expectEqual('Shadow 2', shadow2.querySelector('#collision').textContent);
docEl.remove();
host1.remove();
host2.remove();
}
</script>
<script id="set_id_before_connected">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
const div = document.createElement('div');
div.id = 'early-id';
div.textContent = 'Content';
shadow.appendChild(div);
// Should work even before host is connected
testing.expectEqual('Content', shadow.getElementById('early-id').textContent);
document.body.appendChild(host);
// Should still work after connected
testing.expectEqual('Content', shadow.getElementById('early-id').textContent);
testing.expectEqual(null, document.getElementById('early-id'));
host.remove();
}
</script>

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="ownerDocument_in_shadow">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="child">Content</div>';
document.body.appendChild(host);
const child = shadow.getElementById('child');
// Elements in shadow tree should still reference the document
testing.expectEqual(document, child.ownerDocument);
host.remove();
}
</script>
<script id="host_children_vs_shadow">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Shadow content</p>';
// Try to add regular children to host (light DOM)
const lightChild = document.createElement('div');
lightChild.textContent = 'Light DOM';
host.appendChild(lightChild);
document.body.appendChild(host);
// Host should have light DOM children
testing.expectEqual(1, host.children.length);
testing.expectEqual('Light DOM', host.children[0].textContent);
// Shadow should have shadow DOM children
testing.expectEqual(1, shadow.children.length);
testing.expectEqual('Shadow content', shadow.children[0].textContent);
// querySelector on host should NOT find shadow children
testing.expectEqual(null, host.querySelector('p'));
// querySelector on shadow should NOT find light children
testing.expectEqual(null, shadow.querySelector('div'));
host.remove();
}
</script>
<script id="cloneNode_shadow">
{
const host = document.createElement('div');
host.id = 'original-host';
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Shadow content</p>';
document.body.appendChild(host);
// Clone the host element
const clone = host.cloneNode(true);
// Per spec, cloneNode should NOT clone the shadow root
testing.expectEqual(null, clone.shadowRoot);
testing.expectEqual(0, clone.children.length);
host.remove();
}
</script>
<script id="compareDocumentPosition_shadow">
{
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="a">A</div><div id="b">B</div>';
document.body.appendChild(host);
const a = shadow.getElementById('a');
const b = shadow.getElementById('b');
// Elements within same shadow tree should have document position relationship
const pos = a.compareDocumentPosition(b);
// DOCUMENT_POSITION_FOLLOWING = 4
testing.expectEqual(4, pos);
host.remove();
}
</script>

View File

@@ -22,15 +22,39 @@ const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const Element = @import("Element.zig");
const ShadowRoot = @import("ShadowRoot.zig");
const collections = @import("collections.zig");
const Selector = @import("selector/Selector.zig");
const DocumentFragment = @This();
_type: Type,
_proto: *Node,
pub const Type = union(enum) {
generic,
shadow_root: *ShadowRoot,
};
pub fn is(self: *DocumentFragment, comptime T: type) ?*T {
switch (self._type) {
.shadow_root => |shadow_root| {
if (T == ShadowRoot) {
return shadow_root;
}
},
.generic => {},
}
return null;
}
pub fn as(self: *DocumentFragment, comptime T: type) *T {
return self.is(T).?;
}
pub fn init(page: *Page) !*DocumentFragment {
return page._factory.node(DocumentFragment{
._type = .generic,
._proto = undefined,
});
}
@@ -136,6 +160,27 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText,
}
}
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer) !void {
const dump = @import("../dump.zig");
return dump.children(self.asNode(), .{}, writer);
}
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, page: *Page) !void {
const parent = self.asNode();
page.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
}
if (html.len == 0) {
return;
}
try page.parseHtmlAsChildren(parent, html);
}
pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node {
const fragment = try DocumentFragment.init(page);
const fragment_node = fragment.asNode();
@@ -175,6 +220,13 @@ pub const JsApi = struct {
pub const append = bridge.function(DocumentFragment.append, .{});
pub const prepend = bridge.function(DocumentFragment.prepend, .{});
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{});
pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{});
fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getInnerHTML(&buf.writer);
return buf.written();
}
};
const testing = @import("../../testing.zig");

View File

@@ -33,6 +33,7 @@ const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
pub const DOMStringMap = @import("element/DOMStringMap.zig");
const DOMRect = @import("DOMRect.zig");
const css = @import("css.zig");
const ShadowRoot = @import("ShadowRoot.zig");
pub const Svg = @import("element/Svg.zig");
pub const Html = @import("element/Html.zig");
@@ -42,6 +43,7 @@ const Element = @This();
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const Namespace = enum(u8) {
html,
@@ -311,6 +313,22 @@ fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {
};
}
pub fn getShadowRoot(self: *Element, page: *Page) ?*ShadowRoot {
const shadow_root = page._element_shadow_roots.get(self) orelse return null;
if (shadow_root._mode == .closed) return null;
return shadow_root;
}
pub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowRoot {
if (page._element_shadow_roots.get(self)) |_| {
return error.AlreadyHasShadowRoot;
}
const mode = try ShadowRoot.Mode.fromString(mode_str);
const shadow_root = try ShadowRoot.init(self, mode, page);
try page._element_shadow_roots.put(page.arena, self, shadow_root);
return shadow_root;
}
pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute {
if (attr._element) |el| {
if (el == self) {
@@ -522,6 +540,28 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select
return Selector.querySelectorAll(self.asNode(), input, page);
}
pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element {
if (selector.len == 0) {
return error.SyntaxError;
}
var current: ?*Element = self;
while (current) |el| {
if (try el.matches(selector, page)) {
return el;
}
const parent = el._proto._parent orelse break;
if (parent.is(ShadowRoot) != null) {
break;
}
current = parent.is(Element);
}
return null;
}
pub fn parentElement(self: *Element) ?*Element {
return self._proto.parentElement();
}
@@ -867,6 +907,15 @@ pub const JsApi = struct {
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
const ShadowRootInit = struct {
mode: []const u8,
};
fn _attachShadow(self: *Element, init: ShadowRootInit, page: *Page) !*ShadowRoot {
return self.attachShadow(init.mode, page);
}
pub const replaceChildren = bridge.function(Element.replaceChildren, .{});
pub const remove = bridge.function(Element.remove, .{});
pub const append = bridge.function(Element.append, .{});
@@ -879,6 +928,7 @@ pub const JsApi = struct {
pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });
pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
pub const closest = bridge.function(Element.closest, .{ .dom_exception = true });
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});

View File

@@ -33,6 +33,7 @@ pub const HTMLDocument = @import("HTMLDocument.zig");
pub const Children = @import("children.zig").Children;
pub const DocumentFragment = @import("DocumentFragment.zig");
pub const DocumentType = @import("DocumentType.zig");
pub const ShadowRoot = @import("ShadowRoot.zig");
const Allocator = std.mem.Allocator;
const LinkedList = std.DoublyLinkedList;
@@ -106,6 +107,9 @@ pub fn is(self: *Node, comptime T: type) ?*T {
if (T == DocumentFragment) {
return doc;
}
if (T == ShadowRoot) {
return doc.is(ShadowRoot);
}
},
}
return null;
@@ -191,7 +195,7 @@ pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {
.cdata => |c| c._data = try page.arena.dupe(u8, data),
.document => {},
.document_type => {},
.document_fragment => {},
.document_fragment => |frag| return frag.replaceChildren(&.{.{ .text = data }}, page),
.attribute => |attr| return attr.setValue(data, page),
}
}
@@ -224,6 +228,17 @@ pub fn nodeType(self: *const Node) u8 {
};
}
pub fn isInShadowTree(self: *Node) bool {
var node = self._parent;
while (node) |n| {
if (n.is(ShadowRoot) != null) {
return true;
}
node = n._parent;
}
return false;
}
pub fn isConnected(self: *const Node) bool {
const target = Page.current.document.asNode();
if (self == target) {

View File

@@ -0,0 +1,97 @@
// Copyright (C) 2023-2025 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 js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const DocumentFragment = @import("DocumentFragment.zig");
const Element = @import("Element.zig");
const ShadowRoot = @This();
pub const Mode = enum {
open,
closed,
pub fn fromString(str: []const u8) !Mode {
return std.meta.stringToEnum(Mode, str) orelse error.InvalidMode;
}
};
_proto: *DocumentFragment,
_mode: Mode,
_host: *Element,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
return page._factory.documentFragment(ShadowRoot{
._proto = undefined,
._mode = mode,
._host = host,
});
}
pub fn asDocumentFragment(self: *ShadowRoot) *DocumentFragment {
return self._proto;
}
pub fn asNode(self: *ShadowRoot) *Node {
return self._proto.asNode();
}
pub fn asEventTarget(self: *ShadowRoot) *@import("EventTarget.zig") {
return self.asNode().asEventTarget();
}
pub fn className(_: *const ShadowRoot) []const u8 {
return "[object ShadowRoot]";
}
pub fn getMode(self: *const ShadowRoot) []const u8 {
return @tagName(self._mode);
}
pub fn getHost(self: *const ShadowRoot) *Element {
return self._host;
}
pub fn getElementById(self: *ShadowRoot, id_: ?[]const u8) ?*Element {
const id = id_ orelse return null;
return self._elements_by_id.get(id);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(ShadowRoot);
pub const Meta = struct {
pub const name = "ShadowRoot";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});
pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});
pub const getElementById = bridge.function(ShadowRoot.getElementById, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: ShadowRoot" {
try testing.htmlRunner("shadowroot", .{});
}

View File

@@ -153,14 +153,14 @@ pub const List = struct {
}
fn _put(self: *List, result: NormalizeAndEntry, value: []const u8, element: *Element, page: *Page) !*Entry {
const is_id = isIdForConnected(result.normalized, element);
const is_id = shouldAddToIdMap(result.normalized, element);
var entry: *Entry = undefined;
var old_value: ?[]const u8 = null;
if (result.entry) |e| {
old_value = try page.call_arena.dupe(u8, e._value.str());
if (is_id) {
_ = page.document._elements_by_id.remove(e._value.str());
page.removeElementId(element, e._value.str());
}
e._value = try String.init(page.arena, value, .{});
entry = e;
@@ -174,7 +174,11 @@ pub const List = struct {
}
if (is_id) {
try page.document._elements_by_id.put(page.arena, entry._value.str(), element);
const parent = element.asNode()._parent orelse {
std.debug.assert(false);
return entry;
};
try page.addElementId(parent, element, entry._value.str());
}
page.attributeChange(element, result.normalized, entry._value.str(), old_value);
return entry;
@@ -227,11 +231,11 @@ pub const List = struct {
const result = try self.getEntryAndNormalizedName(name, page);
const entry = result.entry orelse return;
const is_id = isIdForConnected(result.normalized, element);
const is_id = shouldAddToIdMap(result.normalized, element);
const old_value = entry._value.str();
if (is_id) {
_ = page.document._elements_by_id.remove(entry._value.str());
page.removeElementId(element, entry._value.str());
}
page.attributeRemove(element, result.normalized, old_value);
@@ -312,8 +316,18 @@ pub const List = struct {
};
};
fn isIdForConnected(normalized_id: []const u8, element: *const Element) bool {
return std.mem.eql(u8, normalized_id, "id") and element.asConstNode().isConnected();
fn shouldAddToIdMap(normalized_name: []const u8, element: *Element) bool {
if (!std.mem.eql(u8, normalized_name, "id")) {
return false;
}
const node = element.asNode();
// Shadow tree elements are always added to their shadow root's map
if (node.isInShadowTree()) {
return true;
}
// Document tree elements only when connected
return node.isConnected();
}
pub fn normalizeNameForLookup(name: []const u8, page: *Page) ![]const u8 {

View File

@@ -132,7 +132,6 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {
};
}
fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
if (self._definition == null) {
return;

View File

@@ -106,7 +106,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page
const segment_index = anchor.segment_index;
// Look up the element by ID (O(1) hash map lookup)
const id_element = page.document._elements_by_id.get(id) orelse return null;
const id_element = page.getElementByIdFromNode(root, id) orelse return null;
const id_node = id_element.asNode();
if (!root.contains(id_node)) {

View File

@@ -36,7 +36,7 @@ pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Elemen
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
const first = selector.first.parts[0];
if (first == .id) {
const el = page.document._elements_by_id.get(first.id) orelse continue;
const el = page.getElementByIdFromNode(root, first.id) orelse continue;
// Check if the element is within the root subtree
if (root.contains(el.asNode())) {
return el;