mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 18:04:46 +00:00
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:
parent
10526405d7
commit
04027a5656
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user