no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / editor / libeditor / AutoRangeArray.cpp
blobbe8967176084d96a1e0dfe8024ae34772d217692
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 "AutoRangeArray.h"
8 #include "EditAction.h"
9 #include "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
10 #include "EditorForwards.h" // for CollectChildrenOptions
11 #include "HTMLEditUtils.h" // for HTMLEditUtils
12 #include "HTMLEditHelpers.h" // for SplitNodeResult
13 #include "TextEditor.h" // for TextEditor
14 #include "WSRunObject.h" // for WSRunScanner
16 #include "mozilla/IntegerRange.h" // for IntegerRange
17 #include "mozilla/OwningNonNull.h" // for OwningNonNull
18 #include "mozilla/dom/Document.h" // for dom::Document
19 #include "mozilla/dom/HTMLBRElement.h" // for dom HTMLBRElement
20 #include "mozilla/dom/Selection.h" // for dom::Selection
21 #include "mozilla/dom/Text.h" // for dom::Text
23 #include "gfxFontUtils.h" // for gfxFontUtils
24 #include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
25 #include "nsFrameSelection.h" // for nsFrameSelection
26 #include "nsIContent.h" // for nsIContent
27 #include "nsINode.h" // for nsINode
28 #include "nsRange.h" // for nsRange
29 #include "nsTextFragment.h" // for nsTextFragment
31 namespace mozilla {
33 using namespace dom;
35 using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
37 /******************************************************************************
38 * mozilla::AutoRangeArray
39 *****************************************************************************/
41 template AutoRangeArray::AutoRangeArray(const EditorDOMRange& aRange);
42 template AutoRangeArray::AutoRangeArray(const EditorRawDOMRange& aRange);
43 template AutoRangeArray::AutoRangeArray(const EditorDOMPoint& aRange);
44 template AutoRangeArray::AutoRangeArray(const EditorRawDOMPoint& aRange);
46 AutoRangeArray::AutoRangeArray(const dom::Selection& aSelection) {
47 Initialize(aSelection);
50 AutoRangeArray::AutoRangeArray(const AutoRangeArray& aOther)
51 : mAnchorFocusRange(aOther.mAnchorFocusRange),
52 mDirection(aOther.mDirection) {
53 mRanges.SetCapacity(aOther.mRanges.Length());
54 for (const OwningNonNull<nsRange>& range : aOther.mRanges) {
55 RefPtr<nsRange> clonedRange = range->CloneRange();
56 mRanges.AppendElement(std::move(clonedRange));
58 mAnchorFocusRange = aOther.mAnchorFocusRange;
61 template <typename PointType>
62 AutoRangeArray::AutoRangeArray(const EditorDOMRangeBase<PointType>& aRange) {
63 MOZ_ASSERT(aRange.IsPositionedAndValid());
64 RefPtr<nsRange> range = aRange.CreateRange(IgnoreErrors());
65 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
66 return;
68 mRanges.AppendElement(*range);
69 mAnchorFocusRange = std::move(range);
72 template <typename PT, typename CT>
73 AutoRangeArray::AutoRangeArray(const EditorDOMPointBase<PT, CT>& aPoint) {
74 MOZ_ASSERT(aPoint.IsSetAndValid());
75 RefPtr<nsRange> range = aPoint.CreateCollapsedRange(IgnoreErrors());
76 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
77 return;
79 mRanges.AppendElement(*range);
80 mAnchorFocusRange = std::move(range);
83 AutoRangeArray::~AutoRangeArray() {
84 if (mSavedRanges.isSome()) {
85 ClearSavedRanges();
89 // static
90 bool AutoRangeArray::IsEditableRange(const dom::AbstractRange& aRange,
91 const Element& aEditingHost) {
92 // TODO: Perhaps, we should check whether the start/end boundaries are
93 // first/last point of non-editable element.
94 // See https://github.com/w3c/editing/issues/283#issuecomment-788654850
95 EditorRawDOMPoint atStart(aRange.StartRef());
96 const bool isStartEditable =
97 atStart.IsInContentNode() &&
98 EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
99 EditorUtils::EditorType::HTML) &&
100 !HTMLEditUtils::IsNonEditableReplacedContent(
101 *atStart.ContainerAs<nsIContent>());
102 if (!isStartEditable) {
103 return false;
106 if (aRange.GetStartContainer() != aRange.GetEndContainer()) {
107 EditorRawDOMPoint atEnd(aRange.EndRef());
108 const bool isEndEditable =
109 atEnd.IsInContentNode() &&
110 EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
111 EditorUtils::EditorType::HTML) &&
112 !HTMLEditUtils::IsNonEditableReplacedContent(
113 *atEnd.ContainerAs<nsIContent>());
114 if (!isEndEditable) {
115 return false;
118 // Now, both start and end points are editable, but if they are in
119 // different editing host, we cannot edit the range.
120 if (atStart.ContainerAs<nsIContent>() != atEnd.ContainerAs<nsIContent>() &&
121 atStart.ContainerAs<nsIContent>()->GetEditingHost() !=
122 atEnd.ContainerAs<nsIContent>()->GetEditingHost()) {
123 return false;
127 // HTMLEditor does not support modifying outside `<body>` element for now.
128 nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
129 return commonAncestor && commonAncestor->IsContent() &&
130 commonAncestor->IsInclusiveDescendantOf(&aEditingHost);
133 void AutoRangeArray::EnsureOnlyEditableRanges(const Element& aEditingHost) {
134 for (const size_t index : Reversed(IntegerRange(mRanges.Length()))) {
135 const OwningNonNull<nsRange>& range = mRanges[index];
136 if (!AutoRangeArray::IsEditableRange(range, aEditingHost)) {
137 mRanges.RemoveElementAt(index);
138 continue;
140 // Special handling for `inert` attribute. If anchor node is inert, the
141 // range should be treated as not editable.
142 nsIContent* anchorContent =
143 mDirection == eDirNext
144 ? nsIContent::FromNode(range->GetStartContainer())
145 : nsIContent::FromNode(range->GetEndContainer());
146 if (anchorContent && HTMLEditUtils::ContentIsInert(*anchorContent)) {
147 mRanges.RemoveElementAt(index);
148 continue;
150 // Additionally, if focus node is inert, the range should be collapsed to
151 // anchor node.
152 nsIContent* focusContent =
153 mDirection == eDirNext
154 ? nsIContent::FromNode(range->GetEndContainer())
155 : nsIContent::FromNode(range->GetStartContainer());
156 if (focusContent && focusContent != anchorContent &&
157 HTMLEditUtils::ContentIsInert(*focusContent)) {
158 range->Collapse(mDirection == eDirNext);
161 mAnchorFocusRange = mRanges.IsEmpty() ? nullptr : mRanges.LastElement().get();
164 void AutoRangeArray::EnsureRangesInTextNode(const Text& aTextNode) {
165 auto GetOffsetInTextNode = [&aTextNode](const nsINode* aNode,
166 uint32_t aOffset) -> uint32_t {
167 MOZ_DIAGNOSTIC_ASSERT(aNode);
168 if (aNode == &aTextNode) {
169 return aOffset;
171 const nsIContent* anonymousDivElement = aTextNode.GetParent();
172 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement);
173 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->IsHTMLElement(nsGkAtoms::div));
174 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->GetFirstChild() == &aTextNode);
175 if (aNode == anonymousDivElement && aOffset == 0u) {
176 return 0u; // Point before the text node so that use start of the text.
178 MOZ_DIAGNOSTIC_ASSERT(aNode->IsInclusiveDescendantOf(anonymousDivElement));
179 // Point after the text node so that use end of the text.
180 return aTextNode.TextDataLength();
182 for (const OwningNonNull<nsRange>& range : mRanges) {
183 if (MOZ_LIKELY(range->GetStartContainer() == &aTextNode &&
184 range->GetEndContainer() == &aTextNode)) {
185 continue;
187 range->SetStartAndEnd(
188 const_cast<Text*>(&aTextNode),
189 GetOffsetInTextNode(range->GetStartContainer(), range->StartOffset()),
190 const_cast<Text*>(&aTextNode),
191 GetOffsetInTextNode(range->GetEndContainer(), range->EndOffset()));
194 if (MOZ_UNLIKELY(mRanges.Length() >= 2)) {
195 // For avoiding to handle same things in same range, we should drop and
196 // merge unnecessary ranges. Note that the ranges never overlap
197 // because selection ranges are not allowed it so that we need to check only
198 // end offset vs start offset of next one.
199 for (const size_t i : Reversed(IntegerRange(mRanges.Length() - 1u))) {
200 MOZ_ASSERT(mRanges[i]->EndOffset() < mRanges[i + 1]->StartOffset());
201 // XXX Should we delete collapsed range unless the index is 0? Without
202 // Selection API, such situation cannot happen so that `TextEditor`
203 // may behave unexpectedly.
204 if (MOZ_UNLIKELY(mRanges[i]->EndOffset() >=
205 mRanges[i + 1]->StartOffset())) {
206 const uint32_t newEndOffset = mRanges[i + 1]->EndOffset();
207 mRanges.RemoveElementAt(i + 1);
208 if (MOZ_UNLIKELY(NS_WARN_IF(newEndOffset > mRanges[i]->EndOffset()))) {
209 // So, this case shouldn't happen.
210 mRanges[i]->SetStartAndEnd(
211 const_cast<Text*>(&aTextNode), mRanges[i]->StartOffset(),
212 const_cast<Text*>(&aTextNode), newEndOffset);
219 Result<nsIEditor::EDirection, nsresult>
220 AutoRangeArray::ExtendAnchorFocusRangeFor(
221 const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount) {
222 MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable());
223 MOZ_ASSERT(mAnchorFocusRange);
224 MOZ_ASSERT(mAnchorFocusRange->IsPositioned());
225 MOZ_ASSERT(mAnchorFocusRange->StartRef().IsSet());
226 MOZ_ASSERT(mAnchorFocusRange->EndRef().IsSet());
228 if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
229 aDirectionAndAmount, *this)) {
230 return aDirectionAndAmount;
233 if (NS_WARN_IF(!aEditorBase.SelectionRef().RangeCount())) {
234 return Err(NS_ERROR_FAILURE);
237 // By a preceding call of EnsureOnlyEditableRanges(), anchor/focus range may
238 // have been changed. In that case, we cannot use nsFrameSelection anymore.
239 // FIXME: We should make `nsFrameSelection::CreateRangeExtendedToSomewhere`
240 // work without `Selection` instance.
241 if (MOZ_UNLIKELY(
242 aEditorBase.SelectionRef().GetAnchorFocusRange()->StartRef() !=
243 mAnchorFocusRange->StartRef() ||
244 aEditorBase.SelectionRef().GetAnchorFocusRange()->EndRef() !=
245 mAnchorFocusRange->EndRef())) {
246 return aDirectionAndAmount;
249 RefPtr<nsFrameSelection> frameSelection =
250 aEditorBase.SelectionRef().GetFrameSelection();
251 if (NS_WARN_IF(!frameSelection)) {
252 return Err(NS_ERROR_NOT_INITIALIZED);
255 RefPtr<Element> editingHost;
256 if (aEditorBase.IsHTMLEditor()) {
257 editingHost = aEditorBase.AsHTMLEditor()->ComputeEditingHost();
258 if (!editingHost) {
259 return Err(NS_ERROR_FAILURE);
263 Result<RefPtr<nsRange>, nsresult> result(NS_ERROR_UNEXPECTED);
264 nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount;
265 switch (aDirectionAndAmount) {
266 case nsIEditor::eNextWord:
267 result = frameSelection->CreateRangeExtendedToNextWordBoundary<nsRange>();
268 if (NS_WARN_IF(aEditorBase.Destroyed())) {
269 return Err(NS_ERROR_EDITOR_DESTROYED);
271 NS_WARNING_ASSERTION(
272 result.isOk(),
273 "nsFrameSelection::CreateRangeExtendedToNextWordBoundary() failed");
274 // DeleteSelectionWithTransaction() doesn't handle these actions
275 // because it's inside batching, so don't confuse it:
276 directionAndAmountResult = nsIEditor::eNone;
277 break;
278 case nsIEditor::ePreviousWord:
279 result =
280 frameSelection->CreateRangeExtendedToPreviousWordBoundary<nsRange>();
281 if (NS_WARN_IF(aEditorBase.Destroyed())) {
282 return Err(NS_ERROR_EDITOR_DESTROYED);
284 NS_WARNING_ASSERTION(
285 result.isOk(),
286 "nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() "
287 "failed");
288 // DeleteSelectionWithTransaction() doesn't handle these actions
289 // because it's inside batching, so don't confuse it:
290 directionAndAmountResult = nsIEditor::eNone;
291 break;
292 case nsIEditor::eNext:
293 result =
294 frameSelection
295 ->CreateRangeExtendedToNextGraphemeClusterBoundary<nsRange>();
296 if (NS_WARN_IF(aEditorBase.Destroyed())) {
297 return Err(NS_ERROR_EDITOR_DESTROYED);
299 NS_WARNING_ASSERTION(result.isOk(),
300 "nsFrameSelection::"
301 "CreateRangeExtendedToNextGraphemeClusterBoundary() "
302 "failed");
303 // Don't set directionAndAmount to eNone (see Bug 502259)
304 break;
305 case nsIEditor::ePrevious: {
306 // Only extend the selection where the selection is after a UTF-16
307 // surrogate pair or a variation selector.
308 // For other cases we don't want to do that, in order
309 // to make sure that pressing backspace will only delete the last
310 // typed character.
311 // XXX This is odd if the previous one is a sequence for a grapheme
312 // cluster.
313 const auto atStartOfSelection = GetFirstRangeStartPoint<EditorDOMPoint>();
314 if (MOZ_UNLIKELY(NS_WARN_IF(!atStartOfSelection.IsSet()))) {
315 return Err(NS_ERROR_FAILURE);
318 // node might be anonymous DIV, so we find better text node
319 const EditorDOMPoint insertionPoint =
320 aEditorBase.IsTextEditor()
321 ? aEditorBase.AsTextEditor()->FindBetterInsertionPoint(
322 atStartOfSelection)
323 : atStartOfSelection.GetPointInTextNodeIfPointingAroundTextNode<
324 EditorDOMPoint>();
325 if (MOZ_UNLIKELY(!insertionPoint.IsSet())) {
326 NS_WARNING(
327 "EditorBase::FindBetterInsertionPoint() failed, but ignored");
328 return aDirectionAndAmount;
331 if (!insertionPoint.IsInTextNode()) {
332 return aDirectionAndAmount;
335 const nsTextFragment* data =
336 &insertionPoint.ContainerAs<Text>()->TextFragment();
337 uint32_t offset = insertionPoint.Offset();
338 if (!(offset > 1 &&
339 data->IsLowSurrogateFollowingHighSurrogateAt(offset - 1)) &&
340 !(offset > 0 &&
341 gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) {
342 return aDirectionAndAmount;
344 // Different from the `eNext` case, we look for character boundary.
345 // I'm not sure whether this inconsistency between "Delete" and
346 // "Backspace" is intentional or not.
347 result = frameSelection
348 ->CreateRangeExtendedToPreviousCharacterBoundary<nsRange>();
349 if (NS_WARN_IF(aEditorBase.Destroyed())) {
350 return Err(NS_ERROR_EDITOR_DESTROYED);
352 NS_WARNING_ASSERTION(
353 result.isOk(),
354 "nsFrameSelection::"
355 "CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed");
356 break;
358 case nsIEditor::eToBeginningOfLine:
359 result =
360 frameSelection->CreateRangeExtendedToPreviousHardLineBreak<nsRange>();
361 if (NS_WARN_IF(aEditorBase.Destroyed())) {
362 return Err(NS_ERROR_EDITOR_DESTROYED);
364 NS_WARNING_ASSERTION(
365 result.isOk(),
366 "nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() "
367 "failed");
368 directionAndAmountResult = nsIEditor::eNone;
369 break;
370 case nsIEditor::eToEndOfLine:
371 result =
372 frameSelection->CreateRangeExtendedToNextHardLineBreak<nsRange>();
373 if (NS_WARN_IF(aEditorBase.Destroyed())) {
374 return Err(NS_ERROR_EDITOR_DESTROYED);
376 NS_WARNING_ASSERTION(
377 result.isOk(),
378 "nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed");
379 directionAndAmountResult = nsIEditor::eNext;
380 break;
381 default:
382 return aDirectionAndAmount;
385 if (result.isErr()) {
386 return Err(result.inspectErr());
388 RefPtr<nsRange> extendedRange(result.unwrap().forget());
389 if (!extendedRange || NS_WARN_IF(!extendedRange->IsPositioned())) {
390 NS_WARNING("Failed to extend the range, but ignored");
391 return directionAndAmountResult;
394 // If the new range isn't editable, keep using the original range.
395 if (aEditorBase.IsHTMLEditor() &&
396 !AutoRangeArray::IsEditableRange(*extendedRange, *editingHost)) {
397 return aDirectionAndAmount;
400 if (NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
401 extendedRange->GetStartContainer())) ||
402 NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
403 extendedRange->GetEndContainer()))) {
404 NS_WARNING("A range was extended to outer of selection limiter");
405 return Err(NS_ERROR_FAILURE);
408 // Swap focus/anchor range with the extended range.
409 DebugOnly<bool> found = false;
410 for (OwningNonNull<nsRange>& range : mRanges) {
411 if (range == mAnchorFocusRange) {
412 range = *extendedRange;
413 found = true;
414 break;
417 MOZ_ASSERT(found);
418 mAnchorFocusRange.swap(extendedRange);
419 return directionAndAmountResult;
422 Result<bool, nsresult>
423 AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
424 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
425 IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent,
426 const Element* aEditingHost) {
427 if (IsCollapsed()) {
428 return false;
431 switch (aDirectionAndAmount) {
432 case nsIEditor::eNext:
433 case nsIEditor::eNextWord:
434 case nsIEditor::ePrevious:
435 case nsIEditor::ePreviousWord:
436 break;
437 default:
438 return false;
441 bool changed = false;
442 for (const OwningNonNull<nsRange>& range : mRanges) {
443 MOZ_ASSERT(!range->IsInAnySelection(),
444 "Changing range in selection may cause running script");
445 Result<bool, nsresult> result =
446 WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
447 aHTMLEditor, range, aEditingHost);
448 if (result.isErr()) {
449 NS_WARNING(
450 "WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() "
451 "failed");
452 return Err(result.inspectErr());
454 changed |= result.inspect();
457 if (mRanges.Length() == 1 && aIfSelectingOnlyOneAtomicContent ==
458 IfSelectingOnlyOneAtomicContent::Collapse) {
459 MOZ_ASSERT(mRanges[0].get() == mAnchorFocusRange.get());
460 if (mAnchorFocusRange->GetStartContainer() ==
461 mAnchorFocusRange->GetEndContainer() &&
462 mAnchorFocusRange->GetChildAtStartOffset() &&
463 mAnchorFocusRange->StartRef().GetNextSiblingOfChildAtOffset() ==
464 mAnchorFocusRange->GetChildAtEndOffset()) {
465 mAnchorFocusRange->Collapse(aDirectionAndAmount == nsIEditor::eNext ||
466 aDirectionAndAmount == nsIEditor::eNextWord);
467 changed = true;
471 return changed;
474 bool AutoRangeArray::SaveAndTrackRanges(HTMLEditor& aHTMLEditor) {
475 if (mSavedRanges.isSome()) {
476 return false;
478 mSavedRanges.emplace(*this);
479 aHTMLEditor.RangeUpdaterRef().RegisterSelectionState(mSavedRanges.ref());
480 mTrackingHTMLEditor = &aHTMLEditor;
481 return true;
484 void AutoRangeArray::ClearSavedRanges() {
485 if (mSavedRanges.isNothing()) {
486 return;
488 OwningNonNull<HTMLEditor> htmlEditor(std::move(mTrackingHTMLEditor));
489 MOZ_ASSERT(!mTrackingHTMLEditor);
490 htmlEditor->RangeUpdaterRef().DropSelectionState(mSavedRanges.ref());
491 mSavedRanges.reset();
494 // static
495 void AutoRangeArray::
496 UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
497 EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint,
498 const Element& aEditingHost) {
499 // FYI: This was moved from
500 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6743
502 // MOOSE major hack:
503 // The GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() and
504 // GetPointAfterFollowingLineBreakOrAtFollowingBlock() don't really do the
505 // right thing for collapsed ranges inside block elements that contain nothing
506 // but a solo <br>. It's easier/ to put a workaround here than to revamp
507 // them. :-(
508 if (aStartPoint != aEndPoint) {
509 return;
512 if (!aStartPoint.IsInContentNode()) {
513 return;
516 // XXX Perhaps, this should be more careful. This may not select only one
517 // node because this just check whether the block is empty or not,
518 // and may not select in non-editable block. However, for inline
519 // editing host case, it's right to look for block element without
520 // editable state check. Now, this method is used for preparation for
521 // other things. So, cannot write test for this method behavior.
522 // So, perhaps, we should get rid of this method and each caller should
523 // handle its job better.
524 Element* const maybeNonEditableBlockElement =
525 HTMLEditUtils::GetInclusiveAncestorElement(
526 *aStartPoint.ContainerAs<nsIContent>(),
527 HTMLEditUtils::ClosestBlockElement,
528 BlockInlineCheck::UseComputedDisplayStyle);
529 if (!maybeNonEditableBlockElement) {
530 return;
533 // Make sure we don't go higher than our root element in the content tree
534 if (aEditingHost.IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
535 return;
538 if (HTMLEditUtils::IsEmptyNode(
539 *maybeNonEditableBlockElement,
540 {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
541 aStartPoint.Set(maybeNonEditableBlockElement, 0u);
542 aEndPoint.SetToEndOf(maybeNonEditableBlockElement);
547 * Get the point before the line containing aPointInLine.
549 * @return If the line starts after a `<br>` element, returns next
550 * sibling of the `<br>` element.
551 * If the line is first line of a block, returns point of
552 * the block.
553 * NOTE: The result may be point of editing host. I.e., the container may be
554 * outside of editing host.
556 static EditorDOMPoint
557 GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
558 const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
559 BlockInlineCheck aBlockInlineCheck, const Element& aEditingHost) {
560 // FYI: This was moved from
561 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6447
563 if (NS_WARN_IF(!aPointInLine.IsSet())) {
564 return EditorDOMPoint();
567 EditorDOMPoint point(aPointInLine);
568 // Start scanning from the container node if aPoint is in a text node.
569 // XXX Perhaps, IsInDataNode() must be expected.
570 if (point.IsInTextNode()) {
571 if (!point.GetContainer()->GetParentNode()) {
572 // Okay, can't promote any further
573 // XXX Why don't we return start of the text node?
574 return point;
576 // If there is a preformatted linefeed in the text node, let's return
577 // the point after it.
578 EditorDOMPoint atLastPreformattedNewLine =
579 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
580 point);
581 if (atLastPreformattedNewLine.IsSet()) {
582 return atLastPreformattedNewLine.NextPoint();
584 point.Set(point.GetContainer());
587 // Look back through any further inline nodes that aren't across a <br>
588 // from us, and that are enclosed in the same block.
589 // I.e., looking for start of current hard line.
590 constexpr HTMLEditUtils::WalkTreeOptions
591 ignoreNonEditableNodeAndStopAtBlockBoundary{
592 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
593 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
594 for (nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
595 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
596 aBlockInlineCheck, &aEditingHost);
597 previousEditableContent && previousEditableContent->GetParentNode() &&
598 !HTMLEditUtils::IsVisibleBRElement(*previousEditableContent) &&
599 !HTMLEditUtils::IsBlockElement(*previousEditableContent,
600 aBlockInlineCheck);
601 previousEditableContent = HTMLEditUtils::GetPreviousContent(
602 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
603 aBlockInlineCheck, &aEditingHost)) {
604 EditorDOMPoint atLastPreformattedNewLine =
605 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
606 EditorRawDOMPoint::AtEndOf(*previousEditableContent));
607 if (atLastPreformattedNewLine.IsSet()) {
608 return atLastPreformattedNewLine.NextPoint();
610 point.Set(previousEditableContent);
613 // Finding the real start for this point unless current line starts after
614 // <br> element. Look up the tree for as long as we are the first node in
615 // the container (typically, start of nearest block ancestor), and as long
616 // as we haven't hit the body node.
617 for (nsIContent* nearContent = HTMLEditUtils::GetPreviousContent(
618 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
619 aBlockInlineCheck, &aEditingHost);
620 !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
621 point.GetContainerParent();
622 nearContent = HTMLEditUtils::GetPreviousContent(
623 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
624 aBlockInlineCheck, &aEditingHost)) {
625 // Don't keep looking up if we have found a blockquote element to act on
626 // when we handle outdent.
627 // XXX Sounds like this is hacky. If possible, it should be check in
628 // outdent handler for consistency between edit sub-actions.
629 // We should check Chromium's behavior of outdent when Selection
630 // starts from `<blockquote>` and starts from first child of
631 // `<blockquote>`.
632 if (aEditSubAction == EditSubAction::eOutdent &&
633 point.IsContainerHTMLElement(nsGkAtoms::blockquote)) {
634 break;
637 // Don't walk past the editable section. Note that we need to check
638 // before walking up to a parent because we need to return the parent
639 // object, so the parent itself might not be in the editable area, but
640 // it's OK if we're not performing a block-level action.
641 bool blockLevelAction =
642 aEditSubAction == EditSubAction::eIndent ||
643 aEditSubAction == EditSubAction::eOutdent ||
644 aEditSubAction == EditSubAction::eSetOrClearAlignment ||
645 aEditSubAction == EditSubAction::eCreateOrRemoveBlock ||
646 aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand;
647 // XXX So, does this check whether the container is removable or not? It
648 // seems that here can be rewritten as obviously what here tries to
649 // check.
650 if (!point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost) &&
651 (blockLevelAction ||
652 !point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost))) {
653 break;
656 // If we're formatting a block, we should reformat first ancestor format
657 // block.
658 if (aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand &&
659 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
660 *point.ContainerAs<Element>())) {
661 point.Set(point.GetContainer());
662 break;
665 point.Set(point.GetContainer());
667 return point;
671 * Get the point after the following line break or the block which breaks the
672 * line containing aPointInLine.
674 * @return If the line ends with a visible `<br>` element, returns
675 * the point after the `<br>` element.
676 * If the line ends with a preformatted linefeed, returns
677 * the point after the linefeed unless it's an invisible
678 * line break immediately before a block boundary.
679 * If the line ends with a block boundary, returns the
680 * point of the block.
682 static EditorDOMPoint GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
683 const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
684 BlockInlineCheck aBlockInlineCheck, const Element& aEditingHost) {
685 // FYI: This was moved from
686 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6541
688 if (NS_WARN_IF(!aPointInLine.IsSet())) {
689 return EditorDOMPoint();
692 EditorDOMPoint point(aPointInLine);
693 // Start scanning from the container node if aPoint is in a text node.
694 // XXX Perhaps, IsInDataNode() must be expected.
695 if (point.IsInTextNode()) {
696 if (NS_WARN_IF(!point.GetContainer()->GetParentNode())) {
697 // Okay, can't promote any further
698 // XXX Why don't we return end of the text node?
699 return point;
701 EditorDOMPoint atNextPreformattedNewLine =
702 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
703 EditorDOMPoint>(point);
704 if (atNextPreformattedNewLine.IsSet()) {
705 // If the linefeed is last character of the text node, it may be
706 // invisible if it's immediately before a block boundary. In such
707 // case, we should return the block boundary.
708 Element* maybeNonEditableBlockElement = nullptr;
709 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
710 atNextPreformattedNewLine, &maybeNonEditableBlockElement) &&
711 maybeNonEditableBlockElement) {
712 // If the block is a parent of the editing host, let's return end
713 // of editing host.
714 if (maybeNonEditableBlockElement == &aEditingHost ||
715 !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
716 &aEditingHost)) {
717 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
719 // If it's invisible because of parent block boundary, return end
720 // of the block. Otherwise, i.e., it's followed by a child block,
721 // returns the point of the child block.
722 if (atNextPreformattedNewLine.ContainerAs<Text>()
723 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
724 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
726 return EditorDOMPoint(maybeNonEditableBlockElement);
728 // Otherwise, return the point after the preformatted linefeed.
729 return atNextPreformattedNewLine.NextPoint();
731 // want to be after the text node
732 point.SetAfter(point.GetContainer());
733 NS_WARNING_ASSERTION(point.IsSet(), "Failed to set to after the text node");
736 // Look ahead through any further inline nodes that aren't across a <br> from
737 // us, and that are enclosed in the same block.
738 // XXX Currently, we stop block-extending when finding visible <br> element.
739 // This might be different from "block-extend" of execCommand spec.
740 // However, the spec is really unclear.
741 // XXX Probably, scanning only editable nodes is wrong for
742 // EditSubAction::eCreateOrRemoveBlock and
743 // EditSubAction::eFormatBlockForHTMLCommand because it might be better to
744 // wrap existing inline elements even if it's non-editable. For example,
745 // following examples with insertParagraph causes different result:
746 // * <div contenteditable>foo[]<b contenteditable="false">bar</b></div>
747 // * <div contenteditable>foo[]<b>bar</b></div>
748 // * <div contenteditable>foo[]<b contenteditable="false">bar</b>baz</div>
749 // Only in the first case, after the caret position isn't wrapped with
750 // new <div> element.
751 constexpr HTMLEditUtils::WalkTreeOptions
752 ignoreNonEditableNodeAndStopAtBlockBoundary{
753 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
754 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
755 for (nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
756 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
757 aBlockInlineCheck, &aEditingHost);
758 nextEditableContent &&
759 !HTMLEditUtils::IsBlockElement(*nextEditableContent,
760 aBlockInlineCheck) &&
761 nextEditableContent->GetParent();
762 nextEditableContent = HTMLEditUtils::GetNextContent(
763 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
764 aBlockInlineCheck, &aEditingHost)) {
765 EditorDOMPoint atFirstPreformattedNewLine =
766 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
767 EditorDOMPoint>(EditorRawDOMPoint(nextEditableContent, 0));
768 if (atFirstPreformattedNewLine.IsSet()) {
769 // If the linefeed is last character of the text node, it may be
770 // invisible if it's immediately before a block boundary. In such
771 // case, we should return the block boundary.
772 Element* maybeNonEditableBlockElement = nullptr;
773 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
774 atFirstPreformattedNewLine, &maybeNonEditableBlockElement) &&
775 maybeNonEditableBlockElement) {
776 // If the block is a parent of the editing host, let's return end
777 // of editing host.
778 if (maybeNonEditableBlockElement == &aEditingHost ||
779 !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
780 &aEditingHost)) {
781 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
783 // If it's invisible because of parent block boundary, return end
784 // of the block. Otherwise, i.e., it's followed by a child block,
785 // returns the point of the child block.
786 if (atFirstPreformattedNewLine.ContainerAs<Text>()
787 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
788 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
790 return EditorDOMPoint(maybeNonEditableBlockElement);
792 // Otherwise, return the point after the preformatted linefeed.
793 return atFirstPreformattedNewLine.NextPoint();
795 point.SetAfter(nextEditableContent);
796 if (NS_WARN_IF(!point.IsSet())) {
797 break;
799 if (HTMLEditUtils::IsVisibleBRElement(*nextEditableContent)) {
800 break;
804 // Finding the real end for this point unless current line ends with a <br>
805 // element. Look up the tree for as long as we are the last node in the
806 // container (typically, block node), and as long as we haven't hit the body
807 // node.
808 for (nsIContent* nearContent = HTMLEditUtils::GetNextContent(
809 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
810 aBlockInlineCheck, &aEditingHost);
811 !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
812 point.GetContainerParent();
813 nearContent = HTMLEditUtils::GetNextContent(
814 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
815 aBlockInlineCheck, &aEditingHost)) {
816 // Don't walk past the editable section. Note that we need to check before
817 // walking up to a parent because we need to return the parent object, so
818 // the parent itself might not be in the editable area, but it's OK.
819 // XXX Maybe returning parent of editing host is really error prone since
820 // everybody need to check whether the end point is in editing host
821 // when they touch there.
822 if (!point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost) &&
823 !point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost)) {
824 break;
827 // If we're formatting a block, we should reformat first ancestor format
828 // block.
829 if (aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand &&
830 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
831 *point.ContainerAs<Element>())) {
832 point.SetAfter(point.GetContainer());
833 break;
836 point.SetAfter(point.GetContainer());
837 if (NS_WARN_IF(!point.IsSet())) {
838 break;
841 return point;
844 void AutoRangeArray::ExtendRangesToWrapLines(EditSubAction aEditSubAction,
845 BlockInlineCheck aBlockInlineCheck,
846 const Element& aEditingHost) {
847 // FYI: This is originated in
848 // https://searchfox.org/mozilla-central/rev/1739f1301d658c9bff544a0a095ab11fca2e549d/editor/libeditor/HTMLEditSubActionHandler.cpp#6712
850 bool removeSomeRanges = false;
851 for (const OwningNonNull<nsRange>& range : mRanges) {
852 // Remove non-positioned ranges.
853 if (MOZ_UNLIKELY(!range->IsPositioned())) {
854 removeSomeRanges = true;
855 continue;
857 // If the range is native anonymous subtrees, we must meet a bug of
858 // `Selection` so that we need to hack here.
859 if (MOZ_UNLIKELY(range->GetStartContainer()->IsInNativeAnonymousSubtree() ||
860 range->GetEndContainer()->IsInNativeAnonymousSubtree())) {
861 EditorRawDOMRange rawRange(range);
862 if (!rawRange.EnsureNotInNativeAnonymousSubtree()) {
863 range->Reset();
864 removeSomeRanges = true;
865 continue;
867 if (NS_FAILED(
868 range->SetStartAndEnd(rawRange.StartRef().ToRawRangeBoundary(),
869 rawRange.EndRef().ToRawRangeBoundary())) ||
870 MOZ_UNLIKELY(!range->IsPositioned())) {
871 range->Reset();
872 removeSomeRanges = true;
873 continue;
876 // Finally, extend the range.
877 if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
878 range, aEditSubAction, aBlockInlineCheck, aEditingHost))) {
879 // If we failed to extend the range, we should use the original range
880 // as-is unless the range is broken at setting the range.
881 if (NS_WARN_IF(!range->IsPositioned())) {
882 removeSomeRanges = true;
886 if (removeSomeRanges) {
887 for (const size_t i : Reversed(IntegerRange(mRanges.Length()))) {
888 if (!mRanges[i]->IsPositioned()) {
889 mRanges.RemoveElementAt(i);
892 if (!mAnchorFocusRange || !mAnchorFocusRange->IsPositioned()) {
893 if (mRanges.IsEmpty()) {
894 mAnchorFocusRange = nullptr;
895 } else {
896 mAnchorFocusRange = mRanges.LastElement();
902 // static
903 nsresult AutoRangeArray::ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
904 nsRange& aRange, EditSubAction aEditSubAction,
905 BlockInlineCheck aBlockInlineCheck, const Element& aEditingHost) {
906 MOZ_DIAGNOSTIC_ASSERT(
907 !EditorRawDOMPoint(aRange.StartRef()).IsInNativeAnonymousSubtree());
908 MOZ_DIAGNOSTIC_ASSERT(
909 !EditorRawDOMPoint(aRange.EndRef()).IsInNativeAnonymousSubtree());
911 if (NS_WARN_IF(!aRange.IsPositioned())) {
912 return NS_ERROR_INVALID_ARG;
915 EditorDOMPoint startPoint(aRange.StartRef()), endPoint(aRange.EndRef());
917 // If we're joining blocks, we call this for selecting a line to move.
918 // Therefore, we don't want to select the ancestor blocks in this case
919 // even if they are empty.
920 if (aEditSubAction != EditSubAction::eMergeBlockContents) {
921 AutoRangeArray::
922 UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
923 startPoint, endPoint, aEditingHost);
926 // Make a new adjusted range to represent the appropriate block content.
927 // This is tricky. The basic idea is to push out the range endpoints to
928 // truly enclose the blocks that we will affect.
930 // Make sure that the new range ends up to be in the editable section.
931 // XXX Looks like that this check wastes the time. Perhaps, we should
932 // implement a method which checks both two DOM points in the editor
933 // root.
935 startPoint =
936 GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
937 startPoint, aEditSubAction, aBlockInlineCheck, aEditingHost);
938 // XXX GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() may
939 // return point of editing host. Perhaps, we should change it and stop
940 // checking it here since this check may be expensive.
941 // XXX If the container is an element in the editing host but it points end of
942 // the container, this returns nullptr. Is it intentional?
943 if (!startPoint.GetChildOrContainerIfDataNode() ||
944 !startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
945 &aEditingHost)) {
946 return NS_ERROR_FAILURE;
948 endPoint = GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
949 endPoint, aEditSubAction, aBlockInlineCheck, aEditingHost);
950 const EditorDOMPoint lastRawPoint =
951 endPoint.IsStartOfContainer() ? endPoint : endPoint.PreviousPoint();
952 // XXX GetPointAfterFollowingLineBreakOrAtFollowingBlock() may return point of
953 // editing host. Perhaps, we should change it and stop checking it here
954 // since this check may be expensive.
955 // XXX If the container is an element in the editing host but it points end of
956 // the container, this returns nullptr. Is it intentional?
957 if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
958 !lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
959 &aEditingHost)) {
960 return NS_ERROR_FAILURE;
963 nsresult rv = aRange.SetStartAndEnd(startPoint.ToRawRangeBoundary(),
964 endPoint.ToRawRangeBoundary());
965 if (NS_FAILED(rv)) {
966 return NS_ERROR_FAILURE;
968 return NS_OK;
971 Result<EditorDOMPoint, nsresult>
972 AutoRangeArray::SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
973 HTMLEditor& aHTMLEditor, BlockInlineCheck aBlockInlineCheck,
974 const Element& aEditingHost,
975 const nsIContent* aAncestorLimiter /* = nullptr */) {
976 // FYI: The following code is originated in
977 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#6971
979 // Split text nodes. This is necessary, since given ranges may end in text
980 // nodes in case where part of a pre-formatted elements needs to be moved.
981 EditorDOMPoint pointToPutCaret;
982 IgnoredErrorResult ignoredError;
983 for (const OwningNonNull<nsRange>& range : mRanges) {
984 EditorDOMPoint atEnd(range->EndRef());
985 if (NS_WARN_IF(!atEnd.IsSet()) || !atEnd.IsInTextNode() ||
986 atEnd.GetContainer() == aAncestorLimiter) {
987 continue;
990 if (!atEnd.IsStartOfContainer() && !atEnd.IsEndOfContainer()) {
991 // Split the text node.
992 Result<SplitNodeResult, nsresult> splitAtEndResult =
993 aHTMLEditor.SplitNodeWithTransaction(atEnd);
994 if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
995 NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
996 return splitAtEndResult.propagateErr();
998 SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
999 unwrappedSplitAtEndResult.MoveCaretPointTo(
1000 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
1002 // Correct the range.
1003 // The new end parent becomes the parent node of the text.
1004 MOZ_ASSERT(!range->IsInAnySelection());
1005 range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
1006 .ToRawRangeBoundary(),
1007 ignoredError);
1008 NS_WARNING_ASSERTION(!ignoredError.Failed(),
1009 "nsRange::SetEnd() failed, but ignored");
1010 ignoredError.SuppressException();
1014 // FYI: The following code is originated in
1015 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#7023
1016 AutoTArray<OwningNonNull<RangeItem>, 8> rangeItemArray;
1017 rangeItemArray.AppendElements(mRanges.Length());
1019 // First register ranges for special editor gravity
1020 Maybe<size_t> anchorFocusRangeIndex;
1021 for (const size_t index : IntegerRange(rangeItemArray.Length())) {
1022 rangeItemArray[index] = new RangeItem();
1023 rangeItemArray[index]->StoreRange(*mRanges[index]);
1024 aHTMLEditor.RangeUpdaterRef().RegisterRangeItem(*rangeItemArray[index]);
1025 if (mRanges[index] == mAnchorFocusRange) {
1026 anchorFocusRangeIndex = Some(index);
1029 // TODO: We should keep the array, and just update the ranges.
1030 mRanges.Clear();
1031 mAnchorFocusRange = nullptr;
1032 // Now bust up inlines.
1033 nsresult rv = NS_OK;
1034 for (const OwningNonNull<RangeItem>& item : Reversed(rangeItemArray)) {
1035 // MOZ_KnownLive because 'rangeItemArray' is guaranteed to keep it alive.
1036 Result<EditorDOMPoint, nsresult> splitParentsResult =
1037 aHTMLEditor.SplitInlineAncestorsAtRangeBoundaries(
1038 MOZ_KnownLive(*item), aBlockInlineCheck, aEditingHost,
1039 aAncestorLimiter);
1040 if (MOZ_UNLIKELY(splitParentsResult.isErr())) {
1041 NS_WARNING("HTMLEditor::SplitInlineAncestorsAtRangeBoundaries() failed");
1042 rv = splitParentsResult.unwrapErr();
1043 break;
1045 if (splitParentsResult.inspect().IsSet()) {
1046 pointToPutCaret = splitParentsResult.unwrap();
1049 // Then unregister the ranges
1050 for (const size_t index : IntegerRange(rangeItemArray.Length())) {
1051 aHTMLEditor.RangeUpdaterRef().DropRangeItem(rangeItemArray[index]);
1052 RefPtr<nsRange> range = rangeItemArray[index]->GetRange();
1053 if (range && range->IsPositioned()) {
1054 if (anchorFocusRangeIndex.isSome() && index == *anchorFocusRangeIndex) {
1055 mAnchorFocusRange = range;
1057 mRanges.AppendElement(std::move(range));
1060 if (!mAnchorFocusRange && !mRanges.IsEmpty()) {
1061 mAnchorFocusRange = mRanges.LastElement();
1064 // XXX Why do we ignore the other errors here??
1065 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
1066 return Err(NS_ERROR_EDITOR_DESTROYED);
1068 return pointToPutCaret;
1071 nsresult AutoRangeArray::CollectEditTargetNodes(
1072 const HTMLEditor& aHTMLEditor,
1073 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
1074 EditSubAction aEditSubAction,
1075 CollectNonEditableNodes aCollectNonEditableNodes) const {
1076 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
1078 // FYI: This was moved from
1079 // https://searchfox.org/mozilla-central/rev/4bce7d85ba4796dd03c5dcc7cfe8eee0e4c07b3b/editor/libeditor/HTMLEditSubActionHandler.cpp#7060
1081 // Gather up a list of all the nodes
1082 for (const OwningNonNull<nsRange>& range : mRanges) {
1083 DOMSubtreeIterator iter;
1084 nsresult rv = iter.Init(*range);
1085 if (NS_FAILED(rv)) {
1086 NS_WARNING("DOMSubtreeIterator::Init() failed");
1087 return rv;
1089 if (aOutArrayOfContents.IsEmpty()) {
1090 iter.AppendAllNodesToArray(aOutArrayOfContents);
1091 } else {
1092 AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfTopChildren;
1093 iter.AppendNodesToArray(
1094 +[](nsINode& aNode, void* aArray) -> bool {
1095 MOZ_ASSERT(aArray);
1096 return !static_cast<nsTArray<OwningNonNull<nsIContent>>*>(aArray)
1097 ->Contains(&aNode);
1099 arrayOfTopChildren, &aOutArrayOfContents);
1100 aOutArrayOfContents.AppendElements(std::move(arrayOfTopChildren));
1102 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1103 for (const size_t i :
1104 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1105 if (!EditorUtils::IsEditableContent(aOutArrayOfContents[i],
1106 EditorUtils::EditorType::HTML)) {
1107 aOutArrayOfContents.RemoveElementAt(i);
1113 switch (aEditSubAction) {
1114 case EditSubAction::eCreateOrRemoveBlock:
1115 case EditSubAction::eFormatBlockForHTMLCommand: {
1116 // Certain operations should not act on li's and td's, but rather inside
1117 // them. Alter the list as needed.
1118 CollectChildrenOptions options = {
1119 CollectChildrenOption::CollectListChildren,
1120 CollectChildrenOption::CollectTableChildren};
1121 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1122 options += CollectChildrenOption::IgnoreNonEditableChildren;
1124 if (aEditSubAction == EditSubAction::eCreateOrRemoveBlock) {
1125 for (const size_t index :
1126 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1127 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
1128 if (HTMLEditUtils::IsListItem(content)) {
1129 aOutArrayOfContents.RemoveElementAt(index);
1130 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
1131 options);
1134 } else {
1135 // <dd> and <dt> are format blocks. Therefore, we should not handle
1136 // their children directly. They should be replaced with new format
1137 // block.
1138 MOZ_ASSERT(
1139 HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dt));
1140 MOZ_ASSERT(
1141 HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dd));
1142 for (const size_t index :
1143 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1144 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
1145 MOZ_ASSERT_IF(HTMLEditUtils::IsListItem(content),
1146 content->IsAnyOfHTMLElements(
1147 nsGkAtoms::dd, nsGkAtoms::dt, nsGkAtoms::li));
1148 if (content->IsHTMLElement(nsGkAtoms::li)) {
1149 aOutArrayOfContents.RemoveElementAt(index);
1150 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
1151 options);
1155 // Empty text node shouldn't be selected if unnecessary
1156 for (const size_t index :
1157 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1158 if (const Text* text = aOutArrayOfContents[index]->GetAsText()) {
1159 // Don't select empty text except to empty block
1160 if (!HTMLEditUtils::IsVisibleTextNode(*text)) {
1161 aOutArrayOfContents.RemoveElementAt(index);
1165 break;
1167 case EditSubAction::eCreateOrChangeList: {
1168 // XXX aCollectNonEditableNodes is ignored here. Maybe a bug.
1169 CollectChildrenOptions options = {
1170 CollectChildrenOption::CollectTableChildren};
1171 for (const size_t index :
1172 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1173 // Scan for table elements. If we find table elements other than
1174 // table, replace it with a list of any editable non-table content
1175 // because if a selection range starts from end in a table-cell and
1176 // ends at or starts from outside the `<table>`, we need to make
1177 // lists in each selected table-cells.
1178 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
1179 if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
1180 aOutArrayOfContents.RemoveElementAt(index);
1181 HTMLEditUtils::CollectChildren(content, aOutArrayOfContents, index,
1182 options);
1185 // If there is only one node in the array, and it is a `<div>`,
1186 // `<blockquote>` or a list element, then look inside of it until we
1187 // find inner list or content.
1188 if (aOutArrayOfContents.Length() != 1) {
1189 break;
1191 Element* deepestDivBlockquoteOrListElement =
1192 HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
1193 aOutArrayOfContents[0],
1194 {HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode},
1195 BlockInlineCheck::Unused, nsGkAtoms::div, nsGkAtoms::blockquote,
1196 nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl);
1197 if (!deepestDivBlockquoteOrListElement) {
1198 break;
1200 if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
1201 nsGkAtoms::div, nsGkAtoms::blockquote)) {
1202 aOutArrayOfContents.Clear();
1203 // XXX Before we're called, non-editable nodes are ignored. However,
1204 // we may append non-editable nodes here.
1205 HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
1206 aOutArrayOfContents, 0, {});
1207 break;
1209 aOutArrayOfContents.ReplaceElementAt(
1210 0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
1211 break;
1213 case EditSubAction::eOutdent:
1214 case EditSubAction::eIndent:
1215 case EditSubAction::eSetPositionToAbsolute: {
1216 // Indent/outdent already do something special for list items, but we
1217 // still need to make sure we don't act on table elements
1218 CollectChildrenOptions options = {
1219 CollectChildrenOption::CollectListChildren,
1220 CollectChildrenOption::CollectTableChildren};
1221 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1222 options += CollectChildrenOption::IgnoreNonEditableChildren;
1224 for (const size_t index :
1225 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1226 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
1227 if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
1228 aOutArrayOfContents.RemoveElementAt(index);
1229 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
1230 options);
1233 break;
1235 default:
1236 break;
1239 // Outdent should look inside of divs.
1240 if (aEditSubAction == EditSubAction::eOutdent &&
1241 !aHTMLEditor.IsCSSEnabled()) {
1242 CollectChildrenOptions options = {};
1243 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1244 options += CollectChildrenOption::IgnoreNonEditableChildren;
1246 for (const size_t index :
1247 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1248 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
1249 if (content->IsHTMLElement(nsGkAtoms::div)) {
1250 aOutArrayOfContents.RemoveElementAt(index);
1251 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
1252 options);
1257 return NS_OK;
1260 Element* AutoRangeArray::GetClosestAncestorAnyListElementOfRange() const {
1261 for (const OwningNonNull<nsRange>& range : mRanges) {
1262 nsINode* commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
1263 if (MOZ_UNLIKELY(!commonAncestorNode)) {
1264 continue;
1266 for (Element* const element :
1267 commonAncestorNode->InclusiveAncestorsOfType<Element>()) {
1268 if (HTMLEditUtils::IsAnyListElement(element)) {
1269 return element;
1273 return nullptr;
1276 } // namespace mozilla