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()) {
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
) {
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
) {
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()) {
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
);
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
);
150 // Additionally, if focus node is inert, the range should be collapsed to
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
) {
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
)) {
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.
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();
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(
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
;
278 case nsIEditor::ePreviousWord
:
280 frameSelection
->CreateRangeExtendedToPreviousWordBoundary
<nsRange
>();
281 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
282 return Err(NS_ERROR_EDITOR_DESTROYED
);
284 NS_WARNING_ASSERTION(
286 "nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() "
288 // DeleteSelectionWithTransaction() doesn't handle these actions
289 // because it's inside batching, so don't confuse it:
290 directionAndAmountResult
= nsIEditor::eNone
;
292 case nsIEditor::eNext
:
295 ->CreateRangeExtendedToNextGraphemeClusterBoundary
<nsRange
>();
296 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
297 return Err(NS_ERROR_EDITOR_DESTROYED
);
299 NS_WARNING_ASSERTION(result
.isOk(),
301 "CreateRangeExtendedToNextGraphemeClusterBoundary() "
303 // Don't set directionAndAmount to eNone (see Bug 502259)
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
311 // XXX This is odd if the previous one is a sequence for a grapheme
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(
323 : atStartOfSelection
.GetPointInTextNodeIfPointingAroundTextNode
<
325 if (MOZ_UNLIKELY(!insertionPoint
.IsSet())) {
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();
339 data
->IsLowSurrogateFollowingHighSurrogateAt(offset
- 1)) &&
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(
355 "CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed");
358 case nsIEditor::eToBeginningOfLine
:
360 frameSelection
->CreateRangeExtendedToPreviousHardLineBreak
<nsRange
>();
361 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
362 return Err(NS_ERROR_EDITOR_DESTROYED
);
364 NS_WARNING_ASSERTION(
366 "nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() "
368 directionAndAmountResult
= nsIEditor::eNone
;
370 case nsIEditor::eToEndOfLine
:
372 frameSelection
->CreateRangeExtendedToNextHardLineBreak
<nsRange
>();
373 if (NS_WARN_IF(aEditorBase
.Destroyed())) {
374 return Err(NS_ERROR_EDITOR_DESTROYED
);
376 NS_WARNING_ASSERTION(
378 "nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed");
379 directionAndAmountResult
= nsIEditor::eNext
;
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
;
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
) {
431 switch (aDirectionAndAmount
) {
432 case nsIEditor::eNext
:
433 case nsIEditor::eNextWord
:
434 case nsIEditor::ePrevious
:
435 case nsIEditor::ePreviousWord
:
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()) {
450 "WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() "
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
);
474 bool AutoRangeArray::SaveAndTrackRanges(HTMLEditor
& aHTMLEditor
) {
475 if (mSavedRanges
.isSome()) {
478 mSavedRanges
.emplace(*this);
479 aHTMLEditor
.RangeUpdaterRef().RegisterSelectionState(mSavedRanges
.ref());
480 mTrackingHTMLEditor
= &aHTMLEditor
;
484 void AutoRangeArray::ClearSavedRanges() {
485 if (mSavedRanges
.isNothing()) {
488 OwningNonNull
<HTMLEditor
> htmlEditor(std::move(mTrackingHTMLEditor
));
489 MOZ_ASSERT(!mTrackingHTMLEditor
);
490 htmlEditor
->RangeUpdaterRef().DropSelectionState(mSavedRanges
.ref());
491 mSavedRanges
.reset();
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
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
508 if (aStartPoint
!= aEndPoint
) {
512 if (!aStartPoint
.IsInContentNode()) {
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
) {
533 // Make sure we don't go higher than our root element in the content tree
534 if (aEditingHost
.IsInclusiveDescendantOf(maybeNonEditableBlockElement
)) {
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
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?
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
>(
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
,
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
632 if (aEditSubAction
== EditSubAction::eOutdent
&&
633 point
.IsContainerHTMLElement(nsGkAtoms::blockquote
)) {
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
650 if (!point
.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost
) &&
652 !point
.GetContainer()->IsInclusiveDescendantOf(&aEditingHost
))) {
656 // If we're formatting a block, we should reformat first ancestor format
658 if (aEditSubAction
== EditSubAction::eFormatBlockForHTMLCommand
&&
659 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
660 *point
.ContainerAs
<Element
>())) {
661 point
.Set(point
.GetContainer());
665 point
.Set(point
.GetContainer());
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?
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
714 if (maybeNonEditableBlockElement
== &aEditingHost
||
715 !maybeNonEditableBlockElement
->IsInclusiveDescendantOf(
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
778 if (maybeNonEditableBlockElement
== &aEditingHost
||
779 !maybeNonEditableBlockElement
->IsInclusiveDescendantOf(
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())) {
799 if (HTMLEditUtils::IsVisibleBRElement(*nextEditableContent
)) {
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
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
)) {
827 // If we're formatting a block, we should reformat first ancestor format
829 if (aEditSubAction
== EditSubAction::eFormatBlockForHTMLCommand
&&
830 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
831 *point
.ContainerAs
<Element
>())) {
832 point
.SetAfter(point
.GetContainer());
836 point
.SetAfter(point
.GetContainer());
837 if (NS_WARN_IF(!point
.IsSet())) {
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;
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()) {
864 removeSomeRanges
= true;
868 range
->SetStartAndEnd(rawRange
.StartRef().ToRawRangeBoundary(),
869 rawRange
.EndRef().ToRawRangeBoundary())) ||
870 MOZ_UNLIKELY(!range
->IsPositioned())) {
872 removeSomeRanges
= true;
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;
896 mAnchorFocusRange
= mRanges
.LastElement();
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
) {
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
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(
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(
960 return NS_ERROR_FAILURE
;
963 nsresult rv
= aRange
.SetStartAndEnd(startPoint
.ToRawRangeBoundary(),
964 endPoint
.ToRawRangeBoundary());
966 return NS_ERROR_FAILURE
;
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
) {
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(),
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.
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
,
1040 if (MOZ_UNLIKELY(splitParentsResult
.isErr())) {
1041 NS_WARNING("HTMLEditor::SplitInlineAncestorsAtRangeBoundaries() failed");
1042 rv
= splitParentsResult
.unwrapErr();
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");
1089 if (aOutArrayOfContents
.IsEmpty()) {
1090 iter
.AppendAllNodesToArray(aOutArrayOfContents
);
1092 AutoTArray
<OwningNonNull
<nsIContent
>, 24> arrayOfTopChildren
;
1093 iter
.AppendNodesToArray(
1094 +[](nsINode
& aNode
, void* aArray
) -> bool {
1096 return !static_cast<nsTArray
<OwningNonNull
<nsIContent
>>*>(aArray
)
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
,
1135 // <dd> and <dt> are format blocks. Therefore, we should not handle
1136 // their children directly. They should be replaced with new format
1139 HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dt
));
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
,
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
);
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
,
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) {
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
) {
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, {});
1209 aOutArrayOfContents
.ReplaceElementAt(
1210 0, OwningNonNull
<nsIContent
>(*deepestDivBlockquoteOrListElement
));
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
,
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
,
1260 Element
* AutoRangeArray::GetClosestAncestorAnyListElementOfRange() const {
1261 for (const OwningNonNull
<nsRange
>& range
: mRanges
) {
1262 nsINode
* commonAncestorNode
= range
->GetClosestCommonInclusiveAncestor();
1263 if (MOZ_UNLIKELY(!commonAncestorNode
)) {
1266 for (Element
* const element
:
1267 commonAncestorNode
->InclusiveAncestorsOfType
<Element
>()) {
1268 if (HTMLEditUtils::IsAnyListElement(element
)) {
1276 } // namespace mozilla