From 398b556e9032489c4cceb4d79579aaa14d9c6caa Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Sat, 27 Apr 2024 00:36:26 +0000 Subject: [PATCH] Bug 1877513 - Make `HTMLEditor` deletes only preceding lines of right child block if the range starts from start of a line r=m_kato Currently, the editor of Gecko always unwraps first line of the right child block after deleting selected range when the range starts in a parent block and ends in a child block. This behavior is almost same as the other browsers, but the other browsers deletes only preceding lines of the right child block (i.e., without unwrapping the first line of the right child block) if the range starts from start of a preceding line, for example, when deleting `
abc
[def

g]hi
jkl`, Gecko moves "hi" to the parent `

`, but the other browsers keeps it in the child `

`. For emulating this special handling, we need to touch 2 paths. One is `Backspace` when selection is collapsed at start of the child block. In this case, only when the preceding line is empty, i.e., there are 2 line breaks (either `
` or `\n` in `white-space: pre-*`), the following break should be deleted, but the child block should not be touched. The other is, deleting when selection is not collapsed or `Delete` when selection is collapsed at immediately before the child block. In the latter case, `HTMLEditor::HandleDeleteSelection` extends `Selection` using `nsFrameSelection`. Then, handle it with same path as deleting non-collapsed range. The former is handled with `HandleDeleteLineBreak` and `ComputeRangeToDeleteLineBreak`. The latter is handled with `HandleDeleteNonCollapsedRange` and `ComputeRangeToDeleteNonCollapsedRange`. The new handlers use the `ComputeRangeToDelete*`. Therefore, `beforeinput` reports exactly same range from `getTargetRanges`. However, existing paths do not use same approach and this patch makes `HandleDeleteNonCollapsedRange` fall it back to `HandleDeleteNonCollapsedRange`. Therefore, some `if` checks in `HandleDeleteNonCollapsedRange` are ugly, but I have no better idea to implement this smarter. Differential Revision: https://phabricator.services.mozilla.com/D207690 --- editor/libeditor/EditAction.h | 1 + editor/libeditor/HTMLEditorDeleteHandler.cpp | 822 ++++++++++++- .../meta/editing/run/delete.html.ini | 6 - .../meta/editing/run/forwarddelete.html.ini | 3 - ...-deleting-in-list-items.tentative.html.ini | 24 - .../web-platform/tests/editing/data/delete.js | 6 +- .../tests/editing/data/forwarddelete.js | 26 +- .../editing/include/editor-test-utils.js | 75 ++ ...-unwrapping-first-line-of-child-block.html | 1020 +++++++++++++++++ 9 files changed, 1876 insertions(+), 107 deletions(-) create mode 100644 testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html diff --git a/editor/libeditor/EditAction.h b/editor/libeditor/EditAction.h index f74d1c6949f6..6133d82f4aeb 100644 --- a/editor/libeditor/EditAction.h +++ b/editor/libeditor/EditAction.h @@ -642,6 +642,7 @@ inline EditorInputType ToInputType(EditAction aEditAction) { inline bool MayEditActionDeleteAroundCollapsedSelection( const EditAction aEditAction) { switch (aEditAction) { + case EditAction::eCut: case EditAction::eDeleteSelection: case EditAction::eDeleteBackward: case EditAction::eDeleteForward: diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp index 443faa66d9ea..8c58979001df 100644 --- a/editor/libeditor/HTMLEditorDeleteHandler.cpp +++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -352,6 +352,28 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection, 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 + 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 @@ -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 result = - DeleteBRElement(aHTMLEditor, aDirectionAndAmount, aEditingHost); + case Mode::DeleteBRElement: + case Mode::DeletePrecedingBRElementOfBlock: + case Mode::DeletePrecedingPreformattedLineBreak: { + Result 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 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 - 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 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 `

