/* -*- 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 "HTMLEditor.h" #include "HTMLEditHelpers.h" #include "HTMLEditorInlines.h" #include "HTMLEditorNestedClasses.h" #include "AutoClonedRangeArray.h" #include "AutoSelectionRestorer.h" #include "CSSEditUtils.h" #include "EditAction.h" #include "EditorBase.h" #include "EditorDOMPoint.h" #include "EditorLineBreak.h" #include "EditorUtils.h" #include "ErrorList.h" #include "HTMLEditorEventListener.h" #include "HTMLEditUtils.h" #include "InsertNodeTransaction.h" #include "JoinNodesTransaction.h" #include "MoveNodeTransaction.h" #include "PendingStyles.h" #include "ReplaceTextTransaction.h" #include "SplitNodeTransaction.h" #include "WhiteSpaceVisibilityKeeper.h" #include "WSRunScanner.h" #include "mozilla/ComposerCommandsUpdater.h" #include "mozilla/ContentIterator.h" #include "mozilla/DebugOnly.h" #include "mozilla/EditorForwards.h" #include "mozilla/Encoding.h" // for Encoding #include "mozilla/FlushType.h" #include "mozilla/IMEStateManager.h" #include "mozilla/IntegerRange.h" // for IntegerRange #include "mozilla/InternalMutationEvent.h" #include "mozilla/mozInlineSpellChecker.h" #include "mozilla/Preferences.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_editor.h" #include "mozilla/StyleSheet.h" #include "mozilla/StyleSheetInlines.h" #include "mozilla/glean/EditorLibeditorMetrics.h" #include "mozilla/TextControlElement.h" #include "mozilla/TextEditor.h" #include "mozilla/TextEvents.h" #include "mozilla/TextServicesDocument.h" #include "mozilla/ToString.h" #include "mozilla/css/Loader.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Attr.h" #include "mozilla/dom/BorrowedAttrInfo.h" #include "mozilla/dom/DocumentFragment.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/ElementInlines.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/EventTarget.h" #include "mozilla/dom/HTMLAnchorElement.h" #include "mozilla/dom/HTMLBodyElement.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/HTMLButtonElement.h" #include "mozilla/dom/HTMLSummaryElement.h" #include "mozilla/dom/NameSpaceConstants.h" #include "mozilla/dom/Selection.h" #include "nsContentList.h" #include "nsContentUtils.h" #include "nsCRT.h" #include "nsDebug.h" #include "nsElementTable.h" #include "nsFocusManager.h" #include "nsGenericHTMLElement.h" #include "nsGkAtoms.h" #include "nsHTMLDocument.h" #include "nsIContent.h" #include "nsIContentInlines.h" // for nsINode::IsInDesignMode() #include "nsIEditActionListener.h" #include "nsIFrame.h" #include "nsIPrincipal.h" #include "nsISelectionController.h" #include "nsIURI.h" #include "nsIWidget.h" #include "nsNetUtil.h" #include "nsPresContext.h" #include "nsPrintfCString.h" #include "nsPIDOMWindow.h" #include "nsStyledElement.h" #include "nsTextFragment.h" #include "nsUnicharUtils.h" namespace mozilla { using namespace dom; using namespace widget; LazyLogModule gHTMLEditorFocusLog("HTMLEditorFocus"); using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using LeafNodeType = HTMLEditUtils::LeafNodeType; using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; // Some utilities to handle overloading of "A" tag for link and named anchor. static bool IsLinkTag(const nsAtom& aTagName) { return &aTagName == nsGkAtoms::href; } static bool IsNamedAnchorTag(const nsAtom& aTagName) { return &aTagName == nsGkAtoms::anchor; } // Helper struct for DoJoinNodes() and DoSplitNode(). struct MOZ_STACK_CLASS SavedRange final { RefPtr mSelection; nsCOMPtr mStartContainer; nsCOMPtr mEndContainer; uint32_t mStartOffset = 0; uint32_t mEndOffset = 0; }; /****************************************************************************** * HTMLEditor *****************************************************************************/ template Result HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( nsIContent& aContentToInsert, const EditorDOMPoint& aPointToInsert, SplitAtEdges aSplitAtEdges); template Result HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( Element& aContentToInsert, const EditorDOMPoint& aPointToInsert, SplitAtEdges aSplitAtEdges); template Result HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( Text& aContentToInsert, const EditorDOMPoint& aPointToInsert, SplitAtEdges aSplitAtEdges); MOZ_RUNINIT HTMLEditor::InitializeInsertingElement HTMLEditor::DoNothingForNewElement = [](HTMLEditor&, Element&, const EditorDOMPoint&) { return NS_OK; }; MOZ_RUNINIT HTMLEditor::InitializeInsertingElement HTMLEditor::InsertNewBRElement = [](HTMLEditor& aHTMLEditor, Element& aNewElement, const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY { MOZ_ASSERT(!aNewElement.IsInComposedDoc()); Result insertBRElementResultOrError = aHTMLEditor.InsertLineBreak(WithTransaction::No, LineBreakType::BRElement, EditorDOMPoint(&aNewElement, 0u)); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::No, " "LineBreakType::BRElement) failed"); return insertBRElementResultOrError.unwrapErr(); } insertBRElementResultOrError.unwrap().IgnoreCaretPointSuggestion(); return NS_OK; }; // static Result HTMLEditor::AppendNewElementToInsertingElement( HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName, Element& aNewElement, const InitializeInsertingElement& aInitializer) { MOZ_ASSERT(!aNewElement.IsInComposedDoc()); Result createNewElementResult = aHTMLEditor.CreateAndInsertElement( WithTransaction::No, const_cast(aTagName), EditorDOMPoint(&aNewElement, 0u), aInitializer); NS_WARNING_ASSERTION( createNewElementResult.isOk(), "HTMLEditor::CreateAndInsertElement(WithTransaction::No) failed"); return createNewElementResult; } // static Result HTMLEditor::AppendNewElementWithBRToInsertingElement( HTMLEditor& aHTMLEditor, const nsStaticAtom& aTagName, Element& aNewElement) { MOZ_ASSERT(!aNewElement.IsInComposedDoc()); Result createNewElementWithBRResult = HTMLEditor::AppendNewElementToInsertingElement( aHTMLEditor, aTagName, aNewElement, HTMLEditor::InsertNewBRElement); NS_WARNING_ASSERTION( createNewElementWithBRResult.isOk(), "HTMLEditor::AppendNewElementToInsertingElement() failed"); return createNewElementWithBRResult; } MOZ_RUNINIT HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributes = [](HTMLEditor&, const Element&, const Element&, int32_t, const nsAtom&, nsString&) { return true; }; MOZ_RUNINIT HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptId = [](HTMLEditor&, const Element&, const Element&, int32_t aNamespaceID, const nsAtom& aAttrName, nsString&) { return aNamespaceID != kNameSpaceID_None || &aAttrName != nsGkAtoms::id; }; MOZ_RUNINIT HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptDir = [](HTMLEditor&, const Element&, const Element&, int32_t aNamespaceID, const nsAtom& aAttrName, nsString&) { return aNamespaceID != kNameSpaceID_None || &aAttrName != nsGkAtoms::dir; }; MOZ_RUNINIT HTMLEditor::AttributeFilter HTMLEditor::CopyAllAttributesExceptIdAndDir = [](HTMLEditor&, const Element&, const Element&, int32_t aNamespaceID, const nsAtom& aAttrName, nsString&) { return !( aNamespaceID == kNameSpaceID_None && (&aAttrName == nsGkAtoms::id || &aAttrName == nsGkAtoms::dir)); }; HTMLEditor::HTMLEditor(const Document& aDocument) : EditorBase(EditorBase::EditorType::HTML), mCRInParagraphCreatesParagraph(false), mIsObjectResizingEnabled( StaticPrefs::editor_resizing_enabled_by_default()), mIsResizing(false), mPreserveRatio(false), mResizedObjectIsAnImage(false), mIsAbsolutelyPositioningEnabled( StaticPrefs::editor_positioning_enabled_by_default()), mResizedObjectIsAbsolutelyPositioned(false), mGrabberClicked(false), mIsMoving(false), mSnapToGridEnabled(false), mIsInlineTableEditingEnabled( StaticPrefs::editor_inline_table_editing_enabled_by_default()), mIsCSSPrefChecked(StaticPrefs::editor_use_css()), mOriginalX(0), mOriginalY(0), mResizedObjectX(0), mResizedObjectY(0), mResizedObjectWidth(0), mResizedObjectHeight(0), mResizedObjectMarginLeft(0), mResizedObjectMarginTop(0), mResizedObjectBorderLeft(0), mResizedObjectBorderTop(0), mXIncrementFactor(0), mYIncrementFactor(0), mWidthIncrementFactor(0), mHeightIncrementFactor(0), mInfoXIncrement(20), mInfoYIncrement(20), mPositionedObjectX(0), mPositionedObjectY(0), mPositionedObjectWidth(0), mPositionedObjectHeight(0), mPositionedObjectMarginLeft(0), mPositionedObjectMarginTop(0), mPositionedObjectBorderLeft(0), mPositionedObjectBorderTop(0), mGridSize(0), mDefaultParagraphSeparator(ParagraphSeparator::div) {} HTMLEditor::~HTMLEditor() { glean::htmleditors::with_beforeinput_listeners .EnumGet(static_cast( MayHaveBeforeInputEventListenersForTelemetry() ? 1 : 0)) .Add(); glean::htmleditors::overridden_by_beforeinput_listeners .EnumGet(static_cast< glean::htmleditors::OverriddenByBeforeinputListenersLabel>( mHasBeforeInputBeenCanceled ? 1 : 0)) .Add(); glean::htmleditors::with_mutation_listeners_without_beforeinput_listeners .EnumGet(static_cast< glean::htmleditors:: WithMutationListenersWithoutBeforeinputListenersLabel>( !MayHaveBeforeInputEventListenersForTelemetry() && MayHaveMutationEventListeners() ? 1 : 0)) .Add(); glean::htmleditors::with_mutation_observers_without_beforeinput_listeners .EnumGet(static_cast< glean::htmleditors:: WithMutationObserversWithoutBeforeinputListenersLabel>( !MayHaveBeforeInputEventListenersForTelemetry() && MutationObserverHasObservedNodeForTelemetry() ? 1 : 0)) .Add(); mPendingStylesToApplyToNewContent = nullptr; if (mDisabledLinkHandling) { if (Document* doc = GetDocument()) { doc->SetLinkHandlingEnabled(mOldLinkHandlingEnabled); } } RemoveEventListeners(); HideAnonymousEditingUIs(); } NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEditor) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEditor, EditorBase) NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingStylesToApplyToNewContent) NS_IMPL_CYCLE_COLLECTION_UNLINK(mComposerCommandsUpdater) NS_IMPL_CYCLE_COLLECTION_UNLINK(mChangedRangeForTopLevelEditSubAction) NS_IMPL_CYCLE_COLLECTION_UNLINK(mPaddingBRElementForEmptyEditor) NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastCollapsibleWhiteSpaceAppendedTextNode) tmp->HideAnonymousEditingUIs(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEditor, EditorBase) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingStylesToApplyToNewContent) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mComposerCommandsUpdater) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChangedRangeForTopLevelEditSubAction) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPaddingBRElementForEmptyEditor) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastCollapsibleWhiteSpaceAppendedTextNode) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopLeftHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopRightHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLeftHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRightHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomLeftHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBottomRightHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActivatedHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingShadow) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizingInfo) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResizedObject) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbsolutelyPositionedObject) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGrabber) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositioningShadow) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineEditedCell) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnBeforeButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveColumnButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddColumnAfterButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowBeforeButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRemoveRowButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAddRowAfterButton) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_ADDREF_INHERITED(HTMLEditor, EditorBase) NS_IMPL_RELEASE_INHERITED(HTMLEditor, EditorBase) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLEditor) NS_INTERFACE_MAP_ENTRY(nsIHTMLEditor) NS_INTERFACE_MAP_ENTRY(nsIHTMLObjectResizer) NS_INTERFACE_MAP_ENTRY(nsIHTMLAbsPosEditor) NS_INTERFACE_MAP_ENTRY(nsIHTMLInlineTableEditor) NS_INTERFACE_MAP_ENTRY(nsITableEditor) NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport) NS_INTERFACE_MAP_END_INHERITING(EditorBase) nsresult HTMLEditor::Init(Document& aDocument, ComposerCommandsUpdater& aComposerCommandsUpdater, uint32_t aFlags) { MOZ_ASSERT(!mInitSucceeded, "HTMLEditor::Init() called again without calling PreDestroy()?"); MOZ_DIAGNOSTIC_ASSERT(!mComposerCommandsUpdater || mComposerCommandsUpdater == &aComposerCommandsUpdater); mComposerCommandsUpdater = &aComposerCommandsUpdater; RefPtr presShell = aDocument.GetPresShell(); if (NS_WARN_IF(!presShell)) { return NS_ERROR_FAILURE; } nsresult rv = InitInternal(aDocument, nullptr, *presShell, aFlags); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::InitInternal() failed"); return rv; } // Init mutation observer aDocument.AddMutationObserverUnlessExists(this); if (!mRootElement) { UpdateRootElement(); } // disable Composer-only features if (IsMailEditor()) { DebugOnly rvIgnored = SetAbsolutePositioningEnabled(false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "HTMLEditor::SetAbsolutePositioningEnabled(false) failed, but ignored"); rvIgnored = SetSnapToGridEnabled(false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "HTMLEditor::SetSnapToGridEnabled(false) failed, but ignored"); } // disable links Document* document = GetDocument(); if (NS_WARN_IF(!document)) { return NS_ERROR_FAILURE; } if (!IsPlaintextMailComposer() && !IsInteractionAllowed()) { mDisabledLinkHandling = true; mOldLinkHandlingEnabled = document->LinkHandlingEnabled(); document->SetLinkHandlingEnabled(false); } // init the type-in state mPendingStylesToApplyToNewContent = new PendingStyles(); AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_FAILURE; } rv = InitEditorContentAndSelection(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::InitEditorContentAndSelection() failed"); // XXX Sholdn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this // is a public method? return EditorBase::ToGenericNSResult(rv); } // Throw away the old transaction manager if this is not the first time that // we're initializing the editor. ClearUndoRedo(); EnableUndoRedo(); // FYI: Creating mTransactionManager in this call if (mTransactionManager) { mTransactionManager->Attach(*this); } MOZ_ASSERT(!mInitSucceeded, "HTMLEditor::Init() shouldn't be nested"); mInitSucceeded = true; return NS_OK; } nsresult HTMLEditor::PostCreate() { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } nsresult rv = PostCreateInternal(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::PostCreatInternal() failed"); return rv; } void HTMLEditor::PreDestroy() { if (mDidPreDestroy) { return; } mInitSucceeded = false; // FYI: Cannot create AutoEditActionDataSetter here. However, it does not // necessary for the methods called by the following code. RefPtr document = GetDocument(); if (document) { document->RemoveMutationObserver(this); } // Clean up after our anonymous content -- we don't want these nodes to // stay around (which they would, since the frames have an owning reference). PresShell* presShell = GetPresShell(); if (presShell && presShell->IsDestroying()) { // Just destroying PresShell now. // We have to keep UI elements of anonymous content until PresShell // is destroyed. RefPtr self = this; nsContentUtils::AddScriptRunner( NS_NewRunnableFunction("HTMLEditor::PreDestroy", [self]() { self->HideAnonymousEditingUIs(); })); } else { // PresShell is alive or already gone. HideAnonymousEditingUIs(); } mPaddingBRElementForEmptyEditor = nullptr; PreDestroyInternal(); } bool HTMLEditor::IsStyleEditable() const { if (IsInDesignMode()) { return true; } if (IsPlaintextMailComposer()) { return false; } const Element* const editingHost = ComputeEditingHost(LimitInBodyElement::No); // Let's return true if there is no focused editing host for the backward // compatibility. return !editingHost || !editingHost->IsContentEditablePlainTextOnly(); } NS_IMETHODIMP HTMLEditor::GetDocumentCharacterSet(nsACString& aCharacterSet) { nsresult rv = GetDocumentCharsetInternal(aCharacterSet); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::GetDocumentCharsetInternal() failed"); return rv; } NS_IMETHODIMP HTMLEditor::SetDocumentCharacterSet( const nsACString& aCharacterSet) { AutoEditActionDataSetter editActionData(*this, EditAction::eSetCharacterSet); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return EditorBase::ToGenericNSResult(NS_ERROR_NOT_INITIALIZED); } // This method is scriptable, so add-ons could pass in something other // than a canonical name. const Encoding* encoding = Encoding::ForLabelNoReplacement(aCharacterSet); if (!encoding) { NS_WARNING("Encoding::ForLabelNoReplacement() failed"); return EditorBase::ToGenericNSResult(NS_ERROR_INVALID_ARG); } document->SetDocumentCharacterSet(WrapNotNull(encoding)); // Update META charset element. if (UpdateMetaCharsetWithTransaction(*document, aCharacterSet)) { return NS_OK; } // Set attributes to the created element if (aCharacterSet.IsEmpty()) { return NS_OK; } RefPtr headElementList = document->GetElementsByTagName(u"head"_ns); if (NS_WARN_IF(!headElementList)) { return NS_OK; } nsCOMPtr primaryHeadElement = headElementList->Item(0); if (NS_WARN_IF(!primaryHeadElement)) { return NS_OK; } // Create a new meta charset tag Result createNewMetaElementResult = CreateAndInsertElement( WithTransaction::Yes, *nsGkAtoms::meta, EditorDOMPoint(primaryHeadElement, 0), [&aCharacterSet](HTMLEditor&, Element& aMetaElement, const EditorDOMPoint&) { MOZ_ASSERT(!aMetaElement.IsInComposedDoc()); DebugOnly rvIgnored = aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, u"Content-Type"_ns, false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "Element::SetAttr(nsGkAtoms::httpEquiv, \"Content-Type\", " "false) failed, but ignored"); rvIgnored = aMetaElement.SetAttr(kNameSpaceID_None, nsGkAtoms::content, u"text/html;charset="_ns + NS_ConvertASCIItoUTF16(aCharacterSet), false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), nsPrintfCString( "Element::SetAttr(nsGkAtoms::content, " "\"text/html;charset=%s\", false) failed, but ignored", nsPromiseFlatCString(aCharacterSet).get()) .get()); return NS_OK; }); NS_WARNING_ASSERTION(createNewMetaElementResult.isOk(), "HTMLEditor::CreateAndInsertElement(WithTransaction::" "Yes, nsGkAtoms::meta) failed, but ignored"); // Probably, we don't need to update selection in this case since we should // not put selection into element. createNewMetaElementResult.inspect().IgnoreCaretPointSuggestion(); return NS_OK; } bool HTMLEditor::UpdateMetaCharsetWithTransaction( Document& aDocument, const nsACString& aCharacterSet) { // get a list of META tags RefPtr metaElementList = aDocument.GetElementsByTagName(u"meta"_ns); if (NS_WARN_IF(!metaElementList)) { return false; } for (uint32_t i = 0; i < metaElementList->Length(true); ++i) { RefPtr metaElement = metaElementList->Item(i)->AsElement(); MOZ_ASSERT(metaElement); nsAutoString currentValue; metaElement->GetAttr(nsGkAtoms::httpEquiv, currentValue); if (!FindInReadable(u"content-type"_ns, currentValue, nsCaseInsensitiveStringComparator)) { continue; } metaElement->GetAttr(nsGkAtoms::content, currentValue); constexpr auto charsetEquals = u"charset="_ns; nsAString::const_iterator originalStart, start, end; originalStart = currentValue.BeginReading(start); currentValue.EndReading(end); if (!FindInReadable(charsetEquals, start, end, nsCaseInsensitiveStringComparator)) { continue; } // set attribute to charset=text/html nsresult rv = SetAttributeWithTransaction( *metaElement, *nsGkAtoms::content, Substring(originalStart, start) + charsetEquals + NS_ConvertASCIItoUTF16(aCharacterSet)); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction(nsGkAtoms::content) failed"); return NS_SUCCEEDED(rv); } return false; } NS_IMETHODIMP HTMLEditor::NotifySelectionChanged(Document* aDocument, Selection* aSelection, int16_t aReason, int32_t aAmount) { if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } if (mPendingStylesToApplyToNewContent) { RefPtr pendingStyles = mPendingStylesToApplyToNewContent; pendingStyles->OnSelectionChange(*this, aReason); // We used a class which derived from nsISelectionListener to call // HTMLEditor::RefreshEditingUI(). The lifetime of the class was // exactly same as mPendingStylesToApplyToNewContent. So, call it only when // mPendingStylesToApplyToNewContent is not nullptr. if ((aReason & (nsISelectionListener::MOUSEDOWN_REASON | nsISelectionListener::KEYPRESS_REASON | nsISelectionListener::SELECTALL_REASON)) && aSelection) { // the selection changed and we need to check if we have to // hide and/or redisplay resizing handles // FYI: This is an XPCOM method. So, the caller, Selection, guarantees // the lifetime of this instance. So, don't need to grab this with // local variable. DebugOnly rv = RefreshEditingUI(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::RefreshEditingUI() failed, but ignored"); } } if (mComposerCommandsUpdater) { RefPtr updater = mComposerCommandsUpdater; updater->OnSelectionChange(); } nsresult rv = EditorBase::NotifySelectionChanged(aDocument, aSelection, aReason, aAmount); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::NotifySelectionChanged() failed"); return rv; } void HTMLEditor::UpdateRootElement() { // Use the HTML documents body element as the editor root if we didn't // get a root element during initialization. mRootElement = GetBodyElement(); if (!mRootElement) { RefPtr doc = GetDocument(); if (doc) { // If there is no HTML body element, // we should use the document root element instead. mRootElement = doc->GetDocumentElement(); } // else leave it null, for lack of anything better. } } nsresult HTMLEditor::FocusedElementOrDocumentBecomesEditable( Document& aDocument, Element* aElement) { MOZ_LOG(gHTMLEditorFocusLog, LogLevel::Info, ("%s(aDocument=%p, aElement=%s): mHasFocus=%s, mIsInDesignMode=%s, " "aDocument.IsInDesignMode()=%s, aElement->IsInDesignMode()=%s", __FUNCTION__, &aDocument, ToString(RefPtr{aElement}).c_str(), mHasFocus ? "true" : "false", mIsInDesignMode ? "true" : "false", aDocument.IsInDesignMode() ? "true" : "false", aElement ? (aElement->IsInDesignMode() ? "true" : "false") : "N/A")); const bool enteringInDesignMode = (aDocument.IsInDesignMode() && (!aElement || aElement->IsInDesignMode())); // If we should've already handled focus event, selection limiter should not // be set. However, IMEStateManager is not notified the pseudo focus change // in this case. Therefore, we need to notify IMEStateManager of this. if (mHasFocus) { if (enteringInDesignMode) { mIsInDesignMode = true; return NS_OK; } // Although editor is already initialized due to re-used, ISM may not // create IME content observer yet. So we have to create it. IMEState newState; nsresult rv = GetPreferredIMEState(&newState); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::GetPreferredIMEState() failed"); mIsInDesignMode = false; return NS_OK; } const RefPtr focusedElement = GetFocusedElement(); if (focusedElement) { MOZ_ASSERT(focusedElement == aElement); TextControlElement* const textControlElement = TextControlElement::FromNode(focusedElement); if (textControlElement && textControlElement->IsSingleLineTextControlOrTextArea()) { // Let's emulate blur first. DebugOnly rv = FinalizeSelection(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::FinalizeSelection() failed, but ignored"); mHasFocus = false; mIsInDesignMode = false; } IMEStateManager::UpdateIMEState(newState, focusedElement, *this); // XXX Do we need to notify focused TextEditor of focus? In theory, // the TextEditor should get focus event in this case. } mIsInDesignMode = false; return NS_OK; } // If we should be in the design mode, we want to handle focus event fired // on the document node. Therefore, we should emulate it here. if (enteringInDesignMode) { MOZ_ASSERT(&aDocument == GetDocument()); nsresult rv = OnFocus(aDocument); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnFocus() failed"); return rv; } if (NS_WARN_IF(!aElement)) { return NS_ERROR_INVALID_ARG; } // Otherwise, we should've already handled focus event on the element, // therefore, we need to emulate it here. MOZ_ASSERT(nsFocusManager::GetFocusedElementStatic() == aElement); nsresult rv = OnFocus(*aElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OnFocus() failed"); // Note that we don't need to call // IMEStateManager::MaybeOnEditableStateDisabled here because // EditorBase::OnFocus must have already been called IMEStateManager::OnFocus // if succeeded. And perhaps, it's okay that IME is not enabled when // HTMLEditor fails to start handling since nobody can handle composition // events anyway... return rv; } nsresult HTMLEditor::OnFocus(const nsINode& aOriginalEventTargetNode) { MOZ_LOG(gHTMLEditorFocusLog, LogLevel::Info, ("%s(aOriginalEventTarget=%s): mIsInDesignMode=%s, " "aOriginalEventTargetNode.IsInDesignMode()=%s", __FUNCTION__, ToString(RefPtr{&aOriginalEventTargetNode}).c_str(), mIsInDesignMode ? "true" : "false", aOriginalEventTargetNode.IsInDesignMode() ? "true" : "false")); // Before doing anything, we should check whether the original target is still // valid focus event target because it may have already lost focus. if (!CanKeepHandlingFocusEvent(aOriginalEventTargetNode)) { return NS_OK; } AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_FAILURE; } nsresult rv = EditorBase::OnFocus(aOriginalEventTargetNode); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::OnFocus() failed"); return rv; } mHasFocus = true; mIsInDesignMode = aOriginalEventTargetNode.IsInDesignMode(); return NS_OK; } nsresult HTMLEditor::FocusedElementOrDocumentBecomesNotEditable( HTMLEditor* aHTMLEditor, Document& aDocument, Element* aElement) { MOZ_LOG( gHTMLEditorFocusLog, LogLevel::Info, ("%s(aHTMLEditor=%p, aDocument=%p, aElement=%s): " "aHTMLEditor->HasFocus()=%s, aHTMLEditor->IsInDesignMode()=%s, " "aDocument.IsInDesignMode()=%s, aElement->IsInDesignMode()=%s, " "nsFocusManager::GetFocusedElementStatic()=%s", __FUNCTION__, aHTMLEditor, &aDocument, ToString(RefPtr{aElement}).c_str(), aHTMLEditor ? (aHTMLEditor->HasFocus() ? "true" : "false") : "N/A", aHTMLEditor ? (aHTMLEditor->IsInDesignMode() ? "true" : "false") : "N/A", aDocument.IsInDesignMode() ? "true" : "false", aElement ? (aElement->IsInDesignMode() ? "true" : "false") : "N/A", ToString(RefPtr{nsFocusManager::GetFocusedElementStatic()}).c_str())); nsresult rv = [&]() MOZ_CAN_RUN_SCRIPT { // If HTMLEditor has not been created yet, we just need to adjust // IMEStateManager. So, don't return error. if (!aHTMLEditor || !aHTMLEditor->HasFocus()) { return NS_OK; } // Let's emulate blur first. nsresult rv = aHTMLEditor->FinalizeSelection(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::FinalizeSelection() failed"); aHTMLEditor->mHasFocus = false; aHTMLEditor->mIsInDesignMode = false; RefPtr focusedElement = nsFocusManager::GetFocusedElementStatic(); if (focusedElement && !focusedElement->IsInComposedDoc()) { // nsFocusManager may keep storing the focused element even after // disconnected from the tree, but HTMLEditor cannot work with editable // nodes not in a composed document. Therefore, we should treat no // focused element in the case. focusedElement = nullptr; } TextControlElement* const focusedTextControlElement = TextControlElement::FromNodeOrNull(focusedElement); if ((focusedElement && focusedElement->IsEditable() && focusedElement->OwnerDoc() == aHTMLEditor->GetDocument() && (!focusedTextControlElement || !focusedTextControlElement->IsSingleLineTextControlOrTextArea())) || (!focusedElement && aDocument.IsInDesignMode())) { // Then, the focused element is still editable, let's emulate focus to // make the editor be ready to handle input. DebugOnly rvIgnored = aHTMLEditor->OnFocus( focusedElement ? static_cast(*focusedElement) : static_cast(aDocument)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "HTMLEditor::OnFocus() failed, but ignored"); } else if (focusedTextControlElement && focusedTextControlElement->IsSingleLineTextControlOrTextArea()) { if (const RefPtr textEditor = focusedTextControlElement->GetExtantTextEditor()) { textEditor->OnFocus(*focusedElement); } } return rv; }(); // If the element becomes not editable without focus change, IMEStateManager // does not have a chance to disable IME. Therefore, (even if we fail to // handle the emulated blur/focus above,) we should notify IMEStateManager of // the editing state change. Note that if the window of the HTMLEditor has // already lost focus, we don't need to do that and we should not touch the // other windows. if (const RefPtr presContext = aDocument.GetPresContext()) { const RefPtr focusedElementInDocument = Element::FromNodeOrNull(aDocument.GetUnretargetedFocusedContent()); MOZ_ASSERT_IF(focusedElementInDocument, focusedElementInDocument->GetPresContext( Element::PresContextFor::eForComposedDoc)); IMEStateManager::MaybeOnEditableStateDisabled(*presContext, focusedElementInDocument); } return rv; } nsresult HTMLEditor::OnBlur(const EventTarget* aEventTarget) { MOZ_LOG(gHTMLEditorFocusLog, LogLevel::Info, ("%s(aEventTarget=%s): mHasFocus=%s, mIsInDesignMode=%s, " "aEventTarget->IsInDesignMode()=%s", __FUNCTION__, ToString(RefPtr{aEventTarget}).c_str(), mHasFocus ? "true" : "false", mIsInDesignMode ? "true" : "false", nsINode::FromEventTargetOrNull(aEventTarget) ? (nsINode::FromEventTarget(aEventTarget)->IsInDesignMode() ? "true" : "false") : "N/A")); const Element* eventTargetAsElement = Element::FromEventTargetOrNull(aEventTarget); // If another element already has focus, we should not maintain the selection // because we may not have the rights doing it. const Element* focusedElement = nsFocusManager::GetFocusedElementStatic(); if (focusedElement && focusedElement != eventTargetAsElement) { // XXX If we had focus and new focused element is a text control, we may // need to notify focus of its TextEditor... mIsInDesignMode = false; mHasFocus = false; return NS_OK; } // If we're in the designMode and blur occurs, the target must be the document // node. If a blur event is fired and the target is an element, it must be // delayed blur event at initializing the `HTMLEditor`. if (mIsInDesignMode && eventTargetAsElement && eventTargetAsElement->IsInComposedDoc()) { return NS_OK; } nsresult rv = FinalizeSelection(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::FinalizeSelection() failed"); mIsInDesignMode = false; mHasFocus = false; return rv; } Element* HTMLEditor::FindSelectionRoot(const nsINode& aNode) const { MOZ_ASSERT(aNode.IsDocument() || aNode.IsContent(), "aNode must be content or document node"); if (NS_WARN_IF(!aNode.IsInComposedDoc())) { return nullptr; } if (aNode.IsInDesignMode()) { return GetDocument()->GetRootElement(); } nsIContent* content = const_cast(aNode.AsContent()); if (!content->HasFlag(NODE_IS_EDITABLE)) { // If the content is in read-write state but is not editable itself, // return it as the selection root. if (content->IsElement() && content->AsElement()->State().HasState(ElementState::READWRITE)) { return content->AsElement(); } return nullptr; } // For non-readonly editors we want to find the root of the editable subtree // containing aContent. return content->GetEditingHost(); } bool HTMLEditor::EntireDocumentIsEditable() const { Document* document = GetDocument(); return document && document->GetDocumentElement() && (document->GetDocumentElement()->IsEditable() || (document->GetBody() && document->GetBody()->IsEditable())); } void HTMLEditor::CreateEventListeners() { // Don't create the handler twice if (!mEventListener) { mEventListener = new HTMLEditorEventListener(); } } nsresult HTMLEditor::InstallEventListeners() { // FIXME InstallEventListeners() should not be called if we failed to set // document or create an event listener. So, these checks should be // MOZ_DIAGNOSTIC_ASSERT instead. MOZ_ASSERT(GetDocument()); if (MOZ_UNLIKELY(!GetDocument()) || NS_WARN_IF(!mEventListener)) { return NS_ERROR_NOT_INITIALIZED; } // NOTE: HTMLEditor doesn't need to initialize mEventTarget here because // the target must be document node and it must be referenced as weak pointer. HTMLEditorEventListener* listener = reinterpret_cast(mEventListener.get()); nsresult rv = listener->Connect(this); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditorEventListener::Connect() failed"); return rv; } void HTMLEditor::Detach( const ComposerCommandsUpdater& aComposerCommandsUpdater) { MOZ_DIAGNOSTIC_ASSERT_IF( mComposerCommandsUpdater, &aComposerCommandsUpdater == mComposerCommandsUpdater); if (mComposerCommandsUpdater == &aComposerCommandsUpdater) { mComposerCommandsUpdater = nullptr; if (mTransactionManager) { mTransactionManager->Detach(*this); } } } NS_IMETHODIMP HTMLEditor::BeginningOfDocument() { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } nsresult rv = MaybeCollapseSelectionAtFirstEditableNode(false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed"); return rv; } NS_IMETHODIMP HTMLEditor::EndOfDocument() { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() failed"); // This is low level API for embedders and chrome script so that we can return // raw error code here. return rv; } nsresult HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() const { MOZ_ASSERT(IsEditActionDataAvailable()); // We should do nothing with the result of GetRoot() if only a part of the // document is editable. if (!EntireDocumentIsEditable()) { return NS_OK; } RefPtr bodyOrDocumentElement = GetRoot(); if (NS_WARN_IF(!bodyOrDocumentElement)) { return NS_ERROR_NULL_POINTER; } auto pointToPutCaret = [&]() -> EditorRawDOMPoint { nsCOMPtr lastLeafContent = HTMLEditUtils::GetLastLeafContent( *bodyOrDocumentElement, {LeafNodeType::OnlyLeafNode}); if (!lastLeafContent) { return EditorRawDOMPoint::AtEndOf(*bodyOrDocumentElement); } // TODO: We should put caret into text node if it's visible. return lastLeafContent->IsText() || HTMLEditUtils::IsContainerNode(*lastLeafContent) ? EditorRawDOMPoint::AtEndOf(*lastLeafContent) : EditorRawDOMPoint(lastLeafContent); }(); nsresult rv = CollapseSelectionTo(pointToPutCaret); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } void HTMLEditor::InitializeSelectionAncestorLimit( Element& aAncestorLimit) const { MOZ_ASSERT(IsEditActionDataAvailable()); // Hack for initializing selection. // HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to // collapse selection at first editable text node or inline element which // cannot have text nodes as its children. However, selection has already // set into the new editing host by user, we should not change it. For // solving this issue, we should do nothing if selection range is in active // editing host except it's not collapsed at start of the editing host since // aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection // at start of the new limiter if focus node of aSelection is outside of the // editing host. However, we need to check here if selection is already // collapsed at start of the editing host because it's possible JS to do it. // In such case, we should not modify selection with calling // MaybeCollapseSelectionAtFirstEditableNode(). // Basically, we should try to collapse selection at first editable node // in HTMLEditor. bool tryToCollapseSelectionAtFirstEditableNode = true; if (SelectionRef().RangeCount() == 1 && SelectionRef().IsCollapsed()) { Element* editingHost = ComputeEditingHost(); const nsRange* range = SelectionRef().GetRangeAt(0); if (range->GetStartContainer() == editingHost && !range->StartOffset()) { // JS or user operation has already collapsed selection at start of // the editing host. So, we don't need to try to change selection // in this case. tryToCollapseSelectionAtFirstEditableNode = false; } } EditorBase::InitializeSelectionAncestorLimit(aAncestorLimit); // XXX Do we need to check if we still need to change selection? E.g., // we could have already lost focus while we're changing the ancestor // limiter because it may causes "selectionchange" event. if (tryToCollapseSelectionAtFirstEditableNode) { DebugOnly rvIgnored = MaybeCollapseSelectionAtFirstEditableNode(true); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(true) failed, " "but ignored"); } // If the target is a text control element, we won't handle user input // for the `TextEditor` in it. However, we need to be open for `execCommand`. // Therefore, we shouldn't set ancestor limit in this case. // Note that we should do this once setting ancestor limiter for backward // compatiblity of select events, etc. (Selection should be collapsed into // the text control element.) if (aAncestorLimit.HasIndependentSelection()) { SelectionRef().SetAncestorLimiter(nullptr); } } nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( bool aIgnoreIfSelectionInEditingHost) const { MOZ_ASSERT(IsEditActionDataAvailable()); RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return NS_OK; } // If selection range is already in the editing host and the range is not // start of the editing host, we shouldn't reset selection. E.g., window // is activated when the editor had focus before inactivated. if (aIgnoreIfSelectionInEditingHost && SelectionRef().RangeCount() == 1) { const nsRange* range = SelectionRef().GetRangeAt(0); if (!range->Collapsed() || range->GetStartContainer() != editingHost.get() || range->StartOffset()) { return NS_OK; } } for (nsIContent* leafContent = HTMLEditUtils::GetFirstLeafContent( *editingHost, {LeafNodeType::LeafNodeOrNonEditableNode, LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, editingHost); leafContent;) { // If we meet a non-editable node first, we should move caret to start // of the container block or editing host. if (!EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) { MOZ_ASSERT(leafContent->GetParent()); MOZ_ASSERT(EditorUtils::IsEditableContent(*leafContent->GetParent(), EditorType::HTML)); if (const Element* editableBlockElementOrInlineEditingHost = HTMLEditUtils::GetAncestorElement( *leafContent, HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost, BlockInlineCheck::UseComputedDisplayStyle)) { nsresult rv = CollapseSelectionTo( EditorDOMPoint(editableBlockElementOrInlineEditingHost, 0)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } NS_WARNING("Found leaf content did not have editable parent, why?"); return NS_ERROR_FAILURE; } // When we meet an empty inline element, we should look for a next sibling. // For example, if current editor is: //

