/* 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 "EditorBase.h" #include "HTMLEditor.h" #include "HTMLEditorInlines.h" #include "HTMLEditorNestedClasses.h" #include "CSSEditUtils.h" #include "EditAction.h" #include "EditorDOMPoint.h" #include "EditorLineBreak.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "HTMLEditUtils.h" #include "PendingStyles.h" // for SpecifiedStyle #include "WhiteSpaceVisibilityKeeper.h" #include "WSRunScanner.h" #include "ErrorList.h" #include "mozilla/Assertions.h" #include "mozilla/AutoRestore.h" #include "mozilla/ContentIterator.h" #include "mozilla/EditorForwards.h" #include "mozilla/Maybe.h" #include "mozilla/PresShell.h" #include "mozilla/TextComposition.h" #include "mozilla/dom/RangeBinding.h" #include "mozilla/dom/Selection.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsFrameSelection.h" #include "nsGkAtoms.h" #include "nsIContent.h" #include "nsINode.h" #include "nsRange.h" #include "nsTArray.h" #include "nsTextNode.h" class nsISupports; namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions; nsresult HTMLEditor::InsertLineBreakAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } { Result result = CanHandleHTMLEditSubAction(CheckSelectionInReplacedElement::No); 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 RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return NS_ERROR_FAILURE; } AutoInsertLineBreakHandler handler(*this, *editingHost); nsresult rv = handler.Run(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoInsertLineBreakHandler::Run() failed"); return rv; } nsresult HTMLEditor::AutoInsertLineBreakHandler::Run() { MOZ_ASSERT(mHTMLEditor.IsEditActionDataAvailable()); const auto atStartOfSelection = mHTMLEditor.GetFirstSelectionStartPoint(); if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc()); const Maybe lineBreakType = mHTMLEditor.GetPreferredLineBreakType( *atStartOfSelection.ContainerAs(), mEditingHost); if (MOZ_UNLIKELY(!lineBreakType)) { return NS_SUCCESS_DOM_NO_OPERATION; // Cannot insert a line break there. } if (lineBreakType.value() == LineBreakType::BRElement) { nsresult rv = HandleInsertBRElement(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoInsertLineBreakHandler::HandleInsertBRElement()"); return rv; } nsresult rv = HandleInsertLinefeed(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoInsertLineBreakHandler::HandleInsertLinefeed() failed"); return rv; } nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertBRElement() { const EditorDOMPoint pointToInsert = [&]() { const auto atStartOfSelection = mHTMLEditor.GetFirstSelectionStartPoint(); MOZ_ASSERT(atStartOfSelection.IsInContentNode()); return HTMLEditUtils::GetPossiblePointToInsert( atStartOfSelection, *nsGkAtoms::br, mEditingHost); }(); if (NS_WARN_IF(!pointToInsert.IsSet())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(pointToInsert.IsInContentNode()); // XXX Should we check the preferred line break again? Result insertLineBreakResultOrError = mHTMLEditor.InsertLineBreak(WithTransaction::Yes, LineBreakType::BRElement, pointToInsert, nsIEditor::eNext); if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "LineBreakType::BRElement, eNext) failed"); return insertLineBreakResultOrError.unwrapErr(); } CreateLineBreakResult insertLineBreakResult = insertLineBreakResultOrError.unwrap(); MOZ_ASSERT(insertLineBreakResult.Handled()); insertLineBreakResult.IgnoreCaretPointSuggestion(); auto pointToPutCaret = insertLineBreakResult.UnwrapCaretPoint(); if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) { NS_WARNING("Inserted
was unexpectedly removed"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } { AutoTrackDOMPoint trackPointToPutCaret(mHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); Result insertPaddingBRResultOrError = mHTMLEditor.InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded( insertLineBreakResult.AfterLineBreak(), mEditingHost); if (insertPaddingBRResultOrError.isErr()) [[unlikely]] { NS_WARNING( "HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded() " "failed"); return insertPaddingBRResultOrError.propagateErr(); } CreateLineBreakResult insertPaddingBRResult = insertPaddingBRResultOrError.unwrap(); insertPaddingBRResult.IgnoreCaretPointSuggestion(); trackPointToPutCaret.Flush(StopTracking::Yes); if (!insertPaddingBRResult.Handled()) { // If the line break is followed by visible content, we want to put caret // to start of the following content to preserve the style at start of the // line. const WSScanResult nextThingOfBR = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( {WSRunScanner::Option::OnlyEditableNodes, WSRunScanner::Option::StopAtAnyEmptyInlineContainers}, insertLineBreakResult.AfterLineBreak()); if (nextThingOfBR.InVisibleOrCollapsibleCharacters() || nextThingOfBR.ReachedSpecialContent()) { pointToPutCaret = nextThingOfBR.PointAtReachedContent(); } else if (nextThingOfBR.ReachedEmptyInlineContainerElement()) { pointToPutCaret = EditorDOMPoint(nextThingOfBR.ElementPtr(), 0u); } } } nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertLinefeed() { nsresult rv = mHTMLEditor.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) && mHTMLEditor.SelectionRef().IsCollapsed()) { nsresult rv = mHTMLEditor.EnsureCaretNotAfterInvisibleBRElement(mEditingHost); 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 = mHTMLEditor.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"); } } const EditorDOMPoint atStartOfSelection = mHTMLEditor.GetFirstSelectionStartPoint(); if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc()); // Do nothing if the node is read-only if (!HTMLEditUtils::IsSimplyEditableNode( *atStartOfSelection.GetContainer())) { return NS_SUCCESS_DOM_NO_OPERATION; } Result insertLineFeedResult = AutoInsertLineBreakHandler::InsertLinefeed( mHTMLEditor, atStartOfSelection, mEditingHost); if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) { NS_WARNING("AutoInsertLineBreakHandler::InsertLinefeed() failed"); return insertLineFeedResult.unwrapErr(); } EditorDOMPoint pointToPutCaret = insertLineFeedResult.unwrap(); // If the line break is followed by visible content, we want to put caret // to start of the following content to preserve the style at start of the // line. const WSScanResult nextThingOfLinefeed = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( {WSRunScanner::Option::OnlyEditableNodes, WSRunScanner::Option::StopAtAnyEmptyInlineContainers}, pointToPutCaret); if (nextThingOfLinefeed.InVisibleOrCollapsibleCharacters() || nextThingOfLinefeed.ReachedSpecialContent()) { pointToPutCaret = nextThingOfLinefeed.PointAtReachedContent(); } else if (nextThingOfLinefeed.ReachedEmptyInlineContainerElement()) { pointToPutCaret = EditorDOMPoint(nextThingOfLinefeed.ElementPtr(), 0u); } rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } // static Result HTMLEditor::AutoInsertLineBreakHandler::InsertLinefeed( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) { if (NS_WARN_IF(!aPointToBreak.IsSet())) { return Err(NS_ERROR_INVALID_ARG); } const RefPtr document = aHTMLEditor.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 = aHTMLEditor.CreateStyleForInsertText(aPointToBreak, aEditingHost); 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. pointToInsert = HTMLEditUtils::GetPossiblePointToInsert( pointToInsert, *nsGkAtoms::textTagName, aEditingHost); if (NS_WARN_IF(!pointToInsert.IsSet())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(pointToInsert.IsInContentNode()); // FIXME: If the computed point does not preformat linefeed, we should switch // back to inserting a
. However, I think it should be handled before // calling this. AutoRestore disableListener( aHTMLEditor.EditSubActionDataRef().mAdjustChangedRangeFromListener); aHTMLEditor.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(aHTMLEditor); AutoTrackDOMPoint trackingInsertingPosition(aHTMLEditor.RangeUpdaterRef(), &pointToInsert); Result insertLinefeedResultOrError = aHTMLEditor.InsertLineBreak(WithTransaction::Yes, LineBreakType::Linefeed, pointToInsert, eNext); if (MOZ_UNLIKELY(insertLinefeedResultOrError.isErr())) { NS_WARNING( "HTMLEditor::InsertLineBreak(WithTransaction::Yes, " "LineBreakType::Linefeed, eNext) failed"); return insertLinefeedResultOrError.propagateErr(); } trackingInsertingPosition.Flush(StopTracking::Yes); CreateLineBreakResult insertLinefeedResult = insertLinefeedResultOrError.unwrap(); EditorDOMPoint pointToPutCaret = insertLinefeedResult.UnwrapCaretPoint(); // Insert a padding
if the inserted linefeed is followed by a block // boundary. Note that it should always be
for avoiding padding line // breaks appear in `.textContent` value. if (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) { AutoTrackDOMPoint trackingInsertedPosition(aHTMLEditor.RangeUpdaterRef(), &pointToInsert); AutoTrackDOMPoint trackingNewCaretPosition(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); Result insertPaddingBRResultOrError = aHTMLEditor.InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded( insertLinefeedResult.AfterLineBreak(), aEditingHost); if (insertPaddingBRResultOrError.isErr()) [[unlikely]] { NS_WARNING( "HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded() " "failed"); return insertPaddingBRResultOrError.propagateErr(); } insertPaddingBRResultOrError.unwrap().IgnoreCaretPointSuggestion(); } // 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 = aHTMLEditor.SelectionRef().SetInterlinePosition( InterlinePosition::EndOfLine); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Selection::SetInterlinePosition(InterlinePosition::" "EndOfLine) failed, but ignored"); if (NS_FAILED(aHTMLEditor.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(aHTMLEditor.TopLevelEditSubActionDataRef() .mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), pointToPutCaret.ToRawRangeBoundary()))) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(NS_ERROR_FAILURE); } pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine); return pointToPutCaret; } } // namespace mozilla