abc
[]def
`, return + * #div2. + */ + template + static Result + GetMostDistantBlockAncestorIfPointIsStartAtBlock( + const EditorDOMPointType& aPoint, const Element& aEditingHost, + const Element* aAncestorLimiter = nullptr); + + /** + * Extend aRangeToDelete to contain new empty inline ancestors and contain + * an invisible
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
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, 8> mSkippedInvisibleContents; RefPtr mBRElement; + EditorDOMPointInText mPreformattedLineBreak; Mode mMode = Mode::NotInitialized; }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner @@ -1864,7 +1939,7 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( aHTMLEditor, aDirectionAndAmount, *aScanFromCaretPointResult.ElementPtr(), - aWSRunScannerAtCaret.ScanStartRef())) { + aWSRunScannerAtCaret.ScanStartRef(), aEditingHost)) { continue; } handled = true; @@ -2065,7 +2140,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( aHTMLEditor, aDirectionAndAmount, *aScanFromCaretPointResult.ElementPtr(), - aWSRunScannerAtCaret.ScanStartRef())) { + aWSRunScannerAtCaret.ScanStartRef(), aEditingHost)) { continue; } allRangesNotHandled = false; @@ -2488,6 +2563,109 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAtomicContent( return CaretPoint(std::move(pointToPutCaret)); } +// static +Result 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()); + 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()); + 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(), + 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()); + 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, @@ -2541,27 +2719,116 @@ 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 - // `
` element? + ComputeRangeToDeleteLineBreak(const HTMLEditor& aHTMLEditor, + nsRange& aRangeToDelete, + const Element& aEditingHost, + ComputeRangeFor aComputeRangeFor) const { + // FIXME: Scan invisible leading white-spaces after the
. + 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()->TextDataLength() == 1 + ? HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + *mPreformattedLineBreak.ContainerAs(), + 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 -HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( - HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, - const Element& aEditingHost) { +Result 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(); @@ -2586,14 +2853,25 @@ HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( return EditorDOMPoint(); }(); - // If we found a `
` element, we should delete it instead of joining the - // contents. + RefPtr rangeToDelete = + nsRange::Create(const_cast(&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 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) != @@ -2602,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); } @@ -2612,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. @@ -2627,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(); } @@ -2942,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 @@ -2961,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) && @@ -2992,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)) { @@ -3041,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 + //
element if there is. + const Result + 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(); + 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() != + prevVisibleThingBeforeLineBreak + .PointAtReachedContent()); + return prevVisibleThingBeforeCurrentBlock; + } + return WSScanResult::Error(); + }(); + + // If previous visible thing is a
, we should just delete it without + // unwrapping the first line of the right child block. Note that the
+ // is always treated as invisible by HTMLEditUtils because it's immediately + // preceding
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() + .AsInText(); + return true; + } + mLeftContent = ScanJoinTarget(); mRightContent = aCaretPoint.GetContainerAs(); } else { @@ -3288,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 = @@ -3478,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 result = @@ -3495,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()); @@ -3533,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
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 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
followed by the block boundary, + // we want to delete the
or following
element unless it's + // not a part of empty line like `
abc
{
]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
causes an empty line. + mMode = Mode::DeletePrecedingLinesAndContentInRange; + return true; + } + } else if (prevVisibleThingOfStartBoundary + .ReachedPreformattedLineBreak()) { + const WSScanResult nextVisibleThingOfLineBreak = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAfterReachedContent(), + 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(), + 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; } @@ -3856,6 +4325,16 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: const nsTArray>& 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 == @@ -4004,6 +4483,201 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return NS_OK; } +// static +template +Result 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 , and + !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> 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 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
of empty line immediately + // before the most distant inclusive ancestor of the right block like + // `

