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
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())) {
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())) {
79 mRanges
.AppendElement(*range
);
80 mAnchorFocusRange
= std::move(range
);
83 AutoRangeArray::~AutoRangeArray() {
84 if (mSavedRanges
.isSome()) {
89 AutoRangeArray::AutoRangeArray(nsRange
& aRange
) {
90 MOZ_ASSERT(aRange
.IsPositioned());
91 if (NS_WARN_IF(!aRange
.IsPositioned())) {
94 mRanges
.AppendElement(aRange
);
95 mAnchorFocusRange
= &aRange
;
99 bool AutoRangeArray::IsEditableRange(const dom::AbstractRange
& aRange
,
100 const Element
& aEditingHost
) {
101 // TODO: Perhaps, we should check whether the start/end boundaries are
102 // first/last point of non-editable element.
103 // See https://github.com/w3c/editing/issues/283#issuecomment-788654850
104 EditorRawDOMPoint
atStart(aRange
.StartRef());
105 const bool isStartEditable
=
106 atStart
.IsInContentNode() &&
107 EditorUtils::IsEditableContent(*atStart
.ContainerAs
<nsIContent
>(),
108 EditorUtils::EditorType::HTML
) &&
109 !HTMLEditUtils::IsNonEditableReplacedContent(
110 *atStart
.ContainerAs
<nsIContent
>());
111 if (!isStartEditable
) {
115 if (aRange
.GetStartContainer() != aRange
.GetEndContainer()) {
116 EditorRawDOMPoint
atEnd(aRange
.EndRef());
117 const bool isEndEditable
=
118 atEnd
.IsInContentNode() &&
119 EditorUtils::IsEditableContent(*atEnd
.ContainerAs
<nsIContent
>(),
120 EditorUtils::EditorType::HTML
) &&
121 !HTMLEditUtils::IsNonEditableReplacedContent(
122 *atEnd
.ContainerAs
<nsIContent
>());
123 if (!isEndEditable
) {
127 // Now, both start and end points are editable, but if they are in
128 // different editing host, we cannot edit the range.
129 if (atStart
.ContainerAs
<nsIContent
>() != atEnd
.ContainerAs
<nsIContent
>() &&
130 atStart
.ContainerAs
<nsIContent
>()->GetEditingHost() !=
131 atEnd
.ContainerAs
<nsIContent
>()->GetEditingHost()) {
136 // HTMLEditor does not support modifying outside `<body>` element for now.
137 nsINode
* commonAncestor
= aRange
.GetClosestCommonInclusiveAncestor();
138 return commonAncestor
&& commonAncestor
->IsContent() &&
139 commonAncestor
->IsInclusiveDescendantOf(&aEditingHost
);
142 void AutoRangeArray::EnsureOnlyEditableRanges(const Element
& aEditingHost
) {
143 for (const size_t index
: Reversed(IntegerRange(mRanges
.Length()))) {
144 const OwningNonNull
<nsRange
>& range
= mRanges
[index
];
145 if (!AutoRangeArray::IsEditableRange(range
, aEditingHost
)) {
146 mRanges
.RemoveElementAt(index
);
149 // Special handling for `inert` attribute. If anchor node is inert, the
150 // range should be treated as not editable.
151 nsIContent
* anchorContent
=
152 mDirection
== eDirNext
153 ? nsIContent::FromNode(range
->GetStartContainer())
154 : nsIContent::FromNode(range
->GetEndContainer());
155 if (anchorContent
&& HTMLEditUtils::ContentIsInert(*anchorContent
)) {
156 mRanges
.RemoveElementAt(index
);
159 // Additionally, if focus node is inert, the range should be collapsed to
161 nsIContent
* focusContent
=
162 mDirection
== eDirNext
163 ? nsIContent::FromNode(range
->GetEndContainer())
164 : nsIContent::FromNode(range
->GetStartContainer());
165 if (focusContent
&& focusContent
!= anchorContent
&&
166 HTMLEditUtils::ContentIsInert(*focusContent
)) {
167 range
->Collapse(mDirection
== eDirNext
);
170 mAnchorFocusRange
= mRanges
.IsEmpty() ? nullptr : mRanges
.LastElement().get();
173 void AutoRangeArray::EnsureRangesInTextNode(const Text
& aTextNode
) {
174 auto GetOffsetInTextNode
= [&aTextNode
](const nsINode
* aNode
,
175 uint32_t aOffset
) -> uint32_t {
176 MOZ_DIAGNOSTIC_ASSERT(aNode
);
177 if (aNode
== &aTextNode
) {
180 const nsIContent
* anonymousDivElement
= aTextNode
.GetParent();
181 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement
);
182 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement
->IsHTMLElement(nsGkAtoms::div
));
183 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement
->GetFirstChild() == &aTextNode
);
184 if (aNode
== anonymousDivElement
&& aOffset
== 0u) {
185 return 0u; // Point before the text node so that use start of the text.
187 MOZ_DIAGNOSTIC_ASSERT(aNode
->IsInclusiveDescendantOf(anonymousDivElement
));
188 // Point after the text node so that use end of the text.
189 return aTextNode
.TextDataLength();
191 for (const OwningNonNull
<nsRange
>& range
: mRanges
) {
192 if (MOZ_LIKELY(range
->GetStartContainer() == &aTextNode
&&
193 range
->GetEndContainer() == &aTextNode
)) {
196 range
->SetStartAndEnd(
197 const_cast<Text
*>(&aTextNode
),
198 GetOffsetInTextNode(range
->GetStartContainer(), range
->StartOffset()),
199 const_cast<Text
*>(&aTextNode
),
200 GetOffsetInTextNode(range
->GetEndContainer(), range
->EndOffset()));
203 if (MOZ_UNLIKELY(mRanges
.Length() >= 2)) {
204 // For avoiding to handle same things in same range, we should drop and
205 // merge unnecessary ranges. Note that the ranges never overlap
206 // because selection ranges are not allowed it so that we need to check only
207 // end offset vs start offset of next one.
208 for (const size_t i
: Reversed(IntegerRange(mRanges
.Length() - 1u))) {
209 MOZ_ASSERT(mRanges
[i
]->EndOffset() < mRanges
[i
+ 1]->StartOffset());
210 // XXX Should we delete collapsed range unless the index is 0? Without
211 // Selection API, such situation cannot happen so that `TextEditor`
212 // may behave unexpectedly.
213 if (MOZ_UNLIKELY(mRanges
[i
]->EndOffset() >=
214 mRanges
[i
+ 1]->StartOffset())) {
215 const uint32_t newEndOffset
= mRanges
[i
+ 1]->EndOffset();
216 mRanges
.RemoveElementAt(i
+ 1);
217 if (MOZ_UNLIKELY(NS_WARN_IF(newEndOffset
> mRanges
[i
]->EndOffset()))) {
218 // So, this case shouldn't happen.
219 mRanges
[i
]->SetStartAndEnd(
220 const_cast<Text
*>(&aTextNode
), mRanges
[i
]->StartOffset(),
221 const_cast<Text
*>(&aTextNode
), newEndOffset
);
228 Result
<nsIEditor::EDirection
, nsresult
>
229 AutoRangeArray::ExtendAnchorFocusRangeFor(
230 const EditorBase
& aEditorBase
, nsIEditor::EDirection aDirectionAndAmount
) {
231 MOZ_ASSERT(aEditorBase
.IsEditActionDataAvailable());
232 MOZ_ASSERT(mAnchorFocusRange
);
233 MOZ_ASSERT(mAnchorFocusRange
->IsPositioned());
234 MOZ_ASSERT(mAnchorFocusRange
->StartRef().IsSet());
235 MOZ_ASSERT(mAnchorFocusRange
->EndRef().IsSet());
237 if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
238 aDirectionAndAmount
, *this)) {
239 return aDirectionAndAmount
;
242 if (NS_WARN_IF(!aEditorBase
.SelectionRef().RangeCount())) {
243 return Err(NS_ERROR_FAILURE
);
246 // By a preceding call of EnsureOnlyEditableRanges(), anchor/focus range may
247 // have been changed. In that case, we cannot use nsFrameSelection anymore.
248 // FIXME: We should make `nsFrameSelection::CreateRangeExtendedToSomewhere`
249 // work without `Selection` instance.
251 aEditorBase
.SelectionRef().GetAnchorFocusRange()->StartRef() !=
252 mAnchorFocusRange
->StartRef() ||
253 aEditorBase
.SelectionRef().GetAnchorFocusRange()->EndRef() !=
254 mAnchorFocusRange
->EndRef())) {
255 return aDirectionAndAmount
;
258 RefPtr
<nsFrameSelection
> frameSelection
=
259 aEditorBase
.SelectionRef().GetFrameSelection();
260 if (NS_WARN_IF(!frameSelection
)) {
261 return Err(NS_ERROR_NOT_INITIALIZED
);
264 RefPtr
<Element
> editingHost
;
265 if (aEditorBase
.IsHTMLEditor()) {
266 editingHost
= aEditorBase
.AsHTMLEditor()->ComputeEditingHost();
268 return Err(NS_ERROR_FAILURE
);
272 Result
<RefPtr
<nsRange
>, nsresult
> result(NS_ERROR_UNEXPECTED
);
273 nsIEditor::EDirection directionAndAmountResult
= aDirectionAndAmount
;
274 switch (aDirectionAndAmount
) {
275 case nsIEditor::eNextWord
:
276 result
= frameSelection
->CreateRangeExtendedToNextWordBoundary
<nsRange
>();
277 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
278 return Err(NS_ERROR_EDITOR_DESTROYED
);
280 NS_WARNING_ASSERTION(
282 "nsFrameSelection::CreateRangeExtendedToNextWordBoundary() failed");
283 // DeleteSelectionWithTransaction() doesn't handle these actions
284 // because it's inside batching, so don't confuse it:
285 directionAndAmountResult
= nsIEditor::eNone
;
287 case nsIEditor::ePreviousWord
:
289 frameSelection
->CreateRangeExtendedToPreviousWordBoundary
<nsRange
>();
290 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
291 return Err(NS_ERROR_EDITOR_DESTROYED
);
293 NS_WARNING_ASSERTION(
295 "nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() "
297 // DeleteSelectionWithTransaction() doesn't handle these actions
298 // because it's inside batching, so don't confuse it:
299 directionAndAmountResult
= nsIEditor::eNone
;
301 case nsIEditor::eNext
:
304 ->CreateRangeExtendedToNextGraphemeClusterBoundary
<nsRange
>();
305 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
306 return Err(NS_ERROR_EDITOR_DESTROYED
);
308 NS_WARNING_ASSERTION(result
.isOk(),
310 "CreateRangeExtendedToNextGraphemeClusterBoundary() "
312 // Don't set directionAndAmount to eNone (see Bug 502259)
314 case nsIEditor::ePrevious
: {
315 // Only extend the selection where the selection is after a UTF-16
316 // surrogate pair or a variation selector.
317 // For other cases we don't want to do that, in order
318 // to make sure that pressing backspace will only delete the last
320 // XXX This is odd if the previous one is a sequence for a grapheme
322 const auto atStartOfSelection
= GetFirstRangeStartPoint
<EditorDOMPoint
>();
323 if (MOZ_UNLIKELY(NS_WARN_IF(!atStartOfSelection
.IsSet()))) {
324 return Err(NS_ERROR_FAILURE
);
327 // node might be anonymous DIV, so we find better text node
328 const EditorDOMPoint insertionPoint
=
329 aEditorBase
.IsTextEditor()
330 ? aEditorBase
.AsTextEditor()->FindBetterInsertionPoint(
332 : atStartOfSelection
.GetPointInTextNodeIfPointingAroundTextNode
<
334 if (MOZ_UNLIKELY(!insertionPoint
.IsSet())) {
336 "EditorBase::FindBetterInsertionPoint() failed, but ignored");
337 return aDirectionAndAmount
;
340 if (!insertionPoint
.IsInTextNode()) {
341 return aDirectionAndAmount
;
344 const nsTextFragment
* data
=
345 &insertionPoint
.ContainerAs
<Text
>()->TextFragment();
346 uint32_t offset
= insertionPoint
.Offset();
348 data
->IsLowSurrogateFollowingHighSurrogateAt(offset
- 1)) &&
350 gfxFontUtils::IsVarSelector(data
->CharAt(offset
- 1)))) {
351 return aDirectionAndAmount
;
353 // Different from the `eNext` case, we look for character boundary.
354 // I'm not sure whether this inconsistency between "Delete" and
355 // "Backspace" is intentional or not.
356 result
= frameSelection
357 ->CreateRangeExtendedToPreviousCharacterBoundary
<nsRange
>();
358 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
359 return Err(NS_ERROR_EDITOR_DESTROYED
);
361 NS_WARNING_ASSERTION(
364 "CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed");
367 case nsIEditor::eToBeginningOfLine
:
369 frameSelection
->CreateRangeExtendedToPreviousHardLineBreak
<nsRange
>();
370 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
371 return Err(NS_ERROR_EDITOR_DESTROYED
);
373 NS_WARNING_ASSERTION(
375 "nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() "
377 directionAndAmountResult
= nsIEditor::eNone
;
379 case nsIEditor::eToEndOfLine
:
381 frameSelection
->CreateRangeExtendedToNextHardLineBreak
<nsRange
>();
382 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
383 return Err(NS_ERROR_EDITOR_DESTROYED
);
385 NS_WARNING_ASSERTION(
387 "nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed");
388 directionAndAmountResult
= nsIEditor::eNext
;
391 return aDirectionAndAmount
;
394 if (result
.isErr()) {
395 return Err(result
.inspectErr());
397 RefPtr
<nsRange
> extendedRange(result
.unwrap().forget());
398 if (!extendedRange
|| NS_WARN_IF(!extendedRange
->IsPositioned())) {
399 NS_WARNING("Failed to extend the range, but ignored");
400 return directionAndAmountResult
;
403 // If the new range isn't editable, keep using the original range.
404 if (aEditorBase
.IsHTMLEditor() &&
405 !AutoRangeArray::IsEditableRange(*extendedRange
, *editingHost
)) {
406 return aDirectionAndAmount
;
409 if (NS_WARN_IF(!frameSelection
->IsValidSelectionPoint(
410 extendedRange
->GetStartContainer())) ||
411 NS_WARN_IF(!frameSelection
->IsValidSelectionPoint(
412 extendedRange
->GetEndContainer()))) {
413 NS_WARNING("A range was extended to outer of selection limiter");
414 return Err(NS_ERROR_FAILURE
);
417 // Swap focus/anchor range with the extended range.
418 DebugOnly
<bool> found
= false;
419 for (OwningNonNull
<nsRange
>& range
: mRanges
) {
420 if (range
== mAnchorFocusRange
) {
421 range
= *extendedRange
;
427 mAnchorFocusRange
.swap(extendedRange
);
428 return directionAndAmountResult
;
431 Result
<bool, nsresult
>
432 AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
433 const HTMLEditor
& aHTMLEditor
, nsIEditor::EDirection aDirectionAndAmount
,
434 IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent
,
435 const Element
* aEditingHost
) {
440 switch (aDirectionAndAmount
) {
441 case nsIEditor::eNext
:
442 case nsIEditor::eNextWord
:
443 case nsIEditor::ePrevious
:
444 case nsIEditor::ePreviousWord
:
450 bool changed
= false;
451 for (const OwningNonNull
<nsRange
>& range
: mRanges
) {
452 MOZ_ASSERT(!range
->IsInAnySelection(),
453 "Changing range in selection may cause running script");
454 Result
<bool, nsresult
> result
=
455 WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
456 aHTMLEditor
, range
, aEditingHost
);
457 if (result
.isErr()) {
459 "WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() "
461 return Err(result
.inspectErr());
463 changed
|= result
.inspect();
466 if (mRanges
.Length() == 1 && aIfSelectingOnlyOneAtomicContent
==
467 IfSelectingOnlyOneAtomicContent::Collapse
) {
468 MOZ_ASSERT(mRanges
[0].get() == mAnchorFocusRange
.get());
469 if (mAnchorFocusRange
->GetStartContainer() ==
470 mAnchorFocusRange
->GetEndContainer() &&
471 mAnchorFocusRange
->GetChildAtStartOffset() &&
472 mAnchorFocusRange
->StartRef().GetNextSiblingOfChildAtOffset() ==
473 mAnchorFocusRange
->GetChildAtEndOffset()) {
474 mAnchorFocusRange
->Collapse(aDirectionAndAmount
== nsIEditor::eNext
||
475 aDirectionAndAmount
== nsIEditor::eNextWord
);
483 bool AutoRangeArray::SaveAndTrackRanges(HTMLEditor
& aHTMLEditor
) {
484 if (mSavedRanges
.isSome()) {
487 mSavedRanges
.emplace(*this);
488 aHTMLEditor
.RangeUpdaterRef().RegisterSelectionState(mSavedRanges
.ref());
489 mTrackingHTMLEditor
= &aHTMLEditor
;
493 void AutoRangeArray::ClearSavedRanges() {
494 if (mSavedRanges
.isNothing()) {
497 OwningNonNull
<HTMLEditor
> htmlEditor(std::move(mTrackingHTMLEditor
));
498 MOZ_ASSERT(!mTrackingHTMLEditor
);
499 htmlEditor
->RangeUpdaterRef().DropSelectionState(mSavedRanges
.ref());
500 mSavedRanges
.reset();
504 void AutoRangeArray::
505 UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
506 EditorDOMPoint
& aStartPoint
, EditorDOMPoint
& aEndPoint
,
507 const Element
& aEditingHost
) {
508 // FYI: This was moved from
509 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6743
512 // The GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() and
513 // GetPointAfterFollowingLineBreakOrAtFollowingBlock() don't really do the
514 // right thing for collapsed ranges inside block elements that contain nothing
515 // but a solo <br>. It's easier/ to put a workaround here than to revamp
517 if (aStartPoint
!= aEndPoint
) {
521 if (!aStartPoint
.IsInContentNode()) {
525 // XXX Perhaps, this should be more careful. This may not select only one
526 // node because this just check whether the block is empty or not,
527 // and may not select in non-editable block. However, for inline
528 // editing host case, it's right to look for block element without
529 // editable state check. Now, this method is used for preparation for
530 // other things. So, cannot write test for this method behavior.
531 // So, perhaps, we should get rid of this method and each caller should
532 // handle its job better.
533 Element
* const maybeNonEditableBlockElement
=
534 HTMLEditUtils::GetInclusiveAncestorElement(
535 *aStartPoint
.ContainerAs
<nsIContent
>(),
536 HTMLEditUtils::ClosestBlockElement
,
537 BlockInlineCheck::UseComputedDisplayStyle
);
538 if (!maybeNonEditableBlockElement
) {
542 // Make sure we don't go higher than our root element in the content tree
543 if (aEditingHost
.IsInclusiveDescendantOf(maybeNonEditableBlockElement
)) {
547 if (HTMLEditUtils::IsEmptyNode(
548 *maybeNonEditableBlockElement
,
549 {EmptyCheckOption::TreatNonEditableContentAsInvisible
})) {
550 aStartPoint
.Set(maybeNonEditableBlockElement
, 0u);
551 aEndPoint
.SetToEndOf(maybeNonEditableBlockElement
);
556 * Get the point before the line containing aPointInLine.
558 * @return If the line starts after a `<br>` element, returns next
559 * sibling of the `<br>` element.
560 * If the line is first line of a block, returns point of
562 * NOTE: The result may be point of editing host. I.e., the container may be
563 * outside of editing host.
565 static EditorDOMPoint
566 GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
567 const EditorDOMPoint
& aPointInLine
, EditSubAction aEditSubAction
,
568 BlockInlineCheck aBlockInlineCheck
, const Element
& aEditingHost
) {
569 // FYI: This was moved from
570 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6447
572 if (NS_WARN_IF(!aPointInLine
.IsSet())) {
573 return EditorDOMPoint();
576 EditorDOMPoint
point(aPointInLine
);
577 // Start scanning from the container node if aPoint is in a text node.
578 // XXX Perhaps, IsInDataNode() must be expected.
579 if (point
.IsInTextNode()) {
580 if (!point
.GetContainer()->GetParentNode()) {
581 // Okay, can't promote any further
582 // XXX Why don't we return start of the text node?
585 // If there is a preformatted linefeed in the text node, let's return
586 // the point after it.
587 EditorDOMPoint atLastPreformattedNewLine
=
588 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode
<EditorDOMPoint
>(
590 if (atLastPreformattedNewLine
.IsSet()) {
591 return atLastPreformattedNewLine
.NextPoint();
593 point
.Set(point
.GetContainer());
596 // Look back through any further inline nodes that aren't across a <br>
597 // from us, and that are enclosed in the same block.
598 // I.e., looking for start of current hard line.
599 constexpr HTMLEditUtils::WalkTreeOptions
600 ignoreNonEditableNodeAndStopAtBlockBoundary
{
601 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode
,
602 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary
};
603 for (nsIContent
* previousEditableContent
= HTMLEditUtils::GetPreviousContent(
604 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
605 aBlockInlineCheck
, &aEditingHost
);
606 previousEditableContent
&& previousEditableContent
->GetParentNode() &&
607 !HTMLEditUtils::IsVisibleBRElement(*previousEditableContent
) &&
608 !HTMLEditUtils::IsBlockElement(*previousEditableContent
,
610 previousEditableContent
= HTMLEditUtils::GetPreviousContent(
611 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
612 aBlockInlineCheck
, &aEditingHost
)) {
613 EditorDOMPoint atLastPreformattedNewLine
=
614 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode
<EditorDOMPoint
>(
615 EditorRawDOMPoint::AtEndOf(*previousEditableContent
));
616 if (atLastPreformattedNewLine
.IsSet()) {
617 return atLastPreformattedNewLine
.NextPoint();
619 point
.Set(previousEditableContent
);
622 // Finding the real start for this point unless current line starts after
623 // <br> element. Look up the tree for as long as we are the first node in
624 // the container (typically, start of nearest block ancestor), and as long
625 // as we haven't hit the body node.
626 for (nsIContent
* nearContent
= HTMLEditUtils::GetPreviousContent(
627 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
628 aBlockInlineCheck
, &aEditingHost
);
629 !nearContent
&& !point
.IsContainerHTMLElement(nsGkAtoms::body
) &&
630 point
.GetContainerParent();
631 nearContent
= HTMLEditUtils::GetPreviousContent(
632 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
633 aBlockInlineCheck
, &aEditingHost
)) {
634 // Don't keep looking up if we have found a blockquote element to act on
635 // when we handle outdent.
636 // XXX Sounds like this is hacky. If possible, it should be check in
637 // outdent handler for consistency between edit sub-actions.
638 // We should check Chromium's behavior of outdent when Selection
639 // starts from `<blockquote>` and starts from first child of
641 if (aEditSubAction
== EditSubAction::eOutdent
&&
642 point
.IsContainerHTMLElement(nsGkAtoms::blockquote
)) {
646 // Don't walk past the editable section. Note that we need to check
647 // before walking up to a parent because we need to return the parent
648 // object, so the parent itself might not be in the editable area, but
649 // it's OK if we're not performing a block-level action.
650 bool blockLevelAction
=
651 aEditSubAction
== EditSubAction::eIndent
||
652 aEditSubAction
== EditSubAction::eOutdent
||
653 aEditSubAction
== EditSubAction::eSetOrClearAlignment
||
654 aEditSubAction
== EditSubAction::eCreateOrRemoveBlock
||
655 aEditSubAction
== EditSubAction::eFormatBlockForHTMLCommand
;
656 // XXX So, does this check whether the container is removable or not? It
657 // seems that here can be rewritten as obviously what here tries to
659 if (!point
.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost
) &&
661 !point
.GetContainer()->IsInclusiveDescendantOf(&aEditingHost
))) {
665 // If we're formatting a block, we should reformat first ancestor format
667 if (aEditSubAction
== EditSubAction::eFormatBlockForHTMLCommand
&&
668 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
669 *point
.ContainerAs
<Element
>())) {
670 point
.Set(point
.GetContainer());
674 point
.Set(point
.GetContainer());
680 * Get the point after the following line break or the block which breaks the
681 * line containing aPointInLine.
683 * @return If the line ends with a visible `<br>` element, returns
684 * the point after the `<br>` element.
685 * If the line ends with a preformatted linefeed, returns
686 * the point after the linefeed unless it's an invisible
687 * line break immediately before a block boundary.
688 * If the line ends with a block boundary, returns the
689 * point of the block.
691 static EditorDOMPoint
GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
692 const EditorDOMPoint
& aPointInLine
, EditSubAction aEditSubAction
,
693 BlockInlineCheck aBlockInlineCheck
, const Element
& aEditingHost
) {
694 // FYI: This was moved from
695 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6541
697 if (NS_WARN_IF(!aPointInLine
.IsSet())) {
698 return EditorDOMPoint();
701 EditorDOMPoint
point(aPointInLine
);
702 // Start scanning from the container node if aPoint is in a text node.
703 // XXX Perhaps, IsInDataNode() must be expected.
704 if (point
.IsInTextNode()) {
705 if (NS_WARN_IF(!point
.GetContainer()->GetParentNode())) {
706 // Okay, can't promote any further
707 // XXX Why don't we return end of the text node?
710 EditorDOMPoint atNextPreformattedNewLine
=
711 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode
<
712 EditorDOMPoint
>(point
);
713 if (atNextPreformattedNewLine
.IsSet()) {
714 // If the linefeed is last character of the text node, it may be
715 // invisible if it's immediately before a block boundary. In such
716 // case, we should return the block boundary.
717 Element
* maybeNonEditableBlockElement
= nullptr;
718 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
719 atNextPreformattedNewLine
, &maybeNonEditableBlockElement
) &&
720 maybeNonEditableBlockElement
) {
721 // If the block is a parent of the editing host, let's return end
723 if (maybeNonEditableBlockElement
== &aEditingHost
||
724 !maybeNonEditableBlockElement
->IsInclusiveDescendantOf(
726 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement
);
728 // If it's invisible because of parent block boundary, return end
729 // of the block. Otherwise, i.e., it's followed by a child block,
730 // returns the point of the child block.
731 if (atNextPreformattedNewLine
.ContainerAs
<Text
>()
732 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement
)) {
733 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement
);
735 return EditorDOMPoint(maybeNonEditableBlockElement
);
737 // Otherwise, return the point after the preformatted linefeed.
738 return atNextPreformattedNewLine
.NextPoint();
740 // want to be after the text node
741 point
.SetAfter(point
.GetContainer());
742 NS_WARNING_ASSERTION(point
.IsSet(), "Failed to set to after the text node");
745 // Look ahead through any further inline nodes that aren't across a <br> from
746 // us, and that are enclosed in the same block.
747 // XXX Currently, we stop block-extending when finding visible <br> element.
748 // This might be different from "block-extend" of execCommand spec.
749 // However, the spec is really unclear.
750 // XXX Probably, scanning only editable nodes is wrong for
751 // EditSubAction::eCreateOrRemoveBlock and
752 // EditSubAction::eFormatBlockForHTMLCommand because it might be better to
753 // wrap existing inline elements even if it's non-editable. For example,
754 // following examples with insertParagraph causes different result:
755 // * <div contenteditable>foo[]<b contenteditable="false">bar</b></div>
756 // * <div contenteditable>foo[]<b>bar</b></div>
757 // * <div contenteditable>foo[]<b contenteditable="false">bar</b>baz</div>
758 // Only in the first case, after the caret position isn't wrapped with
759 // new <div> element.
760 constexpr HTMLEditUtils::WalkTreeOptions
761 ignoreNonEditableNodeAndStopAtBlockBoundary
{
762 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode
,
763 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary
};
764 for (nsIContent
* nextEditableContent
= HTMLEditUtils::GetNextContent(
765 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
766 aBlockInlineCheck
, &aEditingHost
);
767 nextEditableContent
&&
768 !HTMLEditUtils::IsBlockElement(*nextEditableContent
,
769 aBlockInlineCheck
) &&
770 nextEditableContent
->GetParent();
771 nextEditableContent
= HTMLEditUtils::GetNextContent(
772 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
773 aBlockInlineCheck
, &aEditingHost
)) {
774 EditorDOMPoint atFirstPreformattedNewLine
=
775 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode
<
776 EditorDOMPoint
>(EditorRawDOMPoint(nextEditableContent
, 0));
777 if (atFirstPreformattedNewLine
.IsSet()) {
778 // If the linefeed is last character of the text node, it may be
779 // invisible if it's immediately before a block boundary. In such
780 // case, we should return the block boundary.
781 Element
* maybeNonEditableBlockElement
= nullptr;
782 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
783 atFirstPreformattedNewLine
, &maybeNonEditableBlockElement
) &&
784 maybeNonEditableBlockElement
) {
785 // If the block is a parent of the editing host, let's return end
787 if (maybeNonEditableBlockElement
== &aEditingHost
||
788 !maybeNonEditableBlockElement
->IsInclusiveDescendantOf(
790 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement
);
792 // If it's invisible because of parent block boundary, return end
793 // of the block. Otherwise, i.e., it's followed by a child block,
794 // returns the point of the child block.
795 if (atFirstPreformattedNewLine
.ContainerAs
<Text
>()
796 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement
)) {
797 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement
);
799 return EditorDOMPoint(maybeNonEditableBlockElement
);
801 // Otherwise, return the point after the preformatted linefeed.
802 return atFirstPreformattedNewLine
.NextPoint();
804 point
.SetAfter(nextEditableContent
);
805 if (NS_WARN_IF(!point
.IsSet())) {
808 if (HTMLEditUtils::IsVisibleBRElement(*nextEditableContent
)) {
813 // Finding the real end for this point unless current line ends with a <br>
814 // element. Look up the tree for as long as we are the last node in the
815 // container (typically, block node), and as long as we haven't hit the body
817 for (nsIContent
* nearContent
= HTMLEditUtils::GetNextContent(
818 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
819 aBlockInlineCheck
, &aEditingHost
);
820 !nearContent
&& !point
.IsContainerHTMLElement(nsGkAtoms::body
) &&
821 point
.GetContainerParent();
822 nearContent
= HTMLEditUtils::GetNextContent(
823 point
, ignoreNonEditableNodeAndStopAtBlockBoundary
,
824 aBlockInlineCheck
, &aEditingHost
)) {
825 // Don't walk past the editable section. Note that we need to check before
826 // walking up to a parent because we need to return the parent object, so
827 // the parent itself might not be in the editable area, but it's OK.
828 // XXX Maybe returning parent of editing host is really error prone since
829 // everybody need to check whether the end point is in editing host
830 // when they touch there.
831 if (!point
.GetContainer()->IsInclusiveDescendantOf(&aEditingHost
) &&
832 !point
.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost
)) {
836 // If we're formatting a block, we should reformat first ancestor format
838 if (aEditSubAction
== EditSubAction::eFormatBlockForHTMLCommand
&&
839 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
840 *point
.ContainerAs
<Element
>())) {
841 point
.SetAfter(point
.GetContainer());
845 point
.SetAfter(point
.GetContainer());
846 if (NS_WARN_IF(!point
.IsSet())) {
853 void AutoRangeArray::ExtendRangesToWrapLines(EditSubAction aEditSubAction
,
854 BlockInlineCheck aBlockInlineCheck
,
855 const Element
& aEditingHost
) {
856 // FYI: This is originated in
857 // https://searchfox.org/mozilla-central/rev/1739f1301d658c9bff544a0a095ab11fca2e549d/editor/libeditor/HTMLEditSubActionHandler.cpp#6712
859 bool removeSomeRanges
= false;
860 for (const OwningNonNull
<nsRange
>& range
: mRanges
) {
861 // Remove non-positioned ranges.
862 if (MOZ_UNLIKELY(!range
->IsPositioned())) {
863 removeSomeRanges
= true;
866 // If the range is native anonymous subtrees, we must meet a bug of
867 // `Selection` so that we need to hack here.
868 if (MOZ_UNLIKELY(range
->GetStartContainer()->IsInNativeAnonymousSubtree() ||
869 range
->GetEndContainer()->IsInNativeAnonymousSubtree())) {
870 EditorRawDOMRange
rawRange(range
);
871 if (!rawRange
.EnsureNotInNativeAnonymousSubtree()) {
873 removeSomeRanges
= true;
877 range
->SetStartAndEnd(rawRange
.StartRef().ToRawRangeBoundary(),
878 rawRange
.EndRef().ToRawRangeBoundary())) ||
879 MOZ_UNLIKELY(!range
->IsPositioned())) {
881 removeSomeRanges
= true;
885 // Finally, extend the range.
886 if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
887 range
, aEditSubAction
, aBlockInlineCheck
, aEditingHost
))) {
888 // If we failed to extend the range, we should use the original range
889 // as-is unless the range is broken at setting the range.
890 if (NS_WARN_IF(!range
->IsPositioned())) {
891 removeSomeRanges
= true;
895 if (removeSomeRanges
) {
896 for (const size_t i
: Reversed(IntegerRange(mRanges
.Length()))) {
897 if (!mRanges
[i
]->IsPositioned()) {
898 mRanges
.RemoveElementAt(i
);
901 if (!mAnchorFocusRange
|| !mAnchorFocusRange
->IsPositioned()) {
902 if (mRanges
.IsEmpty()) {
903 mAnchorFocusRange
= nullptr;
905 mAnchorFocusRange
= mRanges
.LastElement();
912 nsresult
AutoRangeArray::ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
913 nsRange
& aRange
, EditSubAction aEditSubAction
,
914 BlockInlineCheck aBlockInlineCheck
, const Element
& aEditingHost
) {
915 MOZ_DIAGNOSTIC_ASSERT(
916 !EditorRawDOMPoint(aRange
.StartRef()).IsInNativeAnonymousSubtree());
917 MOZ_DIAGNOSTIC_ASSERT(
918 !EditorRawDOMPoint(aRange
.EndRef()).IsInNativeAnonymousSubtree());
920 if (NS_WARN_IF(!aRange
.IsPositioned())) {
921 return NS_ERROR_INVALID_ARG
;
924 EditorDOMPoint
startPoint(aRange
.StartRef()), endPoint(aRange
.EndRef());
926 // If we're joining blocks, we call this for selecting a line to move.
927 // Therefore, we don't want to select the ancestor blocks in this case
928 // even if they are empty.
929 if (aEditSubAction
!= EditSubAction::eMergeBlockContents
) {
931 UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
932 startPoint
, endPoint
, aEditingHost
);
935 // Make a new adjusted range to represent the appropriate block content.
936 // This is tricky. The basic idea is to push out the range endpoints to
937 // truly enclose the blocks that we will affect.
939 // Make sure that the new range ends up to be in the editable section.
940 // XXX Looks like that this check wastes the time. Perhaps, we should
941 // implement a method which checks both two DOM points in the editor
945 GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
946 startPoint
, aEditSubAction
, aBlockInlineCheck
, aEditingHost
);
947 // XXX GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() may
948 // return point of editing host. Perhaps, we should change it and stop
949 // checking it here since this check may be expensive.
950 // XXX If the container is an element in the editing host but it points end of
951 // the container, this returns nullptr. Is it intentional?
952 if (!startPoint
.GetChildOrContainerIfDataNode() ||
953 !startPoint
.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
955 return NS_ERROR_FAILURE
;
957 endPoint
= GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
958 endPoint
, aEditSubAction
, aBlockInlineCheck
, aEditingHost
);
959 const EditorDOMPoint lastRawPoint
=
960 endPoint
.IsStartOfContainer() ? endPoint
: endPoint
.PreviousPoint();
961 // XXX GetPointAfterFollowingLineBreakOrAtFollowingBlock() may return point of
962 // editing host. Perhaps, we should change it and stop checking it here
963 // since this check may be expensive.
964 // XXX If the container is an element in the editing host but it points end of
965 // the container, this returns nullptr. Is it intentional?
966 if (!lastRawPoint
.GetChildOrContainerIfDataNode() ||
967 !lastRawPoint
.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
969 return NS_ERROR_FAILURE
;
972 nsresult rv
= aRange
.SetStartAndEnd(startPoint
.ToRawRangeBoundary(),
973 endPoint
.ToRawRangeBoundary());
975 return NS_ERROR_FAILURE
;
980 Result
<EditorDOMPoint
, nsresult
>
981 AutoRangeArray::SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
982 HTMLEditor
& aHTMLEditor
, BlockInlineCheck aBlockInlineCheck
,
983 const Element
& aEditingHost
,
984 const nsIContent
* aAncestorLimiter
/* = nullptr */) {
985 // FYI: The following code is originated in
986 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#6971
988 // Split text nodes. This is necessary, since given ranges may end in text
989 // nodes in case where part of a pre-formatted elements needs to be moved.
990 EditorDOMPoint pointToPutCaret
;
991 IgnoredErrorResult ignoredError
;
992 for (const OwningNonNull
<nsRange
>& range
: mRanges
) {
993 EditorDOMPoint
atEnd(range
->EndRef());
994 if (NS_WARN_IF(!atEnd
.IsSet()) || !atEnd
.IsInTextNode() ||
995 atEnd
.GetContainer() == aAncestorLimiter
) {
999 if (!atEnd
.IsStartOfContainer() && !atEnd
.IsEndOfContainer()) {
1000 // Split the text node.
1001 Result
<SplitNodeResult
, nsresult
> splitAtEndResult
=
1002 aHTMLEditor
.SplitNodeWithTransaction(atEnd
);
1003 if (MOZ_UNLIKELY(splitAtEndResult
.isErr())) {
1004 NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
1005 return splitAtEndResult
.propagateErr();
1007 SplitNodeResult unwrappedSplitAtEndResult
= splitAtEndResult
.unwrap();
1008 unwrappedSplitAtEndResult
.MoveCaretPointTo(
1009 pointToPutCaret
, {SuggestCaret::OnlyIfHasSuggestion
});
1011 // Correct the range.
1012 // The new end parent becomes the parent node of the text.
1013 MOZ_ASSERT(!range
->IsInAnySelection());
1014 range
->SetEnd(unwrappedSplitAtEndResult
.AtNextContent
<EditorRawDOMPoint
>()
1015 .ToRawRangeBoundary(),
1017 NS_WARNING_ASSERTION(!ignoredError
.Failed(),
1018 "nsRange::SetEnd() failed, but ignored");
1019 ignoredError
.SuppressException();
1023 // FYI: The following code is originated in
1024 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#7023
1025 AutoTArray
<OwningNonNull
<RangeItem
>, 8> rangeItemArray
;
1026 rangeItemArray
.AppendElements(mRanges
.Length());
1028 // First register ranges for special editor gravity
1029 Maybe
<size_t> anchorFocusRangeIndex
;
1030 for (const size_t index
: IntegerRange(rangeItemArray
.Length())) {
1031 rangeItemArray
[index
] = new RangeItem();
1032 rangeItemArray
[index
]->StoreRange(*mRanges
[index
]);
1033 aHTMLEditor
.RangeUpdaterRef().RegisterRangeItem(*rangeItemArray
[index
]);
1034 if (mRanges
[index
] == mAnchorFocusRange
) {
1035 anchorFocusRangeIndex
= Some(index
);
1038 // TODO: We should keep the array, and just update the ranges.
1040 mAnchorFocusRange
= nullptr;
1041 // Now bust up inlines.
1042 nsresult rv
= NS_OK
;
1043 for (const OwningNonNull
<RangeItem
>& item
: Reversed(rangeItemArray
)) {
1044 // MOZ_KnownLive because 'rangeItemArray' is guaranteed to keep it alive.
1045 Result
<EditorDOMPoint
, nsresult
> splitParentsResult
=
1046 aHTMLEditor
.SplitInlineAncestorsAtRangeBoundaries(
1047 MOZ_KnownLive(*item
), aBlockInlineCheck
, aEditingHost
,
1049 if (MOZ_UNLIKELY(splitParentsResult
.isErr())) {
1050 NS_WARNING("HTMLEditor::SplitInlineAncestorsAtRangeBoundaries() failed");
1051 rv
= splitParentsResult
.unwrapErr();
1054 if (splitParentsResult
.inspect().IsSet()) {
1055 pointToPutCaret
= splitParentsResult
.unwrap();
1058 // Then unregister the ranges
1059 for (const size_t index
: IntegerRange(rangeItemArray
.Length())) {
1060 aHTMLEditor
.RangeUpdaterRef().DropRangeItem(rangeItemArray
[index
]);
1061 RefPtr
<nsRange
> range
= rangeItemArray
[index
]->GetRange();
1062 if (range
&& range
->IsPositioned()) {
1063 if (anchorFocusRangeIndex
.isSome() && index
== *anchorFocusRangeIndex
) {
1064 mAnchorFocusRange
= range
;
1066 mRanges
.AppendElement(std::move(range
));
1069 if (!mAnchorFocusRange
&& !mRanges
.IsEmpty()) {
1070 mAnchorFocusRange
= mRanges
.LastElement();
1073 // XXX Why do we ignore the other errors here??
1074 if (NS_WARN_IF(rv
== NS_ERROR_EDITOR_DESTROYED
)) {
1075 return Err(NS_ERROR_EDITOR_DESTROYED
);
1077 return pointToPutCaret
;
1080 nsresult
AutoRangeArray::CollectEditTargetNodes(
1081 const HTMLEditor
& aHTMLEditor
,
1082 nsTArray
<OwningNonNull
<nsIContent
>>& aOutArrayOfContents
,
1083 EditSubAction aEditSubAction
,
1084 CollectNonEditableNodes aCollectNonEditableNodes
) const {
1085 MOZ_ASSERT(aHTMLEditor
.IsEditActionDataAvailable());
1087 // FYI: This was moved from
1088 // https://searchfox.org/mozilla-central/rev/4bce7d85ba4796dd03c5dcc7cfe8eee0e4c07b3b/editor/libeditor/HTMLEditSubActionHandler.cpp#7060
1090 // Gather up a list of all the nodes
1091 for (const OwningNonNull
<nsRange
>& range
: mRanges
) {
1092 DOMSubtreeIterator iter
;
1093 nsresult rv
= iter
.Init(*range
);
1094 if (NS_FAILED(rv
)) {
1095 NS_WARNING("DOMSubtreeIterator::Init() failed");
1098 if (aOutArrayOfContents
.IsEmpty()) {
1099 iter
.AppendAllNodesToArray(aOutArrayOfContents
);
1101 AutoTArray
<OwningNonNull
<nsIContent
>, 24> arrayOfTopChildren
;
1102 iter
.AppendNodesToArray(
1103 +[](nsINode
& aNode
, void* aArray
) -> bool {
1105 return !static_cast<nsTArray
<OwningNonNull
<nsIContent
>>*>(aArray
)
1108 arrayOfTopChildren
, &aOutArrayOfContents
);
1109 aOutArrayOfContents
.AppendElements(std::move(arrayOfTopChildren
));
1111 if (aCollectNonEditableNodes
== CollectNonEditableNodes::No
) {
1112 for (const size_t i
:
1113 Reversed(IntegerRange(aOutArrayOfContents
.Length()))) {
1114 if (!EditorUtils::IsEditableContent(aOutArrayOfContents
[i
],
1115 EditorUtils::EditorType::HTML
)) {
1116 aOutArrayOfContents
.RemoveElementAt(i
);
1122 switch (aEditSubAction
) {
1123 case EditSubAction::eCreateOrRemoveBlock
:
1124 case EditSubAction::eFormatBlockForHTMLCommand
: {
1125 // Certain operations should not act on li's and td's, but rather inside
1126 // them. Alter the list as needed.
1127 CollectChildrenOptions options
= {
1128 CollectChildrenOption::CollectListChildren
,
1129 CollectChildrenOption::CollectTableChildren
};
1130 if (aCollectNonEditableNodes
== CollectNonEditableNodes::No
) {
1131 options
+= CollectChildrenOption::IgnoreNonEditableChildren
;
1133 if (aEditSubAction
== EditSubAction::eCreateOrRemoveBlock
) {
1134 for (const size_t index
:
1135 Reversed(IntegerRange(aOutArrayOfContents
.Length()))) {
1136 OwningNonNull
<nsIContent
> content
= aOutArrayOfContents
[index
];
1137 if (HTMLEditUtils::IsListItem(content
)) {
1138 aOutArrayOfContents
.RemoveElementAt(index
);
1139 HTMLEditUtils::CollectChildren(*content
, aOutArrayOfContents
, index
,
1144 // <dd> and <dt> are format blocks. Therefore, we should not handle
1145 // their children directly. They should be replaced with new format
1148 HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dt
));
1150 HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dd
));
1151 for (const size_t index
:
1152 Reversed(IntegerRange(aOutArrayOfContents
.Length()))) {
1153 OwningNonNull
<nsIContent
> content
= aOutArrayOfContents
[index
];
1154 MOZ_ASSERT_IF(HTMLEditUtils::IsListItem(content
),
1155 content
->IsAnyOfHTMLElements(
1156 nsGkAtoms::dd
, nsGkAtoms::dt
, nsGkAtoms::li
));
1157 if (content
->IsHTMLElement(nsGkAtoms::li
)) {
1158 aOutArrayOfContents
.RemoveElementAt(index
);
1159 HTMLEditUtils::CollectChildren(*content
, aOutArrayOfContents
, index
,
1164 // Empty text node shouldn't be selected if unnecessary
1165 for (const size_t index
:
1166 Reversed(IntegerRange(aOutArrayOfContents
.Length()))) {
1167 if (const Text
* text
= aOutArrayOfContents
[index
]->GetAsText()) {
1168 // Don't select empty text except to empty block
1169 if (!HTMLEditUtils::IsVisibleTextNode(*text
)) {
1170 aOutArrayOfContents
.RemoveElementAt(index
);
1176 case EditSubAction::eCreateOrChangeList
: {
1177 // XXX aCollectNonEditableNodes is ignored here. Maybe a bug.
1178 CollectChildrenOptions options
= {
1179 CollectChildrenOption::CollectTableChildren
};
1180 for (const size_t index
:
1181 Reversed(IntegerRange(aOutArrayOfContents
.Length()))) {
1182 // Scan for table elements. If we find table elements other than
1183 // table, replace it with a list of any editable non-table content
1184 // because if a selection range starts from end in a table-cell and
1185 // ends at or starts from outside the `<table>`, we need to make
1186 // lists in each selected table-cells.
1187 OwningNonNull
<nsIContent
> content
= aOutArrayOfContents
[index
];
1188 if (HTMLEditUtils::IsAnyTableElementButNotTable(content
)) {
1189 aOutArrayOfContents
.RemoveElementAt(index
);
1190 HTMLEditUtils::CollectChildren(content
, aOutArrayOfContents
, index
,
1194 // If there is only one node in the array, and it is a `<div>`,
1195 // `<blockquote>` or a list element, then look inside of it until we
1196 // find inner list or content.
1197 if (aOutArrayOfContents
.Length() != 1) {
1200 Element
* deepestDivBlockquoteOrListElement
=
1201 HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
1202 aOutArrayOfContents
[0],
1203 {HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode
},
1204 BlockInlineCheck::Unused
, nsGkAtoms::div
, nsGkAtoms::blockquote
,
1205 nsGkAtoms::ul
, nsGkAtoms::ol
, nsGkAtoms::dl
);
1206 if (!deepestDivBlockquoteOrListElement
) {
1209 if (deepestDivBlockquoteOrListElement
->IsAnyOfHTMLElements(
1210 nsGkAtoms::div
, nsGkAtoms::blockquote
)) {
1211 aOutArrayOfContents
.Clear();
1212 // XXX Before we're called, non-editable nodes are ignored. However,
1213 // we may append non-editable nodes here.
1214 HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement
,
1215 aOutArrayOfContents
, 0, {});
1218 aOutArrayOfContents
.ReplaceElementAt(
1219 0, OwningNonNull
<nsIContent
>(*deepestDivBlockquoteOrListElement
));
1222 case EditSubAction::eOutdent
:
1223 case EditSubAction::eIndent
:
1224 case EditSubAction::eSetPositionToAbsolute
: {
1225 // Indent/outdent already do something special for list items, but we
1226 // still need to make sure we don't act on table elements
1227 CollectChildrenOptions options
= {
1228 CollectChildrenOption::CollectListChildren
,
1229 CollectChildrenOption::CollectTableChildren
};
1230 if (aCollectNonEditableNodes
== CollectNonEditableNodes::No
) {
1231 options
+= CollectChildrenOption::IgnoreNonEditableChildren
;
1233 for (const size_t index
:
1234 Reversed(IntegerRange(aOutArrayOfContents
.Length()))) {
1235 OwningNonNull
<nsIContent
> content
= aOutArrayOfContents
[index
];
1236 if (HTMLEditUtils::IsAnyTableElementButNotTable(content
)) {
1237 aOutArrayOfContents
.RemoveElementAt(index
);
1238 HTMLEditUtils::CollectChildren(*content
, aOutArrayOfContents
, index
,
1248 // Outdent should look inside of divs.
1249 if (aEditSubAction
== EditSubAction::eOutdent
&&
1250 !aHTMLEditor
.IsCSSEnabled()) {
1251 CollectChildrenOptions options
= {};
1252 if (aCollectNonEditableNodes
== CollectNonEditableNodes::No
) {
1253 options
+= CollectChildrenOption::IgnoreNonEditableChildren
;
1255 for (const size_t index
:
1256 Reversed(IntegerRange(aOutArrayOfContents
.Length()))) {
1257 OwningNonNull
<nsIContent
> content
= aOutArrayOfContents
[index
];
1258 if (content
->IsHTMLElement(nsGkAtoms::div
)) {
1259 aOutArrayOfContents
.RemoveElementAt(index
);
1260 HTMLEditUtils::CollectChildren(*content
, aOutArrayOfContents
, index
,
1269 Element
* AutoRangeArray::GetClosestAncestorAnyListElementOfRange() const {
1270 for (const OwningNonNull
<nsRange
>& range
: mRanges
) {
1271 nsINode
* commonAncestorNode
= range
->GetClosestCommonInclusiveAncestor();
1272 if (MOZ_UNLIKELY(!commonAncestorNode
)) {
1275 for (Element
* const element
:
1276 commonAncestorNode
->InclusiveAncestorsOfType
<Element
>()) {
1277 if (HTMLEditUtils::IsAnyListElement(element
)) {
1285 } // namespace mozilla