blob: 15237a365dc2db106f2b97c518e6405d203d4c7b [file] [log] [blame]
/*
* Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "core/editing/FrameSelection.h"
#include <stdio.h>
#include "bindings/core/v8/ExceptionState.h"
#include "core/HTMLNames.h"
#include "core/css/StylePropertySet.h"
#include "core/dom/AXObjectCache.h"
#include "core/dom/CharacterData.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/NodeTraversal.h"
#include "core/dom/NodeWithIndex.h"
#include "core/dom/Text.h"
#include "core/editing/CaretDisplayItemClient.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/Editor.h"
#include "core/editing/FrameCaret.h"
#include "core/editing/GranularityStrategy.h"
#include "core/editing/InputMethodController.h"
#include "core/editing/LayoutSelection.h"
#include "core/editing/SelectionController.h"
#include "core/editing/SelectionEditor.h"
#include "core/editing/SelectionModifier.h"
#include "core/editing/TextAffinity.h"
#include "core/editing/VisibleUnits.h"
#include "core/editing/commands/TypingCommand.h"
#include "core/editing/iterators/TextIterator.h"
#include "core/editing/serializers/Serialization.h"
#include "core/editing/spellcheck/SpellChecker.h"
#include "core/events/Event.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalDOMWindow.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLBodyElement.h"
#include "core/html/HTMLFormElement.h"
#include "core/html/HTMLFrameElementBase.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/HTMLSelectElement.h"
#include "core/input/EventHandler.h"
#include "core/layout/HitTestRequest.h"
#include "core/layout/HitTestResult.h"
#include "core/layout/LayoutPart.h"
#include "core/layout/api/LayoutViewItem.h"
#include "core/loader/DocumentLoader.h"
#include "core/page/EditorClient.h"
#include "core/page/FocusController.h"
#include "core/page/FrameTree.h"
#include "core/page/Page.h"
#include "core/page/SpatialNavigation.h"
#include "core/paint/PaintLayer.h"
#include "platform/SecureTextInput.h"
#include "platform/geometry/FloatQuad.h"
#include "platform/graphics/GraphicsContext.h"
#include "platform/text/UnicodeUtilities.h"
#include "wtf/PtrUtil.h"
#include "wtf/text/CString.h"
#define EDIT_DEBUG 0
namespace blink {
using namespace HTMLNames;
static inline bool shouldAlwaysUseDirectionalSelection(LocalFrame* frame) {
return frame->editor().behavior().shouldConsiderSelectionAsDirectional();
}
FrameSelection::FrameSelection(LocalFrame& frame)
: m_frame(frame),
m_layoutSelection(LayoutSelection::create(*this)),
m_selectionEditor(SelectionEditor::create(frame)),
m_granularity(CharacterGranularity),
m_xPosForVerticalArrowNavigation(NoXPosForVerticalArrowNavigation()),
m_focused(frame.page() &&
frame.page()->focusController().focusedFrame() == frame),
m_frameCaret(new FrameCaret(frame, *m_selectionEditor)) {}
FrameSelection::~FrameSelection() {}
const DisplayItemClient& FrameSelection::caretDisplayItemClientForTesting()
const {
return m_frameCaret->displayItemClient();
}
Document& FrameSelection::document() const {
DCHECK(lifecycleContext());
return *lifecycleContext();
}
bool FrameSelection::isHandleVisible() const {
return selectionInDOMTree().isHandleVisible();
}
const VisibleSelection& FrameSelection::computeVisibleSelectionInDOMTree()
const {
return m_selectionEditor->computeVisibleSelectionInDOMTree();
}
const VisibleSelectionInFlatTree&
FrameSelection::computeVisibleSelectionInFlatTree() const {
return m_selectionEditor->computeVisibleSelectionInFlatTree();
}
const SelectionInDOMTree& FrameSelection::selectionInDOMTree() const {
return m_selectionEditor->selectionInDOMTree();
}
Element* FrameSelection::rootEditableElementOrDocumentElement() const {
Element* selectionRoot =
computeVisibleSelectionInDOMTreeDeprecated().rootEditableElement();
return selectionRoot ? selectionRoot : document().documentElement();
}
// TODO(yosin): We should move |rootEditableElementOrTreeScopeRootNodeOf()| to
// "EditingUtilities.cpp"
ContainerNode* rootEditableElementOrTreeScopeRootNodeOf(
const Position& position) {
Element* selectionRoot = rootEditableElementOf(position);
if (selectionRoot)
return selectionRoot;
Node* const node = position.computeContainerNode();
return node ? &node->treeScope().rootNode() : 0;
}
const VisibleSelection&
FrameSelection::computeVisibleSelectionInDOMTreeDeprecated() const {
// TODO(yosin): We should hoist updateStyleAndLayoutIgnorePendingStylesheets
// to caller. See http://crbug.com/590369 for more details.
document().updateStyleAndLayoutIgnorePendingStylesheets();
return computeVisibleSelectionInDOMTree();
}
const VisibleSelectionInFlatTree& FrameSelection::selectionInFlatTree() const {
return computeVisibleSelectionInFlatTree();
}
void FrameSelection::moveCaretSelection(const IntPoint& point) {
DCHECK(!document().needsLayoutTreeUpdate());
Element* const editable =
computeVisibleSelectionInDOMTree().rootEditableElement();
if (!editable)
return;
const VisiblePosition position =
visiblePositionForContentsPoint(point, frame());
SelectionInDOMTree::Builder builder;
builder.setIsDirectional(selectionInDOMTree().isDirectional());
builder.setIsHandleVisible(true);
if (position.isNotNull())
builder.collapse(position.toPositionWithAffinity());
setSelection(builder.build(), CloseTyping | ClearTypingStyle | UserTriggered);
}
void FrameSelection::setSelection(const SelectionInDOMTree& passedSelection,
SetSelectionOptions options,
CursorAlignOnScroll align,
TextGranularity granularity) {
if (setSelectionDeprecated(passedSelection, options, granularity))
didSetSelectionDeprecated(options, align);
}
bool FrameSelection::setSelectionDeprecated(
const SelectionInDOMTree& passedSelection,
SetSelectionOptions options,
TextGranularity granularity) {
DCHECK(isAvailable());
passedSelection.assertValidFor(document());
SelectionInDOMTree::Builder builder(passedSelection);
if (shouldAlwaysUseDirectionalSelection(m_frame))
builder.setIsDirectional(true);
SelectionInDOMTree newSelection = builder.build();
if (m_granularityStrategy &&
(options & FrameSelection::DoNotClearStrategy) == 0)
m_granularityStrategy->Clear();
bool closeTyping = options & CloseTyping;
bool shouldClearTypingStyle = options & ClearTypingStyle;
m_granularity = granularity;
// TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
// |Editor| class.
if (closeTyping)
TypingCommand::closeTyping(m_frame);
if (shouldClearTypingStyle)
m_frame->editor().clearTypingStyle();
const SelectionInDOMTree oldSelectionInDOMTree =
m_selectionEditor->selectionInDOMTree();
if (oldSelectionInDOMTree == newSelection)
return false;
m_selectionEditor->setSelection(newSelection);
scheduleVisualUpdateForPaintInvalidationIfNeeded();
const Document& currentDocument = document();
// TODO(yosin): We should get rid of unsued |options| for
// |Editor::respondToChangedSelection()|.
// Note: Since, setting focus can modify DOM tree, we should use
// |oldSelection| before setting focus
m_frame->editor().respondToChangedSelection(
oldSelectionInDOMTree.computeStartPosition(), options);
DCHECK_EQ(currentDocument, document());
return true;
}
void FrameSelection::didSetSelectionDeprecated(SetSelectionOptions options,
CursorAlignOnScroll align) {
const Document& currentDocument = document();
if (!selectionInDOMTree().isNone() && !(options & DoNotSetFocus)) {
setFocusedNodeIfNeeded();
// |setFocusedNodeIfNeeded()| dispatches sync events "FocusOut" and
// "FocusIn", |m_frame| may associate to another document.
if (!isAvailable() || document() != currentDocument) {
// Once we get test case to reach here, we should change this
// if-statement to |DCHECK()|.
NOTREACHED();
return;
}
}
m_frameCaret->stopCaretBlinkTimer();
updateAppearance();
// Always clear the x position used for vertical arrow navigation.
// It will be restored by the vertical arrow navigation code if necessary.
m_xPosForVerticalArrowNavigation = NoXPosForVerticalArrowNavigation();
// TODO(yosin): Can we move this to at end of this function?
// This may dispatch a synchronous focus-related events.
if (!(options & DoNotSetFocus)) {
selectFrameElementInParentIfFullySelected();
if (!isAvailable() || document() != currentDocument) {
// editing/selection/selectallchildren-crash.html and
// editing/selection/longpress-selection-in-iframe-removed-crash.html
// reach here.
return;
}
}
EUserTriggered userTriggered = selectionOptionsToUserTriggered(options);
notifyLayoutObjectOfSelectionChange(userTriggered);
if (userTriggered == UserTriggered) {
ScrollAlignment alignment;
if (m_frame->editor().behavior().shouldCenterAlignWhenSelectionIsRevealed())
alignment = (align == CursorAlignOnScroll::Always)
? ScrollAlignment::alignCenterAlways
: ScrollAlignment::alignCenterIfNeeded;
else
alignment = (align == CursorAlignOnScroll::Always)
? ScrollAlignment::alignTopAlways
: ScrollAlignment::alignToEdgeIfNeeded;
revealSelection(alignment, RevealExtent);
}
notifyAccessibilityForSelectionChange();
notifyCompositorForSelectionChange();
notifyEventHandlerForSelectionChange();
m_frame->domWindow()->enqueueDocumentEvent(
Event::create(EventTypeNames::selectionchange));
}
void FrameSelection::setSelection(const SelectionInFlatTree& newSelection,
SetSelectionOptions options,
CursorAlignOnScroll align,
TextGranularity granularity) {
newSelection.assertValidFor(document());
SelectionInDOMTree::Builder builder;
builder.setAffinity(newSelection.affinity())
.setBaseAndExtent(toPositionInDOMTree(newSelection.base()),
toPositionInDOMTree(newSelection.extent()))
.setGranularity(newSelection.granularity())
.setIsDirectional(newSelection.isDirectional())
.setIsHandleVisible(newSelection.isHandleVisible())
.setHasTrailingWhitespace(newSelection.hasTrailingWhitespace());
return setSelection(builder.build(), options, align, granularity);
}
void FrameSelection::nodeChildrenWillBeRemoved(ContainerNode& container) {
if (!container.inActiveDocument())
return;
// TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
// |Editor| class.
if (!document().isRunningExecCommand())
TypingCommand::closeTyping(m_frame);
}
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 (!node.inActiveDocument())
return;
// TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
// |Editor| class.
if (!document().isRunningExecCommand())
TypingCommand::closeTyping(m_frame);
}
void FrameSelection::didChangeFocus() {
// Hits in
// virtual/gpu/compositedscrolling/scrollbars/scrollbar-miss-mousemove-disabled.html
DisableCompositingQueryAsserts disabler;
updateAppearance();
}
static DispatchEventResult dispatchSelectStart(
const VisibleSelection& selection) {
Node* selectStartTarget = selection.extent().computeContainerNode();
if (!selectStartTarget)
return DispatchEventResult::NotCanceled;
return selectStartTarget->dispatchEvent(
Event::createCancelableBubble(EventTypeNames::selectstart));
}
// The return value of |FrameSelection::modify()| is different based on
// value of |userTriggered| parameter.
// When |userTriggered| is |userTriggered|, |modify()| returns false if
// "selectstart" event is dispatched and canceled, otherwise returns true.
// When |userTriggered| is |NotUserTrigged|, return value specifies whether
// selection is modified or not.
bool FrameSelection::modify(EAlteration alter,
SelectionDirection direction,
TextGranularity granularity,
EUserTriggered userTriggered) {
SelectionModifier selectionModifier(
*frame(), computeVisibleSelectionInDOMTreeDeprecated(),
m_xPosForVerticalArrowNavigation);
const bool modified = selectionModifier.modify(alter, direction, granularity);
if (userTriggered == UserTriggered &&
selectionModifier.selection().isRange() &&
computeVisibleSelectionInDOMTreeDeprecated().isCaret() &&
dispatchSelectStart(computeVisibleSelectionInDOMTreeDeprecated()) !=
DispatchEventResult::NotCanceled) {
return false;
}
if (!modified) {
if (userTriggered == NotUserTriggered)
return false;
// If spatial navigation enabled, focus navigator will move focus to
// another element. See snav-input.html and snav-textarea.html
if (isSpatialNavigationEnabled(m_frame))
return false;
// Even if selection isn't changed, we prevent to default action, e.g.
// scroll window when caret is at end of content editable.
return true;
}
const SetSelectionOptions options =
CloseTyping | ClearTypingStyle | userTriggered;
setSelection(selectionModifier.selection().asSelection(), options);
if (granularity == LineGranularity || granularity == ParagraphGranularity)
m_xPosForVerticalArrowNavigation =
selectionModifier.xPosForVerticalArrowNavigation();
if (userTriggered == UserTriggered)
m_granularity = CharacterGranularity;
scheduleVisualUpdateForPaintInvalidationIfNeeded();
return true;
}
bool FrameSelection::modify(EAlteration alter,
unsigned verticalDistance,
VerticalDirection direction) {
SelectionModifier selectionModifier(
*frame(), computeVisibleSelectionInDOMTreeDeprecated());
if (!selectionModifier.modifyWithPageGranularity(alter, verticalDistance,
direction)) {
return false;
}
setSelection(selectionModifier.selection().asSelection(),
CloseTyping | ClearTypingStyle | UserTriggered,
alter == AlterationMove ? CursorAlignOnScroll::Always
: CursorAlignOnScroll::IfNeeded);
m_granularity = CharacterGranularity;
return true;
}
void FrameSelection::clear() {
m_granularity = CharacterGranularity;
if (m_granularityStrategy)
m_granularityStrategy->Clear();
setSelection(SelectionInDOMTree());
}
void FrameSelection::documentAttached(Document* document) {
DCHECK(document);
m_useSecureKeyboardEntryWhenActive = false;
m_selectionEditor->documentAttached(document);
setContext(document);
}
void FrameSelection::contextDestroyed(Document* document) {
m_granularity = CharacterGranularity;
LayoutViewItem view = m_frame->contentLayoutItem();
if (!view.isNull())
view.clearSelection();
m_frame->editor().clearTypingStyle();
}
void FrameSelection::clearPreviousCaretVisualRect(const LayoutBlock& block) {
m_frameCaret->clearPreviousVisualRect(block);
}
void FrameSelection::layoutBlockWillBeDestroyed(const LayoutBlock& block) {
m_frameCaret->layoutBlockWillBeDestroyed(block);
}
void FrameSelection::updateStyleAndLayoutIfNeeded() {
m_frameCaret->updateStyleAndLayoutIfNeeded();
}
void FrameSelection::invalidatePaintIfNeeded(
const LayoutBlock& block,
const PaintInvalidatorContext& context) {
m_frameCaret->invalidatePaintIfNeeded(block, context);
}
bool FrameSelection::shouldPaintCaret(const LayoutBlock& block) const {
DCHECK_GE(document().lifecycle().state(), DocumentLifecycle::LayoutClean);
bool result = m_frameCaret->shouldPaintCaret(block);
DCHECK(!result ||
(computeVisibleSelectionInDOMTreeDeprecated().isCaret() &&
computeVisibleSelectionInDOMTree().hasEditableStyle()));
return result;
}
IntRect FrameSelection::absoluteCaretBounds() {
DCHECK(computeVisibleSelectionInDOMTree().isValidFor(*m_frame->document()));
return m_frameCaret->absoluteCaretBounds();
}
void FrameSelection::paintCaret(GraphicsContext& context,
const LayoutPoint& paintOffset) {
m_frameCaret->paintCaret(context, paintOffset);
}
bool FrameSelection::contains(const LayoutPoint& point) {
if (document().layoutViewItem().isNull())
return false;
// Treat a collapsed selection like no selection.
const VisibleSelectionInFlatTree& visibleSelection =
computeVisibleSelectionInFlatTree();
if (!visibleSelection.isRange())
return false;
HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active);
HitTestResult result(request, point);
document().layoutViewItem().hitTest(result);
Node* innerNode = result.innerNode();
if (!innerNode || !innerNode->layoutObject())
return false;
const VisiblePositionInFlatTree& visiblePos =
createVisiblePosition(fromPositionInDOMTree<EditingInFlatTreeStrategy>(
innerNode->layoutObject()->positionForPoint(result.localPoint())));
if (visiblePos.isNull())
return false;
const VisiblePositionInFlatTree& visibleStart =
visibleSelection.visibleStart();
const VisiblePositionInFlatTree& visibleEnd = visibleSelection.visibleEnd();
if (visibleStart.isNull() || visibleEnd.isNull())
return false;
const PositionInFlatTree& start = visibleStart.deepEquivalent();
const PositionInFlatTree& end = visibleEnd.deepEquivalent();
const PositionInFlatTree& pos = visiblePos.deepEquivalent();
return start.compareTo(pos) <= 0 && pos.compareTo(end) <= 0;
}
// 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.
Frame* parent = m_frame->tree().parent();
if (!parent)
return;
Page* page = m_frame->page();
if (!page)
return;
// Check if the selection contains the entire frame contents; if not, then
// there is nothing to do.
if (selectionInDOMTree().selectionTypeWithLegacyGranularity() !=
RangeSelection) {
return;
}
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
document().updateStyleAndLayoutIgnorePendingStylesheets();
if (!isStartOfDocument(computeVisibleSelectionInDOMTree().visibleStart()))
return;
if (!isEndOfDocument(computeVisibleSelectionInDOMTree().visibleEnd()))
return;
// FIXME: This is not yet implemented for cross-process frame relationships.
if (!parent->isLocalFrame())
return;
// Get to the <iframe> or <frame> (or even <object>) element in the parent
// frame.
// FIXME: Doesn't work for OOPI.
HTMLFrameOwnerElement* ownerElement = m_frame->deprecatedLocalOwner();
if (!ownerElement)
return;
ContainerNode* ownerElementParent = ownerElement->parentNode();
if (!ownerElementParent)
return;
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
ownerElementParent->document().updateStyleAndLayoutIgnorePendingStylesheets();
// 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 (!blink::hasEditableStyle(*ownerElementParent))
return;
// Create compute positions before and after the element.
unsigned ownerElementNodeIndex = ownerElement->nodeIndex();
VisiblePosition beforeOwnerElement = createVisiblePosition(
Position(ownerElementParent, ownerElementNodeIndex));
VisiblePosition afterOwnerElement = createVisiblePosition(
Position(ownerElementParent, ownerElementNodeIndex + 1),
VP_UPSTREAM_IF_POSSIBLE);
SelectionInDOMTree::Builder builder;
builder
.setBaseAndExtentDeprecated(beforeOwnerElement.deepEquivalent(),
afterOwnerElement.deepEquivalent())
.setAffinity(beforeOwnerElement.affinity());
// Focus on the parent frame, and then select from before this element to
// after.
VisibleSelection newSelection = createVisibleSelection(builder.build());
// TODO(yosin): We should call |FocusController::setFocusedFrame()| before
// |createVisibleSelection()|.
page->focusController().setFocusedFrame(parent);
// setFocusedFrame can dispatch synchronous focus/blur events. The document
// tree might be modified.
if (newSelection.isNonOrphanedCaretOrRange())
toLocalFrame(parent)->selection().setSelection(newSelection.asSelection());
}
// Returns a shadow tree node for legacy shadow trees, a child of the
// ShadowRoot node for new shadow trees, or 0 for non-shadow trees.
static Node* nonBoundaryShadowTreeRootNode(const Position& position) {
return position.anchorNode() && !position.anchorNode()->isShadowRoot()
? position.anchorNode()->nonBoundaryShadowTreeRootNode()
: nullptr;
}
void FrameSelection::selectAll() {
if (isHTMLSelectElement(document().focusedElement())) {
HTMLSelectElement* selectElement =
toHTMLSelectElement(document().focusedElement());
if (selectElement->canSelectAll()) {
selectElement->selectAll();
return;
}
}
Node* root = nullptr;
Node* selectStartTarget = nullptr;
if (computeVisibleSelectionInDOMTreeDeprecated().isContentEditable()) {
root = highestEditableRoot(
computeVisibleSelectionInDOMTreeDeprecated().start());
if (Node* shadowRoot = nonBoundaryShadowTreeRootNode(
computeVisibleSelectionInDOMTreeDeprecated().start()))
selectStartTarget = shadowRoot->ownerShadowHost();
else
selectStartTarget = root;
} else {
root = nonBoundaryShadowTreeRootNode(
computeVisibleSelectionInDOMTreeDeprecated().start());
if (root) {
selectStartTarget = root->ownerShadowHost();
} else {
root = document().documentElement();
selectStartTarget = document().body();
}
}
if (!root || editingIgnoresContent(*root))
return;
if (selectStartTarget) {
const Document& expectedDocument = document();
if (selectStartTarget->dispatchEvent(Event::createCancelableBubble(
EventTypeNames::selectstart)) != DispatchEventResult::NotCanceled)
return;
// |root| may be detached due to selectstart event.
if (!root->isConnected() || expectedDocument != root->document())
return;
}
setSelection(SelectionInDOMTree::Builder()
.selectAllChildren(*root)
.setIsHandleVisible(isHandleVisible())
.build());
selectFrameElementInParentIfFullySelected();
notifyLayoutObjectOfSelectionChange(UserTriggered);
}
bool FrameSelection::setSelectedRange(const EphemeralRange& range,
TextAffinity affinity,
SelectionDirectionalMode directional,
SetSelectionOptions options) {
if (range.isNull())
return false;
setSelection(SelectionInDOMTree::Builder()
.setBaseAndExtent(range)
.setAffinity(affinity)
.setIsHandleVisible(isHandleVisible())
.setIsDirectional(directional ==
SelectionDirectionalMode::Directional)
.build(),
options);
return true;
}
void FrameSelection::notifyAccessibilityForSelectionChange() {
if (selectionInDOMTree().isNone())
return;
AXObjectCache* cache = document().existingAXObjectCache();
if (!cache)
return;
const Position& start = selectionInDOMTree().computeStartPosition();
cache->selectionChanged(start.computeContainerNode());
}
void FrameSelection::notifyCompositorForSelectionChange() {
if (!RuntimeEnabledFeatures::compositedSelectionUpdateEnabled())
return;
scheduleVisualUpdate();
}
void FrameSelection::notifyEventHandlerForSelectionChange() {
m_frame->eventHandler().selectionController().notifySelectionChanged();
}
void FrameSelection::focusedOrActiveStateChanged() {
bool activeAndFocused = isFocusedAndActive();
// Trigger style invalidation from the focused element. Even though
// the focused element hasn't changed, the evaluation of focus pseudo
// selectors are dependent on whether the frame is focused and active.
if (Element* element = document().focusedElement())
element->focusStateChanged();
document().updateStyleAndLayoutTree();
// Because LayoutObject::selectionBackgroundColor() and
// LayoutObject::selectionForegroundColor() check if the frame is active,
// we have to update places those colors were painted.
LayoutViewItem view = document().layoutViewItem();
if (!view.isNull())
view.invalidatePaintForSelection();
// Caret appears in the active frame.
if (activeAndFocused)
setSelectionFromNone();
else
m_frame->spellChecker().spellCheckAfterBlur();
m_frameCaret->setCaretVisibility(activeAndFocused ? CaretVisibility::Visible
: CaretVisibility::Hidden);
// Update for caps lock state
m_frame->eventHandler().capsLockStateMayHaveChanged();
// Secure keyboard entry is set by the active frame.
if (m_useSecureKeyboardEntryWhenActive)
setUseSecureKeyboardEntry(activeAndFocused);
}
void FrameSelection::pageActivationChanged() {
focusedOrActiveStateChanged();
}
void FrameSelection::updateSecureKeyboardEntryIfActive() {
if (!isFocusedAndActive())
return;
setUseSecureKeyboardEntry(m_useSecureKeyboardEntryWhenActive);
}
void FrameSelection::setUseSecureKeyboardEntryWhenActive(
bool usesSecureKeyboard) {
if (m_useSecureKeyboardEntryWhenActive == usesSecureKeyboard)
return;
m_useSecureKeyboardEntryWhenActive = usesSecureKeyboard;
updateSecureKeyboardEntryIfActive();
}
void FrameSelection::setUseSecureKeyboardEntry(bool enable) {
if (enable)
enableSecureTextInput();
else
disableSecureTextInput();
}
void FrameSelection::setFocused(bool flag) {
if (m_focused == flag)
return;
m_focused = flag;
focusedOrActiveStateChanged();
}
bool FrameSelection::isFocusedAndActive() const {
return m_focused && m_frame->page() &&
m_frame->page()->focusController().isActive();
}
bool FrameSelection::isAppearanceDirty() const {
return m_layoutSelection->hasPendingSelection();
}
void FrameSelection::commitAppearanceIfNeeded(LayoutView& layoutView) {
return m_layoutSelection->commit(layoutView);
}
void FrameSelection::didLayout() {
updateAppearance();
}
void FrameSelection::updateAppearance() {
DCHECK(!m_frame->contentLayoutItem().isNull());
m_frameCaret->scheduleVisualUpdateForPaintInvalidationIfNeeded();
m_layoutSelection->setHasPendingSelection();
}
void FrameSelection::notifyLayoutObjectOfSelectionChange(
EUserTriggered userTriggered) {
TextControlElement* textControl =
enclosingTextControl(selectionInDOMTree().base());
if (!textControl)
return;
textControl->selectionChanged(userTriggered == UserTriggered);
}
// Helper function that tells whether a particular node is an element that has
// an entire LocalFrame and FrameView, a <frame>, <iframe>, or <object>.
static bool isFrameElement(const Node* n) {
if (!n)
return false;
LayoutObject* layoutObject = n->layoutObject();
if (!layoutObject || !layoutObject->isLayoutPart())
return false;
FrameViewBase* frameViewBase = toLayoutPart(layoutObject)->frameViewBase();
return frameViewBase && frameViewBase->isFrameView();
}
void FrameSelection::setFocusedNodeIfNeeded() {
if (computeVisibleSelectionInDOMTreeDeprecated().isNone() || !isFocused())
return;
if (Element* target =
computeVisibleSelectionInDOMTreeDeprecated().rootEditableElement()) {
// Walk up the DOM tree to search for a node to focus.
document().updateStyleAndLayoutTreeIgnorePendingStylesheets();
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)) {
m_frame->page()->focusController().setFocusedElement(target, m_frame);
return;
}
target = target->parentOrShadowHostElement();
}
document().clearFocusedElement();
}
}
static String extractSelectedText(const FrameSelection& selection,
TextIteratorBehavior behavior) {
const VisibleSelectionInFlatTree& visibleSelection =
selection.computeVisibleSelectionInFlatTree();
const EphemeralRangeInFlatTree& range =
visibleSelection.toNormalizedEphemeralRange();
// We remove '\0' characters because they are not visibly rendered to the
// user.
return plainText(range, behavior).replace(0, "");
}
String FrameSelection::selectedHTMLForClipboard() const {
const VisibleSelectionInFlatTree& visibleSelection =
computeVisibleSelectionInFlatTree();
const EphemeralRangeInFlatTree& range =
visibleSelection.toNormalizedEphemeralRange();
return createMarkup(range.startPosition(), range.endPosition(),
AnnotateForInterchange,
ConvertBlocksToInlines::NotConvert, ResolveNonLocalURLs);
}
String FrameSelection::selectedText(
const TextIteratorBehavior& behavior) const {
return extractSelectedText(*this, behavior);
}
String FrameSelection::selectedText() const {
return selectedText(TextIteratorBehavior());
}
String FrameSelection::selectedTextForClipboard() const {
return extractSelectedText(
*this, TextIteratorBehavior::Builder()
.setEmitsImageAltText(
m_frame->settings() &&
m_frame->settings()->getSelectionIncludesAltImageText())
.build());
}
LayoutRect FrameSelection::bounds() const {
FrameView* view = m_frame->view();
if (!view)
return LayoutRect();
return intersection(unclippedBounds(),
LayoutRect(view->visibleContentRect()));
}
LayoutRect FrameSelection::unclippedBounds() const {
FrameView* view = m_frame->view();
LayoutViewItem layoutView = m_frame->contentLayoutItem();
if (!view || layoutView.isNull())
return LayoutRect();
view->updateLifecycleToLayoutClean();
return LayoutRect(layoutView.selectionBounds());
}
static inline HTMLFormElement* associatedFormElement(HTMLElement& element) {
if (isHTMLFormElement(element))
return &toHTMLFormElement(element);
return element.formOwner();
}
// Scans logically forward from "start", including any child frames.
static HTMLFormElement* scanForForm(Node* start) {
if (!start)
return 0;
for (HTMLElement& element : Traversal<HTMLElement>::startsAt(
start->isHTMLElement() ? toHTMLElement(start)
: Traversal<HTMLElement>::next(*start))) {
if (HTMLFormElement* form = associatedFormElement(element))
return form;
if (isHTMLFrameElementBase(element)) {
Node* childDocument = toHTMLFrameElementBase(element).contentDocument();
if (HTMLFormElement* frameResult = scanForForm(childDocument))
return frameResult;
}
}
return 0;
}
// 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.
Node* start = document().focusedElement();
if (!start)
start = computeVisibleSelectionInDOMTreeDeprecated().start().anchorNode();
if (!start)
return 0;
// Try walking up the node tree to find a form element.
for (HTMLElement* element =
Traversal<HTMLElement>::firstAncestorOrSelf(*start);
element; element = Traversal<HTMLElement>::firstAncestor(*element)) {
if (HTMLFormElement* form = associatedFormElement(*element))
return form;
}
// Try walking forward in the node tree to find a form element.
return scanForForm(start);
}
void FrameSelection::revealSelection(const ScrollAlignment& alignment,
RevealExtentOption revealExtentOption) {
DCHECK(isAvailable());
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// Calculation of absolute caret bounds requires clean layout.
document().updateStyleAndLayoutIgnorePendingStylesheets();
LayoutRect rect;
switch (computeVisibleSelectionInDOMTree().getSelectionType()) {
case NoSelection:
return;
case CaretSelection:
rect = LayoutRect(absoluteCaretBounds());
break;
case RangeSelection:
rect = LayoutRect(revealExtentOption == RevealExtent
? absoluteCaretBoundsOf(createVisiblePosition(
computeVisibleSelectionInDOMTree().extent()))
: enclosingIntRect(unclippedBounds()));
break;
}
Position start = computeVisibleSelectionInDOMTreeDeprecated().start();
DCHECK(start.anchorNode());
if (start.anchorNode() && start.anchorNode()->layoutObject()) {
// FIXME: This code only handles scrolling the startContainer's layer, but
// the selection rect could intersect more than just that.
if (DocumentLoader* documentLoader = m_frame->loader().documentLoader())
documentLoader->initialScrollState().wasScrolledByUser = true;
if (start.anchorNode()->layoutObject()->scrollRectToVisible(rect, alignment,
alignment))
updateAppearance();
}
}
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).
Document* document = m_frame->document();
if (!computeVisibleSelectionInDOMTreeDeprecated().isNone() ||
!(blink::hasEditableStyle(*document)))
return;
Element* documentElement = document->documentElement();
if (!documentElement)
return;
if (HTMLBodyElement* body =
Traversal<HTMLBodyElement>::firstChild(*documentElement)) {
setSelection(SelectionInDOMTree::Builder()
.collapse(firstPositionInOrBeforeNode(body))
.build());
}
}
// TODO(yoichio): We should have LocalFrame having FrameCaret,
// Editor and PendingSelection using FrameCaret directly
// and get rid of this.
bool FrameSelection::shouldShowBlockCursor() const {
return m_frameCaret->shouldShowBlockCursor();
}
// TODO(yoichio): We should have LocalFrame having FrameCaret,
// Editor and PendingSelection using FrameCaret directly
// and get rid of this.
// TODO(yoichio): We should use "caret-shape" in "CSS Basic User Interface
// Module Level 4" https://drafts.csswg.org/css-ui-4/
// To use "caret-shape", we need to expose inserting mode information to CSS;
// https://github.com/w3c/csswg-drafts/issues/133
void FrameSelection::setShouldShowBlockCursor(bool shouldShowBlockCursor) {
m_frameCaret->setShouldShowBlockCursor(shouldShowBlockCursor);
}
#ifndef NDEBUG
void FrameSelection::showTreeForThis() const {
computeVisibleSelectionInDOMTreeDeprecated().showTreeForThis();
}
#endif
DEFINE_TRACE(FrameSelection) {
visitor->trace(m_frame);
visitor->trace(m_layoutSelection);
visitor->trace(m_selectionEditor);
visitor->trace(m_frameCaret);
SynchronousMutationObserver::trace(visitor);
}
void FrameSelection::scheduleVisualUpdate() const {
if (Page* page = m_frame->page())
page->animator().scheduleVisualUpdate(m_frame->localFrameRoot());
}
void FrameSelection::scheduleVisualUpdateForPaintInvalidationIfNeeded() const {
if (FrameView* frameView = m_frame->view())
frameView->scheduleVisualUpdateForPaintInvalidationIfNeeded();
}
bool FrameSelection::selectWordAroundPosition(const VisiblePosition& position) {
static const EWordSide wordSideList[2] = {RightWordIfOnBoundary,
LeftWordIfOnBoundary};
for (EWordSide wordSide : wordSideList) {
// TODO(yoichio): We should have Position version of |start/endOfWord|
// for avoiding unnecessary canonicalization.
VisiblePosition start = startOfWord(position, wordSide);
VisiblePosition end = endOfWord(position, wordSide);
String text =
plainText(EphemeralRange(start.deepEquivalent(), end.deepEquivalent()));
if (!text.isEmpty() && !isSeparator(text.characterStartingAt(0))) {
setSelection(SelectionInDOMTree::Builder()
.collapse(start.toPositionWithAffinity())
.extend(end.deepEquivalent())
.build(),
CloseTyping | ClearTypingStyle,
CursorAlignOnScroll::IfNeeded, WordGranularity);
return true;
}
}
return false;
}
GranularityStrategy* FrameSelection::granularityStrategy() {
// We do lazy initalization for m_granularityStrategy, because if we
// initialize it right in the constructor - the correct settings may not be
// set yet.
SelectionStrategy strategyType = SelectionStrategy::Character;
Settings* settings = m_frame ? m_frame->settings() : 0;
if (settings &&
settings->getSelectionStrategy() == SelectionStrategy::Direction)
strategyType = SelectionStrategy::Direction;
if (m_granularityStrategy && m_granularityStrategy->GetType() == strategyType)
return m_granularityStrategy.get();
if (strategyType == SelectionStrategy::Direction)
m_granularityStrategy = WTF::makeUnique<DirectionGranularityStrategy>();
else
m_granularityStrategy = WTF::makeUnique<CharacterGranularityStrategy>();
return m_granularityStrategy.get();
}
void FrameSelection::moveRangeSelectionExtent(const IntPoint& contentsPoint) {
if (computeVisibleSelectionInDOMTreeDeprecated().isNone())
return;
const SetSelectionOptions options =
FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle |
FrameSelection::DoNotClearStrategy | UserTriggered;
setSelection(SelectionInDOMTree::Builder(
granularityStrategy()->updateExtent(contentsPoint, m_frame))
.setIsHandleVisible(true)
.build(),
options);
}
// TODO(yosin): We should make |FrameSelection::moveRangeSelection()| to take
// two |IntPoint| instead of two |VisiblePosition| like
// |moveRangeSelectionExtent()|.
void FrameSelection::moveRangeSelection(const VisiblePosition& basePosition,
const VisiblePosition& extentPosition,
TextGranularity granularity) {
SelectionInDOMTree newSelection =
SelectionInDOMTree::Builder()
.setBaseAndExtentDeprecated(basePosition.deepEquivalent(),
extentPosition.deepEquivalent())
.setAffinity(basePosition.affinity())
.setGranularity(granularity)
.setIsHandleVisible(isHandleVisible())
.build();
if (newSelection.isNone())
return;
setSelection(newSelection, CloseTyping | ClearTypingStyle,
CursorAlignOnScroll::IfNeeded, granularity);
}
void FrameSelection::setCaretVisible(bool caretIsVisible) {
m_frameCaret->setCaretVisibility(caretIsVisible ? CaretVisibility::Visible
: CaretVisibility::Hidden);
}
void FrameSelection::setCaretBlinkingSuspended(bool suspended) {
m_frameCaret->setCaretBlinkingSuspended(suspended);
}
bool FrameSelection::isCaretBlinkingSuspended() const {
return m_frameCaret->isCaretBlinkingSuspended();
}
void FrameSelection::cacheRangeOfDocument(Range* range) {
m_selectionEditor->cacheRangeOfDocument(range);
}
Range* FrameSelection::documentCachedRange() const {
return m_selectionEditor->documentCachedRange();
}
void FrameSelection::clearDocumentCachedRange() {
m_selectionEditor->clearDocumentCachedRange();
}
} // namespace blink
#ifndef NDEBUG
void showTree(const blink::FrameSelection& sel) {
sel.showTreeForThis();
}
void showTree(const blink::FrameSelection* sel) {
if (sel)
sel->showTreeForThis();
else
LOG(INFO) << "Cannot showTree for <null> FrameSelection.";
}
#endif