diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:35:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:35:29 +0000 |
commit | 59203c63bb777a3bacec32fb8830fba33540e809 (patch) | |
tree | 58298e711c0ff0575818c30485b44a2f21bf28a0 /editor/libeditor/HTMLEditorDeleteHandler.cpp | |
parent | Adding upstream version 126.0.1. (diff) | |
download | firefox-59203c63bb777a3bacec32fb8830fba33540e809.tar.xz firefox-59203c63bb777a3bacec32fb8830fba33540e809.zip |
Adding upstream version 127.0.upstream/127.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor/libeditor/HTMLEditorDeleteHandler.cpp')
-rw-r--r-- | editor/libeditor/HTMLEditorDeleteHandler.cpp | 1104 |
1 files changed, 942 insertions, 162 deletions
diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp index 39bb95151e..8c58979001 100644 --- a/editor/libeditor/HTMLEditorDeleteHandler.cpp +++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -353,6 +353,28 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { const EditorDOMRangeType& aRangeToDelete) const; /** + * Extend the start boundary of aRangeToDelete to contain ancestor inline + * elements which will be empty once the content in aRangeToDelete is removed + * from the tree. + * + * NOTE: This is designed for deleting inline elements which become empty if + * aRangeToDelete which crosses a block boundary of right block child. + * Therefore, you may need to improve this method if you want to use this in + * the other cases. + * + * @param aRangeToDelete [in/out] The range to delete. This start + * boundary may be modified. + * @param aEditingHost The editing host. + * @return true if aRangeToDelete is modified. + * false if aRangeToDelete is not modified. + * error if aRangeToDelete gets unexpected + * situation. + */ + static Result<bool, nsresult> + ExtendRangeToContainAncestorInlineElementsAtStart( + nsRange& aRangeToDelete, const Element& aEditingHost); + + /** * A helper method for ExtendOrShrinkRangeToDelete(). This returns shrunken * range if aRangeToDelete selects all over list elements which have some list * item elements to avoid to delete all list items from the list element. @@ -529,13 +551,15 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { * @param aCurrentBlockElement The current block element. * @param aCaretPoint The caret point (i.e., selection start * or end). + * @param aEditingHost The editing host. * @return true if can continue to handle the * deletion. */ bool PrepareToDeleteAtCurrentBlockBoundary( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, - Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint); + Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint, + const Element& aEditingHost); /** * PrepareToDeleteAtOtherBlockBoundary() considers left content and right @@ -567,11 +591,13 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { * @param aHTMLEditor The HTML editor. * @param aRangeToDelete The range to delete. Must not be * collapsed. + * @param aEditingHost The editing host. * @return true if can continue to handle the * deletion. */ bool PrepareToDeleteNonCollapsedRange(const HTMLEditor& aHTMLEditor, - const nsRange& aRangeToDelete); + const nsRange& aRangeToDelete, + const Element& aEditingHost); /** * Run() executes the joining. @@ -609,17 +635,20 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "HandleDeleteAtOtherBlockBoundary() failed"); return result; } - case Mode::DeleteBRElement: { - Result<EditActionResult, nsresult> result = - DeleteBRElement(aHTMLEditor, aDirectionAndAmount, aEditingHost); + case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: { + Result<EditActionResult, nsresult> result = HandleDeleteLineBreak( + aHTMLEditor, aDirectionAndAmount, aCaretPoint, aEditingHost); NS_WARNING_ASSERTION( result.isOk(), - "AutoBlockElementsJoiner::DeleteBRElement() failed"); + "AutoBlockElementsJoiner::HandleDeleteLineBreak() failed"); return result; } case Mode::JoinBlocksInSameParent: case Mode::DeleteContentInRange: case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other Run()"); return Err(NS_ERROR_UNEXPECTED); @@ -654,16 +683,21 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "ComputeRangeToDeleteAtOtherBlockBoundary() failed"); return rv; } - case Mode::DeleteBRElement: { - nsresult rv = ComputeRangeToDeleteBRElement(aRangeToDelete); + case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: { + nsresult rv = ComputeRangeToDeleteLineBreak( + aHTMLEditor, aRangeToDelete, aEditingHost, + ComputeRangeFor::GetTargetRanges); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoBlockElementsJoiner::" - "ComputeRangeToDeleteBRElement() failed"); + "ComputeRangeToDeleteLineBreak() failed"); return rv; } case Mode::JoinBlocksInSameParent: case Mode::DeleteContentInRange: case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other " "ComputeRangesToDelete()"); @@ -696,6 +730,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { case Mode::JoinCurrentBlock: case Mode::JoinOtherBlock: case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other Run()"); return Err(NS_ERROR_UNEXPECTED); @@ -717,7 +753,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "AutoBlockElementsJoiner::DeleteContentInRange() failed"); return result; } - case Mode::DeleteNonCollapsedRange: { + case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: { Result<EditActionResult, nsresult> result = HandleDeleteNonCollapsedRange( aHTMLEditor, aDirectionAndAmount, aStripWrappers, @@ -744,6 +781,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { case Mode::JoinCurrentBlock: case Mode::JoinOtherBlock: case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: MOZ_ASSERT_UNREACHABLE( "This mode should be handled in the other " "ComputeRangesToDelete()"); @@ -765,7 +804,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { "ComputeRangesToDeleteContentInRanges() failed"); return rv; } - case Mode::DeleteNonCollapsedRange: { + case Mode::DeleteNonCollapsedRange: + case Mode::DeletePrecedingLinesAndContentInRange: { nsresult rv = ComputeRangeToDeleteNonCollapsedRange( aHTMLEditor, aDirectionAndAmount, aRangeToDelete, aSelectionWasCollapsed, aEditingHost); @@ -825,10 +865,14 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> - DeleteBRElement(HTMLEditor& aHTMLEditor, - nsIEditor::EDirection aDirectionAndAmount, - const Element& aEditingHost); - nsresult ComputeRangeToDeleteBRElement(nsRange& aRangeToDelete) const; + HandleDeleteLineBreak(HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, + const Element& aEditingHost); + enum class ComputeRangeFor : bool { GetTargetRanges, ToDeleteTheRange }; + nsresult ComputeRangeToDeleteLineBreak( + const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, + const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> DeleteContentInRange(HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, @@ -913,6 +957,27 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange); + /** + * Return a block element which is an inclusive ancestor of the container of + * aPoint if aPoint is start of ancestor blocks. For example, if `<div + * id=div1>abc<div id=div2><div id=div3>[]def</div></div></div>`, return + * #div2. + */ + template <typename EditorDOMPointType> + static Result<Element*, nsresult> + GetMostDistantBlockAncestorIfPointIsStartAtBlock( + const EditorDOMPointType& aPoint, const Element& aEditingHost, + const Element* aAncestorLimiter = nullptr); + + /** + * Extend aRangeToDelete to contain new empty inline ancestors and contain + * an invisible <br> element before right child block which causes an empty + * line but the range starts after it. + */ + void ExtendRangeToDeleteNonCollapsedRange( + const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, + const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const; + class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner final { public: AutoInclusiveAncestorBlockElementsJoiner() = delete; @@ -1030,8 +1095,17 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { JoinOtherBlock, JoinBlocksInSameParent, DeleteBRElement, + // The instance will handle only the <br> element immediately before a + // block. + DeletePrecedingBRElementOfBlock, + // The instance will handle only the preceding preformatted line break + // before a block. + DeletePrecedingPreformattedLineBreak, DeleteContentInRange, DeleteNonCollapsedRange, + // The instance will handle preceding lines of the right block and content + // in the range in the right block. + DeletePrecedingLinesAndContentInRange, }; AutoDeleteRangesHandler* mDeleteRangesHandler; const AutoDeleteRangesHandler& mDeleteRangesHandlerConst; @@ -1043,6 +1117,7 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { // removed at deletion. AutoTArray<OwningNonNull<nsIContent>, 8> mSkippedInvisibleContents; RefPtr<dom::HTMLBRElement> mBRElement; + EditorDOMPointInText mPreformattedLineBreak; Mode mMode = Mode::NotInitialized; }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner @@ -1371,10 +1446,10 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( WSRunScanner wsRunScannerAtCaret( editingHost, caretPoint, BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult scanFromCaretPointResult = + const WSScanResult scanFromCaretPointResult = aDirectionAndAmount == nsIEditor::eNext - ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( - caretPoint) + ? wsRunScannerAtCaret + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(caretPoint) : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( caretPoint); if (scanFromCaretPointResult.Failed()) { @@ -1383,26 +1458,23 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( "failed"); return NS_ERROR_FAILURE; } - if (!scanFromCaretPointResult.GetContent()) { - return NS_SUCCESS_DOM_NO_OPERATION; - } + MOZ_ASSERT(scanFromCaretPointResult.GetContent()); if (scanFromCaretPointResult.ReachedBRElement()) { if (scanFromCaretPointResult.BRElementPtr() == wsRunScannerAtCaret.GetEditingHost()) { return NS_OK; } - if (!EditorUtils::IsEditableContent( - *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) { + if (!scanFromCaretPointResult.IsContentEditable()) { return NS_SUCCESS_DOM_NO_OPERATION; } - if (HTMLEditUtils::IsInvisibleBRElement( - *scanFromCaretPointResult.BRElementPtr())) { + if (scanFromCaretPointResult.ReachedInvisibleBRElement()) { EditorDOMPoint newCaretPosition = aDirectionAndAmount == nsIEditor::eNext - ? EditorDOMPoint::After( - *scanFromCaretPointResult.BRElementPtr()) - : EditorDOMPoint(scanFromCaretPointResult.BRElementPtr()); + ? scanFromCaretPointResult + .PointAfterReachedContent<EditorDOMPoint>() + : scanFromCaretPointResult + .PointAtReachedContent<EditorDOMPoint>(); if (NS_WARN_IF(!newCaretPosition.IsSet())) { return NS_ERROR_FAILURE; } @@ -1450,7 +1522,8 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( // Otherwise, extend the range to contain the invisible `<br>` // element. - if (EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr()) + if (scanFromCaretPointResult + .PointAtReachedContent<EditorRawDOMPoint>() .IsBefore( aRangesToDelete .GetFirstRangeStartPoint<EditorRawDOMPoint>())) { @@ -1463,12 +1536,13 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( return rv; } if (aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>() - .IsBefore(EditorRawDOMPoint::After( - *scanFromCaretPointResult.BRElementPtr()))) { + .IsBefore( + scanFromCaretPointResult + .PointAfterReachedContent<EditorRawDOMPoint>())) { nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd( aRangesToDelete.FirstRangeRef()->StartRef(), - EditorRawDOMPoint::After( - *scanFromCaretPointResult.BRElementPtr()) + scanFromCaretPointResult + .PointAfterReachedContent<EditorRawDOMPoint>() .ToRawRangeBoundary()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); @@ -1670,10 +1744,11 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( WSRunScanner wsRunScannerAtCaret( &aEditingHost, caretPoint.ref(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult scanFromCaretPointResult = + const WSScanResult scanFromCaretPointResult = aDirectionAndAmount == nsIEditor::eNext - ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( - caretPoint.ref()) + ? wsRunScannerAtCaret + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( + caretPoint.ref()) : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( caretPoint.ref()); if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) { @@ -1682,20 +1757,17 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( "failed"); return Err(NS_ERROR_FAILURE); } - if (!scanFromCaretPointResult.GetContent()) { - return EditActionResult::CanceledResult(); - } + MOZ_ASSERT(scanFromCaretPointResult.GetContent()); + // Short circuit for invisible breaks. delete them and recurse. if (scanFromCaretPointResult.ReachedBRElement()) { if (scanFromCaretPointResult.BRElementPtr() == &aEditingHost) { return EditActionResult::HandledResult(); } - if (!EditorUtils::IsEditableContent( - *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) { + if (!scanFromCaretPointResult.IsContentEditable()) { return EditActionResult::CanceledResult(); } - if (HTMLEditUtils::IsInvisibleBRElement( - *scanFromCaretPointResult.BRElementPtr())) { + if (scanFromCaretPointResult.ReachedInvisibleBRElement()) { // TODO: We should extend the range to delete again before/after // the caret point and use `HandleDeleteNonCollapsedRanges()` // instead after we would create delete range computation @@ -1728,10 +1800,10 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( WSRunScanner wsRunScannerAtCaret( &aEditingHost, caretPoint.ref(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult scanFromCaretPointResult = + const WSScanResult scanFromCaretPointResult = aDirectionAndAmount == nsIEditor::eNext ? wsRunScannerAtCaret - .ScanNextVisibleNodeOrBlockBoundaryFrom( + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( caretPoint.ref()) : wsRunScannerAtCaret .ScanPreviousVisibleNodeOrBlockBoundaryFrom( @@ -1742,7 +1814,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( "VisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } - if (MOZ_UNLIKELY( + if (NS_WARN_IF( scanFromCaretPointResult.ReachedInvisibleBRElement())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } @@ -1786,8 +1858,11 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() || aScanFromCaretPointResult.InNonCollapsibleCharacters() || aScanFromCaretPointResult.ReachedPreformattedLineBreak()) { + // This means that if aDirectionAndAmount == nsIEditor::eNext, collapse + // selection at the found character. Otherwise, collapse selection after + // the found character. nsresult rv = aRangesToDelete.Collapse( - aScanFromCaretPointResult.Point<EditorRawDOMPoint>()); + aScanFromCaretPointResult.Point_Deprecated<EditorRawDOMPoint>()); if (MOZ_UNLIKELY(NS_FAILED(rv))) { NS_WARNING("AutoRangeArray::Collapse() failed"); return NS_ERROR_FAILURE; @@ -1826,7 +1901,7 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.ReachedOtherBlockElement()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + if (NS_WARN_IF(!aScanFromCaretPointResult.ContentIsElement())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); @@ -1854,10 +1929,9 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( return handled ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; } - if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { - return NS_ERROR_FAILURE; - } + if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary() || + aScanFromCaretPointResult.ReachedInlineEditingHostBoundary()) { + MOZ_ASSERT(aScanFromCaretPointResult.ContentIsElement()); MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); bool handled = false; for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) { @@ -1865,7 +1939,7 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( aHTMLEditor, aDirectionAndAmount, *aScanFromCaretPointResult.ElementPtr(), - aWSRunScannerAtCaret.ScanStartRef())) { + aWSRunScannerAtCaret.ScanStartRef(), aEditingHost)) { continue; } handled = true; @@ -1904,8 +1978,11 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() || aScanFromCaretPointResult.InNonCollapsibleCharacters() || aScanFromCaretPointResult.ReachedPreformattedLineBreak()) { + // This means that if aDirectionAndAmount == nsIEditor::eNext, collapse + // selection at the found character. Otherwise, collapse selection after + // the found character. nsresult rv = aRangesToDelete.Collapse( - aScanFromCaretPointResult.Point<EditorRawDOMPoint>()); + aScanFromCaretPointResult.Point_Deprecated<EditorRawDOMPoint>()); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(NS_ERROR_FAILURE); @@ -1959,13 +2036,16 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.InNonCollapsibleCharacters()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsText())) { + if (NS_WARN_IF(!aScanFromCaretPointResult.ContentIsText())) { return Err(NS_ERROR_FAILURE); } Result<CaretPoint, nsresult> caretPointOrError = HandleDeleteCollapsedSelectionAtVisibleChar( aHTMLEditor, aDirectionAndAmount, aRangesToDelete, - aScanFromCaretPointResult.Point<EditorDOMPoint>(), aEditingHost); + // This means that if aDirectionAndAmount == nsIEditor::eNext, + // at the found character. Otherwise, after the found character. + aScanFromCaretPointResult.Point_Deprecated<EditorDOMPoint>(), + aEditingHost); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "AutoDeleteRangesHandler::" @@ -2019,7 +2099,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.ReachedOtherBlockElement()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + if (NS_WARN_IF(!aScanFromCaretPointResult.ContentIsElement())) { return Err(NS_ERROR_FAILURE); } MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); @@ -2049,10 +2129,9 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( : std::move(ret); } - if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) { - if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { - return Err(NS_ERROR_FAILURE); - } + if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary() || + aScanFromCaretPointResult.ReachedInlineEditingHostBoundary()) { + MOZ_ASSERT(aScanFromCaretPointResult.ContentIsElement()); MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); bool allRangesNotHandled = true; auto ret = EditActionResult::IgnoredResult(); @@ -2061,7 +2140,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( aHTMLEditor, aDirectionAndAmount, *aScanFromCaretPointResult.ElementPtr(), - aWSRunScannerAtCaret.ScanStartRef())) { + aWSRunScannerAtCaret.ScanStartRef(), aEditingHost)) { continue; } allRangesNotHandled = false; @@ -2484,6 +2563,109 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAtomicContent( return CaretPoint(std::move(pointToPutCaret)); } +// static +Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + ExtendRangeToContainAncestorInlineElementsAtStart( + nsRange& aRangeToDelete, const Element& aEditingHost) { + MOZ_ASSERT(aRangeToDelete.IsPositioned()); + MOZ_ASSERT(aRangeToDelete.GetCommonAncestorContainer(IgnoreErrors())); + MOZ_ASSERT(aRangeToDelete.GetCommonAncestorContainer(IgnoreErrors()) + ->IsInclusiveDescendantOf(&aEditingHost)); + + EditorRawDOMPoint startPoint(aRangeToDelete.StartRef()); + if (startPoint.IsInTextNode()) { + if (!startPoint.IsStartOfContainer()) { + // FIXME: If before the point has only collapsible white-spaces and the + // text node follows a block boundary, we should treat the range start + // from start of the text node. + return true; + } + startPoint.Set(startPoint.ContainerAs<Text>()); + if (NS_WARN_IF(!startPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + if (startPoint.GetContainer() == &aEditingHost) { + return false; + } + } else if (startPoint.IsInDataNode()) { + startPoint.Set(startPoint.ContainerAs<nsIContent>()); + if (NS_WARN_IF(!startPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + if (startPoint.GetContainer() == &aEditingHost) { + return false; + } + } else if (startPoint.GetContainer() == &aEditingHost) { + return false; + } + + // FYI: This method is designed for deleting inline elements which become + // empty if aRangeToDelete which crosses a block boundary of right block + // child. Therefore, you may need to improve this method if you want to use + // this in the other cases. + + nsINode* const commonAncestor = + nsContentUtils::GetClosestCommonInclusiveAncestor( + startPoint.GetContainer(), aRangeToDelete.GetEndContainer()); + if (NS_WARN_IF(!commonAncestor)) { + return Err(NS_ERROR_FAILURE); + } + MOZ_ASSERT(commonAncestor->IsInclusiveDescendantOf(&aEditingHost)); + + EditorRawDOMPoint newStartPoint(startPoint); + while (newStartPoint.GetContainer() != &aEditingHost && + newStartPoint.GetContainer() != commonAncestor) { + if (NS_WARN_IF(!newStartPoint.IsInContentNode())) { + return Err(NS_ERROR_FAILURE); + } + if (!HTMLEditUtils::IsInlineContent( + *newStartPoint.ContainerAs<nsIContent>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { + break; + } + // The container is inline, check whether the point is first visible point + // or not to consider whether climbing up the tree. + bool foundVisiblePrevSibling = false; + for (nsIContent* content = newStartPoint.GetPreviousSiblingOfChild(); + content; content = content->GetPreviousSibling()) { + if (Text* text = Text::FromNode(content)) { + if (HTMLEditUtils::IsVisibleTextNode(*text)) { + foundVisiblePrevSibling = true; + break; + } + // The text node is invisible. + } else if (content->IsComment()) { + // Ignore the comment node. + } else if (!HTMLEditUtils::IsInlineContent( + *content, + BlockInlineCheck::UseComputedDisplayOutsideStyle) || + !HTMLEditUtils::IsEmptyNode( + *content, + {EmptyCheckOption::TreatSingleBRElementAsVisible})) { + foundVisiblePrevSibling = true; + break; + } + } + if (foundVisiblePrevSibling) { + break; + } + // the point can be treated as start of the parent inline now. + newStartPoint.Set(newStartPoint.ContainerAs<nsIContent>()); + if (NS_WARN_IF(!newStartPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + } + if (newStartPoint == startPoint) { + return false; // Don't need to modify the range + } + IgnoredErrorResult error; + aRangeToDelete.SetStart(newStartPoint.ToRawRangeBoundary(), error); + if (MOZ_UNLIKELY(error.Failed())) { + return Err(NS_ERROR_FAILURE); + } + return true; +} + bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: PrepareToDeleteAtOtherBlockBoundary( const HTMLEditor& aHTMLEditor, @@ -2519,12 +2701,12 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: // Next to a block. See if we are between the block and a `<br>`. // If so, we really want to delete the `<br>`. Else join content at // selection to the block. - WSScanResult scanFromCaretResult = + const WSScanResult scanFromCaretResult = aDirectionAndAmount == nsIEditor::eNext ? aWSRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( aCaretPoint) - : aWSRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( - aCaretPoint); + : aWSRunScannerAtCaret + .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(aCaretPoint); // If we found a `<br>` element, we need to delete it instead of joining the // contents. if (scanFromCaretResult.ReachedBRElement()) { @@ -2537,58 +2719,159 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: } nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: - ComputeRangeToDeleteBRElement(nsRange& aRangeToDelete) const { - MOZ_ASSERT(mBRElement); - // XXX Why don't we scan invisible leading white-spaces which follows the - // `<br>` element? + ComputeRangeToDeleteLineBreak(const HTMLEditor& aHTMLEditor, + nsRange& aRangeToDelete, + const Element& aEditingHost, + ComputeRangeFor aComputeRangeFor) const { + // FIXME: Scan invisible leading white-spaces after the <br>. + MOZ_ASSERT_IF(mMode == Mode::DeleteBRElement, mBRElement); + MOZ_ASSERT_IF(mMode == Mode::DeletePrecedingBRElementOfBlock, mBRElement); + MOZ_ASSERT_IF(mMode == Mode::DeletePrecedingPreformattedLineBreak, + mPreformattedLineBreak.IsSetAndValid()); + MOZ_ASSERT_IF(mMode == Mode::DeletePrecedingPreformattedLineBreak, + mPreformattedLineBreak.IsCharPreformattedNewLine()); + MOZ_ASSERT_IF(aComputeRangeFor == ComputeRangeFor::GetTargetRanges, + aRangeToDelete.IsPositioned()); + + // If we're computing for beforeinput.getTargetRanges() and the inputType + // is not a simple deletion like replacing selected content with new + // content, the range should end at the original end boundary of the given + // range. + const bool preserveEndBoundary = + (mMode == Mode::DeletePrecedingBRElementOfBlock || + mMode == Mode::DeletePrecedingPreformattedLineBreak) && + aComputeRangeFor == ComputeRangeFor::GetTargetRanges && + !MayEditActionDeleteAroundCollapsedSelection(aHTMLEditor.GetEditAction()); + + if (mMode != Mode::DeletePrecedingPreformattedLineBreak) { + Element* const mostDistantInlineAncestor = + HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + *mBRElement, BlockInlineCheck::UseComputedDisplayOutsideStyle, + &aEditingHost); + if (preserveEndBoundary) { + // FIXME: If the range ends at end of an inline element, we may need to + // extend the range. + IgnoredErrorResult error; + aRangeToDelete.SetStart(EditorRawDOMPoint(mostDistantInlineAncestor + ? mostDistantInlineAncestor + : mBRElement) + .ToRawRangeBoundary(), + error); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SetStart() failed"); + MOZ_ASSERT_IF(!error.Failed(), !aRangeToDelete.Collapsed()); + return error.StealNSResult(); + } + IgnoredErrorResult error; + aRangeToDelete.SelectNode( + mostDistantInlineAncestor ? *mostDistantInlineAncestor : *mBRElement, + error); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SelectNode() failed"); + return error.StealNSResult(); + } + + Element* const mostDistantInlineAncestor = + mPreformattedLineBreak.ContainerAs<Text>()->TextDataLength() == 1 + ? HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + *mPreformattedLineBreak.ContainerAs<Text>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost) + : nullptr; + + if (!mostDistantInlineAncestor) { + if (preserveEndBoundary) { + // FIXME: If the range ends at end of an inline element, we may need to + // extend the range. + IgnoredErrorResult error; + aRangeToDelete.SetStart(mPreformattedLineBreak.ToRawRangeBoundary(), + error); + MOZ_ASSERT_IF(!error.Failed(), !aRangeToDelete.Collapsed()); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SetStart() failed"); + return error.StealNSResult(); + } + nsresult rv = aRangeToDelete.SetStartAndEnd( + mPreformattedLineBreak.ToRawRangeBoundary(), + mPreformattedLineBreak.NextPoint().ToRawRangeBoundary()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); + return rv; + } + + if (preserveEndBoundary) { + // FIXME: If the range ends at end of an inline element, we may need to + // extend the range. + IgnoredErrorResult error; + aRangeToDelete.SetStart( + EditorRawDOMPoint(mostDistantInlineAncestor).ToRawRangeBoundary(), + error); + MOZ_ASSERT_IF(!error.Failed(), !aRangeToDelete.Collapsed()); + NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SetStart() failed"); + return error.StealNSResult(); + } + IgnoredErrorResult error; - aRangeToDelete.SelectNode(*mBRElement, error); + aRangeToDelete.SelectNode(*mostDistantInlineAncestor, error); NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SelectNode() failed"); return error.StealNSResult(); } -Result<EditActionResult, nsresult> -HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( - HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, - const Element& aEditingHost) { +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::HandleDeleteLineBreak( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, const Element& aEditingHost) { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); - MOZ_ASSERT(mBRElement); + MOZ_ASSERT(mBRElement || mPreformattedLineBreak.IsSet()); // If we're deleting selection (not replacing with new content), we should // put caret to end of preceding text node if there is. Then, users can type // text in it like the other browsers. EditorDOMPoint pointToPutCaret = [&]() { + // but when we're deleting a preceding line break of current block, we + // should keep the caret position in the current block. + if (mMode == Mode::DeletePrecedingBRElementOfBlock || + mMode == Mode::DeletePrecedingPreformattedLineBreak) { + return aCaretPoint; + } if (!MayEditActionDeleteAroundCollapsedSelection( aHTMLEditor.GetEditAction())) { return EditorDOMPoint(); } WSRunScanner scanner(&aEditingHost, EditorRawDOMPoint(mBRElement), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult maybePreviousText = + const WSScanResult maybePreviousText = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom( EditorRawDOMPoint(mBRElement)); if (maybePreviousText.IsContentEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters() && !HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) { - return maybePreviousText.Point<EditorDOMPoint>(); + return maybePreviousText.PointAfterReachedContent<EditorDOMPoint>(); } - WSScanResult maybeNextText = scanner.ScanNextVisibleNodeOrBlockBoundaryFrom( - EditorRawDOMPoint::After(*mBRElement)); + const WSScanResult maybeNextText = + scanner.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( + EditorRawDOMPoint::After(*mBRElement)); if (maybeNextText.IsContentEditable() && maybeNextText.InVisibleOrCollapsibleCharacters()) { - return maybeNextText.Point<EditorDOMPoint>(); + return maybeNextText.PointAtReachedContent<EditorDOMPoint>(); } return EditorDOMPoint(); }(); - // If we found a `<br>` element, we should delete it instead of joining the - // contents. + RefPtr<nsRange> rangeToDelete = + nsRange::Create(const_cast<Element*>(&aEditingHost)); + MOZ_ASSERT(rangeToDelete); nsresult rv = - aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*mBRElement)); + ComputeRangeToDeleteLineBreak(aHTMLEditor, *rangeToDelete, aEditingHost, + ComputeRangeFor::ToDeleteTheRange); if (NS_FAILED(rv)) { - NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + NS_WARNING( + "AutoBlockElementsJoiner::ComputeRangeToDeleteLineBreak() failed"); return Err(rv); } + Result<EditActionResult, nsresult> result = HandleDeleteNonCollapsedRange( + aHTMLEditor, aDirectionAndAmount, nsIEditor::eNoStrip, *rangeToDelete, + SelectionWasCollapsed::Yes, aEditingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "AutoBlockElementsJoiner::HandleDeleteNonCollapsedRange() failed"); + return result; + } if (mLeftContent && mRightContent && HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) != @@ -2597,7 +2880,7 @@ HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( } // Put selection at edge of block and we are done. - if (NS_WARN_IF(!mLeafContentInOtherBlock)) { + if (NS_WARN_IF(mMode == Mode::DeleteBRElement && !mLeafContentInOtherBlock)) { // XXX This must be odd case. The other block can be empty. return Err(NS_ERROR_FAILURE); } @@ -2607,7 +2890,7 @@ HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } - if (NS_SUCCEEDED(rv)) { + if (mMode == Mode::DeleteBRElement && NS_SUCCEEDED(rv)) { // If we prefer to use style in the previous line, we should forget // previous styles since the caret position has all styles which we want // to use with new content. @@ -2622,7 +2905,9 @@ HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( ->ClearLinkAndItsSpecifiedStyle(); } } else { - NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored"); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed, but ignored"); } return EditActionResult::HandledResult(); } @@ -2937,7 +3222,8 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: PrepareToDeleteAtCurrentBlockBoundary( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, - Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint) { + Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint, + const Element& aEditingHost) { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); // At edge of our block. Look beside it and see if we can join to an @@ -2956,20 +3242,15 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return false; } - Element* editingHost = aHTMLEditor.ComputeEditingHost(); - if (NS_WARN_IF(!editingHost)) { - return false; - } - auto ScanJoinTarget = [&]() -> nsIContent* { nsIContent* targetContent = aDirectionAndAmount == nsIEditor::ePrevious ? HTMLEditUtils::GetPreviousContent( aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, editingHost) + BlockInlineCheck::Unused, &aEditingHost) : HTMLEditUtils::GetNextContent( aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, editingHost); + BlockInlineCheck::Unused, &aEditingHost); // If found content is an invisible text node, let's scan visible things. auto IsIgnorableDataNode = [](nsIContent* aContent) { return aContent && HTMLEditUtils::IsRemovableNode(*aContent) && @@ -2987,22 +3268,22 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: ? HTMLEditUtils::GetPreviousContent( *targetContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost) + &aEditingHost) : HTMLEditUtils::GetNextContent( *targetContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost); + &aEditingHost); adjacentContent; adjacentContent = aDirectionAndAmount == nsIEditor::ePrevious ? HTMLEditUtils::GetPreviousContent( *adjacentContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost) + &aEditingHost) : HTMLEditUtils::GetNextContent( *adjacentContent, {WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, - editingHost)) { + &aEditingHost)) { // If non-editable element is found, we should not skip it to avoid // joining too far nodes. if (!HTMLEditUtils::IsSimplyEditableNode(*adjacentContent)) { @@ -3036,6 +3317,77 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: }; if (aDirectionAndAmount == nsIEditor::ePrevious) { + const WSScanResult prevVisibleThing = [&]() { + // When Backspace at start of a block, we need to delete only a preceding + // <br> element if there is. + const Result<Element*, nsresult> + inclusiveAncestorOfRightChildBlockOrError = AutoBlockElementsJoiner:: + GetMostDistantBlockAncestorIfPointIsStartAtBlock(aCaretPoint, + aEditingHost); + if (NS_WARN_IF(inclusiveAncestorOfRightChildBlockOrError.isErr()) || + !inclusiveAncestorOfRightChildBlockOrError.inspect()) { + return WSScanResult::Error(); + } + const WSScanResult prevVisibleThingBeforeCurrentBlock = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint( + inclusiveAncestorOfRightChildBlockOrError.inspect()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!prevVisibleThingBeforeCurrentBlock.ReachedBRElement() && + !prevVisibleThingBeforeCurrentBlock.ReachedPreformattedLineBreak()) { + return WSScanResult::Error(); + } + // There is a preceding line break, but it may be invisible. Then, users + // want to delete its preceding content not only the line break. + // Therefore, let's check whether the line break follows another line + // break or a block boundary. In these cases, the line break causes an + // empty line which users may want to delete. + const auto atPrecedingLineBreak = + prevVisibleThingBeforeCurrentBlock + .PointAtReachedContent<EditorRawDOMPoint>(); + MOZ_ASSERT(atPrecedingLineBreak.IsSet()); + const WSScanResult prevVisibleThingBeforeLineBreak = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, atPrecedingLineBreak, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingBeforeLineBreak.ReachedBRElement() || + prevVisibleThingBeforeLineBreak.ReachedPreformattedLineBreak() || + prevVisibleThingBeforeLineBreak.ReachedCurrentBlockBoundary()) { + // Target the latter line break for things simpler. It's easier to + // compute the target range. + MOZ_ASSERT_IF( + prevVisibleThingBeforeCurrentBlock.ReachedPreformattedLineBreak() && + prevVisibleThingBeforeLineBreak.ReachedPreformattedLineBreak(), + prevVisibleThingBeforeCurrentBlock + .PointAtReachedContent<EditorRawDOMPoint>() != + prevVisibleThingBeforeLineBreak + .PointAtReachedContent<EditorRawDOMPoint>()); + return prevVisibleThingBeforeCurrentBlock; + } + return WSScanResult::Error(); + }(); + + // If previous visible thing is a <br>, we should just delete it without + // unwrapping the first line of the right child block. Note that the <br> + // is always treated as invisible by HTMLEditUtils because it's immediately + // preceding <br> of the block boundary. However, deleting it is fine + // because the above checks whether it causes empty line or not. + if (prevVisibleThing.ReachedBRElement()) { + mMode = Mode::DeletePrecedingBRElementOfBlock; + mBRElement = prevVisibleThing.BRElementPtr(); + return true; + } + + // Same for a preformatted line break. + if (prevVisibleThing.ReachedPreformattedLineBreak()) { + mMode = Mode::DeletePrecedingPreformattedLineBreak; + mPreformattedLineBreak = + prevVisibleThing.PointAtReachedContent<EditorRawDOMPoint>() + .AsInText(); + return true; + } + mLeftContent = ScanJoinTarget(); mRightContent = aCaretPoint.GetContainerAs<nsIContent>(); } else { @@ -3283,7 +3635,8 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteNonCollapsedRanges( continue; } AutoBlockElementsJoiner joiner(*this); - if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range)) { + if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range, + aEditingHost)) { return NS_ERROR_FAILURE; } nsresult rv = @@ -3473,7 +3826,8 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges( continue; } AutoBlockElementsJoiner joiner(*this); - if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range)) { + if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range, + aEditingHost)) { return Err(NS_ERROR_FAILURE); } Result<EditActionResult, nsresult> result = @@ -3490,7 +3844,8 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges( bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: PrepareToDeleteNonCollapsedRange(const HTMLEditor& aHTMLEditor, - const nsRange& aRangeToDelete) { + const nsRange& aRangeToDelete, + const Element& aEditingHost) { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); MOZ_ASSERT(!aRangeToDelete.Collapsed()); @@ -3528,6 +3883,125 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return true; } + // If the range starts immediately after a line end and ends in a + // child right block, we should not unwrap the right block unless the + // right block will have no nodes. + if (mRightContent->IsInclusiveDescendantOf(mLeftContent)) { + // FYI: Chrome does not remove the right child block even if there will be + // only single <br> or a comment node in it. Therefore, we should use this + // rough check. + const WSScanResult nextVisibleThingOfEndBoundary = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, EditorRawDOMPoint(aRangeToDelete.EndRef()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!nextVisibleThingOfEndBoundary.ReachedCurrentBlockBoundary()) { + MOZ_ASSERT(mLeftContent->IsElement()); + Result<Element*, nsresult> mostDistantBlockOrError = + AutoBlockElementsJoiner:: + GetMostDistantBlockAncestorIfPointIsStartAtBlock( + EditorRawDOMPoint(mRightContent, 0), aEditingHost, + mLeftContent->AsElement()); + MOZ_ASSERT(mostDistantBlockOrError.isOk()); + if (MOZ_LIKELY(mostDistantBlockOrError.inspect())) { + const WSScanResult prevVisibleThingOfStartBoundary = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, EditorRawDOMPoint(aRangeToDelete.StartRef()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingOfStartBoundary.ReachedBRElement()) { + // If the range start after a <br> followed by the block boundary, + // we want to delete the <br> or following <br> element unless it's + // not a part of empty line like `<div>abc<br>{<div>]def`. + const WSScanResult nextVisibleThingOfBR = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint::After( + *prevVisibleThingOfStartBoundary.GetContent()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + MOZ_ASSERT(!nextVisibleThingOfBR.ReachedCurrentBlockBoundary()); + if (!nextVisibleThingOfBR.ReachedOtherBlockElement() || + nextVisibleThingOfBR.GetContent() != + mostDistantBlockOrError.inspect()) { + // The range selects a non-empty line or a child block at least. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + const WSScanResult prevVisibleThingOfBR = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint( + prevVisibleThingOfStartBoundary.GetContent()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingOfBR.ReachedBRElement() || + prevVisibleThingOfBR.ReachedPreformattedLineBreak() || + prevVisibleThingOfBR.ReachedBlockBoundary()) { + // The preceding <br> causes an empty line. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } else if (prevVisibleThingOfStartBoundary + .ReachedPreformattedLineBreak()) { + const WSScanResult nextVisibleThingOfLineBreak = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAfterReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + MOZ_ASSERT( + !nextVisibleThingOfLineBreak.ReachedCurrentBlockBoundary()); + if (!nextVisibleThingOfLineBreak.ReachedOtherBlockElement() || + nextVisibleThingOfLineBreak.GetContent() != + mostDistantBlockOrError.inspect()) { + // The range selects a non-empty line or a child block at least. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + const WSScanResult prevVisibleThingOfLineBreak = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAtReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (prevVisibleThingOfLineBreak.ReachedBRElement() || + prevVisibleThingOfLineBreak.ReachedPreformattedLineBreak() || + prevVisibleThingOfLineBreak.ReachedBlockBoundary()) { + // The preceding line break causes an empty line. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } else if (prevVisibleThingOfStartBoundary + .ReachedCurrentBlockBoundary()) { + MOZ_ASSERT(prevVisibleThingOfStartBoundary.ElementPtr() == + mLeftContent); + const WSScanResult firstVisibleThingInBlock = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint( + prevVisibleThingOfStartBoundary.ElementPtr(), 0), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!firstVisibleThingInBlock.ReachedOtherBlockElement() || + firstVisibleThingInBlock.ElementPtr() != + mostDistantBlockOrError.inspect()) { + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } else if (prevVisibleThingOfStartBoundary.ReachedOtherBlockElement()) { + const WSScanResult firstVisibleThingAfterBlock = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint::After( + *prevVisibleThingOfStartBoundary.ElementPtr()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!firstVisibleThingAfterBlock.ReachedOtherBlockElement() || + firstVisibleThingAfterBlock.ElementPtr() != + mostDistantBlockOrError.inspect()) { + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } + } + } + } + mMode = Mode::DeleteNonCollapsedRange; return true; } @@ -3744,12 +4218,12 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: aHTMLEditor.GetEditAction())) { WSRunScanner scanner(&aEditingHost, startOfRightContent, BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult maybePreviousText = + const WSScanResult maybePreviousText = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent); if (maybePreviousText.IsContentEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters()) { nsresult rv = aHTMLEditor.CollapseSelectionTo( - maybePreviousText.Point<EditorRawDOMPoint>()); + maybePreviousText.PointAfterReachedContent<EditorRawDOMPoint>()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); @@ -3851,6 +4325,16 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) const { + switch (mMode) { + case Mode::DeletePrecedingLinesAndContentInRange: + case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: + return false; + default: + break; + } + // If original selection was collapsed, we need always to join the nodes. // XXX Why? if (aSelectionWasCollapsed == @@ -3890,41 +4374,103 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange) { EditorDOMPoint rangeStart(aRange.StartRef()); EditorDOMPoint rangeEnd(aRange.EndRef()); - if (rangeStart.IsInTextNode() && !rangeStart.IsEndOfContainer()) { - // Delete to last character + if (MOZ_UNLIKELY(aRange.Collapsed())) { + return NS_OK; + } + + EditorDOMPoint pointToPutCaret; + // If the range is in a text node, delete middle of the text or the text node + // itself. + if (rangeStart.IsInTextNode() && + rangeStart.ContainerAs<Text>() == rangeEnd.GetContainer()) { OwningNonNull<Text> textNode = *rangeStart.ContainerAs<Text>(); - Result<CaretPoint, nsresult> caretPointOrError = - aHTMLEditor.DeleteTextWithTransaction( - textNode, rangeStart.Offset(), - rangeStart.GetContainer()->Length() - rangeStart.Offset()); - if (MOZ_UNLIKELY(caretPointOrError.isErr())) { - NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); - return caretPointOrError.unwrapErr(); + if (rangeStart.IsStartOfContainer() && rangeEnd.IsEndOfContainer()) { + EditorDOMPoint pointToPutCaret(textNode); + AutoTrackDOMPoint trackTextNodePoint(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } else { + MOZ_ASSERT(rangeEnd.Offset() - rangeStart.Offset() > 0); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + textNode, rangeStart.Offset(), + rangeEnd.Offset() - rangeStart.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); } - nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( - aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, - SuggestCaret::OnlyIfTransactionsAllowedToDoIt, - SuggestCaret::AndIgnoreTrivialError}); - if (NS_FAILED(rv)) { - NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); - return rv; + } else { + // If the range starts in a text node and ends in a different node, delete + // the text after the start boundary. + if (rangeStart.IsInTextNode() && !rangeStart.IsEndOfContainer()) { + OwningNonNull<Text> textNode = *rangeStart.ContainerAs<Text>(); + if (rangeStart.IsStartOfContainer()) { + pointToPutCaret.Set(textNode); + AutoTrackDOMPoint trackTextNodePoint(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } else { + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + textNode, rangeStart.Offset(), + rangeStart.GetContainer()->Length() - rangeStart.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } } - NS_WARNING_ASSERTION( - rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, - "CaretPoint::SuggestCaretPointTo() failed, but ignored"); - } - if (rangeEnd.IsInTextNode() && !rangeEnd.IsStartOfContainer()) { - // Delete to first character - OwningNonNull<Text> textNode = *rangeEnd.ContainerAs<Text>(); - Result<CaretPoint, nsresult> caretPointOrError = - aHTMLEditor.DeleteTextWithTransaction(textNode, 0, rangeEnd.Offset()); - if (MOZ_UNLIKELY(caretPointOrError.isErr())) { - NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); - return caretPointOrError.unwrapErr(); + + // If the range ends in a text node and starts from a different node, delete + // the text before the end boundary. + if (rangeEnd.IsInTextNode() && !rangeEnd.IsStartOfContainer()) { + OwningNonNull<Text> textNode = *rangeEnd.ContainerAs<Text>(); + if (rangeEnd.IsEndOfContainer()) { + pointToPutCaret.Set(textNode); + AutoTrackDOMPoint trackTextNodePoint(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(textNode); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } else { + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction(textNode, 0, + rangeEnd.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } } - nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( - aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, - SuggestCaret::OnlyIfTransactionsAllowedToDoIt, + } + + if (pointToPutCaret.IsSet()) { + CaretPoint caretPoint(std::move(pointToPutCaret)); + nsresult rv = caretPoint.SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); @@ -3937,6 +4483,201 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return NS_OK; } +// static +template <typename EditorDOMPointType> +Result<Element*, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::GetMostDistantBlockAncestorIfPointIsStartAtBlock( + const EditorDOMPointType& aPoint, const Element& aEditingHost, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT(aPoint.IsSetAndValid()); + MOZ_ASSERT(aPoint.IsInComposedDoc()); + + if (!aAncestorLimiter) { + aAncestorLimiter = &aEditingHost; + } + + const auto ReachedCurrentBlockBoundaryWhichWeCanCross = + [&aEditingHost, aAncestorLimiter](const WSScanResult& aScanResult) { + // When the scan result is "reached current block boundary", it may not + // be so literally. + return aScanResult.ReachedCurrentBlockBoundary() && + HTMLEditUtils::IsRemovableFromParentNode( + *aScanResult.ElementPtr()) && + aScanResult.ElementPtr() != &aEditingHost && + aScanResult.ElementPtr() != aAncestorLimiter && + // Don't cross <body>, <head> and <html> + !aScanResult.ElementPtr()->IsAnyOfHTMLElements( + nsGkAtoms::body, nsGkAtoms::head, nsGkAtoms::html) && + // Don't cross table elements + !HTMLEditUtils::IsAnyTableElement(aScanResult.ElementPtr()); + }; + + const WSScanResult prevVisibleThing = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + aAncestorLimiter, aPoint, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!ReachedCurrentBlockBoundaryWhichWeCanCross(prevVisibleThing)) { + return nullptr; + } + MOZ_ASSERT(HTMLEditUtils::IsBlockElement( + *prevVisibleThing.ElementPtr(), + BlockInlineCheck::UseComputedDisplayOutsideStyle)); + for (Element* ancestorBlock = prevVisibleThing.ElementPtr(); ancestorBlock;) { + const WSScanResult prevVisibleThing = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + aAncestorLimiter, EditorRawDOMPoint(ancestorBlock), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (!ReachedCurrentBlockBoundaryWhichWeCanCross(prevVisibleThing)) { + return ancestorBlock; + } + MOZ_ASSERT(HTMLEditUtils::IsBlockElement( + *prevVisibleThing.ElementPtr(), + BlockInlineCheck::UseComputedDisplayOutsideStyle)); + ancestorBlock = prevVisibleThing.ElementPtr(); + } + return Err(NS_ERROR_FAILURE); +} + +void HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ExtendRangeToDeleteNonCollapsedRange( + const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, + const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const { + MOZ_ASSERT_IF(aComputeRangeFor == ComputeRangeFor::GetTargetRanges, + aRangeToDelete.IsPositioned()); + MOZ_ASSERT(!aRangeToDelete.Collapsed()); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf( + mLeftContent)); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + MOZ_ASSERT( + aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent)); + + const DebugOnly<Result<bool, nsresult>> extendRangeResult = + AutoDeleteRangesHandler:: + ExtendRangeToContainAncestorInlineElementsAtStart(aRangeToDelete, + aEditingHost); + NS_WARNING_ASSERTION(extendRangeResult.value.isOk(), + "AutoDeleteRangesHandler::" + "ExtendRangeToContainAncestorInlineElementsAtStart() " + "failed, but ignored"); + if (mMode != Mode::DeletePrecedingLinesAndContentInRange) { + return; + } + + // If we're computing for beforeinput.getTargetRanges() and the inputType + // is not a simple deletion like replacing selected content with new + // content, the range should end at the original end boundary of the given + // range even if we're deleting only preceding lines of the right child + // block. + const bool preserveEndBoundary = + aComputeRangeFor == ComputeRangeFor::GetTargetRanges && + !MayEditActionDeleteAroundCollapsedSelection(aHTMLEditor.GetEditAction()); + // We need to delete only the preceding lines of the right block. Therefore, + // we need to shrink the range to ends before the right block if the range + // does not contain any meaningful content in the right block. + const Result<Element*, nsresult> inclusiveAncestorCurrentBlockOrError = + AutoBlockElementsJoiner::GetMostDistantBlockAncestorIfPointIsStartAtBlock( + EditorRawDOMPoint(aRangeToDelete.EndRef()), aEditingHost, + mLeftContent->AsElement()); + MOZ_ASSERT(inclusiveAncestorCurrentBlockOrError.isOk()); + MOZ_ASSERT_IF(inclusiveAncestorCurrentBlockOrError.inspect(), + mRightContent->IsInclusiveDescendantOf( + inclusiveAncestorCurrentBlockOrError.inspect())); + if (MOZ_UNLIKELY(!inclusiveAncestorCurrentBlockOrError.isOk() || + !inclusiveAncestorCurrentBlockOrError.inspect())) { + return; + } + + const WSScanResult prevVisibleThingOfStartBoundary = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, EditorRawDOMPoint(aRangeToDelete.StartRef()), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + // If the range starts after an invisible <br> of empty line immediately + // before the most distant inclusive ancestor of the right block like + // `<br><br>{<div>]abc`, we should delete the last empty line because + // users won't see any reaction of the builtin editor in this case. + if (prevVisibleThingOfStartBoundary.ReachedBRElement() || + prevVisibleThingOfStartBoundary.ReachedPreformattedLineBreak()) { + const WSScanResult prevVisibleThingOfPreviousLineBreak = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAtReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + const WSScanResult nextVisibleThingOfPreviousBR = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAfterReachedContent<EditorRawDOMPoint>(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if ((prevVisibleThingOfPreviousLineBreak.ReachedBRElement() || + prevVisibleThingOfPreviousLineBreak.ReachedPreformattedLineBreak()) && + nextVisibleThingOfPreviousBR.ReachedOtherBlockElement() && + nextVisibleThingOfPreviousBR.ElementPtr() == + inclusiveAncestorCurrentBlockOrError.inspect()) { + aRangeToDelete.SetStart(prevVisibleThingOfStartBoundary + .PointAtReachedContent<EditorRawDOMPoint>() + .ToRawRangeBoundary(), + IgnoreErrors()); + } + } + + if (preserveEndBoundary) { + return; + } + + if (aComputeRangeFor == ComputeRangeFor::GetTargetRanges) { + // When we set the end boundary to around the right block, the new end + // boundary should not after inline ancestors of the line break which won't + // be deleted. + const WSScanResult lastVisibleThingBeforeRightChildBlock = + [&]() -> WSScanResult { + EditorRawDOMPoint scanStartPoint(aRangeToDelete.StartRef()); + WSScanResult lastScanResult = WSScanResult::Error(); + while (true) { + WSScanResult scanResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + mLeftContent->AsElement(), scanStartPoint, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if (scanResult.ReachedBlockBoundary() || + scanResult.ReachedInlineEditingHostBoundary()) { + return lastScanResult; + } + scanStartPoint = + scanResult.PointAfterReachedContent<EditorRawDOMPoint>(); + lastScanResult = scanResult; + } + }(); + if (lastVisibleThingBeforeRightChildBlock.GetContent()) { + const nsIContent* commonAncestor = nsIContent::FromNode( + nsContentUtils::GetClosestCommonInclusiveAncestor( + aRangeToDelete.StartRef().Container(), + lastVisibleThingBeforeRightChildBlock.GetContent())); + MOZ_ASSERT(commonAncestor); + if (commonAncestor && + !mRightContent->IsInclusiveDescendantOf(commonAncestor)) { + IgnoredErrorResult error; + aRangeToDelete.SetEnd( + EditorRawDOMPoint::AtEndOf(*commonAncestor).ToRawRangeBoundary(), + error); + NS_WARNING_ASSERTION(!error.Failed(), + "nsRange::SetEnd() failed, but ignored"); + return; + } + } + } + + IgnoredErrorResult error; + aRangeToDelete.SetEnd( + EditorRawDOMPoint(inclusiveAncestorCurrentBlockOrError.inspect()) + .ToRawRangeBoundary(), + error); + NS_WARNING_ASSERTION(!error.Failed(), + "nsRange::SetEnd() failed, but ignored"); +} + nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: ComputeRangeToDeleteNonCollapsedRange( const HTMLEditor& aHTMLEditor, @@ -3954,6 +4695,10 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: MOZ_ASSERT( aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent)); + ExtendRangeToDeleteNonCollapsedRange(aHTMLEditor, aRangeToDelete, + aEditingHost, + ComputeRangeFor::GetTargetRanges); + Result<bool, nsresult> result = ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure( aHTMLEditor, aRangeToDelete, aSelectionWasCollapsed); @@ -4003,14 +4748,20 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); MOZ_ASSERT(!aRangeToDelete.Collapsed()); MOZ_ASSERT(mDeleteRangesHandler); - MOZ_ASSERT(mLeftContent); - MOZ_ASSERT(mLeftContent->IsElement()); - MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf( - mLeftContent)); - MOZ_ASSERT(mRightContent); - MOZ_ASSERT(mRightContent->IsElement()); - MOZ_ASSERT( - aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent)); + + const bool isDeletingLineBreak = + mMode == Mode::DeleteBRElement || + mMode == Mode::DeletePrecedingBRElementOfBlock || + mMode == Mode::DeletePrecedingPreformattedLineBreak; + if (!isDeletingLineBreak) { + MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf( + mLeftContent)); + MOZ_ASSERT(aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf( + mRightContent)); + ExtendRangeToDeleteNonCollapsedRange(aHTMLEditor, aRangeToDelete, + aEditingHost, + ComputeRangeFor::ToDeleteTheRange); + } const bool backspaceInRightBlock = aSelectionWasCollapsed == SelectionWasCollapsed::Yes && @@ -4034,7 +4785,8 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: return deleteResult.propagateErr(); } - const bool joinInclusiveAncestorBlockElements = deleteResult.unwrap(); + const bool joinInclusiveAncestorBlockElements = + !isDeletingLineBreak && deleteResult.unwrap(); // Check endpoints for possible text deletion. We can assume that if // text node is found, we can delete to end or to beginning as @@ -4048,9 +4800,24 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: } if (!joinInclusiveAncestorBlockElements) { + // When we delete only preceding lines of the right child block, we should + // put caret into start of the right block. + if (mMode == Mode::DeletePrecedingLinesAndContentInRange) { + result.MarkAsHandled(); + if (MOZ_LIKELY(mRightContent->IsInComposedDoc())) { + pointToPutCaret = + HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>( + *mRightContent); + } + } break; } + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, *mRightContent); Result<bool, nsresult> canJoinThem = @@ -4106,6 +4873,12 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: break; } + // HandleDeleteLineBreak() should handle the new caret position by itself. + if (isDeletingLineBreak) { + result.MarkAsHandled(); + return result; + } + // If we're deleting selection (not replacing with new content) and // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we // should use it. Otherwise, we should keep the traditional behavior. @@ -4297,7 +5070,8 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost(); WSRunScanner wsScannerForPoint( editingHost, aPoint, BlockInlineCheck::UseComputedDisplayOutsideStyle); - if (!wsScannerForPoint.StartsFromCurrentBlockBoundary()) { + if (!wsScannerForPoint.StartsFromCurrentBlockBoundary() && + !wsScannerForPoint.StartsFromInlineEditingHostBoundary()) { // If there is visible node before the point, we shouldn't remove the // parent block. return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; @@ -4319,8 +5093,8 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( } // Next, check there is visible contents after the point in current block. - WSScanResult forwardScanFromPointResult = - wsScannerForPoint.ScanNextVisibleNodeOrBlockBoundaryFrom(aPoint); + const WSScanResult forwardScanFromPointResult = + wsScannerForPoint.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(aPoint); if (forwardScanFromPointResult.Failed()) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return NS_ERROR_FAILURE; @@ -4340,8 +5114,8 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } if (wsScannerForPoint.GetEndReasonContent()->GetNextSibling()) { - WSScanResult scanResult = - WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + const WSScanResult scanResult = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( editingHost, EditorRawDOMPoint::After( *wsScannerForPoint.GetEndReasonContent()), @@ -4350,13 +5124,15 @@ HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return NS_ERROR_FAILURE; } - if (!scanResult.ReachedCurrentBlockBoundary()) { + if (!scanResult.ReachedCurrentBlockBoundary() && + !scanResult.ReachedInlineEditingHostBoundary()) { // If we couldn't reach the block's end after the invisible <br>, // that means that there is visible content. return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } } - } else if (!forwardScanFromPointResult.ReachedCurrentBlockBoundary()) { + } else if (!forwardScanFromPointResult.ReachedCurrentBlockBoundary() && + !forwardScanFromPointResult.ReachedInlineEditingHostBoundary()) { // If we couldn't reach the block's end, the block has visible content. return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; } @@ -5169,11 +5945,12 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: aHTMLEditor.GetEditAction())) { WSRunScanner scanner(&aEditingHost, startOfRightContent, BlockInlineCheck::UseComputedDisplayStyle); - WSScanResult maybePreviousText = + const WSScanResult maybePreviousText = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent); if (maybePreviousText.IsContentEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters()) { - mPointToPutCaret = maybePreviousText.Point<EditorDOMPoint>(); + mPointToPutCaret = + maybePreviousText.PointAfterReachedContent<EditorDOMPoint>(); } } return result; @@ -6798,11 +7575,12 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( if (rangeToDelete.StartRef().GetContainer() != closestBlockAncestorOrInlineEditingHost) { for (;;) { - WSScanResult backwardScanFromStartResult = + const WSScanResult backwardScanFromStartResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( closestEditingHost, rangeToDelete.StartRef(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary()) { + if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary() && + !backwardScanFromStartResult.ReachedInlineEditingHostBoundary()) { break; } MOZ_ASSERT(backwardScanFromStartResult.GetContent() == @@ -6834,8 +7612,8 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( *backwardScanFromStartResult.ElementPtr())) { break; } - rangeToDelete.SetStart( - backwardScanFromStartResult.PointAtContent<EditorRawDOMPoint>()); + rangeToDelete.SetStart(backwardScanFromStartResult + .PointAtReachedContent<EditorRawDOMPoint>()); } if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint( rangeToDelete.StartRef().GetContainer())) { @@ -6856,8 +7634,8 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( WSRunScanner wsScannerAtEnd( closestEditingHost, rangeToDelete.EndRef(), BlockInlineCheck::UseComputedDisplayOutsideStyle); - WSScanResult forwardScanFromEndResult = - wsScannerAtEnd.ScanNextVisibleNodeOrBlockBoundaryFrom( + const WSScanResult forwardScanFromEndResult = + wsScannerAtEnd.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( rangeToDelete.EndRef()); if (forwardScanFromEndResult.ReachedBRElement()) { // XXX In my understanding, this is odd. The end reason may not be @@ -6881,7 +7659,9 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( continue; } - if (forwardScanFromEndResult.ReachedCurrentBlockBoundary()) { + if (forwardScanFromEndResult.ReachedCurrentBlockBoundary() || + forwardScanFromEndResult.ReachedInlineEditingHostBoundary()) { + MOZ_ASSERT(forwardScanFromEndResult.ContentIsElement()); MOZ_ASSERT(forwardScanFromEndResult.GetContent() == wsScannerAtEnd.GetEndReasonContent()); // We want to keep looking up. But stop if we are crossing table @@ -6895,13 +7675,13 @@ HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( // Don't cross flex-item/grid-item boundary to make new content inserted // into it. if (StaticPrefs::editor_block_inline_check_use_computed_style() && - forwardScanFromEndResult.ContentIsElement() && HTMLEditUtils::IsFlexOrGridItem( *forwardScanFromEndResult.ElementPtr())) { break; } rangeToDelete.SetEnd( - forwardScanFromEndResult.PointAfterContent<EditorRawDOMPoint>()); + forwardScanFromEndResult + .PointAfterReachedContent<EditorRawDOMPoint>()); continue; } |