Element.checkVisibility and Element.checkVisibility

This commit is contained in:
Karl Seguin
2025-11-13 20:18:34 +08:00
parent 32bad5f8bb
commit c5a1d8a8bd
6 changed files with 206 additions and 3 deletions

View File

@@ -488,6 +488,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/DOMImplementation.zig"),
@import("../webapi/DOMTreeWalker.zig"),
@import("../webapi/DOMNodeIterator.zig"),
@import("../webapi/DOMRect.zig"),
@import("../webapi/NodeFilter.zig"),
@import("../webapi/Element.zig"),
@import("../webapi/element/DOMStringMap.zig"),

View File

@@ -0,0 +1,64 @@
const DOMRect = @This();
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
_x: f64,
_y: f64,
_width: f64,
_height: f64,
_top: f64,
_right: f64,
_bottom: f64,
_left: f64,
pub fn getX(self: *DOMRect) f64 {
return self._x;
}
pub fn getY(self: *DOMRect) f64 {
return self._y;
}
pub fn getWidth(self: *DOMRect) f64 {
return self._width;
}
pub fn getHeight(self: *DOMRect) f64 {
return self._height;
}
pub fn getTop(self: *DOMRect) f64 {
return self._top;
}
pub fn getRight(self: *DOMRect) f64 {
return self._right;
}
pub fn getBottom(self: *DOMRect) f64 {
return self._bottom;
}
pub fn getLeft(self: *DOMRect) f64 {
return self._left;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DOMRect);
pub const Meta = struct {
pub const name = "DOMRect";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const x = bridge.accessor(DOMRect.getX, null, .{});
pub const y = bridge.accessor(DOMRect.getY, null, .{});
pub const width = bridge.accessor(DOMRect.getWidth, null, .{});
pub const height = bridge.accessor(DOMRect.getHeight, null, .{});
pub const top = bridge.accessor(DOMRect.getTop, null, .{});
pub const right = bridge.accessor(DOMRect.getRight, null, .{});
pub const bottom = bridge.accessor(DOMRect.getBottom, null, .{});
pub const left = bridge.accessor(DOMRect.getLeft, null, .{});
};

View File

@@ -13,6 +13,8 @@ const Selector = @import("selector/Selector.zig");
pub const Attribute = @import("element/Attribute.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
pub const DOMStringMap = @import("element/DOMStringMap.zig");
const DOMRect = @import("DOMRect.zig");
const css = @import("css.zig");
pub const Svg = @import("element/Svg.zig");
pub const Html = @import("element/Html.zig");
@@ -467,6 +469,126 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select
return Selector.querySelectorAll(self.asNode(), input, page);
}
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| {
const style = try el.getStyle(page);
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
if (std.mem.eql(u8, display, "none")) {
return false;
}
current = el.parentElement();
}
return true;
}
pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
const is_visible = try self.checkVisibility(page);
if (!is_visible) {
return page._factory.create(DOMRect{
._x = 0.0,
._y = 0.0,
._width = 0.0,
._height = 0.0,
._top = 0.0,
._right = 0.0,
._bottom = 0.0,
._left = 0.0,
});
}
const y = calculateDocumentPosition(self.asNode());
var width: f64 = 1.0;
var height: f64 = 1.0;
const style = try self.getStyle(page);
const decl = style.asCSSStyleDeclaration();
width = css.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0;
height = css.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0;
if (width == 1.0 or height == 1.0) {
const tag = self.getTag();
if (tag == .img or tag == .iframe) {
if (self.getAttributeSafe("width")) |w| {
width = std.fmt.parseFloat(f64, w) catch width;
}
if (self.getAttributeSafe("height")) |h| {
height = std.fmt.parseFloat(f64, h) catch height;
}
}
}
const x: f64 = 0.0;
const top = y;
const left = x;
const right = x + width;
const bottom = y + height;
return page._factory.create(DOMRect{
._x = x,
._y = y,
._width = width,
._height = height,
._top = top,
._right = right,
._bottom = bottom,
._left = left,
});
}
// Calculates a pseudo-position in the document using an efficient heuristic.
//
// Instead of walking the entire DOM tree (which would be O(total_nodes)), this
// function walks UP the tree counting previous siblings at each level. Each level
// uses exponential weighting (1000x per depth level) to preserve document order.
//
// This gives O(depth * avg_siblings) complexity while maintaining relative positioning
// that's useful for scraping and understanding element flow in the document.
//
// Example:
// <body> → position 0
// <div> → position 0 (0 siblings at level 1)
// <span></span> → position 0 (0 siblings at level 2)
// <span></span> → position 1 (1 sibling at level 2)
// </div>
// <div> → position 1000 (1 sibling at level 1, weighted by 1000)
// <p></p> → position 1000 (0 siblings at level 2, parent has 1000)
// </div>
// </body>
//
// Trade-offs:
// - Much faster than full tree-walking for deep/large DOMs
// - Positions reflect document order and parent-child relationships
// - Not pixel-accurate, but sufficient for 1x1 layout heuristics
fn calculateDocumentPosition(node: *Node) f64 {
var position: f64 = 0.0;
var multiplier: f64 = 1.0;
var current = node;
while (current.parentNode()) |parent| {
var count: f64 = 0.0;
var sibling = parent.firstChild();
while (sibling) |s| {
if (s == current) break;
count += 1.0;
sibling = s.nextSibling();
}
position += count * multiplier;
multiplier *= 1000.0;
current = parent;
}
return position;
}
const GetElementsByTagNameResult = union(enum) {
tag: collections.NodeLive(.tag),
tag_name: collections.NodeLive(.tag_name),
@@ -702,6 +824,8 @@ pub const JsApi = struct {
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 checkVisibility = bridge.function(Element.checkVisibility, .{});
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
pub const children = bridge.accessor(Element.getChildren, null, .{});

View File

@@ -0,0 +1,14 @@
const std = @import("std");
pub fn parseDimension(value: []const u8) ?f64 {
if (value.len == 0) {
return null;
}
var num_str = value;
if (std.mem.endsWith(u8, value, "px")) {
num_str = value[0 .. value.len - 2];
}
return std.fmt.parseFloat(f64, num_str) catch null;
}

View File

@@ -57,13 +57,13 @@ pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 {
return "";
}
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse return "";
return prop._value.str();
}
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse return "";
return if (prop._important) "important" else "";

View File

@@ -154,7 +154,7 @@ pub const JsApi = struct {
}
}
const value = try self._proto.getPropertyValue(dash_case, page);
const value = self._proto.getPropertyValue(dash_case, page);
// Property accessors have special handling for empty values:
// - Known CSS properties return '' when not set