forked from mirrors/gecko-dev
Before this patch, we had two `checkVisibilty` methods on the nsISelectionController interface, backed by several layers of implementation, ultimately backed by a single function on nsTextFrame (which didn't actually do anything meaningful with any of the parameters). As it turns out, this API only had one caller, in HTMLEditUtils.cpp. This patch converts that caller to directly query nsTextFrame (if the given node's primary frame is indeed a nsTextFrame). The direct function-call is renamed to HasVisibleText(), to be a bit clearer about it being text-specific and also to avoid confusion with the (unrelated) recently-specified HTML checkVisibility() API. With these changes, we can remove the API from the nsISelectionController interface and its implementations. This patch also updates the HTMLEditUtils::IsInVisibleTextFrames documentation (with s/all/any/) to reflect the reality of what the nsTextFrame impl actually does. Differential Revision: https://phabricator.services.mozilla.com/D160563
3058 lines
106 KiB
C++
3058 lines
106 KiB
C++
/* -*- 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/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();
|
|
}
|
|
}
|
|
|
|
return mTextControlState ? mTextControlState->OnEditActionHandled() : NS_OK;
|
|
}
|
|
|
|
void TextInputListener::HandleValueChanged() {
|
|
// 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);
|
|
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");
|
|
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");
|
|
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();
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (IsPasswordTextControl()) {
|
|
// Disable undo for <input type="password">. Note that we want to do this
|
|
// at the very end of InitEditor(), so the calls to EnableUndoRedo() when
|
|
// setting the default value don't screw us up. Since changing the
|
|
// control type does a reframe, we don't have to worry about dynamic type
|
|
// changes here.
|
|
DebugOnly<bool> disabledUndoRedo = newTextEditor->DisableUndoRedo();
|
|
NS_WARNING_ASSERTION(disabledUndoRedo,
|
|
"Failed to disable undo/redo transaction");
|
|
} else {
|
|
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);
|
|
}
|
|
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 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);
|
|
|
|
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;
|
|
}
|
|
|
|
bool TextControlState::HasNonEmptyValue() {
|
|
// If the frame for editor is alive, we can compute it with mTextEditor.
|
|
// Otherwise, we need to check cached value via GetValue().
|
|
if (mTextEditor && mBoundFrame && mEditorInitialized &&
|
|
!(mHandlingState &&
|
|
mHandlingState->IsHandling(TextControlAction::CommitComposition))) {
|
|
return !mTextEditor->IsEmpty();
|
|
}
|
|
|
|
nsAutoString value;
|
|
GetValue(value, true);
|
|
return !value.IsEmpty();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
} // namespace mozilla
|