Bug 1658702 - part 2: Make `AutoDeleteRangesHandler::ComputeRangesToDelete()` handle the case deleting empty ancestor(s) r=m_kato

This patch implements computation of target ranges for this part:
https://searchfox.org/mozilla-central/rev/73a14f1b367948faa571ed2fe5d7eb29460787c1/editor/libeditor/HTMLEditSubActionHandler.cpp#3099-3141

This patch adds some utility methods for computing the ranges.  Currently,
it's not yet standardized, but the other browser engines look for leaf content
of another block when blocks are joined (or a block is deleted like this case).
Therefore, we follow the behavior basically, but different from the other
browsers, we should include invisible white-spaces into the range when they
are included.  That avoids the invisible white-spaces become visible when
web apps do something instead of us.  Note that utility methods have the code,
but this patch does not use it because in this case, we just delete a empty
block ancestor, not join it with previous/next block.

Differential Revision: https://phabricator.services.mozilla.com/D88377
This commit is contained in:
Masayuki Nakano 2020-09-01 02:02:50 +00:00
Родитель 891f3554a7
Коммит f94c0a973f
10 изменённых файлов: 918 добавлений и 58 удалений

Просмотреть файл

@ -783,6 +783,24 @@ class MOZ_STACK_CLASS AutoRangeArray final {
return EditorDOMPoint(mRanges[0]->EndRef());
}
nsresult SelectNode(nsINode& aNode) {
mRanges.Clear();
if (!mAnchorFocusRange) {
mAnchorFocusRange = nsRange::Create(&aNode);
if (!mAnchorFocusRange) {
return NS_ERROR_FAILURE;
}
}
ErrorResult error;
mAnchorFocusRange->SelectNode(aNode, error);
if (error.Failed()) {
mAnchorFocusRange = nullptr;
return error.StealNSResult();
}
mRanges.AppendElement(*mAnchorFocusRange);
return NS_OK;
}
/**
* ExtendAnchorFocusRangeFor() extends the anchor-focus range for deleting
* content for aDirectionAndAmount. The range won't be extended to outer of
@ -822,6 +840,29 @@ class MOZ_STACK_CLASS AutoRangeArray final {
mRanges.AppendElement(*mAnchorFocusRange);
return NS_OK;
}
template <typename SPT, typename SCT, typename EPT, typename ECT>
nsresult SetStartAndEnd(const EditorDOMPointBase<SPT, SCT>& aStart,
const EditorDOMPointBase<EPT, ECT>& aEnd) {
mRanges.Clear();
if (!mAnchorFocusRange) {
ErrorResult error;
mAnchorFocusRange = nsRange::Create(aStart.ToRawRangeBoundary(),
aEnd.ToRawRangeBoundary(), error);
if (error.Failed()) {
mAnchorFocusRange = nullptr;
return error.StealNSResult();
}
} else {
nsresult rv = mAnchorFocusRange->SetStartAndEnd(
aStart.ToRawRangeBoundary(), aEnd.ToRawRangeBoundary());
if (NS_FAILED(rv)) {
mAnchorFocusRange = nullptr;
return rv;
}
}
mRanges.AppendElement(*mAnchorFocusRange);
return NS_OK;
}
const nsRange* GetAnchorFocusRange() const { return mAnchorFocusRange; }
nsDirection GetDirection() const { return mDirection; }

Просмотреть файл

@ -71,8 +71,10 @@ class nsISupports;
namespace mozilla {
using namespace dom;
using StyleDifference = HTMLEditUtils::StyleDifference;
using ChildBlockBoundary = HTMLEditUtils::ChildBlockBoundary;
using InvisibleWhiteSpaces = HTMLEditUtils::InvisibleWhiteSpaces;
using StyleDifference = HTMLEditUtils::StyleDifference;
using TableBoundary = HTMLEditUtils::TableBoundary;
enum { kLonely = 0, kPrevSib = 1, kNextSib = 2, kBothSibs = 3 };
@ -2969,6 +2971,15 @@ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
const HTMLEditor& aHTMLEditor, nsIContent& aStartContent,
Element& aEditingHostElement);
/**
* ComputeTargetRanges() computes "target ranges" for deleting
* `mEmptyInclusiveAncestorBlockElement`.
*/
nsresult ComputeTargetRanges(const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
const Element& aEditingHost,
AutoRangeArray& aRangesToDelete) const;
/**
* Deletes found empty block element by `ScanEmptyBlockInclusiveAncestor()`.
* If found one is a list item element, calls
@ -3124,7 +3135,32 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete(
return rv;
}
// Do complicated things.
SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed()
? SelectionWasCollapsed::Yes
: SelectionWasCollapsed::No;
if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
EditorDOMPoint startPoint(aRangesToDelete.GetStartPointOfFirstRange());
if (NS_WARN_IF(!startPoint.IsSet())) {
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)) {
nsresult rv = deleter.ComputeTargetRanges(
aHTMLEditor, aDirectionAndAmount, *editingHost, aRangesToDelete);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoEmptyBlockAncestorDeleter::ComputeTargetRanges() failed");
return rv;
}
}
}
return NS_OK;
}
@ -3175,8 +3211,7 @@ EditActionResult HTMLEditor::AutoDeleteRangesHandler::Run(
#endif // #ifdef DEBUG
AutoEmptyBlockAncestorDeleter deleter;
if (deleter.ScanEmptyBlockInclusiveAncestor(
aHTMLEditor, MOZ_KnownLive(*startPoint.GetContainerAsContent()),
*editingHost)) {
aHTMLEditor, *startPoint.GetContainerAsContent(), *editingHost)) {
EditActionResult result = deleter.Run(aHTMLEditor, aDirectionAndAmount);
if (result.Failed() || result.Handled()) {
NS_WARNING_ASSERTION(result.Succeeded(),
@ -8973,6 +9008,81 @@ Element* HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
return mEmptyInclusiveAncestorBlockElement;
}
nsresult HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
ComputeTargetRanges(const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
const Element& aEditingHost,
AutoRangeArray& aRangesToDelete) const {
MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
// We'll delete `mEmptyInclusiveAncestorBlockElement` node from the tree, but
// we should return the range from start/end of next/previous editable content
// to end/start of the element for compatiblity with the other browsers.
switch (aDirectionAndAmount) {
case nsIEditor::eNone:
break;
case nsIEditor::ePrevious:
case nsIEditor::ePreviousWord:
case nsIEditor::eToBeginningOfLine: {
EditorRawDOMPoint startPoint =
HTMLEditUtils::GetPreviousEditablePoint<EditorRawDOMPoint>(
*mEmptyInclusiveAncestorBlockElement, &aEditingHost,
// In this case, we don't join block elements so that we won't
// delete invisible trailing whitespaces in the previous element.
InvisibleWhiteSpaces::Preserve,
// In this case, we won't join table cells so that we should
// get a range which is in a table cell even if it's in a
// table.
TableBoundary::NoCrossAnyTableElement);
if (!startPoint.IsSet()) {
NS_WARNING(
"HTMLEditUtils::GetPreviousEditablePoint() didn't return a valid "
"point");
return NS_ERROR_FAILURE;
}
nsresult rv = aRangesToDelete.SetStartAndEnd(
startPoint,
EditorRawDOMPoint::AtEndOf(mEmptyInclusiveAncestorBlockElement));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoRangeArray::SetStartAndEnd() failed");
return rv;
}
case nsIEditor::eNext:
case nsIEditor::eNextWord:
case nsIEditor::eToEndOfLine: {
EditorRawDOMPoint endPoint =
HTMLEditUtils::GetNextEditablePoint<EditorRawDOMPoint>(
*mEmptyInclusiveAncestorBlockElement, &aEditingHost,
// In this case, we don't join block elements so that we won't
// delete invisible trailing whitespaces in the next element.
InvisibleWhiteSpaces::Preserve,
// In this case, we won't join table cells so that we should
// get a range which is in a table cell even if it's in a
// table.
TableBoundary::NoCrossAnyTableElement);
if (!endPoint.IsSet()) {
NS_WARNING(
"HTMLEditUtils::GetNextEditablePoint() didn't return a valid "
"point");
return NS_ERROR_FAILURE;
}
nsresult rv = aRangesToDelete.SetStartAndEnd(
EditorRawDOMPoint(mEmptyInclusiveAncestorBlockElement, 0), endPoint);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoRangeArray::SetStartAndEnd() failed");
return rv;
}
default:
MOZ_ASSERT_UNREACHABLE("Handle the nsIEditor::EDirection value");
break;
}
// No direction, let's select the element to be deleted.
nsresult rv =
aRangesToDelete.SelectNode(*mEmptyInclusiveAncestorBlockElement);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::SelectNode() failed");
return rv;
}
Result<RefPtr<Element>, nsresult>
HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor) {

Просмотреть файл

@ -5,16 +5,21 @@
#include "HTMLEditUtils.h"
#include "CSSEditUtils.h" // for CSSEditUtils
#include "mozilla/ArrayUtils.h" // for ArrayLength
#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
#include "mozilla/EditAction.h" // for EditAction
#include "mozilla/EditorBase.h" // for EditorBase, EditorType
#include "mozilla/EditorUtils.h" // for EditorUtils
#include "CSSEditUtils.h" // for CSSEditUtils
#include "WSRunObject.h" // for WSRunScanner
#include "mozilla/ArrayUtils.h" // for ArrayLength
#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
#include "mozilla/EditAction.h" // for EditAction
#include "mozilla/EditorBase.h" // for EditorBase, EditorType
#include "mozilla/EditorDOMPoint.h" // for EditorDOMPoint, etc.
#include "mozilla/EditorUtils.h" // for EditorUtils
#include "mozilla/dom/Element.h" // for Element, nsINode
#include "mozilla/dom/HTMLAnchorElement.h"
#include "mozilla/dom/Element.h" // for Element, nsINode
#include "nsAString.h" // for nsAString::IsEmpty
#include "nsAtom.h" // for nsAtom
#include "mozilla/dom/Text.h" // for Text
#include "nsAString.h" // for nsAString::IsEmpty
#include "nsAtom.h" // for nsAtom
#include "nsCaseTreatment.h"
#include "nsCOMPtr.h" // for nsCOMPtr, operator==, etc.
#include "nsDebug.h" // for NS_ASSERTION, etc.
@ -26,11 +31,30 @@
#include "nsNameSpaceManager.h" // for kNameSpaceID_None
#include "nsString.h" // for nsAutoString
#include "nsStyledElement.h"
#include "nsTextFragment.h" // for nsTextFragment
namespace mozilla {
using namespace dom;
using EditorType = EditorBase::EditorType;
template EditorDOMPoint HTMLEditUtils::GetPreviousEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template EditorRawDOMPoint HTMLEditUtils::GetPreviousEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template EditorDOMPoint HTMLEditUtils::GetNextEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template EditorRawDOMPoint HTMLEditUtils::GetNextEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
bool HTMLEditUtils::CanContentsBeJoined(const nsIContent& aLeftContent,
const nsIContent& aRightContent,
StyleDifference aStyleDifference) {
@ -670,6 +694,232 @@ bool HTMLEditUtils::IsSingleLineContainer(nsINode& aNode) {
aNode.IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::dt, nsGkAtoms::dd);
}
// static
template <typename EditorDOMPointType>
EditorDOMPointType HTMLEditUtils::GetPreviousEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary) {
MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode(aContent));
NS_ASSERTION(!HTMLEditUtils::IsAnyTableElement(&aContent) ||
HTMLEditUtils::IsTableCellOrCaption(aContent),
"HTMLEditUtils::GetPreviousEditablePoint() may return a point "
"between table structure elements");
// First, look for previous content.
nsIContent* previousContent = aContent.GetPreviousSibling();
if (!previousContent) {
if (!aContent.GetParentElement()) {
return EditorDOMPointType();
}
nsIContent* inclusiveAncestor = &aContent;
for (Element* parentElement : aContent.AncestorsOfType<Element>()) {
previousContent = parentElement->GetPreviousSibling();
if (!previousContent &&
(parentElement == aAncestorLimiter ||
!HTMLEditUtils::IsSimplyEditableNode(*parentElement) ||
!HTMLEditUtils::CanCrossContentBoundary(*parentElement,
aHowToTreatTableBoundary))) {
// If cannot cross the parent element boundary, return the point of
// last inclusive ancestor point.
return EditorDOMPointType(inclusiveAncestor);
}
// Start of the parent element is a next editable point if it's an
// element which is not a table structure element.
if (!HTMLEditUtils::IsAnyTableElement(parentElement) ||
HTMLEditUtils::IsTableCellOrCaption(*parentElement)) {
inclusiveAncestor = parentElement;
}
if (!previousContent) {
continue; // Keep looking for previous sibling of an ancestor.
}
// XXX Should we ignore data node like CDATA, Comment, etc?
// If previous content is not editable, let's return the point after it.
if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
return EditorDOMPointType::After(*previousContent);
}
// If cannot cross previous content boundary, return start of last
// inclusive ancestor.
if (!HTMLEditUtils::CanCrossContentBoundary(*previousContent,
aHowToTreatTableBoundary)) {
return inclusiveAncestor == &aContent
? EditorDOMPointType(inclusiveAncestor)
: EditorDOMPointType(inclusiveAncestor, 0);
}
break;
}
if (!previousContent) {
return EditorDOMPointType(inclusiveAncestor);
}
} else if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) {
return EditorDOMPointType::After(*previousContent);
} else if (!HTMLEditUtils::CanCrossContentBoundary(
*previousContent, aHowToTreatTableBoundary)) {
return EditorDOMPointType(&aContent);
}
// Next, look for end of the previous content.
nsIContent* leafContent = previousContent;
if (previousContent->GetChildCount() &&
HTMLEditUtils::IsContainerNode(*previousContent)) {
for (nsIContent* maybeLeafContent = previousContent->GetLastChild();
maybeLeafContent;
maybeLeafContent = maybeLeafContent->GetLastChild()) {
// If it's not an editable content or cannot cross the boundary,
// return the point after the content. Note that in this case,
// the content must not be any table elements except `<table>`
// because we've climbed down the tree.
if (!HTMLEditUtils::IsSimplyEditableNode(*maybeLeafContent) ||
!HTMLEditUtils::CanCrossContentBoundary(*maybeLeafContent,
aHowToTreatTableBoundary)) {
return EditorDOMPointType::After(*maybeLeafContent);
}
leafContent = maybeLeafContent;
if (!HTMLEditUtils::IsContainerNode(*leafContent)) {
break;
}
}
}
if (leafContent->IsText()) {
Text* textNode = leafContent->AsText();
if (aInvisibleWhiteSpaces == InvisibleWhiteSpaces::Preserve) {
return EditorDOMPointType::AtEndOf(*textNode);
}
// There may be invisible trailing white-spaces which should be
// ignored. Let's scan its start.
return WSRunScanner::GetAfterLastVisiblePoint<EditorDOMPointType>(
*textNode, aAncestorLimiter);
}
// If it's a container element, return end of it. Otherwise, return
// the point after the non-container element.
return HTMLEditUtils::IsContainerNode(*leafContent)
? EditorDOMPointType::AtEndOf(*leafContent)
: EditorDOMPointType::After(*leafContent);
}
// static
template <typename EditorDOMPointType>
EditorDOMPointType HTMLEditUtils::GetNextEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary) {
MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode(aContent));
NS_ASSERTION(!HTMLEditUtils::IsAnyTableElement(&aContent) ||
HTMLEditUtils::IsTableCellOrCaption(aContent),
"HTMLEditUtils::GetPreviousEditablePoint() may return a point "
"between table structure elements");
// First, look for next content.
nsIContent* nextContent = aContent.GetNextSibling();
if (!nextContent) {
if (!aContent.GetParentElement()) {
return EditorDOMPointType();
}
nsIContent* inclusiveAncestor = &aContent;
for (Element* parentElement : aContent.AncestorsOfType<Element>()) {
// End of the parent element is a next editable point if it's an
// element which is not a table structure element.
if (!HTMLEditUtils::IsAnyTableElement(parentElement) ||
HTMLEditUtils::IsTableCellOrCaption(*parentElement)) {
inclusiveAncestor = parentElement;
}
nextContent = parentElement->GetNextSibling();
if (!nextContent &&
(parentElement == aAncestorLimiter ||
!HTMLEditUtils::IsSimplyEditableNode(*parentElement) ||
!HTMLEditUtils::CanCrossContentBoundary(*parentElement,
aHowToTreatTableBoundary))) {
// If cannot cross the parent element boundary, return the point of
// last inclusive ancestor point.
return EditorDOMPointType(inclusiveAncestor);
}
// End of the parent element is a next editable point if it's an
// element which is not a table structure element.
if (!HTMLEditUtils::IsAnyTableElement(parentElement) ||
HTMLEditUtils::IsTableCellOrCaption(*parentElement)) {
inclusiveAncestor = parentElement;
}
if (!nextContent) {
continue; // Keep looking for next sibling of an ancestor.
}
// XXX Should we ignore data node like CDATA, Comment, etc?
// If next content is not editable, let's return the point after
// the last inclusive ancestor.
if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
return EditorDOMPointType::After(*parentElement);
}
// If cannot cross next content boundary, return after the last
// inclusive ancestor.
if (!HTMLEditUtils::CanCrossContentBoundary(*nextContent,
aHowToTreatTableBoundary)) {
return EditorDOMPointType::After(*inclusiveAncestor);
}
break;
}
if (!nextContent) {
return EditorDOMPointType::After(*inclusiveAncestor);
}
} else if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) {
return EditorDOMPointType::After(aContent);
} else if (!HTMLEditUtils::CanCrossContentBoundary(
*nextContent, aHowToTreatTableBoundary)) {
return EditorDOMPointType::After(aContent);
}
// Next, look for start of the next content.
nsIContent* leafContent = nextContent;
if (nextContent->GetChildCount() &&
HTMLEditUtils::IsContainerNode(*nextContent)) {
for (nsIContent* maybeLeafContent = nextContent->GetFirstChild();
maybeLeafContent;
maybeLeafContent = maybeLeafContent->GetFirstChild()) {
// If it's not an editable content or cannot cross the boundary,
// return the point at the content (i.e., start of its parent). Note
// that in this case, the content must not be any table elements except
// `<table>` because we've climbed down the tree.
if (!HTMLEditUtils::IsSimplyEditableNode(*maybeLeafContent) ||
!HTMLEditUtils::CanCrossContentBoundary(*maybeLeafContent,
aHowToTreatTableBoundary)) {
return EditorDOMPointType(maybeLeafContent);
}
leafContent = maybeLeafContent;
if (!HTMLEditUtils::IsContainerNode(*leafContent)) {
break;
}
}
}
if (leafContent->IsText()) {
Text* textNode = leafContent->AsText();
if (aInvisibleWhiteSpaces == InvisibleWhiteSpaces::Preserve) {
return EditorDOMPointType(textNode, 0);
}
// There may be invisible leading white-spaces which should be
// ignored. Let's scan its start.
return WSRunScanner::GetFirstVisiblePoint<EditorDOMPointType>(
*textNode, aAncestorLimiter);
}
// If it's a container element, return start of it. Otherwise, return
// the point at the non-container element (i.e., start of its parent).
return HTMLEditUtils::IsContainerNode(*leafContent)
? EditorDOMPointType(leafContent, 0)
: EditorDOMPointType(leafContent);
}
// static
Element*
HTMLEditUtils::GetInclusiveAncestorEditableBlockElementOrInlineEditingHost(

Просмотреть файл

@ -477,6 +477,32 @@ class HTMLEditUtils final {
return previousContent;
}
/**
* Get previous/next editable point from start or end of aContent.
*/
enum class InvisibleWhiteSpaces {
Ignore, // Ignore invisible white-spaces, i.e., don't return middle of
// them.
Preserve, // Preserve invisible white-spaces, i.e., result may be start or
// end of a text node even if it begins or ends with invisible
// white-spaces.
};
enum class TableBoundary {
Ignore, // May cross any table element boundary.
NoCrossTableElement, // Won't cross `<table>` element boundary.
NoCrossAnyTableElement, // Won't cross any table element boundary.
};
template <typename EditorDOMPointType>
static EditorDOMPointType GetPreviousEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template <typename EditorDOMPointType>
static EditorDOMPointType GetNextEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
/**
* GetAncestorBlockElement() returns parent or nearest ancestor of aContent
* which is a block element. If aAncestorLimiter is not nullptr,
@ -776,6 +802,16 @@ class HTMLEditUtils final {
private:
static bool CanNodeContain(nsHTMLTag aParentTagId, nsHTMLTag aChildTagId);
static bool IsContainerNode(nsHTMLTag aTagId);
static bool CanCrossContentBoundary(nsIContent& aContent,
TableBoundary aHowToTreatTableBoundary) {
const bool cannotCrossBoundary =
(aHowToTreatTableBoundary == TableBoundary::NoCrossAnyTableElement &&
HTMLEditUtils::IsAnyTableElement(&aContent)) ||
(aHowToTreatTableBoundary == TableBoundary::NoCrossTableElement &&
aContent.IsHTMLElement(nsGkAtoms::table));
return !cannotCrossBoundary;
}
};
/**

Просмотреть файл

@ -48,6 +48,14 @@ template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
const EditorDOMPoint& aPoint) const;
template WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
const EditorRawDOMPoint& aPoint) const;
template EditorDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template EditorRawDOMPoint WSRunScanner::GetAfterLastVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template EditorDOMPoint WSRunScanner::GetFirstVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template EditorRawDOMPoint WSRunScanner::GetFirstVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template nsresult WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aScanStartPoint);
@ -2253,6 +2261,44 @@ WSRunScanner::TextFragmentData::GetPreviousEditableCharPoint(
return EditorDOMPointInText();
}
// static
template <typename EditorDOMPointType>
EditorDOMPointType WSRunScanner::GetAfterLastVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter) {
if (EditorUtils::IsContentPreformatted(aTextNode)) {
return EditorDOMPointType::AtEndOf(aTextNode);
}
TextFragmentData textFragmentData(
EditorDOMPoint(&aTextNode,
aTextNode.Length() ? aTextNode.Length() - 1 : 0),
aAncestorLimiter);
const EditorDOMRange& invisibleWhiteSpaceRange =
textFragmentData.InvisibleTrailingWhiteSpaceRangeRef();
if (!invisibleWhiteSpaceRange.IsPositioned() ||
invisibleWhiteSpaceRange.Collapsed()) {
return EditorDOMPointType::AtEndOf(aTextNode);
}
return EditorDOMPointType(invisibleWhiteSpaceRange.StartRef());
}
// static
template <typename EditorDOMPointType>
EditorDOMPointType WSRunScanner::GetFirstVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter) {
if (EditorUtils::IsContentPreformatted(aTextNode)) {
return EditorDOMPointType(&aTextNode, 0);
}
TextFragmentData textFragmentData(EditorDOMPoint(&aTextNode, 0),
aAncestorLimiter);
const EditorDOMRange& invisibleWhiteSpaceRange =
textFragmentData.InvisibleLeadingWhiteSpaceRangeRef();
if (!invisibleWhiteSpaceRange.IsPositioned() ||
invisibleWhiteSpaceRange.Collapsed()) {
return EditorDOMPointType(&aTextNode, 0);
}
return EditorDOMPointType(invisibleWhiteSpaceRange.EndRef());
}
EditorDOMPointInText
WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces(
const EditorDOMPointInText& aPointAtASCIIWhiteSpace) const {

Просмотреть файл

@ -329,6 +329,21 @@ class MOZ_STACK_CLASS WSRunScanner final {
return scanner.GetPreviousEditableCharPoint(aPoint);
}
/**
* Scan aTextNode from end or start to find last or first visible things.
* I.e., this returns a point immediately before or after invisible
* white-spaces of aTextNode if aTextNode ends or begins with some invisible
* white-spaces.
* Note that the result may not be in different text node if aTextNode has
* only invisible white-spaces and there is previous or next text node.
*/
template <typename EditorDOMPointType>
static EditorDOMPointType GetAfterLastVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
template <typename EditorDOMPointType>
static EditorDOMPointType GetFirstVisiblePoint(
Text& aTextNode, const Element* aAncestorLimiter);
/**
* GetRangeInTextNodesToForwardDeleteFrom() returns the range to remove
* text when caret is at aPoint.

Просмотреть файл

@ -60,9 +60,6 @@
[Backspace at "<p>ab[\]c</p>"]
expected: FAIL
[Backspace at "<p>abc{<br>}def</p>"]
expected: FAIL
[Backspace at "<p>abc </p><p> [\] def</p>"]
expected: FAIL

Просмотреть файл

@ -1,4 +1,6 @@
[input-events-get-target-ranges-forwarddelete.tentative.html]
max-asserts: 1
min-asserts: 1
[Shift + Delete at "<p>abc [\]def ghi</p>"]
expected:
if (os == "mac") or (os == "win"): FAIL
@ -77,12 +79,18 @@
[Delete at "<p>abc[\] </p><pre> def</pre>"]
expected: FAIL
[Delete at "<p>abc{<br>}def</p>"]
expected: FAIL
[Delete at "<p>abc [</p><p>\] def</p>"]
expected: FAIL
[Delete at "<div><p>abc[\] </p> def</div>"]
expected: FAIL
[Delete at "<p>{}<br></p><p><span contenteditable=\"false\">abc</span>def</p>"]
expected: FAIL
[Delete at "<p>{}<br></p><p contenteditable=\"false\">abc</p><p>def</p>"]
expected: FAIL
[Delete at "<table><tr><td>{}<br></td><td>cell2</td></tr></table>"]
expected: FAIL

Просмотреть файл

@ -19,7 +19,7 @@ let editor = document.querySelector("div[contenteditable]");
let beforeinput = [];
let input = [];
editor.addEventListener("beforeinput", (e) => {
// NOTE: Blink makes `getTargetRanges()` return empty range after propagaion,
// NOTE: Blink makes `getTargetRanges()` return empty range after propagation,
// but this test wants to check the result during propagation.
// Therefore, we need to cache the result, but will assert if
// `getTargetRanges()` returns different ranges after checking the
@ -124,7 +124,7 @@ function checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedRange) {
assert_equals(beforeinput[0].cachedRanges.length, 1,
"beforeinput.getTargetRanges() should return one range within an array");
}
assert_equals(beforeinput[0].cachedRanges, 1,
assert_equals(beforeinput[0].cachedRanges.length, 1,
"One range should be returned from getTargetRanges() when the key operation deletes something");
checkGetTargetRangesKeepReturningSameValue(beforeinput[0]);
}
@ -140,6 +140,11 @@ function checkGetTargetRangesOfInputOnDeleteSomething() {
checkGetTargetRangesKeepReturningSameValue(input[0]);
}
function checkGetTargetRangesOfInputOnDoNothing() {
assert_equals(input.length, 0,
"input event shouldn't be fired when the key operation does not cause modifying the DOM tree");
}
function checkBeforeinputAndInputEventsOnNOOP() {
assert_equals(beforeinput.length, 0,
"beforeinput event shouldn't be fired when the key operation does not cause modifying the DOM tree");
@ -160,7 +165,7 @@ promise_test(async () => {
endContainer: editor.firstChild.firstChild,
endOffset: 1,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>a[]bc</p>"');
// Simply deletes the previous ASCII character of caret position.
@ -176,7 +181,7 @@ promise_test(async () => {
endContainer: editor.firstChild.firstChild,
endOffset: 2,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>ab[]c</p>"');
// Simply deletes the previous ASCII character of caret position.
@ -192,10 +197,10 @@ promise_test(async () => {
endContainer: editor.firstChild.firstChild,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc[]</p>"');
// Should delete the `<span>` element becase it becomes empty.
// Should delete the `<span>` element because it becomes empty.
// However, we need discussion whether the `<span>` element should be
// contained by a range of `getTargetRanges()`.
// https://github.com/w3c/input-events/issues/112
@ -212,10 +217,10 @@ promise_test(async () => {
endContainer: c,
endOffset: 0,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>a<span>b</span>[]c</p>"');
// Should delete the `<span>` element becase it becomes empty.
// Should delete the `<span>` element because it becomes empty.
// However, we need discussion whether the `<span>` element should be
// contained by a range of `getTargetRanges()`.
// https://github.com/w3c/input-events/issues/112
@ -232,7 +237,7 @@ promise_test(async () => {
endContainer: editor.firstChild,
endOffset: 2,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>a<span>b[]</span>c</p>"');
// Invisible leading white-space may be deleted when the first visible
@ -251,7 +256,7 @@ promise_test(async () => {
endContainer: editor.firstChild.firstChild,
endOffset: 2,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p> a[]bc</p>"');
// Invisible leading white-spaces in current block and invisible trailing
@ -275,7 +280,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc </p><p> []def</p>"');
// Invisible leading white-spaces in current block and invisible trailing
@ -299,7 +304,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc </p><p> [] def</p>"');
// Invisible leading white-spaces in current block and invisible trailing
@ -323,7 +328,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc </p><p> [] def</p>"');
// Invisible leading white-spaces in current block and invisible trailing
@ -347,7 +352,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc </p><p>[] def</p>"');
// Invisible leading white-spaces in current block and invisible trailing
@ -371,7 +376,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc [</p><p>] def</p>"');
// Invisible leading white-spaces in the current block should be deleted
@ -398,7 +403,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<pre>abc </pre><p> []def</p>"');
// Invisible leading/trailing white-spaces in the current block should be
@ -426,11 +431,11 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<pre>abc </pre><p> []def </p>"');
// Invisible trailing white-spaces in the first block should be deleted
// when the block is joined with the preformated following block, but
// when the block is joined with the preformatted following block, but
// the leading white-spaces in the preformatted block shouldn't be
// removed. So, in this case, the invisible trailing white-spaces should
// be in the range of `getTargetRanges()`, but not so for the preformatted
@ -455,7 +460,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc </p><pre>[] def</pre>"');
// If the first block has invisible `<br>` element and joining it with
@ -479,7 +484,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<br></p><p>[]def</p>"');
// If the first block has invisible `<br>` element for empty last line and
@ -503,7 +508,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<br><br></p><p>[]def</p>"');
// Deleting visible `<br>` element should be contained by a range of
@ -521,7 +526,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 0,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<br>[]def</p>"');
// Deleting visible `<br>` element should be contained by a range of
@ -540,7 +545,7 @@ promise_test(async () => {
endContainer: editor.firstChild,
endOffset: 2,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc{<br>}def</p>"');
// Joining parent block and child block should remove invisible preceding
@ -563,7 +568,7 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<div>abc <p> []def<br>ghi</p></div>"');
// Joining child block and parent block should remove invisible trailing
@ -585,9 +590,183 @@ promise_test(async () => {
endContainer: def,
endOffset: 3,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<div><p>abc </p> []def</div>"');
// Backspace in empty paragraph should remove the empty paragraph. In this
// case, it should be treated as joining with the previous paragraph.
// The target range should include the invisible <br> element in the empty
// paragraph.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc</p><p><br></p>";
let p1 = editor.querySelector("p");
let abc = p1.firstChild;
let p2 = p1.nextSibling;
selection.collapse(p2, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abc</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: p2,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc</p><p>{}<br></p>');
// Delete ignore the empty span and the other things must be same as the
// previous test.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc</p><p><span></span><br></p>";
let p1 = editor.querySelector("p");
let abc = p1.firstChild;
let p2 = p1.nextSibling;
let span = p2.firstChild;
selection.collapse(span, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abc</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: 3,
endContainer: p2,
endOffset: 2,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc</p><p><span>{}</span><br></p>');
// If invisible white-spaces are removed with same action as above tests,
// the range should be included in the target ranges.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc </p><p><br></p>";
let p1 = editor.querySelector("p");
let abc = p1.firstChild;
let p2 = p1.nextSibling;
selection.collapse(p2, 0);
await sendBackspaceKey();
assert_in_array(editor.innerHTML, ["<p>abc </p>",
"<p>abc</p>"]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: abc,
startOffset: abc.length,
endContainer: p2,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc </p><p>{}<br></p>');
// If the previous block ends with non-editable content, target range
// should be after the non-editable content node.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc<span contenteditable=\"false\">def</span></p><p><br></p>";
let p1 = editor.querySelector("p");
let span = editor.querySelector("span");
let p2 = p1.nextSibling;
selection.collapse(p2, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abc<span contenteditable=\"false\">def</span></p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p1,
startOffset: 2,
endContainer: p2,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc<span contenteditable="false">def</span></p><p>{}<br></p>"');
// If previous non-editable paragraph is deleted, target range should begin
// with end of the text node in the first paragraph. Otherwise, start from
// after the non-editable paragraph.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc</p><p contenteditable=\"false\">def</p><p><br></p>";
let p1 = editor.querySelector("p");
let abc = p1.firstChild;
let p2 = p1.nextSibling;
let p3 = p2.nextSibling;
selection.collapse(p3, 0);
await sendBackspaceKey();
assert_in_array(editor.innerHTML, ["<p>abc</p>",
"<p>abc</p><p contenteditable=\"false\">def</p>"]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p2.isConnected ? editor : abc,
startOffset: p2.isConnected ? 2 : abc.length,
endContainer: p3,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Backspace at "<p>abc</p><p contenteditable=\"false\">def</p><p>{}<br></p>"');
// If just removes the paragraph, target range should start from after the
// table element.
promise_test(async () => {
reset();
editor.innerHTML = "<table><tr><td>cell</td></tr></table><p><br></p>";
let table = editor.querySelector("table");
let p = table.nextSibling;
selection.collapse(p, 0);
await sendBackspaceKey();
assert_in_array(editor.innerHTML, ["<table><tbody><tr><td>cell</td></tr></tbody></table>",
"<table><tbody><tr><td>cell</td></tr></tbody></table><p><br></p>"]);
if (p.isConnected) {
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 0,
endContainer: p,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDoNothing();
} else {
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: editor,
startOffset: 1,
endContainer: p,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}
}, 'Backspace at "<table><tr><td>cell</td></tr></table><p>{}<br></p>"');
// If table cell won't be joined, target range should be collapsed in the
// cell.
promise_test(async () => {
reset();
editor.innerHTML = "<table><tr><td>cell1</td><td><br></td></tr></table>";
let cell1 = editor.querySelector("td");
let cell2 = cell1.nextSibling;
selection.collapse(cell2, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<table><tbody><tr><td>cell1</td><td><br></td></tr></tbody></table>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: cell2,
startOffset: 0,
endContainer: cell2,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDoNothing();
}, 'Backspace at "<table><tr><td>cell1</td><td>{}<br></td></tr></table>"');
// If table caption won't be deleted, target range should be collapsed in the
// caption element.
promise_test(async () => {
reset();
editor.innerHTML = "<p>abc</p><table><caption><br></caption><tr><td>cell</td></tr></table>";
let caption = editor.querySelector("caption");
selection.collapse(caption, 0);
await sendBackspaceKey();
assert_equals(editor.innerHTML, "<p>abc</p><table><caption><br></caption><tbody><tr><td>cell</td></tr></tbody></table>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: caption,
startOffset: 0,
endContainer: caption,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDoNothing();
}, 'Backspace at "<p>abc</p><table><caption>{}<br></caption><tr><td>cell</td></tr></table>"');
// The following tests check whether the range returned from
// `beforeinput[0].getTargetRanges()` is modified or different range is
// modified instead. I.e., they don't test which type of deletion should
@ -625,7 +804,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Shift + Backspace at "<p>abc def[] ghi</p>"');
promise_test(async () => {
@ -649,7 +828,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Control + Backspace at "<p>abc def[] ghi</p>"');
promise_test(async () => {
@ -673,7 +852,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Alt + Backspace at "<p>abc def[] ghi</p>"');
promise_test(async () => {
@ -697,7 +876,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Meta + Backspace at "<p>abc def[] ghi</p>"');
promise_test(async () => {
@ -724,7 +903,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Shift + Backspace at "<p> abc[] def</p>"');
promise_test(async () => {
@ -751,7 +930,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Control + Backspace at "<p> abc[] def</p>"');
promise_test(async () => {
@ -778,7 +957,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Alt + Backspace at "<p> abc[] def</p>"');
promise_test(async () => {
@ -805,7 +984,7 @@ promise_test(async () => {
endContainer: p.firstChild,
endOffset: startOffset + length,
});
checkGetTargetRangesOfInput();
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Meta + Backspace at "<p> abc[] def</p>"');
</script>

Просмотреть файл

@ -19,7 +19,7 @@ let editor = document.querySelector("div[contenteditable]");
let beforeinput = [];
let input = [];
editor.addEventListener("beforeinput", (e) => {
// NOTE: Blink makes `getTargetRanges()` return empty range after propagaion,
// NOTE: Blink makes `getTargetRanges()` return empty range after propagation,
// but this test wants to check the result during propagation.
// Therefore, we need to cache the result, but will assert if
// `getTargetRanges()` returns different ranges after checking the
@ -124,7 +124,7 @@ function checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedRange) {
assert_equals(beforeinput[0].cachedRanges.length, 1,
"beforeinput.getTargetRanges() should return one range within an array");
}
assert_equals(beforeinput[0].cachedRanges, 1,
assert_equals(beforeinput[0].cachedRanges.length, 1,
"One range should be returned from getTargetRanges() when the key operation deletes something");
checkGetTargetRangesKeepReturningSameValue(beforeinput[0]);
}
@ -140,6 +140,11 @@ function checkGetTargetRangesOfInputOnDeleteSomething() {
checkGetTargetRangesKeepReturningSameValue(input[0]);
}
function checkGetTargetRangesOfInputOnDoNothing() {
assert_equals(input.length, 0,
"input event shouldn't be fired when the key operation does not cause modifying the DOM tree");
}
function checkBeforeinputAndInputEventsOnNOOP() {
assert_equals(beforeinput.length, 0,
"beforeinput event shouldn't be fired when the key operation does not cause modifying the DOM tree");
@ -195,7 +200,7 @@ promise_test(async () => {
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>[]abc</p>"');
// Should delete the `<span>` element becase it becomes empty.
// Should delete the `<span>` element because it becomes empty.
// However, we need discussion whether the `<span>` element should be
// contained by a range of `getTargetRanges()`.
// https://github.com/w3c/input-events/issues/112
@ -215,7 +220,7 @@ promise_test(async () => {
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>a[]<span>b</span>c</p>"');
// Should delete the `<span>` element becase it becomes empty.
// Should delete the `<span>` element because it becomes empty.
// However, we need discussion whether the `<span>` element should be
// contained by a range of `getTargetRanges()`.
// https://github.com/w3c/input-events/issues/112
@ -427,7 +432,7 @@ promise_test(async () => {
}, 'Delete at "<pre>abc []</pre><p> def </p>"');
// Invisible trailing white-spaces in the first block should be deleted
// when the block is joined with the preformated following block, but
// when the block is joined with the preformatted following block, but
// the leading white-spaces in the preformatted block shouldn't be
// removed. So, in this case, the invisible trailing white-spaces should
// be in the range of `getTargetRanges()`, but not so for the preformatted
@ -566,7 +571,7 @@ promise_test(async () => {
// Joining child block and parent block should remove invisible trailing
// white-spaces of the child block and invisible following white-spaces
// in the parent block, and they should be contained by a range of
// `getTaregetRanges()`, but maybe needs discussion.
// `getTargetRanges()`, but maybe needs discussion.
// https://github.com/w3c/input-events/issues/112
promise_test(async () => {
reset();
@ -585,6 +590,179 @@ promise_test(async () => {
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<div><p>abc[] </p> def</div>"');
// Delete in empty paragraph should remove the empty paragraph. In this
// case, it should be treated as joining with the previous paragraph.
// The target range should include the invisible <br> element in the empty
// paragraph.
promise_test(async () => {
reset();
editor.innerHTML = "<p><br></p><p>abc</p>";
let p1 = editor.querySelector("p");
let p2 = p1.nextSibling;
let abc = p2.firstChild;
selection.collapse(p1, 0);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abc</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p1,
startOffset: 0,
endContainer: abc,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>{}<br></p><p>abc</p>');
// Delete ignore the empty span and the other things must be same as the
// previous test.
promise_test(async () => {
reset();
editor.innerHTML = "<p><span></span><br></p><p>abc</p>";
let p1 = editor.querySelector("p");
let span = p1.firstChild;
let p2 = p1.nextSibling;
let abc = p2.firstChild;
selection.collapse(span, 0);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p>abc</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p1,
startOffset: 0,
endContainer: abc,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p><span>{}</span><br></p><p>abc</p>');
// If invisible white-spaces are removed with same action as above tests,
// the range should be included in the target ranges.
promise_test(async () => {
reset();
editor.innerHTML = "<p><br></p><p> abc</p>";
let p1 = editor.querySelector("p");
let p2 = p1.nextSibling;
let abc = p2.firstChild;
selection.collapse(p1, 0);
await sendDeleteKey();
assert_in_array(editor.innerHTML, ["<p> abc</p>",
"<p>abc</p>"]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p1,
startOffset: 0,
endContainer: abc,
endOffset: 5 - abc.length,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>{}<br></p><p> abc</p>');
// If the next block begins with non-editable content, target range
// should be at the non-editable content node.
promise_test(async () => {
reset();
editor.innerHTML = "<p><br></p><p><span contenteditable=\"false\">abc</span>def</p>";
let p1 = editor.querySelector("p");
let p2 = p1.nextSibling;
let span = editor.querySelector("span");
selection.collapse(p1, 0);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<p><span contenteditable=\"false\">abc</span>def</p>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p1,
startOffset: 0,
endContainer: p2,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>{}<br></p><p><span contenteditable="false">abc</span>def</p>"');
// If next non-editable paragraph is deleted, target range should end
// with start of the text node in the last paragraph. Otherwise, ends at
// the non-editable paragraph.
promise_test(async () => {
reset();
editor.innerHTML = "<p><br></p><p contenteditable=\"false\">abc</p><p>def</p>";
let p1 = editor.querySelector("p");
let p2 = p1.nextSibling;
let p3 = p2.nextSibling;
let def = p3.firstChild;
selection.collapse(p3, 0);
await sendDeleteKey();
assert_in_array(editor.innerHTML, ["<p>def</p>",
"<p contenteditable=\"false\">abc</p><p>def</p>"]);
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p1,
startOffset: 0,
endContainer: p2.isConnected ? editor : p3,
endOffset: p2.isConnected ? 1 : 0,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}, 'Delete at "<p>{}<br></p><p contenteditable=\"false\">abc</p><p>def</p>"');
// If just removes the paragraph, target range should end at the table element.
promise_test(async () => {
reset();
editor.innerHTML = "<p><br></p><table><tr><td>cell</td></tr></table>";
let cell = editor.querySelector("td");
let p = editor.querySelector("p");
selection.collapse(p, 0);
await sendDeleteKey();
assert_in_array(editor.innerHTML, ["<table><tbody><tr><td>cell</td></tr></tbody></table>",
"<p><br></p><table><tbody><tr><td>cell</td></tr></tbody></table>"]);
if (p.isConnected) {
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 0,
endContainer: p,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDoNothing();
} else {
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: p,
startOffset: 0,
endContainer: editor,
endOffset: 1,
});
checkGetTargetRangesOfInputOnDeleteSomething();
}
}, 'Delete at "<p>{}<br></p><table><tr><td>cell</td></tr></table>"');
// If table cell won't be joined, target range should be collapsed in the
// cell.
promise_test(async () => {
reset();
editor.innerHTML = "<table><tr><td><br></td><td>cell2</td></tr></table>";
let cell1 = editor.querySelector("td");
let cell2 = cell1.nextSibling;
selection.collapse(cell1, 0);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<table><tbody><tr><td><br></td><td>cell2</td></tr></tbody></table>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: cell1,
startOffset: 0,
endContainer: cell1,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDoNothing();
}, 'Delete at "<table><tr><td>{}<br></td><td>cell2</td></tr></table>"');
// If table caption won't be deleted, target range should be collapsed in the
// caption element.
promise_test(async () => {
reset();
editor.innerHTML = "<table><caption><br></caption><tr><td>cell</td></tr></table>";
let caption = editor.querySelector("caption");
selection.collapse(caption, 0);
await sendDeleteKey();
assert_equals(editor.innerHTML, "<table><caption><br></caption><tbody><tr><td>cell</td></tr></tbody></table>");
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
startContainer: caption,
startOffset: 0,
endContainer: caption,
endOffset: 0,
});
checkGetTargetRangesOfInputOnDoNothing();
}, 'Delete at "<table><caption>{}<br></caption><tr><td>cell</td></tr></table>"');
// The following tests check whether the range returned from
// `beforeinput[0].getTargetRanges()` is modified or different range is
// modified instead. I.e., they don't test which type of deletion should