mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-03-04 07:40:42 +00:00
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:
parent
b68c36a5da
commit
dc906b0477
@ -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
|
||||
*****************************************************************************/
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user