Skip to content

Commit c5a1d8a

Browse files
committed
Element.checkVisibility and Element.checkVisibility
1 parent 32bad5f commit c5a1d8a

File tree

6 files changed

+206
-3
lines changed

6 files changed

+206
-3
lines changed

src/browser/js/bridge.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ pub const JsApis = flattenTypes(&.{
488488
@import("../webapi/DOMImplementation.zig"),
489489
@import("../webapi/DOMTreeWalker.zig"),
490490
@import("../webapi/DOMNodeIterator.zig"),
491+
@import("../webapi/DOMRect.zig"),
491492
@import("../webapi/NodeFilter.zig"),
492493
@import("../webapi/Element.zig"),
493494
@import("../webapi/element/DOMStringMap.zig"),

src/browser/webapi/DOMRect.zig

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const DOMRect = @This();
2+
3+
const js = @import("../js/js.zig");
4+
const Page = @import("../Page.zig");
5+
6+
_x: f64,
7+
_y: f64,
8+
_width: f64,
9+
_height: f64,
10+
_top: f64,
11+
_right: f64,
12+
_bottom: f64,
13+
_left: f64,
14+
15+
pub fn getX(self: *DOMRect) f64 {
16+
return self._x;
17+
}
18+
19+
pub fn getY(self: *DOMRect) f64 {
20+
return self._y;
21+
}
22+
23+
pub fn getWidth(self: *DOMRect) f64 {
24+
return self._width;
25+
}
26+
27+
pub fn getHeight(self: *DOMRect) f64 {
28+
return self._height;
29+
}
30+
31+
pub fn getTop(self: *DOMRect) f64 {
32+
return self._top;
33+
}
34+
35+
pub fn getRight(self: *DOMRect) f64 {
36+
return self._right;
37+
}
38+
39+
pub fn getBottom(self: *DOMRect) f64 {
40+
return self._bottom;
41+
}
42+
43+
pub fn getLeft(self: *DOMRect) f64 {
44+
return self._left;
45+
}
46+
47+
pub const JsApi = struct {
48+
pub const bridge = js.Bridge(DOMRect);
49+
50+
pub const Meta = struct {
51+
pub const name = "DOMRect";
52+
pub const prototype_chain = bridge.prototypeChain();
53+
pub var class_id: bridge.ClassId = undefined;
54+
};
55+
56+
pub const x = bridge.accessor(DOMRect.getX, null, .{});
57+
pub const y = bridge.accessor(DOMRect.getY, null, .{});
58+
pub const width = bridge.accessor(DOMRect.getWidth, null, .{});
59+
pub const height = bridge.accessor(DOMRect.getHeight, null, .{});
60+
pub const top = bridge.accessor(DOMRect.getTop, null, .{});
61+
pub const right = bridge.accessor(DOMRect.getRight, null, .{});
62+
pub const bottom = bridge.accessor(DOMRect.getBottom, null, .{});
63+
pub const left = bridge.accessor(DOMRect.getLeft, null, .{});
64+
};

src/browser/webapi/Element.zig

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const Selector = @import("selector/Selector.zig");
1313
pub const Attribute = @import("element/Attribute.zig");
1414
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
1515
pub const DOMStringMap = @import("element/DOMStringMap.zig");
16+
const DOMRect = @import("DOMRect.zig");
17+
const css = @import("css.zig");
1618

1719
pub const Svg = @import("element/Svg.zig");
1820
pub const Html = @import("element/Html.zig");
@@ -467,6 +469,126 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select
467469
return Selector.querySelectorAll(self.asNode(), input, page);
468470
}
469471

472+
pub fn parentElement(self: *Element) ?*Element {
473+
return self._proto.parentElement();
474+
}
475+
476+
pub fn checkVisibility(self: *Element, page: *Page) !bool {
477+
var current: ?*Element = self;
478+
479+
while (current) |el| {
480+
const style = try el.getStyle(page);
481+
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
482+
if (std.mem.eql(u8, display, "none")) {
483+
return false;
484+
}
485+
current = el.parentElement();
486+
}
487+
488+
return true;
489+
}
490+
491+
pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
492+
const is_visible = try self.checkVisibility(page);
493+
if (!is_visible) {
494+
return page._factory.create(DOMRect{
495+
._x = 0.0,
496+
._y = 0.0,
497+
._width = 0.0,
498+
._height = 0.0,
499+
._top = 0.0,
500+
._right = 0.0,
501+
._bottom = 0.0,
502+
._left = 0.0,
503+
});
504+
}
505+
506+
const y = calculateDocumentPosition(self.asNode());
507+
508+
var width: f64 = 1.0;
509+
var height: f64 = 1.0;
510+
511+
const style = try self.getStyle(page);
512+
const decl = style.asCSSStyleDeclaration();
513+
width = css.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0;
514+
height = css.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0;
515+
516+
if (width == 1.0 or height == 1.0) {
517+
const tag = self.getTag();
518+
if (tag == .img or tag == .iframe) {
519+
if (self.getAttributeSafe("width")) |w| {
520+
width = std.fmt.parseFloat(f64, w) catch width;
521+
}
522+
if (self.getAttributeSafe("height")) |h| {
523+
height = std.fmt.parseFloat(f64, h) catch height;
524+
}
525+
}
526+
}
527+
528+
const x: f64 = 0.0;
529+
const top = y;
530+
const left = x;
531+
const right = x + width;
532+
const bottom = y + height;
533+
534+
return page._factory.create(DOMRect{
535+
._x = x,
536+
._y = y,
537+
._width = width,
538+
._height = height,
539+
._top = top,
540+
._right = right,
541+
._bottom = bottom,
542+
._left = left,
543+
});
544+
}
545+
546+
// Calculates a pseudo-position in the document using an efficient heuristic.
547+
//
548+
// Instead of walking the entire DOM tree (which would be O(total_nodes)), this
549+
// function walks UP the tree counting previous siblings at each level. Each level
550+
// uses exponential weighting (1000x per depth level) to preserve document order.
551+
//
552+
// This gives O(depth * avg_siblings) complexity while maintaining relative positioning
553+
// that's useful for scraping and understanding element flow in the document.
554+
//
555+
// Example:
556+
// <body> → position 0
557+
// <div> → position 0 (0 siblings at level 1)
558+
// <span></span> → position 0 (0 siblings at level 2)
559+
// <span></span> → position 1 (1 sibling at level 2)
560+
// </div>
561+
// <div> → position 1000 (1 sibling at level 1, weighted by 1000)
562+
// <p></p> → position 1000 (0 siblings at level 2, parent has 1000)
563+
// </div>
564+
// </body>
565+
//
566+
// Trade-offs:
567+
// - Much faster than full tree-walking for deep/large DOMs
568+
// - Positions reflect document order and parent-child relationships
569+
// - Not pixel-accurate, but sufficient for 1x1 layout heuristics
570+
fn calculateDocumentPosition(node: *Node) f64 {
571+
var position: f64 = 0.0;
572+
var multiplier: f64 = 1.0;
573+
var current = node;
574+
575+
while (current.parentNode()) |parent| {
576+
var count: f64 = 0.0;
577+
var sibling = parent.firstChild();
578+
while (sibling) |s| {
579+
if (s == current) break;
580+
count += 1.0;
581+
sibling = s.nextSibling();
582+
}
583+
584+
position += count * multiplier;
585+
multiplier *= 1000.0;
586+
current = parent;
587+
}
588+
589+
return position;
590+
}
591+
470592
const GetElementsByTagNameResult = union(enum) {
471593
tag: collections.NodeLive(.tag),
472594
tag_name: collections.NodeLive(.tag_name),
@@ -702,6 +824,8 @@ pub const JsApi = struct {
702824
pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });
703825
pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
704826
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
827+
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
828+
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
705829
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
706830
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
707831
pub const children = bridge.accessor(Element.getChildren, null, .{});

