Skip to content

Commit 9501829

Browse files
committed
Start working on HTMLSlotElement
1 parent bc82023 commit 9501829

File tree

6 files changed

+197
-3
lines changed

6 files changed

+197
-3
lines changed

src/browser/dom/element.zig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,17 @@ pub const Element = struct {
560560
state.shadow_root = sr;
561561
parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self)));
562562

563+
// Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's
564+
// parent. When we render, we go top-down, so we'll have the element, get
565+
// its shadowroot, and go on. that's what the above code does.
566+
// But we sometimes need to go bottom-up, e.g when we have a slot element
567+
// and want to find the containing parent. Unforatunately , we don't have
568+
// that link, so we need to create it. In the DOM, the ShadowRoot is
569+
// represented by this DocumentFragment (it's the ShadowRoot's base prototype)
570+
// So we can also store the ShadowRoot in the DocumentFragment's state.
571+
const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment)));
572+
fragment_state.shadow_root = sr;
573+
563574
return sr;
564575
}
565576

src/browser/dom/node.zig

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
3737
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
3838
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
3939
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
40+
const ShadowRoot = @import("shadow_root.zig").ShadowRoot;
4041
const Walker = @import("walker.zig").WalkerDepthFirst;
4142

4243
// HTML
@@ -354,11 +355,23 @@ pub const Node = struct {
354355
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
355356
// - An Element inside a shadow DOM will return the associated ShadowRoot.
356357
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
357-
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
358+
const GetRootNodeResult = union(enum) {
359+
shadow_root: *ShadowRoot,
360+
node: Union,
361+
};
362+
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
358363
if (options) |options_| if (options_.composed) {
359364
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
360365
};
361-
return try Node.toInterface(try parser.nodeGetRootNode(self));
366+
367+
const root = try parser.nodeGetRootNode(self);
368+
if (page.getNodeState(root)) |state| {
369+
if (state.shadow_root) |sr| {
370+
return .{ .shadow_root = sr };
371+
}
372+
}
373+
374+
return .{ .node = try Node.toInterface(root) };
362375
}
363376

