From c369f68effcaa049bd404724b3693412cd48b69f Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 14 Aug 2025 15:25:19 -0400 Subject: [PATCH] LibWeb: Delete entire graphemes when the delete/backspace key is pressed We currently delete a single code unit. If the user presses backspace on a multi code point emoji, they are going to expect the entire emoji to be removed. This now matches the behavior of Chrome and Firefox. --- Libraries/LibWeb/Editing/Commands.cpp | 14 +++++++--- .../LibWeb/HTML/FormAssociatedElement.cpp | 9 +++---- .../expected/Editing/execCommand-delete.txt | 4 +++ .../Editing/execCommand-forwardDelete.txt | 3 +++ Tests/LibWeb/Text/expected/input-delete.txt | 6 +++++ .../input/Editing/execCommand-delete.html | 26 ++++++++++++------- .../Editing/execCommand-forwardDelete.html | 2 ++ Tests/LibWeb/Text/input/input-delete.html | 24 +++++++++++++++++ 8 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/input-delete.txt create mode 100644 Tests/LibWeb/Text/input/input-delete.html diff --git a/Libraries/LibWeb/Editing/Commands.cpp b/Libraries/LibWeb/Editing/Commands.cpp index 1defb51e07c..6f6260cec75 100644 --- a/Libraries/LibWeb/Editing/Commands.cpp +++ b/Libraries/LibWeb/Editing/Commands.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -204,12 +205,17 @@ bool command_delete_action(DOM::Document& document, Utf16String const&) block_node_child_is_relevant_type = child_element.local_name().is_one_of(HTML::TagNames::br, HTML::TagNames::hr, HTML::TagNames::img); } } - if ((is(*node) && offset != 0) || block_node_child_is_relevant_type) { + + if (auto const* text_node = as_if(*node); (text_node && offset != 0) || block_node_child_is_relevant_type) { + auto start_offset = text_node + ? text_node->grapheme_segmenter().previous_boundary(offset).value_or(offset - 1) + : offset - 1; + // 1. Call collapse(node, offset) on the context object's selection. MUST(selection.collapse(node, offset)); // 2. Call extend(node, offset − 1) on the context object's selection. - MUST(selection.extend(*node, offset - 1)); + MUST(selection.extend(*node, start_offset)); // 3. Delete the selection. delete_the_selection(selection); @@ -927,9 +933,9 @@ bool command_forward_delete_action(DOM::Document& document, Utf16String const&) } // 5. If node is a Text node and offset is not node's length: - if (is(*node) && offset != node->length()) { + if (auto const* text_node = as_if(*node); text_node && offset != node->length()) { // 1. Let end offset be offset plus one. - auto end_offset = offset + 1; + auto end_offset = text_node->grapheme_segmenter().next_boundary(offset).value_or(offset + 1); // FIXME: 2. While end offset is not node's length and the end offsetth code unit of node's data has general category M // when interpreted as a Unicode code point, add one to end offset. diff --git a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp index ec3f2360000..b7cab2fe652 100644 --- a/Libraries/LibWeb/HTML/FormAssociatedElement.cpp +++ b/Libraries/LibWeb/HTML/FormAssociatedElement.cpp @@ -829,13 +829,12 @@ void FormAssociatedTextControlElement::handle_delete(DeleteDirection direction) if (selection_start == selection_end) { if (direction == DeleteDirection::Backward) { - if (selection_start > 0) - MUST(set_range_text({}, selection_start - 1, selection_end, Bindings::SelectionMode::End)); + if (auto offset = text_node->grapheme_segmenter().previous_boundary(m_selection_end); offset.has_value()) + selection_start = *offset; } else { - if (selection_start < text_node->length_in_utf16_code_units()) - MUST(set_range_text({}, selection_start, selection_end + 1, Bindings::SelectionMode::End)); + if (auto offset = text_node->grapheme_segmenter().next_boundary(m_selection_end); offset.has_value()) + selection_end = *offset; } - return; } MUST(set_range_text({}, selection_start, selection_end, Bindings::SelectionMode::End)); diff --git a/Tests/LibWeb/Text/expected/Editing/execCommand-delete.txt b/Tests/LibWeb/Text/expected/Editing/execCommand-delete.txt index 5022da5f91a..2ecb7b918bb 100644 --- a/Tests/LibWeb/Text/expected/Editing/execCommand-delete.txt +++ b/Tests/LibWeb/Text/expected/Editing/execCommand-delete.txt @@ -1,2 +1,6 @@ +--- a --- Before: foobar After: fobar +--- b --- +Before: foo👩🏼‍❤️‍👨🏻bar +After: foobar diff --git a/Tests/LibWeb/Text/expected/Editing/execCommand-forwardDelete.txt b/Tests/LibWeb/Text/expected/Editing/execCommand-forwardDelete.txt index 6a2750fb0d7..b84dcc67b50 100644 --- a/Tests/LibWeb/Text/expected/Editing/execCommand-forwardDelete.txt +++ b/Tests/LibWeb/Text/expected/Editing/execCommand-forwardDelete.txt @@ -19,3 +19,6 @@ After: a    b --- g --- Before:   b After:  b +--- h --- +Before: foo👩🏼‍❤️‍👨🏻bar +After: foobar diff --git a/Tests/LibWeb/Text/expected/input-delete.txt b/Tests/LibWeb/Text/expected/input-delete.txt new file mode 100644 index 00000000000..d96d5415048 --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-delete.txt @@ -0,0 +1,6 @@ +--- a --- +Before: foo👩🏼‍❤️‍👨🏻bar +After: foobar +--- b --- +Before: foo👩🏼‍❤️‍👨🏻bar +After: foobar diff --git a/Tests/LibWeb/Text/input/Editing/execCommand-delete.html b/Tests/LibWeb/Text/input/Editing/execCommand-delete.html index 91f22511c88..ad456e0573b 100644 --- a/Tests/LibWeb/Text/input/Editing/execCommand-delete.html +++ b/Tests/LibWeb/Text/input/Editing/execCommand-delete.html @@ -1,19 +1,25 @@ -
foobar
+
foobar
+
foo👩🏼‍❤️‍👨🏻bar
diff --git a/Tests/LibWeb/Text/input/Editing/execCommand-forwardDelete.html b/Tests/LibWeb/Text/input/Editing/execCommand-forwardDelete.html index 5a1a98adcdc..6b41c4181f1 100644 --- a/Tests/LibWeb/Text/input/Editing/execCommand-forwardDelete.html +++ b/Tests/LibWeb/Text/input/Editing/execCommand-forwardDelete.html @@ -7,6 +7,7 @@
a    b
a     b
  b
+
foo👩🏼‍❤️‍👨🏻bar
diff --git a/Tests/LibWeb/Text/input/input-delete.html b/Tests/LibWeb/Text/input/input-delete.html new file mode 100644 index 00000000000..50962101da2 --- /dev/null +++ b/Tests/LibWeb/Text/input/input-delete.html @@ -0,0 +1,24 @@ + + + + +