Browse Source

LibWeb: Bring HTMLElement.offset{Left,Top,Parent} closer to spec

(Or rather, bring offsetLeft and offsetTop closer to spec, and implement
the previously-missing offsetParent)

This makes mouse inputs on https://nerget.com/fluidSim/ work properly.
Andreas Kling 1 năm trước cách đây
mục cha
commit
cfe9577b48

+ 21 - 0
Tests/LibWeb/Text/expected/HTML/HTMLElement-offsetFoo-in-table-cell.txt

@@ -0,0 +1,21 @@
+   
+nodeName: CANVAS
+offsetTop: 0
+offsetLeft: 0
+offsetParent: [object HTMLTableCellElement]
+
+nodeName: TD
+offsetTop: 2
+offsetLeft: 2
+offsetParent: [object HTMLTableElement]
+
+nodeName: TABLE
+offsetTop: 100
+offsetLeft: 50
+offsetParent: [object HTMLBodyElement]
+
+nodeName: BODY
+offsetTop: 0
+offsetLeft: 0
+offsetParent: null
+

+ 26 - 0
Tests/LibWeb/Text/input/HTML/HTMLElement-offsetFoo-in-table-cell.html

@@ -0,0 +1,26 @@
+<style>
+* {
+    margin: 0;
+    padding: 0;
+}
+table {
+    position: relative;
+    top: 100px;
+    left: 50px;
+}
+</style><table><tr><td><canvas id="c"></canvas></td></tr></table>
+<script src="../include.js"></script>
+<script>
+    test(() => {
+        const c = document.getElementById("c");
+        println("");
+
+        for (let n = c; n; n = n.offsetParent) {
+            println("nodeName: " + n.nodeName);
+            println("offsetTop: " + n.offsetTop);
+            println("offsetLeft: " + n.offsetLeft);
+            println("offsetParent: " + n.offsetParent);
+            println("");
+        }
+    });
+</script>

+ 89 - 8
Userland/Libraries/LibWeb/HTML/HTMLElement.cpp

@@ -155,30 +155,111 @@ String HTMLElement::inner_text()
     return MUST(builder.to_string());
     return MUST(builder.to_string());
 }
 }
 
 
-// // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsettop
+// https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetparent
+JS::GCPtr<DOM::Element> HTMLElement::offset_parent() const
+{
+    const_cast<DOM::Document&>(document()).update_layout();
+
+    // 1. If any of the following holds true return null and terminate this algorithm:
+    //    - The element does not have an associated CSS layout box.
+    //    - The element is the root element.
+    //    - The element is the HTML body element.
+    //    - The element’s computed value of the position property is fixed.
+    if (!layout_node())
+        return nullptr;
+    if (is_document_element())
+        return nullptr;
+    if (is<HTML::HTMLBodyElement>(*this))
+        return nullptr;
+    if (layout_node()->is_fixed_position())
+        return nullptr;
+
+    // 2. Return the nearest ancestor element of the element for which at least one of the following is true
+    //    and terminate this algorithm if such an ancestor is found:
+    //    - The computed value of the position property is not static.
+    //    - It is the HTML body element.
+    //    - The computed value of the position property of the element is static
+    //      and the ancestor is one of the following HTML elements: td, th, or table.
+
+    for (auto* ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) {
+        if (!ancestor->layout_node())
+            continue;
+        if (ancestor->layout_node()->is_positioned())
+            return const_cast<Element*>(ancestor);
+        if (is<HTML::HTMLBodyElement>(*ancestor))
+            return const_cast<Element*>(ancestor);
+        if (!ancestor->layout_node()->is_positioned() && ancestor->local_name().is_one_of(HTML::TagNames::td, HTML::TagNames::th, HTML::TagNames::table))
+            return const_cast<Element*>(ancestor);
+    }
+
+    VERIFY_NOT_REACHED();
+}
+
+// https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsettop
 int HTMLElement::offset_top() const
 int HTMLElement::offset_top() const
 {
 {
+    // 1. If the element is the HTML body element or does not have any associated CSS layout box
+    //    return zero and terminate this algorithm.
+    if (is<HTML::HTMLBodyElement>(*this))
+        return 0;
+
     // NOTE: Ensure that layout is up-to-date before looking at metrics.
     // NOTE: Ensure that layout is up-to-date before looking at metrics.
     const_cast<DOM::Document&>(document()).update_layout();
     const_cast<DOM::Document&>(document()).update_layout();
 
 
-    if (is<HTML::HTMLBodyElement>(this) || !layout_node() || !parent_element() || !parent_element()->layout_node())
+    if (!layout_node())
         return 0;
         return 0;
+
+    // 2. If the offsetParent of the element is null
+    //    return the y-coordinate of the top border edge of the first CSS layout box associated with the element,
+    //    relative to the initial containing block origin,
+    //    ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm.
+    auto offset_parent = this->offset_parent();
+    if (!offset_parent || !offset_parent->layout_node()) {
+        auto position = layout_node()->box_type_agnostic_position();
+        return position.y().to_int();
+    }
+
+    // 3. Return the result of subtracting the y-coordinate of the top padding edge
+    //    of the first box associated with the offsetParent of the element
+    //    from the y-coordinate of the top border edge of the first box associated with the element,
+    //    relative to the initial containing block origin,
+    //    ignoring any transforms that apply to the element and its ancestors.
+    auto offset_parent_position = offset_parent->layout_node()->box_type_agnostic_position();
     auto position = layout_node()->box_type_agnostic_position();
     auto position = layout_node()->box_type_agnostic_position();
-    auto parent_position = parent_element()->layout_node()->box_type_agnostic_position();
-    return position.y().to_int() - parent_position.y().to_int();
+    return position.y().to_int() - offset_parent_position.y().to_int();
 }
 }
 
 
-// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetleft
+// https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetleft
 int HTMLElement::offset_left() const
 int HTMLElement::offset_left() const
 {
 {
+    // 1. If the element is the HTML body element or does not have any associated CSS layout box return zero and terminate this algorithm.
+    if (is<HTML::HTMLBodyElement>(*this))
+        return 0;
+
     // NOTE: Ensure that layout is up-to-date before looking at metrics.
     // NOTE: Ensure that layout is up-to-date before looking at metrics.
     const_cast<DOM::Document&>(document()).update_layout();
     const_cast<DOM::Document&>(document()).update_layout();
 
 
-    if (is<HTML::HTMLBodyElement>(this) || !layout_node() || !parent_element() || !parent_element()->layout_node())
+    if (!layout_node())
         return 0;
         return 0;
+
+    // 2. If the offsetParent of the element is null
+    //    return the x-coordinate of the left border edge of the first CSS layout box associated with the element,
+    //    relative to the initial containing block origin,
+    //    ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm.
+    auto offset_parent = this->offset_parent();
+    if (!offset_parent || !offset_parent->layout_node()) {
+        auto position = layout_node()->box_type_agnostic_position();
+        return position.x().to_int();
+    }
+
+    // 3. Return the result of subtracting the x-coordinate of the left padding edge
+    //    of the first CSS layout box associated with the offsetParent of the element
+    //    from the x-coordinate of the left border edge of the first CSS layout box associated with the element,
+    //    relative to the initial containing block origin,
+    //    ignoring any transforms that apply to the element and its ancestors.
+    auto offset_parent_position = offset_parent->layout_node()->box_type_agnostic_position();
     auto position = layout_node()->box_type_agnostic_position();
     auto position = layout_node()->box_type_agnostic_position();
-    auto parent_position = parent_element()->layout_node()->box_type_agnostic_position();
-    return position.x().to_int() - parent_position.x().to_int();
+    return position.x().to_int() - offset_parent_position.x().to_int();
 }
 }
 
 
 // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth
 // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth

+ 1 - 0
Userland/Libraries/LibWeb/HTML/HTMLElement.h

@@ -44,6 +44,7 @@ public:
     int offset_left() const;
     int offset_left() const;
     int offset_width() const;
     int offset_width() const;
     int offset_height() const;
     int offset_height() const;
+    JS::GCPtr<Element> offset_parent() const;
 
 
     bool cannot_navigate() const;
     bool cannot_navigate() const;
 
 

+ 1 - 1
Userland/Libraries/LibWeb/HTML/HTMLElement.idl

@@ -37,7 +37,7 @@ interface HTMLElement : Element {
     // FIXME: [CEReactions] attribute DOMString? popover;
     // FIXME: [CEReactions] attribute DOMString? popover;
 
 
     // https://drafts.csswg.org/cssom-view/#extensions-to-the-htmlelement-interface
     // https://drafts.csswg.org/cssom-view/#extensions-to-the-htmlelement-interface
-    // FIXME: readonly attribute Element? offsetParent;
+    readonly attribute Element? offsetParent;
     readonly attribute long offsetTop;
     readonly attribute long offsetTop;
     readonly attribute long offsetLeft;
     readonly attribute long offsetLeft;
     readonly attribute long offsetWidth;
     readonly attribute long offsetWidth;