Home | History | Annotate | Download | only in page
      1 /*
      2  * Copyright (C) 2007, 2009 Apple Inc. All rights reserved.
      3  * Copyright (C) 2012 Google Inc. All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions
      7  * are met:
      8  *
      9  * 1.  Redistributions of source code must retain the above copyright
     10  *     notice, this list of conditions and the following disclaimer.
     11  * 2.  Redistributions in binary form must reproduce the above copyright
     12  *     notice, this list of conditions and the following disclaimer in the
     13  *     documentation and/or other materials provided with the distribution.
     14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     15  *     its contributors may be used to endorse or promote products derived
     16  *     from this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28  */
     29 
     30 
     31 #include "config.h"
     32 #include "core/page/DOMSelection.h"
     33 
     34 #include "bindings/v8/ExceptionMessages.h"
     35 #include "bindings/v8/ExceptionState.h"
     36 #include "bindings/v8/ExceptionStatePlaceholder.h"
     37 #include "core/dom/Document.h"
     38 #include "core/dom/ExceptionCode.h"
     39 #include "core/dom/Node.h"
     40 #include "core/dom/Range.h"
     41 #include "core/dom/TreeScope.h"
     42 #include "core/editing/FrameSelection.h"
     43 #include "core/editing/TextIterator.h"
     44 #include "core/editing/htmlediting.h"
     45 #include "core/frame/LocalFrame.h"
     46 #include "wtf/text/WTFString.h"
     47 
     48 namespace WebCore {
     49 
     50 static Node* selectionShadowAncestor(LocalFrame* frame)
     51 {
     52     Node* node = frame->selection().selection().base().anchorNode();
     53     if (!node)
     54         return 0;
     55 
     56     if (!node->isInShadowTree())
     57         return 0;
     58 
     59     return frame->document()->ancestorInThisScope(node);
     60 }
     61 
     62 DOMSelection::DOMSelection(const TreeScope* treeScope)
     63     : DOMWindowProperty(treeScope->rootNode().document().frame())
     64     , m_treeScope(treeScope)
     65 {
     66     ScriptWrappable::init(this);
     67 }
     68 
     69 void DOMSelection::clearTreeScope()
     70 {
     71     m_treeScope = nullptr;
     72 }
     73 
     74 const VisibleSelection& DOMSelection::visibleSelection() const
     75 {
     76     ASSERT(m_frame);
     77     return m_frame->selection().selection();
     78 }
     79 
     80 static Position anchorPosition(const VisibleSelection& selection)
     81 {
     82     Position anchor = selection.isBaseFirst() ? selection.start() : selection.end();
     83     return anchor.parentAnchoredEquivalent();
     84 }
     85 
     86 static Position focusPosition(const VisibleSelection& selection)
     87 {
     88     Position focus = selection.isBaseFirst() ? selection.end() : selection.start();
     89     return focus.parentAnchoredEquivalent();
     90 }
     91 
     92 static Position basePosition(const VisibleSelection& selection)
     93 {
     94     return selection.base().parentAnchoredEquivalent();
     95 }
     96 
     97 static Position extentPosition(const VisibleSelection& selection)
     98 {
     99     return selection.extent().parentAnchoredEquivalent();
    100 }
    101 
    102 Node* DOMSelection::anchorNode() const
    103 {
    104     if (!m_frame)
    105         return 0;
    106 
    107     return shadowAdjustedNode(anchorPosition(visibleSelection()));
    108 }
    109 
    110 int DOMSelection::anchorOffset() const
    111 {
    112     if (!m_frame)
    113         return 0;
    114 
    115     return shadowAdjustedOffset(anchorPosition(visibleSelection()));
    116 }
    117 
    118 Node* DOMSelection::focusNode() const
    119 {
    120     if (!m_frame)
    121         return 0;
    122 
    123     return shadowAdjustedNode(focusPosition(visibleSelection()));
    124 }
    125 
    126 int DOMSelection::focusOffset() const
    127 {
    128     if (!m_frame)
    129         return 0;
    130 
    131     return shadowAdjustedOffset(focusPosition(visibleSelection()));
    132 }
    133 
    134 Node* DOMSelection::baseNode() const
    135 {
    136     if (!m_frame)
    137         return 0;
    138 
    139     return shadowAdjustedNode(basePosition(visibleSelection()));
    140 }
    141 
    142 int DOMSelection::baseOffset() const
    143 {
    144     if (!m_frame)
    145         return 0;
    146 
    147     return shadowAdjustedOffset(basePosition(visibleSelection()));
    148 }
    149 
    150 Node* DOMSelection::extentNode() const
    151 {
    152     if (!m_frame)
    153         return 0;
    154 
    155     return shadowAdjustedNode(extentPosition(visibleSelection()));
    156 }
    157 
    158 int DOMSelection::extentOffset() const
    159 {
    160     if (!m_frame)
    161         return 0;
    162 
    163     return shadowAdjustedOffset(extentPosition(visibleSelection()));
    164 }
    165 
    166 bool DOMSelection::isCollapsed() const
    167 {
    168     if (!m_frame || selectionShadowAncestor(m_frame))
    169         return true;
    170     return !m_frame->selection().isRange();
    171 }
    172 
    173 String DOMSelection::type() const
    174 {
    175     if (!m_frame)
    176         return String();
    177 
    178     FrameSelection& selection = m_frame->selection();
    179 
    180     // This is a WebKit DOM extension, incompatible with an IE extension
    181     // IE has this same attribute, but returns "none", "text" and "control"
    182     // http://msdn.microsoft.com/en-us/library/ms534692(VS.85).aspx
    183     if (selection.isNone())
    184         return "None";
    185     if (selection.isCaret())
    186         return "Caret";
    187     return "Range";
    188 }
    189 
    190 int DOMSelection::rangeCount() const
    191 {
    192     if (!m_frame)
    193         return 0;
    194     return m_frame->selection().isNone() ? 0 : 1;
    195 }
    196 
    197 void DOMSelection::collapse(Node* node, int offset, ExceptionState& exceptionState)
    198 {
    199     ASSERT(node);
    200     if (!m_frame)
    201         return;
    202 
    203     if (offset < 0) {
    204         exceptionState.throwDOMException(IndexSizeError, String::number(offset) + " is not a valid offset.");
    205         return;
    206     }
    207 
    208     if (!isValidForPosition(node))
    209         return;
    210     RefPtrWillBeRawPtr<Range> range = Range::create(node->document());
    211     range->setStart(node, offset, exceptionState);
    212     if (exceptionState.hadException())
    213         return;
    214     range->setEnd(node, offset, exceptionState);
    215     if (exceptionState.hadException())
    216         return;
    217     m_frame->selection().setSelectedRange(range.get(), DOWNSTREAM, m_frame->selection().isDirectional() ? FrameSelection::Directional : FrameSelection::NonDirectional);
    218 }
    219 
    220 void DOMSelection::collapse(Node* node, ExceptionState& exceptionState)
    221 {
    222     collapse(node, 0, exceptionState);
    223 }
    224 
    225 void DOMSelection::collapseToEnd(ExceptionState& exceptionState)
    226 {
    227     if (!m_frame)
    228         return;
    229 
    230     const VisibleSelection& selection = m_frame->selection().selection();
    231 
    232     if (selection.isNone()) {
    233         exceptionState.throwDOMException(InvalidStateError, "there is no selection.");
    234         return;
    235     }
    236 
    237     m_frame->selection().moveTo(VisiblePosition(selection.end(), DOWNSTREAM));
    238 }
    239 
    240 void DOMSelection::collapseToStart(ExceptionState& exceptionState)
    241 {
    242     if (!m_frame)
    243         return;
    244 
    245     const VisibleSelection& selection = m_frame->selection().selection();
    246 
    247     if (selection.isNone()) {
    248         exceptionState.throwDOMException(InvalidStateError, "there is no selection.");
    249         return;
    250     }
    251 
    252     m_frame->selection().moveTo(VisiblePosition(selection.start(), DOWNSTREAM));
    253 }
    254 
    255 void DOMSelection::empty()
    256 {
    257     if (!m_frame)
    258         return;
    259     m_frame->selection().clear();
    260 }
    261 
    262 void DOMSelection::setBaseAndExtent(Node* baseNode, int baseOffset, Node* extentNode, int extentOffset, ExceptionState& exceptionState)
    263 {
    264     if (!m_frame)
    265         return;
    266 
    267     if (baseOffset < 0) {
    268         exceptionState.throwDOMException(IndexSizeError, String::number(baseOffset) + " is not a valid base offset.");
    269         return;
    270     }
    271 
    272     if (extentOffset < 0) {
    273         exceptionState.throwDOMException(IndexSizeError, String::number(extentOffset) + " is not a valid extent offset.");
    274         return;
    275     }
    276 
    277     if (!isValidForPosition(baseNode) || !isValidForPosition(extentNode))
    278         return;
    279 
    280     // FIXME: Eliminate legacy editing positions
    281     VisiblePosition visibleBase = VisiblePosition(createLegacyEditingPosition(baseNode, baseOffset), DOWNSTREAM);
    282     VisiblePosition visibleExtent = VisiblePosition(createLegacyEditingPosition(extentNode, extentOffset), DOWNSTREAM);
    283 
    284     m_frame->selection().moveTo(visibleBase, visibleExtent);
    285 }
    286 
    287 void DOMSelection::modify(const String& alterString, const String& directionString, const String& granularityString)
    288 {
    289     if (!m_frame)
    290         return;
    291 
    292     FrameSelection::EAlteration alter;
    293     if (equalIgnoringCase(alterString, "extend"))
    294         alter = FrameSelection::AlterationExtend;
    295     else if (equalIgnoringCase(alterString, "move"))
    296         alter = FrameSelection::AlterationMove;
    297     else
    298         return;
    299 
    300     SelectionDirection direction;
    301     if (equalIgnoringCase(directionString, "forward"))
    302         direction = DirectionForward;
    303     else if (equalIgnoringCase(directionString, "backward"))
    304         direction = DirectionBackward;
    305     else if (equalIgnoringCase(directionString, "left"))
    306         direction = DirectionLeft;
    307     else if (equalIgnoringCase(directionString, "right"))
    308         direction = DirectionRight;
    309     else
    310         return;
    311 
    312     TextGranularity granularity;
    313     if (equalIgnoringCase(granularityString, "character"))
    314         granularity = CharacterGranularity;
    315     else if (equalIgnoringCase(granularityString, "word"))
    316         granularity = WordGranularity;
    317     else if (equalIgnoringCase(granularityString, "sentence"))
    318         granularity = SentenceGranularity;
    319     else if (equalIgnoringCase(granularityString, "line"))
    320         granularity = LineGranularity;
    321     else if (equalIgnoringCase(granularityString, "paragraph"))
    322         granularity = ParagraphGranularity;
    323     else if (equalIgnoringCase(granularityString, "lineboundary"))
    324         granularity = LineBoundary;
    325     else if (equalIgnoringCase(granularityString, "sentenceboundary"))
    326         granularity = SentenceBoundary;
    327     else if (equalIgnoringCase(granularityString, "paragraphboundary"))
    328         granularity = ParagraphBoundary;
    329     else if (equalIgnoringCase(granularityString, "documentboundary"))
    330         granularity = DocumentBoundary;
    331     else
    332         return;
    333 
    334     m_frame->selection().modify(alter, direction, granularity);
    335 }
    336 
    337 void DOMSelection::extend(Node* node, int offset, ExceptionState& exceptionState)
    338 {
    339     if (!m_frame)
    340         return;
    341 
    342     if (!node) {
    343         exceptionState.throwDOMException(TypeMismatchError, ExceptionMessages::argumentNullOrIncorrectType(1, "Node"));
    344         return;
    345     }
    346 
    347     if (offset < 0) {
    348         exceptionState.throwDOMException(IndexSizeError, String::number(offset) + " is not a valid offset.");
    349         return;
    350     }
    351     if (offset > (node->offsetInCharacters() ? caretMaxOffset(node) : (int)node->countChildren())) {
    352         exceptionState.throwDOMException(IndexSizeError, String::number(offset) + " is larger than the given node's length.");
    353         return;
    354     }
    355 
    356     if (!isValidForPosition(node))
    357         return;
    358 
    359     // FIXME: Eliminate legacy editing positions
    360     m_frame->selection().setExtent(VisiblePosition(createLegacyEditingPosition(node, offset), DOWNSTREAM));
    361 }
    362 
    363 PassRefPtrWillBeRawPtr<Range> DOMSelection::getRangeAt(int index, ExceptionState& exceptionState)
    364 {
    365     if (!m_frame)
    366         return nullptr;
    367 
    368     if (index < 0 || index >= rangeCount()) {
    369         exceptionState.throwDOMException(IndexSizeError, String::number(index) + " is not a valid index.");
    370         return nullptr;
    371     }
    372 
    373     // If you're hitting this, you've added broken multi-range selection support
    374     ASSERT(rangeCount() == 1);
    375 
    376     if (Node* shadowAncestor = selectionShadowAncestor(m_frame)) {
    377         ASSERT(!shadowAncestor->isShadowRoot());
    378         ContainerNode* container = shadowAncestor->parentOrShadowHostNode();
    379         int offset = shadowAncestor->nodeIndex();
    380         return Range::create(shadowAncestor->document(), container, offset, container, offset);
    381     }
    382 
    383     return m_frame->selection().firstRange();
    384 }
    385 
    386 void DOMSelection::removeAllRanges()
    387 {
    388     if (!m_frame)
    389         return;
    390     m_frame->selection().clear();
    391 }
    392 
    393 void DOMSelection::addRange(Range* newRange)
    394 {
    395     if (!m_frame)
    396         return;
    397 
    398     // FIXME: Should we throw DOMException for error cases below?
    399     if (!newRange) {
    400         addConsoleError("The given range is null.");
    401         return;
    402     }
    403 
    404     if (!newRange->startContainer()) {
    405         addConsoleError("The given range has no container. Perhaps 'detach()' has been invoked on it?");
    406         return;
    407     }
    408 
    409     FrameSelection& selection = m_frame->selection();
    410 
    411     if (selection.isNone()) {
    412         selection.setSelectedRange(newRange, VP_DEFAULT_AFFINITY);
    413         return;
    414     }
    415 
    416     RefPtrWillBeRawPtr<Range> originalRange = selection.firstRange();
    417 
    418     if (originalRange->startContainer()->document() != newRange->startContainer()->document()) {
    419         addConsoleError("The given range does not belong to the current selection's document.");
    420         return;
    421     }
    422     if (originalRange->startContainer()->treeScope() != newRange->startContainer()->treeScope()) {
    423         addConsoleError("The given range and the current selection belong to two different document fragments.");
    424         return;
    425     }
    426 
    427     if (originalRange->compareBoundaryPoints(Range::START_TO_END, newRange, ASSERT_NO_EXCEPTION) < 0
    428         || newRange->compareBoundaryPoints(Range::START_TO_END, originalRange.get(), ASSERT_NO_EXCEPTION) < 0) {
    429         addConsoleError("Discontiguous selection is not supported.");
    430         return;
    431     }
    432 
    433     // FIXME: "Merge the ranges if they intersect" is Blink-specific behavior; other browsers supporting discontiguous
    434     // selection (obviously) keep each Range added and return it in getRangeAt(). But it's unclear if we can really
    435     // do the same, since we don't support discontiguous selection. Further discussions at
    436     // <https://code.google.com/p/chromium/issues/detail?id=353069>.
    437 
    438     Range* start = originalRange->compareBoundaryPoints(Range::START_TO_START, newRange, ASSERT_NO_EXCEPTION) < 0 ? originalRange.get() : newRange;
    439     Range* end = originalRange->compareBoundaryPoints(Range::END_TO_END, newRange, ASSERT_NO_EXCEPTION) < 0 ? newRange : originalRange.get();
    440     RefPtrWillBeRawPtr<Range> merged = Range::create(originalRange->startContainer()->document(), start->startContainer(), start->startOffset(), end->endContainer(), end->endOffset());
    441     EAffinity affinity = selection.selection().affinity();
    442     selection.setSelectedRange(merged.get(), affinity);
    443 }
    444 
    445 void DOMSelection::deleteFromDocument()
    446 {
    447     if (!m_frame)
    448         return;
    449 
    450     FrameSelection& selection = m_frame->selection();
    451 
    452     if (selection.isNone())
    453         return;
    454 
    455     RefPtrWillBeRawPtr<Range> selectedRange = selection.selection().toNormalizedRange();
    456     if (!selectedRange)
    457         return;
    458 
    459     selectedRange->deleteContents(ASSERT_NO_EXCEPTION);
    460 
    461     setBaseAndExtent(selectedRange->startContainer(), selectedRange->startOffset(), selectedRange->startContainer(), selectedRange->startOffset(), ASSERT_NO_EXCEPTION);
    462 }
    463 
    464 bool DOMSelection::containsNode(const Node* n, bool allowPartial) const
    465 {
    466     if (!m_frame)
    467         return false;
    468 
    469     FrameSelection& selection = m_frame->selection();
    470 
    471     if (!n || m_frame->document() != n->document() || selection.isNone())
    472         return false;
    473 
    474     unsigned nodeIndex = n->nodeIndex();
    475     RefPtrWillBeRawPtr<Range> selectedRange = selection.selection().toNormalizedRange();
    476 
    477     ContainerNode* parentNode = n->parentNode();
    478     if (!parentNode)
    479         return false;
    480 
    481     TrackExceptionState exceptionState;
    482     bool nodeFullySelected = Range::compareBoundaryPoints(parentNode, nodeIndex, selectedRange->startContainer(), selectedRange->startOffset(), exceptionState) >= 0 && !exceptionState.hadException()
    483         && Range::compareBoundaryPoints(parentNode, nodeIndex + 1, selectedRange->endContainer(), selectedRange->endOffset(), exceptionState) <= 0 && !exceptionState.hadException();
    484     if (exceptionState.hadException())
    485         return false;
    486     if (nodeFullySelected)
    487         return true;
    488 
    489     bool nodeFullyUnselected = (Range::compareBoundaryPoints(parentNode, nodeIndex, selectedRange->endContainer(), selectedRange->endOffset(), exceptionState) > 0 && !exceptionState.hadException())
    490         || (Range::compareBoundaryPoints(parentNode, nodeIndex + 1, selectedRange->startContainer(), selectedRange->startOffset(), exceptionState) < 0 && !exceptionState.hadException());
    491     ASSERT(!exceptionState.hadException());
    492     if (nodeFullyUnselected)
    493         return false;
    494 
    495     return allowPartial || n->isTextNode();
    496 }
    497 
    498 void DOMSelection::selectAllChildren(Node* n, ExceptionState& exceptionState)
    499 {
    500     if (!n)
    501         return;
    502 
    503     // This doesn't (and shouldn't) select text node characters.
    504     setBaseAndExtent(n, 0, n, n->countChildren(), exceptionState);
    505 }
    506 
    507 String DOMSelection::toString()
    508 {
    509     if (!m_frame)
    510         return String();
    511 
    512     return plainText(m_frame->selection().selection().toNormalizedRange().get());
    513 }
    514 
    515 Node* DOMSelection::shadowAdjustedNode(const Position& position) const
    516 {
    517     if (position.isNull())
    518         return 0;
    519 
    520     Node* containerNode = position.containerNode();
    521     Node* adjustedNode = m_treeScope->ancestorInThisScope(containerNode);
    522 
    523     if (!adjustedNode)
    524         return 0;
    525 
    526     if (containerNode == adjustedNode)
    527         return containerNode;
    528 
    529     ASSERT(!adjustedNode->isShadowRoot());
    530     return adjustedNode->parentOrShadowHostNode();
    531 }
    532 
    533 int DOMSelection::shadowAdjustedOffset(const Position& position) const
    534 {
    535     if (position.isNull())
    536         return 0;
    537 
    538     Node* containerNode = position.containerNode();
    539     Node* adjustedNode = m_treeScope->ancestorInThisScope(containerNode);
    540 
    541     if (!adjustedNode)
    542         return 0;
    543 
    544     if (containerNode == adjustedNode)
    545         return position.computeOffsetInContainerNode();
    546 
    547     return adjustedNode->nodeIndex();
    548 }
    549 
    550 bool DOMSelection::isValidForPosition(Node* node) const
    551 {
    552     ASSERT(m_frame);
    553     if (!node)
    554         return true;
    555     return node->document() == m_frame->document();
    556 }
    557 
    558 void DOMSelection::addConsoleError(const String& message)
    559 {
    560     if (m_treeScope)
    561         m_treeScope->document().addConsoleMessage(JSMessageSource, ErrorMessageLevel, message);
    562 }
    563 
    564 } // namespace WebCore
    565