blob: 08d596d4eb7295d2dbc67472bef602495b52a3bd [file] [log] [blame]
/*
* Copyright (C) 2007-2020 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 "Settings.h"
#include "TextIterator.h"
namespace WebCore {
static RefPtr<Node> selectionShadowAncestor(Frame& frame)
{
ASSERT(!frame.settings().liveRangeSelectionEnabled());
auto* node = frame.selection().selection().base().anchorNode();
if (!node || !node->isInShadowTree())
return nullptr;
return node->document().ancestorNodeInThisScope(node);
}
DOMSelection::DOMSelection(DOMWindow& window)
: DOMWindowProperty(&window)
{
}
Ref<DOMSelection> DOMSelection::create(DOMWindow& window)
{
return adoptRef(*new DOMSelection(window));
}
RefPtr<Frame> DOMSelection::frame() const
{
return DOMWindowProperty::frame();
}
std::optional<SimpleRange> DOMSelection::range() const
{
auto frame = this->frame();
if (!frame)
return std::nullopt;
auto range = frame->settings().liveRangeSelectionEnabled()
? frame->selection().selection().range()
: frame->selection().selection().firstRange();
if (!range || range->start.container->isInShadowTree())
return std::nullopt;
return range;
}
Position DOMSelection::anchorPosition() const
{
auto frame = this->frame();
if (!frame)
return { };
if (frame->settings().liveRangeSelectionEnabled())
return frame->selection().selection().anchor();
auto& selection = frame->selection().selection();
return (selection.isBaseFirst() ? selection.start() : selection.end()).parentAnchoredEquivalent();
}
Position DOMSelection::focusPosition() const
{
auto frame = this->frame();
if (!frame)
return { };
if (frame->settings().liveRangeSelectionEnabled())
return frame->selection().selection().focus();
auto& selection = frame->selection().selection();
return (selection.isBaseFirst() ? selection.end() : selection.start()).parentAnchoredEquivalent();
}
Position DOMSelection::basePosition() const
{
// FIXME: Remove this once liveRangeSelectionEnabled is always on, since base and anchor should be the same thing.
auto frame = this->frame();
if (!frame)
return { };
if (frame->settings().liveRangeSelectionEnabled())
return frame->selection().selection().anchor();
return frame->selection().selection().base().parentAnchoredEquivalent();
}
Position DOMSelection::extentPosition() const
{
// FIXME: Remove this once liveRangeSelectionEnabled is always on, since extent and focus should be the same thing.
auto frame = this->frame();
if (!frame)
return { };
if (frame->settings().liveRangeSelectionEnabled())
return frame->selection().selection().focus();
return frame->selection().selection().extent().parentAnchoredEquivalent();
}
RefPtr<Node> DOMSelection::anchorNode() const
{
return shadowAdjustedNode(anchorPosition());
}
unsigned DOMSelection::anchorOffset() const
{
return shadowAdjustedOffset(anchorPosition());
}
RefPtr<Node> DOMSelection::focusNode() const
{
return shadowAdjustedNode(focusPosition());
}
unsigned DOMSelection::focusOffset() const
{
return shadowAdjustedOffset(focusPosition());
}
RefPtr<Node> DOMSelection::baseNode() const
{
return shadowAdjustedNode(basePosition());
}
unsigned DOMSelection::baseOffset() const
{
return shadowAdjustedOffset(basePosition());
}
RefPtr<Node> DOMSelection::extentNode() const
{
return shadowAdjustedNode(extentPosition());
}
unsigned DOMSelection::extentOffset() const
{
return shadowAdjustedOffset(extentPosition());
}
bool DOMSelection::isCollapsed() const
{
auto frame = this->frame();
if (!frame)
return true;
auto range = this->range();
return !range || range->collapsed();
}
String DOMSelection::type() const
{
auto frame = this->frame();
if (!frame)
return "None"_s;
auto& selection = frame->selection();
if (frame->settings().liveRangeSelectionEnabled())
return !selection.isInDocumentTree() ? "None"_s : range()->collapsed() ? "Caret"_s : "Range"_s;
if (selection.isNone())
return "None"_s;
if (selection.isCaret())
return "Caret"_s;
return "Range"_s;
}
unsigned DOMSelection::rangeCount() const
{
auto frame = this->frame();
if (frame && frame->settings().liveRangeSelectionEnabled())
return frame->selection().isInDocumentTree();
return !frame || frame->selection().isNone() ? 0 : 1;
}
ExceptionOr<void> DOMSelection::collapse(Node* node, unsigned offset)
{
auto frame = this->frame();
if (!frame)
return { };
if (frame->settings().liveRangeSelectionEnabled()) {
if (!node) {
removeAllRanges();
return { };
}
if (auto result = Range::checkNodeOffsetPair(*node, offset); result.hasException())
return result.releaseException();
if (!frame->document()->contains(*node))
return { };
} else {
if (!isValidForPosition(node))
return { };
}
auto& selection = frame->selection();
selection.disassociateLiveRange();
selection.moveTo(makeContainerOffsetPosition(node, offset), Affinity::Downstream);
return { };
}
ExceptionOr<void> DOMSelection::collapseToEnd()
{
auto frame = this->frame();
if (!frame)
return { };
auto& selection = frame->selection();
if (selection.isNone())
return Exception { InvalidStateError };
if (frame->settings().liveRangeSelectionEnabled()) {
selection.disassociateLiveRange();
selection.moveTo(selection.selection().uncanonicalizedEnd(), Affinity::Downstream);
} else
selection.moveTo(selection.selection().end(), Affinity::Downstream);
return { };
}
ExceptionOr<void> DOMSelection::collapseToStart()
{
auto frame = this->frame();
if (!frame)
return { };
auto& selection = frame->selection();
if (selection.isNone())
return Exception { InvalidStateError };
if (frame->settings().liveRangeSelectionEnabled()) {
selection.disassociateLiveRange();
selection.moveTo(selection.selection().uncanonicalizedStart(), Affinity::Downstream);
} else
selection.moveTo(selection.selection().start(), Affinity::Downstream);
return { };
}
void DOMSelection::empty()
{
removeAllRanges();
}
ExceptionOr<void> DOMSelection::setBaseAndExtent(Node* baseNode, unsigned baseOffset, Node* extentNode, unsigned extentOffset)
{
auto frame = this->frame();
if (!frame)
return { };
if (frame->settings().liveRangeSelectionEnabled()) {
// FIXME: We should do this by making the arguments non-nullable in the IDL file, once liveRangeSelectionEnabled is always true.
if (!baseNode || !extentNode)
return Exception { TypeError };
if (auto result = Range::checkNodeOffsetPair(*baseNode, baseOffset); result.hasException())
return result.releaseException();
if (auto result = Range::checkNodeOffsetPair(*extentNode, extentOffset); result.hasException())
return result.releaseException();
auto& document = *frame->document();
if (!document.contains(*baseNode) || !document.contains(*extentNode))
return { };
} else {
if (!isValidForPosition(baseNode) || !isValidForPosition(extentNode))
return { };
}
auto& selection = frame->selection();
selection.disassociateLiveRange();
selection.moveTo(makeContainerOffsetPosition(baseNode, baseOffset), makeContainerOffsetPosition(extentNode, extentOffset), Affinity::Downstream);
return { };
}
ExceptionOr<void> DOMSelection::setPosition(Node* node, unsigned offset)
{
return collapse(node, offset);
}
void DOMSelection::modify(const String& alterString, const String& directionString, const String& granularityString)
{
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 = SelectionDirection::Forward;
else if (equalLettersIgnoringASCIICase(directionString, "backward"))
direction = SelectionDirection::Backward;
else if (equalLettersIgnoringASCIICase(directionString, "left"))
direction = SelectionDirection::Left;
else if (equalLettersIgnoringASCIICase(directionString, "right"))
direction = SelectionDirection::Right;
else
return;
TextGranularity granularity;
if (equalLettersIgnoringASCIICase(granularityString, "character"))
granularity = TextGranularity::CharacterGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "word"))
granularity = TextGranularity::WordGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "sentence"))
granularity = TextGranularity::SentenceGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "line"))
granularity = TextGranularity::LineGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "paragraph"))
granularity = TextGranularity::ParagraphGranularity;
else if (equalLettersIgnoringASCIICase(granularityString, "lineboundary"))
granularity = TextGranularity::LineBoundary;
else if (equalLettersIgnoringASCIICase(granularityString, "sentenceboundary"))
granularity = TextGranularity::SentenceBoundary;
else if (equalLettersIgnoringASCIICase(granularityString, "paragraphboundary"))
granularity = TextGranularity::ParagraphBoundary;
else if (equalLettersIgnoringASCIICase(granularityString, "documentboundary"))
granularity = TextGranularity::DocumentBoundary;
else
return;
if (auto frame = this->frame())
frame->selection().modify(alter, direction, granularity);
}
ExceptionOr<void> DOMSelection::extend(Node& node, unsigned offset)
{
auto frame = this->frame();
if (!frame)
return { };
if (frame->settings().liveRangeSelectionEnabled()) {
if (!frame->document()->contains(node))
return { };
if (auto result = Range::checkNodeOffsetPair(node, offset); result.hasException())
return result.releaseException();
auto& selection = frame->selection();
auto newSelection = selection.selection();
newSelection.setExtent(makeContainerOffsetPosition(&node, offset));
selection.disassociateLiveRange();
selection.setSelection(newSelection);
} else {
if (offset > node.length())
return Exception { IndexSizeError };
if (!isValidForPosition(&node))
return { };
frame->selection().setExtent(makeContainerOffsetPosition(&node, offset), Affinity::Downstream);
}
return { };
}
ExceptionOr<Ref<Range>> DOMSelection::getRangeAt(unsigned index)
{
if (index >= rangeCount())
return Exception { IndexSizeError };
auto frame = this->frame().releaseNonNull();
if (frame->settings().liveRangeSelectionEnabled())
return frame->selection().associatedLiveRange().releaseNonNull();
if (auto shadowAncestor = selectionShadowAncestor(frame))
return createLiveRange(makeSimpleRange(*makeBoundaryPointBeforeNode(*shadowAncestor)));
return createLiveRange(*frame->selection().selection().firstRange());
}
void DOMSelection::removeAllRanges()
{
auto frame = this->frame();
if (!frame)
return;
frame->selection().clear();
}
void DOMSelection::addRange(Range& liveRange)
{
auto frame = this->frame();
if (!frame)
return;
auto& selection = frame->selection();
if (frame->settings().liveRangeSelectionEnabled()) {
if (selection.isNone())
selection.associateLiveRange(liveRange);
return;
}
auto range = makeSimpleRange(liveRange);
if (auto selectedRange = selection.selection().toNormalizedRange()) {
if (!selectedRange->start.container->containingShadowRoot() && intersects(*selectedRange, range))
selection.setSelection(unionRange(*selectedRange, range));
return;
}
selection.setSelection(range);
}
ExceptionOr<void> DOMSelection::removeRange(Range& liveRange)
{
auto frame = this->frame();
if (!frame)
return { };
ASSERT(frame->settings().liveRangeSelectionEnabled());
if (&liveRange != frame->selection().associatedLiveRange())
return Exception { NotFoundError };
removeAllRanges();
return { };
}
void DOMSelection::deleteFromDocument()
{
auto frame = this->frame();
if (!frame)
return;
if (frame->settings().liveRangeSelectionEnabled()) {
if (auto range = frame->selection().associatedLiveRange())
range->deleteContents();
return;
}
auto selectedRange = frame->selection().selection().toNormalizedRange();
if (!selectedRange || selectedRange->start.container->containingShadowRoot())
return;
createLiveRange(*selectedRange)->deleteContents();
frame->selection().setSelectedRange(makeSimpleRange(selectedRange->start), Affinity::Downstream, FrameSelection::ShouldCloseTyping::No);
}
bool DOMSelection::containsNode(Node& node, bool allowPartial) const
{
// FIXME: The rule implemented here for text nodes is wrong, and was added to work around anomalies caused when we canonicalize selection endpoints.
if (node.isTextNode() && !node.document().settings().liveRangeSelectionEnabled())
allowPartial = true;
auto range = this->range();
return range && (allowPartial ? intersects(*range, node) : contains(*range, node));
}
ExceptionOr<void> DOMSelection::selectAllChildren(Node& node)
{
// This doesn't (and shouldn't) select the characters in a node if passed a text node.
// Selection API specification seems to have this wrong: https://github.com/w3c/selection-api/issues/125
return setBaseAndExtent(&node, 0, &node, node.countChildNodes());
}
String DOMSelection::toString() const
{
auto frame = this->frame();
if (!frame)
return String();
if (frame->settings().liveRangeSelectionEnabled()) {
auto range = this->range();
return range ? plainText(*range) : emptyString();
}
auto range = frame->selection().selection().firstRange();
return range ? plainText(*range) : emptyString();
}
RefPtr<Node> DOMSelection::shadowAdjustedNode(const Position& position) const
{
if (position.isNull())
return nullptr;
if (frame()->settings().liveRangeSelectionEnabled()) {
auto node = position.containerNode();
return !node || node->isInShadowTree() ? nullptr : node;
}
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;
if (frame()->settings().liveRangeSelectionEnabled())
return shadowAdjustedNode(position) ? position.computeOffsetInContainerNode() : 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();
ASSERT(!frame->settings().liveRangeSelectionEnabled());
return frame && (!node || &node->document() == frame->document());
}
} // namespace WebCore