blob: 506e7b7d1467c2ad0c95e5fbe712fc4c2529dc10 [file] [log] [blame]
/*
* 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_ = false;
cached_visible_selection_in_flat_tree_is_dirty_ = false;
}
void SelectionEditor::Dispose() {
ClearDocumentCachedRange();
ClearVisibleSelection();
}
Document& SelectionEditor::GetDocument() const {
DCHECK(LifecycleContext());
return *LifecycleContext();
}
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(!LifecycleContext()) << LifecycleContext();
style_version_for_dom_tree_ = static_cast<uint64_t>(-1);
style_version_for_flat_tree_ = static_cast<uint64_t>(-1);
ClearVisibleSelection();
SetContext(document);
}
void SelectionEditor::ContextDestroyed(Document*) {
Dispose();
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);
selection_ = SelectionInDOMTree();
cached_visible_selection_in_dom_tree_ = VisibleSelection();
cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree();
cached_visible_selection_in_dom_tree_is_dirty_ = false;
cached_visible_selection_in_flat_tree_is_dirty_ = false;
cached_absolute_bounds_are_dirty_ = false;
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::kBeforeChildren:
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 {
return cached_visible_selection_in_dom_tree_is_dirty_ ||
style_version_for_dom_tree_ != GetDocument().StyleVersion();
}
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;
style_version_for_dom_tree_ = GetDocument().StyleVersion();
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;
style_version_for_flat_tree_ = GetDocument().StyleVersion();
cached_visible_selection_in_flat_tree_is_dirty_ = false;
cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree();
}
bool SelectionEditor::NeedsUpdateVisibleSelectionInFlatTree() const {
return cached_visible_selection_in_flat_tree_is_dirty_ ||
style_version_for_flat_tree_ != GetDocument().StyleVersion();
}
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;
style_version_for_flat_tree_ = GetDocument().StyleVersion();
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;
style_version_for_dom_tree_ = GetDocument().StyleVersion();
cached_visible_selection_in_dom_tree_is_dirty_ = false;
cached_visible_selection_in_dom_tree_ = VisibleSelection();
}
bool SelectionEditor::NeedsUpdateAbsoluteBounds() const {
return cached_absolute_bounds_are_dirty_ ||
style_version_for_absolute_bounds_ != GetDocument().StyleVersion();
}
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());
style_version_for_absolute_bounds_ = GetDocument().StyleVersion();
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) {
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