diff options
Diffstat (limited to 'dom/html/TextControlState.cpp')
-rw-r--r-- | dom/html/TextControlState.cpp | 3061 |
1 files changed, 3061 insertions, 0 deletions
diff --git a/dom/html/TextControlState.cpp b/dom/html/TextControlState.cpp new file mode 100644 index 0000000000..ce15368129 --- /dev/null +++ b/dom/html/TextControlState.cpp @@ -0,0 +1,3061 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TextControlState.h" +#include "mozilla/Attributes.h" +#include "mozilla/IMEContentObserver.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/TextInputListener.h" + +#include "nsCOMPtr.h" +#include "nsView.h" +#include "nsCaret.h" +#include "nsLayoutCID.h" +#include "nsITextControlFrame.h" +#include "nsContentCreatorFunctions.h" +#include "nsTextControlFrame.h" +#include "nsIControllers.h" +#include "nsIControllerContext.h" +#include "nsAttrValue.h" +#include "nsAttrValueInlines.h" +#include "nsGenericHTMLElement.h" +#include "nsIDOMEventListener.h" +#include "nsIWidget.h" +#include "nsIDocumentEncoder.h" +#include "nsPIDOMWindow.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/EventListenerManager.h" +#include "nsContentUtils.h" +#include "mozilla/Preferences.h" +#include "nsTextNode.h" +#include "nsIController.h" +#include "nsIScrollableFrame.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/InputEventOptions.h" +#include "mozilla/NativeKeyBindingsType.h" +#include "mozilla/PresShell.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/Text.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_ui.h" +#include "nsFrameSelection.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Telemetry.h" +#include "mozilla/ShortcutKeys.h" +#include "mozilla/KeyEventHandler.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/ScrollTypes.h" + +namespace mozilla { + +using namespace dom; +using ValueSetterOption = TextControlState::ValueSetterOption; +using ValueSetterOptions = TextControlState::ValueSetterOptions; +using SelectionDirection = nsITextControlFrame::SelectionDirection; + +/***************************************************************************** + * TextControlElement + *****************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(TextControlElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + TextControlElement, nsGenericHTMLFormControlElementWithState) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED( + TextControlElement, nsGenericHTMLFormControlElementWithState) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( + TextControlElement, nsGenericHTMLFormControlElementWithState) + +/*static*/ +bool TextControlElement::GetWrapPropertyEnum( + nsIContent* aContent, TextControlElement::nsHTMLTextWrap& aWrapProp) { + // soft is the default; "physical" defaults to soft as well because all other + // browsers treat it that way and there is no real reason to maintain physical + // and virtual as separate entities if no one else does. Only hard and off + // do anything different. + aWrapProp = eHTMLTextWrap_Soft; // the default + + if (!aContent->IsHTMLElement()) { + return false; + } + + static mozilla::dom::Element::AttrValuesArray strings[] = { + nsGkAtoms::HARD, nsGkAtoms::OFF, nullptr}; + switch (aContent->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::wrap, strings, eIgnoreCase)) { + case 0: + aWrapProp = eHTMLTextWrap_Hard; + break; + case 1: + aWrapProp = eHTMLTextWrap_Off; + break; + } + + return true; +} + +/*static*/ +already_AddRefed<TextControlElement> +TextControlElement::GetTextControlElementFromEditingHost(nsIContent* aHost) { + if (!aHost) { + return nullptr; + } + + RefPtr<TextControlElement> parent = + TextControlElement::FromNodeOrNull(aHost->GetParent()); + return parent.forget(); +} + +TextControlElement::FocusTristate TextControlElement::FocusState() { + // We can't be focused if we aren't in a (composed) document + Document* doc = GetComposedDoc(); + if (!doc) { + return FocusTristate::eUnfocusable; + } + + // first see if we are disabled or not. If disabled then do nothing. + if (IsDisabled()) { + return FocusTristate::eUnfocusable; + } + + return IsInActiveTab(doc) ? FocusTristate::eActiveWindow + : FocusTristate::eInactiveWindow; +} + +using ValueChangeKind = TextControlElement::ValueChangeKind; + +MOZ_CAN_RUN_SCRIPT inline nsresult SetEditorFlagsIfNecessary( + EditorBase& aEditorBase, uint32_t aFlags) { + if (aEditorBase.Flags() == aFlags) { + return NS_OK; + } + return aEditorBase.SetFlags(aFlags); +} + +/***************************************************************************** + * mozilla::AutoInputEventSuppresser + *****************************************************************************/ + +class MOZ_STACK_CLASS AutoInputEventSuppresser final { + public: + explicit AutoInputEventSuppresser(TextEditor* aTextEditor) + : mTextEditor(aTextEditor), + // To protect against a reentrant call to SetValue, we check whether + // another SetValue is already happening for this editor. If it is, + // we must wait until we unwind to re-enable oninput events. + mOuterTransaction(aTextEditor->IsSuppressingDispatchingInputEvent()) { + MOZ_ASSERT(mTextEditor); + mTextEditor->SuppressDispatchingInputEvent(true); + } + ~AutoInputEventSuppresser() { + mTextEditor->SuppressDispatchingInputEvent(mOuterTransaction); + } + + private: + RefPtr<TextEditor> mTextEditor; + bool mOuterTransaction; +}; + +/***************************************************************************** + * mozilla::RestoreSelectionState + *****************************************************************************/ + +class RestoreSelectionState : public Runnable { + public: + RestoreSelectionState(TextControlState* aState, nsTextControlFrame* aFrame) + : Runnable("RestoreSelectionState"), + mFrame(aFrame), + mTextControlState(aState) {} + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + if (!mTextControlState) { + return NS_OK; + } + + AutoHideSelectionChanges hideSelectionChanges( + mFrame->GetConstFrameSelection()); + + if (mFrame) { + // EnsureEditorInitialized and SetSelectionRange leads to + // Selection::AddRangeAndSelectFramesAndNotifyListeners which flushes + // Layout - need to block script to avoid nested PrepareEditor calls (bug + // 642800). + nsAutoScriptBlocker scriptBlocker; + mFrame->EnsureEditorInitialized(); + TextControlState::SelectionProperties& properties = + mTextControlState->GetSelectionProperties(); + if (properties.IsDirty()) { + mFrame->SetSelectionRange(properties.GetStart(), properties.GetEnd(), + properties.GetDirection()); + } + } + + if (mTextControlState) { + mTextControlState->FinishedRestoringSelection(); + } + return NS_OK; + } + + // Let the text editor tell us we're no longer relevant - avoids use of + // AutoWeakFrame + void Revoke() { + mFrame = nullptr; + mTextControlState = nullptr; + } + + private: + nsTextControlFrame* mFrame; + TextControlState* mTextControlState; +}; + +/***************************************************************************** + * mozilla::AutoRestoreEditorState + *****************************************************************************/ + +class MOZ_RAII AutoRestoreEditorState final { + public: + MOZ_CAN_RUN_SCRIPT explicit AutoRestoreEditorState(TextEditor* aTextEditor) + : mTextEditor(aTextEditor), + mSavedFlags(mTextEditor->Flags()), + mSavedMaxLength(mTextEditor->MaxTextLength()), + mSavedEchoingPasswordPrevented( + mTextEditor->EchoingPasswordPrevented()) { + MOZ_ASSERT(mTextEditor); + + // EditorBase::SetFlags() is a virtual method. Even though it does nothing + // if new flags and current flags are same, the calling cost causes + // appearing the method in profile. So, this class should check if it's + // necessary to call. + uint32_t flags = mSavedFlags; + flags &= ~nsIEditor::eEditorReadonlyMask; + if (mSavedFlags != flags) { + // It's aTextEditor and whose lifetime must be guaranteed by the caller. + MOZ_KnownLive(mTextEditor)->SetFlags(flags); + } + mTextEditor->PreventToEchoPassword(); + mTextEditor->SetMaxTextLength(-1); + } + + MOZ_CAN_RUN_SCRIPT ~AutoRestoreEditorState() { + if (!mSavedEchoingPasswordPrevented) { + mTextEditor->AllowToEchoPassword(); + } + mTextEditor->SetMaxTextLength(mSavedMaxLength); + // mTextEditor's lifetime must be guaranteed by owner of the instance + // since the constructor is marked as `MOZ_CAN_RUN_SCRIPT` and this is + // a stack only class. + SetEditorFlagsIfNecessary(MOZ_KnownLive(*mTextEditor), mSavedFlags); + } + + private: + TextEditor* mTextEditor; + uint32_t mSavedFlags; + int32_t mSavedMaxLength; + bool mSavedEchoingPasswordPrevented; +}; + +/***************************************************************************** + * mozilla::AutoDisableUndo + *****************************************************************************/ + +class MOZ_RAII AutoDisableUndo final { + public: + explicit AutoDisableUndo(TextEditor* aTextEditor) + : mTextEditor(aTextEditor), mNumberOfMaximumTransactions(0) { + MOZ_ASSERT(mTextEditor); + + mNumberOfMaximumTransactions = + mTextEditor ? mTextEditor->NumberOfMaximumTransactions() : 0; + DebugOnly<bool> disabledUndoRedo = mTextEditor->DisableUndoRedo(); + NS_WARNING_ASSERTION(disabledUndoRedo, + "Failed to disable undo/redo transactions"); + } + + ~AutoDisableUndo() { + // Don't change enable/disable of undo/redo if it's enabled after + // it's disabled by the constructor because we shouldn't change + // the maximum undo/redo count to the old value. + if (mTextEditor->IsUndoRedoEnabled()) { + return; + } + // If undo/redo was enabled, mNumberOfMaximumTransactions is -1 or lager + // than 0. Only when it's 0, it was disabled. + if (mNumberOfMaximumTransactions) { + DebugOnly<bool> enabledUndoRedo = + mTextEditor->EnableUndoRedo(mNumberOfMaximumTransactions); + NS_WARNING_ASSERTION(enabledUndoRedo, + "Failed to enable undo/redo transactions"); + } else { + DebugOnly<bool> disabledUndoRedo = mTextEditor->DisableUndoRedo(); + NS_WARNING_ASSERTION(disabledUndoRedo, + "Failed to disable undo/redo transactions"); + } + } + + private: + TextEditor* mTextEditor; + int32_t mNumberOfMaximumTransactions; +}; + +static bool SuppressEventHandlers(nsPresContext* aPresContext) { + bool suppressHandlers = false; + + if (aPresContext) { + // Right now we only suppress event handlers and controller manipulation + // when in a print preview or print context! + + // In the current implementation, we only paginate when + // printing or in print preview. + + suppressHandlers = aPresContext->IsPaginated(); + } + + return suppressHandlers; +} + +/***************************************************************************** + * mozilla::TextInputSelectionController + *****************************************************************************/ + +class TextInputSelectionController final : public nsSupportsWeakReference, + public nsISelectionController { + ~TextInputSelectionController() = default; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TextInputSelectionController, + nsISelectionController) + + TextInputSelectionController(PresShell* aPresShell, nsIContent* aLimiter); + + void SetScrollableFrame(nsIScrollableFrame* aScrollableFrame); + nsFrameSelection* GetConstFrameSelection() { return mFrameSelection; } + // Will return null if !mFrameSelection. + Selection* GetSelection(SelectionType aSelectionType); + + // NSISELECTIONCONTROLLER INTERFACES + NS_IMETHOD SetDisplaySelection(int16_t toggle) override; + NS_IMETHOD GetDisplaySelection(int16_t* _retval) override; + NS_IMETHOD SetSelectionFlags(int16_t aInEnable) override; + NS_IMETHOD GetSelectionFlags(int16_t* aOutEnable) override; + NS_IMETHOD GetSelectionFromScript(RawSelectionType aRawSelectionType, + Selection** aSelection) override; + Selection* GetSelection(RawSelectionType aRawSelectionType) override; + NS_IMETHOD ScrollSelectionIntoView(RawSelectionType aRawSelectionType, + int16_t aRegion, int16_t aFlags) override; + NS_IMETHOD RepaintSelection(RawSelectionType aRawSelectionType) override; + nsresult RepaintSelection(nsPresContext* aPresContext, + SelectionType aSelectionType); + NS_IMETHOD SetCaretEnabled(bool enabled) override; + NS_IMETHOD SetCaretReadOnly(bool aReadOnly) override; + NS_IMETHOD GetCaretEnabled(bool* _retval) override; + NS_IMETHOD GetCaretVisible(bool* _retval) override; + NS_IMETHOD SetCaretVisibilityDuringSelection(bool aVisibility) override; + NS_IMETHOD PhysicalMove(int16_t aDirection, int16_t aAmount, + bool aExtend) override; + NS_IMETHOD CharacterMove(bool aForward, bool aExtend) override; + NS_IMETHOD WordMove(bool aForward, bool aExtend) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD LineMove(bool aForward, + bool aExtend) override; + NS_IMETHOD IntraLineMove(bool aForward, bool aExtend) override; + MOZ_CAN_RUN_SCRIPT + NS_IMETHOD PageMove(bool aForward, bool aExtend) override; + NS_IMETHOD CompleteScroll(bool aForward) override; + MOZ_CAN_RUN_SCRIPT NS_IMETHOD CompleteMove(bool aForward, + bool aExtend) override; + NS_IMETHOD ScrollPage(bool aForward) override; + NS_IMETHOD ScrollLine(bool aForward) override; + NS_IMETHOD ScrollCharacter(bool aRight) override; + void SelectionWillTakeFocus() override; + void SelectionWillLoseFocus() override; + + private: + RefPtr<nsFrameSelection> mFrameSelection; + nsIScrollableFrame* mScrollFrame; + nsWeakPtr mPresShellWeak; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextInputSelectionController) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextInputSelectionController) +NS_INTERFACE_TABLE_HEAD(TextInputSelectionController) + NS_INTERFACE_TABLE(TextInputSelectionController, nsISelectionController, + nsISelectionDisplay, nsISupportsWeakReference) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(TextInputSelectionController) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WEAK(TextInputSelectionController, mFrameSelection) + +TextInputSelectionController::TextInputSelectionController( + PresShell* aPresShell, nsIContent* aLimiter) + : mScrollFrame(nullptr) { + if (aPresShell) { + bool accessibleCaretEnabled = + PresShell::AccessibleCaretEnabled(aLimiter->OwnerDoc()->GetDocShell()); + mFrameSelection = + new nsFrameSelection(aPresShell, aLimiter, accessibleCaretEnabled); + mPresShellWeak = do_GetWeakReference(aPresShell); + } +} + +void TextInputSelectionController::SetScrollableFrame( + nsIScrollableFrame* aScrollableFrame) { + mScrollFrame = aScrollableFrame; + if (!mScrollFrame && mFrameSelection) { + mFrameSelection->DisconnectFromPresShell(); + mFrameSelection = nullptr; + } +} + +Selection* TextInputSelectionController::GetSelection( + SelectionType aSelectionType) { + if (!mFrameSelection) { + return nullptr; + } + + return mFrameSelection->GetSelection(aSelectionType); +} + +NS_IMETHODIMP +TextInputSelectionController::SetDisplaySelection(int16_t aToggle) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + mFrameSelection->SetDisplaySelection(aToggle); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::GetDisplaySelection(int16_t* aToggle) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + *aToggle = mFrameSelection->GetDisplaySelection(); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::SetSelectionFlags(int16_t aToggle) { + return NS_OK; // stub this out. not used in input +} + +NS_IMETHODIMP +TextInputSelectionController::GetSelectionFlags(int16_t* aOutEnable) { + *aOutEnable = nsISelectionDisplay::DISPLAY_TEXT; + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::GetSelectionFromScript( + RawSelectionType aRawSelectionType, Selection** aSelection) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + + *aSelection = + mFrameSelection->GetSelection(ToSelectionType(aRawSelectionType)); + + // GetSelection() fails only when aRawSelectionType is invalid value. + if (!(*aSelection)) { + return NS_ERROR_INVALID_ARG; + } + + NS_ADDREF(*aSelection); + return NS_OK; +} + +Selection* TextInputSelectionController::GetSelection( + RawSelectionType aRawSelectionType) { + return GetSelection(ToSelectionType(aRawSelectionType)); +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollSelectionIntoView( + RawSelectionType aRawSelectionType, int16_t aRegion, int16_t aFlags) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->ScrollSelectionIntoView( + ToSelectionType(aRawSelectionType), aRegion, aFlags); +} + +NS_IMETHODIMP +TextInputSelectionController::RepaintSelection( + RawSelectionType aRawSelectionType) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->RepaintSelection(ToSelectionType(aRawSelectionType)); +} + +nsresult TextInputSelectionController::RepaintSelection( + nsPresContext* aPresContext, SelectionType aSelectionType) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->RepaintSelection(aSelectionType); +} + +NS_IMETHODIMP +TextInputSelectionController::SetCaretEnabled(bool enabled) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak); + if (!presShell) { + return NS_ERROR_FAILURE; + } + + // tell the pres shell to enable the caret, rather than settings its + // visibility directly. this way the presShell's idea of caret visibility is + // maintained. + presShell->SetCaretEnabled(enabled); + + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::SetCaretReadOnly(bool aReadOnly) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv; + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv); + if (!presShell) { + return NS_ERROR_FAILURE; + } + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) { + return NS_ERROR_FAILURE; + } + + if (!mFrameSelection) { + return NS_ERROR_FAILURE; + } + + Selection* selection = mFrameSelection->GetSelection(SelectionType::eNormal); + if (selection) { + caret->SetCaretReadOnly(aReadOnly); + } + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::GetCaretEnabled(bool* _retval) { + return GetCaretVisible(_retval); +} + +NS_IMETHODIMP +TextInputSelectionController::GetCaretVisible(bool* _retval) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv; + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv); + if (!presShell) { + return NS_ERROR_FAILURE; + } + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) { + return NS_ERROR_FAILURE; + } + *_retval = caret->IsVisible(); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::SetCaretVisibilityDuringSelection( + bool aVisibility) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv; + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv); + if (!presShell) { + return NS_ERROR_FAILURE; + } + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) { + return NS_ERROR_FAILURE; + } + Selection* selection = mFrameSelection->GetSelection(SelectionType::eNormal); + if (selection) { + caret->SetVisibilityDuringSelection(aVisibility); + } + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::PhysicalMove(int16_t aDirection, int16_t aAmount, + bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->PhysicalMove(aDirection, aAmount, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::CharacterMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->CharacterMove(aForward, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::WordMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->WordMove(aForward, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::LineMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + nsresult result = frameSelection->LineMove(aForward, aExtend); + if (NS_FAILED(result)) { + result = CompleteMove(aForward, aExtend); + } + return result; +} + +NS_IMETHODIMP +TextInputSelectionController::IntraLineMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->IntraLineMove(aForward, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::PageMove(bool aForward, bool aExtend) { + // expected behavior for PageMove is to scroll AND move the caret + // and to remain relative position of the caret in view. see Bug 4302. + if (mScrollFrame) { + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + nsIFrame* scrollFrame = do_QueryFrame(mScrollFrame); + // We won't scroll parent scrollable element of mScrollFrame. Therefore, + // this may be handled when mScrollFrame is completely outside of the view. + // In such case, user may be confused since they might have wanted to + // scroll a parent scrollable element. For making clearer which element + // handles PageDown/PageUp, we should move selection into view even if + // selection is not changed. + return frameSelection->PageMove(aForward, aExtend, scrollFrame, + nsFrameSelection::SelectionIntoView::Yes); + } + // Similarly, if there is no scrollable frame, we should move the editor + // frame into the view for making it clearer which element handles + // PageDown/PageUp. + return ScrollSelectionIntoView( + nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_FOCUS_REGION, + nsISelectionController::SCROLL_SYNCHRONOUS | + nsISelectionController::SCROLL_FOR_CARET_MOVE); +} + +NS_IMETHODIMP +TextInputSelectionController::CompleteScroll(bool aForward) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::WHOLE, + ScrollMode::Instant); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::CompleteMove(bool aForward, bool aExtend) { + if (NS_WARN_IF(!mFrameSelection)) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + + // grab the parent / root DIV for this text widget + nsIContent* parentDIV = frameSelection->GetLimiter(); + if (!parentDIV) { + return NS_ERROR_UNEXPECTED; + } + + // make the caret be either at the very beginning (0) or the very end + int32_t offset = 0; + CaretAssociationHint hint = CARET_ASSOCIATE_BEFORE; + if (aForward) { + offset = parentDIV->GetChildCount(); + + // Prevent the caret from being placed after the last + // BR node in the content tree! + + if (offset > 0) { + nsIContent* child = parentDIV->GetLastChild(); + + if (child->IsHTMLElement(nsGkAtoms::br)) { + --offset; + hint = CARET_ASSOCIATE_AFTER; // for Bug 106855 + } + } + } + + const RefPtr<nsIContent> pinnedParentDIV{parentDIV}; + const nsFrameSelection::FocusMode focusMode = + aExtend ? nsFrameSelection::FocusMode::kExtendSelection + : nsFrameSelection::FocusMode::kCollapseToNewPoint; + frameSelection->HandleClick(pinnedParentDIV, offset, offset, focusMode, hint); + + // if we got this far, attempt to scroll no matter what the above result is + return CompleteScroll(aForward); +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollPage(bool aForward) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::PAGES, + ScrollMode::Smooth); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollLine(bool aForward) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::LINES, + ScrollMode::Smooth); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollCharacter(bool aRight) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(aRight ? 1 : -1, 0), ScrollUnit::LINES, + ScrollMode::Smooth); + return NS_OK; +} + +void TextInputSelectionController::SelectionWillTakeFocus() { + if (mFrameSelection) { + if (PresShell* shell = mFrameSelection->GetPresShell()) { + shell->FrameSelectionWillTakeFocus(*mFrameSelection); + } + } +} + +void TextInputSelectionController::SelectionWillLoseFocus() { + if (mFrameSelection) { + if (PresShell* shell = mFrameSelection->GetPresShell()) { + shell->FrameSelectionWillLoseFocus(*mFrameSelection); + } + } +} + +/***************************************************************************** + * mozilla::TextInputListener + *****************************************************************************/ + +TextInputListener::TextInputListener(TextControlElement* aTxtCtrlElement) + : mFrame(nullptr), + mTxtCtrlElement(aTxtCtrlElement), + mTextControlState(aTxtCtrlElement ? aTxtCtrlElement->GetTextControlState() + : nullptr), + mSelectionWasCollapsed(true), + mHadUndoItems(false), + mHadRedoItems(false), + mSettingValue(false), + mSetValueChanged(true), + mListeningToSelectionChange(false) {} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextInputListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextInputListener) + +NS_INTERFACE_MAP_BEGIN(TextInputListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(TextInputListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(TextInputListener) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TextInputListener) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TextInputListener) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +void TextInputListener::OnSelectionChange(Selection& aSelection, + int16_t aReason) { + if (!mListeningToSelectionChange) { + return; + } + + AutoWeakFrame weakFrame = mFrame; + + // Fire the select event + // The specs don't exactly say when we should fire the select event. + // IE: Whenever you add/remove a character to/from the selection. Also + // each time for select all. Also if you get to the end of the text + // field you will get new event for each keypress or a continuous + // stream of events if you use the mouse. IE will fire select event + // when the selection collapses to nothing if you are holding down + // the shift or mouse button. + // Mozilla: If we have non-empty selection we will fire a new event for each + // keypress (or mouseup) if the selection changed. Mozilla will also + // create the event each time select all is called, even if + // everything was previously selected, because technically select all + // will first collapse and then extend. Mozilla will never create an + // event if the selection collapses to nothing. + // FYI: If you want to skip dispatching eFormSelect event and if there are no + // event listeners, you can refer + // nsPIDOMWindow::HasFormSelectEventListeners(), but be careful about + // some C++ event handlers, e.g., HTMLTextAreaElement::PostHandleEvent(). + bool collapsed = aSelection.IsCollapsed(); + if (!collapsed && (aReason & (nsISelectionListener::MOUSEUP_REASON | + nsISelectionListener::KEYPRESS_REASON | + nsISelectionListener::SELECTALL_REASON))) { + if (nsCOMPtr<nsIContent> content = mFrame->GetContent()) { + if (nsCOMPtr<Document> doc = content->GetComposedDoc()) { + if (RefPtr<PresShell> presShell = doc->GetPresShell()) { + nsEventStatus status = nsEventStatus_eIgnore; + WidgetEvent event(true, eFormSelect); + + presShell->HandleEventWithTarget(&event, mFrame, content, &status); + } + } + } + } + + // if the collapsed state did not change, don't fire notifications + if (collapsed == mSelectionWasCollapsed) { + return; + } + + mSelectionWasCollapsed = collapsed; + + if (!weakFrame.IsAlive() || !mFrame || + !nsContentUtils::IsFocusedContent(mFrame->GetContent())) { + return; + } + + UpdateTextInputCommands(u"select"_ns, &aSelection, aReason); +} + +MOZ_CAN_RUN_SCRIPT +static void DoCommandCallback(Command aCommand, void* aData) { + nsTextControlFrame* frame = static_cast<nsTextControlFrame*>(aData); + nsIContent* content = frame->GetContent(); + + nsCOMPtr<nsIControllers> controllers; + HTMLInputElement* input = HTMLInputElement::FromNode(content); + if (input) { + input->GetControllers(getter_AddRefs(controllers)); + } else { + HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(content); + + if (textArea) { + textArea->GetControllers(getter_AddRefs(controllers)); + } + } + + if (!controllers) { + NS_WARNING("Could not get controllers"); + return; + } + + const char* commandStr = WidgetKeyboardEvent::GetCommandStr(aCommand); + + nsCOMPtr<nsIController> controller; + controllers->GetControllerForCommand(commandStr, getter_AddRefs(controller)); + if (!controller) { + return; + } + + bool commandEnabled; + if (NS_WARN_IF(NS_FAILED( + controller->IsCommandEnabled(commandStr, &commandEnabled)))) { + return; + } + if (commandEnabled) { + controller->DoCommand(commandStr); + } +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +TextInputListener::HandleEvent(Event* aEvent) { + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + + if (!aEvent->IsTrusted()) { + return NS_OK; + } + + RefPtr<KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent(); + if (!keyEvent) { + return NS_ERROR_UNEXPECTED; + } + + WidgetKeyboardEvent* widgetKeyEvent = + aEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (!widgetKeyEvent) { + return NS_ERROR_UNEXPECTED; + } + + { + auto* input = HTMLInputElement::FromNode(mTxtCtrlElement); + if (input && input->StepsInputValue(*widgetKeyEvent)) { + // As an special case, don't handle key events that would step the value + // of our <input type=number>. + return NS_OK; + } + } + + auto ExecuteOurShortcutKeys = [&](TextControlElement& aTextControlElement) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> bool { + KeyEventHandler* keyHandlers = ShortcutKeys::GetHandlers( + aTextControlElement.IsTextArea() ? HandlerType::eTextArea + : HandlerType::eInput); + + RefPtr<nsAtom> eventTypeAtom = + ShortcutKeys::ConvertEventToDOMEventType(widgetKeyEvent); + for (KeyEventHandler* handler = keyHandlers; handler; + handler = handler->GetNextHandler()) { + if (!handler->EventTypeEquals(eventTypeAtom)) { + continue; + } + + if (!handler->KeyEventMatched(keyEvent, 0, IgnoreModifierState())) { + continue; + } + + // XXX Do we execute only one handler even if the handler neither stops + // propagation nor prevents default of the event? + nsresult rv = handler->ExecuteHandler(&aTextControlElement, aEvent); + if (NS_SUCCEEDED(rv)) { + return true; + } + } + return false; + }; + + auto ExecuteNativeKeyBindings = + [&](TextControlElement& aTextControlElement) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> bool { + if (widgetKeyEvent->mMessage != eKeyPress) { + return false; + } + + NativeKeyBindingsType nativeKeyBindingsType = + aTextControlElement.IsTextArea() + ? NativeKeyBindingsType::MultiLineEditor + : NativeKeyBindingsType::SingleLineEditor; + + nsIWidget* widget = widgetKeyEvent->mWidget; + // If the event is created by chrome script, the widget is nullptr. + if (MOZ_UNLIKELY(!widget)) { + widget = mFrame->GetNearestWidget(); + if (MOZ_UNLIKELY(NS_WARN_IF(!widget))) { + return false; + } + } + + // WidgetKeyboardEvent::ExecuteEditCommands() requires non-nullptr mWidget. + // If the event is created by chrome script, it is nullptr but we need to + // execute native key bindings. Therefore, we need to set widget to + // WidgetEvent::mWidget temporarily. + AutoRestore<nsCOMPtr<nsIWidget>> saveWidget(widgetKeyEvent->mWidget); + widgetKeyEvent->mWidget = widget; + if (widgetKeyEvent->ExecuteEditCommands(nativeKeyBindingsType, + DoCommandCallback, mFrame)) { + aEvent->PreventDefault(); + return true; + } + return false; + }; + + OwningNonNull<TextControlElement> textControlElement(*mTxtCtrlElement); + if (StaticPrefs:: + ui_key_textcontrol_prefer_native_key_bindings_over_builtin_shortcut_key_definitions()) { + if (!ExecuteNativeKeyBindings(textControlElement)) { + ExecuteOurShortcutKeys(textControlElement); + } + } else { + if (!ExecuteOurShortcutKeys(textControlElement)) { + ExecuteNativeKeyBindings(textControlElement); + } + } + return NS_OK; +} + +nsresult TextInputListener::OnEditActionHandled(TextEditor& aTextEditor) { + if (mFrame) { + // XXX Do we still need this or can we just remove the mFrame and + // frame.IsAlive() conditions below? + AutoWeakFrame weakFrame = mFrame; + + // Update the undo / redo menus + // + size_t numUndoItems = aTextEditor.NumberOfUndoItems(); + size_t numRedoItems = aTextEditor.NumberOfRedoItems(); + if ((numUndoItems && !mHadUndoItems) || (!numUndoItems && mHadUndoItems) || + (numRedoItems && !mHadRedoItems) || (!numRedoItems && mHadRedoItems)) { + // Modify the menu if undo or redo items are different + UpdateTextInputCommands(u"undo"_ns); + + mHadUndoItems = numUndoItems != 0; + mHadRedoItems = numRedoItems != 0; + } + + if (weakFrame.IsAlive()) { + HandleValueChanged(aTextEditor); + } + } + + return mTextControlState ? mTextControlState->OnEditActionHandled() : NS_OK; +} + +void TextInputListener::HandleValueChanged(TextEditor& aTextEditor) { + // Make sure we know we were changed (do NOT set this to false if there are + // no undo items; JS could change the value and we'd still need to save it) + if (mSetValueChanged) { + mTxtCtrlElement->SetValueChanged(true); + } + + if (!mSettingValue) { + // NOTE(emilio): execCommand might get here even though it might not be a + // "proper" user-interactive change. Might be worth reconsidering which + // ValueChangeKind are we passing down. + mTxtCtrlElement->OnValueChanged(ValueChangeKind::UserInteraction, + aTextEditor.IsEmpty(), nullptr); + if (mTextControlState) { + mTextControlState->ClearLastInteractiveValue(); + } + } +} + +nsresult TextInputListener::UpdateTextInputCommands( + const nsAString& aCommandsToUpdate, Selection* aSelection, + int16_t aReason) { + nsIContent* content = mFrame->GetContent(); + if (NS_WARN_IF(!content)) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<Document> doc = content->GetComposedDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + nsPIDOMWindowOuter* domWindow = doc->GetWindow(); + if (NS_WARN_IF(!domWindow)) { + return NS_ERROR_FAILURE; + } + domWindow->UpdateCommands(aCommandsToUpdate, aSelection, aReason); + return NS_OK; +} + +/***************************************************************************** + * mozilla::AutoTextControlHandlingState + * + * This class is temporarily created in the stack and can manage nested + * handling state of TextControlState. While this instance exists, lifetime of + * TextControlState which created the instance is guaranteed. In other words, + * you can use this class as "kungFuDeathGrip" for TextControlState. + *****************************************************************************/ + +enum class TextControlAction { + CommitComposition, + Destructor, + PrepareEditor, + SetRangeText, + SetSelectionRange, + SetValue, + UnbindFromFrame, + Unlink, +}; + +class MOZ_STACK_CLASS AutoTextControlHandlingState { + public: + AutoTextControlHandlingState() = delete; + explicit AutoTextControlHandlingState(const AutoTextControlHandlingState&) = + delete; + AutoTextControlHandlingState(AutoTextControlHandlingState&&) = delete; + void operator=(AutoTextControlHandlingState&) = delete; + void operator=(const AutoTextControlHandlingState&) = delete; + + /** + * Generic constructor. If TextControlAction does not require additional + * data, must use this constructor. + */ + MOZ_CAN_RUN_SCRIPT AutoTextControlHandlingState( + TextControlState& aTextControlState, TextControlAction aTextControlAction) + : mParent(aTextControlState.mHandlingState), + mTextControlState(aTextControlState), + mTextCtrlElement(aTextControlState.mTextCtrlElement), + mTextInputListener(aTextControlState.mTextListener), + mTextControlAction(aTextControlAction) { + MOZ_ASSERT(aTextControlAction != TextControlAction::SetValue, + "Use specific constructor"); + MOZ_DIAGNOSTIC_ASSERT_IF( + !aTextControlState.mTextListener, + !aTextControlState.mBoundFrame || !aTextControlState.mTextEditor); + 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(); + } + } + + /** + * TextControlAction::SetValue specific constructor. Current setting value + * must be specified and the creator should check whether we succeeded to + * allocate memory for line breaker conversion. + */ + MOZ_CAN_RUN_SCRIPT AutoTextControlHandlingState( + TextControlState& aTextControlState, TextControlAction aTextControlAction, + const nsAString& aSettingValue, const nsAString* aOldValue, + const ValueSetterOptions& aOptions, ErrorResult& aRv) + : mParent(aTextControlState.mHandlingState), + mTextControlState(aTextControlState), + mTextCtrlElement(aTextControlState.mTextCtrlElement), + mTextInputListener(aTextControlState.mTextListener), + mSettingValue(aSettingValue), + mOldValue(aOldValue), + mValueSetterOptions(aOptions), + mTextControlAction(aTextControlAction) { + MOZ_ASSERT(aTextControlAction == TextControlAction::SetValue, + "Use generic constructor"); + MOZ_DIAGNOSTIC_ASSERT_IF( + !aTextControlState.mTextListener, + !aTextControlState.mBoundFrame || !aTextControlState.mTextEditor); + mTextControlState.mHandlingState = this; + 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. + if (mParent) { + // If SetValue is nested, parents cannot trust their old value anymore. + // So, we need to clear them. + mParent->UpdateSettingValueAndInvalidateOldValue(mSettingValue); + } + } + + MOZ_CAN_RUN_SCRIPT ~AutoTextControlHandlingState() { + mTextControlState.mHandlingState = mParent; + if (!mParent && mTextControlStateDestroyed) { + mTextControlState.DeleteOrCacheForReuse(); + } + if (!mTextControlStateDestroyed && mPreareEditorLater) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + mTextControlState.PrepareEditor(); + } + } + + void OnDestroyTextControlState() { + if (IsHandling(TextControlAction::Destructor)) { + // Do nothing since mTextContrlState.DeleteOrCacheForReuse() has + // already been called. + return; + } + mTextControlStateDestroyed = true; + if (mParent) { + mParent->OnDestroyTextControlState(); + } + } + + void PrepareEditorLater() { + MOZ_ASSERT(IsHandling(TextControlAction::SetValue)); + MOZ_ASSERT(!IsHandling(TextControlAction::PrepareEditor)); + // Look for the top most SetValue. + AutoTextControlHandlingState* settingValue = nullptr; + for (AutoTextControlHandlingState* handlingSomething = this; + handlingSomething; handlingSomething = handlingSomething->mParent) { + if (handlingSomething->Is(TextControlAction::SetValue)) { + settingValue = handlingSomething; + } + } + settingValue->mPreareEditorLater = true; + } + + /** + * WillSetValueWithTextEditor() is called when TextControlState sets + * value with its mTextEditor. + */ + void WillSetValueWithTextEditor() { + MOZ_ASSERT(Is(TextControlAction::SetValue)); + MOZ_ASSERT(mTextControlState.mBoundFrame); + mTextControlFrame = mTextControlState.mBoundFrame; + // If we'reemulating user input, we don't need to manage mTextInputListener + // by ourselves since everything should be handled by TextEditor as normal + // user input. + if (mValueSetterOptions.contains(ValueSetterOption::BySetUserInputAPI)) { + return; + } + // Otherwise, if we're setting the value programatically, we need to manage + // mTextInputListener by ourselves since TextEditor users special path + // for the performance. + mTextInputListener->SettingValue(true); + mTextInputListener->SetValueChanged( + mValueSetterOptions.contains(ValueSetterOption::SetValueChanged)); + 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; + } + + /** + * OnEditActionHandled() is called when the TextEditor handles something + * and immediately before dispatching "input" event. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnEditActionHandled() { + MOZ_ASSERT(!mEditActionHandled); + mEditActionHandled = true; + if (!Is(TextControlAction::SetValue)) { + return NS_OK; + } + if (!mValueSetterOptions.contains(ValueSetterOption::BySetUserInputAPI)) { + mTextInputListener->SetValueChanged(true); + mTextInputListener->SettingValue( + mParent && mParent->IsHandling(TextControlAction::SetValue)); + } + if (!IsOriginalTextControlFrameAlive()) { + return SetValueWithoutTextEditorAgain() ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } + // The new value never includes line breaks caused by hard-wrap. + // So, mCachedValue can always cache the new value. + nsITextControlFrame* textControlFrame = + do_QueryFrame(mTextControlFrame.GetFrame()); + return static_cast<nsTextControlFrame*>(textControlFrame) + ->CacheValue(mSettingValue, fallible) + ? NS_OK + : NS_ERROR_OUT_OF_MEMORY; + } + + /** + * SetValueWithoutTextEditorAgain() should be called if the frame for + * mTextControlState was destroyed during setting value. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool SetValueWithoutTextEditorAgain() { + MOZ_ASSERT(!IsOriginalTextControlFrameAlive()); + // If the frame was destroyed because of a flush somewhere inside + // TextEditor, 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 (mTextControlState.mBoundFrame) { + return true; + } + // XXX It's odd to drop flags except + // ValueSetterOption::SetValueChanged. + // Probably, this intended to drop ValueSetterOption::BySetUserInputAPI + // and ValueSetterOption::ByContentAPI, but other flags are added later. + ErrorResult error; + AutoTextControlHandlingState handlingSetValueWithoutEditor( + mTextControlState, TextControlAction::SetValue, mSettingValue, + mOldValue, mValueSetterOptions & ValueSetterOption::SetValueChanged, + error); + if (error.Failed()) { + MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY)); + error.SuppressException(); + return false; + } + return mTextControlState.SetValueWithoutTextEditor( + handlingSetValueWithoutEditor); + } + + bool IsTextControlStateDestroyed() const { + return mTextControlStateDestroyed; + } + bool IsOriginalTextControlFrameAlive() const { + return const_cast<AutoTextControlHandlingState*>(this) + ->mTextControlFrame.IsAlive(); + } + bool HasEditActionHandled() const { return mEditActionHandled; } + bool HasBeforeInputEventDispatched() const { + return mBeforeInputEventHasBeenDispatched; + } + bool Is(TextControlAction aTextControlAction) const { + return mTextControlAction == aTextControlAction; + } + bool IsHandling(TextControlAction aTextControlAction) const { + if (mTextControlAction == aTextControlAction) { + return true; + } + return mParent ? mParent->IsHandling(aTextControlAction) : false; + } + TextControlElement* GetTextControlElement() const { return mTextCtrlElement; } + TextInputListener* GetTextInputListener() const { return mTextInputListener; } + const ValueSetterOptions& ValueSetterOptionsRef() const { + MOZ_ASSERT(Is(TextControlAction::SetValue)); + return mValueSetterOptions; + } + const nsAString* GetOldValue() const { + MOZ_ASSERT(Is(TextControlAction::SetValue)); + return mOldValue; + } + const nsString& GetSettingValue() const { + MOZ_ASSERT(IsHandling(TextControlAction::SetValue)); + if (mTextControlAction == TextControlAction::SetValue) { + return mSettingValue; + } + return mParent->GetSettingValue(); + } + + private: + void UpdateSettingValueAndInvalidateOldValue(const nsString& aSettingValue) { + if (mTextControlAction == TextControlAction::SetValue) { + mSettingValue = aSettingValue; + } + mOldValue = nullptr; + if (mParent) { + mParent->UpdateSettingValueAndInvalidateOldValue(aSettingValue); + } + } + void InvalidateOldValue() { + mOldValue = nullptr; + if (mParent) { + mParent->InvalidateOldValue(); + } + } + + AutoTextControlHandlingState* const mParent; + TextControlState& mTextControlState; + // mTextControlFrame should be set immediately before calling methods + // which may destroy the frame. Then, you can check whether the frame + // was destroyed/replaced. + AutoWeakFrame mTextControlFrame; + // mTextCtrlElement grabs TextControlState::mTextCtrlElement since + // if the text control element releases mTextControlState, only this + // can guarantee the instance of the text control element. + RefPtr<TextControlElement> const mTextCtrlElement; + // mTextInputListener grabs TextControlState::mTextListener because if + // TextControlState is unbind from the frame, it's released. + RefPtr<TextInputListener> const mTextInputListener; + nsString mSettingValue; + const nsAString* mOldValue = nullptr; + ValueSetterOptions mValueSetterOptions; + TextControlAction const mTextControlAction; + bool mTextControlStateDestroyed = false; + bool mEditActionHandled = false; + bool mPreareEditorLater = false; + bool mBeforeInputEventHasBeenDispatched = false; +}; + +/***************************************************************************** + * mozilla::TextControlState + *****************************************************************************/ + +/** + * For avoiding allocation cost of the instance, we should reuse instances + * as far as possible. + * + * FYI: `25` is just a magic number considered without enough investigation, + * but at least, this value must not make damage for footprint. + * Feel free to change it if you find better number. + */ +static constexpr size_t kMaxCountOfCacheToReuse = 25; +static AutoTArray<void*, kMaxCountOfCacheToReuse>* sReleasedInstances = nullptr; +static bool sHasShutDown = false; + +TextControlState::TextControlState(TextControlElement* aOwningElement) + : mTextCtrlElement(aOwningElement), + mEverInited(false), + mEditorInitialized(false), + mValueTransferInProgress(false), + mSelectionCached(true) +// When adding more member variable initializations here, add the same +// also to ::Construct. +{ + MOZ_COUNT_CTOR(TextControlState); + static_assert(sizeof(*this) <= 128, + "Please keep small TextControlState as far as possible"); +} + +TextControlState* TextControlState::Construct( + TextControlElement* aOwningElement) { + void* mem; + if (sReleasedInstances && !sReleasedInstances->IsEmpty()) { + mem = sReleasedInstances->PopLastElement(); + } else { + mem = moz_xmalloc(sizeof(TextControlState)); + } + + return new (mem) TextControlState(aOwningElement); +} + +TextControlState::~TextControlState() { + MOZ_ASSERT(!mHandlingState); + MOZ_COUNT_DTOR(TextControlState); + AutoTextControlHandlingState handlingDesctructor( + *this, TextControlAction::Destructor); + Clear(); +} + +void TextControlState::Shutdown() { + sHasShutDown = true; + if (sReleasedInstances) { + for (void* mem : *sReleasedInstances) { + free(mem); + } + delete sReleasedInstances; + } +} + +void TextControlState::Destroy() { + // If we're handling something, we should be deleted later. + if (mHandlingState) { + mHandlingState->OnDestroyTextControlState(); + return; + } + DeleteOrCacheForReuse(); + // Note that this instance may have already been deleted here. Don't touch + // any members. +} + +void TextControlState::DeleteOrCacheForReuse() { + MOZ_ASSERT(!IsBusy()); + + void* mem = this; + this->~TextControlState(); + + // If we can cache this instance, we should do it instead of deleting it. + if (!sHasShutDown && (!sReleasedInstances || sReleasedInstances->Length() < + kMaxCountOfCacheToReuse)) { + // Put this instance to the cache. Note that now, the array may be full, + // but it's not problem to cache more instances than kMaxCountOfCacheToReuse + // because it just requires reallocation cost of the array buffer. + if (!sReleasedInstances) { + sReleasedInstances = new AutoTArray<void*, kMaxCountOfCacheToReuse>; + } + sReleasedInstances->AppendElement(mem); + } else { + free(mem); + } +} + +nsresult TextControlState::OnEditActionHandled() { + return mHandlingState ? mHandlingState->OnEditActionHandled() : NS_OK; +} + +Element* TextControlState::GetRootNode() { + return mBoundFrame ? mBoundFrame->GetRootNode() : nullptr; +} + +Element* TextControlState::GetPreviewNode() { + return mBoundFrame ? mBoundFrame->GetPreviewNode() : nullptr; +} + +void TextControlState::Clear() { + MOZ_ASSERT(mHandlingState); + MOZ_ASSERT(mHandlingState->Is(TextControlAction::Destructor) || + mHandlingState->Is(TextControlAction::Unlink)); + if (mTextEditor) { + mTextEditor->SetTextInputListener(nullptr); + } + + if (mBoundFrame) { + // Oops, we still have a frame! + // This should happen when the type of a text input control is being changed + // to something which is not a text control. In this case, we should + // pretend that a frame is being destroyed, and clean up after ourselves + // properly. + UnbindFromFrame(mBoundFrame); + mTextEditor = nullptr; + } else { + // If we have a bound frame around, UnbindFromFrame will call DestroyEditor + // for us. + DestroyEditor(); + MOZ_DIAGNOSTIC_ASSERT(!mBoundFrame || !mTextEditor); + } + mTextListener = nullptr; +} + +void TextControlState::Unlink() { + AutoTextControlHandlingState handlingUnlink(*this, TextControlAction::Unlink); + UnlinkInternal(); +} + +void TextControlState::UnlinkInternal() { + MOZ_ASSERT(mHandlingState); + MOZ_ASSERT(mHandlingState->Is(TextControlAction::Unlink)); + TextControlState* tmp = this; + tmp->Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelCon) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextEditor) +} + +void TextControlState::Traverse(nsCycleCollectionTraversalCallback& cb) { + TextControlState* tmp = this; + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelCon) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextEditor) +} + +nsFrameSelection* TextControlState::GetConstFrameSelection() { + return mSelCon ? mSelCon->GetConstFrameSelection() : nullptr; +} + +TextEditor* TextControlState::GetTextEditor() { + // Note that if the instance is destroyed in PrepareEditor(), it returns + // NS_ERROR_NOT_INITIALIZED so that we don't need to create kungFuDeathGrip + // in this hot path. + if (!mTextEditor && NS_WARN_IF(NS_FAILED(PrepareEditor()))) { + return nullptr; + } + return mTextEditor; +} + +TextEditor* TextControlState::GetTextEditorWithoutCreation() { + return mTextEditor; +} + +nsISelectionController* TextControlState::GetSelectionController() const { + return mSelCon; +} + +// Helper class, used below in BindToFrame(). +class PrepareEditorEvent : public Runnable { + public: + PrepareEditorEvent(TextControlState& aState, nsIContent* aOwnerContent, + const nsAString& aCurrentValue) + : Runnable("PrepareEditorEvent"), + mState(&aState), + mOwnerContent(aOwnerContent), + mCurrentValue(aCurrentValue) { + aState.mValueTransferInProgress = true; + } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + if (NS_WARN_IF(!mState)) { + return NS_ERROR_NULL_POINTER; + } + + // Transfer the saved value to the editor if we have one + const nsAString* value = nullptr; + if (!mCurrentValue.IsEmpty()) { + value = &mCurrentValue; + } + + nsAutoScriptBlocker scriptBlocker; + + mState->PrepareEditor(value); + + mState->mValueTransferInProgress = false; + + return NS_OK; + } + + private: + WeakPtr<TextControlState> mState; + nsCOMPtr<nsIContent> mOwnerContent; // strong reference + nsAutoString mCurrentValue; +}; + +nsresult TextControlState::BindToFrame(nsTextControlFrame* aFrame) { + MOZ_ASSERT( + !nsContentUtils::IsSafeToRunScript(), + "TextControlState::BindToFrame() has to be called with script blocker"); + NS_ASSERTION(aFrame, "The frame to bind to should be valid"); + if (!aFrame) { + return NS_ERROR_INVALID_ARG; + } + + NS_ASSERTION(!mBoundFrame, "Cannot bind twice, need to unbind first"); + if (mBoundFrame) { + return NS_ERROR_FAILURE; + } + + // If we'll need to transfer our current value to the editor, save it before + // binding to the frame. + nsAutoString currentValue; + if (mTextEditor) { + GetValue(currentValue, true); + } + + mBoundFrame = aFrame; + + Element* rootNode = aFrame->GetRootNode(); + MOZ_ASSERT(rootNode); + + PresShell* presShell = aFrame->PresContext()->GetPresShell(); + MOZ_ASSERT(presShell); + + // Create a SelectionController + mSelCon = new TextInputSelectionController(presShell, rootNode); + MOZ_ASSERT(!mTextListener, "Should not overwrite the object"); + mTextListener = new TextInputListener(mTextCtrlElement); + + mTextListener->SetFrame(mBoundFrame); + + // Editor will override this as needed from InitializeSelection. + mSelCon->SetDisplaySelection(nsISelectionController::SELECTION_HIDDEN); + + // Get the caret and make it a selection listener. + // FYI: It's safe to use raw pointer for calling + // Selection::AddSelectionListner() because it only appends the listener + // to its internal array. + Selection* selection = mSelCon->GetSelection(SelectionType::eNormal); + if (selection) { + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (caret) { + selection->AddSelectionListener(caret); + } + mTextListener->StartToListenToSelectionChange(); + } + + // If an editor exists from before, prepare it for usage + if (mTextEditor) { + if (NS_WARN_IF(!mTextCtrlElement)) { + return NS_ERROR_FAILURE; + } + + // Set the correct direction on the newly created root node + if (mTextEditor->IsRightToLeft()) { + rootNode->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, u"rtl"_ns, false); + } else if (mTextEditor->IsLeftToRight()) { + rootNode->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, u"ltr"_ns, false); + } else { + // otherwise, inherit the content node's direction + } + + nsContentUtils::AddScriptRunner( + new PrepareEditorEvent(*this, mTextCtrlElement, currentValue)); + } + + return NS_OK; +} + +struct MOZ_STACK_CLASS PreDestroyer { + void Init(TextEditor* aTextEditor) { mTextEditor = aTextEditor; } + ~PreDestroyer() { + if (mTextEditor) { + // In this case, we don't need to restore the unmasked range of password + // editor. + UniquePtr<PasswordMaskData> passwordMaskData = mTextEditor->PreDestroy(); + } + } + void Swap(RefPtr<TextEditor>& aTextEditor) { + return mTextEditor.swap(aTextEditor); + } + + private: + RefPtr<TextEditor> mTextEditor; +}; + +nsresult TextControlState::PrepareEditor(const nsAString* aValue) { + if (!mBoundFrame) { + // Cannot create an editor without a bound frame. + // Don't return a failure code, because js callers can't handle that. + return NS_OK; + } + + if (mEditorInitialized) { + // Do not initialize the editor multiple times. + return NS_OK; + } + + AutoHideSelectionChanges hideSelectionChanges(GetConstFrameSelection()); + + if (mHandlingState) { + // Don't attempt to initialize recursively! + if (mHandlingState->IsHandling(TextControlAction::PrepareEditor)) { + return NS_ERROR_NOT_INITIALIZED; + } + // Reschedule creating editor later if we're setting value. + if (mHandlingState->IsHandling(TextControlAction::SetValue)) { + mHandlingState->PrepareEditorLater(); + return NS_ERROR_NOT_INITIALIZED; + } + } + + MOZ_ASSERT(mTextCtrlElement); + + AutoTextControlHandlingState preparingEditor( + *this, TextControlAction::PrepareEditor); + + // Note that we don't check mTextEditor here, because we might already have + // one around, in which case we don't create a new one, and we'll just tie + // the required machinery to it. + + nsPresContext* presContext = mBoundFrame->PresContext(); + PresShell* presShell = presContext->GetPresShell(); + + // Setup the editor flags + uint32_t editorFlags = nsIEditor::eEditorPlaintextMask; + if (IsSingleLineTextControl()) { + editorFlags |= nsIEditor::eEditorSingleLineMask; + } + if (IsPasswordTextControl()) { + editorFlags |= nsIEditor::eEditorPasswordMask; + } + + // Spell check is diabled at creation time. It is enabled once + // the editor comes into focus. + editorFlags |= nsIEditor::eEditorSkipSpellCheck; + + bool shouldInitializeEditor = false; + RefPtr<TextEditor> newTextEditor; // the editor that we might create + PreDestroyer preDestroyer; + if (!mTextEditor) { + shouldInitializeEditor = true; + + // Create an editor + newTextEditor = new TextEditor(); + preDestroyer.Init(newTextEditor); + + // Make sure we clear out the non-breaking space before we initialize the + // editor + nsresult rv = mBoundFrame->UpdateValueDisplay(true, true); + if (NS_FAILED(rv)) { + NS_WARNING("nsTextControlFrame::UpdateValueDisplay() failed"); + return rv; + } + } else { + if (aValue || !mEditorInitialized) { + // Set the correct value in the root node + nsresult rv = + mBoundFrame->UpdateValueDisplay(true, !mEditorInitialized, aValue); + if (NS_FAILED(rv)) { + NS_WARNING("nsTextControlFrame::UpdateValueDisplay() failed"); + return rv; + } + } + + newTextEditor = mTextEditor; // just pretend that we have a new editor! + + // Don't lose application flags in the process. + if (newTextEditor->IsMailEditor()) { + editorFlags |= nsIEditor::eEditorMailMask; + } + } + + // Get the current value of the textfield from the content. + // Note that if we've created a new editor, mTextEditor is null at this stage, + // so we will get the real value from the content. + nsAutoString defaultValue; + if (aValue) { + defaultValue = *aValue; + } else { + GetValue(defaultValue, true); + } + + if (!mEditorInitialized) { + // Now initialize the editor. + // + // NOTE: Conversion of '\n' to <BR> happens inside the + // editor's Init() call. + + // Get the DOM document + nsCOMPtr<Document> doc = presShell->GetDocument(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + + // What follows is a bit of a hack. The editor uses the public DOM APIs + // for its content manipulations, and it causes it to fail some security + // checks deep inside when initializing. So we explictly make it clear that + // we're native code. + // Note that any script that's directly trying to access our value + // has to be going through some scriptable object to do that and that + // already does the relevant security checks. + AutoNoJSAPI nojsapi; + + RefPtr<Element> anonymousDivElement = GetRootNode(); + if (NS_WARN_IF(!anonymousDivElement) || NS_WARN_IF(!mSelCon)) { + return NS_ERROR_FAILURE; + } + OwningNonNull<TextInputSelectionController> selectionController(*mSelCon); + UniquePtr<PasswordMaskData> passwordMaskData; + if (editorFlags & nsIEditor::eEditorPasswordMask) { + if (mPasswordMaskData) { + passwordMaskData = std::move(mPasswordMaskData); + } else { + passwordMaskData = MakeUnique<PasswordMaskData>(); + } + } else { + mPasswordMaskData = nullptr; + } + nsresult rv = + newTextEditor->Init(*doc, *anonymousDivElement, selectionController, + editorFlags, std::move(passwordMaskData)); + if (NS_FAILED(rv)) { + NS_WARNING("TextEditor::Init() failed"); + return rv; + } + } + + // Initialize the controller for the editor + + nsresult rv = NS_OK; + if (!SuppressEventHandlers(presContext)) { + nsCOMPtr<nsIControllers> controllers; + if (HTMLInputElement* inputElement = + HTMLInputElement::FromNodeOrNull(mTextCtrlElement)) { + nsresult rv = inputElement->GetControllers(getter_AddRefs(controllers)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + HTMLTextAreaElement* textAreaElement = + HTMLTextAreaElement::FromNodeOrNull(mTextCtrlElement); + if (!textAreaElement) { + return NS_ERROR_FAILURE; + } + + nsresult rv = + textAreaElement->GetControllers(getter_AddRefs(controllers)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (controllers) { + // XXX Oddly, nsresult value is overwritten in the following loop, and + // only the last result or `found` decides the value. + uint32_t numControllers; + bool found = false; + rv = controllers->GetControllerCount(&numControllers); + for (uint32_t i = 0; i < numControllers; i++) { + nsCOMPtr<nsIController> controller; + rv = controllers->GetControllerAt(i, getter_AddRefs(controller)); + if (NS_SUCCEEDED(rv) && controller) { + nsCOMPtr<nsIControllerContext> editController = + do_QueryInterface(controller); + if (editController) { + editController->SetCommandContext( + static_cast<nsIEditor*>(newTextEditor)); + found = true; + } + } + } + if (!found) { + rv = NS_ERROR_FAILURE; + } + } + } + + // Initialize the plaintext editor + if (shouldInitializeEditor) { + const int32_t wrapCols = GetWrapCols(); + MOZ_ASSERT(wrapCols >= 0); + newTextEditor->SetWrapColumn(wrapCols); + } + + // Set max text field length + newTextEditor->SetMaxTextLength(mTextCtrlElement->UsedMaxLength()); + + editorFlags = newTextEditor->Flags(); + + // Check if the readonly attribute is set. + // + // TODO: Should probably call IsDisabled(), as it is cheaper. + if (mTextCtrlElement->HasAttr(kNameSpaceID_None, nsGkAtoms::readonly) || + mTextCtrlElement->HasAttr(kNameSpaceID_None, nsGkAtoms::disabled)) { + editorFlags |= nsIEditor::eEditorReadonlyMask; + } + + SetEditorFlagsIfNecessary(*newTextEditor, editorFlags); + + if (shouldInitializeEditor) { + // Hold on to the newly created editor + preDestroyer.Swap(mTextEditor); + } + + // If we have a default value, insert it under the div we created + // above, but be sure to use the editor so that '*' characters get + // displayed for password fields, etc. SetValue() will call the + // editor for us. + + if (!defaultValue.IsEmpty()) { + // XXX rv may store error code which indicates there is no controller. + // However, we overwrite it only in this case. + rv = SetEditorFlagsIfNecessary(*newTextEditor, editorFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now call SetValue() which will make the necessary editor calls to set + // the default value. Make sure to turn off undo before setting the default + // value, and turn it back on afterwards. This will make sure we can't undo + // past the default value. + // So, we use ValueSetterOption::ByInternalAPI only that it will turn off + // undo. + + if (NS_WARN_IF(!SetValue(defaultValue, ValueSetterOption::ByInternalAPI))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Now restore the original editor flags. + rv = SetEditorFlagsIfNecessary(*newTextEditor, editorFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + DebugOnly<bool> enabledUndoRedo = + newTextEditor->EnableUndoRedo(TextControlElement::DEFAULT_UNDO_CAP); + NS_WARNING_ASSERTION(enabledUndoRedo, + "Failed to enable undo/redo transaction"); + + if (!mEditorInitialized) { + newTextEditor->PostCreate(); + mEverInited = true; + mEditorInitialized = true; + } + + if (mTextListener) { + newTextEditor->SetTextInputListener(mTextListener); + } + + // Restore our selection after being bound to a new frame + if (mSelectionCached) { + if (mRestoringSelection) { // paranoia + mRestoringSelection->Revoke(); + } + mRestoringSelection = new RestoreSelectionState(this, mBoundFrame); + if (mRestoringSelection) { + nsContentUtils::AddScriptRunner(mRestoringSelection); + } + } + + // The selection cache is no longer going to be valid. + // + // XXXbz Shouldn't we do this at the point when we're actually about to + // restore the properties or something? As things stand, if UnbindFromFrame + // happens before our RestoreSelectionState runs, it looks like we'll lose our + // selection info, because we will think we don't have it cached and try to + // read it from the selection controller, which will not have it yet. + mSelectionCached = false; + + return preparingEditor.IsTextControlStateDestroyed() + ? NS_ERROR_NOT_INITIALIZED + : rv; +} + +void TextControlState::FinishedRestoringSelection() { + mRestoringSelection = nullptr; +} + +void TextControlState::SyncUpSelectionPropertiesBeforeDestruction() { + if (mBoundFrame) { + UnbindFromFrame(mBoundFrame); + } +} + +void TextControlState::SetSelectionProperties( + TextControlState::SelectionProperties& aProps) { + if (mBoundFrame) { + mBoundFrame->SetSelectionRange(aProps.GetStart(), aProps.GetEnd(), + aProps.GetDirection()); + // The instance may have already been deleted here. + } else { + mSelectionProperties = aProps; + } +} + +void TextControlState::GetSelectionRange(uint32_t* aSelectionStart, + uint32_t* aSelectionEnd, + ErrorResult& aRv) { + MOZ_ASSERT(aSelectionStart); + MOZ_ASSERT(aSelectionEnd); + MOZ_ASSERT(IsSelectionCached() || GetSelectionController(), + "How can we not have a cached selection if we have no selection " + "controller?"); + + // Note that we may have both IsSelectionCached() _and_ + // GetSelectionController() if we haven't initialized our editor yet. + if (IsSelectionCached()) { + const SelectionProperties& props = GetSelectionProperties(); + *aSelectionStart = props.GetStart(); + *aSelectionEnd = props.GetEnd(); + return; + } + + Selection* sel = mSelCon->GetSelection(SelectionType::eNormal); + if (NS_WARN_IF(!sel)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + Element* root = GetRootNode(); + if (NS_WARN_IF(!root)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + nsContentUtils::GetSelectionInTextControl(sel, root, *aSelectionStart, + *aSelectionEnd); +} + +SelectionDirection TextControlState::GetSelectionDirection(ErrorResult& aRv) { + MOZ_ASSERT(IsSelectionCached() || GetSelectionController(), + "How can we not have a cached selection if we have no selection " + "controller?"); + + // Note that we may have both IsSelectionCached() _and_ + // GetSelectionController() if we haven't initialized our editor yet. + if (IsSelectionCached()) { + return GetSelectionProperties().GetDirection(); + } + + Selection* sel = mSelCon->GetSelection(SelectionType::eNormal); + if (NS_WARN_IF(!sel)) { + aRv.Throw(NS_ERROR_FAILURE); + return SelectionDirection::Forward; + } + + nsDirection direction = sel->GetDirection(); + if (direction == eDirNext) { + return SelectionDirection::Forward; + } + + MOZ_ASSERT(direction == eDirPrevious); + return SelectionDirection::Backward; +} + +void TextControlState::SetSelectionRange(uint32_t aStart, uint32_t aEnd, + SelectionDirection aDirection, + ErrorResult& aRv, + ScrollAfterSelection aScroll) { + MOZ_ASSERT(IsSelectionCached() || mBoundFrame, + "How can we have a non-cached selection but no frame?"); + + AutoTextControlHandlingState handlingSetSelectionRange( + *this, TextControlAction::SetSelectionRange); + + if (aStart > aEnd) { + aStart = aEnd; + } + + if (!IsSelectionCached()) { + MOZ_ASSERT(mBoundFrame, "Our frame should still be valid"); + aRv = mBoundFrame->SetSelectionRange(aStart, aEnd, aDirection); + if (aRv.Failed() || + handlingSetSelectionRange.IsTextControlStateDestroyed()) { + return; + } + if (aScroll == ScrollAfterSelection::Yes && mBoundFrame) { + // mBoundFrame could be gone if selection listeners flushed layout for + // example. + mBoundFrame->ScrollSelectionIntoViewAsync(); + } + return; + } + + SelectionProperties& props = GetSelectionProperties(); + if (!props.HasMaxLength()) { + // A clone without a dirty value flag may not have a max length yet + nsAutoString value; + GetValue(value, false); + props.SetMaxLength(value.Length()); + } + + bool changed = props.SetStart(aStart); + changed |= props.SetEnd(aEnd); + changed |= props.SetDirection(aDirection); + + if (!changed) { + return; + } + + // It sure would be nice if we had an existing Element* or so to work with. + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(mTextCtrlElement, eFormSelect, CanBubble::eYes); + asyncDispatcher->PostDOMEvent(); + + // SelectionChangeEventDispatcher covers this when !IsSelectionCached(). + // XXX(krosylight): Shouldn't it fire before select event? + // Currently Gecko and Blink both fire selectionchange after select. + if (IsSelectionCached() && + StaticPrefs::dom_select_events_textcontrols_selectionchange_enabled()) { + asyncDispatcher = new AsyncEventDispatcher( + mTextCtrlElement, eSelectionChange, CanBubble::eYes); + asyncDispatcher->PostDOMEvent(); + } +} + +void TextControlState::SetSelectionStart(const Nullable<uint32_t>& aStart, + ErrorResult& aRv) { + uint32_t start = 0; + if (!aStart.IsNull()) { + start = aStart.Value(); + } + + uint32_t ignored, end; + GetSelectionRange(&ignored, &end, aRv); + if (aRv.Failed()) { + return; + } + + SelectionDirection dir = GetSelectionDirection(aRv); + if (aRv.Failed()) { + return; + } + + if (end < start) { + end = start; + } + + SetSelectionRange(start, end, dir, aRv); + // The instance may have already been deleted here. +} + +void TextControlState::SetSelectionEnd(const Nullable<uint32_t>& aEnd, + ErrorResult& aRv) { + uint32_t end = 0; + if (!aEnd.IsNull()) { + end = aEnd.Value(); + } + + uint32_t start, ignored; + GetSelectionRange(&start, &ignored, aRv); + if (aRv.Failed()) { + return; + } + + SelectionDirection dir = GetSelectionDirection(aRv); + if (aRv.Failed()) { + return; + } + + SetSelectionRange(start, end, dir, aRv); + // The instance may have already been deleted here. +} + +static void DirectionToName(SelectionDirection dir, nsAString& aDirection) { + switch (dir) { + case SelectionDirection::None: + // TODO(mbrodesser): this should be supported, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1541454. + NS_WARNING("We don't actually support this... how did we get it?"); + return aDirection.AssignLiteral("none"); + case SelectionDirection::Forward: + return aDirection.AssignLiteral("forward"); + case SelectionDirection::Backward: + return aDirection.AssignLiteral("backward"); + } + MOZ_ASSERT_UNREACHABLE("Invalid SelectionDirection value"); +} + +void TextControlState::GetSelectionDirectionString(nsAString& aDirection, + ErrorResult& aRv) { + SelectionDirection dir = GetSelectionDirection(aRv); + if (aRv.Failed()) { + return; + } + DirectionToName(dir, aDirection); +} + +static SelectionDirection DirectionStringToSelectionDirection( + const nsAString& aDirection) { + if (aDirection.EqualsLiteral("backward")) { + return SelectionDirection::Backward; + } + // We don't support directionless selections, see bug 1541454. + return SelectionDirection::Forward; +} + +void TextControlState::SetSelectionDirection(const nsAString& aDirection, + ErrorResult& aRv) { + SelectionDirection dir = DirectionStringToSelectionDirection(aDirection); + + uint32_t start, end; + GetSelectionRange(&start, &end, aRv); + if (aRv.Failed()) { + return; + } + + SetSelectionRange(start, end, dir, aRv); + // The instance may have already been deleted here. +} + +static SelectionDirection DirectionStringToSelectionDirection( + const Optional<nsAString>& aDirection) { + if (!aDirection.WasPassed()) { + // We don't support directionless selections. + return SelectionDirection::Forward; + } + + return DirectionStringToSelectionDirection(aDirection.Value()); +} + +void TextControlState::SetSelectionRange(uint32_t aSelectionStart, + uint32_t aSelectionEnd, + const Optional<nsAString>& aDirection, + ErrorResult& aRv, + ScrollAfterSelection aScroll) { + SelectionDirection dir = DirectionStringToSelectionDirection(aDirection); + + SetSelectionRange(aSelectionStart, aSelectionEnd, dir, aRv, aScroll); + // The instance may have already been deleted here. +} + +void TextControlState::SetRangeText(const nsAString& aReplacement, + ErrorResult& aRv) { + uint32_t start, end; + GetSelectionRange(&start, &end, aRv); + if (aRv.Failed()) { + return; + } + + SetRangeText(aReplacement, start, end, SelectionMode::Preserve, aRv, + Some(start), Some(end)); + // The instance may have already been deleted here. +} + +void TextControlState::SetRangeText(const nsAString& aReplacement, + uint32_t aStart, uint32_t aEnd, + SelectionMode aSelectMode, ErrorResult& aRv, + const Maybe<uint32_t>& aSelectionStart, + const Maybe<uint32_t>& aSelectionEnd) { + if (aStart > aEnd) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + AutoTextControlHandlingState handlingSetRangeText( + *this, TextControlAction::SetRangeText); + + nsAutoString value; + mTextCtrlElement->GetValueFromSetRangeText(value); + uint32_t inputValueLength = value.Length(); + + if (aStart > inputValueLength) { + aStart = inputValueLength; + } + + if (aEnd > inputValueLength) { + aEnd = inputValueLength; + } + + uint32_t selectionStart, selectionEnd; + if (!aSelectionStart) { + MOZ_ASSERT(!aSelectionEnd); + GetSelectionRange(&selectionStart, &selectionEnd, aRv); + if (aRv.Failed()) { + return; + } + } else { + MOZ_ASSERT(aSelectionEnd); + selectionStart = *aSelectionStart; + selectionEnd = *aSelectionEnd; + } + + // Batch selectionchanges from SetValueFromSetRangeText and SetSelectionRange + Selection* selection = + mSelCon ? mSelCon->GetSelection(SelectionType::eNormal) : nullptr; + SelectionBatcher selectionBatcher( + selection, __FUNCTION__, + nsISelectionListener::JS_REASON); // no-op if nullptr + + MOZ_ASSERT(aStart <= aEnd); + value.Replace(aStart, aEnd - aStart, aReplacement); + nsresult rv = + MOZ_KnownLive(mTextCtrlElement)->SetValueFromSetRangeText(value); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + uint32_t newEnd = aStart + aReplacement.Length(); + int32_t delta = aReplacement.Length() - (aEnd - aStart); + + switch (aSelectMode) { + case SelectionMode::Select: + selectionStart = aStart; + selectionEnd = newEnd; + break; + case SelectionMode::Start: + selectionStart = selectionEnd = aStart; + break; + case SelectionMode::End: + selectionStart = selectionEnd = newEnd; + break; + case SelectionMode::Preserve: + if (selectionStart > aEnd) { + selectionStart += delta; + } else if (selectionStart > aStart) { + selectionStart = aStart; + } + + if (selectionEnd > aEnd) { + selectionEnd += delta; + } else if (selectionEnd > aStart) { + selectionEnd = newEnd; + } + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown mode!"); + } + + SetSelectionRange(selectionStart, selectionEnd, Optional<nsAString>(), aRv); + if (IsSelectionCached()) { + // SetValueFromSetRangeText skipped SetMaxLength, set it here properly + GetSelectionProperties().SetMaxLength(value.Length()); + } +} + +void TextControlState::DestroyEditor() { + // notify the editor that we are going away + if (mEditorInitialized) { + // FYI: TextEditor checks whether it's destroyed or not immediately after + // changes the DOM tree or selection so that it's safe to call + // PreDestroy() here even while we're handling actions with + // mTextEditor. + MOZ_ASSERT(!mPasswordMaskData); + RefPtr<TextEditor> textEditor = mTextEditor; + mPasswordMaskData = textEditor->PreDestroy(); + MOZ_ASSERT_IF(mPasswordMaskData, !mPasswordMaskData->mTimer); + mEditorInitialized = false; + } +} + +void TextControlState::UnbindFromFrame(nsTextControlFrame* aFrame) { + if (NS_WARN_IF(!mBoundFrame)) { + return; + } + + // If it was, however, it should be unbounded from the same frame. + MOZ_ASSERT(aFrame == mBoundFrame, "Unbinding from the wrong frame"); + if (aFrame && aFrame != mBoundFrame) { + return; + } + + AutoTextControlHandlingState handlingUnbindFromFrame( + *this, TextControlAction::UnbindFromFrame); + + if (mSelCon) { + mSelCon->SelectionWillLoseFocus(); + } + + // We need to start storing the value outside of the editor if we're not + // going to use it anymore, so retrieve it for now. + nsAutoString value; + GetValue(value, true); + + if (mRestoringSelection) { + mRestoringSelection->Revoke(); + mRestoringSelection = nullptr; + } + + // Save our selection state if needed. + // Note that GetSelectionRange will attempt to work with our selection + // controller, so we should make sure we do it before we start doing things + // like destroying our editor (if we have one), tearing down the selection + // controller, and so forth. + if (!IsSelectionCached()) { + // Go ahead and cache it now. + uint32_t start = 0, end = 0; + GetSelectionRange(&start, &end, IgnoreErrors()); + + nsITextControlFrame::SelectionDirection direction = + GetSelectionDirection(IgnoreErrors()); + + SelectionProperties& props = GetSelectionProperties(); + props.SetMaxLength(value.Length()); + props.SetStart(start); + props.SetEnd(end); + props.SetDirection(direction); + mSelectionCached = true; + } + + // Destroy our editor + DestroyEditor(); + + // Clean up the controller + if (!SuppressEventHandlers(mBoundFrame->PresContext())) { + nsCOMPtr<nsIControllers> controllers; + if (HTMLInputElement* inputElement = + HTMLInputElement::FromNodeOrNull(mTextCtrlElement)) { + inputElement->GetControllers(getter_AddRefs(controllers)); + } else { + HTMLTextAreaElement* textAreaElement = + HTMLTextAreaElement::FromNodeOrNull(mTextCtrlElement); + if (textAreaElement) { + textAreaElement->GetControllers(getter_AddRefs(controllers)); + } + } + + if (controllers) { + uint32_t numControllers; + nsresult rv = controllers->GetControllerCount(&numControllers); + NS_ASSERTION((NS_SUCCEEDED(rv)), + "bad result in gfx text control destructor"); + for (uint32_t i = 0; i < numControllers; i++) { + nsCOMPtr<nsIController> controller; + rv = controllers->GetControllerAt(i, getter_AddRefs(controller)); + if (NS_SUCCEEDED(rv) && controller) { + nsCOMPtr<nsIControllerContext> editController = + do_QueryInterface(controller); + if (editController) { + editController->SetCommandContext(nullptr); + } + } + } + } + } + + if (mSelCon) { + if (mTextListener) { + mTextListener->EndListeningToSelectionChange(); + } + + mSelCon->SetScrollableFrame(nullptr); + mSelCon = nullptr; + } + + if (mTextListener) { + mTextListener->SetFrame(nullptr); + + EventListenerManager* manager = + mTextCtrlElement->GetExistingListenerManager(); + if (manager) { + manager->RemoveEventListenerByType(mTextListener, u"keydown"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->RemoveEventListenerByType(mTextListener, u"keypress"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->RemoveEventListenerByType(mTextListener, u"keyup"_ns, + TrustedEventsAtSystemGroupBubble()); + } + + mTextListener = nullptr; + } + + mBoundFrame = nullptr; + + // Now that we don't have a frame any more, store the value in the text + // buffer. The only case where we don't do this is if a value transfer is in + // progress. + if (!mValueTransferInProgress) { + DebugOnly<bool> ok = SetValue(value, ValueSetterOption::ByInternalAPI); + // TODO Find something better to do if this fails... + NS_WARNING_ASSERTION(ok, "SetValue() couldn't allocate memory"); + } +} + +void TextControlState::GetValue(nsAString& aValue, bool aIgnoreWrap) const { + // While SetValue() is being called and requesting to commit composition to + // IME, GetValue() may be called for appending text or something. Then, we + // need to return the latest aValue of SetValue() since the value hasn't + // been set to the editor yet. + // XXX After implementing "beforeinput" event, this becomes wrong. The + // value should be modified immediately after "beforeinput" event for + // "insertReplacementText". + if (mHandlingState && + mHandlingState->IsHandling(TextControlAction::CommitComposition)) { + aValue = mHandlingState->GetSettingValue(); + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + return; + } + + if (mTextEditor && mBoundFrame && + (mEditorInitialized || !IsSingleLineTextControl())) { + if (aIgnoreWrap && !mBoundFrame->CachedValue().IsVoid()) { + aValue = mBoundFrame->CachedValue(); + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + return; + } + + aValue.Truncate(); // initialize out param + + uint32_t flags = (nsIDocumentEncoder::OutputLFLineBreak | + nsIDocumentEncoder::OutputPreformatted | + nsIDocumentEncoder::OutputPersistNBSP | + nsIDocumentEncoder::OutputBodyOnly); + if (!aIgnoreWrap) { + TextControlElement::nsHTMLTextWrap wrapProp; + if (mTextCtrlElement && + TextControlElement::GetWrapPropertyEnum(mTextCtrlElement, wrapProp) && + wrapProp == TextControlElement::eHTMLTextWrap_Hard) { + flags |= nsIDocumentEncoder::OutputWrap; + } + } + + // What follows is a bit of a hack. The problem is that we could be in + // this method because we're being destroyed for whatever reason while + // script is executing. If that happens, editor will run with the + // privileges of the executing script, which means it may not be able to + // access its own DOM nodes! Let's try to deal with that by pushing a null + // JSContext on the JSContext stack to make it clear that we're native + // code. Note that any script that's directly trying to access our value + // has to be going through some scriptable object to do that and that + // already does the relevant security checks. + // XXXbz if we could just get the textContent of our anonymous content (eg + // if plaintext editor didn't create <br> nodes all over), we wouldn't need + // this. + { /* Scope for AutoNoJSAPI. */ + AutoNoJSAPI nojsapi; + + DebugOnly<nsresult> rv = mTextEditor->ComputeTextValue(flags, aValue); + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to get value"); + } + // Only when the result doesn't include line breaks caused by hard-wrap, + // mCacheValue should cache the value. + if (!(flags & nsIDocumentEncoder::OutputWrap)) { + mBoundFrame->CacheValue(aValue); + } else { + mBoundFrame->ClearCachedValue(); + } + } else { + if (!mTextCtrlElement->ValueChanged() || mValue.IsVoid()) { + // Use nsString to avoid copying string buffer at setting aValue. + nsString value; + mTextCtrlElement->GetDefaultValueFromContent(value); + // TODO: We should make default value not include \r. + nsContentUtils::PlatformToDOMLineBreaks(value); + aValue = value; + } else { + aValue = mValue; + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + } + } +} + +bool TextControlState::ValueEquals(const nsAString& aValue) const { + // We can avoid copying string buffer in many cases. Therefore, we should + // use nsString rather than nsAutoString here. + nsString value; + GetValue(value, true); + return aValue.Equals(value); +} + +#ifdef DEBUG +// @param aOptions TextControlState::ValueSetterOptions +bool AreFlagsNotDemandingContradictingMovements( + const ValueSetterOptions& aOptions) { + return !aOptions.contains( + {ValueSetterOption::MoveCursorToBeginSetSelectionDirectionForward, + ValueSetterOption::MoveCursorToEndIfValueChanged}); +} +#endif // DEBUG + +bool TextControlState::SetValue(const nsAString& aValue, + const nsAString* aOldValue, + const ValueSetterOptions& aOptions) { + 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; + } + + if (mPasswordMaskData) { + if (mHandlingState && + mHandlingState->Is(TextControlAction::UnbindFromFrame)) { + // If we're called by UnbindFromFrame, we shouldn't reset unmasked range. + } else { + // Otherwise, we should mask the new password, even if it's same value + // since the same value may be one for different web app's. + mPasswordMaskData->Reset(); + } + } + + const bool wasHandlingSetValue = + mHandlingState && mHandlingState->IsHandling(TextControlAction::SetValue); + + ErrorResult error; + AutoTextControlHandlingState handlingSetValue( + *this, TextControlAction::SetValue, aValue, aOldValue, aOptions, error); + if (error.Failed()) { + MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY)); + error.SuppressException(); + return false; + } + + const auto changeKind = [&] { + if (aOptions.contains(ValueSetterOption::ByInternalAPI)) { + return ValueChangeKind::Internal; + } + if (aOptions.contains(ValueSetterOption::BySetUserInputAPI)) { + return ValueChangeKind::UserInteraction; + } + return ValueChangeKind::Script; + }(); + + if (changeKind == ValueChangeKind::Script) { + // This value change will not be interactive. If we're an input that was + // interactively edited, save the last interactive value now before it goes + // away. + if (auto* input = HTMLInputElement::FromNode(mTextCtrlElement)) { + if (input->LastValueChangeWasInteractive()) { + GetValue(mLastInteractiveValue, /* aIgnoreWrap = */ true); + } + } + } + + // 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 (aOptions.contains(ValueSetterOption::BySetUserInputAPI) || + aOptions.contains(ValueSetterOption::ByContentAPI)) { + if (EditorHasComposition()) { + // When this is called recursively, there shouldn't be composition. + 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. + return true; + } + if (NS_WARN_IF(!mBoundFrame)) { + // We're not sure if this case is possible. + } else { + // If setting value won't change current value, we shouldn't commit + // composition for compatibility with the other browsers. + MOZ_ASSERT(!aOldValue || mBoundFrame->TextEquals(*aOldValue)); + bool isSameAsCurrentValue = + aOldValue + ? aOldValue->Equals(handlingSetValue.GetSettingValue()) + : mBoundFrame->TextEquals(handlingSetValue.GetSettingValue()); + if (isSameAsCurrentValue) { + // Note that in this case, we shouldn't fire any events with setting + // value because event handlers may try to set value recursively but + // we cannot commit composition at that time due to unsafe to run + // script (see below). + return true; + } + } + // If there is composition, need to commit composition first because + // other browsers do that. + // NOTE: We don't need to block nested calls of this because input nor + // other events won't be fired by setting values and script blocker + // 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. + RefPtr<TextEditor> textEditor = mTextEditor; + nsresult rv = textEditor->CommitComposition(); + if (handlingCommitComposition.IsTextControlStateDestroyed()) { + return true; + } + if (NS_FAILED(rv)) { + NS_WARNING("TextControlState failed to commit composition"); + return true; + } + // Note that if a composition event listener sets editor value again, + // we should use the new value here. The new value is stored in + // handlingSetValue right now. + } else { + NS_WARNING( + "SetValue() is called when there is composition but " + "it's not safe to request to commit the composition"); + } + } + } + + if (mTextEditor && mBoundFrame) { + if (!SetValueWithTextEditor(handlingSetValue)) { + return false; + } + } else if (!SetValueWithoutTextEditor(handlingSetValue)) { + return false; + } + + // If we were handling SetValue() before, don't update the DOM state twice, + // just let the outer call do so. + if (!wasHandlingSetValue) { + handlingSetValue.GetTextControlElement()->OnValueChanged( + changeKind, handlingSetValue.GetSettingValue()); + } + return true; +} + +bool 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 + + MOZ_ASSERT(!aHandlingSetValue.GetOldValue() || + mBoundFrame->TextEquals(*aHandlingSetValue.GetOldValue())); + bool isSameAsCurrentValue = + aHandlingSetValue.GetOldValue() + ? aHandlingSetValue.GetOldValue()->Equals( + aHandlingSetValue.GetSettingValue()) + : mBoundFrame->TextEquals(aHandlingSetValue.GetSettingValue()); + + // this is necessary to avoid infinite recursion + if (isSameAsCurrentValue) { + return true; + } + + RefPtr<TextEditor> textEditor = mTextEditor; + + nsCOMPtr<Document> document = textEditor->GetDocument(); + if (NS_WARN_IF(!document)) { + 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, __FUNCTION__); + + // get the flags, remove readonly, disabled and max-length, + // set the value, restore flags + AutoRestoreEditorState restoreState(textEditor); + + aHandlingSetValue.WillSetValueWithTextEditor(); + + if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI)) { + // 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. + nsresult rv = textEditor->ReplaceTextAsAction( + 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), + "EditorBase::ReplaceTextAsAction() failed"); + return rv != NS_ERROR_OUT_OF_MEMORY; + } + + // Don't dispatch "beforeinput" event nor "input" event for setting value + // by script. + AutoInputEventSuppresser suppressInputEventDispatching(textEditor); + + // 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. + // + // (Except if chrome opts into this behavior). + Maybe<AutoDisableUndo> disableUndo; + if (!aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::PreserveUndoHistory)) { + disableUndo.emplace(textEditor); + } + + if (selection) { + // Since we don't use undo transaction, we don't need to store + // selection state. SetText will set selection to tail. + IgnoredErrorResult ignoredError; + MOZ_KnownLive(selection)->RemoveAllRanges(ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Selection::RemoveAllRanges() failed, but ignored"); + } + + // 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(), + aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI) && + !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"); + + // Call the listener's OnEditActionHandled() callback manually if + // OnEditActionHandled() hasn't been called yet since TextEditor don't use + // the transaction manager in this path and it could be that the editor + // would bypass calling the listener for that reason. + if (!aHandlingSetValue.HasEditActionHandled()) { + nsresult rvOnEditActionHandled = + MOZ_KnownLive(aHandlingSetValue.GetTextInputListener()) + ->OnEditActionHandled(*textEditor); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvOnEditActionHandled), + "TextInputListener::OnEditActionHandled() failed"); + if (rv != NS_ERROR_OUT_OF_MEMORY) { + rv = rvOnEditActionHandled; + } + } + + return rv != NS_ERROR_OUT_OF_MEMORY; +} + +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.IsVoid()) { + mValue.SetIsVoid(false); + } + + // We can't just early-return here, because OnValueChanged below still need to + // be called. + if (!mValue.Equals(aHandlingSetValue.GetSettingValue()) || + !StaticPrefs::dom_input_skip_cursor_move_for_same_value_set()) { + 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.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI) && + 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, + 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) { + 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; + } + } + + 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.ValueSetterOptionsRef())); + + SelectionProperties& props = GetSelectionProperties(); + // Setting a max length and thus capping selection range early prevents + // selection change detection in setRangeText. Temporarily disable + // capping here with UINT32_MAX, and set it later in ::SetRangeText(). + props.SetMaxLength(aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetRangeTextAPI) + ? UINT32_MAX + : aHandlingSetValue.GetSettingValue().Length()); + if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::MoveCursorToEndIfValueChanged)) { + props.SetStart(aHandlingSetValue.GetSettingValue().Length()); + props.SetEnd(aHandlingSetValue.GetSettingValue().Length()); + props.SetDirection(SelectionDirection::Forward); + } else if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption:: + MoveCursorToBeginSetSelectionDirectionForward)) { + props.SetStart(0); + props.SetEnd(0); + props.SetDirection(SelectionDirection::Forward); + } + } + + // Update the frame display if needed + if (mBoundFrame) { + mBoundFrame->UpdateValueDisplay(true); + } + + // If the text control element has focus, IMEContentObserver is not + // observing the content changes due to no bound frame or no TextEditor. + // Therefore, we need to let IMEContentObserver know all values are being + // replaced. + if (IMEContentObserver* observer = GetIMEContentObserver()) { + observer->OnTextControlValueChangedWhileNotObservable(mValue); + } + } + + // 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. + // 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.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI)) { + MOZ_ASSERT(aHandlingSetValue.GetTextControlElement()); + + // Update validity state before dispatching "input" event for its + // listeners like `EditorBase::NotifyEditorObservers()`. + aHandlingSetValue.GetTextControlElement()->OnValueChanged( + ValueChangeKind::UserInteraction, + aHandlingSetValue.GetSettingValue()); + + ClearLastInteractiveValue(); + + MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid()); + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent( + MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()), + eEditorInput, EditorInputType::eInsertReplacementText, nullptr, + InputEventOptions(inputEventData, + InputEventOptions::NeverCancelable::No)); + 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 true; +} + +void TextControlState::InitializeKeyboardEventListeners() { + // register key listeners + EventListenerManager* manager = + mTextCtrlElement->GetOrCreateListenerManager(); + if (manager) { + manager->AddEventListenerByType(mTextListener, u"keydown"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->AddEventListenerByType(mTextListener, u"keypress"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->AddEventListenerByType(mTextListener, u"keyup"_ns, + TrustedEventsAtSystemGroupBubble()); + } + + mSelCon->SetScrollableFrame(mBoundFrame->GetScrollTargetFrame()); +} + +void TextControlState::SetPreviewText(const nsAString& aValue, bool aNotify) { + // If we don't have a preview div, there's nothing to do. + Element* previewDiv = GetPreviewNode(); + if (!previewDiv) { + return; + } + + nsAutoString previewValue(aValue); + + nsContentUtils::RemoveNewlines(previewValue); + MOZ_ASSERT(previewDiv->GetFirstChild(), "preview div has no child"); + previewDiv->GetFirstChild()->AsText()->SetText(previewValue, aNotify); +} + +void TextControlState::GetPreviewText(nsAString& aValue) { + // If we don't have a preview div, there's nothing to do. + Element* previewDiv = GetPreviewNode(); + if (!previewDiv) { + return; + } + + MOZ_ASSERT(previewDiv->GetFirstChild(), "preview div has no child"); + const nsTextFragment* text = previewDiv->GetFirstChild()->GetText(); + + aValue.Truncate(); + text->AppendTo(aValue); +} + +bool TextControlState::EditorHasComposition() { + return mTextEditor && mTextEditor->IsIMEComposing(); +} + +IMEContentObserver* TextControlState::GetIMEContentObserver() const { + if (NS_WARN_IF(!mTextCtrlElement) || + mTextCtrlElement != IMEStateManager::GetFocusedElement()) { + return nullptr; + } + return IMEStateManager::GetActiveContentObserver(); +} + +} // namespace mozilla |