Bug 1772588 [wpt PR 34302] - [wpt] Add test for block-in-inline offsetParent., a...
[gecko.git] / editor / libeditor / SelectionState.cpp
blob42179428422c067741f36330bb59a3dce2c9cb04
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 #include "SelectionState.h"
8 #include "EditorUtils.h" // for EditorUtils
9 #include "HTMLEditHelpers.h" // for JoinNodesDirection, SplitNodeDirection
11 #include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
12 #include "mozilla/IntegerRange.h" // for IntegerRange
13 #include "mozilla/Likely.h" // For MOZ_LIKELY and MOZ_UNLIKELY
14 #include "mozilla/RangeUtils.h" // for RangeUtils
15 #include "mozilla/dom/RangeBinding.h"
16 #include "mozilla/dom/Selection.h" // for Selection
17 #include "nsAString.h" // for nsAString::Length
18 #include "nsCycleCollectionParticipant.h"
19 #include "nsDebug.h" // for NS_WARNING, etc.
20 #include "nsError.h" // for NS_OK, etc.
21 #include "nsIContent.h" // for nsIContent
22 #include "nsISupportsImpl.h" // for nsRange::Release
23 #include "nsRange.h" // for nsRange
25 namespace mozilla {
27 using namespace dom;
29 /*****************************************************************************
30 * mozilla::RangeItem
31 *****************************************************************************/
33 nsINode* RangeItem::GetRoot() const {
34 if (MOZ_UNLIKELY(!IsPositioned())) {
35 return nullptr;
37 nsINode* rootNode = RangeUtils::ComputeRootNode(mStartContainer);
38 if (mStartContainer == mEndContainer) {
39 return rootNode;
41 return MOZ_LIKELY(rootNode == RangeUtils::ComputeRootNode(mEndContainer))
42 ? rootNode
43 : nullptr;
46 /******************************************************************************
47 * mozilla::SelectionState
49 * Class for recording selection info. Stores selection as collection of
50 * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
51 * ranges since dom gravity will possibly change the ranges.
52 ******************************************************************************/
54 template nsresult RangeUpdater::SelAdjCreateNode(const EditorDOMPoint& aPoint);
55 template nsresult RangeUpdater::SelAdjCreateNode(
56 const EditorRawDOMPoint& aPoint);
57 template nsresult RangeUpdater::SelAdjInsertNode(const EditorDOMPoint& aPoint);
58 template nsresult RangeUpdater::SelAdjInsertNode(
59 const EditorRawDOMPoint& aPoint);
61 void SelectionState::SaveSelection(Selection& aSelection) {
62 // if we need more items in the array, new them
63 if (mArray.Length() < aSelection.RangeCount()) {
64 for (uint32_t i = mArray.Length(); i < aSelection.RangeCount(); i++) {
65 mArray.AppendElement();
66 mArray[i] = new RangeItem();
68 } else if (mArray.Length() > aSelection.RangeCount()) {
69 // else if we have too many, delete them
70 mArray.TruncateLength(aSelection.RangeCount());
73 // now store the selection ranges
74 const uint32_t rangeCount = aSelection.RangeCount();
75 for (const uint32_t i : IntegerRange(rangeCount)) {
76 MOZ_ASSERT(aSelection.RangeCount() == rangeCount);
77 const nsRange* range = aSelection.GetRangeAt(i);
78 MOZ_ASSERT(range);
79 if (MOZ_UNLIKELY(NS_WARN_IF(!range))) {
80 continue;
82 mArray[i]->StoreRange(*range);
85 mDirection = aSelection.GetDirection();
88 nsresult SelectionState::RestoreSelection(Selection& aSelection) {
89 // clear out selection
90 IgnoredErrorResult ignoredError;
91 aSelection.RemoveAllRanges(ignoredError);
92 NS_WARNING_ASSERTION(!ignoredError.Failed(),
93 "Selection::RemoveAllRanges() failed, but ignored");
95 aSelection.SetDirection(mDirection);
97 ErrorResult error;
98 const CopyableAutoTArray<RefPtr<RangeItem>, 10> rangeItems(mArray);
99 for (const RefPtr<RangeItem>& rangeItem : rangeItems) {
100 RefPtr<nsRange> range = rangeItem->GetRange();
101 if (!range) {
102 NS_WARNING("RangeItem::GetRange() failed");
103 return NS_ERROR_FAILURE;
105 aSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, error);
106 if (error.Failed()) {
107 NS_WARNING(
108 "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
109 return error.StealNSResult();
112 return NS_OK;
115 bool SelectionState::Equals(const SelectionState& aOther) const {
116 if (mArray.Length() != aOther.mArray.Length()) {
117 return false;
119 if (mArray.IsEmpty()) {
120 return false; // XXX Why?
122 if (mDirection != aOther.mDirection) {
123 return false;
126 for (uint32_t i : IntegerRange(mArray.Length())) {
127 if (NS_WARN_IF(!mArray[i]) || NS_WARN_IF(!aOther.mArray[i]) ||
128 !mArray[i]->Equals(*aOther.mArray[i])) {
129 return false;
132 // if we got here, they are equal
133 return true;
136 /******************************************************************************
137 * mozilla::RangeUpdater
139 * Class for updating nsRanges in response to editor actions.
140 ******************************************************************************/
142 RangeUpdater::RangeUpdater() : mLocked(false) {}
144 void RangeUpdater::RegisterRangeItem(RangeItem& aRangeItem) {
145 if (mArray.Contains(&aRangeItem)) {
146 NS_ERROR("tried to register an already registered range");
147 return; // don't register it again. It would get doubly adjusted.
149 mArray.AppendElement(&aRangeItem);
152 void RangeUpdater::DropRangeItem(RangeItem& aRangeItem) {
153 NS_WARNING_ASSERTION(
154 mArray.Contains(&aRangeItem),
155 "aRangeItem is not in the range, but tried to removed from it");
156 mArray.RemoveElement(&aRangeItem);
159 void RangeUpdater::RegisterSelectionState(SelectionState& aSelectionState) {
160 for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
161 if (NS_WARN_IF(!rangeItem)) {
162 continue;
164 RegisterRangeItem(*rangeItem);
168 void RangeUpdater::DropSelectionState(SelectionState& aSelectionState) {
169 for (RefPtr<RangeItem>& rangeItem : aSelectionState.mArray) {
170 if (NS_WARN_IF(!rangeItem)) {
171 continue;
173 DropRangeItem(*rangeItem);
177 // gravity methods:
179 template <typename PT, typename CT>
180 nsresult RangeUpdater::SelAdjCreateNode(
181 const EditorDOMPointBase<PT, CT>& aPoint) {
182 if (mLocked) {
183 // lock set by Will/DidReplaceParent, etc...
184 return NS_OK;
186 if (mArray.IsEmpty()) {
187 return NS_OK;
190 if (NS_WARN_IF(!aPoint.IsSetAndValid())) {
191 return NS_ERROR_INVALID_ARG;
194 for (RefPtr<RangeItem>& rangeItem : mArray) {
195 if (NS_WARN_IF(!rangeItem)) {
196 return NS_ERROR_FAILURE;
198 if (rangeItem->mStartContainer == aPoint.GetContainer() &&
199 rangeItem->mStartOffset > aPoint.Offset()) {
200 rangeItem->mStartOffset++;
202 if (rangeItem->mEndContainer == aPoint.GetContainer() &&
203 rangeItem->mEndOffset > aPoint.Offset()) {
204 rangeItem->mEndOffset++;
207 return NS_OK;
210 template <typename PT, typename CT>
211 nsresult RangeUpdater::SelAdjInsertNode(
212 const EditorDOMPointBase<PT, CT>& aPoint) {
213 nsresult rv = SelAdjCreateNode(aPoint);
214 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
215 "RangeUpdater::SelAdjCreateNode() failed");
216 return rv;
219 void RangeUpdater::SelAdjDeleteNode(nsINode& aNodeToDelete) {
220 if (mLocked) {
221 // lock set by Will/DidReplaceParent, etc...
222 return;
225 if (mArray.IsEmpty()) {
226 return;
229 EditorRawDOMPoint atNodeToDelete(&aNodeToDelete);
230 NS_ASSERTION(atNodeToDelete.IsSetAndValid(),
231 "aNodeToDelete must be an orphan node or this is called "
232 "during mutation");
233 // check for range endpoints that are after aNodeToDelete and in the same
234 // parent
235 for (RefPtr<RangeItem>& rangeItem : mArray) {
236 MOZ_ASSERT(rangeItem);
238 if (rangeItem->mStartContainer == atNodeToDelete.GetContainer() &&
239 rangeItem->mStartOffset > atNodeToDelete.Offset()) {
240 rangeItem->mStartOffset--;
242 if (rangeItem->mEndContainer == atNodeToDelete.GetContainer() &&
243 rangeItem->mEndOffset > atNodeToDelete.Offset()) {
244 rangeItem->mEndOffset--;
247 // check for range endpoints that are in aNodeToDelete
248 if (rangeItem->mStartContainer == &aNodeToDelete) {
249 rangeItem->mStartContainer = atNodeToDelete.GetContainer();
250 rangeItem->mStartOffset = atNodeToDelete.Offset();
252 if (rangeItem->mEndContainer == &aNodeToDelete) {
253 rangeItem->mEndContainer = atNodeToDelete.GetContainer();
254 rangeItem->mEndOffset = atNodeToDelete.Offset();
257 // check for range endpoints that are in descendants of aNodeToDelete
258 bool updateEndBoundaryToo = false;
259 if (EditorUtils::IsDescendantOf(*rangeItem->mStartContainer,
260 aNodeToDelete)) {
261 updateEndBoundaryToo =
262 rangeItem->mStartContainer == rangeItem->mEndContainer;
263 rangeItem->mStartContainer = atNodeToDelete.GetContainer();
264 rangeItem->mStartOffset = atNodeToDelete.Offset();
267 // avoid having to call IsDescendantOf() for common case of range startnode
268 // == range endnode.
269 if (updateEndBoundaryToo ||
270 EditorUtils::IsDescendantOf(*rangeItem->mEndContainer, aNodeToDelete)) {
271 rangeItem->mEndContainer = atNodeToDelete.GetContainer();
272 rangeItem->mEndOffset = atNodeToDelete.Offset();
277 nsresult RangeUpdater::SelAdjSplitNode(nsIContent& aOriginalContent,
278 uint32_t aSplitOffset,
279 nsIContent& aNewContent,
280 SplitNodeDirection aSplitNodeDirection) {
281 if (mLocked) {
282 // lock set by Will/DidReplaceParent, etc...
283 return NS_OK;
286 if (mArray.IsEmpty()) {
287 return NS_OK;
290 EditorRawDOMPoint atNewNode(&aNewContent);
291 nsresult rv = SelAdjInsertNode(atNewNode);
292 if (NS_FAILED(rv)) {
293 NS_WARNING("RangeUpdater::SelAdjInsertNode() failed");
294 return rv;
297 // If point is in the range which are moved from aOriginalContent to
298 // aNewContent, we need to change its container to aNewContent and may need to
299 // adjust the offset. If point is in the range which are not moved from
300 // aOriginalContent, we may need to adjust the offset.
301 auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
302 uint32_t& aOffset) -> void {
303 if (aContainer != &aOriginalContent) {
304 return;
306 if (aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne) {
307 if (aOffset > aSplitOffset) {
308 aOffset -= aSplitOffset;
309 } else {
310 aContainer = &aNewContent;
312 } else if (aOffset >= aSplitOffset) {
313 aContainer = &aNewContent;
314 aOffset = aSplitOffset - aOffset;
318 for (RefPtr<RangeItem>& rangeItem : mArray) {
319 if (NS_WARN_IF(!rangeItem)) {
320 return NS_ERROR_FAILURE;
322 AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
323 AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
325 return NS_OK;
328 nsresult RangeUpdater::SelAdjJoinNodes(
329 const EditorRawDOMPoint& aStartOfRightContent,
330 const nsIContent& aRemovedContent, uint32_t aOffsetOfRemovedContent,
331 JoinNodesDirection aJoinNodesDirection) {
332 MOZ_ASSERT(aStartOfRightContent.IsSetAndValid());
334 if (mLocked) {
335 // lock set by Will/DidReplaceParent, etc...
336 return NS_OK;
339 if (mArray.IsEmpty()) {
340 return NS_OK;
343 auto AdjustDOMPoint = [&](nsCOMPtr<nsINode>& aContainer,
344 uint32_t& aOffset) -> void {
345 if (aContainer == aStartOfRightContent.GetContainerParent()) {
346 // If the point is in common parent of joined content nodes and the
347 // point is after the removed point, decrease the offset.
348 if (aOffset > aOffsetOfRemovedContent) {
349 aOffset--;
351 // If it pointed the removed content node, move to start of right content
352 // which was moved from the removed content.
353 else if (aOffset == aOffsetOfRemovedContent) {
354 aContainer = aStartOfRightContent.GetContainer();
355 aOffset = aStartOfRightContent.Offset();
357 } else if (aContainer == aStartOfRightContent.GetContainer()) {
358 // If the point is in joined node, and removed content is moved to
359 // start of the joined node, we need to adjust the offset.
360 if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
361 aOffset += aStartOfRightContent.Offset();
363 } else if (aContainer == &aRemovedContent) {
364 // If the point is in the removed content, move the point to the new
365 // point in the joined node. If left node content is moved into
366 // right node, the offset should be same. Otherwise, we need to advance
367 // the offset to length of the removed content.
368 aContainer = aStartOfRightContent.GetContainer();
369 if (aJoinNodesDirection == JoinNodesDirection::RightNodeIntoLeftNode) {
370 aOffset += aStartOfRightContent.Offset();
375 for (RefPtr<RangeItem>& rangeItem : mArray) {
376 if (NS_WARN_IF(!rangeItem)) {
377 return NS_ERROR_FAILURE;
379 AdjustDOMPoint(rangeItem->mStartContainer, rangeItem->mStartOffset);
380 AdjustDOMPoint(rangeItem->mEndContainer, rangeItem->mEndOffset);
383 return NS_OK;
386 void RangeUpdater::SelAdjReplaceText(const Text& aTextNode, uint32_t aOffset,
387 uint32_t aReplacedLength,
388 uint32_t aInsertedLength) {
389 if (mLocked) {
390 // lock set by Will/DidReplaceParent, etc...
391 return;
394 // First, adjust selection for insertion because when offset is in the
395 // replaced range, it's adjusted to aOffset and never modified by the
396 // insertion if we adjust selection for deletion first.
397 SelAdjInsertText(aTextNode, aOffset, aInsertedLength);
399 // Then, adjust selection for deletion.
400 SelAdjDeleteText(aTextNode, aOffset, aReplacedLength);
403 void RangeUpdater::SelAdjInsertText(const Text& aTextNode, uint32_t aOffset,
404 uint32_t aInsertedLength) {
405 if (mLocked) {
406 // lock set by Will/DidReplaceParent, etc...
407 return;
410 for (RefPtr<RangeItem>& rangeItem : mArray) {
411 MOZ_ASSERT(rangeItem);
413 if (rangeItem->mStartContainer == &aTextNode &&
414 rangeItem->mStartOffset > aOffset) {
415 rangeItem->mStartOffset += aInsertedLength;
417 if (rangeItem->mEndContainer == &aTextNode &&
418 rangeItem->mEndOffset > aOffset) {
419 rangeItem->mEndOffset += aInsertedLength;
424 void RangeUpdater::SelAdjDeleteText(const Text& aTextNode, uint32_t aOffset,
425 uint32_t aDeletedLength) {
426 if (mLocked) {
427 // lock set by Will/DidReplaceParent, etc...
428 return;
431 for (RefPtr<RangeItem>& rangeItem : mArray) {
432 MOZ_ASSERT(rangeItem);
434 if (rangeItem->mStartContainer == &aTextNode &&
435 rangeItem->mStartOffset > aOffset) {
436 if (rangeItem->mStartOffset >= aDeletedLength) {
437 rangeItem->mStartOffset -= aDeletedLength;
438 } else {
439 rangeItem->mStartOffset = 0;
442 if (rangeItem->mEndContainer == &aTextNode &&
443 rangeItem->mEndOffset > aOffset) {
444 if (rangeItem->mEndOffset >= aDeletedLength) {
445 rangeItem->mEndOffset -= aDeletedLength;
446 } else {
447 rangeItem->mEndOffset = 0;
453 void RangeUpdater::DidReplaceContainer(const Element& aRemovedElement,
454 Element& aInsertedElement) {
455 if (NS_WARN_IF(!mLocked)) {
456 return;
458 mLocked = false;
460 for (RefPtr<RangeItem>& rangeItem : mArray) {
461 if (NS_WARN_IF(!rangeItem)) {
462 return;
465 if (rangeItem->mStartContainer == &aRemovedElement) {
466 rangeItem->mStartContainer = &aInsertedElement;
468 if (rangeItem->mEndContainer == &aRemovedElement) {
469 rangeItem->mEndContainer = &aInsertedElement;
474 void RangeUpdater::DidRemoveContainer(const Element& aRemovedElement,
475 nsINode& aRemovedElementContainerNode,
476 uint32_t aOldOffsetOfRemovedElement,
477 uint32_t aOldChildCountOfRemovedElement) {
478 if (NS_WARN_IF(!mLocked)) {
479 return;
481 mLocked = false;
483 for (RefPtr<RangeItem>& rangeItem : mArray) {
484 if (NS_WARN_IF(!rangeItem)) {
485 return;
488 if (rangeItem->mStartContainer == &aRemovedElement) {
489 rangeItem->mStartContainer = &aRemovedElementContainerNode;
490 rangeItem->mStartOffset += aOldOffsetOfRemovedElement;
491 } else if (rangeItem->mStartContainer == &aRemovedElementContainerNode &&
492 rangeItem->mStartOffset > aOldOffsetOfRemovedElement) {
493 rangeItem->mStartOffset += aOldChildCountOfRemovedElement - 1;
496 if (rangeItem->mEndContainer == &aRemovedElement) {
497 rangeItem->mEndContainer = &aRemovedElementContainerNode;
498 rangeItem->mEndOffset += aOldOffsetOfRemovedElement;
499 } else if (rangeItem->mEndContainer == &aRemovedElementContainerNode &&
500 rangeItem->mEndOffset > aOldOffsetOfRemovedElement) {
501 rangeItem->mEndOffset += aOldChildCountOfRemovedElement - 1;
506 void RangeUpdater::DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
507 const nsINode& aNewParent, uint32_t aNewOffset) {
508 if (mLocked) {
509 // Do nothing if moving nodes is occurred while changing the container.
510 return;
512 for (RefPtr<RangeItem>& rangeItem : mArray) {
513 if (NS_WARN_IF(!rangeItem)) {
514 return;
517 // like a delete in aOldParent
518 if (rangeItem->mStartContainer == &aOldParent &&
519 rangeItem->mStartOffset > aOldOffset) {
520 rangeItem->mStartOffset--;
522 if (rangeItem->mEndContainer == &aOldParent &&
523 rangeItem->mEndOffset > aOldOffset) {
524 rangeItem->mEndOffset--;
527 // and like an insert in aNewParent
528 if (rangeItem->mStartContainer == &aNewParent &&
529 rangeItem->mStartOffset > aNewOffset) {
530 rangeItem->mStartOffset++;
532 if (rangeItem->mEndContainer == &aNewParent &&
533 rangeItem->mEndOffset > aNewOffset) {
534 rangeItem->mEndOffset++;
539 /******************************************************************************
540 * mozilla::RangeItem
542 * Helper struct for SelectionState. This stores range endpoints.
543 ******************************************************************************/
545 NS_IMPL_CYCLE_COLLECTION(RangeItem, mStartContainer, mEndContainer)
546 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(RangeItem, AddRef)
547 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(RangeItem, Release)
549 void RangeItem::StoreRange(const nsRange& aRange) {
550 mStartContainer = aRange.GetStartContainer();
551 mStartOffset = aRange.StartOffset();
552 mEndContainer = aRange.GetEndContainer();
553 mEndOffset = aRange.EndOffset();
556 already_AddRefed<nsRange> RangeItem::GetRange() const {
557 RefPtr<nsRange> range = nsRange::Create(
558 mStartContainer, mStartOffset, mEndContainer, mEndOffset, IgnoreErrors());
559 NS_WARNING_ASSERTION(range, "nsRange::Create() failed");
560 return range.forget();
563 } // namespace mozilla