diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp index ee56e7d95bbc..6ddd173c558e 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp @@ -2003,6 +2003,25 @@ nsDOMWindowUtils::GetIMEStatus(uint32_t* aState) { return NS_OK; } +NS_IMETHODIMP +nsDOMWindowUtils::GetInputContextOrigin(uint32_t* aOrigin) { + NS_ENSURE_ARG_POINTER(aOrigin); + + nsCOMPtr widget = GetWidget(); + if (!widget) { + return NS_ERROR_FAILURE; + } + + InputContext context = widget->GetInputContext(); + static_assert(InputContext::Origin::ORIGIN_MAIN == INPUT_CONTEXT_ORIGIN_MAIN); + static_assert(InputContext::Origin::ORIGIN_CONTENT == + INPUT_CONTEXT_ORIGIN_CONTENT); + MOZ_ASSERT(context.mOrigin == InputContext::Origin::ORIGIN_MAIN || + context.mOrigin == InputContext::Origin::ORIGIN_CONTENT); + *aOrigin = static_cast(context.mOrigin); + return NS_OK; +} + NS_IMETHODIMP nsDOMWindowUtils::GetFocusedInputType(nsAString& aType) { nsCOMPtr widget = GetWidget(); @@ -2337,7 +2356,8 @@ nsDOMWindowUtils::SendSelectionSetEvent(uint32_t aOffset, uint32_t aLength, NS_IMETHODIMP nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType, - nsITransferable* aTransferable) { + nsITransferable* aTransferable, + const nsAString& aString) { // get the widget to send the event to nsCOMPtr widget = GetWidget(); if (!widget) return NS_ERROR_FAILURE; @@ -2355,6 +2375,8 @@ nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType, msg = eContentCommandUndo; } else if (aType.EqualsLiteral("redo")) { msg = eContentCommandRedo; + } else if (aType.EqualsLiteral("insertText")) { + msg = eContentCommandInsertText; } else if (aType.EqualsLiteral("pasteTransferable")) { msg = eContentCommandPasteTransferable; } else { @@ -2362,6 +2384,9 @@ nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType, } WidgetContentCommandEvent event(true, msg, widget); + if (msg == eContentCommandInsertText) { + event.mString.emplace(aString); + } if (msg == eContentCommandPasteTransferable) { event.mTransferable = aTransferable; } diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp index b626bbfce35d..8f4ae2c032d0 100644 --- a/dom/events/EventStateManager.cpp +++ b/dom/events/EventStateManager.cpp @@ -875,6 +875,9 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext, case eContentCommandLookUpDictionary: DoContentCommandEvent(aEvent->AsContentCommandEvent()); break; + case eContentCommandInsertText: + DoContentCommandInsertTextEvent(aEvent->AsContentCommandEvent()); + break; case eContentCommandScroll: DoContentCommandScrollEvent(aEvent->AsContentCommandEvent()); break; @@ -5923,6 +5926,43 @@ nsresult EventStateManager::DoContentCommandEvent( return NS_OK; } +nsresult EventStateManager::DoContentCommandInsertTextEvent( + WidgetContentCommandEvent* aEvent) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aEvent->mMessage == eContentCommandInsertText); + MOZ_DIAGNOSTIC_ASSERT(aEvent->mString.isSome()); + MOZ_DIAGNOSTIC_ASSERT(!aEvent->mString.ref().IsEmpty()); + + aEvent->mIsEnabled = false; + aEvent->mSucceeded = false; + + NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); + + if (XRE_IsParentProcess()) { + // Handle it in focused content process if there is. + if (BrowserParent* remote = BrowserParent::GetFocused()) { + remote->SendInsertText(aEvent->mString.ref()); + aEvent->mIsEnabled = true; // XXX it can be a lie... + aEvent->mSucceeded = true; + return NS_OK; + } + } + + // If there is no active editor in this process, we should treat the command + // is disabled. + RefPtr activeEditor = + nsContentUtils::GetActiveEditor(mPresContext); + if (!activeEditor) { + aEvent->mSucceeded = true; + return NS_OK; + } + + nsresult rv = activeEditor->InsertTextAsAction(aEvent->mString.ref()); + aEvent->mIsEnabled = rv != NS_SUCCESS_DOM_NO_OPERATION; + aEvent->mSucceeded = NS_SUCCEEDED(rv); + return NS_OK; +} + nsresult EventStateManager::DoContentCommandScrollEvent( WidgetContentCommandEvent* aEvent) { NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); diff --git a/dom/events/EventStateManager.h b/dom/events/EventStateManager.h index 1c91e2291f5e..73e8ca47f13d 100644 --- a/dom/events/EventStateManager.h +++ b/dom/events/EventStateManager.h @@ -1039,6 +1039,8 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver { MOZ_CAN_RUN_SCRIPT nsresult DoContentCommandEvent(WidgetContentCommandEvent* aEvent); + MOZ_CAN_RUN_SCRIPT + nsresult DoContentCommandInsertTextEvent(WidgetContentCommandEvent* aEvent); nsresult DoContentCommandScrollEvent(WidgetContentCommandEvent* aEvent); dom::BrowserParent* GetCrossProcessTarget(); diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl index ced63ea06c2c..0cbecb503546 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl @@ -1089,6 +1089,14 @@ interface nsIDOMWindowUtils : nsISupports { */ readonly attribute unsigned long IMEStatus; + /** + * Get whether current input context (including IME status) in the widget + * is set by content or not. + */ + const unsigned long INPUT_CONTEXT_ORIGIN_MAIN = 0; + const unsigned long INPUT_CONTEXT_ORIGIN_CONTENT = 1; + readonly attribute unsigned long inputContextOrigin; + /** * Get the number of screen pixels per CSS pixel. * @@ -1156,12 +1164,16 @@ interface nsIDOMWindowUtils : nsISupports { * Will throw a DOM security error if called without chrome privileges. * * @param aType Type of command content event to send. Can be one of "cut", - * "copy", "paste", "delete", "undo", "redo", or "pasteTransferable". + * "copy", "paste", "delete", "undo", "redo", "insertText" or + * "pasteTransferable". * @param aTransferable an instance of nsITransferable when aType is * "pasteTransferable" + * @param aString The string to be inserted into focused editor when aType is + * "insertText" */ void sendContentCommandEvent(in AString aType, - [optional] in nsITransferable aTransferable); + [optional] in nsITransferable aTransferable, + [optional] in AString aString); /** * If sendQueryContentEvent()'s aAdditionalFlags argument is diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp index b61f0e27a2b9..07f578c16517 100644 --- a/dom/ipc/BrowserChild.cpp +++ b/dom/ipc/BrowserChild.cpp @@ -2105,6 +2105,21 @@ mozilla::ipc::IPCResult BrowserChild::RecvNormalPrioritySelectionEvent( return RecvSelectionEvent(aEvent); } +mozilla::ipc::IPCResult BrowserChild::RecvInsertText( + const nsString& aStringToInsert) { + // Use normal event path to reach focused document. + WidgetContentCommandEvent localEvent(true, eContentCommandInsertText, + mPuppetWidget); + localEvent.mString = Some(aStringToInsert); + DispatchWidgetEventViaAPZ(localEvent); + return IPC_OK(); +} + +mozilla::ipc::IPCResult BrowserChild::RecvNormalPriorityInsertText( + const nsString& aStringToInsert) { + return RecvInsertText(aStringToInsert); +} + mozilla::ipc::IPCResult BrowserChild::RecvPasteTransferable( const IPCDataTransfer& aDataTransfer, const bool& aIsPrivateData, nsIPrincipal* aRequestingPrincipal, diff --git a/dom/ipc/BrowserChild.h b/dom/ipc/BrowserChild.h index c760518c0744..e2ab06181615 100644 --- a/dom/ipc/BrowserChild.h +++ b/dom/ipc/BrowserChild.h @@ -417,6 +417,11 @@ class BrowserChild final : public nsMessageManagerScriptExecutor, mozilla::ipc::IPCResult RecvSetIsUnderHiddenEmbedderElement( const bool& aIsUnderHiddenEmbedderElement); + mozilla::ipc::IPCResult RecvInsertText(const nsString& aStringToInsert); + + mozilla::ipc::IPCResult RecvNormalPriorityInsertText( + const nsString& aStringToInsert); + MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult RecvPasteTransferable( const IPCDataTransfer& aDataTransfer, const bool& aIsPrivateData, diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp index d9236c2824f0..32e7b8bed4e0 100644 --- a/dom/ipc/BrowserParent.cpp +++ b/dom/ipc/BrowserParent.cpp @@ -3072,6 +3072,15 @@ bool BrowserParent::SendSelectionEvent(WidgetSelectionEvent& aEvent) { return true; } +bool BrowserParent::SendInsertText(const nsString& aStringToInsert) { + if (mIsDestroyed) { + return false; + } + return Manager()->IsInputPriorityEventEnabled() + ? PBrowserParent::SendInsertText(aStringToInsert) + : PBrowserParent::SendNormalPriorityInsertText(aStringToInsert); +} + bool BrowserParent::SendPasteTransferable( const IPCDataTransfer& aDataTransfer, const bool& aIsPrivateData, nsIPrincipal* aRequestingPrincipal, diff --git a/dom/ipc/BrowserParent.h b/dom/ipc/BrowserParent.h index 711f78f9e287..5cb3da24b65b 100644 --- a/dom/ipc/BrowserParent.h +++ b/dom/ipc/BrowserParent.h @@ -608,6 +608,8 @@ class BrowserParent final : public PBrowserParent, bool HandleQueryContentEvent(mozilla::WidgetQueryContentEvent& aEvent); + bool SendInsertText(const nsString& aStringToInsert); + bool SendPasteTransferable(const IPCDataTransfer& aDataTransfer, const bool& aIsPrivateData, nsIPrincipal* aRequestingPrincipal, diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl index 62dc50531078..6e9954d9dc81 100644 --- a/dom/ipc/PBrowser.ipdl +++ b/dom/ipc/PBrowser.ipdl @@ -869,6 +869,12 @@ child: [Priority=input] async SelectionEvent(WidgetSelectionEvent event); async NormalPrioritySelectionEvent(WidgetSelectionEvent event); + /** + * Dispatch eContentCommandInsertText event in the remote process. + */ + [Priority=input] async InsertText(nsString aStringToInsert); + async NormalPriorityInsertText(nsString aStringToInsert); + /** * Call PasteTransferable via a controller on the content process * to handle the command content event, "pasteTransferable". diff --git a/editor/libeditor/tests/browser.ini b/editor/libeditor/tests/browser.ini index b2ec0aa52184..0d50945e8974 100644 --- a/editor/libeditor/tests/browser.ini +++ b/editor/libeditor/tests/browser.ini @@ -1,7 +1,6 @@ [browser_bug527935.js] skip-if = toolkit == 'android' -prefs = - editor.white_space_normalization.blink_compatible=true support-files = bug527935.html bug527935_2.html +[browser_content_command_insert_text.js] diff --git a/editor/libeditor/tests/browser_content_command_insert_text.js b/editor/libeditor/tests/browser_content_command_insert_text.js new file mode 100644 index 000000000000..4d172287e0d8 --- /dev/null +++ b/editor/libeditor/tests/browser_content_command_insert_text.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CustomizableUITestUtils } = ChromeUtils.import( + "resource://testing-common/CustomizableUITestUtils.jsm" +); +const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" +); +const gCUITestUtils = new CustomizableUITestUtils(window); +const gDOMWindowUtils = EventUtils._getDOMWindowUtils(window); + +add_task(async function test_setup() { + await gCUITestUtils.addSearchBar(); + registerCleanupFunction(() => { + gCUITestUtils.removeSearchBar(); + }); +}); + +function promiseResettingSearchBarAndFocus() { + const waitForFocusInSearchBar = BrowserTestUtils.waitForEvent( + BrowserSearch.searchBar.textbox, + "focus" + ); + BrowserSearch.searchBar.textbox.focus(); + BrowserSearch.searchBar.textbox.value = ""; + return Promise.all([ + waitForFocusInSearchBar, + TestUtils.waitForCondition( + () => + gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED && + gDOMWindowUtils.inputContextOrigin === + Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_MAIN + ), + ]); +} + +function promiseIMEStateEnabledByRemote() { + return TestUtils.waitForCondition( + () => + gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED && + gDOMWindowUtils.inputContextOrigin === + Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_CONTENT + ); +} + +function promiseContentTick(browser) { + return SpecialPowers.spawn(browser, [], async () => { + await new Promise(r => { + content.requestAnimationFrame(() => { + content.requestAnimationFrame(r); + }); + }); + }); +} + +add_task(async function test_text_editor_in_chrome() { + await promiseResettingSearchBarAndFocus(); + + let events = []; + function logEvent(event) { + events.push(event); + } + BrowserSearch.searchBar.textbox.addEventListener("beforeinput", logEvent); + gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ"); + + is( + BrowserSearch.searchBar.textbox.value, + "XYZ", + "The string should be inserted into the focused search bar" + ); + is( + events.length, + 1, + "One beforeinput event should be fired in the searchbar" + ); + is(events[0]?.inputType, "insertText", 'inputType should be "insertText"'); + is(events[0]?.data, "XYZ", 'inputType should be "XYZ"'); + is(events[0]?.cancelable, true, "beforeinput event should be cancelable"); + BrowserSearch.searchBar.textbox.removeEventListener("beforeinput", logEvent); + + BrowserSearch.searchBar.textbox.blur(); +}); + +add_task(async function test_text_editor_in_content() { + for (const test of [ + { + tag: "input", + target: "input", + nonTarget: "input + input", + page: 'data:text/html,', + }, + { + tag: "textarea", + target: "textarea", + nonTarget: "textarea + textarea", + page: "data:text/html,", + }, + ]) { + // Once, move focus to chrome's searchbar. + await promiseResettingSearchBarAndFocus(); + + await BrowserTestUtils.withNewTab(test.page, async browser => { + await SpecialPowers.spawn(browser, [test], async function(aTest) { + content.window.focus(); + await ContentTaskUtils.waitForCondition(() => + content.document.hasFocus() + ); + const target = content.document.querySelector(aTest.target); + target.focus(); + target.selectionStart = target.selectionEnd = 2; + content.document.documentElement.scrollTop; // Flush pending things + }); + + await promiseIMEStateEnabledByRemote(); + const waitForBeforeInputEvent = SpecialPowers.spawn( + browser, + [test], + async function(aTest) { + await new Promise(resolve => { + content.document.querySelector(aTest.target).addEventListener( + "beforeinput", + event => { + is( + event.inputType, + "insertText", + `The inputType of beforeinput event fired on <${aTest.target}> should be "insertText"` + ); + is( + event.data, + "XYZ", + `The data of beforeinput event fired on <${aTest.target}> should be "XYZ"` + ); + is( + event.cancelable, + true, + `The beforeinput event fired on <${aTest.target}> should be cancelable` + ); + resolve(); + }, + { once: true } + ); + }); + } + ); + const waitForInputEvent = BrowserTestUtils.waitForContentEvent( + browser, + "input" + ); + await promiseContentTick(browser); // Ensure "input" event listener in the remote process + gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ"); + await waitForBeforeInputEvent; + await waitForInputEvent; + + await SpecialPowers.spawn(browser, [test], async function(aTest) { + is( + content.document.querySelector(aTest.target).value, + "abXYZc", + `The string should be inserted into the focused <${aTest.target}> element` + ); + is( + content.document.querySelector(aTest.nonTarget).value, + "def", + `The string should not be inserted into the non-focused <${aTest.nonTarget}> element` + ); + }); + }); + + is( + BrowserSearch.searchBar.textbox.value, + "", + "The string should not be inserted into the previously focused search bar" + ); + } +}); + +add_task(async function test_html_editor_in_content() { + for (const test of [ + { + mode: "contenteditable", + target: "div", + page: "data:text/html,
abc
", + }, + { + mode: "designMode", + target: "div", + page: "data:text/html,
abc
", + }, + ]) { + // Once, move focus to chrome's searchbar. + await promiseResettingSearchBarAndFocus(); + + await BrowserTestUtils.withNewTab(test.page, async browser => { + await SpecialPowers.spawn(browser, [test], async function(aTest) { + content.window.focus(); + await ContentTaskUtils.waitForCondition(() => + content.document.hasFocus() + ); + const target = content.document.querySelector(aTest.target); + if (aTest.mode == "designMode") { + content.document.designMode = "on"; + content.window.focus(); + } else { + target.focus(); + } + content.window.getSelection().collapse(target.firstChild, 2); + content.document.documentElement.scrollTop; // Flush pending things + }); + + await promiseIMEStateEnabledByRemote(); + const waitForBeforeInputEvent = SpecialPowers.spawn( + browser, + [test], + async function(aTest) { + await new Promise(resolve => { + const eventTarget = + aTest.mode === "designMode" + ? content.document + : content.document.querySelector(aTest.target); + eventTarget.addEventListener( + "beforeinput", + event => { + is( + event.inputType, + "insertText", + `The inputType of beforeinput event fired on ${aTest.mode} editor should be "insertText"` + ); + is( + event.data, + "XYZ", + `The data of beforeinput event fired on ${aTest.mode} editor should be "XYZ"` + ); + is( + event.cancelable, + true, + `The beforeinput event fired on ${aTest.mode} editor should be cancelable` + ); + resolve(); + }, + { once: true } + ); + }); + } + ); + const waitForInputEvent = BrowserTestUtils.waitForContentEvent( + browser, + "input" + ); + await promiseContentTick(browser); // Ensure "input" event listener in the remote process + gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ"); + await waitForBeforeInputEvent; + await waitForInputEvent; + + await SpecialPowers.spawn(browser, [test], async function(aTest) { + is( + content.document.querySelector(aTest.target).innerHTML, + "abXYZc", + `The string should be inserted into the focused ${aTest.mode} editor` + ); + }); + }); + + is( + BrowserSearch.searchBar.textbox.value, + "", + "The string should not be inserted into the previously focused search bar" + ); + } +}); diff --git a/widget/EventMessageList.h b/widget/EventMessageList.h index 35f2075f83d3..20b7ee4cc2f2 100644 --- a/widget/EventMessageList.h +++ b/widget/EventMessageList.h @@ -327,6 +327,9 @@ NS_EVENT_MESSAGE(eContentCommandPaste) NS_EVENT_MESSAGE(eContentCommandDelete) NS_EVENT_MESSAGE(eContentCommandUndo) NS_EVENT_MESSAGE(eContentCommandRedo) +// eContentCommandInsertText tries to insert text with replacing selection +// in focused editor. +NS_EVENT_MESSAGE(eContentCommandInsertText) NS_EVENT_MESSAGE(eContentCommandPasteTransferable) NS_EVENT_MESSAGE(eContentCommandLookUpDictionary) // eContentCommandScroll scrolls the nearest scrollable element to the diff --git a/widget/MiscEvents.h b/widget/MiscEvents.h index 38c270756e11..ea82c2c8d752 100644 --- a/widget/MiscEvents.h +++ b/widget/MiscEvents.h @@ -9,10 +9,12 @@ #include #include "mozilla/BasicEvents.h" +#include "mozilla/Maybe.h" #include "nsCOMPtr.h" #include "nsAtom.h" #include "nsGkAtoms.h" #include "nsITransferable.h" +#include "nsString.h" namespace mozilla { @@ -47,6 +49,9 @@ class WidgetContentCommandEvent : public WidgetGUIEvent { return nullptr; } + // eContentCommandInsertText + mozilla::Maybe mString; // [in] + // eContentCommandPasteTransferable nsCOMPtr mTransferable; // [in] @@ -72,6 +77,7 @@ class WidgetContentCommandEvent : public WidgetGUIEvent { bool aCopyTargets) { AssignGUIEventData(aEvent, aCopyTargets); + mString = aEvent.mString; mScroll = aEvent.mScroll; mOnlyEnabledCheck = aEvent.mOnlyEnabledCheck; mSucceeded = aEvent.mSucceeded;