mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Remove all code relating to shadow trees, insertion points, shadow boundaries, traversing composed trees, distribution, template documents, custom elements, registering elements, element registries, element factories, shadow roots, etc. Remove the following features from the IDLs and from the binding generators: CustomElementCallbacks, Reflect*, EventHandler. Remove the CSS custom pseudo-element concept, since we no longer have a UA style sheet worth talking about, no longer have shadow trees or custom elements, no longer use pseudo-elements, and generally therefore don't use this code at all.
850 lines
30 KiB
C++
850 lines
30 KiB
C++
/*
|
|
* Copyright (C) 2004, 2005, 2006, 2007 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 COMPUTER, 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 COMPUTER, 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 "sky/engine/core/editing/htmlediting.h"
|
|
|
|
#include "sky/engine/bindings/exception_state.h"
|
|
#include "sky/engine/bindings/exception_state_placeholder.h"
|
|
#include "sky/engine/core/dom/Document.h"
|
|
#include "sky/engine/core/dom/ElementTraversal.h"
|
|
#include "sky/engine/core/dom/NodeTraversal.h"
|
|
#include "sky/engine/core/dom/PositionIterator.h"
|
|
#include "sky/engine/core/dom/Range.h"
|
|
#include "sky/engine/core/dom/Text.h"
|
|
#include "sky/engine/core/editing/PlainTextRange.h"
|
|
#include "sky/engine/core/editing/TextIterator.h"
|
|
#include "sky/engine/core/editing/VisiblePosition.h"
|
|
#include "sky/engine/core/editing/VisibleUnits.h"
|
|
#include "sky/engine/core/frame/LocalFrame.h"
|
|
#include "sky/engine/core/rendering/RenderObject.h"
|
|
#include "sky/engine/wtf/Assertions.h"
|
|
#include "sky/engine/wtf/StdLibExtras.h"
|
|
#include "sky/engine/wtf/text/StringBuilder.h"
|
|
|
|
namespace blink {
|
|
|
|
// 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->hasChildren() || editingIgnoresContent(node));
|
|
}
|
|
|
|
// Compare two positions. Only works for non-null values.
|
|
int comparePositions(const Position& a, const Position& b)
|
|
{
|
|
ASSERT(a.isNotNull());
|
|
ASSERT(b.isNotNull());
|
|
TreeScope* commonScope = commonTreeScope(a.containerNode(), b.containerNode());
|
|
|
|
ASSERT(commonScope);
|
|
if (!commonScope)
|
|
return 0;
|
|
|
|
Node* nodeA = commonScope->ancestorInThisScope(a.containerNode());
|
|
ASSERT(nodeA);
|
|
bool hasDescendentA = nodeA != a.containerNode();
|
|
int offsetA = hasDescendentA ? 0 : a.computeOffsetInContainerNode();
|
|
|
|
Node* nodeB = commonScope->ancestorInThisScope(b.containerNode());
|
|
ASSERT(nodeB);
|
|
bool hasDescendentB = nodeB != b.containerNode();
|
|
int offsetB = hasDescendentB ? 0 : b.computeOffsetInContainerNode();
|
|
|
|
int bias = 0;
|
|
if (nodeA == nodeB) {
|
|
if (hasDescendentA)
|
|
bias = -1;
|
|
else if (hasDescendentB)
|
|
bias = 1;
|
|
}
|
|
|
|
int result = Range::compareBoundaryPoints(nodeA, offsetA, nodeB, offsetB, IGNORE_EXCEPTION);
|
|
return result ? result : bias;
|
|
}
|
|
|
|
int comparePositions(const PositionWithAffinity& a, const PositionWithAffinity& b)
|
|
{
|
|
return comparePositions(a.position(), b.position());
|
|
}
|
|
|
|
int comparePositions(const VisiblePosition& a, const VisiblePosition& b)
|
|
{
|
|
return comparePositions(a.deepEquivalent(), b.deepEquivalent());
|
|
}
|
|
|
|
ContainerNode* highestEditableRoot(const Position& position, EditableType editableType)
|
|
{
|
|
if (position.isNull())
|
|
return 0;
|
|
|
|
ContainerNode* highestRoot = editableRootForPosition(position, editableType);
|
|
if (!highestRoot)
|
|
return 0;
|
|
|
|
ContainerNode* node = highestRoot->parentNode();
|
|
while (node) {
|
|
if (node->hasEditableStyle(editableType))
|
|
highestRoot = node;
|
|
node = node->parentNode();
|
|
}
|
|
|
|
return highestRoot;
|
|
}
|
|
|
|
Element* lowestEditableAncestor(Node* node)
|
|
{
|
|
while (node) {
|
|
if (node->hasEditableStyle())
|
|
return node->rootEditableElement();
|
|
node = node->parentNode();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool isEditablePosition(const Position& p, EditableType editableType, EUpdateStyle updateStyle)
|
|
{
|
|
Node* node = p.parentAnchoredEquivalent().anchorNode();
|
|
if (!node)
|
|
return false;
|
|
if (updateStyle == UpdateStyle)
|
|
node->document().updateLayout();
|
|
else
|
|
ASSERT(updateStyle == DoNotUpdateStyle);
|
|
|
|
return node->hasEditableStyle(editableType);
|
|
}
|
|
|
|
bool isAtUnsplittableElement(const Position& pos)
|
|
{
|
|
Node* node = pos.deprecatedNode();
|
|
return node == editableRootForPosition(pos);
|
|
}
|
|
|
|
|
|
bool isRichlyEditablePosition(const Position& p, EditableType editableType)
|
|
{
|
|
Node* node = p.deprecatedNode();
|
|
if (!node)
|
|
return false;
|
|
|
|
return node->rendererIsRichlyEditable(editableType);
|
|
}
|
|
|
|
Element* editableRootForPosition(const Position& p, EditableType editableType)
|
|
{
|
|
Node* node = p.containerNode();
|
|
if (!node)
|
|
return 0;
|
|
|
|
return node->rootEditableElement(editableType);
|
|
}
|
|
|
|
// 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& p)
|
|
{
|
|
return editableRootForPosition(p);
|
|
}
|
|
|
|
Position nextCandidate(const Position& position)
|
|
{
|
|
PositionIterator p = position;
|
|
while (!p.atEnd()) {
|
|
p.increment();
|
|
if (p.isCandidate())
|
|
return p;
|
|
}
|
|
return Position();
|
|
}
|
|
|
|
Position nextVisuallyDistinctCandidate(const Position& position)
|
|
{
|
|
Position p = position;
|
|
Position downstreamStart = p.downstream();
|
|
while (!p.atEndOfTree()) {
|
|
p = p.next(Character);
|
|
if (p.isCandidate() && p.downstream() != downstreamStart)
|
|
return p;
|
|
}
|
|
return Position();
|
|
}
|
|
|
|
Position previousCandidate(const Position& position)
|
|
{
|
|
PositionIterator p = position;
|
|
while (!p.atStart()) {
|
|
p.decrement();
|
|
if (p.isCandidate())
|
|
return p;
|
|
}
|
|
return Position();
|
|
}
|
|
|
|
Position previousVisuallyDistinctCandidate(const Position& position)
|
|
{
|
|
Position p = position;
|
|
Position downstreamStart = p.downstream();
|
|
while (!p.atStartOfTree()) {
|
|
p = p.previous(Character);
|
|
if (p.isCandidate() && p.downstream() != downstreamStart)
|
|
return p;
|
|
}
|
|
return Position();
|
|
}
|
|
|
|
VisiblePosition firstEditableVisiblePositionAfterPositionInRoot(const Position& position, ContainerNode* highestRoot)
|
|
{
|
|
// position falls before highestRoot.
|
|
if (comparePositions(position, firstPositionInNode(highestRoot)) == -1 && highestRoot->hasEditableStyle())
|
|
return VisiblePosition(firstPositionInNode(highestRoot));
|
|
|
|
Position editablePosition = position;
|
|
|
|
// TODO(ianh): not really sure what's going on here
|
|
if (position.deprecatedNode()->treeScope() != highestRoot->treeScope())
|
|
editablePosition = positionAfterNode(editablePosition.deprecatedNode());
|
|
|
|
while (editablePosition.deprecatedNode() && !isEditablePosition(editablePosition) && editablePosition.deprecatedNode()->isDescendantOf(highestRoot))
|
|
editablePosition = isAtomicNode(editablePosition.deprecatedNode()) ? positionInParentAfterNode(*editablePosition.deprecatedNode()) : nextVisuallyDistinctCandidate(editablePosition);
|
|
|
|
if (editablePosition.deprecatedNode() && editablePosition.deprecatedNode() != highestRoot && !editablePosition.deprecatedNode()->isDescendantOf(highestRoot))
|
|
return VisiblePosition();
|
|
|
|
return VisiblePosition(editablePosition);
|
|
}
|
|
|
|
VisiblePosition lastEditableVisiblePositionBeforePositionInRoot(const Position& position, ContainerNode* highestRoot)
|
|
{
|
|
return VisiblePosition(lastEditablePositionBeforePositionInRoot(position, highestRoot));
|
|
}
|
|
|
|
Position lastEditablePositionBeforePositionInRoot(const Position& position, Node* highestRoot)
|
|
{
|
|
// When position falls after highestRoot, the result is easy to compute.
|
|
if (comparePositions(position, lastPositionInNode(highestRoot)) == 1)
|
|
return lastPositionInNode(highestRoot);
|
|
|
|
Position editablePosition = position;
|
|
|
|
// TODO(ianh): not really sure what's going on here
|
|
if (position.deprecatedNode()->treeScope() != highestRoot->treeScope())
|
|
editablePosition = firstPositionInOrBeforeNode(editablePosition.deprecatedNode());
|
|
|
|
while (editablePosition.deprecatedNode() && !isEditablePosition(editablePosition) && editablePosition.deprecatedNode()->isDescendantOf(highestRoot))
|
|
editablePosition = isAtomicNode(editablePosition.deprecatedNode()) ? positionInParentBeforeNode(*editablePosition.deprecatedNode()) : previousVisuallyDistinctCandidate(editablePosition);
|
|
|
|
if (editablePosition.deprecatedNode() && editablePosition.deprecatedNode() != highestRoot && !editablePosition.deprecatedNode()->isDescendantOf(highestRoot))
|
|
return Position();
|
|
return editablePosition;
|
|
}
|
|
|
|
// FIXME: The method 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();
|
|
}
|
|
|
|
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 enclosingNode && enclosingNode->isElementNode() ? toElement(enclosingNode) : 0;
|
|
}
|
|
|
|
Element* enclosingBlockFlowElement(Node& node)
|
|
{
|
|
if (isBlockFlowElement(node))
|
|
return &toElement(node);
|
|
|
|
for (Node* n = node.parentNode(); n; n = n->parentNode()) {
|
|
if (isBlockFlowElement(*n))
|
|
return toElement(n);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool inSameContainingBlockFlowElement(Node* a, Node* b)
|
|
{
|
|
return a && b && enclosingBlockFlowElement(*a) == enclosingBlockFlowElement(*b);
|
|
}
|
|
|
|
TextDirection directionOfEnclosingBlock(const Position& position)
|
|
{
|
|
Element* enclosingBlockElement = enclosingBlock(position.containerNode());
|
|
if (!enclosingBlockElement)
|
|
return LTR;
|
|
RenderObject* renderer = enclosingBlockElement->renderer();
|
|
return renderer ? renderer->style()->direction() : LTR;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
ASSERT(node);
|
|
if (!node)
|
|
return 0;
|
|
if (node->offsetInCharacters())
|
|
return node->maxCharacterOffset();
|
|
|
|
if (node->hasChildren())
|
|
return node->countChildren();
|
|
|
|
// NOTE: This should preempt the childNodeCount for, e.g., select nodes
|
|
if (editingIgnoresContent(node))
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
String stringWithRebalancedWhitespace(const String& string, bool startIsStartOfParagraph, bool endIsEndOfParagraph)
|
|
{
|
|
unsigned length = string.length();
|
|
|
|
StringBuilder rebalancedString;
|
|
rebalancedString.reserveCapacity(length);
|
|
|
|
bool previousCharacterWasSpace = false;
|
|
for (size_t i = 0; i < length; i++) {
|
|
UChar c = string[i];
|
|
if (!isWhitespace(c)) {
|
|
rebalancedString.append(c);
|
|
previousCharacterWasSpace = false;
|
|
continue;
|
|
}
|
|
|
|
if (previousCharacterWasSpace || (!i && startIsStartOfParagraph) || (i + 1 == length && endIsEndOfParagraph)) {
|
|
rebalancedString.append(noBreakSpace);
|
|
previousCharacterWasSpace = false;
|
|
} else {
|
|
rebalancedString.append(' ');
|
|
previousCharacterWasSpace = true;
|
|
}
|
|
}
|
|
|
|
ASSERT(rebalancedString.length() == length);
|
|
|
|
return rebalancedString.toString();
|
|
}
|
|
|
|
const String& nonBreakingSpaceString()
|
|
{
|
|
DEFINE_STATIC_LOCAL(String, nonBreakingSpaceString, (&noBreakSpace, 1));
|
|
return nonBreakingSpaceString;
|
|
}
|
|
|
|
// FIXME: need to dump this
|
|
bool isSpecialHTMLElement(const Node* n)
|
|
{
|
|
if (!n)
|
|
return false;
|
|
|
|
if (!n->isElementNode())
|
|
return false;
|
|
|
|
if (n->isLink())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static HTMLElement* firstInSpecialElement(const Position& pos)
|
|
{
|
|
Element* rootEditableElement = pos.containerNode()->rootEditableElement();
|
|
for (Node* n = pos.deprecatedNode(); n && n->rootEditableElement() == rootEditableElement; n = n->parentNode()) {
|
|
if (isSpecialHTMLElement(n)) {
|
|
HTMLElement* specialElement = toHTMLElement(n);
|
|
VisiblePosition vPos = VisiblePosition(pos, DOWNSTREAM);
|
|
VisiblePosition firstInElement = VisiblePosition(firstPositionInOrBeforeNode(specialElement), DOWNSTREAM);
|
|
if (isRenderedTableElement(specialElement) && vPos == firstInElement.next())
|
|
return specialElement;
|
|
if (vPos == firstInElement)
|
|
return specialElement;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static HTMLElement* lastInSpecialElement(const Position& pos)
|
|
{
|
|
Element* rootEditableElement = pos.containerNode()->rootEditableElement();
|
|
for (Node* n = pos.deprecatedNode(); n && n->rootEditableElement() == rootEditableElement; n = n->parentNode()) {
|
|
if (isSpecialHTMLElement(n)) {
|
|
HTMLElement* specialElement = toHTMLElement(n);
|
|
VisiblePosition vPos = VisiblePosition(pos, DOWNSTREAM);
|
|
VisiblePosition lastInElement = VisiblePosition(lastPositionInOrAfterNode(specialElement), DOWNSTREAM);
|
|
if (isRenderedTableElement(specialElement) && vPos == lastInElement.previous())
|
|
return specialElement;
|
|
if (vPos == lastInElement)
|
|
return specialElement;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
Position positionBeforeContainingSpecialElement(const Position& pos, HTMLElement** containingSpecialElement)
|
|
{
|
|
HTMLElement* n = firstInSpecialElement(pos);
|
|
if (!n)
|
|
return pos;
|
|
Position result = positionInParentBeforeNode(*n);
|
|
if (result.isNull() || result.deprecatedNode()->rootEditableElement() != pos.deprecatedNode()->rootEditableElement())
|
|
return pos;
|
|
if (containingSpecialElement)
|
|
*containingSpecialElement = n;
|
|
return result;
|
|
}
|
|
|
|
Position positionAfterContainingSpecialElement(const Position& pos, HTMLElement** containingSpecialElement)
|
|
{
|
|
HTMLElement* n = lastInSpecialElement(pos);
|
|
if (!n)
|
|
return pos;
|
|
Position result = positionInParentAfterNode(*n);
|
|
if (result.isNull() || result.deprecatedNode()->rootEditableElement() != pos.deprecatedNode()->rootEditableElement())
|
|
return pos;
|
|
if (containingSpecialElement)
|
|
*containingSpecialElement = n;
|
|
return result;
|
|
}
|
|
|
|
Element* isFirstPositionAfterTable(const VisiblePosition& visiblePosition)
|
|
{
|
|
Position upstream(visiblePosition.deepEquivalent().upstream());
|
|
if (isRenderedTableElement(upstream.deprecatedNode()) && upstream.atLastEditingPositionForNode())
|
|
return toElement(upstream.deprecatedNode());
|
|
|
|
return 0;
|
|
}
|
|
|
|
Element* isLastPositionBeforeTable(const VisiblePosition& visiblePosition)
|
|
{
|
|
Position downstream(visiblePosition.deepEquivalent().downstream());
|
|
if (isRenderedTableElement(downstream.deprecatedNode()) && downstream.atFirstEditingPositionForNode())
|
|
return toElement(downstream.deprecatedNode());
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Returns the visible position at the beginning of a node
|
|
VisiblePosition visiblePositionBeforeNode(Node& node)
|
|
{
|
|
if (node.hasChildren())
|
|
return VisiblePosition(firstPositionInOrBeforeNode(&node), DOWNSTREAM);
|
|
ASSERT(node.parentNode());
|
|
return VisiblePosition(positionInParentBeforeNode(node));
|
|
}
|
|
|
|
// Returns the visible position at the ending of a node
|
|
VisiblePosition visiblePositionAfterNode(Node& node)
|
|
{
|
|
if (node.hasChildren())
|
|
return VisiblePosition(lastPositionInOrAfterNode(&node), DOWNSTREAM);
|
|
ASSERT(node.parentNode());
|
|
return VisiblePosition(positionInParentAfterNode(node));
|
|
}
|
|
|
|
// Create a range object with two visible positions, start and end.
|
|
// create(Document*, const Position&, const Position&); will use deprecatedEditingOffset
|
|
// Use this function instead of create a regular range object (avoiding editing offset).
|
|
PassRefPtr<Range> createRange(Document& document, const VisiblePosition& start, const VisiblePosition& end, ExceptionState& exceptionState)
|
|
{
|
|
RefPtr<Range> selectedRange = Range::create(document);
|
|
selectedRange->setStart(start.deepEquivalent().containerNode(), start.deepEquivalent().computeOffsetInContainerNode(), exceptionState);
|
|
if (!exceptionState.had_exception())
|
|
selectedRange->setEnd(end.deepEquivalent().containerNode(), end.deepEquivalent().computeOffsetInContainerNode(), exceptionState);
|
|
return selectedRange.release();
|
|
}
|
|
|
|
Element* enclosingElementWithTag(const Position& p, const QualifiedName& tagName)
|
|
{
|
|
if (p.isNull())
|
|
return 0;
|
|
|
|
ContainerNode* root = highestEditableRoot(p);
|
|
Element* ancestor = p.deprecatedNode()->isElementNode() ? toElement(p.deprecatedNode()) : p.deprecatedNode()->parentElement();
|
|
for (; ancestor; ancestor = ancestor->parentElement()) {
|
|
if (root && !ancestor->hasEditableStyle())
|
|
continue;
|
|
if (ancestor->hasTagName(tagName))
|
|
return ancestor;
|
|
if (ancestor == root)
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
Node* enclosingNodeOfType(const Position& p, bool (*nodeIsOfType)(const Node*), EditingBoundaryCrossingRule rule)
|
|
{
|
|
// FIXME: support CanSkipCrossEditingBoundary
|
|
ASSERT(rule == CanCrossEditingBoundary || rule == CannotCrossEditingBoundary);
|
|
if (p.isNull())
|
|
return 0;
|
|
|
|
ContainerNode* root = rule == CannotCrossEditingBoundary ? highestEditableRoot(p) : 0;
|
|
for (Node* n = p.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 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
Node* highestEnclosingNodeOfType(const Position& p, bool (*nodeIsOfType)(const Node*), EditingBoundaryCrossingRule rule, Node* stayWithin)
|
|
{
|
|
Node* highest = 0;
|
|
ContainerNode* root = rule == CannotCrossEditingBoundary ? highestEditableRoot(p) : 0;
|
|
for (Node* n = p.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* excludeNode)
|
|
{
|
|
Node* previousNode = 0;
|
|
Element* rootEditableElement = node ? node->rootEditableElement() : 0;
|
|
for (; node; node = node->parentNode()) {
|
|
if (RenderObject* renderer = node->renderer()) {
|
|
if (!renderer->canHaveChildren() || hasARenderedDescendant(node, previousNode) || rootEditableElement == node || excludeNode == node)
|
|
return previousNode;
|
|
}
|
|
previousNode = node;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
Element* enclosingAnchorElement(const Position& p)
|
|
{
|
|
if (p.isNull())
|
|
return 0;
|
|
|
|
for (Element* ancestor = ElementTraversal::firstAncestorOrSelf(*p.deprecatedNode()); ancestor; ancestor = ElementTraversal::firstAncestor(*ancestor)) {
|
|
if (ancestor->isLink())
|
|
return ancestor;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool canMergeLists(Element* firstList, Element* secondList)
|
|
{
|
|
if (!firstList || !secondList)
|
|
return false;
|
|
|
|
return firstList->hasTagName(secondList->tagQName()) // make sure the list types match (ol vs. ul)
|
|
&& firstList->hasEditableStyle() && secondList->hasEditableStyle() // both lists are editable
|
|
&& firstList->rootEditableElement() == secondList->rootEditableElement() // don't cross editing boundaries
|
|
&& isVisiblyAdjacent(positionInParentAfterNode(*firstList), positionInParentBeforeNode(*secondList));
|
|
// Make sure there is no visible content between this li and the previous list
|
|
}
|
|
|
|
bool isRenderedTableElement(const Node* node)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool isEmptyTableCell(const Node* node)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
PassRefPtr<HTMLElement> createDefaultParagraphElement(Document& document)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
bool isNodeRendered(const Node *node)
|
|
{
|
|
return node && node->renderer();
|
|
}
|
|
|
|
// return first preceding DOM position rendered at a different location, or "this"
|
|
static Position previousCharacterPosition(const Position& position, EAffinity affinity)
|
|
{
|
|
if (position.isNull())
|
|
return Position();
|
|
|
|
Element* fromRootEditableElement = position.anchorNode()->rootEditableElement();
|
|
|
|
bool atStartOfLine = isStartOfLine(VisiblePosition(position, affinity));
|
|
bool rendered = position.isCandidate();
|
|
|
|
Position currentPos = position;
|
|
while (!currentPos.atStartOfTree()) {
|
|
currentPos = currentPos.previous();
|
|
|
|
if (currentPos.anchorNode()->rootEditableElement() != fromRootEditableElement)
|
|
return position;
|
|
|
|
if (atStartOfLine || !rendered) {
|
|
if (currentPos.isCandidate())
|
|
return currentPos;
|
|
} else if (position.rendersInDifferentPosition(currentPos)) {
|
|
return currentPos;
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
// This assumes that it starts in editable content.
|
|
Position leadingWhitespacePosition(const Position& position, EAffinity affinity, WhitespacePositionOption option)
|
|
{
|
|
ASSERT(isEditablePosition(position, ContentIsEditable, DoNotUpdateStyle));
|
|
if (position.isNull())
|
|
return Position();
|
|
|
|
Position prev = previousCharacterPosition(position, affinity);
|
|
if (prev != position && inSameContainingBlockFlowElement(prev.anchorNode(), position.anchorNode()) && prev.anchorNode()->isTextNode()) {
|
|
String string = toText(prev.anchorNode())->data();
|
|
UChar previousCharacter = string[prev.deprecatedEditingOffset()];
|
|
bool isSpace = option == ConsiderNonCollapsibleWhitespace ? (isSpaceOrNewline(previousCharacter) || previousCharacter == noBreakSpace) : isCollapsibleWhitespace(previousCharacter);
|
|
if (isSpace && isEditablePosition(prev))
|
|
return prev;
|
|
}
|
|
|
|
return Position();
|
|
}
|
|
|
|
// This assumes that it starts in editable content.
|
|
Position trailingWhitespacePosition(const Position& position, EAffinity, WhitespacePositionOption option)
|
|
{
|
|
ASSERT(isEditablePosition(position, ContentIsEditable, DoNotUpdateStyle));
|
|
if (position.isNull())
|
|
return Position();
|
|
|
|
VisiblePosition visiblePosition(position);
|
|
UChar characterAfterVisiblePosition = visiblePosition.characterAfter();
|
|
bool isSpace = option == ConsiderNonCollapsibleWhitespace ? (isSpaceOrNewline(characterAfterVisiblePosition) || characterAfterVisiblePosition == noBreakSpace) : isCollapsibleWhitespace(characterAfterVisiblePosition);
|
|
// The space must not be in another paragraph and it must be editable.
|
|
if (isSpace && !isEndOfParagraph(visiblePosition) && visiblePosition.next(CannotCrossEditingBoundary).isNotNull())
|
|
return position;
|
|
return Position();
|
|
}
|
|
|
|
unsigned numEnclosingMailBlockquotes(const Position& p)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
void updatePositionForNodeRemoval(Position& position, Node& node)
|
|
{
|
|
if (position.isNull())
|
|
return;
|
|
switch (position.anchorType()) {
|
|
case Position::PositionIsBeforeChildren:
|
|
if (position.containerNode() == node)
|
|
position = positionInParentBeforeNode(node);
|
|
break;
|
|
case Position::PositionIsAfterChildren:
|
|
if (position.containerNode() == node)
|
|
position = positionInParentAfterNode(node);
|
|
break;
|
|
case Position::PositionIsOffsetInAnchor:
|
|
if (position.containerNode() == node.parentNode() && static_cast<unsigned>(position.offsetInContainerNode()) > node.nodeIndex())
|
|
position.moveToOffset(position.offsetInContainerNode() - 1);
|
|
else if (node.contains(position.containerNode()))
|
|
position = positionInParentBeforeNode(node);
|
|
break;
|
|
case Position::PositionIsAfterAnchor:
|
|
if (node.contains(position.anchorNode()))
|
|
position = positionInParentAfterNode(node);
|
|
break;
|
|
case Position::PositionIsBeforeAnchor:
|
|
if (node.contains(position.anchorNode()))
|
|
position = positionInParentBeforeNode(node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool isMailHTMLBlockquoteElement(const Node* node)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int caretMinOffset(const Node* n)
|
|
{
|
|
RenderObject* r = n->renderer();
|
|
ASSERT(!n->isTextNode() || !r || r->isText()); // FIXME: This was a runtime check that seemingly couldn't fail; changed it to an assertion for now.
|
|
return r ? r->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* n)
|
|
{
|
|
// For rendered text nodes, return the last position that a caret could occupy.
|
|
if (n->isTextNode() && n->renderer())
|
|
return n->renderer()->caretMaxOffset();
|
|
// For containers return the number of children. For others do the same as above.
|
|
return lastOffsetForEditing(n);
|
|
}
|
|
|
|
bool lineBreakExistsAtVisiblePosition(const VisiblePosition& visiblePosition)
|
|
{
|
|
return lineBreakExistsAtPosition(visiblePosition.deepEquivalent().downstream());
|
|
}
|
|
|
|
bool lineBreakExistsAtPosition(const Position& position)
|
|
{
|
|
if (position.isNull())
|
|
return false;
|
|
|
|
if (!position.anchorNode()->renderer())
|
|
return false;
|
|
|
|
if (!position.anchorNode()->isTextNode() || !position.anchorNode()->renderer()->style()->preserveNewline())
|
|
return false;
|
|
|
|
Text* textNode = toText(position.anchorNode());
|
|
unsigned offset = position.offsetInContainerNode();
|
|
return offset < textNode->length() && textNode->data()[offset] == '\n';
|
|
}
|
|
|
|
// FIXME: indexForVisiblePosition and visiblePositionForIndex use TextIterators to convert between
|
|
// VisiblePositions and indices. But TextIterator iteration using TextIteratorEmitsCharactersBetweenAllVisiblePositions
|
|
// does not exactly match VisiblePosition iteration, so using them to preserve a selection during an editing
|
|
// opertion is unreliable. TextIterator's TextIteratorEmitsCharactersBetweenAllVisiblePositions 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;
|
|
|
|
Position p(visiblePosition.deepEquivalent());
|
|
Document& document = *p.document();
|
|
scope = &document;
|
|
|
|
RefPtr<Range> range = Range::create(document, firstPositionInNode(scope.get()), p.parentAnchoredEquivalent());
|
|
|
|
return TextIterator::rangeLength(range.get(), true);
|
|
}
|
|
|
|
VisiblePosition visiblePositionForIndex(int index, ContainerNode* scope)
|
|
{
|
|
if (!scope)
|
|
return VisiblePosition();
|
|
RefPtr<Range> range = PlainTextRange(index).createRangeForSelection(*scope);
|
|
// Check for an invalid index. Certain editing operations invalidate indices because
|
|
// of problems with TextIteratorEmitsCharactersBetweenAllVisiblePositions.
|
|
if (!range)
|
|
return VisiblePosition();
|
|
return VisiblePosition(range->startPosition());
|
|
}
|
|
|
|
// Determines whether two positions are visibly next to each other (first then second)
|
|
// while ignoring whitespaces and unrendered nodes
|
|
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 Range& selectedRange)
|
|
{
|
|
// If the node is inside the range, then it surely is contained within
|
|
if (selectedRange.compareNode(&node, IGNORE_EXCEPTION) == Range::NODE_INSIDE)
|
|
return true;
|
|
|
|
bool startIsVisuallySame = visiblePositionBeforeNode(node) == VisiblePosition(selectedRange.startPosition());
|
|
if (startIsVisuallySame && comparePositions(positionInParentAfterNode(node), selectedRange.endPosition()) < 0)
|
|
return true;
|
|
|
|
bool endIsVisuallySame = visiblePositionAfterNode(node) == VisiblePosition(selectedRange.endPosition());
|
|
if (endIsVisuallySame && comparePositions(selectedRange.startPosition(), positionInParentBeforeNode(node)) < 0)
|
|
return true;
|
|
|
|
return startIsVisuallySame && endIsVisuallySame;
|
|
}
|
|
|
|
bool isRenderedAsNonInlineTableImageOrHR(const Node* node)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool areIdenticalElements(const Node* first, const Node* second)
|
|
{
|
|
if (!first->isElementNode() || !second->isElementNode())
|
|
return false;
|
|
|
|
const Element* firstElement = toElement(first);
|
|
const Element* secondElement = toElement(second);
|
|
if (!firstElement->hasTagName(secondElement->tagQName()))
|
|
return false;
|
|
|
|
return firstElement->hasEquivalentAttributes(secondElement);
|
|
}
|
|
|
|
bool isBlockFlowElement(const Node& node)
|
|
{
|
|
RenderObject* renderer = node.renderer();
|
|
return node.isElementNode() && renderer && renderer->isRenderParagraph();
|
|
}
|
|
|
|
} // namespace blink
|