Bug 1658702 - part 8: Add AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent() to compute atomic content deleting target range r=m_kato

This patch corresponds to the following part:
* https://searchfox.org/mozilla-central/rev/0c682c4f01442c3de0fa6cd286e9cadc8276b45f/editor/libeditor/HTMLEditSubActionHandler.cpp#3346-3360
* https://searchfox.org/mozilla-central/rev/0c682c4f01442c3de0fa6cd286e9cadc8276b45f/editor/libeditor/HTMLEditSubActionHandler.cpp#3750-3761
* https://searchfox.org/mozilla-central/rev/0c682c4f01442c3de0fa6cd286e9cadc8276b45f/editor/libeditor/WSRunObject.cpp#1075-1113

Let's return a range selecting the deleting atomic content (i.e., between
`EditorDOMPoint(&content)` and `EditorDOMPoint::After(content)`).

For doing it, we need to shrink the computed range after
`AutoRangeArray::ExtendAnchorFocusRangeFor()` because layout code puts
range boundaries to start or end of text node in this case.

Then, surprisingly, our deletion code have not used
`HandleDeleteCollapsedSelectionAtAtomicContent()` in most cases because
`AutoRangeArray::ExtendAnchorFocusRangeFor()` makes the collapsed range
to non-collapsed.  For making the deletion faster and simpler, this patch
shrinks the result of `AutoRangeArray::ExtendAnchorFocusRangeFor()` if
it has only one range and selects only one atomic content node.

Differential Revision: https://phabricator.services.mozilla.com/D88968
This commit is contained in:
Masayuki Nakano 2020-09-07 04:59:22 +00:00
parent b68c36a5da
commit dc906b0477
9 changed files with 899 additions and 58 deletions

View File

@ -5,9 +5,11 @@
#include "EditorUtils.h"
#include "WSRunObject.h"
#include "mozilla/ComputedStyle.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/TextEditor.h"
#include "mozilla/dom/Document.h"
@ -239,6 +241,7 @@ AutoRangeArray::ExtendAnchorFocusRangeFor(
NS_WARNING("Failed to extend the range, but ignored");
return directionAndAmountResult;
}
if (NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
extendedRange->GetStartContainer())) ||
NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
@ -261,6 +264,58 @@ AutoRangeArray::ExtendAnchorFocusRangeFor(
return directionAndAmountResult;
}
Result<bool, nsresult>
AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent,
const Element* aEditingHost) {
if (IsCollapsed()) {
return false;
}
switch (aDirectionAndAmount) {
case nsIEditor::eNext:
case nsIEditor::eNextWord:
case nsIEditor::ePrevious:
case nsIEditor::ePreviousWord:
break;
default:
return false;
}
bool changed = false;
for (auto& range : mRanges) {
MOZ_ASSERT(!range->IsInSelection(),
"Changing range in selection may cause running script");
Result<bool, nsresult> result =
WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
aHTMLEditor, range, aEditingHost);
if (result.isErr()) {
NS_WARNING(
"WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() "
"failed");
return Err(result.inspectErr());
}
changed |= result.inspect();
}
if (mRanges.Length() == 1 && aIfSelectingOnlyOneAtomicContent ==
IfSelectingOnlyOneAtomicContent::Collapse) {
MOZ_ASSERT(mRanges[0].get() == mAnchorFocusRange.get());
if (mAnchorFocusRange->GetStartContainer() ==
mAnchorFocusRange->GetEndContainer() &&
mAnchorFocusRange->GetChildAtStartOffset() &&
mAnchorFocusRange->StartRef().GetNextSiblingOfChildAtOffset() ==
mAnchorFocusRange->GetChildAtEndOffset()) {
mAnchorFocusRange->Collapse(aDirectionAndAmount == nsIEditor::eNext ||
aDirectionAndAmount == nsIEditor::eNextWord);
changed = true;
}
}
return changed;
}
/******************************************************************************
* some helper classes for iterating the dom tree
*****************************************************************************/

View File

