replaced by .editorconfig # ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D287844
748 lines
25 KiB
C++
748 lines
25 KiB
C++
/* 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 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 AutoClonedSelectionRangeArray& 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(AutoClonedSelectionRangeArray& 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;
|
|
}
|
|
template <typename PT, typename CT>
|
|
struct SimpleEditorDOMPointBase {
|
|
SimpleEditorDOMPointBase() = default;
|
|
SimpleEditorDOMPointBase(const nsINode* aContainer,
|
|
const nsIContent* aChild, uint32_t aOffset)
|
|
: mContainer(const_cast<nsINode*>(aContainer)),
|
|
mChild(const_cast<nsIContent*>(aChild)),
|
|
mOffset(Some(aOffset)) {}
|
|
SimpleEditorDOMPointBase(const nsIContent* aChild, uint32_t aOffset)
|
|
: mContainer(aChild->GetParentNode()),
|
|
mChild(const_cast<nsIContent*>(aChild)),
|
|
mOffset(Some(aOffset)) {}
|
|
SimpleEditorDOMPointBase(const nsINode* aContainer,
|
|
const nsIContent* aChild)
|
|
: mContainer(const_cast<nsINode*>(aContainer)),
|
|
mChild(const_cast<nsIContent*>(aChild)) {}
|
|
explicit SimpleEditorDOMPointBase(const nsIContent* aChild)
|
|
: mContainer(aChild->GetParentNode()),
|
|
mChild(const_cast<nsIContent*>(aChild)) {}
|
|
|
|
uint32_t Offset() const {
|
|
if (!mOffset && mContainer) {
|
|
mOffset = mContainer->ComputeIndexOf(mChild);
|
|
}
|
|
return mOffset.valueOr(0);
|
|
}
|
|
nsIContent* GetNextSiblingOfChild() const {
|
|
return mChild->GetNextSibling();
|
|
}
|
|
PT mContainer;
|
|
CT mChild;
|
|
mutable Maybe<uint32_t> mOffset;
|
|
};
|
|
using SimpleEditorDOMPoint =
|
|
SimpleEditorDOMPointBase<nsCOMPtr<nsINode>, nsCOMPtr<nsIContent>>;
|
|
using SimpleEditorRawDOMPoint =
|
|
SimpleEditorDOMPointBase<nsINode*, nsIContent*>;
|
|
void DidMoveNodes(const nsTArray<SimpleEditorDOMPoint>& aOldPoints,
|
|
const SimpleEditorDOMPoint& aExpectedDestination,
|
|
const nsTArray<SimpleEditorDOMPoint>& aNewPoints);
|
|
|
|
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;
|
|
};
|
|
|
|
enum class StopTracking : bool { No, Yes };
|
|
|
|
/**
|
|
* 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, EditorDOMPoint* aPoint)
|
|
: mRangeUpdater(aRangeUpdater),
|
|
mPoint(*aPoint),
|
|
mRangeItem(do_AddRef(new RangeItem())) {
|
|
Init();
|
|
}
|
|
|
|
private:
|
|
void Init() {
|
|
if (!mPoint.IsSet()) {
|
|
mIsTracking = false;
|
|
mWasConnected = false;
|
|
return; // Nothing should be tracked.
|
|
}
|
|
mRangeItem->mStartContainer = mPoint.GetContainer();
|
|
mRangeItem->mEndContainer = mPoint.GetContainer();
|
|
mRangeItem->mStartOffset = mPoint.Offset();
|
|
mRangeItem->mEndOffset = mPoint.Offset();
|
|
mDocument = mPoint.GetContainer()->OwnerDoc();
|
|
mWasConnected = mPoint.IsInComposedDoc();
|
|
mIsTracking = true;
|
|
mRangeUpdater.RegisterRangeItem(mRangeItem);
|
|
}
|
|
|
|
public:
|
|
~AutoTrackDOMPoint() { Flush(StopTracking::Yes); }
|
|
|
|
void Flush(StopTracking aStopTracking) {
|
|
if (!mIsTracking) {
|
|
return;
|
|
}
|
|
if (static_cast<bool>(aStopTracking)) {
|
|
mIsTracking = false;
|
|
}
|
|
if (!mIsTracking) {
|
|
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.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.Clear();
|
|
return;
|
|
}
|
|
if (NS_WARN_IF(mRangeItem->mStartContainer->Length() <
|
|
mRangeItem->mStartOffset)) {
|
|
mPoint.SetToEndOf(mRangeItem->mStartContainer);
|
|
return;
|
|
}
|
|
mPoint.Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset);
|
|
}
|
|
|
|
void StopTracking() {
|
|
if (mIsTracking) {
|
|
mIsTracking = false;
|
|
mRangeUpdater.DropRangeItem(mRangeItem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restart to track for the current mPoint value. I.e., the caller must have
|
|
* changed mPoint and wants to track the new point. Be aware, this does not
|
|
* flush, of course. Therefore, you'll completely lose the old point
|
|
* completely even if this fails to restart to track the new point.
|
|
*/
|
|
void RestartToTrack() {
|
|
StopTracking();
|
|
MOZ_ASSERT(mPoint.IsSetAndValid());
|
|
MOZ_ASSERT(mPoint.GetContainer()->OwnerDoc() == mDocument);
|
|
Init();
|
|
}
|
|
|
|
private:
|
|
RangeUpdater& mRangeUpdater;
|
|
EditorDOMPoint& mPoint;
|
|
OwningNonNull<RangeItem> mRangeItem;
|
|
RefPtr<dom::Document> mDocument;
|
|
bool mIsTracking = false;
|
|
bool mWasConnected = false;
|
|
};
|
|
|
|
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, const RefPtr<nsRange>* aRange)
|
|
: mStartPoint((*aRange)->StartRef()),
|
|
mEndPoint((*aRange)->EndRef()),
|
|
mRangeRefPtr(aRange),
|
|
mRangeOwningNonNull(nullptr) {
|
|
mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
|
|
mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
|
|
}
|
|
AutoTrackDOMRange(RangeUpdater& aRangeUpdater,
|
|
const OwningNonNull<nsRange>* aRange)
|
|
: mStartPoint((*aRange)->StartRef()),
|
|
mEndPoint((*aRange)->EndRef()),
|
|
mRangeRefPtr(nullptr),
|
|
mRangeOwningNonNull(aRange) {
|
|
mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
|
|
mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
|
|
}
|
|
~AutoTrackDOMRange() { Flush(StopTracking::Yes); }
|
|
|
|
void Flush(StopTracking aStopTracking) {
|
|
if (!mStartPointTracker && !mEndPointTracker) {
|
|
return;
|
|
}
|
|
if (static_cast<bool>(aStopTracking)) {
|
|
mStartPointTracker.reset();
|
|
mEndPointTracker.reset();
|
|
} else {
|
|
if (mStartPointTracker) {
|
|
mStartPointTracker->Flush(StopTracking::No);
|
|
if (MOZ_UNLIKELY(!mStartPoint.IsSet())) {
|
|
mStartPointTracker.reset();
|
|
}
|
|
}
|
|
if (mEndPointTracker) {
|
|
mEndPointTracker->Flush(StopTracking::No);
|
|
if (MOZ_UNLIKELY(!mEndPoint.IsSet())) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
private:
|
|
Maybe<AutoTrackDOMPoint> mStartPointTracker;
|
|
Maybe<AutoTrackDOMPoint> mEndPointTracker;
|
|
EditorDOMPoint mStartPoint;
|
|
EditorDOMPoint mEndPoint;
|
|
const RefPtr<nsRange>* mRangeRefPtr;
|
|
const OwningNonNull<nsRange>* mRangeOwningNonNull;
|
|
};
|
|
|
|
class MOZ_STACK_CLASS AutoTrackDOMMoveNodeResult final {
|
|
public:
|
|
AutoTrackDOMMoveNodeResult() = delete;
|
|
AutoTrackDOMMoveNodeResult(RangeUpdater& aRangeUpdater,
|
|
MoveNodeResult* aMoveNodeResult);
|
|
|
|
void Flush(StopTracking aStopTracking) {
|
|
mTrackCaretPoint.Flush(aStopTracking);
|
|
mTrackNextInsertionPoint.Flush(aStopTracking);
|
|
mTrackMovedContentRange.Flush(aStopTracking);
|
|
}
|
|
void StopTracking() {
|
|
mTrackCaretPoint.StopTracking();
|
|
mTrackNextInsertionPoint.StopTracking();
|
|
mTrackMovedContentRange.StopTracking();
|
|
}
|
|
|
|
private:
|
|
AutoTrackDOMPoint mTrackCaretPoint;
|
|
AutoTrackDOMPoint mTrackNextInsertionPoint;
|
|
AutoTrackDOMRange mTrackMovedContentRange;
|
|
};
|
|
|
|
class MOZ_STACK_CLASS AutoTrackDOMDeleteRangeResult final {
|
|
public:
|
|
AutoTrackDOMDeleteRangeResult() = delete;
|
|
AutoTrackDOMDeleteRangeResult(RangeUpdater& aRangeUpdater,
|
|
DeleteRangeResult* aDeleteRangeResult);
|
|
|
|
void Flush(StopTracking aStopTracking) {
|
|
mTrackCaretPoint.Flush(aStopTracking);
|
|
mTrackDeleteRange.Flush(aStopTracking);
|
|
}
|
|
void StopTracking() {
|
|
mTrackCaretPoint.StopTracking();
|
|
mTrackDeleteRange.StopTracking();
|
|
}
|
|
|
|
private:
|
|
AutoTrackDOMPoint mTrackCaretPoint;
|
|
AutoTrackDOMRange mTrackDeleteRange;
|
|
};
|
|
|
|
class MOZ_STACK_CLASS AutoTrackLineBreak final {
|
|
public:
|
|
AutoTrackLineBreak() = delete;
|
|
AutoTrackLineBreak(RangeUpdater& aRangeUpdater, EditorLineBreak* aLineBreak);
|
|
~AutoTrackLineBreak() { Flush(StopTracking::Yes); }
|
|
|
|
void Flush(StopTracking aStopTracking);
|
|
void StopTracking() { mTracker.StopTracking(); }
|
|
|
|
private:
|
|
EditorLineBreak* mLineBreak;
|
|
EditorDOMPoint mPoint;
|
|
AutoTrackDOMPoint mTracker;
|
|
};
|
|
|
|
/**
|
|
* 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:
|
|
using SimpleEditorDOMPoint = RangeUpdater::SimpleEditorDOMPoint;
|
|
|
|
AutoMoveNodeSelNotify() = delete;
|
|
explicit AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
|
|
const EditorRawDOMPoint& aExpectedDestination)
|
|
: mRangeUpdater(aRangeUpdater),
|
|
mExpectedDestination(aExpectedDestination.GetContainer(), nullptr,
|
|
aExpectedDestination.Offset()) {}
|
|
AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater, nsIContent& aContent,
|
|
const EditorRawDOMPoint& aExpectedDestination)
|
|
: mRangeUpdater(aRangeUpdater),
|
|
mExpectedDestination(aExpectedDestination.GetContainer(), nullptr,
|
|
aExpectedDestination.Offset()) {
|
|
if (aContent.GetParentNode()) {
|
|
mOldPoints.AppendElement(SimpleEditorDOMPoint(
|
|
&aContent, aContent.ComputeIndexInParentNode().valueOr(0)));
|
|
return;
|
|
}
|
|
mOldPoints.AppendElement(SimpleEditorDOMPoint(&aContent));
|
|
}
|
|
|
|
void AppendContentWhichWillBeMoved(nsIContent& aContent) {
|
|
if (!mOldPoints.IsEmpty() &&
|
|
mOldPoints.LastElement().GetNextSiblingOfChild() == &aContent) {
|
|
mOldPoints.AppendElement(SimpleEditorDOMPoint(
|
|
&aContent, mOldPoints.LastElement().Offset() + 1));
|
|
return;
|
|
}
|
|
if (aContent.GetParentNode()) {
|
|
mOldPoints.AppendElement(SimpleEditorDOMPoint(
|
|
&aContent, aContent.ComputeIndexInParentNode().valueOr(0)));
|
|
return;
|
|
}
|
|
mOldPoints.AppendElement(SimpleEditorDOMPoint(&aContent));
|
|
}
|
|
|
|
void DidMoveContent(nsIContent& aContent) {
|
|
if (!mNewPoints.IsEmpty() &&
|
|
mNewPoints.LastElement().GetNextSiblingOfChild() == &aContent) {
|
|
mNewPoints.AppendElement(SimpleEditorDOMPoint(
|
|
&aContent, mNewPoints.LastElement().Offset() + 1));
|
|
return;
|
|
}
|
|
// Compute offset when we need it.
|
|
mNewPoints.AppendElement(SimpleEditorDOMPoint(&aContent));
|
|
}
|
|
|
|
~AutoMoveNodeSelNotify() {
|
|
mRangeUpdater.DidMoveNodes(mOldPoints, mExpectedDestination, mNewPoints);
|
|
}
|
|
|
|
[[nodiscard]] size_t MovingContentCount() const {
|
|
return mOldPoints.Length();
|
|
}
|
|
[[nodiscard]] nsIContent* GetContentAt(size_t index) const {
|
|
return mOldPoints[index].mChild;
|
|
}
|
|
|
|
private:
|
|
RangeUpdater& mRangeUpdater;
|
|
SimpleEditorDOMPoint mExpectedDestination;
|
|
AutoTArray<SimpleEditorDOMPoint, 12> mOldPoints;
|
|
AutoTArray<SimpleEditorDOMPoint, 12> mNewPoints;
|
|
};
|
|
|
|
} // namespace mozilla
|
|
|
|
#endif // #ifndef mozilla_SelectionState_h
|