mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +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;
|
||||
};
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1485,6 +1501,13 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
||||
if (el.getAttributeSafe("id")) |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;
|
||||
}
|
||||
@@ -1518,7 +1541,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
||||
}
|
||||
|
||||
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 Page = @import("Page.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Slot = @import("webapi/element/html/Slot.zig");
|
||||
|
||||
pub const RootOpts = struct {
|
||||
with_base: bool = false,
|
||||
@@ -27,11 +28,24 @@ pub const RootOpts = struct {
|
||||
|
||||
pub const Opts = struct {
|
||||
strip: Strip = .{},
|
||||
shadow: Shadow = .rendered,
|
||||
|
||||
pub const Strip = struct {
|
||||
js: bool = false,
|
||||
ui: 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 {
|
||||
@@ -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) {
|
||||
.cdata => |cd| try writer.writeAll(cd.getData()),
|
||||
.element => |el| {
|
||||
@@ -56,25 +70,39 @@ pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!
|
||||
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 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)) {
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(el.getTagNameDump());
|
||||
try writer.writeByte('>');
|
||||
}
|
||||
},
|
||||
.document => try children(node, opts, writer),
|
||||
.document => try children(node, opts, writer, page),
|
||||
.document_type => {},
|
||||
.document_fragment => try children(node, opts, writer),
|
||||
.document_fragment => try children(node, opts, writer, page),
|
||||
.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();
|
||||
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
return switch (el._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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,3 +91,4 @@
|
||||
testing.expectEqual(1, connectedCount);
|
||||
}
|
||||
</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;
|
||||
while (idx < page._undefined_custom_elements.items.len) {
|
||||
const custom = page._undefined_custom_elements.items[idx];
|
||||
|
||||
if (!custom._tag_name.eqlSlice(name)) {
|
||||
idx += 1;
|
||||
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");
|
||||
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 {
|
||||
@@ -224,7 +224,7 @@ pub const JsApi = struct {
|
||||
|
||||
fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try self.getInnerHTML(&buf.writer);
|
||||
try self.getInnerHTML(&buf.writer, page);
|
||||
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");
|
||||
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");
|
||||
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 {
|
||||
@@ -906,14 +906,14 @@ pub const JsApi = struct {
|
||||
pub const outerHTML = bridge.accessor(_outerHTML, null, .{});
|
||||
fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try self.getOuterHTML(&buf.writer);
|
||||
try self.getOuterHTML(&buf.writer, page);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
|
||||
fn _innerHTML(self: *Element, page: *Page) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try self.getInnerHTML(&buf.writer);
|
||||
try self.getInnerHTML(&buf.writer, page);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ pub fn invokeConnectedCallback(self: *Custom, page: *Page) void {
|
||||
|
||||
pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void {
|
||||
// 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._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 {
|
||||
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);
|
||||
}
|
||||
|
||||
// Static helpers that work on any Element (autonomous or customized built-in)
|
||||
pub fn invokeConnectedCallbackOnElement(element: *Element, page: *Page) void {
|
||||
pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, page: *Page) !void {
|
||||
// Autonomous custom element
|
||||
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;
|
||||
}
|
||||
|
||||
// Customized built-in element
|
||||
// Check if we've already invoked connectedCallback while connected
|
||||
if (page._customized_builtin_connected_callback_invoked.contains(element)) return;
|
||||
// Customized built-in element - check if it actually has a definition first
|
||||
const definition = page.getCustomizedBuiltInDefinition(element) orelse 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);
|
||||
|
||||
invokeCallbackOnElement(element, "connectedCallback", .{}, page);
|
||||
invokeCallbackOnElement(element, definition, "connectedCallback", .{}, page);
|
||||
}
|
||||
|
||||
pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void {
|
||||
@@ -95,18 +116,20 @@ pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void
|
||||
return;
|
||||
}
|
||||
|
||||
// Customized built-in element
|
||||
// Check if we've already invoked disconnectedCallback while disconnected
|
||||
if (page._customized_builtin_disconnected_callback_invoked.contains(element)) return;
|
||||
// Customized built-in element - check if it actually has a definition first
|
||||
const definition = page.getCustomizedBuiltInDefinition(element) orelse 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,
|
||||
element,
|
||||
{},
|
||||
) catch return;
|
||||
if (gop.found_existing) return;
|
||||
gop.value_ptr.* = {};
|
||||
|
||||
_ = 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 {
|
||||
@@ -119,12 +142,11 @@ pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const
|
||||
// Customized built-in element - check if attribute is observed
|
||||
const definition = page.getCustomizedBuiltInDefinition(element) orelse 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 {
|
||||
// Check if this element has a customized built-in definition
|
||||
_ = page.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
|
||||
_ = definition;
|
||||
|
||||
const context = page.js;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user