Bug 1931425 - Limit how often moz-label's #setStyles runs r=reusable-components-revie...
[gecko.git] / editor / libeditor / AutoClonedRangeArray.cpp
blob54609ae52aa0cf7ed8eefd213ca37c273cb4744f
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 "AutoClonedRangeArray.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 "WSRunScanner.h" // for WSRunScanner
16 #include "mozilla/CaretAssociationHint.h" // for CaretAssociationHint
17 #include "mozilla/IntegerRange.h" // for IntegerRange
18 #include "mozilla/OwningNonNull.h" // for OwningNonNull
19 #include "mozilla/PresShell.h" // for PresShell
20 #include "mozilla/dom/Document.h" // for dom::Document
21 #include "mozilla/dom/HTMLBRElement.h" // for dom HTMLBRElement
22 #include "mozilla/dom/Selection.h" // for dom::Selection
23 #include "mozilla/dom/Text.h" // for dom::Text
25 #include "gfxFontUtils.h" // for gfxFontUtils
26 #include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
27 #include "nsFrameSelection.h" // for nsFrameSelection
28 #include "nsIContent.h" // for nsIContent
29 #include "nsINode.h" // for nsINode
30 #include "nsRange.h" // for nsRange
31 #include "nsTextFragment.h" // for nsTextFragment
33 namespace mozilla {
35 using namespace dom;
37 using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
39 /******************************************************************************
40 * mozilla::AutoClonedRangeArray
41 *****************************************************************************/
43 template AutoClonedRangeArray::AutoClonedRangeArray(
44 const EditorDOMRange& aRange);
45 template AutoClonedRangeArray::AutoClonedRangeArray(
46 const EditorRawDOMRange& aRange);
47 template AutoClonedRangeArray::AutoClonedRangeArray(
48 const EditorDOMPoint& aRange);
49 template AutoClonedRangeArray::AutoClonedRangeArray(
50 const EditorRawDOMPoint& aRange);
52 AutoClonedRangeArray::AutoClonedRangeArray(const AutoClonedRangeArray& aOther)
53 : mAnchorFocusRange(aOther.mAnchorFocusRange),
54 mDirection(aOther.mDirection) {
55 mRanges.SetCapacity(aOther.mRanges.Length());
56 for (const OwningNonNull<nsRange>& range : aOther.mRanges) {
57 RefPtr<nsRange> clonedRange = range->CloneRange();
58 mRanges.AppendElement(std::move(clonedRange));
60 mAnchorFocusRange = aOther.mAnchorFocusRange;
63 template <typename PointType>
64 AutoClonedRangeArray::AutoClonedRangeArray(
65 const EditorDOMRangeBase<PointType>& aRange) {
66 MOZ_ASSERT(aRange.IsPositionedAndValid());
67 RefPtr<nsRange> range = aRange.CreateRange(IgnoreErrors());
68 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
69 return;
71 mRanges.AppendElement(*range);
72 mAnchorFocusRange = std::move(range);
75 template <typename PT, typename CT>
76 AutoClonedRangeArray::AutoClonedRangeArray(
77 const EditorDOMPointBase<PT, CT>& aPoint) {
78 MOZ_ASSERT(aPoint.IsSetAndValid());
79 RefPtr<nsRange> range = aPoint.CreateCollapsedRange(IgnoreErrors());
80 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
81 return;
83 mRanges.AppendElement(*range);
84 mAnchorFocusRange = std::move(range);
87 AutoClonedRangeArray::AutoClonedRangeArray(const nsRange& aRange) {
88 MOZ_ASSERT(aRange.IsPositioned());
89 mRanges.AppendElement(aRange.CloneRange());
90 mAnchorFocusRange = mRanges[0];
93 // static
94 bool AutoClonedRangeArray::IsEditableRange(const dom::AbstractRange& aRange,
95 const Element& aEditingHost) {
96 // TODO: Perhaps, we should check whether the start/end boundaries are
97 // first/last point of non-editable element.
98 // See https://github.com/w3c/editing/issues/283#issuecomment-788654850
99 EditorRawDOMPoint atStart(aRange.StartRef());
100 const bool isStartEditable =
101 atStart.IsInContentNode() &&
102 EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
103 EditorUtils::EditorType::HTML) &&
104 !HTMLEditUtils::IsNonEditableReplacedContent(
105 *atStart.ContainerAs<nsIContent>());
106 if (!isStartEditable) {
107 return false;
110 if (aRange.GetStartContainer() != aRange.GetEndContainer()) {
111 EditorRawDOMPoint atEnd(aRange.EndRef());
112 const bool isEndEditable =
113 atEnd.IsInContentNode() &&
114 EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
115 EditorUtils::EditorType::HTML) &&
116 !HTMLEditUtils::IsNonEditableReplacedContent(
117 *atEnd.ContainerAs<nsIContent>());
118 if (!isEndEditable) {
119 return false;
122 // Now, both start and end points are editable, but if they are in
123 // different editing host, we cannot edit the range.
124 if (atStart.ContainerAs<nsIContent>() != atEnd.ContainerAs<nsIContent>() &&
125 atStart.ContainerAs<nsIContent>()->GetEditingHost() !=
126 atEnd.ContainerAs<nsIContent>()->GetEditingHost()) {
127 return false;
131 // HTMLEditor does not support modifying outside `<body>` element for now.
132 nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
133 return commonAncestor && commonAncestor->IsContent() &&
134 commonAncestor->IsInclusiveDescendantOf(&aEditingHost);
137 void AutoClonedRangeArray::EnsureOnlyEditableRanges(
138 const Element& aEditingHost) {
139 for (const size_t index : Reversed(IntegerRange(mRanges.Length()))) {
140 const OwningNonNull<nsRange>& range = mRanges[index];
141 if (!AutoClonedRangeArray::IsEditableRange(range, aEditingHost)) {
142 mRanges.RemoveElementAt(index);
143 continue;
145 // Special handling for `inert` attribute. If anchor node is inert, the
146 // range should be treated as not editable.
147 nsIContent* anchorContent =
148 mDirection == eDirNext
149 ? nsIContent::FromNode(range->GetStartContainer())
150 : nsIContent::FromNode(range->GetEndContainer());
151 if (anchorContent && HTMLEditUtils::ContentIsInert(*anchorContent)) {
152 mRanges.RemoveElementAt(index);
153 continue;
155 // Additionally, if focus node is inert, the range should be collapsed to
156 // anchor node.
157 nsIContent* focusContent =
158 mDirection == eDirNext
159 ? nsIContent::FromNode(range->GetEndContainer())
160 : nsIContent::FromNode(range->GetStartContainer());
161 if (focusContent && focusContent != anchorContent &&
162 HTMLEditUtils::ContentIsInert(*focusContent)) {
163 range->Collapse(mDirection == eDirNext);
166 mAnchorFocusRange = mRanges.IsEmpty() ? nullptr : mRanges.LastElement().get();
169 void AutoClonedRangeArray::EnsureRangesInTextNode(const Text& aTextNode) {
170 auto GetOffsetInTextNode = [&aTextNode](const nsINode* aNode,
171 uint32_t aOffset) -> uint32_t {
172 MOZ_DIAGNOSTIC_ASSERT(aNode);
173 if (aNode == &aTextNode) {
174 return aOffset;
176 const nsIContent* anonymousDivElement = aTextNode.GetParent();
177 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement);
178 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->IsHTMLElement(nsGkAtoms::div));
179 MOZ_DIAGNOSTIC_ASSERT(anonymousDivElement->GetFirstChild() == &aTextNode);
180 if (aNode == anonymousDivElement && aOffset == 0u) {
181 return 0u; // Point before the text node so that use start of the text.
183 MOZ_DIAGNOSTIC_ASSERT(aNode->IsInclusiveDescendantOf(anonymousDivElement));
184 // Point after the text node so that use end of the text.
185 return aTextNode.TextDataLength();
187 for (const OwningNonNull<nsRange>& range : mRanges) {
188 if (MOZ_LIKELY(range->GetStartContainer() == &aTextNode &&
189 range->GetEndContainer() == &aTextNode)) {
190 continue;
192 range->SetStartAndEnd(
193 const_cast<Text*>(&aTextNode),
194 GetOffsetInTextNode(range->GetStartContainer(), range->StartOffset()),
195 const_cast<Text*>(&aTextNode),
196 GetOffsetInTextNode(range->GetEndContainer(), range->EndOffset()));
199 if (MOZ_UNLIKELY(mRanges.Length() >= 2)) {
200 // For avoiding to handle same things in same range, we should drop and
201 // merge unnecessary ranges. Note that the ranges never overlap
202 // because selection ranges are not allowed it so that we need to check only
203 // end offset vs start offset of next one.
204 for (const size_t i : Reversed(IntegerRange(mRanges.Length() - 1u))) {
205 MOZ_ASSERT(mRanges[i]->EndOffset() < mRanges[i + 1]->StartOffset());
206 // XXX Should we delete collapsed range unless the index is 0? Without
207 // Selection API, such situation cannot happen so that `TextEditor`
208 // may behave unexpectedly.
209 if (MOZ_UNLIKELY(mRanges[i]->EndOffset() >=
210 mRanges[i + 1]->StartOffset())) {
211 const uint32_t newEndOffset = mRanges[i + 1]->EndOffset();
212 mRanges.RemoveElementAt(i + 1);
213 if (MOZ_UNLIKELY(NS_WARN_IF(newEndOffset > mRanges[i]->EndOffset()))) {
214 // So, this case shouldn't happen.
215 mRanges[i]->SetStartAndEnd(
216 const_cast<Text*>(&aTextNode), mRanges[i]->StartOffset(),
217 const_cast<Text*>(&aTextNode), newEndOffset);
224 Result<bool, nsresult>
225 AutoClonedRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
226 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
227 IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent) {
228 if (IsCollapsed()) {
229 return false;
232 switch (aDirectionAndAmount) {
233 case nsIEditor::eNext:
234 case nsIEditor::eNextWord:
235 case nsIEditor::ePrevious:
236 case nsIEditor::ePreviousWord:
237 break;
238 default:
239 return false;
242 bool changed = false;
243 for (const OwningNonNull<nsRange>& range : mRanges) {
244 MOZ_ASSERT(!range->IsInAnySelection(),
245 "Changing range in selection may cause running script");
246 Result<bool, nsresult> result =
247 WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
248 aHTMLEditor, range);
249 if (result.isErr()) {
250 NS_WARNING(
251 "WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() "
252 "failed");
253 return Err(result.inspectErr());
255 changed |= result.inspect();
258 if (mRanges.Length() == 1 && aIfSelectingOnlyOneAtomicContent ==
259 IfSelectingOnlyOneAtomicContent::Collapse) {
260 MOZ_ASSERT(mRanges[0].get() == mAnchorFocusRange.get());
261 if (mAnchorFocusRange->GetStartContainer() ==
262 mAnchorFocusRange->GetEndContainer() &&
263 mAnchorFocusRange->GetChildAtStartOffset() &&
264 mAnchorFocusRange->StartRef().GetNextSiblingOfChildAtOffset() ==
265 mAnchorFocusRange->GetChildAtEndOffset()) {
266 mAnchorFocusRange->Collapse(aDirectionAndAmount == nsIEditor::eNext ||
267 aDirectionAndAmount == nsIEditor::eNextWord);
268 changed = true;
272 return changed;
275 // static
276 void AutoClonedRangeArray::
277 UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
278 EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint,
279 const Element& aEditingHost) {
280 // FYI: This was moved from
281 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6743
283 // MOOSE major hack:
284 // The GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() and
285 // GetPointAfterFollowingLineBreakOrAtFollowingBlock() don't really do the
286 // right thing for collapsed ranges inside block elements that contain nothing
287 // but a solo <br>. It's easier/ to put a workaround here than to revamp
288 // them. :-(
289 if (aStartPoint != aEndPoint) {
290 return;
293 if (!aStartPoint.IsInContentNode()) {
294 return;
297 // XXX Perhaps, this should be more careful. This may not select only one
298 // node because this just check whether the block is empty or not,
299 // and may not select in non-editable block. However, for inline
300 // editing host case, it's right to look for block element without
301 // editable state check. Now, this method is used for preparation for
302 // other things. So, cannot write test for this method behavior.
303 // So, perhaps, we should get rid of this method and each caller should
304 // handle its job better.
305 Element* const maybeNonEditableBlockElement =
306 HTMLEditUtils::GetInclusiveAncestorElement(
307 *aStartPoint.ContainerAs<nsIContent>(),
308 HTMLEditUtils::ClosestBlockElement,
309 BlockInlineCheck::UseComputedDisplayStyle);
310 if (!maybeNonEditableBlockElement) {
311 return;
314 // Make sure we don't go higher than our root element in the content tree
315 if (aEditingHost.IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
316 return;
319 if (HTMLEditUtils::IsEmptyNode(
320 *maybeNonEditableBlockElement,
321 {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
322 aStartPoint.Set(maybeNonEditableBlockElement, 0u);
323 aEndPoint.SetToEndOf(maybeNonEditableBlockElement);
328 * Get the point before the line containing aPointInLine.
330 * @return If the line starts after a `<br>` element, returns next
331 * sibling of the `<br>` element.
332 * If the line is first line of a block, returns point of
333 * the block.
334 * NOTE: The result may be point of editing host. I.e., the container may be
335 * outside of editing host.
337 MOZ_NEVER_INLINE_DEBUG static EditorDOMPoint
338 GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
339 const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
340 BlockInlineCheck aBlockInlineCheck, const Element& aAncestorLimiter) {
341 // FYI: This was moved from
342 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6447
344 if (NS_WARN_IF(!aPointInLine.IsSet())) {
345 return EditorDOMPoint();
348 EditorDOMPoint point(aPointInLine);
349 // Start scanning from the container node if aPoint is in a text node.
350 // XXX Perhaps, IsInDataNode() must be expected.
351 if (point.IsInTextNode()) {
352 if (!point.GetContainer()->GetParentNode()) {
353 // Okay, can't promote any further
354 // XXX Why don't we return start of the text node?
355 return point;
357 // If there is a preformatted linefeed in the text node, let's return
358 // the point after it.
359 EditorDOMPoint atLastPreformattedNewLine =
360 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
361 point);
362 if (atLastPreformattedNewLine.IsSet()) {
363 return atLastPreformattedNewLine.NextPoint();
365 point.Set(point.GetContainer());
368 // Look back through any further inline nodes that aren't across a <br>
369 // from us, and that are enclosed in the same block.
370 // I.e., looking for start of current hard line.
371 constexpr HTMLEditUtils::WalkTreeOptions
372 ignoreNonEditableNodeAndStopAtBlockBoundary{
373 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
374 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
375 for (nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
376 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
377 aBlockInlineCheck, &aAncestorLimiter);
378 previousEditableContent && previousEditableContent->GetParentNode() &&
379 !HTMLEditUtils::IsVisibleBRElement(*previousEditableContent) &&
380 !HTMLEditUtils::IsBlockElement(*previousEditableContent,
381 aBlockInlineCheck);
382 previousEditableContent = HTMLEditUtils::GetPreviousContent(
383 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
384 aBlockInlineCheck, &aAncestorLimiter)) {
385 EditorDOMPoint atLastPreformattedNewLine =
386 HTMLEditUtils::GetPreviousPreformattedNewLineInTextNode<EditorDOMPoint>(
387 EditorRawDOMPoint::AtEndOf(*previousEditableContent));
388 if (atLastPreformattedNewLine.IsSet()) {
389 return atLastPreformattedNewLine.NextPoint();
391 point.Set(previousEditableContent);
394 // Finding the real start for this point unless current line starts after
395 // <br> element. Look up the tree for as long as we are the first node in
396 // the container (typically, start of nearest block ancestor), and as long
397 // as we haven't hit the body node.
398 for (nsIContent* nearContent = HTMLEditUtils::GetPreviousContent(
399 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
400 aBlockInlineCheck, &aAncestorLimiter);
401 !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
402 point.GetContainerParent();
403 nearContent = HTMLEditUtils::GetPreviousContent(
404 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
405 aBlockInlineCheck, &aAncestorLimiter)) {
406 // Don't keep looking up if we have found a blockquote element to act on
407 // when we handle outdent.
408 // XXX Sounds like this is hacky. If possible, it should be check in
409 // outdent handler for consistency between edit sub-actions.
410 // We should check Chromium's behavior of outdent when Selection
411 // starts from `<blockquote>` and starts from first child of
412 // `<blockquote>`.
413 if (aEditSubAction == EditSubAction::eOutdent &&
414 point.IsContainerHTMLElement(nsGkAtoms::blockquote)) {
415 break;
418 // Don't walk past the editable section. Note that we need to check
419 // before walking up to a parent because we need to return the parent
420 // object, so the parent itself might not be in the editable area, but
421 // it's OK if we're not performing a block-level action.
422 bool blockLevelAction =
423 aEditSubAction == EditSubAction::eIndent ||
424 aEditSubAction == EditSubAction::eOutdent ||
425 aEditSubAction == EditSubAction::eSetOrClearAlignment ||
426 aEditSubAction == EditSubAction::eCreateOrRemoveBlock ||
427 aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand;
428 // XXX So, does this check whether the container is removable or not? It
429 // seems that here can be rewritten as obviously what here tries to
430 // check.
431 if (!point.GetContainerParent()->IsInclusiveDescendantOf(
432 &aAncestorLimiter) &&
433 (blockLevelAction ||
434 !point.GetContainer()->IsInclusiveDescendantOf(&aAncestorLimiter))) {
435 break;
438 // If we're formatting a block, we should reformat first ancestor format
439 // block.
440 if (aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand &&
441 point.IsContainerElement() &&
442 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
443 *point.ContainerAs<Element>())) {
444 point.Set(point.GetContainer());
445 break;
448 point.Set(point.GetContainer());
450 return point;
454 * Get the point after the following line break or the block which breaks the
455 * line containing aPointInLine.
457 * @return If the line ends with a visible `<br>` element, returns
458 * the point after the `<br>` element.
459 * If the line ends with a preformatted linefeed, returns
460 * the point after the linefeed unless it's an invisible
461 * line break immediately before a block boundary.
462 * If the line ends with a block boundary, returns the
463 * point of the block.
465 MOZ_NEVER_INLINE_DEBUG static EditorDOMPoint
466 GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
467 const EditorDOMPoint& aPointInLine, EditSubAction aEditSubAction,
468 BlockInlineCheck aBlockInlineCheck, const Element& aAncestorLimiter) {
469 // FYI: This was moved from
470 // https://searchfox.org/mozilla-central/rev/3419858c997f422e3e70020a46baae7f0ec6dacc/editor/libeditor/HTMLEditSubActionHandler.cpp#6541
472 if (NS_WARN_IF(!aPointInLine.IsSet())) {
473 return EditorDOMPoint();
476 EditorDOMPoint point(aPointInLine);
477 // Start scanning from the container node if aPoint is in a text node.
478 // XXX Perhaps, IsInDataNode() must be expected.
479 if (point.IsInTextNode()) {
480 if (NS_WARN_IF(!point.GetContainer()->GetParentNode())) {
481 // Okay, can't promote any further
482 // XXX Why don't we return end of the text node?
483 return point;
485 EditorDOMPoint atNextPreformattedNewLine =
486 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
487 EditorDOMPoint>(point);
488 if (atNextPreformattedNewLine.IsSet()) {
489 // If the linefeed is last character of the text node, it may be
490 // invisible if it's immediately before a block boundary. In such
491 // case, we should return the block boundary.
492 Element* maybeNonEditableBlockElement = nullptr;
493 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
494 atNextPreformattedNewLine, &maybeNonEditableBlockElement) &&
495 maybeNonEditableBlockElement) {
496 // If the block is a parent of the editing host, let's return end
497 // of editing host.
498 if (maybeNonEditableBlockElement == &aAncestorLimiter ||
499 !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
500 &aAncestorLimiter)) {
501 return EditorDOMPoint::AtEndOf(aAncestorLimiter);
503 // If it's invisible because of parent block boundary, return end
504 // of the block. Otherwise, i.e., it's followed by a child block,
505 // returns the point of the child block.
506 if (atNextPreformattedNewLine.ContainerAs<Text>()
507 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
508 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
510 return EditorDOMPoint(maybeNonEditableBlockElement);
512 // Otherwise, return the point after the preformatted linefeed.
513 return atNextPreformattedNewLine.NextPoint();
515 // want to be after the text node
516 point.SetAfter(point.GetContainer());
517 NS_WARNING_ASSERTION(point.IsSet(), "Failed to set to after the text node");
520 // Look ahead through any further inline nodes that aren't across a <br> from
521 // us, and that are enclosed in the same block.
522 // XXX Currently, we stop block-extending when finding visible <br> element.
523 // This might be different from "block-extend" of execCommand spec.
524 // However, the spec is really unclear.
525 // XXX Probably, scanning only editable nodes is wrong for
526 // EditSubAction::eCreateOrRemoveBlock and
527 // EditSubAction::eFormatBlockForHTMLCommand because it might be better to
528 // wrap existing inline elements even if it's non-editable. For example,
529 // following examples with insertParagraph causes different result:
530 // * <div contenteditable>foo[]<b contenteditable="false">bar</b></div>
531 // * <div contenteditable>foo[]<b>bar</b></div>
532 // * <div contenteditable>foo[]<b contenteditable="false">bar</b>baz</div>
533 // Only in the first case, after the caret position isn't wrapped with
534 // new <div> element.
535 constexpr HTMLEditUtils::WalkTreeOptions
536 ignoreNonEditableNodeAndStopAtBlockBoundary{
537 HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode,
538 HTMLEditUtils::WalkTreeOption::StopAtBlockBoundary};
539 for (nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
540 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
541 aBlockInlineCheck, &aAncestorLimiter);
542 nextEditableContent &&
543 !HTMLEditUtils::IsBlockElement(*nextEditableContent,
544 aBlockInlineCheck) &&
545 nextEditableContent->GetParent();
546 nextEditableContent = HTMLEditUtils::GetNextContent(
547 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
548 aBlockInlineCheck, &aAncestorLimiter)) {
549 EditorDOMPoint atFirstPreformattedNewLine =
550 HTMLEditUtils::GetInclusiveNextPreformattedNewLineInTextNode<
551 EditorDOMPoint>(EditorRawDOMPoint(nextEditableContent, 0));
552 if (atFirstPreformattedNewLine.IsSet()) {
553 // If the linefeed is last character of the text node, it may be
554 // invisible if it's immediately before a block boundary. In such
555 // case, we should return the block boundary.
556 Element* maybeNonEditableBlockElement = nullptr;
557 if (HTMLEditUtils::IsInvisiblePreformattedNewLine(
558 atFirstPreformattedNewLine, &maybeNonEditableBlockElement) &&
559 maybeNonEditableBlockElement) {
560 // If the block is a parent of the editing host, let's return end
561 // of editing host.
562 if (maybeNonEditableBlockElement == &aAncestorLimiter ||
563 !maybeNonEditableBlockElement->IsInclusiveDescendantOf(
564 &aAncestorLimiter)) {
565 return EditorDOMPoint::AtEndOf(aAncestorLimiter);
567 // If it's invisible because of parent block boundary, return end
568 // of the block. Otherwise, i.e., it's followed by a child block,
569 // returns the point of the child block.
570 if (atFirstPreformattedNewLine.ContainerAs<Text>()
571 ->IsInclusiveDescendantOf(maybeNonEditableBlockElement)) {
572 return EditorDOMPoint::AtEndOf(*maybeNonEditableBlockElement);
574 return EditorDOMPoint(maybeNonEditableBlockElement);
576 // Otherwise, return the point after the preformatted linefeed.
577 return atFirstPreformattedNewLine.NextPoint();
579 point.SetAfter(nextEditableContent);
580 if (NS_WARN_IF(!point.IsSet())) {
581 break;
583 if (HTMLEditUtils::IsVisibleBRElement(*nextEditableContent)) {
584 break;
588 // Finding the real end for this point unless current line ends with a <br>
589 // element. Look up the tree for as long as we are the last node in the
590 // container (typically, block node), and as long as we haven't hit the body
591 // node.
592 for (nsIContent* nearContent = HTMLEditUtils::GetNextContent(
593 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
594 aBlockInlineCheck, &aAncestorLimiter);
595 !nearContent && !point.IsContainerHTMLElement(nsGkAtoms::body) &&
596 point.GetContainerParent();
597 nearContent = HTMLEditUtils::GetNextContent(
598 point, ignoreNonEditableNodeAndStopAtBlockBoundary,
599 aBlockInlineCheck, &aAncestorLimiter)) {
600 // Don't walk past the editable section. Note that we need to check before
601 // walking up to a parent because we need to return the parent object, so
602 // the parent itself might not be in the editable area, but it's OK.
603 // XXX Maybe returning parent of editing host is really error prone since
604 // everybody need to check whether the end point is in editing host
605 // when they touch there.
606 if (!point.GetContainer()->IsInclusiveDescendantOf(&aAncestorLimiter) &&
607 !point.GetContainerParent()->IsInclusiveDescendantOf(
608 &aAncestorLimiter)) {
609 break;
612 // If we're formatting a block, we should reformat first ancestor format
613 // block.
614 if (aEditSubAction == EditSubAction::eFormatBlockForHTMLCommand &&
615 point.IsContainerElement() &&
616 HTMLEditUtils::IsFormatElementForFormatBlockCommand(
617 *point.ContainerAs<Element>())) {
618 point.SetAfter(point.GetContainer());
619 break;
622 point.SetAfter(point.GetContainer());
623 if (NS_WARN_IF(!point.IsSet())) {
624 break;
627 return point;
630 void AutoClonedRangeArray::ExtendRangesToWrapLines(
631 EditSubAction aEditSubAction, BlockInlineCheck aBlockInlineCheck,
632 const Element& aAncestorLimiter) {
633 // FYI: This is originated in
634 // https://searchfox.org/mozilla-central/rev/1739f1301d658c9bff544a0a095ab11fca2e549d/editor/libeditor/HTMLEditSubActionHandler.cpp#6712
636 bool removeSomeRanges = false;
637 for (const OwningNonNull<nsRange>& range : mRanges) {
638 // Remove non-positioned ranges.
639 if (MOZ_UNLIKELY(!range->IsPositioned())) {
640 removeSomeRanges = true;
641 continue;
643 // If the range is native anonymous subtrees, we must meet a bug of
644 // `Selection` so that we need to hack here.
645 if (MOZ_UNLIKELY(range->GetStartContainer()->IsInNativeAnonymousSubtree() ||
646 range->GetEndContainer()->IsInNativeAnonymousSubtree())) {
647 EditorRawDOMRange rawRange(range);
648 if (!rawRange.EnsureNotInNativeAnonymousSubtree()) {
649 range->Reset();
650 removeSomeRanges = true;
651 continue;
653 if (NS_FAILED(
654 range->SetStartAndEnd(rawRange.StartRef().ToRawRangeBoundary(),
655 rawRange.EndRef().ToRawRangeBoundary())) ||
656 MOZ_UNLIKELY(!range->IsPositioned())) {
657 range->Reset();
658 removeSomeRanges = true;
659 continue;
662 // Finally, extend the range.
663 if (NS_FAILED(ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
664 range, aEditSubAction, aBlockInlineCheck, aAncestorLimiter))) {
665 // If we failed to extend the range, we should use the original range
666 // as-is unless the range is broken at setting the range.
667 if (NS_WARN_IF(!range->IsPositioned())) {
668 removeSomeRanges = true;
672 if (removeSomeRanges) {
673 for (const size_t i : Reversed(IntegerRange(mRanges.Length()))) {
674 if (!mRanges[i]->IsPositioned()) {
675 mRanges.RemoveElementAt(i);
678 if (!mAnchorFocusRange || !mAnchorFocusRange->IsPositioned()) {
679 if (mRanges.IsEmpty()) {
680 mAnchorFocusRange = nullptr;
681 } else {
682 mAnchorFocusRange = mRanges.LastElement();
688 // static
689 nsresult
690 AutoClonedRangeArray::ExtendRangeToWrapStartAndEndLinesContainingBoundaries(
691 nsRange& aRange, EditSubAction aEditSubAction,
692 BlockInlineCheck aBlockInlineCheck, const Element& aEditingHost) {
693 MOZ_DIAGNOSTIC_ASSERT(
694 !EditorRawDOMPoint(aRange.StartRef()).IsInNativeAnonymousSubtree());
695 MOZ_DIAGNOSTIC_ASSERT(
696 !EditorRawDOMPoint(aRange.EndRef()).IsInNativeAnonymousSubtree());
698 if (NS_WARN_IF(!aRange.IsPositioned())) {
699 return NS_ERROR_INVALID_ARG;
702 EditorDOMPoint startPoint(aRange.StartRef()), endPoint(aRange.EndRef());
704 // If we're joining blocks, we call this for selecting a line to move.
705 // Therefore, we don't want to select the ancestor blocks in this case
706 // even if they are empty.
707 if (aEditSubAction != EditSubAction::eMergeBlockContents) {
708 AutoClonedRangeArray::
709 UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
710 startPoint, endPoint, aEditingHost);
713 // Make a new adjusted range to represent the appropriate block content.
714 // This is tricky. The basic idea is to push out the range endpoints to
715 // truly enclose the blocks that we will affect.
717 // Make sure that the new range ends up to be in the editable section.
718 // XXX Looks like that this check wastes the time. Perhaps, we should
719 // implement a method which checks both two DOM points in the editor
720 // root.
722 startPoint =
723 GetPointAtFirstContentOfLineOrParentHTMLBlockIfFirstContentOfBlock(
724 startPoint, aEditSubAction, aBlockInlineCheck, aEditingHost);
725 // XXX GetPointAtFirstContentOfLineOrParentBlockIfFirstContentOfBlock() may
726 // return point of editing host. Perhaps, we should change it and stop
727 // checking it here since this check may be expensive.
728 // XXX If the container is an element in the editing host but it points end of
729 // the container, this returns nullptr. Is it intentional?
730 if (!startPoint.GetChildOrContainerIfDataNode() ||
731 !startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
732 &aEditingHost)) {
733 return NS_ERROR_FAILURE;
735 endPoint = GetPointAfterFollowingLineBreakOrAtFollowingHTMLBlock(
736 endPoint, aEditSubAction, aBlockInlineCheck, aEditingHost);
737 const EditorDOMPoint lastRawPoint =
738 endPoint.IsStartOfContainer() ? endPoint : endPoint.PreviousPoint();
739 // XXX GetPointAfterFollowingLineBreakOrAtFollowingBlock() may return point of
740 // editing host. Perhaps, we should change it and stop checking it here
741 // since this check may be expensive.
742 // XXX If the container is an element in the editing host but it points end of
743 // the container, this returns nullptr. Is it intentional?
744 if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
745 !lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
746 &aEditingHost)) {
747 return NS_ERROR_FAILURE;
750 nsresult rv = aRange.SetStartAndEnd(startPoint.ToRawRangeBoundary(),
751 endPoint.ToRawRangeBoundary());
752 if (NS_FAILED(rv)) {
753 return NS_ERROR_FAILURE;
755 return NS_OK;
758 Result<EditorDOMPoint, nsresult> AutoClonedRangeArray::
759 SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
760 HTMLEditor& aHTMLEditor, BlockInlineCheck aBlockInlineCheck,
761 const Element& aEditingHost,
762 const nsIContent* aAncestorLimiter /* = nullptr */) {
763 // FYI: The following code is originated in
764 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#6971
766 // Split text nodes. This is necessary, since given ranges may end in text
767 // nodes in case where part of a pre-formatted elements needs to be moved.
768 EditorDOMPoint pointToPutCaret;
769 IgnoredErrorResult ignoredError;
770 for (const OwningNonNull<nsRange>& range : mRanges) {
771 EditorDOMPoint atEnd(range->EndRef());
772 if (NS_WARN_IF(!atEnd.IsSet()) || !atEnd.IsInTextNode() ||
773 atEnd.GetContainer() == aAncestorLimiter) {
774 continue;
777 if (!atEnd.IsStartOfContainer() && !atEnd.IsEndOfContainer()) {
778 // Split the text node.
779 Result<SplitNodeResult, nsresult> splitAtEndResult =
780 aHTMLEditor.SplitNodeWithTransaction(atEnd);
781 if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
782 NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
783 return splitAtEndResult.propagateErr();
785 SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
786 unwrappedSplitAtEndResult.MoveCaretPointTo(
787 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
789 // Correct the range.
790 // The new end parent becomes the parent node of the text.
791 MOZ_ASSERT(!range->IsInAnySelection());
792 range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
793 .ToRawRangeBoundary(),
794 ignoredError);
795 NS_WARNING_ASSERTION(!ignoredError.Failed(),
796 "nsRange::SetEnd() failed, but ignored");
797 ignoredError.SuppressException();
801 // FYI: The following code is originated in
802 // https://searchfox.org/mozilla-central/rev/c8e15e17bc6fd28f558c395c948a6251b38774ff/editor/libeditor/HTMLEditSubActionHandler.cpp#7023
803 AutoTArray<OwningNonNull<RangeItem>, 8> rangeItemArray;
804 rangeItemArray.AppendElements(mRanges.Length());
806 // First register ranges for special editor gravity
807 Maybe<size_t> anchorFocusRangeIndex;
808 for (const size_t index : IntegerRange(rangeItemArray.Length())) {
809 rangeItemArray[index] = new RangeItem();
810 rangeItemArray[index]->StoreRange(*mRanges[index]);
811 aHTMLEditor.RangeUpdaterRef().RegisterRangeItem(*rangeItemArray[index]);
812 if (mRanges[index] == mAnchorFocusRange) {
813 anchorFocusRangeIndex = Some(index);
816 // TODO: We should keep the array, and just update the ranges.
817 mRanges.Clear();
818 mAnchorFocusRange = nullptr;
819 // Now bust up inlines.
820 nsresult rv = NS_OK;
821 for (const OwningNonNull<RangeItem>& item : Reversed(rangeItemArray)) {
822 // MOZ_KnownLive because 'rangeItemArray' is guaranteed to keep it alive.
823 Result<EditorDOMPoint, nsresult> splitParentsResult =
824 aHTMLEditor.SplitInlineAncestorsAtRangeBoundaries(
825 MOZ_KnownLive(*item), aBlockInlineCheck, aEditingHost,
826 aAncestorLimiter);
827 if (MOZ_UNLIKELY(splitParentsResult.isErr())) {
828 NS_WARNING("HTMLEditor::SplitInlineAncestorsAtRangeBoundaries() failed");
829 rv = splitParentsResult.unwrapErr();
830 break;
832 if (splitParentsResult.inspect().IsSet()) {
833 pointToPutCaret = splitParentsResult.unwrap();
836 // Then unregister the ranges
837 for (const size_t index : IntegerRange(rangeItemArray.Length())) {
838 aHTMLEditor.RangeUpdaterRef().DropRangeItem(rangeItemArray[index]);
839 RefPtr<nsRange> range = rangeItemArray[index]->GetRange();
840 if (range && range->IsPositioned()) {
841 if (anchorFocusRangeIndex.isSome() && index == *anchorFocusRangeIndex) {
842 mAnchorFocusRange = range;
844 mRanges.AppendElement(std::move(range));
847 if (!mAnchorFocusRange && !mRanges.IsEmpty()) {
848 mAnchorFocusRange = mRanges.LastElement();
851 // XXX Why do we ignore the other errors here??
852 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
853 return Err(NS_ERROR_EDITOR_DESTROYED);
855 return pointToPutCaret;
858 nsresult AutoClonedRangeArray::CollectEditTargetNodes(
859 const HTMLEditor& aHTMLEditor,
860 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents,
861 EditSubAction aEditSubAction,
862 CollectNonEditableNodes aCollectNonEditableNodes) const {
863 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
865 // FYI: This was moved from
866 // https://searchfox.org/mozilla-central/rev/4bce7d85ba4796dd03c5dcc7cfe8eee0e4c07b3b/editor/libeditor/HTMLEditSubActionHandler.cpp#7060
868 // Gather up a list of all the nodes
869 for (const OwningNonNull<nsRange>& range : mRanges) {
870 DOMSubtreeIterator iter;
871 nsresult rv = iter.Init(*range);
872 if (NS_FAILED(rv)) {
873 NS_WARNING("DOMSubtreeIterator::Init() failed");
874 return rv;
876 if (aOutArrayOfContents.IsEmpty()) {
877 iter.AppendAllNodesToArray(aOutArrayOfContents);
878 } else {
879 AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfTopChildren;
880 iter.AppendNodesToArray(
881 +[](nsINode& aNode, void* aArray) -> bool {
882 MOZ_ASSERT(aArray);
883 return !static_cast<nsTArray<OwningNonNull<nsIContent>>*>(aArray)
884 ->Contains(&aNode);
886 arrayOfTopChildren, &aOutArrayOfContents);
887 aOutArrayOfContents.AppendElements(std::move(arrayOfTopChildren));
889 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
890 for (const size_t i :
891 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
892 if (!EditorUtils::IsEditableContent(aOutArrayOfContents[i],
893 EditorUtils::EditorType::HTML)) {
894 aOutArrayOfContents.RemoveElementAt(i);
900 switch (aEditSubAction) {
901 case EditSubAction::eCreateOrRemoveBlock:
902 case EditSubAction::eFormatBlockForHTMLCommand: {
903 // Certain operations should not act on li's and td's, but rather inside
904 // them. Alter the list as needed.
905 CollectChildrenOptions options = {
906 CollectChildrenOption::CollectListChildren,
907 CollectChildrenOption::CollectTableChildren};
908 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
909 options += CollectChildrenOption::IgnoreNonEditableChildren;
911 if (aEditSubAction == EditSubAction::eCreateOrRemoveBlock) {
912 for (const size_t index :
913 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
914 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
915 if (HTMLEditUtils::IsListItem(content)) {
916 aOutArrayOfContents.RemoveElementAt(index);
917 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
918 options);
921 } else {
922 // <dd> and <dt> are format blocks. Therefore, we should not handle
923 // their children directly. They should be replaced with new format
924 // block.
925 MOZ_ASSERT(
926 HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dt));
927 MOZ_ASSERT(
928 HTMLEditUtils::IsFormatTagForFormatBlockCommand(*nsGkAtoms::dd));
929 for (const size_t index :
930 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
931 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
932 MOZ_ASSERT_IF(HTMLEditUtils::IsListItem(content),
933 content->IsAnyOfHTMLElements(
934 nsGkAtoms::dd, nsGkAtoms::dt, nsGkAtoms::li));
935 if (content->IsHTMLElement(nsGkAtoms::li)) {
936 aOutArrayOfContents.RemoveElementAt(index);
937 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
938 options);
942 // Empty text node shouldn't be selected if unnecessary
943 for (const size_t index :
944 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
945 if (const Text* text = aOutArrayOfContents[index]->GetAsText()) {
946 // Don't select empty text except to empty block
947 if (!HTMLEditUtils::IsVisibleTextNode(*text)) {
948 aOutArrayOfContents.RemoveElementAt(index);
952 break;
954 case EditSubAction::eCreateOrChangeList: {
955 // XXX aCollectNonEditableNodes is ignored here. Maybe a bug.
956 CollectChildrenOptions options = {
957 CollectChildrenOption::CollectTableChildren};
958 for (const size_t index :
959 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
960 // Scan for table elements. If we find table elements other than
961 // table, replace it with a list of any editable non-table content
962 // because if a selection range starts from end in a table-cell and
963 // ends at or starts from outside the `<table>`, we need to make
964 // lists in each selected table-cells.
965 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
966 if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
967 aOutArrayOfContents.RemoveElementAt(index);
968 HTMLEditUtils::CollectChildren(content, aOutArrayOfContents, index,
969 options);
972 // If there is only one node in the array, and it is a `<div>`,
973 // `<blockquote>` or a list element, then look inside of it until we
974 // find inner list or content.
975 if (aOutArrayOfContents.Length() != 1) {
976 break;
978 Element* deepestDivBlockquoteOrListElement =
979 HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
980 aOutArrayOfContents[0],
981 {HTMLEditUtils::WalkTreeOption::IgnoreNonEditableNode},
982 BlockInlineCheck::Unused, nsGkAtoms::div, nsGkAtoms::blockquote,
983 nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl);
984 if (!deepestDivBlockquoteOrListElement) {
985 break;
987 if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
988 nsGkAtoms::div, nsGkAtoms::blockquote)) {
989 aOutArrayOfContents.Clear();
990 // XXX Before we're called, non-editable nodes are ignored. However,
991 // we may append non-editable nodes here.
992 HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
993 aOutArrayOfContents, 0, {});
994 break;
996 aOutArrayOfContents.ReplaceElementAt(
997 0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
998 break;
1000 case EditSubAction::eOutdent:
1001 case EditSubAction::eIndent:
1002 case EditSubAction::eSetPositionToAbsolute: {
1003 // Indent/outdent already do something special for list items, but we
1004 // still need to make sure we don't act on table elements
1005 CollectChildrenOptions options = {
1006 CollectChildrenOption::CollectListChildren,
1007 CollectChildrenOption::CollectTableChildren};
1008 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1009 options += CollectChildrenOption::IgnoreNonEditableChildren;
1011 for (const size_t index :
1012 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1013 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
1014 if (HTMLEditUtils::IsAnyTableElementButNotTable(content)) {
1015 aOutArrayOfContents.RemoveElementAt(index);
1016 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
1017 options);
1020 break;
1022 default:
1023 break;
1026 // Outdent should look inside of divs.
1027 if (aEditSubAction == EditSubAction::eOutdent &&
1028 !aHTMLEditor.IsCSSEnabled()) {
1029 CollectChildrenOptions options = {};
1030 if (aCollectNonEditableNodes == CollectNonEditableNodes::No) {
1031 options += CollectChildrenOption::IgnoreNonEditableChildren;
1033 for (const size_t index :
1034 Reversed(IntegerRange(aOutArrayOfContents.Length()))) {
1035 OwningNonNull<nsIContent> content = aOutArrayOfContents[index];
1036 if (content->IsHTMLElement(nsGkAtoms::div)) {
1037 aOutArrayOfContents.RemoveElementAt(index);
1038 HTMLEditUtils::CollectChildren(*content, aOutArrayOfContents, index,
1039 options);
1044 return NS_OK;
1047 Element* AutoClonedRangeArray::GetClosestAncestorAnyListElementOfRange() const {
1048 for (const OwningNonNull<nsRange>& range : mRanges) {
1049 nsINode* commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
1050 if (MOZ_UNLIKELY(!commonAncestorNode)) {
1051 continue;
1053 for (Element* const element :
1054 commonAncestorNode->InclusiveAncestorsOfType<Element>()) {
1055 if (HTMLEditUtils::IsAnyListElement(element)) {
1056 return element;
1060 return nullptr;
1063 /******************************************************************************
1064 * mozilla::AutoClonedSelectionRangeArray
1065 *****************************************************************************/
1067 template AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1068 const EditorDOMRange& aRange,
1069 const LimitersAndCaretData& aLimitersAndCaretData);
1070 template AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1071 const EditorRawDOMRange& aRange,
1072 const LimitersAndCaretData& aLimitersAndCaretData);
1073 template AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1074 const EditorDOMPoint& aRange,
1075 const LimitersAndCaretData& aLimitersAndCaretData);
1076 template AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1077 const EditorRawDOMPoint& aRange,
1078 const LimitersAndCaretData& aLimitersAndCaretData);
1080 AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1081 const dom::Selection& aSelection) {
1082 Initialize(aSelection);
1085 AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1086 const AutoClonedSelectionRangeArray& aOther)
1087 : AutoClonedRangeArray(aOther),
1088 mLimitersAndCaretData(aOther.mLimitersAndCaretData) {}
1090 template <typename PointType>
1091 AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1092 const EditorDOMRangeBase<PointType>& aRange,
1093 const LimitersAndCaretData& aLimitersAndCaretData)
1094 : mLimitersAndCaretData(aLimitersAndCaretData) {
1095 MOZ_ASSERT(aRange.IsPositionedAndValid());
1096 RefPtr<nsRange> range = aRange.CreateRange(IgnoreErrors());
1097 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned()) ||
1098 NS_WARN_IF(!RangeIsInLimiters(*range))) {
1099 return;
1101 mRanges.AppendElement(*range);
1102 mAnchorFocusRange = std::move(range);
1105 template <typename PT, typename CT>
1106 AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1107 const EditorDOMPointBase<PT, CT>& aPoint,
1108 const LimitersAndCaretData& aLimitersAndCaretData)
1109 : mLimitersAndCaretData(aLimitersAndCaretData) {
1110 MOZ_ASSERT(aPoint.IsSetAndValid());
1111 if (NS_WARN_IF(!NodeIsInLimiters(aPoint.GetContainer()))) {
1112 return;
1114 RefPtr<nsRange> range = aPoint.CreateCollapsedRange(IgnoreErrors());
1115 if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
1116 return;
1118 mRanges.AppendElement(*range);
1119 mAnchorFocusRange = std::move(range);
1120 SetNewCaretAssociationHint(aPoint.ToRawRangeBoundary(),
1121 aPoint.GetInterlinePosition());
1124 AutoClonedSelectionRangeArray::AutoClonedSelectionRangeArray(
1125 const nsRange& aRange, const LimitersAndCaretData& aLimitersAndCaretData)
1126 : mLimitersAndCaretData(aLimitersAndCaretData) {
1127 MOZ_ASSERT(aRange.IsPositioned());
1128 if (NS_WARN_IF(!RangeIsInLimiters(aRange))) {
1129 return;
1131 mRanges.AppendElement(aRange.CloneRange());
1132 mAnchorFocusRange = mRanges[0];
1135 void AutoClonedSelectionRangeArray::SetNewCaretAssociationHint(
1136 const RawRangeBoundary& aRawRangeBoundary,
1137 InterlinePosition aInternlinePosition) {
1138 if (aInternlinePosition == Selection::InterlinePosition::Undefined) {
1139 mLimitersAndCaretData.mCaretAssociationHint = ComputeCaretAssociationHint(
1140 mLimitersAndCaretData.mCaretAssociationHint,
1141 mLimitersAndCaretData.mCaretBidiLevel, aRawRangeBoundary);
1142 } else {
1143 SetInterlinePosition(aInternlinePosition);
1147 bool AutoClonedSelectionRangeArray::SaveAndTrackRanges(
1148 HTMLEditor& aHTMLEditor) {
1149 if (mSavedRanges.isSome()) {
1150 return false;
1152 mSavedRanges.emplace(*this);
1153 aHTMLEditor.RangeUpdaterRef().RegisterSelectionState(mSavedRanges.ref());
1154 mTrackingHTMLEditor = &aHTMLEditor;
1155 return true;
1158 void AutoClonedSelectionRangeArray::ClearSavedRanges() {
1159 if (mSavedRanges.isNothing()) {
1160 return;
1162 OwningNonNull<HTMLEditor> htmlEditor(std::move(mTrackingHTMLEditor));
1163 MOZ_ASSERT(!mTrackingHTMLEditor);
1164 htmlEditor->RangeUpdaterRef().DropSelectionState(mSavedRanges.ref());
1165 mSavedRanges.reset();
1168 Result<nsIEditor::EDirection, nsresult>
1169 AutoClonedSelectionRangeArray::ExtendAnchorFocusRangeFor(
1170 const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount) {
1171 MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable());
1172 MOZ_ASSERT(mAnchorFocusRange);
1173 MOZ_ASSERT(mAnchorFocusRange->IsPositioned());
1174 MOZ_ASSERT(mAnchorFocusRange->StartRef().IsSet());
1175 MOZ_ASSERT(mAnchorFocusRange->EndRef().IsSet());
1177 if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
1178 aDirectionAndAmount, *this)) {
1179 return aDirectionAndAmount;
1182 if (NS_WARN_IF(mRanges.IsEmpty())) {
1183 return Err(NS_ERROR_FAILURE);
1186 const RefPtr<PresShell> presShell = aEditorBase.GetPresShell();
1187 if (NS_WARN_IF(!presShell)) {
1188 return Err(NS_ERROR_FAILURE);
1191 const RefPtr<Element> editingHost =
1192 aEditorBase.IsHTMLEditor()
1193 ? aEditorBase.AsHTMLEditor()->ComputeEditingHost()
1194 : nullptr;
1195 if (aEditorBase.IsHTMLEditor() && NS_WARN_IF(!editingHost)) {
1196 return Err(NS_ERROR_FAILURE);
1199 Result<RefPtr<nsRange>, nsresult> result(NS_ERROR_UNEXPECTED);
1200 const OwningNonNull<nsRange> anchorFocusRange = *mAnchorFocusRange;
1201 const LimitersAndCaretData limitersAndCaretData = mLimitersAndCaretData;
1202 const nsDirection rangeDirection =
1203 mDirection == eDirNext ? eDirNext : eDirPrevious;
1204 nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount;
1205 switch (aDirectionAndAmount) {
1206 case nsIEditor::eNextWord:
1207 result = nsFrameSelection::CreateRangeExtendedToNextWordBoundary<nsRange>(
1208 *presShell, limitersAndCaretData, anchorFocusRange, rangeDirection);
1209 if (NS_WARN_IF(aEditorBase.Destroyed())) {
1210 return Err(NS_ERROR_EDITOR_DESTROYED);
1212 NS_WARNING_ASSERTION(
1213 result.isOk(),
1214 "nsFrameSelection::CreateRangeExtendedToNextWordBoundary() failed");
1215 // DeleteSelectionWithTransaction() doesn't handle these actions
1216 // because it's inside batching, so don't confuse it:
1217 directionAndAmountResult = nsIEditor::eNone;
1218 break;
1219 case nsIEditor::ePreviousWord:
1220 result =
1221 nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary<nsRange>(
1222 *presShell, limitersAndCaretData, anchorFocusRange,
1223 rangeDirection);
1224 if (NS_WARN_IF(aEditorBase.Destroyed())) {
1225 return Err(NS_ERROR_EDITOR_DESTROYED);
1227 NS_WARNING_ASSERTION(
1228 result.isOk(),
1229 "nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() "
1230 "failed");
1231 // DeleteSelectionWithTransaction() doesn't handle these actions
1232 // because it's inside batching, so don't confuse it:
1233 directionAndAmountResult = nsIEditor::eNone;
1234 break;
1235 case nsIEditor::eNext:
1236 result =
1237 nsFrameSelection::CreateRangeExtendedToNextGraphemeClusterBoundary<
1238 nsRange>(*presShell, limitersAndCaretData, anchorFocusRange,
1239 rangeDirection);
1240 if (NS_WARN_IF(aEditorBase.Destroyed())) {
1241 return Err(NS_ERROR_EDITOR_DESTROYED);
1243 NS_WARNING_ASSERTION(result.isOk(),
1244 "nsFrameSelection::"
1245 "CreateRangeExtendedToNextGraphemeClusterBoundary() "
1246 "failed");
1247 // Don't set directionAndAmount to eNone (see Bug 502259)
1248 break;
1249 case nsIEditor::ePrevious: {
1250 // Only extend the selection where the selection is after a UTF-16
1251 // surrogate pair or a variation selector.
1252 // For other cases we don't want to do that, in order
1253 // to make sure that pressing backspace will only delete the last
1254 // typed character.
1255 // XXX This is odd if the previous one is a sequence for a grapheme
1256 // cluster.
1257 const auto atStartOfSelection = GetFirstRangeStartPoint<EditorDOMPoint>();
1258 if (MOZ_UNLIKELY(NS_WARN_IF(!atStartOfSelection.IsSet()))) {
1259 return Err(NS_ERROR_FAILURE);
1262 // node might be anonymous DIV, so we find better text node
1263 const EditorDOMPoint insertionPoint =
1264 aEditorBase.IsTextEditor()
1265 ? aEditorBase.AsTextEditor()->FindBetterInsertionPoint(
1266 atStartOfSelection)
1267 : atStartOfSelection.GetPointInTextNodeIfPointingAroundTextNode<
1268 EditorDOMPoint>();
1269 if (MOZ_UNLIKELY(!insertionPoint.IsSet())) {
1270 NS_WARNING(
1271 "EditorBase::FindBetterInsertionPoint() failed, but ignored");
1272 return aDirectionAndAmount;
1275 if (!insertionPoint.IsInTextNode()) {
1276 return aDirectionAndAmount;
1279 const nsTextFragment* data =
1280 &insertionPoint.ContainerAs<Text>()->TextFragment();
1281 uint32_t offset = insertionPoint.Offset();
1282 if (!(offset > 1 &&
1283 data->IsLowSurrogateFollowingHighSurrogateAt(offset - 1)) &&
1284 !(offset > 0 &&
1285 gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) {
1286 return aDirectionAndAmount;
1288 // Different from the `eNext` case, we look for character boundary.
1289 // I'm not sure whether this inconsistency between "Delete" and
1290 // "Backspace" is intentional or not.
1291 result = nsFrameSelection::CreateRangeExtendedToPreviousCharacterBoundary<
1292 nsRange>(*presShell, limitersAndCaretData, anchorFocusRange,
1293 rangeDirection);
1294 if (NS_WARN_IF(aEditorBase.Destroyed())) {
1295 return Err(NS_ERROR_EDITOR_DESTROYED);
1297 NS_WARNING_ASSERTION(
1298 result.isOk(),
1299 "nsFrameSelection::"
1300 "CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed");
1301 break;
1303 case nsIEditor::eToBeginningOfLine:
1304 result =
1305 nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak<nsRange>(
1306 *presShell, limitersAndCaretData, anchorFocusRange,
1307 rangeDirection);
1308 if (NS_WARN_IF(aEditorBase.Destroyed())) {
1309 return Err(NS_ERROR_EDITOR_DESTROYED);
1311 NS_WARNING_ASSERTION(
1312 result.isOk(),
1313 "nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() "
1314 "failed");
1315 directionAndAmountResult = nsIEditor::eNone;
1316 break;
1317 case nsIEditor::eToEndOfLine:
1318 result =
1319 nsFrameSelection::CreateRangeExtendedToNextHardLineBreak<nsRange>(
1320 *presShell, limitersAndCaretData, anchorFocusRange,
1321 rangeDirection);
1322 if (NS_WARN_IF(aEditorBase.Destroyed())) {
1323 return Err(NS_ERROR_EDITOR_DESTROYED);
1325 NS_WARNING_ASSERTION(
1326 result.isOk(),
1327 "nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed");
1328 directionAndAmountResult = nsIEditor::eNext;
1329 break;
1330 default:
1331 return aDirectionAndAmount;
1334 if (result.isErr()) {
1335 return Err(result.inspectErr());
1337 RefPtr<nsRange> extendedRange(result.unwrap().forget());
1338 if (!extendedRange || NS_WARN_IF(!extendedRange->IsPositioned())) {
1339 NS_WARNING("Failed to extend the range, but ignored");
1340 return directionAndAmountResult;
1343 // If the new range isn't editable, keep using the original range.
1344 if (aEditorBase.IsHTMLEditor() &&
1345 !AutoClonedRangeArray::IsEditableRange(*extendedRange, *editingHost)) {
1346 return aDirectionAndAmount;
1349 if (NS_WARN_IF(!mLimitersAndCaretData.RangeInLimiters(*extendedRange))) {
1350 NS_WARNING("A range was extended to outer of selection limiter");
1351 return Err(NS_ERROR_FAILURE);
1354 // Swap focus/anchor range with the extended range.
1355 DebugOnly<bool> found = false;
1356 for (OwningNonNull<nsRange>& range : mRanges) {
1357 if (range == mAnchorFocusRange) {
1358 range = *extendedRange;
1359 found = true;
1360 break;
1363 MOZ_ASSERT(found);
1364 mAnchorFocusRange.swap(extendedRange);
1365 return directionAndAmountResult;
1368 } // namespace mozilla