// then, we should put caret at the
element. So, let's check if found // node is an empty inline container element. if (Element* leafElement = Element::FromNode(leafContent)) { if (HTMLEditUtils::IsInlineContent( *leafElement, BlockInlineCheck::UseComputedDisplayStyle) && !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafElement) && HTMLEditUtils::CanNodeContain(*leafElement, *nsGkAtoms::textTagName)) { // Chromium collapses selection to start of the editing host when this // is the last leaf content. So, we don't need special handling here. leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( *leafElement, {LeafNodeType::LeafNodeOrNonEditableNode, LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, editingHost); continue; } } if (Text* text = leafContent->GetAsText()) { // If there is editable and visible text node, move caret at first of // the visible character. const WSScanResult scanResultInTextNode = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( WSRunScanner::Scan::EditableNodes, EditorRawDOMPoint(text, 0), BlockInlineCheck::UseComputedDisplayStyle); if ((scanResultInTextNode.InVisibleOrCollapsibleCharacters() || scanResultInTextNode.ReachedPreformattedLineBreak()) && scanResultInTextNode.TextPtr() == text) { nsresult rv = CollapseSelectionTo( scanResultInTextNode.PointAtReachedContent()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } // If it's an invisible text node, keep scanning next leaf. leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( *leafContent, {LeafNodeType::LeafNodeOrNonEditableNode, LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, editingHost); continue; } // If there is editable
or something void element like , , //
etc, move caret before it. if (!HTMLEditUtils::CanNodeContain(*leafContent, *nsGkAtoms::textTagName) || HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) { MOZ_ASSERT(leafContent->GetParent()); if (EditorUtils::IsEditableContent(*leafContent, EditorType::HTML)) { nsresult rv = CollapseSelectionTo(EditorDOMPoint(leafContent)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } MOZ_ASSERT_UNREACHABLE( "How do we reach editable leaf in non-editable element?"); // But if it's not editable, let's put caret at start of editing host // for now. nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } // If we meet non-empty block element, we need to scan its child too. if (HTMLEditUtils::IsBlockElement( *leafContent, BlockInlineCheck::UseComputedDisplayStyle) && !HTMLEditUtils::IsEmptyNode( *leafContent, {EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatNonEditableContentAsInvisible}) && !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) { leafContent = HTMLEditUtils::GetFirstLeafContent( *leafContent, {LeafNodeType::LeafNodeOrNonEditableNode, LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, editingHost); continue; } // Otherwise, we must meet an empty block element or a data node like // comment node. Let's ignore it. leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( *leafContent, {LeafNodeType::LeafNodeOrNonEditableNode, LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::UseComputedDisplayStyle, editingHost); } // If there is no visible/editable node except another block element in // current editing host, we should move caret to very first of the editing // host. // XXX This may not make sense, but Chromium behaves so. Therefore, the // reason why we do this is just compatibility with Chromium. nsresult rv = CollapseSelectionTo(EditorDOMPoint(editingHost, 0)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } void HTMLEditor::PreHandleMouseDown(const MouseEvent& aMouseDownEvent) { if (mPendingStylesToApplyToNewContent) { // mPendingStylesToApplyToNewContent will be notified of selection change // even if aMouseDownEvent is not an acceptable event for this editor. // Therefore, we need to notify it of this event too. mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseDownEvent); } } void HTMLEditor::PreHandleMouseUp(const MouseEvent& aMouseUpEvent) { if (mPendingStylesToApplyToNewContent) { // mPendingStylesToApplyToNewContent will be notified of selection change // even if aMouseUpEvent is not an acceptable event for this editor. // Therefore, we need to notify it of this event too. mPendingStylesToApplyToNewContent->PreHandleMouseEvent(aMouseUpEvent); } } void HTMLEditor::PreHandleSelectionChangeCommand(Command aCommand) { if (mPendingStylesToApplyToNewContent) { mPendingStylesToApplyToNewContent->PreHandleSelectionChangeCommand( aCommand); } } void HTMLEditor::PostHandleSelectionChangeCommand(Command aCommand) { if (!mPendingStylesToApplyToNewContent) { return; } AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (!editActionData.CanHandle()) { return; } mPendingStylesToApplyToNewContent->PostHandleSelectionChangeCommand(*this, aCommand); } nsresult HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) { // NOTE: When you change this method, you should also change: // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html if (NS_WARN_IF(!aKeyboardEvent)) { return NS_ERROR_UNEXPECTED; } if (IsReadonly()) { HandleKeyPressEventInReadOnlyMode(*aKeyboardEvent); return NS_OK; } MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress, "HandleKeyPressEvent gets non-keypress event"); switch (aKeyboardEvent->mKeyCode) { case NS_VK_META: case NS_VK_WIN: case NS_VK_SHIFT: case NS_VK_CONTROL: case NS_VK_ALT: // FYI: This shouldn't occur since modifier key shouldn't cause eKeyPress // event. aKeyboardEvent->PreventDefault(); return NS_OK; case NS_VK_BACK: case NS_VK_DELETE: { nsresult rv = EditorBase::HandleKeyPressEvent(aKeyboardEvent); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::HandleKeyPressEvent() failed"); return rv; } case NS_VK_TAB: { // Basically, "Tab" key be used only for focus navigation. // FYI: In web apps, this is always true. if (IsTabbable()) { return NS_OK; } // If we're in the plaintext mode, and not tabbable editor, let's // insert a horizontal tabulation. if (IsPlaintextMailComposer()) { if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta()) { return NS_OK; } // else we insert the tab straight through aKeyboardEvent->PreventDefault(); nsresult rv = OnInputText(u"\t"_ns); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText(\\t) failed"); return rv; } // Otherwise, e.g., we're an embedding editor in chrome, we can handle // "Tab" key as an input. if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta()) { return NS_OK; } RefPtr selection = GetSelection(); if (NS_WARN_IF(!selection) || NS_WARN_IF(!selection->RangeCount())) { return NS_ERROR_FAILURE; } nsINode* startContainer = selection->GetRangeAt(0)->GetStartContainer(); MOZ_ASSERT(startContainer); if (!startContainer->IsContent()) { break; } const Element* editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *startContainer->AsContent(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!editableBlockElement) { break; } // If selection is in a table element, we need special handling. if (HTMLEditUtils::IsAnyTableElement(editableBlockElement)) { Result result = HandleTabKeyPressInTable(aKeyboardEvent); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::HandleTabKeyPressInTable() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } if (!result.inspect().Handled()) { return NS_OK; } nsresult rv = ScrollSelectionFocusIntoView(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::ScrollSelectionFocusIntoView() failed"); return EditorBase::ToGenericNSResult(rv); } // If selection is in an list item element, treat it as indent or outdent. if (HTMLEditUtils::IsListItem(editableBlockElement)) { aKeyboardEvent->PreventDefault(); if (!aKeyboardEvent->IsShift()) { nsresult rv = IndentAsAction(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::IndentAsAction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult rv = OutdentAsAction(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::OutdentAsAction() failed"); return EditorBase::ToGenericNSResult(rv); } // If only "Tab" key is pressed in normal context, just treat it as // horizontal tab character input. if (aKeyboardEvent->IsShift()) { return NS_OK; } aKeyboardEvent->PreventDefault(); nsresult rv = OnInputText(u"\t"_ns); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText(\\t) failed"); return EditorBase::ToGenericNSResult(rv); } case NS_VK_RETURN: { if (!aKeyboardEvent->IsInputtingLineBreak()) { return NS_OK; } // Anyway consume the event even if we cannot handle it actually because // we've already checked whether the an editing host has focus. aKeyboardEvent->PreventDefault(); const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return NS_ERROR_UNEXPECTED; } // Shift + Enter should insert a
or a LF instead of splitting current // paragraph. Additionally, if we're in plaintext-only mode, we should // do so because Chrome does so, but execCommand("insertParagraph") keeps // working as contenteditable=true. So, we cannot redirect in // InsertParagraphSeparatorAsAction(). if (aKeyboardEvent->IsShift() || editingHost->IsContentEditablePlainTextOnly()) { // Only inserts a
element. nsresult rv = InsertLineBreakAsAction(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertLineBreakAsAction() failed"); return EditorBase::ToGenericNSResult(rv); } // uses rules to figure out what to insert nsresult rv = InsertParagraphSeparatorAsAction(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertParagraphSeparatorAsAction() failed"); return EditorBase::ToGenericNSResult(rv); } } if (!aKeyboardEvent->IsInputtingText()) { // we don't PreventDefault() here or keybindings like control-x won't work return NS_OK; } aKeyboardEvent->PreventDefault(); // If we dispatch 2 keypress events for a surrogate pair and we set only // first `.key` value to the surrogate pair, the preceding one has it and the // other has empty string. In this case, we should handle only the first one // with the key value. if (!StaticPrefs::dom_event_keypress_dispatch_once_per_surrogate_pair() && !StaticPrefs::dom_event_keypress_key_allow_lone_surrogate() && aKeyboardEvent->mKeyValue.IsEmpty() && IS_SURROGATE(aKeyboardEvent->mCharCode)) { return NS_OK; } nsAutoString str(aKeyboardEvent->mKeyValue); if (str.IsEmpty()) { str.Assign(static_cast(aKeyboardEvent->mCharCode)); } // FYI: DIfferent from TextEditor, we can treat \r (CR) as-is in HTMLEditor. nsresult rv = OnInputText(str); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed"); return rv; } NS_IMETHODIMP HTMLEditor::NodeIsBlock(nsINode* aNode, bool* aIsBlock) { if (NS_WARN_IF(!aNode)) { return NS_ERROR_INVALID_ARG; } if (MOZ_UNLIKELY(!aNode->IsElement())) { *aIsBlock = false; return NS_OK; } // If the node is in composed doc, we'll refer its style. If we don't flush // pending style here, another API call may change the style. Therefore, // let's flush the pending style changes right now. if (aNode->IsInComposedDoc()) { if (RefPtr presShell = GetPresShell()) { presShell->FlushPendingNotifications(FlushType::Style); } } *aIsBlock = HTMLEditUtils::IsBlockElement( *aNode->AsElement(), BlockInlineCheck::UseComputedDisplayOutsideStyle); return NS_OK; } NS_IMETHODIMP HTMLEditor::UpdateBaseURL() { RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return NS_ERROR_FAILURE; } // Look for an HTML tag RefPtr baseElementList = document->GetElementsByTagName(u"base"_ns); // If no base tag, then set baseURL to the document's URL. This is very // important, else relative URLs for links and images are wrong if (!baseElementList || !baseElementList->Item(0)) { document->SetBaseURI(document->GetDocumentURI()); } return NS_OK; } NS_IMETHODIMP HTMLEditor::InsertLineBreak() { // XPCOM method's InsertLineBreak() should insert paragraph separator in // HTMLEditor. AutoEditActionDataSetter editActionData( *this, EditAction::eInsertParagraphSeparator); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost) { return NS_SUCCESS_DOM_NO_OPERATION; } Result result = InsertParagraphSeparatorAsSubAction(*editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } nsresult HTMLEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) { AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak, aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } if (IsSelectionRangeContainerNotContent()) { return NS_SUCCESS_DOM_NO_OPERATION; } rv = InsertLineBreakAsSubAction(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertLineBreakAsSubAction() failed"); // Don't return NS_SUCCESS_DOM_NO_OPERATION for compatibility of `execCommand` // result of Chrome. return NS_FAILED(rv) ? rv : NS_OK; } nsresult HTMLEditor::InsertParagraphSeparatorAsAction( nsIPrincipal* aPrincipal) { AutoEditActionDataSetter editActionData( *this, EditAction::eInsertParagraphSeparator, aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost) { return NS_SUCCESS_DOM_NO_OPERATION; } Result result = InsertParagraphSeparatorAsSubAction(*editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::InsertParagraphSeparatorAsSubAction() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } Result HTMLEditor::HandleTabKeyPressInTable( WidgetKeyboardEvent* aKeyboardEvent) { MOZ_ASSERT(aKeyboardEvent); AutoEditActionDataSetter dummyEditActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!dummyEditActionData.CanHandle())) { // Do nothing if we didn't find a table cell. return EditActionResult::IgnoredResult(); } // Find enclosing table cell from selection (cell may be selected element) const RefPtr cellElement = GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td); if (!cellElement) { NS_WARNING( "HTMLEditor::GetInclusiveAncestorByTagNameAtSelection(*nsGkAtoms::td) " "returned nullptr"); // Do nothing if we didn't find a table cell. return EditActionResult::IgnoredResult(); } // find enclosing table RefPtr table = HTMLEditUtils::GetClosestAncestorTableElement(*cellElement); if (!table) { NS_WARNING("HTMLEditor::GetClosestAncestorTableElement() failed"); return EditActionResult::IgnoredResult(); } // advance to next cell // first create an iterator over the table PostContentIterator postOrderIter; nsresult rv = postOrderIter.Init(table); if (NS_FAILED(rv)) { NS_WARNING("PostContentIterator::Init() failed"); return Err(rv); } // position postOrderIter at block rv = postOrderIter.PositionAt(cellElement); if (NS_FAILED(rv)) { NS_WARNING("PostContentIterator::PositionAt() failed"); return Err(rv); } do { if (aKeyboardEvent->IsShift()) { postOrderIter.Prev(); } else { postOrderIter.Next(); } nsCOMPtr node = postOrderIter.GetCurrentNode(); if (node && HTMLEditUtils::IsTableCell(node) && HTMLEditUtils::GetClosestAncestorTableElement(*node->AsElement()) == table) { aKeyboardEvent->PreventDefault(); CollapseSelectionToDeepestNonTableFirstChild(node); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } return EditActionResult::HandledResult(); } } while (!postOrderIter.IsDone()); if (aKeyboardEvent->IsShift()) { return EditActionResult::IgnoredResult(); } // If we haven't handled it yet, then we must have run off the end of the // table. Insert a new row. // XXX We should investigate whether this behavior is supported by other // browsers later. AutoEditActionDataSetter editActionData(*this, EditAction::eInsertTableRowElement); rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return Err(rv); } rv = InsertTableRowsWithTransaction(*cellElement, 1, InsertPosition::eAfterSelectedCell); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::InsertTableRowsWithTransaction(*cellElement, 1, " "InsertPosition::eAfterSelectedCell) failed"); return Err(rv); } aKeyboardEvent->PreventDefault(); // Put selection in right place. Use table code to get selection and index // to new row... RefPtr tblElement, cell; int32_t row; rv = GetCellContext(getter_AddRefs(tblElement), getter_AddRefs(cell), nullptr, nullptr, &row, nullptr); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::GetCellContext() failed"); return Err(rv); } if (!tblElement) { NS_WARNING("HTMLEditor::GetCellContext() didn't return table element"); return Err(NS_ERROR_FAILURE); } // ...so that we can ask for first cell in that row... cell = GetTableCellElementAt(*tblElement, row, 0); // ...and then set selection there. (Note that normally you should use // CollapseSelectionToDeepestNonTableFirstChild(), but we know cell is an // empty new cell, so this works fine) if (cell) { nsresult rv = CollapseSelectionToStartOf(*cell); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionToStartOf() failed"); return Err(NS_ERROR_EDITOR_DESTROYED); } } if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } return EditActionResult::HandledResult(); } void HTMLEditor::CollapseSelectionToDeepestNonTableFirstChild(nsINode* aNode) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aNode); nsCOMPtr node = aNode; for (nsIContent* child = node->GetFirstChild(); child; child = child->GetFirstChild()) { // Stop if we find a table, don't want to go into nested tables if (HTMLEditUtils::IsTable(child) || !HTMLEditUtils::IsContainerNode(*child)) { break; } node = child; } DebugOnly rvIgnored = CollapseSelectionToStartOf(*node); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::CollapseSelectionToStartOf() failed, but ignored"); } nsresult HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction( const nsAString& aSourceToInsert) { MOZ_ASSERT(IsEditActionDataAvailable()); // don't do any post processing, rules get confused IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eReplaceHeadWithHTMLSource, nsIEditor::eNone, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); CommitComposition(); // Do not use AutoEditSubActionNotifier -- rules code won't let us insert in // . Use the head node as a parent and delete/insert directly. // XXX We're using AutoEditSubActionNotifier above... RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return NS_ERROR_NOT_INITIALIZED; } RefPtr headElementList = document->GetElementsByTagName(u"head"_ns); if (NS_WARN_IF(!headElementList)) { return NS_ERROR_FAILURE; } RefPtr primaryHeadElement = headElementList->Item(0)->AsElement(); if (NS_WARN_IF(!primaryHeadElement)) { return NS_ERROR_FAILURE; } // First, make sure there are no return chars in the source. Bad things // happen if you insert returns (instead of dom newlines, \n) into an editor // document. nsAutoString inputString(aSourceToInsert); // Windows linebreaks: Map CRLF to LF: inputString.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns); // Mac linebreaks: Map any remaining CR to LF: inputString.ReplaceSubstring(u"\r"_ns, u"\n"_ns); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); // Get the first range in the selection, for context: RefPtr range = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!range)) { return NS_ERROR_FAILURE; } ErrorResult error; RefPtr documentFragment = range->CreateContextualFragment(inputString, error); // XXXX BUG 50965: This is not returning the text between ... // Special code is needed in JS to handle title anyway, so it doesn't matter! if (error.Failed()) { NS_WARNING("nsRange::CreateContextualFragment() failed"); return error.StealNSResult(); } if (NS_WARN_IF(!documentFragment)) { NS_WARNING( "nsRange::CreateContextualFragment() didn't create DocumentFragment"); return NS_ERROR_FAILURE; } // First delete all children in head while (nsCOMPtr child = primaryHeadElement->GetFirstChild()) { nsresult rv = DeleteNodeWithTransaction(*child); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } // Now insert the new nodes int32_t offsetOfNewNode = 0; // Loop over the contents of the fragment and move into the document while (nsCOMPtr child = documentFragment->GetFirstChild()) { Result insertChildContentResult = InsertNodeWithTransaction( *child, EditorDOMPoint(primaryHeadElement, offsetOfNewNode++)); if (MOZ_UNLIKELY(insertChildContentResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertChildContentResult.unwrapErr(); } // We probably don't need to adjust selection here, although we've done it // unless AutoTransactionsConserveSelection is created in a caller. insertChildContentResult.inspect().IgnoreCaretPointSuggestion(); } return NS_OK; } NS_IMETHODIMP HTMLEditor::RebuildDocumentFromSource( const nsAString& aSourceString) { CommitComposition(); AutoEditActionDataSetter editActionData(*this, EditAction::eSetHTML); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } RefPtr rootElement = GetRoot(); if (NS_WARN_IF(!rootElement)) { return NS_ERROR_NULL_POINTER; } // Find where the tag starts. nsReadingIterator beginbody; nsReadingIterator endbody; aSourceString.BeginReading(beginbody); aSourceString.EndReading(endbody); bool foundbody = CaseInsensitiveFindInReadable(u" beginhead; nsReadingIterator endhead; aSourceString.BeginReading(beginhead); aSourceString.EndReading(endhead); bool foundhead = CaseInsensitiveFindInReadable(u" beginbody.get()) { foundhead = false; } nsReadingIterator beginclosehead; nsReadingIterator endclosehead; aSourceString.BeginReading(beginclosehead); aSourceString.EndReading(endclosehead); // Find the index after "" bool foundclosehead = CaseInsensitiveFindInReadable( u""_ns, beginclosehead, endclosehead); // a valid close head appears after a found head if (foundhead && beginhead.get() > beginclosehead.get()) { foundclosehead = false; } // a valid close head appears before a found body if (foundbody && beginclosehead.get() > beginbody.get()) { foundclosehead = false; } // Time to change the document AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); nsReadingIterator endtotal; aSourceString.EndReading(endtotal); if (foundhead) { if (foundclosehead) { nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( Substring(beginhead, beginclosehead)); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " "failed"); return rv; } } else if (foundbody) { nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( Substring(beginhead, beginbody)); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " "failed"); return rv; } } else { // XXX Without recourse to some parser/content sink/docshell hackery we // don't really know where the head ends and the body begins so we assume // that there is no body nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( Substring(beginhead, endtotal)); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " "failed"); return rv; } } } else { nsReadingIterator begintotal; aSourceString.BeginReading(begintotal); constexpr auto head = u""_ns; if (foundclosehead) { nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( head + Substring(begintotal, beginclosehead)); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " "failed"); return rv; } } else if (foundbody) { nsresult rv = ReplaceHeadContentsWithSourceWithTransaction( head + Substring(begintotal, beginbody)); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " "failed"); return rv; } } else { // XXX Without recourse to some parser/content sink/docshell hackery we // don't really know where the head ends and the body begins so we assume // that there is no head nsresult rv = ReplaceHeadContentsWithSourceWithTransaction(head); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::ReplaceHeadContentsWithSourceWithTransaction() " "failed"); return rv; } } } rv = SelectAll(); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::SelectAll() failed"); return rv; } if (!foundbody) { constexpr auto body = u""_ns; // XXX Without recourse to some parser/content sink/docshell hackery we // don't really know where the head ends and the body begins if (foundclosehead) { // assume body starts after the head ends nsresult rv = LoadHTML(body + Substring(endclosehead, endtotal)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::LoadHTML() failed"); return rv; } } else if (foundhead) { // assume there is no body nsresult rv = LoadHTML(body); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::LoadHTML() failed"); return rv; } } else { // assume there is no head, the entire source is body nsresult rv = LoadHTML(body + aSourceString); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::LoadHTML() failed"); return rv; } } RefPtr divElement = CreateElementWithDefaults(*nsGkAtoms::div); if (!divElement) { NS_WARNING( "HTMLEditor::CreateElementWithDefaults(nsGkAtoms::div) failed"); return NS_ERROR_FAILURE; } CloneAttributesWithTransaction(*rootElement, *divElement); nsresult rv = MaybeCollapseSelectionAtFirstEditableNode(false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed"); return rv; } rv = LoadHTML(Substring(beginbody, endtotal)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::LoadHTML() failed"); return rv; } // Now we must copy attributes user might have edited on the tag // because InsertHTML (actually, CreateContextualFragment()) will never // return a body node in the DOM fragment // We already know where " beginclosebody = beginbody; nsReadingIterator endclosebody; aSourceString.EndReading(endclosebody); if (!FindInReadable(u">"_ns, beginclosebody, endclosebody)) { NS_WARNING("'>' was not found"); return NS_ERROR_FAILURE; } // Truncate at the end of the body tag. Kludge of the year: fool the parser // by replacing "body" with "div" so we get a node nsAutoString bodyTag; bodyTag.AssignLiteral("
range = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!range)) { return NS_ERROR_FAILURE; } ErrorResult error; RefPtr documentFragment = range->CreateContextualFragment(bodyTag, error); if (error.Failed()) { NS_WARNING("nsRange::CreateContextualFragment() failed"); return error.StealNSResult(); } if (!documentFragment) { NS_WARNING( "nsRange::CreateContextualFragment() didn't create DocumentFagement"); return NS_ERROR_FAILURE; } nsCOMPtr firstChild = documentFragment->GetFirstChild(); if (!firstChild || !firstChild->IsElement()) { NS_WARNING("First child of DocumentFragment was not an Element node"); return NS_ERROR_FAILURE; } // Copy all attributes from the div child to current body element CloneAttributesWithTransaction(*rootElement, MOZ_KnownLive(*firstChild->AsElement())); // place selection at first editable content rv = MaybeCollapseSelectionAtFirstEditableNode(false); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) failed"); return rv; } NS_IMETHODIMP HTMLEditor::InsertElementAtSelection(Element* aElement, bool aDeleteSelection) { InsertElementOptions options; if (aDeleteSelection) { options += InsertElementOption::DeleteSelection; } nsresult rv = InsertElementAtSelectionAsAction(aElement, options); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertElementAtSelectionAsAction() failed"); return rv; } nsresult HTMLEditor::InsertElementAtSelectionAsAction( Element* aElement, const InsertElementOptions aOptions, nsIPrincipal* aPrincipal) { if (NS_WARN_IF(!aElement)) { return NS_ERROR_INVALID_ARG; } if (IsReadonly()) { return NS_OK; } AutoEditActionDataSetter editActionData( *this, HTMLEditUtils::GetEditActionForInsert(*aElement), aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } DebugOnly rvIgnored = CommitComposition(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::CommitComposition() failed, but ignored"); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } if (result.inspect().Canceled()) { return NS_OK; } } UndefineCaretBidiLevel(); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertElement, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return EditorBase::ToGenericNSResult(NS_ERROR_FAILURE); } rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(*editingHost); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } if (aOptions.contains(InsertElementOption::DeleteSelection) && !SelectionRef().IsCollapsed()) { if (!HTMLEditUtils::IsBlockElement( *aElement, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { // E.g., inserting an image. In this case we don't need to delete any // inline wrappers before we do the insertion. Otherwise we let // DeleteSelectionAndPrepareToCreateNode do the deletion for us, which // calls DeleteSelection with aStripWrappers = eStrip. nsresult rv = DeleteSelectionAsSubAction( eNone, aOptions.contains(InsertElementOption::SplitAncestorInlineElements) ? eStrip : eNoStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed"); return EditorBase::ToGenericNSResult(rv); } } nsresult rv = DeleteSelectionAndPrepareToCreateNode(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteSelectionAndPrepareToCreateNode() failed"); return rv; } } // If deleting, selection will be collapsed. // so if not, we collapse it else { // Named Anchor is a special case, // We collapse to insert element BEFORE the selection // For all other tags, we insert AFTER the selection if (HTMLEditUtils::IsNamedAnchor(aElement)) { IgnoredErrorResult ignoredError; SelectionRef().CollapseToStart(ignoredError); if (NS_WARN_IF(Destroyed())) { return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(!ignoredError.Failed(), "Selection::CollapseToStart() failed, but ignored"); } else { IgnoredErrorResult ignoredError; SelectionRef().CollapseToEnd(ignoredError); if (NS_WARN_IF(Destroyed())) { return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(!ignoredError.Failed(), "Selection::CollapseToEnd() failed, but ignored"); } } if (!SelectionRef().GetAnchorNode()) { return NS_OK; } if (NS_WARN_IF(!SelectionRef().GetAnchorNode()->IsInclusiveDescendantOf( editingHost))) { return NS_ERROR_FAILURE; } EditorRawDOMPoint atAnchor(SelectionRef().AnchorRef()); // Adjust position based on the node we are going to insert. EditorDOMPoint pointToInsert = HTMLEditUtils::GetBetterInsertionPointFor(*aElement, atAnchor); if (!pointToInsert.IsSet()) { NS_WARNING("HTMLEditUtils::GetBetterInsertionPointFor() failed"); return NS_ERROR_FAILURE; } if (StaticPrefs::editor_white_space_normalization_blink_compatible()) { Result pointToInsertOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( *this, pointToInsert, {WhiteSpaceVisibilityKeeper::NormalizeOption:: StopIfFollowingWhiteSpacesStartsWithNBSP}); if (MOZ_UNLIKELY(pointToInsertOrError.isErr())) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt() failed"); return pointToInsertOrError.propagateErr(); } pointToInsert = pointToInsertOrError.unwrap(); if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } if (aOptions.contains(InsertElementOption::SplitAncestorInlineElements)) { if (const RefPtr topmostInlineElement = Element::FromNodeOrNull( HTMLEditUtils::GetMostDistantAncestorInlineElement( *pointToInsert.ContainerAs(), BlockInlineCheck::UseComputedDisplayOutsideStyle, editingHost))) { Result splitInlinesResult = SplitNodeDeepWithTransaction( *topmostInlineElement, pointToInsert, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitInlinesResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitInlinesResult.unwrapErr(); } splitInlinesResult.inspect().IgnoreCaretPointSuggestion(); auto splitPoint = splitInlinesResult.inspect().AtSplitPoint(); if (MOZ_LIKELY(splitPoint.IsSet())) { pointToInsert = std::move(splitPoint); } } } { Result insertElementResult = InsertNodeIntoProperAncestorWithTransaction( *aElement, pointToInsert, SplitAtEdges::eAllowToCreateEmptyContainer); if (MOZ_UNLIKELY(insertElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertNodeIntoProperAncestorWithTransaction(" "SplitAtEdges::eAllowToCreateEmptyContainer) failed"); return EditorBase::ToGenericNSResult(insertElementResult.unwrapErr()); } if (MOZ_LIKELY(aElement->IsInComposedDoc())) { const auto afterElement = EditorDOMPoint::After(*aElement); if (MOZ_LIKELY(afterElement.IsInContentNode())) { nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(afterElement); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed"); return EditorBase::ToGenericNSResult(rv); } } } insertElementResult.inspect().IgnoreCaretPointSuggestion(); } // Set caret after element, but check for special case // of inserting table-related elements: set in first cell instead if (!SetCaretInTableCell(aElement)) { if (NS_WARN_IF(Destroyed())) { return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED); } nsresult rv = CollapseSelectionTo(EditorRawDOMPoint::After(*aElement)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CollapseSelectionTo() failed"); return EditorBase::ToGenericNSResult(rv); } } // check for inserting a whole table at the end of a block. If so insert // a br after it. if (!HTMLEditUtils::IsTable(aElement) || !HTMLEditUtils::IsLastChild(*aElement, {WalkTreeOption::IgnoreNonEditableNode})) { return NS_OK; } Result insertBRElementResultOrError = InsertLineBreak(WithTransaction::Yes, LineBreakType::BRElement, EditorDOMPoint::After(*aElement), // Will collapse selection to before the new line break. ePrevious); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "LineBreakType::BRElement, ePrevious) failed"); return EditorBase::ToGenericNSResult( insertBRElementResultOrError.unwrapErr()); } CreateLineBreakResult insertBRElementResult = insertBRElementResultOrError.unwrap(); MOZ_ASSERT(insertBRElementResult.Handled()); rv = insertBRElementResult.SuggestCaretPointTo(*this, {}); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "CaretPoint::SuggestCaretPointTo() failed"); return EditorBase::ToGenericNSResult(rv); } template Result, nsresult> HTMLEditor::InsertNodeIntoProperAncestorWithTransaction( NodeType& aContentToInsert, const EditorDOMPoint& aPointToInsert, SplitAtEdges aSplitAtEdges) { MOZ_ASSERT(aPointToInsert.IsSetAndValidInComposedDoc()); if (NS_WARN_IF(!aPointToInsert.IsSet())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(aPointToInsert.IsSetAndValid()); if (aContentToInsert.NodeType() == nsINode::DOCUMENT_TYPE_NODE || aContentToInsert.NodeType() == nsINode::PROCESSING_INSTRUCTION_NODE) { return CreateNodeResultBase::NotHandled(); } // Search up the parent chain to find a suitable container. EditorDOMPoint pointToInsert(aPointToInsert); MOZ_ASSERT(pointToInsert.IsSet()); while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), aContentToInsert)) { // If the current parent is a root (body or table element) // then go no further - we can't insert. if (MOZ_UNLIKELY( pointToInsert.IsContainerHTMLElement(nsGkAtoms::body) || HTMLEditUtils::IsAnyTableElement(pointToInsert.GetContainer()))) { NS_WARNING( "There was no proper container element to insert the content node in " "the document"); return Err(NS_ERROR_FAILURE); } // Get the next point. pointToInsert = pointToInsert.ParentPoint(); if (MOZ_UNLIKELY( !pointToInsert.IsInContentNode() || !EditorUtils::IsEditableContent( *pointToInsert.ContainerAs(), EditorType::HTML))) { NS_WARNING( "There was no proper container element to insert the content node in " "the editing host"); return Err(NS_ERROR_FAILURE); } } if (pointToInsert != aPointToInsert) { // We need to split some levels above the original selection parent. MOZ_ASSERT(pointToInsert.GetChild()); Result splitNodeResult = SplitNodeDeepWithTransaction(MOZ_KnownLive(*pointToInsert.GetChild()), aPointToInsert, aSplitAtEdges); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitNodeResult.propagateErr(); } pointToInsert = splitNodeResult.inspect().template AtSplitPoint(); MOZ_ASSERT(pointToInsert.IsSetAndValidInComposedDoc()); // Caret should be set by the caller of this method so that we don't // need to handle it here. splitNodeResult.inspect().IgnoreCaretPointSuggestion(); } // Now we can insert the new node. Result, nsresult> insertContentNodeResult = InsertNodeWithTransaction(aContentToInsert, pointToInsert); if (MOZ_LIKELY(insertContentNodeResult.isOk()) && MOZ_UNLIKELY(NS_WARN_IF(!aContentToInsert.GetParentNode()) || NS_WARN_IF(aContentToInsert.GetParentNode() != pointToInsert.GetContainer()))) { NS_WARNING( "EditorBase::InsertNodeWithTransaction() succeeded, but the inserted " "node was moved or removed by the web app"); insertContentNodeResult.inspect().IgnoreCaretPointSuggestion(); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } NS_WARNING_ASSERTION(insertContentNodeResult.isOk(), "EditorBase::InsertNodeWithTransaction() failed"); return insertContentNodeResult; } NS_IMETHODIMP HTMLEditor::SelectElement(Element* aElement) { if (NS_WARN_IF(!aElement)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } nsresult rv = SelectContentInternal(MOZ_KnownLive(*aElement)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::SelectContentInternal() failed"); return rv; } nsresult HTMLEditor::SelectContentInternal(nsIContent& aContentToSelect) { MOZ_ASSERT(IsEditActionDataAvailable()); // Must be sure that element is contained in the editing host const RefPtr editingHost = ComputeEditingHost(); if (NS_WARN_IF(!editingHost) || NS_WARN_IF(!aContentToSelect.IsInclusiveDescendantOf(editingHost))) { return NS_ERROR_FAILURE; } EditorRawDOMPoint newSelectionStart(&aContentToSelect); if (NS_WARN_IF(!newSelectionStart.IsSet())) { return NS_ERROR_FAILURE; } EditorRawDOMPoint newSelectionEnd(EditorRawDOMPoint::After(aContentToSelect)); MOZ_ASSERT(newSelectionEnd.IsSet()); ErrorResult error; SelectionRef().SetStartAndEndInLimiter(newSelectionStart, newSelectionEnd, error); NS_WARNING_ASSERTION(!error.Failed(), "Selection::SetStartAndEndInLimiter() failed"); return error.StealNSResult(); } nsresult HTMLEditor::AppendContentToSelectionAsRange(nsIContent& aContent) { MOZ_ASSERT(IsEditActionDataAvailable()); EditorRawDOMPoint atContent(&aContent); if (NS_WARN_IF(!atContent.IsSet())) { return NS_ERROR_FAILURE; } RefPtr range = nsRange::Create( atContent.ToRawRangeBoundary(), atContent.NextPoint().ToRawRangeBoundary(), IgnoreErrors()); if (NS_WARN_IF(!range)) { NS_WARNING("nsRange::Create() failed"); return NS_ERROR_FAILURE; } ErrorResult error; SelectionRef().AddRangeAndSelectFramesAndNotifyListeners(*range, error); if (NS_WARN_IF(Destroyed())) { if (error.Failed()) { error.SuppressException(); } return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(!error.Failed(), "Failed to add range to Selection"); return error.StealNSResult(); } nsresult HTMLEditor::ClearSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); ErrorResult error; SelectionRef().RemoveAllRanges(error); if (NS_WARN_IF(Destroyed())) { if (error.Failed()) { error.SuppressException(); } return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(!error.Failed(), "Selection::RemoveAllRanges() failed"); return error.StealNSResult(); } nsresult HTMLEditor::FormatBlockAsAction(const nsAString& aParagraphFormat, nsIPrincipal* aPrincipal) { if (NS_WARN_IF(aParagraphFormat.IsEmpty())) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData( *this, EditAction::eInsertBlockElement, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost || editingHost->IsContentEditablePlainTextOnly()) { return NS_SUCCESS_DOM_NO_OPERATION; } nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } RefPtr tagName = NS_Atomize(aParagraphFormat); MOZ_ASSERT(tagName); if (NS_WARN_IF(!tagName->IsStatic()) || NS_WARN_IF(!HTMLEditUtils::IsFormatTagForFormatBlockCommand( *tagName->AsStatic()))) { return NS_ERROR_INVALID_ARG; } if (tagName == nsGkAtoms::dd || tagName == nsGkAtoms::dt) { // MOZ_KnownLive(tagName->AsStatic()) because nsStaticAtom instances live // while the process is running. Result result = MakeOrChangeListAndListItemAsSubAction( MOZ_KnownLive(*tagName->AsStatic()), EmptyString(), SelectAllOfCurrentList::No, *editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING( "HTMLEditor::MakeOrChangeListAndListItemAsSubAction(" "SelectAllOfCurrentList::No) failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } rv = FormatBlockContainerAsSubAction(MOZ_KnownLive(*tagName->AsStatic()), FormatBlockMode::HTMLFormatBlockCommand, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::FormatBlockContainerAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult HTMLEditor::SetParagraphStateAsAction( const nsAString& aParagraphFormat, nsIPrincipal* aPrincipal) { AutoEditActionDataSetter editActionData( *this, EditAction::eInsertBlockElement, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost || editingHost->IsContentEditablePlainTextOnly()) { return NS_SUCCESS_DOM_NO_OPERATION; } nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } // TODO: Computing the editing host here makes the `execCommand` in // docshell/base/crashtests/file_432114-2.xhtml cannot run // `DOMNodeRemoved` event listener with deleting the bogus
element. // So that it should be rewritten with different mutation event listener // since we'd like to stop using it. nsAutoString lowerCaseTagName(aParagraphFormat); ToLowerCase(lowerCaseTagName); RefPtr tagName = NS_Atomize(lowerCaseTagName); MOZ_ASSERT(tagName); if (NS_WARN_IF(!tagName->IsStatic())) { return NS_ERROR_INVALID_ARG; } if (tagName == nsGkAtoms::dd || tagName == nsGkAtoms::dt) { // MOZ_KnownLive(tagName->AsStatic()) because nsStaticAtom instances live // while the process is running. Result result = MakeOrChangeListAndListItemAsSubAction( MOZ_KnownLive(*tagName->AsStatic()), EmptyString(), SelectAllOfCurrentList::No, *editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING( "HTMLEditor::MakeOrChangeListAndListItemAsSubAction(" "SelectAllOfCurrentList::No) failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } rv = FormatBlockContainerAsSubAction( MOZ_KnownLive(*tagName->AsStatic()), FormatBlockMode::XULParagraphStateCommand, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::FormatBlockContainerAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } // static bool HTMLEditor::IsFormatElement(FormatBlockMode aFormatBlockMode, const nsIContent& aContent) { // FYI: Optimize for HTML command because it may run too many times. return MOZ_LIKELY(aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand) ? HTMLEditUtils::IsFormatElementForFormatBlockCommand(aContent) : (HTMLEditUtils::IsFormatElementForParagraphStateCommand( aContent) && // XXX The XUL paragraph state command treats
,
and //
elements but all handlers do not treat them as a format // node. Therefore, we keep the traditional behavior here. !aContent.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dl, nsGkAtoms::dt)); } NS_IMETHODIMP HTMLEditor::GetParagraphState(bool* aMixed, nsAString& aFirstParagraphState) { if (NS_WARN_IF(!aMixed)) { return NS_ERROR_INVALID_ARG; } if (!mInitSucceeded) { return NS_ERROR_NOT_INITIALIZED; } ErrorResult error; ParagraphStateAtSelection paragraphState( *this, FormatBlockMode::XULParagraphStateCommand, error); if (error.Failed()) { NS_WARNING("ParagraphStateAtSelection failed"); return error.StealNSResult(); } *aMixed = paragraphState.IsMixed(); if (NS_WARN_IF(!paragraphState.GetFirstParagraphStateAtSelection())) { // XXX Odd result, but keep this behavior for now... aFirstParagraphState.AssignASCII("x"); } else { paragraphState.GetFirstParagraphStateAtSelection()->ToString( aFirstParagraphState); } return NS_OK; } nsresult HTMLEditor::GetBackgroundColorState(bool* aMixed, nsAString& aOutColor) { if (NS_WARN_IF(!aMixed)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } if (IsCSSEnabled()) { // if we are in CSS mode, we have to check if the containing block defines // a background color nsresult rv = GetCSSBackgroundColorState( aMixed, aOutColor, {RetrievingBackgroundColorOption::OnlyBlockBackgroundColor, RetrievingBackgroundColorOption:: DefaultColorIfNoSpecificBackgroundColor}); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::GetCSSBackgroundColorState() failed"); return EditorBase::ToGenericNSResult(rv); } // in HTML mode, we look only at page's background nsresult rv = GetHTMLBackgroundColorState(aMixed, aOutColor); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::GetCSSBackgroundColorState() failed"); return EditorBase::ToGenericNSResult(rv); } NS_IMETHODIMP HTMLEditor::GetHighlightColorState(bool* aMixed, nsAString& aOutColor) { if (NS_WARN_IF(!aMixed)) { return NS_ERROR_INVALID_ARG; } *aMixed = false; aOutColor.AssignLiteral("transparent"); if (!IsCSSEnabled() && IsMailEditor()) { return NS_OK; } AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } // in CSS mode, text background can be added by the Text Highlight button // we need to query the background of the selection without looking for // the block container of the ranges in the selection RetrievingBackgroundColorOptions options; if (IsMailEditor()) { options += RetrievingBackgroundColorOption::StopAtInclusiveAncestorBlock; } nsresult rv = GetCSSBackgroundColorState(aMixed, aOutColor, options); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::GetCSSBackgroundColorState() failed"); return rv; } nsresult HTMLEditor::GetCSSBackgroundColorState( bool* aMixed, nsAString& aOutColor, RetrievingBackgroundColorOptions aOptions) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aMixed)) { return NS_ERROR_INVALID_ARG; } *aMixed = false; // the default background color is transparent aOutColor.AssignLiteral("transparent"); RefPtr firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } nsCOMPtr startContainer = firstRange->GetStartContainer(); if (NS_WARN_IF(!startContainer) || NS_WARN_IF(!startContainer->IsContent())) { return NS_ERROR_FAILURE; } // is the selection collapsed? nsIContent* contentToExamine; if (SelectionRef().IsCollapsed() || startContainer->IsText()) { if (NS_WARN_IF(!startContainer->IsContent())) { return NS_ERROR_FAILURE; } // we want to look at the startContainer and ancestors contentToExamine = startContainer->AsContent(); } else { // otherwise we want to look at the first editable node after // {startContainer,offset} and its ancestors for divs with alignment on them contentToExamine = firstRange->GetChildAtStartOffset(); // GetNextNode(startContainer, offset, true, address_of(contentToExamine)); } if (NS_WARN_IF(!contentToExamine)) { return NS_ERROR_FAILURE; } if (aOptions.contains( RetrievingBackgroundColorOption::OnlyBlockBackgroundColor)) { // we are querying the block background (and not the text background), let's // climb to the block container. Note that background color of ancestor // of editing host may be what the caller wants to know. Therefore, we // should ignore the editing host boundaries. Element* const closestBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *contentToExamine, HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (NS_WARN_IF(!closestBlockElement)) { return NS_OK; } for (RefPtr blockElement = closestBlockElement; blockElement;) { RefPtr nextBlockElement = HTMLEditUtils::GetAncestorElement( *blockElement, HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); DebugOnly rvIgnored = CSSEditUtils::GetComputedProperty( *blockElement, *nsGkAtoms::backgroundColor, aOutColor); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (MayHaveMutationEventListeners() && NS_WARN_IF(nextBlockElement != HTMLEditUtils::GetAncestorElement( *blockElement, HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle))) { return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetComputedProperty(nsGkAtoms::" "backgroundColor) failed, but ignored"); // look at parent if the queried color is transparent and if the node to // examine is not the root of the document if (!HTMLEditUtils::IsTransparentCSSColor(aOutColor)) { return NS_OK; } if (aOptions.contains( RetrievingBackgroundColorOption::StopAtInclusiveAncestorBlock)) { aOutColor.AssignLiteral("transparent"); return NS_OK; } blockElement = std::move(nextBlockElement); } if (aOptions.contains(RetrievingBackgroundColorOption:: DefaultColorIfNoSpecificBackgroundColor) && HTMLEditUtils::IsTransparentCSSColor(aOutColor)) { CSSEditUtils::GetDefaultBackgroundColor(aOutColor); } return NS_OK; } // no, we are querying the text background for the Text Highlight button if (contentToExamine->IsText()) { // if the node of interest is a text node, let's climb a level contentToExamine = contentToExamine->GetParent(); } // Return default value due to no parent node if (!contentToExamine) { return NS_OK; } for (RefPtr element : contentToExamine->InclusiveAncestorsOfType()) { // is the node to examine a block ? if (aOptions.contains( RetrievingBackgroundColorOption::StopAtInclusiveAncestorBlock) && HTMLEditUtils::IsBlockElement( *element, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { // yes it is a block; in that case, the text background color is // transparent aOutColor.AssignLiteral("transparent"); break; } // no, it's not; let's retrieve the computed style of background-color // for the node to examine nsCOMPtr parentNode = element->GetParentNode(); DebugOnly rvIgnored = CSSEditUtils::GetComputedProperty( *element, *nsGkAtoms::backgroundColor, aOutColor); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_WARN_IF(parentNode != element->GetParentNode())) { return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetComputedProperty(nsGkAtoms::" "backgroundColor) failed, but ignored"); if (!HTMLEditUtils::IsTransparentCSSColor(aOutColor)) { HTMLEditUtils::GetNormalizedCSSColorValue( aOutColor, HTMLEditUtils::ZeroAlphaColor::RGBAValue, aOutColor); return NS_OK; } } if (aOptions.contains(RetrievingBackgroundColorOption:: DefaultColorIfNoSpecificBackgroundColor) && HTMLEditUtils::IsTransparentCSSColor(aOutColor)) { CSSEditUtils::GetDefaultBackgroundColor(aOutColor); } return NS_OK; } nsresult HTMLEditor::GetHTMLBackgroundColorState(bool* aMixed, nsAString& aOutColor) { MOZ_ASSERT(IsEditActionDataAvailable()); // TODO: We don't handle "mixed" correctly! if (NS_WARN_IF(!aMixed)) { return NS_ERROR_INVALID_ARG; } *aMixed = false; aOutColor.Truncate(); Result, nsresult> cellOrRowOrTableElementOrError = GetSelectedOrParentTableElement(); if (cellOrRowOrTableElementOrError.isErr()) { NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() returned error"); return cellOrRowOrTableElementOrError.unwrapErr(); } for (RefPtr element = cellOrRowOrTableElementOrError.unwrap(); element; element = element->GetParentElement()) { // We are in a cell or selected table element->GetAttr(nsGkAtoms::bgcolor, aOutColor); // Done if we have a color explicitly set if (!aOutColor.IsEmpty()) { return NS_OK; } // Once we hit the body, we're done if (element->IsHTMLElement(nsGkAtoms::body)) { return NS_OK; } // No color is set, but we need to report visible color inherited // from nested cells/tables, so search up parent chain so that // let's keep checking the ancestors. } // If no table or cell found, get page body Element* rootElement = GetRoot(); if (NS_WARN_IF(!rootElement)) { return NS_ERROR_FAILURE; } rootElement->GetAttr(nsGkAtoms::bgcolor, aOutColor); return NS_OK; } NS_IMETHODIMP HTMLEditor::GetListState(bool* aMixed, bool* aOL, bool* aUL, bool* aDL) { if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aOL) || NS_WARN_IF(!aUL) || NS_WARN_IF(!aDL)) { return NS_ERROR_INVALID_ARG; } if (!mInitSucceeded) { return NS_ERROR_NOT_INITIALIZED; } ErrorResult error; ListElementSelectionState state(*this, error); if (error.Failed()) { NS_WARNING("ListElementSelectionState failed"); return error.StealNSResult(); } *aMixed = state.IsNotOneTypeListElementSelected(); *aOL = state.IsOLElementSelected(); *aUL = state.IsULElementSelected(); *aDL = state.IsDLElementSelected(); return NS_OK; } NS_IMETHODIMP HTMLEditor::GetListItemState(bool* aMixed, bool* aLI, bool* aDT, bool* aDD) { if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aLI) || NS_WARN_IF(!aDT) || NS_WARN_IF(!aDD)) { return NS_ERROR_INVALID_ARG; } if (!mInitSucceeded) { return NS_ERROR_NOT_INITIALIZED; } ErrorResult error; ListItemElementSelectionState state(*this, error); if (error.Failed()) { NS_WARNING("ListItemElementSelectionState failed"); return error.StealNSResult(); } // XXX Why do we ignore `
  • ` element selected state? *aMixed = state.IsNotOneTypeDefinitionListItemElementSelected(); *aLI = state.IsLIElementSelected(); *aDT = state.IsDTElementSelected(); *aDD = state.IsDDElementSelected(); return NS_OK; } NS_IMETHODIMP HTMLEditor::GetAlignment(bool* aMixed, nsIHTMLEditor::EAlignment* aAlign) { if (NS_WARN_IF(!aMixed) || NS_WARN_IF(!aAlign)) { return NS_ERROR_INVALID_ARG; } if (!mInitSucceeded) { return NS_ERROR_NOT_INITIALIZED; } ErrorResult error; AlignStateAtSelection state(*this, error); if (error.Failed()) { NS_WARNING("AlignStateAtSelection failed"); return error.StealNSResult(); } *aMixed = false; *aAlign = state.AlignmentAtSelectionStart(); return NS_OK; } NS_IMETHODIMP HTMLEditor::MakeOrChangeList(const nsAString& aListType, bool aEntireList, const nsAString& aBulletType) { RefPtr listTagName = NS_Atomize(aListType); if (NS_WARN_IF(!listTagName) || NS_WARN_IF(!listTagName->IsStatic())) { return NS_ERROR_INVALID_ARG; } // MOZ_KnownLive(listTagName->AsStatic()) because nsStaticAtom instances live // while the process is running. nsresult rv = MakeOrChangeListAsAction( MOZ_KnownLive(*listTagName->AsStatic()), aBulletType, aEntireList ? SelectAllOfCurrentList::Yes : SelectAllOfCurrentList::No); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::MakeOrChangeListAsAction() failed"); return rv; } nsresult HTMLEditor::MakeOrChangeListAsAction( const nsStaticAtom& aListElementTagName, const nsAString& aBulletType, SelectAllOfCurrentList aSelectAllOfCurrentList, nsIPrincipal* aPrincipal) { if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } AutoEditActionDataSetter editActionData( *this, HTMLEditUtils::GetEditActionForInsert(aListElementTagName), aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost || editingHost->IsContentEditablePlainTextOnly()) { return NS_SUCCESS_DOM_NO_OPERATION; } nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } Result result = MakeOrChangeListAndListItemAsSubAction(aListElementTagName, aBulletType, aSelectAllOfCurrentList, *editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::MakeOrChangeListAndListItemAsSubAction() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } NS_IMETHODIMP HTMLEditor::RemoveList(const nsAString& aListType) { nsresult rv = RemoveListAsAction(aListType); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::RemoveListAsAction() failed"); return rv; } nsresult HTMLEditor::RemoveListAsAction(const nsAString& aListType, nsIPrincipal* aPrincipal) { if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } // Note that we ignore aListType when we actually remove parent list elements. // However, we need to set InputEvent.inputType to "insertOrderedList" or // "insertedUnorderedList" when this is called for // execCommand("insertorderedlist") or execCommand("insertunorderedlist"). // Otherwise, comm-central UI may call this methods with "dl" or "". // So, it's okay to use mismatched EditAction here if this is called in // comm-central. RefPtr listAtom = NS_Atomize(aListType); if (NS_WARN_IF(!listAtom)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData( *this, HTMLEditUtils::GetEditActionForRemoveList(*listAtom), aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } const RefPtr editingHost = ComputeEditingHost(); if (!editingHost) { return NS_SUCCESS_DOM_NO_OPERATION; } rv = RemoveListAtSelectionAsSubAction(*editingHost); NS_WARNING_ASSERTION(NS_FAILED(rv), "HTMLEditor::RemoveListAtSelectionAsSubAction() failed"); return rv; } nsresult HTMLEditor::FormatBlockContainerAsSubAction( const nsStaticAtom& aTagName, FormatBlockMode aFormatBlockMode, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } MOZ_ASSERT(&aTagName != nsGkAtoms::dd && &aTagName != nsGkAtoms::dt); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eCreateOrRemoveBlock, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } if (IsSelectionRangeContainerNotContent()) { return NS_SUCCESS_DOM_NO_OPERATION; } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(aEditingHost); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } // FormatBlockContainerWithTransaction() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. AutoClonedSelectionRangeArray selectionRanges(SelectionRef()); Result, nsresult> suggestBlockElementToPutCaretOrError = FormatBlockContainerWithTransaction(selectionRanges, aTagName, aFormatBlockMode, aEditingHost); if (suggestBlockElementToPutCaretOrError.isErr()) { NS_WARNING("HTMLEditor::FormatBlockContainerWithTransaction() failed"); return suggestBlockElementToPutCaretOrError.unwrapErr(); } if (selectionRanges.HasSavedRanges()) { selectionRanges.RestoreFromSavedRanges(); } if (selectionRanges.IsCollapsed()) { // FIXME: If we get rid of the legacy mutation events, we should be able to // just insert a line break without empty check. Result insertPaddingBRElementResultOrError = InsertPaddingBRElementIfInEmptyBlock( selectionRanges.GetFirstRangeStartPoint(), eNoStrip); if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertPaddingBRElementIfInEmptyBlock(eNoStrip) failed"); return insertPaddingBRElementResultOrError.unwrapErr(); } EditorDOMPoint pointToPutCaret; insertPaddingBRElementResultOrError.unwrap().MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (pointToPutCaret.IsSet()) { nsresult rv = selectionRanges.Collapse(pointToPutCaret); if (NS_FAILED(rv)) { NS_WARNING("AutoClonedRangeArray::Collapse() failed"); return rv; } } } if (!suggestBlockElementToPutCaretOrError.inspect() || !selectionRanges.IsCollapsed()) { nsresult rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoClonedSelectionRangeArray::ApplyTo() failed, but ignored"); return rv; } const auto firstSelectionStartPoint = selectionRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!firstSelectionStartPoint.IsSetAndValidInComposedDoc())) { return NS_ERROR_FAILURE; } Result pointInBlockElementOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorRawDOMPoint>(*suggestBlockElementToPutCaretOrError.inspect(), firstSelectionStartPoint); NS_WARNING_ASSERTION( pointInBlockElementOrError.isOk(), "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, but " "ignored"); // Note that if the point is unset, it means that firstSelectionStartPoint is // in the block element. if (MOZ_LIKELY(pointInBlockElementOrError.isOk()) && pointInBlockElementOrError.inspect().IsSet()) { nsresult rv = selectionRanges.Collapse(pointInBlockElementOrError.inspect()); if (NS_FAILED(rv)) { NS_WARNING("AutoClonedRangeArray::Collapse() failed"); return rv; } } rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoClonedSelectionRangeArray::ApplyTo() failed, but ignored"); return rv; } nsresult HTMLEditor::IndentAsAction(nsIPrincipal* aPrincipal) { if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } AutoEditActionDataSetter editActionData(*this, EditAction::eIndent, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost || editingHost->IsContentEditablePlainTextOnly()) { return NS_SUCCESS_DOM_NO_OPERATION; } nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } Result result = IndentAsSubAction(*editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::IndentAsSubAction() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } nsresult HTMLEditor::OutdentAsAction(nsIPrincipal* aPrincipal) { if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } AutoEditActionDataSetter editActionData(*this, EditAction::eOutdent, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost || editingHost->IsContentEditablePlainTextOnly()) { return NS_SUCCESS_DOM_NO_OPERATION; } nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } Result result = OutdentAsSubAction(*editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::OutdentAsSubAction() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } // TODO: IMPLEMENT ALIGNMENT! nsresult HTMLEditor::AlignAsAction(const nsAString& aAlignType, nsIPrincipal* aPrincipal) { AutoEditActionDataSetter editActionData( *this, HTMLEditUtils::GetEditActionForAlignment(aAlignType), aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (!editingHost || editingHost->IsContentEditablePlainTextOnly()) { return NS_SUCCESS_DOM_NO_OPERATION; } nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } Result result = AlignAsSubAction(aAlignType, *editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::AlignAsSubAction() failed"); return EditorBase::ToGenericNSResult(result.unwrapErr()); } return NS_OK; } Element* HTMLEditor::GetInclusiveAncestorByTagName(const nsStaticAtom& aTagName, nsIContent& aContent) const { MOZ_ASSERT(&aTagName != nsGkAtoms::_empty); AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return nullptr; } return GetInclusiveAncestorByTagNameInternal(aTagName, aContent); } Element* HTMLEditor::GetInclusiveAncestorByTagNameAtSelection( const nsStaticAtom& aTagName) const { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(&aTagName != nsGkAtoms::_empty); // If no node supplied, get it from anchor node of current selection const EditorRawDOMPoint atAnchor(SelectionRef().AnchorRef()); if (NS_WARN_IF(!atAnchor.IsInContentNode())) { return nullptr; } // Try to get the actual selected node nsIContent* content = nullptr; if (atAnchor.GetContainer()->HasChildNodes() && atAnchor.ContainerAs()) { content = atAnchor.GetChild(); } // Anchor node is probably a text node - just use that if (!content) { content = atAnchor.ContainerAs(); if (NS_WARN_IF(!content)) { return nullptr; } } return GetInclusiveAncestorByTagNameInternal(aTagName, *content); } Element* HTMLEditor::GetInclusiveAncestorByTagNameInternal( const nsStaticAtom& aTagName, const nsIContent& aContent) const { MOZ_ASSERT(&aTagName != nsGkAtoms::_empty); Element* currentElement = aContent.GetAsElementOrParentElement(); if (NS_WARN_IF(!currentElement)) { MOZ_ASSERT(!aContent.GetParentNode()); return nullptr; } bool lookForLink = IsLinkTag(aTagName); bool lookForNamedAnchor = IsNamedAnchorTag(aTagName); for (Element* element : currentElement->InclusiveAncestorsOfType()) { // Stop searching if parent is a body element. Note: Originally used // IsRoot() to/ stop at table cells, but that's too messy when you are // trying to find the parent table. if (element->IsHTMLElement(nsGkAtoms::body)) { return nullptr; } if (lookForLink) { // Test if we have a link (an anchor with href set) if (HTMLEditUtils::IsLink(element)) { return element; } } else if (lookForNamedAnchor) { // Test if we have a named anchor (an anchor with name set) if (HTMLEditUtils::IsNamedAnchor(element)) { return element; } } else if (&aTagName == nsGkAtoms::list) { // Match "ol", "ul", or "dl" for lists if (HTMLEditUtils::IsAnyListElement(element)) { return element; } } else if (&aTagName == nsGkAtoms::td) { // Table cells are another special case: match either "td" or "th" if (HTMLEditUtils::IsTableCell(element)) { return element; } } else if (&aTagName == element->NodeInfo()->NameAtom()) { return element; } } return nullptr; } NS_IMETHODIMP HTMLEditor::GetElementOrParentByTagName(const nsAString& aTagName, nsINode* aNode, Element** aReturn) { if (NS_WARN_IF(aTagName.IsEmpty()) || NS_WARN_IF(!aReturn)) { return NS_ERROR_INVALID_ARG; } nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName); if (NS_WARN_IF(!tagName)) { // We don't need to support custom elements since this is an internal API. return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } if (NS_WARN_IF(tagName == nsGkAtoms::_empty)) { return NS_ERROR_INVALID_ARG; } if (!aNode) { AutoEditActionDataSetter dummyEditAction(*this, EditAction::eNotEditing); if (NS_WARN_IF(!dummyEditAction.CanHandle())) { return NS_ERROR_NOT_AVAILABLE; } RefPtr parentElement = GetInclusiveAncestorByTagNameAtSelection(*tagName); if (!parentElement) { return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } parentElement.forget(aReturn); return NS_OK; } if (!aNode->IsContent() || !aNode->GetAsElementOrParentElement()) { return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } RefPtr parentElement = GetInclusiveAncestorByTagName(*tagName, *aNode->AsContent()); if (!parentElement) { return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } parentElement.forget(aReturn); return NS_OK; } NS_IMETHODIMP HTMLEditor::GetSelectedElement(const nsAString& aTagName, nsISupports** aReturn) { if (NS_WARN_IF(!aReturn)) { return NS_ERROR_INVALID_ARG; } *aReturn = nullptr; AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } ErrorResult error; nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName); if (!aTagName.IsEmpty() && !tagName) { // We don't need to support custom elements becaus of internal API. return NS_OK; } RefPtr selectedNode = GetSelectedElement(tagName, error); NS_WARNING_ASSERTION(!error.Failed(), "HTMLEditor::GetSelectedElement() failed"); selectedNode.forget(aReturn); return error.StealNSResult(); } already_AddRefed HTMLEditor::GetSelectedElement(const nsAtom* aTagName, ErrorResult& aRv) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aRv.Failed()); // If there is no Selection or two or more selection ranges, that means that // not only one element is selected so that return nullptr. if (SelectionRef().RangeCount() != 1) { return nullptr; } bool isLinkTag = aTagName && IsLinkTag(*aTagName); bool isNamedAnchorTag = aTagName && IsNamedAnchorTag(*aTagName); RefPtr firstRange = SelectionRef().GetRangeAt(0); MOZ_ASSERT(firstRange); const RangeBoundary& startRef = firstRange->StartRef(); if (NS_WARN_IF(!startRef.IsSet())) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } const RangeBoundary& endRef = firstRange->EndRef(); if (NS_WARN_IF(!endRef.IsSet())) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } // Optimization for a single selected element if (startRef.GetContainer() == endRef.GetContainer()) { nsIContent* startContent = startRef.GetChildAtOffset(); nsIContent* endContent = endRef.GetChildAtOffset(); if (startContent && endContent && startContent->GetNextSibling() == endContent) { if (!aTagName) { if (!startContent->IsElement()) { // This means only a text node or something is selected. We should // return nullptr in this case since no other elements are selected. return nullptr; } return do_AddRef(startContent->AsElement()); } // Test for appropriate node type requested if (aTagName == startContent->NodeInfo()->NameAtom() || (isLinkTag && HTMLEditUtils::IsLink(startContent)) || (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(startContent))) { MOZ_ASSERT(startContent->IsElement()); return do_AddRef(startContent->AsElement()); } } } if (isLinkTag && startRef.GetContainer()->IsContent() && endRef.GetContainer()->IsContent()) { // Link node must be the same for both ends of selection. Element* parentLinkOfStart = GetInclusiveAncestorByTagNameInternal( *nsGkAtoms::href, *startRef.GetContainer()->AsContent()); if (parentLinkOfStart) { if (SelectionRef().IsCollapsed()) { // We have just a caret in the link. return do_AddRef(parentLinkOfStart); } // Link node must be the same for both ends of selection. Element* parentLinkOfEnd = GetInclusiveAncestorByTagNameInternal( *nsGkAtoms::href, *endRef.GetContainer()->AsContent()); if (parentLinkOfStart == parentLinkOfEnd) { return do_AddRef(parentLinkOfStart); } } } if (SelectionRef().IsCollapsed()) { return nullptr; } PostContentIterator postOrderIter; nsresult rv = postOrderIter.Init(firstRange); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } RefPtr lastElementInRange; for (nsINode* lastNodeInRange = nullptr; !postOrderIter.IsDone(); postOrderIter.Next()) { if (lastElementInRange) { // When any node follows an element node, not only one element is // selected so that return nullptr. return nullptr; } // This loop ignors any non-element nodes before first element node. // Its purpose must be that this method treats this case as selecting // the element: // -

    abc d[ef}

    // because children of an element node is listed up before the element. // However, this case must not be expected by the initial developer: // -

    a[bc def}

    // When we meet non-parent and non-next-sibling node of previous node, // it means that the range across element boundary (open tag in HTML // source). So, in this case, we should not say only the following // element is selected. nsINode* currentNode = postOrderIter.GetCurrentNode(); MOZ_ASSERT(currentNode); if (lastNodeInRange && lastNodeInRange->GetParentNode() != currentNode && lastNodeInRange->GetNextSibling() != currentNode) { return nullptr; } lastNodeInRange = currentNode; lastElementInRange = Element::FromNodeOrNull(lastNodeInRange); if (!lastElementInRange) { continue; } // And also, if it's followed by a
    element, we shouldn't treat the // the element is selected like this case: // -

    [def}

    // Note that we don't need special handling for because double // clicking it selects the element and we use the first path to handle it. // Additionally, we have this case too: // -

    [def}

    // In these cases, the
    element is not listed up by PostContentIterator. // So, we should return nullptr if next sibling is a `
    ` element or // next sibling starts with `
    ` element. if (nsIContent* nextSibling = lastElementInRange->GetNextSibling()) { if (nextSibling->IsHTMLElement(nsGkAtoms::br)) { return nullptr; } nsIContent* firstEditableLeaf = HTMLEditUtils::GetFirstLeafContent( *nextSibling, {LeafNodeType::OnlyLeafNode}); if (firstEditableLeaf && firstEditableLeaf->IsHTMLElement(nsGkAtoms::br)) { return nullptr; } } if (!aTagName) { continue; } if (isLinkTag && HTMLEditUtils::IsLink(lastElementInRange)) { continue; } if (isNamedAnchorTag && HTMLEditUtils::IsNamedAnchor(lastElementInRange)) { continue; } if (aTagName == lastElementInRange->NodeInfo()->NameAtom()) { continue; } // First element in the range does not match what the caller is looking // for. return nullptr; } return lastElementInRange.forget(); } Result HTMLEditor::CreateAndInsertElement( WithTransaction aWithTransaction, const nsAtom& aTagName, const EditorDOMPoint& aPointToInsert, const InitializeInsertingElement& aInitializer) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToInsert.IsSetAndValid()); // XXX We need offset at new node for RangeUpdaterRef(). Therefore, we need // to compute the offset now but this is expensive. So, if it's possible, // we need to redesign RangeUpdaterRef() as avoiding using indices. Unused << aPointToInsert.Offset(); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eCreateNode, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); // TODO: This method should have a callback function which is called // immediately after creating an element but before it's inserted into // the DOM tree. Then, caller can init the new element's attributes // and children **without** transactions (it'll reduce the number of // legacy mutation events). Finally, we can get rid of // CreatElementTransaction since we can use InsertNodeTransaction // instead. auto createNewElementResult = [&]() MOZ_CAN_RUN_SCRIPT -> Result { RefPtr newElement = CreateHTMLContent(&aTagName); if (MOZ_UNLIKELY(!newElement)) { NS_WARNING("EditorBase::CreateHTMLContent() failed"); return Err(NS_ERROR_FAILURE); } nsresult rv = MarkElementDirty(*newElement); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING("EditorBase::MarkElementDirty() caused destroying the editor"); return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::MarkElementDirty() failed, but ignored"); rv = aInitializer(*this, *newElement, aPointToInsert); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "aInitializer failed"); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } RefPtr transaction = InsertNodeTransaction::Create(*this, *newElement, aPointToInsert); rv = aWithTransaction == WithTransaction::Yes ? DoTransactionInternal(transaction) : transaction->DoTransaction(); // FYI: Transaction::DoTransaction never returns NS_ERROR_EDITOR_*. if (MOZ_UNLIKELY(Destroyed())) { NS_WARNING( "InsertNodeTransaction::DoTransaction() caused destroying the " "editor"); return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("InsertNodeTransaction::DoTransaction() failed"); return Err(rv); } // Override the success code if new element was moved by the web apps. if (newElement && newElement->GetParentNode() != aPointToInsert.GetContainer()) { NS_WARNING("The new element was not inserted into the expected node"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return CreateElementResult( std::move(newElement), transaction->SuggestPointToPutCaret()); }(); if (MOZ_UNLIKELY(createNewElementResult.isErr())) { NS_WARNING("EditorBase::DoTransactionInternal() failed"); // XXX Why do we do this even when DoTransaction() returned error? DebugOnly rvIgnored = RangeUpdaterRef().SelAdjCreateNode(aPointToInsert); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "RangeUpdater::SelAdjCreateNode() failed"); return createNewElementResult; } // If we succeeded to create and insert new element, we need to adjust // ranges in RangeUpdaterRef(). It currently requires offset of the new // node. So, let's call it with original offset. Note that if // aPointToInsert stores child node, it may not be at the offset since new // element must be inserted before the old child. Although, mutation // observer can do anything, but currently, we don't check it. DebugOnly rvIgnored = RangeUpdaterRef().SelAdjCreateNode(EditorRawDOMPoint( aPointToInsert.GetContainer(), aPointToInsert.Offset())); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "RangeUpdater::SelAdjCreateNode() failed, but ignored"); if (MOZ_LIKELY(createNewElementResult.inspect().GetNewNode())) { TopLevelEditSubActionDataRef().DidCreateElement( *this, *createNewElementResult.inspect().GetNewNode()); } return createNewElementResult; } nsresult HTMLEditor::CopyAttributes(WithTransaction aWithTransaction, Element& aDestElement, Element& aSrcElement, const AttributeFilter& aFilterFunc) { if (!aSrcElement.GetAttrCount()) { return NS_OK; } struct MOZ_STACK_CLASS AttrCache { int32_t mNamespaceID; OwningNonNull mName; nsString mValue; }; AutoTArray srcAttrs; srcAttrs.SetCapacity(aSrcElement.GetAttrCount()); for (const uint32_t i : IntegerRange(aSrcElement.GetAttrCount())) { const BorrowedAttrInfo attrInfo = aSrcElement.GetAttrInfoAt(i); if (const nsAttrName* attrName = attrInfo.mName) { MOZ_ASSERT(attrName->LocalName()); MOZ_ASSERT(attrInfo.mValue); nsString attrValue; attrInfo.mValue->ToString(attrValue); srcAttrs.AppendElement(AttrCache{attrInfo.mName->NamespaceID(), *attrName->LocalName(), attrValue}); } } if (aWithTransaction == WithTransaction::No) { for (auto& attr : srcAttrs) { if (!aFilterFunc(*this, aSrcElement, aDestElement, attr.mNamespaceID, attr.mName, attr.mValue)) { continue; } DebugOnly rvIgnored = aDestElement.SetAttr( attr.mNamespaceID, attr.mName, attr.mValue, false); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Element::SetAttr() failed, but ignored"); } if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } return NS_OK; } MOZ_ASSERT_UNREACHABLE("Not implemented yet, but you try to use this"); return NS_ERROR_NOT_IMPLEMENTED; } already_AddRefed HTMLEditor::CreateElementWithDefaults( const nsAtom& aTagName) { // NOTE: Despite of public method, this can be called for internal use. // Although this creates an element, but won't change the DOM tree nor // transaction. So, EditAtion::eNotEditing is proper value here. If // this is called for internal when there is already AutoEditActionDataSetter // instance, this would be initialized with its EditAction value. AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return nullptr; } const nsAtom* realTagName = IsLinkTag(aTagName) || IsNamedAnchorTag(aTagName) ? nsGkAtoms::a : &aTagName; // We don't use editor's CreateElement because we don't want to go through // the transaction system // New call to use instead to get proper HTML element, bug 39919 RefPtr newElement = CreateHTMLContent(realTagName); if (!newElement) { return nullptr; } // Mark the new element dirty, so it will be formatted // XXX Don't we need to check the error result of setting _moz_dirty attr? IgnoredErrorResult ignoredError; newElement->SetAttribute(u"_moz_dirty"_ns, u""_ns, ignoredError); NS_WARNING_ASSERTION(!ignoredError.Failed(), "Element::SetAttribute(_moz_dirty) failed, but ignored"); ignoredError.SuppressException(); // Set default values for new elements if (realTagName == nsGkAtoms::table) { newElement->SetAttr(nsGkAtoms::cellpadding, u"2"_ns, ignoredError); if (ignoredError.Failed()) { NS_WARNING("Element::SetAttr(nsGkAtoms::cellpadding, 2) failed"); return nullptr; } ignoredError.SuppressException(); newElement->SetAttr(nsGkAtoms::cellspacing, u"2"_ns, ignoredError); if (ignoredError.Failed()) { NS_WARNING("Element::SetAttr(nsGkAtoms::cellspacing, 2) failed"); return nullptr; } ignoredError.SuppressException(); newElement->SetAttr(nsGkAtoms::border, u"1"_ns, ignoredError); if (ignoredError.Failed()) { NS_WARNING("Element::SetAttr(nsGkAtoms::border, 1) failed"); return nullptr; } } else if (realTagName == nsGkAtoms::td) { nsresult rv = SetAttributeOrEquivalent(newElement, nsGkAtoms::valign, u"top"_ns, true); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::SetAttributeOrEquivalent(nsGkAtoms::valign, top) " "failed"); return nullptr; } } // ADD OTHER TAGS HERE return newElement.forget(); } NS_IMETHODIMP HTMLEditor::CreateElementWithDefaults(const nsAString& aTagName, Element** aReturn) { if (NS_WARN_IF(aTagName.IsEmpty()) || NS_WARN_IF(!aReturn)) { return NS_ERROR_INVALID_ARG; } *aReturn = nullptr; nsStaticAtom* tagName = EditorUtils::GetTagNameAtom(aTagName); if (NS_WARN_IF(!tagName)) { return NS_ERROR_INVALID_ARG; } RefPtr newElement = CreateElementWithDefaults(MOZ_KnownLive(*tagName)); if (!newElement) { NS_WARNING("HTMLEditor::CreateElementWithDefaults() failed"); return NS_ERROR_FAILURE; } newElement.forget(aReturn); return NS_OK; } NS_IMETHODIMP HTMLEditor::InsertLinkAroundSelection(Element* aAnchorElement) { nsresult rv = InsertLinkAroundSelectionAsAction(aAnchorElement); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertLinkAroundSelectionAsAction() failed"); return rv; } nsresult HTMLEditor::InsertLinkAroundSelectionAsAction( Element* aAnchorElement, nsIPrincipal* aPrincipal) { if (NS_WARN_IF(!aAnchorElement)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLinkElement, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } RefPtr const editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return NS_ERROR_FAILURE; } if (IsPlaintextMailComposer() || editingHost->IsContentEditablePlainTextOnly()) { return NS_SUCCESS_DOM_NO_OPERATION; } if (SelectionRef().IsCollapsed()) { NS_WARNING("Selection was collapsed"); return NS_OK; } // Be sure we were given an anchor element RefPtr anchor = HTMLAnchorElement::FromNodeOrNull(aAnchorElement); if (!anchor) { return NS_OK; } nsAutoString rawHref; anchor->GetAttr(nsGkAtoms::href, rawHref); editActionData.SetData(rawHref); nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } // XXX Is this ok? Does this just want to check that we're a link? If so // there are faster ways to do this. { nsAutoCString href; anchor->GetHref(href); if (href.IsEmpty()) { return NS_OK; } } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); // Set all attributes found on the supplied anchor element // TODO: We should stop using this loop for adding attributes to newly created // `
    ` elements. Then, we can avoid to increate the ref- // counter of attribute names since we can use nsStaticAtom if we don't // need to support unknown attributes. AutoTArray stylesToSet; if (const uint32_t attrCount = anchor->GetAttrCount()) { stylesToSet.SetCapacity(attrCount); for (const uint32_t i : IntegerRange(attrCount)) { const BorrowedAttrInfo attrInfo = anchor->GetAttrInfoAt(i); if (const nsAttrName* attrName = attrInfo.mName) { // We don't need to handle _moz_dirty attribute. If it's required, the // handler should add it to the new element. if (attrName->IsAtom() && attrName->Equals(nsGkAtoms::mozdirty)) { continue; } RefPtr attributeName = attrName->LocalName(); MOZ_ASSERT(attrInfo.mValue); nsString attrValue; attrInfo.mValue->ToString(attrValue); stylesToSet.AppendElement(EditorInlineStyleAndValue( *nsGkAtoms::a, std::move(attributeName), std::move(attrValue))); } } } rv = SetInlinePropertiesAsSubAction(stylesToSet, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::SetInlinePropertiesAsSubAction() failed"); return rv; } nsresult HTMLEditor::SetHTMLBackgroundColorWithTransaction( const nsAString& aColor) { MOZ_ASSERT(IsEditActionDataAvailable()); // Find a selected or enclosing table element to set background on bool isCellSelected = false; Result, nsresult> cellOrRowOrTableElementOrError = GetSelectedOrParentTableElement(&isCellSelected); if (cellOrRowOrTableElementOrError.isErr()) { NS_WARNING("HTMLEditor::GetSelectedOrParentTableElement() failed"); return cellOrRowOrTableElementOrError.unwrapErr(); } bool setColor = !aColor.IsEmpty(); RefPtr rootElementOfBackgroundColor = cellOrRowOrTableElementOrError.unwrap(); if (rootElementOfBackgroundColor) { // Needs to set or remove background color of each selected cell elements. // Therefore, just the cell contains selection range, we don't need to // do this. Note that users can select each cell, but with Selection API, // web apps can select and at same time. With , looks // odd, though. if (isCellSelected || rootElementOfBackgroundColor->IsAnyOfHTMLElements( nsGkAtoms::table, nsGkAtoms::tr)) { SelectedTableCellScanner scanner(SelectionRef()); if (scanner.IsInTableCellSelectionMode()) { if (setColor) { for (const OwningNonNull& cellElement : scanner.ElementsRef()) { // `MOZ_KnownLive(cellElement)` is safe because of `scanner` // is stack only class and keeps grabbing it until it's destroyed. nsresult rv = SetAttributeWithTransaction( MOZ_KnownLive(cellElement), *nsGkAtoms::bgcolor, aColor); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::::SetAttributeWithTransaction(nsGkAtoms::" "bgcolor) failed"); return rv; } } return NS_OK; } for (const OwningNonNull& cellElement : scanner.ElementsRef()) { // `MOZ_KnownLive(cellElement)` is safe because of `scanner` // is stack only class and keeps grabbing it until it's destroyed. nsresult rv = RemoveAttributeWithTransaction( MOZ_KnownLive(cellElement), *nsGkAtoms::bgcolor); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::bgcolor)" " failed"); return rv; } } return NS_OK; } } // If we failed to find a cell, fall through to use originally-found element } else { // No table element -- set the background color on the body tag rootElementOfBackgroundColor = GetRoot(); if (NS_WARN_IF(!rootElementOfBackgroundColor)) { return NS_ERROR_FAILURE; } } // Use the editor method that goes through the transaction system if (setColor) { nsresult rv = SetAttributeWithTransaction(*rootElementOfBackgroundColor, *nsGkAtoms::bgcolor, aColor); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction(nsGkAtoms::bgcolor) failed"); return rv; } nsresult rv = RemoveAttributeWithTransaction(*rootElementOfBackgroundColor, *nsGkAtoms::bgcolor); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::bgcolor) failed"); return rv; } Result HTMLEditor::DeleteEmptyInclusiveAncestorInlineElements( nsIContent& aContent, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(HTMLEditUtils::IsRemovableFromParentNode(aContent)); constexpr static HTMLEditUtils::EmptyCheckOptions kOptionsToCheckInline = HTMLEditUtils::EmptyCheckOptions{ EmptyCheckOption::TreatBlockAsVisible, EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatTableCellAsVisible}; if (&aContent == &aEditingHost || HTMLEditUtils::IsBlockElement( aContent, BlockInlineCheck::UseComputedDisplayOutsideStyle) || !HTMLEditUtils::IsRemovableFromParentNode(aContent) || !aContent.GetParent() || !HTMLEditUtils::IsEmptyNode(aContent, kOptionsToCheckInline)) { return CaretPoint(EditorDOMPoint()); } OwningNonNull content = aContent; for (nsIContent* parentContent : aContent.AncestorsOfType()) { if (HTMLEditUtils::IsBlockElement( *parentContent, BlockInlineCheck::UseComputedDisplayStyle) || !HTMLEditUtils::IsRemovableFromParentNode(*parentContent) || parentContent == &aEditingHost) { break; } bool parentIsEmpty = true; if (parentContent->GetChildCount() > 1) { for (nsIContent* sibling = parentContent->GetFirstChild(); sibling; sibling = sibling->GetNextSibling()) { if (sibling == content) { continue; } if (!HTMLEditUtils::IsEmptyNode(*sibling, kOptionsToCheckInline)) { parentIsEmpty = false; break; } } } if (!parentIsEmpty) { break; } content = *parentContent; } const nsCOMPtr nextSibling = content->GetNextSibling(); const nsCOMPtr parentNode = content->GetParentNode(); MOZ_ASSERT(parentNode); nsresult rv = DeleteNodeWithTransaction(content); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } if (NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode) || NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*parentNode))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Note that even if nextSibling is not editable, we can put caret before it // unless parentNode is not editable. return CaretPoint(nextSibling ? EditorDOMPoint(nextSibling) : EditorDOMPoint::AtEndOf(*parentNode)); } nsresult HTMLEditor::DeleteAllChildrenWithTransaction(Element& aElement) { MOZ_ASSERT(IsEditActionDataAvailable()); // Prevent rules testing until we're done IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eDeleteNode, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "OnStartToHandleTopLevelEditSubAction() failed, but ignored"); while (nsCOMPtr child = aElement.GetLastChild()) { nsresult rv = DeleteNodeWithTransaction(*child); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } return NS_OK; } NS_IMETHODIMP HTMLEditor::DeleteNode(nsINode* aNode, bool aPreserveSelection, uint8_t aOptionalArgCount) { if (NS_WARN_IF(!aNode) || NS_WARN_IF(!aNode->IsContent())) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveNode); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } // Make dispatch `input` event after stopping preserving selection. AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::No, // not a user interaction __FUNCTION__); Maybe preserveSelection; if (aOptionalArgCount && aPreserveSelection) { preserveSelection.emplace(*this); } rv = DeleteNodeWithTransaction(MOZ_KnownLive(*aNode->AsContent())); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DeleteNodeWithTransaction() failed"); return rv; } Result HTMLEditor::DeleteTextWithTransaction( Text& aTextNode, uint32_t aOffset, uint32_t aLength) { if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aTextNode))) { return Err(NS_ERROR_FAILURE); } Result caretPointOrError = EditorBase::DeleteTextWithTransaction(aTextNode, aOffset, aLength); NS_WARNING_ASSERTION(caretPointOrError.isOk(), "EditorBase::DeleteTextWithTransaction() failed"); return caretPointOrError; } Result HTMLEditor::ReplaceTextWithTransaction( dom::Text& aTextNode, const ReplaceWhiteSpacesData& aData) { Result insertTextResultOrError = ReplaceTextWithTransaction(aTextNode, aData.mReplaceStartOffset, aData.ReplaceLength(), aData.mNormalizedString); if (MOZ_UNLIKELY(insertTextResultOrError.isErr()) || aData.mNewOffsetAfterReplace > aTextNode.TextDataLength()) { return insertTextResultOrError; } InsertTextResult insertTextResult = insertTextResultOrError.unwrap(); insertTextResult.IgnoreCaretPointSuggestion(); EditorDOMPoint pointToPutCaret(&aTextNode, aData.mNewOffsetAfterReplace); return InsertTextResult(std::move(insertTextResult), std::move(pointToPutCaret)); } Result HTMLEditor::ReplaceTextWithTransaction( Text& aTextNode, uint32_t aOffset, uint32_t aLength, const nsAString& aStringToInsert) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aLength > 0 || !aStringToInsert.IsEmpty()); if (aStringToInsert.IsEmpty()) { Result caretPointOrError = DeleteTextWithTransaction(aTextNode, aOffset, aLength); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); return caretPointOrError.propagateErr(); } return InsertTextResult(EditorDOMPoint(&aTextNode, aOffset), caretPointOrError.unwrap()); } if (!aLength) { Result insertTextResult = InsertTextWithTransaction(aStringToInsert, EditorDOMPoint(&aTextNode, aOffset), InsertTextTo::ExistingTextNodeIfAvailable); NS_WARNING_ASSERTION(insertTextResult.isOk(), "HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult; } if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aTextNode))) { return Err(NS_ERROR_FAILURE); } // This should emulates inserting text for better undo/redo behavior. IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); // FYI: Create the insertion point before changing the DOM tree because // the point may become invalid offset after that. EditorDOMPointInText pointToInsert(&aTextNode, aOffset); RefPtr transaction = ReplaceTextTransaction::Create( *this, aStringToInsert, aTextNode, aOffset, aLength); MOZ_ASSERT(transaction); if (aLength && !mActionListeners.IsEmpty()) { for (auto& listener : mActionListeners.Clone()) { DebugOnly rvIgnored = listener->WillDeleteText(&aTextNode, aOffset, aLength); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsIEditActionListener::WillDeleteText() failed, but ignored"); } } nsresult rv = DoTransactionInternal(transaction); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DoTransactionInternal() failed"); // Don't check whether we've been destroyed here because we need to notify // listeners and observers below even if we've already destroyed. EditorDOMPoint endOfInsertedText(&aTextNode, aOffset + aStringToInsert.Length()); if (pointToInsert.IsSet()) { auto [begin, end] = ComputeInsertedRange(pointToInsert, aStringToInsert); if (begin.IsSet() && end.IsSet()) { TopLevelEditSubActionDataRef().DidDeleteText( *this, begin.To()); TopLevelEditSubActionDataRef().DidInsertText( *this, begin.To(), end.To()); } // XXX Should we update endOfInsertedText here? } if (!mActionListeners.IsEmpty()) { for (auto& listener : mActionListeners.Clone()) { DebugOnly rvIgnored = listener->DidInsertText(&aTextNode, aOffset, aStringToInsert, rv); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsIEditActionListener::DidInsertText() failed, but ignored"); } } if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } return InsertTextResult( std::move(endOfInsertedText), transaction->SuggestPointToPutCaret()); } Result HTMLEditor::InsertOrReplaceTextWithTransaction( const EditorDOMPoint& aPointToInsert, const NormalizedStringToInsertText& aData) { MOZ_ASSERT(aPointToInsert.IsInContentNodeAndValid()); MOZ_ASSERT_IF(aData.ReplaceLength(), aPointToInsert.IsInTextNode()); Result insertTextResultOrError = !aData.ReplaceLength() ? InsertTextWithTransaction(aData.mNormalizedString, aPointToInsert, InsertTextTo::SpecifiedPoint) : ReplaceTextWithTransaction( MOZ_KnownLive(*aPointToInsert.ContainerAs()), aData.mReplaceStartOffset, aData.ReplaceLength(), aData.mNormalizedString); if (MOZ_UNLIKELY(insertTextResultOrError.isErr())) { NS_WARNING(!aData.ReplaceLength() ? "HTMLEditor::InsertTextWithTransaction() failed" : "HTMLEditor::ReplaceTextWithTransaction() failed"); return insertTextResultOrError; } InsertTextResult insertTextResult = insertTextResultOrError.unwrap(); if (!aData.ReplaceLength()) { auto pointToPutCaret = [&]() -> EditorDOMPoint { return insertTextResult.HasCaretPointSuggestion() ? insertTextResult.UnwrapCaretPoint() : insertTextResult.EndOfInsertedTextRef(); }(); return InsertTextResult(std::move(insertTextResult), std::move(pointToPutCaret)); } insertTextResult.IgnoreCaretPointSuggestion(); Text* const insertedTextNode = insertTextResult.EndOfInsertedTextRef().GetContainerAs(); if (NS_WARN_IF(!insertedTextNode) || NS_WARN_IF(!insertedTextNode->IsInComposedDoc()) || NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*insertedTextNode))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } const uint32_t expectedEndOffset = aData.EndOffsetOfInsertedText(); if (NS_WARN_IF(expectedEndOffset > insertedTextNode->TextDataLength())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // We need to return end point of the insertion string instead of end of // replaced following white-spaces. EditorDOMPoint endOfNewString(insertedTextNode, expectedEndOffset); EditorDOMPoint pointToPutCaret = endOfNewString; return InsertTextResult(std::move(endOfNewString), CaretPoint(std::move(pointToPutCaret))); } Result HTMLEditor::InsertTextWithTransaction( const nsAString& aStringToInsert, const EditorDOMPoint& aPointToInsert, InsertTextTo aInsertTextTo) { if (NS_WARN_IF(!aPointToInsert.IsSet())) { return Err(NS_ERROR_INVALID_ARG); } // Do nothing if the node is read-only if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode( *aPointToInsert.GetContainer()))) { return Err(NS_ERROR_FAILURE); } return EditorBase::InsertTextWithTransaction(aStringToInsert, aPointToInsert, aInsertTextTo); } Result HTMLEditor::PrepareToInsertLineBreak( LineBreakType aLineBreakType, const EditorDOMPoint& aPointToInsert) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aPointToInsert.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } const auto CanInsertLineBreak = [aLineBreakType](const nsIContent& aContent) { if (aLineBreakType == LineBreakType::BRElement) { return HTMLEditUtils::CanNodeContain(aContent, *nsGkAtoms::br); } MOZ_ASSERT(aLineBreakType == LineBreakType::Linefeed); return HTMLEditUtils::CanNodeContain(aContent, *nsGkAtoms::textTagName) && EditorUtils::IsNewLinePreformatted(aContent); }; // If we're being initialized, we cannot normalize white-spaces because the // normalizer may remove invisible `Text`, but it's not allowed during the // initialization. // FIXME: Anyway, we should not do this at initialization. This is required // only for making users can put caret into empty table cells and list items. const bool canNormalizeWhiteSpaces = mInitSucceeded; if (!aPointToInsert.IsInTextNode()) { if (NS_WARN_IF( !CanInsertLineBreak(*aPointToInsert.ContainerAs()))) { return Err(NS_ERROR_FAILURE); } if (!canNormalizeWhiteSpaces || !StaticPrefs::editor_white_space_normalization_blink_compatible()) { return aPointToInsert; } Result pointToInsertOrError = WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( *this, aPointToInsert, {WhiteSpaceVisibilityKeeper::NormalizeOption:: StopIfPrecedingWhiteSpacesEndsWithNBP}); if (NS_WARN_IF(pointToInsertOrError.isErr())) { return pointToInsertOrError.propagateErr(); } return pointToInsertOrError.unwrap(); } // If the text node is not in an element node, we cannot insert a line break // around the text node. const Element* const containerOrNewLineBreak = aPointToInsert.GetContainerParentAs(); if (NS_WARN_IF(!containerOrNewLineBreak) || NS_WARN_IF(!CanInsertLineBreak(*containerOrNewLineBreak))) { return Err(NS_ERROR_FAILURE); } Result pointToInsertOrError = canNormalizeWhiteSpaces && StaticPrefs::editor_white_space_normalization_blink_compatible() ? WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( *this, aPointToInsert, {WhiteSpaceVisibilityKeeper::NormalizeOption:: StopIfPrecedingWhiteSpacesEndsWithNBP}) : aPointToInsert; if (NS_WARN_IF(pointToInsertOrError.isErr())) { return pointToInsertOrError.propagateErr(); } const EditorDOMPoint pointToInsert = pointToInsertOrError.unwrap(); if (!pointToInsert.IsInTextNode()) { return pointToInsert.ParentPoint(); } if (pointToInsert.IsStartOfContainer()) { // Insert before the text node. return pointToInsert.ParentPoint(); } if (pointToInsert.IsEndOfContainer()) { // Insert after the text node. return EditorDOMPoint::After(*pointToInsert.ContainerAs()); } MOZ_DIAGNOSTIC_ASSERT(pointToInsert.IsSetAndValid()); // Unfortunately, we need to split the text node at the offset. Result splitTextNodeResult = SplitNodeWithTransaction(pointToInsert); if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitTextNodeResult.propagateErr(); } // TODO: Stop updating `Selection`. nsresult rv = splitTextNodeResult.inspect().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } // Insert new line break before the right node. auto atNextContent = splitTextNodeResult.inspect().AtNextContent(); if (MOZ_UNLIKELY(!atNextContent.IsInContentNode())) { NS_WARNING("The next node seems not in the DOM tree"); return Err(NS_ERROR_FAILURE); } return atNextContent; } Maybe HTMLEditor::GetPreferredLineBreakType( const nsINode& aNode, const Element& aEditingHost) const { const Element* const container = aNode.GetAsElementOrParentElement(); if (MOZ_UNLIKELY(!container)) { return Nothing(); } // For backward compatibility, we should not insert a linefeed if // paragraph separator is set to "br" which is Gecko-specific mode. if (GetDefaultParagraphSeparator() == ParagraphSeparator::br) { return Some(LineBreakType::BRElement); } // And also if we're the mail composer, the content needs to be serialized. // Therefore, we should always use
    for the serializer. if (IsMailEditor() || IsPlaintextMailComposer()) { return Some(LineBreakType::BRElement); } if (HTMLEditUtils::ShouldInsertLinefeedCharacter(EditorDOMPoint(container, 0), aEditingHost) && HTMLEditUtils::CanNodeContain(*container, *nsGkAtoms::textTagName)) { return Some(LineBreakType::Linefeed); } if (MOZ_UNLIKELY( !HTMLEditUtils::CanNodeContain(*container, *nsGkAtoms::br))) { return Nothing(); } return Some(LineBreakType::BRElement); } Result HTMLEditor::InsertLineBreak( WithTransaction aWithTransaction, LineBreakType aLineBreakType, const EditorDOMPoint& aPointToInsert, EDirection aSelect /* = eNone */) { MOZ_ASSERT(IsEditActionDataAvailable()); Result pointToInsertOrError = PrepareToInsertLineBreak(aLineBreakType, aPointToInsert); if (MOZ_UNLIKELY(pointToInsertOrError.isErr())) { NS_WARNING( nsPrintfCString("HTMLEditor::PrepareToInsertLineBreak(%s) failed", ToString(aWithTransaction).c_str()) .get()); return pointToInsertOrError.propagateErr(); } EditorDOMPoint pointToInsert = pointToInsertOrError.unwrap(); MOZ_ASSERT(pointToInsert.IsInContentNode()); MOZ_ASSERT(pointToInsert.IsSetAndValid()); auto lineBreakOrError = [&]() MOZ_NEVER_INLINE_DEBUG MOZ_CAN_RUN_SCRIPT -> Result { if (aLineBreakType == LineBreakType::BRElement) { Result insertBRElementResultOrError = InsertBRElement(aWithTransaction, BRElementType::Normal, pointToInsert); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING( nsPrintfCString( "EditorBase::InsertBRElement(%s, BRElementType::Normal) failed", ToString(aWithTransaction).c_str()) .get()); return insertBRElementResultOrError.propagateErr(); } CreateElementResult insertBRElementResult = insertBRElementResultOrError.unwrap(); MOZ_ASSERT(insertBRElementResult.Handled()); insertBRElementResult.IgnoreCaretPointSuggestion(); return EditorLineBreak(insertBRElementResult.UnwrapNewNode()); } MOZ_ASSERT(aLineBreakType == LineBreakType::Linefeed); RefPtr newTextNode = CreateTextNode(u"\n"_ns); if (NS_WARN_IF(!newTextNode)) { return Err(NS_ERROR_FAILURE); } if (aWithTransaction == WithTransaction::Yes) { Result insertTextNodeResult = InsertNodeWithTransaction(*newTextNode, pointToInsert); if (MOZ_UNLIKELY(insertTextNodeResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertTextNodeResult.propagateErr(); } insertTextNodeResult.unwrap().IgnoreCaretPointSuggestion(); } else { Unused << pointToInsert.Offset(); RefPtr transaction = InsertNodeTransaction::Create(*this, *newTextNode, pointToInsert); nsresult rv = transaction->DoTransaction(); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("InsertNodeTransaction::DoTransaction() failed"); return Err(rv); } if (NS_WARN_IF(newTextNode->GetParentNode() != pointToInsert.GetContainer())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } RangeUpdaterRef().SelAdjInsertNode(EditorRawDOMPoint( pointToInsert.GetContainer(), pointToInsert.Offset())); } if (NS_WARN_IF(!newTextNode->TextDataLength() || newTextNode->TextFragment().CharAt(0) != '\n')) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return EditorLineBreak(std::move(newTextNode), 0u); }(); if (MOZ_UNLIKELY(lineBreakOrError.isErr())) { return lineBreakOrError.propagateErr(); } EditorLineBreak lineBreak = lineBreakOrError.unwrap(); auto pointToPutCaret = [&]() -> EditorDOMPoint { switch (aSelect) { case eNext: { return lineBreak.After(); } case ePrevious: { return lineBreak.Before(); } default: NS_WARNING( "aSelect has invalid value, the caller need to set selection " "by itself"); [[fallthrough]]; case eNone: return lineBreak.To(); } }(); return CreateLineBreakResult(std::move(lineBreak), std::move(pointToPutCaret)); } nsresult HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak( const EditorDOMPoint& aNextOrAfterModifiedPoint) { MOZ_ASSERT(aNextOrAfterModifiedPoint.IsInContentNode()); // If the point is in a mailcite in plaintext mail composer (it is a // styled as block), we should not treat its padding
    as unnecessary // because it's required by the serializer to give next content of the // mailcite has its own line. if (IsPlaintextMailComposer()) { const Element* const blockElement = HTMLEditUtils::GetInclusiveAncestorElement( *aNextOrAfterModifiedPoint.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayStyle); if (blockElement && HTMLEditUtils::IsMailCite(*blockElement) && HTMLEditUtils::IsInlineContent(*blockElement, BlockInlineCheck::UseHTMLDefaultStyle)) { return NS_OK; } } const bool isWhiteSpacePreformatted = EditorUtils::IsWhiteSpacePreformatted( *aNextOrAfterModifiedPoint.ContainerAs()); const DebugOnly isNewLinePreformatted = EditorUtils::IsNewLinePreformatted( *aNextOrAfterModifiedPoint.ContainerAs()); const Maybe unnecessaryLineBreak = HTMLEditUtils::GetFollowingUnnecessaryLineBreak( aNextOrAfterModifiedPoint); if (MOZ_LIKELY(unnecessaryLineBreak.isNothing() || !unnecessaryLineBreak->IsDeletableFromComposedDoc())) { return NS_OK; } if (unnecessaryLineBreak->IsHTMLBRElement()) { // If the invisible break is a placeholder of ancestor inline elements, we // should not delete it to allow users to insert text with the format // specified by them. if (HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( unnecessaryLineBreak->BRElementRef(), BlockInlineCheck::UseComputedDisplayStyle)) { return NS_OK; } nsresult rv = DeleteNodeWithTransaction( MOZ_KnownLive(unnecessaryLineBreak->BRElementRef())); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DeleteNodeWithTransaction() failed " "to delete unnecessary
    "); return rv; } MOZ_ASSERT(isNewLinePreformatted); const auto IsVisibleChar = [&](char16_t aChar) { switch (aChar) { case HTMLEditUtils::kNewLine: return true; case HTMLEditUtils::kSpace: case HTMLEditUtils::kTab: case HTMLEditUtils::kCarriageReturn: return isWhiteSpacePreformatted; default: return true; } }; const nsTextFragment& textFragment = unnecessaryLineBreak->TextRef().TextFragment(); const uint32_t length = textFragment.GetLength(); const DebugOnly lastChar = textFragment.CharAt(length - 1); MOZ_ASSERT(lastChar == HTMLEditUtils::kNewLine); const bool textNodeHasVisibleChar = [&]() { if (length == 1u) { return false; } for (const uint32_t offset : Reversed(IntegerRange(length - 1))) { if (IsVisibleChar(textFragment.CharAt(offset))) { return true; } } return false; }(); if (!textNodeHasVisibleChar) { // If the invisible break is a placeholder of ancestor inline elements, we // should not delete it to allow users to insert text with the format // specified by them. if (HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( unnecessaryLineBreak->TextRef(), BlockInlineCheck::UseComputedDisplayStyle)) { return NS_OK; } nsresult rv = DeleteNodeWithTransaction( MOZ_KnownLive(unnecessaryLineBreak->TextRef())); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DeleteNodeWithTransaction() failed " "to delete unnecessary Text node"); return rv; } Result result = DeleteTextWithTransaction(MOZ_KnownLive(unnecessaryLineBreak->TextRef()), unnecessaryLineBreak->Offset(), 1); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); return result.unwrapErr(); } result.unwrap().IgnoreCaretPointSuggestion(); return NS_OK; } Result HTMLEditor::InsertContainerWithTransaction( nsIContent& aContentToBeWrapped, const nsAtom& aWrapperTagName, const InitializeInsertingElement& aInitializer) { EditorDOMPoint pointToInsertNewContainer(&aContentToBeWrapped); if (NS_WARN_IF(!pointToInsertNewContainer.IsSet())) { return Err(NS_ERROR_FAILURE); } // aContentToBeWrapped will be moved to the new container before inserting the // new container. So, when we insert the container, the insertion point is // before the next sibling of aContentToBeWrapped. // XXX If pointerToInsertNewContainer stores offset here, the offset and // referring child node become mismatched. Although, currently this // is not a problem since InsertNodeTransaction refers only child node. MOZ_ALWAYS_TRUE(pointToInsertNewContainer.AdvanceOffset()); // Create new container. RefPtr newContainer = CreateHTMLContent(&aWrapperTagName); if (NS_WARN_IF(!newContainer)) { return Err(NS_ERROR_FAILURE); } if (&aInitializer != &HTMLEditor::DoNothingForNewElement) { nsresult rv = aInitializer(*this, *newContainer, EditorDOMPoint(&aContentToBeWrapped)); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("aInitializer() failed"); return Err(rv); } } // Notify our internal selection state listener AutoInsertContainerSelNotify selNotify(RangeUpdaterRef()); // Put aNode in the new container, first. // XXX Perhaps, we should not remove the container if it's not editable. nsresult rv = DeleteNodeWithTransaction(aContentToBeWrapped); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } { // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary // in normal cases. However, it may be required for nested edit // actions which may be caused by legacy mutation event listeners or // chrome script. AutoTransactionsConserveSelection conserveSelection(*this); Result insertContentNodeResult = InsertNodeWithTransaction(aContentToBeWrapped, EditorDOMPoint(newContainer, 0u)); if (MOZ_UNLIKELY(insertContentNodeResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertContentNodeResult.propagateErr(); } insertContentNodeResult.inspect().IgnoreCaretPointSuggestion(); } // Put the new container where aNode was. Result insertNewContainerElementResult = InsertNodeWithTransaction(*newContainer, pointToInsertNewContainer); NS_WARNING_ASSERTION(insertNewContainerElementResult.isOk(), "EditorBase::InsertNodeWithTransaction() failed"); return insertNewContainerElementResult; } Result HTMLEditor::ReplaceContainerWithTransactionInternal( Element& aOldContainer, const nsAtom& aTagName, const nsAtom& aAttribute, const nsAString& aAttributeValue, bool aCloneAllAttributes) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aOldContainer)) || NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aOldContainer))) { return Err(NS_ERROR_FAILURE); } // If we're replacing
    or
    with different type of element, we need to // split the parent
    . OwningNonNull containerElementToDelete = aOldContainer; if (aOldContainer.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dt) && &aTagName != nsGkAtoms::dt && &aTagName != nsGkAtoms::dd && // aOldContainer always has a parent node because of removable. aOldContainer.GetParentNode()->IsHTMLElement(nsGkAtoms::dl)) { OwningNonNull const dlElement = *aOldContainer.GetParentElement(); if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(dlElement)) || NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(dlElement))) { return Err(NS_ERROR_FAILURE); } Result splitDLElementResult = SplitRangeOffFromElement(dlElement, aOldContainer, aOldContainer); if (MOZ_UNLIKELY(splitDLElementResult.isErr())) { NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed"); return splitDLElementResult.propagateErr(); } splitDLElementResult.inspect().IgnoreCaretPointSuggestion(); RefPtr middleDLElement = aOldContainer.GetParentElement(); if (NS_WARN_IF(!middleDLElement) || NS_WARN_IF(!middleDLElement->IsHTMLElement(nsGkAtoms::dl)) || NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*middleDLElement))) { NS_WARNING("The parent
    was lost at splitting it"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } containerElementToDelete = std::move(middleDLElement); } const RefPtr newContainer = CreateHTMLContent(&aTagName); if (NS_WARN_IF(!newContainer)) { return Err(NS_ERROR_FAILURE); } // Set or clone attribute if needed. // FIXME: What should we do attributes of
    elements if we removed it // above? if (aCloneAllAttributes) { MOZ_ASSERT(&aAttribute == nsGkAtoms::_empty); CloneAttributesWithTransaction(*newContainer, aOldContainer); } else if (&aAttribute != nsGkAtoms::_empty) { nsresult rv = newContainer->SetAttr(kNameSpaceID_None, const_cast(&aAttribute), aAttributeValue, true); if (NS_FAILED(rv)) { NS_WARNING("Element::SetAttr() failed"); return Err(NS_ERROR_FAILURE); } } const OwningNonNull parentNode = *containerElementToDelete->GetParentNode(); const nsCOMPtr referenceNode = containerElementToDelete->GetNextSibling(); AutoReplaceContainerSelNotify selStateNotify(RangeUpdaterRef(), aOldContainer, *newContainer); { AutoTArray, 32> arrayOfChildren; HTMLEditUtils::CollectChildren( aOldContainer, arrayOfChildren, 0u, // Move non-editable children too because its container, aElement, is // editable so that all children must be removable node. {}); // TODO: Remove AutoTransactionsConserveSelection here. It's not necessary // in normal cases. However, it may be required for nested edit // actions which may be caused by legacy mutation event listeners or // chrome script. AutoTransactionsConserveSelection conserveSelection(*this); // Move all children from the old container to the new container. // For making all MoveNodeTransactions have a reference node in the current // parent, move nodes from last one to preceding ones. for (const OwningNonNull& child : Reversed(arrayOfChildren)) { Result moveChildResult = MoveNodeWithTransaction(MOZ_KnownLive(child), // due to bug 1622253. EditorDOMPoint(newContainer, 0u)); if (MOZ_UNLIKELY(moveChildResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveChildResult.propagateErr(); } // We'll suggest new caret point which is suggested by new container // element insertion result. Therefore, we need to do nothing here. moveChildResult.inspect().IgnoreCaretPointSuggestion(); } } // Delete containerElementToDelete from the DOM tree to make it not referred // by InsertNodeTransaction. nsresult rv = DeleteNodeWithTransaction(containerElementToDelete); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } if (referenceNode && (!referenceNode->GetParentNode() || parentNode != referenceNode->GetParentNode())) { NS_WARNING( "The reference node for insertion has been moved to different parent, " "so we got lost the insertion point"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Finally, insert the new node to where probably aOldContainer was. Result insertNewContainerElementResult = InsertNodeWithTransaction( *newContainer, referenceNode ? EditorDOMPoint(referenceNode) : EditorDOMPoint::AtEndOf(*parentNode)); NS_WARNING_ASSERTION(insertNewContainerElementResult.isOk(), "EditorBase::InsertNodeWithTransaction() failed"); MOZ_ASSERT_IF( insertNewContainerElementResult.isOk(), insertNewContainerElementResult.inspect().GetNewNode() == newContainer); return insertNewContainerElementResult; } Result HTMLEditor::RemoveContainerWithTransaction( Element& aElement) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(aElement)) || NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(aElement))) { return Err(NS_ERROR_FAILURE); } // Notify our internal selection state listener. AutoRemoveContainerSelNotify selNotify(RangeUpdaterRef(), EditorRawDOMPoint(&aElement)); AutoTArray, 32> arrayOfChildren; HTMLEditUtils::CollectChildren( aElement, arrayOfChildren, 0u, // Move non-editable children too because its container, aElement, is // editable so that all children must be removable node. {}); const OwningNonNull parentNode = *aElement.GetParentNode(); nsCOMPtr previousChild = aElement.GetPreviousSibling(); // For making all MoveNodeTransactions have a referenc node in the current // parent, move nodes from last one to preceding ones. for (const OwningNonNull& child : Reversed(arrayOfChildren)) { if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(child))) { continue; } Result moveChildResult = MoveNodeWithTransaction( MOZ_KnownLive(child), // due to bug 1622253. previousChild ? EditorDOMPoint::After(previousChild) : EditorDOMPoint(parentNode, 0u)); if (MOZ_UNLIKELY(moveChildResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveChildResult.propagateErr(); } // If the reference node was moved to different container, try to recover // the original position. if (previousChild && MOZ_UNLIKELY(previousChild->GetParentNode() != parentNode)) { if (MOZ_UNLIKELY(child->GetParentNode() != parentNode)) { NS_WARNING( "Neither the reference (previous) sibling nor the moved child was " "in the expected parent node"); moveChildResult.inspect().IgnoreCaretPointSuggestion(); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } previousChild = child->GetPreviousSibling(); } // We'll need to put caret at next sibling of aElement if nobody moves // content nodes under the parent node except us. moveChildResult.inspect().IgnoreCaretPointSuggestion(); } if (aElement.GetParentNode() && aElement.GetParentNode() != parentNode) { NS_WARNING( "The removing element has already been moved to another element"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } NS_WARNING_ASSERTION(!aElement.GetFirstChild(), "The removing container still has some children, but " "they are removed by removing the container"); auto GetNextSiblingOf = [](const nsTArray>& aArrayOfMovedContent, const nsINode& aExpectedParentNode) -> nsIContent* { for (const OwningNonNull& movedChild : Reversed(aArrayOfMovedContent)) { if (movedChild != &aExpectedParentNode) { continue; // Ignore moved node which was moved to different place } return movedChild->GetNextSibling(); } // XXX If all nodes were moved by web apps, we cannot suggest "collect" // position without computing the index of aElement. However, I // don't think that it's necessary for the web apps in the wild. return nullptr; }; nsCOMPtr nextSibling = aElement.GetParentNode() ? aElement.GetNextSibling() : GetNextSiblingOf(arrayOfChildren, *parentNode); nsresult rv = DeleteNodeWithTransaction(aElement); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeTransaction() failed"); return Err(rv); } if (nextSibling && nextSibling->GetParentNode() != parentNode) { nextSibling = GetNextSiblingOf(arrayOfChildren, *parentNode); } return nextSibling ? EditorDOMPoint(nextSibling) : EditorDOMPoint::AtEndOf(*parentNode); } MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentAppended( nsIContent* aFirstNewContent) { DoContentInserted(aFirstNewContent, ContentNodeIs::Appended); } MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentInserted( nsIContent* aChild) { DoContentInserted(aChild, ContentNodeIs::Inserted); } bool HTMLEditor::IsInObservedSubtree(nsIContent* aChild) { if (!aChild) { return false; } // FIXME(emilio, bug 1596856): This should probably work if the root is in the // same shadow tree as the child, probably? I don't know what the // contenteditable-in-shadow-dom situation is. if (Element* root = GetRoot()) { // To be super safe here, check both ChromeOnlyAccess and NAC / Shadow DOM. // That catches (also unbound) native anonymous content and ShadowDOM. if (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() || root->IsInNativeAnonymousSubtree() != aChild->IsInNativeAnonymousSubtree() || root->IsInShadowTree() != aChild->IsInShadowTree()) { return false; } } return !aChild->ChromeOnlyAccess() && !aChild->IsInShadowTree() && !aChild->IsInNativeAnonymousSubtree(); } void HTMLEditor::DoContentInserted(nsIContent* aChild, ContentNodeIs aContentNodeIs) { MOZ_ASSERT(aChild); nsINode* container = aChild->GetParentNode(); MOZ_ASSERT(container); if (!IsInObservedSubtree(aChild)) { return; } // XXX Why do we need this? This method is a helper of mutation observer. // So, the callers of mutation observer should guarantee that this won't // be deleted at least during the call. RefPtr kungFuDeathGrip(this); // Do not create AutoEditActionDataSetter here because it grabs `Selection`, // but that appear in the profile. If you need to create to it in some cases, // you should do it in the minimum scope. if (ShouldReplaceRootElement()) { // Forget maybe disconnected root element right now because nobody should // work with it. mRootElement = nullptr; if (mPendingRootElementUpdatedRunner) { return; } mPendingRootElementUpdatedRunner = NewRunnableMethod( "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged); nsContentUtils::AddScriptRunner( do_AddRef(mPendingRootElementUpdatedRunner)); return; } // We don't need to handle our own modifications if (!GetTopLevelEditSubAction() && container->IsEditable()) { if (EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) { // Ignore insertion of the padding
    element. return; } nsresult rv = OnDocumentModified(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::OnDocumentModified() failed, but ignored"); // Update spellcheck for only the newly-inserted node (bug 743819) if (mInlineSpellChecker) { nsIContent* endContent = aChild; if (aContentNodeIs == ContentNodeIs::Appended) { nsIContent* child = nullptr; for (child = aChild; child; child = child->GetNextSibling()) { if (child->InclusiveDescendantMayNeedSpellchecking(this)) { break; } } if (!child) { // No child needed spellchecking, return. return; } // Maybe more than 1 child was appended. endContent = container->GetLastChild(); } else if (!aChild->InclusiveDescendantMayNeedSpellchecking(this)) { return; } RefPtr range = nsRange::Create(aChild); range->SelectNodesInContainer(container, aChild, endContent); DebugOnly rvIgnored = mInlineSpellChecker->SpellCheckRange(range); NS_WARNING_ASSERTION( rvIgnored == NS_ERROR_NOT_INITIALIZED || NS_SUCCEEDED(rvIgnored), "mozInlineSpellChecker::SpellCheckRange() failed, but ignored"); } } } MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentWillBeRemoved( nsIContent* aChild, const BatchRemovalState*) { if (mLastCollapsibleWhiteSpaceAppendedTextNode == aChild) { mLastCollapsibleWhiteSpaceAppendedTextNode = nullptr; } if (!IsInObservedSubtree(aChild)) { return; } // XXX Why do we need to do this? This method is a mutation observer's // method. Therefore, the caller should guarantee that this won't be // deleted during the call. RefPtr kungFuDeathGrip(this); // Do not create AutoEditActionDataSetter here because it grabs `Selection`, // but that appear in the profile. If you need to create to it in some cases, // you should do it in the minimum scope. // FYI: mRootElement may be the of the document or the root element. // Therefore, we don't need to check it across shadow DOM boundaries. if (mRootElement && mRootElement->IsInclusiveDescendantOf(aChild)) { // Forget the disconnected root element right now because nobody should work // with it. mRootElement = nullptr; if (mPendingRootElementUpdatedRunner) { return; } mPendingRootElementUpdatedRunner = NewRunnableMethod( "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged); nsContentUtils::AddScriptRunner( do_AddRef(mPendingRootElementUpdatedRunner)); return; } // We don't need to handle our own modifications if (!GetTopLevelEditSubAction() && aChild->GetParentNode()->IsEditable()) { if (aChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) { // Ignore removal of the padding
    element for empty editor. return; } nsresult rv = OnDocumentModified(aChild); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::OnDocumentModified() failed, but ignored"); } } MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::CharacterDataChanged( nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { if (!mInlineSpellChecker || !aContent->IsEditable() || !IsInObservedSubtree(aContent) || GetTopLevelEditSubAction() != EditSubAction::eNone) { return; } nsIContent* parent = aContent->GetParent(); if (!parent || !parent->InclusiveDescendantMayNeedSpellchecking(this)) { return; } RefPtr range = nsRange::Create(aContent); range->SelectNodesInContainer(parent, aContent, aContent); DebugOnly rvIgnored = mInlineSpellChecker->SpellCheckRange(range); } nsresult HTMLEditor::SelectEntireDocument() { MOZ_ASSERT(IsEditActionDataAvailable()); if (!mInitSucceeded) { return NS_ERROR_NOT_INITIALIZED; } // XXX It's odd to select all of the document body if an contenteditable // element has focus. RefPtr bodyOrDocumentElement = GetRoot(); if (NS_WARN_IF(!bodyOrDocumentElement)) { return NS_ERROR_NOT_INITIALIZED; } // If we're empty, don't select all children because that would select the // padding
    element for empty editor. if (IsEmpty()) { nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return rv; } // Otherwise, select all children. ErrorResult error; SelectionRef().SelectAllChildren(*bodyOrDocumentElement, error); if (NS_WARN_IF(Destroyed())) { error.SuppressException(); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(!error.Failed(), "Selection::SelectAllChildren() failed"); return error.StealNSResult(); } nsresult HTMLEditor::SelectAllInternal() { MOZ_ASSERT(IsEditActionDataAvailable()); CommitComposition(); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } auto GetBodyElementIfElementIsParentOfHTMLBody = [](const Element& aElement) -> Element* { if (!aElement.OwnerDoc()->IsHTMLDocument()) { return const_cast(&aElement); } HTMLBodyElement* bodyElement = aElement.OwnerDoc()->GetBodyElement(); return bodyElement && nsContentUtils::ContentIsFlattenedTreeDescendantOf( bodyElement, &aElement) ? bodyElement : const_cast(&aElement); }; nsCOMPtr selectionRootContent = [&]() MOZ_CAN_RUN_SCRIPT -> nsIContent* { RefPtr elementToBeSelected = [&]() -> Element* { // If there is at least one selection range, we should compute the // selection root from the anchor node. if (SelectionRef().RangeCount()) { if (nsIContent* content = nsIContent::FromNodeOrNull(SelectionRef().GetAnchorNode())) { if (content->IsElement()) { return content->AsElement(); } if (Element* parentElement = content->GetParentElementCrossingShadowRoot()) { return parentElement; } } } // If no element contains a selection range, we should select all children // of the focused element at least. if (Element* focusedElement = GetFocusedElement()) { return focusedElement; } // of the body or document element. Element* bodyOrDocumentElement = GetRoot(); NS_WARNING_ASSERTION(bodyOrDocumentElement, "There was no element in the document"); return bodyOrDocumentElement; }(); // Then, compute the selection root content to select all including // elementToBeSelected. RefPtr presShell = GetPresShell(); nsIContent* computedSelectionRootContent = elementToBeSelected->GetSelectionRootContent( presShell, nsINode::IgnoreOwnIndependentSelection::Yes, nsINode::AllowCrossShadowBoundary::No); if (NS_WARN_IF(!computedSelectionRootContent)) { return nullptr; } if (MOZ_UNLIKELY(!computedSelectionRootContent->IsElement())) { return computedSelectionRootContent; } return GetBodyElementIfElementIsParentOfHTMLBody( *computedSelectionRootContent->AsElement()); }(); if (NS_WARN_IF(!selectionRootContent)) { return NS_ERROR_FAILURE; } Maybe userSelection; // XXX Do we need to mark it as "user initiated" for // `Document.execCommand("selectAll")`? if (!selectionRootContent->IsEditable()) { userSelection.emplace(SelectionRef()); } ErrorResult error; SelectionRef().SelectAllChildren(*selectionRootContent, error); NS_WARNING_ASSERTION(!error.Failed(), "Selection::SelectAllChildren() failed"); return error.StealNSResult(); } bool HTMLEditor::SetCaretInTableCell(Element* aElement) { MOZ_ASSERT(IsEditActionDataAvailable()); if (!aElement || !aElement->IsHTMLElement() || !HTMLEditUtils::IsAnyTableElement(aElement)) { return false; } const RefPtr editingHost = ComputeEditingHost(); if (!editingHost || !aElement->IsInclusiveDescendantOf(editingHost)) { return false; } nsCOMPtr deepestFirstChild = aElement; while (deepestFirstChild->HasChildren()) { deepestFirstChild = deepestFirstChild->GetFirstChild(); } // Set selection at beginning of the found node nsresult rv = CollapseSelectionToStartOf(*deepestFirstChild); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return NS_SUCCEEDED(rv); } /** * This method scans the selection for adjacent text nodes * and collapses them into a single text node. * "adjacent" means literally adjacent siblings of the same parent. * Uses HTMLEditor::JoinNodesWithTransaction() so action is undoable. * Should be called within the context of a batch transaction. */ nsresult HTMLEditor::CollapseAdjacentTextNodes(nsRange& aRange) { AutoTransactionsConserveSelection dontChangeMySelection(*this); // we can't actually do anything during iteration, so store the text nodes in // an array first. DOMSubtreeIterator subtreeIter; if (NS_FAILED(subtreeIter.Init(aRange))) { NS_WARNING("DOMSubtreeIterator::Init() failed"); return NS_ERROR_FAILURE; } AutoTArray, 8> textNodes; subtreeIter.AppendNodesToArray( +[](nsINode& aNode, void*) -> bool { return EditorUtils::IsEditableContent(*aNode.AsText(), EditorType::HTML); }, textNodes); if (textNodes.Length() < 2) { return NS_OK; } OwningNonNull leftTextNode = textNodes[0]; for (size_t rightTextNodeIndex = 1; rightTextNodeIndex < textNodes.Length(); rightTextNodeIndex++) { OwningNonNull& rightTextNode = textNodes[rightTextNodeIndex]; // If the leftTextNode has only preformatted line break, keep it as-is. if (HTMLEditUtils::TextHasOnlyOnePreformattedLinefeed(leftTextNode)) { leftTextNode = rightTextNode; continue; } // If the rightTextNode has only preformatted line break, keep it as-is, and // advance the loop next to the rightTextNode. if (HTMLEditUtils::TextHasOnlyOnePreformattedLinefeed(rightTextNode)) { if (++rightTextNodeIndex == textNodes.Length()) { break; } leftTextNode = textNodes[rightTextNodeIndex]; continue; } // If the text nodes are not direct siblings, we shouldn't join them, and // we don't need to handle the left one anymore. if (leftTextNode->GetNextSibling() != rightTextNode) { leftTextNode = rightTextNode; continue; } Result joinNodesResultOrError = StaticPrefs::editor_white_space_normalization_blink_compatible() ? JoinTextNodesWithNormalizeWhiteSpaces( MOZ_KnownLive(leftTextNode), MOZ_KnownLive(rightTextNode)) : JoinNodesWithTransaction(MOZ_KnownLive(leftTextNode), MOZ_KnownLive(rightTextNode)); if (MOZ_UNLIKELY(joinNodesResultOrError.isErr())) { NS_WARNING( StaticPrefs::editor_white_space_normalization_blink_compatible() ? "HTMLEditor::JoinTextNodesWithNormalizeWhiteSpaces() failed" : "HTMLEditor::JoinNodesWithTransaction() failed"); return joinNodesResultOrError.unwrapErr(); } } return NS_OK; } nsresult HTMLEditor::SetSelectionAtDocumentStart() { MOZ_ASSERT(IsEditActionDataAvailable()); RefPtr rootElement = GetRoot(); if (NS_WARN_IF(!rootElement)) { return NS_ERROR_FAILURE; } nsresult rv = CollapseSelectionToStartOf(*rootElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return rv; } /** * Remove aNode, reparenting any children into the parent of aNode. In * addition, insert any br's needed to preserve identity of removed block. */ Result HTMLEditor::RemoveBlockContainerWithTransaction(Element& aElement) { MOZ_ASSERT(IsEditActionDataAvailable()); // Two possibilities: the container could be empty of editable content. If // that is the case, we need to compare what is before and after aNode to // determine if we need a br. // // Or it could be not empty, in which case we have to compare previous // sibling and first child to determine if we need a leading br, and compare // following sibling and last child to determine if we need a trailing br. const RefPtr parentElement = aElement.GetParentElement(); if (NS_WARN_IF((!parentElement))) { return Err(NS_ERROR_FAILURE); } EditorDOMPoint pointToPutCaret; if (HTMLEditUtils::CanNodeContain(*parentElement, *nsGkAtoms::br)) { if (nsCOMPtr child = HTMLEditUtils::GetFirstChild( aElement, {WalkTreeOption::IgnoreNonEditableNode})) { // The case of aNode not being empty. We need a br at start unless: // 1) previous sibling of aNode is a block, OR // 2) previous sibling of aNode is a br, OR // 3) first child of aNode is a block OR // 4) either is null if (nsIContent* previousSibling = HTMLEditUtils::GetPreviousSibling( aElement, {WalkTreeOption::IgnoreNonEditableNode})) { if (!HTMLEditUtils::IsBlockElement( *previousSibling, BlockInlineCheck::UseComputedDisplayOutsideStyle) && !previousSibling->IsHTMLElement(nsGkAtoms::br) && !HTMLEditUtils::IsBlockElement( *child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { Result insertBRElementResultOrError = InsertLineBreak(WithTransaction::Yes, LineBreakType::BRElement, EditorDOMPoint(&aElement)); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "LineBreakType::BRElement) failed"); return insertBRElementResultOrError.propagateErr(); } CreateLineBreakResult insertBRElementResult = insertBRElementResultOrError.unwrap(); MOZ_ASSERT(insertBRElementResult.Handled()); insertBRElementResult.IgnoreCaretPointSuggestion(); pointToPutCaret = EditorDOMPoint(&aElement, 0); } } // We need a br at end unless: // 1) following sibling of aNode is a block, OR // 2) last child of aNode is a block, OR // 3) last child of aNode is a br OR // 4) either is null if (nsIContent* nextSibling = HTMLEditUtils::GetNextSibling( aElement, {WalkTreeOption::IgnoreNonEditableNode})) { if (nextSibling && !HTMLEditUtils::IsBlockElement( *nextSibling, BlockInlineCheck::UseComputedDisplayStyle)) { if (nsIContent* lastChild = HTMLEditUtils::GetLastChild( aElement, {WalkTreeOption::IgnoreNonEditableNode}, BlockInlineCheck::Unused)) { if (!HTMLEditUtils::IsBlockElement( *lastChild, BlockInlineCheck::UseComputedDisplayStyle) && !lastChild->IsHTMLElement(nsGkAtoms::br)) { Result insertBRElementResultOrError = InsertLineBreak( WithTransaction::Yes, LineBreakType::BRElement, EditorDOMPoint::After(aElement)); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "LineBreakType::BRElement) failed"); return insertBRElementResultOrError.propagateErr(); } CreateLineBreakResult insertBRElementResult = insertBRElementResultOrError.unwrap(); MOZ_ASSERT(insertBRElementResult.Handled()); insertBRElementResult.IgnoreCaretPointSuggestion(); pointToPutCaret = EditorDOMPoint::AtEndOf(aElement); } } } } } else if (nsIContent* previousSibling = HTMLEditUtils::GetPreviousSibling( aElement, {WalkTreeOption::IgnoreNonEditableNode})) { // The case of aNode being empty. We need a br at start unless: // 1) previous sibling of aNode is a block, OR // 2) previous sibling of aNode is a br, OR // 3) following sibling of aNode is a block, OR // 4) following sibling of aNode is a br OR // 5) either is null if (!HTMLEditUtils::IsBlockElement( *previousSibling, BlockInlineCheck::UseComputedDisplayStyle) && !previousSibling->IsHTMLElement(nsGkAtoms::br)) { if (nsIContent* nextSibling = HTMLEditUtils::GetNextSibling( aElement, {WalkTreeOption::IgnoreNonEditableNode})) { if (!HTMLEditUtils::IsBlockElement( *nextSibling, BlockInlineCheck::UseComputedDisplayStyle) && !nextSibling->IsHTMLElement(nsGkAtoms::br)) { Result insertBRElementResultOrError = InsertLineBreak( WithTransaction::Yes, LineBreakType::BRElement, EditorDOMPoint(&aElement)); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "LineBreakType::BRElement) failed"); return insertBRElementResultOrError.propagateErr(); } CreateLineBreakResult insertBRElementResult = insertBRElementResultOrError.unwrap(); MOZ_ASSERT(insertBRElementResult.Handled()); insertBRElementResult.IgnoreCaretPointSuggestion(); pointToPutCaret = EditorDOMPoint(&aElement, 0); } } } } } // Now remove container AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret); Result unwrapBlockElementResult = RemoveContainerWithTransaction(aElement); if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed"); return unwrapBlockElementResult; } trackPointToPutCaret.FlushAndStopTracking(); if (AllowsTransactionsToChangeSelection() && unwrapBlockElementResult.inspect().IsSet()) { pointToPutCaret = unwrapBlockElementResult.unwrap(); } return pointToPutCaret; // May be unset } Result HTMLEditor::SplitNodeWithTransaction( const EditorDOMPoint& aStartOfRightNode) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aStartOfRightNode.IsInContentNode())) { return Err(NS_ERROR_INVALID_ARG); } MOZ_ASSERT(aStartOfRightNode.IsSetAndValid()); if (NS_WARN_IF(!HTMLEditUtils::IsSplittableNode( *aStartOfRightNode.ContainerAs()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eSplitNode, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "OnStartToHandleTopLevelEditSubAction() failed, but ignored"); RefPtr transaction = SplitNodeTransaction::Create(*this, aStartOfRightNode); nsresult rv = DoTransactionInternal(transaction); if (NS_WARN_IF(Destroyed())) { NS_WARNING( "EditorBase::DoTransactionInternal() caused destroying the editor"); return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DoTransactionInternal() failed"); return Err(rv); } nsIContent* newContent = transaction->GetNewContent(); nsIContent* splitContent = transaction->GetSplitContent(); if (NS_WARN_IF(!newContent) || NS_WARN_IF(!splitContent)) { return Err(NS_ERROR_FAILURE); } TopLevelEditSubActionDataRef().DidSplitContent(*this, *splitContent, *newContent); if (NS_WARN_IF(!newContent->IsInComposedDoc()) || NS_WARN_IF(!splitContent->IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return SplitNodeResult(*newContent, *splitContent); } Result HTMLEditor::SplitNodeDeepWithTransaction( nsIContent& aMostAncestorToSplit, const EditorDOMPoint& aDeepestStartOfRightNode, SplitAtEdges aSplitAtEdges) { MOZ_ASSERT(aDeepestStartOfRightNode.IsSetAndValidInComposedDoc()); MOZ_ASSERT( aDeepestStartOfRightNode.GetContainer() == &aMostAncestorToSplit || EditorUtils::IsDescendantOf(*aDeepestStartOfRightNode.GetContainer(), aMostAncestorToSplit)); if (NS_WARN_IF(!aDeepestStartOfRightNode.IsInComposedDoc())) { return Err(NS_ERROR_INVALID_ARG); } nsCOMPtr newLeftNodeOfMostAncestor; EditorDOMPoint atStartOfRightNode(aDeepestStartOfRightNode); // lastResult is as explained by its name, the last result which may not be // split a node actually. SplitNodeResult lastResult = SplitNodeResult::NotHandled(atStartOfRightNode); MOZ_ASSERT(lastResult.AtSplitPoint() .IsSetAndValidInComposedDoc()); while (true) { // Need to insert rules code call here to do things like not split a list // if you are after the last
  • or before the first, etc. For now we // just have some smarts about unnecessarily splitting text nodes, which // should be universal enough to put straight in this EditorBase routine. auto* splittingContent = atStartOfRightNode.GetContainerAs(); if (NS_WARN_IF(!splittingContent)) { lastResult.IgnoreCaretPointSuggestion(); return Err(NS_ERROR_FAILURE); } // If we meet an orphan node before meeting aMostAncestorToSplit, we need // to stop splitting. This is a bug of the caller. if (NS_WARN_IF(splittingContent != &aMostAncestorToSplit && !atStartOfRightNode.GetContainerParentAs())) { lastResult.IgnoreCaretPointSuggestion(); return Err(NS_ERROR_FAILURE); } // If the container is not splitable node such as comment node, atomic // element, etc, we should keep it as-is, and try to split its parents. if (!HTMLEditUtils::IsSplittableNode(*splittingContent)) { if (splittingContent == &aMostAncestorToSplit) { return lastResult; } atStartOfRightNode.Set(splittingContent); continue; } // If the split point is middle of the node or the node is not a text node // and we're allowed to create empty element node, split it. if ((aSplitAtEdges == SplitAtEdges::eAllowToCreateEmptyContainer && !atStartOfRightNode.IsInTextNode()) || (!atStartOfRightNode.IsStartOfContainer() && !atStartOfRightNode.IsEndOfContainer())) { Result splitNodeResult = SplitNodeWithTransaction(atStartOfRightNode); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { lastResult.IgnoreCaretPointSuggestion(); return splitNodeResult; } lastResult = SplitNodeResult::MergeWithDeeperSplitNodeResult( splitNodeResult.unwrap(), lastResult); if (NS_WARN_IF(!lastResult.AtSplitPoint() .IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } MOZ_ASSERT(lastResult.HasCaretPointSuggestion()); MOZ_ASSERT(lastResult.GetOriginalContent() == splittingContent); if (splittingContent == &aMostAncestorToSplit) { // Actually, we split aMostAncestorToSplit. return lastResult; } // Then, try to split its parent before current node. atStartOfRightNode = lastResult.AtNextContent(); } // If the split point is end of the node and it is a text node or we're not // allowed to create empty container node, try to split its parent after it. else if (!atStartOfRightNode.IsStartOfContainer()) { lastResult = SplitNodeResult::HandledButDidNotSplitDueToEndOfContainer( *splittingContent, &lastResult); MOZ_ASSERT(lastResult.AtSplitPoint() .IsSetAndValidInComposedDoc()); if (splittingContent == &aMostAncestorToSplit) { return lastResult; } // Try to split its parent after current node. atStartOfRightNode.SetAfter(splittingContent); } // If the split point is start of the node and it is a text node or we're // not allowed to create empty container node, try to split its parent. else { if (splittingContent == &aMostAncestorToSplit) { return SplitNodeResult::HandledButDidNotSplitDueToStartOfContainer( *splittingContent, &lastResult); } // Try to split its parent before current node. // XXX This is logically wrong. If we've already split something but // this is the last splitable content node in the limiter, this // method will return "not handled". lastResult = SplitNodeResult::NotHandled(atStartOfRightNode, &lastResult); MOZ_ASSERT(lastResult.AtSplitPoint() .IsSetAndValidInComposedDoc()); atStartOfRightNode.Set(splittingContent); MOZ_ASSERT(atStartOfRightNode.IsSetAndValidInComposedDoc()); } } // Not reached because while (true) loop never breaks. } Result HTMLEditor::DoSplitNode( const EditorDOMPoint& aStartOfRightNode, nsIContent& aNewNode) { // Ensure computing the offset if it's initialized with a child content node. Unused << aStartOfRightNode.Offset(); // XXX Perhaps, aStartOfRightNode may be invalid if this is a redo // operation after modifying DOM node with JS. if (NS_WARN_IF(!aStartOfRightNode.IsInContentNode())) { return Err(NS_ERROR_INVALID_ARG); } MOZ_DIAGNOSTIC_ASSERT(aStartOfRightNode.IsSetAndValid()); // Remember all selection points. AutoTArray savedRanges; for (SelectionType selectionType : kPresentSelectionTypes) { SavedRange savingRange; savingRange.mSelection = GetSelection(selectionType); if (NS_WARN_IF(!savingRange.mSelection && selectionType == SelectionType::eNormal)) { return Err(NS_ERROR_FAILURE); } if (!savingRange.mSelection) { // For non-normal selections, skip over the non-existing ones. continue; } for (uint32_t j : IntegerRange(savingRange.mSelection->RangeCount())) { const nsRange* r = savingRange.mSelection->GetRangeAt(j); MOZ_ASSERT(r); MOZ_ASSERT(r->IsPositioned()); // XXX Looks like that SavedRange should have mStart and mEnd which // are RangeBoundary. Then, we can avoid to compute offset here. savingRange.mStartContainer = r->GetStartContainer(); savingRange.mStartOffset = r->StartOffset(); savingRange.mEndContainer = r->GetEndContainer(); savingRange.mEndOffset = r->EndOffset(); savedRanges.AppendElement(savingRange); } } nsCOMPtr parent = aStartOfRightNode.GetContainerParent(); if (NS_WARN_IF(!parent)) { return Err(NS_ERROR_FAILURE); } // For the performance of IMEContentObserver, we should move all data into // aNewNode first because IMEContentObserver needs to compute moved content // length only once when aNewNode is connected. // If we are splitting a text node, we need to move its some data to the // new text node. MOZ_DIAGNOSTIC_ASSERT_IF(aStartOfRightNode.IsInTextNode(), aNewNode.IsText()); MOZ_DIAGNOSTIC_ASSERT_IF(!aStartOfRightNode.IsInTextNode(), !aNewNode.IsText()); const nsCOMPtr firstChildOfRightNode = aStartOfRightNode.GetChild(); nsresult rv = [&]() MOZ_NEVER_INLINE_DEBUG MOZ_CAN_RUN_SCRIPT { if (aStartOfRightNode.IsEndOfContainer()) { return NS_OK; // No content which should be moved into aNewNode. } if (aStartOfRightNode.IsInTextNode()) { Text* originalTextNode = aStartOfRightNode.ContainerAs(); Text* newTextNode = aNewNode.AsText(); nsAutoString movingText; const uint32_t cutStartOffset = aStartOfRightNode.Offset(); const uint32_t cutLength = originalTextNode->Length() - aStartOfRightNode.Offset(); IgnoredErrorResult error; originalTextNode->SubstringData(cutStartOffset, cutLength, movingText, error); NS_WARNING_ASSERTION(!error.Failed(), "Text::SubstringData() failed, but ignored"); error.SuppressException(); // XXX This call may destroy us. DoDeleteText(MOZ_KnownLive(*originalTextNode), cutStartOffset, cutLength, error); NS_WARNING_ASSERTION(!error.Failed(), "EditorBase::DoDeleteText() failed, but ignored"); error.SuppressException(); // XXX This call may destroy us. DoSetText(MOZ_KnownLive(*newTextNode), movingText, error); NS_WARNING_ASSERTION(!error.Failed(), "EditorBase::DoSetText() failed, but ignored"); return NS_OK; } // If the right node is new one and splitting at start of the container, // we need to move all children to the new right node. if (!firstChildOfRightNode->GetPreviousSibling()) { // XXX Why do we ignore an error while moving nodes from the right // node to the left node? nsresult rv = MoveAllChildren(*aStartOfRightNode.GetContainer(), EditorRawDOMPoint(&aNewNode, 0u)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::MoveAllChildren() failed"); return rv; } // If the right node is new one and splitting at middle of the node, we need // to move inclusive next siblings of the split point to the new right node. // XXX Why do we ignore an error while moving nodes from the right node // to the left node? nsresult rv = MoveInclusiveNextSiblings(*firstChildOfRightNode, EditorRawDOMPoint(&aNewNode, 0u)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::MoveInclusiveNextSiblings() failed"); return rv; }(); // To avoid a dataloss bug, we should try to insert aNewNode even if we've // already been destroyed. if (NS_WARN_IF(!aStartOfRightNode.GetContainerParent())) { return NS_WARN_IF(Destroyed()) ? Err(NS_ERROR_EDITOR_DESTROYED) : Err(NS_ERROR_FAILURE); } // Finally, we should insert aNewNode which already has proper data or // children. IgnoredErrorResult error; parent->InsertBefore( aNewNode, aStartOfRightNode.GetContainer()->GetNextSibling(), error); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("nsINode::InsertBefore() failed"); return Err(error.StealNSResult()); } if (NS_FAILED(rv)) { NS_WARNING("Moving children from left node to right node failed"); return Err(rv); } // Handle selection // TODO: Stop doing this, this shouldn't be necessary to update selection. if (RefPtr presShell = GetPresShell()) { presShell->FlushPendingNotifications(FlushType::Frames); } NS_WARNING_ASSERTION(!Destroyed(), "The editor is destroyed during splitting a node"); const bool allowedTransactionsToChangeSelection = AllowsTransactionsToChangeSelection(); RefPtr previousSelection; for (SavedRange& savedRange : savedRanges) { // If we have not seen the selection yet, clear all of its ranges. if (savedRange.mSelection != previousSelection) { MOZ_KnownLive(savedRange.mSelection)->RemoveAllRanges(error); if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("Selection::RemoveAllRanges() failed"); return Err(error.StealNSResult()); } previousSelection = savedRange.mSelection; } // XXX Looks like that we don't need to modify normal selection here // because selection will be modified by the caller if // AllowsTransactionsToChangeSelection() will return true. if (allowedTransactionsToChangeSelection && savedRange.mSelection->Type() == SelectionType::eNormal) { // If the editor should adjust the selection, don't bother restoring // the ranges for the normal selection here. continue; } auto AdjustDOMPoint = [&](nsCOMPtr& aContainer, uint32_t& aOffset) { if (aContainer != aStartOfRightNode.GetContainer()) { return; } // If the container is the left node and offset is after the split // point, the content was moved from the right node to aNewNode. // So, we need to change the container to aNewNode and decrease the // offset. if (aOffset >= aStartOfRightNode.Offset()) { aContainer = &aNewNode; aOffset -= aStartOfRightNode.Offset(); } }; AdjustDOMPoint(savedRange.mStartContainer, savedRange.mStartOffset); AdjustDOMPoint(savedRange.mEndContainer, savedRange.mEndOffset); RefPtr newRange = nsRange::Create(savedRange.mStartContainer, savedRange.mStartOffset, savedRange.mEndContainer, savedRange.mEndOffset, error); if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("nsRange::Create() failed"); return Err(error.StealNSResult()); } // The `MOZ_KnownLive` annotation is only necessary because of a bug // (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253) in the // static analyzer. MOZ_KnownLive(savedRange.mSelection) ->AddRangeAndSelectFramesAndNotifyListeners(*newRange, error); if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING( "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed"); return Err(error.StealNSResult()); } } // We don't need to set selection here because the caller should do that // in any case. // If splitting the node causes running mutation event listener and we've // got unexpected result, we should return error because callers will // continue to do their work without complicated DOM tree result. // NOTE: Perhaps, we shouldn't do this immediately after each DOM tree change // because stopping handling it causes some data loss. E.g., user // may loose the text which is moved to the new text node. // XXX We cannot check all descendants in the right node and the new left // node for performance reason. I think that if caller needs to access // some of the descendants, they should check by themselves. if (NS_WARN_IF(parent != aStartOfRightNode.GetContainer()->GetParentNode()) || NS_WARN_IF(parent != aNewNode.GetParentNode()) || NS_WARN_IF(aNewNode.GetPreviousSibling() != aStartOfRightNode.GetContainer())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } DebugOnly rvIgnored = RangeUpdaterRef().SelAdjSplitNode( *aStartOfRightNode.ContainerAs(), aStartOfRightNode.Offset(), aNewNode); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "RangeUpdater::SelAdjSplitNode() failed, but ignored"); return SplitNodeResult(aNewNode, *aStartOfRightNode.ContainerAs()); } Result HTMLEditor::JoinNodesWithTransaction( nsIContent& aLeftContent, nsIContent& aRightContent) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(&aLeftContent != &aRightContent); MOZ_ASSERT(aLeftContent.GetParentNode()); MOZ_ASSERT(aRightContent.GetParentNode()); MOZ_ASSERT(aLeftContent.GetParentNode() == aRightContent.GetParentNode()); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eJoinNodes, nsIEditor::ePrevious, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); if (NS_WARN_IF(!aRightContent.GetParentNode())) { return Err(NS_ERROR_FAILURE); } RefPtr transaction = JoinNodesTransaction::MaybeCreate(*this, aLeftContent, aRightContent); if (MOZ_UNLIKELY(!transaction)) { NS_WARNING("JoinNodesTransaction::MaybeCreate() failed"); return Err(NS_ERROR_FAILURE); } const nsresult rv = DoTransactionInternal(transaction); // FYI: Now, DidJoinNodesTransaction() must have been run if succeeded. if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } // This shouldn't occur unless the cycle collector runs by chrome script // forcibly. if (NS_WARN_IF(!transaction->GetRemovedContent()) || NS_WARN_IF(!transaction->GetExistingContent())) { return Err(NS_ERROR_UNEXPECTED); } // If joined node is moved to different place, offset may not have any // meaning. In this case, the web app modified the DOM tree takes on the // responsibility for the remaning things. if (NS_WARN_IF(transaction->GetExistingContent()->GetParent() != transaction->GetParentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DoTransactionInternal() failed"); return Err(rv); } return JoinNodesResult(transaction->CreateJoinedPoint(), *transaction->GetRemovedContent()); } void HTMLEditor::DidJoinNodesTransaction( const JoinNodesTransaction& aTransaction, nsresult aDoJoinNodesResult) { // This shouldn't occur unless the cycle collector runs by chrome script // forcibly. if (MOZ_UNLIKELY(NS_WARN_IF(!aTransaction.GetRemovedContent()) || NS_WARN_IF(!aTransaction.GetExistingContent()))) { return; } // If joined node is moved to different place, offset may not have any // meaning. In this case, the web app modified the DOM tree takes on the // responsibility for the remaning things. if (MOZ_UNLIKELY(aTransaction.GetExistingContent()->GetParentNode() != aTransaction.GetParentNode())) { return; } // Be aware, the joined point should be created for each call because // they may refer the child node, but some of them may change the DOM tree // after that, thus we need to avoid invalid point (Although it shouldn't // occur). TopLevelEditSubActionDataRef().DidJoinContents( *this, aTransaction.CreateJoinedPoint()); if (NS_SUCCEEDED(aDoJoinNodesResult)) { if (RefPtr textServicesDocument = mTextServicesDocument) { textServicesDocument->DidJoinContents( aTransaction.CreateJoinedPoint(), *aTransaction.GetRemovedContent()); } } if (!mActionListeners.IsEmpty()) { for (auto& listener : mActionListeners.Clone()) { DebugOnly rvIgnored = listener->DidJoinContents( aTransaction.CreateJoinedPoint(), aTransaction.GetRemovedContent()); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsIEditActionListener::DidJoinContents() failed, but ignored"); } } } nsresult HTMLEditor::DoJoinNodes(nsIContent& aContentToKeep, nsIContent& aContentToRemove) { MOZ_ASSERT(IsEditActionDataAvailable()); const uint32_t keepingContentLength = aContentToKeep.Length(); const EditorDOMPoint oldPointAtRightContent(&aContentToRemove); if (MOZ_LIKELY(oldPointAtRightContent.IsSet())) { Unused << oldPointAtRightContent.Offset(); // Fix the offset } // Remember all selection points. // XXX Do we need to restore all types of selections by ourselves? Normal // selection should be modified later as result of handling edit action. // IME selections shouldn't be there when nodes are joined. Spellcheck // selections should be recreated with newer text. URL selections // shouldn't be there because of used only by the URL bar. AutoTArray savedRanges; { EditorRawDOMPoint atRemovingNode(&aContentToRemove); EditorRawDOMPoint atNodeToKeep(&aContentToKeep); for (SelectionType selectionType : kPresentSelectionTypes) { SavedRange savingRange; savingRange.mSelection = GetSelection(selectionType); if (selectionType == SelectionType::eNormal) { if (NS_WARN_IF(!savingRange.mSelection)) { return NS_ERROR_FAILURE; } } else if (!savingRange.mSelection) { // For non-normal selections, skip over the non-existing ones. continue; } const uint32_t rangeCount = savingRange.mSelection->RangeCount(); for (const uint32_t j : IntegerRange(rangeCount)) { MOZ_ASSERT(savingRange.mSelection->RangeCount() == rangeCount); const RefPtr r = savingRange.mSelection->GetRangeAt(j); MOZ_ASSERT(r); MOZ_ASSERT(r->IsPositioned()); savingRange.mStartContainer = r->GetStartContainer(); savingRange.mStartOffset = r->StartOffset(); savingRange.mEndContainer = r->GetEndContainer(); savingRange.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 (savingRange.mStartContainer) { MOZ_ASSERT(savingRange.mEndContainer); auto AdjustDOMPoint = [&](nsCOMPtr& aContainer, uint32_t& aOffset) { // If range boundary points aContentToRemove and aContentToKeep is // its left node, remember it as being at end of aContentToKeep. // Then, it will point start of the first content of moved content // from aContentToRemove. if (aContainer == atRemovingNode.GetContainer() && atNodeToKeep.Offset() < aOffset && aOffset <= atRemovingNode.Offset()) { aContainer = &aContentToKeep; aOffset = keepingContentLength; } }; AdjustDOMPoint(savingRange.mStartContainer, savingRange.mStartOffset); AdjustDOMPoint(savingRange.mEndContainer, savingRange.mEndOffset); } savedRanges.AppendElement(savingRange); } } } // OK, ready to do join now. nsresult rv = [&]() MOZ_NEVER_INLINE_DEBUG MOZ_CAN_RUN_SCRIPT { // If it's a text node, just shuffle around some text. if (aContentToKeep.IsText() && aContentToRemove.IsText()) { nsAutoString rightText; aContentToRemove.AsText()->GetData(rightText); // Delete the node first to minimize the text change range from // IMEContentObserver of view. aContentToRemove.Remove(); // Even if we've already destroyed, let's update aContentToKeep for // avoiding a dataloss bug. IgnoredErrorResult ignoredError; DoInsertText(MOZ_KnownLive(*aContentToKeep.AsText()), aContentToKeep.AsText()->TextDataLength(), rightText, ignoredError); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(!ignoredError.Failed(), "EditorBase::DoSetText() failed, but ignored"); return NS_OK; } // Otherwise it's an interior node, so shuffle around the children. AutoTArray, 64> arrayOfChildContents; HTMLEditUtils::CollectAllChildren(aContentToRemove, arrayOfChildContents); // Delete the node first to minimize the text change range from // IMEContentObserver of view. aContentToRemove.Remove(); // Even if we've already destroyed, let's update aContentToKeep for avoiding // a dataloss bug. nsresult rv = NS_OK; for (const OwningNonNull& child : arrayOfChildContents) { IgnoredErrorResult error; aContentToKeep.AppendChild(child, error); if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("nsINode::AppendChild() failed"); rv = error.StealNSResult(); } } if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } return rv; }(); if (MOZ_LIKELY(oldPointAtRightContent.IsSet())) { DebugOnly rvIgnored = RangeUpdaterRef().SelAdjJoinNodes( EditorRawDOMPoint(&aContentToKeep, std::min(keepingContentLength, aContentToKeep.Length())), aContentToRemove, oldPointAtRightContent); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "RangeUpdater::SelAdjJoinNodes() failed, but ignored"); } if (MOZ_UNLIKELY(NS_FAILED(rv))) { return rv; } const bool allowedTransactionsToChangeSelection = AllowsTransactionsToChangeSelection(); // And adjust the selection if needed. RefPtr previousSelection; for (SavedRange& savedRange : savedRanges) { // If we have not seen the selection yet, clear all of its ranges. if (savedRange.mSelection != previousSelection) { IgnoredErrorResult error; MOZ_KnownLive(savedRange.mSelection)->RemoveAllRanges(error); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (error.Failed()) { NS_WARNING("Selection::RemoveAllRanges() failed"); return error.StealNSResult(); } previousSelection = savedRange.mSelection; } if (allowedTransactionsToChangeSelection && savedRange.mSelection->Type() == SelectionType::eNormal) { // If the editor should adjust the selection, don't bother restoring // the ranges for the normal selection here. continue; } auto AdjustDOMPoint = [&](nsCOMPtr& aContainer, uint32_t& aOffset) { // Now, all content of aContentToRemove are moved to end of // aContentToKeep. Therefore, if a range boundary was in // aContentToRemove, we need to change the container to aContentToKeep and // adjust the offset to after the original content of aContentToKeep. if (aContainer == &aContentToRemove) { aContainer = &aContentToKeep; aOffset += keepingContentLength; } }; AdjustDOMPoint(savedRange.mStartContainer, savedRange.mStartOffset); AdjustDOMPoint(savedRange.mEndContainer, savedRange.mEndOffset); const RefPtr newRange = nsRange::Create( savedRange.mStartContainer, savedRange.mStartOffset, savedRange.mEndContainer, savedRange.mEndOffset, IgnoreErrors()); if (!newRange) { NS_WARNING("nsRange::Create() failed"); return NS_ERROR_FAILURE; } IgnoredErrorResult error; // The `MOZ_KnownLive` annotation is only necessary because of a bug // (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253) in the // static analyzer. MOZ_KnownLive(savedRange.mSelection) ->AddRangeAndSelectFramesAndNotifyListeners(*newRange, error); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_WARN_IF(error.Failed())) { return error.StealNSResult(); } } if (allowedTransactionsToChangeSelection) { // Editor wants us to set selection at join point. DebugOnly rvIgnored = CollapseSelectionToStartOf(aContentToKeep); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionTo() caused destroying the editor"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBases::CollapseSelectionTos() failed, but ignored"); } return NS_OK; } Result HTMLEditor::MoveNodeWithTransaction( nsIContent& aContentToMove, const EditorDOMPoint& aPointToInsert) { MOZ_ASSERT(aPointToInsert.IsSetAndValid()); EditorDOMPoint oldPoint(&aContentToMove); if (NS_WARN_IF(!oldPoint.IsSet())) { return Err(NS_ERROR_FAILURE); } // Don't do anything if it's already in right place. if (aPointToInsert == oldPoint) { return MoveNodeResult::IgnoredResult(aPointToInsert.NextPoint()); } RefPtr moveNodeTransaction = MoveNodeTransaction::MaybeCreate(*this, aContentToMove, aPointToInsert); if (MOZ_UNLIKELY(!moveNodeTransaction)) { NS_WARNING("MoveNodeTransaction::MaybeCreate() failed"); return Err(NS_ERROR_FAILURE); } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eMoveNode, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); TopLevelEditSubActionDataRef().WillDeleteContent(*this, aContentToMove); nsresult rv = DoTransactionInternal(moveNodeTransaction); if (NS_SUCCEEDED(rv)) { if (mTextServicesDocument) { const OwningNonNull textServicesDocument = *mTextServicesDocument; textServicesDocument->DidDeleteContent(aContentToMove); } } if (!mActionListeners.IsEmpty()) { for (auto& listener : mActionListeners.Clone()) { DebugOnly rvIgnored = listener->DidDeleteNode(&aContentToMove, rv); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsIEditActionListener::DidDeleteNode() failed, but ignored"); } } if (MOZ_UNLIKELY(Destroyed())) { NS_WARNING( "MoveNodeTransaction::DoTransaction() caused destroying the editor"); return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("MoveNodeTransaction::DoTransaction() failed"); return Err(rv); } TopLevelEditSubActionDataRef().DidInsertContent(*this, aContentToMove); return MoveNodeResult::HandledResult( moveNodeTransaction->SuggestNextInsertionPoint(), moveNodeTransaction->SuggestPointToPutCaret()); } Result, nsresult> HTMLEditor::DeleteSelectionAndCreateElement( nsAtom& aTag, const InitializeInsertingElement& aInitializer) { MOZ_ASSERT(IsEditActionDataAvailable()); nsresult rv = DeleteSelectionAndPrepareToCreateNode(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteSelectionAndPrepareToCreateNode() failed"); return Err(rv); } EditorDOMPoint pointToInsert(SelectionRef().AnchorRef()); if (!pointToInsert.IsSet()) { return Err(NS_ERROR_FAILURE); } Result createNewElementResult = CreateAndInsertElement(WithTransaction::Yes, aTag, pointToInsert, aInitializer); if (MOZ_UNLIKELY(createNewElementResult.isErr())) { NS_WARNING( "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed"); return createNewElementResult.propagateErr(); } MOZ_ASSERT(createNewElementResult.inspect().GetNewNode()); // We want the selection to be just after the new node createNewElementResult.inspect().IgnoreCaretPointSuggestion(); rv = CollapseSelectionTo( EditorRawDOMPoint::After(*createNewElementResult.inspect().GetNewNode())); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } return createNewElementResult.unwrap().UnwrapNewNode(); } nsresult HTMLEditor::DeleteSelectionAndPrepareToCreateNode() { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!SelectionRef().GetAnchorFocusRange())) { return NS_OK; } if (!SelectionRef().GetAnchorFocusRange()->Collapsed()) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteSelectionAsSubAction() failed"); return rv; } MOZ_ASSERT(SelectionRef().GetAnchorFocusRange() && SelectionRef().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 EditorDOMPoint atAnchor(SelectionRef().AnchorRef()); if (NS_WARN_IF(!atAnchor.IsSet()) || !atAnchor.IsInDataNode()) { return NS_OK; } if (NS_WARN_IF(!atAnchor.GetContainerParent())) { return NS_ERROR_FAILURE; } if (atAnchor.IsStartOfContainer()) { const EditorRawDOMPoint atAnchorContainer(atAnchor.GetContainer()); if (NS_WARN_IF(!atAnchorContainer.IsSetAndValid())) { return NS_ERROR_FAILURE; } nsresult rv = CollapseSelectionTo(atAnchorContainer); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } if (atAnchor.IsEndOfContainer()) { EditorRawDOMPoint afterAnchorContainer(atAnchor.GetContainer()); if (NS_WARN_IF(!afterAnchorContainer.AdvanceOffset())) { return NS_ERROR_FAILURE; } nsresult rv = CollapseSelectionTo(afterAnchorContainer); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } Result splitAtAnchorResult = SplitNodeWithTransaction(atAnchor); if (MOZ_UNLIKELY(splitAtAnchorResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitAtAnchorResult.unwrapErr(); } splitAtAnchorResult.inspect().IgnoreCaretPointSuggestion(); const auto atRightContent = splitAtAnchorResult.inspect().AtNextContent(); if (NS_WARN_IF(!atRightContent.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atRightContent.IsSetAndValid()); nsresult rv = CollapseSelectionTo(atRightContent); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } bool HTMLEditor::IsEmpty() const { if (mPaddingBRElementForEmptyEditor) { return true; } const Element* activeElement = GetDocument() ? GetDocument()->GetActiveElement() : nullptr; const Element* editingHostOrBodyOrRootElement = activeElement && activeElement->IsEditable() ? ComputeEditingHost(*activeElement, LimitInBodyElement::No) : ComputeEditingHost(LimitInBodyElement::No); if (MOZ_UNLIKELY(!editingHostOrBodyOrRootElement)) { // If there is no active element nor no selection range in the document, // let's check entire the document as what we do traditionally. editingHostOrBodyOrRootElement = GetRoot(); if (!editingHostOrBodyOrRootElement) { return true; } } for (nsIContent* childContent = editingHostOrBodyOrRootElement->GetFirstChild(); childContent; childContent = childContent->GetNextSibling()) { if (!childContent->IsText() || childContent->Length()) { return false; } } return true; } // add to aElement the CSS inline styles corresponding to the HTML attribute // aAttribute with its value aValue nsresult HTMLEditor::SetAttributeOrEquivalent(Element* aElement, nsAtom* aAttribute, const nsAString& aValue, bool aSuppressTransaction) { MOZ_ASSERT(aElement); MOZ_ASSERT(aAttribute); nsAutoScriptBlocker scriptBlocker; nsStyledElement* styledElement = nsStyledElement::FromNodeOrNull(aElement); if (!IsCSSEnabled()) { // we are not in an HTML+CSS editor; let's set the attribute the HTML way if (EditorElementStyle::IsHTMLStyle(aAttribute)) { const EditorElementStyle elementStyle = EditorElementStyle::Create(*aAttribute); if (styledElement && elementStyle.IsCSSRemovable(*styledElement)) { // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must // be guaranteed by the caller because of MOZ_CAN_RUN_SCRIPT method. nsresult rv = CSSEditUtils::RemoveCSSEquivalentToStyle( aSuppressTransaction ? WithTransaction::No : WithTransaction::Yes, *this, MOZ_KnownLive(*styledElement), elementStyle, nullptr); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "CSSEditUtils::RemoveCSSEquivalentToStyle() failed, but ignored"); } } if (aSuppressTransaction) { nsresult rv = aElement->SetAttr(kNameSpaceID_None, aAttribute, aValue, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed"); return rv; } nsresult rv = SetAttributeWithTransaction(*aElement, *aAttribute, aValue); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction() failed"); return rv; } if (EditorElementStyle::IsHTMLStyle(aAttribute)) { const EditorElementStyle elementStyle = EditorElementStyle::Create(*aAttribute); if (styledElement && elementStyle.IsCSSSettable(*styledElement)) { // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must // be guaranteed by the caller because of MOZ_CAN_RUN_SCRIPT method. Result count = CSSEditUtils::SetCSSEquivalentToStyle( aSuppressTransaction ? WithTransaction::No : WithTransaction::Yes, *this, MOZ_KnownLive(*styledElement), elementStyle, &aValue); if (MOZ_UNLIKELY(count.isErr())) { if (NS_WARN_IF(count.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle() failed, but ignored"); } if (count.inspect()) { // we found an equivalence ; let's remove the HTML attribute itself if // it is set nsAutoString existingValue; if (!aElement->GetAttr(aAttribute, existingValue)) { return NS_OK; } if (aSuppressTransaction) { nsresult rv = aElement->UnsetAttr(kNameSpaceID_None, aAttribute, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::UnsetAttr() failed"); return rv; } nsresult rv = RemoveAttributeWithTransaction(*aElement, *aAttribute); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::RemoveAttributeWithTransaction() failed"); return rv; } } } // count is an integer that represents the number of CSS declarations // applied to the element. If it is zero, we found no equivalence in this // implementation for the attribute if (aAttribute == nsGkAtoms::style) { // if it is the style attribute, just add the new value to the existing // style attribute's value nsString existingValue; // Use nsString to avoid copying the string // buffer at setting the attribute below. aElement->GetAttr(nsGkAtoms::style, existingValue); if (!existingValue.IsEmpty()) { existingValue.Append(HTMLEditUtils::kSpace); } existingValue.Append(aValue); if (aSuppressTransaction) { nsresult rv = aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, existingValue, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr(nsGkAtoms::style) failed"); return rv; } nsresult rv = SetAttributeWithTransaction(*aElement, *nsGkAtoms::style, existingValue); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction(nsGkAtoms::style) failed"); return rv; } // we have no CSS equivalence for this attribute and it is not the style // attribute; let's set it the good'n'old HTML way if (aSuppressTransaction) { nsresult rv = aElement->SetAttr(kNameSpaceID_None, aAttribute, aValue, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed"); return rv; } nsresult rv = SetAttributeWithTransaction(*aElement, *aAttribute, aValue); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction() failed"); return rv; } nsresult HTMLEditor::RemoveAttributeOrEquivalent(Element* aElement, nsAtom* aAttribute, bool aSuppressTransaction) { MOZ_ASSERT(aElement); MOZ_ASSERT(aAttribute); if (IsCSSEnabled() && EditorElementStyle::IsHTMLStyle(aAttribute)) { const EditorElementStyle elementStyle = EditorElementStyle::Create(*aAttribute); if (elementStyle.IsCSSRemovable(*aElement)) { // XXX It might be keep handling attribute even if aElement is not // an nsStyledElement instance. nsStyledElement* styledElement = nsStyledElement::FromNodeOrNull(aElement); if (NS_WARN_IF(!styledElement)) { return NS_ERROR_INVALID_ARG; } // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must // be guaranteed by the caller because of MOZ_CAN_RUN_SCRIPT method. nsresult rv = CSSEditUtils::RemoveCSSEquivalentToStyle( aSuppressTransaction ? WithTransaction::No : WithTransaction::Yes, *this, MOZ_KnownLive(*styledElement), elementStyle, nullptr); if (NS_FAILED(rv)) { NS_WARNING("CSSEditUtils::RemoveCSSEquivalentToStyle() failed"); return rv; } } } if (!aElement->HasAttr(aAttribute)) { return NS_OK; } if (aSuppressTransaction) { nsresult rv = aElement->UnsetAttr(kNameSpaceID_None, aAttribute, /* aNotify = */ true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::UnsetAttr() failed"); return rv; } nsresult rv = RemoveAttributeWithTransaction(*aElement, *aAttribute); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RemoveAttributeWithTransaction() failed"); return rv; } NS_IMETHODIMP HTMLEditor::SetIsCSSEnabled(bool aIsCSSPrefChecked) { AutoEditActionDataSetter editActionData(*this, EditAction::eEnableOrDisableCSS); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } mIsCSSPrefChecked = aIsCSSPrefChecked; return NS_OK; } // Set the block background color nsresult HTMLEditor::SetBlockBackgroundColorWithCSSAsSubAction( const nsAString& aColor) { MOZ_ASSERT(IsEditActionDataAvailable()); // background-color change and committing composition should be undone // together AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); CommitComposition(); // XXX Shouldn't we do this before calling `CommitComposition()`? if (IsPlaintextMailComposer()) { return NS_OK; } { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertElement, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION(!ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() " "failed, but ignored"); // TODO: We don't need AutoTransactionsConserveSelection here in the normal // cases, but removing this may cause the behavior with the legacy // mutation event listeners. We should try to delete this in a bug. AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoClonedSelectionRangeArray selectionRanges(SelectionRef()); MOZ_ALWAYS_TRUE(selectionRanges.SaveAndTrackRanges(*this)); for (const OwningNonNull& domRange : selectionRanges.Ranges()) { EditorDOMRange range(domRange); if (NS_WARN_IF(!range.IsPositioned())) { continue; } if (range.InSameContainer()) { // If the range is in a text node, set background color of its parent // block. if (range.StartRef().IsInTextNode()) { const RefPtr editableBlockStyledElement = nsStyledElement::FromNodeOrNull(HTMLEditUtils::GetAncestorElement( *range.StartRef().ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle)); if (!editableBlockStyledElement || !EditorElementStyle::BGColor().IsCSSSettable( *editableBlockStyledElement)) { continue; } Result result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::Yes, *this, *editableBlockStyledElement, EditorElementStyle::BGColor(), &aColor); if (MOZ_UNLIKELY(result.isErr())) { if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::" "BGColor()) failed, but ignored"); } continue; } // If `Selection` is collapsed in a `` element, set background // color of the `` element. if (range.Collapsed() && range.StartRef().IsContainerHTMLElement(nsGkAtoms::body)) { const RefPtr styledElement = range.StartRef().GetContainerAs(); if (!styledElement || !EditorElementStyle::BGColor().IsCSSSettable(*styledElement)) { continue; } Result result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::Yes, *this, *styledElement, EditorElementStyle::BGColor(), &aColor); if (MOZ_UNLIKELY(result.isErr())) { if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::" "BGColor()) failed, but ignored"); } continue; } // If one node is selected, set background color of it if it's a // block, or of its parent block otherwise. if ((range.StartRef().IsStartOfContainer() && range.EndRef().IsStartOfContainer()) || range.StartRef().Offset() + 1 == range.EndRef().Offset()) { if (NS_WARN_IF(range.StartRef().IsInDataNode())) { continue; } const RefPtr editableBlockStyledElement = nsStyledElement::FromNodeOrNull( HTMLEditUtils::GetInclusiveAncestorElement( *range.StartRef().GetChild(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle)); if (!editableBlockStyledElement || !EditorElementStyle::BGColor().IsCSSSettable( *editableBlockStyledElement)) { continue; } Result result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::Yes, *this, *editableBlockStyledElement, EditorElementStyle::BGColor(), &aColor); if (MOZ_UNLIKELY(result.isErr())) { if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::" "BGColor()) failed, but ignored"); } continue; } } // if (range.InSameContainer()) // Collect editable nodes which are entirely contained in the range. AutoTArray, 64> arrayOfContents; { ContentSubtreeIterator subtreeIter; // If there is no node which is entirely in the range, // `ContentSubtreeIterator::Init()` fails, but this is possible case, // don't warn it. nsresult rv = subtreeIter.Init(range.StartRef().ToRawRangeBoundary(), range.EndRef().ToRawRangeBoundary()); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "ContentSubtreeIterator::Init() failed, but ignored"); if (NS_SUCCEEDED(rv)) { for (; !subtreeIter.IsDone(); subtreeIter.Next()) { nsINode* node = subtreeIter.GetCurrentNode(); if (NS_WARN_IF(!node)) { return NS_ERROR_FAILURE; } if (node->IsContent() && EditorUtils::IsEditableContent( *node->AsContent(), EditorType::HTML)) { arrayOfContents.AppendElement(*node->AsContent()); } } } } // This caches block parent if we set its background color. RefPtr handledBlockParent; // If start node is a text node, set background color of its parent // block. if (range.StartRef().IsInTextNode() && EditorUtils::IsEditableContent(*range.StartRef().ContainerAs(), EditorType::HTML)) { Element* const editableBlockElement = HTMLEditUtils::GetAncestorElement( *range.StartRef().ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (editableBlockElement && handledBlockParent != editableBlockElement) { handledBlockParent = editableBlockElement; nsStyledElement* const blockStyledElement = nsStyledElement::FromNode(handledBlockParent); if (blockStyledElement && EditorElementStyle::BGColor().IsCSSSettable(*blockStyledElement)) { // MOZ_KnownLive(*blockStyledElement): It's handledBlockParent // whose type is RefPtr. Result result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::Yes, *this, MOZ_KnownLive(*blockStyledElement), EditorElementStyle::BGColor(), &aColor); if (MOZ_UNLIKELY(result.isErr())) { if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::" "BGColor()) failed, but ignored"); } } } } // Then, set background color of each block or block parent of all nodes // in the range entirely. for (OwningNonNull& content : arrayOfContents) { Element* const editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( content, HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (editableBlockElement && handledBlockParent != editableBlockElement) { handledBlockParent = editableBlockElement; nsStyledElement* const blockStyledElement = nsStyledElement::FromNode(handledBlockParent); if (blockStyledElement && EditorElementStyle::BGColor().IsCSSSettable(*blockStyledElement)) { // MOZ_KnownLive(*blockStyledElement): It's handledBlockParent whose // type is RefPtr. Result result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::Yes, *this, MOZ_KnownLive(*blockStyledElement), EditorElementStyle::BGColor(), &aColor); if (MOZ_UNLIKELY(result.isErr())) { if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::" "BGColor()) failed, but ignored"); } } } } // Finally, if end node is a text node, set background color of its // parent block. if (range.EndRef().IsInTextNode() && EditorUtils::IsEditableContent(*range.EndRef().ContainerAs(), EditorType::HTML)) { Element* const editableBlockElement = HTMLEditUtils::GetAncestorElement( *range.EndRef().ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (editableBlockElement && handledBlockParent != editableBlockElement) { const RefPtr blockStyledElement = nsStyledElement::FromNode(editableBlockElement); if (blockStyledElement && EditorElementStyle::BGColor().IsCSSSettable(*blockStyledElement)) { Result result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::Yes, *this, *blockStyledElement, EditorElementStyle::BGColor(), &aColor); if (MOZ_UNLIKELY(result.isErr())) { if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::" "BGColor()) failed, but ignored"); } } } } } // for-loop of selectionRanges MOZ_ASSERT(selectionRanges.HasSavedRanges()); selectionRanges.RestoreFromSavedRanges(); nsresult rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoClonedSelectionRangeArray::ApplyTo() failed"); return rv; } NS_IMETHODIMP HTMLEditor::SetBackgroundColor(const nsAString& aColor) { nsresult rv = SetBackgroundColorAsAction(aColor); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::SetBackgroundColorAsAction() failed"); return rv; } nsresult HTMLEditor::SetBackgroundColorAsAction(const nsAString& aColor, nsIPrincipal* aPrincipal) { AutoEditActionDataSetter editActionData( *this, EditAction::eSetBackgroundColor, aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); return EditorBase::ToGenericNSResult(rv); } if (IsCSSEnabled()) { // if we are in CSS mode, we have to apply the background color to the // containing block (or the body if we have no block-level element in // the document) nsresult rv = SetBlockBackgroundColorWithCSSAsSubAction(aColor); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::SetBlockBackgroundColorWithCSSAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } // but in HTML mode, we can only set the document's background color rv = SetHTMLBackgroundColorWithTransaction(aColor); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::SetHTMLBackgroundColorWithTransaction() failed"); return EditorBase::ToGenericNSResult(rv); } Result HTMLEditor::CopyLastEditableChildStylesWithTransaction( Element& aPreviousBlock, Element& aNewBlock, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); // First, clear out aNewBlock. Contract is that we want only the styles // from aPreviousBlock. AutoTArray, 32> newBlockChildren; HTMLEditUtils::CollectAllChildren(aNewBlock, newBlockChildren); for (const OwningNonNull& child : newBlockChildren) { // MOZ_KNownLive(child) because of bug 1622253 nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(child)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } if (MOZ_UNLIKELY(aNewBlock.GetFirstChild())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // XXX aNewBlock may be moved or removed. Even in such case, we should // keep cloning the styles? // Look for the deepest last editable leaf node in aPreviousBlock. // Then, if found one is a
    element, look for non-
    element. nsIContent* deepestEditableContent = nullptr; for (nsCOMPtr child = &aPreviousBlock; child; child = HTMLEditUtils::GetLastChild( *child, {WalkTreeOption::IgnoreNonEditableNode})) { deepestEditableContent = child; } while (deepestEditableContent && deepestEditableContent->IsHTMLElement(nsGkAtoms::br)) { deepestEditableContent = HTMLEditUtils::GetPreviousContent( *deepestEditableContent, {WalkTreeOption::IgnoreNonEditableNode}, BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost); } if (!deepestEditableContent) { return EditorDOMPoint(&aNewBlock, 0u); } Element* deepestVisibleEditableElement = deepestEditableContent->GetAsElementOrParentElement(); if (!deepestVisibleEditableElement) { return EditorDOMPoint(&aNewBlock, 0u); } // Clone inline elements to keep current style in the new block. // XXX Looks like that this is really slow if lastEditableDescendant is // far from aPreviousBlock. Probably, we should clone inline containers // from ancestor to descendants without transactions, then, insert it // after that with transaction. RefPtr lastClonedElement, firstClonedElement; for (RefPtr elementInPreviousBlock = deepestVisibleEditableElement; elementInPreviousBlock && elementInPreviousBlock != &aPreviousBlock; elementInPreviousBlock = elementInPreviousBlock->GetParentElement()) { if (!HTMLEditUtils::IsInlineStyle(elementInPreviousBlock) && !elementInPreviousBlock->IsHTMLElement(nsGkAtoms::span)) { continue; } OwningNonNull tagName = *elementInPreviousBlock->NodeInfo()->NameAtom(); // At first time, just create the most descendant inline container // element. if (!firstClonedElement) { Result createNewElementResult = CreateAndInsertElement( WithTransaction::Yes, tagName, EditorDOMPoint(&aNewBlock, 0u), // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 [&elementInPreviousBlock]( HTMLEditor& aHTMLEditor, Element& aNewElement, const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY { // Clone all attributes. Note that despite the method name, // CloneAttributesWithTransaction does not create // transactions in this case because aNewElement has not // been connected yet. // XXX Looks like that this clones id attribute too. aHTMLEditor.CloneAttributesWithTransaction( aNewElement, *elementInPreviousBlock); return NS_OK; }); if (MOZ_UNLIKELY(createNewElementResult.isErr())) { NS_WARNING( "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed"); return createNewElementResult.propagateErr(); } CreateElementResult unwrappedCreateNewElementResult = createNewElementResult.unwrap(); // We'll return with a point suggesting new caret position and the // following path does not require an update of selection here. // Therefore, we don't need to update selection here. unwrappedCreateNewElementResult.IgnoreCaretPointSuggestion(); firstClonedElement = lastClonedElement = unwrappedCreateNewElementResult.UnwrapNewNode(); continue; } // Otherwise, inserts new parent inline container to the previous inserted // inline container. Result wrapClonedElementResult = InsertContainerWithTransaction(*lastClonedElement, tagName); if (MOZ_UNLIKELY(wrapClonedElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return wrapClonedElementResult.propagateErr(); } CreateElementResult unwrappedWrapClonedElementResult = wrapClonedElementResult.unwrap(); // We'll return with a point suggesting new caret so that we don't need to // update selection here. unwrappedWrapClonedElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedWrapClonedElementResult.GetNewNode()); lastClonedElement = unwrappedWrapClonedElementResult.UnwrapNewNode(); CloneAttributesWithTransaction(*lastClonedElement, *elementInPreviousBlock); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } } if (!firstClonedElement) { // XXX Even if no inline elements are cloned, shouldn't we create new //
    element for aNewBlock? return EditorDOMPoint(&aNewBlock, 0u); } Result insertBRElementResultOrError = InsertLineBreak(WithTransaction::Yes, LineBreakType::BRElement, EditorDOMPoint(firstClonedElement, 0u)); if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "LineBreakType::BRElement) failed"); return insertBRElementResultOrError.propagateErr(); } CreateLineBreakResult insertBRElementResult = insertBRElementResultOrError.unwrap(); MOZ_ASSERT(insertBRElementResult.Handled()); insertBRElementResult.IgnoreCaretPointSuggestion(); return insertBRElementResult.AtLineBreak(); } nsresult HTMLEditor::GetElementOrigin(Element& aElement, int32_t& aX, int32_t& aY) { aX = 0; aY = 0; if (NS_WARN_IF(!IsInitialized())) { return NS_ERROR_NOT_INITIALIZED; } PresShell* presShell = GetPresShell(); if (NS_WARN_IF(!presShell)) { return NS_ERROR_NOT_INITIALIZED; } nsIFrame* frame = aElement.GetPrimaryFrame(); if (NS_WARN_IF(!frame)) { return NS_OK; } nsIFrame* absoluteContainerBlockFrame = presShell->GetAbsoluteContainingBlock(frame); if (NS_WARN_IF(!absoluteContainerBlockFrame)) { return NS_OK; } nsPoint off = frame->GetOffsetTo(absoluteContainerBlockFrame); aX = nsPresContext::AppUnitsToIntCSSPixels(off.x); aY = nsPresContext::AppUnitsToIntCSSPixels(off.y); return NS_OK; } Element* HTMLEditor::GetSelectionContainerElement() const { MOZ_ASSERT(IsEditActionDataAvailable()); nsINode* focusNode = nullptr; if (SelectionRef().IsCollapsed()) { focusNode = SelectionRef().GetFocusNode(); if (NS_WARN_IF(!focusNode)) { return nullptr; } } else { const uint32_t rangeCount = SelectionRef().RangeCount(); MOZ_ASSERT(rangeCount, "If 0, Selection::IsCollapsed() should return true"); if (rangeCount == 1) { const nsRange* range = SelectionRef().GetRangeAt(0); const RangeBoundary& startRef = range->StartRef(); const RangeBoundary& endRef = range->EndRef(); // This method called GetSelectedElement() to retrieve proper container // when only one node is selected. However, it simply returns start // node of Selection with additional cost. So, we do not need to call // it anymore. if (startRef.GetContainer()->IsElement() && startRef.GetContainer() == endRef.GetContainer() && startRef.GetChildAtOffset() && startRef.GetChildAtOffset()->GetNextSibling() == endRef.GetChildAtOffset()) { focusNode = startRef.GetChildAtOffset(); MOZ_ASSERT(focusNode, "Start container must not be nullptr"); } else { focusNode = range->GetClosestCommonInclusiveAncestor(); if (!focusNode) { NS_WARNING( "AbstractRange::GetClosestCommonInclusiveAncestor() returned " "nullptr"); return nullptr; } } } else { for (const uint32_t i : IntegerRange(rangeCount)) { MOZ_ASSERT(SelectionRef().RangeCount() == rangeCount); const nsRange* range = SelectionRef().GetRangeAt(i); MOZ_ASSERT(range); nsINode* startContainer = range->GetStartContainer(); if (!focusNode) { focusNode = startContainer; } else if (focusNode != startContainer) { // XXX Looks odd to use parent of startContainer because previous // range may not be in the parent node of current // startContainer. focusNode = startContainer->GetParentNode(); // XXX Looks odd to break the for-loop here because we refer only // first range and another range which starts from different // container, and the latter range is preferred. Why? break; } } if (!focusNode) { NS_WARNING("Focused node of selection was not found"); return nullptr; } } } if (focusNode->IsText()) { focusNode = focusNode->GetParentNode(); if (NS_WARN_IF(!focusNode)) { return nullptr; } } if (NS_WARN_IF(!focusNode->IsElement())) { return nullptr; } return focusNode->AsElement(); } NS_IMETHODIMP HTMLEditor::IsAnonymousElement(Element* aElement, bool* aReturn) { if (NS_WARN_IF(!aElement)) { return NS_ERROR_INVALID_ARG; } *aReturn = aElement->IsRootOfNativeAnonymousSubtree(); return NS_OK; } nsresult HTMLEditor::SetReturnInParagraphCreatesNewParagraph( bool aCreatesNewParagraph) { mCRInParagraphCreatesParagraph = aCreatesNewParagraph; return NS_OK; } bool HTMLEditor::GetReturnInParagraphCreatesNewParagraph() const { return mCRInParagraphCreatesParagraph; } nsresult HTMLEditor::GetReturnInParagraphCreatesNewParagraph( bool* aCreatesNewParagraph) { *aCreatesNewParagraph = mCRInParagraphCreatesParagraph; return NS_OK; } NS_IMETHODIMP HTMLEditor::GetWrapWidth(int32_t* aWrapColumn) { if (NS_WARN_IF(!aWrapColumn)) { return NS_ERROR_INVALID_ARG; } *aWrapColumn = WrapWidth(); return NS_OK; } // // See if the style value includes this attribute, and if it does, // cut out everything from the attribute to the next semicolon. // static void CutStyle(const char* stylename, nsString& styleValue) { // Find the current wrapping type: int32_t styleStart = styleValue.LowerCaseFindASCII(stylename); if (styleStart >= 0) { int32_t styleEnd = styleValue.Find(u";", styleStart); if (styleEnd > styleStart) { styleValue.Cut(styleStart, styleEnd - styleStart + 1); } else { styleValue.Cut(styleStart, styleValue.Length() - styleStart); } } } NS_IMETHODIMP HTMLEditor::SetWrapWidth(int32_t aWrapColumn) { AutoEditActionDataSetter editActionData(*this, EditAction::eSetWrapWidth); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } mWrapColumn = aWrapColumn; // Make sure we're a plaintext editor, otherwise we shouldn't // do the rest of this. if (!IsPlaintextMailComposer()) { return NS_OK; } // Ought to set a style sheet here... RefPtr rootElement = GetRoot(); if (NS_WARN_IF(!rootElement)) { return NS_ERROR_NOT_INITIALIZED; } // Get the current style for this root element: nsAutoString styleValue; rootElement->GetAttr(nsGkAtoms::style, styleValue); // We'll replace styles for these values: CutStyle("white-space", styleValue); CutStyle("width", styleValue); CutStyle("font-family", styleValue); // If we have other style left, trim off any existing semicolons // or white-space, then add a known semicolon-space: if (!styleValue.IsEmpty()) { styleValue.Trim("; \t", false, true); styleValue.AppendLiteral("; "); } // Make sure we have fixed-width font. This should be done for us, // but it isn't, see bug 22502, so we have to add "font: -moz-fixed;". // Only do this if we're wrapping. if (IsWrapHackEnabled() && aWrapColumn >= 0) { styleValue.AppendLiteral("font-family: -moz-fixed; "); } // and now we're ready to set the new white-space/wrapping style. if (aWrapColumn > 0) { // Wrap to a fixed column. styleValue.AppendLiteral("white-space: pre-wrap; width: "); styleValue.AppendInt(aWrapColumn); styleValue.AppendLiteral("ch;"); } else if (!aWrapColumn) { styleValue.AppendLiteral("white-space: pre-wrap;"); } else { styleValue.AppendLiteral("white-space: pre;"); } nsresult rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr(nsGkAtoms::style) failed"); return rv; } Element* HTMLEditor::GetFocusedElement() const { Element* const focusedElement = nsFocusManager::GetFocusedElementStatic(); Document* document = GetDocument(); if (NS_WARN_IF(!document)) { return nullptr; } const bool inDesignMode = focusedElement ? focusedElement->IsInDesignMode() : document->IsInDesignMode(); if (!focusedElement) { // in designMode, nobody gets focus in most cases. if (inDesignMode && OurWindowHasFocus()) { return document->GetRootElement(); } return nullptr; } if (inDesignMode) { return OurWindowHasFocus() && focusedElement->IsInclusiveDescendantOf(document) ? focusedElement : nullptr; } // We're HTML editor for contenteditable // If the focused content isn't editable, or it has independent selection, // we don't have focus. if (!focusedElement->HasFlag(NODE_IS_EDITABLE) || focusedElement->HasIndependentSelection()) { return nullptr; } // If our window is focused, we're focused. return OurWindowHasFocus() ? focusedElement : nullptr; } bool HTMLEditor::IsActiveInDOMWindow() const { nsFocusManager* focusManager = nsFocusManager::GetFocusManager(); if (NS_WARN_IF(!focusManager)) { return false; } Document* document = GetDocument(); if (NS_WARN_IF(!document)) { return false; } // If we're in designMode, we're always active in the DOM window. if (IsInDesignMode()) { return true; } nsPIDOMWindowOuter* ourWindow = document->GetWindow(); nsCOMPtr win; nsIContent* content = nsFocusManager::GetFocusedDescendant( ourWindow, nsFocusManager::eOnlyCurrentWindow, getter_AddRefs(win)); if (!content) { return false; } if (content->IsInDesignMode()) { return true; } // We're HTML editor for contenteditable // If the active content isn't editable, or it has independent selection, // we're not active). if (!content->HasFlag(NODE_IS_EDITABLE) || content->HasIndependentSelection()) { return false; } return true; } Element* HTMLEditor::ComputeEditingHostInternal( const nsIContent* aContent, LimitInBodyElement aLimitInBodyElement) const { Document* document = GetDocument(); if (NS_WARN_IF(!document)) { return nullptr; } auto MaybeLimitInBodyElement = [&](const Element* aCandidateEditingHost) -> Element* { if (!aCandidateEditingHost) { return nullptr; } if (aLimitInBodyElement != LimitInBodyElement::Yes) { return const_cast(aCandidateEditingHost); } // By default, we should limit editing host to the element for // avoiding deleting or creating unexpected elements outside the . // However, this is incompatible with Chrome so that we should stop // doing this with adding safety checks more. if (document->GetBodyElement() && nsContentUtils::ContentIsFlattenedTreeDescendantOf( aCandidateEditingHost, document->GetBodyElement())) { return const_cast(aCandidateEditingHost); } // XXX If aContent is an editing host and has no parent node, we reach here, // but returning the which is not connected to aContent is odd. return document->GetBodyElement(); }; // We're HTML editor for contenteditable AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return nullptr; } const nsIContent* const content = [&]() -> const nsIContent* { if (aContent) { return aContent; } // If there are selection ranges, let's look for their common ancestor's // editing host because selection ranges may be visible for users. nsIContent* selectionCommonAncestor = nullptr; for (uint32_t i : IntegerRange(SelectionRef().RangeCount())) { nsRange* range = SelectionRef().GetRangeAt(i); MOZ_ASSERT(range); nsIContent* commonAncestor = nsIContent::FromNodeOrNull(range->GetCommonAncestorContainer( IgnoreErrors(), AllowRangeCrossShadowBoundary::Yes)); if (MOZ_UNLIKELY(!commonAncestor)) { continue; } if (!selectionCommonAncestor) { selectionCommonAncestor = commonAncestor; } else { selectionCommonAncestor = nsContentUtils::GetCommonFlattenedTreeAncestorForSelection( commonAncestor, selectionCommonAncestor); } } if (selectionCommonAncestor) { return selectionCommonAncestor; } // Otherwise, let's use the focused element in the window. nsPIDOMWindowInner* const innerWindow = document->GetInnerWindow(); if (MOZ_UNLIKELY(!innerWindow)) { return nullptr; } if (Element* focusedElementInWindow = innerWindow->GetFocusedElement()) { if (focusedElementInWindow->ChromeOnlyAccess()) { focusedElementInWindow = Element::FromNodeOrNull( // XXX Should we use // nsIContent::FindFirstNonChromeOnlyAccessContent() instead of // nsINode::GetClosestNativeAnonymousSubtreeRootParentOrHost()? focusedElementInWindow ->GetClosestNativeAnonymousSubtreeRootParentOrHost()); } if (focusedElementInWindow) { return focusedElementInWindow->IsEditable() ? focusedElementInWindow : nullptr; } } // If there is no focused element and the document is in the design mode, // let's return the . if (document->IsInDesignMode()) { return document->GetBodyElement(); } // Otherwise, we cannot find the editing host... return nullptr; }(); if ((content && content->IsInDesignMode()) || (!content && document->IsInDesignMode())) { // FIXME: There may be no . In such case and aLimitInBodyElement is // "No", we should use root element instead. return document->GetBodyElement(); } if (NS_WARN_IF(!content)) { return nullptr; } // If the active content isn't editable, we're not active. if (!content->HasFlag(NODE_IS_EDITABLE)) { return nullptr; } // Although the content shouldn't be in a native anonymous subtree, but // perhaps due to a bug of Selection or Range API, it may occur. HTMLEditor // shouldn't touch native anonymous subtree so that return nullptr in such // case. if (MOZ_UNLIKELY(content->IsInNativeAnonymousSubtree())) { return nullptr; } // Note that `Selection` can be in or