mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 08:18:59 +00:00
improve parsed (i.e. static) custom element callbacks
This commit is contained in:
@@ -1216,6 +1216,22 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// After constructor runs, invoke attributeChangedCallback for initial attributes
|
||||||
|
const element = node.as(Element);
|
||||||
|
if (element._attributes) |attributes| {
|
||||||
|
var it = attributes.iterator();
|
||||||
|
while (it.next()) |attr| {
|
||||||
|
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(
|
||||||
|
element,
|
||||||
|
attr._name.str(),
|
||||||
|
null, // old_value is null for initial attributes
|
||||||
|
attr._value.str(),
|
||||||
|
self,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1485,6 +1501,13 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
|||||||
if (el.getAttributeSafe("id")) |id| {
|
if (el.getAttributeSafe("id")) |id| {
|
||||||
try self.addElementId(parent, el, id);
|
try self.addElementId(parent, el, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invoke connectedCallback for custom elements during parsing
|
||||||
|
// For main document parsing, we know nodes are connected (fast path)
|
||||||
|
// For fragment parsing (innerHTML), we need to check connectivity
|
||||||
|
if (self._parse_mode == .document or child.isConnected()) {
|
||||||
|
try Element.Html.Custom.invokeConnectedCallbackOnElement(true, el, self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1518,7 +1541,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (should_invoke_connected) {
|
if (should_invoke_connected) {
|
||||||
Element.Html.Custom.invokeConnectedCallbackOnElement(el, self);
|
try Element.Html.Custom.invokeConnectedCallbackOnElement(false, el, self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Slot = @import("webapi/element/html/Slot.zig");
|
||||||
|
|
||||||
pub const RootOpts = struct {
|
pub const RootOpts = struct {
|
||||||
with_base: bool = false,
|
with_base: bool = false,
|
||||||
@@ -27,11 +28,24 @@ pub const RootOpts = struct {
|
|||||||
|
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
strip: Strip = .{},
|
strip: Strip = .{},
|
||||||
|
shadow: Shadow = .rendered,
|
||||||
|
|
||||||
pub const Strip = struct {
|
pub const Strip = struct {
|
||||||
js: bool = false,
|
js: bool = false,
|
||||||
ui: bool = false,
|
ui: bool = false,
|
||||||
css: bool = false,
|
css: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Shadow = enum {
|
||||||
|
// Skip shadow DOM entirely (innerHTML/outerHTML)
|
||||||
|
skip,
|
||||||
|
|
||||||
|
// Dump everyhting (like "view source")
|
||||||
|
complete,
|
||||||
|
|
||||||
|
// Resolve slot elements (like what actually gets rendered)
|
||||||
|
rendered,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
@@ -45,10 +59,10 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deep(doc.asNode(), .{ .strip = opts.strip }, writer);
|
return deep(doc.asNode(), .{ .strip = opts.strip }, writer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void {
|
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||||
switch (node._type) {
|
switch (node._type) {
|
||||||
.cdata => |cd| try writer.writeAll(cd.getData()),
|
.cdata => |cd| try writer.writeAll(cd.getData()),
|
||||||
.element => |el| {
|
.element => |el| {
|
||||||
@@ -56,25 +70,39 @@ pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle <slot> elements in rendered mode
|
||||||
|
if (opts.shadow == .rendered) {
|
||||||
|
if (el.is(Slot)) |slot| {
|
||||||
|
return dumpSlotContent(slot, opts, writer, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try el.format(writer);
|
try el.format(writer);
|
||||||
try children(node, opts, writer);
|
|
||||||
|
if (opts.shadow != .skip) {
|
||||||
|
if (page._element_shadow_roots.get(el)) |shadow| {
|
||||||
|
try children(shadow.asNode(), opts, writer, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try children(node, opts, writer, page);
|
||||||
if (!isVoidElement(el)) {
|
if (!isVoidElement(el)) {
|
||||||
try writer.writeAll("</");
|
try writer.writeAll("</");
|
||||||
try writer.writeAll(el.getTagNameDump());
|
try writer.writeAll(el.getTagNameDump());
|
||||||
try writer.writeByte('>');
|
try writer.writeByte('>');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.document => try children(node, opts, writer),
|
.document => try children(node, opts, writer, page),
|
||||||
.document_type => {},
|
.document_type => {},
|
||||||
.document_fragment => try children(node, opts, writer),
|
.document_fragment => try children(node, opts, writer, page),
|
||||||
.attribute => unreachable,
|
.attribute => unreachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer) !void {
|
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
var it = parent.childrenIterator();
|
var it = parent.childrenIterator();
|
||||||
while (it.next()) |child| {
|
while (it.next()) |child| {
|
||||||
try deep(child, opts, writer);
|
try deep(child, opts, writer, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +146,18 @@ pub fn toJSON(node: *Node, writer: *std.json.Stringify) !void {
|
|||||||
try writer.endObject();
|
try writer.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
const assigned = slot.assignedNodes(null, page) catch return;
|
||||||
|
|
||||||
|
if (assigned.len > 0) {
|
||||||
|
for (assigned) |assigned_node| {
|
||||||
|
try deep(assigned_node, opts, writer, page);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try children(slot.asNode(), opts, writer, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn isVoidElement(el: *const Node.Element) bool {
|
fn isVoidElement(el: *const Node.Element) bool {
|
||||||
return switch (el._type) {
|
return switch (el._type) {
|
||||||
.html => |html| switch (html._type) {
|
.html => |html| switch (html._type) {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
|||||||
|
|
||||||
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
||||||
return error.JSExecCallback;
|
return error.JSExecCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,3 +91,4 @@
|
|||||||
testing.expectEqual(1, connectedCount);
|
testing.expectEqual(1, connectedCount);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
122
src/browser/tests/custom_elements/connected_from_parser.html
Normal file
122
src/browser/tests/custom_elements/connected_from_parser.html
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script>
|
||||||
|
{
|
||||||
|
// Define the custom element BEFORE the HTML is parsed
|
||||||
|
window.preParseConnectedCount = 0;
|
||||||
|
|
||||||
|
class PreParseElement extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
window.preParseConnectedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('pre-parse-element', PreParseElement);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- This element is in the HTML and should have connectedCallback invoked -->
|
||||||
|
<pre-parse-element id="static-element"></pre-parse-element>
|
||||||
|
|
||||||
|
<script id="test-static-element">
|
||||||
|
{
|
||||||
|
// connectedCallback should have been called for the element in HTML
|
||||||
|
testing.expectEqual(1, window.preParseConnectedCount);
|
||||||
|
|
||||||
|
const el = document.getElementById('static-element');
|
||||||
|
testing.expectTrue(el !== null);
|
||||||
|
testing.expectEqual('PRE-PARSE-ELEMENT', el.tagName);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="test-programmatic-still-works">
|
||||||
|
{
|
||||||
|
// Reset counter
|
||||||
|
window.preParseConnectedCount = 0;
|
||||||
|
|
||||||
|
// This should still work (programmatic creation)
|
||||||
|
const el = document.createElement('pre-parse-element');
|
||||||
|
testing.expectEqual(0, window.preParseConnectedCount);
|
||||||
|
|
||||||
|
document.body.appendChild(el);
|
||||||
|
testing.expectEqual(1, window.preParseConnectedCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{
|
||||||
|
window.nestedParentCount = 0;
|
||||||
|
window.nestedChildCount = 0;
|
||||||
|
|
||||||
|
class NestedParent extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
window.nestedParentCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NestedChild extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
window.nestedChildCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('nested-parent', NestedParent);
|
||||||
|
customElements.define('nested-child', NestedChild);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nested-parent id="parent-element">
|
||||||
|
<nested-child id="child-element"></nested-child>
|
||||||
|
</nested-parent>
|
||||||
|
|
||||||
|
<script id="verify-nested">
|
||||||
|
{
|
||||||
|
// Both parent and child should have connectedCallback invoked
|
||||||
|
testing.expectEqual(1, window.nestedParentCount);
|
||||||
|
testing.expectEqual(1, window.nestedChildCount);
|
||||||
|
|
||||||
|
const parent = document.getElementById('parent-element');
|
||||||
|
const child = document.getElementById('child-element');
|
||||||
|
testing.expectTrue(parent !== null);
|
||||||
|
testing.expectTrue(child !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{
|
||||||
|
// Test attributeChangedCallback for initial attributes during parsing
|
||||||
|
window.attrChangedCalls = [];
|
||||||
|
|
||||||
|
class AttrElement extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['foo', 'bar'];
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
|
window.attrChangedCalls.push({ name, oldValue, newValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('attr-element', AttrElement);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<attr-element foo="value1" bar="value2" ignored="value3"></attr-element>
|
||||||
|
|
||||||
|
<script id="verify-attribute-changed">
|
||||||
|
{
|
||||||
|
// attributeChangedCallback should have been called for initial attributes
|
||||||
|
testing.expectEqual(2, window.attrChangedCalls.length);
|
||||||
|
|
||||||
|
testing.expectEqual('foo', window.attrChangedCalls[0].name);
|
||||||
|
testing.expectEqual(null, window.attrChangedCalls[0].oldValue);
|
||||||
|
testing.expectEqual('value1', window.attrChangedCalls[0].newValue);
|
||||||
|
|
||||||
|
testing.expectEqual('bar', window.attrChangedCalls[1].name);
|
||||||
|
testing.expectEqual(null, window.attrChangedCalls[1].oldValue);
|
||||||
|
testing.expectEqual('value2', window.attrChangedCalls[1].newValue);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
151
src/browser/tests/shadowroot/dump.html
Normal file
151
src/browser/tests/shadowroot/dump.html
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
<script id="innerHTML_excludes_shadow">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<p>Shadow content</p>';
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// Per spec, innerHTML does NOT include shadow DOM
|
||||||
|
const html = host.innerHTML;
|
||||||
|
testing.expectEqual('', html);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="innerHTML_only_light_dom">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<p>Shadow content with <slot></slot></p>';
|
||||||
|
|
||||||
|
const lightChild = document.createElement('span');
|
||||||
|
lightChild.textContent = 'Light DOM';
|
||||||
|
host.appendChild(lightChild);
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
const html = host.innerHTML;
|
||||||
|
// innerHTML only returns light DOM, not shadow DOM
|
||||||
|
testing.expectEqual(false, html.indexOf('Shadow content') >= 0);
|
||||||
|
testing.expectEqual(true, html.indexOf('Light DOM') >= 0);
|
||||||
|
testing.expectEqual(true, html.indexOf('<span>Light DOM</span>') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="outerHTML_excludes_shadow">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
host.id = 'test-host';
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<p>Shadow content</p>';
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// outerHTML also excludes shadow DOM per spec
|
||||||
|
const html = host.outerHTML;
|
||||||
|
testing.expectEqual(true, html.indexOf('<div id="test-host">') >= 0);
|
||||||
|
testing.expectEqual(false, html.indexOf('Shadow content') >= 0);
|
||||||
|
testing.expectEqual(true, html.indexOf('</div>') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="innerHTML_closed_shadow">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'closed' });
|
||||||
|
shadow.innerHTML = '<p>Closed shadow content</p>';
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// innerHTML never includes shadow DOM (open or closed)
|
||||||
|
const html = host.innerHTML;
|
||||||
|
testing.expectEqual('', html);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="innerHTML_nested_shadow">
|
||||||
|
{
|
||||||
|
const outer = document.createElement('div');
|
||||||
|
const outerShadow = outer.attachShadow({ mode: 'open' });
|
||||||
|
outerShadow.innerHTML = '<div id="inner-host"></div>';
|
||||||
|
|
||||||
|
const innerHost = outerShadow.getElementById('inner-host');
|
||||||
|
const innerShadow = innerHost.attachShadow({ mode: 'open' });
|
||||||
|
innerShadow.innerHTML = '<p>Nested shadow content</p>';
|
||||||
|
|
||||||
|
document.body.appendChild(outer);
|
||||||
|
|
||||||
|
// innerHTML on outer doesn't include its shadow DOM
|
||||||
|
const html = outer.innerHTML;
|
||||||
|
testing.expectEqual('', html);
|
||||||
|
|
||||||
|
outer.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="shadowRoot_innerHTML">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<p>Para 1</p><p>Para 2</p>';
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
const shadowHtml = shadow.innerHTML;
|
||||||
|
testing.expectEqual(true, shadowHtml.indexOf('Para 1') >= 0);
|
||||||
|
testing.expectEqual(true, shadowHtml.indexOf('Para 2') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="innerHTML_with_light_dom">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<slot></slot>';
|
||||||
|
|
||||||
|
const lightChild = document.createElement('span');
|
||||||
|
lightChild.textContent = 'Light only';
|
||||||
|
host.appendChild(lightChild);
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// innerHTML returns light DOM children
|
||||||
|
const html = host.innerHTML;
|
||||||
|
testing.expectEqual(true, html.indexOf('Light only') >= 0);
|
||||||
|
testing.expectEqual(true, html.indexOf('<span>Light only</span>') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="shadowRoot_innerHTML_direct">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<p class="test" data-foo="bar">Shadow text</p>';
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// Accessing shadow.innerHTML directly DOES return shadow content
|
||||||
|
const html = shadow.innerHTML;
|
||||||
|
testing.expectEqual(true, html.indexOf('class="test"') >= 0);
|
||||||
|
testing.expectEqual(true, html.indexOf('data-foo="bar"') >= 0);
|
||||||
|
testing.expectEqual(true, html.indexOf('Shadow text') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
84
src/browser/tests/shadowroot/innerHTML_spec.html
Normal file
84
src/browser/tests/shadowroot/innerHTML_spec.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
<script id="innerHTML_spec_compliance">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<p>Before <slot></slot> After</p>';
|
||||||
|
|
||||||
|
const slotted = document.createElement('span');
|
||||||
|
slotted.textContent = 'SLOTTED';
|
||||||
|
host.appendChild(slotted);
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// host.innerHTML returns only light DOM (spec compliant)
|
||||||
|
const hostHTML = host.innerHTML;
|
||||||
|
testing.expectEqual(true, hostHTML.indexOf('SLOTTED') >= 0);
|
||||||
|
testing.expectEqual(false, hostHTML.indexOf('Before') >= 0);
|
||||||
|
testing.expectEqual(false, hostHTML.indexOf('<slot>') >= 0);
|
||||||
|
|
||||||
|
// shadow.innerHTML returns shadow DOM content
|
||||||
|
const shadowHTML = shadow.innerHTML;
|
||||||
|
testing.expectEqual(true, shadowHTML.indexOf('Before') >= 0);
|
||||||
|
testing.expectEqual(true, shadowHTML.indexOf('<slot>') >= 0);
|
||||||
|
testing.expectEqual(false, shadowHTML.indexOf('SLOTTED') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="innerHTML_named_slots">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
shadow.innerHTML = '<slot name="header"></slot><slot name="footer"></slot>';
|
||||||
|
|
||||||
|
const header = document.createElement('h1');
|
||||||
|
header.setAttribute('slot', 'header');
|
||||||
|
header.textContent = 'Header Content';
|
||||||
|
host.appendChild(header);
|
||||||
|
|
||||||
|
const footer = document.createElement('p');
|
||||||
|
footer.setAttribute('slot', 'footer');
|
||||||
|
footer.textContent = 'Footer Content';
|
||||||
|
host.appendChild(footer);
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// host.innerHTML returns light DOM with slot attributes
|
||||||
|
const hostHTML = host.innerHTML;
|
||||||
|
testing.expectEqual(true, hostHTML.indexOf('Header Content') >= 0);
|
||||||
|
testing.expectEqual(true, hostHTML.indexOf('Footer Content') >= 0);
|
||||||
|
testing.expectEqual(true, hostHTML.indexOf('slot="header"') >= 0);
|
||||||
|
testing.expectEqual(true, hostHTML.indexOf('slot="footer"') >= 0);
|
||||||
|
|
||||||
|
// shadow.innerHTML returns slot elements
|
||||||
|
const shadowHTML = shadow.innerHTML;
|
||||||
|
testing.expectEqual(true, shadowHTML.indexOf('<slot name="header"></slot>') >= 0);
|
||||||
|
testing.expectEqual(true, shadowHTML.indexOf('<slot name="footer"></slot>') >= 0);
|
||||||
|
testing.expectEqual(false, shadowHTML.indexOf('Header Content') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="innerHTML_no_shadow">
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const child = document.createElement('span');
|
||||||
|
child.textContent = 'Regular content';
|
||||||
|
host.appendChild(child);
|
||||||
|
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// Without shadow DOM, innerHTML works normally
|
||||||
|
const html = host.innerHTML;
|
||||||
|
testing.expectEqual(true, html.indexOf('Regular content') >= 0);
|
||||||
|
testing.expectEqual(true, html.indexOf('<span>') >= 0);
|
||||||
|
|
||||||
|
host.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -84,7 +84,6 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
|
|||||||
var idx: usize = 0;
|
var idx: usize = 0;
|
||||||
while (idx < page._undefined_custom_elements.items.len) {
|
while (idx < page._undefined_custom_elements.items.len) {
|
||||||
const custom = page._undefined_custom_elements.items[idx];
|
const custom = page._undefined_custom_elements.items[idx];
|
||||||
|
|
||||||
if (!custom._tag_name.eqlSlice(name)) {
|
if (!custom._tag_name.eqlSlice(name)) {
|
||||||
idx += 1;
|
idx += 1;
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -160,9 +160,9 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer) !void {
|
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
const dump = @import("../dump.zig");
|
const dump = @import("../dump.zig");
|
||||||
return dump.children(self.asNode(), .{}, writer);
|
return dump.children(self.asNode(), .{ .shadow = .complete }, writer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, page: *Page) !void {
|
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, page: *Page) !void {
|
||||||
@@ -224,7 +224,7 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 {
|
fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 {
|
||||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||||
try self.getInnerHTML(&buf.writer);
|
try self.getInnerHTML(&buf.writer, page);
|
||||||
return buf.written();
|
return buf.written();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -227,14 +227,14 @@ pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer) !void {
|
pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
const dump = @import("../dump.zig");
|
const dump = @import("../dump.zig");
|
||||||
return dump.deep(self.asNode(), .{}, writer);
|
return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer) !void {
|
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
const dump = @import("../dump.zig");
|
const dump = @import("../dump.zig");
|
||||||
return dump.children(self.asNode(), .{}, writer);
|
return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {
|
pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {
|
||||||
@@ -906,14 +906,14 @@ pub const JsApi = struct {
|
|||||||
pub const outerHTML = bridge.accessor(_outerHTML, null, .{});
|
pub const outerHTML = bridge.accessor(_outerHTML, null, .{});
|
||||||
fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
|
fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
|
||||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||||
try self.getOuterHTML(&buf.writer);
|
try self.getOuterHTML(&buf.writer, page);
|
||||||
return buf.written();
|
return buf.written();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
|
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
|
||||||
fn _innerHTML(self: *Element, page: *Page) ![]const u8 {
|
fn _innerHTML(self: *Element, page: *Page) ![]const u8 {
|
||||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||||
try self.getInnerHTML(&buf.writer);
|
try self.getInnerHTML(&buf.writer, page);
|
||||||
return buf.written();
|
return buf.written();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ pub fn invokeConnectedCallback(self: *Custom, page: *Page) void {
|
|||||||
|
|
||||||
pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void {
|
pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void {
|
||||||
// Only invoke if we haven't already called it while disconnected
|
// Only invoke if we haven't already called it while disconnected
|
||||||
if (self._disconnected_callback_invoked) return;
|
if (self._disconnected_callback_invoked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self._disconnected_callback_invoked = true;
|
self._disconnected_callback_invoked = true;
|
||||||
self._connected_callback_invoked = false;
|
self._connected_callback_invoked = false;
|
||||||
@@ -62,30 +64,49 @@ pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void {
|
|||||||
|
|
||||||
pub fn invokeAttributeChangedCallback(self: *Custom, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void {
|
pub fn invokeAttributeChangedCallback(self: *Custom, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void {
|
||||||
const definition = self._definition orelse return;
|
const definition = self._definition orelse return;
|
||||||
if (!definition.isAttributeObserved(name)) return;
|
if (!definition.isAttributeObserved(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value }, page);
|
self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value }, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static helpers that work on any Element (autonomous or customized built-in)
|
pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, page: *Page) !void {
|
||||||
pub fn invokeConnectedCallbackOnElement(element: *Element, page: *Page) void {
|
|
||||||
// Autonomous custom element
|
// Autonomous custom element
|
||||||
if (element.is(Custom)) |custom| {
|
if (element.is(Custom)) |custom| {
|
||||||
custom.invokeConnectedCallback(page);
|
if (comptime from_parser) {
|
||||||
|
// From parser, we know the element is brand new
|
||||||
|
custom._connected_callback_invoked = true;
|
||||||
|
custom.invokeCallback("connectedCallback", .{}, page);
|
||||||
|
} else {
|
||||||
|
custom.invokeConnectedCallback(page);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customized built-in element
|
// Customized built-in element - check if it actually has a definition first
|
||||||
// Check if we've already invoked connectedCallback while connected
|
const definition = page.getCustomizedBuiltInDefinition(element) orelse return;
|
||||||
if (page._customized_builtin_connected_callback_invoked.contains(element)) return;
|
|
||||||
|
if (comptime from_parser) {
|
||||||
|
// From parser, we know the element is brand new, skip the tracking check
|
||||||
|
try page._customized_builtin_connected_callback_invoked.put(
|
||||||
|
page.arena,
|
||||||
|
element,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Not from parser, check if we've already invoked while connected
|
||||||
|
const gop = try page._customized_builtin_connected_callback_invoked.getOrPut(
|
||||||
|
page.arena,
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
if (gop.found_existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* = {};
|
||||||
|
}
|
||||||
|
|
||||||
page._customized_builtin_connected_callback_invoked.put(
|
|
||||||
page.arena,
|
|
||||||
element,
|
|
||||||
{},
|
|
||||||
) catch return;
|
|
||||||
_ = page._customized_builtin_disconnected_callback_invoked.remove(element);
|
_ = page._customized_builtin_disconnected_callback_invoked.remove(element);
|
||||||
|
invokeCallbackOnElement(element, definition, "connectedCallback", .{}, page);
|
||||||
invokeCallbackOnElement(element, "connectedCallback", .{}, page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void {
|
pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void {
|
||||||
@@ -95,18 +116,20 @@ pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customized built-in element
|
// Customized built-in element - check if it actually has a definition first
|
||||||
// Check if we've already invoked disconnectedCallback while disconnected
|
const definition = page.getCustomizedBuiltInDefinition(element) orelse return;
|
||||||
if (page._customized_builtin_disconnected_callback_invoked.contains(element)) return;
|
|
||||||
|
|
||||||
page._customized_builtin_disconnected_callback_invoked.put(
|
// Check if we've already invoked disconnectedCallback while disconnected
|
||||||
|
const gop = page._customized_builtin_disconnected_callback_invoked.getOrPut(
|
||||||
page.arena,
|
page.arena,
|
||||||
element,
|
element,
|
||||||
{},
|
|
||||||
) catch return;
|
) catch return;
|
||||||
|
if (gop.found_existing) return;
|
||||||
|
gop.value_ptr.* = {};
|
||||||
|
|
||||||
_ = page._customized_builtin_connected_callback_invoked.remove(element);
|
_ = page._customized_builtin_connected_callback_invoked.remove(element);
|
||||||
|
|
||||||
invokeCallbackOnElement(element, "disconnectedCallback", .{}, page);
|
invokeCallbackOnElement(element, definition, "disconnectedCallback", .{}, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void {
|
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void {
|
||||||
@@ -119,12 +142,11 @@ pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const
|
|||||||
// Customized built-in element - check if attribute is observed
|
// Customized built-in element - check if attribute is observed
|
||||||
const definition = page.getCustomizedBuiltInDefinition(element) orelse return;
|
const definition = page.getCustomizedBuiltInDefinition(element) orelse return;
|
||||||
if (!definition.isAttributeObserved(name)) return;
|
if (!definition.isAttributeObserved(name)) return;
|
||||||
invokeCallbackOnElement(element, "attributeChangedCallback", .{ name, old_value, new_value }, page);
|
invokeCallbackOnElement(element, definition, "attributeChangedCallback", .{ name, old_value, new_value }, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invokeCallbackOnElement(element: *Element, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
|
fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
|
||||||
// Check if this element has a customized built-in definition
|
_ = definition;
|
||||||
_ = page.getCustomizedBuiltInDefinition(element) orelse return;
|
|
||||||
|
|
||||||
const context = page.js;
|
const context = page.js;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user