Bug 1951518 - Make HTMLEditor::DeleteRangesWithTransaction stop deleting Text which has only a collapsible white-space r=m_kato

In bug 1925635, I moved the post processing after deleting ranges in
`EditorBase::DeleteRangesWithTransaction` into the `HTMLEditor`'s override.
At this time, I makes it uses `HTMLEditUtils::IsEmptyNode` to check whether
the `Text` and its containers become empty. Therefore, if `Ctrl`+`Backspace`
deletes the only word in the `Text` which starts with a collapsible white-space
causes deleting the `Text` after deleting all visible characters in the `Text`.

Therefore, this makes check it with `Text::TextDataLength` too before searching
the most distant ancestor inline element which becomes empty.

However, it may cause leading invisible white-spaces of the only word visible
because `nsFrameSelection` computes the word range strictly in the `Text`.
Therefore, this patch also makes `HTMLEditor::DeleteRangesWithTransaction` and
`AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction` extend
each range to delete to include surrounding invisible white-spaces too.

Therefore, this patch adds the new method to `AutoClonedRangeArray`.

Then, I hit 4 existing bugs with new test failures.

One is `HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible`.  It checks
whether the candidate point to insert a `<br>` is followed by a block boundary
first.  Then, it checks whether the candidate point follows a collapsible
white-space or a block boundary.  However, it uses
`WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary()` which ignores invisible
white-spaces.  Therefore, it will ignore the new invisible white-space and
reaches preceding `Text`.  Thus, it fails to put a `<br>` and makes the new
invisible white-space "fixed" as invisible.

Therefore, this patch rewrites the check with using
`HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement()`.

Next one is, `HTMLEditor::DeleteEmptyInclusiveAncestorInlineElements()`
returns end of deleted node if it's followed by a non-editable node, i.e.,
an element has `contenteditable="false"`.  Therefore, its caller inserts a
`<br>` to the end of the container when deleting preceding editable node of a
non-editable node.

Therefore, this patch removes the editable state check.

