Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "HTMLEditor.h"
#include "HTMLEditorNestedClasses.h"
#include <algorithm>
#include <utility>
#include "AutoRangeArray.h"
#include "CSSEditUtils.h"
#include "EditAction.h"
#include "EditorDOMPoint.h"
#include "EditorUtils.h"
#include "HTMLEditHelpers.h"
#include "HTMLEditUtils.h"
#include "WSRunObject.h"
#include "ErrorList.h"
#include "js/ErrorReport.h"
#include "mozilla/Assertions.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/ComputedStyle.h" // for ComputedStyle
#include "mozilla/ContentIterator.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/InternalMutationEvent.h"
#include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/SelectionState.h"
#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
#include "mozilla/Unused.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/mozalloc.h"
#include "nsAString.h"
#include "nsAtom.h"
#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsFrameSelection.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsINode.h"
#include "nsRange.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsStyleConsts.h" // for StyleWhiteSpace
#include "nsTArray.h"
// NOTE: This file was split from:
namespace mozilla {
using namespace dom;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using InvisibleWhiteSpaces = HTMLEditUtils::InvisibleWhiteSpaces;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using ScanLineBreak = HTMLEditUtils::ScanLineBreak;
using TableBoundary = HTMLEditUtils::TableBoundary;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
template Result<CaretPoint, nsresult>
HTMLEditor::DeleteTextAndTextNodesWithTransaction(
const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint,
TreatEmptyTextNodes aTreatEmptyTextNodes);
template Result<CaretPoint, nsresult>
HTMLEditor::DeleteTextAndTextNodesWithTransaction(
const EditorDOMPointInText& aStartPoint,
const EditorDOMPointInText& aEndPoint,
TreatEmptyTextNodes aTreatEmptyTextNodes);
/*****************************************************************************
* AutoSetTemporaryAncestorLimiter
****************************************************************************/
class MOZ_RAII AutoSetTemporaryAncestorLimiter final {
public:
AutoSetTemporaryAncestorLimiter(const HTMLEditor& aHTMLEditor,
Selection& aSelection,
nsINode& aStartPointNode,
AutoRangeArray* aRanges = nullptr) {
MOZ_ASSERT(aSelection.GetType() == SelectionType::eNormal);
if (aSelection.GetAncestorLimiter()) {
return;
}
Element* selectionRootElement =
aHTMLEditor.FindSelectionRoot(aStartPointNode);
if (!selectionRootElement) {
return;
}
aHTMLEditor.InitializeSelectionAncestorLimit(*selectionRootElement);
mSelection = &aSelection;
// Setting ancestor limiter may change ranges which were outer of
// the new limiter. Therefore, we need to reinitialize aRanges.
if (aRanges) {
aRanges->Initialize(aSelection);
}
}
~AutoSetTemporaryAncestorLimiter() {
if (mSelection) {
mSelection->SetAncestorLimiter(nullptr);
}
}
private:
RefPtr<Selection> mSelection;
};
/*****************************************************************************
* AutoDeleteRangesHandler
****************************************************************************/
class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
public:
explicit AutoDeleteRangesHandler(
const AutoDeleteRangesHandler* aParent = nullptr)
: mParent(aParent),
mOriginalDirectionAndAmount(nsIEditor::eNone),
mOriginalStripWrappers(nsIEditor::eNoStrip) {}
/**
* ComputeRangesToDelete() computes actual deletion ranges.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ComputeRangesToDelete(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
/**
* Deletes content in or around aRangesToDelete.
* NOTE: This method creates SelectionBatcher. Therefore, each caller
* needs to check if the editor is still available even if this returns
* NS_OK.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
const Element& aEditingHost);
private:
bool IsHandlingRecursively() const { return mParent != nullptr; }
bool CanFallbackToDeleteRangesWithTransaction(
const AutoRangeArray& aRangesToDelete) const {
return !IsHandlingRecursively() && !aRangesToDelete.Ranges().IsEmpty() &&
(!aRangesToDelete.IsCollapsed() ||
EditorBase::HowToHandleCollapsedRangeFor(
mOriginalDirectionAndAmount) !=
EditorBase::HowToHandleCollapsedRange::Ignore);
}
/**
* HandleDeleteAroundCollapsedRanges() handles deletion with collapsed
* ranges. Callers must guarantee that this is called only when
* aRangesToDelete.IsCollapsed() returns true.
*
* @param aDirectionAndAmount Direction of the deletion.
* @param aStripWrappers Must be eStrip or eNoStrip.
* @param aRangesToDelete Ranges to delete. This `IsCollapsed()` must
* return true.
* @param aWSRunScannerAtCaret Scanner instance which scanned from
* caret point.
* @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret
* toward aDirectionAndAmount.
* @param aEditingHost The editing host.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
HandleDeleteAroundCollapsedRanges(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
const WSRunScanner& aWSRunScannerAtCaret,
const WSScanResult& aScanFromCaretPointResult,
const Element& aEditingHost);
nsresult ComputeRangesToDeleteAroundCollapsedRanges(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret,
const WSScanResult& aScanFromCaretPointResult,
const Element& aEditingHost) const;
/**
* HandleDeleteNonCollapsedRanges() handles deletion with non-collapsed
* ranges. Callers must guarantee that this is called only when
* aRangesToDelete.IsCollapsed() returns false.
*
* @param aDirectionAndAmount Direction of the deletion.
* @param aStripWrappers Must be eStrip or eNoStrip.
* @param aRangesToDelete The ranges to delete.
* @param aSelectionWasCollapsed If the caller extended `Selection`
* from collapsed, set this to `Yes`.
* Otherwise, i.e., `Selection` is not
* collapsed from the beginning, set
* this to `No`.
* @param aEditingHost The editing host.
*/
enum class SelectionWasCollapsed { Yes, No };
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
HandleDeleteNonCollapsedRanges(HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
AutoRangeArray& aRangesToDelete,
SelectionWasCollapsed aSelectionWasCollapsed,
const Element& aEditingHost);
nsresult ComputeRangesToDeleteNonCollapsedRanges(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete,
SelectionWasCollapsed aSelectionWasCollapsed,
const Element& aEditingHost) const;
/**
* Handle deletion of collapsed ranges in a text node.
*
* @param aDirectionAndAmount Must be eNext or ePrevious.
* @param aCaretPosition The position where caret is. This container
* must be a text node.
* @param aEditingHost The editing host.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
HandleDeleteTextAroundCollapsedRanges(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
nsresult ComputeRangesToDeleteTextAroundCollapsedRanges(
nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const;
/**
* Handles deletion of collapsed selection at white-spaces in a text node.
*
* @param aDirectionAndAmount Direction of the deletion.
* @param aPointToDelete The point to delete. I.e., typically, caret
* position.
* @param aEditingHost The editing host.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
HandleDeleteCollapsedSelectionAtWhiteSpaces(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
const EditorDOMPoint& aPointToDelete, const Element& aEditingHost);
/**
* Handle deletion of collapsed selection in a text node.
*
* @param aDirectionAndAmount Direction of the deletion.
* @param aRangesToDelete Computed selection ranges to delete.
* @param aPointAtDeletingChar The visible char position which you want to
* delete.
* @param aEditingHost The editing host.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
HandleDeleteCollapsedSelectionAtVisibleChar(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete,
const EditorDOMPoint& aPointAtDeletingChar, const Element& aEditingHost);
/**
* Handle deletion of atomic elements like <br>, <hr>, <img>, <input>, etc and
* data nodes except text node (e.g., comment node). Note that don't call this
* directly with `<hr>` element.
*
* @param aAtomicContent The atomic content to be deleted.
* @param aCaretPoint The caret point (i.e., selection start or
* end).
* @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
* with the caret point.
* @param aEditingHost The editing host.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
HandleDeleteAtomicContent(HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
const EditorDOMPoint& aCaretPoint,
const WSRunScanner& aWSRunScannerAtCaret,
const Element& aEditingHost);
nsresult ComputeRangesToDeleteAtomicContent(
Element* aEditingHost, const nsIContent& aAtomicContent,
AutoRangeArray& aRangesToDelete) const;
/**
* GetAtomicContnetToDelete() returns better content that is deletion of
* atomic element. If aScanFromCaretPointResult is special, since this
* point may not be editable, we look for better point to remove atomic
* content.
*
* @param aDirectionAndAmount Direction of the deletion.
* @param aWSRunScannerAtCaret WSRunScanner instance which was
* initialized with the caret point.
* @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret
* toward aDirectionAndAmount.
*/
static nsIContent* GetAtomicContentToDelete(
nsIEditor::EDirection aDirectionAndAmount,
const WSRunScanner& aWSRunScannerAtCaret,
const WSScanResult& aScanFromCaretPointResult) MOZ_NONNULL_RETURN;
/**
* HandleDeleteAtOtherBlockBoundary() handles deletion at other block boundary
* (i.e., immediately before or after a block). If this does not join blocks,
* `Run()` may be called recursively with creating another instance.
*
* @param aDirectionAndAmount Direction of the deletion.
* @param aStripWrappers Must be eStrip or eNoStrip.
* @param aOtherBlockElement The block element which follows the caret or
* is followed by caret.
* @param aCaretPoint The caret point (i.e., selection start or
* end).
* @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
* with the caret point.
* @param aRangesToDelete Ranges to delete of the caller. This should
* be collapsed and the point should match with
* aCaretPoint.
* @param aEditingHost The editing host.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
HandleDeleteAtOtherBlockBoundary(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers, Element& aOtherBlockElement,
const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret,
AutoRangeArray& aRangesToDelete, const Element& aEditingHost);
/**
* ExtendOrShrinkRangeToDelete() extends aRangeToDelete if there are
* an invisible <br> element and/or some parent empty elements.
*
* @param aFrameSelection If the caller wants range in selection limiter,
* set this to non-nullptr which knows the limiter.
* @param aRangeToDelete The range to be extended for deletion. This
* must not be collapsed, must be positioned.
*/
template <typename EditorDOMRangeType>
Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToDelete(
const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection,
const EditorDOMRangeType& aRangeToDelete) const;
/**
* A helper method for ExtendOrShrinkRangeToDelete(). This returns shrunken
* range if aRangeToDelete selects all over list elements which have some list
* item elements to avoid to delete all list items from the list element.
*/
MOZ_NEVER_INLINE_DEBUG static EditorRawDOMRange
GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
const EditorRawDOMRange& aRangeToDelete);
/**
* DeleteUnnecessaryNodes() removes unnecessary nodes around aRange.
* Note that aRange is tracked with AutoTrackDOMRange.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
DeleteUnnecessaryNodes(HTMLEditor& aHTMLEditor, EditorDOMRange& aRange);
/**
* DeleteUnnecessaryNodesAndCollapseSelection() calls DeleteUnnecessaryNodes()
* and then, collapse selection at tracked aSelectionStartPoint or
* aSelectionEndPoint (depending on aDirectionAndAmount).
*
* @param aDirectionAndAmount Direction of the deletion.
* If nsIEditor::ePrevious, selection
* will be collapsed to aSelectionEndPoint.
* Otherwise, selection will be collapsed
* to aSelectionStartPoint.
* @param aSelectionStartPoint First selection range start after
* computing the deleting range.
* @param aSelectionEndPoint First selection range end after
* computing the deleting range.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
DeleteUnnecessaryNodesAndCollapseSelection(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
const EditorDOMPoint& aSelectionStartPoint,
const EditorDOMPoint& aSelectionEndPoint);
/**
* If aContent is a text node that contains only collapsed white-space or
* empty and editable.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
DeleteNodeIfInvisibleAndEditableTextNode(HTMLEditor& aHTMLEditor,
nsIContent& aContent);
/**
* DeleteParentBlocksIfEmpty() removes parent block elements if they
* don't have visible contents. Note that due performance issue of
* WhiteSpaceVisibilityKeeper, this call may be expensive. And also note that
* this removes a empty block with a transaction. So, please make sure that
* you've already created `AutoPlaceholderBatch`.
*
* @param aPoint The point whether this method climbing up the DOM
* tree to remove empty parent blocks.
* @return NS_OK if one or more empty block parents are deleted.
* NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND if the point is
* not in empty block.
* Or NS_ERROR_* if something unexpected occurs.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
DeleteParentBlocksWithTransactionIfEmpty(HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aPoint);
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
FallbackToDeleteRangesWithTransaction(HTMLEditor& aHTMLEditor,
AutoRangeArray& aRangesToDelete) const {
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
Result<CaretPoint, nsresult> caretPointOrError =
aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount,
mOriginalStripWrappers,
aRangesToDelete);
NS_WARNING_ASSERTION(caretPointOrError.isOk(),
"HTMLEditor::DeleteRangesWithTransaction() failed");
return caretPointOrError;
}
/**
* ComputeRangesToDeleteRangesWithTransaction() computes target ranges
* which will be called by `EditorBase::DeleteRangesWithTransaction()`.
* TODO: We should not use it for consistency with each deletion handler
* in this and nested classes.
*/
nsresult ComputeRangesToDeleteRangesWithTransaction(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete) const;
nsresult FallbackToComputeRangesToDeleteRangesWithTransaction(
const HTMLEditor& aHTMLEditor, AutoRangeArray& aRangesToDelete) const {
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
nsresult rv = ComputeRangesToDeleteRangesWithTransaction(
aHTMLEditor, mOriginalDirectionAndAmount, aRangesToDelete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoDeleteRangesHandler::"
"ComputeRangesToDeleteRangesWithTransaction() failed");
return rv;
}
class MOZ_STACK_CLASS AutoBlockElementsJoiner final {
public:
AutoBlockElementsJoiner() = delete;
explicit AutoBlockElementsJoiner(
AutoDeleteRangesHandler& aDeleteRangesHandler)
: mDeleteRangesHandler(&aDeleteRangesHandler),
mDeleteRangesHandlerConst(aDeleteRangesHandler) {}
explicit AutoBlockElementsJoiner(
const AutoDeleteRangesHandler& aDeleteRangesHandler)
: mDeleteRangesHandler(nullptr),
mDeleteRangesHandlerConst(aDeleteRangesHandler) {}
/**
* PrepareToDeleteAtCurrentBlockBoundary() considers left content and right
* content which are joined for handling deletion at current block boundary
* (i.e., at start or end of the current block).
*
* @param aHTMLEditor The HTML editor.
* @param aDirectionAndAmount Direction of the deletion.
* @param aCurrentBlockElement The current block element.
* @param aCaretPoint The caret point (i.e., selection start
* or end).
* @return true if can continue to handle the
* deletion.
*/
bool PrepareToDeleteAtCurrentBlockBoundary(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint);
/**
* PrepareToDeleteAtOtherBlockBoundary() considers left content and right
* content which are joined for handling deletion at other block boundary
* (i.e., immediately before or after a block).
*
* @param aHTMLEditor The HTML editor.
* @param aDirectionAndAmount Direction of the deletion.
* @param aOtherBlockElement The block element which follows the
* caret or is followed by caret.
* @param aCaretPoint The caret point (i.e., selection start
* or end).
* @param aWSRunScannerAtCaret WSRunScanner instance which was
* initialized with the caret point.
* @return true if can continue to handle the
* deletion.
*/
bool PrepareToDeleteAtOtherBlockBoundary(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement,
const EditorDOMPoint& aCaretPoint,
const WSRunScanner& aWSRunScannerAtCaret);
/**
* PrepareToDeleteNonCollapsedRanges() considers left block element and
* right block element which are inclusive ancestor block element of
* start and end container of first range of aRangesToDelete.
*
* @param aHTMLEditor The HTML editor.
* @param aRangesToDelete Ranges to delete. Must not be
* collapsed.
* @return true if can continue to handle the
* deletion.
*/
bool PrepareToDeleteNonCollapsedRanges(
const HTMLEditor& aHTMLEditor, const AutoRangeArray& aRangesToDelete);
/**
* Run() executes the joining.
*
* @param aHTMLEditor The HTML editor.
* @param aDirectionAndAmount Direction of the deletion.
* @param aStripWrappers Must be eStrip or eNoStrip.
* @param aCaretPoint The caret point (i.e., selection start
* or end).
* @param aRangesToDelete Ranges to delete of the caller.
* This should be collapsed and match
* with aCaretPoint.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete,
const Element& aEditingHost) {
switch (mMode) {
case Mode::JoinCurrentBlock: {
Result<EditActionResult, nsresult> result =
HandleDeleteAtCurrentBlockBoundary(
aHTMLEditor, aDirectionAndAmount, aCaretPoint, aEditingHost);
NS_WARNING_ASSERTION(result.isOk(),
"AutoBlockElementsJoiner::"
"HandleDeleteAtCurrentBlockBoundary() failed");
return result;
}
case Mode::JoinOtherBlock: {
Result<EditActionResult, nsresult> result =
HandleDeleteAtOtherBlockBoundary(aHTMLEditor, aDirectionAndAmount,
aStripWrappers, aCaretPoint,
aRangesToDelete, aEditingHost);
NS_WARNING_ASSERTION(result.isOk(),
"AutoBlockElementsJoiner::"
"HandleDeleteAtOtherBlockBoundary() failed");
return result;
}
case Mode::DeleteBRElement: {
Result<EditActionResult, nsresult> result =
DeleteBRElement(aHTMLEditor, aDirectionAndAmount, aEditingHost);
NS_WARNING_ASSERTION(
result.isOk(),
"AutoBlockElementsJoiner::DeleteBRElement() failed");
return result;
}
case Mode::JoinBlocksInSameParent:
case Mode::DeleteContentInRanges:
case Mode::DeleteNonCollapsedRanges:
MOZ_ASSERT_UNREACHABLE(
"This mode should be handled in the other Run()");
return Err(NS_ERROR_UNEXPECTED);
case Mode::NotInitialized:
return EditActionResult::IgnoredResult();
}
return Err(NS_ERROR_NOT_INITIALIZED);
}
nsresult ComputeRangesToDelete(const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
const EditorDOMPoint& aCaretPoint,
AutoRangeArray& aRangesToDelete,
const Element& aEditingHost) const {
switch (mMode) {
case Mode::JoinCurrentBlock: {
nsresult rv = ComputeRangesToDeleteAtCurrentBlockBoundary(
aHTMLEditor, aCaretPoint, aRangesToDelete, aEditingHost);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoBlockElementsJoiner::"
"ComputeRangesToDeleteAtCurrentBlockBoundary() failed");
return rv;
}
case Mode::JoinOtherBlock: {
nsresult rv = ComputeRangesToDeleteAtOtherBlockBoundary(
aHTMLEditor, aDirectionAndAmount, aCaretPoint, aRangesToDelete,
aEditingHost);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoBlockElementsJoiner::"
"ComputeRangesToDeleteAtOtherBlockBoundary() failed");
return rv;
}
case Mode::DeleteBRElement: {
nsresult rv = ComputeRangesToDeleteBRElement(aRangesToDelete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoBlockElementsJoiner::"
"ComputeRangesToDeleteBRElement() failed");
return rv;
}
case Mode::JoinBlocksInSameParent:
case Mode::DeleteContentInRanges:
case Mode::DeleteNonCollapsedRanges:
MOZ_ASSERT_UNREACHABLE(
"This mode should be handled in the other "
"ComputeRangesToDelete()");
return NS_ERROR_UNEXPECTED;
case Mode::NotInitialized:
return NS_OK;
}
return NS_ERROR_NOT_IMPLEMENTED;
}
/**
* Run() executes the joining.
*
* @param aHTMLEditor The HTML editor.
* @param aDirectionAndAmount Direction of the deletion.
* @param aStripWrappers Whether delete or keep new empty
* ancestor elements.
* @param aRangesToDelete Ranges to delete. Must not be
* collapsed.
* @param aSelectionWasCollapsed Whether selection was or was not
* collapsed when starting to handle
* deletion.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
AutoRangeArray& aRangesToDelete,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
const Element& aEditingHost) {
switch (mMode) {
case Mode::JoinCurrentBlock:
case Mode::JoinOtherBlock:
case Mode::DeleteBRElement:
MOZ_ASSERT_UNREACHABLE(
"This mode should be handled in the other Run()");
return Err(NS_ERROR_UNEXPECTED);
case Mode::JoinBlocksInSameParent: {
Result<EditActionResult, nsresult> result =
JoinBlockElementsInSameParent(
aHTMLEditor, aDirectionAndAmount, aStripWrappers,
aRangesToDelete, aSelectionWasCollapsed, aEditingHost);
NS_WARNING_ASSERTION(result.isOk(),
"AutoBlockElementsJoiner::"
"JoinBlockElementsInSameParent() failed");
return result;
}
case Mode::DeleteContentInRanges: {
Result<EditActionResult, nsresult> result =
DeleteContentInRanges(aHTMLEditor, aDirectionAndAmount,
aStripWrappers, aRangesToDelete);
NS_WARNING_ASSERTION(
result.isOk(),
"AutoBlockElementsJoiner::DeleteContentInRanges() failed");
return result;
}
case Mode::DeleteNonCollapsedRanges: {
Result<EditActionResult, nsresult> result =
HandleDeleteNonCollapsedRanges(
aHTMLEditor, aDirectionAndAmount, aStripWrappers,
aRangesToDelete, aSelectionWasCollapsed, aEditingHost);
NS_WARNING_ASSERTION(result.isOk(),
"AutoBlockElementsJoiner::"
"HandleDeleteNonCollapsedRange() failed");
return result;
}
case Mode::NotInitialized:
MOZ_ASSERT_UNREACHABLE(
"Call Run() after calling a preparation method");
return EditActionResult::IgnoredResult();
}
return Err(NS_ERROR_NOT_INITIALIZED);
}
nsresult ComputeRangesToDelete(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
const Element& aEditingHost) const {
switch (mMode) {
case Mode::JoinCurrentBlock:
case Mode::JoinOtherBlock:
case Mode::DeleteBRElement:
MOZ_ASSERT_UNREACHABLE(
"This mode should be handled in the other "
"ComputeRangesToDelete()");
return NS_ERROR_UNEXPECTED;
case Mode::JoinBlocksInSameParent: {
nsresult rv = ComputeRangesToJoinBlockElementsInSameParent(
aHTMLEditor, aDirectionAndAmount, aRangesToDelete);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoBlockElementsJoiner::"
"ComputeRangesToJoinBlockElementsInSameParent() failed");
return rv;
}
case Mode::DeleteContentInRanges: {
nsresult rv = ComputeRangesToDeleteContentInRanges(
aHTMLEditor, aDirectionAndAmount, aRangesToDelete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoBlockElementsJoiner::"
"ComputeRangesToDeleteContentInRanges() failed");
return rv;
}
case Mode::DeleteNonCollapsedRanges: {
nsresult rv = ComputeRangesToDeleteNonCollapsedRanges(
aHTMLEditor, aDirectionAndAmount, aRangesToDelete,
aSelectionWasCollapsed, aEditingHost);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoBlockElementsJoiner::"
"ComputeRangesToDeleteNonCollapsedRanges() failed");
return rv;
}
case Mode::NotInitialized:
MOZ_ASSERT_UNREACHABLE(
"Call ComputeRangesToDelete() after calling a preparation "
"method");
return NS_ERROR_NOT_INITIALIZED;
}
return NS_ERROR_NOT_INITIALIZED;
}
nsIContent* GetLeafContentInOtherBlockElement() const {
MOZ_ASSERT(mMode == Mode::JoinOtherBlock);
return mLeafContentInOtherBlock;
}
private:
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
HandleDeleteAtCurrentBlockBoundary(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
const EditorDOMPoint& aCaretPoint, const Element& aEditingHost);
nsresult ComputeRangesToDeleteAtCurrentBlockBoundary(
const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
HandleDeleteAtOtherBlockBoundary(HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
const EditorDOMPoint& aCaretPoint,
AutoRangeArray& aRangesToDelete,
const Element& aEditingHost);
// FYI: This method may modify selection, but it won't cause running
// script because of `AutoHideSelectionChanges` which blocks
// selection change listeners and the selection change event
// dispatcher.
MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
ComputeRangesToDeleteAtOtherBlockBoundary(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete,
const Element& aEditingHost) const;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
JoinBlockElementsInSameParent(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
AutoRangeArray& aRangesToDelete,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
const Element& aEditingHost);
nsresult ComputeRangesToJoinBlockElementsInSameParent(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete) const;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
DeleteBRElement(HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
const Element& aEditingHost);
nsresult ComputeRangesToDeleteBRElement(
AutoRangeArray& aRangesToDelete) const;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
DeleteContentInRanges(HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
AutoRangeArray& aRangesToDelete);
nsresult ComputeRangesToDeleteContentInRanges(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete) const;
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
HandleDeleteNonCollapsedRanges(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers,
AutoRangeArray& aRangesToDelete,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
const Element& aEditingHost);
nsresult ComputeRangesToDeleteNonCollapsedRanges(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
const Element& aEditingHost) const;
/**
* JoinNodesDeepWithTransaction() joins aLeftNode and aRightNode "deeply".
* First, they are joined simply, then, new right node is assumed as the
* child at length of the left node before joined and new left node is
* assumed as its previous sibling. Then, they will be joined again.
* And then, these steps are repeated.
*
* @param aLeftContent The node which will be removed form the tree.
* @param aRightContent The node which will be inserted the contents of
* aRightContent.
* @return The point of the first child of the last right
* node. The result is always set if this succeeded.
*/
MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
JoinNodesDeepWithTransaction(HTMLEditor& aHTMLEditor,
nsIContent& aLeftContent,
nsIContent& aRightContent);
/**
* DeleteNodesEntirelyInRangeButKeepTableStructure() removes nodes which are
* entirely in aRange. Howevers, if some nodes are part of a table,
* removes all children of them instead. I.e., this does not make damage to
* table structure at the range, but may remove table entirely if it's
* in the range.
*
* @return true if inclusive ancestor block elements at
* start and end of the range should be joined.
*/
MOZ_CAN_RUN_SCRIPT Result<bool, nsresult>
DeleteNodesEntirelyInRangeButKeepTableStructure(
HTMLEditor& aHTMLEditor, nsRange& aRange,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed);
bool NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure(
const HTMLEditor& aHTMLEditor,
const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
const;
Result<bool, nsresult>
ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure(
const HTMLEditor& aHTMLEditor, nsRange& aRange,
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
const;
/**
* DeleteContentButKeepTableStructure() removes aContent if it's an element
* which is part of a table structure. If it's a part of table structure,
* removes its all children recursively. I.e., this may delete all of a
* table, but won't break table structure partially.
*
* @param aContent The content which or whose all children should
* be removed.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor,
nsIContent& aContent);
/**
* DeleteTextAtStartAndEndOfRange() removes text if start and/or end of
* aRange is in a text node.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange);
class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner final {
public:
AutoInclusiveAncestorBlockElementsJoiner() = delete;
AutoInclusiveAncestorBlockElementsJoiner(
nsIContent& aInclusiveDescendantOfLeftBlockElement,
nsIContent& aInclusiveDescendantOfRightBlockElement)
: mInclusiveDescendantOfLeftBlockElement(
aInclusiveDescendantOfLeftBlockElement),
mInclusiveDescendantOfRightBlockElement(
aInclusiveDescendantOfRightBlockElement),
mCanJoinBlocks(false),
mFallbackToDeleteLeafContent(false) {}
bool IsSet() const { return mLeftBlockElement && mRightBlockElement; }
bool IsSameBlockElement() const {
return mLeftBlockElement && mLeftBlockElement == mRightBlockElement;
}
const EditorDOMPoint& PointRefToPutCaret() const {
return mPointToPutCaret;
}
/**
* Prepare for joining inclusive ancestor block elements. When this
* returns false, the deletion should be canceled.
*/
Result<bool, nsresult> Prepare(const HTMLEditor& aHTMLEditor,
const Element& aEditingHost);
/**
* When this returns true, this can join the blocks with `Run()`.
*/
bool CanJoinBlocks() const { return mCanJoinBlocks; }
/**
* When this returns true, `Run()` must return "ignored" so that
* caller can skip calling `Run()`. This is available only when
* `CanJoinBlocks()` returns `true`.
* TODO: This should be merged into `CanJoinBlocks()` in the future.
*/
bool ShouldDeleteLeafContentInstead() const {
MOZ_ASSERT(CanJoinBlocks());
return mFallbackToDeleteLeafContent;
}
/**
* ComputeRangesToDelete() extends aRangesToDelete includes the element
* boundaries between joining blocks. If they won't be joined, this
* collapses the range to aCaretPoint.
*/
nsresult ComputeRangesToDelete(const HTMLEditor& aHTMLEditor,
const EditorDOMPoint& aCaretPoint,
AutoRangeArray& aRangesToDelete) const;
/**
* Join inclusive ancestor block elements which are found by preceding
* Preare() call.
* The right element is always joined to the left element.
* If the elements are the same type and not nested within each other,
* JoinEditableNodesWithTransaction() is called (example, joining two
* list items together into one).
* If the elements are not the same type, or one is a descendant of the
* other, we instead destroy the right block placing its children into
* left block.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
HTMLEditor& aHTMLEditor, const Element& aEditingHost);
private:
/**
* This method returns true when
* `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`,
* `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` and
* `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` handle it
* with the `if` block of their main blocks.
*/
bool CanMergeLeftAndRightBlockElements() const {
if (!IsSet()) {
return false;
}
// `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`
if (mPointContainingTheOtherBlockElement.GetContainer() ==
mRightBlockElement) {
return mNewListElementTagNameOfRightListElement.isSome();
}
// `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()`
if (mPointContainingTheOtherBlockElement.GetContainer() ==
mLeftBlockElement) {
return mNewListElementTagNameOfRightListElement.isSome() &&
!mRightBlockElement->GetChildCount();
}
MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet());
// `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()`
return mNewListElementTagNameOfRightListElement.isSome() ||
mLeftBlockElement->NodeInfo()->NameAtom() ==
mRightBlockElement->NodeInfo()->NameAtom();
}
OwningNonNull<nsIContent> mInclusiveDescendantOfLeftBlockElement;
OwningNonNull<nsIContent> mInclusiveDescendantOfRightBlockElement;
RefPtr<Element> mLeftBlockElement;
RefPtr<Element> mRightBlockElement;
Maybe<nsAtom*> mNewListElementTagNameOfRightListElement;
EditorDOMPoint mPointContainingTheOtherBlockElement;
EditorDOMPoint mPointToPutCaret;
RefPtr<dom::HTMLBRElement> mPrecedingInvisibleBRElement;
bool mCanJoinBlocks;
bool mFallbackToDeleteLeafContent;
}; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::
// AutoInclusiveAncestorBlockElementsJoiner
enum class Mode {
NotInitialized,
JoinCurrentBlock,
JoinOtherBlock,
JoinBlocksInSameParent,
DeleteBRElement,
DeleteContentInRanges,
DeleteNonCollapsedRanges,
};
AutoDeleteRangesHandler* mDeleteRangesHandler;
const AutoDeleteRangesHandler& mDeleteRangesHandlerConst;
nsCOMPtr<nsIContent> mLeftContent;
nsCOMPtr<nsIContent> mRightContent;
nsCOMPtr<nsIContent> mLeafContentInOtherBlock;
// mSkippedInvisibleContents stores all content nodes which are skipped at
// scanning mLeftContent and mRightContent. The content nodes should be
// removed at deletion.
AutoTArray<OwningNonNull<nsIContent>, 8> mSkippedInvisibleContents;
RefPtr<dom::HTMLBRElement> mBRElement;
Mode mMode = Mode::NotInitialized;
}; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner
class MOZ_STACK_CLASS AutoEmptyBlockAncestorDeleter final {
public:
/**
* ScanEmptyBlockInclusiveAncestor() scans an inclusive ancestor element
* which is empty and a block element. Then, stores the result and
* returns the found empty block element.
*
* @param aHTMLEditor The HTMLEditor.
* @param aStartContent Start content to look for empty ancestors.
*/
[[nodiscard]] Element* ScanEmptyBlockInclusiveAncestor(
const HTMLEditor& aHTMLEditor, nsIContent& aStartContent);
/**
* ComputeTargetRanges() computes "target ranges" for deleting
* `mEmptyInclusiveAncestorBlockElement`.
*/
nsresult ComputeTargetRanges(const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount,
const Element& aEditingHost,
AutoRangeArray& aRangesToDelete) const;
/**
* Deletes found empty block element by `ScanEmptyBlockInclusiveAncestor()`.
* If found one is a list item element, calls
* `MaybeInsertBRElementBeforeEmptyListItemElement()` before deleting
* the list item element.
* If found empty ancestor is not a list item element,
* `GetNewCaretPosition()` will be called to determine new caret position.
* Finally, removes the empty block ancestor.
*
* @param aHTMLEditor The HTMLEditor.
* @param aDirectionAndAmount If found empty ancestor block is a list item
* element, this is ignored. Otherwise:
* - If eNext, eNextWord or eToEndOfLine,
* collapse Selection to after found empty
* ancestor.
* - If ePrevious, ePreviousWord or
* eToBeginningOfLine, collapse Selection to
* end of previous editable node.
* - Otherwise, eNone is allowed but does
* nothing.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount);
private:
/**
* MaybeReplaceSubListWithNewListItem() replaces
* mEmptyInclusiveAncestorBlockElement with new list item element
* (containing <br>) if:
* - mEmptyInclusiveAncestorBlockElement is a list element
* - The parent of mEmptyInclusiveAncestorBlockElement is a list element
* - The parent becomes empty after deletion
* If this does not perform the replacement, returns "ignored".
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
MaybeReplaceSubListWithNewListItem(HTMLEditor& aHTMLEditor);
/**
* MaybeInsertBRElementBeforeEmptyListItemElement() inserts a `<br>` element
* if `mEmptyInclusiveAncestorBlockElement` is a list item element which
* is first editable element in its parent, and its grand parent is not a
* list element, inserts a `<br>` element before the empty list item.
*/
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor);
/**
* GetNewCaretPosition() returns new caret position after deleting
* `mEmptyInclusiveAncestorBlockElement`.
*/
[[nodiscard]] Result<CaretPoint, nsresult> GetNewCaretPosition(
const HTMLEditor& aHTMLEditor,
nsIEditor::EDirection aDirectionAndAmount) const;
RefPtr<Element> mEmptyInclusiveAncestorBlockElement;
}; // HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter
const AutoDeleteRangesHandler* const mParent;
nsIEditor::EDirection mOriginalDirectionAndAmount;
nsIEditor::EStripWrappers mOriginalStripWrappers;
}; // HTMLEditor::AutoDeleteRangesHandler
nsresult HTMLEditor::ComputeTargetRanges(
nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete) const {
MOZ_ASSERT(IsEditActionDataAvailable());
Element* editingHost = ComputeEditingHost();
if (!editingHost) {
aRangesToDelete.RemoveAllRanges();
return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
}
// First check for table selection mode. If so, hand off to table editor.
SelectedTableCellScanner scanner(aRangesToDelete);
if (scanner.IsInTableCellSelectionMode()) {
// If it's in table cell selection mode, we'll delete all childen in
// the all selected table cell elements,
if (scanner.ElementsRef().Length() == aRangesToDelete.Ranges().Length()) {
return NS_OK;
}
// but will ignore all ranges which does not select a table cell.
size_t removedRanges = 0;
for (size_t i = 1; i < scanner.ElementsRef().Length(); i++) {
if (HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(
aRangesToDelete.Ranges()[i - removedRanges]) !=
scanner.ElementsRef()[i]) {
// XXX Need to manage anchor-focus range too!
aRangesToDelete.Ranges().RemoveElementAt(i - removedRanges);
removedRanges++;
}
}
return NS_OK;
}
aRangesToDelete.EnsureOnlyEditableRanges(*editingHost);
if (aRangesToDelete.Ranges().IsEmpty()) {
NS_WARNING(
"There is no range which we can delete entire of or around the caret");
return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
}
AutoDeleteRangesHandler deleteHandler;
// Should we delete target ranges which cannot delete actually?
nsresult rv = deleteHandler.ComputeRangesToDelete(
*this, aDirectionAndAmount, aRangesToDelete, *editingHost);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoDeleteRangesHandler::ComputeRangesToDelete() failed");
return rv;
}
Result<EditActionResult, nsresult> HTMLEditor::HandleDeleteSelection(
nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
aStripWrappers == nsIEditor::eNoStrip);
if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) {
return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
}
RefPtr<Element> editingHost = ComputeEditingHost();
if (MOZ_UNLIKELY(!editingHost)) {
return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
}
// Remember that we did a selection deletion. Used by
// CreateStyleForInsertText()
TopLevelEditSubActionDataRef().mDidDeleteSelection = true;
if (MOZ_UNLIKELY(IsEmpty())) {
return EditActionResult::CanceledResult();
}
// First check for table selection mode. If so, hand off to table editor.
if (HTMLEditUtils::IsInTableCellSelectionMode(SelectionRef())) {
nsresult rv = DeleteTableCellContentsWithTransaction();
if (NS_WARN_IF(Destroyed())) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteTableCellContentsWithTransaction() failed");
return Err(rv);
}
return EditActionResult::HandledResult();
}
AutoRangeArray rangesToDelete(SelectionRef());
rangesToDelete.EnsureOnlyEditableRanges(*editingHost);
if (MOZ_UNLIKELY(rangesToDelete.Ranges().IsEmpty())) {
NS_WARNING(
"There is no range which we can delete entire the ranges or around the "
"caret");
return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
}
AutoDeleteRangesHandler deleteHandler;
Result<EditActionResult, nsresult> result = deleteHandler.Run(
*this, aDirectionAndAmount, aStripWrappers, rangesToDelete, *editingHost);
if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Canceled()) {
NS_WARNING_ASSERTION(result.isOk(),
"AutoDeleteRangesHandler::Run() failed");
return result;
}
// XXX At here, selection may have no range because of mutation event
// listeners can do anything so that we should just return NS_OK instead
// of returning error.
const auto atNewStartOfSelection =
GetFirstSelectionStartPoint<EditorDOMPoint>();
if (NS_WARN_IF(!atNewStartOfSelection.IsSet())) {
return Err(NS_ERROR_FAILURE);
}
if (atNewStartOfSelection.IsInContentNode()) {
nsresult rv = DeleteMostAncestorMailCiteElementIfEmpty(
MOZ_KnownLive(*atNewStartOfSelection.ContainerAs<nsIContent>()));
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty() failed");
return Err(rv);
}
}
return EditActionResult::HandledResult();
}
nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete(
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
AutoRangeArray& aRangesToDelete, const Element& aEditingHost) {
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
mOriginalDirectionAndAmount = aDirectionAndAmount;
mOriginalStripWrappers = nsIEditor::eNoStrip;
if (aHTMLEditor.mPaddingBRElementForEmptyEditor) {
nsresult rv = aRangesToDelete.Collapse(
EditorRawDOMPoint(aHTMLEditor.mPaddingBRElementForEmptyEditor));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
return rv;
}
SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed()
? SelectionWasCollapsed::Yes
: SelectionWasCollapsed::No;
if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
const auto startPoint =
aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
if (NS_WARN_IF(!startPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost();
if (NS_WARN_IF(!editingHost)) {
return NS_ERROR_FAILURE;
}
if (startPoint.IsInContentNode()) {
AutoEmptyBlockAncestorDeleter deleter;
if (deleter.ScanEmptyBlockInclusiveAncestor(
aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) {
nsresult rv = deleter.ComputeTargetRanges(
aHTMLEditor, aDirectionAndAmount, *editingHost, aRangesToDelete);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoEmptyBlockAncestorDeleter::ComputeTargetRanges() failed");
return rv;
}
}
// We shouldn't update caret bidi level right now, but we need to check
// whether the deletion will be canceled or not.
AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount,
startPoint);
if (bidiLevelManager.Failed()) {
NS_WARNING(
"EditorBase::AutoCaretBidiLevelManager failed to initialize itself");
return NS_ERROR_FAILURE;
}
if (bidiLevelManager.Canceled()) {
return NS_SUCCESS_DOM_NO_OPERATION;
}
// AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection`
// to extend the range for deletion. But if focus event doesn't receive
// yet, ancestor isn't set. So we must set root element of editor to
// ancestor temporarily.
AutoSetTemporaryAncestorLimiter autoSetter(
aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(),
&aRangesToDelete);
Result<nsIEditor::EDirection, nsresult> extendResult =
aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor,
aDirectionAndAmount);
if (extendResult.isErr()) {
NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
return extendResult.unwrapErr();
}
// For compatibility with other browsers, we should set target ranges
// to start from and/or end after an atomic content rather than start
// from preceding text node end nor end at following text node start.
Result<bool, nsresult> shrunkenResult =
aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
aHTMLEditor, aDirectionAndAmount,
AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
editingHost);
if (shrunkenResult.isErr()) {
NS_WARNING(
"AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() "
"failed");
return shrunkenResult.unwrapErr();
}
if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) {
aDirectionAndAmount = extendResult.unwrap();
}
if (aDirectionAndAmount == nsIEditor::eNone) {
MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
if (!CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)) {
// XXX In this case, do we need to modify the range again?
return NS_SUCCESS_DOM_NO_OPERATION;
}
nsresult rv = FallbackToComputeRangesToDeleteRangesWithTransaction(
aHTMLEditor, aRangesToDelete);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoDeleteRangesHandler::"
"FallbackToComputeRangesToDeleteRangesWithTransaction() failed");
return rv;
}
if (aRangesToDelete.IsCollapsed()) {
const auto caretPoint =
aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
if (MOZ_UNLIKELY(NS_WARN_IF(!caretPoint.IsInContentNode()))) {
return NS_ERROR_FAILURE;
}
if (!EditorUtils::IsEditableContent(*caretPoint.ContainerAs<nsIContent>(),
EditorType::HTML)) {
return NS_SUCCESS_DOM_NO_OPERATION;
}
WSRunScanner wsRunScannerAtCaret(editingHost, caretPoint);
WSScanResult scanFromCaretPointResult =
aDirectionAndAmount == nsIEditor::eNext
? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
caretPoint)
: wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
caretPoint);
if (scanFromCaretPointResult.Failed()) {
NS_WARNING(
"WSRunScanner::Scan(Next|Previous)VisibleNodeOrBlockBoundaryFrom() "
"failed");
return NS_ERROR_FAILURE;
}
if (!scanFromCaretPointResult.GetContent()) {
return NS_SUCCESS_DOM_NO_OPERATION;
}
if (scanFromCaretPointResult.ReachedBRElement()) {
if (scanFromCaretPointResult.BRElementPtr() ==
wsRunScannerAtCaret.GetEditingHost()) {
return NS_OK;
}
if (!EditorUtils::IsEditableContent(
*scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) {
return NS_SUCCESS_DOM_NO_OPERATION;
}
if (HTMLEditUtils::IsInvisibleBRElement(
*scanFromCaretPointResult.BRElementPtr())) {
EditorDOMPoint newCaretPosition =
aDirectionAndAmount == nsIEditor::eNext
? EditorDOMPoint::After(
*scanFromCaretPointResult.BRElementPtr())
: EditorDOMPoint(scanFromCaretPointResult.BRElementPtr());
if (NS_WARN_IF(!newCaretPosition.IsSet())) {
return NS_ERROR_FAILURE;
}
AutoHideSelectionChanges blockSelectionListeners(
aHTMLEditor.SelectionRef());
nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition);
if (MOZ_UNLIKELY(NS_FAILED(rv))) {
NS_WARNING("EditorBase::CollapseSelectionTo() failed");
return NS_ERROR_FAILURE;
}
if (NS_WARN_IF(!aHTMLEditor.SelectionRef().RangeCount())) {
return NS_ERROR_UNEXPECTED;
}
aRangesToDelete.Initialize(aHTMLEditor.SelectionRef());
AutoDeleteRangesHandler anotherHandler(this);
rv = anotherHandler.ComputeRangesToDelete(
aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() "
"failed");
rv = aHTMLEditor.CollapseSelectionTo(caretPoint);
if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
NS_WARNING(
"EditorBase::CollapseSelectionTo() caused destroying the "
"editor");
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionTo() failed to "
"restore original selection, but ignored");
MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1);
// If the range is collapsed, there is no content which should
// be removed together. In this case, only the invisible `<br>`
// element should be selected.
if (aRangesToDelete.IsCollapsed()) {
nsresult rv = aRangesToDelete.SelectNode(
*scanFromCaretPointResult.BRElementPtr());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoRangeArray::SelectNode() failed");
return rv;
}
// Otherwise, extend the range to contain the invisible `<br>`
// element.
if (EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr())
.IsBefore(
aRangesToDelete
.GetFirstRangeStartPoint<EditorRawDOMPoint>())) {
nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr())
.ToRawRangeBoundary(),
aRangesToDelete.FirstRangeRef()->EndRef());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsRange::SetStartAndEnd() failed");
return rv;
}
if (aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>()
.IsBefore(EditorRawDOMPoint::After(
*scanFromCaretPointResult.BRElementPtr()))) {
nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd(
aRangesToDelete.FirstRangeRef()->StartRef(),
EditorRawDOMPoint::After(
*scanFromCaretPointResult.BRElementPtr())
.ToRawRangeBoundary());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsRange::SetStartAndEnd() failed");
return rv;
}
NS_WARNING("Was the invisible `<br>` element selected?");
return NS_OK;
}
}
nsresult rv = ComputeRangesToDeleteAroundCollapsedRanges(
aHTMLEditor, aDirectionAndAmount, aRangesToDelete,
wsRunScannerAtCaret, scanFromCaretPointResult, aEditingHost);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges("
") failed");
return rv;
}
}
nsresult rv = ComputeRangesToDeleteNonCollapsedRanges(
aHTMLEditor, aDirectionAndAmount, aRangesToDelete, selectionWasCollapsed,
aEditingHost);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"AutoDeleteRangesHandler::"
"ComputeRangesToDeleteNonCollapsedRanges() failed");
return rv;
}
Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run(
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete,
const Element& aEditingHost) {
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
aStripWrappers == nsIEditor::eNoStrip);
MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty());
mOriginalDirectionAndAmount = aDirectionAndAmount;
mOriginalStripWrappers = aStripWrappers;
if (MOZ_UNLIKELY(aHTMLEditor.IsEmpty())) {
return EditActionResult::CanceledResult();
}
// selectionWasCollapsed is used later to determine whether we should join
// blocks in HandleDeleteNonCollapsedRanges(). We don't really care about
// collapsed because it will be modified by
// AutoRangeArray::ExtendAnchorFocusRangeFor() later.
// AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner should
// happen if the original selection is collapsed and the cursor is at the end
// of a block element, in which case
// AutoRangeArray::ExtendAnchorFocusRangeFor() would always make the selection
// not collapsed.
SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed()
? SelectionWasCollapsed::Yes
: SelectionWasCollapsed::No;
if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
const auto startPoint =
aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>();
if (NS_WARN_IF(!startPoint.IsSet())) {
return Err(NS_ERROR_FAILURE);
}
// If we are inside an empty block, delete it.
if (startPoint.IsInContentNode()) {
#ifdef DEBUG
nsMutationGuard debugMutation;
#endif // #ifdef DEBUG
AutoEmptyBlockAncestorDeleter deleter;
if (deleter.ScanEmptyBlockInclusiveAncestor(
aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) {
Result<EditActionResult, nsresult> result =
deleter.Run(aHTMLEditor, aDirectionAndAmount);
if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Handled()) {
NS_WARNING_ASSERTION(result.isOk(),
"AutoEmptyBlockAncestorDeleter::Run() failed");
return result;
}
}
MOZ_ASSERT(!debugMutation.Mutated(0),
"AutoEmptyBlockAncestorDeleter shouldn't modify the DOM tree "
"if it returns not handled nor error");
}
// Test for distance between caret and text that will be deleted.
// Note that this call modifies `nsFrameSelection` without modifying
// `Selection`. However, it does not have problem for now because
// it'll be referred by `AutoRangeArray::ExtendAnchorFocusRangeFor()`
// before modifying `Selection`.
// XXX This looks odd. `ExtendAnchorFocusRangeFor()` will extend
// anchor-focus range, but here refers the first range.
AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount,
startPoint);
if (MOZ_UNLIKELY(bidiLevelManager.Failed())) {
NS_WARNING(
"EditorBase::AutoCaretBidiLevelManager failed to initialize itself");
return Err(NS_ERROR_FAILURE);
}
bidiLevelManager.MaybeUpdateCaretBidiLevel(aHTMLEditor);
if (bidiLevelManager.Canceled()) {
return EditActionResult::CanceledResult();
}
// AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection`
// to extend the range for deletion. But if focus event doesn't receive
// yet, ancestor isn't set. So we must set root element of editor to
// ancestor temporarily.
AutoSetTemporaryAncestorLimiter autoSetter(
aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(),
&aRangesToDelete);
// Calling `ExtendAnchorFocusRangeFor()` and
// `ShrinkRangesIfStartFromOrEndAfterAtomicContent()` may move caret to
// the container of deleting atomic content. However, it may be different
// from the original caret's container. The original caret container may
// be important to put caret after deletion so that let's cache the
// original position.
Maybe<EditorDOMPoint> caretPoint;
if (aRangesToDelete.IsCollapsed() && !aRangesToDelete.Ranges().IsEmpty()) {
caretPoint =
Some(aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>());
if (NS_WARN_IF(!caretPoint.ref().IsInContentNode())) {
return Err(NS_ERROR_FAILURE);
}
}
Result<nsIEditor::EDirection, nsresult> extendResult =
aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor,
aDirectionAndAmount);
if (MOZ_UNLIKELY(extendResult.isErr())) {
NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
return extendResult.propagateErr();
}
if (caretPoint.isSome() &&
MOZ_UNLIKELY(!caretPoint.ref().IsSetAndValid())) {
NS_WARNING("The caret position became invalid");
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
// If there is only one range and it selects an atomic content, we should
// delete it with collapsed range path for making consistent behavior
// between both cases, the content is selected case and caret is at it or
// after it case.
Result<bool, nsresult> shrunkenResult =
aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent(
aHTMLEditor, aDirectionAndAmount,
AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse,
&aEditingHost);
if (MOZ_UNLIKELY(shrunkenResult.isErr())) {