mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +00:00
add native custom elements
This commit is contained in:
@@ -1,107 +0,0 @@
|
||||
// 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 std = @import("std");
|
||||
const log = @import("log.zig");
|
||||
|
||||
const timestamp = @import("datetime.zig").milliTimestamp;
|
||||
|
||||
const Queue = std.PriorityQueue(Task, void, struct {
|
||||
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
||||
return std.math.order(a.run_at, b.run_at);
|
||||
}
|
||||
}.compare);
|
||||
|
||||
const Scheduler = @This();
|
||||
|
||||
low_priority: Queue,
|
||||
high_priority: Queue,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Scheduler {
|
||||
return .{
|
||||
.low_priority = Queue.init(allocator, {}),
|
||||
.high_priority = Queue.init(allocator, {}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Scheduler) void {
|
||||
self.low_priority.cap = 0;
|
||||
self.low_priority.items.len = 0;
|
||||
|
||||
self.high_priority.cap = 0;
|
||||
self.high_priority.items.len = 0;
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
name: []const u8 = "",
|
||||
low_priority: bool = false,
|
||||
};
|
||||
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||
log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority });
|
||||
var queue = if (opts.low_priority) &self.low_priority else &self.high_priority;
|
||||
return queue.add(.{
|
||||
.ctx = ctx,
|
||||
.callback = cb,
|
||||
.name = opts.name,
|
||||
.run_at = timestamp(.monotonic) + run_in_ms,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn run(self: *Scheduler) !?u64 {
|
||||
_ = try self.runQueue(&self.low_priority);
|
||||
return self.runQueue(&self.high_priority);
|
||||
}
|
||||
|
||||
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||
if (queue.count() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = timestamp(.monotonic);
|
||||
|
||||
std.debug.print("running: {s}\n", .{task.name});
|
||||
while (queue.peek()) |*task_| {
|
||||
if (task_.run_at > now) {
|
||||
return @intCast(task_.run_at - now);
|
||||
}
|
||||
var task = queue.remove();
|
||||
log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name });
|
||||
|
||||
const repeat_in_ms = task.callback(task.ctx) catch |err| {
|
||||
log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err });
|
||||
continue;
|
||||
};
|
||||
|
||||
if (repeat_in_ms) |ms| {
|
||||
// Task cannot be repeated immediately, and they should know that
|
||||
std.debug.assert(ms != 0);
|
||||
task.run_at = now + ms;
|
||||
try self.low_priority.add(task);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Task = struct {
|
||||
run_at: u64,
|
||||
ctx: *anyopaque,
|
||||
name: []const u8,
|
||||
callback: Callback,
|
||||
};
|
||||
|
||||
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||
@@ -61,6 +61,8 @@ _size_128_8: MemoryPoolAligned([128]u8, .@"8"),
|
||||
_size_144_8: MemoryPoolAligned([144]u8, .@"8"),
|
||||
_size_152_8: MemoryPoolAligned([152]u8, .@"8"),
|
||||
_size_160_8: MemoryPoolAligned([160]u8, .@"8"),
|
||||
_size_184_8: MemoryPoolAligned([184]u8, .@"8"),
|
||||
_size_192_8: MemoryPoolAligned([192]u8, .@"8"),
|
||||
_size_648_8: MemoryPoolAligned([648]u8, .@"8"),
|
||||
|
||||
pub fn init(page: *Page) Factory {
|
||||
@@ -82,6 +84,8 @@ pub fn init(page: *Page) Factory {
|
||||
._size_144_8 = MemoryPoolAligned([144]u8, .@"8").init(page.arena),
|
||||
._size_152_8 = MemoryPoolAligned([152]u8, .@"8").init(page.arena),
|
||||
._size_160_8 = MemoryPoolAligned([160]u8, .@"8").init(page.arena),
|
||||
._size_184_8 = MemoryPoolAligned([184]u8, .@"8").init(page.arena),
|
||||
._size_192_8 = MemoryPoolAligned([192]u8, .@"8").init(page.arena),
|
||||
._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena),
|
||||
};
|
||||
}
|
||||
@@ -235,6 +239,8 @@ pub fn createT(self: *Factory, comptime T: type) !*T {
|
||||
if (comptime SO == 128) return @ptrCast(try self._size_128_8.create());
|
||||
if (comptime SO == 152) return @ptrCast(try self._size_152_8.create());
|
||||
if (comptime SO == 160) return @ptrCast(try self._size_160_8.create());
|
||||
if (comptime SO == 184) return @ptrCast(try self._size_184_8.create());
|
||||
if (comptime SO == 192) return @ptrCast(try self._size_192_8.create());
|
||||
if (comptime SO == 648) return @ptrCast(try self._size_648_8.create());
|
||||
@compileError(std.fmt.comptimePrint("No pool configured for @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(T), @typeName(T) }));
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ const Performance = @import("webapi/Performance.zig");
|
||||
const HtmlScript = @import("webapi/Element.zig").Html.Script;
|
||||
const MutationObserver = @import("webapi/MutationObserver.zig");
|
||||
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
||||
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
||||
const storage = @import("webapi/storage/storage.zig");
|
||||
|
||||
const timestamp = @import("../datetime.zig").timestamp;
|
||||
@@ -101,6 +102,16 @@ _mutation_delivery_scheduled: bool = false,
|
||||
_intersection_observers: std.ArrayList(*IntersectionObserver) = .{},
|
||||
_intersection_delivery_scheduled: bool = false,
|
||||
|
||||
// Lookup for customized built-in elements. Maps element pointer to definition.
|
||||
_customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{},
|
||||
|
||||
// This is set when an element is being upgraded (constructor is called).
|
||||
// The constructor can access this to get the element being upgraded.
|
||||
_upgrading_element: ?*Node = null,
|
||||
|
||||
// List of custom elements that were created before their definition was registered
|
||||
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
|
||||
|
||||
_polyfill_loader: polyfill.Loader = .{},
|
||||
|
||||
// for heap allocations and managing WebAPI objects
|
||||
@@ -207,8 +218,9 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self._mutation_delivery_scheduled = false;
|
||||
self._intersection_observers = .{};
|
||||
self._intersection_delivery_scheduled = false;
|
||||
self._customized_builtin_definitions = .{};
|
||||
self._undefined_custom_elements = .{};
|
||||
|
||||
try polyfill.preload(self.arena, self.js);
|
||||
try self.registerBackgroundTasks();
|
||||
}
|
||||
|
||||
@@ -1121,9 +1133,39 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
|
||||
return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag });
|
||||
}
|
||||
|
||||
// If we had a custom element registry, now is when we would look it up
|
||||
// and, if found, return an Element.Html.Custom
|
||||
const tag_name = try String.init(self.arena, name, .{});
|
||||
|
||||
// Check if this is a custom element (must have hyphen for HTML namespace)
|
||||
const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null;
|
||||
if (has_hyphen and namespace == .html) {
|
||||
const definition = self.window._custom_elements._definitions.get(name);
|
||||
const node = try self.createHtmlElementT(Element.Html.Custom, namespace, attribute_iterator, .{
|
||||
._proto = undefined,
|
||||
._tag_name = tag_name,
|
||||
._definition = definition,
|
||||
});
|
||||
|
||||
const def = definition orelse {
|
||||
const element = node.as(Element);
|
||||
const custom = element.is(Element.Html.Custom).?;
|
||||
try self._undefined_custom_elements.append(self.arena, custom);
|
||||
return node;
|
||||
};
|
||||
|
||||
// Save and restore upgrading element to allow nested createElement calls
|
||||
const prev_upgrading = self._upgrading_element;
|
||||
self._upgrading_element = node;
|
||||
defer self._upgrading_element = prev_upgrading;
|
||||
|
||||
var result: JS.Function.Result = undefined;
|
||||
_ = def.constructor.newInstance(&result) catch |err| {
|
||||
log.warn(.js, "custom element constructor", .{ .name = name, .err = err });
|
||||
return node;
|
||||
};
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name });
|
||||
}
|
||||
|
||||
@@ -1133,6 +1175,9 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac
|
||||
element._namespace = namespace;
|
||||
try self.populateElementAttributes(element, attribute_iterator);
|
||||
|
||||
// Check for customized built-in element via "is" attribute
|
||||
try Element.Html.Custom.checkAndAttachBuiltIn(element, self);
|
||||
|
||||
const node = element.asNode();
|
||||
if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) {
|
||||
@call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| {
|
||||
@@ -1269,13 +1314,15 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
|
||||
|
||||
// The child was connected and now it no longer is. We need to "disconnect"
|
||||
// it and all of its descendants. For now "disconnect" just means updating
|
||||
// document._elements_by_id
|
||||
// document._elements_by_id and invoking disconnectedCallback for custom elements
|
||||
var elements_by_id = &self.document._elements_by_id;
|
||||
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
|
||||
while (tw.next()) |el| {
|
||||
if (el.getAttributeSafe("id")) |id| {
|
||||
_ = elements_by_id.remove(id);
|
||||
}
|
||||
|
||||
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1415,6 +1462,8 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
||||
gop.value_ptr.* = el;
|
||||
}
|
||||
}
|
||||
|
||||
Element.Html.Custom.invokeConnectedCallbackOnElement(el, self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1423,6 +1472,8 @@ pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value:
|
||||
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err });
|
||||
};
|
||||
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self);
|
||||
|
||||
for (self._mutation_observers.items) |observer| {
|
||||
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
||||
log.err(.page, "attributeChange.notifyObserver", .{ .err = err });
|
||||
@@ -1435,6 +1486,8 @@ pub fn attributeRemove(self: *Page, element: *Element, name: []const u8, old_val
|
||||
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err });
|
||||
};
|
||||
|
||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self);
|
||||
|
||||
for (self._mutation_observers.items) |observer| {
|
||||
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
||||
log.err(.page, "attributeRemove.notifyObserver", .{ .err = err });
|
||||
@@ -1446,6 +1499,14 @@ pub fn hasMutationObservers(self: *const Page) bool {
|
||||
return self._mutation_observers.items.len > 0;
|
||||
}
|
||||
|
||||
pub fn getCustomizedBuiltInDefinition(self: *Page, element: *Element) ?*CustomElementDefinition {
|
||||
return self._customized_builtin_definitions.get(element);
|
||||
}
|
||||
|
||||
pub fn setCustomizedBuiltInDefinition(self: *Page, element: *Element, definition: *CustomElementDefinition) !void {
|
||||
try self._customized_builtin_definitions.put(self.arena, element, definition);
|
||||
}
|
||||
|
||||
pub fn characterDataChange(
|
||||
self: *Page,
|
||||
target: *Node,
|
||||
|
||||
@@ -106,13 +106,22 @@ pub fn _constructor(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo)
|
||||
@compileError(@typeName(F) ++ " has a constructor without a return type");
|
||||
};
|
||||
|
||||
const this = info.getThis();
|
||||
const new_this = info.getThis();
|
||||
var this = new_this;
|
||||
if (@typeInfo(ReturnType) == .error_union) {
|
||||
const non_error_res = res catch |err| return err;
|
||||
_ = try self.context.mapZigInstanceToJs(this, non_error_res);
|
||||
this = (try self.context.mapZigInstanceToJs(this, non_error_res)).castToObject();
|
||||
} else {
|
||||
_ = try self.context.mapZigInstanceToJs(this, res);
|
||||
this = (try self.context.mapZigInstanceToJs(this, res)).castToObject();
|
||||
}
|
||||
|
||||
// If we got back a different object (existing wrapper), copy the prototype
|
||||
// from new object. (this happens when we're upgrading an CustomElement)
|
||||
if (this.handle != new_this.handle) {
|
||||
const new_prototype = new_this.getPrototype();
|
||||
_ = this.setPrototype(self.context.v8_context, new_prototype.castTo(v8.Object));
|
||||
}
|
||||
|
||||
info.getReturnValue().set(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -399,6 +399,13 @@ pub fn createValue(self: *const Context, value: v8.Value) js.Value {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn createObject(self: *Context, js_value: v8.Value) js.Object {
|
||||
return .{
|
||||
.js_obj = js_value.castTo(v8.Object),
|
||||
.context = self,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn createFunction(self: *Context, js_value: v8.Value) !js.Function {
|
||||
// caller should have made sure this was a function
|
||||
std.debug.assert(js_value.isFunction());
|
||||
@@ -1930,7 +1937,6 @@ pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
||||
self.isolate.enqueueMicrotaskFunc(cb.func.castToFunction());
|
||||
}
|
||||
|
||||
|
||||
// == Misc ==
|
||||
// An interface for types that want to have their jsDeinit function to be
|
||||
// called when the call context ends
|
||||
|
||||
@@ -159,3 +159,10 @@ pub fn src(self: *const Function) ![]const u8 {
|
||||
const value = self.func.castToFunction().toValue();
|
||||
return self.context.valueToString(value, .{});
|
||||
}
|
||||
|
||||
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
||||
const func_obj = self.func.castToFunction().toObject();
|
||||
const key = v8.String.initUtf8(self.context.isolate, name);
|
||||
const value = func_obj.getValue(self.context.v8_context, key) catch return null;
|
||||
return self.context.createValue(value);
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@ pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
||||
return try context.createFunction(js_value);
|
||||
}
|
||||
|
||||
pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {
|
||||
const func = try self.getFunction(method_name) orelse return error.MethodNotFound;
|
||||
return func.callWithThis(T, self, args);
|
||||
}
|
||||
|
||||
pub fn isNull(self: Object) bool {
|
||||
return self.js_obj.toValue().isNull();
|
||||
}
|
||||
|
||||
@@ -546,4 +546,5 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/Performance.zig"),
|
||||
@import("../webapi/MutationObserver.zig"),
|
||||
@import("../webapi/IntersectionObserver.zig"),
|
||||
@import("../webapi/CustomElementRegistry.zig"),
|
||||
});
|
||||
|
||||
@@ -162,6 +162,23 @@ pub const Value = struct {
|
||||
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
||||
return Value{ .context = ctx, .value = value };
|
||||
}
|
||||
|
||||
pub fn isArray(self: Value) bool {
|
||||
return self.value.isArray();
|
||||
}
|
||||
|
||||
pub fn arrayLength(self: Value) u32 {
|
||||
std.debug.assert(self.value.isArray());
|
||||
return self.value.castTo(v8.Array).length();
|
||||
}
|
||||
|
||||
pub fn arrayGet(self: Value, index: u32) !Value {
|
||||
std.debug.assert(self.value.isArray());
|
||||
const array_obj = self.value.castTo(v8.Array).castTo(v8.Object);
|
||||
const idx_key = v8.Integer.initU32(self.context.isolate, index);
|
||||
const elem_val = try array_obj.getValue(self.context.v8_context, idx_key.toValue());
|
||||
return self.context.createValue(elem_val);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ValueIterator = struct {
|
||||
|
||||
@@ -26,9 +26,7 @@ const Allocator = std.mem.Allocator;
|
||||
pub const Loader = struct {
|
||||
state: enum { empty, loading } = .empty,
|
||||
|
||||
done: struct {
|
||||
webcomponents: bool = false,
|
||||
} = .{},
|
||||
done: struct {} = .{},
|
||||
|
||||
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *js.Context) void {
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
@@ -55,17 +53,6 @@ pub const Loader = struct {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self.done.webcomponents and isWebcomponents(name)) {
|
||||
const source = @import("webcomponents.zig").source;
|
||||
self.load("webcomponents", source, js_context);
|
||||
// We return false here: We want v8 to continue the calling chain
|
||||
// to finally find the polyfill we just inserted. If we want to
|
||||
// return false and stops the call chain, we have to use
|
||||
// `info.GetReturnValue.Set()` function, or `undefined` will be
|
||||
// returned immediately.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
log.debug(.unknown_prop, "unkown global property", .{
|
||||
.info = "but the property can exist in pure JS",
|
||||
@@ -76,25 +63,4 @@ pub const Loader = struct {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isWebcomponents(name: []const u8) bool {
|
||||
if (std.mem.eql(u8, name, "customElements")) return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn preload(allocator: Allocator, js_context: *js.Context) !void {
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const name = "webcomponents-pre";
|
||||
const source = @import("webcomponents.zig").pre;
|
||||
_ = js_context.exec(source, name) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
@license @nocompile
|
||||
Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
(function(){/*
|
||||
|
||||
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at
|
||||
http://polymer.github.io/LICENSE.txt The complete set of authors may be found
|
||||
at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
|
||||
be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
|
||||
Google as part of the polymer project is also subject to an additional IP
|
||||
rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
'use strict';var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype,
|
||||
"textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.toggleAttribute,E=window.Element.prototype.getAttributeNS,F=window.Element.prototype.setAttributeNS,G=window.Element.prototype.removeAttributeNS,H=window.Element.prototype.insertAdjacentElement,fa=window.Element.prototype.insertAdjacentHTML,
|
||||
ha=window.Element.prototype.prepend,ia=window.Element.prototype.append,ja=window.Element.prototype.before,ka=window.Element.prototype.after,la=window.Element.prototype.replaceWith,ma=window.Element.prototype.remove,na=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),oa=window.HTMLElement.prototype.insertAdjacentElement,pa=window.HTMLElement.prototype.insertAdjacentHTML;var qa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return qa.add(a)});function ra(a){var b=qa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var sa=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement);
|
||||
function J(a){var b=a.isConnected;if(void 0!==b)return b;if(sa(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b}
|
||||
function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null}
|
||||
function M(a,b,d){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var c=f;b(c);var e=c.localName;if("link"===e&&"import"===c.getAttribute("rel")){f=c.import;void 0===d&&(d=new Set);if(f instanceof Node&&!d.has(f))for(d.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,d);f=L(a,c);continue}else if("template"===e){f=L(a,c);continue}if(c=c.__CE_shadowRoot)for(c=c.firstChild;c;c=c.nextSibling)M(c,b,d)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.m=[];this.g=[];this.j=!1;this.shadyDomFastWalk=b;this.I=!a}function P(a,b,d,f){var c=window.ShadyDOM;if(a.shadyDomFastWalk&&c&&c.inUse){if(b.nodeType===Node.ELEMENT_NODE&&d(b),b.querySelectorAll)for(a=c.nativeMethods.querySelectorAll.call(b,"*"),b=0;b<a.length;b++)d(a[b])}else M(b,d,f)}function ta(a,b){a.j=!0;a.m.push(b)}function ua(a,b){a.j=!0;a.g.push(b)}
|
||||
function Q(a,b){a.j&&P(a,b,function(d){return R(a,d)})}function R(a,b){if(a.j&&!b.__CE_patched){b.__CE_patched=!0;for(var d=0;d<a.m.length;d++)a.m[d](b);for(d=0;d<a.g.length;d++)a.g[d](b)}}function S(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state?a.connectedCallback(f):T(a,f)}}function U(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state&&a.disconnectedCallback(f)}}
|
||||
function V(a,b,d){d=void 0===d?{}:d;var f=d.J,c=d.upgrade||function(g){return T(a,g)},e=[];P(a,b,function(g){a.j&&R(a,g);if("link"===g.localName&&"import"===g.getAttribute("rel")){var h=g.import;h instanceof Node&&(h.__CE_isImportDocument=!0,h.__CE_registry=document.__CE_registry);h&&"complete"===h.readyState?h.__CE_documentLoadHandled=!0:g.addEventListener("load",function(){var k=g.import;if(!k.__CE_documentLoadHandled){k.__CE_documentLoadHandled=!0;var l=new Set;f&&(f.forEach(function(m){return l.add(m)}),
|
||||
l.delete(k));V(a,k,{J:l,upgrade:c})}})}else e.push(g)},f);for(b=0;b<e.length;b++)c(e[b])}
|
||||
function T(a,b){try{var d=b.ownerDocument,f=d.__CE_registry;var c=f&&(d.defaultView||d.__CE_isImportDocument)?W(f,b.localName):void 0;if(c&&void 0===b.__CE_state){c.constructionStack.push(b);try{try{if(new c.constructorFunction!==b)throw Error("The custom element constructor did not produce the element being upgraded.");}finally{c.constructionStack.pop()}}catch(k){throw b.__CE_state=2,k;}b.__CE_state=1;b.__CE_definition=c;if(c.attributeChangedCallback&&b.hasAttributes()){var e=c.observedAttributes;
|
||||
for(c=0;c<e.length;c++){var g=e[c],h=b.getAttribute(g);null!==h&&a.attributeChangedCallback(b,g,null,h,null)}}J(b)&&a.connectedCallback(b)}}catch(k){X(k)}}N.prototype.connectedCallback=function(a){var b=a.__CE_definition;if(b.connectedCallback)try{b.connectedCallback.call(a)}catch(d){X(d)}};N.prototype.disconnectedCallback=function(a){var b=a.__CE_definition;if(b.disconnectedCallback)try{b.disconnectedCallback.call(a)}catch(d){X(d)}};
|
||||
N.prototype.attributeChangedCallback=function(a,b,d,f,c){var e=a.__CE_definition;if(e.attributeChangedCallback&&-1<e.observedAttributes.indexOf(b))try{e.attributeChangedCallback.call(a,b,d,f,c)}catch(g){X(g)}};
|
||||
function va(a,b,d,f){var c=b.__CE_registry;if(c&&(null===f||"http://www.w3.org/1999/xhtml"===f)&&(c=W(c,d)))try{var e=new c.constructorFunction;if(void 0===e.__CE_state||void 0===e.__CE_definition)throw Error("Failed to construct '"+d+"': The returned value was not constructed with the HTMLElement constructor.");if("http://www.w3.org/1999/xhtml"!==e.namespaceURI)throw Error("Failed to construct '"+d+"': The constructed element's namespace must be the HTML namespace.");if(e.hasAttributes())throw Error("Failed to construct '"+
|
||||
d+"': The constructed element must not have any attributes.");if(null!==e.firstChild)throw Error("Failed to construct '"+d+"': The constructed element must not have any children.");if(null!==e.parentNode)throw Error("Failed to construct '"+d+"': The constructed element must not have a parent node.");if(e.ownerDocument!==b)throw Error("Failed to construct '"+d+"': The constructed element's owner document is incorrect.");if(e.localName!==d)throw Error("Failed to construct '"+d+"': The constructed element's local name is incorrect.");
|
||||
return e}catch(g){return X(g),b=null===f?n.call(b,d):p.call(b,f,d),Object.setPrototypeOf(b,HTMLUnknownElement.prototype),b.__CE_state=2,b.__CE_definition=void 0,R(a,b),b}b=null===f?n.call(b,d):p.call(b,f,d);R(a,b);return b}
|
||||
function X(a){var b="",d="",f=0,c=0;a instanceof Error?(b=a.message,d=a.sourceURL||a.fileName||"",f=a.line||a.lineNumber||0,c=a.column||a.columnNumber||0):b="Uncaught "+String(a);var e=void 0;void 0===ErrorEvent.prototype.initErrorEvent?e=new ErrorEvent("error",{cancelable:!0,message:b,filename:d,lineno:f,colno:c,error:a}):(e=document.createEvent("ErrorEvent"),e.initErrorEvent("error",!1,!0,b,d,f),e.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{configurable:!0,get:function(){return!0}})});
|
||||
void 0===e.error&&Object.defineProperty(e,"error",{configurable:!0,enumerable:!0,get:function(){return a}});window.dispatchEvent(e);e.defaultPrevented||console.error(a)};function wa(){var a=this;this.g=void 0;this.F=new Promise(function(b){a.l=b})}wa.prototype.resolve=function(a){if(this.g)throw Error("Already resolved.");this.g=a;this.l(a)};function xa(a){var b=document;this.l=void 0;this.h=a;this.g=b;V(this.h,this.g);"loading"===this.g.readyState&&(this.l=new MutationObserver(this.G.bind(this)),this.l.observe(this.g,{childList:!0,subtree:!0}))}function ya(a){a.l&&a.l.disconnect()}xa.prototype.G=function(a){var b=this.g.readyState;"interactive"!==b&&"complete"!==b||ya(this);for(b=0;b<a.length;b++)for(var d=a[b].addedNodes,f=0;f<d.length;f++)V(this.h,d[f])};function Y(a){this.s=new Map;this.u=new Map;this.C=new Map;this.A=!1;this.B=new Map;this.o=function(b){return b()};this.i=!1;this.v=[];this.h=a;this.D=a.I?new xa(a):void 0}Y.prototype.H=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructor getters must be functions.");za(this,a);this.s.set(a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};
|
||||
Y.prototype.define=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructors must be functions.");za(this,a);Ba(this,a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};function za(a,b){if(!ra(b))throw new SyntaxError("The element name '"+b+"' is not valid.");if(W(a,b))throw Error("A custom element with name '"+(b+"' has already been defined."));if(a.A)throw Error("A custom element is already being defined.");}
|
||||
function Ba(a,b,d){a.A=!0;var f;try{var c=d.prototype;if(!(c instanceof Object))throw new TypeError("The custom element constructor's prototype is not an object.");var e=function(m){var x=c[m];if(void 0!==x&&!(x instanceof Function))throw Error("The '"+m+"' callback must be a function.");return x};var g=e("connectedCallback");var h=e("disconnectedCallback");var k=e("adoptedCallback");var l=(f=e("attributeChangedCallback"))&&d.observedAttributes||[]}catch(m){throw m;}finally{a.A=!1}d={localName:b,
|
||||
constructorFunction:d,connectedCallback:g,disconnectedCallback:h,adoptedCallback:k,attributeChangedCallback:f,observedAttributes:l,constructionStack:[]};a.u.set(b,d);a.C.set(d.constructorFunction,d);return d}Y.prototype.upgrade=function(a){V(this.h,a)};
|
||||
function Aa(a){if(!1!==a.i){a.i=!1;for(var b=[],d=a.v,f=new Map,c=0;c<d.length;c++)f.set(d[c],[]);V(a.h,document,{upgrade:function(k){if(void 0===k.__CE_state){var l=k.localName,m=f.get(l);m?m.push(k):a.u.has(l)&&b.push(k)}}});for(c=0;c<b.length;c++)T(a.h,b[c]);for(c=0;c<d.length;c++){for(var e=d[c],g=f.get(e),h=0;h<g.length;h++)T(a.h,g[h]);(e=a.B.get(e))&&e.resolve(void 0)}d.length=0}}Y.prototype.get=function(a){if(a=W(this,a))return a.constructorFunction};
|
||||
Y.prototype.whenDefined=function(a){if(!ra(a))return Promise.reject(new SyntaxError("'"+a+"' is not a valid custom element name."));var b=this.B.get(a);if(b)return b.F;b=new wa;this.B.set(a,b);var d=this.u.has(a)||this.s.has(a);a=-1===this.v.indexOf(a);d&&a&&b.resolve(void 0);return b.F};Y.prototype.polyfillWrapFlushCallback=function(a){this.D&&ya(this.D);var b=this.o;this.o=function(d){return a(function(){return b(d)})}};
|
||||
function W(a,b){var d=a.u.get(b);if(d)return d;if(d=a.s.get(b)){a.s.delete(b);try{return Ba(a,b,d())}catch(f){X(f)}}}Y.prototype.define=Y.prototype.define;Y.prototype.upgrade=Y.prototype.upgrade;Y.prototype.get=Y.prototype.get;Y.prototype.whenDefined=Y.prototype.whenDefined;Y.prototype.polyfillDefineLazy=Y.prototype.H;Y.prototype.polyfillWrapFlushCallback=Y.prototype.polyfillWrapFlushCallback;function Z(a,b,d){function f(c){return function(e){for(var g=[],h=0;h<arguments.length;++h)g[h]=arguments[h];h=[];for(var k=[],l=0;l<g.length;l++){var m=g[l];m instanceof Element&&J(m)&&k.push(m);if(m instanceof DocumentFragment)for(m=m.firstChild;m;m=m.nextSibling)h.push(m);else h.push(m)}c.apply(this,g);for(g=0;g<k.length;g++)U(a,k[g]);if(J(this))for(g=0;g<h.length;g++)k=h[g],k instanceof Element&&S(a,k)}}void 0!==d.prepend&&(b.prepend=f(d.prepend));void 0!==d.append&&(b.append=f(d.append))};function Ca(a){Document.prototype.createElement=function(b){return va(a,this,b,null)};Document.prototype.importNode=function(b,d){b=aa.call(this,b,!!d);this.__CE_registry?V(a,b):Q(a,b);return b};Document.prototype.createElementNS=function(b,d){return va(a,this,d,b)};Z(a,Document.prototype,{prepend:ba,append:ca})};function Da(a){function b(f){return function(c){for(var e=[],g=0;g<arguments.length;++g)e[g]=arguments[g];g=[];for(var h=[],k=0;k<e.length;k++){var l=e[k];l instanceof Element&&J(l)&&h.push(l);if(l instanceof DocumentFragment)for(l=l.firstChild;l;l=l.nextSibling)g.push(l);else g.push(l)}f.apply(this,e);for(e=0;e<h.length;e++)U(a,h[e]);if(J(this))for(e=0;e<g.length;e++)h=g[e],h instanceof Element&&S(a,h)}}var d=Element.prototype;void 0!==ja&&(d.before=b(ja));void 0!==ka&&(d.after=b(ka));void 0!==la&&
|
||||
(d.replaceWith=function(f){for(var c=[],e=0;e<arguments.length;++e)c[e]=arguments[e];e=[];for(var g=[],h=0;h<c.length;h++){var k=c[h];k instanceof Element&&J(k)&&g.push(k);if(k instanceof DocumentFragment)for(k=k.firstChild;k;k=k.nextSibling)e.push(k);else e.push(k)}h=J(this);la.apply(this,c);for(c=0;c<g.length;c++)U(a,g[c]);if(h)for(U(a,this),c=0;c<e.length;c++)g=e[c],g instanceof Element&&S(a,g)});void 0!==ma&&(d.remove=function(){var f=J(this);ma.call(this);f&&U(a,this)})};function Ea(a){function b(c,e){Object.defineProperty(c,"innerHTML",{enumerable:e.enumerable,configurable:!0,get:e.get,set:function(g){var h=this,k=void 0;J(this)&&(k=[],P(a,this,function(x){x!==h&&k.push(x)}));e.set.call(this,g);if(k)for(var l=0;l<k.length;l++){var m=k[l];1===m.__CE_state&&a.disconnectedCallback(m)}this.ownerDocument.__CE_registry?V(a,this):Q(a,this);return g}})}function d(c,e){c.insertAdjacentElement=function(g,h){var k=J(h);g=e.call(this,g,h);k&&U(a,h);J(g)&&S(a,h);return g}}function f(c,
|
||||
e){function g(h,k){for(var l=[];h!==k;h=h.nextSibling)l.push(h);for(k=0;k<l.length;k++)V(a,l[k])}c.insertAdjacentHTML=function(h,k){h=h.toLowerCase();if("beforebegin"===h){var l=this.previousSibling;e.call(this,h,k);g(l||this.parentNode.firstChild,this)}else if("afterbegin"===h)l=this.firstChild,e.call(this,h,k),g(this.firstChild,l);else if("beforeend"===h)l=this.lastChild,e.call(this,h,k),g(l||this.firstChild,null);else if("afterend"===h)l=this.nextSibling,e.call(this,h,k),g(this.nextSibling,l);
|
||||
else throw new SyntaxError("The value provided ("+String(h)+") is not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.");}}y&&(Element.prototype.attachShadow=function(c){c=y.call(this,c);if(a.j&&!c.__CE_patched){c.__CE_patched=!0;for(var e=0;e<a.m.length;e++)a.m[e](c)}return this.__CE_shadowRoot=c});z&&z.get?b(Element.prototype,z):I&&I.get?b(HTMLElement.prototype,I):ua(a,function(c){b(c,{enumerable:!0,configurable:!0,get:function(){return q.call(this,!0).innerHTML},set:function(e){var g=
|
||||
"template"===this.localName,h=g?this.content:this,k=p.call(document,this.namespaceURI,this.localName);for(k.innerHTML=e;0<h.childNodes.length;)u.call(h,h.childNodes[0]);for(e=g?k.content:k;0<e.childNodes.length;)r.call(h,e.childNodes[0])}})});Element.prototype.setAttribute=function(c,e){if(1!==this.__CE_state)return B.call(this,c,e);var g=A.call(this,c);B.call(this,c,e);e=A.call(this,c);a.attributeChangedCallback(this,c,g,e,null)};Element.prototype.setAttributeNS=function(c,e,g){if(1!==this.__CE_state)return F.call(this,
|
||||
c,e,g);var h=E.call(this,c,e);F.call(this,c,e,g);g=E.call(this,c,e);a.attributeChangedCallback(this,e,h,g,c)};Element.prototype.removeAttribute=function(c){if(1!==this.__CE_state)return C.call(this,c);var e=A.call(this,c);C.call(this,c);null!==e&&a.attributeChangedCallback(this,c,e,null,null)};D&&(Element.prototype.toggleAttribute=function(c,e){if(1!==this.__CE_state)return D.call(this,c,e);var g=A.call(this,c),h=null!==g;e=D.call(this,c,e);h!==e&&a.attributeChangedCallback(this,c,g,e?"":null,null);
|
||||
return e});Element.prototype.removeAttributeNS=function(c,e){if(1!==this.__CE_state)return G.call(this,c,e);var g=E.call(this,c,e);G.call(this,c,e);var h=E.call(this,c,e);g!==h&&a.attributeChangedCallback(this,e,g,h,c)};oa?d(HTMLElement.prototype,oa):H&&d(Element.prototype,H);pa?f(HTMLElement.prototype,pa):fa&&f(Element.prototype,fa);Z(a,Element.prototype,{prepend:ha,append:ia});Da(a)};var Fa={};function Ga(a){function b(){var d=this.constructor;var f=document.__CE_registry.C.get(d);if(!f)throw Error("Failed to construct a custom element: The constructor was not registered with `customElements`.");var c=f.constructionStack;if(0===c.length)return c=n.call(document,f.localName),Object.setPrototypeOf(c,d.prototype),c.__CE_state=1,c.__CE_definition=f,R(a,c),c;var e=c.length-1,g=c[e];if(g===Fa)throw Error("Failed to construct '"+f.localName+"': This element was already constructed.");c[e]=Fa;
|
||||
Object.setPrototypeOf(g,d.prototype);R(a,g);return g}b.prototype=na.prototype;Object.defineProperty(HTMLElement.prototype,"constructor",{writable:!0,configurable:!0,enumerable:!1,value:b});window.HTMLElement=b};function Ha(a){function b(d,f){Object.defineProperty(d,"textContent",{enumerable:f.enumerable,configurable:!0,get:f.get,set:function(c){if(this.nodeType===Node.TEXT_NODE)f.set.call(this,c);else{var e=void 0;if(this.firstChild){var g=this.childNodes,h=g.length;if(0<h&&J(this)){e=Array(h);for(var k=0;k<h;k++)e[k]=g[k]}}f.set.call(this,c);if(e)for(c=0;c<e.length;c++)U(a,e[c])}}})}Node.prototype.insertBefore=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=t.call(this,d,f);if(J(this))for(f=
|
||||
0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);f=t.call(this,d,f);c&&U(a,d);J(this)&&S(a,d);return f};Node.prototype.appendChild=function(d){if(d instanceof DocumentFragment){var f=K(d);d=r.call(this,d);if(J(this))for(var c=0;c<f.length;c++)S(a,f[c]);return d}f=d instanceof Element&&J(d);c=r.call(this,d);f&&U(a,d);J(this)&&S(a,d);return c};Node.prototype.cloneNode=function(d){d=q.call(this,!!d);this.ownerDocument.__CE_registry?V(a,d):Q(a,d);return d};Node.prototype.removeChild=function(d){var f=
|
||||
d instanceof Element&&J(d),c=u.call(this,d);f&&U(a,d);return c};Node.prototype.replaceChild=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=v.call(this,d,f);if(J(this))for(U(a,f),f=0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);var e=v.call(this,d,f),g=J(this);g&&U(a,f);c&&U(a,d);g&&S(a,d);return e};w&&w.get?b(Node.prototype,w):ta(a,function(d){b(d,{enumerable:!0,configurable:!0,get:function(){for(var f=[],c=this.firstChild;c;c=c.nextSibling)c.nodeType!==Node.COMMENT_NODE&&
|
||||
f.push(c.textContent);return f.join("")},set:function(f){for(;this.firstChild;)u.call(this,this.firstChild);null!=f&&""!==f&&r.call(this,document.createTextNode(f))}})})};var O=window.customElements;function Ia(){var a=new N;Ga(a);Ca(a);Z(a,DocumentFragment.prototype,{prepend:da,append:ea});Ha(a);Ea(a);window.CustomElementRegistry=Y;a=new Y(a);document.__CE_registry=a;Object.defineProperty(window,"customElements",{configurable:!0,enumerable:!0,value:a})}O&&!O.forcePolyfill&&"function"==typeof O.define&&"function"==typeof O.get||Ia();window.__CE_installPolyfill=Ia;/*
|
||||
|
||||
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
}).call(this);
|
||||
@@ -1,43 +0,0 @@
|
||||
// webcomponents.js code comes from
|
||||
// https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs
|
||||
//
|
||||
// The original code source is available in a "BSD style license".
|
||||
//
|
||||
// This is the `webcomponents-ce.js` bundle
|
||||
pub const source = @embedFile("webcomponents.js");
|
||||
|
||||
// The main webcomponents.js is lazilly loaded when window.customElements is
|
||||
// called. But, if you look at the test below, you'll notice that we declare
|
||||
// our custom element (LightPanda) before we call `customElements.define`. We
|
||||
// _have_ to declare it before we can register it.
|
||||
// That causes an issue, because the LightPanda class extends HTMLElement, which
|
||||
// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is
|
||||
// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement
|
||||
// constructor does (and that's correct).
|
||||
// However, once HTMLElement is monkeypatched, it'll work. One simple solution
|
||||
// is to run the webcomponents.js polyfill proactively on each page, ensuring
|
||||
// that HTMLElement is monkeypatched before any other JavaScript is run. But
|
||||
// that adds _a lot_ of overhead.
|
||||
// So instead of always running the [large and intrusive] webcomponents.js
|
||||
// polyfill, we'll always run this little snippet. It wraps the HTMLElement
|
||||
// constructor. When the Lightpanda class is created, it'll extend our little
|
||||
// wrapper. But, unlike the Zig default constructor which throws, our code
|
||||
// calls the "real" constructor. That might seem like the same thing, but by the
|
||||
// time our wrapper is called, the webcomponents.js polyfill will have been
|
||||
// loaded and the "real" constructor will be the monkeypatched version.
|
||||
// TL;DR creates a layer of indirection for the constructor, so that, when it's
|
||||
// actually instantiated, the webcomponents.js polyfill will have been loaded.
|
||||
pub const pre =
|
||||
\\ (() => {
|
||||
\\ const HE = window.HTMLElement;
|
||||
\\ const b = function() { return HE.prototype.constructor.call(this); }
|
||||
\\ b.prototype = HE.prototype;
|
||||
\\ window.HTMLElement = b;
|
||||
\\ })();
|
||||
;
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Polyfill.WebComponents" {
|
||||
// @ZIGDOM
|
||||
// try testing.htmlRunner("polyfill/webcomponents.html", .{});
|
||||
}
|
||||
158
src/browser/tests/custom_elements/attribute_changed.html
Normal file
158
src/browser/tests/custom_elements/attribute_changed.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="attribute_changed">
|
||||
{
|
||||
let callbackCalls = [];
|
||||
|
||||
class MyElement extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['data-foo', 'data-bar'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
callbackCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-element', MyElement);
|
||||
|
||||
const el = document.createElement('my-element');
|
||||
testing.expectEqual(0, callbackCalls.length);
|
||||
|
||||
el.setAttribute('data-foo', 'value1');
|
||||
testing.expectEqual(1, callbackCalls.length);
|
||||
testing.expectEqual('data-foo', callbackCalls[0].name);
|
||||
testing.expectEqual(null, callbackCalls[0].oldValue);
|
||||
testing.expectEqual('value1', callbackCalls[0].newValue);
|
||||
|
||||
el.setAttribute('data-foo', 'value2');
|
||||
testing.expectEqual(2, callbackCalls.length);
|
||||
testing.expectEqual('data-foo', callbackCalls[1].name);
|
||||
testing.expectEqual('value1', callbackCalls[1].oldValue);
|
||||
testing.expectEqual('value2', callbackCalls[1].newValue);
|
||||
|
||||
el.setAttribute('data-bar', 'bar-value');
|
||||
testing.expectEqual(3, callbackCalls.length);
|
||||
testing.expectEqual('data-bar', callbackCalls[2].name);
|
||||
testing.expectEqual(null, callbackCalls[2].oldValue);
|
||||
testing.expectEqual('bar-value', callbackCalls[2].newValue);
|
||||
}
|
||||
|
||||
{
|
||||
let callbackCalls = [];
|
||||
|
||||
class ObservedElement extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['watched'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
callbackCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('observed-element', ObservedElement);
|
||||
|
||||
const el = document.createElement('observed-element');
|
||||
el.setAttribute('unwatched', 'value');
|
||||
testing.expectEqual(0, callbackCalls.length);
|
||||
|
||||
el.setAttribute('watched', 'value');
|
||||
testing.expectEqual(1, callbackCalls.length);
|
||||
}
|
||||
|
||||
{
|
||||
let callbackCalls = [];
|
||||
|
||||
class RemoveAttrElement extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['test'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
callbackCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('remove-attr-element', RemoveAttrElement);
|
||||
|
||||
const el = document.createElement('remove-attr-element');
|
||||
el.setAttribute('test', 'value');
|
||||
testing.expectEqual(1, callbackCalls.length);
|
||||
|
||||
el.removeAttribute('test');
|
||||
testing.expectEqual(2, callbackCalls.length);
|
||||
testing.expectEqual('test', callbackCalls[1].name);
|
||||
testing.expectEqual('value', callbackCalls[1].oldValue);
|
||||
testing.expectEqual(null, callbackCalls[1].newValue);
|
||||
}
|
||||
|
||||
{
|
||||
let callbackCalls = [];
|
||||
|
||||
class NoObservedElement extends HTMLElement {
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
callbackCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('no-observed-element', NoObservedElement);
|
||||
|
||||
const el = document.createElement('no-observed-element');
|
||||
el.setAttribute('test', 'value');
|
||||
testing.expectEqual(0, callbackCalls.length);
|
||||
}
|
||||
|
||||
{
|
||||
let callbackCalls = [];
|
||||
|
||||
class UpgradeAttrElement extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['existing'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
callbackCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
const el = document.createElement('upgrade-attr-element');
|
||||
el.setAttribute('existing', 'before-upgrade');
|
||||
testing.expectEqual(0, callbackCalls.length);
|
||||
|
||||
customElements.define('upgrade-attr-element', UpgradeAttrElement);
|
||||
testing.expectEqual(0, callbackCalls.length);
|
||||
|
||||
el.setAttribute('existing', 'after-upgrade');
|
||||
testing.expectEqual(1, callbackCalls.length);
|
||||
testing.expectEqual('existing', callbackCalls[0].name);
|
||||
testing.expectEqual('before-upgrade', callbackCalls[0].oldValue);
|
||||
testing.expectEqual('after-upgrade', callbackCalls[0].newValue);
|
||||
}
|
||||
|
||||
{
|
||||
let callbackCalls = [];
|
||||
|
||||
class SetAttrMethodElement extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['test'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
callbackCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('set-attr-method-element', SetAttrMethodElement);
|
||||
|
||||
const el = document.createElement('set-attr-method-element');
|
||||
el.setAttribute('test', 'initial');
|
||||
testing.expectEqual(1, callbackCalls.length);
|
||||
|
||||
el.setAttribute('test', 'initial');
|
||||
testing.expectEqual(2, callbackCalls.length);
|
||||
testing.expectEqual('initial', callbackCalls[1].oldValue);
|
||||
testing.expectEqual('initial', callbackCalls[1].newValue);
|
||||
}
|
||||
</script>
|
||||
137
src/browser/tests/custom_elements/built_in.html
Normal file
137
src/browser/tests/custom_elements/built_in.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="built_in">
|
||||
{
|
||||
class FancyButton extends HTMLElement {}
|
||||
customElements.define('fancy-button', FancyButton, { extends: 'button' });
|
||||
|
||||
const definition = customElements.get('fancy-button');
|
||||
testing.expectEqual(FancyButton, definition);
|
||||
}
|
||||
|
||||
{
|
||||
let threw = false;
|
||||
try {
|
||||
class BadExtends extends HTMLElement {}
|
||||
customElements.define('bad-extends', BadExtends, { extends: 'not-a-real-element' });
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
|
||||
{
|
||||
let threw = false;
|
||||
try {
|
||||
class ExtendCustom extends HTMLElement {}
|
||||
customElements.define('extend-custom', ExtendCustom, { extends: 'fancy-button' });
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
|
||||
{
|
||||
class FancyParagraph extends HTMLElement {}
|
||||
customElements.define('fancy-paragraph', FancyParagraph, { extends: 'p' });
|
||||
|
||||
const definition = customElements.get('fancy-paragraph');
|
||||
testing.expectEqual(FancyParagraph, definition);
|
||||
}
|
||||
|
||||
{
|
||||
let constructorCalled = 0;
|
||||
|
||||
class ConstructedButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalled++;
|
||||
this.clicked = false;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('constructed-button', ConstructedButton, { extends: 'button' });
|
||||
|
||||
const btn = document.createElement('button', { is: 'constructed-button' });
|
||||
testing.expectEqual(1, constructorCalled);
|
||||
testing.expectEqual('BUTTON', btn.tagName);
|
||||
testing.expectEqual(false, btn.clicked);
|
||||
}
|
||||
|
||||
{
|
||||
class WrongTag extends HTMLElement {}
|
||||
customElements.define('wrong-tag', WrongTag, { extends: 'button' });
|
||||
|
||||
const div = document.createElement('div', { is: 'wrong-tag' });
|
||||
testing.expectEqual('DIV', div.tagName);
|
||||
}
|
||||
|
||||
{
|
||||
let connectedCount = 0;
|
||||
let disconnectedCount = 0;
|
||||
let attributeChanges = [];
|
||||
|
||||
class LifecycleButton extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['data-test'];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
connectedCount++;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
disconnectedCount++;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
attributeChanges.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('lifecycle-button', LifecycleButton, { extends: 'button' });
|
||||
|
||||
const btn = document.createElement('button', { is: 'lifecycle-button' });
|
||||
testing.expectEqual(0, connectedCount);
|
||||
|
||||
document.body.appendChild(btn);
|
||||
testing.expectEqual(1, connectedCount);
|
||||
|
||||
btn.setAttribute('data-test', 'value1');
|
||||
testing.expectEqual(1, attributeChanges.length);
|
||||
testing.expectEqual('data-test', attributeChanges[0].name);
|
||||
testing.expectEqual(null, attributeChanges[0].oldValue);
|
||||
testing.expectEqual('value1', attributeChanges[0].newValue);
|
||||
|
||||
btn.remove();
|
||||
testing.expectEqual(1, disconnectedCount);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
{
|
||||
let constructorCalled = 0;
|
||||
let connectedCalled = 0;
|
||||
|
||||
class ParsedButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalled++;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
connectedCalled++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('parsed-button', ParsedButton, { extends: 'button' });
|
||||
}
|
||||
</script>
|
||||
<button is="parsed-button" id="parsed"></button>
|
||||
<script>
|
||||
{
|
||||
const btn = document.getElementById('parsed');
|
||||
testing.expectEqual('BUTTON', btn.tagName);
|
||||
}
|
||||
</script>
|
||||
93
src/browser/tests/custom_elements/connected.html
Normal file
93
src/browser/tests/custom_elements/connected.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="connected">
|
||||
{
|
||||
let connectedCount = 0;
|
||||
|
||||
class MyElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
connectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-element', MyElement);
|
||||
|
||||
const el = document.createElement('my-element');
|
||||
testing.expectEqual(0, connectedCount);
|
||||
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(1, connectedCount);
|
||||
}
|
||||
|
||||
{
|
||||
let order = [];
|
||||
|
||||
class ParentElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
order.push('parent');
|
||||
}
|
||||
}
|
||||
|
||||
class ChildElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
order.push('child');
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('parent-element', ParentElement);
|
||||
customElements.define('child-element', ChildElement);
|
||||
|
||||
const parent = document.createElement('parent-element');
|
||||
const child = document.createElement('child-element');
|
||||
parent.appendChild(child);
|
||||
|
||||
testing.expectEqual(0, order.length);
|
||||
|
||||
document.body.appendChild(parent);
|
||||
testing.expectEqual(2, order.length);
|
||||
testing.expectEqual('parent', order[0]);
|
||||
testing.expectEqual('child', order[1]);
|
||||
}
|
||||
|
||||
{
|
||||
let connectedCount = 0;
|
||||
|
||||
class MoveElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
connectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('move-element', MoveElement);
|
||||
|
||||
const el = document.createElement('move-element');
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(1, connectedCount);
|
||||
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
div.appendChild(el);
|
||||
testing.expectEqual(1, connectedCount);
|
||||
}
|
||||
|
||||
{
|
||||
let connectedCount = 0;
|
||||
|
||||
class DetachedElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
connectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('detached-element', DetachedElement);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const el = document.createElement('detached-element');
|
||||
container.appendChild(el);
|
||||
testing.expectEqual(0, connectedCount);
|
||||
|
||||
document.body.appendChild(container);
|
||||
testing.expectEqual(1, connectedCount);
|
||||
}
|
||||
</script>
|
||||
55
src/browser/tests/custom_elements/constructor.html
Normal file
55
src/browser/tests/custom_elements/constructor.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="constructor">
|
||||
{
|
||||
let constructorCalled = false;
|
||||
let constructorThis = null;
|
||||
|
||||
class MyElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalled = true;
|
||||
constructorThis = this;
|
||||
this.customProperty = 'initialized';
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-element', MyElement);
|
||||
|
||||
const el = document.createElement('my-element');
|
||||
testing.expectEqual(true, constructorCalled);
|
||||
testing.expectEqual(el, constructorThis);
|
||||
testing.expectEqual('initialized', el.customProperty);
|
||||
}
|
||||
|
||||
{
|
||||
class CounterElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.count++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('counter-element', CounterElement);
|
||||
|
||||
const counter = document.createElement('counter-element');
|
||||
testing.expectEqual(0, counter.count);
|
||||
counter.increment();
|
||||
testing.expectEqual(1, counter.count);
|
||||
counter.increment();
|
||||
testing.expectEqual(2, counter.count);
|
||||
}
|
||||
|
||||
{
|
||||
class NoConstructorElement extends HTMLElement {}
|
||||
|
||||
customElements.define('no-constructor-element', NoConstructorElement);
|
||||
|
||||
const el = document.createElement('no-constructor-element');
|
||||
testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);
|
||||
}
|
||||
</script>
|
||||
151
src/browser/tests/custom_elements/disconnected.html
Normal file
151
src/browser/tests/custom_elements/disconnected.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="disconnected">
|
||||
{
|
||||
let disconnectedCount = 0;
|
||||
|
||||
class MyElement extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
disconnectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-element', MyElement);
|
||||
|
||||
const el = document.createElement('my-element');
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(0, disconnectedCount);
|
||||
|
||||
el.remove();
|
||||
testing.expectEqual(1, disconnectedCount);
|
||||
}
|
||||
|
||||
{
|
||||
let order = [];
|
||||
|
||||
class ParentElement extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
order.push('parent');
|
||||
}
|
||||
}
|
||||
|
||||
class ChildElement extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
order.push('child');
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('parent-element', ParentElement);
|
||||
customElements.define('child-element', ChildElement);
|
||||
|
||||
const parent = document.createElement('parent-element');
|
||||
const child = document.createElement('child-element');
|
||||
parent.appendChild(child);
|
||||
document.body.appendChild(parent);
|
||||
|
||||
testing.expectEqual(0, order.length);
|
||||
|
||||
parent.remove();
|
||||
testing.expectEqual(2, order.length);
|
||||
testing.expectEqual('parent', order[0]);
|
||||
testing.expectEqual('child', order[1]);
|
||||
}
|
||||
|
||||
{
|
||||
let disconnectedCount = 0;
|
||||
|
||||
class RemoveChildElement extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
disconnectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('remove-child-element', RemoveChildElement);
|
||||
|
||||
const el = document.createElement('remove-child-element');
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(0, disconnectedCount);
|
||||
|
||||
document.body.removeChild(el);
|
||||
testing.expectEqual(1, disconnectedCount);
|
||||
}
|
||||
|
||||
{
|
||||
let disconnectedCount = 0;
|
||||
|
||||
class MoveElement extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
disconnectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('move-element', MoveElement);
|
||||
|
||||
const el = document.createElement('move-element');
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(0, disconnectedCount);
|
||||
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
div.appendChild(el);
|
||||
testing.expectEqual(0, disconnectedCount);
|
||||
}
|
||||
|
||||
{
|
||||
let disconnectedCount = 0;
|
||||
|
||||
class ReplaceChildElement extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
disconnectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('replace-child-element', ReplaceChildElement);
|
||||
|
||||
const el = document.createElement('replace-child-element');
|
||||
const replacement = document.createElement('div');
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(0, disconnectedCount);
|
||||
|
||||
document.body.replaceChild(replacement, el);
|
||||
testing.expectEqual(1, disconnectedCount);
|
||||
}
|
||||
|
||||
{
|
||||
let connectedCount = 0;
|
||||
let disconnectedCount = 0;
|
||||
|
||||
class LifecycleElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
connectedCount++;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
disconnectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('lifecycle-element', LifecycleElement);
|
||||
|
||||
const el = document.createElement('lifecycle-element');
|
||||
testing.expectEqual(0, connectedCount);
|
||||
testing.expectEqual(0, disconnectedCount);
|
||||
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(1, connectedCount);
|
||||
testing.expectEqual(0, disconnectedCount);
|
||||
|
||||
el.remove();
|
||||
testing.expectEqual(1, connectedCount);
|
||||
testing.expectEqual(1, disconnectedCount);
|
||||
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(2, connectedCount);
|
||||
testing.expectEqual(1, disconnectedCount);
|
||||
|
||||
el.remove();
|
||||
testing.expectEqual(2, connectedCount);
|
||||
testing.expectEqual(2, disconnectedCount);
|
||||
}
|
||||
</script>
|
||||
83
src/browser/tests/custom_elements/registry.html
Normal file
83
src/browser/tests/custom_elements/registry.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="registry">
|
||||
{
|
||||
testing.expectEqual(true, window.customElements !== undefined);
|
||||
testing.expectEqual('function', typeof window.customElements.define);
|
||||
testing.expectEqual('function', typeof window.customElements.get);
|
||||
}
|
||||
|
||||
{
|
||||
class MyElement extends HTMLElement {}
|
||||
customElements.define('my-element', MyElement);
|
||||
const retrieved = customElements.get('my-element');
|
||||
testing.expectEqual(true, retrieved === MyElement);
|
||||
}
|
||||
|
||||
{
|
||||
const retrieved = customElements.get('not-defined');
|
||||
testing.expectEqual(undefined, retrieved);
|
||||
}
|
||||
|
||||
{
|
||||
class AnotherElement extends HTMLElement {}
|
||||
customElements.define('another-element', AnotherElement);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
customElements.define('another-element', AnotherElement);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
|
||||
{
|
||||
let threw = false;
|
||||
try {
|
||||
customElements.define('nohyphen', class extends HTMLElement {});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
|
||||
{
|
||||
let threw = false;
|
||||
try {
|
||||
customElements.define('UPPERCASE-ELEMENT', class extends HTMLElement {});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
|
||||
{
|
||||
let threw = false;
|
||||
try {
|
||||
customElements.define('annotation-xml', class extends HTMLElement {});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
|
||||
{
|
||||
class TestElement extends HTMLElement {}
|
||||
customElements.define('test-element', TestElement);
|
||||
|
||||
const el = document.createElement('test-element');
|
||||
testing.expectEqual('TEST-ELEMENT', el.tagName);
|
||||
testing.expectEqual(true, el instanceof HTMLElement);
|
||||
}
|
||||
|
||||
{
|
||||
const el = document.createElement('undefined-element');
|
||||
testing.expectEqual('UNDEFINED-ELEMENT', el.tagName);
|
||||
}
|
||||
|
||||
{
|
||||
const el = document.createElement('no-hyphen-invalid');
|
||||
testing.expectEqual('NO-HYPHEN-INVALID', el.tagName);
|
||||
}
|
||||
</script>
|
||||
95
src/browser/tests/custom_elements/upgrade.html
Normal file
95
src/browser/tests/custom_elements/upgrade.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<my-early id="early"></my-early>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="upgrade">
|
||||
{
|
||||
let constructorCalled = 0;
|
||||
let connectedCalled = 0;
|
||||
|
||||
class MyEarly extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalled++;
|
||||
this.upgraded = true;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
connectedCalled++;
|
||||
}
|
||||
}
|
||||
|
||||
const early = document.getElementById('early');
|
||||
testing.expectEqual(undefined, early.upgraded);
|
||||
testing.expectEqual(0, constructorCalled);
|
||||
testing.expectEqual(0, connectedCalled);
|
||||
|
||||
customElements.define('my-early', MyEarly);
|
||||
testing.expectEqual(true, early.upgraded);
|
||||
testing.expectEqual(1, constructorCalled);
|
||||
testing.expectEqual(1, connectedCalled);
|
||||
}
|
||||
|
||||
{
|
||||
let order = [];
|
||||
|
||||
class UpgradeParent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
order.push('parent-constructor');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
order.push('parent-connected');
|
||||
}
|
||||
}
|
||||
|
||||
class UpgradeChild extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
order.push('child-constructor');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
order.push('child-connected');
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
testing.expectEqual(0, order.length);
|
||||
|
||||
customElements.define('upgrade-parent', UpgradeParent);
|
||||
testing.expectEqual(2, order.length);
|
||||
testing.expectEqual('parent-constructor', order[0]);
|
||||
testing.expectEqual('parent-connected', order[1]);
|
||||
|
||||
customElements.define('upgrade-child', UpgradeChild);
|
||||
testing.expectEqual(4, order.length);
|
||||
testing.expectEqual('child-constructor', order[2]);
|
||||
testing.expectEqual('child-connected', order[3]);
|
||||
}
|
||||
|
||||
{
|
||||
let connectedCalled = 0;
|
||||
|
||||
class DetachedUpgrade extends HTMLElement {
|
||||
connectedCallback() {
|
||||
connectedCalled++;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<detached-upgrade></detached-upgrade>';
|
||||
|
||||
testing.expectEqual(0, connectedCalled);
|
||||
|
||||
customElements.define('detached-upgrade', DetachedUpgrade);
|
||||
testing.expectEqual(0, connectedCalled);
|
||||
|
||||
document.body.appendChild(container);
|
||||
testing.expectEqual(1, connectedCalled);
|
||||
}
|
||||
</script>
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<div id=main></div>
|
||||
|
||||
<script id=webcomponents>
|
||||
class LightPanda extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
connectedCallback() {
|
||||
this.append('connected');
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define("lightpanda-test", LightPanda);
|
||||
const main = document.getElementById('main');
|
||||
main.appendChild(document.createElement('lightpanda-test'));
|
||||
|
||||
testing.expectEqual('<lightpanda-test>connected</lightpanda-test>', main.innerHTML)
|
||||
testing.expectEqual('[object DataSet]', document.createElement('lightpanda-test').dataset.toString());
|
||||
testing.expectEqual('[object DataSet]', document.createElement('lightpanda-test.x').dataset.toString());
|
||||
</script>
|
||||
@@ -53,7 +53,7 @@
|
||||
testing.eventually(() => testing.expectEqual(1, wst1));
|
||||
|
||||
let wst2 = 1;
|
||||
window.setTimeout((a, b) => {wst2 = a + b}, 1, 2, 3);
|
||||
window.setTimeout((a, b) => {wst2 = a+ b}, 1, 2, 3);
|
||||
testing.eventually(() => testing.expectEqual(5, wst2));
|
||||
</script>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
testing.expectEqual(true, called);
|
||||
</script>
|
||||
|
||||
<!-- <script id=btoa_atob>
|
||||
<script id=btoa_atob>
|
||||
const b64 = btoa('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder')
|
||||
testing.expectEqual('aHR0cHM6Ly96aWdsYW5nLm9yZy9kb2N1bWVudGF0aW9uL21hc3Rlci9zdGQvI3N0ZC5iYXNlNjQuQmFzZTY0RGVjb2Rlcg==', b64);
|
||||
|
||||
@@ -79,9 +79,9 @@
|
||||
testing.expectError('Error: InvalidCharacterError', () => {
|
||||
atob('b');
|
||||
});
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
<!-- <script id=scroll>
|
||||
<script id=scroll>
|
||||
let scroll = false;
|
||||
let scrollend = false
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
testing.expectEqual(0, scrollY);
|
||||
testing.expectEqual(0, pageYOffset);
|
||||
</script>
|
||||
-->
|
||||
<!-- <script id=queueMicroTask>
|
||||
|
||||
<script id=queueMicroTask>
|
||||
var qm = false;
|
||||
window.queueMicrotask(() => {qm = true });
|
||||
testing.eventually(() => testing.expectEqual(true, qm));
|
||||
@@ -134,9 +134,9 @@
|
||||
dcl = e.target == document;
|
||||
});
|
||||
testing.eventually(() => testing.expectEqual(true, dcl));
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
<!-- <script id=window.onload>
|
||||
<script id=window.onload>
|
||||
let isWindowTarget = false;
|
||||
|
||||
const callback = (e) => isWindowTarget = e.target === window;
|
||||
@@ -150,9 +150,9 @@
|
||||
testing.expectEqual(callback, window.onload);
|
||||
|
||||
testing.eventually(() => testing.expectEqual(true, isWindowTarget));
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
<!-- <script id=reportError>
|
||||
<script id=reportError>
|
||||
let errorEventFired = false;
|
||||
let capturedError = null;
|
||||
|
||||
@@ -166,4 +166,4 @@
|
||||
|
||||
testing.expectEqual(true, errorEventFired);
|
||||
testing.expectEqual(testError, capturedError);
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
43
src/browser/webapi/CustomElementDefinition.zig
Normal file
43
src/browser/webapi/CustomElementDefinition.zig
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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 std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Element = @import("Element.zig");
|
||||
|
||||
const CustomElementDefinition = @This();
|
||||
|
||||
name: []const u8,
|
||||
constructor: js.Function,
|
||||
observed_attributes: std.StringHashMapUnmanaged(void) = .{},
|
||||
// For customized built-in elements, this is the element tag they extend (e.g., .button)
|
||||
// For autonomous custom elements, this is null
|
||||
extends: ?Element.Tag = null,
|
||||
|
||||
pub fn isAttributeObserved(self: *const CustomElementDefinition, name: []const u8) bool {
|
||||
return self.observed_attributes.contains(name);
|
||||
}
|
||||
|
||||
pub fn isAutonomous(self: *const CustomElementDefinition) bool {
|
||||
return self.extends == null;
|
||||
}
|
||||
|
||||
pub fn isCustomizedBuiltIn(self: *const CustomElementDefinition) bool {
|
||||
return self.extends != null;
|
||||
}
|
||||
174
src/browser/webapi/CustomElementRegistry.zig
Normal file
174
src/browser/webapi/CustomElementRegistry.zig
Normal file
@@ -0,0 +1,174 @@
|
||||
// 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 std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Element = @import("Element.zig");
|
||||
const CustomElementDefinition = @import("CustomElementDefinition.zig");
|
||||
|
||||
const CustomElementRegistry = @This();
|
||||
|
||||
_definitions: std.StringHashMapUnmanaged(*CustomElementDefinition) = .{},
|
||||
|
||||
const DefineOptions = struct {
|
||||
extends: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Function, options_: ?DefineOptions, page: *Page) !void {
|
||||
const options = options_ orelse DefineOptions{};
|
||||
|
||||
try validateName(name);
|
||||
|
||||
// Parse and validate extends option
|
||||
const extends_tag: ?Element.Tag = if (options.extends) |extends_name| blk: {
|
||||
const tag = std.meta.stringToEnum(Element.Tag, extends_name) orelse return error.NotSupported;
|
||||
|
||||
// Can't extend custom elements
|
||||
if (tag == .custom) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
break :blk tag;
|
||||
} else null;
|
||||
|
||||
const gop = try self._definitions.getOrPut(page.arena, name);
|
||||
if (gop.found_existing) {
|
||||
return error.AlreadyDefined;
|
||||
}
|
||||
|
||||
const owned_name = try page.dupeString(name);
|
||||
|
||||
const definition = try page._factory.create(CustomElementDefinition{
|
||||
.name = owned_name,
|
||||
.constructor = constructor,
|
||||
.extends = extends_tag,
|
||||
});
|
||||
|
||||
// Read observedAttributes static property from constructor
|
||||
if (constructor.getPropertyValue("observedAttributes") catch null) |observed_attrs| {
|
||||
if (observed_attrs.isArray()) {
|
||||
const len = observed_attrs.arrayLength();
|
||||
var i: u32 = 0;
|
||||
while (i < len) : (i += 1) {
|
||||
const attr_val = observed_attrs.arrayGet(i) catch continue;
|
||||
const attr_name = attr_val.toString(page.arena) catch continue;
|
||||
const owned_attr = page.dupeString(attr_name) catch continue;
|
||||
definition.observed_attributes.put(page.arena, owned_attr, {}) catch continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gop.key_ptr.* = owned_name;
|
||||
gop.value_ptr.* = definition;
|
||||
|
||||
// Upgrade any undefined custom elements with this name
|
||||
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;
|
||||
}
|
||||
|
||||
custom._definition = definition;
|
||||
|
||||
const node = custom.asNode();
|
||||
const prev_upgrading = page._upgrading_element;
|
||||
page._upgrading_element = node;
|
||||
defer page._upgrading_element = prev_upgrading;
|
||||
|
||||
var result: js.Function.Result = undefined;
|
||||
_ = definition.constructor.newInstance(&result) catch |err| {
|
||||
log.warn(.js, "custom element upgrade", .{ .name = name, .err = err });
|
||||
_ = page._undefined_custom_elements.swapRemove(idx);
|
||||
continue;
|
||||
};
|
||||
|
||||
if (node.isConnected()) {
|
||||
custom.invokeConnectedCallback(page);
|
||||
}
|
||||
|
||||
_ = page._undefined_custom_elements.swapRemove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function {
|
||||
const definition = self._definitions.get(name) orelse return null;
|
||||
return definition.constructor;
|
||||
}
|
||||
|
||||
fn validateName(name: []const u8) !void {
|
||||
if (name.len == 0) {
|
||||
return error.InvalidCustomElementName;
|
||||
}
|
||||
|
||||
if (std.mem.indexOf(u8, name, "-") == null) {
|
||||
return error.InvalidCustomElementName;
|
||||
}
|
||||
|
||||
if (name[0] < 'a' or name[0] > 'z') {
|
||||
return error.InvalidCustomElementName;
|
||||
}
|
||||
|
||||
const reserved_names = [_][]const u8{
|
||||
"annotation-xml",
|
||||
"color-profile",
|
||||
"font-face",
|
||||
"font-face-src",
|
||||
"font-face-uri",
|
||||
"font-face-format",
|
||||
"font-face-name",
|
||||
"missing-glyph",
|
||||
};
|
||||
|
||||
for (reserved_names) |reserved| {
|
||||
if (std.mem.eql(u8, name, reserved)) {
|
||||
return error.InvalidCustomElementName;
|
||||
}
|
||||
}
|
||||
|
||||
for (name) |c| {
|
||||
const valid = (c >= 'a' and c <= 'z') or
|
||||
(c >= '0' and c <= '9') or
|
||||
c == '-';
|
||||
if (!valid) {
|
||||
return error.InvalidCustomElementName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(CustomElementRegistry);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "CustomElementRegistry";
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
};
|
||||
|
||||
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true });
|
||||
pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: CustomElementRegistry" {
|
||||
try testing.htmlRunner("custom_elements", .{});
|
||||
}
|
||||
@@ -77,9 +77,21 @@ pub fn getURL(_: *const Document, page: *const Page) [:0]const u8 {
|
||||
return page.url;
|
||||
}
|
||||
|
||||
pub fn createElement(_: *const Document, name: []const u8, page: *Page) !*Element {
|
||||
const CreateElementOptions = struct {
|
||||
is: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
|
||||
const node = try page.createElement(null, name, null);
|
||||
return node.as(Element);
|
||||
const element = node.as(Element);
|
||||
|
||||
const options = options_ orelse return element;
|
||||
if (options.is) |is_value| {
|
||||
try element.setAttribute("is", is_value, page);
|
||||
try Element.Html.Custom.checkAndAttachBuiltIn(element, page);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
|
||||
|
||||
@@ -33,6 +33,7 @@ const EventTarget = @import("EventTarget.zig");
|
||||
const ErrorEvent = @import("event/ErrorEvent.zig");
|
||||
const MediaQueryList = @import("css/MediaQueryList.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
const CustomElementRegistry = @import("CustomElementRegistry.zig");
|
||||
|
||||
const Window = @This();
|
||||
|
||||
@@ -47,6 +48,7 @@ _on_load: ?js.Function = null,
|
||||
_location: *Location,
|
||||
_timer_id: u30 = 0,
|
||||
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
|
||||
_custom_elements: CustomElementRegistry = .{},
|
||||
|
||||
pub fn asEventTarget(self: *Window) *EventTarget {
|
||||
return self._proto;
|
||||
@@ -92,6 +94,10 @@ pub fn getHistory(self: *Window) *History {
|
||||
return &self._history;
|
||||
}
|
||||
|
||||
pub fn getCustomElements(self: *Window) *CustomElementRegistry {
|
||||
return &self._custom_elements;
|
||||
}
|
||||
|
||||
pub fn getOnLoad(self: *const Window) ?js.Function {
|
||||
return self._on_load;
|
||||
}
|
||||
@@ -228,8 +234,13 @@ fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: Schedul
|
||||
const timer_id = self._timer_id +% 1;
|
||||
self._timer_id = timer_id;
|
||||
|
||||
for (opts.params) |*js_obj| {
|
||||
js_obj.* = try js_obj.persist();
|
||||
const params = opts.params;
|
||||
var persisted_params: []js.Object = &.{};
|
||||
if (params.len > 0) {
|
||||
persisted_params = try page.arena.alloc(js.Object, params.len);
|
||||
for (params, persisted_params) |a, *ca| {
|
||||
ca.* = try a.persist();
|
||||
}
|
||||
}
|
||||
|
||||
const gop = try self._timers.getOrPut(page.arena, timer_id);
|
||||
@@ -244,7 +255,7 @@ fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: Schedul
|
||||
.page = page,
|
||||
.name = opts.name,
|
||||
.timer_id = timer_id,
|
||||
.params = opts.params,
|
||||
.params = persisted_params,
|
||||
.animation_frame = opts.animation_frame,
|
||||
.repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,
|
||||
});
|
||||
@@ -334,6 +345,7 @@ pub const JsApi = struct {
|
||||
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
|
||||
pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" });
|
||||
pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" });
|
||||
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
|
||||
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
|
||||
pub const fetch = bridge.function(Window.fetch, .{});
|
||||
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
|
||||
@@ -350,15 +362,21 @@ pub const JsApi = struct {
|
||||
pub const atob = bridge.function(Window.atob, .{});
|
||||
pub const reportError = bridge.function(Window.reportError, .{});
|
||||
pub const frames = bridge.accessor(Window.getWindow, null, .{ .cache = "frames" });
|
||||
pub const length = bridge.accessor(struct{
|
||||
fn wrap(_: *const Window) u32 { return 0; }
|
||||
pub const length = bridge.accessor(struct {
|
||||
fn wrap(_: *const Window) u32 {
|
||||
return 0;
|
||||
}
|
||||
}.wrap, null, .{ .cache = "length" });
|
||||
|
||||
pub const innerWidth = bridge.accessor(struct{
|
||||
fn wrap(_: *const Window) u32 { return 1920; }
|
||||
pub const innerWidth = bridge.accessor(struct {
|
||||
fn wrap(_: *const Window) u32 {
|
||||
return 1920;
|
||||
}
|
||||
}.wrap, null, .{ .cache = "innerWidth" });
|
||||
pub const innerHeight = bridge.accessor(struct{
|
||||
fn wrap(_: *const Window) u32 { return 1080; }
|
||||
pub const innerHeight = bridge.accessor(struct {
|
||||
fn wrap(_: *const Window) u32 {
|
||||
return 1080;
|
||||
}
|
||||
}.wrap, null, .{ .cache = "innerHeight" });
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const js = @import("../../js/js.zig");
|
||||
const reflect = @import("../../reflect.zig");
|
||||
|
||||
const Page = @import("../../Page.zig");
|
||||
const Node = @import("../Node.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
|
||||
@@ -57,6 +58,12 @@ const HtmlElement = @This();
|
||||
_type: Type,
|
||||
_proto: *Element,
|
||||
|
||||
// Special constructor for custom elements
|
||||
pub fn construct(page: *Page) !*Element {
|
||||
const node = page._upgrading_element orelse return error.IllegalConstructor;
|
||||
return node.is(Element) orelse return error.IllegalConstructor;
|
||||
}
|
||||
|
||||
pub const Type = union(enum) {
|
||||
anchor: Anchor,
|
||||
body: Body,
|
||||
@@ -145,6 +152,8 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(HtmlElement.construct, .{});
|
||||
};
|
||||
|
||||
pub const Build = struct {
|
||||
|
||||
@@ -16,17 +16,22 @@
|
||||
// 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 std = @import("std");
|
||||
const String = @import("../../../../string.zig").String;
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const log = @import("../../../../log.zig");
|
||||
const Page = @import("../../../Page.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
const CustomElementDefinition = @import("../../CustomElementDefinition.zig");
|
||||
|
||||
const Custom = @This();
|
||||
_proto: *HtmlElement,
|
||||
_tag_name: String,
|
||||
_definition: ?*CustomElementDefinition,
|
||||
|
||||
pub fn asElement(self: *Custom) *Element {
|
||||
return self._proto._proto;
|
||||
@@ -35,6 +40,112 @@ pub fn asNode(self: *Custom) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn invokeConnectedCallback(self: *Custom, page: *Page) void {
|
||||
self.invokeCallback("connectedCallback", .{}, page);
|
||||
}
|
||||
|
||||
pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void {
|
||||
self.invokeCallback("disconnectedCallback", .{}, page);
|
||||
}
|
||||
|
||||
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;
|
||||
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 {
|
||||
// Autonomous custom element
|
||||
if (element.is(Custom)) |custom| {
|
||||
custom.invokeConnectedCallback(page);
|
||||
return;
|
||||
}
|
||||
|
||||
// Customized built-in element
|
||||
invokeCallbackOnElement(element, "connectedCallback", .{}, page);
|
||||
}
|
||||
|
||||
pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void {
|
||||
// Autonomous custom element
|
||||
if (element.is(Custom)) |custom| {
|
||||
custom.invokeDisconnectedCallback(page);
|
||||
return;
|
||||
}
|
||||
|
||||
// Customized built-in element
|
||||
invokeCallbackOnElement(element, "disconnectedCallback", .{}, page);
|
||||
}
|
||||
|
||||
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void {
|
||||
// Autonomous custom element
|
||||
if (element.is(Custom)) |custom| {
|
||||
custom.invokeAttributeChangedCallback(name, old_value, new_value, page);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const context = page.js;
|
||||
|
||||
// Get the JS element object
|
||||
const js_val = context.zigValueToJs(element, .{}) catch return;
|
||||
const js_element = context.createObject(js_val);
|
||||
|
||||
// Call the callback method if it exists
|
||||
js_element.callMethod(void, callback_name, args) catch return;
|
||||
}
|
||||
|
||||
// Check if element has "is" attribute and attach customized built-in definition
|
||||
pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {
|
||||
const is_value = element.getAttributeSafe("is") orelse return;
|
||||
|
||||
const custom_elements = page.window.getCustomElements();
|
||||
const definition = custom_elements._definitions.get(is_value) orelse return;
|
||||
|
||||
const extends_tag = definition.extends orelse return;
|
||||
if (extends_tag != element.getTag()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach the definition
|
||||
try page.setCustomizedBuiltInDefinition(element, definition);
|
||||
|
||||
// Invoke constructor
|
||||
const prev_upgrading = page._upgrading_element;
|
||||
const node = element.asNode();
|
||||
page._upgrading_element = node;
|
||||
defer page._upgrading_element = prev_upgrading;
|
||||
|
||||
var result: js.Function.Result = undefined;
|
||||
_ = definition.constructor.newInstance(&result) catch |err| {
|
||||
log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err });
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
|
||||
if (self._definition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = page.js;
|
||||
|
||||
const js_val = context.zigValueToJs(self, .{}) catch return;
|
||||
const js_element = context.createObject(js_val);
|
||||
|
||||
js_element.callMethod(void, callback_name, args) catch return;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Custom);
|
||||
|
||||
|
||||
@@ -644,7 +644,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
|
||||
// debugger events
|
||||
|
||||
|
||||
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {
|
||||
// onRunMessageLoopOnPause is called when a breakpoint is hit.
|
||||
// Until quit pause, we must continue to run a nested message loop
|
||||
@@ -743,12 +742,9 @@ const IsolatedWorld = struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn createContextAndLoadPolyfills(self: *IsolatedWorld, arena: Allocator, page: *Page) !void {
|
||||
pub fn createContextAndLoadPolyfills(self: *IsolatedWorld, page: *Page) !void {
|
||||
// We need to recreate the isolated world context
|
||||
try self.createContext(page);
|
||||
|
||||
const loader = @import("../browser/polyfill/polyfill.zig");
|
||||
try loader.preload(arena, &self.executor.context.?);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ fn createIsolatedWorld(cmd: anytype) !void {
|
||||
|
||||
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
try world.createContextAndLoadPolyfills(bc.arena, page);
|
||||
try world.createContextAndLoadPolyfills(page);
|
||||
const js_context = &world.executor.context.?;
|
||||
|
||||
// Create the auxdata json for the contextCreated event
|
||||
@@ -281,7 +281,7 @@ pub fn pageRemove(bc: anytype) !void {
|
||||
|
||||
pub fn pageCreated(bc: anytype, page: *Page) !void {
|
||||
for (bc.isolated_worlds.items) |*isolated_world| {
|
||||
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
|
||||
try isolated_world.createContextAndLoadPolyfills(page);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user