Add HTMLSlotElement, PerformanceObserver and Script get/set type

This commit is contained in:
Karl Seguin
2025-11-25 19:50:53 +08:00
parent 6d6f1340af
commit be0a808f01
12 changed files with 1192 additions and 30 deletions

View File

@@ -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", .{});
}

View File

@@ -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) {

View File

@@ -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"),
});

View 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>

View 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>

View File

@@ -102,4 +102,3 @@
testing.expectEqual(24, screen.pixelDepth);
testing.expectEqual(screen, window.screen);
</script>

View File

@@ -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,

View File

@@ -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", .{});

View 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, .{});
};

View File

@@ -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]",

View File

@@ -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, .{});

View 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", .{});
}