blob: 475e64badf5004952a2f63fd6671d5132a3441db [file] [log] [blame]
/*
* Copyright (C) 2007, 2009, 2016 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 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 "config.h"
#include "DOMSelection.h"
#include "Document.h"
#include "Editing.h"
#include "Frame.h"
#include "FrameSelection.h"
#include "Range.h"
#include "TextIterator.h"
namespace WebCore {
static Node* selectionShadowAncestor(Frame& frame)
{
auto* node = frame.selection().selection().base().anchorNode();
if (!node)
return nullptr;
if (!node->isInShadowTree())
return nullptr;
// FIXME: Unclear on why this needs to be the possibly null frame.document() instead of the never null node->document().
return frame.document()->ancestorNodeInThisScope(node);
}
DOMSelection::DOMSelection(DOMWindow& window)
: DOMWindowProperty(&window)
{
}
const VisibleSelection& DOMSelection::visibleSelection() const
{
ASSERT(frame());
return frame()->selection().selection();
}
static Position anchorPosition(const VisibleSelection& selection)
{
auto anchor = selection.isBaseFirst() ? selection.start() : selection.end();
return anchor.parentAnchoredEquivalent();
}
static Position focusPosition(const VisibleSelection& selection)
{
auto 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 (!frame())
return nullptr;
return shadowAdjustedNode(anchorPosition(visibleSelection()));
}
unsigned DOMSelection::anchorOffset() const
{
if (!frame())
return 0;
return shadowAdjustedOffset(anchorPosition(visibleSelection()));
}
Node* DOMSelection::focusNode() const
{
if (!frame())
return nullptr;
return shadowAdjustedNode(focusPosition(visibleSelection()));
}
unsigned DOMSelection::focusOffset() const
{
if (!frame())
return 0;
return shadowAdjustedOffset(focusPosition(visibleSelection()));
}
Node* DOMSelection::baseNode() const
{
if (!frame())
return nullptr;
return shadowAdjustedNode(basePosition(visibleSelection()));
}
unsigned DOMSelection::baseOffset() const
{
if (!frame())
return 0;
return shadowAdjustedOffset(basePosition(visibleSelection()));
}
Node* DOMSelection::extentNode() const
{
if (!frame())
return nullptr;
return shadowAdjustedNode(extentPosition(visibleSelection()));
}
unsigned DOMSelection::extentOffset() const
{
if (!frame())
return 0;
return shadowAdjustedOffset(extentPosition(visibleSelection()));
}
bool DOMSelection::isCollapsed() const
{
auto* frame = this->frame();
if (!frame || selectionShadowAncestor(*frame))
return true;
return !frame->selection().isRange();
}
String DOMSelection::type() const
{
auto* frame = this->frame();
if (!frame)
return "None"_s;
auto& selection = frame->selection();
if (selection.isNone())
return "None"_s;
if (selection.isCaret())
return "Caret"_s;
return "Range"_s;
}
unsigned DOMSelection::rangeCount() const
{
auto* frame = this->frame();
return !frame || frame->selection().isNone() ? 0 : 1;
}
void DOMSelection::collapse(Node* node, unsigned offset)
{
if (!isValidForPosition(node))
return;
Ref<Frame> protectedFrame(*frame());
protectedFrame->selection().moveTo(createLegacyEditingPosition(node, offset), DOWNSTREAM);
}
ExceptionOr<void> DOMSelection::collapseToEnd()
{
auto* frame = this->frame();
if (!frame)
return { };
auto& selection = frame->selection();
if (selection.isNone())
return Exception { InvalidStateError };
Ref<Frame> protector(*frame);
selection.moveTo(selection.selection().end(), DOWNSTREAM);
return { };
}
ExceptionOr<void> DOMSelection::collapseToStart()
{
auto* frame = this->frame();
if (!frame)
return { };
auto& selection = frame->selection();
if (selection.isNone())
return Exception { InvalidStateError };
Ref<Frame> protector(*frame);
selection.moveTo(selection.selection().start(), DOWNSTREAM);
return { };
}
void DOMSelection::empty()
{
auto* frame = this->frame();
if (!frame)
return;
frame->selection().clear();
}
void DOMSelection::setBaseAndExtent(Node* baseNode, unsigned baseOffset, Node* extentNode, unsigned extentOffset)
{
if (!isValidForPosition(baseNode) || !isValidForPosition(extentNode))
return;
Ref<Frame> protectedFrame(*frame());
protectedFrame->selection().moveTo(createLegacyEditingPosition(baseNode, baseOffset), createLegacyEditingPosition(extentNode, extentOffset), DOWNSTREAM);
}
void DOMSelection::setPosition(Node* node, unsigned offset)
{
if (!isValidForPosition(node))
return;
Ref<Frame> protectedFrame(*frame());
protectedFrame->selection().moveTo(createLegacyEditingPosition(node, offset), DOWNSTREAM);
}
void DOMSelection::modify(const String& alterString, const String& directionString, const String& granularityString)
{
auto* frame = this->frame();
if (!frame)
return;
FrameSelection::EAlteration alter;
if (equalLettersIgnoringASCIICase(alterString, "extend"))
alter = FrameSelection::AlterationExtend;
else if (equalLettersIgnoringASCIICase(alterString, "move"))
alter = FrameSelection::AlterationMove;
else
return;
SelectionDirection direction;
if (equalLettersIgnoringASCIICase(directionString, "forward"))
direction = DirectionForward;
else if (equalLettersIgnoringASCIICase(directionString, "backward"))
direction = DirectionBackward;
else if (equalLettersIgnoringASCIICase(directionString, "left"))
direction = DirectionLeft;
else if (equalLettersIgnoringASCIICase(directionString, "right"))
direction = DirectionRight;
else
return;
TextGranularity granularity;
if (equalLettersIgnoringASCIICase(granularityString, "character"))
granularity = CharacterGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "word"))
granularity = WordGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "sentence"))
granularity = SentenceGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "line"))
granularity = LineGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "paragraph"))
granularity = ParagraphGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "lineboundary"))
granularity = LineBoundary;
else if (equalLettersIgnoringASCIICase(granularityString, "sentenceboundary"))
granularity = SentenceBoundary;
else if (equalLettersIgnoringASCIICase(granularityString, "paragraphboundary"))
granularity = ParagraphBoundary;
else if (equalLettersIgnoringASCIICase(granularityString, "documentboundary"))
granularity = DocumentBoundary;
else
return;
Ref<Frame> protector(*frame);
frame->selection().modify(alter, direction, granularity);
}
ExceptionOr<void> DOMSelection::extend(Node& node, unsigned offset)
{
auto* frame = this->frame();
if (!frame)
return { };
if (offset > (node.isCharacterDataNode() ? caretMaxOffset(node) : node.countChildNodes()))
return Exception { IndexSizeError };
if (!isValidForPosition(&node))
return { };
Ref<Frame> protector(*frame);
frame->selection().setExtent(createLegacyEditingPosition(&node, offset), DOWNSTREAM);
return { };
}
ExceptionOr<Ref<Range>> DOMSelection::getRangeAt(unsigned index)
{
if (index >= rangeCount())
return Exception { IndexSizeError };
// If you're hitting this, you've added broken multi-range selection support.
ASSERT(rangeCount() == 1);
auto* frame = this->frame();
if (auto* shadowAncestor = selectionShadowAncestor(*frame)) {
auto* container = shadowAncestor->parentNodeGuaranteedHostFree();
unsigned offset = shadowAncestor->computeNodeIndex();
return Range::create(shadowAncestor->document(), container, offset, container, offset);
}
auto firstRange = frame->selection().selection().firstRange();
ASSERT(firstRange);
if (!firstRange)
return Exception { IndexSizeError };
return firstRange.releaseNonNull();
}
void DOMSelection::removeAllRanges()
{
auto* frame = this->frame();
if (!frame)
return;
frame->selection().clear();
}
void DOMSelection::addRange(Range& range)
{
auto* frame = this->frame();
if (!frame)
return;
Ref<Frame> protector(*frame);
auto& selection = frame->selection();
if (selection.isNone()) {
selection.moveTo(&range);
return;
}
auto normalizedRange = selection.selection().toNormalizedRange();
if (!normalizedRange)
return;
auto result = range.compareBoundaryPoints(Range::START_TO_START, *normalizedRange);
if (!result.hasException() && result.releaseReturnValue() == -1) {
// We don't support discontiguous selection. We don't do anything if the two ranges don't intersect.
result = range.compareBoundaryPoints(Range::START_TO_END, *normalizedRange);
if (!result.hasException() && result.releaseReturnValue() > -1) {
result = range.compareBoundaryPoints(Range::END_TO_END, *normalizedRange);
if (!result.hasException() && result.releaseReturnValue() == -1) {
// The ranges intersect.
selection.moveTo(range.startPosition(), normalizedRange->endPosition(), DOWNSTREAM);
} else {
// The new range contains the original range.
selection.moveTo(&range);
}
}
} else {
// We don't support discontiguous selection. We don't do anything if the two ranges don't intersect.
result = range.compareBoundaryPoints(Range::END_TO_START, *normalizedRange);
if (!result.hasException() && result.releaseReturnValue() < 1) {
result = range.compareBoundaryPoints(Range::END_TO_END, *normalizedRange);
if (!result.hasException() && result.releaseReturnValue() == -1) {
// The original range contains the new range.
selection.moveTo(normalizedRange.get());
} else {
// The ranges intersect.
selection.moveTo(normalizedRange->startPosition(), range.endPosition(), DOWNSTREAM);
}
}
}
}
void DOMSelection::deleteFromDocument()
{
auto* frame = this->frame();
if (!frame)
return;
auto& selection = frame->selection();
if (selection.isNone())
return;
auto selectedRange = selection.selection().toNormalizedRange();
if (!selectedRange || selectedRange->shadowRoot())
return;
Ref<Frame> protector(*frame);
selectedRange->deleteContents();
setBaseAndExtent(&selectedRange->startContainer(), selectedRange->startOffset(), &selectedRange->startContainer(), selectedRange->startOffset());
}
bool DOMSelection::containsNode(Node& node, bool allowPartial) const
{
auto* frame = this->frame();
if (!frame)
return false;
auto& selection = frame->selection();
if (frame->document() != &node.document() || selection.isNone())
return false;
Ref<Node> protectedNode(node);
auto selectedRange = selection.selection().toNormalizedRange();
if (!selectedRange)
return false;
ContainerNode* parentNode = node.parentNode();
if (!parentNode || !parentNode->isConnected())
return false;
unsigned nodeIndex = node.computeNodeIndex();
auto startsResult = Range::compareBoundaryPoints(parentNode, nodeIndex, &selectedRange->startContainer(), selectedRange->startOffset());
if (startsResult.hasException())
return false;
auto endsResult = Range::compareBoundaryPoints(parentNode, nodeIndex + 1, &selectedRange->endContainer(), selectedRange->endOffset());
ASSERT(!endsResult.hasException());
bool isNodeFullySelected = !startsResult.hasException() && startsResult.releaseReturnValue() >= 0
&& !endsResult.hasException() && endsResult.releaseReturnValue() <= 0;
if (isNodeFullySelected)
return true;
auto startEndResult = Range::compareBoundaryPoints(parentNode, nodeIndex, &selectedRange->endContainer(), selectedRange->endOffset());
ASSERT(!startEndResult.hasException());
auto endStartResult = Range::compareBoundaryPoints(parentNode, nodeIndex + 1, &selectedRange->startContainer(), selectedRange->startOffset());
ASSERT(!endStartResult.hasException());
bool isNodeFullyUnselected = (!startEndResult.hasException() && startEndResult.releaseReturnValue() > 0)
|| (!endStartResult.hasException() && endStartResult.releaseReturnValue() < 0);
if (isNodeFullyUnselected)
return false;
return allowPartial || node.isTextNode();
}
void DOMSelection::selectAllChildren(Node& node)
{
// This doesn't (and shouldn't) select text node characters.
setBaseAndExtent(&node, 0, &node, node.countChildNodes());
}
String DOMSelection::toString()
{
auto* frame = this->frame();
if (!frame)
return String();
return plainText(frame->selection().selection().toNormalizedRange().get());
}
Node* DOMSelection::shadowAdjustedNode(const Position& position) const
{
if (position.isNull())
return nullptr;
auto* containerNode = position.containerNode();
auto* adjustedNode = frame()->document()->ancestorNodeInThisScope(containerNode);
if (!adjustedNode)
return nullptr;
if (containerNode == adjustedNode)
return containerNode;
return adjustedNode->parentNodeGuaranteedHostFree();
}
unsigned DOMSelection::shadowAdjustedOffset(const Position& position) const
{
if (position.isNull())
return 0;
auto* containerNode = position.containerNode();
auto* adjustedNode = frame()->document()->ancestorNodeInThisScope(containerNode);
if (!adjustedNode)
return 0;
if (containerNode == adjustedNode)
return position.computeOffsetInContainerNode();
return adjustedNode->computeNodeIndex();
}
bool DOMSelection::isValidForPosition(Node* node) const
{
auto* frame = this->frame();
if (!frame)
return false;
if (!node)
return true;
return &node->document() == frame->document();
}
} // namespace WebCore