Bug 1588745 - part 5: Split TextControlState::SetValue() r=Ehsan

`TextControlState::SetValue()` does 4 things.
1. Committing composition if there is and if possible.
2. Setting value with `TextEditor` if text editor and frame are available.
3. Setting value without `TextEditor` otherwise.
4. Notifying value changed.

We can split #2 and #3 from it now because `AutoTextControlHandlingState`
manages nested actions.  Therefore, this patch creates
`SetValueWithTextEditor()` and `SetValueWithoutTextEditor()` which take
`AutoTextControlHandlingState`.

Depends on D51394

Differential Revision: https://phabricator.services.mozilla.com/D51395
This commit is contained in:
Masayuki Nakano
2019-11-02 23:23:54 +00:00
parent 55109d6fed
commit c2dc961e9a
4 changed files with 778 additions and 271 deletions

View File

@@ -1092,10 +1092,19 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
: mParent(aTextControlState.mHandlingState),
mTextControlState(aTextControlState),
mTextCtrlElement(aTextControlState.mTextCtrlElement),
mTextInputListener(aTextControlState.mTextListener),
mTextControlAction(aTextControlAction) {
MOZ_ASSERT(aTextControlAction != TextControlAction::SetValue,
"Use specific constructor");
mTextControlState.mHandlingState = this;
if (Is(TextControlAction::CommitComposition)) {
MOZ_ASSERT(mParent);
MOZ_ASSERT(mParent->Is(TextControlAction::SetValue));
// If we're trying to commit composition before handling SetValue,
// the parent old values will be outdated so that we need to clear
// them.
mParent->InvalidateOldValue();
}
}
/**
@@ -1105,23 +1114,31 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
*/
AutoTextControlHandlingState(TextControlState& aTextControlState,
TextControlAction aTextControlAction,
const nsAString& aSettingValue, ErrorResult& aRv)
const nsAString& aSettingValue,
const nsAString* aOldValue, uint32_t aFlags,
ErrorResult& aRv)
: mParent(aTextControlState.mHandlingState),
mTextControlState(aTextControlState),
mTextCtrlElement(aTextControlState.mTextCtrlElement),
mTextInputListener(aTextControlState.mTextListener),
mSettingValue(aSettingValue),
mOldValue(aOldValue),
mSetValueFlags(aFlags),
mTextControlAction(aTextControlAction) {
MOZ_ASSERT(aTextControlAction == TextControlAction::SetValue,
"Use generic constructor");
mTextControlState.mHandlingState = this;
// FYI: Using nsString may avoid copying the string buffer.
nsString settingValue(aSettingValue);
if (!nsContentUtils::PlatformToDOMLineBreaks(settingValue, fallible)) {
if (!nsContentUtils::PlatformToDOMLineBreaks(mSettingValue, fallible)) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
// Update all setting value's new value because older value shouldn't
// overwrite newer value.
UpdateSettingValue(settingValue);
if (mParent) {
// If SetValue is nested, parents cannot trust their old value anymore.
// So, we need to clear them.
mParent->UpdateSettingValueAndInvalidateOldValue(mSettingValue);
}
}
~AutoTextControlHandlingState() {
@@ -1141,12 +1158,27 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
bool IsTextControlStateDestroyed() const {
return mTextControlStateDestroyed;
}
bool Is(TextControlAction aTextControlAction) const {
return mTextControlAction == aTextControlAction;
}
bool IsHandling(TextControlAction aTextControlAction) const {
if (mTextControlAction == aTextControlAction) {
return true;
}
return mParent ? mParent->IsHandling(aTextControlAction) : false;
}
nsITextControlElement* GetTextControlElement() const {
return mTextCtrlElement;
}
TextInputListener* GetTextInputListener() const { return mTextInputListener; }
uint32_t GetSetValueFlags() const {
MOZ_ASSERT(Is(TextControlAction::SetValue));
return mSetValueFlags;
}
const nsAString* GetOldValue() const {
MOZ_ASSERT(Is(TextControlAction::SetValue));
return mOldValue;
}
const nsString& GetSettingValue() const {
MOZ_ASSERT(IsHandling(TextControlAction::SetValue));
if (mTextControlAction == TextControlAction::SetValue) {
@@ -1156,23 +1188,35 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
}
private:
void UpdateSettingValue(const nsString& aSettingValue) {
void UpdateSettingValueAndInvalidateOldValue(const nsString& aSettingValue) {
if (mTextControlAction == TextControlAction::SetValue) {
mSettingValue = aSettingValue;
}
mOldValue = nullptr;
if (mParent) {
mParent->UpdateSettingValue(aSettingValue);
mParent->UpdateSettingValueAndInvalidateOldValue(aSettingValue);
}
}
void InvalidateOldValue() {
mOldValue = nullptr;
if (mParent) {
mParent->InvalidateOldValue();
}
}
AutoTextControlHandlingState* mParent;
AutoTextControlHandlingState* const mParent;
TextControlState& mTextControlState;
// mTextCtrlElement grabs TextControlState::mTextCtrlElement since
// if the text control element releases mTextControlState, only this
// can guarantee the instance of the text control element.
nsCOMPtr<nsITextControlElement> mTextCtrlElement;
nsCOMPtr<nsITextControlElement> const mTextCtrlElement;
// mTextInputListener grabs TextControlState::mTextListener because if
// TextControlState is unbind from the frame, it's released.
RefPtr<TextInputListener> const mTextInputListener;
nsString mSettingValue;
TextControlAction mTextControlAction;
const nsAString* mOldValue = nullptr;
uint32_t mSetValueFlags = 0;
TextControlAction const mTextControlAction;
bool mTextControlStateDestroyed = false;
};
@@ -2387,29 +2431,29 @@ bool AreFlagsNotDemandingContradictingMovements(uint32_t aFlags) {
bool TextControlState::SetValue(const nsAString& aValue,
const nsAString* aOldValue, uint32_t aFlags) {
if (mHandlingState &&
mHandlingState->IsHandling(TextControlAction::CommitComposition)) {
// GetValue doesn't return current text frame's content during committing.
// So we cannot trust this old value
aOldValue = nullptr;
}
ErrorResult error;
AutoTextControlHandlingState handlingSetValue(
*this, TextControlAction::SetValue, aValue, error);
*this, TextControlAction::SetValue, aValue, aOldValue, aFlags, error);
if (error.Failed()) {
MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY));
error.SuppressException();
return false;
}
if (handlingSetValue.IsHandling(TextControlAction::CommitComposition)) {
// GetValue doesn't return current text frame's content during committing.
// So we cannot trust this old value
aOldValue = nullptr;
}
// 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.
if (aFlags & (eSetValue_BySetUserInput | eSetValue_ByContent)) {
if (EditorHasComposition()) {
// When this is called recursively, there shouldn't be composition.
if (NS_WARN_IF(handlingSetValue.IsHandling(
TextControlAction::CommitComposition))) {
if (handlingSetValue.IsHandling(TextControlAction::CommitComposition)) {
// Don't request to commit composition again. But if it occurs,
// we should skip to set the new value to the editor here. It should
// be set later with the newest value.
@@ -2437,9 +2481,6 @@ bool TextControlState::SetValue(const nsAString& aValue,
// script (see below).
return true;
}
// IME might commit composition, then change value, so we cannot
// trust old value from parameter.
aOldValue = nullptr;
}
// If there is composition, need to commit composition first because
// other browsers do that.
@@ -2448,12 +2489,12 @@ bool TextControlState::SetValue(const nsAString& aValue,
// is used during setting the value to the editor. IE also allows
// to set the editor value on the input event which is caused by
// forcibly committing composition.
AutoTextControlHandlingState handlingCommitComposition(
*this, TextControlAction::CommitComposition);
if (nsContentUtils::IsSafeToRunScript()) {
// WARNING: During this call, compositionupdate, compositionend, input
// events will be fired. Therefore, everything can occur. E.g., the
// document may be unloaded.
AutoTextControlHandlingState handlingCommitComposition(
*this, TextControlAction::CommitComposition);
RefPtr<TextEditor> textEditor = mTextEditor;
nsresult rv = textEditor->CommitComposition();
if (handlingCommitComposition.IsTextControlStateDestroyed()) {
@@ -2474,268 +2515,51 @@ bool TextControlState::SetValue(const nsAString& aValue,
}
}
// mTextCtrlElement may be cleared when we dispatch an event so that
// we should keep grabbing it with local variable.
nsCOMPtr<nsITextControlElement> textControlElement(mTextCtrlElement);
if (mTextEditor && mBoundFrame) {
// The InsertText call below might flush pending notifications, which
// The methods of mTextEditor might flush pending notifications, which
// could lead into a scheduled PrepareEditor to be called. That will
// lead to crashes (or worse) because we'd be initializing the editor
// before InsertText returns. This script blocker makes sure that
// PrepareEditor cannot be called prematurely.
nsAutoScriptBlocker scriptBlocker;
#ifdef DEBUG
if (IsSingleLineTextControl()) {
NS_ASSERTION(mEditorInitialized || handlingSetValue.IsHandling(
TextControlAction::PrepareEditor),
"We should never try to use the editor if we're not "
"initialized unless we're being initialized");
}
#endif
nsAutoString currentValue;
if (aOldValue) {
#ifdef DEBUG
mBoundFrame->GetText(currentValue);
MOZ_ASSERT(currentValue.Equals(*aOldValue));
#endif
currentValue.Assign(*aOldValue);
} else {
mBoundFrame->GetText(currentValue);
}
AutoWeakFrame weakFrame(mBoundFrame);
// this is necessary to avoid infinite recursion
if (currentValue != handlingSetValue.GetSettingValue()) {
RefPtr<TextEditor> textEditor = mTextEditor;
SetValueWithTextEditor(handlingSetValue);
nsCOMPtr<Document> document = textEditor->GetDocument();
if (NS_WARN_IF(!document)) {
if (!weakFrame.IsAlive()) {
// If the frame was destroyed because of a flush somewhere inside
// InsertText, mBoundFrame here will be nullptr. But it's also
// possible for the frame to go away because of another reason (such
// as deleting the existing selection -- see bug 574558), in which
// case we don't need to reset the value here.
if (mBoundFrame) {
return true;
}
// Time to mess with our security context... See comments in GetValue()
// for why this is needed. Note that we have to do this up here, because
// otherwise SelectAll() will fail.
{
AutoNoJSAPI nojsapi;
// FYI: It's safe to use raw pointer for selection here because
// SelectionBatcher will grab it with RefPtr.
Selection* selection = mSelCon->GetSelection(SelectionType::eNormal);
SelectionBatcher selectionBatcher(selection);
if (NS_WARN_IF(!weakFrame.IsAlive())) {
return true;
}
// get the flags, remove readonly, disabled and max-length,
// set the value, restore flags
{
AutoRestoreEditorState restoreState(textEditor);
if (aFlags & eSetValue_BySetUserInput) {
// If the caller inserts text as part of user input, for example,
// autocomplete, we need to replace the text as "insert string"
// because undo should cancel only this operation (i.e., previous
// transactions typed by user shouldn't be merged with this).
// In this case, we need to dispatch "input" event because
// web apps may need to know the user's operation.
// In this case, we need to dispatch "beforeinput" events since
// we're emulating the user's input. Passing nullptr as
// nsIPrincipal means that that may be user's input. So, let's
// do it.
DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(
handlingSetValue.GetSettingValue(), nullptr, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to set the new value");
} else {
// If we're emulating user input, we don't need to manage
// mTextListener state since it should use same path as user
// typing something. On the other hand, if we're setting value
// programatically, we need to manage mTextListener by ourselves
// and suppress "input" events.
mTextListener->SettingValue(true);
bool notifyValueChanged = !!(aFlags & eSetValue_Notify);
mTextListener->SetValueChanged(notifyValueChanged);
AutoInputEventSuppresser suppressInputEventDispatching(textEditor);
if (aFlags & eSetValue_ForXUL) {
// On XUL <textbox> element, we need to preserve existing undo
// transactions.
// XXX Do we really need to do such complicated optimization?
// This was landed for web pages which set <textarea> value
// per line (bug 518122). For example:
// for (;;) {
// textarea.value += oneLineText + "\n";
// }
// However, this path won't be used in web content anymore.
nsCOMPtr<nsISelectionController> kungFuDeathGrip = mSelCon.get();
uint32_t currentLength = currentValue.Length();
uint32_t newlength = handlingSetValue.GetSettingValue().Length();
if (!currentLength ||
!StringBeginsWith(handlingSetValue.GetSettingValue(),
currentValue)) {
// Replace the whole text.
currentLength = 0;
kungFuDeathGrip->SelectAll();
} else {
// Collapse selection to the end so that we can append data.
mBoundFrame->SelectAllOrCollapseToEndOfText(false);
}
const nsAString& insertValue =
StringTail(handlingSetValue.GetSettingValue(),
newlength - currentLength);
if (insertValue.IsEmpty()) {
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for
// now.
DebugOnly<nsresult> rv = textEditor->DeleteSelectionAsAction(
nsIEditor::eNone, nsIEditor::eStrip, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to remove the text");
} else {
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for
// now.
DebugOnly<nsresult> rv =
textEditor->InsertTextAsAction(insertValue, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to insert the new value");
}
} else {
// On <input> or <textarea>, we shouldn't preserve existing undo
// transactions because other browsers do not preserve them too
// and not preserving transactions makes setting value faster.
AutoDisableUndo disableUndo(textEditor);
if (selection) {
// Since we don't use undo transaction, we don't need to store
// selection state. SetText will set selection to tail.
// Note that textEditor will collapse selection to the end.
// Therefore, it's safe to use RemoveAllRangesTemporarily()
// here.
selection->RemoveAllRangesTemporarily();
}
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for now.
textEditor->SetTextAsAction(handlingSetValue.GetSettingValue(),
nullptr);
// Call the listener's HandleValueChanged() callback manually,
// since we don't use the transaction manager in this path and it
// could be that the editor would bypass calling the listener for
// that reason.
mTextListener->HandleValueChanged();
}
mTextListener->SetValueChanged(true);
mTextListener->SettingValue(false);
if (!notifyValueChanged) {
// Listener doesn't update frame, but it is required for
// placeholder
ValueWasChanged(true);
}
}
}
if (!weakFrame.IsAlive()) {
// If the frame was destroyed because of a flush somewhere inside
// InsertText, mBoundFrame here will be nullptr. But it's also
// possible for the frame to go away because of another reason (such
// as deleting the existing selection -- see bug 574558), in which
// case we don't need to reset the value here.
if (!mBoundFrame) {
return SetValue(handlingSetValue.GetSettingValue(),
aFlags & eSetValue_Notify);
}
return true;
}
// The new value never includes line breaks caused by hard-wrap.
// So, mCachedValue can always cache the new value.
if (!mBoundFrame->CacheValue(handlingSetValue.GetSettingValue(),
fallible)) {
return false;
}
}
}
} else {
if (!mValue) {
mValue.emplace();
}
// We can't just early-return here, because ValueWasChanged and
// OnValueChanged below still need to be called.
if (!mValue->Equals(handlingSetValue.GetSettingValue()) ||
!StaticPrefs::dom_input_skip_cursor_move_for_same_value_set()) {
if (!mValue->Assign(handlingSetValue.GetSettingValue(), fallible)) {
// XXX It's odd to drop flags except eSetValue_Notify. Probably, this
// intended to drop eSetValue_BySetUserInput and eSetValue_ByContent,
// but other flags are added later.
ErrorResult error;
AutoTextControlHandlingState handlingSetValueWithoutEditor(
*this, TextControlAction::SetValue,
handlingSetValue.GetSettingValue(), handlingSetValue.GetOldValue(),
handlingSetValue.GetSetValueFlags() & eSetValue_Notify, error);
if (error.Failed()) {
MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY));
error.SuppressException();
return false;
}
// Since we have no editor we presumably have cached selection state.
if (IsSelectionCached()) {
MOZ_ASSERT(AreFlagsNotDemandingContradictingMovements(aFlags));
SelectionProperties& props = GetSelectionProperties();
if (aFlags & eSetValue_MoveCursorToEndIfValueChanged) {
props.SetStart(handlingSetValue.GetSettingValue().Length());
props.SetEnd(handlingSetValue.GetSettingValue().Length());
props.SetDirection(nsITextControlFrame::eForward);
} else if (aFlags &
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(),
handlingSetValue.GetSettingValue().Length()));
props.SetEnd(std::min(props.GetEnd(),
handlingSetValue.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"
// 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.
if (aFlags & eSetValue_BySetUserInput) {
nsCOMPtr<Element> element = do_QueryInterface(textControlElement);
MOZ_ASSERT(element);
MOZ_ASSERT(!handlingSetValue.GetSettingValue().IsVoid());
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
element, EditorInputType::eInsertReplacementText, nullptr,
nsContentUtils::InputEventOptions(
handlingSetValue.GetSettingValue()));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
} else {
// Even if our value is not actually changing, apparently we need to mark
// our SelectionProperties dirty to make accessibility tests happy.
// Probably because they depend on the SetSelectionRange() call we make on
// our frame in RestoreSelectionState, but I have no idea why they do.
if (IsSelectionCached()) {
SelectionProperties& props = GetSelectionProperties();
props.SetIsDirty();
}
return SetValueWithoutTextEditor(handlingSetValueWithoutEditor);
}
// If we've reached the point where the root node has been created, we
// can assume that it's safe to notify.
ValueWasChanged(!!mBoundFrame);
// The new value never includes line breaks caused by hard-wrap.
// So, mCachedValue can always cache the new value.
if (!mBoundFrame->CacheValue(handlingSetValue.GetSettingValue(),
fallible)) {
return false;
}
} else if (!SetValueWithoutTextEditor(handlingSetValue)) {
return false;
}
// TODO(emilio): It seems wrong to pass ValueChangeKind::Script if
@@ -2745,7 +2569,256 @@ bool TextControlState::SetValue(const nsAString& aValue,
// XXX Should we stop notifying "value changed" if mTextCtrlElement has
// been cleared?
textControlElement->OnValueChanged(/* aNotify = */ !!mBoundFrame, changeKind);
handlingSetValue.GetTextControlElement()->OnValueChanged(
/* aNotify = */ !!mBoundFrame, changeKind);
return true;
}
void TextControlState::SetValueWithTextEditor(
AutoTextControlHandlingState& aHandlingSetValue) {
MOZ_ASSERT(aHandlingSetValue.Is(TextControlAction::SetValue));
MOZ_ASSERT(mTextEditor);
MOZ_ASSERT(mBoundFrame);
NS_WARNING_ASSERTION(!EditorHasComposition(),
"Failed to commit composition before setting value. "
"Investigate the cause!");
#ifdef DEBUG
if (IsSingleLineTextControl()) {
NS_ASSERTION(mEditorInitialized || aHandlingSetValue.IsHandling(
TextControlAction::PrepareEditor),
"We should never try to use the editor if we're not "
"initialized unless we're being initialized");
}
#endif
nsAutoString currentValue;
if (aHandlingSetValue.GetOldValue()) {
#ifdef DEBUG
mBoundFrame->GetText(currentValue);
MOZ_ASSERT(currentValue.Equals(*aHandlingSetValue.GetOldValue()));
#endif
currentValue.Assign(*aHandlingSetValue.GetOldValue());
} else {
mBoundFrame->GetText(currentValue);
}
AutoWeakFrame weakFrame(mBoundFrame);
// this is necessary to avoid infinite recursion
if (currentValue == aHandlingSetValue.GetSettingValue()) {
return;
}
RefPtr<TextEditor> textEditor = mTextEditor;
nsCOMPtr<Document> document = textEditor->GetDocument();
if (NS_WARN_IF(!document)) {
return;
}
// Time to mess with our security context... See comments in GetValue()
// for why this is needed. Note that we have to do this up here, because
// otherwise SelectAll() will fail.
AutoNoJSAPI nojsapi;
// FYI: It's safe to use raw pointer for selection here because
// SelectionBatcher will grab it with RefPtr.
Selection* selection = mSelCon->GetSelection(SelectionType::eNormal);
SelectionBatcher selectionBatcher(selection);
if (NS_WARN_IF(!weakFrame.IsAlive())) {
return;
}
// get the flags, remove readonly, disabled and max-length,
// set the value, restore flags
AutoRestoreEditorState restoreState(textEditor);
if (aHandlingSetValue.GetSetValueFlags() & eSetValue_BySetUserInput) {
// If the caller inserts text as part of user input, for example,
// autocomplete, we need to replace the text as "insert string"
// because undo should cancel only this operation (i.e., previous
// transactions typed by user shouldn't be merged with this).
// In this case, we need to dispatch "input" event because
// web apps may need to know the user's operation.
// In this case, we need to dispatch "beforeinput" events since
// we're emulating the user's input. Passing nullptr as
// nsIPrincipal means that that may be user's input. So, let's
// do it.
DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(
aHandlingSetValue.GetSettingValue(), nullptr, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to set the new value");
return;
}
// If we're emulating user input, we don't need to manage
// mTextListener state since it should use same path as user
// typing something. On the other hand, if we're setting value
// programatically, we need to manage mTextListener by ourselves
// and suppress "input" events.
mTextListener->SettingValue(true);
bool notifyValueChanged =
!!(aHandlingSetValue.GetSetValueFlags() & eSetValue_Notify);
mTextListener->SetValueChanged(notifyValueChanged);
AutoInputEventSuppresser suppressInputEventDispatching(textEditor);
if (aHandlingSetValue.GetSetValueFlags() & eSetValue_ForXUL) {
// On XUL <textbox> element, we need to preserve existing undo
// transactions.
// XXX Do we really need to do such complicated optimization?
// This was landed for web pages which set <textarea> value
// per line (bug 518122). For example:
// for (;;) {
// textarea.value += oneLineText + "\n";
// }
// However, this path won't be used in web content anymore.
nsCOMPtr<nsISelectionController> kungFuDeathGrip = mSelCon.get();
uint32_t currentLength = currentValue.Length();
uint32_t newlength = aHandlingSetValue.GetSettingValue().Length();
if (!currentLength ||
!StringBeginsWith(aHandlingSetValue.GetSettingValue(), currentValue)) {
// Replace the whole text.
currentLength = 0;
kungFuDeathGrip->SelectAll();
} else {
// Collapse selection to the end so that we can append data.
mBoundFrame->SelectAllOrCollapseToEndOfText(false);
}
const nsAString& insertValue = StringTail(
aHandlingSetValue.GetSettingValue(), newlength - currentLength);
if (insertValue.IsEmpty()) {
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for
// now.
DebugOnly<nsresult> rv = textEditor->DeleteSelectionAsAction(
nsIEditor::eNone, nsIEditor::eStrip, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove the text");
} else {
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for
// now.
DebugOnly<nsresult> rv =
textEditor->InsertTextAsAction(insertValue, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new value");
}
} else {
// On <input> or <textarea>, we shouldn't preserve existing undo
// transactions because other browsers do not preserve them too
// and not preserving transactions makes setting value faster.
AutoDisableUndo disableUndo(textEditor);
if (selection) {
// Since we don't use undo transaction, we don't need to store
// selection state. SetText will set selection to tail.
// Note that textEditor will collapse selection to the end.
// Therefore, it's safe to use RemoveAllRangesTemporarily()
// here.
selection->RemoveAllRangesTemporarily();
}
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for now.
textEditor->SetTextAsAction(aHandlingSetValue.GetSettingValue(), nullptr);
// Call the listener's HandleValueChanged() callback manually,
// since we don't use the transaction manager in this path and it
// could be that the editor would bypass calling the listener for
// that reason.
aHandlingSetValue.GetTextInputListener()->HandleValueChanged();
}
aHandlingSetValue.GetTextInputListener()->SetValueChanged(true);
aHandlingSetValue.GetTextInputListener()->SettingValue(false);
if (!notifyValueChanged) {
// Listener doesn't update frame, but it is required for
// placeholder
ValueWasChanged(true);
}
}
bool TextControlState::SetValueWithoutTextEditor(
AutoTextControlHandlingState& aHandlingSetValue) {
MOZ_ASSERT(aHandlingSetValue.Is(TextControlAction::SetValue));
MOZ_ASSERT(!mTextEditor || !mBoundFrame);
NS_WARNING_ASSERTION(!EditorHasComposition(),
"Failed to commit composition before setting value. "
"Investigate the cause!");
if (!mValue) {
mValue.emplace();
}
// We can't just early-return here, because ValueWasChanged and
// 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()));
}
}
// 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"
// 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.
if (aHandlingSetValue.GetSetValueFlags() & eSetValue_BySetUserInput) {
nsCOMPtr<Element> element =
do_QueryInterface(aHandlingSetValue.GetTextControlElement());
MOZ_ASSERT(element);
MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid());
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
element, EditorInputType::eInsertReplacementText, nullptr,
nsContentUtils::InputEventOptions(
aHandlingSetValue.GetSettingValue()));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
} else {
// Even if our value is not actually changing, apparently we need to mark
// our SelectionProperties dirty to make accessibility tests happy.
// Probably because they depend on the SetSelectionRange() call we make on
// our frame in RestoreSelectionState, but I have no idea why they do.
if (IsSelectionCached()) {
SelectionProperties& props = GetSelectionProperties();
props.SetIsDirty();
}
}
// If we've reached the point where the root node has been created, we
// can assume that it's safe to notify.
ValueWasChanged(!!mBoundFrame);
return true;
}