blob: e67f81be07fa1e42a38d9b9d55a68e8ecacba91b [file] [log] [blame]
/*
* Copyright (C) 2007, 2009 Apple Inc. All rights reserved.
* Copyright (C) 2012 Google 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.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "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 OR ITS 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/DOMSelection.h"
#include "bindings/core/v8/ExceptionMessages.h"
#include "bindings/core/v8/ExceptionState.h"
#include "bindings/core/v8/ExceptionStatePlaceholder.h"
#include "core/dom/Document.h"
#include "core/dom/ExceptionCode.h"
#include "core/dom/Node.h"
#include "core/dom/Range.h"
#include "core/dom/TreeScope.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/FrameSelection.h"
#include "core/editing/iterators/TextIterator.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/UseCounter.h"
#include "core/inspector/ConsoleMessage.h"
#include "wtf/text/WTFString.h"
namespace blink {
static Position createPosition(Node* node, int offset) {
DCHECK_GE(offset, 0);
if (!node)
return Position();
return Position(node, offset);
}
static Node* selectionShadowAncestor(LocalFrame* frame) {
Node* node = frame->selection().selection().base().anchorNode();
if (!node)
return 0;
if (!node->isInShadowTree())
return 0;
return frame->document()->ancestorInThisScope(node);
}
DOMSelection::DOMSelection(const TreeScope* treeScope)
: DOMWindowProperty(treeScope->rootNode().document().frame()),
m_treeScope(treeScope) {}
void DOMSelection::clearTreeScope() {
m_treeScope = nullptr;
}
bool DOMSelection::isAvailable() const {
return frame() && frame()->selection().isAvailable();
}
const VisibleSelection& DOMSelection::visibleSelection() const {
DCHECK(frame());
return frame()->selection().selection();
}
static Position anchorPosition(const VisibleSelection& selection) {
Position anchor =
selection.isBaseFirst() ? selection.start() : selection.end();
return anchor.parentAnchoredEquivalent();
}
static Position focusPosition(const VisibleSelection& selection) {
Position focus =
selection.isBaseFirst() ? selection.end() : selection.start();
return focus.parentAnchoredEquivalent();
}
static Position basePosition(const VisibleSelection& selection) {
return selection.base().parentAnchoredEquivalent();
}
static Position extentPosition(const VisibleSelection& selection) {
return selection.extent().parentAnchoredEquivalent();
}
Node* DOMSelection::anchorNode() const {
if (!isAvailable())
return 0;
return shadowAdjustedNode(anchorPosition(visibleSelection()));
}
int DOMSelection::anchorOffset() const {
if (!isAvailable())
return 0;
return shadowAdjustedOffset(anchorPosition(visibleSelection()));
}
Node* DOMSelection::focusNode() const {
if (!isAvailable())
return 0;
return shadowAdjustedNode(focusPosition(visibleSelection()));
}
int DOMSelection::focusOffset() const {
if (!isAvailable())
return 0;
return shadowAdjustedOffset(focusPosition(visibleSelection()));
}
Node* DOMSelection::baseNode() const {
if (!isAvailable())
return 0;
return shadowAdjustedNode(basePosition(visibleSelection()));
}
int DOMSelection::baseOffset() const {
if (!isAvailable())
return 0;
return shadowAdjustedOffset(basePosition(visibleSelection()));
}
Node* DOMSelection::extentNode() const {
if (!isAvailable())
return 0;
return shadowAdjustedNode(extentPosition(visibleSelection()));
}
int DOMSelection::extentOffset() const {
if (!isAvailable())
return 0;
return shadowAdjustedOffset(extentPosition(visibleSelection()));
}
bool DOMSelection::isCollapsed() const {
if (!isAvailable() || selectionShadowAncestor(frame()))
return true;
return !frame()->selection().isRange();
}
String DOMSelection::type() const {
if (!isAvailable())
return String();
FrameSelection& selection = frame()->selection();
// This is a WebKit DOM extension, incompatible with an IE extension
// IE has this same attribute, but returns "none", "text" and "control"
// http://msdn.microsoft.com/en-us/library/ms534692(VS.85).aspx
if (selection.isNone())
return "None";
if (selection.isCaret())
return "Caret";
return "Range";
}
int DOMSelection::rangeCount() const {
if (!isAvailable())
return 0;
return frame()->selection().isNone() ? 0 : 1;
}
void DOMSelection::collapse(Node* node,
int offset,
ExceptionState& exceptionState) {
if (!isAvailable())
return;
if (!node) {
UseCounter::count(frame(), UseCounter::SelectionCollapseNull);
frame()->selection().clear();
return;
}
if (offset < 0) {
exceptionState.throwDOMException(
IndexSizeError, String::number(offset) + " is not a valid offset.");
return;
}
if (!isValidForPosition(node))
return;
Range::checkNodeWOffset(node, offset, exceptionState);
if (exceptionState.hadException())
return;
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// In the long term, we should change FrameSelection::setSelection to take a
// parameter that does not require clean layout, so that modifying selection
// no longer performs synchronous layout by itself.
frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets();
frame()->selection().setSelection(createVisibleSelection(
Position(node, offset), frame()->selection().isDirectional()));
}
void DOMSelection::collapseToEnd(ExceptionState& exceptionState) {
if (!isAvailable())
return;
const VisibleSelection& selection = frame()->selection().selection();
if (selection.isNone()) {
exceptionState.throwDOMException(InvalidStateError,
"there is no selection.");
return;
}
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// In the long term, we should change FrameSelection::setSelection to take a
// parameter that does not require clean layout, so that modifying selection
// no longer performs synchronous layout by itself.
frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets();
frame()->selection().moveTo(selection.end(), SelDefaultAffinity);
}
void DOMSelection::collapseToStart(ExceptionState& exceptionState) {
if (!isAvailable())
return;
const VisibleSelection& selection = frame()->selection().selection();
if (selection.isNone()) {
exceptionState.throwDOMException(InvalidStateError,
"there is no selection.");
return;
}
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// In the long term, we should change FrameSelection::setSelection to take a
// parameter that does not require clean layout, so that modifying selection
// no longer performs synchronous layout by itself.
frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets();
frame()->selection().moveTo(selection.start(), SelDefaultAffinity);
}
void DOMSelection::empty() {
if (!isAvailable())
return;
frame()->selection().clear();
}
void DOMSelection::setBaseAndExtent(Node* baseNode,
int baseOffset,
Node* extentNode,
int extentOffset,
ExceptionState& exceptionState) {
if (!isAvailable())
return;
if (baseOffset < 0) {
exceptionState.throwDOMException(
IndexSizeError,
String::number(baseOffset) + " is not a valid base offset.");
return;
}
if (extentOffset < 0) {
exceptionState.throwDOMException(
IndexSizeError,
String::number(extentOffset) + " is not a valid extent offset.");
return;
}
if (!baseNode || !extentNode)
UseCounter::count(frame(), UseCounter::SelectionSetBaseAndExtentNull);
if (!isValidForPosition(baseNode) || !isValidForPosition(extentNode))
return;
Position base = createPosition(baseNode, baseOffset);
Position extent = createPosition(extentNode, extentOffset);
const bool selectionHasDirection = true;
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// In the long term, we should change FrameSelection::setSelection to take a
// parameter that does not require clean layout, so that modifying selection
// no longer performs synchronous layout by itself.
frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets();
frame()->selection().setSelection(createVisibleSelection(
base, extent, SelDefaultAffinity, selectionHasDirection));
}
void DOMSelection::modify(const String& alterString,
const String& directionString,
const String& granularityString) {
if (!isAvailable())
return;
FrameSelection::EAlteration alter;
if (equalIgnoringCase(alterString, "extend"))
alter = FrameSelection::AlterationExtend;
else if (equalIgnoringCase(alterString, "move"))
alter = FrameSelection::AlterationMove;
else
return;
SelectionDirection direction;
if (equalIgnoringCase(directionString, "forward"))
direction = DirectionForward;
else if (equalIgnoringCase(directionString, "backward"))
direction = DirectionBackward;
else if (equalIgnoringCase(directionString, "left"))
direction = DirectionLeft;
else if (equalIgnoringCase(directionString, "right"))
direction = DirectionRight;
else
return;
TextGranularity granularity;
if (equalIgnoringCase(granularityString, "character"))
granularity = CharacterGranularity;
else if (equalIgnoringCase(granularityString, "word"))
granularity = WordGranularity;
else if (equalIgnoringCase(granularityString, "sentence"))
granularity = SentenceGranularity;
else if (equalIgnoringCase(granularityString, "line"))
granularity = LineGranularity;
else if (equalIgnoringCase(granularityString, "paragraph"))
granularity = ParagraphGranularity;
else if (equalIgnoringCase(granularityString, "lineboundary"))
granularity = LineBoundary;
else if (equalIgnoringCase(granularityString, "sentenceboundary"))
granularity = SentenceBoundary;
else if (equalIgnoringCase(granularityString, "paragraphboundary"))
granularity = ParagraphBoundary;
else if (equalIgnoringCase(granularityString, "documentboundary"))
granularity = DocumentBoundary;
else
return;
frame()->selection().modify(alter, direction, granularity);
}
void DOMSelection::extend(Node* node,
int offset,
ExceptionState& exceptionState) {
DCHECK(node);
if (!isAvailable())
return;
if (offset < 0) {
exceptionState.throwDOMException(
IndexSizeError, String::number(offset) + " is not a valid offset.");
return;
}
if (static_cast<unsigned>(offset) > node->lengthOfContents()) {
exceptionState.throwDOMException(
IndexSizeError,
String::number(offset) + " is larger than the given node's length.");
return;
}
if (!isValidForPosition(node))
return;
const Position& base = frame()->selection().base();
const Position& extent = createPosition(node, offset);
const bool selectionHasDirection = true;
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// In the long term, we should change FrameSelection::setSelection to take a
// parameter that does not require clean layout, so that modifying selection
// no longer performs synchronous layout by itself.
frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets();
const VisibleSelection newSelection = createVisibleSelection(
base, extent, TextAffinity::Downstream, selectionHasDirection);
frame()->selection().setSelection(newSelection);
}
Range* DOMSelection::getRangeAt(int index, ExceptionState& exceptionState) {
if (!isAvailable())
return nullptr;
if (index < 0 || index >= rangeCount()) {
exceptionState.throwDOMException(
IndexSizeError, String::number(index) + " is not a valid index.");
return nullptr;
}
// If you're hitting this, you've added broken multi-range selection support
DCHECK_EQ(rangeCount(), 1);
Position anchor = anchorPosition(visibleSelection());
if (!anchor.anchorNode()->isInShadowTree())
return frame()->selection().firstRange();
Node* node = shadowAdjustedNode(anchor);
if (!node) // crbug.com/595100
return nullptr;
if (!visibleSelection().isBaseFirst())
return Range::create(*anchor.document(), focusNode(), focusOffset(), node,
anchorOffset());
return Range::create(*anchor.document(), node, anchorOffset(), focusNode(),
focusOffset());
}
void DOMSelection::removeAllRanges() {
if (!isAvailable())
return;
frame()->selection().clear();
}
void DOMSelection::addRange(Range* newRange) {
DCHECK(newRange);
if (!isAvailable())
return;
if (newRange->ownerDocument() != frame()->document())
return;
if (!newRange->isConnected()) {
addConsoleError("The given range isn't in document.");
return;
}
FrameSelection& selection = frame()->selection();
if (newRange->ownerDocument() != selection.document()) {
// "editing/selection/selection-in-iframe-removed-crash.html" goes here.
return;
}
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
// In the long term, we should change FrameSelection::setSelection to take a
// parameter that does not require clean layout, so that modifying selection
// no longer performs synchronous layout by itself.
frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets();
if (selection.isNone()) {
selection.setSelectedRange(newRange, VP_DEFAULT_AFFINITY);
return;
}
Range* originalRange = selection.firstRange();
if (originalRange->startContainer()->document() !=
newRange->startContainer()->document()) {
addConsoleError(
"The given range does not belong to the current selection's document.");
return;
}
if (originalRange->startContainer()->treeScope() !=
newRange->startContainer()->treeScope()) {
addConsoleError(
"The given range and the current selection belong to two different "
"document fragments.");
return;
}
if (originalRange->compareBoundaryPoints(Range::kStartToEnd, newRange,
ASSERT_NO_EXCEPTION) < 0 ||
newRange->compareBoundaryPoints(Range::kStartToEnd, originalRange,
ASSERT_NO_EXCEPTION) < 0) {
addConsoleError("Discontiguous selection is not supported.");
return;
}
// FIXME: "Merge the ranges if they intersect" is Blink-specific behavior;
// other browsers supporting discontiguous selection (obviously) keep each
// Range added and return it in getRangeAt(). But it's unclear if we can
// really do the same, since we don't support discontiguous selection. Further
// discussions at
// <https://code.google.com/p/chromium/issues/detail?id=353069>.
Range* start = originalRange->compareBoundaryPoints(
Range::kStartToStart, newRange, ASSERT_NO_EXCEPTION) < 0
? originalRange
: newRange;
Range* end = originalRange->compareBoundaryPoints(Range::kEndToEnd, newRange,
ASSERT_NO_EXCEPTION) < 0
? newRange
: originalRange;
Range* merged = Range::create(originalRange->startContainer()->document(),
start->startContainer(), start->startOffset(),
end->endContainer(), end->endOffset());
TextAffinity affinity = selection.selection().affinity();
selection.setSelectedRange(merged, affinity);
}
void DOMSelection::deleteFromDocument() {
if (!isAvailable())
return;
FrameSelection& selection = frame()->selection();
if (selection.isNone())
return;
Range* selectedRange =
createRange(selection.selection().toNormalizedEphemeralRange());
if (!selectedRange)
return;
selectedRange->deleteContents(ASSERT_NO_EXCEPTION);
setBaseAndExtent(selectedRange->startContainer(),
selectedRange->startOffset(),
selectedRange->startContainer(),
selectedRange->startOffset(), ASSERT_NO_EXCEPTION);
}
bool DOMSelection::containsNode(const Node* n, bool allowPartial) const {
DCHECK(n);
if (!isAvailable())
return false;
FrameSelection& selection = frame()->selection();
if (frame()->document() != n->document() || selection.isNone())
return false;
unsigned nodeIndex = n->nodeIndex();
const EphemeralRange selectedRange =
selection.selection().toNormalizedEphemeralRange();
ContainerNode* parentNode = n->parentNode();
if (!parentNode)
return false;
const Position startPosition =
selectedRange.startPosition().toOffsetInAnchor();
const Position endPosition = selectedRange.endPosition().toOffsetInAnchor();
TrackExceptionState exceptionState;
bool nodeFullySelected =
Range::compareBoundaryPoints(
parentNode, nodeIndex, startPosition.computeContainerNode(),
startPosition.offsetInContainerNode(), exceptionState) >= 0 &&
!exceptionState.hadException() &&
Range::compareBoundaryPoints(
parentNode, nodeIndex + 1, endPosition.computeContainerNode(),
endPosition.offsetInContainerNode(), exceptionState) <= 0 &&
!exceptionState.hadException();
if (exceptionState.hadException())
return false;
if (nodeFullySelected)
return true;
bool nodeFullyUnselected =
(Range::compareBoundaryPoints(
parentNode, nodeIndex, endPosition.computeContainerNode(),
endPosition.offsetInContainerNode(), exceptionState) > 0 &&
!exceptionState.hadException()) ||
(Range::compareBoundaryPoints(
parentNode, nodeIndex + 1, startPosition.computeContainerNode(),
startPosition.offsetInContainerNode(), exceptionState) < 0 &&
!exceptionState.hadException());
DCHECK(!exceptionState.hadException());
if (nodeFullyUnselected)
return false;
return allowPartial || n->isTextNode();
}
void DOMSelection::selectAllChildren(Node* n, ExceptionState& exceptionState) {
DCHECK(n);
// This doesn't (and shouldn't) select text node characters.
setBaseAndExtent(n, 0, n, n->countChildren(), exceptionState);
}
String DOMSelection::toString() {
if (!isAvailable())
return String();
// TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets();
DocumentLifecycle::DisallowTransitionScope disallowTransition(
frame()->document()->lifecycle());
const EphemeralRange range =
frame()->selection().selection().toNormalizedEphemeralRange();
return plainText(range, TextIteratorForSelectionToString);
}
Node* DOMSelection::shadowAdjustedNode(const Position& position) const {
if (position.isNull())
return 0;
Node* containerNode = position.computeContainerNode();
Node* adjustedNode = m_treeScope->ancestorInThisScope(containerNode);
if (!adjustedNode)
return 0;
if (containerNode == adjustedNode)
return containerNode;
DCHECK(!adjustedNode->isShadowRoot()) << adjustedNode;
return adjustedNode->parentOrShadowHostNode();
}
int DOMSelection::shadowAdjustedOffset(const Position& position) const {
if (position.isNull())
return 0;
Node* containerNode = position.computeContainerNode();
Node* adjustedNode = m_treeScope->ancestorInThisScope(containerNode);
if (!adjustedNode)
return 0;
if (containerNode == adjustedNode)
return position.computeOffsetInContainerNode();
return adjustedNode->nodeIndex();
}
bool DOMSelection::isValidForPosition(Node* node) const {
DCHECK(frame());
if (!node)
return true;
return node->document() == frame()->document() && node->isConnected();
}
void DOMSelection::addConsoleError(const String& message) {
if (m_treeScope)
m_treeScope->document().addConsoleMessage(
ConsoleMessage::create(JSMessageSource, ErrorMessageLevel, message));
}
DEFINE_TRACE(DOMSelection) {
visitor->trace(m_treeScope);
DOMWindowProperty::trace(visitor);
}
} // namespace blink