script: Implement Range::getClientRects and Range::getBoundingClientRect (#35993)

* Add doc comments to boundary point

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Allow querying content box of text fragments

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Implement Range::getBoundingClientRect

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Update WPT expectations

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2025-03-19 09:33:11 +01:00 committed by GitHub
parent 07e06f5635
commit 6be7612d16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 73 additions and 29 deletions

View file

@ -155,6 +155,7 @@ impl FragmentTree {
} }
}, },
Fragment::Positioning(fragment) => fragment.borrow().rect.cast_unit(), Fragment::Positioning(fragment) => fragment.borrow().rect.cast_unit(),
Fragment::Text(text_fragment) => text_fragment.borrow().rect,
_ => return None, _ => return None,
}; };

View file

@ -94,10 +94,13 @@ impl AbstractRangeMethods<crate::DomTypeHolder> for AbstractRange {
} }
} }
/// <https://dom.spec.whatwg.org/#concept-range-bp>
#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)] #[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct BoundaryPoint { pub(crate) struct BoundaryPoint {
/// <https://dom.spec.whatwg.org/#boundary-point-node>
node: MutDom<Node>, node: MutDom<Node>,
/// <https://dom.spec.whatwg.org/#concept-range-bp-offset>
offset: Cell<u32>, offset: Cell<u32>,
} }

View file

