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/Maybe.h"
11 #include "mozilla/OwningNonNull.h"
13 #include "nsDirection.h"
19 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();
55 bool IsCollapsed() const {
56 return mStartContainer
== mEndContainer
&& mStartOffset
== mEndOffset
;
58 bool IsSet() const { return mStartContainer
&& mEndContainer
; }
59 EditorDOMPoint
StartPoint() const {
60 return EditorDOMPoint(mStartContainer
, mStartOffset
);
62 EditorDOMPoint
EndPoint() const {
63 return EditorDOMPoint(mEndContainer
, mEndOffset
);
65 EditorRawDOMPoint
StartRawPoint() const {
66 return EditorRawDOMPoint(mStartContainer
, mStartOffset
);
68 EditorRawDOMPoint
EndRawPoint() const {
69 return EditorRawDOMPoint(mEndContainer
, mEndOffset
);
72 NS_INLINE_DECL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem
)
73 NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem
)
75 nsCOMPtr
<nsINode
> mStartContainer
;
76 nsCOMPtr
<nsINode
> mEndContainer
;
77 uint32_t mStartOffset
;
82 * mozilla::SelectionState
84 * Class for recording selection info. Stores selection as collection of
85 * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
86 * ranges since dom gravity will possibly change the ranges.
89 class SelectionState final
{
92 ~SelectionState() { Clear(); }
94 void SaveSelection(dom::Selection
& aSelection
);
95 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
96 RestoreSelection(dom::Selection
& aSelection
);
97 bool IsCollapsed() const;
98 bool Equals(SelectionState
& aOther
) const;
100 bool IsEmpty() const;
103 CopyableAutoTArray
<RefPtr
<RangeItem
>, 1> mArray
;
104 nsDirection mDirection
;
106 friend class RangeUpdater
;
107 friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback
&,
108 SelectionState
&, const char*,
110 friend void ImplCycleCollectionUnlink(SelectionState
&);
113 inline void ImplCycleCollectionTraverse(
114 nsCycleCollectionTraversalCallback
& aCallback
, SelectionState
& aField
,
115 const char* aName
, uint32_t aFlags
= 0) {
116 ImplCycleCollectionTraverse(aCallback
, aField
.mArray
, aName
, aFlags
);
119 inline void ImplCycleCollectionUnlink(SelectionState
& aField
) {
120 ImplCycleCollectionUnlink(aField
.mArray
);
123 class MOZ_STACK_CLASS RangeUpdater final
{
127 void RegisterRangeItem(RangeItem
& aRangeItem
);
128 void DropRangeItem(RangeItem
& aRangeItem
);
129 void RegisterSelectionState(SelectionState
& aSelectionState
);
130 void DropSelectionState(SelectionState
& aSelectionState
);
132 // editor selection gravity routines. Note that we can't always depend on
133 // DOM Range gravity to do what we want to the "real" selection. For
134 // instance, if you move a node, that corresponds to deleting it and
135 // reinserting it. DOM Range gravity will promote the selection out of the
136 // node on deletion, which is not what you want if you know you are
138 template <typename PT
, typename CT
>
139 nsresult
SelAdjCreateNode(const EditorDOMPointBase
<PT
, CT
>& aPoint
);
140 template <typename PT
, typename CT
>
141 nsresult
SelAdjInsertNode(const EditorDOMPointBase
<PT
, CT
>& aPoint
);
142 void SelAdjDeleteNode(nsINode
& aNode
);
143 nsresult
SelAdjSplitNode(nsIContent
& aRightNode
, nsIContent
& aNewLeftNode
);
144 nsresult
SelAdjJoinNodes(nsINode
& aLeftNode
, nsINode
& aRightNode
,
145 nsINode
& aParent
, uint32_t aOffset
,
146 uint32_t aOldLeftNodeLength
);
147 void SelAdjInsertText(const dom::Text
& aTextNode
, uint32_t aOffset
,
148 uint32_t aInsertedLength
);
149 void SelAdjDeleteText(const dom::Text
& aTextNode
, uint32_t aOffset
,
150 uint32_t aDeletedLength
);
151 void SelAdjReplaceText(const dom::Text
& aTextNode
, uint32_t aOffset
,
152 uint32_t aReplacedLength
, uint32_t aInsertedLength
);
153 // the following gravity routines need will/did sandwiches, because the other
154 // gravity routines will be called inside of these sandwiches, but should be
156 void WillReplaceContainer() {
157 // XXX Isn't this possible with mutation event listener?
158 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
161 void DidReplaceContainer(const dom::Element
& aRemovedElement
,
162 dom::Element
& aInsertedElement
);
163 void WillRemoveContainer() {
164 // XXX Isn't this possible with mutation event listener?
165 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
168 void DidRemoveContainer(const dom::Element
& aRemovedElement
,
169 nsINode
& aRemovedElementContainerNode
,
170 uint32_t aOldOffsetOfRemovedElement
,
171 uint32_t aOldChildCountOfRemovedElement
);
172 void WillInsertContainer() {
173 // XXX Isn't this possible with mutation event listener?
174 NS_WARNING_ASSERTION(!mLocked
, "Has already been locked");
177 void DidInsertContainer() {
178 NS_WARNING_ASSERTION(mLocked
, "Not locked");
181 void WillMoveNode() { mLocked
= true; }
182 void DidMoveNode(const nsINode
& aOldParent
, uint32_t aOldOffset
,
183 const nsINode
& aNewParent
, uint32_t aNewOffset
);
186 // TODO: A lot of loop in these methods check whether each item `nullptr` or
187 // not. We should make it not nullable later.
188 nsTArray
<RefPtr
<RangeItem
>> mArray
;
193 * Helper class for using SelectionState. Stack based class for doing
194 * preservation of dom points across editor actions.
197 class MOZ_STACK_CLASS AutoTrackDOMPoint final
{
199 AutoTrackDOMPoint() = delete;
200 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, nsCOMPtr
<nsINode
>* aNode
,
202 : mRangeUpdater(aRangeUpdater
),
206 mRangeItem(do_AddRef(new RangeItem())) {
207 mRangeItem
->mStartContainer
= *mNode
;
208 mRangeItem
->mEndContainer
= *mNode
;
209 mRangeItem
->mStartOffset
= *mOffset
;
210 mRangeItem
->mEndOffset
= *mOffset
;
211 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
214 AutoTrackDOMPoint(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aPoint
)
215 : mRangeUpdater(aRangeUpdater
),
219 mRangeItem(do_AddRef(new RangeItem())) {
220 mRangeItem
->mStartContainer
= mPoint
->GetContainer();
221 mRangeItem
->mEndContainer
= mPoint
->GetContainer();
222 mRangeItem
->mStartOffset
= mPoint
->Offset();
223 mRangeItem
->mEndOffset
= mPoint
->Offset();
224 mRangeUpdater
.RegisterRangeItem(mRangeItem
);
227 ~AutoTrackDOMPoint() {
228 mRangeUpdater
.DropRangeItem(mRangeItem
);
230 // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
231 // and the number of times may be too many. (E.g., 1533913.html hits
232 // over 700 times!) We should just put warning instead.
233 if (NS_WARN_IF(!mRangeItem
->mStartContainer
)) {
237 if (NS_WARN_IF(mRangeItem
->mStartContainer
->Length() <
238 mRangeItem
->mStartOffset
)) {
239 mPoint
->SetToEndOf(mRangeItem
->mStartContainer
);
242 mPoint
->Set(mRangeItem
->mStartContainer
, mRangeItem
->mStartOffset
);
245 *mNode
= mRangeItem
->mStartContainer
;
246 *mOffset
= mRangeItem
->mStartOffset
;
250 RangeUpdater
& mRangeUpdater
;
251 // Allow tracking nsINode until nsNode is gone
252 nsCOMPtr
<nsINode
>* mNode
;
254 EditorDOMPoint
* mPoint
;
255 OwningNonNull
<RangeItem
> mRangeItem
;
258 class MOZ_STACK_CLASS AutoTrackDOMRange final
{
260 AutoTrackDOMRange() = delete;
261 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMPoint
* aStartPoint
,
262 EditorDOMPoint
* aEndPoint
)
263 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
264 mStartPointTracker
.emplace(aRangeUpdater
, aStartPoint
);
265 mEndPointTracker
.emplace(aRangeUpdater
, aEndPoint
);
267 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, EditorDOMRange
* aRange
)
268 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
269 mStartPointTracker
.emplace(
270 aRangeUpdater
, const_cast<EditorDOMPoint
*>(&aRange
->StartRef()));
271 mEndPointTracker
.emplace(aRangeUpdater
,
272 const_cast<EditorDOMPoint
*>(&aRange
->EndRef()));
274 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, RefPtr
<nsRange
>* aRange
)
275 : mStartPoint((*aRange
)->StartRef()),
276 mEndPoint((*aRange
)->EndRef()),
277 mRangeRefPtr(aRange
),
278 mRangeOwningNonNull(nullptr) {
279 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
280 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
282 AutoTrackDOMRange(RangeUpdater
& aRangeUpdater
, OwningNonNull
<nsRange
>* aRange
)
283 : mStartPoint((*aRange
)->StartRef()),
284 mEndPoint((*aRange
)->EndRef()),
285 mRangeRefPtr(nullptr),
286 mRangeOwningNonNull(aRange
) {
287 mStartPointTracker
.emplace(aRangeUpdater
, &mStartPoint
);
288 mEndPointTracker
.emplace(aRangeUpdater
, &mEndPoint
);
290 ~AutoTrackDOMRange() {
291 if (!mRangeRefPtr
&& !mRangeOwningNonNull
) {
292 // The destructor of the trackers will update automatically.
295 // Otherwise, destroy them now.
296 mStartPointTracker
.reset();
297 mEndPointTracker
.reset();
300 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
301 mEndPoint
.ToRawRangeBoundary());
304 if (mRangeOwningNonNull
) {
305 (*mRangeOwningNonNull
)
306 ->SetStartAndEnd(mStartPoint
.ToRawRangeBoundary(),
307 mEndPoint
.ToRawRangeBoundary());
313 Maybe
<AutoTrackDOMPoint
> mStartPointTracker
;
314 Maybe
<AutoTrackDOMPoint
> mEndPointTracker
;
315 EditorDOMPoint mStartPoint
;
316 EditorDOMPoint mEndPoint
;
317 RefPtr
<nsRange
>* mRangeRefPtr
;
318 OwningNonNull
<nsRange
>* mRangeOwningNonNull
;
322 * Another helper class for SelectionState. Stack based class for doing
323 * Will/DidReplaceContainer()
326 class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final
{
328 AutoReplaceContainerSelNotify() = delete;
329 // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
332 AutoReplaceContainerSelNotify(RangeUpdater
& aRangeUpdater
,
333 dom::Element
& aOriginalElement
,
334 dom::Element
& aNewElement
)
335 : mRangeUpdater(aRangeUpdater
),
336 mOriginalElement(aOriginalElement
),
337 mNewElement(aNewElement
) {
338 mRangeUpdater
.WillReplaceContainer();
341 ~AutoReplaceContainerSelNotify() {
342 mRangeUpdater
.DidReplaceContainer(mOriginalElement
, mNewElement
);
346 RangeUpdater
& mRangeUpdater
;
347 dom::Element
& mOriginalElement
;
348 dom::Element
& mNewElement
;
352 * Another helper class for SelectionState. Stack based class for doing
353 * Will/DidRemoveContainer()
356 class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final
{
358 AutoRemoveContainerSelNotify() = delete;
359 AutoRemoveContainerSelNotify(RangeUpdater
& aRangeUpdater
,
360 const EditorDOMPoint
& aAtRemovingElement
)
361 : mRangeUpdater(aRangeUpdater
),
362 mRemovingElement(*aAtRemovingElement
.GetChild()->AsElement()),
363 mParentNode(*aAtRemovingElement
.GetContainer()),
364 mOffsetInParent(aAtRemovingElement
.Offset()),
365 mChildCountOfRemovingElement(mRemovingElement
->GetChildCount()) {
366 MOZ_ASSERT(aAtRemovingElement
.IsSet());
367 mRangeUpdater
.WillRemoveContainer();
370 ~AutoRemoveContainerSelNotify() {
371 mRangeUpdater
.DidRemoveContainer(mRemovingElement
, mParentNode
,
373 mChildCountOfRemovingElement
);
377 RangeUpdater
& mRangeUpdater
;
378 OwningNonNull
<dom::Element
> mRemovingElement
;
379 OwningNonNull
<nsINode
> mParentNode
;
380 uint32_t mOffsetInParent
;
381 uint32_t mChildCountOfRemovingElement
;
385 * Another helper class for SelectionState. Stack based class for doing
386 * Will/DidInsertContainer()
387 * XXX The lock state isn't useful if the edit action is triggered from
388 * a mutation event listener so that looks like that we can remove
392 class MOZ_STACK_CLASS AutoInsertContainerSelNotify final
{
394 RangeUpdater
& mRangeUpdater
;
397 AutoInsertContainerSelNotify() = delete;
398 explicit AutoInsertContainerSelNotify(RangeUpdater
& aRangeUpdater
)
399 : mRangeUpdater(aRangeUpdater
) {
400 mRangeUpdater
.WillInsertContainer();
403 ~AutoInsertContainerSelNotify() { mRangeUpdater
.DidInsertContainer(); }
407 * Another helper class for SelectionState. Stack based class for doing
411 class MOZ_STACK_CLASS AutoMoveNodeSelNotify final
{
413 AutoMoveNodeSelNotify() = delete;
414 AutoMoveNodeSelNotify(RangeUpdater
& aRangeUpdater
,
415 const EditorDOMPoint
& aOldPoint
,
416 const EditorDOMPoint
& aNewPoint
)
417 : mRangeUpdater(aRangeUpdater
),
418 mOldParent(*aOldPoint
.GetContainer()),
419 mNewParent(*aNewPoint
.GetContainer()),
420 mOldOffset(aOldPoint
.Offset()),
421 mNewOffset(aNewPoint
.Offset()) {
422 MOZ_ASSERT(aOldPoint
.IsSet());
423 MOZ_ASSERT(aNewPoint
.IsSet());
424 mRangeUpdater
.WillMoveNode();
427 ~AutoMoveNodeSelNotify() {
428 mRangeUpdater
.DidMoveNode(mOldParent
, mOldOffset
, mNewParent
, mNewOffset
);
431 EditorRawDOMPoint
ComputeInsertionPoint() const {
432 if (&mOldParent
== &mNewParent
&& mOldOffset
< mNewOffset
) {
433 return EditorRawDOMPoint(&mNewParent
, mNewOffset
- 1);
435 return EditorRawDOMPoint(&mNewParent
, mNewOffset
);
439 RangeUpdater
& mRangeUpdater
;
446 } // namespace mozilla
448 #endif // #ifndef mozilla_SelectionState_h