summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/EditorBase.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'editor/libeditor/EditorBase.cpp')
-rw-r--r--editor/libeditor/EditorBase.cpp5244
1 files changed, 5244 insertions, 0 deletions
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp
new file mode 100644
index 000000000..13505b2d3
--- /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