diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index b5f41df9..f34cb84e 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -106,6 +106,7 @@ _element_rel_lists: Element.RelListLookup = .empty,
_element_shadow_roots: Element.ShadowRootLookup = .empty,
_node_owner_documents: Node.OwnerDocumentLookup = .empty,
_element_assigned_slots: Element.AssignedSlotLookup = .empty,
+_element_scroll_positions: Element.ScrollPositionLookup = .empty,
/// Lazily-created inline event listeners (or listeners provided as attributes).
/// Avoids bloating all elements with extra function fields for rare usage.
diff --git a/src/browser/tests/element/position.html b/src/browser/tests/element/position.html
new file mode 100644
index 00000000..178951dc
--- /dev/null
+++ b/src/browser/tests/element/position.html
@@ -0,0 +1,116 @@
+
+
+
+
Test Element
+Another Element
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 81cef212..3d8dbbd0 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -49,6 +49,12 @@ pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTok
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
+pub const ScrollPosition = struct {
+ x: u32 = 0,
+ y: u32 = 0,
+};
+pub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);
+
pub const Namespace = enum(u8) {
html,
svg,
@@ -1027,6 +1033,82 @@ pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
return ptr[0..1];
}
+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 (!try self.checkVisibility(page)) {
+ return 0.0;
+ }
+ const dims = try self.getElementDimensions(page);
+ return dims.height;
+}
+
+pub fn getOffsetWidth(self: *Element, page: *Page) !f64 {
+ if (!try self.checkVisibility(page)) {
+ return 0.0;
+ }
+ const dims = try self.getElementDimensions(page);
+ return dims.width;
+}
+
+pub fn getOffsetTop(self: *Element, page: *Page) !f64 {
+ if (!try self.checkVisibility(page)) {
+ return 0.0;
+ }
+ return calculateDocumentPosition(self.asNode());
+}
+
+pub fn getOffsetLeft(self: *Element, page: *Page) !f64 {
+ if (!try 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.
//
@@ -1502,6 +1584,16 @@ pub const JsApi = struct {
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, .{});