LibWeb: Fix ability to modify selection outside of inputs using keyboard
Fixes regression introduced in a8077f79cc
This commit is contained in:
parent
7dc11050f2
commit
4a1e109678
Notes:
github-actions[bot]
2024-11-01 17:12:08 +00:00
Author: https://github.com/kalenikaliaksandr Commit: https://github.com/LadybirdBrowser/ladybird/commit/4a1e1096783 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2108 Reviewed-by: https://github.com/trflynn89 ✅
6 changed files with 147 additions and 73 deletions
2
Tests/LibWeb/Text/expected/select-text.txt
Normal file
2
Tests/LibWeb/Text/expected/select-text.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Lorem
|
||||
Lorem ip
|
25
Tests/LibWeb/Text/input/select-text.html
Normal file
25
Tests/LibWeb/Text/input/select-text.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script src="include.js"></script>
|
||||
<style>
|
||||
div {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
<div id="text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sapien velit, sagittis vel mauris eget, placerat mattis arcu. Praesent ac pharetra dolor.</div>
|
||||
<script>
|
||||
test(() => {
|
||||
// Select word "Lorem" by dispatching double click event
|
||||
internals.doubleclick(20, 20);
|
||||
|
||||
// Should be equal to "Lorem"
|
||||
println(window.getSelection());
|
||||
|
||||
const text = document.getElementById("text");
|
||||
|
||||
let modifiers = 1 << 2; // Mod_Shift
|
||||
internals.sendKey(text, "Right", modifiers);
|
||||
internals.sendKey(text, "Right", modifiers);
|
||||
internals.sendKey(text, "Right", modifiers);
|
||||
// Should be equal to "Lorem ip"
|
||||
println(window.getSelection());
|
||||
});
|
||||
</script>
|
|
@ -4,8 +4,6 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/EditingHostManager.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
|
@ -132,96 +130,33 @@ void EditingHostManager::move_cursor_to_end(CollapseSelection collapse)
|
|||
void EditingHostManager::increment_cursor_position_offset(CollapseSelection collapse)
|
||||
{
|
||||
auto selection = m_document->get_selection();
|
||||
auto node = selection->anchor_node();
|
||||
if (!node || !is<DOM::Text>(*node))
|
||||
if (!selection)
|
||||
return;
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*node);
|
||||
if (auto offset = text_node.grapheme_segmenter().next_boundary(selection->focus_offset()); offset.has_value()) {
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
MUST(selection->collapse(*node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
|
||||
}
|
||||
}
|
||||
selection->move_offset_to_next_character(collapse == CollapseSelection::Yes);
|
||||
}
|
||||
|
||||
void EditingHostManager::decrement_cursor_position_offset(CollapseSelection collapse)
|
||||
{
|
||||
auto selection = m_document->get_selection();
|
||||
auto node = selection->anchor_node();
|
||||
if (!node || !is<DOM::Text>(*node)) {
|
||||
if (!selection)
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*node);
|
||||
if (auto offset = text_node.grapheme_segmenter().previous_boundary(selection->focus_offset()); offset.has_value()) {
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
MUST(selection->collapse(*node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
|
||||
}
|
||||
}
|
||||
selection->move_offset_to_previous_character(collapse == CollapseSelection::Yes);
|
||||
}
|
||||
|
||||
void EditingHostManager::increment_cursor_position_to_next_word(CollapseSelection collapse)
|
||||
{
|
||||
auto selection = m_document->get_selection();
|
||||
auto node = selection->anchor_node();
|
||||
if (!node || !is<DOM::Text>(*node)) {
|
||||
if (!selection)
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*node);
|
||||
|
||||
while (true) {
|
||||
auto focus_offset = selection->focus_offset();
|
||||
if (focus_offset == text_node.data().bytes_as_string_view().length()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto offset = text_node.word_segmenter().next_boundary(focus_offset); offset.has_value()) {
|
||||
auto word = text_node.data().code_points().substring_view(focus_offset, *offset - focus_offset);
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
MUST(selection->collapse(node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
|
||||
}
|
||||
if (Unicode::Segmenter::should_continue_beyond_word(word))
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
selection->move_offset_to_next_character(collapse == CollapseSelection::Yes);
|
||||
}
|
||||
|
||||
void EditingHostManager::decrement_cursor_position_to_previous_word(CollapseSelection collapse)
|
||||
{
|
||||
auto selection = m_document->get_selection();
|
||||
auto node = selection->anchor_node();
|
||||
if (!node || !is<DOM::Text>(*node)) {
|
||||
if (!selection)
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*node);
|
||||
|
||||
while (true) {
|
||||
auto focus_offset = selection->focus_offset();
|
||||
if (auto offset = text_node.word_segmenter().previous_boundary(focus_offset); offset.has_value()) {
|
||||
auto word = text_node.data().code_points().substring_view(focus_offset, focus_offset - *offset);
|
||||
if (collapse == CollapseSelection::Yes) {
|
||||
MUST(selection->collapse(node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, *offset));
|
||||
}
|
||||
if (Unicode::Segmenter::should_continue_beyond_word(word))
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
selection->move_offset_to_previous_word(collapse == CollapseSelection::Yes);
|
||||
}
|
||||
|
||||
void EditingHostManager::handle_delete(DeleteDirection direction)
|
||||
|
|
|
@ -1023,6 +1023,22 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u
|
|||
FIRE(input_event(UIEvents::EventNames::input, UIEvents::InputTypes::insertText, m_navigable, code_point));
|
||||
return EventResult::Handled;
|
||||
}
|
||||
} else if (auto selection = document->get_selection(); selection && !selection->is_collapsed()) {
|
||||
if (modifiers & UIEvents::Mod_Shift) {
|
||||
if (key == UIEvents::KeyCode::Key_Right) {
|
||||
if (modifiers & UIEvents::Mod_PlatformWordJump)
|
||||
selection->move_offset_to_next_word(false);
|
||||
else
|
||||
selection->move_offset_to_next_character(false);
|
||||
return EventResult::Handled;
|
||||
} else if (key == UIEvents::KeyCode::Key_Left) {
|
||||
if (modifiers & UIEvents::Mod_PlatformWordJump)
|
||||
selection->move_offset_to_previous_word(false);
|
||||
else
|
||||
selection->move_offset_to_previous_character(false);
|
||||
return EventResult::Handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Implement scroll by line and by page instead of approximating the behavior of other browsers.
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Bindings/SelectionPrototype.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Position.h>
|
||||
#include <LibWeb/DOM/Range.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/Selection/Selection.h>
|
||||
|
||||
namespace Web::Selection {
|
||||
|
@ -484,4 +486,92 @@ JS::GCPtr<DOM::Position> Selection::cursor_position() const
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
void Selection::move_offset_to_next_character(bool collapse_selection)
|
||||
{
|
||||
auto anchor_node = this->anchor_node();
|
||||
if (!anchor_node || !is<DOM::Text>(*anchor_node))
|
||||
return;
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*anchor_node);
|
||||
if (auto offset = text_node.grapheme_segmenter().next_boundary(focus_offset()); offset.has_value()) {
|
||||
if (collapse_selection) {
|
||||
MUST(collapse(*anchor_node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(set_base_and_extent(*anchor_node, anchor_offset(), *anchor_node, *offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Selection::move_offset_to_previous_character(bool collapse_selection)
|
||||
{
|
||||
auto anchor_node = this->anchor_node();
|
||||
if (!anchor_node || !is<DOM::Text>(*anchor_node))
|
||||
return;
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*anchor_node);
|
||||
if (auto offset = text_node.grapheme_segmenter().previous_boundary(focus_offset()); offset.has_value()) {
|
||||
if (collapse_selection) {
|
||||
MUST(collapse(*anchor_node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(set_base_and_extent(*anchor_node, anchor_offset(), *anchor_node, *offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Selection::move_offset_to_next_word(bool collapse_selection)
|
||||
{
|
||||
auto anchor_node = this->anchor_node();
|
||||
if (!anchor_node || !is<DOM::Text>(*anchor_node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*anchor_node);
|
||||
while (true) {
|
||||
auto focus_offset = this->focus_offset();
|
||||
if (focus_offset == text_node.data().bytes_as_string_view().length()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto offset = text_node.word_segmenter().next_boundary(focus_offset); offset.has_value()) {
|
||||
auto word = text_node.data().code_points().substring_view(focus_offset, *offset - focus_offset);
|
||||
if (collapse_selection) {
|
||||
MUST(collapse(anchor_node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(set_base_and_extent(*anchor_node, this->anchor_offset(), *anchor_node, *offset));
|
||||
}
|
||||
if (Unicode::Segmenter::should_continue_beyond_word(word))
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Selection::move_offset_to_previous_word(bool collapse_selection)
|
||||
{
|
||||
auto anchor_node = this->anchor_node();
|
||||
if (!anchor_node || !is<DOM::Text>(*anchor_node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& text_node = static_cast<DOM::Text&>(*anchor_node);
|
||||
while (true) {
|
||||
auto focus_offset = this->focus_offset();
|
||||
if (auto offset = text_node.word_segmenter().previous_boundary(focus_offset); offset.has_value()) {
|
||||
auto word = text_node.data().code_points().substring_view(focus_offset, focus_offset - *offset);
|
||||
if (collapse_selection) {
|
||||
MUST(collapse(anchor_node, *offset));
|
||||
m_document->reset_cursor_blink_cycle();
|
||||
} else {
|
||||
MUST(set_base_and_extent(*anchor_node, anchor_offset(), *anchor_node, *offset));
|
||||
}
|
||||
if (Unicode::Segmenter::should_continue_beyond_word(word))
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -61,6 +61,12 @@ public:
|
|||
// Non-standard
|
||||
JS::GCPtr<DOM::Position> cursor_position() const;
|
||||
|
||||
// Non-standard
|
||||
void move_offset_to_next_character(bool collapse_selection);
|
||||
void move_offset_to_previous_character(bool collapse_selection);
|
||||
void move_offset_to_next_word(bool collapse_selection);
|
||||
void move_offset_to_previous_word(bool collapse_selection);
|
||||
|
||||
private:
|
||||
Selection(JS::NonnullGCPtr<JS::Realm>, JS::NonnullGCPtr<DOM::Document>);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue