Bug 1723125 - Ignore normal selection when updating composition string r=m_kato

Web apps can modify normal selection even during IME composition and no
browsers stop composition by it.  However, our editor tries to delete
non-collapsed selected range before updating composition.  Therefore,
we need additional state at handling inserting text whether selection
should be deleted or ignored.

Depends on D121371

Differential Revision: https://phabricator.services.mozilla.com/D121372
This commit is contained in:
Masayuki Nakano 2021-08-02 08:23:50 +00:00
parent 99a58ebbc9
commit 6122a660a0
11 changed files with 165 additions and 25 deletions

View File

@ -1785,7 +1785,7 @@ nsresult EditorBase::InsertTextAt(const nsAString& aStringToInsert,
return rv;
}
rv = InsertTextAsSubAction(aStringToInsert);
rv = InsertTextAsSubAction(aStringToInsert, SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
return rv;
@ -3650,6 +3650,7 @@ nsresult EditorBase::OnCompositionChange(
MOZ_ASSERT(
!mPlaceholderBatch,
"UpdateIMEComposition() must be called without place holder batch");
bool wasComposing = mComposition->IsComposing();
TextComposition::CompositionChangeEventHandlingMarker
compositionChangeEventHandlingMarker(mComposition,
&aCompositionChangeEvent);
@ -3667,7 +3668,10 @@ nsresult EditorBase::OnCompositionChange(
if (IsHTMLEditor()) {
nsContentUtils::PlatformToDOMLineBreaks(data);
}
rv = InsertTextAsSubAction(data);
// If we're updating composition, we need to ignore normal selection
// which may be updated by the web content.
rv = InsertTextAsSubAction(data, wasComposing ? SelectionHandling::Ignore
: SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
@ -5000,7 +5004,7 @@ nsresult EditorBase::OnInputText(const nsAString& aStringToInsert) {
AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
ScrollSelectionIntoView::Yes);
rv = InsertTextAsSubAction(aStringToInsert);
rv = InsertTextAsSubAction(aStringToInsert, SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
return EditorBase::ToGenericNSResult(rv);
@ -5129,7 +5133,7 @@ nsresult EditorBase::ReplaceSelectionAsSubAction(const nsAString& aString) {
return rv;
}
nsresult rv = InsertTextAsSubAction(aString);
nsresult rv = InsertTextAsSubAction(aString, SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
return rv;
@ -5963,22 +5967,28 @@ nsresult EditorBase::InsertTextAsAction(const nsAString& aStringToInsert,
}
AutoPlaceholderBatch treatAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
rv = InsertTextAsSubAction(stringToInsert);
rv = InsertTextAsSubAction(stringToInsert, SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult EditorBase::InsertTextAsSubAction(const nsAString& aStringToInsert) {
nsresult EditorBase::InsertTextAsSubAction(
const nsAString& aStringToInsert, SelectionHandling aSelectionHandling) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(mPlaceholderBatch);
MOZ_ASSERT(IsHTMLEditor() ||
aStringToInsert.FindChar(nsCRT::CR) == kNotFound);
MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore, mComposition);
if (NS_WARN_IF(!mInitSucceeded)) {
return NS_ERROR_NOT_INITIALIZED;
}
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
EditSubAction editSubAction = ShouldHandleIMEComposition()
? EditSubAction::eInsertTextComingFromIME
: EditSubAction::eInsertText;
@ -5993,7 +6003,8 @@ nsresult EditorBase::InsertTextAsSubAction(const nsAString& aStringToInsert) {
!ignoredError.Failed(),
"TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
EditActionResult result = HandleInsertText(editSubAction, aStringToInsert);
EditActionResult result =
HandleInsertText(editSubAction, aStringToInsert, aSelectionHandling);
NS_WARNING_ASSERTION(result.Succeeded(),
"EditorBase::HandleInsertText() failed");
return result.Rv();

View File

@ -1664,9 +1664,12 @@ class EditorBase : public nsIEditor,
* should be used for handling it as an edit sub-action.
*
* @param aStringToInsert The string to insert.
* @param aSelectionHandling Specify whether selected content should be
* deleted or ignored.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
InsertTextAsSubAction(const nsAString& aStringToInsert);
enum class SelectionHandling { Ignore, Delete };
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertTextAsSubAction(
const nsAString& aStringToInsert, SelectionHandling aSelectionHandling);
/**
* InsertTextWithTransaction() inserts aStringToInsert to aPointToInsert or
@ -2050,9 +2053,12 @@ class EditorBase : public nsIEditor,
* @param aEditSubAction Must be EditSubAction::eInsertText or
* EditSubAction::eInsertTextComingFromIME.
* @param aInsertionString String to be inserted at selection.
* @param aSelectionHandling Specify whether selected content should be
* deleted or ignored.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual EditActionResult HandleInsertText(
EditSubAction aEditSubAction, const nsAString& aInsertionString) = 0;
EditSubAction aEditSubAction, const nsAString& aInsertionString,
SelectionHandling aSelectionHandling) = 0;
/**
* InsertWithQuotationsAsSubAction() inserts aQuotedText with appending ">"

View File

@ -926,10 +926,13 @@ nsresult HTMLEditor::PrepareInlineStylesForCaret() {
}
EditActionResult HTMLEditor::HandleInsertText(
EditSubAction aEditSubAction, const nsAString& aInsertionString) {
EditSubAction aEditSubAction, const nsAString& aInsertionString,
SelectionHandling aSelectionHandling) {
MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME);
MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore,
aEditSubAction == EditSubAction::eInsertTextComingFromIME);
EditActionResult result = CanHandleHTMLEditSubAction();
if (result.Failed() || result.Canceled()) {
@ -942,7 +945,8 @@ EditActionResult HTMLEditor::HandleInsertText(
// If the selection isn't collapsed, delete it. Don't delete existing inline
// tags, because we're hopefully going to insert text (bug 787432).
if (!SelectionRef().IsCollapsed()) {
if (!SelectionRef().IsCollapsed() &&
aSelectionHandling == SelectionHandling::Delete) {
nsresult rv =
DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
if (NS_FAILED(rv)) {

View File

@ -1064,7 +1064,8 @@ class HTMLEditor final : public EditorBase,
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult PrepareInlineStylesForCaret();
[[nodiscard]] MOZ_CAN_RUN_SCRIPT EditActionResult HandleInsertText(
EditSubAction aEditSubAction, const nsAString& aInsertionString) final;
EditSubAction aEditSubAction, const nsAString& aInsertionString,
SelectionHandling aSelectionHandling) final;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertDroppedDataTransferAsAction(
AutoEditActionDataSetter& aEditActionData,

View File

@ -1840,7 +1840,8 @@ nsresult HTMLEditor::InsertFromTransferable(nsITransferable* aTransferable,
return rv;
}
} else {
nsresult rv = InsertTextAsSubAction(stuffToPaste);
nsresult rv =
InsertTextAsSubAction(stuffToPaste, SelectionHandling::Delete);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
return rv;
@ -2625,7 +2626,7 @@ nsresult HTMLEditor::InsertWithQuotationsAsSubAction(
}
}
rv = InsertTextAsSubAction(quotedStuff);
rv = InsertTextAsSubAction(quotedStuff, SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
return rv;
@ -2740,7 +2741,7 @@ nsresult HTMLEditor::InsertTextWithQuotationsInternal(
"HTMLEditor::InsertAsPlaintextQuotation() failed, "
"but might be ignored");
} else {
rv = InsertTextAsSubAction(curHunk);
rv = InsertTextAsSubAction(curHunk, SelectionHandling::Delete);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed, but might be ignored");
@ -2919,7 +2920,7 @@ nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
return rv;
}
} else {
rv = InsertTextAsSubAction(aQuotedText);
rv = InsertTextAsSubAction(aQuotedText, SelectionHandling::Delete);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
return rv;
@ -3171,7 +3172,8 @@ nsresult HTMLEditor::InsertAsCitedQuotationInternal(
return rv;
}
} else {
rv = InsertTextAsSubAction(aQuotedText); // XXX ignore charset
rv = InsertTextAsSubAction(
aQuotedText, SelectionHandling::Delete); // XXX ignore charset
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}

View File

@ -338,10 +338,13 @@ void TextEditor::HandleNewLinesInStringForSingleLineEditor(
}
EditActionResult TextEditor::HandleInsertText(
EditSubAction aEditSubAction, const nsAString& aInsertionString) {
EditSubAction aEditSubAction, const nsAString& aInsertionString,
SelectionHandling aSelectionHandling) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME);
MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore,
aEditSubAction == EditSubAction::eInsertTextComingFromIME);
UndefineCaretBidiLevel();
@ -384,7 +387,8 @@ EditActionResult TextEditor::HandleInsertText(
}
// if the selection isn't collapsed, delete it.
if (!SelectionRef().IsCollapsed()) {
if (!SelectionRef().IsCollapsed() &&
aSelectionHandling == SelectionHandling::Delete) {
nsresult rv =
DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
if (NS_FAILED(rv)) {

View File

@ -583,7 +583,7 @@ nsresult TextEditor::InsertWithQuotationsAsSubAction(
// also in single line editor)?
MaybeDoAutoPasswordMasking();
rv = InsertTextAsSubAction(quotedStuff);
rv = InsertTextAsSubAction(quotedStuff, SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
return rv;

View File

@ -417,7 +417,8 @@ class TextEditor final : public EditorBase,
void HandleNewLinesInStringForSingleLineEditor(nsString& aString) const;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT EditActionResult HandleInsertText(
EditSubAction aEditSubAction, const nsAString& aInsertionString) final;
EditSubAction aEditSubAction, const nsAString& aInsertionString,
SelectionHandling aSelectionHandling) final;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertDroppedDataTransferAsAction(
AutoEditActionDataSetter& aEditActionData,

View File

@ -78,7 +78,8 @@ nsresult TextEditor::InsertTextFromTransferable(
AutoPlaceholderBatch treatAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
nsresult rv = InsertTextAsSubAction(stuffToPaste);
nsresult rv =
InsertTextAsSubAction(stuffToPaste, SelectionHandling::Delete);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
return rv;

View File

@ -24,8 +24,8 @@
// '!(GetStateBits() & NS_FRAME_FIRST_REFLOW) || (GetParent()->GetStateBits() &
// NS_FRAME_TOO_DEEP_IN_FRAME_TREE)'" in nsTextFrame.cpp.
// Strangely, this doesn't occur with RDP on Windows.
// 12 assertions are: assertions in WSRunScanner::TextFragmentData::GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace()
SimpleTest.expectAssertions(0, 3 + 12);
// 16 assertions are: assertions in WSRunScanner::TextFragmentData::GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace()
SimpleTest.expectAssertions(0, 3 + 16);
SimpleTest.waitForExplicitFinish();
window.openDialog("window_composition_text_querycontent.xhtml", "_blank",
"chrome,width=600,height=600,noopener", window);

View File

@ -5708,6 +5708,115 @@ function runBug722639Test()
}
}
function runCompositionWithSelectionChange() {
function doTest(aEditor, aDescription) {
aEditor.focus();
const isHTMLEditor =
aEditor.nodeName.toLowerCase() != "input" && aEditor.nodeName.toLowerCase() != "textarea";
const win = isHTMLEditor ? windowOfContenteditable : window;
function getValue() {
return isHTMLEditor ? aEditor.innerHTML : aEditor.value;
}
function setSelection(aStart, aLength) {
if (isHTMLEditor) {
win.getSelection().setBaseAndExtent(aEditor.firstChild, aStart, aEditor.firstChild, aStart + aLength);
} else {
aEditor.setSelectionRange(aStart, aStart + aLength);
}
}
if (isHTMLEditor) {
aEditor.innerHTML = "abcxyz";
} else {
aEditor.value = "abcxyz";
}
setSelection("abc".length, 0);
synthesizeCompositionChange({
composition: {
string: "1",
clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
caret: { start: 1, length: 0 },
}
});
is(getValue(), "abc1xyz",
`${aDescription}: First composing character should be inserted middle of the text`);
aEditor.addEventListener("compositionupdate", () => {
setSelection("abc".length, "1".length);
}, {once: true});
synthesizeCompositionChange({
composition: {
string: "12",
clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
caret: { start: 2, length: 0 },
}
});
is(getValue(), "abc12xyz",
`${aDescription}: Only composition string should be updated even if selection range is updated by "compositionupdate" event listener`);
aEditor.addEventListener("compositionupdate", () => {
setSelection("abc1".length, "2d".length);
}, {once: true});
synthesizeCompositionChange({
composition: {
string: "123",
clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
caret: { start: 3, length: 0 },
}
});
is(getValue(), "abc123xyz",
`${aDescription}: Only composition string should be updated even if selection range wider than composition string is updated by "compositionupdate" event listener`);
aEditor.addEventListener("compositionupdate", () => {
setSelection("ab".length, "c123d".length);
}, {once: true});
synthesizeCompositionChange({
composition: {
string: "456",
clauses: [{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
caret: { start: 3, length: 0 },
}
});
is(getValue(), "abc456xyz",
`${aDescription}: Only composition string should be updated even if selection range which covers all over the composition string is updated by "compositionupdate" event listener`);
aEditor.addEventListener("beforeinput", () => {
setSelection("abc456d".length, 0);
}, {once: true});
synthesizeComposition({ type: "compositioncommitasis" });
is(getValue(), "abc456xyz",
`${aDescription}: Only composition string should be updated when committing composition but selection is updated by "beforeinput" event listener`);
if (isHTMLEditor) {
is(win.getSelection().focusNode, aEditor.firstChild,
`${aDescription}: The focus node after composition should be the text node`);
is(win.getSelection().focusOffset, "abc456".length,
`${aDescription}: The focus offset after composition should be end of the composition string`);
is(win.getSelection().anchorNode, aEditor.firstChild,
`${aDescription}: The anchor node after composition should be the text node`);
is(win.getSelection().anchorOffset, "abc456".length,
`${aDescription}: The anchor offset after composition should be end of the composition string`);
} else {
is(aEditor.selectionStart, "abc456".length,
`${aDescription}: The selectionStart after composition should be end of the composition string`);
is(aEditor.selectionEnd, "abc456".length,
`${aDescription}: The selectionEnd after composition should be end of the composition string`);
}
}
doTest(textarea, "runCompositionWithSelectionChange(textarea)");
doTest(input, "runCompositionWithSelectionChange(input)");
doTest(contenteditable, "runCompositionWithSelectionChange(contenteditable)");
}
function runForceCommitTest()
{
let events;
@ -9603,6 +9712,7 @@ async function runTest()
runBug1571375Test();
runBug1675313Test();
runCommitCompositionWithSpaceKey();
runCompositionWithSelectionChange();
runForceCommitTest();
runNestedSettingValue();
runBug811755Test();