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