LibWeb: Make message drag selection more eager
Use selection-specific caret hit testing while starting and extending mouse selections. The public caret-position API keeps its normal line ranking, but selection drags now snap below-line movement to line edges and prefer the previous line when starting in a nearby inter-line gap. Add coverage for dragging from message text, after-text space, gutters, author names, avatar-adjacent areas, and row bottoms so these inert message regions reliably start selection.
This commit is contained in:
parent
76da9aa116
commit
fe188b7d57
12 changed files with 294 additions and 8 deletions
|
|
@ -8521,7 +8521,27 @@ Optional<Painting::CaretPosition> Document::caret_position_from_point(CSSPixelPo
|
|||
if (!hit_test_display_list || !viewport_paintable)
|
||||
return {};
|
||||
viewport_paintable->refresh_scroll_state();
|
||||
return hit_test_display_list->caret_position_from_point(position, *viewport_paintable, page().client().device_pixels_per_css_pixel(), page().chrome_metrics());
|
||||
return hit_test_display_list->caret_position_from_point(position, *viewport_paintable, page().client().device_pixels_per_css_pixel(), page().chrome_metrics(), Painting::CaretPositionMode::Normal);
|
||||
}
|
||||
|
||||
Optional<Painting::CaretPosition> Document::caret_position_from_point_for_selection_start(CSSPixelPoint position)
|
||||
{
|
||||
auto hit_test_display_list = ensure_hit_test_display_list();
|
||||
auto viewport_paintable = paintable();
|
||||
if (!hit_test_display_list || !viewport_paintable)
|
||||
return {};
|
||||
viewport_paintable->refresh_scroll_state();
|
||||
return hit_test_display_list->caret_position_from_point(position, *viewport_paintable, page().client().device_pixels_per_css_pixel(), page().chrome_metrics(), Painting::CaretPositionMode::SelectionStart);
|
||||
}
|
||||
|
||||
Optional<Painting::CaretPosition> Document::caret_position_from_point_for_selection(CSSPixelPoint position)
|
||||
{
|
||||
auto hit_test_display_list = ensure_hit_test_display_list();
|
||||
auto viewport_paintable = paintable();
|
||||
if (!hit_test_display_list || !viewport_paintable)
|
||||
return {};
|
||||
viewport_paintable->refresh_scroll_state();
|
||||
return hit_test_display_list->caret_position_from_point(position, *viewport_paintable, page().client().device_pixels_per_css_pixel(), page().chrome_metrics(), Painting::CaretPositionMode::Selection);
|
||||
}
|
||||
|
||||
TraversalDecision Document::hit_test_all(CSSPixelPoint position, Function<TraversalDecision(Painting::HitTestResult)> const& callback)
|
||||
|
|
|
|||
|
|
@ -982,6 +982,8 @@ 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);
|
||||
Optional<Painting::CaretPosition> caret_position_from_point_for_selection_start(CSSPixelPoint);
|
||||
Optional<Painting::CaretPosition> caret_position_from_point_for_selection(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&);
|
||||
|
||||
|
|
|
|||
|
|
@ -1599,7 +1599,7 @@ void EventHandler::run_mousedown_default_actions(DOM::Document& document, CSSPix
|
|||
return;
|
||||
|
||||
// NB: Now we can do selection with a caret-position hit test.
|
||||
auto caret_position = document.caret_position_from_point(visual_viewport_position);
|
||||
auto caret_position = document.caret_position_from_point_for_selection_start(visual_viewport_position);
|
||||
if (!caret_position.has_value())
|
||||
return;
|
||||
|
||||
|
|
@ -2095,7 +2095,7 @@ void EventHandler::update_mouse_selection(CSSPixelPoint visual_viewport_position
|
|||
void EventHandler::apply_mouse_selection(CSSPixelPoint visual_viewport_position)
|
||||
{
|
||||
auto& document = *m_navigable->active_document();
|
||||
auto caret_position = document.caret_position_from_point(visual_viewport_position);
|
||||
auto caret_position = document.caret_position_from_point_for_selection(visual_viewport_position);
|
||||
if (!caret_position.has_value())
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
namespace Web::Painting {
|
||||
|
||||
class HitTestDisplayList;
|
||||
enum class CaretPositionMode : u8;
|
||||
enum class HitTestType : u8;
|
||||
struct CaretPosition;
|
||||
struct HitTestResult;
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_for_hit_container(Ite
|
|||
};
|
||||
}
|
||||
|
||||
Optional<CaretPosition> HitTestDisplayList::caret_position_for_line(CaretLine const& line, CSSPixelPoint local_point) const
|
||||
Optional<CaretPosition> HitTestDisplayList::caret_position_for_line(CaretLine const& line, CSSPixelPoint local_point, CaretPositionMode mode) const
|
||||
{
|
||||
auto const& first_item = m_items[m_caret_item_indices[line.first_caret_item_index]];
|
||||
auto writing_mode = first_item.paintable->computed_values().writing_mode();
|
||||
|
|
@ -587,6 +587,12 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_for_line(CaretLine co
|
|||
return caret_position_for_item(item_at_line_edge(CaretPositionType::Before), local_point, CaretPositionType::Before);
|
||||
|
||||
auto inline_coordinate = inline_axis_coordinate(local_point, writing_mode);
|
||||
if (mode == CaretPositionMode::Selection && block_coordinate >= block_axis_end(line.rect, writing_mode))
|
||||
return caret_position_for_item(item_at_line_edge(CaretPositionType::After), local_point, CaretPositionType::After);
|
||||
|
||||
if (block_coordinate >= block_axis_end(line.rect, writing_mode) + caret_line_block_axis_compare_slop)
|
||||
return caret_position_for_item(item_at_line_edge(CaretPositionType::After), local_point, CaretPositionType::After);
|
||||
|
||||
if (block_coordinate >= block_axis_end(line.rect, writing_mode)
|
||||
&& (inline_coordinate < inline_axis_start(line.rect, writing_mode) || inline_coordinate >= inline_axis_end(line.rect, writing_mode)))
|
||||
return caret_position_for_item(item_at_line_edge(CaretPositionType::After), local_point, CaretPositionType::After);
|
||||
|
|
@ -626,6 +632,21 @@ bool HitTestDisplayList::line_contains_descendant_of(CaretLine const& line, DOM:
|
|||
return false;
|
||||
}
|
||||
|
||||
bool HitTestDisplayList::item_is_inline_adjacent_to_line(Item const& item, CaretLine const& line) const
|
||||
{
|
||||
if (item.visual_context_index != line.visual_context_index || item.rect.is_empty())
|
||||
return false;
|
||||
|
||||
auto first_item_index = m_caret_item_indices[line.first_caret_item_index];
|
||||
auto const& first_item = m_items[first_item_index];
|
||||
auto writing_mode = first_item.paintable->computed_values().writing_mode();
|
||||
if (!rects_overlap_in_block_axis(item.rect, line.rect, writing_mode))
|
||||
return false;
|
||||
|
||||
return inline_axis_end(item.rect, writing_mode) <= inline_axis_start(line.rect, writing_mode)
|
||||
|| inline_axis_end(line.rect, writing_mode) <= inline_axis_start(item.rect, writing_mode);
|
||||
}
|
||||
|
||||
void HitTestDisplayList::find_topmost_item_in_list(Vector<size_t> const& item_indices, CSSPixelPoint local_point, ChromeMetrics const& chrome_metrics, Optional<size_t>& topmost_item_index) const
|
||||
{
|
||||
for (auto item_index : item_indices.in_reverse()) {
|
||||
|
|
@ -661,7 +682,7 @@ void HitTestDisplayList::find_items_in_list(Vector<size_t> const& item_indices,
|
|||
}
|
||||
}
|
||||
|
||||
Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPoint point, ViewportPaintable const& viewport_paintable, double device_pixels_per_css_pixel, ChromeMetrics const& chrome_metrics) const
|
||||
Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPoint point, ViewportPaintable const& viewport_paintable, double device_pixels_per_css_pixel, ChromeMetrics const& chrome_metrics, CaretPositionMode mode) const
|
||||
{
|
||||
if (m_visual_context_tree_version != viewport_paintable.visual_context_tree().version())
|
||||
return {};
|
||||
|
|
@ -745,6 +766,7 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPo
|
|||
auto find_closest_line = [&](DOM::Node const* scope_dom_node) {
|
||||
ClosestLine closest_line;
|
||||
ClosestLine closest_line_after_point;
|
||||
ClosestLine closest_line_before_point;
|
||||
|
||||
for (size_t line_index = 0; line_index < m_caret_lines.size(); ++line_index) {
|
||||
auto const& line = m_caret_lines[line_index];
|
||||
|
|
@ -771,6 +793,17 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPo
|
|||
closest_line.is_before_point = block_axis_end(line.rect, writing_mode) < block_coordinate;
|
||||
}
|
||||
|
||||
if (block_axis_end(line.rect, writing_mode) < block_coordinate
|
||||
&& (!closest_line_before_point.index.has_value()
|
||||
|| caret_line_is_better_candidate(block_distance, inline_distance, closest_line_before_point.block_distance, closest_line_before_point.inline_distance, caret_line_block_axis_compare_slop))) {
|
||||
closest_line_before_point.index = line_index;
|
||||
closest_line_before_point.local_point = local_point;
|
||||
closest_line_before_point.block_distance = block_distance;
|
||||
closest_line_before_point.inline_distance = inline_distance;
|
||||
closest_line_before_point.block_container_margin_rect = line.block_container_margin_rect;
|
||||
closest_line_before_point.is_before_point = true;
|
||||
}
|
||||
|
||||
auto block_start = block_axis_start(line.rect, writing_mode);
|
||||
if (block_start <= block_coordinate)
|
||||
continue;
|
||||
|
|
@ -789,6 +822,12 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPo
|
|||
}
|
||||
}
|
||||
|
||||
if (mode == CaretPositionMode::SelectionStart
|
||||
&& closest_line_before_point.index.has_value()
|
||||
&& closest_line.index != closest_line_before_point.index
|
||||
&& closest_line_before_point.block_distance <= caret_item_block_axis_compare_slop)
|
||||
return closest_line_before_point;
|
||||
|
||||
if (closest_line.index.has_value()
|
||||
&& closest_line.is_before_point
|
||||
&& closest_line_after_point.index.has_value()
|
||||
|
|
@ -835,7 +874,7 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPo
|
|||
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);
|
||||
auto caret_position = caret_position_for_line(m_caret_lines[*closest_line.index], *closest_line.local_point, mode);
|
||||
if (!caret_position.has_value())
|
||||
return {};
|
||||
if (caret_position->debug_rect.has_value())
|
||||
|
|
@ -851,6 +890,8 @@ Optional<CaretPosition> HitTestDisplayList::caret_position_from_point(CSSPixelPo
|
|||
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;
|
||||
}
|
||||
if (item_is_inline_adjacent_to_line(topmost_hit_item, m_caret_lines[*closest_line.index]))
|
||||
return caret_position;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ namespace Painting {
|
|||
class PaintableFragment;
|
||||
class ViewportPaintable;
|
||||
|
||||
enum class CaretPositionMode : u8 {
|
||||
Normal,
|
||||
// Starting or extending selection should feel more eager than the public caret-position API in inter-line gaps.
|
||||
SelectionStart,
|
||||
Selection,
|
||||
};
|
||||
|
||||
class WEB_API HitTestDisplayList : public RefCounted<HitTestDisplayList> {
|
||||
public:
|
||||
static NonnullRefPtr<HitTestDisplayList> create(u64 visual_context_tree_version);
|
||||
|
|
@ -34,7 +41,7 @@ public:
|
|||
|
||||
u64 visual_context_tree_version() const { return m_visual_context_tree_version; }
|
||||
[[nodiscard]] Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType, ViewportPaintable const&, double device_pixels_per_css_pixel, ChromeMetrics const&) const;
|
||||
[[nodiscard]] Optional<CaretPosition> caret_position_from_point(CSSPixelPoint, ViewportPaintable const&, double device_pixels_per_css_pixel, ChromeMetrics const&) const;
|
||||
[[nodiscard]] Optional<CaretPosition> caret_position_from_point(CSSPixelPoint, ViewportPaintable const&, double device_pixels_per_css_pixel, ChromeMetrics const&, CaretPositionMode = CaretPositionMode::Normal) const;
|
||||
TraversalDecision hit_test_all(CSSPixelPoint, ViewportPaintable const&, double device_pixels_per_css_pixel, ChromeMetrics const&, Function<TraversalDecision(HitTestResult)> const&) const;
|
||||
|
||||
private:
|
||||
|
|
@ -95,8 +102,9 @@ private:
|
|||
[[nodiscard]] HitTestResult hit_test_result_for_item(Item const&, CSSPixelPoint local_point) const;
|
||||
[[nodiscard]] Optional<CaretPosition> caret_position_for_item(Item const&, CSSPixelPoint local_point, CaretPositionType = CaretPositionType::Closest) const;
|
||||
[[nodiscard]] Optional<CaretPosition> caret_position_for_hit_container(Item const&) const;
|
||||
[[nodiscard]] Optional<CaretPosition> caret_position_for_line(CaretLine const&, CSSPixelPoint local_point) const;
|
||||
[[nodiscard]] Optional<CaretPosition> caret_position_for_line(CaretLine const&, CSSPixelPoint local_point, CaretPositionMode) const;
|
||||
[[nodiscard]] bool line_contains_descendant_of(CaretLine const&, DOM::Node const&) const;
|
||||
[[nodiscard]] bool item_is_inline_adjacent_to_line(Item const&, CaretLine const&) const;
|
||||
void find_topmost_item_in_list(Vector<size_t> const&, CSSPixelPoint local_point, ChromeMetrics const&, Optional<size_t>& topmost_item_index) const;
|
||||
void find_topmost_caret_item_in_list(Vector<size_t> const&, CSSPixelPoint local_point, ChromeMetrics const&, Optional<size_t>& topmost_item_index) const;
|
||||
void find_items_in_list(Vector<size_t> const&, CSSPixelPoint local_point, ChromeMetrics const&, Vector<size_t>& hit_item_indices) const;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
text_mid_to_next: true
|
||||
text_after_to_next: true
|
||||
below_text_mid_tiny_down: true
|
||||
below_text_mid_to_next: true
|
||||
below_text_after_to_next: true
|
||||
left_gutter_to_next: true
|
||||
name_mid_to_next: true
|
||||
name_after_to_next: true
|
||||
name_below_to_next: true
|
||||
avatar_center_to_next: true
|
||||
avatar_left_to_next: true
|
||||
avatar_right_to_next: true
|
||||
avatar_below_to_next: true
|
||||
row_bottom_to_next: true
|
||||
|
|
@ -0,0 +1 @@
|
|||
selected chat text: true
|
||||
|
|
@ -0,0 +1 @@
|
|||
selected chat text: true
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font: 16px Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
min-height: 72px;
|
||||
padding-left: 72px;
|
||||
position: relative;
|
||||
width: 520px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #5865f2;
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
left: 16px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.text {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.compact {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.compact .text {
|
||||
padding-top: 4px;
|
||||
}
|
||||
</style>
|
||||
<div class="message" id="m1">
|
||||
<div class="avatar" id="avatar"></div>
|
||||
<div class="name" id="name">Alice</div>
|
||||
<div class="text" id="text1">Alice says hello from the chat history.</div>
|
||||
</div>
|
||||
<div class="message compact" id="m2">
|
||||
<div class="text" id="text2">Bob is looking at the next message now.</div>
|
||||
</div>
|
||||
<script>
|
||||
function textRect(element) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element.firstChild);
|
||||
return range.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function rectFor(id) {
|
||||
return document.getElementById(id).getBoundingClientRect();
|
||||
}
|
||||
|
||||
function point(name) {
|
||||
const textRect1 = textRect(text1);
|
||||
const nameRect = textRect(document.getElementById("name"));
|
||||
const avatarRect = rectFor("avatar");
|
||||
const messageRect = rectFor("m1");
|
||||
const nextTextRect = textRect(text2);
|
||||
const points = {
|
||||
text_mid: { x: textRect1.x + textRect1.width / 2, y: textRect1.y + textRect1.height / 2 },
|
||||
text_after: { x: textRect1.right + 10, y: textRect1.y + textRect1.height / 2 },
|
||||
text_below_mid: { x: textRect1.x + textRect1.width / 2, y: textRect1.bottom + 1 },
|
||||
text_below_after: { x: textRect1.right + 10, y: textRect1.bottom + 1 },
|
||||
text_left_gutter: { x: textRect1.x - 20, y: textRect1.y + textRect1.height / 2 },
|
||||
name_mid: { x: nameRect.x + nameRect.width / 2, y: nameRect.y + nameRect.height / 2 },
|
||||
name_after: { x: nameRect.right + 10, y: nameRect.y + nameRect.height / 2 },
|
||||
name_below_mid: { x: nameRect.x + nameRect.width / 2, y: nameRect.bottom + 1 },
|
||||
avatar_center: { x: avatarRect.x + avatarRect.width / 2, y: avatarRect.y + avatarRect.height / 2 },
|
||||
avatar_left: { x: avatarRect.x - 4, y: avatarRect.y + avatarRect.height / 2 },
|
||||
avatar_right: { x: avatarRect.right + 4, y: avatarRect.y + avatarRect.height / 2 },
|
||||
avatar_below: { x: avatarRect.x + avatarRect.width / 2, y: avatarRect.bottom + 4 },
|
||||
row_bottom_mid: { x: messageRect.x + messageRect.width / 2, y: messageRect.bottom - 2 },
|
||||
next_text_start: { x: nextTextRect.x + 8, y: nextTextRect.y + nextTextRect.height / 2 },
|
||||
tiny_down_from_below: { x: textRect1.x + textRect1.width / 2, y: textRect1.bottom + 4 },
|
||||
};
|
||||
return points[name];
|
||||
}
|
||||
|
||||
function dragSelect(caseName, startName, endName) {
|
||||
getSelection().removeAllRanges();
|
||||
const start = point(startName);
|
||||
const end = point(endName);
|
||||
internals.mouseDown(start.x, start.y);
|
||||
internals.mouseMove(end.x, end.y);
|
||||
internals.mouseUp(end.x, end.y);
|
||||
println(`${caseName}: ${getSelection().toString().length > 0}`);
|
||||
}
|
||||
|
||||
test(() => {
|
||||
document.body.offsetWidth;
|
||||
|
||||
dragSelect("text_mid_to_next", "text_mid", "next_text_start");
|
||||
dragSelect("text_after_to_next", "text_after", "next_text_start");
|
||||
dragSelect("below_text_mid_tiny_down", "text_below_mid", "tiny_down_from_below");
|
||||
dragSelect("below_text_mid_to_next", "text_below_mid", "next_text_start");
|
||||
dragSelect("below_text_after_to_next", "text_below_after", "next_text_start");
|
||||
dragSelect("left_gutter_to_next", "text_left_gutter", "next_text_start");
|
||||
dragSelect("name_mid_to_next", "name_mid", "next_text_start");
|
||||
dragSelect("name_after_to_next", "name_after", "next_text_start");
|
||||
dragSelect("name_below_to_next", "name_below_mid", "next_text_start");
|
||||
dragSelect("avatar_center_to_next", "avatar_center", "next_text_start");
|
||||
dragSelect("avatar_left_to_next", "avatar_left", "next_text_start");
|
||||
dragSelect("avatar_right_to_next", "avatar_right", "next_text_start");
|
||||
dragSelect("avatar_below_to_next", "avatar_below", "next_text_start");
|
||||
dragSelect("row_bottom_to_next", "row_bottom_mid", "next_text_start");
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font: 16px Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
min-height: 48px;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 4px;
|
||||
}
|
||||
</style>
|
||||
<div class="message">
|
||||
<div class="content" id="content">Alice says hello from the chat history.</div>
|
||||
</div>
|
||||
<script>
|
||||
test(() => {
|
||||
document.body.offsetWidth;
|
||||
|
||||
const textNode = content.firstChild;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(textNode);
|
||||
const rect = range.getBoundingClientRect();
|
||||
const x = rect.x + rect.width / 2;
|
||||
const y = rect.bottom + 1;
|
||||
internals.mouseDown(x, y);
|
||||
internals.mouseMove(x, y + 3);
|
||||
internals.mouseUp(x, y + 3);
|
||||
|
||||
println(`selected chat text: ${getSelection().toString().length > 0}`);
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font: 16px Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
min-height: 60px;
|
||||
position: relative;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
height: 60px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 72px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
<div class="message">
|
||||
<div class="gutter"></div>
|
||||
<div class="content" id="content">Alice says hello from the chat history.</div>
|
||||
</div>
|
||||
<script>
|
||||
test(() => {
|
||||
document.body.offsetWidth;
|
||||
|
||||
const contentRect = content.getBoundingClientRect();
|
||||
internals.mouseDown(10, contentRect.top + 10);
|
||||
internals.mouseMove(contentRect.left + 80, contentRect.top + 20);
|
||||
internals.mouseUp(contentRect.left + 80, contentRect.top + 20);
|
||||
|
||||
println(`selected chat text: ${getSelection().toString().includes("Alice")}`);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue