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:
@@ -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
|
||||
*****************************************************************************/
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user