LibWeb: Do not update selection on uneditable contents without Shift key

If selection navigation happens through an editing host, we should
enforce that for collapsed navigations (i.e. moving the caret) it can
only happen if the focus node of the selection is editable.
This commit is contained in:
Jelle Raaijmakers 2025-08-21 20:31:56 +02:00 committed by Jelle Raaijmakers
parent 09645875ea
commit 60a501d824
4 changed files with 93 additions and 12 deletions

View File

@ -78,39 +78,58 @@ void EditingHostManager::set_selection_focus(GC::Ref<DOM::Node> focus_node, size
m_document->reset_cursor_blink_cycle();
}
GC::Ptr<Selection::Selection> EditingHostManager::get_selection_for_navigation(CollapseSelection collapse) const
{
// In order for navigation to happen inside an editing host, the document must have a selection,
auto selection = m_document->get_selection();
if (!selection)
return {};
// and the focus node must be inside a text node,
auto focus_node = selection->focus_node();
if (!is<Text>(focus_node.ptr()))
return {};
// and if we're performing collapsed navigation (i.e. moving the caret), the focus node must be editable.
if (collapse == CollapseSelection::Yes && !focus_node->is_editable())
return {};
return selection;
}
void EditingHostManager::move_cursor_to_start(CollapseSelection collapse)
{
auto selection = m_document->get_selection();
auto node = selection->anchor_node();
if (!node || !is<DOM::Text>(*node))
auto selection = get_selection_for_navigation(collapse);
if (!selection)
return;
auto node = selection->focus_node();
if (collapse == CollapseSelection::Yes) {
MUST(selection->collapse(node, 0));
m_document->reset_cursor_blink_cycle();
return;
}
MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, 0));
MUST(selection->set_base_and_extent(*selection->anchor_node(), selection->anchor_offset(), *node, 0));
}
void EditingHostManager::move_cursor_to_end(CollapseSelection collapse)
{
auto selection = m_document->get_selection();
auto node = selection->anchor_node();
if (!node || !is<DOM::Text>(*node))
auto selection = get_selection_for_navigation(collapse);
if (!selection)
return;
auto node = selection->focus_node();
if (collapse == CollapseSelection::Yes) {
m_document->reset_cursor_blink_cycle();
MUST(selection->collapse(node, node->length()));
return;
}
MUST(selection->set_base_and_extent(*node, selection->anchor_offset(), *node, node->length()));
MUST(selection->set_base_and_extent(*selection->anchor_node(), selection->anchor_offset(), *node, node->length()));
}
void EditingHostManager::increment_cursor_position_offset(CollapseSelection collapse)
{
auto selection = m_document->get_selection();
auto selection = get_selection_for_navigation(collapse);
if (!selection)
return;
selection->move_offset_to_next_character(collapse == CollapseSelection::Yes);
@ -118,7 +137,7 @@ void EditingHostManager::increment_cursor_position_offset(CollapseSelection coll
void EditingHostManager::decrement_cursor_position_offset(CollapseSelection collapse)
{
auto selection = m_document->get_selection();
auto selection = get_selection_for_navigation(collapse);
if (!selection)
return;
selection->move_offset_to_previous_character(collapse == CollapseSelection::Yes);
@ -126,7 +145,7 @@ void EditingHostManager::decrement_cursor_position_offset(CollapseSelection coll
void EditingHostManager::increment_cursor_position_to_next_word(CollapseSelection collapse)
{
auto selection = m_document->get_selection();
auto selection = get_selection_for_navigation(collapse);
if (!selection)
return;
selection->move_offset_to_next_word(collapse == CollapseSelection::Yes);
@ -134,7 +153,7 @@ void EditingHostManager::increment_cursor_position_to_next_word(CollapseSelectio
void EditingHostManager::decrement_cursor_position_to_previous_word(CollapseSelection collapse)
{
auto selection = m_document->get_selection();
auto selection = get_selection_for_navigation(collapse);
if (!selection)
return;
selection->move_offset_to_previous_word(collapse == CollapseSelection::Yes);

View File

@ -50,6 +50,8 @@ private:
virtual GC::Ref<JS::Cell> as_cell() override { return *this; }
GC::Ptr<Selection::Selection> get_selection_for_navigation(CollapseSelection) const;
GC::Ref<Document> m_document;
GC::Ptr<DOM::Node> m_active_contenteditable_element;
};

View File

@ -0,0 +1,14 @@
#text 0 - #text 3
--- Left/Right key should not modify a selection on uneditable contents ---
#text 0 - #text 3
#text 0 - #text 3
--- Neither should WordJump + Left/Right ---
#text 0 - #text 3
#text 0 - #text 3
--- Shift + Left/Right key however, should modify the selection ---
#text 0 - #text 4
#text 0 - #text 3
--- Shift + WordJump + Left/Right as well ---
#text 0 - #text 7
#text 0 - #text 11
#text 0 - #text 8

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<div>foo bar baz</div>
<script src="include.js"></script>
<script>
test(() => {
function reportSelection() {
const range = getSelection().getRangeAt(0);
println(`${range.startContainer.nodeName} ${range.startOffset} - ${range.endContainer.nodeName} ${range.endOffset}`);
}
const divElm = document.querySelector('div');
getSelection().setBaseAndExtent(divElm.childNodes[0], 0, divElm.childNodes[0], 3);
reportSelection();
const modAlt = 1 << 0;
const modControl = 1 << 1;
const modShift = 1 << 2;
const modWordJump = modAlt | modControl; // macOS uses Mod_Alt, we should probably select just one modifier here.
println('--- Left/Right key should not modify a selection on uneditable contents ---');
internals.sendKey(divElm, 'Right');
reportSelection();
internals.sendKey(divElm, 'Left');
reportSelection();
println('--- Neither should WordJump + Left/Right ---');
internals.sendKey(divElm, 'Right', modWordJump);
reportSelection();
internals.sendKey(divElm, 'Left', modWordJump);
reportSelection();
println('--- Shift + Left/Right key however, should modify the selection ---');
internals.sendKey(divElm, 'Right', modShift);
reportSelection();
internals.sendKey(divElm, 'Left', modShift);
reportSelection();
println('--- Shift + WordJump + Left/Right as well ---');
internals.sendKey(divElm, 'Right', modWordJump | modShift);
reportSelection();
internals.sendKey(divElm, 'Right', modWordJump | modShift);
reportSelection();
internals.sendKey(divElm, 'Left', modWordJump | modShift);
reportSelection();
});
</script>