зеркало из https://github.com/mozilla/gecko-dev.git
1196 строки
49 KiB
C++
1196 строки
49 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "AutoRangeArray.h"
|
|
|
|
#include "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
|
|
#include "EditorForwards.h" // for CollectChildrenOptions
|
|
#include "HTMLEditUtils.h" // for HTMLEditUtils
|
|
#include "HTMLEditHelpers.h" // for SplitNodeResult
|
|
#include "WSRunObject.h" // for WSRunScanner
|
|
|
|
#include "mozilla/OwningNonNull.h" // for OwningNonNull
|
|
#include "mozilla/dom/Document.h" // for dom::Document
|
|
#include "mozilla/dom/HTMLBRElement.h" // for dom HTMLBRElement
|
|
#include "mozilla/dom/Selection.h" // for dom::Selection
|
|
#include "mozilla/dom/Text.h" // for dom::Text
|
|
|
|
#include "gfxFontUtils.h" // for gfxFontUtils
|
|
#include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
|
|
#include "nsFrameSelection.h" // for nsFrameSelection
|
|
#include "nsIContent.h" // for nsIContent
|
|
#include "nsINode.h" // for nsINode
|
|
#include "nsRange.h" // for nsRange
|
|
#include "nsTextFragment.h" // for nsTextFragment
|
|
|
|
namespace mozilla {
|
|
|
|
using namespace dom;
|
|
|
|
/******************************************************************************
|
|
* mozilla::AutoRangeArray
|
|
*****************************************************************************/
|
|
|
|
template AutoRangeArray::AutoRangeArray(const EditorDOMRange& aRange);
|
|
template AutoRangeArray::AutoRangeArray(const EditorRawDOMRange& aRange);
|
|
template AutoRangeArray::AutoRangeArray(const EditorDOMPoint& aRange);
|
|
template AutoRangeArray::AutoRangeArray(const EditorRawDOMPoint& aRange);
|
|
|
|
AutoRangeArray::AutoRangeArray(const dom::Selection& aSelection) {
|
|
Initialize(aSelection);
|
|
}
|
|
|
|
AutoRangeArray::AutoRangeArray(const AutoRangeArray& aOther)
|
|
: mAnchorFocusRange(aOther.mAnchorFocusRange),
|
|
mDirection(aOther.mDirection) {
|
|
mRanges.SetCapacity(aOther.mRanges.Length());
|
|
for (const OwningNonNull<nsRange>& range : aOther.mRanges) {
|
|
RefPtr<nsRange> clonedRange = range->CloneRange();
|
|
mRanges.AppendElement(std::move(clonedRange));
|
|
}
|
|
mAnchorFocusRange = aOther.mAnchorFocusRange;
|
|
}
|
|
|
|
template <typename PointType>
|
|
AutoRangeArray::AutoRangeArray(const EditorDOMRangeBase<PointType>& aRange) {
|
|
MOZ_ASSERT(aRange.IsPositionedAndValid());
|
|
RefPtr<nsRange> range = aRange.CreateRange(IgnoreErrors());
|
|
if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
|
|
return;
|
|
}
|
|
mRanges.AppendElement(*range);
|
|
mAnchorFocusRange = std::move(range);
|
|
}
|
|
|
|
template <typename PT, typename CT>
|
|
AutoRangeArray::AutoRangeArray(const EditorDOMPointBase<PT, CT>& aPoint) {
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
RefPtr<nsRange> range = aPoint.CreateCollapsedRange(IgnoreErrors());
|
|
if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
|
|
return;
|
|
}
|
|
mRanges.AppendElement(*range);
|
|
mAnchorFocusRange = std::move(range);
|
|
}
|
|
|
|
AutoRangeArray::~AutoRangeArray() {
|
|
if (mSavedRanges.isSome()) {
|
|
ClearSavedRanges();
|
|
}
|
|
}
|
|
|
|
// static
|
|
bool AutoRangeArray::IsEditableRange(const dom::AbstractRange& aRange,
|
|
const Element& aEditingHost) {
|
|
// TODO: Perhaps, we should check whether the start/end boundaries are
|
|
// first/last point of non-editable element.
|
|
// See https://github.com/w3c/editing/issues/283#issuecomment-788654850
|
|
EditorRawDOMPoint atStart(aRange.StartRef());
|
|
const bool isStartEditable =
|
|
atStart.IsInContentNode() &&
|
|
EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
|
|
EditorUtils::EditorType::HTML) &&
|
|
!HTMLEditUtils::IsNonEditableReplacedContent(
|
|
*atStart.ContainerAs<nsIContent>());
|
|
if (!isStartEditable) {
|
|
return false;
|
|
}
|
|
|
|
if (aRange.GetStartContainer() != aRange.GetEndContainer()) {
|
|
EditorRawDOMPoint atEnd(aRange.EndRef());
|
|
const bool isEndEditable =
|
|
atEnd.IsInContentNode() &&
|
|
EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
|
|
EditorUtils::EditorType::HTML) &&
|
|
!HTMLEditUtils::IsNonEditableReplacedContent(
|
|
*atEnd.ContainerAs<nsIContent>());
|
|
if (!isEndEditable) {
|
|
return false;
|
|
}
|
|
|
|
// Now, both start and end points are editable, but if they are in
|
|
// different editing host, we cannot edit the range.
|
|
if (atStart.ContainerAs<nsIContent>() != atEnd.ContainerAs<nsIContent>() &&
|
|
atStart.ContainerAs<nsIContent>()->GetEditingHost() !=
|
|
atEnd.ContainerAs<nsIContent>()->GetEditingHost()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// HTMLEditor does not support modifying outside `<body>` element for now.
|
|
nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
|
|
return commonAncestor && commonAncestor->IsContent() &&
|
|
commonAncestor->IsInclusiveDescendantOf(&aEditingHost);
|
|
}
|
|
|
|
void AutoRangeArray::EnsureOnlyEditableRanges(const Element& aEditingHost) {
|
|
for (size_t i = mRanges.Length(); i > 0; i--) {
|
|
const OwningNonNull<nsRange>& range = mRanges[i - 1];
|
|
if (!AutoRangeArray::IsEditableRange(range, aEditingHost)) {
|
|
mRanges.RemoveElementAt(i - 1);
|
|
continue;
|
|
}
|
|
// Special handling for `inert` attribute. If anchor node is inert, the
|
|
// range should be treated as not editable.
|
|
nsIContent* anchorContent =
|
|
mDirection == eDirNext
|
|
? nsIContent::FromNode(range->GetStartContainer())
|
|
: nsIContent::FromNode(range->GetEndContainer());
|
|
if (anchorContent && HTMLEditUtils::ContentIsInert(*anchorContent)) {
|
|
mRanges.RemoveElementAt(i - 1);
|
|
continue;
|
|
}
|
|
// Additionally, if focus node is inert, the range should be collapsed to
|
|
// anchor node.
|
|
nsIContent* focusContent =
|
|
mDirection == eDirNext
|
|
? nsIContent::FromNode(range->GetEndContainer())
|
|
: nsIContent::FromNode(range->GetStartContainer());
|
|
if (focusContent && focusContent != anchorContent &&
|
|
HTMLEditUtils::ContentIsInert(*focusContent)) {
|
|
range->Collapse(mDirection == eDirNext);
|
|
}
|
|
}
|
|
mAnchorFocusRange = mRanges.IsEmpty() ? nullptr : mRanges.LastElement().get();
|
|
}
|
|
|
|
void AutoRangeArray::EnsureRangesInTextNode(const Text& aTextNode) {
|
|
auto GetOffsetInTextNode = [&aTextNode](const nsINode* aNode,
|
|
uint32_t aOffset) -> uint32_t {
|
|
MOZ_DIAGNOSTIC_ASSERT(aNode);
|
|
if (aNode == &aTextNode) {
|
|
return aOffset;
|
|
}
|
|
const nsIContent* anonymousDivElement = aTextNode.GetParent();
|
|
MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement);
|
|
MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->IsHTMLElement(nsGkAtoms::div));
|
|
MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->GetFirstChild() == &aTextNode);
|
|
if (aNode == anonymousDivElement && aOffset == 0u) {
|
|
return 0u; // Point before the text node so that use start of the text.
|
|
}
|
|
MOZ_DIAGNOSTIC_ASSERT(aNode->IsInclusiveDescendantOf(anonymousDivElement));
|
|
// Point after the text node so that use end of the text.
|
|
return aTextNode.TextDataLength();
|
|
};
|
|
for (uint32_t i : IntegerRange(mRanges.Length())) {
|
|
const OwningNonNull<nsRange>& range = mRanges[i];
|
|
if (MOZ_LIKELY(range->GetStartContainer() == &aTextNode &&
|
|
range->GetEndContainer() == &aTextNode)) {
|
|
continue;
|
|
}
|
|
range->SetStartAndEnd(
|
|
const_cast<Text*>(&aTextNode),
|
|
GetOffsetInTextNode(range->GetStartContainer(), range->StartOffset()),
|
|
const_cast<Text*>(&aTextNode),
|
|
GetOffsetInTextNode(range->GetEndContainer(), range->EndOffset()));
|
|
}
|
|
|
|
if (MOZ_UNLIKELY(mRanges.Length() >= 2)) {
|
|
// For avoiding to handle same things in same range, we should drop and
|
|
// merge unnecessary ranges. Note that the ranges never overlap
|
|
// because selection ranges are not allowed it so that we need to check only
|
|
// end offset vs start offset of next one.
|
|
for (uint32_t i : Reversed(IntegerRange(mRanges.Length() - 1u))) {
|
|
MOZ_ASSERT(mRanges[i]->EndOffset() < mRanges[i + 1]->StartOffset());
|
|
// XXX Should we delete collapsed range unless the index is 0? Without
|
|
// Selection API, such situation cannot happen so that `TextEditor`
|
|
// may behave unexpectedly.
|
|
if (MOZ_UNLIKELY(mRanges[i]->EndOffset() >=
|
|
mRanges[i + 1]->StartOffset())) {
|
|
const uint32_t newEndOffset = mRanges[i + 1]->EndOffset();
|
|
mRanges.RemoveElementAt(i + 1);
|
|
if (MOZ_UNLIKELY(NS_WARN_IF(newEndOffset > mRanges[i]->EndOffset()))) {
|
|
// So, this case shouldn't happen.
|
|
mRanges[i]->SetStartAndEnd(
|
|
const_cast<Text*>(&aTextNode), mRanges[i]->StartOffset(),
|
|
const_cast<Text*>(&aTextNode), newEndOffset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Result<nsIEditor::EDirection, nsresult>
|
|
AutoRangeArray::ExtendAnchorFocusRangeFor(
|
|
const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount) {
|
|
MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable());
|
|
MOZ_ASSERT(mAnchorFocusRange);
|
|
MOZ_ASSERT(mAnchorFocusRange->IsPositioned());
|
|
MOZ_ASSERT(mAnchorFocusRange->StartRef().IsSet());
|
|
MOZ_ASSERT(mAnchorFocusRange->EndRef().IsSet());
|
|
|
|
if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
|
|
aDirectionAndAmount, *this)) {
|
|
return aDirectionAndAmount;
|
|
}
|
|
|
|
if (NS_WARN_IF(!aEditorBase.SelectionRef().RangeCount())) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
// By a preceding call of EnsureOnlyEditableRanges(), anchor/focus range may
|
|
// have been changed. In that case, we cannot use nsFrameSelection anymore.
|
|
// FIXME: We should make `nsFrameSelection::CreateRangeExtendedToSomewhere`
|
|
// work without `Selection` instance.
|
|
if (MOZ_UNLIKELY(
|
|
aEditorBase.SelectionRef().GetAnchorFocusRange()->StartRef() !=
|
|
mAnchorFocusRange->StartRef() ||
|
|
aEditorBase.SelectionRef().GetAnchorFocusRange()->EndRef() !=
|
|
mAnchorFocusRange->EndRef())) {
|
|
return aDirectionAndAmount;
|
|
}
|
|
|
|
RefPtr<nsFrameSelection> frameSelection =
|
|
aEditorBase.SelectionRef().GetFrameSelection();
|
|
if (NS_WARN_IF(!frameSelection)) {
|
|
return Err(NS_ERROR_NOT_INITIALIZED);
|
|
}
|
|
|
|
RefPtr<Element> editingHost;
|
|
if (aEditorBase.IsHTMLEditor()) {
|
|
editingHost = aEditorBase.AsHTMLEditor()->ComputeEditingHost();
|
|
if (!editingHost) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
}
|
|
|
|
Result<RefPtr<nsRange>, nsresult> result(NS_ERROR_UNEXPECTED);
|
|
nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount;
|
|
switch (aDirectionAndAmount) {
|
|
case nsIEditor::eNextWord:
|
|
result = frameSelection->CreateRangeExtendedToNextWordBoundary<nsRange>();
|
|
if (NS_WARN_IF(aEditorBase.Destroyed())) {
|
|
return Err(NS_ERROR_EDITOR_DESTROYED);
|
|
}
|
|
NS_WARNING_ASSERTION(
|
|
result.isOk(),
|
|
"nsFrameSelection::CreateRangeExtendedToNextWordBoundary() failed");
|
|
// DeleteSelectionWithTransaction() doesn't handle these actions
|
|
// because it's inside batching, so don't confuse it:
|
|
directionAndAmountResult = nsIEditor::eNone;
|
|
break;
|
|
case nsIEditor::ePreviousWord:
|
|
result =
|
|
frameSelection->CreateRangeExtendedToPreviousWordBoundary<nsRange>();
|
|
if (NS_WARN_IF(aEditorBase.Destroyed())) {
|
|
return Err(NS_ERROR_EDITOR_DESTROYED);
|
|
}
|
|
NS_WARNING_ASSERTION(
|
|
result.isOk(),
|
|
"nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() "
|
|
"failed");
|
|
// DeleteSelectionWithTransaction() doesn't handle these actions
|
|
// because it's inside batching, so don't confuse it:
|
|
directionAndAmountResult = nsIEditor::eNone;
|
|
break;
|
|
case nsIEditor::eNext:
|
|
result =
|
|
frameSelection
|
|
->CreateRangeExtendedToNextGraphemeClusterBoundary<nsRange>();
|
|
if (NS_WARN_IF(aEditorBase.Destroyed())) {
|
|
return Err(NS_ERROR_EDITOR_DESTROYED);
|
|
}
|
|
NS_WARNING_ASSERTION(result.isOk(),
|
|
"nsFrameSelection::"
|
|
"CreateRangeExtendedToNextGraphemeClusterBoundary() "
|
|
"failed");
|
|
// Don't set directionAndAmount to eNone (see Bug 502259)
|
|
break;
|
|
case nsIEditor::ePrevious: {
|
|
// Only extend the selection where the selection is after a UTF-16
|
|
// surrogate pair or a variation selector.
|
|
// For other cases we don't want to do that, in order
|
|
// to make sure that pressing backspace will only delete the last
|
|
// typed character.
|
|
// XXX This is odd if the previous one is a sequence for a grapheme
|
|
// cluster.
|
|
const auto atStartOfSelection = GetFirstRangeStartPoint<EditorDOMPoint>();
|
|
if (MOZ_UNLIKELY(NS_WARN_IF(!atStartOfSelection.IsSet()))) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
// node might be anonymous DIV, so we find better text node
|
|
const EditorDOMPoint insertionPoint =
|
|
aEditorBase.FindBetterInsertionPoint(atStartOfSelection);
|
|
if (MOZ_UNLIKELY(!insertionPoint.IsSet())) {
|
|
NS_WARNING(
|
|
"EditorBase::FindBetterInsertionPoint() failed, but ignored");
|
|
return aDirectionAndAmount;
|
|
}
|
|
|
|
if (!insertionPoint.IsInTextNode()) {
|
|
return aDirectionAndAmount;
|
|
}
|
|
|
|
const nsTextFragment* data =
|
|
&insertionPoint.ContainerAs<Text>()->TextFragment();
|
|
uint32_t offset = insertionPoint.Offset();
|
|
if (!(offset > 1 &&
|
|
data->IsLowSurrogateFollowingHighSurrogateAt(offset - 1)) &&
|
|
!(offset > 0 &&
|
|
gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) {
|
|
return aDirectionAndAmount;
|
|
}
|
|
// Different from the `eNext` case, we look for character boundary.
|
|
// I'm not sure whether this inconsistency between "Delete" and
|
|
// "Backspace" is intentional or not.
|
|
result = frameSelection
|
|
->CreateRangeExtendedToPreviousCharacterBoundary<nsRange>();
|
|
if (NS_WARN_IF(aEditorBase.Destroyed())) {
|
|
return Err(NS_ERROR_EDITOR_DESTROYED);
|
|
}
|
|
NS_WARNING_ASSERTION(
|
|
result.isOk(),
|
|
"nsFrameSelection::"
|
|
"CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed");
|
|
break;
|
|
}
|
|
case nsIEditor::eToBeginningOfLine:
|
|
result =
|
|
frameSelection->CreateRangeExtendedToPreviousHardLineBreak<nsRange>();
|
|
if (NS_WARN_IF(aEditorBase.Destroyed())) {
|
|
return Err(NS_ERROR_EDITOR_DESTROYED);
|
|
}
|
|
NS_WARNING_ASSERTION(
|
|
result.isOk(),
|
|
"nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() "
|
|
"failed");
|
|
directionAndAmountResult = nsIEditor::eNone;
|
|
break;
|
|
case nsIEditor::eToEndOfLine:
|
|
result =
|
|
frameSelection->CreateRangeExtendedToNextHardLineBreak<nsRange>();
|
|
if (NS_WARN_IF(aEditorBase.Destroyed())) {
|
|
return Err(NS_ERROR_EDITOR_DESTROYED);
|
|
}
|
|
NS_WARNING_ASSERTION(
|
|
result.isOk(),
|
|
"nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed");
|
|
directionAndAmountResult = nsIEditor::eNext;
|
|
break;
|
|
default:
|
|
return aDirectionAndAmount;
|
|
}
|
|
|
|
if (result.isErr()) {
|
|
return Err(result.inspectErr());
|
|
}
|
|
RefPtr<nsRange> extendedRange(result.unwrap().forget());
|
|
if (!extendedRange || NS_WARN_IF(!extendedRange->IsPositioned())) {
|
|
NS_WARNING("Failed to extend the range, but ignored");
|
|
return directionAndAmountResult;
|
|
}
|
|
|
|
// If the new range isn't editable, keep using the original range.
|
|
if (aEditorBase.IsHTMLEditor() &&
|
|
!AutoRangeArray::IsEditableRange(*extendedRange, *editingHost)) {
|
|
return aDirectionAndAmount;
|
|
}
|
|
|
|
if (NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
|
|
extendedRange->GetStartContainer())) ||
|
|
NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
|
|
extendedRange->GetEndContainer()))) {
|
|
NS_WARNING("A range was extended to outer of selection limiter");
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
// Swap focus/anchor range with the extended range.
|
|
DebugOnly<bool> found = false;
|
|
for (OwningNonNull<nsRange>& range : mRanges) {
|
|
if (range == mAnchorFocusRange) {
|
|
range = *extendedRange;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
MOZ_ASSERT(found);
|
|
mAnchorFocusRange.swap(extendedRange);
|
|
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->IsInAnySelection(),
|
|
"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;
|
|
}
|
|
|
|
bool AutoRangeArray::SaveAndTrackRanges(HTMLEditor& aHTMLEditor) {
|
|
if (mSavedRanges.isSome()) {
|
|
return false;
|
|
}
|
|
mSavedRanges.emplace(*this);
|
|
aHTMLEditor.RangeUpdaterRef().RegisterSelectionState(mSavedRanges.ref());
|
|
mTrackingHTMLEditor = &aHTMLEditor;
|
|
return true;
|
|
}
|
|
|
|
void AutoRangeArray::ClearSavedRanges() {
|
|
if (mSavedRanges.isNothing()) {
|
|
return;
|
|
}
|
|
OwningNonNull<HTMLEditor> htmlEditor(std::move(mTrackingHTMLEditor));
|
|
MOZ_ASSERT(!mTrackingHTMLEditor);
|
|
htmlEditor->RangeUpdaterRef().DropSelectionState(mSavedRanges.ref());
|
|
mSavedRanges.reset();
|
|
}
|
|
|
|
// static
|
|
void AutoRangeArray::
|
|
UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
|
|
EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint,
|
|
const Element& aEditingHost) {
|
|
// FYI: This was moved from
|
|
// https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6743
|
|
|
|
// MOOSE major hack:
|
|
// The GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() and
|
|
// GetPointAfterFollowingLineBreakOrAtFollowingBlock() don't really do the
|
|
// right thing for collapsed ranges inside block elements that contain nothing
|
|
// but a solo <br>. It's easier/ to put a workaround here than to revamp
|
|
// them. :-(
|
|
if (aStartPoint != aEndPoint) {
|
|
return;
|
|
}
|
|
|
|
if (!aStartPoint.IsInContentNode()) {
|
|
return;
|
|
}
|
|
|
|
// XXX Perhaps, this should be more careful. This may not select only one
|
|
// node because this just check whether the block is empty or not,
|
|
// and may not select in non-editable block. However, for inline
|
|
// editing host case, it's right to look for block element without
|
|
// editable state check. Now, this method is used for preparation for
|
|
// other things. So, cannot write test for this method behavior.
|
|
// So, perhaps, we should get rid of this method and each caller should
|
|
// handle its job better.
|
|
Element* const maybeNonEditableBlockElement =
|
|
HTMLEditUtils::GetInclusiveAncestorElement(
|
|
*aStartPoint.ContainerAs<nsIContent>(),
|
|
HTMLEditUtils::ClosestBlockElement);
|
|
if (!maybeNonEditableBlockElement) {
|
|
return;
|
|
}
|
|
|
|
// Make sure we don't go higher than our root element in the content tree
|
|
if (aEditingHost.IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
|
|
return;
|
|
}
|
|
|
|
if (HTMLEditUtils::IsEmptyNode(*maybeNonEditableBlockElement)) {
|
|
aStartPoint.Set(maybeNonEditableBlockElement, 0u);
|
|
aEndPoint.SetToEndOf(maybeNonEditableBlockElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the point before the line containing aPointInLine.
|
|
*
|
|
* @return If the line starts after a `<br>` element, returns next
|
|
* sibling of the `<br>` element.
|
|
* If the line is first line of a block, returns point of
|
|
* the block.
|
|
* NOTE: The result may be point of editing host. I.e., the container may be
|
|
* outside of editing host.
|
|
*/
|
|
static EditorDOMPoint
|
|
GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock(
|
|
const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
|
|
const Element& aEditingHost) {
|
|
// FYI: This was moved from
|
|
// https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6447
|
|
|
|
if (NS_WARN_IF(!aPointInLine.IsSet())) {
|
|
return EditorDOMPoint();
|
|
}
|
|
|
|
EditorDOMPoint point(aPointInLine);
|
|
// Start scanning from the container node if aPoint is in a text node.
|
|
// XXX Perhaps, IsInDataNode() must be expected.
|
|
if (point.IsInTextNode()) {
|
|
if (!point.GetContainer()->GetParentNode()) {
|
|
// Okay, can't promote any further
|
|
// XXX Why don't we return start of the text node?
|
|
return point;
|
|
}
|
|
// If there is a preformatted linefeed in the text node, let's return
|
|
// the point after it.
|
|
EditorDOMPoint atLastPreformattedNewLine =
|
|
HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
|
|
point);
|
|
if (atLastPreformattedNewLine.IsSet()) {
|
|
return atLastPreformattedNewLine.NextPoint();
|
|
}
|
|
point.Set(point.GetContainer());
|
|
}
|
|
|
|
// Look back through any further inline nodes that aren't across a <br>
|
|
// from us, and that are enclosed in the same block.
|
|
// I.e., looking for start of current hard line.
|
|
constexpr HTMLEditUtils::WalkTreeOptions
|
|
ignoreNonEditableNodeAndStopAtBlockBoundary{
|
|
HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
|
|
HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
|
|
for (nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
|
|
previousEditableContent && previousEditableContent->GetParentNode() &&
|
|
!HTMLEditUtils::IsVisibleBRElement(*previousEditableContent) &&
|
|
!HTMLEditUtils::IsBlockElement(*previousEditableContent);
|
|
previousEditableContent = HTMLEditUtils::GetPreviousContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
|
|
EditorDOMPoint atLastPreformattedNewLine =
|
|
HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
|
|
EditorRawDOMPoint::AtEndOf(*previousEditableContent));
|
|
if (atLastPreformattedNewLine.IsSet()) {
|
|
return atLastPreformattedNewLine.NextPoint();
|
|
}
|
|
point.Set(previousEditableContent);
|
|
}
|
|
|
|
// Finding the real start for this point unless current line starts after
|
|
// <br> element. Look up the tree for as long as we are the first node in
|
|
// the container (typically, start of nearest block ancestor), and as long
|
|
// as we haven't hit the body node.
|
|
for (nsIContent* nearContent = HTMLEditUtils::GetPreviousContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
|
|
!nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
|
|
point.GetContainerParent();
|
|
nearContent = HTMLEditUtils::GetPreviousContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
|
|
// Don't keep looking up if we have found a blockquote element to act on
|
|
// when we handle outdent.
|
|
// XXX Sounds like this is hacky. If possible, it should be check in
|
|
// outdent handler for consistency between edit sub-actions.
|
|
// We should check Chromium's behavior of outdent when Selection
|
|
// starts from `<blockquote>` and starts from first child of
|
|
// `<blockquote>`.
|
|
if (aEditSubAction == EditSubAction::eOutdent &&
|
|
point.IsContainerHTMLElement(nsGkAtoms::blockquote)) {
|
|
break;
|
|
}
|
|
|
|
// Don't walk past the editable section. Note that we need to check
|
|
// before walking up to a parent because we need to return the parent
|
|
// object, so the parent itself might not be in the editable area, but
|
|
// it's OK if we're not performing a block-level action.
|
|
bool blockLevelAction =
|
|
aEditSubAction == EditSubAction::eIndent ||
|
|
aEditSubAction == EditSubAction::eOutdent ||
|
|
aEditSubAction == EditSubAction::eSetOrClearAlignment ||
|
|
aEditSubAction == EditSubAction::eCreateOrRemoveBlock;
|
|
// XXX So, does this check whether the container is removable or not? It
|
|
// seems that here can be rewritten as obviously what here tries to
|
|
// check.
|
|
if (!point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost) &&
|
|
(blockLevelAction ||
|
|
!point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost))) {
|
|
break;
|
|
}
|
|
|
|
point.Set(point.GetContainer());
|
|
}
|
|
return point;
|
|
}
|
|
|
|
/**
|
|
* Get the point after the following line break or the block which breaks the
|
|
* line containing aPointInLine.
|
|
*
|
|
* @return If the line ends with a visible `<br>` element, returns
|
|
* the point after the `<br>` element.
|
|
* If the line ends with a preformatted linefeed, returns
|
|
* the point after the linefeed unless it's an invisible
|
|
* line break immediately before a block boundary.
|
|
* If the line ends with a block boundary, returns the
|
|
* point of the block.
|
|
*/
|
|
static EditorDOMPoint GetPointAfterFollowingLineBreakOrAtFollowingBlock(
|
|
const EditorDOMPoint& aPointInLine, const Element& aEditingHost) {
|
|
// FYI: This was moved from
|
|
// https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6541
|
|
|
|
if (NS_WARN_IF(!aPointInLine.IsSet())) {
|
|
return EditorDOMPoint();
|
|
}
|
|
|
|
EditorDOMPoint point(aPointInLine);
|
|
// Start scanning from the container node if aPoint is in a text node.
|
|
// XXX Perhaps, IsInDataNode() must be expected.
|
|
if (point.IsInTextNode()) {
|
|
if (NS_WARN_IF(!point.GetContainer()->GetParentNode())) {
|
|
// Okay, can't promote any further
|
|
// XXX Why don't we return end of the text node?
|
|
return point;
|
|
}
|
|
EditorDOMPoint atNextPreformattedNewLine =
|
|
HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
|
|
EditorDOMPoint>(point);
|
|
if (atNextPreformattedNewLine.IsSet()) {
|
|
// If the linefeed is last character of the text node, it may be
|
|
// invisible if it's immediately before a block boundary. In such
|
|
// case, we should retrun the block boundary.
|
|
Element* maybeNonEditableBlockElement = nullptr;
|
|
if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
|
|
atNextPreformattedNewLine, &maybeNonEditableBlockElement) &&
|
|
maybeNonEditableBlockElement) {
|
|
// If the block is a parent of the editing host, let's return end
|
|
// of editing host.
|
|
if (maybeNonEditableBlockElement == &aEditingHost ||
|
|
!maybeNonEditableBlockElement->IsInclusiveDescendantOf(
|
|
&aEditingHost)) {
|
|
return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
|
|
}
|
|
// If it's invisible because of parent block boundary, return end
|
|
// of the block. Otherwise, i.e., it's followed by a child block,
|
|
// returns the point of the child block.
|
|
if (atNextPreformattedNewLine.ContainerAs<Text>()
|
|
->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
|
|
return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
|
|
}
|
|
return EditorDOMPoint(maybeNonEditableBlockElement);
|
|
}
|
|
// Otherwise, return the point after the preformatted linefeed.
|
|
return atNextPreformattedNewLine.NextPoint();
|
|
}
|
|
// want to be after the text node
|
|
point.SetAfter(point.GetContainer());
|
|
NS_WARNING_ASSERTION(point.IsSet(), "Failed to set to after the text node");
|
|
}
|
|
|
|
// Look ahead through any further inline nodes that aren't across a <br> from
|
|
// us, and that are enclosed in the same block.
|
|
// XXX Currently, we stop block-extending when finding visible <br> element.
|
|
// This might be different from "block-extend" of execCommand spec.
|
|
// However, the spec is really unclear.
|
|
// XXX Probably, scanning only editable nodes is wrong for
|
|
// EditSubAction::eCreateOrRemoveBlock because it might be better to wrap
|
|
// existing inline elements even if it's non-editable. For example,
|
|
// following examples with insertParagraph causes different result:
|
|
// * <div contenteditable>foo[]<b contenteditable="false">bar</b></div>
|
|
// * <div contenteditable>foo[]<b>bar</b></div>
|
|
// * <div contenteditable>foo[]<b contenteditable="false">bar</b>baz</div>
|
|
// Only in the first case, after the caret position isn't wrapped with
|
|
// new <div> element.
|
|
constexpr HTMLEditUtils::WalkTreeOptions
|
|
ignoreNonEditableNodeAndStopAtBlockBoundary{
|
|
HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
|
|
HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
|
|
for (nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
|
|
nextEditableContent &&
|
|
!HTMLEditUtils::IsBlockElement(*nextEditableContent) &&
|
|
nextEditableContent->GetParent();
|
|
nextEditableContent = HTMLEditUtils::GetNextContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
|
|
EditorDOMPoint atFirstPreformattedNewLine =
|
|
HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
|
|
EditorDOMPoint>(EditorRawDOMPoint(nextEditableContent, 0));
|
|
if (atFirstPreformattedNewLine.IsSet()) {
|
|
// If the linefeed is last character of the text node, it may be
|
|
// invisible if it's immediately before a block boundary. In such
|
|
// case, we should retrun the block boundary.
|
|
Element* maybeNonEditableBlockElement = nullptr;
|
|
if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
|
|
atFirstPreformattedNewLine, &maybeNonEditableBlockElement) &&
|
|
maybeNonEditableBlockElement) {
|
|
// If the block is a parent of the editing host, let's return end
|
|
// of editing host.
|
|
if (maybeNonEditableBlockElement == &aEditingHost ||
|
|
!maybeNonEditableBlockElement->IsInclusiveDescendantOf(
|
|
&aEditingHost)) {
|
|
return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
|
|
}
|
|
// If it's invisible because of parent block boundary, return end
|
|
// of the block. Otherwise, i.e., it's followed by a child block,
|
|
// returns the point of the child block.
|
|
if (atFirstPreformattedNewLine.ContainerAs<Text>()
|
|
->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
|
|
return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
|
|
}
|
|
return EditorDOMPoint(maybeNonEditableBlockElement);
|
|
}
|
|
// Otherwise, return the point after the preformatted linefeed.
|
|
return atFirstPreformattedNewLine.NextPoint();
|
|
}
|
|
point.SetAfter(nextEditableContent);
|
|
if (NS_WARN_IF(!point.IsSet())) {
|
|
break;
|
|
}
|
|
if (HTMLEditUtils::IsVisibleBRElement(*nextEditableContent)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Finding the real end for this point unless current line ends with a <br>
|
|
// element. Look up the tree for as long as we are the last node in the
|
|
// container (typically, block node), and as long as we haven't hit the body
|
|
// node.
|
|
for (nsIContent* nearContent = HTMLEditUtils::GetNextContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
|
|
!nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
|
|
point.GetContainerParent();
|
|
nearContent = HTMLEditUtils::GetNextContent(
|
|
point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
|
|
// Don't walk past the editable section. Note that we need to check before
|
|
// walking up to a parent because we need to return the parent object, so
|
|
// the parent itself might not be in the editable area, but it's OK.
|
|
// XXX Maybe returning parent of editing host is really error prone since
|
|
// everybody need to check whether the end point is in editing host
|
|
// when they touch there.
|
|
if (!point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost) &&
|
|
!point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost)) {
|
|
break;
|
|
}
|
|
|
|
point.SetAfter(point.GetContainer());
|
|
if (NS_WARN_IF(!point.IsSet())) {
|
|
break;
|
|
}
|
|
}
|
|
return point;
|
|
}
|
|
|
|
void AutoRangeArray::ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
|
|
EditSubAction aEditSubAction, const Element& aEditingHost) {
|
|
// FYI: This is originated in
|
|
// https://searchfox.org/mozilla-central/rev/1739f1301d658c9bff544a0a095ab11fca2e549d/editor/libeditor/HTMLEditSubActionHandler.cpp#6712
|
|
|
|
bool removeSomeRanges = false;
|
|
for (OwningNonNull<nsRange>& range : mRanges) {
|
|
// Remove non-positioned ranges.
|
|
if (MOZ_UNLIKELY(!range->IsPositioned())) {
|
|
removeSomeRanges = true;
|
|
continue;
|
|
}
|
|
// If the range is native anonymous subtrees, we must meet a bug of
|
|
// `Selection` so that we need to hack here.
|
|
if (MOZ_UNLIKELY(range->GetStartContainer()->IsInNativeAnonymousSubtree() ||
|
|
range->GetEndContainer()->IsInNativeAnonymousSubtree())) {
|
|
EditorRawDOMRange rawRange(range);
|
|
if (!rawRange.EnsureNotInNativeAnonymousSubtree()) {
|
|
range->Reset();
|
|
removeSomeRanges = true;
|
|
continue;
|
|
}
|
|
if (NS_FAILED(
|
|
range->SetStartAndEnd(rawRange.StartRef().ToRawRangeBoundary(),
|
|
rawRange.EndRef().ToRawRangeBoundary())) ||
|
|
MOZ_UNLIKELY(!range->IsPositioned())) {
|
|
range->Reset();
|
|
removeSomeRanges = true;
|
|
continue;
|
|
}
|
|
}
|
|
// Finally, extend the range.
|
|
if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
|
|
range, aEditSubAction, aEditingHost))) {
|
|
// If we failed to extend the range, we should use the original range
|
|
// as-is unless the range is broken at setting the range.
|
|
if (NS_WARN_IF(!range->IsPositioned())) {
|
|
removeSomeRanges = true;
|
|
}
|
|
}
|
|
}
|
|
if (removeSomeRanges) {
|
|
for (size_t i : Reversed(IntegerRange(mRanges.Length()))) {
|
|
if (!mRanges[i]->IsPositioned()) {
|
|
mRanges.RemoveElementAt(i);
|
|
}
|
|
}
|
|
if (!mAnchorFocusRange || !mAnchorFocusRange->IsPositioned()) {
|
|
if (mRanges.IsEmpty()) {
|
|
mAnchorFocusRange = nullptr;
|
|
} else {
|
|
mAnchorFocusRange = mRanges.LastElement();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// static
|
|
nsresult AutoRangeArray::ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
|
|
nsRange& aRange, EditSubAction aEditSubAction,
|
|
const Element& aEditingHost) {
|
|
MOZ_DIAGNOSTIC_ASSERT(
|
|
!EditorRawDOMPoint(aRange.StartRef()).IsInNativeAnonymousSubtree());
|
|
MOZ_DIAGNOSTIC_ASSERT(
|
|
!EditorRawDOMPoint(aRange.EndRef()).IsInNativeAnonymousSubtree());
|
|
|
|
if (NS_WARN_IF(!aRange.IsPositioned())) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
EditorDOMPoint startPoint(aRange.StartRef()), endPoint(aRange.EndRef());
|
|
AutoRangeArray::UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
|
|
startPoint, endPoint, aEditingHost);
|
|
|
|
// Make a new adjusted range to represent the appropriate block content.
|
|
// This is tricky. The basic idea is to push out the range endpoints to
|
|
// truly enclose the blocks that we will affect.
|
|
|
|
// Make sure that the new range ends up to be in the editable section.
|
|
// XXX Looks like that this check wastes the time. Perhaps, we should
|
|
// implement a method which checks both two DOM points in the editor
|
|
// root.
|
|
|
|
startPoint = GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock(
|
|
startPoint, aEditSubAction, aEditingHost);
|
|
// XXX GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() may
|
|
// return point of editing host. Perhaps, we should change it and stop
|
|
// checking it here since this check may be expensive.
|
|
// XXX If the container is an element in the editing host but it points end of
|
|
// the container, this returns nullptr. Is it intentional?
|
|
if (!startPoint.GetChildOrContainerIfDataNode() ||
|
|
!startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
|
|
&aEditingHost)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
endPoint =
|
|
GetPointAfterFollowingLineBreakOrAtFollowingBlock(endPoint, aEditingHost);
|
|
const EditorDOMPoint lastRawPoint =
|
|
endPoint.IsStartOfContainer() ? endPoint : endPoint.PreviousPoint();
|
|
// XXX GetPointAfterFollowingLineBreakOrAtFollowingBlock() may return point of
|
|
// editing host. Perhaps, we should change it and stop checking it here
|
|
// since this check may be expensive.
|
|
// XXX If the container is an element in the editing host but it points end of
|
|
// the container, this returns nullptr. Is it intentional?
|
|
if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
|
|
!lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
|
|
&aEditingHost)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsresult rv = aRange.SetStartAndEnd(startPoint.ToRawRangeBoundary(),
|
|
endPoint.ToRawRangeBoundary());
|
|
if (NS_FAILED(rv)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
Result<EditorDOMPoint, nsresult>
|
|
AutoRangeArray::SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
|
|
HTMLEditor& aHTMLEditor, const Element& aEditingHost,
|
|
const nsIContent* aAncestorLimiter /* = nullptr */) {
|
|
// FYI: The following code is originated in
|
|
// https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#6971
|
|
|
|
// Split text nodes. This is necessary, since given ranges may end in text
|
|
// nodes in case where part of a pre-formatted elements needs to be moved.
|
|
EditorDOMPoint pointToPutCaret;
|
|
IgnoredErrorResult ignoredError;
|
|
for (const OwningNonNull<nsRange>& range : mRanges) {
|
|
EditorDOMPoint atEnd(range->EndRef());
|
|
if (NS_WARN_IF(!atEnd.IsSet()) || !atEnd.IsInTextNode() ||
|
|
atEnd.GetContainer() == aAncestorLimiter) {
|
|
continue;
|
|
}
|
|
|
|
if (!atEnd.IsStartOfContainer() && !atEnd.IsEndOfContainer()) {
|
|
// Split the text node.
|
|
Result<SplitNodeResult, nsresult> splitAtEndResult =
|
|
aHTMLEditor.SplitNodeWithTransaction(atEnd);
|
|
if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
|
|
NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
|
|
return splitAtEndResult.propagateErr();
|
|
}
|
|
SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
|
|
unwrappedSplitAtEndResult.MoveCaretPointTo(
|
|
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
|
|
|
|
// Correct the range.
|
|
// The new end parent becomes the parent node of the text.
|
|
MOZ_ASSERT(!range->IsInAnySelection());
|
|
range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
|
|
.ToRawRangeBoundary(),
|
|
ignoredError);
|
|
NS_WARNING_ASSERTION(!ignoredError.Failed(),
|
|
"nsRange::SetEnd() failed, but ignored");
|
|
ignoredError.SuppressException();
|
|
}
|
|
}
|
|
|
|
// FYI: The following code is originated in
|
|
// https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#7023
|
|
AutoTArray<OwningNonNull<RangeItem>, 8> rangeItemArray;
|
|
rangeItemArray.AppendElements(mRanges.Length());
|
|
|
|
// First register ranges for special editor gravity
|
|
Maybe<size_t> anchorFocusRangeIndex;
|
|
for (size_t index : IntegerRange(rangeItemArray.Length())) {
|
|
rangeItemArray[index] = new RangeItem();
|
|
rangeItemArray[index]->StoreRange(*mRanges[index]);
|
|
aHTMLEditor.RangeUpdaterRef().RegisterRangeItem(*rangeItemArray[index]);
|
|
if (mRanges[index] == mAnchorFocusRange) {
|
|
anchorFocusRangeIndex = Some(index);
|
|
}
|
|
}
|
|
// TODO: We should keep the array, and just update the ranges.
|
|
mRanges.Clear();
|
|
mAnchorFocusRange = nullptr;
|
|
// Now bust up inlines.
|
|
nsresult rv = NS_OK;
|
|
for (OwningNonNull<RangeItem>& item : Reversed(rangeItemArray)) {
|
|
// MOZ_KnownLive because 'rangeItemArray' is guaranteed to keep it alive.
|
|
Result<EditorDOMPoint, nsresult> splitParentsResult =
|
|
aHTMLEditor.SplitParentInlineElementsAtRangeBoundaries(
|
|
MOZ_KnownLive(*item), aEditingHost, aAncestorLimiter);
|
|
if (MOZ_UNLIKELY(splitParentsResult.isErr())) {
|
|
NS_WARNING(
|
|
"HTMLEditor::SplitParentInlineElementsAtRangeBoundaries() failed");
|
|
rv = splitParentsResult.unwrapErr();
|
|
break;
|
|
}
|
|
if (splitParentsResult.inspect().IsSet()) {
|
|
pointToPutCaret = splitParentsResult.unwrap();
|
|
}
|
|
}
|
|
// Then unregister the ranges
|
|
for (size_t index : IntegerRange(rangeItemArray.Length())) {
|
|
aHTMLEditor.RangeUpdaterRef().DropRangeItem(rangeItemArray[index]);
|
|
RefPtr<nsRange> range = rangeItemArray[index]->GetRange();
|
|
if (range && range->IsPositioned()) {
|
|
if (anchorFocusRangeIndex.isSome() && index == *anchorFocusRangeIndex) {
|
|
mAnchorFocusRange = range;
|
|
}
|
|
mRanges.AppendElement(std::move(range));
|
|
}
|
|
}
|
|
if (!mAnchorFocusRange && !mRanges.IsEmpty()) {
|
|
mAnchorFocusRange = mRanges.LastElement();
|
|
}
|
|
|
|
// XXX Why do we ignore the other errors here??
|
|
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
|
|
return Err(NS_ERROR_EDITOR_DESTROYED);
|
|
}
|
|
return pointToPutCaret;
|
|
}
|
|
|
|
nsresult AutoRangeArray::CollectEditTargetNodes(
|
|
const HTMLEditor& aHTMLEditor,
|
|
nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
|
|
EditSubAction aEditSubAction,
|
|
CollectNonEditableNodes aCollectNonEditableNodes) const {
|
|
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
|
|
|
|
// FYI: This was moved from
|
|
// https://searchfox.org/mozilla-central/rev/4bce7d85ba4796dd03c5dcc7cfe8eee0e4c07b3b/editor/libeditor/HTMLEditSubActionHandler.cpp#7060
|
|
|
|
// Gather up a list of all the nodes
|
|
for (const OwningNonNull<nsRange>& range : mRanges) {
|
|
DOMSubtreeIterator iter;
|
|
nsresult rv = iter.Init(*range);
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("DOMSubtreeIterator::Init() failed");
|
|
return rv;
|
|
}
|
|
if (aOutArrayOfContents.IsEmpty()) {
|
|
iter.AppendAllNodesToArray(aOutArrayOfContents);
|
|
} else {
|
|
AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfTopChildren;
|
|
iter.AppendNodesToArray(
|
|
+[](nsINode& aNode, void* aArray) -> bool {
|
|
MOZ_ASSERT(aArray);
|
|
return !static_cast<nsTArray<OwningNonNull<nsIContent>>*>(aArray)
|
|
->Contains(&aNode);
|
|
},
|
|
arrayOfTopChildren, &aOutArrayOfContents);
|
|
aOutArrayOfContents.AppendElements(std::move(arrayOfTopChildren));
|
|
}
|
|
if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
|
|
for (size_t i : Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
|
|
if (!EditorUtils::IsEditableContent(aOutArrayOfContents[i],
|
|
EditorUtils::EditorType::HTML)) {
|
|
aOutArrayOfContents.RemoveElementAt(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (aEditSubAction) {
|
|
case EditSubAction::eCreateOrRemoveBlock: {
|
|
// Certain operations should not act on li's and td's, but rather inside
|
|
// them. Alter the list as needed.
|
|
CollectChildrenOptions options = {
|
|
CollectChildrenOption::CollectListChildren,
|
|
CollectChildrenOption::CollectTableChildren};
|
|
if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
|
|
options += CollectChildrenOption::IgnoreNonEditableChildren;
|
|
}
|
|
for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
|
|
OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
|
|
if (HTMLEditUtils::IsListItem(content)) {
|
|
aOutArrayOfContents.RemoveElementAt(i);
|
|
HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
|
|
options);
|
|
}
|
|
}
|
|
// Empty text node shouldn't be selected if unnecessary
|
|
for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
|
|
if (Text* text = aOutArrayOfContents[i]->GetAsText()) {
|
|
// Don't select empty text except to empty block
|
|
if (!HTMLEditUtils::IsVisibleTextNode(*text)) {
|
|
aOutArrayOfContents.RemoveElementAt(i);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case EditSubAction::eCreateOrChangeList: {
|
|
// XXX aCollectNonEditableNodes is ignored here. Maybe a bug.
|
|
CollectChildrenOptions options = {
|
|
CollectChildrenOption::CollectTableChildren};
|
|
for (size_t i = aOutArrayOfContents.Length(); i > 0; i--) {
|
|
// Scan for table elements. If we find table elements other than
|
|
// table, replace it with a list of any editable non-table content
|
|
// because if a selection range starts from end in a table-cell and
|
|
// ends at or starts from outside the `<table>`, we need to make
|
|
// lists in each selected table-cells.
|
|
OwningNonNull<nsIContent> content = aOutArrayOfContents[i - 1];
|
|
if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
|
|
aOutArrayOfContents.RemoveElementAt(i - 1);
|
|
HTMLEditUtils::CollectChildren(content, aOutArrayOfContents, i - 1,
|
|
options);
|
|
}
|
|
}
|
|
// If there is only one node in the array, and it is a `<div>`,
|
|
// `<blockquote>` or a list element, then look inside of it until we
|
|
// find inner list or content.
|
|
if (aOutArrayOfContents.Length() != 1) {
|
|
break;
|
|
}
|
|
Element* deepestDivBlockquoteOrListElement =
|
|
HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
|
|
aOutArrayOfContents[0],
|
|
{HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode},
|
|
nsGkAtoms::div, nsGkAtoms::blockquote, nsGkAtoms::ul,
|
|
nsGkAtoms::ol, nsGkAtoms::dl);
|
|
if (!deepestDivBlockquoteOrListElement) {
|
|
break;
|
|
}
|
|
if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
|
|
nsGkAtoms::div, nsGkAtoms::blockquote)) {
|
|
aOutArrayOfContents.Clear();
|
|
// XXX Before we're called, non-editable nodes are ignored. However,
|
|
// we may append non-editable nodes here.
|
|
HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
|
|
aOutArrayOfContents, 0, {});
|
|
break;
|
|
}
|
|
aOutArrayOfContents.ReplaceElementAt(
|
|
0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
|
|
break;
|
|
}
|
|
case EditSubAction::eOutdent:
|
|
case EditSubAction::eIndent:
|
|
case EditSubAction::eSetPositionToAbsolute: {
|
|
// Indent/outdent already do something special for list items, but we
|
|
// still need to make sure we don't act on table elements
|
|
CollectChildrenOptions options = {
|
|
CollectChildrenOption::CollectListChildren,
|
|
CollectChildrenOption::CollectTableChildren};
|
|
if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
|
|
options += CollectChildrenOption::IgnoreNonEditableChildren;
|
|
}
|
|
for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
|
|
OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
|
|
if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
|
|
aOutArrayOfContents.RemoveElementAt(i);
|
|
HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
|
|
options);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Outdent should look inside of divs.
|
|
if (aEditSubAction == EditSubAction::eOutdent &&
|
|
!aHTMLEditor.IsCSSEnabled()) {
|
|
CollectChildrenOptions options = {};
|
|
if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
|
|
options += CollectChildrenOption::IgnoreNonEditableChildren;
|
|
}
|
|
for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
|
|
OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
|
|
if (content->IsHTMLElement(nsGkAtoms::div)) {
|
|
aOutArrayOfContents.RemoveElementAt(i);
|
|
HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
|
|
options);
|
|
}
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
Element* AutoRangeArray::GetClosestAncestorAnyListElementOfRange() const {
|
|
for (const OwningNonNull<nsRange>& range : mRanges) {
|
|
nsINode* commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
|
|
if (MOZ_UNLIKELY(!commonAncestorNode)) {
|
|
continue;
|
|
}
|
|
for (Element* element :
|
|
commonAncestorNode->InclusiveAncestorsOfType<Element>()) {
|
|
if (HTMLEditUtils::IsAnyListElement(element)) {
|
|
return element;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace mozilla
|