blob: 3a1f49d936c27310c6f965e21ebe4b25046bacfe [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/accessibility/ax_selection.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/set_selection_options.h"
#include "third_party/blink/renderer/core/editing/text_affinity.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
namespace blink {
namespace {
// TODO(nektar): Add Web tests for this event.
void ScheduleSelectEvent(TextControlElement& text_control) {
Event* event = Event::CreateBubble(event_type_names::kSelect);
event->SetTarget(&text_control);
text_control.GetDocument().EnqueueAnimationFrameEvent(event);
}
// TODO(nektar): Add Web tests for this event.
DispatchEventResult DispatchSelectStart(Node* node) {
if (!node)
return DispatchEventResult::kNotCanceled;
return node->DispatchEvent(
*Event::CreateCancelableBubble(event_type_names::kSelectstart));
}
} // namespace
//
// AXSelection::Builder
//
AXSelection::Builder& AXSelection::Builder::SetBase(const AXPosition& base) {
DCHECK(base.IsValid());
selection_.base_ = base;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetBase(const Position& base) {
const auto ax_base = AXPosition::FromPosition(base);
DCHECK(ax_base.IsValid());
selection_.base_ = ax_base;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetExtent(
const AXPosition& extent) {
DCHECK(extent.IsValid());
selection_.extent_ = extent;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetExtent(const Position& extent) {
const auto ax_extent = AXPosition::FromPosition(extent);
DCHECK(ax_extent.IsValid());
selection_.extent_ = ax_extent;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetSelection(
const SelectionInDOMTree& selection) {
if (selection.IsNone())
return *this;
selection_.base_ = AXPosition::FromPosition(selection.Base());
selection_.extent_ = AXPosition::FromPosition(selection.Extent());
return *this;
}
const AXSelection AXSelection::Builder::Build() {
if (!selection_.Base().IsValid() || !selection_.Extent().IsValid())
return {};
const Document* document = selection_.Base().ContainerObject()->GetDocument();
DCHECK(document);
DCHECK(document->IsActive());
DCHECK(!document->NeedsLayoutTreeUpdate());
// We don't support selections that span across documents.
if (selection_.Extent().ContainerObject()->GetDocument() != document)
return {};
#if DCHECK_IS_ON()
selection_.dom_tree_version_ = document->DomTreeVersion();
selection_.style_version_ = document->StyleVersion();
#endif
return selection_;
}
//
// AXSelection
//
// static
void AXSelection::ClearCurrentSelection(Document& document) {
LocalFrame* frame = document.GetFrame();
if (!frame)
return;
FrameSelection& frame_selection = frame->Selection();
if (!frame_selection.IsAvailable())
return;
frame_selection.Clear();
}
// static
AXSelection AXSelection::FromCurrentSelection(
const Document& document,
const AXSelectionBehavior selection_behavior) {
const LocalFrame* frame = document.GetFrame();
if (!frame)
return {};
const FrameSelection& frame_selection = frame->Selection();
if (!frame_selection.IsAvailable())
return {};
return FromSelection(frame_selection.GetSelectionInDOMTree(),
selection_behavior);
}
// static
AXSelection AXSelection::FromCurrentSelection(
const TextControlElement& text_control) {
const Document& document = text_control.GetDocument();
AXObjectCache* ax_object_cache = document.ExistingAXObjectCache();
if (!ax_object_cache)
return {};
auto* ax_object_cache_impl = static_cast<AXObjectCacheImpl*>(ax_object_cache);
const AXObject* ax_text_control =
ax_object_cache_impl->GetOrCreate(&text_control);
DCHECK(ax_text_control);
// We can't directly use "text_control.Selection()" because the selection it
// returns is inside the shadow DOM and it's not anchored to the text field
// itself.
const TextAffinity extent_affinity = text_control.Selection().Affinity();
const TextAffinity base_affinity =
text_control.selectionStart() == text_control.selectionEnd()
? extent_affinity
: TextAffinity::kDownstream;
const bool is_backward = (text_control.selectionDirection() == "backward");
const auto ax_base = AXPosition::CreatePositionInTextObject(
*ax_text_control,
(is_backward ? int{text_control.selectionEnd()}
: int{text_control.selectionStart()}),
base_affinity);
const auto ax_extent = AXPosition::CreatePositionInTextObject(
*ax_text_control,
(is_backward ? int{text_control.selectionStart()}
: int{text_control.selectionEnd()}),
extent_affinity);
if (!ax_base.IsValid() || !ax_extent.IsValid())
return {};
AXSelection::Builder selection_builder;
selection_builder.SetBase(ax_base).SetExtent(ax_extent);
return selection_builder.Build();
}
// static
AXSelection AXSelection::FromSelection(
const SelectionInDOMTree& selection,
const AXSelectionBehavior selection_behavior) {
if (selection.IsNone())
return {};
DCHECK(selection.AssertValid());
const Position dom_base = selection.Base();
const Position dom_extent = selection.Extent();
const TextAffinity extent_affinity = selection.Affinity();
const TextAffinity base_affinity =
selection.IsCaret() ? extent_affinity : TextAffinity::kDownstream;
AXPositionAdjustmentBehavior base_adjustment =
AXPositionAdjustmentBehavior::kMoveRight;
AXPositionAdjustmentBehavior extent_adjustment =
AXPositionAdjustmentBehavior::kMoveRight;
// If the selection is not collapsed, extend or shrink the DOM selection if
// there is no equivalent selection in the accessibility tree, i.e. if the
// corresponding endpoints are either ignored or unavailable in the
// accessibility tree. If the selection is collapsed, move both endpoints to
// the next valid position in the accessibility tree but do not extend or
// shrink the selection, because this will result in a non-collapsed selection
// in the accessibility tree.
if (!selection.IsCaret()) {
switch (selection_behavior) {
case AXSelectionBehavior::kShrinkToValidRange:
if (selection.IsBaseFirst()) {
base_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
} else {
base_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
}
break;
case AXSelectionBehavior::kExtendToValidRange:
if (selection.IsBaseFirst()) {
base_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
} else {
base_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
}
break;
}
}
const auto ax_base =
AXPosition::FromPosition(dom_base, base_affinity, base_adjustment);
const auto ax_extent =
AXPosition::FromPosition(dom_extent, extent_affinity, extent_adjustment);
if (!ax_base.IsValid() || !ax_extent.IsValid())
return {};
AXSelection::Builder selection_builder;
selection_builder.SetBase(ax_base).SetExtent(ax_extent);
return selection_builder.Build();
}
AXSelection::AXSelection() : base_(), extent_() {
#if DCHECK_IS_ON()
dom_tree_version_ = 0;
style_version_ = 0;
#endif
}
bool AXSelection::IsValid() const {
if (!base_.IsValid() || !extent_.IsValid())
return false;
// We don't support selections that span across documents.
if (base_.ContainerObject()->GetDocument() !=
extent_.ContainerObject()->GetDocument()) {
return false;
}
//
// The following code checks if a text position in a text control is valid.
// Since the contents of a text control are implemented using user agent
// shadow DOM, we want to prevent users from selecting across the shadow DOM
// boundary.
//
// TODO(nektar): Generalize this logic to adjust user selection if it crosses
// disallowed shadow DOM boundaries such as user agent shadow DOM, editing
// boundaries, replaced elements, CSS user-select, etc.
//
if (base_.IsTextPosition() &&
base_.ContainerObject()->IsNativeTextControl() &&
!(base_.ContainerObject() == extent_.ContainerObject() &&
extent_.IsTextPosition() &&
extent_.ContainerObject()->IsNativeTextControl())) {
return false;
}
if (extent_.IsTextPosition() &&
extent_.ContainerObject()->IsNativeTextControl() &&
!(base_.ContainerObject() == extent_.ContainerObject() &&
base_.IsTextPosition() &&
base_.ContainerObject()->IsNativeTextControl())) {
return false;
}
DCHECK(!base_.ContainerObject()->GetDocument()->NeedsLayoutTreeUpdate());
#if DCHECK_IS_ON()
DCHECK_EQ(base_.ContainerObject()->GetDocument()->DomTreeVersion(),
dom_tree_version_);
DCHECK_EQ(base_.ContainerObject()->GetDocument()->StyleVersion(),
style_version_);
#endif // DCHECK_IS_ON()
return true;
}
const SelectionInDOMTree AXSelection::AsSelection(
const AXSelectionBehavior selection_behavior) const {
if (!IsValid())
return {};
AXPositionAdjustmentBehavior base_adjustment =
AXPositionAdjustmentBehavior::kMoveLeft;
AXPositionAdjustmentBehavior extent_adjustment =
AXPositionAdjustmentBehavior::kMoveLeft;
switch (selection_behavior) {
case AXSelectionBehavior::kShrinkToValidRange:
if (base_ < extent_) {
base_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
} else if (base_ > extent_) {
base_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
}
break;
case AXSelectionBehavior::kExtendToValidRange:
if (base_ < extent_) {
base_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
} else if (base_ > extent_) {
base_adjustment = AXPositionAdjustmentBehavior::kMoveRight;
extent_adjustment = AXPositionAdjustmentBehavior::kMoveLeft;
}
break;
}
const auto dom_base = base_.ToPositionWithAffinity(base_adjustment);
const auto dom_extent = extent_.ToPositionWithAffinity(extent_adjustment);
SelectionInDOMTree::Builder selection_builder;
selection_builder.SetBaseAndExtent(dom_base.GetPosition(),
dom_extent.GetPosition());
if (extent_.IsTextPosition())
selection_builder.SetAffinity(extent_.Affinity());
return selection_builder.Build();
}
void AXSelection::UpdateSelectionIfNecessary() {
Document* document = base_.ContainerObject()->GetDocument();
DCHECK(document);
LocalFrameView* view = document->View();
if (!view || !view->LayoutPending())
return;
document->UpdateStyleAndLayout(DocumentUpdateReason::kSelection);
#if DCHECK_IS_ON()
base_.dom_tree_version_ = extent_.dom_tree_version_ = dom_tree_version_ =
document->DomTreeVersion();
base_.style_version_ = extent_.style_version_ = style_version_ =
document->StyleVersion();
#endif // DCHECK_IS_ON()
}
bool AXSelection::Select(const AXSelectionBehavior selection_behavior) {
if (!IsValid()) {
NOTREACHED() << "Trying to select an invalid accessibility selection.";
return false;
}
base::Optional<AXSelection::TextControlSelection> text_control_selection =
AsTextControlSelection();
if (text_control_selection.has_value()) {
DCHECK_LE(text_control_selection->start, text_control_selection->end);
TextControlElement& text_control = ToTextControl(
*base_.ContainerObject()->GetNativeTextControlAncestor()->GetNode());
if (!text_control.SetSelectionRange(text_control_selection->start,
text_control_selection->end,
text_control_selection->direction)) {
return false;
}
// TextControl::SetSelectionRange deliberately does not set focus. But if
// we're updating the selection, the text control should be focused.
ScheduleSelectEvent(text_control);
text_control.focus();
return true;
}
const SelectionInDOMTree old_selection = AsSelection(selection_behavior);
DCHECK(old_selection.AssertValid());
Document* document = old_selection.Base().GetDocument();
if (!document) {
NOTREACHED() << "Valid DOM selections should have an attached document.";
return false;
}
LocalFrame* frame = document->GetFrame();
if (!frame) {
NOTREACHED();
return false;
}
FrameSelection& frame_selection = frame->Selection();
if (!frame_selection.IsAvailable())
return false;
// See the following section in the Selection API Specification:
// https://w3c.github.io/selection-api/#selectstart-event
if (DispatchSelectStart(old_selection.Base().ComputeContainerNode()) !=
DispatchEventResult::kNotCanceled) {
return false;
}
// If the anchor or focus is removed during the "selectstart" event, do
// not proceed.
if (base_.ContainerObject()->IsDetached() ||
extent_.ContainerObject()->IsDetached())
return false;
UpdateSelectionIfNecessary();
// Dispatching the "selectstart" event could potentially change the document
// associated with the current frame.
if (!frame_selection.IsAvailable())
return false;
// Re-retrieve the SelectionInDOMTree in case a DOM mutation took place.
// That way it will also have the updated DOM tree and Style versions,
// and the SelectionTemplate checks for each won't fail.
const SelectionInDOMTree selection = AsSelection(selection_behavior);
SetSelectionOptions::Builder options_builder;
options_builder.SetIsDirectional(true)
.SetShouldCloseTyping(true)
.SetShouldClearTypingStyle(true)
.SetSetSelectionBy(SetSelectionBy::kUser);
frame_selection.SetSelectionForAccessibility(selection,
options_builder.Build());
return true;
}
String AXSelection::ToString() const {
if (!IsValid())
return "Invalid AXSelection";
return "AXSelection from " + Base().ToString() + " to " + Extent().ToString();
}
base::Optional<AXSelection::TextControlSelection>
AXSelection::AsTextControlSelection() const {
if (!IsValid() || !base_.IsTextPosition() || !extent_.IsTextPosition() ||
base_.ContainerObject() != extent_.ContainerObject()) {
return {};
}
const AXObject* text_control =
base_.ContainerObject()->GetNativeTextControlAncestor();
if (!text_control)
return {};
DCHECK(IsTextControl(text_control->GetNode()));
if (base_ <= extent_) {
return TextControlSelection(base_.TextOffset(), extent_.TextOffset(),
kSelectionHasForwardDirection);
}
return TextControlSelection(extent_.TextOffset(), base_.TextOffset(),
kSelectionHasBackwardDirection);
}
bool operator==(const AXSelection& a, const AXSelection& b) {
DCHECK(a.IsValid() && b.IsValid());
return a.Base() == b.Base() && a.Extent() == b.Extent();
}
bool operator!=(const AXSelection& a, const AXSelection& b) {
return !(a == b);
}
std::ostream& operator<<(std::ostream& ostream, const AXSelection& selection) {
return ostream << selection.ToString().Utf8();
}
} // namespace blink