src/browser/webapi/css.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const std = @import("std");
2+
3+
pub fn parseDimension(value: []const u8) ?f64 {
4+
if (value.len == 0) {
5+
return null;
6+
}
7+
8+
var num_str = value;
9+
if (std.mem.endsWith(u8, value, "px")) {
10+
num_str = value[0 .. value.len - 2];
11+
}
12+
13+
return std.fmt.parseFloat(f64, num_str) catch null;
14+
}

src/browser/webapi/css/CSSStyleDeclaration.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 {
5757
return "";
5858
}
5959

60-
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
60+
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
6161
const normalized = normalizePropertyName(property_name, &page.buf);
6262
const prop = self.findProperty(normalized) orelse return "";
6363
return prop._value.str();
6464
}
6565

66-
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
66+
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
6767
const normalized = normalizePropertyName(property_name, &page.buf);
6868
const prop = self.findProperty(normalized) orelse return "";
6969
return if (prop._important) "important" else "";

src/browser/webapi/css/CSSStyleProperties.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ pub const JsApi = struct {
154154
}
155155
}
156156

157-
const value = try self._proto.getPropertyValue(dash_case, page);
157+
const value = self._proto.getPropertyValue(dash_case, page);
158158

159159
// Property accessors have special handling for empty values:
160160
// - Known CSS properties return '' when not set

0 commit comments

Comments
 (0)