mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 15:28:57 +00:00
add Document.elementFromPoint and elementsFromPoint
This commit is contained in:
233
src/browser/tests/document/element_from_point.html
Normal file
233
src/browser/tests/document/element_from_point.html
Normal file
@@ -0,0 +1,233 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<body>
|
||||
<div id="div1" style="width: 100px; height: 50px;">Div 1</div>
|
||||
<div id="div2" style="width: 100px; height: 50px;">Div 2</div>
|
||||
<div id="hidden" style="display: none; width: 100px; height: 50px;">Hidden</div>
|
||||
<div id="parent" style="width: 100px; height: 50px;">
|
||||
<div id="child" style="width: 80px; height: 30px;">Child</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script id="basic_usage">
|
||||
{
|
||||
// Test finding an element at a specific point
|
||||
const div1 = document.getElementById('div1');
|
||||
const rect1 = div1.getBoundingClientRect();
|
||||
|
||||
// Query near the top of div1 to avoid overlap with later elements
|
||||
const x = rect1.left + 10;
|
||||
const y = rect1.top + 5;
|
||||
const element = document.elementFromPoint(x, y);
|
||||
|
||||
// Should return div1 or a parent (body/html) - not null
|
||||
testing.expectTrue(element !== null);
|
||||
// If it returns div1 specifically, that's ideal
|
||||
// But we also accept parent elements
|
||||
testing.expectTrue(element === div1 || element.tagName === 'BODY' || element.tagName === 'HTML');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="nested_elements">
|
||||
{
|
||||
// Test that nested elements are found (topmost in document order)
|
||||
const child = document.getElementById('child');
|
||||
const parent = document.getElementById('parent');
|
||||
const rect = child.getBoundingClientRect();
|
||||
|
||||
const centerX = (rect.left + rect.right) / 2;
|
||||
const centerY = (rect.top + rect.bottom) / 2;
|
||||
const element = document.elementFromPoint(centerX, centerY);
|
||||
|
||||
// Should return the child element (topmost in document order) or parent
|
||||
testing.expectTrue(element !== null);
|
||||
testing.expectTrue(element === child || element === parent ||
|
||||
element.tagName === 'BODY' || element.tagName === 'HTML');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="hidden_elements">
|
||||
{
|
||||
// Test that hidden elements are not returned
|
||||
const hidden = document.getElementById('hidden');
|
||||
const rect = hidden.getBoundingClientRect();
|
||||
|
||||
// Even though hidden element has dimensions, it shouldn't be returned
|
||||
// because it has display: none
|
||||
const centerX = (rect.left + rect.right) / 2;
|
||||
const centerY = (rect.top + rect.bottom) / 2;
|
||||
const element = document.elementFromPoint(centerX, centerY);
|
||||
|
||||
// Should not return the hidden element (should return body or html instead)
|
||||
testing.expectTrue(element === null || element.id !== 'hidden');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="outside_viewport">
|
||||
{
|
||||
// Test points outside all elements
|
||||
const element = document.elementFromPoint(-1000, -1000);
|
||||
testing.expectEqual(null, element);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="at_element_center">
|
||||
{
|
||||
// Test querying at the center of an element
|
||||
const div1 = document.getElementById('div1');
|
||||
const rect = div1.getBoundingClientRect();
|
||||
|
||||
const centerX = (rect.left + rect.right) / 2;
|
||||
const centerY = (rect.top + rect.bottom) / 2;
|
||||
const element = document.elementFromPoint(centerX, centerY);
|
||||
|
||||
// Should return div1 or one of its ancestors (body/html)
|
||||
testing.expectTrue(element !== null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="dynamically_created">
|
||||
{
|
||||
// Test with dynamically created elements
|
||||
const newDiv = document.createElement('div');
|
||||
newDiv.style.width = '50px';
|
||||
newDiv.style.height = '50px';
|
||||
newDiv.textContent = 'Dynamic';
|
||||
document.body.appendChild(newDiv);
|
||||
|
||||
const rect = newDiv.getBoundingClientRect();
|
||||
const centerX = (rect.left + rect.right) / 2;
|
||||
const centerY = (rect.top + rect.bottom) / 2;
|
||||
const element = document.elementFromPoint(centerX, centerY);
|
||||
|
||||
testing.expectEqual(newDiv, element);
|
||||
|
||||
// Clean up
|
||||
newDiv.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="elementsFromPoint_basic">
|
||||
{
|
||||
// Test that elementsFromPoint returns array of element and its ancestors
|
||||
const div1 = document.getElementById('div1');
|
||||
const rect = div1.getBoundingClientRect();
|
||||
|
||||
const x = rect.left + 10;
|
||||
const y = rect.top + 5;
|
||||
const elements = document.elementsFromPoint(x, y);
|
||||
|
||||
// Should return an array
|
||||
testing.expectTrue(Array.isArray(elements));
|
||||
testing.expectTrue(elements.length > 0);
|
||||
|
||||
// First element should be div1 or one of its ancestors
|
||||
testing.expectTrue(elements[0] !== null);
|
||||
|
||||
// All elements should be ancestors of each other (parent chain)
|
||||
for (let i = 1; i < elements.length; i++) {
|
||||
let found = false;
|
||||
let parent = elements[i - 1].parentElement;
|
||||
while (parent) {
|
||||
if (parent === elements[i]) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
testing.expectTrue(found || elements[i].tagName === 'HTML');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="elementsFromPoint_nested">
|
||||
{
|
||||
// Test with nested elements - should return child, parent, body, html in order
|
||||
const child = document.getElementById('child');
|
||||
const parent = document.getElementById('parent');
|
||||
const rect = child.getBoundingClientRect();
|
||||
|
||||
const centerX = (rect.left + rect.right) / 2;
|
||||
const centerY = (rect.top + rect.bottom) / 2;
|
||||
const elements = document.elementsFromPoint(centerX, centerY);
|
||||
|
||||
testing.expectTrue(Array.isArray(elements));
|
||||
testing.expectTrue(elements.length >= 2); // At least child and parent (or more ancestors)
|
||||
|
||||
// First element should be the deepest (child or parent)
|
||||
testing.expectTrue(elements[0] === child || elements[0] === parent ||
|
||||
elements[0].tagName === 'BODY' || elements[0].tagName === 'HTML');
|
||||
|
||||
// If we got child as first element, parent should be in the array
|
||||
if (elements[0] === child) {
|
||||
let foundParent = false;
|
||||
for (let el of elements) {
|
||||
if (el === parent) {
|
||||
foundParent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
testing.expectTrue(foundParent);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="elementsFromPoint_order">
|
||||
{
|
||||
// Test that elements are returned in order from topmost to deepest
|
||||
const child = document.getElementById('child');
|
||||
const parent = document.getElementById('parent');
|
||||
const rect = child.getBoundingClientRect();
|
||||
|
||||
const centerX = (rect.left + rect.right) / 2;
|
||||
const centerY = (rect.top + rect.bottom) / 2;
|
||||
const elements = document.elementsFromPoint(centerX, centerY);
|
||||
|
||||
// Elements should be ordered such that each element is a parent of the previous
|
||||
for (let i = 1; i < elements.length; i++) {
|
||||
const current = elements[i - 1];
|
||||
const next = elements[i];
|
||||
|
||||
// next should be an ancestor of current
|
||||
let isAncestor = false;
|
||||
let p = current.parentElement;
|
||||
while (p) {
|
||||
if (p === next) {
|
||||
isAncestor = true;
|
||||
break;
|
||||
}
|
||||
p = p.parentElement;
|
||||
}
|
||||
|
||||
testing.expectTrue(isAncestor || next.tagName === 'HTML');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="elementsFromPoint_outside">
|
||||
{
|
||||
// Test with point outside all elements
|
||||
const elements = document.elementsFromPoint(-1000, -1000);
|
||||
|
||||
testing.expectTrue(Array.isArray(elements));
|
||||
testing.expectEqual(0, elements.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="elementsFromPoint_hidden">
|
||||
{
|
||||
// Test that hidden elements are not included
|
||||
const hidden = document.getElementById('hidden');
|
||||
const rect = hidden.getBoundingClientRect();
|
||||
|
||||
const centerX = (rect.left + rect.right) / 2;
|
||||
const centerY = (rect.top + rect.bottom) / 2;
|
||||
const elements = document.elementsFromPoint(centerX, centerY);
|
||||
|
||||
// Should not include the hidden element
|
||||
for (let el of elements) {
|
||||
testing.expectTrue(el.id !== 'hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -52,13 +52,6 @@
|
||||
let div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
div1.getClientRects(); // clal this to position it
|
||||
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString());
|
||||
|
||||
let elems = document.elementsFromPoint(2.5, 2.5);
|
||||
testing.expectEqual(3, elems.length);
|
||||
testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
|
||||
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
|
||||
testing.expectEqual('[object HTMLHtmlElement]', elems[2].toString());
|
||||
|
||||
let a = document.createElement('a');
|
||||
a.href = "https://lightpanda.io";
|
||||
@@ -66,20 +59,11 @@
|
||||
// Note this will be placed after the div of previous test
|
||||
a.getClientRects();
|
||||
|
||||
let a_again = document.elementFromPoint(7.5, 0.5);
|
||||
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
|
||||
testing.expectEqual('https://lightpanda.io', a_again.href);
|
||||
|
||||
let a_agains = document.elementsFromPoint(7.5, 0.5);
|
||||
testing.expectEqual('https://lightpanda.io', a_agains[0].href);
|
||||
|
||||
|
||||
testing.expectEqual(true, !document.all);
|
||||
testing.expectEqual(false, !!document.all);
|
||||
testing.expectEqual('[object HTMLScriptElement]', document.all(5).toString());
|
||||
testing.expectEqual('[object HTMLScriptElement]', document.all(6).toString());
|
||||
testing.expectEqual('[object HTMLDivElement]', document.all('content').toString());
|
||||
|
||||
|
||||
testing.expectEqual(document, document.defaultView.document );
|
||||
testing.expectEqual('loading', document.readyState);
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
let lyric = new Image
|
||||
testing.expectEqual('', lyric.src);
|
||||
lyric.src = 'okay';
|
||||
testing.expectEqual('okay', lyric.src);
|
||||
testing.expectEqual('http://localhost:9589/html/okay', lyric.src);
|
||||
lyric.src = 15;
|
||||
testing.expectEqual('15', lyric.src);
|
||||
testing.expectEqual('http://localhost:9589/html/15', lyric.src);
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
testing.expectEqual('_blank', link.target);
|
||||
link.target = '';
|
||||
|
||||
testing.expectEqual('foo', link.href);
|
||||
testing.expectEqual('http://localhost:9589/html/foo', link.href);
|
||||
link.href = 'https://lightpanda.io/';
|
||||
testing.expectEqual('https://lightpanda.io/', link.href);
|
||||
|
||||
|
||||
@@ -4,67 +4,26 @@
|
||||
<script id=navigator>
|
||||
// Navigator should be accessible from window
|
||||
testing.expectEqual(navigator, window.navigator);
|
||||
</script>
|
||||
|
||||
<script id=userAgent>
|
||||
testing.expectEqual(true, navigator.userAgent.length > 0);
|
||||
testing.expectEqual(true, navigator.userAgent.includes('LiteFetch'));
|
||||
</script>
|
||||
|
||||
<script id=appName>
|
||||
testing.expectEqual('LiteFetch', navigator.appName);
|
||||
</script>
|
||||
|
||||
<script id=appVersion>
|
||||
testing.expectEqual('0.1', navigator.appVersion);
|
||||
</script>
|
||||
|
||||
<script id=platform>
|
||||
testing.expectEqual(true, navigator.userAgent.includes('Lightpanda'));
|
||||
testing.expectEqual('Netscape', navigator.appName);
|
||||
testing.expectEqual('1.0', navigator.appVersion);
|
||||
testing.expectEqual(true, navigator.platform.length > 0);
|
||||
|
||||
// Platform should be one of the known values
|
||||
const validPlatforms = ['MacIntel', 'Win32', 'Linux x86_64', 'FreeBSD', 'Unknown'];
|
||||
testing.expectEqual(true, validPlatforms.includes(navigator.platform));
|
||||
</script>
|
||||
|
||||
<script id=language>
|
||||
testing.expectEqual('en-US', navigator.language);
|
||||
</script>
|
||||
|
||||
<script id=languages>
|
||||
testing.expectEqual(true, Array.isArray(navigator.languages));
|
||||
testing.expectEqual(1, navigator.languages.length);
|
||||
testing.expectEqual('en-US', navigator.languages[0]);
|
||||
</script>
|
||||
|
||||
<script id=onLine>
|
||||
testing.expectEqual(true, navigator.onLine);
|
||||
</script>
|
||||
|
||||
<script id=cookieEnabled>
|
||||
testing.expectEqual(false, navigator.cookieEnabled);
|
||||
</script>
|
||||
|
||||
<script id=hardwareConcurrency>
|
||||
testing.expectEqual(true, navigator.hardwareConcurrency > 0);
|
||||
testing.expectEqual(4, navigator.hardwareConcurrency);
|
||||
</script>
|
||||
|
||||
<script id=maxTouchPoints>
|
||||
testing.expectEqual(0, navigator.maxTouchPoints);
|
||||
</script>
|
||||
|
||||
<script id=vendor>
|
||||
testing.expectEqual('LiteFetch', navigator.vendor);
|
||||
</script>
|
||||
|
||||
<script id=product>
|
||||
testing.expectEqual('', navigator.vendor);
|
||||
testing.expectEqual('Gecko', navigator.product);
|
||||
</script>
|
||||
|
||||
<script id=javaEnabled>
|
||||
testing.expectEqual(false, navigator.javaEnabled());
|
||||
</script>
|
||||
|
||||
<script id=webdriver>
|
||||
testing.expectEqual(false, navigator.webdriver);
|
||||
</script>
|
||||
|
||||
@@ -345,6 +345,48 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !vo
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element {
|
||||
// Traverse document in depth-first order to find the topmost (last in document order)
|
||||
// element that contains the point (x, y)
|
||||
var topmost: ?*Element = null;
|
||||
|
||||
const root = self.asNode();
|
||||
var stack: std.ArrayList(*Node) = .empty;
|
||||
try stack.append(page.call_arena, root);
|
||||
|
||||
while (stack.items.len > 0) {
|
||||
const node = stack.pop() orelse break;
|
||||
if (node.is(Element)) |element| {
|
||||
if (try element.checkVisibility(page)) {
|
||||
const rect = try element.getBoundingClientRect(page);
|
||||
if (x >= rect._left and x <= rect._right and y >= rect._top and y <= rect._bottom) {
|
||||
topmost = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add children to stack in reverse order so we process them in document order
|
||||
var child = node.lastChild();
|
||||
while (child) |c| {
|
||||
try stack.append(page.call_arena, c);
|
||||
child = c.previousSibling();
|
||||
}
|
||||
}
|
||||
|
||||
return topmost;
|
||||
}
|
||||
|
||||
pub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const *Element {
|
||||
// Get topmost element
|
||||
var current: ?*Element = (try self.elementFromPoint(x, y, page)) orelse return &.{};
|
||||
var result: std.ArrayList(*Element) = .empty;
|
||||
while (current) |el| {
|
||||
try result.append(page.call_arena, el);
|
||||
current = el.parentElement();
|
||||
}
|
||||
return result.items;
|
||||
}
|
||||
|
||||
const ReadyState = enum {
|
||||
loading,
|
||||
interactive,
|
||||
@@ -404,6 +446,8 @@ pub const JsApi = struct {
|
||||
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true });
|
||||
pub const append = bridge.function(Document.append, .{});
|
||||
pub const prepend = bridge.function(Document.prepend, .{});
|
||||
pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});
|
||||
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
|
||||
|
||||
pub const defaultView = bridge.accessor(struct {
|
||||
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
|
||||
|
||||
@@ -25,15 +25,19 @@ _pad: bool = false,
|
||||
pub const init: Navigator = .{};
|
||||
|
||||
pub fn getUserAgent(_: *const Navigator) []const u8 {
|
||||
return "Mozilla/5.0 (compatible; LiteFetch/0.1)";
|
||||
return "Lightpanda/1.0";
|
||||
}
|
||||
|
||||
pub fn getAppName(_: *const Navigator) []const u8 {
|
||||
return "LiteFetch";
|
||||
return "Netscape";
|
||||
}
|
||||
|
||||
pub fn getAppCodeName(_: *const Navigator) []const u8 {
|
||||
return "Netscape";
|
||||
}
|
||||
|
||||
pub fn getAppVersion(_: *const Navigator) []const u8 {
|
||||
return "0.1";
|
||||
return "1.0";
|
||||
}
|
||||
|
||||
pub fn getPlatform(_: *const Navigator) []const u8 {
|
||||
@@ -73,7 +77,7 @@ pub fn getMaxTouchPoints(_: *const Navigator) u32 {
|
||||
|
||||
/// Returns the vendor name
|
||||
pub fn getVendor(_: *const Navigator) []const u8 {
|
||||
return "LiteFetch";
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Returns the product name (typically "Gecko" for compatibility)
|
||||
@@ -104,6 +108,7 @@ pub const JsApi = struct {
|
||||
// Read-only properties
|
||||
pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{});
|
||||
pub const appName = bridge.accessor(Navigator.getAppName, null, .{});
|
||||
pub const appCodeName = bridge.accessor(Navigator.getAppCodeName, null, .{});
|
||||
pub const appVersion = bridge.accessor(Navigator.getAppVersion, null, .{});
|
||||
pub const platform = bridge.accessor(Navigator.getPlatform, null, .{});
|
||||
pub const language = bridge.accessor(Navigator.getLanguage, null, .{});
|
||||
|
||||
@@ -30,11 +30,6 @@ const HTMLOptionsCollection = @This();
|
||||
_proto: *HTMLCollection,
|
||||
_select: *@import("../element/html/Select.zig"),
|
||||
|
||||
pub fn deinit(self: *HTMLOptionsCollection) void {
|
||||
const page = Page.current;
|
||||
page._factory.destroy(self);
|
||||
}
|
||||
|
||||
// Forward length to HTMLCollection
|
||||
pub fn length(self: *HTMLOptionsCollection, page: *Page) u32 {
|
||||
return self._proto.length(page);
|
||||
@@ -102,7 +97,6 @@ pub const JsApi = struct {
|
||||
pub const name = "HTMLOptionsCollection";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const finalizer = HTMLOptionsCollection.deinit;
|
||||
pub const manage = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -120,11 +120,11 @@ pub fn is(self: *HtmlElement, comptime T: type) ?*T {
|
||||
|
||||
pub fn className(self: *const HtmlElement) []const u8 {
|
||||
return switch (self._type) {
|
||||
.anchor => "[object HtmlAnchorElement]",
|
||||
.div => "[object HtmlDivElement]",
|
||||
.embed => "[object HtmlEmbedElement]",
|
||||
.anchor => "[object HTMLAnchorElement]",
|
||||
.div => "[object HTMLDivElement]",
|
||||
.embed => "[object HTMLEmbedElement]",
|
||||
.form => "[object HTMLFormElement]",
|
||||
.p => "[object HtmlParagraphElement]",
|
||||
.p => "[object HTMLParagraphElement]",
|
||||
.custom => "[object CUSTOM-TODO]",
|
||||
.data => "[object HTMLDataElement]",
|
||||
.dialog => "[object HTMLDialogElement]",
|
||||
@@ -137,22 +137,22 @@ pub fn className(self: *const HtmlElement) []const u8 {
|
||||
.ul => "[object HTMLULElement]",
|
||||
.ol => "[object HTMLOLElement]",
|
||||
.generic => "[object HTMLElement]",
|
||||
.script => "[object HtmlScriptElement]",
|
||||
.script => "[object HTMLScriptElement]",
|
||||
.select => "[object HTMLSelectElement]",
|
||||
.slot => "[object HTMLSlotElement]",
|
||||
.template => "[object HTMLTemplateElement]",
|
||||
.option => "[object HTMLOptionElement]",
|
||||
.text_area => "[object HtmlTextAreaElement]",
|
||||
.input => "[object HtmlInputElement]",
|
||||
.link => "[object HtmlLinkElement]",
|
||||
.meta => "[object HtmlMetaElement]",
|
||||
.hr => "[object HtmlHRElement]",
|
||||
.style => "[object HtmlSyleElement]",
|
||||
.title => "[object HtmlTitleElement]",
|
||||
.body => "[object HtmlBodyElement]",
|
||||
.html => "[object HtmlHtmlElement]",
|
||||
.head => "[object HtmlHeadElement]",
|
||||
.unknown => "[object HtmlUnknownElement]",
|
||||
.text_area => "[object HTMLTextAreaElement]",
|
||||
.input => "[object HTMLInputElement]",
|
||||
.link => "[object HTMLLinkElement]",
|
||||
.meta => "[object HTMLMetaElement]",
|
||||
.hr => "[object HTMLHRElement]",
|
||||
.style => "[object HTMLSyleElement]",
|
||||
.title => "[object HTMLTitleElement]",
|
||||
.body => "[object HTMLBodyElement]",
|
||||
.html => "[object HTMLHtmlElement]",
|
||||
.head => "[object HTMLHeadElement]",
|
||||
.unknown => "[object HTMLUnknownElement]",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,12 +77,6 @@ pub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *List) void {
|
||||
const page = Page.current;
|
||||
page._mem.releaseArena(self._arena);
|
||||
page._factory.destroy(self);
|
||||
}
|
||||
|
||||
const OptimizeResult = struct {
|
||||
root: *Node,
|
||||
exclude_root: bool,
|
||||
|
||||
Reference in New Issue
Block a user