LibWeb: Expose document caretPositionFromPoint
Add the CSSOM View CaretPosition interface and expose the Document API. Use retained hit-test data to populate offsetNode, offset, and getClientRect(). Update IDL coverage and the window property baseline. Add a text test.
This commit is contained in:
parent
e621739189
commit
5c26e86354
17 changed files with 305 additions and 23 deletions
|
|
@ -334,6 +334,7 @@ set(SOURCES
|
|||
DOM/AnchorNameMap.cpp
|
||||
DOM/Attr.cpp
|
||||
DOM/CDATASection.cpp
|
||||
DOM/CaretPosition.cpp
|
||||
DOM/CharacterData.cpp
|
||||
DOM/Comment.cpp
|
||||
DOM/CustomEvent.cpp
|
||||
|
|
|
|||
50
Libraries/LibWeb/DOM/CaretPosition.cpp
Normal file
50
Libraries/LibWeb/DOM/CaretPosition.cpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Bindings/CaretPosition.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/DOM/CaretPosition.h>
|
||||
#include <LibWeb/Geometry/DOMRect.h>
|
||||
|
||||
namespace Web::DOM {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(CaretPosition);
|
||||
|
||||
GC::Ref<CaretPosition> CaretPosition::create(JS::Realm& realm, GC::Ref<Node> offset_node, WebIDL::UnsignedLong offset, Optional<Gfx::FloatRect> client_rect)
|
||||
{
|
||||
return realm.create<CaretPosition>(realm, offset_node, offset, move(client_rect));
|
||||
}
|
||||
|
||||
CaretPosition::CaretPosition(JS::Realm& realm, GC::Ref<Node> offset_node, WebIDL::UnsignedLong offset, Optional<Gfx::FloatRect> client_rect)
|
||||
: Bindings::PlatformObject(realm)
|
||||
, m_offset_node(offset_node)
|
||||
, m_offset(offset)
|
||||
, m_client_rect(move(client_rect))
|
||||
{
|
||||
}
|
||||
|
||||
CaretPosition::~CaretPosition() = default;
|
||||
|
||||
void CaretPosition::initialize(JS::Realm& realm)
|
||||
{
|
||||
WEB_SET_PROTOTYPE_FOR_INTERFACE(CaretPosition);
|
||||
Base::initialize(realm);
|
||||
}
|
||||
|
||||
void CaretPosition::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_offset_node);
|
||||
}
|
||||
|
||||
GC::Ptr<Geometry::DOMRect> CaretPosition::get_client_rect() const
|
||||
{
|
||||
if (!m_client_rect.has_value())
|
||||
return nullptr;
|
||||
return Geometry::DOMRect::create(realm(), *m_client_rect);
|
||||
}
|
||||
|
||||
}
|
||||
42
Libraries/LibWeb/DOM/CaretPosition.h
Normal file
42
Libraries/LibWeb/DOM/CaretPosition.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
#include <LibWeb/DOM/Node.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
#include <LibWeb/WebIDL/Types.h>
|
||||
|
||||
namespace Web::DOM {
|
||||
|
||||
class CaretPosition final : public Bindings::PlatformObject {
|
||||
WEB_PLATFORM_OBJECT(CaretPosition, Bindings::PlatformObject);
|
||||
GC_DECLARE_ALLOCATOR(CaretPosition);
|
||||
|
||||
public:
|
||||
[[nodiscard]] static GC::Ref<CaretPosition> create(JS::Realm&, GC::Ref<Node> offset_node, WebIDL::UnsignedLong offset, Optional<Gfx::FloatRect> client_rect);
|
||||
|
||||
virtual ~CaretPosition() override;
|
||||
|
||||
GC::Ref<Node> offset_node() const { return m_offset_node; }
|
||||
WebIDL::UnsignedLong offset() const { return m_offset; }
|
||||
|
||||
GC::Ptr<Geometry::DOMRect> get_client_rect() const;
|
||||
|
||||
private:
|
||||
CaretPosition(JS::Realm&, GC::Ref<Node> offset_node, WebIDL::UnsignedLong offset, Optional<Gfx::FloatRect> client_rect);
|
||||
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
GC::Ref<Node> m_offset_node;
|
||||
WebIDL::UnsignedLong m_offset { 0 };
|
||||
Optional<Gfx::FloatRect> m_client_rect;
|
||||
};
|
||||
|
||||
}
|
||||
7
Libraries/LibWeb/DOM/CaretPosition.idl
Normal file
7
Libraries/LibWeb/DOM/CaretPosition.idl
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// https://drafts.csswg.org/cssom-view/#caretposition
|
||||
[Exposed=Window]
|
||||
interface CaretPosition {
|
||||
readonly attribute Node offsetNode;
|
||||
readonly attribute unsigned long offset;
|
||||
[NewObject] DOMRect? getClientRect();
|
||||
};
|
||||
|
|
@ -82,6 +82,7 @@
|
|||
#include <LibWeb/DOM/AdoptedStyleSheets.h>
|
||||
#include <LibWeb/DOM/Attr.h>
|
||||
#include <LibWeb/DOM/CDATASection.h>
|
||||
#include <LibWeb/DOM/CaretPosition.h>
|
||||
#include <LibWeb/DOM/Comment.h>
|
||||
#include <LibWeb/DOM/CustomEvent.h>
|
||||
#include <LibWeb/DOM/DOMImplementation.h>
|
||||
|
|
@ -7206,6 +7207,60 @@ GC::RootVector<GC::Ref<Element>> Document::elements_from_point(double x, double
|
|||
return sequence;
|
||||
}
|
||||
|
||||
static bool shadow_root_is_allowed_for_caret_position(ShadowRoot const& shadow_root, Bindings::CaretPositionFromPointOptions const& options)
|
||||
{
|
||||
for (auto const& allowed_shadow_root : options.shadow_roots) {
|
||||
if (shadow_root.is_shadow_including_inclusive_ancestor_of(allowed_shadow_root))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/cssom-view/#dom-document-caretpositionfrompoint
|
||||
GC::Ptr<CaretPosition> Document::caret_position_from_point(double x, double y, Bindings::CaretPositionFromPointOptions const& options)
|
||||
{
|
||||
// 1. If there is no viewport associated with the document, return null.
|
||||
// 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),
|
||||
// return null.
|
||||
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 nullptr;
|
||||
|
||||
// Ensure the layout tree exists prior to hit testing.
|
||||
update_layout(UpdateLayoutReason::DocumentCaretPositionFromPoint);
|
||||
|
||||
// 3. If at the coordinates x,y in the viewport no text insertion point indicator would have been inserted when
|
||||
// applying the transforms that apply to the descendants of the viewport, return null.
|
||||
auto caret_position = caret_position_from_point(position);
|
||||
if (!caret_position.has_value())
|
||||
return nullptr;
|
||||
|
||||
// FIXME: 4. If at the coordinates x,y in the viewport a text insertion point indicator would have been inserted
|
||||
// in a text entry widget which is also a replaced element, when applying the transforms that apply to
|
||||
// the descendants of the viewport, return a caret position for the text entry widget.
|
||||
|
||||
// 5. Otherwise, retarget shadow tree positions whose roots are not allowed by options.shadowRoots.
|
||||
auto start_node = caret_position->boundary.node;
|
||||
auto start_offset = caret_position->boundary.offset;
|
||||
auto* shadow_root = as_if<ShadowRoot>(start_node->root());
|
||||
while (shadow_root && !shadow_root_is_allowed_for_caret_position(*shadow_root, options)) {
|
||||
auto* host = shadow_root->host();
|
||||
auto* host_parent = host->parent();
|
||||
if (!host_parent)
|
||||
return nullptr;
|
||||
start_offset = host->index();
|
||||
start_node = *host_parent;
|
||||
shadow_root = as_if<ShadowRoot>(start_node->root());
|
||||
}
|
||||
|
||||
return CaretPosition::create(realm(), start_node, start_offset, caret_position->debug_rect.map([](auto const& rect) {
|
||||
return rect.template to_type<float>();
|
||||
}));
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/cssom-view/#dom-document-scrollingelement
|
||||
GC::Ptr<Element const> Document::scrolling_element() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ enum class InvalidateLayoutTreeReason {
|
|||
X(Debugging) \
|
||||
X(DocumentElementFromPoint) \
|
||||
X(DocumentElementsFromPoint) \
|
||||
X(DocumentCaretPositionFromPoint) \
|
||||
X(DocumentFindMatchingText) \
|
||||
X(DocumentSetDesignMode) \
|
||||
X(DumpDisplayList) \
|
||||
|
|
@ -981,6 +982,7 @@ public:
|
|||
Painting::HitTestDisplayList const* ensure_hit_test_display_list();
|
||||
Optional<Painting::HitTestResult> hit_test(CSSPixelPoint, Painting::HitTestType);
|
||||
Optional<Painting::CaretPosition> caret_position_from_point(CSSPixelPoint);
|
||||
GC::Ptr<CaretPosition> caret_position_from_point(double x, double y, Bindings::CaretPositionFromPointOptions const&);
|
||||
TraversalDecision hit_test_all(CSSPixelPoint, Function<TraversalDecision(Painting::HitTestResult)> const&);
|
||||
|
||||
void set_needs_to_record_display_list();
|
||||
|
|
|
|||
|
|
@ -109,6 +109,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);
|
||||
CaretPosition? caretPositionFromPoint(double x, double y, optional CaretPositionFromPointOptions options = {});
|
||||
readonly attribute Element? scrollingElement;
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/
|
||||
|
|
@ -134,6 +135,10 @@ interface Document : Node {
|
|||
undefined exitPointerLock();
|
||||
};
|
||||
|
||||
dictionary CaretPositionFromPointOptions {
|
||||
sequence<ShadowRoot> shadowRoots = [];
|
||||
};
|
||||
|
||||
// https://dom.spec.whatwg.org/#dictdef-elementcreationoptions
|
||||
dictionary ElementCreationOptions {
|
||||
CustomElementRegistry? customElementRegistry;
|
||||
|
|
|
|||
|
|
@ -531,6 +531,7 @@ class AccessibilityTreeNode;
|
|||
class AnchorNameMap;
|
||||
class Attr;
|
||||
class CDATASection;
|
||||
class CaretPosition;
|
||||
class CharacterData;
|
||||
class Comment;
|
||||
class CustomEvent;
|
||||
|
|
|
|||
|
|
@ -488,6 +488,7 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_for_item(Item const&
|
|||
return CaretPosition {
|
||||
.paintable = item.paintable,
|
||||
.boundary = { const_cast<DOM::Node&>(*fragment_dom_node), static_cast<WebIDL::UnsignedLong>(index_in_node) },
|
||||
.debug_rect = fragment.range_rect(Paintable::SelectionState::StartAndEnd, index_in_node, index_in_node),
|
||||
};
|
||||
}
|
||||
case ItemKind::EmptyEditable: {
|
||||
|
|
@ -497,6 +498,7 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_for_item(Item const&
|
|||
return CaretPosition {
|
||||
.paintable = item.paintable,
|
||||
.boundary = { *dom_node, 0 },
|
||||
.debug_rect = item.caret_rect,
|
||||
};
|
||||
}
|
||||
case ItemKind::Box: {
|
||||
|
|
@ -521,6 +523,7 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_for_item(Item const&
|
|||
.paintable = item.paintable,
|
||||
.boundary = point_is_before_box ? before_boundary : after_boundary,
|
||||
.secondary_boundary = point_is_before_box ? after_boundary : before_boundary,
|
||||
.debug_rect = item.caret_rect,
|
||||
};
|
||||
}
|
||||
case ItemKind::ChromeWidget:
|
||||
|
|
@ -538,6 +541,7 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_for_hit_container(Ite
|
|||
return CaretPosition {
|
||||
.paintable = item.paintable,
|
||||
.boundary = { const_cast<DOM::Node&>(*dom_node), 0 },
|
||||
.debug_rect = item.caret_rect,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -699,8 +703,12 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPo
|
|||
};
|
||||
if (topmost_item_index.has_value() && (!topmost_hit_item_index.has_value() || topmost_caret_item_matches_hit_item())) {
|
||||
VERIFY(topmost_item_local_point.has_value());
|
||||
if (auto caret_position = caret_position_for_item(m_items[*topmost_item_index], *topmost_item_local_point); caret_position.has_value())
|
||||
if (auto caret_position = caret_position_for_item(m_items[*topmost_item_index], *topmost_item_local_point); caret_position.has_value()) {
|
||||
auto const& item = m_items[*topmost_item_index];
|
||||
if (caret_position->debug_rect.has_value())
|
||||
caret_position->debug_rect = viewport_rect_for_item(item, *caret_position->debug_rect, viewport_paintable, device_pixels_per_css_pixel);
|
||||
return caret_position;
|
||||
}
|
||||
}
|
||||
|
||||
// If the point is over a non-caret item, only consider caret lines inside that item's event-dispatch node first.
|
||||
|
|
@ -814,21 +822,31 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPo
|
|||
}
|
||||
|
||||
if (!closest_line.index.has_value()) {
|
||||
if (topmost_hit_item_index.has_value())
|
||||
return caret_position_for_hit_container(m_items[*topmost_hit_item_index]);
|
||||
if (topmost_hit_item_index.has_value()) {
|
||||
auto const& item = m_items[*topmost_hit_item_index];
|
||||
auto caret_position = caret_position_for_hit_container(item);
|
||||
if (caret_position.has_value() && caret_position->debug_rect.has_value())
|
||||
caret_position->debug_rect = viewport_rect_for_item(item, *caret_position->debug_rect, viewport_paintable, device_pixels_per_css_pixel);
|
||||
return caret_position;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
VERIFY(closest_line.local_point.has_value());
|
||||
auto caret_position = caret_position_for_line(m_caret_lines[*closest_line.index], *closest_line.local_point);
|
||||
if (!caret_position.has_value())
|
||||
return {};
|
||||
if (caret_position->debug_rect.has_value())
|
||||
caret_position->debug_rect = viewport_rect_for_item(m_items[m_caret_item_indices[m_caret_lines[*closest_line.index].first_caret_item_index]], *caret_position->debug_rect, viewport_paintable, device_pixels_per_css_pixel);
|
||||
|
||||
if (topmost_hit_item_index.has_value()) {
|
||||
auto const& topmost_hit_item = m_items[*topmost_hit_item_index];
|
||||
if (auto const* topmost_hit_dom_node = event_dispatch_dom_node_for_item(topmost_hit_item); topmost_hit_dom_node && !topmost_hit_dom_node->is_inclusive_ancestor_of(*caret_position->boundary.node)) {
|
||||
if (item_can_produce_caret_position(topmost_hit_item) && item_is_direct_caret_target(topmost_hit_item)) {
|
||||
VERIFY(topmost_hit_item_local_point.has_value());
|
||||
return caret_position_for_item(topmost_hit_item, *topmost_hit_item_local_point);
|
||||
auto caret_position_for_topmost_hit_item = caret_position_for_item(topmost_hit_item, *topmost_hit_item_local_point);
|
||||
if (caret_position_for_topmost_hit_item.has_value() && caret_position_for_topmost_hit_item->debug_rect.has_value())
|
||||
caret_position_for_topmost_hit_item->debug_rect = viewport_rect_for_item(topmost_hit_item, *caret_position_for_topmost_hit_item->debug_rect, viewport_paintable, device_pixels_per_css_pixel);
|
||||
return caret_position_for_topmost_hit_item;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ struct CaretPosition {
|
|||
NonnullRefPtr<Paintable> paintable;
|
||||
DOM::BoundaryPoint boundary;
|
||||
Optional<DOM::BoundaryPoint> secondary_boundary {};
|
||||
Optional<CSSPixelRect> debug_rect {};
|
||||
};
|
||||
|
||||
enum class HitTestType : u8 {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ libweb_js_bindings(DOM/AbortSignal)
|
|||
libweb_js_bindings(DOM/AbstractRange)
|
||||
libweb_js_bindings(DOM/Attr)
|
||||
libweb_js_bindings(DOM/CDATASection)
|
||||
libweb_js_bindings(DOM/CaretPosition)
|
||||
libweb_js_bindings(DOM/CharacterData)
|
||||
libweb_js_bindings(DOM/ChildNode)
|
||||
libweb_js_bindings(DOM/Comment)
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ CacheStorage
|
|||
CanvasGradient
|
||||
CanvasPattern
|
||||
CanvasRenderingContext2D
|
||||
CaretPosition
|
||||
ChannelMergerNode
|
||||
ChannelSplitterNode
|
||||
CharacterData
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
without shadowRoots retargets to host parent: true
|
||||
without shadowRoots retargets to host index: true
|
||||
unrelated shadowRoot still retargets to host parent: true
|
||||
unrelated shadowRoot still retargets to host index: true
|
||||
allowed shadowRoot exposes text node: true
|
||||
allowed shadowRoot exposes text offset: true
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
negative x returns null: true
|
||||
negative y returns null: true
|
||||
caret position exists: true
|
||||
offset node is text node: true
|
||||
offset is valid for text node: true
|
||||
client rect exists: true
|
||||
|
|
@ -2,8 +2,8 @@ Harness status: OK
|
|||
|
||||
Found 400 tests
|
||||
|
||||
326 Pass
|
||||
74 Fail
|
||||
343 Pass
|
||||
57 Fail
|
||||
Pass idl_test setup
|
||||
Pass idl_test validation
|
||||
Pass Partial interface Window: original interface defined
|
||||
|
|
@ -106,20 +106,20 @@ Pass Screen interface: screen must inherit property "width" with the proper type
|
|||
Pass Screen interface: screen must inherit property "height" with the proper type
|
||||
Pass Screen interface: screen must inherit property "colorDepth" with the proper type
|
||||
Pass Screen interface: screen must inherit property "pixelDepth" with the proper type
|
||||
Fail CaretPosition interface: existence and properties of interface object
|
||||
Fail CaretPosition interface object length
|
||||
Fail CaretPosition interface object name
|
||||
Fail CaretPosition interface: existence and properties of interface prototype object
|
||||
Fail CaretPosition interface: existence and properties of interface prototype object's "constructor" property
|
||||
Fail CaretPosition interface: existence and properties of interface prototype object's @@unscopables property
|
||||
Fail CaretPosition interface: attribute offsetNode
|
||||
Fail CaretPosition interface: attribute offset
|
||||
Fail CaretPosition interface: operation getClientRect()
|
||||
Fail CaretPosition must be primary interface of document.caretPositionFromPoint(5, 5)
|
||||
Fail Stringification of document.caretPositionFromPoint(5, 5)
|
||||
Fail CaretPosition interface: document.caretPositionFromPoint(5, 5) must inherit property "offsetNode" with the proper type
|
||||
Fail CaretPosition interface: document.caretPositionFromPoint(5, 5) must inherit property "offset" with the proper type
|
||||
Fail CaretPosition interface: document.caretPositionFromPoint(5, 5) must inherit property "getClientRect()" with the proper type
|
||||
Pass CaretPosition interface: existence and properties of interface object
|
||||
Pass CaretPosition interface object length
|
||||
Pass CaretPosition interface object name
|
||||
Pass CaretPosition interface: existence and properties of interface prototype object
|
||||
Pass CaretPosition interface: existence and properties of interface prototype object's "constructor" property
|
||||
Pass CaretPosition interface: existence and properties of interface prototype object's @@unscopables property
|
||||
Pass CaretPosition interface: attribute offsetNode
|
||||
Pass CaretPosition interface: attribute offset
|
||||
Pass CaretPosition interface: operation getClientRect()
|
||||
Pass CaretPosition must be primary interface of document.caretPositionFromPoint(5, 5)
|
||||
Pass Stringification of document.caretPositionFromPoint(5, 5)
|
||||
Pass CaretPosition interface: document.caretPositionFromPoint(5, 5) must inherit property "offsetNode" with the proper type
|
||||
Pass CaretPosition interface: document.caretPositionFromPoint(5, 5) must inherit property "offset" with the proper type
|
||||
Pass CaretPosition interface: document.caretPositionFromPoint(5, 5) must inherit property "getClientRect()" with the proper type
|
||||
Pass VisualViewport interface: existence and properties of interface object
|
||||
Pass VisualViewport interface object length
|
||||
Pass VisualViewport interface object name
|
||||
|
|
@ -309,7 +309,7 @@ Pass Window interface: window must inherit property "outerHeight" with the prope
|
|||
Pass Window interface: window must inherit property "devicePixelRatio" with the proper type
|
||||
Pass Document interface: operation elementFromPoint(double, double)
|
||||
Pass Document interface: operation elementsFromPoint(double, double)
|
||||
Fail Document interface: operation caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)
|
||||
Pass Document interface: operation caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)
|
||||
Pass Document interface: attribute scrollingElement
|
||||
Fail Document interface: operation getBoxQuads(optional BoxQuadOptions)
|
||||
Fail Document interface: operation convertQuadFromNode(DOMQuadInit, GeometryNode, optional ConvertCoordinateOptions)
|
||||
|
|
@ -319,8 +319,8 @@ Pass Document interface: document must inherit property "elementFromPoint(double
|
|||
Pass Document interface: calling elementFromPoint(double, double) on document with too few arguments must throw TypeError
|
||||
Pass Document interface: document must inherit property "elementsFromPoint(double, double)" with the proper type
|
||||
Pass Document interface: calling elementsFromPoint(double, double) on document with too few arguments must throw TypeError
|
||||
Fail Document interface: document must inherit property "caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)" with the proper type
|
||||
Fail Document interface: calling caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions) on document with too few arguments must throw TypeError
|
||||
Pass Document interface: document must inherit property "caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)" with the proper type
|
||||
Pass Document interface: calling caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions) on document with too few arguments must throw TypeError
|
||||
Pass Document interface: document must inherit property "scrollingElement" with the proper type
|
||||
Fail Document interface: document must inherit property "getBoxQuads(optional BoxQuadOptions)" with the proper type
|
||||
Fail Document interface: calling getBoxQuads(optional BoxQuadOptions) on document with too few arguments must throw TypeError
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font: 16px Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#container {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
<div id="container"><span id="host"></span><span id="other-host"></span></div>
|
||||
<script>
|
||||
function textRect(textNode, text) {
|
||||
const index = textNode.data.indexOf(text);
|
||||
const range = document.createRange();
|
||||
range.setStart(textNode, index);
|
||||
range.setEnd(textNode, index + text.length);
|
||||
return range.getBoundingClientRect();
|
||||
}
|
||||
|
||||
test(() => {
|
||||
const host = document.getElementById("host");
|
||||
const otherHost = document.getElementById("other-host");
|
||||
const shadowRoot = host.attachShadow({ mode: "open" });
|
||||
const inner = document.createElement("span");
|
||||
inner.textContent = "shadow text";
|
||||
shadowRoot.append(inner);
|
||||
|
||||
const otherShadowRoot = otherHost.attachShadow({ mode: "open" });
|
||||
otherShadowRoot.append("other shadow text");
|
||||
|
||||
document.body.offsetWidth;
|
||||
|
||||
const textNode = inner.firstChild;
|
||||
const rect = textRect(textNode, "shadow");
|
||||
const x = rect.x + rect.width / 2;
|
||||
const y = rect.y + rect.height / 2;
|
||||
const hostIndex = Array.prototype.indexOf.call(container.childNodes, host);
|
||||
|
||||
const retargetedPosition = document.caretPositionFromPoint(x, y);
|
||||
println(`without shadowRoots retargets to host parent: ${retargetedPosition.offsetNode === container}`);
|
||||
println(`without shadowRoots retargets to host index: ${retargetedPosition.offset === hostIndex}`);
|
||||
|
||||
const unrelatedShadowRootPosition = document.caretPositionFromPoint(x, y, { shadowRoots: [otherShadowRoot] });
|
||||
println(`unrelated shadowRoot still retargets to host parent: ${unrelatedShadowRootPosition.offsetNode === container}`);
|
||||
println(`unrelated shadowRoot still retargets to host index: ${unrelatedShadowRootPosition.offset === hostIndex}`);
|
||||
|
||||
const exposedPosition = document.caretPositionFromPoint(x, y, { shadowRoots: [shadowRoot] });
|
||||
println(`allowed shadowRoot exposes text node: ${exposedPosition.offsetNode === textNode}`);
|
||||
println(`allowed shadowRoot exposes text offset: ${exposedPosition.offset >= 0 && exposedPosition.offset <= textNode.length}`);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font: 16px Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#target {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
<div id="target">hello world</div>
|
||||
<script>
|
||||
test(() => {
|
||||
document.body.offsetWidth;
|
||||
|
||||
const textNode = target.firstChild;
|
||||
const range = document.createRange();
|
||||
range.setStart(textNode, 1);
|
||||
range.setEnd(textNode, 1);
|
||||
const rect = range.getBoundingClientRect();
|
||||
const caretPosition = document.caretPositionFromPoint(rect.x, rect.y + rect.height / 2);
|
||||
|
||||
println(`negative x returns null: ${document.caretPositionFromPoint(-1, 0) === null}`);
|
||||
println(`negative y returns null: ${document.caretPositionFromPoint(0, -1) === null}`);
|
||||
println(`caret position exists: ${caretPosition instanceof CaretPosition}`);
|
||||
println(`offset node is text node: ${caretPosition.offsetNode === textNode}`);
|
||||
println(`offset is valid for text node: ${caretPosition.offset >= 0 && caretPosition.offset <= textNode.length}`);
|
||||
println(`client rect exists: ${caretPosition.getClientRect() instanceof DOMRect}`);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue