diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig
index 3746abcc..6021ad62 100644
--- a/src/browser/dom/element.zig
+++ b/src/browser/dom/element.zig
@@ -558,6 +558,8 @@ pub const Element = struct {
.proto = fragment,
};
state.shadow_root = sr;
+ parser.documentFragmentSetHost(sr.proto, @alignCast(@ptrCast(self)));
+
return sr;
}
diff --git a/src/browser/dom/shadow_root.zig b/src/browser/dom/shadow_root.zig
index 7d8977df..ade9c4e9 100644
--- a/src/browser/dom/shadow_root.zig
+++ b/src/browser/dom/shadow_root.zig
@@ -17,10 +17,12 @@
// along with this program. If not, see
hello
'", null }, + .{ "sr1.innerHTML", "hello
" }, + .{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" }, + + .{ "sr1.innerHTML = null", null }, + .{ "sr1.innerHTML", "" }, + .{ "sr1.querySelector('*')", "null" }, + }, .{}); } diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index 4a524be0..8d16be9e 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -24,6 +24,7 @@ const parser = @import("../netsurf.zig"); const generate = @import("../../runtime/generate.zig"); const Page = @import("../page.zig").Page; +const Node = @import("../dom/node.zig").Node; const DOMException = @import("../dom/exceptions.zig").DOMException; const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTargetUnion = @import("../dom/event_target.zig").Union; @@ -139,6 +140,64 @@ pub const Event = struct { pub fn _preventDefault(self: *parser.Event) !void { return try parser.eventPreventDefault(self); } + + pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion { + const et_ = try parser.eventTarget(self); + const et = et_ orelse return &.{}; + + var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) { + .libdom_node => @as(*parser.Node, @ptrCast(et)), + .plain => parser.eventTargetToNode(et), + else => { + // Window, XHR, MessagePort, etc...no path beyond the event itself + return &.{try EventTarget.toInterface(et, page)}; + }, + }; + + const arena = page.call_arena; + var path: std.ArrayListUnmanaged(EventTargetUnion) = .empty; + while (node) |n| { + try path.append(arena, .{ + .node = try Node.toInterface(n), + }); + + node = try parser.nodeParentNode(n); + if (node == null and try parser.nodeType(n) == .document_fragment) { + // we have a non-continuous hook from a shadowroot to its host ( + // it's parent element). libdom doesn't really support ShdowRoots + // and, for the most part, that works out well since it naturally + // provides isolation. But events don't follow the same + // shadowroot isolation as most other things, so, if this is + // a parent-less document fragment, we need to check if it has + // a host. + if (parser.documentFragmentGetHost(@ptrCast(n))) |host| { + node = host; + + // If a document fragment has a host, then that host + // _has_ to have a state and that state _has_ to have + // a shadow_root field. All of this is set in Element._attachShadow + if (page.getNodeState(host).?.shadow_root.?.mode == .closed) { + // if the shadow root is closed, then the composedPath + // starts at the host element. + path.clearRetainingCapacity(); + } + } else { + // Our document fragement has no parent and no host, we + // can break out of the loop. + break; + } + } + } + + if (path.getLastOrNull()) |last| { + // the Window isn't part of the DOM hierarchy, but for events, it + // is, so we need to glue it on. + if (last.node == .HTMLDocument and last.node.HTMLDocument == page.window.document) { + try path.append(arena, .{ .node = .{ .Window = &page.window } }); + } + } + return path.items; + } }; pub const EventHandler = struct { @@ -446,4 +505,37 @@ test "Browser.Event" { .{ "nb", "2" }, .{ "document.removeEventListener('count', cbk)", "undefined" }, }, .{}); + + try runner.testCases(&.{ + .{ "new Event('').composedPath()", "" }, + .{ + \\ let div1 = document.createElement('div'); + \\ let sr1 = div1.attachShadow({mode: 'open'}); + \\ sr1.innerHTML = ""; + \\ document.getElementsByTagName('body')[0].appendChild(div1); + \\ let cp = null; + \\ div1.addEventListener('click', (e) => { + \\ cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString()); + \\ }); + \\ sr1.getElementById('srp1').click(); + \\ cp.join(', '); + , + "srp1, #document-fragment, DIV, BODY, HTML, #document, [object Window]", + }, + + .{ + \\ let div2 = document.createElement('div'); + \\ let sr2 = div2.attachShadow({mode: 'closed'}); + \\ sr2.innerHTML = ""; + \\ document.getElementsByTagName('body')[0].appendChild(div2); + \\ cp = null; + \\ div2.addEventListener('click', (e) => { + \\ cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString()); + \\ }); + \\ sr2.getElementById('srp2').click(); + \\ cp.join(', '); + , + "DIV, BODY, HTML, #document, [object Window]", + }, + }, .{}); } diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 068df34d..73e9a1ab 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -1993,6 +1993,15 @@ pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node { return @as(*Node, @alignCast(@ptrCast(doc))); } +pub fn documentFragmentGetHost(frag: *DocumentFragment) ?*Node { + var node: ?*NodeExternal = undefined; + c._dom_document_fragment_get_host(frag, &node); + return if (node) |n| @ptrCast(n) else null; +} +pub fn documentFragmentSetHost(frag: *DocumentFragment, host: *Node) void { + c._dom_document_fragment_set_host(frag, host); +} + // Document Position pub const DocumentPosition = enum(u32) { diff --git a/vendor/netsurf/libdom b/vendor/netsurf/libdom index 0c590b26..c0df4581 160000 --- a/vendor/netsurf/libdom +++ b/vendor/netsurf/libdom @@ -1 +1 @@ -Subproject commit 0c590b265a65b937042d68ad34902c9b4a05839a +Subproject commit c0df458132162aba136d57ce1ba2179122a9e717