364377
pub fn _hasChildNodes(self: *parser.Node) !bool {

src/browser/html/elements.zig

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const Page = @import("../page.zig").Page;
2626
const urlStitch = @import("../../url.zig").URL.stitch;
2727
const URL = @import("../url/url.zig").URL;
2828
const Node = @import("../dom/node.zig").Node;
29+
const NodeUnion = @import("../dom/node.zig").Union;
2930
const Element = @import("../dom/element.zig").Element;
3031
const DataSet = @import("DataSet.zig");
3132

@@ -85,6 +86,7 @@ pub const Interfaces = .{
8586
HTMLScriptElement,
8687
HTMLSourceElement,
8788
HTMLSpanElement,
89+
HTMLSlotElement,
8890
HTMLStyleElement,
8991
HTMLTableElement,
9092
HTMLTableCaptionElement,
@@ -1008,6 +1010,101 @@ pub const HTMLSpanElement = struct {
10081010
pub const subtype = .node;
10091011
};
10101012

1013+
pub const HTMLSlotElement = struct {
1014+
pub const Self = parser.Slot;
1015+
pub const prototype = *HTMLElement;
1016+
pub const subtype = .node;
1017+
1018+
pub fn get_name(self: *parser.Slot) !?[]const u8 {
1019+
return (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name")) orelse "";
1020+
}
1021+
1022+
pub fn set_name(self: *parser.Slot, value: []const u8) !void {
1023+
return parser.elementSetAttribute(@ptrCast(@alignCast(self)), "name", value);
1024+
}
1025+
1026+
const AssignedNodesOpts = struct {
1027+
flatten: bool = false,
1028+
};
1029+
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
1030+
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
1031+
1032+
if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
1033+
return nodes;
1034+
}
1035+
1036+
if (!opts.flatten) {
1037+
return &.{};
1038+
}
1039+
1040+
const node: *parser.Node = @ptrCast(@alignCast(self));
1041+
const nl = try parser.nodeGetChildNodes(node);
1042+
const len = try parser.nodeListLength(nl);
1043+
if (len == 0) {
1044+
return &.{};
1045+
}
1046+
1047+
var assigned = try page.call_arena.alloc(NodeUnion, len);
1048+
var i: usize = 0;
1049+
while (true) : (i += 1) {
1050+
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
1051+
assigned[i] = try Node.toInterface(child);
1052+
}
1053+
return assigned[0..i];
1054+
}
1055+
1056+
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
1057+
if (opts.flatten) {
1058+
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
1059+
}
1060+
1061+
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
1062+
const node: *parser.Node = @ptrCast(@alignCast(self));
1063+
var root = try parser.nodeGetRootNode(node);
1064+
if (page.getNodeState(root)) |state| {
1065+
if (state.shadow_root) |sr| {
1066+
root = @ptrCast(@alignCast(sr.host));
1067+
}
1068+
}
1069+
1070+
var arr: std.ArrayList(NodeUnion) = .empty;
1071+
const w = @import("../dom/walker.zig").WalkerChildren{};
1072+
var next: ?*parser.Node = null;
1073+
while (true) {
1074+
next = try w.get_next(root, next) orelse break;
1075+
if (try parser.nodeType(next.?) != .element) {
1076+
if (slot_name == null) {
1077+
// default slot (with no name), takes everything
1078+
try arr.append(page.call_arena, try Node.toInterface(next.?));
1079+
}
1080+
continue;
1081+
}
1082+
const el: *parser.Element = @ptrCast(@alignCast(next.?));
1083+
const element_slot = try parser.elementGetAttribute(el, "slot");
1084+
1085+
if (nullableStringsAreEqual(slot_name, element_slot)) {
1086+
// either they're the same string or they are both null
1087+
try arr.append(page.call_arena, try Node.toInterface(next.?));
1088+
continue;
1089+
}
1090+
}
1091+
return if (arr.items.len == 0) null else arr.items;
1092+
}
1093+
1094+
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
1095+
if (a == null and b == null) {
1096+
return true;
1097+
}
1098+
if (a) |aa| {
1099+
const bb = b orelse return false;
1100+
return std.mem.eql(u8, aa, bb);
1101+
}
1102+
1103+
// a is null, but b isn't (else the first guard clause would have hit)
1104+
return false;
1105+
}
1106+
};
1107+
10111108
pub const HTMLStyleElement = struct {
10121109
pub const Self = parser.Style;
10131110
pub const prototype = *HTMLElement;
@@ -1168,6 +1265,7 @@ pub fn toInterfaceFromTag(comptime T: type, e: *parser.Element, tag: parser.Tag)
11681265
.select => .{ .HTMLSelectElement = @as(*parser.Select, @ptrCast(e)) },
11691266
.source => .{ .HTMLSourceElement = @as(*parser.Source, @ptrCast(e)) },
11701267
.span => .{ .HTMLSpanElement = @as(*parser.Span, @ptrCast(e)) },
1268+
.slot => .{ .HTMLSlotElement = @as(*parser.Slot, @ptrCast(e)) },
11711269
.style => .{ .HTMLStyleElement = @as(*parser.Style, @ptrCast(e)) },
11721270
.table => .{ .HTMLTableElement = @as(*parser.Table, @ptrCast(e)) },
11731271
.caption => .{ .HTMLTableCaptionElement = @as(*parser.TableCaption, @ptrCast(e)) },
@@ -1484,6 +1582,10 @@ test "Browser: HTML.HTMLScriptElement" {
14841582
try testing.htmlRunner("html/script/inline_defer.html");
14851583
}
14861584

1585+
test "Browser: HTML.HTMLSlotElement" {
1586+
try testing.htmlRunner("html/html_slot_element.html");
1587+
}
1588+
14871589
const Check = struct {
14881590
input: []const u8,
14891591
expected: ?[]const u8 = null, // Needed when input != expected

src/browser/netsurf.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ pub const Tag = enum(u8) {
259259
_var = c.DOM_HTML_ELEMENT_TYPE_VAR,
260260
video = c.DOM_HTML_ELEMENT_TYPE_VIDEO,
261261
wbr = c.DOM_HTML_ELEMENT_TYPE_WBR,
262+
slot = c.DOM_HTML_ELEMENT_TYPE_SLOT,
262263
undef = c.DOM_HTML_ELEMENT_TYPE__UNKNOWN,
263264

264265
pub fn all() []Tag {
@@ -1973,6 +1974,7 @@ pub const Script = c.dom_html_script_element;
19731974
pub const Select = c.dom_html_select_element;
19741975
pub const Source = struct { base: *c.dom_html_element };
19751976
pub const Span = struct { base: *c.dom_html_element };
1977+
pub const Slot = c.dom_html_slot_element;
19761978
pub const Style = c.dom_html_style_element;
19771979
pub const Table = c.dom_html_table_element;
19781980
pub const TableCaption = c.dom_html_table_caption_element;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script src="../testing.js"></script>
2+
<script>
3+
class LightPanda extends HTMLElement {
4+
constructor() {
5+
super();
6+
}
7+
8+
connectedCallback() {
9+
const shadow = this.attachShadow({ mode: "open" });
10+
11+
const slot1 = document.createElement('slot');
12+
slot1.name = 'slot-1';
13+
shadow.appendChild(slot1);
14+
15+
switch (this.getAttribute('mode')) {
16+
case '1':
17+
slot1.innerHTML = 'hello';
18+
break;
19+
case '2':
20+
const slot2 = document.createElement('slot');
21+
shadow.appendChild(slot2);
22+
break;
23+
}
24+
}
25+
}
26+
window.customElements.define("lp-test", LightPanda);
27+
</script>
28+
29+
<lp-test id=lp1 mode=1></lp-test>
30+
<lp-test id=lp2 mode=0></lp-test>
31+
<lp-test id=lp3 mode=0>default</lp-test>
32+
<lp-test id=lp4 mode=1><p slot=other>default</p></lp-test>
33+
<lp-test id=lp5 mode=1><p slot=slot-1>default</p> xx <b slot=slot-1>other</b></lp-test>
34+
<lp-test id=lp6 mode=2>More <p slot=slot-1>default2</p> <span>!!</span></lp-test>
35+
36+
<script id=HTMLSlotElement>
37+
function assertNodes(expected, actual) {
38+
actual = actual.map((n) => n.id || n.textContent)
39+
testing.expectEqual(expected, actual);
40+
}
41+
42+
for (let idx of [1, 2, 3, 4]) {
43+
const lp = $(`#lp${idx}`);
44+
const slot = lp.shadowRoot.querySelector('slot');
45+
46+
assertNodes([], slot.assignedNodes());
47+
assertNodes([], slot.assignedNodes({}));
48+
assertNodes([], slot.assignedNodes({flatten: false}));
49+
if (lp.getAttribute('mode') === '1') {
50+
assertNodes(['hello'], slot.assignedNodes({flatten: true}));
51+
} else {
52+
assertNodes([], slot.assignedNodes({flatten: true}));
53+
}
54+
}
55+
56+
const lp5 = $('#lp5');
57+
const s5 = lp5.shadowRoot.querySelector('slot');
58+
assertNodes(['default', 'other'], s5.assignedNodes());
59+
60+
const lp6 = $('#lp6');
61+
const s6 = lp6.shadowRoot.querySelectorAll('slot');
62+
assertNodes(['default2'], s6[0].assignedNodes({}));
63+
assertNodes(['default2'], s6[0].assignedNodes({flatten: true}));
64+
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({}));
65+
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({flatten: true}));
66+
</script>

0 commit comments

Comments
 (0)