/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#ifndef mozilla_EditorBase_h
#define mozilla_EditorBase_h
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
#include "mozilla/EditAction.h" // for EditAction and EditSubAction
#include "mozilla/EditorDOMPoint.h" // for EditorDOMPoint
#include "mozilla/EditorForwards.h"
#include "mozilla/EventForwards.h" // for InputEventTargetRanges
#include "mozilla/Likely.h" // for MOZ_UNLIKELY, MOZ_LIKELY
#include "mozilla/Maybe.h" // for Maybe
#include "mozilla/OwningNonNull.h" // for OwningNonNull
#include "mozilla/PendingStyles.h" // for PendingStyle, PendingStyleCache
#include "mozilla/RangeBoundary.h" // for RawRangeBoundary, RangeBoundary
#include "mozilla/SelectionState.h" // for RangeUpdater, etc.
#include "mozilla/StyleSheet.h" // for StyleSheet
#include "mozilla/TransactionManager.h" // for TransactionManager
#include "mozilla/WeakPtr.h" // for WeakPtr
#include "mozilla/dom/DataTransfer.h" // for dom::DataTransfer
#include "mozilla/dom/HTMLBRElement.h" // for dom::HTMLBRElement
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/Text.h"
#include "nsAtom.h" // for nsAtom, nsStaticAtom
#include "nsCOMPtr.h" // for already_AddRefed, nsCOMPtr
#include "nsCycleCollectionParticipant.h"
#include "nsGkAtoms.h"
#include "nsIClipboard.h" // for nsIClipboard::ClipboardType
#include "nsIContentInlines.h" // for nsINode::IsEditable()
#include "nsIEditor.h" // for nsIEditor, etc.
#include "nsISelectionController.h" // for nsISelectionController constants
#include "nsISelectionListener.h" // for nsISelectionListener
#include "nsISupportsImpl.h" // for EditorBase::Release, etc.
#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
#include "nsLiteralString.h" // for NS_LITERAL_STRING
#include "nsPIDOMWindow.h" // for nsPIDOMWindowInner, etc.
#include "nsString.h" // for nsCString
#include "nsTArray.h" // for nsTArray and AutoTArray
#include "nsWeakReference.h" // for nsSupportsWeakReference
#include "nscore.h" // for nsresult, nsAString, etc.
#include <tuple>
// for std::tuple
class mozInlineSpellChecker;
class nsAtom;
class nsCaret;
class nsIContent;
class nsIDocumentEncoder;
class nsIDocumentStateListener;
class nsIEditActionListener;
class nsINode;
class nsIPrincipal;
class nsISupports;
class nsITransferable;
class nsITransaction;
class nsIWidget;
class nsRange;
namespace mozilla {
class AlignStateAtSelection;
class AutoTransactionsConserveSelection;
class AutoUpdateViewBatch;
class ErrorResult;
class IMEContentObserver;
class ListElementSelectionState;
class ListItemElementSelectionState;
class ParagraphStateAtSelection;
class PresShell;
class TextComposition;
class TextInputListener;
class TextServicesDocument;
namespace dom {
class AbstractRange;
class DataTransfer;
class Document;
class DragEvent;
class Element;
class EventTarget;
class HTMLBRElement;
}
// namespace dom
namespace widget {
struct IMEState;
}
// namespace widget
/**
* Implementation of an editor object. it will be the controller/focal point
* for the main editor services. i.e. the GUIManager, publishing, transaction
* manager, event interfaces. the idea for the event interfaces is to have them
* delegate the actual commands to the editor independent of the XPFE
* implementation.
*/
class EditorBase :
public nsIEditor,
public nsISelectionListener,
public nsSupportsWeakReference {
public:
/****************************************************************************
* NOTE: DO NOT MAKE YOUR NEW METHODS PUBLIC IF they are called by other
* classes under libeditor except EditorEventListener and
* HTMLEditorEventListener because each public method which may fire
* eEditorInput event will need to instantiate new stack class for
* managing input type value of eEditorInput and cache some objects
* for smarter handling. In other words, when you add new root
* method to edit the DOM tree, you can make your new method public.
****************************************************************************/
using DataTransfer = dom::DataTransfer;
using Document = dom::Document;
using Element = dom::Element;
using InterlinePosition = dom::Selection::InterlinePosition;
using Selection = dom::Selection;
using Text = dom::Text;
enum class EditorType { Text, HTML };
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EditorBase, nsIEditor)
// nsIEditor methods
NS_DECL_NSIEDITOR
// nsISelectionListener method
NS_DECL_NSISELECTIONLISTENER
/**
* The default constructor. This should suffice. the setting of the
* interfaces is done after the construction of the editor class.
*/
explicit EditorBase(EditorType aEditorType);
[[nodiscard]]
bool IsInitialized()
const {
return mDocument && mDidPostCreate;
}
[[nodiscard]]
bool IsBeingInitialized()
const {
return mDocument && !mDidPostCreate;
}
[[nodiscard]]
bool Destroyed()
const {
return mDidPreDestroy; }
Document* GetDocument()
const {
return mDocument; }
nsPIDOMWindowOuter* GetWindow()
const;
nsPIDOMWindowInner* GetInnerWindow()
const;
/**
* MayHaveMutationEventListeners() returns true when the window may have
* mutation event listeners.
*
* @param aMutationEventType One or multiple of NS_EVENT_BITS_MUTATION_*.
* @return true if the editor is an HTMLEditor instance,
* and at least one of NS_EVENT_BITS_MUTATION_* is
* set to the window or in debug build.
*/
bool MayHaveMutationEventListeners(
uint32_t aMutationEventType = 0xFFFFFFFF)
const {
if (IsTextEditor()) {
// DOM mutation event listeners cannot catch the changes of
// <input type="text"> nor <textarea>.
return false;
}
#ifdef DEBUG
// On debug build, this should always return true for testing complicated
// path without mutation event listeners because when mutation event
// listeners do not touch the DOM, editor needs to run as there is no
// mutation event listeners.
return true;
#else // #ifdef DEBUG
nsPIDOMWindowInner* window = GetInnerWindow();
return window ? window->HasMutationListeners(aMutationEventType) :
false;
#endif // #ifdef DEBUG #else
}
/**
* MayHaveBeforeInputEventListenersForTelemetry() returns true when the
* window may have or have had one or more `beforeinput` event listeners.
* Note that this may return false even if there is a `beforeinput`.
* See nsPIDOMWindowInner::HasBeforeInputEventListenersForTelemetry()'s
* comment for the detail.
*/
bool MayHaveBeforeInputEventListenersForTelemetry()
const {
if (
const nsPIDOMWindowInner* window = GetInnerWindow()) {
return window->HasBeforeInputEventListenersForTelemetry();
}
return false;
}
/**
* MutationObserverHasObservedNodeForTelemetry() returns true when a node in
* the window may have been observed by the web apps with a mutation observer
* (i.e., `MutationObserver.observe()` called by chrome script and addon's
* script does not make this returns true).
* Note that this may return false even if there is a node observed by
* a MutationObserver. See
* nsPIDOMWindowInner::MutationObserverHasObservedNodeForTelemetry()'s comment
* for the detail.
*/
bool MutationObserverHasObservedNodeForTelemetry()
const {
if (
const nsPIDOMWindowInner* window = GetInnerWindow()) {
return window->MutationObserverHasObservedNodeForTelemetry();
}
return false;
}
/**
* This checks whether the call with aPrincipal should or should not be
* treated as user input.
*/
[[nodiscard]]
static bool TreatAsUserInput(nsIPrincipal* aPrincipal);
PresShell* GetPresShell()
const;
nsPresContext* GetPresContext()
const;
already_AddRefed<nsCaret> GetCaret()
const;
already_AddRefed<nsIWidget> GetWidget()
const;
nsISelectionController* GetSelectionController()
const;
nsresult GetSelection(SelectionType aSelectionType,
Selection** aSelection)
const;
Selection* GetSelection(
SelectionType aSelectionType = SelectionType::eNormal)
const {
if (aSelectionType == SelectionType::eNormal &&
IsEditActionDataAvailable()) {
return &SelectionRef();
}
nsISelectionController* sc = GetSelectionController();
if (!sc) {
return nullptr;
}
Selection* selection = sc->GetSelection(ToRawSelectionType(aSelectionType));
return selection;
}
/**
* @return Ancestor limiter of normal selection
*/
[[nodiscard]] nsIContent* GetSelectionAncestorLimiter()
const {
Selection* selection = GetSelection(SelectionType::eNormal);
return selection ? selection->GetAncestorLimiter() : nullptr;
}
/**
* Create a DataTransfer object that can be shared between the paste event
* and pasting into a DOM element.
*/
already_AddRefed<DataTransfer> CreateDataTransferForPaste(
EventMessage aEventMessage,
nsIClipboard::ClipboardType aClipboardType)
const;
/**
* Fast non-refcounting editor root element accessor
*/
Element* GetRoot()
const {
return mRootElement; }
/**
* Likewise, but gets the text control element instead of the root for
* plaintext editors.
*/
Element* GetExposedRoot()
const;
/**
* Set or unset TextInputListener. If setting non-nullptr when the editor
* already has a TextInputListener, this will crash in debug build.
*/
void SetTextInputListener(TextInputListener* aTextInputListener);
/**
* Set or unset IMEContentObserver. If setting non-nullptr when the editor
* already has an IMEContentObserver, this will crash in debug build.
*/
void SetIMEContentObserver(IMEContentObserver* aIMEContentObserver);
/**
* Returns current composition.
*/
TextComposition* GetComposition()
const;
/**
* Get preferred IME status of current widget.
*/
virtual nsresult GetPreferredIMEState(widget::IMEState* aState);
/**
* Returns true if there is composition string and not fixed.
*/
bool IsIMEComposing()
const;
/**
* Commit composition if there is.
* Note that when there is a composition, this requests to commit composition
* to native IME. Therefore, when there is composition, this can do anything.
* For example, the editor instance, the widget or the process itself may
* be destroyed.
*/
nsresult CommitComposition();
/**
* ToggleTextDirection() toggles text-direction of the root element.
*
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult
ToggleTextDirectionAsAction(nsIPrincipal* aPrincipal = nullptr);
/**
* SwitchTextDirectionTo() sets the text-direction of the root element to
* LTR or RTL.
*/
enum class TextDirection {
eLTR,
eRTL,
};
MOZ_CAN_RUN_SCRIPT
void SwitchTextDirectionTo(TextDirection aTextDirection);
/**
* Finalizes selection and caret for the editor.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult FinalizeSelection();
/**
* Returns true if selection is in an editable element and both the range
* start and the range end are editable. E.g., even if the selection range
* includes non-editable elements, returns true when one of common ancestors
* of the range start and the range end is editable. Otherwise, false.
*/
bool IsSelectionEditable();
/**
* Returns number of undo or redo items.
*/
size_t NumberOfUndoItems()
const {
return mTransactionManager ? mTransactionManager->NumberOfUndoItems() : 0;
}
size_t NumberOfRedoItems()
const {
return mTransactionManager ? mTransactionManager->NumberOfRedoItems() : 0;
}
/**
* Returns number of maximum undo/redo transactions.
*/
int32_t NumberOfMaximumTransactions()
const {
return mTransactionManager
? mTransactionManager->NumberOfMaximumTransactions()
: 0;
}
/**
* Returns true if this editor can store transactions for undo/redo.
*/
bool IsUndoRedoEnabled()
const {
return mTransactionManager &&
mTransactionManager->NumberOfMaximumTransactions();
}
/**
* Return true if it's possible to undo/redo right now.
*/
bool CanUndo()
const {
return IsUndoRedoEnabled() && NumberOfUndoItems() > 0;
}
bool CanRedo()
const {
return IsUndoRedoEnabled() && NumberOfRedoItems() > 0;
}
/**
* Enables or disables undo/redo feature. Returns true if it succeeded,
* otherwise, e.g., we're undoing or redoing, returns false.
*/
bool EnableUndoRedo(int32_t aMaxTransactionCount = -1) {
if (!mTransactionManager) {
mTransactionManager =
new TransactionManager();
}
return mTransactionManager->EnableUndoRedo(aMaxTransactionCount);
}
bool DisableUndoRedo() {
if (!mTransactionManager) {
return true;
}
return mTransactionManager->DisableUndoRedo();
}
bool ClearUndoRedo() {
if (!mTransactionManager) {
return true;
}
return mTransactionManager->ClearUndoRedo();
}
/**
* See Document::AreClipboardCommandsUnconditionallyEnabled.
*/
bool AreClipboardCommandsUnconditionallyEnabled()
const;
/**
* IsCutCommandEnabled() returns whether cut command can be enabled or
* disabled. This always returns true if we're in non-chrome HTML/XHTML
* document. Otherwise, same as the result of `IsCopyToClipboardAllowed()`.
*/
MOZ_CAN_RUN_SCRIPT
bool IsCutCommandEnabled()
const;
/**
* IsCopyCommandEnabled() returns copy command can be enabled or disabled.
* This always returns true if we're in non-chrome HTML/XHTML document.
* Otherwise, same as the result of `IsCopyToClipboardAllowed()`.
*/
MOZ_CAN_RUN_SCRIPT
bool IsCopyCommandEnabled()
const;
/**
* IsCopyToClipboardAllowed() returns true if the selected content can
* be copied into the clipboard. This returns true when:
* - `Selection` is not collapsed and we're not a password editor.
* - `Selection` is not collapsed and we're a password editor but selection
* range is in unmasked range.
*/
bool IsCopyToClipboardAllowed()
const {
AutoEditActionDataSetter editActionData(*
this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return false;
}
return IsCopyToClipboardAllowedInternal();
}
/**
* HandleDropEvent() is called from EditorEventListener::Drop that is handler
* of drop event.
*/
MOZ_CAN_RUN_SCRIPT nsresult HandleDropEvent(dom::DragEvent* aDropEvent);
MOZ_CAN_RUN_SCRIPT
virtual nsresult HandleKeyPressEvent(
WidgetKeyboardEvent* aKeyboardEvent);
virtual dom::EventTarget* GetDOMEventTarget()
const = 0;
/**
* OnCompositionStart() is called when editor receives eCompositionStart
* event which should be handled in this editor.
*/
nsresult OnCompositionStart(WidgetCompositionEvent& aCompositionStartEvent);
/**
* OnCompositionChange() is called when editor receives an eCompositioChange
* event which should be handled in this editor.
*
* @param aCompositionChangeEvent eCompositionChange event which should
* be handled in this editor.
*/
MOZ_CAN_RUN_SCRIPT nsresult
OnCompositionChange(WidgetCompositionEvent& aCompositionChangeEvent);
/**
* OnCompositionEnd() is called when editor receives an eCompositionChange
* event and it's followed by eCompositionEnd event and after
* OnCompositionChange() is called.
*/
MOZ_CAN_RUN_SCRIPT
void OnCompositionEnd(
WidgetCompositionEvent& aCompositionEndEvent);
/**
* Accessor methods to flags.
*/
uint32_t Flags()
const {
return mFlags; }
MOZ_CAN_RUN_SCRIPT nsresult AddFlags(uint32_t aFlags) {
const uint32_t kOldFlags = Flags();
const uint32_t kNewFlags = (kOldFlags | aFlags);
if (kNewFlags == kOldFlags) {
return NS_OK;
}
return SetFlags(kNewFlags);
// virtual call and may be expensive.
}
MOZ_CAN_RUN_SCRIPT nsresult RemoveFlags(uint32_t aFlags) {
const uint32_t kOldFlags = Flags();
const uint32_t kNewFlags = (kOldFlags & ~aFlags);
if (kNewFlags == kOldFlags) {
return NS_OK;
}
return SetFlags(kNewFlags);
// virtual call and may be expensive.
}
MOZ_CAN_RUN_SCRIPT nsresult AddAndRemoveFlags(uint32_t aAddingFlags,
uint32_t aRemovingFlags) {
MOZ_ASSERT(!(aAddingFlags & aRemovingFlags),
"Same flags are specified both adding and removing");
const uint32_t kOldFlags = Flags();
const uint32_t kNewFlags = ((kOldFlags | aAddingFlags) & ~aRemovingFlags);
if (kNewFlags == kOldFlags) {
return NS_OK;
}
return SetFlags(kNewFlags);
// virtual call and may be expensive.
}
bool IsSingleLineEditor()
const {
const bool isSingleLineEditor =
(mFlags & nsIEditor::eEditorSingleLineMask) != 0;
MOZ_ASSERT_IF(isSingleLineEditor, IsTextEditor());
return isSingleLineEditor;
}
bool IsPasswordEditor()
const {
const bool isPasswordEditor =
(mFlags & nsIEditor::eEditorPasswordMask) != 0;
MOZ_ASSERT_IF(isPasswordEditor, IsTextEditor());
return isPasswordEditor;
}
// FYI: Both IsRightToLeft() and IsLeftToRight() may return false if
// the editor inherits the content node's direction.
bool IsRightToLeft()
const {
return (mFlags & nsIEditor::eEditorRightToLeft) != 0;
}
bool IsLeftToRight()
const {
return (mFlags & nsIEditor::eEditorLeftToRight) != 0;
}
bool IsReadonly()
const {
return (mFlags & nsIEditor::eEditorReadonlyMask) != 0;
}
bool IsMailEditor()
const {
return (mFlags & nsIEditor::eEditorMailMask) != 0;
}
bool IsInteractionAllowed()
const {
const bool isInteractionAllowed =
(mFlags & nsIEditor::eEditorAllowInteraction) != 0;
MOZ_ASSERT_IF(isInteractionAllowed, IsHTMLEditor());
return isInteractionAllowed;
}
bool ShouldSkipSpellCheck()
const {
return (mFlags & nsIEditor::eEditorSkipSpellCheck) != 0;
}
bool HasIndependentSelection()
const {
MOZ_ASSERT_IF(mSelectionController, IsTextEditor());
return !!mSelectionController;
}
bool IsModifiable()
const {
return !IsReadonly(); }
/**
* IsInEditSubAction() return true while the instance is handling an edit
* sub-action. Otherwise, false.
*/
bool IsInEditSubAction()
const {
return mIsInEditSubAction; }
/**
* IsEmpty() checks whether the editor is empty. If editor has only padding
* <br> element for empty editor, returns true. If editor's root element has
* non-empty text nodes or other nodes like <br>, returns false.
*/
virtual bool IsEmpty()
const = 0;
/**
* SuppressDispatchingInputEvent() suppresses or unsuppresses dispatching
* "input" event.
*/
void SuppressDispatchingInputEvent(
bool aSuppress) {
mDispatchInputEvent = !aSuppress;
}
/**
* IsSuppressingDispatchingInputEvent() returns true if the editor stops
* dispatching input event. Otherwise, false.
*/
bool IsSuppressingDispatchingInputEvent()
const {
return !mDispatchInputEvent;
}
/**
* Returns true if markNodeDirty() has any effect. Returns false if
* markNodeDirty() is a no-op.
*/
bool OutputsMozDirty()
const {
// Return true for Composer (!IsInteractionAllowed()) or mail
// (IsMailEditor()), but false for webpages.
return !IsInteractionAllowed() || IsMailEditor();
}
/**
* Get the focused element, if we're focused. Returns null otherwise.
*/
virtual Element* GetFocusedElement()
const;
/**
* Whether the aGUIEvent should be handled by this editor or not. When this
* returns false, The aGUIEvent shouldn't be handled on this editor,
* i.e., The aGUIEvent should be handled by another inner editor or ancestor
* elements.
*/
virtual bool IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent)
const;
/**
* FindSelectionRoot() returns a selection root of this editor when aNode
* gets focus. aNode must be a content node or a document node. When the
* target isn't a part of this editor, returns nullptr. If this is for
* designMode, you should set the document node to aNode except that an
* element in the document has focus.
*/
[[nodiscard]]
virtual Element* FindSelectionRoot(
const nsINode& aNode)
const;
/**
* OnFocus() is called when we get a focus event.
*
* @param aOriginalEventTargetNode The original event target node of the
* focus event.
*/
MOZ_CAN_RUN_SCRIPT
virtual nsresult OnFocus(
const nsINode& aOriginalEventTargetNode);
/**
* OnBlur() is called when we're blurred.
*
* @param aEventTarget The event target of the blur event.
*/
virtual nsresult OnBlur(
const dom::EventTarget* aEventTarget) = 0;
/** Resyncs spellchecking state (enabled/disabled). This should be called
* when anything that affects spellchecking state changes, such as the
* spellcheck attribute value.
*/
void SyncRealTimeSpell();
/**
* Do "cut".
*
* @param aPrincipal If you know current context is subject
* principal or system principal, set it.
* When nullptr, this checks it automatically.
*/
MOZ_CAN_RUN_SCRIPT nsresult CutAsAction(nsIPrincipal* aPrincipal = nullptr);
/**
* CanPaste() returns true if user can paste something at current selection.
*/
virtual bool CanPaste(nsIClipboard::ClipboardType aClipboardType)
const = 0;
/**
* Do "undo" or "redo".
*
* @param aCount How many count of transactions should be
* handled.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult UndoAsAction(uint32_t aCount,
nsIPrincipal* aPrincipal = nullptr);
MOZ_CAN_RUN_SCRIPT nsresult RedoAsAction(uint32_t aCount,
nsIPrincipal* aPrincipal = nullptr);
/**
* InsertTextAsAction() inserts aStringToInsert at selection.
* Although this method is implementation of nsIEditor.insertText(),
* this treats the input is an edit action. If you'd like to insert text
* as part of edit action, you probably should use InsertTextAsSubAction().
*
* @param aStringToInsert The string to insert.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult InsertTextAsAction(
const nsAString& aStringToInsert, nsIPrincipal* aPrincipal = nullptr);
/**
* InsertLineBreakAsAction() is called when user inputs a line break with
* Enter or something. If the instance is `HTMLEditor`, this is called
* when Shift + Enter or "insertlinebreak" command.
*
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT
virtual nsresult InsertLineBreakAsAction(
nsIPrincipal* aPrincipal = nullptr) = 0;
/**
* CanDeleteSelection() returns true if `Selection` is not collapsed and
* it's allowed to be removed.
*/
bool CanDeleteSelection()
const {
AutoEditActionDataSetter editActionData(*
this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return false;
}
return IsModifiable() && !SelectionRef().IsCollapsed();
}
/**
* DeleteSelectionAsAction() removes selection content or content around
* caret with transactions. This should be used for handling it as an
* edit action. If you'd like to remove selection for preparing to insert
* something, you probably should use DeleteSelectionAsSubAction().
*
* @param aDirectionAndAmount How much range should be removed.
* @param aStripWrappers Whether the parent blocks should be removed
* when they become empty.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult
DeleteSelectionAsAction(nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
nsIPrincipal* aPrincipal = nullptr);
enum class AllowBeforeInputEventCancelable {
No,
Yes,
};
enum class PreventSetSelection {
No,
Yes,
};
/**
* Replace text in aReplaceRange or all text in this editor with aString and
* treat the change as inserting the string.
*
* @param aString The string to set.
* @param aReplaceRange The range to be replaced.
* If nullptr, all contents will be replaced.
* NOTE: Currently, nullptr is not allowed if
* the editor is an HTMLEditor.
* @param aAllowBeforeInputEventCancelable
* Whether `beforeinput` event which will be
* dispatched for this can be cancelable or not.
* @param aPreventSetSelection
* Whether setting selection after replacing text.
* If No, selection is the tail of replaced text.
* If Yes, selection isn't changed.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult ReplaceTextAsAction(
const nsAString& aString, nsRange* aReplaceRange,
AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
PreventSetSelection aPreventSetSelection = PreventSetSelection::No,
nsIPrincipal* aPrincipal = nullptr);
/**
* Can we paste |aTransferable| or, if |aTransferable| is null, will a call
* to pasteTransferable later possibly succeed if given an instance of
* nsITransferable then? True if the doc is modifiable, and, if
* |aTransfeable| is non-null, we have pasteable data in |aTransfeable|.
*/
virtual bool CanPasteTransferable(nsITransferable* aTransferable) = 0;
/**
* PasteAsAction() pastes clipboard content to Selection. This method
* may dispatch ePaste event first. If its defaultPrevent() is called,
* this does nothing but returns NS_OK.
*
* @param aClipboardType nsIClipboard::kGlobalClipboard or
* nsIClipboard::kSelectionClipboard.
* @param aDispatchPasteEvent Yes if this should dispatch ePaste event
* before pasting. Otherwise, No.
* @param aDataTransfer The object containing the data to use for the
* paste operation. May be nullptr, in which case
* this will just get the data from the clipboard.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
enum class DispatchPasteEvent { No, Yes };
MOZ_CAN_RUN_SCRIPT nsresult
PasteAsAction(nsIClipboard::ClipboardType aClipboardType,
DispatchPasteEvent aDispatchPasteEvent,
DataTransfer* aDataTransfer = nullptr,
nsIPrincipal* aPrincipal = nullptr);
/**
* Paste aTransferable at Selection.
*
* @param aTransferable Must not be nullptr.
* @param aDispatchPasteEvent Yes if this should dispatch ePaste event
* before pasting. Otherwise, No.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult PasteTransferableAsAction(
nsITransferable* aTransferable, DispatchPasteEvent aDispatchPasteEvent,
nsIPrincipal* aPrincipal = nullptr);
/**
* PasteAsQuotationAsAction() pastes content in clipboard as quotation.
* If the editor is TextEditor or in plaintext mode, will paste the content
* with appending ">" to start of each line.
* if the editor is HTMLEditor and is not in plaintext mode, will patste it
* into newly created blockquote element.
*
* @param aClipboardType nsIClipboard::kGlobalClipboard or
* nsIClipboard::kSelectionClipboard.
* @param aDispatchPasteEvent Yes if this should dispatch ePaste event
* before pasting. Otherwise, No.
* @param aDataTransfer The object containing the data to use for the
* paste operation. May be nullptr, in which case
* this will just get the data from the clipboard.
* @param aPrincipal Set subject principal if it may be called by
* JS. If set to nullptr, will be treated as
* called by system.
*/
MOZ_CAN_RUN_SCRIPT nsresult
PasteAsQuotationAsAction(nsIClipboard::ClipboardType aClipboardType,
DispatchPasteEvent aDispatchPasteEvent,
DataTransfer* aDataTransfer = nullptr,
nsIPrincipal* aPrincipal = nullptr);
/**
* Return true if `beforeinput` or `input` event is being dispatched.
*/
[[nodiscard]]
bool IsDispatchingInputEvent()
const {
return mEditActionData && mEditActionData->IsDispatchingInputEvent();
}
protected:
// May be used by friends.
class AutoEditActionDataSetter;
/**
* TopLevelEditSubActionData stores temporary data while we're handling
* top-level edit sub-action.
*/
struct MOZ_STACK_CLASS TopLevelEditSubActionData final {
friend class AutoEditActionDataSetter;
// Set selected range before edit. Then, RangeUpdater keep modifying
// the range while we're changing the DOM tree.
RefPtr<RangeItem> mSelectedRange;
// Computing changed range while we're handling sub actions.
RefPtr<nsRange> mChangedRange;
// XXX In strict speaking, mCachedPendingStyles isn't enough to cache
// inline styles because inline style can be specified with "style"
// attribute and/or CSS in <style> elements or CSS files. So, we need
// to look for better implementation about this.
// FYI: Initialization cost of AutoPendingStyleCacheArray is expensive and
// it is not used by TextEditor so that we should construct it only
// when we're an HTMLEditor.
Maybe<AutoPendingStyleCacheArray> mCachedPendingStyles;
// If we tried to delete selection, set to true.
bool mDidDeleteSelection;
// If we have explicitly set selection inter line, set to true.
// `AfterEdit()` or something shouldn't overwrite it in such case.
bool mDidExplicitlySetInterLine;
// If we have deleted non-collapsed range set to true, there are only 2
// cases for now:
// - non-collapsed range was selected.
// - selection was collapsed in a text node and a Unicode character
// was removed.
bool mDidDeleteNonCollapsedRange;
// If we have deleted parent empty blocks, set to true.
bool mDidDeleteEmptyParentBlocks;
// If we're a contenteditable editor, we temporarily increase edit count
// of the document between `BeforeEdit()` and `AfterEdit()`. I.e., if
// we increased the count in `BeforeEdit()`, we need to decrease it in
// `AfterEdit()`, however, the document may be changed to designMode or
// non-editable. Therefore, we need to store with this whether we need
// to restore it.
bool mRestoreContentEditableCount;
// If we explicitly normalized whitespaces around the changed range,
// set to true.
bool mDidNormalizeWhitespaces;
// Set to true by default. If somebody inserts an HTML fragment
// intentionally, any empty elements shouldn't be cleaned up later. In the
// case this is set to false.
// TODO: We should not do this by default. If it's necessary, each edit
// action handler do it by itself instead. Then, we can avoid such
// unnecessary DOM tree scan.
bool mNeedsToCleanUpEmptyElements;
/**
* The following methods modifies some data of this struct and
* `EditSubActionData` struct. Currently, these are required only
* by `HTMLEditor`. Therefore, for cutting the runtime cost of
* `TextEditor`, these methods should be called only by `HTMLEditor`.
* But it's fine to use these methods in `TextEditor` if necessary.
* If so, you need to call `DidDeleteText()` and `DidInsertText()`
* from `SetTextNodeWithoutTransaction()`.
*/
void DidCreateElement(EditorBase& aEditorBase, Element& aNewElement);
void DidInsertContent(EditorBase& aEditorBase, nsIContent& aNewContent);
void WillDeleteContent(EditorBase& aEditorBase,
nsIContent& aRemovingContent);
void DidSplitContent(EditorBase& aEditorBase, nsIContent& aSplitContent,
nsIContent& aNewContent);
void DidJoinContents(EditorBase& aEditorBase,
const EditorRawDOMPoint& aJoinedPoint);
void DidInsertText(EditorBase& aEditorBase,
const EditorRawDOMPoint& aInsertionBegin,
const EditorRawDOMPoint& aInsertionEnd);
void DidDeleteText(EditorBase& aEditorBase,
const EditorRawDOMPoint& aStartInTextNode);
void WillDeleteRange(EditorBase& aEditorBase,
const EditorRawDOMPoint& aStart,
const EditorRawDOMPoint& aEnd);
private:
void Clear() {
mDidExplicitlySetInterLine =
false;
// We don't need to clear other members which are referred only when the
// editor is an HTML editor anymore. Note that if `mSelectedRange` is
// non-nullptr, that means that we're in `HTMLEditor`.
if (!mSelectedRange) {
return;
}
mSelectedRange->Clear();
mChangedRange->Reset();
if (mCachedPendingStyles.isSome()) {
mCachedPendingStyles->Clear();
}
mDidDeleteSelection =
false;
mDidDeleteNonCollapsedRange =
false;
mDidDeleteEmptyParentBlocks =
false;
mRestoreContentEditableCount =
false;
mDidNormalizeWhitespaces =
false;
mNeedsToCleanUpEmptyElements =
true;
}
/**
* Extend mChangedRange to include `aNode`.
*/
nsresult AddNodeToChangedRange(
const HTMLEditor& aHTMLEditor,
nsINode& aNode);
/**
* Extend mChangedRange to include `aPoint`.
*/
nsresult AddPointToChangedRange(
const HTMLEditor& aHTMLEditor,
const EditorRawDOMPoint& aPoint);
/**
* Extend mChangedRange to include `aStart` and `aEnd`.
*/
nsresult AddRangeToChangedRange(
const HTMLEditor& aHTMLEditor,
const EditorRawDOMPoint& aStart,
const EditorRawDOMPoint& aEnd);
TopLevelEditSubActionData() =
default;
TopLevelEditSubActionData(
const TopLevelEditSubActionData& aOther) =
delete;
};
struct MOZ_STACK_CLASS EditSubActionData final {
// While this is set to false, TopLevelEditSubActionData::mChangedRange
// shouldn't be modified since in some cases, modifying it in the setter
// itself may be faster. Note that we should affect this only for current
// edit sub action since mutation event listener may edit different range.
bool mAdjustChangedRangeFromListener;
private:
void Clear() { mAdjustChangedRangeFromListener =
true; }
friend EditorBase;
};
protected:
// AutoEditActionDataSetter, this shouldn't be accessed by friends.
/**
* SettingDataTransfer enum class is used to specify whether DataTransfer
* should be initialized with or without format. For example, when user
* uses Accel + Shift + V to paste text without format, DataTransfer should
* have only plain/text data to make web apps treat it without format.
*/
enum class SettingDataTransfer {
eWithFormat,
eWithoutFormat,
};
/**
* AutoEditActionDataSetter grabs some necessary objects for handling any
* edit actions and store the edit action what we're handling. When this is
* created, its pointer is set to the mEditActionData, and this guarantees
* the lifetime of grabbing objects until it's destroyed.
*/
class MOZ_STACK_CLASS AutoEditActionDataSetter final {
public:
// NOTE: aPrincipal will be used when we implement "beforeinput" event.
// It's set only when maybe we shouldn't dispatch it because of
// called by JS. I.e., if this is nullptr, we can always dispatch
// it.
AutoEditActionDataSetter(
const EditorBase& aEditorBase,
EditAction aEditAction,
nsIPrincipal* aPrincipal = nullptr);
~AutoEditActionDataSetter();
void SetSelectionCreatedByDoubleclick(
bool aSelectionCreatedByDoubleclick) {
mSelectionCreatedByDoubleclick = aSelectionCreatedByDoubleclick;
}
[[nodiscard]]
bool SelectionCreatedByDoubleclick()
const {
return mSelectionCreatedByDoubleclick;
}
void UpdateEditAction(EditAction aEditAction) {
MOZ_ASSERT(!mHasTriedToDispatchBeforeInputEvent,
"It's too late to update EditAction since this may have "
"already dispatched a beforeinput event");
mEditAction = aEditAction;
}
/**
* CanHandle() or CanHandleAndHandleBeforeInput() must be called
* immediately after creating the instance. If caller does not need to
* handle "beforeinput" event or caller needs to set additional information
* the events later, use the former. Otherwise, use the latter. If caller
* uses the former, it's required to call MaybeDispatchBeforeInputEvent() by
* itself.
*
*/
[[nodiscard]]
bool CanHandle()
const {
#ifdef DEBUG
mHasCanHandleChecked =
true;
#endif // #ifdefn DEBUG
// Don't allow to run new edit action when an edit action caused
// destroying the editor while it's being handled.
if (mEditAction != EditAction::eInitializing &&
mEditorWasDestroyedDuringHandlingEditAction) {
NS_WARNING(
"Editor was destroyed during an edit action being handled");
return false;
}
return IsDataAvailable();
}
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
CanHandleAndMaybeDispatchBeforeInputEvent() {
if (MOZ_UNLIKELY(NS_WARN_IF(!CanHandle()))) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv = MaybeFlushPendingNotifications();
if (MOZ_UNLIKELY(NS_FAILED(rv))) {
return rv;
}
return MaybeDispatchBeforeInputEvent();
}
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
CanHandleAndFlushPendingNotifications() {
if (MOZ_UNLIKELY(NS_WARN_IF(!CanHandle()))) {
return NS_ERROR_NOT_INITIALIZED;
}
MOZ_ASSERT(MayEditActionRequireLayout(mRawEditAction));
return MaybeFlushPendingNotifications();
}
[[nodiscard]]
bool IsDataAvailable()
const {
return mSelection && mEditorBase.mDocument;
}
/**
* MaybeDispatchBeforeInputEvent() considers whether this instance needs to
* dispatch "beforeinput" event or not. Then,
* mHasTriedToDispatchBeforeInputEvent is set to true.
*
* @param aDeleteDirectionAndAmount
* If `MayEditActionDeleteAroundCollapsedSelection(
* mEditAction)` returns true, this must be set.
* Otherwise, don't set explicitly.
* @return If this method actually dispatches "beforeinput" event
* and it's canceled, returns
* NS_ERROR_EDITOR_ACTION_CANCELED.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent(
nsIEditor::EDirection aDeleteDirectionAndAmount = nsIEditor::eNone);
/**
* MarkAsBeforeInputHasBeenDispatched() should be called only when updating
* the DOM occurs asynchronously from user input (e.g., inserting blob
* object which is loaded asynchronously) and `beforeinput` has already
* been dispatched (always should be so).
*/
void MarkAsBeforeInputHasBeenDispatched() {
MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent());
MOZ_ASSERT(mEditAction == EditAction::ePaste ||
mEditAction == EditAction::ePasteAsQuotation ||
mEditAction == EditAction::eDrop);
mHasTriedToDispatchBeforeInputEvent =
true;
}
/**
* MarkAsHandled() is called before dispatching `input` event and notifying
* editor observers. After this is called, any nested edit action become
* non illegal case.
*/
void MarkAsHandled() {
MOZ_ASSERT(!mHandled);
mHandled =
true;
}
/**
* ShouldAlreadyHaveHandledBeforeInputEventDispatching() returns true if the
* edit action requires to handle "beforeinput" event but not yet dispatched
* it nor considered as not dispatched it and can dispatch it when this is
* called.
*/
bool ShouldAlreadyHaveHandledBeforeInputEventDispatching()
const {
return !HasTriedToDispatchBeforeInputEvent() &&
NeedsBeforeInputEventHandling(mEditAction) &&
IsBeforeInputEventEnabled()
/* &&
// If we still need to dispatch a clipboard event, we should
// dispatch it first, then, we need to dispatch beforeinput
// event later.
!NeedsToDispatchClipboardEvent()*/
;
}
/**
* HasTriedToDispatchBeforeInputEvent() returns true if the instance's
* MaybeDispatchBeforeInputEvent() has already been called.
*/
bool HasTriedToDispatchBeforeInputEvent()
const {
return mHasTriedToDispatchBeforeInputEvent;
}
bool IsCanceled()
const {
return mBeforeInputEventCanceled; }
/**
* Returns a `Selection` for normal selection. The lifetime is guaranteed
* during alive this instance in the stack.
*/
MOZ_KNOWN_LIVE Selection& SelectionRef()
const {
MOZ_ASSERT(!mSelection ||
(mSelection->GetType() == SelectionType::eNormal));
return *mSelection;
}
nsIPrincipal* GetPrincipal()
const {
return mPrincipal; }
EditAction GetEditAction()
const {
return mEditAction; }
template <
typename PT,
typename CT>
void SetSpellCheckRestartPoint(
const EditorDOMPointBase<PT, CT>& aPoint) {
MOZ_ASSERT(aPoint.IsSet());
// We should store only container and offset because new content may
// be inserted before referring child.
// XXX Shouldn't we compare whether aPoint is before
// mSpellCheckRestartPoint if it's set.
mSpellCheckRestartPoint =
EditorDOMPoint(aPoint.GetContainer(), aPoint.Offset());
}
void ClearSpellCheckRestartPoint() { mSpellCheckRestartPoint.Clear(); }
const EditorDOMPoint& GetSpellCheckRestartPoint()
const {
return mSpellCheckRestartPoint;
}
void SetData(
const nsAString& aData) {
MOZ_ASSERT(!mHasTriedToDispatchBeforeInputEvent,
"It's too late to set data since this may have already "
"dispatched a beforeinput event");
mData = aData;
}
const nsString& GetData()
const {
return mData; }
void SetColorData(
const nsAString& aData);
/**
* InitializeDataTransfer(DataTransfer*) sets mDataTransfer to
* aDataTransfer. In this case, aDataTransfer should not be read/write
* because it'll be set to InputEvent.dataTransfer and which should be
* read-only.
*/
void InitializeDataTransfer(DataTransfer* aDataTransfer);
/**
* InitializeDataTransfer(nsITransferable*) creates new DataTransfer
* instance, initializes it with aTransferable and sets mDataTransfer to
* it.
*/
void InitializeDataTransfer(nsITransferable* aTransferable);
/**
* InitializeDataTransfer(const nsAString&) creates new DataTransfer
* instance, initializes it with aString and sets mDataTransfer to it.
*/
void InitializeDataTransfer(
const nsAString& aString);
/**
* InitializeDataTransferWithClipboard() creates new DataTransfer instance,
* initializes it with clipboard and sets mDataTransfer to it.
*/
void InitializeDataTransferWithClipboard(
SettingDataTransfer aSettingDataTransfer, DataTransfer* aDataTransfer,
nsIClipboard::ClipboardType aClipboardType);
DataTransfer* GetDataTransfer()
const {
return mDataTransfer; }
/**
* AppendTargetRange() appends aTargetRange to target ranges. This should
* be used only by edit action handlers which do not want to set target
* ranges to selection ranges.
*/
void AppendTargetRange(dom::StaticRange& aTargetRange);
/**
* Make dispatching `beforeinput` forcibly non-cancelable.
*/
void MakeBeforeInputEventNonCancelable() {
mMakeBeforeInputEventNonCancelable =
true;
}
/**
* NotifyOfDispatchingClipboardEvent() is called after dispatching
* a clipboard event.
*/
void NotifyOfDispatchingClipboardEvent() {
MOZ_ASSERT(NeedsToDispatchClipboardEvent());
MOZ_ASSERT(!mHasTriedToDispatchClipboardEvent);
mHasTriedToDispatchClipboardEvent =
true;
}
void Abort() { mAborted =
true; }
bool IsAborted()
const {
return mAborted; }
void OnEditorDestroy() {
if (!mHandled && mHasTriedToDispatchBeforeInputEvent) {
// Remember the editor was destroyed only when this edit action is being
// handled because they are caused by mutation event listeners or
// something other unexpected event listeners. In the cases, new child
// edit action shouldn't been aborted.
mEditorWasDestroyedDuringHandlingEditAction =
true;
}
if (mParentData) {
mParentData->OnEditorDestroy();
}
}
bool HasEditorDestroyedDuringHandlingEditAction()
const {
return mEditorWasDestroyedDuringHandlingEditAction;
}
void SetTopLevelEditSubAction(EditSubAction aEditSubAction,
EDirection aDirection = eNone) {
mTopLevelEditSubAction = aEditSubAction;
TopLevelEditSubActionDataRef().Clear();
switch (mTopLevelEditSubAction) {
case EditSubAction::eInsertNode:
case EditSubAction::eMoveNode:
case EditSubAction::eCreateNode:
case EditSubAction::eSplitNode:
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
case EditSubAction::eSetTextProperty:
case EditSubAction::eRemoveTextProperty:
case EditSubAction::eRemoveAllTextProperties:
case EditSubAction::eSetText:
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::eCreateOrChangeList:
case EditSubAction::eIndent:
case EditSubAction::eOutdent:
case EditSubAction::eSetOrClearAlignment:
case EditSubAction::eCreateOrRemoveBlock:
case EditSubAction::eFormatBlockForHTMLCommand:
case EditSubAction::eMergeBlockContents:
case EditSubAction::eRemoveList:
case EditSubAction::eCreateOrChangeDefinitionListItem:
case EditSubAction::eInsertElement:
case EditSubAction::eInsertQuotation:
case EditSubAction::eInsertQuotedText:
case EditSubAction::ePasteHTMLContent:
case EditSubAction::eInsertHTMLSource:
case EditSubAction::eSetPositionToAbsolute:
case EditSubAction::eSetPositionToStatic:
case EditSubAction::eDecreaseZIndex:
case EditSubAction::eIncreaseZIndex:
MOZ_ASSERT(aDirection == eNext);
mDirectionOfTopLevelEditSubAction = eNext;
break;
case EditSubAction::eJoinNodes:
case EditSubAction::eDeleteText:
MOZ_ASSERT(aDirection == ePrevious);
mDirectionOfTopLevelEditSubAction = ePrevious;
break;
case EditSubAction::eUndo:
case EditSubAction::eRedo:
case EditSubAction::eComputeTextToOutput:
case EditSubAction::eCreatePaddingBRElementForEmptyEditor:
case EditSubAction::eMaintainWhiteSpaceVisibility:
case EditSubAction::eNone:
case EditSubAction::eReplaceHeadWithHTMLSource:
MOZ_ASSERT(aDirection == eNone);
mDirectionOfTopLevelEditSubAction = eNone;
break;
case EditSubAction::eDeleteNode:
case EditSubAction::eDeleteSelectedContent:
// Unfortunately, eDeleteNode and eDeleteSelectedContent is used with
// any direction. We might have specific sub-action for each
// direction, but there are some points referencing
// eDeleteSelectedContent so that we should keep storing direction
// as-is for now.
mDirectionOfTopLevelEditSubAction = aDirection;
break;
}
}
EditSubAction GetTopLevelEditSubAction()
const {
MOZ_ASSERT(IsDataAvailable());
return mTopLevelEditSubAction;
}
EDirection GetDirectionOfTopLevelEditSubAction()
const {
return mDirectionOfTopLevelEditSubAction;
}
const TopLevelEditSubActionData& TopLevelEditSubActionDataRef()
const {
return mParentData ? mParentData->TopLevelEditSubActionDataRef()
: mTopLevelEditSubActionData;
}
TopLevelEditSubActionData& TopLevelEditSubActionDataRef() {
return mParentData ? mParentData->TopLevelEditSubActionDataRef()
: mTopLevelEditSubActionData;
}
const EditSubActionData& EditSubActionDataRef()
const {
return mEditSubActionData;
}
EditSubActionData& EditSubActionDataRef() {
return mEditSubActionData; }
SelectionState& SavedSelectionRef() {
return mParentData ? mParentData->SavedSelectionRef() : mSavedSelection;
}
const SelectionState& SavedSelectionRef()
const {
return mParentData ? mParentData->SavedSelectionRef() : mSavedSelection;
}
RangeUpdater& RangeUpdaterRef() {
return mParentData ? mParentData->RangeUpdaterRef() : mRangeUpdater;
}
const RangeUpdater& RangeUpdaterRef()
const {
return mParentData ? mParentData->RangeUpdaterRef() : mRangeUpdater;
}
void UpdateSelectionCache(Selection& aSelection);
bool IsDispatchingInputEvent()
const {
return mDispatchingInputEvent ||
(mParentData && mParentData->IsDispatchingInputEvent());
}
void WillDispatchInputEvent() {
MOZ_ASSERT(!mDispatchingInputEvent);
mDispatchingInputEvent =
true;
}
void DidDispatchInputEvent() {
MOZ_ASSERT(mDispatchingInputEvent);
mDispatchingInputEvent =
false;
}
private:
bool IsBeforeInputEventEnabled()
const;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
MaybeFlushPendingNotifications()
const;
static bool NeedsBeforeInputEventHandling(EditAction aEditAction) {
MOZ_ASSERT(aEditAction != EditAction::eNone);
switch (aEditAction) {
case EditAction::eNone:
// If we're not handling edit action, we don't need to handle
// "beforeinput" event.
case EditAction::eNotEditing:
// If we're being initialized, we may need to create a padding <br>
// element, but it shouldn't cause `beforeinput` event.
case EditAction::eInitializing:
// If we're just selecting or getting table cells, we shouldn't
// dispatch `beforeinput` event.
case NS_EDIT_ACTION_CASES_ACCESSING_TABLE_DATA_WITHOUT_EDITING:
// If raw level transaction API is used, the API user needs to handle
// both "beforeinput" event and "input" event if it's necessary.
case EditAction::eUnknown:
// Hiding/showing password affects only layout so that we don't need
// to handle beforeinput event for it.
case EditAction::eHidePassword:
// We don't need to dispatch "beforeinput" event before
// "compositionstart".
case EditAction::eStartComposition:
// We don't need to let web apps know the mode change.
case EditAction::eEnableOrDisableCSS:
case EditAction::eEnableOrDisableAbsolutePositionEditor:
case EditAction::eEnableOrDisableResizer:
case EditAction::eEnableOrDisableInlineTableEditingUI:
// We don't need to let contents in chrome's editor to know the size
// change.
case EditAction::eSetWrapWidth:
// While resizing or moving element, we update only shadow, i.e.,
// don't touch to the DOM in content. Therefore, we don't need to
// dispatch "beforeinput" event.
case EditAction::eResizingElement:
case EditAction::eMovingElement:
// Perhaps, we don't need to dispatch "beforeinput" event for
// padding `<br>` element for empty editor because it's internal
// handling and it should be occurred by another change.
case EditAction::eCreatePaddingBRElementForEmptyEditor:
return false;
default:
return true;
}
}
bool NeedsToDispatchClipboardEvent()
const {
if (mHasTriedToDispatchClipboardEvent) {
return false;
}
switch (mEditAction) {
case EditAction::ePaste:
case EditAction::ePasteAsQuotation:
case EditAction::eCut:
case EditAction::eCopy:
return true;
default:
return false;
}
}
void MarkEditActionCanceled();
EditorBase& mEditorBase;
RefPtr<Selection> mSelection;
nsTArray<OwningNonNull<Selection>> mRetiredSelections;
// True if the selection was created by doubleclicking a word.
bool mSelectionCreatedByDoubleclick{
false};
nsCOMPtr<nsIPrincipal> mPrincipal;
// EditAction may be nested, for example, a command may be executed
// from mutation event listener which is run while editor changes
// the DOM tree. In such case, we need to handle edit action separately.
AutoEditActionDataSetter* mParentData;
// Cached selection for AutoSelectionRestorer.
SelectionState mSavedSelection;
// Utility class object for maintaining preserved ranges.
RangeUpdater mRangeUpdater;
// The data should be set to InputEvent.data.
nsString mData;
// The dataTransfer should be set to InputEvent.dataTransfer.
RefPtr<DataTransfer> mDataTransfer;
// They are used for result of InputEvent.getTargetRanges() of beforeinput.
OwningNonNullStaticRangeArray mTargetRanges;
// Start point where spell checker should check from. This is used only
// by TextEditor.
EditorDOMPoint mSpellCheckRestartPoint;
// Different from mTopLevelEditSubAction, its data should be stored only
// in the most ancestor AutoEditActionDataSetter instance since we don't
// want to pay the copying cost and sync cost.
TopLevelEditSubActionData mTopLevelEditSubActionData;
// Different from mTopLevelEditSubActionData, this stores temporaly data
// for current edit sub action.
EditSubActionData mEditSubActionData;
// mEditAction and mRawEditActions stores edit action. The difference of
// them is, if and only if edit actions are nested and parent edit action
// is one of trying to edit something, but nested one is not so, it's
// overwritten by the parent edit action.
EditAction mEditAction;
EditAction mRawEditAction;
// Different from its data, you can refer "current" AutoEditActionDataSetter
// instance's mTopLevelEditSubAction member since it's copied from the
// parent instance at construction and it's always cleared before this
// won't be overwritten and cleared before destruction.
EditSubAction mTopLevelEditSubAction;
EDirection mDirectionOfTopLevelEditSubAction;
bool mAborted;
// Set to true when this handles "beforeinput" event dispatching. Note
// that even if "beforeinput" event shouldn't be dispatched for this,
// instance, this is set to true when it's considered.
bool mHasTriedToDispatchBeforeInputEvent;
// Set to true if "beforeinput" event was dispatched and it's canceled.
bool mBeforeInputEventCanceled;
// Set to true if `beforeinput` event must not be cancelable even if
// its inputType is defined as cancelable by the standards.
bool mMakeBeforeInputEventNonCancelable;
// Set to true when the edit action handler tries to dispatch a clipboard
// event.
bool mHasTriedToDispatchClipboardEvent;
// The editor instance may be destroyed once temporarily if `document.write`
// etc runs. In such case, we should mark this flag of being handled
// edit action.
bool mEditorWasDestroyedDuringHandlingEditAction;
// This is set before dispatching `input` event and notifying editor
// observers.
bool mHandled;
// Whether the editor is dispatching a `beforeinput` or `input` event.
bool mDispatchingInputEvent =
false;
#ifdef DEBUG
mutable bool mHasCanHandleChecked =
false;
#endif // #ifdef DEBUG
AutoEditActionDataSetter() =
delete;
AutoEditActionDataSetter(
const AutoEditActionDataSetter& aOther) =
delete;
};
void UpdateEditActionData(
const nsAString& aData) {
mEditActionData->SetData(aData);
}
void NotifyOfDispatchingClipboardEvent() {
MOZ_ASSERT(mEditActionData);
mEditActionData->NotifyOfDispatchingClipboardEvent();
}
protected:
// May be called by friends.
/****************************************************************************
* Some friend classes are allowed to call the following protected methods.
* However, those methods won't prepare caches of some objects which are
* necessary for them. So, if you call them from friend classes, you need
* to make sure that AutoEditActionDataSetter is created.
****************************************************************************/
bool IsEditActionCanceled()
const {
MOZ_ASSERT(mEditActionData);
return mEditActionData->IsCanceled();
}
bool ShouldAlreadyHaveHandledBeforeInputEventDispatching()
const {
MOZ_ASSERT(mEditActionData);
return mEditActionData
->ShouldAlreadyHaveHandledBeforeInputEventDispatching();
}
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent() {
MOZ_ASSERT(mEditActionData);
return mEditActionData->MaybeDispatchBeforeInputEvent();
}
void MarkAsBeforeInputHasBeenDispatched() {
MOZ_ASSERT(mEditActionData);
return mEditActionData->MarkAsBeforeInputHasBeenDispatched();
}
bool HasTriedToDispatchBeforeInputEvent()
const {
return mEditActionData &&
mEditActionData->HasTriedToDispatchBeforeInputEvent();
}
bool IsEditActionDataAvailable()
const {
return mEditActionData && mEditActionData->IsDataAvailable();
}
bool IsTopLevelEditSubActionDataAvailable()
const {
return mEditActionData && !!GetTopLevelEditSubAction();
}
bool IsEditActionAborted()
const {
MOZ_ASSERT(mEditActionData);
return mEditActionData->IsAborted();
}
nsresult GetDataFromDataTransferOrClipboard(
DataTransfer* aDataTransfer, nsITransferable* aTransferable,
nsIClipboard::ClipboardType aClipboardType)
const;
/**
* SelectionRef() returns cached normal Selection. This is pretty faster than
* EditorBase::GetSelection() if available.
* Note that this never crash unless public methods ignore the result of
* AutoEditActionDataSetter::CanHandle() and keep handling edit action but any
* methods should stop handling edit action if it returns false.
*/
MOZ_KNOWN_LIVE Selection& SelectionRef()
const {
MOZ_ASSERT(mEditActionData);
MOZ_ASSERT(mEditActionData->SelectionRef().GetType() ==
SelectionType::eNormal);
return mEditActionData->SelectionRef();
}
nsIPrincipal* GetEditActionPrincipal()
const {
MOZ_ASSERT(mEditActionData);
return mEditActionData->GetPrincipal();
}
/**
* GetEditAction() returns EditAction which is being handled. If some
* edit actions are nested, this returns the innermost edit action.
*/
EditAction GetEditAction()
const {
return mEditActionData ? mEditActionData->GetEditAction()
: EditAction::eNone;
}
/**
* GetInputEventData() returns inserting or inserted text value with
* current edit action. The result is proper for InputEvent.data value.
*/
const nsString& GetInputEventData()
const {
return mEditActionData ? mEditActionData->GetData() : VoidString();
}
/**
* GetInputEventDataTransfer() returns inserting or inserted transferable
* content with current edit action. The result is proper for
* InputEvent.dataTransfer value.
*/
DataTransfer* GetInputEventDataTransfer()
const {
return mEditActionData ? mEditActionData->GetDataTransfer() : nullptr;
}
/**
* GetTopLevelEditSubAction() returns the top level edit sub-action.
* For example, if selected content is being replaced with inserted text,
* while removing selected content, the top level edit sub-action may be
* EditSubAction::eDeleteSelectedContent. However, while inserting new
* text, the top level edit sub-action may be EditSubAction::eInsertText.
* So, this result means what we are doing right now unless you're looking
* for a case which the method is called via mutation event listener or
* selectionchange event listener which are fired while handling the edit
* sub-action.
*/
EditSubAction GetTopLevelEditSubAction()
const {
return mEditActionData ? mEditActionData->GetTopLevelEditSubAction()
: EditSubAction::eNone;
}
/**
* GetDirectionOfTopLevelEditSubAction() returns direction which user
* intended for doing the edit sub-action.
*/
EDirection GetDirectionOfTopLevelEditSubAction()
const {
return mEditActionData
? mEditActionData->GetDirectionOfTopLevelEditSubAction()
: eNone;
}
/**
* SavedSelection() returns reference to saved selection which are
* stored by AutoSelectionRestorer.
*/
SelectionState& SavedSelectionRef() {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->SavedSelectionRef();
}
const SelectionState& SavedSelectionRef()
const {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->SavedSelectionRef();
}
RangeUpdater& RangeUpdaterRef() {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->RangeUpdaterRef();
}
const RangeUpdater& RangeUpdaterRef()
const {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->RangeUpdaterRef();
}
template <
typename PT,
typename CT>
void SetSpellCheckRestartPoint(
const EditorDOMPointBase<PT, CT>& aPoint) {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->SetSpellCheckRestartPoint(aPoint);
}
void ClearSpellCheckRestartPoint() {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->ClearSpellCheckRestartPoint();
}
const EditorDOMPoint& GetSpellCheckRestartPoint()
const {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->GetSpellCheckRestartPoint();
}
const TopLevelEditSubActionData& TopLevelEditSubActionDataRef()
const {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->TopLevelEditSubActionDataRef();
}
TopLevelEditSubActionData& TopLevelEditSubActionDataRef() {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->TopLevelEditSubActionDataRef();
}
const EditSubActionData& EditSubActionDataRef()
const {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->EditSubActionDataRef();
}
EditSubActionData& EditSubActionDataRef() {
MOZ_ASSERT(IsEditActionDataAvailable());
return mEditActionData->EditSubActionDataRef();
}
/**
--> --------------------
--> maximum size reached
--> --------------------