@@ -24,6 +24,7 @@ const parser = @import("../netsurf.zig");
2424const generate = @import ("../../runtime/generate.zig" );
2525
2626const Page = @import ("../page.zig" ).Page ;
27+ const Node = @import ("../dom/node.zig" ).Node ;
2728const DOMException = @import ("../dom/exceptions.zig" ).DOMException ;
2829const EventTarget = @import ("../dom/event_target.zig" ).EventTarget ;
2930const EventTargetUnion = @import ("../dom/event_target.zig" ).Union ;
@@ -139,6 +140,64 @@ pub const Event = struct {
139140 pub fn _preventDefault (self : * parser.Event ) ! void {
140141 return try parser .eventPreventDefault (self );
141142 }
143+
144+ pub fn _composedPath (self : * parser.Event , page : * Page ) ! []const EventTargetUnion {
145+ const et_ = try parser .eventTarget (self );
146+ const et = et_ orelse return &.{};
147+
148+ var node : ? * parser.Node = switch (try parser .eventTargetInternalType (et )) {
149+ .libdom_node = > @as (* parser .Node , @ptrCast (et )),
150+ .plain = > parser .eventTargetToNode (et ),
151+ else = > {
152+ // Window, XHR, MessagePort, etc...no path beyond the event itself
153+ return &.{try EventTarget .toInterface (et , page )};
154+ },
155+ };
156+
157+ const arena = page .call_arena ;
158+ var path : std .ArrayListUnmanaged (EventTargetUnion ) = .empty ;
159+ while (node ) | n | {
160+ try path .append (arena , .{
161+ .node = try Node .toInterface (n ),
162+ });
163+
164+ node = try parser .nodeParentNode (n );
165+ if (node == null and try parser .nodeType (n ) == .document_fragment ) {
166+ // we have a non-continuous hook from a shadowroot to its host (
167+ // it's parent element). libdom doesn't really support ShdowRoots
168+ // and, for the most part, that works out well since it naturally
169+ // provides isolation. But events don't follow the same
170+ // shadowroot isolation as most other things, so, if this is
171+ // a parent-less document fragment, we need to check if it has
172+ // a host.
173+ if (parser .documentFragmentGetHost (@ptrCast (n ))) | host | {
174+ node = host ;
175+
176+ // If a document fragment has a host, then that host
177+ // _has_ to have a state and that state _has_ to have
178+ // a shadow_root field. All of this is set in Element._attachShadow
179+ if (page .getNodeState (host ).? .shadow_root .? .mode == .closed ) {
180+ // if the shadow root is closed, then the composedPath
181+ // starts at the host element.
182+ path .clearRetainingCapacity ();
183+ }
184+ } else {
185+ // Our document fragement has no parent and no host, we
186+ // can break out of the loop.
187+ break ;
188+ }
189+ }
190+ }
191+
192+ if (path .getLastOrNull ()) | last | {
193+ // the Window isn't part of the DOM hierarchy, but for events, it
194+ // is, so we need to glue it on.
195+ if (last .node == .HTMLDocument and last .node .HTMLDocument == page .window .document ) {
196+ try path .append (arena , .{ .node = .{ .Window = & page .window } });
197+ }
198+ }
199+ return path .items ;
200+ }
142201};
143202
144203pub const EventHandler = struct {
@@ -446,4 +505,37 @@ test "Browser.Event" {
446505 .{ "nb" , "2" },
447506 .{ "document.removeEventListener('count', cbk)" , "undefined" },
448507 }, .{});
508+
509+ try runner .testCases (&.{
510+ .{ "new Event('').composedPath()" , "" },
511+ .{
512+ \\ let div1 = document.createElement('div');
513+ \\ let sr1 = div1.attachShadow({mode: 'open'});
514+ \\ sr1.innerHTML = "<p id=srp1></p>";
515+ \\ document.getElementsByTagName('body')[0].appendChild(div1);
516+ \\ let cp = null;
517+ \\ div1.addEventListener('click', (e) => {
518+ \\ cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString());
519+ \\ });
520+ \\ sr1.getElementById('srp1').click();
521+ \\ cp.join(', ');
522+ ,
523+ "srp1, #document-fragment, DIV, BODY, HTML, #document, [object Window]" ,
524+ },
525+
526+ .{
527+ \\ let div2 = document.createElement('div');
528+ \\ let sr2 = div2.attachShadow({mode: 'closed'});
529+ \\ sr2.innerHTML = "<p id=srp2></p>";
530+ \\ document.getElementsByTagName('body')[0].appendChild(div2);
531+ \\ cp = null;
532+ \\ div2.addEventListener('click', (e) => {
533+ \\ cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString());
534+ \\ });
535+ \\ sr2.getElementById('srp2').click();
536+ \\ cp.join(', ');
537+ ,
538+ "DIV, BODY, HTML, #document, [object Window]" ,
539+ },
540+ }, .{});
449541}
0 commit comments