Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / editor / libeditor / HTMLEditorDeleteHandler.cpp
blob39bb95151eaa8441559c41ec54b76005b2b30e67
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "HTMLEditor.h"
8 #include "HTMLEditorNestedClasses.h"
10 #include <algorithm>
11 #include <utility>
13 #include "AutoRangeArray.h"
14 #include "CSSEditUtils.h"
15 #include "EditAction.h"
16 #include "EditorDOMPoint.h"
17 #include "EditorUtils.h"
18 #include "HTMLEditHelpers.h"
19 #include "HTMLEditorInlines.h"
20 #include "HTMLEditUtils.h"
21 #include "WSRunObject.h"
23 #include "ErrorList.h"
24 #include "js/ErrorReport.h"
25 #include "mozilla/Assertions.h"
26 #include "mozilla/CheckedInt.h"
27 #include "mozilla/ComputedStyle.h" // for ComputedStyle
28 #include "mozilla/ContentIterator.h"
29 #include "mozilla/EditorDOMPoint.h"
30 #include "mozilla/EditorForwards.h"
31 #include "mozilla/InternalMutationEvent.h"
32 #include "mozilla/Logging.h"
33 #include "mozilla/Maybe.h"
34 #include "mozilla/OwningNonNull.h"
35 #include "mozilla/SelectionState.h"
36 #include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
37 #include "mozilla/Unused.h"
38 #include "mozilla/dom/AncestorIterator.h"
39 #include "mozilla/dom/Element.h"
40 #include "mozilla/dom/HTMLBRElement.h"
41 #include "mozilla/dom/Selection.h"
42 #include "mozilla/mozalloc.h"
43 #include "nsAString.h"
44 #include "nsAtom.h"
45 #include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
46 #include "nsContentUtils.h"
47 #include "nsDebug.h"
48 #include "nsError.h"
49 #include "nsFrameSelection.h"
50 #include "nsGkAtoms.h"
51 #include "nsIContent.h"
52 #include "nsINode.h"
53 #include "nsRange.h"
54 #include "nsString.h"
55 #include "nsStringFwd.h"
56 #include "nsStyleConsts.h" // for StyleWhiteSpace
57 #include "nsTArray.h"
59 // NOTE: This file was split from:
60 // https://searchfox.org/mozilla-central/rev/c409dd9235c133ab41eba635f906aa16e050c197/editor/libeditor/HTMLEditSubActionHandler.cpp
62 namespace mozilla {
64 using namespace dom;
65 using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
66 using InvisibleWhiteSpaces = HTMLEditUtils::InvisibleWhiteSpaces;
67 using LeafNodeType = HTMLEditUtils::LeafNodeType;
68 using ScanLineBreak = HTMLEditUtils::ScanLineBreak;
69 using TableBoundary = HTMLEditUtils::TableBoundary;
70 using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
72 static LazyLogModule gOneLineMoverLog("AutoMoveOneLineHandler");
74 template Result<CaretPoint, nsresult>
75 HTMLEditor::DeleteTextAndTextNodesWithTransaction(
76 const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint,
77 TreatEmptyTextNodes aTreatEmptyTextNodes);
78 template Result<CaretPoint, nsresult>
79 HTMLEditor::DeleteTextAndTextNodesWithTransaction(
80 const EditorDOMPointInText& aStartPoint,
81 const EditorDOMPointInText& aEndPoint,
82 TreatEmptyTextNodes aTreatEmptyTextNodes);
84 /*****************************************************************************
85 * AutoSetTemporaryAncestorLimiter
86 ****************************************************************************/
88 class MOZ_RAII AutoSetTemporaryAncestorLimiter final {
89 public:
90 AutoSetTemporaryAncestorLimiter(const HTMLEditor& aHTMLEditor,
91 Selection& aSelection,
92 nsINode& aStartPointNode,
93 AutoRangeArray* aRanges = nullptr) {
94 MOZ_ASSERT(aSelection.GetType() == SelectionType::eNormal);
96 if (aSelection.GetAncestorLimiter()) {
97 return;
100 Element* selectionRootElement =
101 aHTMLEditor.FindSelectionRoot(aStartPointNode);
102 if (!selectionRootElement) {
103 return;
105 aHTMLEditor.InitializeSelectionAncestorLimit(*selectionRootElement);
106 mSelection = &aSelection;
107 // Setting ancestor limiter may change ranges which were outer of
108 // the new limiter. Therefore, we need to reinitialize aRanges.
109 if (aRanges) {
110 aRanges->Initialize(aSelection);
114 ~AutoSetTemporaryAncestorLimiter() {
115 if (mSelection) {
116 mSelection->SetAncestorLimiter(nullptr);
120 private:
121 RefPtr<Selection> mSelection;
124 /*****************************************************************************
125 * AutoDeleteRangesHandler
126 ****************************************************************************/
128 class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
129 public:
130 explicit AutoDeleteRangesHandler(
131 const AutoDeleteRangesHandler* aParent = nullptr)
132 : mParent(aParent),
133 mOriginalDirectionAndAmount(nsIEditor::eNone),
134 mOriginalStripWrappers(nsIEditor::eNoStrip) {}
137 * ComputeRangesToDelete() computes actual deletion ranges.
139 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ComputeRangesToDelete(
140 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
141 AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
144 * Deletes content in or around aRangesToDelete.
145 * NOTE: This method creates SelectionBatcher. Therefore, each caller
146 * needs to check if the editor is still available even if this returns
147 * NS_OK.
149 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
150 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
151 nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
152 const Element& aEditingHost);
154 private:
155 [[nodiscard]] bool IsHandlingRecursively() const {
156 return mParent != nullptr;
159 [[nodiscard]] bool CanFallbackToDeleteRangeWithTransaction(
160 const nsRange& aRangeToDelete) const {
161 return !IsHandlingRecursively() &&
162 (!aRangeToDelete.Collapsed() ||
163 EditorBase::HowToHandleCollapsedRangeFor(
164 mOriginalDirectionAndAmount) !=
165 EditorBase::HowToHandleCollapsedRange::Ignore);
168 [[nodiscard]] bool CanFallbackToDeleteRangesWithTransaction(
169 const AutoRangeArray& aRangesToDelete) const {
170 return !IsHandlingRecursively() && !aRangesToDelete.Ranges().IsEmpty() &&
171 (!aRangesToDelete.IsCollapsed() ||
172 EditorBase::HowToHandleCollapsedRangeFor(
173 mOriginalDirectionAndAmount) !=
174 EditorBase::HowToHandleCollapsedRange::Ignore);
178 * HandleDeleteAroundCollapsedRanges() handles deletion with collapsed
179 * ranges. Callers must guarantee that this is called only when
180 * aRangesToDelete.IsCollapsed() returns true.
182 * @param aDirectionAndAmount Direction of the deletion.
183 * @param aStripWrappers Must be eStrip or eNoStrip.
184 * @param aRangesToDelete Ranges to delete. This `IsCollapsed()` must
185 * return true.
186 * @param aWSRunScannerAtCaret Scanner instance which scanned from
187 * caret point.
188 * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret
189 * toward aDirectionAndAmount.
190 * @param aEditingHost The editing host.
192 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
193 HandleDeleteAroundCollapsedRanges(
194 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
195 nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
196 const WSRunScanner& aWSRunScannerAtCaret,
197 const WSScanResult& aScanFromCaretPointResult,
198 const Element& aEditingHost);
199 nsresult ComputeRangesToDeleteAroundCollapsedRanges(
200 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
201 AutoRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret,
202 const WSScanResult& aScanFromCaretPointResult,
203 const Element& aEditingHost) const;
206 * HandleDeleteNonCollapsedRanges() handles deletion with non-collapsed
207 * ranges. Callers must guarantee that this is called only when
208 * aRangesToDelete.IsCollapsed() returns false.
210 * @param aDirectionAndAmount Direction of the deletion.
211 * @param aStripWrappers Must be eStrip or eNoStrip.
212 * @param aRangesToDelete The ranges to delete.
213 * @param aSelectionWasCollapsed If the caller extended `Selection`
214 * from collapsed, set this to `Yes`.
215 * Otherwise, i.e., `Selection` is not
216 * collapsed from the beginning, set
217 * this to `No`.
218 * @param aEditingHost The editing host.
220 enum class SelectionWasCollapsed { Yes, No };
221 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
222 HandleDeleteNonCollapsedRanges(HTMLEditor& aHTMLEditor,
223 nsIEditor::EDirection aDirectionAndAmount,
224 nsIEditor::EStripWrappers aStripWrappers,
225 AutoRangeArray& aRangesToDelete,
226 SelectionWasCollapsed aSelectionWasCollapsed,
227 const Element& aEditingHost);
228 nsresult ComputeRangesToDeleteNonCollapsedRanges(
229 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
230 AutoRangeArray& aRangesToDelete,
231 SelectionWasCollapsed aSelectionWasCollapsed,
232 const Element& aEditingHost) const;
235 * Handle deletion of collapsed ranges in a text node.
237 * @param aDirectionAndAmount Must be eNext or ePrevious.
238 * @param aCaretPosition The position where caret is. This container
239 * must be a text node.
240 * @param aEditingHost The editing host.
242 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
243 HandleDeleteTextAroundCollapsedRanges(
244 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
245 AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
246 nsresult ComputeRangesToDeleteTextAroundCollapsedRanges(
247 nsIEditor::EDirection aDirectionAndAmount,
248 AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const;
251 * Handles deletion of collapsed selection at white-spaces in a text node.
253 * @param aDirectionAndAmount Direction of the deletion.
254 * @param aPointToDelete The point to delete. I.e., typically, caret
255 * position.
256 * @param aEditingHost The editing host.
258 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
259 HandleDeleteCollapsedSelectionAtWhiteSpaces(
260 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
261 const EditorDOMPoint& aPointToDelete, const Element& aEditingHost);
264 * Handle deletion of collapsed selection in a text node.
266 * @param aDirectionAndAmount Direction of the deletion.
267 * @param aRangesToDelete Computed selection ranges to delete.
268 * @param aPointAtDeletingChar The visible char position which you want to
269 * delete.
270 * @param aEditingHost The editing host.
272 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
273 HandleDeleteCollapsedSelectionAtVisibleChar(
274 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
275 AutoRangeArray& aRangesToDelete,
276 const EditorDOMPoint& aPointAtDeletingChar, const Element& aEditingHost);
279 * Handle deletion of atomic elements like <br>, <hr>, <img>, <input>, etc and
280 * data nodes except text node (e.g., comment node). Note that don't call this
281 * directly with `<hr>` element.
283 * @param aAtomicContent The atomic content to be deleted.
284 * @param aCaretPoint The caret point (i.e., selection start or
285 * end).
286 * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
287 * with the caret point.
288 * @param aEditingHost The editing host.
290 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
291 HandleDeleteAtomicContent(HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
292 const EditorDOMPoint& aCaretPoint,
293 const WSRunScanner& aWSRunScannerAtCaret,
294 const Element& aEditingHost);
295 nsresult ComputeRangesToDeleteAtomicContent(
296 Element* aEditingHost, const nsIContent& aAtomicContent,
297 AutoRangeArray& aRangesToDelete) const;
300 * GetAtomicContnetToDelete() returns better content that is deletion of
301 * atomic element. If aScanFromCaretPointResult is special, since this
302 * point may not be editable, we look for better point to remove atomic
303 * content.
305 * @param aDirectionAndAmount Direction of the deletion.
306 * @param aWSRunScannerAtCaret WSRunScanner instance which was
307 * initialized with the caret point.
308 * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret
309 * toward aDirectionAndAmount.
311 static nsIContent* GetAtomicContentToDelete(
312 nsIEditor::EDirection aDirectionAndAmount,
313 const WSRunScanner& aWSRunScannerAtCaret,
314 const WSScanResult& aScanFromCaretPointResult) MOZ_NONNULL_RETURN;
317 * HandleDeleteAtOtherBlockBoundary() handles deletion at other block boundary
318 * (i.e., immediately before or after a block). If this does not join blocks,
319 * `Run()` may be called recursively with creating another instance.
321 * @param aDirectionAndAmount Direction of the deletion.
322 * @param aStripWrappers Must be eStrip or eNoStrip.
323 * @param aOtherBlockElement The block element which follows the caret or
324 * is followed by caret.
325 * @param aCaretPoint The caret point (i.e., selection start or
326 * end).
327 * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
328 * with the caret point.
329 * @param aRangesToDelete Ranges to delete of the caller. This should
330 * be collapsed and the point should match with
331 * aCaretPoint.
332 * @param aEditingHost The editing host.
334 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
335 HandleDeleteAtOtherBlockBoundary(
336 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
337 nsIEditor::EStripWrappers aStripWrappers, Element& aOtherBlockElement,
338 const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret,
339 AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
342 * ExtendOrShrinkRangeToDelete() extends aRangeToDelete if there are
343 * an invisible <br> element and/or some parent empty elements.
345 * @param aFrameSelection If the caller wants range in selection limiter,
346 * set this to non-nullptr which knows the limiter.
347 * @param aRangeToDelete The range to be extended for deletion. This
348 * must not be collapsed, must be positioned.
350 template <typename EditorDOMRangeType>
351 Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToDelete(
352 const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection,
353 const EditorDOMRangeType& aRangeToDelete) const;
356 * A helper method for ExtendOrShrinkRangeToDelete(). This returns shrunken
357 * range if aRangeToDelete selects all over list elements which have some list
358 * item elements to avoid to delete all list items from the list element.
360 MOZ_NEVER_INLINE_DEBUG static EditorRawDOMRange
361 GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
362 const EditorRawDOMRange& aRangeToDelete);
365 * DeleteUnnecessaryNodes() removes unnecessary nodes around aRange.
366 * Note that aRange is tracked with AutoTrackDOMRange.
368 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
369 DeleteUnnecessaryNodes(HTMLEditor& aHTMLEditor, EditorDOMRange& aRange);
372 * DeleteUnnecessaryNodesAndCollapseSelection() calls DeleteUnnecessaryNodes()
373 * and then, collapse selection at tracked aSelectionStartPoint or
374 * aSelectionEndPoint (depending on aDirectionAndAmount).
376 * @param aDirectionAndAmount Direction of the deletion.
377 * If nsIEditor::ePrevious, selection
378 * will be collapsed to aSelectionEndPoint.
379 * Otherwise, selection will be collapsed
380 * to aSelectionStartPoint.
381 * @param aSelectionStartPoint First selection range start after
382 * computing the deleting range.
383 * @param aSelectionEndPoint First selection range end after
384 * computing the deleting range.
386 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
387 DeleteUnnecessaryNodesAndCollapseSelection(
388 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
389 const EditorDOMPoint& aSelectionStartPoint,
390 const EditorDOMPoint& aSelectionEndPoint);
393 * If aContent is a text node that contains only collapsed white-space or
394 * empty and editable.
396 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
397 DeleteNodeIfInvisibleAndEditableTextNode(HTMLEditor& aHTMLEditor,
398 nsIContent& aContent);
401 * DeleteParentBlocksIfEmpty() removes parent block elements if they
402 * don't have visible contents. Note that due performance issue of
403 * WhiteSpaceVisibilityKeeper, this call may be expensive. And also note that
404 * this removes a empty block with a transaction. So, please make sure that
405 * you've already created `AutoPlaceholderBatch`.
407 * @param aPoint The point whether this method climbing up the DOM
408 * tree to remove empty parent blocks.
409 * @return NS_OK if one or more empty block parents are deleted.
410 * NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND if the point is
411 * not in empty block.
412 * Or NS_ERROR_* if something unexpected occurs.
414 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
415 DeleteParentBlocksWithTransactionIfEmpty(HTMLEditor& aHTMLEditor,
416 const EditorDOMPoint& aPoint);
418 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
419 FallbackToDeleteRangeWithTransaction(HTMLEditor& aHTMLEditor,
420 nsRange& aRangeToDelete) const {
421 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
422 MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete));
423 Result<CaretPoint, nsresult> caretPointOrError =
424 aHTMLEditor.DeleteRangeWithTransaction(mOriginalDirectionAndAmount,
425 mOriginalStripWrappers,
426 aRangeToDelete);
427 NS_WARNING_ASSERTION(caretPointOrError.isOk(),
428 "EditorBase::DeleteRangeWithTransaction() failed");
429 return caretPointOrError;
432 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
433 FallbackToDeleteRangesWithTransaction(HTMLEditor& aHTMLEditor,
434 AutoRangeArray& aRangesToDelete) const {
435 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
436 MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
437 Result<CaretPoint, nsresult> caretPointOrError =
438 aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount,
439 mOriginalStripWrappers,
440 aRangesToDelete);
441 NS_WARNING_ASSERTION(caretPointOrError.isOk(),
442 "EditorBase::DeleteRangesWithTransaction() failed");
443 return caretPointOrError;
447 * Compute target range(s) which will be called by
448 * `EditorBase::DeleteRangeWithTransaction()` or
449 * `EditorBase::DeleteRangesWithTransaction()`.
450 * TODO: We should not use it for consistency with each deletion handler
451 * in this and nested classes.
453 nsresult ComputeRangeToDeleteRangeWithTransaction(
454 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
455 nsRange& aRange, const Element& aEditingHost) const;
456 nsresult ComputeRangesToDeleteRangesWithTransaction(
457 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
458 AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const {
459 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
460 const EditorBase::HowToHandleCollapsedRange howToHandleCollapsedRange =
461 EditorBase::HowToHandleCollapsedRangeFor(aDirectionAndAmount);
462 if (NS_WARN_IF(aRangesToDelete.IsCollapsed() &&
463 howToHandleCollapsedRange ==
464 EditorBase::HowToHandleCollapsedRange::Ignore)) {
465 return NS_ERROR_FAILURE;
468 for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
469 if (range->Collapsed()) {
470 continue;
472 nsresult rv = ComputeRangeToDeleteRangeWithTransaction(
473 aHTMLEditor, aDirectionAndAmount, range, aEditingHost);
474 if (NS_FAILED(rv)) {
475 NS_WARNING(
476 "AutoDeleteRangesHandler::ComputeRangeToDeleteRangeWithTransaction("
477 ") failed");
478 return rv;
481 return NS_OK;
484 nsresult FallbackToComputeRangeToDeleteRangeWithTransaction(
485 const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete,
486 const Element& aEditingHost) const {
487 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
488 MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete));
489 nsresult rv = ComputeRangeToDeleteRangeWithTransaction(
490 aHTMLEditor, mOriginalDirectionAndAmount, aRangeToDelete, aEditingHost);
491 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
492 "AutoDeleteRangesHandler::"
493 "ComputeRangeToDeleteRangeWithTransaction() failed");
494 return rv;
496 nsresult FallbackToComputeRangesToDeleteRangesWithTransaction(
497 const HTMLEditor& aHTMLEditor, AutoRangeArray& aRangesToDelete,
498 const Element& aEditingHost) const {
499 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
500 MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
501 nsresult rv = ComputeRangesToDeleteRangesWithTransaction(
502 aHTMLEditor, mOriginalDirectionAndAmount, aRangesToDelete,
503 aEditingHost);
504 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
505 "AutoDeleteRangesHandler::"
506 "ComputeRangesToDeleteRangesWithTransaction() failed");
507 return rv;
510 class MOZ_STACK_CLASS AutoBlockElementsJoiner final {
511 public:
512 AutoBlockElementsJoiner() = delete;
513 explicit AutoBlockElementsJoiner(
514 AutoDeleteRangesHandler& aDeleteRangesHandler)
515 : mDeleteRangesHandler(&aDeleteRangesHandler),
516 mDeleteRangesHandlerConst(aDeleteRangesHandler) {}
517 explicit AutoBlockElementsJoiner(
518 const AutoDeleteRangesHandler& aDeleteRangesHandler)
519 : mDeleteRangesHandler(nullptr),
520 mDeleteRangesHandlerConst(aDeleteRangesHandler) {}
523 * PrepareToDeleteAtCurrentBlockBoundary() considers left content and right
524 * content which are joined for handling deletion at current block boundary
525 * (i.e., at start or end of the current block).
527 * @param aHTMLEditor The HTML editor.
528 * @param aDirectionAndAmount Direction of the deletion.
529 * @param aCurrentBlockElement The current block element.
530 * @param aCaretPoint The caret point (i.e., selection start
531 * or end).
532 * @return true if can continue to handle the
533 * deletion.
535 bool PrepareToDeleteAtCurrentBlockBoundary(
536 const HTMLEditor& aHTMLEditor,
537 nsIEditor::EDirection aDirectionAndAmount,
538 Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint);
541 * PrepareToDeleteAtOtherBlockBoundary() considers left content and right
542 * content which are joined for handling deletion at other block boundary
543 * (i.e., immediately before or after a block).
545 * @param aHTMLEditor The HTML editor.
546 * @param aDirectionAndAmount Direction of the deletion.
547 * @param aOtherBlockElement The block element which follows the
548 * caret or is followed by caret.
549 * @param aCaretPoint The caret point (i.e., selection start
550 * or end).
551 * @param aWSRunScannerAtCaret WSRunScanner instance which was
552 * initialized with the caret point.
553 * @return true if can continue to handle the
554 * deletion.
556 bool PrepareToDeleteAtOtherBlockBoundary(
557 const HTMLEditor& aHTMLEditor,
558 nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement,
559 const EditorDOMPoint& aCaretPoint,
560 const WSRunScanner& aWSRunScannerAtCaret);
563 * PrepareToDeleteNonCollapsedRange() considers left block element and
564 * right block element which are inclusive ancestor block element of
565 * start and end container of aRangeToDelete
567 * @param aHTMLEditor The HTML editor.
568 * @param aRangeToDelete The range to delete. Must not be
569 * collapsed.
570 * @return true if can continue to handle the
571 * deletion.
573 bool PrepareToDeleteNonCollapsedRange(const HTMLEditor& aHTMLEditor,
574 const nsRange& aRangeToDelete);
577 * Run() executes the joining.
579 * @param aHTMLEditor The HTML editor.
580 * @param aDirectionAndAmount Direction of the deletion.
581 * @param aStripWrappers Must be eStrip or eNoStrip.
582 * @param aCaretPoint The caret point (i.e., selection start
583 * or end).
584 * @param aRangeToDelete The range to delete. This should be
585 * collapsed and match with aCaretPoint.
587 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
588 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
589 nsIEditor::EStripWrappers aStripWrappers,
590 const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete,
591 const Element& aEditingHost) {
592 switch (mMode) {
593 case Mode::JoinCurrentBlock: {
594 Result<EditActionResult, nsresult> result =
595 HandleDeleteAtCurrentBlockBoundary(
596 aHTMLEditor, aDirectionAndAmount, aCaretPoint, aEditingHost);
597 NS_WARNING_ASSERTION(result.isOk(),
598 "AutoBlockElementsJoiner::"
599 "HandleDeleteAtCurrentBlockBoundary() failed");
600 return result;
602 case Mode::JoinOtherBlock: {
603 Result<EditActionResult, nsresult> result =
604 HandleDeleteAtOtherBlockBoundary(aHTMLEditor, aDirectionAndAmount,
605 aStripWrappers, aCaretPoint,
606 aRangeToDelete, aEditingHost);
607 NS_WARNING_ASSERTION(result.isOk(),
608 "AutoBlockElementsJoiner::"
609 "HandleDeleteAtOtherBlockBoundary() failed");
610 return result;
612 case Mode::DeleteBRElement: {
613 Result<EditActionResult, nsresult> result =
614 DeleteBRElement(aHTMLEditor, aDirectionAndAmount, aEditingHost);
615 NS_WARNING_ASSERTION(
616 result.isOk(),
617 "AutoBlockElementsJoiner::DeleteBRElement() failed");
618 return result;
620 case Mode::JoinBlocksInSameParent:
621 case Mode::DeleteContentInRange:
622 case Mode::DeleteNonCollapsedRange:
623 MOZ_ASSERT_UNREACHABLE(
624 "This mode should be handled in the other Run()");
625 return Err(NS_ERROR_UNEXPECTED);
626 case Mode::NotInitialized:
627 return EditActionResult::IgnoredResult();
629 return Err(NS_ERROR_NOT_INITIALIZED);
632 nsresult ComputeRangeToDelete(const HTMLEditor& aHTMLEditor,
633 nsIEditor::EDirection aDirectionAndAmount,
634 const EditorDOMPoint& aCaretPoint,
635 nsRange& aRangeToDelete,
636 const Element& aEditingHost) const {
637 switch (mMode) {
638 case Mode::JoinCurrentBlock: {
639 nsresult rv = ComputeRangeToDeleteAtCurrentBlockBoundary(
640 aHTMLEditor, aCaretPoint, aRangeToDelete, aEditingHost);
641 NS_WARNING_ASSERTION(
642 NS_SUCCEEDED(rv),
643 "AutoBlockElementsJoiner::"
644 "ComputeRangeToDeleteAtCurrentBlockBoundary() failed");
645 return rv;
647 case Mode::JoinOtherBlock: {
648 nsresult rv = ComputeRangeToDeleteAtOtherBlockBoundary(
649 aHTMLEditor, aDirectionAndAmount, aCaretPoint, aRangeToDelete,
650 aEditingHost);
651 NS_WARNING_ASSERTION(
652 NS_SUCCEEDED(rv),
653 "AutoBlockElementsJoiner::"
654 "ComputeRangeToDeleteAtOtherBlockBoundary() failed");
655 return rv;
657 case Mode::DeleteBRElement: {
658 nsresult rv = ComputeRangeToDeleteBRElement(aRangeToDelete);
659 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
660 "AutoBlockElementsJoiner::"
661 "ComputeRangeToDeleteBRElement() failed");
662 return rv;
664 case Mode::JoinBlocksInSameParent:
665 case Mode::DeleteContentInRange:
666 case Mode::DeleteNonCollapsedRange:
667 MOZ_ASSERT_UNREACHABLE(
668 "This mode should be handled in the other "
669 "ComputeRangesToDelete()");
670 return NS_ERROR_UNEXPECTED;
671 case Mode::NotInitialized:
672 return NS_OK;
674 return NS_ERROR_NOT_IMPLEMENTED;
678 * Run() executes the joining.
680 * @param aHTMLEditor The HTML editor.
681 * @param aDirectionAndAmount Direction of the deletion.
682 * @param aStripWrappers Whether delete or keep new empty
683 * ancestor elements.
684 * @param aRangeToDelete The range to delete. Must not be
685 * collapsed.
686 * @param aSelectionWasCollapsed Whether selection was or was not
687 * collapsed when starting to handle
688 * deletion.
690 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
691 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
692 nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
693 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
694 const Element& aEditingHost) {
695 switch (mMode) {
696 case Mode::JoinCurrentBlock:
697 case Mode::JoinOtherBlock:
698 case Mode::DeleteBRElement:
699 MOZ_ASSERT_UNREACHABLE(
700 "This mode should be handled in the other Run()");
701 return Err(NS_ERROR_UNEXPECTED);
702 case Mode::JoinBlocksInSameParent: {
703 Result<EditActionResult, nsresult> result =
704 JoinBlockElementsInSameParent(
705 aHTMLEditor, aDirectionAndAmount, aStripWrappers,
706 aRangeToDelete, aSelectionWasCollapsed, aEditingHost);
707 NS_WARNING_ASSERTION(result.isOk(),
708 "AutoBlockElementsJoiner::"
709 "JoinBlockElementsInSameParent() failed");
710 return result;
712 case Mode::DeleteContentInRange: {
713 Result<EditActionResult, nsresult> result = DeleteContentInRange(
714 aHTMLEditor, aDirectionAndAmount, aStripWrappers, aRangeToDelete);
715 NS_WARNING_ASSERTION(
716 result.isOk(),
717 "AutoBlockElementsJoiner::DeleteContentInRange() failed");
718 return result;
720 case Mode::DeleteNonCollapsedRange: {
721 Result<EditActionResult, nsresult> result =
722 HandleDeleteNonCollapsedRange(
723 aHTMLEditor, aDirectionAndAmount, aStripWrappers,
724 aRangeToDelete, aSelectionWasCollapsed, aEditingHost);
725 NS_WARNING_ASSERTION(result.isOk(),
726 "AutoBlockElementsJoiner::"
727 "HandleDeleteNonCollapsedRange() failed");
728 return result;
730 case Mode::NotInitialized:
731 MOZ_ASSERT_UNREACHABLE(
732 "Call Run() after calling a preparation method");
733 return EditActionResult::IgnoredResult();
735 return Err(NS_ERROR_NOT_INITIALIZED);
738 nsresult ComputeRangeToDelete(
739 const HTMLEditor& aHTMLEditor,
740 nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
741 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
742 const Element& aEditingHost) const {
743 switch (mMode) {
744 case Mode::JoinCurrentBlock:
745 case Mode::JoinOtherBlock:
746 case Mode::DeleteBRElement:
747 MOZ_ASSERT_UNREACHABLE(
748 "This mode should be handled in the other "
749 "ComputeRangesToDelete()");
750 return NS_ERROR_UNEXPECTED;
751 case Mode::JoinBlocksInSameParent: {
752 nsresult rv = ComputeRangeToJoinBlockElementsInSameParent(
753 aHTMLEditor, aDirectionAndAmount, aRangeToDelete, aEditingHost);
754 NS_WARNING_ASSERTION(
755 NS_SUCCEEDED(rv),
756 "AutoBlockElementsJoiner::"
757 "ComputeRangesToJoinBlockElementsInSameParent() failed");
758 return rv;
760 case Mode::DeleteContentInRange: {
761 nsresult rv = ComputeRangeToDeleteContentInRange(
762 aHTMLEditor, aDirectionAndAmount, aRangeToDelete, aEditingHost);
763 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
764 "AutoBlockElementsJoiner::"
765 "ComputeRangesToDeleteContentInRanges() failed");
766 return rv;
768 case Mode::DeleteNonCollapsedRange: {
769 nsresult rv = ComputeRangeToDeleteNonCollapsedRange(
770 aHTMLEditor, aDirectionAndAmount, aRangeToDelete,
771 aSelectionWasCollapsed, aEditingHost);
772 NS_WARNING_ASSERTION(
773 NS_SUCCEEDED(rv),
774 "AutoBlockElementsJoiner::"
775 "ComputeRangesToDeleteNonCollapsedRanges() failed");
776 return rv;
778 case Mode::NotInitialized:
779 MOZ_ASSERT_UNREACHABLE(
780 "Call ComputeRangesToDelete() after calling a preparation "
781 "method");
782 return NS_ERROR_NOT_INITIALIZED;
784 return NS_ERROR_NOT_INITIALIZED;
787 nsIContent* GetLeafContentInOtherBlockElement() const {
788 MOZ_ASSERT(mMode == Mode::JoinOtherBlock);
789 return mLeafContentInOtherBlock;
792 private:
793 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
794 HandleDeleteAtCurrentBlockBoundary(
795 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
796 const EditorDOMPoint& aCaretPoint, const Element& aEditingHost);
797 nsresult ComputeRangeToDeleteAtCurrentBlockBoundary(
798 const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
799 nsRange& aRangeToDelete, const Element& aEditingHost) const;
800 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
801 HandleDeleteAtOtherBlockBoundary(HTMLEditor& aHTMLEditor,
802 nsIEditor::EDirection aDirectionAndAmount,
803 nsIEditor::EStripWrappers aStripWrappers,
804 const EditorDOMPoint& aCaretPoint,
805 nsRange& aRangeToDelete,
806 const Element& aEditingHost);
807 // FYI: This method may modify selection, but it won't cause running
808 // script because of `AutoHideSelectionChanges` which blocks
809 // selection change listeners and the selection change event
810 // dispatcher.
811 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
812 ComputeRangeToDeleteAtOtherBlockBoundary(
813 const HTMLEditor& aHTMLEditor,
814 nsIEditor::EDirection aDirectionAndAmount,
815 const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete,
816 const Element& aEditingHost) const;
817 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
818 JoinBlockElementsInSameParent(
819 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
820 nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
821 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
822 const Element& aEditingHost);
823 nsresult ComputeRangeToJoinBlockElementsInSameParent(
824 const HTMLEditor& aHTMLEditor,
825 nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
826 const Element& aEditingHost) const;
827 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
828 DeleteBRElement(HTMLEditor& aHTMLEditor,
829 nsIEditor::EDirection aDirectionAndAmount,
830 const Element& aEditingHost);
831 nsresult ComputeRangeToDeleteBRElement(nsRange& aRangeToDelete) const;
832 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
833 DeleteContentInRange(HTMLEditor& aHTMLEditor,
834 nsIEditor::EDirection aDirectionAndAmount,
835 nsIEditor::EStripWrappers aStripWrappers,
836 nsRange& aRangeToDelete);
837 nsresult ComputeRangeToDeleteContentInRange(
838 const HTMLEditor& aHTMLEditor,
839 nsIEditor::EDirection aDirectionAndAmount, nsRange& aRange,
840 const Element& aEditingHost) const;
841 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
842 HandleDeleteNonCollapsedRange(
843 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
844 nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
845 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
846 const Element& aEditingHost);
847 nsresult ComputeRangeToDeleteNonCollapsedRange(
848 const HTMLEditor& aHTMLEditor,
849 nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
850 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
851 const Element& aEditingHost) const;
854 * JoinNodesDeepWithTransaction() joins aLeftNode and aRightNode "deeply".
855 * First, they are joined simply, then, new right node is assumed as the
856 * child at length of the left node before joined and new left node is
857 * assumed as its previous sibling. Then, they will be joined again.
858 * And then, these steps are repeated.
860 * @param aLeftContent The node which will be removed form the tree.
861 * @param aRightContent The node which will be inserted the contents of
862 * aRightContent.
863 * @return The point of the first child of the last right
864 * node. The result is always set if this succeeded.
866 MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
867 JoinNodesDeepWithTransaction(HTMLEditor& aHTMLEditor,
868 nsIContent& aLeftContent,
869 nsIContent& aRightContent);
872 * DeleteNodesEntirelyInRangeButKeepTableStructure() removes nodes which are
873 * entirely in aRange. Howevers, if some nodes are part of a table,
874 * removes all children of them instead. I.e., this does not make damage to
875 * table structure at the range, but may remove table entirely if it's
876 * in the range.
878 * @return true if inclusive ancestor block elements at
879 * start and end of the range should be joined.
881 MOZ_CAN_RUN_SCRIPT Result<bool, nsresult>
882 DeleteNodesEntirelyInRangeButKeepTableStructure(
883 HTMLEditor& aHTMLEditor, nsRange& aRange,
884 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed);
885 bool NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
886 const HTMLEditor& aHTMLEditor,
887 const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
888 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
889 const;
890 Result<bool, nsresult>
891 ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure(
892 const HTMLEditor& aHTMLEditor, nsRange& aRange,
893 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
894 const;
897 * DeleteContentButKeepTableStructure() removes aContent if it's an element
898 * which is part of a table structure. If it's a part of table structure,
899 * removes its all children recursively. I.e., this may delete all of a
900 * table, but won't break table structure partially.
902 * @param aContent The content which or whose all children should
903 * be removed.
905 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
906 DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor,
907 nsIContent& aContent);
910 * DeleteTextAtStartAndEndOfRange() removes text if start and/or end of
911 * aRange is in a text node.
913 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
914 DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange);
916 class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner final {
917 public:
918 AutoInclusiveAncestorBlockElementsJoiner() = delete;
919 AutoInclusiveAncestorBlockElementsJoiner(
920 nsIContent& aInclusiveDescendantOfLeftBlockElement,
921 nsIContent& aInclusiveDescendantOfRightBlockElement)
922 : mInclusiveDescendantOfLeftBlockElement(
923 aInclusiveDescendantOfLeftBlockElement),
924 mInclusiveDescendantOfRightBlockElement(
925 aInclusiveDescendantOfRightBlockElement),
926 mCanJoinBlocks(false),
927 mFallbackToDeleteLeafContent(false) {}
929 bool IsSet() const { return mLeftBlockElement && mRightBlockElement; }
930 bool IsSameBlockElement() const {
931 return mLeftBlockElement && mLeftBlockElement == mRightBlockElement;
934 const EditorDOMPoint& PointRefToPutCaret() const {
935 return mPointToPutCaret;
939 * Prepare for joining inclusive ancestor block elements. When this
940 * returns false, the deletion should be canceled.
942 Result<bool, nsresult> Prepare(const HTMLEditor& aHTMLEditor,
943 const Element& aEditingHost);
946 * When this returns true, this can join the blocks with `Run()`.
948 bool CanJoinBlocks() const { return mCanJoinBlocks; }
951 * When this returns true, `Run()` must return "ignored" so that
952 * caller can skip calling `Run()`. This is available only when
953 * `CanJoinBlocks()` returns `true`.
954 * TODO: This should be merged into `CanJoinBlocks()` in the future.
956 bool ShouldDeleteLeafContentInstead() const {
957 MOZ_ASSERT(CanJoinBlocks());
958 return mFallbackToDeleteLeafContent;
962 * ComputeRangesToDelete() extends aRangeToDelete includes the element
963 * boundaries between joining blocks. If they won't be joined, this
964 * collapses the range to aCaretPoint.
966 nsresult ComputeRangeToDelete(const HTMLEditor& aHTMLEditor,
967 const EditorDOMPoint& aCaretPoint,
968 nsRange& aRangeToDelete) const;
971 * Join inclusive ancestor block elements which are found by preceding
972 * Prepare() call.
973 * The right element is always joined to the left element.
974 * If the elements are the same type and not nested within each other,
975 * JoinEditableNodesWithTransaction() is called (example, joining two
976 * list items together into one).
977 * If the elements are not the same type, or one is a descendant of the
978 * other, we instead destroy the right block placing its children into
979 * left block.
981 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
982 HTMLEditor& aHTMLEditor, const Element& aEditingHost);
984 private:
986 * This method returns true when
987 * `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`,
988 * `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` and
989 * `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` handle it
990 * with the `if` block of their main blocks.
992 bool CanMergeLeftAndRightBlockElements() const {
993 if (!IsSet()) {
994 return false;
996 // `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`
997 if (mPointContainingTheOtherBlockElement.GetContainer() ==
998 mRightBlockElement) {
999 return mNewListElementTagNameOfRightListElement.isSome();
1001 // `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()`
1002 if (mPointContainingTheOtherBlockElement.GetContainer() ==
1003 mLeftBlockElement) {
1004 return mNewListElementTagNameOfRightListElement.isSome() &&
1005 !mRightBlockElement->GetChildCount();
1007 MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet());
1008 // `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()`
1009 return mNewListElementTagNameOfRightListElement.isSome() ||
1010 mLeftBlockElement->NodeInfo()->NameAtom() ==
1011 mRightBlockElement->NodeInfo()->NameAtom();
1014 OwningNonNull<nsIContent> mInclusiveDescendantOfLeftBlockElement;
1015 OwningNonNull<nsIContent> mInclusiveDescendantOfRightBlockElement;
1016 RefPtr<Element> mLeftBlockElement;
1017 RefPtr<Element> mRightBlockElement;
1018 Maybe<nsAtom*> mNewListElementTagNameOfRightListElement;
1019 EditorDOMPoint mPointContainingTheOtherBlockElement;
1020 EditorDOMPoint mPointToPutCaret;
1021 RefPtr<dom::HTMLBRElement> mPrecedingInvisibleBRElement;
1022 bool mCanJoinBlocks;
1023 bool mFallbackToDeleteLeafContent;
1024 }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
1025 // AutoInclusiveAncestorBlockElementsJoiner
1027 enum class Mode {
1028 NotInitialized,
1029 JoinCurrentBlock,
1030 JoinOtherBlock,
1031 JoinBlocksInSameParent,
1032 DeleteBRElement,
1033 DeleteContentInRange,
1034 DeleteNonCollapsedRange,
1036 AutoDeleteRangesHandler* mDeleteRangesHandler;
1037 const AutoDeleteRangesHandler& mDeleteRangesHandlerConst;
1038 nsCOMPtr<nsIContent> mLeftContent;
1039 nsCOMPtr<nsIContent> mRightContent;
1040 nsCOMPtr<nsIContent> mLeafContentInOtherBlock;
1041 // mSkippedInvisibleContents stores all content nodes which are skipped at
1042 // scanning mLeftContent and mRightContent. The content nodes should be
1043 // removed at deletion.
1044 AutoTArray<OwningNonNull<nsIContent>, 8> mSkippedInvisibleContents;
1045 RefPtr<dom::HTMLBRElement> mBRElement;
1046 Mode mMode = Mode::NotInitialized;
1047 }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner
1049 class MOZ_STACK_CLASS AutoEmptyBlockAncestorDeleter final {
1050 public:
1052 * ScanEmptyBlockInclusiveAncestor() scans an inclusive ancestor element
1053 * which is empty and a block element. Then, stores the result and
1054 * returns the found empty block element.
1056 * @param aHTMLEditor The HTMLEditor.
1057 * @param aStartContent Start content to look for empty ancestors.
1059 [[nodiscard]] Element* ScanEmptyBlockInclusiveAncestor(
1060 const HTMLEditor& aHTMLEditor, nsIContent& aStartContent);
1063 * ComputeTargetRanges() computes "target ranges" for deleting
1064 * `mEmptyInclusiveAncestorBlockElement`.
1066 nsresult ComputeTargetRanges(const HTMLEditor& aHTMLEditor,
1067 nsIEditor::EDirection aDirectionAndAmount,
1068 const Element& aEditingHost,
1069 AutoRangeArray& aRangesToDelete) const;
1072 * Deletes found empty block element by `ScanEmptyBlockInclusiveAncestor()`.
1073 * If found one is a list item element, calls
1074 * `MaybeInsertBRElementBeforeEmptyListItemElement()` before deleting
1075 * the list item element.
1076 * If found empty ancestor is not a list item element,
1077 * `GetNewCaretPosition()` will be called to determine new caret position.
1078 * Finally, removes the empty block ancestor.
1080 * @param aHTMLEditor The HTMLEditor.
1081 * @param aDirectionAndAmount If found empty ancestor block is a list item
1082 * element, this is ignored. Otherwise:
1083 * - If eNext, eNextWord or eToEndOfLine,
1084 * collapse Selection to after found empty
1085 * ancestor.
1086 * - If ePrevious, ePreviousWord or
1087 * eToBeginningOfLine, collapse Selection to
1088 * end of previous editable node.
1089 * - Otherwise, eNone is allowed but does
1090 * nothing.
1092 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
1093 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount);
1095 private:
1097 * MaybeReplaceSubListWithNewListItem() replaces
1098 * mEmptyInclusiveAncestorBlockElement with new list item element
1099 * (containing <br>) if:
1100 * - mEmptyInclusiveAncestorBlockElement is a list element
1101 * - The parent of mEmptyInclusiveAncestorBlockElement is a list element
1102 * - The parent becomes empty after deletion
1103 * If this does not perform the replacement, returns "ignored".
1105 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
1106 MaybeReplaceSubListWithNewListItem(HTMLEditor& aHTMLEditor);
1109 * MaybeInsertBRElementBeforeEmptyListItemElement() inserts a `<br>` element
1110 * if `mEmptyInclusiveAncestorBlockElement` is a list item element which
1111 * is first editable element in its parent, and its grand parent is not a
1112 * list element, inserts a `<br>` element before the empty list item.
1114 [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
1115 MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor);
1118 * GetNewCaretPosition() returns new caret position after deleting
1119 * `mEmptyInclusiveAncestorBlockElement`.
1121 [[nodiscard]] Result<CaretPoint, nsresult> GetNewCaretPosition(
1122 const HTMLEditor& aHTMLEditor,
1123 nsIEditor::EDirection aDirectionAndAmount) const;
1125 RefPtr<Element> mEmptyInclusiveAncestorBlockElement;
1126 }; // HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter
1128 const AutoDeleteRangesHandler* const mParent;
1129 nsIEditor::EDirection mOriginalDirectionAndAmount;
1130 nsIEditor::EStripWrappers mOriginalStripWrappers;
1131 }; // HTMLEditor::AutoDeleteRangesHandler
1133 nsresult HTMLEditor::ComputeTargetRanges(
1134 nsIEditor::EDirection aDirectionAndAmount,
1135 AutoRangeArray& aRangesToDelete) const {
1136 MOZ_ASSERT(IsEditActionDataAvailable());
1138 Element* editingHost = ComputeEditingHost();
1139 if (!editingHost) {
1140 aRangesToDelete.RemoveAllRanges();
1141 return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
1144 // First check for table selection mode. If so, hand off to table editor.
1145 SelectedTableCellScanner scanner(aRangesToDelete);
1146 if (scanner.IsInTableCellSelectionMode()) {
1147 // If it's in table cell selection mode, we'll delete all childen in
1148 // the all selected table cell elements,
1149 if (scanner.ElementsRef().Length() == aRangesToDelete.Ranges().Length()) {
1150 return NS_OK;
1152 // but will ignore all ranges which does not select a table cell.
1153 size_t removedRanges = 0;
1154 for (size_t i = 1; i < scanner.ElementsRef().Length(); i++) {
1155 if (HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(
1156 aRangesToDelete.Ranges()[i - removedRanges]) !=
1157 scanner.ElementsRef()[i]) {
1158 // XXX Need to manage anchor-focus range too!
1159 aRangesToDelete.Ranges().RemoveElementAt(i - removedRanges);
1160 removedRanges++;
1163 return NS_OK;
1166 aRangesToDelete.EnsureOnlyEditableRanges(*editingHost);
1167 if (aRangesToDelete.Ranges().IsEmpty()) {
1168 NS_WARNING(
1169 "There is no range which we can delete entire of or around the caret");
1170 return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
1172 AutoDeleteRangesHandler deleteHandler;
1173 // Should we delete target ranges which cannot delete actually?
1174 nsresult rv = deleteHandler.ComputeRangesToDelete(
1175 *this, aDirectionAndAmount, aRangesToDelete, *editingHost);
1176 NS_WARNING_ASSERTION(
1177 NS_SUCCEEDED(rv),
1178 "AutoDeleteRangesHandler::ComputeRangesToDelete() failed");
1179 return rv;
1182 Result<EditActionResult, nsresult> HTMLEditor::HandleDeleteSelection(
1183 nsIEditor::EDirection aDirectionAndAmount,
1184 nsIEditor::EStripWrappers aStripWrappers) {
1185 MOZ_ASSERT(IsEditActionDataAvailable());
1186 MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
1187 aStripWrappers == nsIEditor::eNoStrip);
1189 if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) {
1190 return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
1193 RefPtr<Element> editingHost = ComputeEditingHost();
1194 if (MOZ_UNLIKELY(!editingHost)) {
1195 return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
1198 // Remember that we did a selection deletion. Used by
1199 // CreateStyleForInsertText()
1200 TopLevelEditSubActionDataRef().mDidDeleteSelection = true;
1202 if (MOZ_UNLIKELY(IsEmpty())) {
1203 return EditActionResult::CanceledResult();
1206 // First check for table selection mode. If so, hand off to table editor.
1207 if (HTMLEditUtils::IsInTableCellSelectionMode(SelectionRef())) {
1208 nsresult rv = DeleteTableCellContentsWithTransaction();
1209 if (NS_WARN_IF(Destroyed())) {
1210 return Err(NS_ERROR_EDITOR_DESTROYED);
1212 if (NS_FAILED(rv)) {
1213 NS_WARNING("HTMLEditor::DeleteTableCellContentsWithTransaction() failed");
1214 return Err(rv);
1216 return EditActionResult::HandledResult();
1219 AutoRangeArray rangesToDelete(SelectionRef());
1220 rangesToDelete.EnsureOnlyEditableRanges(*editingHost);
1221 if (MOZ_UNLIKELY(rangesToDelete.Ranges().IsEmpty())) {
1222 NS_WARNING(
1223 "There is no range which we can delete entire the ranges or around the "
1224 "caret");
1225 return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
1227 AutoDeleteRangesHandler deleteHandler;
1228 Result<EditActionResult, nsresult> result = deleteHandler.Run(
1229 *this, aDirectionAndAmount, aStripWrappers, rangesToDelete, *editingHost);
1230 if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Canceled()) {
1231 NS_WARNING_ASSERTION(result.isOk(),
1232 "AutoDeleteRangesHandler::Run() failed");
1233 return result;
1236 // XXX At here, selection may have no range because of mutation event
1237 // listeners can do anything so that we should just return NS_OK instead
1238 // of returning error.
1239 const auto atNewStartOfSelection =
1240 GetFirstSelectionStartPoint<EditorDOMPoint>();
1241 if (NS_WARN_IF(!atNewStartOfSelection.IsSet())) {
1242 return Err(NS_ERROR_FAILURE);
1244 if (atNewStartOfSelection.IsInContentNode()) {
1245 nsresult rv = DeleteMostAncestorMailCiteElementIfEmpty(
1246 MOZ_KnownLive(*atNewStartOfSelection.ContainerAs<nsIContent>()));
1247 if (NS_FAILED(rv)) {
1248 NS_WARNING(
1249 "HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty() failed");
1250 return Err(rv);
1253 return EditActionResult::HandledResult();
1256 nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete(
1257 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
1258 AutoRangeArray& aRangesToDelete, const Element& aEditingHost) {
1259 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
1260 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
1262 mOriginalDirectionAndAmount = aDirectionAndAmount;
1263 mOriginalStripWrappers = nsIEditor::eNoStrip;
1265 if (aHTMLEditor.mPaddingBRElementForEmptyEditor) {
1266 nsresult rv = aRangesToDelete.Collapse(
1267 EditorRawDOMPoint(aHTMLEditor.mPaddingBRElementForEmptyEditor));
1268 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
1269 return rv;
1272 SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed()
1273 ? SelectionWasCollapsed::Yes
1274 : SelectionWasCollapsed::No;
1275 if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
1276 const auto startPoint =
1277 aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
1278 if (NS_WARN_IF(!startPoint.IsSet())) {
1279 return NS_ERROR_FAILURE;
1281 RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost();
1282 if (NS_WARN_IF(!editingHost)) {
1283 return NS_ERROR_FAILURE;
1285 if (startPoint.IsInContentNode()) {
1286 AutoEmptyBlockAncestorDeleter deleter;
1287 if (deleter.ScanEmptyBlockInclusiveAncestor(
1288 aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) {
1289 nsresult rv = deleter.ComputeTargetRanges(
1290 aHTMLEditor, aDirectionAndAmount, *editingHost, aRangesToDelete);
1291 NS_WARNING_ASSERTION(
1292 NS_SUCCEEDED(rv),
1293 "AutoEmptyBlockAncestorDeleter::ComputeTargetRanges() failed");
1294 return rv;
1298 // We shouldn't update caret bidi level right now, but we need to check
1299 // whether the deletion will be canceled or not.
1300 AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount,
1301 startPoint);
1302 if (bidiLevelManager.Failed()) {
1303 NS_WARNING(
1304 "EditorBase::AutoCaretBidiLevelManager failed to initialize itself");
1305 return NS_ERROR_FAILURE;
1307 if (bidiLevelManager.Canceled()) {
1308 return NS_SUCCESS_DOM_NO_OPERATION;
1311 // AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection`
1312 // to extend the range for deletion. But if focus event doesn't receive
1313 // yet, ancestor isn't set. So we must set root element of editor to
1314 // ancestor temporarily.
1315 AutoSetTemporaryAncestorLimiter autoSetter(
1316 aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(),
1317 &aRangesToDelete);
1319 Result<nsIEditor::EDirection, nsresult> extendResult =
1320 aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor,
1321 aDirectionAndAmount);
1322 if (extendResult.isErr()) {
1323 NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
1324 return extendResult.unwrapErr();
1327 // For compatibility with other browsers, we should set target ranges
1328 // to start from and/or end after an atomic content rather than start
1329 // from preceding text node end nor end at following text node start.
1330 Result<bool, nsresult> shrunkenResult =
1331 aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
1332 aHTMLEditor, aDirectionAndAmount,
1333 AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
1334 editingHost);
1335 if (shrunkenResult.isErr()) {
1336 NS_WARNING(
1337 "AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() "
1338 "failed");
1339 return shrunkenResult.unwrapErr();
1342 if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) {
1343 aDirectionAndAmount = extendResult.unwrap();
1346 if (aDirectionAndAmount == nsIEditor::eNone) {
1347 MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
1348 if (!CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)) {
1349 // XXX In this case, do we need to modify the range again?
1350 return NS_SUCCESS_DOM_NO_OPERATION;
1352 nsresult rv = FallbackToComputeRangesToDeleteRangesWithTransaction(
1353 aHTMLEditor, aRangesToDelete, *editingHost);
1354 NS_WARNING_ASSERTION(
1355 NS_SUCCEEDED(rv),
1356 "AutoDeleteRangesHandler::"
1357 "FallbackToComputeRangesToDeleteRangesWithTransaction() failed");
1358 return rv;
1361 if (aRangesToDelete.IsCollapsed()) {
1362 const auto caretPoint =
1363 aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
1364 if (MOZ_UNLIKELY(NS_WARN_IF(!caretPoint.IsInContentNode()))) {
1365 return NS_ERROR_FAILURE;
1367 if (!EditorUtils::IsEditableContent(*caretPoint.ContainerAs<nsIContent>(),
1368 EditorType::HTML)) {
1369 return NS_SUCCESS_DOM_NO_OPERATION;
1371 WSRunScanner wsRunScannerAtCaret(
1372 editingHost, caretPoint,
1373 BlockInlineCheck::UseComputedDisplayOutsideStyle);
1374 WSScanResult scanFromCaretPointResult =
1375 aDirectionAndAmount == nsIEditor::eNext
1376 ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
1377 caretPoint)
1378 : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
1379 caretPoint);
1380 if (scanFromCaretPointResult.Failed()) {
1381 NS_WARNING(
1382 "WSRunScanner::Scan(Next|Previous)VisibleNodeOrBlockBoundaryFrom() "
1383 "failed");
1384 return NS_ERROR_FAILURE;
1386 if (!scanFromCaretPointResult.GetContent()) {
1387 return NS_SUCCESS_DOM_NO_OPERATION;
1390 if (scanFromCaretPointResult.ReachedBRElement()) {
1391 if (scanFromCaretPointResult.BRElementPtr() ==
1392 wsRunScannerAtCaret.GetEditingHost()) {
1393 return NS_OK;
1395 if (!EditorUtils::IsEditableContent(
1396 *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) {
1397 return NS_SUCCESS_DOM_NO_OPERATION;
1399 if (HTMLEditUtils::IsInvisibleBRElement(
1400 *scanFromCaretPointResult.BRElementPtr())) {
1401 EditorDOMPoint newCaretPosition =
1402 aDirectionAndAmount == nsIEditor::eNext
1403 ? EditorDOMPoint::After(
1404 *scanFromCaretPointResult.BRElementPtr())
1405 : EditorDOMPoint(scanFromCaretPointResult.BRElementPtr());
1406 if (NS_WARN_IF(!newCaretPosition.IsSet())) {
1407 return NS_ERROR_FAILURE;
1409 AutoHideSelectionChanges blockSelectionListeners(
1410 aHTMLEditor.SelectionRef());
1411 nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition);
1412 if (MOZ_UNLIKELY(NS_FAILED(rv))) {
1413 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
1414 return NS_ERROR_FAILURE;
1416 if (NS_WARN_IF(!aHTMLEditor.SelectionRef().RangeCount())) {
1417 return NS_ERROR_UNEXPECTED;
1419 aRangesToDelete.Initialize(aHTMLEditor.SelectionRef());
1420 AutoDeleteRangesHandler anotherHandler(this);
1421 rv = anotherHandler.ComputeRangesToDelete(
1422 aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost);
1423 NS_WARNING_ASSERTION(
1424 NS_SUCCEEDED(rv),
1425 "Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() "
1426 "failed");
1428 rv = aHTMLEditor.CollapseSelectionTo(caretPoint);
1429 if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
1430 NS_WARNING(
1431 "EditorBase::CollapseSelectionTo() caused destroying the "
1432 "editor");
1433 return NS_ERROR_EDITOR_DESTROYED;
1435 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
1436 "EditorBase::CollapseSelectionTo() failed to "
1437 "restore original selection, but ignored");
1439 MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
1440 // If the range is collapsed, there is no content which should
1441 // be removed together. In this case, only the invisible `<br>`
1442 // element should be selected.
1443 if (aRangesToDelete.IsCollapsed()) {
1444 nsresult rv = aRangesToDelete.SelectNode(
1445 *scanFromCaretPointResult.BRElementPtr());
1446 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
1447 "AutoRangeArray::SelectNode() failed");
1448 return rv;
1451 // Otherwise, extend the range to contain the invisible `<br>`
1452 // element.
1453 if (EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr())
1454 .IsBefore(
1455 aRangesToDelete
1456 .GetFirstRangeStartPoint<EditorRawDOMPoint>())) {
1457 nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
1458 EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr())
1459 .ToRawRangeBoundary(),
1460 aRangesToDelete.FirstRangeRef()->EndRef());
1461 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
1462 "nsRange::SetStartAndEnd() failed");
1463 return rv;
1465 if (aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>()
1466 .IsBefore(EditorRawDOMPoint::After(
1467 *scanFromCaretPointResult.BRElementPtr()))) {
1468 nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
1469 aRangesToDelete.FirstRangeRef()->StartRef(),
1470 EditorRawDOMPoint::After(
1471 *scanFromCaretPointResult.BRElementPtr())
1472 .ToRawRangeBoundary());
1473 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
1474 "nsRange::SetStartAndEnd() failed");
1475 return rv;
1477 NS_WARNING("Was the invisible `<br>` element selected?");
1478 return NS_OK;
1482 nsresult rv = ComputeRangesToDeleteAroundCollapsedRanges(
1483 aHTMLEditor, aDirectionAndAmount, aRangesToDelete,
1484 wsRunScannerAtCaret, scanFromCaretPointResult, aEditingHost);
1485 NS_WARNING_ASSERTION(
1486 NS_SUCCEEDED(rv),
1487 "AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges("
1488 ") failed");
1489 return rv;
1493 nsresult rv = ComputeRangesToDeleteNonCollapsedRanges(
1494 aHTMLEditor, aDirectionAndAmount, aRangesToDelete, selectionWasCollapsed,
1495 aEditingHost);
1496 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
1497 "AutoDeleteRangesHandler::"
1498 "ComputeRangesToDeleteNonCollapsedRanges() failed");
1499 return rv;
1502 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run(
1503 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
1504 nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
1505 const Element& aEditingHost) {
1506 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
1507 MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
1508 aStripWrappers == nsIEditor::eNoStrip);
1509 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
1511 mOriginalDirectionAndAmount = aDirectionAndAmount;
1512 mOriginalStripWrappers = aStripWrappers;
1514 if (MOZ_UNLIKELY(aHTMLEditor.IsEmpty())) {
1515 return EditActionResult::CanceledResult();
1518 // selectionWasCollapsed is used later to determine whether we should join
1519 // blocks in HandleDeleteNonCollapsedRanges(). We don't really care about
1520 // collapsed because it will be modified by
1521 // AutoRangeArray::ExtendAnchorFocusRangeFor() later.
1522 // AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner should
1523 // happen if the original selection is collapsed and the cursor is at the end
1524 // of a block element, in which case
1525 // AutoRangeArray::ExtendAnchorFocusRangeFor() would always make the selection
1526 // not collapsed.
1527 SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed()
1528 ? SelectionWasCollapsed::Yes
1529 : SelectionWasCollapsed::No;
1531 if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
1532 const auto startPoint =
1533 aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
1534 if (NS_WARN_IF(!startPoint.IsSet())) {
1535 return Err(NS_ERROR_FAILURE);
1538 // If we are inside an empty block, delete it.
1539 if (startPoint.IsInContentNode()) {
1540 #ifdef DEBUG
1541 nsMutationGuard debugMutation;
1542 #endif // #ifdef DEBUG
1543 AutoEmptyBlockAncestorDeleter deleter;
1544 if (deleter.ScanEmptyBlockInclusiveAncestor(
1545 aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) {
1546 Result<EditActionResult, nsresult> result =
1547 deleter.Run(aHTMLEditor, aDirectionAndAmount);
1548 if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Handled()) {
1549 NS_WARNING_ASSERTION(result.isOk(),
1550 "AutoEmptyBlockAncestorDeleter::Run() failed");
1551 return result;
1554 MOZ_ASSERT(!debugMutation.Mutated(0),
1555 "AutoEmptyBlockAncestorDeleter shouldn't modify the DOM tree "
1556 "if it returns not handled nor error");
1559 // Test for distance between caret and text that will be deleted.
1560 // Note that this call modifies `nsFrameSelection` without modifying
1561 // `Selection`. However, it does not have problem for now because
1562 // it'll be referred by `AutoRangeArray::ExtendAnchorFocusRangeFor()`
1563 // before modifying `Selection`.
1564 // XXX This looks odd. `ExtendAnchorFocusRangeFor()` will extend
1565 // anchor-focus range, but here refers the first range.
1566 AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount,
1567 startPoint);
1568 if (MOZ_UNLIKELY(bidiLevelManager.Failed())) {
1569 NS_WARNING(
1570 "EditorBase::AutoCaretBidiLevelManager failed to initialize itself");
1571 return Err(NS_ERROR_FAILURE);
1573 bidiLevelManager.MaybeUpdateCaretBidiLevel(aHTMLEditor);
1574 if (bidiLevelManager.Canceled()) {
1575 return EditActionResult::CanceledResult();
1578 // AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection`
1579 // to extend the range for deletion. But if focus event doesn't receive
1580 // yet, ancestor isn't set. So we must set root element of editor to
1581 // ancestor temporarily.
1582 AutoSetTemporaryAncestorLimiter autoSetter(
1583 aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(),
1584 &aRangesToDelete);
1586 // Calling `ExtendAnchorFocusRangeFor()` and
1587 // `ShrinkRangesIfStartFromOrEndAfterAtomicContent()` may move caret to
1588 // the container of deleting atomic content. However, it may be different
1589 // from the original caret's container. The original caret container may
1590 // be important to put caret after deletion so that let's cache the
1591 // original position.
1592 Maybe<EditorDOMPoint> caretPoint;
1593 if (aRangesToDelete.IsCollapsed() && !aRangesToDelete.Ranges().IsEmpty()) {
1594 caretPoint =
1595 Some(aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>());
1596 if (NS_WARN_IF(!caretPoint.ref().IsInContentNode())) {
1597 return Err(NS_ERROR_FAILURE);
1601 Result<nsIEditor::EDirection, nsresult> extendResult =
1602 aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor,
1603 aDirectionAndAmount);
1604 if (MOZ_UNLIKELY(extendResult.isErr())) {
1605 NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
1606 return extendResult.propagateErr();
1608 if (caretPoint.isSome() &&
1609 MOZ_UNLIKELY(!caretPoint.ref().IsSetAndValid())) {
1610 NS_WARNING("The caret position became invalid");
1611 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
1614 // If there is only one range and it selects an atomic content, we should
1615 // delete it with collapsed range path for making consistent behavior
1616 // between both cases, the content is selected case and caret is at it or
1617 // after it case.
1618 Result<bool, nsresult> shrunkenResult =
1619 aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
1620 aHTMLEditor, aDirectionAndAmount,
1621 AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
1622 &aEditingHost);
1623 if (MOZ_UNLIKELY(shrunkenResult.isErr())) {
1624 NS_WARNING(
1625 "AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() "
1626 "failed");
1627 return shrunkenResult.propagateErr();
1630 if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) {
1631 aDirectionAndAmount = extendResult.unwrap();
1634 if (aDirectionAndAmount == nsIEditor::eNone) {
1635 MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
1636 if (!CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)) {
1637 return EditActionResult::IgnoredResult();
1639 Result<CaretPoint, nsresult> caretPointOrError =
1640 FallbackToDeleteRangesWithTransaction(aHTMLEditor, aRangesToDelete);
1641 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
1642 NS_WARNING(
1643 "AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() "
1644 "failed");
1646 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
1647 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
1648 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
1649 SuggestCaret::AndIgnoreTrivialError});
1650 if (NS_FAILED(rv)) {
1651 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
1652 return Err(rv);
1654 NS_WARNING_ASSERTION(
1655 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
1656 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
1657 // Don't return "ignored" to avoid to fall it back to delete ranges
1658 // recursively.
1659 return EditActionResult::HandledResult();
1662 if (aRangesToDelete.IsCollapsed()) {
1663 // Use the original caret position for handling the deletion around
1664 // collapsed range because the container may be different from the
1665 // new collapsed position's container.
1666 if (!EditorUtils::IsEditableContent(
1667 *caretPoint.ref().ContainerAs<nsIContent>(), EditorType::HTML)) {
1668 return EditActionResult::CanceledResult();
1670 WSRunScanner wsRunScannerAtCaret(
1671 &aEditingHost, caretPoint.ref(),
1672 BlockInlineCheck::UseComputedDisplayOutsideStyle);
1673 WSScanResult scanFromCaretPointResult =
1674 aDirectionAndAmount == nsIEditor::eNext
1675 ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
1676 caretPoint.ref())
1677 : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
1678 caretPoint.ref());
1679 if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) {
1680 NS_WARNING(
1681 "WSRunScanner::Scan(Next|Previous)VisibleNodeOrBlockBoundaryFrom() "
1682 "failed");
1683 return Err(NS_ERROR_FAILURE);
1685 if (!scanFromCaretPointResult.GetContent()) {
1686 return EditActionResult::CanceledResult();
1688 // Short circuit for invisible breaks. delete them and recurse.
1689 if (scanFromCaretPointResult.ReachedBRElement()) {
1690 if (scanFromCaretPointResult.BRElementPtr() == &aEditingHost) {
1691 return EditActionResult::HandledResult();
1693 if (!EditorUtils::IsEditableContent(
1694 *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) {
1695 return EditActionResult::CanceledResult();
1697 if (HTMLEditUtils::IsInvisibleBRElement(
1698 *scanFromCaretPointResult.BRElementPtr())) {
1699 // TODO: We should extend the range to delete again before/after
1700 // the caret point and use `HandleDeleteNonCollapsedRanges()`
1701 // instead after we would create delete range computation
1702 // method at switching to the new white-space normalizer.
1703 Result<CaretPoint, nsresult> caretPointOrError =
1704 WhiteSpaceVisibilityKeeper::
1705 DeleteContentNodeAndJoinTextNodesAroundIt(
1706 aHTMLEditor,
1707 MOZ_KnownLive(*scanFromCaretPointResult.BRElementPtr()),
1708 caretPoint.ref(), aEditingHost);
1709 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
1710 NS_WARNING(
1711 "WhiteSpaceVisibilityKeeper::"
1712 "DeleteContentNodeAndJoinTextNodesAroundIt() failed");
1713 return caretPointOrError.propagateErr();
1715 if (caretPointOrError.inspect().HasCaretPointSuggestion()) {
1716 caretPoint = Some(caretPointOrError.unwrap().UnwrapCaretPoint());
1718 if (NS_WARN_IF(!caretPoint->IsSetAndValid())) {
1719 return Err(NS_ERROR_FAILURE);
1721 AutoRangeArray rangesToDelete(caretPoint.ref());
1722 if (aHTMLEditor.MayHaveMutationEventListeners(
1723 NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED |
1724 NS_EVENT_BITS_MUTATION_NODEREMOVED |
1725 NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT)) {
1726 // Let's check whether there is new invisible `<br>` element
1727 // for avoiding infinite recursive calls.
1728 WSRunScanner wsRunScannerAtCaret(
1729 &aEditingHost, caretPoint.ref(),
1730 BlockInlineCheck::UseComputedDisplayOutsideStyle);
1731 WSScanResult scanFromCaretPointResult =
1732 aDirectionAndAmount == nsIEditor::eNext
1733 ? wsRunScannerAtCaret
1734 .ScanNextVisibleNodeOrBlockBoundaryFrom(
1735 caretPoint.ref())
1736 : wsRunScannerAtCaret
1737 .ScanPreviousVisibleNodeOrBlockBoundaryFrom(
1738 caretPoint.ref());
1739 if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) {
1740 NS_WARNING(
1741 "WSRunScanner::Scan(Next|Previous)"
1742 "VisibleNodeOrBlockBoundaryFrom() failed");
1743 return Err(NS_ERROR_FAILURE);
1745 if (MOZ_UNLIKELY(
1746 scanFromCaretPointResult.ReachedInvisibleBRElement())) {
1747 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
1750 AutoDeleteRangesHandler anotherHandler(this);
1751 Result<EditActionResult, nsresult> result =
1752 anotherHandler.Run(aHTMLEditor, aDirectionAndAmount,
1753 aStripWrappers, rangesToDelete, aEditingHost);
1754 NS_WARNING_ASSERTION(
1755 result.isOk(), "Recursive AutoDeleteRangesHandler::Run() failed");
1756 return result;
1760 Result<EditActionResult, nsresult> result =
1761 HandleDeleteAroundCollapsedRanges(
1762 aHTMLEditor, aDirectionAndAmount, aStripWrappers, aRangesToDelete,
1763 wsRunScannerAtCaret, scanFromCaretPointResult, aEditingHost);
1764 NS_WARNING_ASSERTION(result.isOk(),
1765 "AutoDeleteRangesHandler::"
1766 "HandleDeleteAroundCollapsedRanges() failed");
1767 return result;
1771 Result<EditActionResult, nsresult> result = HandleDeleteNonCollapsedRanges(
1772 aHTMLEditor, aDirectionAndAmount, aStripWrappers, aRangesToDelete,
1773 selectionWasCollapsed, aEditingHost);
1774 NS_WARNING_ASSERTION(
1775 result.isOk(),
1776 "AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges() failed");
1777 return result;
1780 nsresult
1781 HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges(
1782 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
1783 AutoRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret,
1784 const WSScanResult& aScanFromCaretPointResult,
1785 const Element& aEditingHost) const {
1786 if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() ||
1787 aScanFromCaretPointResult.InNonCollapsibleCharacters() ||
1788 aScanFromCaretPointResult.ReachedPreformattedLineBreak()) {
1789 nsresult rv = aRangesToDelete.Collapse(
1790 aScanFromCaretPointResult.Point<EditorRawDOMPoint>());
1791 if (MOZ_UNLIKELY(NS_FAILED(rv))) {
1792 NS_WARNING("AutoRangeArray::Collapse() failed");
1793 return NS_ERROR_FAILURE;
1795 rv = ComputeRangesToDeleteTextAroundCollapsedRanges(
1796 aDirectionAndAmount, aRangesToDelete, aEditingHost);
1797 NS_WARNING_ASSERTION(
1798 NS_SUCCEEDED(rv),
1799 "AutoDeleteRangesHandler::"
1800 "ComputeRangesToDeleteTextAroundCollapsedRanges() failed");
1801 return rv;
1804 if (aScanFromCaretPointResult.ReachedSpecialContent() ||
1805 aScanFromCaretPointResult.ReachedBRElement() ||
1806 aScanFromCaretPointResult.ReachedHRElement() ||
1807 aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) {
1808 if (aScanFromCaretPointResult.GetContent() ==
1809 aWSRunScannerAtCaret.GetEditingHost()) {
1810 return NS_OK;
1812 nsIContent* atomicContent = GetAtomicContentToDelete(
1813 aDirectionAndAmount, aWSRunScannerAtCaret, aScanFromCaretPointResult);
1814 if (!HTMLEditUtils::IsRemovableNode(*atomicContent)) {
1815 NS_WARNING(
1816 "AutoDeleteRangesHandler::GetAtomicContentToDelete() cannot find "
1817 "removable atomic content");
1818 return NS_ERROR_FAILURE;
1820 nsresult rv = ComputeRangesToDeleteAtomicContent(
1821 aWSRunScannerAtCaret.GetEditingHost(), *atomicContent, aRangesToDelete);
1822 NS_WARNING_ASSERTION(
1823 NS_SUCCEEDED(rv),
1824 "AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent() failed");
1825 return rv;
1828 if (aScanFromCaretPointResult.ReachedOtherBlockElement()) {
1829 if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
1830 return NS_ERROR_FAILURE;
1832 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
1833 bool handled = false;
1834 for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
1835 MOZ_ASSERT(range->IsPositioned());
1836 AutoBlockElementsJoiner joiner(*this);
1837 if (!joiner.PrepareToDeleteAtOtherBlockBoundary(
1838 aHTMLEditor, aDirectionAndAmount,
1839 *aScanFromCaretPointResult.ElementPtr(),
1840 aWSRunScannerAtCaret.ScanStartRef(), aWSRunScannerAtCaret)) {
1841 continue;
1843 handled = true;
1844 nsresult rv = joiner.ComputeRangeToDelete(
1845 aHTMLEditor, aDirectionAndAmount, aWSRunScannerAtCaret.ScanStartRef(),
1846 range, aEditingHost);
1847 if (NS_FAILED(rv)) {
1848 NS_WARNING(
1849 "AutoBlockElementsJoiner::ComputeRangeToDelete() failed (other "
1850 "block boundary)");
1851 return rv;
1854 return handled ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION;
1857 if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) {
1858 if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
1859 return NS_ERROR_FAILURE;
1861 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
1862 bool handled = false;
1863 for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
1864 AutoBlockElementsJoiner joiner(*this);
1865 if (!joiner.PrepareToDeleteAtCurrentBlockBoundary(
1866 aHTMLEditor, aDirectionAndAmount,
1867 *aScanFromCaretPointResult.ElementPtr(),
1868 aWSRunScannerAtCaret.ScanStartRef())) {
1869 continue;
1871 handled = true;
1872 nsresult rv = joiner.ComputeRangeToDelete(
1873 aHTMLEditor, aDirectionAndAmount, aWSRunScannerAtCaret.ScanStartRef(),
1874 range, aEditingHost);
1875 if (NS_FAILED(rv)) {
1876 NS_WARNING(
1877 "AutoBlockElementsJoiner::ComputeRangeToDelete() failed (current "
1878 "block boundary)");
1879 return rv;
1882 return handled ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION;
1885 return NS_OK;
1888 Result<EditActionResult, nsresult>
1889 HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges(
1890 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
1891 nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
1892 const WSRunScanner& aWSRunScannerAtCaret,
1893 const WSScanResult& aScanFromCaretPointResult,
1894 const Element& aEditingHost) {
1895 MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
1896 MOZ_ASSERT(aRangesToDelete.IsCollapsed());
1897 MOZ_ASSERT(aDirectionAndAmount != nsIEditor::eNone);
1898 MOZ_ASSERT(aWSRunScannerAtCaret.ScanStartRef().IsInContentNode());
1899 MOZ_ASSERT(EditorUtils::IsEditableContent(
1900 *aWSRunScannerAtCaret.ScanStartRef().ContainerAs<nsIContent>(),
1901 EditorType::HTML));
1903 if (StaticPrefs::editor_white_space_normalization_blink_compatible()) {
1904 if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() ||
1905 aScanFromCaretPointResult.InNonCollapsibleCharacters() ||
1906 aScanFromCaretPointResult.ReachedPreformattedLineBreak()) {
1907 nsresult rv = aRangesToDelete.Collapse(
1908 aScanFromCaretPointResult.Point<EditorRawDOMPoint>());
1909 if (NS_FAILED(rv)) {
1910 NS_WARNING("AutoRangeArray::Collapse() failed");
1911 return Err(NS_ERROR_FAILURE);
1913 Result<CaretPoint, nsresult> caretPointOrError =
1914 HandleDeleteTextAroundCollapsedRanges(
1915 aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost);
1916 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
1917 NS_WARNING(
1918 "AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges() "
1919 "failed");
1920 return caretPointOrError.propagateErr();
1922 rv = caretPointOrError.unwrap().SuggestCaretPointTo(
1923 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
1924 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
1925 SuggestCaret::AndIgnoreTrivialError});
1926 if (NS_FAILED(rv)) {
1927 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
1928 return Err(rv);
1930 NS_WARNING_ASSERTION(
1931 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
1932 "CaretPoint::SuggestCaretPoint() failed, but ignored");
1933 return EditActionResult::HandledResult();
1937 if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() ||
1938 aScanFromCaretPointResult.ReachedPreformattedLineBreak()) {
1939 Result<CaretPoint, nsresult> caretPointOrError =
1940 HandleDeleteCollapsedSelectionAtWhiteSpaces(
1941 aHTMLEditor, aDirectionAndAmount,
1942 aWSRunScannerAtCaret.ScanStartRef(), aEditingHost);
1943 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
1944 NS_WARNING(
1945 "AutoDeleteRangesHandler::"
1946 "HandleDeleteCollapsedSelectionAtWhiteSpaces() failed");
1947 return caretPointOrError.propagateErr();
1949 nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
1950 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
1951 if (NS_FAILED(rv)) {
1952 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
1953 return Err(rv);
1955 NS_WARNING_ASSERTION(
1956 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
1957 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
1958 return EditActionResult::HandledResult();
1961 if (aScanFromCaretPointResult.InNonCollapsibleCharacters()) {
1962 if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsText())) {
1963 return Err(NS_ERROR_FAILURE);
1965 Result<CaretPoint, nsresult> caretPointOrError =
1966 HandleDeleteCollapsedSelectionAtVisibleChar(
1967 aHTMLEditor, aDirectionAndAmount, aRangesToDelete,
1968 aScanFromCaretPointResult.Point<EditorDOMPoint>(), aEditingHost);
1969 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
1970 NS_WARNING(
1971 "AutoDeleteRangesHandler::"
1972 "HandleDeleteCollapsedSelectionAtVisibleChar() failed");
1973 return caretPointOrError.propagateErr();
1975 nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
1976 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
1977 if (NS_FAILED(rv)) {
1978 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
1979 return Err(rv);
1981 NS_WARNING_ASSERTION(
1982 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
1983 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
1984 return EditActionResult::HandledResult();
1987 if (aScanFromCaretPointResult.ReachedSpecialContent() ||
1988 aScanFromCaretPointResult.ReachedBRElement() ||
1989 aScanFromCaretPointResult.ReachedHRElement() ||
1990 aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) {
1991 if (aScanFromCaretPointResult.GetContent() == &aEditingHost) {
1992 return EditActionResult::HandledResult();
1994 nsCOMPtr<nsIContent> atomicContent = GetAtomicContentToDelete(
1995 aDirectionAndAmount, aWSRunScannerAtCaret, aScanFromCaretPointResult);
1996 if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*atomicContent))) {
1997 NS_WARNING(
1998 "AutoDeleteRangesHandler::GetAtomicContentToDelete() cannot find "
1999 "removable atomic content");
2000 return Err(NS_ERROR_FAILURE);
2002 Result<CaretPoint, nsresult> caretPointOrError = HandleDeleteAtomicContent(
2003 aHTMLEditor, *atomicContent, aWSRunScannerAtCaret.ScanStartRef(),
2004 aWSRunScannerAtCaret, aEditingHost);
2005 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2006 NS_WARNING("AutoDeleteRangesHandler::HandleDeleteAtomicContent() failed");
2007 return caretPointOrError.propagateErr();
2009 nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
2010 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
2011 if (NS_FAILED(rv)) {
2012 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
2013 return Err(rv);
2015 NS_WARNING_ASSERTION(
2016 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
2017 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
2018 return EditActionResult::HandledResult();
2021 if (aScanFromCaretPointResult.ReachedOtherBlockElement()) {
2022 if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
2023 return Err(NS_ERROR_FAILURE);
2025 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
2026 bool allRangesNotHandled = true;
2027 auto ret = EditActionResult::IgnoredResult();
2028 for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
2029 AutoBlockElementsJoiner joiner(*this);
2030 if (!joiner.PrepareToDeleteAtOtherBlockBoundary(
2031 aHTMLEditor, aDirectionAndAmount,
2032 *aScanFromCaretPointResult.ElementPtr(),
2033 aWSRunScannerAtCaret.ScanStartRef(), aWSRunScannerAtCaret)) {
2034 continue;
2036 allRangesNotHandled = false;
2037 Result<EditActionResult, nsresult> result =
2038 joiner.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers,
2039 aWSRunScannerAtCaret.ScanStartRef(), MOZ_KnownLive(range),
2040 aEditingHost);
2041 if (MOZ_UNLIKELY(result.isErr())) {
2042 NS_WARNING(
2043 "AutoBlockElementsJoiner::Run() failed (other block boundary)");
2044 return result;
2046 ret |= result.inspect();
2048 return allRangesNotHandled ? EditActionResult::CanceledResult()
2049 : std::move(ret);
2052 if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) {
2053 if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) {
2054 return Err(NS_ERROR_FAILURE);
2056 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
2057 bool allRangesNotHandled = true;
2058 auto ret = EditActionResult::IgnoredResult();
2059 for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
2060 AutoBlockElementsJoiner joiner(*this);
2061 if (!joiner.PrepareToDeleteAtCurrentBlockBoundary(
2062 aHTMLEditor, aDirectionAndAmount,
2063 *aScanFromCaretPointResult.ElementPtr(),
2064 aWSRunScannerAtCaret.ScanStartRef())) {
2065 continue;
2067 allRangesNotHandled = false;
2068 Result<EditActionResult, nsresult> result =
2069 joiner.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers,
2070 aWSRunScannerAtCaret.ScanStartRef(), MOZ_KnownLive(range),
2071 aEditingHost);
2072 if (MOZ_UNLIKELY(result.isErr())) {
2073 NS_WARNING(
2074 "AutoBlockElementsJoiner::Run() failed (current block boundary)");
2075 return result;
2077 ret |= result.inspect();
2079 return allRangesNotHandled ? EditActionResult::CanceledResult()
2080 : std::move(ret);
2083 MOZ_ASSERT_UNREACHABLE("New type of reached content hasn't been handled yet");
2084 return EditActionResult::IgnoredResult();
2087 nsresult HTMLEditor::AutoDeleteRangesHandler::
2088 ComputeRangesToDeleteTextAroundCollapsedRanges(
2089 nsIEditor::EDirection aDirectionAndAmount,
2090 AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const {
2091 MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext ||
2092 aDirectionAndAmount == nsIEditor::ePrevious);
2094 const auto caretPosition =
2095 aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
2096 MOZ_ASSERT(caretPosition.IsSetAndValid());
2097 if (MOZ_UNLIKELY(NS_WARN_IF(!caretPosition.IsInContentNode()))) {
2098 return NS_ERROR_FAILURE;
2101 EditorDOMRangeInTexts rangeToDelete;
2102 if (aDirectionAndAmount == nsIEditor::eNext) {
2103 Result<EditorDOMRangeInTexts, nsresult> result =
2104 WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(caretPosition,
2105 aEditingHost);
2106 if (result.isErr()) {
2107 NS_WARNING(
2108 "WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom() failed");
2109 return result.unwrapErr();
2111 rangeToDelete = result.unwrap();
2112 if (!rangeToDelete.IsPositioned()) {
2113 return NS_OK; // no range to delete, but consume it.
2115 } else {
2116 Result<EditorDOMRangeInTexts, nsresult> result =
2117 WSRunScanner::GetRangeInTextNodesToBackspaceFrom(caretPosition,
2118 aEditingHost);
2119 if (result.isErr()) {
2120 NS_WARNING("WSRunScanner::GetRangeInTextNodesToBackspaceFrom() failed");
2121 return result.unwrapErr();
2123 rangeToDelete = result.unwrap();
2124 if (!rangeToDelete.IsPositioned()) {
2125 return NS_OK; // no range to delete, but consume it.
2129 nsresult rv = aRangesToDelete.SetStartAndEnd(rangeToDelete.StartRef(),
2130 rangeToDelete.EndRef());
2131 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
2132 "AutoArrayRanges::SetStartAndEnd() failed");
2133 return rv;
2136 Result<CaretPoint, nsresult>
2137 HTMLEditor::AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges(
2138 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
2139 AutoRangeArray& aRangesToDelete, const Element& aEditingHost) {
2140 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2141 MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext ||
2142 aDirectionAndAmount == nsIEditor::ePrevious);
2144 nsresult rv = ComputeRangesToDeleteTextAroundCollapsedRanges(
2145 aDirectionAndAmount, aRangesToDelete, aEditingHost);
2146 if (NS_FAILED(rv)) {
2147 return Err(NS_ERROR_FAILURE);
2149 if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) {
2150 return CaretPoint(EditorDOMPoint()); // no range to delete
2153 // FYI: rangeToDelete does not contain newly empty inline ancestors which
2154 // are removed by DeleteTextAndNormalizeSurroundingWhiteSpaces().
2155 // So, if `getTargetRanges()` needs to include parent empty elements,
2156 // we need to extend the range with
2157 // HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement().
2158 EditorRawDOMRange rangeToDelete(aRangesToDelete.FirstRangeRef());
2159 if (MOZ_UNLIKELY(!rangeToDelete.IsInTextNodes())) {
2160 NS_WARNING("The extended range to delete character was not in text nodes");
2161 return Err(NS_ERROR_FAILURE);
2164 Result<CaretPoint, nsresult> caretPointOrError =
2165 aHTMLEditor.DeleteTextAndNormalizeSurroundingWhiteSpaces(
2166 rangeToDelete.StartRef().AsInText(),
2167 rangeToDelete.EndRef().AsInText(),
2168 TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors,
2169 aDirectionAndAmount == nsIEditor::eNext ? DeleteDirection::Forward
2170 : DeleteDirection::Backward);
2171 aHTMLEditor.TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces = true;
2172 NS_WARNING_ASSERTION(
2173 caretPointOrError.isOk(),
2174 "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() failed");
2175 return caretPointOrError;
2178 Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
2179 HandleDeleteCollapsedSelectionAtWhiteSpaces(
2180 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
2181 const EditorDOMPoint& aPointToDelete, const Element& aEditingHost) {
2182 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2183 MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible());
2185 EditorDOMPoint pointToPutCaret;
2186 if (aDirectionAndAmount == nsIEditor::eNext) {
2187 Result<CaretPoint, nsresult> caretPointOrError =
2188 WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace(
2189 aHTMLEditor, aPointToDelete, aEditingHost);
2190 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2191 NS_WARNING(
2192 "WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace() failed");
2193 return caretPointOrError;
2195 caretPointOrError.unwrap().MoveCaretPointTo(
2196 pointToPutCaret, aHTMLEditor,
2197 {SuggestCaret::OnlyIfHasSuggestion,
2198 SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
2199 } else {
2200 Result<CaretPoint, nsresult> caretPointOrError =
2201 WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace(
2202 aHTMLEditor, aPointToDelete, aEditingHost);
2203 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2204 NS_WARNING(
2205 "WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace() failed");
2206 return caretPointOrError;
2208 caretPointOrError.unwrap().MoveCaretPointTo(
2209 pointToPutCaret, aHTMLEditor,
2210 {SuggestCaret::OnlyIfHasSuggestion,
2211 SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
2213 const auto newCaretPosition =
2214 aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
2215 if (MOZ_UNLIKELY(!newCaretPosition.IsSet())) {
2216 NS_WARNING("There was no selection range");
2217 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
2219 Result<CaretPoint, nsresult> caretPointOrError =
2220 aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
2221 newCaretPosition);
2222 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2223 NS_WARNING(
2224 "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()"
2225 " failed");
2226 return caretPointOrError;
2228 caretPointOrError.unwrap().MoveCaretPointTo(
2229 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
2230 return CaretPoint(std::move(pointToPutCaret));
2233 Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
2234 HandleDeleteCollapsedSelectionAtVisibleChar(
2235 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
2236 AutoRangeArray& aRangesToDelete,
2237 const EditorDOMPoint& aPointAtDeletingChar,
2238 const Element& aEditingHost) {
2239 MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
2240 MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible());
2241 MOZ_ASSERT(aPointAtDeletingChar.IsSet());
2242 MOZ_ASSERT(aPointAtDeletingChar.IsInTextNode());
2244 OwningNonNull<Text> visibleTextNode =
2245 *aPointAtDeletingChar.ContainerAs<Text>();
2246 EditorDOMPoint startToDelete, endToDelete;
2247 // FIXME: This does not care grapheme cluster of complicate character
2248 // sequence like Emoji.
2249 // TODO: Investigate what happens if a grapheme cluster which should be
2250 // delete once is split to multiple text nodes.
2251 // TODO: We should stop using this path, instead, we should extend the range
2252 // before calling this method.
2253 if (aDirectionAndAmount == nsIEditor::ePrevious) {
2254 if (MOZ_UNLIKELY(aPointAtDeletingChar.IsStartOfContainer())) {
2255 return Err(NS_ERROR_UNEXPECTED);
2257 startToDelete = aPointAtDeletingChar.PreviousPoint();
2258 endToDelete = aPointAtDeletingChar;
2259 // Bug 1068979: delete both codepoints if surrogate pair
2260 if (!startToDelete.IsStartOfContainer()) {
2261 const nsTextFragment* text = &visibleTextNode->TextFragment();
2262 if (text->IsLowSurrogateFollowingHighSurrogateAt(
2263 startToDelete.Offset())) {
2264 startToDelete.RewindOffset();
2267 } else {
2268 if (NS_WARN_IF(aRangesToDelete.Ranges().IsEmpty()) ||
2269 NS_WARN_IF(aRangesToDelete.FirstRangeRef()->GetStartContainer() !=
2270 aPointAtDeletingChar.GetContainer()) ||
2271 NS_WARN_IF(aRangesToDelete.FirstRangeRef()->GetEndContainer() !=
2272 aPointAtDeletingChar.GetContainer())) {
2273 return Err(NS_ERROR_FAILURE);
2275 startToDelete = aRangesToDelete.FirstRangeRef()->StartRef();
2276 endToDelete = aRangesToDelete.FirstRangeRef()->EndRef();
2280 Result<CaretPoint, nsresult> caretPointOrError =
2281 WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints(
2282 aHTMLEditor, &startToDelete, &endToDelete, aEditingHost);
2283 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2284 NS_WARNING(
2285 "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() "
2286 "failed");
2287 return caretPointOrError.propagateErr();
2289 // Ignore caret position because we'll set caret position below
2290 caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
2293 if (aHTMLEditor.MayHaveMutationEventListeners(
2294 NS_EVENT_BITS_MUTATION_NODEREMOVED |
2295 NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
2296 NS_EVENT_BITS_MUTATION_ATTRMODIFIED |
2297 NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
2298 (NS_WARN_IF(!startToDelete.IsSetAndValid()) ||
2299 NS_WARN_IF(!startToDelete.IsInTextNode()) ||
2300 NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
2301 NS_WARN_IF(!endToDelete.IsInTextNode()) ||
2302 NS_WARN_IF(startToDelete.ContainerAs<Text>() != visibleTextNode) ||
2303 NS_WARN_IF(endToDelete.ContainerAs<Text>() != visibleTextNode) ||
2304 NS_WARN_IF(startToDelete.Offset() >= endToDelete.Offset()))) {
2305 NS_WARNING("Mutation event listener changed the DOM tree");
2306 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
2309 EditorDOMPoint pointToPutCaret = startToDelete;
2311 AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
2312 &pointToPutCaret);
2313 Result<CaretPoint, nsresult> caretPointOrError =
2314 aHTMLEditor.DeleteTextWithTransaction(
2315 visibleTextNode, startToDelete.Offset(),
2316 endToDelete.Offset() - startToDelete.Offset());
2317 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2318 NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
2319 return caretPointOrError.propagateErr();
2321 trackPointToPutCaret.FlushAndStopTracking();
2322 caretPointOrError.unwrap().MoveCaretPointTo(
2323 pointToPutCaret, aHTMLEditor,
2324 {SuggestCaret::OnlyIfHasSuggestion,
2325 SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
2328 // XXX When Backspace key is pressed, Chromium removes following empty
2329 // text nodes when removing the last character of the non-empty text
2330 // node. However, Edge never removes empty text nodes even if
2331 // selection is in the following empty text node(s). For now, we
2332 // should keep our traditional behavior same as Edge for backward
2333 // compatibility.
2334 // XXX When Delete key is pressed, Edge removes all preceding empty
2335 // text nodes when removing the first character of the non-empty
2336 // text node. Chromium removes only selected empty text node and
2337 // following empty text nodes and the first character of the
2338 // non-empty text node. For now, we should keep our traditional
2339 // behavior same as Chromium for backward compatibility.
2341 AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
2342 &pointToPutCaret);
2343 nsresult rv =
2344 DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, visibleTextNode);
2345 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
2346 return Err(NS_ERROR_EDITOR_DESTROYED);
2348 NS_WARNING_ASSERTION(
2349 NS_SUCCEEDED(rv),
2350 "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() "
2351 "failed, but ignored");
2354 if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
2355 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
2358 // XXX `Selection` may be modified by mutation event listeners so
2359 // that we should use EditorDOMPoint::AtEndOf(visibleTextNode)
2360 // instead. (Perhaps, we don't and/or shouldn't need to do this
2361 // if the text node is preformatted.)
2362 AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
2363 &pointToPutCaret);
2364 Result<CaretPoint, nsresult> caretPointOrError =
2365 aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
2366 pointToPutCaret);
2367 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2368 NS_WARNING(
2369 "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()"
2370 " failed");
2371 return caretPointOrError.propagateErr();
2373 trackPointToPutCaret.FlushAndStopTracking();
2374 caretPointOrError.unwrap().MoveCaretPointTo(
2375 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
2376 // Remember that we did a ranged delete for the benefit of
2377 // AfterEditInner().
2378 aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true;
2379 return CaretPoint(std::move(pointToPutCaret));
2382 // static
2383 nsIContent* HTMLEditor::AutoDeleteRangesHandler::GetAtomicContentToDelete(
2384 nsIEditor::EDirection aDirectionAndAmount,
2385 const WSRunScanner& aWSRunScannerAtCaret,
2386 const WSScanResult& aScanFromCaretPointResult) {
2387 MOZ_ASSERT(aScanFromCaretPointResult.GetContent());
2389 if (!aScanFromCaretPointResult.ReachedSpecialContent()) {
2390 return aScanFromCaretPointResult.GetContent();
2393 if (!aScanFromCaretPointResult.GetContent()->IsText() ||
2394 HTMLEditUtils::IsRemovableNode(*aScanFromCaretPointResult.GetContent())) {
2395 return aScanFromCaretPointResult.GetContent();
2398 // aScanFromCaretPointResult is non-removable text node.
2399 // Since we try removing atomic content, we look for removable node from
2400 // scanned point that is non-removable text.
2401 nsIContent* removableRoot = aScanFromCaretPointResult.GetContent();
2402 while (removableRoot && !HTMLEditUtils::IsRemovableNode(*removableRoot)) {
2403 removableRoot = removableRoot->GetParent();
2406 if (removableRoot) {
2407 return removableRoot;
2410 // Not found better content. This content may not be removable.
2411 return aScanFromCaretPointResult.GetContent();
2414 nsresult
2415 HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent(
2416 Element* aEditingHost, const nsIContent& aAtomicContent,
2417 AutoRangeArray& aRangesToDelete) const {
2418 EditorDOMRange rangeToDelete =
2419 WSRunScanner::GetRangesForDeletingAtomicContent(aEditingHost,
2420 aAtomicContent);
2421 if (!rangeToDelete.IsPositioned()) {
2422 NS_WARNING("WSRunScanner::GetRangeForDeleteAContentNode() failed");
2423 return NS_ERROR_FAILURE;
2425 nsresult rv = aRangesToDelete.SetStartAndEnd(rangeToDelete.StartRef(),
2426 rangeToDelete.EndRef());
2427 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
2428 "AutoRangeArray::SetStartAndEnd() failed");
2429 return rv;
2432 Result<CaretPoint, nsresult>
2433 HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAtomicContent(
2434 HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
2435 const EditorDOMPoint& aCaretPoint, const WSRunScanner& aWSRunScannerAtCaret,
2436 const Element& aEditingHost) {
2437 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2438 MOZ_ASSERT(!HTMLEditUtils::IsInvisibleBRElement(aAtomicContent));
2439 MOZ_ASSERT(&aAtomicContent != aWSRunScannerAtCaret.GetEditingHost());
2441 EditorDOMPoint pointToPutCaret = aCaretPoint;
2443 AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
2444 &pointToPutCaret);
2445 Result<CaretPoint, nsresult> caretPointOrError =
2446 WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt(
2447 aHTMLEditor, aAtomicContent, aCaretPoint, aEditingHost);
2448 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2449 NS_WARNING(
2450 "WhiteSpaceVisibilityKeeper::"
2451 "DeleteContentNodeAndJoinTextNodesAroundIt() failed");
2452 return caretPointOrError;
2454 trackPointToPutCaret.FlushAndStopTracking();
2455 caretPointOrError.unwrap().MoveCaretPointTo(
2456 pointToPutCaret, aHTMLEditor,
2457 {SuggestCaret::OnlyIfHasSuggestion,
2458 SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
2459 if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
2460 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
2465 AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(),
2466 &pointToPutCaret);
2467 Result<CaretPoint, nsresult> caretPointOrError =
2468 aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
2469 pointToPutCaret);
2470 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2471 NS_WARNING(
2472 "HTMLEditor::"
2473 "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()"
2474 " failed");
2475 return caretPointOrError;
2477 trackPointToPutCaret.FlushAndStopTracking();
2478 caretPointOrError.unwrap().MoveCaretPointTo(
2479 pointToPutCaret, aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion});
2480 if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
2481 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
2484 return CaretPoint(std::move(pointToPutCaret));
2487 bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
2488 PrepareToDeleteAtOtherBlockBoundary(
2489 const HTMLEditor& aHTMLEditor,
2490 nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement,
2491 const EditorDOMPoint& aCaretPoint,
2492 const WSRunScanner& aWSRunScannerAtCaret) {
2493 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2494 MOZ_ASSERT(aCaretPoint.IsSetAndValid());
2496 mMode = Mode::JoinOtherBlock;
2498 // Make sure it's not a table element. If so, cancel the operation
2499 // (translation: users cannot backspace or delete across table cells)
2500 if (HTMLEditUtils::IsAnyTableElement(&aOtherBlockElement)) {
2501 return false;
2504 // First find the adjacent node in the block
2505 if (aDirectionAndAmount == nsIEditor::ePrevious) {
2506 mLeafContentInOtherBlock = HTMLEditUtils::GetLastLeafContent(
2507 aOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode},
2508 BlockInlineCheck::Unused, &aOtherBlockElement);
2509 mLeftContent = mLeafContentInOtherBlock;
2510 mRightContent = aCaretPoint.GetContainerAs<nsIContent>();
2511 } else {
2512 mLeafContentInOtherBlock = HTMLEditUtils::GetFirstLeafContent(
2513 aOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode},
2514 BlockInlineCheck::Unused, &aOtherBlockElement);
2515 mLeftContent = aCaretPoint.GetContainerAs<nsIContent>();
2516 mRightContent = mLeafContentInOtherBlock;
2519 // Next to a block. See if we are between the block and a `<br>`.
2520 // If so, we really want to delete the `<br>`. Else join content at
2521 // selection to the block.
2522 WSScanResult scanFromCaretResult =
2523 aDirectionAndAmount == nsIEditor::eNext
2524 ? aWSRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
2525 aCaretPoint)
2526 : aWSRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
2527 aCaretPoint);
2528 // If we found a `<br>` element, we need to delete it instead of joining the
2529 // contents.
2530 if (scanFromCaretResult.ReachedBRElement()) {
2531 mBRElement = scanFromCaretResult.BRElementPtr();
2532 mMode = Mode::DeleteBRElement;
2533 return true;
2536 return mLeftContent && mRightContent;
2539 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
2540 ComputeRangeToDeleteBRElement(nsRange& aRangeToDelete) const {
2541 MOZ_ASSERT(mBRElement);
2542 // XXX Why don't we scan invisible leading white-spaces which follows the
2543 // `<br>` element?
2544 IgnoredErrorResult error;
2545 aRangeToDelete.SelectNode(*mBRElement, error);
2546 NS_WARNING_ASSERTION(!error.Failed(), "nsRange::SelectNode() failed");
2547 return error.StealNSResult();
2550 Result<EditActionResult, nsresult>
2551 HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement(
2552 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
2553 const Element& aEditingHost) {
2554 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2555 MOZ_ASSERT(mBRElement);
2557 // If we're deleting selection (not replacing with new content), we should
2558 // put caret to end of preceding text node if there is. Then, users can type
2559 // text in it like the other browsers.
2560 EditorDOMPoint pointToPutCaret = [&]() {
2561 if (!MayEditActionDeleteAroundCollapsedSelection(
2562 aHTMLEditor.GetEditAction())) {
2563 return EditorDOMPoint();
2565 WSRunScanner scanner(&aEditingHost, EditorRawDOMPoint(mBRElement),
2566 BlockInlineCheck::UseComputedDisplayOutsideStyle);
2567 WSScanResult maybePreviousText =
2568 scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
2569 EditorRawDOMPoint(mBRElement));
2570 if (maybePreviousText.IsContentEditable() &&
2571 maybePreviousText.InVisibleOrCollapsibleCharacters() &&
2572 !HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) {
2573 return maybePreviousText.Point<EditorDOMPoint>();
2575 WSScanResult maybeNextText = scanner.ScanNextVisibleNodeOrBlockBoundaryFrom(
2576 EditorRawDOMPoint::After(*mBRElement));
2577 if (maybeNextText.IsContentEditable() &&
2578 maybeNextText.InVisibleOrCollapsibleCharacters()) {
2579 return maybeNextText.Point<EditorDOMPoint>();
2581 return EditorDOMPoint();
2582 }();
2584 // If we found a `<br>` element, we should delete it instead of joining the
2585 // contents.
2586 nsresult rv =
2587 aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*mBRElement));
2588 if (NS_FAILED(rv)) {
2589 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
2590 return Err(rv);
2593 if (mLeftContent && mRightContent &&
2594 HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) !=
2595 HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) {
2596 return EditActionResult::HandledResult();
2599 // Put selection at edge of block and we are done.
2600 if (NS_WARN_IF(!mLeafContentInOtherBlock)) {
2601 // XXX This must be odd case. The other block can be empty.
2602 return Err(NS_ERROR_FAILURE);
2605 if (pointToPutCaret.IsSet()) {
2606 nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
2607 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
2608 return Err(NS_ERROR_EDITOR_DESTROYED);
2610 if (NS_SUCCEEDED(rv)) {
2611 // If we prefer to use style in the previous line, we should forget
2612 // previous styles since the caret position has all styles which we want
2613 // to use with new content.
2614 if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) {
2615 aHTMLEditor.TopLevelEditSubActionDataRef()
2616 .mCachedPendingStyles->Clear();
2618 // And we don't want to keep extending a link at ex-end of the previous
2619 // paragraph.
2620 if (HTMLEditor::GetLinkElement(pointToPutCaret.GetContainer())) {
2621 aHTMLEditor.mPendingStylesToApplyToNewContent
2622 ->ClearLinkAndItsSpecifiedStyle();
2624 } else {
2625 NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored");
2627 return EditActionResult::HandledResult();
2630 EditorRawDOMPoint newCaretPosition =
2631 HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>(
2632 *mLeafContentInOtherBlock, aDirectionAndAmount);
2633 if (MOZ_UNLIKELY(!newCaretPosition.IsSet())) {
2634 NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
2635 return Err(NS_ERROR_FAILURE);
2637 rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition);
2638 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
2639 return Err(NS_ERROR_EDITOR_DESTROYED);
2641 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
2642 "EditorBase::CollapseSelectionTo() failed, but ignored");
2643 return EditActionResult::HandledResult();
2646 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
2647 ComputeRangeToDeleteAtOtherBlockBoundary(
2648 const HTMLEditor& aHTMLEditor,
2649 nsIEditor::EDirection aDirectionAndAmount,
2650 const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete,
2651 const Element& aEditingHost) const {
2652 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2653 MOZ_ASSERT(aCaretPoint.IsSetAndValid());
2654 MOZ_ASSERT(mLeftContent);
2655 MOZ_ASSERT(mRightContent);
2657 if (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) !=
2658 HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) {
2659 if (!mDeleteRangesHandlerConst.CanFallbackToDeleteRangeWithTransaction(
2660 aRangeToDelete)) {
2661 nsresult rv = aRangeToDelete.CollapseTo(aCaretPoint.ToRawRangeBoundary());
2662 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::CollapseTo() failed");
2663 return rv;
2665 nsresult rv = mDeleteRangesHandlerConst
2666 .FallbackToComputeRangeToDeleteRangeWithTransaction(
2667 aHTMLEditor, aRangeToDelete, aEditingHost);
2668 NS_WARNING_ASSERTION(
2669 NS_SUCCEEDED(rv),
2670 "AutoDeleteRangesHandler::"
2671 "FallbackToComputeRangeToDeleteRangeWithTransaction() failed");
2672 return rv;
2675 AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
2676 *mRightContent);
2677 Result<bool, nsresult> canJoinThem =
2678 joiner.Prepare(aHTMLEditor, aEditingHost);
2679 if (canJoinThem.isErr()) {
2680 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
2681 return canJoinThem.unwrapErr();
2683 if (canJoinThem.inspect() && joiner.CanJoinBlocks() &&
2684 !joiner.ShouldDeleteLeafContentInstead()) {
2685 nsresult rv =
2686 joiner.ComputeRangeToDelete(aHTMLEditor, aCaretPoint, aRangeToDelete);
2687 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
2688 "AutoInclusiveAncestorBlockElementsJoiner::"
2689 "ComputeRangeToDelete() failed");
2690 return rv;
2693 // If AutoInclusiveAncestorBlockElementsJoiner didn't handle it and it's not
2694 // canceled, user may want to modify the start leaf node or the last leaf
2695 // node of the block.
2696 if (mLeafContentInOtherBlock == aCaretPoint.GetContainer()) {
2697 return NS_OK;
2700 AutoHideSelectionChanges hideSelectionChanges(aHTMLEditor.SelectionRef());
2702 // If it's ignored, it didn't modify the DOM tree. In this case, user must
2703 // want to delete nearest leaf node in the other block element.
2704 // TODO: We need to consider this before calling ComputeRangesToDelete() for
2705 // computing the deleting range.
2706 EditorRawDOMPoint newCaretPoint =
2707 aDirectionAndAmount == nsIEditor::ePrevious
2708 ? EditorRawDOMPoint::AtEndOf(*mLeafContentInOtherBlock)
2709 : EditorRawDOMPoint(mLeafContentInOtherBlock, 0);
2710 // If new caret position is same as current caret position, we can do
2711 // nothing anymore.
2712 if (aRangeToDelete.Collapsed() &&
2713 aRangeToDelete.EndRef() == newCaretPoint.ToRawRangeBoundary()) {
2714 return NS_OK;
2716 // TODO: Stop modifying the `Selection` for computing the target ranges.
2717 nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPoint);
2718 if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
2719 NS_WARNING(
2720 "EditorBase::CollapseSelectionTo() caused destroying the editor");
2721 return NS_ERROR_EDITOR_DESTROYED;
2723 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
2724 "EditorBase::CollapseSelectionTo() failed");
2725 if (NS_SUCCEEDED(rv)) {
2726 AutoRangeArray rangeArray(aHTMLEditor.SelectionRef());
2727 AutoDeleteRangesHandler anotherHandler(mDeleteRangesHandlerConst);
2728 rv = anotherHandler.ComputeRangesToDelete(aHTMLEditor, aDirectionAndAmount,
2729 rangeArray, aEditingHost);
2730 if (NS_SUCCEEDED(rv)) {
2731 if (MOZ_LIKELY(!rangeArray.Ranges().IsEmpty())) {
2732 MOZ_ASSERT(rangeArray.Ranges().Length() == 1);
2733 aRangeToDelete.SetStartAndEnd(rangeArray.FirstRangeRef()->StartRef(),
2734 rangeArray.FirstRangeRef()->EndRef());
2735 } else {
2736 NS_WARNING(
2737 "Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() "
2738 "returned no range");
2739 rv = NS_ERROR_FAILURE;
2741 } else {
2742 NS_WARNING(
2743 "Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() failed");
2746 // Restore selection.
2747 nsresult rvCollapsingSelectionTo =
2748 aHTMLEditor.CollapseSelectionTo(aCaretPoint);
2749 if (MOZ_UNLIKELY(rvCollapsingSelectionTo == NS_ERROR_EDITOR_DESTROYED)) {
2750 NS_WARNING(
2751 "EditorBase::CollapseSelectionTo() caused destroying the editor");
2752 return NS_ERROR_EDITOR_DESTROYED;
2754 NS_WARNING_ASSERTION(
2755 NS_SUCCEEDED(rvCollapsingSelectionTo),
2756 "EditorBase::CollapseSelectionTo() failed to restore caret position");
2757 return NS_SUCCEEDED(rv) && NS_SUCCEEDED(rvCollapsingSelectionTo)
2758 ? NS_OK
2759 : NS_ERROR_FAILURE;
2762 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
2763 AutoBlockElementsJoiner::HandleDeleteAtOtherBlockBoundary(
2764 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
2765 nsIEditor::EStripWrappers aStripWrappers,
2766 const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete,
2767 const Element& aEditingHost) {
2768 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2769 MOZ_ASSERT(aCaretPoint.IsSetAndValid());
2770 MOZ_ASSERT(mDeleteRangesHandler);
2771 MOZ_ASSERT(mLeftContent);
2772 MOZ_ASSERT(mRightContent);
2774 if (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) !=
2775 HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) {
2776 // If we have not deleted `<br>` element and are not called recursively,
2777 // we should call `DeleteRangesWithTransaction()` here.
2778 if (!mDeleteRangesHandler->CanFallbackToDeleteRangeWithTransaction(
2779 aRangeToDelete)) {
2780 return EditActionResult::IgnoredResult();
2782 Result<CaretPoint, nsresult> caretPointOrError =
2783 mDeleteRangesHandler->FallbackToDeleteRangeWithTransaction(
2784 aHTMLEditor, aRangeToDelete);
2785 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
2786 NS_WARNING(
2787 "AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() "
2788 "failed");
2789 return caretPointOrError.propagateErr();
2791 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
2792 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
2793 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
2794 SuggestCaret::AndIgnoreTrivialError});
2795 if (NS_FAILED(rv)) {
2796 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
2797 return Err(rv);
2799 NS_WARNING_ASSERTION(
2800 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
2801 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
2802 // Don't return "ignored" to avoid to fall it back to delete ranges
2803 // recursively.
2804 return EditActionResult::HandledResult();
2807 // Else we are joining content to block
2808 AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
2809 *mRightContent);
2810 Result<bool, nsresult> canJoinThem =
2811 joiner.Prepare(aHTMLEditor, aEditingHost);
2812 if (MOZ_UNLIKELY(canJoinThem.isErr())) {
2813 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
2814 return canJoinThem.propagateErr();
2817 if (!canJoinThem.inspect()) {
2818 nsresult rv = aHTMLEditor.CollapseSelectionTo(aCaretPoint);
2819 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
2820 return Err(NS_ERROR_EDITOR_DESTROYED);
2822 NS_WARNING_ASSERTION(
2823 NS_SUCCEEDED(rv),
2824 "EditorBase::CollapseSelectionTo() failed, but ignored");
2825 return EditActionResult::CanceledResult();
2828 auto result = EditActionResult::IgnoredResult();
2829 EditorDOMPoint pointToPutCaret(aCaretPoint);
2830 if (joiner.CanJoinBlocks()) {
2832 AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(),
2833 &pointToPutCaret);
2834 Result<EditActionResult, nsresult> joinResult =
2835 joiner.Run(aHTMLEditor, aEditingHost);
2836 if (MOZ_UNLIKELY(joinResult.isErr())) {
2837 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed");
2838 return joinResult;
2840 result |= joinResult.unwrap();
2841 #ifdef DEBUG
2842 if (joiner.ShouldDeleteLeafContentInstead()) {
2843 NS_ASSERTION(
2844 result.Ignored(),
2845 "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
2846 "returning ignored, but returned not ignored");
2847 } else {
2848 NS_ASSERTION(
2849 !result.Ignored(),
2850 "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
2851 "returning handled, but returned ignored");
2853 #endif // #ifdef DEBUG
2854 // If we're deleting selection (not replacing with new content) and
2855 // AutoInclusiveAncestorBlockElementsJoiner computed new caret position,
2856 // we should use it. Otherwise, we should keep the our traditional
2857 // behavior.
2858 if (result.Handled() && joiner.PointRefToPutCaret().IsSet()) {
2859 nsresult rv =
2860 aHTMLEditor.CollapseSelectionTo(joiner.PointRefToPutCaret());
2861 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
2862 return Err(NS_ERROR_EDITOR_DESTROYED);
2864 if (NS_FAILED(rv)) {
2865 NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored");
2866 return result;
2868 // If we prefer to use style in the previous line, we should forget
2869 // previous styles since the caret position has all styles which we want
2870 // to use with new content.
2871 if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) {
2872 aHTMLEditor.TopLevelEditSubActionDataRef()
2873 .mCachedPendingStyles->Clear();
2875 // And we don't want to keep extending a link at ex-end of the previous
2876 // paragraph.
2877 if (HTMLEditor::GetLinkElement(
2878 joiner.PointRefToPutCaret().GetContainer())) {
2879 aHTMLEditor.mPendingStylesToApplyToNewContent
2880 ->ClearLinkAndItsSpecifiedStyle();
2882 return result;
2886 // If AutoInclusiveAncestorBlockElementsJoiner didn't handle it and it's not
2887 // canceled, user may want to modify the start leaf node or the last leaf
2888 // node of the block.
2889 if (result.Ignored() &&
2890 mLeafContentInOtherBlock != aCaretPoint.GetContainer()) {
2891 // If it's ignored, it didn't modify the DOM tree. In this case, user
2892 // must want to delete nearest leaf node in the other block element.
2893 // TODO: We need to consider this before calling Run() for computing the
2894 // deleting range.
2895 EditorRawDOMPoint newCaretPoint =
2896 aDirectionAndAmount == nsIEditor::ePrevious
2897 ? EditorRawDOMPoint::AtEndOf(*mLeafContentInOtherBlock)
2898 : EditorRawDOMPoint(mLeafContentInOtherBlock, 0);
2899 // If new caret position is same as current caret position, we can do
2900 // nothing anymore.
2901 if (aRangeToDelete.Collapsed() &&
2902 aRangeToDelete.EndRef() == newCaretPoint.ToRawRangeBoundary()) {
2903 return EditActionResult::CanceledResult();
2905 nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPoint);
2906 if (NS_FAILED(rv)) {
2907 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
2908 return Err(rv);
2910 AutoRangeArray rangesToDelete(aHTMLEditor.SelectionRef());
2911 AutoDeleteRangesHandler anotherHandler(mDeleteRangesHandler);
2912 Result<EditActionResult, nsresult> fallbackResult =
2913 anotherHandler.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers,
2914 rangesToDelete, aEditingHost);
2915 if (MOZ_UNLIKELY(fallbackResult.isErr())) {
2916 NS_WARNING("Recursive AutoDeleteRangesHandler::Run() failed");
2917 return fallbackResult;
2919 result |= fallbackResult.unwrap();
2920 return result;
2922 } else {
2923 result.MarkAsHandled();
2926 // Otherwise, we must have deleted the selection as user expected.
2927 nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
2928 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
2929 return Err(NS_ERROR_EDITOR_DESTROYED);
2931 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
2932 "EditorBase::CollapseSelectionTo() failed, but ignored");
2933 return result;
2936 bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
2937 PrepareToDeleteAtCurrentBlockBoundary(
2938 const HTMLEditor& aHTMLEditor,
2939 nsIEditor::EDirection aDirectionAndAmount,
2940 Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint) {
2941 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
2943 // At edge of our block. Look beside it and see if we can join to an
2944 // adjacent block
2945 mMode = Mode::JoinCurrentBlock;
2947 // Don't break the basic structure of the HTML document.
2948 if (aCurrentBlockElement.IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head,
2949 nsGkAtoms::body)) {
2950 return false;
2953 // Make sure it's not a table element. If so, cancel the operation
2954 // (translation: users cannot backspace or delete across table cells)
2955 if (HTMLEditUtils::IsAnyTableElement(&aCurrentBlockElement)) {
2956 return false;
2959 Element* editingHost = aHTMLEditor.ComputeEditingHost();
2960 if (NS_WARN_IF(!editingHost)) {
2961 return false;
2964 auto ScanJoinTarget = [&]() -> nsIContent* {
2965 nsIContent* targetContent =
2966 aDirectionAndAmount == nsIEditor::ePrevious
2967 ? HTMLEditUtils::GetPreviousContent(
2968 aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode},
2969 BlockInlineCheck::Unused, editingHost)
2970 : HTMLEditUtils::GetNextContent(
2971 aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode},
2972 BlockInlineCheck::Unused, editingHost);
2973 // If found content is an invisible text node, let's scan visible things.
2974 auto IsIgnorableDataNode = [](nsIContent* aContent) {
2975 return aContent && HTMLEditUtils::IsRemovableNode(*aContent) &&
2976 ((aContent->IsText() &&
2977 aContent->AsText()->TextIsOnlyWhitespace() &&
2978 !HTMLEditUtils::IsVisibleTextNode(*aContent->AsText())) ||
2979 (aContent->IsCharacterData() && !aContent->IsText()));
2981 if (!IsIgnorableDataNode(targetContent)) {
2982 return targetContent;
2984 MOZ_ASSERT(mSkippedInvisibleContents.IsEmpty());
2985 for (nsIContent* adjacentContent =
2986 aDirectionAndAmount == nsIEditor::ePrevious
2987 ? HTMLEditUtils::GetPreviousContent(
2988 *targetContent, {WalkTreeOption::StopAtBlockBoundary},
2989 BlockInlineCheck::UseComputedDisplayOutsideStyle,
2990 editingHost)
2991 : HTMLEditUtils::GetNextContent(
2992 *targetContent, {WalkTreeOption::StopAtBlockBoundary},
2993 BlockInlineCheck::UseComputedDisplayOutsideStyle,
2994 editingHost);
2995 adjacentContent;
2996 adjacentContent =
2997 aDirectionAndAmount == nsIEditor::ePrevious
2998 ? HTMLEditUtils::GetPreviousContent(
2999 *adjacentContent, {WalkTreeOption::StopAtBlockBoundary},
3000 BlockInlineCheck::UseComputedDisplayOutsideStyle,
3001 editingHost)
3002 : HTMLEditUtils::GetNextContent(
3003 *adjacentContent, {WalkTreeOption::StopAtBlockBoundary},
3004 BlockInlineCheck::UseComputedDisplayOutsideStyle,
3005 editingHost)) {
3006 // If non-editable element is found, we should not skip it to avoid
3007 // joining too far nodes.
3008 if (!HTMLEditUtils::IsSimplyEditableNode(*adjacentContent)) {
3009 break;
3011 // If block element is found, we should join last leaf content in it.
3012 if (HTMLEditUtils::IsBlockElement(
3013 *adjacentContent,
3014 BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
3015 nsIContent* leafContent =
3016 aDirectionAndAmount == nsIEditor::ePrevious
3017 ? HTMLEditUtils::GetLastLeafContent(
3018 *adjacentContent, {LeafNodeType::OnlyEditableLeafNode})
3019 : HTMLEditUtils::GetFirstLeafContent(
3020 *adjacentContent, {LeafNodeType::OnlyEditableLeafNode});
3021 mSkippedInvisibleContents.AppendElement(*targetContent);
3022 return leafContent ? leafContent : adjacentContent;
3024 // Only when the found node is an invisible text node or a non-text data
3025 // node, we should keep scanning.
3026 if (IsIgnorableDataNode(adjacentContent)) {
3027 mSkippedInvisibleContents.AppendElement(*targetContent);
3028 targetContent = adjacentContent;
3029 continue;
3031 // Otherwise, we find a visible things. We should join with last found
3032 // invisible text node.
3033 break;
3035 return targetContent;
3038 if (aDirectionAndAmount == nsIEditor::ePrevious) {
3039 mLeftContent = ScanJoinTarget();
3040 mRightContent = aCaretPoint.GetContainerAs<nsIContent>();
3041 } else {
3042 mRightContent = ScanJoinTarget();
3043 mLeftContent = aCaretPoint.GetContainerAs<nsIContent>();
3046 // Nothing to join
3047 if (!mLeftContent || !mRightContent) {
3048 return false;
3051 // Don't cross table boundaries.
3052 return HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) ==
3053 HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent);
3056 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3057 ComputeRangeToDeleteAtCurrentBlockBoundary(
3058 const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
3059 nsRange& aRangeToDelete, const Element& aEditingHost) const {
3060 MOZ_ASSERT(mLeftContent);
3061 MOZ_ASSERT(mRightContent);
3063 AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
3064 *mRightContent);
3065 Result<bool, nsresult> canJoinThem =
3066 joiner.Prepare(aHTMLEditor, aEditingHost);
3067 if (canJoinThem.isErr()) {
3068 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
3069 return canJoinThem.unwrapErr();
3071 if (canJoinThem.inspect()) {
3072 nsresult rv =
3073 joiner.ComputeRangeToDelete(aHTMLEditor, aCaretPoint, aRangeToDelete);
3074 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3075 "AutoInclusiveAncestorBlockElementsJoiner::"
3076 "ComputeRangesToDelete() failed");
3077 return rv;
3080 // In this case, nothing will be deleted so that the affected range should
3081 // be collapsed.
3082 nsresult rv = aRangeToDelete.CollapseTo(aCaretPoint.ToRawRangeBoundary());
3083 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::CollapseTo() failed");
3084 return rv;
3087 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
3088 AutoBlockElementsJoiner::HandleDeleteAtCurrentBlockBoundary(
3089 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
3090 const EditorDOMPoint& aCaretPoint, const Element& aEditingHost) {
3091 MOZ_ASSERT(mLeftContent);
3092 MOZ_ASSERT(mRightContent);
3094 AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
3095 *mRightContent);
3096 Result<bool, nsresult> canJoinThem =
3097 joiner.Prepare(aHTMLEditor, aEditingHost);
3098 if (MOZ_UNLIKELY(canJoinThem.isErr())) {
3099 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
3100 return Err(canJoinThem.unwrapErr());
3103 if (!canJoinThem.inspect()) {
3104 nsresult rv = aHTMLEditor.CollapseSelectionTo(aCaretPoint);
3105 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
3106 return Err(NS_ERROR_EDITOR_DESTROYED);
3108 NS_WARNING_ASSERTION(
3109 NS_SUCCEEDED(rv),
3110 "EditorBase::CollapseSelectionTo() failed, but ignored");
3111 return EditActionResult::CanceledResult();
3114 EditActionResult result = EditActionResult::IgnoredResult();
3115 EditorDOMPoint pointToPutCaret(aCaretPoint);
3116 if (joiner.CanJoinBlocks()) {
3117 AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret);
3118 Result<EditActionResult, nsresult> joinResult =
3119 joiner.Run(aHTMLEditor, aEditingHost);
3120 if (MOZ_UNLIKELY(joinResult.isErr())) {
3121 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed");
3122 return joinResult;
3124 result |= joinResult.unwrap();
3125 #ifdef DEBUG
3126 if (joiner.ShouldDeleteLeafContentInstead()) {
3127 NS_ASSERTION(result.Ignored(),
3128 "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
3129 "returning ignored, but returned not ignored");
3130 } else {
3131 NS_ASSERTION(!result.Ignored(),
3132 "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
3133 "returning handled, but returned ignored");
3135 #endif // #ifdef DEBUG
3137 // Cleaning up invisible nodes which are skipped at scanning mLeftContent or
3138 // mRightContent.
3139 for (const OwningNonNull<nsIContent>& content : mSkippedInvisibleContents) {
3140 nsresult rv =
3141 aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(content));
3142 if (NS_FAILED(rv)) {
3143 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
3144 return Err(rv);
3147 mSkippedInvisibleContents.Clear();
3149 // If we're deleting selection (not replacing with new content) and
3150 // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we
3151 // should use it. Otherwise, we should keep the our traditional behavior.
3152 if (result.Handled() && joiner.PointRefToPutCaret().IsSet()) {
3153 nsresult rv =
3154 aHTMLEditor.CollapseSelectionTo(joiner.PointRefToPutCaret());
3155 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
3156 return Err(NS_ERROR_EDITOR_DESTROYED);
3158 if (NS_FAILED(rv)) {
3159 NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored");
3160 return result;
3162 // If we prefer to use style in the previous line, we should forget
3163 // previous styles since the caret position has all styles which we want
3164 // to use with new content.
3165 if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) {
3166 aHTMLEditor.TopLevelEditSubActionDataRef()
3167 .mCachedPendingStyles->Clear();
3169 // And we don't want to keep extending a link at ex-end of the previous
3170 // paragraph.
3171 if (HTMLEditor::GetLinkElement(
3172 joiner.PointRefToPutCaret().GetContainer())) {
3173 aHTMLEditor.mPendingStylesToApplyToNewContent
3174 ->ClearLinkAndItsSpecifiedStyle();
3176 return result;
3179 // This should claim that trying to join the block means that
3180 // this handles the action because the caller shouldn't do anything
3181 // anymore in this case.
3182 result.MarkAsHandled();
3184 nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
3185 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
3186 return Err(NS_ERROR_EDITOR_DESTROYED);
3188 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3189 "EditorBase::CollapseSelectionTo() failed, but ignored");
3190 return result;
3193 nsresult
3194 HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteNonCollapsedRanges(
3195 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
3196 AutoRangeArray& aRangesToDelete,
3197 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
3198 const Element& aEditingHost) const {
3199 MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
3201 if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->StartRef().IsSet()) ||
3202 NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->EndRef().IsSet())) {
3203 return NS_ERROR_FAILURE;
3206 if (aRangesToDelete.Ranges().Length() == 1) {
3207 nsFrameSelection* frameSelection =
3208 aHTMLEditor.SelectionRef().GetFrameSelection();
3209 if (NS_WARN_IF(!frameSelection)) {
3210 return NS_ERROR_FAILURE;
3212 Result<EditorRawDOMRange, nsresult> result = ExtendOrShrinkRangeToDelete(
3213 aHTMLEditor, frameSelection,
3214 EditorRawDOMRange(aRangesToDelete.FirstRangeRef()));
3215 if (MOZ_UNLIKELY(result.isErr())) {
3216 NS_WARNING(
3217 "AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete() failed");
3218 return NS_ERROR_FAILURE;
3220 EditorRawDOMRange newRange(result.unwrap());
3221 if (MOZ_UNLIKELY(NS_FAILED(aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
3222 newRange.StartRef().ToRawRangeBoundary(),
3223 newRange.EndRef().ToRawRangeBoundary())))) {
3224 NS_WARNING("nsRange::SetStartAndEnd() failed");
3225 return NS_ERROR_FAILURE;
3227 if (MOZ_UNLIKELY(
3228 NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()))) {
3229 return NS_ERROR_FAILURE;
3231 if (NS_WARN_IF(aRangesToDelete.FirstRangeRef()->Collapsed())) {
3232 return NS_OK; // Hmm, there is nothing to delete...?
3236 if (!aHTMLEditor.IsPlaintextMailComposer()) {
3237 EditorDOMRange firstRange(aRangesToDelete.FirstRangeRef());
3238 EditorDOMRange extendedRange =
3239 WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries(
3240 aHTMLEditor.ComputeEditingHost(),
3241 EditorDOMRange(aRangesToDelete.FirstRangeRef()));
3242 if (firstRange != extendedRange) {
3243 nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
3244 extendedRange.StartRef().ToRawRangeBoundary(),
3245 extendedRange.EndRef().ToRawRangeBoundary());
3246 if (NS_FAILED(rv)) {
3247 NS_WARNING("nsRange::SetStartAndEnd() failed");
3248 return NS_ERROR_FAILURE;
3253 if (aRangesToDelete.FirstRangeRef()->GetStartContainer() ==
3254 aRangesToDelete.FirstRangeRef()->GetEndContainer()) {
3255 if (!aRangesToDelete.FirstRangeRef()->Collapsed()) {
3256 nsresult rv = ComputeRangesToDeleteRangesWithTransaction(
3257 aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost);
3258 NS_WARNING_ASSERTION(
3259 NS_SUCCEEDED(rv),
3260 "AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction("
3261 ") failed");
3262 return rv;
3264 // `DeleteUnnecessaryNodesAndCollapseSelection()` may delete parent
3265 // elements, but it does not affect computing target ranges. Therefore,
3266 // we don't need to touch aRangesToDelete in this case.
3267 return NS_OK;
3270 Element* startCiteNode = aHTMLEditor.GetMostDistantAncestorMailCiteElement(
3271 *aRangesToDelete.FirstRangeRef()->GetStartContainer());
3272 Element* endCiteNode = aHTMLEditor.GetMostDistantAncestorMailCiteElement(
3273 *aRangesToDelete.FirstRangeRef()->GetEndContainer());
3275 if (startCiteNode && !endCiteNode) {
3276 aDirectionAndAmount = nsIEditor::eNext;
3277 } else if (!startCiteNode && endCiteNode) {
3278 aDirectionAndAmount = nsIEditor::ePrevious;
3281 for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
3282 if (MOZ_UNLIKELY(range->Collapsed())) {
3283 continue;
3285 AutoBlockElementsJoiner joiner(*this);
3286 if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range)) {
3287 return NS_ERROR_FAILURE;
3289 nsresult rv =
3290 joiner.ComputeRangeToDelete(aHTMLEditor, aDirectionAndAmount, range,
3291 aSelectionWasCollapsed, aEditingHost);
3292 if (NS_FAILED(rv)) {
3293 NS_WARNING("AutoBlockElementsJoiner::ComputeRangeToDelete() failed");
3294 return rv;
3297 return NS_OK;
3300 Result<EditActionResult, nsresult>
3301 HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges(
3302 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
3303 nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
3304 SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost) {
3305 MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
3306 MOZ_ASSERT(!aRangesToDelete.IsCollapsed());
3308 if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->StartRef().IsSet()) ||
3309 NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->EndRef().IsSet())) {
3310 return Err(NS_ERROR_FAILURE);
3313 MOZ_ASSERT_IF(aRangesToDelete.Ranges().Length() == 1,
3314 aRangesToDelete.IsFirstRangeEditable(aEditingHost));
3316 // Else we have a non-collapsed selection. First adjust the selection.
3317 // XXX Why do we extend selection only when there is only one range?
3318 if (aRangesToDelete.Ranges().Length() == 1) {
3319 nsFrameSelection* frameSelection =
3320 aHTMLEditor.SelectionRef().GetFrameSelection();
3321 if (NS_WARN_IF(!frameSelection)) {
3322 return Err(NS_ERROR_FAILURE);
3324 Result<EditorRawDOMRange, nsresult> result = ExtendOrShrinkRangeToDelete(
3325 aHTMLEditor, frameSelection,
3326 EditorRawDOMRange(aRangesToDelete.FirstRangeRef()));
3327 if (MOZ_UNLIKELY(result.isErr())) {
3328 NS_WARNING(
3329 "AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete() failed");
3330 return Err(NS_ERROR_FAILURE);
3332 EditorRawDOMRange newRange(result.unwrap());
3333 if (NS_FAILED(aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
3334 newRange.StartRef().ToRawRangeBoundary(),
3335 newRange.EndRef().ToRawRangeBoundary()))) {
3336 NS_WARNING("nsRange::SetStartAndEnd() failed");
3337 return Err(NS_ERROR_FAILURE);
3339 if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned())) {
3340 return Err(NS_ERROR_FAILURE);
3342 if (NS_WARN_IF(aRangesToDelete.FirstRangeRef()->Collapsed())) {
3343 // Hmm, there is nothing to delete...?
3344 // In this case, the callers want collapsed selection. Therefore, we need
3345 // to change the `Selection` here.
3346 nsresult rv = aHTMLEditor.CollapseSelectionTo(
3347 aRangesToDelete.GetFirstRangeStartPoint<EditorRawDOMPoint>());
3348 if (NS_FAILED(rv)) {
3349 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
3350 return Err(rv);
3352 return EditActionResult::HandledResult();
3354 MOZ_ASSERT(aRangesToDelete.IsFirstRangeEditable(aEditingHost));
3357 // Remember that we did a ranged delete for the benefit of AfterEditInner().
3358 aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true;
3360 // Figure out if the endpoints are in nodes that can be merged. Adjust
3361 // surrounding white-space in preparation to delete selection.
3362 if (!aHTMLEditor.IsPlaintextMailComposer()) {
3364 AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
3365 &aRangesToDelete.FirstRangeRef());
3366 Result<CaretPoint, nsresult> caretPointOrError =
3367 WhiteSpaceVisibilityKeeper::PrepareToDeleteRange(
3368 aHTMLEditor, EditorDOMRange(aRangesToDelete.FirstRangeRef()),
3369 aEditingHost);
3370 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
3371 NS_WARNING("WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() failed");
3372 return caretPointOrError.propagateErr();
3374 // Ignore caret point suggestion because there was
3375 // AutoTransactionsConserveSelection.
3376 caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
3378 if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) ||
3379 (aHTMLEditor.MayHaveMutationEventListeners() &&
3380 NS_WARN_IF(!aRangesToDelete.IsFirstRangeEditable(aEditingHost)))) {
3381 NS_WARNING(
3382 "WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() made the first "
3383 "range invalid");
3384 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
3388 // XXX This is odd. We do we simply use `DeleteRangesWithTransaction()`
3389 // only when **first** range is in same container?
3390 if (aRangesToDelete.FirstRangeRef()->GetStartContainer() ==
3391 aRangesToDelete.FirstRangeRef()->GetEndContainer()) {
3392 // Because of previous DOM tree changes, the range may be collapsed.
3393 // If we've already removed all contents in the range, we shouldn't
3394 // delete anything around the caret.
3395 if (!aRangesToDelete.FirstRangeRef()->Collapsed()) {
3397 AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
3398 &aRangesToDelete.FirstRangeRef());
3399 Result<CaretPoint, nsresult> caretPointOrError =
3400 aHTMLEditor.DeleteRangesWithTransaction(
3401 aDirectionAndAmount, aStripWrappers, aRangesToDelete);
3402 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
3403 NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed");
3404 return caretPointOrError.propagateErr();
3406 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
3407 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
3408 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
3409 SuggestCaret::AndIgnoreTrivialError});
3410 if (NS_FAILED(rv)) {
3411 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
3412 return Err(rv);
3414 NS_WARNING_ASSERTION(
3415 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
3416 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
3418 if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) ||
3419 (aHTMLEditor.MayHaveMutationEventListeners(
3420 NS_EVENT_BITS_MUTATION_NODEREMOVED |
3421 NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
3422 NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) &&
3423 NS_WARN_IF(!aRangesToDelete.IsFirstRangeEditable(aEditingHost)))) {
3424 NS_WARNING(
3425 "EditorBase::DeleteRangesWithTransaction() made the first range "
3426 "invalid");
3427 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
3430 // However, even if the range is removed, we may need to clean up the
3431 // containers which become empty.
3432 nsresult rv = DeleteUnnecessaryNodesAndCollapseSelection(
3433 aHTMLEditor, aDirectionAndAmount,
3434 EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()),
3435 EditorDOMPoint(aRangesToDelete.FirstRangeRef()->EndRef()));
3436 if (NS_FAILED(rv)) {
3437 NS_WARNING(
3438 "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection("
3439 ") failed");
3440 return Err(rv);
3442 return EditActionResult::HandledResult();
3445 if (NS_WARN_IF(
3446 !aRangesToDelete.FirstRangeRef()->GetStartContainer()->IsContent()) ||
3447 NS_WARN_IF(
3448 !aRangesToDelete.FirstRangeRef()->GetEndContainer()->IsContent())) {
3449 return Err(NS_ERROR_FAILURE);
3452 // Figure out mailcite ancestors
3453 RefPtr<Element> startCiteNode =
3454 aHTMLEditor.GetMostDistantAncestorMailCiteElement(
3455 *aRangesToDelete.FirstRangeRef()->GetStartContainer());
3456 RefPtr<Element> endCiteNode =
3457 aHTMLEditor.GetMostDistantAncestorMailCiteElement(
3458 *aRangesToDelete.FirstRangeRef()->GetEndContainer());
3460 // If we only have a mailcite at one of the two endpoints, set the
3461 // directionality of the deletion so that the selection will end up
3462 // outside the mailcite.
3463 if (startCiteNode && !endCiteNode) {
3464 aDirectionAndAmount = nsIEditor::eNext;
3465 } else if (!startCiteNode && endCiteNode) {
3466 aDirectionAndAmount = nsIEditor::ePrevious;
3469 MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
3470 auto ret = EditActionResult::IgnoredResult();
3471 for (const OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) {
3472 if (MOZ_UNLIKELY(range->Collapsed())) {
3473 continue;
3475 AutoBlockElementsJoiner joiner(*this);
3476 if (!joiner.PrepareToDeleteNonCollapsedRange(aHTMLEditor, range)) {
3477 return Err(NS_ERROR_FAILURE);
3479 Result<EditActionResult, nsresult> result =
3480 joiner.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers,
3481 MOZ_KnownLive(range), aSelectionWasCollapsed, aEditingHost);
3482 if (MOZ_UNLIKELY(result.isErr())) {
3483 NS_WARNING("AutoBlockElementsJoiner::Run() failed");
3484 return result;
3486 ret |= result.inspect();
3488 return ret;
3491 bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3492 PrepareToDeleteNonCollapsedRange(const HTMLEditor& aHTMLEditor,
3493 const nsRange& aRangeToDelete) {
3494 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3495 MOZ_ASSERT(!aRangeToDelete.Collapsed());
3497 mLeftContent = HTMLEditUtils::GetInclusiveAncestorElement(
3498 *aRangeToDelete.GetStartContainer()->AsContent(),
3499 HTMLEditUtils::ClosestEditableBlockElement,
3500 BlockInlineCheck::UseComputedDisplayOutsideStyle);
3501 mRightContent = HTMLEditUtils::GetInclusiveAncestorElement(
3502 *aRangeToDelete.GetEndContainer()->AsContent(),
3503 HTMLEditUtils::ClosestEditableBlockElement,
3504 BlockInlineCheck::UseComputedDisplayOutsideStyle);
3505 // Note that mLeftContent and/or mRightContent can be nullptr if editing host
3506 // is an inline element. If both editable ancestor block is exactly same
3507 // one or one reaches an inline editing host, we can just delete the content
3508 // in ranges.
3509 if (mLeftContent == mRightContent || !mLeftContent || !mRightContent) {
3510 MOZ_ASSERT_IF(
3511 !mLeftContent || !mRightContent,
3512 aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost() ==
3513 aRangeToDelete.GetEndContainer()->AsContent()->GetEditingHost());
3514 mMode = Mode::DeleteContentInRange;
3515 return true;
3518 // If left block and right block are adjacent siblings and they are same
3519 // type of elements, we can merge them after deleting the selected contents.
3520 // MOOSE: this could conceivably screw up a table.. fix me.
3521 if (mLeftContent->GetParentNode() == mRightContent->GetParentNode() &&
3522 HTMLEditUtils::CanContentsBeJoined(*mLeftContent, *mRightContent) &&
3523 // XXX What's special about these three types of block?
3524 (mLeftContent->IsHTMLElement(nsGkAtoms::p) ||
3525 HTMLEditUtils::IsListItem(mLeftContent) ||
3526 HTMLEditUtils::IsHeader(*mLeftContent))) {
3527 mMode = Mode::JoinBlocksInSameParent;
3528 return true;
3531 mMode = Mode::DeleteNonCollapsedRange;
3532 return true;
3535 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3536 ComputeRangeToDeleteContentInRange(
3537 const HTMLEditor& aHTMLEditor,
3538 nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
3539 const Element& aEditingHost) const {
3540 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3541 MOZ_ASSERT(!aRangeToDelete.Collapsed());
3542 MOZ_ASSERT(mMode == Mode::DeleteContentInRange);
3543 MOZ_ASSERT(aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost());
3544 MOZ_ASSERT(
3545 aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost() ==
3546 aRangeToDelete.GetEndContainer()->AsContent()->GetEditingHost());
3547 MOZ_ASSERT(!mLeftContent == !mRightContent);
3548 MOZ_ASSERT_IF(mLeftContent, mLeftContent->IsElement());
3549 MOZ_ASSERT_IF(mLeftContent,
3550 aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf(
3551 mLeftContent));
3552 MOZ_ASSERT_IF(mRightContent, mRightContent->IsElement());
3553 MOZ_ASSERT_IF(
3554 mRightContent,
3555 aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent));
3556 MOZ_ASSERT_IF(
3557 !mLeftContent,
3558 HTMLEditUtils::IsInlineContent(
3559 *aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost(),
3560 BlockInlineCheck::UseComputedDisplayOutsideStyle));
3562 nsresult rv =
3563 mDeleteRangesHandlerConst.ComputeRangeToDeleteRangeWithTransaction(
3564 aHTMLEditor, aDirectionAndAmount, aRangeToDelete, aEditingHost);
3565 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3566 "AutoDeleteRangesHandler::"
3567 "ComputeRangeToDeleteRangeWithTransaction() failed");
3568 return rv;
3571 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
3572 AutoBlockElementsJoiner::DeleteContentInRange(
3573 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
3574 nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete) {
3575 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3576 MOZ_ASSERT(!aRangeToDelete.Collapsed());
3577 MOZ_ASSERT(mMode == Mode::DeleteContentInRange);
3578 MOZ_ASSERT(mDeleteRangesHandler);
3579 MOZ_ASSERT(aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost());
3580 MOZ_ASSERT(
3581 aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost() ==
3582 aRangeToDelete.GetEndContainer()->AsContent()->GetEditingHost());
3583 MOZ_ASSERT_IF(mLeftContent, mLeftContent->IsElement());
3584 MOZ_ASSERT_IF(mLeftContent,
3585 aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf(
3586 mLeftContent));
3587 MOZ_ASSERT_IF(mRightContent, mRightContent->IsElement());
3588 MOZ_ASSERT_IF(
3589 mRightContent,
3590 aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent));
3591 MOZ_ASSERT_IF(
3592 !mLeftContent,
3593 HTMLEditUtils::IsInlineContent(
3594 *aRangeToDelete.GetStartContainer()->AsContent()->GetEditingHost(),
3595 BlockInlineCheck::UseComputedDisplayOutsideStyle));
3598 AutoRangeArray rangesToDelete(aRangeToDelete);
3599 AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
3600 &rangesToDelete.FirstRangeRef());
3601 Result<CaretPoint, nsresult> caretPointOrError =
3602 aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
3603 aStripWrappers, rangesToDelete);
3604 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
3605 if (NS_WARN_IF(caretPointOrError.inspectErr() ==
3606 NS_ERROR_EDITOR_DESTROYED)) {
3607 return Err(NS_ERROR_EDITOR_DESTROYED);
3609 NS_WARNING(
3610 "EditorBase::DeleteRangesWithTransaction() failed, but ignored");
3611 } else {
3612 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
3613 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
3614 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
3615 SuggestCaret::AndIgnoreTrivialError});
3616 if (NS_FAILED(rv)) {
3617 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
3618 return Err(rv);
3620 NS_WARNING_ASSERTION(
3621 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
3622 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
3626 if (NS_WARN_IF(!aRangeToDelete.IsPositioned())) {
3627 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
3630 nsresult rv =
3631 mDeleteRangesHandler->DeleteUnnecessaryNodesAndCollapseSelection(
3632 aHTMLEditor, aDirectionAndAmount,
3633 EditorDOMPoint(aRangeToDelete.StartRef()),
3634 EditorDOMPoint(aRangeToDelete.EndRef()));
3635 if (NS_FAILED(rv)) {
3636 NS_WARNING(
3637 "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection() "
3638 "failed");
3639 return Err(rv);
3641 return EditActionResult::HandledResult();
3644 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3645 ComputeRangeToJoinBlockElementsInSameParent(
3646 const HTMLEditor& aHTMLEditor,
3647 nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
3648 const Element& aEditingHost) const {
3649 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3650 MOZ_ASSERT(!aRangeToDelete.Collapsed());
3651 MOZ_ASSERT(mMode == Mode::JoinBlocksInSameParent);
3652 MOZ_ASSERT(mLeftContent);
3653 MOZ_ASSERT(mLeftContent->IsElement());
3654 MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf(
3655 mLeftContent));
3656 MOZ_ASSERT(mRightContent);
3657 MOZ_ASSERT(mRightContent->IsElement());
3658 MOZ_ASSERT(
3659 aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent));
3660 MOZ_ASSERT(mLeftContent->GetParentNode() == mRightContent->GetParentNode());
3662 nsresult rv =
3663 mDeleteRangesHandlerConst.ComputeRangeToDeleteRangeWithTransaction(
3664 aHTMLEditor, aDirectionAndAmount, aRangeToDelete, aEditingHost);
3665 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3666 "AutoDeleteRangesHandler::"
3667 "ComputeRangeToDeleteRangeWithTransaction() failed");
3668 return rv;
3671 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
3672 AutoBlockElementsJoiner::JoinBlockElementsInSameParent(
3673 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
3674 nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
3675 SelectionWasCollapsed aSelectionWasCollapsed,
3676 const Element& aEditingHost) {
3677 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3678 MOZ_ASSERT(!aRangeToDelete.Collapsed());
3679 MOZ_ASSERT(mMode == Mode::JoinBlocksInSameParent);
3680 MOZ_ASSERT(mLeftContent);
3681 MOZ_ASSERT(mLeftContent->IsElement());
3682 MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf(
3683 mLeftContent));
3684 MOZ_ASSERT(mRightContent);
3685 MOZ_ASSERT(mRightContent->IsElement());
3686 MOZ_ASSERT(
3687 aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent));
3688 MOZ_ASSERT(mLeftContent->GetParentNode() == mRightContent->GetParentNode());
3690 const bool backspaceInRightBlock =
3691 aSelectionWasCollapsed == SelectionWasCollapsed::Yes &&
3692 nsIEditor::DirectionIsBackspace(aDirectionAndAmount);
3694 AutoRangeArray rangesToDelete(aRangeToDelete);
3695 Result<CaretPoint, nsresult> caretPointOrError =
3696 aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount,
3697 aStripWrappers, rangesToDelete);
3698 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
3699 NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed");
3700 return caretPointOrError.propagateErr();
3703 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
3704 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
3705 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
3706 SuggestCaret::AndIgnoreTrivialError});
3707 if (NS_FAILED(rv)) {
3708 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
3709 return Err(rv);
3711 NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
3712 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
3714 if (NS_WARN_IF(!mLeftContent->GetParentNode()) ||
3715 NS_WARN_IF(!mRightContent->GetParentNode()) ||
3716 NS_WARN_IF(mLeftContent->GetParentNode() !=
3717 mRightContent->GetParentNode())) {
3718 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
3721 auto startOfRightContent =
3722 HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
3723 *mRightContent);
3724 AutoTrackDOMPoint trackStartOfRightContent(aHTMLEditor.RangeUpdaterRef(),
3725 &startOfRightContent);
3726 Result<EditorDOMPoint, nsresult> atFirstChildOfTheLastRightNodeOrError =
3727 JoinNodesDeepWithTransaction(aHTMLEditor, MOZ_KnownLive(*mLeftContent),
3728 MOZ_KnownLive(*mRightContent));
3729 if (MOZ_UNLIKELY(atFirstChildOfTheLastRightNodeOrError.isErr())) {
3730 NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() failed");
3731 return atFirstChildOfTheLastRightNodeOrError.propagateErr();
3733 MOZ_ASSERT(atFirstChildOfTheLastRightNodeOrError.inspect().IsSet());
3734 trackStartOfRightContent.FlushAndStopTracking();
3735 if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
3736 NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
3737 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
3740 // If we're deleting selection (not replacing with new content) and the joined
3741 // point follows a text node, we should put caret to end of the preceding text
3742 // node because the other browsers insert following inputs into there.
3743 if (MayEditActionDeleteAroundCollapsedSelection(
3744 aHTMLEditor.GetEditAction())) {
3745 WSRunScanner scanner(&aEditingHost, startOfRightContent,
3746 BlockInlineCheck::UseComputedDisplayOutsideStyle);
3747 WSScanResult maybePreviousText =
3748 scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent);
3749 if (maybePreviousText.IsContentEditable() &&
3750 maybePreviousText.InVisibleOrCollapsibleCharacters()) {
3751 nsresult rv = aHTMLEditor.CollapseSelectionTo(
3752 maybePreviousText.Point<EditorRawDOMPoint>());
3753 if (NS_FAILED(rv)) {
3754 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
3755 return Err(rv);
3757 // If we prefer to use style in the previous line, we should forget
3758 // previous styles since the caret position has all styles which we want
3759 // to use with new content.
3760 if (backspaceInRightBlock) {
3761 aHTMLEditor.TopLevelEditSubActionDataRef()
3762 .mCachedPendingStyles->Clear();
3764 // And we don't want to keep extending a link at ex-end of the previous
3765 // paragraph.
3766 if (HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) {
3767 aHTMLEditor.mPendingStylesToApplyToNewContent
3768 ->ClearLinkAndItsSpecifiedStyle();
3770 return EditActionResult::HandledResult();
3774 // Otherwise, we should put caret at start of the right content.
3775 rv = aHTMLEditor.CollapseSelectionTo(
3776 atFirstChildOfTheLastRightNodeOrError.inspect());
3777 if (NS_FAILED(rv)) {
3778 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
3779 return Err(rv);
3781 return EditActionResult::HandledResult();
3784 Result<bool, nsresult>
3785 HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3786 ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure(
3787 const HTMLEditor& aHTMLEditor, nsRange& aRange,
3788 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
3789 const {
3790 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3792 AutoTArray<OwningNonNull<nsIContent>, 10> arrayOfTopChildren;
3793 DOMSubtreeIterator iter;
3794 nsresult rv = iter.Init(aRange);
3795 if (NS_FAILED(rv)) {
3796 NS_WARNING("DOMSubtreeIterator::Init() failed");
3797 return Err(rv);
3799 iter.AppendAllNodesToArray(arrayOfTopChildren);
3800 return NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
3801 aHTMLEditor, arrayOfTopChildren, aSelectionWasCollapsed);
3804 Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler::
3805 AutoBlockElementsJoiner::DeleteNodesEntirelyInRangeButKeepTableStructure(
3806 HTMLEditor& aHTMLEditor, nsRange& aRange,
3807 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) {
3808 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3810 // Build a list of direct child nodes in the range
3811 AutoTArray<OwningNonNull<nsIContent>, 10> arrayOfTopChildren;
3812 DOMSubtreeIterator iter;
3813 nsresult rv = iter.Init(aRange);
3814 if (NS_FAILED(rv)) {
3815 NS_WARNING("DOMSubtreeIterator::Init() failed");
3816 return Err(rv);
3818 iter.AppendAllNodesToArray(arrayOfTopChildren);
3820 // Now that we have the list, delete non-table elements
3821 bool needsToJoinLater =
3822 NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
3823 aHTMLEditor, arrayOfTopChildren, aSelectionWasCollapsed);
3824 for (auto& content : arrayOfTopChildren) {
3825 // XXX After here, the child contents in the array may have been moved
3826 // to somewhere or removed. We should handle it.
3828 // MOZ_KnownLive because 'arrayOfTopChildren' is guaranteed to
3829 // keep it alive.
3831 // Even with https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 fixed
3832 // this might need to stay, because 'arrayOfTopChildren' is not const,
3833 // so it's not obvious how to prove via static analysis that it won't
3834 // change and release us.
3835 nsresult rv =
3836 DeleteContentButKeepTableStructure(aHTMLEditor, MOZ_KnownLive(content));
3837 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
3838 return Err(NS_ERROR_EDITOR_DESTROYED);
3840 NS_WARNING_ASSERTION(
3841 NS_SUCCEEDED(rv),
3842 "AutoBlockElementsJoiner::DeleteContentButKeepTableStructure() failed, "
3843 "but ignored");
3845 return needsToJoinLater;
3848 bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3849 NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
3850 const HTMLEditor& aHTMLEditor,
3851 const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
3852 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
3853 const {
3854 // If original selection was collapsed, we need always to join the nodes.
3855 // XXX Why?
3856 if (aSelectionWasCollapsed ==
3857 AutoDeleteRangesHandler::SelectionWasCollapsed::No) {
3858 return true;
3860 // If something visible is deleted, no need to join. Visible means
3861 // all nodes except non-visible textnodes and breaks.
3862 if (aArrayOfContents.IsEmpty()) {
3863 return true;
3865 for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
3866 if (content->IsText()) {
3867 if (HTMLEditUtils::IsInVisibleTextFrames(aHTMLEditor.GetPresContext(),
3868 *content->AsText())) {
3869 return false;
3871 continue;
3873 // XXX If it's an element node, we should check whether it has visible
3874 // frames or not.
3875 if (!content->IsElement() ||
3876 HTMLEditUtils::IsEmptyNode(
3877 *content->AsElement(),
3878 {EmptyCheckOption::TreatSingleBRElementAsVisible,
3879 EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
3880 continue;
3882 if (!HTMLEditUtils::IsInvisibleBRElement(*content)) {
3883 return false;
3886 return true;
3889 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3890 DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange) {
3891 EditorDOMPoint rangeStart(aRange.StartRef());
3892 EditorDOMPoint rangeEnd(aRange.EndRef());
3893 if (rangeStart.IsInTextNode() && !rangeStart.IsEndOfContainer()) {
3894 // Delete to last character
3895 OwningNonNull<Text> textNode = *rangeStart.ContainerAs<Text>();
3896 Result<CaretPoint, nsresult> caretPointOrError =
3897 aHTMLEditor.DeleteTextWithTransaction(
3898 textNode, rangeStart.Offset(),
3899 rangeStart.GetContainer()->Length() - rangeStart.Offset());
3900 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
3901 NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
3902 return caretPointOrError.unwrapErr();
3904 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
3905 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
3906 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
3907 SuggestCaret::AndIgnoreTrivialError});
3908 if (NS_FAILED(rv)) {
3909 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
3910 return rv;
3912 NS_WARNING_ASSERTION(
3913 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
3914 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
3916 if (rangeEnd.IsInTextNode() && !rangeEnd.IsStartOfContainer()) {
3917 // Delete to first character
3918 OwningNonNull<Text> textNode = *rangeEnd.ContainerAs<Text>();
3919 Result<CaretPoint, nsresult> caretPointOrError =
3920 aHTMLEditor.DeleteTextWithTransaction(textNode, 0, rangeEnd.Offset());
3921 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
3922 NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
3923 return caretPointOrError.unwrapErr();
3925 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
3926 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
3927 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
3928 SuggestCaret::AndIgnoreTrivialError});
3929 if (NS_FAILED(rv)) {
3930 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
3931 return rv;
3933 NS_WARNING_ASSERTION(
3934 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
3935 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
3937 return NS_OK;
3940 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
3941 ComputeRangeToDeleteNonCollapsedRange(
3942 const HTMLEditor& aHTMLEditor,
3943 nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
3944 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
3945 const Element& aEditingHost) const {
3946 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
3947 MOZ_ASSERT(!aRangeToDelete.Collapsed());
3948 MOZ_ASSERT(mLeftContent);
3949 MOZ_ASSERT(mLeftContent->IsElement());
3950 MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf(
3951 mLeftContent));
3952 MOZ_ASSERT(mRightContent);
3953 MOZ_ASSERT(mRightContent->IsElement());
3954 MOZ_ASSERT(
3955 aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent));
3957 Result<bool, nsresult> result =
3958 ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure(
3959 aHTMLEditor, aRangeToDelete, aSelectionWasCollapsed);
3960 if (result.isErr()) {
3961 NS_WARNING(
3962 "AutoBlockElementsJoiner::"
3963 "ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure() "
3964 "failed");
3965 return result.unwrapErr();
3967 if (!result.unwrap()) {
3968 return NS_OK;
3971 AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
3972 *mRightContent);
3973 Result<bool, nsresult> canJoinThem =
3974 joiner.Prepare(aHTMLEditor, aEditingHost);
3975 if (canJoinThem.isErr()) {
3976 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
3977 return canJoinThem.unwrapErr();
3980 if (!canJoinThem.unwrap()) {
3981 return NS_SUCCESS_DOM_NO_OPERATION;
3984 if (!joiner.CanJoinBlocks()) {
3985 return NS_OK;
3988 nsresult rv = joiner.ComputeRangeToDelete(aHTMLEditor, EditorDOMPoint(),
3989 aRangeToDelete);
3990 NS_WARNING_ASSERTION(
3991 NS_SUCCEEDED(rv),
3992 "AutoInclusiveAncestorBlockElementsJoiner::ComputeRangeToDelete() "
3993 "failed");
3994 return rv;
3997 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
3998 AutoBlockElementsJoiner::HandleDeleteNonCollapsedRange(
3999 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
4000 nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
4001 AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
4002 const Element& aEditingHost) {
4003 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
4004 MOZ_ASSERT(!aRangeToDelete.Collapsed());
4005 MOZ_ASSERT(mDeleteRangesHandler);
4006 MOZ_ASSERT(mLeftContent);
4007 MOZ_ASSERT(mLeftContent->IsElement());
4008 MOZ_ASSERT(aRangeToDelete.GetStartContainer()->IsInclusiveDescendantOf(
4009 mLeftContent));
4010 MOZ_ASSERT(mRightContent);
4011 MOZ_ASSERT(mRightContent->IsElement());
4012 MOZ_ASSERT(
4013 aRangeToDelete.GetEndContainer()->IsInclusiveDescendantOf(mRightContent));
4015 const bool backspaceInRightBlock =
4016 aSelectionWasCollapsed == SelectionWasCollapsed::Yes &&
4017 nsIEditor::DirectionIsBackspace(aDirectionAndAmount);
4019 // Otherwise, delete every nodes in the range, then, clean up something.
4020 EditActionResult result = EditActionResult::IgnoredResult();
4021 EditorDOMPoint pointToPutCaret;
4022 while (true) {
4023 OwningNonNull<nsRange> rangeToDelete(aRangeToDelete);
4024 AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(),
4025 &rangeToDelete);
4027 Result<bool, nsresult> deleteResult =
4028 DeleteNodesEntirelyInRangeButKeepTableStructure(
4029 aHTMLEditor, rangeToDelete, aSelectionWasCollapsed);
4030 if (MOZ_UNLIKELY(deleteResult.isErr())) {
4031 NS_WARNING(
4032 "AutoBlockElementsJoiner::"
4033 "DeleteNodesEntirelyInRangeButKeepTableStructure() failed");
4034 return deleteResult.propagateErr();
4037 const bool joinInclusiveAncestorBlockElements = deleteResult.unwrap();
4039 // Check endpoints for possible text deletion. We can assume that if
4040 // text node is found, we can delete to end or to beginning as
4041 // appropriate, since the case where both sel endpoints in same text
4042 // node was already handled (we wouldn't be here)
4043 nsresult rv = DeleteTextAtStartAndEndOfRange(aHTMLEditor, rangeToDelete);
4044 if (NS_FAILED(rv)) {
4045 NS_WARNING(
4046 "AutoBlockElementsJoiner::DeleteTextAtStartAndEndOfRange() failed");
4047 return Err(rv);
4050 if (!joinInclusiveAncestorBlockElements) {
4051 break;
4054 AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent,
4055 *mRightContent);
4056 Result<bool, nsresult> canJoinThem =
4057 joiner.Prepare(aHTMLEditor, aEditingHost);
4058 if (canJoinThem.isErr()) {
4059 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed");
4060 return canJoinThem.propagateErr();
4063 // If we're joining blocks: if deleting forward the selection should
4064 // be collapsed to the end of the selection, if deleting backward the
4065 // selection should be collapsed to the beginning of the selection.
4066 // But if we're not joining then the selection should collapse to the
4067 // beginning of the selection if we're deleting forward, because the
4068 // end of the selection will still be in the next block. And same
4069 // thing for deleting backwards (selection should collapse to the end,
4070 // because the beginning will still be in the first block). See Bug
4071 // 507936.
4072 if (aDirectionAndAmount == nsIEditor::eNext) {
4073 aDirectionAndAmount = nsIEditor::ePrevious;
4074 } else {
4075 aDirectionAndAmount = nsIEditor::eNext;
4078 if (!canJoinThem.inspect()) {
4079 result.MarkAsCanceled();
4080 break;
4083 if (!joiner.CanJoinBlocks()) {
4084 break;
4087 Result<EditActionResult, nsresult> joinResult =
4088 joiner.Run(aHTMLEditor, aEditingHost);
4089 if (MOZ_UNLIKELY(joinResult.isErr())) {
4090 NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed");
4091 return joinResult;
4093 result |= joinResult.unwrap();
4094 #ifdef DEBUG
4095 if (joiner.ShouldDeleteLeafContentInstead()) {
4096 NS_ASSERTION(result.Ignored(),
4097 "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
4098 "returning ignored, but returned not ignored");
4099 } else {
4100 NS_ASSERTION(!result.Ignored(),
4101 "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` "
4102 "returning handled, but returned ignored");
4104 #endif // #ifdef DEBUG
4105 pointToPutCaret = joiner.PointRefToPutCaret();
4106 break;
4109 // If we're deleting selection (not replacing with new content) and
4110 // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we
4111 // should use it. Otherwise, we should keep the traditional behavior.
4112 if (result.Handled() && pointToPutCaret.IsSet()) {
4113 EditorDOMRange range(aRangeToDelete);
4114 nsresult rv =
4115 mDeleteRangesHandler->DeleteUnnecessaryNodes(aHTMLEditor, range);
4116 if (NS_FAILED(rv)) {
4117 NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed");
4118 return Err(rv);
4120 rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
4121 if (NS_FAILED(rv)) {
4122 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
4123 return Err(rv);
4125 // If we prefer to use style in the previous line, we should forget
4126 // previous styles since the caret position has all styles which we want
4127 // to use with new content.
4128 if (backspaceInRightBlock) {
4129 aHTMLEditor.TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
4131 // And we don't want to keep extending a link at ex-end of the previous
4132 // paragraph.
4133 if (HTMLEditor::GetLinkElement(pointToPutCaret.GetContainer())) {
4134 aHTMLEditor.mPendingStylesToApplyToNewContent
4135 ->ClearLinkAndItsSpecifiedStyle();
4137 return result;
4140 nsresult rv =
4141 mDeleteRangesHandler->DeleteUnnecessaryNodesAndCollapseSelection(
4142 aHTMLEditor, aDirectionAndAmount,
4143 EditorDOMPoint(aRangeToDelete.StartRef()),
4144 EditorDOMPoint(aRangeToDelete.EndRef()));
4145 if (NS_FAILED(rv)) {
4146 NS_WARNING(
4147 "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection() "
4148 "failed");
4149 return Err(rv);
4152 result.MarkAsHandled();
4153 return result;
4156 nsresult HTMLEditor::AutoDeleteRangesHandler::DeleteUnnecessaryNodes(
4157 HTMLEditor& aHTMLEditor, EditorDOMRange& aRange) {
4158 MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
4159 MOZ_ASSERT(EditorUtils::IsEditableContent(
4160 *aRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML));
4161 MOZ_ASSERT(EditorUtils::IsEditableContent(
4162 *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML));
4164 // If we're handling DnD, this is called to delete dragging item from the
4165 // tree. In this case, we should remove parent blocks if it becomes empty.
4166 if (aHTMLEditor.GetEditAction() == EditAction::eDrop ||
4167 aHTMLEditor.GetEditAction() == EditAction::eDeleteByDrag) {
4168 MOZ_ASSERT(aRange.Collapsed() ||
4169 (aRange.StartRef().GetContainer()->GetNextSibling() ==
4170 aRange.EndRef().GetContainer() &&
4171 aRange.StartRef().IsEndOfContainer() &&
4172 aRange.EndRef().IsStartOfContainer()));
4173 AutoTrackDOMRange trackRange(aHTMLEditor.RangeUpdaterRef(), &aRange);
4175 nsresult rv = DeleteParentBlocksWithTransactionIfEmpty(aHTMLEditor,
4176 aRange.StartRef());
4177 if (NS_FAILED(rv)) {
4178 NS_WARNING(
4179 "HTMLEditor::DeleteParentBlocksWithTransactionIfEmpty() failed");
4180 return rv;
4182 aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks =
4183 rv == NS_OK;
4184 // If we removed parent blocks, Selection should be collapsed at where
4185 // the most ancestor empty block has been.
4186 if (aHTMLEditor.TopLevelEditSubActionDataRef()
4187 .mDidDeleteEmptyParentBlocks) {
4188 return NS_OK;
4192 if (NS_WARN_IF(!aRange.IsInContentNodes()) ||
4193 NS_WARN_IF(!EditorUtils::IsEditableContent(
4194 *aRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML)) ||
4195 NS_WARN_IF(!EditorUtils::IsEditableContent(
4196 *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML))) {
4197 return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
4200 // We might have left only collapsed white-space in the start/end nodes
4201 AutoTrackDOMRange trackRange(aHTMLEditor.RangeUpdaterRef(), &aRange);
4203 OwningNonNull<nsIContent> startContainer =
4204 *aRange.StartRef().ContainerAs<nsIContent>();
4205 OwningNonNull<nsIContent> endContainer =
4206 *aRange.EndRef().ContainerAs<nsIContent>();
4207 nsresult rv =
4208 DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, startContainer);
4209 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
4210 return NS_ERROR_EDITOR_DESTROYED;
4212 NS_WARNING_ASSERTION(
4213 NS_SUCCEEDED(rv),
4214 "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() "
4215 "failed to remove start node, but ignored");
4216 // If we've not handled the selection end container, and it's still
4217 // editable, let's handle it.
4218 if (aRange.InSameContainer() ||
4219 !EditorUtils::IsEditableContent(
4220 *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
4221 return NS_OK;
4223 rv = DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, endContainer);
4224 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
4225 return NS_ERROR_EDITOR_DESTROYED;
4227 NS_WARNING_ASSERTION(
4228 NS_SUCCEEDED(rv),
4229 "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() "
4230 "failed to remove end node, but ignored");
4231 return NS_OK;
4234 nsresult
4235 HTMLEditor::AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection(
4236 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
4237 const EditorDOMPoint& aSelectionStartPoint,
4238 const EditorDOMPoint& aSelectionEndPoint) {
4239 EditorDOMRange range(aSelectionStartPoint, aSelectionEndPoint);
4240 nsresult rv = DeleteUnnecessaryNodes(aHTMLEditor, range);
4241 if (NS_FAILED(rv)) {
4242 NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed");
4243 return rv;
4246 if (aHTMLEditor.GetEditAction() == EditAction::eDrop ||
4247 aHTMLEditor.GetEditAction() == EditAction::eDeleteByDrag) {
4248 // If we removed parent blocks, Selection should be collapsed at where
4249 // the most ancestor empty block has been.
4250 // XXX I think that if the range is not in active editing host, we should
4251 // not try to collapse selection here.
4252 if (aHTMLEditor.TopLevelEditSubActionDataRef()
4253 .mDidDeleteEmptyParentBlocks) {
4254 nsresult rv = aHTMLEditor.CollapseSelectionTo(range.StartRef());
4255 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
4256 "EditorBase::CollapseSelectionTo() failed");
4257 return rv;
4261 rv = aHTMLEditor.CollapseSelectionTo(
4262 aDirectionAndAmount == nsIEditor::ePrevious ? range.EndRef()
4263 : range.StartRef());
4264 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
4265 "EditorBase::CollapseSelectionTo() failed");
4266 return rv;
4269 nsresult
4270 HTMLEditor::AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode(
4271 HTMLEditor& aHTMLEditor, nsIContent& aContent) {
4272 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
4274 Text* text = aContent.GetAsText();
4275 if (!text) {
4276 return NS_OK;
4279 if (!HTMLEditUtils::IsRemovableFromParentNode(*text) ||
4280 HTMLEditUtils::IsVisibleTextNode(*text)) {
4281 return NS_OK;
4284 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContent);
4285 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
4286 "EditorBase::DeleteNodeWithTransaction() failed");
4287 return rv;
4290 nsresult
4291 HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty(
4292 HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) {
4293 MOZ_ASSERT(aPoint.IsSet());
4294 MOZ_ASSERT(aHTMLEditor.mPlaceholderBatch);
4296 // First, check there is visible contents before the point in current block.
4297 RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost();
4298 WSRunScanner wsScannerForPoint(
4299 editingHost, aPoint, BlockInlineCheck::UseComputedDisplayOutsideStyle);
4300 if (!wsScannerForPoint.StartsFromCurrentBlockBoundary()) {
4301 // If there is visible node before the point, we shouldn't remove the
4302 // parent block.
4303 return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
4305 if (NS_WARN_IF(!wsScannerForPoint.GetStartReasonContent()) ||
4306 NS_WARN_IF(!wsScannerForPoint.GetStartReasonContent()->GetParentNode())) {
4307 return NS_ERROR_FAILURE;
4309 if (editingHost == wsScannerForPoint.GetStartReasonContent()) {
4310 // If we reach editing host, there is no parent blocks which can be removed.
4311 return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
4313 if (HTMLEditUtils::IsTableCellOrCaption(
4314 *wsScannerForPoint.GetStartReasonContent())) {
4315 // If we reach a <td>, <th> or <caption>, we shouldn't remove it even
4316 // becomes empty because removing such element changes the structure of
4317 // the <table>.
4318 return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
4321 // Next, check there is visible contents after the point in current block.
4322 WSScanResult forwardScanFromPointResult =
4323 wsScannerForPoint.ScanNextVisibleNodeOrBlockBoundaryFrom(aPoint);
4324 if (forwardScanFromPointResult.Failed()) {
4325 NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed");
4326 return NS_ERROR_FAILURE;
4328 if (forwardScanFromPointResult.ReachedBRElement()) {
4329 // XXX In my understanding, this is odd. The end reason may not be
4330 // same as the reached <br> element because the equality is
4331 // guaranteed only when ReachedCurrentBlockBoundary() returns true.
4332 // However, looks like that this code assumes that
4333 // GetEndReasonContent() returns the (or a) <br> element.
4334 NS_ASSERTION(wsScannerForPoint.GetEndReasonContent() ==
4335 forwardScanFromPointResult.BRElementPtr(),
4336 "End reason is not the reached <br> element");
4337 // If the <br> element is visible, we shouldn't remove the parent block.
4338 if (HTMLEditUtils::IsVisibleBRElement(
4339 *wsScannerForPoint.GetEndReasonContent())) {
4340 return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
4342 if (wsScannerForPoint.GetEndReasonContent()->GetNextSibling()) {
4343 WSScanResult scanResult =
4344 WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
4345 editingHost,
4346 EditorRawDOMPoint::After(
4347 *wsScannerForPoint.GetEndReasonContent()),
4348 BlockInlineCheck::UseComputedDisplayOutsideStyle);
4349 if (scanResult.Failed()) {
4350 NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
4351 return NS_ERROR_FAILURE;
4353 if (!scanResult.ReachedCurrentBlockBoundary()) {
4354 // If we couldn't reach the block's end after the invisible <br>,
4355 // that means that there is visible content.
4356 return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
4359 } else if (!forwardScanFromPointResult.ReachedCurrentBlockBoundary()) {
4360 // If we couldn't reach the block's end, the block has visible content.
4361 return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
4364 // Delete the parent block.
4365 EditorDOMPoint nextPoint(
4366 wsScannerForPoint.GetStartReasonContent()->GetParentNode(), 0);
4367 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
4368 MOZ_KnownLive(*wsScannerForPoint.GetStartReasonContent()));
4369 if (NS_FAILED(rv)) {
4370 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
4371 return rv;
4373 // If we reach editing host, return NS_OK.
4374 if (nextPoint.GetContainer() == editingHost) {
4375 return NS_OK;
4378 // Otherwise, we need to check whether we're still in empty block or not.
4380 // If we have mutation event listeners, the next point is now outside of
4381 // editing host or editing hos has been changed.
4382 if (aHTMLEditor.MayHaveMutationEventListeners(
4383 NS_EVENT_BITS_MUTATION_NODEREMOVED |
4384 NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
4385 NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED)) {
4386 Element* newEditingHost = aHTMLEditor.ComputeEditingHost();
4387 if (NS_WARN_IF(!newEditingHost) ||
4388 NS_WARN_IF(newEditingHost != editingHost)) {
4389 return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
4391 if (NS_WARN_IF(!EditorUtils::IsDescendantOf(*nextPoint.GetContainer(),
4392 *newEditingHost))) {
4393 return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
4397 rv = DeleteParentBlocksWithTransactionIfEmpty(aHTMLEditor, nextPoint);
4398 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
4399 "AutoDeleteRangesHandler::"
4400 "DeleteParentBlocksWithTransactionIfEmpty() failed");
4401 return rv;
4404 nsresult
4405 HTMLEditor::AutoDeleteRangesHandler::ComputeRangeToDeleteRangeWithTransaction(
4406 const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
4407 nsRange& aRangeToDelete, const Element& aEditingHost) const {
4408 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
4410 const EditorBase::HowToHandleCollapsedRange howToHandleCollapsedRange =
4411 EditorBase::HowToHandleCollapsedRangeFor(aDirectionAndAmount);
4412 if (MOZ_UNLIKELY(aRangeToDelete.Collapsed() &&
4413 howToHandleCollapsedRange ==
4414 EditorBase::HowToHandleCollapsedRange::Ignore)) {
4415 return NS_SUCCESS_DOM_NO_OPERATION;
4418 // If it's not collapsed, `DeleteRangeTransaction::Create()` will be called
4419 // with it and `DeleteRangeTransaction` won't modify the range.
4420 if (!aRangeToDelete.Collapsed()) {
4421 return NS_OK;
4424 const auto ExtendRangeToSelectCharacterForward =
4425 [](nsRange& aRange, const EditorRawDOMPointInText& aCaretPoint) -> void {
4426 const nsTextFragment& textFragment =
4427 aCaretPoint.ContainerAs<Text>()->TextFragment();
4428 if (!textFragment.GetLength()) {
4429 return;
4431 if (textFragment.IsHighSurrogateFollowedByLowSurrogateAt(
4432 aCaretPoint.Offset())) {
4433 DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
4434 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset(),
4435 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() + 2);
4436 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
4437 "nsRange::SetStartAndEnd() failed");
4438 return;
4440 DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
4441 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset(),
4442 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() + 1);
4443 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
4444 "nsRange::SetStartAndEnd() failed");
4446 const auto ExtendRangeToSelectCharacterBackward =
4447 [](nsRange& aRange, const EditorRawDOMPointInText& aCaretPoint) -> void {
4448 if (aCaretPoint.IsStartOfContainer()) {
4449 return;
4451 const nsTextFragment& textFragment =
4452 aCaretPoint.ContainerAs<Text>()->TextFragment();
4453 if (!textFragment.GetLength()) {
4454 return;
4456 if (textFragment.IsLowSurrogateFollowingHighSurrogateAt(
4457 aCaretPoint.Offset() - 1)) {
4458 DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
4459 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() - 2,
4460 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset());
4461 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
4462 "nsRange::SetStartAndEnd() failed");
4463 return;
4465 DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd(
4466 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() - 1,
4467 aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset());
4468 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
4469 "nsRange::SetStartAndEnd() failed");
4472 // In the other cases, `EditorBase::CreateTransactionForCollapsedRange()`
4473 // will handle the collapsed range.
4474 EditorRawDOMPoint caretPoint(aRangeToDelete.StartRef());
4475 if (howToHandleCollapsedRange ==
4476 EditorBase::HowToHandleCollapsedRange::ExtendBackward &&
4477 caretPoint.IsStartOfContainer()) {
4478 nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent(
4479 *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode},
4480 BlockInlineCheck::Unused, &aEditingHost);
4481 if (!previousEditableContent) {
4482 return NS_OK;
4484 if (!previousEditableContent->IsText()) {
4485 IgnoredErrorResult ignoredError;
4486 aRangeToDelete.SelectNode(*previousEditableContent, ignoredError);
4487 NS_WARNING_ASSERTION(!ignoredError.Failed(),
4488 "nsRange::SelectNode() failed");
4489 return NS_OK;
4492 ExtendRangeToSelectCharacterBackward(
4493 aRangeToDelete,
4494 EditorRawDOMPointInText::AtEndOf(*previousEditableContent->AsText()));
4495 return NS_OK;
4498 if (howToHandleCollapsedRange ==
4499 EditorBase::HowToHandleCollapsedRange::ExtendForward &&
4500 caretPoint.IsEndOfContainer()) {
4501 nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent(
4502 *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode},
4503 BlockInlineCheck::Unused, &aEditingHost);
4504 if (!nextEditableContent) {
4505 return NS_OK;
4508 if (!nextEditableContent->IsText()) {
4509 IgnoredErrorResult ignoredError;
4510 aRangeToDelete.SelectNode(*nextEditableContent, ignoredError);
4511 NS_WARNING_ASSERTION(!ignoredError.Failed(),
4512 "nsRange::SelectNode() failed");
4513 return NS_OK;
4516 ExtendRangeToSelectCharacterForward(
4517 aRangeToDelete,
4518 EditorRawDOMPointInText(nextEditableContent->AsText(), 0));
4519 return NS_OK;
4522 if (caretPoint.IsInTextNode()) {
4523 if (howToHandleCollapsedRange ==
4524 EditorBase::HowToHandleCollapsedRange::ExtendBackward) {
4525 ExtendRangeToSelectCharacterBackward(
4526 aRangeToDelete,
4527 EditorRawDOMPointInText(caretPoint.ContainerAs<Text>(),
4528 caretPoint.Offset()));
4529 return NS_OK;
4531 ExtendRangeToSelectCharacterForward(
4532 aRangeToDelete, EditorRawDOMPointInText(caretPoint.ContainerAs<Text>(),
4533 caretPoint.Offset()));
4534 return NS_OK;
4537 nsIContent* editableContent =
4538 howToHandleCollapsedRange ==
4539 EditorBase::HowToHandleCollapsedRange::ExtendBackward
4540 ? HTMLEditUtils::GetPreviousContent(
4541 caretPoint, {WalkTreeOption::IgnoreNonEditableNode},
4542 BlockInlineCheck::Unused, &aEditingHost)
4543 : HTMLEditUtils::GetNextContent(
4544 caretPoint, {WalkTreeOption::IgnoreNonEditableNode},
4545 BlockInlineCheck::Unused, &aEditingHost);
4546 if (!editableContent) {
4547 return NS_OK;
4549 while (editableContent && editableContent->IsCharacterData() &&
4550 !editableContent->Length()) {
4551 editableContent =
4552 howToHandleCollapsedRange ==
4553 EditorBase::HowToHandleCollapsedRange::ExtendBackward
4554 ? HTMLEditUtils::GetPreviousContent(
4555 *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
4556 BlockInlineCheck::Unused, &aEditingHost)
4557 : HTMLEditUtils::GetNextContent(
4558 *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
4559 BlockInlineCheck::Unused, &aEditingHost);
4561 if (!editableContent) {
4562 return NS_OK;
4565 if (!editableContent->IsText()) {
4566 IgnoredErrorResult ignoredError;
4567 aRangeToDelete.SelectNode(*editableContent, ignoredError);
4568 NS_WARNING_ASSERTION(!ignoredError.Failed(),
4569 "nsRange::SelectNode() failed, but ignored");
4570 return NS_OK;
4573 if (howToHandleCollapsedRange ==
4574 EditorBase::HowToHandleCollapsedRange::ExtendBackward) {
4575 ExtendRangeToSelectCharacterBackward(
4576 aRangeToDelete,
4577 EditorRawDOMPointInText::AtEndOf(*editableContent->AsText()));
4578 return NS_OK;
4580 ExtendRangeToSelectCharacterForward(
4581 aRangeToDelete, EditorRawDOMPointInText(editableContent->AsText(), 0));
4583 return NS_OK;
4586 template <typename EditorDOMPointType>
4587 Result<CaretPoint, nsresult> HTMLEditor::DeleteTextAndTextNodesWithTransaction(
4588 const EditorDOMPointType& aStartPoint, const EditorDOMPointType& aEndPoint,
4589 TreatEmptyTextNodes aTreatEmptyTextNodes) {
4590 if (NS_WARN_IF(!aStartPoint.IsSet()) || NS_WARN_IF(!aEndPoint.IsSet())) {
4591 return Err(NS_ERROR_INVALID_ARG);
4594 // MOOSE: this routine needs to be modified to preserve the integrity of the
4595 // wsFragment info.
4597 if (aStartPoint == aEndPoint) {
4598 // Nothing to delete
4599 return CaretPoint(EditorDOMPoint());
4602 RefPtr<Element> editingHost = ComputeEditingHost();
4603 auto DeleteEmptyContentNodeWithTransaction =
4604 [this, &aTreatEmptyTextNodes, &editingHost](nsIContent& aContent)
4605 MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> nsresult {
4606 OwningNonNull<nsIContent> nodeToRemove = aContent;
4607 if (aTreatEmptyTextNodes ==
4608 TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors) {
4609 Element* emptyParentElementToRemove =
4610 HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
4611 nodeToRemove, BlockInlineCheck::UseComputedDisplayOutsideStyle,
4612 editingHost);
4613 if (emptyParentElementToRemove) {
4614 nodeToRemove = *emptyParentElementToRemove;
4617 nsresult rv = DeleteNodeWithTransaction(nodeToRemove);
4618 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
4619 "EditorBase::DeleteNodeWithTransaction() failed");
4620 return rv;
4623 if (aStartPoint.GetContainer() == aEndPoint.GetContainer() &&
4624 aStartPoint.IsInTextNode()) {
4625 if (aTreatEmptyTextNodes !=
4626 TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries &&
4627 aStartPoint.IsStartOfContainer() && aEndPoint.IsEndOfContainer()) {
4628 nsresult rv = DeleteEmptyContentNodeWithTransaction(
4629 MOZ_KnownLive(*aStartPoint.template ContainerAs<Text>()));
4630 if (NS_FAILED(rv)) {
4631 NS_WARNING("deleteEmptyContentNodeWithTransaction() failed");
4632 return Err(rv);
4634 return CaretPoint(EditorDOMPoint());
4636 RefPtr<Text> textNode = aStartPoint.template ContainerAs<Text>();
4637 Result<CaretPoint, nsresult> caretPointOrError =
4638 DeleteTextWithTransaction(*textNode, aStartPoint.Offset(),
4639 aEndPoint.Offset() - aStartPoint.Offset());
4640 NS_WARNING_ASSERTION(caretPointOrError.isOk(),
4641 "HTMLEditor::DeleteTextWithTransaction() failed");
4642 return caretPointOrError;
4645 RefPtr<nsRange> range =
4646 nsRange::Create(aStartPoint.ToRawRangeBoundary(),
4647 aEndPoint.ToRawRangeBoundary(), IgnoreErrors());
4648 if (!range) {
4649 NS_WARNING("nsRange::Create() failed");
4650 return Err(NS_ERROR_FAILURE);
4653 // Collect editable text nodes in the given range.
4654 AutoTArray<OwningNonNull<Text>, 16> arrayOfTextNodes;
4655 DOMIterator iter;
4656 if (NS_FAILED(iter.Init(*range))) {
4657 return CaretPoint(EditorDOMPoint()); // Nothing to delete in the range.
4659 iter.AppendNodesToArray(
4660 +[](nsINode& aNode, void*) {
4661 MOZ_ASSERT(aNode.IsText());
4662 return HTMLEditUtils::IsSimplyEditableNode(aNode);
4664 arrayOfTextNodes);
4665 EditorDOMPoint pointToPutCaret;
4666 for (OwningNonNull<Text>& textNode : arrayOfTextNodes) {
4667 if (textNode == aStartPoint.GetContainer()) {
4668 if (aStartPoint.IsEndOfContainer()) {
4669 continue;
4671 if (aStartPoint.IsStartOfContainer() &&
4672 aTreatEmptyTextNodes !=
4673 TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) {
4674 AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
4675 &pointToPutCaret);
4676 nsresult rv = DeleteEmptyContentNodeWithTransaction(
4677 MOZ_KnownLive(*aStartPoint.template ContainerAs<Text>()));
4678 if (NS_FAILED(rv)) {
4679 NS_WARNING("DeleteEmptyContentNodeWithTransaction() failed");
4680 return Err(rv);
4682 continue;
4684 AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
4685 &pointToPutCaret);
4686 Result<CaretPoint, nsresult> caretPointOrError =
4687 DeleteTextWithTransaction(MOZ_KnownLive(textNode),
4688 aStartPoint.Offset(),
4689 textNode->Length() - aStartPoint.Offset());
4690 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
4691 NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
4692 return caretPointOrError;
4694 trackPointToPutCaret.FlushAndStopTracking();
4695 caretPointOrError.unwrap().MoveCaretPointTo(
4696 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
4697 continue;
4700 if (textNode == aEndPoint.GetContainer()) {
4701 if (aEndPoint.IsStartOfContainer()) {
4702 break;
4704 if (aEndPoint.IsEndOfContainer() &&
4705 aTreatEmptyTextNodes !=
4706 TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) {
4707 AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
4708 &pointToPutCaret);
4709 nsresult rv = DeleteEmptyContentNodeWithTransaction(
4710 MOZ_KnownLive(*aEndPoint.template ContainerAs<Text>()));
4711 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
4712 "DeleteEmptyContentNodeWithTransaction() failed");
4713 return Err(rv);
4715 AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
4716 &pointToPutCaret);
4717 Result<CaretPoint, nsresult> caretPointOrError =
4718 DeleteTextWithTransaction(MOZ_KnownLive(textNode), 0,
4719 aEndPoint.Offset());
4720 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
4721 NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
4722 return caretPointOrError;
4724 trackPointToPutCaret.FlushAndStopTracking();
4725 caretPointOrError.unwrap().MoveCaretPointTo(
4726 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
4727 return CaretPoint(pointToPutCaret);
4730 nsresult rv =
4731 DeleteEmptyContentNodeWithTransaction(MOZ_KnownLive(textNode));
4732 if (NS_FAILED(rv)) {
4733 NS_WARNING("DeleteEmptyContentNodeWithTransaction() failed");
4734 return Err(rv);
4738 return CaretPoint(pointToPutCaret);
4741 Result<EditorDOMPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
4742 AutoBlockElementsJoiner::JoinNodesDeepWithTransaction(
4743 HTMLEditor& aHTMLEditor, nsIContent& aLeftContent,
4744 nsIContent& aRightContent) {
4745 // While the rightmost children and their descendants of the left node match
4746 // the leftmost children and their descendants of the right node, join them
4747 // up.
4749 nsCOMPtr<nsIContent> leftContentToJoin = &aLeftContent;
4750 nsCOMPtr<nsIContent> rightContentToJoin = &aRightContent;
4751 nsCOMPtr<nsINode> parentNode = aRightContent.GetParentNode();
4753 EditorDOMPoint ret;
4754 while (leftContentToJoin && rightContentToJoin && parentNode &&
4755 HTMLEditUtils::CanContentsBeJoined(*leftContentToJoin,
4756 *rightContentToJoin)) {
4757 // Do the join
4758 Result<JoinNodesResult, nsresult> joinNodesResult =
4759 aHTMLEditor.JoinNodesWithTransaction(*leftContentToJoin,
4760 *rightContentToJoin);
4761 if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
4762 NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
4763 return joinNodesResult.propagateErr();
4766 ret = joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
4767 if (NS_WARN_IF(!ret.IsSet())) {
4768 return Err(NS_ERROR_FAILURE);
4771 if (parentNode->IsText()) {
4772 // We've joined all the way down to text nodes, we're done!
4773 return ret;
4776 // Get new left and right nodes, and begin anew
4777 rightContentToJoin = ret.GetCurrentChildAtOffset();
4778 if (rightContentToJoin) {
4779 leftContentToJoin = rightContentToJoin->GetPreviousSibling();
4780 } else {
4781 leftContentToJoin = nullptr;
4784 // Skip over non-editable nodes
4785 while (leftContentToJoin && !EditorUtils::IsEditableContent(
4786 *leftContentToJoin, EditorType::HTML)) {
4787 leftContentToJoin = leftContentToJoin->GetPreviousSibling();
4789 if (!leftContentToJoin) {
4790 return ret;
4793 while (rightContentToJoin && !EditorUtils::IsEditableContent(
4794 *rightContentToJoin, EditorType::HTML)) {
4795 rightContentToJoin = rightContentToJoin->GetNextSibling();
4797 if (!rightContentToJoin) {
4798 return ret;
4802 if (!ret.IsSet()) {
4803 NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() joined no contents");
4804 return Err(NS_ERROR_FAILURE);
4806 return ret;
4809 Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler::
4810 AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner::Prepare(
4811 const HTMLEditor& aHTMLEditor, const Element& aEditingHost) {
4812 mLeftBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
4813 mInclusiveDescendantOfLeftBlockElement,
4814 HTMLEditUtils::ClosestEditableBlockElementExceptHRElement,
4815 BlockInlineCheck::UseComputedDisplayOutsideStyle);
4816 mRightBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
4817 mInclusiveDescendantOfRightBlockElement,
4818 HTMLEditUtils::ClosestEditableBlockElementExceptHRElement,
4819 BlockInlineCheck::UseComputedDisplayOutsideStyle);
4821 if (NS_WARN_IF(!IsSet())) {
4822 mCanJoinBlocks = false;
4823 return Err(NS_ERROR_UNEXPECTED);
4826 // Don't join the blocks if both of them are basic structure of the HTML
4827 // document (Note that `<body>` can be joined with its children).
4828 if (mLeftBlockElement->IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head,
4829 nsGkAtoms::body) &&
4830 mRightBlockElement->IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head,
4831 nsGkAtoms::body)) {
4832 mCanJoinBlocks = false;
4833 return false;
4836 if (HTMLEditUtils::IsAnyTableElement(mLeftBlockElement) ||
4837 HTMLEditUtils::IsAnyTableElement(mRightBlockElement)) {
4838 // Do not try to merge table elements, cancel the deletion.
4839 mCanJoinBlocks = false;
4840 return false;
4843 // Bail if both blocks the same
4844 if (IsSameBlockElement()) {
4845 mCanJoinBlocks = true; // XXX Anyway, Run() will ingore this case.
4846 mFallbackToDeleteLeafContent = true;
4847 return true;
4850 // Joining a list item to its parent is a NOP.
4851 if (HTMLEditUtils::IsAnyListElement(mLeftBlockElement) &&
4852 HTMLEditUtils::IsListItem(mRightBlockElement) &&
4853 mRightBlockElement->GetParentNode() == mLeftBlockElement) {
4854 mCanJoinBlocks = false;
4855 return true;
4858 // Special rule here: if we are trying to join list items, and they are in
4859 // different lists, join the lists instead.
4860 if (HTMLEditUtils::IsListItem(mLeftBlockElement) &&
4861 HTMLEditUtils::IsListItem(mRightBlockElement)) {
4862 // XXX leftListElement and/or rightListElement may be not list elements.
4863 Element* leftListElement = mLeftBlockElement->GetParentElement();
4864 Element* rightListElement = mRightBlockElement->GetParentElement();
4865 EditorDOMPoint atChildInBlock;
4866 if (leftListElement && rightListElement &&
4867 leftListElement != rightListElement &&
4868 !EditorUtils::IsDescendantOf(*leftListElement, *mRightBlockElement,
4869 &atChildInBlock) &&
4870 !EditorUtils::IsDescendantOf(*rightListElement, *mLeftBlockElement,
4871 &atChildInBlock)) {
4872 // There are some special complications if the lists are descendants of
4873 // the other lists' items. Note that it is okay for them to be
4874 // descendants of the other lists themselves, which is the usual case for
4875 // sublists in our implementation.
4876 MOZ_DIAGNOSTIC_ASSERT(!atChildInBlock.IsSet());
4877 mLeftBlockElement = leftListElement;
4878 mRightBlockElement = rightListElement;
4879 mNewListElementTagNameOfRightListElement =
4880 Some(leftListElement->NodeInfo()->NameAtom());
4884 if (!EditorUtils::IsDescendantOf(*mLeftBlockElement, *mRightBlockElement,
4885 &mPointContainingTheOtherBlockElement)) {
4886 Unused << EditorUtils::IsDescendantOf(
4887 *mRightBlockElement, *mLeftBlockElement,
4888 &mPointContainingTheOtherBlockElement);
4891 if (mPointContainingTheOtherBlockElement.GetContainer() ==
4892 mRightBlockElement) {
4893 mPrecedingInvisibleBRElement =
4894 WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
4895 aHTMLEditor.ComputeEditingHost(),
4896 EditorDOMPoint::AtEndOf(mLeftBlockElement),
4897 BlockInlineCheck::UseComputedDisplayOutsideStyle);
4898 // `WhiteSpaceVisibilityKeeper::
4899 // MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`
4900 // returns ignored when:
4901 // - No preceding invisible `<br>` element and
4902 // - mNewListElementTagNameOfRightListElement is nothing and
4903 // - There is no content to move from right block element.
4904 if (!mPrecedingInvisibleBRElement) {
4905 if (CanMergeLeftAndRightBlockElements()) {
4906 // Always marked as handled in this case.
4907 mFallbackToDeleteLeafContent = false;
4908 } else {
4909 // Marked as handled only when it actually moves a content node.
4910 Result<bool, nsresult> firstLineHasContent =
4911 AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
4912 mPointContainingTheOtherBlockElement
4913 .NextPoint<EditorDOMPoint>(),
4914 aEditingHost);
4915 mFallbackToDeleteLeafContent =
4916 firstLineHasContent.isOk() && !firstLineHasContent.inspect();
4918 } else {
4919 // Marked as handled when deleting the invisible `<br>` element.
4920 mFallbackToDeleteLeafContent = false;
4922 } else if (mPointContainingTheOtherBlockElement.GetContainer() ==
4923 mLeftBlockElement) {
4924 mPrecedingInvisibleBRElement =
4925 WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
4926 aHTMLEditor.ComputeEditingHost(),
4927 mPointContainingTheOtherBlockElement,
4928 BlockInlineCheck::UseComputedDisplayOutsideStyle);
4929 // `WhiteSpaceVisibilityKeeper::
4930 // MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()`
4931 // returns ignored when:
4932 // - No preceding invisible `<br>` element and
4933 // - mNewListElementTagNameOfRightListElement is some and
4934 // - The right block element has no children
4935 // or,
4936 // - No preceding invisible `<br>` element and
4937 // - mNewListElementTagNameOfRightListElement is nothing and
4938 // - There is no content to move from right block element.
4939 if (!mPrecedingInvisibleBRElement) {
4940 if (CanMergeLeftAndRightBlockElements()) {
4941 // Marked as handled only when it actualy moves a content node.
4942 Result<bool, nsresult> rightBlockHasContent =
4943 aHTMLEditor.CanMoveChildren(*mRightBlockElement,
4944 *mLeftBlockElement);
4945 mFallbackToDeleteLeafContent =
4946 rightBlockHasContent.isOk() && !rightBlockHasContent.inspect();
4947 } else {
4948 // Marked as handled only when it actually moves a content node.
4949 Result<bool, nsresult> firstLineHasContent =
4950 AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
4951 EditorDOMPoint(mRightBlockElement, 0u), aEditingHost);
4952 mFallbackToDeleteLeafContent =
4953 firstLineHasContent.isOk() && !firstLineHasContent.inspect();
4955 } else {
4956 // Marked as handled when deleting the invisible `<br>` element.
4957 mFallbackToDeleteLeafContent = false;
4959 } else {
4960 mPrecedingInvisibleBRElement =
4961 WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound(
4962 aHTMLEditor.ComputeEditingHost(),
4963 EditorDOMPoint::AtEndOf(mLeftBlockElement),
4964 BlockInlineCheck::UseComputedDisplayOutsideStyle);
4965 // `WhiteSpaceVisibilityKeeper::
4966 // MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` always
4967 // return "handled".
4968 mFallbackToDeleteLeafContent = false;
4971 mCanJoinBlocks = true;
4972 return true;
4975 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
4976 AutoInclusiveAncestorBlockElementsJoiner::ComputeRangeToDelete(
4977 const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
4978 nsRange& aRangeToDelete) const {
4979 MOZ_ASSERT(mLeftBlockElement);
4980 MOZ_ASSERT(mRightBlockElement);
4982 if (IsSameBlockElement()) {
4983 if (!aCaretPoint.IsSet()) {
4984 return NS_OK; // The ranges are not collapsed, keep them as-is.
4986 nsresult rv = aRangeToDelete.CollapseTo(aCaretPoint.ToRawRangeBoundary());
4987 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::CollapseTo() failed");
4988 return rv;
4991 EditorDOMPoint pointContainingTheOtherBlock;
4992 if (!EditorUtils::IsDescendantOf(*mLeftBlockElement, *mRightBlockElement,
4993 &pointContainingTheOtherBlock)) {
4994 Unused << EditorUtils::IsDescendantOf(
4995 *mRightBlockElement, *mLeftBlockElement, &pointContainingTheOtherBlock);
4997 EditorDOMRange range =
4998 WSRunScanner::GetRangeForDeletingBlockElementBoundaries(
4999 aHTMLEditor, *mLeftBlockElement, *mRightBlockElement,
5000 pointContainingTheOtherBlock);
5001 if (!range.IsPositioned()) {
5002 NS_WARNING(
5003 "WSRunScanner::GetRangeForDeletingBlockElementBoundaries() failed");
5004 return NS_ERROR_FAILURE;
5006 if (!aCaretPoint.IsSet()) {
5007 // Don't shrink the original range.
5008 bool noNeedToChangeStart = false;
5009 const EditorDOMPoint atStart(aRangeToDelete.StartRef());
5010 if (atStart.IsBefore(range.StartRef())) {
5011 // If the range starts from end of a container, and computed block
5012 // boundaries range starts from an invisible `<br>` element, we
5013 // may need to shrink the range.
5014 Element* editingHost = aHTMLEditor.ComputeEditingHost();
5015 NS_WARNING_ASSERTION(editingHost, "There was no editing host");
5016 nsIContent* nextContent =
5017 atStart.IsEndOfContainer() && range.StartRef().GetChild() &&
5018 HTMLEditUtils::IsInvisibleBRElement(
5019 *range.StartRef().GetChild())
5020 ? HTMLEditUtils::GetNextContent(
5021 *atStart.ContainerAs<nsIContent>(),
5022 {WalkTreeOption::IgnoreDataNodeExceptText,
5023 WalkTreeOption::StopAtBlockBoundary},
5024 BlockInlineCheck::UseComputedDisplayOutsideStyle,
5025 editingHost)
5026 : nullptr;
5027 if (!nextContent || nextContent != range.StartRef().GetChild()) {
5028 noNeedToChangeStart = true;
5029 range.SetStart(EditorRawDOMPoint(aRangeToDelete.StartRef()));
5032 if (range.EndRef().IsBefore(EditorRawDOMPoint(aRangeToDelete.EndRef()))) {
5033 if (noNeedToChangeStart) {
5034 return NS_OK; // We don't need to modify the range.
5036 range.SetEnd(EditorRawDOMPoint(aRangeToDelete.EndRef()));
5039 nsresult rv =
5040 aRangeToDelete.SetStartAndEnd(range.StartRef().ToRawRangeBoundary(),
5041 range.EndRef().ToRawRangeBoundary());
5042 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
5043 "AutoRangeArray::SetStartAndEnd() failed");
5044 return rv;
5047 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
5048 AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner::Run(
5049 HTMLEditor& aHTMLEditor, const Element& aEditingHost) {
5050 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
5051 MOZ_ASSERT(mLeftBlockElement);
5052 MOZ_ASSERT(mRightBlockElement);
5054 if (IsSameBlockElement()) {
5055 return EditActionResult::IgnoredResult();
5058 if (!mCanJoinBlocks) {
5059 return EditActionResult::HandledResult();
5062 EditorDOMPoint startOfRightContent;
5064 // If the left block element is in the right block element, move the hard
5065 // line including the right block element to end of the left block.
5066 // However, if we are merging list elements, we don't join them.
5067 Result<EditActionResult, nsresult> result(NS_ERROR_NOT_INITIALIZED);
5068 if (mPointContainingTheOtherBlockElement.GetContainer() ==
5069 mRightBlockElement) {
5070 startOfRightContent = mPointContainingTheOtherBlockElement.NextPoint();
5071 if (Element* element = startOfRightContent.GetChildAs<Element>()) {
5072 startOfRightContent =
5073 HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
5074 *element);
5076 AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(),
5077 &startOfRightContent);
5078 result = WhiteSpaceVisibilityKeeper::
5079 MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement(
5080 aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement),
5081 MOZ_KnownLive(*mRightBlockElement),
5082 mPointContainingTheOtherBlockElement,
5083 mNewListElementTagNameOfRightListElement,
5084 MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost);
5085 if (MOZ_UNLIKELY(result.isErr())) {
5086 NS_WARNING(
5087 "WhiteSpaceVisibilityKeeper::"
5088 "MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement() "
5089 "failed");
5090 return result;
5092 if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
5093 NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
5094 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
5098 // If the right block element is in the left block element:
5099 // - move list item elements in the right block element to where the left
5100 // list element is
5101 // - or first hard line in the right block element to where:
5102 // - the left block element is.
5103 // - or the given left content in the left block is.
5104 else if (mPointContainingTheOtherBlockElement.GetContainer() ==
5105 mLeftBlockElement) {
5106 startOfRightContent =
5107 HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
5108 *mRightBlockElement);
5109 AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(),
5110 &startOfRightContent);
5111 result = WhiteSpaceVisibilityKeeper::
5112 MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement(
5113 aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement),
5114 MOZ_KnownLive(*mRightBlockElement),
5115 mPointContainingTheOtherBlockElement,
5116 MOZ_KnownLive(*mInclusiveDescendantOfLeftBlockElement),
5117 mNewListElementTagNameOfRightListElement,
5118 MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost);
5119 if (MOZ_UNLIKELY(result.isErr())) {
5120 NS_WARNING(
5121 "WhiteSpaceVisibilityKeeper::"
5122 "MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement() "
5123 "failed");
5124 return result;
5126 trackStartOfRightBlock.FlushAndStopTracking();
5127 if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
5128 NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
5129 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
5134 // Normal case. Blocks are siblings, or at least close enough. An example
5135 // of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>. The
5136 // first li and the p are not true siblings, but we still want to join them
5137 // if you backspace from li into p.
5138 else {
5139 MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet());
5141 startOfRightContent =
5142 HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
5143 *mRightBlockElement);
5144 AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(),
5145 &startOfRightContent);
5146 result = WhiteSpaceVisibilityKeeper::
5147 MergeFirstLineOfRightBlockElementIntoLeftBlockElement(
5148 aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement),
5149 MOZ_KnownLive(*mRightBlockElement),
5150 mNewListElementTagNameOfRightListElement,
5151 MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost);
5152 if (MOZ_UNLIKELY(result.isErr())) {
5153 NS_WARNING(
5154 "WhiteSpaceVisibilityKeeper::"
5155 "MergeFirstLineOfRightBlockElementIntoLeftBlockElement() failed");
5156 return result;
5158 trackStartOfRightBlock.FlushAndStopTracking();
5159 if (NS_WARN_IF(!startOfRightContent.IsSet()) ||
5160 NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) {
5161 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
5165 // If we're deleting selection (meaning not replacing selection with new
5166 // content), we should put caret to end of preceding text node if there is.
5167 // Then, users can type text into it like the other browsers.
5168 if (MayEditActionDeleteAroundCollapsedSelection(
5169 aHTMLEditor.GetEditAction())) {
5170 WSRunScanner scanner(&aEditingHost, startOfRightContent,
5171 BlockInlineCheck::UseComputedDisplayStyle);
5172 WSScanResult maybePreviousText =
5173 scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent);
5174 if (maybePreviousText.IsContentEditable() &&
5175 maybePreviousText.InVisibleOrCollapsibleCharacters()) {
5176 mPointToPutCaret = maybePreviousText.Point<EditorDOMPoint>();
5179 return result;
5182 // static
5183 Result<bool, nsresult>
5184 HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine(
5185 const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost) {
5186 if (NS_WARN_IF(!aPointInHardLine.IsSet()) ||
5187 NS_WARN_IF(aPointInHardLine.IsInNativeAnonymousSubtree())) {
5188 return Err(NS_ERROR_INVALID_ARG);
5191 RefPtr<nsRange> oneLineRange =
5192 AutoRangeArray::CreateRangeWrappingStartAndEndLinesContainingBoundaries(
5193 aPointInHardLine, aPointInHardLine,
5194 EditSubAction::eMergeBlockContents,
5195 BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost);
5196 if (!oneLineRange || oneLineRange->Collapsed() ||
5197 !oneLineRange->IsPositioned() ||
5198 !oneLineRange->GetStartContainer()->IsContent() ||
5199 !oneLineRange->GetEndContainer()->IsContent()) {
5200 return false;
5203 // If there is only a padding `<br>` element in a empty block, it's selected
5204 // by `UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement()`.
5205 // However, it won't be moved. Although it'll be deleted,
5206 // AutoMoveOneLineHandler returns "ignored". Therefore, we should return
5207 // `false` in this case.
5208 if (nsIContent* childContent = oneLineRange->GetChildAtStartOffset()) {
5209 if (childContent->IsHTMLElement(nsGkAtoms::br) &&
5210 childContent->GetParent()) {
5211 if (const Element* blockElement =
5212 HTMLEditUtils::GetInclusiveAncestorElement(
5213 *childContent->GetParent(),
5214 HTMLEditUtils::ClosestBlockElement,
5215 BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
5216 if (HTMLEditUtils::IsEmptyNode(
5217 *blockElement,
5218 {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
5219 return false;
5225 nsINode* commonAncestor = oneLineRange->GetClosestCommonInclusiveAncestor();
5226 // Currently, we move non-editable content nodes too.
5227 EditorRawDOMPoint startPoint(oneLineRange->StartRef());
5228 if (!startPoint.IsEndOfContainer()) {
5229 return true;
5231 EditorRawDOMPoint endPoint(oneLineRange->EndRef());
5232 if (!endPoint.IsStartOfContainer()) {
5233 return true;
5235 if (startPoint.GetContainer() != commonAncestor) {
5236 while (true) {
5237 EditorRawDOMPoint pointInParent(startPoint.GetContainerAs<nsIContent>());
5238 if (NS_WARN_IF(!pointInParent.IsInContentNode())) {
5239 return Err(NS_ERROR_FAILURE);
5241 if (pointInParent.GetContainer() == commonAncestor) {
5242 startPoint = pointInParent;
5243 break;
5245 if (!pointInParent.IsEndOfContainer()) {
5246 return true;
5250 if (endPoint.GetContainer() != commonAncestor) {
5251 while (true) {
5252 EditorRawDOMPoint pointInParent(endPoint.GetContainerAs<nsIContent>());
5253 if (NS_WARN_IF(!pointInParent.IsInContentNode())) {
5254 return Err(NS_ERROR_FAILURE);
5256 if (pointInParent.GetContainer() == commonAncestor) {
5257 endPoint = pointInParent;
5258 break;
5260 if (!pointInParent.IsStartOfContainer()) {
5261 return true;
5265 // If start point and end point in the common ancestor are direct siblings,
5266 // there is no content to move or delete.
5267 // E.g., `<b>abc<br>[</b><i>]<br>def</i>`.
5268 return startPoint.GetNextSiblingOfChild() != endPoint.GetChild();
5271 nsresult HTMLEditor::AutoMoveOneLineHandler::Prepare(
5272 HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointInHardLine,
5273 const Element& aEditingHost) {
5274 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
5275 MOZ_ASSERT(aPointInHardLine.IsInContentNode());
5276 MOZ_ASSERT(mPointToInsert.IsSetAndValid());
5278 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5279 ("Prepare(aHTMLEditor=%p, aPointInHardLine=%s, aEditingHost=%s), "
5280 "mPointToInsert=%s, mMoveToEndOfContainer=%s",
5281 &aHTMLEditor, ToString(aPointInHardLine).c_str(),
5282 ToString(aEditingHost).c_str(), ToString(mPointToInsert).c_str(),
5283 ForceMoveToEndOfContainer() ? "MoveToEndOfContainer::Yes"
5284 : "MoveToEndOfContainer::No"));
5286 if (NS_WARN_IF(mPointToInsert.IsInNativeAnonymousSubtree())) {
5287 MOZ_LOG(
5288 gOneLineMoverLog, LogLevel::Error,
5289 ("Failed because mPointToInsert was in a native anonymous subtree"));
5290 return Err(NS_ERROR_INVALID_ARG);
5293 mSrcInclusiveAncestorBlock =
5294 aPointInHardLine.IsInContentNode()
5295 ? HTMLEditUtils::GetInclusiveAncestorElement(
5296 *aPointInHardLine.ContainerAs<nsIContent>(),
5297 HTMLEditUtils::ClosestBlockElement,
5298 BlockInlineCheck::UseComputedDisplayOutsideStyle)
5299 : nullptr;
5300 mDestInclusiveAncestorBlock =
5301 mPointToInsert.IsInContentNode()
5302 ? HTMLEditUtils::GetInclusiveAncestorElement(
5303 *mPointToInsert.ContainerAs<nsIContent>(),
5304 HTMLEditUtils::ClosestBlockElement,
5305 BlockInlineCheck::UseComputedDisplayOutsideStyle)
5306 : nullptr;
5307 mMovingToParentBlock =
5308 mDestInclusiveAncestorBlock && mSrcInclusiveAncestorBlock &&
5309 mDestInclusiveAncestorBlock != mSrcInclusiveAncestorBlock &&
5310 mSrcInclusiveAncestorBlock->IsInclusiveDescendantOf(
5311 mDestInclusiveAncestorBlock);
5312 mTopmostSrcAncestorBlockInDestBlock =
5313 mMovingToParentBlock
5314 ? AutoMoveOneLineHandler::
5315 GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement(
5316 *mSrcInclusiveAncestorBlock, *mDestInclusiveAncestorBlock)
5317 : nullptr;
5318 MOZ_ASSERT_IF(mMovingToParentBlock, mTopmostSrcAncestorBlockInDestBlock);
5320 mPreserveWhiteSpaceStyle =
5321 AutoMoveOneLineHandler::ConsiderWhetherPreserveWhiteSpaceStyle(
5322 aPointInHardLine.GetContainerAs<nsIContent>(),
5323 mDestInclusiveAncestorBlock);
5325 AutoRangeArray rangesToWrapTheLine(aPointInHardLine);
5326 rangesToWrapTheLine.ExtendRangesToWrapLines(
5327 EditSubAction::eMergeBlockContents,
5328 BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost);
5329 MOZ_ASSERT(rangesToWrapTheLine.Ranges().Length() <= 1u);
5330 mLineRange = EditorDOMRange(rangesToWrapTheLine.FirstRangeRef());
5332 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5333 ("mSrcInclusiveAncestorBlock=%s, mDestInclusiveAncestorBlock=%s, "
5334 "mMovingToParentBlock=%s, mTopmostSrcAncestorBlockInDestBlock=%s, "
5335 "mPreserveWhiteSpaceStyle=%s, mLineRange=%s",
5336 mSrcInclusiveAncestorBlock
5337 ? ToString(*mSrcInclusiveAncestorBlock).c_str()
5338 : "nullptr",
5339 mDestInclusiveAncestorBlock
5340 ? ToString(*mDestInclusiveAncestorBlock).c_str()
5341 : "nullptr",
5342 mMovingToParentBlock ? "true" : "false",
5343 mTopmostSrcAncestorBlockInDestBlock
5344 ? ToString(*mTopmostSrcAncestorBlockInDestBlock).c_str()
5345 : "nullptr",
5346 ToString(mPreserveWhiteSpaceStyle).c_str(),
5347 ToString(mLineRange).c_str()));
5349 return NS_OK;
5352 Result<CaretPoint, nsresult>
5353 HTMLEditor::AutoMoveOneLineHandler::SplitToMakeTheLineIsolated(
5354 HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer,
5355 const Element& aEditingHost,
5356 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) const {
5357 AutoRangeArray rangesToWrapTheLine(mLineRange);
5358 Result<EditorDOMPoint, nsresult> splitResult =
5359 rangesToWrapTheLine
5360 .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
5361 aHTMLEditor, BlockInlineCheck::UseComputedDisplayOutsideStyle,
5362 aEditingHost, &aNewContainer);
5363 if (MOZ_UNLIKELY(splitResult.isErr())) {
5364 NS_WARNING(
5365 "AutoRangeArray::"
5366 "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
5367 return Err(splitResult.unwrapErr());
5369 EditorDOMPoint pointToPutCaret;
5370 if (splitResult.inspect().IsSet()) {
5371 pointToPutCaret = splitResult.unwrap();
5373 nsresult rv = rangesToWrapTheLine.CollectEditTargetNodes(
5374 aHTMLEditor, aOutArrayOfContents, EditSubAction::eMergeBlockContents,
5375 AutoRangeArray::CollectNonEditableNodes::Yes);
5376 if (NS_FAILED(rv)) {
5377 NS_WARNING(
5378 "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
5379 "eMergeBlockContents, CollectNonEditableNodes::Yes) failed");
5380 return Err(rv);
5382 return CaretPoint(pointToPutCaret);
5385 // static
5386 Element* HTMLEditor::AutoMoveOneLineHandler::
5387 GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement(
5388 Element& aBlockElement, const Element& aAncestorElement) {
5389 MOZ_ASSERT(aBlockElement.IsInclusiveDescendantOf(&aAncestorElement));
5390 MOZ_ASSERT(HTMLEditUtils::IsBlockElement(
5391 aBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle));
5393 if (&aBlockElement == &aAncestorElement) {
5394 return nullptr;
5397 Element* lastBlockAncestor = &aBlockElement;
5398 for (Element* element : aBlockElement.InclusiveAncestorsOfType<Element>()) {
5399 if (element == &aAncestorElement) {
5400 return lastBlockAncestor;
5402 if (HTMLEditUtils::IsBlockElement(
5403 *lastBlockAncestor,
5404 BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
5405 lastBlockAncestor = element;
5408 return nullptr;
5411 // static
5412 HTMLEditor::PreserveWhiteSpaceStyle
5413 HTMLEditor::AutoMoveOneLineHandler::ConsiderWhetherPreserveWhiteSpaceStyle(
5414 const nsIContent* aContentInLine,
5415 const Element* aInclusiveAncestorBlockOfInsertionPoint) {
5416 if (MOZ_UNLIKELY(!aInclusiveAncestorBlockOfInsertionPoint)) {
5417 return PreserveWhiteSpaceStyle::No;
5420 // If we move content from or to <pre>, we don't need to preserve the
5421 // white-space style for compatibility with both our traditional behavior
5422 // and the other browsers.
5424 // TODO: If `white-space` is specified by non-UA stylesheet, we should
5425 // preserve it even if the right block is <pre> for compatibility with the
5426 // other browsers.
5427 const auto IsInclusiveDescendantOfPre = [](const nsIContent& aContent) {
5428 // If the content has different `white-space` style from <pre>, we
5429 // shouldn't treat it as a descendant of <pre> because web apps or
5430 // the user intent to treat the white-spaces in aContent not as `pre`.
5431 if (EditorUtils::GetComputedWhiteSpaceStyles(aContent).valueOr(std::pair(
5432 StyleWhiteSpaceCollapse::Collapse, StyleTextWrapMode::Wrap)) !=
5433 std::pair(StyleWhiteSpaceCollapse::Preserve,
5434 StyleTextWrapMode::Nowrap)) {
5435 return false;
5437 for (const Element* element :
5438 aContent.InclusiveAncestorsOfType<Element>()) {
5439 if (element->IsHTMLElement(nsGkAtoms::pre)) {
5440 return true;
5443 return false;
5445 if (IsInclusiveDescendantOfPre(*aInclusiveAncestorBlockOfInsertionPoint) ||
5446 MOZ_UNLIKELY(!aContentInLine) ||
5447 IsInclusiveDescendantOfPre(*aContentInLine)) {
5448 return PreserveWhiteSpaceStyle::No;
5450 return PreserveWhiteSpaceStyle::Yes;
5453 Result<MoveNodeResult, nsresult> HTMLEditor::AutoMoveOneLineHandler::Run(
5454 HTMLEditor& aHTMLEditor, const Element& aEditingHost) {
5455 EditorDOMPoint pointToInsert(NextInsertionPointRef());
5456 MOZ_ASSERT(pointToInsert.IsInContentNode());
5458 MOZ_LOG(
5459 gOneLineMoverLog, LogLevel::Info,
5460 ("Run(aHTMLEditor=%p, aEditingHost=%s), pointToInsert=%s", &aHTMLEditor,
5461 ToString(aEditingHost).c_str(), ToString(pointToInsert).c_str()));
5463 EditorDOMPoint pointToPutCaret;
5464 AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
5466 AutoTrackDOMPoint tackPointToInsert(aHTMLEditor.RangeUpdaterRef(),
5467 &pointToInsert);
5469 Result<CaretPoint, nsresult> splitAtLineEdgesResult =
5470 SplitToMakeTheLineIsolated(
5471 aHTMLEditor,
5472 MOZ_KnownLive(*pointToInsert.ContainerAs<nsIContent>()),
5473 aEditingHost, arrayOfContents);
5474 if (MOZ_UNLIKELY(splitAtLineEdgesResult.isErr())) {
5475 NS_WARNING("AutoMoveOneLineHandler::SplitToMakeTheLineIsolated() failed");
5476 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5477 ("Run: SplitToMakeTheLineIsolated() failed"));
5478 return splitAtLineEdgesResult.propagateErr();
5480 splitAtLineEdgesResult.unwrap().MoveCaretPointTo(
5481 pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
5482 MOZ_LOG(gOneLineMoverLog, LogLevel::Verbose,
5483 ("Run: pointToPutCaret=%s", ToString(pointToPutCaret).c_str()));
5485 Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
5486 aHTMLEditor.MaybeSplitElementsAtEveryBRElement(
5487 arrayOfContents, EditSubAction::eMergeBlockContents);
5488 if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
5489 NS_WARNING(
5490 "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
5491 "eMergeBlockContents) failed");
5492 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5493 ("Run: MaybeSplitElementsAtEveryBRElement() failed"));
5494 return splitAtBRElementsResult.propagateErr();
5496 if (splitAtBRElementsResult.inspect().IsSet()) {
5497 pointToPutCaret = splitAtBRElementsResult.unwrap();
5499 MOZ_LOG(gOneLineMoverLog, LogLevel::Verbose,
5500 ("Run: pointToPutCaret=%s", ToString(pointToPutCaret).c_str()));
5503 if (!pointToInsert.IsSetAndValid()) {
5504 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5505 ("Run: Failed because pointToInsert pointed invalid position"));
5506 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
5509 if (aHTMLEditor.AllowsTransactionsToChangeSelection() &&
5510 pointToPutCaret.IsSet()) {
5511 nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret);
5512 if (NS_FAILED(rv)) {
5513 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
5514 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5515 ("Run: Failed because of "
5516 "aHTMLEditor.CollapseSelectionTo(pointToPutCaret) failure"));
5517 return Err(rv);
5521 if (arrayOfContents.IsEmpty()) {
5522 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5523 ("Run: Did nothing because of no content to be moved"));
5524 return MoveNodeResult::IgnoredResult(std::move(pointToInsert));
5527 // Track the range which contains the moved contents.
5528 if (ForceMoveToEndOfContainer()) {
5529 pointToInsert = NextInsertionPointRef();
5531 EditorDOMRange movedContentRange(pointToInsert);
5532 MoveNodeResult moveContentsInLineResult =
5533 MoveNodeResult::IgnoredResult(pointToInsert);
5534 for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
5535 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5536 ("Run: content=%s, pointToInsert=%s, movedContentRange=%s, "
5537 "mPointToInsert=%s",
5538 ToString(content.ref()).c_str(), ToString(pointToInsert).c_str(),
5539 ToString(movedContentRange).c_str(),
5540 ToString(mPointToInsert).c_str()));
5542 AutoEditorDOMRangeChildrenInvalidator lockOffsets(movedContentRange);
5543 AutoTrackDOMRange trackMovedContentRange(aHTMLEditor.RangeUpdaterRef(),
5544 &movedContentRange);
5545 // If the content is a block element, move all children of it to the
5546 // new container, and then, remove the (probably) empty block element.
5547 if (HTMLEditUtils::IsBlockElement(
5548 content, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
5549 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5550 ("Run: Unwrapping children of content because of a block"));
5551 Result<MoveNodeResult, nsresult> moveChildrenResult =
5552 aHTMLEditor.MoveChildrenWithTransaction(
5553 MOZ_KnownLive(*content->AsElement()), pointToInsert,
5554 mPreserveWhiteSpaceStyle, RemoveIfCommentNode::Yes);
5555 if (MOZ_UNLIKELY(moveChildrenResult.isErr())) {
5556 NS_WARNING("HTMLEditor::MoveChildrenWithTransaction() failed");
5557 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5558 ("Run: MoveChildrenWithTransaction() failed"));
5559 moveContentsInLineResult.IgnoreCaretPointSuggestion();
5560 return moveChildrenResult;
5562 moveContentsInLineResult |= moveChildrenResult.inspect();
5563 // MOZ_KnownLive due to bug 1620312
5564 nsresult rv =
5565 aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(content));
5566 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
5567 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5568 ("Run: Aborted because DeleteNodeWithTransaction() caused "
5569 "destroying the editor"));
5570 moveContentsInLineResult.IgnoreCaretPointSuggestion();
5571 return Err(NS_ERROR_EDITOR_DESTROYED);
5573 if (NS_FAILED(rv)) {
5574 NS_WARNING(
5575 "EditorBase::DeleteNodeWithTransaction() failed, but ignored");
5576 MOZ_LOG(gOneLineMoverLog, LogLevel::Warning,
5577 ("Run: Failed to delete content but the error was ignored"));
5580 // If the moving content is a comment node or an empty inline node, we
5581 // don't want it to appear in the dist paragraph.
5582 else if (content->IsComment() ||
5583 (content->IsText() && !content->AsText()->TextDataLength()) ||
5584 HTMLEditUtils::IsEmptyInlineContainer(
5585 content,
5586 {EmptyCheckOption::TreatSingleBRElementAsVisible,
5587 EmptyCheckOption::TreatListItemAsVisible,
5588 EmptyCheckOption::TreatTableCellAsVisible,
5589 EmptyCheckOption::TreatNonEditableContentAsInvisible},
5590 BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
5591 nsCOMPtr<nsIContent> emptyContent =
5592 HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
5593 content, BlockInlineCheck::UseComputedDisplayOutsideStyle,
5594 &aEditingHost, pointToInsert.ContainerAs<nsIContent>());
5595 if (!emptyContent) {
5596 emptyContent = content;
5598 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5599 ("Run: Deleting content because of %s%s",
5600 content->IsComment() ? "a comment node"
5601 : content->IsText() ? "an empty text node"
5602 : "an empty inline container",
5603 content != emptyContent
5604 ? nsPrintfCString(" (deleting topmost empty ancestor: %s)",
5605 ToString(*emptyContent).c_str())
5606 .get()
5607 : ""));
5608 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*emptyContent);
5609 if (NS_FAILED(rv)) {
5610 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
5611 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5612 ("Run: DeleteNodeWithTransaction() failed"));
5613 moveContentsInLineResult.IgnoreCaretPointSuggestion();
5614 return Err(rv);
5616 } else {
5617 MOZ_LOG(gOneLineMoverLog, LogLevel::Info, ("Run: Moving content"));
5618 // MOZ_KnownLive due to bug 1620312
5619 Result<MoveNodeResult, nsresult> moveNodeOrChildrenResult =
5620 aHTMLEditor.MoveNodeOrChildrenWithTransaction(
5621 MOZ_KnownLive(content), pointToInsert, mPreserveWhiteSpaceStyle,
5622 RemoveIfCommentNode::Yes);
5623 if (MOZ_UNLIKELY(moveNodeOrChildrenResult.isErr())) {
5624 NS_WARNING("HTMLEditor::MoveNodeOrChildrenWithTransaction() failed");
5625 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5626 ("Run: MoveNodeOrChildrenWithTransaction() failed"));
5627 moveContentsInLineResult.IgnoreCaretPointSuggestion();
5628 return moveNodeOrChildrenResult;
5630 moveContentsInLineResult |= moveNodeOrChildrenResult.inspect();
5633 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5634 ("Run: movedContentRange=%s, mPointToInsert=%s",
5635 ToString(movedContentRange).c_str(),
5636 ToString(mPointToInsert).c_str()));
5637 moveContentsInLineResult.MarkAsHandled();
5638 if (NS_WARN_IF(!movedContentRange.IsPositioned())) {
5639 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5640 ("Run: Failed because movedContentRange was not positioned"));
5641 moveContentsInLineResult.IgnoreCaretPointSuggestion();
5642 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
5644 // For backward compatibility, we should move contents to end of the
5645 // container if the instance is created without specific insertion point.
5646 if (ForceMoveToEndOfContainer()) {
5647 pointToInsert = NextInsertionPointRef();
5648 MOZ_ASSERT(pointToInsert.IsSet());
5649 MOZ_ASSERT(movedContentRange.StartRef().EqualsOrIsBefore(pointToInsert));
5650 movedContentRange.SetEnd(pointToInsert);
5651 MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
5652 ("Run: Updated movedContentRange end to next insertion point"));
5654 // And also if pointToInsert has been made invalid with removing preceding
5655 // children, we should move the content to the end of the container.
5656 else if (aHTMLEditor.MayHaveMutationEventListeners() &&
5657 MOZ_UNLIKELY(!moveContentsInLineResult.NextInsertionPointRef()
5658 .IsSetAndValid())) {
5659 mPointToInsert.SetToEndOf(mPointToInsert.GetContainer());
5660 pointToInsert = NextInsertionPointRef();
5661 movedContentRange.SetEnd(pointToInsert);
5662 MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
5663 ("Run: Updated mPointToInsert to end of container and updated "
5664 "movedContentRange"));
5665 } else {
5666 MOZ_DIAGNOSTIC_ASSERT(
5667 moveContentsInLineResult.NextInsertionPointRef().IsSet());
5668 mPointToInsert = moveContentsInLineResult.NextInsertionPointRef();
5669 pointToInsert = NextInsertionPointRef();
5670 if (!aHTMLEditor.MayHaveMutationEventListeners() ||
5671 movedContentRange.EndRef().IsBefore(pointToInsert)) {
5672 MOZ_ASSERT(pointToInsert.IsSet());
5673 MOZ_ASSERT(
5674 movedContentRange.StartRef().EqualsOrIsBefore(pointToInsert));
5675 movedContentRange.SetEnd(pointToInsert);
5676 MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
5677 ("Run: Updated mPointToInsert and updated movedContentRange"));
5678 } else {
5679 MOZ_LOG(gOneLineMoverLog, LogLevel::Debug,
5680 ("Run: Updated only mPointToInsert"));
5685 // Nothing has been moved, we don't need to clean up unnecessary <br> element.
5686 // And also if we're not moving content into a block, we can quit right now.
5687 if (moveContentsInLineResult.Ignored() ||
5688 MOZ_UNLIKELY(!mDestInclusiveAncestorBlock)) {
5689 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5690 (moveContentsInLineResult.Ignored()
5691 ? "Run: Did nothing for any children"
5692 : "Run: Finished (not dest block)"));
5693 return moveContentsInLineResult;
5696 // If we couldn't track the range to clean up, we should just stop cleaning up
5697 // because returning error from here may change the behavior of web apps using
5698 // mutation event listeners.
5699 if (MOZ_UNLIKELY(!movedContentRange.IsPositioned() ||
5700 movedContentRange.Collapsed())) {
5701 MOZ_LOG(gOneLineMoverLog, LogLevel::Info,
5702 (!movedContentRange.IsPositioned()
5703 ? "Run: Finished (Couldn't track moved line)"
5704 : "Run: Finished (Moved line was empty)"));
5705 return moveContentsInLineResult;
5708 nsresult rv = DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
5709 aHTMLEditor, movedContentRange, aEditingHost);
5710 if (NS_FAILED(rv)) {
5711 NS_WARNING(
5712 "AutoMoveOneLineHandler::"
5713 "DeleteUnnecessaryTrailingLineBreakInMovedLineEnd() failed");
5714 MOZ_LOG(gOneLineMoverLog, LogLevel::Error,
5715 ("Run: DeleteUnnecessaryTrailingLineBreakInMovedLineEnd() failed"));
5716 moveContentsInLineResult.IgnoreCaretPointSuggestion();
5717 return Err(rv);
5720 MOZ_LOG(gOneLineMoverLog, LogLevel::Info, ("Run: Finished"));
5721 return moveContentsInLineResult;
5724 nsresult HTMLEditor::AutoMoveOneLineHandler::
5725 DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
5726 HTMLEditor& aHTMLEditor, const EditorDOMRange& aMovedContentRange,
5727 const Element& aEditingHost) const {
5728 MOZ_ASSERT(mDestInclusiveAncestorBlock);
5729 MOZ_ASSERT(aMovedContentRange.IsPositioned());
5730 MOZ_ASSERT(!aMovedContentRange.Collapsed());
5732 // If we didn't preserve white-space for backward compatibility and
5733 // white-space becomes not preformatted, we need to clean it up the last text
5734 // node if it ends with a preformatted line break.
5735 if (mPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) {
5736 const RefPtr<Text> textNodeEndingWithUnnecessaryLineBreak = [&]() -> Text* {
5737 Text* lastTextNode = Text::FromNodeOrNull(
5738 mMovingToParentBlock
5739 ? HTMLEditUtils::GetPreviousContent(
5740 *mTopmostSrcAncestorBlockInDestBlock,
5741 {WalkTreeOption::StopAtBlockBoundary},
5742 BlockInlineCheck::UseComputedDisplayOutsideStyle,
5743 mDestInclusiveAncestorBlock)
5744 : HTMLEditUtils::GetLastLeafContent(
5745 *mDestInclusiveAncestorBlock,
5746 {LeafNodeType::LeafNodeOrNonEditableNode}));
5747 if (!lastTextNode ||
5748 !HTMLEditUtils::IsSimplyEditableNode(*lastTextNode)) {
5749 return nullptr;
5751 const nsTextFragment& textFragment = lastTextNode->TextFragment();
5752 const char16_t lastCh =
5753 textFragment.GetLength()
5754 ? textFragment.CharAt(textFragment.GetLength() - 1u)
5755 : 0;
5756 return lastCh == HTMLEditUtils::kNewLine &&
5757 !EditorUtils::IsNewLinePreformatted(*lastTextNode)
5758 ? lastTextNode
5759 : nullptr;
5760 }();
5761 if (textNodeEndingWithUnnecessaryLineBreak) {
5762 if (textNodeEndingWithUnnecessaryLineBreak->TextDataLength() == 1u) {
5763 const RefPtr<Element> inlineElement =
5764 HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
5765 *textNodeEndingWithUnnecessaryLineBreak,
5766 BlockInlineCheck::UseComputedDisplayOutsideStyle,
5767 &aEditingHost);
5768 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
5769 inlineElement ? static_cast<nsIContent&>(*inlineElement)
5770 : static_cast<nsIContent&>(
5771 *textNodeEndingWithUnnecessaryLineBreak));
5772 if (NS_FAILED(rv)) {
5773 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
5774 return Err(rv);
5776 } else {
5777 Result<CaretPoint, nsresult> caretPointOrError =
5778 aHTMLEditor.DeleteTextWithTransaction(
5779 *textNodeEndingWithUnnecessaryLineBreak,
5780 textNodeEndingWithUnnecessaryLineBreak->TextDataLength() - 1u,
5781 1u);
5782 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
5783 NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
5784 return caretPointOrError.propagateErr();
5786 nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo(
5787 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
5788 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
5789 SuggestCaret::AndIgnoreTrivialError});
5790 if (NS_FAILED(rv)) {
5791 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
5792 return Err(rv);
5794 NS_WARNING_ASSERTION(
5795 rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
5796 "CaretPoint::SuggestCaretPointTo() failed, but ignored");
5801 nsCOMPtr<nsIContent> lastLineBreakContent =
5802 mMovingToParentBlock
5803 ? HTMLEditUtils::GetUnnecessaryLineBreakContent(
5804 *mTopmostSrcAncestorBlockInDestBlock,
5805 ScanLineBreak::BeforeBlock)
5806 : HTMLEditUtils::GetUnnecessaryLineBreakContent(
5807 *mDestInclusiveAncestorBlock, ScanLineBreak::AtEndOfBlock);
5808 if (!lastLineBreakContent) {
5809 return NS_OK;
5811 EditorRawDOMPoint atUnnecessaryLineBreak(lastLineBreakContent);
5812 if (NS_WARN_IF(!atUnnecessaryLineBreak.IsSet())) {
5813 return NS_ERROR_FAILURE;
5815 // If the found unnecessary line break is not what we moved above, we
5816 // shouldn't remove it. E.g., the web app may have inserted it intentionally.
5817 MOZ_ASSERT(aMovedContentRange.StartRef().IsSetAndValid());
5818 MOZ_ASSERT(aMovedContentRange.EndRef().IsSetAndValid());
5819 if (!aMovedContentRange.Contains(atUnnecessaryLineBreak)) {
5820 return NS_OK;
5823 AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
5824 // If it's a text node and ending with a preformatted line break, we should
5825 // delete it.
5826 if (Text* textNode = Text::FromNode(lastLineBreakContent)) {
5827 MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(*textNode));
5828 if (textNode->TextDataLength() > 1) {
5829 Result<CaretPoint, nsresult> caretPointOrError =
5830 aHTMLEditor.DeleteTextWithTransaction(
5831 MOZ_KnownLive(*textNode), textNode->TextDataLength() - 1u, 1u);
5832 if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
5833 NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
5834 return caretPointOrError.unwrapErr();
5836 // IgnoreCaretPointSuggestion() because of dontChangeMySelection above.
5837 caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
5838 return NS_OK;
5840 } else {
5841 MOZ_ASSERT(lastLineBreakContent->IsHTMLElement(nsGkAtoms::br));
5843 // If last line break content is the only content of its inline parent, we
5844 // should remove the parent too.
5845 if (const RefPtr<Element> inlineElement =
5846 HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
5847 *lastLineBreakContent,
5848 BlockInlineCheck::UseComputedDisplayOutsideStyle,
5849 &aEditingHost)) {
5850 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*inlineElement);
5851 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
5852 "EditorBase::DeleteNodeWithTransaction() failed");
5853 return rv;
5855 // Or if the text node has only the preformatted line break or <br> element,
5856 // we should remove it.
5857 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*lastLineBreakContent);
5858 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
5859 "EditorBase::DeleteNodeWithTransaction() failed");
5860 return rv;
5863 Result<bool, nsresult> HTMLEditor::CanMoveNodeOrChildren(
5864 const nsIContent& aContent, const nsINode& aNewContainer) const {
5865 if (HTMLEditUtils::CanNodeContain(aNewContainer, aContent)) {
5866 return true;
5868 if (aContent.IsElement()) {
5869 return CanMoveChildren(*aContent.AsElement(), aNewContainer);
5871 return true;
5874 Result<MoveNodeResult, nsresult> HTMLEditor::MoveNodeOrChildrenWithTransaction(
5875 nsIContent& aContentToMove, const EditorDOMPoint& aPointToInsert,
5876 PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle,
5877 RemoveIfCommentNode aRemoveIfCommentNode) {
5878 MOZ_ASSERT(IsEditActionDataAvailable());
5879 MOZ_ASSERT(aPointToInsert.IsInContentNode());
5881 const auto destWhiteSpaceStyles =
5882 [&]() -> Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>> {
5883 if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No ||
5884 !aPointToInsert.IsInContentNode()) {
5885 return Nothing();
5887 auto styles = EditorUtils::GetComputedWhiteSpaceStyles(
5888 *aPointToInsert.ContainerAs<nsIContent>());
5889 if (NS_WARN_IF(styles.isSome() &&
5890 styles.value().first ==
5891 StyleWhiteSpaceCollapse::PreserveSpaces)) {
5892 return Nothing();
5894 return styles;
5895 }();
5896 const auto srcWhiteSpaceStyles =
5897 [&]() -> Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>> {
5898 if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) {
5899 return Nothing();
5901 auto styles = EditorUtils::GetComputedWhiteSpaceStyles(aContentToMove);
5902 if (NS_WARN_IF(styles.isSome() &&
5903 styles.value().first ==
5904 StyleWhiteSpaceCollapse::PreserveSpaces)) {
5905 return Nothing();
5907 return styles;
5908 }();
5909 // Get the `white-space` shorthand form for the given collapse + mode pair.
5910 const auto GetWhiteSpaceStyleValue =
5911 [](std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode> aStyles) {
5912 if (aStyles.second == StyleTextWrapMode::Wrap) {
5913 switch (aStyles.first) {
5914 case StyleWhiteSpaceCollapse::Collapse:
5915 return u"normal"_ns;
5916 case StyleWhiteSpaceCollapse::Preserve:
5917 return u"pre-wrap"_ns;
5918 case StyleWhiteSpaceCollapse::PreserveBreaks:
5919 return u"pre-line"_ns;
5920 case StyleWhiteSpaceCollapse::PreserveSpaces:
5921 return u"preserve-spaces"_ns;
5922 case StyleWhiteSpaceCollapse::BreakSpaces:
5923 return u"break-spaces"_ns;
5925 } else {
5926 switch (aStyles.first) {
5927 case StyleWhiteSpaceCollapse::Collapse:
5928 return u"nowrap"_ns;
5929 case StyleWhiteSpaceCollapse::Preserve:
5930 return u"pre"_ns;
5931 case StyleWhiteSpaceCollapse::PreserveBreaks:
5932 return u"nowrap preserve-breaks"_ns;
5933 case StyleWhiteSpaceCollapse::PreserveSpaces:
5934 return u"nowrap preserve-spaces"_ns;
5935 case StyleWhiteSpaceCollapse::BreakSpaces:
5936 return u"nowrap break-spaces"_ns;
5939 MOZ_ASSERT_UNREACHABLE("all values should be handled above!");
5940 return u"normal"_ns;
5943 if (aRemoveIfCommentNode == RemoveIfCommentNode::Yes &&
5944 aContentToMove.IsComment()) {
5945 EditorDOMPoint pointToInsert(aPointToInsert);
5947 AutoTrackDOMPoint trackPointToInsert(RangeUpdaterRef(), &pointToInsert);
5948 nsresult rv = DeleteNodeWithTransaction(aContentToMove);
5949 if (NS_FAILED(rv)) {
5950 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
5951 return Err(rv);
5954 if (NS_WARN_IF(!pointToInsert.IsSetAndValid())) {
5955 return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
5957 return MoveNodeResult::HandledResult(std::move(pointToInsert));
5960 // Check if this node can go into the destination node
5961 if (HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(),
5962 aContentToMove)) {
5963 EditorDOMPoint pointToInsert(aPointToInsert);
5964 // Preserve white-space in the new position with using `style` attribute.
5965 // This is additional path from point of view of our traditional behavior.
5966 // Therefore, ignore errors especially if we got unexpected DOM tree.
5967 if (destWhiteSpaceStyles.isSome() && srcWhiteSpaceStyles.isSome() &&
5968 destWhiteSpaceStyles.value() != srcWhiteSpaceStyles.value()) {
5969 // Set `white-space` with `style` attribute if it's nsStyledElement.
5970 if (nsStyledElement* styledElement =
5971 nsStyledElement::FromNode(&aContentToMove)) {
5972 DebugOnly<nsresult> rvIgnored =
5973 CSSEditUtils::SetCSSPropertyWithTransaction(
5974 *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::white_space,
5975 GetWhiteSpaceStyleValue(srcWhiteSpaceStyles.value()));
5976 if (NS_WARN_IF(Destroyed())) {
5977 return Err(NS_ERROR_EDITOR_DESTROYED);
5979 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
5980 "CSSEditUtils::SetCSSPropertyWithTransaction("
5981 "nsGkAtoms::white_space) failed, but ignored");
5983 // Otherwise, if the dest container can have <span> element and <span>
5984 // element can have the moving content node, we should insert it.
5985 else if (HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(),
5986 *nsGkAtoms::span) &&
5987 HTMLEditUtils::CanNodeContain(*nsGkAtoms::span,
5988 aContentToMove)) {
5989 RefPtr<Element> newSpanElement = CreateHTMLContent(nsGkAtoms::span);
5990 if (NS_WARN_IF(!newSpanElement)) {
5991 return Err(NS_ERROR_FAILURE);
5993 nsAutoString styleAttrValue(u"white-space: "_ns);
5994 styleAttrValue.Append(
5995 GetWhiteSpaceStyleValue(srcWhiteSpaceStyles.value()));
5996 IgnoredErrorResult error;
5997 newSpanElement->SetAttr(nsGkAtoms::style, styleAttrValue, error);
5998 NS_WARNING_ASSERTION(!error.Failed(),
5999 "Element::SetAttr(nsGkAtoms::span) failed");
6000 if (MOZ_LIKELY(!error.Failed())) {
6001 Result<CreateElementResult, nsresult> insertSpanElementResult =
6002 InsertNodeWithTransaction<Element>(*newSpanElement,
6003 aPointToInsert);
6004 if (MOZ_UNLIKELY(insertSpanElementResult.isErr())) {
6005 if (NS_WARN_IF(insertSpanElementResult.inspectErr() ==
6006 NS_ERROR_EDITOR_DESTROYED)) {
6007 return Err(NS_ERROR_EDITOR_DESTROYED);
6009 NS_WARNING(
6010 "HTMLEditor::InsertNodeWithTransaction() failed, but ignored");
6011 } else {
6012 // We should move the node into the new <span> to preserve the
6013 // style.
6014 pointToInsert.Set(newSpanElement, 0u);
6015 // We should put caret after aContentToMove after moving it so that
6016 // we do not need the suggested caret point here.
6017 insertSpanElementResult.inspect().IgnoreCaretPointSuggestion();
6022 // If it can, move it there.
6023 Result<MoveNodeResult, nsresult> moveNodeResult =
6024 MoveNodeWithTransaction(aContentToMove, pointToInsert);
6025 NS_WARNING_ASSERTION(moveNodeResult.isOk(),
6026 "HTMLEditor::MoveNodeWithTransaction() failed");
6027 // XXX This is odd to override the handled state here, but stopping this
6028 // hits an NS_ASSERTION in WhiteSpaceVisibilityKeeper::
6029 // MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement.
6030 if (moveNodeResult.isOk()) {
6031 MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
6032 unwrappedMoveNodeResult.MarkAsHandled();
6033 return unwrappedMoveNodeResult;
6035 return moveNodeResult;
6038 // If it can't, move its children (if any), and then delete it.
6039 auto moveNodeResult =
6040 [&]() MOZ_CAN_RUN_SCRIPT -> Result<MoveNodeResult, nsresult> {
6041 if (!aContentToMove.IsElement()) {
6042 return MoveNodeResult::HandledResult(aPointToInsert);
6044 Result<MoveNodeResult, nsresult> moveChildrenResult =
6045 MoveChildrenWithTransaction(MOZ_KnownLive(*aContentToMove.AsElement()),
6046 aPointToInsert, aPreserveWhiteSpaceStyle,
6047 aRemoveIfCommentNode);
6048 NS_WARNING_ASSERTION(moveChildrenResult.isOk(),
6049 "HTMLEditor::MoveChildrenWithTransaction() failed");
6050 return moveChildrenResult;
6051 }();
6052 if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
6053 return moveNodeResult; // Already warned in the lambda.
6056 nsresult rv = DeleteNodeWithTransaction(aContentToMove);
6057 if (NS_FAILED(rv)) {
6058 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
6059 moveNodeResult.inspect().IgnoreCaretPointSuggestion();
6060 return Err(rv);
6062 if (!MayHaveMutationEventListeners()) {
6063 return moveNodeResult;
6065 // Mutation event listener may make `offset` value invalid with
6066 // removing some previous children while we call
6067 // `DeleteNodeWithTransaction()` so that we should adjust it here.
6068 if (moveNodeResult.inspect().NextInsertionPointRef().IsSetAndValid()) {
6069 return moveNodeResult;
6071 moveNodeResult.inspect().IgnoreCaretPointSuggestion();
6072 return MoveNodeResult::HandledResult(
6073 EditorDOMPoint::AtEndOf(*aPointToInsert.GetContainer()));
6076 Result<bool, nsresult> HTMLEditor::CanMoveChildren(
6077 const Element& aElement, const nsINode& aNewContainer) const {
6078 if (NS_WARN_IF(&aElement == &aNewContainer)) {
6079 return Err(NS_ERROR_FAILURE);
6081 for (nsIContent* childContent = aElement.GetFirstChild(); childContent;
6082 childContent = childContent->GetNextSibling()) {
6083 Result<bool, nsresult> result =
6084 CanMoveNodeOrChildren(*childContent, aNewContainer);
6085 if (result.isErr() || result.inspect()) {
6086 return result;
6089 return false;
6092 Result<MoveNodeResult, nsresult> HTMLEditor::MoveChildrenWithTransaction(
6093 Element& aElement, const EditorDOMPoint& aPointToInsert,
6094 PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle,
6095 RemoveIfCommentNode aRemoveIfCommentNode) {
6096 MOZ_ASSERT(aPointToInsert.IsSet());
6098 if (NS_WARN_IF(&aElement == aPointToInsert.GetContainer())) {
6099 return Err(NS_ERROR_INVALID_ARG);
6102 MoveNodeResult moveChildrenResult =
6103 MoveNodeResult::IgnoredResult(aPointToInsert);
6104 while (aElement.GetFirstChild()) {
6105 Result<MoveNodeResult, nsresult> moveNodeOrChildrenResult =
6106 MoveNodeOrChildrenWithTransaction(
6107 MOZ_KnownLive(*aElement.GetFirstChild()),
6108 moveChildrenResult.NextInsertionPointRef(),
6109 aPreserveWhiteSpaceStyle, aRemoveIfCommentNode);
6110 if (MOZ_UNLIKELY(moveNodeOrChildrenResult.isErr())) {
6111 NS_WARNING("HTMLEditor::MoveNodeOrChildrenWithTransaction() failed");
6112 moveChildrenResult.IgnoreCaretPointSuggestion();
6113 return moveNodeOrChildrenResult;
6115 moveChildrenResult |= moveNodeOrChildrenResult.inspect();
6117 return moveChildrenResult;
6120 nsresult HTMLEditor::MoveAllChildren(nsINode& aContainer,
6121 const EditorRawDOMPoint& aPointToInsert) {
6122 if (!aContainer.HasChildren()) {
6123 return NS_OK;
6125 nsIContent* firstChild = aContainer.GetFirstChild();
6126 if (NS_WARN_IF(!firstChild)) {
6127 return NS_ERROR_FAILURE;
6129 nsIContent* lastChild = aContainer.GetLastChild();
6130 if (NS_WARN_IF(!lastChild)) {
6131 return NS_ERROR_FAILURE;
6133 nsresult rv = MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert);
6134 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
6135 "HTMLEditor::MoveChildrenBetween() failed");
6136 return rv;
6139 nsresult HTMLEditor::MoveChildrenBetween(
6140 nsIContent& aFirstChild, nsIContent& aLastChild,
6141 const EditorRawDOMPoint& aPointToInsert) {
6142 nsCOMPtr<nsINode> oldContainer = aFirstChild.GetParentNode();
6143 if (NS_WARN_IF(oldContainer != aLastChild.GetParentNode()) ||
6144 NS_WARN_IF(!aPointToInsert.IsInContentNode()) ||
6145 NS_WARN_IF(!aPointToInsert.CanContainerHaveChildren())) {
6146 return NS_ERROR_INVALID_ARG;
6149 // First, store all children which should be moved to the new container.
6150 AutoTArray<nsCOMPtr<nsIContent>, 10> children;
6151 for (nsIContent* child = &aFirstChild; child;
6152 child = child->GetNextSibling()) {
6153 children.AppendElement(child);
6154 if (child == &aLastChild) {
6155 break;
6159 if (NS_WARN_IF(children.LastElement() != &aLastChild)) {
6160 return NS_ERROR_INVALID_ARG;
6163 nsCOMPtr<nsIContent> newContainer = aPointToInsert.ContainerAs<nsIContent>();
6164 nsCOMPtr<nsIContent> nextNode = aPointToInsert.GetChild();
6165 IgnoredErrorResult error;
6166 for (size_t i = children.Length(); i > 0; --i) {
6167 nsCOMPtr<nsIContent>& child = children[i - 1];
6168 if (child->GetParentNode() != oldContainer) {
6169 // If the child has been moved to different container, we shouldn't
6170 // touch it.
6171 continue;
6173 if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*child))) {
6174 return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
6176 oldContainer->RemoveChild(*child, error);
6177 if (NS_WARN_IF(Destroyed())) {
6178 return NS_ERROR_EDITOR_DESTROYED;
6180 if (error.Failed()) {
6181 NS_WARNING("nsINode::RemoveChild() failed");
6182 return error.StealNSResult();
6184 if (nextNode) {
6185 // If we're not appending the children to the new container, we should
6186 // check if referring next node of insertion point is still in the new
6187 // container.
6188 EditorRawDOMPoint pointToInsert(nextNode);
6189 if (NS_WARN_IF(!pointToInsert.IsSet()) ||
6190 NS_WARN_IF(pointToInsert.GetContainer() != newContainer)) {
6191 // The next node of insertion point has been moved by mutation observer.
6192 // Let's stop moving the remaining nodes.
6193 // XXX Or should we move remaining children after the last moved child?
6194 return NS_ERROR_FAILURE;
6197 if (NS_WARN_IF(
6198 !EditorUtils::IsEditableContent(*newContainer, EditorType::HTML))) {
6199 return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
6201 newContainer->InsertBefore(*child, nextNode, error);
6202 if (NS_WARN_IF(Destroyed())) {
6203 return NS_ERROR_EDITOR_DESTROYED;
6205 if (error.Failed()) {
6206 NS_WARNING("nsINode::InsertBefore() failed");
6207 return error.StealNSResult();
6209 // If the child was inserted or appended properly, the following children
6210 // should be inserted before it. Otherwise, keep using current position.
6211 if (child->GetParentNode() == newContainer) {
6212 nextNode = child;
6215 return NS_OK;
6218 nsresult HTMLEditor::MovePreviousSiblings(
6219 nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert) {
6220 if (NS_WARN_IF(!aChild.GetParentNode())) {
6221 return NS_ERROR_INVALID_ARG;
6223 nsIContent* firstChild = aChild.GetParentNode()->GetFirstChild();
6224 if (NS_WARN_IF(!firstChild)) {
6225 return NS_ERROR_FAILURE;
6227 nsIContent* lastChild =
6228 &aChild == firstChild ? firstChild : aChild.GetPreviousSibling();
6229 if (NS_WARN_IF(!lastChild)) {
6230 return NS_ERROR_FAILURE;
6232 nsresult rv = MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert);
6233 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
6234 "HTMLEditor::MoveChildrenBetween() failed");
6235 return rv;
6238 nsresult HTMLEditor::MoveInclusiveNextSiblings(
6239 nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert) {
6240 if (NS_WARN_IF(!aChild.GetParentNode())) {
6241 return NS_ERROR_INVALID_ARG;
6243 nsIContent* lastChild = aChild.GetParentNode()->GetLastChild();
6244 if (NS_WARN_IF(!lastChild)) {
6245 return NS_ERROR_FAILURE;
6247 nsresult rv = MoveChildrenBetween(aChild, *lastChild, aPointToInsert);
6248 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
6249 "HTMLEditor::MoveChildrenBetween() failed");
6250 return rv;
6253 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
6254 DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor,
6255 nsIContent& aContent) {
6256 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
6258 if (!HTMLEditUtils::IsAnyTableElementButNotTable(&aContent)) {
6259 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContent);
6260 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
6261 "EditorBase::DeleteNodeWithTransaction() failed");
6262 return rv;
6265 // XXX For performance, this should just call
6266 // DeleteContentButKeepTableStructure() while there are children in
6267 // aContent. If we need to avoid infinite loop because mutation event
6268 // listeners can add unexpected nodes into aContent, we should just loop
6269 // only original count of the children.
6270 AutoTArray<OwningNonNull<nsIContent>, 10> childList;
6271 for (nsIContent* child = aContent.GetFirstChild(); child;
6272 child = child->GetNextSibling()) {
6273 childList.AppendElement(*child);
6276 for (const auto& child : childList) {
6277 // MOZ_KnownLive because 'childList' is guaranteed to
6278 // keep it alive.
6279 nsresult rv =
6280 DeleteContentButKeepTableStructure(aHTMLEditor, MOZ_KnownLive(child));
6281 if (NS_FAILED(rv)) {
6282 NS_WARNING("HTMLEditor::DeleteContentButKeepTableStructure() failed");
6283 return rv;
6286 return NS_OK;
6289 nsresult HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty(
6290 nsIContent& aContent) {
6291 MOZ_ASSERT(IsEditActionDataAvailable());
6293 // The element must be `<blockquote type="cite">` or
6294 // `<span _moz_quote="true">`.
6295 RefPtr<Element> mailCiteElement =
6296 GetMostDistantAncestorMailCiteElement(aContent);
6297 if (!mailCiteElement) {
6298 return NS_OK;
6300 bool seenBR = false;
6301 if (!HTMLEditUtils::IsEmptyNode(
6302 *mailCiteElement,
6303 {EmptyCheckOption::TreatListItemAsVisible,
6304 EmptyCheckOption::TreatTableCellAsVisible,
6305 EmptyCheckOption::TreatNonEditableContentAsInvisible},
6306 &seenBR)) {
6307 return NS_OK;
6309 EditorDOMPoint atEmptyMailCiteElement(mailCiteElement);
6311 AutoEditorDOMPointChildInvalidator lockOffset(atEmptyMailCiteElement);
6312 nsresult rv = DeleteNodeWithTransaction(*mailCiteElement);
6313 if (NS_FAILED(rv)) {
6314 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
6315 return rv;
6319 if (!atEmptyMailCiteElement.IsSet() || !seenBR) {
6320 NS_WARNING_ASSERTION(
6321 atEmptyMailCiteElement.IsSet(),
6322 "Mutation event listener might changed the DOM tree during "
6323 "EditorBase::DeleteNodeWithTransaction(), but ignored");
6324 return NS_OK;
6327 Result<CreateElementResult, nsresult> insertBRElementResult =
6328 InsertBRElement(WithTransaction::Yes, atEmptyMailCiteElement);
6329 if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
6330 NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
6331 return insertBRElementResult.unwrapErr();
6333 MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
6334 insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
6335 nsresult rv = CollapseSelectionTo(
6336 EditorRawDOMPoint(insertBRElementResult.inspect().GetNewNode()));
6337 if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
6338 return NS_ERROR_EDITOR_DESTROYED;
6340 NS_WARNING_ASSERTION(
6341 NS_SUCCEEDED(rv),
6342 "EditorBase::::CollapseSelectionTo() failed, but ignored");
6343 return NS_OK;
6346 Element* HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
6347 ScanEmptyBlockInclusiveAncestor(const HTMLEditor& aHTMLEditor,
6348 nsIContent& aStartContent) {
6349 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
6350 MOZ_ASSERT(!mEmptyInclusiveAncestorBlockElement);
6352 // If we are inside an empty block, delete it.
6353 // Note: do NOT delete table elements this way.
6354 // Note: do NOT delete non-editable block element.
6355 Element* editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
6356 aStartContent, HTMLEditUtils::ClosestEditableBlockElement,
6357 BlockInlineCheck::UseComputedDisplayOutsideStyle);
6358 if (!editableBlockElement) {
6359 return nullptr;
6361 // XXX Perhaps, this is slow loop. If empty blocks are nested, then,
6362 // each block checks whether it's empty or not. However, descendant
6363 // blocks are checked again and again by IsEmptyNode(). Perhaps, it
6364 // should be able to take "known empty element" for avoiding same checks.
6365 while (editableBlockElement &&
6366 HTMLEditUtils::IsRemovableFromParentNode(*editableBlockElement) &&
6367 !HTMLEditUtils::IsAnyTableElement(editableBlockElement) &&
6368 HTMLEditUtils::IsEmptyNode(*editableBlockElement)) {
6369 // If the removable empty list item is a child of editing host list element,
6370 // we should not delete it.
6371 if (HTMLEditUtils::IsListItem(editableBlockElement)) {
6372 Element* const parentElement = editableBlockElement->GetParentElement();
6373 if (parentElement && HTMLEditUtils::IsAnyListElement(parentElement) &&
6374 !HTMLEditUtils::IsRemovableFromParentNode(*parentElement) &&
6375 HTMLEditUtils::IsEmptyNode(*parentElement)) {
6376 break;
6379 mEmptyInclusiveAncestorBlockElement = editableBlockElement;
6380 editableBlockElement = HTMLEditUtils::GetAncestorElement(
6381 *mEmptyInclusiveAncestorBlockElement,
6382 HTMLEditUtils::ClosestEditableBlockElement,
6383 BlockInlineCheck::UseComputedDisplayOutsideStyle);
6385 if (!mEmptyInclusiveAncestorBlockElement) {
6386 return nullptr;
6389 // XXX Because of not checking whether found block element is editable
6390 // in the above loop, empty ediable block element may be overwritten
6391 // with empty non-editable clock element. Therefore, we fail to
6392 // remove the found empty nodes.
6393 if (NS_WARN_IF(!mEmptyInclusiveAncestorBlockElement->IsEditable()) ||
6394 NS_WARN_IF(!mEmptyInclusiveAncestorBlockElement->GetParentElement())) {
6395 mEmptyInclusiveAncestorBlockElement = nullptr;
6397 return mEmptyInclusiveAncestorBlockElement;
6400 nsresult HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
6401 ComputeTargetRanges(const HTMLEditor& aHTMLEditor,
6402 nsIEditor::EDirection aDirectionAndAmount,
6403 const Element& aEditingHost,
6404 AutoRangeArray& aRangesToDelete) const {
6405 MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
6407 // We'll delete `mEmptyInclusiveAncestorBlockElement` node from the tree, but
6408 // we should return the range from start/end of next/previous editable content
6409 // to end/start of the element for compatiblity with the other browsers.
6410 switch (aDirectionAndAmount) {
6411 case nsIEditor::eNone:
6412 break;
6413 case nsIEditor::ePrevious:
6414 case nsIEditor::ePreviousWord:
6415 case nsIEditor::eToBeginningOfLine: {
6416 EditorRawDOMPoint startPoint =
6417 HTMLEditUtils::GetPreviousEditablePoint<EditorRawDOMPoint>(
6418 *mEmptyInclusiveAncestorBlockElement, &aEditingHost,
6419 // In this case, we don't join block elements so that we won't
6420 // delete invisible trailing whitespaces in the previous element.
6421 InvisibleWhiteSpaces::Preserve,
6422 // In this case, we won't join table cells so that we should
6423 // get a range which is in a table cell even if it's in a
6424 // table.
6425 TableBoundary::NoCrossAnyTableElement);
6426 if (!startPoint.IsSet()) {
6427 NS_WARNING(
6428 "HTMLEditUtils::GetPreviousEditablePoint() didn't return a valid "
6429 "point");
6430 return NS_ERROR_FAILURE;
6432 nsresult rv = aRangesToDelete.SetStartAndEnd(
6433 startPoint,
6434 EditorRawDOMPoint::AtEndOf(mEmptyInclusiveAncestorBlockElement));
6435 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
6436 "AutoRangeArray::SetStartAndEnd() failed");
6437 return rv;
6439 case nsIEditor::eNext:
6440 case nsIEditor::eNextWord:
6441 case nsIEditor::eToEndOfLine: {
6442 EditorRawDOMPoint endPoint =
6443 HTMLEditUtils::GetNextEditablePoint<EditorRawDOMPoint>(
6444 *mEmptyInclusiveAncestorBlockElement, &aEditingHost,
6445 // In this case, we don't join block elements so that we won't
6446 // delete invisible trailing whitespaces in the next element.
6447 InvisibleWhiteSpaces::Preserve,
6448 // In this case, we won't join table cells so that we should
6449 // get a range which is in a table cell even if it's in a
6450 // table.
6451 TableBoundary::NoCrossAnyTableElement);
6452 if (!endPoint.IsSet()) {
6453 NS_WARNING(
6454 "HTMLEditUtils::GetNextEditablePoint() didn't return a valid "
6455 "point");
6456 return NS_ERROR_FAILURE;
6458 nsresult rv = aRangesToDelete.SetStartAndEnd(
6459 EditorRawDOMPoint(mEmptyInclusiveAncestorBlockElement, 0), endPoint);
6460 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
6461 "AutoRangeArray::SetStartAndEnd() failed");
6462 return rv;
6464 default:
6465 MOZ_ASSERT_UNREACHABLE("Handle the nsIEditor::EDirection value");
6466 break;
6468 // No direction, let's select the element to be deleted.
6469 nsresult rv =
6470 aRangesToDelete.SelectNode(*mEmptyInclusiveAncestorBlockElement);
6471 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::SelectNode() failed");
6472 return rv;
6475 Result<RefPtr<Element>, nsresult>
6476 HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::
6477 MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor) {
6478 MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
6479 MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement());
6480 MOZ_ASSERT(HTMLEditUtils::IsListItem(mEmptyInclusiveAncestorBlockElement));
6482 // If the found empty block is a list item element and its grand parent
6483 // (i.e., parent of list element) is NOT a list element, insert <br>
6484 // element before the list element which has the empty list item.
6485 // This odd list structure may occur if `Document.execCommand("indent")`
6486 // is performed for list items.
6487 // XXX Chrome does not remove empty list elements when last content in
6488 // last list item is deleted. We should follow it since current
6489 // behavior is annoying when you type new list item with selecting
6490 // all list items.
6491 if (!HTMLEditUtils::IsFirstChild(*mEmptyInclusiveAncestorBlockElement,
6492 {WalkTreeOption::IgnoreNonEditableNode})) {
6493 return RefPtr<Element>();
6496 EditorDOMPoint atParentOfEmptyListItem(
6497 mEmptyInclusiveAncestorBlockElement->GetParentElement());
6498 if (NS_WARN_IF(!atParentOfEmptyListItem.IsSet())) {
6499 return Err(NS_ERROR_FAILURE);
6501 if (HTMLEditUtils::IsAnyListElement(atParentOfEmptyListItem.GetContainer())) {
6502 return RefPtr<Element>();
6504 Result<CreateElementResult, nsresult> insertBRElementResult =
6505 aHTMLEditor.InsertBRElement(WithTransaction::Yes,
6506 atParentOfEmptyListItem);
6507 if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
6508 NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
6509 return insertBRElementResult.propagateErr();
6511 CreateElementResult unwrappedInsertBRElementResult =
6512 insertBRElementResult.unwrap();
6513 nsresult rv = unwrappedInsertBRElementResult.SuggestCaretPointTo(
6514 aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion,
6515 SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
6516 SuggestCaret::AndIgnoreTrivialError});
6517 if (NS_FAILED(rv)) {
6518 NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
6519 return Err(rv);
6521 MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
6522 return unwrappedInsertBRElementResult.UnwrapNewNode();
6525 Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler::
6526 AutoEmptyBlockAncestorDeleter::GetNewCaretPosition(
6527 const HTMLEditor& aHTMLEditor,
6528 nsIEditor::EDirection aDirectionAndAmount) const {
6529 MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
6530 MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement());
6531 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
6533 switch (aDirectionAndAmount) {
6534 case nsIEditor::eNext:
6535 case nsIEditor::eNextWord:
6536 case nsIEditor::eToEndOfLine: {
6537 // Collapse Selection to next node of after empty block element
6538 // if there is. Otherwise, to just after the empty block.
6539 auto afterEmptyBlock(
6540 EditorDOMPoint::After(mEmptyInclusiveAncestorBlockElement));
6541 MOZ_ASSERT(afterEmptyBlock.IsSet());
6542 if (nsIContent* nextContentOfEmptyBlock = HTMLEditUtils::GetNextContent(
6543 afterEmptyBlock, {}, BlockInlineCheck::Unused,
6544 aHTMLEditor.ComputeEditingHost())) {
6545 EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>(
6546 *nextContentOfEmptyBlock, aDirectionAndAmount);
6547 if (!pt.IsSet()) {
6548 NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
6549 return Err(NS_ERROR_FAILURE);
6551 return CaretPoint(std::move(pt));
6553 if (NS_WARN_IF(!afterEmptyBlock.IsSet())) {
6554 return Err(NS_ERROR_FAILURE);
6556 return CaretPoint(std::move(afterEmptyBlock));
6558 case nsIEditor::ePrevious:
6559 case nsIEditor::ePreviousWord:
6560 case nsIEditor::eToBeginningOfLine: {
6561 // Collapse Selection to previous editable node of the empty block
6562 // if there is. Otherwise, to after the empty block.
6563 EditorRawDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement);
6564 if (nsIContent* previousContentOfEmptyBlock =
6565 HTMLEditUtils::GetPreviousContent(
6566 atEmptyBlock, {WalkTreeOption::IgnoreNonEditableNode},
6567 BlockInlineCheck::Unused, aHTMLEditor.ComputeEditingHost())) {
6568 EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>(
6569 *previousContentOfEmptyBlock, aDirectionAndAmount);
6570 if (!pt.IsSet()) {
6571 NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
6572 return Err(NS_ERROR_FAILURE);
6574 return CaretPoint(std::move(pt));
6576 auto afterEmptyBlock =
6577 EditorDOMPoint::After(*mEmptyInclusiveAncestorBlockElement);
6578 if (NS_WARN_IF(!afterEmptyBlock.IsSet())) {
6579 return Err(NS_ERROR_FAILURE);
6581 return CaretPoint(std::move(afterEmptyBlock));
6583 case nsIEditor::eNone: {
6584 // Collapse selection at the removing block when we are replacing
6585 // selected content.
6586 EditorDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement);
6587 if (NS_WARN_IF(!atEmptyBlock.IsSet())) {
6588 return Err(NS_ERROR_FAILURE);
6590 return CaretPoint(std::move(atEmptyBlock));
6592 default:
6593 MOZ_CRASH(
6594 "AutoEmptyBlockAncestorDeleter doesn't support this action yet");
6595 return Err(NS_ERROR_FAILURE);
6599 Result<EditActionResult, nsresult>
6600 HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::Run(
6601 HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount) {
6602 MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement);
6603 MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement());
6604 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
6607 Result<EditActionResult, nsresult> result =
6608 MaybeReplaceSubListWithNewListItem(aHTMLEditor);
6609 if (MOZ_UNLIKELY(result.isErr())) {
6610 NS_WARNING(
6611 "AutoEmptyBlockAncestorDeleter::MaybeReplaceSubListWithNewListItem() "
6612 "failed");
6613 return result;
6615 if (result.inspect().Handled()) {
6616 return result;
6620 if (HTMLEditUtils::IsListItem(mEmptyInclusiveAncestorBlockElement)) {
6621 Result<RefPtr<Element>, nsresult> result =
6622 MaybeInsertBRElementBeforeEmptyListItemElement(aHTMLEditor);
6623 if (MOZ_UNLIKELY(result.isErr())) {
6624 NS_WARNING(
6625 "AutoEmptyBlockAncestorDeleter::"
6626 "MaybeInsertBRElementBeforeEmptyListItemElement() failed");
6627 return result.propagateErr();
6629 // If a `<br>` element is inserted, caret should be moved to after it.
6630 if (RefPtr<Element> brElement = result.unwrap()) {
6631 nsresult rv =
6632 aHTMLEditor.CollapseSelectionTo(EditorRawDOMPoint(brElement));
6633 if (NS_FAILED(rv)) {
6634 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
6635 "EditorBase::CollapseSelectionTo() failed");
6636 return Err(rv);
6639 } else {
6640 Result<CaretPoint, nsresult> result =
6641 GetNewCaretPosition(aHTMLEditor, aDirectionAndAmount);
6642 if (MOZ_UNLIKELY(result.isErr())) {
6643 NS_WARNING("AutoEmptyBlockAncestorDeleter::GetNewCaretPosition() failed");
6644 return result.propagateErr();
6646 MOZ_ASSERT(result.inspect().HasCaretPointSuggestion());
6647 nsresult rv = result.inspect().SuggestCaretPointTo(aHTMLEditor, {});
6648 if (NS_FAILED(rv)) {
6649 NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
6650 return Err(rv);
6653 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
6654 MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement));
6655 if (NS_FAILED(rv)) {
6656 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
6657 return Err(rv);
6659 return EditActionResult::HandledResult();
6662 Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::
6663 AutoEmptyBlockAncestorDeleter::MaybeReplaceSubListWithNewListItem(
6664 HTMLEditor& aHTMLEditor) {
6665 // If we're deleting sublist element and it's the last list item of its parent
6666 // list, we should replace it with a list element.
6667 if (!HTMLEditUtils::IsAnyListElement(mEmptyInclusiveAncestorBlockElement)) {
6668 return EditActionResult::IgnoredResult();
6670 RefPtr<Element> parentElement =
6671 mEmptyInclusiveAncestorBlockElement->GetParentElement();
6672 if (!parentElement || !HTMLEditUtils::IsAnyListElement(parentElement) ||
6673 !HTMLEditUtils::IsEmptyNode(
6674 *parentElement,
6675 {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
6676 return EditActionResult::IgnoredResult();
6679 nsCOMPtr<nsINode> nextSibling =
6680 mEmptyInclusiveAncestorBlockElement->GetNextSibling();
6681 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(
6682 MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement));
6683 if (NS_FAILED(rv)) {
6684 NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
6685 return Err(rv);
6687 Result<CreateElementResult, nsresult> insertListItemResult =
6688 aHTMLEditor.CreateAndInsertElement(
6689 WithTransaction::Yes,
6690 parentElement->IsHTMLElement(nsGkAtoms::dl) ? *nsGkAtoms::dd
6691 : *nsGkAtoms::li,
6692 !nextSibling || nextSibling->GetParentNode() != parentElement
6693 ? EditorDOMPoint::AtEndOf(*parentElement)
6694 : EditorDOMPoint(nextSibling),
6695 [](HTMLEditor& aHTMLEditor, Element& aNewElement,
6696 const EditorDOMPoint& aPointToInsert) -> nsresult {
6697 RefPtr<Element> brElement =
6698 aHTMLEditor.CreateHTMLContent(nsGkAtoms::br);
6699 if (MOZ_UNLIKELY(!brElement)) {
6700 NS_WARNING(
6701 "EditorBase::CreateHTMLContent(nsGkAtoms::br) failed, but "
6702 "ignored");
6703 return NS_OK; // Just gives up to insert <br>
6705 IgnoredErrorResult error;
6706 aNewElement.AppendChild(*brElement, error);
6707 NS_WARNING_ASSERTION(!error.Failed(),
6708 "nsINode::AppendChild() failed, but ignored");
6709 return NS_OK;
6711 if (MOZ_UNLIKELY(insertListItemResult.isErr())) {
6712 NS_WARNING("HTMLEditor::CreateAndInsertElement() failed");
6713 return insertListItemResult.propagateErr();
6715 CreateElementResult unwrappedInsertListItemResult =
6716 insertListItemResult.unwrap();
6717 unwrappedInsertListItemResult.IgnoreCaretPointSuggestion();
6718 rv = aHTMLEditor.CollapseSelectionTo(
6719 EditorRawDOMPoint(unwrappedInsertListItemResult.GetNewNode(), 0u));
6720 if (NS_FAILED(rv)) {
6721 NS_WARNING("EditorBase::CollapseSelectionTo() failed");
6722 return Err(rv);
6724 return EditActionResult::HandledResult();
6727 template <typename EditorDOMRangeType>
6728 Result<EditorRawDOMRange, nsresult>
6729 HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete(
6730 const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection,
6731 const EditorDOMRangeType& aRangeToDelete) const {
6732 MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
6733 MOZ_ASSERT(!aRangeToDelete.Collapsed());
6734 MOZ_ASSERT(aRangeToDelete.IsPositioned());
6736 const nsIContent* commonAncestor = nsIContent::FromNodeOrNull(
6737 nsContentUtils::GetClosestCommonInclusiveAncestor(
6738 aRangeToDelete.StartRef().GetContainer(),
6739 aRangeToDelete.EndRef().GetContainer()));
6740 if (MOZ_UNLIKELY(NS_WARN_IF(!commonAncestor))) {
6741 return Err(NS_ERROR_FAILURE);
6744 // Editing host may be nested and outer one could have focus. Let's use
6745 // the closest editing host instead.
6746 const RefPtr<Element> closestEditingHost =
6747 aHTMLEditor.ComputeEditingHost(*commonAncestor, LimitInBodyElement::No);
6748 if (NS_WARN_IF(!closestEditingHost)) {
6749 return Err(NS_ERROR_FAILURE);
6752 // Look for the common ancestor's block element in the editing host. It's
6753 // fine that we get non-editable block element which is ancestor of inline
6754 // editing host because the following code checks editing host too.
6755 const RefPtr<Element> closestBlockAncestorOrInlineEditingHost = [&]() {
6756 // Note that if non-closest editing host has focus, found block may be
6757 // non-editable.
6758 if (Element* const maybeEditableBlockElement =
6759 HTMLEditUtils::GetInclusiveAncestorElement(
6760 *commonAncestor, HTMLEditUtils::ClosestBlockElement,
6761 BlockInlineCheck::UseComputedDisplayOutsideStyle,
6762 closestEditingHost)) {
6763 return maybeEditableBlockElement;
6765 return closestEditingHost.get();
6766 }();
6768 // Set up for loops and cache our root element
6769 // If only one list element is selected, and if the list element is empty,
6770 // we should delete only the list element. Or if the list element is not
6771 // empty, we should make the list has only one empty list item element.
6772 if (const Element* maybeListElement =
6773 HTMLEditUtils::GetElementIfOnlyOneSelected(aRangeToDelete)) {
6774 if (HTMLEditUtils::IsAnyListElement(maybeListElement) &&
6775 !HTMLEditUtils::IsEmptyAnyListElement(*maybeListElement)) {
6776 EditorRawDOMRange range =
6777 HTMLEditUtils::GetRangeSelectingAllContentInAllListItems<
6778 EditorRawDOMRange>(*maybeListElement);
6779 if (range.IsPositioned()) {
6780 if (EditorUtils::IsEditableContent(
6781 *range.StartRef().ContainerAs<nsIContent>(),
6782 EditorType::HTML) &&
6783 EditorUtils::IsEditableContent(
6784 *range.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
6785 return range;
6788 // If the first and/or last list item is not editable, we need to do more
6789 // complicated things probably, but we just delete the list element with
6790 // invisible things around it for now since it must be rare case.
6792 // Otherwise, if the list item is empty, we should delete it with invisible
6793 // things around it.
6796 // Find previous visible things before start of selection
6797 EditorRawDOMRange rangeToDelete(aRangeToDelete);
6798 if (rangeToDelete.StartRef().GetContainer() !=
6799 closestBlockAncestorOrInlineEditingHost) {
6800 for (;;) {
6801 WSScanResult backwardScanFromStartResult =
6802 WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
6803 closestEditingHost, rangeToDelete.StartRef(),
6804 BlockInlineCheck::UseComputedDisplayOutsideStyle);
6805 if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary()) {
6806 break;
6808 MOZ_ASSERT(backwardScanFromStartResult.GetContent() ==
6809 WSRunScanner(closestEditingHost, rangeToDelete.StartRef(),
6810 BlockInlineCheck::UseComputedDisplayOutsideStyle)
6811 .GetStartReasonContent());
6812 // We want to keep looking up. But stop if we are crossing table
6813 // element boundaries, or if we hit the root.
6814 if (HTMLEditUtils::IsAnyTableElement(
6815 backwardScanFromStartResult.GetContent()) ||
6816 backwardScanFromStartResult.GetContent() ==
6817 closestBlockAncestorOrInlineEditingHost ||
6818 backwardScanFromStartResult.GetContent() == closestEditingHost) {
6819 break;
6821 // Don't cross list element boundary because we don't want to delete list
6822 // element at start position unless it's empty.
6823 if (HTMLEditUtils::IsAnyListElement(
6824 backwardScanFromStartResult.GetContent()) &&
6825 !HTMLEditUtils::IsEmptyAnyListElement(
6826 *backwardScanFromStartResult.ElementPtr())) {
6827 break;
6829 // Don't cross flex-item/grid-item boundary to make new content inserted
6830 // into it.
6831 if (StaticPrefs::editor_block_inline_check_use_computed_style() &&
6832 backwardScanFromStartResult.ContentIsElement() &&
6833 HTMLEditUtils::IsFlexOrGridItem(
6834 *backwardScanFromStartResult.ElementPtr())) {
6835 break;
6837 rangeToDelete.SetStart(
6838 backwardScanFromStartResult.PointAtContent<EditorRawDOMPoint>());
6840 if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint(
6841 rangeToDelete.StartRef().GetContainer())) {
6842 NS_WARNING("Computed start container was out of selection limiter");
6843 return Err(NS_ERROR_FAILURE);
6847 // Expand selection endpoint only if we don't pass an invisible `<br>`, or if
6848 // we really needed to pass that `<br>` (i.e., its block is now totally
6849 // selected).
6851 // Find next visible things after end of selection
6852 EditorDOMPoint atFirstInvisibleBRElement;
6853 if (rangeToDelete.EndRef().GetContainer() !=
6854 closestBlockAncestorOrInlineEditingHost) {
6855 for (;;) {
6856 WSRunScanner wsScannerAtEnd(
6857 closestEditingHost, rangeToDelete.EndRef(),
6858 BlockInlineCheck::UseComputedDisplayOutsideStyle);
6859 WSScanResult forwardScanFromEndResult =
6860 wsScannerAtEnd.ScanNextVisibleNodeOrBlockBoundaryFrom(
6861 rangeToDelete.EndRef());
6862 if (forwardScanFromEndResult.ReachedBRElement()) {
6863 // XXX In my understanding, this is odd. The end reason may not be
6864 // same as the reached <br> element because the equality is
6865 // guaranteed only when ReachedCurrentBlockBoundary() returns true.
6866 // However, looks like that this code assumes that
6867 // GetEndReasonContent() returns the (or a) <br> element.
6868 NS_ASSERTION(wsScannerAtEnd.GetEndReasonContent() ==
6869 forwardScanFromEndResult.BRElementPtr(),
6870 "End reason is not the reached <br> element");
6871 if (HTMLEditUtils::IsVisibleBRElement(
6872 *wsScannerAtEnd.GetEndReasonContent())) {
6873 break;
6875 if (!atFirstInvisibleBRElement.IsSet()) {
6876 atFirstInvisibleBRElement =
6877 rangeToDelete.EndRef().To<EditorDOMPoint>();
6879 rangeToDelete.SetEnd(
6880 EditorRawDOMPoint::After(*wsScannerAtEnd.GetEndReasonContent()));
6881 continue;
6884 if (forwardScanFromEndResult.ReachedCurrentBlockBoundary()) {
6885 MOZ_ASSERT(forwardScanFromEndResult.GetContent() ==
6886 wsScannerAtEnd.GetEndReasonContent());
6887 // We want to keep looking up. But stop if we are crossing table
6888 // element boundaries, or if we hit the root.
6889 if (HTMLEditUtils::IsAnyTableElement(
6890 forwardScanFromEndResult.GetContent()) ||
6891 forwardScanFromEndResult.GetContent() ==
6892 closestBlockAncestorOrInlineEditingHost) {
6893 break;
6895 // Don't cross flex-item/grid-item boundary to make new content inserted
6896 // into it.
6897 if (StaticPrefs::editor_block_inline_check_use_computed_style() &&
6898 forwardScanFromEndResult.ContentIsElement() &&
6899 HTMLEditUtils::IsFlexOrGridItem(
6900 *forwardScanFromEndResult.ElementPtr())) {
6901 break;
6903 rangeToDelete.SetEnd(
6904 forwardScanFromEndResult.PointAfterContent<EditorRawDOMPoint>());
6905 continue;
6908 break;
6911 if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint(
6912 rangeToDelete.EndRef().GetContainer())) {
6913 NS_WARNING("Computed end container was out of selection limiter");
6914 return Err(NS_ERROR_FAILURE);
6918 // If range boundaries are in list element, and the positions are very
6919 // start/end of first/last list item, we may need to shrink the ranges for
6920 // preventing to remove only all list item elements.
6922 EditorRawDOMRange rangeToDeleteListOrLeaveOneEmptyListItem =
6923 AutoDeleteRangesHandler::
6924 GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
6925 rangeToDelete);
6926 if (rangeToDeleteListOrLeaveOneEmptyListItem.IsPositioned()) {
6927 rangeToDelete = std::move(rangeToDeleteListOrLeaveOneEmptyListItem);
6931 if (atFirstInvisibleBRElement.IsInContentNode()) {
6932 // Find block node containing invisible `<br>` element.
6933 if (const RefPtr<const Element> editableBlockContainingBRElement =
6934 HTMLEditUtils::GetInclusiveAncestorElement(
6935 *atFirstInvisibleBRElement.ContainerAs<nsIContent>(),
6936 HTMLEditUtils::ClosestEditableBlockElement,
6937 BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
6938 if (rangeToDelete.Contains(
6939 EditorRawDOMPoint(editableBlockContainingBRElement))) {
6940 return rangeToDelete;
6942 // Otherwise, the new range should end at the invisible `<br>`.
6943 if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint(
6944 atFirstInvisibleBRElement.GetContainer())) {
6945 NS_WARNING(
6946 "Computed end container (`<br>` element) was out of selection "
6947 "limiter");
6948 return Err(NS_ERROR_FAILURE);
6950 rangeToDelete.SetEnd(atFirstInvisibleBRElement);
6954 return rangeToDelete;
6957 // static
6958 EditorRawDOMRange HTMLEditor::AutoDeleteRangesHandler::
6959 GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
6960 const EditorRawDOMRange& aRangeToDelete) {
6961 MOZ_ASSERT(aRangeToDelete.IsPositionedAndValid());
6963 auto GetDeepestEditableStartPointOfList = [](Element& aListElement) {
6964 Element* const firstListItemElement =
6965 HTMLEditUtils::GetFirstListItemElement(aListElement);
6966 if (MOZ_UNLIKELY(!firstListItemElement)) {
6967 return EditorRawDOMPoint();
6969 if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(*firstListItemElement,
6970 EditorType::HTML))) {
6971 return EditorRawDOMPoint(firstListItemElement);
6973 return HTMLEditUtils::GetDeepestEditableStartPointOf<EditorRawDOMPoint>(
6974 *firstListItemElement);
6977 auto GetDeepestEditableEndPointOfList = [](Element& aListElement) {
6978 Element* const lastListItemElement =
6979 HTMLEditUtils::GetLastListItemElement(aListElement);
6980 if (MOZ_UNLIKELY(!lastListItemElement)) {
6981 return EditorRawDOMPoint();
6983 if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(*lastListItemElement,
6984 EditorType::HTML))) {
6985 return EditorRawDOMPoint::After(*lastListItemElement);
6987 return HTMLEditUtils::GetDeepestEditableEndPointOf<EditorRawDOMPoint>(
6988 *lastListItemElement);
6991 Element* const startListElement =
6992 aRangeToDelete.StartRef().IsInContentNode()
6993 ? HTMLEditUtils::GetClosestInclusiveAncestorAnyListElement(
6994 *aRangeToDelete.StartRef().ContainerAs<nsIContent>())
6995 : nullptr;
6996 Element* const endListElement =
6997 aRangeToDelete.EndRef().IsInContentNode()
6998 ? HTMLEditUtils::GetClosestInclusiveAncestorAnyListElement(
6999 *aRangeToDelete.EndRef().ContainerAs<nsIContent>())
7000 : nullptr;
7001 if (!startListElement && !endListElement) {
7002 return EditorRawDOMRange();
7005 // FIXME: If there are invalid children, we cannot handle first/last list item
7006 // elements properly. In that case, we should treat list elements and list
7007 // item elements as normal block elements.
7008 if (startListElement &&
7009 NS_WARN_IF(!HTMLEditUtils::IsValidListElement(
7010 *startListElement, HTMLEditUtils::TreatSubListElementAs::Valid))) {
7011 return EditorRawDOMRange();
7013 if (endListElement && startListElement != endListElement &&
7014 NS_WARN_IF(!HTMLEditUtils::IsValidListElement(
7015 *endListElement, HTMLEditUtils::TreatSubListElementAs::Valid))) {
7016 return EditorRawDOMRange();
7019 const bool startListElementIsEmpty =
7020 startListElement &&
7021 HTMLEditUtils::IsEmptyAnyListElement(*startListElement);
7022 const bool endListElementIsEmpty =
7023 startListElement == endListElement
7024 ? startListElementIsEmpty
7025 : endListElement &&
7026 HTMLEditUtils::IsEmptyAnyListElement(*endListElement);
7027 // If both list elements are empty, we should not shrink the range since
7028 // we want to delete the list.
7029 if (startListElementIsEmpty && endListElementIsEmpty) {
7030 return EditorRawDOMRange();
7033 // There may be invisible white-spaces and there are elements in the
7034 // list items. Therefore, we need to compare the deepest positions
7035 // and range boundaries.
7036 EditorRawDOMPoint deepestStartPointOfStartList =
7037 startListElement ? GetDeepestEditableStartPointOfList(*startListElement)
7038 : EditorRawDOMPoint();
7039 EditorRawDOMPoint deepestEndPointOfEndList =
7040 endListElement ? GetDeepestEditableEndPointOfList(*endListElement)
7041 : EditorRawDOMPoint();
7042 if (MOZ_UNLIKELY(!deepestStartPointOfStartList.IsSet() &&
7043 !deepestEndPointOfEndList.IsSet())) {
7044 // FIXME: This does not work well if there is non-list-item contents in the
7045 // list elements. Perhaps, for fixing this invalid cases, we need to wrap
7046 // the content into new list item like Chrome.
7047 return EditorRawDOMRange();
7050 // We don't want to shrink the range into empty sublist.
7051 if (deepestStartPointOfStartList.IsSet()) {
7052 for (nsIContent* const maybeList :
7053 deepestStartPointOfStartList.GetContainer()
7054 ->InclusiveAncestorsOfType<nsIContent>()) {
7055 if (aRangeToDelete.StartRef().GetContainer() == maybeList) {
7056 break;
7058 if (HTMLEditUtils::IsAnyListElement(maybeList) &&
7059 HTMLEditUtils::IsEmptyAnyListElement(*maybeList->AsElement())) {
7060 deepestStartPointOfStartList.Set(maybeList);
7064 if (deepestEndPointOfEndList.IsSet()) {
7065 for (nsIContent* const maybeList :
7066 deepestEndPointOfEndList.GetContainer()
7067 ->InclusiveAncestorsOfType<nsIContent>()) {
7068 if (aRangeToDelete.EndRef().GetContainer() == maybeList) {
7069 break;
7071 if (HTMLEditUtils::IsAnyListElement(maybeList) &&
7072 HTMLEditUtils::IsEmptyAnyListElement(*maybeList->AsElement())) {
7073 deepestEndPointOfEndList.SetAfter(maybeList);
7078 const EditorRawDOMPoint deepestEndPointOfStartList =
7079 startListElement ? GetDeepestEditableEndPointOfList(*startListElement)
7080 : EditorRawDOMPoint();
7081 MOZ_ASSERT_IF(deepestStartPointOfStartList.IsSet(),
7082 deepestEndPointOfStartList.IsSet());
7083 MOZ_ASSERT_IF(!deepestStartPointOfStartList.IsSet(),
7084 !deepestEndPointOfStartList.IsSet());
7086 const bool rangeStartsFromBeginningOfStartList =
7087 deepestStartPointOfStartList.IsSet() &&
7088 aRangeToDelete.StartRef().EqualsOrIsBefore(deepestStartPointOfStartList);
7089 const bool rangeEndsByEndingOfStartListOrLater =
7090 !deepestEndPointOfStartList.IsSet() ||
7091 deepestEndPointOfStartList.EqualsOrIsBefore(aRangeToDelete.EndRef());
7092 const bool rangeEndsByEndingOfEndList =
7093 deepestEndPointOfEndList.IsSet() &&
7094 deepestEndPointOfEndList.EqualsOrIsBefore(aRangeToDelete.EndRef());
7096 EditorRawDOMRange newRangeToDelete;
7097 // If all over the list element at start boundary is selected, we should
7098 // shrink the range to start from the first list item to avoid to delete
7099 // all list items.
7100 if (!startListElementIsEmpty && rangeStartsFromBeginningOfStartList &&
7101 rangeEndsByEndingOfStartListOrLater) {
7102 newRangeToDelete.SetStart(EditorRawDOMPoint(
7103 deepestStartPointOfStartList.ContainerAs<nsIContent>(), 0u));
7105 // If all over the list element at end boundary is selected, and...
7106 if (!endListElementIsEmpty && rangeEndsByEndingOfEndList) {
7107 // If the range starts before the range at end boundary of the range,
7108 // we want to delete the list completely, thus, we should extend the
7109 // range to contain the list element.
7110 if (aRangeToDelete.StartRef().IsBefore(
7111 EditorRawDOMPoint(endListElement, 0u))) {
7112 newRangeToDelete.SetEnd(EditorRawDOMPoint::After(*endListElement));
7113 MOZ_ASSERT_IF(newRangeToDelete.StartRef().IsSet(),
7114 newRangeToDelete.IsPositionedAndValid());
7116 // Otherwise, if the range starts in the end list element, we shouldn't
7117 // delete the list. Therefore, we should shrink the range to end by end
7118 // of the last list item element to avoid to delete all list items.
7119 else {
7120 newRangeToDelete.SetEnd(EditorRawDOMPoint::AtEndOf(
7121 *deepestEndPointOfEndList.ContainerAs<nsIContent>()));
7122 MOZ_ASSERT_IF(newRangeToDelete.StartRef().IsSet(),
7123 newRangeToDelete.IsPositionedAndValid());
7127 if (!newRangeToDelete.StartRef().IsSet() &&
7128 !newRangeToDelete.EndRef().IsSet()) {
7129 return EditorRawDOMRange();
7132 if (!newRangeToDelete.StartRef().IsSet()) {
7133 newRangeToDelete.SetStart(aRangeToDelete.StartRef());
7134 MOZ_ASSERT(newRangeToDelete.IsPositionedAndValid());
7136 if (!newRangeToDelete.EndRef().IsSet()) {
7137 newRangeToDelete.SetEnd(aRangeToDelete.EndRef());
7138 MOZ_ASSERT(newRangeToDelete.IsPositionedAndValid());
7141 return newRangeToDelete;
7144 } // namespace mozilla