1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 08:37:46 +00:00

LibWeb: Implement document.elementsFromPoint

This API seems to be used by WPT for sending synthetic input events.

Implementing the naive translation of elementFromPoint to the spec steps
for this algorithm turns 4 'tests had errors unexpectedly' and 3 'tests
had timeouts unexpectedly' into 1 pass and 7 'tests had unexpected
subtest results' on the infrastructure/ subdirectory of WPT.
This commit is contained in:
Andrew Kaster 2024-02-09 13:50:44 -07:00 committed by Andrew Kaster
parent e7daa02bf2
commit 6a0fe08604
5 changed files with 104 additions and 0 deletions

View file

@ -0,0 +1,13 @@
Negative coordinates return empty array: true
Coordinates outside the viewport return empty array: true
== Elements at (500, 10) ==
<DIV id="large-box" >
<HTML >
== FIXME: Elements at (550, 60) ==
<DIV id="small-box" >
<DIV id="large-box" >
<DIV id="large-box" >
<DIV id="large-box" >
<DIV id="small-box" >
<PRE id="out" >
<HTML >

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<style>
#large-box {
position: absolute;
top: 10px;
left: 500px;
width: 100px;
height: 100px;
background-color: magenta;
}
#small-box {
position: absolute;
top: 35px;
left: 525px;
width: 50px;
height: 50px;
background-color: yellow;
}
</style>
<script src="../include.js"></script>
<div id="large-box"></div><div id="small-box"></div>
<script>
function isEmptyArray(val) {
return Array.isArray(val) && val.length === 0;
}
test(() => {
println(`Negative coordinates return empty array: ${isEmptyArray(document.elementsFromPoint(-1, -1))}`);
println(`Coordinates outside the viewport return empty array: ${isEmptyArray(document.elementsFromPoint(99999, 99999))}`);
println("== Elements at (500, 10) ==")
for (let elem of document.elementsFromPoint(500, 10)) {
printElement(elem);
}
println("== FIXME: Elements at (550, 60) ==")
for (let elem of document.elementsFromPoint(550, 60)) {
printElement(elem);
}
// FIXME: 550, 60 is supposed to print the following, but the algorithm is wrong.
// <DIV id="small-box" >
// <DIV id="large-box" >
// <PRE id="out" >
// <BODY >
// <HTML >
});
</script>

View file

@ -3808,4 +3808,47 @@ Element const* Document::element_from_point(double x, double y)
return nullptr;
}
// https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint
Vector<JS::NonnullGCPtr<Element>> Document::elements_from_point(double x, double y)
{
// 1. Let sequence be a new empty sequence.
Vector<JS::NonnullGCPtr<Element>> sequence;
// 2. If either argument is negative, x is greater than the viewport width excluding the size of a rendered scroll bar (if any),
// or y is greater than the viewport height excluding the size of a rendered scroll bar (if any),
// or there is no viewport associated with the document, return sequence and terminate these steps.
auto viewport_rect = this->viewport_rect();
CSSPixelPoint position { x, y };
// FIXME: This should account for the size of the scroll bar.
if (x < 0 || y < 0 || position.x() > viewport_rect.width() || position.y() > viewport_rect.height())
return sequence;
// Ensure the layout tree exists prior to hit testing.
update_layout();
// 3. For each box in the viewport, in paint order, starting with the topmost box, that would be a target for
// hit testing at coordinates x,y even if nothing would be overlapping it, when applying the transforms that
// apply to the descendants of the viewport, append the associated element to sequence.
// FIXME: Paintable box tree order is not the same as paint order. We need a helper to traverse the paint tree in
// paint order with a custom callback.
if (auto const* paintable_box = this->paintable_box(); paintable_box) {
paintable_box->for_each_in_inclusive_subtree_of_type<Painting::PaintableBox>([&](auto& paintable_box) {
if (auto result = paintable_box.hit_test(position, Painting::HitTestType::Exact); result.has_value()) {
if (auto* dom_node = result->dom_node(); dom_node && dom_node->is_element())
sequence.append(*static_cast<Element*>(dom_node));
return Painting::TraversalDecision::Continue;
}
return Painting::TraversalDecision::SkipChildrenAndContinue;
});
}
// 4. If the document has a root element, and the last item in sequence is not the root element,
// append the root element to sequence.
if (auto* root_element = document_element(); root_element && (sequence.is_empty() || (sequence.last() != root_element)))
sequence.append(*root_element);
// 5. Return sequence.
return sequence;
}
}

View file

@ -567,6 +567,7 @@ public:
WebIDL::ExceptionOr<void> set_design_mode(String const&);
Element const* element_from_point(double x, double y);
Vector<JS::NonnullGCPtr<Element>> elements_from_point(double x, double y);
void set_needs_to_resolve_paint_only_properties() { m_needs_to_resolve_paint_only_properties = true; }

View file

@ -121,6 +121,7 @@ interface Document : Node {
// https://drafts.csswg.org/cssom-view/#extensions-to-the-document-interface
Element? elementFromPoint(double x, double y);
sequence<Element> elementsFromPoint(double x, double y);
};
dictionary ElementCreationOptions {