gecko-dev/editor/libeditor/AutoRangeArray.h

467 lines
17 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/. */
#ifndef AutoRangeArray_h
#define AutoRangeArray_h
#include "EditAction.h" // for EditSubAction
#include "EditorBase.h" // for EditorBase
#include "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
#include "EditorForwards.h"
#include "SelectionState.h" // for SelectionState
#include "mozilla/ErrorResult.h" // for ErrorResult
#include "mozilla/IntegerRange.h" // for IntegerRange
#include "mozilla/Maybe.h" // for Maybe
#include "mozilla/RangeBoundary.h" // for RangeBoundary
#include "mozilla/Result.h" // for Result<>
#include "mozilla/dom/Element.h" // for dom::Element
#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 "nsDebug.h" // for NS_WARNING, etc
#include "nsDirection.h" // for nsDirection
#include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
#include "nsRange.h" // for nsRange
namespace mozilla {
/******************************************************************************
* AutoRangeArray stores ranges which do no belong any `Selection`.
* So, different from `AutoSelectionRangeArray`, this can be used for
* ranges which may need to be modified before touching the DOM tree,
* but does not want to modify `Selection` for the performance.
*****************************************************************************/
class MOZ_STACK_CLASS AutoRangeArray final {
public:
explicit AutoRangeArray(const dom::Selection& aSelection);
template <typename PointType>
explicit AutoRangeArray(const EditorDOMRangeBase<PointType>& aRange);
template <typename PT, typename CT>
explicit AutoRangeArray(const EditorDOMPointBase<PT, CT>& aPoint);
// The copy constructor copies everything except saved ranges.
explicit AutoRangeArray(const AutoRangeArray& aOther);
~AutoRangeArray();
void Initialize(const dom::Selection& aSelection) {
ClearSavedRanges();
mDirection = aSelection.GetDirection();
mRanges.Clear();
for (const uint32_t i : IntegerRange(aSelection.RangeCount())) {
MOZ_ASSERT(aSelection.GetRangeAt(i));
mRanges.AppendElement(aSelection.GetRangeAt(i)->CloneRange());
if (aSelection.GetRangeAt(i) == aSelection.GetAnchorFocusRange()) {
mAnchorFocusRange = mRanges.LastElement();
}
}
}
/**
* Check whether all ranges in content nodes or not. If the ranges is empty,
* this returns false.
*/
[[nodiscard]] bool IsInContent() const {
if (mRanges.IsEmpty()) {
return false;
}
for (const OwningNonNull<nsRange>& range : mRanges) {
if (MOZ_UNLIKELY(!range->IsPositioned() || !range->GetStartContainer() ||
!range->GetStartContainer()->IsContent() ||
!range->GetEndContainer() ||
!range->GetEndContainer()->IsContent())) {
return false;
}
}
return true;
}
/**
* EnsureOnlyEditableRanges() removes ranges which cannot modify.
* Note that this is designed only for `HTMLEditor` because this must not
* be required by `TextEditor`.
*/
void EnsureOnlyEditableRanges(const dom::Element& aEditingHost);
/**
* EnsureRangesInTextNode() is designed for TextEditor to guarantee that
* all ranges are in its text node which is first child of the anonymous <div>
* element and is first child.
*/
void EnsureRangesInTextNode(const dom::Text& aTextNode);
/**
* Extend ranges to wrap lines to handle block level edit actions such as
* updating the block parent or indent/outdent around the selection.
*/
void ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
EditSubAction aEditSubAction, const dom::Element& aEditingHost);
/**
* Check whether the range is in aEditingHost and both containers of start and
* end boundaries of the range are editable.
*/
[[nodiscard]] static bool IsEditableRange(const dom::AbstractRange& aRange,
const dom::Element& aEditingHost);
/**
* Check whether the first range is in aEditingHost and both containers of
* start and end boundaries of the first range are editable.
*/
[[nodiscard]] bool IsFirstRangeEditable(
const dom::Element& aEditingHost) const {
return IsEditableRange(FirstRangeRef(), aEditingHost);
}
/**
* IsAtLeastOneContainerOfRangeBoundariesInclusiveDescendantOf() returns true
* if at least one of the containers of the range boundaries is an inclusive
* descendant of aContent.
*/
[[nodiscard]] bool
IsAtLeastOneContainerOfRangeBoundariesInclusiveDescendantOf(
const nsIContent& aContent) const {
for (const OwningNonNull<nsRange>& range : mRanges) {
nsINode* startContainer = range->GetStartContainer();
if (startContainer &&
startContainer->IsInclusiveDescendantOf(&aContent)) {
return true;
}
nsINode* endContainer = range->GetEndContainer();
if (startContainer == endContainer) {
continue;
}
if (endContainer && endContainer->IsInclusiveDescendantOf(&aContent)) {
return true;
}
}
return false;
}
[[nodiscard]] auto& Ranges() { return mRanges; }
[[nodiscard]] const auto& Ranges() const { return mRanges; }
[[nodiscard]] OwningNonNull<nsRange>& FirstRangeRef() { return mRanges[0]; }
[[nodiscard]] const OwningNonNull<nsRange>& FirstRangeRef() const {
return mRanges[0];
}
template <template <typename> typename StrongPtrType>
[[nodiscard]] AutoTArray<StrongPtrType<nsRange>, 8> CloneRanges() const {
AutoTArray<StrongPtrType<nsRange>, 8> ranges;
for (const auto& range : mRanges) {
ranges.AppendElement(range->CloneRange());
}
return ranges;
}
template <typename EditorDOMPointType>
[[nodiscard]] EditorDOMPointType GetFirstRangeStartPoint() const {
if (mRanges.IsEmpty() || !mRanges[0]->IsPositioned()) {
return EditorDOMPointType();
}
return EditorDOMPointType(mRanges[0]->StartRef());
}
template <typename EditorDOMPointType>
[[nodiscard]] EditorDOMPointType GetFirstRangeEndPoint() const {
if (mRanges.IsEmpty() || !mRanges[0]->IsPositioned()) {
return EditorDOMPointType();
}
return EditorDOMPointType(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
* selection limiter. Note that if a range is extened, the range is
* recreated. Therefore, caller cannot cache pointer of any ranges before
* calling this.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<nsIEditor::EDirection, nsresult>
ExtendAnchorFocusRangeFor(const EditorBase& aEditorBase,
nsIEditor::EDirection aDirectionAndAmount);
/**
* For compatiblity with the other browsers, we should shrink ranges to
* start from an atomic content and/or end after one instead of start
* from end of a preceding text node and end by start of a follwing text
* node. Returns true if this modifies a range.
*/
enum class IfSelectingOnlyOneAtomicContent {
Collapse, // Collapse to the range selecting only one atomic content to
// start or after of it. Whether to collapse start or after
// it depends on aDirectionAndAmount. This is ignored if
// there are multiple ranges.
KeepSelecting, // Won't collapse the range.
};
Result<bool, nsresult> ShrinkRangesIfStartFromOrEndAfterAtomicContent(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent,
const dom::Element* aEditingHost);
/**
* The following methods are same as `Selection`'s methods.
*/
[[nodiscard]] bool IsCollapsed() const {
return mRanges.IsEmpty() ||
(mRanges.Length() == 1 && mRanges[0]->Collapsed());
}
template <typename PT, typename CT>
nsresult Collapse(const EditorDOMPointBase<PT, CT>& aPoint) {
mRanges.Clear();
if (!mAnchorFocusRange) {
ErrorResult error;
mAnchorFocusRange = nsRange::Create(aPoint.ToRawRangeBoundary(),
aPoint.ToRawRangeBoundary(), error);
if (error.Failed()) {
mAnchorFocusRange = nullptr;
return error.StealNSResult();
}
} else {
nsresult rv = mAnchorFocusRange->CollapseTo(aPoint.ToRawRangeBoundary());
if (NS_FAILED(rv)) {
mAnchorFocusRange = nullptr;
return rv;
}
}
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;
}
template <typename SPT, typename SCT, typename EPT, typename ECT>
nsresult SetBaseAndExtent(const EditorDOMPointBase<SPT, SCT>& aAnchor,
const EditorDOMPointBase<EPT, ECT>& aFocus) {
if (MOZ_UNLIKELY(!aAnchor.IsSet()) || MOZ_UNLIKELY(!aFocus.IsSet())) {
mRanges.Clear();
mAnchorFocusRange = nullptr;
return NS_ERROR_INVALID_ARG;
}
return aAnchor.EqualsOrIsBefore(aFocus) ? SetStartAndEnd(aAnchor, aFocus)
: SetStartAndEnd(aFocus, aAnchor);
}
[[nodiscard]] const nsRange* GetAnchorFocusRange() const {
return mAnchorFocusRange;
}
[[nodiscard]] nsDirection GetDirection() const { return mDirection; }
void SetDirection(nsDirection aDirection) { mDirection = aDirection; }
[[nodiscard]] const RangeBoundary& AnchorRef() const {
if (!mAnchorFocusRange) {
static RangeBoundary sEmptyRangeBoundary;
return sEmptyRangeBoundary;
}
return mDirection == nsDirection::eDirNext ? mAnchorFocusRange->StartRef()
: mAnchorFocusRange->EndRef();
}
[[nodiscard]] nsINode* GetAnchorNode() const {
return AnchorRef().IsSet() ? AnchorRef().Container() : nullptr;
}
[[nodiscard]] uint32_t GetAnchorOffset() const {
return AnchorRef().IsSet()
? AnchorRef()
.Offset(RangeBoundary::OffsetFilter::kValidOffsets)
.valueOr(0)
: 0;
}
[[nodiscard]] nsIContent* GetChildAtAnchorOffset() const {
return AnchorRef().IsSet() ? AnchorRef().GetChildAtOffset() : nullptr;
}
[[nodiscard]] const RangeBoundary& FocusRef() const {
if (!mAnchorFocusRange) {
static RangeBoundary sEmptyRangeBoundary;
return sEmptyRangeBoundary;
}
return mDirection == nsDirection::eDirNext ? mAnchorFocusRange->EndRef()
: mAnchorFocusRange->StartRef();
}
[[nodiscard]] nsINode* GetFocusNode() const {
return FocusRef().IsSet() ? FocusRef().Container() : nullptr;
}
[[nodiscard]] uint32_t FocusOffset() const {
return FocusRef().IsSet()
? FocusRef()
.Offset(RangeBoundary::OffsetFilter::kValidOffsets)
.valueOr(0)
: 0;
}
[[nodiscard]] nsIContent* GetChildAtFocusOffset() const {
return FocusRef().IsSet() ? FocusRef().GetChildAtOffset() : nullptr;
}
void RemoveAllRanges() {
mRanges.Clear();
mAnchorFocusRange = nullptr;
mDirection = nsDirection::eDirNext;
}
/**
* APIs to store ranges with only container node and offset in it, and track
* them with RangeUpdater.
*/
[[nodiscard]] bool SaveAndTrackRanges(HTMLEditor& aHTMLEditor);
[[nodiscard]] bool HasSavedRanges() const { return mSavedRanges.isSome(); }
void ClearSavedRanges();
void RestoreFromSavedRanges() {
MOZ_DIAGNOSTIC_ASSERT(mSavedRanges.isSome());
if (mSavedRanges.isNothing()) {
return;
}
mSavedRanges->ApplyTo(*this);
ClearSavedRanges();
}
/**
* Apply mRanges and mDirection to aSelection.
*/
MOZ_CAN_RUN_SCRIPT nsresult ApplyTo(dom::Selection& aSelection) {
dom::SelectionBatcher selectionBatcher(aSelection, __FUNCTION__);
aSelection.RemoveAllRanges(IgnoreErrors());
MOZ_ASSERT(!aSelection.RangeCount());
aSelection.SetDirection(mDirection);
IgnoredErrorResult error;
for (const OwningNonNull<nsRange>& range : mRanges) {
// MOZ_KnownLive(range) due to bug 1622253
aSelection.AddRangeAndSelectFramesAndNotifyListeners(MOZ_KnownLive(range),
error);
if (error.Failed()) {
return error.StealNSResult();
}
}
return NS_OK;
}
/**
* If the points are same (i.e., mean a collapsed range) and in an empty block
* element except the padding <br> element, this makes aStartPoint and
* aEndPoint contain the padding <br> element.
*/
static void UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint,
const dom::Element& aEditingHost);
/**
* CreateRangeExtendedToHardLineStartAndEnd() creates an nsRange instance
* which may be expanded to start/end of hard line at both edges of the given
* range. If this fails handling something, returns nullptr.
*/
static already_AddRefed<nsRange>
CreateRangeWrappingStartAndEndLinesContainingBoundaries(
const EditorDOMRange& aRange, EditSubAction aEditSubAction,
const dom::Element& aEditingHost) {
if (!aRange.IsPositioned()) {
return nullptr;
}
return CreateRangeWrappingStartAndEndLinesContainingBoundaries(
aRange.StartRef(), aRange.EndRef(), aEditSubAction, aEditingHost);
}
static already_AddRefed<nsRange>
CreateRangeWrappingStartAndEndLinesContainingBoundaries(
const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint,
EditSubAction aEditSubAction, const dom::Element& aEditingHost) {
RefPtr<nsRange> range =
nsRange::Create(aStartPoint.ToRawRangeBoundary(),
aEndPoint.ToRawRangeBoundary(), IgnoreErrors());
if (MOZ_UNLIKELY(!range)) {
return nullptr;
}
if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
*range, aEditSubAction, aEditingHost)) ||
MOZ_UNLIKELY(!range->IsPositioned())) {
return nullptr;
}
return range.forget();
}
/**
* Splits text nodes if each range end is in middle of a text node, then,
* calls HTMLEditor::SplitParentInlineElementsAtRangeEdges(RangeItem&) for
* each range. Finally, updates ranges to keep edit target ranges as
* expected.
*
* @param aHTMLEditor The HTMLEditor which will handle the splittings.
* @return A suggest point to put caret if succeeded, but it may be
* unset.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries(
HTMLEditor& aHTMLEditor);
/**
* CollectEditTargetNodes() collects edit target nodes the ranges.
* First, this collects all nodes in given ranges, then, modifies the
* result for specific edit sub-actions.
*/
enum class CollectNonEditableNodes { No, Yes };
nsresult CollectEditTargetNodes(
const HTMLEditor& aHTMLEditor,
nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
EditSubAction aEditSubAction,
CollectNonEditableNodes aCollectNonEditableNodes) const;
/**
* Retrieve a closest ancestor list element of a common ancestor of _A_ range
* of the ranges. This tries to retrieve it from the first range to the last
* range.
*/
dom::Element* GetClosestAncestorAnyListElementOfRange() const;
private:
static nsresult ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
nsRange& aRange, EditSubAction aEditSubAction,
const dom::Element& aEditingHost);
AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges;
RefPtr<nsRange> mAnchorFocusRange;
nsDirection mDirection = nsDirection::eDirNext;
Maybe<SelectionState> mSavedRanges;
RefPtr<HTMLEditor> mTrackingHTMLEditor;
};
} // namespace mozilla
#endif // #ifndef AutoRangeArray_h