Bug 1758420 - part 1: Let `TextComposition` know where is changed in storing text node r=m_kato

`IMEContentObserver` observes the text node change which contains the current
composition string.  Therefore, it can let `TextComposition` know where is
updated by web apps and adjust offset and length in the text node.

Differential Revision: https://phabricator.services.mozilla.com/D141193
This commit is contained in:
Masayuki Nakano 2022-04-21 03:37:56 +00:00
Родитель c6aaf4f760
Коммит 5c4fed5f9d
6 изменённых файлов: 1178 добавлений и 83 удалений

Просмотреть файл

@ -792,6 +792,15 @@ void IMEContentObserver::CharacterDataChanged(
// node.
}
// Let TextComposition have a change to update composition string range in
// the text node if the change is caused by the web apps.
if (mWidget && !IsEditorHandlingEventForComposition()) {
if (RefPtr<TextComposition> composition =
IMEStateManager::GetTextCompositionFor(mWidget)) {
composition->OnCharacterDataChanged(*aContent->AsText(), aInfo);
}
}
if (!NeedsTextChangeNotification() ||
!nsContentUtils::IsInSameAnonymousTree(mRootContent, aContent)) {
return;

Просмотреть файл

@ -9,6 +9,7 @@
#include "IMEStateManager.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIMutationObserver.h"
#include "nsPresContext.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/EditorBase.h"
@ -87,6 +88,82 @@ void TextComposition::Destroy() {
// this being destroyed for cleaning up the stuff.
}
void TextComposition::OnCharacterDataChanged(
Text& aText, const CharacterDataChangeInfo& aInfo) {
if (mContainerTextNode != &aText ||
mCompositionStartOffsetInTextNode == UINT32_MAX ||
mCompositionLengthInTextNode == UINT32_MAX) {
return;
}
// Ignore changes after composition string.
if (aInfo.mChangeStart >=
mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) {
return;
}
// If the change ends before the composition string, we need only to adjust
// the start offset.
if (aInfo.mChangeEnd <= mCompositionStartOffsetInTextNode) {
MOZ_ASSERT(aInfo.LengthOfRemovedText() <=
mCompositionStartOffsetInTextNode);
mCompositionStartOffsetInTextNode -= aInfo.LengthOfRemovedText();
mCompositionStartOffsetInTextNode += aInfo.mReplaceLength;
return;
}
// If this is caused by a splitting text node, the composition string
// may be split out to the new right node. In the case,
// CompositionTransaction::DoTransaction handles it with warking the
// following text nodes. Therefore, we should NOT shrink the composing
// range for avoind breaking the fix of bug 1310912. Although the handling
// looks buggy so that we need to move the handling into here later.
if (aInfo.mDetails &&
aInfo.mDetails->mType == CharacterDataChangeInfo::Details::eSplit) {
return;
}
// If the change removes/replaces the last character of the composition
// string, we should shrink the composition range before the change start.
// Then, the replace string will be never updated by coming composition
// updates.
if (aInfo.mChangeEnd >=
mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) {
// If deleting the first character of the composition string, collapse IME
// selection temporarily. Updating composition string will insert new
// composition string there.
if (aInfo.mChangeStart <= mCompositionStartOffsetInTextNode) {
mCompositionStartOffsetInTextNode = aInfo.mChangeStart;
mCompositionLengthInTextNode = 0u;
return;
}
// If some characters in the composition still stay, composition range
// should be shrunken.
MOZ_ASSERT(aInfo.mChangeStart > mCompositionStartOffsetInTextNode);
mCompositionLengthInTextNode =
aInfo.mChangeStart - mCompositionStartOffsetInTextNode;
return;
}
// If removed range starts in the composition string, we need only adjust
// the length to make composition range contain the replace string.
if (aInfo.mChangeStart >= mCompositionStartOffsetInTextNode) {
MOZ_ASSERT(aInfo.LengthOfRemovedText() <= mCompositionLengthInTextNode);
mCompositionLengthInTextNode -= aInfo.LengthOfRemovedText();
mCompositionLengthInTextNode += aInfo.mReplaceLength;
return;
}
// If preceding characers of the composition string is also removed, new
// composition start will be there and new composition ends at current
// position.
const uint32_t removedLengthInCompositionString =
aInfo.mChangeEnd - mCompositionStartOffsetInTextNode;
mCompositionStartOffsetInTextNode = aInfo.mChangeStart;
mCompositionLengthInTextNode -= removedLengthInCompositionString;
mCompositionLengthInTextNode += aInfo.mReplaceLength;
}
bool TextComposition::IsValidStateForComposition(nsIWidget* aWidget) const {
return !Destroyed() && aWidget && !aWidget->Destroyed() &&
mPresContext->GetPresShell() &&

Просмотреть файл

@ -21,6 +21,10 @@
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/dom/Text.h"
class nsRange;
struct CharacterDataChangeInfo;
namespace mozilla {
class EditorBase;
@ -274,6 +278,13 @@ class TextComposition final {
// composition in new text node.
}
/**
* OnCharacterDataChanged() is called when IMEContentObserver receives
* character data change notifications.
*/
void OnCharacterDataChanged(Text& aText,
const CharacterDataChangeInfo& aInfo);
private:
// Private destructor, to discourage deletion outside of Release():
~TextComposition() {

Просмотреть файл

@ -125,6 +125,13 @@ NS_IMETHODIMP CompositionTransaction::DoTransaction() {
// If composition string is split to multiple text nodes, we should put
// whole new composition string to the first text node and remove the
// compostion string in other nodes.
// TODO: This should be handled by `TextComposition` because this assumes
// that composition string has never touched by JS. However, it
// would occur if the web app is a corrabolation software which
// multiple users can modify anyware in an editor.
// TODO: And if composition starts from a following text node, the offset
// here is outdated and it will cause inserting composition string
// **before** the proper point from point of view of the users.
uint32_t replaceableLength = textNode->TextLength() - mOffset;
ErrorResult error;
editorBase->DoReplaceText(textNode, mOffset, mReplaceLength,

Просмотреть файл

@ -22,96 +22,217 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1310912
<script class="testbody" type="application/javascript">
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(function() {
let editor = document.querySelector("div[contenteditable]");
const editor = document.querySelector("div[contenteditable]");
editor.focus();
let sel = window.getSelection();
const sel = window.getSelection();
sel.collapse(editor.childNodes[0], editor.textContent.length);
synthesizeCompositionChange({
composition: {
string: "DEF",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE },
],
},
caret: { start: 3, length: 0 },
});
is(editor.textContent, "ABCDEF", "composing text should be set");
(function testInsertEmptyTextNodeWhenCaretIsAtEndOfComposition() {
const description =
"testInsertEmptyTextNodeWhenCaretIsAtEndOfComposition: ";
synthesizeCompositionChange({
composition: {
string: "DEF",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE },
],
},
caret: { start: 3, length: 0 },
});
is(
editor.textContent,
"ABCDEF",
`${description} Composing text "DEF" should be inserted at end of the text node`
);
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
synthesizeCompositionChange({
composition: {
string: "GHI",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
],
},
caret: { start: 0, length: 0 },
});
is(editor.textContent, "ABCGHI", "composing text should be replaced");
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
is(
editor.childNodes[0].data,
"ABCDEF",
`${
description
} First text node should have both preceding text and the composing text`
);
is(
editor.childNodes[1].data,
"",
`${description} Second text node should be empty`
);
})();
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
synthesizeCompositionChange({
composition: {
string: "JKL",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
],
},
caret: { start: 0, length: 0 },
});
is(editor.textContent, "ABCJKL", "composing text should be replaced");
(function testInsertEmptyTextNodeWhenCaretIsAtStartOfComposition() {
const description =
"testInsertEmptyTextNodeWhenCaretIsAtStartOfComposition: ";
synthesizeCompositionChange({
composition: {
string: "GHI",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
],
},
caret: { start: 0, length: 0 },
});
is(
editor.textContent,
"ABCGHI",
`${description} Composing text should be replaced with new one`
);
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
synthesizeCompositionChange({
composition: {
string: "MNO",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
],
},
caret: { start: 1, length: 0 },
});
is(editor.textContent, "ABCMNO", "composing text should be replaced");
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
is(
editor.childNodes[0].data,
"ABC",
`${
description
} First text node should have only the preceding text of the composition`
);
is(
editor.childNodes[1].data,
"",
`${description} Second text node should have be empty`
);
is(
editor.childNodes[2].data,
"GHI",
`${description} Third text node should have only composing text`
);
})();
// Normal selection is the caret, therefore, inserting empty text node
// creates the following DOM tree:
// <div contenteditable>
// |- #text ("ABCM")
// |- #text ("")
// +- #text ("NO")
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
is(editor.childNodes[0].data, "ABCM",
"First text node should only have \"M\" of the composition string");
is(editor.childNodes[1].data, "",
"Second text node should be the inserted empty text node");
is(editor.childNodes[2].data, "NO",
"Third text node should have the remaining composition string");
todo_is(editor.childNodes[3].nodeName, "BR",
"Forth node is empty text node, but I don't where this comes from");
(function testInsertEmptyTextNodeWhenCaretIsAtStartOfCompositionAgain() {
const description =
"testInsertEmptyTextNodeWhenCaretIsAtStartOfCompositionAgain: ";
synthesizeCompositionChange({
composition: {
string: "JKL",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
],
},
caret: { start: 0, length: 0 },
});
is(
editor.textContent,
"ABCJKL",
`${description} Composing text should be replaced`
);
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
is(
editor.childNodes[0].data,
"ABC",
`${
description
} First text node should have only the preceding text of the composition`
);
is(
editor.childNodes[1].data,
"",
`${description} Second text node should have be empty`
);
is(
editor.childNodes[2].data,
"JKL",
`${description} Third text node should have only composing text`
);
})();
(function testInsertEmptyTextNodeWhenCaretIsAtMiddleOfComposition() {
const description =
"testInsertEmptyTextNodeWhenCaretIsAtMiddleOfComposition: ";
synthesizeCompositionChange({
composition: {
string: "MNO",
clauses: [
{ length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE },
],
},
caret: { start: 1, length: 0 },
});
is(
editor.textContent,
"ABCMNO",
`${description} Composing text should be replaced`
);
// Normal selection is the caret, therefore, inserting empty text node
// creates the following DOM tree:
// <div contenteditable>
// |- #text ("ABCM")
// |- #text ("")
// +- #text ("NO")
window.getSelection().getRangeAt(0).insertNode(document.createTextNode(""));
is(
editor.childNodes[0].data,
"ABCM",
`${
description
} First text node should have the preceding text and composing string before the split point`
);
is(
editor.childNodes[1].data,
"",
`${description} Second text node should be empty`
);
is(
editor.childNodes[2].data,
"NO",
`${
description
} Third text node should have the remaining composing string`
);
todo_is(editor.childNodes[3].nodeName, "BR",
"Forth node is empty text node, but I don't where this comes from");
})();
// Then, committing composition makes the commit string into the first
// text node and makes the following text nodes empty.
// XXX I don't know whether the empty text nodes should be removed or not
// at this moment.
synthesizeComposition({ type: "compositioncommitasis" });
is(editor.textContent, "ABCMNO",
"composing text should be committed");
is(editor.childNodes[0].data, "ABCMNO",
"First text node should have the committed string");
(function testCommitComposition() {
const description = "testCommitComposition: ";
synthesizeComposition({ type: "compositioncommitasis" });
is(
editor.textContent,
"ABCMNO",
`${description} Composing text should be committed as-is`
);
is(
editor.childNodes[0].data,
"ABCMNO",
`${description} First text node should have the committed string`
);
})();
synthesizeKey("Z", { accelKey: true });
is(editor.textContent, "ABC",
"text should be undone (commit string should've gone");
is(editor.childNodes[0].data, "ABC",
"First text node should have the committed string after undone");
(function testUndoComposition() {
const description = "testUndoComposition: ";
synthesizeKey("Z", { accelKey: true });
is(
editor.textContent,
"ABC",
`${description} Text should be undone (commit string should've gone)`
);
is(
editor.childNodes[0].data,
"ABC",
`${description} First text node should have all text`
);
})();
synthesizeKey("Z", { accelKey: true, shiftKey: true });
is(editor.textContent, "ABCMNO",
"text should be redone (commit string should've be back");
is(editor.childNodes[0].data, "ABCMNO",
"First text node should have the committed string after redone");
(function testUndoAgain() {
const description = "testUndoAgain: ";
synthesizeKey("Z", { accelKey: true, shiftKey: true });
is(
editor.textContent,
"ABCMNO",
`${description} Text should be redone (commit string should've be back)`
);
is(
editor.childNodes[0].data,
"ABCMNO",
`${description} First text node should have all text`
);
})();
SimpleTest.finish();
});

