/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 HTMLEditUtils_h #define HTMLEditUtils_h
/** * This header declares/defines static helper methods as members of * HTMLEditUtils. If you want to create or look for helper trivial classes for * HTMLEditor, see HTMLEditHelpers.h.
*/
enumclass CollectChildrenOption { // Ignore non-editable nodes
IgnoreNonEditableChildren, // Ignore invisible text nodes
IgnoreInvisibleTextNodes, // Collect list children too.
CollectListChildren, // Collect table children too.
CollectTableChildren,
};
class HTMLEditUtils final { using AbstractRange = dom::AbstractRange; using Element = dom::Element; using Selection = dom::Selection; using Text = dom::Text;
/** * IsSimplyEditableNode() returns true when aNode is simply editable. * This does NOT means that aNode can be removed from current parent nor * aNode's data is editable.
*/ staticbool IsSimplyEditableNode(const nsINode& aNode) { return aNode.IsEditable();
}
/** * Return true if aElement is an editing host which is either: * - the root element * - parent is not editable * - the <body> element of the document
*/
[[nodiscard]] staticbool ElementIsEditableRoot(const Element& aElement);
/** * Return true if inclusive flat tree ancestor has `inert` state.
*/ staticbool ContentIsInert(const nsIContent& aContent);
/** * IsNeverContentEditableElementByUser() returns true if the element's content * is never editable by user. E.g., the content is always replaced by * native anonymous node or something.
*/ staticbool IsNeverElementContentsEditableByUser(const nsIContent& aContent) { return aContent.IsElement() &&
(!HTMLEditUtils::IsContainerNode(aContent) ||
aContent.IsAnyOfHTMLElements(
nsGkAtoms::applet, nsGkAtoms::colgroup, nsGkAtoms::frameset,
nsGkAtoms::head, nsGkAtoms::html, nsGkAtoms::iframe,
nsGkAtoms::meter, nsGkAtoms::progress, nsGkAtoms::select,
nsGkAtoms::textarea));
}
/** * IsNonEditableReplacedContent() returns true when aContent is an inclusive * descendant of a replaced element whose content shouldn't be editable by * user's operation.
*/ staticbool IsNonEditableReplacedContent(const nsIContent& aContent) { for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) { if (element->IsAnyOfHTMLElements(nsGkAtoms::select, nsGkAtoms::option,
nsGkAtoms::optgroup)) { returntrue;
}
} returnfalse;
}
/* * IsRemovalNode() returns true when parent of aContent is editable even * if aContent isn't editable. * This is a valid method to check it if you find the content from point * of view of siblings or parents of aContent. * Note that padding `<br>` element for empty editor and manual native * anonymous content should be deletable even after `HTMLEditor` is destroyed * because they are owned/managed by `HTMLEditor`.
*/ staticbool IsRemovableNode(const nsIContent& aContent) { return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) ||
aContent.IsRootOfNativeAnonymousSubtree() ||
(aContent.GetParentNode() &&
aContent.GetParentNode()->IsEditable() &&
&aContent != aContent.OwnerDoc()->GetBody() &&
&aContent != aContent.OwnerDoc()->GetDocumentElement());
}
/** * IsRemovableFromParentNode() returns true when aContent is editable, has a * parent node and the parent node is also editable. * This is a valid method to check it if you find the content from point * of view of descendants of aContent. * Note that padding `<br>` element for empty editor and manual native * anonymous content should be deletable even after `HTMLEditor` is destroyed * because they are owned/managed by `HTMLEditor`.
*/ staticbool IsRemovableFromParentNode(const nsIContent& aContent) { return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) ||
aContent.IsRootOfNativeAnonymousSubtree() ||
(aContent.IsEditable() && aContent.GetParentNode() &&
aContent.GetParentNode()->IsEditable() &&
&aContent != aContent.OwnerDoc()->GetBody() &&
&aContent != aContent.OwnerDoc()->GetDocumentElement());
}
/** * CanContentsBeJoined() returns true if aLeftContent and aRightContent can be * joined.
*/ staticbool CanContentsBeJoined(const nsIContent& aLeftContent, const nsIContent& aRightContent);
/** * Returns true if aContent is an element and it should be treated as a block. * * @param aBlockInlineCheck * - If UseHTMLDefaultStyle, this returns true only for HTML elements which * are defined as a block by the default style. I.e., non-HTML elements are * always treated as inline. * - If UseComputedDisplayOutsideStyle, this returns true for element nodes * whose display-outside is not inline nor ruby. This is useful to get * inclusive ancestor block element. * - If UseComputedDisplayStyle, this returns true for element nodes whose * display-outside is not inline or whose display-inside is flow-root and they * do not appear as a form control. This is useful to check whether * collapsible white-spaces at the element edges are visible or invisible or * whether <br> element at end of the element is visible or invisible.
*/
[[nodiscard]] staticbool IsBlockElement(const nsIContent& aContent,
BlockInlineCheck aBlockInlineCheck);
/** * This is designed to check elements or non-element nodes which are layed out * as inline. Therefore, inline-block etc and ruby are treated as inline. * Note that invisible non-element nodes like comment nodes are also treated * as inline. * * @param aBlockInlineCheck UseComputedDisplayOutsideStyle and * UseComputedDisplayStyle return same result for * any elements.
*/
[[nodiscard]] staticbool IsInlineContent(const nsIContent& aContent,
BlockInlineCheck aBlockInlineCheck);
/** * IsVisibleElementEvenIfLeafNode() returns true if aContent is an empty block * element, a visible replaced element such as a form control. This does not * check the layout information.
*/ staticbool IsVisibleElementEvenIfLeafNode(const nsIContent& aContent);
staticbool IsInlineStyle(nsINode* aNode);
/** * IsDisplayOutsideInline() returns true if display-outside value is * "inside". This does NOT flush the layout.
*/
[[nodiscard]] staticbool IsDisplayOutsideInline(const Element& aElement);
/** * IsDisplayInsideFlowRoot() returns true if display-inline value of aElement * is "flow-root". This does NOT flush the layout.
*/
[[nodiscard]] staticbool IsDisplayInsideFlowRoot(const Element& aElement);
/** * Return true if aElement is a flex item or a grid item. This works only * when aElement has a primary frame.
*/
[[nodiscard]] staticbool IsFlexOrGridItem(const Element& aElement);
/** * IsRemovableInlineStyleElement() returns true if aElement is an inline * element and can be removed or split to in order to modifying inline * styles.
*/ staticbool IsRemovableInlineStyleElement(Element& aElement);
/** * Return true if aContent is a format element of * Document.execCommand("formatBlock").
*/
[[nodiscard]] staticbool IsFormatElementForFormatBlockCommand( const nsIContent& aContent) { if (!aContent.IsHTMLElement() ||
!aContent.NodeInfo()->NameAtom()->IsStatic()) { returnfalse;
} const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic(); return IsFormatTagForFormatBlockCommand(*tagName);
}
/** * Return true if aTagName is one of the format element name of * cmd_paragraphState.
*/
[[nodiscard]] staticbool IsFormatTagForParagraphStateCommand( const nsStaticAtom& aTagName) { return // clang-format off
&aTagName == nsGkAtoms::address ||
&aTagName == nsGkAtoms::dd ||
&aTagName == nsGkAtoms::dl ||
&aTagName == nsGkAtoms::dt ||
&aTagName == nsGkAtoms::h1 ||
&aTagName == nsGkAtoms::h2 ||
&aTagName == nsGkAtoms::h3 ||
&aTagName == nsGkAtoms::h4 ||
&aTagName == nsGkAtoms::h5 ||
&aTagName == nsGkAtoms::h6 ||
&aTagName == nsGkAtoms::p ||
&aTagName == nsGkAtoms::pre; // clang-format on
}
/** * Return true if aContent is a format element of cmd_paragraphState.
*/
[[nodiscard]] staticbool IsFormatElementForParagraphStateCommand( const nsIContent& aContent) { if (!aContent.IsHTMLElement() ||
!aContent.NodeInfo()->NameAtom()->IsStatic()) { returnfalse;
} const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic(); return IsFormatTagForParagraphStateCommand(*tagName);
}
/** * CanElementContainParagraph() returns true if aElement can have a <p> * element as its child or its descendant.
*/ staticbool CanElementContainParagraph(const Element& aElement) { if (HTMLEditUtils::CanNodeContain(aElement, *nsGkAtoms::p)) { returntrue;
}
// Even if the element cannot have a <p> element as a child, it can contain // <p> element as a descendant if it's one of the following elements. if (aElement.IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul,
nsGkAtoms::dl, nsGkAtoms::table,
nsGkAtoms::thead, nsGkAtoms::tbody,
nsGkAtoms::tfoot, nsGkAtoms::tr)) { returntrue;
}
// XXX Otherwise, Chromium checks the CSS box is a block, but we don't do it // for now. returnfalse;
}
/** * Return a point which can insert a node whose name is aTagName scanning * from aPoint to its ancestor points.
*/ template <typename EditorDOMPointType> static EditorDOMPoint GetInsertionPointInInclusiveAncestor( const nsAtom& aTagName, const EditorDOMPointType& aPoint, const Element* aAncestorLimit = nullptr) { if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) { return EditorDOMPoint();
}
Element* lastChild = nullptr; for (Element* containerElement :
aPoint.template ContainerAs<nsIContent>()
->template InclusiveAncestorsOfType<Element>()) { if (!HTMLEditUtils::IsSimplyEditableNode(*containerElement)) { return EditorDOMPoint();
} if (HTMLEditUtils::CanNodeContain(*containerElement, aTagName)) { return lastChild ? EditorDOMPoint(lastChild)
: aPoint.template To<EditorDOMPoint>();
} if (containerElement == aAncestorLimit) { return EditorDOMPoint();
}
lastChild = containerElement;
} return EditorDOMPoint();
}
/** * IsContainerNode() returns true if aContent is a container node.
*/ staticbool IsContainerNode(const nsIContent& aContent) {
nsHTMLTag tagEnum; // XXX Should this handle #cdata-section too? if (aContent.IsText()) {
tagEnum = eHTMLTag_text;
} else { // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some // difference?
tagEnum = nsHTMLTags::StringTagToId(aContent.NodeName());
} return HTMLEditUtils::IsContainerNode(tagEnum);
}
/** * IsSplittableNode() returns true if aContent can split.
*/ staticbool IsSplittableNode(const nsIContent& aContent) { if (!EditorUtils::IsEditableContent(aContent,
EditorUtils::EditorType::HTML) ||
!HTMLEditUtils::IsRemovableFromParentNode(aContent)) { returnfalse;
} if (aContent.IsElement()) { // XXX Perhaps, instead of using container, we should have "splittable" // information in the DB. E.g., `<template>`, `<script>` elements // can have children, but shouldn't be split. return HTMLEditUtils::IsContainerNode(aContent) &&
!aContent.IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::button,
nsGkAtoms::caption, nsGkAtoms::table,
nsGkAtoms::tbody, nsGkAtoms::tfoot,
nsGkAtoms::thead, nsGkAtoms::tr) &&
!HTMLEditUtils::IsNeverElementContentsEditableByUser(aContent) &&
!HTMLEditUtils::IsNonEditableReplacedContent(aContent);
} return aContent.IsText() && aContent.Length() > 0;
}
/** * Return true if aText has only a linefeed and it's preformatted.
*/
[[nodiscard]] staticbool TextHasOnlyOnePreformattedLinefeed( const Text& aText) { return aText.TextDataLength() == 1u &&
aText.TextFragment().CharAt(0u) == kNewLine &&
EditorUtils::IsNewLinePreformatted(aText);
}
/** * IsVisibleTextNode() returns true if aText has visible text. If it has * only white-spaces and they are collapsed, returns false.
*/
[[nodiscard]] staticbool IsVisibleTextNode(const Text& aText);
/** * IsInVisibleTextFrames() returns true if any text in aText is in visible * text frames. Callers have to guarantee that there is no pending reflow.
*/ staticbool IsInVisibleTextFrames(nsPresContext* aPresContext, const Text& aText);
/** * IsVisibleBRElement() and IsInvisibleBRElement() return true if aContent is * a visible HTML <br> element, i.e., not a padding <br> element for making * last line in a block element visible, or an invisible <br> element.
*/ staticbool IsVisibleBRElement(const nsIContent& aContent) { if (const dom::HTMLBRElement* brElement =
dom::HTMLBRElement::FromNode(&aContent)) { return IsVisibleBRElement(*brElement);
} returnfalse;
} staticbool IsVisibleBRElement(const dom::HTMLBRElement& aBRElement) { // If followed by a block boundary without visible content, it's invisible // <br> element. return !HTMLEditUtils::GetElementOfImmediateBlockBoundary(
aBRElement, WalkTreeDirection::Forward);
} staticbool IsInvisibleBRElement(const nsIContent& aContent) { if (const dom::HTMLBRElement* brElement =
dom::HTMLBRElement::FromNode(&aContent)) { return IsInvisibleBRElement(*brElement);
} returnfalse;
} staticbool IsInvisibleBRElement(const dom::HTMLBRElement& aBRElement) { return !HTMLEditUtils::IsVisibleBRElement(aBRElement);
}
enumclass IgnoreInvisibleLineBreak { No, Yes };
/** * Return true if aPoint is immediately before current block boundary. If * aIgnoreInvisibleLineBreak is "Yes", this returns true if aPoint is before * invisible line break before a block boundary.
*/ template <typename PT, typename CT>
[[nodiscard]] staticbool PointIsImmediatelyBeforeCurrentBlockBoundary( const EditorDOMPointBase<PT, CT>& aPoint,
IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak);
/** * Return true if aRange crosses the inclusive ancestor block element at * start boundary, in other words, if aRange ends outside of the inclusive * ancestor block of the start boundary.
*/ template <typename EditorDOMPointType>
[[nodiscard]] staticbool RangeIsAcrossStartBlockBoundary( const EditorDOMRangeBase<EditorDOMPointType>& aRange) {
MOZ_ASSERT(aRange.IsPositionedAndValid()); if (MOZ_UNLIKELY(!aRange.StartRef().IsInContentNode())) { returnfalse;
} const Element* const startBlockElement =
HTMLEditUtils::GetInclusiveAncestorElement(
*aRange.StartRef().template ContainerAs<nsIContent>(),
ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayOutsideStyle); if (MOZ_UNLIKELY(!startBlockElement)) { returnfalse;
} return EditorRawDOMPoint::After(*startBlockElement)
.EqualsOrIsBefore(aRange.EndRef());
}
/** * Return true if `display` of inclusive ancestor of aContent is `none`.
*/ staticbool IsInclusiveAncestorCSSDisplayNone(const nsIContent& aContent);
/** * IsVisiblePreformattedNewLine() and IsInvisiblePreformattedNewLine() return * true if the point is preformatted linefeed and it's visible or invisible. * If linefeed is immediately before a block boundary, it's invisible. * * @param aFollowingBlockElement [out] If the node is followed by a block * boundary, this is set to the element * creating the block boundary.
*/ template <typename EditorDOMPointType> staticbool IsVisiblePreformattedNewLine( const EditorDOMPointType& aPoint,
Element** aFollowingBlockElement = nullptr) { if (aFollowingBlockElement) {
*aFollowingBlockElement = nullptr;
} if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
!aPoint.IsCharPreformattedNewLine()) { returnfalse;
} // If there are some other characters in the text node, it's a visible // linefeed. if (!aPoint.IsAtLastContent()) { if (EditorUtils::IsWhiteSpacePreformatted(
*aPoint.template ContainerAs<Text>())) { returntrue;
} const nsTextFragment& textFragment =
aPoint.template ContainerAs<Text>()->TextFragment(); for (uint32_t offset = aPoint.Offset() + 1;
offset < textFragment.GetLength(); ++offset) {
char16_t ch = textFragment.CharAt(AssertedCast<int32_t>(offset)); if (nsCRT::IsAsciiSpace(ch) && ch != HTMLEditUtils::kNewLine) { continue; // ASCII white-space which is collapsed into the linefeed.
} returntrue; // There is a visible character after it.
}
} // If followed by a block boundary without visible content, it's invisible // linefeed.
Element* followingBlockElement =
HTMLEditUtils::GetElementOfImmediateBlockBoundary(
*aPoint.template ContainerAs<Text>(), WalkTreeDirection::Forward); if (aFollowingBlockElement) {
*aFollowingBlockElement = followingBlockElement;
} return !followingBlockElement;
} template <typename EditorDOMPointType> staticbool IsInvisiblePreformattedNewLine( const EditorDOMPointType& aPoint,
Element** aFollowingBlockElement = nullptr) { if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() ||
!aPoint.IsCharPreformattedNewLine()) { if (aFollowingBlockElement) {
*aFollowingBlockElement = nullptr;
} returnfalse;
} return !IsVisiblePreformattedNewLine(aPoint, aFollowingBlockElement);
}
/** * Return a point to insert a padding line break if aPoint is following a * collapsible ASCII white-space or a block boundary and the line containing * aPoint requires a following padding line break which there is not.
*/ template <typename PT, typename CT> static EditorDOMPoint LineRequiresPaddingLineBreakToBeVisible( const EditorDOMPointBase<PT, CT>& aPoint, const Element& aEditingHost);
/** * ShouldInsertLinefeedCharacter() returns true if the caller should insert * a linefeed character instead of <br> element.
*/ staticbool ShouldInsertLinefeedCharacter( const EditorDOMPoint& aPointToInsert, const Element& aEditingHost);
/** * IsEmptyNode() returns false if aNode has some visible content nodes, * list elements or table elements. * * @param aPresContext Must not be nullptr if * EmptyCheckOption::SafeToAskLayout is set. * @param aNode The node to check whether it's empty. * @param aOptions You can specify which type of elements are visible * and/or whether this can access layout information. * @param aSeenBR [Out] Set to true if this meets an <br> element * before meeting visible things.
*/ enumclass EmptyCheckOption {
TreatSingleBRElementAsVisible,
TreatBlockAsVisible,
TreatListItemAsVisible,
TreatTableCellAsVisible,
TreatNonEditableContentAsInvisible,
SafeToAskLayout,
}; using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>; staticbool IsEmptyNode(nsPresContext* aPresContext, const nsINode& aNode, const EmptyCheckOptions& aOptions = {}, bool* aSeenBR = nullptr); staticbool IsEmptyNode(const nsINode& aNode, const EmptyCheckOptions& aOptions = {}, bool* aSeenBR = nullptr) {
MOZ_ASSERT(!aOptions.contains(EmptyCheckOption::SafeToAskLayout)); return IsEmptyNode(nullptr, aNode, aOptions, aSeenBR);
}
/** * IsEmptyInlineContainer() returns true if aContent is an inline element * which can have children and does not have meaningful content.
*/ staticbool IsEmptyInlineContainer(const nsIContent& aContent, const EmptyCheckOptions& aOptions,
BlockInlineCheck aBlockInlineCheck) { return HTMLEditUtils::IsInlineContent(aContent, aBlockInlineCheck) &&
HTMLEditUtils::IsContainerNode(aContent) &&
HTMLEditUtils::IsEmptyNode(aContent, aOptions);
}
/** * IsEmptyBlockElement() returns true if aElement is a block level element * and it doesn't have any visible content.
*/ staticbool IsEmptyBlockElement(const Element& aElement, const EmptyCheckOptions& aOptions,
BlockInlineCheck aBlockInlineCheck) { return HTMLEditUtils::IsBlockElement(aElement, aBlockInlineCheck) &&
HTMLEditUtils::IsEmptyNode(aElement, aOptions);
}
/** * Return true if aListElement is completely empty or it has only one list * item element which is empty.
*/
[[nodiscard]] staticbool IsEmptyAnyListElement(const Element& aListElement) {
MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement)); bool foundListItem = false; for (nsIContent* child = aListElement.GetFirstChild(); child;
child = child->GetNextSibling()) { if (HTMLEditUtils::IsListItem(child)) { if (foundListItem) { returnfalse; // 2 list items found.
} if (!IsEmptyNode(*child, {})) { returnfalse; // found non-empty list item.
}
foundListItem = true; continue;
} if (child->IsElement()) { returnfalse; // found sublist or illegal child.
} if (child->IsText() &&
HTMLEditUtils::IsVisibleTextNode(*child->AsText())) { returnfalse; // found illegal visible text node.
}
} returntrue;
}
/** * Return true if aListElement does not have invalid child.
*/ enumclass TreatSubListElementAs { Invalid, Valid };
[[nodiscard]] staticbool IsValidListElement( const Element& aListElement,
TreatSubListElementAs aTreatSubListElementAs) {
MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement)); for (nsIContent* child = aListElement.GetFirstChild(); child;
child = child->GetNextSibling()) { if (HTMLEditUtils::IsAnyListElement(child)) { if (aTreatSubListElementAs == TreatSubListElementAs::Invalid) { returnfalse;
} continue;
} if (child->IsHTMLElement(nsGkAtoms::li)) { if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::ol,
nsGkAtoms::ul))) { returnfalse;
} continue;
} if (child->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd)) { if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::dl))) { returnfalse;
} continue;
} if (MOZ_UNLIKELY(child->IsElement())) { returnfalse;
} if (MOZ_LIKELY(child->IsText())) { if (MOZ_UNLIKELY(HTMLEditUtils::IsVisibleTextNode(*child->AsText()))) { returnfalse;
}
}
} returntrue;
}
/** * IsEmptyOneHardLine() returns true if aArrayOfContents does not represent * 2 or more lines and have meaningful content.
*/ staticbool IsEmptyOneHardLine(
nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
BlockInlineCheck aBlockInlineCheck) { if (NS_WARN_IF(aArrayOfContents.IsEmpty())) { returntrue;
}
bool brElementHasFound = false; for (OwningNonNull<nsIContent>& content : aArrayOfContents) { if (!EditorUtils::IsEditableContent(content,
EditorUtils::EditorType::HTML)) { continue;
} if (content->IsHTMLElement(nsGkAtoms::br)) { // If there are 2 or more `<br>` elements, it's not empty line since // there may be only one `<br>` element in a hard line. if (brElementHasFound) { returnfalse;
}
brElementHasFound = true; continue;
} if (!HTMLEditUtils::IsEmptyInlineContainer(
content,
{EmptyCheckOption::TreatSingleBRElementAsVisible,
EmptyCheckOption::TreatNonEditableContentAsInvisible},
aBlockInlineCheck)) { returnfalse;
}
} returntrue;
}
/** * IsPointAtEdgeOfLink() returns true if aPoint is at start or end of a * link.
*/ template <typename PT, typename CT> staticbool IsPointAtEdgeOfLink(const EditorDOMPointBase<PT, CT>& aPoint,
Element** aFoundLinkElement = nullptr) { if (aFoundLinkElement) {
*aFoundLinkElement = nullptr;
} if (!aPoint.IsInContentNode()) { returnfalse;
} if (!aPoint.IsStartOfContainer() && !aPoint.IsEndOfContainer()) { returnfalse;
} // XXX Assuming it's not in an empty text node because it's unrealistic edge // case. bool maybeStartOfAnchor = aPoint.IsStartOfContainer(); for (EditorRawDOMPoint point(aPoint.template ContainerAs<nsIContent>());
point.IsSet() && (maybeStartOfAnchor ? point.IsStartOfContainer()
: point.IsAtLastContent());
point = point.ParentPoint()) { if (HTMLEditUtils::IsLink(point.GetContainer())) { // Now, we're at start or end of <a href>. if (aFoundLinkElement) {
*aFoundLinkElement =
do_AddRef(point.template ContainerAs<Element>()).take();
} returntrue;
}
} returnfalse;
}
/** * IsContentInclusiveDescendantOfLink() returns true if aContent is a * descendant of a link element. * Note that this returns true even if editing host of aContent is in a link * element.
*/ staticbool IsContentInclusiveDescendantOfLink(
nsIContent& aContent, Element** aFoundLinkElement = nullptr) { if (aFoundLinkElement) {
*aFoundLinkElement = nullptr;
} for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) { if (HTMLEditUtils::IsLink(element)) { if (aFoundLinkElement) {
*aFoundLinkElement = do_AddRef(element).take();
} returntrue;
}
} returnfalse;
}
/** * IsRangeEntirelyInLink() returns true if aRange is entirely in a link * element. * Note that this returns true even if editing host of the range is in a link * element.
*/ template <typename EditorDOMRangeType> staticbool IsRangeEntirelyInLink(const EditorDOMRangeType& aRange,
Element** aFoundLinkElement = nullptr) {
MOZ_ASSERT(aRange.IsPositionedAndValid()); if (aFoundLinkElement) {
*aFoundLinkElement = nullptr;
}
nsINode* commonAncestorNode =
nsContentUtils::GetClosestCommonInclusiveAncestor(
aRange.StartRef().GetContainer(), aRange.EndRef().GetContainer()); if (NS_WARN_IF(!commonAncestorNode) || !commonAncestorNode->IsContent()) { returnfalse;
} return IsContentInclusiveDescendantOfLink(*commonAncestorNode->AsContent(),
aFoundLinkElement);
}
/** * Get adjacent content node of aNode if there is (even if one is in different * parent element). * * @param aNode The node from which we start to walk the DOM * tree. * @param aOptions See WalkTreeOption for the detail. * @param aBlockInlineCheck Whether considering block vs. inline with the * computed style or the HTML default style. * @param aAncestorLimiter Ancestor limiter element which these methods * never cross its boundary. This is typically * the editing host.
*/ enumclass WalkTreeOption {
IgnoreNonEditableNode, // Ignore non-editable nodes and their children.
IgnoreDataNodeExceptText, // Ignore data nodes which are not text node.
IgnoreWhiteSpaceOnlyText, // Ignore text nodes having only white-spaces.
StopAtBlockBoundary, // Stop waking the tree at a block boundary.
}; using WalkTreeOptions = EnumSet<WalkTreeOption>; static nsIContent* GetPreviousContent( const nsINode& aNode, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr) { if (&aNode == aAncestorLimiter ||
(aAncestorLimiter &&
!aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { return nullptr;
} return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Backward,
aOptions, aBlockInlineCheck,
aAncestorLimiter);
} static nsIContent* GetNextContent(const nsINode& aNode, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr) { if (&aNode == aAncestorLimiter ||
(aAncestorLimiter &&
!aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { return nullptr;
} return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Forward,
aOptions, aBlockInlineCheck,
aAncestorLimiter);
}
/** * And another version that takes a point in DOM tree rather than a node.
*/ template <typename PT, typename CT> static nsIContent* GetPreviousContent( const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr);
/** * And another version that takes a point in DOM tree rather than a node. * * Note that this may return the child at the offset. E.g., following code * causes infinite loop. * * EditorRawDOMPoint point(aEditableNode); * while (nsIContent* content = * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode})) { * // Do something... * point.Set(content); * } * * Following code must be you expected: * * while (nsIContent* content = * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) { * // Do something... * DebugOnly<bool> advanced = point.Advanced(); * MOZ_ASSERT(advanced); * point.Set(point.GetChild()); * }
*/ template <typename PT, typename CT> static nsIContent* GetNextContent(const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr);
/** * GetPreviousSibling() return the preceding sibling of aContent which matches * with aOption. * * @param aBlockInlineCheck Can be Unused if aOptions does not contain * StopAtBlockBoundary.
*/ static nsIContent* GetPreviousSibling( const nsIContent& aContent, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { for (nsIContent* sibling = aContent.GetPreviousSibling(); sibling;
sibling = sibling->GetPreviousSibling()) { if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) { continue;
} if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) { return nullptr;
} return sibling;
} return nullptr;
}
/** * GetNextSibling() return the following sibling of aContent which matches * with aOption. * * @param aBlockInlineCheck Can be Unused if aOptions does not contain * StopAtBlockBoundary.
*/ static nsIContent* GetNextSibling( const nsIContent& aContent, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { for (nsIContent* sibling = aContent.GetNextSibling(); sibling;
sibling = sibling->GetNextSibling()) { if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) { continue;
} if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) { return nullptr;
} return sibling;
} return nullptr;
}
/** * Return the last child of aNode which matches with aOption. * * @param aBlockInlineCheck Can be unused if aOptions does not contain * StopAtBlockBoundary.
*/ static nsIContent* GetLastChild( const nsINode& aNode, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { for (nsIContent* child = aNode.GetLastChild(); child;
child = child->GetPreviousSibling()) { if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) { continue;
} if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) { return nullptr;
} return child;
} return nullptr;
}
/** * Return the first child of aNode which matches with aOption. * * @param aBlockInlineCheck Can be unused if aOptions does not contain * StopAtBlockBoundary.
*/ static nsIContent* GetFirstChild( const nsINode& aNode, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { for (nsIContent* child = aNode.GetFirstChild(); child;
child = child->GetNextSibling()) { if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) { continue;
} if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) { return nullptr;
} return child;
} return nullptr;
}
/** * Return true if aContent is the last child of aNode with ignoring all * children which do not match with aOption. * * @param aBlockInlineCheck Can be unused if aOptions does not contain * StopAtBlockBoundary.
*/ staticbool IsLastChild( const nsIContent& aContent, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
nsINode* parentNode = aContent.GetParentNode(); if (!parentNode) { returnfalse;
} return HTMLEditUtils::GetLastChild(*parentNode, aOptions,
aBlockInlineCheck) == &aContent;
}
/** * Return true if aContent is the first child of aNode with ignoring all * children which do not match with aOption. * * @param aBlockInlineCheck Can be unused if aOptions does not contain * StopAtBlockBoundary.
*/ staticbool IsFirstChild( const nsIContent& aContent, const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) {
nsINode* parentNode = aContent.GetParentNode(); if (!parentNode) { returnfalse;
} return HTMLEditUtils::GetFirstChild(*parentNode, aOptions,
aBlockInlineCheck) == &aContent;
}
/** * GetAdjacentContentToPutCaret() walks the DOM tree to find an editable node * near aPoint where may be a good point to put caret and keep typing or * deleting. * * @param aPoint The DOM point where to start to search from. * @return If found, returns non-nullptr. Otherwise, nullptr. * Note that if found node is in different table structure * element, this returns nullptr.
*/ enumclass WalkTreeDirection { Forward, Backward }; template <typename PT, typename CT> static nsIContent* GetAdjacentContentToPutCaret( const EditorDOMPointBase<PT, CT>& aPoint,
WalkTreeDirection aWalkTreeDirection, const Element& aEditingHost) {
MOZ_ASSERT(aPoint.IsSetAndValid());
nsIContent* editableContent = nullptr; if (aWalkTreeDirection == WalkTreeDirection::Backward) {
editableContent = HTMLEditUtils::GetPreviousContent(
aPoint, {WalkTreeOption::IgnoreNonEditableNode},
BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost); if (!editableContent) { return nullptr; // Not illegal.
}
} else {
editableContent = HTMLEditUtils::GetNextContent(
aPoint, {WalkTreeOption::IgnoreNonEditableNode},
BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost); if (NS_WARN_IF(!editableContent)) { // Perhaps, illegal because the node pointed by aPoint isn't editable // and nobody of previous nodes is editable. return nullptr;
}
}
// scan in the right direction until we find an eligible text node, // but don't cross any breaks, images, or table elements. // XXX This comment sounds odd. editableContent may have already crossed // breaks and/or images if they are non-editable. while (editableContent && !editableContent->IsText() &&
!editableContent->IsHTMLElement(nsGkAtoms::br) &&
!HTMLEditUtils::IsImage(editableContent)) { if (aWalkTreeDirection == WalkTreeDirection::Backward) {
editableContent = HTMLEditUtils::GetPreviousContent(
*editableContent, {WalkTreeOption::IgnoreNonEditableNode},
BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost); if (NS_WARN_IF(!editableContent)) { return nullptr;
}
} else {
editableContent = HTMLEditUtils::GetNextContent(
*editableContent, {WalkTreeOption::IgnoreNonEditableNode},
BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost); if (NS_WARN_IF(!editableContent)) { return nullptr;
}
}
}
// don't cross any table elements if ((!aPoint.IsInContentNode() &&
!!HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
*editableContent)) ||
(HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*editableContent) !=
HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
*aPoint.template ContainerAs<nsIContent>()))) { return nullptr;
}
// otherwise, ok, we have found a good spot to put the selection return editableContent;
}
enumclass LeafNodeType { // Even if there is a child block, keep scanning a leaf content in it.
OnlyLeafNode, // If there is a child block, return it too. Note that this does not // mean that block siblings are not treated as leaf nodes.
LeafNodeOrChildBlock, // If there is a non-editable element if and only if scanning from editable // node, return it too.
LeafNodeOrNonEditableNode, // Ignore non-editable content at walking the tree.
OnlyEditableLeafNode,
}; using LeafNodeTypes = EnumSet<LeafNodeType>;
/** * GetLastLeafContent() returns rightmost leaf content in aNode. It depends * on aLeafNodeTypes whether this which types of nodes are treated as leaf * nodes. * * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain * LeafNodeOrCHildBlock.
*/ static nsIContent* GetLastLeafContent( const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused, const Element* aAncestorLimiter = nullptr) {
MOZ_ASSERT_IF(
aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
!aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); // editor shouldn't touch child nodes which are replaced with native // anonymous nodes. if (aNode.IsElement() &&
HTMLEditUtils::IsNeverElementContentsEditableByUser(
*aNode.AsElement())) { return nullptr;
} for (nsIContent* content = aNode.GetLastChild(); content;) { if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) &&
!EditorUtils::IsEditableContent(*content,
EditorUtils::EditorType::HTML)) {
content = HTMLEditUtils::GetPreviousContent(
*content, {WalkTreeOption::IgnoreNonEditableNode},
aBlockInlineCheck, aAncestorLimiter); continue;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
HTMLEditUtils::IsBlockElement(*content, aBlockInlineCheck)) { return content;
} if (!content->HasChildren() ||
HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) { return content;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!HTMLEditUtils::IsSimplyEditableNode(*content)) { return content;
}
content = content->GetLastChild();
} return nullptr;
}
/** * GetFirstLeafContent() returns leftmost leaf content in aNode. It depends * on aLeafNodeTypes whether this scans into a block child or treat block as a * leaf. * * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain * LeafNodeOrCHildBlock.
*/ static nsIContent* GetFirstLeafContent( const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes,
BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused, const Element* aAncestorLimiter = nullptr) {
MOZ_ASSERT_IF(
aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
!aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); // editor shouldn't touch child nodes which are replaced with native // anonymous nodes. if (aNode.IsElement() &&
HTMLEditUtils::IsNeverElementContentsEditableByUser(
*aNode.AsElement())) { return nullptr;
} for (nsIContent* content = aNode.GetFirstChild(); content;) { if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) &&
!EditorUtils::IsEditableContent(*content,
EditorUtils::EditorType::HTML)) {
content = HTMLEditUtils::GetNextContent(
*content, {WalkTreeOption::IgnoreNonEditableNode},
aBlockInlineCheck, aAncestorLimiter); continue;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
HTMLEditUtils::IsBlockElement(*content, aBlockInlineCheck)) { return content;
} if (!content->HasChildren() ||
HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) { return content;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!HTMLEditUtils::IsSimplyEditableNode(*content)) { return content;
}
content = content->GetFirstChild();
} return nullptr;
}
/** * GetNextLeafContentOrNextBlockElement() returns next leaf content or * next block element of aStartContent inside aAncestorLimiter. * * @param aStartContent The start content to scan next content. * @param aLeafNodeTypes See LeafNodeType. * @param aAncestorLimiter Optional, if you set this, it must be an * inclusive ancestor of aStartContent.
*/ static nsIContent* GetNextLeafContentOrNextBlockElement( const nsIContent& aStartContent, const LeafNodeTypes& aLeafNodeTypes,
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr) {
MOZ_ASSERT_IF(
aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
!aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
if (&aStartContent == aAncestorLimiter) { return nullptr;
}
nsIContent* nextContent = aStartContent.GetNextSibling(); if (!nextContent) { if (!aStartContent.GetParentElement()) {
NS_WARNING("Reached orphan node while climbing up the DOM tree"); return nullptr;
} for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) { if (parentElement == aAncestorLimiter ||
HTMLEditUtils::IsBlockElement(*parentElement, aBlockInlineCheck)) { return nullptr;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!parentElement->IsEditable()) { return nullptr;
}
nextContent = parentElement->GetNextSibling(); if (nextContent) { break;
} if (!parentElement->GetParentElement()) {
NS_WARNING("Reached orphan node while climbing up the DOM tree"); return nullptr;
}
}
MOZ_ASSERT(nextContent);
aBlockInlineCheck = IgnoreInsideBlockBoundary(aBlockInlineCheck);
}
// We have a next content. If it's a block, return it. if (HTMLEditUtils::IsBlockElement(*nextContent, aBlockInlineCheck)) { return nextContent;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!nextContent->IsEditable()) { return nextContent;
} if (HTMLEditUtils::IsContainerNode(*nextContent)) { // Else if it's a container, get deep leftmost child if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
*nextContent, aLeafNodeTypes, aBlockInlineCheck)) { return child;
}
} // Else return the next content itself. return nextContent;
}
/** * Similar to the above method, but take a DOM point to specify scan start * point.
*/ template <typename PT, typename CT> static nsIContent* GetNextLeafContentOrNextBlockElement( const EditorDOMPointBase<PT, CT>& aStartPoint, const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr) {
MOZ_ASSERT(aStartPoint.IsSet());
MOZ_ASSERT_IF(
aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
!aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), "Not implemented yet");
if (!aStartPoint.IsInContentNode()) { return nullptr;
} if (aStartPoint.IsInTextNode()) { return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*aStartPoint.template ContainerAs<Text>(), aLeafNodeTypes,
aBlockInlineCheck, aAncestorLimiter);
} if (!HTMLEditUtils::IsContainerNode(
*aStartPoint.template ContainerAs<nsIContent>())) { return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes,
aBlockInlineCheck, aAncestorLimiter);
}
nsCOMPtr<nsIContent> nextContent = aStartPoint.GetChild(); if (!nextContent) { if (aStartPoint.GetContainer() == aAncestorLimiter ||
HTMLEditUtils::IsBlockElement(
*aStartPoint.template ContainerAs<nsIContent>(),
aBlockInlineCheck)) { // We are at end of the block. return nullptr;
}
// We are at end of non-block container return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
*aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes,
IgnoreInsideBlockBoundary(aBlockInlineCheck), aAncestorLimiter);
}
// We have a next node. If it's a block, return it. if (HTMLEditUtils::IsBlockElement(*nextContent, aBlockInlineCheck)) { return nextContent;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { return nextContent;
} if (HTMLEditUtils::IsContainerNode(*nextContent)) { // else if it's a container, get deep leftmost child if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
*nextContent, aLeafNodeTypes,
IgnoreInsideBlockBoundary(aBlockInlineCheck))) { return child;
}
} // Else return the node itself return nextContent;
}
/** * GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf * content or previous block element of aStartContent inside * aAncestorLimiter. * * @param aStartContent The start content to scan previous content. * @param aLeafNodeTypes See LeafNodeType. * @param aAncestorLimiter Optional, if you set this, it must be an * inclusive ancestor of aStartContent.
*/ static nsIContent* GetPreviousLeafContentOrPreviousBlockElement( const nsIContent& aStartContent, const LeafNodeTypes& aLeafNodeTypes,
BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr) {
MOZ_ASSERT_IF(
aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
!aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), "Not implemented yet");
if (&aStartContent == aAncestorLimiter) { return nullptr;
}
nsIContent* previousContent = aStartContent.GetPreviousSibling(); if (!previousContent) { if (!aStartContent.GetParentElement()) {
NS_WARNING("Reached orphan node while climbing up the DOM tree"); return nullptr;
} for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) { if (parentElement == aAncestorLimiter ||
HTMLEditUtils::IsBlockElement(*parentElement, aBlockInlineCheck)) { return nullptr;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!parentElement->IsEditable()) { return nullptr;
}
previousContent = parentElement->GetPreviousSibling(); if (previousContent) { break;
} if (!parentElement->GetParentElement()) {
NS_WARNING("Reached orphan node while climbing up the DOM tree"); return nullptr;
}
}
MOZ_ASSERT(previousContent);
aBlockInlineCheck = IgnoreInsideBlockBoundary(aBlockInlineCheck);
}
// We have a next content. If it's a block, return it. if (HTMLEditUtils::IsBlockElement(*previousContent, aBlockInlineCheck)) { return previousContent;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { return previousContent;
} if (HTMLEditUtils::IsContainerNode(*previousContent)) { // Else if it's a container, get deep rightmost child if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
*previousContent, aLeafNodeTypes, aBlockInlineCheck)) { return child;
}
} // Else return the next content itself. return previousContent;
}
/** * Similar to the above method, but take a DOM point to specify scan start * point.
*/ template <typename PT, typename CT> static nsIContent* GetPreviousLeafContentOrPreviousBlockElement( const EditorDOMPointBase<PT, CT>& aStartPoint, const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter = nullptr) {
MOZ_ASSERT(aStartPoint.IsSet());
MOZ_ASSERT_IF(
aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
!aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), "Not implemented yet");
if (!aStartPoint.IsInContentNode()) { return nullptr;
} if (aStartPoint.IsInTextNode()) { return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*aStartPoint.template ContainerAs<Text>(), aLeafNodeTypes,
aBlockInlineCheck, aAncestorLimiter);
} if (!HTMLEditUtils::IsContainerNode(
*aStartPoint.template ContainerAs<nsIContent>())) { return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes,
aBlockInlineCheck, aAncestorLimiter);
}
if (aStartPoint.IsStartOfContainer()) { if (aStartPoint.GetContainer() == aAncestorLimiter ||
HTMLEditUtils::IsBlockElement(
*aStartPoint.template ContainerAs<nsIContent>(),
aBlockInlineCheck)) { // We are at start of the block. return nullptr;
}
// We are at start of non-block container return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes,
IgnoreInsideBlockBoundary(aBlockInlineCheck), aAncestorLimiter);
}
nsCOMPtr<nsIContent> previousContent =
aStartPoint.GetPreviousSiblingOfChild(); if (NS_WARN_IF(!previousContent)) { return nullptr;
}
// We have a prior node. If it's a block, return it. if (HTMLEditUtils::IsBlockElement(*previousContent, aBlockInlineCheck)) { return previousContent;
} if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { return previousContent;
} if (HTMLEditUtils::IsContainerNode(*previousContent)) { // Else if it's a container, get deep rightmost child if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
*previousContent, aLeafNodeTypes,
IgnoreInsideBlockBoundary(aBlockInlineCheck))) { return child;
}
} // Else return the node itself return previousContent;
}
/** * Returns a content node whose inline styles should be preserved after * deleting content in a range. Typically, you should set aPoint to start * boundary of the range to delete.
*/ template <typename EditorDOMPointType> static nsIContent* GetContentToPreserveInlineStyles( const EditorDOMPointType& aPoint, const Element& aEditingHost);
/** * Get previous/next editable point from start or end of aContent.
*/ enumclass InvisibleWhiteSpaces {
Ignore, // Ignore invisible white-spaces, i.e., don't return middle of // them.
Preserve, // Preserve invisible white-spaces, i.e., result may be start or // end of a text node even if it begins or ends with invisible // white-spaces.
}; enumclass TableBoundary {
Ignore, // May cross any table element boundary.
NoCrossTableElement, // Won't cross `<table>` element boundary.
NoCrossAnyTableElement, // Won't cross any table element boundary.
}; template <typename EditorDOMPointType> static EditorDOMPointType GetPreviousEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary); template <typename EditorDOMPointType> static EditorDOMPointType GetNextEditablePoint(
nsIContent& aContent, const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
/** * GetAncestorElement() and GetInclusiveAncestorElement() return * (inclusive) block ancestor element of aContent whose time matches * aAncestorTypes.
*/ enumclass AncestorType { // If there is an ancestor block, it's a limiter of the scan.
ClosestBlockElement, // If there is no ancestor block in the range, the topmost inline element is // a limiter of the scan.
MostDistantInlineElementInBlock, // Ignore ancestor <hr> elements to check whether a block.
IgnoreHRElement, // If there is an ancestor <button> element, it's also a limiter of the // scan.
ButtonElement, // The root element of the scan start node or the ancestor limiter may be // return if there is no proper element.
AllowRootOrAncestorLimiterElement,
/** * GetClosestAncestorTableElement() returns the nearest inclusive ancestor * <table> element of aContent.
*/ static Element* GetClosestAncestorTableElement(const nsIContent& aContent) { // TODO: the method name and its documentation clash with the // implementation. Split this method into // `GetClosestAncestorTableElement` and // `GetClosestInclusiveAncestorTableElement`. if (!aContent.GetParent()) { return nullptr;
}
--> --------------------
--> maximum size reached
--> --------------------
¤ Dauer der Verarbeitung: 0.64 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.