Skip to content

Commit 4b1eb27

Browse files
committed
Add ShadowRoot get/set innerHTML
Adds event.composedPath() This depends on lightpanda-io/libdom#34
1 parent 6a2dd11 commit 4b1eb27

File tree

5 files changed

+154
-4
lines changed

5 files changed

+154
-4
lines changed

src/browser/dom/element.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,8 @@ pub const Element = struct {
558558
.proto = fragment,
559559
};
560560
state.shadow_root = sr;
561+
parser.documentFragmentSetHost(sr.proto, @alignCast(@ptrCast(self)));
562+
561563
return sr;
562564
}
563565

src/browser/dom/shadow_root.zig

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
const std = @import("std");
20+
const dump = @import("../dump.zig");
2021
const parser = @import("../netsurf.zig");
2122

2223
const Env = @import("../env.zig").Env;
2324
const Page = @import("../page.zig").Page;
25+
const Node = @import("node.zig").Node;
2426
const Element = @import("element.zig").Element;
2527
const ElementUnion = @import("element.zig").Union;
2628

@@ -56,13 +58,48 @@ pub const ShadowRoot = struct {
5658
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
5759
self.adopted_style_sheets = try sheets.persist();
5860
}
61+
62+
pub fn get_innerHTML(self: *ShadowRoot, page: *Page) ![]const u8 {
63+
var buf = std.ArrayList(u8).init(page.call_arena);
64+
try dump.writeChildren(parser.documentFragmentToNode(self.proto), .{}, buf.writer());
65+
return buf.items;
66+
}
67+
68+
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
69+
const sr_doc = parser.documentFragmentToNode(self.proto);
70+
const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
71+
try Node.removeChildren(sr_doc);
72+
const str = str_ orelse return;
73+
74+
const fragment = try parser.documentParseFragmentFromStr(doc, str);
75+
const fragment_node = parser.documentFragmentToNode(fragment);
76+
77+
// Element.set_innerHTML also has some weirdness here. It isn't clear
78+
// what should and shouldn't be set. Whatever string you pass to libdom,
79+
// it always creates a full HTML document, with an html, head and body
80+
// element.
81+
// For ShadowRoot, it appears the only the children within the body should
82+
// be set.
83+
const html = try parser.nodeFirstChild(fragment_node) orelse return;
84+
const head = try parser.nodeFirstChild(html) orelse return;
85+
const body = try parser.nodeNextSibling(head) orelse return;
86+
87+
const children = try parser.nodeGetChildNodes(body);
88+
const ln = try parser.nodeListLength(children);
89+
for (0..ln) |_| {
90+
// always index 0, because nodeAppendChild moves the node out of
91+
// the nodeList and into the new tree
92+
const child = try parser.nodeListItem(children, 0) orelse continue;
93+
_ = try parser.nodeAppendChild(sr_doc, child);
94+
}
95+
}
5996
};
6097

6198
const testing = @import("../../testing.zig");
6299
test "Browser.DOM.ShadowRoot" {
63100
defer testing.reset();
64101

65-
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
102+
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
66103
\\ <div id=conflict>nope</div>
67104
});
68105
defer runner.deinit();
@@ -94,8 +131,8 @@ test "Browser.DOM.ShadowRoot" {
94131
try runner.testCases(&.{
95132
.{ "sr2.getElementById('conflict')", "null" },
96133
.{ "const n1 = document.createElement('div')", null },
97-
.{ "n1.id = 'conflict'", null},
98-
.{ "sr2.append(n1)", null},
134+
.{ "n1.id = 'conflict'", null },
135+
.{ "sr2.append(n1)", null },
99136
.{ "sr2.getElementById('conflict') == n1", "true" },
100137
}, .{});
101138

@@ -105,4 +142,14 @@ test "Browser.DOM.ShadowRoot" {
105142
.{ "acss.push(new CSSStyleSheet())", null },
106143
.{ "sr2.adoptedStyleSheets.length", "1" },
107144
}, .{});
145+
146+
try runner.testCases(&.{
147+
.{ "sr1.innerHTML = '<p>hello</p>'", null },
148+
.{ "sr1.innerHTML", "<p>hello</p>" },
149+
.{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" },
150+
151+
.{ "sr1.innerHTML = null", null },
152+
.{ "sr1.innerHTML", "" },
153+
.{ "sr1.querySelector('*')", "null" },
154+
}, .{});
108155
}

src/browser/events/event.zig

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const parser = @import("../netsurf.zig");
2424
const generate = @import("../../runtime/generate.zig");
2525

2626
const Page = @import("../page.zig").Page;
27+
const Node = @import("../dom/node.zig").Node;
2728
const DOMException = @import("../dom/exceptions.zig").DOMException;
2829
const EventTarget = @import("../dom/event_target.zig").EventTarget;
2930
const 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

144203
pub 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
}

src/browser/netsurf.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,15 @@ pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node {
19931993
return @as(*Node, @alignCast(@ptrCast(doc)));
19941994
}
19951995

1996+
pub fn documentFragmentGetHost(frag: *DocumentFragment) ?*Node {
1997+
var node: ?*NodeExternal = undefined;
1998+
c._dom_document_fragment_get_host(frag, &node);
1999+
return if (node) |n| @ptrCast(n) else null;
2000+
}
2001+
pub fn documentFragmentSetHost(frag: *DocumentFragment, host: *Node) void {
2002+
c._dom_document_fragment_set_host(frag, host);
2003+
}
2004+
19962005
// Document Position
19972006

19982007
pub const DocumentPosition = enum(u32) {

0 commit comments

Comments
 (0)