/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sw=2 et tw=80: */ /* 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 "ErrorList.h" #include "HTMLEditor.h" #include "HTMLEditorInlines.h" #include "HTMLEditorNestedClasses.h" #include #include #include "AutoRangeArray.h" #include "CSSEditUtils.h" #include "EditAction.h" #include "EditorDOMPoint.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "HTMLEditUtils.h" #include "PendingStyles.h" // for SpecifiedStyle #include "WSRunObject.h" #include "mozilla/Assertions.h" #include "mozilla/CheckedInt.h" #include "mozilla/ContentIterator.h" #include "mozilla/IntegerRange.h" #include "mozilla/InternalMutationEvent.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/Maybe.h" #include "mozilla/OwningNonNull.h" #include "mozilla/Preferences.h" #include "mozilla/RangeUtils.h" #include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_* #include "mozilla/TextComposition.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/RangeBinding.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/StaticRange.h" #include "mozilla/mozalloc.h" #include "nsAString.h" #include "nsAlgorithm.h" #include "nsAtom.h" #include "nsCRT.h" #include "nsCRTGlue.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsFrameSelection.h" #include "nsGkAtoms.h" #include "nsHTMLDocument.h" #include "nsIContent.h" #include "nsID.h" #include "nsIFrame.h" #include "nsINode.h" #include "nsLiteralString.h" #include "nsPrintfCString.h" #include "nsRange.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsStyledElement.h" #include "nsTArray.h" #include "nsTextNode.h" #include "nsThreadUtils.h" #include "nsUnicharUtils.h" class nsISupports; namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions; using LeafNodeType = HTMLEditUtils::LeafNodeType; using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; using StyleDifference = HTMLEditUtils::StyleDifference; using WalkTextOption = HTMLEditUtils::WalkTextOption; using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; /******************************************************** * first some helpful functors we will use ********************************************************/ static bool IsPendingStyleCachePreservingSubAction( EditSubAction aEditSubAction) { switch (aEditSubAction) { case EditSubAction::eDeleteSelectedContent: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::eCreateOrChangeList: case EditSubAction::eIndent: case EditSubAction::eOutdent: case EditSubAction::eSetOrClearAlignment: case EditSubAction::eCreateOrRemoveBlock: case EditSubAction::eMergeBlockContents: case EditSubAction::eRemoveList: case EditSubAction::eCreateOrChangeDefinitionListItem: case EditSubAction::eInsertElement: case EditSubAction::eInsertQuotation: case EditSubAction::eInsertQuotedText: return true; default: return false; } } template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMRange& aRange); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMRange& aRange); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint); nsresult HTMLEditor::InitEditorContentAndSelection() { 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; } nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() failed"); return rv; } // If the selection hasn't been set up yet, set it up collapsed to the end of // our editable content. // XXX I think that this shouldn't do it in `HTMLEditor` because it maybe // removed by the web app and if they call `Selection::AddRange()` without // checking the range count, it may cause multiple selection ranges. if (!SelectionRef().RangeCount()) { nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() " "failed"); return rv; } } if (IsInPlaintextMode()) { // XXX Should we do this in HTMLEditor? It's odd to guarantee that last // empty line is visible only when it's in the plain text mode. nsresult rv = EnsurePaddingBRElementInMultilineEditor(); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed"); return rv; } } Element* bodyOrDocumentElement = GetRoot(); if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) { return NS_ERROR_FAILURE; } if (!bodyOrDocumentElement) { return NS_OK; } rv = InsertBRElementToEmptyListItemsAndTableCellsInRange( RawRangeBoundary(bodyOrDocumentElement, 0u), RawRangeBoundary(bodyOrDocumentElement, bodyOrDocumentElement->GetChildCount())); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange() " "failed, but ignored"); return NS_OK; } void HTMLEditor::OnStartToHandleTopLevelEditSubAction( EditSubAction aTopLevelEditSubAction, nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aRv.Failed()); EditorBase::OnStartToHandleTopLevelEditSubAction( aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv); MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction); MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == aDirectionOfTopLevelEditSubAction); if (NS_WARN_IF(Destroyed())) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } if (!mInitSucceeded) { return; // We should do nothing if we're being initialized. } NS_WARNING_ASSERTION( !aRv.Failed(), "EditorBase::OnStartToHandleTopLevelEditSubAction() failed"); // Remember where our selection was before edit action took place: const auto atCompositionStart = GetFirstIMESelectionStartPoint(); if (atCompositionStart.IsSet()) { // If there is composition string, let's remember current composition // range. TopLevelEditSubActionDataRef().mSelectedRange->StoreRange( atCompositionStart, GetLastIMESelectionEndPoint()); } else { // Get the selection location // XXX This may occur so that I think that we shouldn't throw exception // in this case. if (NS_WARN_IF(!SelectionRef().RangeCount())) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } if (const nsRange* range = SelectionRef().GetRangeAt(0)) { TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(*range); } } // Register with range updater to track this as we perturb the doc RangeUpdaterRef().RegisterRangeItem( *TopLevelEditSubActionDataRef().mSelectedRange); // Remember current inline styles for deletion and normal insertion ops const bool cacheInlineStyles = [&]() { switch (aTopLevelEditSubAction) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: return true; default: return IsPendingStyleCachePreservingSubAction(aTopLevelEditSubAction); } }(); if (cacheInlineStyles) { const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { aRv.Throw(NS_ERROR_FAILURE); return; } nsIContent* const startContainer = HTMLEditUtils::GetContentToPreserveInlineStyles( TopLevelEditSubActionDataRef() .mSelectedRange->StartPoint(), *editingHost); if (NS_WARN_IF(!startContainer)) { aRv.Throw(NS_ERROR_FAILURE); return; } if (const RefPtr startContainerElement = startContainer->GetAsElementOrParentElement()) { nsresult rv = CacheInlineStyles(*startContainerElement); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CacheInlineStyles() failed"); aRv.Throw(rv); return; } } } // Stabilize the document against contenteditable count changes Document* document = GetDocument(); if (NS_WARN_IF(!document)) { aRv.Throw(NS_ERROR_FAILURE); return; } if (document->GetEditingState() == Document::EditingState::eContentEditable) { document->ChangeContentEditableCount(nullptr, +1); TopLevelEditSubActionDataRef().mRestoreContentEditableCount = true; } // Check that selection is in subtree defined by body node nsresult rv = EnsureSelectionInBodyOrDocumentElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() " "failed, but ignored"); } nsresult HTMLEditor::OnEndHandlingTopLevelEditSubAction() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); nsresult rv; while (true) { if (NS_WARN_IF(Destroyed())) { rv = NS_ERROR_EDITOR_DESTROYED; break; } if (!mInitSucceeded) { rv = NS_OK; // We should do nothing if we're being initialized. break; } // Do all the tricky stuff rv = OnEndHandlingTopLevelEditSubActionInternal(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() failied"); // Perhaps, we need to do the following jobs even if the editor has been // destroyed since they adjust some states of HTML document but don't // modify the DOM tree nor Selection. // Free up selectionState range item if (TopLevelEditSubActionDataRef().mSelectedRange) { RangeUpdaterRef().DropRangeItem( *TopLevelEditSubActionDataRef().mSelectedRange); } // Reset the contenteditable count to its previous value if (TopLevelEditSubActionDataRef().mRestoreContentEditableCount) { Document* document = GetDocument(); if (NS_WARN_IF(!document)) { rv = NS_ERROR_FAILURE; break; } if (document->GetEditingState() == Document::EditingState::eContentEditable) { document->ChangeContentEditableCount(nullptr, -1); } } break; } DebugOnly rvIgnored = EditorBase::OnEndHandlingTopLevelEditSubAction(); NS_WARNING_ASSERTION( NS_FAILED(rv) || NS_SUCCEEDED(rvIgnored), "EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored"); MOZ_ASSERT(!GetTopLevelEditSubAction()); MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone); return rv; } nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); nsresult rv = EnsureSelectionInBodyOrDocumentElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() " "failed, but ignored"); switch (GetTopLevelEditSubAction()) { case EditSubAction::eReplaceHeadWithHTMLSource: case EditSubAction::eCreatePaddingBRElementForEmptyEditor: return NS_OK; default: break; } if (TopLevelEditSubActionDataRef().mChangedRange->IsPositioned() && GetTopLevelEditSubAction() != EditSubAction::eUndo && GetTopLevelEditSubAction() != EditSubAction::eRedo) { // don't let any txns in here move the selection around behind our back. // Note that this won't prevent explicit selection setting from working. AutoTransactionsConserveSelection dontChangeMySelection(*this); { EditorDOMRange changedRange( *TopLevelEditSubActionDataRef().mChangedRange); if (changedRange.IsPositioned() && changedRange.EnsureNotInNativeAnonymousSubtree()) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::eDeleteText: { // XXX We should investigate whether this is really needed because // it seems that the following code does not handle the // white-spaces. RefPtr extendedChangedRange = CreateRangeIncludingAdjuscentWhiteSpaces(changedRange); if (extendedChangedRange) { MOZ_ASSERT(extendedChangedRange->IsPositioned()); // Use extended range temporarily. TopLevelEditSubActionDataRef().mChangedRange = std::move(extendedChangedRange); } break; } default: { if (Element* editingHost = ComputeEditingHost()) { if (RefPtr extendedChangedRange = AutoRangeArray:: CreateRangeWrappingStartAndEndLinesContainingBoundaries( changedRange, GetTopLevelEditSubAction(), *editingHost)) { MOZ_ASSERT(extendedChangedRange->IsPositioned()); // Use extended range temporarily. TopLevelEditSubActionDataRef().mChangedRange = std::move(extendedChangedRange); } break; } } } } } // if we did a ranged deletion or handling backspace key, make sure we have // a place to put caret. // Note we only want to do this if the overall operation was deletion, // not if deletion was done along the way for // EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc. // That's why this is here rather than DeleteSelectionAsSubAction(). // However, we shouldn't insert
elements if we've already removed // empty block parents because users may want to disappear the line by // the deletion. // XXX We should make HandleDeleteSelection() store expected container // for handling this here since we cannot trust current selection is // collapsed at deleted point. if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent && TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange && !TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks) { const auto newCaretPosition = GetFirstSelectionStartPoint(); if (!newCaretPosition.IsSet()) { NS_WARNING("There was no selection range"); return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() " "failed"); return rv; } } // add in any needed
s, and remove any unneeded ones. nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInRange( TopLevelEditSubActionDataRef().mChangedRange->StartRef().AsRaw(), TopLevelEditSubActionDataRef().mChangedRange->EndRef().AsRaw()); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange()" " failed, but ignored"); // merge any adjacent text nodes switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: break; default: { nsresult rv = CollapseAdjacentTextNodes( MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CollapseAdjacentTextNodes() failed"); return rv; } break; } } // Clean up any empty nodes in the changed range unless they are inserted // intentionally. if (TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements) { nsresult rv = RemoveEmptyNodesIn( EditorDOMRange(*TopLevelEditSubActionDataRef().mChangedRange)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveEmptyNodesIn() failed"); return rv; } } // attempt to transform any unneeded nbsp's into spaces after doing various // operations switch (GetTopLevelEditSubAction()) { case EditSubAction::eDeleteSelectedContent: if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) { break; } [[fallthrough]]; case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::ePasteHTMLContent: case EditSubAction::eInsertHTMLSource: { // TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII // white-spaces with NPSPs and then, we'll replace them with ASCII // white-spaces here. We should avoid this overwriting things as // far as possible because replacing characters in text nodes // causes running mutation event listeners which are really // expensive. // Adjust end of composition string if there is composition string. auto pointToAdjust = GetLastIMESelectionEndPoint(); if (!pointToAdjust.IsInContentNode()) { // Otherwise, adjust current selection start point. pointToAdjust = GetFirstSelectionStartPoint(); if (NS_WARN_IF(!pointToAdjust.IsInContentNode())) { return NS_ERROR_FAILURE; } } if (EditorUtils::IsEditableContent( *pointToAdjust.ContainerAs(), EditorType::HTML)) { AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(), &pointToAdjust); nsresult rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( *this, pointToAdjust); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed"); return rv; } } // also do this for original selection endpoints. // XXX Hmm, if `NormalizeVisibleWhiteSpacesAt()` runs mutation event // listener and that causes changing `mSelectedRange`, what we // should do? if (NS_WARN_IF(!TopLevelEditSubActionDataRef() .mSelectedRange->IsPositioned())) { return NS_ERROR_FAILURE; } EditorDOMPoint atStart = TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(); if (atStart != pointToAdjust && atStart.IsInContentNode() && EditorUtils::IsEditableContent(*atStart.ContainerAs(), EditorType::HTML)) { AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(), &pointToAdjust); AutoTrackDOMPoint trackStartPoint(RangeUpdaterRef(), &atStart); nsresult rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( *this, atStart); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed, but ignored"); } // we only need to handle old selection endpoint if it was different // from start EditorDOMPoint atEnd = TopLevelEditSubActionDataRef().mSelectedRange->EndPoint(); if (!TopLevelEditSubActionDataRef().mSelectedRange->Collapsed() && atEnd != pointToAdjust && atEnd != atStart && atEnd.IsInContentNode() && EditorUtils::IsEditableContent(*atEnd.ContainerAs(), EditorType::HTML)) { nsresult rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(*this, atEnd); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed, but ignored"); } break; } default: break; } // Adjust selection for insert text, html paste, and delete actions if // we haven't removed new empty blocks. Note that if empty block parents // are removed, Selection should've been adjusted by the method which // did it. if (!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks && SelectionRef().IsCollapsed()) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::ePasteHTMLContent: case EditSubAction::eInsertHTMLSource: // XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally // does not create padding `
` element for empty editor. // Investigate which is better that whether this should does it // or wait MaybeCreatePaddingBRElementForEmptyEditor(). rv = AdjustCaretPositionAndEnsurePaddingBRElement( GetDirectionOfTopLevelEditSubAction()); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement() " "failed"); return rv; } break; default: break; } } // check for any styles which were removed inappropriately bool reapplyCachedStyle; switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: reapplyCachedStyle = true; break; default: reapplyCachedStyle = IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction()); break; } // If the selection is in empty inline HTML elements, we should delete // them unless it's inserted intentionally. if (mPlaceholderBatch && TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements && SelectionRef().IsCollapsed() && SelectionRef().GetFocusNode()) { RefPtr mostDistantEmptyInlineAncestor = nullptr; for (Element* ancestor : SelectionRef().GetFocusNode()->InclusiveAncestorsOfType()) { if (!ancestor->IsHTMLElement() || !HTMLEditUtils::IsRemovableFromParentNode(*ancestor) || !HTMLEditUtils::IsEmptyInlineContainer( *ancestor, {EmptyCheckOption::TreatSingleBRElementAsVisible})) { break; } mostDistantEmptyInlineAncestor = ancestor; } if (mostDistantEmptyInlineAncestor) { nsresult rv = DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteNodeWithTransaction() failed at deleting " "empty inline ancestors"); return rv; } } } // But the cached inline styles should be restored from type-in-state later. if (reapplyCachedStyle) { DebugOnly rvIgnored = mPendingStylesToApplyToNewContent->UpdateSelState(*this); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "PendingStyles::UpdateSelState() failed, but ignored"); rvIgnored = ReapplyCachedStyles(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "HTMLEditor::ReapplyCachedStyles() failed, but ignored"); TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear(); } } rv = HandleInlineSpellCheck( TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(), TopLevelEditSubActionDataRef().mChangedRange); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::HandleInlineSpellCheck() failed"); return rv; } // detect empty doc // XXX Need to investigate when the padding
element is removed because // I don't see the
element with testing manually. If it won't be // used, we can get rid of this cost. rv = MaybeCreatePaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed"); return rv; } // adjust selection HINT if needed if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine && SelectionRef().IsCollapsed()) { SetSelectionInterlinePosition(); } return NS_OK; } Result HTMLEditor::CanHandleHTMLEditSubAction( CheckSelectionInReplacedElement aCheckSelectionInReplacedElement /* = CheckSelectionInReplacedElement::Yes */) const { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } // If there is not selection ranges, we should ignore the result. if (!SelectionRef().RangeCount()) { return EditActionResult::CanceledResult(); } const nsRange* range = SelectionRef().GetRangeAt(0); nsINode* selStartNode = range->GetStartContainer(); if (NS_WARN_IF(!selStartNode) || NS_WARN_IF(!selStartNode->IsContent())) { return Err(NS_ERROR_FAILURE); } if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) { return EditActionResult::CanceledResult(); } nsINode* selEndNode = range->GetEndContainer(); if (NS_WARN_IF(!selEndNode) || NS_WARN_IF(!selEndNode->IsContent())) { return Err(NS_ERROR_FAILURE); } if (selStartNode == selEndNode) { if (aCheckSelectionInReplacedElement == CheckSelectionInReplacedElement::Yes && HTMLEditUtils::IsNonEditableReplacedContent( *selStartNode->AsContent())) { return EditActionResult::CanceledResult(); } return EditActionResult::IgnoredResult(); } if (HTMLEditUtils::IsNonEditableReplacedContent(*selStartNode->AsContent()) || HTMLEditUtils::IsNonEditableReplacedContent(*selEndNode->AsContent())) { return EditActionResult::CanceledResult(); } if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) { return EditActionResult::CanceledResult(); } // XXX What does it mean the common ancestor is editable? I have no idea. // It should be in same (active) editing host, and even if it's editable, // there may be non-editable contents in the range. nsINode* commonAncestor = range->GetClosestCommonInclusiveAncestor(); if (MOZ_UNLIKELY(!commonAncestor)) { NS_WARNING( "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr"); return Err(NS_ERROR_FAILURE); } return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor) ? EditActionResult::IgnoredResult() : EditActionResult::CanceledResult(); } MOZ_CAN_RUN_SCRIPT static nsStaticAtom& MarginPropertyAtomForIndent( nsIContent& aContent) { nsAutoString direction; DebugOnly rvIgnored = CSSEditUtils::GetComputedProperty( aContent, *nsGkAtoms::direction, direction); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetComputedProperty(nsGkAtoms::direction)" " failed, but ignored"); return direction.EqualsLiteral("rtl") ? *nsGkAtoms::marginRight : *nsGkAtoms::marginLeft; } nsresult HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(SelectionRef().IsCollapsed()); // If we are after a padding `
` element for empty last line in the same // block, then move selection to be before it const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorRawDOMPoint atSelectionStart(firstRange->StartRef()); if (NS_WARN_IF(!atSelectionStart.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atSelectionStart.IsSetAndValid()); if (!atSelectionStart.IsInContentNode()) { return NS_OK; } Element* editingHost = ComputeEditingHost(); if (!editingHost) { NS_WARNING( "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() did nothing " "because of no editing host"); return NS_OK; } nsIContent* previousBRElement = HTMLEditUtils::GetPreviousContent(atSelectionStart, {}, editingHost); if (!previousBRElement || !previousBRElement->IsHTMLElement(nsGkAtoms::br) || !previousBRElement->GetParent() || !EditorUtils::IsEditableContent(*previousBRElement->GetParent(), EditorType::HTML) || !HTMLEditUtils::IsInvisibleBRElement(*previousBRElement)) { return NS_OK; } const RefPtr blockElementAtSelectionStart = HTMLEditUtils::GetInclusiveAncestorElement( *atSelectionStart.ContainerAs(), HTMLEditUtils::ClosestBlockElement); const RefPtr parentBlockElementOfBRElement = HTMLEditUtils::GetAncestorElement(*previousBRElement, HTMLEditUtils::ClosestBlockElement); if (!blockElementAtSelectionStart || blockElementAtSelectionStart != parentBlockElementOfBRElement) { return NS_OK; } // If we are here then the selection is right after a padding
// element for empty last line that is in the same block as the // selection. We need to move the selection start to be before the // padding
element. EditorRawDOMPoint atInvisibleBRElement(previousBRElement); nsresult rv = CollapseSelectionTo(atInvisibleBRElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() { MOZ_ASSERT(IsEditActionDataAvailable()); if (mPaddingBRElementForEmptyEditor) { return NS_OK; } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eCreatePaddingBRElementForEmptyEditor, 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"); ignoredError.SuppressException(); RefPtr rootElement = GetRoot(); if (!rootElement) { return NS_OK; } // Now we've got the body element. Iterate over the body element's children, // looking for editable content. If no editable content is found, insert the // padding
element. EditorType editorType = GetEditorType(); bool isRootEditable = EditorUtils::IsEditableContent(*rootElement, editorType); for (nsIContent* rootChild = rootElement->GetFirstChild(); rootChild; rootChild = rootChild->GetNextSibling()) { if (EditorUtils::IsPaddingBRElementForEmptyEditor(*rootChild) || !isRootEditable || EditorUtils::IsEditableContent(*rootChild, editorType) || HTMLEditUtils::IsBlockElement(*rootChild)) { return NS_OK; } } // Skip adding the padding
element for empty editor if body // is read-only. if (IsHTMLEditor() && !HTMLEditUtils::IsSimplyEditableNode(*rootElement)) { return NS_OK; } // Create a br. RefPtr newBRElement = CreateHTMLContent(nsGkAtoms::br); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_WARN_IF(!newBRElement)) { return NS_ERROR_FAILURE; } mPaddingBRElementForEmptyEditor = static_cast(newBRElement.get()); // Give it a special attribute. newBRElement->SetFlags(NS_PADDING_FOR_EMPTY_EDITOR); // Put the node in the document. Result insertBRElementResult = InsertNodeWithTransaction(*newBRElement, EditorDOMPoint(rootElement, 0u)); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertBRElementResult.unwrapErr(); } // Set selection. insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); nsresult rv = CollapseSelectionToStartOf(*rootElement); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionToStartOf() caused destroying the " "editor"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed, but ignored"); return NS_OK; } nsresult HTMLEditor::EnsureNoPaddingBRElementForEmptyEditor() { MOZ_ASSERT(IsEditActionDataAvailable()); if (!mPaddingBRElementForEmptyEditor) { return NS_OK; } // If we're an HTML editor, a mutation event listener may recreate padding //
element for empty editor again during the call of // DeleteNodeWithTransaction(). So, move it first. RefPtr paddingBRElement( std::move(mPaddingBRElementForEmptyEditor)); nsresult rv = DeleteNodeWithTransaction(*paddingBRElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DeleteNodeWithTransaction() failed"); return rv; } nsresult HTMLEditor::ReflectPaddingBRElementForEmptyEditor() { if (NS_WARN_IF(!mRootElement)) { NS_WARNING("Failed to handle padding BR element due to no root element"); return NS_ERROR_FAILURE; } // The idea here is to see if the magic empty node has suddenly reappeared. If // it has, set our state so we remember it. There is a tradeoff between doing // here and at redo, or doing it everywhere else that might care. Since undo // and redo are relatively rare, it makes sense to take the (small) // performance hit here. nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafContent( *mRootElement, {LeafNodeType::OnlyLeafNode}); if (firstLeafChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) { mPaddingBRElementForEmptyEditor = static_cast(firstLeafChild); } else { mPaddingBRElementForEmptyEditor = nullptr; } return NS_OK; } nsresult HTMLEditor::PrepareInlineStylesForCaret() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(SelectionRef().IsCollapsed()); // XXX This method works with the top level edit sub-action, but this // must be wrong if we are handling nested edit action. if (TopLevelEditSubActionDataRef().mDidDeleteSelection) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: { nsresult rv = ReapplyCachedStyles(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed"); return rv; } break; } default: break; } } // For most actions we want to clear the cached styles, but there are // exceptions if (!IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) { TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear(); } return NS_OK; } Result HTMLEditor::HandleInsertText( EditSubAction aEditSubAction, const nsAString& aInsertionString, SelectionHandling aSelectionHandling) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText || aEditSubAction == EditSubAction::eInsertTextComingFromIME); MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore, aEditSubAction == EditSubAction::eInsertTextComingFromIME); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. Don't delete existing inline // tags, because we're hopefully going to insert text (bug 787432). if (!SelectionRef().IsCollapsed() && aSelectionHandling == SelectionHandling::Delete) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, " "nsIEditor::eNoStrip) failed"); return Err(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return Err(NS_ERROR_FAILURE); } const RefPtr editingHost = ComputeEditingHost( GetDocument()->IsXMLDocument() ? LimitInBodyElement::No : LimitInBodyElement::Yes); if (NS_WARN_IF(!editingHost)) { return Err(NS_ERROR_FAILURE); } auto pointToInsert = GetFirstSelectionStartPoint(); if (MOZ_UNLIKELY(!pointToInsert.IsSet())) { return Err(NS_ERROR_FAILURE); } // for every property that is set, insert a new inline style node Result setStyleResult = CreateStyleForInsertText(pointToInsert); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return setStyleResult.propagateErr(); } if (setStyleResult.inspect().IsSet()) { pointToInsert = setStyleResult.unwrap(); } if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(pointToInsert.IsSetAndValid()); // If the point is not in an element which can contain text nodes, climb up // the DOM tree. if (!pointToInsert.IsInTextNode()) { while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), *nsGkAtoms::textTagName)) { if (NS_WARN_IF(pointToInsert.GetContainer() == editingHost) || NS_WARN_IF(!pointToInsert.GetContainerParentAs())) { NS_WARNING("Selection start point couldn't have text nodes"); return Err(NS_ERROR_FAILURE); } pointToInsert.Set(pointToInsert.ContainerAs()); } } if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) { auto compositionStartPoint = GetFirstIMESelectionStartPoint(); if (!compositionStartPoint.IsSet()) { compositionStartPoint = pointToInsert; } if (aInsertionString.IsEmpty()) { // Right now the WhiteSpaceVisibilityKeeper code bails on empty strings, // but IME needs the InsertTextWithTransaction() call to still happen // since empty strings are meaningful there. Result insertTextResult = InsertTextWithTransaction(*document, aInsertionString, compositionStartPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult.propagateErr(); } return EditActionResult::HandledResult(); } auto compositionEndPoint = GetLastIMESelectionEndPoint(); if (!compositionEndPoint.IsSet()) { compositionEndPoint = compositionStartPoint; } Result replaceTextResult = WhiteSpaceVisibilityKeeper::ReplaceText( *this, aInsertionString, EditorDOMRange(compositionStartPoint, compositionEndPoint)); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed"); return replaceTextResult.propagateErr(); } compositionStartPoint = GetFirstIMESelectionStartPoint(); compositionEndPoint = GetLastIMESelectionEndPoint(); if (NS_WARN_IF(!compositionStartPoint.IsSet()) || NS_WARN_IF(!compositionEndPoint.IsSet())) { // Mutation event listener has changed the DOM tree... return EditActionResult::HandledResult(); } nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( compositionStartPoint.ToRawRangeBoundary(), compositionEndPoint.ToRawRangeBoundary()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } return EditActionResult::HandledResult(); } MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText); // find where we are EditorDOMPoint currentPoint(pointToInsert); // is our text going to be PREformatted? // We remember this so that we know how to handle tabs. const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted( *pointToInsert.ContainerAs()); // turn off the edit listener: we know how to // build the "doc changed range" ourselves, and it's // must faster to do it once here than to track all // the changes one at a time. AutoRestore disableListener( EditSubActionDataRef().mAdjustChangedRangeFromListener); EditSubActionDataRef().mAdjustChangedRangeFromListener = false; // don't change my selection in subtransactions AutoTransactionsConserveSelection dontChangeMySelection(*this); int32_t pos = 0; constexpr auto newlineStr = NS_LITERAL_STRING_FROM_CSTRING(LFSTR); { AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert); // for efficiency, break out the pre case separately. This is because // its a lot cheaper to search the input string for only newlines than // it is to search for both tabs and newlines. if (!isWhiteSpaceCollapsible || IsInPlaintextMode()) { while (pos != -1 && pos < AssertedCast(aInsertionString.Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = aInsertionString.FindChar(nsCRT::LF, oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = aInsertionString.Length() - oldPos; pos = aInsertionString.Length(); } nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen); // is it a return? if (subStr.Equals(newlineStr)) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, currentPoint); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We don't want to update selection here because we've blocked // InsertNodeTransaction updating selection with // dontChangeMySelection. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(!AllowsTransactionsToChangeSelection()); pos++; RefPtr brElement = unwrappedInsertBRElementResult.UnwrapNewNode(); if (brElement->GetNextSibling()) { pointToInsert.Set(brElement->GetNextSibling()); } else { pointToInsert.SetToEndOf(currentPoint.GetContainer()); } // XXX In most cases, pointToInsert and currentPoint are same here. // But if the
element has been moved to different point by // mutation observer, those points become different. currentPoint.SetAfter(brElement); NS_WARNING_ASSERTION(currentPoint.IsSet(), "Failed to set after the
element"); NS_WARNING_ASSERTION(currentPoint == pointToInsert, "Perhaps,
element position has been moved " "to different point " "by mutation observer"); } else { Result insertTextResult = InsertTextWithTransaction(*document, subStr, currentPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult.propagateErr(); } currentPoint = insertTextResult.inspect(); pointToInsert = insertTextResult.unwrap(); } } } else { constexpr auto tabStr = u"\t"_ns; constexpr auto spacesStr = u" "_ns; nsAutoString insertionString(aInsertionString); // For FindCharInSet(). while (pos != -1 && pos < AssertedCast(insertionString.Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = insertionString.FindCharInSet(u"\t\n", oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = insertionString.Length() - oldPos; pos = insertionString.Length(); } nsDependentSubstring subStr(insertionString, oldPos, subStrLen); // is it a tab? if (subStr.Equals(tabStr)) { Result insertTextResult = WhiteSpaceVisibilityKeeper::InsertText(*this, spacesStr, currentPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return insertTextResult.propagateErr(); } pos++; MOZ_ASSERT(insertTextResult.inspect().IsSet()); currentPoint = insertTextResult.inspect(); pointToInsert = insertTextResult.unwrap(); } // is it a return? else if (subStr.Equals(newlineStr)) { Result insertBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, currentPoint, *editingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // TODO: Some methods called for handling non-preformatted text use // ComputeEditingHost(). Therefore, they depend on the latest // selection. So we cannot skip updating selection here. nsresult rv = unwrappedInsertBRElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CreateElementResult::SuggestCaretPointTo() failed, but ignored"); pos++; RefPtr newBRElement = unwrappedInsertBRElementResult.UnwrapNewNode(); MOZ_DIAGNOSTIC_ASSERT(newBRElement); if (newBRElement->GetNextSibling()) { pointToInsert.Set(newBRElement->GetNextSibling()); } else { pointToInsert.SetToEndOf(currentPoint.GetContainer()); } currentPoint.SetAfter(newBRElement); NS_WARNING_ASSERTION(currentPoint.IsSet(), "Failed to set after the new
element"); // XXX If the newBRElement has been moved or removed by mutation // observer, we hit this assert. We need to check if // newBRElement is in expected point, though, we must have // a lot of same bugs... NS_WARNING_ASSERTION( currentPoint == pointToInsert, "Perhaps, newBRElement has been moved or removed unexpectedly"); } else { Result insertTextResult = WhiteSpaceVisibilityKeeper::InsertText(*this, subStr, currentPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return insertTextResult.propagateErr(); } MOZ_ASSERT(insertTextResult.inspect().IsSet()); currentPoint = insertTextResult.inspect(); pointToInsert = insertTextResult.unwrap(); } } } // After this block, pointToInsert is updated by AutoTrackDOMPoint. } if (currentPoint.IsSet()) { currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine); nsresult rv = CollapseSelectionTo(currentPoint); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Selection::Collapse() failed, but ignored"); // manually update the doc changed range so that AfterEdit will clean up // the correct portion of the document. rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } return EditActionResult::HandledResult(); } DebugOnly rvIgnored = SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Selection::SetInterlinePosition(InterlinePosition::" "EndOfLine) failed, but ignored"); rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert); if (NS_FAILED(rv)) { NS_WARNING("nsRange::CollapseTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } nsresult HTMLEditor::InsertLineBreakAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } // XXX This may be called by execCommand() with "insertLineBreak". // In such case, naming the transaction "TypingTxnName" is odd. AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, ScrollSelectionIntoView::Yes, __FUNCTION__); // calling it text insertion to trigger moz br treatment by rules // XXX Why do we use EditSubAction::eInsertText here? Looks like // EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode // is better. IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertText, 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"); UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. if (!SelectionRef().IsCollapsed()) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); return rv; } } const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); RefPtr editingHost = ComputeEditingHost(); if (NS_WARN_IF(!editingHost)) { return NS_ERROR_FAILURE; } // 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 || !HTMLEditUtils::ShouldInsertLinefeedCharacter(atStartOfSelection, *editingHost)) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, atStartOfSelection, nsIEditor::eNext); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); // Next inserting text should be inserted into styled inline elements if // they have first visible thing in the new line. auto pointToPutCaret = [&]() -> EditorDOMPoint { WSScanResult forwardScanResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( editingHost, EditorRawDOMPoint::After( *unwrappedInsertBRElementResult.GetNewNode())); if (forwardScanResult.InVisibleOrCollapsibleCharacters()) { unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); return forwardScanResult.Point(); } if (forwardScanResult.ReachedSpecialContent()) { unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); return forwardScanResult.PointAtContent(); } return unwrappedInsertBRElementResult.UnwrapCaretPoint(); }(); nsresult rv = CollapseSelectionTo(pointToPutCaret); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "CreateElementResult::SuggestCaretPointTo() failed"); return rv; } 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(); 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"); } } firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } atStartOfSelection = EditorDOMPoint(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); // Do nothing if the node is read-only if (!HTMLEditUtils::IsSimplyEditableNode( *atStartOfSelection.GetContainer())) { return NS_SUCCESS_DOM_NO_OPERATION; } Result insertLineFeedResult = HandleInsertLinefeed(atStartOfSelection, *editingHost); if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed"); return insertLineFeedResult.unwrapErr(); } rv = CollapseSelectionTo(insertLineFeedResult.inspect()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } Result HTMLEditor::InsertParagraphSeparatorAsSubAction(const Element& aEditingHost) { if (NS_WARN_IF(!mInitSucceeded)) { return Err(NS_ERROR_NOT_INITIALIZED); } { Result result = CanHandleHTMLEditSubAction( CheckSelectionInReplacedElement::OnlyWhenNotInSameNode); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } // XXX This may be called by execCommand() with "insertParagraph". // In such case, naming the transaction "TypingTxnName" is odd. AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertParagraphSeparator, nsIEditor::eNext, 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"); UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. if (!SelectionRef().IsCollapsed()) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); return Err(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } AutoRangeArray selectionRanges(SelectionRef()); { // If the editing host is the body element, the selection may be outside // aEditingHost. In the case, we should use the editing host outside the // only here for keeping our traditional behavior for now. // This should be fixed in bug 1634351. const Element* editingHostMaybeOutsideBody = &aEditingHost; if (aEditingHost.IsHTMLElement(nsGkAtoms::body)) { editingHostMaybeOutsideBody = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHostMaybeOutsideBody)) { return Err(NS_ERROR_FAILURE); } } selectionRanges.EnsureOnlyEditableRanges(*editingHostMaybeOutsideBody); if (NS_WARN_IF(selectionRanges.Ranges().IsEmpty())) { return Err(NS_ERROR_FAILURE); } } auto pointToInsert = selectionRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } while (true) { Element* element = pointToInsert.GetContainerOrContainerParentElement(); if (MOZ_UNLIKELY(!element)) { return Err(NS_ERROR_FAILURE); } // If the element can have a
element (it means that the element or its // container must be able to have
or

too), we can handle // insertParagraph at the point. if (HTMLEditUtils::CanNodeContain(*element, *nsGkAtoms::br)) { break; } // Otherwise, try to insert paragraph at the parent. pointToInsert = pointToInsert.ParentPoint(); } if (IsMailEditor()) { if (RefPtr mailCiteElement = GetMostDistantAncestorMailCiteElement( *pointToInsert.ContainerAs())) { // Split any mailcites in the way. Should we abort this if we encounter // table cell boundaries? Result atNewBRElementOrError = HandleInsertParagraphInMailCiteElement(*mailCiteElement, pointToInsert, aEditingHost); if (MOZ_UNLIKELY(atNewBRElementOrError.isErr())) { NS_WARNING( "HTMLEditor::HandleInsertParagraphInMailCiteElement() failed"); return atNewBRElementOrError.propagateErr(); } EditorDOMPoint pointToPutCaret = atNewBRElementOrError.unwrap(); MOZ_ASSERT(pointToPutCaret.IsSet()); pointToPutCaret.SetInterlinePosition(InterlinePosition::StartOfNextLine); MOZ_ASSERT(pointToPutCaret.GetChild()); MOZ_ASSERT(pointToPutCaret.GetChild()->IsHTMLElement(nsGkAtoms::br)); nsresult rv = CollapseSelectionTo(pointToPutCaret); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } } // If the active editing host is an inline element, or if the active editing // host is the block parent itself and we're configured to use
as a // paragraph separator, just append a
. // If the editing host parent element is editable, it means that the editing // host must be a element and the selection may be outside the body // element. If the selection is outside the editing host, we should not // insert new paragraph nor
element. // XXX Currently, we don't support editing outside element, but Blink // does it. if (aEditingHost.GetParentElement() && HTMLEditUtils::IsSimplyEditableNode(*aEditingHost.GetParentElement()) && !nsContentUtils::ContentIsFlattenedTreeDescendantOf( pointToInsert.ContainerAs(), &aEditingHost)) { return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); } auto InsertLineBreakInstead = [](const Element* aEditableBlockElement, const EditorDOMPoint& aCandidatePointToSplit, ParagraphSeparator aDefaultParagraphSeparator, const Element& aEditingHost) { // If there is no block parent in the editing host, i.e., the editing // host itself is also a non-block element, we should insert a line // break. if (!aEditableBlockElement) { // XXX Chromium checks if the CSS box of the editing host is a block. return true; } // If the editable block element is not splittable, e.g., it's an // editing host, and the default paragraph separator is
or the // element cannot contain a

element, we should insert a
// element. if (!HTMLEditUtils::IsSplittableNode(*aEditableBlockElement)) { return aDefaultParagraphSeparator == ParagraphSeparator::br || !HTMLEditUtils::CanElementContainParagraph(aEditingHost) || HTMLEditUtils::ShouldInsertLinefeedCharacter( aCandidatePointToSplit, aEditingHost); } // If the nearest block parent is a single-line container declared in // the execCommand spec and not the editing host, we should separate the // block even if the default paragraph separator is
element. if (HTMLEditUtils::IsSingleLineContainer(*aEditableBlockElement)) { return false; } // Otherwise, unless there is no block ancestor which can contain

// element, we shouldn't insert a line break here. for (const Element* editableBlockAncestor = aEditableBlockElement; editableBlockAncestor; editableBlockAncestor = HTMLEditUtils::GetAncestorElement( *editableBlockAncestor, HTMLEditUtils::ClosestEditableBlockElementOrButtonElement)) { if (HTMLEditUtils::CanElementContainParagraph( *editableBlockAncestor)) { return false; } } return true; }; // Look for the nearest parent block. However, don't return error even if // there is no block parent here because in such case, i.e., editing host // is an inline element, we should insert
simply. RefPtr editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *pointToInsert.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElementOrButtonElement); // If we cannot insert a

/

element at the selection, we should insert // a
element or a linefeed instead. const ParagraphSeparator separator = GetDefaultParagraphSeparator(); if (InsertLineBreakInstead(editableBlockElement, pointToInsert, separator, aEditingHost)) { // For backward compatibility, we should not insert a linefeed if // paragraph separator is set to "br" which is Gecko-specific mode. if (separator != ParagraphSeparator::br && HTMLEditUtils::ShouldInsertLinefeedCharacter(pointToInsert, aEditingHost)) { Result insertLineFeedResult = HandleInsertLinefeed(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed"); return insertLineFeedResult.propagateErr(); } nsresult rv = CollapseSelectionTo(insertLineFeedResult.inspect()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } Result insertBRElementResult = HandleInsertBRElement(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return insertBRElementResult.propagateErr(); } nsresult rv = insertBRElementResult.inspect().SuggestCaretPointTo(*this, {}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } // If somebody wants to restrict caret position in a block element below, // we should guarantee it. Otherwise, we can put caret to the candidate // point. auto CollapseSelection = [this](const EditorDOMPoint& aCandidatePointToPutCaret, const Element* aBlockElementShouldHaveCaret, const SuggestCaretOptions& aOptions) MOZ_CAN_RUN_SCRIPT -> nsresult { if (!aCandidatePointToPutCaret.IsSet()) { if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion)) { return NS_OK; } return aOptions.contains(SuggestCaret::AndIgnoreTrivialError) ? NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR : NS_ERROR_FAILURE; } EditorDOMPoint pointToPutCaret(aCandidatePointToPutCaret); if (aBlockElementShouldHaveCaret) { Result pointToPutCaretOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorDOMPoint>(*aBlockElementShouldHaveCaret, aCandidatePointToPutCaret); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING( "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() " "failed, but ignored"); } else if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } } nsresult rv = CollapseSelectionTo(pointToPutCaret); if (NS_FAILED(rv) && MOZ_LIKELY(rv != NS_ERROR_EDITOR_DESTROYED) && aOptions.contains(SuggestCaret::AndIgnoreTrivialError)) { rv = NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR; } return rv; }; RefPtr blockElementToPutCaret; // If the default paragraph separator is not
and selection is not in // a splittable block element, we should wrap selected contents in a new // paragraph, then, split it. if (!HTMLEditUtils::IsSplittableNode(*editableBlockElement) && separator != ParagraphSeparator::br) { MOZ_ASSERT(separator == ParagraphSeparator::div || separator == ParagraphSeparator::p); Result, nsresult> suggestBlockElementToPutCaretOrError = FormatBlockContainerWithTransaction( selectionRanges, MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)), aEditingHost); if (MOZ_UNLIKELY(suggestBlockElementToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditor::FormatBlockContainerWithTransaction() failed"); return suggestBlockElementToPutCaretOrError.propagateErr(); } if (selectionRanges.HasSavedRanges()) { selectionRanges.RestoreFromSavedRanges(); } pointToInsert = selectionRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } MOZ_ASSERT(pointToInsert.IsSetAndValid()); blockElementToPutCaret = suggestBlockElementToPutCaretOrError.unwrap(); editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *pointToInsert.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElementOrButtonElement); if (NS_WARN_IF(!editableBlockElement)) { return Err(NS_ERROR_UNEXPECTED); } if (NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*editableBlockElement))) { // Didn't create a new block for some reason, fall back to
Result insertBRElementResult = HandleInsertBRElement(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); EditorDOMPoint pointToPutCaret = unwrappedInsertBRElementResult.UnwrapCaretPoint(); if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) { NS_WARNING( "HTMLEditor::HandleInsertBRElement() didn't suggest a point to put " "caret"); return Err(NS_ERROR_FAILURE); } nsresult rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } return EditActionResult::HandledResult(); } // We want to collapse selection in the editable block element. blockElementToPutCaret = editableBlockElement; } // If block is empty, populate with br. (For example, imagine a div that // contains the word "text". The user selects "text" and types return. // "Text" is deleted leaving an empty block. We want to put in one br to // make block have a line. Then code further below will put in a second br.) RefPtr insertedPaddingBRElement; if (HTMLEditUtils::IsEmptyBlockElement( *editableBlockElement, {EmptyCheckOption::TreatSingleBRElementAsVisible})) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, EditorDOMPoint::AtEndOf(*editableBlockElement)); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); insertedPaddingBRElement = unwrappedInsertBRElementResult.UnwrapNewNode(); pointToInsert = selectionRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } RefPtr maybeNonEditableListItem = HTMLEditUtils::GetClosestAncestorListItemElement(*editableBlockElement, &aEditingHost); if (maybeNonEditableListItem && HTMLEditUtils::IsSplittableNode(*maybeNonEditableListItem)) { Result insertParagraphInListItemResult = HandleInsertParagraphInListItemElement(*maybeNonEditableListItem, pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertParagraphInListItemResult.isErr())) { if (NS_WARN_IF(insertParagraphInListItemResult.unwrapErr() == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING( "HTMLEditor::HandleInsertParagraphInListItemElement() failed, but " "ignored"); return EditActionResult::HandledResult(); } InsertParagraphResult unwrappedInsertParagraphInListItemResult = insertParagraphInListItemResult.unwrap(); MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.Handled()); MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.GetNewNode()); const RefPtr listItemOrParagraphElement = unwrappedInsertParagraphInListItemResult.UnwrapNewNode(); const EditorDOMPoint pointToPutCaret = unwrappedInsertParagraphInListItemResult.UnwrapCaretPoint(); nsresult rv = CollapseSelection(pointToPutCaret, listItemOrParagraphElement, {SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CollapseSelection() failed, but ignored"); return EditActionResult::HandledResult(); } if (HTMLEditUtils::IsHeader(*editableBlockElement)) { Result insertParagraphInHeadingElementResult = HandleInsertParagraphInHeadingElement(*editableBlockElement, pointToInsert); if (MOZ_UNLIKELY(insertParagraphInHeadingElementResult.isErr())) { NS_WARNING( "HTMLEditor::HandleInsertParagraphInHeadingElement() failed, but " "ignored"); return EditActionResult::HandledResult(); } InsertParagraphResult unwrappedInsertParagraphInHeadingElementResult = insertParagraphInHeadingElementResult.unwrap(); if (unwrappedInsertParagraphInHeadingElementResult.Handled()) { MOZ_ASSERT(unwrappedInsertParagraphInHeadingElementResult.GetNewNode()); blockElementToPutCaret = unwrappedInsertParagraphInHeadingElementResult.UnwrapNewNode(); } const EditorDOMPoint pointToPutCaret = unwrappedInsertParagraphInHeadingElementResult.UnwrapCaretPoint(); nsresult rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CollapseSelection() failed, but ignored"); return EditActionResult::HandledResult(); } // XXX Ideally, we should take same behavior with both

container and //

container. However, we are still using
as default // paragraph separator (non-standard) and we've split only

container // long time. Therefore, some web apps may depend on this behavior like // Gmail. So, let's use traditional odd behavior only when the default // paragraph separator is
. Otherwise, take consistent behavior // between

container and

container. if ((separator == ParagraphSeparator::br && editableBlockElement->IsHTMLElement(nsGkAtoms::p)) || (separator != ParagraphSeparator::br && editableBlockElement->IsAnyOfHTMLElements(nsGkAtoms::p, nsGkAtoms::div))) { // Paragraphs: special rules to look for
s Result splitNodeResult = HandleInsertParagraphInParagraph( *editableBlockElement, insertedPaddingBRElement ? EditorDOMPoint(insertedPaddingBRElement) : pointToInsert, aEditingHost); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertParagraphInParagraph() failed"); return splitNodeResult.propagateErr(); } if (splitNodeResult.inspect().Handled()) { SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap(); const RefPtr rightParagraphElement = unwrappedSplitNodeResult.DidSplit() ? unwrappedSplitNodeResult.GetNextContentAs() : blockElementToPutCaret.get(); const EditorDOMPoint pointToPutCaret = unwrappedSplitNodeResult.UnwrapCaretPoint(); nsresult rv = CollapseSelection(pointToPutCaret, rightParagraphElement, {SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CollapseSelection() failed, but ignored"); return EditActionResult::HandledResult(); } MOZ_ASSERT(!splitNodeResult.inspect().HasCaretPointSuggestion()); // Fall through, if HandleInsertParagraphInParagraph() didn't handle it. MOZ_ASSERT(pointToInsert.IsSetAndValid(), "HTMLEditor::HandleInsertParagraphInParagraph() shouldn't touch " "the DOM tree if it returns not-handled state"); } // If nobody handles this edit action, let's insert new
at the selection. Result insertBRElementResult = HandleInsertBRElement(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); EditorDOMPoint pointToPutCaret = unwrappedInsertBRElementResult.UnwrapCaretPoint(); rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } Result HTMLEditor::HandleInsertBRElement( const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) { MOZ_ASSERT(aPointToBreak.IsSet()); MOZ_ASSERT(IsEditActionDataAvailable()); bool brElementIsAfterBlock = false, brElementIsBeforeBlock = false; // First, insert a
element. RefPtr brElement; if (IsInPlaintextMode()) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, aPointToBreak); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult; } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We'll return with suggesting new caret position and nobody refers // selection after here. So we don't need to update selection here. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); brElement = unwrappedInsertBRElementResult.UnwrapNewNode(); } else { EditorDOMPoint pointToBreak(aPointToBreak); WSRunScanner wsRunScanner(&aEditingHost, pointToBreak); WSScanResult backwardScanResult = wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToBreak); if (MOZ_UNLIKELY(backwardScanResult.Failed())) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary(); WSScanResult forwardScanResult = wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(pointToBreak); if (MOZ_UNLIKELY(forwardScanResult.Failed())) { NS_WARNING( "WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary(); // If the container of the break is a link, we need to split it and // insert new
between the split links. RefPtr linkNode = HTMLEditor::GetLinkElement(pointToBreak.GetContainer()); if (linkNode) { Result splitLinkNodeResult = SplitNodeDeepWithTransaction( *linkNode, pointToBreak, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitLinkNodeResult.isErr())) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::" "eDoNotCreateEmptyContainer) failed"); return splitLinkNodeResult.propagateErr(); } // TODO: Some methods called by // WhiteSpaceVisibilityKeeper::InsertBRElement() use // ComputeEditingHost() which depends on selection. Therefore, // we cannot skip updating selection here. nsresult rv = splitLinkNodeResult.inspect().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } pointToBreak = splitLinkNodeResult.inspect().AtSplitPoint(); } Result insertBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToBreak, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return insertBRElementResult; } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We'll return with suggesting new caret position and nobody refers // selection after here. So we don't need to update selection here. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); brElement = unwrappedInsertBRElementResult.UnwrapNewNode(); MOZ_ASSERT(brElement); } if (MOZ_UNLIKELY(!brElement->GetParentNode())) { NS_WARNING("Inserted
element was removed by the web app"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (brElementIsAfterBlock && brElementIsBeforeBlock) { // We just placed a
between block boundaries. This is the one case // where we want the selection to be before the br we just placed, as the // br will be on a new line, rather than at end of prior line. // XXX brElementIsAfterBlock and brElementIsBeforeBlock were set before // modifying the DOM tree. So, now, the
element may not be // between blocks. EditorDOMPoint pointToPutCaret(brElement, InterlinePosition::StartOfNextLine); return CreateElementResult(std::move(brElement), std::move(pointToPutCaret)); } auto afterBRElement = EditorDOMPoint::After(brElement); WSScanResult forwardScanFromAfterBRElementResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, afterBRElement); if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return Err(NS_ERROR_FAILURE); } if (forwardScanFromAfterBRElementResult.ReachedBRElement()) { // The next thing after the break we inserted is another break. Move the // second break to be the first break's sibling. This will prevent them // from being in different inline nodes, which would break // SetInterlinePosition(). It will also assure that if the user clicks // away and then clicks back on their new blank line, they will still get // the style from the line above. if (brElement->GetNextSibling() != forwardScanFromAfterBRElementResult.BRElementPtr()) { MOZ_ASSERT(forwardScanFromAfterBRElementResult.BRElementPtr()); Result moveBRElementResult = MoveNodeWithTransaction( MOZ_KnownLive( *forwardScanFromAfterBRElementResult.BRElementPtr()), afterBRElement); if (MOZ_UNLIKELY(moveBRElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveBRElementResult.propagateErr(); } nsresult rv = moveBRElementResult.inspect().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "MoveNodeResult::SuggestCaretPointTo() failed, but ignored"); } } // We want the caret to stick to whatever is past the break. This is because // the break is on the same line we were on, but the next content will be on // the following line. // An exception to this is if the break has a next sibling that is a block // node. Then we stick to the left to avoid an uber caret. nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling(); afterBRElement.SetInterlinePosition( nextSiblingOfBRElement && HTMLEditUtils::IsBlockElement(*nextSiblingOfBRElement) ? InterlinePosition::EndOfLine : InterlinePosition::StartOfNextLine); return CreateElementResult(std::move(brElement), afterBRElement); } Result HTMLEditor::HandleInsertLinefeed( const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aPointToBreak.IsSet())) { return Err(NS_ERROR_INVALID_ARG); } const RefPtr document = GetDocument(); MOZ_DIAGNOSTIC_ASSERT(document); if (NS_WARN_IF(!document)) { return Err(NS_ERROR_FAILURE); } // TODO: The following code is duplicated from `HandleInsertText`. They // should be merged when we fix bug 92921. Result setStyleResult = CreateStyleForInsertText(aPointToBreak); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return setStyleResult.propagateErr(); } EditorDOMPoint pointToInsert = setStyleResult.inspect().IsSet() ? setStyleResult.inspect() : aPointToBreak; if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } MOZ_ASSERT(pointToInsert.IsSetAndValid()); // The node may not be able to have a text node so that we need to check it // here. if (!pointToInsert.IsInTextNode() && !HTMLEditUtils::CanNodeContain(*pointToInsert.ContainerAs(), *nsGkAtoms::textTagName)) { NS_WARNING( "HTMLEditor::HandleInsertLinefeed() couldn't insert a linefeed because " "the insertion position couldn't have text nodes"); return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); } AutoRestore disableListener( EditSubActionDataRef().mAdjustChangedRangeFromListener); EditSubActionDataRef().mAdjustChangedRangeFromListener = false; // 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); EditorDOMPoint pointToPutCaret; { AutoTrackDOMPoint trackingInsertingPosition(RangeUpdaterRef(), &pointToInsert); Result insertTextResult = InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult; } pointToPutCaret = insertTextResult.unwrap(); } // Insert a padding
element at the end of the block element if there is // no content between the inserted linefeed and the following block boundary // to make sure that the last line is visible. // XXX Blink/WebKit inserts another linefeed character in this case. However, // for doing it, we need more work, e.g., updating serializer, deleting // unnecessary padding
element at modifying the last line. if (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) { WSRunScanner wsScannerAtCaret(&aEditingHost, pointToPutCaret); if (wsScannerAtCaret.StartsFromPreformattedLineBreak() && wsScannerAtCaret.EndsByBlockBoundary() && HTMLEditUtils::CanNodeContain(*wsScannerAtCaret.GetEndReasonContent(), *nsGkAtoms::br)) { AutoTrackDOMPoint trackingInsertedPosition(RangeUpdaterRef(), &pointToInsert); AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &pointToPutCaret); Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, pointToPutCaret); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } // We're tracking next caret position with newCaretPosition. Therefore, // we don't need to update selection here. insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode()); } } // manually update the doc changed range so that // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct // portion of the document. MOZ_ASSERT(pointToPutCaret.IsSet()); if (NS_WARN_IF(!pointToPutCaret.IsSet())) { // XXX Here is odd. We did mChangedRange->SetStartAndEnd(pointToInsert, // pointToPutCaret), but it always fails because of the latter is unset. // Therefore, always returning NS_ERROR_FAILURE from here is the // traditional behavior... // TODO: Stop updating the interline position of Selection with fixing here // and returning expected point. DebugOnly rvIgnored = SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Selection::SetInterlinePosition(InterlinePosition::" "EndOfLine) failed, but ignored"); if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->CollapseTo( pointToInsert))) { NS_WARNING("nsRange::CollapseTo() failed"); return Err(NS_ERROR_FAILURE); } NS_WARNING( "We always return NS_ERROR_FAILURE here because of a failure of " "updating mChangedRange"); return Err(NS_ERROR_FAILURE); } if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), pointToPutCaret.ToRawRangeBoundary()))) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(NS_ERROR_FAILURE); } pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine); return pointToPutCaret; } Result HTMLEditor::HandleInsertParagraphInMailCiteElement( Element& aMailCiteElement, const EditorDOMPoint& aPointToSplit, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToSplit.IsSet()); NS_ASSERTION(!HTMLEditUtils::IsEmptyNode(aMailCiteElement), "The mail-cite element will be deleted, does it expected result " "for you?"); auto splitCiteElementResult = [&]() MOZ_CAN_RUN_SCRIPT -> Result { EditorDOMPoint pointToSplit(aPointToSplit); // If our selection is just before a break, nudge it to be just after // it. This does two things for us. It saves us the trouble of having // to add a break here ourselves to preserve the "blockness" of the // inline span mailquote (in the inline case), and : it means the break // won't end up making an empty line that happens to be inside a // mailquote (in either inline or block case). The latter can confuse a // user if they click there and start typing, because being in the // mailquote may affect wrapping behavior, or font color, etc. WSScanResult forwardScanFromPointToSplitResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, pointToSplit); if (forwardScanFromPointToSplitResult.Failed()) { return Err(NS_ERROR_FAILURE); } // If selection start point is before a break and it's inside the // mailquote, let's split it after the visible node. if (forwardScanFromPointToSplitResult.ReachedBRElement() && forwardScanFromPointToSplitResult.BRElementPtr() != &aMailCiteElement && aMailCiteElement.Contains( forwardScanFromPointToSplitResult.BRElementPtr())) { pointToSplit = forwardScanFromPointToSplitResult.PointAfterContent(); } if (NS_WARN_IF(!pointToSplit.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } Result splitResult = SplitNodeDeepWithTransaction(aMailCiteElement, pointToSplit, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(aMailCiteElement, " "SplitAtEdges::eDoNotCreateEmptyContainer) failed"); return splitResult; } nsresult rv = splitResult.inspect().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } return splitResult; }(); if (MOZ_UNLIKELY(splitCiteElementResult.isErr())) { NS_WARNING("Failed to split a mail-cite element"); return splitCiteElementResult.propagateErr(); } SplitNodeResult unwrappedSplitCiteElementResult = splitCiteElementResult.unwrap(); // When adding caret suggestion to SplitNodeResult, here didn't change // selection so that just ignore it. unwrappedSplitCiteElementResult.IgnoreCaretPointSuggestion(); // Add an invisible
to the end of left cite node if it was a of // style="display: block". This is important, since when serializing the cite // to plain text, the span which caused the visual break is discarded. So the // added
will guarantee that the serializer will insert a break where the // user saw one. // FYI: unwrappedSplitCiteElementResult grabs the previous node and the next // node with nsCOMPtr or EditorDOMPoint. So, it's safe to access // leftCiteElement and rightCiteElement even after changing the DOM tree // and/or selection even though it's raw pointer. auto* const leftCiteElement = unwrappedSplitCiteElementResult.GetPreviousContentAs(); auto* const rightCiteElement = unwrappedSplitCiteElementResult.GetNextContentAs(); if (leftCiteElement && leftCiteElement->IsHTMLElement(nsGkAtoms::span) && // XXX Oh, this depends on layout information of new element, and it's // created by the hacky flush in DoSplitNode(). So we need to // redesign around this for bug 1710784. leftCiteElement->GetPrimaryFrame() && leftCiteElement->GetPrimaryFrame()->IsBlockFrameOrSubclass()) { nsIContent* lastChild = leftCiteElement->GetLastChild(); if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) { Result insertInvisibleBRElementResult = InsertBRElement(WithTransaction::Yes, EditorDOMPoint::AtEndOf(*leftCiteElement)); if (MOZ_UNLIKELY(insertInvisibleBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertInvisibleBRElementResult.propagateErr(); } // We don't need to update selection here because we'll do another // InsertBRElement call soon. insertInvisibleBRElementResult.inspect().IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertInvisibleBRElementResult.inspect().GetNewNode()); } } // In most cases,
should be inserted after current cite. However, if // left cite hasn't been created because the split point was start of the // cite node,
should be inserted before the current cite. Result insertBRElementResult = InsertBRElement( WithTransaction::Yes, unwrappedSplitCiteElementResult.AtSplitPoint()); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return Err(insertBRElementResult.unwrapErr()); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We'll return with suggesting caret position. Therefore, we don't need // to update selection here. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); // if aMailCiteElement wasn't a block, we might also want another break before // it. We need to examine the content both before the br we just added and // also just after it. If we don't have another br or block boundary // adjacent, then we will need a 2nd br added to achieve blank line that user // expects. if (HTMLEditUtils::IsInlineElement(aMailCiteElement)) { nsresult rvOfInsertingBRElement = [&]() MOZ_CAN_RUN_SCRIPT { EditorDOMPoint pointToCreateNewBRElement( unwrappedInsertBRElementResult.GetNewNode()); // XXX Cannot we replace this complicated check with just a call of // HTMLEditUtils::IsVisibleBRElement with // resultOfInsertingBRElement.inspect()? WSScanResult backwardScanFromPointToCreateNewBRElementResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( &aEditingHost, pointToCreateNewBRElement); if (MOZ_UNLIKELY( backwardScanFromPointToCreateNewBRElementResult.Failed())) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() " "failed"); return NS_ERROR_FAILURE; } if (!backwardScanFromPointToCreateNewBRElementResult .InVisibleOrCollapsibleCharacters() && !backwardScanFromPointToCreateNewBRElementResult .ReachedSpecialContent()) { return NS_SUCCESS_DOM_NO_OPERATION; } WSScanResult forwardScanFromPointAfterNewBRElementResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( &aEditingHost, EditorRawDOMPoint::After(pointToCreateNewBRElement)); if (MOZ_UNLIKELY(forwardScanFromPointAfterNewBRElementResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return NS_ERROR_FAILURE; } if (!forwardScanFromPointAfterNewBRElementResult .InVisibleOrCollapsibleCharacters() && !forwardScanFromPointAfterNewBRElementResult .ReachedSpecialContent() && // In case we're at the very end. !forwardScanFromPointAfterNewBRElementResult .ReachedCurrentBlockBoundary()) { return NS_SUCCESS_DOM_NO_OPERATION; } Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, pointToCreateNewBRElement); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode()); return NS_OK; }(); if (NS_FAILED(rvOfInsertingBRElement)) { NS_WARNING( "Failed to insert additional
element before the inline right " "mail-cite element"); return Err(rvOfInsertingBRElement); } } if (leftCiteElement && HTMLEditUtils::IsEmptyNode(*leftCiteElement)) { // MOZ_KnownLive(leftCiteElement) because it's grabbed by // unwrappedSplitCiteElementResult. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*leftCiteElement)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } if (rightCiteElement && HTMLEditUtils::IsEmptyNode(*rightCiteElement)) { // MOZ_KnownLive(rightCiteElement) because it's grabbed by // unwrappedSplitCiteElementResult. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*rightCiteElement)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } if (MOZ_UNLIKELY(!unwrappedInsertBRElementResult.GetNewNode()->GetParent())) { NS_WARNING("Inserted
shouldn't become an orphan node"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return EditorDOMPoint(unwrappedInsertBRElementResult.GetNewNode()); } HTMLEditor::CharPointData HTMLEditor::GetPreviousCharPointDataForNormalizingWhiteSpaces( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); if (!aPoint.IsStartOfContainer()) { return CharPointData::InSameTextNode( HTMLEditor::GetPreviousCharPointType(aPoint)); } const auto previousCharPoint = WSRunScanner::GetPreviousEditableCharPoint( ComputeEditingHost(), aPoint); if (!previousCharPoint.IsSet()) { return CharPointData::InDifferentTextNode(CharPointType::TextEnd); } return CharPointData::InDifferentTextNode( HTMLEditor::GetCharPointType(previousCharPoint)); } HTMLEditor::CharPointData HTMLEditor::GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); if (!aPoint.IsEndOfContainer()) { return CharPointData::InSameTextNode(HTMLEditor::GetCharPointType(aPoint)); } const auto nextCharPoint = WSRunScanner::GetInclusiveNextEditableCharPoint( ComputeEditingHost(), aPoint); if (!nextCharPoint.IsSet()) { return CharPointData::InDifferentTextNode(CharPointType::TextEnd); } return CharPointData::InDifferentTextNode( HTMLEditor::GetCharPointType(nextCharPoint)); } // static void HTMLEditor::GenerateWhiteSpaceSequence( nsAString& aResult, uint32_t aLength, const CharPointData& aPreviousCharPointData, const CharPointData& aNextCharPointData) { MOZ_ASSERT(aResult.IsEmpty()); MOZ_ASSERT(aLength); // For now, this method does not assume that result will be append to // white-space sequence in the text node. MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() || !aPreviousCharPointData.IsCollapsibleWhiteSpace()); // For now, this method does not assume that the result will be inserted // into white-space sequence nor start of white-space sequence. MOZ_ASSERT(aNextCharPointData.AcrossTextNodeBoundary() || !aNextCharPointData.IsCollapsibleWhiteSpace()); if (aLength == 1) { // Even if previous/next char is in different text node, we should put // an ASCII white-space between visible characters. // XXX This means that this does not allow to put an NBSP in HTML editor // without preformatted style. However, Chrome has same issue too. if (aPreviousCharPointData.Type() == CharPointType::VisibleChar && aNextCharPointData.Type() == CharPointType::VisibleChar) { aResult.Assign(HTMLEditUtils::kSpace); return; } // If it's start or end of text, put an NBSP. if (aPreviousCharPointData.Type() == CharPointType::TextEnd || aNextCharPointData.Type() == CharPointType::TextEnd) { aResult.Assign(HTMLEditUtils::kNBSP); return; } // If the character is next to a preformatted linefeed, we need to put // an NBSP for avoiding collapsed into the linefeed. if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak || aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) { aResult.Assign(HTMLEditUtils::kNBSP); return; } // Now, the white-space will be inserted to a white-space sequence, but not // end of text. We can put an ASCII white-space only when both sides are // not ASCII white-spaces. aResult.Assign( aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace); return; } // Generate pairs of NBSP and ASCII white-space. aResult.SetLength(aLength); bool appendNBSP = true; // Basically, starts with an NBSP. char16_t* lastChar = aResult.EndWriting() - 1; for (char16_t* iter = aResult.BeginWriting(); iter != lastChar; iter++) { *iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; appendNBSP = !appendNBSP; } // If the final one is expected to an NBSP, we can put an NBSP simply. if (appendNBSP) { *lastChar = HTMLEditUtils::kNBSP; return; } // If next char point is end of text node, an ASCII white-space or // preformatted linefeed, we need to put an NBSP. *lastChar = aNextCharPointData.AcrossTextNodeBoundary() || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aNextCharPointData.Type() == CharPointType::PreformattedLineBreak ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; } void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces( EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete, nsAString& aNormalizedWhiteSpacesInStartNode, nsAString& aNormalizedWhiteSpacesInEndNode) const { MOZ_ASSERT(aStartToDelete.IsSetAndValid()); MOZ_ASSERT(aEndToDelete.IsSetAndValid()); MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete)); MOZ_ASSERT(aNormalizedWhiteSpacesInStartNode.IsEmpty()); MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.IsEmpty()); // First, check whether there is surrounding white-spaces or not, and if there // are, check whether they are collapsible or not. Note that we shouldn't // touch white-spaces in different text nodes for performance, but we need // adjacent text node's first or last character information in some cases. Element* editingHost = ComputeEditingHost(); const EditorDOMPointInText precedingCharPoint = WSRunScanner::GetPreviousEditableCharPoint(editingHost, aStartToDelete); const EditorDOMPointInText followingCharPoint = WSRunScanner::GetInclusiveNextEditableCharPoint(editingHost, aEndToDelete); // Blink-compat: Normalize white-spaces in first node only when not removing // its last character or no text nodes follow the first node. // If removing last character of first node and there are // following text nodes, white-spaces in following text node are // normalized instead. const bool removingLastCharOfStartNode = aStartToDelete.ContainerAs() != aEndToDelete.ContainerAs() || (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet()); const bool maybeNormalizePrecedingWhiteSpaces = !removingLastCharOfStartNode && precedingCharPoint.IsSet() && !precedingCharPoint.IsEndOfContainer() && precedingCharPoint.ContainerAs() == aStartToDelete.ContainerAs() && precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); const bool maybeNormalizeFollowingWhiteSpaces = followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() && (followingCharPoint.ContainerAs() == aEndToDelete.ContainerAs() || removingLastCharOfStartNode) && followingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); if (!maybeNormalizePrecedingWhiteSpaces && !maybeNormalizeFollowingWhiteSpaces) { return; // There are no white-spaces. } // Next, consider the range to normalize. EditorDOMPointInText startToNormalize, endToNormalize; if (maybeNormalizePrecedingWhiteSpaces) { Maybe previousCharOffsetOfWhiteSpaces = HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( precedingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); startToNormalize.Set(precedingCharPoint.ContainerAs(), previousCharOffsetOfWhiteSpaces.isSome() ? previousCharOffsetOfWhiteSpaces.value() + 1 : 0); MOZ_ASSERT(!startToNormalize.IsEndOfContainer()); } if (maybeNormalizeFollowingWhiteSpaces) { Maybe nextCharOffsetOfWhiteSpaces = HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( followingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); if (nextCharOffsetOfWhiteSpaces.isSome()) { endToNormalize.Set(followingCharPoint.ContainerAs(), nextCharOffsetOfWhiteSpaces.value()); } else { endToNormalize.SetToEndOf(followingCharPoint.ContainerAs()); } MOZ_ASSERT(!endToNormalize.IsStartOfContainer()); } // Next, retrieve surrounding information of white-space sequence. // If we're removing first text node's last character, we need to // normalize white-spaces starts from another text node. In this case, // we need to lie for avoiding assertion in GenerateWhiteSpaceSequence(). CharPointData previousCharPointData = removingLastCharOfStartNode ? CharPointData::InDifferentTextNode(CharPointType::TextEnd) : GetPreviousCharPointDataForNormalizingWhiteSpaces( startToNormalize.IsSet() ? startToNormalize : aStartToDelete); CharPointData nextCharPointData = GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( endToNormalize.IsSet() ? endToNormalize : aEndToDelete); // Next, compute number of white-spaces in start/end node. uint32_t lengthInStartNode = 0, lengthInEndNode = 0; if (startToNormalize.IsSet()) { MOZ_ASSERT(startToNormalize.ContainerAs() == aStartToDelete.ContainerAs()); lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset(); MOZ_ASSERT(lengthInStartNode); } if (endToNormalize.IsSet()) { lengthInEndNode = endToNormalize.ContainerAs() == aEndToDelete.ContainerAs() ? endToNormalize.Offset() - aEndToDelete.Offset() : endToNormalize.Offset(); MOZ_ASSERT(lengthInEndNode); // If we normalize white-spaces in a text node, we can replace all of them // with one ReplaceTextTransaction. if (endToNormalize.ContainerAs() == aStartToDelete.ContainerAs()) { lengthInStartNode += lengthInEndNode; lengthInEndNode = 0; } } MOZ_ASSERT(lengthInStartNode + lengthInEndNode); // Next, generate normalized white-spaces. if (!lengthInEndNode) { HTMLEditor::GenerateWhiteSpaceSequence( aNormalizedWhiteSpacesInStartNode, lengthInStartNode, previousCharPointData, nextCharPointData); } else if (!lengthInStartNode) { HTMLEditor::GenerateWhiteSpaceSequence( aNormalizedWhiteSpacesInEndNode, lengthInEndNode, previousCharPointData, nextCharPointData); } else { // For making `GenerateWhiteSpaceSequence()` simpler, we should create // whole white-space sequence first, then, copy to the out params. nsAutoString whiteSpaces; HTMLEditor::GenerateWhiteSpaceSequence( whiteSpaces, lengthInStartNode + lengthInEndNode, previousCharPointData, nextCharPointData); aNormalizedWhiteSpacesInStartNode = Substring(whiteSpaces, 0, lengthInStartNode); aNormalizedWhiteSpacesInEndNode = Substring(whiteSpaces, lengthInStartNode); MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.Length() == lengthInEndNode); } // TODO: Shrink the replacing range and string as far as possible because // this may run a lot, i.e., HTMLEditor creates ReplaceTextTransaction // a lot for normalizing white-spaces. Then, each transaction shouldn't // have all white-spaces every time because once it's normalized, we // don't need to normalize all of the sequence again, but currently // we do. // Finally, extend the range. if (startToNormalize.IsSet()) { aStartToDelete = startToNormalize; } if (endToNormalize.IsSet()) { aEndToDelete = endToNormalize; } } Result HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces( const EditorDOMPointInText& aStartToDelete, const EditorDOMPointInText& aEndToDelete, TreatEmptyTextNodes aTreatEmptyTextNodes, DeleteDirection aDeleteDirection) { MOZ_ASSERT(aStartToDelete.IsSetAndValid()); MOZ_ASSERT(aEndToDelete.IsSetAndValid()); MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete)); // Use nsString for these replacing string because we should avoid to copy // the buffer from auto storange to ReplaceTextTransaction. nsString normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode; // First, check whether we need to normalize white-spaces after deleting // the given range. EditorDOMPointInText startToDelete(aStartToDelete); EditorDOMPointInText endToDelete(aEndToDelete); ExtendRangeToDeleteWithNormalizingWhiteSpaces( startToDelete, endToDelete, normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode); // If extended range is still collapsed, i.e., the caller just wants to // normalize white-space sequence, but there is no white-spaces which need to // be replaced, we need to do nothing here. if (startToDelete == endToDelete) { return aStartToDelete.To(); } // Note that the container text node of startToDelete may be removed from // the tree if it becomes empty. Therefore, we need to track the point. EditorDOMPoint newCaretPosition; if (aStartToDelete.ContainerAs() == aEndToDelete.ContainerAs()) { newCaretPosition = aEndToDelete.To(); } else if (aDeleteDirection == DeleteDirection::Forward) { newCaretPosition.SetToEndOf(aStartToDelete.ContainerAs()); } else { newCaretPosition.Set(aEndToDelete.ContainerAs(), 0u); } // Then, modify the text nodes in the range. while (true) { AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); // Use ReplaceTextTransaction if we need to normalize white-spaces in // the first text node. if (!normalizedWhiteSpacesInFirstNode.IsEmpty()) { EditorDOMPoint trackingEndToDelete(endToDelete.ContainerAs(), endToDelete.Offset()); { AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(), &trackingEndToDelete); uint32_t lengthToReplaceInFirstTextNode = startToDelete.ContainerAs() == trackingEndToDelete.ContainerAs() ? trackingEndToDelete.Offset() - startToDelete.Offset() : startToDelete.ContainerAs()->TextLength() - startToDelete.Offset(); nsresult rv = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAs()), startToDelete.Offset(), lengthToReplaceInFirstTextNode, normalizedWhiteSpacesInFirstNode); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return Err(rv); } if (startToDelete.ContainerAs() == trackingEndToDelete.ContainerAs()) { MOZ_ASSERT(normalizedWhiteSpacesInLastNode.IsEmpty()); break; // There is no more text which we need to delete. } } if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) && (NS_WARN_IF(!trackingEndToDelete.IsSetAndValid()) || NS_WARN_IF(!trackingEndToDelete.IsInTextNode()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } MOZ_ASSERT(trackingEndToDelete.IsInTextNode()); endToDelete.Set(trackingEndToDelete.ContainerAs(), trackingEndToDelete.Offset()); // If the remaining range was modified by mutation event listener, // we should stop handling the deletion. startToDelete = EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAs()); if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) && NS_WARN_IF(!startToDelete.IsBefore(endToDelete))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // Delete ASCII whiteSpaces in the range simpley if there are some text // nodes which we don't need to replace their text. if (normalizedWhiteSpacesInLastNode.IsEmpty() || startToDelete.ContainerAs() != endToDelete.ContainerAs()) { // If we need to replace text in the last text node, we should // delete text before its previous text node. EditorDOMPointInText endToDeleteExceptReplaceRange = normalizedWhiteSpacesInLastNode.IsEmpty() ? endToDelete : EditorDOMPointInText(endToDelete.ContainerAs(), 0); if (startToDelete != endToDeleteExceptReplaceRange) { nsresult rv = DeleteTextAndTextNodesWithTransaction( startToDelete, endToDeleteExceptReplaceRange, aTreatEmptyTextNodes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return Err(rv); } if (normalizedWhiteSpacesInLastNode.IsEmpty()) { break; // There is no more text which we need to delete. } if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED | NS_EVENT_BITS_MUTATION_NODEREMOVED | NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT | NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) && (NS_WARN_IF(!endToDeleteExceptReplaceRange.IsSetAndValid()) || NS_WARN_IF(!endToDelete.IsSetAndValid()) || NS_WARN_IF(endToDelete.IsStartOfContainer()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Then, replace the text in the last text node. startToDelete = endToDeleteExceptReplaceRange; } } // Replace ASCII whiteSpaces in the range and following character in the // last text node. MOZ_ASSERT(!normalizedWhiteSpacesInLastNode.IsEmpty()); MOZ_ASSERT(startToDelete.ContainerAs() == endToDelete.ContainerAs()); nsresult rv = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAs()), startToDelete.Offset(), endToDelete.Offset() - startToDelete.Offset(), normalizedWhiteSpacesInLastNode); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return Err(rv); } break; } if (!newCaretPosition.IsSetAndValid() || !newCaretPosition.GetContainer()->IsInComposedDoc()) { NS_WARNING( "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() got lost " "the modifying line"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Look for leaf node to put caret if we remove some empty inline ancestors // at new caret position. if (!newCaretPosition.IsInTextNode()) { if (const Element* editableBlockElementOrInlineEditingHost = HTMLEditUtils::GetInclusiveAncestorElement( *newCaretPosition.ContainerAs(), HTMLEditUtils:: ClosestEditableBlockElementOrInlineEditingHost)) { Element* editingHost = ComputeEditingHost(); // Try to put caret next to immediately after previous editable leaf. nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( newCaretPosition, *editableBlockElementOrInlineEditingHost, {LeafNodeType::LeafNodeOrNonEditableNode}, editingHost); if (previousContent && !HTMLEditUtils::IsBlockElement(*previousContent)) { newCaretPosition = previousContent->IsText() || HTMLEditUtils::IsContainerNode(*previousContent) ? EditorDOMPoint::AtEndOf(*previousContent) : EditorDOMPoint::After(*previousContent); } // But if the point is very first of a block element or immediately after // a child block, look for next editable leaf instead. else if (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( newCaretPosition, *editableBlockElementOrInlineEditingHost, {LeafNodeType::LeafNodeOrNonEditableNode}, editingHost)) { newCaretPosition = nextContent->IsText() || HTMLEditUtils::IsContainerNode(*nextContent) ? EditorDOMPoint(nextContent, 0) : EditorDOMPoint(nextContent); } } } // For compatibility with Blink, we should move caret to end of previous // text node if it's direct previous sibling of the first text node in the // range. if (newCaretPosition.IsStartOfContainer() && newCaretPosition.IsInTextNode() && newCaretPosition.GetContainer()->GetPreviousSibling() && newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) { newCaretPosition.SetToEndOf( newCaretPosition.GetContainer()->GetPreviousSibling()->AsText()); } { AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed"); return Err(rv); } } if (!newCaretPosition.IsSetAndValid()) { NS_WARNING("Inserting
element caused unexpected DOM tree"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return newCaretPosition; } nsresult HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( const EditorDOMPoint& aPointToInsert) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToInsert.IsSet()); if (!aPointToInsert.IsInContentNode()) { return NS_OK; } // If container of the point is not in a block, we don't need to put a // `
` element here. if (!HTMLEditUtils::IsBlockElement( *aPointToInsert.ContainerAs())) { return NS_OK; } WSRunScanner wsRunScanner(ComputeEditingHost(), aPointToInsert); // If the point is not start of a hard line, we don't need to put a `
` // element here. if (!wsRunScanner.StartsFromHardLineBreak()) { return NS_OK; } // If the point is not end of a hard line or the hard line does not end with // block boundary, we don't need to put a `
` element here. if (!wsRunScanner.EndsByBlockBoundary()) { return NS_OK; } // If we cannot insert a `
` element here, do nothing. if (!HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(), *nsGkAtoms::br)) { return NS_OK; } Result insertBRElementResult = InsertBRElement( WithTransaction::Yes, aPointToInsert, nsIEditor::ePrevious); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertBRElement(WithTransaction::Yes, ePrevious) failed"); return insertBRElementResult.unwrapErr(); } nsresult rv = insertBRElementResult.inspect().SuggestCaretPointTo(*this, {}); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "CreateElementResult::SuggestCaretPointTo() failed"); return rv; } Result HTMLEditor::MakeOrChangeListAndListItemAsSubAction( nsAtom& aListElementOrListItemElementTagName, const nsAString& aBulletType, SelectAllOfCurrentList aSelectAllOfCurrentList) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(&aListElementOrListItemElementTagName == nsGkAtoms::ul || &aListElementOrListItemElementTagName == nsGkAtoms::ol || &aListElementOrListItemElementTagName == nsGkAtoms::dl || &aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt); if (NS_WARN_IF(!mInitSucceeded)) { return Err(NS_ERROR_NOT_INITIALIZED); } { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionResult::IgnoredResult(); } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); // XXX EditSubAction::eCreateOrChangeDefinitionListItem and // EditSubAction::eCreateOrChangeList are treated differently in // HTMLEditor::MaybeSplitElementsAtEveryBRElement(). Only when // EditSubAction::eCreateOrChangeList, it splits inline nodes. // Currently, it shouldn't be done when we called for formatting // `
` or `
` by // HTMLEditor::MakeDefinitionListItemWithTransaction(). But this // difference may be a bug. We should investigate this later. IgnoredErrorResult error; AutoEditSubActionNotifier startToHandleEditSubAction( *this, &aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt ? EditSubAction::eCreateOrChangeDefinitionListItem : EditSubAction::eCreateOrChangeList, nsIEditor::eNext, error); if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(error.StealNSResult()); } NS_WARNING_ASSERTION( !error.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } nsAtom* listTagName = nullptr; nsAtom* listItemTagName = nullptr; if (&aListElementOrListItemElementTagName == nsGkAtoms::ul || &aListElementOrListItemElementTagName == nsGkAtoms::ol) { listTagName = &aListElementOrListItemElementTagName; listItemTagName = nsGkAtoms::li; } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dl) { listTagName = &aListElementOrListItemElementTagName; listItemTagName = nsGkAtoms::dd; } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt) { listTagName = nsGkAtoms::dl; listItemTagName = &aListElementOrListItemElementTagName; } else { NS_WARNING( "aListElementOrListItemElementTagName was neither list element name " "nor " "definition listitem element name"); return Err(NS_ERROR_INVALID_ARG); } const RefPtr editingHost = ComputeEditingHost(); if (MOZ_UNLIKELY(!editingHost)) { return EditActionResult::CanceledResult(); } // Expands selection range to include the immediate block parent, and then // further expands to include any ancestors whose children are all in the // range. // XXX Why do we do this only when there is only one selection range? if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( SelectionRef().GetRangeAt(0u), *editingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.propagateErr(); } // Note that end point may be prior to start point. So, we // cannot use Selection::SetStartAndEndInLimit() here. error.SuppressException(); SelectionRef().SetBaseAndExtentInLimiter( extendedRange.inspect().StartRef().ToRawRangeBoundary(), extendedRange.inspect().EndRef().ToRawRangeBoundary(), error); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed"); return Err(error.StealNSResult()); } } AutoRangeArray selectionRanges(SelectionRef()); Result result = ConvertContentAroundRangesToList( selectionRanges, MOZ_KnownLive(*listTagName), MOZ_KnownLive(*listItemTagName), aBulletType, aSelectAllOfCurrentList, *editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::ConvertContentAroundRangesToList() failed"); // XXX Should we try to restore selection ranges in this case? return result; } rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::ApplyTo() failed"); return Err(rv); } return result.inspect().Ignored() ? EditActionResult::CanceledResult() : EditActionResult::HandledResult(); } Result HTMLEditor::ConvertContentAroundRangesToList( AutoRangeArray& aRanges, nsAtom& aListElementTagName, nsAtom& aListItemElementTagName, const nsAString& aBulletType, SelectAllOfCurrentList aSelectAllOfCurrentList, const Element& aEditingHost) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) { return Err(NS_ERROR_FAILURE); } AutoTArray, 64> arrayOfContents; if (Element* parentListElement = aSelectAllOfCurrentList == SelectAllOfCurrentList::Yes ? aRanges.GetClosestAncestorAnyListElementOfRange() : nullptr) { arrayOfContents.AppendElement( OwningNonNull(*parentListElement)); } else { AutoRangeArray extendedRanges(aRanges); // 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); extendedRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( EditSubAction::eCreateOrChangeList, aEditingHost); Result splitResult = extendedRanges .SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries( *this); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries() " "failed"); return splitResult.propagateErr(); } nsresult rv = extendedRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eCreateOrChangeList, AutoRangeArray::CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return Err(rv); } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eCreateOrChangeList); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eCreateOrChangeList) failed"); return splitAtBRElementsResult.propagateErr(); } } // check if all our nodes are
s, or empty inlines auto IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements = [](const nsTArray>& aArrayOfContents) { for (auto& content : aArrayOfContents) { // if content is not a Break or empty inline, we're done if (!content->IsHTMLElement(nsGkAtoms::br) && !HTMLEditUtils::IsEmptyInlineContainer( content, {EmptyCheckOption::TreatSingleBRElementAsVisible})) { return false; } } return true; }; // if no nodes, we make empty list. Ditto if the user tried to make a list // of some # of breaks. if (IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(arrayOfContents)) { // if only breaks, delete them for (auto& content : arrayOfContents) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } const auto firstRangeStartPoint = aRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) { return Err(NS_ERROR_FAILURE); } // Make sure we can put a list here. if (!HTMLEditUtils::CanNodeContain(*firstRangeStartPoint.GetContainer(), aListElementTagName)) { aRanges.RestoreFromSavedRanges(); return EditActionResult::CanceledResult(); } RefPtr newListItemElement; Result createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( aListElementTagName, firstRangeStartPoint, BRElementNextToSplitPoint::Keep, aEditingHost, // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 [&newListItemElement, &aListItemElementTagName]( HTMLEditor& aHTMLEditor, Element& aListElement, const EditorDOMPoint& aPointToInsert) MOZ_CAN_RUN_SCRIPT_BOUNDARY { const auto withTransaction = aListElement.IsInComposedDoc() ? WithTransaction::Yes : WithTransaction::No; Result createNewListItemElementResult = aHTMLEditor.CreateAndInsertElement( withTransaction, aListItemElementTagName, EditorDOMPoint(&aListElement, 0u)); if (MOZ_UNLIKELY(createNewListItemElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::CreateAndInsertElement(%s) failed", ToString(withTransaction).c_str()) .get()); return createNewListItemElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewListItemElementResult = createNewListItemElementResult.unwrap(); // There is AutoSelectionRestorer in this method so that it'll // be restored or updated with making it abort. Therefore, // we don't need to update selection here. // XXX I'd like to check aRanges.HasSavedRanges() here, but it // requires ifdefs to avoid bustage of opt builds caused // by unused warning... unwrappedCreateNewListItemElementResult .IgnoreCaretPointSuggestion(); newListItemElement = unwrappedCreateNewListItemElementResult.UnwrapNewNode(); MOZ_ASSERT(newListItemElement); return NS_OK; }); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(&aListElementTagName).get()) .get()); return createNewListElementResult.propagateErr(); } MOZ_ASSERT(createNewListElementResult.inspect().GetNewNode()); // Put selection in new list item and don't restore the Selection. createNewListElementResult.inspect().IgnoreCaretPointSuggestion(); aRanges.ClearSavedRanges(); nsresult rv = aRanges.Collapse(EditorRawDOMPoint(newListItemElement, 0u)); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(rv); } return EditActionResult::IgnoredResult(); } // if there is only one node in the array, and it is a list, div, or // blockquote, then look inside of it until we find inner list or content. if (arrayOfContents.Length() == 1) { if (Element* deepestDivBlockquoteOrListElement = HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild( arrayOfContents[0], {WalkTreeOption::IgnoreNonEditableNode}, nsGkAtoms::div, nsGkAtoms::blockquote, nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl)) { if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements( nsGkAtoms::div, nsGkAtoms::blockquote)) { arrayOfContents.Clear(); HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement, arrayOfContents, 0, {}); } else { arrayOfContents.ReplaceElementAt( 0, OwningNonNull(*deepestDivBlockquoteOrListElement)); } } } // Ok, now go through all the nodes and put then in the list, // or whatever is appropriate. Wohoo! uint32_t countOfCollectedContents = arrayOfContents.Length(); RefPtr curList, prevListItem, listItemOrListToPutCaret; for (uint32_t i = 0; i < countOfCollectedContents; i++) { // here's where we actually figure out what to do const OwningNonNull content = arrayOfContents[i]; // make sure we don't assemble content that is in different table cells // into the same list. respect table cell boundaries when listifying. if (curList && HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*curList) != HTMLEditUtils::GetInclusiveAncestorAnyTableElement(content)) { curList = nullptr; } // If current node is a `
` element, delete it and forget previous // list item element. // If current node is an empty inline node, just delete it. if (EditorUtils::IsEditableContent(content, EditorType::HTML) && (content->IsHTMLElement(nsGkAtoms::br) || HTMLEditUtils::IsEmptyInlineContainer( content, {EmptyCheckOption::TreatSingleBRElementAsVisible}))) { nsresult rv = DeleteNodeWithTransaction(*content); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } if (content->IsHTMLElement(nsGkAtoms::br)) { prevListItem = nullptr; } continue; } if (HTMLEditUtils::IsAnyListElement(content)) { // If we met a list element and current list element is not a descendant // of the list, append current node to end of the current list element. // Then, wrap it with list item element and delete the old container. if (curList && !EditorUtils::IsDescendantOf(*content, *curList)) { Result moveNodeResult = MoveNodeToEndWithTransaction(*content, *curList); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); moveNodeResult.inspect().IgnoreCaretPointSuggestion(); Result convertListTypeResult = ChangeListElementType(MOZ_KnownLive(*content->AsElement()), aListElementTagName, aListItemElementTagName); if (MOZ_UNLIKELY(convertListTypeResult.isErr())) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return convertListTypeResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); convertListTypeResult.inspect().IgnoreCaretPointSuggestion(); Result unwrapNewListElementResult = RemoveBlockContainerWithTransaction( MOZ_KnownLive(*convertListTypeResult.inspect().GetNewNode())); if (MOZ_UNLIKELY(unwrapNewListElementResult.isErr())) { NS_WARNING( "HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return unwrapNewListElementResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); // So, ignore the suggested point prevListItem = nullptr; continue; } // If current list element is in found list element or we've not met a // list element, convert current list element to proper type. Result convertListTypeResult = ChangeListElementType(MOZ_KnownLive(*content->AsElement()), aListElementTagName, aListItemElementTagName); if (MOZ_UNLIKELY(convertListTypeResult.isErr())) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return convertListTypeResult.propagateErr(); } CreateElementResult unwrappedConvertListTypeResult = convertListTypeResult.unwrap(); MOZ_ASSERT(aRanges.HasSavedRanges()); unwrappedConvertListTypeResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedConvertListTypeResult.GetNewNode()); curList = unwrappedConvertListTypeResult.UnwrapNewNode(); prevListItem = nullptr; continue; } EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(atContent.IsSetAndValid()); if (HTMLEditUtils::IsListItem(content)) { // If current list item element is not in proper list element, we need // to convert the list element. if (!atContent.IsContainerHTMLElement(&aListElementTagName)) { // If we've not met a list element or current node is not in current // list element, insert a list element at current node and set // current list element to the new one. if (!curList || EditorUtils::IsDescendantOf(*content, *curList)) { if (NS_WARN_IF(!atContent.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } Result splitListItemParentResult = SplitNodeWithTransaction(atContent); if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitListItemParentResult.propagateErr(); } SplitNodeResult unwrappedSplitListItemParentResult = splitListItemParentResult.unwrap(); MOZ_ASSERT(unwrappedSplitListItemParentResult.DidSplit()); MOZ_ASSERT(aRanges.HasSavedRanges()); unwrappedSplitListItemParentResult.IgnoreCaretPointSuggestion(); Result createNewListElementResult = CreateAndInsertElement(WithTransaction::Yes, aListElementTagName, unwrappedSplitListItemParentResult .AtNextContent()); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) " "failed"); return createNewListElementResult.propagateErr(); } CreateElementResult unwrapCreateNewListElementResult = createNewListElementResult.unwrap(); MOZ_ASSERT(aRanges.HasSavedRanges()); unwrapCreateNewListElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrapCreateNewListElementResult.GetNewNode()); curList = unwrapCreateNewListElementResult.UnwrapNewNode(); } // Then, move current node into current list element. Result moveNodeResult = MoveNodeToEndWithTransaction(*content, *curList); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); moveNodeResult.inspect().IgnoreCaretPointSuggestion(); // Convert list item type if current node is different list item type. if (!content->IsHTMLElement(&aListItemElementTagName)) { Result newListItemElementOrError = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return newListItemElementOrError.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); newListItemElementOrError.inspect().IgnoreCaretPointSuggestion(); } } else { // If we've not met a list element, set current list element to the // parent of current list item element. if (!curList) { curList = atContent.GetContainerAs(); NS_WARNING_ASSERTION( HTMLEditUtils::IsAnyListElement(curList), "Current list item parent is not a list element"); } // If current list item element is not a child of current list element, // move it into current list item. else if (atContent.GetContainer() != curList) { Result moveNodeResult = MoveNodeToEndWithTransaction(*content, *curList); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); moveNodeResult.inspect().IgnoreCaretPointSuggestion(); } // Then, if current list item element is not proper type for current // list element, convert list item element to proper element. if (!content->IsHTMLElement(&aListItemElementTagName)) { Result newListItemElementOrError = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return newListItemElementOrError.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); newListItemElementOrError.inspect().IgnoreCaretPointSuggestion(); } } Element* element = Element::FromNode(content); if (NS_WARN_IF(!element)) { return Err(NS_ERROR_FAILURE); } // If bullet type is specified, set list type attribute. // XXX Cannot we set type attribute before inserting the list item // element into the DOM tree? if (!aBulletType.IsEmpty()) { nsresult rv = SetAttributeWithTransaction( MOZ_KnownLive(*element), *nsGkAtoms::type, aBulletType); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::SetAttributeWithTransaction(nsGkAtoms::type) " "failed"); return Err(rv); } continue; } // Otherwise, remove list type attribute if there is. if (!element->HasAttr(nsGkAtoms::type)) { continue; } nsresult rv = RemoveAttributeWithTransaction(MOZ_KnownLive(*element), *nsGkAtoms::type); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::type) " "failed"); return Err(rv); } continue; } MOZ_ASSERT(!HTMLEditUtils::IsAnyListElement(content) && !HTMLEditUtils::IsListItem(content)); // If current node is a `
` element, replace it in the array with // its children. // XXX I think that this should be done when we collect the nodes above. // Then, we can change this `for` loop to ranged-for loop. if (content->IsHTMLElement(nsGkAtoms::div)) { prevListItem = nullptr; HTMLEditUtils::CollectChildren( *content, arrayOfContents, i + 1, {CollectChildrenOption::CollectListChildren, CollectChildrenOption::CollectTableChildren}); Result unwrapDivElementResult = RemoveContainerWithTransaction(MOZ_KnownLive(*content->AsElement())); if (MOZ_UNLIKELY(unwrapDivElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed"); return unwrapDivElementResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); // So, ignore the suggested point // Extend the loop length to handle all children collected here. countOfCollectedContents = arrayOfContents.Length(); continue; } // If we've not met a list element, create a list element and make it // current list element. if (!curList) { prevListItem = nullptr; Result createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( aListElementTagName, atContent, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::" "InsertElementWithSplittingAncestorsWithTransaction(%s) failed", nsAtomCString(&aListElementTagName).get()) .get()); return createNewListElementResult.propagateErr(); } CreateElementResult unwrappedCreateNewListElementResult = createNewListElementResult.unwrap(); MOZ_ASSERT(aRanges.HasSavedRanges()); unwrappedCreateNewListElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode()); listItemOrListToPutCaret = unwrappedCreateNewListElementResult.GetNewNode(); curList = unwrappedCreateNewListElementResult.UnwrapNewNode(); // atContent is now referring the right node with mOffset but // referring the left node with mRef. So, invalidate it now. atContent.Clear(); } // If we're currently handling contents of a list item and current node // is not a block element, move current node into the list item. if (HTMLEditUtils::IsInlineElement(content) && prevListItem) { Result moveInlineElementResult = MoveNodeToEndWithTransaction(*content, *prevListItem); if (MOZ_UNLIKELY(moveInlineElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveInlineElementResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); moveInlineElementResult.inspect().IgnoreCaretPointSuggestion(); continue; } // If current node is a paragraph, that means that it does not contain // block children so that we can just replace it with new list item // element and move it into current list element. // XXX This is too rough handling. If web apps modifies DOM tree directly, // any elements can have block elements as children. if (content->IsHTMLElement(nsGkAtoms::p)) { Result newListItemElementOrError = ReplaceContainerWithTransaction(MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return newListItemElementOrError.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); newListItemElementOrError.inspect().IgnoreCaretPointSuggestion(); MOZ_ASSERT(newListItemElementOrError.inspect().GetNewNode()); Result moveListItemElementResult = MoveNodeToEndWithTransaction( MOZ_KnownLive(*newListItemElementOrError.inspect().GetNewNode()), *curList); if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveListItemElementResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); moveListItemElementResult.inspect().IgnoreCaretPointSuggestion(); prevListItem = nullptr; // XXX Why don't we set `type` attribute here?? continue; } // If current node is not a paragraph, wrap current node with new list // item element and move it into current list element. Result wrapContentInListItemElementResult = InsertContainerWithTransaction(*content, aListItemElementTagName); if (MOZ_UNLIKELY(wrapContentInListItemElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return wrapContentInListItemElementResult.propagateErr(); } CreateElementResult unwrappedWrapContentInListItemElementResult = wrapContentInListItemElementResult.unwrap(); MOZ_ASSERT(aRanges.HasSavedRanges()); unwrappedWrapContentInListItemElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedWrapContentInListItemElementResult.GetNewNode()); // MOZ_KnownLive(unwrappedWrapContentInListItemElementResult.GetNewNode()): // The result is grabbed by unwrappedWrapContentInListItemElementResult. Result moveListItemElementResult = MoveNodeToEndWithTransaction( MOZ_KnownLive( *unwrappedWrapContentInListItemElementResult.GetNewNode()), *curList); if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveListItemElementResult.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); moveListItemElementResult.inspect().IgnoreCaretPointSuggestion(); // If current node is not a block element, new list item should have // following inline nodes too. if (HTMLEditUtils::IsInlineElement(content)) { prevListItem = unwrappedWrapContentInListItemElementResult.UnwrapNewNode(); } else { prevListItem = nullptr; } // XXX Why don't we set `type` attribute here?? } MOZ_ASSERT(aRanges.HasSavedRanges()); aRanges.RestoreFromSavedRanges(); // If selection will be collapsed but not in listItemOrListToPutCaret, we need // to adjust the caret position into it. if (listItemOrListToPutCaret && aRanges.IsCollapsed() && aRanges.Ranges().Length()) { const auto firstRangeStartPoint = aRanges.GetFirstRangeStartPoint(); if (MOZ_LIKELY(firstRangeStartPoint.IsSet())) { const Result pointToPutCaretOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorRawDOMPoint>(*listItemOrListToPutCaret, firstRangeStartPoint); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING( "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() " "failed, but ignored"); } else if (pointToPutCaretOrError.inspect().IsSet()) { DebugOnly rvIgnored = aRanges.Collapse(pointToPutCaretOrError.inspect()); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::CollapseSelectionTo() failed, but ignored"); } } } return EditActionResult::HandledResult(); } nsresult HTMLEditor::RemoveListAtSelectionAsSubAction( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult error; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eRemoveList, nsIEditor::eNext, error); if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return error.StealNSResult(); } NS_WARNING_ASSERTION( !error.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); // XXX Why do we do this only when there is only one selection range? if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( SelectionRef().GetRangeAt(0u), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.unwrapErr(); } // Note that end point may be prior to start point. So, we // cannot use Selection::SetStartAndEndInLimit() here. error.SuppressException(); SelectionRef().SetBaseAndExtentInLimiter( extendedRange.inspect().StartRef().ToRawRangeBoundary(), extendedRange.inspect().EndRef().ToRawRangeBoundary(), error); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (error.Failed()) { NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed"); return error.StealNSResult(); } } AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; { // 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); { AutoRangeArray extendedSelectionRanges(SelectionRef()); extendedSelectionRanges .ExtendRangesToWrapLinesToHandleBlockLevelEditAction( EditSubAction::eCreateOrChangeList, aEditingHost); Result splitResult = extendedSelectionRanges .SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries( *this); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries()" " failed"); return splitResult.unwrapErr(); } nsresult rv = extendedSelectionRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eCreateOrChangeList, AutoRangeArray::CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return rv; } } const Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eCreateOrChangeList); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eCreateOrChangeList) failed"); return splitAtBRElementsResult.inspectErr(); } } // Remove all non-editable nodes. Leave them be. // XXX CollectEditTargetNodes() should return only editable contents when it's // called with CollectNonEditableNodes::No, but checking it here, looks // like just wasting the runtime cost. for (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) { OwningNonNull& content = arrayOfContents[i]; if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { arrayOfContents.RemoveElementAt(i); } } // Only act on lists or list items in the array for (auto& content : arrayOfContents) { // here's where we actually figure out what to do if (HTMLEditUtils::IsListItem(content)) { // unlist this listitem nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()), LiftUpFromAllParentListElements::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:" ":Yes) failed"); return rv; } continue; } if (HTMLEditUtils::IsAnyListElement(content)) { // node is a list, move list items out nsresult rv = DestroyListStructureRecursively(MOZ_KnownLive(*content->AsElement())); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed"); return rv; } continue; } } return NS_OK; } Result, nsresult> HTMLEditor::FormatBlockContainerWithTransaction( AutoRangeArray& aSelectionRanges, nsAtom& blockType, const Element& aEditingHost) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); // XXX Why do we do this only when there is only one selection range? if (!aSelectionRanges.IsCollapsed() && aSelectionRanges.Ranges().Length() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( aSelectionRanges.FirstRangeRef(), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.propagateErr(); } // Note that end point may be prior to start point. So, we // cannot use AutoRangeArray::SetStartAndEnd() here. if (NS_FAILED(aSelectionRanges.SetBaseAndExtent( extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef()))) { NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed"); return Err(NS_ERROR_FAILURE); } } MOZ_ALWAYS_TRUE(aSelectionRanges.SaveAndTrackRanges(*this)); // 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); AutoTArray, 64> arrayOfContents; aSelectionRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( EditSubAction::eCreateOrRemoveBlock, aEditingHost); Result splitResult = aSelectionRanges .SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries( *this); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries() " "failed"); return splitResult.propagateErr(); } nsresult rv = aSelectionRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eCreateOrRemoveBlock, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" "eCreateOrRemoveBlock, CollectNonEditableNodes::No) failed"); return Err(rv); } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eCreateOrRemoveBlock); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eCreateOrRemoveBlock) failed"); return splitAtBRElementsResult.propagateErr(); } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (HTMLEditUtils::IsEmptyOneHardLine(arrayOfContents)) { if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) { return Err(NS_ERROR_FAILURE); } auto pointToInsertBlock = aSelectionRanges.GetFirstRangeStartPoint(); if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { if (!pointToInsertBlock.IsInContentNode()) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent because container of the point is not content"); return Err(NS_ERROR_FAILURE); } // We are removing blocks (going to "body text") const RefPtr editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *pointToInsertBlock.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement); if (!editableBlockElement) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent"); return Err(NS_ERROR_FAILURE); } if (!HTMLEditUtils::IsFormatNode(editableBlockElement)) { return RefPtr(); } // If the first editable node after selection is a br, consume it. // Otherwise it gets pushed into a following block after the split, // which is visually bad. if (nsCOMPtr brContent = HTMLEditUtils::GetNextContent( pointToInsertBlock, {WalkTreeOption::IgnoreNonEditableNode}, &aEditingHost)) { if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); nsresult rv = DeleteNodeWithTransaction(*brContent); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } } // Do the splits! Result splitNodeResult = SplitNodeDeepWithTransaction( *editableBlockElement, pointToInsertBlock, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitNodeResult.propagateErr(); } SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap(); unwrappedSplitNodeResult.IgnoreCaretPointSuggestion(); // Put a
element at the split point Result insertBRElementResult = InsertBRElement( WithTransaction::Yes, unwrappedSplitNodeResult.AtSplitPoint()); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode()); aSelectionRanges.ClearSavedRanges(); nsresult rv = aSelectionRanges.Collapse( EditorRawDOMPoint(insertBRElementResult.inspect().GetNewNode())); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(rv); } return RefPtr(); } // We are making a block. Consume a br, if needed. if (nsCOMPtr maybeBRContent = HTMLEditUtils::GetNextContent( pointToInsertBlock, {WalkTreeOption::IgnoreNonEditableNode, WalkTreeOption::StopAtBlockBoundary}, &aEditingHost)) { if (maybeBRContent->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); nsresult rv = DeleteNodeWithTransaction(*maybeBRContent); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } // We don't need to act on this node any more arrayOfContents.RemoveElement(maybeBRContent); } } // Make sure we can put a block here. Result createNewBlockElementResult = InsertElementWithSplittingAncestorsWithTransaction( blockType, pointToInsertBlock, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(&blockType).get()) .get()); return createNewBlockElementResult.propagateErr(); } CreateElementResult unwrappedCreateNewBlockElementResult = createNewBlockElementResult.unwrap(); unwrappedCreateNewBlockElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode()); // Delete anything that was in the list of nodes while (!arrayOfContents.IsEmpty()) { OwningNonNull& content = arrayOfContents[0]; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } arrayOfContents.RemoveElementAt(0); } // Put selection in new block aSelectionRanges.ClearSavedRanges(); nsresult rv = aSelectionRanges.Collapse(EditorRawDOMPoint( unwrappedCreateNewBlockElementResult.GetNewNode(), 0u)); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(rv); } return unwrappedCreateNewBlockElementResult.UnwrapNewNode(); } // Okay, now go through all the nodes and make the right kind of blocks, or // whatever is appropriate. // Note: blockquote is handled a little differently. if (&blockType == nsGkAtoms::blockquote) { Result wrapContentsInBlockquoteElementsResult = WrapContentsInBlockquoteElementsWithTransaction(arrayOfContents, aEditingHost); if (MOZ_UNLIKELY(wrapContentsInBlockquoteElementsResult.isErr())) { NS_WARNING( "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() " "failed"); return wrapContentsInBlockquoteElementsResult.propagateErr(); } wrapContentsInBlockquoteElementsResult.inspect() .IgnoreCaretPointSuggestion(); return wrapContentsInBlockquoteElementsResult.unwrap().UnwrapNewNode(); } if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { Result removeBlockContainerElementsResult = RemoveBlockContainerElementsWithTransaction(arrayOfContents); if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) { NS_WARNING( "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed"); return removeBlockContainerElementsResult.propagateErr(); } return RefPtr(); } Result wrapContentsInBlockElementResult = CreateOrChangeBlockContainerElement(arrayOfContents, blockType, aEditingHost); if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) { NS_WARNING("HTMLEditor::CreateOrChangeBlockContainerElement() failed"); return wrapContentsInBlockElementResult.propagateErr(); } wrapContentsInBlockElementResult.inspect().IgnoreCaretPointSuggestion(); return wrapContentsInBlockElementResult.unwrap().UnwrapNewNode(); } nsresult HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRef().IsCollapsed()) { return NS_OK; } const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } if (!atStartOfSelection.Container()->IsElement()) { return NS_OK; } OwningNonNull startContainerElement = *atStartOfSelection.Container()->AsElement(); nsresult rv = InsertPaddingBRElementForEmptyLastLineIfNeeded(startContainerElement); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded() failed"); return rv; } Result HTMLEditor::IndentAsSubAction( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eIndent, nsIEditor::eNext, 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"); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionResult::IgnoredResult(); } Result result = HandleIndentAtSelection(aEditingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::HandleIndentAtSelection() failed"); return result; } if (result.inspect().Canceled()) { return result; } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Mutation event listener might have changed selection"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // TODO: Investigate when we need to put a `
` element after indenting // ranges. Then, we could stop calling this here, or maybe we need to // do it while moving content nodes. nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); if (NS_FAILED(rv)) { NS_WARNING( "MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() failed"); return Err(rv); } return result; } Result HTMLEditor::IndentListChildWithTransaction( RefPtr* aSubListElement, const EditorDOMPoint& aPointInListElement, nsIContent& aContentMovingToSubList, const Element& aEditingHost) { MOZ_ASSERT( HTMLEditUtils::IsAnyListElement(aPointInListElement.GetContainer()), "unexpected container"); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); // some logic for putting list items into nested lists... // If aContentMovingToSubList is followed by a sub-list element whose tag is // same as the parent list element's tag, we can move it to start of the // sub-list. if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling( aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode})) { if (HTMLEditUtils::IsAnyListElement(nextEditableSibling) && aPointInListElement.GetContainer()->NodeInfo()->NameAtom() == nextEditableSibling->NodeInfo()->NameAtom() && aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() == nextEditableSibling->NodeInfo()->NamespaceID()) { Result moveListElementResult = MoveNodeWithTransaction(aContentMovingToSubList, EditorDOMPoint(nextEditableSibling, 0u)); if (MOZ_UNLIKELY(moveListElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveListElementResult.propagateErr(); } return moveListElementResult.unwrap().UnwrapCaretPoint(); } } // If aContentMovingToSubList follows a sub-list element whose tag is same // as the parent list element's tag, we can move it to end of the sub-list. if (nsCOMPtr previousEditableSibling = HTMLEditUtils::GetPreviousSibling( aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode})) { if (HTMLEditUtils::IsAnyListElement(previousEditableSibling) && aPointInListElement.GetContainer()->NodeInfo()->NameAtom() == previousEditableSibling->NodeInfo()->NameAtom() && aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() == previousEditableSibling->NodeInfo()->NamespaceID()) { Result moveListElementResult = MoveNodeToEndWithTransaction(aContentMovingToSubList, *previousEditableSibling); if (MOZ_UNLIKELY(moveListElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveListElementResult.propagateErr(); } return moveListElementResult.unwrap().UnwrapCaretPoint(); } } // If aContentMovingToSubList does not follow aSubListElement, we need // to create new sub-list element. EditorDOMPoint pointToPutCaret; nsIContent* previousEditableSibling = *aSubListElement ? HTMLEditUtils::GetPreviousSibling( aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode}) : nullptr; if (!*aSubListElement || (previousEditableSibling && previousEditableSibling != *aSubListElement)) { nsAtom* containerName = aPointInListElement.GetContainer()->NodeInfo()->NameAtom(); // Create a new nested list of correct type. Result createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( MOZ_KnownLive(*containerName), aPointInListElement, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(containerName).get()) .get()); return createNewListElementResult.propagateErr(); } CreateElementResult unwrappedCreateNewListElementResult = createNewListElementResult.unwrap(); MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode()); pointToPutCaret = unwrappedCreateNewListElementResult.UnwrapCaretPoint(); *aSubListElement = unwrappedCreateNewListElementResult.UnwrapNewNode(); } // Finally, we should move aContentMovingToSubList into aSubListElement. const RefPtr subListElement = *aSubListElement; Result moveNodeResult = MoveNodeToEndWithTransaction(aContentMovingToSubList, *subListElement); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint(); } return pointToPutCaret; } Result HTMLEditor::HandleIndentAtSelection( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } AutoRangeArray selectionRanges(SelectionRef()); if (MOZ_UNLIKELY(!selectionRanges.IsInContent())) { NS_WARNING("Mutation event listener might have changed the selection"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (IsCSSEnabled()) { nsresult rv = HandleCSSIndentAroundRanges(selectionRanges, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::HandleCSSIndentAroundRanges() failed"); return Err(rv); } } else { nsresult rv = HandleHTMLIndentAroundRanges(selectionRanges, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::HandleHTMLIndentAroundRanges() failed"); return Err(rv); } } rv = selectionRanges.ApplyTo(SelectionRef()); if (MOZ_UNLIKELY(Destroyed())) { NS_WARNING("AutoRangeArray::ApplyTo() caused destroying the editor"); return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::ApplyTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } nsresult HTMLEditor::HandleCSSIndentAroundRanges(AutoRangeArray& aRanges, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!aRanges.Ranges().IsEmpty()); MOZ_ASSERT(aRanges.IsInContent()); if (aRanges.Ranges().IsEmpty()) { NS_WARNING("There is no selection range"); return NS_ERROR_FAILURE; } // XXX Why do we do this only when there is only one selection range? if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( aRanges.FirstRangeRef(), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.unwrapErr(); } // Note that end point may be prior to start point. So, we // cannot use SetStartAndEnd() here. nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef()); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed"); return rv; } } if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) { return NS_ERROR_FAILURE; } AutoTArray, 64> arrayOfContents; // short circuit: detect case of collapsed selection inside an
  • . // just sublist that
  • . This prevents bug 97797. if (aRanges.IsCollapsed()) { const auto atCaret = aRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!atCaret.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atCaret.IsInContentNode()); Element* const editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *atCaret.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement); if (editableBlockElement && HTMLEditUtils::IsListItem(editableBlockElement)) { arrayOfContents.AppendElement(*editableBlockElement); } } EditorDOMPoint pointToPutCaret; if (arrayOfContents.IsEmpty()) { { AutoRangeArray extendedRanges(aRanges); extendedRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( EditSubAction::eIndent, aEditingHost); Result splitResult = extendedRanges .SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries( *this); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries()" " failed"); return splitResult.unwrapErr(); } if (splitResult.inspect().IsSet()) { pointToPutCaret = splitResult.unwrap(); } nsresult rv = extendedRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eIndent, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, " "CollectNonEditableNodes::Yes) failed"); return rv; } } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eIndent); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eIndent) failed"); return splitAtBRElementsResult.inspectErr(); } if (splitAtBRElementsResult.inspect().IsSet()) { pointToPutCaret = splitAtBRElementsResult.unwrap(); } } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (HTMLEditUtils::IsEmptyOneHardLine(arrayOfContents)) { const EditorDOMPoint pointToInsertDivElement = pointToPutCaret.IsSet() ? std::move(pointToPutCaret) : aRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsertDivElement.IsSet())) { return NS_ERROR_FAILURE; } // make sure we can put a block here Result createNewDivElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::div, pointToInsertDivElement, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::div) failed"); return createNewDivElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewDivElementResult = createNewDivElementResult.unwrap(); // We'll collapse ranges below, so we don't need to touch the ranges here. unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion(); const RefPtr newDivElement = unwrappedCreateNewDivElementResult.UnwrapNewNode(); MOZ_ASSERT(newDivElement); const Result pointToPutCaretOrError = ChangeMarginStart(*newDivElement, ChangeMargin::Increase, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but " "ignored"); } // delete anything that was in the list of nodes // XXX We don't need to remove the nodes from the array for performance. for (const OwningNonNull& content : arrayOfContents) { // MOZ_KnownLive(content) due to bug 1622253 nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(content)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } aRanges.ClearSavedRanges(); nsresult rv = aRanges.Collapse(EditorDOMPoint(newDivElement, 0u)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed"); return rv; } RefPtr latestNewBlockElement; auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside = [&]() -> nsresult { MOZ_ASSERT(aRanges.HasSavedRanges()); aRanges.RestoreFromSavedRanges(); if (!latestNewBlockElement || !aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) { return NS_OK; } const auto firstRangeStartRawPoint = aRanges.GetFirstRangeStartPoint(); if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) { return NS_OK; } Result pointInNewBlockElementOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint); if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) { NS_WARNING( "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, " "but ignored"); return NS_OK; } if (!pointInNewBlockElementOrError.inspect().IsSet()) { return NS_OK; } return aRanges.Collapse(pointInNewBlockElementOrError.unwrap()); }; // Ok, now go through all the nodes and put them into sub-list element // elements and new
    elements which have start margin. RefPtr subListElement, divElement; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do. EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { continue; } // Ignore all non-editable nodes. Leave them be. // XXX We ignore non-editable nodes here, but not so in the above block. if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { continue; } if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) { const RefPtr oldSubListElement = subListElement; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result pointToPutCaretOrError = IndentListChildWithTransaction(&subListElement, atContent, MOZ_KnownLive(content), aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed"); return pointToPutCaretOrError.unwrapErr(); } if (subListElement != oldSubListElement) { // New list element is created, so we should put caret into the new list // element. latestNewBlockElement = subListElement; } if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } continue; } // Not a list item. if (HTMLEditUtils::IsBlockElement(content)) { Result pointToPutCaretOrError = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Increase, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but " "ignored"); } else if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } divElement = nullptr; continue; } if (!divElement) { // First, check that our element can contain a div. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::div)) { // XXX This is odd, why do we stop indenting remaining content nodes? // Perhaps, `continue` is better. nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElement" "IfOutside() failed"); return rv; } Result createNewDivElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::div) failed"); return createNewDivElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewDivElementResult = createNewDivElementResult.unwrap(); pointToPutCaret = unwrappedCreateNewDivElementResult.UnwrapCaretPoint(); MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode()); divElement = unwrappedCreateNewDivElementResult.UnwrapNewNode(); Result pointToPutCaretOrError = ChangeMarginStart(*divElement, ChangeMargin::Increase, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but " "ignored"); } else if (AllowsTransactionsToChangeSelection() && pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } latestNewBlockElement = divElement; } // Move the content into the
    which has start margin. // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result moveNodeResult = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *divElement); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.unwrapErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint(); } } nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed"); return rv; } nsresult HTMLEditor::HandleHTMLIndentAroundRanges(AutoRangeArray& aRanges, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!aRanges.Ranges().IsEmpty()); MOZ_ASSERT(aRanges.IsInContent()); // XXX Why do we do this only when there is only one range? if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( aRanges.FirstRangeRef(), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.unwrapErr(); } // Note that end point may be prior to start point. So, we cannot use // SetStartAndEnd() here. nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef()); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed"); return rv; } } if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) { return NS_ERROR_FAILURE; } EditorDOMPoint pointToPutCaret; // convert the selection ranges into "promoted" selection ranges: // this basically just expands the range to include the immediate // block parent, and then further expands to include any ancestors // whose children are all in the range // use these ranges to construct a list of nodes to act on. AutoTArray, 64> arrayOfContents; { AutoRangeArray extendedRanges(aRanges); extendedRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( EditSubAction::eIndent, aEditingHost); Result splitResult = extendedRanges .SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries( *this); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries() " "failed"); return splitResult.unwrapErr(); } if (splitResult.inspect().IsSet()) { pointToPutCaret = splitResult.unwrap(); } nsresult rv = extendedRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eIndent, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, " "CollectNonEditableNodes::Yes) failed"); return rv; } } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eIndent); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::eIndent)" " failed"); return splitAtBRElementsResult.inspectErr(); } if (splitAtBRElementsResult.inspect().IsSet()) { pointToPutCaret = splitAtBRElementsResult.unwrap(); } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (HTMLEditUtils::IsEmptyOneHardLine(arrayOfContents)) { const EditorDOMPoint pointToInsertBlockquoteElement = pointToPutCaret.IsSet() ? std::move(pointToPutCaret) : EditorBase::GetFirstSelectionStartPoint(); if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) { return NS_ERROR_FAILURE; } // Make sure we can put a block here. Result createNewBlockquoteElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::blockquote, pointToInsertBlockquoteElement, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::blockquote) failed"); return createNewBlockquoteElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewBlockquoteElementResult = createNewBlockquoteElementResult.unwrap(); unwrappedCreateNewBlockquoteElementResult.IgnoreCaretPointSuggestion(); RefPtr newBlockquoteElement = unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode(); MOZ_ASSERT(newBlockquoteElement); // delete anything that was in the list of nodes // XXX We don't need to remove the nodes from the array for performance. for (const OwningNonNull& content : arrayOfContents) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } aRanges.ClearSavedRanges(); nsresult rv = aRanges.Collapse(EditorRawDOMPoint(newBlockquoteElement, 0u)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return rv; } RefPtr latestNewBlockElement; auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside = [&]() -> nsresult { MOZ_ASSERT(aRanges.HasSavedRanges()); aRanges.RestoreFromSavedRanges(); if (!latestNewBlockElement || !aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) { return NS_OK; } const auto firstRangeStartRawPoint = aRanges.GetFirstRangeStartPoint(); if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) { return NS_OK; } Result pointInNewBlockElementOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint); if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) { NS_WARNING( "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, " "but ignored"); return NS_OK; } if (!pointInNewBlockElementOrError.inspect().IsSet()) { return NS_OK; } return aRanges.Collapse(pointInNewBlockElementOrError.unwrap()); }; // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. Wohoo! RefPtr subListElement, blockquoteElement, indentedListItemElement; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do. EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { continue; } // Ignore all non-editable nodes. Leave them be. // XXX We ignore non-editable nodes here, but not so in the above block. if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { continue; } if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) { const RefPtr oldSubListElement = subListElement; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result pointToPutCaretOrError = IndentListChildWithTransaction(&subListElement, atContent, MOZ_KnownLive(content), aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed"); return pointToPutCaretOrError.unwrapErr(); } if (oldSubListElement != subListElement) { // New list element is created, so we should put caret into the new list // element. latestNewBlockElement = subListElement; } if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } blockquoteElement = nullptr; continue; } // Not a list item, use blockquote? // if we are inside a list item, we don't want to blockquote, we want // to sublist the list item. We may have several nodes listed in the // array of nodes to act on, that are in the same list item. Since // we only want to indent that li once, we must keep track of the most // recent indented list item, and not indent it if we find another node // to act on that is still inside the same li. if (RefPtr listItem = HTMLEditUtils::GetClosestAncestorListItemElement(content, &aEditingHost)) { if (indentedListItemElement == listItem) { // already indented this list item continue; } // check to see if subListElement is still appropriate. Which it is if // content is still right after it in the same list. nsIContent* previousEditableSibling = subListElement ? HTMLEditUtils::GetPreviousSibling( *listItem, {WalkTreeOption::IgnoreNonEditableNode}) : nullptr; if (!subListElement || (previousEditableSibling && previousEditableSibling != subListElement)) { EditorDOMPoint atListItem(listItem); if (NS_WARN_IF(!listItem)) { return NS_ERROR_FAILURE; } nsAtom* containerName = atListItem.GetContainer()->NodeInfo()->NameAtom(); // Create a new nested list of correct type. Result createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( MOZ_KnownLive(*containerName), atListItem, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING(nsPrintfCString("HTMLEditor::" "InsertElementWithSplittingAncestorsWithTr" "ansaction(%s) failed", nsAtomCString(containerName).get()) .get()); return createNewListElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewListElementResult = createNewListElementResult.unwrap(); if (unwrappedCreateNewListElementResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedCreateNewListElementResult.UnwrapCaretPoint(); } MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode()); subListElement = unwrappedCreateNewListElementResult.UnwrapNewNode(); } Result moveListItemElementResult = MoveNodeToEndWithTransaction(*listItem, *subListElement); if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveListItemElementResult.unwrapErr(); } MoveNodeResult unwrappedMoveListItemElementResult = moveListItemElementResult.unwrap(); if (unwrappedMoveListItemElementResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveListItemElementResult.UnwrapCaretPoint(); } // Remember the list item element which we indented now for ignoring its // children to avoid using
    in it. indentedListItemElement = std::move(listItem); continue; } // need to make a blockquote to put things in if we haven't already, // or if this node doesn't go in blockquote we used earlier. // One reason it might not go in prio blockquote is if we are now // in a different table cell. if (blockquoteElement && HTMLEditUtils::GetInclusiveAncestorAnyTableElement( *blockquoteElement) != HTMLEditUtils::GetInclusiveAncestorAnyTableElement(content)) { blockquoteElement = nullptr; } if (!blockquoteElement) { // First, check that our element can contain a blockquote. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::blockquote)) { // XXX This is odd, why do we stop indenting remaining content nodes? // Perhaps, `continue` is better. nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElement" "IfOutside() failed"); return rv; } Result createNewBlockquoteElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::blockquote, atContent, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::blockquote) failed"); return createNewBlockquoteElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewBlockquoteElementResult = createNewBlockquoteElementResult.unwrap(); if (unwrappedCreateNewBlockquoteElementResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedCreateNewBlockquoteElementResult.UnwrapCaretPoint(); } MOZ_ASSERT(unwrappedCreateNewBlockquoteElementResult.GetNewNode()); blockquoteElement = unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode(); latestNewBlockElement = blockquoteElement; } // tuck the node into the end of the active blockquote // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result moveNodeResult = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *blockquoteElement); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.unwrapErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint(); } subListElement = nullptr; } nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed"); return rv; } Result HTMLEditor::OutdentAsSubAction( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eOutdent, nsIEditor::eNext, 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"); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionResult::IgnoredResult(); } Result result = HandleOutdentAtSelection(aEditingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::HandleOutdentAtSelection() failed"); return result; } if (result.inspect().Canceled()) { return result; } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Mutation event listener might have changed the selection"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() " "failed"); return Err(rv); } return result; } Result HTMLEditor::HandleOutdentAtSelection( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); // XXX Why do we do this only when there is only one selection range? if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( SelectionRef().GetRangeAt(0u), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.propagateErr(); } // Note that end point may be prior to start point. So, we // cannot use Selection::SetStartAndEndInLimit() here. IgnoredErrorResult error; SelectionRef().SetBaseAndExtentInLimiter( extendedRange.inspect().StartRef().ToRawRangeBoundary(), extendedRange.inspect().EndRef().ToRawRangeBoundary(), error); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed"); return Err(error.StealNSResult()); } } // HandleOutdentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, the editor might have been destroyed // at restoring Selection. Result outdentResult = HandleOutdentAtSelectionInternal(aEditingHost); MOZ_ASSERT_IF(outdentResult.isOk(), !outdentResult.inspect().HasCaretPointSuggestion()); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::HandleOutdentAtSelectionInternal() failed"); return outdentResult.propagateErr(); } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); // Make sure selection didn't stick to last piece of content in old bq (only // a problem for collapsed selections) if (!unwrappedOutdentResult.GetLeftContent() && !unwrappedOutdentResult.GetRightContent()) { return EditActionResult::HandledResult(); } if (!SelectionRef().IsCollapsed()) { return EditActionResult::HandledResult(); } // Push selection past end of left element of last split indented element. if (unwrappedOutdentResult.GetLeftContent()) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionResult::HandledResult(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return Err(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == unwrappedOutdentResult.GetLeftContent() || EditorUtils::IsDescendantOf(*atStartOfSelection.Container(), *unwrappedOutdentResult.GetLeftContent())) { // Selection is inside the left node - push it past it. EditorRawDOMPoint afterRememberedLeftBQ( EditorRawDOMPoint::After(*unwrappedOutdentResult.GetLeftContent())); NS_WARNING_ASSERTION( afterRememberedLeftBQ.IsSet(), "Failed to set after remembered left blockquote element"); nsresult rv = CollapseSelectionTo(afterRememberedLeftBQ); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); } } // And pull selection before beginning of right element of last split // indented element. if (unwrappedOutdentResult.GetRightContent()) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionResult::HandledResult(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return Err(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == unwrappedOutdentResult.GetRightContent() || EditorUtils::IsDescendantOf( *atStartOfSelection.Container(), *unwrappedOutdentResult.GetRightContent())) { // Selection is inside the right element - push it before it. EditorRawDOMPoint atRememberedRightBQ( unwrappedOutdentResult.GetRightContent()); nsresult rv = CollapseSelectionTo(atRememberedRightBQ); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); } } return EditActionResult::HandledResult(); } Result HTMLEditor::HandleOutdentAtSelectionInternal(const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); AutoSelectionRestorer restoreSelectionLater(*this); bool useCSS = IsCSSEnabled(); // Convert the selection ranges into "promoted" selection ranges: this // basically just expands the range to include the immediate block parent, // and then further expands to include any ancestors whose children are all // in the range AutoTArray, 64> arrayOfContents; { AutoRangeArray extendedSelectionRanges(SelectionRef()); extendedSelectionRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( EditSubAction::eOutdent, aEditingHost); nsresult rv = extendedSelectionRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eOutdent, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eOutdent, " "CollectNonEditableNodes::Yes) failed"); return Err(rv); } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eOutdent); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eOutdent) failed"); return splitAtBRElementsResult.propagateErr(); } if (AllowsTransactionsToChangeSelection() && splitAtBRElementsResult.inspect().IsSet()) { nsresult rv = CollapseSelectionTo(splitAtBRElementsResult.inspect()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } } } nsCOMPtr leftContentOfLastOutdented; nsCOMPtr middleContentOfLastOutdented; nsCOMPtr rightContentOfLastOutdented; RefPtr indentedParentElement; nsCOMPtr firstContentToBeOutdented, lastContentToBeOutdented; BlockIndentedWith indentedParentIndentedWith = BlockIndentedWith::HTML; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do EditorDOMPoint atContent(content); if (!atContent.IsSet()) { continue; } // If it's a `
    `, remove it to outdent its children. if (content->IsHTMLElement(nsGkAtoms::blockquote)) { // If we've already found an ancestor block element indented, we need to // split it and remove the block element first. if (indentedParentElement) { NS_WARNING_ASSERTION(indentedParentElement == content, "Indented parent element is not the
    "); Result outdentResult = OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith, aEditingHost); if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); unwrappedOutdentResult.IgnoreCaretPointSuggestion(); leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent(); middleContentOfLastOutdented = unwrappedOutdentResult.UnwrapMiddleContent(); rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } Result unwrapBlockquoteElementResult = RemoveBlockContainerWithTransaction( MOZ_KnownLive(*content->AsElement())); if (MOZ_UNLIKELY(unwrapBlockquoteElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return unwrapBlockquoteElementResult.propagateErr(); } const EditorDOMPoint& pointToPutCaret = unwrapBlockquoteElementResult.inspect(); if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) { nsresult rv = CollapseSelectionTo(pointToPutCaret); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } } continue; } // If we're using CSS and the node is a block element, check its start // margin whether it's indented with CSS. if (useCSS && HTMLEditUtils::IsBlockElement(content)) { nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(MOZ_KnownLive(content)); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty(content, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetSpecifiedProperty() failed, but ignored"); float startMargin = 0; RefPtr unit; CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit)); // If indented with CSS, we should decrease the start margin. if (startMargin > 0) { const Result pointToPutCaretOrError = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Decrease, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed, " "but ignored"); } else if (AllowsTransactionsToChangeSelection() && pointToPutCaretOrError.inspect().IsSet()) { nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } } continue; } } // If it's a list item, we should treat as that it "indents" its children. if (HTMLEditUtils::IsListItem(content)) { // If it is a list item, that means we are not outdenting whole list. // XXX I don't understand this sentence... We may meet parent list // element, no? if (indentedParentElement) { Result outdentResult = OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith, aEditingHost); if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); unwrappedOutdentResult.IgnoreCaretPointSuggestion(); leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent(); middleContentOfLastOutdented = unwrappedOutdentResult.UnwrapMiddleContent(); rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } // XXX `content` could become different element since // `OutdentPartOfBlock()` may run mutation event listeners. nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()), LiftUpFromAllParentListElements::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:" ":No) failed"); return Err(rv); } continue; } // If we've found an ancestor block element which indents its children // and the current node is NOT a descendant of it, we should remove it to // outdent its children. Otherwise, i.e., current node is a descendant of // it, we meet new node which should be outdented when the indented parent // is removed. if (indentedParentElement) { if (EditorUtils::IsDescendantOf(*content, *indentedParentElement)) { // Extend the range to be outdented at removing the // indentedParentElement. lastContentToBeOutdented = content; continue; } Result outdentResult = OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith, aEditingHost); if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); unwrappedOutdentResult.IgnoreCaretPointSuggestion(); leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent(); middleContentOfLastOutdented = unwrappedOutdentResult.UnwrapMiddleContent(); rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; // curBlockIndentedWith = HTMLEditor::BlockIndentedWith::HTML; // Then, we need to look for next indentedParentElement. } indentedParentIndentedWith = BlockIndentedWith::HTML; for (nsCOMPtr parentContent = content->GetParent(); parentContent && !parentContent->IsHTMLElement(nsGkAtoms::body) && parentContent != &aEditingHost && (parentContent->IsHTMLElement(nsGkAtoms::table) || !HTMLEditUtils::IsAnyTableElement(parentContent)); parentContent = parentContent->GetParent()) { // If we reach a `
    ` ancestor, it should be split at next // time at least for outdenting current node. if (parentContent->IsHTMLElement(nsGkAtoms::blockquote)) { indentedParentElement = parentContent->AsElement(); firstContentToBeOutdented = content; lastContentToBeOutdented = content; break; } if (!useCSS) { continue; } nsCOMPtr grandParentNode = parentContent->GetParentNode(); nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(MOZ_KnownLive(content)); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_WARN_IF(grandParentNode != parentContent->GetParentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty( *parentContent, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetSpecifiedProperty() failed, but ignored"); // XXX Now, editing host may become different element. If so, shouldn't // we stop this handling? float startMargin; RefPtr unit; CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit)); // If we reach a block element which indents its children with start // margin, we should remove it at next time. if (startMargin > 0 && !(HTMLEditUtils::IsAnyListElement(atContent.GetContainer()) && HTMLEditUtils::IsAnyListElement(content))) { indentedParentElement = parentContent->AsElement(); firstContentToBeOutdented = content; lastContentToBeOutdented = content; indentedParentIndentedWith = BlockIndentedWith::CSS; break; } } if (indentedParentElement) { continue; } // If we don't have any block elements which indents current node and // both current node and its parent are list element, remove current // node to move all its children to the parent list. // XXX This is buggy. When both lists' item types are different, // we create invalid tree. E.g., `