Bug 1776056 - Switch to the tab an animation is running and make sure the animation...
[gecko.git] / editor / libeditor / AutoRangeArray.cpp
blobac598ac8f0a2fb8ec627374cc48be07f852e4878
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 "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc
9 #include "EditorForwards.h" // for CollectChildrenOptions
10 #include "HTMLEditUtils.h" // for HTMLEditUtils
11 #include "HTMLEditHelpers.h" // for SplitNodeResult
12 #include "WSRunObject.h" // for WSRunScanner
14 #include "mozilla/OwningNonNull.h" // for OwningNonNull
15 #include "mozilla/dom/Document.h" // for dom::Document
16 #include "mozilla/dom/HTMLBRElement.h" // for dom HTMLBRElement
17 #include "mozilla/dom/Selection.h" // for dom::Selection
18 #include "mozilla/dom/Text.h" // for dom::Text
20 #include "gfxFontUtils.h" // for gfxFontUtils
21 #include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
22 #include "nsFrameSelection.h" // for nsFrameSelection
23 #include "nsIContent.h" // for nsIContent
24 #include "nsINode.h" // for nsINode
25 #include "nsRange.h" // for nsRange
26 #include "nsTextFragment.h" // for nsTextFragment
28 namespace mozilla {
30 using namespace dom;
32 /******************************************************************************
33 * mozilla::AutoRangeArray
34 *****************************************************************************/
36 template AutoRangeArray::AutoRangeArray(const EditorDOMRange& aRange);
37 template AutoRangeArray::AutoRangeArray(const EditorRawDOMRange& aRange);
38 template AutoRangeArray::AutoRangeArray(const EditorDOMPoint& aRange);
39 template AutoRangeArray::AutoRangeArray(const EditorRawDOMPoint& aRange);
41 AutoRangeArray::AutoRangeArray(const dom::Selection& aSelection) {
42 Initialize(aSelection);
45 AutoRangeArray::AutoRangeArray(const AutoRangeArray& aOther)
46 : mAnchorFocusRange(aOther.mAnchorFocusRange),
47 mDirection(aOther.mDirection) {
48 mRanges.SetCapacity(aOther.mRanges.Length());
49 for (const OwningNonNull<nsRange>& range : aOther.mRanges) {
50 RefPtr<nsRange> clonedRange = range->CloneRange();
51 mRanges.AppendElement(std::move(clonedRange));
55 template <typename PointType>
56 AutoRangeArray::AutoRangeArray(const EditorDOMRangeBase<PointType>& aRange) {
57 MOZ_ASSERT(aRange.IsPositionedAndValid());
58 RefPtr<nsRange> range = aRange.CreateRange(IgnoreErrors());
59 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
60 return;
62 mRanges.AppendElement(std::move(range));
65 template <typename PT, typename CT>
66 AutoRangeArray::AutoRangeArray(const EditorDOMPointBase<PT, CT>& aPoint) {
67 MOZ_ASSERT(aPoint.IsSetAndValid());
68 RefPtr<nsRange> range = aPoint.CreateCollapsedRange(IgnoreErrors());
69 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
70 return;
72 mRanges.AppendElement(std::move(range));
75 AutoRangeArray::~AutoRangeArray() {
76 if (mSavedRanges.isSome()) {
77 ClearSavedRanges();
81 // static
82 bool AutoRangeArray::IsEditableRange(const dom::AbstractRange& aRange,
83 const Element& aEditingHost) {
84 // TODO: Perhaps, we should check whether the start/end boundaries are
85 // first/last point of non-editable element.
86 // See https://github.com/w3c/editing/issues/283#issuecomment-788654850
87 EditorRawDOMPoint atStart(aRange.StartRef());
88 const bool isStartEditable =
89 atStart.IsInContentNode() &&
90 EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
91 EditorUtils::EditorType::HTML) &&
92 !HTMLEditUtils::IsNonEditableReplacedContent(
93 *atStart.ContainerAs<nsIContent>());
94 if (!isStartEditable) {
95 return false;
98 if (aRange.GetStartContainer() != aRange.GetEndContainer()) {
99 EditorRawDOMPoint atEnd(aRange.EndRef());
100 const bool isEndEditable =
101 atEnd.IsInContentNode() &&
102 EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
103 EditorUtils::EditorType::HTML) &&
104 !HTMLEditUtils::IsNonEditableReplacedContent(
105 *atEnd.ContainerAs<nsIContent>());
106 if (!isEndEditable) {
107 return false;
110 // Now, both start and end points are editable, but if they are in
111 // different editing host, we cannot edit the range.
112 if (atStart.ContainerAs<nsIContent>() != atEnd.ContainerAs<nsIContent>() &&
113 atStart.ContainerAs<nsIContent>()->GetEditingHost() !=
114 atEnd.ContainerAs<nsIContent>()->GetEditingHost()) {
115 return false;
119 // HTMLEditor does not support modifying outside `<body>` element for now.
120 nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
121 return commonAncestor && commonAncestor->IsContent() &&
122 commonAncestor->IsInclusiveDescendantOf(&aEditingHost);
125 void AutoRangeArray::EnsureOnlyEditableRanges(const Element& aEditingHost) {
126 for (size_t i = mRanges.Length(); i > 0; i--) {
127 const OwningNonNull<nsRange>& range = mRanges[i - 1];
128 if (!AutoRangeArray::IsEditableRange(range, aEditingHost)) {
129 mRanges.RemoveElementAt(i - 1);
132 mAnchorFocusRange = mRanges.IsEmpty() ? nullptr : mRanges.LastElement().get();
135 void AutoRangeArray::EnsureRangesInTextNode(const Text& aTextNode) {
136 auto GetOffsetInTextNode = [&aTextNode](const nsINode* aNode,
137 uint32_t aOffset) -> uint32_t {
138 MOZ_DIAGNOSTIC_ASSERT(aNode);
139 if (aNode == &aTextNode) {
140 return aOffset;
142 const nsIContent* anonymousDivElement = aTextNode.GetParent();
143 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement);
144 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->IsHTMLElement(nsGkAtoms::div));
145 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->GetFirstChild() == &aTextNode);
146 if (aNode == anonymousDivElement && aOffset == 0u) {
147 return 0u; // Point before the text node so that use start of the text.
149 MOZ_DIAGNOSTIC_ASSERT(aNode->IsInclusiveDescendantOf(anonymousDivElement));
150 // Point after the text node so that use end of the text.
151 return aTextNode.TextDataLength();
153 for (uint32_t i : IntegerRange(mRanges.Length())) {
154 const OwningNonNull<nsRange>& range = mRanges[i];
155 if (MOZ_LIKELY(range->GetStartContainer() == &aTextNode &&
156 range->GetEndContainer() == &aTextNode)) {
157 continue;
159 range->SetStartAndEnd(
160 const_cast<Text*>(&aTextNode),
161 GetOffsetInTextNode(range->GetStartContainer(), range->StartOffset()),
162 const_cast<Text*>(&aTextNode),
163 GetOffsetInTextNode(range->GetEndContainer(), range->EndOffset()));
166 if (MOZ_UNLIKELY(mRanges.Length() >= 2)) {
167 // For avoiding to handle same things in same range, we should drop and
168 // merge unnecessary ranges. Note that the ranges never overlap
169 // because selection ranges are not allowed it so that we need to check only
170 // end offset vs start offset of next one.
171 for (uint32_t i : Reversed(IntegerRange(mRanges.Length() - 1u))) {
172 MOZ_ASSERT(mRanges[i]->EndOffset() < mRanges[i + 1]->StartOffset());
173 // XXX Should we delete collapsed range unless the index is 0? Without
174 // Selection API, such situation cannot happen so that `TextEditor`
175 // may behave unexpectedly.
176 if (MOZ_UNLIKELY(mRanges[i]->EndOffset() >=
177 mRanges[i + 1]->StartOffset())) {
178 const uint32_t newEndOffset = mRanges[i + 1]->EndOffset();
179 mRanges.RemoveElementAt(i + 1);
180 if (MOZ_UNLIKELY(NS_WARN_IF(newEndOffset > mRanges[i]->EndOffset()))) {
181 // So, this case shouldn't happen.
182 mRanges[i]->SetStartAndEnd(
183 const_cast<Text*>(&aTextNode), mRanges[i]->StartOffset(),
184 const_cast<Text*>(&aTextNode), newEndOffset);
191 Result<nsIEditor::EDirection, nsresult>
192 AutoRangeArray::ExtendAnchorFocusRangeFor(
193 const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount) {
194 MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable());
195 MOZ_ASSERT(mAnchorFocusRange);
196 MOZ_ASSERT(mAnchorFocusRange->IsPositioned());
197 MOZ_ASSERT(mAnchorFocusRange->StartRef().IsSet());
198 MOZ_ASSERT(mAnchorFocusRange->EndRef().IsSet());
200 if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
201 aDirectionAndAmount, *this)) {
202 return aDirectionAndAmount;
205 if (NS_WARN_IF(!aEditorBase.SelectionRef().RangeCount())) {
206 return Err(NS_ERROR_FAILURE);
209 // At this point, the anchor-focus ranges must match for bidi information.
210 // See `EditorBase::AutoCaretBidiLevelManager`.
211 MOZ_ASSERT(aEditorBase.SelectionRef().GetAnchorFocusRange()->StartRef() ==
212 mAnchorFocusRange->StartRef());
213 MOZ_ASSERT(aEditorBase.SelectionRef().GetAnchorFocusRange()->EndRef() ==
214 mAnchorFocusRange->EndRef());
216 RefPtr<nsFrameSelection> frameSelection =
217 aEditorBase.SelectionRef().GetFrameSelection();
218 if (NS_WARN_IF(!frameSelection)) {
219 return Err(NS_ERROR_NOT_INITIALIZED);
222 RefPtr<Element> editingHost;
223 if (aEditorBase.IsHTMLEditor()) {
224 editingHost = aEditorBase.AsHTMLEditor()->ComputeEditingHost();
225 if (!editingHost) {
226 return Err(NS_ERROR_FAILURE);
230 Result<RefPtr<nsRange>, nsresult> result(NS_ERROR_UNEXPECTED);
231 nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount;
232 switch (aDirectionAndAmount) {
233 case nsIEditor::eNextWord:
234 result = frameSelection->CreateRangeExtendedToNextWordBoundary<nsRange>();
235 if (NS_WARN_IF(aEditorBase.Destroyed())) {
236 return Err(NS_ERROR_EDITOR_DESTROYED);
238 NS_WARNING_ASSERTION(
239 result.isOk(),
240 "nsFrameSelection::CreateRangeExtendedToNextWordBoundary() failed");
241 // DeleteSelectionWithTransaction() doesn't handle these actions
242 // because it's inside batching, so don't confuse it:
243 directionAndAmountResult = nsIEditor::eNone;
244 break;
245 case nsIEditor::ePreviousWord:
246 result =
247 frameSelection->CreateRangeExtendedToPreviousWordBoundary<nsRange>();
248 if (NS_WARN_IF(aEditorBase.Destroyed())) {
249 return Err(NS_ERROR_EDITOR_DESTROYED);
251 NS_WARNING_ASSERTION(
252 result.isOk(),
253 "nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() "
254 "failed");
255 // DeleteSelectionWithTransaction() doesn't handle these actions
256 // because it's inside batching, so don't confuse it:
257 directionAndAmountResult = nsIEditor::eNone;
258 break;
259 case nsIEditor::eNext:
260 result =
261 frameSelection
262 ->CreateRangeExtendedToNextGraphemeClusterBoundary<nsRange>();
263 if (NS_WARN_IF(aEditorBase.Destroyed())) {
264 return Err(NS_ERROR_EDITOR_DESTROYED);
266 NS_WARNING_ASSERTION(result.isOk(),
267 "nsFrameSelection::"
268 "CreateRangeExtendedToNextGraphemeClusterBoundary() "
269 "failed");
270 // Don't set directionAndAmount to eNone (see Bug 502259)
271 break;
272 case nsIEditor::ePrevious: {
273 // Only extend the selection where the selection is after a UTF-16
274 // surrogate pair or a variation selector.
275 // For other cases we don't want to do that, in order
276 // to make sure that pressing backspace will only delete the last
277 // typed character.
278 // XXX This is odd if the previous one is a sequence for a grapheme
279 // cluster.
280 const auto atStartOfSelection = GetFirstRangeStartPoint<EditorDOMPoint>();
281 if (MOZ_UNLIKELY(NS_WARN_IF(!atStartOfSelection.IsSet()))) {
282 return Err(NS_ERROR_FAILURE);
285 // node might be anonymous DIV, so we find better text node
286 const EditorDOMPoint insertionPoint =
287 aEditorBase.FindBetterInsertionPoint(atStartOfSelection);
288 if (MOZ_UNLIKELY(!insertionPoint.IsSet())) {
289 NS_WARNING(
290 "EditorBase::FindBetterInsertionPoint() failed, but ignored");
291 return aDirectionAndAmount;
294 if (!insertionPoint.IsInTextNode()) {
295 return aDirectionAndAmount;
298 const nsTextFragment* data =
299 &insertionPoint.ContainerAs<Text>()->TextFragment();
300 uint32_t offset = insertionPoint.Offset();
301 if (!(offset > 1 &&
302 data->IsLowSurrogateFollowingHighSurrogateAt(offset - 1)) &&
303 !(offset > 0 &&
304 gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) {
305 return aDirectionAndAmount;
307 // Different from the `eNext` case, we look for character boundary.
308 // I'm not sure whether this inconsistency between "Delete" and
309 // "Backspace" is intentional or not.
310 result = frameSelection
311 ->CreateRangeExtendedToPreviousCharacterBoundary<nsRange>();
312 if (NS_WARN_IF(aEditorBase.Destroyed())) {
313 return Err(NS_ERROR_EDITOR_DESTROYED);
315 NS_WARNING_ASSERTION(
316 result.isOk(),
317 "nsFrameSelection::"
318 "CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed");
319 break;
321 case nsIEditor::eToBeginningOfLine:
322 result =
323 frameSelection->CreateRangeExtendedToPreviousHardLineBreak<nsRange>();
324 if (NS_WARN_IF(aEditorBase.Destroyed())) {
325 return Err(NS_ERROR_EDITOR_DESTROYED);
327 NS_WARNING_ASSERTION(
328 result.isOk(),
329 "nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() "
330 "failed");
331 directionAndAmountResult = nsIEditor::eNone;
332 break;
333 case nsIEditor::eToEndOfLine:
334 result =
335 frameSelection->CreateRangeExtendedToNextHardLineBreak<nsRange>();
336 if (NS_WARN_IF(aEditorBase.Destroyed())) {
337 return Err(NS_ERROR_EDITOR_DESTROYED);
339 NS_WARNING_ASSERTION(
340 result.isOk(),
341 "nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed");
342 directionAndAmountResult = nsIEditor::eNext;
343 break;
344 default:
345 return aDirectionAndAmount;
348 if (result.isErr()) {
349 return Err(result.inspectErr());
351 RefPtr<nsRange> extendedRange(result.unwrap().forget());
352 if (!extendedRange || NS_WARN_IF(!extendedRange->IsPositioned())) {
353 NS_WARNING("Failed to extend the range, but ignored");
354 return directionAndAmountResult;
357 // If the new range isn't editable, keep using the original range.
358 if (aEditorBase.IsHTMLEditor() &&
359 !AutoRangeArray::IsEditableRange(*extendedRange, *editingHost)) {
360 return aDirectionAndAmount;
363 if (NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
364 extendedRange->GetStartContainer())) ||
365 NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
366 extendedRange->GetEndContainer()))) {
367 NS_WARNING("A range was extended to outer of selection limiter");
368 return Err(NS_ERROR_FAILURE);
371 // Swap focus/anchor range with the extended range.
372 DebugOnly<bool> found = false;
373 for (OwningNonNull<nsRange>& range : mRanges) {
374 if (range == mAnchorFocusRange) {
375 range = *extendedRange;
376 found = true;
377 break;
380 MOZ_ASSERT(found);
381 mAnchorFocusRange.swap(extendedRange);
382 return directionAndAmountResult;
385 Result<bool, nsresult>
386 AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
387 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
388 IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent,
389 const Element* aEditingHost) {
390 if (IsCollapsed()) {
391 return false;
394 switch (aDirectionAndAmount) {
395 case nsIEditor::eNext:
396 case nsIEditor::eNextWord:
397 case nsIEditor::ePrevious:
398 case nsIEditor::ePreviousWord:
399 break;
400 default:
401 return false;
404 bool changed = false;
405 for (auto& range : mRanges) {
406 MOZ_ASSERT(!range->IsInSelection(),
407 "Changing range in selection may cause running script");
408 Result<bool, nsresult> result =
409 WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
410 aHTMLEditor, range, aEditingHost);
411 if (result.isErr()) {
412 NS_WARNING(
413 "WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() "
414 "failed");
415 return Err(result.inspectErr());
417 changed |= result.inspect();
420 if (mRanges.Length() == 1 && aIfSelectingOnlyOneAtomicContent ==
421 IfSelectingOnlyOneAtomicContent::Collapse) {
422 MOZ_ASSERT(mRanges[0].get() == mAnchorFocusRange.get());
423 if (mAnchorFocusRange->GetStartContainer() ==
424 mAnchorFocusRange->GetEndContainer() &&
425 mAnchorFocusRange->GetChildAtStartOffset() &&
426 mAnchorFocusRange->StartRef().GetNextSiblingOfChildAtOffset() ==
427 mAnchorFocusRange->GetChildAtEndOffset()) {
428 mAnchorFocusRange->Collapse(aDirectionAndAmount == nsIEditor::eNext ||
429 aDirectionAndAmount == nsIEditor::eNextWord);
430 changed = true;
434 return changed;
437 bool AutoRangeArray::SaveAndTrackRanges(HTMLEditor& aHTMLEditor) {
438 if (mSavedRanges.isSome()) {
439 return false;
441 mSavedRanges.emplace(*this);
442 aHTMLEditor.RangeUpdaterRef().RegisterSelectionState(mSavedRanges.ref());
443 mTrackingHTMLEditor = &aHTMLEditor;
444 return true;
447 void AutoRangeArray::ClearSavedRanges() {
448 if (mSavedRanges.isNothing()) {
449 return;
451 OwningNonNull<HTMLEditor> htmlEditor(std::move(mTrackingHTMLEditor));
452 MOZ_ASSERT(!mTrackingHTMLEditor);
453 htmlEditor->RangeUpdaterRef().DropSelectionState(mSavedRanges.ref());
454 mSavedRanges.reset();
457 // static
458 void AutoRangeArray::
459 UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
460 EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint,
461 const Element& aEditingHost) {
462 // FYI: This was moved from
463 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6743
465 // MOOSE major hack:
466 // The GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() and
467 // GetPointAfterFollowingLineBreakOrAtFollowingBlock() don't really do the
468 // right thing for collapsed ranges inside block elements that contain nothing
469 // but a solo <br>. It's easier/ to put a workaround here than to revamp
470 // them. :-(
471 if (aStartPoint != aEndPoint) {
472 return;
475 if (!aStartPoint.IsInContentNode()) {
476 return;
479 // XXX Perhaps, this should be more careful. This may not select only one
480 // node because this just check whether the block is empty or not,
481 // and may not select in non-editable block. However, for inline
482 // editing host case, it's right to look for block element without
483 // editable state check. Now, this method is used for preparation for
484 // other things. So, cannot write test for this method behavior.
485 // So, perhaps, we should get rid of this method and each caller should
486 // handle its job better.
487 Element* const maybeNonEditableBlockElement =
488 HTMLEditUtils::GetInclusiveAncestorElement(
489 *aStartPoint.ContainerAs<nsIContent>(),
490 HTMLEditUtils::ClosestBlockElement);
491 if (!maybeNonEditableBlockElement) {
492 return;
495 // Make sure we don't go higher than our root element in the content tree
496 if (aEditingHost.IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
497 return;
500 if (HTMLEditUtils::IsEmptyNode(*maybeNonEditableBlockElement)) {
501 aStartPoint.Set(maybeNonEditableBlockElement, 0u);
502 aEndPoint.SetToEndOf(maybeNonEditableBlockElement);
507 * Get the point before the line containing aPointInLine.
509 * @return If the line starts after a `<br>` element, returns next
510 * sibling of the `<br>` element.
511 * If the line is first line of a block, returns point of
512 * the block.
513 * NOTE: The result may be point of editing host. I.e., the container may be
514 * outside of editing host.
516 static EditorDOMPoint
517 GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock(
518 const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
519 const Element& aEditingHost) {
520 // FYI: This was moved from
521 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6447
523 if (NS_WARN_IF(!aPointInLine.IsSet())) {
524 return EditorDOMPoint();
527 EditorDOMPoint point(aPointInLine);
528 // Start scanning from the container node if aPoint is in a text node.
529 // XXX Perhaps, IsInDataNode() must be expected.
530 if (point.IsInTextNode()) {
531 if (!point.GetContainer()->GetParentNode()) {
532 // Okay, can't promote any further
533 // XXX Why don't we return start of the text node?
534 return point;
536 // If there is a preformatted linefeed in the text node, let's return
537 // the point after it.
538 EditorDOMPoint atLastPreformattedNewLine =
539 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
540 point);
541 if (atLastPreformattedNewLine.IsSet()) {
542 return atLastPreformattedNewLine.NextPoint();
544 point.Set(point.GetContainer());
547 // Look back through any further inline nodes that aren't across a <br>
548 // from us, and that are enclosed in the same block.
549 // I.e., looking for start of current hard line.
550 constexpr HTMLEditUtils::WalkTreeOptions
551 ignoreNonEditableNodeAndStopAtBlockBoundary{
552 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
553 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
554 for (nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
555 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
556 previousEditableContent && previousEditableContent->GetParentNode() &&
557 !HTMLEditUtils::IsVisibleBRElement(*previousEditableContent) &&
558 !HTMLEditUtils::IsBlockElement(*previousEditableContent);
559 previousEditableContent = HTMLEditUtils::GetPreviousContent(
560 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
561 EditorDOMPoint atLastPreformattedNewLine =
562 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
563 EditorRawDOMPoint::AtEndOf(*previousEditableContent));
564 if (atLastPreformattedNewLine.IsSet()) {
565 return atLastPreformattedNewLine.NextPoint();
567 point.Set(previousEditableContent);
570 // Finding the real start for this point unless current line starts after
571 // <br> element. Look up the tree for as long as we are the first node in
572 // the container (typically, start of nearest block ancestor), and as long
573 // as we haven't hit the body node.
574 for (nsIContent* nearContent = HTMLEditUtils::GetPreviousContent(
575 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
576 !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
577 point.GetContainerParent();
578 nearContent = HTMLEditUtils::GetPreviousContent(
579 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
580 // Don't keep looking up if we have found a blockquote element to act on
581 // when we handle outdent.
582 // XXX Sounds like this is hacky. If possible, it should be check in
583 // outdent handler for consistency between edit sub-actions.
584 // We should check Chromium's behavior of outdent when Selection
585 // starts from `<blockquote>` and starts from first child of
586 // `<blockquote>`.
587 if (aEditSubAction == EditSubAction::eOutdent &&
588 point.IsContainerHTMLElement(nsGkAtoms::blockquote)) {
589 break;
592 // Don't walk past the editable section. Note that we need to check
593 // before walking up to a parent because we need to return the parent
594 // object, so the parent itself might not be in the editable area, but
595 // it's OK if we're not performing a block-level action.
596 bool blockLevelAction =
597 aEditSubAction == EditSubAction::eIndent ||
598 aEditSubAction == EditSubAction::eOutdent ||
599 aEditSubAction == EditSubAction::eSetOrClearAlignment ||
600 aEditSubAction == EditSubAction::eCreateOrRemoveBlock;
601 // XXX So, does this check whether the container is removable or not? It
602 // seems that here can be rewritten as obviously what here tries to
603 // check.
604 if (!point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost) &&
605 (blockLevelAction ||
606 !point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost))) {
607 break;
610 point.Set(point.GetContainer());
612 return point;
616 * Get the point after the following line break or the block which breaks the
617 * line containing aPointInLine.
619 * @return If the line ends with a visible `<br>` element, returns
620 * the point after the `<br>` element.
621 * If the line ends with a preformatted linefeed, returns
622 * the point after the linefeed unless it's an invisible
623 * line break immediately before a block boundary.
624 * If the line ends with a block boundary, returns the
625 * point of the block.
627 static EditorDOMPoint GetPointAfterFollowingLineBreakOrAtFollowingBlock(
628 const EditorDOMPoint& aPointInLine, const Element& aEditingHost) {
629 // FYI: This was moved from
630 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6541
632 if (NS_WARN_IF(!aPointInLine.IsSet())) {
633 return EditorDOMPoint();
636 EditorDOMPoint point(aPointInLine);
637 // Start scanning from the container node if aPoint is in a text node.
638 // XXX Perhaps, IsInDataNode() must be expected.
639 if (point.IsInTextNode()) {
640 if (NS_WARN_IF(!point.GetContainer()->GetParentNode())) {
641 // Okay, can't promote any further
642 // XXX Why don't we return end of the text node?
643 return point;
645 EditorDOMPoint atNextPreformattedNewLine =
646 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
647 EditorDOMPoint>(point);
648 if (atNextPreformattedNewLine.IsSet()) {
649 // If the linefeed is last character of the text node, it may be
650 // invisible if it's immediately before a block boundary. In such
651 // case, we should retrun the block boundary.
652 Element* maybeNonEditableBlockElement = nullptr;
653 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
654 atNextPreformattedNewLine, &maybeNonEditableBlockElement) &&
655 maybeNonEditableBlockElement) {
656 // If the block is a parent of the editing host, let's return end
657 // of editing host.
658 if (maybeNonEditableBlockElement == &aEditingHost ||
659 !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
660 &aEditingHost)) {
661 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
663 // If it's invisible because of parent block boundary, return end
664 // of the block. Otherwise, i.e., it's followed by a child block,
665 // returns the point of the child block.
666 if (atNextPreformattedNewLine.ContainerAs<Text>()
667 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
668 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
670 return EditorDOMPoint(maybeNonEditableBlockElement);
672 // Otherwise, return the point after the preformatted linefeed.
673 return atNextPreformattedNewLine.NextPoint();
675 // want to be after the text node
676 point.SetAfter(point.GetContainer());
677 NS_WARNING_ASSERTION(point.IsSet(), "Failed to set to after the text node");
680 // Look ahead through any further inline nodes that aren't across a <br> from
681 // us, and that are enclosed in the same block.
682 // XXX Currently, we stop block-extending when finding visible <br> element.
683 // This might be different from "block-extend" of execCommand spec.
684 // However, the spec is really unclear.
685 // XXX Probably, scanning only editable nodes is wrong for
686 // EditSubAction::eCreateOrRemoveBlock because it might be better to wrap
687 // existing inline elements even if it's non-editable. For example,
688 // following examples with insertParagraph causes different result:
689 // * <div contenteditable>foo[]<b contenteditable="false">bar</b></div>
690 // * <div contenteditable>foo[]<b>bar</b></div>
691 // * <div contenteditable>foo[]<b contenteditable="false">bar</b>baz</div>
692 // Only in the first case, after the caret position isn't wrapped with
693 // new <div> element.
694 constexpr HTMLEditUtils::WalkTreeOptions
695 ignoreNonEditableNodeAndStopAtBlockBoundary{
696 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
697 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
698 for (nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
699 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
700 nextEditableContent &&
701 !HTMLEditUtils::IsBlockElement(*nextEditableContent) &&
702 nextEditableContent->GetParent();
703 nextEditableContent = HTMLEditUtils::GetNextContent(
704 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
705 EditorDOMPoint atFirstPreformattedNewLine =
706 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
707 EditorDOMPoint>(EditorRawDOMPoint(nextEditableContent, 0));
708 if (atFirstPreformattedNewLine.IsSet()) {
709 // If the linefeed is last character of the text node, it may be
710 // invisible if it's immediately before a block boundary. In such
711 // case, we should retrun the block boundary.
712 Element* maybeNonEditableBlockElement = nullptr;
713 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
714 atFirstPreformattedNewLine, &maybeNonEditableBlockElement) &&
715 maybeNonEditableBlockElement) {
716 // If the block is a parent of the editing host, let's return end
717 // of editing host.
718 if (maybeNonEditableBlockElement == &aEditingHost ||
719 !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
720 &aEditingHost)) {
721 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
723 // If it's invisible because of parent block boundary, return end
724 // of the block. Otherwise, i.e., it's followed by a child block,
725 // returns the point of the child block.
726 if (atFirstPreformattedNewLine.ContainerAs<Text>()
727 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
728 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
730 return EditorDOMPoint(maybeNonEditableBlockElement);
732 // Otherwise, return the point after the preformatted linefeed.
733 return atFirstPreformattedNewLine.NextPoint();
735 point.SetAfter(nextEditableContent);
736 if (NS_WARN_IF(!point.IsSet())) {
737 break;
739 if (HTMLEditUtils::IsVisibleBRElement(*nextEditableContent)) {
740 break;
744 // Finding the real end for this point unless current line ends with a <br>
745 // element. Look up the tree for as long as we are the last node in the
746 // container (typically, block node), and as long as we haven't hit the body
747 // node.
748 for (nsIContent* nearContent = HTMLEditUtils::GetNextContent(
749 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost);
750 !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
751 point.GetContainerParent();
752 nearContent = HTMLEditUtils::GetNextContent(
753 point, ignoreNonEditableNodeAndStopAtBlockBoundary, &aEditingHost)) {
754 // Don't walk past the editable section. Note that we need to check before
755 // walking up to a parent because we need to return the parent object, so
756 // the parent itself might not be in the editable area, but it's OK.
757 // XXX Maybe returning parent of editing host is really error prone since
758 // everybody need to check whether the end point is in editing host
759 // when they touch there.
760 if (!point.GetContainer()->IsInclusiveDescendantOf(&aEditingHost) &&
761 !point.GetContainerParent()->IsInclusiveDescendantOf(&aEditingHost)) {
762 break;
765 point.SetAfter(point.GetContainer());
766 if (NS_WARN_IF(!point.IsSet())) {
767 break;
770 return point;
773 void AutoRangeArray::ExtendRangesToWrapLinesToHandleBlockLevelEditAction(
774 EditSubAction aEditSubAction, const Element& aEditingHost) {
775 // FYI: This is originated in
776 // https://searchfox.org/mozilla-central/rev/1739f1301d658c9bff544a0a095ab11fca2e549d/editor/libeditor/HTMLEditSubActionHandler.cpp#6712
778 bool removeSomeRanges = false;
779 for (OwningNonNull<nsRange>& range : mRanges) {
780 // Remove non-positioned ranges.
781 if (MOZ_UNLIKELY(!range->IsPositioned())) {
782 removeSomeRanges = true;
783 continue;
785 // If the range is native anonymous subtrees, we must meet a bug of
786 // `Selection` so that we need to hack here.
787 if (MOZ_UNLIKELY(range->GetStartContainer()->IsInNativeAnonymousSubtree() ||
788 range->GetEndContainer()->IsInNativeAnonymousSubtree())) {
789 EditorRawDOMRange rawRange(range);
790 if (!rawRange.EnsureNotInNativeAnonymousSubtree()) {
791 range->Reset();
792 removeSomeRanges = true;
793 continue;
795 if (NS_FAILED(
796 range->SetStartAndEnd(rawRange.StartRef().ToRawRangeBoundary(),
797 rawRange.EndRef().ToRawRangeBoundary())) ||
798 MOZ_UNLIKELY(!range->IsPositioned())) {
799 range->Reset();
800 removeSomeRanges = true;
801 continue;
804 // Finally, extend the range.
805 if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
806 range, aEditSubAction, aEditingHost))) {
807 // If we failed to extend the range, we should use the original range
808 // as-is unless the range is broken at setting the range.
809 if (NS_WARN_IF(!range->IsPositioned())) {
810 removeSomeRanges = true;
814 if (removeSomeRanges) {
815 for (size_t i : Reversed(IntegerRange(mRanges.Length()))) {
816 if (!mRanges[i]->IsPositioned()) {
817 mRanges.RemoveElementAt(i);
823 // static
824 nsresult AutoRangeArray::ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
825 nsRange& aRange, EditSubAction aEditSubAction,
826 const Element& aEditingHost) {
827 MOZ_DIAGNOSTIC_ASSERT(
828 !EditorRawDOMPoint(aRange.StartRef()).IsInNativeAnonymousSubtree());
829 MOZ_DIAGNOSTIC_ASSERT(
830 !EditorRawDOMPoint(aRange.EndRef()).IsInNativeAnonymousSubtree());
832 if (NS_WARN_IF(!aRange.IsPositioned())) {
833 return NS_ERROR_INVALID_ARG;
836 EditorDOMPoint startPoint(aRange.StartRef()), endPoint(aRange.EndRef());
837 AutoRangeArray::UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
838 startPoint, endPoint, aEditingHost);
840 // Make a new adjusted range to represent the appropriate block content.
841 // This is tricky. The basic idea is to push out the range endpoints to
842 // truly enclose the blocks that we will affect.
844 // Make sure that the new range ends up to be in the editable section.
845 // XXX Looks like that this check wastes the time. Perhaps, we should
846 // implement a method which checks both two DOM points in the editor
847 // root.
849 startPoint = GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock(
850 startPoint, aEditSubAction, aEditingHost);
851 // XXX GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() may
852 // return point of editing host. Perhaps, we should change it and stop
853 // checking it here since this check may be expensive.
854 // XXX If the container is an element in the editing host but it points end of
855 // the container, this returns nullptr. Is it intentional?
856 if (!startPoint.GetChildOrContainerIfDataNode() ||
857 !startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
858 &aEditingHost)) {
859 return NS_ERROR_FAILURE;
861 endPoint =
862 GetPointAfterFollowingLineBreakOrAtFollowingBlock(endPoint, aEditingHost);
863 const EditorDOMPoint lastRawPoint =
864 endPoint.IsStartOfContainer() ? endPoint : endPoint.PreviousPoint();
865 // XXX GetPointAfterFollowingLineBreakOrAtFollowingBlock() may return point of
866 // editing host. Perhaps, we should change it and stop checking it here
867 // since this check may be expensive.
868 // XXX If the container is an element in the editing host but it points end of
869 // the container, this returns nullptr. Is it intentional?
870 if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
871 !lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
872 &aEditingHost)) {
873 return NS_ERROR_FAILURE;
876 nsresult rv = aRange.SetStartAndEnd(startPoint.ToRawRangeBoundary(),
877 endPoint.ToRawRangeBoundary());
878 if (NS_FAILED(rv)) {
879 return NS_ERROR_FAILURE;
881 return NS_OK;
884 Result<EditorDOMPoint, nsresult> AutoRangeArray::
885 SplitTextNodesAtEndBoundariesAndParentInlineElementsAtBoundaries(
886 HTMLEditor& aHTMLEditor) {
887 // FYI: The following code is originated in
888 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#6971
890 // Split text nodes. This is necessary, since given ranges may end in text
891 // nodes in case where part of a pre-formatted elements needs to be moved.
892 EditorDOMPoint pointToPutCaret;
893 IgnoredErrorResult ignoredError;
894 for (const OwningNonNull<nsRange>& range : mRanges) {
895 EditorDOMPoint atEnd(range->EndRef());
896 if (NS_WARN_IF(!atEnd.IsSet()) || !atEnd.IsInTextNode()) {
897 continue;
900 if (!atEnd.IsStartOfContainer() && !atEnd.IsEndOfContainer()) {
901 // Split the text node.
902 Result<SplitNodeResult, nsresult> splitAtEndResult =
903 aHTMLEditor.SplitNodeWithTransaction(atEnd);
904 if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
905 NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
906 return splitAtEndResult.propagateErr();
908 SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
909 unwrappedSplitAtEndResult.MoveCaretPointTo(
910 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
912 // Correct the range.
913 // The new end parent becomes the parent node of the text.
914 MOZ_ASSERT(!range->IsInSelection());
915 range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
916 .ToRawRangeBoundary(),
917 ignoredError);
918 NS_WARNING_ASSERTION(!ignoredError.Failed(),
919 "nsRange::SetEnd() failed, but ignored");
920 ignoredError.SuppressException();
924 // FYI: The following code is originated in
925 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#7023
926 nsTArray<OwningNonNull<RangeItem>> rangeItemArray;
927 rangeItemArray.AppendElements(mRanges.Length());
929 // First register ranges for special editor gravity
930 for (OwningNonNull<RangeItem>& rangeItem : rangeItemArray) {
931 rangeItem = new RangeItem();
932 rangeItem->StoreRange(*mRanges[0]);
933 aHTMLEditor.RangeUpdaterRef().RegisterRangeItem(*rangeItem);
934 // TODO: We should keep the array, and just update the ranges.
935 mRanges.RemoveElementAt(0);
937 // Now bust up inlines.
938 nsresult rv = NS_OK;
939 for (OwningNonNull<RangeItem>& item : Reversed(rangeItemArray)) {
940 // MOZ_KnownLive because 'rangeItemArray' is guaranteed to keep it alive.
941 Result<EditorDOMPoint, nsresult> splitParentsResult =
942 aHTMLEditor.SplitParentInlineElementsAtRangeEdges(MOZ_KnownLive(*item));
943 if (MOZ_UNLIKELY(splitParentsResult.isErr())) {
944 NS_WARNING("HTMLEditor::SplitParentInlineElementsAtRangeEdges() failed");
945 rv = splitParentsResult.unwrapErr();
946 break;
948 if (splitParentsResult.inspect().IsSet()) {
949 pointToPutCaret = splitParentsResult.unwrap();
952 // Then unregister the ranges
953 for (OwningNonNull<RangeItem>& item : rangeItemArray) {
954 aHTMLEditor.RangeUpdaterRef().DropRangeItem(item);
955 RefPtr<nsRange> range = item->GetRange();
956 if (range) {
957 mRanges.AppendElement(std::move(range));
961 // XXX Why do we ignore the other errors here??
962 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
963 return Err(NS_ERROR_EDITOR_DESTROYED);
965 return pointToPutCaret;
968 nsresult AutoRangeArray::CollectEditTargetNodes(
969 const HTMLEditor& aHTMLEditor,
970 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
971 EditSubAction aEditSubAction,
972 CollectNonEditableNodes aCollectNonEditableNodes) const {
973 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
975 // FYI: This was moved from
976 // https://searchfox.org/mozilla-central/rev/4bce7d85ba4796dd03c5dcc7cfe8eee0e4c07b3b/editor/libeditor/HTMLEditSubActionHandler.cpp#7060
978 // Gather up a list of all the nodes
979 for (const OwningNonNull<nsRange>& range : mRanges) {
980 DOMSubtreeIterator iter;
981 nsresult rv = iter.Init(*range);
982 if (NS_FAILED(rv)) {
983 NS_WARNING("DOMSubtreeIterator::Init() failed");
984 return rv;
986 if (aOutArrayOfContents.IsEmpty()) {
987 iter.AppendAllNodesToArray(aOutArrayOfContents);
988 } else {
989 AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfTopChildren;
990 iter.AppendNodesToArray(
991 +[](nsINode& aNode, void* aArray) -> bool {
992 MOZ_ASSERT(aArray);
993 return !static_cast<nsTArray<OwningNonNull<nsIContent>>*>(aArray)
994 ->Contains(&aNode);
996 arrayOfTopChildren, &aOutArrayOfContents);
997 aOutArrayOfContents.AppendElements(std::move(arrayOfTopChildren));
999 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1000 for (size_t i : Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1001 if (!EditorUtils::IsEditableContent(aOutArrayOfContents[i],
1002 EditorUtils::EditorType::HTML)) {
1003 aOutArrayOfContents.RemoveElementAt(i);
1009 switch (aEditSubAction) {
1010 case EditSubAction::eCreateOrRemoveBlock: {
1011 // Certain operations should not act on li's and td's, but rather inside
1012 // them. Alter the list as needed.
1013 CollectChildrenOptions options = {
1014 CollectChildrenOption::CollectListChildren,
1015 CollectChildrenOption::CollectTableChildren};
1016 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1017 options += CollectChildrenOption::IgnoreNonEditableChildren;
1019 for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
1020 OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
1021 if (HTMLEditUtils::IsListItem(content)) {
1022 aOutArrayOfContents.RemoveElementAt(i);
1023 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
1024 options);
1027 // Empty text node shouldn't be selected if unnecessary
1028 for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
1029 if (Text* text = aOutArrayOfContents[i]->GetAsText()) {
1030 // Don't select empty text except to empty block
1031 if (!HTMLEditUtils::IsVisibleTextNode(*text)) {
1032 aOutArrayOfContents.RemoveElementAt(i);
1036 break;
1038 case EditSubAction::eCreateOrChangeList: {
1039 // XXX aCollectNonEditableNodes is ignored here. Maybe a bug.
1040 CollectChildrenOptions options = {
1041 CollectChildrenOption::CollectTableChildren};
1042 for (size_t i = aOutArrayOfContents.Length(); i > 0; i--) {
1043 // Scan for table elements. If we find table elements other than
1044 // table, replace it with a list of any editable non-table content
1045 // because if a selection range starts from end in a table-cell and
1046 // ends at or starts from outside the `<table>`, we need to make
1047 // lists in each selected table-cells.
1048 OwningNonNull<nsIContent> content = aOutArrayOfContents[i - 1];
1049 if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
1050 aOutArrayOfContents.RemoveElementAt(i - 1);
1051 HTMLEditUtils::CollectChildren(content, aOutArrayOfContents, i - 1,
1052 options);
1055 // If there is only one node in the array, and it is a `<div>`,
1056 // `<blockquote>` or a list element, then look inside of it until we
1057 // find inner list or content.
1058 if (aOutArrayOfContents.Length() != 1) {
1059 break;
1061 Element* deepestDivBlockquoteOrListElement =
1062 HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
1063 aOutArrayOfContents[0],
1064 {HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode},
1065 nsGkAtoms::div, nsGkAtoms::blockquote, nsGkAtoms::ul,
1066 nsGkAtoms::ol, nsGkAtoms::dl);
1067 if (!deepestDivBlockquoteOrListElement) {
1068 break;
1070 if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
1071 nsGkAtoms::div, nsGkAtoms::blockquote)) {
1072 aOutArrayOfContents.Clear();
1073 // XXX Before we're called, non-editable nodes are ignored. However,
1074 // we may append non-editable nodes here.
1075 HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
1076 aOutArrayOfContents, 0, {});
1077 break;
1079 aOutArrayOfContents.ReplaceElementAt(
1080 0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
1081 break;
1083 case EditSubAction::eOutdent:
1084 case EditSubAction::eIndent:
1085 case EditSubAction::eSetPositionToAbsolute: {
1086 // Indent/outdent already do something special for list items, but we
1087 // still need to make sure we don't act on table elements
1088 CollectChildrenOptions options = {
1089 CollectChildrenOption::CollectListChildren,
1090 CollectChildrenOption::CollectTableChildren};
1091 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1092 options += CollectChildrenOption::IgnoreNonEditableChildren;
1094 for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
1095 OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
1096 if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
1097 aOutArrayOfContents.RemoveElementAt(i);
1098 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
1099 options);
1102 break;
1104 default:
1105 break;
1108 // Outdent should look inside of divs.
1109 if (aEditSubAction == EditSubAction::eOutdent &&
1110 !aHTMLEditor.IsCSSEnabled()) {
1111 CollectChildrenOptions options = {};
1112 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1113 options += CollectChildrenOption::IgnoreNonEditableChildren;
1115 for (int32_t i = aOutArrayOfContents.Length() - 1; i >= 0; i--) {
1116 OwningNonNull<nsIContent> content = aOutArrayOfContents[i];
1117 if (content->IsHTMLElement(nsGkAtoms::div)) {
1118 aOutArrayOfContents.RemoveElementAt(i);
1119 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, i,
1120 options);
1125 return NS_OK;
1128 Element* AutoRangeArray::GetClosestAncestorAnyListElementOfRange() const {
1129 for (const OwningNonNull<nsRange>& range : mRanges) {
1130 nsINode* commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
1131 if (MOZ_UNLIKELY(!commonAncestorNode)) {
1132 continue;
1134 for (Element* element :
1135 commonAncestorNode->InclusiveAncestorsOfType<Element>()) {
1136 if (HTMLEditUtils::IsAnyListElement(element)) {
1137 return element;
1141 return nullptr;
1144 } // namespace mozilla