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 DidMoveNode(const nsINode
& aOldParent
, uint32_t aOldOffset
,
276 const nsINode
& aNewParent
, uint32_t aNewOffset
);
279 // TODO: A lot of loop in these methods check whether each item `nullptr` or
280 // not. We should make it not nullable later.
281 nsTArray
<RefPtr
<RangeItem
>> mArray
;
286 * Helper class for using SelectionState. Stack based class for doing
287 * preservation of dom points across editor actions.
290 class MOZ_STACK_CLASS AutoTrackDOMPoint final
{
292 AutoTrackDOMPoint() = delete;
293 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, nsCOMPtr
<nsINode
>* aNode
,
295 : mRangeUpdater(aRangeUpdater
),
298 mRangeItem(do_AddRef(new RangeItem())) {
299 mRangeItem
->mStartContainer
= *mNode
;
300 mRangeItem
->mEndContainer
= *mNode
;
301 mRangeItem
->mStartOffset
= *mOffset
;
302 mRangeItem
->mEndOffset
= *mOffset
;
303 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
306 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aPoint
)
307 : mRangeUpdater(aRangeUpdater
),
310 mPoint(Some(aPoint
->IsSet() ? aPoint
: nullptr)),
311 mRangeItem(do_AddRef(new RangeItem())) {
312 if (!aPoint
->IsSet()) {
313 return; // Nothing should be tracked.
315 mRangeItem
->mStartContainer
= aPoint
->GetContainer();
316 mRangeItem
->mEndContainer
= aPoint
->GetContainer();
317 mRangeItem
->mStartOffset
= aPoint
->Offset();
318 mRangeItem
->mEndOffset
= aPoint
->Offset();
319 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
322 ~AutoTrackDOMPoint() {
323 if (mPoint
.isSome()) {
325 return; // We don't track anything.
327 mRangeUpdater
.DropRangeItem(mRangeItem
);
328 // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
329 // and the number of times may be too many. (E.g., 1533913.html hits
330 // over 700 times!) We should just put warning instead.
331 if (NS_WARN_IF(!mRangeItem
->mStartContainer
)) {
332 mPoint
.ref()->Clear();
335 if (NS_WARN_IF(mRangeItem
->mStartContainer
->Length() <
336 mRangeItem
->mStartOffset
)) {
337 mPoint
.ref()->SetToEndOf(mRangeItem
->mStartContainer
);
340 mPoint
.ref()->Set(mRangeItem
->mStartContainer
, mRangeItem
->mStartOffset
);
343 mRangeUpdater
.DropRangeItem(mRangeItem
);
344 *mNode
= mRangeItem
->mStartContainer
;
345 *mOffset
= mRangeItem
->mStartOffset
;
349 RangeUpdater
& mRangeUpdater
;
350 // Allow tracking nsINode until nsNode is gone
351 nsCOMPtr
<nsINode
>* mNode
;
353 Maybe
<EditorDOMPoint
*> mPoint
;
354 OwningNonNull
<RangeItem
> mRangeItem
;
357 class MOZ_STACK_CLASS AutoTrackDOMRange final
{
359 AutoTrackDOMRange() = delete;
360 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aStartPoint
,
361 EditorDOMPoint
* aEndPoint
)
362 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
363 mStartPointTracker
.emplace(aRangeUpdater
, aStartPoint
);
364 mEndPointTracker
.emplace(aRangeUpdater
, aEndPoint
);
366 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMRange
* aRange
)
367 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
368 mStartPointTracker
.emplace(
369 aRangeUpdater
, const_cast<EditorDOMPoint
*>(&aRange
->StartRef()));
370 mEndPointTracker
.emplace(aRangeUpdater
,
371 const_cast<EditorDOMPoint
*>(&aRange
->EndRef()));
373 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, RefPtr
<nsRange
>* aRange
)
374 : mStartPoint((*aRange
)->StartRef()),
375 mEndPoint((*aRange
)->EndRef()),
376 mRangeRefPtr(aRange
),
377 mRangeOwningNonNull(nullptr) {
378 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
379 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
381 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, OwningNonNull
<nsRange
>* aRange
)
382 : mStartPoint((*aRange
)->StartRef()),
383 mEndPoint((*aRange
)->EndRef()),
384 mRangeRefPtr(nullptr),
385 mRangeOwningNonNull(aRange
) {
386 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
387 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
389 ~AutoTrackDOMRange() {
390 if (!mRangeRefPtr
&& !mRangeOwningNonNull
) {
391 // The destructor of the trackers will update automatically.
394 // Otherwise, destroy them now.
395 mStartPointTracker
.reset();
396 mEndPointTracker
.reset();
399 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
400 mEndPoint
.ToRawRangeBoundary());
403 if (mRangeOwningNonNull
) {
404 (*mRangeOwningNonNull
)
405 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
406 mEndPoint
.ToRawRangeBoundary());
412 Maybe
<AutoTrackDOMPoint
> mStartPointTracker
;
413 Maybe
<AutoTrackDOMPoint
> mEndPointTracker
;
414 EditorDOMPoint mStartPoint
;
415 EditorDOMPoint mEndPoint
;
416 RefPtr
<nsRange
>* mRangeRefPtr
;
417 OwningNonNull
<nsRange
>* mRangeOwningNonNull
;
421 * Another helper class for SelectionState. Stack based class for doing
422 * Will/DidReplaceContainer()
425 class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final
{
427 AutoReplaceContainerSelNotify() = delete;
428 // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
431 AutoReplaceContainerSelNotify(RangeUpdater
& aRangeUpdater
,
432 dom::Element
& aOriginalElement
,
433 dom::Element
& aNewElement
)
434 : mRangeUpdater(aRangeUpdater
),
435 mOriginalElement(aOriginalElement
),
436 mNewElement(aNewElement
) {
437 mRangeUpdater
.WillReplaceContainer();
440 ~AutoReplaceContainerSelNotify() {
441 mRangeUpdater
.DidReplaceContainer(mOriginalElement
, mNewElement
);
445 RangeUpdater
& mRangeUpdater
;
446 dom::Element
& mOriginalElement
;
447 dom::Element
& mNewElement
;
451 * Another helper class for SelectionState. Stack based class for doing
452 * Will/DidRemoveContainer()
455 class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final
{
457 AutoRemoveContainerSelNotify() = delete;
458 AutoRemoveContainerSelNotify(RangeUpdater
& aRangeUpdater
,
459 const EditorRawDOMPoint
& aAtRemovingElement
)
460 : mRangeUpdater(aRangeUpdater
),
461 mRemovingElement(*aAtRemovingElement
.GetChild()->AsElement()),
462 mParentNode(*aAtRemovingElement
.GetContainer()),
463 mOffsetInParent(aAtRemovingElement
.Offset()),
464 mChildCountOfRemovingElement(mRemovingElement
->GetChildCount()) {
465 MOZ_ASSERT(aAtRemovingElement
.IsSet());
466 mRangeUpdater
.WillRemoveContainer();
469 ~AutoRemoveContainerSelNotify() {
470 mRangeUpdater
.DidRemoveContainer(mRemovingElement
, mParentNode
,
472 mChildCountOfRemovingElement
);
476 RangeUpdater
& mRangeUpdater
;
477 OwningNonNull
<dom::Element
> mRemovingElement
;
478 OwningNonNull
<nsINode
> mParentNode
;
479 uint32_t mOffsetInParent
;
480 uint32_t mChildCountOfRemovingElement
;
484 * Another helper class for SelectionState. Stack based class for doing
485 * Will/DidInsertContainer()
486 * XXX The lock state isn't useful if the edit action is triggered from
487 * a mutation event listener so that looks like that we can remove
491 class MOZ_STACK_CLASS AutoInsertContainerSelNotify final
{
493 RangeUpdater
& mRangeUpdater
;
496 AutoInsertContainerSelNotify() = delete;
497 explicit AutoInsertContainerSelNotify(RangeUpdater
& aRangeUpdater
)
498 : mRangeUpdater(aRangeUpdater
) {
499 mRangeUpdater
.WillInsertContainer();
502 ~AutoInsertContainerSelNotify() { mRangeUpdater
.DidInsertContainer(); }
506 * Another helper class for SelectionState. Stack based class for doing
510 class MOZ_STACK_CLASS AutoMoveNodeSelNotify final
{
512 AutoMoveNodeSelNotify() = delete;
513 AutoMoveNodeSelNotify(RangeUpdater
& aRangeUpdater
,
514 const EditorRawDOMPoint
& aOldPoint
,
515 const EditorRawDOMPoint
& aNewPoint
)
516 : mRangeUpdater(aRangeUpdater
),
517 mOldParent(*aOldPoint
.GetContainer()),
518 mNewParent(*aNewPoint
.GetContainer()),
519 mOldOffset(aOldPoint
.Offset()),
520 mNewOffset(aNewPoint
.Offset()) {
521 MOZ_ASSERT(aOldPoint
.IsSet());
522 MOZ_ASSERT(aNewPoint
.IsSet());
525 ~AutoMoveNodeSelNotify() {
526 mRangeUpdater
.DidMoveNode(mOldParent
, mOldOffset
, mNewParent
, mNewOffset
);
530 RangeUpdater
& mRangeUpdater
;
533 const uint32_t mOldOffset
;
534 const uint32_t mNewOffset
;
537 } // namespace mozilla
539 #endif // #ifndef mozilla_SelectionState_h