Files
browser/src/browser/webapi/Element.zig
egrs a90bcde38c fix WPT failures: nodeName prefix case, PI validation, willValidate, maxLength
- uppercase entire qualified name in tagName (including prefix)
- validate PI data for "?>" and use proper XML Name production with Unicode
- implement willValidate on HTMLInputElement
- throw IndexSizeError DOMException for negative maxLength assignment

flips: Node-nodeName, Document-createProcessingInstruction, button,
maxlength, input-willvalidate (+6 subtests)
2026-02-21 13:11:06 +01:00

1749 lines
58 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 lp = @import("lightpanda");
const log = @import("../../log.zig");
const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const reflect = @import("../reflect.zig");
const Node = @import("Node.zig");
const CSS = @import("CSS.zig");
const ShadowRoot = @import("ShadowRoot.zig");
const EventTarget = @import("EventTarget.zig");
const collections = @import("collections.zig");
const Selector = @import("selector/Selector.zig");
const Animation = @import("animation/Animation.zig");
const DOMStringMap = @import("element/DOMStringMap.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
pub const DOMRect = @import("DOMRect.zig");
pub const Svg = @import("element/Svg.zig");
pub const Html = @import("element/Html.zig");
pub const Attribute = @import("element/Attribute.zig");
const Element = @This();
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
pub const NamespaceUriLookup = std.AutoHashMapUnmanaged(*Element, []const u8);
pub const ScrollPosition = struct {
x: u32 = 0,
y: u32 = 0,
};
pub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);
pub const Namespace = enum(u8) {
html,
svg,
mathml,
xml,
// We should keep the original value, but don't. If this becomes important
// consider storing it in a page lookup, like `_element_class_lists`, rather
// that adding a slice directly here (directly in every element).
unknown,
null,
pub fn toUri(self: Namespace) ?[]const u8 {
return switch (self) {
.html => "http://www.w3.org/1999/xhtml",
.svg => "http://www.w3.org/2000/svg",
.mathml => "http://www.w3.org/1998/Math/MathML",
.xml => "http://www.w3.org/XML/1998/namespace",
.unknown => "http://lightpanda.io/unsupported/namespace",
.null => null,
};
}
pub fn parse(namespace_: ?[]const u8) Namespace {
const namespace = namespace_ orelse return .null;
if (namespace.len == "http://www.w3.org/1999/xhtml".len) {
// Common case, avoid the string comparion. Recklessly
@branchHint(.likely);
return .html;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/XML/1998/namespace")) {
return .xml;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/svg")) {
return .svg;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/1998/Math/MathML")) {
return .mathml;
}
return .unknown;
}
};
_type: Type,
_proto: *Node,
_namespace: Namespace = .html,
_attributes: ?*Attribute.List = null,
pub const Type = union(enum) {
html: *Html,
svg: *Svg,
};
pub fn is(self: *Element, comptime T: type) ?*T {
const type_name = @typeName(T);
switch (self._type) {
.html => |el| {
if (T == Html) {
return el;
}
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.")) {
return el.is(T);
}
},
.svg => |svg| {
if (T == Svg) {
return svg;
}
if (comptime std.mem.startsWith(u8, type_name, "webapi.element.svg.")) {
return svg.is(T);
}
},
}
return null;
}
pub fn as(self: *Element, comptime T: type) *T {
return self.is(T).?;
}
pub fn asNode(self: *Element) *Node {
return self._proto;
}
pub fn asEventTarget(self: *Element) *EventTarget {
return self._proto.asEventTarget();
}
pub fn asConstNode(self: *const Element) *const Node {
return self._proto;
}
pub fn attributesEql(self: *const Element, other: *Element) bool {
if (self._attributes) |attr_list| {
const other_list = other._attributes orelse return false;
return attr_list.eql(other_list);
}
// Make sure no attrs in both sides.
return other._attributes == null;
}
/// TODO: localName and prefix comparison.
pub fn isEqualNode(self: *Element, other: *Element) bool {
const self_tag = self.getTagNameDump();
const other_tag = other.getTagNameDump();
// Compare namespaces and tags.
const dirty = self._namespace != other._namespace or !std.mem.eql(u8, self_tag, other_tag);
if (dirty) {
return false;
}
// Compare attributes.
if (!self.attributesEql(other)) {
return false;
}
// Compare children.
var self_iter = self.asNode().childrenIterator();
var other_iter = other.asNode().childrenIterator();
var self_count: usize = 0;
var other_count: usize = 0;
while (self_iter.next()) |self_node| : (self_count += 1) {
const other_node = other_iter.next() orelse return false;
other_count += 1;
if (self_node.isEqualNode(other_node)) {
continue;
}
return false;
}
// Make sure both have equal number of children.
return self_count == other_count;
}
pub fn getTagNameLower(self: *const Element) []const u8 {
switch (self._type) {
.html => |he| switch (he._type) {
.custom => |ce| {
@branchHint(.unlikely);
return ce._tag_name.str();
},
else => return switch (he._type) {
.anchor => "a",
.area => "area",
.base => "base",
.body => "body",
.br => "br",
.button => "button",
.canvas => "canvas",
.custom => |e| e._tag_name.str(),
.data => "data",
.datalist => "datalist",
.dialog => "dialog",
.directory => "dir",
.div => "div",
.dl => "dl",
.embed => "embed",
.fieldset => "fieldset",
.font => "font",
.form => "form",
.generic => |e| e._tag_name.str(),
.heading => |e| e._tag_name.str(),
.head => "head",
.html => "html",
.hr => "hr",
.iframe => "iframe",
.img => "img",
.input => "input",
.label => "label",
.legend => "legend",
.li => "li",
.link => "link",
.map => "map",
.media => |m| switch (m._type) {
.audio => "audio",
.video => "video",
.generic => "media",
},
.meta => "meta",
.meter => "meter",
.mod => |e| e._tag_name.str(),
.object => "object",
.ol => "ol",
.optgroup => "optgroup",
.option => "option",
.output => "output",
.p => "p",
.picture => "picture",
.param => "param",
.pre => "pre",
.progress => "progress",
.quote => |e| e._tag_name.str(),
.script => "script",
.select => "select",
.slot => "slot",
.source => "source",
.span => "span",
.style => "style",
.table => "table",
.table_caption => "caption",
.table_cell => |e| e._tag_name.str(),
.table_col => |e| e._tag_name.str(),
.table_row => "tr",
.table_section => |e| e._tag_name.str(),
.template => "template",
.textarea => "textarea",
.time => "time",
.title => "title",
.track => "track",
.ul => "ul",
.unknown => |e| e._tag_name.str(),
},
},
.svg => |svg| return svg._tag_name.str(),
}
}
pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
return switch (self._type) {
.html => |he| switch (he._type) {
.anchor => "A",
.area => "AREA",
.base => "BASE",
.body => "BODY",
.br => "BR",
.button => "BUTTON",
.canvas => "CANVAS",
.custom => |e| upperTagName(&e._tag_name, buf),
.data => "DATA",
.datalist => "DATALIST",
.dialog => "DIALOG",
.directory => "DIR",
.div => "DIV",
.dl => "DL",
.embed => "EMBED",
.fieldset => "FIELDSET",
.font => "FONT",
.form => "FORM",
.generic => |e| upperTagName(&e._tag_name, buf),
.heading => |e| upperTagName(&e._tag_name, buf),
.head => "HEAD",
.html => "HTML",
.hr => "HR",
.iframe => "IFRAME",
.img => "IMG",
.input => "INPUT",
.label => "LABEL",
.legend => "LEGEND",
.li => "LI",
.link => "LINK",
.map => "MAP",
.meta => "META",
.media => |m| switch (m._type) {
.audio => "AUDIO",
.video => "VIDEO",
.generic => "MEDIA",
},
.meter => "METER",
.mod => |e| upperTagName(&e._tag_name, buf),
.object => "OBJECT",
.ol => "OL",
.optgroup => "OPTGROUP",
.option => "OPTION",
.output => "OUTPUT",
.p => "P",
.picture => "PICTURE",
.param => "PARAM",
.pre => "PRE",
.progress => "PROGRESS",
.quote => |e| upperTagName(&e._tag_name, buf),
.script => "SCRIPT",
.select => "SELECT",
.slot => "SLOT",
.source => "SOURCE",
.span => "SPAN",
.style => "STYLE",
.table => "TABLE",
.table_caption => "CAPTION",
.table_cell => |e| upperTagName(&e._tag_name, buf),
.table_col => |e| upperTagName(&e._tag_name, buf),
.table_row => "TR",
.table_section => |e| upperTagName(&e._tag_name, buf),
.template => "TEMPLATE",
.textarea => "TEXTAREA",
.time => "TIME",
.title => "TITLE",
.track => "TRACK",
.ul => "UL",
.unknown => |e| switch (self._namespace) {
.html => upperTagName(&e._tag_name, buf),
.svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),
},
},
.svg => |svg| svg._tag_name.str(),
};
}
pub fn getTagNameDump(self: *const Element) []const u8 {
switch (self._type) {
.html => return self.getTagNameLower(),
.svg => |svg| return svg._tag_name.str(),
}
}
pub fn getNamespaceURI(self: *const Element) ?[]const u8 {
return self._namespace.toUri();
}
pub fn getNamespaceUri(self: *Element, page: *Page) ?[]const u8 {
if (self._namespace != .unknown) return self._namespace.toUri();
return page._element_namespace_uris.get(self);
}
pub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, page: *Page) ?[]const u8 {
// Hardcoded reserved prefixes
if (prefix) |p| {
if (std.mem.eql(u8, p, "xml")) return "http://www.w3.org/XML/1998/namespace";
if (std.mem.eql(u8, p, "xmlns")) return "http://www.w3.org/2000/xmlns/";
}
// Step 1: check element's own namespace/prefix
if (self.getNamespaceUri(page)) |ns_uri| {
const el_prefix = self._prefix();
const match = if (prefix == null and el_prefix == null)
true
else if (prefix != null and el_prefix != null)
std.mem.eql(u8, prefix.?, el_prefix.?)
else
false;
if (match) return ns_uri;
}
// Step 2: search xmlns attributes
if (self._attributes) |attrs| {
var iter = attrs.iterator();
while (iter.next()) |entry| {
if (prefix == null) {
if (entry._name.eql(comptime .wrap("xmlns"))) {
const val = entry._value.str();
return if (val.len == 0) null else val;
}
} else {
const name = entry._name.str();
if (std.mem.startsWith(u8, name, "xmlns:")) {
if (std.mem.eql(u8, name["xmlns:".len..], prefix.?)) {
const val = entry._value.str();
return if (val.len == 0) null else val;
}
}
}
}
}
// Step 3: recurse to parent element
const parent = self.asNode().parentElement() orelse return null;
return parent.lookupNamespaceURIForElement(prefix, page);
}
fn _prefix(self: *const Element) ?[]const u8 {
const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
return name[0..pos];
}
return null;
}
pub fn getLocalName(self: *Element) []const u8 {
const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
return name[pos + 1 ..];
}
return name;
}
// Wrapper methods that delegate to Html implementations
pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {
const he = self.is(Html) orelse return error.NotHtmlElement;
return he.getInnerText(writer);
}
pub fn setInnerText(self: *Element, text: []const u8, page: *Page) !void {
const he = self.is(Html) orelse return error.NotHtmlElement;
return he.setInnerText(text, page);
}
pub fn insertAdjacentHTML(
self: *Element,
position: []const u8,
html_or_xml: []const u8,
page: *Page,
) !void {
const he = self.is(Html) orelse return error.NotHtmlElement;
return he.insertAdjacentHTML(position, html_or_xml, page);
}
pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
const dump = @import("../dump.zig");
return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page);
}
pub fn setOuterHTML(self: *Element, html: []const u8, page: *Page) !void {
const node = self.asNode();
const parent = node._parent orelse return;
page.domChanged();
if (html.len > 0) {
const fragment = (try Node.DocumentFragment.init(page)).asNode();
try page.parseHtmlAsChildren(fragment, html);
try page.insertAllChildrenBefore(fragment, parent, node);
}
page.removeNode(parent, node, .{ .will_be_reconnected = false });
}
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
const dump = @import("../dump.zig");
return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page);
}
pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {
const parent = self.asNode();
// Remove all existing children
page.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
}
// Fast path: skip parsing if html is empty
if (html.len == 0) {
return;
}
// Parse and add new children
try page.parseHtmlAsChildren(parent, html);
}
pub fn getId(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("id")) orelse "";
}
pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe(comptime .wrap("id"), .wrap(value), page);
}
pub fn getSlot(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("slot")) orelse "";
}
pub fn setSlot(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe(comptime .wrap("slot"), .wrap(value), page);
}
pub fn getDir(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("dir")) orelse "";
}
pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe(comptime .wrap("dir"), .wrap(value), page);
}
pub fn getClassName(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("class")) orelse "";
}
pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe(comptime .wrap("class"), .wrap(value), page);
}
pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
const attributes = self._attributes orelse return .{};
return attributes.iterator();
}
pub fn getAttribute(self: *const Element, name: String, page: *Page) !?String {
const attributes = self._attributes orelse return null;
return attributes.get(name, page);
}
/// For simplicity, the namespace is currently ignored and only the local name is used.
pub fn getAttributeNS(
self: *const Element,
maybe_namespace: ?[]const u8,
local_name: String,
page: *Page,
) !?String {
if (maybe_namespace) |namespace| {
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
}
}
return self.getAttribute(local_name, page);
}
pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
const attributes = self._attributes orelse return null;
return attributes.getSafe(name);
}
pub fn hasAttribute(self: *const Element, name: String, page: *Page) !bool {
const attributes = self._attributes orelse return false;
const value = try attributes.get(name, page);
return value != null;
}
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
const attributes = self._attributes orelse return false;
return attributes.hasSafe(name);
}
pub fn hasAttributes(self: *const Element) bool {
const attributes = self._attributes orelse return false;
return attributes.isEmpty() == false;
}
pub fn getAttributeNode(self: *Element, name: String, page: *Page) !?*Attribute {
const attributes = self._attributes orelse return null;
return attributes.getAttribute(name, self, page);
}
pub fn setAttribute(self: *Element, name: String, value: String, page: *Page) !void {
try Attribute.validateAttributeName(name);
const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.put(name, value, self, page);
}
pub fn setAttributeNS(
self: *Element,
maybe_namespace: ?[]const u8,
qualified_name: []const u8,
value: String,
page: *Page,
) !void {
const attr_name = if (maybe_namespace) |namespace| blk: {
// For xmlns namespace, store the full qualified name (e.g. "xmlns:bar")
// so lookupNamespaceURI can find namespace declarations.
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/xmlns/")) {
break :blk qualified_name;
}
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
}
break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
qualified_name[idx + 1 ..]
else
qualified_name;
} else blk: {
break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
qualified_name[idx + 1 ..]
else
qualified_name;
};
return self.setAttribute(.wrap(attr_name), value, page);
}
pub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void {
const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.putSafe(name, value, self, page);
}
pub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {
return self._attributes orelse return self.createAttributeList(page);
}
pub fn createAttributeList(self: *Element, page: *Page) !*Attribute.List {
lp.assert(self._attributes == null, "Element.createAttributeList non-null _attributes", .{});
const a = try page.arena.create(Attribute.List);
a.* = .{ .normalize = self._namespace == .html };
self._attributes = a;
return a;
}
pub fn getShadowRoot(self: *Element, page: *Page) ?*ShadowRoot {
const shadow_root = page._element_shadow_roots.get(self) orelse return null;
if (shadow_root._mode == .closed) return null;
return shadow_root;
}
pub fn getAssignedSlot(self: *Element, page: *Page) ?*Html.Slot {
return page._element_assigned_slots.get(self);
}
pub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowRoot {
if (page._element_shadow_roots.get(self)) |_| {
return error.AlreadyHasShadowRoot;
}
const mode = try ShadowRoot.Mode.fromString(mode_str);
const shadow_root = try ShadowRoot.init(self, mode, page);
try page._element_shadow_roots.put(page.arena, self, shadow_root);
return shadow_root;
}
pub fn insertAdjacentElement(
self: *Element,
position: []const u8,
element: *Element,
page: *Page,
) !void {
const target_node, const prev_node = try self.asNode().findAdjacentNodes(position);
_ = try target_node.insertBefore(element.asNode(), prev_node, page);
}
pub fn insertAdjacentText(
self: *Element,
where: []const u8,
data: []const u8,
page: *Page,
) !void {
const text_node = try page.createTextNode(data);
const target_node, const prev_node = try self.asNode().findAdjacentNodes(where);
_ = try target_node.insertBefore(text_node, prev_node, page);
}
pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute {
if (attr._element) |el| {
if (el == self) {
return attr;
}
attr._element = null;
_ = try el.removeAttributeNode(attr, page);
}
const attributes = try self.getOrCreateAttributeList(page);
return attributes.putAttribute(attr, self, page);
}
pub fn removeAttribute(self: *Element, name: String, page: *Page) !void {
const attributes = self._attributes orelse return;
return attributes.delete(name, self, page);
}
pub fn toggleAttribute(self: *Element, name: String, force: ?bool, page: *Page) !bool {
try Attribute.validateAttributeName(name);
const has = try self.hasAttribute(name, page);
const should_add = force orelse !has;
if (should_add and !has) {
try self.setAttribute(name, String.empty, page);
return true;
} else if (!should_add and has) {
try self.removeAttribute(name, page);
return false;
}
return should_add;
}
pub fn removeAttributeNode(self: *Element, attr: *Attribute, page: *Page) !*Attribute {
if (attr._element == null or attr._element.? != self) {
return error.NotFound;
}
try self.removeAttribute(attr._name, page);
attr._element = null;
return attr;
}
pub fn getAttributeNames(self: *const Element, page: *Page) ![][]const u8 {
const attributes = self._attributes orelse return &.{};
return attributes.getNames(page);
}
pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNodeMap {
const gop = try page._attribute_named_node_map_lookup.getOrPut(page.arena, @intFromPtr(self));
if (!gop.found_existing) {
const attributes = try self.getOrCreateAttributeList(page);
const named_node_map = try page._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self });
gop.value_ptr.* = named_node_map;
}
return gop.value_ptr.*;
}
pub fn getOrCreateStyle(self: *Element, page: *Page) !*CSSStyleProperties {
const gop = try page._element_styles.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try CSSStyleProperties.init(self, false, page);
}
return gop.value_ptr.*;
}
fn getStyle(self: *Element, page: *Page) ?*CSSStyleProperties {
return page._element_styles.get(self);
}
pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
const gop = try page._element_class_lists.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = comptime .wrap("class"),
});
}
return gop.value_ptr.*;
}
pub fn setClassList(self: *Element, value: String, page: *Page) !void {
const class_list = try self.getClassList(page);
try class_list.setValue(value, page);
}
pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {
const gop = try page._element_rel_lists.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = comptime .wrap("rel"),
});
}
return gop.value_ptr.*;
}
pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
const gop = try page._element_datasets.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try page._factory.create(DOMStringMap{
._element = self,
});
}
return gop.value_ptr.*;
}
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
page.domChanged();
var parent = self.asNode();
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
}
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| {
var child_connected = false;
const child = try node_or_text.toNode(page);
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
}
}
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
page.domChanged();
const ref_node = self.asNode();
const parent = ref_node._parent orelse return;
const parent_is_connected = parent.isConnected();
// Detect if the ref_node must be removed (byt default) or kept.
// We kept it when ref_node is present into the nodes list.
var rm_ref_node = true;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
// If a child is the ref node. We keep it at its own current position.
if (child == ref_node) {
rm_ref_node = false;
continue;
}
if (child._parent) |current_parent| {
page.removeNode(current_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.insertNodeRelative(
parent,
child,
.{ .before = ref_node },
.{ .child_already_connected = child.isConnected() },
);
}
if (rm_ref_node) {
page.removeNode(parent, ref_node, .{ .will_be_reconnected = false });
}
}
pub fn remove(self: *Element, page: *Page) void {
page.domChanged();
const node = self.asNode();
const parent = node._parent orelse return;
page.removeNode(parent, node, .{ .will_be_reconnected = false });
}
pub fn focus(self: *Element, page: *Page) !void {
if (self.asNode().isConnected() == false) {
// a disconnected node cannot take focus
return;
}
const FocusEvent = @import("event/FocusEvent.zig");
const new_target = self.asEventTarget();
const old_active = page.document._active_element;
page.document._active_element = self;
if (old_active) |old| {
if (old == self) {
return;
}
const old_target = old.asEventTarget();
// Dispatch blur on old element (no bubble, composed)
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true, .relatedTarget = new_target }, page);
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
try page._event_manager.dispatch(old_target, blur_event.asEvent());
// Dispatch focusout on old element (bubbles, composed)
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, page);
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
}
const old_related: ?*EventTarget = if (old_active) |old| old.asEventTarget() else null;
// Dispatch focus on new element (no bubble, composed)
const focus_event = try FocusEvent.initTrusted(comptime .wrap("focus"), .{ .composed = true, .relatedTarget = old_related }, page);
defer if (!focus_event.asEvent()._v8_handoff) focus_event.deinit(false);
try page._event_manager.dispatch(new_target, focus_event.asEvent());
// Dispatch focusin on new element (bubbles, composed)
const focusin_event = try FocusEvent.initTrusted(comptime .wrap("focusin"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, page);
defer if (!focusin_event.asEvent()._v8_handoff) focusin_event.deinit(false);
try page._event_manager.dispatch(new_target, focusin_event.asEvent());
}
pub fn blur(self: *Element, page: *Page) !void {
if (page.document._active_element != self) return;
page.document._active_element = null;
const FocusEvent = @import("event/FocusEvent.zig");
const old_target = self.asEventTarget();
// Dispatch blur (no bubble, composed)
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true }, page);
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
try page._event_manager.dispatch(old_target, blur_event.asEvent());
// Dispatch focusout (bubbles, composed)
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true }, page);
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
}
pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) {
return collections.NodeLive(.child_elements).init(self.asNode(), {}, page);
}
pub fn append(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
const parent = self.asNode();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.appendChild(child, page);
}
}
pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
const parent = self.asNode();
var i = nodes.len;
while (i > 0) {
i -= 1;
const child = try nodes[i].toNode(page);
_ = try parent.insertBefore(child, parent.firstChild(), page);
}
}
pub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, node, page);
}
}
pub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, viable_next, page);
}
}
pub fn firstElementChild(self: *Element) ?*Element {
var maybe_child = self.asNode().firstChild();
while (maybe_child) |child| {
if (child.is(Element)) |el| return el;
maybe_child = child.nextSibling();
}
return null;
}
pub fn lastElementChild(self: *Element) ?*Element {
var maybe_child = self.asNode().lastChild();
while (maybe_child) |child| {
if (child.is(Element)) |el| return el;
maybe_child = child.previousSibling();
}
return null;
}
pub fn nextElementSibling(self: *Element) ?*Element {
var maybe_sibling = self.asNode().nextSibling();
while (maybe_sibling) |sibling| {
if (sibling.is(Element)) |el| return el;
maybe_sibling = sibling.nextSibling();
}
return null;
}
pub fn previousElementSibling(self: *Element) ?*Element {
var maybe_sibling = self.asNode().previousSibling();
while (maybe_sibling) |sibling| {
if (sibling.is(Element)) |el| return el;
maybe_sibling = sibling.previousSibling();
}
return null;
}
pub fn getChildElementCount(self: *Element) usize {
var count: usize = 0;
var it = self.asNode().childrenIterator();
while (it.next()) |node| {
if (node.is(Element) != null) {
count += 1;
}
}
return count;
}
pub fn matches(self: *Element, selector: []const u8, page: *Page) !bool {
return Selector.matches(self, selector, page);
}
pub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element {
return Selector.querySelector(self.asNode(), selector, page);
}
pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input, page);
}
pub fn getAnimations(_: *const Element) []*Animation {
return &.{};
}
pub fn animate(_: *Element, _: ?js.Object, _: ?js.Object, page: *Page) !*Animation {
return Animation.init(page);
}
pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element {
if (selector.len == 0) {
return error.SyntaxError;
}
var current: ?*Element = self;
while (current) |el| {
if (try Selector.matchesWithScope(el, selector, self, page)) {
return el;
}
const parent = el._proto._parent orelse break;
if (parent.is(ShadowRoot) != null) {
break;
}
current = parent.is(Element);
}
return null;
}
pub fn parentElement(self: *Element) ?*Element {
return self._proto.parentElement();
}
pub fn checkVisibility(self: *Element, page: *Page) bool {
var current: ?*Element = self;
while (current) |el| {
if (el.getStyle(page)) |style| {
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
if (std.mem.eql(u8, display, "none")) {
return false;
}
}
current = el.parentElement();
}
return true;
}
fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
var width: f64 = 5.0;
var height: f64 = 5.0;
if (self.getStyle(page)) |style| {
const decl = style.asCSSStyleDeclaration();
width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0;
height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 5.0;
}
if (width == 5.0 or height == 5.0) {
const tag = self.getTag();
// Root containers get large default size to contain descendant positions.
// With calculateDocumentPosition using linear depth scaling (100px per level),
// even very deep trees (100 levels) stay within 10,000px.
// 100M pixels is plausible for very long documents.
if (tag == .html or tag == .body) {
if (width == 5.0) width = 1920.0;
if (height == 5.0) height = 100_000_000.0;
} else if (tag == .img or tag == .iframe) {
if (self.getAttributeSafe(comptime .wrap("width"))) |w| {
width = std.fmt.parseFloat(f64, w) catch width;
}
if (self.getAttributeSafe(comptime .wrap("height"))) |h| {
height = std.fmt.parseFloat(f64, h) catch height;
}
}
}
return .{ .width = width, .height = height };
}
pub fn getClientWidth(self: *Element, page: *Page) f64 {
if (!self.checkVisibility(page)) {
return 0.0;
}
const dims = self.getElementDimensions(page);
return dims.width;
}
pub fn getClientHeight(self: *Element, page: *Page) f64 {
if (!self.checkVisibility(page)) {
return 0.0;
}
const dims = self.getElementDimensions(page);
return dims.height;
}
pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
if (!self.checkVisibility(page)) {
return .{
._x = 0.0,
._y = 0.0,
._width = 0.0,
._height = 0.0,
};
}
return self.getBoundingClientRectForVisible(page);
}
// Some cases need a the BoundingClientRect but have already done the
// visibility check.
pub fn getBoundingClientRectForVisible(self: *Element, page: *Page) DOMRect {
const y = calculateDocumentPosition(self.asNode());
const dims = self.getElementDimensions(page);
// Use sibling position for x coordinate to ensure siblings have different x values
const x = calculateSiblingPosition(self.asNode());
return .{
._x = x,
._y = y,
._width = dims.width,
._height = dims.height,
};
}
pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
if (!self.checkVisibility(page)) {
return &.{};
}
const rects = try page.call_arena.alloc(DOMRect, 1);
rects[0] = self.getBoundingClientRectForVisible(page);
return rects;
}
pub fn getScrollTop(self: *Element, page: *Page) u32 {
const pos = page._element_scroll_positions.get(self) orelse return 0;
return pos.y;
}
pub fn setScrollTop(self: *Element, value: i32, page: *Page) !void {
const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
gop.value_ptr.y = @intCast(@max(0, value));
}
pub fn getScrollLeft(self: *Element, page: *Page) u32 {
const pos = page._element_scroll_positions.get(self) orelse return 0;
return pos.x;
}
pub fn setScrollLeft(self: *Element, value: i32, page: *Page) !void {
const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
gop.value_ptr.x = @intCast(@max(0, value));
}
pub fn getScrollHeight(self: *Element, page: *Page) f64 {
// In our dummy layout engine, content doesn't overflow
return self.getClientHeight(page);
}
pub fn getScrollWidth(self: *Element, page: *Page) f64 {
// In our dummy layout engine, content doesn't overflow
return self.getClientWidth(page);
}
pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
if (!self.checkVisibility(page)) {
return 0.0;
}
const dims = self.getElementDimensions(page);
return dims.height;
}
pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
if (!self.checkVisibility(page)) {
return 0.0;
}
const dims = self.getElementDimensions(page);
return dims.width;
}
pub fn getOffsetTop(self: *Element, page: *Page) f64 {
if (!self.checkVisibility(page)) {
return 0.0;
}
return calculateDocumentPosition(self.asNode());
}
pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
if (!self.checkVisibility(page)) {
return 0.0;
}
return calculateSiblingPosition(self.asNode());
}
pub fn getClientTop(_: *Element) f64 {
// Border width - in our dummy layout, we don't apply borders to layout
return 0.0;
}
pub fn getClientLeft(_: *Element) f64 {
// Border width - in our dummy layout, we don't apply borders to layout
return 0.0;
}
// Calculates document position by counting all nodes that appear before this one
// in tree order, but only traversing the "left side" of the tree.
//
// This walks up from the target node to the root, and at each level counts:
// 1. All previous siblings and their descendants
// 2. The parent itself
//
// Example:
// <body> → y=0
// <h1>Text</h1> → y=1 (body=1)
// <h2> → y=2 (body=1 + h1=1)
// <a>Link1</a> → y=3 (body=1 + h1=1 + h2=1)
// </h2>
// <p>Text</p> → y=5 (body=1 + h1=1 + h2=2)
// <h2> → y=6 (body=1 + h1=1 + h2=2 + p=1)
// <a>Link2</a> → y=7 (body=1 + h1=1 + h2=2 + p=1 + h2=1)
// </h2>
// </body>
//
// Trade-offs:
// - O(depth × siblings × subtree_height) - only left-side traversal
// - Linear scaling: 5px per node
// - Perfect document order, guaranteed unique positions
// - Compact coordinates (1000 nodes ≈ 5,000px)
fn calculateDocumentPosition(node: *Node) f64 {
var position: f64 = 0.0;
var current = node;
// Walk up to root, counting preceding nodes
while (current.parentNode()) |parent| {
// Count all previous siblings and their descendants
var sibling = parent.firstChild();
while (sibling) |s| {
if (s == current) break;
position += countSubtreeNodes(s);
sibling = s.nextSibling();
}
// Count the parent itself
position += 1.0;
current = parent;
}
return position * 5.0; // 5px per node
}
// Counts total nodes in a subtree (node + all descendants)
fn countSubtreeNodes(node: *Node) f64 {
var count: f64 = 1.0; // Count this node
var child = node.firstChild();
while (child) |c| {
count += countSubtreeNodes(c);
child = c.nextSibling();
}
return count;
}
// Calculates horizontal position using the same approach as y,
// just scaled differently for visual distinction
fn calculateSiblingPosition(node: *Node) f64 {
var position: f64 = 0.0;
var current = node;
// Walk up to root, counting preceding nodes (same as y)
while (current.parentNode()) |parent| {
// Count all previous siblings and their descendants
var sibling = parent.firstChild();
while (sibling) |s| {
if (s == current) break;
position += countSubtreeNodes(s);
sibling = s.nextSibling();
}
// Count the parent itself
position += 1.0;
current = parent;
}
return position * 5.0; // 5px per node
}
pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {
return self.asNode().getElementsByTagName(tag_name, page);
}
pub fn getElementsByTagNameNS(self: *Element, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {
return self.asNode().getElementsByTagNameNS(namespace, local_name, page);
}
pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
return self.asNode().getElementsByClassName(class_name, page);
}
pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
const tag_name = self.getTagNameDump();
const node = try page.createElementNS(self._namespace, tag_name, self._attributes);
// Allow element-specific types to copy their runtime state
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), page }) catch |err| {
log.err(.dom, "element.clone.failed", .{ .err = err });
};
if (deep) {
var child_it = self.asNode().childrenIterator();
while (child_it.next()) |child| {
const cloned_child = try child.cloneNode(true, page);
// We pass `true` to `child_already_connected` as a hacky optimization
// We _know_ this child isn't connected (Becasue the parent isn't connected)
// setting this to `true` skips all connection checks and just assumes t
try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
}
}
return node;
}
pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void {
_ = center_if_needed;
}
const ScrollIntoViewOpts = union {
align_to_top: bool,
obj: js.Object,
};
pub fn scrollIntoView(_: *const Element, opts: ?ScrollIntoViewOpts) void {
_ = opts;
}
pub fn format(self: *Element, writer: *std.Io.Writer) !void {
try writer.writeByte('<');
try writer.writeAll(self.getTagNameDump());
if (self._attributes) |attributes| {
var it = attributes.iterator();
while (it.next()) |attr| {
try writer.print(" {f}", .{attr});
}
}
try writer.writeByte('>');
}
fn upperTagName(tag_name: *String, buf: []u8) []const u8 {
if (tag_name.len > buf.len) {
log.info(.dom, "tag.long.name", .{ .name = tag_name.str() });
return tag_name.str();
}
const tag = tag_name.str();
return std.ascii.upperString(buf, tag);
}
pub fn getTag(self: *const Element) Tag {
return switch (self._type) {
.html => |he| switch (he._type) {
.anchor => .anchor,
.area => .area,
.base => .base,
.div => .div,
.dl => .dl,
.embed => .embed,
.form => .form,
.p => .p,
.custom => .custom,
.data => .data,
.datalist => .datalist,
.dialog => .dialog,
.directory => .directory,
.iframe => .iframe,
.img => .img,
.br => .br,
.button => .button,
.canvas => .canvas,
.fieldset => .fieldset,
.font => .font,
.heading => |h| h._tag,
.label => .label,
.legend => .legend,
.li => .li,
.map => .map,
.ul => .ul,
.ol => .ol,
.object => .object,
.optgroup => .optgroup,
.output => .output,
.picture => .picture,
.param => .param,
.pre => .pre,
.generic => |g| g._tag,
.media => |m| switch (m._type) {
.audio => .audio,
.video => .video,
.generic => .media,
},
.meter => .meter,
.mod => |m| m._tag,
.progress => .progress,
.quote => |q| q._tag,
.script => .script,
.select => .select,
.slot => .slot,
.source => .source,
.span => .span,
.option => .option,
.table => .table,
.table_caption => .caption,
.table_cell => |tc| tc._tag,
.table_col => |tc| tc._tag,
.table_row => .tr,
.table_section => |ts| ts._tag,
.template => .template,
.textarea => .textarea,
.time => .time,
.track => .track,
.input => .input,
.link => .link,
.meta => .meta,
.hr => .hr,
.style => .style,
.title => .title,
.body => .body,
.html => .html,
.head => .head,
.unknown => .unknown,
},
.svg => |se| switch (se._type) {
.svg => .svg,
.generic => |g| g._tag,
},
};
}
pub const Tag = enum {
address,
anchor,
audio,
area,
aside,
article,
b,
blockquote,
body,
br,
button,
base,
canvas,
caption,
circle,
code,
col,
colgroup,
custom,
data,
datalist,
dd,
details,
del,
dfn,
dialog,
div,
directory,
dl,
dt,
embed,
ellipse,
em,
fieldset,
figure,
form,
font,
footer,
g,
h1,
h2,
h3,
h4,
h5,
h6,
head,
header,
heading,
hgroup,
hr,
html,
i,
iframe,
img,
input,
ins,
label,
legend,
li,
line,
link,
main,
map,
marquee,
media,
menu,
meta,
meter,
nav,
noscript,
object,
ol,
optgroup,
option,
output,
p,
path,
param,
picture,
polygon,
polyline,
pre,
progress,
quote,
rect,
s,
script,
section,
select,
slot,
source,
span,
strong,
style,
sub,
summary,
sup,
svg,
table,
time,
tbody,
td,
text,
template,
textarea,
tfoot,
th,
thead,
title,
tr,
track,
ul,
video,
unknown,
// If the tag is "unknown", we can't use the optimized tag matching, but
// need to fallback to the actual tag name
pub fn parseForMatch(lower: []const u8) ?Tag {
const tag = std.meta.stringToEnum(Tag, lower) orelse return null;
return switch (tag) {
.unknown, .custom => null,
else => tag,
};
}
};
pub const JsApi = struct {
pub const bridge = js.Bridge(Element);
pub const Meta = struct {
pub const name = "Element";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
};
pub const tagName = bridge.accessor(_tagName, null, .{});
fn _tagName(self: *Element, page: *Page) []const u8 {
return self.getTagNameSpec(&page.buf);
}
pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{});
fn _innerText(self: *Element, page: *const Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getInnerText(&buf.writer);
return buf.written();
}
pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{});
fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getOuterHTML(&buf.writer, page);
return buf.written();
}
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
fn _innerHTML(self: *Element, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getInnerHTML(&buf.writer, page);
return buf.written();
}
pub const prefix = bridge.accessor(Element._prefix, null, .{});
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true });
fn _setAttribute(self: *Element, name: String, value: js.Value, page: *Page) !void {
return self.setAttribute(name, .wrap(try value.toStringSlice()), page);
}
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true });
fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, page: *Page) !void {
return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), page);
}
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
pub const style = bridge.accessor(Element.getOrCreateStyle, null, .{});
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
pub const getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true });
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true });
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true });
const ShadowRootInit = struct {
mode: []const u8,
};
fn _attachShadow(self: *Element, init: ShadowRootInit, page: *Page) !*ShadowRoot {
return self.attachShadow(init.mode, page);
}
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true });
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true });
pub const remove = bridge.function(Element.remove, .{});
pub const append = bridge.function(Element.append, .{ .dom_exception = true });
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true });
pub const before = bridge.function(Element.before, .{ .dom_exception = true });
pub const after = bridge.function(Element.after, .{ .dom_exception = true });
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});
pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{});
pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });
pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
pub const closest = bridge.function(Element.closest, .{ .dom_exception = true });
pub const getAnimations = bridge.function(Element.getAnimations, .{});
pub const animate = bridge.function(Element.animate, .{});
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{});
pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{});
pub const clientTop = bridge.accessor(Element.getClientTop, null, .{});
pub const clientLeft = bridge.accessor(Element.getClientLeft, null, .{});
pub const scrollTop = bridge.accessor(Element.getScrollTop, Element.setScrollTop, .{});
pub const scrollLeft = bridge.accessor(Element.getScrollLeft, Element.setScrollLeft, .{});
pub const scrollHeight = bridge.accessor(Element.getScrollHeight, null, .{});
pub const scrollWidth = bridge.accessor(Element.getScrollWidth, null, .{});
pub const offsetTop = bridge.accessor(Element.getOffsetTop, null, .{});
pub const offsetLeft = bridge.accessor(Element.getOffsetLeft, null, .{});
pub const offsetWidth = bridge.accessor(Element.getOffsetWidth, null, .{});
pub const offsetHeight = bridge.accessor(Element.getOffsetHeight, null, .{});
pub const getClientRects = bridge.function(Element.getClientRects, .{});
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
pub const getElementsByTagNameNS = bridge.function(Element.getElementsByTagNameNS, .{});
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
pub const children = bridge.accessor(Element.getChildren, null, .{});
pub const focus = bridge.function(Element.focus, .{});
pub const blur = bridge.function(Element.blur, .{});
pub const scrollIntoView = bridge.function(Element.scrollIntoView, .{});
pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});
};
pub const Build = struct {
// Calls `func_name` with `args` on the most specific type where it is
// implement. This could be on the Element itself.
pub fn call(self: *const Element, comptime func_name: []const u8, args: anytype) !bool {
inline for (@typeInfo(Element.Type).@"union".fields) |f| {
if (@field(Element.Type, f.name) == self._type) {
// The inner type implements this function. Call it and we're done.
const S = reflect.Struct(f.type);
if (@hasDecl(S, "Build")) {
if (@hasDecl(S.Build, "call")) {
const sub = @field(self._type, f.name);
return S.Build.call(sub, func_name, args);
}
// The inner type implements this function. Call it and we're done.
if (@hasDecl(f.type, func_name)) {
return @call(.auto, @field(f.type, func_name), args);
}
}
}
}
if (@hasDecl(Element.Build, func_name)) {
// Our last resort - the element implements this function.
try @call(.auto, @field(Element.Build, func_name), args);
return true;
}
// inform our caller (the Node) that we didn't find anything that implemented
// func_name and it should keep searching for a match.
return false;
}
};
const testing = @import("../../testing.zig");
test "WebApi: Element" {
try testing.htmlRunner("element", .{});
}