DOMSelection.cpp   [plain text]


/*
 * 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();
}

Optional<SimpleRange> DOMSelection::range() const
{
    auto frame = this->frame();
    if (!frame)
        return WTF::nullopt;
    auto range = frame->settings().liveRangeSelectionEnabled()
        ? frame->selection().selection().range()
        : frame->selection().selection().firstRange();
    if (!range || range->start.container->isInShadowTree())
        return WTF::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)));
    auto simpleRange = frame->selection().selection().firstRange();
    ASSERT(simpleRange); // selection is orphaned but this shouldn't happen.
    if (!simpleRange)
        return Exception { IndexSizeError };
    return createLiveRange(*simpleRange);
}

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