| /* |
| * 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 "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/Deprecation.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/frame/UseCounter.h" |
| #include "core/inspector/ConsoleMessage.h" |
| #include "wtf/text/WTFString.h" |
| |
| namespace blink { |
| |
| static Node* selectionShadowAncestor(LocalFrame* frame) { |
| Node* node = frame->selection() |
| .computeVisibleSelectionInDOMTreeDeprecated() |
| .base() |
| .anchorNode(); |
| if (!node) |
| return 0; |
| |
| if (!node->isInShadowTree()) |
| return 0; |
| |
| return frame->document()->ancestorInThisScope(node); |
| } |
| |
| DOMSelection::DOMSelection(const TreeScope* treeScope) |
| : ContextClient(treeScope->rootNode().document().frame()), |
| m_treeScope(treeScope) {} |
| |
| void DOMSelection::clearTreeScope() { |
| m_treeScope = nullptr; |
| } |
| |
| // TODO(editing-dev): The behavior after loosing browsing context is not |
| // specified. https://github.com/w3c/selection-api/issues/82 |
| bool DOMSelection::isAvailable() const { |
| return frame() && frame()->selection().isAvailable(); |
| } |
| |
| void DOMSelection::updateFrameSelection(const SelectionInDOMTree& selection, |
| Range* newCachedRange) const { |
| DCHECK(frame()); |
| FrameSelection& frameSelection = frame()->selection(); |
| // TODO(tkent): Specify FrameSelection::DoNotSetFocus. crbug.com/690272 |
| bool didSet = frameSelection.setSelectionDeprecated(selection); |
| cacheRangeIfSelectionOfDocument(newCachedRange); |
| if (didSet) |
| frameSelection.didSetSelectionDeprecated(); |
| } |
| |
| const VisibleSelection& DOMSelection::visibleSelection() const { |
| DCHECK(frame()); |
| return frame()->selection().computeVisibleSelectionInDOMTreeDeprecated(); |
| } |
| |
| bool DOMSelection::isBaseFirstInSelection() const { |
| DCHECK(frame()); |
| const SelectionInDOMTree& selection = |
| frame()->selection().selectionInDOMTree(); |
| return selection.base() <= selection.extent(); |
| } |
| |
| const Position& DOMSelection::anchorPosition() const { |
| DCHECK(frame()); |
| return frame()->selection().selectionInDOMTree().base(); |
| } |
| |
| // TODO(tkent): Following four functions based on VisibleSelection should be |
| // removed. |
| 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 (Range* range = primaryRangeOrNull()) { |
| if (!frame() || isBaseFirstInSelection()) |
| return range->startContainer(); |
| return range->endContainer(); |
| } |
| return nullptr; |
| } |
| |
| unsigned DOMSelection::anchorOffset() const { |
| if (Range* range = primaryRangeOrNull()) { |
| if (!frame() || isBaseFirstInSelection()) |
| return range->startOffset(); |
| return range->endOffset(); |
| } |
| return 0; |
| } |
| |
| Node* DOMSelection::focusNode() const { |
| if (Range* range = primaryRangeOrNull()) { |
| if (!frame() || isBaseFirstInSelection()) |
| return range->endContainer(); |
| return range->startContainer(); |
| } |
| return nullptr; |
| } |
| |
| unsigned DOMSelection::focusOffset() const { |
| if (Range* range = primaryRangeOrNull()) { |
| if (!frame() || isBaseFirstInSelection()) |
| return range->endOffset(); |
| return range->startOffset(); |
| } |
| return 0; |
| } |
| |
| Node* DOMSelection::baseNode() const { |
| if (!isAvailable()) |
| return 0; |
| |
| return shadowAdjustedNode(basePosition(visibleSelection())); |
| } |
| |
| unsigned DOMSelection::baseOffset() const { |
| if (!isAvailable()) |
| return 0; |
| |
| return shadowAdjustedOffset(basePosition(visibleSelection())); |
| } |
| |
| Node* DOMSelection::extentNode() const { |
| if (!isAvailable()) |
| return 0; |
| |
| return shadowAdjustedNode(extentPosition(visibleSelection())); |
| } |
| |
| unsigned DOMSelection::extentOffset() const { |
| if (!isAvailable()) |
| return 0; |
| |
| return shadowAdjustedOffset(extentPosition(visibleSelection())); |
| } |
| |
| bool DOMSelection::isCollapsed() const { |
| if (!isAvailable() || selectionShadowAncestor(frame())) |
| return true; |
| if (Range* range = primaryRangeOrNull()) |
| return range->collapsed(); |
| return true; |
| } |
| |
| String DOMSelection::type() const { |
| if (!isAvailable()) |
| return String(); |
| // 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 (rangeCount() == 0) |
| return "None"; |
| if (isCollapsed()) |
| return "Caret"; |
| return "Range"; |
| } |
| |
| unsigned DOMSelection::rangeCount() const { |
| if (!isAvailable()) |
| return 0; |
| if (documentCachedRange()) |
| return 1; |
| if (frame() |
| ->selection() |
| .computeVisibleSelectionInDOMTreeDeprecated() |
| .isNone()) |
| return 0; |
| // Any selection can be adjusted to Range for Document. |
| if (isSelectionOfDocument()) |
| return 1; |
| // In ShadowRoot, we need to try adjustment. |
| return createRangeFromSelectionEditor() ? 1 : 0; |
| } |
| |
| // https://www.w3.org/TR/selection-api/#dom-selection-collapse |
| void DOMSelection::collapse(Node* node, |
| unsigned offset, |
| ExceptionState& exceptionState) { |
| if (!isAvailable()) |
| return; |
| |
| // 1. If node is null, this method must behave identically as |
| // removeAllRanges() and abort these steps. |
| if (!node) { |
| UseCounter::count(frame(), UseCounter::SelectionCollapseNull); |
| frame()->selection().clear(); |
| return; |
| } |
| |
| // 2. The method must throw an IndexSizeError exception if offset is longer |
| // than node's length ([DOM4]) and abort these steps. |
| Range::checkNodeWOffset(node, offset, exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| |
| // 3. If node's root is not the document associated with the context object, |
| // abort these steps. |
| if (!isValidForPosition(node)) |
| return; |
| |
| // 4. Otherwise, let newRange be a new range. |
| Range* newRange = Range::create(*frame()->document()); |
| |
| // 5. Set ([DOM4]) the start and the end of newRange to (node, offset). |
| newRange->setStart(node, offset, exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| newRange->setEnd(node, offset, exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| |
| // 6. Set the context object's range to newRange. |
| updateFrameSelection( |
| SelectionInDOMTree::Builder() |
| .collapse(Position(node, offset)) |
| .setIsDirectional(frame()->selection().isDirectional()) |
| .build(), |
| newRange); |
| } |
| |
| // https://www.w3.org/TR/selection-api/#dom-selection-collapsetoend |
| void DOMSelection::collapseToEnd(ExceptionState& exceptionState) { |
| if (!isAvailable()) |
| return; |
| |
| // The method must throw InvalidStateError exception if the context object is |
| // empty. |
| if (rangeCount() == 0) { |
| exceptionState.throwDOMException(InvalidStateError, |
| "there is no selection."); |
| return; |
| } |
| |
| // Otherwise, it must create a new range, set both its start and end to the |
| // end of the context object's range, |
| Range* newRange = getRangeAt(0, ASSERT_NO_EXCEPTION)->cloneRange(); |
| newRange->collapse(false); |
| |
| // and then set the context object's range to the newly-created range. |
| SelectionInDOMTree::Builder builder; |
| builder.collapse(newRange->endPosition()); |
| updateFrameSelection(builder.build(), newRange); |
| } |
| |
| // https://www.w3.org/TR/selection-api/#dom-selection-collapsetostart |
| void DOMSelection::collapseToStart(ExceptionState& exceptionState) { |
| if (!isAvailable()) |
| return; |
| |
| // The method must throw InvalidStateError ([DOM4]) exception if the context |
| // object is empty. |
| if (rangeCount() == 0) { |
| exceptionState.throwDOMException(InvalidStateError, |
| "there is no selection."); |
| return; |
| } |
| |
| // Otherwise, it must create a new range, set both its start and end to the |
| // start of the context object's range, |
| Range* newRange = getRangeAt(0, ASSERT_NO_EXCEPTION)->cloneRange(); |
| newRange->collapse(true); |
| |
| // and then set the context object's range to the newly-created range. |
| SelectionInDOMTree::Builder builder; |
| builder.collapse(newRange->startPosition()); |
| updateFrameSelection(builder.build(), newRange); |
| } |
| |
| void DOMSelection::empty() { |
| if (!isAvailable()) |
| return; |
| frame()->selection().clear(); |
| } |
| |
| void DOMSelection::setBaseAndExtent(Node* baseNode, |
| unsigned baseOffset, |
| Node* extentNode, |
| unsigned extentOffset, |
| ExceptionState& exceptionState) { |
| if (!isAvailable()) |
| return; |
| |
| // TODO(editing-dev): Behavior on where base or extent is null is still |
| // under discussion: https://github.com/w3c/selection-api/issues/72 |
| if (!baseNode) { |
| UseCounter::count(frame(), UseCounter::SelectionSetBaseAndExtentNull); |
| frame()->selection().clear(); |
| return; |
| } |
| if (!extentNode) { |
| UseCounter::count(frame(), UseCounter::SelectionSetBaseAndExtentNull); |
| extentOffset = 0; |
| } |
| |
| Range::checkNodeWOffset(baseNode, baseOffset, exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| if (extentNode) { |
| Range::checkNodeWOffset(extentNode, extentOffset, exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| } |
| |
| if (!isValidForPosition(baseNode) || !isValidForPosition(extentNode)) |
| return; |
| |
| clearCachedRangeIfSelectionOfDocument(); |
| |
| // TODO(editing-dev): Once SVG USE element doesn't modify DOM tree, we |
| // should get rid of this update layout call. |
| // See http://crbug.com/566281 |
| // See "svg/text/textpath-reference-crash.html" |
| frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| Position basePosition(baseNode, baseOffset); |
| Position extentPosition(extentNode, extentOffset); |
| Range* newRange = Range::create(baseNode->document()); |
| if (extentPosition.isNull()) { |
| newRange->setStart(baseNode, baseOffset); |
| newRange->setEnd(baseNode, baseOffset); |
| } else if (basePosition < extentPosition) { |
| newRange->setStart(baseNode, baseOffset); |
| newRange->setEnd(extentNode, extentOffset); |
| } else { |
| newRange->setStart(extentNode, extentOffset); |
| newRange->setEnd(baseNode, baseOffset); |
| } |
| updateFrameSelection( |
| SelectionInDOMTree::Builder() |
| .setBaseAndExtentDeprecated(basePosition, extentPosition) |
| .setIsDirectional(true) |
| .build(), |
| newRange); |
| } |
| |
| 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); |
| } |
| |
| // https://www.w3.org/TR/selection-api/#dom-selection-extend |
| void DOMSelection::extend(Node* node, |
| unsigned offset, |
| ExceptionState& exceptionState) { |
| DCHECK(node); |
| if (!isAvailable()) |
| return; |
| |
| // 1. If node's root is not the document associated with the context object, |
| // abort these steps. |
| if (!isValidForPosition(node)) |
| return; |
| |
| // 2. If the context object is empty, throw an InvalidStateError exception and |
| // abort these steps. |
| if (rangeCount() == 0) { |
| exceptionState.throwDOMException( |
| InvalidStateError, "This Selection object doesn't have any Ranges."); |
| return; |
| } |
| |
| Range::checkNodeWOffset(node, offset, exceptionState); |
| if (exceptionState.hadException()) |
| return; |
| |
| // 3. Let oldAnchor and oldFocus be the context object's anchor and focus, and |
| // let newFocus be the boundary point (node, offset). |
| const Position& oldAnchor = anchorPosition(); |
| // TODO(tkent): Diagnostic checks for crbug.com/693578. They should be |
| // removed before M58 branch. |
| if (oldAnchor.isNull()) { |
| if (Range* range = documentCachedRange()) { |
| LOG(FATAL) |
| << "Selection has a cached Range, but anchorPosition is null. start=" |
| << range->startContainer() << " end=" << range->endContainer(); |
| } else if (frame() && |
| !frame() |
| ->selection() |
| .computeVisibleSelectionInDOMTreeDeprecated() |
| .isNone()) { |
| LOG(FATAL) << "FrameSelection is not none, but anchorPosition is null."; |
| } |
| } |
| DCHECK(!oldAnchor.isNull()); |
| const Position newFocus(node, offset); |
| |
| clearCachedRangeIfSelectionOfDocument(); |
| |
| // 4. Let newRange be a new range. |
| Range* newRange = Range::create(*frame()->document()); |
| |
| // 5. If node's root is not the same as the context object's range's root, set |
| // newRange's start and end to newFocus. |
| // E.g. oldAnchor might point in shadow Text node in TextControlElement. |
| if (oldAnchor.anchorNode()->treeRoot() != node->treeRoot()) { |
| newRange->setStart(node, offset); |
| newRange->setEnd(node, offset); |
| |
| } else if (oldAnchor <= newFocus) { |
| // 6. Otherwise, if oldAnchor is before or equal to newFocus, set newRange's |
| // start to oldAnchor, then set its end to newFocus. |
| newRange->setStart(oldAnchor.anchorNode(), |
| oldAnchor.offsetInContainerNode()); |
| newRange->setEnd(node, offset); |
| |
| } else { |
| // 7. Otherwise, set newRange's start to newFocus, then set its end to |
| // oldAnchor. |
| newRange->setStart(node, offset); |
| newRange->setEnd(oldAnchor.anchorNode(), oldAnchor.offsetInContainerNode()); |
| } |
| |
| // 8. Set the context object's range to newRange. |
| SelectionInDOMTree::Builder builder; |
| if (newRange->collapsed()) |
| builder.collapse(newFocus); |
| else |
| builder.collapse(oldAnchor).extend(newFocus); |
| updateFrameSelection(builder.setIsDirectional(true).build(), newRange); |
| } |
| |
| Range* DOMSelection::getRangeAt(unsigned index, |
| ExceptionState& exceptionState) const { |
| if (!isAvailable()) |
| return nullptr; |
| |
| if (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(), 1u); |
| |
| if (Range* cachedRange = documentCachedRange()) |
| return cachedRange; |
| |
| Range* range = createRangeFromSelectionEditor(); |
| cacheRangeIfSelectionOfDocument(range); |
| return range; |
| } |
| |
| Range* DOMSelection::primaryRangeOrNull() const { |
| return rangeCount() > 0 ? getRangeAt(0, ASSERT_NO_EXCEPTION) : nullptr; |
| } |
| |
| Range* DOMSelection::createRangeFromSelectionEditor() const { |
| Position anchor = blink::anchorPosition(visibleSelection()); |
| if (isSelectionOfDocument() && !anchor.anchorNode()->isInShadowTree()) |
| return frame()->selection().firstRange(); |
| |
| Node* node = shadowAdjustedNode(anchor); |
| if (!node) // crbug.com/595100 |
| return nullptr; |
| Position focus = focusPosition(visibleSelection()); |
| if (!visibleSelection().isBaseFirst()) { |
| return Range::create(*anchor.document(), shadowAdjustedNode(focus), |
| shadowAdjustedOffset(focus), node, |
| shadowAdjustedOffset(anchor)); |
| } |
| return Range::create(*anchor.document(), node, shadowAdjustedOffset(anchor), |
| shadowAdjustedNode(focus), shadowAdjustedOffset(focus)); |
| } |
| |
| bool DOMSelection::isSelectionOfDocument() const { |
| return m_treeScope == m_treeScope->document(); |
| } |
| |
| void DOMSelection::cacheRangeIfSelectionOfDocument(Range* range) const { |
| if (!isSelectionOfDocument()) |
| return; |
| if (!frame()) |
| return; |
| frame()->selection().cacheRangeOfDocument(range); |
| } |
| |
| Range* DOMSelection::documentCachedRange() const { |
| if (!isSelectionOfDocument()) |
| return nullptr; |
| return frame()->selection().documentCachedRange(); |
| } |
| |
| void DOMSelection::clearCachedRangeIfSelectionOfDocument() { |
| if (!isSelectionOfDocument()) |
| return; |
| frame()->selection().clearDocumentCachedRange(); |
| } |
| |
| void DOMSelection::removeRange(Range* range) { |
| DCHECK(range); |
| if (!isAvailable()) |
| return; |
| if (range == primaryRangeOrNull()) |
| frame()->selection().clear(); |
| } |
| |
| 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; |
| } |
| |
| if (rangeCount() == 0) { |
| updateFrameSelection(SelectionInDOMTree::Builder() |
| .collapse(newRange->startPosition()) |
| .extend(newRange->endPosition()) |
| .build(), |
| newRange); |
| return; |
| } |
| |
| Range* originalRange = primaryRangeOrNull(); |
| DCHECK(originalRange); |
| |
| if (originalRange->startContainer()->treeScope() != |
| newRange->startContainer()->treeScope()) { |
| return; |
| } |
| |
| if (originalRange->compareBoundaryPoints(Range::kStartToEnd, newRange, |
| ASSERT_NO_EXCEPTION) < 0 || |
| newRange->compareBoundaryPoints(Range::kStartToEnd, originalRange, |
| ASSERT_NO_EXCEPTION) < 0) { |
| return; |
| } |
| |
| // TODO(tkent): "Merge the ranges if they intersect" was removed. We show a |
| // warning message for a while, and continue to collect the usage data. |
| // <https://code.google.com/p/chromium/issues/detail?id=353069>. |
| Deprecation::countDeprecation(frame(), |
| UseCounter::SelectionAddRangeIntersect); |
| } |
| |
| // https://www.w3.org/TR/selection-api/#dom-selection-deletefromdocument |
| void DOMSelection::deleteFromDocument() { |
| if (!isAvailable()) |
| return; |
| |
| // The method must invoke deleteContents() ([DOM4]) on the context object's |
| // range if the context object is not empty. Otherwise the method must do |
| // nothing. |
| if (Range* range = documentCachedRange()) { |
| range->deleteContents(ASSERT_NO_EXCEPTION); |
| return; |
| } |
| |
| // The following code is necessary for |
| // editing/selection/deleteFromDocument-crash.html, which assumes |
| // deleteFromDocument() for text selection in a TEXTAREA deletes the TEXTAREA |
| // value. |
| |
| FrameSelection& selection = frame()->selection(); |
| |
| if (selection.computeVisibleSelectionInDOMTreeDeprecated().isNone()) |
| return; |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| // |VisibleSelection::toNormalizedEphemeralRange| requires clean layout. |
| frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| Range* selectedRange = |
| createRange(selection.computeVisibleSelectionInDOMTreeDeprecated() |
| .toNormalizedEphemeralRange()); |
| if (!selectedRange) |
| return; |
| |
| // |selectedRange| may point nodes in a different root. |
| selectedRange->deleteContents(ASSERT_NO_EXCEPTION); |
| } |
| |
| bool DOMSelection::containsNode(const Node* n, bool allowPartial) const { |
| DCHECK(n); |
| |
| if (!isAvailable()) |
| return false; |
| |
| if (frame()->document() != n->document()) |
| return false; |
| |
| unsigned nodeIndex = n->nodeIndex(); |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| // |VisibleSelection::toNormalizedEphemeralRange| requires clean layout. |
| frame()->document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| FrameSelection& selection = frame()->selection(); |
| const EphemeralRange selectedRange = |
| selection.computeVisibleSelectionInDOMTreeDeprecated() |
| .toNormalizedEphemeralRange(); |
| if (selectedRange.isNull()) |
| return false; |
| |
| ContainerNode* parentNode = n->parentNode(); |
| if (!parentNode) |
| return false; |
| |
| const Position startPosition = |
| selectedRange.startPosition().toOffsetInAnchor(); |
| const Position endPosition = selectedRange.endPosition().toOffsetInAnchor(); |
| DummyExceptionStateForTesting 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() |
| .computeVisibleSelectionInDOMTreeDeprecated() |
| .toNormalizedEphemeralRange(); |
| return plainText( |
| range, |
| TextIteratorBehavior::Builder().setForSelectionToString(true).build()); |
| } |
| |
| 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(); |
| } |
| |
| unsigned 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); |
| ContextClient::trace(visitor); |
| } |
| |
| } // namespace blink |