Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#ifndef mozilla_SelectionState_h
#define mozilla_SelectionState_h
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/dom/Document.h"
#include "nsCOMPtr.h"
#include "nsDirection.h"
#include "nsINode.h"
#include "nsRange.h"
#include "nsTArray.h"
#include "nscore.h"
class nsCycleCollectionTraversalCallback;
class nsRange;
namespace mozilla {
namespace dom {
class Element;
class Selection;
class Text;
} // namespace dom
/**
* A helper struct for saving/setting ranges.
*/
struct RangeItem final {
RangeItem() : mStartOffset(0), mEndOffset(0) {}
private:
// Private destructor, to discourage deletion outside of Release():
~RangeItem() = default;
public:
void StoreRange(const nsRange& aRange);
void StoreRange(const EditorRawDOMPoint& aStartPoint,
const EditorRawDOMPoint& aEndPoint) {
MOZ_ASSERT(aStartPoint.IsSet());
MOZ_ASSERT(aEndPoint.IsSet());
mStartContainer = aStartPoint.GetContainer();
mStartOffset = aStartPoint.Offset();
mEndContainer = aEndPoint.GetContainer();
mEndOffset = aEndPoint.Offset();
}
void Clear() {
mStartContainer = mEndContainer = nullptr;
mStartOffset = mEndOffset = 0;
}
already_AddRefed<nsRange> GetRange() const;
// Same as the API of dom::AbstractRange
[[nodiscard]] nsINode* GetRoot() const;
[[nodiscard]] bool Collapsed() const {
return mStartContainer == mEndContainer && mStartOffset == mEndOffset;
}
[[nodiscard]] bool IsPositioned() const {
return mStartContainer && mEndContainer;
}
[[nodiscard]] bool Equals(const RangeItem& aOther) const {
return mStartContainer == aOther.mStartContainer &&
mEndContainer == aOther.mEndContainer &&
mStartOffset == aOther.mStartOffset &&
mEndOffset == aOther.mEndOffset;
}
template <typename EditorDOMPointType = EditorDOMPoint>
EditorDOMPointType StartPoint() const {
return EditorDOMPointType(mStartContainer, mStartOffset);
}
template <typename EditorDOMPointType = EditorDOMPoint>
EditorDOMPointType EndPoint() const {
return EditorDOMPointType(mEndContainer, mEndOffset);
}
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem)
NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem)
nsCOMPtr<nsINode> mStartContainer;
nsCOMPtr<nsINode> mEndContainer;
uint32_t mStartOffset;
uint32_t mEndOffset;
};
/**
* mozilla::SelectionState
*
* Class for recording selection info. Stores selection as collection of
* { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
* ranges since dom gravity will possibly change the ranges.
*/
class SelectionState final {
public:
SelectionState() = default;
explicit SelectionState(const AutoRangeArray& aRanges);
/**
* Same as the API as dom::Selection
*/
[[nodiscard]] bool IsCollapsed() const {
if (mArray.Length() != 1) {
return false;
}
return mArray[0]->Collapsed();
}
void RemoveAllRanges() {
mArray.Clear();
mDirection = eDirNext;
}
[[nodiscard]] uint32_t RangeCount() const { return mArray.Length(); }
/**
* Saving all ranges of aSelection.
*/
void SaveSelection(dom::Selection& aSelection);
/**
* Setting aSelection to have all ranges stored by this instance.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
RestoreSelection(dom::Selection& aSelection);
/**
* Setting aRanges to have all ranges stored by this instance.
*/
void ApplyTo(AutoRangeArray& aRanges);
/**
* HasOnlyCollapsedRange() returns true only when there is a positioned range
* which is collapsed. I.e., the selection represents a caret point.
*/
[[nodiscard]] bool HasOnlyCollapsedRange() const {
if (mArray.Length() != 1) {
return false;
}
if (!mArray[0]->IsPositioned() || !mArray[0]->Collapsed()) {
return false;
}
return true;
}
/**
* Equals() returns true only when there are same number of ranges and
* all their containers and offsets are exactly same. This won't check
* the validity of each range with the current DOM tree.
*/
[[nodiscard]] bool Equals(const SelectionState& aOther) const;
/**
* Returns common root node of all ranges' start and end containers.
* Some of them have different root nodes, this returns nullptr.
*/
[[nodiscard]] nsINode* GetCommonRootNode() const {
nsINode* rootNode = nullptr;
for (const RefPtr<RangeItem>& rangeItem : mArray) {
nsINode* newRootNode = rangeItem->GetRoot();
if (!newRootNode || (rootNode && rootNode != newRootNode)) {
return nullptr;
}
rootNode = newRootNode;
}
return rootNode;
}
private:
CopyableAutoTArray<RefPtr<RangeItem>, 1> mArray;
nsDirection mDirection = eDirNext;
friend class RangeUpdater;
friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
SelectionState&, const char*,
uint32_t);
friend void ImplCycleCollectionUnlink(SelectionState&);
};
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback, SelectionState& aField,
const char* aName, uint32_t aFlags = 0) {
ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
}
inline void ImplCycleCollectionUnlink(SelectionState& aField) {
ImplCycleCollectionUnlink(aField.mArray);
}
class MOZ_STACK_CLASS RangeUpdater final {
public:
RangeUpdater();
void RegisterRangeItem(RangeItem& aRangeItem);
void DropRangeItem(RangeItem& aRangeItem);
void RegisterSelectionState(SelectionState& aSelectionState);
void DropSelectionState(SelectionState& aSelectionState);
// editor selection gravity routines. Note that we can't always depend on
// DOM Range gravity to do what we want to the "real" selection. For
// instance, if you move a node, that corresponds to deleting it and
// reinserting it. DOM Range gravity will promote the selection out of the
// node on deletion, which is not what you want if you know you are
// reinserting it.
template <typename PT, typename CT>
nsresult SelAdjCreateNode(const EditorDOMPointBase<PT, CT>& aPoint);
template <typename PT, typename CT>
nsresult SelAdjInsertNode(const EditorDOMPointBase<PT, CT>& aPoint);
void SelAdjDeleteNode(nsINode& aNode);
/**
* SelAdjSplitNode() is called immediately after spliting aOriginalNode
* and inserted aNewContent into the DOM tree.
*
* @param aOriginalContent The node which was split.
* @param aSplitOffset The old offset in aOriginalContent at splitting
* it.
* @param aNewContent The new content node which was inserted into
* the DOM tree.
*/
nsresult SelAdjSplitNode(nsIContent& aOriginalContent, uint32_t aSplitOffset,
nsIContent& aNewContent);
/**
* SelAdjJoinNodes() is called immediately after joining aRemovedContent and
* the container of aStartOfRightContent.
*
* @param aStartOfRightContent The container is joined content node which
* now has all children or text data which were
* in aRemovedContent. And this points where
* the joined position.
* @param aRemovedContent The removed content.
* @param aOldPointAtRightContent The point where the right content node was
* before joining them. The offset must have
* been initialized before the joining.
*/
nsresult SelAdjJoinNodes(const EditorRawDOMPoint& aStartOfRightContent,
const nsIContent& aRemovedContent,
const EditorDOMPoint& aOldPointAtRightContent);
void SelAdjInsertText(const dom::Text& aTextNode, uint32_t aOffset,
uint32_t aInsertedLength);
void SelAdjDeleteText(const dom::Text& aTextNode, uint32_t aOffset,
uint32_t aDeletedLength);
void SelAdjReplaceText(const dom::Text& aTextNode, uint32_t aOffset,
uint32_t aReplacedLength, uint32_t aInsertedLength);
// the following gravity routines need will/did sandwiches, because the other
// gravity routines will be called inside of these sandwiches, but should be
// ignored.
void WillReplaceContainer() {
// XXX Isn't this possible with mutation event listener?
NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
mLocked = true;
}
void DidReplaceContainer(const dom::Element& aRemovedElement,
dom::Element& aInsertedElement);
void WillRemoveContainer() {
// XXX Isn't this possible with mutation event listener?
NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
mLocked = true;
}
void DidRemoveContainer(const dom::Element& aRemovedElement,
nsINode& aRemovedElementContainerNode,
uint32_t aOldOffsetOfRemovedElement,
uint32_t aOldChildCountOfRemovedElement);
void WillInsertContainer() {
// XXX Isn't this possible with mutation event listener?
NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
mLocked = true;
}
void DidInsertContainer() {
NS_WARNING_ASSERTION(mLocked, "Not locked");
mLocked = false;
}
void DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
const nsINode& aNewParent, uint32_t aNewOffset);
private:
// TODO: A lot of loop in these methods check whether each item `nullptr` or
// not. We should make it not nullable later.
nsTArray<RefPtr<RangeItem>> mArray;
bool mLocked;
};
/**
* Helper class for using SelectionState. Stack based class for doing
* preservation of dom points across editor actions.
*/
class MOZ_STACK_CLASS AutoTrackDOMPoint final {
public:
AutoTrackDOMPoint() = delete;
AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, CaretPoint* aCaretPoint);
AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, nsCOMPtr<nsINode>* aNode,
uint32_t* aOffset)
: mRangeUpdater(aRangeUpdater),
mNode(aNode),
mOffset(aOffset),
mRangeItem(do_AddRef(new RangeItem())),
mWasConnected(aNode && (*aNode)->IsInComposedDoc()) {
mRangeItem->mStartContainer = *mNode;
mRangeItem->mEndContainer = *mNode;
mRangeItem->mStartOffset = *mOffset;
mRangeItem->mEndOffset = *mOffset;
mDocument = (*mNode)->OwnerDoc();
mRangeUpdater.RegisterRangeItem(mRangeItem);
}
AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, EditorDOMPoint* aPoint)
: mRangeUpdater(aRangeUpdater),
mNode(nullptr),
mOffset(nullptr),
mPoint(Some(aPoint->IsSet() ? aPoint : nullptr)),
mRangeItem(do_AddRef(new RangeItem())),
mWasConnected(aPoint && aPoint->IsInComposedDoc()) {
if (!aPoint->IsSet()) {
mIsTracking = false;
return; // Nothing should be tracked.
}
mRangeItem->mStartContainer = aPoint->GetContainer();
mRangeItem->mEndContainer = aPoint->GetContainer();
mRangeItem->mStartOffset = aPoint->Offset();
mRangeItem->mEndOffset = aPoint->Offset();
mDocument = aPoint->GetContainer()->OwnerDoc();
mRangeUpdater.RegisterRangeItem(mRangeItem);
}
~AutoTrackDOMPoint() { FlushAndStopTracking(); }
void FlushAndStopTracking() {
if (!mIsTracking) {
return;
}
mIsTracking = false;
if (mPoint.isSome()) {
mRangeUpdater.DropRangeItem(mRangeItem);
// Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
// and the number of times may be too many. (E.g., 1533913.html hits
// over 700 times!) We should just put warning instead.
if (NS_WARN_IF(!mRangeItem->mStartContainer)) {
mPoint.ref()->Clear();
return;
}
// If the node was removed from the original document, clear the instance
// since the user should not keep handling the adopted or orphan node
// anymore.
if (NS_WARN_IF(mWasConnected &&
!mRangeItem->mStartContainer->IsInComposedDoc()) ||
NS_WARN_IF(mRangeItem->mStartContainer->OwnerDoc() != mDocument)) {
mPoint.ref()->Clear();
return;
}
if (NS_WARN_IF(mRangeItem->mStartContainer->Length() <
mRangeItem->mStartOffset)) {
mPoint.ref()->SetToEndOf(mRangeItem->mStartContainer);
return;
}
mPoint.ref()->Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset);
return;
}
mRangeUpdater.DropRangeItem(mRangeItem);
*mNode = mRangeItem->mStartContainer;
*mOffset = mRangeItem->mStartOffset;
if (!(*mNode)) {
return;
}
// If the node was removed from the original document, clear the instances
// since the user should not keep handling the adopted or orphan node
// anymore.
if (NS_WARN_IF(mWasConnected && !(*mNode)->IsInComposedDoc()) ||
NS_WARN_IF((*mNode)->OwnerDoc() != mDocument)) {
*mNode = nullptr;
*mOffset = 0;
}
}
void StopTracking() { mIsTracking = false; }
private:
RangeUpdater& mRangeUpdater;
// Allow tracking nsINode until nsNode is gone
nsCOMPtr<nsINode>* mNode;
uint32_t* mOffset;
Maybe<EditorDOMPoint*> mPoint;
OwningNonNull<RangeItem> mRangeItem;
RefPtr<dom::Document> mDocument;
bool mIsTracking = true;
bool mWasConnected;
};
class MOZ_STACK_CLASS AutoTrackDOMRange final {
public:
AutoTrackDOMRange() = delete;
AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMPoint* aStartPoint,
EditorDOMPoint* aEndPoint)
: mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
mStartPointTracker.emplace(aRangeUpdater, aStartPoint);
mEndPointTracker.emplace(aRangeUpdater, aEndPoint);
}
AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMRange* aRange)
: mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
mStartPointTracker.emplace(
aRangeUpdater, const_cast<EditorDOMPoint*>(&aRange->StartRef()));
mEndPointTracker.emplace(aRangeUpdater,
const_cast<EditorDOMPoint*>(&aRange->EndRef()));
}
AutoTrackDOMRange(RangeUpdater& aRangeUpdater, RefPtr<nsRange>* aRange)
: mStartPoint((*aRange)->StartRef()),
mEndPoint((*aRange)->EndRef()),
mRangeRefPtr(aRange),
mRangeOwningNonNull(nullptr) {
mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
}
AutoTrackDOMRange(RangeUpdater& aRangeUpdater, OwningNonNull<nsRange>* aRange)
: mStartPoint((*aRange)->StartRef()),
mEndPoint((*aRange)->EndRef()),
mRangeRefPtr(nullptr),
mRangeOwningNonNull(aRange) {
mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
}
~AutoTrackDOMRange() { FlushAndStopTracking(); }
void FlushAndStopTracking() {
if (!mStartPointTracker && !mEndPointTracker) {
return;
}
mStartPointTracker.reset();
mEndPointTracker.reset();
if (!mRangeRefPtr && !mRangeOwningNonNull) {
// This must be created with EditorDOMRange or EditorDOMPoints. In the
// cases, destroying mStartPointTracker and mEndPointTracker has done
// everything which we need to do.
return;
}
// Otherwise, update the DOM ranges by ourselves.
if (mRangeRefPtr) {
if (!mStartPoint.IsSet() || !mEndPoint.IsSet()) {
(*mRangeRefPtr)->Reset();
return;
}
(*mRangeRefPtr)
->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
mEndPoint.ToRawRangeBoundary());
return;
}
if (mRangeOwningNonNull) {
if (!mStartPoint.IsSet() || !mEndPoint.IsSet()) {
(*mRangeOwningNonNull)->Reset();
return;
}
(*mRangeOwningNonNull)
->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
mEndPoint.ToRawRangeBoundary());
return;
}
}
void StopTracking() {
if (mStartPointTracker) {
mStartPointTracker->StopTracking();
}
if (mEndPointTracker) {
mEndPointTracker->StopTracking();
}
}
void StopTrackingStartBoundary() {
MOZ_ASSERT(!mRangeRefPtr,
"StopTrackingStartBoundary() is not available when tracking "
"RefPtr<nsRange>");
MOZ_ASSERT(!mRangeOwningNonNull,
"StopTrackingStartBoundary() is not available when tracking "
"OwningNonNull<nsRange>");
if (!mStartPointTracker) {
return;
}
mStartPointTracker->StopTracking();
}
void StopTrackingEndBoundary() {
MOZ_ASSERT(!mRangeRefPtr,
"StopTrackingEndBoundary() is not available when tracking "
"RefPtr<nsRange>");
MOZ_ASSERT(!mRangeOwningNonNull,
"StopTrackingEndBoundary() is not available when tracking "
"OwningNonNull<nsRange>");
if (!mEndPointTracker) {
return;
}
mEndPointTracker->StopTracking();
}
private:
Maybe<AutoTrackDOMPoint> mStartPointTracker;
Maybe<AutoTrackDOMPoint> mEndPointTracker;
EditorDOMPoint mStartPoint;
EditorDOMPoint mEndPoint;
RefPtr<nsRange>* mRangeRefPtr;
OwningNonNull<nsRange>* mRangeOwningNonNull;
};
class MOZ_STACK_CLASS AutoTrackDOMMoveNodeResult final {
public:
AutoTrackDOMMoveNodeResult(RangeUpdater& aRangeUpdater,
MoveNodeResult* aMoveNodeResult);
void FlushAndStopTracking() {
mTrackCaretPoint.FlushAndStopTracking();
mTrackNextInsertionPoint.FlushAndStopTracking();
mTrackMovedContentRange.FlushAndStopTracking();
}
void StopTracking() {
mTrackCaretPoint.StopTracking();
mTrackNextInsertionPoint.StopTracking();
mTrackMovedContentRange.StopTracking();
}
private:
AutoTrackDOMPoint mTrackCaretPoint;
AutoTrackDOMPoint mTrackNextInsertionPoint;
AutoTrackDOMRange mTrackMovedContentRange;
};
class MOZ_STACK_CLASS AutoTrackDOMDeleteRangeResult final {
public:
AutoTrackDOMDeleteRangeResult(RangeUpdater& aRangeUpdater,
DeleteRangeResult* aDeleteRangeResult);
void FlushAndStopTracking() {
mTrackCaretPoint.FlushAndStopTracking();
mTrackDeleteRange.FlushAndStopTracking();
}
void StopTracking() {
mTrackCaretPoint.StopTracking();
mTrackDeleteRange.StopTracking();
}
private:
AutoTrackDOMPoint mTrackCaretPoint;
AutoTrackDOMRange mTrackDeleteRange;
};
/**
* Another helper class for SelectionState. Stack based class for doing
* Will/DidReplaceContainer()
*/
class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final {
public:
AutoReplaceContainerSelNotify() = delete;
// FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
// for the members.
MOZ_CAN_RUN_SCRIPT
AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater,
dom::Element& aOriginalElement,
dom::Element& aNewElement)
: mRangeUpdater(aRangeUpdater),
mOriginalElement(aOriginalElement),
mNewElement(aNewElement) {
mRangeUpdater.WillReplaceContainer();
}
~AutoReplaceContainerSelNotify() {
mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement);
}
private:
RangeUpdater& mRangeUpdater;
dom::Element& mOriginalElement;
dom::Element& mNewElement;
};
/**
* Another helper class for SelectionState. Stack based class for doing
* Will/DidRemoveContainer()
*/
class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final {
public:
AutoRemoveContainerSelNotify() = delete;
AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater,
const EditorRawDOMPoint& aAtRemovingElement)
: mRangeUpdater(aRangeUpdater),
mRemovingElement(*aAtRemovingElement.GetChild()->AsElement()),
mParentNode(*aAtRemovingElement.GetContainer()),
mOffsetInParent(aAtRemovingElement.Offset()),
mChildCountOfRemovingElement(mRemovingElement->GetChildCount()) {
MOZ_ASSERT(aAtRemovingElement.IsSet());
mRangeUpdater.WillRemoveContainer();
}
~AutoRemoveContainerSelNotify() {
mRangeUpdater.DidRemoveContainer(mRemovingElement, mParentNode,
mOffsetInParent,
mChildCountOfRemovingElement);
}
private:
RangeUpdater& mRangeUpdater;
OwningNonNull<dom::Element> mRemovingElement;
OwningNonNull<nsINode> mParentNode;
uint32_t mOffsetInParent;
uint32_t mChildCountOfRemovingElement;
};
/**
* Another helper class for SelectionState. Stack based class for doing
* Will/DidInsertContainer()
* XXX The lock state isn't useful if the edit action is triggered from
* a mutation event listener so that looks like that we can remove
* this class.
*/
class MOZ_STACK_CLASS AutoInsertContainerSelNotify final {
private:
RangeUpdater& mRangeUpdater;
public:
AutoInsertContainerSelNotify() = delete;
explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater)
: mRangeUpdater(aRangeUpdater) {
mRangeUpdater.WillInsertContainer();
}
~AutoInsertContainerSelNotify() { mRangeUpdater.DidInsertContainer(); }
};
/**
* Another helper class for SelectionState. Stack based class for doing
* DidMoveNode()
*/
class MOZ_STACK_CLASS AutoMoveNodeSelNotify final {
public:
AutoMoveNodeSelNotify() = delete;
AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
const EditorRawDOMPoint& aOldPoint,
const EditorRawDOMPoint& aNewPoint)
: mRangeUpdater(aRangeUpdater),
mOldParent(*aOldPoint.GetContainer()),
mNewParent(*aNewPoint.GetContainer()),
mOldOffset(aOldPoint.Offset()),
mNewOffset(aNewPoint.Offset()) {
MOZ_ASSERT(aOldPoint.IsSet());
MOZ_ASSERT(aNewPoint.IsSet());
}
~AutoMoveNodeSelNotify() {
mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset);
}
private:
RangeUpdater& mRangeUpdater;
nsINode& mOldParent;
nsINode& mNewParent;
const uint32_t mOldOffset;
const uint32_t mNewOffset;
};
} // namespace mozilla
#endif // #ifndef mozilla_SelectionState_h