| /* |
| * Copyright (C) 2004, 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_editor.h" |
| |
| #include "third_party/blink/renderer/core/dom/node_with_index.h" |
| #include "third_party/blink/renderer/core/dom/text.h" |
| #include "third_party/blink/renderer/core/editing/editing_behavior.h" |
| #include "third_party/blink/renderer/core/editing/editing_utilities.h" |
| #include "third_party/blink/renderer/core/editing/editor.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/frame_caret.h" |
| #include "third_party/blink/renderer/core/editing/local_caret_rect.h" |
| #include "third_party/blink/renderer/core/editing/position_with_affinity.h" |
| #include "third_party/blink/renderer/core/editing/selection_adjuster.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| |
| namespace blink { |
| |
| SelectionEditor::SelectionEditor(LocalFrame& frame) : frame_(frame) { |
| ClearVisibleSelection(); |
| } |
| |
| SelectionEditor::~SelectionEditor() = default; |
| |
| void SelectionEditor::AssertSelectionValid() const { |
| #if DCHECK_IS_ON() |
| // Since We don't track dom tree version during attribute changes, we can't |
| // use it for validity of |selection_|. |
| const_cast<SelectionEditor*>(this)->selection_.dom_tree_version_ = |
| GetDocument().DomTreeVersion(); |
| #endif |
| selection_.AssertValidFor(GetDocument()); |
| } |
| |
| void SelectionEditor::ClearVisibleSelection() { |
| selection_ = SelectionInDOMTree(); |
| cached_visible_selection_in_dom_tree_ = VisibleSelection(); |
| cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree(); |
| cached_visible_selection_in_dom_tree_is_dirty_ = true; |
| cached_visible_selection_in_flat_tree_is_dirty_ = true; |
| } |
| |
| void SelectionEditor::Dispose() { |
| ClearDocumentCachedRange(); |
| ClearVisibleSelection(); |
| } |
| |
| Document& SelectionEditor::GetDocument() const { |
| DCHECK(SynchronousMutationObserver::GetDocument()); |
| return *SynchronousMutationObserver::GetDocument(); |
| } |
| |
| VisibleSelection SelectionEditor::ComputeVisibleSelectionInDOMTree() const { |
| DCHECK_EQ(GetFrame()->GetDocument(), GetDocument()); |
| DCHECK_EQ(GetFrame(), GetDocument().GetFrame()); |
| UpdateCachedVisibleSelectionIfNeeded(); |
| if (cached_visible_selection_in_dom_tree_.IsNone()) |
| return cached_visible_selection_in_dom_tree_; |
| DCHECK_EQ(cached_visible_selection_in_dom_tree_.Base().GetDocument(), |
| GetDocument()); |
| return cached_visible_selection_in_dom_tree_; |
| } |
| |
| VisibleSelectionInFlatTree SelectionEditor::ComputeVisibleSelectionInFlatTree() |
| const { |
| DCHECK_EQ(GetFrame()->GetDocument(), GetDocument()); |
| DCHECK_EQ(GetFrame(), GetDocument().GetFrame()); |
| UpdateCachedVisibleSelectionInFlatTreeIfNeeded(); |
| if (cached_visible_selection_in_flat_tree_.IsNone()) |
| return cached_visible_selection_in_flat_tree_; |
| DCHECK_EQ(cached_visible_selection_in_flat_tree_.Base().GetDocument(), |
| GetDocument()); |
| return cached_visible_selection_in_flat_tree_; |
| } |
| |
| bool SelectionEditor::ComputeAbsoluteBounds(IntRect& anchor, |
| IntRect& focus) const { |
| DCHECK_EQ(GetFrame()->GetDocument(), GetDocument()); |
| DCHECK_EQ(GetFrame(), GetDocument().GetFrame()); |
| UpdateCachedAbsoluteBoundsIfNeeded(); |
| if (!has_selection_bounds_) |
| return has_selection_bounds_; |
| anchor = cached_anchor_bounds_; |
| focus = cached_focus_bounds_; |
| return has_selection_bounds_; |
| } |
| |
| SelectionInDOMTree SelectionEditor::GetSelectionInDOMTree() const { |
| AssertSelectionValid(); |
| return selection_; |
| } |
| |
| void SelectionEditor::MarkCacheDirty() { |
| if (!cached_visible_selection_in_dom_tree_is_dirty_) { |
| cached_visible_selection_in_dom_tree_ = VisibleSelection(); |
| cached_visible_selection_in_dom_tree_is_dirty_ = true; |
| } |
| if (!cached_visible_selection_in_flat_tree_is_dirty_) { |
| cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree(); |
| cached_visible_selection_in_flat_tree_is_dirty_ = true; |
| } |
| if (!cached_absolute_bounds_are_dirty_) { |
| cached_absolute_bounds_are_dirty_ = true; |
| has_selection_bounds_ = false; |
| cached_anchor_bounds_ = IntRect(); |
| cached_focus_bounds_ = IntRect(); |
| } |
| } |
| |
| void SelectionEditor::SetSelectionAndEndTyping( |
| const SelectionInDOMTree& new_selection) { |
| new_selection.AssertValidFor(GetDocument()); |
| DCHECK_NE(selection_, new_selection); |
| ClearDocumentCachedRange(); |
| MarkCacheDirty(); |
| selection_ = new_selection; |
| } |
| |
| void SelectionEditor::DidChangeChildren(const ContainerNode&) { |
| selection_.ResetDirectionCache(); |
| MarkCacheDirty(); |
| DidFinishDOMMutation(); |
| } |
| |
| void SelectionEditor::DidFinishTextChange(const Position& new_base, |
| const Position& new_extent) { |
| if (new_base == selection_.base_ && new_extent == selection_.extent_) { |
| DidFinishDOMMutation(); |
| return; |
| } |
| selection_.base_ = new_base; |
| selection_.extent_ = new_extent; |
| selection_.ResetDirectionCache(); |
| MarkCacheDirty(); |
| DidFinishDOMMutation(); |
| } |
| |
| void SelectionEditor::DidFinishDOMMutation() { |
| AssertSelectionValid(); |
| } |
| |
| void SelectionEditor::DidAttachDocument(Document* document) { |
| DCHECK(document); |
| DCHECK(!SynchronousMutationObserver::GetDocument()) |
| << SynchronousMutationObserver::GetDocument(); |
| #if DCHECK_IS_ON() |
| style_version_for_dom_tree_ = static_cast<uint64_t>(-1); |
| style_version_for_flat_tree_ = static_cast<uint64_t>(-1); |
| #endif |
| ClearVisibleSelection(); |
| SetDocument(document); |
| } |
| |
| void SelectionEditor::ContextDestroyed() { |
| Dispose(); |
| #if DCHECK_IS_ON() |
| style_version_for_dom_tree_ = static_cast<uint64_t>(-1); |
| style_version_for_flat_tree_ = static_cast<uint64_t>(-1); |
| style_version_for_absolute_bounds_ = static_cast<uint64_t>(-1); |
| #endif |
| selection_ = SelectionInDOMTree(); |
| cached_visible_selection_in_dom_tree_ = VisibleSelection(); |
| cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree(); |
| cached_visible_selection_in_dom_tree_is_dirty_ = true; |
| cached_visible_selection_in_flat_tree_is_dirty_ = true; |
| cached_absolute_bounds_are_dirty_ = true; |
| has_selection_bounds_ = false; |
| cached_anchor_bounds_ = IntRect(); |
| cached_focus_bounds_ = IntRect(); |
| } |
| |
| static Position ComputePositionForChildrenRemoval(const Position& position, |
| ContainerNode& container) { |
| Node* node = position.ComputeContainerNode(); |
| #if DCHECK_IS_ON() |
| DCHECK(node) << position; |
| #else |
| // TODO(https://crbug.com/882592): Once we know the root cause, we should |
| // get rid of following if-statement. |
| if (!node) |
| return position; |
| #endif |
| if (!container.ContainsIncludingHostElements(*node)) |
| return position; |
| if (auto* element = DynamicTo<Element>(container)) { |
| if (auto* shadow_root = element->GetShadowRoot()) { |
| // Removal of light children does not affect position in the |
| // shadow tree. |
| if (shadow_root->ContainsIncludingHostElements(*node)) |
| return position; |
| } |
| } |
| return Position::FirstPositionInNode(container); |
| } |
| |
| void SelectionEditor::NodeChildrenWillBeRemoved(ContainerNode& container) { |
| if (selection_.IsNone()) |
| return; |
| const Position old_base = selection_.base_; |
| const Position old_extent = selection_.extent_; |
| const Position& new_base = |
| ComputePositionForChildrenRemoval(old_base, container); |
| const Position& new_extent = |
| ComputePositionForChildrenRemoval(old_extent, container); |
| if (new_base == old_base && new_extent == old_extent) |
| return; |
| selection_ = SelectionInDOMTree::Builder() |
| .SetBaseAndExtent(new_base, new_extent) |
| .Build(); |
| MarkCacheDirty(); |
| } |
| |
| void SelectionEditor::NodeWillBeRemoved(Node& node_to_be_removed) { |
| if (selection_.IsNone()) |
| return; |
| const Position old_base = selection_.base_; |
| const Position old_extent = selection_.extent_; |
| const Position& new_base = |
| ComputePositionForNodeRemoval(old_base, node_to_be_removed); |
| const Position& new_extent = |
| ComputePositionForNodeRemoval(old_extent, node_to_be_removed); |
| if (new_base == old_base && new_extent == old_extent) |
| return; |
| selection_ = SelectionInDOMTree::Builder() |
| .SetBaseAndExtent(new_base, new_extent) |
| .Build(); |
| MarkCacheDirty(); |
| } |
| |
| static Position UpdatePositionAfterAdoptingTextReplacement( |
| const Position& position, |
| CharacterData* node, |
| unsigned offset, |
| unsigned old_length, |
| unsigned new_length) { |
| if (position.AnchorNode() != node) |
| return position; |
| |
| if (position.IsBeforeAnchor()) { |
| return UpdatePositionAfterAdoptingTextReplacement( |
| Position(node, 0), node, offset, old_length, new_length); |
| } |
| if (position.IsAfterAnchor()) { |
| return UpdatePositionAfterAdoptingTextReplacement( |
| Position(node, old_length), node, offset, old_length, new_length); |
| } |
| |
| // See: |
| // http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation |
| DCHECK_GE(position.OffsetInContainerNode(), 0); |
| unsigned position_offset = |
| static_cast<unsigned>(position.OffsetInContainerNode()); |
| // Replacing text can be viewed as a deletion followed by insertion. |
| if (position_offset >= offset && position_offset <= offset + old_length) |
| position_offset = offset; |
| |
| // Adjust the offset if the position is after the end of the deleted contents |
| // (positionOffset > offset + oldLength) to avoid having a stale offset. |
| if (position_offset > offset + old_length) |
| position_offset = position_offset - old_length + new_length; |
| |
| // Due to case folding |
| // (http://unicode.org/Public/UCD/latest/ucd/CaseFolding.txt), LayoutText |
| // length may be different from Text length. A correct implementation would |
| // translate the LayoutText offset to a Text offset; this is just a safety |
| // precaution to avoid offset values that run off the end of the Text. |
| if (position_offset > node->length()) |
| position_offset = node->length(); |
| |
| return Position(node, position_offset); |
| } |
| |
| void SelectionEditor::DidUpdateCharacterData(CharacterData* node, |
| unsigned offset, |
| unsigned old_length, |
| unsigned new_length) { |
| // The fragment check is a performance optimization. See |
| // http://trac.webkit.org/changeset/30062. |
| if (selection_.IsNone() || !node || !node->isConnected()) { |
| DidFinishDOMMutation(); |
| return; |
| } |
| const Position& new_base = UpdatePositionAfterAdoptingTextReplacement( |
| selection_.base_, node, offset, old_length, new_length); |
| const Position& new_extent = UpdatePositionAfterAdoptingTextReplacement( |
| selection_.extent_, node, offset, old_length, new_length); |
| DidFinishTextChange(new_base, new_extent); |
| } |
| |
| static Position UpdatePostionAfterAdoptingTextNodesMerged( |
| const Position& position, |
| const Text& merged_node, |
| const NodeWithIndex& node_to_be_removed_with_index, |
| unsigned old_length) { |
| Node* const anchor_node = position.AnchorNode(); |
| const Node& node_to_be_removed = node_to_be_removed_with_index.GetNode(); |
| switch (position.AnchorType()) { |
| case PositionAnchorType::kAfterChildren: |
| return position; |
| case PositionAnchorType::kBeforeAnchor: |
| if (anchor_node == node_to_be_removed) |
| return Position(merged_node, merged_node.length()); |
| return position; |
| case PositionAnchorType::kAfterAnchor: |
| if (anchor_node == node_to_be_removed) |
| return Position(merged_node, merged_node.length()); |
| if (anchor_node == merged_node) |
| return Position(merged_node, old_length); |
| return position; |
| case PositionAnchorType::kOffsetInAnchor: { |
| const int offset = position.OffsetInContainerNode(); |
| if (anchor_node == node_to_be_removed) |
| return Position(merged_node, old_length + offset); |
| if (anchor_node == node_to_be_removed.parentNode() && |
| offset == node_to_be_removed_with_index.Index()) { |
| return Position(merged_node, old_length); |
| } |
| return position; |
| } |
| } |
| NOTREACHED() << position; |
| return position; |
| } |
| |
| void SelectionEditor::DidMergeTextNodes( |
| const Text& merged_node, |
| const NodeWithIndex& node_to_be_removed_with_index, |
| unsigned old_length) { |
| if (selection_.IsNone()) { |
| DidFinishDOMMutation(); |
| return; |
| } |
| const Position& new_base = UpdatePostionAfterAdoptingTextNodesMerged( |
| selection_.base_, merged_node, node_to_be_removed_with_index, old_length); |
| const Position& new_extent = UpdatePostionAfterAdoptingTextNodesMerged( |
| selection_.extent_, merged_node, node_to_be_removed_with_index, |
| old_length); |
| DidFinishTextChange(new_base, new_extent); |
| } |
| |
| static Position UpdatePostionAfterAdoptingTextNodeSplit( |
| const Position& position, |
| const Text& old_node) { |
| if (!position.AnchorNode() || position.AnchorNode() != &old_node || |
| !position.IsOffsetInAnchor()) |
| return position; |
| // See: |
| // http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation |
| DCHECK_GE(position.OffsetInContainerNode(), 0); |
| unsigned position_offset = |
| static_cast<unsigned>(position.OffsetInContainerNode()); |
| unsigned old_length = old_node.length(); |
| if (position_offset <= old_length) |
| return position; |
| return Position(To<Text>(old_node.nextSibling()), |
| position_offset - old_length); |
| } |
| |
| void SelectionEditor::DidSplitTextNode(const Text& old_node) { |
| if (selection_.IsNone() || !old_node.isConnected()) { |
| DidFinishDOMMutation(); |
| return; |
| } |
| const Position& new_base = |
| UpdatePostionAfterAdoptingTextNodeSplit(selection_.base_, old_node); |
| const Position& new_extent = |
| UpdatePostionAfterAdoptingTextNodeSplit(selection_.extent_, old_node); |
| DidFinishTextChange(new_base, new_extent); |
| } |
| |
| bool SelectionEditor::ShouldAlwaysUseDirectionalSelection() const { |
| return GetFrame() |
| ->GetEditor() |
| .Behavior() |
| .ShouldConsiderSelectionAsDirectional(); |
| } |
| |
| bool SelectionEditor::NeedsUpdateVisibleSelection() const { |
| #if DCHECK_IS_ON() |
| // Verify that cache has been marked dirty on style changes |
| DCHECK(cached_visible_selection_in_dom_tree_is_dirty_ || |
| style_version_for_dom_tree_ == GetDocument().StyleVersion()); |
| #endif |
| return cached_visible_selection_in_dom_tree_is_dirty_; |
| } |
| |
| void SelectionEditor::UpdateCachedVisibleSelectionIfNeeded() const { |
| // Note: Since we |FrameCaret::updateApperance()| is called from |
| // |FrameView::performPostLayoutTasks()|, we check lifecycle against |
| // |AfterPerformLayout| instead of |LayoutClean|. |
| DCHECK_GE(GetDocument().Lifecycle().GetState(), |
| DocumentLifecycle::kAfterPerformLayout); |
| AssertSelectionValid(); |
| if (!NeedsUpdateVisibleSelection()) |
| return; |
| #if DCHECK_IS_ON() |
| style_version_for_dom_tree_ = GetDocument().StyleVersion(); |
| #endif |
| cached_visible_selection_in_dom_tree_is_dirty_ = false; |
| cached_visible_selection_in_dom_tree_ = CreateVisibleSelection(selection_); |
| if (!cached_visible_selection_in_dom_tree_.IsNone()) |
| return; |
| #if DCHECK_IS_ON() |
| style_version_for_flat_tree_ = GetDocument().StyleVersion(); |
| #endif |
| cached_visible_selection_in_flat_tree_is_dirty_ = false; |
| cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree(); |
| } |
| |
| bool SelectionEditor::NeedsUpdateVisibleSelectionInFlatTree() const { |
| #if DCHECK_IS_ON() |
| // Verify that cache has been marked dirty on style changes |
| DCHECK(cached_visible_selection_in_flat_tree_is_dirty_ || |
| style_version_for_flat_tree_ == GetDocument().StyleVersion()); |
| #endif |
| return cached_visible_selection_in_flat_tree_is_dirty_; |
| } |
| |
| void SelectionEditor::UpdateCachedVisibleSelectionInFlatTreeIfNeeded() const { |
| // Note: Since we |FrameCaret::updateApperance()| is called from |
| // |FrameView::performPostLayoutTasks()|, we check lifecycle against |
| // |AfterPerformLayout| instead of |LayoutClean|. |
| DCHECK_GE(GetDocument().Lifecycle().GetState(), |
| DocumentLifecycle::kAfterPerformLayout); |
| AssertSelectionValid(); |
| if (!NeedsUpdateVisibleSelectionInFlatTree()) |
| return; |
| #if DCHECK_IS_ON() |
| style_version_for_flat_tree_ = GetDocument().StyleVersion(); |
| #endif |
| cached_visible_selection_in_flat_tree_is_dirty_ = false; |
| SelectionInFlatTree::Builder builder; |
| const PositionInFlatTree& base = ToPositionInFlatTree(selection_.Base()); |
| const PositionInFlatTree& extent = ToPositionInFlatTree(selection_.Extent()); |
| if (base.IsNotNull() && extent.IsNotNull()) |
| builder.SetBaseAndExtent(base, extent); |
| else if (base.IsNotNull()) |
| builder.Collapse(base); |
| else if (extent.IsNotNull()) |
| builder.Collapse(extent); |
| builder.SetAffinity(selection_.Affinity()); |
| cached_visible_selection_in_flat_tree_ = |
| CreateVisibleSelection(builder.Build()); |
| if (!cached_visible_selection_in_flat_tree_.IsNone()) |
| return; |
| #if DCHECK_IS_ON() |
| style_version_for_dom_tree_ = GetDocument().StyleVersion(); |
| #endif |
| cached_visible_selection_in_dom_tree_is_dirty_ = false; |
| cached_visible_selection_in_dom_tree_ = VisibleSelection(); |
| } |
| |
| bool SelectionEditor::NeedsUpdateAbsoluteBounds() const { |
| #if DCHECK_IS_ON() |
| // Verify that cache has been marked dirty on style changes |
| DCHECK(cached_absolute_bounds_are_dirty_ || |
| style_version_for_absolute_bounds_ == GetDocument().StyleVersion()); |
| #endif |
| return cached_absolute_bounds_are_dirty_; |
| } |
| |
| void SelectionEditor::UpdateCachedAbsoluteBoundsIfNeeded() const { |
| // Note: Since we |FrameCaret::updateApperance()| is called from |
| // |FrameView::performPostLayoutTasks()|, we check lifecycle against |
| // |AfterPerformLayout| instead of |LayoutClean|. |
| DCHECK_GE(GetDocument().Lifecycle().GetState(), |
| DocumentLifecycle::kAfterPerformLayout); |
| AssertSelectionValid(); |
| if (!NeedsUpdateAbsoluteBounds()) |
| return; |
| |
| DocumentLifecycle::DisallowTransitionScope disallow_transition( |
| frame_->GetDocument()->Lifecycle()); |
| |
| #if DCHECK_IS_ON() |
| style_version_for_absolute_bounds_ = GetDocument().StyleVersion(); |
| #endif |
| cached_absolute_bounds_are_dirty_ = false; |
| |
| const VisibleSelection selection = ComputeVisibleSelectionInDOMTree(); |
| |
| if (selection.IsCaret()) { |
| DCHECK(selection.IsValidFor(*frame_->GetDocument())); |
| const PositionWithAffinity caret(selection.Start(), selection.Affinity()); |
| cached_anchor_bounds_ = cached_focus_bounds_ = AbsoluteCaretBoundsOf(caret); |
| } else { |
| const EphemeralRange selected_range = |
| selection.ToNormalizedEphemeralRange(); |
| if (selected_range.IsNull()) { |
| has_selection_bounds_ = false; |
| return; |
| } |
| cached_anchor_bounds_ = |
| FirstRectForRange(EphemeralRange(selected_range.StartPosition())); |
| cached_focus_bounds_ = |
| FirstRectForRange(EphemeralRange(selected_range.EndPosition())); |
| } |
| |
| if (!selection.IsBaseFirst()) |
| std::swap(cached_anchor_bounds_, cached_focus_bounds_); |
| |
| has_selection_bounds_ = true; |
| } |
| |
| void SelectionEditor::CacheRangeOfDocument(Range* range) { |
| cached_range_ = range; |
| } |
| |
| Range* SelectionEditor::DocumentCachedRange() const { |
| return cached_range_; |
| } |
| |
| void SelectionEditor::ClearDocumentCachedRange() { |
| cached_range_ = nullptr; |
| } |
| |
| void SelectionEditor::Trace(Visitor* visitor) const { |
| visitor->Trace(frame_); |
| visitor->Trace(selection_); |
| visitor->Trace(cached_visible_selection_in_dom_tree_); |
| visitor->Trace(cached_visible_selection_in_flat_tree_); |
| visitor->Trace(cached_range_); |
| SynchronousMutationObserver::Trace(visitor); |
| } |
| |
| } // namespace blink |