When deleting right block completely, we should wrap the right block into the range to delete completely. So, in that case, we don't need to use `HandleNonCollapsedRange` which considers whether the `mLeftContent` and `mRightContent` need to be merged after deleting content in the range since we always delete the right block as a variation of the merge. Instead, we can use `DeleteContentInRange` which is simpler and faster. Differential Revision: https://phabricator.services.mozilla.com/D288127
2097 lines
92 KiB
C++
2097 lines
92 KiB
C++
/* 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/. */
|
|
|
|
#ifndef HTMLEditorNestedClasses_h
|
|
#define HTMLEditorNestedClasses_h
|
|
|
|
#include "EditorDOMPoint.h"
|
|
#include "EditorForwards.h"
|
|
#include "HTMLEditor.h" // for HTMLEditor
|
|
#include "HTMLEditHelpers.h" // for EditorInlineStyleAndValue
|
|
#include "HTMLEditUtils.h" // for HTMLEditUtils::IsContainerNode
|
|
|
|
#include "mozilla/Attributes.h"
|
|
#include "mozilla/OwningNonNull.h"
|
|
#include "mozilla/Result.h"
|
|
#include "mozilla/dom/CharacterDataBuffer.h"
|
|
#include "mozilla/dom/Text.h"
|
|
|
|
namespace mozilla {
|
|
|
|
struct LimitersAndCaretData; // Declared in nsFrameSelection.h
|
|
namespace dom {
|
|
class HTMLBRElement;
|
|
};
|
|
|
|
/*****************************************************************************
|
|
* AutoInlineStyleSetter is a temporary class to set an inline style to
|
|
* specific nodes.
|
|
****************************************************************************/
|
|
|
|
class MOZ_STACK_CLASS HTMLEditor::AutoInlineStyleSetter final
|
|
: private EditorInlineStyleAndValue {
|
|
using Element = dom::Element;
|
|
using Text = dom::Text;
|
|
|
|
public:
|
|
explicit AutoInlineStyleSetter(
|
|
const EditorInlineStyleAndValue& aStyleAndValue)
|
|
: EditorInlineStyleAndValue(aStyleAndValue) {}
|
|
|
|
void Reset() {
|
|
mFirstHandledPoint.Clear();
|
|
mLastHandledPoint.Clear();
|
|
}
|
|
|
|
const EditorDOMPoint& FirstHandledPointRef() const {
|
|
return mFirstHandledPoint;
|
|
}
|
|
const EditorDOMPoint& LastHandledPointRef() const {
|
|
return mLastHandledPoint;
|
|
}
|
|
|
|
/**
|
|
* Split aText at aStartOffset and aEndOffset (except when they are start or
|
|
* end of its data) and wrap the middle text node in an element to apply the
|
|
* style.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
|
|
SplitTextNodeAndApplyStyleToMiddleNode(HTMLEditor& aHTMLEditor, Text& aText,
|
|
uint32_t aStartOffset,
|
|
uint32_t aEndOffset);
|
|
|
|
/**
|
|
* Remove same style from children and apply the style entire (except
|
|
* non-editable nodes) aContent.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
|
ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor,
|
|
nsIContent& aContent);
|
|
|
|
/**
|
|
* Invert the style with creating new element or something. This should
|
|
* be called only when IsInvertibleWithCSS() returns true.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Element& aElement);
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
|
|
InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Text& aTextNode,
|
|
uint32_t aStartOffset, uint32_t aEndOffset);
|
|
|
|
/**
|
|
* Extend or shrink aRange for applying the style to the range.
|
|
* See comments in the definition what this does.
|
|
*/
|
|
Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToApplyTheStyle(
|
|
const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
|
|
const Element& aEditingHost) const;
|
|
|
|
/**
|
|
* Returns next/previous sibling of aContent or an ancestor of it if it's
|
|
* editable and does not cross block boundary.
|
|
*/
|
|
[[nodiscard]] static nsIContent* GetNextEditableInlineContent(
|
|
const nsIContent& aContent, const nsINode* aLimiter = nullptr);
|
|
[[nodiscard]] static nsIContent* GetPreviousEditableInlineContent(
|
|
const nsIContent& aContent, const nsINode* aLimiter = nullptr);
|
|
|
|
/**
|
|
* GetEmptyTextNodeToApplyNewStyle creates new empty text node to insert
|
|
* a new element which will contain newly inserted text or returns existing
|
|
* empty text node if aCandidatePointToInsert is around it.
|
|
*
|
|
* NOTE: Unfortunately, editor does not want to insert text into empty inline
|
|
* element in some places (e.g., automatically adjusting caret position to
|
|
* nearest text node). Therefore, we need to create new empty text node to
|
|
* prepare new styles for inserting text. This method is designed for the
|
|
* preparation.
|
|
*
|
|
* @param aHTMLEditor The editor.
|
|
* @param aCandidatePointToInsert The point where the caller wants to
|
|
* insert new text.
|
|
* @return If this creates new empty text node returns it.
|
|
* If this couldn't create new empty text node due to
|
|
* the point or aEditingHost cannot have text node,
|
|
* returns nullptr.
|
|
* Otherwise, returns error.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<RefPtr<Text>, nsresult>
|
|
GetEmptyTextNodeToApplyNewStyle(
|
|
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert);
|
|
|
|
private:
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> ApplyStyle(
|
|
HTMLEditor& aHTMLEditor, nsIContent& aContent);
|
|
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
|
ApplyCSSTextDecoration(HTMLEditor& aHTMLEditor, nsIContent& aContent);
|
|
|
|
/**
|
|
* Returns true if aStyledElement is a good element to set `style` attribute.
|
|
*/
|
|
[[nodiscard]] bool ElementIsGoodContainerToSetStyle(
|
|
nsStyledElement& aStyledElement) const;
|
|
|
|
/**
|
|
* ElementIsGoodContainerForTheStyle() returns true if aElement is a
|
|
* good container for applying the style to a node. I.e., if this returns
|
|
* true, moving nodes into aElement is enough to apply the style to them.
|
|
* Otherwise, you need to create new element for the style.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<bool, nsresult>
|
|
ElementIsGoodContainerForTheStyle(HTMLEditor& aHTMLEditor,
|
|
Element& aElement) const;
|
|
|
|
/**
|
|
* Return true if the node is an element node and it represents the style or
|
|
* sets the style (including when setting different value) with `style`
|
|
* attribute.
|
|
*/
|
|
[[nodiscard]] bool ContentIsElementSettingTheStyle(
|
|
const HTMLEditor& aHTMLEditor, nsIContent& aContent) const;
|
|
|
|
/**
|
|
* Helper methods to shrink range to apply the style.
|
|
*/
|
|
[[nodiscard]] EditorRawDOMPoint GetShrunkenRangeStart(
|
|
const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
|
|
const nsINode& aCommonAncestorOfRange,
|
|
const nsIContent* aFirstEntirelySelectedContentNodeInRange) const;
|
|
[[nodiscard]] EditorRawDOMPoint GetShrunkenRangeEnd(
|
|
const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
|
|
const nsINode& aCommonAncestorOfRange,
|
|
const nsIContent* aLastEntirelySelectedContentNodeInRange) const;
|
|
|
|
/**
|
|
* Helper methods to extend the range to apply the style.
|
|
*/
|
|
[[nodiscard]] EditorRawDOMPoint
|
|
GetExtendedRangeStartToWrapAncestorApplyingSameStyle(
|
|
const HTMLEditor& aHTMLEditor,
|
|
const EditorRawDOMPoint& aStartPoint) const;
|
|
[[nodiscard]] EditorRawDOMPoint
|
|
GetExtendedRangeEndToWrapAncestorApplyingSameStyle(
|
|
const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aEndPoint) const;
|
|
[[nodiscard]] EditorRawDOMRange
|
|
GetExtendedRangeToMinimizeTheNumberOfNewElements(
|
|
const HTMLEditor& aHTMLEditor, const nsINode& aCommonAncestor,
|
|
EditorRawDOMPoint&& aStartPoint, EditorRawDOMPoint&& aEndPoint) const;
|
|
|
|
/**
|
|
* OnHandled() are called when this class creates new element to apply the
|
|
* style, applies new style to existing element or ignores to apply the style
|
|
* due to already set.
|
|
*/
|
|
void OnHandled(const EditorDOMPoint& aStartPoint,
|
|
const EditorDOMPoint& aEndPoint) {
|
|
if (!mFirstHandledPoint.IsSet()) {
|
|
mFirstHandledPoint = aStartPoint;
|
|
}
|
|
mLastHandledPoint = aEndPoint;
|
|
}
|
|
void OnHandled(nsIContent& aContent) {
|
|
if (aContent.IsElement() && !HTMLEditUtils::IsContainerNode(aContent)) {
|
|
if (!mFirstHandledPoint.IsSet()) {
|
|
mFirstHandledPoint.Set(&aContent);
|
|
}
|
|
mLastHandledPoint.SetAfter(&aContent);
|
|
return;
|
|
}
|
|
if (!mFirstHandledPoint.IsSet()) {
|
|
mFirstHandledPoint.Set(&aContent, 0u);
|
|
}
|
|
mLastHandledPoint = EditorDOMPoint::AtEndOf(aContent);
|
|
}
|
|
|
|
// mFirstHandledPoint and mLastHandledPoint store the first and last points
|
|
// which are newly created or apply the new style, or just ignored at trying
|
|
// to split a text node.
|
|
EditorDOMPoint mFirstHandledPoint;
|
|
EditorDOMPoint mLastHandledPoint;
|
|
};
|
|
|
|
/**
|
|
* AutoMoveOneLineHandler moves the content in a line (between line breaks/block
|
|
* boundaries) to specific point or end of a container element.
|
|
*/
|
|
class MOZ_STACK_CLASS HTMLEditor::AutoMoveOneLineHandler final {
|
|
public:
|
|
/**
|
|
* Use this constructor when you want a line to move specific point.
|
|
*/
|
|
explicit AutoMoveOneLineHandler(const EditorDOMPoint& aPointToInsert)
|
|
: mPointToInsert(aPointToInsert),
|
|
mMoveToEndOfContainer(MoveToEndOfContainer::No) {
|
|
MOZ_ASSERT(mPointToInsert.IsSetAndValid());
|
|
MOZ_ASSERT(mPointToInsert.IsInContentNode());
|
|
}
|
|
/**
|
|
* Use this constructor when you want a line to move end of
|
|
* aNewContainerElement.
|
|
*/
|
|
explicit AutoMoveOneLineHandler(Element& aNewContainerElement)
|
|
: mPointToInsert(&aNewContainerElement, 0),
|
|
mMoveToEndOfContainer(MoveToEndOfContainer::Yes) {
|
|
MOZ_ASSERT(mPointToInsert.IsSetAndValid());
|
|
}
|
|
|
|
/**
|
|
* Must be called before calling Run().
|
|
*
|
|
* @param aHTMLEditor The HTML editor.
|
|
* @param aPointInHardLine A point in a line which you want to move.
|
|
* @param aEditingHost The editing host.
|
|
*/
|
|
[[nodiscard]] nsresult Prepare(HTMLEditor& aHTMLEditor,
|
|
const EditorDOMPoint& aPointInHardLine,
|
|
const Element& aEditingHost);
|
|
/**
|
|
* Must be called if Prepare() returned NS_OK.
|
|
*
|
|
* @param aHTMLEditor The HTML editor.
|
|
* @param aEditingHost The editing host.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<MoveNodeResult, nsresult> Run(
|
|
HTMLEditor& aHTMLEditor, const Element& aEditingHost);
|
|
|
|
/**
|
|
* Returns true if there are some content nodes which can be moved to another
|
|
* place or deleted in the line containing aPointInHardLine. Note that if
|
|
* there is only a padding <br> element in an empty block element, this
|
|
* returns false even though it may be deleted.
|
|
*/
|
|
static Result<bool, nsresult> CanMoveOrDeleteSomethingInLine(
|
|
const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost);
|
|
|
|
AutoMoveOneLineHandler(const AutoMoveOneLineHandler& aOther) = delete;
|
|
AutoMoveOneLineHandler(AutoMoveOneLineHandler&& aOther) = delete;
|
|
|
|
private:
|
|
[[nodiscard]] bool ForceMoveToEndOfContainer() const {
|
|
return mMoveToEndOfContainer == MoveToEndOfContainer::Yes;
|
|
}
|
|
[[nodiscard]] EditorDOMPoint& NextInsertionPointRef() {
|
|
if (ForceMoveToEndOfContainer()) {
|
|
mPointToInsert.SetToEndOf(mPointToInsert.GetContainer());
|
|
}
|
|
return mPointToInsert;
|
|
}
|
|
|
|
/**
|
|
* Consider whether Run() should preserve or does not preserve white-space
|
|
* style of moving content.
|
|
*
|
|
* @param aContentInLine Specify a content node in the moving line.
|
|
* Typically, container of aPointInHardLine of
|
|
* Prepare().
|
|
* @param aInclusiveAncestorBlockOfInsertionPoint
|
|
* Inclusive ancestor block element of insertion
|
|
* point. Typically, computed
|
|
* mDestInclusiveAncestorBlock.
|
|
*/
|
|
[[nodiscard]] static PreserveWhiteSpaceStyle
|
|
ConsiderWhetherPreserveWhiteSpaceStyle(
|
|
const nsIContent* aContentInLine,
|
|
const Element* aInclusiveAncestorBlockOfInsertionPoint);
|
|
|
|
/**
|
|
* Look for inclusive ancestor block element of aBlockElement and a descendant
|
|
* of aAncestorElement. If aBlockElement and aAncestorElement are same one,
|
|
* this returns nullptr.
|
|
*
|
|
* @param aBlockElement A block element which is a descendant of
|
|
* aAncestorElement.
|
|
* @param aAncestorElement An inclusive ancestor block element of
|
|
* aBlockElement.
|
|
*/
|
|
[[nodiscard]] static Element*
|
|
GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement(
|
|
Element& aBlockElement, const Element& aAncestorElement);
|
|
|
|
/**
|
|
* Split ancestors at the line range boundaries and collect array of contents
|
|
* in the line to aOutArrayOfContents. Specify aNewContainer to the container
|
|
* of insertion point to avoid splitting the destination.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
|
SplitToMakeTheLineIsolated(
|
|
HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer,
|
|
const Element& aEditingHost,
|
|
nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) const;
|
|
|
|
/**
|
|
* Delete unnecessary trailing line break in aMovedContentRange if there is.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
|
|
HTMLEditor& aHTMLEditor, const EditorDOMRange& aMovedContentRange,
|
|
const Element& aEditingHost) const;
|
|
|
|
// Range of selected line.
|
|
EditorDOMRange mLineRange;
|
|
// Next insertion point. If mMoveToEndOfContainer is `Yes`, this is
|
|
// recomputed with its container in NextInsertionPointRef. Therefore, this
|
|
// should not be referred directly.
|
|
EditorDOMPoint mPointToInsert;
|
|
// An inclusive ancestor block element of the moving line.
|
|
RefPtr<Element> mSrcInclusiveAncestorBlock;
|
|
// An inclusive ancestor block element of the insertion point.
|
|
RefPtr<Element> mDestInclusiveAncestorBlock;
|
|
// nullptr if mMovingToParentBlock is false.
|
|
// Must be non-nullptr if mMovingToParentBlock is true. The topmost ancestor
|
|
// block element which contains mSrcInclusiveAncestorBlock and a descendant of
|
|
// mDestInclusiveAncestorBlock. I.e., this may be same as
|
|
// mSrcInclusiveAncestorBlock, but never same as mDestInclusiveAncestorBlock.
|
|
RefPtr<Element> mTopmostSrcAncestorBlockInDestBlock;
|
|
enum class MoveToEndOfContainer { No, Yes };
|
|
MoveToEndOfContainer mMoveToEndOfContainer;
|
|
PreserveWhiteSpaceStyle mPreserveWhiteSpaceStyle =
|
|
PreserveWhiteSpaceStyle::No;
|
|
// true if mDestInclusiveAncestorBlock is an ancestor of
|
|
// mSrcInclusiveAncestorBlock.
|
|
bool mMovingToParentBlock = false;
|
|
};
|
|
|
|
/**
|
|
* Convert contents around aRanges of Run() to specified list element. If there
|
|
* are some different type of list elements, this method converts them to
|
|
* specified list items too. Basically, each line will be wrapped in a list
|
|
* item element. However, only when <p> element is selected, its child <br>
|
|
* elements won't be treated as line separators. Perhaps, this is a bug.
|
|
*/
|
|
class MOZ_STACK_CLASS HTMLEditor::AutoListElementCreator final {
|
|
public:
|
|
/**
|
|
* @param aListElementTagName The new list element tag name.
|
|
* @param aListItemElementTagName The new list item element tag name.
|
|
* @param aBulletType If this is not empty string, it's set
|
|
* to `type` attribute of new list item
|
|
* elements. Otherwise, existing `type`
|
|
* attributes will be removed.
|
|
*/
|
|
AutoListElementCreator(const nsStaticAtom& aListElementTagName,
|
|
const nsStaticAtom& aListItemElementTagName,
|
|
const nsAString& aBulletType)
|
|
// Needs const_cast hack here because the struct users may want
|
|
// non-const nsStaticAtom pointer due to bug 1794954
|
|
: mListTagName(const_cast<nsStaticAtom&>(aListElementTagName)),
|
|
mListItemTagName(const_cast<nsStaticAtom&>(aListItemElementTagName)),
|
|
mBulletType(aBulletType) {
|
|
MOZ_ASSERT(&mListTagName == nsGkAtoms::ul ||
|
|
&mListTagName == nsGkAtoms::ol ||
|
|
&mListTagName == nsGkAtoms::dl);
|
|
MOZ_ASSERT_IF(
|
|
&mListTagName == nsGkAtoms::ul || &mListTagName == nsGkAtoms::ol,
|
|
&mListItemTagName == nsGkAtoms::li);
|
|
MOZ_ASSERT_IF(&mListTagName == nsGkAtoms::dl,
|
|
&mListItemTagName == nsGkAtoms::dt ||
|
|
&mListItemTagName == nsGkAtoms::dd);
|
|
}
|
|
|
|
/**
|
|
* @param aHTMLEditor The HTML editor.
|
|
* @param aRanges [in/out] The ranges which will be converted to list.
|
|
* The instance must not have saved ranges because it'll
|
|
* be used in this method.
|
|
* If succeeded, this will have selection ranges which
|
|
* should be applied to `Selection`.
|
|
* If failed, this keeps storing original selection
|
|
* ranges.
|
|
* @param aSelectAllOfCurrentList Yes if this should treat all of
|
|
* ancestor list element at selection.
|
|
* @param aEditingHost The editing host.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
|
|
HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRanges,
|
|
HTMLEditor::SelectAllOfCurrentList aSelectAllOfCurrentList,
|
|
const Element& aEditingHost) const;
|
|
|
|
private:
|
|
using ContentNodeArray = nsTArray<OwningNonNull<nsIContent>>;
|
|
using AutoContentNodeArray = AutoTArray<OwningNonNull<nsIContent>, 64>;
|
|
|
|
/**
|
|
* If aSelectAllOfCurrentList is "Yes" and aRanges is in a list element,
|
|
* returns the list element.
|
|
* Otherwise, extend aRanges to select start and end lines selected by it and
|
|
* correct all topmost content nodes in the extended ranges with splitting
|
|
* ancestors at range edges.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList(
|
|
HTMLEditor& aHTMLEditor, AutoClonedRangeArray& aRanges,
|
|
SelectAllOfCurrentList aSelectAllOfCurrentList,
|
|
const Element& aEditingHost, ContentNodeArray& aOutArrayOfContents) const;
|
|
|
|
/**
|
|
* Return true if aArrayOfContents has only <br> elements or empty inline
|
|
* container elements. I.e., it means that aArrayOfContents represents
|
|
* only empty line(s) if this returns true.
|
|
*/
|
|
[[nodiscard]] static bool
|
|
IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(
|
|
const ContentNodeArray& aArrayOfContents);
|
|
|
|
/**
|
|
* Delete all content nodes ina ArrayOfContents, and if we can put new list
|
|
* element at start of the first range of aRanges, insert new list element
|
|
* there.
|
|
*
|
|
* @return The empty list item element in new list element.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
|
|
ReplaceContentNodesWithEmptyNewList(
|
|
HTMLEditor& aHTMLEditor, const AutoClonedRangeArray& aRanges,
|
|
const AutoContentNodeArray& aArrayOfContents,
|
|
const Element& aEditingHost) const;
|
|
|
|
/**
|
|
* Creat new list elements or use existing list elements and move
|
|
* aArrayOfContents into list item elements.
|
|
*
|
|
* @return A list or list item element which should have caret.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
|
|
WrapContentNodesIntoNewListElements(HTMLEditor& aHTMLEditor,
|
|
AutoClonedRangeArray& aRanges,
|
|
AutoContentNodeArray& aArrayOfContents,
|
|
const Element& aEditingHost) const;
|
|
|
|
struct MOZ_STACK_CLASS AutoHandlingState final {
|
|
// Current list element which is a good container to create new list item
|
|
// element.
|
|
RefPtr<Element> mCurrentListElement;
|
|
// Previously handled list item element.
|
|
RefPtr<Element> mPreviousListItemElement;
|
|
// List or list item element which should have caret after handling all
|
|
// contents.
|
|
RefPtr<Element> mListOrListItemElementToPutCaret;
|
|
// Replacing block element. This is typically already removed from the DOM
|
|
// tree.
|
|
RefPtr<Element> mReplacingBlockElement;
|
|
// Once id attribute of mReplacingBlockElement copied, the id attribute
|
|
// shouldn't be copied again.
|
|
bool mMaybeCopiedReplacingBlockElementId = false;
|
|
};
|
|
|
|
/**
|
|
* Helper methods of WrapContentNodesIntoNewListElements. They are called for
|
|
* handling one content node of aArrayOfContents. It's set to aHandling*.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildContent(
|
|
HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
|
|
AutoHandlingState& aState, const Element& aEditingHost) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
HandleChildListElement(HTMLEditor& aHTMLEditor, Element& aHandlingListElement,
|
|
AutoHandlingState& aState) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemElement(
|
|
HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
|
|
AutoHandlingState& aState) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
HandleChildListItemInDifferentTypeList(HTMLEditor& aHTMLEditor,
|
|
Element& aHandlingListItemElement,
|
|
AutoHandlingState& aState) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemInSameTypeList(
|
|
HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
|
|
AutoHandlingState& aState) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildDivOrParagraphElement(
|
|
HTMLEditor& aHTMLEditor, Element& aHandlingDivOrParagraphElement,
|
|
AutoHandlingState& aState, const Element& aEditingHost) const;
|
|
enum class EmptyListItem { NotCreate, Create };
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult CreateAndUpdateCurrentListElement(
|
|
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
|
|
EmptyListItem aEmptyListItem, AutoHandlingState& aState,
|
|
const Element& aEditingHost) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
|
|
AppendListItemElement(HTMLEditor& aHTMLEditor, const Element& aListElement,
|
|
AutoHandlingState& aState) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
|
|
MaybeCloneAttributesToNewListItem(HTMLEditor& aHTMLEditor,
|
|
Element& aListItemElement,
|
|
AutoHandlingState& aState);
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildInlineContent(
|
|
HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent,
|
|
AutoHandlingState& aState) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult WrapContentIntoNewListItemElement(
|
|
HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
|
|
AutoHandlingState& aState) const;
|
|
|
|
/**
|
|
* If aRanges is collapsed outside aListItemOrListToPutCaret, this collapse
|
|
* aRanges in aListItemOrListToPutCaret again.
|
|
*/
|
|
nsresult EnsureCollapsedRangeIsInListItemOrListElement(
|
|
Element& aListItemOrListToPutCaret, AutoClonedRangeArray& aRanges) const;
|
|
|
|
MOZ_KNOWN_LIVE nsStaticAtom& mListTagName;
|
|
MOZ_KNOWN_LIVE nsStaticAtom& mListItemTagName;
|
|
const nsAutoString mBulletType;
|
|
};
|
|
|
|
/**
|
|
* Handle "insertParagraph" command.
|
|
*/
|
|
class MOZ_STACK_CLASS HTMLEditor::AutoInsertParagraphHandler final {
|
|
public:
|
|
AutoInsertParagraphHandler() = delete;
|
|
AutoInsertParagraphHandler(const AutoInsertParagraphHandler&) = delete;
|
|
AutoInsertParagraphHandler(AutoInsertParagraphHandler&&) = delete;
|
|
|
|
MOZ_CAN_RUN_SCRIPT explicit AutoInsertParagraphHandler(
|
|
HTMLEditor& aHTMLEditor, const Element& aEditingHost)
|
|
: mHTMLEditor(aHTMLEditor),
|
|
mEditingHost(aEditingHost),
|
|
mDefaultParagraphSeparatorTagName(
|
|
aHTMLEditor.DefaultParagraphSeparatorTagName()),
|
|
mDefaultParagraphSeparator(aHTMLEditor.GetDefaultParagraphSeparator()) {
|
|
}
|
|
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run();
|
|
|
|
private:
|
|
/**
|
|
* Insert <br> element.
|
|
*
|
|
* @param aPointToInsert The position where the new <br> should be
|
|
* inserted.
|
|
* @param aBlockElementWhichShouldHaveCaret
|
|
* [optional] If set, this collapse selection into
|
|
* the element with
|
|
* CollapseSelectionToPointOrIntoBlockWhichShouldHaveCaret().
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
|
|
HandleInsertBRElement(
|
|
const EditorDOMPoint& aPointToInsert,
|
|
const Element* aBlockElementWhichShouldHaveCaret = nullptr);
|
|
|
|
/**
|
|
* Insert a linefeed.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
|
|
HandleInsertLinefeed(const EditorDOMPoint& aPointToInsert);
|
|
|
|
/**
|
|
* SplitParagraphWithTransaction() splits the parent block, aParentDivOrP, at
|
|
* aPointToSplit.
|
|
*
|
|
* @param aBlockElementToSplit The current paragraph which should be split.
|
|
* @param aPointToSplit The point to split aBlockElementToSplit.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
|
|
SplitParagraphWithTransaction(Element& aBlockElementToSplit,
|
|
const EditorDOMPoint& aPointToSplit);
|
|
|
|
/**
|
|
* Delete preceding invisible line break before aPointToSplit if and only if
|
|
* there is.
|
|
*
|
|
* @return New point to split aBlockElementToSplit
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
|
|
EnsureNoInvisibleLineBreakBeforePointToSplit(
|
|
const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit);
|
|
|
|
/**
|
|
* Maybe insert a <br> element if it's required to keep the inline container
|
|
* visible after splitting aBlockElementToSplit at aPointToSplit.
|
|
*
|
|
* @return New point to split aBlockElementToSplit
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
|
|
MaybeInsertFollowingBRElementToPreserveRightBlock(
|
|
const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit);
|
|
|
|
/**
|
|
* Return true if the HTMLEditor is in the mode which `insertParagraph` should
|
|
* always create a new paragraph or in the cases that we create a new
|
|
* paragraph in the legacy mode.
|
|
*/
|
|
[[nodiscard]] bool ShouldCreateNewParagraph(
|
|
Element& aParentDivOrP, const EditorDOMPoint& aPointToSplit) const;
|
|
|
|
/**
|
|
* Return true if aBRElement is nullptr or an invisible <br> or a padding <br>
|
|
* for making the last empty line visible.
|
|
*/
|
|
[[nodiscard]] static bool
|
|
IsNullOrInvisibleBRElementOrPaddingOneForEmptyLastLine(
|
|
const dom::HTMLBRElement* aBRElement);
|
|
|
|
/**
|
|
* Handle insertParagraph command (i.e., handling Enter key press) in a
|
|
* heading element. This splits aHeadingElement element at aPointToSplit.
|
|
* Then, if right heading element is empty, it'll be removed and new paragraph
|
|
* is created (its type is decided with default paragraph separator).
|
|
*
|
|
* @param aHeadingElement The heading element to be split.
|
|
* @param aPointToSplit The point to split aHeadingElement.
|
|
* @return New paragraph element, meaning right heading
|
|
* element if aHeadingElement is split, or newly
|
|
* created or existing paragraph element.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
|
|
HandleInHeadingElement(Element& aHeadingElement,
|
|
const EditorDOMPoint& aPointToSplit);
|
|
|
|
/**
|
|
* Handle insertParagraph command at end of a heading element.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
|
|
HandleAtEndOfHeadingElement(Element& aHeadingElement);
|
|
|
|
/**
|
|
* Handle insertParagraph command (i.e., handling Enter key press) in a list
|
|
* item element.
|
|
*
|
|
* @param aListItemElement The list item which has the following point.
|
|
* @param aPointToSplit The point to split aListItemElement.
|
|
* @return New paragraph element, meaning right list item
|
|
* element if aListItemElement is split, or newly
|
|
* created paragraph element.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
|
|
HandleInListItemElement(Element& aListItemElement,
|
|
const EditorDOMPoint& aPointToSplit);
|
|
|
|
/**
|
|
* Split aMailCiteElement at aPointToSplit.
|
|
*
|
|
* @param aMailCiteElement The mail-cite element which should be split.
|
|
* @param aPointToSplit The point to split.
|
|
* @return Candidate caret position where is at inserted
|
|
* <br> element into the split point.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
|
HandleInMailCiteElement(Element& aMailCiteElement,
|
|
const EditorDOMPoint& aPointToSplit);
|
|
|
|
/**
|
|
* Insert a <br> element into aPointToBreak.
|
|
* This may split container elements at the point and/or may move following
|
|
* <br> element to immediately after the new <br> element if necessary.
|
|
*
|
|
* @param aPointToBreak The point where new <br> element will be
|
|
* inserted before.
|
|
* @return If succeeded, returns new <br> element and
|
|
* candidate caret point.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
|
|
InsertBRElement(const EditorDOMPoint& aPointToBreak);
|
|
|
|
/**
|
|
* Return true if we should insert a line break instead of a paragraph.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT bool ShouldInsertLineBreakInstead(
|
|
const Element* aEditableBlockElement,
|
|
const EditorDOMPoint& aCandidatePointToSplit);
|
|
|
|
enum class InsertBRElementIntoEmptyBlock : bool { Start, End };
|
|
|
|
/**
|
|
* Make sure that aMaybeBlockElement is visible with putting a <br> element if
|
|
* and only if it's an empty block element.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateLineBreakResult, nsresult>
|
|
InsertBRElementIfEmptyBlockElement(
|
|
Element& aMaybeBlockElement,
|
|
InsertBRElementIntoEmptyBlock aInsertBRElementIntoEmptyBlock,
|
|
BlockInlineCheck aBlockInlineCheck);
|
|
|
|
/**
|
|
* Split aMailCiteElement at aPointToSplit. This deletes all inclusive
|
|
* ancestors of aPointToSplit in aMailCiteElement too.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
|
|
SplitMailCiteElement(const EditorDOMPoint& aPointToSplit,
|
|
Element& aMailCiteElement);
|
|
|
|
/**
|
|
* aMailCiteElement may be a <span> element which is styled as block. If it's
|
|
* followed by a block boundary, it requires a padding <br> element when it's
|
|
* serialized. This method may insert a <br> element if it's required.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
MaybeInsertPaddingBRElementToInlineMailCiteElement(
|
|
const EditorDOMPoint& aPointToInsertBRElement, Element& aMailCiteElement);
|
|
|
|
/**
|
|
* Return the deepest inline container element which is the first leaf or the
|
|
* first leaf container of aBlockElement.
|
|
*/
|
|
[[nodiscard]] static Element* GetDeepestFirstChildInlineContainerElement(
|
|
Element& aBlockElement);
|
|
|
|
/**
|
|
* Collapse `Selection` to aCandidatePointToPutCaret or into
|
|
* aBlockElementShouldHaveCaret. If aBlockElementShouldHaveCaret is specified
|
|
* and aCandidatePointToPutCaret is outside it, this ignores
|
|
* aCandidatePointToPutCaret and collapse `Selection` into
|
|
* aBlockElementShouldHaveCaret.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
CollapseSelectionToPointOrIntoBlockWhichShouldHaveCaret(
|
|
const EditorDOMPoint& aCandidatePointToPutCaret,
|
|
const Element* aBlockElementShouldHaveCaret,
|
|
const SuggestCaretOptions& aOptions);
|
|
|
|
/**
|
|
* Return a better point to split the paragraph to avoid to keep a typing in a
|
|
* link or a paragraph in list item in the new paragraph.
|
|
*/
|
|
[[nodiscard]] EditorDOMPoint GetBetterPointToSplitParagraph(
|
|
const Element& aBlockElementToSplit,
|
|
const EditorDOMPoint& aCandidatePointToSplit,
|
|
const Element& aEditingHost);
|
|
|
|
enum class IgnoreBlockBoundaries : bool { No, Yes };
|
|
|
|
/**
|
|
* Return true if splitting aBlockElementToSplit at aPointToSplit will create
|
|
* empty left element.
|
|
*
|
|
* @param aBlockElementToSplit The paragraph element which we want to
|
|
* split.
|
|
* @param aPointToSplit The split position in aBlockElementToSplit.
|
|
* @param aIgnoreBlockBoundaries If No, return true only when aPointToSplit
|
|
* is immediately after a block boundary of
|
|
* aBlockElementToSplit. In other words,
|
|
* may return true only when aPointToSplit
|
|
* is not in a child block of
|
|
* aBlockElementToSplit.
|
|
* If Yes, return true even when aPointToSplit
|
|
* is immediately after any current block
|
|
* boundary which is followed by the block
|
|
* boundary of aBlockElementToSplit. In other
|
|
* words, return true when aPointToSplit is in
|
|
* a child block which is start of any ancestor
|
|
* block elements.
|
|
*/
|
|
[[nodiscard]] static bool SplitPointIsStartOfSplittingBlock(
|
|
const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit,
|
|
IgnoreBlockBoundaries aIgnoreBlockBoundaries);
|
|
|
|
/**
|
|
* Return true if splitting aBlockElementToSplit at aPointToSplit will create
|
|
* empty right element.
|
|
*
|
|
* @param aBlockElementToSplit The paragraph element which we want to
|
|
* split.
|
|
* @param aPointToSplit The split position in aBlockElementToSplit.
|
|
* @param aIgnoreBlockBoundaries If No, return true only when aPointToSplit
|
|
* is immediately before a block boundary of
|
|
* aBlockElementToSplit. In other words,
|
|
* may return true only when aPointToSplit
|
|
* is not in a child block of
|
|
* aBlockElementToSplit.
|
|
* If Yes, return true even when aPointToSplit
|
|
* is immediately before any current block
|
|
* boundary which is followed by the block
|
|
* boundary of aBlockElementToSplit. In other
|
|
* words, return true when aPointToSplit is in
|
|
* a child block which is end of any ancestor
|
|
* block elements.
|
|
*/
|
|
[[nodiscard]] static bool SplitPointIsEndOfSplittingBlock(
|
|
const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit,
|
|
IgnoreBlockBoundaries aIgnoreBlockBoundaries);
|
|
|
|
MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor;
|
|
MOZ_KNOWN_LIVE const Element& mEditingHost;
|
|
MOZ_KNOWN_LIVE nsStaticAtom& mDefaultParagraphSeparatorTagName;
|
|
const ParagraphSeparator mDefaultParagraphSeparator;
|
|
};
|
|
|
|
/**
|
|
* Handle "insertLineBreak" command.
|
|
*/
|
|
class MOZ_STACK_CLASS HTMLEditor::AutoInsertLineBreakHandler final {
|
|
public:
|
|
AutoInsertLineBreakHandler() = delete;
|
|
AutoInsertLineBreakHandler(const AutoInsertLineBreakHandler&) = delete;
|
|
AutoInsertLineBreakHandler(AutoInsertLineBreakHandler&&) = delete;
|
|
|
|
MOZ_CAN_RUN_SCRIPT explicit AutoInsertLineBreakHandler(
|
|
HTMLEditor& aHTMLEditor, const Element& aEditingHost)
|
|
: mHTMLEditor(aHTMLEditor), mEditingHost(aEditingHost) {}
|
|
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult Run();
|
|
|
|
private:
|
|
/**
|
|
* Insert a linefeed character into aPointToBreak.
|
|
*
|
|
* @param aPointToBreak The point where new linefeed character will be
|
|
* inserted before.
|
|
* @param aEditingHost Current active editing host.
|
|
* @return A suggest point to put caret.
|
|
*/
|
|
[[nodiscard]] static MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
|
|
InsertLinefeed(HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToBreak,
|
|
const Element& aEditingHost);
|
|
|
|
/**
|
|
* Insert <br> element at `Selection`.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleInsertBRElement();
|
|
|
|
/**
|
|
* Insert a linefeed at `Selection`.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleInsertLinefeed();
|
|
|
|
friend class AutoInsertParagraphHandler;
|
|
|
|
MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor;
|
|
MOZ_KNOWN_LIVE const Element& mEditingHost;
|
|
};
|
|
|
|
/**
|
|
* Handle delete multiple ranges, typically they are the selection ranges.
|
|
*/
|
|
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,
|
|
AutoClonedSelectionRangeArray& 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,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
const Element& aEditingHost);
|
|
|
|
private:
|
|
enum class ComputeRangeFor : bool { GetTargetRanges, ToDeleteTheRange };
|
|
|
|
[[nodiscard]] bool IsHandlingRecursively() const {
|
|
return mParent != nullptr;
|
|
}
|
|
|
|
[[nodiscard]] bool CanFallbackToDeleteRangeWithTransaction(
|
|
const nsRange& aRangeToDelete) const;
|
|
|
|
[[nodiscard]] bool CanFallbackToDeleteRangesWithTransaction(
|
|
const AutoClonedSelectionRangeArray& aRangesToDelete) const;
|
|
|
|
/**
|
|
* 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,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
const WSRunScanner& aWSRunScannerAtCaret,
|
|
const WSScanResult& aScanFromCaretPointResult,
|
|
const Element& aEditingHost);
|
|
nsresult ComputeRangesToDeleteAroundCollapsedRanges(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
AutoClonedSelectionRangeArray& 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,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
SelectionWasCollapsed aSelectionWasCollapsed,
|
|
const Element& aEditingHost);
|
|
nsresult ComputeRangesToDeleteNonCollapsedRanges(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
AutoClonedSelectionRangeArray& 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,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
const Element& aEditingHost);
|
|
nsresult ComputeRangesToDeleteTextAroundCollapsedRanges(
|
|
nsIEditor::EDirection aDirectionAndAmount,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete) const;
|
|
|
|
/**
|
|
* 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(
|
|
const nsIContent& aAtomicContent,
|
|
AutoClonedSelectionRangeArray& 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.
|
|
*/
|
|
[[nodiscard]] 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,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
const Element& aEditingHost);
|
|
|
|
/**
|
|
* ExtendOrShrinkRangeToDelete() extends aRangeToDelete if there are
|
|
* an invisible <br> element and/or some parent empty elements.
|
|
*
|
|
* @param aLimitersAndCaretData The frame selection data.
|
|
* @param aRangeToDelete The range to be extended for deletion. This
|
|
* must not be collapsed, must be positioned.
|
|
*/
|
|
template <typename EditorDOMRangeType>
|
|
[[nodiscard]] Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToDelete(
|
|
const HTMLEditor& aHTMLEditor,
|
|
const LimitersAndCaretData& aLimitersAndCaretData,
|
|
const EditorDOMRangeType& aRangeToDelete,
|
|
SelectionWasCollapsed aSelectionWasCollapsed,
|
|
ComputeRangeFor aComputeRangeFor, const Element& aEditingHost) const;
|
|
|
|
/**
|
|
* Extend the start boundary of aRangeToDelete to contain ancestor inline
|
|
* elements which will be empty once the content in aRangeToDelete is removed
|
|
* from the tree.
|
|
*
|
|
* NOTE: This is designed for deleting inline elements which become empty if
|
|
* aRangeToDelete which crosses a block boundary of right block child.
|
|
* Therefore, you may need to improve this method if you want to use this in
|
|
* the other cases.
|
|
*
|
|
* @param aRangeToDelete [in/out] The range to delete. This start
|
|
* boundary may be modified.
|
|
* @param aEditingHost The editing host.
|
|
* @return true if aRangeToDelete is modified.
|
|
* false if aRangeToDelete is not modified.
|
|
* error if aRangeToDelete gets unexpected
|
|
* situation.
|
|
*/
|
|
[[nodiscard]] static Result<bool, nsresult>
|
|
ExtendRangeToContainAncestorInlineElementsAtStart(
|
|
nsRange& aRangeToDelete, const Element& aEditingHost);
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
[[nodiscard]] static EditorRawDOMRange
|
|
GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
|
|
const EditorRawDOMRange& aRangeToDelete,
|
|
ComputeRangeFor aComputeRangeFor);
|
|
|
|
/**
|
|
* DeleteUnnecessaryNodes() removes unnecessary nodes around aRange.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
|
DeleteUnnecessaryNodes(HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
|
|
const Element& aEditingHost);
|
|
|
|
/**
|
|
* 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,
|
|
const Element& aEditingHost);
|
|
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
|
FallbackToDeleteRangeWithTransaction(HTMLEditor& aHTMLEditor,
|
|
nsRange& aRangeToDelete) const {
|
|
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
|
|
MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete));
|
|
Result<CaretPoint, nsresult> caretPointOrError =
|
|
aHTMLEditor.DeleteRangeWithTransaction(mOriginalDirectionAndAmount,
|
|
mOriginalStripWrappers,
|
|
aRangeToDelete);
|
|
NS_WARNING_ASSERTION(caretPointOrError.isOk(),
|
|
"EditorBase::DeleteRangeWithTransaction() failed");
|
|
return caretPointOrError;
|
|
}
|
|
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
|
|
FallbackToDeleteRangesWithTransaction(
|
|
HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
const Element& aEditingHost) const;
|
|
|
|
/**
|
|
* Compute target range(s) which will be called by
|
|
* `EditorBase::DeleteRangeWithTransaction()` or
|
|
* `HTMLEditor::DeleteRangesWithTransaction()`.
|
|
* TODO: We should not use it for consistency with each deletion handler
|
|
* in this and nested classes.
|
|
*/
|
|
nsresult ComputeRangeToDeleteRangeWithTransaction(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
nsRange& aRange, const Element& aEditingHost) const;
|
|
nsresult ComputeRangesToDeleteRangesWithTransaction(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
const Element& aEditingHost) const;
|
|
|
|
nsresult FallbackToComputeRangeToDeleteRangeWithTransaction(
|
|
const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete,
|
|
const Element& aEditingHost) const {
|
|
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
|
|
MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete));
|
|
nsresult rv = ComputeRangeToDeleteRangeWithTransaction(
|
|
aHTMLEditor, mOriginalDirectionAndAmount, aRangeToDelete, aEditingHost);
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"AutoDeleteRangesHandler::"
|
|
"ComputeRangeToDeleteRangeWithTransaction() failed");
|
|
return rv;
|
|
}
|
|
nsresult FallbackToComputeRangesToDeleteRangesWithTransaction(
|
|
const HTMLEditor& aHTMLEditor,
|
|
AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
const Element& aEditingHost) const {
|
|
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
|
|
MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
|
|
nsresult rv = ComputeRangesToDeleteRangesWithTransaction(
|
|
aHTMLEditor, mOriginalDirectionAndAmount, aRangesToDelete,
|
|
aEditingHost);
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"AutoDeleteRangesHandler::"
|
|
"ComputeRangesToDeleteRangesWithTransaction() failed");
|
|
return rv;
|
|
}
|
|
|
|
class MOZ_STACK_CLASS AutoBlockElementsJoiner;
|
|
class MOZ_STACK_CLASS AutoEmptyBlockAncestorDeleter;
|
|
|
|
const AutoDeleteRangesHandler* const mParent;
|
|
nsIEditor::EDirection mOriginalDirectionAndAmount;
|
|
nsIEditor::EStripWrappers mOriginalStripWrappers;
|
|
};
|
|
|
|
/**
|
|
* Handle join block elements. Despite the name, this may just move first line
|
|
* of a block into another block or just deleted the range with keeping table
|
|
* structure.
|
|
*/
|
|
class MOZ_STACK_CLASS
|
|
HTMLEditor::AutoDeleteRangesHandler::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).
|
|
* @param aEditingHost The editing host.
|
|
* @return true if can continue to handle the
|
|
* deletion.
|
|
*/
|
|
[[nodiscard]] bool PrepareToDeleteAtCurrentBlockBoundary(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint,
|
|
const Element& aEditingHost);
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
[[nodiscard]] bool PrepareToDeleteAtOtherBlockBoundary(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
Element& aOtherBlockElement, const EditorDOMPoint& aCaretPoint,
|
|
const WSRunScanner& aWSRunScannerAtCaret);
|
|
|
|
/**
|
|
* PrepareToDeleteNonCollapsedRange() considers left block element and
|
|
* right block element which are inclusive ancestor block element of
|
|
* start and end container of aRangeToDelete
|
|
*
|
|
* @param aHTMLEditor The HTML editor.
|
|
* @param aRangeToDelete The range to delete. Must not be
|
|
* collapsed.
|
|
* @param aEditingHost The editing host.
|
|
* @return true if can continue to handle the
|
|
* deletion.
|
|
*/
|
|
[[nodiscard]] bool PrepareToDeleteNonCollapsedRange(
|
|
const HTMLEditor& aHTMLEditor, const nsRange& aRangeToDelete,
|
|
const Element& aEditingHost);
|
|
|
|
/**
|
|
* 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 aRangeToDelete The range to delete. 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, nsRange& aRangeToDelete,
|
|
const Element& aEditingHost);
|
|
|
|
nsresult ComputeRangeToDelete(const HTMLEditor& aHTMLEditor,
|
|
nsIEditor::EDirection aDirectionAndAmount,
|
|
const EditorDOMPoint& aCaretPoint,
|
|
nsRange& aRangeToDelete,
|
|
const Element& aEditingHost) const;
|
|
|
|
/**
|
|
* Run() executes the joining.
|
|
*
|
|
* @param aHTMLEditor The HTML editor.
|
|
* @param aLimitersAndCaretData The data copied from nsFrameSelection.
|
|
* @param aDirectionAndAmount Direction of the deletion.
|
|
* @param aStripWrappers Whether delete or keep new empty
|
|
* ancestor elements.
|
|
* @param aRangeToDelete The range 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,
|
|
const LimitersAndCaretData& aLimitersAndCaretData,
|
|
nsIEditor::EDirection aDirectionAndAmount,
|
|
nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
|
|
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
|
|
const Element& aEditingHost);
|
|
|
|
nsresult ComputeRangeToDelete(
|
|
const HTMLEditor& aHTMLEditor,
|
|
const AutoClonedSelectionRangeArray& aRangesToDelete,
|
|
nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
|
|
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
|
|
const Element& aEditingHost) const;
|
|
|
|
[[nodiscard]] 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 ComputeRangeToDeleteAtCurrentBlockBoundary(
|
|
const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
|
|
nsRange& aRangeToDelete, const Element& aEditingHost) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
|
|
HandleDeleteAtOtherBlockBoundary(HTMLEditor& aHTMLEditor,
|
|
nsIEditor::EDirection aDirectionAndAmount,
|
|
nsIEditor::EStripWrappers aStripWrappers,
|
|
const EditorDOMPoint& aCaretPoint,
|
|
nsRange& aRangeToDelete,
|
|
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 ComputeRangeToDeleteAtOtherBlockBoundary(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete,
|
|
const Element& aEditingHost) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
|
|
JoinBlockElementsInSameParent(
|
|
HTMLEditor& aHTMLEditor,
|
|
const LimitersAndCaretData& aLimitersAndCaretData,
|
|
nsIEditor::EDirection aDirectionAndAmount,
|
|
nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
|
|
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
|
|
const Element& aEditingHost);
|
|
nsresult ComputeRangeToJoinBlockElementsInSameParent(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
nsRange& aRangeToDelete, const Element& aEditingHost) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
|
|
HandleDeleteLineBreak(HTMLEditor& aHTMLEditor,
|
|
nsIEditor::EDirection aDirectionAndAmount,
|
|
const EditorDOMPoint& aCaretPoint,
|
|
const Element& aEditingHost);
|
|
nsresult ComputeRangeToDeleteLineBreak(
|
|
const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete,
|
|
const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
|
|
DeleteContentInRange(HTMLEditor& aHTMLEditor,
|
|
const LimitersAndCaretData& aLimitersAndCaretData,
|
|
nsIEditor::EDirection aDirectionAndAmount,
|
|
nsIEditor::EStripWrappers aStripWrappers,
|
|
nsRange& aRangeToDelete, const Element& aEditingHost);
|
|
nsresult ComputeRangeToDeleteContentInRange(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
nsRange& aRange, const Element& aEditingHost) const;
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
|
|
HandleDeleteNonCollapsedRange(
|
|
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
|
|
AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
|
|
const Element& aEditingHost);
|
|
nsresult ComputeRangeToDeleteNonCollapsedRange(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
nsRange& aRangeToDelete,
|
|
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.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
|
|
JoinNodesDeepWithTransaction(HTMLEditor& aHTMLEditor,
|
|
nsIContent& aLeftContent,
|
|
nsIContent& aRightContent);
|
|
|
|
enum class PutCaretTo : bool { StartOfRange, EndOfRange };
|
|
|
|
/**
|
|
* DeleteNodesEntirelyInRangeButKeepTableStructure() removes each node in
|
|
* aArrayOfContent. However, 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.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult>
|
|
DeleteNodesEntirelyInRangeButKeepTableStructure(
|
|
HTMLEditor& aHTMLEditor,
|
|
const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContent,
|
|
PutCaretTo aPutCaretTo);
|
|
[[nodiscard]] bool NeedsToJoinNodesAfterDeleteNodesEntirelyInRange() const;
|
|
Result<bool, nsresult>
|
|
ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure(
|
|
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 Result<DeleteRangeResult, 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 Result<DeleteRangeResult, nsresult>
|
|
DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange,
|
|
PutCaretTo aPutCaretTo);
|
|
|
|
/**
|
|
* Return a block element which is an inclusive ancestor of the container of
|
|
* aPoint if aPoint is start of ancestor blocks. For example, if `<div
|
|
* id=div1>abc<div id=div2><div id=div3>[]def</div></div></div>`, return
|
|
* #div2.
|
|
*/
|
|
template <typename EditorDOMPointType>
|
|
[[nodiscard]] static Result<Element*, nsresult>
|
|
GetMostDistantBlockAncestorIfPointIsStartAtBlock(
|
|
const EditorDOMPointType& aPoint, const Element& aEditingHost,
|
|
const Element* aAncestorLimiter = nullptr);
|
|
|
|
/**
|
|
* Extend aRangeToDelete to contain new empty inline ancestors and contain
|
|
* an invisible <br> element before right child block which causes an empty
|
|
* line but the range starts after it.
|
|
*/
|
|
void ExtendRangeToDeleteNonCollapsedRange(
|
|
const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete,
|
|
const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const;
|
|
|
|
/**
|
|
* Compute mLeafContentInOtherBlock from the DOM.
|
|
*/
|
|
[[nodiscard]] nsIContent* ComputeLeafContentInOtherBlockElement(
|
|
nsIEditor::EDirection aDirectionAndAmount) const;
|
|
|
|
class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner;
|
|
|
|
enum class Mode {
|
|
NotInitialized,
|
|
JoinCurrentBlock,
|
|
JoinOtherBlock,
|
|
JoinBlocksInSameParent,
|
|
DeleteBRElement,
|
|
// The instance will handle only the <br> element immediately before a
|
|
// block.
|
|
DeletePrecedingBRElementOfBlock,
|
|
// The instance will handle only the preceding preformatted line break
|
|
// before a block.
|
|
DeletePrecedingPreformattedLineBreak,
|
|
DeleteContentInRange,
|
|
DeleteNonCollapsedRange,
|
|
// The instance will handle preceding lines of the right block and content
|
|
// in the range in the right block.
|
|
DeletePrecedingLinesAndContentInRange,
|
|
};
|
|
AutoDeleteRangesHandler* mDeleteRangesHandler;
|
|
const AutoDeleteRangesHandler& mDeleteRangesHandlerConst;
|
|
nsCOMPtr<nsIContent> mLeftContent;
|
|
nsCOMPtr<nsIContent> mRightContent;
|
|
nsCOMPtr<nsIContent> mLeafContentInOtherBlock;
|
|
RefPtr<Element> mOtherBlockElement;
|
|
// 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;
|
|
EditorDOMPointInText mPreformattedLineBreak;
|
|
Mode mMode = Mode::NotInitialized;
|
|
};
|
|
|
|
/**
|
|
* Actually handle joining inclusive ancestor block elements.
|
|
*/
|
|
class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler::
|
|
AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner final {
|
|
public:
|
|
AutoInclusiveAncestorBlockElementsJoiner() = delete;
|
|
AutoInclusiveAncestorBlockElementsJoiner(
|
|
nsIContent& aInclusiveDescendantOfLeftBlockElement,
|
|
nsIContent& aInclusiveDescendantOfRightBlockElement)
|
|
: mInclusiveDescendantOfLeftBlockElement(
|
|
aInclusiveDescendantOfLeftBlockElement),
|
|
mInclusiveDescendantOfRightBlockElement(
|
|
aInclusiveDescendantOfRightBlockElement),
|
|
mCanJoinBlocks(false),
|
|
mFallbackToDeleteLeafContent(false) {}
|
|
|
|
[[nodiscard]] bool IsSet() const {
|
|
return mLeftBlockElement && mRightBlockElement;
|
|
}
|
|
[[nodiscard]] bool IsSameBlockElement() const {
|
|
return mLeftBlockElement && mLeftBlockElement == mRightBlockElement;
|
|
}
|
|
|
|
/**
|
|
* Prepare for joining inclusive ancestor block elements. When this
|
|
* returns false, the deletion should be canceled.
|
|
*/
|
|
[[nodiscard]] Result<bool, nsresult> Prepare(const HTMLEditor& aHTMLEditor,
|
|
const Element& aEditingHost);
|
|
|
|
/**
|
|
* When this returns true, this can join the blocks with `Run()`.
|
|
*/
|
|
[[nodiscard]] 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.
|
|
*/
|
|
[[nodiscard]] bool ShouldDeleteLeafContentInstead() const {
|
|
MOZ_ASSERT(CanJoinBlocks());
|
|
return mFallbackToDeleteLeafContent;
|
|
}
|
|
|
|
/**
|
|
* ComputeRangesToDelete() extends aRangeToDelete includes the element
|
|
* boundaries between joining blocks. If they won't be joined, this
|
|
* collapses the range to aCaretPoint.
|
|
*/
|
|
[[nodiscard]] nsresult ComputeRangeToDelete(
|
|
const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
|
|
nsRange& aRangeToDelete, const Element& aEditingHost) const;
|
|
|
|
/**
|
|
* Join inclusive ancestor block elements which are found by preceding
|
|
* Prepare() 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<DeleteRangeResult, nsresult> Run(
|
|
HTMLEditor& aHTMLEditor, const Element& aEditingHost);
|
|
|
|
private:
|
|
/**
|
|
* This method returns true when
|
|
* `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`,
|
|
* `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` and
|
|
* `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` handle it
|
|
* with the `if` block of the main lambda of them.
|
|
*/
|
|
[[nodiscard]] 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() &&
|
|
EditorUtils::GetComputedWhiteSpaceStyles(*mLeftBlockElement) ==
|
|
EditorUtils::GetComputedWhiteSpaceStyles(*mRightBlockElement));
|
|
}
|
|
|
|
OwningNonNull<nsIContent> mInclusiveDescendantOfLeftBlockElement;
|
|
OwningNonNull<nsIContent> mInclusiveDescendantOfRightBlockElement;
|
|
RefPtr<Element> mLeftBlockElement;
|
|
RefPtr<Element> mRightBlockElement;
|
|
Maybe<nsAtom*> mNewListElementTagNameOfRightListElement;
|
|
EditorDOMPoint mPointContainingTheOtherBlockElement;
|
|
RefPtr<dom::HTMLBRElement> mPrecedingInvisibleBRElement;
|
|
bool mCanJoinBlocks;
|
|
bool mFallbackToDeleteLeafContent;
|
|
};
|
|
|
|
/**
|
|
* Handle deleting empty block ancestors.
|
|
*/
|
|
class MOZ_STACK_CLASS
|
|
HTMLEditor::AutoDeleteRangesHandler::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,
|
|
AutoClonedSelectionRangeArray& 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.
|
|
* @param aEditingHost The editing host.
|
|
*/
|
|
[[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult> Run(
|
|
HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
const Element& aEditingHost);
|
|
|
|
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<DeleteRangeResult, 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<CreateLineBreakResult, nsresult>
|
|
MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor);
|
|
|
|
/**
|
|
* GetNewCaretPosition() returns new caret position after deleting
|
|
* `mEmptyInclusiveAncestorBlockElement`.
|
|
*/
|
|
[[nodiscard]] Result<CaretPoint, nsresult> GetNewCaretPosition(
|
|
const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
|
|
const Element& aEditingHost) const;
|
|
|
|
RefPtr<Element> mEmptyInclusiveAncestorBlockElement;
|
|
};
|
|
|
|
/******************************************************************************
|
|
* NormalizedStringToInsertText stores normalized insertion string with
|
|
* normalized surrounding white-spaces if the insertion point is surrounded by
|
|
* collapsible white-spaces. For deleting invisible (collapsed) white-spaces,
|
|
* this also stores the replace range and new white-space length before and
|
|
* after the inserting text.
|
|
******************************************************************************/
|
|
|
|
struct MOZ_STACK_CLASS HTMLEditor::NormalizedStringToInsertText final {
|
|
NormalizedStringToInsertText(
|
|
const nsAString& aStringToInsertWithoutSurroundingWhiteSpaces,
|
|
const EditorDOMPoint& aPointToInsert)
|
|
: mNormalizedString(aStringToInsertWithoutSurroundingWhiteSpaces),
|
|
mReplaceStartOffset(
|
|
aPointToInsert.IsInTextNode() ? aPointToInsert.Offset() : 0u),
|
|
mReplaceEndOffset(mReplaceStartOffset) {
|
|
MOZ_ASSERT(aStringToInsertWithoutSurroundingWhiteSpaces.Length() ==
|
|
InsertingTextLength());
|
|
}
|
|
|
|
NormalizedStringToInsertText(
|
|
const nsAString& aStringToInsertWithSurroundingWhiteSpaces,
|
|
uint32_t aInsertOffset, uint32_t aReplaceStartOffset,
|
|
uint32_t aReplaceLength,
|
|
uint32_t aNewPrecedingWhiteSpaceLengthBeforeInsertionString,
|
|
uint32_t aNewFollowingWhiteSpaceLengthAfterInsertionString)
|
|
: mNormalizedString(aStringToInsertWithSurroundingWhiteSpaces),
|
|
mReplaceStartOffset(aReplaceStartOffset),
|
|
mReplaceEndOffset(mReplaceStartOffset + aReplaceLength),
|
|
mReplaceLengthBefore(aInsertOffset - mReplaceStartOffset),
|
|
mReplaceLengthAfter(aReplaceLength - mReplaceLengthBefore),
|
|
mNewLengthBefore(aNewPrecedingWhiteSpaceLengthBeforeInsertionString),
|
|
mNewLengthAfter(aNewFollowingWhiteSpaceLengthAfterInsertionString) {
|
|
MOZ_ASSERT(aReplaceStartOffset <= aInsertOffset);
|
|
MOZ_ASSERT(aReplaceStartOffset + aReplaceLength >= aInsertOffset);
|
|
MOZ_ASSERT(aNewPrecedingWhiteSpaceLengthBeforeInsertionString +
|
|
aNewFollowingWhiteSpaceLengthAfterInsertionString <
|
|
mNormalizedString.Length());
|
|
MOZ_ASSERT(mReplaceLengthBefore + mReplaceLengthAfter == ReplaceLength());
|
|
MOZ_ASSERT(mReplaceLengthBefore >= mNewLengthBefore);
|
|
MOZ_ASSERT(mReplaceLengthAfter >= mNewLengthAfter);
|
|
}
|
|
|
|
NormalizedStringToInsertText GetMinimizedData(const Text& aText) const {
|
|
if (mNormalizedString.IsEmpty() || !ReplaceLength()) {
|
|
return *this;
|
|
}
|
|
const dom::CharacterDataBuffer& characterDataBuffer = aText.DataBuffer();
|
|
const uint32_t minimizedReplaceStart = [&]() {
|
|
const auto firstDiffCharOffset =
|
|
mNewLengthBefore ? characterDataBuffer.FindFirstDifferentCharOffset(
|
|
PrecedingWhiteSpaces(), mReplaceStartOffset)
|
|
: dom::CharacterDataBuffer::kNotFound;
|
|
if (firstDiffCharOffset == dom::CharacterDataBuffer::kNotFound) {
|
|
return
|
|
// We don't need to insert new normalized white-spaces before the
|
|
// inserting string,
|
|
(mReplaceStartOffset + mReplaceLengthBefore)
|
|
// but keep extending the replacing range for deleting invisible
|
|
// white-spaces.
|
|
- DeletingPrecedingInvisibleWhiteSpaces();
|
|
}
|
|
return firstDiffCharOffset;
|
|
}();
|
|
const uint32_t minimizedReplaceEnd = [&]() {
|
|
const auto lastDiffCharOffset =
|
|
mNewLengthAfter ? characterDataBuffer.RFindFirstDifferentCharOffset(
|
|
FollowingWhiteSpaces(), mReplaceEndOffset)
|
|
: dom::CharacterDataBuffer::kNotFound;
|
|
if (lastDiffCharOffset == dom::CharacterDataBuffer::kNotFound) {
|
|
return
|
|
// We don't need to insert new normalized white-spaces after the
|
|
// inserting string,
|
|
(mReplaceEndOffset - mReplaceLengthAfter)
|
|
// but keep extending the replacing range for deleting invisible
|
|
// white-spaces.
|
|
+ DeletingFollowingInvisibleWhiteSpaces();
|
|
}
|
|
return lastDiffCharOffset + 1u;
|
|
}();
|
|
if (minimizedReplaceStart == mReplaceStartOffset &&
|
|
minimizedReplaceEnd == mReplaceEndOffset) {
|
|
return *this;
|
|
}
|
|
const uint32_t newPrecedingWhiteSpaceLength =
|
|
mNewLengthBefore - (minimizedReplaceStart - mReplaceStartOffset);
|
|
const uint32_t newFollowingWhiteSpaceLength =
|
|
mNewLengthAfter - (mReplaceEndOffset - minimizedReplaceEnd);
|
|
return NormalizedStringToInsertText(
|
|
Substring(mNormalizedString,
|
|
mNewLengthBefore - newPrecedingWhiteSpaceLength,
|
|
mNormalizedString.Length() -
|
|
(mNewLengthBefore - newPrecedingWhiteSpaceLength) -
|
|
(mNewLengthAfter - newFollowingWhiteSpaceLength)),
|
|
OffsetToInsertText(), minimizedReplaceStart,
|
|
minimizedReplaceEnd - minimizedReplaceStart,
|
|
newPrecedingWhiteSpaceLength, newFollowingWhiteSpaceLength);
|
|
}
|
|
|
|
/**
|
|
* Return offset to insert the given text.
|
|
*/
|
|
[[nodiscard]] uint32_t OffsetToInsertText() const {
|
|
return mReplaceStartOffset + mReplaceLengthBefore;
|
|
}
|
|
|
|
/**
|
|
* Return inserting text length not containing the surrounding white-spaces.
|
|
*/
|
|
[[nodiscard]] uint32_t InsertingTextLength() const {
|
|
return mNormalizedString.Length() - mNewLengthBefore - mNewLengthAfter;
|
|
}
|
|
|
|
/**
|
|
* Return end offset of inserted string after replacing the text with
|
|
* mNormalizedString.
|
|
*/
|
|
[[nodiscard]] uint32_t EndOffsetOfInsertedText() const {
|
|
return OffsetToInsertText() + InsertingTextLength();
|
|
}
|
|
|
|
/**
|
|
* Return the length to replace with mNormalizedString. The result means that
|
|
* it's the length of surrounding white-spaces at the insertion point.
|
|
*/
|
|
[[nodiscard]] uint32_t ReplaceLength() const {
|
|
return mReplaceEndOffset - mReplaceStartOffset;
|
|
}
|
|
|
|
[[nodiscard]] uint32_t DeletingPrecedingInvisibleWhiteSpaces() const {
|
|
return mReplaceLengthBefore - mNewLengthBefore;
|
|
}
|
|
[[nodiscard]] uint32_t DeletingFollowingInvisibleWhiteSpaces() const {
|
|
return mReplaceLengthAfter - mNewLengthAfter;
|
|
}
|
|
|
|
[[nodiscard]] nsDependentSubstring PrecedingWhiteSpaces() const {
|
|
return Substring(mNormalizedString, 0u, mNewLengthBefore);
|
|
}
|
|
[[nodiscard]] nsDependentSubstring FollowingWhiteSpaces() const {
|
|
return Substring(mNormalizedString,
|
|
mNormalizedString.Length() - mNewLengthAfter);
|
|
}
|
|
|
|
// Normalizes string which should be inserted.
|
|
nsAutoString mNormalizedString;
|
|
// Start offset in the `Text` to replace.
|
|
const uint32_t mReplaceStartOffset;
|
|
// End offset in the `Text` to replace.
|
|
const uint32_t mReplaceEndOffset;
|
|
// If it needs to replace preceding and/or following white-spaces, these
|
|
// members store the length of white-spaces which should be replaced
|
|
// before/after the insertion point.
|
|
const uint32_t mReplaceLengthBefore = 0u;
|
|
const uint32_t mReplaceLengthAfter = 0u;
|
|
// If it needs to replace preceding and/or following white-spaces, these
|
|
// members store the new length of white-spaces before/after the insertion
|
|
// string.
|
|
const uint32_t mNewLengthBefore = 0u;
|
|
const uint32_t mNewLengthAfter = 0u;
|
|
};
|
|
|
|
/******************************************************************************
|
|
* ReplaceWhiteSpacesData stores normalized string to replace white-spaces in
|
|
* a `Text`. If ReplaceLength() returns 0, this user needs to do nothing.
|
|
******************************************************************************/
|
|
|
|
struct MOZ_STACK_CLASS HTMLEditor::ReplaceWhiteSpacesData final {
|
|
ReplaceWhiteSpacesData() = default;
|
|
|
|
/**
|
|
* @param aWhiteSpaces The new white-spaces which we will replace the
|
|
* range with.
|
|
* @param aStartOffset Replace start offset in the text node.
|
|
* @param aReplaceLength Replace length in the text node.
|
|
* @param aOffsetAfterReplacing
|
|
* [optional] If the caller may want to put caret
|
|
* middle of the white-spaces, the offset may be
|
|
* changed by deleting some invisible white-spaces.
|
|
* Therefore, this may be set for the purpose.
|
|
*/
|
|
ReplaceWhiteSpacesData(const nsAString& aWhiteSpaces, uint32_t aStartOffset,
|
|
uint32_t aReplaceLength,
|
|
uint32_t aOffsetAfterReplacing = UINT32_MAX)
|
|
: mNormalizedString(aWhiteSpaces),
|
|
mReplaceStartOffset(aStartOffset),
|
|
mReplaceEndOffset(aStartOffset + aReplaceLength),
|
|
mNewOffsetAfterReplace(aOffsetAfterReplacing) {
|
|
MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
|
|
MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX,
|
|
mNewOffsetAfterReplace <=
|
|
mReplaceStartOffset + mNormalizedString.Length());
|
|
}
|
|
|
|
/**
|
|
* @param aWhiteSpaces The new white-spaces which we will replace the
|
|
* range with.
|
|
* @param aStartOffset Replace start offset in the text node.
|
|
* @param aReplaceLength Replace length in the text node.
|
|
* @param aOffsetAfterReplacing
|
|
* [optional] If the caller may want to put caret
|
|
* middle of the white-spaces, the offset may be
|
|
* changed by deleting some invisible white-spaces.
|
|
* Therefore, this may be set for the purpose.
|
|
*/
|
|
ReplaceWhiteSpacesData(nsAutoString&& aWhiteSpaces, uint32_t aStartOffset,
|
|
uint32_t aReplaceLength,
|
|
uint32_t aOffsetAfterReplacing = UINT32_MAX)
|
|
: mNormalizedString(std::forward<nsAutoString>(aWhiteSpaces)),
|
|
mReplaceStartOffset(aStartOffset),
|
|
mReplaceEndOffset(aStartOffset + aReplaceLength),
|
|
mNewOffsetAfterReplace(aOffsetAfterReplacing) {
|
|
MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
|
|
MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX,
|
|
mNewOffsetAfterReplace <=
|
|
mReplaceStartOffset + mNormalizedString.Length());
|
|
}
|
|
|
|
ReplaceWhiteSpacesData GetMinimizedData(const Text& aText) const {
|
|
if (!ReplaceLength()) {
|
|
return *this;
|
|
}
|
|
const dom::CharacterDataBuffer& characterDataBuffer = aText.DataBuffer();
|
|
const auto minimizedReplaceStart = [&]() -> uint32_t {
|
|
if (mNormalizedString.IsEmpty()) {
|
|
return mReplaceStartOffset;
|
|
}
|
|
const uint32_t firstDiffCharOffset =
|
|
characterDataBuffer.FindFirstDifferentCharOffset(mNormalizedString,
|
|
mReplaceStartOffset);
|
|
if (firstDiffCharOffset == dom::CharacterDataBuffer::kNotFound) {
|
|
// We don't need to insert new white-spaces,
|
|
return mReplaceStartOffset + mNormalizedString.Length();
|
|
}
|
|
return firstDiffCharOffset;
|
|
}();
|
|
const auto minimizedReplaceEnd = [&]() -> uint32_t {
|
|
if (mNormalizedString.IsEmpty()) {
|
|
return mReplaceEndOffset;
|
|
}
|
|
if (minimizedReplaceStart ==
|
|
mReplaceStartOffset + mNormalizedString.Length()) {
|
|
// Note that here may be invisible white-spaces before
|
|
// mReplaceEndOffset. Then, this value may be larger than
|
|
// minimizedReplaceStart.
|
|
MOZ_ASSERT(mReplaceEndOffset >= minimizedReplaceStart);
|
|
return mReplaceEndOffset;
|
|
}
|
|
if (ReplaceLength() != mNormalizedString.Length()) {
|
|
// If we're deleting some invisible white-spaces, don't shrink the end
|
|
// of the replacing range because it may shrink mNormalizedString too
|
|
// much.
|
|
return mReplaceEndOffset;
|
|
}
|
|
const auto lastDiffCharOffset =
|
|
characterDataBuffer.RFindFirstDifferentCharOffset(mNormalizedString,
|
|
mReplaceEndOffset);
|
|
MOZ_ASSERT(lastDiffCharOffset != dom::CharacterDataBuffer::kNotFound);
|
|
return lastDiffCharOffset == dom::CharacterDataBuffer::kNotFound
|
|
? mReplaceEndOffset
|
|
: lastDiffCharOffset + 1u;
|
|
}();
|
|
if (minimizedReplaceStart == mReplaceStartOffset &&
|
|
minimizedReplaceEnd == mReplaceEndOffset) {
|
|
return *this;
|
|
}
|
|
const uint32_t precedingUnnecessaryLength =
|
|
minimizedReplaceStart - mReplaceStartOffset;
|
|
const uint32_t followingUnnecessaryLength =
|
|
mReplaceEndOffset - minimizedReplaceEnd;
|
|
return ReplaceWhiteSpacesData(
|
|
Substring(mNormalizedString, precedingUnnecessaryLength,
|
|
mNormalizedString.Length() - (precedingUnnecessaryLength +
|
|
followingUnnecessaryLength)),
|
|
minimizedReplaceStart, minimizedReplaceEnd - minimizedReplaceStart,
|
|
mNewOffsetAfterReplace);
|
|
}
|
|
|
|
/**
|
|
* Return the normalized string before mNewOffsetAfterReplace. So,
|
|
* mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range
|
|
* when this is called.
|
|
*
|
|
* @param aReplaceEndOffset Specify the offset in the Text node of
|
|
* mNewOffsetAfterReplace before replacing with the
|
|
* data.
|
|
* @return The substring before mNewOffsetAfterReplace which is typically set
|
|
* for new caret position in the Text node or collapsed deleting range
|
|
* surrounded by the white-spaces.
|
|
*/
|
|
[[nodiscard]] ReplaceWhiteSpacesData PreviousDataOfNewOffset(
|
|
uint32_t aReplaceEndOffset) const {
|
|
MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX);
|
|
MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace);
|
|
MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace);
|
|
MOZ_ASSERT(mReplaceStartOffset <= aReplaceEndOffset);
|
|
MOZ_ASSERT(mReplaceEndOffset >= aReplaceEndOffset);
|
|
if (!ReplaceLength() || aReplaceEndOffset == mReplaceStartOffset) {
|
|
return ReplaceWhiteSpacesData();
|
|
}
|
|
return ReplaceWhiteSpacesData(
|
|
Substring(mNormalizedString, 0u,
|
|
mNewOffsetAfterReplace - mReplaceStartOffset),
|
|
mReplaceStartOffset, aReplaceEndOffset - mReplaceStartOffset);
|
|
}
|
|
|
|
/**
|
|
* Return the normalized string after mNewOffsetAfterReplace. So,
|
|
* mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range
|
|
* when this is called.
|
|
*
|
|
* @param aReplaceStartOffset Specify the replace start offset with the
|
|
* normalized white-spaces.
|
|
* @return The substring after mNewOffsetAfterReplace which is typically set
|
|
* for new caret position in the Text node or collapsed deleting range
|
|
* surrounded by the white-spaces.
|
|
*/
|
|
[[nodiscard]] ReplaceWhiteSpacesData NextDataOfNewOffset(
|
|
uint32_t aReplaceStartOffset) const {
|
|
MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX);
|
|
MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace);
|
|
MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace);
|
|
MOZ_ASSERT(mReplaceStartOffset <= aReplaceStartOffset);
|
|
MOZ_ASSERT(mReplaceEndOffset >= aReplaceStartOffset);
|
|
if (!ReplaceLength() || aReplaceStartOffset == mReplaceEndOffset) {
|
|
return ReplaceWhiteSpacesData();
|
|
}
|
|
return ReplaceWhiteSpacesData(
|
|
Substring(mNormalizedString,
|
|
mNewOffsetAfterReplace - mReplaceStartOffset),
|
|
aReplaceStartOffset, mReplaceEndOffset - aReplaceStartOffset);
|
|
}
|
|
|
|
[[nodiscard]] uint32_t ReplaceLength() const {
|
|
return mReplaceEndOffset - mReplaceStartOffset;
|
|
}
|
|
[[nodiscard]] uint32_t DeletingInvisibleWhiteSpaces() const {
|
|
return ReplaceLength() - mNormalizedString.Length();
|
|
}
|
|
|
|
[[nodiscard]] ReplaceWhiteSpacesData operator+(
|
|
const ReplaceWhiteSpacesData& aOther) const {
|
|
if (!ReplaceLength()) {
|
|
return aOther;
|
|
}
|
|
if (!aOther.ReplaceLength()) {
|
|
return *this;
|
|
}
|
|
MOZ_ASSERT(mReplaceEndOffset == aOther.mReplaceStartOffset);
|
|
MOZ_ASSERT_IF(
|
|
aOther.mNewOffsetAfterReplace != UINT32_MAX,
|
|
aOther.mNewOffsetAfterReplace >= DeletingInvisibleWhiteSpaces());
|
|
return ReplaceWhiteSpacesData(
|
|
nsAutoString(mNormalizedString + aOther.mNormalizedString),
|
|
mReplaceStartOffset, aOther.mReplaceEndOffset,
|
|
aOther.mNewOffsetAfterReplace != UINT32_MAX
|
|
? aOther.mNewOffsetAfterReplace - DeletingInvisibleWhiteSpaces()
|
|
: mNewOffsetAfterReplace);
|
|
}
|
|
|
|
nsAutoString mNormalizedString;
|
|
const uint32_t mReplaceStartOffset = 0u;
|
|
const uint32_t mReplaceEndOffset = 0u;
|
|
// If the caller specifies a point in a white-space sequence, some invisible
|
|
// white-spaces will be deleted with replacing them with normalized string.
|
|
// Then, they may want to keep the position for putting caret or something.
|
|
// So, this may store a specific offset in the text node after replacing.
|
|
const uint32_t mNewOffsetAfterReplace = UINT32_MAX;
|
|
};
|
|
|
|
/******************************************************************************
|
|
* A runnable to run HTMLEditor::OnModifiedDocument when it's safe.
|
|
******************************************************************************/
|
|
|
|
class HTMLEditor::DocumentModifiedEvent final : public Runnable {
|
|
public:
|
|
explicit DocumentModifiedEvent(HTMLEditor& aHTMLEditor)
|
|
: Runnable("DocumentModifiedEvent"), mHTMLEditor(aHTMLEditor) {}
|
|
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() {
|
|
(void)MOZ_KnownLive(mHTMLEditor)->OnModifyDocument(*this);
|
|
return NS_OK;
|
|
}
|
|
|
|
const nsTArray<EditorDOMPointInText>& NewInvisibleWhiteSpacesRef() const {
|
|
return mNewInvisibleWhiteSpaces;
|
|
}
|
|
|
|
private:
|
|
~DocumentModifiedEvent() = default;
|
|
|
|
const OwningNonNull<HTMLEditor> mHTMLEditor;
|
|
nsTArray<EditorDOMPointInText> mNewInvisibleWhiteSpaces;
|
|
};
|
|
|
|
} // namespace mozilla
|
|
|
|
#endif // #ifndef HTMLEditorNestedClasses_h
|