@ -812,6 +812,24 @@ class MOZ_STACK_CLASS AutoRangeArray final {
ExtendAnchorFocusRangeFor(const EditorBase& aEditorBase,
nsIEditor::EDirection aDirectionAndAmount);
/**
* For compatiblity with the other browsers, we should shrink ranges to
* start from an atomic content and/or end after one instead of start
* from end of a preceding text node and end by start of a follwing text
* node. Returns true if this modifies a range.
*/
enum class IfSelectingOnlyOneAtomicContent {
Collapse, // Collapse to the range selecting only one atomic content to
// start or after of it. Whether to collapse start or after
// it depends on aDirectionAndAmount. This is ignored if
// there are multiple ranges.
KeepSelecting, // Won't collapse the range.
};
Result<bool, nsresult> ShrinkRangesIfStartFromOrEndAfterAtomicContent(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent,
const dom::Element* aEditingHost);
/**
* The following methods are same as `Selection`'s methods.
*/

View File

@ -2487,12 +2487,11 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
const EditorDOMPoint& aPointToDelete);
/**
* HandleDeleteCollapsedSelectionAtAtomicContent() handles deletion of
* atomic elements like `<br>`, `<hr>`, `<img>`, `<input>`, etc and
* data nodes except text node (e.g., comment node).
* Note that don't call this directly with `<hr>` element. Instead, call
* `HandleDeleteCollapsedSelectionAtHRElement()`.
* Note that don't call this for invisible `<br>` element.
* HandleDeleteAtomicContent() handles deletion of atomic elements like
* `<br>`, `<hr>`, `<img>`, `<input>`, etc and data nodes except text node
* (e.g., comment node). Note that don't call this directly with `<hr>`
* element. Instead, call `HandleDeleteCollapsedSelectionAtHRElement()`. Note
* that don't call this for invisible `<br>` element.
*
* @param aAtomicContent The atomic content to be deleted.
* @param aCaretPoint The caret point (i.e., selection start or
@ -2501,10 +2500,12 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
* with the caret point.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT EditActionResult
HandleDeleteCollapsedSelectionAtAtomicContent(
HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
const EditorDOMPoint& aCaretPoint,
const WSRunScanner& aWSRunScannerAtCaret);
HandleDeleteAtomicContent(HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
const EditorDOMPoint& aCaretPoint,
const WSRunScanner& aWSRunScannerAtCaret);
nsresult ComputeRangesToDeleteAtomicContent(
const HTMLEditor& aHTMLEditor, const nsIContent& aAtomicContent,
AutoRangeArray& aRangesToDelete) const;
/**
* HandleDeleteCollapsedSelectionAtHRElement() handles deletion around
@ -3151,11 +3152,11 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete(
if (NS_WARN_IF(!startPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
RefPtr<Element> editingHost = aHTMLEditor.GetActiveEditingHost();
if (NS_WARN_IF(!editingHost)) {
return NS_ERROR_FAILURE;
}
if (startPoint.GetContainerAsContent()) {
RefPtr<Element> editingHost = aHTMLEditor.GetActiveEditingHost();
if (NS_WARN_IF(!editingHost)) {
return NS_ERROR_FAILURE;
}
AutoEmptyBlockAncestorDeleter deleter;
if (deleter.ScanEmptyBlockInclusiveAncestor(
aHTMLEditor, *startPoint.GetContainerAsContent(), *editingHost)) {
@ -3196,7 +3197,25 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete(
NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
return extendResult.unwrapErr();
}
aDirectionAndAmount = extendResult.unwrap();
// For compatibility with other browsers, we should set target ranges
// to start from and/or end after an atomic content rather than start
// from preceding text node end nor end at following text node start.
Result<bool, nsresult> shrunkenResult =
aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
aHTMLEditor, aDirectionAndAmount,
AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
editingHost);
if (shrunkenResult.isErr()) {
NS_WARNING(
"AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() "
"failed");
return shrunkenResult.unwrapErr();
}
if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) {
aDirectionAndAmount = extendResult.unwrap();
}
if (aDirectionAndAmount == nsIEditor::eNone) {
MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
@ -3296,8 +3315,8 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
}
// If we are inside an empty block, delete it.
RefPtr<Element> editingHost = aHTMLEditor.GetActiveEditingHost();
if (startPoint.GetContainerAsContent()) {
RefPtr<Element> editingHost = aHTMLEditor.GetActiveEditingHost();
if (NS_WARN_IF(!editingHost)) {
return EditActionResult(NS_ERROR_FAILURE);
}
@ -3346,6 +3365,20 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
aHTMLEditor, *aHTMLEditor.SelectionRefPtr(), *startPoint.GetContainer(),
&aRangesToDelete);
// Calling `ExtendAnchorFocusRangeFor()` and
// `ShrinkRangesIfStartFromOrEndAfterAtomicContent()` may move caret to
// the container of deleting atomic content. However, it may be different
// from the original caret's container. The original caret container may
// be important to put caret after deletion so that let's cache the
// original position.
Maybe<EditorDOMPoint> caretPoint;
if (aRangesToDelete.IsCollapsed() && !aRangesToDelete.Ranges().IsEmpty()) {
caretPoint = Some(aRangesToDelete.GetStartPointOfFirstRange());
if (NS_WARN_IF(!caretPoint.ref().IsInContentNode())) {
return EditActionResult(NS_ERROR_FAILURE);
}
}
Result<nsIEditor::EDirection, nsresult> extendResult =
aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor,
aDirectionAndAmount);
@ -3353,7 +3386,30 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
return EditActionResult(extendResult.unwrapErr());
}
aDirectionAndAmount = extendResult.unwrap();
if (caretPoint.isSome() && !caretPoint.ref().IsSetAndValid()) {
NS_WARNING("The caret position became invalid");
return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
// If there is only one range and it selects an atomic content, we should
// delete it with collapsed range path for making consistent behavior
// between both cases, the content is selected case and caret is at it or
// after it case.
Result<bool, nsresult> shrunkenResult =
aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
aHTMLEditor, aDirectionAndAmount,
AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
editingHost);
if (shrunkenResult.isErr()) {
NS_WARNING(
"AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() "
"failed");
return EditActionResult(shrunkenResult.unwrapErr());
}
if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) {
aDirectionAndAmount = extendResult.unwrap();
}
if (aDirectionAndAmount == nsIEditor::eNone) {
MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
@ -3369,21 +3425,20 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
}
if (aRangesToDelete.IsCollapsed()) {
EditorDOMPoint caretPoint(aRangesToDelete.GetStartPointOfFirstRange());
if (NS_WARN_IF(!caretPoint.IsInContentNode())) {
return EditActionResult(NS_ERROR_FAILURE);
}
if (!EditorUtils::IsEditableContent(*caretPoint.ContainerAsContent(),
EditorType::HTML)) {
// Use the original caret position for handling the deletion around
// collapsed range because the container may be different from the
// new collapsed position's container.
if (!EditorUtils::IsEditableContent(
*caretPoint.ref().ContainerAsContent(), EditorType::HTML)) {
return EditActionCanceled();
}
WSRunScanner wsRunScannerAtCaret(aHTMLEditor, caretPoint);
WSRunScanner wsRunScannerAtCaret(aHTMLEditor, caretPoint.ref());
WSScanResult scanFromCaretPointResult =
aDirectionAndAmount == nsIEditor::eNext
? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
caretPoint)
caretPoint.ref())
: wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
caretPoint);
caretPoint.ref());
if (!scanFromCaretPointResult.GetContent()) {
return EditActionCanceled();
}
@ -3407,7 +3462,7 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
DeleteContentNodeAndJoinTextNodesAroundIt(
aHTMLEditor,
MOZ_KnownLive(*scanFromCaretPointResult.BRElementPtr()),
caretPoint);
caretPoint.ref());
if (NS_FAILED(rv)) {
NS_WARNING(
"WhiteSpaceVisibilityKeeper::"
@ -3421,8 +3476,8 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
AutoRangeArray rangesToDelete(*aHTMLEditor.SelectionRefPtr());
caretPoint = aRangesToDelete.GetStartPointOfFirstRange();
if (!caretPoint.IsSet()) {
caretPoint = Some(aRangesToDelete.GetStartPointOfFirstRange());
if (!caretPoint.ref().IsSet()) {
NS_WARNING(
"New selection after deleting invisible `<br>` element was "
"invalid");
@ -3434,14 +3489,15 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT)) {
// Let's check whether there is new invisible `<br>` element
// for avoiding infinit recursive calls.
WSRunScanner wsRunScannerAtCaret(aHTMLEditor, caretPoint);
WSRunScanner wsRunScannerAtCaret(aHTMLEditor, caretPoint.ref());
WSScanResult scanFromCaretPointResult =
aDirectionAndAmount == nsIEditor::eNext
? wsRunScannerAtCaret
.ScanNextVisibleNodeOrBlockBoundaryFrom(caretPoint)
.ScanNextVisibleNodeOrBlockBoundaryFrom(
caretPoint.ref())
: wsRunScannerAtCaret
.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
caretPoint);
caretPoint.ref());
if (scanFromCaretPointResult.ReachedBRElement() &&
!aHTMLEditor.IsVisibleBRElement(
scanFromCaretPointResult.BRElementPtr())) {
@ -3499,6 +3555,20 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges(
return rv;
}
if (aScanFromCaretPointResult.ReachedSpecialContent() ||
aScanFromCaretPointResult.ReachedBRElement()) {
if (aScanFromCaretPointResult.GetContent() ==
aWSRunScannerAtCaret.GetEditingHost()) {
return NS_OK;
}
nsresult rv = ComputeRangesToDeleteAtomicContent(
aHTMLEditor, *aScanFromCaretPointResult.GetContent(), aRangesToDelete);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent() failed");
return rv;
}
return NS_OK;
}
@ -3562,13 +3632,12 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges(
aWSRunScannerAtCaret.GetEditingHost()) {
return EditActionHandled();
}
EditActionResult result = HandleDeleteCollapsedSelectionAtAtomicContent(
EditActionResult result = HandleDeleteAtomicContent(
aHTMLEditor, MOZ_KnownLive(*aScanFromCaretPointResult.GetContent()),
aWSRunScannerAtCaret.ScanStartRef(), aWSRunScannerAtCaret);
NS_WARNING_ASSERTION(result.Succeeded(),
"AutoDeleteRangesHandler::"
"HandleDeleteCollapsedSelectionAtAtomicContent() "
"failed");
NS_WARNING_ASSERTION(
result.Succeeded(),
"AutoDeleteRangesHandler::HandleDeleteAtomicContent() failed");
return result;
}
@ -3942,12 +4011,11 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteCollapsedSelectionAtHRElement(
return EditActionHandled(canDeleteHRElement.unwrapErr());
}
if (canDeleteHRElement.inspect()) {
EditActionResult result = HandleDeleteCollapsedSelectionAtAtomicContent(
EditActionResult result = HandleDeleteAtomicContent(
aHTMLEditor, aHRElement, aCaretPoint, aWSRunScannerAtCaret);
NS_WARNING_ASSERTION(
result.Succeeded(),
"AutoDeleteRangesHandler::"
"HandleDeleteCollapsedSelectionAtAtomicContent() failed");
"AutoDeleteRangesHandler::HandleDeleteAtomicContent() failed");
return result;
}
@ -3997,11 +4065,28 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteCollapsedSelectionAtHRElement(
return EditActionHandled(rv);
}
EditActionResult HTMLEditor::AutoDeleteRangesHandler::
HandleDeleteCollapsedSelectionAtAtomicContent(
HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
const EditorDOMPoint& aCaretPoint,
const WSRunScanner& aWSRunScannerAtCaret) {
nsresult
HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent(
const HTMLEditor& aHTMLEditor, const nsIContent& aAtomicContent,
AutoRangeArray& aRangesToDelete) const {
EditorDOMRange rangeToDelete =
WSRunScanner::GetRangesForDeletingAtomicContent(aHTMLEditor,
aAtomicContent);
if (!rangeToDelete.IsPositioned()) {
NS_WARNING("WSRunScanner::GetRangeForDeleteAContentNode() failed");
return NS_ERROR_FAILURE;
}
nsresult rv = aRangesToDelete.SetStartAndEnd(rangeToDelete.StartRef(),
rangeToDelete.EndRef());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoRangeArray::SetStartAndEnd() failed");
return rv;
}
EditActionResult HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAtomicContent(
HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
const EditorDOMPoint& aCaretPoint,
const WSRunScanner& aWSRunScannerAtCaret) {
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT_IF(aAtomicContent.IsHTMLElement(nsGkAtoms::br),
aHTMLEditor.IsVisibleBRElement(&aAtomicContent));

View File

@ -1598,6 +1598,46 @@ WSRunScanner::TextFragmentData::InvisibleTrailingWhiteSpaceRangeRef() const {
return mTrailingWhiteSpaceRange.ref();
}
EditorDOMRangeInTexts
WSRunScanner::TextFragmentData::GetNonCollapsedRangeInTexts(
const EditorDOMRange& aRange) const {
if (!aRange.IsPositioned()) {
return EditorDOMRangeInTexts();
}
if (aRange.IsInTextNodes()) {
if (aRange.Collapsed()) {
return EditorDOMRangeInTexts();
}
// Note that this may return a range which don't include any invisible
// white-spaces due to empty text nodes.
return aRange.GetAsInTexts();
}
EditorDOMPointInText firstPoint =
aRange.StartRef().IsInTextNode()
? aRange.StartRef().AsInText()
: GetInclusiveNextEditableCharPoint(aRange.StartRef());
if (!firstPoint.IsSet()) {
return EditorDOMRangeInTexts();
}
EditorDOMPointInText endPoint;
if (aRange.EndRef().IsInTextNode()) {
endPoint = aRange.EndRef().AsInText();
} else {
// FYI: GetPreviousEditableCharPoint() returns last character's point
// of preceding text node if it's not empty, but we need end of
// the text node here.
endPoint = GetPreviousEditableCharPoint(aRange.EndRef());
if (endPoint.IsSet() && endPoint.IsAtLastContent()) {
MOZ_ALWAYS_TRUE(endPoint.AdvanceOffset());
}
}
if (!endPoint.IsSet() || firstPoint == endPoint) {
return EditorDOMRangeInTexts();
}
return EditorDOMRangeInTexts(firstPoint, endPoint);
}
const WSRunScanner::VisibleWhiteSpacesData&
WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const {
if (mVisibleWhiteSpacesData.isSome()) {
@ -3194,4 +3234,165 @@ WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(
: rangeToDelete;
}
// static
EditorDOMRange WSRunScanner::GetRangesForDeletingAtomicContent(
const HTMLEditor& aHTMLEditor, const nsIContent& aAtomicContent) {
if (aAtomicContent.IsHTMLElement(nsGkAtoms::br)) {
// Preceding white-spaces should be preserved, but the following
// white-spaces should be invisible around `<br>` element.
Element* editingHost = aHTMLEditor.GetActiveEditingHost();
TextFragmentData textFragmentDataAfterBRElement(
EditorDOMPoint::After(aAtomicContent), editingHost);
const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
textFragmentDataAfterBRElement.GetNonCollapsedRangeInTexts(
textFragmentDataAfterBRElement
.InvisibleLeadingWhiteSpaceRangeRef());
return followingInvisibleWhiteSpaces.IsPositioned() &&
!followingInvisibleWhiteSpaces.Collapsed()
? EditorDOMRange(
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
followingInvisibleWhiteSpaces.EndRef())
: EditorDOMRange(
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
EditorDOMPoint::After(aAtomicContent));
}
if (!HTMLEditUtils::IsBlockElement(aAtomicContent)) {
// Both preceding and following white-spaces around it should be preserved
// around inline elements like `<img>`.
return EditorDOMRange(
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
EditorDOMPoint::After(aAtomicContent));
}
// Both preceding and following white-spaces can be invisible around a
// block element.
Element* editingHost = aHTMLEditor.GetActiveEditingHost();
TextFragmentData textFragmentDataBeforeAtomicContent(
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)), editingHost);
const EditorDOMRangeInTexts precedingInvisibleWhiteSpaces =
textFragmentDataBeforeAtomicContent.GetNonCollapsedRangeInTexts(
textFragmentDataBeforeAtomicContent
.InvisibleTrailingWhiteSpaceRangeRef());
TextFragmentData textFragmentDataAfterAtomicContent(
EditorDOMPoint::After(aAtomicContent), editingHost);
const EditorDOMRangeInTexts followingInvisibleWhiteSpaces =
textFragmentDataAfterAtomicContent.GetNonCollapsedRangeInTexts(
textFragmentDataAfterAtomicContent
.InvisibleLeadingWhiteSpaceRangeRef());
if (precedingInvisibleWhiteSpaces.StartRef().IsSet() &&
followingInvisibleWhiteSpaces.EndRef().IsSet()) {
return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
followingInvisibleWhiteSpaces.EndRef());
}
if (precedingInvisibleWhiteSpaces.StartRef().IsSet()) {
return EditorDOMRange(precedingInvisibleWhiteSpaces.StartRef(),
EditorDOMPoint::After(aAtomicContent));
}
if (followingInvisibleWhiteSpaces.EndRef().IsSet()) {
return EditorDOMRange(
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
followingInvisibleWhiteSpaces.EndRef());
}
return EditorDOMRange(
EditorDOMPoint(const_cast<nsIContent*>(&aAtomicContent)),
EditorDOMPoint::After(aAtomicContent));
}
/******************************************************************************
* Utilities for other things.
******************************************************************************/
// static
Result<bool, nsresult>
WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
const HTMLEditor& aHTMLEditor, nsRange& aRange,
const Element* aEditingHost) {
MOZ_ASSERT(aRange.IsPositioned());
MOZ_ASSERT(!aRange.IsInSelection(),
"Changing range in selection may cause running script");
if (NS_WARN_IF(!aRange.GetStartContainer()) ||
NS_WARN_IF(!aRange.GetEndContainer())) {
return Err(NS_ERROR_FAILURE);
}
if (!aRange.GetStartContainer()->IsContent() ||
!aRange.GetEndContainer()->IsContent()) {
return false;
}
// If the range crosses a block boundary, we should do nothing for now
// because it hits a bug of inserting a padding `<br>` element after
// joining the blocks.
if (HTMLEditUtils::GetInclusiveAncestorBlockElementExceptHRElement(
*aRange.GetStartContainer()->AsContent(), aEditingHost) !=
HTMLEditUtils::GetInclusiveAncestorBlockElementExceptHRElement(
*aRange.GetEndContainer()->AsContent(), aEditingHost)) {
return false;
}
nsIContent* startContent = nullptr;
if (aRange.GetStartContainer() && aRange.GetStartContainer()->IsText() &&
aRange.GetStartContainer()->AsText()->Length() == aRange.StartOffset()) {
// If next content is a visible `<br>` element, special inline content
// (e.g., `<img>`, non-editable text node, etc) or a block level void
// element like `<hr>`, the range should start with it.
TextFragmentData textFragmentDataAtStart(
EditorRawDOMPoint(aRange.StartRef()), aEditingHost);
if (textFragmentDataAtStart.EndsByBRElement()) {
if (aHTMLEditor.IsVisibleBRElement(
textFragmentDataAtStart.EndReasonBRElementPtr())) {
startContent = textFragmentDataAtStart.EndReasonBRElementPtr();
}
} else if (textFragmentDataAtStart.EndsBySpecialContent() ||
(textFragmentDataAtStart.EndsByOtherBlockElement() &&
!HTMLEditUtils::IsContainerNode(
*textFragmentDataAtStart
.EndReasonOtherBlockElementPtr()))) {
startContent = textFragmentDataAtStart.GetEndReasonContent();
}
}
nsIContent* endContent = nullptr;
if (aRange.GetEndContainer() && aRange.GetEndContainer()->IsText() &&
!aRange.EndOffset()) {
// If previous content is a visible `<br>` element, special inline content
// (e.g., `<img>`, non-editable text node, etc) or a block level void
// element like `<hr>`, the range should end after it.
TextFragmentData textFragmentDataAtEnd(EditorRawDOMPoint(aRange.EndRef()),
aEditingHost);
if (textFragmentDataAtEnd.StartsFromBRElement()) {
if (aHTMLEditor.IsVisibleBRElement(
textFragmentDataAtEnd.StartReasonBRElementPtr())) {
endContent = textFragmentDataAtEnd.StartReasonBRElementPtr();
}
} else if (textFragmentDataAtEnd.StartsFromSpecialContent() ||
(textFragmentDataAtEnd.StartsFromOtherBlockElement() &&
!HTMLEditUtils::IsContainerNode(
*textFragmentDataAtEnd
.StartReasonOtherBlockElementPtr()))) {
endContent = textFragmentDataAtEnd.GetStartReasonContent();
}
}
if (!startContent && !endContent) {
return false;
}
nsresult rv = aRange.SetStartAndEnd(
startContent ? RangeBoundary(
startContent->GetParentNode(),
startContent->GetPreviousSibling()) // at startContent
: aRange.StartRef(),
endContent ? RangeBoundary(endContent->GetParentNode(),
endContent) // after endContent
: aRange.EndRef());
if (NS_FAILED(rv)) {
NS_WARNING("nsRange::SetStartAndEnd() failed");
return Err(rv);
}
return true;
}
} // namespace mozilla

View File

@ -360,6 +360,23 @@ class MOZ_STACK_CLASS WSRunScanner final {
GetRangeInTextNodesToBackspaceFrom(const HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint);
/**
* GetRangesForDeletingAtomicContent() returns the range to delete
* aAtomicContent. If it's followed by invisible white-spaces, they will
* be included into the range.
*/
static EditorDOMRange GetRangesForDeletingAtomicContent(
const HTMLEditor& aHTMLEditor, const nsIContent& aAtomicContent);
/**
* ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() may shrink aRange if it
* starts and/or ends with an atomic content, but the range boundary
* is in adjacent text nodes. Returns true if this modifies the range.
*/
static Result<bool, nsresult> ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
const HTMLEditor& aHTMLEditor, nsRange& aRange,
const Element* aEditingHost);
/**
* GetPrecedingBRElementUnlessVisibleContentFound() scans a `<br>` element
* backward, but stops scanning it if the scanner finds visible character
@ -869,6 +886,13 @@ class MOZ_STACK_CLASS WSRunScanner final {
EditorDOMPointInText GetFirstASCIIWhiteSpacePointCollapsedTo(
const EditorDOMPointInText& aPointAtASCIIWhiteSpace) const;
/**
* GetNonCollapsedRangeInTexts() returns non-empty range in texts which
* is the largest range in aRange if there is some text nodes.
*/
EditorDOMRangeInTexts GetNonCollapsedRangeInTexts(
const EditorDOMRange& aRange) const;
/**
* InvisibleLeadingWhiteSpaceRangeRef() retruns reference to two DOM points,
* start of the line and first visible point or end of the hard line. When

View File

@ -40,9 +40,6 @@
[Backspace at "<pre>abc </pre><p> [\]def </p>"]
expected: FAIL
[Backspace at "<p>abc<br>[\]def</p>"]
expected: FAIL
[Backspace at "<p>abc </p><p> [\] def</p>"]
expected: FAIL
@ -64,3 +61,15 @@
[Backspace at "<pre>abc </pre><p> [\]def</p>"]
expected: FAIL
[Backspace at "<div>abc<hr>[\]def</div>"]
expected: FAIL
[Backspace at "<div>abc <hr>[\]def</div>"]
expected: FAIL
[Backspace at "<div>abc<hr> [\]def</div>"]
expected: FAIL
[Backspace at "<div>abc<br><hr>[\]def</div>"]
expected: FAIL

View File

@ -42,9 +42,6 @@
[Delete at "<p>abc[\] </p><p> def</p>"]
expected: FAIL
[Delete at "<p>abc[\]<br>def</p>"]
expected: FAIL
[Delete at "<p>abc [\] </p><p> def</p>"]
expected: FAIL
@ -66,3 +63,15 @@
[Delete at "<table><tr><td>{}<br></td><td>cell2</td></tr></table>"]
expected: FAIL
[Delete at "<div>abc[\]<hr>def</div>"]
expected: FAIL
[Delete at "<div>abc[\]<hr> def</div>"]
expected: FAIL
[Delete at "<div>abc[\] <hr>def</div>"]
expected: FAIL
[Delete at "<div>abc[\]<br><hr>def</div>"]
expected: FAIL

View File

@ -15,6 +15,8 @@ const kMeta = "\uE03d";
const kControl = "\uE009";
const kAlt = "\uE00A";
const kImgSrc = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg==";
let selection = getSelection();
let editor = document.querySelector("div[contenteditable]");
let beforeinput = [];
@ -524,19 +526,247 @@ promise_test(async () => {
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc<br>def</p>";
let p = document.querySelector("p");
let def = editor.querySelector("br").nextSibling;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abcdef</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: editor.firstChild,
startContainer: p,
startOffset: 1,
endContainer: def,
endOffset: 0,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<br>[]def</p>"');
// Deleting visible `<br>` element following white-space should not include
// the preceding white-space in the range.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc <br>def</p>";
let p = editor.querySelector("p");
let def = editor.querySelector("br").nextSibling;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abc def</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc <br>[]def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
let def = p.lastChild;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abcdef</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<img>[]def</p>"');
// White-spaces around `<img>` element are visible so that they shouldn't
// be included into the target ranges.
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc <img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
let def = p.lastChild;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abc def</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc <img>[]def</p>"');
// White-spaces around `<img>` element are visible so that they shouldn't
// be included into the target ranges.
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}"> def</p>`;
let p = editor.querySelector("p");
let def = p.lastChild;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abc def</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<img>[] def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
let abc = p.firstChild;
selection.collapse(p, 2);
await sendBackspaceKey();
assert_equals(editor.innerHTML, `<p>abc<img src="${kImgSrc}">def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<img>{}<img>def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
let def = p.lastChild;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, `<p>abc<img src="${kImgSrc}">def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 2,
endContainer: p,
endOffset: 3,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<img><img>[]def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc<hr>def</div>`;
let div = editor.querySelector("div");
let hr = editor.querySelector("hr");
let def = hr.nextSibling;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: div,
startOffset: 1,
endContainer: div,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<div>abc<hr>[]def</div>"');
// White-spaces around block element are invisible white-spaces so that
// they should be included into the target ranges to avoid they bcome
// visible.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc <hr>def</div>`;
let div = editor.querySelector("div");
let abc = div.firstChild;
let hr = editor.querySelector("hr");
let def = hr.nextSibling;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: div,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<div>abc <hr>[]def</div>"');
// White-spaces around block element are invisible white-spaces so that
// they should be included into the target ranges to avoid they bcome
// visible.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc<hr> def</div>`;
let div = editor.querySelector("div");
let hr = editor.querySelector("hr");
let def = hr.nextSibling;
selection.collapse(def, 1);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: div,
startOffset: 1,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<div>abc<hr> []def</div>"');
// Invisible `<br>` element immediately before `<hr>` element should be
// deleted once, and both of them should be included in the target range.
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc<br><hr>def</div>`;
let div = editor.querySelector("div");
let def = div.lastChild;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: div,
startOffset: 1,
endContainer: div,
endOffset: 3,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<div>abc<br><hr>[]def</div>"');
// Deleting visible `<br>` element followed by white-space should include
// the following white-space in the range because it shouldn't become
// visible and should be deleted for avoiding it.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc<br> def</p>";
let p = editor.querySelector("p");
let def = editor.querySelector("br").nextSibling;
selection.collapse(def, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abcdef</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<br>[] def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc<br> def</p>";
let p = editor.querySelector("p");
let def = editor.querySelector("br").nextSibling;
selection.collapse(def, 1);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abcdef</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<br> []def</p>"');
// Deleting visible `<br>` element should be contained by a range of
// `getTargetRanges()`. However, when only the `<br>` element is selected,
// the range shouldn't start from nor end by surrounding text nodes?

View File

@ -15,6 +15,8 @@ const kMeta = "\uE03d";
const kControl = "\uE009";
const kAlt = "\uE00A";
const kImgSrc = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg==";
let selection = getSelection();
let editor = document.querySelector("div[contenteditable]");
let beforeinput = [];
@ -521,19 +523,61 @@ promise_test(async () => {
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc<br>def</p>";
let abc = editor.firstChild.firstChild;
let p = editor.querySelector("p");
let abc = p.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abcdef</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: editor.querySelector("p"),
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc[]<br>def</p>"');
// Deleting visible `<br>` element following white-space should not include
// the preceding white-space in the range.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc <br>def</p>";
let p = editor.querySelector("p");
let abc = p.firstChild;
selection.collapse(abc, 4);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abc def</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc []<br>def</p>"');
// Deleting visible `<br>` element followed by white-space should include
// the following white-space in the range because it shouldn't become
// visible and should be deleted for avoiding it.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc<br> def</p>";
let p = editor.querySelector("p");
let abc = p.firstChild;
let def = editor.querySelector("br").nextSibling;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abcdef</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: def,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc[]<br> def</p>"');
// Deleting visible `<br>` element should be contained by a range of
// `getTargetRanges()`. However, when only the `<br>` element is selected,
// the range shouldn't start from nor end by surrounding text nodes?
@ -553,6 +597,172 @@ promise_test(async () => {
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc{<br>}def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
let abc = p.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abcdef</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc[]<img>def</p>"');
// White-spaces around `<img>` element are visible so that they shouldn't
// be included into the target ranges.
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc <img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
let abc = p.firstChild;
selection.collapse(abc, 4);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abc def</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc []<img>def</p>"');
// White-spaces around `<img>` element are visible so that they shouldn't
// be included into the target ranges.
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}"> def</p>`;
let p = editor.querySelector("p");
let abc = p.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abc def</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc[]<img> def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
let abc = p.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, `<p>abc<img src="${kImgSrc}">def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 1,
endContainer: p,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc[]<img><img>def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<p>abc<img src="${kImgSrc}"><img src="${kImgSrc}">def</p>`;
let p = editor.querySelector("p");
selection.collapse(p, 2);
await sendDeleteKey();
assert_equals(editor.innerHTML, `<p>abc<img src="${kImgSrc}">def</p>`);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 2,
endContainer: p,
endOffset: 3,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>abc<img>{}<img>def</p>"');
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc<hr>def</div>`;
let div = editor.querySelector("div");
let abc = div.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: div,
startOffset: 1,
endContainer: div,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<div>abc[]<hr>def</div>"');
// White-spaces around block element are invisible white-spaces so that
// they should be included into the target ranges to avoid they become
// visible.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc <hr>def</div>`;
let div = editor.querySelector("div");
let abc = div.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: div,
startOffset: 1,
endContainer: div,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<div>abc[] <hr>def</div>"');
// White-spaces around block element are invisible white-spaces so that
// they should be included into the target ranges to avoid they become
// visible.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc<hr> def</div>`;
let div = editor.querySelector("div");
let abc = div.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: div,
startOffset: 1,
endContainer: div,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<div>abc[]<hr> def</div>"');
// Invisible `<br>` element immediately before `<hr>` element should be
// delete once, and both of them should be included in the target range.
promise_test(async () => {
reset();
editor.innerHTML = `<div>abc<br><hr>def</div>`;
let div = editor.querySelector("div");
let abc = div.firstChild;
selection.collapse(abc, 3);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<div>abcdef</div>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: div,
startOffset: 1,
endContainer: div,
endOffset: 3,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<div>abc[]<br><hr>def</div>"');
// Joining parent block and child block should remove invisible preceding
// white-spaces of the child block and invisible leading white-spaces in
// the child block, and they should be contained in a range of