mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 16:28:58 +00:00
Add HTMLSlotElement, PerformanceObserver and Script get/set type
This commit is contained in:
@@ -1024,6 +1024,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
|||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
4 => switch (@as(u32, @bitCast(name[0..4].*))) {
|
4 => switch (@as(u32, @bitCast(name[0..4].*))) {
|
||||||
|
asUint("span") => return self.createHtmlElementT(
|
||||||
|
Element.Html.Generic,
|
||||||
|
namespace,
|
||||||
|
attribute_iterator,
|
||||||
|
.{ ._proto = undefined, ._tag_name = String.init(undefined, "span", .{}) catch unreachable, ._tag = .span },
|
||||||
|
),
|
||||||
asUint("meta") => return self.createHtmlElementT(
|
asUint("meta") => return self.createHtmlElementT(
|
||||||
Element.Html.Meta,
|
Element.Html.Meta,
|
||||||
namespace,
|
namespace,
|
||||||
@@ -1036,6 +1042,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
|||||||
attribute_iterator,
|
attribute_iterator,
|
||||||
.{ ._proto = undefined },
|
.{ ._proto = undefined },
|
||||||
),
|
),
|
||||||
|
asUint("slot") => return self.createHtmlElementT(
|
||||||
|
Element.Html.Slot,
|
||||||
|
namespace,
|
||||||
|
attribute_iterator,
|
||||||
|
.{ ._proto = undefined },
|
||||||
|
),
|
||||||
asUint("html") => return self.createHtmlElementT(
|
asUint("html") => return self.createHtmlElementT(
|
||||||
Element.Html.Html,
|
Element.Html.Html,
|
||||||
namespace,
|
namespace,
|
||||||
@@ -1066,12 +1078,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
|||||||
attribute_iterator,
|
attribute_iterator,
|
||||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "main", .{}) catch unreachable, ._tag = .main },
|
.{ ._proto = undefined, ._tag_name = String.init(undefined, "main", .{}) catch unreachable, ._tag = .main },
|
||||||
),
|
),
|
||||||
asUint("span") => return self.createHtmlElementT(
|
|
||||||
Element.Html.Generic,
|
|
||||||
namespace,
|
|
||||||
attribute_iterator,
|
|
||||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "span", .{}) catch unreachable, ._tag = .span },
|
|
||||||
),
|
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
5 => switch (@as(u40, @bitCast(name[0..5].*))) {
|
5 => switch (@as(u40, @bitCast(name[0..5].*))) {
|
||||||
@@ -1787,3 +1793,7 @@ const testing = @import("../testing.zig");
|
|||||||
test "WebApi: Page" {
|
test "WebApi: Page" {
|
||||||
try testing.htmlRunner("page", .{});
|
try testing.htmlRunner("page", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "WebApi: Integration" {
|
||||||
|
try testing.htmlRunner("integration", .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,11 +249,14 @@ pub fn addFromElement(self: *ScriptManager, script_element: *Element.Html.Script
|
|||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
if (comptime IS_DEBUG) {
|
||||||
.ctx = ctx,
|
log.debug(.http, "script queue", .{
|
||||||
.url = remote_url.?,
|
.ctx = ctx,
|
||||||
.stack = page.js.stackTrace() catch "???",
|
.url = remote_url.?,
|
||||||
});
|
.element = element,
|
||||||
|
.stack = page.js.stackTrace() catch "???",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (script.mode != .normal) {
|
if (script.mode != .normal) {
|
||||||
@@ -326,12 +329,14 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
var headers = try self.client.newHeaders();
|
var headers = try self.client.newHeaders();
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
if (comptime IS_DEBUG) {
|
||||||
.url = url,
|
log.debug(.http, "script queue", .{
|
||||||
.ctx = "module",
|
.url = url,
|
||||||
.referrer = referrer,
|
.ctx = "module",
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.referrer = referrer,
|
||||||
});
|
.stack = self.page.js.stackTrace() catch "???",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
@@ -403,12 +408,14 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
var headers = try self.client.newHeaders();
|
var headers = try self.client.newHeaders();
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
if (comptime IS_DEBUG) {
|
||||||
.url = url,
|
log.debug(.http, "script queue", .{
|
||||||
.ctx = "dynamic module",
|
.url = url,
|
||||||
.referrer = referrer,
|
.ctx = "dynamic module",
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.referrer = referrer,
|
||||||
});
|
.stack = self.page.js.stackTrace() catch "???",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// It's possible, but unlikely, for client.request to immediately finish
|
// It's possible, but unlikely, for client.request to immediately finish
|
||||||
// a request, thus calling our callback. We generally don't want a call
|
// a request, thus calling our callback. We generally don't want a call
|
||||||
@@ -617,11 +624,13 @@ const Script = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(.http, "script header", .{
|
if (comptime IS_DEBUG) {
|
||||||
.req = transfer,
|
log.debug(.http, "script header", .{
|
||||||
.status = header.status,
|
.req = transfer,
|
||||||
.content_type = header.contentType(),
|
.status = header.status,
|
||||||
});
|
.content_type = header.contentType(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// If this isn't true, then we'll likely leak memory. If you don't
|
// If this isn't true, then we'll likely leak memory. If you don't
|
||||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
||||||
@@ -649,7 +658,9 @@ const Script = struct {
|
|||||||
fn doneCallback(ctx: *anyopaque) !void {
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
const self: *Script = @ptrCast(@alignCast(ctx));
|
const self: *Script = @ptrCast(@alignCast(ctx));
|
||||||
self.complete = true;
|
self.complete = true;
|
||||||
log.debug(.http, "script fetch complete", .{ .req = self.url });
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.http, "script fetch complete", .{ .req = self.url });
|
||||||
|
}
|
||||||
|
|
||||||
const manager = self.manager;
|
const manager = self.manager;
|
||||||
if (self.mode == .async or self.mode == .import_async) {
|
if (self.mode == .async or self.mode == .import_async) {
|
||||||
|
|||||||
@@ -538,6 +538,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/html/Paragraph.zig"),
|
@import("../webapi/element/html/Paragraph.zig"),
|
||||||
@import("../webapi/element/html/Script.zig"),
|
@import("../webapi/element/html/Script.zig"),
|
||||||
@import("../webapi/element/html/Select.zig"),
|
@import("../webapi/element/html/Select.zig"),
|
||||||
|
@import("../webapi/element/html/Slot.zig"),
|
||||||
@import("../webapi/element/html/Style.zig"),
|
@import("../webapi/element/html/Style.zig"),
|
||||||
@import("../webapi/element/html/Template.zig"),
|
@import("../webapi/element/html/Template.zig"),
|
||||||
@import("../webapi/element/html/TextArea.zig"),
|
@import("../webapi/element/html/TextArea.zig"),
|
||||||
@@ -574,4 +575,5 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/Blob.zig"),
|
@import("../webapi/Blob.zig"),
|
||||||
@import("../webapi/File.zig"),
|
@import("../webapi/File.zig"),
|
||||||
@import("../webapi/Screen.zig"),
|
@import("../webapi/Screen.zig"),
|
||||||
|
@import("../webapi/PerformanceObserver.zig"),
|
||||||
});
|
});
|
||||||
|
|||||||
384
src/browser/tests/element/html/slot.html
Normal file
384
src/browser/tests/element/html/slot.html
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="Slot#basic_creation">
|
||||||
|
{
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
testing.expectEqual('SLOT', slot.tagName);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#name_attribute">
|
||||||
|
{
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
|
||||||
|
// Set name via setAttribute
|
||||||
|
slot.setAttribute('name', 'header');
|
||||||
|
testing.expectEqual('header', slot.getAttribute('name'));
|
||||||
|
|
||||||
|
// Change name
|
||||||
|
slot.setAttribute('name', 'footer');
|
||||||
|
testing.expectEqual('footer', slot.getAttribute('name'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#assignedNodes_empty">
|
||||||
|
{
|
||||||
|
// Slot not in shadow tree
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
const nodes = slot.assignedNodes();
|
||||||
|
testing.expectEqual(0, nodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#assignedElements_empty">
|
||||||
|
{
|
||||||
|
// Slot not in shadow tree
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
const elements = slot.assignedElements();
|
||||||
|
testing.expectEqual(0, elements.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#default_slot_basic">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
// Create default slot (no name)
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
shadow.appendChild(slot);
|
||||||
|
|
||||||
|
// Add content to host
|
||||||
|
const span1 = document.createElement('span');
|
||||||
|
span1.textContent = 'Content 1';
|
||||||
|
host.appendChild(span1);
|
||||||
|
|
||||||
|
const span2 = document.createElement('span');
|
||||||
|
span2.textContent = 'Content 2';
|
||||||
|
host.appendChild(span2);
|
||||||
|
|
||||||
|
// Both spans should be assigned to default slot
|
||||||
|
const nodes = slot.assignedNodes();
|
||||||
|
testing.expectEqual(2, nodes.length);
|
||||||
|
testing.expectTrue(nodes[0] === span1);
|
||||||
|
testing.expectTrue(nodes[1] === span2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#named_slot">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
// Create named slot
|
||||||
|
const headerSlot = document.createElement('slot');
|
||||||
|
headerSlot.name = 'header';
|
||||||
|
shadow.appendChild(headerSlot);
|
||||||
|
|
||||||
|
// Add content with slot attribute
|
||||||
|
const h1 = document.createElement('h1');
|
||||||
|
h1.textContent = 'Title';
|
||||||
|
h1.setAttribute('slot', 'header');
|
||||||
|
host.appendChild(h1);
|
||||||
|
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.textContent = 'Body';
|
||||||
|
host.appendChild(p);
|
||||||
|
|
||||||
|
// Only h1 should be assigned to header slot
|
||||||
|
const nodes = headerSlot.assignedNodes();
|
||||||
|
testing.expectEqual(1, nodes.length);
|
||||||
|
testing.expectTrue(nodes[0] === h1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#multiple_slots">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
// Create multiple named slots
|
||||||
|
const headerSlot = document.createElement('slot');
|
||||||
|
headerSlot.name = 'header';
|
||||||
|
shadow.appendChild(headerSlot);
|
||||||
|
|
||||||
|
const footerSlot = document.createElement('slot');
|
||||||
|
footerSlot.name = 'footer';
|
||||||
|
shadow.appendChild(footerSlot);
|
||||||
|
|
||||||
|
const defaultSlot = document.createElement('slot');
|
||||||
|
shadow.appendChild(defaultSlot);
|
||||||
|
|
||||||
|
// Add content
|
||||||
|
const h1 = document.createElement('h1');
|
||||||
|
h1.setAttribute('slot', 'header');
|
||||||
|
host.appendChild(h1);
|
||||||
|
|
||||||
|
const p = document.createElement('p');
|
||||||
|
host.appendChild(p);
|
||||||
|
|
||||||
|
const footer = document.createElement('footer');
|
||||||
|
footer.setAttribute('slot', 'footer');
|
||||||
|
host.appendChild(footer);
|
||||||
|
|
||||||
|
// Check each slot
|
||||||
|
const headerNodes = headerSlot.assignedNodes();
|
||||||
|
testing.expectEqual(1, headerNodes.length);
|
||||||
|
testing.expectTrue(headerNodes[0] === h1);
|
||||||
|
|
||||||
|
const footerNodes = footerSlot.assignedNodes();
|
||||||
|
testing.expectEqual(1, footerNodes.length);
|
||||||
|
testing.expectTrue(footerNodes[0] === footer);
|
||||||
|
|
||||||
|
const defaultNodes = defaultSlot.assignedNodes();
|
||||||
|
testing.expectEqual(1, defaultNodes.length);
|
||||||
|
testing.expectTrue(defaultNodes[0] === p);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#assignedElements_filters_text_nodes">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
shadow.appendChild(slot);
|
||||||
|
|
||||||
|
// Add mixed content
|
||||||
|
const span = document.createElement('span');
|
||||||
|
host.appendChild(span);
|
||||||
|
|
||||||
|
const text = document.createTextNode('Some text');
|
||||||
|
host.appendChild(text);
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
host.appendChild(div);
|
||||||
|
|
||||||
|
// assignedNodes should include all
|
||||||
|
const nodes = slot.assignedNodes();
|
||||||
|
testing.expectEqual(3, nodes.length);
|
||||||
|
|
||||||
|
// assignedElements should only include elements
|
||||||
|
const elements = slot.assignedElements();
|
||||||
|
testing.expectEqual(2, elements.length);
|
||||||
|
testing.expectTrue(elements[0] === span);
|
||||||
|
testing.expectTrue(elements[1] === div);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#text_nodes_default_slot_only">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const namedSlot = document.createElement('slot');
|
||||||
|
namedSlot.name = 'named';
|
||||||
|
shadow.appendChild(namedSlot);
|
||||||
|
|
||||||
|
const defaultSlot = document.createElement('slot');
|
||||||
|
shadow.appendChild(defaultSlot);
|
||||||
|
|
||||||
|
// Add text node
|
||||||
|
const text = document.createTextNode('Text content');
|
||||||
|
host.appendChild(text);
|
||||||
|
|
||||||
|
// Text should go to default slot only
|
||||||
|
const namedNodes = namedSlot.assignedNodes();
|
||||||
|
testing.expectEqual(0, namedNodes.length);
|
||||||
|
|
||||||
|
const defaultNodes = defaultSlot.assignedNodes();
|
||||||
|
testing.expectEqual(1, defaultNodes.length);
|
||||||
|
testing.expectTrue(defaultNodes[0] === text);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#flatten_false">
|
||||||
|
{
|
||||||
|
const outerHost = document.createElement('div');
|
||||||
|
const outerShadow = outerHost.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const innerHost = document.createElement('div');
|
||||||
|
const innerShadow = innerHost.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
// Inner slot
|
||||||
|
const innerSlot = document.createElement('slot');
|
||||||
|
innerShadow.appendChild(innerSlot);
|
||||||
|
|
||||||
|
// Outer slot contains inner host
|
||||||
|
const outerSlot = document.createElement('slot');
|
||||||
|
outerShadow.appendChild(outerSlot);
|
||||||
|
outerHost.appendChild(innerHost);
|
||||||
|
|
||||||
|
// Add content to inner host
|
||||||
|
const span = document.createElement('span');
|
||||||
|
innerHost.appendChild(span);
|
||||||
|
|
||||||
|
// Without flatten, outer slot should see inner host (not span)
|
||||||
|
const nodes = outerSlot.assignedNodes();
|
||||||
|
testing.expectEqual(1, nodes.length);
|
||||||
|
testing.expectTrue(nodes[0] === innerHost);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#flatten_true">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
shadow.appendChild(slot);
|
||||||
|
|
||||||
|
// Add an element with a nested slot in it
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = '<slot name="nested"></slot>';
|
||||||
|
host.appendChild(container);
|
||||||
|
|
||||||
|
// Add a regular span
|
||||||
|
const span = document.createElement('span');
|
||||||
|
host.appendChild(span);
|
||||||
|
|
||||||
|
// Without flatten: should see container and span (2 elements)
|
||||||
|
const nodesNoFlat = slot.assignedNodes({ flatten: false });
|
||||||
|
testing.expectEqual(2, nodesNoFlat.length);
|
||||||
|
|
||||||
|
// With flatten: should still see container and span
|
||||||
|
// (flatten only matters if the assigned node is itself a slot in a shadow tree)
|
||||||
|
const nodesFlat = slot.assignedNodes({ flatten: true });
|
||||||
|
testing.expectEqual(2, nodesFlat.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#assignedElements_with_flatten">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
shadow.appendChild(slot);
|
||||||
|
|
||||||
|
// Add mixed content
|
||||||
|
const div = document.createElement('div');
|
||||||
|
host.appendChild(div);
|
||||||
|
|
||||||
|
const text = document.createTextNode('text');
|
||||||
|
host.appendChild(text);
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
host.appendChild(span);
|
||||||
|
|
||||||
|
// assignedElements with flatten should only return elements
|
||||||
|
const elements = slot.assignedElements({ flatten: true });
|
||||||
|
testing.expectEqual(2, elements.length);
|
||||||
|
testing.expectTrue(elements[0] === div);
|
||||||
|
testing.expectTrue(elements[1] === span);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#empty_slot_name_matches_no_slot_attribute">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const defaultSlot = document.createElement('slot');
|
||||||
|
shadow.appendChild(defaultSlot);
|
||||||
|
|
||||||
|
// Element without slot attribute
|
||||||
|
const div = document.createElement('div');
|
||||||
|
host.appendChild(div);
|
||||||
|
|
||||||
|
// Element with empty slot attribute
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.setAttribute('slot', '');
|
||||||
|
host.appendChild(span);
|
||||||
|
|
||||||
|
// Both should go to default slot
|
||||||
|
const nodes = defaultSlot.assignedNodes();
|
||||||
|
testing.expectEqual(2, nodes.length);
|
||||||
|
testing.expectTrue(nodes[0] === div);
|
||||||
|
testing.expectTrue(nodes[1] === span);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#slot_attribute_case_sensitive">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
slot.name = 'MySlot';
|
||||||
|
shadow.appendChild(slot);
|
||||||
|
|
||||||
|
// Matching case
|
||||||
|
const div1 = document.createElement('div');
|
||||||
|
div1.setAttribute('slot', 'MySlot');
|
||||||
|
host.appendChild(div1);
|
||||||
|
|
||||||
|
// Different case
|
||||||
|
const div2 = document.createElement('div');
|
||||||
|
div2.setAttribute('slot', 'myslot');
|
||||||
|
host.appendChild(div2);
|
||||||
|
|
||||||
|
// Only exact match should be assigned
|
||||||
|
const nodes = slot.assignedNodes();
|
||||||
|
testing.expectEqual(1, nodes.length);
|
||||||
|
testing.expectTrue(nodes[0] === div1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#slot_in_slot_shadow_tree">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
// Create a container in shadow tree
|
||||||
|
const container = document.createElement('div');
|
||||||
|
shadow.appendChild(container);
|
||||||
|
|
||||||
|
// Slot inside container
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
container.appendChild(slot);
|
||||||
|
|
||||||
|
// Add content to host
|
||||||
|
const span = document.createElement('span');
|
||||||
|
host.appendChild(span);
|
||||||
|
|
||||||
|
// Slot should still work even when not direct child of shadow root
|
||||||
|
const nodes = slot.assignedNodes();
|
||||||
|
testing.expectEqual(1, nodes.length);
|
||||||
|
testing.expectTrue(nodes[0] === span);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="Slot#flatten_with_slot_and_siblings">
|
||||||
|
{
|
||||||
|
// Test that flatten continues processing siblings after a nested slot
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const outerSlot = document.createElement('slot');
|
||||||
|
shadow.appendChild(outerSlot);
|
||||||
|
|
||||||
|
// Add a nested slot as first child
|
||||||
|
const innerSlot = document.createElement('slot');
|
||||||
|
innerSlot.setAttribute('name', 'inner');
|
||||||
|
host.appendChild(innerSlot);
|
||||||
|
|
||||||
|
// Add a regular element after the slot
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = 'After slot';
|
||||||
|
host.appendChild(div);
|
||||||
|
|
||||||
|
// Add another element
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = 'Another element';
|
||||||
|
host.appendChild(span);
|
||||||
|
|
||||||
|
// With flatten=true, should get all elements including those after the nested slot
|
||||||
|
const flattened = outerSlot.assignedElements({ flatten: true });
|
||||||
|
testing.expectEqual(3, flattened.length);
|
||||||
|
testing.expectTrue(flattened[0] === innerSlot);
|
||||||
|
testing.expectTrue(flattened[1] === div);
|
||||||
|
testing.expectTrue(flattened[2] === span);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
456
src/browser/tests/integration/custom_element_composition.html
Normal file
456
src/browser/tests/integration/custom_element_composition.html
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<body></body>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<!-- Test complex component composition patterns like React/Next.js -->
|
||||||
|
|
||||||
|
<script id="card_component_with_slots">
|
||||||
|
{
|
||||||
|
// Define a card component with header, body, footer slots (like React children props)
|
||||||
|
customElements.define('app-card', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<slot name="header">Default Header</slot>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create card with mixed content
|
||||||
|
const card = document.createElement('app-card');
|
||||||
|
|
||||||
|
const header = document.createElement('h2');
|
||||||
|
header.setAttribute('slot', 'header');
|
||||||
|
header.textContent = 'My Card';
|
||||||
|
card.appendChild(header);
|
||||||
|
|
||||||
|
const body = document.createElement('p');
|
||||||
|
body.textContent = 'Card content';
|
||||||
|
card.appendChild(body);
|
||||||
|
|
||||||
|
const footer = document.createElement('span');
|
||||||
|
footer.setAttribute('slot', 'footer');
|
||||||
|
footer.textContent = 'Footer text';
|
||||||
|
card.appendChild(footer);
|
||||||
|
|
||||||
|
document.body.appendChild(card);
|
||||||
|
|
||||||
|
// Verify slot assignments
|
||||||
|
const headerSlot = card.shadowRoot.querySelector('slot[name="header"]');
|
||||||
|
const defaultSlot = card.shadowRoot.querySelectorAll('slot:not([name])')[0];
|
||||||
|
const footerSlot = card.shadowRoot.querySelector('slot[name="footer"]');
|
||||||
|
|
||||||
|
testing.expectEqual(1, headerSlot.assignedElements().length);
|
||||||
|
testing.expectEqual(1, defaultSlot.assignedElements().length);
|
||||||
|
testing.expectEqual(1, footerSlot.assignedElements().length);
|
||||||
|
|
||||||
|
testing.expectTrue(headerSlot.assignedElements()[0] === header);
|
||||||
|
testing.expectTrue(defaultSlot.assignedElements()[0] === body);
|
||||||
|
testing.expectTrue(footerSlot.assignedElements()[0] === footer);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="nested_components_with_slots">
|
||||||
|
{
|
||||||
|
// Like React HOCs or wrapper components
|
||||||
|
customElements.define('app-wrapper', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<div class="wrapper">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define('app-inner', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<div class="inner">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nest components
|
||||||
|
const wrapper = document.createElement('app-wrapper');
|
||||||
|
const inner = document.createElement('app-inner');
|
||||||
|
const content = document.createElement('span');
|
||||||
|
content.textContent = 'Deeply nested';
|
||||||
|
|
||||||
|
inner.appendChild(content);
|
||||||
|
wrapper.appendChild(inner);
|
||||||
|
document.body.appendChild(wrapper);
|
||||||
|
|
||||||
|
// Verify outer wrapper slot
|
||||||
|
const wrapperSlot = wrapper.shadowRoot.querySelector('slot');
|
||||||
|
const wrapperAssigned = wrapperSlot.assignedElements();
|
||||||
|
testing.expectEqual(1, wrapperAssigned.length);
|
||||||
|
testing.expectTrue(wrapperAssigned[0] === inner);
|
||||||
|
|
||||||
|
// Verify inner component slot
|
||||||
|
const innerSlot = inner.shadowRoot.querySelector('slot');
|
||||||
|
const innerAssigned = innerSlot.assignedElements();
|
||||||
|
testing.expectEqual(1, innerAssigned.length);
|
||||||
|
testing.expectTrue(innerAssigned[0] === content);
|
||||||
|
|
||||||
|
// With flatten, outer slot should see through to content
|
||||||
|
const flatAssigned = wrapperSlot.assignedElements({ flatten: true });
|
||||||
|
testing.expectEqual(1, flatAssigned.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="tab_component_pattern">
|
||||||
|
{
|
||||||
|
// Tabs pattern - common in UI libraries
|
||||||
|
customElements.define('app-tabs', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab-buttons">
|
||||||
|
<slot name="tabs"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<slot name="panels"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = document.createElement('app-tabs');
|
||||||
|
|
||||||
|
// Add tab buttons
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.setAttribute('slot', 'tabs');
|
||||||
|
button.textContent = `Tab ${i}`;
|
||||||
|
tabs.appendChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add panels
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.setAttribute('slot', 'panels');
|
||||||
|
panel.textContent = `Panel ${i}`;
|
||||||
|
tabs.appendChild(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(tabs);
|
||||||
|
|
||||||
|
const tabSlot = tabs.shadowRoot.querySelector('slot[name="tabs"]');
|
||||||
|
const panelSlot = tabs.shadowRoot.querySelector('slot[name="panels"]');
|
||||||
|
|
||||||
|
testing.expectEqual(3, tabSlot.assignedElements().length);
|
||||||
|
testing.expectEqual(3, panelSlot.assignedElements().length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="layout_component_all_slots">
|
||||||
|
{
|
||||||
|
// App layout pattern from Next.js/React
|
||||||
|
customElements.define('app-layout', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<div class="layout">
|
||||||
|
<header><slot name="header"></slot></header>
|
||||||
|
<aside><slot name="sidebar"></slot></aside>
|
||||||
|
<main><slot></slot></main>
|
||||||
|
<footer><slot name="footer"></slot></footer>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const layout = document.createElement('app-layout');
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.setAttribute('slot', 'header');
|
||||||
|
header.textContent = 'Header';
|
||||||
|
layout.appendChild(header);
|
||||||
|
|
||||||
|
const sidebar = document.createElement('nav');
|
||||||
|
sidebar.setAttribute('slot', 'sidebar');
|
||||||
|
sidebar.textContent = 'Nav';
|
||||||
|
layout.appendChild(sidebar);
|
||||||
|
|
||||||
|
const main = document.createElement('article');
|
||||||
|
main.textContent = 'Main content';
|
||||||
|
layout.appendChild(main);
|
||||||
|
|
||||||
|
const footer = document.createElement('div');
|
||||||
|
footer.setAttribute('slot', 'footer');
|
||||||
|
footer.textContent = 'Footer';
|
||||||
|
layout.appendChild(footer);
|
||||||
|
|
||||||
|
document.body.appendChild(layout);
|
||||||
|
|
||||||
|
// All slots should be filled
|
||||||
|
const slots = layout.shadowRoot.querySelectorAll('slot');
|
||||||
|
testing.expectEqual(4, slots.length);
|
||||||
|
|
||||||
|
for (const slot of slots) {
|
||||||
|
testing.expectTrue(slot.assignedNodes().length > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="dynamic_slot_reassignment">
|
||||||
|
{
|
||||||
|
// Simulating conditional rendering like React
|
||||||
|
customElements.define('app-container', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<slot name="primary"></slot>
|
||||||
|
<slot name="secondary"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement('app-container');
|
||||||
|
const elem = document.createElement('div');
|
||||||
|
elem.textContent = 'Content';
|
||||||
|
elem.setAttribute('slot', 'primary');
|
||||||
|
container.appendChild(elem);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
const primarySlot = container.shadowRoot.querySelector('slot[name="primary"]');
|
||||||
|
const secondarySlot = container.shadowRoot.querySelector('slot[name="secondary"]');
|
||||||
|
|
||||||
|
// Initially in primary
|
||||||
|
testing.expectEqual(1, primarySlot.assignedElements().length);
|
||||||
|
testing.expectEqual(0, secondarySlot.assignedElements().length);
|
||||||
|
|
||||||
|
// Move to secondary (like React re-rendering with different props)
|
||||||
|
elem.setAttribute('slot', 'secondary');
|
||||||
|
|
||||||
|
// Should now be in secondary
|
||||||
|
testing.expectEqual(0, primarySlot.assignedElements().length);
|
||||||
|
testing.expectEqual(1, secondarySlot.assignedElements().length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="list_rendering_pattern">
|
||||||
|
{
|
||||||
|
// List rendering like map() in React
|
||||||
|
customElements.define('app-list', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<ul><slot></slot></ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define('app-list-item', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<li><slot></slot></li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = document.createElement('app-list');
|
||||||
|
|
||||||
|
const items = ['Item 1', 'Item 2', 'Item 3'];
|
||||||
|
items.forEach(text => {
|
||||||
|
const item = document.createElement('app-list-item');
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = text;
|
||||||
|
item.appendChild(span);
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(list);
|
||||||
|
|
||||||
|
// List should have 3 items assigned
|
||||||
|
const listSlot = list.shadowRoot.querySelector('slot');
|
||||||
|
testing.expectEqual(3, listSlot.assignedElements().length);
|
||||||
|
|
||||||
|
// Each item should have content
|
||||||
|
const itemElements = listSlot.assignedElements();
|
||||||
|
itemElements.forEach(item => {
|
||||||
|
const itemSlot = item.shadowRoot.querySelector('slot');
|
||||||
|
testing.expectEqual(1, itemSlot.assignedElements().length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="fallback_content_pattern">
|
||||||
|
{
|
||||||
|
// Default/fallback content like React default props
|
||||||
|
customElements.define('app-message', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<div class="message">
|
||||||
|
<slot>No message provided</slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Without content - should use fallback
|
||||||
|
const empty = document.createElement('app-message');
|
||||||
|
document.body.appendChild(empty);
|
||||||
|
const emptySlot = empty.shadowRoot.querySelector('slot');
|
||||||
|
testing.expectEqual(0, emptySlot.assignedNodes().length);
|
||||||
|
|
||||||
|
// With content - should override fallback
|
||||||
|
const filled = document.createElement('app-message');
|
||||||
|
const text = document.createTextNode('Custom message');
|
||||||
|
filled.appendChild(text);
|
||||||
|
document.body.appendChild(filled);
|
||||||
|
const filledSlot = filled.shadowRoot.querySelector('slot');
|
||||||
|
testing.expectEqual(1, filledSlot.assignedNodes().length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="form_custom_input">
|
||||||
|
{
|
||||||
|
// Custom form input component
|
||||||
|
customElements.define('app-input', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<label>
|
||||||
|
<slot name="label">Label</slot>
|
||||||
|
<input type="text">
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = document.createElement('app-input');
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.setAttribute('slot', 'label');
|
||||||
|
label.textContent = 'Username:';
|
||||||
|
input.appendChild(label);
|
||||||
|
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
const labelSlot = input.shadowRoot.querySelector('slot[name="label"]');
|
||||||
|
testing.expectEqual(1, labelSlot.assignedElements().length);
|
||||||
|
testing.expectEqual('Username:', labelSlot.assignedElements()[0].textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="deeply_nested_slot_chain">
|
||||||
|
{
|
||||||
|
// Three levels of nesting with slots
|
||||||
|
customElements.define('app-outer', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `<div class="outer"><slot></slot></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define('app-middle', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `<div class="middle"><slot></slot></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define('app-leaf', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `<div class="leaf"><slot></slot></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const outer = document.createElement('app-outer');
|
||||||
|
const middle = document.createElement('app-middle');
|
||||||
|
const leaf = document.createElement('app-leaf');
|
||||||
|
const content = document.createElement('span');
|
||||||
|
content.textContent = 'Deep content';
|
||||||
|
|
||||||
|
leaf.appendChild(content);
|
||||||
|
middle.appendChild(leaf);
|
||||||
|
outer.appendChild(middle);
|
||||||
|
document.body.appendChild(outer);
|
||||||
|
|
||||||
|
// Each level should see one element
|
||||||
|
const outerSlot = outer.shadowRoot.querySelector('slot');
|
||||||
|
testing.expectEqual(1, outerSlot.assignedElements().length);
|
||||||
|
testing.expectTrue(outerSlot.assignedElements()[0] === middle);
|
||||||
|
|
||||||
|
const middleSlot = middle.shadowRoot.querySelector('slot');
|
||||||
|
testing.expectEqual(1, middleSlot.assignedElements().length);
|
||||||
|
testing.expectTrue(middleSlot.assignedElements()[0] === leaf);
|
||||||
|
|
||||||
|
const leafSlot = leaf.shadowRoot.querySelector('slot');
|
||||||
|
testing.expectEqual(1, leafSlot.assignedElements().length);
|
||||||
|
testing.expectTrue(leafSlot.assignedElements()[0] === content);
|
||||||
|
|
||||||
|
// Flatten should not traverse through non-slot elements
|
||||||
|
const outerFlat = outerSlot.assignedElements({ flatten: true });
|
||||||
|
testing.expectEqual(1, outerFlat.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="mixed_slotted_and_unslotted">
|
||||||
|
{
|
||||||
|
// Some children slotted, some not (edge case)
|
||||||
|
customElements.define('app-mixed', class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<slot name="named"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mixed = document.createElement('app-mixed');
|
||||||
|
|
||||||
|
const named = document.createElement('div');
|
||||||
|
named.setAttribute('slot', 'named');
|
||||||
|
mixed.appendChild(named);
|
||||||
|
|
||||||
|
const unnamed1 = document.createElement('span');
|
||||||
|
mixed.appendChild(unnamed1);
|
||||||
|
|
||||||
|
const unnamed2 = document.createElement('p');
|
||||||
|
mixed.appendChild(unnamed2);
|
||||||
|
|
||||||
|
document.body.appendChild(mixed);
|
||||||
|
|
||||||
|
const namedSlot = mixed.shadowRoot.querySelector('slot[name="named"]');
|
||||||
|
const defaultSlot = mixed.shadowRoot.querySelectorAll('slot:not([name])')[0];
|
||||||
|
|
||||||
|
testing.expectEqual(1, namedSlot.assignedElements().length);
|
||||||
|
testing.expectEqual(2, defaultSlot.assignedElements().length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -102,4 +102,3 @@
|
|||||||
testing.expectEqual(24, screen.pixelDepth);
|
testing.expectEqual(24, screen.pixelDepth);
|
||||||
testing.expectEqual(screen, window.screen);
|
testing.expectEqual(screen, window.screen);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
|||||||
.p => "p",
|
.p => "p",
|
||||||
.script => "script",
|
.script => "script",
|
||||||
.select => "select",
|
.select => "select",
|
||||||
|
.slot => "slot",
|
||||||
.style => "style",
|
.style => "style",
|
||||||
.template => "template",
|
.template => "template",
|
||||||
.text_area => "textarea",
|
.text_area => "textarea",
|
||||||
@@ -192,6 +193,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
|||||||
.p => "P",
|
.p => "P",
|
||||||
.script => "SCRIPT",
|
.script => "SCRIPT",
|
||||||
.select => "SELECT",
|
.select => "SELECT",
|
||||||
|
.slot => "SLOT",
|
||||||
.style => "STYLE",
|
.style => "STYLE",
|
||||||
.template => "TEMPLATE",
|
.template => "TEMPLATE",
|
||||||
.text_area => "TEXTAREA",
|
.text_area => "TEXTAREA",
|
||||||
@@ -790,6 +792,7 @@ pub fn getTag(self: *const Element) Tag {
|
|||||||
.generic => |g| g._tag,
|
.generic => |g| g._tag,
|
||||||
.script => .script,
|
.script => .script,
|
||||||
.select => .select,
|
.select => .select,
|
||||||
|
.slot => .slot,
|
||||||
.option => .option,
|
.option => .option,
|
||||||
.template => .template,
|
.template => .template,
|
||||||
.text_area => .textarea,
|
.text_area => .textarea,
|
||||||
@@ -855,6 +858,7 @@ pub const Tag = enum {
|
|||||||
rect,
|
rect,
|
||||||
script,
|
script,
|
||||||
select,
|
select,
|
||||||
|
slot,
|
||||||
span,
|
span,
|
||||||
strong,
|
strong,
|
||||||
style,
|
style,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const datetime = @import("../../datetime.zig");
|
const datetime = @import("../../datetime.zig");
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{
|
||||||
|
Performance,
|
||||||
|
Entry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const Performance = @This();
|
const Performance = @This();
|
||||||
|
|
||||||
_time_origin: u64,
|
_time_origin: u64,
|
||||||
@@ -34,6 +41,65 @@ pub const JsApi = struct {
|
|||||||
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
|
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Entry = struct {
|
||||||
|
_duration: f64 = 0.0,
|
||||||
|
_entry_type: Type,
|
||||||
|
_name: []const u8,
|
||||||
|
_start_time: f64 = 0.0,
|
||||||
|
|
||||||
|
const Type = enum {
|
||||||
|
element,
|
||||||
|
event,
|
||||||
|
first_input,
|
||||||
|
largest_contentful_paint,
|
||||||
|
layout_shift,
|
||||||
|
long_animation_frame,
|
||||||
|
longtask,
|
||||||
|
mark,
|
||||||
|
measure,
|
||||||
|
navigation,
|
||||||
|
paint,
|
||||||
|
resource,
|
||||||
|
taskattribution,
|
||||||
|
visibility_state,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getDuration(self: *const Entry) f64 {
|
||||||
|
return self._duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getEntryType(self: *const Entry) []const u8 {
|
||||||
|
return switch (self._entry_type) {
|
||||||
|
.first_input => "first-input",
|
||||||
|
.largest_contentful_paint => "largest-contentful-paint",
|
||||||
|
.layout_shift => "layout-shift",
|
||||||
|
.long_animation_frame => "long-animation-frame",
|
||||||
|
.visibility_state => "visibility-state",
|
||||||
|
else => |t| @tagName(t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getName(self: *const Entry) []const u8 {
|
||||||
|
return self._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getStartTime(self: *const Entry) f64 {
|
||||||
|
return self._start_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(Entry);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PerformanceEntry";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
pub const duration = bridge.accessor(Entry.getDuration, null, .{});
|
||||||
|
pub const entryType = bridge.accessor(Entry.getEntryType, null, .{});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "WebApi: Performance" {
|
test "WebApi: Performance" {
|
||||||
try testing.htmlRunner("performance.html", .{});
|
try testing.htmlRunner("performance.html", .{});
|
||||||
|
|||||||
67
src/browser/webapi/PerformanceObserver.zig
Normal file
67
src/browser/webapi/PerformanceObserver.zig
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
|
const Entry = @import("Performance.zig").Entry;
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
|
||||||
|
const PerformanceObserver = @This();
|
||||||
|
|
||||||
|
pub fn init(callback: js.Function) PerformanceObserver {
|
||||||
|
_ = callback;
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ObserverOptions = struct {
|
||||||
|
buffered: ?bool = null,
|
||||||
|
durationThreshold: ?f64 = null,
|
||||||
|
entryTypes: ?[]const []const u8 = null,
|
||||||
|
type: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn observe(self: *const PerformanceObserver, opts_: ?ObserverOptions) void {
|
||||||
|
_ = self;
|
||||||
|
_ = opts_;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(self: *PerformanceObserver) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn takeRecords(_: *const PerformanceObserver) []const Entry {
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(PerformanceObserver);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PerformanceObserver";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const empty_with_no_proto = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(PerformanceObserver.init, .{});
|
||||||
|
|
||||||
|
pub const observe = bridge.function(PerformanceObserver.observe, .{});
|
||||||
|
pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{});
|
||||||
|
pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{});
|
||||||
|
};
|
||||||
@@ -51,6 +51,7 @@ pub const Template = @import("html/Template.zig");
|
|||||||
pub const TextArea = @import("html/TextArea.zig");
|
pub const TextArea = @import("html/TextArea.zig");
|
||||||
pub const Paragraph = @import("html/Paragraph.zig");
|
pub const Paragraph = @import("html/Paragraph.zig");
|
||||||
pub const Select = @import("html/Select.zig");
|
pub const Select = @import("html/Select.zig");
|
||||||
|
pub const Slot = @import("html/Slot.zig");
|
||||||
pub const Option = @import("html/Option.zig");
|
pub const Option = @import("html/Option.zig");
|
||||||
pub const IFrame = @import("html/IFrame.zig");
|
pub const IFrame = @import("html/IFrame.zig");
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ pub const Type = union(enum) {
|
|||||||
p: Paragraph,
|
p: Paragraph,
|
||||||
script: *Script,
|
script: *Script,
|
||||||
select: Select,
|
select: Select,
|
||||||
|
slot: Slot,
|
||||||
style: Style,
|
style: Style,
|
||||||
template: *Template,
|
template: *Template,
|
||||||
text_area: *TextArea,
|
text_area: *TextArea,
|
||||||
@@ -131,6 +133,7 @@ pub fn className(self: *const HtmlElement) []const u8 {
|
|||||||
.generic => "[object HTMLElement]",
|
.generic => "[object HTMLElement]",
|
||||||
.script => "[object HtmlScriptElement]",
|
.script => "[object HtmlScriptElement]",
|
||||||
.select => "[object HTMLSelectElement]",
|
.select => "[object HTMLSelectElement]",
|
||||||
|
.slot => "[object HTMLSlotElement]",
|
||||||
.template => "[object HTMLTemplateElement]",
|
.template => "[object HTMLTemplateElement]",
|
||||||
.option => "[object HTMLOptionElement]",
|
.option => "[object HTMLOptionElement]",
|
||||||
.text_area => "[object HtmlTextAreaElement]",
|
.text_area => "[object HtmlTextAreaElement]",
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ pub fn setSrc(self: *Script, src: []const u8, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getType(self: *const Script) []const u8 {
|
||||||
|
return self.asConstElement().getAttributeSafe("type") orelse "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setType(self: *Script, value: []const u8, page: *Page) !void {
|
||||||
|
return self.asElement().setAttributeSafe("type", value, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getOnLoad(self: *const Script) ?js.Function {
|
pub fn getOnLoad(self: *const Script) ?js.Function {
|
||||||
return self._on_load;
|
return self._on_load;
|
||||||
}
|
}
|
||||||
@@ -95,6 +103,7 @@ pub const JsApi = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{});
|
pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{});
|
||||||
|
pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{});
|
||||||
pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{});
|
pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{});
|
||||||
pub const onerorr = bridge.accessor(Script.getOnError, Script.setOnError, .{});
|
pub const onerorr = bridge.accessor(Script.getOnError, Script.setOnError, .{});
|
||||||
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
|
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
|
||||||
|
|||||||
151
src/browser/webapi/element/html/Slot.zig
Normal file
151
src/browser/webapi/element/html/Slot.zig
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const log = @import("../../../../log.zig");
|
||||||
|
const js = @import("../../../js/js.zig");
|
||||||
|
const Page = @import("../../../Page.zig");
|
||||||
|
const Node = @import("../../Node.zig");
|
||||||
|
const Element = @import("../../Element.zig");
|
||||||
|
const HtmlElement = @import("../Html.zig");
|
||||||
|
const ShadowRoot = @import("../../ShadowRoot.zig");
|
||||||
|
|
||||||
|
const Slot = @This();
|
||||||
|
|
||||||
|
_proto: *HtmlElement,
|
||||||
|
|
||||||
|
pub fn asElement(self: *Slot) *Element {
|
||||||
|
return self._proto._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asConstElement(self: *const Slot) *const Element {
|
||||||
|
return self._proto._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asNode(self: *Slot) *Node {
|
||||||
|
return self.asElement().asNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getName(self: *const Slot) []const u8 {
|
||||||
|
return self.asConstElement().getAttributeSafe("name") orelse "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setName(self: *Slot, name: []const u8, page: *Page) !void {
|
||||||
|
try self.asElement().setAttributeSafe("name", name, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssignedNodesOptions = struct {
|
||||||
|
flatten: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn assignedNodes(self: *Slot, opts_: ?AssignedNodesOptions, page: *Page) ![]const *Node {
|
||||||
|
const opts = opts_ orelse AssignedNodesOptions{};
|
||||||
|
var nodes: std.ArrayList(*Node) = .empty;
|
||||||
|
try self.collectAssignedNodes(false, &nodes, opts, page);
|
||||||
|
return nodes.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assignedElements(self: *Slot, opts_: ?AssignedNodesOptions, page: *Page) ![]const *Element {
|
||||||
|
const opts = opts_ orelse AssignedNodesOptions{};
|
||||||
|
var elements: std.ArrayList(*Element) = .empty;
|
||||||
|
try self.collectAssignedNodes(true, &elements, opts, page);
|
||||||
|
return elements.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn CollectionType(comptime elements: bool) type {
|
||||||
|
return if (elements) *std.ArrayList(*Element) else *std.ArrayList(*Node);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectAssignedNodes(self: *Slot, comptime elements: bool, coll: CollectionType(elements), opts: AssignedNodesOptions, page: *Page) !void {
|
||||||
|
// Find the shadow root this slot belongs to
|
||||||
|
const shadow_root = self.findShadowRoot() orelse return;
|
||||||
|
|
||||||
|
const slot_name = self.getName();
|
||||||
|
const allocator = page.call_arena;
|
||||||
|
|
||||||
|
const host = shadow_root.getHost();
|
||||||
|
var it = host.asNode().childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
if (!isAssignedToSlot(child, slot_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.flatten) {
|
||||||
|
if (child.is(Slot)) |child_slot| {
|
||||||
|
// Only flatten if the child slot is actually in a shadow tree
|
||||||
|
if (child_slot.findShadowRoot()) |_| {
|
||||||
|
try child_slot.collectAssignedNodes(elements, coll, opts, page);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Otherwise, treat it as a regular element and fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime elements) {
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
try coll.append(allocator, el);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try coll.append(allocator, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assign(self: *Slot, nodes: []const *Node) void {
|
||||||
|
// Imperative slot assignment API
|
||||||
|
// This would require storing manually assigned nodes
|
||||||
|
// For now, this is a placeholder for the API
|
||||||
|
_ = self;
|
||||||
|
_ = nodes;
|
||||||
|
|
||||||
|
// let's see if this is ever actually used
|
||||||
|
log.warn(.not_implemented, "Slot.assign", .{ });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findShadowRoot(self: *Slot) ?*ShadowRoot {
|
||||||
|
// Walk up the parent chain to find the shadow root
|
||||||
|
var parent = self.asNode()._parent;
|
||||||
|
while (parent) |p| {
|
||||||
|
if (p.is(ShadowRoot)) |shadow_root| {
|
||||||
|
return shadow_root;
|
||||||
|
}
|
||||||
|
parent = p._parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isAssignedToSlot(node: *Node, slot_name: []const u8) bool {
|
||||||
|
// Check if a node should be assigned to a slot with the given name
|
||||||
|
if (node.is(Element)) |element| {
|
||||||
|
// Get the slot attribute from the element
|
||||||
|
const node_slot = element.getAttributeSafe("slot") orelse "";
|
||||||
|
|
||||||
|
// Match if:
|
||||||
|
// - Both are empty (default slot)
|
||||||
|
// - They match exactly
|
||||||
|
return std.mem.eql(u8, node_slot, slot_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text nodes, comments, etc. are only assigned to the default slot
|
||||||
|
// (when they have no preceding/following element siblings with slot attributes)
|
||||||
|
// For simplicity, text nodes go to default slot if slot_name is empty
|
||||||
|
return slot_name.len == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(Slot);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "HTMLSlotElement";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const name = bridge.accessor(Slot.getName, Slot.setName, .{});
|
||||||
|
pub const assignedNodes = bridge.function(Slot.assignedNodes, .{});
|
||||||
|
pub const assignedElements = bridge.function(Slot.assignedElements, .{});
|
||||||
|
pub const assign = bridge.function(Slot.assign, .{});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../../../testing.zig");
|
||||||
|
test "WebApi: HTMLSlotElement" {
|
||||||
|
try testing.htmlRunner("element/html/slot.html", .{});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user