diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc | 769 |
1 files changed, 769 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc b/chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc new file mode 100644 index 00000000000..728e433002b --- /dev/null +++ b/chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc @@ -0,0 +1,769 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 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 "third_party/blink/renderer/core/editing/selection_adjuster.h" + +#include "third_party/blink/renderer/core/editing/editing_utilities.h" +#include "third_party/blink/renderer/core/editing/ephemeral_range.h" +#include "third_party/blink/renderer/core/editing/position.h" +#include "third_party/blink/renderer/core/editing/selection_template.h" +#include "third_party/blink/renderer/core/editing/visible_position.h" +#include "third_party/blink/renderer/core/editing/visible_selection.h" +#include "third_party/blink/renderer/core/editing/visible_units.h" +#include "third_party/blink/renderer/core/layout/layout_object.h" + +namespace blink { + +namespace { + +template <typename Strategy> +SelectionTemplate<Strategy> ComputeAdjustedSelection( + const SelectionTemplate<Strategy> selection, + const EphemeralRangeTemplate<Strategy>& range) { + if (selection.ComputeRange() == range) { + // To pass "editing/deleting/delete_after_block_image.html", we need to + // return original selection. + return selection; + } + if (range.StartPosition().CompareTo(range.EndPosition()) == 0) { + return typename SelectionTemplate<Strategy>::Builder() + .Collapse(selection.IsBaseFirst() ? range.StartPosition() + : range.EndPosition()) + .Build(); + } + if (selection.IsBaseFirst()) { + return typename SelectionTemplate<Strategy>::Builder() + .SetAsForwardSelection(range) + .Build(); + } + return typename SelectionTemplate<Strategy>::Builder() + .SetAsBackwardSelection(range) + .Build(); +} + +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 + // layoutObject, including :before and :after layoutObject + // .) the BR child of such a table cell + + // Find rendered node + while (node && !node->GetLayoutObject()) + 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. + const LayoutObject* layout_object = node->GetLayoutObject(); + if (layout_object->IsBR()) { + layout_object = layout_object->Parent(); + if (!layout_object) + return false; + } + if (!layout_object->IsTableCell()) + return false; + + // Check that the table cell contains no child layoutObjects except for + // perhaps a single <br>. + const LayoutObject* const child_layout_object = + layout_object->SlowFirstChild(); + if (!child_layout_object) + return true; + if (!child_layout_object->IsBR()) + return false; + return !child_layout_object->NextSibling(); +} + +} // anonymous namespace + +class GranularityAdjuster final { + STATIC_ONLY(GranularityAdjuster); + + public: + template <typename Strategy> + static PositionTemplate<Strategy> ComputeStartRespectingGranularityAlgorithm( + const PositionWithAffinityTemplate<Strategy>& passed_start, + TextGranularity granularity) { + DCHECK(passed_start.IsNotNull()); + + switch (granularity) { + case TextGranularity::kCharacter: + // Don't do any expansion. + return passed_start.GetPosition(); + case TextGranularity::kWord: { + // General case: Select the word the caret is positioned inside of. + // If the caret is on the word boundary, select the word according to + // |wordSide|. + // Edge case: If the caret is after the last word in a soft-wrapped line + // or the last word in the document, select that last word + // (kPreviousWordIfOnBoundary). + // Edge case: If the caret is after the last word in a paragraph, select + // from the the end of the last word to the line break (also + // kNextWordIfOnBoundary); + const VisiblePositionTemplate<Strategy> visible_start = + CreateVisiblePosition(passed_start); + return StartOfWord(visible_start, ChooseWordSide(visible_start)) + .DeepEquivalent(); + } + case TextGranularity::kSentence: + return StartOfSentence(CreateVisiblePosition(passed_start)) + .DeepEquivalent(); + case TextGranularity::kLine: + return StartOfLine(CreateVisiblePosition(passed_start)) + .DeepEquivalent(); + case TextGranularity::kLineBoundary: + return StartOfLine(CreateVisiblePosition(passed_start)) + .DeepEquivalent(); + case TextGranularity::kParagraph: { + const VisiblePositionTemplate<Strategy> pos = + CreateVisiblePosition(passed_start); + if (IsStartOfLine(pos) && IsEndOfEditableOrNonEditableContent(pos)) + return StartOfParagraph(PreviousPositionOf(pos)).DeepEquivalent(); + return StartOfParagraph(pos).DeepEquivalent(); + } + case TextGranularity::kDocumentBoundary: + return StartOfDocument(CreateVisiblePosition(passed_start)) + .DeepEquivalent(); + case TextGranularity::kParagraphBoundary: + return StartOfParagraph(CreateVisiblePosition(passed_start)) + .DeepEquivalent(); + case TextGranularity::kSentenceBoundary: + return StartOfSentence(CreateVisiblePosition(passed_start)) + .DeepEquivalent(); + } + + NOTREACHED(); + return passed_start.GetPosition(); + } + + template <typename Strategy> + static PositionTemplate<Strategy> ComputeEndRespectingGranularityAlgorithm( + const PositionTemplate<Strategy>& start, + const PositionWithAffinityTemplate<Strategy>& passed_end, + TextGranularity granularity) { + DCHECK(passed_end.IsNotNull()); + + switch (granularity) { + case TextGranularity::kCharacter: + // Don't do any expansion. + return passed_end.GetPosition(); + case TextGranularity::kWord: { + // General case: Select the word the caret is positioned inside of. + // If the caret is on the word boundary, select the word according to + // |wordSide|. + // Edge case: If the caret is after the last word in a soft-wrapped line + // or the last word in the document, select that last word + // (|kPreviousWordIfOnBoundary|). + // Edge case: If the caret is after the last word in a paragraph, select + // from the the end of the last word to the line break (also + // |kNextWordIfOnBoundary|); + const VisiblePositionTemplate<Strategy> original_end = + CreateVisiblePosition(passed_end); + const VisiblePositionTemplate<Strategy> word_end = + EndOfWord(original_end, ChooseWordSide(original_end)); + if (!IsEndOfParagraph(original_end)) + return word_end.DeepEquivalent(); + if (IsEmptyTableCell(start.AnchorNode())) + return word_end.DeepEquivalent(); + + // Select the paragraph break (the space from the end of a paragraph + // to the start of the next one) to match TextEdit. + const VisiblePositionTemplate<Strategy> end = NextPositionOf(word_end); + Element* const table = TableElementJustBefore(end); + if (!table) { + if (end.IsNull()) + return word_end.DeepEquivalent(); + return end.DeepEquivalent(); + } + + if (!IsEnclosingBlock(table)) + return word_end.DeepEquivalent(); + + // The paragraph break after the last paragraph in the last cell + // of a block table ends at the start of the paragraph after the + // table. + const VisiblePositionTemplate<Strategy> next = + NextPositionOf(end, kCannotCrossEditingBoundary); + if (next.IsNull()) + return word_end.DeepEquivalent(); + return next.DeepEquivalent(); + } + case TextGranularity::kSentence: + return EndOfSentence(CreateVisiblePosition(passed_end)) + .DeepEquivalent(); + case TextGranularity::kLine: { + const VisiblePositionTemplate<Strategy> end = + EndOfLine(CreateVisiblePosition(passed_end)); + if (!IsEndOfParagraph(end)) + return end.DeepEquivalent(); + // If the end of this line is at the end of a paragraph, include the + // space after the end of the line in the selection. + const VisiblePositionTemplate<Strategy> next = NextPositionOf(end); + if (next.IsNull()) + return end.DeepEquivalent(); + return next.DeepEquivalent(); + } + case TextGranularity::kLineBoundary: + return EndOfLine(CreateVisiblePosition(passed_end)).DeepEquivalent(); + case TextGranularity::kParagraph: { + const VisiblePositionTemplate<Strategy> visible_paragraph_end = + EndOfParagraph(CreateVisiblePosition(passed_end)); + + // Include the "paragraph break" (the space from the end of this + // paragraph to the start of the next one) in the selection. + const VisiblePositionTemplate<Strategy> end = + NextPositionOf(visible_paragraph_end); + + Element* const table = TableElementJustBefore(end); + if (!table) { + if (end.IsNull()) + return visible_paragraph_end.DeepEquivalent(); + return end.DeepEquivalent(); + } + + if (!IsEnclosingBlock(table)) { + // There is no paragraph break after the last paragraph in the + // last cell of an inline table. + return visible_paragraph_end.DeepEquivalent(); + } + + // The paragraph break after the last paragraph in the last cell of + // a block table ends at the start of the paragraph after the table, + // not at the position just after the table. + const VisiblePositionTemplate<Strategy> next = + NextPositionOf(end, kCannotCrossEditingBoundary); + if (next.IsNull()) + return visible_paragraph_end.DeepEquivalent(); + return next.DeepEquivalent(); + } + case TextGranularity::kDocumentBoundary: + return EndOfDocument(CreateVisiblePosition(passed_end)) + .DeepEquivalent(); + case TextGranularity::kParagraphBoundary: + return EndOfParagraph(CreateVisiblePosition(passed_end)) + .DeepEquivalent(); + case TextGranularity::kSentenceBoundary: + return EndOfSentence(CreateVisiblePosition(passed_end)) + .DeepEquivalent(); + } + NOTREACHED(); + return passed_end.GetPosition(); + } + + template <typename Strategy> + static SelectionTemplate<Strategy> AdjustSelection( + const SelectionTemplate<Strategy>& canonicalized_selection, + TextGranularity granularity) { + const TextAffinity affinity = canonicalized_selection.Affinity(); + + const PositionTemplate<Strategy> start = + canonicalized_selection.ComputeStartPosition(); + const PositionTemplate<Strategy> new_start = + ComputeStartRespectingGranularityAlgorithm( + PositionWithAffinityTemplate<Strategy>(start, affinity), + granularity); + const PositionTemplate<Strategy> expanded_start = + new_start.IsNotNull() ? new_start : start; + + const PositionTemplate<Strategy> end = + canonicalized_selection.ComputeEndPosition(); + const PositionTemplate<Strategy> new_end = + ComputeEndRespectingGranularityAlgorithm( + expanded_start, + PositionWithAffinityTemplate<Strategy>(end, affinity), granularity); + const PositionTemplate<Strategy> expanded_end = + new_end.IsNotNull() ? new_end : end; + + const EphemeralRangeTemplate<Strategy> expanded_range(expanded_start, + expanded_end); + return ComputeAdjustedSelection(canonicalized_selection, expanded_range); + } + + private: + template <typename Strategy> + static EWordSide ChooseWordSide( + const VisiblePositionTemplate<Strategy>& position) { + return IsEndOfEditableOrNonEditableContent(position) || + (IsEndOfLine(position) && !IsStartOfLine(position) && + !IsEndOfParagraph(position)) + ? kPreviousWordIfOnBoundary + : kNextWordIfOnBoundary; + } +}; + +PositionInFlatTree ComputeStartRespectingGranularity( + const PositionInFlatTreeWithAffinity& start, + TextGranularity granularity) { + return GranularityAdjuster::ComputeStartRespectingGranularityAlgorithm( + start, granularity); +} + +PositionInFlatTree ComputeEndRespectingGranularity( + const PositionInFlatTree& start, + const PositionInFlatTreeWithAffinity& end, + TextGranularity granularity) { + return GranularityAdjuster::ComputeEndRespectingGranularityAlgorithm( + start, end, granularity); +} + +SelectionInDOMTree SelectionAdjuster::AdjustSelectionRespectingGranularity( + const SelectionInDOMTree& selection, + TextGranularity granularity) { + return GranularityAdjuster::AdjustSelection(selection, granularity); +} + +SelectionInFlatTree SelectionAdjuster::AdjustSelectionRespectingGranularity( + const SelectionInFlatTree& selection, + TextGranularity granularity) { + return GranularityAdjuster::AdjustSelection(selection, granularity); +} + +class ShadowBoundaryAdjuster final { + STATIC_ONLY(ShadowBoundaryAdjuster); + + public: + template <typename Strategy> + static SelectionTemplate<Strategy> AdjustSelection( + const SelectionTemplate<Strategy>& selection) { + if (!selection.IsRange()) + return selection; + + const EphemeralRangeTemplate<Strategy> expanded_range = + selection.ComputeRange(); + + const EphemeralRangeTemplate<Strategy> shadow_adjusted_range = + selection.IsBaseFirst() + ? EphemeralRangeTemplate<Strategy>( + expanded_range.StartPosition(), + AdjustSelectionEndToAvoidCrossingShadowBoundaries( + expanded_range)) + : EphemeralRangeTemplate<Strategy>( + AdjustSelectionStartToAvoidCrossingShadowBoundaries( + expanded_range), + expanded_range.EndPosition()); + return ComputeAdjustedSelection(selection, shadow_adjusted_range); + } + + private: + static Node* EnclosingShadowHost(Node* node) { + for (Node* runner = node; runner; + runner = FlatTreeTraversal::Parent(*runner)) { + if (IsShadowHost(runner)) + return runner; + } + return nullptr; + } + + static bool IsEnclosedBy(const PositionInFlatTree& position, + const Node& node) { + DCHECK(position.IsNotNull()); + Node* anchor_node = position.AnchorNode(); + if (anchor_node == node) + return !position.IsAfterAnchor() && !position.IsBeforeAnchor(); + + return FlatTreeTraversal::IsDescendantOf(*anchor_node, node); + } + + static bool IsSelectionBoundary(const Node& node) { + return IsHTMLTextAreaElement(node) || IsHTMLInputElement(node) || + IsHTMLSelectElement(node); + } + + static Node* EnclosingShadowHostForStart(const PositionInFlatTree& position) { + Node* node = position.NodeAsRangeFirstNode(); + if (!node) + return nullptr; + Node* shadow_host = EnclosingShadowHost(node); + if (!shadow_host) + return nullptr; + if (!IsEnclosedBy(position, *shadow_host)) + return nullptr; + return IsSelectionBoundary(*shadow_host) ? shadow_host : nullptr; + } + + static Node* EnclosingShadowHostForEnd(const PositionInFlatTree& position) { + Node* node = position.NodeAsRangeLastNode(); + if (!node) + return nullptr; + Node* shadow_host = EnclosingShadowHost(node); + if (!shadow_host) + return nullptr; + if (!IsEnclosedBy(position, *shadow_host)) + return nullptr; + return IsSelectionBoundary(*shadow_host) ? shadow_host : nullptr; + } + + static PositionInFlatTree AdjustPositionInFlatTreeForStart( + const PositionInFlatTree& position, + Node* shadow_host) { + if (IsEnclosedBy(position, *shadow_host)) { + if (position.IsBeforeChildren()) + return PositionInFlatTree::BeforeNode(*shadow_host); + return PositionInFlatTree::AfterNode(*shadow_host); + } + + // We use |firstChild|'s after instead of beforeAllChildren for backward + // compatibility. The positions are same but the anchors would be different, + // and selection painting uses anchor nodes. + if (Node* first_child = FlatTreeTraversal::FirstChild(*shadow_host)) + return PositionInFlatTree::BeforeNode(*first_child); + return PositionInFlatTree(); + } + + static Position AdjustPositionForEnd(const Position& current_position, + Node* start_container_node) { + TreeScope& tree_scope = start_container_node->GetTreeScope(); + + DCHECK(current_position.ComputeContainerNode()->GetTreeScope() != + tree_scope); + + if (Node* ancestor = tree_scope.AncestorInThisScope( + current_position.ComputeContainerNode())) { + if (ancestor->contains(start_container_node)) + return Position::AfterNode(*ancestor); + return Position::BeforeNode(*ancestor); + } + + if (Node* last_child = tree_scope.RootNode().lastChild()) + return Position::AfterNode(*last_child); + + return Position(); + } + + static PositionInFlatTree AdjustPositionInFlatTreeForEnd( + const PositionInFlatTree& position, + Node* shadow_host) { + if (IsEnclosedBy(position, *shadow_host)) { + if (position.IsAfterChildren()) + return PositionInFlatTree::AfterNode(*shadow_host); + return PositionInFlatTree::BeforeNode(*shadow_host); + } + + // We use |lastChild|'s after instead of afterAllChildren for backward + // compatibility. The positions are same but the anchors would be different, + // and selection painting uses anchor nodes. + if (Node* last_child = FlatTreeTraversal::LastChild(*shadow_host)) + return PositionInFlatTree::AfterNode(*last_child); + return PositionInFlatTree(); + } + + static Position AdjustPositionForStart(const Position& current_position, + Node* end_container_node) { + TreeScope& tree_scope = end_container_node->GetTreeScope(); + + DCHECK(current_position.ComputeContainerNode()->GetTreeScope() != + tree_scope); + + if (Node* ancestor = tree_scope.AncestorInThisScope( + current_position.ComputeContainerNode())) { + if (ancestor->contains(end_container_node)) + return Position::BeforeNode(*ancestor); + return Position::AfterNode(*ancestor); + } + + if (Node* first_child = tree_scope.RootNode().firstChild()) + return Position::BeforeNode(*first_child); + + return Position(); + } + + // TODO(hajimehoshi): Checking treeScope is wrong when a node is + // distributed, but we leave it as it is for backward compatibility. + static bool IsCrossingShadowBoundaries(const EphemeralRange& range) { + DCHECK(range.IsNotNull()); + return range.StartPosition().AnchorNode()->GetTreeScope() != + range.EndPosition().AnchorNode()->GetTreeScope(); + } + + static Position AdjustSelectionStartToAvoidCrossingShadowBoundaries( + const EphemeralRange& range) { + DCHECK(range.IsNotNull()); + if (!IsCrossingShadowBoundaries(range)) + return range.StartPosition(); + return AdjustPositionForStart(range.StartPosition(), + range.EndPosition().ComputeContainerNode()); + } + + static Position AdjustSelectionEndToAvoidCrossingShadowBoundaries( + const EphemeralRange& range) { + DCHECK(range.IsNotNull()); + if (!IsCrossingShadowBoundaries(range)) + return range.EndPosition(); + return AdjustPositionForEnd(range.EndPosition(), + range.StartPosition().ComputeContainerNode()); + } + + static PositionInFlatTree AdjustSelectionStartToAvoidCrossingShadowBoundaries( + const EphemeralRangeInFlatTree& range) { + Node* const shadow_host_start = + EnclosingShadowHostForStart(range.StartPosition()); + Node* const shadow_host_end = + EnclosingShadowHostForEnd(range.EndPosition()); + if (shadow_host_start == shadow_host_end) + return range.StartPosition(); + Node* const shadow_host = + shadow_host_end ? shadow_host_end : shadow_host_start; + return AdjustPositionInFlatTreeForStart(range.StartPosition(), shadow_host); + } + + static PositionInFlatTree AdjustSelectionEndToAvoidCrossingShadowBoundaries( + const EphemeralRangeInFlatTree& range) { + Node* const shadow_host_start = + EnclosingShadowHostForStart(range.StartPosition()); + Node* const shadow_host_end = + EnclosingShadowHostForEnd(range.EndPosition()); + if (shadow_host_start == shadow_host_end) + return range.EndPosition(); + Node* const shadow_host = + shadow_host_start ? shadow_host_start : shadow_host_end; + return AdjustPositionInFlatTreeForEnd(range.EndPosition(), shadow_host); + } +}; + +SelectionInDOMTree +SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries( + const SelectionInDOMTree& selection) { + return ShadowBoundaryAdjuster::AdjustSelection(selection); +} +SelectionInFlatTree +SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries( + const SelectionInFlatTree& selection) { + return ShadowBoundaryAdjuster::AdjustSelection(selection); +} + +class EditingBoundaryAdjuster final { + STATIC_ONLY(EditingBoundaryAdjuster); + + public: + template <typename Strategy> + static SelectionTemplate<Strategy> AdjustSelection( + const SelectionTemplate<Strategy>& shadow_adjusted_selection) { + // TODO(editing-dev): Refactor w/o EphemeralRange. + const EphemeralRangeTemplate<Strategy> shadow_adjusted_range = + shadow_adjusted_selection.ComputeRange(); + const EphemeralRangeTemplate<Strategy> editing_adjusted_range = + AdjustSelectionToAvoidCrossingEditingBoundaries( + shadow_adjusted_range, shadow_adjusted_selection.Base()); + return ComputeAdjustedSelection(shadow_adjusted_selection, + editing_adjusted_range); + } + + private: + static Element* LowestEditableAncestor(Node* node) { + while (node) { + if (HasEditableStyle(*node)) + return RootEditableElement(*node); + if (IsHTMLBodyElement(*node)) + break; + node = node->parentNode(); + } + + return nullptr; + } + + // Returns true if |position| is editable or its lowest editable root is not + // |base_editable_ancestor|. + template <typename Strategy> + static bool ShouldContinueSearchEditingBoundary( + const PositionTemplate<Strategy>& position, + Element* base_editable_ancestor) { + if (position.IsNull()) + return false; + if (IsEditablePosition(position)) + return true; + return LowestEditableAncestor(position.ComputeContainerNode()) != + base_editable_ancestor; + } + + template <typename Strategy> + static bool ShouldAdjustPositionToAvoidCrossingEditingBoundaries( + const PositionTemplate<Strategy>& position, + const ContainerNode* editable_root, + const Element* base_editable_ancestor) { + if (editable_root) + return true; + Element* const editable_ancestor = + LowestEditableAncestor(position.ComputeContainerNode()); + return editable_ancestor != base_editable_ancestor; + } + + // The selection ends in editable content or non-editable content inside a + // different editable ancestor, move backward until non-editable content + // inside the same lowest editable ancestor is reached. + template <typename Strategy> + static PositionTemplate<Strategy> + AdjustSelectionEndToAvoidCrossingEditingBoundaries( + const PositionTemplate<Strategy>& end, + ContainerNode* end_root, + Element* base_editable_ancestor) { + if (ShouldAdjustPositionToAvoidCrossingEditingBoundaries( + end, end_root, base_editable_ancestor)) { + PositionTemplate<Strategy> position = + PreviousVisuallyDistinctCandidate(end); + Element* shadow_ancestor = + end_root ? end_root->OwnerShadowHost() : nullptr; + if (position.IsNull() && shadow_ancestor) + position = PositionTemplate<Strategy>::AfterNode(*shadow_ancestor); + while (ShouldContinueSearchEditingBoundary(position, + base_editable_ancestor)) { + Element* root = RootEditableElementOf(position); + shadow_ancestor = root ? root->OwnerShadowHost() : nullptr; + position = IsAtomicNode(position.ComputeContainerNode()) + ? PositionTemplate<Strategy>::InParentBeforeNode( + *position.ComputeContainerNode()) + : PreviousVisuallyDistinctCandidate(position); + if (position.IsNull() && shadow_ancestor) + position = PositionTemplate<Strategy>::AfterNode(*shadow_ancestor); + } + return CreateVisiblePosition(position).DeepEquivalent(); + } + return end; + } + + // The selection starts in editable content or non-editable content inside a + // different editable ancestor, move forward until non-editable content inside + // the same lowest editable ancestor is reached. + template <typename Strategy> + static PositionTemplate<Strategy> + AdjustSelectionStartToAvoidCrossingEditingBoundaries( + const PositionTemplate<Strategy>& start, + ContainerNode* start_root, + Element* base_editable_ancestor) { + if (ShouldAdjustPositionToAvoidCrossingEditingBoundaries( + start, start_root, base_editable_ancestor)) { + PositionTemplate<Strategy> position = + NextVisuallyDistinctCandidate(start); + Element* shadow_ancestor = + start_root ? start_root->OwnerShadowHost() : nullptr; + if (position.IsNull() && shadow_ancestor) + position = PositionTemplate<Strategy>::BeforeNode(*shadow_ancestor); + while (ShouldContinueSearchEditingBoundary(position, + base_editable_ancestor)) { + Element* root = RootEditableElementOf(position); + shadow_ancestor = root ? root->OwnerShadowHost() : nullptr; + position = IsAtomicNode(position.ComputeContainerNode()) + ? PositionTemplate<Strategy>::InParentAfterNode( + *position.ComputeContainerNode()) + : NextVisuallyDistinctCandidate(position); + if (position.IsNull() && shadow_ancestor) + position = PositionTemplate<Strategy>::BeforeNode(*shadow_ancestor); + } + return CreateVisiblePosition(position).DeepEquivalent(); + } + return start; + } + + template <typename Strategy> + static EphemeralRangeTemplate<Strategy> + AdjustSelectionToAvoidCrossingEditingBoundaries( + const EphemeralRangeTemplate<Strategy>& range, + const PositionTemplate<Strategy>& base) { + DCHECK(base.IsNotNull()); + DCHECK(range.IsNotNull()); + + ContainerNode* base_root = HighestEditableRoot(base); + ContainerNode* start_root = HighestEditableRoot(range.StartPosition()); + ContainerNode* end_root = HighestEditableRoot(range.EndPosition()); + + Element* base_editable_ancestor = + LowestEditableAncestor(base.ComputeContainerNode()); + + // The base, start and end are all in the same region. No adjustment + // necessary. + if (base_root == start_root && base_root == end_root) + return range; + + // The selection is based in editable content. + if (base_root) { + // If the start is outside the base's editable root, cap it at the start + // of that root. If the start is in non-editable content that is inside + // the base's editable root, put it at the first editable position after + // start inside the base's editable root. + PositionTemplate<Strategy> start = range.StartPosition(); + if (start_root != base_root) { + const VisiblePositionTemplate<Strategy> first = + FirstEditableVisiblePositionAfterPositionInRoot(start, *base_root); + start = first.DeepEquivalent(); + if (start.IsNull()) { + NOTREACHED(); + return {}; + } + } + // If the end is outside the base's editable root, cap it at the end of + // that root. If the end is in non-editable content that is inside the + // base's root, put it at the last editable position before the end inside + // the base's root. + PositionTemplate<Strategy> end = range.EndPosition(); + if (end_root != base_root) { + const VisiblePositionTemplate<Strategy> last = + LastEditableVisiblePositionBeforePositionInRoot(end, *base_root); + end = last.DeepEquivalent(); + if (end.IsNull()) + end = start; + } + return {start, end}; + } + + // The selection is based in non-editable content. + // FIXME: Non-editable pieces inside editable content should be atomic, in + // the same way that editable pieces in non-editable content are atomic. + const PositionTemplate<Strategy>& end = + AdjustSelectionEndToAvoidCrossingEditingBoundaries( + range.EndPosition(), end_root, base_editable_ancestor); + if (end.IsNull()) { + // The selection crosses an Editing boundary. This is a + // programmer error in the editing code. Happy debugging! + NOTREACHED(); + return {}; + } + + const PositionTemplate<Strategy>& start = + AdjustSelectionStartToAvoidCrossingEditingBoundaries( + range.StartPosition(), start_root, base_editable_ancestor); + if (start.IsNull()) { + // The selection crosses an Editing boundary. This is a + // programmer error in the editing code. Happy debugging! + NOTREACHED(); + return {}; + } + return {start, end}; + } +}; + +SelectionInDOMTree +SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( + const SelectionInDOMTree& selection) { + return EditingBoundaryAdjuster::AdjustSelection(selection); +} +SelectionInFlatTree +SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( + const SelectionInFlatTree& selection) { + return EditingBoundaryAdjuster::AdjustSelection(selection); +} + +} // namespace blink |