mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-17 00:38:59 +00:00
ShadowRoot
This commit is contained in:
@@ -121,6 +121,16 @@ pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|||||||
return child_ptr;
|
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) {
|
pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
const child_ptr = try self.createT(@TypeOf(child));
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
child_ptr.* = child;
|
child_ptr.* = child;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ const Element = @import("webapi/Element.zig");
|
|||||||
const Window = @import("webapi/Window.zig");
|
const Window = @import("webapi/Window.zig");
|
||||||
const Location = @import("webapi/Location.zig");
|
const Location = @import("webapi/Location.zig");
|
||||||
const Document = @import("webapi/Document.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 Performance = @import("webapi/Performance.zig");
|
||||||
const HtmlScript = @import("webapi/Element.zig").Html.Script;
|
const HtmlScript = @import("webapi/Element.zig").Html.Script;
|
||||||
const MutationObserver = @import("webapi/MutationObserver.zig");
|
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_styles: Element.StyleLookup = .{},
|
||||||
_element_datasets: Element.DatasetLookup = .{},
|
_element_datasets: Element.DatasetLookup = .{},
|
||||||
_element_class_lists: Element.ClassListLookup = .{},
|
_element_class_lists: Element.ClassListLookup = .{},
|
||||||
|
_element_shadow_roots: Element.ShadowRootLookup = .{},
|
||||||
|
|
||||||
_script_manager: ScriptManager,
|
_script_manager: ScriptManager,
|
||||||
|
|
||||||
@@ -211,6 +214,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
|||||||
self._element_styles = .{};
|
self._element_styles = .{};
|
||||||
self._element_datasets = .{};
|
self._element_datasets = .{};
|
||||||
self._element_class_lists = .{};
|
self._element_class_lists = .{};
|
||||||
|
self._element_shadow_roots = .{};
|
||||||
self._notified_network_idle = .init;
|
self._notified_network_idle = .init;
|
||||||
self._notified_network_almost_idle = .init;
|
self._notified_network_almost_idle = .init;
|
||||||
|
|
||||||
@@ -714,6 +718,39 @@ pub fn domChanged(self: *Page) void {
|
|||||||
self.version += 1;
|
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 {
|
pub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void {
|
||||||
try self._mutation_observers.append(self.arena, observer);
|
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"
|
// 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
|
// it and all of its descendants. For now "disconnect" just means updating
|
||||||
// document._elements_by_id and invoking disconnectedCallback for custom elements
|
// the ID map and invoking disconnectedCallback for custom elements
|
||||||
var elements_by_id = &self.document._elements_by_id;
|
|
||||||
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
|
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
|
||||||
while (tw.next()) |el| {
|
while (tw.next()) |el| {
|
||||||
if (el.getAttributeSafe("id")) |id| {
|
if (el.getAttributeSafe("id")) |id| {
|
||||||
_ = elements_by_id.remove(id);
|
self.removeElementId(el, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
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 (comptime from_parser) {
|
||||||
if (child.is(Element)) |el| {
|
if (child.is(Element)) |el| {
|
||||||
if (el.getAttributeSafe("id")) |id| {
|
if (el.getAttributeSafe("id")) |id| {
|
||||||
const gop = try document_by_id.getOrPut(self.arena, id);
|
try self.addElementId(parent, el, id);
|
||||||
if (!gop.found_existing) {
|
|
||||||
gop.value_ptr.* = el;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1446,24 +1477,27 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent.isConnected() == false) {
|
const parent_in_shadow = parent.is(ShadowRoot) != null or parent.isInShadowTree();
|
||||||
// The parent isn't connected, we don't have to connect the child
|
const parent_is_connected = parent.isConnected();
|
||||||
|
|
||||||
|
if (!parent_in_shadow and !parent_is_connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're here, it means that a disconnected child became connected. We
|
// If we're here, it means either:
|
||||||
// need to connect it (and all of its descendants)
|
// 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, .{});
|
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
|
||||||
while (tw.next()) |el| {
|
while (tw.next()) |el| {
|
||||||
if (el.getAttributeSafe("id")) |id| {
|
if (el.getAttributeSafe("id")) |id| {
|
||||||
const gop = try document_by_id.getOrPut(self.arena, id);
|
try self.addElementId(el.asNode()._parent.?, el, id);
|
||||||
if (!gop.found_existing) {
|
|
||||||
gop.value_ptr.* = el;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/KeyValueList.zig"),
|
@import("../webapi/KeyValueList.zig"),
|
||||||
@import("../webapi/DocumentFragment.zig"),
|
@import("../webapi/DocumentFragment.zig"),
|
||||||
@import("../webapi/DocumentType.zig"),
|
@import("../webapi/DocumentType.zig"),
|
||||||
|
@import("../webapi/ShadowRoot.zig"),
|
||||||
@import("../webapi/DOMException.zig"),
|
@import("../webapi/DOMException.zig"),
|
||||||
@import("../webapi/DOMImplementation.zig"),
|
@import("../webapi/DOMImplementation.zig"),
|
||||||
@import("../webapi/DOMTreeWalker.zig"),
|
@import("../webapi/DOMTreeWalker.zig"),
|
||||||
|
|||||||
94
src/browser/tests/element/closest.html
Normal file
94
src/browser/tests/element/closest.html
Normal 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>
|
||||||
156
src/browser/tests/shadowroot/basic.html
Normal file
156
src/browser/tests/shadowroot/basic.html
Normal 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>
|
||||||
138
src/browser/tests/shadowroot/custom_elements.html
Normal file
138
src/browser/tests/shadowroot/custom_elements.html
Normal 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>
|
||||||
142
src/browser/tests/shadowroot/dom_traversal.html
Normal file
142
src/browser/tests/shadowroot/dom_traversal.html
Normal 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>
|
||||||
186
src/browser/tests/shadowroot/edge_cases.html
Normal file
186
src/browser/tests/shadowroot/edge_cases.html
Normal 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>
|
||||||
85
src/browser/tests/shadowroot/events.html
Normal file
85
src/browser/tests/shadowroot/events.html
Normal 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>
|
||||||
55
src/browser/tests/shadowroot/id_collision.html
Normal file
55
src/browser/tests/shadowroot/id_collision.html
Normal 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>
|
||||||
167
src/browser/tests/shadowroot/id_management.html
Normal file
167
src/browser/tests/shadowroot/id_management.html
Normal 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>
|
||||||
92
src/browser/tests/shadowroot/scoping.html
Normal file
92
src/browser/tests/shadowroot/scoping.html
Normal 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>
|
||||||
@@ -22,15 +22,39 @@ const js = @import("../js/js.zig");
|
|||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const Node = @import("Node.zig");
|
const Node = @import("Node.zig");
|
||||||
const Element = @import("Element.zig");
|
const Element = @import("Element.zig");
|
||||||
|
const ShadowRoot = @import("ShadowRoot.zig");
|
||||||
const collections = @import("collections.zig");
|
const collections = @import("collections.zig");
|
||||||
const Selector = @import("selector/Selector.zig");
|
const Selector = @import("selector/Selector.zig");
|
||||||
|
|
||||||
const DocumentFragment = @This();
|
const DocumentFragment = @This();
|
||||||
|
|
||||||
|
_type: Type,
|
||||||
_proto: *Node,
|
_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 {
|
pub fn init(page: *Page) !*DocumentFragment {
|
||||||
return page._factory.node(DocumentFragment{
|
return page._factory.node(DocumentFragment{
|
||||||
|
._type = .generic,
|
||||||
._proto = undefined,
|
._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 {
|
pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node {
|
||||||
const fragment = try DocumentFragment.init(page);
|
const fragment = try DocumentFragment.init(page);
|
||||||
const fragment_node = fragment.asNode();
|
const fragment_node = fragment.asNode();
|
||||||
@@ -175,6 +220,13 @@ pub const JsApi = struct {
|
|||||||
pub const append = bridge.function(DocumentFragment.append, .{});
|
pub const append = bridge.function(DocumentFragment.append, .{});
|
||||||
pub const prepend = bridge.function(DocumentFragment.prepend, .{});
|
pub const prepend = bridge.function(DocumentFragment.prepend, .{});
|
||||||
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{});
|
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");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
|
|||||||
pub const DOMStringMap = @import("element/DOMStringMap.zig");
|
pub const DOMStringMap = @import("element/DOMStringMap.zig");
|
||||||
const DOMRect = @import("DOMRect.zig");
|
const DOMRect = @import("DOMRect.zig");
|
||||||
const css = @import("css.zig");
|
const css = @import("css.zig");
|
||||||
|
const ShadowRoot = @import("ShadowRoot.zig");
|
||||||
|
|
||||||
pub const Svg = @import("element/Svg.zig");
|
pub const Svg = @import("element/Svg.zig");
|
||||||
pub const Html = @import("element/Html.zig");
|
pub const Html = @import("element/Html.zig");
|
||||||
@@ -42,6 +43,7 @@ const Element = @This();
|
|||||||
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
|
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
|
||||||
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
|
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
|
||||||
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
|
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
|
||||||
|
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
|
||||||
|
|
||||||
pub const Namespace = enum(u8) {
|
pub const Namespace = enum(u8) {
|
||||||
html,
|
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 {
|
pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute {
|
||||||
if (attr._element) |el| {
|
if (attr._element) |el| {
|
||||||
if (el == self) {
|
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);
|
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 {
|
pub fn parentElement(self: *Element) ?*Element {
|
||||||
return self._proto.parentElement();
|
return self._proto.parentElement();
|
||||||
}
|
}
|
||||||
@@ -867,6 +907,15 @@ pub const JsApi = struct {
|
|||||||
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
|
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
|
||||||
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
|
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
|
||||||
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
|
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 replaceChildren = bridge.function(Element.replaceChildren, .{});
|
||||||
pub const remove = bridge.function(Element.remove, .{});
|
pub const remove = bridge.function(Element.remove, .{});
|
||||||
pub const append = bridge.function(Element.append, .{});
|
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 matches = bridge.function(Element.matches, .{ .dom_exception = true });
|
||||||
pub const querySelector = bridge.function(Element.querySelector, .{ .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 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 checkVisibility = bridge.function(Element.checkVisibility, .{});
|
||||||
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
|
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
|
||||||
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
|
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub const HTMLDocument = @import("HTMLDocument.zig");
|
|||||||
pub const Children = @import("children.zig").Children;
|
pub const Children = @import("children.zig").Children;
|
||||||
pub const DocumentFragment = @import("DocumentFragment.zig");
|
pub const DocumentFragment = @import("DocumentFragment.zig");
|
||||||
pub const DocumentType = @import("DocumentType.zig");
|
pub const DocumentType = @import("DocumentType.zig");
|
||||||
|
pub const ShadowRoot = @import("ShadowRoot.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const LinkedList = std.DoublyLinkedList;
|
const LinkedList = std.DoublyLinkedList;
|
||||||
@@ -106,6 +107,9 @@ pub fn is(self: *Node, comptime T: type) ?*T {
|
|||||||
if (T == DocumentFragment) {
|
if (T == DocumentFragment) {
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
if (T == ShadowRoot) {
|
||||||
|
return doc.is(ShadowRoot);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return null;
|
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),
|
.cdata => |c| c._data = try page.arena.dupe(u8, data),
|
||||||
.document => {},
|
.document => {},
|
||||||
.document_type => {},
|
.document_type => {},
|
||||||
.document_fragment => {},
|
.document_fragment => |frag| return frag.replaceChildren(&.{.{ .text = data }}, page),
|
||||||
.attribute => |attr| return attr.setValue(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 {
|
pub fn isConnected(self: *const Node) bool {
|
||||||
const target = Page.current.document.asNode();
|
const target = Page.current.document.asNode();
|
||||||
if (self == target) {
|
if (self == target) {
|
||||||
|
|||||||
97
src/browser/webapi/ShadowRoot.zig
Normal file
97
src/browser/webapi/ShadowRoot.zig
Normal 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", .{});
|
||||||
|
}
|
||||||
@@ -153,14 +153,14 @@ pub const List = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn _put(self: *List, result: NormalizeAndEntry, value: []const u8, element: *Element, page: *Page) !*Entry {
|
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 entry: *Entry = undefined;
|
||||||
var old_value: ?[]const u8 = null;
|
var old_value: ?[]const u8 = null;
|
||||||
if (result.entry) |e| {
|
if (result.entry) |e| {
|
||||||
old_value = try page.call_arena.dupe(u8, e._value.str());
|
old_value = try page.call_arena.dupe(u8, e._value.str());
|
||||||
if (is_id) {
|
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, .{});
|
e._value = try String.init(page.arena, value, .{});
|
||||||
entry = e;
|
entry = e;
|
||||||
@@ -174,7 +174,11 @@ pub const List = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_id) {
|
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);
|
page.attributeChange(element, result.normalized, entry._value.str(), old_value);
|
||||||
return entry;
|
return entry;
|
||||||
@@ -227,11 +231,11 @@ pub const List = struct {
|
|||||||
const result = try self.getEntryAndNormalizedName(name, page);
|
const result = try self.getEntryAndNormalizedName(name, page);
|
||||||
const entry = result.entry orelse return;
|
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();
|
const old_value = entry._value.str();
|
||||||
|
|
||||||
if (is_id) {
|
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);
|
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 {
|
fn shouldAddToIdMap(normalized_name: []const u8, element: *Element) bool {
|
||||||
return std.mem.eql(u8, normalized_id, "id") and element.asConstNode().isConnected();
|
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 {
|
pub fn normalizeNameForLookup(name: []const u8, page: *Page) ![]const u8 {
|
||||||
|
|||||||
@@ -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 {
|
fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
|
||||||
if (self._definition == null) {
|
if (self._definition == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ fn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page
|
|||||||
const segment_index = anchor.segment_index;
|
const segment_index = anchor.segment_index;
|
||||||
|
|
||||||
// Look up the element by ID (O(1) hash map lookup)
|
// 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();
|
const id_node = id_element.asNode();
|
||||||
|
|
||||||
if (!root.contains(id_node)) {
|
if (!root.contains(id_node)) {
|
||||||
|
|||||||
@@ -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) {
|
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
|
||||||
const first = selector.first.parts[0];
|
const first = selector.first.parts[0];
|
||||||
if (first == .id) {
|
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
|
// Check if the element is within the root subtree
|
||||||
if (root.contains(el.asNode())) {
|
if (root.contains(el.asNode())) {
|
||||||
return el;
|
return el;
|
||||||
|
|||||||
Reference in New Issue
Block a user