Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/browser/dom/element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,8 @@ pub const Element = struct {
.proto = fragment,
};
state.shadow_root = sr;
parser.documentFragmentSetHost(sr.proto, @alignCast(@ptrCast(self)));

return sr;
}

Expand Down
53 changes: 50 additions & 3 deletions src/browser/dom/shadow_root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const dump = @import("../dump.zig");
const parser = @import("../netsurf.zig");

const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;

Expand Down Expand Up @@ -56,13 +58,48 @@ pub const ShadowRoot = struct {
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
self.adopted_style_sheets = try sheets.persist();
}

pub fn get_innerHTML(self: *ShadowRoot, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.call_arena);
try dump.writeChildren(parser.documentFragmentToNode(self.proto), .{}, buf.writer());
return buf.items;
}

pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
const sr_doc = parser.documentFragmentToNode(self.proto);
const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
try Node.removeChildren(sr_doc);
const str = str_ orelse return;

const fragment = try parser.documentParseFragmentFromStr(doc, str);
const fragment_node = parser.documentFragmentToNode(fragment);

// Element.set_innerHTML also has some weirdness here. It isn't clear
// what should and shouldn't be set. Whatever string you pass to libdom,
// it always creates a full HTML document, with an html, head and body
// element.
// For ShadowRoot, it appears the only the children within the body should
// be set.
const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = try parser.nodeFirstChild(html) orelse return;
const body = try parser.nodeNextSibling(head) orelse return;

const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(sr_doc, child);
}
}
};

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

var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <div id=conflict>nope</div>
});
defer runner.deinit();
Expand Down Expand Up @@ -94,8 +131,8 @@ test "Browser.DOM.ShadowRoot" {
try runner.testCases(&.{
.{ "sr2.getElementById('conflict')", "null" },
.{ "const n1 = document.createElement('div')", null },
.{ "n1.id = 'conflict'", null},
.{ "sr2.append(n1)", null},
.{ "n1.id = 'conflict'", null },
.{ "sr2.append(n1)", null },
.{ "sr2.getElementById('conflict') == n1", "true" },
}, .{});

Expand All @@ -105,4 +142,14 @@ test "Browser.DOM.ShadowRoot" {
.{ "acss.push(new CSSStyleSheet())", null },
.{ "sr2.adoptedStyleSheets.length", "1" },
}, .{});

try runner.testCases(&.{
.{ "sr1.innerHTML = '<p>hello</p>'", null },
.{ "sr1.innerHTML", "<p>hello</p>" },
.{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" },

.{ "sr1.innerHTML = null", null },
.{ "sr1.innerHTML", "" },
.{ "sr1.querySelector('*')", "null" },
}, .{});
}
92 changes: 92 additions & 0 deletions src/browser/events/event.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = "<p id=srp1></p>";
\\ 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 = "<p id=srp2></p>";
\\ 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]",
},
}, .{});
}
9 changes: 9 additions & 0 deletions src/browser/netsurf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down