diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /editor/libeditor/EditorBase.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/EditorBase.cpp | 7065 |
1 files changed, 7065 insertions, 0 deletions
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp new file mode 100644 index 0000000000..0f864a4bb2 --- /dev/null +++ b/editor/libeditor/EditorBase.cpp @@ -0,0 +1,7065 @@ +/* -*- 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/. */ + +#include "EditorBase.h" + +#include <stdio.h> // for nullptr, stdout +#include <string.h> // for strcmp + +#include "AutoRangeArray.h" // for AutoRangeArray +#include "ChangeAttributeTransaction.h" +#include "CompositionTransaction.h" +#include "DeleteContentTransactionBase.h" +#include "DeleteMultipleRangesTransaction.h" +#include "DeleteNodeTransaction.h" +#include "DeleteRangeTransaction.h" +#include "DeleteTextTransaction.h" +#include "EditAction.h" // for EditSubAction +#include "EditorDOMPoint.h" // for EditorDOMPoint +#include "EditorUtils.h" // for various helper classes. +#include "EditTransactionBase.h" // for EditTransactionBase +#include "EditorEventListener.h" // for EditorEventListener +#include "HTMLEditor.h" // for HTMLEditor +#include "HTMLEditorInlines.h" +#include "HTMLEditUtils.h" // for HTMLEditUtils +#include "InsertNodeTransaction.h" // for InsertNodeTransaction +#include "InsertTextTransaction.h" // for InsertTextTransaction +#include "JoinNodesTransaction.h" // for JoinNodesTransaction +#include "PlaceholderTransaction.h" // for PlaceholderTransaction +#include "SplitNodeTransaction.h" // for SplitNodeTransaction +#include "TextEditor.h" // for TextEditor + +#include "ErrorList.h" +#include "gfxFontUtils.h" // for gfxFontUtils +#include "mozilla/Assertions.h" +#include "mozilla/intl/BidiEmbeddingLevel.h" +#include "mozilla/BasePrincipal.h" // for BasePrincipal +#include "mozilla/CheckedInt.h" // for CheckedInt +#include "mozilla/ComposerCommandsUpdater.h" // for ComposerCommandsUpdater +#include "mozilla/ContentEvents.h" // for InternalClipboardEvent +#include "mozilla/DebugOnly.h" // for DebugOnly +#include "mozilla/EditorSpellCheck.h" // for EditorSpellCheck +#include "mozilla/Encoding.h" // for Encoding (used in Document::GetDocumentCharacterSet) +#include "mozilla/EventDispatcher.h" // for EventChainPreVisitor, etc. +#include "mozilla/FlushType.h" // for FlushType::Frames +#include "mozilla/IMEContentObserver.h" // for IMEContentObserver +#include "mozilla/IMEStateManager.h" // for IMEStateManager +#include "mozilla/InputEventOptions.h" // for InputEventOptions +#include "mozilla/IntegerRange.h" // for IntegerRange +#include "mozilla/InternalMutationEvent.h" // for NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED +#include "mozilla/mozalloc.h" // for operator new, etc. +#include "mozilla/mozInlineSpellChecker.h" // for mozInlineSpellChecker +#include "mozilla/mozSpellChecker.h" // for mozSpellChecker +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/PresShell.h" // for PresShell +#include "mozilla/RangeBoundary.h" // for RawRangeBoundary, RangeBoundary +#include "mozilla/Services.h" // for GetObserverService +#include "mozilla/StaticPrefs_bidi.h" // for StaticPrefs::bidi_* +#include "mozilla/StaticPrefs_dom.h" // for StaticPrefs::dom_* +#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_* +#include "mozilla/StaticPrefs_layout.h" // for StaticPrefs::layout_* +#include "mozilla/TextComposition.h" // for TextComposition +#include "mozilla/TextControlElement.h" // for TextControlElement +#include "mozilla/TextInputListener.h" // for TextInputListener +#include "mozilla/TextServicesDocument.h" // for TextServicesDocument +#include "mozilla/TextEvents.h" +#include "mozilla/TransactionManager.h" // for TransactionManager +#include "mozilla/dom/AbstractRange.h" // for AbstractRange +#include "mozilla/dom/Attr.h" // for Attr +#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext +#include "mozilla/dom/CharacterData.h" // for CharacterData +#include "mozilla/dom/DataTransfer.h" // for DataTransfer +#include "mozilla/dom/Document.h" // for Document +#include "mozilla/dom/DocumentInlines.h" // for GetObservingPresShell +#include "mozilla/dom/DragEvent.h" // for DragEvent +#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement +#include "mozilla/dom/EventTarget.h" // for EventTarget +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/HTMLBRElement.h" +#include "mozilla/dom/Selection.h" // for Selection, etc. +#include "mozilla/dom/StaticRange.h" // for StaticRange +#include "mozilla/dom/Text.h" +#include "mozilla/dom/Event.h" +#include "nsAString.h" // for nsAString::Length, etc. +#include "nsCCUncollectableMarker.h" // for nsCCUncollectableMarker +#include "nsCaret.h" // for nsCaret +#include "nsCaseTreatment.h" +#include "nsCharTraits.h" // for NS_IS_HIGH_SURROGATE, etc. +#include "nsContentUtils.h" // for nsContentUtils +#include "nsCopySupport.h" // for nsCopySupport +#include "nsDOMString.h" // for DOMStringIsNull +#include "nsDebug.h" // for NS_WARNING, etc. +#include "nsError.h" // for NS_OK, etc. +#include "nsFocusManager.h" // for nsFocusManager +#include "nsFrameSelection.h" // for nsFrameSelection +#include "nsGenericHTMLElement.h" // for nsGenericHTMLElement +#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::dir +#include "nsIClipboard.h" // for nsIClipboard +#include "nsIContent.h" // for nsIContent +#include "nsIContentInlines.h" // for nsINode::IsInDesignMode() +#include "nsIDocumentEncoder.h" // for nsIDocumentEncoder +#include "nsIDocumentStateListener.h" // for nsIDocumentStateListener +#include "nsIDocShell.h" // for nsIDocShell +#include "nsIEditActionListener.h" // for nsIEditActionListener +#include "nsIFrame.h" // for nsIFrame +#include "nsIInlineSpellChecker.h" // for nsIInlineSpellChecker, etc. +#include "nsNameSpaceManager.h" // for kNameSpaceID_None, etc. +#include "nsINode.h" // for nsINode, etc. +#include "nsISelectionController.h" // for nsISelectionController, etc. +#include "nsISelectionDisplay.h" // for nsISelectionDisplay, etc. +#include "nsISupports.h" // for nsISupports +#include "nsISupportsUtils.h" // for NS_ADDREF, NS_IF_ADDREF +#include "nsITransferable.h" // for nsITransferable +#include "nsIWeakReference.h" // for nsISupportsWeakReference +#include "nsIWidget.h" // for nsIWidget, IMEState, etc. +#include "nsPIDOMWindow.h" // for nsPIDOMWindow +#include "nsPresContext.h" // for nsPresContext +#include "nsRange.h" // for nsRange +#include "nsReadableUtils.h" // for EmptyString, ToNewCString +#include "nsString.h" // for nsAutoString, nsString, etc. +#include "nsStringFwd.h" // for nsString +#include "nsStyleConsts.h" // for StyleDirection::Rtl, etc. +#include "nsStyleStruct.h" // for nsStyleDisplay, nsStyleText, etc. +#include "nsStyleStructFwd.h" // for nsIFrame::StyleUIReset, etc. +#include "nsTextNode.h" // for nsTextNode +#include "nsThreadUtils.h" // for nsRunnable +#include "prtime.h" // for PR_Now + +class nsIOutputStream; +class nsITransferable; + +namespace mozilla { + +using namespace dom; +using namespace widget; + +using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; +using LeafNodeType = HTMLEditUtils::LeafNodeType; +using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using WalkTreeOption = HTMLEditUtils::WalkTreeOption; + +/***************************************************************************** + * mozilla::EditorBase + *****************************************************************************/ +template EditorDOMPoint EditorBase::GetFirstIMESelectionStartPoint() const; +template EditorRawDOMPoint EditorBase::GetFirstIMESelectionStartPoint() const; +template EditorDOMPoint EditorBase::GetLastIMESelectionEndPoint() const; +template EditorRawDOMPoint EditorBase::GetLastIMESelectionEndPoint() const; + +template Result<CreateContentResult, nsresult> +EditorBase::InsertNodeWithTransaction(nsIContent& aContentToInsert, + const EditorDOMPoint& aPointToInsert); +template Result<CreateElementResult, nsresult> +EditorBase::InsertNodeWithTransaction(Element& aContentToInsert, + const EditorDOMPoint& aPointToInsert); +template Result<CreateTextResult, nsresult> +EditorBase::InsertNodeWithTransaction(Text& aContentToInsert, + const EditorDOMPoint& aPointToInsert); + +template EditorDOMPoint EditorBase::GetFirstSelectionStartPoint() const; +template EditorRawDOMPoint EditorBase::GetFirstSelectionStartPoint() const; +template EditorDOMPoint EditorBase::GetFirstSelectionEndPoint() const; +template EditorRawDOMPoint EditorBase::GetFirstSelectionEndPoint() const; + +template EditorDOMPoint EditorBase::FindBetterInsertionPoint( + const EditorDOMPoint& aPoint) const; +template EditorRawDOMPoint EditorBase::FindBetterInsertionPoint( + const EditorRawDOMPoint& aPoint) const; + +template EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager( + const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aPointAtCaret); +template EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager( + const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount, + const EditorRawDOMPoint& aPointAtCaret); + +EditorBase::EditorBase(EditorType aEditorType) + : mEditActionData(nullptr), + mPlaceholderName(nullptr), + mModCount(0), + mFlags(0), + mUpdateCount(0), + mPlaceholderBatch(0), + mWrapColumn(0), + mNewlineHandling(StaticPrefs::editor_singleLine_pasteNewlines()), + mCaretStyle(StaticPrefs::layout_selection_caret_style()), + mDocDirtyState(-1), + mSpellcheckCheckboxState(eTriUnset), + mInitSucceeded(false), + mAllowsTransactionsToChangeSelection(true), + mDidPreDestroy(false), + mDidPostCreate(false), + mDispatchInputEvent(true), + mIsInEditSubAction(false), + mHidingCaret(false), + mSpellCheckerDictionaryUpdated(true), + mIsHTMLEditorClass(aEditorType == EditorType::HTML) { +#ifdef XP_WIN + if (!mCaretStyle && !IsTextEditor()) { + // Wordpad-like caret behavior. + mCaretStyle = 1; + } +#endif // #ifdef XP_WIN + if (mNewlineHandling < nsIEditor::eNewlinesPasteIntact || + mNewlineHandling > nsIEditor::eNewlinesStripSurroundingWhitespace) { + mNewlineHandling = nsIEditor::eNewlinesPasteToFirst; + } +} + +EditorBase::~EditorBase() { + MOZ_ASSERT(!IsInitialized() || mDidPreDestroy, + "Why PreDestroy hasn't been called?"); + + if (mComposition) { + mComposition->OnEditorDestroyed(); + mComposition = nullptr; + } + // If this editor is still hiding the caret, we need to restore it. + HideCaret(false); + mTransactionManager = nullptr; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(EditorBase) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EditorBase) + // Remove event listeners first since EditorEventListener may need + // mDocument, mEventTarget, etc. + if (tmp->mEventListener) { + tmp->mEventListener->Disconnect(); + tmp->mEventListener = nullptr; + } + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIMEContentObserver) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSpellChecker) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextServicesDocument) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextInputListener) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransactionManager) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionListeners) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocStateListeners) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlaceholderTransaction) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedDocumentEncoder) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditorBase) + Document* currentDoc = + tmp->mRootElement ? tmp->mRootElement->GetUncomposedDoc() : nullptr; + if (currentDoc && nsCCUncollectableMarker::InGeneration( + cb, currentDoc->GetMarkedCCGeneration())) { + return NS_SUCCESS_INTERRUPTED_TRAVERSE; + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMEContentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineSpellChecker) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextServicesDocument) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextInputListener) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransactionManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionListeners) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocStateListeners) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlaceholderTransaction) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedDocumentEncoder) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditorBase) + NS_INTERFACE_MAP_ENTRY(nsISelectionListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIEditor) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditor) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorBase) + +nsresult EditorBase::InitInternal(Document& aDocument, Element* aRootElement, + nsISelectionController& aSelectionController, + uint32_t aFlags) { + MOZ_ASSERT_IF( + !mEditActionData || + !mEditActionData->HasEditorDestroyedDuringHandlingEditAction(), + GetTopLevelEditSubAction() == EditSubAction::eNone); + + // First only set flags, but other stuff shouldn't be initialized now. + // Note that SetFlags() will be called by PostCreate(). + mFlags = aFlags; + + mDocument = &aDocument; + // nsISelectionController should be stored only when we're a `TextEditor`. + // Otherwise, in `HTMLEditor`, it's `PresShell`, and grabbing it causes + // a circular reference and memory leak. + // XXX Should we move `mSelectionController to `TextEditor`? + MOZ_ASSERT_IF(!IsTextEditor(), &aSelectionController == GetPresShell()); + if (IsTextEditor()) { + MOZ_ASSERT(&aSelectionController != GetPresShell()); + mSelectionController = &aSelectionController; + } + + if (mEditActionData) { + // During edit action, selection is cached. But this selection is invalid + // now since selection controller is updated, so we have to update this + // cache. + Selection* selection = aSelectionController.GetSelection( + nsISelectionController::SELECTION_NORMAL); + NS_WARNING_ASSERTION(selection, + "SelectionController::GetSelection() failed"); + if (selection) { + mEditActionData->UpdateSelectionCache(*selection); + } + } + + // set up root element if we are passed one. + if (aRootElement) { + mRootElement = aRootElement; + } + + // If this is an editor for <input> or <textarea>, the text node which + // has composition string is always recreated with same content. Therefore, + // we need to nodify mComposition of text node destruction and replacing + // composing string when this receives eCompositionChange event next time. + if (mComposition && mComposition->GetContainerTextNode() && + !mComposition->GetContainerTextNode()->IsInComposedDoc()) { + mComposition->OnTextNodeRemoved(); + } + + // Show the caret. + DebugOnly<nsresult> rvIgnored = aSelectionController.SetCaretReadOnly(false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsISelectionController::SetCaretReadOnly(false) failed, but ignored"); + // Show all the selection reflected to user. + rvIgnored = + aSelectionController.SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsISelectionController::SetSelectionFlags(" + "nsISelectionDisplay::DISPLAY_ALL) failed, but ignored"); + + MOZ_ASSERT(IsInitialized()); + + AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_FAILURE; + } + + SelectionRef().AddSelectionListener(this); + + // Make sure that the editor will be destroyed properly + mDidPreDestroy = false; + // Make sure that the editor will be created properly + mDidPostCreate = false; + + return NS_OK; +} + +nsresult EditorBase::EnsureEmptyTextFirstChild() { + MOZ_ASSERT(IsTextEditor()); + RefPtr<Element> root = GetRoot(); + nsIContent* firstChild = root->GetFirstChild(); + + if (!firstChild || !firstChild->IsText()) { + RefPtr<nsTextNode> newTextNode = CreateTextNode(u""_ns); + if (!newTextNode) { + NS_WARNING("EditorBase::CreateTextNode() failed"); + return NS_ERROR_UNEXPECTED; + } + IgnoredErrorResult ignoredError; + root->InsertChildBefore(newTextNode, root->GetFirstChild(), true, + ignoredError); + MOZ_ASSERT(!ignoredError.Failed()); + } + + return NS_OK; +} + +nsresult EditorBase::PostCreateInternal() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // Synchronize some stuff for the flags. SetFlags() will initialize + // something by the flag difference. This is first time of that, so, all + // initializations must be run. For such reason, we need to invert mFlags + // value first. + mFlags = ~mFlags; + nsresult rv = SetFlags(~mFlags); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::SetFlags() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // These operations only need to happen on the first PostCreate call + if (!mDidPostCreate) { + mDidPostCreate = true; + + // Set up listeners + CreateEventListeners(); + nsresult rv = InstallEventListeners(); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::InstallEventListeners() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // nuke the modification count, so the doc appears unmodified + // do this before we notify listeners + DebugOnly<nsresult> rvIgnored = ResetModificationCount(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::ResetModificationCount() failed, but ignored"); + + // update the UI with our state + rvIgnored = NotifyDocumentListeners(eDocumentCreated); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "EditorBase::NotifyDocumentListeners(eDocumentCreated)" + " failed, but ignored"); + rvIgnored = NotifyDocumentListeners(eDocumentStateChanged); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "EditorBase::NotifyDocumentListeners(" + "eDocumentStateChanged) failed, but ignored"); + } + + // update nsTextStateManager and caret if we have focus + if (RefPtr<Element> focusedElement = GetFocusedElement()) { + DebugOnly<nsresult> rvIgnored = InitializeSelection(*focusedElement); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::InitializeSelection() failed, but ignored"); + + // If the text control gets reframed during focus, Focus() would not be + // called, so take a chance here to see if we need to spell check the text + // control. + nsresult rv = FlushPendingSpellCheck(); + if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { + NS_WARNING( + "EditorBase::FlushPendingSpellCheck() caused destroying the editor"); + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::FlushPendingSpellCheck() failed, but ignored"); + + IMEState newState; + rv = GetPreferredIMEState(&newState); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::GetPreferredIMEState() failed"); + return NS_OK; + } + IMEStateManager::UpdateIMEState(newState, focusedElement, *this); + } + + // FYI: This call might cause destroying this editor. + IMEStateManager::OnEditorInitialized(*this); + + return NS_OK; +} + +void EditorBase::SetTextInputListener(TextInputListener* aTextInputListener) { + MOZ_ASSERT(!mTextInputListener || !aTextInputListener || + mTextInputListener == aTextInputListener); + mTextInputListener = aTextInputListener; +} + +void EditorBase::SetIMEContentObserver( + IMEContentObserver* aIMEContentObserver) { + MOZ_ASSERT(!mIMEContentObserver || !aIMEContentObserver || + mIMEContentObserver == aIMEContentObserver); + mIMEContentObserver = aIMEContentObserver; +} + +void EditorBase::CreateEventListeners() { + // Don't create the handler twice + if (!mEventListener) { + mEventListener = new EditorEventListener(); + } +} + +nsresult EditorBase::InstallEventListeners() { + if (NS_WARN_IF(!IsInitialized()) || NS_WARN_IF(!mEventListener)) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Initialize the event target. + mEventTarget = GetExposedRoot(); + if (NS_WARN_IF(!mEventTarget)) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = mEventListener->Connect(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::Connect() failed"); + if (mComposition) { + // If mComposition has already been destroyed, we should forget it. + // This may happen if it ended while we don't listen to composition + // events. + if (mComposition->Destroyed()) { + // XXX We may need to fix existing composition transaction here. + // However, this may be called when it's not safe. + // Perhaps, we should stop handling composition with events. + mComposition = nullptr; + } + // Otherwise, Restart to handle composition with new editor contents. + else { + mComposition->StartHandlingComposition(this); + } + } + return rv; +} + +void EditorBase::RemoveEventListeners() { + if (!IsInitialized() || !mEventListener) { + return; + } + mEventListener->Disconnect(); + if (mComposition) { + // Even if this is called, don't release mComposition because this is + // may be reused after reframing. + mComposition->EndHandlingComposition(this); + } + mEventTarget = nullptr; +} + +bool EditorBase::IsListeningToEvents() const { + return IsInitialized() && mEventListener && + !mEventListener->DetachedFromEditor(); +} + +bool EditorBase::GetDesiredSpellCheckState() { + // Check user override on this element + if (mSpellcheckCheckboxState != eTriUnset) { + return (mSpellcheckCheckboxState == eTriTrue); + } + + // Check user preferences + int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1); + + if (!spellcheckLevel) { + return false; // Spellchecking forced off globally + } + + if (!CanEnableSpellCheck()) { + return false; + } + + PresShell* presShell = GetPresShell(); + if (presShell) { + nsPresContext* context = presShell->GetPresContext(); + if (context && !context->IsDynamic()) { + return false; + } + } + + // Check DOM state + nsCOMPtr<nsIContent> content = GetExposedRoot(); + if (!content) { + return false; + } + + auto element = nsGenericHTMLElement::FromNode(content); + if (!element) { + return false; + } + + if (!IsInPlaintextMode()) { + // Some of the page content might be editable and some not, if spellcheck= + // is explicitly set anywhere, so if there's anything editable on the page, + // return true and let the spellchecker figure it out. + Document* doc = content->GetComposedDoc(); + return doc && doc->IsEditingOn(); + } + + return element->Spellcheck(); +} + +void EditorBase::PreDestroyInternal() { + MOZ_ASSERT(!mDidPreDestroy); + + mInitSucceeded = false; + + Selection* selection = GetSelection(); + if (selection) { + selection->RemoveSelectionListener(this); + } + + IMEStateManager::OnEditorDestroying(*this); + + // Let spellchecker clean up its observers etc. It is important not to + // actually free the spellchecker here, since the spellchecker could have + // caused flush notifications, which could have gotten here if a textbox + // is being removed. Setting the spellchecker to nullptr could free the + // object that is still in use! It will be freed when the editor is + // destroyed. + if (mInlineSpellChecker) { + DebugOnly<nsresult> rvIgnored = + mInlineSpellChecker->Cleanup(IsTextEditor()); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "mozInlineSpellChecker::Cleanup() failed, but ignored"); + } + + // tell our listeners that the doc is going away + DebugOnly<nsresult> rvIgnored = + NotifyDocumentListeners(eDocumentToBeDestroyed); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "EditorBase::NotifyDocumentListeners(" + "eDocumentToBeDestroyed) failed, but ignored"); + + // Unregister event listeners + RemoveEventListeners(); + // If this editor is still hiding the caret, we need to restore it. + HideCaret(false); + mActionListeners.Clear(); + mDocStateListeners.Clear(); + mInlineSpellChecker = nullptr; + mTextServicesDocument = nullptr; + mTextInputListener = nullptr; + mSpellcheckCheckboxState = eTriUnset; + mRootElement = nullptr; + + // Transaction may grab this instance. Therefore, they should be released + // here for stopping the circular reference with this instance. + if (mTransactionManager) { + DebugOnly<bool> disabledUndoRedo = DisableUndoRedo(); + NS_WARNING_ASSERTION(disabledUndoRedo, + "EditorBase::DisableUndoRedo() failed, but ignored"); + mTransactionManager = nullptr; + } + + if (mEditActionData) { + mEditActionData->OnEditorDestroy(); + } + + mDidPreDestroy = true; +} + +NS_IMETHODIMP EditorBase::GetFlags(uint32_t* aFlags) { + // NOTE: If you need to override this method, you need to make Flags() + // virtual. + *aFlags = Flags(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::SetFlags(uint32_t aFlags) { + if (mFlags == aFlags) { + return NS_OK; + } + + // If we're a `TextEditor` instance, the plaintext mode should always be set. + // If we're an `HTMLEditor` instance, either is fine. + MOZ_ASSERT_IF(IsTextEditor(), !!(aFlags & nsIEditor::eEditorPlaintextMask)); + // If we're an `HTMLEditor` instance, we cannot treat it as a single line + // editor. So, eEditorSingleLineMask is available only when we're a + // `TextEditor` instance. + MOZ_ASSERT_IF(IsHTMLEditor(), !(aFlags & nsIEditor::eEditorSingleLineMask)); + // If we're an `HTMLEditor` instance, we cannot treat it as a password editor. + // So, eEditorPasswordMask is available only when we're a `TextEditor` + // instance. + MOZ_ASSERT_IF(IsHTMLEditor(), !(aFlags & nsIEditor::eEditorPasswordMask)); + // eEditorAllowInteraction changes the behavior of `HTMLEditor`. So, it's + // not available with `TextEditor` instance. + MOZ_ASSERT_IF(IsTextEditor(), !(aFlags & nsIEditor::eEditorAllowInteraction)); + + const bool isCalledByPostCreate = (mFlags == ~aFlags); + // We don't support dynamic password flag change. + MOZ_ASSERT_IF(!isCalledByPostCreate, + !((mFlags ^ aFlags) & nsIEditor::eEditorPasswordMask)); + bool spellcheckerWasEnabled = !isCalledByPostCreate && CanEnableSpellCheck(); + mFlags = aFlags; + + if (!IsInitialized()) { + // If we're initializing, we shouldn't do anything now. + // SetFlags() will be called by PostCreate(), + // we should synchronize some stuff for the flags at that time. + return NS_OK; + } + + // The flag change may cause the spellchecker state change + if (CanEnableSpellCheck() != spellcheckerWasEnabled) { + SyncRealTimeSpell(); + } + + // If this is called from PostCreate(), it will update the IME state if it's + // necessary. + if (!mDidPostCreate) { + return NS_OK; + } + + // Might be changing editable state, so, we need to reset current IME state + // if we're focused and the flag change causes IME state change. + if (RefPtr<Element> focusedElement = GetFocusedElement()) { + IMEState newState; + nsresult rv = GetPreferredIMEState(&newState); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::GetPreferredIMEState() failed, but ignored"); + if (NS_SUCCEEDED(rv)) { + // NOTE: When the enabled state isn't going to be modified, this method + // is going to do nothing. + IMEStateManager::UpdateIMEState(newState, focusedElement, *this); + } + } + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetIsSelectionEditable(bool* aIsSelectionEditable) { + if (NS_WARN_IF(!aIsSelectionEditable)) { + return NS_ERROR_INVALID_ARG; + } + *aIsSelectionEditable = IsSelectionEditable(); + return NS_OK; +} + +bool EditorBase::IsSelectionEditable() { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return false; + } + + if (IsTextEditor()) { + // XXX we just check that the anchor node is editable at the moment + // we should check that all nodes in the selection are editable + const nsINode* anchorNode = SelectionRef().GetAnchorNode(); + return anchorNode && anchorNode->IsContent() && anchorNode->IsEditable(); + } + + const nsINode* anchorNode = SelectionRef().GetAnchorNode(); + const nsINode* focusNode = SelectionRef().GetFocusNode(); + if (!anchorNode || !focusNode) { + return false; + } + + // if anchorNode or focusNode is in a native anonymous subtree, HTMLEditor + // shouldn't edit content in it. + // XXX This must be a bug of Selection API. + if (MOZ_UNLIKELY(anchorNode->IsInNativeAnonymousSubtree() || + focusNode->IsInNativeAnonymousSubtree())) { + return false; + } + + // Per the editing spec as of June 2012: we have to have a selection whose + // start and end nodes are editable, and which share an ancestor editing + // host. (Bug 766387.) + bool isSelectionEditable = SelectionRef().RangeCount() && + anchorNode->IsEditable() && + focusNode->IsEditable(); + if (!isSelectionEditable) { + return false; + } + + const nsINode* commonAncestor = + SelectionRef().GetAnchorFocusRange()->GetClosestCommonInclusiveAncestor(); + while (commonAncestor && !commonAncestor->IsEditable()) { + commonAncestor = commonAncestor->GetParentNode(); + } + // If there is no editable common ancestor, return false. + return !!commonAncestor; +} + +NS_IMETHODIMP EditorBase::GetIsDocumentEditable(bool* aIsDocumentEditable) { + if (NS_WARN_IF(!aIsDocumentEditable)) { + return NS_ERROR_INVALID_ARG; + } + RefPtr<Document> document = GetDocument(); + *aIsDocumentEditable = document && IsModifiable(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetDocument(Document** aDocument) { + if (NS_WARN_IF(!aDocument)) { + return NS_ERROR_INVALID_ARG; + } + *aDocument = do_AddRef(mDocument).take(); + return NS_WARN_IF(!*aDocument) ? NS_ERROR_NOT_INITIALIZED : NS_OK; +} + +already_AddRefed<nsIWidget> EditorBase::GetWidget() const { + nsPresContext* presContext = GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return nullptr; + } + nsCOMPtr<nsIWidget> widget = presContext->GetRootWidget(); + return NS_WARN_IF(!widget) ? nullptr : widget.forget(); +} + +NS_IMETHODIMP EditorBase::GetContentsMIMEType(nsAString& aContentsMIMEType) { + aContentsMIMEType = mContentMIMEType; + return NS_OK; +} + +NS_IMETHODIMP EditorBase::SetContentsMIMEType( + const nsAString& aContentsMIMEType) { + mContentMIMEType.Assign(aContentsMIMEType); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetSelectionController( + nsISelectionController** aSelectionController) { + if (NS_WARN_IF(!aSelectionController)) { + return NS_ERROR_INVALID_ARG; + } + *aSelectionController = do_AddRef(GetSelectionController()).take(); + return NS_WARN_IF(!*aSelectionController) ? NS_ERROR_FAILURE : NS_OK; +} + +NS_IMETHODIMP EditorBase::DeleteSelection(EDirection aAction, + EStripWrappers aStripWrappers) { + nsresult rv = DeleteSelectionAsAction(aAction, aStripWrappers); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteSelectionAsAction() failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::GetSelection(Selection** aSelection) { + nsresult rv = GetSelection(SelectionType::eNormal, aSelection); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::GetSelection(SelectionType::eNormal) failed"); + return rv; +} + +nsresult EditorBase::GetSelection(SelectionType aSelectionType, + Selection** aSelection) const { + if (NS_WARN_IF(!aSelection)) { + return NS_ERROR_INVALID_ARG; + } + if (IsEditActionDataAvailable()) { + *aSelection = do_AddRef(&SelectionRef()).take(); + return NS_OK; + } + nsISelectionController* selectionController = GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + *aSelection = nullptr; + return NS_ERROR_NOT_INITIALIZED; + } + *aSelection = do_AddRef(selectionController->GetSelection( + ToRawSelectionType(aSelectionType))) + .take(); + return NS_WARN_IF(!*aSelection) ? NS_ERROR_FAILURE : NS_OK; +} + +nsresult EditorBase::DoTransactionInternal(nsITransaction* aTransaction) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!ShouldAlreadyHaveHandledBeforeInputEventDispatching(), + "beforeinput event hasn't been dispatched yet"); + + if (mPlaceholderBatch && !mPlaceholderTransaction) { + MOZ_DIAGNOSTIC_ASSERT(mPlaceholderName); + mPlaceholderTransaction = PlaceholderTransaction::Create( + *this, *mPlaceholderName, std::move(mSelState)); + MOZ_ASSERT(mSelState.isNothing()); + + // We will recurse, but will not hit this case in the nested call + RefPtr<PlaceholderTransaction> placeholderTransaction = + mPlaceholderTransaction; + DebugOnly<nsresult> rvIgnored = + DoTransactionInternal(placeholderTransaction); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::DoTransactionInternal() failed, but ignored"); + + if (mTransactionManager) { + if (nsCOMPtr<nsITransaction> topTransaction = + mTransactionManager->PeekUndoStack()) { + if (RefPtr<EditTransactionBase> topTransactionBase = + topTransaction->GetAsEditTransactionBase()) { + if (PlaceholderTransaction* topPlaceholderTransaction = + topTransactionBase->GetAsPlaceholderTransaction()) { + // there is a placeholder transaction on top of the undo stack. It + // is either the one we just created, or an earlier one that we are + // now merging into. From here on out remember this placeholder + // instead of the one we just created. + mPlaceholderTransaction = topPlaceholderTransaction; + } + } + } + } + } + + if (aTransaction) { + // XXX: Why are we doing selection specific batching stuff here? + // XXX: Most entry points into the editor have auto variables that + // XXX: should trigger Begin/EndUpdateViewBatch() calls that will make + // XXX: these selection batch calls no-ops. + // XXX: + // XXX: I suspect that this was placed here to avoid multiple + // XXX: selection changed notifications from happening until after + // XXX: the transaction was done. I suppose that can still happen + // XXX: if an embedding application called DoTransaction() directly + // XXX: to pump its own transactions through the system, but in that + // XXX: case, wouldn't we want to use Begin/EndUpdateViewBatch() or + // XXX: its auto equivalent AutoUpdateViewBatch to ensure that + // XXX: selection listeners have access to accurate frame data? + // XXX: + // XXX: Note that if we did add Begin/EndUpdateViewBatch() calls + // XXX: we will need to make sure that they are disabled during + // XXX: the init of the editor for text widgets to avoid layout + // XXX: re-entry during initial reflow. - kin + + // get the selection and start a batch change + SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__); + + if (mTransactionManager) { + RefPtr<TransactionManager> transactionManager(mTransactionManager); + nsresult rv = transactionManager->DoTransaction(aTransaction); + if (NS_FAILED(rv)) { + NS_WARNING("TransactionManager::DoTransaction() failed"); + return rv; + } + } else { + nsresult rv = aTransaction->DoTransaction(); + if (NS_FAILED(rv)) { + NS_WARNING("nsITransaction::DoTransaction() failed"); + return rv; + } + } + + DoAfterDoTransaction(aTransaction); + } + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::EnableUndo(bool aEnable) { + // XXX Should we return NS_ERROR_FAILURE if EdnableUndoRedo() or + // DisableUndoRedo() returns false? + if (aEnable) { + DebugOnly<bool> enabledUndoRedo = EnableUndoRedo(); + NS_WARNING_ASSERTION(enabledUndoRedo, + "EditorBase::EnableUndoRedo() failed, but ignored"); + return NS_OK; + } + DebugOnly<bool> disabledUndoRedo = DisableUndoRedo(); + NS_WARNING_ASSERTION(disabledUndoRedo, + "EditorBase::DisableUndoRedo() failed, but ignored"); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::ClearUndoRedoXPCOM() { + if (MOZ_UNLIKELY(!ClearUndoRedo())) { + return NS_ERROR_FAILURE; // We're handling a transaction + } + return NS_OK; +} + +NS_IMETHODIMP EditorBase::Undo() { + nsresult rv = UndoAsAction(1u); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::UndoAll() { + if (!mTransactionManager) { + return NS_OK; + } + size_t numberOfUndoItems = mTransactionManager->NumberOfUndoItems(); + if (!numberOfUndoItems) { + return NS_OK; // no transactions + } + nsresult rv = UndoAsAction(numberOfUndoItems); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::GetUndoRedoEnabled(bool* aIsEnabled) { + MOZ_ASSERT(aIsEnabled); + *aIsEnabled = IsUndoRedoEnabled(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetCanUndo(bool* aCanUndo) { + MOZ_ASSERT(aCanUndo); + *aCanUndo = CanUndo(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::Redo() { + nsresult rv = RedoAsAction(1u); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RedoAsAction() failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::GetCanRedo(bool* aCanRedo) { + MOZ_ASSERT(aCanRedo); + *aCanRedo = CanRedo(); + return NS_OK; +} + +nsresult EditorBase::UndoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) { + if (aCount == 0 || IsReadonly()) { + return NS_OK; + } + + // If we don't have transaction in the undo stack, we shouldn't notify + // anybody of trying to undo since it's not useful notification but we + // need to pay some runtime cost. + if (!CanUndo()) { + return NS_OK; + } + + // If there is composition, we shouldn't allow to undo with committing + // composition since Chrome doesn't allow it and it doesn't make sense + // because committing composition causes one transaction and Undo(1) + // undoes the committing composition. + if (GetComposition()) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eUndo, aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + if (NS_WARN_IF(!CanUndo()) || NS_WARN_IF(Destroyed())) { + return NS_ERROR_FAILURE; + } + + rv = NS_OK; + { + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eUndo, nsIEditor::eNone, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return EditorBase::ToGenericNSResult(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() " + "failed, but ignored"); + + RefPtr<TransactionManager> transactionManager(mTransactionManager); + for (uint32_t i = 0; i < aCount; ++i) { + if (NS_FAILED(transactionManager->Undo())) { + NS_WARNING("TransactionManager::Undo() failed"); + break; + } + DoAfterUndoTransaction(); + } + + if (IsHTMLEditor()) { + rv = AsHTMLEditor()->ReflectPaddingBRElementForEmptyEditor(); + } + } + + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::RedoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) { + if (aCount == 0 || IsReadonly()) { + return NS_OK; + } + + // If we don't have transaction in the redo stack, we shouldn't notify + // anybody of trying to redo since it's not useful notification but we + // need to pay some runtime cost. + if (!CanRedo()) { + return NS_OK; + } + + // If there is composition, we shouldn't allow to redo with committing + // composition since Chrome doesn't allow it and it doesn't make sense + // because committing composition causes removing all transactions from + // the redo queue. So, it becomes impossible to redo anything. + if (GetComposition()) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eRedo, aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + if (NS_WARN_IF(!CanRedo()) || NS_WARN_IF(Destroyed())) { + return NS_ERROR_FAILURE; + } + + rv = NS_OK; + { + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eRedo, nsIEditor::eNone, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() " + "failed, but ignored"); + + RefPtr<TransactionManager> transactionManager(mTransactionManager); + for (uint32_t i = 0; i < aCount; ++i) { + if (NS_FAILED(transactionManager->Redo())) { + NS_WARNING("TransactionManager::Redo() failed"); + break; + } + DoAfterRedoTransaction(); + } + + if (IsHTMLEditor()) { + rv = AsHTMLEditor()->ReflectPaddingBRElementForEmptyEditor(); + } + } + + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP EditorBase::BeginTransaction() { + AutoEditActionDataSetter editActionData(*this, EditAction::eUnknown); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_FAILURE; + } + + BeginTransactionInternal(__FUNCTION__); + return NS_OK; +} + +void EditorBase::BeginTransactionInternal(const char* aRequesterFuncName) { + BeginUpdateViewBatch(aRequesterFuncName); + + if (NS_WARN_IF(!mTransactionManager)) { + return; + } + + RefPtr<TransactionManager> transactionManager(mTransactionManager); + DebugOnly<nsresult> rvIgnored = transactionManager->BeginBatch(nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "TransactionManager::BeginBatch() failed, but ignored"); +} + +NS_IMETHODIMP EditorBase::EndTransaction() { + AutoEditActionDataSetter editActionData(*this, EditAction::eUnknown); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_FAILURE; + } + + EndTransactionInternal(__FUNCTION__); + return NS_OK; +} + +void EditorBase::EndTransactionInternal(const char* aRequesterFuncName) { + if (mTransactionManager) { + RefPtr<TransactionManager> transactionManager(mTransactionManager); + DebugOnly<nsresult> rvIgnored = transactionManager->EndBatch(false); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "TransactionManager::EndBatch() failed, but ignored"); + } + + EndUpdateViewBatch(aRequesterFuncName); +} + +void EditorBase::BeginPlaceholderTransaction(nsStaticAtom& aTransactionName, + const char* aRequesterFuncName) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(mPlaceholderBatch >= 0, "negative placeholder batch count!"); + + if (!mPlaceholderBatch) { + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + // time to turn on the batch + BeginUpdateViewBatch(aRequesterFuncName); + mPlaceholderTransaction = nullptr; + mPlaceholderName = &aTransactionName; + mSelState.emplace(); + mSelState->SaveSelection(SelectionRef()); + // Composition transaction can modify multiple nodes and it merges text + // node for ime into single text node. + // So if current selection is into IME text node, it might be failed + // to restore selection by UndoTransaction. + // So we need update selection by range updater. + if (mPlaceholderName == nsGkAtoms::IMETxnName) { + RangeUpdaterRef().RegisterSelectionState(*mSelState); + } + } + mPlaceholderBatch++; +} + +void EditorBase::EndPlaceholderTransaction( + ScrollSelectionIntoView aScrollSelectionIntoView, + const char* aRequesterFuncName) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(mPlaceholderBatch > 0, + "zero or negative placeholder batch count when ending batch!"); + + if (!(--mPlaceholderBatch)) { + // By making the assumption that no reflow happens during the calls + // to EndUpdateViewBatch and ScrollSelectionFocusIntoView, we are able to + // allow the selection to cache a frame offset which is used by the + // caret drawing code. We only enable this cache here; at other times, + // we have no way to know whether reflow invalidates it + // See bugs 35296 and 199412. + SelectionRef().SetCanCacheFrameOffset(true); + + // time to turn off the batch + EndUpdateViewBatch(aRequesterFuncName); + // make sure selection is in view + + // After ScrollSelectionFocusIntoView(), the pending notifications might be + // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. + // XXX Even if we're destroyed, we need to keep handling below because + // this method changes a lot of status. We should rewrite this safer. + if (aScrollSelectionIntoView == ScrollSelectionIntoView::Yes) { + DebugOnly<nsresult> rvIgnored = ScrollSelectionFocusIntoView(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::ScrollSelectionFocusIntoView() failed, but Ignored"); + } + + // cached for frame offset are Not available now + SelectionRef().SetCanCacheFrameOffset(false); + + if (mSelState) { + // we saved the selection state, but never got to hand it to placeholder + // (else we ould have nulled out this pointer), so destroy it to prevent + // leaks. + if (mPlaceholderName == nsGkAtoms::IMETxnName) { + RangeUpdaterRef().DropSelectionState(*mSelState); + } + mSelState.reset(); + } + // We might have never made a placeholder if no action took place. + if (mPlaceholderTransaction) { + // FYI: Disconnect placeholder transaction before dispatching "input" + // event because an input event listener may start other things. + // TODO: We should forget EditActionDataSetter too. + RefPtr<PlaceholderTransaction> placeholderTransaction = + std::move(mPlaceholderTransaction); + DebugOnly<nsresult> rvIgnored = + placeholderTransaction->EndPlaceHolderBatch(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "PlaceholderTransaction::EndPlaceHolderBatch() failed, but ignored"); + // notify editor observers of action but if composing, it's done by + // compositionchange event handler. + if (!mComposition) { + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + } + } else { + NotifyEditorObservers(eNotifyEditorObserversOfCancel); + } + } +} + +NS_IMETHODIMP EditorBase::SetShouldTxnSetSelection(bool aShould) { + MakeThisAllowTransactionsToChangeSelection(aShould); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetDocumentIsEmpty(bool* aDocumentIsEmpty) { + MOZ_ASSERT(aDocumentIsEmpty); + *aDocumentIsEmpty = IsEmpty(); + return NS_OK; +} + +// XXX: The rule system should tell us which node to select all on (ie, the +// root, or the body) +NS_IMETHODIMP EditorBase::SelectAll() { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = SelectAllInternal(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SelectAllInternal() failed"); + // This is low level API for XUL applcation. So, we should return raw + // error code here. + return rv; +} + +nsresult EditorBase::SelectAllInternal() { + MOZ_ASSERT(IsInitialized()); + + DebugOnly<nsresult> rvIgnored = CommitComposition(); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "EditorBase::CommitComposition() failed, but ignored"); + + // XXX Do we need to keep handling after committing composition causes moving + // focus to different element? Although TextEditor has independent + // selection, so, we may not see any odd behavior even in such case. + + nsresult rv = SelectEntireDocument(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::SelectEntireDocument() failed"); + return rv; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP EditorBase::BeginningOfDocument() { + MOZ_ASSERT(IsTextEditor()); + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + // get the root element + RefPtr<Element> rootElement = GetRoot(); + if (NS_WARN_IF(!rootElement)) { + return NS_ERROR_NULL_POINTER; + } + + // find first editable thingy + nsCOMPtr<nsIContent> firstEditableLeaf; + // If we're `TextEditor`, the first editable leaf node is a text node or + // padding `<br>` element. In the first case, we need to collapse selection + // into it. + if (rootElement->GetFirstChild() && rootElement->GetFirstChild()->IsText()) { + firstEditableLeaf = rootElement->GetFirstChild(); + } + if (!firstEditableLeaf) { + // just the root node, set selection to inside the root + nsresult rv = CollapseSelectionToStartOf(*rootElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionToStartOf() failed"); + return rv; + } + + if (firstEditableLeaf->IsText()) { + // If firstEditableLeaf is text, set selection to beginning of the text + // node. + nsresult rv = CollapseSelectionToStartOf(*firstEditableLeaf); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionToStartOf() failed"); + return rv; + } + + // Otherwise, it's a leaf node and we set the selection just in front of it. + nsCOMPtr<nsIContent> parent = firstEditableLeaf->GetParent(); + if (NS_WARN_IF(!parent)) { + return NS_ERROR_NULL_POINTER; + } + + MOZ_ASSERT( + parent->ComputeIndexOf(firstEditableLeaf).valueOr(UINT32_MAX) == 0, + "How come the first node isn't the left most child in its parent?"); + nsresult rv = CollapseSelectionToStartOf(*parent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionToStartOf() failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::EndOfDocument() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP EditorBase::GetDocumentModified(bool* aOutDocModified) { + if (NS_WARN_IF(!aOutDocModified)) { + return NS_ERROR_INVALID_ARG; + } + + int32_t modCount = 0; + DebugOnly<nsresult> rvIgnored = GetModificationCount(&modCount); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::GetModificationCount() failed, but ignored"); + + *aOutDocModified = (modCount != 0); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetDocumentCharacterSet(nsACString& aCharacterSet) { + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult EditorBase::GetDocumentCharsetInternal(nsACString& aCharset) const { + Document* document = GetDocument(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_NOT_INITIALIZED; + } + document->GetDocumentCharacterSet()->Name(aCharset); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::SetDocumentCharacterSet( + const nsACString& aCharacterSet) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP EditorBase::OutputToString(const nsAString& aFormatType, + uint32_t aDocumentEncoderFlags, + nsAString& aOutputString) { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = + ComputeValueInternal(aFormatType, aDocumentEncoderFlags, aOutputString); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::ComputeValueInternal() failed"); + // This is low level API for XUL application. So, we should return raw + // error code here. + return rv; +} + +nsresult EditorBase::ComputeValueInternal(const nsAString& aFormatType, + uint32_t aDocumentEncoderFlags, + nsAString& aOutputString) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // First, let's try to get the value simply only from text node if the + // caller wants plaintext value. + if (aFormatType.LowerCaseEqualsLiteral("text/plain") && + !(aDocumentEncoderFlags & (nsIDocumentEncoder::OutputSelectionOnly | + nsIDocumentEncoder::OutputWrap))) { + // Shortcut for empty editor case. + if (IsEmpty()) { + aOutputString.Truncate(); + return NS_OK; + } + // NOTE: If it's neither <input type="text"> nor <textarea>, e.g., an HTML + // editor which is in plaintext mode (e.g., plaintext email composer on + // Thunderbird), it should be handled by the expensive path. + if (IsTextEditor()) { + // If it's necessary to check selection range or the editor wraps hard, + // we need some complicated handling. In such case, we need to use the + // expensive path. + // XXX Anything else what we cannot return the text node data simply? + Result<EditActionResult, nsresult> result = + AsTextEditor()->ComputeValueFromTextNodeAndBRElement(aOutputString); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("TextEditor::ComputeValueFromTextNodeAndBRElement() failed"); + return result.unwrapErr(); + } + if (!result.inspect().Ignored()) { + return NS_OK; + } + } + } + + nsAutoCString charset; + nsresult rv = GetDocumentCharsetInternal(charset); + if (NS_FAILED(rv) || charset.IsEmpty()) { + charset.AssignLiteral("windows-1252"); // XXX Why don't we use "UTF-8"? + } + + nsCOMPtr<nsIDocumentEncoder> encoder = + GetAndInitDocEncoder(aFormatType, aDocumentEncoderFlags, charset); + if (!encoder) { + NS_WARNING("EditorBase::GetAndInitDocEncoder() failed"); + return NS_ERROR_FAILURE; + } + + rv = encoder->EncodeToString(aOutputString); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "nsIDocumentEncoder::EncodeToString() failed"); + return rv; +} + +already_AddRefed<nsIDocumentEncoder> EditorBase::GetAndInitDocEncoder( + const nsAString& aFormatType, uint32_t aDocumentEncoderFlags, + const nsACString& aCharset) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + nsCOMPtr<nsIDocumentEncoder> docEncoder; + if (!mCachedDocumentEncoder || + !mCachedDocumentEncoderType.Equals(aFormatType)) { + nsAutoCString formatType; + LossyAppendUTF16toASCII(aFormatType, formatType); + docEncoder = do_createDocumentEncoder(PromiseFlatCString(formatType).get()); + if (NS_WARN_IF(!docEncoder)) { + return nullptr; + } + mCachedDocumentEncoder = docEncoder; + mCachedDocumentEncoderType = aFormatType; + } else { + docEncoder = mCachedDocumentEncoder; + } + + RefPtr<Document> doc = GetDocument(); + NS_ASSERTION(doc, "Need a document"); + + nsresult rv = docEncoder->NativeInit( + doc, aFormatType, + aDocumentEncoderFlags | nsIDocumentEncoder::RequiresReinitAfterOutput); + if (NS_FAILED(rv)) { + NS_WARNING("nsIDocumentEncoder::NativeInit() failed"); + return nullptr; + } + + if (!aCharset.IsEmpty() && !aCharset.EqualsLiteral("null")) { + DebugOnly<nsresult> rvIgnored = docEncoder->SetCharset(aCharset); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIDocumentEncoder::SetCharset() failed, but ignored"); + } + + const int32_t wrapWidth = std::max(WrapWidth(), 0); + DebugOnly<nsresult> rvIgnored = docEncoder->SetWrapColumn(wrapWidth); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIDocumentEncoder::SetWrapColumn() failed, but ignored"); + + // Set the selection, if appropriate. + // We do this either if the OutputSelectionOnly flag is set, + // in which case we use our existing selection ... + if (aDocumentEncoderFlags & nsIDocumentEncoder::OutputSelectionOnly) { + if (NS_FAILED(docEncoder->SetSelection(&SelectionRef()))) { + NS_WARNING("nsIDocumentEncoder::SetSelection() failed"); + return nullptr; + } + } + // ... or if the root element is not a body, + // in which case we set the selection to encompass the root. + else { + Element* rootElement = GetRoot(); + if (NS_WARN_IF(!rootElement)) { + return nullptr; + } + if (!rootElement->IsHTMLElement(nsGkAtoms::body)) { + if (NS_FAILED(docEncoder->SetContainerNode(rootElement))) { + NS_WARNING("nsIDocumentEncoder::SetContainerNode() failed"); + return nullptr; + } + } + } + + return docEncoder.forget(); +} + +bool EditorBase::AreClipboardCommandsUnconditionallyEnabled() const { + Document* document = GetDocument(); + return document && document->AreClipboardCommandsUnconditionallyEnabled(); +} + +bool EditorBase::CheckForClipboardCommandListener( + nsAtom* aCommand, EventMessage aEventMessage) const { + RefPtr<Document> document = GetDocument(); + if (!document) { + return false; + } + + // We exclude XUL and chrome docs here to maintain current behavior where + // in these cases the editor element alone is expected to handle clipboard + // command availability. + if (!document->AreClipboardCommandsUnconditionallyEnabled()) { + return false; + } + + // So in web content documents, "unconditionally" enabled Cut/Copy are not + // really unconditional; they're enabled if there is a listener that wants + // to handle them. What they're not conditional on here is whether there is + // currently a selection in the editor. + RefPtr<PresShell> presShell = document->GetObservingPresShell(); + if (!presShell) { + return false; + } + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + if (!presContext) { + return false; + } + + RefPtr<EventTarget> et = GetDOMEventTarget(); + while (et) { + EventListenerManager* elm = et->GetExistingListenerManager(); + if (elm && elm->HasListenersFor(aCommand)) { + return true; + } + InternalClipboardEvent event(true, aEventMessage); + EventChainPreVisitor visitor(presContext, &event, nullptr, + nsEventStatus_eIgnore, false, et); + et->GetEventTargetParent(visitor); + et = visitor.GetParentTarget(); + } + + return false; +} + +Result<EditorBase::ClipboardEventResult, nsresult> +EditorBase::DispatchClipboardEventAndUpdateClipboard(EventMessage aEventMessage, + int32_t aClipboardType) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + const bool isPasting = + aEventMessage == ePaste || aEventMessage == ePasteNoFormatting; + if (isPasting) { + CommitComposition(); + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + } + + RefPtr<PresShell> presShell = GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + + const RefPtr<Selection> sel = [&]() { + if (IsHTMLEditor() && aEventMessage == eCopy && + SelectionRef().IsCollapsed()) { + // If we don't have a usable selection for copy and we're an HTML + // editor (which is global for the document) try to use the last + // focused selection instead. + return nsCopySupport::GetSelectionForCopy(GetDocument()); + } + return do_AddRef(&SelectionRef()); + }(); + + bool actionTaken = false; + const bool doDefault = nsCopySupport::FireClipboardEvent( + aEventMessage, aClipboardType, presShell, sel, &actionTaken); + NotifyOfDispatchingClipboardEvent(); + + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + + if (doDefault) { + MOZ_ASSERT(actionTaken); + return ClipboardEventResult::DoDefault; + } + // If we handle a "paste" and nsCopySupport::FireClipboardEvent sets + // actionTaken to "false" means that it's an error. Otherwise, the "paste" + // event is just canceled. + if (isPasting) { + return actionTaken ? ClipboardEventResult::DefaultPreventedOfPaste + : ClipboardEventResult::IgnoredOrError; + } + // If we handle a "copy", actionTaken is set to true only when + // nsCopySupport::FireClipboardEvent does not meet an error. + // If we handle a "cut", actionTaken is set to true only when + // nsCopySupport::FireClipboardEvent does not meet an error and + // - the selection is collapsed in editable elements when the event is not + // canceled. + // - the event is canceled but update the clipboard with the dataTransfer + // of the event. + return actionTaken ? ClipboardEventResult::CopyOrCutHandled + : ClipboardEventResult::IgnoredOrError; +} + +NS_IMETHODIMP EditorBase::Cut() { + nsresult rv = CutAsAction(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed"); + return rv; +} + +nsresult EditorBase::CutAsAction(nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData(*this, EditAction::eCut, aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + { + RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!focusManager)) { + return NS_ERROR_UNEXPECTED; + } + const RefPtr<Element> focusedElement = focusManager->GetFocusedElement(); + + Result<ClipboardEventResult, nsresult> ret = + DispatchClipboardEventAndUpdateClipboard( + eCut, nsIClipboard::kGlobalClipboard); + if (MOZ_UNLIKELY(ret.isErr())) { + NS_WARNING( + "EditorBase::DispatchClipboardEventAndUpdateClipboard(eCut, " + "nsIClipboard::kGlobalClipboard) failed"); + return EditorBase::ToGenericNSResult(ret.unwrapErr()); + } + switch (ret.unwrap()) { + case ClipboardEventResult::DoDefault: + break; + case ClipboardEventResult::CopyOrCutHandled: + return NS_OK; + case ClipboardEventResult::IgnoredOrError: + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + case ClipboardEventResult::DefaultPreventedOfPaste: + MOZ_ASSERT_UNREACHABLE("Invalid result for eCut"); + } + + // If focus is changed by a "cut" event listener, we should stop handling + // the cut. + const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement(); + if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) { + if (focusManager->GetFocusedWindow() != GetWindow()) { + return NS_OK; + } + RefPtr<EditorBase> editorBase = + nsContentUtils::GetActiveEditor(GetPresContext()); + if (!editorBase || (editorBase->IsHTMLEditor() && + !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) { + return NS_OK; + } + if (editorBase != this) { + return NS_OK; + } + } + } + + // Dispatch "beforeinput" event after dispatching "cut" event. + nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "MaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + // XXX This transaction name is referred by PlaceholderTransaction::Merge() + // so that we need to keep using it here. + AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::DeleteTxnName, + ScrollSelectionIntoView::Yes, + __FUNCTION__); + rv = DeleteSelectionAsSubAction( + eNone, IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::DeleteSelectionAsSubAction(eNone) failed, but ignored"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP EditorBase::CanCut(bool* aCanCut) { + if (NS_WARN_IF(!aCanCut)) { + return NS_ERROR_INVALID_ARG; + } + *aCanCut = IsCutCommandEnabled(); + return NS_OK; +} + +bool EditorBase::IsCutCommandEnabled() const { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return false; + } + + if (IsModifiable() && IsCopyToClipboardAllowedInternal()) { + return true; + } + + // If there's an event listener for "cut", we always enable the command + // as we don't really know what the listener may want to do in response. + // We look up the event target chain for a possible listener on a parent + // in addition to checking the immediate target. + return CheckForClipboardCommandListener(nsGkAtoms::oncut, eCut); +} + +NS_IMETHODIMP EditorBase::Copy() { + AutoEditActionDataSetter editActionData(*this, EditAction::eCopy); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + Result<ClipboardEventResult, nsresult> ret = + DispatchClipboardEventAndUpdateClipboard(eCopy, + nsIClipboard::kGlobalClipboard); + if (MOZ_UNLIKELY(ret.isErr())) { + NS_WARNING( + "EditorBase::DispatchClipboardEventAndUpdateClipboard(eCopy, " + "nsIClipboard::kGlobalClipboard) failed"); + return EditorBase::ToGenericNSResult(ret.unwrapErr()); + } + switch (ret.unwrap()) { + case ClipboardEventResult::DoDefault: + case ClipboardEventResult::CopyOrCutHandled: + return NS_OK; + case ClipboardEventResult::IgnoredOrError: + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + case ClipboardEventResult::DefaultPreventedOfPaste: + MOZ_ASSERT_UNREACHABLE("Invalid result for eCopy"); + } + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP EditorBase::CanCopy(bool* aCanCopy) { + if (NS_WARN_IF(!aCanCopy)) { + return NS_ERROR_INVALID_ARG; + } + *aCanCopy = IsCopyCommandEnabled(); + return NS_OK; +} + +bool EditorBase::IsCopyCommandEnabled() const { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return false; + } + + if (IsCopyToClipboardAllowedInternal()) { + return true; + } + + // Like "cut", always enable "copy" if there's a listener. + return CheckForClipboardCommandListener(nsGkAtoms::oncopy, eCopy); +} + +NS_IMETHODIMP EditorBase::Paste(int32_t aClipboardType) { + const nsresult rv = PasteAsAction(aClipboardType, DispatchPasteEvent::Yes); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::PasteAsAction(DispatchPasteEvent::Yes) failed"); + return rv; +} + +nsresult EditorBase::PasteAsAction(int32_t aClipboardType, + DispatchPasteEvent aDispatchPasteEvent, + nsIPrincipal* aPrincipal /* = nullptr */) { + if (IsHTMLEditor() && IsReadonly()) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::ePaste, + aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aDispatchPasteEvent == DispatchPasteEvent::Yes) { + RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!focusManager)) { + return NS_ERROR_UNEXPECTED; + } + const RefPtr<Element> focusedElement = focusManager->GetFocusedElement(); + + Result<ClipboardEventResult, nsresult> ret = + DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType); + if (MOZ_UNLIKELY(ret.isErr())) { + NS_WARNING( + "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) " + "failed"); + return EditorBase::ToGenericNSResult(ret.unwrapErr()); + } + switch (ret.inspect()) { + case ClipboardEventResult::DoDefault: + break; + case ClipboardEventResult::DefaultPreventedOfPaste: + case ClipboardEventResult::IgnoredOrError: + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + case ClipboardEventResult::CopyOrCutHandled: + MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste"); + } + + // If focus is changed by a "paste" event listener, we should keep handling + // the "pasting" in new focused editor because Chrome works as so. + const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement(); + if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) { + // For the privacy reason, let's top handling it if new focused element is + // in different document. + if (focusManager->GetFocusedWindow() != GetWindow()) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + } + RefPtr<EditorBase> editorBase = + nsContentUtils::GetActiveEditor(GetPresContext()); + if (!editorBase || (editorBase->IsHTMLEditor() && + !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + } + if (editorBase != this) { + nsresult rv = editorBase->PasteAsAction( + aClipboardType, DispatchPasteEvent::No, aPrincipal); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::PasteAsAction(DispatchPasteEvent::No) failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + } else { + // The caller must already have dispatched a "paste" event. + editActionData.NotifyOfDispatchingClipboardEvent(); + } + + nsresult rv = HandlePaste(editActionData, aClipboardType); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::HandlePaste() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::PasteAsQuotationAsAction( + int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent, + nsIPrincipal* aPrincipal /* = nullptr */) { + MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard || + aClipboardType == nsIClipboard::kSelectionClipboard); + + if (IsHTMLEditor() && IsReadonly()) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::ePasteAsQuotation, + aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aDispatchPasteEvent == DispatchPasteEvent::Yes) { + RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!focusManager)) { + return NS_ERROR_UNEXPECTED; + } + const RefPtr<Element> focusedElement = focusManager->GetFocusedElement(); + + Result<ClipboardEventResult, nsresult> ret = + DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType); + if (MOZ_UNLIKELY(ret.isErr())) { + NS_WARNING( + "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) " + "failed"); + return EditorBase::ToGenericNSResult(ret.unwrapErr()); + } + switch (ret.inspect()) { + case ClipboardEventResult::DoDefault: + break; + case ClipboardEventResult::DefaultPreventedOfPaste: + case ClipboardEventResult::IgnoredOrError: + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + case ClipboardEventResult::CopyOrCutHandled: + MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste"); + } + + // If focus is changed by a "paste" event listener, we should keep handling + // the "pasting" in new focused editor because Chrome works as so. + const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement(); + if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) { + // For the privacy reason, let's top handling it if new focused element is + // in different document. + if (focusManager->GetFocusedWindow() != GetWindow()) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + } + RefPtr<EditorBase> editorBase = + nsContentUtils::GetActiveEditor(GetPresContext()); + if (!editorBase || (editorBase->IsHTMLEditor() && + !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + } + if (editorBase != this) { + nsresult rv = editorBase->PasteAsQuotationAsAction( + aClipboardType, DispatchPasteEvent::No, aPrincipal); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::PasteAsQuotationAsAction(" + "DispatchPasteEvent::No) failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + } else { + // The caller must already have dispatched a "paste" event. + editActionData.NotifyOfDispatchingClipboardEvent(); + } + + nsresult rv = HandlePasteAsQuotation(editActionData, aClipboardType); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::HandlePasteAsQuotation() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::PasteTransferableAsAction( + nsITransferable* aTransferable, DispatchPasteEvent aDispatchPasteEvent, + nsIPrincipal* aPrincipal /* = nullptr */) { + // FIXME: This may be called as a call of nsIEditor::PasteTransferable. + // In this case, we should keep handling the paste even in the readonly mode. + if (IsHTMLEditor() && IsReadonly()) { + return NS_OK; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::ePaste, + aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aDispatchPasteEvent == DispatchPasteEvent::Yes) { + RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!focusManager)) { + return NS_ERROR_UNEXPECTED; + } + const RefPtr<Element> focusedElement = focusManager->GetFocusedElement(); + + // Use an invalid value for the clipboard type as data comes from + // aTransferable and we don't currently implement a way to put that in the + // data transfer in TextEditor yet. + Result<ClipboardEventResult, nsresult> ret = + DispatchClipboardEventAndUpdateClipboard( + ePaste, IsTextEditor() ? -1 : nsIClipboard::kGlobalClipboard); + if (MOZ_UNLIKELY(ret.isErr())) { + NS_WARNING( + "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) " + "failed"); + return EditorBase::ToGenericNSResult(ret.unwrapErr()); + } + switch (ret.inspect()) { + case ClipboardEventResult::DoDefault: + break; + case ClipboardEventResult::DefaultPreventedOfPaste: + case ClipboardEventResult::IgnoredOrError: + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + case ClipboardEventResult::CopyOrCutHandled: + MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste"); + } + + // If focus is changed by a "paste" event listener, we should keep handling + // the "pasting" in new focused editor because Chrome works as so. + const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement(); + if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) { + // For the privacy reason, let's top handling it if new focused element is + // in different document. + if (focusManager->GetFocusedWindow() != GetWindow()) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + } + RefPtr<EditorBase> editorBase = + nsContentUtils::GetActiveEditor(GetPresContext()); + if (!editorBase || (editorBase->IsHTMLEditor() && + !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) { + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED); + } + if (editorBase != this) { + nsresult rv = editorBase->PasteTransferableAsAction( + aTransferable, DispatchPasteEvent::No, aPrincipal); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::PasteTransferableAsAction(" + "DispatchPasteEvent::No) failed"); + return EditorBase::ToGenericNSResult(rv); + } + } + } else { + // The caller must already have dispatched a "paste" event. + editActionData.NotifyOfDispatchingClipboardEvent(); + } + + if (NS_WARN_IF(!aTransferable)) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv = HandlePasteTransferable(editActionData, *aTransferable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::HandlePasteTransferable() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::PrepareToInsertContent( + const EditorDOMPoint& aPointToInsert, + DeleteSelectedContent aDeleteSelectedContent) { + // TODO: Move this method to `EditorBase`. + MOZ_ASSERT(IsEditActionDataAvailable()); + + MOZ_ASSERT(aPointToInsert.IsSet()); + + EditorDOMPoint pointToInsert(aPointToInsert); + if (aDeleteSelectedContent == DeleteSelectedContent::Yes) { + AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert); + nsresult rv = DeleteSelectionAsSubAction( + nsIEditor::eNone, + IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteSelectionAsSubAction(eNone) failed"); + return rv; + } + } + + nsresult rv = CollapseSelectionTo(pointToInsert); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; +} + +nsresult EditorBase::InsertTextAt( + const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert, + DeleteSelectedContent aDeleteSelectedContent) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aPointToInsert.IsSet()); + + nsresult rv = PrepareToInsertContent(aPointToInsert, aDeleteSelectedContent); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::PrepareToInsertContent() failed"); + return rv; + } + + rv = InsertTextAsSubAction(aStringToInsert, SelectionHandling::Delete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::InsertTextAsSubAction() failed"); + return rv; +} + +EditorBase::SafeToInsertData EditorBase::IsSafeToInsertData( + nsIPrincipal* aSourcePrincipal) const { + // Try to determine whether we should use a sanitizing fragment sink + RefPtr<Document> destdoc = GetDocument(); + NS_ASSERTION(destdoc, "Where is our destination doc?"); + + nsIDocShell* docShell = nullptr; + if (RefPtr<BrowsingContext> bc = destdoc->GetBrowsingContext()) { + RefPtr<BrowsingContext> root = bc->Top(); + MOZ_ASSERT(root, "root should not be null"); + + docShell = root->GetDocShell(); + } + + bool isSafe = + docShell && docShell->GetAppType() == nsIDocShell::APP_TYPE_EDITOR; + + if (!isSafe && aSourcePrincipal) { + nsIPrincipal* destPrincipal = destdoc->NodePrincipal(); + NS_ASSERTION(destPrincipal, "How come we don't have a principal?"); + DebugOnly<nsresult> rvIgnored = + aSourcePrincipal->Subsumes(destPrincipal, &isSafe); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsIPrincipal::Subsumes() failed, but ignored"); + } + + return isSafe ? SafeToInsertData::Yes : SafeToInsertData::No; +} + +NS_IMETHODIMP EditorBase::PasteTransferable(nsITransferable* aTransferable) { + nsresult rv = + PasteTransferableAsAction(aTransferable, DispatchPasteEvent::Yes); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::PasteTransferableAsAction(DispatchPasteEvent::Yes) failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::CanPaste(int32_t aClipboardType, bool* aCanPaste) { + if (NS_WARN_IF(!aCanPaste)) { + return NS_ERROR_INVALID_ARG; + } + *aCanPaste = CanPaste(aClipboardType); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::SetAttribute(Element* aElement, + const nsAString& aAttribute, + const nsAString& aValue) { + if (NS_WARN_IF(aAttribute.IsEmpty()) || NS_WARN_IF(!aElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<nsAtom> attribute = NS_Atomize(aAttribute); + rv = SetAttributeWithTransaction(*aElement, *attribute, aValue); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::SetAttributeWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::SetAttributeWithTransaction(Element& aElement, + nsAtom& aAttribute, + const nsAString& aValue) { + RefPtr<ChangeAttributeTransaction> transaction = + ChangeAttributeTransaction::Create(aElement, aAttribute, aValue); + nsresult rv = DoTransactionInternal(transaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::RemoveAttribute(Element* aElement, + const nsAString& aAttribute) { + if (NS_WARN_IF(aAttribute.IsEmpty()) || NS_WARN_IF(!aElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveAttribute); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<nsAtom> attribute = NS_Atomize(aAttribute); + rv = RemoveAttributeWithTransaction(*aElement, *attribute); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::RemoveAttributeWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::RemoveAttributeWithTransaction(Element& aElement, + nsAtom& aAttribute) { + if (!aElement.HasAttr(&aAttribute)) { + return NS_OK; + } + RefPtr<ChangeAttributeTransaction> transaction = + ChangeAttributeTransaction::CreateToRemove(aElement, aAttribute); + nsresult rv = DoTransactionInternal(transaction); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + return rv; +} + +nsresult EditorBase::MarkElementDirty(Element& aElement) const { + // Mark the node dirty, but not for webpages (bug 599983) + if (!OutputsMozDirty()) { + return NS_OK; + } + DebugOnly<nsresult> rvIgnored = + aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::mozdirty, u""_ns, false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "Element::SetAttr(nsGkAtoms::mozdirty) failed, but ignored"); + return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; +} + +NS_IMETHODIMP EditorBase::GetInlineSpellChecker( + bool aAutoCreate, nsIInlineSpellChecker** aInlineSpellChecker) { + if (NS_WARN_IF(!aInlineSpellChecker)) { + return NS_ERROR_INVALID_ARG; + } + + if (mDidPreDestroy) { + // Don't allow people to get or create the spell checker once the editor + // is going away. + *aInlineSpellChecker = nullptr; + return aAutoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK; + } + + // We don't want to show the spell checking UI if there are no spell check + // dictionaries available. + if (!mozInlineSpellChecker::CanEnableInlineSpellChecking()) { + *aInlineSpellChecker = nullptr; + return NS_ERROR_FAILURE; + } + + if (!mInlineSpellChecker && aAutoCreate) { + mInlineSpellChecker = new mozInlineSpellChecker(); + } + + if (mInlineSpellChecker) { + nsresult rv = mInlineSpellChecker->Init(this); + if (NS_FAILED(rv)) { + NS_WARNING("mozInlineSpellChecker::Init() failed"); + mInlineSpellChecker = nullptr; + return rv; + } + } + + *aInlineSpellChecker = do_AddRef(mInlineSpellChecker).take(); + return NS_OK; +} + +void EditorBase::SyncRealTimeSpell() { + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return; + } + + bool enable = GetDesiredSpellCheckState(); + + // Initializes mInlineSpellChecker + nsCOMPtr<nsIInlineSpellChecker> spellChecker; + DebugOnly<nsresult> rvIgnored = + GetInlineSpellChecker(enable, getter_AddRefs(spellChecker)); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::GetInlineSpellChecker() failed, but ignored"); + + if (mInlineSpellChecker) { + if (!mSpellCheckerDictionaryUpdated && enable) { + DebugOnly<nsresult> rvIgnored = + mInlineSpellChecker->UpdateCurrentDictionary(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "mozInlineSpellChecker::UpdateCurrentDictionary() " + "failed, but ignored"); + mSpellCheckerDictionaryUpdated = true; + } + + // We might have a mInlineSpellChecker even if there are no dictionaries + // available since we don't destroy the mInlineSpellChecker when the last + // dictionariy is removed, but in that case spellChecker is null + DebugOnly<nsresult> rvIgnored = + mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "mozInlineSpellChecker::SetEnableRealTimeSpell() failed, but ignored"); + } +} + +NS_IMETHODIMP EditorBase::SetSpellcheckUserOverride(bool enable) { + mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse; + SyncRealTimeSpell(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::InsertNode(nsINode* aNodeToInsert, + nsINode* aContainer, uint32_t aOffset) { + nsCOMPtr<nsIContent> contentToInsert = do_QueryInterface(aNodeToInsert); + if (NS_WARN_IF(!contentToInsert) || NS_WARN_IF(!aContainer)) { + return NS_ERROR_NULL_POINTER; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eInsertNode); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + const uint32_t offset = std::min(aOffset, aContainer->Length()); + Result<CreateContentResult, nsresult> insertContentResult = + InsertNodeWithTransaction(*contentToInsert, + EditorDOMPoint(aContainer, offset)); + if (MOZ_UNLIKELY(insertContentResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return EditorBase::ToGenericNSResult(insertContentResult.unwrapErr()); + } + rv = insertContentResult.inspect().SuggestCaretPointTo( + *this, {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt, + SuggestCaret::AndIgnoreTrivialError}); + if (NS_FAILED(rv)) { + NS_WARNING("CreateContentResult::SuggestCaretPointTo() failed"); + return EditorBase::ToGenericNSResult(rv); + } + NS_WARNING_ASSERTION( + rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, + "CreateContentResult::SuggestCaretPointTo() failed, but ignored"); + return NS_OK; +} + +template <typename ContentNodeType> +Result<CreateNodeResultBase<ContentNodeType>, nsresult> +EditorBase::InsertNodeWithTransaction(ContentNodeType& aContentToInsert, + const EditorDOMPoint& aPointToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT_IF(IsTextEditor(), !aContentToInsert.IsText()); + + if (NS_WARN_IF(!aPointToInsert.IsSet())) { + return Err(NS_ERROR_INVALID_ARG); + } + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertNode, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return Err(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + RefPtr<InsertNodeTransaction> transaction = + InsertNodeTransaction::Create(*this, aContentToInsert, aPointToInsert); + nsresult rv = DoTransactionInternal(transaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + + DebugOnly<nsresult> rvIgnored = + RangeUpdaterRef().SelAdjInsertNode(aPointToInsert); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "RangeUpdater::SelAdjInsertNode() failed, but ignored"); + + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_WARN_IF(aContentToInsert.GetParentNode() != + aPointToInsert.GetContainer())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + if (NS_FAILED(rv)) { + return Err(rv); + } + + if (IsHTMLEditor()) { + TopLevelEditSubActionDataRef().DidInsertContent(*this, aContentToInsert); + } + + return CreateNodeResultBase<ContentNodeType>( + &aContentToInsert, transaction->SuggestPointToPutCaret<EditorDOMPoint>()); +} + +Result<CreateElementResult, nsresult> +EditorBase::InsertPaddingBRElementForEmptyLastLineWithTransaction( + const EditorDOMPoint& aPointToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(IsHTMLEditor() || !aPointToInsert.IsInTextNode()); + + if (MOZ_UNLIKELY(!aPointToInsert.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + EditorDOMPoint pointToInsert; + if (IsTextEditor()) { + pointToInsert = aPointToInsert; + } else { + Result<EditorDOMPoint, nsresult> maybePointToInsert = + MOZ_KnownLive(AsHTMLEditor())->PrepareToInsertBRElement(aPointToInsert); + if (maybePointToInsert.isErr()) { + return maybePointToInsert.propagateErr(); + } + MOZ_ASSERT(maybePointToInsert.inspect().IsSetAndValid()); + pointToInsert = maybePointToInsert.unwrap(); + } + + RefPtr<Element> newBRElement = CreateHTMLContent(nsGkAtoms::br); + if (NS_WARN_IF(!newBRElement)) { + return Err(NS_ERROR_FAILURE); + } + newBRElement->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE); + + Result<CreateElementResult, nsresult> insertBRElementResult = + InsertNodeWithTransaction<Element>(*newBRElement, pointToInsert); + NS_WARNING_ASSERTION(insertBRElementResult.isOk(), + "EditorBase::InsertNodeWithTransaction() failed"); + return insertBRElementResult; +} + +NS_IMETHODIMP EditorBase::DeleteNode(nsINode* aNode) { + if (NS_WARN_IF(!aNode) || NS_WARN_IF(!aNode->IsContent())) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveNode); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = DeleteNodeWithTransaction(MOZ_KnownLive(*aNode->AsContent())); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::DeleteNodeWithTransaction(nsIContent& aContent) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT_IF(IsTextEditor(), !aContent.IsText()); + + // Do nothing if the node is read-only. + if (IsHTMLEditor() && NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aContent))) { + return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; + } + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteNode, nsIEditor::ePrevious, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + if (IsHTMLEditor()) { + TopLevelEditSubActionDataRef().WillDeleteContent(*this, aContent); + } + + // FYI: DeleteNodeTransaction grabs aContent while it's alive. So, it's safe + // to refer aContent even after calling DoTransaction(). + RefPtr<DeleteNodeTransaction> deleteNodeTransaction = + DeleteNodeTransaction::MaybeCreate(*this, aContent); + NS_WARNING_ASSERTION(deleteNodeTransaction, + "DeleteNodeTransaction::MaybeCreate() failed"); + nsresult rv; + if (deleteNodeTransaction) { + rv = DoTransactionInternal(deleteNodeTransaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + + if (mTextServicesDocument && NS_SUCCEEDED(rv)) { + RefPtr<TextServicesDocument> textServicesDocument = mTextServicesDocument; + textServicesDocument->DidDeleteContent(aContent); + } + } else { + rv = NS_ERROR_FAILURE; + } + + if (!mActionListeners.IsEmpty()) { + for (auto& listener : mActionListeners.Clone()) { + DebugOnly<nsresult> rvIgnored = listener->DidDeleteNode(&aContent, rv); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::DidDeleteNode() failed, but ignored"); + } + } + + return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : rv; +} + +NS_IMETHODIMP EditorBase::NotifySelectionChanged(Document* aDocument, + Selection* aSelection, + int16_t aReason, + int32_t aAmount) { + if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) { + return NS_ERROR_INVALID_ARG; + } + + if (mTextInputListener) { + RefPtr<TextInputListener> textInputListener = mTextInputListener; + textInputListener->OnSelectionChange(*aSelection, aReason); + } + + if (mIMEContentObserver) { + RefPtr<IMEContentObserver> observer = mIMEContentObserver; + observer->OnSelectionChange(*aSelection); + } + + return NS_OK; +} + +void EditorBase::NotifyEditorObservers( + NotificationForEditorObservers aNotification) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + switch (aNotification) { + case eNotifyEditorObserversOfEnd: + mIsInEditSubAction = false; + + if (mEditActionData) { + mEditActionData->MarkAsHandled(); + } + + if (mTextInputListener) { + // TODO: TextInputListener::OnEditActionHandled() may return + // NS_ERROR_OUT_OF_MEMORY. If so and if + // TextControlState::SetValue() setting value with us, we should + // return the result to EditorBase::ReplaceTextAsAction(), + // EditorBase::DeleteSelectionAsAction() and + // TextEditor::InsertTextAsAction(). However, it requires a lot + // of changes in editor classes, but it's not so important since + // editor does not use fallible allocation. Therefore, normally, + // the process must be crashed anyway. + RefPtr<TextInputListener> listener = mTextInputListener; + nsresult rv = + listener->OnEditActionHandled(MOZ_KnownLive(*AsTextEditor())); + MOZ_RELEASE_ASSERT(rv != NS_ERROR_OUT_OF_MEMORY, + "Setting value failed due to out of memory"); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "TextInputListener::OnEditActionHandled() failed, but ignored"); + } + + if (mIMEContentObserver) { + RefPtr<IMEContentObserver> observer = mIMEContentObserver; + observer->OnEditActionHandled(); + } + + if (!mDispatchInputEvent || IsEditActionAborted() || + IsEditActionCanceled()) { + break; + } + + DispatchInputEvent(); + break; + case eNotifyEditorObserversOfBefore: + if (NS_WARN_IF(mIsInEditSubAction)) { + return; + } + + mIsInEditSubAction = true; + + if (mIMEContentObserver) { + RefPtr<IMEContentObserver> observer = mIMEContentObserver; + observer->BeforeEditAction(); + } + return; + case eNotifyEditorObserversOfCancel: + mIsInEditSubAction = false; + + if (mEditActionData) { + mEditActionData->MarkAsHandled(); + } + + if (mIMEContentObserver) { + RefPtr<IMEContentObserver> observer = mIMEContentObserver; + observer->CancelEditAction(); + } + break; + default: + MOZ_CRASH("Handle all notifications here"); + break; + } + + if (IsHTMLEditor() && !Destroyed()) { + // We may need to show resizing handles or update existing ones after + // all transactions are done. This way of doing is preferred to DOM + // mutation events listeners because all the changes the user can apply + // to a document may result in multiple events, some of them quite hard + // to listen too (in particular when an ancestor of the selection is + // changed but the selection itself is not changed). + DebugOnly<nsresult> rvIgnored = + MOZ_KnownLive(AsHTMLEditor())->RefreshEditingUI(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "HTMLEditor::RefreshEditingUI() failed, but ignored"); + } +} + +void EditorBase::DispatchInputEvent() { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!IsEditActionCanceled(), + "If preceding beforeinput event is canceled, we shouldn't " + "dispatch input event"); + MOZ_ASSERT( + !ShouldAlreadyHaveHandledBeforeInputEventDispatching(), + "We've not handled beforeinput event but trying to dispatch input event"); + + // We don't need to dispatch multiple input events if there is a pending + // input event. However, it may have different event target. If we resolved + // this issue, we need to manage the pending events in an array. But it's + // overwork. We don't need to do it for the very rare case. + // TODO: However, we start to set InputEvent.inputType. So, each "input" + // event now notifies web app each change. So, perhaps, we should + // not omit input events. + + RefPtr<Element> targetElement = GetInputEventTargetElement(); + if (NS_WARN_IF(!targetElement)) { + return; + } + RefPtr<DataTransfer> dataTransfer = GetInputEventDataTransfer(); + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent( + targetElement, eEditorInput, ToInputType(GetEditAction()), this, + dataTransfer ? InputEventOptions(dataTransfer, + InputEventOptions::NeverCancelable::No) + : InputEventOptions(GetInputEventData(), + InputEventOptions::NeverCancelable::No)); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsContentUtils::DispatchInputEvent() failed, but ignored"); +} + +NS_IMETHODIMP EditorBase::AddEditActionListener( + nsIEditActionListener* aListener) { + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_INVALID_ARG; + } + + // If given edit action listener is text services document for the inline + // spell checker, store it as reference of concrete class for performance + // reason. + if (mInlineSpellChecker) { + EditorSpellCheck* editorSpellCheck = + mInlineSpellChecker->GetEditorSpellCheck(); + if (editorSpellCheck) { + mozSpellChecker* spellChecker = editorSpellCheck->GetSpellChecker(); + if (spellChecker) { + TextServicesDocument* textServicesDocument = + spellChecker->GetTextServicesDocument(); + if (static_cast<nsIEditActionListener*>(textServicesDocument) == + aListener) { + mTextServicesDocument = textServicesDocument; + return NS_OK; + } + } + } + } + + // Make sure the listener isn't already on the list + if (!mActionListeners.Contains(aListener)) { + mActionListeners.AppendElement(*aListener); + NS_WARNING_ASSERTION( + mActionListeners.Length() != 1, + "nsIEditActionListener installed, this editor becomes slower"); + } + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::RemoveEditActionListener( + nsIEditActionListener* aListener) { + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_INVALID_ARG; + } + + if (static_cast<nsIEditActionListener*>(mTextServicesDocument) == aListener) { + mTextServicesDocument = nullptr; + return NS_OK; + } + + NS_WARNING_ASSERTION(mActionListeners.Length() != 1, + "All nsIEditActionListeners have been removed, this " + "editor becomes faster"); + mActionListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::AddDocumentStateListener( + nsIDocumentStateListener* aListener) { + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_INVALID_ARG; + } + + if (!mDocStateListeners.Contains(aListener)) { + mDocStateListeners.AppendElement(*aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::RemoveDocumentStateListener( + nsIDocumentStateListener* aListener) { + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mDocStateListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::ForceCompositionEnd() { + nsresult rv = CommitComposition(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CommitComposition() failed"); + return rv; +} + +nsresult EditorBase::CommitComposition() { + nsPresContext* presContext = GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!mComposition) { + return NS_OK; + } + nsresult rv = + IMEStateManager::NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, presContext); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "IMEStateManager::NotifyIME() failed"); + return rv; +} + +nsresult EditorBase::GetPreferredIMEState(IMEState* aState) { + if (NS_WARN_IF(!aState)) { + return NS_ERROR_INVALID_ARG; + } + + aState->mEnabled = IMEEnabled::Enabled; + aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE; + + if (IsReadonly()) { + aState->mEnabled = IMEEnabled::Disabled; + return NS_OK; + } + + Element* rootElement = GetRoot(); + if (NS_WARN_IF(!rootElement)) { + return NS_ERROR_FAILURE; + } + + nsIFrame* frameForRootElement = rootElement->GetPrimaryFrame(); + if (NS_WARN_IF(!frameForRootElement)) { + return NS_ERROR_FAILURE; + } + + switch (frameForRootElement->StyleUIReset()->mIMEMode) { + case StyleImeMode::Auto: + if (IsPasswordEditor()) { + aState->mEnabled = IMEEnabled::Password; + } + break; + case StyleImeMode::Disabled: + // we should use password state for |ime-mode: disabled;|. + aState->mEnabled = IMEEnabled::Password; + break; + case StyleImeMode::Active: + aState->mOpen = IMEState::OPEN; + break; + case StyleImeMode::Inactive: + aState->mOpen = IMEState::CLOSED; + break; + case StyleImeMode::Normal: + break; + } + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetComposing(bool* aResult) { + if (NS_WARN_IF(!aResult)) { + return NS_ERROR_INVALID_ARG; + } + *aResult = IsIMEComposing(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetRootElement(Element** aRootElement) { + if (NS_WARN_IF(!aRootElement)) { + return NS_ERROR_INVALID_ARG; + } + *aRootElement = do_AddRef(mRootElement).take(); + return NS_WARN_IF(!*aRootElement) ? NS_ERROR_NOT_AVAILABLE : NS_OK; +} + +void EditorBase::OnStartToHandleTopLevelEditSubAction( + EditSubAction aTopLevelEditSubAction, + nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!aRv.Failed()); + mEditActionData->SetTopLevelEditSubAction(aTopLevelEditSubAction, + aDirectionOfTopLevelEditSubAction); +} + +nsresult EditorBase::OnEndHandlingTopLevelEditSubAction() { + MOZ_ASSERT(IsEditActionDataAvailable()); + mEditActionData->SetTopLevelEditSubAction(EditSubAction::eNone, eNone); + return NS_OK; +} + +void EditorBase::DoInsertText(Text& aText, uint32_t aOffset, + const nsAString& aStringToInsert, + ErrorResult& aRv) { + aText.InsertData(aOffset, aStringToInsert, aRv); + if (NS_WARN_IF(Destroyed())) { + aRv = NS_ERROR_EDITOR_DESTROYED; + return; + } + if (aRv.Failed()) { + NS_WARNING("Text::InsertData() failed"); + return; + } + if (IsTextEditor() && !aStringToInsert.IsEmpty()) { + aRv = MOZ_KnownLive(AsTextEditor()) + ->DidInsertText(aText.TextLength(), aOffset, + aStringToInsert.Length()); + NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed"); + } +} + +void EditorBase::DoDeleteText(Text& aText, uint32_t aOffset, uint32_t aCount, + ErrorResult& aRv) { + if (IsTextEditor() && aCount > 0) { + AsTextEditor()->WillDeleteText(aText.TextLength(), aOffset, aCount); + } + aText.DeleteData(aOffset, aCount, aRv); + if (NS_WARN_IF(Destroyed())) { + aRv = NS_ERROR_EDITOR_DESTROYED; + return; + } + NS_WARNING_ASSERTION(!aRv.Failed(), "Text::DeleteData() failed"); +} + +void EditorBase::DoReplaceText(Text& aText, uint32_t aOffset, uint32_t aCount, + const nsAString& aStringToInsert, + ErrorResult& aRv) { + if (IsTextEditor() && aCount > 0) { + AsTextEditor()->WillDeleteText(aText.TextLength(), aOffset, aCount); + } + aText.ReplaceData(aOffset, aCount, aStringToInsert, aRv); + if (NS_WARN_IF(Destroyed())) { + aRv = NS_ERROR_EDITOR_DESTROYED; + return; + } + if (aRv.Failed()) { + NS_WARNING("Text::ReplaceData() failed"); + return; + } + if (IsTextEditor() && !aStringToInsert.IsEmpty()) { + aRv = MOZ_KnownLive(AsTextEditor()) + ->DidInsertText(aText.TextLength(), aOffset, + aStringToInsert.Length()); + NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed"); + } +} + +void EditorBase::DoSetText(Text& aText, const nsAString& aStringToSet, + ErrorResult& aRv) { + if (IsTextEditor()) { + uint32_t length = aText.TextLength(); + if (length > 0) { + AsTextEditor()->WillDeleteText(length, 0, length); + } + } + aText.SetData(aStringToSet, aRv); + if (NS_WARN_IF(Destroyed())) { + aRv = NS_ERROR_EDITOR_DESTROYED; + return; + } + if (aRv.Failed()) { + NS_WARNING("Text::SetData() failed"); + return; + } + if (IsTextEditor() && !aStringToSet.IsEmpty()) { + aRv = MOZ_KnownLive(AsTextEditor()) + ->DidInsertText(aText.Length(), 0, aStringToSet.Length()); + NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed"); + } +} + +nsresult EditorBase::CloneAttributeWithTransaction(nsAtom& aAttribute, + Element& aDestElement, + Element& aSourceElement) { + nsAutoString attrValue; + if (aSourceElement.GetAttr(kNameSpaceID_None, &aAttribute, attrValue)) { + nsresult rv = + SetAttributeWithTransaction(aDestElement, aAttribute, attrValue); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::SetAttributeWithTransaction() failed"); + return rv; + } + nsresult rv = RemoveAttributeWithTransaction(aDestElement, aAttribute); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::RemoveAttributeWithTransaction() failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::CloneAttributes(Element* aDestElement, + Element* aSourceElement) { + if (NS_WARN_IF(!aDestElement) || NS_WARN_IF(!aSourceElement)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + CloneAttributesWithTransaction(*aDestElement, *aSourceElement); + + return NS_OK; +} + +void EditorBase::CloneAttributesWithTransaction(Element& aDestElement, + Element& aSourceElement) { + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + // Use transaction system for undo only if destination is already in the + // document + Element* rootElement = GetRoot(); + if (NS_WARN_IF(!rootElement)) { + return; + } + + OwningNonNull<Element> destElement(aDestElement); + OwningNonNull<Element> sourceElement(aSourceElement); + bool isDestElementInBody = rootElement->Contains(destElement); + + // Clear existing attributes + RefPtr<nsDOMAttributeMap> destAttributes = destElement->Attributes(); + while (RefPtr<Attr> attr = destAttributes->Item(0)) { + if (isDestElementInBody) { + DebugOnly<nsresult> rvIgnored = RemoveAttributeWithTransaction( + destElement, MOZ_KnownLive(*attr->NodeInfo()->NameAtom())); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::RemoveAttributeWithTransaction() failed, but ignored"); + } else { + DebugOnly<nsresult> rvIgnored = destElement->UnsetAttr( + kNameSpaceID_None, attr->NodeInfo()->NameAtom(), true); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Element::UnsetAttr() failed, but ignored"); + } + } + + // Set just the attributes that the source element has + RefPtr<nsDOMAttributeMap> sourceAttributes = sourceElement->Attributes(); + uint32_t sourceCount = sourceAttributes->Length(); + for (uint32_t i = 0; i < sourceCount; i++) { + RefPtr<Attr> attr = sourceAttributes->Item(i); + nsAutoString value; + attr->GetValue(value); + if (isDestElementInBody) { + DebugOnly<nsresult> rvIgnored = SetAttributeOrEquivalent( + destElement, MOZ_KnownLive(attr->NodeInfo()->NameAtom()), value, + false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::SetAttributeOrEquivalent() failed, but ignored"); + } else { + // The element is not inserted in the document yet, we don't want to put + // a transaction on the UndoStack + DebugOnly<nsresult> rvIgnored = SetAttributeOrEquivalent( + destElement, MOZ_KnownLive(attr->NodeInfo()->NameAtom()), value, + true); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::SetAttributeOrEquivalent() failed, but ignored"); + } + } +} + +nsresult EditorBase::ScrollSelectionFocusIntoView() const { + nsISelectionController* selectionController = GetSelectionController(); + if (!selectionController) { + return NS_OK; + } + + DebugOnly<nsresult> rvIgnored = selectionController->ScrollSelectionIntoView( + nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_FOCUS_REGION, + nsISelectionController::SCROLL_OVERFLOW_HIDDEN); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsISelectionController::ScrollSelectionIntoView() failed, but ignored"); + return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; +} + +template <typename EditorDOMPointType> +EditorDOMPointType EditorBase::FindBetterInsertionPoint( + const EditorDOMPointType& aPoint) const { + if (MOZ_UNLIKELY(NS_WARN_IF(!aPoint.IsInContentNode()))) { + return aPoint; + } + + MOZ_ASSERT(aPoint.IsSetAndValid()); + + if (aPoint.IsInTextNode()) { + // There is no "better" insertion point. + return aPoint; + } + + if (!IsInPlaintextMode()) { + // We cannot find "better" insertion point in HTML editor. + // WARNING: When you add some code to find better node in HTML editor, + // you need to call this before calling InsertTextWithTransaction() + // in HTMLEditor. + return aPoint; + } + + RefPtr<Element> rootElement = GetRoot(); + if (aPoint.GetContainer() == rootElement) { + // In some cases, aNode is the anonymous DIV, and offset is 0. To avoid + // injecting unneeded text nodes, we first look to see if we have one + // available. In that case, we'll just adjust node and offset accordingly. + if (aPoint.IsStartOfContainer() && aPoint.GetContainer()->HasChildren() && + aPoint.GetContainer()->GetFirstChild()->IsText()) { + return EditorDOMPointType(aPoint.GetContainer()->GetFirstChild(), 0u); + } + + // In some other cases, aNode is the anonymous DIV, and offset points to + // the terminating padding <br> element for empty last line. In that case, + // we'll adjust aInOutNode and aInOutOffset to the preceding text node, + // if any. + if (!aPoint.IsStartOfContainer()) { + if (IsHTMLEditor()) { + // Fall back to a slow path that uses GetChildAt_Deprecated() for + // Thunderbird's plaintext editor. + nsIContent* child = aPoint.GetPreviousSiblingOfChild(); + if (child && child->IsText()) { + return EditorDOMPointType::AtEndOf(*child); + } + } else { + // If we're in a real plaintext editor, use a fast path that avoids + // calling GetChildAt_Deprecated() which may perform a linear search. + nsIContent* child = aPoint.GetContainer()->GetLastChild(); + while (child) { + if (child->IsText()) { + return EditorDOMPointType::AtEndOf(*child); + } + child = child->GetPreviousSibling(); + } + } + } + } + + // Sometimes, aNode is the padding <br> element itself. In that case, we'll + // adjust the insertion point to the previous text node, if one exists, or + // to the parent anonymous DIV. + if (EditorUtils::IsPaddingBRElementForEmptyLastLine( + *aPoint.template ContainerAs<nsIContent>()) && + aPoint.IsStartOfContainer()) { + nsIContent* previousSibling = aPoint.GetContainer()->GetPreviousSibling(); + if (previousSibling && previousSibling->IsText()) { + return EditorDOMPointType::AtEndOf(*previousSibling); + } + + nsINode* parentOfContainer = aPoint.GetContainerParent(); + if (parentOfContainer && parentOfContainer == rootElement) { + return EditorDOMPointType(parentOfContainer, + aPoint.template ContainerAs<nsIContent>(), 0u); + } + } + + return aPoint; +} + +Result<InsertTextResult, nsresult> EditorBase::InsertTextWithTransaction( + Document& aDocument, const nsAString& aStringToInsert, + const EditorDOMPoint& aPointToInsert) { + if (NS_WARN_IF(!aPointToInsert.IsSet())) { + return Err(NS_ERROR_INVALID_ARG); + } + + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + + if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) { + return InsertTextResult(); + } + + // In some cases, the node may be the anonymous div element or a padding + // <br> element for empty last line. Let's try to look for better insertion + // point in the nearest text node if there is. + EditorDOMPoint pointToInsert = FindBetterInsertionPoint(aPointToInsert); + + // If a neighboring text node already exists, use that + if (!pointToInsert.IsInTextNode()) { + nsIContent* child = nullptr; + if (!pointToInsert.IsStartOfContainer() && + (child = pointToInsert.GetPreviousSiblingOfChild()) && + child->IsText()) { + pointToInsert.Set(child, child->Length()); + } else if (!pointToInsert.IsEndOfContainer() && + (child = pointToInsert.GetChild()) && child->IsText()) { + pointToInsert.Set(child, 0); + } + } + + if (ShouldHandleIMEComposition()) { + if (!pointToInsert.IsInTextNode()) { + // create a text node + RefPtr<nsTextNode> newTextNode = CreateTextNode(u""_ns); + if (NS_WARN_IF(!newTextNode)) { + return Err(NS_ERROR_FAILURE); + } + // then we insert it into the dom tree + Result<CreateTextResult, nsresult> insertTextNodeResult = + InsertNodeWithTransaction<Text>(*newTextNode, pointToInsert); + if (MOZ_UNLIKELY(insertTextNodeResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertTextNodeResult.propagateErr(); + } + insertTextNodeResult.unwrap().IgnoreCaretPointSuggestion(); + pointToInsert.Set(newTextNode, 0u); + } + Result<InsertTextResult, nsresult> insertTextResult = + InsertTextIntoTextNodeWithTransaction(aStringToInsert, + pointToInsert.AsInText()); + NS_WARNING_ASSERTION( + insertTextResult.isOk(), + "EditorBase::InsertTextIntoTextNodeWithTransaction() failed"); + return insertTextResult; + } + + if (pointToInsert.IsInTextNode()) { + // we are inserting text into an existing text node. + Result<InsertTextResult, nsresult> insertTextResult = + InsertTextIntoTextNodeWithTransaction(aStringToInsert, + pointToInsert.AsInText()); + NS_WARNING_ASSERTION( + insertTextResult.isOk(), + "EditorBase::InsertTextIntoTextNodeWithTransaction() failed"); + return insertTextResult; + } + + // we are inserting text into a non-text node. first we have to create a + // textnode (this also populates it with the text) + RefPtr<nsTextNode> newTextNode = CreateTextNode(aStringToInsert); + if (NS_WARN_IF(!newTextNode)) { + return Err(NS_ERROR_FAILURE); + } + // then we insert it into the dom tree + Result<CreateTextResult, nsresult> insertTextNodeResult = + InsertNodeWithTransaction<Text>(*newTextNode, pointToInsert); + if (MOZ_UNLIKELY(insertTextNodeResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return Err(insertTextNodeResult.unwrapErr()); + } + insertTextNodeResult.unwrap().IgnoreCaretPointSuggestion(); + if (NS_WARN_IF(!newTextNode->IsInComposedDoc())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + return InsertTextResult(EditorDOMPointInText::AtEndOf(*newTextNode), + EditorDOMPoint::AtEndOf(*newTextNode)); +} + +static bool TextFragmentBeginsWithStringAtOffset( + const nsTextFragment& aTextFragment, const uint32_t aOffset, + const nsAString& aString) { + const uint32_t stringLength = aString.Length(); + + if (aOffset + stringLength > aTextFragment.GetLength()) { + return false; + } + + if (aTextFragment.Is2b()) { + return aString.Equals(aTextFragment.Get2b() + aOffset); + } + + return aString.EqualsLatin1(aTextFragment.Get1b() + aOffset, stringLength); +} + +static std::tuple<EditorDOMPointInText, EditorDOMPointInText> +AdjustTextInsertionRange(const EditorDOMPointInText& aInsertedPoint, + const nsAString& aInsertedString) { + if (TextFragmentBeginsWithStringAtOffset( + aInsertedPoint.ContainerAs<Text>()->TextFragment(), + aInsertedPoint.Offset(), aInsertedString)) { + return {aInsertedPoint, + EditorDOMPointInText( + aInsertedPoint.ContainerAs<Text>(), + aInsertedPoint.Offset() + aInsertedString.Length())}; + } + + return {EditorDOMPointInText(aInsertedPoint.ContainerAs<Text>(), 0), + EditorDOMPointInText::AtEndOf(*aInsertedPoint.ContainerAs<Text>())}; +} + +std::tuple<EditorDOMPointInText, EditorDOMPointInText> +EditorBase::ComputeInsertedRange(const EditorDOMPointInText& aInsertedPoint, + const nsAString& aInsertedString) const { + MOZ_ASSERT(aInsertedPoint.IsSet()); + + // The DOM was potentially modified during the transaction. This is possible + // through mutation event listeners. That is, the node could've been removed + // from the doc or otherwise modified. + if (!MayHaveMutationEventListeners( + NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED)) { + EditorDOMPointInText endOfInsertion( + aInsertedPoint.ContainerAs<Text>(), + aInsertedPoint.Offset() + aInsertedString.Length()); + return {aInsertedPoint, endOfInsertion}; + } + if (aInsertedPoint.ContainerAs<Text>()->IsInComposedDoc()) { + EditorDOMPointInText begin, end; + return AdjustTextInsertionRange(aInsertedPoint, aInsertedString); + } + return {EditorDOMPointInText(), EditorDOMPointInText()}; +} + +Result<InsertTextResult, nsresult> +EditorBase::InsertTextIntoTextNodeWithTransaction( + const nsAString& aStringToInsert, + const EditorDOMPointInText& aPointToInsert) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + + RefPtr<EditTransactionBase> transaction; + bool isIMETransaction = false; + if (ShouldHandleIMEComposition()) { + transaction = + CompositionTransaction::Create(*this, aStringToInsert, aPointToInsert); + isIMETransaction = true; + } else { + transaction = + InsertTextTransaction::Create(*this, aStringToInsert, aPointToInsert); + } + + // XXX We may not need these view batches anymore. This is handled at a + // higher level now I believe. + BeginUpdateViewBatch(__FUNCTION__); + nsresult rv = DoTransactionInternal(transaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + EndUpdateViewBatch(__FUNCTION__); + + // Don't check whether we've been destroyed here because we need to notify + // listeners and observers below even if we've already destroyed. + + auto pointToInsert = [&]() -> EditorDOMPointInText { + if (!isIMETransaction) { + return aPointToInsert; + } + if (NS_WARN_IF(!mComposition->GetContainerTextNode())) { + return aPointToInsert; + } + return EditorDOMPointInText( + mComposition->GetContainerTextNode(), + std::min(mComposition->XPOffsetInTextNode(), + mComposition->GetContainerTextNode()->TextDataLength())); + }(); + + EditorDOMPointInText endOfInsertedText( + pointToInsert.ContainerAs<Text>(), + pointToInsert.Offset() + aStringToInsert.Length()); + + if (IsHTMLEditor()) { + auto [begin, end] = ComputeInsertedRange(pointToInsert, aStringToInsert); + if (begin.IsSet() && end.IsSet()) { + TopLevelEditSubActionDataRef().DidInsertText( + *this, begin.To<EditorRawDOMPoint>(), end.To<EditorRawDOMPoint>()); + } + if (isIMETransaction) { + // Let's mark the text node as "modified frequently" if it interact with + // IME since non-ASCII character may be inserted into it in most cases. + pointToInsert.ContainerAs<Text>()->MarkAsMaybeModifiedFrequently(); + } + // XXX Should we update endOfInsertedText here? + } + + // let listeners know what happened + if (!mActionListeners.IsEmpty()) { + for (auto& listener : mActionListeners.Clone()) { + // TODO: might need adaptation because of mutation event listeners called + // during `DoTransactionInternal`. + DebugOnly<nsresult> rvIgnored = listener->DidInsertText( + pointToInsert.ContainerAs<Text>(), + static_cast<int32_t>(pointToInsert.Offset()), aStringToInsert, rv); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::DidInsertText() failed, but ignored"); + } + } + + // Added some cruft here for bug 43366. Layout was crashing because we left + // an empty text node lying around in the document. So I delete empty text + // nodes caused by IME. I have to mark the IME transaction as "fixed", which + // means that furure IME txns won't merge with it. This is because we don't + // want future IME txns trying to put their text into a node that is no + // longer in the document. This does not break undo/redo, because all these + // txns are wrapped in a parent PlaceHolder txn, and placeholder txns are + // already savvy to having multiple ime txns inside them. + + // Delete empty IME text node if there is one + if (IsHTMLEditor() && isIMETransaction && mComposition) { + RefPtr<Text> textNode = mComposition->GetContainerTextNode(); + if (textNode && !textNode->Length()) { + rv = DeleteNodeWithTransaction(*textNode); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeTransaction() failed"); + if (MOZ_LIKELY(!textNode->IsInComposedDoc())) { + mComposition->OnTextNodeRemoved(); + } + static_cast<CompositionTransaction*>(transaction.get())->MarkFixed(); + } + } + + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + + InsertTextTransaction* const insertTextTransaction = + transaction->GetAsInsertTextTransaction(); + return insertTextTransaction + ? InsertTextResult(std::move(endOfInsertedText), + insertTextTransaction + ->SuggestPointToPutCaret<EditorDOMPoint>()) + : InsertTextResult(std::move(endOfInsertedText)); +} + +nsresult EditorBase::NotifyDocumentListeners( + TDocumentListenerNotification aNotificationType) { + switch (aNotificationType) { + case eDocumentCreated: + if (IsTextEditor()) { + return NS_OK; + } + if (RefPtr<ComposerCommandsUpdater> composerCommandsUpdate = + AsHTMLEditor()->mComposerCommandsUpdater) { + composerCommandsUpdate->OnHTMLEditorCreated(); + } + return NS_OK; + + case eDocumentToBeDestroyed: { + RefPtr<ComposerCommandsUpdater> composerCommandsUpdate = + IsHTMLEditor() ? AsHTMLEditor()->mComposerCommandsUpdater : nullptr; + if (!mDocStateListeners.Length() && !composerCommandsUpdate) { + return NS_OK; + } + // Needs to store all listeners before notifying ComposerCommandsUpdate + // since notifying it might change mDocStateListeners. + const AutoDocumentStateListenerArray listeners( + mDocStateListeners.Clone()); + if (composerCommandsUpdate) { + composerCommandsUpdate->OnBeforeHTMLEditorDestroyed(); + } + for (auto& listener : listeners) { + // MOZ_KnownLive because 'listeners' is guaranteed to + // keep it alive. + // + // This can go away once + // https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 is fixed. + nsresult rv = MOZ_KnownLive(listener)->NotifyDocumentWillBeDestroyed(); + if (NS_FAILED(rv)) { + NS_WARNING( + "nsIDocumentStateListener::NotifyDocumentWillBeDestroyed() " + "failed"); + return rv; + } + } + return NS_OK; + } + case eDocumentStateChanged: { + bool docIsDirty; + nsresult rv = GetDocumentModified(&docIsDirty); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::GetDocumentModified() failed"); + return rv; + } + + if (static_cast<int8_t>(docIsDirty) == mDocDirtyState) { + return NS_OK; + } + + mDocDirtyState = docIsDirty; + + RefPtr<ComposerCommandsUpdater> composerCommandsUpdate = + IsHTMLEditor() ? AsHTMLEditor()->mComposerCommandsUpdater : nullptr; + if (!mDocStateListeners.Length() && !composerCommandsUpdate) { + return NS_OK; + } + // Needs to store all listeners before notifying ComposerCommandsUpdate + // since notifying it might change mDocStateListeners. + const AutoDocumentStateListenerArray listeners( + mDocStateListeners.Clone()); + if (composerCommandsUpdate) { + composerCommandsUpdate->OnHTMLEditorDirtyStateChanged(mDocDirtyState); + } + for (auto& listener : listeners) { + // MOZ_KnownLive because 'listeners' is guaranteed to + // keep it alive. + // + // This can go away once + // https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 is fixed. + nsresult rv = + MOZ_KnownLive(listener)->NotifyDocumentStateChanged(mDocDirtyState); + if (NS_FAILED(rv)) { + NS_WARNING( + "nsIDocumentStateListener::NotifyDocumentStateChanged() failed"); + return rv; + } + } + return NS_OK; + } + default: + MOZ_ASSERT_UNREACHABLE("Unknown notification"); + return NS_ERROR_FAILURE; + } +} + +nsresult EditorBase::SetTextNodeWithoutTransaction(const nsAString& aString, + Text& aTextNode) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(IsTextEditor()); + MOZ_ASSERT(!IsUndoRedoEnabled()); + + const uint32_t length = aTextNode.Length(); + + // Let listeners know what's up + if (!mActionListeners.IsEmpty() && length) { + for (auto& listener : mActionListeners.Clone()) { + DebugOnly<nsresult> rvIgnored = + listener->WillDeleteText(MOZ_KnownLive(&aTextNode), 0, length); + if (NS_WARN_IF(Destroyed())) { + NS_WARNING( + "nsIEditActionListener::WillDeleteText() failed, but ignored"); + return NS_ERROR_EDITOR_DESTROYED; + } + } + } + + // We don't support undo here, so we don't really need all of the transaction + // machinery, therefore we can run our transaction directly, breaking all of + // the rules! + IgnoredErrorResult error; + DoSetText(aTextNode, aString, error); + if (MOZ_UNLIKELY(error.Failed())) { + NS_WARNING("EditorBase::DoSetText() failed"); + return error.StealNSResult(); + } + + CollapseSelectionTo(EditorRawDOMPoint(&aTextNode, aString.Length()), error); + if (MOZ_UNLIKELY(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + NS_WARNING("EditorBase::CollapseSelection() caused destroying the editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_ASSERTION(!error.Failed(), + "EditorBase::CollapseSelectionTo() failed, but ignored"); + + RangeUpdaterRef().SelAdjReplaceText(aTextNode, 0, length, aString.Length()); + + // Let listeners know what happened + if (!mActionListeners.IsEmpty() && !aString.IsEmpty()) { + for (auto& listener : mActionListeners.Clone()) { + DebugOnly<nsresult> rvIgnored = + listener->DidInsertText(&aTextNode, 0, aString, NS_OK); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::DidInsertText() failed, but ignored"); + } + } + + return NS_OK; +} + +Result<CaretPoint, nsresult> EditorBase::DeleteTextWithTransaction( + Text& aTextNode, uint32_t aOffset, uint32_t aLength) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + RefPtr<DeleteTextTransaction> transaction = + DeleteTextTransaction::MaybeCreate(*this, aTextNode, aOffset, aLength); + if (MOZ_UNLIKELY(!transaction)) { + NS_WARNING("DeleteTextTransaction::MaybeCreate() failed"); + return Err(NS_ERROR_FAILURE); + } + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteText, nsIEditor::ePrevious, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return Err(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + // Let listeners know what's up + if (!mActionListeners.IsEmpty()) { + for (auto& listener : mActionListeners.Clone()) { + DebugOnly<nsresult> rvIgnored = + listener->WillDeleteText(&aTextNode, aOffset, aLength); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::WillDeleteText() failed, but ignored"); + } + } + + nsresult rv = DoTransactionInternal(transaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + + if (IsHTMLEditor()) { + TopLevelEditSubActionDataRef().DidDeleteText( + *this, EditorRawDOMPoint(&aTextNode, aOffset)); + } + + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + return Err(rv); + } + + return CaretPoint(transaction->SuggestPointToPutCaret()); +} + +bool EditorBase::IsRoot(const nsINode* inNode) const { + if (NS_WARN_IF(!inNode)) { + return false; + } + nsINode* rootNode = GetRoot(); + return inNode == rootNode; +} + +bool EditorBase::IsDescendantOfRoot(const nsINode* inNode) const { + if (NS_WARN_IF(!inNode)) { + return false; + } + nsIContent* root = GetRoot(); + if (NS_WARN_IF(!root)) { + return false; + } + + return inNode->IsInclusiveDescendantOf(root); +} + +NS_IMETHODIMP EditorBase::IncrementModificationCount(int32_t inNumMods) { + uint32_t oldModCount = mModCount; + + mModCount += inNumMods; + + if ((!oldModCount && mModCount) || (oldModCount && !mModCount)) { + DebugOnly<nsresult> rvIgnored = + NotifyDocumentListeners(eDocumentStateChanged); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::NotifyDocumentListeners() failed, but ignored"); + } + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetModificationCount(int32_t* aOutModCount) { + if (NS_WARN_IF(!aOutModCount)) { + return NS_ERROR_INVALID_ARG; + } + *aOutModCount = mModCount; + return NS_OK; +} + +NS_IMETHODIMP EditorBase::ResetModificationCount() { + bool doNotify = (mModCount != 0); + + mModCount = 0; + + if (!doNotify) { + return NS_OK; + } + + DebugOnly<nsresult> rvIgnored = + NotifyDocumentListeners(eDocumentStateChanged); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::NotifyDocumentListeners() failed, but ignored"); + return NS_OK; +} + +template <typename EditorDOMPointType> +EditorDOMPointType EditorBase::GetFirstSelectionStartPoint() const { + MOZ_ASSERT(IsEditActionDataAvailable()); + if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) { + return EditorDOMPointType(); + } + + const nsRange* range = SelectionRef().GetRangeAt(0); + if (MOZ_UNLIKELY(NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned()))) { + return EditorDOMPointType(); + } + + return EditorDOMPointType(range->StartRef()); +} + +template <typename EditorDOMPointType> +EditorDOMPointType EditorBase::GetFirstSelectionEndPoint() const { + MOZ_ASSERT(IsEditActionDataAvailable()); + if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) { + return EditorDOMPointType(); + } + + const nsRange* range = SelectionRef().GetRangeAt(0); + if (MOZ_UNLIKELY(NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned()))) { + return EditorDOMPointType(); + } + + return EditorDOMPointType(range->EndRef()); +} + +// static +nsresult EditorBase::GetEndChildNode(const Selection& aSelection, + nsIContent** aEndNode) { + MOZ_ASSERT(aEndNode); + + *aEndNode = nullptr; + + if (NS_WARN_IF(!aSelection.RangeCount())) { + return NS_ERROR_FAILURE; + } + + const nsRange* range = aSelection.GetRangeAt(0); + if (NS_WARN_IF(!range)) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!range->IsPositioned())) { + return NS_ERROR_FAILURE; + } + + NS_IF_ADDREF(*aEndNode = range->GetChildAtEndOffset()); + return NS_OK; +} + +nsresult EditorBase::EnsurePaddingBRElementInMultilineEditor() { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(IsInPlaintextMode()); + MOZ_ASSERT(!IsSingleLineEditor()); + + Element* anonymousDivOrBodyElement = GetRoot(); + if (NS_WARN_IF(!anonymousDivOrBodyElement)) { + return NS_ERROR_FAILURE; + } + + // Assuming EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() has been + // called first. + // XXX This assumption is wrong. This method may be called alone. Actually, + // we see this warning in mochitest log. So, we should fix this bug + // later. + if (NS_WARN_IF(!anonymousDivOrBodyElement->GetLastChild())) { + return NS_ERROR_FAILURE; + } + + RefPtr<HTMLBRElement> brElement = + HTMLBRElement::FromNode(anonymousDivOrBodyElement->GetLastChild()); + if (!brElement) { + // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary + // in normal cases. However, it may be required for nested edit + // actions which may be caused by legacy mutation event listeners or + // chrome script. + AutoTransactionsConserveSelection dontChangeMySelection(*this); + EditorDOMPoint endOfAnonymousDiv( + EditorDOMPoint::AtEndOf(*anonymousDivOrBodyElement)); + Result<CreateElementResult, nsresult> insertPaddingBRElementResult = + InsertPaddingBRElementForEmptyLastLineWithTransaction( + endOfAnonymousDiv); + if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) { + NS_WARNING( + "EditorBase::InsertPaddingBRElementForEmptyLastLineWithTransaction() " + "failed"); + return insertPaddingBRElementResult.unwrapErr(); + } + insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion(); + return NS_OK; + } + + // Check to see if the trailing BR is a former padding <br> element for empty + // editor - this will have stuck around if we previously morphed a trailing + // node into a padding <br> element. + if (!brElement->IsPaddingForEmptyEditor()) { + return NS_OK; + } + + // Morph it back to a padding <br> element for empty last line. + brElement->UnsetFlags(NS_PADDING_FOR_EMPTY_EDITOR); + brElement->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE); + + return NS_OK; +} + +void EditorBase::BeginUpdateViewBatch(const char* aRequesterFuncName) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(mUpdateCount >= 0, "bad state"); + + if (!mUpdateCount) { + // Turn off selection updates and notifications. + SelectionRef().StartBatchChanges(aRequesterFuncName); + } + + mUpdateCount++; +} + +void EditorBase::EndUpdateViewBatch(const char* aRequesterFuncName) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(mUpdateCount > 0, "bad state"); + + if (NS_WARN_IF(mUpdateCount <= 0)) { + mUpdateCount = 0; + return; + } + + if (--mUpdateCount) { + return; + } + + // Turn selection updating and notifications back on. + SelectionRef().EndBatchChanges(aRequesterFuncName); +} + +TextComposition* EditorBase::GetComposition() const { return mComposition; } + +template <typename EditorDOMPointType> +EditorDOMPointType EditorBase::GetFirstIMESelectionStartPoint() const { + return mComposition + ? EditorDOMPointType(mComposition->FirstIMESelectionStartRef()) + : EditorDOMPointType(); +} + +template <typename EditorDOMPointType> +EditorDOMPointType EditorBase::GetLastIMESelectionEndPoint() const { + return mComposition + ? EditorDOMPointType(mComposition->LastIMESelectionEndRef()) + : EditorDOMPointType(); +} + +bool EditorBase::IsIMEComposing() const { + return mComposition && mComposition->IsComposing(); +} + +bool EditorBase::ShouldHandleIMEComposition() const { + // When the editor is being reframed, the old value may be restored with + // InsertText(). In this time, the text should be inserted as not a part + // of the composition. + return mComposition && mDidPostCreate; +} + +bool EditorBase::EnsureComposition(WidgetCompositionEvent& aCompositionEvent) { + if (mComposition) { + return true; + } + // The compositionstart event must cause creating new TextComposition + // instance at being dispatched by IMEStateManager. + mComposition = IMEStateManager::GetTextCompositionFor(&aCompositionEvent); + if (!mComposition) { + // However, TextComposition may be committed before the composition + // event comes here. + return false; + } + mComposition->StartHandlingComposition(this); + return true; +} + +nsresult EditorBase::OnCompositionStart( + WidgetCompositionEvent& aCompositionStartEvent) { + if (mComposition) { + NS_WARNING("There was a composition at receiving compositionstart event"); + return NS_OK; + } + + // "beforeinput" event shouldn't be fired before "compositionstart". + AutoEditActionDataSetter editActionData(*this, EditAction::eStartComposition); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + EnsureComposition(aCompositionStartEvent); + NS_WARNING_ASSERTION(mComposition, "Failed to get TextComposition instance?"); + return NS_OK; +} + +nsresult EditorBase::OnCompositionChange( + WidgetCompositionEvent& aCompositionChangeEvent) { + MOZ_ASSERT(aCompositionChangeEvent.mMessage == eCompositionChange, + "The event should be eCompositionChange"); + + if (!mComposition) { + NS_WARNING( + "There is no composition, but receiving compositionchange event"); + return NS_ERROR_FAILURE; + } + + AutoEditActionDataSetter editActionData(*this, + EditAction::eUpdateComposition); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + // If: + // - new composition string is not empty, + // - there is no composition string in the DOM tree, + // - and there is non-collapsed Selection, + // the selected content will be removed by this composition. + if (aCompositionChangeEvent.mData.IsEmpty() && + mComposition->String().IsEmpty() && !SelectionRef().IsCollapsed()) { + editActionData.UpdateEditAction(EditAction::eDeleteByComposition); + } + + // If Input Events Level 2 is enabled, EditAction::eDeleteByComposition is + // mapped to EditorInputType::eDeleteByComposition and it requires null + // for InputEvent.data. Therefore, only otherwise, we should set data. + if (ToInputType(editActionData.GetEditAction()) != + EditorInputType::eDeleteByComposition) { + MOZ_ASSERT(ToInputType(editActionData.GetEditAction()) == + EditorInputType::eInsertCompositionText); + MOZ_ASSERT(!aCompositionChangeEvent.mData.IsVoid()); + editActionData.SetData(aCompositionChangeEvent.mData); + } + + // If we're an `HTMLEditor` and this is second or later composition change, + // we should set target range to the range of composition string. + // Otherwise, set target ranges to selection ranges (will be done by + // editActionData itself before dispatching `beforeinput` event). + if (IsHTMLEditor() && mComposition->GetContainerTextNode()) { + RefPtr<StaticRange> targetRange = StaticRange::Create( + mComposition->GetContainerTextNode(), + mComposition->XPOffsetInTextNode(), + mComposition->GetContainerTextNode(), + mComposition->XPEndOffsetInTextNode(), IgnoreErrors()); + NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(), + "StaticRange::Create() failed"); + if (targetRange && targetRange->IsPositioned()) { + editActionData.AppendTargetRange(*targetRange); + } + } + + // TODO: We need to use different EditAction value for beforeinput event + // if the event is followed by "compositionend" because corresponding + // "input" event will be fired from OnCompositionEnd() later with + // different EditAction value. + // TODO: If Input Events Level 2 is enabled, "beforeinput" event may be + // actually canceled if edit action is eDeleteByComposition. In such + // case, we might need to keep selected text, but insert composition + // string before or after the selection. However, the spec is still + // unstable. We should keep handling the composition since other + // parts including widget may not be ready for such complicated + // behavior. + nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); + if (rv != NS_ERROR_EDITOR_ACTION_CANCELED && NS_FAILED(rv)) { + NS_WARNING("MaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + if (!EnsureComposition(aCompositionChangeEvent)) { + NS_WARNING("EditorBase::EnsureComposition() failed"); + return NS_OK; + } + + if (NS_WARN_IF(!GetPresShell())) { + return NS_ERROR_NOT_INITIALIZED; + } + + // NOTE: TextComposition should receive selection change notification before + // CompositionChangeEventHandlingMarker notifies TextComposition of the + // end of handling compositionchange event because TextComposition may + // need to ignore selection changes caused by composition. Therefore, + // CompositionChangeEventHandlingMarker must be destroyed after a call + // of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or + // NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies + // TextComposition of a selection change. + MOZ_ASSERT( + !mPlaceholderBatch, + "UpdateIMEComposition() must be called without place holder batch"); + nsString data(aCompositionChangeEvent.mData); + if (IsHTMLEditor()) { + nsContentUtils::PlatformToDOMLineBreaks(data); + } + + { + // This needs to be destroyed before dispatching "input" event from + // the following call of `NotifyEditorObservers`. Therefore, we need to + // put this in this block rather than outside of this. + const bool wasComposing = mComposition->IsComposing(); + TextComposition::CompositionChangeEventHandlingMarker + compositionChangeEventHandlingMarker(mComposition, + &aCompositionChangeEvent); + AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::IMETxnName, + ScrollSelectionIntoView::Yes, + __FUNCTION__); + + // XXX Why don't we get caret after the DOM mutation? + RefPtr<nsCaret> caret = GetCaret(); + + MOZ_ASSERT( + mIsInEditSubAction, + "AutoPlaceholderBatch should've notified the observes of before-edit"); + // If we're updating composition, we need to ignore normal selection + // which may be updated by the web content. + rv = InsertTextAsSubAction(data, wasComposing ? SelectionHandling::Ignore + : SelectionHandling::Delete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::InsertTextAsSubAction() failed"); + + if (caret) { + caret->SetSelection(&SelectionRef()); + } + } + + // If still composing, we should fire input event via observer. + // Note that if the composition will be committed by the following + // compositionend event, we don't need to notify editor observes of this + // change. + // NOTE: We must notify after the auto batch will be gone. + if (!aCompositionChangeEvent.IsFollowedByCompositionEnd()) { + // If we're a TextEditor, we'll be initialized with a new anonymous subtree, + // which can be caused by reframing from a "input" event listener. At that + // time, we'll move composition from current text node to the new text node + // with using mComposition's data. Therefore, it's important that + // mComposition already has the latest information here. + MOZ_ASSERT_IF(mComposition, mComposition->String() == data); + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + } + + return EditorBase::ToGenericNSResult(rv); +} + +void EditorBase::OnCompositionEnd( + WidgetCompositionEvent& aCompositionEndEvent) { + if (!mComposition) { + NS_WARNING("There is no composition, but receiving compositionend event"); + return; + } + + EditAction editAction = aCompositionEndEvent.mData.IsEmpty() + ? EditAction::eCancelComposition + : EditAction::eCommitComposition; + AutoEditActionDataSetter editActionData(*this, editAction); + // If Input Events Level 2 is enabled, EditAction::eCancelComposition is + // mapped to EditorInputType::eDeleteCompositionText and it requires null + // for InputEvent.data. Therefore, only otherwise, we should set data. + if (ToInputType(editAction) != EditorInputType::eDeleteCompositionText) { + MOZ_ASSERT( + ToInputType(editAction) == EditorInputType::eInsertCompositionText || + ToInputType(editAction) == EditorInputType::eInsertFromComposition); + MOZ_ASSERT(!aCompositionEndEvent.mData.IsVoid()); + editActionData.SetData(aCompositionEndEvent.mData); + } + + // commit the IME transaction..we can get at it via the transaction mgr. + // Note that this means IME won't work without an undo stack! + if (mTransactionManager) { + if (nsCOMPtr<nsITransaction> transaction = + mTransactionManager->PeekUndoStack()) { + if (RefPtr<EditTransactionBase> transactionBase = + transaction->GetAsEditTransactionBase()) { + if (PlaceholderTransaction* placeholderTransaction = + transactionBase->GetAsPlaceholderTransaction()) { + placeholderTransaction->Commit(); + } + } + } + } + + // Note that this just marks as that we've already handled "beforeinput" for + // preventing assertions in FireInputEvent(). Note that corresponding + // "beforeinput" event for the following "input" event should've already + // been dispatched from `OnCompositionChange()`. + DebugOnly<nsresult> rvIgnored = + editActionData.MaybeDispatchBeforeInputEvent(); + MOZ_ASSERT(rvIgnored != NS_ERROR_EDITOR_ACTION_CANCELED, + "Why beforeinput event was canceled in this case?"); + MOZ_ASSERT(NS_SUCCEEDED(rvIgnored), + "MaybeDispatchBeforeInputEvent() should just mark the instance as " + "handled it"); + + // Composition string may have hidden the caret. Therefore, we need to + // cancel it here. + HideCaret(false); + + // FYI: mComposition still keeps storing container text node of committed + // string, its offset and length. However, they will be invalidated + // soon since its Destroy() will be called by IMEStateManager. + mComposition->EndHandlingComposition(this); + mComposition = nullptr; + + // notify editor observers of action + // FYI: With current draft, "input" event should be fired from + // OnCompositionChange(), however, it requires a lot of our UI code + // change and does not make sense. See spec issue: + // https://github.com/w3c/uievents/issues/202 + NotifyEditorObservers(eNotifyEditorObserversOfEnd); +} + +void EditorBase::DoAfterDoTransaction(nsITransaction* aTransaction) { + bool isTransientTransaction; + MOZ_ALWAYS_SUCCEEDS(aTransaction->GetIsTransient(&isTransientTransaction)); + + if (!isTransientTransaction) { + // we need to deal here with the case where the user saved after some + // edits, then undid one or more times. Then, the undo count is -ve, + // but we can't let a do take it back to zero. So we flip it up to + // a +ve number. + int32_t modCount; + DebugOnly<nsresult> rvIgnored = GetModificationCount(&modCount); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::GetModificationCount() failed, but ignored"); + if (modCount < 0) { + modCount = -modCount; + } + + // don't count transient transactions + MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1)); + } +} + +void EditorBase::DoAfterUndoTransaction() { + // all undoable transactions are non-transient + MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(-1)); +} + +void EditorBase::DoAfterRedoTransaction() { + // all redoable transactions are non-transient + MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1)); +} + +already_AddRefed<DeleteMultipleRangesTransaction> +EditorBase::CreateTransactionForDeleteSelection( + HowToHandleCollapsedRange aHowToHandleCollapsedRange, + const AutoRangeArray& aRangesToDelete) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); + + // Check whether the selection is collapsed and we should do nothing: + if (NS_WARN_IF(aRangesToDelete.IsCollapsed() && + aHowToHandleCollapsedRange == + HowToHandleCollapsedRange::Ignore)) { + return nullptr; + } + + // allocate the out-param transaction + RefPtr<DeleteMultipleRangesTransaction> transaction = + DeleteMultipleRangesTransaction::Create(); + for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) { + // Same with range as with selection; if it is collapsed and action + // is eNone, do nothing. + if (!range->Collapsed()) { + RefPtr<DeleteRangeTransaction> deleteRangeTransaction = + DeleteRangeTransaction::Create(*this, range); + // XXX Oh, not checking if deleteRangeTransaction can modify the range... + transaction->AppendChild(*deleteRangeTransaction); + continue; + } + + if (aHowToHandleCollapsedRange == HowToHandleCollapsedRange::Ignore) { + continue; + } + + // Let's extend the collapsed range to delete content around it. + RefPtr<DeleteContentTransactionBase> deleteNodeOrTextTransaction = + CreateTransactionForCollapsedRange(range, aHowToHandleCollapsedRange); + // XXX When there are two or more ranges and at least one of them is + // not editable, deleteNodeOrTextTransaction may be nullptr. + // In such case, should we stop removing other ranges too? + if (!deleteNodeOrTextTransaction) { + NS_WARNING("EditorBase::CreateTransactionForCollapsedRange() failed"); + return nullptr; + } + transaction->AppendChild(*deleteNodeOrTextTransaction); + } + + return transaction.forget(); +} + +// XXX: currently, this doesn't handle edge conditions because GetNext/GetPrior +// are not implemented +already_AddRefed<DeleteContentTransactionBase> +EditorBase::CreateTransactionForCollapsedRange( + const nsRange& aCollapsedRange, + HowToHandleCollapsedRange aHowToHandleCollapsedRange) { + MOZ_ASSERT(aCollapsedRange.Collapsed()); + MOZ_ASSERT( + aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward || + aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendForward); + + EditorRawDOMPoint point(aCollapsedRange.StartRef()); + if (NS_WARN_IF(!point.IsSet())) { + return nullptr; + } + if (IsTextEditor()) { + // There should be only one text node in the anonymous `<div>` (but may + // be followed by a padding `<br>`). We should adjust the point into + // the text node (or return nullptr if there is no text to delete) for + // avoiding finding the text node with complicated API. + if (!point.IsInTextNode()) { + const Element* anonymousDiv = GetRoot(); + if (NS_WARN_IF(!anonymousDiv)) { + return nullptr; + } + if (!anonymousDiv->GetFirstChild() || + !anonymousDiv->GetFirstChild()->IsText()) { + return nullptr; // The value is empty. + } + if (point.GetContainer() == anonymousDiv) { + if (point.IsStartOfContainer()) { + point.Set(anonymousDiv->GetFirstChild(), 0); + } else { + point.SetToEndOf(anonymousDiv->GetFirstChild()); + } + } else { + // Must be referring a padding `<br>` element or after the text node. + point.SetToEndOf(anonymousDiv->GetFirstChild()); + } + } + MOZ_ASSERT(!point.ContainerAs<Text>()->GetPreviousSibling()); + MOZ_ASSERT(!point.ContainerAs<Text>()->GetNextSibling() || + !point.ContainerAs<Text>()->GetNextSibling()->IsText()); + if (aHowToHandleCollapsedRange == + HowToHandleCollapsedRange::ExtendBackward && + point.IsStartOfContainer()) { + return nullptr; + } + if (aHowToHandleCollapsedRange == + HowToHandleCollapsedRange::ExtendForward && + point.IsEndOfContainer()) { + return nullptr; + } + } + + // XXX: if the container of point is empty, then we'll need to delete the node + // as well as the 1 child + + // build a transaction for deleting the appropriate data + // XXX: this has to come from rule section + const Element* const anonymousDivOrEditingHost = + IsTextEditor() ? GetRoot() : AsHTMLEditor()->ComputeEditingHost(); + if (aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward && + point.IsStartOfContainer()) { + MOZ_ASSERT(IsHTMLEditor()); + // We're backspacing from the beginning of a node. Delete the last thing + // of previous editable content. + nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent( + *point.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, + anonymousDivOrEditingHost); + if (!previousEditableContent) { + NS_WARNING("There was no editable content before the collapsed range"); + return nullptr; + } + + // There is an editable content, so delete its last child (if a text node, + // delete the last char). If it has no children, delete it. + if (previousEditableContent->IsText()) { + uint32_t length = previousEditableContent->Length(); + // Bail out for empty text node. + // XXX Do we want to do something else? + // XXX If other browsers delete empty text node, we should follow it. + if (NS_WARN_IF(!length)) { + NS_WARNING("Previous editable content was an empty text node"); + return nullptr; + } + RefPtr<DeleteTextTransaction> deleteTextTransaction = + DeleteTextTransaction::MaybeCreateForPreviousCharacter( + *this, *previousEditableContent->AsText(), length); + if (!deleteTextTransaction) { + NS_WARNING( + "DeleteTextTransaction::MaybeCreateForPreviousCharacter() failed"); + return nullptr; + } + return deleteTextTransaction.forget(); + } + + if (IsHTMLEditor() && + NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*previousEditableContent))) { + return nullptr; + } + RefPtr<DeleteNodeTransaction> deleteNodeTransaction = + DeleteNodeTransaction::MaybeCreate(*this, *previousEditableContent); + if (!deleteNodeTransaction) { + NS_WARNING("DeleteNodeTransaction::MaybeCreate() failed"); + return nullptr; + } + return deleteNodeTransaction.forget(); + } + + if (aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendForward && + point.IsEndOfContainer()) { + MOZ_ASSERT(IsHTMLEditor()); + // We're deleting from the end of a node. Delete the first thing of + // next editable content. + nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent( + *point.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, + anonymousDivOrEditingHost); + if (!nextEditableContent) { + NS_WARNING("There was no editable content after the collapsed range"); + return nullptr; + } + + // There is an editable content, so delete its first child (if a text node, + // delete the first char). If it has no children, delete it. + if (nextEditableContent->IsText()) { + uint32_t length = nextEditableContent->Length(); + // Bail out for empty text node. + // XXX Do we want to do something else? + // XXX If other browsers delete empty text node, we should follow it. + if (!length) { + NS_WARNING("Next editable content was an empty text node"); + return nullptr; + } + RefPtr<DeleteTextTransaction> deleteTextTransaction = + DeleteTextTransaction::MaybeCreateForNextCharacter( + *this, *nextEditableContent->AsText(), 0); + if (!deleteTextTransaction) { + NS_WARNING( + "DeleteTextTransaction::MaybeCreateForNextCharacter() failed"); + return nullptr; + } + return deleteTextTransaction.forget(); + } + + if (IsHTMLEditor() && + NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*nextEditableContent))) { + return nullptr; + } + RefPtr<DeleteNodeTransaction> deleteNodeTransaction = + DeleteNodeTransaction::MaybeCreate(*this, *nextEditableContent); + if (!deleteNodeTransaction) { + NS_WARNING("DeleteNodeTransaction::MaybeCreate() failed"); + return nullptr; + } + return deleteNodeTransaction.forget(); + } + + if (point.IsInTextNode()) { + if (aHowToHandleCollapsedRange == + HowToHandleCollapsedRange::ExtendBackward) { + RefPtr<DeleteTextTransaction> deleteTextTransaction = + DeleteTextTransaction::MaybeCreateForPreviousCharacter( + *this, *point.ContainerAs<Text>(), point.Offset()); + NS_WARNING_ASSERTION( + deleteTextTransaction, + "DeleteTextTransaction::MaybeCreateForPreviousCharacter() failed"); + return deleteTextTransaction.forget(); + } + RefPtr<DeleteTextTransaction> deleteTextTransaction = + DeleteTextTransaction::MaybeCreateForNextCharacter( + *this, *point.ContainerAs<Text>(), point.Offset()); + NS_WARNING_ASSERTION( + deleteTextTransaction, + "DeleteTextTransaction::MaybeCreateForNextCharacter() failed"); + return deleteTextTransaction.forget(); + } + + nsIContent* editableContent = nullptr; + if (IsHTMLEditor()) { + editableContent = + aHowToHandleCollapsedRange == HowToHandleCollapsedRange::ExtendBackward + ? HTMLEditUtils::GetPreviousContent( + point, {WalkTreeOption::IgnoreNonEditableNode}, + anonymousDivOrEditingHost) + : HTMLEditUtils::GetNextContent( + point, {WalkTreeOption::IgnoreNonEditableNode}, + anonymousDivOrEditingHost); + if (!editableContent) { + NS_WARNING("There was no editable content around the collapsed range"); + return nullptr; + } + while (editableContent && editableContent->IsCharacterData() && + !editableContent->Length()) { + // Can't delete an empty text node (bug 762183) + editableContent = + aHowToHandleCollapsedRange == + HowToHandleCollapsedRange::ExtendBackward + ? HTMLEditUtils::GetPreviousContent( + *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, + anonymousDivOrEditingHost) + : HTMLEditUtils::GetNextContent( + *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, + anonymousDivOrEditingHost); + } + if (!editableContent) { + NS_WARNING( + "There was no editable content which is not empty around the " + "collapsed range"); + return nullptr; + } + } else { + MOZ_ASSERT(point.IsInTextNode()); + editableContent = point.GetContainerAs<nsIContent>(); + if (!editableContent) { + NS_WARNING("If there was no text node, should've been handled first"); + return nullptr; + } + } + + if (editableContent->IsText()) { + if (aHowToHandleCollapsedRange == + HowToHandleCollapsedRange::ExtendBackward) { + RefPtr<DeleteTextTransaction> deleteTextTransaction = + DeleteTextTransaction::MaybeCreateForPreviousCharacter( + *this, *editableContent->AsText(), editableContent->Length()); + NS_WARNING_ASSERTION( + deleteTextTransaction, + "DeleteTextTransaction::MaybeCreateForPreviousCharacter() failed"); + return deleteTextTransaction.forget(); + } + + RefPtr<DeleteTextTransaction> deleteTextTransaction = + DeleteTextTransaction::MaybeCreateForNextCharacter( + *this, *editableContent->AsText(), 0); + NS_WARNING_ASSERTION( + deleteTextTransaction, + "DeleteTextTransaction::MaybeCreateForNextCharacter() failed"); + return deleteTextTransaction.forget(); + } + + MOZ_ASSERT(IsHTMLEditor()); + if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*editableContent))) { + return nullptr; + } + RefPtr<DeleteNodeTransaction> deleteNodeTransaction = + DeleteNodeTransaction::MaybeCreate(*this, *editableContent); + NS_WARNING_ASSERTION(deleteNodeTransaction, + "DeleteNodeTransaction::MaybeCreate() failed"); + return deleteNodeTransaction.forget(); +} + +bool EditorBase::FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + nsIEditor::EDirection aDirectionAndAmount) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(Destroyed())) { + return false; + } + if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection( + aDirectionAndAmount, SelectionRef())) { + return true; + } + // Although AutoRangeArray::ExtendAnchorFocusRangeFor() will use + // nsFrameSelection, if it still has dirty frame, nsFrameSelection doesn't + // extend selection since we block script. + if (RefPtr<PresShell> presShell = GetPresShell()) { + presShell->FlushPendingNotifications(FlushType::Layout); + if (NS_WARN_IF(Destroyed())) { + return false; + } + } + return true; +} + +nsresult EditorBase::DeleteSelectionAsAction( + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + // Showing this assertion is fine if this method is called by outside via + // mutation event listener or something. Otherwise, this is called by + // wrong method. + NS_ASSERTION( + !mPlaceholderBatch, + "Should be called only when this is the only edit action of the " + "operation unless mutation event listener nests some operations"); + + // If we're a TextEditor instance, we don't need to treat parent elements + // so that we can ignore aStripWrappers for skipping unnecessary cost. + if (IsTextEditor()) { + aStripWrappers = nsIEditor::eNoStrip; + } + + EditAction editAction = EditAction::eDeleteSelection; + switch (aDirectionAndAmount) { + case nsIEditor::ePrevious: + editAction = EditAction::eDeleteBackward; + break; + case nsIEditor::eNext: + editAction = EditAction::eDeleteForward; + break; + case nsIEditor::ePreviousWord: + editAction = EditAction::eDeleteWordBackward; + break; + case nsIEditor::eNextWord: + editAction = EditAction::eDeleteWordForward; + break; + case nsIEditor::eToBeginningOfLine: + editAction = EditAction::eDeleteToBeginningOfSoftLine; + break; + case nsIEditor::eToEndOfLine: + editAction = EditAction::eDeleteToEndOfSoftLine; + break; + } + + AutoEditActionDataSetter editActionData(*this, editAction, aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + // If there is an existing selection when an extended delete is requested, + // platforms that use "caret-style" caret positioning collapse the + // selection to the start and then create a new selection. + // Platforms that use "selection-style" caret positioning just delete the + // existing selection without extending it. + if (!SelectionRef().IsCollapsed()) { + switch (aDirectionAndAmount) { + case eNextWord: + case ePreviousWord: + case eToBeginningOfLine: + case eToEndOfLine: { + if (mCaretStyle != 1) { + aDirectionAndAmount = eNone; + break; + } + ErrorResult error; + SelectionRef().CollapseToStart(error); + if (NS_WARN_IF(Destroyed())) { + error.SuppressException(); + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + if (error.Failed()) { + NS_WARNING("Selection::CollapseToStart() failed"); + editActionData.Abort(); + return EditorBase::ToGenericNSResult(error.StealNSResult()); + } + break; + } + default: + break; + } + } + + // If Selection is still NOT collapsed, it does not important removing + // range of the operation since we'll remove the selected content. However, + // information of direction (backward or forward) may be important for + // web apps. E.g., web apps may want to mark selected range as "deleted" + // and move caret before or after the range. Therefore, we should forget + // only the range information but keep range information. See discussion + // of the spec issue for the detail: + // https://github.com/w3c/input-events/issues/82 + if (!SelectionRef().IsCollapsed()) { + switch (editAction) { + case EditAction::eDeleteWordBackward: + case EditAction::eDeleteToBeginningOfSoftLine: + editActionData.UpdateEditAction(EditAction::eDeleteBackward); + break; + case EditAction::eDeleteWordForward: + case EditAction::eDeleteToEndOfSoftLine: + editActionData.UpdateEditAction(EditAction::eDeleteForward); + break; + default: + break; + } + } + + editActionData.SetSelectionCreatedByDoubleclick( + SelectionRef().GetFrameSelection() && + SelectionRef().GetFrameSelection()->IsDoubleClickSelection()); + + if (!FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + aDirectionAndAmount)) { + NS_WARNING("Flusing pending notifications caused destroying the editor"); + editActionData.Abort(); + return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); + } + + nsresult rv = + editActionData.MaybeDispatchBeforeInputEvent(aDirectionAndAmount); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "MaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // delete placeholder txns merge. + AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::DeleteTxnName, + ScrollSelectionIntoView::Yes, + __FUNCTION__); + rv = DeleteSelectionAsSubAction(aDirectionAndAmount, aStripWrappers); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteSelectionAsSubAction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::DeleteSelectionAsSubAction( + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers) { + MOZ_ASSERT(IsEditActionDataAvailable()); + // If handling edit action is for table editing, this may be called with + // selecting an any table element by the caller, but it's not usual work of + // this so that `MayEditActionDeleteSelection()` returns false. + MOZ_ASSERT(MayEditActionDeleteSelection(GetEditAction()) || + IsEditActionTableEditing(GetEditAction())); + MOZ_ASSERT(mPlaceholderBatch); + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + NS_ASSERTION(IsHTMLEditor() || aStripWrappers == nsIEditor::eNoStrip, + "TextEditor does not support strip wrappers"); + + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteSelectedContent, aDirectionAndAmount, + ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + { + Result<EditActionResult, nsresult> result = + HandleDeleteSelection(aDirectionAndAmount, aStripWrappers); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("TextEditor::HandleDeleteSelection() failed"); + return result.unwrapErr(); + } + if (result.inspect().Canceled()) { + return NS_OK; + } + } + + // XXX This is odd. We just tries to remove empty text node here but we + // refer `Selection`. It may be modified by mutation event listeners + // so that we should remove the empty text node when we make it empty. + const auto atNewStartOfSelection = + GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!atNewStartOfSelection.IsSet())) { + // XXX And also it seems that we don't need to return error here. + // Why don't we just ignore? `Selection::RemoveAllRanges()` may + // have been called by mutation event listeners. + return NS_ERROR_FAILURE; + } + if (IsHTMLEditor() && atNewStartOfSelection.IsInTextNode() && + !atNewStartOfSelection.GetContainer()->Length()) { + nsresult rv = DeleteNodeWithTransaction( + MOZ_KnownLive(*atNewStartOfSelection.ContainerAs<Text>())); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } + + // XXX I don't think that this is necessary in anonymous `<div>` element of + // TextEditor since there should be at most one text node and at most + // one padding `<br>` element so that `<br>` element won't be before + // caret. + if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine) { + // We prevent the caret from sticking on the left of previous `<br>` + // element (i.e. the end of previous line) after this deletion. Bug 92124. + if (MOZ_UNLIKELY(NS_FAILED(SelectionRef().SetInterlinePosition( + InterlinePosition::StartOfNextLine)))) { + NS_WARNING( + "Selection::SetInterlinePosition(InterlinePosition::StartOfNextLine) " + "failed"); + return NS_ERROR_FAILURE; // Don't need to return NS_ERROR_NOT_INITIALIZED + } + } + + return NS_OK; +} + +nsresult EditorBase::HandleDropEvent(DragEvent* aDropEvent) { + if (NS_WARN_IF(!aDropEvent)) { + return NS_ERROR_INVALID_ARG; + } + + DebugOnly<nsresult> rvIgnored = CommitComposition(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "EditorBase::CommitComposition() failed, but ignored"); + + AutoEditActionDataSetter editActionData(*this, EditAction::eDrop); + // We need to initialize data or dataTransfer later. Therefore, we cannot + // dispatch "beforeinput" event until then. + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<DataTransfer> dataTransfer = aDropEvent->GetDataTransfer(); + if (NS_WARN_IF(!dataTransfer)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (NS_WARN_IF(!dragSession)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsINode> sourceNode = dataTransfer->GetMozSourceNode(); + + // If there is no source document, then the drag was from another application + // or another process (such as an out of process subframe). The latter case is + // not currently handled below when checking for a move/copy and deleting the + // existing text. + RefPtr<Document> srcdoc; + if (sourceNode) { + srcdoc = sourceNode->OwnerDoc(); + } + + nsCOMPtr<nsIPrincipal> sourcePrincipal; + dragSession->GetTriggeringPrincipal(getter_AddRefs(sourcePrincipal)); + + if (nsContentUtils::CheckForSubFrameDrop( + dragSession, aDropEvent->WidgetEventPtr()->AsDragEvent())) { + // Don't allow drags from subframe documents with different origins than + // the drop destination. + if (IsSafeToInsertData(sourcePrincipal) == SafeToInsertData::No) { + return NS_OK; + } + } + + // Current doc is destination + RefPtr<Document> document = GetDocument(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_NOT_INITIALIZED; + } + + const uint32_t numItems = dataTransfer->MozItemCount(); + if (NS_WARN_IF(!numItems)) { + return NS_ERROR_FAILURE; // Nothing to drop? + } + + // We have to figure out whether to delete and relocate caret only once + // Parent and offset are under the mouse cursor. + int32_t dropOffset = -1; + nsCOMPtr<nsIContent> dropParentContent = + aDropEvent->GetRangeParentContentAndOffset(&dropOffset); + if (dropOffset < 0) { + NS_WARNING( + "DropEvent::GetRangeParentContentAndOffset() returned negative offset"); + return NS_ERROR_FAILURE; + } + EditorDOMPoint droppedAt(dropParentContent, + AssertedCast<uint32_t>(dropOffset)); + if (NS_WARN_IF(!droppedAt.IsInContentNode())) { + return NS_ERROR_FAILURE; + } + + // Check if dropping into a selected range. If so and the source comes from + // same document, jump through some hoops to determine if mouse is over + // selection (bail) and whether user wants to copy selection or delete it. + if (sourceNode && sourceNode->IsEditable() && srcdoc == document) { + bool isPointInSelection = EditorUtils::IsPointInSelection( + SelectionRef(), *droppedAt.GetContainer(), droppedAt.Offset()); + if (isPointInSelection) { + // If source document and destination document is same and dropping + // into one of selected ranges, we don't need to do nothing. + // XXX If the source comes from outside of this editor, this check + // means that we don't allow to drop the item in the selected + // range. However, the selection is hidden until the <input> or + // <textarea> gets focus, therefore, this looks odd. + return NS_OK; + } + } + + // Delete if user doesn't want to copy when user moves selected content + // to different place in same editor. + // XXX Do we need the check whether it's in same document or not? + RefPtr<EditorBase> editorToDeleteSelection; + if (sourceNode && sourceNode->IsEditable() && srcdoc == document) { + if ((dataTransfer->DropEffectInt() & + nsIDragService::DRAGDROP_ACTION_MOVE) && + !(dataTransfer->DropEffectInt() & + nsIDragService::DRAGDROP_ACTION_COPY)) { + // If the source node is in native anonymous tree, it must be in + // <input> or <textarea> element. If so, its TextEditor can remove it. + if (sourceNode->IsInNativeAnonymousSubtree()) { + if (RefPtr textControlElement = TextControlElement::FromNodeOrNull( + sourceNode + ->GetClosestNativeAnonymousSubtreeRootParentOrHost())) { + editorToDeleteSelection = textControlElement->GetTextEditor(); + } + } + // Otherwise, must be the content is in HTMLEditor. + else if (IsHTMLEditor()) { + editorToDeleteSelection = this; + } else { + editorToDeleteSelection = + nsContentUtils::GetHTMLEditor(srcdoc->GetPresContext()); + } + } + // If the found editor isn't modifiable, we should not try to delete + // selection. + if (editorToDeleteSelection && !editorToDeleteSelection->IsModifiable()) { + editorToDeleteSelection = nullptr; + } + // If the found editor has collapsed selection, we need to delete nothing + // in the editor. + if (editorToDeleteSelection) { + if (Selection* selection = editorToDeleteSelection->GetSelection()) { + if (selection->IsCollapsed()) { + editorToDeleteSelection = nullptr; + } + } + } + } + + if (IsInPlaintextMode()) { + for (nsIContent* content = droppedAt.ContainerAs<nsIContent>(); content; + content = content->GetParent()) { + nsCOMPtr<nsIFormControl> formControl(do_QueryInterface(content)); + if (formControl && !formControl->AllowDrop()) { + // Don't allow dropping into a form control that doesn't allow being + // dropped into. + return NS_OK; + } + } + } + + // Combine any deletion and drop insertion into one transaction. + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + // Don't dispatch "selectionchange" event until inserting all contents. + SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__); + + // Track dropped point with nsRange because we shouldn't insert the + // dropped content into different position even if some event listeners + // modify selection. Note that Chrome's behavior is really odd. So, + // we don't need to worry about web-compat about this. + IgnoredErrorResult ignoredError; + RefPtr<nsRange> rangeAtDropPoint = + nsRange::Create(droppedAt.ToRawRangeBoundary(), + droppedAt.ToRawRangeBoundary(), ignoredError); + if (NS_WARN_IF(ignoredError.Failed()) || + NS_WARN_IF(!rangeAtDropPoint->IsPositioned())) { + editActionData.Abort(); + return NS_ERROR_FAILURE; + } + + // Remove selected contents first here because we need to fire a pair of + // "beforeinput" and "input" for deletion and web apps can cancel only + // this deletion. Note that callee may handle insertion asynchronously. + // Therefore, it is the best to remove selected content here. + if (editorToDeleteSelection) { + nsresult rv = editorToDeleteSelection->DeleteSelectionByDragAsAction( + mDispatchInputEvent); + if (NS_WARN_IF(Destroyed())) { + editActionData.Abort(); + return NS_OK; + } + // Ignore the editor instance specific error if it's another editor. + if (this != editorToDeleteSelection && + (rv == NS_ERROR_NOT_INITIALIZED || rv == NS_ERROR_EDITOR_DESTROYED)) { + rv = NS_OK; + } + // Don't cancel "insertFromDrop" even if "deleteByDrag" is canceled. + if (rv != NS_ERROR_EDITOR_ACTION_CANCELED && NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteSelectionByDragAsAction() failed"); + editActionData.Abort(); + return EditorBase::ToGenericNSResult(rv); + } + if (NS_WARN_IF(!rangeAtDropPoint->IsPositioned()) || + NS_WARN_IF(!rangeAtDropPoint->GetStartContainer()->IsContent())) { + editActionData.Abort(); + return NS_ERROR_FAILURE; + } + droppedAt = rangeAtDropPoint->StartRef(); + MOZ_ASSERT(droppedAt.IsSetAndValid()); + MOZ_ASSERT(droppedAt.IsInContentNode()); + } + + // Before inserting dropping content, we need to move focus for compatibility + // with Chrome and firing "beforeinput" event on new editing host. + RefPtr<Element> focusedElement, newFocusedElement; + if (IsTextEditor()) { + newFocusedElement = GetExposedRoot(); + focusedElement = IsActiveInDOMWindow() ? newFocusedElement : nullptr; + } + // TODO: We need to add automated tests when dropping something into an + // editing host for contenteditable which is in a shadow DOM tree + // and its host which is in design mode. + else if (!AsHTMLEditor()->IsInDesignMode()) { + focusedElement = AsHTMLEditor()->ComputeEditingHost(); + if (focusedElement && + droppedAt.ContainerAs<nsIContent>()->IsInclusiveDescendantOf( + focusedElement)) { + newFocusedElement = focusedElement; + } else { + newFocusedElement = droppedAt.ContainerAs<nsIContent>()->GetEditingHost(); + } + } + // Move selection right now. Note that this does not move focus because + // `Selection` moves focus with selection change only when the API caller is + // JS. And also this does not notify selection listeners (nor + // "selectionchange") since we created SelectionBatcher above. + ErrorResult error; + SelectionRef().SetStartAndEnd(droppedAt.ToRawRangeBoundary(), + droppedAt.ToRawRangeBoundary(), error); + if (error.Failed()) { + NS_WARNING("Selection::SetStartAndEnd() failed"); + editActionData.Abort(); + return error.StealNSResult(); + } + if (NS_WARN_IF(Destroyed())) { + editActionData.Abort(); + return NS_OK; + } + // Then, move focus if necessary. This must cause dispatching "blur" event + // and "focus" event. + if (newFocusedElement && focusedElement != newFocusedElement) { + RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager(); + DebugOnly<nsresult> rvIgnored = fm->SetFocus(newFocusedElement, 0); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsFocusManager::SetFocus() failed to set focus " + "to the element, but ignored"); + if (NS_WARN_IF(Destroyed())) { + editActionData.Abort(); + return NS_OK; + } + // "blur" or "focus" event listener may have changed the value. + // Let's keep using the original point. + if (NS_WARN_IF(!rangeAtDropPoint->IsPositioned()) || + NS_WARN_IF(!rangeAtDropPoint->GetStartContainer()->IsContent())) { + return NS_ERROR_FAILURE; + } + droppedAt = rangeAtDropPoint->StartRef(); + MOZ_ASSERT(droppedAt.IsSetAndValid()); + + // If focus is changed to different element and we're handling drop in + // contenteditable, we cannot handle it without focus. So, we should give + // it up. + if (IsHTMLEditor() && !AsHTMLEditor()->IsInDesignMode() && + NS_WARN_IF(newFocusedElement != AsHTMLEditor()->ComputeEditingHost())) { + editActionData.Abort(); + return NS_OK; + } + } + + nsresult rv = InsertDroppedDataTransferAsAction(editActionData, *dataTransfer, + droppedAt, sourcePrincipal); + if (rv == NS_ERROR_EDITOR_DESTROYED || + rv == NS_ERROR_EDITOR_ACTION_CANCELED) { + return EditorBase::ToGenericNSResult(rv); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::InsertDroppedDataTransferAsAction() failed, but ignored"); + + rv = ScrollSelectionFocusIntoView(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::ScrollSelectionFocusIntoView() failed"); + return rv; +} + +nsresult EditorBase::DeleteSelectionByDragAsAction(bool aDispatchInputEvent) { + // TODO: Move this method to `EditorBase`. + AutoRestore<bool> saveDispatchInputEvent(mDispatchInputEvent); + mDispatchInputEvent = aDispatchInputEvent; + // Even if we're handling "deleteByDrag" in same editor as "insertFromDrop", + // we need to recreate edit action data here because + // `AutoEditActionDataSetter` needs to manage event state separately. + bool requestedByAnotherEditor = GetEditAction() != EditAction::eDrop; + AutoEditActionDataSetter editActionData(*this, EditAction::eDeleteByDrag); + MOZ_ASSERT(!SelectionRef().IsCollapsed()); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return rv; + } + // But keep using placeholder transaction for "insertFromDrop" if there is. + Maybe<AutoPlaceholderBatch> treatAsOneTransaction; + if (requestedByAnotherEditor) { + treatAsOneTransaction.emplace(*this, ScrollSelectionIntoView::Yes, + __FUNCTION__); + } + + // We may need to update the source node to dispatch "dragend" below. + // Chrome restricts the new target under the <body> here. Therefore, we + // should follow it here. + // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/editing_utilities.cc;l=254;drc=da35f4ed6398ae287d5adc828b9546eec95f668a + const RefPtr<Element> editingHost = + IsHTMLEditor() ? AsHTMLEditor()->ComputeEditingHost( + HTMLEditor::LimitInBodyElement::Yes) + : nullptr; + + rv = DeleteSelectionAsSubAction(nsIEditor::eNone, IsTextEditor() + ? nsIEditor::eNoStrip + : nsIEditor::eStrip); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteSelectionAsSubAction(eNone) failed"); + return rv; + } + + if (!mDispatchInputEvent) { + return NS_OK; + } + + if (treatAsOneTransaction.isNothing()) { + DispatchInputEvent(); + } + + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + + // If we success everything here, we may need to retarget "dragend" event + // target for compatibility with the other browsers. They do this only when + // their builtin editor delete the source node from the document. Then, + // they retarget the source node to the editing host. + // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/page/drag_controller.cc;l=724;drc=d9ba13b8cd8ac0faed7afc3d1f7e4b67ebac2a0b + if (editingHost) { + if (nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1")) { + dragService->MaybeEditorDeletedSourceNode(editingHost); + } + } + return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; +} + +nsresult EditorBase::DeleteSelectionWithTransaction( + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + + AutoRangeArray rangesToDelete(SelectionRef()); + if (NS_WARN_IF(rangesToDelete.Ranges().IsEmpty())) { + NS_ASSERTION( + false, + "For avoiding to throw incompatible exception for `execCommand`, fix " + "the caller"); + return NS_ERROR_FAILURE; + } + + if (IsTextEditor()) { + if (const Text* theTextNode = AsTextEditor()->GetTextNode()) { + rangesToDelete.EnsureRangesInTextNode(*theTextNode); + } + } + + Result<CaretPoint, nsresult> caretPointOrError = DeleteRangesWithTransaction( + aDirectionAndAmount, aStripWrappers, rangesToDelete); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + *this, {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt, + SuggestCaret::AndIgnoreTrivialError}); + if (NS_FAILED(rv)) { + NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); + } + NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, + "CaretPoint::SuggestCaretPointTo() failed, but ignored"); + return NS_OK; +} + +Result<CaretPoint, nsresult> EditorBase::DeleteRangesWithTransaction( + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + const AutoRangeArray& aRangesToDelete) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!Destroyed()); + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); + + HowToHandleCollapsedRange howToHandleCollapsedRange = + EditorBase::HowToHandleCollapsedRangeFor(aDirectionAndAmount); + if (NS_WARN_IF(aRangesToDelete.IsCollapsed() && + howToHandleCollapsedRange == + HowToHandleCollapsedRange::Ignore)) { + NS_ASSERTION( + false, + "For avoiding to throw incompatible exception for `execCommand`, fix " + "the caller"); + return Err(NS_ERROR_FAILURE); + } + + RefPtr<DeleteMultipleRangesTransaction> deleteSelectionTransaction = + CreateTransactionForDeleteSelection(howToHandleCollapsedRange, + aRangesToDelete); + if (MOZ_UNLIKELY(!deleteSelectionTransaction)) { + NS_WARNING("EditorBase::CreateTransactionForDeleteSelection() failed"); + return Err(NS_ERROR_FAILURE); + } + + // XXX This is odd, this assumes that there are no multiple collapsed + // ranges in `Selection`, but it's possible scenario. + // XXX This loop looks slow, but it's rarely so because of multiple + // selection is not used so many times. + nsCOMPtr<nsIContent> deleteContent; + uint32_t deleteCharOffset = 0; + for (const OwningNonNull<EditTransactionBase>& transactionBase : + Reversed(deleteSelectionTransaction->ChildTransactions())) { + if (DeleteTextTransaction* deleteTextTransaction = + transactionBase->GetAsDeleteTextTransaction()) { + deleteContent = deleteTextTransaction->GetText(); + deleteCharOffset = deleteTextTransaction->Offset(); + break; + } + if (DeleteNodeTransaction* deleteNodeTransaction = + transactionBase->GetAsDeleteNodeTransaction()) { + deleteContent = deleteNodeTransaction->GetContent(); + break; + } + } + + RefPtr<CharacterData> deleteCharData = + CharacterData::FromNodeOrNull(deleteContent); + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eDeleteSelectedContent, aDirectionAndAmount, + ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return Err(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + if (IsHTMLEditor()) { + if (!deleteContent) { + // XXX We may remove multiple ranges in the following. Therefore, + // this must have a bug since we only add the first range into + // the changed range. + TopLevelEditSubActionDataRef().WillDeleteRange( + *this, aRangesToDelete.GetFirstRangeStartPoint<EditorRawDOMPoint>(), + aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>()); + } else if (!deleteCharData) { + TopLevelEditSubActionDataRef().WillDeleteContent(*this, *deleteContent); + } + } + + // Notify nsIEditActionListener::WillDelete[Selection|Text] + if (!mActionListeners.IsEmpty()) { + if (!deleteContent) { + MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); + AutoTArray<RefPtr<nsRange>, 8> rangesToDelete( + aRangesToDelete.CloneRanges<RefPtr>()); + AutoActionListenerArray listeners(mActionListeners.Clone()); + for (auto& listener : listeners) { + DebugOnly<nsresult> rvIgnored = + listener->WillDeleteRanges(rangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::WillDeleteRanges() failed, but ignored"); + MOZ_DIAGNOSTIC_ASSERT(!Destroyed(), + "nsIEditActionListener::WillDeleteRanges() " + "must not destroy the editor"); + } + } else if (deleteCharData) { + AutoActionListenerArray listeners(mActionListeners.Clone()); + for (auto& listener : listeners) { + // XXX Why don't we notify listeners of actual length? + DebugOnly<nsresult> rvIgnored = + listener->WillDeleteText(deleteCharData, deleteCharOffset, 1); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::WillDeleteText() failed, but ignored"); + MOZ_DIAGNOSTIC_ASSERT(!Destroyed(), + "nsIEditActionListener::WillDeleteText() must " + "not destroy the editor"); + } + } + } + + // Delete the specified amount + nsresult rv = DoTransactionInternal(deleteSelectionTransaction); + // I'm not sure whether we should keep notifying edit action listeners or + // stop doing it. For now, just keep traditional behavior. + bool destroyedByTransaction = Destroyed(); + NS_WARNING_ASSERTION(destroyedByTransaction || NS_SUCCEEDED(rv), + "EditorBase::DoTransactionInternal() failed"); + + if (IsHTMLEditor() && deleteCharData) { + MOZ_ASSERT(deleteContent); + TopLevelEditSubActionDataRef().DidDeleteText( + *this, EditorRawDOMPoint(deleteContent)); + } + + if (mTextServicesDocument && NS_SUCCEEDED(rv) && deleteContent && + !deleteCharData) { + RefPtr<TextServicesDocument> textServicesDocument = mTextServicesDocument; + textServicesDocument->DidDeleteContent(*deleteContent); + MOZ_ASSERT( + destroyedByTransaction || !Destroyed(), + "TextServicesDocument::DidDeleteContent() must not destroy the editor"); + } + + if (!mActionListeners.IsEmpty() && deleteContent && !deleteCharData) { + for (auto& listener : mActionListeners.Clone()) { + DebugOnly<nsresult> rvIgnored = + listener->DidDeleteNode(deleteContent, rv); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIEditActionListener::DidDeleteNode() failed, but ignored"); + MOZ_DIAGNOSTIC_ASSERT( + destroyedByTransaction || !Destroyed(), + "nsIEditActionListener::DidDeleteNode() must not destroy the editor"); + } + } + + if (NS_WARN_IF(destroyedByTransaction)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + return Err(rv); + } + + EditorDOMPoint pointToPutCaret = + deleteSelectionTransaction->SuggestPointToPutCaret(); + if (IsHTMLEditor() && aStripWrappers == nsIEditor::eStrip) { + const nsCOMPtr<nsIContent> anchorContent = + pointToPutCaret.GetContainerAs<nsIContent>(); + if (MOZ_LIKELY(anchorContent) && + MOZ_LIKELY(HTMLEditUtils::IsSimplyEditableNode(*anchorContent)) && + // FIXME: Perhaps, this should use `HTMLEditor::IsEmptyNode` instead. + !anchorContent->Length()) { + AutoTrackDOMPoint trackPoint(RangeUpdaterRef(), &pointToPutCaret); + nsresult rv = + MOZ_KnownLive(AsHTMLEditor()) + ->RemoveEmptyInclusiveAncestorInlineElements(*anchorContent); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::RemoveEmptyInclusiveAncestorInlineElements() " + "failed"); + return Err(rv); + } + } + } + + return CaretPoint(std::move(pointToPutCaret)); +} + +already_AddRefed<Element> EditorBase::CreateHTMLContent( + const nsAtom* aTag) const { + MOZ_ASSERT(aTag); + + RefPtr<Document> document = GetDocument(); + if (NS_WARN_IF(!document)) { + return nullptr; + } + + // XXX Wallpaper over editor bug (editor tries to create elements with an + // empty nodename). + if (aTag == nsGkAtoms::_empty) { + NS_ERROR( + "Don't pass an empty tag to EditorBase::CreateHTMLContent, " + "check caller."); + return nullptr; + } + + return document->CreateElem(nsDependentAtomString(aTag), nullptr, + kNameSpaceID_XHTML); +} + +already_AddRefed<nsTextNode> EditorBase::CreateTextNode( + const nsAString& aData) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + Document* document = GetDocument(); + if (NS_WARN_IF(!document)) { + return nullptr; + } + RefPtr<nsTextNode> text = document->CreateEmptyTextNode(); + text->MarkAsMaybeModifiedFrequently(); + if (IsPasswordEditor()) { + text->MarkAsMaybeMasked(); + } + // Don't notify; this node is still being created. + DebugOnly<nsresult> rvIgnored = text->SetText(aData, false); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Text::SetText() failed, but ignored"); + return text.forget(); +} + +NS_IMETHODIMP EditorBase::SetAttributeOrEquivalent(Element* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) { + if (NS_WARN_IF(!aElement)) { + return NS_ERROR_NULL_POINTER; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<nsAtom> attribute = NS_Atomize(aAttribute); + rv = SetAttributeOrEquivalent(aElement, attribute, aValue, + aSuppressTransaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::SetAttributeOrEquivalent() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP EditorBase::RemoveAttributeOrEquivalent( + Element* aElement, const nsAString& aAttribute, bool aSuppressTransaction) { + if (NS_WARN_IF(!aElement)) { + return NS_ERROR_NULL_POINTER; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveAttribute); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + RefPtr<nsAtom> attribute = NS_Atomize(aAttribute); + rv = RemoveAttributeOrEquivalent(aElement, attribute, aSuppressTransaction); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::RemoveAttributeOrEquivalent() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +void EditorBase::HandleKeyPressEventInReadOnlyMode( + WidgetKeyboardEvent& aKeyboardEvent) const { + MOZ_ASSERT(IsReadonly()); + MOZ_ASSERT(aKeyboardEvent.mMessage == eKeyPress); + + switch (aKeyboardEvent.mKeyCode) { + case NS_VK_BACK: + // If it's a `Backspace` key, let's consume it because it may be mapped + // to "Back" of the history navigation. So, it's possible that user + // tries to delete a character with `Backspace` even in the read-only + // editor. + aKeyboardEvent.PreventDefault(); + break; + } + // XXX How about space key (page up and page down in browser navigation)? +} + +nsresult EditorBase::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) { + MOZ_ASSERT(!IsReadonly()); + MOZ_ASSERT(aKeyboardEvent); + MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress); + + // NOTE: When you change this method, you should also change: + // * editor/libeditor/tests/test_texteditor_keyevent_handling.html + // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html + // + // And also when you add new key handling, you need to change the subclass's + // HandleKeyPressEvent()'s switch statement. + + switch (aKeyboardEvent->mKeyCode) { + case NS_VK_META: + case NS_VK_WIN: + case NS_VK_SHIFT: + case NS_VK_CONTROL: + case NS_VK_ALT: + MOZ_ASSERT_UNREACHABLE( + "eKeyPress event shouldn't be fired for modifier keys"); + return NS_ERROR_UNEXPECTED; + + case NS_VK_BACK: { + if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() || + aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) { + return NS_OK; + } + DebugOnly<nsresult> rvIgnored = + DeleteSelectionAsAction(nsIEditor::ePrevious, nsIEditor::eStrip); + aKeyboardEvent->PreventDefault(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::DeleteSelectionAsAction() failed, but ignored"); + return NS_OK; + } + case NS_VK_DELETE: { + // on certain platforms (such as windows) the shift key + // modifies what delete does (cmd_cut in this case). + // bailing here to allow the keybindings to do the cut. + if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() || + aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() || + aKeyboardEvent->IsOS()) { + return NS_OK; + } + DebugOnly<nsresult> rvIgnored = + DeleteSelectionAsAction(nsIEditor::eNext, nsIEditor::eStrip); + aKeyboardEvent->PreventDefault(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::DeleteSelectionAsAction() failed, but ignored"); + return NS_OK; + } + } + return NS_OK; +} + +nsresult EditorBase::OnInputText(const nsAString& aStringToInsert) { + AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText); + MOZ_ASSERT(!aStringToInsert.IsVoid()); + editActionData.SetData(aStringToInsert); + // FYI: For conforming to current UI Events spec, we should dispatch + // "beforeinput" event before "keypress" event, but here is in a + // "keypress" event listener. However, the other browsers dispatch + // "beforeinput" event after "keypress" event. Therefore, it makes + // sense to follow the other browsers. Spec issue: + // https://github.com/w3c/uievents/issues/220 + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, + ScrollSelectionIntoView::Yes, + __FUNCTION__); + rv = InsertTextAsSubAction(aStringToInsert, SelectionHandling::Delete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::InsertTextAsSubAction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::ReplaceTextAsAction( + const nsAString& aString, nsRange* aReplaceRange, + AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable, + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound); + MOZ_ASSERT_IF(!aReplaceRange, IsTextEditor()); + + AutoEditActionDataSetter editActionData(*this, EditAction::eReplaceText, + aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) { + editActionData.MakeBeforeInputEventNonCancelable(); + } + + if (IsTextEditor()) { + editActionData.SetData(aString); + } else { + editActionData.InitializeDataTransfer(aString); + RefPtr<StaticRange> targetRange; + if (aReplaceRange) { + // Compute offset of the range before dispatching `beforeinput` event + // because it may be referred after the DOM tree is changed and the + // range may have not computed the offset yet. + targetRange = StaticRange::Create( + aReplaceRange->GetStartContainer(), aReplaceRange->StartOffset(), + aReplaceRange->GetEndContainer(), aReplaceRange->EndOffset(), + IgnoreErrors()); + NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(), + "StaticRange::Create() failed"); + } else { + Element* editingHost = AsHTMLEditor()->ComputeEditingHost(); + NS_WARNING_ASSERTION(editingHost, + "No active editing host, no target ranges"); + if (editingHost) { + targetRange = StaticRange::Create( + editingHost, 0, editingHost, editingHost->Length(), IgnoreErrors()); + NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(), + "StaticRange::Create() failed"); + } + } + if (targetRange && targetRange->IsPositioned()) { + editActionData.AppendTargetRange(*targetRange); + } + } + + nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "MaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + + // This should emulates inserting text for better undo/redo behavior. + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return EditorBase::ToGenericNSResult(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + if (!aReplaceRange) { + // Use fast path if we're `TextEditor` because it may be in a hot path. + if (IsTextEditor()) { + nsresult rv = MOZ_KnownLive(AsTextEditor())->SetTextAsSubAction(aString); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "TextEditor::SetTextAsSubAction() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + MOZ_ASSERT_UNREACHABLE("Setting value of `HTMLEditor` isn't supported"); + return EditorBase::ToGenericNSResult(NS_ERROR_FAILURE); + } + + if (aString.IsEmpty() && aReplaceRange->Collapsed()) { + NS_WARNING("Setting value was empty and replaced range was empty"); + return NS_OK; + } + + // Note that do not notify selectionchange caused by selecting all text + // because it's preparation of our delete implementation so web apps + // shouldn't receive such selectionchange before the first mutation. + AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__); + + // Select the range but as far as possible, we should not create new range + // even if it's part of special Selection. + ErrorResult error; + SelectionRef().RemoveAllRanges(error); + if (error.Failed()) { + NS_WARNING("Selection::RemoveAllRanges() failed"); + return error.StealNSResult(); + } + SelectionRef().AddRangeAndSelectFramesAndNotifyListeners(*aReplaceRange, + error); + if (error.Failed()) { + NS_WARNING("Selection::AddRangeAndSelectFramesAndNotifyListeners() failed"); + return error.StealNSResult(); + } + + rv = ReplaceSelectionAsSubAction(aString); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::ReplaceSelectionAsSubAction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::ReplaceSelectionAsSubAction(const nsAString& aString) { + if (aString.IsEmpty()) { + nsresult rv = DeleteSelectionAsSubAction( + nsIEditor::eNone, + IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::DeleteSelectionAsSubAction(eNone) failed"); + return rv; + } + + nsresult rv = InsertTextAsSubAction(aString, SelectionHandling::Delete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::InsertTextAsSubAction() failed"); + return rv; +} + +nsresult EditorBase::HandleInlineSpellCheck( + const EditorDOMPoint& aPreviouslySelectedStart, + const AbstractRange* aRange) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (!mInlineSpellChecker) { + return NS_OK; + } + nsresult rv = mInlineSpellChecker->SpellCheckAfterEditorChange( + GetTopLevelEditSubAction(), SelectionRef(), + aPreviouslySelectedStart.GetContainer(), + aPreviouslySelectedStart.Offset(), + aRange ? aRange->GetStartContainer() : nullptr, + aRange ? aRange->StartOffset() : 0, + aRange ? aRange->GetEndContainer() : nullptr, + aRange ? aRange->EndOffset() : 0); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "mozInlineSpellChecker::SpellCheckAfterEditorChange() failed"); + return rv; +} + +Element* EditorBase::FindSelectionRoot(const nsINode& aNode) const { + return GetRoot(); +} + +void EditorBase::InitializeSelectionAncestorLimit( + nsIContent& aAncestorLimit) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + SelectionRef().SetAncestorLimiter(&aAncestorLimit); +} + +nsresult EditorBase::InitializeSelection( + const nsINode& aOriginalEventTargetNode) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + nsCOMPtr<nsIContent> selectionRootContent = + FindSelectionRoot(aOriginalEventTargetNode); + if (!selectionRootContent) { + return NS_OK; + } + + nsCOMPtr<nsISelectionController> selectionController = + GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + return NS_ERROR_FAILURE; + } + + // Init the caret + RefPtr<nsCaret> caret = GetCaret(); + if (NS_WARN_IF(!caret)) { + return NS_ERROR_FAILURE; + } + caret->SetSelection(&SelectionRef()); + DebugOnly<nsresult> rvIgnored = + selectionController->SetCaretReadOnly(IsReadonly()); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsISelectionController::SetCaretReadOnly() failed, but ignored"); + rvIgnored = selectionController->SetCaretEnabled(true); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsISelectionController::SetCaretEnabled() failed, but ignored"); + // NOTE(emilio): It's important for this call to be after + // SetCaretEnabled(true), since that would override mIgnoreUserModify to true. + // + // Also, make sure to always ignore it for designMode, since that effectively + // overrides everything and we allow to edit stuff with + // contenteditable="false" subtrees in such a document. + caret->SetIgnoreUserModify(aOriginalEventTargetNode.IsInDesignMode()); + + // Init selection + rvIgnored = + selectionController->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsISelectionController::SetSelectionFlags() failed, but ignored"); + + selectionController->SelectionWillTakeFocus(); + + // If the computed selection root isn't root content, we should set it + // as selection ancestor limit. However, if that is root element, it means + // there is not limitation of the selection, then, we must set nullptr. + // NOTE: If we set a root element to the ancestor limit, some selection + // methods don't work fine. + if (selectionRootContent->GetParent()) { + InitializeSelectionAncestorLimit(*selectionRootContent); + } else { + SelectionRef().SetAncestorLimiter(nullptr); + } + + // If there is composition when this is called, we may need to restore IME + // selection because if the editor is reframed, this already forgot IME + // selection and the transaction. + if (mComposition && mComposition->IsMovingToNewTextNode()) { + // We need to look for the new text node from current selection. + // XXX If selection is changed during reframe, this doesn't work well! + const nsRange* firstRange = SelectionRef().GetRangeAt(0); + if (NS_WARN_IF(!firstRange)) { + return NS_ERROR_FAILURE; + } + EditorRawDOMPoint atStartOfFirstRange(firstRange->StartRef()); + EditorRawDOMPoint betterInsertionPoint = + FindBetterInsertionPoint(atStartOfFirstRange); + RefPtr<Text> textNode = betterInsertionPoint.GetContainerAs<Text>(); + MOZ_ASSERT(textNode, + "There must be text node if composition string is not empty"); + if (textNode) { + MOZ_ASSERT(textNode->Length() >= mComposition->XPEndOffsetInTextNode(), + "The text node must be different from the old text node"); + RefPtr<TextRangeArray> ranges = mComposition->GetRanges(); + DebugOnly<nsresult> rvIgnored = CompositionTransaction::SetIMESelection( + *this, textNode, mComposition->XPOffsetInTextNode(), + mComposition->XPLengthInTextNode(), ranges); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "CompositionTransaction::SetIMESelection() failed, but ignored"); + mComposition->OnUpdateCompositionInEditor( + mComposition->String(), *textNode, + mComposition->XPOffsetInTextNode()); + } + } + + return NS_OK; +} + +nsresult EditorBase::FinalizeSelection() { + nsCOMPtr<nsISelectionController> selectionController = + GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + return NS_ERROR_FAILURE; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + SelectionRef().SetAncestorLimiter(nullptr); + + if (NS_WARN_IF(!GetPresShell())) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (RefPtr<nsCaret> caret = GetCaret()) { + caret->SetIgnoreUserModify(true); + DebugOnly<nsresult> rvIgnored = selectionController->SetCaretEnabled(false); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsISelectionController::SetCaretEnabled(false) failed, but ignored"); + } + + RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!focusManager)) { + return NS_ERROR_NOT_INITIALIZED; + } + // TODO: Running script from here makes harder to handle blur events. We + // should do this asynchronously. + focusManager->UpdateCaretForCaretBrowsingMode(); + if (nsCOMPtr<nsINode> node = do_QueryInterface(GetDOMEventTarget())) { + if (node->OwnerDoc()->GetUnretargetedFocusedContent() != node) { + selectionController->SelectionWillLoseFocus(); + } else { + // We leave this selection as the focused one. When the focus returns, it + // either returns to us (nothing to do), or it returns to something else, + // and nsDocumentViewerFocusListener::HandleEvent fixes it up. + } + } + return NS_OK; +} + +Element* EditorBase::GetExposedRoot() const { + Element* rootElement = GetRoot(); + if (!rootElement || !rootElement->IsInNativeAnonymousSubtree()) { + return rootElement; + } + return Element::FromNodeOrNull( + rootElement->GetClosestNativeAnonymousSubtreeRootParentOrHost()); +} + +nsresult EditorBase::DetermineCurrentDirection() { + // Get the current root direction from its frame + Element* rootElement = GetExposedRoot(); + if (NS_WARN_IF(!rootElement)) { + return NS_ERROR_FAILURE; + } + + // If we don't have an explicit direction, determine our direction + // from the content's direction + if (!IsRightToLeft() && !IsLeftToRight()) { + nsIFrame* frameForRootElement = rootElement->GetPrimaryFrame(); + if (NS_WARN_IF(!frameForRootElement)) { + return NS_ERROR_FAILURE; + } + + // Set the flag here, to enable us to use the same code path below. + // It will be flipped before returning from the function. + if (frameForRootElement->StyleVisibility()->mDirection == + StyleDirection::Rtl) { + mFlags |= nsIEditor::eEditorRightToLeft; + } else { + mFlags |= nsIEditor::eEditorLeftToRight; + } + } + + return NS_OK; +} + +nsresult EditorBase::ToggleTextDirectionAsAction(nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData(*this, EditAction::eSetTextDirection, + aPrincipal); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = DetermineCurrentDirection(); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DetermineCurrentDirection() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + MOZ_ASSERT(IsRightToLeft() || IsLeftToRight()); + // Note that we need to consider new direction before dispatching + // "beforeinput" event since "beforeinput" event listener may change it + // but not canceled. + TextDirection newDirection = + IsRightToLeft() ? TextDirection::eLTR : TextDirection::eRTL; + editActionData.SetData(IsRightToLeft() ? u"ltr"_ns : u"rtl"_ns); + + // FYI: Oddly, Chrome does not dispatch beforeinput event in this case but + // dispatches input event. + rv = editActionData.MaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "MaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = SetTextDirectionTo(newDirection); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::SetTextDirectionTo() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + editActionData.MarkAsHandled(); + + // XXX When we don't change the text direction, do we really need to + // dispatch input event? + DispatchInputEvent(); + + return NS_OK; +} + +void EditorBase::SwitchTextDirectionTo(TextDirection aTextDirection) { + MOZ_ASSERT(aTextDirection == TextDirection::eLTR || + aTextDirection == TextDirection::eRTL); + + AutoEditActionDataSetter editActionData(*this, EditAction::eSetTextDirection); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return; + } + + nsresult rv = DetermineCurrentDirection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + editActionData.SetData(aTextDirection == TextDirection::eLTR ? u"ltr"_ns + : u"rtl"_ns); + + // FYI: Oddly, Chrome does not dispatch beforeinput event in this case but + // dispatches input event. + rv = editActionData.MaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "MaybeDispatchBeforeInputEvent() failed"); + return; + } + + if ((aTextDirection == TextDirection::eLTR && IsRightToLeft()) || + (aTextDirection == TextDirection::eRTL && IsLeftToRight())) { + // Do it only when the direction is still different from the original + // new direction. Note that "beforeinput" event listener may have already + // changed the direction here, but they may not cancel the event. + nsresult rv = SetTextDirectionTo(aTextDirection); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::SetTextDirectionTo() failed"); + return; + } + } + + editActionData.MarkAsHandled(); + + // XXX When we don't change the text direction, do we really need to + // dispatch input event? + DispatchInputEvent(); +} + +nsresult EditorBase::SetTextDirectionTo(TextDirection aTextDirection) { + Element* rootElement = GetExposedRoot(); + + if (aTextDirection == TextDirection::eLTR) { + NS_ASSERTION(!IsLeftToRight(), "Unexpected mutually exclusive flag"); + mFlags &= ~nsIEditor::eEditorRightToLeft; + mFlags |= nsIEditor::eEditorLeftToRight; + nsresult rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, + u"ltr"_ns, true); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Element::SetAttr(nsGkAtoms::dir, ltr) failed"); + return rv; + } + + if (aTextDirection == TextDirection::eRTL) { + NS_ASSERTION(!IsRightToLeft(), "Unexpected mutually exclusive flag"); + mFlags |= nsIEditor::eEditorRightToLeft; + mFlags &= ~nsIEditor::eEditorLeftToRight; + nsresult rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, + u"rtl"_ns, true); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Element::SetAttr(nsGkAtoms::dir, rtl) failed"); + return rv; + } + + return NS_OK; +} + +Element* EditorBase::GetFocusedElement() const { + EventTarget* eventTarget = GetDOMEventTarget(); + if (!eventTarget) { + return nullptr; + } + + nsFocusManager* focusManager = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!focusManager)) { + return nullptr; + } + + Element* focusedElement = focusManager->GetFocusedElement(); + MOZ_ASSERT((focusedElement == eventTarget) == + SameCOMIdentity(focusedElement, eventTarget)); + + return (focusedElement == eventTarget) ? focusedElement : nullptr; +} + +bool EditorBase::IsActiveInDOMWindow() const { + EventTarget* piTarget = GetDOMEventTarget(); + if (!piTarget) { + return false; + } + + nsFocusManager* focusManager = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!focusManager)) { + return false; // Do we need to check the singleton instance?? + } + + Document* document = GetDocument(); + if (NS_WARN_IF(!document)) { + return false; + } + nsPIDOMWindowOuter* ourWindow = document->GetWindow(); + nsCOMPtr<nsPIDOMWindowOuter> win; + nsIContent* content = nsFocusManager::GetFocusedDescendant( + ourWindow, nsFocusManager::eOnlyCurrentWindow, getter_AddRefs(win)); + return SameCOMIdentity(content, piTarget); +} + +bool EditorBase::IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent) const { + // If the event is trusted, the event should always cause input. + if (NS_WARN_IF(!aGUIEvent)) { + return false; + } + + // If this is dispatched by using cordinates but this editor doesn't have + // focus, we shouldn't handle it. + if (aGUIEvent->IsUsingCoordinates() && !GetFocusedElement()) { + return false; + } + + // If a composition event isn't dispatched via widget, we need to ignore them + // since they cannot be managed by TextComposition. E.g., the event was + // created by chrome JS. + // Note that if we allow to handle such events, editor may be confused by + // strange event order. + bool needsWidget = false; + switch (aGUIEvent->mMessage) { + case eUnidentifiedEvent: + // If events are not created with proper event interface, their message + // are initialized with eUnidentifiedEvent. Let's ignore such event. + return false; + case eCompositionStart: + case eCompositionEnd: + case eCompositionUpdate: + case eCompositionChange: + case eCompositionCommitAsIs: + // Don't allow composition events whose internal event are not + // WidgetCompositionEvent. + if (!aGUIEvent->AsCompositionEvent()) { + return false; + } + needsWidget = true; + break; + default: + break; + } + if (needsWidget && !aGUIEvent->mWidget) { + return false; + } + + // Accept all trusted events. + if (aGUIEvent->IsTrusted()) { + return true; + } + + // Ignore untrusted mouse event. + // XXX Why are we handling other untrusted input events? + if (aGUIEvent->AsMouseEventBase()) { + return false; + } + + // Otherwise, we shouldn't handle any input events when we're not an active + // element of the DOM window. + return IsActiveInDOMWindow(); +} + +nsresult EditorBase::FlushPendingSpellCheck() { + // If the spell check skip flag is still enabled from creation time, + // disable it because focused editors are allowed to spell check. + if (!ShouldSkipSpellCheck()) { + return NS_OK; + } + MOZ_ASSERT(!IsHTMLEditor(), "HTMLEditor should not has pending spell checks"); + nsresult rv = RemoveFlags(nsIEditor::eEditorSkipSpellCheck); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::RemoveFlags(nsIEditor::eEditorSkipSpellCheck) failed"); + return rv; +} + +bool EditorBase::CanKeepHandlingFocusEvent( + const nsINode& aOriginalEventTargetNode) const { + if (MOZ_UNLIKELY(!IsListeningToEvents() || Destroyed())) { + return false; + } + + nsFocusManager* focusManager = nsFocusManager::GetFocusManager(); + if (MOZ_UNLIKELY(!focusManager)) { + return false; + } + + // If the event target is document mode, we only need to handle the focus + // event when the document is still in designMode. Otherwise, the + // mode has been disabled by somebody while we're handling the focus event. + if (aOriginalEventTargetNode.IsDocument()) { + return IsHTMLEditor() && aOriginalEventTargetNode.IsInDesignMode(); + } + MOZ_ASSERT(aOriginalEventTargetNode.IsContent()); + + // If nobody has focus, the focus event target has been blurred by somebody + // else. So the editor shouldn't initialize itself to start to handle + // anything. + if (!focusManager->GetFocusedElement()) { + return false; + } + const nsIContent* exposedTargetContent = + aOriginalEventTargetNode.AsContent() + ->FindFirstNonChromeOnlyAccessContent(); + const nsIContent* exposedFocusedContent = + focusManager->GetFocusedElement()->FindFirstNonChromeOnlyAccessContent(); + return exposedTargetContent && exposedFocusedContent && + exposedTargetContent == exposedFocusedContent; +} + +nsresult EditorBase::OnFocus(const nsINode& aOriginalEventTargetNode) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + InitializeSelection(aOriginalEventTargetNode); + mSpellCheckerDictionaryUpdated = false; + if (mInlineSpellChecker && CanEnableSpellCheck()) { + DebugOnly<nsresult> rvIgnored = + mInlineSpellChecker->UpdateCurrentDictionary(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "mozInlineSpellCHecker::UpdateCurrentDictionary() failed, but ignored"); + mSpellCheckerDictionaryUpdated = true; + } + // XXX Why don't we stop handling focus with the spell checker immediately + // after calling InitializeSelection? + if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent(aOriginalEventTargetNode))) { + return NS_ERROR_EDITOR_DESTROYED; + } + + const RefPtr<Element> focusedElement = GetFocusedElement(); + RefPtr<nsPresContext> presContext = + focusedElement ? focusedElement->GetPresContext( + Element::PresContextFor::eForComposedDoc) + : GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return NS_ERROR_FAILURE; + } + IMEStateManager::OnFocusInEditor(*presContext, focusedElement, *this); + + return NS_OK; +} + +void EditorBase::HideCaret(bool aHide) { + if (mHidingCaret == aHide) { + return; + } + + RefPtr<nsCaret> caret = GetCaret(); + if (NS_WARN_IF(!caret)) { + return; + } + + mHidingCaret = aHide; + if (aHide) { + caret->AddForceHide(); + } else { + caret->RemoveForceHide(); + } +} + +NS_IMETHODIMP EditorBase::Unmask(uint32_t aStart, int64_t aEnd, + uint32_t aTimeout, uint8_t aArgc) { + if (NS_WARN_IF(!IsPasswordEditor())) { + return NS_ERROR_NOT_AVAILABLE; + } + if (NS_WARN_IF(aArgc >= 1 && aStart == UINT32_MAX) || + NS_WARN_IF(aArgc >= 2 && aEnd == 0) || + NS_WARN_IF(aArgc >= 2 && aEnd > 0 && aStart >= aEnd)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + uint32_t start = aArgc < 1 ? 0 : aStart; + uint32_t length = aArgc < 2 || aEnd < 0 ? UINT32_MAX : aEnd - start; + uint32_t timeout = aArgc < 3 ? 0 : aTimeout; + nsresult rv = MOZ_KnownLive(AsTextEditor()) + ->SetUnmaskRangeAndNotify(start, length, timeout); + if (NS_FAILED(rv)) { + NS_WARNING("TextEditor::SetUnmaskRangeAndNotify() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // Flush pending layout right now since the caller may access us before + // doing it. + if (RefPtr<PresShell> presShell = GetPresShell()) { + presShell->FlushPendingNotifications(FlushType::Layout); + } + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::Mask() { + if (NS_WARN_IF(!IsPasswordEditor())) { + return NS_ERROR_NOT_AVAILABLE; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = MOZ_KnownLive(AsTextEditor())->MaskAllCharactersAndNotify(); + if (NS_FAILED(rv)) { + NS_WARNING("TextEditor::MaskAllCharactersAndNotify() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + // Flush pending layout right now since the caller may access us before + // doing it. + if (RefPtr<PresShell> presShell = GetPresShell()) { + presShell->FlushPendingNotifications(FlushType::Layout); + } + + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetUnmaskedStart(uint32_t* aResult) { + if (NS_WARN_IF(!IsPasswordEditor())) { + *aResult = 0; + return NS_ERROR_NOT_AVAILABLE; + } + *aResult = + AsTextEditor()->IsAllMasked() ? 0 : AsTextEditor()->UnmaskedStart(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetUnmaskedEnd(uint32_t* aResult) { + if (NS_WARN_IF(!IsPasswordEditor())) { + *aResult = 0; + return NS_ERROR_NOT_AVAILABLE; + } + *aResult = AsTextEditor()->IsAllMasked() ? 0 : AsTextEditor()->UnmaskedEnd(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetAutoMaskingEnabled(bool* aResult) { + if (NS_WARN_IF(!IsPasswordEditor())) { + *aResult = false; + return NS_ERROR_NOT_AVAILABLE; + } + *aResult = AsTextEditor()->IsMaskingPassword(); + return NS_OK; +} + +NS_IMETHODIMP EditorBase::GetPasswordMask(nsAString& aPasswordMask) { + aPasswordMask.Assign(TextEditor::PasswordMask()); + return NS_OK; +} + +template <typename PT, typename CT> +EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager( + const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPointBase<PT, CT>& aPointAtCaret) { + MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable()); + + nsPresContext* presContext = aEditorBase.GetPresContext(); + if (NS_WARN_IF(!presContext)) { + mFailed = true; + return; + } + + if (!presContext->BidiEnabled()) { + return; // Perform the deletion + } + + if (!aPointAtCaret.IsInContentNode()) { + mFailed = true; + return; + } + + // XXX Not sure whether this requires strong reference here. + RefPtr<nsFrameSelection> frameSelection = + aEditorBase.SelectionRef().GetFrameSelection(); + if (NS_WARN_IF(!frameSelection)) { + mFailed = true; + return; + } + + nsPrevNextBidiLevels levels = frameSelection->GetPrevNextBidiLevels( + aPointAtCaret.template ContainerAs<nsIContent>(), aPointAtCaret.Offset(), + true); + + mozilla::intl::BidiEmbeddingLevel levelBefore = levels.mLevelBefore; + mozilla::intl::BidiEmbeddingLevel levelAfter = levels.mLevelAfter; + + mozilla::intl::BidiEmbeddingLevel currentCaretLevel = + frameSelection->GetCaretBidiLevel(); + + mozilla::intl::BidiEmbeddingLevel levelOfDeletion; + levelOfDeletion = (nsIEditor::eNext == aDirectionAndAmount || + nsIEditor::eNextWord == aDirectionAndAmount) + ? levelAfter + : levelBefore; + + if (currentCaretLevel == levelOfDeletion) { + return; // Perform the deletion + } + + // Set the bidi level of the caret to that of the + // character that will be (or would have been) deleted + mNewCaretBidiLevel = Some(levelOfDeletion); + mCanceled = + !StaticPrefs::bidi_edit_delete_immediately() && levelBefore != levelAfter; +} + +void EditorBase::AutoCaretBidiLevelManager::MaybeUpdateCaretBidiLevel( + const EditorBase& aEditorBase) const { + MOZ_ASSERT(!mFailed); + if (mNewCaretBidiLevel.isNothing()) { + return; + } + RefPtr<nsFrameSelection> frameSelection = + aEditorBase.SelectionRef().GetFrameSelection(); + MOZ_ASSERT(frameSelection); + frameSelection->SetCaretBidiLevelAndMaybeSchedulePaint( + mNewCaretBidiLevel.value()); +} + +void EditorBase::UndefineCaretBidiLevel() const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + /** + * After inserting text the caret Bidi level must be set to the level of the + * inserted text.This is difficult, because we cannot know what the level is + * until after the Bidi algorithm is applied to the whole paragraph. + * + * So we set the caret Bidi level to UNDEFINED here, and the caret code will + * set it correctly later + */ + nsFrameSelection* frameSelection = SelectionRef().GetFrameSelection(); + if (frameSelection) { + frameSelection->UndefineCaretBidiLevel(); + } +} + +NS_IMETHODIMP EditorBase::GetTextLength(uint32_t* aCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP EditorBase::GetWrapWidth(int32_t* aWrapColumn) { + if (NS_WARN_IF(!aWrapColumn)) { + return NS_ERROR_INVALID_ARG; + } + *aWrapColumn = WrapWidth(); + return NS_OK; +} + +// +// See if the style value includes this attribute, and if it does, +// cut out everything from the attribute to the next semicolon. +// +static void CutStyle(const char* stylename, nsString& styleValue) { + // Find the current wrapping type: + int32_t styleStart = styleValue.LowerCaseFindASCII(stylename); + if (styleStart >= 0) { + int32_t styleEnd = styleValue.Find(u";", styleStart); + if (styleEnd > styleStart) { + styleValue.Cut(styleStart, styleEnd - styleStart + 1); + } else { + styleValue.Cut(styleStart, styleValue.Length() - styleStart); + } + } +} + +NS_IMETHODIMP EditorBase::SetWrapWidth(int32_t aWrapColumn) { + AutoEditActionDataSetter editActionData(*this, EditAction::eSetWrapWidth); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + SetWrapColumn(aWrapColumn); + + // Make sure we're a plaintext editor, otherwise we shouldn't + // do the rest of this. + if (!IsInPlaintextMode()) { + return NS_OK; + } + + // Ought to set a style sheet here ... + // Probably should keep around an mPlaintextStyleSheet for this purpose. + RefPtr<Element> rootElement = GetRoot(); + if (NS_WARN_IF(!rootElement)) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Get the current style for this root element: + nsAutoString styleValue; + rootElement->GetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue); + + // We'll replace styles for these values: + CutStyle("white-space", styleValue); + CutStyle("width", styleValue); + CutStyle("font-family", styleValue); + + // If we have other style left, trim off any existing semicolons + // or white-space, then add a known semicolon-space: + if (!styleValue.IsEmpty()) { + styleValue.Trim("; \t", false, true); + styleValue.AppendLiteral("; "); + } + + // Make sure we have fixed-width font. This should be done for us, + // but it isn't, see bug 22502, so we have to add "font: -moz-fixed;". + // Only do this if we're wrapping. + if (IsWrapHackEnabled() && aWrapColumn >= 0) { + styleValue.AppendLiteral("font-family: -moz-fixed; "); + } + + // and now we're ready to set the new white-space/wrapping style. + if (aWrapColumn > 0) { + // Wrap to a fixed column. + styleValue.AppendLiteral("white-space: pre-wrap; width: "); + styleValue.AppendInt(aWrapColumn); + styleValue.AppendLiteral("ch;"); + } else if (!aWrapColumn) { + styleValue.AppendLiteral("white-space: pre-wrap;"); + } else { + styleValue.AppendLiteral("white-space: pre;"); + } + + nsresult rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + styleValue, true); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Element::SetAttr(nsGkAtoms::style) failed"); + return rv; +} + +NS_IMETHODIMP EditorBase::GetNewlineHandling(int32_t* aNewlineHandling) { + if (NS_WARN_IF(!aNewlineHandling)) { + return NS_ERROR_INVALID_ARG; + } + *aNewlineHandling = mNewlineHandling; + return NS_OK; +} + +NS_IMETHODIMP EditorBase::SetNewlineHandling(int32_t aNewlineHandling) { + switch (aNewlineHandling) { + case nsIEditor::eNewlinesPasteIntact: + case nsIEditor::eNewlinesPasteToFirst: + case nsIEditor::eNewlinesReplaceWithSpaces: + case nsIEditor::eNewlinesStrip: + case nsIEditor::eNewlinesReplaceWithCommas: + case nsIEditor::eNewlinesStripSurroundingWhitespace: + mNewlineHandling = aNewlineHandling; + return NS_OK; + default: + NS_ERROR("SetNewlineHandling() is called with wrong value"); + return NS_ERROR_INVALID_ARG; + } +} + +bool EditorBase::IsSelectionRangeContainerNotContent() const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // TODO: Make all callers use !AutoRangeArray::IsInContent() instead. + const uint32_t rangeCount = SelectionRef().RangeCount(); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(SelectionRef().RangeCount() == rangeCount); + const nsRange* range = SelectionRef().GetRangeAt(i); + MOZ_ASSERT(range); + if (MOZ_UNLIKELY(!range) || MOZ_UNLIKELY(!range->GetStartContainer()) || + MOZ_UNLIKELY(!range->GetStartContainer()->IsContent()) || + MOZ_UNLIKELY(!range->GetEndContainer()) || + MOZ_UNLIKELY(!range->GetEndContainer()->IsContent())) { + return true; + } + } + return false; +} + +NS_IMETHODIMP EditorBase::InsertText(const nsAString& aStringToInsert) { + nsresult rv = InsertTextAsAction(aStringToInsert); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::InsertTextAsAction() failed"); + return rv; +} + +nsresult EditorBase::InsertTextAsAction(const nsAString& aStringToInsert, + nsIPrincipal* aPrincipal) { + // Showing this assertion is fine if this method is called by outside via + // mutation event listener or something. Otherwise, this is called by + // wrong method. + NS_ASSERTION(!mPlaceholderBatch, + "Should be called only when this is the only edit action of the " + "operation " + "unless mutation event listener nests some operations"); + + AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText, + aPrincipal); + // Note that we don't need to replace native line breaks with XP line breaks + // here because Chrome does not do it. + MOZ_ASSERT(!aStringToInsert.IsVoid()); + editActionData.SetData(aStringToInsert); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); + return EditorBase::ToGenericNSResult(rv); + } + + nsString stringToInsert(aStringToInsert); + if (IsTextEditor()) { + nsContentUtils::PlatformToDOMLineBreaks(stringToInsert); + } + AutoPlaceholderBatch treatAsOneTransaction( + *this, ScrollSelectionIntoView::Yes, __FUNCTION__); + rv = InsertTextAsSubAction(stringToInsert, SelectionHandling::Delete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::InsertTextAsSubAction() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult EditorBase::InsertTextAsSubAction( + const nsAString& aStringToInsert, SelectionHandling aSelectionHandling) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(mPlaceholderBatch); + MOZ_ASSERT(IsHTMLEditor() || + aStringToInsert.FindChar(nsCRT::CR) == kNotFound); + MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore, mComposition); + + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + + EditSubAction editSubAction = ShouldHandleIMEComposition() + ? EditSubAction::eInsertTextComingFromIME + : EditSubAction::eInsertText; + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, editSubAction, nsIEditor::eNext, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + Result<EditActionResult, nsresult> result = + HandleInsertText(editSubAction, aStringToInsert, aSelectionHandling); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("EditorBase::HandleInsertText() failed"); + return result.unwrapErr(); + } + return NS_OK; +} + +NS_IMETHODIMP EditorBase::InsertLineBreak() { return NS_ERROR_NOT_IMPLEMENTED; } + +/***************************************************************************** + * mozilla::EditorBase::AutoEditActionDataSetter + *****************************************************************************/ + +EditorBase::AutoEditActionDataSetter::AutoEditActionDataSetter( + const EditorBase& aEditorBase, EditAction aEditAction, + nsIPrincipal* aPrincipal /* = nullptr */) + : mEditorBase(const_cast<EditorBase&>(aEditorBase)), + mPrincipal(aPrincipal), + mParentData(aEditorBase.mEditActionData), + mData(VoidString()), + mRawEditAction(aEditAction), + mTopLevelEditSubAction(EditSubAction::eNone), + mAborted(false), + mHasTriedToDispatchBeforeInputEvent(false), + mBeforeInputEventCanceled(false), + mMakeBeforeInputEventNonCancelable(false), + mHasTriedToDispatchClipboardEvent(false), + mEditorWasDestroyedDuringHandlingEditAction( + mParentData && + mParentData->mEditorWasDestroyedDuringHandlingEditAction), + mHandled(false) { + // If we're nested edit action, copies necessary data from the parent. + if (mParentData) { + mSelection = mParentData->mSelection; + MOZ_ASSERT(!mSelection || + (mSelection->GetType() == SelectionType::eNormal)); + + // If we're not editing something, we should inherit the parent's edit + // action. This may occur if creator or its callee use public methods which + // just returns something. + if (IsEditActionInOrderToEditSomething(aEditAction)) { + mEditAction = aEditAction; + } else { + mEditAction = mParentData->mEditAction; + // If we inherit an edit action whose handler needs to dispatch a + // clipboard event, we should inherit the clipboard dispatching state + // too because this nest occurs by a clipboard event listener or + // a beforeinput/mutation event listener is important for checking + // whether we've already called `MaybeDispatchBeforeInputEvent()` + // property in some points. If the former case, not yet dispatching + // beforeinput event is okay (not fine). + mHasTriedToDispatchClipboardEvent = + mParentData->mHasTriedToDispatchClipboardEvent; + } + mTopLevelEditSubAction = mParentData->mTopLevelEditSubAction; + + // Parent's mTopLevelEditSubActionData should be referred instead so that + // we don't need to set mTopLevelEditSubActionData.mSelectedRange nor + // mTopLevelEditActionData.mChangedRange here. + + mDirectionOfTopLevelEditSubAction = + mParentData->mDirectionOfTopLevelEditSubAction; + } else { + mSelection = mEditorBase.GetSelection(); + if (NS_WARN_IF(!mSelection)) { + return; + } + + MOZ_ASSERT(mSelection->GetType() == SelectionType::eNormal); + + mEditAction = aEditAction; + mDirectionOfTopLevelEditSubAction = eNone; + if (mEditorBase.IsHTMLEditor()) { + mTopLevelEditSubActionData.mSelectedRange = + mEditorBase.AsHTMLEditor() + ->GetSelectedRangeItemForTopLevelEditSubAction(); + mTopLevelEditSubActionData.mChangedRange = + mEditorBase.AsHTMLEditor()->GetChangedRangeForTopLevelEditSubAction(); + mTopLevelEditSubActionData.mCachedPendingStyles.emplace(); + } + } + mEditorBase.mEditActionData = this; +} + +EditorBase::AutoEditActionDataSetter::~AutoEditActionDataSetter() { + MOZ_ASSERT(mHasCanHandleChecked); + + if (!mSelection || NS_WARN_IF(mEditorBase.mEditActionData != this)) { + return; + } + mEditorBase.mEditActionData = mParentData; + + MOZ_ASSERT( + !mTopLevelEditSubActionData.mSelectedRange || + (!mTopLevelEditSubActionData.mSelectedRange->mStartContainer && + !mTopLevelEditSubActionData.mSelectedRange->mEndContainer), + "mTopLevelEditSubActionData.mSelectedRange should've been cleared"); +} + +void EditorBase::AutoEditActionDataSetter::UpdateSelectionCache( + Selection& aSelection) { + MOZ_ASSERT(aSelection.GetType() == SelectionType::eNormal); + + if (mSelection == &aSelection) { + return; + } + + AutoEditActionDataSetter& topLevelEditActionData = + [&]() -> AutoEditActionDataSetter& { + for (AutoEditActionDataSetter* editActionData = this;; + editActionData = editActionData->mParentData) { + if (!editActionData->mParentData) { + return *editActionData; + } + } + MOZ_ASSERT_UNREACHABLE("You do something wrong"); + }(); + + // Keep grabbing the old selection in the top level edit action data until the + // all owners end handling it. + if (mSelection) { + topLevelEditActionData.mRetiredSelections.AppendElement(*mSelection); + } + + // If the old selection is in batch, we should end the batch which + // `EditorBase::BeginUpdateViewBatch` started. + if (mEditorBase.mUpdateCount && mSelection) { + mSelection->EndBatchChanges(__FUNCTION__); + } + + Selection* previousSelection = mSelection; + mSelection = &aSelection; + for (AutoEditActionDataSetter* parentActionData = mParentData; + parentActionData; parentActionData = parentActionData->mParentData) { + if (!parentActionData->mSelection) { + continue; + } + // Skip scanning mRetiredSelections if we've already handled the selection + // previous time. + if (parentActionData->mSelection != previousSelection) { + if (!topLevelEditActionData.mRetiredSelections.Contains( + OwningNonNull<Selection>(*parentActionData->mSelection))) { + topLevelEditActionData.mRetiredSelections.AppendElement( + *parentActionData->mSelection); + } + previousSelection = parentActionData->mSelection; + } + parentActionData->mSelection = &aSelection; + } + + // Restart the batching in the new selection. + if (mEditorBase.mUpdateCount) { + aSelection.StartBatchChanges(__FUNCTION__); + } +} + +void EditorBase::AutoEditActionDataSetter::SetColorData( + const nsAString& aData) { + MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(), + "It's too late to set data since this may have already dispatched " + "a beforeinput event"); + + if (aData.IsEmpty()) { + // When removing color/background-color, let's use empty string. + mData.Truncate(); + MOZ_ASSERT(!mData.IsVoid()); + return; + } + + DebugOnly<bool> validColorValue = HTMLEditUtils::GetNormalizedCSSColorValue( + aData, HTMLEditUtils::ZeroAlphaColor::RGBAValue, mData); + MOZ_ASSERT_IF(validColorValue, !mData.IsVoid()); +} + +void EditorBase::AutoEditActionDataSetter::InitializeDataTransfer( + DataTransfer* aDataTransfer) { + MOZ_ASSERT(aDataTransfer); + MOZ_ASSERT(aDataTransfer->IsReadOnly()); + MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(), + "It's too late to set dataTransfer since this may have already " + "dispatched a beforeinput event"); + + mDataTransfer = aDataTransfer; +} + +void EditorBase::AutoEditActionDataSetter::InitializeDataTransfer( + nsITransferable* aTransferable) { + MOZ_ASSERT(aTransferable); + MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(), + "It's too late to set dataTransfer since this may have already " + "dispatched a beforeinput event"); + + Document* document = mEditorBase.GetDocument(); + nsIGlobalObject* scopeObject = + document ? document->GetScopeObject() : nullptr; + mDataTransfer = new DataTransfer(scopeObject, eEditorInput, aTransferable); +} + +void EditorBase::AutoEditActionDataSetter::InitializeDataTransfer( + const nsAString& aString) { + MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(), + "It's too late to set dataTransfer since this may have already " + "dispatched a beforeinput event"); + Document* document = mEditorBase.GetDocument(); + nsIGlobalObject* scopeObject = + document ? document->GetScopeObject() : nullptr; + mDataTransfer = new DataTransfer(scopeObject, eEditorInput, aString); +} + +void EditorBase::AutoEditActionDataSetter::InitializeDataTransferWithClipboard( + SettingDataTransfer aSettingDataTransfer, int32_t aClipboardType) { + MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(), + "It's too late to set dataTransfer since this may have already " + "dispatched a beforeinput event"); + + Document* document = mEditorBase.GetDocument(); + nsIGlobalObject* scopeObject = + document ? document->GetScopeObject() : nullptr; + // mDataTransfer will be used for eEditorInput event, but we can keep + // using ePaste and ePasteNoFormatting here. If we need to use eEditorInput, + // we need to create eEditorInputNoFormatting or something... + mDataTransfer = + new DataTransfer(scopeObject, + aSettingDataTransfer == SettingDataTransfer::eWithFormat + ? ePaste + : ePasteNoFormatting, + true /* is external */, aClipboardType); +} + +void EditorBase::AutoEditActionDataSetter::AppendTargetRange( + StaticRange& aTargetRange) { + mTargetRanges.AppendElement(aTargetRange); +} + +bool EditorBase::AutoEditActionDataSetter::IsBeforeInputEventEnabled() const { + if (!StaticPrefs::dom_input_events_beforeinput_enabled()) { + return false; + } + + // Don't dispatch "beforeinput" event when the editor user makes us stop + // dispatching input event. + if (mEditorBase.IsSuppressingDispatchingInputEvent()) { + return false; + } + + // If mPrincipal has set, it means that we're handling an edit action + // which is requested by JS. If it's not chrome script, we shouldn't + // dispatch "beforeinput" event. + if (mPrincipal && !mPrincipal->IsSystemPrincipal()) { + // But if it's content script of an addon, `execCommand` calls are a + // part of browser's default action from point of view of web apps. + // Therefore, we should dispatch `beforeinput` event. + // https://github.com/w3c/input-events/issues/91 + if (!mPrincipal->GetIsAddonOrExpandedAddonPrincipal()) { + return false; + } + } + + return true; +} + +nsresult EditorBase::AutoEditActionDataSetter::MaybeFlushPendingNotifications() + const { + MOZ_ASSERT(CanHandle()); + if (!MayEditActionRequireLayout(mRawEditAction)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + OwningNonNull<EditorBase> editorBase = mEditorBase; + RefPtr<PresShell> presShell = editorBase->GetPresShell(); + if (MOZ_UNLIKELY(NS_WARN_IF(!presShell))) { + return NS_ERROR_NOT_AVAILABLE; + } + presShell->FlushPendingNotifications(FlushType::Layout); + if (MOZ_UNLIKELY(NS_WARN_IF(editorBase->Destroyed()))) { + return NS_ERROR_EDITOR_DESTROYED; + } + return NS_OK; +} + +nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent( + nsIEditor::EDirection aDeleteDirectionAndAmount /* = nsIEditor::eNone */) { + MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(), + "We've already handled beforeinput event"); + MOZ_ASSERT(CanHandle()); + MOZ_ASSERT_IF(IsBeforeInputEventEnabled(), + ShouldAlreadyHaveHandledBeforeInputEventDispatching()); + MOZ_ASSERT_IF(!MayEditActionDeleteAroundCollapsedSelection(mEditAction), + aDeleteDirectionAndAmount == nsIEditor::eNone); + + mHasTriedToDispatchBeforeInputEvent = true; + + if (!IsBeforeInputEventEnabled()) { + return NS_OK; + } + + // If we're called from OnCompositionEnd(), we shouldn't dispatch + // "beforeinput" event since the preceding OnCompositionChange() call has + // already dispatched "beforeinput" event for this. + if (mEditAction == EditAction::eCommitComposition || + mEditAction == EditAction::eCancelComposition) { + return NS_OK; + } + + RefPtr<Element> targetElement = mEditorBase.GetInputEventTargetElement(); + if (!targetElement) { + // If selection is not in editable element and it is outside of any + // editing hosts, there may be no target element to dispatch `beforeinput` + // event. In this case, the caller shouldn't keep handling the edit + // action since web apps cannot override it with `beforeinput` event + // listener, but for backward compatibility, we should return a special + // success code instead of error. + return NS_OK; + } + OwningNonNull<EditorBase> editorBase = mEditorBase; + EditorInputType inputType = ToInputType(mEditAction); + if (editorBase->IsHTMLEditor() && mTargetRanges.IsEmpty()) { + // If the edit action will delete selected ranges, compute the range + // strictly. + if (MayEditActionDeleteAroundCollapsedSelection(mEditAction) || + (!editorBase->SelectionRef().IsCollapsed() && + MayEditActionDeleteSelection(mEditAction))) { + if (!editorBase + ->FlushPendingNotificationsIfToHandleDeletionWithFrameSelection( + aDeleteDirectionAndAmount)) { + NS_WARNING( + "Flusing pending notifications caused destroying the editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + + AutoRangeArray rangesToDelete(editorBase->SelectionRef()); + if (!rangesToDelete.Ranges().IsEmpty()) { + nsresult rv = MOZ_KnownLive(editorBase->AsHTMLEditor()) + ->ComputeTargetRanges(aDeleteDirectionAndAmount, + rangesToDelete); + if (rv == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING("HTMLEditor::ComputeTargetRanges() destroyed the editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + if (rv == NS_ERROR_EDITOR_NO_EDITABLE_RANGE) { + // For now, keep dispatching `beforeinput` event even if no selection + // range can be editable. + rv = NS_OK; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "HTMLEditor::ComputeTargetRanges() failed, but ignored"); + for (auto& range : rangesToDelete.Ranges()) { + RefPtr<StaticRange> staticRange = + StaticRange::Create(range, IgnoreErrors()); + if (NS_WARN_IF(!staticRange)) { + continue; + } + AppendTargetRange(*staticRange); + } + } + } + // Otherwise, just set target ranges to selection ranges. + else if (MayHaveTargetRangesOnHTMLEditor(inputType)) { + if (uint32_t rangeCount = editorBase->SelectionRef().RangeCount()) { + mTargetRanges.SetCapacity(rangeCount); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(editorBase->SelectionRef().RangeCount() == rangeCount); + const nsRange* range = editorBase->SelectionRef().GetRangeAt(i); + MOZ_ASSERT(range); + MOZ_ASSERT(range->IsPositioned()); + if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || + MOZ_UNLIKELY(NS_WARN_IF(!range->IsPositioned()))) { + continue; + } + // Now, we need to fix the offset of target range because it may + // be referred after modifying the DOM tree and range boundaries + // of `range` may have not computed offset yet. + RefPtr<StaticRange> targetRange = StaticRange::Create( + range->GetStartContainer(), range->StartOffset(), + range->GetEndContainer(), range->EndOffset(), IgnoreErrors()); + if (NS_WARN_IF(!targetRange) || + NS_WARN_IF(!targetRange->IsPositioned())) { + continue; + } + mTargetRanges.AppendElement(std::move(targetRange)); + } + } + } + } + nsEventStatus status = nsEventStatus_eIgnore; + InputEventOptions::NeverCancelable neverCancelable = + mMakeBeforeInputEventNonCancelable + ? InputEventOptions::NeverCancelable::Yes + : InputEventOptions::NeverCancelable::No; + nsresult rv = nsContentUtils::DispatchInputEvent( + targetElement, eEditorBeforeInput, inputType, editorBase, + mDataTransfer + ? InputEventOptions(mDataTransfer, std::move(mTargetRanges), + neverCancelable) + : InputEventOptions(mData, std::move(mTargetRanges), neverCancelable), + &status); + if (NS_WARN_IF(mEditorBase.Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (NS_FAILED(rv)) { + NS_WARNING("nsContentUtils::DispatchInputEvent() failed"); + return rv; + } + mBeforeInputEventCanceled = status == nsEventStatus_eConsumeNoDefault; + if (mBeforeInputEventCanceled && mEditorBase.IsHTMLEditor()) { + mEditorBase.AsHTMLEditor()->mHasBeforeInputBeenCanceled = true; + } + return mBeforeInputEventCanceled ? NS_ERROR_EDITOR_ACTION_CANCELED : NS_OK; +} + +/***************************************************************************** + * mozilla::EditorBase::TopLevelEditSubActionData + *****************************************************************************/ + +nsresult EditorBase::TopLevelEditSubActionData::AddNodeToChangedRange( + const HTMLEditor& aHTMLEditor, nsINode& aNode) { + EditorRawDOMPoint startPoint(&aNode); + EditorRawDOMPoint endPoint(&aNode); + DebugOnly<bool> advanced = endPoint.AdvanceOffset(); + NS_WARNING_ASSERTION(advanced, "Failed to set endPoint to next to aNode"); + nsresult rv = AddRangeToChangedRange(aHTMLEditor, startPoint, endPoint); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "TopLevelEditSubActionData::AddRangeToChangedRange() failed"); + return rv; +} + +nsresult EditorBase::TopLevelEditSubActionData::AddPointToChangedRange( + const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aPoint) { + nsresult rv = AddRangeToChangedRange(aHTMLEditor, aPoint, aPoint); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "TopLevelEditSubActionData::AddRangeToChangedRange() failed"); + return rv; +} + +nsresult EditorBase::TopLevelEditSubActionData::AddRangeToChangedRange( + const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aStart, + const EditorRawDOMPoint& aEnd) { + if (NS_WARN_IF(!aStart.IsSet()) || NS_WARN_IF(!aEnd.IsSet())) { + return NS_ERROR_INVALID_ARG; + } + + if (!aHTMLEditor.IsDescendantOfRoot(aStart.GetContainer()) || + (aStart.GetContainer() != aEnd.GetContainer() && + !aHTMLEditor.IsDescendantOfRoot(aEnd.GetContainer()))) { + return NS_OK; + } + + // If mChangedRange hasn't been set, we can just set it to `aStart` and + // `aEnd`. + if (!mChangedRange->IsPositioned()) { + nsresult rv = mChangedRange->SetStartAndEnd(aStart.ToRawRangeBoundary(), + aEnd.ToRawRangeBoundary()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); + return rv; + } + + Maybe<int32_t> relation = + mChangedRange->StartRef().IsSet() + ? nsContentUtils::ComparePoints(mChangedRange->StartRef(), + aStart.ToRawRangeBoundary()) + : Some(1); + if (NS_WARN_IF(!relation)) { + return NS_ERROR_FAILURE; + } + + // If aStart is before start of mChangedRange, reset the start. + if (*relation > 0) { + ErrorResult error; + mChangedRange->SetStart(aStart.ToRawRangeBoundary(), error); + if (error.Failed()) { + NS_WARNING("nsRange::SetStart() failed"); + return error.StealNSResult(); + } + } + + relation = mChangedRange->EndRef().IsSet() + ? nsContentUtils::ComparePoints(mChangedRange->EndRef(), + aEnd.ToRawRangeBoundary()) + : Some(1); + if (NS_WARN_IF(!relation)) { + return NS_ERROR_FAILURE; + } + + // If aEnd is after end of mChangedRange, reset the end. + if (*relation < 0) { + ErrorResult error; + mChangedRange->SetEnd(aEnd.ToRawRangeBoundary(), error); + if (error.Failed()) { + NS_WARNING("nsRange::SetEnd() failed"); + return error.StealNSResult(); + } + } + + return NS_OK; +} + +void EditorBase::TopLevelEditSubActionData::DidCreateElement( + EditorBase& aEditorBase, Element& aNewElement) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + DebugOnly<nsresult> rvIgnored = + AddNodeToChangedRange(*aEditorBase.AsHTMLEditor(), aNewElement); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddNodeToChangedRange() failed, but ignored"); +} + +void EditorBase::TopLevelEditSubActionData::DidInsertContent( + EditorBase& aEditorBase, nsIContent& aNewContent) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + DebugOnly<nsresult> rvIgnored = + AddNodeToChangedRange(*aEditorBase.AsHTMLEditor(), aNewContent); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddNodeToChangedRange() failed, but ignored"); +} + +void EditorBase::TopLevelEditSubActionData::WillDeleteContent( + EditorBase& aEditorBase, nsIContent& aRemovingContent) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + DebugOnly<nsresult> rvIgnored = + AddNodeToChangedRange(*aEditorBase.AsHTMLEditor(), aRemovingContent); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddNodeToChangedRange() failed, but ignored"); +} + +void EditorBase::TopLevelEditSubActionData::DidSplitContent( + EditorBase& aEditorBase, nsIContent& aSplitContent, nsIContent& aNewContent, + SplitNodeDirection aSplitNodeDirection) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + DebugOnly<nsresult> rvIgnored = + aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne + ? AddRangeToChangedRange(*aEditorBase.AsHTMLEditor(), + EditorRawDOMPoint(&aNewContent, 0), + EditorRawDOMPoint(&aSplitContent, 0)) + : AddRangeToChangedRange(*aEditorBase.AsHTMLEditor(), + EditorRawDOMPoint::AtEndOf(aSplitContent), + EditorRawDOMPoint::AtEndOf(aNewContent)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddRangeToChangedRange() " + "failed, but ignored"); +} + +void EditorBase::TopLevelEditSubActionData::DidJoinContents( + EditorBase& aEditorBase, const EditorRawDOMPoint& aJoinedPoint) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + DebugOnly<nsresult> rvIgnored = + AddPointToChangedRange(*aEditorBase.AsHTMLEditor(), aJoinedPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddPointToChangedRange() " + "failed, but ignored"); +} + +void EditorBase::TopLevelEditSubActionData::DidInsertText( + EditorBase& aEditorBase, const EditorRawDOMPoint& aInsertionBegin, + const EditorRawDOMPoint& aInsertionEnd) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + DebugOnly<nsresult> rvIgnored = AddRangeToChangedRange( + *aEditorBase.AsHTMLEditor(), aInsertionBegin, aInsertionEnd); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddRangeToChangedRange() " + "failed, but ignored"); +} + +void EditorBase::TopLevelEditSubActionData::DidDeleteText( + EditorBase& aEditorBase, const EditorRawDOMPoint& aStartInTextNode) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + DebugOnly<nsresult> rvIgnored = + AddPointToChangedRange(*aEditorBase.AsHTMLEditor(), aStartInTextNode); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddPointToChangedRange() " + "failed, but ignored"); +} + +void EditorBase::TopLevelEditSubActionData::WillDeleteRange( + EditorBase& aEditorBase, const EditorRawDOMPoint& aStart, + const EditorRawDOMPoint& aEnd) { + MOZ_ASSERT(aEditorBase.AsHTMLEditor()); + MOZ_ASSERT(aStart.IsSet()); + MOZ_ASSERT(aEnd.IsSet()); + + if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) { + return; // We have not been initialized yet or already been destroyed. + } + + if (!aEditorBase.EditSubActionDataRef().mAdjustChangedRangeFromListener) { + return; // Temporarily disabled by edit sub-action handler. + } + + // XXX Looks like that this is wrong. We delete multiple selection ranges + // once, but this adds only first range into the changed range. + // Anyway, we should take the range as an argument. + DebugOnly<nsresult> rvIgnored = + AddRangeToChangedRange(*aEditorBase.AsHTMLEditor(), aStart, aEnd); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "TopLevelEditSubActionData::AddRangeToChangedRange() " + "failed, but ignored"); +} + +nsPIDOMWindowOuter* EditorBase::GetWindow() const { + return mDocument ? mDocument->GetWindow() : nullptr; +} + +nsPIDOMWindowInner* EditorBase::GetInnerWindow() const { + return mDocument ? mDocument->GetInnerWindow() : nullptr; +} + +PresShell* EditorBase::GetPresShell() const { + return mDocument ? mDocument->GetPresShell() : nullptr; +} + +nsPresContext* EditorBase::GetPresContext() const { + PresShell* presShell = GetPresShell(); + return presShell ? presShell->GetPresContext() : nullptr; +} + +already_AddRefed<nsCaret> EditorBase::GetCaret() const { + PresShell* presShell = GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + return presShell->GetCaret(); +} + +nsISelectionController* EditorBase::GetSelectionController() const { + if (mSelectionController) { + return mSelectionController; + } + if (!mDocument) { + return nullptr; + } + return mDocument->GetPresShell(); +} + +} // namespace mozilla |