commit af6ad4e82c09ec30ed88a4fe61b81f60bc1636f3
parent 5b8f66f51002a6254b998a9af11d2ee14ac80852
Author: Qiu Chaofan <qcf@ecnelises.com>
Date: Fri, 12 Dec 2025 01:56:38 +0000
Bug 1728746 - consider grapheme cluster when deleting backwards r=masayuki
Differential Revision: https://phabricator.services.mozilla.com/D272511
Diffstat:
3 files changed, 109 insertions(+), 8 deletions(-)
diff --git a/editor/libeditor/AutoClonedRangeArray.cpp b/editor/libeditor/AutoClonedRangeArray.cpp
@@ -1459,9 +1459,10 @@ AutoClonedSelectionRangeArray::ExtendAnchorFocusRangeFor(
// Different from the `eNext` case, we look for character boundary.
// I'm not sure whether this inconsistency between "Delete" and
// "Backspace" is intentional or not.
- result = nsFrameSelection::CreateRangeExtendedToPreviousCharacterBoundary<
- nsRange>(*presShell, limitersAndCaretData, anchorFocusRange,
- rangeDirection);
+ result = nsFrameSelection::
+ CreateRangeExtendedToPreviousGraphemeClusterBoundary<nsRange>(
+ *presShell, limitersAndCaretData, anchorFocusRange,
+ rangeDirection);
if (NS_WARN_IF(aEditorBase.Destroyed())) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
diff --git a/layout/generic/nsFrameSelection.h b/layout/generic/nsFrameSelection.h
@@ -681,9 +681,9 @@ class nsFrameSelection final {
}
/**
- * CreateRangeExtendedToPreviousCharacterBoundary() returns range which is
- * extended from normal selection range to start of previous character
- * boundary.
+ * CreateRangeExtendedToPreviousGraphemeClusterBoundary() returns range
+ * which is extended from normal selection range to start of previous
+ * grapheme cluster boundary.
*
* @param aLimitersAndCaretData The data of limiters and additional
* caret data.
@@ -695,13 +695,13 @@ class nsFrameSelection final {
*/
template <typename RangeType>
MOZ_CAN_RUN_SCRIPT static mozilla::Result<RefPtr<RangeType>, nsresult>
- CreateRangeExtendedToPreviousCharacterBoundary(
+ CreateRangeExtendedToPreviousGraphemeClusterBoundary(
mozilla::PresShell& aPresShell,
const mozilla::LimitersAndCaretData& aLimitersAndCaretData,
const mozilla::dom::AbstractRange& aRange, nsDirection aRangeDirection) {
return CreateRangeExtendedToSomewhere<RangeType>(
aPresShell, aLimitersAndCaretData, aRange, aRangeDirection,
- eDirPrevious, eSelectCharacter, eLogical);
+ eDirPrevious, eSelectCluster, eLogical);
}
/**
diff --git a/testing/web-platform/mozilla/tests/editor/delete-backwards-grapheme-cluster.html b/testing/web-platform/mozilla/tests/editor/delete-backwards-grapheme-cluster.html
@@ -0,0 +1,100 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<title>Deleting grapheme cluster</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../editing/include/editor-test-utils.js"></script>
+</head>
+<body>
+<div contenteditable></div>
+<script>
+"use strict";
+
+const editingHost = document.querySelector("div[contenteditable]");
+const utils = new EditorTestUtils(editingHost);
+
+const flag = "\u{1F1E8}\u{1F1F3}";
+const halfWidthKatakana = "\uFF76\uFF9E";
+const rainbowFlag = "\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}";
+const family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
+
+const tests = [
+ {
+ name: "Backwards delete grapheme cluster",
+ initialInnerHTML: `a${flag}[]b`,
+ command: "delete",
+ expected: "ab"
+ },
+ {
+ name: "Forwards delete grapheme cluster",
+ initialInnerHTML: `a[]${flag}b`,
+ command: "forwarddelete",
+ expected: "ab"
+ },
+ {
+ name: "Backwards delete half-width katakana",
+ initialInnerHTML: `${halfWidthKatakana}[]`,
+ command: "delete",
+ expected: "\uFF76"
+ },
+ {
+ name: "Forwards delete half-width katakana",
+ initialInnerHTML: `[]${halfWidthKatakana}`,
+ command: "forwarddelete",
+ expected: ["", "<br>"]
+ },
+ {
+ name: "Backwards delete rainbow flag",
+ initialInnerHTML: `${rainbowFlag}[]`,
+ command: "delete",
+ expected: ["", "<br>"]
+ },
+ {
+ name: "Forwards delete rainbow flag",
+ initialInnerHTML: `[]${rainbowFlag}`,
+ command: "forwarddelete",
+ expected: ["", "<br>"]
+ },
+ {
+ name: "Backwards delete rainbow flag around text",
+ initialInnerHTML: `start${rainbowFlag}[]end`,
+ command: "delete",
+ expected: "startend"
+ },
+ {
+ name: "Forwards delete rainbow flag around text",
+ initialInnerHTML: `start[]${rainbowFlag}end`,
+ command: "forwarddelete",
+ expected: "startend"
+ },
+ {
+ name: "Backwards delete family emoji",
+ initialInnerHTML: `${family}[]`,
+ command: "delete",
+ expected: ["", "<br>"]
+ },
+ {
+ name: "Forwards delete family emoji",
+ initialInnerHTML: `[]${family}`,
+ command: "forwarddelete",
+ expected: ["", "<br>"]
+ }
+];
+
+for (const t of tests) {
+ test(() => {
+ utils.setupEditingHost(t.initialInnerHTML);
+ document.execCommand(t.command);
+ if (Array.isArray(t.expected)) {
+ assert_in_array(editingHost.innerHTML, t.expected);
+ } else {
+ assert_equals(editingHost.innerHTML, t.expected);
+ }
+ }, t.name);
+}
+</script>
+</body>
+</html>