mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 23:38:57 +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 => {},
|
||||
},
|
||||
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(
|
||||
Element.Html.Meta,
|
||||
namespace,
|
||||
@@ -1036,6 +1042,12 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
asUint("slot") => return self.createHtmlElementT(
|
||||
Element.Html.Slot,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
asUint("html") => return self.createHtmlElementT(
|
||||
Element.Html.Html,
|
||||
namespace,
|
||||
@@ -1066,12 +1078,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
||||
attribute_iterator,
|
||||
.{ ._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 => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(name[0..5].*))) {
|
||||
@@ -1787,3 +1793,7 @@ const testing = @import("../testing.zig");
|
||||
test "WebApi: 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,
|
||||
});
|
||||
|
||||
log.debug(.http, "script queue", .{
|
||||
.ctx = ctx,
|
||||
.url = remote_url.?,
|
||||
.stack = page.js.stackTrace() catch "???",
|
||||
});
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "script queue", .{
|
||||
.ctx = ctx,
|
||||
.url = remote_url.?,
|
||||
.element = element,
|
||||
.stack = page.js.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "module",
|
||||
.referrer = referrer,
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "module",
|
||||
.referrer = referrer,
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
|
||||
try self.client.request(.{
|
||||
.url = url,
|
||||
@@ -403,12 +408,14 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "dynamic module",
|
||||
.referrer = referrer,
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "dynamic module",
|
||||
.referrer = referrer,
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
|
||||
// It's possible, but unlikely, for client.request to immediately finish
|
||||
// a request, thus calling our callback. We generally don't want a call
|
||||
@@ -617,11 +624,13 @@ const Script = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -649,7 +658,9 @@ const Script = struct {
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
const self: *Script = @ptrCast(@alignCast(ctx));
|
||||
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;
|
||||
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/Script.zig"),
|
||||
@import("../webapi/element/html/Select.zig"),
|
||||
@import("../webapi/element/html/Slot.zig"),
|
||||
@import("../webapi/element/html/Style.zig"),
|
||||
@import("../webapi/element/html/Template.zig"),
|
||||
@import("../webapi/element/html/TextArea.zig"),
|
||||
@@ -574,4 +575,5 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/Blob.zig"),
|
||||
@import("../webapi/File.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(screen, window.screen);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
||||
.p => "p",
|
||||
.script => "script",
|
||||
.select => "select",
|
||||
.slot => "slot",
|
||||
.style => "style",
|
||||
.template => "template",
|
||||
.text_area => "textarea",
|
||||
@@ -192,6 +193,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
.p => "P",
|
||||
.script => "SCRIPT",
|
||||
.select => "SELECT",
|
||||
.slot => "SLOT",
|
||||
.style => "STYLE",
|
||||
.template => "TEMPLATE",
|
||||
.text_area => "TEXTAREA",
|
||||
@@ -790,6 +792,7 @@ pub fn getTag(self: *const Element) Tag {
|
||||
.generic => |g| g._tag,
|
||||
.script => .script,
|
||||
.select => .select,
|
||||
.slot => .slot,
|
||||
.option => .option,
|
||||
.template => .template,
|
||||
.text_area => .textarea,
|
||||
@@ -855,6 +858,7 @@ pub const Tag = enum {
|
||||
rect,
|
||||
script,
|
||||
select,
|
||||
slot,
|
||||
span,
|
||||
strong,
|
||||
style,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const js = @import("../js/js.zig");
|
||||
const datetime = @import("../../datetime.zig");
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{
|
||||
Performance,
|
||||
Entry,
|
||||
};
|
||||
}
|
||||
|
||||
const Performance = @This();
|
||||
|
||||
_time_origin: u64,
|
||||
@@ -34,6 +41,65 @@ pub const JsApi = struct {
|
||||
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");
|
||||
test "WebApi: Performance" {
|
||||
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 Paragraph = @import("html/Paragraph.zig");
|
||||
pub const Select = @import("html/Select.zig");
|
||||
pub const Slot = @import("html/Slot.zig");
|
||||
pub const Option = @import("html/Option.zig");
|
||||
pub const IFrame = @import("html/IFrame.zig");
|
||||
|
||||
@@ -90,6 +91,7 @@ pub const Type = union(enum) {
|
||||
p: Paragraph,
|
||||
script: *Script,
|
||||
select: Select,
|
||||
slot: Slot,
|
||||
style: Style,
|
||||
template: *Template,
|
||||
text_area: *TextArea,
|
||||
@@ -131,6 +133,7 @@ pub fn className(self: *const HtmlElement) []const u8 {
|
||||
.generic => "[object HTMLElement]",
|
||||
.script => "[object HtmlScriptElement]",
|
||||
.select => "[object HTMLSelectElement]",
|
||||
.slot => "[object HTMLSlotElement]",
|
||||
.template => "[object HTMLTemplateElement]",
|
||||
.option => "[object HTMLOptionElement]",
|
||||
.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 {
|
||||
return self._on_load;
|
||||
}
|
||||
@@ -95,6 +103,7 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
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 onerorr = bridge.accessor(Script.getOnError, Script.setOnError, .{});
|
||||
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