{
]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(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + const WSScanResult nextVisibleThingOfPreviousBR = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( + &aEditingHost, + prevVisibleThingOfStartBoundary + .PointAfterReachedContent(), + BlockInlineCheck::UseComputedDisplayOutsideStyle); + if ((prevVisibleThingOfPreviousLineBreak.ReachedBRElement() || + prevVisibleThingOfPreviousLineBreak.ReachedPreformattedLineBreak()) && + nextVisibleThingOfPreviousBR.ReachedOtherBlockElement() && + nextVisibleThingOfPreviousBR.ElementPtr() == + inclusiveAncestorCurrentBlockOrError.inspect()) { + aRangeToDelete.SetStart(prevVisibleThingOfStartBoundary + .PointAtReachedContent() + .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(); + 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, @@ -4021,6 +4695,10 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: MOZ_ASSERT( aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent)); + ExtendRangeToDeleteNonCollapsedRange(aHTMLEditor, aRangeToDelete, + aEditingHost, + ComputeRangeFor::GetTargetRanges); + Result result = ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure( aHTMLEditor, aRangeToDelete, aSelectionWasCollapsed); @@ -4070,14 +4748,20 @@ Result 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 && @@ -4101,7 +4785,8 @@ Result 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 @@ -4115,9 +4800,24 @@ Result 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( + *mRightContent); + } + } break; } + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, *mRightContent); Result canJoinThem = @@ -4173,6 +4873,12 @@ Result 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. diff --git a/testing/web-platform/meta/editing/run/delete.html.ini b/testing/web-platform/meta/editing/run/delete.html.ini index 4c98409d7b18..d82d94957e8e 100644 --- a/testing/web-platform/meta/editing/run/delete.html.ini +++ b/testing/web-platform/meta/editing/run/delete.html.ini @@ -10,9 +10,6 @@ [[["delete",""\]\] "foo[\]baz" compare innerHTML] expected: FAIL - [[["delete",""\]\] "foo

[\]bar

" compare innerHTML] - expected: FAIL - [[["defaultparagraphseparator","div"\],["delete",""\]\] "foo

[\]bar

" compare innerHTML] expected: FAIL @@ -25,9 +22,6 @@ [[["delete",""\]\] "

foo


[\]bar

" compare innerHTML] expected: FAIL - [[["delete",""\]\] "

foo



[\]bar

" compare innerHTML] - expected: FAIL - [[["delete",""\]\] "foo[\]bar" compare innerHTML] expected: FAIL diff --git a/testing/web-platform/meta/editing/run/forwarddelete.html.ini b/testing/web-platform/meta/editing/run/forwarddelete.html.ini index 51934197f76a..51fb8428671f 100644 --- a/testing/web-platform/meta/editing/run/forwarddelete.html.ini +++ b/testing/web-platform/meta/editing/run/forwarddelete.html.ini @@ -436,9 +436,6 @@ [[["forwarddelete",""\]\] "
  1. foo

{}

  1. bar
" compare innerHTML] expected: FAIL - [[["forwarddelete",""\]\] "
    1. foo
  1. {}
    1. bar
": execCommand("forwarddelete", false, "") return value] - expected: FAIL - [[["forwarddelete",""\]\] "
    1. foo
  1. {}
    1. bar
" compare innerHTML] expected: FAIL diff --git a/testing/web-platform/meta/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html.ini b/testing/web-platform/meta/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html.ini index 41e6572e80ec..2136cc638053 100644 --- a/testing/web-platform/meta/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html.ini +++ b/testing/web-platform/meta/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html.ini @@ -7,9 +7,6 @@ [Backspace at "
  • list-item1[
  • }list-item2
    second line in list-item2
" - comparing innerHTML] expected: FAIL - [Backspace at "
  • list-item1
  • [list-item2
    1. list-item3
    2. }list-item4
" - comparing innerHTML] - expected: FAIL - [Backspace at "
    1. list-item1
    2. [list-item2
  • }list-item3
" - comparing innerHTML] expected: FAIL @@ -355,9 +352,6 @@ [Backspace at "
  • list-item1
  • [\]list-item2

" - comparing innerHTML] expected: FAIL - [Backspace at "
  • list-item1
  • [list-item2
    • list-item3
    • }list-item4
" - comparing innerHTML] - expected: FAIL - [Backspace at "
    • list-item1[
  1. list-item2\]
"] expected: FAIL @@ -875,15 +869,9 @@ [Delete at "
    1. [list-item1
  • list-item2\]
" - comparing innerHTML] expected: FAIL - [Delete at "
  • list-item1
  • [list-item2
    • list-item3
    • }list-item4
" - comparing innerHTML] - expected: FAIL - [Delete at "
    • [list-item1
  • list-item2\]
" - comparing innerHTML] expected: FAIL - [Delete at "
  • list-item1
  • [list-item2
    1. list-item3
    2. }list-item4
" - comparing innerHTML] - expected: FAIL - [Delete at "
  • [list-item1
  • list-item2\]
" - comparing innerHTML] expected: FAIL @@ -1128,9 +1116,6 @@ [Backspace at "
    1. list-item1[
  • list-item2\]
"] expected: FAIL - [Backspace at "
  1. list-item1
  2. [list-item2
    1. list-item3
    2. }list-item4
" - comparing innerHTML] - expected: FAIL - [Backspace at "
    • [list-item1
  • }list-item2
"] expected: FAIL @@ -1155,9 +1140,6 @@ [Backspace at "
  1. [list-item1
    1. }list-item2
" - comparing innerHTML] expected: FAIL - [Backspace at "
  1. list-item1
  2. [list-item2
    • list-item3
    • }list-item4
" - comparing innerHTML] - expected: FAIL - [Backspace at "
    • list-item1
    • [list-item2
  1. }list-item3
" - comparing innerHTML] expected: FAIL @@ -1718,9 +1700,6 @@ [Delete at "
    1. [list-item1
  1. list-item2\]
" - comparing innerHTML] expected: FAIL - [Delete at "
  1. list-item1
  2. [list-item2
    • list-item3
    • }list-item4
" - comparing innerHTML] - expected: FAIL - [Delete at "
    • [list-item1
  1. }list-item2
" - comparing innerHTML] expected: FAIL @@ -1847,9 +1826,6 @@ [Delete at "
  1. [list-item1
    1. list-item2\]
" - comparing innerHTML] expected: FAIL - [Delete at "
  1. list-item1
  2. [list-item2
    1. list-item3
    2. }list-item4
" - comparing innerHTML] - expected: FAIL - [Delete at "
    • [list-item1
  1. }list-item2
"] expected: FAIL diff --git a/testing/web-platform/tests/editing/data/delete.js b/testing/web-platform/tests/editing/data/delete.js index 131c99b1d5af..c4d1225ef35b 100644 --- a/testing/web-platform/tests/editing/data/delete.js +++ b/testing/web-platform/tests/editing/data/delete.js @@ -2044,12 +2044,12 @@ var browserTests = [ {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"delete":[false,false,"",false,false,""]}], ["foo

{

]bar

", [["defaultparagraphseparator","div"],["delete",""]], - "foo
{}bar", + "foo

bar

", [true,true], {"defaultparagraphseparator":[false,false,"p",false,false,"div"],"delete":[false,false,"",false,false,""]}], ["foo

{

]bar

", [["defaultparagraphseparator","p"],["delete",""]], - "foo
{}bar", + "foo

bar

", [true,true], {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"delete":[false,false,"",false,false,""]}], ["

foo
{

}bar

", @@ -2359,7 +2359,7 @@ var browserTests = [ {"delete":[false,false,"",false,false,""]}], ["
  1. foo
[bar
  1. ]baz
", [["delete",""]], - "
  1. foo
{}baz", + "
  1. foo
  1. baz
", [true], {"delete":[false,false,"",false,false,""]}], ["
  1. foo

[bar

  1. ]baz
", diff --git a/testing/web-platform/tests/editing/data/forwarddelete.js b/testing/web-platform/tests/editing/data/forwarddelete.js index ea590a4fbba1..a881fb6ccf46 100644 --- a/testing/web-platform/tests/editing/data/forwarddelete.js +++ b/testing/web-platform/tests/editing/data/forwarddelete.js @@ -2009,12 +2009,12 @@ var browserTests = [ {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}], ["foo

{

]bar

", [["defaultparagraphseparator","div"],["forwarddelete",""]], - "foo
{}bar", + "foo

bar

", [true,true], {"defaultparagraphseparator":[false,false,"p",false,false,"div"],"forwarddelete":[false,false,"",false,false,""]}], ["foo

{

]bar

", [["defaultparagraphseparator","p"],["forwarddelete",""]], - "foo
{}bar", + "foo

bar

", [true,true], {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}], ["

foo
{

}bar

", @@ -2184,7 +2184,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
{}
  1. bar
", [["forwarddelete",""]], - "
  1. foo
{}bar", + "
  1. foo
  1. bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo

{}

  1. bar
", @@ -2199,22 +2199,22 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
{}
  1. bar
", [["forwarddelete",""]], - "
  1. foo
{}bar", + "
  1. foo
  1. bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
{}
  1. bar
", [["forwarddelete",""]], - "
  1. foo
{}bar", + "
  1. foo
  1. bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
{}
  1. bar
", [["forwarddelete",""]], - "
  1. foo
{}bar", + "
  1. foo
  1. bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
{}
  1. bar
", [["forwarddelete",""]], - "
  1. foo
{}bar", + "
  1. foo
  1. bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
    1. foo
  1. {}
    1. bar
", @@ -2259,7 +2259,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
[bar
  1. ]baz
", [["forwarddelete",""]], - "
  1. foo
{}baz", + "
  1. foo
  1. baz
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo

[bar

  1. ]baz
", @@ -2269,12 +2269,12 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo

[bar

  1. ]baz

", [["defaultparagraphseparator","div"],["forwarddelete",""]], - "
  1. foo

{}baz

", + "
  1. foo
  1. {}baz

", [true,true], {"defaultparagraphseparator":[false,false,"p",false,false,"div"],"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo

[bar

  1. ]baz

", [["defaultparagraphseparator","p"],["forwarddelete",""]], - "
  1. foo

{}baz

", + "
  1. foo
  1. {}baz

", [true,true], {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
  1. []bar
", @@ -2289,7 +2289,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["
  • foo
{}
  • bar
", [["forwarddelete",""]], - "
  • foo
{}bar", + "
  • foo
  • bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  • foo

{}

  • bar
", @@ -2304,7 +2304,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo
{}
  • bar
", [["forwarddelete",""]], - "
  1. foo
{}bar", + "
  1. foo
  • bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  1. foo

{}

  • bar
", @@ -2314,7 +2314,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["
  • foo
{}
  1. bar
", [["forwarddelete",""]], - "
  • foo
{}bar", + "
  • foo
  1. bar
", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["
  • foo

{}

  1. bar
", diff --git a/testing/web-platform/tests/editing/include/editor-test-utils.js b/testing/web-platform/tests/editing/include/editor-test-utils.js index d0d50d22a666..b180f3343fde 100644 --- a/testing/web-platform/tests/editing/include/editor-test-utils.js +++ b/testing/web-platform/tests/editing/include/editor-test-utils.js @@ -424,4 +424,79 @@ class EditorTestUtils { ); } } + + static getRangeArrayDescription(arrayOfRanges) { + if (arrayOfRanges === null) { + return "null"; + } + if (arrayOfRanges === undefined) { + return "undefined"; + } + if (!Array.isArray(arrayOfRanges)) { + return "Unknown Object"; + } + if (arrayOfRanges.length === 0) { + return "[]"; + } + let result = ""; + for (let range of arrayOfRanges) { + if (result === "") { + result = "["; + } else { + result += ","; + } + result += `{${EditorTestUtils.getRangeDescription(range)}}`; + } + result += "]"; + return result; + } + + static getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + case Node.COMMENT_NODE: + case Node.CDATA_SECTION_NODE: + return `${node.nodeName} "${node.data.replaceAll("\n", "\\\\n")}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}${ + node.hasAttribute("id") ? ` id="${node.getAttribute("id")}"` : "" + }${ + node.hasAttribute("class") ? ` class="${node.getAttribute("class")}"` : "" + }${ + node.hasAttribute("contenteditable") + ? ` contenteditable="${node.getAttribute("contenteditable")}"` + : "" + }${ + node.inert ? ` inert` : "" + }${ + node.hidden ? ` hidden` : "" + }${ + node.readonly ? ` readonly` : "" + }${ + node.disabled ? ` disabled` : "" + }>`; + default: + return `${node.nodeName}`; + } + } + + static getRangeDescription(range) { + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${EditorTestUtils.getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${EditorTestUtils.getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${EditorTestUtils.getNodeDescription(range.endContainer)}, ${range.endOffset})`; + } + + } diff --git a/testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html b/testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html new file mode 100644 index 000000000000..99f8f058888d --- /dev/null +++ b/testing/web-platform/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html @@ -0,0 +1,1020 @@ + + + + + + + + + + + + + +Tests for deleting preceding lines of right child block if range ends at start of the right child + + + + + + + + +
+