diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig
index d4f7428b..89f76f42 100644
--- a/src/browser/Factory.zig
+++ b/src/browser/Factory.zig
@@ -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;
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 33e1e565..079e4006 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -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);
+ }
}
}
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index bd75bee8..984d68c9 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -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"),
diff --git a/src/browser/tests/element/closest.html b/src/browser/tests/element/closest.html
new file mode 100644
index 00000000..65c1a373
--- /dev/null
+++ b/src/browser/tests/element/closest.html
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/basic.html b/src/browser/tests/shadowroot/basic.html
new file mode 100644
index 00000000..ed82ab18
--- /dev/null
+++ b/src/browser/tests/shadowroot/basic.html
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/custom_elements.html b/src/browser/tests/shadowroot/custom_elements.html
new file mode 100644
index 00000000..32a7ee31
--- /dev/null
+++ b/src/browser/tests/shadowroot/custom_elements.html
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/dom_traversal.html b/src/browser/tests/shadowroot/dom_traversal.html
new file mode 100644
index 00000000..a1b7c7c7
--- /dev/null
+++ b/src/browser/tests/shadowroot/dom_traversal.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/edge_cases.html b/src/browser/tests/shadowroot/edge_cases.html
new file mode 100644
index 00000000..3412e110
--- /dev/null
+++ b/src/browser/tests/shadowroot/edge_cases.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/events.html b/src/browser/tests/shadowroot/events.html
new file mode 100644
index 00000000..46285cb2
--- /dev/null
+++ b/src/browser/tests/shadowroot/events.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/id_collision.html b/src/browser/tests/shadowroot/id_collision.html
new file mode 100644
index 00000000..d8389249
--- /dev/null
+++ b/src/browser/tests/shadowroot/id_collision.html
@@ -0,0 +1,55 @@
+
+
+
+Document
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/id_management.html b/src/browser/tests/shadowroot/id_management.html
new file mode 100644
index 00000000..0d498d3e
--- /dev/null
+++ b/src/browser/tests/shadowroot/id_management.html
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/shadowroot/scoping.html b/src/browser/tests/shadowroot/scoping.html
new file mode 100644
index 00000000..37caaeb1
--- /dev/null
+++ b/src/browser/tests/shadowroot/scoping.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig
index 9879f766..f7b20878 100644
--- a/src/browser/webapi/DocumentFragment.zig
+++ b/src/browser/webapi/DocumentFragment.zig
@@ -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");
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 7b727a21..6f5f15aa 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -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, .{});
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index 73c59476..9ae1ec48 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -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) {
diff --git a/src/browser/webapi/ShadowRoot.zig b/src/browser/webapi/ShadowRoot.zig
new file mode 100644
index 00000000..9fb11c07
--- /dev/null
+++ b/src/browser/webapi/ShadowRoot.zig
@@ -0,0 +1,97 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+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", .{});
+}
diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig
index c07c68dd..46e5705e 100644
--- a/src/browser/webapi/element/Attribute.zig
+++ b/src/browser/webapi/element/Attribute.zig
@@ -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 {
diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig
index abf8a341..31e46bcf 100644
--- a/src/browser/webapi/element/html/Custom.zig
+++ b/src/browser/webapi/element/html/Custom.zig
@@ -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;
diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig
index 449fc70b..8f91d8ec 100644
--- a/src/browser/webapi/selector/List.zig
+++ b/src/browser/webapi/selector/List.zig
@@ -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)) {
diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig
index 3f72c442..bed15801 100644
--- a/src/browser/webapi/selector/Selector.zig
+++ b/src/browser/webapi/selector/Selector.zig
@@ -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;