@ -4,6 +4,7 @@
use std::cell::UnsafeCell; use std::cell::UnsafeCell;
use std::cmp::{Ordering, PartialOrd}; use std::cmp::{Ordering, PartialOrd};
use std::iter;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use js::jsapi::JSTracer; use js::jsapi::JSTracer;
@ -29,9 +30,11 @@ use crate::dom::bindings::weakref::{WeakRef, WeakRefVec};
use crate::dom::characterdata::CharacterData; use crate::dom::characterdata::CharacterData;
use crate::dom::document::Document; use crate::dom::document::Document;
use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentfragment::DocumentFragment;
use crate::dom::domrect::DOMRect;
use crate::dom::domrectlist::DOMRectList;
use crate::dom::element::Element; use crate::dom::element::Element;
use crate::dom::htmlscriptelement::HTMLScriptElement; use crate::dom::htmlscriptelement::HTMLScriptElement;
use crate::dom::node::{Node, ShadowIncluding, UnbindContext}; use crate::dom::node::{Node, NodeTraits, ShadowIncluding, UnbindContext};
use crate::dom::selection::Selection; use crate::dom::selection::Selection;
use crate::dom::text::Text; use crate::dom::text::Text;
use crate::dom::window::Window; use crate::dom::window::Window;
@ -324,6 +327,23 @@ impl Range {
pub(crate) fn collapsed(&self) -> bool { pub(crate) fn collapsed(&self) -> bool {
self.abstract_range().Collapsed() self.abstract_range().Collapsed()
} }
fn client_rects(
&self,
can_gc: CanGc,
) -> impl Iterator<Item = euclid::Rect<app_units::Au, euclid::UnknownUnit>> {
// FIXME: For text nodes that are only partially selected, this should return the client
// rect of the selected part, not the whole text node.
let start = self.start_container();
let end = self.end_container();
let document = start.owner_doc();
let end_clone = end.clone();
start
.following_nodes(document.upcast::<Node>())
.take_while(move |node| node != &end)
.chain(iter::once(end_clone))
.flat_map(move |node| node.content_boxes(can_gc))
}
} }
impl RangeMethods<crate::DomTypeHolder> for Range { impl RangeMethods<crate::DomTypeHolder> for Range {
@ -1105,6 +1125,51 @@ impl RangeMethods<crate::DomTypeHolder> for Range {
// Step 5. // Step 5.
Ok(fragment_node) Ok(fragment_node)
} }
/// <https://drafts.csswg.org/cssom-view/#dom-range-getclientrects>
fn GetClientRects(&self, can_gc: CanGc) -> DomRoot<DOMRectList> {
let start = self.start_container();
let window = start.owner_window();
let client_rects = self
.client_rects(can_gc)
.map(|rect| {
DOMRect::new(
window.upcast(),
rect.origin.x.to_f64_px(),
rect.origin.y.to_f64_px(),
rect.size.width.to_f64_px(),
rect.size.height.to_f64_px(),
can_gc,
)
})
.collect();
DOMRectList::new(&window, client_rects, can_gc)
}
/// <https://drafts.csswg.org/cssom-view/#dom-range-getboundingclientrect>
fn GetBoundingClientRect(&self, can_gc: CanGc) -> DomRoot<DOMRect> {
let window = self.start_container().owner_window();
// Step 1. Let list be the result of invoking getClientRects() on the same range this method was invoked on.
let list = self.client_rects(can_gc);
// Step 2. If list is empty return a DOMRect object whose x, y, width and height members are zero.
// Step 3. If all rectangles in list have zero width or height, return the first rectangle in list.
// Step 4. Otherwise, return a DOMRect object describing the smallest rectangle that includes all
// of the rectangles in list of which the height or width is not zero.
let bounding_rect = list.fold(euclid::Rect::zero(), |acc, rect| acc.union(&rect));
DOMRect::new(
window.upcast(),
bounding_rect.origin.x.to_f64_px(),
bounding_rect.origin.y.to_f64_px(),
bounding_rect.size.width.to_f64_px(),
bounding_rect.size.height.to_f64_px(),
can_gc,
)
}
} }
pub(crate) struct WeakRangeVec { pub(crate) struct WeakRangeVec {

View file

@ -469,7 +469,7 @@ DOMInterfaces = {
}, },
'Range': { 'Range': {
'canGc': ['CloneContents', 'CloneRange', 'CreateContextualFragment', 'ExtractContents', 'SurroundContents', 'InsertNode'], 'canGc': ['CloneContents', 'CloneRange', 'CreateContextualFragment', 'ExtractContents', 'SurroundContents', 'InsertNode', 'GetClientRects', 'GetBoundingClientRect'],
'weakReferenceable': True, 'weakReferenceable': True,
}, },

View file

@ -73,7 +73,6 @@ partial interface Range {
// http://dev.w3.org/csswg/cssom-view/#extensions-to-the-range-interface // http://dev.w3.org/csswg/cssom-view/#extensions-to-the-range-interface
partial interface Range { partial interface Range {
// sequence<DOMRect> getClientRects(); DOMRectList getClientRects();
// [NewObject] [NewObject] DOMRect getBoundingClientRect();
// DOMRect getBoundingClientRect();
}; };

View file

@ -1,3 +0,0 @@
[DOMRectList.html]
[Range getClientRects()]
expected: FAIL

View file

@ -1,9 +0,0 @@
[getBoundingClientRect-svg.html]
[Element.getBoundingClientRect() and Range.getBoudingClientRect() should match for an SVG <text>]
expected: FAIL
[Element.getBoundingClientRect() and Range.getBoudingClientRect() should match for an SVG <text> with a transform]
expected: FAIL
[Element.getBoundingClientRect() and Range.getBoudingClientRect() should match for an SVG <text> with a rotate]
expected: FAIL

View file

@ -26,9 +26,6 @@
[Element interface: document.createElement("div") must inherit property "getBoxQuads(BoxQuadOptions)" with the proper type] [Element interface: document.createElement("div") must inherit property "getBoxQuads(BoxQuadOptions)" with the proper type]
expected: FAIL expected: FAIL
[Range interface: new Range() must inherit property "getBoundingClientRect()" with the proper type]
expected: FAIL
[Element interface: calling convertRectFromNode(DOMRectReadOnly, GeometryNode, ConvertCoordinateOptions) on document.createElementNS("x", "y") with too few arguments must throw TypeError] [Element interface: calling convertRectFromNode(DOMRectReadOnly, GeometryNode, ConvertCoordinateOptions) on document.createElementNS("x", "y") with too few arguments must throw TypeError]
expected: FAIL expected: FAIL
@ -62,9 +59,6 @@
[CaretPosition interface: attribute offset] [CaretPosition interface: attribute offset]
expected: FAIL expected: FAIL
[Range interface: operation getBoundingClientRect()]
expected: FAIL
[Partial dictionary MouseEventInit: member names are unique] [Partial dictionary MouseEventInit: member names are unique]
expected: FAIL expected: FAIL
@ -98,9 +92,6 @@
[CaretPosition interface: existence and properties of interface prototype object] [CaretPosition interface: existence and properties of interface prototype object]
expected: FAIL expected: FAIL
[Range interface: operation getClientRects()]
expected: FAIL
[Element interface: document.createElementNS("x", "y") must inherit property "convertRectFromNode(DOMRectReadOnly, GeometryNode, ConvertCoordinateOptions)" with the proper type] [Element interface: document.createElementNS("x", "y") must inherit property "convertRectFromNode(DOMRectReadOnly, GeometryNode, ConvertCoordinateOptions)" with the proper type]
expected: FAIL expected: FAIL
@ -272,9 +263,6 @@
[Element interface: document.createElement("img") must inherit property "convertPointFromNode(DOMPointInit, GeometryNode, ConvertCoordinateOptions)" with the proper type] [Element interface: document.createElement("img") must inherit property "convertPointFromNode(DOMPointInit, GeometryNode, ConvertCoordinateOptions)" with the proper type]
expected: FAIL expected: FAIL
[Range interface: new Range() must inherit property "getClientRects()" with the proper type]
expected: FAIL
[Window interface: window must inherit property "screenLeft" with the proper type] [Window interface: window must inherit property "screenLeft" with the proper type]
expected: FAIL expected: FAIL