diff options
Diffstat (limited to 'editor/libeditor/EditorBase.cpp')
-rw-r--r-- | editor/libeditor/EditorBase.cpp | 5244 |
1 files changed, 5244 insertions, 0 deletions
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp new file mode 100644 index 0000000000..13505b2d3a --- /dev/null +++ b/editor/libeditor/EditorBase.cpp @@ -0,0 +1,5244 @@ +/* -*- 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 "mozilla/EditorBase.h" + +#include "mozilla/DebugOnly.h" // for DebugOnly + +#include <stdio.h> // for nullptr, stdout +#include <string.h> // for strcmp + +#include "ChangeAttributeTransaction.h" // for ChangeAttributeTransaction +#include "CompositionTransaction.h" // for CompositionTransaction +#include "CreateElementTransaction.h" // for CreateElementTransaction +#include "DeleteNodeTransaction.h" // for DeleteNodeTransaction +#include "DeleteRangeTransaction.h" // for DeleteRangeTransaction +#include "DeleteTextTransaction.h" // for DeleteTextTransaction +#include "EditAggregateTransaction.h" // for EditAggregateTransaction +#include "EditorEventListener.h" // for EditorEventListener +#include "InsertNodeTransaction.h" // for InsertNodeTransaction +#include "InsertTextTransaction.h" // for InsertTextTransaction +#include "JoinNodeTransaction.h" // for JoinNodeTransaction +#include "PlaceholderTransaction.h" // for PlaceholderTransaction +#include "SplitNodeTransaction.h" // for SplitNodeTransaction +#include "StyleSheetTransactions.h" // for AddStyleSheetTransaction, etc. +#include "TextEditUtils.h" // for TextEditUtils +#include "mozFlushType.h" // for mozFlushType::Flush_Frames +#include "mozInlineSpellChecker.h" // for mozInlineSpellChecker +#include "mozilla/CheckedInt.h" // for CheckedInt +#include "mozilla/EditorUtils.h" // for AutoRules, etc. +#include "mozilla/EditTransactionBase.h" // for EditTransactionBase +#include "mozilla/IMEStateManager.h" // for IMEStateManager +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/dom/Selection.h" // for Selection, etc. +#include "mozilla/Services.h" // for GetObserverService +#include "mozilla/TextComposition.h" // for TextComposition +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement +#include "mozilla/dom/Text.h" +#include "mozilla/dom/Event.h" +#include "mozilla/mozalloc.h" // for operator new, etc. +#include "nsAString.h" // for nsAString_internal::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 "nsComponentManagerUtils.h" // for do_CreateInstance +#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle +#include "nsContentUtils.h" // for nsContentUtils +#include "nsDOMString.h" // for DOMStringIsNull +#include "nsDebug.h" // for NS_ENSURE_TRUE, etc. +#include "nsError.h" // for NS_OK, etc. +#include "nsFocusManager.h" // for nsFocusManager +#include "nsFrameSelection.h" // for nsFrameSelection +#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::dir +#include "nsIAbsorbingTransaction.h" // for nsIAbsorbingTransaction +#include "nsIAtom.h" // for nsIAtom +#include "nsIContent.h" // for nsIContent +#include "nsIDOMAttr.h" // for nsIDOMAttr +#include "nsIDOMCharacterData.h" // for nsIDOMCharacterData +#include "nsIDOMDocument.h" // for nsIDOMDocument +#include "nsIDOMElement.h" // for nsIDOMElement +#include "nsIDOMEvent.h" // for nsIDOMEvent +#include "nsIDOMEventListener.h" // for nsIDOMEventListener +#include "nsIDOMEventTarget.h" // for nsIDOMEventTarget +#include "nsIDOMHTMLElement.h" // for nsIDOMHTMLElement +#include "nsIDOMKeyEvent.h" // for nsIDOMKeyEvent, etc. +#include "nsIDOMMozNamedAttrMap.h" // for nsIDOMMozNamedAttrMap +#include "nsIDOMMouseEvent.h" // for nsIDOMMouseEvent +#include "nsIDOMNode.h" // for nsIDOMNode, etc. +#include "nsIDOMNodeList.h" // for nsIDOMNodeList +#include "nsIDOMText.h" // for nsIDOMText +#include "nsIDocument.h" // for nsIDocument +#include "nsIDocumentStateListener.h" // for nsIDocumentStateListener +#include "nsIEditActionListener.h" // for nsIEditActionListener +#include "nsIEditorObserver.h" // for nsIEditorObserver +#include "nsIEditorSpellCheck.h" // for nsIEditorSpellCheck +#include "nsIFrame.h" // for nsIFrame +#include "nsIHTMLDocument.h" // for nsIHTMLDocument +#include "nsIInlineSpellChecker.h" // for nsIInlineSpellChecker, etc. +#include "nsNameSpaceManager.h" // for kNameSpaceID_None, etc. +#include "nsINode.h" // for nsINode, etc. +#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc. +#include "nsIPresShell.h" // for nsIPresShell +#include "nsISelectionController.h" // for nsISelectionController, etc. +#include "nsISelectionDisplay.h" // for nsISelectionDisplay, etc. +#include "nsISupportsBase.h" // for nsISupports +#include "nsISupportsUtils.h" // for NS_ADDREF, NS_IF_ADDREF +#include "nsITransaction.h" // for nsITransaction +#include "nsITransactionManager.h" +#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 nsAFlatString +#include "nsStyleConsts.h" // for NS_STYLE_DIRECTION_RTL, etc. +#include "nsStyleContext.h" // for nsStyleContext +#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 "nsTransactionManager.h" // for nsTransactionManager +#include "prtime.h" // for PR_Now + +class nsIOutputStream; +class nsIParserService; +class nsITransferable; + +#ifdef DEBUG +#include "nsIDOMHTMLDocument.h" // for nsIDOMHTMLDocument +#endif + +// Defined in nsEditorRegistration.cpp +extern nsIParserService *sParserService; + +namespace mozilla { + +using namespace dom; +using namespace widget; + +/***************************************************************************** + * mozilla::EditorBase + *****************************************************************************/ + +EditorBase::EditorBase() + : mPlaceHolderName(nullptr) + , mSelState(nullptr) + , mPhonetic(nullptr) + , mModCount(0) + , mFlags(0) + , mUpdateCount(0) + , mPlaceHolderBatch(0) + , mAction(EditAction::none) + , mIMETextOffset(0) + , mIMETextLength(0) + , mDirection(eNone) + , mDocDirtyState(-1) + , mSpellcheckCheckboxState(eTriUnset) + , mShouldTxnSetSelection(true) + , mDidPreDestroy(false) + , mDidPostCreate(false) + , mDispatchInputEvent(true) + , mIsInEditAction(false) + , mHidingCaret(false) +{ +} + +EditorBase::~EditorBase() +{ + NS_ASSERTION(!mDocWeak || 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); + mTxnMgr = nullptr; + + delete mPhonetic; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(EditorBase) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EditorBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSpellChecker) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTxnMgr) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIMETextNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionListeners) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorObservers) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocStateListeners) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventListener) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSavedSel); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRangeUpdater); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditorBase) + nsIDocument* 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(mInlineSpellChecker) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTxnMgr) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMETextNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionListeners) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorObservers) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocStateListeners) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSavedSel); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRangeUpdater); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditorBase) + NS_INTERFACE_MAP_ENTRY(nsIPhonetic) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIEditorIMESupport) + 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) + + +NS_IMETHODIMP +EditorBase::Init(nsIDOMDocument* aDoc, + nsIContent* aRoot, + nsISelectionController* aSelCon, + uint32_t aFlags, + const nsAString& aValue) +{ + MOZ_ASSERT(mAction == EditAction::none, + "Initializing during an edit action is an error"); + MOZ_ASSERT(aDoc); + if (!aDoc) + return NS_ERROR_NULL_POINTER; + + // First only set flags, but other stuff shouldn't be initialized now. + // Don't move this call after initializing mDocWeak. + // SetFlags() can check whether it's called during initialization or not by + // them. Note that SetFlags() will be called by PostCreate(). +#ifdef DEBUG + nsresult rv = +#endif + SetFlags(aFlags); + NS_ASSERTION(NS_SUCCEEDED(rv), "SetFlags() failed"); + + mDocWeak = do_GetWeakReference(aDoc); // weak reference to doc + // HTML editors currently don't have their own selection controller, + // so they'll pass null as aSelCon, and we'll get the selection controller + // off of the presshell. + nsCOMPtr<nsISelectionController> selCon; + if (aSelCon) { + mSelConWeak = do_GetWeakReference(aSelCon); // weak reference to selectioncontroller + selCon = aSelCon; + } else { + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + selCon = do_QueryInterface(presShell); + } + NS_ASSERTION(selCon, "Selection controller should be available at this point"); + + //set up root element if we are passed one. + if (aRoot) + mRootElement = do_QueryInterface(aRoot); + + mUpdateCount=0; + + // If this is an editor for <input> or <textarea>, mIMETextNode is always + // recreated with same content. Therefore, we need to forget mIMETextNode, + // but we need to keep storing mIMETextOffset and mIMETextLength becuase + // they are necessary to restore IME selection and replacing composing string + // when this receives eCompositionChange event next time. + if (mIMETextNode && !mIMETextNode->IsInComposedDoc()) { + mIMETextNode = nullptr; + } + + /* Show the caret */ + selCon->SetCaretReadOnly(false); + selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON); + + selCon->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);//we want to see all the selection reflected to user + + NS_POSTCONDITION(mDocWeak, "bad state"); + + // Make sure that the editor will be destroyed properly + mDidPreDestroy = false; + // Make sure that the ediotr will be created properly + mDidPostCreate = false; + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::PostCreate() +{ + // 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); + NS_ENSURE_SUCCESS(rv, rv); + + // These operations only need to happen on the first PostCreate call + if (!mDidPostCreate) { + mDidPostCreate = true; + + // Set up listeners + CreateEventListeners(); + rv = InstallEventListeners(); + NS_ENSURE_SUCCESS(rv, rv); + + // nuke the modification count, so the doc appears unmodified + // do this before we notify listeners + ResetModificationCount(); + + // update the UI with our state + NotifyDocumentListeners(eDocumentCreated); + NotifyDocumentListeners(eDocumentStateChanged); + } + + // update nsTextStateManager and caret if we have focus + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (focusedContent) { + nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(focusedContent); + if (target) { + InitializeSelection(target); + } + + // 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. + EditorEventListener* listener = + reinterpret_cast<EditorEventListener*>(mEventListener.get()); + listener->SpellCheckIfNeeded(); + + IMEState newState; + rv = GetPreferredIMEState(&newState); + NS_ENSURE_SUCCESS(rv, NS_OK); + nsCOMPtr<nsIContent> content = GetFocusedContentForIME(); + IMEStateManager::UpdateIMEState(newState, content, *this); + } + + // FYI: This call might cause destroying this editor. + IMEStateManager::OnEditorInitialized(this); + + return NS_OK; +} + +void +EditorBase::CreateEventListeners() +{ + // Don't create the handler twice + if (!mEventListener) { + mEventListener = new EditorEventListener(); + } +} + +nsresult +EditorBase::InstallEventListeners() +{ + NS_ENSURE_TRUE(mDocWeak && mEventListener, + NS_ERROR_NOT_INITIALIZED); + + // Initialize the event target. + nsCOMPtr<nsIContent> rootContent = GetRoot(); + NS_ENSURE_TRUE(rootContent, NS_ERROR_NOT_AVAILABLE); + mEventTarget = do_QueryInterface(rootContent->GetParent()); + NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_AVAILABLE); + + EditorEventListener* listener = + reinterpret_cast<EditorEventListener*>(mEventListener.get()); + nsresult rv = listener->Connect(this); + if (mComposition) { + // Restart to handle composition with new editor contents. + mComposition->StartHandlingComposition(this); + } + return rv; +} + +void +EditorBase::RemoveEventListeners() +{ + if (!mDocWeak || !mEventListener) { + return; + } + reinterpret_cast<EditorEventListener*>(mEventListener.get())->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::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; + } + + nsCOMPtr<nsIPresShell> 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; + } + + nsCOMPtr<nsIDOMHTMLElement> element = do_QueryInterface(content); + if (!element) { + return false; + } + + if (!IsPlaintextEditor()) { + // 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. + nsCOMPtr<nsIHTMLDocument> doc = do_QueryInterface(content->GetUncomposedDoc()); + return doc && doc->IsEditingOn(); + } + + bool enable; + element->GetSpellcheck(&enable); + + return enable; +} + +NS_IMETHODIMP +EditorBase::PreDestroy(bool aDestroyingFrames) +{ + if (mDidPreDestroy) + return NS_OK; + + 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) + mInlineSpellChecker->Cleanup(aDestroyingFrames); + + // tell our listeners that the doc is going away + NotifyDocumentListeners(eDocumentToBeDestroyed); + + // Unregister event listeners + RemoveEventListeners(); + // If this editor is still hiding the caret, we need to restore it. + HideCaret(false); + mActionListeners.Clear(); + mEditorObservers.Clear(); + mDocStateListeners.Clear(); + mInlineSpellChecker = nullptr; + mSpellcheckCheckboxState = eTriUnset; + mRootElement = nullptr; + + mDidPreDestroy = true; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetFlags(uint32_t* aFlags) +{ + *aFlags = mFlags; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetFlags(uint32_t aFlags) +{ + if (mFlags == aFlags) { + return NS_OK; + } + + bool spellcheckerWasEnabled = CanEnableSpellCheck(); + mFlags = aFlags; + + if (!mDocWeak) { + // 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) { + nsresult rv = SyncRealTimeSpell(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // 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. + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (focusedContent) { + IMEState newState; + nsresult rv = GetPreferredIMEState(&newState); + if (NS_SUCCEEDED(rv)) { + // NOTE: When the enabled state isn't going to be modified, this method + // is going to do nothing. + nsCOMPtr<nsIContent> content = GetFocusedContentForIME(); + IMEStateManager::UpdateIMEState(newState, content, *this); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetIsSelectionEditable(bool* aIsSelectionEditable) +{ + NS_ENSURE_ARG_POINTER(aIsSelectionEditable); + + // get current selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // XXX we just check that the anchor node is editable at the moment + // we should check that all nodes in the selection are editable + nsCOMPtr<nsINode> anchorNode = selection->GetAnchorNode(); + *aIsSelectionEditable = anchorNode && IsEditable(anchorNode); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetIsDocumentEditable(bool* aIsDocumentEditable) +{ + NS_ENSURE_ARG_POINTER(aIsDocumentEditable); + nsCOMPtr<nsIDocument> doc = GetDocument(); + *aIsDocumentEditable = !!doc; + + return NS_OK; +} + +already_AddRefed<nsIDocument> +EditorBase::GetDocument() +{ + NS_PRECONDITION(mDocWeak, "bad state, mDocWeak weak pointer not initialized"); + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + return doc.forget(); +} + +already_AddRefed<nsIDOMDocument> +EditorBase::GetDOMDocument() +{ + NS_PRECONDITION(mDocWeak, "bad state, mDocWeak weak pointer not initialized"); + nsCOMPtr<nsIDOMDocument> doc = do_QueryReferent(mDocWeak); + return doc.forget(); +} + +NS_IMETHODIMP +EditorBase::GetDocument(nsIDOMDocument** aDoc) +{ + *aDoc = GetDOMDocument().take(); + return *aDoc ? NS_OK : NS_ERROR_NOT_INITIALIZED; +} + +already_AddRefed<nsIPresShell> +EditorBase::GetPresShell() +{ + NS_PRECONDITION(mDocWeak, "bad state, null mDocWeak"); + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, nullptr); + nsCOMPtr<nsIPresShell> ps = doc->GetShell(); + return ps.forget(); +} + +already_AddRefed<nsIWidget> +EditorBase::GetWidget() +{ + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, nullptr); + nsPresContext* pc = ps->GetPresContext(); + NS_ENSURE_TRUE(pc, nullptr); + nsCOMPtr<nsIWidget> widget = pc->GetRootWidget(); + NS_ENSURE_TRUE(widget.get(), nullptr); + return widget.forget(); +} + +NS_IMETHODIMP +EditorBase::GetContentsMIMEType(char** aContentsMIMEType) +{ + NS_ENSURE_ARG_POINTER(aContentsMIMEType); + *aContentsMIMEType = ToNewCString(mContentMIMEType); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetContentsMIMEType(const char* aContentsMIMEType) +{ + mContentMIMEType.Assign(aContentsMIMEType ? aContentsMIMEType : ""); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetSelectionController(nsISelectionController** aSel) +{ + NS_ENSURE_TRUE(aSel, NS_ERROR_NULL_POINTER); + *aSel = nullptr; // init out param + nsCOMPtr<nsISelectionController> selCon; + if (mSelConWeak) { + selCon = do_QueryReferent(mSelConWeak); + } else { + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + selCon = do_QueryInterface(presShell); + } + if (!selCon) { + return NS_ERROR_NOT_INITIALIZED; + } + NS_ADDREF(*aSel = selCon); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::DeleteSelection(EDirection aAction, + EStripWrappers aStripWrappers) +{ + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + return DeleteSelectionImpl(aAction, aStripWrappers); +} + +NS_IMETHODIMP +EditorBase::GetSelection(nsISelection** aSelection) +{ + return GetSelection(SelectionType::eNormal, aSelection); +} + +nsresult +EditorBase::GetSelection(SelectionType aSelectionType, + nsISelection** aSelection) +{ + NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); + *aSelection = nullptr; + nsCOMPtr<nsISelectionController> selcon; + GetSelectionController(getter_AddRefs(selcon)); + if (!selcon) { + return NS_ERROR_NOT_INITIALIZED; + } + return selcon->GetSelection(ToRawSelectionType(aSelectionType), aSelection); +} + +Selection* +EditorBase::GetSelection(SelectionType aSelectionType) +{ + nsCOMPtr<nsISelection> sel; + nsresult rv = GetSelection(aSelectionType, getter_AddRefs(sel)); + if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!sel)) { + return nullptr; + } + + return sel->AsSelection(); +} + +NS_IMETHODIMP +EditorBase::DoTransaction(nsITransaction* aTxn) +{ + if (mPlaceHolderBatch && !mPlaceHolderTxn) { + nsCOMPtr<nsIAbsorbingTransaction> placeholderTransaction = + new PlaceholderTransaction(); + + // Save off weak reference to placeholder transaction + mPlaceHolderTxn = do_GetWeakReference(placeholderTransaction); + placeholderTransaction->Init(mPlaceHolderName, mSelState, this); + // placeholder txn took ownership of this pointer + mSelState = nullptr; + + // QI to an nsITransaction since that's what DoTransaction() expects + nsCOMPtr<nsITransaction> transaction = + do_QueryInterface(placeholderTransaction); + // We will recurse, but will not hit this case in the nested call + DoTransaction(transaction); + + if (mTxnMgr) { + nsCOMPtr<nsITransaction> topTxn = mTxnMgr->PeekUndoStack(); + if (topTxn) { + placeholderTransaction = do_QueryInterface(topTxn); + if (placeholderTransaction) { + // 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. + mPlaceHolderTxn = do_GetWeakReference(placeholderTransaction); + } + } + } + } + + if (aTxn) { + // 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 + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + selection->StartBatchChanges(); + + nsresult rv; + if (mTxnMgr) { + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + rv = txnMgr->DoTransaction(aTxn); + } else { + rv = aTxn->DoTransaction(); + } + if (NS_SUCCEEDED(rv)) { + DoAfterDoTransaction(aTxn); + } + + // no need to check rv here, don't lose result of operation + selection->EndBatchChanges(); + + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::EnableUndo(bool aEnable) +{ + if (aEnable) { + if (!mTxnMgr) { + mTxnMgr = new nsTransactionManager(); + } + mTxnMgr->SetMaxTransactionCount(-1); + } else if (mTxnMgr) { + // disable the transaction manager if it is enabled + mTxnMgr->Clear(); + mTxnMgr->SetMaxTransactionCount(0); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetNumberOfUndoItems(int32_t* aNumItems) +{ + *aNumItems = 0; + return mTxnMgr ? mTxnMgr->GetNumberOfUndoItems(aNumItems) : NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetNumberOfRedoItems(int32_t* aNumItems) +{ + *aNumItems = 0; + return mTxnMgr ? mTxnMgr->GetNumberOfRedoItems(aNumItems) : NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetTransactionManager(nsITransactionManager** aTxnManager) +{ + NS_ENSURE_ARG_POINTER(aTxnManager); + + *aTxnManager = nullptr; + NS_ENSURE_TRUE(mTxnMgr, NS_ERROR_FAILURE); + + NS_ADDREF(*aTxnManager = mTxnMgr); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetTransactionManager(nsITransactionManager* aTxnManager) +{ + NS_ENSURE_TRUE(aTxnManager, NS_ERROR_FAILURE); + + // nsITransactionManager is builtinclass, so this is safe + mTxnMgr = static_cast<nsTransactionManager*>(aTxnManager); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::Undo(uint32_t aCount) +{ + ForceCompositionEnd(); + + bool hasTxnMgr, hasTransaction = false; + CanUndo(&hasTxnMgr, &hasTransaction); + NS_ENSURE_TRUE(hasTransaction, NS_OK); + + AutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone); + + if (!mTxnMgr) { + return NS_OK; + } + + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + for (uint32_t i = 0; i < aCount; ++i) { + nsresult rv = txnMgr->UndoTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + DoAfterUndoTransaction(); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::CanUndo(bool* aIsEnabled, + bool* aCanUndo) +{ + NS_ENSURE_TRUE(aIsEnabled && aCanUndo, NS_ERROR_NULL_POINTER); + *aIsEnabled = !!mTxnMgr; + if (*aIsEnabled) { + int32_t numTxns = 0; + mTxnMgr->GetNumberOfUndoItems(&numTxns); + *aCanUndo = !!numTxns; + } else { + *aCanUndo = false; + } + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::Redo(uint32_t aCount) +{ + bool hasTxnMgr, hasTransaction = false; + CanRedo(&hasTxnMgr, &hasTransaction); + NS_ENSURE_TRUE(hasTransaction, NS_OK); + + AutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone); + + if (!mTxnMgr) { + return NS_OK; + } + + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + for (uint32_t i = 0; i < aCount; ++i) { + nsresult rv = txnMgr->RedoTransaction(); + NS_ENSURE_SUCCESS(rv, rv); + + DoAfterRedoTransaction(); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::CanRedo(bool* aIsEnabled, bool* aCanRedo) +{ + NS_ENSURE_TRUE(aIsEnabled && aCanRedo, NS_ERROR_NULL_POINTER); + + *aIsEnabled = !!mTxnMgr; + if (*aIsEnabled) { + int32_t numTxns = 0; + mTxnMgr->GetNumberOfRedoItems(&numTxns); + *aCanRedo = !!numTxns; + } else { + *aCanRedo = false; + } + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::BeginTransaction() +{ + BeginUpdateViewBatch(); + + if (mTxnMgr) { + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + txnMgr->BeginBatch(nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::EndTransaction() +{ + if (mTxnMgr) { + RefPtr<nsTransactionManager> txnMgr = mTxnMgr; + txnMgr->EndBatch(false); + } + + EndUpdateViewBatch(); + + return NS_OK; +} + + +// These two routines are similar to the above, but do not use +// the transaction managers batching feature. Instead we use +// a placeholder transaction to wrap up any further transaction +// while the batch is open. The advantage of this is that +// placeholder transactions can later merge, if needed. Merging +// is unavailable between transaction manager batches. + +NS_IMETHODIMP +EditorBase::BeginPlaceHolderTransaction(nsIAtom* aName) +{ + NS_PRECONDITION(mPlaceHolderBatch >= 0, "negative placeholder batch count!"); + if (!mPlaceHolderBatch) { + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + // time to turn on the batch + BeginUpdateViewBatch(); + mPlaceHolderTxn = nullptr; + mPlaceHolderName = aName; + RefPtr<Selection> selection = GetSelection(); + if (selection) { + mSelState = new SelectionState(); + mSelState->SaveSelection(selection); + // 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) { + mRangeUpdater.RegisterSelectionState(*mSelState); + } + } + } + mPlaceHolderBatch++; + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::EndPlaceHolderTransaction() +{ + NS_PRECONDITION(mPlaceHolderBatch > 0, "zero or negative placeholder batch count when ending batch!"); + if (mPlaceHolderBatch == 1) { + RefPtr<Selection> selection = GetSelection(); + + // By making the assumption that no reflow happens during the calls + // to EndUpdateViewBatch and ScrollSelectionIntoView, 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. + if (selection) { + selection->SetCanCacheFrameOffset(true); + } + + { + // Hide the caret here to avoid hiding it twice, once in EndUpdateViewBatch + // and once in ScrollSelectionIntoView. + RefPtr<nsCaret> caret; + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + + if (presShell) { + caret = presShell->GetCaret(); + } + + // time to turn off the batch + EndUpdateViewBatch(); + // make sure selection is in view + + // After ScrollSelectionIntoView(), the pending notifications might be + // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. + ScrollSelectionIntoView(false); + } + + // cached for frame offset are Not available now + if (selection) { + selection->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) { + mRangeUpdater.DropSelectionState(*mSelState); + } + delete mSelState; + mSelState = nullptr; + } + // We might have never made a placeholder if no action took place. + if (mPlaceHolderTxn) { + nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryReferent(mPlaceHolderTxn); + if (plcTxn) { + plcTxn->EndPlaceHolderBatch(); + } else { + // in the future we will check to make sure undo is off here, + // since that is the only known case where the placeholdertxn would disappear on us. + // For now just removing the assert. + } + // notify editor observers of action but if composing, it's done by + // compositionchange event handler. + if (!mComposition) { + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + } + } else { + NotifyEditorObservers(eNotifyEditorObserversOfCancel); + } + } + mPlaceHolderBatch--; + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::ShouldTxnSetSelection(bool* aResult) +{ + NS_ENSURE_TRUE(aResult, NS_ERROR_NULL_POINTER); + *aResult = mShouldTxnSetSelection; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetShouldTxnSetSelection(bool aShould) +{ + mShouldTxnSetSelection = aShould; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetDocumentIsEmpty(bool* aDocumentIsEmpty) +{ + *aDocumentIsEmpty = true; + + dom::Element* root = GetRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER); + + *aDocumentIsEmpty = !root->HasChildren(); + 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() +{ + if (!mDocWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + ForceCompositionEnd(); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + return SelectEntireDocument(selection); +} + +NS_IMETHODIMP +EditorBase::BeginningOfDocument() +{ + if (!mDocWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + + // get the selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); + + // get the root element + dom::Element* rootElement = GetRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER); + + // find first editable thingy + nsCOMPtr<nsINode> firstNode = GetFirstEditableNode(rootElement); + if (!firstNode) { + // just the root node, set selection to inside the root + return selection->CollapseNative(rootElement, 0); + } + + if (firstNode->NodeType() == nsIDOMNode::TEXT_NODE) { + // If firstNode is text, set selection to beginning of the text node. + return selection->CollapseNative(firstNode, 0); + } + + // Otherwise, it's a leaf node and we set the selection just in front of it. + nsCOMPtr<nsIContent> parent = firstNode->GetParent(); + if (!parent) { + return NS_ERROR_NULL_POINTER; + } + + int32_t offsetInParent = parent->IndexOf(firstNode); + return selection->CollapseNative(parent, offsetInParent); +} + +NS_IMETHODIMP +EditorBase::EndOfDocument() +{ + NS_ENSURE_TRUE(mDocWeak, NS_ERROR_NOT_INITIALIZED); + + // get selection + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + // get the root element + nsINode* node = GetRoot(); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + nsINode* child = node->GetLastChild(); + + while (child && IsContainer(child->AsDOMNode())) { + node = child; + child = node->GetLastChild(); + } + + uint32_t length = node->Length(); + return selection->CollapseNative(node, int32_t(length)); +} + +NS_IMETHODIMP +EditorBase::GetDocumentModified(bool* outDocModified) +{ + NS_ENSURE_TRUE(outDocModified, NS_ERROR_NULL_POINTER); + + int32_t modCount = 0; + GetModificationCount(&modCount); + + *outDocModified = (modCount != 0); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetDocumentCharacterSet(nsACString& characterSet) +{ + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED); + + characterSet = doc->GetDocumentCharacterSet(); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetDocumentCharacterSet(const nsACString& characterSet) +{ + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_UNEXPECTED); + + doc->SetDocumentCharacterSet(characterSet); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::Cut() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanCut(bool* aCanCut) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::Copy() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanCopy(bool* aCanCut) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanDelete(bool* aCanDelete) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::Paste(int32_t aSelectionType) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::PasteTransferable(nsITransferable* aTransferable) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanPaste(int32_t aSelectionType, bool* aCanPaste) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::CanPasteTransferable(nsITransferable* aTransferable, + bool* aCanPaste) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::SetAttribute(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute); + + RefPtr<ChangeAttributeTransaction> transaction = + CreateTxnForSetAttribute(*element, *attribute, aValue); + return DoTransaction(transaction); +} + +NS_IMETHODIMP +EditorBase::GetAttributeValue(nsIDOMElement* aElement, + const nsAString& aAttribute, + nsAString& aResultValue, + bool* aResultIsSet) +{ + NS_ENSURE_TRUE(aResultIsSet, NS_ERROR_NULL_POINTER); + *aResultIsSet = false; + if (!aElement) { + return NS_OK; + } + nsAutoString value; + nsresult rv = aElement->GetAttribute(aAttribute, value); + NS_ENSURE_SUCCESS(rv, rv); + if (!DOMStringIsNull(value)) { + *aResultIsSet = true; + aResultValue = value; + } + return rv; +} + +NS_IMETHODIMP +EditorBase::RemoveAttribute(nsIDOMElement* aElement, + const nsAString& aAttribute) +{ + nsCOMPtr<Element> element = do_QueryInterface(aElement); + NS_ENSURE_TRUE(element, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIAtom> attribute = NS_Atomize(aAttribute); + + RefPtr<ChangeAttributeTransaction> transaction = + CreateTxnForRemoveAttribute(*element, *attribute); + return DoTransaction(transaction); +} + +bool +EditorBase::OutputsMozDirty() +{ + // Return true for Composer (!eEditorAllowInteraction) or mail + // (eEditorMailMask), but false for webpages. + return !(mFlags & nsIPlaintextEditor::eEditorAllowInteraction) || + (mFlags & nsIPlaintextEditor::eEditorMailMask); +} + +NS_IMETHODIMP +EditorBase::MarkNodeDirty(nsIDOMNode* aNode) +{ + // Mark the node dirty, but not for webpages (bug 599983) + if (!OutputsMozDirty()) { + return NS_OK; + } + nsCOMPtr<dom::Element> element = do_QueryInterface(aNode); + if (element) { + element->SetAttr(kNameSpaceID_None, nsGkAtoms::mozdirty, + EmptyString(), false); + } + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetInlineSpellChecker(bool autoCreate, + nsIInlineSpellChecker** aInlineSpellChecker) +{ + NS_ENSURE_ARG_POINTER(aInlineSpellChecker); + + if (mDidPreDestroy) { + // Don't allow people to get or create the spell checker once the editor + // is going away. + *aInlineSpellChecker = nullptr; + return autoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK; + } + + // We don't want to show the spell checking UI if there are no spell check dictionaries available. + bool canSpell = mozInlineSpellChecker::CanEnableInlineSpellChecking(); + if (!canSpell) { + *aInlineSpellChecker = nullptr; + return NS_ERROR_FAILURE; + } + + nsresult rv; + if (!mInlineSpellChecker && autoCreate) { + mInlineSpellChecker = do_CreateInstance(MOZ_INLINESPELLCHECKER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mInlineSpellChecker) { + rv = mInlineSpellChecker->Init(this); + if (NS_FAILED(rv)) { + mInlineSpellChecker = nullptr; + } + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_IF_ADDREF(*aInlineSpellChecker = mInlineSpellChecker); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SyncRealTimeSpell() +{ + bool enable = GetDesiredSpellCheckState(); + + // Initializes mInlineSpellChecker + nsCOMPtr<nsIInlineSpellChecker> spellChecker; + GetInlineSpellChecker(enable, getter_AddRefs(spellChecker)); + + if (mInlineSpellChecker) { + // 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 + mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetSpellcheckUserOverride(bool enable) +{ + mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse; + + return SyncRealTimeSpell(); +} + +NS_IMETHODIMP +EditorBase::CreateNode(const nsAString& aTag, + nsIDOMNode* aParent, + int32_t aPosition, + nsIDOMNode** aNewNode) +{ + nsCOMPtr<nsIAtom> tag = NS_Atomize(aTag); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + NS_ENSURE_STATE(parent); + *aNewNode = GetAsDOMNode(CreateNode(tag, parent, aPosition).take()); + NS_ENSURE_STATE(*aNewNode); + return NS_OK; +} + +already_AddRefed<Element> +EditorBase::CreateNode(nsIAtom* aTag, + nsINode* aParent, + int32_t aPosition) +{ + MOZ_ASSERT(aTag && aParent); + + AutoRules beginRulesSniffing(this, EditAction::createNode, nsIEditor::eNext); + + for (auto& listener : mActionListeners) { + listener->WillCreateNode(nsDependentAtomString(aTag), + GetAsDOMNode(aParent), aPosition); + } + + nsCOMPtr<Element> ret; + + RefPtr<CreateElementTransaction> transaction = + CreateTxnForCreateElement(*aTag, *aParent, aPosition); + nsresult rv = DoTransaction(transaction); + if (NS_SUCCEEDED(rv)) { + ret = transaction->GetNewNode(); + MOZ_ASSERT(ret); + } + + mRangeUpdater.SelAdjCreateNode(aParent, aPosition); + + for (auto& listener : mActionListeners) { + listener->DidCreateNode(nsDependentAtomString(aTag), GetAsDOMNode(ret), + GetAsDOMNode(aParent), aPosition, rv); + } + + return ret.forget(); +} + +NS_IMETHODIMP +EditorBase::InsertNode(nsIDOMNode* aNode, + nsIDOMNode* aParent, + int32_t aPosition) +{ + nsCOMPtr<nsIContent> node = do_QueryInterface(aNode); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + NS_ENSURE_TRUE(node && parent, NS_ERROR_NULL_POINTER); + + return InsertNode(*node, *parent, aPosition); +} + +nsresult +EditorBase::InsertNode(nsIContent& aNode, + nsINode& aParent, + int32_t aPosition) +{ + AutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); + + for (auto& listener : mActionListeners) { + listener->WillInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(), + aPosition); + } + + RefPtr<InsertNodeTransaction> transaction = + CreateTxnForInsertNode(aNode, aParent, aPosition); + nsresult rv = DoTransaction(transaction); + + mRangeUpdater.SelAdjInsertNode(aParent.AsDOMNode(), aPosition); + + for (auto& listener : mActionListeners) { + listener->DidInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(), aPosition, + rv); + } + + return rv; +} + +NS_IMETHODIMP +EditorBase::SplitNode(nsIDOMNode* aNode, + int32_t aOffset, + nsIDOMNode** aNewLeftNode) +{ + nsCOMPtr<nsIContent> node = do_QueryInterface(aNode); + NS_ENSURE_STATE(node); + ErrorResult rv; + nsCOMPtr<nsIContent> newNode = SplitNode(*node, aOffset, rv); + *aNewLeftNode = GetAsDOMNode(newNode.forget().take()); + return rv.StealNSResult(); +} + +nsIContent* +EditorBase::SplitNode(nsIContent& aNode, + int32_t aOffset, + ErrorResult& aResult) +{ + AutoRules beginRulesSniffing(this, EditAction::splitNode, nsIEditor::eNext); + + for (auto& listener : mActionListeners) { + listener->WillSplitNode(aNode.AsDOMNode(), aOffset); + } + + RefPtr<SplitNodeTransaction> transaction = + CreateTxnForSplitNode(aNode, aOffset); + aResult = DoTransaction(transaction); + + nsCOMPtr<nsIContent> newNode = aResult.Failed() ? nullptr + : transaction->GetNewNode(); + + mRangeUpdater.SelAdjSplitNode(aNode, aOffset, newNode); + + nsresult rv = aResult.StealNSResult(); + for (auto& listener : mActionListeners) { + listener->DidSplitNode(aNode.AsDOMNode(), aOffset, GetAsDOMNode(newNode), + rv); + } + // Note: result might be a success code, so we can't use Throw() to + // set it on aResult. + aResult = rv; + + return newNode; +} + +NS_IMETHODIMP +EditorBase::JoinNodes(nsIDOMNode* aLeftNode, + nsIDOMNode* aRightNode, + nsIDOMNode*) +{ + nsCOMPtr<nsINode> leftNode = do_QueryInterface(aLeftNode); + nsCOMPtr<nsINode> rightNode = do_QueryInterface(aRightNode); + NS_ENSURE_STATE(leftNode && rightNode && leftNode->GetParentNode()); + return JoinNodes(*leftNode, *rightNode); +} + +nsresult +EditorBase::JoinNodes(nsINode& aLeftNode, + nsINode& aRightNode) +{ + nsCOMPtr<nsINode> parent = aLeftNode.GetParentNode(); + MOZ_ASSERT(parent); + + AutoRules beginRulesSniffing(this, EditAction::joinNode, + nsIEditor::ePrevious); + + // Remember some values; later used for saved selection updating. + // Find the offset between the nodes to be joined. + int32_t offset = parent->IndexOf(&aRightNode); + // Find the number of children of the lefthand node + uint32_t oldLeftNodeLen = aLeftNode.Length(); + + for (auto& listener : mActionListeners) { + listener->WillJoinNodes(aLeftNode.AsDOMNode(), aRightNode.AsDOMNode(), + parent->AsDOMNode()); + } + + nsresult rv = NS_OK; + RefPtr<JoinNodeTransaction> transaction = + CreateTxnForJoinNode(aLeftNode, aRightNode); + if (transaction) { + rv = DoTransaction(transaction); + } + + mRangeUpdater.SelAdjJoinNodes(aLeftNode, aRightNode, *parent, offset, + (int32_t)oldLeftNodeLen); + + for (auto& listener : mActionListeners) { + listener->DidJoinNodes(aLeftNode.AsDOMNode(), aRightNode.AsDOMNode(), + parent->AsDOMNode(), rv); + } + + return rv; +} + +NS_IMETHODIMP +EditorBase::DeleteNode(nsIDOMNode* aNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_STATE(node); + return DeleteNode(node); +} + +nsresult +EditorBase::DeleteNode(nsINode* aNode) +{ + AutoRules beginRulesSniffing(this, EditAction::createNode, + nsIEditor::ePrevious); + + // save node location for selection updating code. + for (auto& listener : mActionListeners) { + listener->WillDeleteNode(aNode->AsDOMNode()); + } + + RefPtr<DeleteNodeTransaction> transaction; + nsresult rv = CreateTxnForDeleteNode(aNode, getter_AddRefs(transaction)); + if (NS_SUCCEEDED(rv)) { + rv = DoTransaction(transaction); + } + + for (auto& listener : mActionListeners) { + listener->DidDeleteNode(aNode->AsDOMNode(), rv); + } + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +/** + * ReplaceContainer() replaces inNode with a new node (outNode) which is + * constructed to be of type aNodeType. Put inNodes children into outNode. + * Callers responsibility to make sure inNode's children can go in outNode. + */ +already_AddRefed<Element> +EditorBase::ReplaceContainer(Element* aOldContainer, + nsIAtom* aNodeType, + nsIAtom* aAttribute, + const nsAString* aValue, + ECloneAttributes aCloneAttributes) +{ + MOZ_ASSERT(aOldContainer && aNodeType); + + nsCOMPtr<nsIContent> parent = aOldContainer->GetParent(); + NS_ENSURE_TRUE(parent, nullptr); + + int32_t offset = parent->IndexOf(aOldContainer); + + // create new container + nsCOMPtr<Element> ret = CreateHTMLContent(aNodeType); + NS_ENSURE_TRUE(ret, nullptr); + + // set attribute if needed + if (aAttribute && aValue && aAttribute != nsGkAtoms::_empty) { + nsresult rv = ret->SetAttr(kNameSpaceID_None, aAttribute, *aValue, true); + NS_ENSURE_SUCCESS(rv, nullptr); + } + if (aCloneAttributes == eCloneAttributes) { + CloneAttributes(ret, aOldContainer); + } + + // notify our internal selection state listener + // (Note: An AutoSelectionRestorer object must be created + // before calling this to initialize mRangeUpdater) + AutoReplaceContainerSelNotify selStateNotify(mRangeUpdater, aOldContainer, + ret); + { + AutoTransactionsConserveSelection conserveSelection(this); + while (aOldContainer->HasChildren()) { + nsCOMPtr<nsIContent> child = aOldContainer->GetFirstChild(); + + nsresult rv = DeleteNode(child); + NS_ENSURE_SUCCESS(rv, nullptr); + + rv = InsertNode(*child, *ret, -1); + NS_ENSURE_SUCCESS(rv, nullptr); + } + } + + // insert new container into tree + nsresult rv = InsertNode(*ret, *parent, offset); + NS_ENSURE_SUCCESS(rv, nullptr); + + // delete old container + rv = DeleteNode(aOldContainer); + NS_ENSURE_SUCCESS(rv, nullptr); + + return ret.forget(); +} + +/** + * RemoveContainer() removes inNode, reparenting its children (if any) into the + * parent of inNode. + */ +nsresult +EditorBase::RemoveContainer(nsIContent* aNode) +{ + MOZ_ASSERT(aNode); + + nsCOMPtr<nsINode> parent = aNode->GetParentNode(); + NS_ENSURE_STATE(parent); + + int32_t offset = parent->IndexOf(aNode); + + // Loop through the children of inNode and promote them into inNode's parent + uint32_t nodeOrigLen = aNode->GetChildCount(); + + // notify our internal selection state listener + AutoRemoveContainerSelNotify selNotify(mRangeUpdater, aNode, parent, + offset, nodeOrigLen); + + while (aNode->HasChildren()) { + nsCOMPtr<nsIContent> child = aNode->GetLastChild(); + nsresult rv = DeleteNode(child); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InsertNode(*child, *parent, offset); + NS_ENSURE_SUCCESS(rv, rv); + } + + return DeleteNode(aNode); +} + +/** + * InsertContainerAbove() inserts a new parent for inNode, which is contructed + * to be of type aNodeType. outNode becomes a child of inNode's earlier + * parent. Caller's responsibility to make sure inNode's can be child of + * outNode, and outNode can be child of old parent. + */ +already_AddRefed<Element> +EditorBase::InsertContainerAbove(nsIContent* aNode, + nsIAtom* aNodeType, + nsIAtom* aAttribute, + const nsAString* aValue) +{ + MOZ_ASSERT(aNode && aNodeType); + + nsCOMPtr<nsIContent> parent = aNode->GetParent(); + NS_ENSURE_TRUE(parent, nullptr); + int32_t offset = parent->IndexOf(aNode); + + // Create new container + nsCOMPtr<Element> newContent = CreateHTMLContent(aNodeType); + NS_ENSURE_TRUE(newContent, nullptr); + + // Set attribute if needed + if (aAttribute && aValue && aAttribute != nsGkAtoms::_empty) { + nsresult rv = + newContent->SetAttr(kNameSpaceID_None, aAttribute, *aValue, true); + NS_ENSURE_SUCCESS(rv, nullptr); + } + + // Notify our internal selection state listener + AutoInsertContainerSelNotify selNotify(mRangeUpdater); + + // Put inNode in new parent, outNode + nsresult rv = DeleteNode(aNode); + NS_ENSURE_SUCCESS(rv, nullptr); + + { + AutoTransactionsConserveSelection conserveSelection(this); + rv = InsertNode(*aNode, *newContent, 0); + NS_ENSURE_SUCCESS(rv, nullptr); + } + + // Put new parent in doc + rv = InsertNode(*newContent, *parent, offset); + NS_ENSURE_SUCCESS(rv, nullptr); + + return newContent.forget(); +} + +/** + * MoveNode() moves aNode to {aParent,aOffset}. + */ +nsresult +EditorBase::MoveNode(nsIContent* aNode, + nsINode* aParent, + int32_t aOffset) +{ + MOZ_ASSERT(aNode); + MOZ_ASSERT(aParent); + MOZ_ASSERT(aOffset == -1 || + (0 <= aOffset && + AssertedCast<uint32_t>(aOffset) <= aParent->Length())); + + nsCOMPtr<nsINode> oldParent = aNode->GetParentNode(); + int32_t oldOffset = oldParent ? oldParent->IndexOf(aNode) : -1; + + if (aOffset == -1) { + // Magic value meaning "move to end of aParent" + aOffset = AssertedCast<int32_t>(aParent->Length()); + } + + // Don't do anything if it's already in right place + if (aParent == oldParent && aOffset == oldOffset) { + return NS_OK; + } + + // Notify our internal selection state listener + AutoMoveNodeSelNotify selNotify(mRangeUpdater, oldParent, oldOffset, + aParent, aOffset); + + // Need to adjust aOffset if we're moving aNode later in its current parent + if (aParent == oldParent && oldOffset < aOffset) { + // When we delete aNode, it will make the offsets after it off by one + aOffset--; + } + + // Hold a reference so aNode doesn't go away when we remove it (bug 772282) + nsCOMPtr<nsINode> kungFuDeathGrip = aNode; + + nsresult rv = DeleteNode(aNode); + NS_ENSURE_SUCCESS(rv, rv); + + return InsertNode(*aNode, *aParent, aOffset); +} + +NS_IMETHODIMP +EditorBase::AddEditorObserver(nsIEditorObserver* aObserver) +{ + // we don't keep ownership of the observers. They must + // remove themselves as observers before they are destroyed. + + NS_ENSURE_TRUE(aObserver, NS_ERROR_NULL_POINTER); + + // Make sure the listener isn't already on the list + if (!mEditorObservers.Contains(aObserver)) { + mEditorObservers.AppendElement(*aObserver); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::RemoveEditorObserver(nsIEditorObserver* aObserver) +{ + NS_ENSURE_TRUE(aObserver, NS_ERROR_FAILURE); + + mEditorObservers.RemoveElement(aObserver); + + return NS_OK; +} + +class EditorInputEventDispatcher final : public Runnable +{ +public: + EditorInputEventDispatcher(EditorBase* aEditorBase, + nsIContent* aTarget, + bool aIsComposing) + : mEditorBase(aEditorBase) + , mTarget(aTarget) + , mIsComposing(aIsComposing) + { + } + + NS_IMETHOD Run() override + { + // Note that we don't need to check mDispatchInputEvent here. We need + // to check it only when the editor requests to dispatch the input event. + + if (!mTarget->IsInComposedDoc()) { + return NS_OK; + } + + nsCOMPtr<nsIPresShell> ps = mEditorBase->GetPresShell(); + if (!ps) { + return NS_OK; + } + + nsCOMPtr<nsIWidget> widget = mEditorBase->GetWidget(); + if (!widget) { + return NS_OK; + } + + // Even if the change is caused by untrusted event, we need to dispatch + // trusted input event since it's a fact. + InternalEditorInputEvent inputEvent(true, eEditorInput, widget); + inputEvent.mTime = static_cast<uint64_t>(PR_Now() / 1000); + inputEvent.mIsComposing = mIsComposing; + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = + ps->HandleEventWithTarget(&inputEvent, nullptr, mTarget, &status); + NS_ENSURE_SUCCESS(rv, NS_OK); // print the warning if error + return NS_OK; + } + +private: + RefPtr<EditorBase> mEditorBase; + nsCOMPtr<nsIContent> mTarget; + bool mIsComposing; +}; + +void +EditorBase::NotifyEditorObservers(NotificationForEditorObservers aNotification) +{ + // Copy the observers since EditAction()s can modify mEditorObservers. + nsTArray<mozilla::OwningNonNull<nsIEditorObserver>> observers(mEditorObservers); + switch (aNotification) { + case eNotifyEditorObserversOfEnd: + mIsInEditAction = false; + for (auto& observer : observers) { + observer->EditAction(); + } + + if (!mDispatchInputEvent) { + return; + } + + FireInputEvent(); + break; + case eNotifyEditorObserversOfBefore: + if (NS_WARN_IF(mIsInEditAction)) { + break; + } + mIsInEditAction = true; + for (auto& observer : observers) { + observer->BeforeEditAction(); + } + break; + case eNotifyEditorObserversOfCancel: + mIsInEditAction = false; + for (auto& observer : observers) { + observer->CancelEditAction(); + } + break; + default: + MOZ_CRASH("Handle all notifications here"); + break; + } +} + +void +EditorBase::FireInputEvent() +{ + // 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. + + nsCOMPtr<nsIContent> target = GetInputEventTargetContent(); + NS_ENSURE_TRUE_VOID(target); + + // NOTE: Don't refer IsIMEComposing() because it returns false even before + // compositionend. However, DOM Level 3 Events defines it should be + // true after compositionstart and before compositionend. + nsContentUtils::AddScriptRunner( + new EditorInputEventDispatcher(this, target, !!GetComposition())); +} + +NS_IMETHODIMP +EditorBase::AddEditActionListener(nsIEditActionListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + // Make sure the listener isn't already on the list + if (!mActionListeners.Contains(aListener)) { + mActionListeners.AppendElement(*aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::RemoveEditActionListener(nsIEditActionListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE); + + mActionListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::AddDocumentStateListener(nsIDocumentStateListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + if (!mDocStateListeners.Contains(aListener)) { + mDocStateListeners.AppendElement(*aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::RemoveDocumentStateListener(nsIDocumentStateListener* aListener) +{ + NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER); + + mDocStateListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::OutputToString(const nsAString& aFormatType, + uint32_t aFlags, + nsAString& aOutputString) +{ + // these should be implemented by derived classes. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::OutputToStream(nsIOutputStream* aOutputStream, + const nsAString& aFormatType, + const nsACString& aCharsetOverride, + uint32_t aFlags) +{ + // these should be implemented by derived classes. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +EditorBase::DumpContentTree() +{ +#ifdef DEBUG + if (mRootElement) { + mRootElement->List(stdout); + } +#endif + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::DebugDumpContent() +{ +#ifdef DEBUG + nsCOMPtr<nsIDOMHTMLDocument> doc = do_QueryReferent(mDocWeak); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIDOMHTMLElement>bodyElem; + doc->GetBody(getter_AddRefs(bodyElem)); + nsCOMPtr<nsIContent> content = do_QueryInterface(bodyElem); + if (content) { + content->List(); + } +#endif + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::DebugUnitTests(int32_t* outNumTests, + int32_t* outNumTestsFailed) +{ +#ifdef DEBUG + NS_NOTREACHED("This should never get called. Overridden by subclasses"); +#endif + return NS_OK; +} + +bool +EditorBase::ArePreservingSelection() +{ + return !(mSavedSel.IsEmpty()); +} + +void +EditorBase::PreserveSelectionAcrossActions(Selection* aSel) +{ + mSavedSel.SaveSelection(aSel); + mRangeUpdater.RegisterSelectionState(mSavedSel); +} + +nsresult +EditorBase::RestorePreservedSelection(Selection* aSel) +{ + if (mSavedSel.IsEmpty()) { + return NS_ERROR_FAILURE; + } + mSavedSel.RestoreSelection(aSel); + StopPreservingSelection(); + return NS_OK; +} + +void +EditorBase::StopPreservingSelection() +{ + mRangeUpdater.DropSelectionState(mSavedSel); + mSavedSel.MakeEmpty(); +} + +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::BeginIMEComposition(WidgetCompositionEvent* aCompositionEvent) +{ + MOZ_ASSERT(!mComposition, "There is composition already"); + if (!EnsureComposition(aCompositionEvent)) { + return NS_OK; + } + if (mPhonetic) { + mPhonetic->Truncate(0); + } + return NS_OK; +} + +void +EditorBase::EndIMEComposition() +{ + NS_ENSURE_TRUE_VOID(mComposition); // nothing to do + + // 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 (mTxnMgr) { + nsCOMPtr<nsITransaction> txn = mTxnMgr->PeekUndoStack(); + nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryInterface(txn); + if (plcTxn) { + DebugOnly<nsresult> rv = plcTxn->Commit(); + NS_ASSERTION(NS_SUCCEEDED(rv), + "nsIAbsorbingTransaction::Commit() failed"); + } + } + + // Composition string may have hidden the caret. Therefore, we need to + // cancel it here. + HideCaret(false); + + /* reset the data we need to construct a transaction */ + mIMETextNode = nullptr; + mIMETextOffset = 0; + mIMETextLength = 0; + mComposition->EndHandlingComposition(this); + mComposition = nullptr; + + // notify editor observers of action + NotifyEditorObservers(eNotifyEditorObserversOfEnd); +} + +NS_IMETHODIMP +EditorBase::GetPhonetic(nsAString& aPhonetic) +{ + if (mPhonetic) { + aPhonetic = *mPhonetic; + } else { + aPhonetic.Truncate(0); + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::ForceCompositionEnd() +{ + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + if (!ps) { + return NS_ERROR_NOT_AVAILABLE; + } + nsPresContext* pc = ps->GetPresContext(); + if (!pc) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mComposition ? + IMEStateManager::NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, pc) : NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetPreferredIMEState(IMEState* aState) +{ + NS_ENSURE_ARG_POINTER(aState); + aState->mEnabled = IMEState::ENABLED; + aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE; + + if (IsReadonly() || IsDisabled()) { + aState->mEnabled = IMEState::DISABLED; + return NS_OK; + } + + nsCOMPtr<nsIContent> content = GetRoot(); + NS_ENSURE_TRUE(content, NS_ERROR_FAILURE); + + nsIFrame* frame = content->GetPrimaryFrame(); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + switch (frame->StyleUIReset()->mIMEMode) { + case NS_STYLE_IME_MODE_AUTO: + if (IsPasswordEditor()) + aState->mEnabled = IMEState::PASSWORD; + break; + case NS_STYLE_IME_MODE_DISABLED: + // we should use password state for |ime-mode: disabled;|. + aState->mEnabled = IMEState::PASSWORD; + break; + case NS_STYLE_IME_MODE_ACTIVE: + aState->mOpen = IMEState::OPEN; + break; + case NS_STYLE_IME_MODE_INACTIVE: + aState->mOpen = IMEState::CLOSED; + break; + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetComposing(bool* aResult) +{ + NS_ENSURE_ARG_POINTER(aResult); + *aResult = IsIMEComposing(); + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetRootElement(nsIDOMElement** aRootElement) +{ + NS_ENSURE_ARG_POINTER(aRootElement); + NS_ENSURE_TRUE(mRootElement, NS_ERROR_NOT_AVAILABLE); + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mRootElement); + rootElement.forget(aRootElement); + return NS_OK; +} + +/** + * All editor operations which alter the doc should be prefaced + * with a call to StartOperation, naming the action and direction. + */ +NS_IMETHODIMP +EditorBase::StartOperation(EditAction opID, + nsIEditor::EDirection aDirection) +{ + mAction = opID; + mDirection = aDirection; + return NS_OK; +} + +/** + * All editor operations which alter the doc should be followed + * with a call to EndOperation. + */ +NS_IMETHODIMP +EditorBase::EndOperation() +{ + mAction = EditAction::none; + mDirection = eNone; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::CloneAttribute(const nsAString& aAttribute, + nsIDOMNode* aDestNode, + nsIDOMNode* aSourceNode) +{ + NS_ENSURE_TRUE(aDestNode && aSourceNode, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIDOMElement> destElement = do_QueryInterface(aDestNode); + nsCOMPtr<nsIDOMElement> sourceElement = do_QueryInterface(aSourceNode); + NS_ENSURE_TRUE(destElement && sourceElement, NS_ERROR_NO_INTERFACE); + + nsAutoString attrValue; + bool isAttrSet; + nsresult rv = GetAttributeValue(sourceElement, + aAttribute, + attrValue, + &isAttrSet); + NS_ENSURE_SUCCESS(rv, rv); + if (isAttrSet) { + rv = SetAttribute(destElement, aAttribute, attrValue); + } else { + rv = RemoveAttribute(destElement, aAttribute); + } + + return rv; +} + +/** + * @param aDest Must be a DOM element. + * @param aSource Must be a DOM element. + */ +NS_IMETHODIMP +EditorBase::CloneAttributes(nsIDOMNode* aDest, + nsIDOMNode* aSource) +{ + NS_ENSURE_TRUE(aDest && aSource, NS_ERROR_NULL_POINTER); + + nsCOMPtr<Element> dest = do_QueryInterface(aDest); + nsCOMPtr<Element> source = do_QueryInterface(aSource); + NS_ENSURE_TRUE(dest && source, NS_ERROR_NO_INTERFACE); + + CloneAttributes(dest, source); + + return NS_OK; +} + +void +EditorBase::CloneAttributes(Element* aDest, + Element* aSource) +{ + MOZ_ASSERT(aDest && aSource); + + AutoEditBatch beginBatching(this); + + // Use transaction system for undo only if destination is already in the + // document + NS_ENSURE_TRUE(GetRoot(), ); + bool destInBody = GetRoot()->Contains(aDest); + + // Clear existing attributes + RefPtr<nsDOMAttributeMap> destAttributes = aDest->Attributes(); + while (RefPtr<Attr> attr = destAttributes->Item(0)) { + if (destInBody) { + RemoveAttribute(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)), + attr->NodeName()); + } else { + ErrorResult ignored; + aDest->RemoveAttribute(attr->NodeName(), ignored); + } + } + + // Set just the attributes that the source element has + RefPtr<nsDOMAttributeMap> sourceAttributes = aSource->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 (destInBody) { + SetAttributeOrEquivalent(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)), + attr->NodeName(), value, false); + } else { + // The element is not inserted in the document yet, we don't want to put + // a transaction on the UndoStack + SetAttributeOrEquivalent(static_cast<nsIDOMElement*>(GetAsDOMNode(aDest)), + attr->NodeName(), value, true); + } + } +} + +NS_IMETHODIMP +EditorBase::ScrollSelectionIntoView(bool aScrollToAnchor) +{ + nsCOMPtr<nsISelectionController> selCon; + if (NS_SUCCEEDED(GetSelectionController(getter_AddRefs(selCon))) && selCon) { + int16_t region = nsISelectionController::SELECTION_FOCUS_REGION; + + if (aScrollToAnchor) { + region = nsISelectionController::SELECTION_ANCHOR_REGION; + } + + selCon->ScrollSelectionIntoView(nsISelectionController::SELECTION_NORMAL, + region, nsISelectionController::SCROLL_OVERFLOW_HIDDEN); + } + + return NS_OK; +} + +void +EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsIDOMNode>& aNode, + int32_t& aOffset) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + FindBetterInsertionPoint(node, aOffset); + aNode = do_QueryInterface(node); +} + +void +EditorBase::FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode, + int32_t& aOffset) +{ + if (aNode->IsNodeOfType(nsINode::eTEXT)) { + // There is no "better" insertion point. + return; + } + + if (!IsPlaintextEditor()) { + // 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 InsertTextImpl() in + // HTMLEditRules. + return; + } + + nsCOMPtr<nsINode> node = aNode; + int32_t offset = aOffset; + + nsCOMPtr<nsINode> root = GetRoot(); + if (aNode == root) { + // 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 (!offset && node->HasChildren() && + node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) { + aNode = node->GetFirstChild(); + aOffset = 0; + return; + } + + // In some other cases, aNode is the anonymous DIV, and offset points to the + // terminating mozBR. In that case, we'll adjust aInOutNode and + // aInOutOffset to the preceding text node, if any. + if (offset > 0 && node->GetChildAt(offset - 1) && + node->GetChildAt(offset - 1)->IsNodeOfType(nsINode::eTEXT)) { + NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX); + aNode = node->GetChildAt(offset - 1); + aOffset = static_cast<int32_t>(aNode->Length()); + return; + } + } + + // Sometimes, aNode is the mozBR 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 (TextEditUtils::IsMozBR(node) && !offset) { + if (node->GetPreviousSibling() && + node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) { + NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX); + aNode = node->GetPreviousSibling(); + aOffset = static_cast<int32_t>(aNode->Length()); + return; + } + + if (node->GetParentNode() && node->GetParentNode() == root) { + aNode = node->GetParentNode(); + aOffset = 0; + return; + } + } +} + +nsresult +EditorBase::InsertTextImpl(const nsAString& aStringToInsert, + nsCOMPtr<nsINode>* aInOutNode, + int32_t* aInOutOffset, + nsIDocument* aDoc) +{ + // NOTE: caller *must* have already used AutoTransactionsConserveSelection + // stack-based class to turn off txn selection updating. Caller also turned + // on rules sniffing if desired. + + NS_ENSURE_TRUE(aInOutNode && *aInOutNode && aInOutOffset && aDoc, + NS_ERROR_NULL_POINTER); + + if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) { + return NS_OK; + } + + // This method doesn't support over INT32_MAX length text since aInOutOffset + // is int32_t*. + CheckedInt<int32_t> lengthToInsert(aStringToInsert.Length()); + NS_ENSURE_TRUE(lengthToInsert.isValid(), NS_ERROR_INVALID_ARG); + + nsCOMPtr<nsINode> node = *aInOutNode; + int32_t offset = *aInOutOffset; + + // In some cases, the node may be the anonymous div elemnt or a mozBR + // element. Let's try to look for better insertion point in the nearest + // text node if there is. + FindBetterInsertionPoint(node, offset); + + if (ShouldHandleIMEComposition()) { + CheckedInt<int32_t> newOffset; + if (!node->IsNodeOfType(nsINode::eTEXT)) { + // create a text node + RefPtr<nsTextNode> newNode = aDoc->CreateTextNode(EmptyString()); + // then we insert it into the dom tree + nsresult rv = InsertNode(*newNode, *node, offset); + NS_ENSURE_SUCCESS(rv, rv); + node = newNode; + offset = 0; + newOffset = lengthToInsert; + } else { + newOffset = lengthToInsert + offset; + NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE); + } + nsresult rv = + InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset); + NS_ENSURE_SUCCESS(rv, rv); + offset = newOffset.value(); + } else { + if (node->IsNodeOfType(nsINode::eTEXT)) { + CheckedInt<int32_t> newOffset = lengthToInsert + offset; + NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE); + // we are inserting text into an existing text node. + nsresult rv = + InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(), offset); + NS_ENSURE_SUCCESS(rv, rv); + offset = newOffset.value(); + } else { + // 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> newNode = aDoc->CreateTextNode(aStringToInsert); + // then we insert it into the dom tree + nsresult rv = InsertNode(*newNode, *node, offset); + NS_ENSURE_SUCCESS(rv, rv); + node = newNode; + offset = lengthToInsert.value(); + } + } + + *aInOutNode = node; + *aInOutOffset = offset; + return NS_OK; +} + +nsresult +EditorBase::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert, + Text& aTextNode, + int32_t aOffset, + bool aSuppressIME) +{ + RefPtr<EditTransactionBase> transaction; + bool isIMETransaction = false; + RefPtr<Text> insertedTextNode = &aTextNode; + int32_t insertedOffset = aOffset; + // aSuppressIME is used when editor must insert text, yet this text is not + // part of the current IME operation. Example: adjusting whitespace around an + // IME insertion. + if (ShouldHandleIMEComposition() && !aSuppressIME) { + if (!mIMETextNode) { + mIMETextNode = &aTextNode; + mIMETextOffset = aOffset; + } + // Modify mPhonetic with raw text input clauses. + const TextRangeArray* ranges = mComposition->GetRanges(); + for (uint32_t i = 0; i < (ranges ? ranges->Length() : 0); ++i) { + const TextRange& textRange = ranges->ElementAt(i); + if (!textRange.Length() || + textRange.mRangeType != TextRangeType::eRawClause) { + continue; + } + if (!mPhonetic) { + mPhonetic = new nsString(); + } + nsAutoString stringToInsert(aStringToInsert); + stringToInsert.Mid(*mPhonetic, + textRange.mStartOffset, textRange.Length()); + } + + transaction = CreateTxnForComposition(aStringToInsert); + isIMETransaction = true; + // All characters of the composition string will be replaced with + // aStringToInsert. So, we need to emulate to remove the composition + // string. + insertedTextNode = mIMETextNode; + insertedOffset = mIMETextOffset; + mIMETextLength = aStringToInsert.Length(); + } else { + transaction = CreateTxnForInsertText(aStringToInsert, aTextNode, aOffset); + } + + // Let listeners know what's up + for (auto& listener : mActionListeners) { + listener->WillInsertText( + static_cast<nsIDOMCharacterData*>(insertedTextNode->AsDOMNode()), + insertedOffset, aStringToInsert); + } + + // XXX We may not need these view batches anymore. This is handled at a + // higher level now I believe. + BeginUpdateViewBatch(); + nsresult rv = DoTransaction(transaction); + EndUpdateViewBatch(); + + // let listeners know what happened + for (auto& listener : mActionListeners) { + listener->DidInsertText( + static_cast<nsIDOMCharacterData*>(insertedTextNode->AsDOMNode()), + insertedOffset, aStringToInsert, rv); + } + + // 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 (isIMETransaction && mIMETextNode) { + uint32_t len = mIMETextNode->Length(); + if (!len) { + DeleteNode(mIMETextNode); + mIMETextNode = nullptr; + static_cast<CompositionTransaction*>(transaction.get())->MarkFixed(); + } + } + + return rv; +} + +nsresult +EditorBase::SelectEntireDocument(Selection* aSelection) +{ + if (!aSelection) { + return NS_ERROR_NULL_POINTER; + } + + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot()); + if (!rootElement) { + return NS_ERROR_NOT_INITIALIZED; + } + + return aSelection->SelectAllChildren(rootElement); +} + +nsINode* +EditorBase::GetFirstEditableNode(nsINode* aRoot) +{ + MOZ_ASSERT(aRoot); + + nsIContent* node = GetLeftmostChild(aRoot); + if (node && !IsEditable(node)) { + node = GetNextNode(node, /* aEditableNode = */ true); + } + + return (node != aRoot) ? node : nullptr; +} + +NS_IMETHODIMP +EditorBase::NotifyDocumentListeners( + TDocumentListenerNotification aNotificationType) +{ + if (!mDocStateListeners.Length()) { + // Maybe there just aren't any. + return NS_OK; + } + + nsTArray<OwningNonNull<nsIDocumentStateListener>> + listeners(mDocStateListeners); + nsresult rv = NS_OK; + + switch (aNotificationType) { + case eDocumentCreated: + for (auto& listener : listeners) { + rv = listener->NotifyDocumentCreated(); + if (NS_FAILED(rv)) { + break; + } + } + break; + + case eDocumentToBeDestroyed: + for (auto& listener : listeners) { + rv = listener->NotifyDocumentWillBeDestroyed(); + if (NS_FAILED(rv)) { + break; + } + } + break; + + case eDocumentStateChanged: { + bool docIsDirty; + rv = GetDocumentModified(&docIsDirty); + NS_ENSURE_SUCCESS(rv, rv); + + if (static_cast<int8_t>(docIsDirty) == mDocDirtyState) { + return NS_OK; + } + + mDocDirtyState = docIsDirty; + + for (auto& listener : listeners) { + rv = listener->NotifyDocumentStateChanged(mDocDirtyState); + if (NS_FAILED(rv)) { + break; + } + } + break; + } + default: + NS_NOTREACHED("Unknown notification"); + } + + return rv; +} + +already_AddRefed<InsertTextTransaction> +EditorBase::CreateTxnForInsertText(const nsAString& aStringToInsert, + Text& aTextNode, + int32_t aOffset) +{ + RefPtr<InsertTextTransaction> transaction = + new InsertTextTransaction(aTextNode, aOffset, aStringToInsert, *this, + &mRangeUpdater); + return transaction.forget(); +} + +nsresult +EditorBase::DeleteText(nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aLength) +{ + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteText(aCharData, aOffset, aLength); + NS_ENSURE_STATE(transaction); + + AutoRules beginRulesSniffing(this, EditAction::deleteText, + nsIEditor::ePrevious); + + // Let listeners know what's up + for (auto& listener : mActionListeners) { + listener->WillDeleteText( + static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset, + aLength); + } + + nsresult rv = DoTransaction(transaction); + + // Let listeners know what happened + for (auto& listener : mActionListeners) { + listener->DidDeleteText( + static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset, + aLength, rv); + } + + return rv; +} + +already_AddRefed<DeleteTextTransaction> +EditorBase::CreateTxnForDeleteText(nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aLength) +{ + RefPtr<DeleteTextTransaction> transaction = + new DeleteTextTransaction(*this, aCharData, aOffset, aLength, + &mRangeUpdater); + nsresult rv = transaction->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + + return transaction.forget(); +} + +already_AddRefed<SplitNodeTransaction> +EditorBase::CreateTxnForSplitNode(nsIContent& aNode, + uint32_t aOffset) +{ + RefPtr<SplitNodeTransaction> transaction = + new SplitNodeTransaction(*this, aNode, aOffset); + return transaction.forget(); +} + +already_AddRefed<JoinNodeTransaction> +EditorBase::CreateTxnForJoinNode(nsINode& aLeftNode, + nsINode& aRightNode) +{ + RefPtr<JoinNodeTransaction> transaction = + new JoinNodeTransaction(*this, aLeftNode, aRightNode); + + NS_ENSURE_SUCCESS(transaction->CheckValidity(), nullptr); + + return transaction.forget(); +} + +struct SavedRange final +{ + RefPtr<Selection> mSelection; + nsCOMPtr<nsINode> mStartNode; + nsCOMPtr<nsINode> mEndNode; + int32_t mStartOffset; + int32_t mEndOffset; +}; + +nsresult +EditorBase::SplitNodeImpl(nsIContent& aExistingRightNode, + int32_t aOffset, + nsIContent& aNewLeftNode) +{ + // Remember all selection points. + AutoTArray<SavedRange, 10> savedRanges; + for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) { + SelectionType selectionType(ToSelectionType(1 << i)); + SavedRange range; + range.mSelection = GetSelection(selectionType); + if (selectionType == SelectionType::eNormal) { + NS_ENSURE_TRUE(range.mSelection, NS_ERROR_NULL_POINTER); + } else if (!range.mSelection) { + // For non-normal selections, skip over the non-existing ones. + continue; + } + + for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) { + RefPtr<nsRange> r = range.mSelection->GetRangeAt(j); + MOZ_ASSERT(r->IsPositioned()); + range.mStartNode = r->GetStartParent(); + range.mStartOffset = r->StartOffset(); + range.mEndNode = r->GetEndParent(); + range.mEndOffset = r->EndOffset(); + + savedRanges.AppendElement(range); + } + } + + nsCOMPtr<nsINode> parent = aExistingRightNode.GetParentNode(); + NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); + + ErrorResult rv; + nsCOMPtr<nsINode> refNode = &aExistingRightNode; + parent->InsertBefore(aNewLeftNode, refNode, rv); + NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); + + // Split the children between the two nodes. At this point, + // aExistingRightNode has all the children. Move all the children whose + // index is < aOffset to aNewLeftNode. + if (aOffset < 0) { + // This means move no children + return NS_OK; + } + + // If it's a text node, just shuffle around some text + if (aExistingRightNode.GetAsText() && aNewLeftNode.GetAsText()) { + // Fix right node + nsAutoString leftText; + aExistingRightNode.GetAsText()->SubstringData(0, aOffset, leftText); + aExistingRightNode.GetAsText()->DeleteData(0, aOffset); + // Fix left node + aNewLeftNode.GetAsText()->SetData(leftText); + } else { + // Otherwise it's an interior node, so shuffle around the children. Go + // through list backwards so deletes don't interfere with the iteration. + nsCOMPtr<nsINodeList> childNodes = aExistingRightNode.ChildNodes(); + for (int32_t i = aOffset - 1; i >= 0; i--) { + nsCOMPtr<nsIContent> childNode = childNodes->Item(i); + if (childNode) { + aExistingRightNode.RemoveChild(*childNode, rv); + if (!rv.Failed()) { + nsCOMPtr<nsIContent> firstChild = aNewLeftNode.GetFirstChild(); + aNewLeftNode.InsertBefore(*childNode, firstChild, rv); + } + } + if (rv.Failed()) { + break; + } + } + } + + // Handle selection + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + if (ps) { + ps->FlushPendingNotifications(Flush_Frames); + } + + bool shouldSetSelection = GetShouldTxnSetSelection(); + + RefPtr<Selection> previousSelection; + for (size_t i = 0; i < savedRanges.Length(); ++i) { + // Adjust the selection if needed. + SavedRange& range = savedRanges[i]; + + // If we have not seen the selection yet, clear all of its ranges. + if (range.mSelection != previousSelection) { + nsresult rv = range.mSelection->RemoveAllRanges(); + NS_ENSURE_SUCCESS(rv, rv); + previousSelection = range.mSelection; + } + + if (shouldSetSelection && + range.mSelection->Type() == SelectionType::eNormal) { + // If the editor should adjust the selection, don't bother restoring + // the ranges for the normal selection here. + continue; + } + + // Split the selection into existing node and new node. + if (range.mStartNode == &aExistingRightNode) { + if (range.mStartOffset < aOffset) { + range.mStartNode = &aNewLeftNode; + } else { + range.mStartOffset -= aOffset; + } + } + + if (range.mEndNode == &aExistingRightNode) { + if (range.mEndOffset < aOffset) { + range.mEndNode = &aNewLeftNode; + } else { + range.mEndOffset -= aOffset; + } + } + + RefPtr<nsRange> newRange; + nsresult rv = nsRange::CreateRange(range.mStartNode, range.mStartOffset, + range.mEndNode, range.mEndOffset, + getter_AddRefs(newRange)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range.mSelection->AddRange(newRange); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (shouldSetSelection) { + // Editor wants us to set selection at split point. + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + selection->Collapse(&aNewLeftNode, aOffset); + } + + return NS_OK; +} + +nsresult +EditorBase::JoinNodesImpl(nsINode* aNodeToKeep, + nsINode* aNodeToJoin, + nsINode* aParent) +{ + MOZ_ASSERT(aNodeToKeep); + MOZ_ASSERT(aNodeToJoin); + MOZ_ASSERT(aParent); + + uint32_t firstNodeLength = aNodeToJoin->Length(); + + int32_t joinOffset; + GetNodeLocation(aNodeToJoin, &joinOffset); + int32_t keepOffset; + nsINode* parent = GetNodeLocation(aNodeToKeep, &keepOffset); + + // Remember all selection points. + AutoTArray<SavedRange, 10> savedRanges; + for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) { + SelectionType selectionType(ToSelectionType(1 << i)); + SavedRange range; + range.mSelection = GetSelection(selectionType); + if (selectionType == SelectionType::eNormal) { + NS_ENSURE_TRUE(range.mSelection, NS_ERROR_NULL_POINTER); + } else if (!range.mSelection) { + // For non-normal selections, skip over the non-existing ones. + continue; + } + + for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) { + RefPtr<nsRange> r = range.mSelection->GetRangeAt(j); + MOZ_ASSERT(r->IsPositioned()); + range.mStartNode = r->GetStartParent(); + range.mStartOffset = r->StartOffset(); + range.mEndNode = r->GetEndParent(); + range.mEndOffset = r->EndOffset(); + + // If selection endpoint is between the nodes, remember it as being + // in the one that is going away instead. This simplifies later selection + // adjustment logic at end of this method. + if (range.mStartNode) { + if (range.mStartNode == parent && + joinOffset < range.mStartOffset && + range.mStartOffset <= keepOffset) { + range.mStartNode = aNodeToJoin; + range.mStartOffset = firstNodeLength; + } + if (range.mEndNode == parent && + joinOffset < range.mEndOffset && + range.mEndOffset <= keepOffset) { + range.mEndNode = aNodeToJoin; + range.mEndOffset = firstNodeLength; + } + } + + savedRanges.AppendElement(range); + } + } + + // OK, ready to do join now. + // If it's a text node, just shuffle around some text. + nsCOMPtr<nsIDOMCharacterData> keepNodeAsText( do_QueryInterface(aNodeToKeep) ); + nsCOMPtr<nsIDOMCharacterData> joinNodeAsText( do_QueryInterface(aNodeToJoin) ); + if (keepNodeAsText && joinNodeAsText) { + nsAutoString rightText; + nsAutoString leftText; + keepNodeAsText->GetData(rightText); + joinNodeAsText->GetData(leftText); + leftText += rightText; + keepNodeAsText->SetData(leftText); + } else { + // Otherwise it's an interior node, so shuffle around the children. + nsCOMPtr<nsINodeList> childNodes = aNodeToJoin->ChildNodes(); + MOZ_ASSERT(childNodes); + + // Remember the first child in aNodeToKeep, we'll insert all the children of aNodeToJoin in front of it + // GetFirstChild returns nullptr firstNode if aNodeToKeep has no children, that's OK. + nsCOMPtr<nsIContent> firstNode = aNodeToKeep->GetFirstChild(); + + // Have to go through the list backwards to keep deletes from interfering with iteration. + for (uint32_t i = childNodes->Length(); i; --i) { + nsCOMPtr<nsIContent> childNode = childNodes->Item(i - 1); + if (childNode) { + // prepend children of aNodeToJoin + ErrorResult err; + aNodeToKeep->InsertBefore(*childNode, firstNode, err); + NS_ENSURE_TRUE(!err.Failed(), err.StealNSResult()); + firstNode = childNode.forget(); + } + } + } + + // Delete the extra node. + ErrorResult err; + aParent->RemoveChild(*aNodeToJoin, err); + + bool shouldSetSelection = GetShouldTxnSetSelection(); + + RefPtr<Selection> previousSelection; + for (size_t i = 0; i < savedRanges.Length(); ++i) { + // And adjust the selection if needed. + SavedRange& range = savedRanges[i]; + + // If we have not seen the selection yet, clear all of its ranges. + if (range.mSelection != previousSelection) { + nsresult rv = range.mSelection->RemoveAllRanges(); + NS_ENSURE_SUCCESS(rv, rv); + previousSelection = range.mSelection; + } + + if (shouldSetSelection && + range.mSelection->Type() == SelectionType::eNormal) { + // If the editor should adjust the selection, don't bother restoring + // the ranges for the normal selection here. + continue; + } + + // Check to see if we joined nodes where selection starts. + if (range.mStartNode == aNodeToJoin) { + range.mStartNode = aNodeToKeep; + } else if (range.mStartNode == aNodeToKeep) { + range.mStartOffset += firstNodeLength; + } + + // Check to see if we joined nodes where selection ends. + if (range.mEndNode == aNodeToJoin) { + range.mEndNode = aNodeToKeep; + } else if (range.mEndNode == aNodeToKeep) { + range.mEndOffset += firstNodeLength; + } + + RefPtr<nsRange> newRange; + nsresult rv = nsRange::CreateRange(range.mStartNode, range.mStartOffset, + range.mEndNode, range.mEndOffset, + getter_AddRefs(newRange)); + NS_ENSURE_SUCCESS(rv, rv); + rv = range.mSelection->AddRange(newRange); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (shouldSetSelection) { + // Editor wants us to set selection at join point. + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + selection->Collapse(aNodeToKeep, AssertedCast<int32_t>(firstNodeLength)); + } + + return err.StealNSResult(); +} + +int32_t +EditorBase::GetChildOffset(nsIDOMNode* aChild, + nsIDOMNode* aParent) +{ + MOZ_ASSERT(aChild && aParent); + + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + nsCOMPtr<nsINode> child = do_QueryInterface(aChild); + MOZ_ASSERT(parent && child); + + int32_t idx = parent->IndexOf(child); + MOZ_ASSERT(idx != -1); + return idx; +} + +// static +already_AddRefed<nsIDOMNode> +EditorBase::GetNodeLocation(nsIDOMNode* aChild, + int32_t* outOffset) +{ + MOZ_ASSERT(aChild && outOffset); + NS_ENSURE_TRUE(aChild && outOffset, nullptr); + *outOffset = -1; + + nsCOMPtr<nsIDOMNode> parent; + + MOZ_ALWAYS_SUCCEEDS(aChild->GetParentNode(getter_AddRefs(parent))); + if (parent) { + *outOffset = GetChildOffset(aChild, parent); + } + + return parent.forget(); +} + +nsINode* +EditorBase::GetNodeLocation(nsINode* aChild, + int32_t* aOffset) +{ + MOZ_ASSERT(aChild); + MOZ_ASSERT(aOffset); + + nsINode* parent = aChild->GetParentNode(); + if (parent) { + *aOffset = parent->IndexOf(aChild); + MOZ_ASSERT(*aOffset != -1); + } else { + *aOffset = -1; + } + return parent; +} + +/** + * Returns the number of things inside aNode. If aNode is text, returns number + * of characters. If not, returns number of children nodes. + */ +nsresult +EditorBase::GetLengthOfDOMNode(nsIDOMNode* aNode, + uint32_t& aCount) +{ + aCount = 0; + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); + aCount = node->Length(); + return NS_OK; +} + +nsIContent* +EditorBase::GetPriorNode(nsINode* aParentNode, + int32_t aOffset, + bool aEditableNode, + bool aNoBlockCrossing) +{ + MOZ_ASSERT(aParentNode); + + // If we are at the beginning of the node, or it is a text node, then just + // look before it. + if (!aOffset || aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) { + if (aNoBlockCrossing && IsBlockNode(aParentNode)) { + // If we aren't allowed to cross blocks, don't look before this block. + return nullptr; + } + return GetPriorNode(aParentNode, aEditableNode, aNoBlockCrossing); + } + + // else look before the child at 'aOffset' + if (nsIContent* child = aParentNode->GetChildAt(aOffset)) { + return GetPriorNode(child, aEditableNode, aNoBlockCrossing); + } + + // unless there isn't one, in which case we are at the end of the node + // and want the deep-right child. + nsIContent* resultNode = GetRightmostChild(aParentNode, aNoBlockCrossing); + if (!resultNode || !aEditableNode || IsEditable(resultNode)) { + return resultNode; + } + + // restart the search from the non-editable node we just found + return GetPriorNode(resultNode, aEditableNode, aNoBlockCrossing); +} + +nsIContent* +EditorBase::GetNextNode(nsINode* aParentNode, + int32_t aOffset, + bool aEditableNode, + bool aNoBlockCrossing) +{ + MOZ_ASSERT(aParentNode); + + // if aParentNode is a text node, use its location instead + if (aParentNode->NodeType() == nsIDOMNode::TEXT_NODE) { + nsINode* parent = aParentNode->GetParentNode(); + NS_ENSURE_TRUE(parent, nullptr); + aOffset = parent->IndexOf(aParentNode) + 1; // _after_ the text node + aParentNode = parent; + } + + // look at the child at 'aOffset' + nsIContent* child = aParentNode->GetChildAt(aOffset); + if (child) { + if (aNoBlockCrossing && IsBlockNode(child)) { + return child; + } + + nsIContent* resultNode = GetLeftmostChild(child, aNoBlockCrossing); + if (!resultNode) { + return child; + } + + if (!IsDescendantOfEditorRoot(resultNode)) { + return nullptr; + } + + if (!aEditableNode || IsEditable(resultNode)) { + return resultNode; + } + + // restart the search from the non-editable node we just found + return GetNextNode(resultNode, aEditableNode, aNoBlockCrossing); + } + + // unless there isn't one, in which case we are at the end of the node + // and want the next one. + if (aNoBlockCrossing && IsBlockNode(aParentNode)) { + // don't cross out of parent block + return nullptr; + } + + return GetNextNode(aParentNode, aEditableNode, aNoBlockCrossing); +} + +nsIContent* +EditorBase::GetPriorNode(nsINode* aCurrentNode, + bool aEditableNode, + bool aNoBlockCrossing /* = false */) +{ + MOZ_ASSERT(aCurrentNode); + + if (!IsDescendantOfEditorRoot(aCurrentNode)) { + return nullptr; + } + + return FindNode(aCurrentNode, false, aEditableNode, aNoBlockCrossing); +} + +nsIContent* +EditorBase::FindNextLeafNode(nsINode* aCurrentNode, + bool aGoForward, + bool bNoBlockCrossing) +{ + // called only by GetPriorNode so we don't need to check params. + NS_PRECONDITION(IsDescendantOfEditorRoot(aCurrentNode) && + !IsEditorRoot(aCurrentNode), + "Bogus arguments"); + + nsINode* cur = aCurrentNode; + for (;;) { + // if aCurrentNode has a sibling in the right direction, return + // that sibling's closest child (or itself if it has no children) + nsIContent* sibling = + aGoForward ? cur->GetNextSibling() : cur->GetPreviousSibling(); + if (sibling) { + if (bNoBlockCrossing && IsBlockNode(sibling)) { + // don't look inside prevsib, since it is a block + return sibling; + } + nsIContent *leaf = + aGoForward ? GetLeftmostChild(sibling, bNoBlockCrossing) : + GetRightmostChild(sibling, bNoBlockCrossing); + if (!leaf) { + return sibling; + } + + return leaf; + } + + nsINode *parent = cur->GetParentNode(); + if (!parent) { + return nullptr; + } + + NS_ASSERTION(IsDescendantOfEditorRoot(parent), + "We started with a proper descendant of root, and should stop " + "if we ever hit the root, so we better have a descendant of " + "root now!"); + if (IsEditorRoot(parent) || + (bNoBlockCrossing && IsBlockNode(parent))) { + return nullptr; + } + + cur = parent; + } + + NS_NOTREACHED("What part of for(;;) do you not understand?"); + return nullptr; +} + +nsIContent* +EditorBase::GetNextNode(nsINode* aCurrentNode, + bool aEditableNode, + bool bNoBlockCrossing) +{ + MOZ_ASSERT(aCurrentNode); + + if (!IsDescendantOfEditorRoot(aCurrentNode)) { + return nullptr; + } + + return FindNode(aCurrentNode, true, aEditableNode, bNoBlockCrossing); +} + +nsIContent* +EditorBase::FindNode(nsINode* aCurrentNode, + bool aGoForward, + bool aEditableNode, + bool bNoBlockCrossing) +{ + if (IsEditorRoot(aCurrentNode)) { + // Don't allow traversal above the root node! This helps + // prevent us from accidentally editing browser content + // when the editor is in a text widget. + + return nullptr; + } + + nsCOMPtr<nsIContent> candidate = + FindNextLeafNode(aCurrentNode, aGoForward, bNoBlockCrossing); + + if (!candidate) { + return nullptr; + } + + if (!aEditableNode || IsEditable(candidate)) { + return candidate; + } + + return FindNode(candidate, aGoForward, aEditableNode, bNoBlockCrossing); +} + +nsIContent* +EditorBase::GetRightmostChild(nsINode* aCurrentNode, + bool bNoBlockCrossing) +{ + NS_ENSURE_TRUE(aCurrentNode, nullptr); + nsIContent *cur = aCurrentNode->GetLastChild(); + if (!cur) { + return nullptr; + } + for (;;) { + if (bNoBlockCrossing && IsBlockNode(cur)) { + return cur; + } + nsIContent* next = cur->GetLastChild(); + if (!next) { + return cur; + } + cur = next; + } + + NS_NOTREACHED("What part of for(;;) do you not understand?"); + return nullptr; +} + +nsIContent* +EditorBase::GetLeftmostChild(nsINode* aCurrentNode, + bool bNoBlockCrossing) +{ + NS_ENSURE_TRUE(aCurrentNode, nullptr); + nsIContent *cur = aCurrentNode->GetFirstChild(); + if (!cur) { + return nullptr; + } + for (;;) { + if (bNoBlockCrossing && IsBlockNode(cur)) { + return cur; + } + nsIContent *next = cur->GetFirstChild(); + if (!next) { + return cur; + } + cur = next; + } + + NS_NOTREACHED("What part of for(;;) do you not understand?"); + return nullptr; +} + +bool +EditorBase::IsBlockNode(nsINode* aNode) +{ + // stub to be overridden in HTMLEditor. + // screwing around with the class hierarchy here in order + // to not duplicate the code in GetNextNode/GetPrevNode + // across both EditorBase/HTMLEditor. + return false; +} + +bool +EditorBase::CanContain(nsINode& aParent, + nsIContent& aChild) +{ + switch (aParent.NodeType()) { + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::DOCUMENT_FRAGMENT_NODE: + return TagCanContain(*aParent.NodeInfo()->NameAtom(), aChild); + } + return false; +} + +bool +EditorBase::CanContainTag(nsINode& aParent, + nsIAtom& aChildTag) +{ + switch (aParent.NodeType()) { + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::DOCUMENT_FRAGMENT_NODE: + return TagCanContainTag(*aParent.NodeInfo()->NameAtom(), aChildTag); + } + return false; +} + +bool +EditorBase::TagCanContain(nsIAtom& aParentTag, + nsIContent& aChild) +{ + switch (aChild.NodeType()) { + case nsIDOMNode::TEXT_NODE: + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::DOCUMENT_FRAGMENT_NODE: + return TagCanContainTag(aParentTag, *aChild.NodeInfo()->NameAtom()); + } + return false; +} + +bool +EditorBase::TagCanContainTag(nsIAtom& aParentTag, + nsIAtom& aChildTag) +{ + return true; +} + +bool +EditorBase::IsRoot(nsIDOMNode* inNode) +{ + NS_ENSURE_TRUE(inNode, false); + + nsCOMPtr<nsIDOMNode> rootNode = do_QueryInterface(GetRoot()); + + return inNode == rootNode; +} + +bool +EditorBase::IsRoot(nsINode* inNode) +{ + NS_ENSURE_TRUE(inNode, false); + + nsCOMPtr<nsINode> rootNode = GetRoot(); + + return inNode == rootNode; +} + +bool +EditorBase::IsEditorRoot(nsINode* aNode) +{ + NS_ENSURE_TRUE(aNode, false); + nsCOMPtr<nsINode> rootNode = GetEditorRoot(); + return aNode == rootNode; +} + +bool +EditorBase::IsDescendantOfRoot(nsIDOMNode* inNode) +{ + nsCOMPtr<nsINode> node = do_QueryInterface(inNode); + return IsDescendantOfRoot(node); +} + +bool +EditorBase::IsDescendantOfRoot(nsINode* inNode) +{ + NS_ENSURE_TRUE(inNode, false); + nsCOMPtr<nsIContent> root = GetRoot(); + NS_ENSURE_TRUE(root, false); + + return nsContentUtils::ContentIsDescendantOf(inNode, root); +} + +bool +EditorBase::IsDescendantOfEditorRoot(nsINode* aNode) +{ + NS_ENSURE_TRUE(aNode, false); + nsCOMPtr<nsIContent> root = GetEditorRoot(); + NS_ENSURE_TRUE(root, false); + + return nsContentUtils::ContentIsDescendantOf(aNode, root); +} + +bool +EditorBase::IsContainer(nsINode* aNode) +{ + return aNode ? true : false; +} + +bool +EditorBase::IsContainer(nsIDOMNode* aNode) +{ + return aNode ? true : false; +} + +static inline bool +IsElementVisible(Element* aElement) +{ + if (aElement->GetPrimaryFrame()) { + // It's visible, for our purposes + return true; + } + + nsIContent *cur = aElement; + for (;;) { + // Walk up the tree looking for the nearest ancestor with a frame. + // The state of the child right below it will determine whether + // we might possibly have a frame or not. + bool haveLazyBitOnChild = cur->HasFlag(NODE_NEEDS_FRAME); + cur = cur->GetFlattenedTreeParent(); + if (!cur) { + if (!haveLazyBitOnChild) { + // None of our ancestors have lazy bits set, so we shouldn't + // have a frame + return false; + } + + // The root has a lazy frame construction bit. We need to check + // our style. + break; + } + + if (cur->GetPrimaryFrame()) { + if (!haveLazyBitOnChild) { + // Our ancestor directly under |cur| doesn't have lazy bits; + // that means we won't get a frame + return false; + } + + if (cur->GetPrimaryFrame()->IsLeaf()) { + // Nothing under here will ever get frames + return false; + } + + // Otherwise, we might end up with a frame when that lazy bit is + // processed. Figure out our actual style. + break; + } + } + + // Now it might be that we have no frame because we're in a + // display:none subtree, or it might be that we're just dealing with + // lazy frame construction and it hasn't happened yet. Check which + // one it is. + RefPtr<nsStyleContext> styleContext = + nsComputedDOMStyle::GetStyleContextForElementNoFlush(aElement, + nullptr, nullptr); + if (styleContext) { + return styleContext->StyleDisplay()->mDisplay != StyleDisplay::None; + } + return false; +} + +bool +EditorBase::IsEditable(nsIDOMNode* aNode) +{ + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + return IsEditable(content); +} + +bool +EditorBase::IsEditable(nsINode* aNode) +{ + NS_ENSURE_TRUE(aNode, false); + + if (!aNode->IsNodeOfType(nsINode::eCONTENT) || IsMozEditorBogusNode(aNode) || + !IsModifiableNode(aNode)) { + return false; + } + + // see if it has a frame. If so, we'll edit it. + // special case for textnodes: frame must have width. + if (aNode->IsElement() && !IsElementVisible(aNode->AsElement())) { + // If the element has no frame, it's not editable. Note that we + // need to check IsElement() here, because some of our tests + // rely on frameless textnodes being visible. + return false; + } + switch (aNode->NodeType()) { + case nsIDOMNode::ELEMENT_NODE: + case nsIDOMNode::TEXT_NODE: + return true; // element or text node; not invisible + default: + return false; + } +} + +bool +EditorBase::IsMozEditorBogusNode(nsINode* element) +{ + return element && element->IsElement() && + element->AsElement()->AttrValueIs(kNameSpaceID_None, + kMOZEditorBogusNodeAttrAtom, kMOZEditorBogusNodeValue, + eCaseMatters); +} + +uint32_t +EditorBase::CountEditableChildren(nsINode* aNode) +{ + MOZ_ASSERT(aNode); + uint32_t count = 0; + for (nsIContent* child = aNode->GetFirstChild(); + child; + child = child->GetNextSibling()) { + if (IsEditable(child)) { + ++count; + } + } + return count; +} + +NS_IMETHODIMP +EditorBase::IncrementModificationCount(int32_t inNumMods) +{ + uint32_t oldModCount = mModCount; + + mModCount += inNumMods; + + if ((!oldModCount && mModCount) || + (oldModCount && !mModCount)) { + NotifyDocumentListeners(eDocumentStateChanged); + } + return NS_OK; +} + + +NS_IMETHODIMP +EditorBase::GetModificationCount(int32_t* outModCount) +{ + NS_ENSURE_ARG_POINTER(outModCount); + *outModCount = mModCount; + return NS_OK; +} + + +NS_IMETHODIMP +EditorBase::ResetModificationCount() +{ + bool doNotify = (mModCount != 0); + + mModCount = 0; + + if (doNotify) { + NotifyDocumentListeners(eDocumentStateChanged); + } + return NS_OK; +} + +nsIAtom* +EditorBase::GetTag(nsIDOMNode* aNode) +{ + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + + if (!content) { + NS_ASSERTION(aNode, "null node passed to EditorBase::GetTag()"); + return nullptr; + } + + return content->NodeInfo()->NameAtom(); +} + +nsresult +EditorBase::GetTagString(nsIDOMNode* aNode, + nsAString& outString) +{ + if (!aNode) { + NS_NOTREACHED("null node passed to EditorBase::GetTagString()"); + return NS_ERROR_NULL_POINTER; + } + + nsIAtom *atom = GetTag(aNode); + if (!atom) { + return NS_ERROR_FAILURE; + } + + atom->ToString(outString); + return NS_OK; +} + +bool +EditorBase::NodesSameType(nsIDOMNode* aNode1, + nsIDOMNode* aNode2) +{ + if (!aNode1 || !aNode2) { + NS_NOTREACHED("null node passed to EditorBase::NodesSameType()"); + return false; + } + + nsCOMPtr<nsIContent> content1 = do_QueryInterface(aNode1); + NS_ENSURE_TRUE(content1, false); + + nsCOMPtr<nsIContent> content2 = do_QueryInterface(aNode2); + NS_ENSURE_TRUE(content2, false); + + return AreNodesSameType(content1, content2); +} + +bool +EditorBase::AreNodesSameType(nsIContent* aNode1, + nsIContent* aNode2) +{ + MOZ_ASSERT(aNode1); + MOZ_ASSERT(aNode2); + return aNode1->NodeInfo()->NameAtom() == aNode2->NodeInfo()->NameAtom(); +} + +bool +EditorBase::IsTextNode(nsIDOMNode* aNode) +{ + if (!aNode) { + NS_NOTREACHED("null node passed to IsTextNode()"); + return false; + } + + uint16_t nodeType; + aNode->GetNodeType(&nodeType); + return (nodeType == nsIDOMNode::TEXT_NODE); +} + +bool +EditorBase::IsTextNode(nsINode* aNode) +{ + return aNode->NodeType() == nsIDOMNode::TEXT_NODE; +} + +nsCOMPtr<nsIDOMNode> +EditorBase::GetChildAt(nsIDOMNode* aParent, int32_t aOffset) +{ + nsCOMPtr<nsIDOMNode> resultNode; + + nsCOMPtr<nsIContent> parent = do_QueryInterface(aParent); + + NS_ENSURE_TRUE(parent, resultNode); + + resultNode = do_QueryInterface(parent->GetChildAt(aOffset)); + + return resultNode; +} + +/** + * GetNodeAtRangeOffsetPoint() returns the node at this position in a range, + * assuming that aParentOrNode is the node itself if it's a text node, or + * the node's parent otherwise. + */ +nsIContent* +EditorBase::GetNodeAtRangeOffsetPoint(nsIDOMNode* aParentOrNode, + int32_t aOffset) +{ + nsCOMPtr<nsINode> parentOrNode = do_QueryInterface(aParentOrNode); + NS_ENSURE_TRUE(parentOrNode || !aParentOrNode, nullptr); + if (parentOrNode->GetAsText()) { + return parentOrNode->AsContent(); + } + return parentOrNode->GetChildAt(aOffset); +} + +/** + * GetStartNodeAndOffset() returns whatever the start parent & offset is of + * the first range in the selection. + */ +nsresult +EditorBase::GetStartNodeAndOffset(Selection* aSelection, + nsIDOMNode** outStartNode, + int32_t* outStartOffset) +{ + NS_ENSURE_TRUE(outStartNode && outStartOffset && aSelection, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> startNode; + nsresult rv = GetStartNodeAndOffset(aSelection, getter_AddRefs(startNode), + outStartOffset); + if (NS_FAILED(rv)) { + return rv; + } + + if (startNode) { + NS_ADDREF(*outStartNode = startNode->AsDOMNode()); + } else { + *outStartNode = nullptr; + } + return NS_OK; +} + +nsresult +EditorBase::GetStartNodeAndOffset(Selection* aSelection, + nsINode** aStartNode, + int32_t* aStartOffset) +{ + MOZ_ASSERT(aSelection); + MOZ_ASSERT(aStartNode); + MOZ_ASSERT(aStartOffset); + + *aStartNode = nullptr; + *aStartOffset = 0; + + if (!aSelection->RangeCount()) { + return NS_ERROR_FAILURE; + } + + const nsRange* range = aSelection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE); + + NS_IF_ADDREF(*aStartNode = range->GetStartParent()); + *aStartOffset = range->StartOffset(); + return NS_OK; +} + +/** + * GetEndNodeAndOffset() returns whatever the end parent & offset is of + * the first range in the selection. + */ +nsresult +EditorBase::GetEndNodeAndOffset(Selection* aSelection, + nsIDOMNode** outEndNode, + int32_t* outEndOffset) +{ + NS_ENSURE_TRUE(outEndNode && outEndOffset && aSelection, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsINode> endNode; + nsresult rv = GetEndNodeAndOffset(aSelection, getter_AddRefs(endNode), + outEndOffset); + NS_ENSURE_SUCCESS(rv, rv); + + if (endNode) { + NS_ADDREF(*outEndNode = endNode->AsDOMNode()); + } else { + *outEndNode = nullptr; + } + return NS_OK; +} + +nsresult +EditorBase::GetEndNodeAndOffset(Selection* aSelection, + nsINode** aEndNode, + int32_t* aEndOffset) +{ + MOZ_ASSERT(aSelection); + MOZ_ASSERT(aEndNode); + MOZ_ASSERT(aEndOffset); + + *aEndNode = nullptr; + *aEndOffset = 0; + + NS_ENSURE_TRUE(aSelection->RangeCount(), NS_ERROR_FAILURE); + + const nsRange* range = aSelection->GetRangeAt(0); + NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + + NS_ENSURE_TRUE(range->IsPositioned(), NS_ERROR_FAILURE); + + NS_IF_ADDREF(*aEndNode = range->GetEndParent()); + *aEndOffset = range->EndOffset(); + return NS_OK; +} + +/** + * IsPreformatted() checks the style info for the node for the preformatted + * text style. + */ +nsresult +EditorBase::IsPreformatted(nsIDOMNode* aNode, + bool* aResult) +{ + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + + NS_ENSURE_TRUE(aResult && content, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + // Look at the node (and its parent if it's not an element), and grab its style context + RefPtr<nsStyleContext> elementStyle; + if (!content->IsElement()) { + content = content->GetParent(); + } + if (content && content->IsElement()) { + elementStyle = nsComputedDOMStyle::GetStyleContextForElementNoFlush(content->AsElement(), + nullptr, + ps); + } + + if (!elementStyle) { + // Consider nodes without a style context to be NOT preformatted: + // For instance, this is true of JS tags inside the body (which show + // up as #text nodes but have no style context). + *aResult = false; + return NS_OK; + } + + const nsStyleText* styleText = elementStyle->StyleText(); + + *aResult = styleText->WhiteSpaceIsSignificant(); + return NS_OK; +} + + +/** + * This splits a node "deeply", splitting children as appropriate. The place + * to split is represented by a DOM point at {splitPointParent, + * splitPointOffset}. That DOM point must be inside aNode, which is the node + * to split. We return the offset in the parent of aNode where the split + * terminates - where you would want to insert a new element, for instance, if + * that's why you were splitting the node. + * + * -1 is returned on failure, in unlikely cases like the selection being + * unavailable or cloning the node failing. Make sure not to use the returned + * offset for anything without checking that it's valid! If you're not using + * the offset, it's okay to ignore the return value. + */ +int32_t +EditorBase::SplitNodeDeep(nsIContent& aNode, + nsIContent& aSplitPointParent, + int32_t aSplitPointOffset, + EmptyContainers aEmptyContainers, + nsIContent** aOutLeftNode, + nsIContent** aOutRightNode) +{ + MOZ_ASSERT(&aSplitPointParent == &aNode || + EditorUtils::IsDescendantOf(&aSplitPointParent, &aNode)); + int32_t offset = aSplitPointOffset; + + nsCOMPtr<nsIContent> leftNode, rightNode; + OwningNonNull<nsIContent> nodeToSplit = aSplitPointParent; + while (true) { + // Need to insert rules code call here to do things like not split a list + // if you are after the last <li> or before the first, etc. For now we + // just have some smarts about unneccessarily splitting text nodes, which + // should be universal enough to put straight in this EditorBase routine. + + bool didSplit = false; + + if ((aEmptyContainers == EmptyContainers::yes && + !nodeToSplit->GetAsText()) || + (offset && offset != (int32_t)nodeToSplit->Length())) { + didSplit = true; + ErrorResult rv; + nsCOMPtr<nsIContent> newLeftNode = SplitNode(nodeToSplit, offset, rv); + NS_ENSURE_TRUE(!NS_FAILED(rv.StealNSResult()), -1); + + rightNode = nodeToSplit; + leftNode = newLeftNode; + } + + NS_ENSURE_TRUE(nodeToSplit->GetParent(), -1); + OwningNonNull<nsIContent> parentNode = *nodeToSplit->GetParent(); + + if (!didSplit && offset) { + // Must be "end of text node" case, we didn't split it, just move past it + offset = parentNode->IndexOf(nodeToSplit) + 1; + leftNode = nodeToSplit; + } else { + offset = parentNode->IndexOf(nodeToSplit); + rightNode = nodeToSplit; + } + + if (nodeToSplit == &aNode) { + // we split all the way up to (and including) aNode; we're done + break; + } + + nodeToSplit = parentNode; + } + + if (aOutLeftNode) { + leftNode.forget(aOutLeftNode); + } + if (aOutRightNode) { + rightNode.forget(aOutRightNode); + } + + return offset; +} + +/** + * This joins two like nodes "deeply", joining children as appropriate. + * Returns the point of the join, or (nullptr, -1) in case of error. + */ +EditorDOMPoint +EditorBase::JoinNodeDeep(nsIContent& aLeftNode, + nsIContent& aRightNode) +{ + // While the rightmost children and their descendants of the left node match + // the leftmost children and their descendants of the right node, join them + // up. + + nsCOMPtr<nsIContent> leftNodeToJoin = &aLeftNode; + nsCOMPtr<nsIContent> rightNodeToJoin = &aRightNode; + nsCOMPtr<nsINode> parentNode = aRightNode.GetParentNode(); + + EditorDOMPoint ret; + + while (leftNodeToJoin && rightNodeToJoin && parentNode && + AreNodesSameType(leftNodeToJoin, rightNodeToJoin)) { + uint32_t length = leftNodeToJoin->Length(); + + ret.node = rightNodeToJoin; + ret.offset = length; + + // Do the join + nsresult rv = JoinNodes(*leftNodeToJoin, *rightNodeToJoin); + NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); + + if (parentNode->GetAsText()) { + // We've joined all the way down to text nodes, we're done! + return ret; + } + + // Get new left and right nodes, and begin anew + parentNode = rightNodeToJoin; + leftNodeToJoin = parentNode->GetChildAt(length - 1); + rightNodeToJoin = parentNode->GetChildAt(length); + + // Skip over non-editable nodes + while (leftNodeToJoin && !IsEditable(leftNodeToJoin)) { + leftNodeToJoin = leftNodeToJoin->GetPreviousSibling(); + } + if (!leftNodeToJoin) { + return ret; + } + + while (rightNodeToJoin && !IsEditable(rightNodeToJoin)) { + rightNodeToJoin = rightNodeToJoin->GetNextSibling(); + } + if (!rightNodeToJoin) { + return ret; + } + } + + return ret; +} + +void +EditorBase::BeginUpdateViewBatch() +{ + NS_PRECONDITION(mUpdateCount >= 0, "bad state"); + + if (!mUpdateCount) { + // Turn off selection updates and notifications. + RefPtr<Selection> selection = GetSelection(); + if (selection) { + selection->StartBatchChanges(); + } + } + + mUpdateCount++; +} + +nsresult +EditorBase::EndUpdateViewBatch() +{ + NS_PRECONDITION(mUpdateCount > 0, "bad state"); + + if (mUpdateCount <= 0) { + mUpdateCount = 0; + return NS_ERROR_FAILURE; + } + + mUpdateCount--; + + if (!mUpdateCount) { + // Turn selection updating and notifications back on. + RefPtr<Selection> selection = GetSelection(); + if (selection) { + selection->EndBatchChanges(); + } + } + + return NS_OK; +} + +bool +EditorBase::GetShouldTxnSetSelection() +{ + return mShouldTxnSetSelection; +} + +NS_IMETHODIMP +EditorBase::DeleteSelectionImpl(EDirection aAction, + EStripWrappers aStripWrappers) +{ + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + RefPtr<EditAggregateTransaction> transaction; + nsCOMPtr<nsINode> deleteNode; + int32_t deleteCharOffset = 0, deleteCharLength = 0; + nsresult rv = CreateTxnForDeleteSelection(aAction, + getter_AddRefs(transaction), + getter_AddRefs(deleteNode), + &deleteCharOffset, + &deleteCharLength); + nsCOMPtr<nsIDOMCharacterData> deleteCharData(do_QueryInterface(deleteNode)); + + if (NS_SUCCEEDED(rv)) { + AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aAction); + // Notify nsIEditActionListener::WillDelete[Selection|Text|Node] + if (!deleteNode) { + for (auto& listener : mActionListeners) { + listener->WillDeleteSelection(selection); + } + } else if (deleteCharData) { + for (auto& listener : mActionListeners) { + listener->WillDeleteText(deleteCharData, deleteCharOffset, 1); + } + } else { + for (auto& listener : mActionListeners) { + listener->WillDeleteNode(deleteNode->AsDOMNode()); + } + } + + // Delete the specified amount + rv = DoTransaction(transaction); + + // Notify nsIEditActionListener::DidDelete[Selection|Text|Node] + if (!deleteNode) { + for (auto& listener : mActionListeners) { + listener->DidDeleteSelection(selection); + } + } else if (deleteCharData) { + for (auto& listener : mActionListeners) { + listener->DidDeleteText(deleteCharData, deleteCharOffset, 1, rv); + } + } else { + for (auto& listener : mActionListeners) { + listener->DidDeleteNode(deleteNode->AsDOMNode(), rv); + } + } + } + + return rv; +} + +already_AddRefed<Element> +EditorBase::DeleteSelectionAndCreateElement(nsIAtom& aTag) +{ + nsresult rv = DeleteSelectionAndPrepareToCreateNode(); + NS_ENSURE_SUCCESS(rv, nullptr); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, nullptr); + + nsCOMPtr<nsINode> node = selection->GetAnchorNode(); + uint32_t offset = selection->AnchorOffset(); + + nsCOMPtr<Element> newElement = CreateNode(&aTag, node, offset); + + // We want the selection to be just after the new node + rv = selection->Collapse(node, offset + 1); + NS_ENSURE_SUCCESS(rv, nullptr); + + return newElement.forget(); +} + +TextComposition* +EditorBase::GetComposition() const +{ + return mComposition; +} + +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; +} + +nsresult +EditorBase::DeleteSelectionAndPrepareToCreateNode() +{ + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + MOZ_ASSERT(selection->GetAnchorFocusRange()); + + if (!selection->GetAnchorFocusRange()->Collapsed()) { + nsresult rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(selection->GetAnchorFocusRange() && + selection->GetAnchorFocusRange()->Collapsed(), + "Selection not collapsed after delete"); + } + + // If the selection is a chardata node, split it if necessary and compute + // where to put the new node + nsCOMPtr<nsINode> node = selection->GetAnchorNode(); + MOZ_ASSERT(node, "Selection has no ranges in it"); + + if (node && node->IsNodeOfType(nsINode::eDATA_NODE)) { + NS_ASSERTION(node->GetParentNode(), + "It's impossible to insert into chardata with no parent -- " + "fix the caller"); + NS_ENSURE_STATE(node->GetParentNode()); + + uint32_t offset = selection->AnchorOffset(); + + if (!offset) { + nsresult rv = selection->Collapse(node->GetParentNode(), + node->GetParentNode()->IndexOf(node)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + } else if (offset == node->Length()) { + nsresult rv = + selection->Collapse(node->GetParentNode(), + node->GetParentNode()->IndexOf(node) + 1); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCOMPtr<nsIDOMNode> tmp; + nsresult rv = SplitNode(node->AsDOMNode(), offset, getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + rv = selection->Collapse(node->GetParentNode(), + node->GetParentNode()->IndexOf(node)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +void +EditorBase::DoAfterDoTransaction(nsITransaction* aTxn) +{ + bool isTransientTransaction; + MOZ_ALWAYS_SUCCEEDS(aTxn->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; + GetModificationCount(&modCount); + 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<ChangeAttributeTransaction> +EditorBase::CreateTxnForSetAttribute(Element& aElement, + nsIAtom& aAttribute, + const nsAString& aValue) +{ + RefPtr<ChangeAttributeTransaction> transaction = + new ChangeAttributeTransaction(aElement, aAttribute, &aValue); + + return transaction.forget(); +} + +already_AddRefed<ChangeAttributeTransaction> +EditorBase::CreateTxnForRemoveAttribute(Element& aElement, + nsIAtom& aAttribute) +{ + RefPtr<ChangeAttributeTransaction> transaction = + new ChangeAttributeTransaction(aElement, aAttribute, nullptr); + + return transaction.forget(); +} + +already_AddRefed<CreateElementTransaction> +EditorBase::CreateTxnForCreateElement(nsIAtom& aTag, + nsINode& aParent, + int32_t aPosition) +{ + RefPtr<CreateElementTransaction> transaction = + new CreateElementTransaction(*this, aTag, aParent, aPosition); + + return transaction.forget(); +} + + +already_AddRefed<InsertNodeTransaction> +EditorBase::CreateTxnForInsertNode(nsIContent& aNode, + nsINode& aParent, + int32_t aPosition) +{ + RefPtr<InsertNodeTransaction> transaction = + new InsertNodeTransaction(aNode, aParent, aPosition, *this); + return transaction.forget(); +} + +nsresult +EditorBase::CreateTxnForDeleteNode(nsINode* aNode, + DeleteNodeTransaction** aTransaction) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + + RefPtr<DeleteNodeTransaction> transaction = new DeleteNodeTransaction(); + + nsresult rv = transaction->Init(this, aNode, &mRangeUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + transaction.forget(aTransaction); + return NS_OK; +} + +already_AddRefed<CompositionTransaction> +EditorBase::CreateTxnForComposition(const nsAString& aStringToInsert) +{ + MOZ_ASSERT(mIMETextNode); + // During handling IME composition, mComposition must have been initialized. + // TODO: We can simplify CompositionTransaction::Init() with TextComposition + // class. + RefPtr<CompositionTransaction> transaction = + new CompositionTransaction(*mIMETextNode, mIMETextOffset, mIMETextLength, + mComposition->GetRanges(), aStringToInsert, + *this, &mRangeUpdater); + return transaction.forget(); +} + +NS_IMETHODIMP +EditorBase::CreateTxnForAddStyleSheet(StyleSheet* aSheet, + AddStyleSheetTransaction** aTransaction) +{ + RefPtr<AddStyleSheetTransaction> transaction = new AddStyleSheetTransaction(); + + nsresult rv = transaction->Init(this, aSheet); + if (NS_SUCCEEDED(rv)) { + transaction.forget(aTransaction); + } + + return rv; +} + +NS_IMETHODIMP +EditorBase::CreateTxnForRemoveStyleSheet( + StyleSheet* aSheet, + RemoveStyleSheetTransaction** aTransaction) +{ + RefPtr<RemoveStyleSheetTransaction> transaction = + new RemoveStyleSheetTransaction(); + + nsresult rv = transaction->Init(this, aSheet); + if (NS_SUCCEEDED(rv)) { + transaction.forget(aTransaction); + } + + return rv; +} + +nsresult +EditorBase::CreateTxnForDeleteSelection(EDirection aAction, + EditAggregateTransaction** aTransaction, + nsINode** aNode, + int32_t* aOffset, + int32_t* aLength) +{ + MOZ_ASSERT(aTransaction); + *aTransaction = nullptr; + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + // Check whether the selection is collapsed and we should do nothing: + if (selection->Collapsed() && aAction == eNone) { + return NS_OK; + } + + // allocate the out-param transaction + RefPtr<EditAggregateTransaction> aggregateTransaction = + new EditAggregateTransaction(); + + for (uint32_t rangeIdx = 0; rangeIdx < selection->RangeCount(); ++rangeIdx) { + RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); + NS_ENSURE_STATE(range); + + // Same with range as with selection; if it is collapsed and action + // is eNone, do nothing. + if (!range->Collapsed()) { + RefPtr<DeleteRangeTransaction> transaction = new DeleteRangeTransaction(); + transaction->Init(this, range, &mRangeUpdater); + aggregateTransaction->AppendChild(transaction); + } else if (aAction != eNone) { + // we have an insertion point. delete the thing in front of it or + // behind it, depending on aAction + nsresult rv = CreateTxnForDeleteInsertionPoint(range, aAction, + aggregateTransaction, + aNode, aOffset, aLength); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + aggregateTransaction.forget(aTransaction); + + return NS_OK; +} + +already_AddRefed<DeleteTextTransaction> +EditorBase::CreateTxnForDeleteCharacter(nsGenericDOMDataNode& aData, + uint32_t aOffset, + EDirection aDirection) +{ + NS_ASSERTION(aDirection == eNext || aDirection == ePrevious, + "Invalid direction"); + nsAutoString data; + aData.GetData(data); + NS_ASSERTION(data.Length(), "Trying to delete from a zero-length node"); + NS_ENSURE_TRUE(data.Length(), nullptr); + + uint32_t segOffset = aOffset, segLength = 1; + if (aDirection == eNext) { + if (segOffset + 1 < data.Length() && + NS_IS_HIGH_SURROGATE(data[segOffset]) && + NS_IS_LOW_SURROGATE(data[segOffset+1])) { + // Delete both halves of the surrogate pair + ++segLength; + } + } else if (aOffset > 0) { + --segOffset; + if (segOffset > 0 && + NS_IS_LOW_SURROGATE(data[segOffset]) && + NS_IS_HIGH_SURROGATE(data[segOffset-1])) { + ++segLength; + --segOffset; + } + } else { + return nullptr; + } + return CreateTxnForDeleteText(aData, segOffset, segLength); +} + +//XXX: currently, this doesn't handle edge conditions because GetNext/GetPrior +//are not implemented +nsresult +EditorBase::CreateTxnForDeleteInsertionPoint( + nsRange* aRange, + EDirection aAction, + EditAggregateTransaction* aTransaction, + nsINode** aNode, + int32_t* aOffset, + int32_t* aLength) +{ + MOZ_ASSERT(aAction != eNone); + + // get the node and offset of the insertion point + nsCOMPtr<nsINode> node = aRange->GetStartParent(); + NS_ENSURE_STATE(node); + + int32_t offset = aRange->StartOffset(); + + // determine if the insertion point is at the beginning, middle, or end of + // the node + + uint32_t count = node->Length(); + + bool isFirst = !offset; + bool isLast = (count == (uint32_t)offset); + + // XXX: if isFirst && isLast, 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 + if (aAction == ePrevious && isFirst) { + // we're backspacing from the beginning of the node. Delete the first + // thing to our left + nsCOMPtr<nsIContent> priorNode = GetPriorNode(node, true); + NS_ENSURE_STATE(priorNode); + + // there is a priorNode, so delete its last child (if chardata, delete the + // last char). if it has no children, delete it + if (priorNode->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> priorNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(priorNode.get()); + uint32_t length = priorNode->Length(); + // Bail out for empty chardata XXX: Do we want to do something else? + NS_ENSURE_STATE(length); + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteCharacter(*priorNodeAsCharData, length, ePrevious); + NS_ENSURE_STATE(transaction); + + *aOffset = transaction->GetOffset(); + *aLength = transaction->GetNumCharsToDelete(); + aTransaction->AppendChild(transaction); + } else { + // priorNode is not chardata, so tell its parent to delete it + RefPtr<DeleteNodeTransaction> transaction; + nsresult rv = + CreateTxnForDeleteNode(priorNode, getter_AddRefs(transaction)); + NS_ENSURE_SUCCESS(rv, rv); + + aTransaction->AppendChild(transaction); + } + + NS_ADDREF(*aNode = priorNode); + + return NS_OK; + } + + if (aAction == eNext && isLast) { + // we're deleting from the end of the node. Delete the first thing to our + // right + nsCOMPtr<nsIContent> nextNode = GetNextNode(node, true); + NS_ENSURE_STATE(nextNode); + + // there is a nextNode, so delete its first child (if chardata, delete the + // first char). if it has no children, delete it + if (nextNode->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> nextNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(nextNode.get()); + uint32_t length = nextNode->Length(); + // Bail out for empty chardata XXX: Do we want to do something else? + NS_ENSURE_STATE(length); + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteCharacter(*nextNodeAsCharData, 0, eNext); + NS_ENSURE_STATE(transaction); + + *aOffset = transaction->GetOffset(); + *aLength = transaction->GetNumCharsToDelete(); + aTransaction->AppendChild(transaction); + } else { + // nextNode is not chardata, so tell its parent to delete it + RefPtr<DeleteNodeTransaction> transaction; + nsresult rv = + CreateTxnForDeleteNode(nextNode, getter_AddRefs(transaction)); + NS_ENSURE_SUCCESS(rv, rv); + aTransaction->AppendChild(transaction); + } + + NS_ADDREF(*aNode = nextNode); + + return NS_OK; + } + + if (node->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> nodeAsCharData = + static_cast<nsGenericDOMDataNode*>(node.get()); + // we have chardata, so delete a char at the proper offset + RefPtr<DeleteTextTransaction> transaction = + CreateTxnForDeleteCharacter(*nodeAsCharData, offset, aAction); + NS_ENSURE_STATE(transaction); + + aTransaction->AppendChild(transaction); + NS_ADDREF(*aNode = node); + *aOffset = transaction->GetOffset(); + *aLength = transaction->GetNumCharsToDelete(); + } else { + // we're either deleting a node or chardata, need to dig into the next/prev + // node to find out + nsCOMPtr<nsINode> selectedNode; + if (aAction == ePrevious) { + selectedNode = GetPriorNode(node, offset, true); + } else if (aAction == eNext) { + selectedNode = GetNextNode(node, offset, true); + } + + while (selectedNode && + selectedNode->IsNodeOfType(nsINode::eDATA_NODE) && + !selectedNode->Length()) { + // Can't delete an empty chardata node (bug 762183) + if (aAction == ePrevious) { + selectedNode = GetPriorNode(selectedNode, true); + } else if (aAction == eNext) { + selectedNode = GetNextNode(selectedNode, true); + } + } + NS_ENSURE_STATE(selectedNode); + + if (selectedNode->IsNodeOfType(nsINode::eDATA_NODE)) { + RefPtr<nsGenericDOMDataNode> selectedNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(selectedNode.get()); + // we are deleting from a chardata node, so do a character deletion + uint32_t position = 0; + if (aAction == ePrevious) { + position = selectedNode->Length(); + } + RefPtr<DeleteTextTransaction> deleteTextTransaction = + CreateTxnForDeleteCharacter(*selectedNodeAsCharData, position, + aAction); + NS_ENSURE_TRUE(deleteTextTransaction, NS_ERROR_NULL_POINTER); + + aTransaction->AppendChild(deleteTextTransaction); + *aOffset = deleteTextTransaction->GetOffset(); + *aLength = deleteTextTransaction->GetNumCharsToDelete(); + } else { + RefPtr<DeleteNodeTransaction> deleteNodeTransaction; + nsresult rv = + CreateTxnForDeleteNode(selectedNode, + getter_AddRefs(deleteNodeTransaction)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(deleteNodeTransaction, NS_ERROR_NULL_POINTER); + + aTransaction->AppendChild(deleteNodeTransaction); + } + + NS_ADDREF(*aNode = selectedNode); + } + + return NS_OK; +} + +nsresult +EditorBase::CreateRange(nsIDOMNode* aStartParent, + int32_t aStartOffset, + nsIDOMNode* aEndParent, + int32_t aEndOffset, + nsRange** aRange) +{ + return nsRange::CreateRange(aStartParent, aStartOffset, aEndParent, + aEndOffset, aRange); +} + +nsresult +EditorBase::AppendNodeToSelectionAsRange(nsIDOMNode* aNode) +{ + NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + + nsCOMPtr<nsIDOMNode> parentNode; + nsresult rv = aNode->GetParentNode(getter_AddRefs(parentNode)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(parentNode, NS_ERROR_NULL_POINTER); + + int32_t offset = GetChildOffset(aNode, parentNode); + + RefPtr<nsRange> range; + rv = CreateRange(parentNode, offset, parentNode, offset + 1, + getter_AddRefs(range)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER); + + return selection->AddRange(range); +} + +nsresult +EditorBase::ClearSelection() +{ + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); + return selection->RemoveAllRanges(); +} + +already_AddRefed<Element> +EditorBase::CreateHTMLContent(nsIAtom* aTag) +{ + MOZ_ASSERT(aTag); + + nsCOMPtr<nsIDocument> doc = GetDocument(); + if (!doc) { + 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 doc->CreateElem(nsDependentAtomString(aTag), nullptr, + kNameSpaceID_XHTML); +} + +nsresult +EditorBase::SetAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) +{ + return SetAttribute(aElement, aAttribute, aValue); +} + +nsresult +EditorBase::RemoveAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + bool aSuppressTransaction) +{ + return RemoveAttribute(aElement, aAttribute); +} + +nsresult +EditorBase::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) +{ + // 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. + + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent(); + NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED); + NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress, + "HandleKeyPressEvent gets non-keypress event"); + + // if we are readonly or disabled, then do nothing. + if (IsReadonly() || IsDisabled()) { + // consume backspace for disabled and readonly textfields, to prevent + // back in history, which could be confusing to users + if (nativeKeyEvent->mKeyCode == NS_VK_BACK) { + aKeyEvent->AsEvent()->PreventDefault(); + } + return NS_OK; + } + + switch (nativeKeyEvent->mKeyCode) { + case NS_VK_META: + case NS_VK_WIN: + case NS_VK_SHIFT: + case NS_VK_CONTROL: + case NS_VK_ALT: + aKeyEvent->AsEvent()->PreventDefault(); // consumed + return NS_OK; + case NS_VK_BACK: + if (nativeKeyEvent->IsControl() || nativeKeyEvent->IsAlt() || + nativeKeyEvent->IsMeta() || nativeKeyEvent->IsOS()) { + return NS_OK; + } + DeleteSelection(nsIEditor::ePrevious, nsIEditor::eStrip); + aKeyEvent->AsEvent()->PreventDefault(); // consumed + 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 (nativeKeyEvent->IsShift() || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + return NS_OK; + } + DeleteSelection(nsIEditor::eNext, nsIEditor::eStrip); + aKeyEvent->AsEvent()->PreventDefault(); // consumed + return NS_OK; + } + return NS_OK; +} + +nsresult +EditorBase::HandleInlineSpellCheck(EditAction action, + Selection* aSelection, + nsIDOMNode* previousSelectedNode, + int32_t previousSelectedOffset, + nsIDOMNode* aStartNode, + int32_t aStartOffset, + nsIDOMNode* aEndNode, + int32_t aEndOffset) +{ + // Have to cast action here because this method is from an IDL + return mInlineSpellChecker ? mInlineSpellChecker->SpellCheckAfterEditorChange( + (int32_t)action, aSelection, + previousSelectedNode, previousSelectedOffset, + aStartNode, aStartOffset, aEndNode, + aEndOffset) + : NS_OK; +} + +already_AddRefed<nsIContent> +EditorBase::FindSelectionRoot(nsINode* aNode) +{ + nsCOMPtr<nsIContent> rootContent = GetRoot(); + return rootContent.forget(); +} + +nsresult +EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget) +{ + nsCOMPtr<nsINode> targetNode = do_QueryInterface(aFocusEventTarget); + NS_ENSURE_TRUE(targetNode, NS_ERROR_INVALID_ARG); + nsCOMPtr<nsIContent> selectionRootContent = FindSelectionRoot(targetNode); + if (!selectionRootContent) { + return NS_OK; + } + + bool isTargetDoc = + targetNode->NodeType() == nsIDOMNode::DOCUMENT_NODE && + targetNode->HasFlag(NODE_IS_EDITABLE); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsISelectionController> selCon; + nsresult rv = GetSelectionController(getter_AddRefs(selCon)); + NS_ENSURE_SUCCESS(rv, rv); + + // Init the caret + RefPtr<nsCaret> caret = presShell->GetCaret(); + NS_ENSURE_TRUE(caret, NS_ERROR_UNEXPECTED); + caret->SetIgnoreUserModify(false); + caret->SetSelection(selection); + selCon->SetCaretReadOnly(IsReadonly()); + selCon->SetCaretEnabled(true); + + // Init selection + selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON); + selCon->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL); + selCon->RepaintSelection(nsISelectionController::SELECTION_NORMAL); + // 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()) { + selection->SetAncestorLimiter(selectionRootContent); + } else { + selection->SetAncestorLimiter(nullptr); + } + + // XXX What case needs this? + if (isTargetDoc) { + int32_t rangeCount; + selection->GetRangeCount(&rangeCount); + if (!rangeCount) { + BeginningOfDocument(); + } + } + + // 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 && !mIMETextNode && mIMETextLength) { + // We need to look for the new mIMETextNode from current selection. + // XXX If selection is changed during reframe, this doesn't work well! + nsRange* firstRange = selection->GetRangeAt(0); + NS_ENSURE_TRUE(firstRange, NS_ERROR_FAILURE); + nsCOMPtr<nsINode> startNode = firstRange->GetStartParent(); + int32_t startOffset = firstRange->StartOffset(); + FindBetterInsertionPoint(startNode, startOffset); + Text* textNode = startNode->GetAsText(); + MOZ_ASSERT(textNode, + "There must be text node if mIMETextLength is larger than 0"); + if (textNode) { + MOZ_ASSERT(textNode->Length() >= mIMETextOffset + mIMETextLength, + "The text node must be different from the old mIMETextNode"); + CompositionTransaction::SetIMESelection(*this, textNode, mIMETextOffset, + mIMETextLength, + mComposition->GetRanges()); + } + } + + return NS_OK; +} + +class RepaintSelectionRunner final : public Runnable { +public: + explicit RepaintSelectionRunner(nsISelectionController* aSelectionController) + : mSelectionController(aSelectionController) + { + } + + NS_IMETHOD Run() override + { + mSelectionController->RepaintSelection( + nsISelectionController::SELECTION_NORMAL); + return NS_OK; + } + +private: + nsCOMPtr<nsISelectionController> mSelectionController; +}; + +NS_IMETHODIMP +EditorBase::FinalizeSelection() +{ + nsCOMPtr<nsISelectionController> selCon; + nsresult rv = GetSelectionController(getter_AddRefs(selCon)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + selection->SetAncestorLimiter(nullptr); + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED); + + selCon->SetCaretEnabled(false); + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, NS_ERROR_NOT_INITIALIZED); + fm->UpdateCaretForCaretBrowsingMode(); + + if (!HasIndependentSelection()) { + // If this editor doesn't have an independent selection, i.e., it must + // mean that it is an HTML editor, the selection controller is shared with + // presShell. So, even this editor loses focus, other part of the document + // may still have focus. + nsCOMPtr<nsIDocument> doc = GetDocument(); + ErrorResult ret; + if (!doc || !doc->HasFocus(ret)) { + // If the document already lost focus, mark the selection as disabled. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_DISABLED); + } else { + // Otherwise, mark selection as normal because outside of a + // contenteditable element should be selected with normal selection + // color after here. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON); + } + } else if (IsFormWidget() || IsPasswordEditor() || + IsReadonly() || IsDisabled() || IsInputFiltered()) { + // In <input> or <textarea>, the independent selection should be hidden + // while this editor doesn't have focus. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_HIDDEN); + } else { + // Otherwise, although we're not sure how this case happens, the + // independent selection should be marked as disabled. + selCon->SetDisplaySelection(nsISelectionController::SELECTION_DISABLED); + } + + + // FinalizeSelection might be called from ContentRemoved even if selection + // isn't updated. So we need to call RepaintSelection after updated it. + nsContentUtils::AddScriptRunner( + new RepaintSelectionRunner(selCon)); + return NS_OK; +} + +Element* +EditorBase::GetRoot() +{ + if (!mRootElement) { + // Let GetRootElement() do the work + nsCOMPtr<nsIDOMElement> root; + GetRootElement(getter_AddRefs(root)); + } + + return mRootElement; +} + +Element* +EditorBase::GetEditorRoot() +{ + return GetRoot(); +} + +Element* +EditorBase::GetExposedRoot() +{ + Element* rootElement = GetRoot(); + + // For plaintext editors, we need to ask the input/textarea element directly. + if (rootElement && rootElement->IsRootOfNativeAnonymousSubtree()) { + rootElement = rootElement->GetParent()->AsElement(); + } + + return rootElement; +} + +nsresult +EditorBase::DetermineCurrentDirection() +{ + // Get the current root direction from its frame + nsIContent* rootElement = GetExposedRoot(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE); + + // If we don't have an explicit direction, determine our direction + // from the content's direction + if (!(mFlags & (nsIPlaintextEditor::eEditorLeftToRight | + nsIPlaintextEditor::eEditorRightToLeft))) { + nsIFrame* frame = rootElement->GetPrimaryFrame(); + NS_ENSURE_TRUE(frame, 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 (frame->StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL) { + mFlags |= nsIPlaintextEditor::eEditorRightToLeft; + } else { + mFlags |= nsIPlaintextEditor::eEditorLeftToRight; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SwitchTextDirection() +{ + // Get the current root direction from its frame + nsIContent* rootElement = GetExposedRoot(); + + nsresult rv = DetermineCurrentDirection(); + NS_ENSURE_SUCCESS(rv, rv); + + // Apply the opposite direction + if (mFlags & nsIPlaintextEditor::eEditorRightToLeft) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorLeftToRight), + "Unexpected mutually exclusive flag"); + mFlags &= ~nsIPlaintextEditor::eEditorRightToLeft; + mFlags |= nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("ltr"), true); + } else if (mFlags & nsIPlaintextEditor::eEditorLeftToRight) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorRightToLeft), + "Unexpected mutually exclusive flag"); + mFlags |= nsIPlaintextEditor::eEditorRightToLeft; + mFlags &= ~nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("rtl"), true); + } + + if (NS_SUCCEEDED(rv)) { + FireInputEvent(); + } + + return rv; +} + +void +EditorBase::SwitchTextDirectionTo(uint32_t aDirection) +{ + // Get the current root direction from its frame + nsIContent* rootElement = GetExposedRoot(); + + nsresult rv = DetermineCurrentDirection(); + NS_ENSURE_SUCCESS_VOID(rv); + + // Apply the requested direction + if (aDirection == nsIPlaintextEditor::eEditorLeftToRight && + (mFlags & nsIPlaintextEditor::eEditorRightToLeft)) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorLeftToRight), + "Unexpected mutually exclusive flag"); + mFlags &= ~nsIPlaintextEditor::eEditorRightToLeft; + mFlags |= nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("ltr"), true); + } else if (aDirection == nsIPlaintextEditor::eEditorRightToLeft && + (mFlags & nsIPlaintextEditor::eEditorLeftToRight)) { + NS_ASSERTION(!(mFlags & nsIPlaintextEditor::eEditorRightToLeft), + "Unexpected mutually exclusive flag"); + mFlags |= nsIPlaintextEditor::eEditorRightToLeft; + mFlags &= ~nsIPlaintextEditor::eEditorLeftToRight; + rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("rtl"), true); + } + + if (NS_SUCCEEDED(rv)) { + FireInputEvent(); + } +} + +#if DEBUG_JOE +void +EditorBase::DumpNode(nsIDOMNode* aNode, + int32_t indent) +{ + for (int32_t i = 0; i < indent; i++) { + printf(" "); + } + + nsCOMPtr<nsIDOMElement> element = do_QueryInterface(aNode); + nsCOMPtr<nsIDOMDocumentFragment> docfrag = do_QueryInterface(aNode); + + if (element || docfrag) { + if (element) { + nsAutoString tag; + element->GetTagName(tag); + printf("<%s>\n", NS_LossyConvertUTF16toASCII(tag).get()); + } else { + printf("<document fragment>\n"); + } + nsCOMPtr<nsIDOMNodeList> childList; + aNode->GetChildNodes(getter_AddRefs(childList)); + NS_ENSURE_TRUE(childList, NS_ERROR_NULL_POINTER); + uint32_t numChildren; + childList->GetLength(&numChildren); + nsCOMPtr<nsIDOMNode> child, tmp; + aNode->GetFirstChild(getter_AddRefs(child)); + for (uint32_t i = 0; i < numChildren; i++) { + DumpNode(child, indent + 1); + child->GetNextSibling(getter_AddRefs(tmp)); + child = tmp; + } + } else if (IsTextNode(aNode)) { + nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(aNode); + nsAutoString str; + textNode->GetData(str); + nsAutoCString cstr; + LossyCopyUTF16toASCII(str, cstr); + cstr.ReplaceChar('\n', ' '); + printf("<textnode> %s\n", cstr.get()); + } +} +#endif + +bool +EditorBase::IsModifiableNode(nsIDOMNode* aNode) +{ + return true; +} + +bool +EditorBase::IsModifiableNode(nsINode* aNode) +{ + return true; +} + +already_AddRefed<nsIContent> +EditorBase::GetFocusedContent() +{ + nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget(); + if (!piTarget) { + return nullptr; + } + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, nullptr); + + nsCOMPtr<nsIContent> content = fm->GetFocusedContent(); + return SameCOMIdentity(content, piTarget) ? content.forget() : nullptr; +} + +already_AddRefed<nsIContent> +EditorBase::GetFocusedContentForIME() +{ + return GetFocusedContent(); +} + +bool +EditorBase::IsActiveInDOMWindow() +{ + nsCOMPtr<nsIDOMEventTarget> piTarget = GetDOMEventTarget(); + if (!piTarget) { + return false; + } + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + NS_ENSURE_TRUE(fm, false); + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocWeak); + nsPIDOMWindowOuter* ourWindow = doc->GetWindow(); + nsCOMPtr<nsPIDOMWindowOuter> win; + nsIContent* content = + nsFocusManager::GetFocusedDescendant(ourWindow, false, + getter_AddRefs(win)); + return SameCOMIdentity(content, piTarget); +} + +bool +EditorBase::IsAcceptableInputEvent(nsIDOMEvent* aEvent) +{ + // If the event is trusted, the event should always cause input. + NS_ENSURE_TRUE(aEvent, false); + + WidgetEvent* widgetEvent = aEvent->WidgetEventPtr(); + if (NS_WARN_IF(!widgetEvent)) { + return false; + } + + // If this is dispatched by using cordinates but this editor doesn't have + // focus, we shouldn't handle it. + if (widgetEvent->IsUsingCoordinates()) { + nsCOMPtr<nsIContent> focusedContent = GetFocusedContent(); + if (!focusedContent) { + 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; + WidgetGUIEvent* widgetGUIEvent = nullptr; + switch (widgetEvent->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. + widgetGUIEvent = aEvent->WidgetEventPtr()->AsCompositionEvent(); + needsWidget = true; + break; + default: + break; + } + if (needsWidget && + (!widgetGUIEvent || !widgetGUIEvent->mWidget)) { + return false; + } + + // Accept all trusted events. + if (widgetEvent->IsTrusted()) { + return true; + } + + // Ignore untrusted mouse event. + // XXX Why are we handling other untrusted input events? + if (widgetEvent->AsMouseEventBase()) { + return false; + } + + // Otherwise, we shouldn't handle any input events when we're not an active + // element of the DOM window. + return IsActiveInDOMWindow(); +} + +void +EditorBase::OnFocus(nsIDOMEventTarget* aFocusEventTarget) +{ + InitializeSelection(aFocusEventTarget); + if (mInlineSpellChecker) { + mInlineSpellChecker->UpdateCurrentDictionary(); + } +} + +NS_IMETHODIMP +EditorBase::GetSuppressDispatchingInputEvent(bool* aSuppressed) +{ + NS_ENSURE_ARG_POINTER(aSuppressed); + *aSuppressed = !mDispatchInputEvent; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::SetSuppressDispatchingInputEvent(bool aSuppress) +{ + mDispatchInputEvent = !aSuppress; + return NS_OK; +} + +NS_IMETHODIMP +EditorBase::GetIsInEditAction(bool* aIsInEditAction) +{ + MOZ_ASSERT(aIsInEditAction, "aIsInEditAction must not be null"); + *aIsInEditAction = mIsInEditAction; + return NS_OK; +} + +int32_t +EditorBase::GetIMESelectionStartOffsetIn(nsINode* aTextNode) +{ + MOZ_ASSERT(aTextNode, "aTextNode must not be nullptr"); + + nsCOMPtr<nsISelectionController> selectionController; + nsresult rv = GetSelectionController(getter_AddRefs(selectionController)); + NS_ENSURE_SUCCESS(rv, -1); + NS_ENSURE_TRUE(selectionController, -1); + + int32_t minOffset = INT32_MAX; + static const SelectionType kIMESelectionTypes[] = { + SelectionType::eIMERawClause, + SelectionType::eIMESelectedRawClause, + SelectionType::eIMEConvertedClause, + SelectionType::eIMESelectedClause + }; + for (auto selectionType : kIMESelectionTypes) { + RefPtr<Selection> selection = GetSelection(selectionType); + if (!selection) { + continue; + } + for (uint32_t i = 0; i < selection->RangeCount(); i++) { + RefPtr<nsRange> range = selection->GetRangeAt(i); + if (NS_WARN_IF(!range)) { + continue; + } + if (NS_WARN_IF(range->GetStartParent() != aTextNode)) { + // ignore the start offset... + } else { + MOZ_ASSERT(range->StartOffset() >= 0, + "start offset shouldn't be negative"); + minOffset = std::min(minOffset, range->StartOffset()); + } + if (NS_WARN_IF(range->GetEndParent() != aTextNode)) { + // ignore the end offset... + } else { + MOZ_ASSERT(range->EndOffset() >= 0, + "start offset shouldn't be negative"); + minOffset = std::min(minOffset, range->EndOffset()); + } + } + } + return minOffset < INT32_MAX ? minOffset : -1; +} + +void +EditorBase::HideCaret(bool aHide) +{ + if (mHidingCaret == aHide) { + return; + } + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE_VOID(presShell); + RefPtr<nsCaret> caret = presShell->GetCaret(); + NS_ENSURE_TRUE_VOID(caret); + + mHidingCaret = aHide; + if (aHide) { + caret->AddForceHide(); + } else { + caret->RemoveForceHide(); + } +} + +} // namespace mozilla |