Next, `AutoBlockElementsJoiner::HandleDeleteNonCollapsedRange` may put a padding
`<br>` after the moved line, but it does not assume that the moved line does not
ends with a block boundary.  This causes failing #46 and #48 tests in
`text_bug772796.html`.  E.g., when pressing `Delete` in
`<div>foo[]<div><span style="white-space:pre"><div>bar</div>baz</span>`, we move
the second `<div>` as a child of the parent `<span>` to end of the first `<div>`
like `<div>foo<span style="white-space:pre"><div>bar</div></span></div>...`.
Without the change, it starts to put unnecessary `<br>` after ` the `<span>`
because of the bug fix in `HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible`
above.  This result is completely odd from the users point of view (looks like
just move caret), but we should avoid to put the unnecessary `<br>`.

Finally, we'll fail an assertion when putting caret at the last of
`AutoBlockElementsJoiner::DeleteContentInRange` because it forgets to flush
the tracking range before using it. This appeared by the changes above.

Therefore, this patch fixes this bug too.

Differential Revision: https://phabricator.services.mozilla.com/D240703
This commit is contained in:
Masayuki Nakano
2025-03-17 23:02:10 +00:00
parent b39e4558a2
commit 8e23c15e1b
13 changed files with 577 additions and 133 deletions

View File

@@ -1078,6 +1078,159 @@ void AutoClonedRangeArray::RemoveCollapsedRanges() {
}
}
void AutoClonedRangeArray::ExtendRangeToContainSurroundingInvisibleWhiteSpaces(
nsIEditor::EStripWrappers aStripWrappers) {
const auto PointAfterLineBoundary =
[](const WSScanResult& aPreviousThing) -> EditorRawDOMPoint {
if (aPreviousThing.ReachedCurrentBlockBoundary()) {
return EditorRawDOMPoint(aPreviousThing.ElementPtr(), 0u);
}
return aPreviousThing.PointAfterReachedContent<EditorRawDOMPoint>();
};
const auto PointeAtLineBoundary =
[](const WSScanResult& aNextThing) -> EditorRawDOMPoint {
if (aNextThing.ReachedCurrentBlockBoundary()) {
return EditorRawDOMPoint::AtEndOf(*aNextThing.ElementPtr());
}
return aNextThing.PointAtReachedContent<EditorRawDOMPoint>();
};
for (const OwningNonNull<nsRange>& range : mRanges) {
if (MOZ_UNLIKELY(range->Collapsed())) {
// Don't extend the collapsed range to do nothing for the range.
continue;
}
const WSScanResult previousThing =
WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes,
EditorRawDOMPoint(range->StartRef()),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (previousThing.ReachedLineBoundary()) {
const EditorRawDOMPoint mostDistantNewStart =
[&]() MOZ_NEVER_INLINE_DEBUG {
if (aStripWrappers == nsIEditor::eStrip) {
nsINode* const commonAncestor =
range->GetClosestCommonInclusiveAncestor();
MOZ_ASSERT(commonAncestor);
Element* const commonContainer =
commonAncestor->GetAsElementOrParentElement();
if (NS_WARN_IF(!commonContainer)) {
return EditorRawDOMPoint();
}
return EditorRawDOMPoint(commonContainer, 0u);
}
Element* const container =
range->StartRef().GetContainer()->GetAsElementOrParentElement();
if (NS_WARN_IF(!container)) {
return EditorRawDOMPoint();
}
return EditorRawDOMPoint(container, 0u);
}();
const EditorRawDOMPoint afterLineBoundary =
PointAfterLineBoundary(previousThing);
const auto& newStart =
[&]() MOZ_NEVER_INLINE_DEBUG -> const EditorRawDOMPoint& {
// If the container wraps the line boundary, we can extend the range
// to the line boundary.
if (MOZ_UNLIKELY(!mostDistantNewStart.IsSet()) ||
mostDistantNewStart.IsBefore(afterLineBoundary)) {
return afterLineBoundary;
}
// If the container does not wrap the line boundary, we can delete
// first content of the container.
return mostDistantNewStart;
}();
const auto betterNewStart = [&]() MOZ_NEVER_INLINE_DEBUG {
if (MOZ_UNLIKELY(!newStart.IsSet())) {
return EditorRawDOMPoint();
}
MOZ_ASSERT_IF(mostDistantNewStart.IsSet(),
mostDistantNewStart.IsStartOfContainer());
auto* const firstText = Text::FromNodeOrNull(
newStart == mostDistantNewStart
? mostDistantNewStart.GetContainer()->GetFirstChild()
: newStart.GetChild());
if (!firstText) {
return newStart;
}
return EditorRawDOMPoint(firstText, 0u);
}();
if (MOZ_LIKELY(!NS_WARN_IF(!betterNewStart.IsSet())) &&
betterNewStart != range->StartRef()) {
IgnoredErrorResult ignoredError;
range->SetStart(betterNewStart.ToRawRangeBoundary(), ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"nsRange::SetStart() failed, but ignored");
}
}
const WSScanResult nextThing =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes,
EditorRawDOMPoint(range->EndRef()),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (!nextThing.ReachedLineBoundary()) {
continue;
}
const EditorRawDOMPoint mostDistantNewEnd = [&]() MOZ_NEVER_INLINE_DEBUG {
if (aStripWrappers == nsIEditor::eStrip) {
nsINode* const commonAncestor =
range->GetClosestCommonInclusiveAncestor();
MOZ_ASSERT(commonAncestor);
Element* const commonContainer =
commonAncestor->GetAsElementOrParentElement();
if (NS_WARN_IF(!commonContainer)) {
return EditorRawDOMPoint();
}
return EditorRawDOMPoint::AtEndOf(*commonContainer);
}
Element* const container =
range->EndRef().GetContainer()->GetAsElementOrParentElement();
if (NS_WARN_IF(!container)) {
return EditorRawDOMPoint();
}
return EditorRawDOMPoint::AtEndOf(*container);
}();
if (MOZ_UNLIKELY(!mostDistantNewEnd.IsSet())) {
continue;
}
const EditorRawDOMPoint atLineBoundary = PointeAtLineBoundary(nextThing);
const auto& newEnd =
[&]() MOZ_NEVER_INLINE_DEBUG -> const EditorRawDOMPoint& {
// If the container wraps the line boundary, we can use the boundary
// point.
if (atLineBoundary.IsBefore(mostDistantNewEnd)) {
return atLineBoundary;
}
// If the container does not wrap the line boundary, we can delete last
// content of the container.
return mostDistantNewEnd;
}();
if (MOZ_UNLIKELY(!newEnd.IsSet())) {
continue;
}
const auto betterNewEnd = [&]() MOZ_NEVER_INLINE_DEBUG {
MOZ_ASSERT_IF(mostDistantNewEnd.IsSet(),
mostDistantNewEnd.IsEndOfContainer());
auto* const lastText = Text::FromNodeOrNull(
newEnd == mostDistantNewEnd
? mostDistantNewEnd.GetContainer()->GetLastChild()
: (!newEnd.IsStartOfContainer()
? newEnd.GetPreviousSiblingOfChild()
: nullptr));
if (!lastText) {
return newEnd;
}
return EditorRawDOMPoint::AtEndOf(*lastText);
}();
if (NS_WARN_IF(!betterNewEnd.IsSet()) || betterNewEnd == range->EndRef()) {
continue;
}
IgnoredErrorResult ignoredError;
range->SetEnd(betterNewEnd.ToRawRangeBoundary(), ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"nsRange::SetEnd() failed, but ignored");
}
}
/******************************************************************************
* mozilla::AutoClonedSelectionRangeArray
*****************************************************************************/

View File

@@ -407,6 +407,16 @@ class MOZ_STACK_CLASS AutoClonedRangeArray {
[[nodiscard]] virtual bool HasSavedRanges() const { return false; }
/**
* Extend all ranges to contain surrounding invisible white-spaces if there
* are.
*
* @param aStripWrappers nsIEditor::eStrip if the caller wants to delete
* inline ancestors too.
*/
void ExtendRangeToContainSurroundingInvisibleWhiteSpaces(
nsIEditor::EStripWrappers aStripWrappers);
protected:
AutoClonedRangeArray() = default;

View File

@@ -5181,7 +5181,7 @@ Result<CaretPoint, nsresult> EditorBase::DeleteRangeWithTransaction(
Result<CaretPoint, nsresult> EditorBase::DeleteRangesWithTransaction(
nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
const AutoClonedRangeArray& aRangesToDelete) {
AutoClonedRangeArray& aRangesToDelete) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(!Destroyed());
MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);

View File

@@ -2666,7 +2666,7 @@ class EditorBase : public nsIEditor,
[[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<CaretPoint, nsresult>
DeleteRangesWithTransaction(nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
const AutoClonedRangeArray& aRangesToDelete);
AutoClonedRangeArray& aRangesToDelete);
/**
* Create a transaction for delete the content in aRangesToDelete.

View File

@@ -849,24 +849,35 @@ EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(
point.IsStartOfContainer()) {
return true;
}
const WSScanResult previousThing =
WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes, preferredPaddingLineBreakPoint,
// We need to scan previous `Text` which may ends with invisible white-space
// because we want to make it visible. Therefore, we cannot use
// WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() here.
nsIContent* const previousVisibleLeafOrChildBlock =
HTMLEditUtils::GetPreviousNonEmptyLeafContentOrPreviousBlockElement(
preferredPaddingLineBreakPoint,
{LeafNodeType::LeafNodeOrChildBlock},
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (previousThing.ContentIsText()) {
if (MOZ_UNLIKELY(!previousThing.TextPtr()->TextDataLength())) {
return false;
}
auto atLastChar = EditorRawDOMPointInText(
previousThing.TextPtr(),
previousThing.TextPtr()->TextDataLength() - 1);
if (atLastChar.IsCharCollapsibleASCIISpace()) {
preferredPaddingLineBreakPoint.SetAfter(previousThing.TextPtr());
return true;
}
if (!previousVisibleLeafOrChildBlock) {
// Reached current block.
return true;
}
if (HTMLEditUtils::IsBlockElement(
*previousVisibleLeafOrChildBlock,
BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
// We reached previous child block.
return true;
}
Text* const previousVisibleText =
Text::FromNode(previousVisibleLeafOrChildBlock);
if (!previousVisibleText) {
// We reached visible inline element.
return false;
}
return previousThing.ReachedBlockBoundary();
MOZ_ASSERT(previousVisibleText->TextDataLength());
// We reached previous (currently) invisible white-space or visible
// character.
return EditorRawDOMPoint::AtEndOf(*previousVisibleText)
.IsPreviousCharASCIISpace();
}();
if (!followingBlockBoundaryOrCollapsibleWhiteSpace) {
return EditorDOMPoint();

View File

@@ -1585,6 +1585,96 @@ class HTMLEditUtils final {
return previousContent;
}
/**
* Return previous non-empty leaf content or child block or non-editable
* content (depending on aLeafNodeTypes). This ignores invisible inline leaf
* element like `<b></b>` and empty `Text` nodes. So, this may return
* invisible `Text` node, but it may be useful to consider whether we need to
* insert a padding <br> element.
*/
[[nodiscard]] static nsIContent*
GetPreviousNonEmptyLeafContentOrPreviousBlockElement(
const nsIContent& aContent, const LeafNodeTypes& aLeafNodeTypes,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter = nullptr) {
for (nsIContent* previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
aContent, aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter);
previousContent;
previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*previousContent, aLeafNodeTypes, aBlockInlineCheck,
aAncestorLimiter)) {
if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
HTMLEditUtils::IsBlockElement(
*previousContent,
BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
return previousContent; // Reached block element
}
if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
return previousContent; // Reached non-editable content
}
Text* const previousText = Text::FromNode(previousContent);
if (!previousText) {
if (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) {
continue; // Ignore invisible inline elements
}
return previousContent; // Reached visible inline element
}
if (!previousText->TextDataLength()) {
continue; // Ignore empty Text nodes.
}
return previousText; // Reached non-empty text
}
return nullptr;
}
/**
* Return previous visible leaf content or child block or non-editable content
* (depending on aLeafNodeTypes). This ignores invisible inline leaf element
* like `<b></b>` and empty `Text` nodes. So, this may return invisible
* `Text` node, but it may be useful to consider whether we need to insert a
* padding <br> element.
*/
template <typename PT, typename CT>
[[nodiscard]] static nsIContent*
GetPreviousNonEmptyLeafContentOrPreviousBlockElement(
const EditorDOMPointBase<PT, CT>& aPoint,
const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter = nullptr) {
for (nsIContent* previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
aPoint, aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter);
previousContent;
previousContent =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*previousContent, aLeafNodeTypes, aBlockInlineCheck,
aAncestorLimiter)) {
if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
HTMLEditUtils::IsBlockElement(
*previousContent,
BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
return previousContent; // Reached block element
}
if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
return previousContent; // Reached non-editable content
}
Text* const previousText = Text::FromNode(previousContent);
if (!previousText) {
if (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) {
continue; // Ignore invisible inline elements
}
return previousContent; // Reached visible inline element
}
if (!previousText->TextDataLength()) {
continue; // Ignore empty Text nodes.
}
return previousText; // Reached non-empty text
}
return nullptr;
}
/**
* Returns a content node whose inline styles should be preserved after
* deleting content in a range. Typically, you should set aPoint to start

View File

@@ -4158,18 +4158,20 @@ HTMLEditor::DeleteEmptyInclusiveAncestorInlineElements(
const nsCOMPtr<nsIContent> nextSibling = content->GetNextSibling();
const nsCOMPtr<nsINode> parentNode = content->GetParentNode();
MOZ_ASSERT(parentNode);
nsresult rv = DeleteNodeWithTransaction(content);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
if (NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode)) {
if (NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parentNode) ||
NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(*parentNode))) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return CaretPoint(nextSibling &&
HTMLEditUtils::IsSimplyEditableNode(*nextSibling)
? EditorDOMPoint(nextSibling)
: EditorDOMPoint::AtEndOf(*parentNode));
// Note that even if nextSibling is not editable, we can put caret before it
// unless parentNode is not editable.
return CaretPoint(nextSibling ? EditorDOMPoint(nextSibling)
: EditorDOMPoint::AtEndOf(*parentNode));
}
nsresult HTMLEditor::DeleteAllChildrenWithTransaction(Element& aElement) {

View File

@@ -1713,10 +1713,9 @@ class HTMLEditor final : public EditorBase,
const Element& aEditingHost);
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
DeleteRangesWithTransaction(
nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
const AutoClonedRangeArray& aRangesToDelete) override;
DeleteRangesWithTransaction(nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
AutoClonedRangeArray& aRangesToDelete) override;
/**
* SplitParagraphWithTransaction() splits the parent block, aParentDivOrP, at

View File

@@ -393,11 +393,19 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
FallbackToDeleteRangesWithTransaction(
HTMLEditor& aHTMLEditor,
AutoClonedSelectionRangeArray& aRangesToDelete) const {
HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRangesToDelete,
const Element& aEditingHost) const {
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
const auto stripWrappers = [&]() -> nsIEditor::EStripWrappers {
if (mOriginalStripWrappers == nsIEditor::eStrip &&
aEditingHost.IsContentEditablePlainTextOnly()) {
return nsIEditor::eNoStrip;
}
return mOriginalStripWrappers;
}();
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
&aRangesToDelete.FirstRangeRef());
@@ -439,8 +447,7 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount,
mOriginalStripWrappers,
aRangesToDelete);
stripWrappers, aRangesToDelete);
NS_WARNING_ASSERTION(caretPointOrError.isOk(),
"HTMLEditor::DeleteRangesWithTransaction() failed");
return caretPointOrError;
@@ -469,28 +476,20 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
return NS_ERROR_FAILURE;
}
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
for (OwningNonNull<nsRange>& range : Reversed(aRangesToDelete.Ranges())) {
if (MOZ_UNLIKELY(!range->IsPositioned() || range->Collapsed())) {
continue;
}
const EditorDOMRange extendedRange = WSRunScanner::
GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
WSRunScanner::Scan::EditableNodes, EditorDOMRange(range));
if (EditorRawDOMRange(range) != extendedRange) {
nsresult rv = range->SetStartAndEnd(
extendedRange.StartRef().ToRawRangeBoundary(),
extendedRange.EndRef().ToRawRangeBoundary());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return NS_ERROR_FAILURE;
}
}
}
aRangesToDelete.RemoveCollapsedRanges();
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
return NS_OK;
const auto stripWrappers = [&]() -> nsIEditor::EStripWrappers {
if (mOriginalStripWrappers == nsIEditor::eStrip &&
aEditingHost.IsContentEditablePlainTextOnly()) {
return nsIEditor::eNoStrip;
}
return mOriginalStripWrappers;
}();
aRangesToDelete.ExtendRangeToContainSurroundingInvisibleWhiteSpaces(
stripWrappers);
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed() &&
howToHandleCollapsedRange ==
EditorBase::HowToHandleCollapsedRange::Ignore)) {
return NS_OK;
}
for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
@@ -1389,13 +1388,19 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::DeleteLineBreakWithTransaction(
Result<CaretPoint, nsresult> HTMLEditor::DeleteRangesWithTransaction(
nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
const AutoClonedRangeArray& aRangesToDelete) {
AutoClonedRangeArray& aRangesToDelete) {
const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No);
if (NS_WARN_IF(!editingHost)) {
return Err(NS_ERROR_UNEXPECTED);
}
aRangesToDelete.ExtendRangeToContainSurroundingInvisibleWhiteSpaces(
aStripWrappers);
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
return CaretPoint(EditorDOMPoint(aRangesToDelete.FocusRef()));
}
Result<CaretPoint, nsresult> result = EditorBase::DeleteRangesWithTransaction(
aDirectionAndAmount, aStripWrappers, aRangesToDelete);
if (MOZ_UNLIKELY(result.isErr())) {
@@ -1420,35 +1425,41 @@ Result<CaretPoint, nsresult> HTMLEditor::DeleteRangesWithTransaction(
EditorDOMPoint pointToInsertLineBreak(range->StartRef());
// Don't remove empty inline elements in the plaintext-only mode because
// nobody can restore the style again.
if (aStripWrappers == nsIEditor::eStrip &&
!editingHost->IsContentEditablePlainTextOnly()) {
if (aStripWrappers == nsIEditor::eStrip) {
const OwningNonNull<nsIContent> maybeEmptyContent =
*pointToInsertLineBreak.ContainerAs<nsIContent>();
if (MOZ_UNLIKELY(
!HTMLEditUtils::IsRemovableFromParentNode(maybeEmptyContent))) {
continue;
}
Result<CaretPoint, nsresult> caretPointOrError =
DeleteEmptyInclusiveAncestorInlineElements(maybeEmptyContent,
*editingHost);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"HTMLEditor::DeleteEmptyInclusiveAncestorInlineElements() "
"failed");
return caretPointOrError.propagateErr();
}
if (NS_WARN_IF(!range->IsPositioned() ||
!range->GetStartContainer()->IsContent())) {
continue;
}
MOZ_ASSERT_IF(
caretPointOrError.inspect().HasCaretPointSuggestion(),
HTMLEditUtils::IsSimplyEditableNode(
*caretPointOrError.inspect().CaretPointRef().GetContainer()));
caretPointOrError.unwrap().MoveCaretPointTo(
pointToInsertLineBreak, {SuggestCaret::OnlyIfHasSuggestion});
if (NS_WARN_IF(!pointToInsertLineBreak.IsSetAndValidInComposedDoc())) {
continue;
// If the `Text` becomes invisible but has collapsible white-spaces, we
// shouldn't delete it because the deletion deletes only before or after
// the white-space to keep the white-space visible.
if (!maybeEmptyContent->IsText() ||
!maybeEmptyContent->AsText()->TextDataLength()) {
Result<CaretPoint, nsresult> caretPointOrError =
DeleteEmptyInclusiveAncestorInlineElements(maybeEmptyContent,
*editingHost);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"HTMLEditor::DeleteEmptyInclusiveAncestorInlineElements() "
"failed");
return caretPointOrError.propagateErr();
}
if (NS_WARN_IF(!range->IsPositioned() ||
!range->GetStartContainer()->IsContent())) {
continue;
}
MOZ_ASSERT_IF(
caretPointOrError.inspect().HasCaretPointSuggestion(),
HTMLEditUtils::IsSimplyEditableNode(
*caretPointOrError.inspect().CaretPointRef().GetContainer()));
caretPointOrError.unwrap().MoveCaretPointTo(
pointToInsertLineBreak, {SuggestCaret::OnlyIfHasSuggestion});
if (NS_WARN_IF(
!pointToInsertLineBreak.IsSetAndValidInComposedDoc())) {
continue;
}
}
}
@@ -1876,7 +1887,8 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run(
return EditActionResult::IgnoredResult();
}
Result<CaretPoint, nsresult> caretPointOrError =
FallbackToDeleteRangesWithTransaction(aHTMLEditor, aRangesToDelete);
FallbackToDeleteRangesWithTransaction(aHTMLEditor, aRangesToDelete,
aEditingHost);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() "
@@ -4309,28 +4321,33 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges(
// If we've already removed all contents in the range, we shouldn't
// delete anything around the caret.
if (!aRangesToDelete.FirstRangeRef()->Collapsed()) {
{
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
&aRangesToDelete.FirstRangeRef());
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(
aDirectionAndAmount, aStripWrappers, aRangesToDelete);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING("HTMLEditor::DeleteRangesWithTransaction() failed");
return caretPointOrError.propagateErr();
const auto stripWrappers = [&]() -> nsIEditor::EStripWrappers {
if (mOriginalStripWrappers == nsIEditor::eStrip &&
aEditingHost.IsContentEditablePlainTextOnly()) {
return nsIEditor::eNoStrip;
}
nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
return Err(rv);
}
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
return mOriginalStripWrappers;
}();
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
&aRangesToDelete.FirstRangeRef());
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(
aDirectionAndAmount, stripWrappers, aRangesToDelete);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING("HTMLEditor::DeleteRangesWithTransaction() failed");
return caretPointOrError.propagateErr();
}
nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
SuggestCaret::AndIgnoreTrivialError});
if (NS_FAILED(rv)) {
NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
return Err(rv);
}
NS_WARNING_ASSERTION(
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) ||
(aHTMLEditor.MayHaveMutationEventListeners(
NS_EVENT_BITS_MUTATION_NODEREMOVED |
@@ -4713,6 +4730,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed");
return Err(rv);
}
trackRangeToCleanUp.FlushAndStopTracking();
const auto& pointToPutCaret =
!nsIEditor::DirectionIsBackspace(aDirectionAndAmount) ||
(aHTMLEditor.TopLevelEditSubActionDataRef()
@@ -4799,8 +4817,8 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
}
}
if (!rangeToDelete->Collapsed()) {
const AutoClonedSelectionRangeArray rangesToDelete(rangeToDelete,
aLimitersAndCaretData);
AutoClonedSelectionRangeArray rangesToDelete(rangeToDelete,
aLimitersAndCaretData);
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
aStripWrappers, rangesToDelete);
@@ -5756,20 +5774,42 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
// maintain padding line break at end of moved content.
if (moveFirstLineResult.Handled() &&
moveFirstLineResult.DeleteRangeRef().IsPositioned()) {
nsresult rv;
if (NS_WARN_IF(
NS_FAILED(rv = EnsureNoFollowingUnnecessaryLineBreak(
moveFirstLineResult.DeleteRangeRef().EndRef())))) {
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
moveFirstLineResult.DeleteRangeRef().EndRef());
if (NS_FAILED(rv)) {
NS_WARNING("EnsureNoFollowingUnnecessaryLineBreak() failed");
return Err(rv);
}
Result<CaretPoint, nsresult> caretPointOrError =
InsertPaddingBRElementIfNeeded(
moveFirstLineResult.DeleteRangeRef().EndRef());
if (NS_WARN_IF(caretPointOrError.isErr())) {
return caretPointOrError.propagateErr();
// If we moved a child block of the first line (although this is
// logically wrong...), we should not put a <br> after that.
const bool movedLineEndsWithBlockBoundary = [&]() {
Element* const commonAncestor =
Element::FromNodeOrNull(moveFirstLineResult.DeleteRangeRef()
.GetClosestCommonInclusiveAncestor());
nsIContent* const previousVisibleLeafOrChildBlock =
HTMLEditUtils::GetPreviousNonEmptyLeafContentOrPreviousBlockElement(
moveFirstLineResult.DeleteRangeRef().EndRef(),
{LeafNodeType::LeafNodeOrChildBlock},
BlockInlineCheck::UseComputedDisplayOutsideStyle, commonAncestor);
if (!previousVisibleLeafOrChildBlock) {
return false;
}
return HTMLEditUtils::IsBlockElement(
*previousVisibleLeafOrChildBlock,
BlockInlineCheck::UseComputedDisplayOutsideStyle) &&
moveFirstLineResult.DeleteRangeRef().StartRef().EqualsOrIsBefore(
EditorRawDOMPoint::After(*previousVisibleLeafOrChildBlock));
}();
if (MOZ_LIKELY(!movedLineEndsWithBlockBoundary)) {
Result<CaretPoint, nsresult> caretPointOrError =
InsertPaddingBRElementIfNeeded(
moveFirstLineResult.DeleteRangeRef().EndRef());
if (NS_WARN_IF(caretPointOrError.isErr())) {
return caretPointOrError.propagateErr();
}
caretPointOrError.unwrap().MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
}
caretPointOrError.unwrap().MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
}
// If we only deleted content in the range, we need to maintain padding line
// breaks at both deleted range boundaries.

View File

@@ -17,8 +17,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1315065
/** Test for Bug 1315065 **/
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(() => {
const kNewWhiteSpaceNormalizerEnabled =
SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible");
const editor = document.getElementsByTagName("div")[0];
function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) {
editor.innerHTML = "<p id='p'>abc<br></p>";
@@ -69,6 +67,7 @@ SimpleTest.waitForFocus(() => {
// When Backspace key is pressed even in empty text nodes, Gecko should not remove empty text nodes for now
// because we should keep our traditional behavior (same as Edge) for backward compatibility as far as possible.
// In this case, Chromium removes all empty text nodes, but Edge doesn't remove any empty text nodes.
// XXX It's fine to delete empty text nodes too if it's reasonable for handling deletion.
is(p.childNodes.length, 4, kDescription + ": <p> should have 4 children after pressing Backspace key");
is(p.childNodes.item(0).textContent, "ab", kDescription + ": 'c' should be removed by pressing Backspace key");
is(p.childNodes.item(1).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Backspace key");
@@ -123,25 +122,18 @@ SimpleTest.waitForFocus(() => {
const p = document.getElementById("p");
ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press");
is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Delete key press");
if (i == 0) {
// If Delete key is pressed in non-empty text node, only the text node should be modified.
// This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed");
is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key");
is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key");
is(p.childNodes.item(2).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Delete key");
is(p.childNodes.item(3).textContent, "bc", kDescription + ": 'a' should be removed by pressing Delete key");
} else if (!kNewWhiteSpaceNormalizerEnabled) {
// If Delete key is pressed in an empty text node, it and following empty text nodes should be removed and the non-empty text node should be modified.
// This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
const expectedEmptyTextNodes = 3 - i;
is(p.childNodes.length, expectedEmptyTextNodes + 2, kDescription + ": <p> should have only " + i + " children after pressing Delete key (" + i + " empty text nodes should be removed");
is(p.childNodes.item(expectedEmptyTextNodes).textContent, "bc", kDescription + ": empty text nodes and 'a' should be removed by pressing Delete key");
} else {
is(p.childNodes.length, 2, `${kDescription}: <p> should have only 2 children after pressing Delete key`);
is(p.childNodes.item(0).textContent, "bc", `${kDescription}: Empty text nodes and 'a' should be removed by pressing Delete key`);
is(p.childNodes.item(1).nodeName, "BR", `${kDescription}: The <br> should be preserved since outside of the deleting range`);
}
// If Delete key is pressed in an empty text node, it's fine to delete all
// empty text nodes, but the non-empty text node should be modified.
is(
p.childNodes.length,
2,
`${kDescription}: <p> should have at least one text node after pressing Delete key`
);
is(
p.textContent,
"bc",
`${kDescription}: empty text nodes and 'a' should be removed by pressing Delete key`
);
editor.blur();
}
SimpleTest.finish();