mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-26 12:17:52 +00:00
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.
This commit is contained in:
parent
76bab90812
commit
c369f68eff
Notes:
github-actions[bot]
2025-08-14 20:22:53 +00:00
Author: https://github.com/trflynn89
Commit: c369f68eff
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5859
Reviewed-by: https://github.com/gmta ✅
8 changed files with 69 additions and 19 deletions
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibUnicode/Segmenter.h>
|
||||
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
|
||||
#include <LibWeb/DOM/Comment.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
|
@ -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<DOM::Text>(*node) && offset != 0) || block_node_child_is_relevant_type) {
|
||||
|
||||
if (auto const* text_node = as_if<DOM::Text>(*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<DOM::Text>(*node) && offset != node->length()) {
|
||||
if (auto const* text_node = as_if<DOM::Text>(*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.
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
--- a ---
|
||||
Before: foobar
|
||||
After: fobar
|
||||
--- b ---
|
||||
Before: foo👩🏼❤️👨🏻bar
|
||||
After: foobar
|
||||
|
|
|
@ -19,3 +19,6 @@ After: a b
|
|||
--- g ---
|
||||
Before: b
|
||||
After: b
|
||||
--- h ---
|
||||
Before: foo👩🏼❤️👨🏻bar
|
||||
After: foobar
|
||||
|
|
6
Tests/LibWeb/Text/expected/input-delete.txt
Normal file
6
Tests/LibWeb/Text/expected/input-delete.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
--- a ---
|
||||
Before: foo👩🏼❤️👨🏻bar
|
||||
After: foobar
|
||||
--- b ---
|
||||
Before: foo👩🏼❤️👨🏻bar
|
||||
After: foobar
|
|
@ -1,19 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<div contenteditable="true">foobar</div>
|
||||
<div id="a" contenteditable="true">foobar</div>
|
||||
<div id="b" contenteditable="true">foo👩🏼❤️👨🏻bar</div>
|
||||
<script>
|
||||
test(() => {
|
||||
var divElm = document.querySelector('div');
|
||||
println(`Before: ${divElm.textContent}`);
|
||||
const testDelete = function (divId, position) {
|
||||
println(`--- ${divId} ---`);
|
||||
const divElm = document.querySelector(`div#${divId}`);
|
||||
println(`Before: ${divElm.textContent}`);
|
||||
|
||||
// Put cursor after 'foo'
|
||||
var range = document.createRange();
|
||||
range.setStart(divElm.childNodes[0], 3);
|
||||
getSelection().addRange(range);
|
||||
// Place cursor
|
||||
const node = divElm.childNodes[0];
|
||||
getSelection().setBaseAndExtent(node, position, node, position);
|
||||
|
||||
// Press backspace
|
||||
document.execCommand('delete');
|
||||
// Press backspace
|
||||
document.execCommand("delete");
|
||||
|
||||
println(`After: ${divElm.textContent}`);
|
||||
println(`After: ${divElm.textContent}`);
|
||||
};
|
||||
|
||||
testDelete("a", 3);
|
||||
testDelete("b", 15);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<div id="e" contenteditable="true">a b</div>
|
||||
<div id="f" contenteditable="true">a b</div>
|
||||
<div id="g" contenteditable="true"> b</div>
|
||||
<div id="h" contenteditable="true">foo👩🏼❤️👨🏻bar</div>
|
||||
<script>
|
||||
test(() => {
|
||||
const testForwardDelete = function(divId, position) {
|
||||
|
@ -31,5 +32,6 @@
|
|||
testForwardDelete('e', 1);
|
||||
testForwardDelete('f', 1);
|
||||
testForwardDelete('g', 0);
|
||||
testForwardDelete('h', 3);
|
||||
});
|
||||
</script>
|
||||
|
|
24
Tests/LibWeb/Text/input/input-delete.html
Normal file
24
Tests/LibWeb/Text/input/input-delete.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<input id="a" value="foo👩🏼❤️👨🏻bar" />
|
||||
<input id="b" value="foo👩🏼❤️👨🏻bar" />
|
||||
<script>
|
||||
test(() => {
|
||||
const testDelete = function (id, position, key) {
|
||||
println(`--- ${id} ---`);
|
||||
const input = document.querySelector(`input#${id}`);
|
||||
println(`Before: ${input.value}`);
|
||||
|
||||
// Place cursor
|
||||
input.setSelectionRange(position, position);
|
||||
|
||||
// Press backspace
|
||||
internals.sendKey(input, key);
|
||||
|
||||
println(`After: ${input.value}`);
|
||||
};
|
||||
|
||||
testDelete("a", 15, "Backspace");
|
||||
testDelete("b", 3, "Delete");
|
||||
});
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue