[IME] Fire 'compositionend' after 'textInput' event and all other DOM updates

Before CL Blink/Webkit fires 'textInput' after 'compositionend', and 'textInput's default handler will modify DOM. (violates spec)

This CL cleans up code about events during composition, and only fire
'compositionend' event after 'textInput' event and all other DOM updates.

This CL follows option b) in BUG=41945

Potential interop issue:
Safari and Edge still fires 'textInput' after 'compositionend', so it might affect websites that gets IME input using shadow elements.

Alternative Approach (option c in the bug description):
Preserve the order but don't modify DOM in 'textInput' default handler. (matches Edge, but will affect websites with UA check)

PS: There is another issue about Chinese IBus IME on Linux where the order seems non-fixable, will create a separate issue.

SPEC=https://w3c.github.io/uievents/#compositionend
BUG=41945

Review-Url: https://codereview.chromium.org/1998783002
Cr-Commit-Position: refs/heads/master@{#395930}
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/input/ImeTest.java b/content/public/android/javatests/src/org/chromium/content/browser/input/ImeTest.java
index b1f13c0..f2e37c2 100644
--- a/content/public/android/javatests/src/org/chromium/content/browser/input/ImeTest.java
+++ b/content/public/android/javatests/src/org/chromium/content/browser/input/ImeTest.java
@@ -1127,8 +1127,8 @@
         // TODO(changwan): reduce the number of selection changes
         waitForEventLogs("selectionchange,selectionchange,selectionchange,"
                 + "keydown(229),compositionstart(),compositionupdate(a),input,"
-                + "keyup(229),compositionend(a),input,selectionchange,selectionchange,"
-                + "selectionchange,selectionchange,selectionchange");
+                + "keyup(229),compositionupdate(a),input,compositionend(a),selectionchange,"
+                + "selectionchange,selectionchange,selectionchange,selectionchange");
     }
 
     @MediumTest
diff --git a/third_party/WebKit/LayoutTests/fast/events/composition-event-source-device-event-sender-expected.txt b/third_party/WebKit/LayoutTests/fast/events/composition-event-source-device-event-sender-expected.txt
index 78b5054..2197789 100644
--- a/third_party/WebKit/LayoutTests/fast/events/composition-event-source-device-event-sender-expected.txt
+++ b/third_party/WebKit/LayoutTests/fast/events/composition-event-source-device-event-sender-expected.txt
@@ -9,6 +9,9 @@
 compositionupdate
 PASS event.sourceCapabilities is non-null.
 PASS event.sourceCapabilities.firesTouchEvents is false
+compositionupdate
+PASS event.sourceCapabilities is non-null.
+PASS event.sourceCapabilities.firesTouchEvents is false
 compositionend
 PASS event.sourceCapabilities is non-null.
 PASS event.sourceCapabilities.firesTouchEvents is false
diff --git a/third_party/WebKit/LayoutTests/fast/events/ime-composition-events-001-expected.txt b/third_party/WebKit/LayoutTests/fast/events/ime-composition-events-001-expected.txt
index 10f11ef..c10a564 100644
--- a/third_party/WebKit/LayoutTests/fast/events/ime-composition-events-001-expected.txt
+++ b/third_party/WebKit/LayoutTests/fast/events/ime-composition-events-001-expected.txt
@@ -11,10 +11,12 @@
 PASS event.data is "2"
 PASS event.type is "compositionupdate"
 PASS event.data is "3"
-PASS event.type is "compositionend"
+PASS event.type is "compositionupdate"
 PASS event.data is "4"
 PASS event.type is "textInput"
 PASS event.data is "4"
+PASS event.type is "compositionend"
+PASS event.data is "4"
 PASS event.type is "compositionstart"
 PASS event.data is ""
 PASS event.type is "compositionupdate"
@@ -23,6 +25,10 @@
 PASS event.data is "6"
 PASS event.type is "compositionupdate"
 PASS event.data is "7"
+PASS event.type is "compositionupdate"
+PASS event.data is ""
+PASS event.type is "textInput"
+PASS event.data is ""
 PASS event.type is "compositionend"
 PASS event.data is ""
 PASS event.type is "textInput"
@@ -31,19 +37,23 @@
 PASS event.data is ""
 PASS event.type is "compositionupdate"
 PASS event.data is "9"
-PASS event.type is "compositionend"
+PASS event.type is "compositionupdate"
 PASS event.data is "9"
 PASS event.type is "textInput"
 PASS event.data is "9"
+PASS event.type is "compositionend"
+PASS event.data is "9"
 PASS event.type is "compositionstart"
 PASS event.data is "have"
 PASS event.type is "compositionupdate"
 PASS event.data is "lost"
 PASS test.value is "I lost a pen"
-PASS event.type is "compositionend"
+PASS event.type is "compositionupdate"
 PASS event.data is "made"
 PASS event.type is "textInput"
 PASS event.data is "made"
+PASS event.type is "compositionend"
+PASS event.data is "made"
 PASS test.value is "I made a pen"
 PASS successfullyParsed is true
 
diff --git a/third_party/WebKit/Source/core/editing/EditingUtilities.cpp b/third_party/WebKit/Source/core/editing/EditingUtilities.cpp
index cb18baa..7417544f 100644
--- a/third_party/WebKit/Source/core/editing/EditingUtilities.cpp
+++ b/third_party/WebKit/Source/core/editing/EditingUtilities.cpp
@@ -1746,13 +1746,13 @@
     return target->dispatchEvent(beforeInputEvent);
 }
 
-DispatchEventResult dispatchBeforeInputFromComposition(EventTarget* target, InputEvent::InputType inputType, const String& data)
+DispatchEventResult dispatchBeforeInputFromComposition(EventTarget* target, InputEvent::InputType inputType, const String& data, InputEvent::EventCancelable cancelable)
 {
     if (!RuntimeEnabledFeatures::inputEventEnabled())
         return DispatchEventResult::NotCanceled;
     if (!target)
         return DispatchEventResult::NotCanceled;
-    InputEvent* beforeInputEvent = InputEvent::createBeforeInput(inputType, data, InputEvent::EventCancelable::NotCancelable, InputEvent::EventIsComposing::IsComposing);
+    InputEvent* beforeInputEvent = InputEvent::createBeforeInput(inputType, data, cancelable, InputEvent::EventIsComposing::IsComposing);
     return target->dispatchEvent(beforeInputEvent);
 }
 
diff --git a/third_party/WebKit/Source/core/editing/EditingUtilities.h b/third_party/WebKit/Source/core/editing/EditingUtilities.h
index 5cec6b5..65fdc4f 100644
--- a/third_party/WebKit/Source/core/editing/EditingUtilities.h
+++ b/third_party/WebKit/Source/core/editing/EditingUtilities.h
@@ -353,7 +353,7 @@
 
 // Functions dispatch InputEvent
 DispatchEventResult dispatchBeforeInputInsertText(EventTarget*, const String& data);
-DispatchEventResult dispatchBeforeInputFromComposition(EventTarget*, InputEvent::InputType, const String& data);
+DispatchEventResult dispatchBeforeInputFromComposition(EventTarget*, InputEvent::InputType, const String& data, InputEvent::EventCancelable);
 DispatchEventResult dispatchBeforeInputEditorCommand(EventTarget*, InputEvent::InputType, const String& data = "");
 
 } // namespace blink
diff --git a/third_party/WebKit/Source/core/editing/InputMethodController.cpp b/third_party/WebKit/Source/core/editing/InputMethodController.cpp
index b70befe..bce455e 100644
--- a/third_party/WebKit/Source/core/editing/InputMethodController.cpp
+++ b/third_party/WebKit/Source/core/editing/InputMethodController.cpp
@@ -44,6 +44,80 @@
 
 namespace blink {
 
+namespace {
+
+void dispatchCompositionUpdateEvent(LocalFrame& frame, const String& text)
+{
+    Element* target = frame.document()->focusedElement();
+    if (!target)
+        return;
+
+    CompositionEvent* event = CompositionEvent::create(EventTypeNames::compositionupdate, frame.domWindow(), text);
+    target->dispatchEvent(event);
+}
+
+void dispatchCompositionEndEvent(LocalFrame& frame, const String& text)
+{
+    Element* target = frame.document()->focusedElement();
+    if (!target)
+        return;
+
+    CompositionEvent* event = CompositionEvent::create(EventTypeNames::compositionend, frame.domWindow(), text);
+    target->dispatchEvent(event);
+}
+
+// Used to insert/replace text during composition update and confirm composition.
+// Procedure:
+//   1. Fire 'beforeinput' event for (TODO(chongz): deleted composed text) and inserted text
+//   2. Fire 'compositionupdate' event
+//   3. Fire TextEvent and modify DOM
+//   TODO(chongz): 4. Fire 'input' event
+void insertTextDuringCompositionWithEvents(LocalFrame& frame, const String& text, TypingCommand::Options options, TypingCommand::TextCompositionType compositionType)
+{
+    DCHECK(compositionType == TypingCommand::TextCompositionType::TextCompositionUpdate || compositionType == TypingCommand::TextCompositionType::TextCompositionConfirm)
+        << "compositionType should be TextCompositionUpdate or TextCompositionConfirm, but got " << static_cast<int>(compositionType);
+    if (!frame.document())
+        return;
+
+    Element* target = frame.document()->focusedElement();
+    if (!target)
+        return;
+
+    // TODO(chongz): Fire 'beforeinput' for the composed text being replaced/deleted.
+
+    // Only the last confirmed text is cancelable.
+    InputEvent::EventCancelable beforeInputCancelable = (compositionType == TypingCommand::TextCompositionType::TextCompositionUpdate) ? InputEvent::EventCancelable::NotCancelable : InputEvent::EventCancelable::IsCancelable;
+    DispatchEventResult result = dispatchBeforeInputFromComposition(target, InputEvent::InputType::InsertText, text, beforeInputCancelable);
+
+    if (beforeInputCancelable == InputEvent::EventCancelable::IsCancelable && result != DispatchEventResult::NotCanceled)
+        return;
+
+    // 'beforeinput' event handler may destroy document.
+    if (!frame.document())
+        return;
+
+    dispatchCompositionUpdateEvent(frame, text);
+    // 'compositionupdate' event handler may destroy document.
+    if (!frame.document())
+        return;
+
+    switch (compositionType) {
+    case TypingCommand::TextCompositionType::TextCompositionUpdate:
+        TypingCommand::insertText(*frame.document(), text, options, compositionType);
+        break;
+    case TypingCommand::TextCompositionType::TextCompositionConfirm:
+        // TODO(chongz): Use TypingCommand::insertText after TextEvent was removed. (Removed from spec since 2012)
+        // See TextEvent.idl.
+        frame.eventHandler().handleTextInputEvent(text, 0, TextEventInputComposition);
+        break;
+    default:
+        NOTREACHED();
+    }
+    // TODO(chongz): Fire 'input' event.
+}
+
+} // anonymous namespace
+
 InputMethodController::SelectionOffsetsScope::SelectionOffsetsScope(InputMethodController* inputMethodController)
     : m_inputMethodController(inputMethodController)
     , m_offsets(inputMethodController->getSelectionOffsets())
@@ -96,11 +170,6 @@
     m_compositionRange = nullptr;
 }
 
-bool InputMethodController::insertTextForConfirmedComposition(const String& text)
-{
-    return frame().eventHandler().handleTextInputEvent(text, 0, TextEventInputComposition);
-}
-
 void InputMethodController::selectComposition() const
 {
     const EphemeralRange range = compositionEphemeralRange();
@@ -119,19 +188,6 @@
     return confirmComposition(composingText());
 }
 
-static void dispatchCompositionEndEvent(LocalFrame& frame, const String& text)
-{
-    // We should send this event before sending a TextEvent as written in
-    // Section 6.2.2 and 6.2.3 of the DOM Event specification.
-    Element* target = frame.document()->focusedElement();
-    if (!target)
-        return;
-
-    CompositionEvent* event =
-        CompositionEvent::create(EventTypeNames::compositionend, frame.domWindow(), text);
-    target->dispatchEvent(event);
-}
-
 bool InputMethodController::confirmComposition(const String& text, ConfirmCompositionBehavior confirmBehavior)
 {
     if (!hasComposition())
@@ -155,8 +211,6 @@
     if (frame().selection().isNone())
         return false;
 
-    dispatchCompositionEndEvent(frame(), text);
-
     if (!frame().document())
         return false;
 
@@ -168,12 +222,13 @@
 
     clear();
 
-    // TODO(chongz): DOM update should happen before 'compositionend' and along with 'compositionupdate'.
-    // https://crbug.com/575294
-    if (dispatchBeforeInputInsertText(frame().document()->focusedElement(), text) != DispatchEventResult::NotCanceled)
+    insertTextDuringCompositionWithEvents(frame(), text, 0, TypingCommand::TextCompositionType::TextCompositionConfirm);
+    // Event handler might destroy document.
+    if (!frame().document())
         return false;
 
-    insertTextForConfirmedComposition(text);
+    // No DOM update after 'compositionend'.
+    dispatchCompositionEndEvent(frame(), text);
 
     return true;
 }
@@ -213,13 +268,22 @@
     if (frame().selection().isNone())
         return;
 
-    dispatchCompositionEndEvent(frame(), emptyString());
     clear();
-    insertTextForConfirmedComposition(emptyString());
+
+    // TODO(chongz): Update InputType::DeleteComposedCharacter with latest discussion.
+    dispatchBeforeInputFromComposition(frame().document()->focusedElement(), InputEvent::InputType::DeleteComposedCharacter, emptyString(), InputEvent::EventCancelable::NotCancelable);
+    dispatchCompositionUpdateEvent(frame(), emptyString());
+    insertTextDuringCompositionWithEvents(frame(), emptyString(), 0, TypingCommand::TextCompositionType::TextCompositionConfirm);
+    // Event handler might destroy document.
+    if (!frame().document())
+        return;
 
     // An open typing command that disagrees about current selection would cause
     // issues with typing later on.
     TypingCommand::closeTyping(m_frame);
+
+    // No DOM update after 'compositionend'.
+    dispatchCompositionEndEvent(frame(), emptyString());
 }
 
 void InputMethodController::cancelCompositionIfSelectionIsInvalid()
@@ -253,58 +317,52 @@
     if (frame().selection().isNone())
         return;
 
-    if (Element* target = frame().document()->focusedElement()) {
-        // Dispatch an appropriate composition event to the focused node.
-        // We check the composition status and choose an appropriate composition event since this
-        // function is used for three purposes:
-        // 1. Starting a new composition.
-        //    Send a compositionstart and a compositionupdate event when this function creates
-        //    a new composition node, i.e.
-        //    !hasComposition() && !text.isEmpty().
-        //    Sending a compositionupdate event at this time ensures that at least one
-        //    compositionupdate event is dispatched.
-        // 2. Updating the existing composition node.
-        //    Send a compositionupdate event when this function updates the existing composition
-        //    node, i.e. hasComposition() && !text.isEmpty().
-        // 3. Canceling the ongoing composition.
-        //    Send a compositionend event when function deletes the existing composition node, i.e.
-        //    !hasComposition() && test.isEmpty().
-        CompositionEvent* event = nullptr;
-        if (!hasComposition()) {
-            // We should send a compositionstart event only when the given text is not empty because this
-            // function doesn't create a composition node when the text is empty.
-            if (!text.isEmpty()) {
-                target->dispatchEvent(CompositionEvent::create(EventTypeNames::compositionstart, frame().domWindow(), frame().selectedText()));
-                event = CompositionEvent::create(EventTypeNames::compositionupdate, frame().domWindow(), text);
-            }
-        } else {
-            if (!text.isEmpty())
-                event = CompositionEvent::create(EventTypeNames::compositionupdate, frame().domWindow(), text);
-            else
-                event = CompositionEvent::create(EventTypeNames::compositionend, frame().domWindow(), text);
+    Element* target = frame().document()->focusedElement();
+    if (!target)
+        return;
+
+    // Dispatch an appropriate composition event to the focused node.
+    // We check the composition status and choose an appropriate composition event since this
+    // function is used for three purposes:
+    // 1. Starting a new composition.
+    //    Send a compositionstart and a compositionupdate event when this function creates
+    //    a new composition node, i.e.
+    //    !hasComposition() && !text.isEmpty().
+    //    Sending a compositionupdate event at this time ensures that at least one
+    //    compositionupdate event is dispatched.
+    // 2. Updating the existing composition node.
+    //    Send a compositionupdate event when this function updates the existing composition
+    //    node, i.e. hasComposition() && !text.isEmpty().
+    // 3. Canceling the ongoing composition.
+    //    Send a compositionend event when function deletes the existing composition node, i.e.
+    //    !hasComposition() && test.isEmpty().
+    if (text.isEmpty()) {
+        if (hasComposition()) {
+            confirmComposition(emptyString());
+            return;
         }
-        if (event) {
-            // TODO(chongz): Support canceling IME composition.
-            // TODO(chongz): Should fire InsertText or DeleteComposedCharacter based on action.
-            if (event->type() == EventTypeNames::compositionupdate)
-                dispatchBeforeInputFromComposition(target, InputEvent::InputType::InsertText, text);
-            target->dispatchEvent(event);
-        }
+        // It's weird to call |setComposition()| with empty text outside composition, however some IME
+        // (e.g. Japanese IBus-Anthy) did this, so we simply delete selection without sending extra events.
+        TypingCommand::deleteSelection(*frame().document(), TypingCommand::PreventSpellChecking);
+        return;
     }
 
-    // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input
-    // will delete the old composition with an optimized replace operation.
-    if (text.isEmpty()) {
-        DCHECK(frame().document());
-        TypingCommand::deleteSelection(*frame().document(), TypingCommand::PreventSpellChecking);
+    // We should send a 'compositionstart' event only when the given text is not empty because this
+    // function doesn't create a composition node when the text is empty.
+    if (!hasComposition()) {
+        target->dispatchEvent(CompositionEvent::create(EventTypeNames::compositionstart, frame().domWindow(), frame().selectedText()));
+        if (!frame().document())
+            return;
     }
 
+    DCHECK(!text.isEmpty());
+
     clear();
 
-    if (text.isEmpty())
+    insertTextDuringCompositionWithEvents(frame(), text, TypingCommand::SelectInsertedText | TypingCommand::PreventSpellChecking, TypingCommand::TextCompositionUpdate);
+    // Event handlers might destroy document.
+    if (!frame().document())
         return;
-    DCHECK(frame().document());
-    TypingCommand::insertText(*frame().document(), text, TypingCommand::SelectInsertedText | TypingCommand::PreventSpellChecking, TypingCommand::TextCompositionUpdate);
 
     // Find out what node has the composition now.
     Position base = mostForwardCaretPosition(frame().selection().base());
diff --git a/third_party/WebKit/Source/core/editing/InputMethodController.h b/third_party/WebKit/Source/core/editing/InputMethodController.h
index f5ce71d..bf7e152 100644
--- a/third_party/WebKit/Source/core/editing/InputMethodController.h
+++ b/third_party/WebKit/Source/core/editing/InputMethodController.h
@@ -107,7 +107,6 @@
     }
 
     String composingText() const;
-    bool insertTextForConfirmedComposition(const String& text);
     void selectComposition() const;
     bool setSelectionOffsets(const PlainTextRange&);
 };
diff --git a/third_party/WebKit/Source/core/editing/InputMethodControllerTest.cpp b/third_party/WebKit/Source/core/editing/InputMethodControllerTest.cpp
index 541c84e..b176244 100644
--- a/third_party/WebKit/Source/core/editing/InputMethodControllerTest.cpp
+++ b/third_party/WebKit/Source/core/editing/InputMethodControllerTest.cpp
@@ -372,7 +372,8 @@
 
     document().setTitle(emptyString());
     controller().confirmComposition();
-    EXPECT_STREQ("beforeinput.isComposing:false", document().title().utf8().data());
+    // Last 'beforeinput' should also be inside composition scope.
+    EXPECT_STREQ("beforeinput.isComposing:true", document().title().utf8().data());
 }
 
 } // namespace blink