Bug 970802 - part 4: Make TextControlState dispatch "beforeinput" event if there is no TextEditor r=smaug
If `TextControlState` does not have `TextEditor` and its `SetValue()` is called from `SetUserInput()`, `TextControlState` itself needs to dispatch `beforeinput` event. If the value is modified by `beforeinput` event listener, it's intended that `preventDefault()` is called by the web apps. However, the behavior in this case is not mentioned by UI Events nor Input Events spec. We should just file a spec issue instead of emulating Chrome's behavior for now because it requires more changes, but this case must be an edge case. The spec issue is: https://github.com/w3c/input-events/issues/106 Differential Revision: https://phabricator.services.mozilla.com/D58126
This commit is contained in:
@@ -1209,6 +1209,19 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
|
||||
mTextInputListener->SetValueChanged(mSetValueFlags &
|
||||
TextControlState::eSetValue_Notify);
|
||||
mEditActionHandled = false;
|
||||
// Even if falling back to `TextControlState::SetValueWithoutTextEditor()`
|
||||
// due to editor destruction, it shouldn't dispatch "beforeinput" event
|
||||
// anymore. Therefore, we should mark that we've already dispatched
|
||||
// "beforeinput" event.
|
||||
WillDispatchBeforeInputEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* WillDispatchBeforeInputEvent() is called immediately before dispatching
|
||||
* "beforeinput" event in `TextControlState`.
|
||||
*/
|
||||
void WillDispatchBeforeInputEvent() {
|
||||
mBeforeInputEventHasBeenDispatched = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1282,6 +1295,9 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
|
||||
->mTextControlFrame.IsAlive();
|
||||
}
|
||||
bool HasEditActionHandled() const { return mEditActionHandled; }
|
||||
bool HasBeforeInputEventDispatched() const {
|
||||
return mBeforeInputEventHasBeenDispatched;
|
||||
}
|
||||
bool Is(TextControlAction aTextControlAction) const {
|
||||
return mTextControlAction == aTextControlAction;
|
||||
}
|
||||
@@ -1346,6 +1362,7 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
|
||||
bool mTextControlStateDestroyed = false;
|
||||
bool mEditActionHandled = false;
|
||||
bool mPreareEditorLater = false;
|
||||
bool mBeforeInputEventHasBeenDispatched = false;
|
||||
};
|
||||
|
||||
/*****************************************************************************
|
||||
@@ -2651,6 +2668,11 @@ bool TextControlState::SetValue(const nsAString& aValue,
|
||||
// Note that if this may be called during reframe of the editor. In such
|
||||
// case, we shouldn't commit composition. Therefore, when this is called
|
||||
// for internal processing, we shouldn't commit the composition.
|
||||
// TODO: In strictly speaking, we should move committing composition into
|
||||
// editor because if "beforeinput" for this setting value is canceled,
|
||||
// we shouldn't commit composition. However, in Firefox, we never
|
||||
// call this via `setUserInput` during composition. Therefore, the
|
||||
// bug must not be reproducible actually.
|
||||
if (aFlags & (eSetValue_BySetUserInput | eSetValue_ByContent)) {
|
||||
if (EditorHasComposition()) {
|
||||
// When this is called recursively, there shouldn't be composition.
|
||||
@@ -2918,39 +2940,92 @@ bool TextControlState::SetValueWithoutTextEditor(
|
||||
// OnValueChanged below still need to be called.
|
||||
if (!mValue->Equals(aHandlingSetValue.GetSettingValue()) ||
|
||||
!StaticPrefs::dom_input_skip_cursor_move_for_same_value_set()) {
|
||||
if (!mValue->Assign(aHandlingSetValue.GetSettingValue(), fallible)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since we have no editor we presumably have cached selection state.
|
||||
if (IsSelectionCached()) {
|
||||
MOZ_ASSERT(AreFlagsNotDemandingContradictingMovements(
|
||||
aHandlingSetValue.GetSetValueFlags()));
|
||||
|
||||
SelectionProperties& props = GetSelectionProperties();
|
||||
if (aHandlingSetValue.GetSetValueFlags() &
|
||||
eSetValue_MoveCursorToEndIfValueChanged) {
|
||||
props.SetStart(aHandlingSetValue.GetSettingValue().Length());
|
||||
props.SetEnd(aHandlingSetValue.GetSettingValue().Length());
|
||||
props.SetDirection(nsITextControlFrame::eForward);
|
||||
} else if (aHandlingSetValue.GetSetValueFlags() &
|
||||
eSetValue_MoveCursorToBeginSetSelectionDirectionForward) {
|
||||
props.SetStart(0);
|
||||
props.SetEnd(0);
|
||||
props.SetDirection(nsITextControlFrame::eForward);
|
||||
} else {
|
||||
// Make sure our cached selection position is not outside the new
|
||||
// value.
|
||||
props.SetStart(std::min(props.GetStart(),
|
||||
aHandlingSetValue.GetSettingValue().Length()));
|
||||
props.SetEnd(std::min(props.GetEnd(),
|
||||
aHandlingSetValue.GetSettingValue().Length()));
|
||||
bool handleSettingValue = true;
|
||||
// If `SetValue()` call is nested, `GetSettingValue()` result will be
|
||||
// modified. So, we need to store input event data value before
|
||||
// dispatching beforeinput event.
|
||||
nsString inputEventData(aHandlingSetValue.GetSettingValue());
|
||||
if ((aHandlingSetValue.GetSetValueFlags() & eSetValue_BySetUserInput) &&
|
||||
StaticPrefs::dom_input_events_beforeinput_enabled() &&
|
||||
!aHandlingSetValue.HasBeforeInputEventDispatched()) {
|
||||
// This probably occurs when session restorer sets the old value with
|
||||
// `setUserInput`. If so, we need to dispatch "beforeinput" event of
|
||||
// "insertReplacementText" for conforming to the spec. However, the
|
||||
// spec does NOT treat the session restoring case. Therefore, if this
|
||||
// breaks session restorere in a lot of web apps, we should probably
|
||||
// stop dispatching it or make it non-cancelable.
|
||||
MOZ_ASSERT(aHandlingSetValue.GetTextControlElement());
|
||||
MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid());
|
||||
aHandlingSetValue.WillDispatchBeforeInputEvent();
|
||||
nsEventStatus status = nsEventStatus_eIgnore;
|
||||
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
|
||||
MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()),
|
||||
eEditorBeforeInput, EditorInputType::eInsertReplacementText, nullptr,
|
||||
nsContentUtils::InputEventOptions(inputEventData), &status);
|
||||
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
|
||||
"Failed to dispatch beforeinput event");
|
||||
if (status == nsEventStatus_eConsumeNoDefault) {
|
||||
return true; // "beforeinput" event was canceled.
|
||||
}
|
||||
// If we were destroyed by "beforeinput" event listeners, probably, we
|
||||
// don't need to keep handling it.
|
||||
if (aHandlingSetValue.IsTextControlStateDestroyed()) {
|
||||
return true;
|
||||
}
|
||||
// Even if "beforeinput" event was not canceled, its listeners may do
|
||||
// something. If it causes creating `TextEditor` and bind this to a
|
||||
// frame, we need to use the path, but `TextEditor` shouldn't fire
|
||||
// "beforeinput" event again. Therefore, we need to prevent editor
|
||||
// to dispatch it.
|
||||
if (mTextEditor && mBoundFrame) {
|
||||
AutoInputEventSuppresser suppressInputEvent(mTextEditor);
|
||||
if (!SetValueWithTextEditor(aHandlingSetValue)) {
|
||||
return false;
|
||||
}
|
||||
// If we were destroyed by "beforeinput" event listeners, probably, we
|
||||
// don't need to keep handling it.
|
||||
if (aHandlingSetValue.IsTextControlStateDestroyed()) {
|
||||
return true;
|
||||
}
|
||||
handleSettingValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the frame display if needed
|
||||
if (mBoundFrame) {
|
||||
mBoundFrame->UpdateValueDisplay(true);
|
||||
if (handleSettingValue) {
|
||||
if (!mValue->Assign(aHandlingSetValue.GetSettingValue(), fallible)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since we have no editor we presumably have cached selection state.
|
||||
if (IsSelectionCached()) {
|
||||
MOZ_ASSERT(AreFlagsNotDemandingContradictingMovements(
|
||||
aHandlingSetValue.GetSetValueFlags()));
|
||||
|
||||
SelectionProperties& props = GetSelectionProperties();
|
||||
if (aHandlingSetValue.GetSetValueFlags() &
|
||||
eSetValue_MoveCursorToEndIfValueChanged) {
|
||||
props.SetStart(aHandlingSetValue.GetSettingValue().Length());
|
||||
props.SetEnd(aHandlingSetValue.GetSettingValue().Length());
|
||||
props.SetDirection(nsITextControlFrame::eForward);
|
||||
} else if (aHandlingSetValue.GetSetValueFlags() &
|
||||
eSetValue_MoveCursorToBeginSetSelectionDirectionForward) {
|
||||
props.SetStart(0);
|
||||
props.SetEnd(0);
|
||||
props.SetDirection(nsITextControlFrame::eForward);
|
||||
} else {
|
||||
// Make sure our cached selection position is not outside the new
|
||||
// value.
|
||||
props.SetStart(std::min(
|
||||
props.GetStart(), aHandlingSetValue.GetSettingValue().Length()));
|
||||
props.SetEnd(std::min(props.GetEnd(),
|
||||
aHandlingSetValue.GetSettingValue().Length()));
|
||||
}
|
||||
}
|
||||
|
||||
// Update the frame display if needed
|
||||
if (mBoundFrame) {
|
||||
mBoundFrame->UpdateValueDisplay(true);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is called as part of user input, we need to dispatch "input"
|
||||
@@ -2966,8 +3041,7 @@ bool TextControlState::SetValueWithoutTextEditor(
|
||||
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
|
||||
MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()),
|
||||
eEditorInput, EditorInputType::eInsertReplacementText, nullptr,
|
||||
nsContentUtils::InputEventOptions(
|
||||
aHandlingSetValue.GetSettingValue()));
|
||||
nsContentUtils::InputEventOptions(inputEventData));
|
||||
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
|
||||
"Failed to dispatch input event");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user