Просмотреть файл

@ -101,6 +101,46 @@ function isGreaterThan(aLeft, aRight, aMessage)
ok(aLeft > aRight, aMessage + ", got=" + aLeft + ", expected minimum value=" + aRight);
}
/**
* synthesizeSimpleCompositionChange synthesizes a composition which has only
* one clause and put caret end of it.
*
* @param aComposition string or object. If string, it's treated as
* composition string whose attribute is
* COMPOSITION_ATTR_RAW_CLAUSE.
* If object, it must have .string whose type is "string".
* Additionally, .attr can be specified if you'd like to
* use the other attribute instead of
* COMPOSITION_ATTR_RAW_CLAUSE.
*/
function synthesizeSimpleCompositionChange(aComposition, aWindow, aCallback) {
const comp = (() => {
if (typeof aComposition == "string") {
return { string: aComposition, attr: COMPOSITION_ATTR_RAW_CLAUSE };
}
return {
string: aComposition.string,
attr: aComposition.attr === undefined
? COMPOSITION_ATTR_RAW_CLAUSE
: aComposition.attr
};
})();
synthesizeCompositionChange(
{
composition: {
string: comp.string,
clauses: [
{ length: comp.string.length, attr: comp.attr },
],
},
caret: { start: comp.string.length, length: 0 },
},
aWindow,
aCallback
);
}
var div = document.getElementById("div");
var textarea = document.getElementById("textarea");
var panel = document.getElementById("panel");
@ -234,12 +274,24 @@ function checkSelection(aExpectedOffset, aExpectedText, aMessage, aID)
selectedText.text == aExpectedText;
}
function checkIMESelection(aSelectionType, aExpectedFound, aExpectedOffset, aExpectedText, aMessage, aID)
{
function checkIMESelection(
aSelectionType,
aExpectedFound,
aExpectedOffset,
aExpectedText,
aMessage,
aID,
aToDo = {}
) {
if (!aID) {
aID = "";
}
aMessage += " (" + aSelectionType + ")";
let {
notFound = is,
offset = is,
text = is,
} = aToDo;
let selectionType = 0;
switch (aSelectionType) {
case "RawClause":
@ -263,16 +315,26 @@ function checkIMESelection(aSelectionType, aExpectedFound, aExpectedOffset, aExp
": synthesizeQuerySelectedText " + aID)) {
return false;
}
is(selectedText.notFound, !aExpectedFound,
aMessage + ": selection should " + (aExpectedFound ? "" : "not") + " be found " + aID);
notFound(
selectedText.notFound,
!aExpectedFound,
`${aMessage}: selection should ${
aExpectedFound ? "" : "not"
} be found ${aID}`);
if (selectedText.notFound) {
return selectedText.notFound == !aExpectedFound;
}
is(selectedText.offset, aExpectedOffset,
aMessage + ": selection offset is wrong " + aID);
is(selectedText.text, aExpectedText,
aMessage + ": selected text is wrong " + aID);
offset(
selectedText.offset,
aExpectedOffset,
`${aMessage}: selection offset is wrong ${aID}`
);
text(
selectedText.text,
aExpectedText,
`${aMessage}: selected text is wrong ${aID}`
);
return selectedText.offset == aExpectedOffset &&
selectedText.text == aExpectedText;
}
@ -2559,6 +2621,813 @@ function runCompositionEventTest()
formEventHandlerForInput, true);
}
function runCompositionTestWhoseTextNodeModified() {
const selection = windowOfContenteditable.getSelection();
(function testInsertTextBeforeComposition() {
const description =
"runCompositionTestWhoseTextNodeModified: testInsertTextBeforeComposition:";
contenteditable.focus();
contenteditable.innerHTML = "<p>def</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "def".length);
// Insert composition to the end of a text node
synthesizeSimpleCompositionChange("g");
is(
textNode.data,
"defg",
`${description} Composition should be inserted to end of the text node`
);
// Insert a character before the composition string
textNode.insertData(0, "c");
is(
textNode.data,
"cdefg",
`${
description
} Composition should be shifted when a character is inserted before it`
);
checkIMESelection(
"RawClause",
true,
`${kLF}cdef`.length,
"g",
`${
description
} IME selection should be shifted when a character is inserted before it`
);
// Update composition string (appending a character)
synthesizeSimpleCompositionChange("gh");
is(
textNode.data,
"cdefgh",
`${
description
} Composition should be updated correctly after inserted a character before it`
);
checkIMESelection(
"RawClause",
true,
`${kLF}cdef`.length,
"gh",
`${
description
} IME selection should be extended correctly at updating composition after inserted a character before it`
);
// Insert another character before the composition
textNode.insertData(0, "b");
is(
textNode.data,
"bcdefgh",
`${
description
} Composition should be shifted when a character is inserted again before it`
);
checkIMESelection(
"RawClause",
true,
`${kLF}bcdef`.length,
"gh",
`${
description
} IME selection should be shifted when a character is inserted again before it`
);
// Update the composition string again (appending another character)
synthesizeSimpleCompositionChange("ghi");
is(
textNode.data,
"bcdefghi",
`${
description
} Composition should be updated correctly after inserted 2 characters before it`
);
checkIMESelection(
"RawClause",
true,
`${kLF}bcdef`.length,
"ghi",
`${
description
} IME selection should be extended correctly at updating composition after inserted 2 characters before it`
);
// Insert a new character before the composition string
textNode.insertData(0, "a");
is(
textNode.data,
"abcdefghi",
`${
description
} Composition should be shifted when a character is inserted again and again before it`
);
checkIMESelection(
"RawClause",
true,
`${kLF}abcdef`.length,
"ghi",
`${
description
} IME selection should be shifted when a character is inserted again and again before it`
);
// Commit the composition string
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abcdefghi",
`${
description
} Composition should be committed as is`
);
is(
selection.focusOffset,
"abcdefghi".length,
`${description} Selection should be collapsed at end of the commit string`
);
// Undo the commit
synthesizeKey("z", { accelKey: true });
is(
textNode.data,
"abcdef",
`${
description
} Composition should be undone correctly`
);
is(
selection.focusOffset,
"abcdef".length,
`${
description
} Selection should be collapsed at where the composition was after undoing`
);
// Redo the commit
synthesizeKey("z", { accelKey: true, shiftKey: true });
is(
textNode.data,
"abcdefghi",
`${
description
} Composition should be redone correctly`
);
is(
selection.focusOffset,
"abcdefghi".length,
`${
description
} focus offset of Selection should be at end of the commit string after redoing`
);
})();
(function testInsertTextImmediatelyBeforeComposition() {
const description =
"runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyBeforeComposition:";
contenteditable.focus();
contenteditable.innerHTML = "<p>d</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, 0);
// Insert composition at start of the text node
synthesizeSimpleCompositionChange("b");
is(
textNode.data,
"bd",
`${description} Composition should be inserted to start of the text node`
);
// Insert a character before the composition string
textNode.insertData(0, "a");
is(
textNode.data,
"abd",
`${
description
} Composition should be shifted when a character is inserted immediately before it`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"b",
`${
description
} IME selection should be shifted when a character is inserted immediately before it`,
"",
{ offset: todo_is, text: todo_is }
);
// Update the composition string after inserting character immediately before it
synthesizeSimpleCompositionChange("bc");
is(
textNode.data,
"abcd",
`${description} Composition should be updated after the inserted character`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"bc",
`${
description
} IME selection should be set at the composition string after the inserted character`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abcd",
`${
description
} Composition should be committed after the inserted character`
);
is(
selection.focusOffset,
"abc".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
(function testInsertTextImmediatelyAfterComposition() {
const description =
"runCompositionTestWhoseTextNodeModified: testInsertTextImmediatelyAfterComposition:";
contenteditable.focus();
contenteditable.innerHTML = "<p>a</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "a".length);
// Insert composition at end of the text node
synthesizeSimpleCompositionChange("b");
is(
textNode.data,
"ab",
`${description} Composition should be inserted to start of the text node`
);
// Insert a character after the composition string
textNode.insertData("ab".length, "d");
is(
textNode.data,
"abd",
`${
description
} Composition should stay when a character is inserted immediately after it`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"b",
`${
description
} IME selection should stay when a character is inserted immediately after it`
);
// Update the composition string after inserting character immediately after it
synthesizeSimpleCompositionChange("bc");
is(
textNode.data,
"abcd",
`${description} Composition should be updated before the inserted character`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"bc",
`${
description
} IME selection should be set at the composition string before the inserted character`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abcd",
`${
description
} Composition should be committed before the inserted character`
);
is(
selection.focusOffset,
"abc".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
// Inserting/replacing text before the last character of composition string
// should be contained by the composition, i.e., updated by next composition
// update. This is Chrome compatible.
(function testInsertTextMiddleOfComposition() {
const description =
"runCompositionTestWhoseTextNodeModified: testInsertTextMiddleOfComposition:";
contenteditable.focus();
contenteditable.innerHTML = "<p>a</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "a".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("bd");
is(
textNode.data,
"abd",
`${description} Composition should be inserted to end of the text node`
);
// Insert a character before the composition string
textNode.insertData("ab".length, "c");
is(
textNode.data,
"abcd",
`${
description
} Inserted string should inserted into the middle of composition string`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"bcd",
`${
description
} IME selection should be extended when a character is inserted into middle of it`
);
// Update the composition string after inserting character into it
synthesizeSimpleCompositionChange("BD");
is(
textNode.data,
"aBD",
`${
description
} Composition should be replace the range containing the inserted character`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"BD",
`${
description
} IME selection should be set at the updated composition string`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"aBD",
`${
description
} Composition should be committed without the inserted character`
);
is(
selection.focusOffset,
"aBD".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
(function testReplaceFirstCharOfCompositionString() {
const description =
"runCompositionTestWhoseTextNodeModified: testReplaceFirstCharOfCompositionString:";
contenteditable.focus();
contenteditable.innerHTML = "<p>abfg</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "ab".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("cde");
is(
textNode.data,
"abcdefg",
`${description} Composition should be inserted`
);
// Replace the composition string
textNode.replaceData("ab".length, "c".length, "XYZ");
is(
textNode.data,
"abXYZdefg",
`${description} First character of the composition should be replaced`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"XYZde",
`${description} IME selection should contain the replace string`
);
// Update the composition string after replaced
synthesizeSimpleCompositionChange("CDE");
is(
textNode.data,
"abCDEfg",
`${description} Composition should update the replace string too`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"CDE",
`${description} IME selection should update the replace string too`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abCDEfg",
`${description} Composition should be committed`
);
is(
selection.focusOffset,
"abCDE".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
// Although Chrome commits composition if all composition string is removed,
// let's keep composition for making TSF stable...
(function testReplaceAllCompositionString() {
const description =
"runCompositionTestWhoseTextNodeModified: testReplaceAllCompositionString:";
contenteditable.focus();
contenteditable.innerHTML = "<p>abfg</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "ab".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("cde");
is(
textNode.data,
"abcdefg",
`${description} Composition should be inserted to the text node`
);
// Replace the composition string
textNode.replaceData("ab".length, "cde".length, "XYZ");
is(
textNode.data,
"abXYZfg",
`${description} Composition should be replaced`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"",
`${
description
} IME selection should be collapsed before the replace string`
);
// Update the composition string after replaced
synthesizeSimpleCompositionChange("CDE");
is(
textNode.data,
"abCDEXYZfg",
`${description} Composition should be inserted again`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"CDE",
`${description} IME selection should not contain the replace string`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abCDEXYZfg",
`${description} Composition should be committed`
);
is(
selection.focusOffset,
"abCDE".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
(function testReplaceCompositionStringAndSurroundedCharacters() {
const description =
"runCompositionTestWhoseTextNodeModified: testReplaceCompositionStringAndSurroundedCharacters:";
contenteditable.focus();
contenteditable.innerHTML = "<p>abfg</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "ab".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("cde");
is(
textNode.data,
"abcdefg",
`${description} Composition should be inserted to the text node`
);
// Replace the composition string
textNode.replaceData("a".length, "bcdef".length, "XYZ");
is(
textNode.data,
"aXYZg",
`${description} Composition should be replaced`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"",
`${
description
} IME selection should be collapsed before the replace string`
);
// Update the composition string after replaced
synthesizeSimpleCompositionChange("CDE");
is(
textNode.data,
"aCDEXYZg",
`${description} Composition should be inserted again`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"CDE",
`${description} IME selection should not contain the replace string`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"aCDEXYZg",
`${description} Composition should be committed`
);
is(
selection.focusOffset,
"aCDE".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
// If start boundary characters are replaced, the replace string should be
// contained into the composition range. This is Chrome compatible.
(function testReplaceStartBoundaryOfCompositionString() {
const description =
"runCompositionTestWhoseTextNodeModified: testReplaceStartBoundaryOfCompositionString:";
contenteditable.focus();
contenteditable.innerHTML = "<p>abfg</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "ab".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("cde");
is(
textNode.data,
"abcdefg",
`${description} Composition should be inserted to the text node`
);
// Replace some text
textNode.replaceData("a".length, "bc".length, "XYZ");
is(
textNode.data,
"aXYZdefg",
`${
description
} Start of the composition should be replaced`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"XYZde",
`${description} IME selection should contain the replace string`
);
// Update the replace string and remaining composition.
synthesizeSimpleCompositionChange("CDE");
is(
textNode.data,
"aCDEfg",
`${description} Composition should update the replace string too`
);
checkIMESelection(
"RawClause",
true,
`${kLF}a`.length,
"CDE",
`${description} IME selection should contain the replace string`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"aCDEfg",
`${
description
} Composition should be committed`
);
is(
selection.focusOffset,
"aCDE".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
// If start boundary characters are replaced, the replace string should NOT
// be contained in the composition range. This is Chrome compatible.
(function testReplaceEndBoundaryOfCompositionString() {
const description =
"runCompositionTestWhoseTextNodeModified: testReplaceEndBoundaryOfCompositionString:";
contenteditable.focus();
contenteditable.innerHTML = "<p>abfg</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "ab".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("cde");
is(
textNode.data,
"abcdefg",
`${description} Composition should be inserted to the text node`
);
// Replace the composition string
textNode.replaceData("abcd".length, "ef".length, "XYZ");
is(
textNode.data,
"abcdXYZg",
`${
description
} End half of the composition should be replaced`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"cd",
`${
description
} IME selection should be shrunken to the non-replaced part`
);
// Update the composition string after replaced
synthesizeSimpleCompositionChange("CDE");
is(
textNode.data,
"abCDEXYZg",
`${description} Only the remaining composition string should be updated`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"CDE",
`${description} IME selection should NOT include the replace string`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abCDEXYZg",
`${description} Composition should be committed`
);
is(
selection.focusOffset,
"abCDE".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
// If the last character of composition is replaced, i.e., it should NOT be
// treated as a part of composition string. This is Chrome compatible.
(function testReplaceLastCharOfCompositionString() {
const description =
"runCompositionTestWhoseTextNodeModified: testReplaceLastCharOfCompositionString:";
contenteditable.focus();
contenteditable.innerHTML = "<p>abfg</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "ab".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("cde");
is(
textNode.data,
"abcdefg",
`${description} Composition should be inserted`
);
// Replace the composition string
textNode.replaceData("abcd".length, "e".length, "XYZ");
is(
textNode.data,
"abcdXYZfg",
`${description} Last character of the composition should be replaced`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"cd",
`${description} IME selection should be shrunken`
);
// Update the composition string after replaced
synthesizeSimpleCompositionChange("CDE");
is(
textNode.data,
"abCDEXYZfg",
`${description} Composition should NOT update the replace string`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"CDE",
`${description} IME selection should not contain the replace string`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abCDEXYZfg",
`${description} Composition should be committed`
);
is(
selection.focusOffset,
"abCDE".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
(function testReplaceMiddleCharOfCompositionString() {
const description =
"runCompositionTestWhoseTextNodeModified: testReplaceMiddleCharOfCompositionString:";
contenteditable.focus();
contenteditable.innerHTML = "<p>abfg</p>";
const textNode = contenteditable.firstChild.firstChild;
selection.collapse(textNode, "ab".length);
// Insert composition at middle of the text node
synthesizeSimpleCompositionChange("cde");
is(
textNode.data,
"abcdefg",
`${description} Composition should be inserted`
);
// Replace the composition string
textNode.replaceData("abc".length, "d".length, "XYZ");
is(
textNode.data,
"abcXYZefg",
`${
description
} Middle character of the composition should be replaced`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"cXYZe",
`${description} IME selection should be extended by the replace string`
);
// Update the composition string after replaced
synthesizeSimpleCompositionChange("CDE");
is(
textNode.data,
"abCDEfg",
`${description} Composition should update the replace string`
);
checkIMESelection(
"RawClause",
true,
`${kLF}ab`.length,
"CDE",
`${description} IME selection should be shrunken after update`
);
// Commit it
synthesizeComposition({ type: "compositioncommitasis" });
is(
textNode.data,
"abCDEfg",
`${description} Composition should be committed`
);
is(
selection.focusOffset,
"abCDE".length,
`${description} Selection should be collapsed at end of the commit string`
);
})();
}
// eslint-disable-next-line complexity
function runQueryTextRectInContentEditableTest()
{
@ -9801,6 +10670,7 @@ async function runTest()
runCompositionCommitAsIsTest();
runCompositionCommitTest();
runCompositionEventTest();
runCompositionTestWhoseTextNodeModified();
runQueryTextRectInContentEditableTest();
runCharAtPointTest(textarea, "textarea in the document");
runCharAtPointAtOutsideTest();