mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge pull request #2233 from lightpanda-io/custom_elements
Custom elements
This commit is contained in:
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.4.0'
|
||||
default: 'v0.4.1'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.4.0
|
||||
ARG ZIG_V8=v0.4.1
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.0.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH61yIBAD04dV4CHW0qIFiqbOGvkN_-amGdmgbQ3dU",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.1.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH672HBAA1hQIa2Uv4mzs_qHC9-Py-M5ssqSSVhWtK",
|
||||
},
|
||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||
.brotli = .{
|
||||
|
||||
@@ -1667,11 +1667,17 @@ pub fn setNodeOwnerDocument(self: *Frame, node: *Node, owner: *Document) !void {
|
||||
}
|
||||
|
||||
// Recursively sets the owner document for a node and all its descendants
|
||||
pub fn adoptNodeTree(self: *Frame, node: *Node, new_owner: *Document) !void {
|
||||
pub fn adoptNodeTree(self: *Frame, node: *Node, old_owner: *Document, new_owner: *Document) !void {
|
||||
try self.setNodeOwnerDocument(node, new_owner);
|
||||
|
||||
// Per spec, adopted steps run on each element after its document is set.
|
||||
if (node.is(Element)) |el| {
|
||||
Element.Html.Custom.invokeAdoptedCallbackOnElement(el, old_owner, new_owner, self);
|
||||
}
|
||||
|
||||
var it = node.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
try self.adoptNodeTree(child, new_owner);
|
||||
try self.adoptNodeTree(child, old_owner, new_owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2373,6 +2379,7 @@ pub fn createElementNS(self: *Frame, namespace: Element.Namespace, name: []const
|
||||
attr._name,
|
||||
null, // old_value is null for initial attributes
|
||||
attr._value,
|
||||
null,
|
||||
self,
|
||||
);
|
||||
}
|
||||
@@ -2469,6 +2476,35 @@ fn populateElementAttributes(self: *Frame, element: *Element, list: anytype) !vo
|
||||
}
|
||||
}
|
||||
|
||||
// Called when `new MyElement()` is invoked directly in JS (not via the
|
||||
// customElements.define/upgrade path). `new_target` is the constructor
|
||||
// function that was used with `new`. We find the matching definition in the
|
||||
// registry by function identity and allocate a detached Custom element with
|
||||
// the registered tag name.
|
||||
pub fn constructCustomElement(self: *Frame, new_target: JS.Function) !*Element {
|
||||
var it = self.window._custom_elements._definitions.iterator();
|
||||
const definition = while (it.next()) |entry| {
|
||||
if (entry.value_ptr.*.constructor.isEqual(new_target)) {
|
||||
break entry.value_ptr.*;
|
||||
}
|
||||
} else return error.IllegalConstructor;
|
||||
|
||||
// Customized built-ins (`class Foo extends HTMLDivElement`, etc.) would
|
||||
// need to allocate the extended HTML type rather than Custom. Not yet
|
||||
// supported via direct `new` — upgrade path still works for those.
|
||||
if (definition.isCustomizedBuiltIn()) {
|
||||
return error.IllegalConstructor;
|
||||
}
|
||||
|
||||
const tag_name = try String.init(self.arena, definition.name, .{});
|
||||
const node = try self.createHtmlElementT(Element.Html.Custom, .html, @as(?*Element.Attribute.List, null), .{
|
||||
._proto = undefined,
|
||||
._tag_name = tag_name,
|
||||
._definition = definition,
|
||||
});
|
||||
return node.as(Element);
|
||||
}
|
||||
|
||||
pub fn createTextNode(self: *Frame, text: []const u8) !*Node {
|
||||
const cd = try self._factory.node(CData{
|
||||
._proto = undefined,
|
||||
@@ -2914,7 +2950,10 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
}
|
||||
|
||||
if (opts.child_already_connected and !opts.adopting_to_new_document) {
|
||||
// The child is already connected in the same document, we don't have to reconnect it
|
||||
// The child is already connected in the same document, we don't have to reconnect it.
|
||||
// On cross-document adoption the child has already fired
|
||||
// disconnectedCallback against the old tree and must re-fire
|
||||
// connectedCallback for the new tree, so we fall through.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2932,7 +2971,10 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
// Only invoke connectedCallback if the root child is transitioning from
|
||||
// disconnected to connected. When that happens, all descendants should also
|
||||
// get connectedCallback invoked (they're becoming connected as a group).
|
||||
const should_invoke_connected = parent_is_connected and !opts.child_already_connected;
|
||||
// Cross-document adoption also counts as a transition: the element fired
|
||||
// disconnectedCallback against the old tree during removeNode and must
|
||||
// now fire connectedCallback against the new tree.
|
||||
const should_invoke_connected = parent_is_connected and (!opts.child_already_connected or opts.adopting_to_new_document);
|
||||
|
||||
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
|
||||
while (tw.next()) |el| {
|
||||
@@ -2951,7 +2993,7 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str
|
||||
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self);
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, null, self);
|
||||
|
||||
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
@@ -2977,7 +3019,7 @@ pub fn attributeRemove(self: *Frame, element: *Element, name: String, old_value:
|
||||
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self);
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, null, self);
|
||||
|
||||
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
|
||||
@@ -110,6 +110,10 @@ pub const CallOpts = struct {
|
||||
dom_exception: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
as_typed_array: bool = false,
|
||||
// Constructor-only. When true, `new.target` is pulled from the
|
||||
// FunctionCallbackInfo and passed as the first argument to the Zig
|
||||
// function (as a js.Function). See bridge.Constructor.Opts.
|
||||
new_target: bool = false,
|
||||
};
|
||||
|
||||
pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
@@ -126,15 +130,20 @@ pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *cons
|
||||
return;
|
||||
}
|
||||
|
||||
self._constructor(func, info) catch |err| {
|
||||
self._constructor(func, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
};
|
||||
}
|
||||
|
||||
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
|
||||
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||
const F = @TypeOf(func);
|
||||
const local = &self.local;
|
||||
const args = try getArgs(F, 0, local, info);
|
||||
const offset: comptime_int = if (opts.new_target) 1 else 0;
|
||||
var args = try getArgs(F, offset, local, info);
|
||||
if (comptime opts.new_target) {
|
||||
const new_target_handle = v8.v8__FunctionCallbackInfo__NewTarget(info.handle).?;
|
||||
@field(args, "0") = js.Function{ .local = local, .handle = @ptrCast(new_target_handle) };
|
||||
}
|
||||
const res = @call(.auto, func, args);
|
||||
|
||||
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
|
||||
|
||||
@@ -111,6 +111,10 @@ pub const Constructor = struct {
|
||||
|
||||
const Opts = struct {
|
||||
dom_exception: bool = false,
|
||||
// When true, the constructor function receives `new.target` (as a
|
||||
// js.Function) as its first parameter. Used by HTMLElement to support
|
||||
// direct instantiation of custom elements via `new MyElement()`.
|
||||
new_target: bool = false,
|
||||
};
|
||||
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
|
||||
@@ -125,6 +129,7 @@ pub const Constructor = struct {
|
||||
|
||||
caller.constructor(T, func, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.new_target = opts.new_target,
|
||||
});
|
||||
}
|
||||
}.wrap };
|
||||
|
||||
48
src/browser/tests/custom_elements/adopted.html
Normal file
48
src/browser/tests/custom_elements/adopted.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="adopted">
|
||||
{
|
||||
let calls = [];
|
||||
|
||||
class MyElement extends HTMLElement {
|
||||
connectedCallback() { calls.push('connected'); }
|
||||
adoptedCallback(oldDoc, newDoc) {
|
||||
calls.push('adopted');
|
||||
calls.push(oldDoc === document);
|
||||
calls.push(newDoc === otherDoc);
|
||||
}
|
||||
disconnectedCallback() { calls.push('disconnected'); }
|
||||
}
|
||||
customElements.define('my-adopted-element', MyElement);
|
||||
|
||||
const otherDoc = document.implementation.createHTMLDocument('other');
|
||||
|
||||
// Case 1: adopting a detached element into another document
|
||||
{
|
||||
const el = document.createElement('my-adopted-element');
|
||||
calls = [];
|
||||
otherDoc.body.appendChild(el);
|
||||
testing.expectEqual('adopted,true,true,connected', calls.join(','));
|
||||
}
|
||||
|
||||
// Case 2: moving a connected element into another document fires
|
||||
// disconnected -> adopted -> connected in order.
|
||||
{
|
||||
const el = document.createElement('my-adopted-element');
|
||||
document.body.appendChild(el);
|
||||
calls = [];
|
||||
otherDoc.body.appendChild(el);
|
||||
testing.expectEqual('disconnected,adopted,true,true,connected', calls.join(','));
|
||||
}
|
||||
|
||||
// Case 3: appending into the same document does NOT fire adopted.
|
||||
{
|
||||
const el = document.createElement('my-adopted-element');
|
||||
calls = [];
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual('connected', calls.join(','));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
@@ -52,6 +52,40 @@
|
||||
const el = document.createElement('no-constructor-element');
|
||||
testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);
|
||||
}
|
||||
|
||||
{
|
||||
// Direct instantiation: `new MyElement()` should work for a registered
|
||||
// autonomous custom element.
|
||||
class DirectInstantiation extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.init = 'direct';
|
||||
}
|
||||
}
|
||||
customElements.define('direct-instantiation', DirectInstantiation);
|
||||
|
||||
const el = new DirectInstantiation();
|
||||
testing.expectEqual(true, el instanceof DirectInstantiation);
|
||||
testing.expectEqual(true, el instanceof HTMLElement);
|
||||
testing.expectEqual('direct-instantiation', el.localName);
|
||||
testing.expectEqual('DIRECT-INSTANTIATION', el.tagName);
|
||||
testing.expectEqual('direct', el.init);
|
||||
}
|
||||
|
||||
{
|
||||
// `new HTMLElement()` directly is illegal (no registered constructor).
|
||||
let threw = false;
|
||||
try { new HTMLElement(); } catch (e) { threw = true; }
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
|
||||
{
|
||||
// Unregistered subclass of HTMLElement is also illegal.
|
||||
class Unregistered extends HTMLElement {}
|
||||
let threw = false;
|
||||
try { new Unregistered(); } catch (e) { threw = true; }
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=clone_container></div>
|
||||
|
||||
@@ -195,7 +195,7 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
|
||||
while (attr_it.next()) |attr| {
|
||||
const name = attr._name;
|
||||
if (definition.isAttributeObserved(name)) {
|
||||
custom.invokeAttributeChangedCallback(name, null, attr._value, frame);
|
||||
custom.invokeAttributeChangedCallback(name, null, attr._value, null, frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,12 +241,16 @@ pub fn appendChild(self: *Node, child: *Node, frame: *Frame) !*Node {
|
||||
if (child._parent) |parent| {
|
||||
// we can signal removeNode that the child will remain connected
|
||||
// (when it's appended to self) so that it can be a bit more efficient.
|
||||
frame.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() });
|
||||
// But on cross-document moves the child must fully disconnect from the
|
||||
// source document (firing disconnectedCallback) before adoption.
|
||||
frame.removeNode(parent, child, .{
|
||||
.will_be_reconnected = self.isConnected() and !adopting_to_new_document,
|
||||
});
|
||||
}
|
||||
|
||||
// Adopt the node tree if moving between documents
|
||||
if (adopting_to_new_document) {
|
||||
try frame.adoptNodeTree(child, parent_owner);
|
||||
try frame.adoptNodeTree(child, child_owner.?, parent_owner);
|
||||
}
|
||||
|
||||
try frame.appendNode(self, child, .{
|
||||
@@ -591,14 +595,14 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
|
||||
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
|
||||
|
||||
frame.domChanged();
|
||||
const will_be_reconnected = self.isConnected();
|
||||
const will_be_reconnected = self.isConnected() and !adopting_to_new_document;
|
||||
if (new_node._parent) |parent| {
|
||||
frame.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });
|
||||
}
|
||||
|
||||
// Adopt the node tree if moving between documents
|
||||
if (adopting_to_new_document) {
|
||||
try frame.adoptNodeTree(new_node, parent_owner);
|
||||
try frame.adoptNodeTree(new_node, child_owner.?, parent_owner);
|
||||
}
|
||||
|
||||
try frame.insertNodeRelative(
|
||||
|
||||
@@ -103,10 +103,17 @@ const HtmlElement = @This();
|
||||
_type: Type,
|
||||
_proto: *Element,
|
||||
|
||||
// Special constructor for custom elements
|
||||
pub fn construct(frame: *Frame) !*Element {
|
||||
const node = frame._upgrading_element orelse return error.IllegalConstructor;
|
||||
return node.is(Element) orelse return error.IllegalConstructor;
|
||||
// Special constructor for custom elements.
|
||||
// Two paths:
|
||||
// - Upgrade path: customElements.define / createElement / upgrade set
|
||||
// `_upgrading_element` before calling newInstance, and we just return it.
|
||||
// - Direct path: `new MyElement()` from user code. `new.target` tells us
|
||||
// which custom element class was invoked; look it up in the registry.
|
||||
pub fn construct(new_target: js.Function, frame: *Frame) !*Element {
|
||||
if (frame._upgrading_element) |node| {
|
||||
return node.is(Element) orelse return error.IllegalConstructor;
|
||||
}
|
||||
return frame.constructCustomElement(new_target);
|
||||
}
|
||||
|
||||
pub const Type = union(enum) {
|
||||
@@ -286,42 +293,15 @@ pub fn insertAdjacentHTML(
|
||||
html: []const u8,
|
||||
frame: *Frame,
|
||||
) !void {
|
||||
|
||||
// Create a new HTMLDocument.
|
||||
const doc = try frame._factory.document(@import("../HTMLDocument.zig"){
|
||||
._proto = undefined,
|
||||
});
|
||||
const doc_node = doc.asNode();
|
||||
|
||||
const arena = try frame.getArena(.medium, "HTML.insertAdjacentHTML");
|
||||
defer frame.releaseArena(arena);
|
||||
|
||||
const Parser = @import("../../parser/Parser.zig");
|
||||
var parser = Parser.init(arena, doc_node, frame);
|
||||
parser.parse(html);
|
||||
|
||||
// Check if there's parsing error.
|
||||
if (parser.err) |_| {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
// The parser wraps content in a document structure:
|
||||
// - Typical: <html><head>...</head><body>...</body></html>
|
||||
// - Head-only: <html><head><meta></head></html> (no body)
|
||||
// - Empty/comments: May have no <html> element at all
|
||||
const html_node = doc_node.firstChild() orelse return;
|
||||
const DocumentFragment = @import("../DocumentFragment.zig");
|
||||
const fragment = (try DocumentFragment.init(frame)).asNode();
|
||||
try frame.parseHtmlAsChildren(fragment, html);
|
||||
|
||||
const target_node, const prev_node = try self.asElement().asNode().findAdjacentNodes(position);
|
||||
|
||||
// Iterate through all children of <html> (typically <head> and/or <body>)
|
||||
// and insert their children (not the containers themselves) into the target.
|
||||
// This handles both body content AND head-only elements like <meta>, <title>, etc.
|
||||
var html_children = html_node.childrenIterator();
|
||||
while (html_children.next()) |container| {
|
||||
var iter = container.childrenIterator();
|
||||
while (iter.next()) |child_node| {
|
||||
_ = try target_node.insertBefore(child_node, prev_node, frame);
|
||||
}
|
||||
var iter = fragment.childrenIterator();
|
||||
while (iter.next()) |child_node| {
|
||||
_ = try target_node.insertBefore(child_node, prev_node, frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,7 +1205,7 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(HtmlElement.construct, .{});
|
||||
pub const constructor = bridge.constructor(HtmlElement.construct, .{ .new_target = true });
|
||||
|
||||
pub const innerText = bridge.accessor(_innerText, HtmlElement.setInnerText, .{});
|
||||
fn _innerText(self: *HtmlElement, frame: *const Frame) ![]const u8 {
|
||||
|
||||
@@ -24,6 +24,7 @@ const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const Document = @import("../../Document.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const CustomElementDefinition = @import("../../CustomElementDefinition.zig");
|
||||
|
||||
@@ -66,12 +67,25 @@ pub fn invokeDisconnectedCallback(self: *Custom, frame: *Frame) void {
|
||||
self.invokeCallback("disconnectedCallback", .{}, frame);
|
||||
}
|
||||
|
||||
pub fn invokeAttributeChangedCallback(self: *Custom, name: String, old_value: ?String, new_value: ?String, frame: *Frame) void {
|
||||
pub fn invokeAttributeChangedCallback(self: *Custom, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
|
||||
const definition = self._definition orelse return;
|
||||
if (!definition.isAttributeObserved(name)) {
|
||||
return;
|
||||
}
|
||||
self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value }, frame);
|
||||
self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value, namespace }, frame);
|
||||
}
|
||||
|
||||
pub fn invokeAdoptedCallback(self: *Custom, old_document: *Document, new_document: *Document, frame: *Frame) void {
|
||||
self.invokeCallback("adoptedCallback", .{ old_document, new_document }, frame);
|
||||
}
|
||||
|
||||
pub fn invokeAdoptedCallbackOnElement(element: *Element, old_document: *Document, new_document: *Document, frame: *Frame) void {
|
||||
if (element.is(Custom)) |custom| {
|
||||
custom.invokeAdoptedCallback(old_document, new_document, frame);
|
||||
return;
|
||||
}
|
||||
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
invokeCallbackOnElement(element, definition, "adoptedCallback", .{ old_document, new_document }, frame);
|
||||
}
|
||||
|
||||
pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, frame: *Frame) !void {
|
||||
@@ -146,17 +160,17 @@ pub fn invokeDisconnectedCallbackOnElement(element: *Element, frame: *Frame) voi
|
||||
invokeCallbackOnElement(element, definition, "disconnectedCallback", .{}, frame);
|
||||
}
|
||||
|
||||
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, frame: *Frame) void {
|
||||
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
|
||||
// Autonomous custom element
|
||||
if (element.is(Custom)) |custom| {
|
||||
custom.invokeAttributeChangedCallback(name, old_value, new_value, frame);
|
||||
custom.invokeAttributeChangedCallback(name, old_value, new_value, namespace, frame);
|
||||
return;
|
||||
}
|
||||
|
||||
// Customized built-in element - check if attribute is observed
|
||||
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
|
||||
if (!definition.isAttributeObserved(name)) return;
|
||||
invokeCallbackOnElement(element, definition, "attributeChangedCallback", .{ name, old_value, new_value }, frame);
|
||||
invokeCallbackOnElement(element, definition, "attributeChangedCallback", .{ name, old_value, new_value, namespace }, frame);
|
||||
}
|
||||
|
||||
fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, frame: *Frame) void {
|
||||
|
||||
Reference in New Issue
Block a user