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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user