1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #ifndef mozilla_SelectionState_h
7 #define mozilla_SelectionState_h
9 #include "mozilla/EditorDOMPoint.h"
10 #include "mozilla/EditorForwards.h"
11 #include "mozilla/Maybe.h"
12 #include "mozilla/OwningNonNull.h"
14 #include "nsDirection.h"
20 class nsCycleCollectionTraversalCallback
;
30 * A helper struct for saving/setting ranges.
32 struct RangeItem final
{
33 RangeItem() : mStartOffset(0), mEndOffset(0) {}
36 // Private destructor, to discourage deletion outside of Release():
37 ~RangeItem() = default;
40 void StoreRange(const nsRange
& aRange
);
41 void StoreRange(const EditorRawDOMPoint
& aStartPoint
,
42 const EditorRawDOMPoint
& aEndPoint
) {
43 MOZ_ASSERT(aStartPoint
.IsSet());
44 MOZ_ASSERT(aEndPoint
.IsSet());
45 mStartContainer
= aStartPoint
.GetContainer();
46 mStartOffset
= aStartPoint
.Offset();
47 mEndContainer
= aEndPoint
.GetContainer();
48 mEndOffset
= aEndPoint
.Offset();
51 mStartContainer
= mEndContainer
= nullptr;
52 mStartOffset
= mEndOffset
= 0;
54 already_AddRefed
<nsRange
> GetRange() const;
56 // Same as the API of dom::AbstractRange
57 [[nodiscard
]] nsINode
* GetRoot() const;
58 [[nodiscard
]] bool Collapsed() const {
59 return mStartContainer
== mEndContainer
&& mStartOffset
== mEndOffset
;
61 [[nodiscard
]] bool IsPositioned() const {
62 return mStartContainer
&& mEndContainer
;
64 [[nodiscard
]] bool Equals(const RangeItem
& aOther
) const {
65 return mStartContainer
== aOther
.mStartContainer
&&
66 mEndContainer
== aOther
.mEndContainer
&&
67 mStartOffset
== aOther
.mStartOffset
&&
68 mEndOffset
== aOther
.mEndOffset
;
70 EditorDOMPoint
StartPoint() const {
71 return EditorDOMPoint(mStartContainer
, mStartOffset
);
73 EditorDOMPoint
EndPoint() const {
74 return EditorDOMPoint(mEndContainer
, mEndOffset
);
76 EditorRawDOMPoint
StartRawPoint() const {
77 return EditorRawDOMPoint(mStartContainer
, mStartOffset
);
79 EditorRawDOMPoint
EndRawPoint() const {
80 return EditorRawDOMPoint(mEndContainer
, mEndOffset
);
83 NS_INLINE_DECL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem
)
84 NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem
)
86 nsCOMPtr
<nsINode
> mStartContainer
;
87 nsCOMPtr
<nsINode
> mEndContainer
;
88 uint32_t mStartOffset
;
93 * mozilla::SelectionState
95 * Class for recording selection info. Stores selection as collection of
96 * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
97 * ranges since dom gravity will possibly change the ranges.
100 class SelectionState final
{
103 * Same as the API as dom::Selection
105 [[nodiscard
]] bool IsCollapsed() const {
106 if (mArray
.Length() != 1) {
109 return mArray
[0]->Collapsed();
112 void RemoveAllRanges() {
114 mDirection
= eDirNext
;
117 [[nodiscard
]] uint32_t RangeCount() const { return mArray
.Length(); }
120 * Saving all ranges of aSelection.
122 void SaveSelection(dom::Selection
& aSelection
);
125 * Setting aSelection to have all ranges stored by this instance.
127 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
128 RestoreSelection(dom::Selection
& aSelection
);
131 * HasOnlyCollapsedRange() returns true only when there is a positioned range
132 * which is collapsed. I.e., the selection represents a caret point.
134 [[nodiscard
]] bool HasOnlyCollapsedRange() const {
135 if (mArray
.Length() != 1) {
138 if (!mArray
[0]->IsPositioned() || !mArray
[0]->Collapsed()) {
145 * Equals() returns true only when there are same number of ranges and
146 * all their containers and offsets are exactly same. This won't check
147 * the validity of each range with the current DOM tree.
149 [[nodiscard
]] bool Equals(const SelectionState
& aOther
) const;
152 * Returns common root node of all ranges' start and end containers.
153 * Some of them have different root nodes, this returns nullptr.
155 [[nodiscard
]] nsINode
* GetCommonRootNode() const {
156 nsINode
* rootNode
= nullptr;
157 for (const RefPtr
<RangeItem
>& rangeItem
: mArray
) {
158 nsINode
* newRootNode
= rangeItem
->GetRoot();
159 if (!newRootNode
|| (rootNode
&& rootNode
!= newRootNode
)) {
162 rootNode
= newRootNode
;
168 CopyableAutoTArray
<RefPtr
<RangeItem
>, 1> mArray
;
169 nsDirection mDirection
= eDirNext
;
171 friend class RangeUpdater
;
172 friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback
&,
173 SelectionState
&, const char*,
175 friend void ImplCycleCollectionUnlink(SelectionState
&);
178 inline void ImplCycleCollectionTraverse(
179 nsCycleCollectionTraversalCallback
& aCallback
, SelectionState
& aField
,
180 const char* aName
, uint32_t aFlags
= 0) {
181 ImplCycleCollectionTraverse(aCallback
, aField
.mArray
, aName
, aFlags
);
184 inline void ImplCycleCollectionUnlink(SelectionState
& aField
) {
185 ImplCycleCollectionUnlink(aField
.mArray
);
188 class MOZ_STACK_CLASS RangeUpdater final
{
192 void RegisterRangeItem(RangeItem
& aRangeItem
);
193 void DropRangeItem(RangeItem
& aRangeItem
);
194 void RegisterSelectionState(SelectionState
& aSelectionState
);
195 void DropSelectionState(SelectionState
& aSelectionState
);
197 // editor selection gravity routines. Note that we can't always depend on
198 // DOM Range gravity to do what we want to the "real" selection. For
199 // instance, if you move a node, that corresponds to deleting it and
200 // reinserting it. DOM Range gravity will promote the selection out of the
201 // node on deletion, which is not what you want if you know you are
203 template <typename PT
, typename CT
>
204 nsresult
SelAdjCreateNode(const EditorDOMPointBase
<PT
, CT
>& aPoint
);
205 template <typename PT
, typename CT
>
206 nsresult
SelAdjInsertNode(const EditorDOMPointBase
<PT
, CT
>& aPoint
);
207 void SelAdjDeleteNode(nsINode
& aNode
);
210 * SelAdjSplitNode() is called immediately after spliting aOriginalNode
211 * and inserted aNewContent into the DOM tree.
213 * @param aOriginalContent The node which was split.
214 * @param aSplitOffset The old offset in aOriginalContent at splitting
216 * @param aNewContent The new content node which was inserted into
218 * @param aSplitNodeDirection Whether aNewNode was inserted before or after
221 nsresult
SelAdjSplitNode(nsIContent
& aOriginalContent
, uint32_t aSplitOffset
,
222 nsIContent
& aNewContent
,
223 SplitNodeDirection aSplitNodeDirection
);
226 * SelAdjJoinNodes() is called immediately after joining aRemovedContent and
227 * the container of aStartOfRightContent.
229 * @param aStartOfRightContent The container is joined content node which
230 * now has all children or text data which were
231 * in aRemovedContent. And this points where
232 * the joined position.
233 * @param aRemovedContent The removed content.
234 * @param aOffsetOfRemovedContent The offset which aRemovedContent was in
237 nsresult
SelAdjJoinNodes(const EditorRawDOMPoint
& aStartOfRightContent
,
238 const nsIContent
& aRemovedContent
,
239 uint32_t aOffsetOfRemovedContent
,
240 JoinNodesDirection aJoinNodesDirection
);
241 void SelAdjInsertText(const dom::Text
& aTextNode
, uint32_t aOffset
,
242 uint32_t aInsertedLength
);
243 void SelAdjDeleteText(const dom::Text
& aTextNode
, uint32_t aOffset
,
244 uint32_t aDeletedLength
);
245 void SelAdjReplaceText(const dom::Text
& aTextNode
, uint32_t aOffset
,
246 uint32_t aReplacedLength
, uint32_t aInsertedLength
);
247 // the following gravity routines need will/did sandwiches, because the other
248 // gravity routines will be called inside of these sandwiches, but should be
250 void WillReplaceContainer() {
251 // XXX Isn't this possible with mutation event listener?
252 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
255 void DidReplaceContainer(const dom::Element
& aRemovedElement
,
256 dom::Element
& aInsertedElement
);
257 void WillRemoveContainer() {
258 // XXX Isn't this possible with mutation event listener?
259 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
262 void DidRemoveContainer(const dom::Element
& aRemovedElement
,
263 nsINode
& aRemovedElementContainerNode
,
264 uint32_t aOldOffsetOfRemovedElement
,
265 uint32_t aOldChildCountOfRemovedElement
);
266 void WillInsertContainer() {
267 // XXX Isn't this possible with mutation event listener?
268 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
271 void DidInsertContainer() {
272 NS_WARNING_ASSERTION(mLocked
, "Not locked");
275 void WillMoveNode() { mLocked
= true; }
276 void DidMoveNode(const nsINode
& aOldParent
, uint32_t aOldOffset
,
277 const nsINode
& aNewParent
, uint32_t aNewOffset
);
280 // TODO: A lot of loop in these methods check whether each item `nullptr` or
281 // not. We should make it not nullable later.
282 nsTArray
<RefPtr
<RangeItem
>> mArray
;
287 * Helper class for using SelectionState. Stack based class for doing
288 * preservation of dom points across editor actions.
291 class MOZ_STACK_CLASS AutoTrackDOMPoint final
{
293 AutoTrackDOMPoint() = delete;
294 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, nsCOMPtr
<nsINode
>* aNode
,
296 : mRangeUpdater(aRangeUpdater
),
299 mRangeItem(do_AddRef(new RangeItem())) {
300 mRangeItem
->mStartContainer
= *mNode
;
301 mRangeItem
->mEndContainer
= *mNode
;
302 mRangeItem
->mStartOffset
= *mOffset
;
303 mRangeItem
->mEndOffset
= *mOffset
;
304 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
307 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aPoint
)
308 : mRangeUpdater(aRangeUpdater
),
311 mPoint(Some(aPoint
->IsSet() ? aPoint
: nullptr)),
312 mRangeItem(do_AddRef(new RangeItem())) {
313 if (!aPoint
->IsSet()) {
314 return; // Nothing should be tracked.
316 mRangeItem
->mStartContainer
= aPoint
->GetContainer();
317 mRangeItem
->mEndContainer
= aPoint
->GetContainer();
318 mRangeItem
->mStartOffset
= aPoint
->Offset();
319 mRangeItem
->mEndOffset
= aPoint
->Offset();
320 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
323 ~AutoTrackDOMPoint() {
324 if (mPoint
.isSome()) {
326 return; // We don't track anything.
328 mRangeUpdater
.DropRangeItem(mRangeItem
);
329 // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
330 // and the number of times may be too many. (E.g., 1533913.html hits
331 // over 700 times!) We should just put warning instead.
332 if (NS_WARN_IF(!mRangeItem
->mStartContainer
)) {
333 mPoint
.ref()->Clear();
336 if (NS_WARN_IF(mRangeItem
->mStartContainer
->Length() <
337 mRangeItem
->mStartOffset
)) {
338 mPoint
.ref()->SetToEndOf(mRangeItem
->mStartContainer
);
341 mPoint
.ref()->Set(mRangeItem
->mStartContainer
, mRangeItem
->mStartOffset
);
344 mRangeUpdater
.DropRangeItem(mRangeItem
);
345 *mNode
= mRangeItem
->mStartContainer
;
346 *mOffset
= mRangeItem
->mStartOffset
;
350 RangeUpdater
& mRangeUpdater
;
351 // Allow tracking nsINode until nsNode is gone
352 nsCOMPtr
<nsINode
>* mNode
;
354 Maybe
<EditorDOMPoint
*> mPoint
;
355 OwningNonNull
<RangeItem
> mRangeItem
;
358 class MOZ_STACK_CLASS AutoTrackDOMRange final
{
360 AutoTrackDOMRange() = delete;
361 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aStartPoint
,
362 EditorDOMPoint
* aEndPoint
)
363 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
364 mStartPointTracker
.emplace(aRangeUpdater
, aStartPoint
);
365 mEndPointTracker
.emplace(aRangeUpdater
, aEndPoint
);
367 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMRange
* aRange
)
368 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
369 mStartPointTracker
.emplace(
370 aRangeUpdater
, const_cast<EditorDOMPoint
*>(&aRange
->StartRef()));
371 mEndPointTracker
.emplace(aRangeUpdater
,
372 const_cast<EditorDOMPoint
*>(&aRange
->EndRef()));
374 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, RefPtr
<nsRange
>* aRange
)
375 : mStartPoint((*aRange
)->StartRef()),
376 mEndPoint((*aRange
)->EndRef()),
377 mRangeRefPtr(aRange
),
378 mRangeOwningNonNull(nullptr) {
379 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
380 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
382 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, OwningNonNull
<nsRange
>* aRange
)
383 : mStartPoint((*aRange
)->StartRef()),
384 mEndPoint((*aRange
)->EndRef()),
385 mRangeRefPtr(nullptr),
386 mRangeOwningNonNull(aRange
) {
387 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
388 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
390 ~AutoTrackDOMRange() {
391 if (!mRangeRefPtr
&& !mRangeOwningNonNull
) {
392 // The destructor of the trackers will update automatically.
395 // Otherwise, destroy them now.
396 mStartPointTracker
.reset();
397 mEndPointTracker
.reset();
400 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
401 mEndPoint
.ToRawRangeBoundary());
404 if (mRangeOwningNonNull
) {
405 (*mRangeOwningNonNull
)
406 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
407 mEndPoint
.ToRawRangeBoundary());
413 Maybe
<AutoTrackDOMPoint
> mStartPointTracker
;
414 Maybe
<AutoTrackDOMPoint
> mEndPointTracker
;
415 EditorDOMPoint mStartPoint
;
416 EditorDOMPoint mEndPoint
;
417 RefPtr
<nsRange
>* mRangeRefPtr
;
418 OwningNonNull
<nsRange
>* mRangeOwningNonNull
;
422 * Another helper class for SelectionState. Stack based class for doing
423 * Will/DidReplaceContainer()
426 class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final
{
428 AutoReplaceContainerSelNotify() = delete;
429 // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
432 AutoReplaceContainerSelNotify(RangeUpdater
& aRangeUpdater
,
433 dom::Element
& aOriginalElement
,
434 dom::Element
& aNewElement
)
435 : mRangeUpdater(aRangeUpdater
),
436 mOriginalElement(aOriginalElement
),
437 mNewElement(aNewElement
) {
438 mRangeUpdater
.WillReplaceContainer();
441 ~AutoReplaceContainerSelNotify() {
442 mRangeUpdater
.DidReplaceContainer(mOriginalElement
, mNewElement
);
446 RangeUpdater
& mRangeUpdater
;
447 dom::Element
& mOriginalElement
;
448 dom::Element
& mNewElement
;
452 * Another helper class for SelectionState. Stack based class for doing
453 * Will/DidRemoveContainer()
456 class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final
{
458 AutoRemoveContainerSelNotify() = delete;
459 AutoRemoveContainerSelNotify(RangeUpdater
& aRangeUpdater
,
460 const EditorDOMPoint
& aAtRemovingElement
)
461 : mRangeUpdater(aRangeUpdater
),
462 mRemovingElement(*aAtRemovingElement
.GetChild()->AsElement()),
463 mParentNode(*aAtRemovingElement
.GetContainer()),
464 mOffsetInParent(aAtRemovingElement
.Offset()),
465 mChildCountOfRemovingElement(mRemovingElement
->GetChildCount()) {
466 MOZ_ASSERT(aAtRemovingElement
.IsSet());
467 mRangeUpdater
.WillRemoveContainer();
470 ~AutoRemoveContainerSelNotify() {
471 mRangeUpdater
.DidRemoveContainer(mRemovingElement
, mParentNode
,
473 mChildCountOfRemovingElement
);
477 RangeUpdater
& mRangeUpdater
;
478 OwningNonNull
<dom::Element
> mRemovingElement
;
479 OwningNonNull
<nsINode
> mParentNode
;
480 uint32_t mOffsetInParent
;
481 uint32_t mChildCountOfRemovingElement
;
485 * Another helper class for SelectionState. Stack based class for doing
486 * Will/DidInsertContainer()
487 * XXX The lock state isn't useful if the edit action is triggered from
488 * a mutation event listener so that looks like that we can remove
492 class MOZ_STACK_CLASS AutoInsertContainerSelNotify final
{
494 RangeUpdater
& mRangeUpdater
;
497 AutoInsertContainerSelNotify() = delete;
498 explicit AutoInsertContainerSelNotify(RangeUpdater
& aRangeUpdater
)
499 : mRangeUpdater(aRangeUpdater
) {
500 mRangeUpdater
.WillInsertContainer();
503 ~AutoInsertContainerSelNotify() { mRangeUpdater
.DidInsertContainer(); }
507 * Another helper class for SelectionState. Stack based class for doing
511 class MOZ_STACK_CLASS AutoMoveNodeSelNotify final
{
513 AutoMoveNodeSelNotify() = delete;
514 AutoMoveNodeSelNotify(RangeUpdater
& aRangeUpdater
,
515 const EditorDOMPoint
& aOldPoint
,
516 const EditorDOMPoint
& aNewPoint
)
517 : mRangeUpdater(aRangeUpdater
),
518 mOldParent(*aOldPoint
.GetContainer()),
519 mNewParent(*aNewPoint
.GetContainer()),
520 mOldOffset(aOldPoint
.Offset()),
521 mNewOffset(aNewPoint
.Offset()) {
522 MOZ_ASSERT(aOldPoint
.IsSet());
523 MOZ_ASSERT(aNewPoint
.IsSet());
524 mRangeUpdater
.WillMoveNode();
527 ~AutoMoveNodeSelNotify() {
528 mRangeUpdater
.DidMoveNode(mOldParent
, mOldOffset
, mNewParent
, mNewOffset
);
531 template <typename EditorDOMPointType
>
532 EditorDOMPointType
ComputeInsertionPoint() const {
533 if (&mOldParent
== &mNewParent
&& mOldOffset
< mNewOffset
) {
534 return EditorDOMPointType(&mNewParent
, mNewOffset
- 1);
536 return EditorDOMPointType(&mNewParent
, mNewOffset
);
540 RangeUpdater
& mRangeUpdater
;
547 } // namespace mozilla
549 #endif // #ifndef mozilla_SelectionState_h