diff options
Diffstat (limited to 'editor/libeditor/EditorBase.h')
-rw-r--r-- | editor/libeditor/EditorBase.h | 2928 |
1 files changed, 2928 insertions, 0 deletions
diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h new file mode 100644 index 0000000000..fb78692071 --- /dev/null +++ b/editor/libeditor/EditorBase.h @@ -0,0 +1,2928 @@ +/* -*- 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 "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 nsAutoTArray +#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 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); + + bool IsInitialized() const { return !!mDocument; } + 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; + } + + 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; + } + + /** + * 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); + + /** + * Similar to the setter for wrapWidth, but just sets the editor + * internal state without actually changing the content being edited + * to wrap at that column. This should only be used by callers who + * are sure that their content is already set up correctly. + */ + void SetWrapColumn(int32_t aWrapColumn) { mWrapColumn = aWrapColumn; } + + /** + * 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 IsInPlaintextMode() const { + const bool isPlaintextMode = + (mFlags & nsIEditor::eEditorPlaintextMask) != 0; + MOZ_ASSERT_IF(IsTextEditor(), isPlaintextMode); + return isPlaintextMode; + } + + 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 IsWrapHackEnabled() const { + return (mFlags & nsIEditor::eEditorEnableWrapHackMask) != 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(int32_t 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, + }; + + /** + * 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 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, + 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 true if this should dispatch ePaste event + * before pasting. Otherwise, false. + * @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 PasteAsAction( + int32_t aClipboardType, bool aDispatchPasteEvent, + nsIPrincipal* aPrincipal = nullptr) = 0; + + /** + * Paste aTransferable at Selection. + * + * @param aTransferable Must not be nullptr. + * @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 PasteTransferableAsAction( + nsITransferable* aTransferable, nsIPrincipal* aPrincipal = nullptr) = 0; + + /** + * 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 true if this should dispatch ePaste event + * before pasting. Otherwise, false. + * @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 PasteAsQuotationAsAction( + int32_t aClipboardType, bool aDispatchPasteEvent, + nsIPrincipal* aPrincipal = nullptr) = 0; + + 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, + SplitNodeDirection aSplitNodeDirection); + 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.IsInitialized(); + } + + /** + * 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(dom::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, int32_t aClipboardType); + dom::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::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::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); + + 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; + } + } + + 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 HTMLEditor::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<dom::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; + +#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(); + } + + /** + * 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. + */ + dom::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 HTMLEditor::AutoSelectionRestorer. + */ + SelectionState& SavedSelectionRef() { + MOZ_ASSERT(IsHTMLEditor()); + MOZ_ASSERT(IsEditActionDataAvailable()); + return mEditActionData->SavedSelectionRef(); + } + const SelectionState& SavedSelectionRef() const { + MOZ_ASSERT(IsHTMLEditor()); + 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(); + } + + /** + * GetFirstIMESelectionStartPoint() and GetLastIMESelectionEndPoint() returns + * start of first IME selection range or end of last IME selection range if + * there is. Otherwise, returns non-set DOM point. + */ + template <typename EditorDOMPointType> + EditorDOMPointType GetFirstIMESelectionStartPoint() const; + template <typename EditorDOMPointType> + EditorDOMPointType GetLastIMESelectionEndPoint() const; + + /** + * IsSelectionRangeContainerNotContent() returns true if one of container + * of selection ranges is not a content node, i.e., a Document node. + */ + bool IsSelectionRangeContainerNotContent() const; + + /** + * OnInputText() is called when user inputs text with keyboard or something. + * + * @param aStringToInsert The string to insert. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + OnInputText(const nsAString& aStringToInsert); + + /** + * InsertTextAsSubAction() inserts aStringToInsert at selection. This + * should be used for handling it as an edit sub-action. + * + * @param aStringToInsert The string to insert. + * @param aSelectionHandling Specify whether selected content should be + * deleted or ignored. + */ + enum class SelectionHandling { Ignore, Delete }; + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertTextAsSubAction( + const nsAString& aStringToInsert, SelectionHandling aSelectionHandling); + + /** + * InsertTextWithTransaction() inserts aStringToInsert to aPointToInsert or + * better insertion point around it. If aPointToInsert isn't in a text node, + * this method looks for the nearest point in a text node with + * FindBetterInsertionPoint(). If there is no text node, this creates + * new text node and put aStringToInsert to it. + * + * @param aDocument The document of this editor. + * @param aStringToInsert The string to insert. + * @param aPointToInsert The point to insert aStringToInsert. + * Must be valid DOM point. + * @return If succeeded, returns the point after inserted + * aStringToInsert. So, when this method actually + * inserts string, returns a point in the text node. + * Otherwise, returns aPointToInsert. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<EditorDOMPoint, nsresult> + InsertTextWithTransaction(Document& aDocument, + const nsAString& aStringToInsert, + const EditorDOMPoint& aPointToInsert); + + /** + * InsertTextIntoTextNodeWithTransaction() inserts aStringToInsert into + * aOffset of aTextNode with transaction. + * + * @param aStringToInsert String to be inserted. + * @param aPointToInsert The insertion point. + * @param aSuppressIME true if it's not a part of IME composition. + * E.g., adjusting white-spaces during composition. + * false, otherwise. + */ + MOZ_CAN_RUN_SCRIPT nsresult InsertTextIntoTextNodeWithTransaction( + const nsAString& aStringToInsert, + const EditorDOMPointInText& aPointToInsert, bool aSuppressIME = false); + + /** + * SetTextNodeWithoutTransaction() is optimized path to set new value to + * the text node directly and without transaction. This is used when + * setting `<input>.value` and `<textarea>.value`. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + SetTextNodeWithoutTransaction(const nsAString& aString, Text& aTextNode); + + /** + * DeleteNodeWithTransaction() removes aContent from the DOM tree. + * + * @param aContent The node which will be removed form the DOM tree. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteNodeWithTransaction(nsIContent& aContent); + + /** + * InsertNodeWithTransaction() inserts aContentToInsert before the child + * specified by aPointToInsert. + * + * @param aContentToInsert The node to be inserted. + * @param aPointToInsert The insertion point of aContentToInsert. + * If this refers end of the container, the + * transaction will append the node to the + * container. Otherwise, will insert the node + * before child node referred by this. + * @return If succeeded, returns the new content node and + * point to put caret. + */ + template <typename ContentNodeType> + [[nodiscard]] MOZ_CAN_RUN_SCRIPT + Result<CreateNodeResultBase<ContentNodeType>, nsresult> + InsertNodeWithTransaction(ContentNodeType& aContentToInsert, + const EditorDOMPoint& aPointToInsert); + + /** + * InsertPaddingBRElementForEmptyLastLineWithTransaction() creates a padding + * <br> element with setting flags to NS_PADDING_FOR_EMPTY_LAST_LINE and + * inserts it around aPointToInsert. + * + * @param aPointToInsert The DOM point where should be <br> node inserted + * before. + * @return If succeeded, returns the new <br> element and + * point to put caret around it. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult> + InsertPaddingBRElementForEmptyLastLineWithTransaction( + const EditorDOMPoint& aPointToInsert); + + /** + * CloneAttributesWithTransaction() clones all attributes from + * aSourceElement to aDestElement after removing all attributes in + * aDestElement. + */ + MOZ_CAN_RUN_SCRIPT void CloneAttributesWithTransaction( + Element& aDestElement, Element& aSourceElement); + + /** + * CloneAttributeWithTransaction() copies aAttribute of aSourceElement to + * aDestElement. If aSourceElement doesn't have aAttribute, this removes + * aAttribute from aDestElement. + * + * @param aAttribute Attribute name to be cloned. + * @param aDestElement Element node which will be set aAttribute or + * whose aAttribute will be removed. + * @param aSourceElement Element node which provides the value of + * aAttribute in aDestElement. + */ + MOZ_CAN_RUN_SCRIPT nsresult CloneAttributeWithTransaction( + nsAtom& aAttribute, Element& aDestElement, Element& aSourceElement); + + /** + * RemoveAttributeWithTransaction() removes aAttribute from aElement. + * + * @param aElement Element node which will lose aAttribute. + * @param aAttribute Attribute name to be removed from aElement. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + RemoveAttributeWithTransaction(Element& aElement, nsAtom& aAttribute); + + MOZ_CAN_RUN_SCRIPT virtual nsresult RemoveAttributeOrEquivalent( + Element* aElement, nsAtom* aAttribute, bool aSuppressTransaction) = 0; + + /** + * SetAttributeWithTransaction() sets aAttribute of aElement to aValue. + * + * @param aElement Element node which will have aAttribute. + * @param aAttribute Attribute name to be set. + * @param aValue Attribute value be set to aAttribute. + */ + MOZ_CAN_RUN_SCRIPT nsresult SetAttributeWithTransaction( + Element& aElement, nsAtom& aAttribute, const nsAString& aValue); + + MOZ_CAN_RUN_SCRIPT virtual nsresult SetAttributeOrEquivalent( + Element* aElement, nsAtom* aAttribute, const nsAString& aValue, + bool aSuppressTransaction) = 0; + + /** + * Method to replace certain CreateElementNS() calls. + * + * @param aTag Tag you want. + */ + already_AddRefed<Element> CreateHTMLContent(const nsAtom* aTag) const; + + /** + * Creates text node which is marked as "maybe modified frequently" and + * "maybe masked" if this is a password editor. + */ + already_AddRefed<nsTextNode> CreateTextNode(const nsAString& aData) const; + + /** + * DoInsertText(), DoDeleteText(), DoReplaceText() and DoSetText() are + * wrapper of `CharacterData::InsertData()`, `CharacterData::DeleteData()`, + * `CharacterData::ReplaceData()` and `CharacterData::SetData()`. + */ + MOZ_CAN_RUN_SCRIPT void DoInsertText(dom::Text& aText, uint32_t aOffset, + const nsAString& aStringToInsert, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void DoDeleteText(dom::Text& aText, uint32_t aOffset, + uint32_t aCount, ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void DoReplaceText(dom::Text& aText, uint32_t aOffset, + uint32_t aCount, + const nsAString& aStringToInsert, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void DoSetText(dom::Text& aText, + const nsAString& aStringToSet, + ErrorResult& aRv); + + /** + * DeleteTextWithTransaction() removes text in the range from aTextNode. + * + * @param aTextNode The text node which should be modified. + * @param aOffset Start offset of removing text in aTextNode. + * @param aLength Length of removing text. + */ + MOZ_CAN_RUN_SCRIPT nsresult DeleteTextWithTransaction(dom::Text& aTextNode, + uint32_t aOffset, + uint32_t aLength); + + /** + * MarkElementDirty() sets a special dirty attribute on the element. + * Usually this will be called immediately after creating a new node. + * + * @param aElement The element for which to insert formatting. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + MarkElementDirty(Element& aElement) const; + + MOZ_CAN_RUN_SCRIPT nsresult + DoTransactionInternal(nsITransaction* aTransaction); + + /** + * Returns true if aNode is our root node. The root is: + * If TextEditor, the anonymous <div> element. + * If HTMLEditor, a <body> element or the document element which may not be + * editable if it's not in the design mode. + */ + bool IsRoot(const nsINode* inNode) const; + + /** + * Returns true if aNode is a descendant of our root node. + * See the comment for IsRoot() for what the root node means. + */ + bool IsDescendantOfRoot(const nsINode* inNode) const; + + /** + * Returns true when inserting text should be a part of current composition. + */ + bool ShouldHandleIMEComposition() const; + + template <typename EditorDOMPointType> + EditorDOMPointType GetFirstSelectionStartPoint() const; + template <typename EditorDOMPointType> + EditorDOMPointType GetFirstSelectionEndPoint() const; + + static nsresult GetEndChildNode(const Selection& aSelection, + nsIContent** aEndNode); + + template <typename PT, typename CT> + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + CollapseSelectionTo(const EditorDOMPointBase<PT, CT>& aPoint) const { + // We don't need to throw exception directly for a failure of updating + // selection. Therefore, let's use IgnoredErrorResult for the performance. + IgnoredErrorResult error; + CollapseSelectionTo(aPoint, error); + return error.StealNSResult(); + } + + template <typename PT, typename CT> + MOZ_CAN_RUN_SCRIPT void CollapseSelectionTo( + const EditorDOMPointBase<PT, CT>& aPoint, ErrorResult& aRv) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!aRv.Failed()); + + if (aPoint.GetInterlinePosition() != InterlinePosition::Undefined) { + if (MOZ_UNLIKELY(NS_FAILED(SelectionRef().SetInterlinePosition( + aPoint.GetInterlinePosition())))) { + NS_WARNING("Selection::SetInterlinePosition() failed"); + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } + + SelectionRef().CollapseInLimiter(aPoint, aRv); + if (MOZ_UNLIKELY(Destroyed())) { + NS_WARNING("Selection::CollapseInLimiter() caused destroying the editor"); + aRv.Throw(NS_ERROR_EDITOR_DESTROYED); + return; + } + NS_WARNING_ASSERTION(!aRv.Failed(), + "Selection::CollapseInLimiter() failed"); + } + + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + CollapseSelectionToStartOf(nsINode& aNode) const { + return CollapseSelectionTo(EditorRawDOMPoint(&aNode, 0u)); + } + + MOZ_CAN_RUN_SCRIPT void CollapseSelectionToStartOf(nsINode& aNode, + ErrorResult& aRv) const { + CollapseSelectionTo(EditorRawDOMPoint(&aNode, 0u), aRv); + } + + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + CollapseSelectionToEndOf(nsINode& aNode) const { + return CollapseSelectionTo(EditorRawDOMPoint::AtEndOf(aNode)); + } + + MOZ_CAN_RUN_SCRIPT void CollapseSelectionToEndOf(nsINode& aNode, + ErrorResult& aRv) const { + CollapseSelectionTo(EditorRawDOMPoint::AtEndOf(aNode), aRv); + } + + /** + * AllowsTransactionsToChangeSelection() returns true if editor allows any + * transactions to change Selection. Otherwise, transactions shouldn't + * change Selection. + */ + inline bool AllowsTransactionsToChangeSelection() const { + return mAllowsTransactionsToChangeSelection; + } + + /** + * MakeThisAllowTransactionsToChangeSelection() with true makes this editor + * allow transactions to change Selection. Otherwise, i.e., with false, + * makes this editor not allow transactions to change Selection. + */ + inline void MakeThisAllowTransactionsToChangeSelection(bool aAllow) { + mAllowsTransactionsToChangeSelection = aAllow; + } + + nsresult HandleInlineSpellCheck( + const EditorDOMPoint& aPreviouslySelectedStart, + const dom::AbstractRange* aRange = nullptr); + + /** + * Whether the editor is active on the DOM window. Note that when this + * returns true but GetFocusedElement() returns null, it means that this + * editor was focused when the DOM window was active. + */ + virtual bool IsActiveInDOMWindow() const; + + /** + * FindBetterInsertionPoint() tries to look for better insertion point which + * is typically the nearest text node and offset in it. + * + * @param aPoint Insertion point which the callers found. + * @return Better insertion point if there is. If not returns + * same point as aPoint. + */ + template <typename EditorDOMPointType> + EditorDOMPointType FindBetterInsertionPoint( + const EditorDOMPointType& aPoint) const; + + /** + * HideCaret() hides caret with nsCaret::AddForceHide() or may show carent + * with nsCaret::RemoveForceHide(). This does NOT set visibility of + * nsCaret. Therefore, this is stateless. + */ + void HideCaret(bool aHide); + + protected: // Edit sub-action handler + /** + * AutoCaretBidiLevelManager() computes bidi level of caret, deleting + * character(s) from aPointAtCaret at construction. Then, if you'll + * need to extend the selection, you should calls `UpdateCaretBidiLevel()`, + * then, this class may update caret bidi level for you if it's required. + */ + class MOZ_RAII AutoCaretBidiLevelManager final { + public: + /** + * @param aEditorBase The editor. + * @param aPointAtCaret Collapsed `Selection` point. + * @param aDirectionAndAmount The direction and amount to delete. + */ + template <typename PT, typename CT> + AutoCaretBidiLevelManager(const EditorBase& aEditorBase, + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPointBase<PT, CT>& aPointAtCaret); + + /** + * Failed() returns true if the constructor failed to handle the bidi + * information. + */ + bool Failed() const { return mFailed; } + + /** + * Canceled() returns true if when the caller should stop deleting + * characters since caret position is not visually adjacent the deleting + * characters and user does not wand to delete them in that case. + */ + bool Canceled() const { return mCanceled; } + + /** + * MaybeUpdateCaretBidiLevel() may update caret bidi level and schedule to + * paint it if they are necessary. + */ + void MaybeUpdateCaretBidiLevel(const EditorBase& aEditorBase) const; + + private: + Maybe<mozilla::intl::BidiEmbeddingLevel> mNewCaretBidiLevel; + bool mFailed = false; + bool mCanceled = false; + }; + + /** + * UndefineCaretBidiLevel() resets bidi level of the caret. + */ + void UndefineCaretBidiLevel() const; + + /** + * Flushing pending notifications if nsFrameSelection requires the latest + * layout information to compute deletion range. This may destroy the + * editor instance itself. When this returns false, don't keep doing + * anything. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool + FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + nsIEditor::EDirection aDirectionAndAmount) const; + + /** + * DeleteSelectionAsSubAction() removes selection content or content around + * caret with transactions. This should be used for handling it as an + * edit sub-action. + * + * @param aDirectionAndAmount How much range should be removed. + * @param aStripWrappers Whether the parent blocks should be removed + * when they become empty. If this instance is + * a TextEditor, Must be nsIEditor::eNoStrip. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteSelectionAsSubAction(nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers); + + /** + * This method handles "delete selection" commands. + * NOTE: Don't call this method recursively from the helper methods since + * when nobody handled it without canceling and returing an error, + * this falls it back to `DeleteSelectionWithTransaction()`. + * + * @param aDirectionAndAmount Direction of the deletion. + * @param aStripWrappers Must be nsIEditor::eNoStrip if this is a + * TextEditor instance. Otherwise, + * nsIEditor::eStrip is also valid. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<EditActionResult, nsresult> + HandleDeleteSelection(nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers) = 0; + + /** + * ReplaceSelectionAsSubAction() replaces selection with aString. + * + * @param aString The string to replace. + */ + MOZ_CAN_RUN_SCRIPT nsresult + ReplaceSelectionAsSubAction(const nsAString& aString); + + /** + * HandleInsertText() handles inserting text at selection. + * + * @param aEditSubAction Must be EditSubAction::eInsertText or + * EditSubAction::eInsertTextComingFromIME. + * @param aInsertionString String to be inserted at selection. + * @param aSelectionHandling Specify whether selected content should be + * deleted or ignored. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<EditActionResult, nsresult> + HandleInsertText(EditSubAction aEditSubAction, + const nsAString& aInsertionString, + SelectionHandling aSelectionHandling) = 0; + + /** + * InsertWithQuotationsAsSubAction() inserts aQuotedText with appending ">" + * to start of every line. + * + * @param aQuotedText String to insert. This will be quoted by ">" + * automatically. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual nsresult + InsertWithQuotationsAsSubAction(const nsAString& aQuotedText) = 0; + + /** + * PrepareInsertContent() is a helper method of InsertTextAt(), + * HTMLEditor::HTMLWithContextInserter::Run(). They insert content coming + * from clipboard or drag and drop. Before that, they may need to remove + * selected contents and adjust selection. This does them instead. + * + * @param aPointToInsert Point to insert. Must be set. Callers + * shouldn't use this instance after calling this + * method because this method may cause changing + * the DOM tree and Selection. + */ + enum class DeleteSelectedContent : bool { + No, // Don't delete selection + Yes, // Delete selected content + }; + MOZ_CAN_RUN_SCRIPT nsresult + PrepareToInsertContent(const EditorDOMPoint& aPointToInsert, + DeleteSelectedContent aDeleteSelectedContent); + + /** + * InsertTextAt() inserts aStringToInsert at aPointToInsert. + * + * @param aStringToInsert The string which you want to insert. + * @param aPointToInsert The insertion point. + */ + MOZ_CAN_RUN_SCRIPT nsresult InsertTextAt( + const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert, + DeleteSelectedContent aDeleteSelectedContent); + + /** + * Return whether the data is safe to insert as the source and destination + * principals match, or we are in a editor context where this doesn't matter. + * Otherwise, the data must be sanitized first. + */ + enum class SafeToInsertData : bool { No, Yes }; + SafeToInsertData IsSafeToInsertData(nsIPrincipal* aSourcePrincipal) const; + + protected: // Called by helper classes. + /** + * OnStartToHandleTopLevelEditSubAction() is called when + * GetTopLevelEditSubAction() is EditSubAction::eNone and somebody starts to + * handle aEditSubAction. + * + * @param aTopLevelEditSubAction Top level edit sub action which + * will be handled soon. + * @param aDirectionOfTopLevelEditSubAction Direction of aEditSubAction. + */ + MOZ_CAN_RUN_SCRIPT virtual void OnStartToHandleTopLevelEditSubAction( + EditSubAction aTopLevelEditSubAction, + nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, + ErrorResult& aRv); + + /** + * OnEndHandlingTopLevelEditSubAction() is called after + * SetTopLevelEditSubAction() is handled. + */ + MOZ_CAN_RUN_SCRIPT virtual nsresult OnEndHandlingTopLevelEditSubAction(); + + /** + * OnStartToHandleEditSubAction() and OnEndHandlingEditSubAction() are called + * when starting to handle an edit sub action and ending handling an edit + * sub action. + */ + void OnStartToHandleEditSubAction() { EditSubActionDataRef().Clear(); } + void OnEndHandlingEditSubAction() { EditSubActionDataRef().Clear(); } + + /** + * (Begin|End)PlaceholderTransaction() are called by AutoPlaceholderBatch. + * This set of methods are similar to the (Begin|End)Transaction(), but do + * not use the transaction managers batching feature. Instead we use a + * placeholder transaction to wrap up any further transaction while the + * batch is open. The advantage of this is that placeholder transactions + * can later merge, if needed. Merging is unavailable between transaction + * manager batches. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void BeginPlaceholderTransaction( + nsStaticAtom& aTransactionName, const char* aRequesterFuncName); + enum class ScrollSelectionIntoView { No, Yes }; + MOZ_CAN_RUN_SCRIPT_BOUNDARY void EndPlaceholderTransaction( + ScrollSelectionIntoView aScrollSelectionIntoView, + const char* aRequesterFuncName); + + void BeginUpdateViewBatch(const char* aRequesterFuncName); + MOZ_CAN_RUN_SCRIPT void EndUpdateViewBatch(const char* aRequesterFuncName); + + /** + * Used by HTMLEditor::AutoTransactionBatch, nsIEditor::BeginTransaction + * and nsIEditor::EndTransation. After calling BeginTransactionInternal(), + * all transactions will be treated as an atomic transaction. I.e., two or + * more transactions are undid once. + * XXX What's the difference with PlaceholderTransaction? Should we always + * use it instead? + */ + MOZ_CAN_RUN_SCRIPT void BeginTransactionInternal( + const char* aRequesterFuncName); + MOZ_CAN_RUN_SCRIPT void EndTransactionInternal( + const char* aRequesterFuncName); + + protected: // Shouldn't be used by friend classes + /** + * The default destructor. This should suffice. Should this be pure virtual + * for someone to derive from the EditorBase later? I don't believe so. + */ + virtual ~EditorBase(); + + /** + * @param aDocument The dom document interface being observed + * @param aRootElement + * This is the root of the editable section of this + * document. If it is null then we get root from document + * body. + * @param aSelectionController + * The selection controller of selections which will be + * used in this editor. + * @param aFlags Some of nsIEditor::eEditor*Mask flags. + */ + MOZ_CAN_RUN_SCRIPT nsresult + InitInternal(Document& aDocument, Element* aRootElement, + nsISelectionController& aSelectionController, uint32_t aFlags); + + /** + * PostCreateInternal() should be called after InitInternal(), and is the time + * that the editor tells its documentStateObservers that the document has been + * created. + */ + MOZ_CAN_RUN_SCRIPT nsresult PostCreateInternal(); + + /** + * PreDestroyInternal() is called before the editor goes away, and gives the + * editor a chance to tell its documentStateObservers that the document is + * going away. + */ + MOZ_CAN_RUN_SCRIPT virtual void PreDestroyInternal(); + + MOZ_ALWAYS_INLINE EditorType GetEditorType() const { + return mIsHTMLEditorClass ? EditorType::HTML : EditorType::Text; + } + + /** + * Check whether the caller can keep handling focus event. + * + * @param aOriginalEventTargetNode The original event target of the focus + * event. + */ + [[nodiscard]] bool CanKeepHandlingFocusEvent( + const nsINode& aOriginalEventTargetNode) const; + + /** + * If this editor has skipped spell checking and not yet flushed, this runs + * the spell checker. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult FlushPendingSpellCheck(); + + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult EnsureEmptyTextFirstChild(); + + int32_t WrapWidth() const { return mWrapColumn; } + + /** + * ToGenericNSResult() computes proper nsresult value for the editor users. + * This should be used only when public methods return result of internal + * methods. + */ + static inline nsresult ToGenericNSResult(nsresult aRv) { + switch (aRv) { + // If the editor is destroyed while handling an edit action, editor needs + // to stop handling it. However, editor throw exception in this case + // because Chrome does not throw exception even in this case. + case NS_ERROR_EDITOR_DESTROYED: + return NS_OK; + // If editor meets unexpected DOM tree due to modified by mutation event + // listener, editor needs to stop handling it. However, editor shouldn't + // return error for the users because Chrome does not throw exception in + // this case. + case NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE: + return NS_OK; + // If the editing action is canceled by event listeners, editor needs + // to stop handling it. However, editor shouldn't return error for + // the callers but they should be able to distinguish whether it's + // canceled or not. Although it's DOM specific code, let's return + // DOM_SUCCESS_DOM_NO_OPERATION here. + case NS_ERROR_EDITOR_ACTION_CANCELED: + return NS_SUCCESS_DOM_NO_OPERATION; + // If there is no selection range or editable selection ranges, editor + // needs to stop handling it. However, editor shouldn't return error for + // the callers to avoid throwing exception. However, they may want to + // check whether it works or not. Therefore, we should return + // NS_SUCCESS_DOM_NO_OPERATION instead. + case NS_ERROR_EDITOR_NO_EDITABLE_RANGE: + return NS_SUCCESS_DOM_NO_OPERATION; + // If CreateNodeResultBase::SuggestCaretPointTo etc is called with + // SuggestCaret::AndIgnoreTrivialErrors and CollapseSelectionTo returns + // non-critical error e.g., not NS_ERROR_EDITOR_DESTROYED, it returns + // this success code instead of actual error code for making the caller + // handle the case easier. Therefore, this should be mapped to NS_OK + // for the users of editor. + case NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR: + return NS_OK; + default: + return aRv; + } + } + + /** + * GetDocumentCharsetInternal() returns charset of the document. + */ + nsresult GetDocumentCharsetInternal(nsACString& aCharset) const; + + /** + * ComputeValueInternal() computes string value of this editor for given + * format. This may be too expensive if it's in hot path. + * + * @param aFormatType MIME type like "text/plain". + * @param aDocumentEncoderFlags Flags of nsIDocumentEncoder. + * @param aCharset Encoding of the document. + */ + nsresult ComputeValueInternal(const nsAString& aFormatType, + uint32_t aDocumentEncoderFlags, + nsAString& aOutputString) const; + + /** + * GetAndInitDocEncoder() returns a document encoder instance for aFormatType + * after initializing it. The result may be cached for saving recreation + * cost. + * + * @param aFormatType MIME type like "text/plain". + * @param aDocumentEncoderFlags Flags of nsIDocumentEncoder. + * @param aCharset Encoding of the document. + */ + already_AddRefed<nsIDocumentEncoder> GetAndInitDocEncoder( + const nsAString& aFormatType, uint32_t aDocumentEncoderFlags, + const nsACString& aCharset) const; + + /** + * EnsurePaddingBRElementInMultilineEditor() creates a padding `<br>` element + * at end of multiline text editor. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + EnsurePaddingBRElementInMultilineEditor(); + + /** + * SelectAllInternal() should be used instead of SelectAll() in editor + * because SelectAll() creates AutoEditActionSetter but we should avoid + * to create it as far as possible. + */ + MOZ_CAN_RUN_SCRIPT virtual nsresult SelectAllInternal(); + + nsresult DetermineCurrentDirection(); + + /** + * DispatchInputEvent() dispatches an "input" event synchronously or + * asynchronously if it's not safe to dispatch. + */ + MOZ_CAN_RUN_SCRIPT void DispatchInputEvent(); + + /** + * Called after a transaction is done successfully. + */ + MOZ_CAN_RUN_SCRIPT void DoAfterDoTransaction(nsITransaction* aTransaction); + + /** + * Called after a transaction is undone successfully. + */ + + MOZ_CAN_RUN_SCRIPT void DoAfterUndoTransaction(); + + /** + * Called after a transaction is redone successfully. + */ + MOZ_CAN_RUN_SCRIPT void DoAfterRedoTransaction(); + + /** + * Tell the doc state listeners that the doc state has changed. + */ + enum TDocumentListenerNotification { + eDocumentCreated, + eDocumentToBeDestroyed, + eDocumentStateChanged + }; + MOZ_CAN_RUN_SCRIPT nsresult + NotifyDocumentListeners(TDocumentListenerNotification aNotificationType); + + /** + * Make the given selection span the entire document. + */ + MOZ_CAN_RUN_SCRIPT virtual nsresult SelectEntireDocument() = 0; + + /** + * Helper method for scrolling the selection into view after + * an edit operation. + * + * Editor methods *should* call this method instead of the versions + * in the various selection interfaces, since this makes sure that + * the editor's sync/async settings for reflowing, painting, and scrolling + * match. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + ScrollSelectionFocusIntoView() const; + + virtual nsresult InstallEventListeners(); + virtual void CreateEventListeners(); + virtual void RemoveEventListeners(); + [[nodiscard]] bool IsListeningToEvents() const; + + /** + * Called if and only if this editor is in readonly mode. + */ + void HandleKeyPressEventInReadOnlyMode( + WidgetKeyboardEvent& aKeyboardEvent) const; + + /** + * Get the input event target. This might return null. + */ + virtual already_AddRefed<Element> GetInputEventTargetElement() const = 0; + + /** + * Return true if spellchecking should be enabled for this editor. + */ + [[nodiscard]] bool GetDesiredSpellCheckState(); + + [[nodiscard]] bool CanEnableSpellCheck() const { + // Check for password/readonly/disabled, which are not spellchecked + // regardless of DOM. Also, check to see if spell check should be skipped + // or not. + return !IsPasswordEditor() && !IsReadonly() && !ShouldSkipSpellCheck(); + } + + /** + * InitializeSelectionAncestorLimit() is called by InitializeSelection(). + * When this is called, each implementation has to call + * Selection::SetAncestorLimiter() with aAnotherLimit. + * + * @param aAncestorLimit New ancestor limit of Selection. This always + * has parent node. So, it's always safe to + * call SetAncestorLimit() with this node. + */ + virtual void InitializeSelectionAncestorLimit( + nsIContent& aAncestorLimit) const; + + /** + * Initializes selection and caret for the editor at getting focus. If + * aOriginalEventTargetNode isn't a host of the editor, i.e., the editor + * doesn't get focus, this does nothing. + * + * @param aOriginalEventTargetNode The original event target node of the + * focus event. + */ + MOZ_CAN_RUN_SCRIPT nsresult + InitializeSelection(const nsINode& aOriginalEventTargetNode); + + enum NotificationForEditorObservers { + eNotifyEditorObserversOfEnd, + eNotifyEditorObserversOfBefore, + eNotifyEditorObserversOfCancel + }; + MOZ_CAN_RUN_SCRIPT void NotifyEditorObservers( + NotificationForEditorObservers aNotification); + + /** + * HowToHandleCollapsedRange indicates how collapsed range should be treated. + */ + enum class HowToHandleCollapsedRange { + // Ignore collapsed range. + Ignore, + // Extend collapsed range for removing previous content. + ExtendBackward, + // Extend collapsed range for removing next content. + ExtendForward, + }; + + static HowToHandleCollapsedRange HowToHandleCollapsedRangeFor( + nsIEditor::EDirection aDirectionAndAmount) { + switch (aDirectionAndAmount) { + case nsIEditor::eNone: + return HowToHandleCollapsedRange::Ignore; + case nsIEditor::ePrevious: + return HowToHandleCollapsedRange::ExtendBackward; + case nsIEditor::eNext: + return HowToHandleCollapsedRange::ExtendForward; + case nsIEditor::ePreviousWord: + case nsIEditor::eNextWord: + case nsIEditor::eToBeginningOfLine: + case nsIEditor::eToEndOfLine: + // If the amount is word or + // line,`AutoRangeArray::ExtendAnchorFocusRangeFor()` must have already + // been extended collapsed ranges before. + return HowToHandleCollapsedRange::Ignore; + } + MOZ_ASSERT_UNREACHABLE("Invalid nsIEditor::EDirection value"); + return HowToHandleCollapsedRange::Ignore; + } + + /** + * InsertDroppedDataTransferAsAction() inserts all data items in aDataTransfer + * at aDroppedAt unless the editor is destroyed. + * + * @param aEditActionData The edit action data whose edit action must be + * EditAction::eDrop. + * @param aDataTransfer The data transfer object which is dropped. + * @param aDroppedAt The DOM tree position whether aDataTransfer + * is dropped. + * @param aSourcePrincipal Principal of the source of the drag. + * May be nullptr if it comes from another app + * or process. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual nsresult + InsertDroppedDataTransferAsAction(AutoEditActionDataSetter& aEditActionData, + dom::DataTransfer& aDataTransfer, + const EditorDOMPoint& aDroppedAt, + nsIPrincipal* aSourcePrincipal) = 0; + + /** + * DeleteSelectionByDragAsAction() removes selection and dispatch "input" + * event whose inputType is "deleteByDrag". + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteSelectionByDragAsAction(bool aDispatchInputEvent); + + /** + * DeleteSelectionWithTransaction() removes selected content or content + * around caret with transactions and remove empty inclusive ancestor + * inline elements of collapsed selection after removing the contents. + * + * @param aDirectionAndAmount How much range should be removed. + * @param aStripWrappers Whether the parent blocks should be removed + * when they become empty. + * Note that this must be `nsIEditor::eNoStrip` + * if this is a TextEditor because anyway it'll + * be ignored. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteSelectionWithTransaction(nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers); + + /** + * DeleteRangesWithTransaction() removes content in aRangesToDelete or content + * around collapsed ranges in aRangesToDelete with transactions and remove + * empty inclusive ancestor inline elements of collapsed ranges after + * removing the contents. + * + * @param aDirectionAndAmount How much range should be removed. + * @param aStripWrappers Whether the parent blocks should be removed + * when they become empty. + * Note that this must be `nsIEditor::eNoStrip` + * if this is a TextEditor because anyway it'll + * be ignored. + * @param aRangesToDelete The ranges to delete content. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteRangesWithTransaction(nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + const AutoRangeArray& aRangesToDelete); + + /** + * Create an aggregate transaction for delete the content in aRangesToDelete. + * The result may include DeleteNodeTransactions and/or DeleteTextTransactions + * as its children. + * + * @param aHowToHandleCollapsedRange + * How to handle collapsed ranges. + * @param aRangesToDelete The ranges to delete content. + * @return If it can remove the content in ranges, returns + * an aggregate transaction which has some + * DeleteNodeTransactions and/or + * DeleteTextTransactions as its children. + */ + already_AddRefed<EditAggregateTransaction> + CreateTransactionForDeleteSelection( + HowToHandleCollapsedRange aHowToHandleCollapsedRange, + const AutoRangeArray& aRangesToDelete); + + /** + * Create a transaction for removing the nodes and/or text around + * aRangeToDelete. + * + * @param aCollapsedRange The range to be removed. This must be + * collapsed. + * @param aHowToHandleCollapsedRange + * How to handle aCollapsedRange. Must + * be HowToHandleCollapsedRange::ExtendBackward or + * HowToHandleCollapsedRange::ExtendForward. + * @return The transaction to remove content around the + * range. Its type is DeleteNodeTransaction or + * DeleteTextTransaction. + */ + already_AddRefed<EditTransactionBase> CreateTransactionForCollapsedRange( + const nsRange& aCollapsedRange, + HowToHandleCollapsedRange aHowToHandleCollapsedRange); + + /** + * ComputeInsertedRange() returns actual range modified by inserting string + * in a text node. If mutation event listener changed the text data, this + * returns a range which covers all over the text data. + */ + std::tuple<EditorDOMPointInText, EditorDOMPointInText> ComputeInsertedRange( + const EditorDOMPointInText& aInsertedPoint, + const nsAString& aInsertedString) const; + + /** + * EnsureComposition() should be called by composition event handlers. This + * tries to get the composition for the event and set it to mComposition. + * However, this may fail because the composition may be committed before + * the event comes to the editor. + * + * @return true if there is a composition. Otherwise, for example, + * a composition event handler in web contents moved focus + * for committing the composition, returns false. + */ + bool EnsureComposition(WidgetCompositionEvent& aCompositionEvent); + + /** + * See comment of IsCopyToClipboardAllowed() for the detail. + */ + virtual bool IsCopyToClipboardAllowedInternal() const { + MOZ_ASSERT(IsEditActionDataAvailable()); + return !SelectionRef().IsCollapsed(); + } + + /** + * Helper for Is{Cut|Copy}CommandEnabled. + * Look for a listener for the given command, including up the target chain. + */ + MOZ_CAN_RUN_SCRIPT bool CheckForClipboardCommandListener( + nsAtom* aCommand, EventMessage aEventMessage) const; + + /** + * FireClipboardEvent() may dispatch a clipboard event. + * + * @param aEventMessage The event message which may be set to the + * dispatching event. + * @param aClipboardType Working with global clipboard or selection. + * @param aActionTaken [optional][out] If set to non-nullptr, will be + * set to true if the action for the event is + * handled or prevented default. + * @return false if dispatching event is canceled. + */ + bool FireClipboardEvent(EventMessage aEventMessage, int32_t aClipboardType, + bool* aActionTaken = nullptr); + + private: + nsCOMPtr<nsISelectionController> mSelectionController; + RefPtr<Document> mDocument; + + AutoEditActionDataSetter* mEditActionData; + + /** + * SetTextDirectionTo() sets text-direction of the root element. + * Should use SwitchTextDirectionTo() or ToggleTextDirection() instead. + * This is a helper class of them. + */ + nsresult SetTextDirectionTo(TextDirection aTextDirection); + + protected: // helper classes which may be used by friends + /** + * Stack based helper class for batching a collection of transactions inside + * a placeholder transaction. Different from AutoTransactionBatch, this + * notifies editor observers of before/end edit action handling, and + * dispatches "input" event if it's necessary. + */ + class MOZ_RAII AutoPlaceholderBatch final { + public: + /** + * @param aRequesterFuncName function name which wants to end the batch. + * This won't be stored nor exposed to selection listeners etc, used only + * for logging. This MUST be alive when the destructor runs. + */ + AutoPlaceholderBatch(EditorBase& aEditorBase, + ScrollSelectionIntoView aScrollSelectionIntoView, + const char* aRequesterFuncName) + : mEditorBase(aEditorBase), + mScrollSelectionIntoView(aScrollSelectionIntoView), + mRequesterFuncName(aRequesterFuncName) { + mEditorBase->BeginPlaceholderTransaction(*nsGkAtoms::_empty, + mRequesterFuncName); + } + + AutoPlaceholderBatch(EditorBase& aEditorBase, + nsStaticAtom& aTransactionName, + ScrollSelectionIntoView aScrollSelectionIntoView, + const char* aRequesterFuncName) + : mEditorBase(aEditorBase), + mScrollSelectionIntoView(aScrollSelectionIntoView), + mRequesterFuncName(aRequesterFuncName) { + mEditorBase->BeginPlaceholderTransaction(aTransactionName, + mRequesterFuncName); + } + + ~AutoPlaceholderBatch() { + mEditorBase->EndPlaceholderTransaction(mScrollSelectionIntoView, + mRequesterFuncName); + } + + protected: + const OwningNonNull<EditorBase> mEditorBase; + const ScrollSelectionIntoView mScrollSelectionIntoView; + const char* const mRequesterFuncName; + }; + + /** + * AutoEditSubActionNotifier notifies editor of start to handle + * top level edit sub-action and end handling top level edit sub-action. + */ + class MOZ_RAII AutoEditSubActionNotifier final { + public: + MOZ_CAN_RUN_SCRIPT AutoEditSubActionNotifier( + EditorBase& aEditorBase, EditSubAction aEditSubAction, + nsIEditor::EDirection aDirection, ErrorResult& aRv) + : mEditorBase(aEditorBase), mIsTopLevel(true) { + // The top level edit sub action has already be set if this is nested call + // XXX Looks like that this is not aware of unexpected nested edit action + // handling via selectionchange event listener or mutation event + // listener. + if (!mEditorBase.GetTopLevelEditSubAction()) { + MOZ_KnownLive(mEditorBase) + .OnStartToHandleTopLevelEditSubAction(aEditSubAction, aDirection, + aRv); + } else { + mIsTopLevel = false; + } + mEditorBase.OnStartToHandleEditSubAction(); + } + + MOZ_CAN_RUN_SCRIPT ~AutoEditSubActionNotifier() { + mEditorBase.OnEndHandlingEditSubAction(); + if (mIsTopLevel) { + MOZ_KnownLive(mEditorBase).OnEndHandlingTopLevelEditSubAction(); + } + } + + protected: + EditorBase& mEditorBase; + bool mIsTopLevel; + }; + + /** + * Stack based helper class for turning off active selection adjustment + * by low level transactions + */ + class MOZ_RAII AutoTransactionsConserveSelection final { + public: + explicit AutoTransactionsConserveSelection(EditorBase& aEditorBase) + : mEditorBase(aEditorBase), + mAllowedTransactionsToChangeSelection( + aEditorBase.AllowsTransactionsToChangeSelection()) { + mEditorBase.MakeThisAllowTransactionsToChangeSelection(false); + } + + ~AutoTransactionsConserveSelection() { + mEditorBase.MakeThisAllowTransactionsToChangeSelection( + mAllowedTransactionsToChangeSelection); + } + + protected: + EditorBase& mEditorBase; + bool mAllowedTransactionsToChangeSelection; + }; + + /*************************************************************************** + * stack based helper class for batching reflow and paint requests. + */ + class MOZ_RAII AutoUpdateViewBatch final { + public: + /** + * @param aRequesterFuncName function name which wants to end the batch. + * This won't be stored nor exposed to selection listeners etc, used only + * for logging. This MUST be alive when the destructor runs. + */ + MOZ_CAN_RUN_SCRIPT explicit AutoUpdateViewBatch( + EditorBase& aEditorBase, const char* aRequesterFuncName) + : mEditorBase(aEditorBase), mRequesterFuncName(aRequesterFuncName) { + mEditorBase.BeginUpdateViewBatch(mRequesterFuncName); + } + + MOZ_CAN_RUN_SCRIPT ~AutoUpdateViewBatch() { + MOZ_KnownLive(mEditorBase).EndUpdateViewBatch(mRequesterFuncName); + } + + protected: + EditorBase& mEditorBase; + const char* const mRequesterFuncName; + }; + + protected: + enum Tristate { eTriUnset, eTriFalse, eTriTrue }; + + // MIME type of the doc we are editing. + nsString mContentMIMEType; + + RefPtr<mozInlineSpellChecker> mInlineSpellChecker; + // Reference to text services document for mInlineSpellChecker. + RefPtr<TextServicesDocument> mTextServicesDocument; + + RefPtr<TransactionManager> mTransactionManager; + // Cached root node. + RefPtr<Element> mRootElement; + + // The form field as an event receiver. + nsCOMPtr<dom::EventTarget> mEventTarget; + RefPtr<EditorEventListener> mEventListener; + // Strong reference to placeholder for begin/end batch purposes. + RefPtr<PlaceholderTransaction> mPlaceholderTransaction; + // Name of placeholder transaction. + nsStaticAtom* mPlaceholderName; + // Saved selection state for placeholder transaction batching. + mozilla::Maybe<SelectionState> mSelState; + // IME composition this is not null between compositionstart and + // compositionend. + RefPtr<TextComposition> mComposition; + + RefPtr<TextInputListener> mTextInputListener; + + RefPtr<IMEContentObserver> mIMEContentObserver; + + // These members cache last encoder and its type for the performance in + // TextEditor::ComputeTextValue() which is the implementation of + // `<input>.value` and `<textarea>.value`. See `GetAndInitDocEncoder()`. + mutable nsCOMPtr<nsIDocumentEncoder> mCachedDocumentEncoder; + mutable nsString mCachedDocumentEncoderType; + + // Listens to all low level actions on the doc. + // Edit action listener is currently used by highlighter of the findbar and + // the spellchecker. So, we should reserve only 2 items. + typedef AutoTArray<OwningNonNull<nsIEditActionListener>, 2> + AutoActionListenerArray; + AutoActionListenerArray mActionListeners; + // Listen to overall doc state (dirty or not, just created, etc.). + // Document state listener is currently used by FinderHighlighter and + // BlueGriffon so that reserving only one is enough. + typedef AutoTArray<OwningNonNull<nsIDocumentStateListener>, 1> + AutoDocumentStateListenerArray; + AutoDocumentStateListenerArray mDocStateListeners; + + // Number of modifications (for undo/redo stack). + uint32_t mModCount; + // Behavior flags. See nsIEditor.idl for the flags we use. + uint32_t mFlags; + + int32_t mUpdateCount; + + // Nesting count for batching. + int32_t mPlaceholderBatch; + + int32_t mWrapColumn; + int32_t mNewlineHandling; + int32_t mCaretStyle; + + // -1 = not initialized + int8_t mDocDirtyState; + // A Tristate value. + uint8_t mSpellcheckCheckboxState; + + // If true, initialization was succeeded. + bool mInitSucceeded; + // If false, transactions should not change Selection even after modifying + // the DOM tree. + bool mAllowsTransactionsToChangeSelection; + // Whether PreDestroy has been called. + bool mDidPreDestroy; + // Whether PostCreate has been called. + bool mDidPostCreate; + bool mDispatchInputEvent; + // True while the instance is handling an edit sub-action. + bool mIsInEditSubAction; + // Whether caret is hidden forcibly. + bool mHidingCaret; + // Whether spellchecker dictionary is initialized after focused. + bool mSpellCheckerDictionaryUpdated; + // Whether we are an HTML editor class. + bool mIsHTMLEditorClass; + + friend class AlignStateAtSelection; // AutoEditActionDataSetter, + // ToGenericNSResult + friend class AutoRangeArray; // IsSEditActionDataAvailable, SelectionRef + friend class CaretPoint; // AllowsTransactionsToChangeSelection, + // CollapseSelectionTo + friend class CompositionTransaction; // CollapseSelectionTo, DoDeleteText, + // DoInsertText, DoReplaceText, + // HideCaret, RangeupdaterRef + friend class DeleteNodeTransaction; // RangeUpdaterRef + friend class DeleteRangeTransaction; // AllowsTransactionsToChangeSelection, + // CollapseSelectionTo + friend class DeleteTextTransaction; // AllowsTransactionsToChangeSelection, + // DoDeleteText, DoInsertText, + // RangeUpdaterRef + friend class InsertNodeTransaction; // AllowsTransactionsToChangeSelection, + // CollapseSelectionTo, + // MarkElementDirty, ToGenericNSResult + friend class InsertTextTransaction; // AllowsTransactionsToChangeSelection, + // CollapseSelectionTo, DoDeleteText, + // DoInsertText, RangeUpdaterRef + friend class ListElementSelectionState; // AutoEditActionDataSetter, + // ToGenericNSResult + friend class ListItemElementSelectionState; // AutoEditActionDataSetter, + // ToGenericNSResult + friend class MoveNodeTransaction; // ToGenericNSResult + friend class ParagraphStateAtSelection; // AutoEditActionDataSetter, + // ToGenericNSResult + friend class PendingStyles; // GetEditAction, + // GetFirstSelectionStartPoint, + // SelectionRef + friend class ReplaceTextTransaction; // AllowsTransactionsToChangeSelection, + // CollapseSelectionTo, DoReplaceText, + // RangeUpdaterRef + friend class SplitNodeTransaction; // ToGenericNSResult + friend class WhiteSpaceVisibilityKeeper; // AutoTransactionsConserveSelection + friend class nsIEditor; // mIsHTMLEditorClass +}; + +} // namespace mozilla + +bool nsIEditor::IsTextEditor() const { + return !AsEditorBase()->mIsHTMLEditorClass; +} + +bool nsIEditor::IsHTMLEditor() const { + return AsEditorBase()->mIsHTMLEditorClass; +} + +mozilla::EditorBase* nsIEditor::AsEditorBase() { + return static_cast<mozilla::EditorBase*>(this); +} + +const mozilla::EditorBase* nsIEditor::AsEditorBase() const { + return static_cast<const mozilla::EditorBase*>(this); +} + +#endif // #ifndef mozilla_EditorBase_h |