1224 lines
44 KiB
C++
1224 lines
44 KiB
C++
/*
|
|
* Copyright (C) 2004-2007, 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "Editing.h"
|
|
|
|
#include "AXObjectCache.h"
|
|
#include "CachedImage.h"
|
|
#include "Document.h"
|
|
#include "Editor.h"
|
|
#include "Frame.h"
|
|
#include "HTMLBodyElement.h"
|
|
#include "HTMLDListElement.h"
|
|
#include "HTMLDivElement.h"
|
|
#include "HTMLElementFactory.h"
|
|
#include "HTMLImageElement.h"
|
|
#include "HTMLInterchange.h"
|
|
#include "HTMLLIElement.h"
|
|
#include "HTMLNames.h"
|
|
#include "HTMLOListElement.h"
|
|
#include "HTMLParagraphElement.h"
|
|
#include "HTMLSpanElement.h"
|
|
#include "HTMLTableElement.h"
|
|
#include "HTMLTextFormControlElement.h"
|
|
#include "HTMLUListElement.h"
|
|
#include "NodeTraversal.h"
|
|
#include "PositionIterator.h"
|
|
#include "Range.h"
|
|
#include "RenderBlock.h"
|
|
#include "RenderElement.h"
|
|
#include "RenderTableCell.h"
|
|
#include "RenderTextControlSingleLine.h"
|
|
#include "ShadowRoot.h"
|
|
#include "Text.h"
|
|
#include "TextControlInnerElements.h"
|
|
#include "TextIterator.h"
|
|
#include "VisibleUnits.h"
|
|
#include <wtf/Assertions.h>
|
|
#include <wtf/StdLibExtras.h>
|
|
#include <wtf/text/StringBuilder.h>
|
|
#include <wtf/unicode/CharacterNames.h>
|
|
|
|
namespace WebCore {
|
|
|
|
using namespace HTMLNames;
|
|
|
|
static bool isVisiblyAdjacent(const Position&, const Position&);
|
|
|
|
bool canHaveChildrenForEditing(const Node& node)
|
|
{
|
|
return !is<Text>(node) && node.canContainRangeEndPoint();
|
|
}
|
|
|
|
// Atomic means that the node has no children, or has children which are ignored for the purposes of editing.
|
|
bool isAtomicNode(const Node* node)
|
|
{
|
|
return node && (!node->hasChildNodes() || editingIgnoresContent(*node));
|
|
}
|
|
|
|
ContainerNode* highestEditableRoot(const Position& position, EditableType editableType)
|
|
{
|
|
ContainerNode* highestEditableRoot = editableRootForPosition(position, editableType);
|
|
if (!highestEditableRoot)
|
|
return nullptr;
|
|
|
|
for (ContainerNode* node = highestEditableRoot; !is<HTMLBodyElement>(*node); ) {
|
|
node = node->parentNode();
|
|
if (!node)
|
|
break;
|
|
// FIXME: Can this ever be a Document or DocumentFragment? If not, this should return Element* instead.
|
|
if (hasEditableStyle(*node, editableType))
|
|
highestEditableRoot = node;
|
|
}
|
|
|
|
return highestEditableRoot;
|
|
}
|
|
|
|
Element* lowestEditableAncestor(Node* node)
|
|
{
|
|
for (; node; node = node->parentNode()) {
|
|
if (node->hasEditableStyle())
|
|
return node->rootEditableElement();
|
|
if (is<HTMLBodyElement>(*node))
|
|
break;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static bool isEditableToAccessibility(const Node& node)
|
|
{
|
|
ASSERT(AXObjectCache::accessibilityEnabled());
|
|
ASSERT(node.document().existingAXObjectCache());
|
|
|
|
if (auto* cache = node.document().existingAXObjectCache())
|
|
return cache->rootAXEditableElement(&node);
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool computeEditability(const Node& node, EditableType editableType, Node::ShouldUpdateStyle shouldUpdateStyle)
|
|
{
|
|
if (node.computeEditability(Node::UserSelectAllIsAlwaysNonEditable, shouldUpdateStyle) != Node::Editability::ReadOnly)
|
|
return true;
|
|
|
|
switch (editableType) {
|
|
case ContentIsEditable:
|
|
return false;
|
|
case HasEditableAXRole:
|
|
return isEditableToAccessibility(node);
|
|
}
|
|
ASSERT_NOT_REACHED();
|
|
return false;
|
|
}
|
|
|
|
bool hasEditableStyle(const Node& node, EditableType editableType)
|
|
{
|
|
return computeEditability(node, editableType, Node::ShouldUpdateStyle::DoNotUpdate);
|
|
}
|
|
|
|
bool isEditableNode(const Node& node)
|
|
{
|
|
return computeEditability(node, ContentIsEditable, Node::ShouldUpdateStyle::Update);
|
|
}
|
|
|
|
bool isEditablePosition(const Position& position, EditableType editableType)
|
|
{
|
|
Node* node = position.containerNode();
|
|
return node && computeEditability(*node, editableType, Node::ShouldUpdateStyle::Update);
|
|
}
|
|
|
|
bool isAtUnsplittableElement(const Position& position)
|
|
{
|
|
Node* node = position.containerNode();
|
|
return node == editableRootForPosition(position) || node == enclosingNodeOfType(position, isTableCell);
|
|
}
|
|
|
|
bool isRichlyEditablePosition(const Position& position)
|
|
{
|
|
auto* node = position.containerNode();
|
|
return node && node->hasRichlyEditableStyle();
|
|
}
|
|
|
|
Element* editableRootForPosition(const Position& position, EditableType editableType)
|
|
{
|
|
Node* node = position.containerNode();
|
|
if (!node)
|
|
return nullptr;
|
|
|
|
switch (editableType) {
|
|
case HasEditableAXRole:
|
|
if (auto* cache = node->document().existingAXObjectCache())
|
|
return const_cast<Element*>(cache->rootAXEditableElement(node));
|
|
FALLTHROUGH;
|
|
case ContentIsEditable:
|
|
return node->rootEditableElement();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Finds the enclosing element until which the tree can be split.
|
|
// When a user hits ENTER, he/she won't expect this element to be split into two.
|
|
// You may pass it as the second argument of splitTreeToNode.
|
|
Element* unsplittableElementForPosition(const Position& position)
|
|
{
|
|
// Since enclosingNodeOfType won't search beyond the highest root editable node,
|
|
// this code works even if the closest table cell was outside of the root editable node.
|
|
if (auto* enclosingCell = downcast<Element>(enclosingNodeOfType(position, &isTableCell)))
|
|
return enclosingCell;
|
|
return editableRootForPosition(position);
|
|
}
|
|
|
|
Position nextCandidate(const Position& position)
|
|
{
|
|
for (PositionIterator nextPosition = position; !nextPosition.atEnd(); ) {
|
|
nextPosition.increment();
|
|
if (nextPosition.isCandidate())
|
|
return nextPosition;
|
|
}
|
|
return { };
|
|
}
|
|
|
|
Position nextVisuallyDistinctCandidate(const Position& position)
|
|
{
|
|
// FIXME: Use PositionIterator instead.
|
|
Position nextPosition = position;
|
|
Position downstreamStart = nextPosition.downstream();
|
|
while (!nextPosition.atEndOfTree()) {
|
|
nextPosition = nextPosition.next(Character);
|
|
if (nextPosition.isCandidate() && nextPosition.downstream() != downstreamStart)
|
|
return nextPosition;
|
|
if (auto* node = nextPosition.containerNode()) {
|
|
if (!node->renderer())
|
|
nextPosition = lastPositionInOrAfterNode(node);
|
|
}
|
|
}
|
|
return { };
|
|
}
|
|
|
|
Position previousCandidate(const Position& position)
|
|
{
|
|
PositionIterator previousPosition = position;
|
|
while (!previousPosition.atStart()) {
|
|
previousPosition.decrement();
|
|
if (previousPosition.isCandidate())
|
|
return previousPosition;
|
|
}
|
|
return { };
|
|
}
|
|
|
|
Position previousVisuallyDistinctCandidate(const Position& position)
|
|
{
|
|
// FIXME: Use PositionIterator instead.
|
|
Position previousPosition = position;
|
|
Position downstreamStart = previousPosition.downstream();
|
|
while (!previousPosition.atStartOfTree()) {
|
|
previousPosition = previousPosition.previous(Character);
|
|
if (previousPosition.isCandidate() && previousPosition.downstream() != downstreamStart)
|
|
return previousPosition;
|
|
if (auto* node = previousPosition.containerNode()) {
|
|
if (!node->renderer())
|
|
previousPosition = firstPositionInOrBeforeNode(node);
|
|
}
|
|
}
|
|
return { };
|
|
}
|
|
|
|
Position firstEditablePositionAfterPositionInRoot(const Position& position, ContainerNode* highestRoot)
|
|
{
|
|
if (!highestRoot)
|
|
return { };
|
|
|
|
// position falls before highestRoot.
|
|
if (position < firstPositionInNode(highestRoot) && highestRoot->hasEditableStyle())
|
|
return firstPositionInNode(highestRoot);
|
|
|
|
Position candidate = position;
|
|
|
|
if (&position.deprecatedNode()->treeScope() != &highestRoot->treeScope()) {
|
|
auto* shadowAncestor = highestRoot->treeScope().ancestorNodeInThisScope(position.deprecatedNode());
|
|
if (!shadowAncestor)
|
|
return { };
|
|
|
|
candidate = positionAfterNode(shadowAncestor);
|
|
}
|
|
|
|
while (candidate.deprecatedNode() && !isEditablePosition(candidate) && candidate.deprecatedNode()->isDescendantOf(*highestRoot))
|
|
candidate = isAtomicNode(candidate.deprecatedNode()) ? positionInParentAfterNode(candidate.deprecatedNode()) : nextVisuallyDistinctCandidate(candidate);
|
|
|
|
if (candidate.deprecatedNode() && candidate.deprecatedNode() != highestRoot && !candidate.deprecatedNode()->isDescendantOf(*highestRoot))
|
|
return { };
|
|
|
|
return candidate;
|
|
}
|
|
|
|
Position lastEditablePositionBeforePositionInRoot(const Position& position, ContainerNode* highestRoot)
|
|
{
|
|
if (!highestRoot)
|
|
return { };
|
|
|
|
// When position falls after highestRoot, the result is easy to compute.
|
|
if (position > lastPositionInNode(highestRoot))
|
|
return lastPositionInNode(highestRoot);
|
|
|
|
Position candidate = position;
|
|
|
|
if (&position.deprecatedNode()->treeScope() != &highestRoot->treeScope()) {
|
|
auto* shadowAncestor = highestRoot->treeScope().ancestorNodeInThisScope(position.deprecatedNode());
|
|
if (!shadowAncestor)
|
|
return { };
|
|
|
|
candidate = firstPositionInOrBeforeNode(shadowAncestor);
|
|
}
|
|
|
|
while (candidate.deprecatedNode() && !isEditablePosition(candidate) && candidate.deprecatedNode()->isDescendantOf(*highestRoot))
|
|
candidate = isAtomicNode(candidate.deprecatedNode()) ? positionInParentBeforeNode(candidate.deprecatedNode()) : previousVisuallyDistinctCandidate(candidate);
|
|
|
|
if (candidate.deprecatedNode() && candidate.deprecatedNode() != highestRoot && !candidate.deprecatedNode()->isDescendantOf(*highestRoot))
|
|
return { };
|
|
|
|
return candidate;
|
|
}
|
|
|
|
// FIXME: The function name, comment, and code say three different things here!
|
|
// Whether or not content before and after this node will collapse onto the same line as it.
|
|
bool isBlock(const Node* node)
|
|
{
|
|
return node && node->renderer() && !node->renderer()->isInline() && !node->renderer()->isRubyText();
|
|
}
|
|
|
|
bool isInline(const Node* node)
|
|
{
|
|
return node && node->renderer() && node->renderer()->isInline();
|
|
}
|
|
|
|
// FIXME: Deploy this in all of the places where enclosingBlockFlow/enclosingBlockFlowOrTableElement are used.
|
|
// FIXME: Pass a position to this function. The enclosing block of [table, x] for example, should be the
|
|
// block that contains the table and not the table, and this function should be the only one responsible for
|
|
// knowing about these kinds of special cases.
|
|
Element* enclosingBlock(Node* node, EditingBoundaryCrossingRule rule)
|
|
{
|
|
Node* enclosingNode = enclosingNodeOfType(firstPositionInOrBeforeNode(node), isBlock, rule);
|
|
return is<Element>(enclosingNode) ? downcast<Element>(enclosingNode) : nullptr;
|
|
}
|
|
|
|
TextDirection directionOfEnclosingBlock(const Position& position)
|
|
{
|
|
auto block = enclosingBlock(position.containerNode());
|
|
if (!block)
|
|
return TextDirection::LTR;
|
|
auto renderer = block->renderer();
|
|
if (!renderer)
|
|
return TextDirection::LTR;
|
|
return renderer->style().direction();
|
|
}
|
|
|
|
// This method is used to create positions in the DOM. It returns the maximum valid offset
|
|
// in a node. It returns 1 for some elements even though they do not have children, which
|
|
// creates technically invalid DOM Positions. Be sure to call parentAnchoredEquivalent
|
|
// on a Position before using it to create a DOM Range, or an exception will be thrown.
|
|
int lastOffsetForEditing(const Node& node)
|
|
{
|
|
if (node.isCharacterDataNode() || node.hasChildNodes())
|
|
return node.length();
|
|
|
|
// FIXME: Might be more helpful to return 1 for any node where editingIgnoresContent is true, even one that happens to have child nodes, like a select element with option node children.
|
|
return editingIgnoresContent(node) ? 1 : 0;
|
|
}
|
|
|
|
bool isAmbiguousBoundaryCharacter(UChar character)
|
|
{
|
|
// These are characters that can behave as word boundaries, but can appear within words.
|
|
// If they are just typed, i.e. if they are immediately followed by a caret, we want to delay text checking until the next character has been typed.
|
|
// FIXME: this is required until <rdar://problem/6853027> is fixed and text checking can do this for us.
|
|
return character == '\'' || character == '@' || character == rightSingleQuotationMark || character == hebrewPunctuationGershayim;
|
|
}
|
|
|
|
String stringWithRebalancedWhitespace(const String& string, bool startIsStartOfParagraph, bool endIsEndOfParagraph)
|
|
{
|
|
StringBuilder rebalancedString;
|
|
|
|
bool previousCharacterWasSpace = false;
|
|
unsigned length = string.length();
|
|
for (unsigned i = 0; i < length; ++i) {
|
|
auto character = string[i];
|
|
if (!deprecatedIsEditingWhitespace(character)) {
|
|
previousCharacterWasSpace = false;
|
|
continue;
|
|
}
|
|
LChar selectedWhitespaceCharacter;
|
|
if (previousCharacterWasSpace || (!i && startIsStartOfParagraph) || (i == length - 1 && endIsEndOfParagraph)) {
|
|
selectedWhitespaceCharacter = noBreakSpace;
|
|
previousCharacterWasSpace = false;
|
|
} else {
|
|
selectedWhitespaceCharacter = ' ';
|
|
previousCharacterWasSpace = true;
|
|
}
|
|
if (character == selectedWhitespaceCharacter)
|
|
continue;
|
|
rebalancedString.reserveCapacity(length);
|
|
rebalancedString.appendSubstring(string, rebalancedString.length(), i - rebalancedString.length());
|
|
rebalancedString.append(selectedWhitespaceCharacter);
|
|
}
|
|
|
|
if (rebalancedString.isEmpty())
|
|
return string;
|
|
|
|
rebalancedString.reserveCapacity(length);
|
|
rebalancedString.appendSubstring(string, rebalancedString.length(), length - rebalancedString.length());
|
|
return rebalancedString.toString();
|
|
}
|
|
|
|
bool isTableStructureNode(const Node* node)
|
|
{
|
|
auto* renderer = node->renderer();
|
|
return renderer && (renderer->isTableCell() || renderer->isTableRow() || renderer->isTableSection() || renderer->isRenderTableCol());
|
|
}
|
|
|
|
const String& nonBreakingSpaceString()
|
|
{
|
|
static NeverDestroyed<String> nonBreakingSpaceString(&noBreakSpace, 1);
|
|
return nonBreakingSpaceString;
|
|
}
|
|
|
|
Element* isFirstPositionAfterTable(const VisiblePosition& position)
|
|
{
|
|
Position upstream(position.deepEquivalent().upstream());
|
|
auto* node = upstream.deprecatedNode();
|
|
if (!node)
|
|
return nullptr;
|
|
auto* renderer = node->renderer();
|
|
if (!renderer || !renderer->isTable() || !upstream.atLastEditingPositionForNode())
|
|
return nullptr;
|
|
return &downcast<Element>(*node);
|
|
}
|
|
|
|
Element* isLastPositionBeforeTable(const VisiblePosition& position)
|
|
{
|
|
Position downstream(position.deepEquivalent().downstream());
|
|
auto* node = downstream.deprecatedNode();
|
|
if (!node)
|
|
return nullptr;
|
|
auto* renderer = node->renderer();
|
|
if (!renderer || !renderer->isTable() || !downstream.atFirstEditingPositionForNode())
|
|
return nullptr;
|
|
return &downcast<Element>(*node);
|
|
}
|
|
|
|
// Returns the visible position at the beginning of a node
|
|
VisiblePosition visiblePositionBeforeNode(Node& node)
|
|
{
|
|
if (node.hasChildNodes())
|
|
return VisiblePosition(firstPositionInOrBeforeNode(&node));
|
|
ASSERT(node.parentNode());
|
|
ASSERT(!node.parentNode()->isShadowRoot());
|
|
return positionInParentBeforeNode(&node);
|
|
}
|
|
|
|
// Returns the visible position at the ending of a node
|
|
VisiblePosition visiblePositionAfterNode(Node& node)
|
|
{
|
|
if (node.hasChildNodes())
|
|
return VisiblePosition(lastPositionInOrAfterNode(&node));
|
|
ASSERT(node.parentNode());
|
|
ASSERT(!node.parentNode()->isShadowRoot());
|
|
return positionInParentAfterNode(&node);
|
|
}
|
|
|
|
VisiblePosition closestEditablePositionInElementForAbsolutePoint(const Element& element, const IntPoint& point)
|
|
{
|
|
if (!element.isConnected() || !element.document().frame())
|
|
return { };
|
|
|
|
Ref<const Element> protectedElement { element };
|
|
element.document().updateLayoutIgnorePendingStylesheets();
|
|
|
|
RenderObject* renderer = element.renderer();
|
|
// Look at the inner element of a form control, not the control itself, as it is the editable part.
|
|
if (is<HTMLTextFormControlElement>(element)) {
|
|
auto& formControlElement = downcast<HTMLTextFormControlElement>(element);
|
|
if (!formControlElement.isInnerTextElementEditable())
|
|
return { };
|
|
if (auto innerTextElement = formControlElement.innerTextElement())
|
|
renderer = innerTextElement->renderer();
|
|
}
|
|
if (!renderer)
|
|
return { };
|
|
auto absoluteBoundingBox = renderer->absoluteBoundingBoxRect();
|
|
auto constrainedAbsolutePoint = point.constrainedBetween(absoluteBoundingBox.minXMinYCorner(), absoluteBoundingBox.maxXMaxYCorner());
|
|
auto localPoint = renderer->absoluteToLocal(constrainedAbsolutePoint, UseTransforms);
|
|
auto visiblePosition = renderer->positionForPoint(flooredLayoutPoint(localPoint), nullptr);
|
|
return isEditablePosition(visiblePosition.deepEquivalent()) ? visiblePosition : VisiblePosition { };
|
|
}
|
|
|
|
bool isListHTMLElement(Node* node)
|
|
{
|
|
return node && (is<HTMLUListElement>(*node) || is<HTMLOListElement>(*node) || is<HTMLDListElement>(*node));
|
|
}
|
|
|
|
bool isListItem(const Node* node)
|
|
{
|
|
return node && (isListHTMLElement(node->parentNode()) || (node->renderer() && node->renderer()->isListItem()));
|
|
}
|
|
|
|
Element* enclosingElementWithTag(const Position& position, const QualifiedName& tagName)
|
|
{
|
|
auto* root = highestEditableRoot(position);
|
|
for (Node* node = position.deprecatedNode(); node; node = node->parentNode()) {
|
|
if (root && !node->hasEditableStyle())
|
|
continue;
|
|
if (!is<Element>(*node))
|
|
continue;
|
|
if (downcast<Element>(*node).hasTagName(tagName))
|
|
return &downcast<Element>(*node);
|
|
if (node == root)
|
|
return nullptr;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Node* enclosingNodeOfType(const Position& position, bool (*nodeIsOfType)(const Node*), EditingBoundaryCrossingRule rule)
|
|
{
|
|
// FIXME: support CanSkipCrossEditingBoundary
|
|
ASSERT(rule == CanCrossEditingBoundary || rule == CannotCrossEditingBoundary);
|
|
auto* root = rule == CannotCrossEditingBoundary ? highestEditableRoot(position) : nullptr;
|
|
for (Node* n = position.deprecatedNode(); n; n = n->parentNode()) {
|
|
// Don't return a non-editable node if the input position was editable, since
|
|
// the callers from editing will no doubt want to perform editing inside the returned node.
|
|
if (root && !n->hasEditableStyle())
|
|
continue;
|
|
if (nodeIsOfType(n))
|
|
return n;
|
|
if (n == root)
|
|
return nullptr;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Node* highestEnclosingNodeOfType(const Position& position, bool (*nodeIsOfType)(const Node*), EditingBoundaryCrossingRule rule, Node* stayWithin)
|
|
{
|
|
Node* highest = nullptr;
|
|
auto* root = rule == CannotCrossEditingBoundary ? highestEditableRoot(position) : nullptr;
|
|
for (Node* n = position.containerNode(); n && n != stayWithin; n = n->parentNode()) {
|
|
if (root && !n->hasEditableStyle())
|
|
continue;
|
|
if (nodeIsOfType(n))
|
|
highest = n;
|
|
if (n == root)
|
|
break;
|
|
}
|
|
return highest;
|
|
}
|
|
|
|
static bool hasARenderedDescendant(Node* node, Node* excludedNode)
|
|
{
|
|
for (Node* n = node->firstChild(); n;) {
|
|
if (n == excludedNode) {
|
|
n = NodeTraversal::nextSkippingChildren(*n, node);
|
|
continue;
|
|
}
|
|
if (n->renderer())
|
|
return true;
|
|
n = NodeTraversal::next(*n, node);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Node* highestNodeToRemoveInPruning(Node* node)
|
|
{
|
|
Node* previousNode = nullptr;
|
|
auto* rootEditableElement = node ? node->rootEditableElement() : nullptr;
|
|
for (auto currentNode = makeRefPtr(node); currentNode; currentNode = currentNode->parentNode()) {
|
|
if (auto* renderer = currentNode->renderer()) {
|
|
if (!renderer->canHaveChildren() || hasARenderedDescendant(currentNode.get(), previousNode) || rootEditableElement == currentNode.get())
|
|
return previousNode;
|
|
}
|
|
previousNode = currentNode.get();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Element* enclosingTableCell(const Position& position)
|
|
{
|
|
return downcast<Element>(enclosingNodeOfType(position, isTableCell));
|
|
}
|
|
|
|
Element* enclosingAnchorElement(const Position& p)
|
|
{
|
|
for (Node* node = p.deprecatedNode(); node; node = node->parentNode()) {
|
|
if (is<Element>(*node) && node->isLink())
|
|
return downcast<Element>(node);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
HTMLElement* enclosingList(Node* node)
|
|
{
|
|
if (!node)
|
|
return nullptr;
|
|
|
|
auto* root = highestEditableRoot(firstPositionInOrBeforeNode(node));
|
|
|
|
for (ContainerNode* ancestor = node->parentNode(); ancestor; ancestor = ancestor->parentNode()) {
|
|
if (is<HTMLUListElement>(*ancestor) || is<HTMLOListElement>(*ancestor))
|
|
return downcast<HTMLElement>(ancestor);
|
|
if (ancestor == root)
|
|
return nullptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Node* enclosingListChild(Node *node)
|
|
{
|
|
if (!node)
|
|
return nullptr;
|
|
|
|
// Check for a list item element, or for a node whose parent is a list element. Such a node
|
|
// will appear visually as a list item (but without a list marker)
|
|
auto* root = highestEditableRoot(firstPositionInOrBeforeNode(node));
|
|
|
|
// FIXME: This function is inappropriately named since it starts with node instead of node->parentNode()
|
|
for (Node* n = node; n && n->parentNode(); n = n->parentNode()) {
|
|
if (is<HTMLLIElement>(*n) || (isListHTMLElement(n->parentNode()) && n != root))
|
|
return n;
|
|
if (n == root || isTableCell(n))
|
|
return nullptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static HTMLElement* embeddedSublist(Node* listItem)
|
|
{
|
|
// Check the DOM so that we'll find collapsed sublists without renderers.
|
|
for (Node* n = listItem->firstChild(); n; n = n->nextSibling()) {
|
|
if (isListHTMLElement(n))
|
|
return downcast<HTMLElement>(n);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static Node* appendedSublist(Node* listItem)
|
|
{
|
|
// Check the DOM so that we'll find collapsed sublists without renderers.
|
|
for (Node* n = listItem->nextSibling(); n; n = n->nextSibling()) {
|
|
if (isListHTMLElement(n))
|
|
return downcast<HTMLElement>(n);
|
|
if (isListItem(listItem))
|
|
return nullptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// FIXME: This function should not need to call isStartOfParagraph/isEndOfParagraph.
|
|
Node* enclosingEmptyListItem(const VisiblePosition& position)
|
|
{
|
|
// Check that position is on a line by itself inside a list item
|
|
auto* listChildNode = enclosingListChild(position.deepEquivalent().deprecatedNode());
|
|
if (!listChildNode || !isStartOfParagraph(position) || !isEndOfParagraph(position))
|
|
return nullptr;
|
|
|
|
VisiblePosition firstInListChild(firstPositionInOrBeforeNode(listChildNode));
|
|
VisiblePosition lastInListChild(lastPositionInOrAfterNode(listChildNode));
|
|
|
|
if (firstInListChild != position || lastInListChild != position)
|
|
return nullptr;
|
|
|
|
if (embeddedSublist(listChildNode) || appendedSublist(listChildNode))
|
|
return nullptr;
|
|
|
|
return listChildNode;
|
|
}
|
|
|
|
HTMLElement* outermostEnclosingList(Node* node, Node* rootList)
|
|
{
|
|
auto* list = enclosingList(node);
|
|
if (!list)
|
|
return nullptr;
|
|
|
|
while (auto* nextList = enclosingList(list)) {
|
|
if (nextList == rootList)
|
|
break;
|
|
list = nextList;
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
bool canMergeLists(Element* firstList, Element* secondList)
|
|
{
|
|
if (!is<HTMLElement>(firstList) || !is<HTMLElement>(secondList))
|
|
return false;
|
|
|
|
auto& first = downcast<HTMLElement>(*firstList);
|
|
auto& second = downcast<HTMLElement>(*secondList);
|
|
|
|
return first.localName() == second.localName() // make sure the list types match (ol vs. ul)
|
|
&& first.hasEditableStyle() && second.hasEditableStyle() // both lists are editable
|
|
&& first.rootEditableElement() == second.rootEditableElement() // don't cross editing boundaries
|
|
// Make sure there is no visible content between this li and the previous list.
|
|
&& isVisiblyAdjacent(positionInParentAfterNode(&first), positionInParentBeforeNode(&second));
|
|
}
|
|
|
|
static Node* previousNodeConsideringAtomicNodes(const Node* node)
|
|
{
|
|
if (node->previousSibling()) {
|
|
Node* n = node->previousSibling();
|
|
while (!isAtomicNode(n) && n->lastChild())
|
|
n = n->lastChild();
|
|
return n;
|
|
}
|
|
if (node->parentNode())
|
|
return node->parentNode();
|
|
return nullptr;
|
|
}
|
|
|
|
static Node* nextNodeConsideringAtomicNodes(const Node* node)
|
|
{
|
|
if (!isAtomicNode(node) && node->firstChild())
|
|
return node->firstChild();
|
|
if (node->nextSibling())
|
|
return node->nextSibling();
|
|
const Node* n = node;
|
|
while (n && !n->nextSibling())
|
|
n = n->parentNode();
|
|
if (n)
|
|
return n->nextSibling();
|
|
return nullptr;
|
|
}
|
|
|
|
Node* previousLeafNode(const Node* node)
|
|
{
|
|
while ((node = previousNodeConsideringAtomicNodes(node))) {
|
|
if (isAtomicNode(node))
|
|
return const_cast<Node*>(node);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Node* nextLeafNode(const Node* node)
|
|
{
|
|
while ((node = nextNodeConsideringAtomicNodes(node))) {
|
|
if (isAtomicNode(node))
|
|
return const_cast<Node*>(node);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// FIXME: Do not require renderer, so that this can be used within fragments.
|
|
bool isRenderedTable(const Node* node)
|
|
{
|
|
if (!is<HTMLElement>(node))
|
|
return false;
|
|
auto* renderer = downcast<HTMLElement>(*node).renderer();
|
|
return renderer && renderer->isTable();
|
|
}
|
|
|
|
bool isTableCell(const Node* node)
|
|
{
|
|
auto* renderer = node->renderer();
|
|
if (!renderer)
|
|
return node->hasTagName(tdTag) || node->hasTagName(thTag);
|
|
return renderer->isTableCell();
|
|
}
|
|
|
|
bool isEmptyTableCell(const Node* node)
|
|
{
|
|
// Returns true IFF the passed in node is one of:
|
|
// .) a table cell with no children,
|
|
// .) a table cell with a single BR child, and which has no other child renderers, including :before and :after renderers
|
|
// .) the BR child of such a table cell
|
|
|
|
// Find rendered node
|
|
while (node && !node->renderer())
|
|
node = node->parentNode();
|
|
if (!node)
|
|
return false;
|
|
|
|
// Make sure the rendered node is a table cell or <br>.
|
|
// If it's a <br>, then the parent node has to be a table cell.
|
|
auto* renderer = node->renderer();
|
|
if (renderer->isBR()) {
|
|
renderer = renderer->parent();
|
|
if (!renderer)
|
|
return false;
|
|
}
|
|
if (!is<RenderTableCell>(*renderer))
|
|
return false;
|
|
|
|
// Check that the table cell contains no child renderers except for perhaps a single <br>.
|
|
auto* childRenderer = downcast<RenderTableCell>(*renderer).firstChild();
|
|
if (!childRenderer)
|
|
return true;
|
|
if (!childRenderer->isBR())
|
|
return false;
|
|
return !childRenderer->nextSibling();
|
|
}
|
|
|
|
Ref<HTMLElement> createDefaultParagraphElement(Document& document)
|
|
{
|
|
switch (document.editor().defaultParagraphSeparator()) {
|
|
case EditorParagraphSeparatorIsDiv:
|
|
return HTMLDivElement::create(document);
|
|
case EditorParagraphSeparatorIsP:
|
|
break;
|
|
}
|
|
return HTMLParagraphElement::create(document);
|
|
}
|
|
|
|
Ref<HTMLElement> createHTMLElement(Document& document, const QualifiedName& name)
|
|
{
|
|
return HTMLElementFactory::createElement(name, document);
|
|
}
|
|
|
|
Ref<HTMLElement> createHTMLElement(Document& document, const AtomString& tagName)
|
|
{
|
|
return createHTMLElement(document, QualifiedName(nullAtom(), tagName, xhtmlNamespaceURI));
|
|
}
|
|
|
|
bool isTabSpanNode(const Node* node)
|
|
{
|
|
return is<HTMLSpanElement>(node) && downcast<HTMLSpanElement>(*node).attributeWithoutSynchronization(classAttr) == AppleTabSpanClass;
|
|
}
|
|
|
|
bool isTabSpanTextNode(const Node* node)
|
|
{
|
|
return is<Text>(node) && isTabSpanNode(node->parentNode());
|
|
}
|
|
|
|
HTMLSpanElement* tabSpanNode(const Node* node)
|
|
{
|
|
return isTabSpanTextNode(node) ? downcast<HTMLSpanElement>(node->parentNode()) : nullptr;
|
|
}
|
|
|
|
static Ref<Element> createTabSpanElement(Document& document, Text& tabTextNode)
|
|
{
|
|
auto spanElement = HTMLSpanElement::create(document);
|
|
|
|
spanElement->setAttributeWithoutSynchronization(classAttr, AppleTabSpanClass);
|
|
spanElement->setAttribute(styleAttr, "white-space:pre");
|
|
|
|
spanElement->appendChild(tabTextNode);
|
|
|
|
return spanElement;
|
|
}
|
|
|
|
Ref<Element> createTabSpanElement(Document& document, const String& tabText)
|
|
{
|
|
return createTabSpanElement(document, document.createTextNode(tabText));
|
|
}
|
|
|
|
Ref<Element> createTabSpanElement(Document& document)
|
|
{
|
|
return createTabSpanElement(document, document.createEditingTextNode("\t"_s));
|
|
}
|
|
|
|
bool isNodeRendered(const Node& node)
|
|
{
|
|
auto* renderer = node.renderer();
|
|
return renderer && renderer->style().visibility() == Visibility::Visible;
|
|
}
|
|
|
|
unsigned numEnclosingMailBlockquotes(const Position& position)
|
|
{
|
|
unsigned count = 0;
|
|
for (Node* node = position.deprecatedNode(); node; node = node->parentNode()) {
|
|
if (isMailBlockquote(node))
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
void updatePositionForNodeRemoval(Position& position, Node& node)
|
|
{
|
|
if (position.isNull())
|
|
return;
|
|
switch (position.anchorType()) {
|
|
case Position::PositionIsBeforeChildren:
|
|
if (node.containsIncludingShadowDOM(position.containerNode()))
|
|
position = positionInParentBeforeNode(&node);
|
|
break;
|
|
case Position::PositionIsAfterChildren:
|
|
if (node.containsIncludingShadowDOM(position.containerNode()))
|
|
position = positionInParentBeforeNode(&node);
|
|
break;
|
|
case Position::PositionIsOffsetInAnchor:
|
|
if (position.containerNode() == node.parentNode() && static_cast<unsigned>(position.offsetInContainerNode()) > node.computeNodeIndex())
|
|
position.moveToOffset(position.offsetInContainerNode() - 1);
|
|
else if (node.containsIncludingShadowDOM(position.containerNode()))
|
|
position = positionInParentBeforeNode(&node);
|
|
break;
|
|
case Position::PositionIsAfterAnchor:
|
|
if (node.containsIncludingShadowDOM(position.anchorNode()))
|
|
position = positionInParentAfterNode(&node);
|
|
break;
|
|
case Position::PositionIsBeforeAnchor:
|
|
if (node.containsIncludingShadowDOM(position.anchorNode()))
|
|
position = positionInParentBeforeNode(&node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool isMailBlockquote(const Node* node)
|
|
{
|
|
ASSERT(node);
|
|
if (!node->hasTagName(blockquoteTag))
|
|
return false;
|
|
return downcast<HTMLElement>(*node).attributeWithoutSynchronization(typeAttr) == "cite";
|
|
}
|
|
|
|
int caretMinOffset(const Node& node)
|
|
{
|
|
auto* renderer = node.renderer();
|
|
ASSERT(!node.isCharacterDataNode() || !renderer || renderer->isText());
|
|
return renderer ? renderer->caretMinOffset() : 0;
|
|
}
|
|
|
|
// If a node can contain candidates for VisiblePositions, return the offset of the last candidate, otherwise
|
|
// return the number of children for container nodes and the length for unrendered text nodes.
|
|
int caretMaxOffset(const Node& node)
|
|
{
|
|
// For rendered text nodes, return the last position that a caret could occupy.
|
|
if (is<Text>(node)) {
|
|
if (auto* renderer = downcast<Text>(node).renderer())
|
|
return renderer->caretMaxOffset();
|
|
}
|
|
return lastOffsetForEditing(node);
|
|
}
|
|
|
|
bool lineBreakExistsAtVisiblePosition(const VisiblePosition& position)
|
|
{
|
|
return lineBreakExistsAtPosition(position.deepEquivalent().downstream());
|
|
}
|
|
|
|
bool lineBreakExistsAtPosition(const Position& position)
|
|
{
|
|
if (position.isNull())
|
|
return false;
|
|
|
|
if (position.anchorNode()->hasTagName(brTag) && position.atFirstEditingPositionForNode())
|
|
return true;
|
|
|
|
if (!position.anchorNode()->renderer())
|
|
return false;
|
|
|
|
if (!is<Text>(*position.anchorNode()) || !position.anchorNode()->renderer()->style().preserveNewline())
|
|
return false;
|
|
|
|
Text& textNode = downcast<Text>(*position.anchorNode());
|
|
unsigned offset = position.offsetInContainerNode();
|
|
return offset < textNode.length() && textNode.data()[offset] == '\n';
|
|
}
|
|
|
|
// Modifies selections that have an end point at the edge of a table
|
|
// that contains the other endpoint so that they don't confuse
|
|
// code that iterates over selected paragraphs.
|
|
VisibleSelection selectionForParagraphIteration(const VisibleSelection& original)
|
|
{
|
|
VisibleSelection newSelection(original);
|
|
VisiblePosition startOfSelection(newSelection.visibleStart());
|
|
VisiblePosition endOfSelection(newSelection.visibleEnd());
|
|
|
|
// If the end of the selection to modify is just after a table, and
|
|
// if the start of the selection is inside that table, then the last paragraph
|
|
// that we'll want modify is the last one inside the table, not the table itself
|
|
// (a table is itself a paragraph).
|
|
if (auto* table = isFirstPositionAfterTable(endOfSelection)) {
|
|
if (startOfSelection.deepEquivalent().deprecatedNode()->isDescendantOf(*table))
|
|
newSelection = VisibleSelection(startOfSelection, endOfSelection.previous(CannotCrossEditingBoundary));
|
|
}
|
|
|
|
// If the start of the selection to modify is just before a table,
|
|
// and if the end of the selection is inside that table, then the first paragraph
|
|
// we'll want to modify is the first one inside the table, not the paragraph
|
|
// containing the table itself.
|
|
if (auto* table = isLastPositionBeforeTable(startOfSelection)) {
|
|
if (endOfSelection.deepEquivalent().deprecatedNode()->isDescendantOf(*table))
|
|
newSelection = VisibleSelection(startOfSelection.next(CannotCrossEditingBoundary), endOfSelection);
|
|
}
|
|
|
|
return newSelection;
|
|
}
|
|
|
|
// FIXME: indexForVisiblePosition and visiblePositionForIndex use TextIterators to convert between
|
|
// VisiblePositions and indices. But TextIterator iteration using TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions
|
|
// does not exactly match VisiblePosition iteration, so using them to preserve a selection during an editing
|
|
// opertion is unreliable. TextIterator's TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions mode needs to be fixed,
|
|
// or these functions need to be changed to iterate using actual VisiblePositions.
|
|
// FIXME: Deploy these functions everywhere that TextIterators are used to convert between VisiblePositions and indices.
|
|
int indexForVisiblePosition(const VisiblePosition& visiblePosition, RefPtr<ContainerNode>& scope)
|
|
{
|
|
if (visiblePosition.isNull())
|
|
return 0;
|
|
|
|
auto position = visiblePosition.deepEquivalent();
|
|
auto& document = *position.document();
|
|
|
|
auto* editableRoot = highestEditableRoot(position, AXObjectCache::accessibilityEnabled() ? HasEditableAXRole : ContentIsEditable);
|
|
if (editableRoot && !document.inDesignMode())
|
|
scope = editableRoot;
|
|
else {
|
|
if (position.containerNode()->isInShadowTree())
|
|
scope = position.containerNode()->containingShadowRoot();
|
|
else
|
|
scope = &document;
|
|
}
|
|
|
|
auto range = *makeSimpleRange(makeBoundaryPointBeforeNodeContents(*scope), position);
|
|
return characterCount(range, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
|
|
}
|
|
|
|
// FIXME: Merge this function with the one above.
|
|
int indexForVisiblePosition(Node& node, const VisiblePosition& visiblePosition, bool forSelectionPreservation)
|
|
{
|
|
auto range = makeSimpleRange(makeBoundaryPointBeforeNodeContents(node), visiblePosition);
|
|
TextIteratorBehaviors behaviors;
|
|
if (forSelectionPreservation)
|
|
behaviors.add(TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
|
|
return range ? characterCount(*range, behaviors) : 0;
|
|
}
|
|
|
|
VisiblePosition visiblePositionForPositionWithOffset(const VisiblePosition& position, int offset)
|
|
{
|
|
RefPtr<ContainerNode> root;
|
|
unsigned startIndex = indexForVisiblePosition(position, root);
|
|
if (!root)
|
|
return { };
|
|
|
|
return visiblePositionForIndex(startIndex + offset, root.get());
|
|
}
|
|
|
|
VisiblePosition visiblePositionForIndex(int index, ContainerNode* scope)
|
|
{
|
|
if (!scope)
|
|
return { };
|
|
return { makeDeprecatedLegacyPosition(resolveCharacterLocation(makeRangeSelectingNodeContents(*scope), index, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions)) };
|
|
}
|
|
|
|
VisiblePosition visiblePositionForIndexUsingCharacterIterator(Node& node, int index)
|
|
{
|
|
if (index <= 0)
|
|
return { firstPositionInOrBeforeNode(&node) };
|
|
|
|
auto range = makeRangeSelectingNodeContents(node);
|
|
CharacterIterator it(range);
|
|
if (!it.atEnd())
|
|
it.advance(index - 1);
|
|
|
|
if (!it.atEnd() && it.text().length() == 1 && it.text()[0] == '\n') {
|
|
// FIXME: workaround for collapsed range (where only start position is correct) emitted for some emitted newlines.
|
|
it.advance(1);
|
|
if (!it.atEnd())
|
|
return { makeDeprecatedLegacyPosition(it.range().start) };
|
|
}
|
|
|
|
return { makeDeprecatedLegacyPosition((it.atEnd() ? range : it.range()).end), Affinity::Upstream };
|
|
}
|
|
|
|
// Determines whether two positions are visibly next to each other (first then second)
|
|
// while ignoring whitespaces and unrendered nodes
|
|
static bool isVisiblyAdjacent(const Position& first, const Position& second)
|
|
{
|
|
return VisiblePosition(first) == VisiblePosition(second.upstream());
|
|
}
|
|
|
|
// Determines whether a node is inside a range or visibly starts and ends at the boundaries of the range.
|
|
// Call this function to determine whether a node is visibly fit inside selectedRange
|
|
bool isNodeVisiblyContainedWithin(Node& node, const SimpleRange& range)
|
|
{
|
|
if (contains<ComposedTree>(range, node))
|
|
return true;
|
|
|
|
auto startPosition = makeDeprecatedLegacyPosition(range.start);
|
|
auto endPosition = makeDeprecatedLegacyPosition(range.end);
|
|
|
|
bool startIsVisuallySame = visiblePositionBeforeNode(node) == startPosition;
|
|
if (startIsVisuallySame && positionInParentAfterNode(&node) < endPosition)
|
|
return true;
|
|
|
|
bool endIsVisuallySame = visiblePositionAfterNode(node) == endPosition;
|
|
if (endIsVisuallySame && startPosition < positionInParentBeforeNode(&node))
|
|
return true;
|
|
|
|
return startIsVisuallySame && endIsVisuallySame;
|
|
}
|
|
|
|
bool isRenderedAsNonInlineTableImageOrHR(const Node* node)
|
|
{
|
|
if (!node)
|
|
return false;
|
|
RenderObject* renderer = node->renderer();
|
|
return renderer && ((renderer->isTable() && !renderer->isInline()) || (renderer->isImage() && !renderer->isInline()) || renderer->isHR());
|
|
}
|
|
|
|
bool areIdenticalElements(const Node& first, const Node& second)
|
|
{
|
|
if (!is<Element>(first) || !is<Element>(second))
|
|
return false;
|
|
auto& firstElement = downcast<Element>(first);
|
|
auto& secondElement = downcast<Element>(second);
|
|
return firstElement.hasTagName(secondElement.tagQName()) && firstElement.hasEquivalentAttributes(secondElement);
|
|
}
|
|
|
|
bool isNonTableCellHTMLBlockElement(const Node* node)
|
|
{
|
|
return node->hasTagName(listingTag)
|
|
|| node->hasTagName(olTag)
|
|
|| node->hasTagName(preTag)
|
|
|| is<HTMLTableElement>(*node)
|
|
|| node->hasTagName(ulTag)
|
|
|| node->hasTagName(xmpTag)
|
|
|| node->hasTagName(h1Tag)
|
|
|| node->hasTagName(h2Tag)
|
|
|| node->hasTagName(h3Tag)
|
|
|| node->hasTagName(h4Tag)
|
|
|| node->hasTagName(h5Tag);
|
|
}
|
|
|
|
Position adjustedSelectionStartForStyleComputation(const VisibleSelection& selection)
|
|
{
|
|
// This function is used by range style computations to avoid bugs like:
|
|
// <rdar://problem/4017641> REGRESSION (Mail): you can only bold/unbold a selection starting from end of line once
|
|
// It is important to skip certain irrelevant content at the start of the selection, so we do not wind up
|
|
// with a spurious "mixed" style.
|
|
|
|
auto visiblePosition = selection.visibleStart();
|
|
if (visiblePosition.isNull())
|
|
return { };
|
|
|
|
// if the selection is a caret, just return the position, since the style
|
|
// behind us is relevant
|
|
if (selection.isCaret())
|
|
return visiblePosition.deepEquivalent();
|
|
|
|
// if the selection starts just before a paragraph break, skip over it
|
|
if (isEndOfParagraph(visiblePosition))
|
|
return visiblePosition.next().deepEquivalent().downstream();
|
|
|
|
// otherwise, make sure to be at the start of the first selected node,
|
|
// instead of possibly at the end of the last node before the selection
|
|
return visiblePosition.deepEquivalent().downstream();
|
|
}
|
|
|
|
// FIXME: Should this be deprecated like deprecatedEnclosingBlockFlowElement is?
|
|
bool isBlockFlowElement(const Node& node)
|
|
{
|
|
if (!node.isElementNode())
|
|
return false;
|
|
auto* renderer = downcast<Element>(node).renderer();
|
|
return renderer && renderer->isRenderBlockFlow();
|
|
}
|
|
|
|
Element* deprecatedEnclosingBlockFlowElement(Node* node)
|
|
{
|
|
if (!node)
|
|
return nullptr;
|
|
if (isBlockFlowElement(*node))
|
|
return downcast<Element>(node);
|
|
while ((node = node->parentNode())) {
|
|
if (isBlockFlowElement(*node) || is<HTMLBodyElement>(*node))
|
|
return downcast<Element>(node);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static inline bool caretRendersInsideNode(const Node& node)
|
|
{
|
|
return !isRenderedTable(&node) && !editingIgnoresContent(node);
|
|
}
|
|
|
|
RenderBlock* rendererForCaretPainting(const Node* node)
|
|
{
|
|
if (!node)
|
|
return nullptr;
|
|
|
|
auto* renderer = node->renderer();
|
|
if (!renderer)
|
|
return nullptr;
|
|
|
|
// If caretNode is a block and caret is inside it, then caret should be painted by that block.
|
|
bool paintedByBlock = is<RenderBlockFlow>(*renderer) && caretRendersInsideNode(*node);
|
|
return paintedByBlock ? downcast<RenderBlock>(renderer) : renderer->containingBlock();
|
|
}
|
|
|
|
LayoutRect localCaretRectInRendererForCaretPainting(const VisiblePosition& caretPosition, RenderBlock*& caretPainter)
|
|
{
|
|
if (caretPosition.isNull())
|
|
return LayoutRect();
|
|
ASSERT(caretPosition.deepEquivalent().deprecatedNode()->renderer());
|
|
auto [localRect, renderer] = caretPosition.localCaretRect();
|
|
return localCaretRectInRendererForRect(localRect, caretPosition.deepEquivalent().deprecatedNode(), renderer, caretPainter);
|
|
}
|
|
|
|
LayoutRect localCaretRectInRendererForRect(LayoutRect& localRect, Node* node, RenderObject* renderer, RenderBlock*& caretPainter)
|
|
{
|
|
// Get the renderer that will be responsible for painting the caret
|
|
// (which is either the renderer we just found, or one of its containers).
|
|
caretPainter = rendererForCaretPainting(node);
|
|
|
|
// Compute an offset between the renderer and the caretPainter.
|
|
while (renderer != caretPainter) {
|
|
RenderElement* containerObject = renderer->container();
|
|
if (!containerObject)
|
|
return LayoutRect();
|
|
localRect.move(renderer->offsetFromContainer(*containerObject, localRect.location()));
|
|
renderer = containerObject;
|
|
}
|
|
|
|
return localRect;
|
|
}
|
|
|
|
IntRect absoluteBoundsForLocalCaretRect(RenderBlock* rendererForCaretPainting, const LayoutRect& rect, bool* insideFixed)
|
|
{
|
|
if (insideFixed)
|
|
*insideFixed = false;
|
|
|
|
if (!rendererForCaretPainting || rect.isEmpty())
|
|
return IntRect();
|
|
|
|
LayoutRect localRect(rect);
|
|
rendererForCaretPainting->flipForWritingMode(localRect);
|
|
return rendererForCaretPainting->localToAbsoluteQuad(FloatRect(localRect), UseTransforms, insideFixed).enclosingBoundingBox();
|
|
}
|
|
|
|
HashSet<RefPtr<HTMLImageElement>> visibleImageElementsInRangeWithNonLoadedImages(const SimpleRange& range)
|
|
{
|
|
HashSet<RefPtr<HTMLImageElement>> result;
|
|
for (TextIterator iterator(range); !iterator.atEnd(); iterator.advance()) {
|
|
if (!is<HTMLImageElement>(iterator.node()))
|
|
continue;
|
|
|
|
auto& imageElement = downcast<HTMLImageElement>(*iterator.node());
|
|
auto* cachedImage = imageElement.cachedImage();
|
|
if (cachedImage && cachedImage->isLoading())
|
|
result.add(&imageElement);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace WebCore
|