Bug 970802 - part 3: Implement beforeinput event dispatcher and add onbeforeinput event handler attribute r=smaug

This patch makes `nsContentUtils::DispatchInputEvent()` dispatch `beforeinput`
event too.  And also adds `onbeforeinput` event handler which is really
important for feature detection (although Chrome has not implemented this
attribute yet: https://bugs.chromium.org/p/chromium/issues/detail?id=947408).

However, we don't implement `InputEvent.getTargetRanges()` in this bug and
implementing `beforeinput` event may hit bugs of some web apps.  Therefore,
this patch disables `beforeinput` event by default even in Nightly channel.

Differential Revision: https://phabricator.services.mozilla.com/D58125
This commit is contained in:
Masayuki Nakano
2020-01-08 09:23:40 +00:00
parent fc0e25e156
commit 5c4adfac5d
11 changed files with 217 additions and 30 deletions

View File

@@ -4059,7 +4059,8 @@ nsresult nsContentUtils::DispatchTrustedEvent(
Document* aDoc, nsISupports* aTarget, const nsAString& aEventName,
CanBubble aCanBubble, Cancelable aCancelable, Composed aComposed,
bool* aDefaultAction) {
MOZ_ASSERT(!aEventName.EqualsLiteral("input"),
MOZ_ASSERT(!aEventName.EqualsLiteral("input") &&
!aEventName.EqualsLiteral("beforeinput"),
"Use DispatchInputEvent() instead");
return DispatchEvent(aDoc, aTarget, aEventName, aCanBubble, aCancelable,
aComposed, Trusted::eYes, aDefaultAction);
@@ -4141,10 +4142,14 @@ nsContentUtils::InputEventOptions::InputEventOptions(
}
// static
nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
EditorInputType aEditorInputType,
TextEditor* aTextEditor,
const InputEventOptions& aOptions) {
nsresult nsContentUtils::DispatchInputEvent(
Element* aEventTargetElement, EventMessage aEventMessage,
EditorInputType aEditorInputType, TextEditor* aTextEditor,
const InputEventOptions& aOptions,
nsEventStatus* aEventStatus /* = nullptr */) {
MOZ_ASSERT(aEventMessage == eEditorInput ||
aEventMessage == eEditorBeforeInput);
if (NS_WARN_IF(!aEventTargetElement)) {
return NS_ERROR_INVALID_ARG;
}
@@ -4152,7 +4157,8 @@ nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
// If this is called from editor, the instance should be set to aTextEditor.
// Otherwise, we need to look for an editor for aEventTargetElement.
// However, we don't need to do it for HTMLEditor since nobody shouldn't
// dispatch "input" event for HTMLEditor except HTMLEditor itself.
// dispatch "beforeinput" nor "input" event for HTMLEditor except HTMLEditor
// itself.
bool useInputEvent = false;
if (aTextEditor) {
useInputEvent = true;
@@ -4177,15 +4183,18 @@ nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
// If the event target is an <input> element, we need to update
// validationMessage value before dispatching "input" event because
// "input" event listener may need to check it.
HTMLInputElement* inputElement =
HTMLInputElement::FromNode(aEventTargetElement);
if (inputElement) {
MOZ_KnownLive(inputElement)->MaybeUpdateAllValidityStates(true);
// XXX Should we stop dispatching "input" event if the target is removed
// from the DOM tree?
if (aEventMessage == eEditorInput) {
HTMLInputElement* inputElement =
HTMLInputElement::FromNode(aEventTargetElement);
if (inputElement) {
MOZ_KnownLive(inputElement)->MaybeUpdateAllValidityStates(true);
// XXX Should we stop dispatching "input" event if the target is removed
// from the DOM tree?
}
}
if (!useInputEvent) {
MOZ_ASSERT(aEventMessage == eEditorInput);
MOZ_ASSERT(aEditorInputType == EditorInputType::eUnknown);
// Dispatch "input" event with Event instance.
WidgetEvent widgetEvent(true, eUnidentifiedEvent);
@@ -4227,7 +4236,12 @@ nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
}
// Dispatch "input" event with InputEvent instance.
InternalEditorInputEvent inputEvent(true, eEditorInput, widget);
InternalEditorInputEvent inputEvent(true, aEventMessage, widget);
inputEvent.mFlags.mCancelable =
aEventMessage == eEditorBeforeInput &&
IsCancelableBeforeInputEvent(aEditorInputType);
MOZ_ASSERT(!inputEvent.mFlags.mCancelable || aEventStatus);
// Using same time as old event dispatcher in EditorBase for backward
// compatibility.
@@ -4239,8 +4253,7 @@ nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
// Otherwise, i.e., editor hasn't been created for the element yet,
// we should set isComposing to false since the element can never has
// composition without editor.
inputEvent.mIsComposing =
aTextEditor ? !!aTextEditor->GetComposition() : false;
inputEvent.mIsComposing = aTextEditor && aTextEditor->GetComposition();
if (!aTextEditor || !aTextEditor->AsHTMLEditor()) {
if (IsDataAvailableOnTextEditor(aEditorInputType)) {
@@ -4279,9 +4292,27 @@ nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
inputEvent.mInputType = aEditorInputType;
(new AsyncEventDispatcher(aEventTargetElement, inputEvent))
->RunDOMEventWhenSafe();
return NS_OK;
if (!IsSafeToRunScript()) {
// If we cannot dispatch an event right now, we cannot make it cancelable.
NS_ASSERTION(
!inputEvent.mFlags.mCancelable,
"Cancelable beforeinput event dispatcher should run when it's safe");
inputEvent.mFlags.mCancelable = false;
(new AsyncEventDispatcher(aEventTargetElement, inputEvent))
->RunDOMEventWhenSafe();
return NS_OK;
}
RefPtr<nsPresContext> presContext =
aEventTargetElement->OwnerDoc()->GetPresContext();
if (NS_WARN_IF(!presContext)) {
return NS_ERROR_FAILURE;
}
nsresult rv = EventDispatcher::Dispatch(aEventTargetElement, presContext,
&inputEvent, nullptr, aEventStatus);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Dispatching `beforeinput` or `input` event failed");
return rv;
}
nsresult nsContentUtils::DispatchChromeEvent(

View File

@@ -1458,15 +1458,19 @@ class nsContentUtils {
}
/**
* This method dispatches "input" event with proper event class. If it's
* unsafe to dispatch, this put the event into the script runner queue.
* This method dispatches "beforeinput" event with EditorInputEvent or
* "input" event with proper event class. If it's unsafe to dispatch,
* this put the event into the script runner queue. In such case, the
* event becomes not cancelable even if it's defined as cancelable by
* the spec.
* Input Events spec defines as:
* Input events are dispatched on elements that act as editing hosts,
* including elements with the contenteditable attribute set, textarea
* elements, and input elements that permit text input.
*
* @param aEventTarget The event target element of the "input" event.
* Must not be nullptr.
* @param aEventTarget The event target element of the "beforeinput"
* or "input" event. Must not be nullptr.
* @param aEventMessage Muse be eEditorBeforeInput or eEditorInput.
* @param aEditorInputType The inputType value of InputEvent.
* If aEventTarget won't dispatch "input" event
* with InputEvent, set EditorInputType::eUnknown.
@@ -1476,11 +1480,18 @@ class nsContentUtils {
* @param aOptions Optional. If aEditorInputType value requires
* some additional data, they should be properly
* set with this argument.
* @param aEventStatus Returns nsEventStatus_eConsumeNoDefault if
* the dispatching event is cancelable and the
* event was canceled by script (including
* chrome script). Otherwise, returns given
* value. Note that this can be nullptr only
* when the dispatching event is not cancelable.
*/
MOZ_CAN_RUN_SCRIPT
static nsresult DispatchInputEvent(Element* aEventTarget) {
return DispatchInputEvent(aEventTarget, mozilla::EditorInputType::eUnknown,
nullptr, InputEventOptions());
return DispatchInputEvent(aEventTarget, mozilla::eEditorInput,
mozilla::EditorInputType::eUnknown, nullptr,
InputEventOptions());
}
struct MOZ_STACK_CLASS InputEventOptions final {
InputEventOptions() = default;
@@ -1493,9 +1504,11 @@ class nsContentUtils {
};
MOZ_CAN_RUN_SCRIPT
static nsresult DispatchInputEvent(Element* aEventTarget,
mozilla::EventMessage aEventMessage,
mozilla::EditorInputType aEditorInputType,
mozilla::TextEditor* aTextEditor,
const InputEventOptions& aOptions);
const InputEventOptions& aOptions,
nsEventStatus* aEventStatus = nullptr);
/**
* This method creates and dispatches a untrusted event.

View File

@@ -180,6 +180,8 @@ EVENT(finish, eMarqueeFinish, EventNameType_HTMLMarqueeOnly, eBasicEventClass)
EVENT(formdata, eFormData, EventNameType_HTML, eBasicEventClass)
EVENT(fullscreenchange, eFullscreenChange, EventNameType_HTML, eBasicEventClass)
EVENT(fullscreenerror, eFullscreenError, EventNameType_HTML, eBasicEventClass)
EVENT(beforeinput, eEditorBeforeInput, EventNameType_HTMLXUL,
eEditorInputEventClass)
EVENT(input, eEditorInput, EventNameType_HTMLXUL, eEditorInputEventClass)
EVENT(invalid, eFormInvalid, EventNameType_HTMLXUL, eBasicEventClass)
EVENT(keydown, eKeyDown, EventNameType_HTMLXUL, eKeyboardEventClass)

View File

@@ -2957,12 +2957,15 @@ bool TextControlState::SetValueWithoutTextEditor(
// event with "insertReplacementText" since web apps may want to know
// the user operation which changes editor value with a built-in function
// like autocomplete, password manager, session restore, etc.
// XXX Should we stop dispatching `input` event if the text control
// element has already removed from the DOM tree by a `beforeinput`
// event listener?
if (aHandlingSetValue.GetSetValueFlags() & eSetValue_BySetUserInput) {
MOZ_ASSERT(aHandlingSetValue.GetTextControlElement());
MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid());
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()),
EditorInputType::eInsertReplacementText, nullptr,
eEditorInput, EditorInputType::eInsertReplacementText, nullptr,
nsContentUtils::InputEventOptions(
aHandlingSetValue.GetSettingValue()));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),

View File

@@ -31,6 +31,8 @@ interface mixin GlobalEventHandlers {
attribute EventHandler onfocus;
//(Not implemented)attribute EventHandler oncancel;
attribute EventHandler onauxclick;
[Pref="dom.input_events.beforeinput.enabled"]
attribute EventHandler onbeforeinput;
attribute EventHandler oncanplay;
attribute EventHandler oncanplaythrough;
attribute EventHandler onchange;

View File

@@ -2250,7 +2250,7 @@ void EditorBase::DispatchInputEvent(EditAction aEditAction,
}
RefPtr<TextEditor> textEditor = AsTextEditor();
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
targetElement, ToInputType(aEditAction), textEditor,
targetElement, eEditorInput, ToInputType(aEditAction), textEditor,
aDataTransfer ? nsContentUtils::InputEventOptions(aDataTransfer)
: nsContentUtils::InputEventOptions(aData));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),

View File

@@ -1617,6 +1617,11 @@
value: false
mirror: always
- name: dom.input_events.beforeinput.enabled
type: bool
value: false
mirror: always
# Whether we conform to Input Events Level 1 or Input Events Level 2.
# true: conforming to Level 1
# false: conforming to Level 2

View File

@@ -8,10 +8,10 @@
#include <stdint.h>
#include "mozilla/dom/EventTarget.h"
#include "mozilla/layers/LayersTypes.h"
#include "mozilla/EventForwards.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/EventTarget.h"
#include "mozilla/layers/LayersTypes.h"
#include "nsCOMPtr.h"
#include "nsAtom.h"
#include "nsISupportsImpl.h"
@@ -855,7 +855,8 @@ class WidgetEvent : public WidgetEventTime {
mMessage == eDragStart || mMessage == eDrop;
break;
case eEditorInputEventClass:
mFlags.mComposed = mMessage == eEditorInput;
mFlags.mComposed =
mMessage == eEditorInput || mMessage == eEditorBeforeInput;
break;
case eFocusEventClass:
mFlags.mComposed = mMessage == eBlur || mMessage == eFocus ||

View File

@@ -11,6 +11,10 @@
#include "nsStringFwd.h"
#include "nsTArray.h"
#ifdef DEBUG
# include "mozilla/StaticPrefs_dom.h"
#endif // #ifdef DEBUG
class nsCommandParams;
/**
@@ -204,6 +208,130 @@ inline bool IsDataTransferAvailableOnHTMLEditor(EditorInputType aInputType) {
}
}
/**
* IsCancelableBeforeInputEvent() returns true if `beforeinput` event for
* aInputType should be cancelable.
*
* Input Events Level 1:
* https://rawgit.com/w3c/input-events/v1/index.html#x5-1-2-attributes
* Input Events Level 2:
* https://w3c.github.io/input-events/#interface-InputEvent-Attributes
*/
inline bool IsCancelableBeforeInputEvent(EditorInputType aInputType) {
switch (aInputType) {
case EditorInputType::eInsertText:
return true; // In Level 1, undefined.
case EditorInputType::eInsertReplacementText:
return true; // In Level 1, undefined.
case EditorInputType::eInsertLineBreak:
return true; // In Level 1, undefined.
case EditorInputType::eInsertParagraph:
return true; // In Level 1, undefined.
case EditorInputType::eInsertOrderedList:
return true;
case EditorInputType::eInsertUnorderedList:
return true;
case EditorInputType::eInsertHorizontalRule:
return true;
case EditorInputType::eInsertFromYank:
return true;
case EditorInputType::eInsertFromDrop:
return true;
case EditorInputType::eInsertFromPaste:
return true;
case EditorInputType::eInsertFromPasteAsQuotation:
return true;
case EditorInputType::eInsertTranspose:
return true;
case EditorInputType::eInsertCompositionText:
return false;
case EditorInputType::eInsertFromComposition:
MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
return true;
case EditorInputType::eInsertLink:
return true;
case EditorInputType::eDeleteByComposition:
MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
return true;
case EditorInputType::eDeleteCompositionText:
MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
return false;
case EditorInputType::eDeleteWordBackward:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteWordForward:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteSoftLineBackward:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteSoftLineForward:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteEntireSoftLine:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteHardLineBackward:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteHardLineForward:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteByDrag:
return true;
case EditorInputType::eDeleteByCut:
return true;
case EditorInputType::eDeleteContent:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteContentBackward:
return true; // In Level 1, undefined.
case EditorInputType::eDeleteContentForward:
return true; // In Level 1, undefined.
case EditorInputType::eHistoryUndo:
return true;
case EditorInputType::eHistoryRedo:
return true;
case EditorInputType::eFormatBold:
return true;
case EditorInputType::eFormatItalic:
return true;
case EditorInputType::eFormatUnderline:
return true;
case EditorInputType::eFormatStrikeThrough:
return true;
case EditorInputType::eFormatSuperscript:
return true;
case EditorInputType::eFormatSubscript:
return true;
case EditorInputType::eFormatJustifyFull:
return true;
case EditorInputType::eFormatJustifyCenter:
return true;
case EditorInputType::eFormatJustifyRight:
return true;
case EditorInputType::eFormatJustifyLeft:
return true;
case EditorInputType::eFormatIndent:
return true;
case EditorInputType::eFormatOutdent:
return true;
case EditorInputType::eFormatRemove:
return true;
case EditorInputType::eFormatSetBlockTextDirection:
return true;
case EditorInputType::eFormatSetInlineTextDirection:
return true;
case EditorInputType::eFormatBackColor:
return true;
case EditorInputType::eFormatFontColor:
return true;
case EditorInputType::eFormatFontName:
return true;
case EditorInputType::eUnknown:
// This is not declared by Input Events, but it does not make sense to
// allow web apps to cancel default action without inputType value check.
// If some our specific edit actions should be cancelable, new inputType
// value for them should be declared by the spec.
return false;
default:
MOZ_ASSERT_UNREACHABLE("The new input type is not handled");
return false;
}
}
#define NS_DEFINE_COMMAND(aName, aCommandStr) , aName
#define NS_DEFINE_COMMAND_WITH_PARAM(aName, aCommandStr, aParam) , aName
#define NS_DEFINE_COMMAND_NO_EXEC_COMMAND(aName) , aName

View File

@@ -458,6 +458,7 @@ NS_EVENT_MESSAGE_FIRST_LAST(eGamepadEvent, eGamepadButtonDown,
// input and beforeinput events.
NS_EVENT_MESSAGE(eEditorInput)
NS_EVENT_MESSAGE(eEditorBeforeInput)
// selection events
NS_EVENT_MESSAGE(eSelectStart)

View File

@@ -738,6 +738,7 @@ STATIC_ATOMS = [
Atom("onauxclick", "onauxclick"),
Atom("onbeforecopy", "onbeforecopy"),
Atom("onbeforecut", "onbeforecut"),
Atom("onbeforeinput", "onbeforeinput"),
Atom("onbeforepaste", "onbeforepaste"),
Atom("onbeforeprint", "onbeforeprint"),
Atom("onbeforescriptexecute", "onbeforescriptexecute"),