blob: 4c3b4edb7118778933c03eff2e3286c6a6a9ec4b [file] [log] [blame]
/*
* Copyright (C) 2004-2020 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 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 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 "config.h"
#include "FrameSelection.h"
#include "AXObjectCache.h"
#include "CharacterData.h"
#include "ColorBlending.h"
#include "DeleteSelectionCommand.h"
#include "DocumentInlines.h"
#include "Editing.h"
#include "Editor.h"
#include "EditorClient.h"
#include "Element.h"
#include "ElementIterator.h"
#include "Event.h"
#include "EventNames.h"
#include "FloatQuad.h"
#include "FocusController.h"
#include "Frame.h"
#include "FrameTree.h"
#include "FrameView.h"
#include "GraphicsContext.h"
#include "HTMLBodyElement.h"
#include "HTMLFormElement.h"
#include "HTMLFrameElement.h"
#include "HTMLIFrameElement.h"
#include "HTMLNames.h"
#include "HTMLSelectElement.h"
#include "HitTestRequest.h"
#include "HitTestResult.h"
#include "ImageOverlay.h"
#include "InlineRunAndOffset.h"
#include "LegacyInlineTextBox.h"
#include "Logging.h"
#include "Page.h"
#include "Range.h"
#include "RenderLayer.h"
#include "RenderLayerScrollableArea.h"
#include "RenderText.h"
#include "RenderTextControl.h"
#include "RenderTheme.h"
#include "RenderView.h"
#include "RenderWidget.h"
#include "RenderedPosition.h"
#include "ScriptDisallowedScope.h"
#include "Settings.h"
#include "SimpleRange.h"
#include "SpatialNavigation.h"
#include "StyleProperties.h"
#include "StyleTreeResolver.h"
#include "TypingCommand.h"
#include "VisibleUnits.h"
#include <stdio.h>
#include <wtf/text/CString.h>
#include <wtf/text/TextStream.h>
#if PLATFORM(IOS_FAMILY)
#include "Chrome.h"
#include "ChromeClient.h"
#include "Color.h"
#include "RenderObject.h"
#include "RenderStyle.h"
#include "SelectionGeometry.h"
#endif
namespace WebCore {
using namespace HTMLNames;
static inline LayoutUnit NoXPosForVerticalArrowNavigation()
{
return LayoutUnit::min();
}
CaretBase::CaretBase(CaretVisibility visibility)
: m_caretRectNeedsUpdate(true)
, m_caretVisibility(visibility)
{
}
DragCaretController::DragCaretController()
: CaretBase(Visible)
{
}
bool DragCaretController::isContentRichlyEditable() const
{
return isRichlyEditablePosition(m_position.deepEquivalent());
}
IntRect DragCaretController::caretRectInRootViewCoordinates() const
{
if (!hasCaret())
return { };
if (auto* document = m_position.deepEquivalent().document()) {
if (auto* documentView = document->view())
return documentView->contentsToRootView(m_position.absoluteCaretBounds());
}
return { };
}
IntRect DragCaretController::editableElementRectInRootViewCoordinates() const
{
if (!hasCaret())
return { };
RefPtr<ContainerNode> editableContainer;
if (auto* formControl = enclosingTextFormControl(m_position.deepEquivalent()))
editableContainer = formControl;
else
editableContainer = highestEditableRoot(m_position.deepEquivalent());
if (!editableContainer)
return { };
auto* renderer = editableContainer->renderer();
if (!renderer)
return { };
if (auto* view = editableContainer->document().view())
return view->contentsToRootView(renderer->absoluteBoundingBoxRect()); // FIXME: Wrong for elements with visible layout overflow.
return { };
}
static inline bool shouldAlwaysUseDirectionalSelection(Document* document)
{
return !document || document->editor().behavior().shouldConsiderSelectionAsDirectional();
}
FrameSelection::FrameSelection(Document* document)
: m_document(document)
, m_granularity(TextGranularity::CharacterGranularity)
#if ENABLE(TEXT_CARET)
, m_caretBlinkTimer(*this, &FrameSelection::caretBlinkTimerFired)
#endif
, m_appearanceUpdateTimer(*this, &FrameSelection::appearanceUpdateTimerFired)
, m_caretInsidePositionFixed(false)
, m_absCaretBoundsDirty(true)
, m_caretPaint(true)
, m_isCaretBlinkingSuspended(false)
, m_focused(document && document->frame() && document->page() && document->page()->focusController().focusedFrame() == document->frame())
, m_shouldShowBlockCursor(false)
, m_pendingSelectionUpdate(false)
, m_alwaysAlignCursorOnScrollWhenRevealingSelection(false)
#if PLATFORM(IOS_FAMILY)
, m_updateAppearanceEnabled(false)
, m_caretBlinks(true)
#endif
{
if (shouldAlwaysUseDirectionalSelection(m_document.get()))
m_selection.setIsDirectional(true);
bool activeAndFocused = isFocusedAndActive();
if (activeAndFocused)
setSelectionFromNone();
#if USE(UIKIT_EDITING)
// Caret blinking (blinks | does not blink)
setCaretVisible(activeAndFocused);
#else
setCaretVisibility(activeAndFocused ? Visible : Hidden, ShouldUpdateAppearance::No);
#endif
}
Element* FrameSelection::rootEditableElementOrDocumentElement() const
{
Element* selectionRoot = m_selection.rootEditableElement();
return selectionRoot ? selectionRoot : m_document->documentElement();
}
void FrameSelection::moveTo(const VisiblePosition& position, EUserTriggered userTriggered, CursorAlignOnScroll align)
{
setSelection(VisibleSelection(position.deepEquivalent(), position.deepEquivalent(), position.affinity(), m_selection.isDirectional()),
defaultSetSelectionOptions(userTriggered), AXTextStateChangeIntent(), align);
}
void FrameSelection::moveTo(const VisiblePosition& base, const VisiblePosition& extent, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(base.deepEquivalent(), extent.deepEquivalent(), base.affinity(), selectionHasDirection), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::moveTo(const Position& position, Affinity affinity, EUserTriggered userTriggered)
{
setSelection(VisibleSelection(position, affinity, m_selection.isDirectional()), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::moveTo(const Position& base, const Position& extent, Affinity affinity, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(base, extent, affinity, selectionHasDirection), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::moveWithoutValidationTo(const Position& base, const Position& extent, bool selectionHasDirection, bool shouldSetFocus, SelectionRevealMode revealMode, const AXTextStateChangeIntent& intent)
{
VisibleSelection newSelection;
newSelection.setWithoutValidation(base, extent);
newSelection.setIsDirectional(selectionHasDirection);
AXTextStateChangeIntent newIntent = intent.type == AXTextStateChangeTypeUnknown ? AXTextStateChangeIntent(AXTextStateChangeTypeSelectionMove, AXTextSelection { AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, false }) : intent;
auto options = defaultSetSelectionOptions();
if (!shouldSetFocus)
options.add(DoNotSetFocus);
switch (revealMode) {
case SelectionRevealMode::DoNotReveal:
break;
case SelectionRevealMode::Reveal:
options.add(RevealSelection);
break;
case SelectionRevealMode::RevealUpToMainFrame:
options.add(RevealSelectionUpToMainFrame);
break;
case SelectionRevealMode::DelegateMainFrameScroll:
options.add(DelegateMainFrameScroll);
break;
}
setSelection(newSelection, options, newIntent);
}
void DragCaretController::setCaretPosition(const VisiblePosition& position)
{
if (Node* node = m_position.deepEquivalent().deprecatedNode())
invalidateCaretRect(node);
m_position = position;
setCaretRectNeedsUpdate();
Document* document = nullptr;
if (Node* node = m_position.deepEquivalent().deprecatedNode()) {
invalidateCaretRect(node);
document = &node->document();
}
if (m_position.isNull() || m_position.isOrphan())
clearCaretRect();
else
updateCaretRect(*document, m_position);
}
static void adjustEndpointsAtBidiBoundary(VisiblePosition& visibleBase, VisiblePosition& visibleExtent)
{
RenderedPosition base(visibleBase);
RenderedPosition extent(visibleExtent);
if (base.isNull() || extent.isNull() || base.isEquivalent(extent))
return;
if (base.atLeftBoundaryOfBidiRun()) {
if (!extent.atRightBoundaryOfBidiRun(base.bidiLevelOnRight())
&& base.isEquivalent(extent.leftBoundaryOfBidiRun(base.bidiLevelOnRight()))) {
visibleBase = base.positionAtLeftBoundaryOfBiDiRun();
return;
}
return;
}
if (base.atRightBoundaryOfBidiRun()) {
if (!extent.atLeftBoundaryOfBidiRun(base.bidiLevelOnLeft())
&& base.isEquivalent(extent.rightBoundaryOfBidiRun(base.bidiLevelOnLeft()))) {
visibleBase = base.positionAtRightBoundaryOfBiDiRun();
return;
}
return;
}
if (extent.atLeftBoundaryOfBidiRun() && extent.isEquivalent(base.leftBoundaryOfBidiRun(extent.bidiLevelOnRight()))) {
visibleExtent = extent.positionAtLeftBoundaryOfBiDiRun();
return;
}
if (extent.atRightBoundaryOfBidiRun() && extent.isEquivalent(base.rightBoundaryOfBidiRun(extent.bidiLevelOnLeft()))) {
visibleExtent = extent.positionAtRightBoundaryOfBiDiRun();
return;
}
}
void FrameSelection::setSelectionByMouseIfDifferent(const VisibleSelection& passedNewSelection, TextGranularity granularity,
EndPointsAdjustmentMode endpointsAdjustmentMode)
{
VisibleSelection newSelection = passedNewSelection;
bool isDirectional = shouldAlwaysUseDirectionalSelection(m_document.get()) || newSelection.isDirectional();
VisiblePosition base = m_originalBase.isNotNull() ? m_originalBase : newSelection.visibleBase();
VisiblePosition newBase = base;
VisiblePosition extent = newSelection.visibleExtent();
VisiblePosition newExtent = extent;
if (endpointsAdjustmentMode == EndPointsAdjustmentMode::AdjustAtBidiBoundary)
adjustEndpointsAtBidiBoundary(newBase, newExtent);
if (newBase != base || newExtent != extent) {
m_originalBase = base;
newSelection.setBase(newBase);
newSelection.setExtent(newExtent);
} else if (m_originalBase.isNotNull()) {
if (m_selection.base() == newSelection.base())
newSelection.setBase(m_originalBase);
m_originalBase = { };
}
newSelection.setIsDirectional(isDirectional); // Adjusting base and extent will make newSelection always directional
if (m_selection == newSelection || !shouldChangeSelection(newSelection))
return;
AXTextStateChangeIntent intent;
if (AXObjectCache::accessibilityEnabled() && newSelection.isCaret())
intent = AXTextStateChangeIntent(AXTextStateChangeTypeSelectionMove, AXTextSelection { AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, false });
else
intent = AXTextStateChangeIntent();
setSelection(newSelection, defaultSetSelectionOptions() | FireSelectEvent, intent, AlignCursorOnScrollIfNeeded, granularity);
}
bool FrameSelection::setSelectionWithoutUpdatingAppearance(const VisibleSelection& newSelectionPossiblyWithoutDirection, OptionSet<SetSelectionOption> options, CursorAlignOnScroll align, TextGranularity granularity)
{
bool closeTyping = options.contains(CloseTyping);
bool shouldClearTypingStyle = options.contains(ClearTypingStyle);
VisibleSelection newSelection = newSelectionPossiblyWithoutDirection;
if (shouldAlwaysUseDirectionalSelection(m_document.get()))
newSelection.setIsDirectional(true);
// <http://bugs.webkit.org/show_bug.cgi?id=23464>: Infinite recursion at FrameSelection::setSelection
// if document->frame() == m_document->frame() we can get into an infinite loop
if (Document* newSelectionDocument = newSelection.base().document()) {
if (RefPtr<Frame> newSelectionFrame = newSelectionDocument->frame()) {
if (m_document && newSelectionFrame != m_document->frame() && newSelectionDocument != m_document) {
newSelectionDocument->selection().setSelection(newSelection, options, AXTextStateChangeIntent(), align, granularity);
// It's possible that during the above set selection, this FrameSelection has been modified by
// selectFrameElementInParentIfFullySelected, but that the selection is no longer valid since
// the frame is about to be destroyed. If this is the case, clear our selection.
if (newSelectionFrame->hasOneRef() && m_selection.isNoneOrOrphaned())
clear();
return false;
}
}
}
VisibleSelection oldSelection = m_selection;
bool willMutateSelection = oldSelection != newSelection;
if (willMutateSelection && m_document)
m_document->editor().selectionWillChange();
{
ScriptDisallowedScope::InMainThread scriptDisallowedScope;
if (newSelection.isOrphan()) {
ASSERT_NOT_REACHED();
clear();
return false;
}
if (!m_document || (!m_document->frame() && !newSelection.document())) {
m_selection = newSelection;
updateAssociatedLiveRange();
return false;
}
bool selectionEndpointsBelongToMultipleDocuments = newSelection.base().document() && !newSelection.document();
bool selectionIsInAnotherDocument = newSelection.document() && newSelection.document() != m_document.get();
bool selectionIsInDetachedDocument = newSelection.document() && !newSelection.document()->frame();
if (selectionEndpointsBelongToMultipleDocuments || selectionIsInAnotherDocument || selectionIsInDetachedDocument) {
clear();
return false;
}
ASSERT(m_document->frame());
if (closeTyping)
TypingCommand::closeTyping(*m_document);
if (shouldClearTypingStyle)
clearTypingStyle();
m_granularity = granularity;
m_selection = newSelection;
updateAssociatedLiveRange();
}
// Selection offsets should increase when LF is inserted before the caret in InsertLineBreakCommand. See <https://webkit.org/b/56061>.
if (HTMLTextFormControlElement* textControl = enclosingTextFormControl(newSelection.start()))
textControl->selectionChanged(options.contains(FireSelectEvent));
if (!willMutateSelection)
return false;
setCaretRectNeedsUpdate();
if (!newSelection.isNone() && !(options & DoNotSetFocus)) {
auto* oldFocusedElement = m_document->focusedElement();
setFocusedElementIfNeeded();
if (!m_document->frame())
return false;
// FIXME: Should not be needed.
if (m_document->focusedElement() != oldFocusedElement)
m_document->updateStyleIfNeeded();
}
// Always clear the x position used for vertical arrow navigation.
// It will be restored by the vertical arrow navigation code if necessary.
m_xPosForVerticalArrowNavigation = std::nullopt;
selectFrameElementInParentIfFullySelected();
m_document->editor().respondToChangedSelection(oldSelection, options);
// https://www.w3.org/TR/selection-api/#selectionchange-event
// FIXME: Spec doesn't specify which task source to use.
m_document->queueTaskToDispatchEvent(TaskSource::UserInteraction, Event::create(eventNames().selectionchangeEvent, Event::CanBubble::No, Event::IsCancelable::No));
return true;
}
void FrameSelection::setSelection(const VisibleSelection& selection, OptionSet<SetSelectionOption> options, AXTextStateChangeIntent intent, CursorAlignOnScroll align, TextGranularity granularity)
{
LOG_WITH_STREAM(Selection, stream << "FrameSelection::setSelection " << selection);
RefPtr protectedDocument { m_document.get() };
if (!setSelectionWithoutUpdatingAppearance(selection, options, align, granularity))
return;
if (options & RevealSelectionUpToMainFrame)
m_selectionRevealMode = SelectionRevealMode::RevealUpToMainFrame;
else if (options & RevealSelection)
m_selectionRevealMode = SelectionRevealMode::Reveal;
else if (options & DelegateMainFrameScroll)
m_selectionRevealMode = SelectionRevealMode::DelegateMainFrameScroll;
else
m_selectionRevealMode = SelectionRevealMode::DoNotReveal;
m_alwaysAlignCursorOnScrollWhenRevealingSelection = align == AlignCursorOnScrollAlways;
m_selectionRevealIntent = intent;
m_pendingSelectionUpdate = true;
if (protectedDocument->hasPendingStyleRecalc())
return;
auto frameView = protectedDocument->view();
if (frameView && frameView->layoutContext().isLayoutPending())
return;
if (!(options & IsUserTriggered)) {
scheduleAppearanceUpdateAfterStyleChange();
return;
}
updateAndRevealSelection(intent, options.contains(SmoothScroll) ? ScrollBehavior::Smooth : ScrollBehavior::Instant, options.contains(RevealSelectionBounds) ? RevealExtentOption::DoNotRevealExtent : RevealExtentOption::RevealExtent);
if (options & IsUserTriggered) {
if (auto* client = protectedDocument->editor().client())
client->didEndUserTriggeredSelectionChanges();
}
}
void FrameSelection::updateSelectionAppearanceNow()
{
if (!m_document || !m_document->hasLivingRenderTree())
return;
Ref document = *m_document;
#if ENABLE(TEXT_CARET)
document->updateLayoutIgnorePendingStylesheets();
#else
document->updateStyleIfNeeded();
#endif
if (m_pendingSelectionUpdate)
updateAppearance();
}
void FrameSelection::setNeedsSelectionUpdate(RevealSelectionAfterUpdate revealMode)
{
m_selectionRevealIntent = AXTextStateChangeIntent();
if (revealMode == RevealSelectionAfterUpdate::Forced)
m_selectionRevealMode = SelectionRevealMode::Reveal;
m_pendingSelectionUpdate = true;
if (RenderView* view = m_document->renderView())
view->selection().clear();
}
void FrameSelection::updateAndRevealSelection(const AXTextStateChangeIntent& intent, ScrollBehavior scrollBehavior, RevealExtentOption revealExtent)
{
if (!m_pendingSelectionUpdate)
return;
m_pendingSelectionUpdate = false;
updateAppearance();
if (m_selectionRevealMode != SelectionRevealMode::DoNotReveal) {
ScrollAlignment alignment;
if (m_document->editor().behavior().shouldCenterAlignWhenSelectionIsRevealed())
alignment = m_alwaysAlignCursorOnScrollWhenRevealingSelection ? ScrollAlignment::alignCenterAlways : ScrollAlignment::alignCenterIfNeeded;
else
alignment = m_alwaysAlignCursorOnScrollWhenRevealingSelection ? ScrollAlignment::alignTopAlways : ScrollAlignment::alignToEdgeIfNeeded;
revealSelection(m_selectionRevealMode, alignment, revealExtent, scrollBehavior);
}
if (!m_document->editor().ignoreSelectionChanges())
notifyAccessibilityForSelectionChange(intent);
}
void FrameSelection::updateDataDetectorsForSelection()
{
#if ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS_FAMILY)
m_document->editor().scanSelectionForTelephoneNumbers();
#endif
}
static bool removingNodeRemovesPosition(Node& node, const Position& position)
{
if (!position.anchorNode())
return false;
if (position.anchorNode() == &node)
return true;
if (!is<Element>(node))
return false;
return downcast<Element>(node).containsIncludingShadowDOM(position.anchorNode());
}
void DragCaretController::nodeWillBeRemoved(Node& node)
{
if (!hasCaret() || !node.isConnected())
return;
if (!removingNodeRemovesPosition(node, m_position.deepEquivalent()))
return;
if (RenderView* view = node.document().renderView())
view->selection().clear();
// It's important to avoid updating style or layout here, since we're in the middle of removing the node from the document.
clearCaretPositionWithoutUpdatingStyle();
}
void DragCaretController::clearCaretPositionWithoutUpdatingStyle()
{
if (RefPtr node = m_position.deepEquivalent().anchorNode())
invalidateCaretRect(node.get(), true);
m_position = { };
clearCaretRect();
}
void FrameSelection::nodeWillBeRemoved(Node& node)
{
// There can't be a selection inside a fragment, so if a fragment's node is being removed,
// the selection in the document that created the fragment needs no adjustment.
if ((isNone() && !m_document->settings().liveRangeSelectionEnabled()) || !node.isConnected())
return;
respondToNodeModification(node, removingNodeRemovesPosition(node, m_selection.anchor()), removingNodeRemovesPosition(node, m_selection.focus()),
removingNodeRemovesPosition(node, m_selection.base()), removingNodeRemovesPosition(node, m_selection.extent()),
removingNodeRemovesPosition(node, m_selection.start()), removingNodeRemovesPosition(node, m_selection.end()));
}
void FrameSelection::respondToNodeModification(Node& node, bool anchorRemoved, bool focusRemoved, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved)
{
bool clearRenderTreeSelection = false;
bool clearDOMTreeSelection = false;
if (m_document->settings().liveRangeSelectionEnabled() && (anchorRemoved || focusRemoved)) {
Position anchor = m_selection.anchor();
Position focus = m_selection.focus();
if (anchorRemoved)
updatePositionForNodeRemoval(anchor, node);
if (focusRemoved)
updatePositionForNodeRemoval(focus, node);
if (anchor.isNotNull() && focus.isNotNull())
m_selection.setWithoutValidation(anchor, focus);
else
clearDOMTreeSelection = true;
clearRenderTreeSelection = true;
} if (startRemoved || endRemoved) {
Position start = m_selection.start();
Position end = m_selection.end();
if (startRemoved)
updatePositionForNodeRemoval(start, node);
if (endRemoved)
updatePositionForNodeRemoval(end, node);
if (start.isNotNull() && end.isNotNull()) {
if (m_selection.isBaseFirst())
m_selection.setWithoutValidation(start, end);
else
m_selection.setWithoutValidation(end, start);
} else
clearDOMTreeSelection = true;
clearRenderTreeSelection = true;
} else if (baseRemoved || extentRemoved) {
// The base and/or extent are about to be removed, but the start and end aren't.
// Change the base and extent to the start and end, but don't re-validate the
// selection, since doing so could move the start and end into the node
// that is about to be removed.
if (m_selection.isBaseFirst())
m_selection.setWithoutValidation(m_selection.start(), m_selection.end());
else
m_selection.setWithoutValidation(m_selection.end(), m_selection.start());
} else if (isRange()) {
if (auto range = m_selection.firstRange(); range && intersects<ComposedTree>(*range, node)) {
// If we did nothing here, when this node's renderer was destroyed, the rect that it
// occupied would be invalidated, but, selection gaps that change as a result of
// the removal wouldn't be invalidated.
// FIXME: Don't do so much unnecessary invalidation.
clearRenderTreeSelection = true;
}
}
if (clearRenderTreeSelection) {
if (auto* renderView = node.document().renderView()) {
renderView->selection().clear();
// Trigger a selection update so the selection will be set again.
m_selectionRevealIntent = AXTextStateChangeIntent();
m_pendingSelectionUpdate = true;
renderView->frameView().scheduleSelectionUpdate();
}
}
if (clearDOMTreeSelection)
setSelection(VisibleSelection(), DoNotSetFocus);
}
static void updatePositionAfterAdoptingTextReplacement(Position& position, CharacterData& node, unsigned offset, unsigned oldLength, unsigned newLength)
{
if (position.anchorNode() != &node || position.anchorType() != Position::PositionIsOffsetInAnchor)
return;
// See: http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation
ASSERT(position.offsetInContainerNode() >= 0);
unsigned positionOffset = static_cast<unsigned>(position.offsetInContainerNode());
// Replacing text can be viewed as a deletion followed by insertion.
if (positionOffset >= offset && positionOffset <= offset + oldLength)
position.moveToOffset(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 (positionOffset > offset + oldLength)
position.moveToOffset(positionOffset - oldLength + newLength);
ASSERT(static_cast<unsigned>(position.offsetInContainerNode()) <= node.length());
}
void FrameSelection::textWasReplaced(CharacterData& node, unsigned offset, unsigned oldLength, unsigned newLength)
{
if (isNone() || !node.isConnected())
return;
Position base = m_selection.base();
Position extent = m_selection.extent();
Position start = m_selection.start();
Position end = m_selection.end();
updatePositionAfterAdoptingTextReplacement(base, node, offset, oldLength, newLength);
updatePositionAfterAdoptingTextReplacement(extent, node, offset, oldLength, newLength);
updatePositionAfterAdoptingTextReplacement(start, node, offset, oldLength, newLength);
updatePositionAfterAdoptingTextReplacement(end, node, offset, oldLength, newLength);
if (base != m_selection.base() || extent != m_selection.extent() || start != m_selection.start() || end != m_selection.end()) {
VisibleSelection newSelection;
if (base != extent)
newSelection.setWithoutValidation(base, extent);
else if (m_selection.isDirectional() && !m_selection.isBaseFirst())
newSelection.setWithoutValidation(end, start);
else
newSelection.setWithoutValidation(start, end);
setSelection(newSelection, DoNotSetFocus);
}
}
TextDirection FrameSelection::directionOfEnclosingBlock()
{
return WebCore::directionOfEnclosingBlock(m_selection.extent());
}
TextDirection FrameSelection::directionOfSelection()
{
// Get bot VisiblePositions first because visibleStart() and visibleEnd()
// can cause layout, which has the potential to invalidate lineboxes.
auto startPosition = m_selection.visibleStart();
auto endPosition = m_selection.visibleEnd();
auto startBox = startPosition.inlineBoxAndOffset().box;
auto endBox = endPosition.inlineBoxAndOffset().box;
if (startBox && endBox && startBox->direction() == endBox->direction())
return startBox->direction();
return directionOfEnclosingBlock();
}
void FrameSelection::willBeModified(EAlteration alter, SelectionDirection direction)
{
if (alter != AlterationExtend)
return;
Position start = m_selection.start();
Position end = m_selection.end();
bool baseIsStart = true;
if (m_selection.isDirectional()) {
// Make base and extent match start and end so we extend the user-visible selection.
// This only matters for cases where base and extend point to different positions than
// start and end (e.g. after a double-click to select a word).
if (m_selection.isBaseFirst())
baseIsStart = true;
else
baseIsStart = false;
} else {
switch (direction) {
case SelectionDirection::Right:
if (directionOfSelection() == TextDirection::LTR)
baseIsStart = true;
else
baseIsStart = false;
break;
case SelectionDirection::Forward:
baseIsStart = true;
break;
case SelectionDirection::Left:
if (directionOfSelection() == TextDirection::LTR)
baseIsStart = false;
else
baseIsStart = true;
break;
case SelectionDirection::Backward:
baseIsStart = false;
break;
}
}
if (baseIsStart) {
m_selection.setBase(start);
m_selection.setExtent(end);
} else {
m_selection.setBase(end);
m_selection.setExtent(start);
}
}
VisiblePosition FrameSelection::positionForPlatform(bool isGetStart) const
{
// FIXME: VisibleSelection should be fixed to ensure as an invariant that
// base/extent always point to the same nodes as start/end, but which points
// to which depends on the value of isBaseFirst. Then this can be changed
// to just return m_sel.extent().
if (m_document && m_document->editor().behavior().shouldAlwaysExtendSelectionFromExtentEndpoint())
return m_selection.isBaseFirst() ? m_selection.visibleEnd() : m_selection.visibleStart();
return isGetStart ? m_selection.visibleStart() : m_selection.visibleEnd();
}
VisiblePosition FrameSelection::startForPlatform() const
{
return positionForPlatform(true);
}
VisiblePosition FrameSelection::endForPlatform() const
{
return positionForPlatform(false);
}
VisiblePosition FrameSelection::nextWordPositionForPlatform(const VisiblePosition& originalPosition)
{
VisiblePosition positionAfterCurrentWord = nextWordPosition(originalPosition);
if (m_document && m_document->editor().behavior().shouldSkipSpaceWhenMovingRight()) {
// In order to skip spaces when moving right, we advance one
// word further and then move one word back. Given the
// semantics of previousWordPosition() this will put us at the
// beginning of the word following.
VisiblePosition positionAfterSpacingAndFollowingWord = nextWordPosition(positionAfterCurrentWord);
if (positionAfterSpacingAndFollowingWord != positionAfterCurrentWord)
positionAfterCurrentWord = previousWordPosition(positionAfterSpacingAndFollowingWord);
bool movingBackwardsMovedPositionToStartOfCurrentWord = positionAfterCurrentWord == previousWordPosition(nextWordPosition(originalPosition));
if (movingBackwardsMovedPositionToStartOfCurrentWord)
positionAfterCurrentWord = positionAfterSpacingAndFollowingWord;
}
return positionAfterCurrentWord;
}
static void adjustPositionForUserSelectAll(VisiblePosition& pos, bool isForward)
{
if (Node* rootUserSelectAll = Position::rootUserSelectAllForNode(pos.deepEquivalent().anchorNode()))
pos = isForward ? positionAfterNode(rootUserSelectAll).downstream(CanCrossEditingBoundary) : positionBeforeNode(rootUserSelectAll).upstream(CanCrossEditingBoundary);
}
VisiblePosition FrameSelection::modifyExtendingRight(TextGranularity granularity)
{
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
// The difference between modifyExtendingRight and modifyExtendingForward is:
// modifyExtendingForward always extends forward logically.
// modifyExtendingRight behaves the same as modifyExtendingForward except for extending character or word,
// it extends forward logically if the enclosing block is TextDirection::LTR,
// but it extends backward logically if the enclosing block is TextDirection::RTL.
switch (granularity) {
case TextGranularity::CharacterGranularity:
if (directionOfEnclosingBlock() == TextDirection::LTR)
pos = pos.next(CannotCrossEditingBoundary);
else
pos = pos.previous(CannotCrossEditingBoundary);
break;
case TextGranularity::WordGranularity:
if (directionOfEnclosingBlock() == TextDirection::LTR)
pos = nextWordPositionForPlatform(pos);
else
pos = previousWordPosition(pos);
break;
case TextGranularity::LineBoundary:
if (directionOfEnclosingBlock() == TextDirection::LTR)
pos = modifyExtendingForward(granularity);
else
pos = modifyExtendingBackward(granularity);
break;
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
// FIXME: implement all of the above?
pos = modifyExtendingForward(granularity);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
adjustPositionForUserSelectAll(pos, directionOfEnclosingBlock() == TextDirection::LTR);
return pos;
}
VisiblePosition FrameSelection::modifyExtendingForward(TextGranularity granularity)
{
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
switch (granularity) {
case TextGranularity::CharacterGranularity:
pos = pos.next(CannotCrossEditingBoundary);
break;
case TextGranularity::WordGranularity:
pos = nextWordPositionForPlatform(pos);
break;
case TextGranularity::SentenceGranularity:
pos = nextSentencePosition(pos);
break;
case TextGranularity::LineGranularity:
pos = nextLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent));
break;
case TextGranularity::ParagraphGranularity:
pos = nextParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent));
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
case TextGranularity::SentenceBoundary:
pos = endOfSentence(endForPlatform());
break;
case TextGranularity::LineBoundary:
pos = logicalEndOfLine(endForPlatform());
break;
case TextGranularity::ParagraphBoundary:
pos = endOfParagraph(endForPlatform());
break;
case TextGranularity::DocumentBoundary:
pos = endForPlatform();
if (isEditablePosition(pos.deepEquivalent()))
pos = endOfEditableContent(pos);
else
pos = endOfDocument(pos);
break;
}
adjustPositionForUserSelectAll(pos, directionOfEnclosingBlock() == TextDirection::LTR);
return pos;
}
VisiblePosition FrameSelection::modifyMovingRight(TextGranularity granularity, bool* reachedBoundary)
{
if (reachedBoundary)
*reachedBoundary = false;
VisiblePosition pos;
switch (granularity) {
case TextGranularity::CharacterGranularity:
if (isRange()) {
if (directionOfSelection() == TextDirection::LTR)
pos = VisiblePosition(m_selection.end(), m_selection.affinity());
else
pos = VisiblePosition(m_selection.start(), m_selection.affinity());
} else
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).right(true, reachedBoundary);
break;
case TextGranularity::WordGranularity: {
bool skipsSpaceWhenMovingRight = m_document && m_document->editor().behavior().shouldSkipSpaceWhenMovingRight();
VisiblePosition currentPosition(m_selection.extent(), m_selection.affinity());
pos = rightWordPosition(currentPosition, skipsSpaceWhenMovingRight);
if (reachedBoundary)
*reachedBoundary = pos == currentPosition;
break;
}
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
// FIXME: Implement all of the above.
pos = modifyMovingForward(granularity, reachedBoundary);
break;
case TextGranularity::LineBoundary:
pos = rightBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock(), reachedBoundary);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
return pos;
}
VisiblePosition FrameSelection::modifyMovingForward(TextGranularity granularity, bool* reachedBoundary)
{
if (reachedBoundary)
*reachedBoundary = false;
VisiblePosition currentPosition;
switch (granularity) {
case TextGranularity::WordGranularity:
case TextGranularity::SentenceGranularity:
currentPosition = VisiblePosition(m_selection.extent(), m_selection.affinity());
break;
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
currentPosition = endForPlatform();
break;
default:
break;
}
VisiblePosition pos;
// FIXME: Stay in editable content for the less common granularities.
switch (granularity) {
case TextGranularity::CharacterGranularity:
if (isRange())
pos = VisiblePosition(m_selection.end(), m_selection.affinity());
else
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).next(CannotCrossEditingBoundary, reachedBoundary);
break;
case TextGranularity::WordGranularity:
pos = nextWordPositionForPlatform(currentPosition);
break;
case TextGranularity::SentenceGranularity:
pos = nextSentencePosition(currentPosition);
break;
case TextGranularity::LineGranularity: {
// down-arrowing from a range selection that ends at the start of a line needs
// to leave the selection at that line start (no need to call nextLinePosition!)
pos = currentPosition;
if (!isRange() || !isStartOfLine(pos))
pos = nextLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(Start));
break;
}
case TextGranularity::ParagraphGranularity:
pos = nextParagraphPosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(Start));
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
case TextGranularity::SentenceBoundary:
pos = endOfSentence(currentPosition);
break;
case TextGranularity::LineBoundary:
pos = logicalEndOfLine(endForPlatform(), reachedBoundary);
break;
case TextGranularity::ParagraphBoundary:
pos = endOfParagraph(currentPosition);
break;
case TextGranularity::DocumentBoundary:
pos = currentPosition;
if (isEditablePosition(pos.deepEquivalent()))
pos = endOfEditableContent(pos);
else
pos = endOfDocument(pos);
break;
}
switch (granularity) {
case TextGranularity::WordGranularity:
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
if (reachedBoundary)
*reachedBoundary = pos == currentPosition;
break;
default:
break;
}
return pos;
}
VisiblePosition FrameSelection::modifyExtendingLeft(TextGranularity granularity)
{
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
// The difference between modifyExtendingLeft and modifyExtendingBackward is:
// modifyExtendingBackward always extends backward logically.
// modifyExtendingLeft behaves the same as modifyExtendingBackward except for extending character or word,
// it extends backward logically if the enclosing block is TextDirection::LTR,
// but it extends forward logically if the enclosing block is TextDirection::RTL.
switch (granularity) {
case TextGranularity::CharacterGranularity:
if (directionOfEnclosingBlock() == TextDirection::LTR)
pos = pos.previous(CannotCrossEditingBoundary);
else
pos = pos.next(CannotCrossEditingBoundary);
break;
case TextGranularity::WordGranularity:
if (directionOfEnclosingBlock() == TextDirection::LTR)
pos = previousWordPosition(pos);
else
pos = nextWordPositionForPlatform(pos);
break;
case TextGranularity::LineBoundary:
if (directionOfEnclosingBlock() == TextDirection::LTR)
pos = modifyExtendingBackward(granularity);
else
pos = modifyExtendingForward(granularity);
break;
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
pos = modifyExtendingBackward(granularity);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
adjustPositionForUserSelectAll(pos, !(directionOfEnclosingBlock() == TextDirection::LTR));
return pos;
}
VisiblePosition FrameSelection::modifyExtendingBackward(TextGranularity granularity)
{
VisiblePosition pos(m_selection.extent(), m_selection.affinity());
// Extending a selection backward by word or character from just after a table selects
// the table. This "makes sense" from the user perspective, esp. when deleting.
// It was done here instead of in VisiblePosition because we want VPs to iterate
// over everything.
switch (granularity) {
case TextGranularity::CharacterGranularity:
pos = pos.previous(CannotCrossEditingBoundary);
break;
case TextGranularity::WordGranularity:
pos = previousWordPosition(pos);
break;
case TextGranularity::SentenceGranularity:
pos = previousSentencePosition(pos);
break;
case TextGranularity::LineGranularity:
pos = previousLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent));
break;
case TextGranularity::ParagraphGranularity:
pos = previousParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent));
break;
case TextGranularity::SentenceBoundary:
pos = startOfSentence(startForPlatform());
break;
case TextGranularity::LineBoundary:
pos = logicalStartOfLine(startForPlatform());
break;
case TextGranularity::ParagraphBoundary:
pos = startOfParagraph(startForPlatform());
break;
case TextGranularity::DocumentBoundary:
pos = startForPlatform();
if (isEditablePosition(pos.deepEquivalent()))
pos = startOfEditableContent(pos);
else
pos = startOfDocument(pos);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
adjustPositionForUserSelectAll(pos, !(directionOfEnclosingBlock() == TextDirection::LTR));
return pos;
}
VisiblePosition FrameSelection::modifyMovingLeft(TextGranularity granularity, bool* reachedBoundary)
{
if (reachedBoundary)
*reachedBoundary = false;
VisiblePosition pos;
switch (granularity) {
case TextGranularity::CharacterGranularity:
if (isRange())
if (directionOfSelection() == TextDirection::LTR)
pos = VisiblePosition(m_selection.start(), m_selection.affinity());
else
pos = VisiblePosition(m_selection.end(), m_selection.affinity());
else
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).left(true, reachedBoundary);
break;
case TextGranularity::WordGranularity: {
bool skipsSpaceWhenMovingRight = m_document && m_document->editor().behavior().shouldSkipSpaceWhenMovingRight();
VisiblePosition currentPosition(m_selection.extent(), m_selection.affinity());
pos = leftWordPosition(currentPosition, skipsSpaceWhenMovingRight);
if (reachedBoundary)
*reachedBoundary = pos == currentPosition;
break;
}
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
// FIXME: Implement all of the above.
pos = modifyMovingBackward(granularity, reachedBoundary);
break;
case TextGranularity::LineBoundary:
pos = leftBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock(), reachedBoundary);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
return pos;
}
VisiblePosition FrameSelection::modifyMovingBackward(TextGranularity granularity, bool* reachedBoundary)
{
if (reachedBoundary)
*reachedBoundary = false;
VisiblePosition currentPosition;
switch (granularity) {
case TextGranularity::WordGranularity:
case TextGranularity::SentenceGranularity:
currentPosition = VisiblePosition(m_selection.extent(), m_selection.affinity());
break;
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
currentPosition = startForPlatform();
break;
default:
break;
}
VisiblePosition pos;
switch (granularity) {
case TextGranularity::CharacterGranularity:
if (isRange())
pos = VisiblePosition(m_selection.start(), m_selection.affinity());
else
pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).previous(CannotCrossEditingBoundary, reachedBoundary);
break;
case TextGranularity::WordGranularity:
pos = previousWordPosition(currentPosition);
break;
case TextGranularity::SentenceGranularity:
pos = previousSentencePosition(currentPosition);
break;
case TextGranularity::LineGranularity:
pos = previousLinePosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(Start));
break;
case TextGranularity::ParagraphGranularity:
pos = previousParagraphPosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(Start));
break;
case TextGranularity::SentenceBoundary:
pos = startOfSentence(currentPosition);
break;
case TextGranularity::LineBoundary:
pos = logicalStartOfLine(startForPlatform(), reachedBoundary);
break;
case TextGranularity::ParagraphBoundary:
pos = startOfParagraph(currentPosition);
break;
case TextGranularity::DocumentBoundary:
pos = currentPosition;
if (isEditablePosition(pos.deepEquivalent()))
pos = startOfEditableContent(pos);
else
pos = startOfDocument(pos);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
switch (granularity) {
case TextGranularity::WordGranularity:
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
if (reachedBoundary)
*reachedBoundary = pos == currentPosition;
break;
default:
break;
}
return pos;
}
static bool isBoundary(TextGranularity granularity)
{
return granularity == TextGranularity::LineBoundary || granularity == TextGranularity::ParagraphBoundary || granularity == TextGranularity::DocumentBoundary;
}
AXTextStateChangeIntent FrameSelection::textSelectionIntent(EAlteration alter, SelectionDirection direction, TextGranularity granularity)
{
AXTextStateChangeIntent intent = AXTextStateChangeIntent();
bool flip = false;
if (alter == FrameSelection::AlterationMove) {
intent.type = AXTextStateChangeTypeSelectionMove;
flip = isRange() && directionOfSelection() == TextDirection::RTL;
} else
intent.type = AXTextStateChangeTypeSelectionExtend;
switch (granularity) {
case TextGranularity::CharacterGranularity:
intent.selection.granularity = AXTextSelectionGranularityCharacter;
break;
case TextGranularity::WordGranularity:
intent.selection.granularity = AXTextSelectionGranularityWord;
break;
case TextGranularity::SentenceGranularity:
case TextGranularity::SentenceBoundary:
intent.selection.granularity = AXTextSelectionGranularitySentence;
break;
case TextGranularity::LineGranularity:
case TextGranularity::LineBoundary:
intent.selection.granularity = AXTextSelectionGranularityLine;
break;
case TextGranularity::ParagraphGranularity:
case TextGranularity::ParagraphBoundary:
intent.selection.granularity = AXTextSelectionGranularityParagraph;
break;
case TextGranularity::DocumentGranularity:
case TextGranularity::DocumentBoundary:
intent.selection.granularity = AXTextSelectionGranularityDocument;
break;
}
bool boundary = false;
switch (granularity) {
case TextGranularity::CharacterGranularity:
case TextGranularity::WordGranularity:
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::DocumentGranularity:
break;
case TextGranularity::SentenceBoundary:
case TextGranularity::LineBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
boundary = true;
break;
}
switch (direction) {
case SelectionDirection::Right:
case SelectionDirection::Forward:
if (boundary)
intent.selection.direction = flip ? AXTextSelectionDirectionBeginning : AXTextSelectionDirectionEnd;
else
intent.selection.direction = flip ? AXTextSelectionDirectionPrevious : AXTextSelectionDirectionNext;
break;
case SelectionDirection::Left:
case SelectionDirection::Backward:
if (boundary)
intent.selection.direction = flip ? AXTextSelectionDirectionEnd : AXTextSelectionDirectionBeginning;
else
intent.selection.direction = flip ? AXTextSelectionDirectionNext : AXTextSelectionDirectionPrevious;
break;
}
return intent;
}
static AXTextSelection textSelectionWithDirectionAndGranularity(SelectionDirection direction, TextGranularity granularity)
{
// FIXME: Account for BIDI in SelectionDirection::Right & SelectionDirection::Left. (In a RTL block, Right would map to Previous/Beginning and Left to Next/End.)
AXTextSelectionDirection intentDirection = AXTextSelectionDirectionUnknown;
switch (direction) {
case SelectionDirection::Forward:
intentDirection = AXTextSelectionDirectionNext;
break;
case SelectionDirection::Right:
intentDirection = AXTextSelectionDirectionNext;
break;
case SelectionDirection::Backward:
intentDirection = AXTextSelectionDirectionPrevious;
break;
case SelectionDirection::Left:
intentDirection = AXTextSelectionDirectionPrevious;
break;
}
AXTextSelectionGranularity intentGranularity = AXTextSelectionGranularityUnknown;
switch (granularity) {
case TextGranularity::CharacterGranularity:
intentGranularity = AXTextSelectionGranularityCharacter;
break;
case TextGranularity::WordGranularity:
intentGranularity = AXTextSelectionGranularityWord;
break;
case TextGranularity::SentenceGranularity:
case TextGranularity::SentenceBoundary: // FIXME: Boundary should affect direction.
intentGranularity = AXTextSelectionGranularitySentence;
break;
case TextGranularity::LineGranularity:
intentGranularity = AXTextSelectionGranularityLine;
break;
case TextGranularity::ParagraphGranularity:
case TextGranularity::ParagraphBoundary: // FIXME: Boundary should affect direction.
intentGranularity = AXTextSelectionGranularityParagraph;
break;
case TextGranularity::DocumentGranularity:
case TextGranularity::DocumentBoundary: // FIXME: Boundary should affect direction.
intentGranularity = AXTextSelectionGranularityDocument;
break;
case TextGranularity::LineBoundary:
intentGranularity = AXTextSelectionGranularityLine;
switch (direction) {
case SelectionDirection::Forward:
intentDirection = AXTextSelectionDirectionEnd;
break;
case SelectionDirection::Right:
intentDirection = AXTextSelectionDirectionEnd;
break;
case SelectionDirection::Backward:
intentDirection = AXTextSelectionDirectionBeginning;
break;
case SelectionDirection::Left:
intentDirection = AXTextSelectionDirectionBeginning;
break;
}
break;
}
return { intentDirection, intentGranularity, false };
}
bool FrameSelection::modify(EAlteration alter, SelectionDirection direction, TextGranularity granularity, EUserTriggered userTriggered)
{
if (userTriggered == UserTriggered) {
FrameSelection trialFrameSelection;
trialFrameSelection.setSelection(m_selection);
trialFrameSelection.modify(alter, direction, granularity, NotUserTriggered);
bool change = shouldChangeSelection(trialFrameSelection.selection());
if (!change)
return false;
if (trialFrameSelection.selection().isRange() && m_selection.isCaret() && !dispatchSelectStart())
return false;
}
willBeModified(alter, direction);
// Before modifying selection, update layout and disable post resolution callbacks.
// That way, unaverted tree changes are avoided while browsing the document.
auto selectionDocument = m_selection.document();
if (!selectionDocument)
return false;
selectionDocument->updateLayoutIgnorePendingStylesheets();
Style::PostResolutionCallbackDisabler disabler(*selectionDocument);
bool reachedBoundary = false;
bool wasRange = m_selection.isRange();
Position originalStartPosition = m_selection.start();
VisiblePosition position;
switch (direction) {
case SelectionDirection::Right:
if (alter == AlterationMove)
position = modifyMovingRight(granularity, &reachedBoundary);
else
position = modifyExtendingRight(granularity);
break;
case SelectionDirection::Forward:
if (alter == AlterationExtend)
position = modifyExtendingForward(granularity);
else
position = modifyMovingForward(granularity, &reachedBoundary);
break;
case SelectionDirection::Left:
if (alter == AlterationMove)
position = modifyMovingLeft(granularity, &reachedBoundary);
else
position = modifyExtendingLeft(granularity);
break;
case SelectionDirection::Backward:
if (alter == AlterationExtend)
position = modifyExtendingBackward(granularity);
else
position = modifyMovingBackward(granularity, &reachedBoundary);
break;
}
if (reachedBoundary && !isRange() && userTriggered == UserTriggered && m_document && AXObjectCache::accessibilityEnabled()) {
notifyAccessibilityForSelectionChange({ AXTextStateChangeTypeSelectionBoundary, textSelectionWithDirectionAndGranularity(direction, granularity) });
return true;
}
if (position.isNull())
return false;
if (m_document && isSpatialNavigationEnabled(m_document->frame())) {
if (!wasRange && alter == AlterationMove && position == originalStartPosition)
return false;
}
if (m_document && AXObjectCache::accessibilityEnabled()) {
if (AXObjectCache* cache = m_document->existingAXObjectCache())
cache->setTextSelectionIntent(textSelectionIntent(alter, direction, granularity));
}
// Some of the above operations set an xPosForVerticalArrowNavigation.
// Setting a selection will clear it, so save it to possibly restore later.
// Note: the Start position type is arbitrary because it is unused, it would be
// the requested position type if there were no xPosForVerticalArrowNavigation set.
LayoutUnit x = lineDirectionPointForBlockDirectionNavigation(Start);
m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(m_document.get()) || alter == AlterationExtend);
switch (alter) {
case AlterationMove:
moveTo(position, userTriggered);
break;
case AlterationExtend:
if (!m_selection.isCaret()
&& (granularity == TextGranularity::WordGranularity || granularity == TextGranularity::ParagraphGranularity || granularity == TextGranularity::LineGranularity)
&& m_document && !m_document->editor().behavior().shouldExtendSelectionByWordOrLineAcrossCaret()) {
// Don't let the selection go across the base position directly. Needed to match mac
// behavior when, for instance, word-selecting backwards starting with the caret in
// the middle of a word and then word-selecting forward, leaving the caret in the
// same place where it was, instead of directly selecting to the end of the word.
VisibleSelection newSelection = m_selection;
newSelection.setExtent(position);
if (m_selection.isBaseFirst() != newSelection.isBaseFirst())
position = m_selection.base();
}
// Standard Mac behavior when extending to a boundary is grow the selection rather than leaving the
// base in place and moving the extent. Matches NSTextView.
if (!m_document || !m_document->editor().behavior().shouldAlwaysGrowSelectionWhenExtendingToBoundary() || m_selection.isCaret() || !isBoundary(granularity))
setExtent(position, userTriggered);
else {
TextDirection textDirection = directionOfEnclosingBlock();
if (direction == SelectionDirection::Forward || (textDirection == TextDirection::LTR && direction == SelectionDirection::Right) || (textDirection == TextDirection::RTL && direction == SelectionDirection::Left))
setEnd(position, userTriggered);
else
setStart(position, userTriggered);
}
break;
}
if (granularity == TextGranularity::LineGranularity || granularity == TextGranularity::ParagraphGranularity)
m_xPosForVerticalArrowNavigation = x;
if (userTriggered == UserTriggered)
m_granularity = TextGranularity::CharacterGranularity;
setCaretRectNeedsUpdate();
return true;
}
// FIXME: Maybe baseline would be better?
static bool absoluteCaretY(const VisiblePosition& c, int& y)
{
IntRect rect = c.absoluteCaretBounds();
if (rect.isEmpty())
return false;
y = rect.y() + rect.height() / 2;
return true;
}
bool FrameSelection::modify(EAlteration alter, unsigned verticalDistance, VerticalDirection direction, EUserTriggered userTriggered, CursorAlignOnScroll align)
{
if (!verticalDistance)
return false;
if (userTriggered == UserTriggered) {
FrameSelection trialFrameSelection;
trialFrameSelection.setSelection(m_selection);
trialFrameSelection.modify(alter, verticalDistance, direction, NotUserTriggered);
bool change = shouldChangeSelection(trialFrameSelection.selection());
if (!change)
return false;
}
willBeModified(alter, direction == DirectionUp ? SelectionDirection::Backward : SelectionDirection::Forward);
VisiblePosition pos;
LayoutUnit xPos;
switch (alter) {
case AlterationMove:
pos = VisiblePosition(direction == DirectionUp ? m_selection.start() : m_selection.end(), m_selection.affinity());
xPos = lineDirectionPointForBlockDirectionNavigation(direction == DirectionUp ? Start : End);
m_selection.setAffinity(direction == DirectionUp ? Affinity::Upstream : Affinity::Downstream);
break;
case AlterationExtend:
pos = VisiblePosition(m_selection.extent(), m_selection.affinity());
xPos = lineDirectionPointForBlockDirectionNavigation(Extent);
m_selection.setAffinity(Affinity::Downstream);
break;
}
int startY;
if (!absoluteCaretY(pos, startY))
return false;
if (direction == DirectionUp)
startY = -startY;
int lastY = startY;
VisiblePosition result;
VisiblePosition next;
for (VisiblePosition p = pos; ; p = next) {
if (direction == DirectionUp)
next = previousLinePosition(p, xPos);
else
next = nextLinePosition(p, xPos);
if (next.isNull() || next == p)
break;
int nextY;
if (!absoluteCaretY(next, nextY))
break;
if (direction == DirectionUp)
nextY = -nextY;
if (nextY - startY > static_cast<int>(verticalDistance))
break;
if (nextY >= lastY) {
lastY = nextY;
result = next;
}
}
if (result.isNull())
return false;
switch (alter) {
case AlterationMove:
moveTo(result, userTriggered, align);
break;
case AlterationExtend:
setExtent(result, userTriggered);
break;
}
if (userTriggered == UserTriggered)
m_granularity = TextGranularity::CharacterGranularity;
m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(m_document.get()) || alter == AlterationExtend);
return true;
}
LayoutUnit FrameSelection::lineDirectionPointForBlockDirectionNavigation(PositionType type)
{
if (isNone())
return 0;
// FIXME: Can we use visibleStart/End/Extent?
Position position;
switch (type) {
case Start:
position = m_selection.start();
break;
case End:
position = m_selection.end();
break;
case Extent:
position = m_selection.extent();
break;
}
// FIXME: Why is this check needed? What's the harm in doing a little more work without a frame?
if (!position.anchorNode()->document().frame())
return 0;
// FIXME: Can we do this before getting the position from the selection?
if (m_xPosForVerticalArrowNavigation)
return *m_xPosForVerticalArrowNavigation;
// VisiblePosition creation can fail here if a node containing the selection becomes
// visibility:hidden after the selection is created and before this function is called.
VisiblePosition visiblePosition(position, m_selection.affinity());
auto x = visiblePosition.isNotNull() ? visiblePosition.lineDirectionPointForBlockDirectionNavigation() : 0;
m_xPosForVerticalArrowNavigation = { x };
return x;
}
void FrameSelection::clear()
{
m_granularity = TextGranularity::CharacterGranularity;
setSelection(VisibleSelection());
}
void FrameSelection::willBeRemovedFromFrame()
{
m_granularity = TextGranularity::CharacterGranularity;
#if ENABLE(TEXT_CARET)
m_caretBlinkTimer.stop();
#endif
if (auto* view = m_document->renderView())
view->selection().clear();
setSelectionWithoutUpdatingAppearance(VisibleSelection(), defaultSetSelectionOptions(), AlignCursorOnScrollIfNeeded, TextGranularity::CharacterGranularity);
m_previousCaretNode = nullptr;
m_typingStyle = nullptr;
m_appearanceUpdateTimer.stop();
}
void FrameSelection::setStart(const VisiblePosition& position, EUserTriggered trigger)
{
if (m_selection.isBaseFirst())
setBase(position, trigger);
else
setExtent(position, trigger);
}
void FrameSelection::setEnd(const VisiblePosition& position, EUserTriggered trigger)
{
if (m_selection.isBaseFirst())
setExtent(position, trigger);
else
setBase(position, trigger);
}
void FrameSelection::setBase(const VisiblePosition& position, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(position.deepEquivalent(), m_selection.extent(), position.affinity(), selectionHasDirection), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::setExtent(const VisiblePosition& position, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(m_selection.base(), position.deepEquivalent(), position.affinity(), selectionHasDirection), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::setBase(const Position& position, Affinity affinity, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(position, m_selection.extent(), affinity, selectionHasDirection), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::setExtent(const Position& position, Affinity affinity, EUserTriggered userTriggered)
{
const bool selectionHasDirection = true;
setSelection(VisibleSelection(m_selection.base(), position, affinity, selectionHasDirection), defaultSetSelectionOptions(userTriggered));
}
void CaretBase::clearCaretRect()
{
m_caretLocalRect = LayoutRect();
}
bool CaretBase::updateCaretRect(Document& document, const VisiblePosition& caretPosition)
{
document.updateLayoutIgnorePendingStylesheets();
m_caretRectNeedsUpdate = false;
RenderBlock* renderer;
m_caretLocalRect = localCaretRectInRendererForCaretPainting(caretPosition, renderer);
return !m_caretLocalRect.isEmpty();
}
RenderBlock* FrameSelection::caretRendererWithoutUpdatingLayout() const
{
return rendererForCaretPainting(m_selection.start().deprecatedNode());
}
RenderBlock* DragCaretController::caretRenderer() const
{
return rendererForCaretPainting(m_position.deepEquivalent().deprecatedNode());
}
static bool isNonOrphanedCaret(const VisibleSelection& selection)
{
return selection.isCaret() && !selection.start().isOrphan() && !selection.end().isOrphan();
}
IntRect FrameSelection::absoluteCaretBounds(bool* insideFixed)
{
if (!m_document)
return IntRect();
updateSelectionAppearanceNow();
recomputeCaretRect();
if (insideFixed)
*insideFixed = m_caretInsidePositionFixed;
return m_absCaretBounds;
}
static void repaintCaretForLocalRect(Node* node, const LayoutRect& rect)
{
if (auto* caretPainter = rendererForCaretPainting(node))
caretPainter->repaintRectangle(rect);
}
bool FrameSelection::recomputeCaretRect()
{
if (!shouldUpdateCaretRect())
return false;
if (!m_document)
return false;
FrameView* v = m_document->view();
if (!v)
return false;
LayoutRect oldRect = localCaretRectWithoutUpdate();
RefPtr<Node> caretNode = m_previousCaretNode;
if (shouldUpdateCaretRect()) {
if (!isNonOrphanedCaret(m_selection))
clearCaretRect();
else {
VisiblePosition visibleStart = m_selection.visibleStart();
if (updateCaretRect(*m_document, visibleStart)) {
caretNode = visibleStart.deepEquivalent().deprecatedNode();
m_absCaretBoundsDirty = true;
}
}
}
LayoutRect newRect = localCaretRectWithoutUpdate();
if (caretNode == m_previousCaretNode && oldRect == newRect && !m_absCaretBoundsDirty)
return false;
IntRect oldAbsCaretBounds = m_absCaretBounds;
bool isInsideFixed;
m_absCaretBounds = absoluteBoundsForLocalCaretRect(rendererForCaretPainting(caretNode.get()), newRect, &isInsideFixed);
m_caretInsidePositionFixed = isInsideFixed;
if (m_absCaretBoundsDirty && m_selection.isCaret()) // We should be able to always assert this condition.
ASSERT(m_absCaretBounds == m_selection.visibleStart().absoluteCaretBounds());
m_absCaretBoundsDirty = false;
if (caretNode == m_previousCaretNode && oldAbsCaretBounds == m_absCaretBounds)
return false;
#if ENABLE(TEXT_CARET)
if (RenderView* view = m_document->renderView()) {
bool previousOrNewCaretNodeIsContentEditable = m_selection.isContentEditable() || (m_previousCaretNode && m_previousCaretNode->isContentEditable());
if (shouldRepaintCaret(view, previousOrNewCaretNodeIsContentEditable)) {
if (m_previousCaretNode)
repaintCaretForLocalRect(m_previousCaretNode.get(), oldRect);
m_previousCaretNode = caretNode;
repaintCaretForLocalRect(caretNode.get(), newRect);
}
}
#endif
return true;
}
bool CaretBase::shouldRepaintCaret(const RenderView* view, bool isContentEditable) const
{
ASSERT(view);
Frame* frame = &view->frameView().frame(); // The frame where the selection started.
bool caretBrowsing = frame && frame->settings().caretBrowsingEnabled();
return (caretBrowsing || isContentEditable);
}
void FrameSelection::invalidateCaretRect()
{
if (!isCaret())
return;
CaretBase::invalidateCaretRect(m_selection.start().deprecatedNode(), recomputeCaretRect());
}
void CaretBase::invalidateCaretRect(Node* node, bool caretRectChanged)
{
// EDIT FIXME: This is an unfortunate hack.
// Basically, we can't trust this layout position since we
// can't guarantee that the check to see if we are in unrendered
// content will work at this point. We may have to wait for
// a layout and re-render of the document to happen. So, resetting this
// flag will cause another caret layout to happen the first time
// that we try to paint the caret after this call. That one will work since
// it happens after the document has accounted for any editing
// changes which may have been done.
// And, we need to leave this layout here so the caret moves right
// away after clicking.
m_caretRectNeedsUpdate = true;
if (caretRectChanged)
return;
if (RenderView* view = node->document().renderView()) {
if (shouldRepaintCaret(view, isEditableNode(*node)))
repaintCaretForLocalRect(node, localCaretRectWithoutUpdate());
}
}
void FrameSelection::paintCaret(GraphicsContext& context, const LayoutPoint& paintOffset, const LayoutRect& clipRect)
{
if (m_selection.isCaret() && m_caretPaint && m_selection.start().deprecatedNode())
CaretBase::paintCaret(*m_selection.start().deprecatedNode(), context, paintOffset, clipRect);
}
Color CaretBase::computeCaretColor(const RenderStyle& elementStyle, const Node* node)
{
// On iOS, we want to fall back to the tintColor, and only override if CSS has explicitly specified a custom color.
#if PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST)
UNUSED_PARAM(node);
return elementStyle.caretColor();
#else
RefPtr parentElement = node ? node->parentElement() : nullptr;
auto* parentStyle = parentElement && parentElement->renderer() ? &parentElement->renderer()->style() : nullptr;
// CSS value "auto" is treated as an invalid color.
if (!elementStyle.caretColor().isValid() && parentStyle) {
auto parentBackgroundColor = parentStyle->visitedDependentColorWithColorFilter(CSSPropertyBackgroundColor);
auto elementBackgroundColor = elementStyle.visitedDependentColorWithColorFilter(CSSPropertyBackgroundColor);
auto disappearsIntoBackground = blendSourceOver(parentBackgroundColor, elementBackgroundColor) == parentBackgroundColor;
if (disappearsIntoBackground)
return parentStyle->visitedDependentColorWithColorFilter(CSSPropertyCaretColor);
}
return elementStyle.visitedDependentColorWithColorFilter(CSSPropertyCaretColor);
#endif
}
void CaretBase::paintCaret(const Node& node, GraphicsContext& context, const LayoutPoint& paintOffset, const LayoutRect& clipRect) const
{
#if ENABLE(TEXT_CARET)
if (m_caretVisibility == Hidden)
return;
auto drawingRect = localCaretRectWithoutUpdate();
if (auto* renderer = rendererForCaretPainting(&node))
renderer->flipForWritingMode(drawingRect);
drawingRect.moveBy(paintOffset);
auto caret = intersection(drawingRect, clipRect);
if (caret.isEmpty())
return;
Color caretColor = Color::black;
auto* element = is<Element>(node) ? downcast<Element>(&node) : node.parentElement();
if (element && element->renderer())
caretColor = CaretBase::computeCaretColor(element->renderer()->style(), &node);
auto pixelSnappedCaretRect = snapRectToDevicePixels(caret, node.document().deviceScaleFactor());
context.fillRect(pixelSnappedCaretRect, caretColor);
#else
UNUSED_PARAM(node);
UNUSED_PARAM(context);
UNUSED_PARAM(paintOffset);
UNUSED_PARAM(clipRect);
#endif
}
void FrameSelection::debugRenderer(RenderObject* renderer, bool selected) const
{
if (is<Element>(*renderer->node())) {
Element& element = downcast<Element>(*renderer->node());
fprintf(stderr, "%s%s\n", selected ? "==> " : " ", element.localName().string().utf8().data());
} else if (is<RenderText>(*renderer)) {
RenderText& textRenderer = downcast<RenderText>(*renderer);
if (textRenderer.text().isEmpty() || !textRenderer.firstTextBox()) {
fprintf(stderr, "%s#text (empty)\n", selected ? "==> " : " ");
return;
}
static const int max = 36;
String text = textRenderer.text();
int textLength = text.length();
if (selected) {
int offset = 0;
if (renderer->node() == m_selection.start().containerNode())
offset = m_selection.start().computeOffsetInContainerNode();
else if (renderer->node() == m_selection.end().containerNode())
offset = m_selection.end().computeOffsetInContainerNode();
int pos;
LegacyInlineTextBox* box = textRenderer.findNextInlineTextBox(offset, pos);
text = text.substring(box->start(), box->len());
String show;
int mid = max / 2;
int caret = 0;
// text is shorter than max
if (textLength < max) {
show = text;
caret = pos;
} else if (pos - mid < 0) {
// too few characters to left
show = makeString(StringView(text).left(max - 3), "...");
caret = pos;
} else if (pos - mid >= 0 && pos + mid <= textLength) {
// enough characters on each side
show = makeString("...", StringView(text).substring(pos - mid + 3, max - 6), "...");
caret = mid;
} else {
// too few characters on right
show = makeString("...", StringView(text).right(max - 3));
caret = pos - (textLength - show.length());
}
show = makeStringByReplacingAll(show, '\n', ' ');
show = makeStringByReplacingAll(show, '\r', ' ');
fprintf(stderr, "==> #text : \"%s\" at offset %d\n", show.utf8().data(), pos);
fprintf(stderr, " ");
for (int i = 0; i < caret; i++)
fprintf(stderr, " ");
fprintf(stderr, "^\n");
} else {
if ((int)text.length() > max)
text = text.left(max - 3) + "...";
else
text = text.left(max);
fprintf(stderr, " #text : \"%s\"\n", text.utf8().data());
}
}
}
bool FrameSelection::contains(const LayoutPoint& point) const
{
// Treat a collapsed selection like no selection.
if (!isRange())
return false;
auto range = m_selection.firstRange();
if (!range)
return false;
if (!m_document)
return false;
HitTestResult result(point);
m_document->hitTest(HitTestRequest(), result);
RefPtr innerNode = result.innerNode();
if (!innerNode || !innerNode->renderer())
return false;
if (ImageOverlay::isInsideOverlay(*range) && ImageOverlay::isInsideOverlay(*innerNode)) {
for (auto quad : RenderObject::absoluteTextQuads(*range, { RenderObject::BoundingRectBehavior::UseSelectionHeight })) {
if (!quad.isEmpty() && quad.containsPoint(point))
return true;
}
return false;
}
return WebCore::contains<ComposedTree>(*range, makeBoundaryPoint(innerNode->renderer()->positionForPoint(result.localPoint(), nullptr)));
}
// Workaround for the fact that it's hard to delete a frame.
// Call this after doing user-triggered selections to make it easy to delete the frame you entirely selected.
// Can't do this implicitly as part of every setSelection call because in some contexts it might not be good
// for the focus to move to another frame. So instead we call it from places where we are selecting with the
// mouse or the keyboard after setting the selection.
void FrameSelection::selectFrameElementInParentIfFullySelected()
{
// Find the parent frame; if there is none, then we have nothing to do.
RefPtr document { m_document.get() };
if (!document)
return;
RefPtr frame { document->frame() };
if (!frame)
return;
RefPtr parent { frame->tree().parent() };
if (!parent)
return;
Page* page = m_document->page();
if (!page)
return;
// Check if the selection contains the entire frame contents; if not, then there is nothing to do.
if (!isRange())
return;
if (!isStartOfDocument(selection().visibleStart()))
return;
if (!isEndOfDocument(selection().visibleEnd()))
return;
// Get to the <iframe> or <frame> (or even <object>) element in the parent frame.
RefPtr ownerElement { m_document->ownerElement() };
if (!ownerElement)
return;
RefPtr ownerElementParent { ownerElement->parentNode() };
if (!ownerElementParent)
return;
// This method's purpose is it to make it easier to select iframes (in order to delete them). Don't do anything if the iframe isn't deletable.
if (!ownerElementParent->hasEditableStyle())
return;
// Create compute positions before and after the element.
unsigned ownerElementNodeIndex = ownerElement->computeNodeIndex();
VisiblePosition beforeOwnerElement(VisiblePosition(Position(ownerElementParent.get(), ownerElementNodeIndex, Position::PositionIsOffsetInAnchor)));
VisiblePosition afterOwnerElement(VisiblePosition(Position(ownerElementParent.get(), ownerElementNodeIndex + 1, Position::PositionIsOffsetInAnchor), Affinity::Upstream));
// Focus on the parent frame, and then select from before this element to after.
VisibleSelection newSelection(beforeOwnerElement, afterOwnerElement);
if (parent->selection().shouldChangeSelection(newSelection) && page) {
CheckedRef(page->focusController())->setFocusedFrame(parent.get());
// Previous focus can trigger DOM events, ensure the selection did not become orphan.
if (newSelection.isOrphan())
parent->selection().clear();
else
parent->selection().setSelection(newSelection);
}
}
void FrameSelection::selectAll()
{
Element* focusedElement = m_document->focusedElement();
if (is<HTMLSelectElement>(focusedElement)) {
HTMLSelectElement& selectElement = downcast<HTMLSelectElement>(*focusedElement);
if (selectElement.canSelectAll()) {
selectElement.selectAll();
return;
}
}
RefPtr<Node> root;
Node* selectStartTarget = nullptr;
if (m_selection.isContentEditable()) {
root = highestEditableRoot(m_selection.start());
if (Node* shadowRoot = m_selection.nonBoundaryShadowTreeRootNode())
selectStartTarget = shadowRoot->shadowHost();
else
selectStartTarget = root.get();
} else {
if (m_selection.isNone() && focusedElement) {
if (focusedElement->isTextField()) {
downcast<HTMLTextFormControlElement>(*focusedElement).select();
return;
}
root = focusedElement->nonBoundaryShadowTreeRootNode();
} else
root = m_selection.nonBoundaryShadowTreeRootNode();
if (root)
selectStartTarget = root->shadowHost();
else {
root = m_document->documentElement();
selectStartTarget = m_document->bodyOrFrameset();
}
}
if (!root)
return;
if (selectStartTarget) {
auto event = Event::create(eventNames().selectstartEvent, Event::CanBubble::Yes, Event::IsCancelable::Yes);
selectStartTarget->dispatchEvent(event);
if (event->defaultPrevented())
return;
}
VisibleSelection newSelection(VisibleSelection::selectionFromContentsOfNode(root.get()));
if (shouldChangeSelection(newSelection)) {
AXTextStateChangeIntent intent(AXTextStateChangeTypeSelectionExtend, AXTextSelection { AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityAll, false });
setSelection(newSelection, defaultSetSelectionOptions() | FireSelectEvent, intent);
}
}
bool FrameSelection::setSelectedRange(const std::optional<SimpleRange>& range, Affinity affinity, ShouldCloseTyping closeTyping, EUserTriggered userTriggered)
{
if (!range)
return false;
if (&range->start.document() != &range->end.document())
return false;
VisibleSelection newSelection(*range, affinity);
#if PLATFORM(IOS_FAMILY)
// FIXME: Why do we need this check only in iOS?
if (newSelection.isNone())
return false;
#endif
OptionSet<SetSelectionOption> selectionOptions { ClearTypingStyle };
if (closeTyping == ShouldCloseTyping::Yes)
selectionOptions.add(CloseTyping);
if (userTriggered == UserTriggered) {
FrameSelection trialFrameSelection;
trialFrameSelection.setSelection(newSelection, selectionOptions);
if (!shouldChangeSelection(trialFrameSelection.selection()))
return false;
selectionOptions.add(IsUserTriggered);
}
setSelection(newSelection, selectionOptions);
return true;
}
void FrameSelection::focusedOrActiveStateChanged()
{
bool activeAndFocused = isFocusedAndActive();
m_document->updateStyleIfNeeded();
#if USE(UIKIT_EDITING)
// Caret blinking (blinks | does not blink)
if (activeAndFocused)
setSelectionFromNone();
setCaretVisible(activeAndFocused);
#else
// Because RenderObject::selectionBackgroundColor() and
// RenderObject::selectionForegroundColor() check if the frame is active,
// we have to update places those colors were painted.
if (RenderView* view = m_document->renderView())
view->selection().repaint();
// Caret appears in the active frame.
if (activeAndFocused)
setSelectionFromNone();
setCaretVisibility(activeAndFocused ? Visible : Hidden, ShouldUpdateAppearance::Yes);
// Because Style::Resolver::checkOneSelector() and
// RenderTheme::isFocused() check if the frame is active, we have to
// update style and theme state that depended on those.
if (Element* element = m_document->focusedElement()) {
element->invalidateStyleForSubtree();
if (RenderObject* renderer = element->renderer())
if (renderer && renderer->style().hasEffectiveAppearance())
renderer->theme().stateChanged(*renderer, ControlStates::States::Focused);
}
#endif
}
void FrameSelection::pageActivationChanged()
{
focusedOrActiveStateChanged();
}
void FrameSelection::setFocused(bool flag)
{
if (m_focused == flag)
return;
m_focused = flag;
focusedOrActiveStateChanged();
}
bool FrameSelection::isFocusedAndActive() const
{
return m_focused && m_document->page() && m_document->page()->focusController().isActive();
}
#if ENABLE(TEXT_CARET)
inline static bool shouldStopBlinkingDueToTypingCommand(Document* document)
{
return document->editor().lastEditCommand() && document->editor().lastEditCommand()->shouldStopCaretBlinking();
}
#endif
void FrameSelection::updateAppearance()
{
#if PLATFORM(IOS_FAMILY)
if (!m_updateAppearanceEnabled)
return;
#endif
// Paint a block cursor instead of a caret in overtype mode unless the caret is at the end of a line (in this case
// the FrameSelection will paint a blinking caret as usual).
VisibleSelection oldSelection = selection();
#if ENABLE(TEXT_CARET)
bool paintBlockCursor = m_shouldShowBlockCursor && m_selection.isCaret() && !isLogicalEndOfLine(m_selection.visibleEnd());
bool caretRectChangedOrCleared = recomputeCaretRect();
bool caretBrowsing = m_document->settings().caretBrowsingEnabled();
bool shouldBlink = !paintBlockCursor && caretIsVisible() && isCaret() && (oldSelection.isContentEditable() || caretBrowsing);
// If the caret moved, stop the blink timer so we can restart with a
// black caret in the new location.
if (caretRectChangedOrCleared || !shouldBlink || shouldStopBlinkingDueToTypingCommand(m_document.get()))
m_caretBlinkTimer.stop();
// Start blinking with a black caret. Be sure not to restart if we're
// already blinking in the right location.
if (shouldBlink && !m_caretBlinkTimer.isActive()) {
if (Seconds blinkInterval = RenderTheme::singleton().caretBlinkInterval())
m_caretBlinkTimer.startRepeating(blinkInterval);
if (!m_caretPaint) {
m_caretPaint = true;
invalidateCaretRect();
}
}
#endif
// Construct a new VisibleSolution, since m_selection is not necessarily valid, and the following steps
// assume a valid selection. See <https://bugs.webkit.org/show_bug.cgi?id=69563> and <rdar://problem/10232866>.
#if ENABLE(TEXT_CARET)
VisiblePosition endVisiblePosition = paintBlockCursor ? modifyExtendingForward(TextGranularity::CharacterGranularity) : oldSelection.visibleEnd();
VisibleSelection selection(oldSelection.visibleStart(), endVisiblePosition);
#else
VisibleSelection selection(oldSelection.visibleStart(), oldSelection.visibleEnd());
#endif
{
ScriptDisallowedScope scriptDisallowedScope;
auto* view = m_document->renderView();
if (!view)
return;
if (!selection.isRange()) {
view->selection().clear();
return;
}
}
// Use the rightmost candidate for the start of the selection, and the leftmost candidate for the end of the selection.
// Example: foo <a>bar</a>. Imagine that a line wrap occurs after 'foo', and that 'bar' is selected. If we pass [foo, 3]
// as the start of the selection, the selection painting code will think that content on the line containing 'foo' is selected
// and will fill the gap before 'bar'.
Position startPos = selection.start();
Position candidate = startPos.downstream();
if (candidate.isCandidate())
startPos = candidate;
Position endPos = selection.end();
candidate = endPos.upstream();
if (candidate.isCandidate())
endPos = candidate;
// We can get into a state where the selection endpoints map to the same VisiblePosition when a selection is deleted
// because we don't yet notify the FrameSelection of text removal.
if (auto* view = m_document->renderView(); startPos.isNotNull() && endPos.isNotNull() && selection.visibleStart() != selection.visibleEnd()) {
RenderObject* startRenderer = startPos.deprecatedNode()->renderer();
int startOffset = startPos.deprecatedEditingOffset();
RenderObject* endRenderer = endPos.deprecatedNode()->renderer();
int endOffset = endPos.deprecatedEditingOffset();
ASSERT(startOffset >= 0 && endOffset >= 0);
view->selection().set({ startRenderer, endRenderer, static_cast<unsigned>(startOffset), static_cast<unsigned>(endOffset) });
}
}
void FrameSelection::setCaretVisibility(CaretVisibility visibility, ShouldUpdateAppearance doAppearanceUpdate)
{
if (caretVisibility() == visibility)
return;
// FIXME: We shouldn't trigger a synchronous layout here.
if (doAppearanceUpdate == ShouldUpdateAppearance::Yes && m_document)
updateSelectionAppearanceNow();
#if ENABLE(TEXT_CARET)
if (m_caretPaint) {
m_caretPaint = false;
invalidateCaretRect();
}
CaretBase::setCaretVisibility(visibility);
#endif
if (doAppearanceUpdate == ShouldUpdateAppearance::Yes)
updateAppearance();
}
void FrameSelection::caretBlinkTimerFired()
{
#if ENABLE(TEXT_CARET)
if (!isCaret())
return;
ASSERT(caretIsVisible());
bool caretPaint = m_caretPaint;
if (isCaretBlinkingSuspended() && caretPaint)
return;
m_caretPaint = !caretPaint;
invalidateCaretRect();
#endif
}
// Helper function that tells whether a particular node is an element that has an entire
// Frame and FrameView, a <frame>, <iframe>, or <object>.
static bool isFrameElement(const Node* n)
{
if (!n)
return false;
RenderObject* renderer = n->renderer();
if (!is<RenderWidget>(renderer))
return false;
Widget* widget = downcast<RenderWidget>(*renderer).widget();
return widget && widget->isFrameView();
}
void FrameSelection::setFocusedElementIfNeeded()
{
if (isNone() || !isFocused())
return;
bool caretBrowsing = m_document->settings().caretBrowsingEnabled();
if (caretBrowsing) {
if (RefPtr anchor = enclosingAnchorElement(m_selection.base())) {
CheckedRef focusController { m_document->page()->focusController() };
focusController->setFocusedElement(anchor.get(), *m_document->frame());
return;
}
}
if (Element* target = m_selection.rootEditableElement()) {
// Walk up the DOM tree to search for an element to focus.
while (target) {
// We don't want to set focus on a subframe when selecting in a parent frame,
// so add the !isFrameElement check here. There's probably a better way to make this
// work in the long term, but this is the safest fix at this time.
if (target->isMouseFocusable() && !isFrameElement(target)) {
CheckedRef(m_document->page()->focusController())->setFocusedElement(target, *m_document->frame());
return;
}
target = target->parentOrShadowHostElement();
}
m_document->setFocusedElement(nullptr);
}
if (caretBrowsing)
CheckedRef(m_document->page()->focusController())->setFocusedElement(nullptr, *m_document->frame());
}
void DragCaretController::paintDragCaret(Frame* frame, GraphicsContext& p, const LayoutPoint& paintOffset, const LayoutRect& clipRect) const
{
#if ENABLE(TEXT_CARET)
if (m_position.deepEquivalent().deprecatedNode() && m_position.deepEquivalent().deprecatedNode()->document().frame() == frame)
paintCaret(*m_position.deepEquivalent().deprecatedNode(), p, paintOffset, clipRect);
#else
UNUSED_PARAM(frame);
UNUSED_PARAM(p);
UNUSED_PARAM(paintOffset);
UNUSED_PARAM(clipRect);
#endif
}
RefPtr<MutableStyleProperties> FrameSelection::copyTypingStyle() const
{
if (!m_typingStyle || !m_typingStyle->style())
return nullptr;
return m_typingStyle->style()->mutableCopy();
}
bool FrameSelection::shouldDeleteSelection(const VisibleSelection& selection) const
{
#if PLATFORM(IOS_FAMILY)
if (m_document->frame() && m_document->frame()->selectionChangeCallbacksDisabled())
return true;
#endif
return m_document->editor().client()->shouldDeleteRange(selection.toNormalizedRange());
}
FloatRect FrameSelection::selectionBounds(ClipToVisibleContent clipToVisibleContent)
{
if (!m_document)
return LayoutRect();
updateSelectionAppearanceNow();
auto* renderView = m_document->renderView();
if (!renderView)
return LayoutRect();
if (!m_selection.range())
return LayoutRect();
#if PLATFORM(IOS_FAMILY)
auto selectionGeometries = RenderObject::collectSelectionGeometries(m_selection.range().value());
IntRect visibleSelectionRect;
for (auto geometry : selectionGeometries)
visibleSelectionRect.unite(geometry.rect());
if (clipToVisibleContent == ClipToVisibleContent::No)
return visibleSelectionRect;
#else
auto& selection = renderView->selection();
auto visibleSelectionRect = selection.boundsClippedToVisibleContent();
if (clipToVisibleContent == ClipToVisibleContent::No)
return selection.bounds();
#endif
return intersection(visibleSelectionRect, renderView->frameView().visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect));
}
void FrameSelection::getClippedVisibleTextRectangles(Vector<FloatRect>& rectangles, TextRectangleHeight textRectHeight) const
{
if (!m_document->renderView())
return;
auto range = selection().toNormalizedRange();
if (!range)
return;
OptionSet<RenderObject::BoundingRectBehavior> behavior;
if (textRectHeight == TextRectangleHeight::SelectionHeight)
behavior.add(RenderObject::BoundingRectBehavior::UseSelectionHeight);
auto visibleContentRect = m_document->view()->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
for (auto& rect : boundingBoxes(RenderObject::absoluteTextQuads(*range, behavior))) {
auto intersectionRect = intersection(rect, visibleContentRect);
if (!intersectionRect.isEmpty())
rectangles.append(intersectionRect);
}
}
// Scans logically forward from "start", including any child frames.
static HTMLFormElement* scanForForm(Element* start)
{
if (!start)
return nullptr;
for (auto& element : descendantsOfType<HTMLElement>(start->document())) {
if (is<HTMLFormElement>(element))
return &downcast<HTMLFormElement>(element);
if (is<HTMLFormControlElement>(element))
return downcast<HTMLFormControlElement>(element).form();
if (is<HTMLFrameElementBase>(element)) {
if (auto* contentDocument = downcast<HTMLFrameElementBase>(element).contentDocument()) {
if (auto* frameResult = scanForForm(contentDocument->documentElement()))
return frameResult;
}
}
}
return nullptr;
}
// We look for either the form containing the current focus, or for one immediately after it
HTMLFormElement* FrameSelection::currentForm() const
{
// Start looking either at the active (first responder) node, or where the selection is.
Element* start = m_document->focusedElement();
if (!start)
start = m_selection.start().element();
if (!start)
return nullptr;
if (auto form = lineageOfType<HTMLFormElement>(*start).first())
return form;
if (auto formControl = lineageOfType<HTMLFormControlElement>(*start).first())
return formControl->form();
// Try walking forward in the node tree to find a form element.
return scanForForm(start);
}
void FrameSelection::revealSelection(SelectionRevealMode revealMode, const ScrollAlignment& alignment, RevealExtentOption revealExtentOption, ScrollBehavior scrollBehavior)
{
if (revealMode == SelectionRevealMode::DoNotReveal)
return;
if (isNone())
return;
updateSelectionAppearanceNow();
LayoutRect rect;
bool insideFixed = false;
if (isCaret())
rect = absoluteCaretBounds(&insideFixed);
else
rect = revealExtentOption == RevealExtent ? VisiblePosition(m_selection.extent()).absoluteCaretBounds() : enclosingIntRect(selectionBounds(ClipToVisibleContent::No));
Position start = m_selection.start();
ASSERT(start.deprecatedNode());
if (start.deprecatedNode() && start.deprecatedNode()->renderer()) {
#if PLATFORM(IOS_FAMILY)
if (RenderLayer* layer = start.deprecatedNode()->renderer()->enclosingLayer()) {
if (!m_scrollingSuppressCount) {
layer->scrollRectToVisible(rect, insideFixed, { revealMode, alignment, alignment, ShouldAllowCrossOriginScrolling::Yes, scrollBehavior});
updateAppearance();
if (m_document->page())
m_document->page()->chrome().client().notifyRevealedSelectionByScrollingFrame(*m_document->frame());
}
}
#else
// FIXME: This code only handles scrolling the startContainer's layer, but
// the selection rect could intersect more than just that.
// See <rdar://problem/4799899>.
if (start.deprecatedNode()->renderer()->scrollRectToVisible(rect, insideFixed, { revealMode, alignment, alignment, ShouldAllowCrossOriginScrolling::Yes, scrollBehavior}))
updateAppearance();
#endif
}
}
void FrameSelection::setSelectionFromNone()
{
// Put a caret inside the body if the entire frame is editable (either the
// entire WebView is editable or designMode is on for this document).
bool caretBrowsing = m_document->settings().caretBrowsingEnabled();
if (!m_document || !isNone() || !(m_document->hasEditableStyle() || caretBrowsing))
return;
if (auto* body = m_document->body())
setSelection(VisibleSelection(firstPositionInOrBeforeNode(body)));
}
bool FrameSelection::shouldChangeSelection(const VisibleSelection& newSelection) const
{
#if PLATFORM(IOS_FAMILY)
if (m_document->frame() && m_document->frame()->selectionChangeCallbacksDisabled())
return true;
#endif
return m_document->editor().shouldChangeSelection(selection(), newSelection, newSelection.affinity(), false);
}
bool FrameSelection::dispatchSelectStart()
{
Node* selectStartTarget = m_selection.extent().containerNode();
if (!selectStartTarget)
return true;
auto event = Event::create(eventNames().selectstartEvent, Event::CanBubble::Yes, Event::IsCancelable::Yes);
selectStartTarget->dispatchEvent(event);
return !event->defaultPrevented();
}
void FrameSelection::setShouldShowBlockCursor(bool shouldShowBlockCursor)
{
m_shouldShowBlockCursor = shouldShowBlockCursor;
m_document->updateLayoutIgnorePendingStylesheets();
updateAppearance();
}
void FrameSelection::updateAppearanceAfterLayout()
{
m_appearanceUpdateTimer.stop();
updateAppearanceAfterLayoutOrStyleChange();
}
void FrameSelection::scheduleAppearanceUpdateAfterStyleChange()
{
m_appearanceUpdateTimer.startOneShot(0_s);
}
void FrameSelection::appearanceUpdateTimerFired()
{
Ref<Document> protector(*m_document);
updateAppearanceAfterLayoutOrStyleChange();
}
void FrameSelection::updateAppearanceAfterLayoutOrStyleChange()
{
if (auto* client = m_document->editor().client())
client->updateEditorStateAfterLayoutIfEditabilityChanged();
setCaretRectNeedsUpdate();
updateAndRevealSelection(m_selectionRevealIntent);
updateDataDetectorsForSelection();
}
#if ENABLE(TREE_DEBUGGING)
String FrameSelection::debugDescription() const
{
return m_selection.debugDescription();
}
void FrameSelection::showTreeForThis() const
{
m_selection.showTreeForThis();
}
#endif
#if PLATFORM(IOS_FAMILY)
void FrameSelection::expandSelectionToElementContainingCaretSelection()
{
auto range = elementRangeContainingCaretSelection();
if (!range)
return;
setSelection(VisibleSelection(*range));
}
std::optional<SimpleRange> FrameSelection::elementRangeContainingCaretSelection() const
{
auto element = deprecatedEnclosingBlockFlowElement(m_selection.visibleStart().deepEquivalent().deprecatedNode());
if (!element)
return std::nullopt;
auto start = VisiblePosition(makeContainerOffsetPosition(element, 0));
auto end = VisiblePosition(makeContainerOffsetPosition(element, element->countChildNodes()));
if (start.isNull() || end.isNull())
return std::nullopt;
auto selection = m_selection;
selection.setBase(start);
selection.setExtent(end);
return selection.toNormalizedRange();
}
void FrameSelection::expandSelectionToWordContainingCaretSelection()
{
VisibleSelection selection(wordSelectionContainingCaretSelection(m_selection));
if (selection.isCaretOrRange())
setSelection(selection);
}
std::optional<SimpleRange> FrameSelection::wordRangeContainingCaretSelection()
{
return wordSelectionContainingCaretSelection(m_selection).toNormalizedRange();
}
void FrameSelection::expandSelectionToStartOfWordContainingCaretSelection()
{
if (m_selection.isNone() || isStartOfDocument(m_selection.start()))
return;
VisiblePosition s1(m_selection.start());
VisiblePosition e1(m_selection.end());
VisibleSelection expanded(wordSelectionContainingCaretSelection(m_selection));
VisiblePosition s2(expanded.start());
// Don't allow the start to become greater after the expansion.
if (s2.isNull() || s2 > s1)
s2 = s1;
moveTo(s2, e1);
}
UChar FrameSelection::characterInRelationToCaretSelection(int amount) const
{
auto position = m_selection.visibleStart();
if (amount < 0) {
int count = abs(amount);
for (int i = 0; i < count; i++)
position = position.previous();
return position.characterBefore();
}
for (int i = 0; i < amount; i++)
position = position.next();
return position.characterAfter();
}
bool FrameSelection::selectionAtWordStart() const
{
auto position = m_selection.visibleStart();
if (isStartOfParagraph(position))
return true;
unsigned previousCount = 0;
for (position = position.previous(); !position.isNull(); position = position.previous()) {
previousCount++;
if (isStartOfParagraph(position))
return previousCount != 1;
if (UChar c = position.characterAfter())
return isSpaceOrNewline(c) || c == noBreakSpace || (u_ispunct(c) && c != ',' && c != '-' && c != '\'');
}
return true;
}
std::optional<SimpleRange> FrameSelection::rangeByMovingCurrentSelection(int amount) const
{
return rangeByAlteringCurrentSelection(AlterationMove, amount);
}
std::optional<SimpleRange> FrameSelection::rangeByExtendingCurrentSelection(int amount) const
{
return rangeByAlteringCurrentSelection(AlterationExtend, amount);
}
VisibleSelection FrameSelection::wordSelectionContainingCaretSelection(const VisibleSelection& selection)
{
if (selection.isNone())
return VisibleSelection();
ASSERT(selection.isCaretOrRange());
FrameSelection frameSelection;
frameSelection.setSelection(selection);
Position startPosBeforeExpansion(selection.start());
Position endPosBeforeExpansion(selection.end());
VisiblePosition startVisiblePosBeforeExpansion(startPosBeforeExpansion);
VisiblePosition endVisiblePosBeforeExpansion(endPosBeforeExpansion);
if (endVisiblePosBeforeExpansion.isNull())
return VisibleSelection();
if (isEndOfParagraph(endVisiblePosBeforeExpansion)) {
UChar c(endVisiblePosBeforeExpansion.characterBefore());
if (isSpaceOrNewline(c) || c == noBreakSpace) {
// End of paragraph with space.
return VisibleSelection();
}
}
// If at end of paragraph, move backwards one character.
// This has the effect of selecting the word on the line (which is
// what we want, rather than selecting past the end of the line).
if (isEndOfParagraph(endVisiblePosBeforeExpansion) && !isStartOfParagraph(endVisiblePosBeforeExpansion))
frameSelection.modify(FrameSelection::AlterationMove, SelectionDirection::Backward, TextGranularity::CharacterGranularity);
VisibleSelection newSelection = frameSelection.selection();
newSelection.expandUsingGranularity(TextGranularity::WordGranularity);
frameSelection.setSelection(newSelection, defaultSetSelectionOptions(), AXTextStateChangeIntent(), AlignCursorOnScrollIfNeeded, frameSelection.granularity());
Position startPos(frameSelection.selection().start());
Position endPos(frameSelection.selection().end());
// Expansion cannot be allowed to change selection so that it is no longer
// touches (or contains) the original, unexpanded selection.
// Enforce this on the way into these additional calculations to give them
// the best chance to yield a suitable answer.
if (startPos > startPosBeforeExpansion)
startPos = startPosBeforeExpansion;
if (endPos < endPosBeforeExpansion)
endPos = endPosBeforeExpansion;
VisiblePosition startVisiblePos(startPos);
VisiblePosition endVisiblePos(endPos);
if (startVisiblePos.isNull() || endVisiblePos.isNull()) {
// Start or end is nil
return VisibleSelection();
}
if (isEndOfLine(endVisiblePosBeforeExpansion)) {
VisiblePosition previous(endVisiblePos.previous());
if (previous == endVisiblePos) {
// Empty document
return VisibleSelection();
}
UChar c(previous.characterAfter());
if (isSpaceOrNewline(c) || c == noBreakSpace) {
// Space at end of line
return VisibleSelection();
}
}
// Expansion has selected past end of line.
// Try repositioning backwards.
if (isEndOfLine(startVisiblePos) && isStartOfLine(endVisiblePos)) {
VisiblePosition previous(startVisiblePos.previous());
if (isEndOfLine(previous)) {
// On empty line
return VisibleSelection();
}
UChar c(previous.characterAfter());
if (isSpaceOrNewline(c) || c == noBreakSpace) {
// Space at end of line
return VisibleSelection();
}
frameSelection.moveTo(startVisiblePos);
frameSelection.modify(FrameSelection::AlterationExtend, SelectionDirection::Backward, TextGranularity::WordGranularity);
startPos = frameSelection.selection().start();
endPos = frameSelection.selection().end();
startVisiblePos = VisiblePosition(startPos);
endVisiblePos = VisiblePosition(endPos);
if (startVisiblePos.isNull() || endVisiblePos.isNull()) {
// Start or end is nil
return VisibleSelection();
}
}
// Now loop backwards until we find a non-space.
while (endVisiblePos != startVisiblePos) {
VisiblePosition previous(endVisiblePos.previous());
UChar c(previous.characterAfter());
if (!isSpaceOrNewline(c) && c != noBreakSpace)
break;
endVisiblePos = previous;
}
// Expansion cannot be allowed to change selection so that it is no longer
// touches (or contains) the original, unexpanded selection.
// Enforce this on the way out of the function to preserve the invariant.
if (startVisiblePos > startVisiblePosBeforeExpansion)
startVisiblePos = startVisiblePosBeforeExpansion;
if (endVisiblePos < endVisiblePosBeforeExpansion)
endVisiblePos = endVisiblePosBeforeExpansion;
return VisibleSelection(startVisiblePos, endVisiblePos);
}
bool FrameSelection::selectionAtSentenceStart() const
{
auto position = m_selection.visibleStart();
if (position.isNull())
return false;
if (isStartOfParagraph(position))
return true;
bool sawSpace = false;
unsigned previousCount = 0;
for (position = position.previous(); !position.isNull(); position = position.previous()) {
previousCount++;
if (isStartOfParagraph(position))
return previousCount != 1 && (previousCount != 2 || !sawSpace);
if (auto c = position.characterAfter()) {
if (isSpaceOrNewline(c) || c == noBreakSpace)
sawSpace = true;
else
return c == '.' || c == '!' || c == '?';
}
}
return true;
}
std::optional<SimpleRange> FrameSelection::rangeByAlteringCurrentSelection(EAlteration alteration, int amount) const
{
if (m_selection.isNone())
return std::nullopt;
if (!amount)
return m_selection.toNormalizedRange();
FrameSelection frameSelection;
frameSelection.setSelection(m_selection);
SelectionDirection direction = amount > 0 ? SelectionDirection::Forward : SelectionDirection::Backward;
for (int i = 0; i < abs(amount); i++)
frameSelection.modify(alteration, direction, TextGranularity::CharacterGranularity);
return frameSelection.selection().toNormalizedRange();
}
void FrameSelection::clearCurrentSelection()
{
setSelection(VisibleSelection());
}
void FrameSelection::setCaretBlinks(bool caretBlinks)
{
if (m_caretBlinks == caretBlinks)
return;
#if ENABLE(TEXT_CARET)
m_document->updateLayoutIgnorePendingStylesheets();
if (m_caretPaint) {
m_caretPaint = false;
invalidateCaretRect();
}
#endif
if (caretBlinks)
setFocusedElementIfNeeded();
m_caretBlinks = caretBlinks;
updateAppearance();
}
void FrameSelection::setCaretColor(const Color& caretColor)
{
if (m_caretColor != caretColor) {
m_caretColor = caretColor;
if (caretIsVisible() && m_caretBlinks && isCaret())
invalidateCaretRect();
}
}
#endif // PLATFORM(IOS_FAMILY)
static bool containsEndpoints(const WeakPtr<Document>& document, const std::optional<SimpleRange>& range)
{
return document && range && document->contains(range->start.container) && document->contains(range->end.container);
}
static bool containsEndpoints(const WeakPtr<Document>& document, const Range& liveRange)
{
// Only need to check the start container because live ranges enforce the invariant that start and end have a common ancestor.
return document && document->contains(liveRange.startContainer());
}
bool FrameSelection::isInDocumentTree() const
{
return containsEndpoints(m_document, m_selection.range());
}
bool FrameSelection::isConnectedToDocument() const
{
return selection().document() == m_document.get();
}
RefPtr<Range> FrameSelection::associatedLiveRange()
{
if (!m_associatedLiveRange) {
if (auto range = m_selection.range(); containsEndpoints(m_document, range)) {
m_associatedLiveRange = createLiveRange(*range);
m_associatedLiveRange->didAssociateWithSelection();
}
}
return m_associatedLiveRange;
}
void FrameSelection::disassociateLiveRange()
{
if (auto previouslyAssociatedLiveRange = std::exchange(m_associatedLiveRange, nullptr))
previouslyAssociatedLiveRange->didDisassociateFromSelection();
}
void FrameSelection::associateLiveRange(Range& liveRange)
{
disassociateLiveRange();
m_associatedLiveRange = &liveRange;
liveRange.didAssociateWithSelection();
updateFromAssociatedLiveRange();
}
void FrameSelection::updateFromAssociatedLiveRange()
{
ASSERT(m_associatedLiveRange);
if (!containsEndpoints(m_document, *m_associatedLiveRange))
disassociateLiveRange();
else {
// Don't use VisibleSelection's constructor that takes a SimpleRange, because it uses makeDeprecatedLegacyPosition instead of makeContainerOffsetPosition.
auto start = makeContainerOffsetPosition(&m_associatedLiveRange->startContainer(), m_associatedLiveRange->startOffset());
auto end = makeContainerOffsetPosition(&m_associatedLiveRange->endContainer(), m_associatedLiveRange->endOffset());
setSelection({ start, end });
}
}
void FrameSelection::updateAssociatedLiveRange()
{
auto range = m_selection.range();
if (!containsEndpoints(m_document, range)) {
// The selection was cleared or is now within a shadow tree.
disassociateLiveRange();
} else {
if (m_associatedLiveRange)
m_associatedLiveRange->updateFromSelection(*range);
}
}
}
#if ENABLE(TREE_DEBUGGING)
void showTree(const WebCore::FrameSelection& selection)
{
selection.showTreeForThis();
}
void showTree(const WebCore::FrameSelection* selection)
{
if (selection)
selection->showTreeForThis();
}
#endif