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
|
* mozilla::AutoClonedSelectionRangeArray
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|||||||
@@ -407,6 +407,16 @@ class MOZ_STACK_CLASS AutoClonedRangeArray {
|
|||||||
|
|
||||||
[[nodiscard]] virtual bool HasSavedRanges() const { return false; }
|
[[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:
|
protected:
|
||||||
AutoClonedRangeArray() = default;
|
AutoClonedRangeArray() = default;
|
||||||
|
|
||||||
|
|||||||
@@ -5181,7 +5181,7 @@ Result<CaretPoint, nsresult> EditorBase::DeleteRangeWithTransaction(
|
|||||||
Result<CaretPoint, nsresult> EditorBase::DeleteRangesWithTransaction(
|
Result<CaretPoint, nsresult> EditorBase::DeleteRangesWithTransaction(
|
||||||
nsIEditor::EDirection aDirectionAndAmount,
|
nsIEditor::EDirection aDirectionAndAmount,
|
||||||
nsIEditor::EStripWrappers aStripWrappers,
|
nsIEditor::EStripWrappers aStripWrappers,
|
||||||
const AutoClonedRangeArray& aRangesToDelete) {
|
AutoClonedRangeArray& aRangesToDelete) {
|
||||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||||
MOZ_ASSERT(!Destroyed());
|
MOZ_ASSERT(!Destroyed());
|
||||||
MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
|
MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
|
||||||
|
|||||||
@@ -2666,7 +2666,7 @@ class EditorBase : public nsIEditor,
|
|||||||
[[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<CaretPoint, nsresult>
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT virtual Result<CaretPoint, nsresult>
|
||||||
DeleteRangesWithTransaction(nsIEditor::EDirection aDirectionAndAmount,
|
DeleteRangesWithTransaction(nsIEditor::EDirection aDirectionAndAmount,
|
||||||
nsIEditor::EStripWrappers aStripWrappers,
|
nsIEditor::EStripWrappers aStripWrappers,
|
||||||
const AutoClonedRangeArray& aRangesToDelete);
|
AutoClonedRangeArray& aRangesToDelete);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a transaction for delete the content in aRangesToDelete.
|
* Create a transaction for delete the content in aRangesToDelete.
|
||||||
|
|||||||
@@ -849,24 +849,35 @@ EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(
|
|||||||
point.IsStartOfContainer()) {
|
point.IsStartOfContainer()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const WSScanResult previousThing =
|
// We need to scan previous `Text` which may ends with invisible white-space
|
||||||
WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
|
// because we want to make it visible. Therefore, we cannot use
|
||||||
WSRunScanner::Scan::EditableNodes, preferredPaddingLineBreakPoint,
|
// WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() here.
|
||||||
|
nsIContent* const previousVisibleLeafOrChildBlock =
|
||||||
|
HTMLEditUtils::GetPreviousNonEmptyLeafContentOrPreviousBlockElement(
|
||||||
|
preferredPaddingLineBreakPoint,
|
||||||
|
{LeafNodeType::LeafNodeOrChildBlock},
|
||||||
BlockInlineCheck::UseComputedDisplayOutsideStyle);
|
BlockInlineCheck::UseComputedDisplayOutsideStyle);
|
||||||
if (previousThing.ContentIsText()) {
|
if (!previousVisibleLeafOrChildBlock) {
|
||||||
if (MOZ_UNLIKELY(!previousThing.TextPtr()->TextDataLength())) {
|
// Reached current block.
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
auto atLastChar = EditorRawDOMPointInText(
|
if (HTMLEditUtils::IsBlockElement(
|
||||||
previousThing.TextPtr(),
|
*previousVisibleLeafOrChildBlock,
|
||||||
previousThing.TextPtr()->TextDataLength() - 1);
|
BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
|
||||||
if (atLastChar.IsCharCollapsibleASCIISpace()) {
|
// We reached previous child block.
|
||||||
preferredPaddingLineBreakPoint.SetAfter(previousThing.TextPtr());
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
Text* const previousVisibleText =
|
||||||
|
Text::FromNode(previousVisibleLeafOrChildBlock);
|
||||||
|
if (!previousVisibleText) {
|
||||||
|
// We reached visible inline element.
|
||||||
return false;
|
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) {
|
if (!followingBlockBoundaryOrCollapsibleWhiteSpace) {
|
||||||
return EditorDOMPoint();
|
return EditorDOMPoint();
|
||||||
|
|||||||
@@ -1585,6 +1585,96 @@ class HTMLEditUtils final {
|
|||||||
return previousContent;
|
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
|
* Returns a content node whose inline styles should be preserved after
|
||||||
* deleting content in a range. Typically, you should set aPoint to start
|
* 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<nsIContent> nextSibling = content->GetNextSibling();
|
||||||
const nsCOMPtr<nsINode> parentNode = content->GetParentNode();
|
const nsCOMPtr<nsINode> parentNode = content->GetParentNode();
|
||||||
|
MOZ_ASSERT(parentNode);
|
||||||
nsresult rv = DeleteNodeWithTransaction(content);
|
nsresult rv = DeleteNodeWithTransaction(content);
|
||||||
if (NS_FAILED(rv)) {
|
if (NS_FAILED(rv)) {
|
||||||
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
|
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
|
||||||
return Err(rv);
|
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 Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||||
}
|
}
|
||||||
return CaretPoint(nextSibling &&
|
// Note that even if nextSibling is not editable, we can put caret before it
|
||||||
HTMLEditUtils::IsSimplyEditableNode(*nextSibling)
|
// unless parentNode is not editable.
|
||||||
? EditorDOMPoint(nextSibling)
|
return CaretPoint(nextSibling ? EditorDOMPoint(nextSibling)
|
||||||
: EditorDOMPoint::AtEndOf(*parentNode));
|
: EditorDOMPoint::AtEndOf(*parentNode));
|
||||||
}
|
}
|
||||||
|
|
||||||
nsresult HTMLEditor::DeleteAllChildrenWithTransaction(Element& aElement) {
|
nsresult HTMLEditor::DeleteAllChildrenWithTransaction(Element& aElement) {
|
||||||
|
|||||||
@@ -1713,10 +1713,9 @@ class HTMLEditor final : public EditorBase,
|
|||||||
const Element& aEditingHost);
|
const Element& aEditingHost);
|
||||||
|
|
||||||
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
||||||
DeleteRangesWithTransaction(
|
DeleteRangesWithTransaction(nsIEditor::EDirection aDirectionAndAmount,
|
||||||
nsIEditor::EDirection aDirectionAndAmount,
|
nsIEditor::EStripWrappers aStripWrappers,
|
||||||
nsIEditor::EStripWrappers aStripWrappers,
|
AutoClonedRangeArray& aRangesToDelete) override;
|
||||||
const AutoClonedRangeArray& aRangesToDelete) override;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SplitParagraphWithTransaction() splits the parent block, aParentDivOrP, at
|
* 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>
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
||||||
FallbackToDeleteRangesWithTransaction(
|
FallbackToDeleteRangesWithTransaction(
|
||||||
HTMLEditor& aHTMLEditor,
|
HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRangesToDelete,
|
||||||
AutoClonedSelectionRangeArray& aRangesToDelete) const {
|
const Element& aEditingHost) const {
|
||||||
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
|
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
|
||||||
MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
|
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()) {
|
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
|
||||||
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
|
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
|
||||||
&aRangesToDelete.FirstRangeRef());
|
&aRangesToDelete.FirstRangeRef());
|
||||||
@@ -439,8 +447,7 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
|
|||||||
|
|
||||||
Result<CaretPoint, nsresult> caretPointOrError =
|
Result<CaretPoint, nsresult> caretPointOrError =
|
||||||
aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount,
|
aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount,
|
||||||
mOriginalStripWrappers,
|
stripWrappers, aRangesToDelete);
|
||||||
aRangesToDelete);
|
|
||||||
NS_WARNING_ASSERTION(caretPointOrError.isOk(),
|
NS_WARNING_ASSERTION(caretPointOrError.isOk(),
|
||||||
"HTMLEditor::DeleteRangesWithTransaction() failed");
|
"HTMLEditor::DeleteRangesWithTransaction() failed");
|
||||||
return caretPointOrError;
|
return caretPointOrError;
|
||||||
@@ -469,28 +476,20 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
|
|||||||
return NS_ERROR_FAILURE;
|
return NS_ERROR_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
|
const auto stripWrappers = [&]() -> nsIEditor::EStripWrappers {
|
||||||
for (OwningNonNull<nsRange>& range : Reversed(aRangesToDelete.Ranges())) {
|
if (mOriginalStripWrappers == nsIEditor::eStrip &&
|
||||||
if (MOZ_UNLIKELY(!range->IsPositioned() || range->Collapsed())) {
|
aEditingHost.IsContentEditablePlainTextOnly()) {
|
||||||
continue;
|
return nsIEditor::eNoStrip;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return mOriginalStripWrappers;
|
||||||
|
}();
|
||||||
|
|
||||||
|
aRangesToDelete.ExtendRangeToContainSurroundingInvisibleWhiteSpaces(
|
||||||
|
stripWrappers);
|
||||||
|
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed() &&
|
||||||
|
howToHandleCollapsedRange ==
|
||||||
|
EditorBase::HowToHandleCollapsedRange::Ignore)) {
|
||||||
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
|
for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
|
||||||
@@ -1389,13 +1388,19 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::DeleteLineBreakWithTransaction(
|
|||||||
Result<CaretPoint, nsresult> HTMLEditor::DeleteRangesWithTransaction(
|
Result<CaretPoint, nsresult> HTMLEditor::DeleteRangesWithTransaction(
|
||||||
nsIEditor::EDirection aDirectionAndAmount,
|
nsIEditor::EDirection aDirectionAndAmount,
|
||||||
nsIEditor::EStripWrappers aStripWrappers,
|
nsIEditor::EStripWrappers aStripWrappers,
|
||||||
const AutoClonedRangeArray& aRangesToDelete) {
|
AutoClonedRangeArray& aRangesToDelete) {
|
||||||
const RefPtr<Element> editingHost =
|
const RefPtr<Element> editingHost =
|
||||||
ComputeEditingHost(LimitInBodyElement::No);
|
ComputeEditingHost(LimitInBodyElement::No);
|
||||||
if (NS_WARN_IF(!editingHost)) {
|
if (NS_WARN_IF(!editingHost)) {
|
||||||
return Err(NS_ERROR_UNEXPECTED);
|
return Err(NS_ERROR_UNEXPECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aRangesToDelete.ExtendRangeToContainSurroundingInvisibleWhiteSpaces(
|
||||||
|
aStripWrappers);
|
||||||
|
if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
|
||||||
|
return CaretPoint(EditorDOMPoint(aRangesToDelete.FocusRef()));
|
||||||
|
}
|
||||||
|
|
||||||
Result<CaretPoint, nsresult> result = EditorBase::DeleteRangesWithTransaction(
|
Result<CaretPoint, nsresult> result = EditorBase::DeleteRangesWithTransaction(
|
||||||
aDirectionAndAmount, aStripWrappers, aRangesToDelete);
|
aDirectionAndAmount, aStripWrappers, aRangesToDelete);
|
||||||
if (MOZ_UNLIKELY(result.isErr())) {
|
if (MOZ_UNLIKELY(result.isErr())) {
|
||||||
@@ -1420,35 +1425,41 @@ Result<CaretPoint, nsresult> HTMLEditor::DeleteRangesWithTransaction(
|
|||||||
EditorDOMPoint pointToInsertLineBreak(range->StartRef());
|
EditorDOMPoint pointToInsertLineBreak(range->StartRef());
|
||||||
// Don't remove empty inline elements in the plaintext-only mode because
|
// Don't remove empty inline elements in the plaintext-only mode because
|
||||||
// nobody can restore the style again.
|
// nobody can restore the style again.
|
||||||
if (aStripWrappers == nsIEditor::eStrip &&
|
if (aStripWrappers == nsIEditor::eStrip) {
|
||||||
!editingHost->IsContentEditablePlainTextOnly()) {
|
|
||||||
const OwningNonNull<nsIContent> maybeEmptyContent =
|
const OwningNonNull<nsIContent> maybeEmptyContent =
|
||||||
*pointToInsertLineBreak.ContainerAs<nsIContent>();
|
*pointToInsertLineBreak.ContainerAs<nsIContent>();
|
||||||
if (MOZ_UNLIKELY(
|
if (MOZ_UNLIKELY(
|
||||||
!HTMLEditUtils::IsRemovableFromParentNode(maybeEmptyContent))) {
|
!HTMLEditUtils::IsRemovableFromParentNode(maybeEmptyContent))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Result<CaretPoint, nsresult> caretPointOrError =
|
// If the `Text` becomes invisible but has collapsible white-spaces, we
|
||||||
DeleteEmptyInclusiveAncestorInlineElements(maybeEmptyContent,
|
// shouldn't delete it because the deletion deletes only before or after
|
||||||
*editingHost);
|
// the white-space to keep the white-space visible.
|
||||||
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
|
if (!maybeEmptyContent->IsText() ||
|
||||||
NS_WARNING(
|
!maybeEmptyContent->AsText()->TextDataLength()) {
|
||||||
"HTMLEditor::DeleteEmptyInclusiveAncestorInlineElements() "
|
Result<CaretPoint, nsresult> caretPointOrError =
|
||||||
"failed");
|
DeleteEmptyInclusiveAncestorInlineElements(maybeEmptyContent,
|
||||||
return caretPointOrError.propagateErr();
|
*editingHost);
|
||||||
}
|
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
|
||||||
if (NS_WARN_IF(!range->IsPositioned() ||
|
NS_WARNING(
|
||||||
!range->GetStartContainer()->IsContent())) {
|
"HTMLEditor::DeleteEmptyInclusiveAncestorInlineElements() "
|
||||||
continue;
|
"failed");
|
||||||
}
|
return caretPointOrError.propagateErr();
|
||||||
MOZ_ASSERT_IF(
|
}
|
||||||
caretPointOrError.inspect().HasCaretPointSuggestion(),
|
if (NS_WARN_IF(!range->IsPositioned() ||
|
||||||
HTMLEditUtils::IsSimplyEditableNode(
|
!range->GetStartContainer()->IsContent())) {
|
||||||
*caretPointOrError.inspect().CaretPointRef().GetContainer()));
|
continue;
|
||||||
caretPointOrError.unwrap().MoveCaretPointTo(
|
}
|
||||||
pointToInsertLineBreak, {SuggestCaret::OnlyIfHasSuggestion});
|
MOZ_ASSERT_IF(
|
||||||
if (NS_WARN_IF(!pointToInsertLineBreak.IsSetAndValidInComposedDoc())) {
|
caretPointOrError.inspect().HasCaretPointSuggestion(),
|
||||||
continue;
|
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();
|
return EditActionResult::IgnoredResult();
|
||||||
}
|
}
|
||||||
Result<CaretPoint, nsresult> caretPointOrError =
|
Result<CaretPoint, nsresult> caretPointOrError =
|
||||||
FallbackToDeleteRangesWithTransaction(aHTMLEditor, aRangesToDelete);
|
FallbackToDeleteRangesWithTransaction(aHTMLEditor, aRangesToDelete,
|
||||||
|
aEditingHost);
|
||||||
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
|
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
|
||||||
NS_WARNING(
|
NS_WARNING(
|
||||||
"AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() "
|
"AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() "
|
||||||
@@ -4309,28 +4321,33 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges(
|
|||||||
// If we've already removed all contents in the range, we shouldn't
|
// If we've already removed all contents in the range, we shouldn't
|
||||||
// delete anything around the caret.
|
// delete anything around the caret.
|
||||||
if (!aRangesToDelete.FirstRangeRef()->Collapsed()) {
|
if (!aRangesToDelete.FirstRangeRef()->Collapsed()) {
|
||||||
{
|
const auto stripWrappers = [&]() -> nsIEditor::EStripWrappers {
|
||||||
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
|
if (mOriginalStripWrappers == nsIEditor::eStrip &&
|
||||||
&aRangesToDelete.FirstRangeRef());
|
aEditingHost.IsContentEditablePlainTextOnly()) {
|
||||||
Result<CaretPoint, nsresult> caretPointOrError =
|
return nsIEditor::eNoStrip;
|
||||||
aHTMLEditor.DeleteRangesWithTransaction(
|
|
||||||
aDirectionAndAmount, aStripWrappers, aRangesToDelete);
|
|
||||||
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
|
|
||||||
NS_WARNING("HTMLEditor::DeleteRangesWithTransaction() failed");
|
|
||||||
return caretPointOrError.propagateErr();
|
|
||||||
}
|
}
|
||||||
nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
|
return mOriginalStripWrappers;
|
||||||
aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
|
}();
|
||||||
SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
|
AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
|
||||||
SuggestCaret::AndIgnoreTrivialError});
|
&aRangesToDelete.FirstRangeRef());
|
||||||
if (NS_FAILED(rv)) {
|
Result<CaretPoint, nsresult> caretPointOrError =
|
||||||
NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
|
aHTMLEditor.DeleteRangesWithTransaction(
|
||||||
return Err(rv);
|
aDirectionAndAmount, stripWrappers, aRangesToDelete);
|
||||||
}
|
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
|
||||||
NS_WARNING_ASSERTION(
|
NS_WARNING("HTMLEditor::DeleteRangesWithTransaction() failed");
|
||||||
rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
|
return caretPointOrError.propagateErr();
|
||||||
"CaretPoint::SuggestCaretPointTo() failed, but ignored");
|
|
||||||
}
|
}
|
||||||
|
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()) ||
|
if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) ||
|
||||||
(aHTMLEditor.MayHaveMutationEventListeners(
|
(aHTMLEditor.MayHaveMutationEventListeners(
|
||||||
NS_EVENT_BITS_MUTATION_NODEREMOVED |
|
NS_EVENT_BITS_MUTATION_NODEREMOVED |
|
||||||
@@ -4713,6 +4730,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
|
|||||||
NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed");
|
NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed");
|
||||||
return Err(rv);
|
return Err(rv);
|
||||||
}
|
}
|
||||||
|
trackRangeToCleanUp.FlushAndStopTracking();
|
||||||
const auto& pointToPutCaret =
|
const auto& pointToPutCaret =
|
||||||
!nsIEditor::DirectionIsBackspace(aDirectionAndAmount) ||
|
!nsIEditor::DirectionIsBackspace(aDirectionAndAmount) ||
|
||||||
(aHTMLEditor.TopLevelEditSubActionDataRef()
|
(aHTMLEditor.TopLevelEditSubActionDataRef()
|
||||||
@@ -4799,8 +4817,8 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!rangeToDelete->Collapsed()) {
|
if (!rangeToDelete->Collapsed()) {
|
||||||
const AutoClonedSelectionRangeArray rangesToDelete(rangeToDelete,
|
AutoClonedSelectionRangeArray rangesToDelete(rangeToDelete,
|
||||||
aLimitersAndCaretData);
|
aLimitersAndCaretData);
|
||||||
Result<CaretPoint, nsresult> caretPointOrError =
|
Result<CaretPoint, nsresult> caretPointOrError =
|
||||||
aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
|
aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
|
||||||
aStripWrappers, rangesToDelete);
|
aStripWrappers, rangesToDelete);
|
||||||
@@ -5756,20 +5774,42 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
|
|||||||
// maintain padding line break at end of moved content.
|
// maintain padding line break at end of moved content.
|
||||||
if (moveFirstLineResult.Handled() &&
|
if (moveFirstLineResult.Handled() &&
|
||||||
moveFirstLineResult.DeleteRangeRef().IsPositioned()) {
|
moveFirstLineResult.DeleteRangeRef().IsPositioned()) {
|
||||||
nsresult rv;
|
nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
|
||||||
if (NS_WARN_IF(
|
moveFirstLineResult.DeleteRangeRef().EndRef());
|
||||||
NS_FAILED(rv = EnsureNoFollowingUnnecessaryLineBreak(
|
if (NS_FAILED(rv)) {
|
||||||
moveFirstLineResult.DeleteRangeRef().EndRef())))) {
|
NS_WARNING("EnsureNoFollowingUnnecessaryLineBreak() failed");
|
||||||
return Err(rv);
|
return Err(rv);
|
||||||
}
|
}
|
||||||
Result<CaretPoint, nsresult> caretPointOrError =
|
// If we moved a child block of the first line (although this is
|
||||||
InsertPaddingBRElementIfNeeded(
|
// logically wrong...), we should not put a <br> after that.
|
||||||
moveFirstLineResult.DeleteRangeRef().EndRef());
|
const bool movedLineEndsWithBlockBoundary = [&]() {
|
||||||
if (NS_WARN_IF(caretPointOrError.isErr())) {
|
Element* const commonAncestor =
|
||||||
return caretPointOrError.propagateErr();
|
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
|
// If we only deleted content in the range, we need to maintain padding line
|
||||||
// breaks at both deleted range boundaries.
|
// breaks at both deleted range boundaries.
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1315065
|
|||||||
/** Test for Bug 1315065 **/
|
/** Test for Bug 1315065 **/
|
||||||
SimpleTest.waitForExplicitFinish();
|
SimpleTest.waitForExplicitFinish();
|
||||||
SimpleTest.waitForFocus(() => {
|
SimpleTest.waitForFocus(() => {
|
||||||
const kNewWhiteSpaceNormalizerEnabled =
|
|
||||||
SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible");
|
|
||||||
const editor = document.getElementsByTagName("div")[0];
|
const editor = document.getElementsByTagName("div")[0];
|
||||||
function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) {
|
function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) {
|
||||||
editor.innerHTML = "<p id='p'>abc<br></p>";
|
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
|
// 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.
|
// 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.
|
// 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.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(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");
|
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");
|
const p = document.getElementById("p");
|
||||||
ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press");
|
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");
|
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 an empty text node, it's fine to delete all
|
||||||
// If Delete key is pressed in non-empty text node, only the text node should be modified.
|
// empty text nodes, but 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.
|
is(
|
||||||
is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed");
|
p.childNodes.length,
|
||||||
is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key");
|
2,
|
||||||
is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key");
|
`${kDescription}: <p> should have at least one text node after 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");
|
is(
|
||||||
} else if (!kNewWhiteSpaceNormalizerEnabled) {
|
p.textContent,
|
||||||
// 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.
|
"bc",
|
||||||
// This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case.
|
`${kDescription}: empty text nodes and 'a' should be removed by pressing Delete key`
|
||||||
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`);
|
|
||||||
}
|
|
||||||
editor.blur();
|
editor.blur();
|
||||||
}
|
}
|
||||||
SimpleTest.finish();
|
SimpleTest.finish();
|
||||||
|
|||||||
@@ -17,9 +17,6 @@
|
|||||||
[Control + Delete at "<p>abc [\]def </p>" - comparing innerHTML]
|
[Control + Delete at "<p>abc [\]def </p>" - comparing innerHTML]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Delete at "<p>ab[\]c </p>"]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Delete at "<p>a<span>[\]b</span>c</p>"]
|
[Delete at "<p>a<span>[\]b</span>c</p>"]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="variant" content="?white-space=normal">
|
||||||
|
<meta name="variant" content="?white-space=pre">
|
||||||
|
<meta name="variant" content="?white-space=pre-wrap">
|
||||||
|
<meta name="variant" content="?white-space=pre-line">
|
||||||
|
<title>Delete per word should not change the deleting range whether the surrounding word is wrapped in an inline element</title>
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/resources/testdriver.js"></script>
|
||||||
|
<script src="/resources/testdriver-vendor.js"></script>
|
||||||
|
<script src="/resources/testdriver-actions.js"></script>
|
||||||
|
<script src="../include/editor-test-utils.js"></script>
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(document.location.search);
|
||||||
|
const whiteSpace = searchParams.get("white-space");
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const editingHost = document.querySelector("div[contenteditable]");
|
||||||
|
editingHost.style.whiteSpace = whiteSpace;
|
||||||
|
const utils = new EditorTestUtils(editingHost);
|
||||||
|
/**
|
||||||
|
* How to delete per word may depend on the browser and the platform. On the
|
||||||
|
* other hand, the result (at least in the plaintext point of view) should be
|
||||||
|
* same even if surrounding word is wrapped in a <span>.
|
||||||
|
*/
|
||||||
|
for (const data of [
|
||||||
|
{
|
||||||
|
innerHTML: `abc <span>def</span> ghi[]`,
|
||||||
|
referenceInnerHTML: `abc def ghi[]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc <span>def</span>[] ghi`,
|
||||||
|
referenceInnerHTML: `abc def[] ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc <span>def[]</span> ghi`,
|
||||||
|
referenceInnerHTML: `abc def[] ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc <span>[]def</span> ghi`,
|
||||||
|
referenceInnerHTML: `abc []def ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc []<span>def</span> ghi`,
|
||||||
|
referenceInnerHTML: `abc []def ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc[] <span>def</span> ghi`,
|
||||||
|
referenceInnerHTML: `abc[] def ghi`,
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
promise_test(async () => {
|
||||||
|
utils.setupEditingHost(data.referenceInnerHTML);
|
||||||
|
await utils.sendBackspaceKey(utils.deleteWordModifier);
|
||||||
|
const expectedInnerText = editingHost.innerText;
|
||||||
|
utils.setupEditingHost(data.innerHTML);
|
||||||
|
await utils.sendBackspaceKey(utils.deleteWordModifier);
|
||||||
|
assert_equals(
|
||||||
|
editingHost.innerText.replaceAll("\u00A0", " "),
|
||||||
|
expectedInnerText.replaceAll("\u00A0", " ")
|
||||||
|
);
|
||||||
|
}, `Ctrl/Cmd - Backspace when "${data.innerHTML}" should get same innerText as when "${data.referenceInnerHTML}"`);
|
||||||
|
}
|
||||||
|
}, {once: true});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div contenteditable></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="variant" content="?white-space=normal">
|
||||||
|
<meta name="variant" content="?white-space=pre">
|
||||||
|
<meta name="variant" content="?white-space=pre-wrap">
|
||||||
|
<meta name="variant" content="?white-space=pre-line">
|
||||||
|
<title>Forward-delete per word should not change the deleting range whether the surrounding word is wrapped in an inline element</title>
|
||||||
|
<script src="/resources/testharness.js"></script>
|
||||||
|
<script src="/resources/testharnessreport.js"></script>
|
||||||
|
<script src="/resources/testdriver.js"></script>
|
||||||
|
<script src="/resources/testdriver-vendor.js"></script>
|
||||||
|
<script src="/resources/testdriver-actions.js"></script>
|
||||||
|
<script src="../include/editor-test-utils.js"></script>
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(document.location.search);
|
||||||
|
const whiteSpace = searchParams.get("white-space");
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const editingHost = document.querySelector("div[contenteditable]");
|
||||||
|
editingHost.style.whiteSpace = whiteSpace;
|
||||||
|
const utils = new EditorTestUtils(editingHost);
|
||||||
|
/**
|
||||||
|
* How to delete per word may depend on the browser and the platform. On the
|
||||||
|
* other hand, the result (at least in the plaintext point of view) should be
|
||||||
|
* same even if surrounding word is wrapped in a <span>.
|
||||||
|
*/
|
||||||
|
for (const data of [
|
||||||
|
{
|
||||||
|
innerHTML: `[]abc <span>def</span> ghi`,
|
||||||
|
referenceInnerHTML: `[]abc def ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc[] <span>def</span> ghi`,
|
||||||
|
referenceInnerHTML: `abc[] def ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc []<span>def</span> ghi`,
|
||||||
|
referenceInnerHTML: `abc []def ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc <span>[]def</span> ghi`,
|
||||||
|
referenceInnerHTML: `abc []def ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc <span>def</span>[] ghi`,
|
||||||
|
referenceInnerHTML: `abc def[] ghi`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerHTML: `abc <span>def</span> []ghi`,
|
||||||
|
referenceInnerHTML: `abc def []ghi`,
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
promise_test(async () => {
|
||||||
|
utils.setupEditingHost(data.referenceInnerHTML);
|
||||||
|
await utils.sendDeleteKey(utils.deleteWordModifier);
|
||||||
|
const expectedInnerText = editingHost.innerText;
|
||||||
|
utils.setupEditingHost(data.innerHTML);
|
||||||
|
await utils.sendDeleteKey(utils.deleteWordModifier);
|
||||||
|
assert_equals(
|
||||||
|
editingHost.innerText.replaceAll("\u00A0", " "),
|
||||||
|
expectedInnerText.replaceAll("\u00A0", " ")
|
||||||
|
);
|
||||||
|
}, `Ctrl/Cmd - Delete when "${data.innerHTML}" should get same innerText as when "${data.referenceInnerHTML}"`);
|
||||||
|
}
|
||||||
|
}, {once: true});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div contenteditable></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user