/* -*- 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) if (tmp->mEventListener) { EditorEventListener* listener = reinterpret_cast<EditorEventListener*>(tmp->mEventListener.get()); listener->Disconnect(); tmp->mEventListener = nullptr; } 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; // Transaction may grab this instance. Therefore, they should be released // here for stopping the circular reference with this instance. if (mTxnMgr) { mTxnMgr->Clear(); mTxnMgr = 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