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/. */
5 
6 #ifndef mozilla_SelectionState_h
7 #define mozilla_SelectionState_h
8 
9 #include "mozilla/EditorDOMPoint.h"
10 #include "mozilla/Maybe.h"
11 #include "mozilla/OwningNonNull.h"
12 #include "nsCOMPtr.h"
13 #include "nsDirection.h"
14 #include "nsINode.h"
15 #include "nsRange.h"
16 #include "nsTArray.h"
17 #include "nscore.h"
18 
19 class nsCycleCollectionTraversalCallback;
20 class nsRange;
21 namespace mozilla {
22 class RangeUpdater;
23 namespace dom {
24 class Element;
25 class Selection;
26 class Text;
27 }  // namespace dom
28 
29 /**
30  * A helper struct for saving/setting ranges.
31  */
32 struct RangeItem final {
RangeItemfinal33   RangeItem() : mStartOffset(0), mEndOffset(0) {}
34 
35  private:
36   // Private destructor, to discourage deletion outside of Release():
37   ~RangeItem() = default;
38 
39  public:
40   void StoreRange(const nsRange& aRange);
StoreRangefinal41   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();
49   }
Clearfinal50   void Clear() {
51     mStartContainer = mEndContainer = nullptr;
52     mStartOffset = mEndOffset = 0;
53   }
54   already_AddRefed<nsRange> GetRange();
IsCollapsedfinal55   bool IsCollapsed() const {
56     return mStartContainer == mEndContainer && mStartOffset == mEndOffset;
57   }
IsSetfinal58   bool IsSet() const { return mStartContainer && mEndContainer; }
StartPointfinal59   EditorDOMPoint StartPoint() const {
60     return EditorDOMPoint(mStartContainer, mStartOffset);
61   }
EndPointfinal62   EditorDOMPoint EndPoint() const {
63     return EditorDOMPoint(mEndContainer, mEndOffset);
64   }
StartRawPointfinal65   EditorRawDOMPoint StartRawPoint() const {
66     return EditorRawDOMPoint(mStartContainer, mStartOffset);
67   }
EndRawPointfinal68   EditorRawDOMPoint EndRawPoint() const {
69     return EditorRawDOMPoint(mEndContainer, mEndOffset);
70   }
71 
72   NS_INLINE_DECL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem)
73   NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem)
74 
75   nsCOMPtr<nsINode> mStartContainer;
76   nsCOMPtr<nsINode> mEndContainer;
77   uint32_t mStartOffset;
78   uint32_t mEndOffset;
79 };
80 
81 /**
82  * mozilla::SelectionState
83  *
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.
87  */
88 
89 class SelectionState final {
90  public:
91   SelectionState();
~SelectionState()92   ~SelectionState() { Clear(); }
93 
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;
99   void Clear();
100   bool IsEmpty() const;
101 
102  private:
103   CopyableAutoTArray<RefPtr<RangeItem>, 1> mArray;
104   nsDirection mDirection;
105 
106   friend class RangeUpdater;
107   friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
108                                           SelectionState&, const char*,
109                                           uint32_t);
110   friend void ImplCycleCollectionUnlink(SelectionState&);
111 };
112 
113 inline void ImplCycleCollectionTraverse(
114     nsCycleCollectionTraversalCallback& aCallback, SelectionState& aField,
115     const char* aName, uint32_t aFlags = 0) {
116   ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
117 }
118 
ImplCycleCollectionUnlink(SelectionState & aField)119 inline void ImplCycleCollectionUnlink(SelectionState& aField) {
120   ImplCycleCollectionUnlink(aField.mArray);
121 }
122 
123 class MOZ_STACK_CLASS RangeUpdater final {
124  public:
125   RangeUpdater();
126 
127   void RegisterRangeItem(RangeItem& aRangeItem);
128   void DropRangeItem(RangeItem& aRangeItem);
129   void RegisterSelectionState(SelectionState& aSelectionState);
130   void DropSelectionState(SelectionState& aSelectionState);
131 
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
137   // reinserting it.
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
155   // ignored.
WillReplaceContainer()156   void WillReplaceContainer() {
157     // XXX Isn't this possible with mutation event listener?
158     NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
159     mLocked = true;
160   }
161   void DidReplaceContainer(const dom::Element& aRemovedElement,
162                            dom::Element& aInsertedElement);
WillRemoveContainer()163   void WillRemoveContainer() {
164     // XXX Isn't this possible with mutation event listener?
165     NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
166     mLocked = true;
167   }
168   void DidRemoveContainer(const dom::Element& aRemovedElement,
169                           nsINode& aRemovedElementContainerNode,
170                           uint32_t aOldOffsetOfRemovedElement,
171                           uint32_t aOldChildCountOfRemovedElement);
WillInsertContainer()172   void WillInsertContainer() {
173     // XXX Isn't this possible with mutation event listener?
174     NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
175     mLocked = true;
176   }
DidInsertContainer()177   void DidInsertContainer() {
178     NS_WARNING_ASSERTION(mLocked, "Not locked");
179     mLocked = false;
180   }
WillMoveNode()181   void WillMoveNode() { mLocked = true; }
182   void DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
183                    const nsINode& aNewParent, uint32_t aNewOffset);
184 
185  private:
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;
189   bool mLocked;
190 };
191 
192 /**
193  * Helper class for using SelectionState.  Stack based class for doing
194  * preservation of dom points across editor actions.
195  */
196 
197 class MOZ_STACK_CLASS AutoTrackDOMPoint final {
198  public:
199   AutoTrackDOMPoint() = delete;
AutoTrackDOMPoint(RangeUpdater & aRangeUpdater,nsCOMPtr<nsINode> * aNode,uint32_t * aOffset)200   AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, nsCOMPtr<nsINode>* aNode,
201                     uint32_t* aOffset)
202       : mRangeUpdater(aRangeUpdater),
203         mNode(aNode),
204         mOffset(aOffset),
205         mRangeItem(do_AddRef(new RangeItem())) {
206     mRangeItem->mStartContainer = *mNode;
207     mRangeItem->mEndContainer = *mNode;
208     mRangeItem->mStartOffset = *mOffset;
209     mRangeItem->mEndOffset = *mOffset;
210     mRangeUpdater.RegisterRangeItem(mRangeItem);
211   }
212 
AutoTrackDOMPoint(RangeUpdater & aRangeUpdater,EditorDOMPoint * aPoint)213   AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, EditorDOMPoint* aPoint)
214       : mRangeUpdater(aRangeUpdater),
215         mNode(nullptr),
216         mOffset(nullptr),
217         mPoint(Some(aPoint->IsSet() ? aPoint : nullptr)),
218         mRangeItem(do_AddRef(new RangeItem())) {
219     if (!aPoint->IsSet()) {
220       return;  // Nothing should be tracked.
221     }
222     mRangeItem->mStartContainer = aPoint->GetContainer();
223     mRangeItem->mEndContainer = aPoint->GetContainer();
224     mRangeItem->mStartOffset = aPoint->Offset();
225     mRangeItem->mEndOffset = aPoint->Offset();
226     mRangeUpdater.RegisterRangeItem(mRangeItem);
227   }
228 
~AutoTrackDOMPoint()229   ~AutoTrackDOMPoint() {
230     if (mPoint.isSome()) {
231       if (!mPoint.ref()) {
232         return;  // We don't track anything.
233       }
234       mRangeUpdater.DropRangeItem(mRangeItem);
235       // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
236       // and the number of times may be too many.  (E.g., 1533913.html hits
237       // over 700 times!)  We should just put warning instead.
238       if (NS_WARN_IF(!mRangeItem->mStartContainer)) {
239         mPoint.ref()->Clear();
240         return;
241       }
242       if (NS_WARN_IF(mRangeItem->mStartContainer->Length() <
243                      mRangeItem->mStartOffset)) {
244         mPoint.ref()->SetToEndOf(mRangeItem->mStartContainer);
245         return;
246       }
247       mPoint.ref()->Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset);
248       return;
249     }
250     mRangeUpdater.DropRangeItem(mRangeItem);
251     *mNode = mRangeItem->mStartContainer;
252     *mOffset = mRangeItem->mStartOffset;
253   }
254 
255  private:
256   RangeUpdater& mRangeUpdater;
257   // Allow tracking nsINode until nsNode is gone
258   nsCOMPtr<nsINode>* mNode;
259   uint32_t* mOffset;
260   Maybe<EditorDOMPoint*> mPoint;
261   OwningNonNull<RangeItem> mRangeItem;
262 };
263 
264 class MOZ_STACK_CLASS AutoTrackDOMRange final {
265  public:
266   AutoTrackDOMRange() = delete;
AutoTrackDOMRange(RangeUpdater & aRangeUpdater,EditorDOMPoint * aStartPoint,EditorDOMPoint * aEndPoint)267   AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMPoint* aStartPoint,
268                     EditorDOMPoint* aEndPoint)
269       : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
270     mStartPointTracker.emplace(aRangeUpdater, aStartPoint);
271     mEndPointTracker.emplace(aRangeUpdater, aEndPoint);
272   }
AutoTrackDOMRange(RangeUpdater & aRangeUpdater,EditorDOMRange * aRange)273   AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMRange* aRange)
274       : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
275     mStartPointTracker.emplace(
276         aRangeUpdater, const_cast<EditorDOMPoint*>(&aRange->StartRef()));
277     mEndPointTracker.emplace(aRangeUpdater,
278                              const_cast<EditorDOMPoint*>(&aRange->EndRef()));
279   }
AutoTrackDOMRange(RangeUpdater & aRangeUpdater,RefPtr<nsRange> * aRange)280   AutoTrackDOMRange(RangeUpdater& aRangeUpdater, RefPtr<nsRange>* aRange)
281       : mStartPoint((*aRange)->StartRef()),
282         mEndPoint((*aRange)->EndRef()),
283         mRangeRefPtr(aRange),
284         mRangeOwningNonNull(nullptr) {
285     mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
286     mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
287   }
AutoTrackDOMRange(RangeUpdater & aRangeUpdater,OwningNonNull<nsRange> * aRange)288   AutoTrackDOMRange(RangeUpdater& aRangeUpdater, OwningNonNull<nsRange>* aRange)
289       : mStartPoint((*aRange)->StartRef()),
290         mEndPoint((*aRange)->EndRef()),
291         mRangeRefPtr(nullptr),
292         mRangeOwningNonNull(aRange) {
293     mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
294     mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
295   }
~AutoTrackDOMRange()296   ~AutoTrackDOMRange() {
297     if (!mRangeRefPtr && !mRangeOwningNonNull) {
298       // The destructor of the trackers will update automatically.
299       return;
300     }
301     // Otherwise, destroy them now.
302     mStartPointTracker.reset();
303     mEndPointTracker.reset();
304     if (mRangeRefPtr) {
305       (*mRangeRefPtr)
306           ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
307                            mEndPoint.ToRawRangeBoundary());
308       return;
309     }
310     if (mRangeOwningNonNull) {
311       (*mRangeOwningNonNull)
312           ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
313                            mEndPoint.ToRawRangeBoundary());
314       return;
315     }
316   }
317 
318  private:
319   Maybe<AutoTrackDOMPoint> mStartPointTracker;
320   Maybe<AutoTrackDOMPoint> mEndPointTracker;
321   EditorDOMPoint mStartPoint;
322   EditorDOMPoint mEndPoint;
323   RefPtr<nsRange>* mRangeRefPtr;
324   OwningNonNull<nsRange>* mRangeOwningNonNull;
325 };
326 
327 /**
328  * Another helper class for SelectionState.  Stack based class for doing
329  * Will/DidReplaceContainer()
330  */
331 
332 class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final {
333  public:
334   AutoReplaceContainerSelNotify() = delete;
335   // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
336   //      for the members.
337   MOZ_CAN_RUN_SCRIPT
AutoReplaceContainerSelNotify(RangeUpdater & aRangeUpdater,dom::Element & aOriginalElement,dom::Element & aNewElement)338   AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater,
339                                 dom::Element& aOriginalElement,
340                                 dom::Element& aNewElement)
341       : mRangeUpdater(aRangeUpdater),
342         mOriginalElement(aOriginalElement),
343         mNewElement(aNewElement) {
344     mRangeUpdater.WillReplaceContainer();
345   }
346 
~AutoReplaceContainerSelNotify()347   ~AutoReplaceContainerSelNotify() {
348     mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement);
349   }
350 
351  private:
352   RangeUpdater& mRangeUpdater;
353   dom::Element& mOriginalElement;
354   dom::Element& mNewElement;
355 };
356 
357 /**
358  * Another helper class for SelectionState.  Stack based class for doing
359  * Will/DidRemoveContainer()
360  */
361 
362 class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final {
363  public:
364   AutoRemoveContainerSelNotify() = delete;
AutoRemoveContainerSelNotify(RangeUpdater & aRangeUpdater,const EditorDOMPoint & aAtRemovingElement)365   AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater,
366                                const EditorDOMPoint& aAtRemovingElement)
367       : mRangeUpdater(aRangeUpdater),
368         mRemovingElement(*aAtRemovingElement.GetChild()->AsElement()),
369         mParentNode(*aAtRemovingElement.GetContainer()),
370         mOffsetInParent(aAtRemovingElement.Offset()),
371         mChildCountOfRemovingElement(mRemovingElement->GetChildCount()) {
372     MOZ_ASSERT(aAtRemovingElement.IsSet());
373     mRangeUpdater.WillRemoveContainer();
374   }
375 
~AutoRemoveContainerSelNotify()376   ~AutoRemoveContainerSelNotify() {
377     mRangeUpdater.DidRemoveContainer(mRemovingElement, mParentNode,
378                                      mOffsetInParent,
379                                      mChildCountOfRemovingElement);
380   }
381 
382  private:
383   RangeUpdater& mRangeUpdater;
384   OwningNonNull<dom::Element> mRemovingElement;
385   OwningNonNull<nsINode> mParentNode;
386   uint32_t mOffsetInParent;
387   uint32_t mChildCountOfRemovingElement;
388 };
389 
390 /**
391  * Another helper class for SelectionState.  Stack based class for doing
392  * Will/DidInsertContainer()
393  * XXX The lock state isn't useful if the edit action is triggered from
394  *     a mutation event listener so that looks like that we can remove
395  *     this class.
396  */
397 
398 class MOZ_STACK_CLASS AutoInsertContainerSelNotify final {
399  private:
400   RangeUpdater& mRangeUpdater;
401 
402  public:
403   AutoInsertContainerSelNotify() = delete;
AutoInsertContainerSelNotify(RangeUpdater & aRangeUpdater)404   explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater)
405       : mRangeUpdater(aRangeUpdater) {
406     mRangeUpdater.WillInsertContainer();
407   }
408 
~AutoInsertContainerSelNotify()409   ~AutoInsertContainerSelNotify() { mRangeUpdater.DidInsertContainer(); }
410 };
411 
412 /**
413  * Another helper class for SelectionState.  Stack based class for doing
414  * Will/DidMoveNode()
415  */
416 
417 class MOZ_STACK_CLASS AutoMoveNodeSelNotify final {
418  public:
419   AutoMoveNodeSelNotify() = delete;
AutoMoveNodeSelNotify(RangeUpdater & aRangeUpdater,const EditorDOMPoint & aOldPoint,const EditorDOMPoint & aNewPoint)420   AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
421                         const EditorDOMPoint& aOldPoint,
422                         const EditorDOMPoint& aNewPoint)
423       : mRangeUpdater(aRangeUpdater),
424         mOldParent(*aOldPoint.GetContainer()),
425         mNewParent(*aNewPoint.GetContainer()),
426         mOldOffset(aOldPoint.Offset()),
427         mNewOffset(aNewPoint.Offset()) {
428     MOZ_ASSERT(aOldPoint.IsSet());
429     MOZ_ASSERT(aNewPoint.IsSet());
430     mRangeUpdater.WillMoveNode();
431   }
432 
~AutoMoveNodeSelNotify()433   ~AutoMoveNodeSelNotify() {
434     mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset);
435   }
436 
ComputeInsertionPoint()437   EditorRawDOMPoint ComputeInsertionPoint() const {
438     if (&mOldParent == &mNewParent && mOldOffset < mNewOffset) {
439       return EditorRawDOMPoint(&mNewParent, mNewOffset - 1);
440     }
441     return EditorRawDOMPoint(&mNewParent, mNewOffset);
442   }
443 
444  private:
445   RangeUpdater& mRangeUpdater;
446   nsINode& mOldParent;
447   nsINode& mNewParent;
448   uint32_t mOldOffset;
449   uint32_t mNewOffset;
450 };
451 
452 }  // namespace mozilla
453 
454 #endif  // #ifndef mozilla_SelectionState_h
455