mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 14:52:16 +00:00
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:
parent
99a58ebbc9
commit
6122a660a0
@ -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();
|
||||
|
@ -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 ">"
|
||||
|
@ -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)) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user