Bug 1607131 - Make beforeinput event for MozEditableElement.setUserInput() not cancelable by default r=smaug

Blink and WebKit do not fire `beforeinput` event when user uses build-in
password manager and autocomplete.  But the `inputType` value for this case,
`"insertReplacementText"` is defined as cancelable in the spec, and it's
actually cancelable when it's fired for correcting a word with built-in
spellchecker of them.

For making only our users' autocomplete and password manager not blocked by
web apps, we should make them not cancelable by default, but I think that we
should keep dispatching such non-cancelable `beforeinput` for conforming to
the standard unless we'd get a web-compat report for this.

Differential Revision: https://phabricator.services.mozilla.com/D93206
This commit is contained in:
Masayuki Nakano 2020-10-20 00:13:43 +00:00
parent 10526405d7
commit 04027a5656
21 changed files with 230 additions and 66 deletions

View File

@ -73,8 +73,8 @@ async function confirmClear(selector) {
beforeInputFired = true;
ok(event instanceof InputEvent,
'"beforeinput" event should be dispatched with InputEvent interface');
is(event.cancelable, true,
'"beforeinput" event should be cancelable');
is(event.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
`"beforeinput" event should be cancelable unless it's disabled by the pref`);
is(event.bubbles, true,
'"beforeinput" event should always bubble');
is(event.inputType, "insertReplacementText",

View File

@ -173,8 +173,10 @@ async function triggerAutofillAndCheckProfile(profile) {
);
is(
event.cancelable,
true,
`"beforeinput" event should be cancelable on ${element.tagName}`
SpecialPowers.getBoolPref(
"dom.input_event.allow_to_cancel_set_user_input"
),
`"beforeinput" event should be cancelable on ${element.tagName} unless it's suppressed by the pref`
);
is(
event.bubbles,

View File

@ -72,8 +72,8 @@ function checkElementFilled(element, expectedvalue) {
'dataTransfer value of "beforeinput" event should be null');
is(event.getTargetRanges().length, 0,
'getTargetRanges() of "beforeinput" event should return empty array');
is(event.cancelable, true,
`"beforeinput" event should be cancelable on ${element.name}`);
is(event.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
`"beforeinput" event should be cancelable on ${element.name} unless it's suppressed by the pref`);
is(event.bubbles, true,
`"input" event should always bubble on ${element.name}`);
is(element.value, oldValue,

View File

@ -4159,6 +4159,7 @@ nsresult nsContentUtils::DispatchInputEvent(
if (!useInputEvent) {
MOZ_ASSERT(aEventMessage == eEditorInput);
MOZ_ASSERT(aEditorInputType == EditorInputType::eUnknown);
MOZ_ASSERT(!aOptions.mNeverCancelable);
// Dispatch "input" event with Event instance.
WidgetEvent widgetEvent(true, eUnidentifiedEvent);
widgetEvent.mSpecifiedEventType = nsGkAtoms::oninput;
@ -4172,6 +4173,12 @@ nsresult nsContentUtils::DispatchInputEvent(
return NS_OK;
}
MOZ_ASSERT_IF(aEventMessage != eEditorBeforeInput,
!aOptions.mNeverCancelable);
MOZ_ASSERT_IF(
aEventMessage == eEditorBeforeInput && aOptions.mNeverCancelable,
aEditorInputType == EditorInputType::eInsertReplacementText);
nsCOMPtr<nsIWidget> widget;
if (aTextEditor) {
widget = aTextEditor->GetWidget();
@ -4202,7 +4209,7 @@ nsresult nsContentUtils::DispatchInputEvent(
InternalEditorInputEvent inputEvent(true, aEventMessage, widget);
inputEvent.mFlags.mCancelable =
aEventMessage == eEditorBeforeInput &&
!aOptions.mNeverCancelable && aEventMessage == eEditorBeforeInput &&
IsCancelableBeforeInputEvent(aEditorInputType);
MOZ_ASSERT(!inputEvent.mFlags.mCancelable || aEventStatus);

View File

@ -27,24 +27,38 @@ namespace mozilla {
* here.
*/
struct MOZ_STACK_CLASS InputEventOptions final {
InputEventOptions() = default;
enum class NeverCancelable {
No,
Yes,
};
InputEventOptions() : mDataTransfer(nullptr), mNeverCancelable(false) {}
explicit InputEventOptions(const InputEventOptions& aOther) = delete;
InputEventOptions(InputEventOptions&& aOther) = default;
explicit InputEventOptions(const nsAString& aData)
: mData(aData), mDataTransfer(nullptr) {}
explicit InputEventOptions(dom::DataTransfer* aDataTransfer)
: mDataTransfer(aDataTransfer) {
explicit InputEventOptions(const nsAString& aData,
NeverCancelable aNeverCancelable)
: mData(aData),
mDataTransfer(nullptr),
mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) {}
explicit InputEventOptions(dom::DataTransfer* aDataTransfer,
NeverCancelable aNeverCancelable)
: mDataTransfer(aDataTransfer),
mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) {
MOZ_ASSERT(mDataTransfer);
MOZ_ASSERT(mDataTransfer->IsReadOnly());
}
InputEventOptions(const nsAString& aData,
OwningNonNullStaticRangeArray&& aTargetRanges)
OwningNonNullStaticRangeArray&& aTargetRanges,
NeverCancelable aNeverCancelable)
: mData(aData),
mDataTransfer(nullptr),
mTargetRanges(std::move(aTargetRanges)) {}
mTargetRanges(std::move(aTargetRanges)),
mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) {}
InputEventOptions(dom::DataTransfer* aDataTransfer,
OwningNonNullStaticRangeArray&& aTargetRanges)
: mDataTransfer(aDataTransfer), mTargetRanges(std::move(aTargetRanges)) {
OwningNonNullStaticRangeArray&& aTargetRanges,
NeverCancelable aNeverCancelable)
: mDataTransfer(aDataTransfer),
mTargetRanges(std::move(aTargetRanges)),
mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) {
MOZ_ASSERT(mDataTransfer);
MOZ_ASSERT(mDataTransfer->IsReadOnly());
}
@ -52,6 +66,8 @@ struct MOZ_STACK_CLASS InputEventOptions final {
nsString mData;
dom::DataTransfer* mDataTransfer;
OwningNonNullStaticRangeArray mTargetRanges;
// If this is set to true, dispatching event won't be cancelable.
bool mNeverCancelable;
};
} // namespace mozilla

View File

@ -2761,7 +2761,11 @@ bool TextControlState::SetValueWithTextEditor(
// nsIPrincipal means that that may be user's input. So, let's
// do it.
nsresult rv = textEditor->ReplaceTextAsAction(
aHandlingSetValue.GetSettingValue(), nullptr, nullptr);
aHandlingSetValue.GetSettingValue(), nullptr,
StaticPrefs::dom_input_event_allow_to_cancel_set_user_input()
? TextEditor::AllowBeforeInputEventCancelable::Yes
: TextEditor::AllowBeforeInputEventCancelable::No,
nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"TextEditor::ReplaceTextAsAction() failed");
return rv != NS_ERROR_OUT_OF_MEMORY;
@ -2837,8 +2841,13 @@ bool TextControlState::SetValueWithTextEditor(
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for now.
nsresult rv =
textEditor->SetTextAsAction(aHandlingSetValue.GetSettingValue(), nullptr);
nsresult rv = textEditor->SetTextAsAction(
aHandlingSetValue.GetSettingValue(),
(aHandlingSetValue.GetSetValueFlags() & eSetValue_BySetUserInput) &&
!StaticPrefs::dom_input_event_allow_to_cancel_set_user_input()
? TextEditor::AllowBeforeInputEventCancelable::No
: TextEditor::AllowBeforeInputEventCancelable::Yes,
nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"TextEditor::SetTextAsAction() failed");
@ -2897,7 +2906,12 @@ bool TextControlState::SetValueWithoutTextEditor(
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()),
eEditorBeforeInput, EditorInputType::eInsertReplacementText, nullptr,
InputEventOptions(inputEventData), &status);
InputEventOptions(
inputEventData,
StaticPrefs::dom_input_event_allow_to_cancel_set_user_input()
? InputEventOptions::NeverCancelable::No
: InputEventOptions::NeverCancelable::Yes),
&status);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch beforeinput event");
if (status == nsEventStatus_eConsumeNoDefault) {
@ -2983,7 +2997,8 @@ bool TextControlState::SetValueWithoutTextEditor(
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()),
eEditorInput, EditorInputType::eInsertReplacementText, nullptr,
InputEventOptions(inputEventData));
InputEventOptions(inputEventData,
InputEventOptions::NeverCancelable::No));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}

View File

@ -20,6 +20,7 @@ SimpleTest.waitForFocus(async () => {
await SpecialPowers.pushPrefEnv({
set: [["dom.input_events.beforeinput.enabled", true]],
});
const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
let content = document.getElementById("content");
/**
@ -181,8 +182,8 @@ SimpleTest.waitForFocus(async () => {
`"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
} else {
if (beforeInputEvents.length > 0 && test.result.fireBeforeInputEvent) {
is(beforeInputEvents[0].cancelable, true,
`"beforeinput" event for "insertReplacementText" should be cancelable when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
is(beforeInputEvents[0].cancelable, kSetUserInputCancelable,
`"beforeinput" event for "insertReplacementText" should be cancelable when setUserInput("${test.input.before}") is called before ${tag} gets focus unless it's suppressed by the pref`);
is(beforeInputEvents[0].inputType, "insertReplacementText",
`inputType of "beforeinput"event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
is(beforeInputEvents[0].data, test.input.before,
@ -267,8 +268,8 @@ SimpleTest.waitForFocus(async () => {
`"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
} else {
if (beforeInputEvents.length > 0 && test.result.fireBeforeInputEvent) {
is(beforeInputEvents[0].cancelable, true,
`"beforeinput" event should be cancelable when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
is(beforeInputEvents[0].cancelable, kSetUserInputCancelable,
`"beforeinput" event should be cancelable when setUserInput("${test.input.after}") is called after ${tag} gets focus unless it's suppressed by the pref`);
is(beforeInputEvents[0].inputType, "insertReplacementText",
`inputType of "beforeinput" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
is(beforeInputEvents[0].data, test.input.after,
@ -535,6 +536,47 @@ SimpleTest.waitForFocus(async () => {
testEditorValueAtEachEvent("text");
testEditorValueAtEachEvent("textarea");
async function testBeforeInputCancelable(aType) {
let tag = aType === "textarea" ? "<textarea>" : `<input type="${aType}">`
let closeTag = aType === "textarea" ? "</textarea>" : "";
for (const kShouldBeCancelable of [true, false]) {
await SpecialPowers.pushPrefEnv({
set: [["dom.input_event.allow_to_cancel_set_user_input", kShouldBeCancelable]],
});
content.innerHTML = `${tag}${closeTag}`;
content.scrollTop; // Flush pending layout.
let target = content.firstChild;
target.value = "Old Value";
let description = `Setting new value of ${tag} before setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `;
let onBeforeInput = (aEvent) => {
is(aEvent.cancelable, kShouldBeCancelable,
`${description}The "beforeinput" event should be ${kShouldBeCancelable ? "cancelable" : "not be cancelable due to suppressed by the pref"}`);
};
let onInput = (aEvent) => {
is(aEvent.cancelable, false,
`${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`);
};
target.addEventListener("beforeinput", onBeforeInput);
target.addEventListener("input", onInput);
SpecialPowers.wrap(target).setUserInput("New Value");
description = `Setting new value of ${tag} after setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `;
target.value = "Old Value";
target.focus();
SpecialPowers.wrap(target).setUserInput("New Value");
target.removeEventListener("beforeinput", onBeforeInput);
target.removeEventListener("input", onInput);
}
await SpecialPowers.clearUserPref({
clear: [["dom.input_event.allow_to_cancel_set_user_input"]],
});
}
await testBeforeInputCancelable("text");
await testBeforeInputCancelable("textarea");
SimpleTest.finish();
});
</script>

View File

@ -366,7 +366,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=851780
// are tested in test_input_number_mouse_events.html
var number = document.getElementById("input_number");
if (isDesktop) { // up/down arrow keys not supported on android/b2g
if (isDesktop) { // up/down arrow keys not supported on android
number.value = "";
number.focus();
// <input type="number">'s inputType value hasn't been decided, see

View File

@ -19,6 +19,7 @@ SimpleTest.waitForFocus(async () => {
await SpecialPowers.pushPrefEnv({
set: [["dom.input_events.beforeinput.enabled", true]],
});
const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
let input = document.querySelector("input[type=text]");
@ -417,7 +418,7 @@ SimpleTest.waitForFocus(async () => {
inputEvents.push(aEvent);
}
}
let condition = `(${aWithEditor ? "with editor" : "without editor"}${aPreventDefaultOfBeforeInput ? ' and canceling "beforeinput" event' : ""})`;
let condition = `(${aWithEditor ? "with editor" : "without editor"}${aPreventDefaultOfBeforeInput ? ' and canceling "beforeinput" event' : ""}, the pref ${kSetUserInputCancelable ? "allows" : "disallows"} to cancel "beforeinput" event})`;
function Reset() {
beforeInputEvents = [];
inputEvents = [];
@ -449,7 +450,7 @@ SimpleTest.waitForFocus(async () => {
}
input.addEventListener("beforeinput", (aEvent) => {
is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
ok(aEvent.cancelable, `${description}"beforeinput" event should be cancelable`);
is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
input.addEventListener("beforeinput", recordEvent);
input.addEventListener("input", recordEvent);
@ -460,7 +461,7 @@ SimpleTest.waitForFocus(async () => {
}, {once: true});
SpecialPowers.wrap(input).setUserInput("def");
is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
if (aPreventDefaultOfBeforeInput) {
if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
is(input.value, "hig",
`${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
is(inputEvents.length, 0,
@ -490,7 +491,7 @@ SimpleTest.waitForFocus(async () => {
}
input.addEventListener("beforeinput", (aEvent) => {
is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
ok(aEvent.cancelable, `${description}"beforeinput" event should be cancelable`);
is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
input.addEventListener("beforeinput", recordEvent);
input.addEventListener("input", recordEvent);
@ -502,7 +503,7 @@ SimpleTest.waitForFocus(async () => {
}, {once: true});
SpecialPowers.wrap(input).setUserInput("def");
is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
if (aPreventDefaultOfBeforeInput) {
if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
is(input.value, "hig",
`${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
is(inputEvents.length, 0,
@ -527,7 +528,7 @@ SimpleTest.waitForFocus(async () => {
}
input.addEventListener("beforeinput", (aEvent) => {
is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
ok(aEvent.cancelable, `${description}"beforeinput" event should be cancelable`);
is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
input.addEventListener("beforeinput", recordEvent);
input.addEventListener("input", recordEvent);
@ -539,7 +540,7 @@ SimpleTest.waitForFocus(async () => {
}, {once: true});
SpecialPowers.wrap(input).setUserInput("def");
is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
if (aPreventDefaultOfBeforeInput) {
if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
is(input.value, "hig",
`${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
is(inputEvents.length, 0,
@ -574,7 +575,7 @@ SimpleTest.waitForFocus(async () => {
}
input.addEventListener("beforeinput", (aEvent) => {
is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
ok(aEvent.cancelable, `${description}"beforeinput" event should be cancelable`);
is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
input.addEventListener("beforeinput", recordEvent);
input.addEventListener("input", recordEvent);
@ -586,7 +587,7 @@ SimpleTest.waitForFocus(async () => {
}, {once: true});
SpecialPowers.wrap(input).setUserInput("def");
is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
if (aPreventDefaultOfBeforeInput) {
if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
is(input.value, "hig",
`${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
is(inputEvents.length, 0,

View File

@ -1810,8 +1810,10 @@ void EditorBase::DispatchInputEvent() {
RefPtr<DataTransfer> dataTransfer = GetInputEventDataTransfer();
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
targetElement, eEditorInput, ToInputType(GetEditAction()), textEditor,
dataTransfer ? InputEventOptions(dataTransfer)
: InputEventOptions(GetInputEventData()));
dataTransfer ? InputEventOptions(dataTransfer,
InputEventOptions::NeverCancelable::No)
: InputEventOptions(GetInputEventData(),
InputEventOptions::NeverCancelable::No));
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"nsContentUtils::DispatchInputEvent() failed, but ignored");
@ -5127,7 +5129,8 @@ EditorBase::AutoEditActionDataSetter::AutoEditActionDataSetter(
mTopLevelEditSubAction(EditSubAction::eNone),
mAborted(false),
mHasTriedToDispatchBeforeInputEvent(false),
mBeforeInputEventCanceled(false) {
mBeforeInputEventCanceled(false),
mMakeBeforeInputEventNonCancelable(false) {
// If we're nested edit action, copies necessary data from the parent.
if (mParentData) {
mSelection = mParentData->mSelection;
@ -5407,10 +5410,16 @@ nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent(
}
}
nsEventStatus status = nsEventStatus_eIgnore;
InputEventOptions::NeverCancelable neverCancelable =
mMakeBeforeInputEventNonCancelable
? InputEventOptions::NeverCancelable::Yes
: InputEventOptions::NeverCancelable::No;
nsresult rv = nsContentUtils::DispatchInputEvent(
targetElement, eEditorBeforeInput, inputType, textEditor,
mDataTransfer ? InputEventOptions(mDataTransfer, std::move(mTargetRanges))
: InputEventOptions(mData, std::move(mTargetRanges)),
mDataTransfer
? InputEventOptions(mDataTransfer, std::move(mTargetRanges),
neverCancelable)
: InputEventOptions(mData, std::move(mTargetRanges), neverCancelable),
&status);
if (NS_WARN_IF(mEditorBase.Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;

View File

@ -1007,6 +1007,13 @@ class EditorBase : public nsIEditor,
*/
void AppendTargetRange(dom::StaticRange& aTargetRange);
/**
* Make dispatching `beforeinput` forcibly non-cancelable.
*/
void MakeBeforeInputEventNonCancelable() {
mMakeBeforeInputEventNonCancelable = true;
}
void Abort() { mAborted = true; }
bool IsAborted() const { return mAborted; }
@ -1218,6 +1225,9 @@ class EditorBase : public nsIEditor,
bool mHasTriedToDispatchBeforeInputEvent;
// Set to true if "beforeinput" event was dispatched and it's canceled.
bool mBeforeInputEventCanceled;
// Set to true if `beforeinput` event must not be cancelable even if
// its inputType is defined as cancelable by the standards.
bool mMakeBeforeInputEventNonCancelable;
#ifdef DEBUG
mutable bool mHasCanHandleChecked = false;

View File

@ -429,13 +429,18 @@ nsresult TextEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) {
return EditorBase::ToGenericNSResult(rv);
}
nsresult TextEditor::SetTextAsAction(const nsAString& aString,
nsIPrincipal* aPrincipal) {
nsresult TextEditor::SetTextAsAction(
const nsAString& aString,
AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
nsIPrincipal* aPrincipal) {
MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound);
MOZ_ASSERT(!AsHTMLEditor());
AutoEditActionDataSetter editActionData(*this, EditAction::eSetText,
aPrincipal);
if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) {
editActionData.MakeBeforeInputEventNonCancelable();
}
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
@ -451,9 +456,10 @@ nsresult TextEditor::SetTextAsAction(const nsAString& aString,
return EditorBase::ToGenericNSResult(rv);
}
nsresult TextEditor::ReplaceTextAsAction(const nsAString& aString,
nsRange* aReplaceRange,
nsIPrincipal* aPrincipal) {
nsresult TextEditor::ReplaceTextAsAction(
const nsAString& aString, nsRange* aReplaceRange,
AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
nsIPrincipal* aPrincipal) {
MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound);
AutoEditActionDataSetter editActionData(*this, EditAction::eReplaceText,
@ -461,6 +467,9 @@ nsresult TextEditor::ReplaceTextAsAction(const nsAString& aString,
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) {
editActionData.MakeBeforeInputEventNonCancelable();
}
if (!AsHTMLEditor()) {
editActionData.SetData(aString);

View File

@ -216,17 +216,27 @@ class TextEditor : public EditorBase, public nsITimerCallback, public nsINamed {
int32_t MaxTextLength() const { return mMaxTextLength; }
void SetMaxTextLength(int32_t aLength) { mMaxTextLength = aLength; }
enum class AllowBeforeInputEventCancelable {
No,
Yes,
};
/**
* Replace existed string with a string.
* This is fast path to replace all string when using single line control.
*
* @param aString The string to be set
* @param aAllowBeforeInputEventCancelable
* Whether `beforeinput` event which will be
* dispatched for this can be cancelable or not.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult
SetTextAsAction(const nsAString& aString, nsIPrincipal* aPrincipal = nullptr);
MOZ_CAN_RUN_SCRIPT nsresult SetTextAsAction(
const nsAString& aString,
AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
nsIPrincipal* aPrincipal = nullptr);
/**
* Replace text in aReplaceRange or all text in this editor with aString and
@ -235,13 +245,17 @@ class TextEditor : public EditorBase, public nsITimerCallback, public nsINamed {
* @param aString The string to set.
* @param aReplaceRange The range to be replaced.
* If nullptr, all contents will be replaced.
* @param aAllowBeforeInputEventCancelable
* Whether `beforeinput` event which will be
* dispatched for this can be cancelable or not.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult
ReplaceTextAsAction(const nsAString& aString, nsRange* aReplaceRange,
nsIPrincipal* aPrincipal = nullptr);
MOZ_CAN_RUN_SCRIPT nsresult ReplaceTextAsAction(
const nsAString& aString, nsRange* aReplaceRange,
AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
nsIPrincipal* aPrincipal = nullptr);
/**
* InsertLineBreakAsAction() is called when user inputs a line break with

View File

@ -19,7 +19,13 @@ SimpleTest.waitForExplicitFinish();
SimpleTest.expectAssertions(0, 1); // In a11y module
SimpleTest.waitForFocus(async () => {
await SpecialPowers.pushPrefEnv({
set: [["dom.input_events.beforeinput.enabled", true]],
set: [
["dom.input_events.beforeinput.enabled", true],
// Even if `beforeinput` events for `setUserInput()` calls are not
// allowed to cancel, correcting the spells should be cancelable for
// compatibility with the other browsers.
["dom.input_event.allow_to_cancel_set_user_input", false],
],
});
let textarea = document.getElementById("textarea");

View File

@ -864,8 +864,11 @@ mozInlineSpellChecker::ReplaceWord(nsINode* aNode, int32_t aOffset,
nsContentUtils::PlatformToDOMLineBreaks(newWord);
}
// Blink dispatches cancelable `beforeinput` event at collecting misspelled
// word so that we should allow to dispatch cancelable event.
RefPtr<TextEditor> textEditor(mTextEditor);
DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(newWord, range);
DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(
newWord, range, TextEditor::AllowBeforeInputEventCancelable::Yes);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new word");
return NS_OK;
}

View File

@ -2058,6 +2058,14 @@
value: true
mirror: always
# Whether to allow or disallow web apps to cancel `beforeinput` events caused
# by MozEditableElement#setUserInput() which is used by autocomplete, autofill
# and password manager.
- name: dom.input_event.allow_to_cancel_set_user_input
type: bool
value: false
mirror: always
# How long a content process can take before closing its IPC channel
# after shutdown is initiated. If the process exceeds the timeout,
# we fear the worst and kill it.

View File

@ -143,6 +143,7 @@ Form History test: form field autocomplete
SpecialPowers.pushPrefEnv({"set": [["security.allow_eval_with_system_principal", true],
["dom.input_events.beforeinput.enabled", true]]});
var kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
var input = $_(1, "field1");
@ -244,7 +245,7 @@ registerPopupShownListener(popupShownListener);
* to listen to for when the search is complete.
* - some items still use setTimeout
*/
function runTest() { // eslint-disable-line complexity
async function runTest() { // eslint-disable-line complexity
testNum++;
ok(true, "Starting test #" + testNum);
@ -1005,7 +1006,8 @@ function runTest() { // eslint-disable-line complexity
ok(event instanceof InputEvent,
`${testNum} "beforeinput" event should be dispatched with InputEvent interface`);
ok(event.bubbles, `${testNum} "beforeinput" event should bubble`);
ok(event.cancelable, `${testNum} "beforeinput" event for "insertReplacementText" should be cancelable`);
is(event.cancelable, kSetUserInputCancelable,
`${testNum} "beforeinput" event for "insertReplacementText" should be cancelable unless it's suppressed by the pref`);
is(event.inputType, "insertReplacementText",
`${testNum} inputType of "beforeinput" event should be "insertReplacementText"`);
is(event.data, "value1",
@ -1048,6 +1050,9 @@ function runTest() { // eslint-disable-line complexity
}
// Check that canceling the beforeinput event cancels autocompletion.
case 501: {
await SpecialPowers.pushPrefEnv({
set: [["dom.input_event.allow_to_cancel_set_user_input", true]],
});
input.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
let inputFired = false;
input.addEventListener("input", () => { inputFired = true; }, {once: true});
@ -1059,6 +1064,10 @@ function runTest() { // eslint-disable-line complexity
ok(!inputFired, `${testNum} "input" event should not have been fired since "beforeinput" was canceled`);
checkForm("");
await SpecialPowers.pushPrefEnv({
clear: [["dom.input_event.allow_to_cancel_set_user_input"]],
});
// Go to test 500.
testNum = 599;
setTimeout(runTest, 100);

View File

@ -74,6 +74,7 @@ function checkForm(expectedValue) {
var testNum = 0;
var prevValue;
var expectingPopup = false;
var kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
function expectPopup() {
info("expecting popup for test " + testNum);
@ -101,7 +102,7 @@ registerPopupShownListener(popupShownListener);
* - call waitForMenuChange(x) to run the next test when the autocomplete popup
* to have x items in it
*/
function runTest() {
async function runTest() {
testNum++;
let datalist;
@ -465,7 +466,7 @@ function runTest() {
ok(event instanceof InputEvent,
`${testNum} "beforeinput" event should be dispatched with InputEvent interface`);
ok(event.bubbles, `${testNum} "beforeinput" event should bubble`);
ok(event.cancelable, `${testNum} "beforeinput" event for "insertReplacementText" should be cancelable`);
is(event.cancelable, kSetUserInputCancelable, `${testNum} "beforeinput" event for "insertReplacementText" should be cancelable unless it's suppressed`);
is(event.inputType, "insertReplacementText", `${testNum} inputType of "beforeinput" event should be "insertReplacementText"`);
is(event.data, "Google", `${testNum} data of "beforeinput" event should be "Google"`);
is(event.dataTransfer, null, `${testNum} dataTransfer of "beforeinput" event should be null`);
@ -498,6 +499,9 @@ function runTest() {
}
case 401: {
await SpecialPowers.pushPrefEnv({
set: [["dom.input_event.allow_to_cancel_set_user_input", true]],
});
input.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true});
let inputFired = false;
input.addEventListener("input", () => { inputFired = true; }, {once: true});
@ -510,6 +514,9 @@ function runTest() {
checkForm("");
input.blur();
await SpecialPowers.pushPrefEnv({
clear: [["dom.input_event.allow_to_cancel_set_user_input"]],
});
SimpleTest.finish();
break;
}

View File

@ -31,6 +31,7 @@ async function runTest() {
await SpecialPowers.pushPrefEnv({
set: [["dom.input_events.beforeinput.enabled", true]],
});
const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
let resolveFunc = null;
function onPopup() {
@ -83,8 +84,8 @@ async function runTest() {
input.addEventListener("beforeinput", (event) => {
ok(!beforeInputFired, '"input" event should be fired only once at typing');
beforeInputFired = true;
ok(event.cancelable,
'"beforeinput" event for "insertReplacementText" should be cancelable');
is(event.cancelable, kSetUserInputCancelable,
`"beforeinput" event for "insertReplacementText" should be cancelable unless it's suppressed by the pref`);
is(event.inputType, "insertReplacementText",
"inputType of \"beforeinput\" event should be \"insertReplacementText\"");
ok(!input.validity.valid,
@ -116,8 +117,8 @@ async function runTest() {
input.addEventListener("beforeinput", (event) => {
ok(!beforeInputFired, '"input" event should be fired only once at typing');
beforeInputFired = true;
ok(event.cancelable,
'"beforeinput" event for "insertReplacementText" should be cancelable');
is(event.cancelable, kSetUserInputCancelable,
`"beforeinput" event for "insertReplacementText" should be cancelable unless it's suppressed by the pref`);
is(event.inputType, "insertReplacementText",
"inputType of \"beforeinput\" event should be \"insertReplacementText\"");
ok(input.validity.valid,

View File

@ -40,8 +40,8 @@ function handleBeforeInput(aEvent) {
is(input.value, "value", "The value should've not been modified yet");
ok(aEvent instanceof InputEvent,
'"beforeinput" event should be dispatched with InputEvent interface');
is(aEvent.cancelable, true,
'"beforeinput" event should be cancelable');
is(aEvent.cancelable, SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"),
`"beforeinput" event should be cancelable unless it's supporessed by the pref`);
is(aEvent.bubbles, true,
'"beforeinput" event should always bubble');
is(aEvent.inputType, "insertReplacementText",

View File

@ -145,14 +145,19 @@ nsDoTestsForEditorWithAutoComplete.prototype = {
true,
`${this._description}, ${aTest.description}: "${events[i].type}" event should be dispatched with InputEvent interface`
);
let expectCancelable =
events[i].type === "beforeinput" &&
(aTest.inputEvents[i].inputType !== "insertReplacementText" ||
SpecialPowers.getBoolPref(
"dom.input_event.allow_to_cancel_set_user_input"
));
this._is(
events[i].cancelable,
events[i].type === "beforeinput",
expectCancelable,
`${this._description}, ${aTest.description}: "${
events[i].type
}" event should ${
events[i].type === "beforeinput" ? "be" : "be never"
} cancelable`
}" event should ${expectCancelable ? "be" : "be never"} cancelable`
);
this._is(
events[i].bubbles,