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"
13 #include "mozilla/dom/Document.h"
15 #include "nsDirection.h"
21 class nsCycleCollectionTraversalCallback
;
31 * A helper struct for saving/setting ranges.
33 struct RangeItem final
{
34 RangeItem() : mStartOffset(0), mEndOffset(0) {}
37 // Private destructor, to discourage deletion outside of Release():
38 ~RangeItem() = default;
41 void StoreRange(const nsRange
& aRange
);
42 void StoreRange(const EditorRawDOMPoint
& aStartPoint
,
43 const EditorRawDOMPoint
& aEndPoint
) {
44 MOZ_ASSERT(aStartPoint
.IsSet());
45 MOZ_ASSERT(aEndPoint
.IsSet());
46 mStartContainer
= aStartPoint
.GetContainer();
47 mStartOffset
= aStartPoint
.Offset();
48 mEndContainer
= aEndPoint
.GetContainer();
49 mEndOffset
= aEndPoint
.Offset();
52 mStartContainer
= mEndContainer
= nullptr;
53 mStartOffset
= mEndOffset
= 0;
55 already_AddRefed
<nsRange
> GetRange() const;
57 // Same as the API of dom::AbstractRange
58 [[nodiscard
]] nsINode
* GetRoot() const;
59 [[nodiscard
]] bool Collapsed() const {
60 return mStartContainer
== mEndContainer
&& mStartOffset
== mEndOffset
;
62 [[nodiscard
]] bool IsPositioned() const {
63 return mStartContainer
&& mEndContainer
;
65 [[nodiscard
]] bool Equals(const RangeItem
& aOther
) const {
66 return mStartContainer
== aOther
.mStartContainer
&&
67 mEndContainer
== aOther
.mEndContainer
&&
68 mStartOffset
== aOther
.mStartOffset
&&
69 mEndOffset
== aOther
.mEndOffset
;
71 template <typename EditorDOMPointType
= EditorDOMPoint
>
72 EditorDOMPointType
StartPoint() const {
73 return EditorDOMPointType(mStartContainer
, mStartOffset
);
75 template <typename EditorDOMPointType
= EditorDOMPoint
>
76 EditorDOMPointType
EndPoint() const {
77 return EditorDOMPointType(mEndContainer
, mEndOffset
);
80 NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem
)
81 NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem
)
83 nsCOMPtr
<nsINode
> mStartContainer
;
84 nsCOMPtr
<nsINode
> mEndContainer
;
85 uint32_t mStartOffset
;
90 * mozilla::SelectionState
92 * Class for recording selection info. Stores selection as collection of
93 * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
94 * ranges since dom gravity will possibly change the ranges.
97 class SelectionState final
{
99 SelectionState() = default;
100 explicit SelectionState(const AutoRangeArray
& aRanges
);
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 * Setting aRanges to have all ranges stored by this instance.
133 void ApplyTo(AutoRangeArray
& aRanges
);
136 * HasOnlyCollapsedRange() returns true only when there is a positioned range
137 * which is collapsed. I.e., the selection represents a caret point.
139 [[nodiscard
]] bool HasOnlyCollapsedRange() const {
140 if (mArray
.Length() != 1) {
143 if (!mArray
[0]->IsPositioned() || !mArray
[0]->Collapsed()) {
150 * Equals() returns true only when there are same number of ranges and
151 * all their containers and offsets are exactly same. This won't check
152 * the validity of each range with the current DOM tree.
154 [[nodiscard
]] bool Equals(const SelectionState
& aOther
) const;
157 * Returns common root node of all ranges' start and end containers.
158 * Some of them have different root nodes, this returns nullptr.
160 [[nodiscard
]] nsINode
* GetCommonRootNode() const {
161 nsINode
* rootNode
= nullptr;
162 for (const RefPtr
<RangeItem
>& rangeItem
: mArray
) {
163 nsINode
* newRootNode
= rangeItem
->GetRoot();
164 if (!newRootNode
|| (rootNode
&& rootNode
!= newRootNode
)) {
167 rootNode
= newRootNode
;
173 CopyableAutoTArray
<RefPtr
<RangeItem
>, 1> mArray
;
174 nsDirection mDirection
= eDirNext
;
176 friend class RangeUpdater
;
177 friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback
&,
178 SelectionState
&, const char*,
180 friend void ImplCycleCollectionUnlink(SelectionState
&);
183 inline void ImplCycleCollectionTraverse(
184 nsCycleCollectionTraversalCallback
& aCallback
, SelectionState
& aField
,
185 const char* aName
, uint32_t aFlags
= 0) {
186 ImplCycleCollectionTraverse(aCallback
, aField
.mArray
, aName
, aFlags
);
189 inline void ImplCycleCollectionUnlink(SelectionState
& aField
) {
190 ImplCycleCollectionUnlink(aField
.mArray
);
193 class MOZ_STACK_CLASS RangeUpdater final
{
197 void RegisterRangeItem(RangeItem
& aRangeItem
);
198 void DropRangeItem(RangeItem
& aRangeItem
);
199 void RegisterSelectionState(SelectionState
& aSelectionState
);
200 void DropSelectionState(SelectionState
& aSelectionState
);
202 // editor selection gravity routines. Note that we can't always depend on
203 // DOM Range gravity to do what we want to the "real" selection. For
204 // instance, if you move a node, that corresponds to deleting it and
205 // reinserting it. DOM Range gravity will promote the selection out of the
206 // node on deletion, which is not what you want if you know you are
208 template <typename PT
, typename CT
>
209 nsresult
SelAdjCreateNode(const EditorDOMPointBase
<PT
, CT
>& aPoint
);
210 template <typename PT
, typename CT
>
211 nsresult
SelAdjInsertNode(const EditorDOMPointBase
<PT
, CT
>& aPoint
);
212 void SelAdjDeleteNode(nsINode
& aNode
);
215 * SelAdjSplitNode() is called immediately after spliting aOriginalNode
216 * and inserted aNewContent into the DOM tree.
218 * @param aOriginalContent The node which was split.
219 * @param aSplitOffset The old offset in aOriginalContent at splitting
221 * @param aNewContent The new content node which was inserted into
224 nsresult
SelAdjSplitNode(nsIContent
& aOriginalContent
, uint32_t aSplitOffset
,
225 nsIContent
& aNewContent
);
228 * SelAdjJoinNodes() is called immediately after joining aRemovedContent and
229 * the container of aStartOfRightContent.
231 * @param aStartOfRightContent The container is joined content node which
232 * now has all children or text data which were
233 * in aRemovedContent. And this points where
234 * the joined position.
235 * @param aRemovedContent The removed content.
236 * @param aOldPointAtRightContent The point where the right content node was
237 * before joining them. The offset must have
238 * been initialized before the joining.
240 nsresult
SelAdjJoinNodes(const EditorRawDOMPoint
& aStartOfRightContent
,
241 const nsIContent
& aRemovedContent
,
242 const EditorDOMPoint
& aOldPointAtRightContent
);
243 void SelAdjInsertText(const dom::Text
& aTextNode
, uint32_t aOffset
,
244 uint32_t aInsertedLength
);
245 void SelAdjDeleteText(const dom::Text
& aTextNode
, uint32_t aOffset
,
246 uint32_t aDeletedLength
);
247 void SelAdjReplaceText(const dom::Text
& aTextNode
, uint32_t aOffset
,
248 uint32_t aReplacedLength
, uint32_t aInsertedLength
);
249 // the following gravity routines need will/did sandwiches, because the other
250 // gravity routines will be called inside of these sandwiches, but should be
252 void WillReplaceContainer() {
253 // XXX Isn't this possible with mutation event listener?
254 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
257 void DidReplaceContainer(const dom::Element
& aRemovedElement
,
258 dom::Element
& aInsertedElement
);
259 void WillRemoveContainer() {
260 // XXX Isn't this possible with mutation event listener?
261 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
264 void DidRemoveContainer(const dom::Element
& aRemovedElement
,
265 nsINode
& aRemovedElementContainerNode
,
266 uint32_t aOldOffsetOfRemovedElement
,
267 uint32_t aOldChildCountOfRemovedElement
);
268 void WillInsertContainer() {
269 // XXX Isn't this possible with mutation event listener?
270 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
273 void DidInsertContainer() {
274 NS_WARNING_ASSERTION(mLocked
, "Not locked");
277 void DidMoveNode(const nsINode
& aOldParent
, uint32_t aOldOffset
,
278 const nsINode
& aNewParent
, uint32_t aNewOffset
);
281 // TODO: A lot of loop in these methods check whether each item `nullptr` or
282 // not. We should make it not nullable later.
283 nsTArray
<RefPtr
<RangeItem
>> mArray
;
288 * Helper class for using SelectionState. Stack based class for doing
289 * preservation of dom points across editor actions.
292 class MOZ_STACK_CLASS AutoTrackDOMPoint final
{
294 AutoTrackDOMPoint() = delete;
295 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, nsCOMPtr
<nsINode
>* aNode
,
297 : mRangeUpdater(aRangeUpdater
),
300 mRangeItem(do_AddRef(new RangeItem())),
301 mWasConnected(aNode
&& (*aNode
)->IsInComposedDoc()) {
302 mRangeItem
->mStartContainer
= *mNode
;
303 mRangeItem
->mEndContainer
= *mNode
;
304 mRangeItem
->mStartOffset
= *mOffset
;
305 mRangeItem
->mEndOffset
= *mOffset
;
306 mDocument
= (*mNode
)->OwnerDoc();
307 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
310 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aPoint
)
311 : mRangeUpdater(aRangeUpdater
),
314 mPoint(Some(aPoint
->IsSet() ? aPoint
: nullptr)),
315 mRangeItem(do_AddRef(new RangeItem())),
316 mWasConnected(aPoint
&& aPoint
->IsInComposedDoc()) {
317 if (!aPoint
->IsSet()) {
319 return; // Nothing should be tracked.
321 mRangeItem
->mStartContainer
= aPoint
->GetContainer();
322 mRangeItem
->mEndContainer
= aPoint
->GetContainer();
323 mRangeItem
->mStartOffset
= aPoint
->Offset();
324 mRangeItem
->mEndOffset
= aPoint
->Offset();
325 mDocument
= aPoint
->GetContainer()->OwnerDoc();
326 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
329 ~AutoTrackDOMPoint() { FlushAndStopTracking(); }
331 void FlushAndStopTracking() {
336 if (mPoint
.isSome()) {
337 mRangeUpdater
.DropRangeItem(mRangeItem
);
338 // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
339 // and the number of times may be too many. (E.g., 1533913.html hits
340 // over 700 times!) We should just put warning instead.
341 if (NS_WARN_IF(!mRangeItem
->mStartContainer
)) {
342 mPoint
.ref()->Clear();
345 // If the node was removed from the original document, clear the instance
346 // since the user should not keep handling the adopted or orphan node
348 if (NS_WARN_IF(mWasConnected
&&
349 !mRangeItem
->mStartContainer
->IsInComposedDoc()) ||
350 NS_WARN_IF(mRangeItem
->mStartContainer
->OwnerDoc() != mDocument
)) {
351 mPoint
.ref()->Clear();
354 if (NS_WARN_IF(mRangeItem
->mStartContainer
->Length() <
355 mRangeItem
->mStartOffset
)) {
356 mPoint
.ref()->SetToEndOf(mRangeItem
->mStartContainer
);
359 mPoint
.ref()->Set(mRangeItem
->mStartContainer
, mRangeItem
->mStartOffset
);
362 mRangeUpdater
.DropRangeItem(mRangeItem
);
363 *mNode
= mRangeItem
->mStartContainer
;
364 *mOffset
= mRangeItem
->mStartOffset
;
368 // If the node was removed from the original document, clear the instances
369 // since the user should not keep handling the adopted or orphan node
371 if (NS_WARN_IF(mWasConnected
&& !(*mNode
)->IsInComposedDoc()) ||
372 NS_WARN_IF((*mNode
)->OwnerDoc() != mDocument
)) {
378 void StopTracking() { mIsTracking
= false; }
381 RangeUpdater
& mRangeUpdater
;
382 // Allow tracking nsINode until nsNode is gone
383 nsCOMPtr
<nsINode
>* mNode
;
385 Maybe
<EditorDOMPoint
*> mPoint
;
386 OwningNonNull
<RangeItem
> mRangeItem
;
387 RefPtr
<dom::Document
> mDocument
;
388 bool mIsTracking
= true;
392 class MOZ_STACK_CLASS AutoTrackDOMRange final
{
394 AutoTrackDOMRange() = delete;
395 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aStartPoint
,
396 EditorDOMPoint
* aEndPoint
)
397 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
398 mStartPointTracker
.emplace(aRangeUpdater
, aStartPoint
);
399 mEndPointTracker
.emplace(aRangeUpdater
, aEndPoint
);
401 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMRange
* aRange
)
402 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
403 mStartPointTracker
.emplace(
404 aRangeUpdater
, const_cast<EditorDOMPoint
*>(&aRange
->StartRef()));
405 mEndPointTracker
.emplace(aRangeUpdater
,
406 const_cast<EditorDOMPoint
*>(&aRange
->EndRef()));
408 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, RefPtr
<nsRange
>* aRange
)
409 : mStartPoint((*aRange
)->StartRef()),
410 mEndPoint((*aRange
)->EndRef()),
411 mRangeRefPtr(aRange
),
412 mRangeOwningNonNull(nullptr) {
413 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
414 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
416 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, OwningNonNull
<nsRange
>* aRange
)
417 : mStartPoint((*aRange
)->StartRef()),
418 mEndPoint((*aRange
)->EndRef()),
419 mRangeRefPtr(nullptr),
420 mRangeOwningNonNull(aRange
) {
421 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
422 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
424 ~AutoTrackDOMRange() { FlushAndStopTracking(); }
426 void FlushAndStopTracking() {
427 if (!mStartPointTracker
&& !mEndPointTracker
) {
430 mStartPointTracker
.reset();
431 mEndPointTracker
.reset();
432 if (!mRangeRefPtr
&& !mRangeOwningNonNull
) {
433 // This must be created with EditorDOMRange or EditorDOMPoints. In the
434 // cases, destroying mStartPointTracker and mEndPointTracker has done
435 // everything which we need to do.
438 // Otherwise, update the DOM ranges by ourselves.
440 if (!mStartPoint
.IsSet() || !mEndPoint
.IsSet()) {
441 (*mRangeRefPtr
)->Reset();
445 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
446 mEndPoint
.ToRawRangeBoundary());
449 if (mRangeOwningNonNull
) {
450 if (!mStartPoint
.IsSet() || !mEndPoint
.IsSet()) {
451 (*mRangeOwningNonNull
)->Reset();
454 (*mRangeOwningNonNull
)
455 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
456 mEndPoint
.ToRawRangeBoundary());
461 void StopTracking() {
462 if (mStartPointTracker
) {
463 mStartPointTracker
->StopTracking();
465 if (mEndPointTracker
) {
466 mEndPointTracker
->StopTracking();
469 void StopTrackingStartBoundary() {
470 MOZ_ASSERT(!mRangeRefPtr
,
471 "StopTrackingStartBoundary() is not available when tracking "
473 MOZ_ASSERT(!mRangeOwningNonNull
,
474 "StopTrackingStartBoundary() is not available when tracking "
475 "OwningNonNull<nsRange>");
476 if (!mStartPointTracker
) {
479 mStartPointTracker
->StopTracking();
481 void StopTrackingEndBoundary() {
482 MOZ_ASSERT(!mRangeRefPtr
,
483 "StopTrackingEndBoundary() is not available when tracking "
485 MOZ_ASSERT(!mRangeOwningNonNull
,
486 "StopTrackingEndBoundary() is not available when tracking "
487 "OwningNonNull<nsRange>");
488 if (!mEndPointTracker
) {
491 mEndPointTracker
->StopTracking();
495 Maybe
<AutoTrackDOMPoint
> mStartPointTracker
;
496 Maybe
<AutoTrackDOMPoint
> mEndPointTracker
;
497 EditorDOMPoint mStartPoint
;
498 EditorDOMPoint mEndPoint
;
499 RefPtr
<nsRange
>* mRangeRefPtr
;
500 OwningNonNull
<nsRange
>* mRangeOwningNonNull
;
504 * Another helper class for SelectionState. Stack based class for doing
505 * Will/DidReplaceContainer()
508 class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final
{
510 AutoReplaceContainerSelNotify() = delete;
511 // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
514 AutoReplaceContainerSelNotify(RangeUpdater
& aRangeUpdater
,
515 dom::Element
& aOriginalElement
,
516 dom::Element
& aNewElement
)
517 : mRangeUpdater(aRangeUpdater
),
518 mOriginalElement(aOriginalElement
),
519 mNewElement(aNewElement
) {
520 mRangeUpdater
.WillReplaceContainer();
523 ~AutoReplaceContainerSelNotify() {
524 mRangeUpdater
.DidReplaceContainer(mOriginalElement
, mNewElement
);
528 RangeUpdater
& mRangeUpdater
;
529 dom::Element
& mOriginalElement
;
530 dom::Element
& mNewElement
;
534 * Another helper class for SelectionState. Stack based class for doing
535 * Will/DidRemoveContainer()
538 class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final
{
540 AutoRemoveContainerSelNotify() = delete;
541 AutoRemoveContainerSelNotify(RangeUpdater
& aRangeUpdater
,
542 const EditorRawDOMPoint
& aAtRemovingElement
)
543 : mRangeUpdater(aRangeUpdater
),
544 mRemovingElement(*aAtRemovingElement
.GetChild()->AsElement()),
545 mParentNode(*aAtRemovingElement
.GetContainer()),
546 mOffsetInParent(aAtRemovingElement
.Offset()),
547 mChildCountOfRemovingElement(mRemovingElement
->GetChildCount()) {
548 MOZ_ASSERT(aAtRemovingElement
.IsSet());
549 mRangeUpdater
.WillRemoveContainer();
552 ~AutoRemoveContainerSelNotify() {
553 mRangeUpdater
.DidRemoveContainer(mRemovingElement
, mParentNode
,
555 mChildCountOfRemovingElement
);
559 RangeUpdater
& mRangeUpdater
;
560 OwningNonNull
<dom::Element
> mRemovingElement
;
561 OwningNonNull
<nsINode
> mParentNode
;
562 uint32_t mOffsetInParent
;
563 uint32_t mChildCountOfRemovingElement
;
567 * Another helper class for SelectionState. Stack based class for doing
568 * Will/DidInsertContainer()
569 * XXX The lock state isn't useful if the edit action is triggered from
570 * a mutation event listener so that looks like that we can remove
574 class MOZ_STACK_CLASS AutoInsertContainerSelNotify final
{
576 RangeUpdater
& mRangeUpdater
;
579 AutoInsertContainerSelNotify() = delete;
580 explicit AutoInsertContainerSelNotify(RangeUpdater
& aRangeUpdater
)
581 : mRangeUpdater(aRangeUpdater
) {
582 mRangeUpdater
.WillInsertContainer();
585 ~AutoInsertContainerSelNotify() { mRangeUpdater
.DidInsertContainer(); }
589 * Another helper class for SelectionState. Stack based class for doing
593 class MOZ_STACK_CLASS AutoMoveNodeSelNotify final
{
595 AutoMoveNodeSelNotify() = delete;
596 AutoMoveNodeSelNotify(RangeUpdater
& aRangeUpdater
,
597 const EditorRawDOMPoint
& aOldPoint
,
598 const EditorRawDOMPoint
& aNewPoint
)
599 : mRangeUpdater(aRangeUpdater
),
600 mOldParent(*aOldPoint
.GetContainer()),
601 mNewParent(*aNewPoint
.GetContainer()),
602 mOldOffset(aOldPoint
.Offset()),
603 mNewOffset(aNewPoint
.Offset()) {
604 MOZ_ASSERT(aOldPoint
.IsSet());
605 MOZ_ASSERT(aNewPoint
.IsSet());
608 ~AutoMoveNodeSelNotify() {
609 mRangeUpdater
.DidMoveNode(mOldParent
, mOldOffset
, mNewParent
, mNewOffset
);
613 RangeUpdater
& mRangeUpdater
;
616 const uint32_t mOldOffset
;
617 const uint32_t mNewOffset
;
620 } // namespace mozilla
622 #endif // #ifndef mozilla_SelectionState_h