bool isStartOfDocument(const VisiblePosition &p) { return p.isNotNull() && p.previous().isNull(); }
VisiblePositionRange AccessibilityObject::rightLineVisiblePositionRange(const VisiblePosition& visiblePos) const { if (visiblePos.isNull()) return VisiblePositionRange(); // make sure we move off of a line end VisiblePosition nextVisiblePos = visiblePos.next(); if (nextVisiblePos.isNull()) return VisiblePositionRange(); VisiblePosition startPosition = startOfLine(nextVisiblePos); // fetch for a valid line start position if (startPosition.isNull()) { startPosition = visiblePos; nextVisiblePos = nextVisiblePos.next(); } else startPosition = updateAXLineStartForVisiblePosition(startPosition); VisiblePosition endPosition = endOfLine(nextVisiblePos); // as long as the position hasn't reached the end of the doc, keep searching for a valid line end position // Unless the VisiblePosition is at the very end, there should always be a valid line range. However, endOfLine will // return null for position by a floating object, since floating object doesn't really belong to any line. // This check will reposition the marker after the floating object, to ensure we get a line end. while (endPosition.isNull() && nextVisiblePos.isNotNull()) { nextVisiblePos = nextVisiblePos.next(); endPosition = endOfLine(nextVisiblePos); } return VisiblePositionRange(startPosition, endPosition); }
void InsertListCommand::doApply() { if (!endingSelection().isNonOrphanedCaretOrRange()) return; if (!endingSelection().rootEditableElement()) return; VisiblePosition visibleEnd = endingSelection().visibleEnd(); VisiblePosition visibleStart = endingSelection().visibleStart(); // When a selection ends at the start of a paragraph, we rarely paint // the selection gap before that paragraph, because there often is no gap. // In a case like this, it's not obvious to the user that the selection // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List // operated on that paragraph. // FIXME: We paint the gap before some paragraphs that are indented with left // margin/padding, but not others. We should make the gap painting more consistent and // then use a left margin/padding rule here. if (visibleEnd.deepEquivalent() != visibleStart.deepEquivalent() && isStartOfParagraph(visibleEnd, CanSkipOverEditingBoundary)) { setEndingSelection(VisibleSelection(visibleStart, previousPositionOf(visibleEnd, CannotCrossEditingBoundary), endingSelection().isDirectional())); if (!endingSelection().rootEditableElement()) return; } const HTMLQualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag; if (endingSelection().isRange()) { bool forceListCreation = false; VisibleSelection selection = selectionForParagraphIteration(endingSelection()); ASSERT(selection.isRange()); VisiblePosition startOfSelection = selection.visibleStart(); VisiblePosition endOfSelection = selection.visibleEnd(); VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary); RefPtrWillBeRawPtr<Range> currentSelection = firstRangeOf(endingSelection()); RefPtrWillBeRawPtr<ContainerNode> scopeForStartOfSelection = nullptr; RefPtrWillBeRawPtr<ContainerNode> scopeForEndOfSelection = nullptr; // FIXME: This is an inefficient way to keep selection alive because // indexForVisiblePosition walks from the beginning of the document to the // endOfSelection everytime this code is executed. But not using index is hard // because there are so many ways we can los eselection inside doApplyForSingleParagraph. int indexForStartOfSelection = indexForVisiblePosition(startOfSelection, scopeForStartOfSelection); int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scopeForEndOfSelection); if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary).deepEquivalent() != startOfLastParagraph.deepEquivalent()) { forceListCreation = !selectionHasListOfType(selection, listTag); VisiblePosition startOfCurrentParagraph = startOfSelection; while (inSameTreeAndOrdered(startOfCurrentParagraph, startOfLastParagraph) && !inSameParagraph(startOfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) { // doApply() may operate on and remove the last paragraph of the selection from the document // if it's in the same list item as startOfCurrentParagraph. Return early to avoid an // infinite loop and because there is no more work to be done. // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here. Compute // the new location of endOfSelection and use it as the end of the new selection. if (!startOfLastParagraph.deepEquivalent().inDocument()) return; setEndingSelection(startOfCurrentParagraph); // Save and restore endOfSelection and startOfLastParagraph when necessary // since moveParagraph and movePragraphWithClones can remove nodes. if (!doApplyForSingleParagraph(forceListCreation, listTag, *currentSelection)) break; if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) { endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scopeForEndOfSelection.get()); // If endOfSelection is null, then some contents have been deleted from the document. // This should never happen and if it did, exit early immediately because we've lost the loop invariant. ASSERT(endOfSelection.isNotNull()); if (endOfSelection.isNull() || !rootEditableElementOf(endOfSelection)) return; startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary); } startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart()); } setEndingSelection(endOfSelection); } doApplyForSingleParagraph(forceListCreation, listTag, *currentSelection); // Fetch the end of the selection, for the reason mentioned above. if (endOfSelection.isNull() || endOfSelection.isOrphan()) { endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scopeForEndOfSelection.get()); if (endOfSelection.isNull()) return; } if (startOfSelection.isNull() || startOfSelection.isOrphan()) { startOfSelection = visiblePositionForIndex(indexForStartOfSelection, scopeForStartOfSelection.get()); if (startOfSelection.isNull()) return; } setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional())); return; } ASSERT(firstRangeOf(endingSelection())); doApplyForSingleParagraph(false, listTag, *firstRangeOf(endingSelection())); }
void Selection::setBase(const VisiblePosition& visiblePosition) { m_base = visiblePosition.deepEquivalent(); validate(); }
void Selection::validate() { // Move the selection to rendered positions, if possible. bool baseAndExtentEqual = m_base == m_extent; if (m_base.isNotNull()) { m_base = VisiblePosition(m_base, m_affinity).deepEquivalent(); if (baseAndExtentEqual) m_extent = m_base; } if (m_extent.isNotNull() && !baseAndExtentEqual) m_extent = VisiblePosition(m_extent, m_affinity).deepEquivalent(); // Make sure we do not have a dangling base or extent. if (m_base.isNull() && m_extent.isNull()) m_baseIsFirst = true; else if (m_base.isNull()) { m_base = m_extent; m_baseIsFirst = true; } else if (m_extent.isNull()) { m_extent = m_base; m_baseIsFirst = true; } else { m_baseIsFirst = comparePositions(m_base, m_extent) <= 0; } if (m_baseIsFirst) { m_start = m_base; m_end = m_extent; } else { m_start = m_extent; m_end = m_base; } // Expand the selection if requested. switch (m_granularity) { case CharacterGranularity: // Don't do any expansion. break; case WordGranularity: { // General case: Select the word the caret is positioned inside of, or at the start of (RightWordIfOnBoundary). // Edge case: If the caret is after the last word in a soft-wrapped line or the last word in // the document, select that last word (LeftWordIfOnBoundary). // Edge case: If the caret is after the last word in a paragraph, select from the the end of the // last word to the line break (also RightWordIfOnBoundary); VisiblePosition start = VisiblePosition(m_start, m_affinity); VisiblePosition originalEnd(m_end, m_affinity); EWordSide side = RightWordIfOnBoundary; if (isEndOfDocument(start) || (isEndOfLine(start) && !isStartOfLine(start) && !isEndOfParagraph(start))) side = LeftWordIfOnBoundary; m_start = startOfWord(start, side).deepEquivalent(); side = RightWordIfOnBoundary; if (isEndOfDocument(originalEnd) || (isEndOfLine(originalEnd) && !isStartOfLine(originalEnd) && !isEndOfParagraph(originalEnd))) side = LeftWordIfOnBoundary; VisiblePosition wordEnd(endOfWord(originalEnd, side)); VisiblePosition end(wordEnd); if (isEndOfParagraph(originalEnd)) { // Select the paragraph break (the space from the end of a paragraph to the start of // the next one) to match TextEdit. end = wordEnd.next(); if (Node* table = isFirstPositionAfterTable(end)) { // The paragraph break after the last paragraph in the last cell of a block table ends // at the start of the paragraph after the table. if (isBlock(table)) end = end.next(true); else end = wordEnd; } if (end.isNull()) end = wordEnd; } m_end = end.deepEquivalent(); break; } case SentenceGranularity: { m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; } case LineGranularity: { m_start = startOfLine(VisiblePosition(m_start, m_affinity)).deepEquivalent(); VisiblePosition end = endOfLine(VisiblePosition(m_end, m_affinity)); // If the end of this line is at the end of a paragraph, include the space // after the end of the line in the selection. if (isEndOfParagraph(end)) { VisiblePosition next = end.next(); if (next.isNotNull()) end = next; } m_end = end.deepEquivalent(); break; } case LineBoundary: m_start = startOfLine(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfLine(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; case ParagraphGranularity: { VisiblePosition pos(m_start, m_affinity); if (isStartOfLine(pos) && isEndOfDocument(pos)) pos = pos.previous(); m_start = startOfParagraph(pos).deepEquivalent(); VisiblePosition visibleParagraphEnd = endOfParagraph(VisiblePosition(m_end, m_affinity)); // Include the "paragraph break" (the space from the end of this paragraph to the start // of the next one) in the selection. VisiblePosition end(visibleParagraphEnd.next()); if (Node* table = isFirstPositionAfterTable(end)) { // The paragraph break after the last paragraph in the last cell of a block table ends // at the start of the paragraph after the table, not at the position just after the table. if (isBlock(table)) end = end.next(true); // There is no parargraph break after the last paragraph in the last cell of an inline table. else end = visibleParagraphEnd; } if (end.isNull()) end = visibleParagraphEnd; m_end = end.deepEquivalent(); break; } case DocumentBoundary: m_start = startOfDocument(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfDocument(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; case ParagraphBoundary: m_start = startOfParagraph(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfParagraph(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; case SentenceBoundary: m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; } // Make sure we do not have a dangling start or end. if (m_start.isNull()) m_start = m_end; if (m_end.isNull()) m_end = m_start; adjustForEditableContent(); // adjust the state if (m_start.isNull()) { ASSERT(m_end.isNull()); m_state = NONE; // enforce downstream affinity if not caret, as affinity only // makes sense for caret m_affinity = DOWNSTREAM; } else if (m_start == m_end || m_start.upstream() == m_end.upstream()) { m_state = CARET; } else { m_state = RANGE; // enforce downstream affinity if not caret, as affinity only // makes sense for caret m_affinity = DOWNSTREAM; // "Constrain" the selection to be the smallest equivalent range of nodes. // This is a somewhat arbitrary choice, but experience shows that it is // useful to make to make the selection "canonical" (if only for // purposes of comparing selections). This is an ideal point of the code // to do this operation, since all selection changes that result in a RANGE // come through here before anyone uses it. m_start = m_start.downstream(); m_end = m_end.upstream(); } }
// If a selection starts in one block and ends in another, we have to merge to bring content before the // start together with content after the end. void DeleteSelectionCommand::mergeParagraphs() { if (!m_mergeBlocksAfterDelete) { if (m_pruneStartBlockIfNecessary) { // We aren't going to merge into the start block, so remove it if it's empty. prune(m_startBlock); // Removing the start block during a deletion is usually an indication that we need // a placeholder, but not in this case. m_needPlaceholder = false; } return; } // It shouldn't have been asked to both try and merge content into the start block and prune it. ASSERT(!m_pruneStartBlockIfNecessary); // FIXME: Deletion should adjust selection endpoints as it removes nodes so that we never get into this state (4099839). if (!m_downstreamEnd.anchorNode()->inDocument() || !m_upstreamStart.anchorNode()->inDocument()) return; // FIXME: The deletion algorithm shouldn't let this happen. if (comparePositions(m_upstreamStart, m_downstreamEnd) > 0) return; // There's nothing to merge. if (m_upstreamStart == m_downstreamEnd) return; VisiblePosition startOfParagraphToMove(m_downstreamEnd); VisiblePosition mergeDestination(m_upstreamStart); // m_downstreamEnd's block has been emptied out by deletion. There is no content inside of it to // move, so just remove it. Element* endBlock = static_cast<Element*>(enclosingBlock(m_downstreamEnd.deprecatedNode())); if (!endBlock || !endBlock->contains(startOfParagraphToMove.deepEquivalent().deprecatedNode()) || !startOfParagraphToMove.deepEquivalent().deprecatedNode()) { removeNode(enclosingBlock(m_downstreamEnd.deprecatedNode())); return; } // We need to merge into m_upstreamStart's block, but it's been emptied out and collapsed by deletion. if (!mergeDestination.deepEquivalent().deprecatedNode() || !mergeDestination.deepEquivalent().deprecatedNode()->isDescendantOf(enclosingBlock(m_upstreamStart.containerNode())) || m_startsAtEmptyLine) { insertNodeAt(createBreakElement(document()).get(), m_upstreamStart); mergeDestination = VisiblePosition(m_upstreamStart); } if (mergeDestination == startOfParagraphToMove) return; VisiblePosition endOfParagraphToMove = endOfParagraph(startOfParagraphToMove); if (mergeDestination == endOfParagraphToMove) return; // The rule for merging into an empty block is: only do so if its farther to the right. // FIXME: Consider RTL. if (!m_startsAtEmptyLine && isStartOfParagraph(mergeDestination) && startOfParagraphToMove.absoluteCaretBounds().x() > mergeDestination.absoluteCaretBounds().x()) { if (mergeDestination.deepEquivalent().downstream().deprecatedNode()->hasTagName(brTag)) { removeNodeAndPruneAncestors(mergeDestination.deepEquivalent().downstream().deprecatedNode()); m_endingPosition = startOfParagraphToMove.deepEquivalent(); return; } } // Block images, tables and horizontal rules cannot be made inline with content at mergeDestination. If there is // any (!isStartOfParagraph(mergeDestination)), don't merge, just move the caret to just before the selection we deleted. // See https://bugs.webkit.org/show_bug.cgi?id=25439 if (isRenderedAsNonInlineTableImageOrHR(startOfParagraphToMove.deepEquivalent().deprecatedNode()) && !isStartOfParagraph(mergeDestination)) { m_endingPosition = m_upstreamStart; return; } RefPtr<Range> range = Range::create(document(), startOfParagraphToMove.deepEquivalent().parentAnchoredEquivalent(), endOfParagraphToMove.deepEquivalent().parentAnchoredEquivalent()); RefPtr<Range> rangeToBeReplaced = Range::create(document(), mergeDestination.deepEquivalent().parentAnchoredEquivalent(), mergeDestination.deepEquivalent().parentAnchoredEquivalent()); if (!document()->frame()->editor()->client()->shouldMoveRangeAfterDelete(range.get(), rangeToBeReplaced.get())) return; // moveParagraphs will insert placeholders if it removes blocks that would require their use, don't let block // removals that it does cause the insertion of *another* placeholder. bool needPlaceholder = m_needPlaceholder; bool paragraphToMergeIsEmpty = (startOfParagraphToMove == endOfParagraphToMove); moveParagraph(startOfParagraphToMove, endOfParagraphToMove, mergeDestination, false, !paragraphToMergeIsEmpty); m_needPlaceholder = needPlaceholder; // The endingPosition was likely clobbered by the move, so recompute it (moveParagraph selects the moved paragraph). m_endingPosition = endingSelection().start(); }
AccessibilityObject* objectFocusedAndCaretOffsetUnignored(AccessibilityObject* referenceObject, int& offset) { // Indication that something bogus has transpired. offset = -1; Document* document = referenceObject->document(); if (!document) return 0; Node* focusedNode = referenceObject->selection().end().containerNode(); if (!focusedNode) return 0; RenderObject* focusedRenderer = focusedNode->renderer(); if (!focusedRenderer) return 0; AccessibilityObject* focusedObject = document->axObjectCache()->getOrCreate(focusedRenderer); if (!focusedObject) return 0; // Look for the actual (not ignoring accessibility) selected object. AccessibilityObject* firstUnignoredParent = focusedObject; if (firstUnignoredParent->accessibilityIsIgnored()) firstUnignoredParent = firstUnignoredParent->parentObjectUnignored(); if (!firstUnignoredParent) return 0; // Don't ignore links if the offset is being requested for a link. if (!referenceObject->isLink() && firstUnignoredParent->isLink()) firstUnignoredParent = firstUnignoredParent->parentObjectUnignored(); if (!firstUnignoredParent) return 0; // The reference object must either coincide with the focused // object being considered, or be a descendant of it. if (referenceObject->isDescendantOfObject(firstUnignoredParent)) referenceObject = firstUnignoredParent; Node* startNode = 0; if (firstUnignoredParent != referenceObject || firstUnignoredParent->isTextControl()) { // We need to use the first child's node of the reference // object as the start point to calculate the caret offset // because we want it to be relative to the object of // reference, not just to the focused object (which could have // previous siblings which should be taken into account too). AccessibilityObject* axFirstChild = referenceObject->firstChild(); if (axFirstChild) startNode = axFirstChild->node(); } if (!startNode) startNode = firstUnignoredParent->node(); // Check if the node for the first parent object not ignoring // accessibility is null again before using it. This might happen // with certain kind of accessibility objects, such as the root // one (the scroller containing the webArea object). if (!startNode) return 0; VisiblePosition startPosition = VisiblePosition(positionBeforeNode(startNode), DOWNSTREAM); VisiblePosition endPosition = firstUnignoredParent->selection().visibleEnd(); if (startPosition == endPosition) offset = 0; else if (!isStartOfLine(endPosition)) { RefPtr<Range> range = makeRange(startPosition, endPosition.previous()); offset = TextIterator::rangeLength(range.get(), true) + 1; } else { RefPtr<Range> range = makeRange(startPosition, endPosition); offset = TextIterator::rangeLength(range.get(), true); } return firstUnignoredParent; }
// FIXME: We should merge this function with ApplyBlockElementCommand::formatSelection void IndentOutdentCommand::outdentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection) { VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection); VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection); if (endOfCurrentParagraph == endOfLastParagraph) { outdentParagraph(); return; } Position originalSelectionEnd = endingSelection().end(); VisiblePosition endAfterSelection = endOfParagraph(endOfLastParagraph.next()); while (endOfCurrentParagraph != endAfterSelection) { VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); if (endOfCurrentParagraph == endOfLastParagraph) setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM)); else setEndingSelection(endOfCurrentParagraph); outdentParagraph(); // outdentParagraph could move more than one paragraph if the paragraph // is in a list item. As a result, endAfterSelection and endOfNextParagraph // could refer to positions no longer in the document. if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().inDocument()) break; if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().inDocument()) { endOfCurrentParagraph = VisiblePosition(endingSelection().end()); endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); } endOfCurrentParagraph = endOfNextParagraph; } }
void ApplyBlockElementCommand::doApply() { if (!endingSelection().rootEditableElement()) return; VisiblePosition visibleEnd = endingSelection().visibleEnd(); VisiblePosition visibleStart = endingSelection().visibleStart(); if (visibleStart.isNull() || visibleStart.isOrphan() || visibleEnd.isNull() || visibleEnd.isOrphan()) return; // When a selection ends at the start of a paragraph, we rarely paint // the selection gap before that paragraph, because there often is no gap. // In a case like this, it's not obvious to the user that the selection // ends "inside" that paragraph, so it would be confusing if Indent/Outdent // operated on that paragraph. // FIXME: We paint the gap before some paragraphs that are indented with left // margin/padding, but not others. We should make the gap painting more consistent and // then use a left margin/padding rule here. if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional())); VisibleSelection selection = selectionForParagraphIteration(endingSelection()); VisiblePosition startOfSelection = selection.visibleStart(); VisiblePosition endOfSelection = selection.visibleEnd(); ASSERT(!startOfSelection.isNull()); ASSERT(!endOfSelection.isNull()); RefPtr<ContainerNode> startScope; int startIndex = indexForVisiblePosition(startOfSelection, startScope); RefPtr<ContainerNode> endScope; int endIndex = indexForVisiblePosition(endOfSelection, endScope); formatSelection(startOfSelection, endOfSelection); document()->updateLayoutIgnorePendingStylesheets(); ASSERT(startScope == endScope); ASSERT(startIndex >= 0); ASSERT(startIndex <= endIndex); if (startScope == endScope && startIndex >= 0 && startIndex <= endIndex) { VisiblePosition start(visiblePositionForIndex(startIndex, startScope.get())); VisiblePosition end(visiblePositionForIndex(endIndex, endScope.get())); if (start.isNotNull() && end.isNotNull()) setEndingSelection(VisibleSelection(start, end, endingSelection().isDirectional())); } }
void FormatBlockCommand::doApply() { if (!endingSelection().isNonOrphanedCaretOrRange()) return; if (!endingSelection().rootEditableElement()) return; VisiblePosition visibleEnd = endingSelection().visibleEnd(); VisiblePosition visibleStart = endingSelection().visibleStart(); // When a selection ends at the start of a paragraph, we rarely paint // the selection gap before that paragraph, because there often is no gap. // In a case like this, it's not obvious to the user that the selection // ends "inside" that paragraph, so it would be confusing if FormatBlock // operated on that paragraph. // FIXME: We paint the gap before some paragraphs that are indented with left // margin/padding, but not others. We should make the gap painting more consistent and // then use a left margin/padding rule here. if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true))); if (endingSelection().isRange() && modifyRange()) return; ExceptionCode ec; String localName, prefix; if (!Document::parseQualifiedName(m_tagName, prefix, localName, ec)) return; QualifiedName qTypeOfBlock(prefix, localName, xhtmlNamespaceURI); Node* refNode = enclosingBlockFlowElement(endingSelection().visibleStart()); if (refNode->hasTagName(qTypeOfBlock)) // We're already in a block with the format we want, so we don't have to do anything return; VisiblePosition paragraphStart = startOfParagraph(endingSelection().visibleStart()); VisiblePosition paragraphEnd = endOfParagraph(endingSelection().visibleStart()); VisiblePosition blockStart = startOfBlock(endingSelection().visibleStart()); VisiblePosition blockEnd = endOfBlock(endingSelection().visibleStart()); RefPtr<Element> blockNode = createHTMLElement(document(), m_tagName); RefPtr<Element> placeholder = createBreakElement(document()); Node* root = endingSelection().start().node()->rootEditableElement(); if (validBlockTag(refNode->nodeName().lower()) && paragraphStart == blockStart && paragraphEnd == blockEnd && refNode != root && !root->isDescendantOf(refNode)) // Already in a valid block tag that only contains the current paragraph, so we can swap with the new tag insertNodeBefore(blockNode, refNode); else { // Avoid inserting inside inline elements that surround paragraphStart with upstream(). // This is only to avoid creating bloated markup. insertNodeAt(blockNode, paragraphStart.deepEquivalent().upstream()); } appendNode(placeholder, blockNode); VisiblePosition destination(Position(placeholder.get(), 0)); if (paragraphStart == paragraphEnd && !lineBreakExistsAtVisiblePosition(paragraphStart)) { setEndingSelection(destination); return; } moveParagraph(paragraphStart, paragraphEnd, destination, true, false); }
void IndentOutdentCommand::outdentParagraph() { VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart()); VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph); HTMLElement* enclosingElement = toHTMLElement(enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isHTMLListOrBlockquoteElement)); if (!enclosingElement || !enclosingElement->parentNode()->hasEditableStyle()) // We can't outdent if there is no place to go! return; // Use InsertListCommand to remove the selection from the list if (isHTMLOListElement(*enclosingElement)) { applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList)); return; } if (isHTMLUListElement(*enclosingElement)) { applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList)); return; } // The selection is inside a blockquote i.e. enclosingNode is a blockquote VisiblePosition positionInEnclosingBlock = VisiblePosition(firstPositionInNode(enclosingElement)); // If the blockquote is inline, the start of the enclosing block coincides with // positionInEnclosingBlock. VisiblePosition startOfEnclosingBlock = (enclosingElement->layoutObject() && enclosingElement->layoutObject()->isInline()) ? positionInEnclosingBlock : startOfBlock(positionInEnclosingBlock); VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(lastPositionInNode(enclosingElement)); VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock); if (visibleStartOfParagraph == startOfEnclosingBlock && visibleEndOfParagraph == endOfEnclosingBlock) { // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed. Node* splitPoint = enclosingElement->nextSibling(); removeNodePreservingChildren(enclosingElement); // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true if (splitPoint) { if (Element* splitPointParent = splitPoint->parentElement()) { if (splitPointParent->hasTagName(blockquoteTag) && !splitPoint->hasTagName(blockquoteTag) && splitPointParent->parentNode()->hasEditableStyle()) // We can't outdent if there is no place to go! splitElement(splitPointParent, splitPoint); } } document().updateLayoutIgnorePendingStylesheets(); visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent()); visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent()); if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph)) insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent()); if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph)) insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent()); return; } RefPtrWillBeRawPtr<Node> splitBlockquoteNode = enclosingElement; if (Element* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().deprecatedNode())) { if (enclosingBlockFlow != enclosingElement) { splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingElement, true); } else { // We split the blockquote at where we start outdenting. Node* highestInlineNode = highestEnclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), isInline, CannotCrossEditingBoundary, enclosingBlockFlow); splitElement(enclosingElement, highestInlineNode ? highestInlineNode : visibleStartOfParagraph.deepEquivalent().deprecatedNode()); } } VisiblePosition startOfParagraphToMove(startOfParagraph(visibleStartOfParagraph)); VisiblePosition endOfParagraphToMove(endOfParagraph(visibleEndOfParagraph)); if (startOfParagraphToMove.isNull() || endOfParagraphToMove.isNull()) return; RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document()); insertNodeBefore(placeholder, splitBlockquoteNode); moveParagraph(startOfParagraphToMove, endOfParagraphToMove, VisiblePosition(positionBeforeNode(placeholder.get())), true); }
bool SelectionModifier::modifyWithPageGranularity(EAlteration alter, unsigned verticalDistance, VerticalDirection direction) { if (!verticalDistance) return false; DCHECK(!frame()->document()->needsLayoutTreeUpdate()); DocumentLifecycle::DisallowTransitionScope disallowTransition( frame()->document()->lifecycle()); willBeModified(alter, direction == FrameSelection::DirectionUp ? DirectionBackward : DirectionForward); VisiblePosition pos; LayoutUnit xPos; switch (alter) { case FrameSelection::AlterationMove: pos = createVisiblePosition(direction == FrameSelection::DirectionUp ? m_selection.start() : m_selection.end(), m_selection.affinity()); xPos = lineDirectionPointForBlockDirectionNavigation( direction == FrameSelection::DirectionUp ? START : END); m_selection.setAffinity(direction == FrameSelection::DirectionUp ? TextAffinity::Upstream : TextAffinity::Downstream); break; case FrameSelection::AlterationExtend: pos = createVisiblePosition(m_selection.extent(), m_selection.affinity()); xPos = lineDirectionPointForBlockDirectionNavigation(EXTENT); m_selection.setAffinity(TextAffinity::Downstream); break; } int startY; if (!absoluteCaretY(pos, startY)) return false; if (direction == FrameSelection::DirectionUp) startY = -startY; int lastY = startY; VisiblePosition result; VisiblePosition next; for (VisiblePosition p = pos;; p = next) { if (direction == FrameSelection::DirectionUp) next = previousLinePosition(p, xPos); else next = nextLinePosition(p, xPos); if (next.isNull() || next.deepEquivalent() == p.deepEquivalent()) break; int nextY; if (!absoluteCaretY(next, nextY)) break; if (direction == FrameSelection::DirectionUp) nextY = -nextY; if (nextY - startY > static_cast<int>(verticalDistance)) break; if (nextY >= lastY) { lastY = nextY; result = next; } } if (result.isNull()) return false; switch (alter) { case FrameSelection::AlterationMove: m_selection = createVisibleSelection( SelectionInDOMTree::Builder() .collapse(result.toPositionWithAffinity()) .setIsDirectional(m_selection.isDirectional()) .build()); break; case FrameSelection::AlterationExtend: m_selection.setExtent(result); break; } m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(frame()) || alter == FrameSelection::AlterationExtend); return true; }
bool SelectionModifier::modify(EAlteration alter, SelectionDirection direction, TextGranularity granularity) { DCHECK(!frame()->document()->needsLayoutTreeUpdate()); DocumentLifecycle::DisallowTransitionScope disallowTransition( frame()->document()->lifecycle()); willBeModified(alter, direction); bool wasRange = m_selection.isRange(); VisiblePosition originalStartPosition = m_selection.visibleStart(); VisiblePosition position; switch (direction) { case DirectionRight: if (alter == FrameSelection::AlterationMove) position = modifyMovingRight(granularity); else position = modifyExtendingRight(granularity); break; case DirectionForward: if (alter == FrameSelection::AlterationExtend) position = modifyExtendingForward(granularity); else position = modifyMovingForward(granularity); break; case DirectionLeft: if (alter == FrameSelection::AlterationMove) position = modifyMovingLeft(granularity); else position = modifyExtendingLeft(granularity); break; case DirectionBackward: if (alter == FrameSelection::AlterationExtend) position = modifyExtendingBackward(granularity); else position = modifyMovingBackward(granularity); break; } if (position.isNull()) return false; if (isSpatialNavigationEnabled(frame())) { if (!wasRange && alter == FrameSelection::AlterationMove && position.deepEquivalent() == originalStartPosition.deepEquivalent()) return false; } // Some of the above operations set an xPosForVerticalArrowNavigation. // Setting a selection will clear it, so save it to possibly restore later. // Note: the START position type is arbitrary because it is unused, it would // be the requested position type if there were no // xPosForVerticalArrowNavigation set. LayoutUnit x = lineDirectionPointForBlockDirectionNavigation(START); m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(frame()) || alter == FrameSelection::AlterationExtend); switch (alter) { case FrameSelection::AlterationMove: m_selection = createVisibleSelection( SelectionInDOMTree::Builder() .collapse(position.toPositionWithAffinity()) .setIsDirectional(m_selection.isDirectional()) .build()); break; case FrameSelection::AlterationExtend: if (!m_selection.isCaret() && (granularity == WordGranularity || granularity == ParagraphGranularity || granularity == LineGranularity) && frame() && !frame() ->editor() .behavior() .shouldExtendSelectionByWordOrLineAcrossCaret()) { // Don't let the selection go across the base position directly. Needed // to match mac behavior when, for instance, word-selecting backwards // starting with the caret in the middle of a word and then // word-selecting forward, leaving the caret in the same place where it // was, instead of directly selecting to the end of the word. VisibleSelection newSelection = m_selection; newSelection.setExtent(position); if (m_selection.isBaseFirst() != newSelection.isBaseFirst()) position = m_selection.visibleBase(); } // Standard Mac behavior when extending to a boundary is grow the // selection rather than leaving the base in place and moving the // extent. Matches NSTextView. if (!frame() || !frame() ->editor() .behavior() .shouldAlwaysGrowSelectionWhenExtendingToBoundary() || m_selection.isCaret() || !isBoundary(granularity)) { m_selection.setExtent(position); } else { TextDirection textDirection = directionOfEnclosingBlock(); if (direction == DirectionForward || (textDirection == LTR && direction == DirectionRight) || (textDirection == RTL && direction == DirectionLeft)) setSelectionEnd(&m_selection, position); else setSelectionStart(&m_selection, position); } break; } if (granularity == LineGranularity || granularity == ParagraphGranularity) m_xPosForVerticalArrowNavigation = x; return true; }
bool isEndOfDocument(const VisiblePosition &p) { return p.isNotNull() && p.next().isNull(); }
void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool killRing) { LocalFrame* frame = document().frame(); if (!frame) return; frame->spellChecker().updateMarkersForWordsAffectedByEditing(false); VisibleSelection selectionToDelete; VisibleSelection selectionAfterUndo; switch (endingSelection().selectionType()) { case RangeSelection: selectionToDelete = endingSelection(); selectionAfterUndo = selectionToDelete; break; case CaretSelection: { m_smartDelete = false; // Handle delete at beginning-of-block case. // Do nothing in the case that the caret is at the start of a // root editable element or at the start of a document. OwnPtrWillBeRawPtr<FrameSelection> selection = FrameSelection::create(); selection->setSelection(endingSelection()); selection->modify(FrameSelection::AlterationExtend, DirectionForward, granularity); if (killRing && selection->isCaret() && granularity != CharacterGranularity) selection->modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity); Position downstreamEnd = endingSelection().end().downstream(); VisiblePosition visibleEnd = endingSelection().visibleEnd(); Node* enclosingTableCell = enclosingNodeOfType(visibleEnd.deepEquivalent(), &isTableCell); if (enclosingTableCell && visibleEnd == VisiblePosition(lastPositionInNode(enclosingTableCell))) return; if (visibleEnd == endOfParagraph(visibleEnd)) downstreamEnd = visibleEnd.next(CannotCrossEditingBoundary).deepEquivalent().downstream(); // When deleting tables: Select the table first, then perform the deletion if (isRenderedTableElement(downstreamEnd.containerNode()) && downstreamEnd.computeOffsetInContainerNode() <= caretMinOffset(downstreamEnd.containerNode())) { setEndingSelection(VisibleSelection(endingSelection().end(), positionAfterNode(downstreamEnd.containerNode()), DOWNSTREAM, endingSelection().isDirectional())); typingAddedToOpenCommand(ForwardDeleteKey); return; } // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any) if (granularity == ParagraphBoundary && selection->selection().isCaret() && isEndOfParagraph(selection->selection().visibleEnd())) selection->modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity); selectionToDelete = selection->selection(); if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start()) selectionAfterUndo = selectionToDelete; else { // It's a little tricky to compute what the starting selection would have been in the original document. // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on // the current state of the document and we'll get the wrong result. Position extent = startingSelection().end(); if (extent.containerNode() != selectionToDelete.end().containerNode()) extent = selectionToDelete.extent(); else { int extraCharacters; if (selectionToDelete.start().containerNode() == selectionToDelete.end().containerNode()) extraCharacters = selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode(); else extraCharacters = selectionToDelete.end().computeOffsetInContainerNode(); extent = Position(extent.containerNode(), extent.computeOffsetInContainerNode() + extraCharacters, Position::PositionIsOffsetInAnchor); } selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent); } break; } case NoSelection: ASSERT_NOT_REACHED(); break; } ASSERT(!selectionToDelete.isNone()); if (selectionToDelete.isNone()) return; if (selectionToDelete.isCaret()) return; if (killRing) frame->editor().addToKillRing(selectionToDelete.toNormalizedRange().get(), false); // Make undo select what was deleted on Mac alone if (frame->editor().behavior().shouldUndoOfDeleteSelectText()) setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); typingAddedToOpenCommand(ForwardDeleteKey); }
void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool shouldAddToKillRing) { Frame& frame = this->frame(); frame.editor().updateMarkersForWordsAffectedByEditing(false); VisibleSelection selectionToDelete; VisibleSelection selectionAfterUndo; switch (endingSelection().selectionType()) { case VisibleSelection::RangeSelection: selectionToDelete = endingSelection(); selectionAfterUndo = selectionToDelete; break; case VisibleSelection::CaretSelection: { m_smartDelete = false; // Handle delete at beginning-of-block case. // Do nothing in the case that the caret is at the start of a // root editable element or at the start of a document. FrameSelection selection; selection.setSelection(endingSelection()); selection.modify(FrameSelection::AlterationExtend, DirectionForward, granularity); if (shouldAddToKillRing && selection.isCaret() && granularity != CharacterGranularity) selection.modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity); Position downstreamEnd = endingSelection().end().downstream(); VisiblePosition visibleEnd = endingSelection().visibleEnd(); Node* enclosingTableCell = enclosingNodeOfType(visibleEnd.deepEquivalent(), &isTableCell); if (enclosingTableCell && visibleEnd == lastPositionInNode(enclosingTableCell)) return; if (visibleEnd == endOfParagraph(visibleEnd)) downstreamEnd = visibleEnd.next(CannotCrossEditingBoundary).deepEquivalent().downstream(); // When deleting tables: Select the table first, then perform the deletion if (downstreamEnd.containerNode() && downstreamEnd.containerNode()->renderer() && downstreamEnd.containerNode()->renderer()->isTable() && downstreamEnd.computeOffsetInContainerNode() <= caretMinOffset(downstreamEnd.containerNode())) { setEndingSelection(VisibleSelection(endingSelection().end(), positionAfterNode(downstreamEnd.containerNode()), DOWNSTREAM, endingSelection().isDirectional())); typingAddedToOpenCommand(ForwardDeleteKey); return; } // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any) if (granularity == ParagraphBoundary && selection.selection().isCaret() && isEndOfParagraph(selection.selection().visibleEnd())) selection.modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity); selectionToDelete = selection.selection(); if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start()) selectionAfterUndo = selectionToDelete; else { // It's a little tricky to compute what the starting selection would have been in the original document. // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on // the current state of the document and we'll get the wrong result. Position extent = startingSelection().end(); if (extent.containerNode() != selectionToDelete.end().containerNode()) extent = selectionToDelete.extent(); else { int extraCharacters; if (selectionToDelete.start().containerNode() == selectionToDelete.end().containerNode()) extraCharacters = selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode(); else extraCharacters = selectionToDelete.end().computeOffsetInContainerNode(); extent = Position(extent.containerNode(), extent.computeOffsetInContainerNode() + extraCharacters, Position::PositionIsOffsetInAnchor); } selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent); } break; } case VisibleSelection::NoSelection: ASSERT_NOT_REACHED(); break; } ASSERT(!selectionToDelete.isNone()); if (selectionToDelete.isNone()) { #if PLATFORM(IOS) // Workaround for this bug: // <rdar://problem/4653755> UIKit text widgets should use WebKit editing API to manipulate text setEndingSelection(frame.selection().selection()); closeTyping(&frame); #endif return; } if (selectionToDelete.isCaret() || !frame.selection().shouldDeleteSelection(selectionToDelete)) return; if (shouldAddToKillRing) frame.editor().addRangeToKillRing(*selectionToDelete.toNormalizedRange().get(), Editor::KillRingInsertionMode::AppendText); // make undo select what was deleted setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); typingAddedToOpenCommand(ForwardDeleteKey); }
void DeleteSelectionCommand::initializePositionData() { Position start, end; initializeStartEnd(start, end); m_upstreamStart = start.upstream(); m_downstreamStart = start.downstream(); m_upstreamEnd = end.upstream(); m_downstreamEnd = end.downstream(); m_startRoot = editableRootForPosition(start); m_endRoot = editableRootForPosition(end); m_startTableRow = enclosingNodeOfType(start, &isTableRow); m_endTableRow = enclosingNodeOfType(end, &isTableRow); // Don't move content out of a table cell. // If the cell is non-editable, enclosingNodeOfType won't return it by default, so // tell that function that we don't care if it returns non-editable nodes. Node* startCell = enclosingNodeOfType(m_upstreamStart, &isTableCell, CanCrossEditingBoundary); Node* endCell = enclosingNodeOfType(m_downstreamEnd, &isTableCell, CanCrossEditingBoundary); // FIXME: This isn't right. A borderless table with two rows and a single column would appear as two paragraphs. if (endCell && endCell != startCell) m_mergeBlocksAfterDelete = false; // Usually the start and the end of the selection to delete are pulled together as a result of the deletion. // Sometimes they aren't (like when no merge is requested), so we must choose one position to hold the caret // and receive the placeholder after deletion. VisiblePosition visibleEnd(m_downstreamEnd); if (m_mergeBlocksAfterDelete && !isEndOfParagraph(visibleEnd)) m_endingPosition = m_downstreamEnd; else m_endingPosition = m_downstreamStart; // We don't want to merge into a block if it will mean changing the quote level of content after deleting // selections that contain a whole number paragraphs plus a line break, since it is unclear to most users // that such a selection actually ends at the start of the next paragraph. This matches TextEdit behavior // for indented paragraphs. // Only apply this rule if the endingSelection is a range selection. If it is a caret, then other operations have created // the selection we're deleting (like the process of creating a selection to delete during a backspace), and the user isn't in the situation described above. if (numEnclosingMailBlockquotes(start) != numEnclosingMailBlockquotes(end) && isStartOfParagraph(visibleEnd) && isStartOfParagraph(VisiblePosition(start)) && endingSelection().isRange()) { m_mergeBlocksAfterDelete = false; m_pruneStartBlockIfNecessary = true; } // Handle leading and trailing whitespace, as well as smart delete adjustments to the selection m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.affinity()); m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY); if (m_smartDelete) { // skip smart delete if the selection to delete already starts or ends with whitespace Position pos = VisiblePosition(m_upstreamStart, m_selectionToDelete.affinity()).deepEquivalent(); bool skipSmartDelete = pos.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull(); if (!skipSmartDelete) skipSmartDelete = m_downstreamEnd.leadingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull(); // extend selection upstream if there is whitespace there bool hasLeadingWhitespaceBeforeAdjustment = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.affinity(), true).isNotNull(); if (!skipSmartDelete && hasLeadingWhitespaceBeforeAdjustment) { VisiblePosition visiblePos = VisiblePosition(m_upstreamStart, VP_DEFAULT_AFFINITY).previous(); pos = visiblePos.deepEquivalent(); // Expand out one character upstream for smart delete and recalculate // positions based on this change. m_upstreamStart = pos.upstream(); m_downstreamStart = pos.downstream(); m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(visiblePos.affinity()); setStartingSelectionOnSmartDelete(m_upstreamStart, m_upstreamEnd); } // trailing whitespace is only considered for smart delete if there is no leading // whitespace, as in the case where you double-click the first word of a paragraph. if (!skipSmartDelete && !hasLeadingWhitespaceBeforeAdjustment && m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull()) { // Expand out one character downstream for smart delete and recalculate // positions based on this change. pos = VisiblePosition(m_downstreamEnd, VP_DEFAULT_AFFINITY).next().deepEquivalent(); m_upstreamEnd = pos.upstream(); m_downstreamEnd = pos.downstream(); m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY); setStartingSelectionOnSmartDelete(m_downstreamStart, m_downstreamEnd); } } // We must pass call parentAnchoredEquivalent on the positions since some editing positions // that appear inside their nodes aren't really inside them. [hr, 0] is one example. // FIXME: parentAnchoredEquivalent should eventually be moved into enclosing element getters // like the one below, since editing functions should obviously accept editing positions. // FIXME: Passing false to enclosingNodeOfType tells it that it's OK to return a non-editable // node. This was done to match existing behavior, but it seems wrong. m_startBlock = enclosingNodeOfType(m_downstreamStart.parentAnchoredEquivalent(), &isBlock, CanCrossEditingBoundary); m_endBlock = enclosingNodeOfType(m_upstreamEnd.parentAnchoredEquivalent(), &isBlock, CanCrossEditingBoundary); }
int comparePositions(const VisiblePosition& a, const VisiblePosition& b) { return comparePositions(a.deepEquivalent(), b.deepEquivalent()); }
void InsertParagraphSeparatorCommand::doApply() { if (!endingSelection().isNonOrphanedCaretOrRange()) return; Position insertionPosition = endingSelection().start(); EAffinity affinity = endingSelection().affinity(); // Delete the current selection. if (endingSelection().isRange()) { calculateStyleBeforeInsertion(insertionPosition); deleteSelection(false, true); insertionPosition = endingSelection().start(); affinity = endingSelection().affinity(); } // FIXME: The parentAnchoredEquivalent conversion needs to be moved into enclosingBlock. RefPtr<Element> startBlock = enclosingBlock(insertionPosition.parentAnchoredEquivalent().containerNode()); Position canonicalPos = VisiblePosition(insertionPosition).deepEquivalent(); if (!startBlock || !startBlock->nonShadowBoundaryParentNode() || isTableCell(startBlock.get()) || isHTMLFormElement(startBlock.get()) // FIXME: If the node is hidden, we don't have a canonical position so we will do the wrong thing for tables and <hr>. https://bugs.webkit.org/show_bug.cgi?id=40342 || (!canonicalPos.isNull() && canonicalPos.deprecatedNode()->renderer() && canonicalPos.deprecatedNode()->renderer()->isTable()) || (!canonicalPos.isNull() && canonicalPos.deprecatedNode()->hasTagName(hrTag))) { applyCommandToComposite(InsertLineBreakCommand::create(document())); return; } // Use the leftmost candidate. insertionPosition = insertionPosition.upstream(); if (!insertionPosition.isCandidate()) insertionPosition = insertionPosition.downstream(); // Adjust the insertion position after the delete insertionPosition = positionAvoidingSpecialElementBoundary(insertionPosition); VisiblePosition visiblePos(insertionPosition, affinity); calculateStyleBeforeInsertion(insertionPosition); //--------------------------------------------------------------------- // Handle special case of typing return on an empty list item if (breakOutOfEmptyListItem()) return; //--------------------------------------------------------------------- // Prepare for more general cases. bool isFirstInBlock = isStartOfBlock(visiblePos); bool isLastInBlock = isEndOfBlock(visiblePos); bool nestNewBlock = false; // Create block to be inserted. RefPtr<Element> blockToInsert; if (startBlock->isRootEditableElement()) { blockToInsert = createDefaultParagraphElement(document()); nestNewBlock = true; } else if (shouldUseDefaultParagraphElement(startBlock.get())) blockToInsert = createDefaultParagraphElement(document()); else blockToInsert = startBlock->cloneElementWithoutChildren(); //--------------------------------------------------------------------- // Handle case when position is in the last visible position in its block, // including when the block is empty. if (isLastInBlock) { if (nestNewBlock) { if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) { // The block is empty. Create an empty block to // represent the paragraph that we're leaving. RefPtr<Element> extraBlock = createDefaultParagraphElement(document()); appendNode(extraBlock, startBlock); appendBlockPlaceholder(extraBlock); } appendNode(blockToInsert, startBlock); } else { // We can get here if we pasted a copied portion of a blockquote with a newline at the end and are trying to paste it // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted. if (m_pasteBlockqutoeIntoUnquotedArea) { if (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) startBlock = toElement(highestBlockquote); } // Most of the time we want to stay at the nesting level of the startBlock (e.g., when nesting within lists). However, // for div nodes, this can result in nested div tags that are hard to break out of. Element* siblingNode = startBlock.get(); if (blockToInsert->hasTagName(divTag)) siblingNode = highestVisuallyEquivalentDivBelowRoot(startBlock.get()); insertNodeAfter(blockToInsert, siblingNode); } // Recreate the same structure in the new paragraph. Vector<RefPtr<Element>> ancestors; getAncestorsInsideBlock(positionOutsideTabSpan(insertionPosition).deprecatedNode(), startBlock.get(), ancestors); RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); appendBlockPlaceholder(parent); setEndingSelection(VisibleSelection(firstPositionInNode(parent.get()), DOWNSTREAM, endingSelection().isDirectional())); return; } //--------------------------------------------------------------------- // Handle case when position is in the first visible position in its block, and // similar case where previous position is in another, presumeably nested, block. if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) { Node *refNode; insertionPosition = positionOutsideTabSpan(insertionPosition); if (isFirstInBlock && !nestNewBlock) refNode = startBlock.get(); else if (isFirstInBlock && nestNewBlock) { // startBlock should always have children, otherwise isLastInBlock would be true and it's handled above. ASSERT(startBlock->firstChild()); refNode = startBlock->firstChild(); } else if (insertionPosition.deprecatedNode() == startBlock && nestNewBlock) { refNode = startBlock->traverseToChildAt(insertionPosition.deprecatedEditingOffset()); ASSERT(refNode); // must be true or we'd be in the end of block case } else refNode = insertionPosition.deprecatedNode(); // find ending selection position easily before inserting the paragraph insertionPosition = insertionPosition.downstream(); insertNodeBefore(blockToInsert, refNode); // Recreate the same structure in the new paragraph. Vector<RefPtr<Element>> ancestors; getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(positionOutsideTabSpan(insertionPosition)).deprecatedNode(), startBlock.get(), ancestors); appendBlockPlaceholder(cloneHierarchyUnderNewBlock(ancestors, blockToInsert)); // In this case, we need to set the new ending selection. setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM, endingSelection().isDirectional())); return; } //--------------------------------------------------------------------- // Handle the (more complicated) general case, // All of the content in the current block after visiblePos is // about to be wrapped in a new paragraph element. Add a br before // it if visiblePos is at the start of a paragraph so that the // content will move down a line. if (isStartOfParagraph(visiblePos)) { RefPtr<Element> br = createBreakElement(document()); insertNodeAt(br.get(), insertionPosition); insertionPosition = positionInParentAfterNode(br.get()); // If the insertion point is a break element, there is nothing else // we need to do. if (visiblePos.deepEquivalent().anchorNode()->renderer()->isBR()) { setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM, endingSelection().isDirectional())); return; } } // Move downstream. Typing style code will take care of carrying along the // style of the upstream position. insertionPosition = insertionPosition.downstream(); // At this point, the insertionPosition's node could be a container, and we want to make sure we include // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position // before we walk the DOM tree. insertionPosition = positionOutsideTabSpan(VisiblePosition(insertionPosition).deepEquivalent()); // If the returned position lies either at the end or at the start of an element that is ignored by editing // we should move to its upstream or downstream position. if (editingIgnoresContent(insertionPosition.deprecatedNode())) { if (insertionPosition.atLastEditingPositionForNode()) insertionPosition = insertionPosition.downstream(); else if (insertionPosition.atFirstEditingPositionForNode()) insertionPosition = insertionPosition.upstream(); } // Make sure we do not cause a rendered space to become unrendered. // FIXME: We need the affinity for pos, but pos.downstream() does not give it Position leadingWhitespace = insertionPosition.leadingWhitespacePosition(VP_DEFAULT_AFFINITY); // FIXME: leadingWhitespacePosition is returning the position before preserved newlines for positions // after the preserved newline, causing the newline to be turned into a nbsp. if (leadingWhitespace.isNotNull() && leadingWhitespace.deprecatedNode()->isTextNode()) { Text* textNode = toText(leadingWhitespace.deprecatedNode()); ASSERT(!textNode->renderer() || textNode->renderer()->style().collapseWhiteSpace()); replaceTextInNodePreservingMarkers(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); } // Split at pos if in the middle of a text node. Position positionAfterSplit; if (insertionPosition.anchorType() == Position::PositionIsOffsetInAnchor && insertionPosition.containerNode()->isTextNode()) { RefPtr<Text> textNode = toText(insertionPosition.containerNode()); bool atEnd = static_cast<unsigned>(insertionPosition.offsetInContainerNode()) >= textNode->length(); if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) { splitTextNode(textNode, insertionPosition.offsetInContainerNode()); positionAfterSplit = firstPositionInNode(textNode.get()); if (!textNode->previousSibling()) return; // Bail out if mutation events detachd the split text node. insertionPosition.moveToPosition(textNode->previousSibling(), insertionPosition.offsetInContainerNode()); visiblePos = VisiblePosition(insertionPosition); } } // If we got detached due to mutation events, just bail out. if (!startBlock->parentNode()) return; // Put the added block in the tree. if (nestNewBlock) appendNode(blockToInsert.get(), startBlock); else insertNodeAfter(blockToInsert.get(), startBlock); document().updateLayoutIgnorePendingStylesheets(); // If the paragraph separator was inserted at the end of a paragraph, an empty line must be // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph // element. If the first node to be inserted won't be one that will hold an empty line open, add a br. if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) appendNode(createBreakElement(document()).get(), blockToInsert.get()); // Move the start node and the siblings of the start node. if (VisiblePosition(insertionPosition) != VisiblePosition(positionBeforeNode(blockToInsert.get()))) { Node* n; if (insertionPosition.containerNode() == startBlock) n = insertionPosition.computeNodeAfterPosition(); else { Node* splitTo = insertionPosition.containerNode(); if (splitTo->isTextNode() && insertionPosition.offsetInContainerNode() >= caretMaxOffset(splitTo)) splitTo = NodeTraversal::next(splitTo, startBlock.get()); ASSERT(splitTo); splitTreeToNode(splitTo, startBlock.get()); for (n = startBlock->firstChild(); n; n = n->nextSibling()) { VisiblePosition beforeNodePosition = positionBeforeNode(n); if (!beforeNodePosition.isNull() && comparePositions(VisiblePosition(insertionPosition), beforeNodePosition) <= 0) break; } } moveRemainingSiblingsToNewParent(n, blockToInsert.get(), blockToInsert); } // Handle whitespace that occurs after the split if (positionAfterSplit.isNotNull()) { document().updateLayoutIgnorePendingStylesheets(); if (!positionAfterSplit.isRenderedCharacter()) { // Clear out all whitespace and insert one non-breaking space ASSERT(!positionAfterSplit.containerNode()->renderer() || positionAfterSplit.containerNode()->renderer()->style().collapseWhiteSpace()); deleteInsignificantTextDownstream(positionAfterSplit); if (positionAfterSplit.deprecatedNode()->isTextNode()) insertTextIntoNode(toText(positionAfterSplit.containerNode()), 0, nonBreakingSpaceString()); } } setEndingSelection(VisibleSelection(firstPositionInNode(blockToInsert.get()), DOWNSTREAM, endingSelection().isDirectional())); applyStyleAfterInsertion(startBlock.get()); }
bool lineBreakExistsAtVisiblePosition(const VisiblePosition& visiblePosition) { return lineBreakExistsAtPosition(visiblePosition.deepEquivalent().downstream()); }
void InsertListCommand::doApply() { if (endingSelection().isNone()) return; if (!endingSelection().rootEditableElement()) return; VisiblePosition visibleEnd = endingSelection().visibleEnd(); VisiblePosition visibleStart = endingSelection().visibleStart(); // When a selection ends at the start of a paragraph, we rarely paint // the selection gap before that paragraph, because there often is no gap. // In a case like this, it's not obvious to the user that the selection // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List // operated on that paragraph. // FIXME: We paint the gap before some paragraphs that are indented with left // margin/padding, but not others. We should make the gap painting more consistent and // then use a left margin/padding rule here. if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true))); if (endingSelection().isRange() && modifyRange()) return; // FIXME: This will produce unexpected results for a selection that starts just before a // table and ends inside the first cell, selectionForParagraphIteration should probably // be renamed and deployed inside setEndingSelection(). Node* selectionNode = endingSelection().start().node(); const QualifiedName listTag = (m_type == OrderedList) ? olTag : ulTag; Node* listChildNode = enclosingListChild(selectionNode); bool switchListType = false; if (listChildNode) { // Remove the list chlild. HTMLElement* listNode = enclosingList(listChildNode); if (!listNode) listNode = fixOrphanedListChild(listChildNode); if (!listNode->hasTagName(listTag)) // listChildNode will be removed from the list and a list of type m_type will be created. switchListType = true; Node* nextListChild; Node* previousListChild; VisiblePosition start; VisiblePosition end; if (listChildNode->hasTagName(liTag)) { start = firstDeepEditingPositionForNode(listChildNode); end = lastDeepEditingPositionForNode(listChildNode); nextListChild = listChildNode->nextSibling(); previousListChild = listChildNode->previousSibling(); } else { // A paragraph is visually a list item minus a list marker. The paragraph will be moved. start = startOfParagraph(endingSelection().visibleStart()); end = endOfParagraph(endingSelection().visibleEnd()); nextListChild = enclosingListChild(end.next().deepEquivalent().node()); ASSERT(nextListChild != listChildNode); if (enclosingList(nextListChild) != listNode) nextListChild = 0; previousListChild = enclosingListChild(start.previous().deepEquivalent().node()); ASSERT(previousListChild != listChildNode); if (enclosingList(previousListChild) != listNode) previousListChild = 0; } // When removing a list, we must always create a placeholder to act as a point of insertion // for the list content being removed. RefPtr<Element> placeholder = createBreakElement(document()); RefPtr<Element> nodeToInsert = placeholder; // If the content of the list item will be moved into another list, put it in a list item // so that we don't create an orphaned list child. if (enclosingList(listNode)) { nodeToInsert = createListItemElement(document()); appendNode(placeholder, nodeToInsert); } if (nextListChild && previousListChild) { // We want to pull listChildNode out of listNode, and place it before nextListChild // and after previousListChild, so we split listNode and insert it between the two lists. // But to split listNode, we must first split ancestors of listChildNode between it and listNode, // if any exist. // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is // unrendered. But we ought to remove nextListChild too, if it is unrendered. splitElement(listNode, splitTreeToNode(nextListChild, listNode)); insertNodeBefore(nodeToInsert, listNode); } else if (nextListChild || listChildNode->parentNode() != listNode) { // Just because listChildNode has no previousListChild doesn't mean there isn't any content // in listNode that comes before listChildNode, as listChildNode could have ancestors // between it and listNode. So, we split up to listNode before inserting the placeholder // where we're about to move listChildNode to. if (listChildNode->parentNode() != listNode) splitElement(listNode, splitTreeToNode(listChildNode, listNode).get()); insertNodeBefore(nodeToInsert, listNode); } else insertNodeAfter(nodeToInsert, listNode); VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0)); moveParagraphs(start, end, insertionPoint, true); } if (!listChildNode || switchListType || m_forceCreateList) { // Create list. VisiblePosition start = startOfParagraph(endingSelection().visibleStart()); VisiblePosition end = endOfParagraph(endingSelection().visibleEnd()); // Check for adjoining lists. VisiblePosition previousPosition = start.previous(true); VisiblePosition nextPosition = end.next(true); RefPtr<HTMLElement> listItemElement = createListItemElement(document()); RefPtr<HTMLElement> placeholder = createBreakElement(document()); appendNode(placeholder, listItemElement); Element* previousList = outermostEnclosingList(previousPosition.deepEquivalent().node()); Element* nextList = outermostEnclosingList(nextPosition.deepEquivalent().node()); Node* startNode = start.deepEquivalent().node(); Node* previousCell = enclosingTableCell(previousPosition.deepEquivalent()); Node* nextCell = enclosingTableCell(nextPosition.deepEquivalent()); Node* currentCell = enclosingTableCell(start.deepEquivalent()); if (previousList && (!previousList->hasTagName(listTag) || startNode->isDescendantOf(previousList) || previousCell != currentCell)) previousList = 0; if (nextList && (!nextList->hasTagName(listTag) || startNode->isDescendantOf(nextList) || nextCell != currentCell)) nextList = 0; // Place list item into adjoining lists. if (previousList) appendNode(listItemElement, previousList); else if (nextList) insertNodeAt(listItemElement, Position(nextList, 0)); else { // Create the list. RefPtr<HTMLElement> listElement = m_type == OrderedList ? createOrderedListElement(document()) : createUnorderedListElement(document()); m_listElement = listElement; appendNode(listItemElement, listElement); if (start == end && isBlock(start.deepEquivalent().node())) { // Inserting the list into an empty paragraph that isn't held open // by a br or a '\n', will invalidate start and end. Insert // a placeholder and then recompute start and end. RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent()); start = VisiblePosition(Position(placeholder.get(), 0)); end = start; } // Insert the list at a position visually equivalent to start of the // paragraph that is being moved into the list. // Try to avoid inserting it somewhere where it will be surrounded by // inline ancestors of start, since it is easier for editing to produce // clean markup when inline elements are pushed down as far as possible. Position insertionPos(start.deepEquivalent().upstream()); // Also avoid the containing list item. Node* listChild = enclosingListChild(insertionPos.node()); if (listChild && listChild->hasTagName(liTag)) insertionPos = positionBeforeNode(listChild); insertNodeAt(listElement, insertionPos); } moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true); if (nextList && previousList) mergeIdenticalElements(static_cast<Element*>(previousList), static_cast<Element*>(nextList)); } }
VisibleSelection DirectionGranularityStrategy::updateExtent( const IntPoint& extentPoint, LocalFrame* frame) { const VisibleSelection& selection = frame->selection().selection(); if (m_state == StrategyState::Cleared) m_state = StrategyState::Expanding; VisiblePosition oldOffsetExtentPosition = selection.visibleExtent(); IntPoint oldExtentLocation = positionLocation(oldOffsetExtentPosition); IntPoint oldOffsetExtentPoint = oldExtentLocation + m_diffExtentPointFromExtentPosition; IntPoint oldExtentPoint = IntPoint(oldOffsetExtentPoint.x() - m_offset, oldOffsetExtentPoint.y()); // Apply the offset. IntPoint newOffsetExtentPoint = extentPoint; int dx = extentPoint.x() - oldExtentPoint.x(); if (m_offset != 0) { if (m_offset > 0 && dx > 0) m_offset = std::max(0, m_offset - dx); else if (m_offset < 0 && dx < 0) m_offset = std::min(0, m_offset - dx); newOffsetExtentPoint.move(m_offset, 0); } VisiblePosition newOffsetExtentPosition = visiblePositionForContentsPoint(newOffsetExtentPoint, frame); IntPoint newOffsetLocation = positionLocation(newOffsetExtentPosition); // Reset the offset in case of a vertical change in the location (could be // due to a line change or due to an unusual layout, e.g. rotated text). bool verticalChange = newOffsetLocation.y() != oldExtentLocation.y(); if (verticalChange) { m_offset = 0; m_granularity = CharacterGranularity; newOffsetExtentPoint = extentPoint; newOffsetExtentPosition = visiblePositionForContentsPoint(extentPoint, frame); } const VisiblePosition base = selection.visibleBase(); // Do not allow empty selection. if (newOffsetExtentPosition.deepEquivalent() == base.deepEquivalent()) return selection; // The direction granularity strategy, particularly the "offset" feature // doesn't work with non-horizontal text (e.g. when the text is rotated). // So revert to the behavior equivalent to the character granularity // strategy if we detect that the text's baseline coordinate changed // without a line change. if (verticalChange && inSameLine(newOffsetExtentPosition, oldOffsetExtentPosition)) { return createVisibleSelection(selection.visibleBase(), newOffsetExtentPosition); } int oldExtentBaseOrder = selection.isBaseFirst() ? 1 : -1; int newExtentBaseOrder; bool thisMoveShrunkSelection; if (newOffsetExtentPosition.deepEquivalent() == oldOffsetExtentPosition.deepEquivalent()) { if (m_granularity == CharacterGranularity) return selection; // If we are in Word granularity, we cannot exit here, since we may pass // the middle of the word without changing the position (in which case // the selection needs to expand). thisMoveShrunkSelection = false; newExtentBaseOrder = oldExtentBaseOrder; } else { bool selectionExpanded = arePositionsInSpecifiedOrder( newOffsetExtentPosition, oldOffsetExtentPosition, oldExtentBaseOrder); bool extentBaseOrderSwitched = selectionExpanded ? false : !arePositionsInSpecifiedOrder(newOffsetExtentPosition, base, oldExtentBaseOrder); newExtentBaseOrder = extentBaseOrderSwitched ? -oldExtentBaseOrder : oldExtentBaseOrder; // Determine the word boundary, i.e. the boundary extending beyond which // should change the granularity to WordGranularity. VisiblePosition wordBoundary; if (extentBaseOrderSwitched) { // Special case. // If the extent-base order was switched, then the selection is now // expanding in a different direction than before. Therefore we // calculate the word boundary in this new direction and based on // the |base| position. wordBoundary = nextWordBound(base, newExtentBaseOrder > 0 ? SearchDirection::SearchForward : SearchDirection::SearchBackwards, BoundAdjust::NextBoundIfOnBound); m_granularity = CharacterGranularity; } else { // Calculate the word boundary based on |oldExtentWithGranularity|. // If selection was shrunk in the last update and the extent is now // exactly on the word boundary - we need to take the next bound as // the bound of the current word. wordBoundary = nextWordBound(oldOffsetExtentPosition, oldExtentBaseOrder > 0 ? SearchDirection::SearchForward : SearchDirection::SearchBackwards, m_state == StrategyState::Shrinking ? BoundAdjust::NextBoundIfOnBound : BoundAdjust::CurrentPosIfOnBound); } bool expandedBeyondWordBoundary; if (selectionExpanded) expandedBeyondWordBoundary = arePositionsInSpecifiedOrder( newOffsetExtentPosition, wordBoundary, newExtentBaseOrder); else if (extentBaseOrderSwitched) expandedBeyondWordBoundary = arePositionsInSpecifiedOrder( newOffsetExtentPosition, wordBoundary, newExtentBaseOrder); else expandedBeyondWordBoundary = false; // The selection is shrunk if the extent changes position to be closer to // the base, and the extent/base order wasn't switched. thisMoveShrunkSelection = !extentBaseOrderSwitched && !selectionExpanded; if (expandedBeyondWordBoundary) m_granularity = WordGranularity; else if (thisMoveShrunkSelection) m_granularity = CharacterGranularity; } VisiblePosition newSelectionExtent = newOffsetExtentPosition; if (m_granularity == WordGranularity) { // Determine the bounds of the word where the extent is located. // Set the selection extent to one of the two bounds depending on // whether the extent is passed the middle of the word. VisiblePosition boundBeforeExtent = nextWordBound(newOffsetExtentPosition, SearchDirection::SearchBackwards, BoundAdjust::CurrentPosIfOnBound); VisiblePosition boundAfterExtent = nextWordBound(newOffsetExtentPosition, SearchDirection::SearchForward, BoundAdjust::CurrentPosIfOnBound); int xMiddleBetweenBounds = (positionLocation(boundAfterExtent).x() + positionLocation(boundBeforeExtent).x()) / 2; bool offsetExtentBeforeMiddle = newOffsetExtentPoint.x() < xMiddleBetweenBounds; newSelectionExtent = offsetExtentBeforeMiddle ? boundBeforeExtent : boundAfterExtent; // Update the offset if selection expanded in word granularity. if (newSelectionExtent.deepEquivalent() != selection.visibleExtent().deepEquivalent() && ((newExtentBaseOrder > 0 && !offsetExtentBeforeMiddle) || (newExtentBaseOrder < 0 && offsetExtentBeforeMiddle))) { m_offset = positionLocation(newSelectionExtent).x() - extentPoint.x(); } } // Only update the state if the selection actually changed as a result of // this move. if (newSelectionExtent.deepEquivalent() != selection.visibleExtent().deepEquivalent()) m_state = thisMoveShrunkSelection ? StrategyState::Shrinking : StrategyState::Expanding; m_diffExtentPointFromExtentPosition = extentPoint + IntSize(m_offset, 0) - positionLocation(newSelectionExtent); VisibleSelection newSelection = selection; newSelection.setExtent(newSelectionExtent); return newSelection; }
void Selection::setExtent(const VisiblePosition& visiblePosition) { m_extent = visiblePosition.deepEquivalent(); validate(); }
void IndentOutdentCommand::outdentParagraph() { VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart()); VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph); Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote); if (!enclosingNode || !enclosingNode->parentNode()->isContentEditable()) // We can't outdent if there is no place to go! return; // Use InsertListCommand to remove the selection from the list if (enclosingNode->hasTagName(olTag)) { applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList)); return; } if (enclosingNode->hasTagName(ulTag)) { applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList)); return; } // The selection is inside a blockquote i.e. enclosingNode is a blockquote VisiblePosition positionInEnclosingBlock = VisiblePosition(firstPositionInNode(enclosingNode)); // If the blockquote is inline, the start of the enclosing block coincides with // positionInEnclosingBlock. VisiblePosition startOfEnclosingBlock = (enclosingNode->renderer() && enclosingNode->renderer()->isInline()) ? positionInEnclosingBlock : startOfBlock(positionInEnclosingBlock); VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(lastPositionInNode(enclosingNode)); VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock); if (visibleStartOfParagraph == startOfEnclosingBlock && visibleEndOfParagraph == endOfEnclosingBlock) { // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed. Node* splitPoint = enclosingNode->nextSibling(); removeNodePreservingChildren(enclosingNode); // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true if (splitPoint) { if (ContainerNode* splitPointParent = splitPoint->parentNode()) { if (splitPointParent->hasTagName(blockquoteTag) && !splitPoint->hasTagName(blockquoteTag) && splitPointParent->parentNode()->isContentEditable()) // We can't outdent if there is no place to go! splitElement(static_cast<Element*>(splitPointParent), splitPoint); } } updateLayout(); visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent()); visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent()); if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph)) insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent()); if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph)) insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent()); return; } Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().node()); RefPtr<Node> splitBlockquoteNode = enclosingNode; if (enclosingBlockFlow != enclosingNode) splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true); else { // We split the blockquote at where we start outdenting. splitElement(static_cast<Element*>(enclosingNode), visibleStartOfParagraph.deepEquivalent().node()); } RefPtr<Node> placeholder = createBreakElement(document()); insertNodeBefore(placeholder, splitBlockquoteNode); moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), positionBeforeNode(placeholder.get()), true); }
void Selection::adjustForEditableContent() { if (m_base.isNull() || m_start.isNull() || m_end.isNull()) return; Node* baseRoot = highestEditableRoot(m_base); Node* startRoot = highestEditableRoot(m_start); Node* endRoot = highestEditableRoot(m_end); Node* baseEditableAncestor = lowestEditableAncestor(m_base.node()); // The base, start and end are all in the same region. No adjustment necessary. if (baseRoot == startRoot && baseRoot == endRoot) return; // The selection is based in editable content. if (baseRoot) { // If the start is outside the base's editable root, cap it at the start of that root. // If the start is in non-editable content that is inside the base's editable root, put it // at the first editable position after start inside the base's editable root. if (startRoot != baseRoot) { VisiblePosition first = firstEditablePositionAfterPositionInRoot(m_start, baseRoot); m_start = first.deepEquivalent(); if (m_start.isNull()) { ASSERT_NOT_REACHED(); m_start = m_end; } } // If the end is outside the base's editable root, cap it at the end of that root. // If the end is in non-editable content that is inside the base's root, put it // at the last editable position before the end inside the base's root. if (endRoot != baseRoot) { VisiblePosition last = lastEditablePositionBeforePositionInRoot(m_end, baseRoot); m_end = last.deepEquivalent(); if (m_end.isNull()) { ASSERT_NOT_REACHED(); m_end = m_start; } } // The selection is based in non-editable content. } else { // FIXME: Non-editable pieces inside editable content should be atomic, in the same way that editable // pieces in non-editable content are atomic. // The selection ends in editable content or non-editable content inside a different editable ancestor, // move backward until non-editable content inside the same lowest editable ancestor is reached. Node* endEditableAncestor = lowestEditableAncestor(m_end.node()); if (endRoot || endEditableAncestor != baseEditableAncestor) { Position p = previousVisuallyDistinctCandidate(m_end); Node* shadowAncestor = endRoot ? endRoot->shadowAncestorNode() : 0; if (p.isNull() && endRoot && (shadowAncestor != endRoot)) p = Position(shadowAncestor, maxDeepOffset(shadowAncestor)); while (p.isNotNull() && !(lowestEditableAncestor(p.node()) == baseEditableAncestor && !isEditablePosition(p))) { Node* root = editableRootForPosition(p); shadowAncestor = root ? root->shadowAncestorNode() : 0; p = isAtomicNode(p.node()) ? positionBeforeNode(p.node()) : previousVisuallyDistinctCandidate(p); if (p.isNull() && (shadowAncestor != root)) p = Position(shadowAncestor, maxDeepOffset(shadowAncestor)); } VisiblePosition previous(p); if (previous.isNull()) { ASSERT_NOT_REACHED(); m_base = Position(); m_extent = Position(); validate(); return; } m_end = previous.deepEquivalent(); } // The selection starts in editable content or non-editable content inside a different editable ancestor, // move forward until non-editable content inside the same lowest editable ancestor is reached. Node* startEditableAncestor = lowestEditableAncestor(m_start.node()); if (startRoot || startEditableAncestor != baseEditableAncestor) { Position p = nextVisuallyDistinctCandidate(m_start); Node* shadowAncestor = startRoot ? startRoot->shadowAncestorNode() : 0; if (p.isNull() && startRoot && (shadowAncestor != startRoot)) p = Position(shadowAncestor, 0); while (p.isNotNull() && !(lowestEditableAncestor(p.node()) == baseEditableAncestor && !isEditablePosition(p))) { Node* root = editableRootForPosition(p); shadowAncestor = root ? root->shadowAncestorNode() : 0; p = isAtomicNode(p.node()) ? positionAfterNode(p.node()) : nextVisuallyDistinctCandidate(p); if (p.isNull() && (shadowAncestor != root)) p = Position(shadowAncestor, 0); } VisiblePosition next(p); if (next.isNull()) { ASSERT_NOT_REACHED(); m_base = Position(); m_extent = Position(); validate(); return; } m_start = next.deepEquivalent(); } } // Correct the extent if necessary. if (baseEditableAncestor != lowestEditableAncestor(m_extent.node())) m_extent = m_baseIsFirst ? m_end : m_start; }
bool CaretBase::updateCaretRect(Document* document, const VisiblePosition& caretPosition) { return updateCaretRect(document, PositionWithAffinity(caretPosition.deepEquivalent(), caretPosition.affinity())); }
static bool inSameTreeAndOrdered(const VisiblePosition& shouldBeFormer, const VisiblePosition& shouldBeLater) { const Position formerPosition = shouldBeFormer.deepEquivalent(); const Position laterPosition = shouldBeLater.deepEquivalent(); return Position::commonAncestorTreeScope(formerPosition, laterPosition) && comparePositions(formerPosition, laterPosition) <= 0; }
void VisibleSelection::setStartAndEndFromBaseAndExtentRespectingGranularity(TextGranularity granularity) { if (m_baseIsFirst) { m_start = m_base; m_end = m_extent; } else { m_start = m_extent; m_end = m_base; } switch (granularity) { case CharacterGranularity: // Don't do any expansion. break; case WordGranularity: { // General case: Select the word the caret is positioned inside of, or at the start of (RightWordIfOnBoundary). // Edge case: If the caret is after the last word in a soft-wrapped line or the last word in // the document, select that last word (LeftWordIfOnBoundary). // Edge case: If the caret is after the last word in a paragraph, select from the the end of the // last word to the line break (also RightWordIfOnBoundary); VisiblePosition start = VisiblePosition(m_start, m_affinity); VisiblePosition originalEnd(m_end, m_affinity); EWordSide side = RightWordIfOnBoundary; if (isEndOfEditableOrNonEditableContent(start) || (isEndOfLine(start) && !isStartOfLine(start) && !isEndOfParagraph(start))) side = LeftWordIfOnBoundary; m_start = startOfWord(start, side).deepEquivalent(); side = RightWordIfOnBoundary; if (isEndOfEditableOrNonEditableContent(originalEnd) || (isEndOfLine(originalEnd) && !isStartOfLine(originalEnd) && !isEndOfParagraph(originalEnd))) side = LeftWordIfOnBoundary; VisiblePosition wordEnd(endOfWord(originalEnd, side)); VisiblePosition end(wordEnd); if (isEndOfParagraph(originalEnd) && !isEmptyTableCell(m_start.deprecatedNode())) { // Select the paragraph break (the space from the end of a paragraph to the start of // the next one) to match TextEdit. end = wordEnd.next(); if (Element* table = isFirstPositionAfterTable(end)) { // The paragraph break after the last paragraph in the last cell of a block table ends // at the start of the paragraph after the table. if (isBlock(table)) end = end.next(CannotCrossEditingBoundary); else end = wordEnd; } if (end.isNull()) end = wordEnd; } m_end = end.deepEquivalent(); break; } case SentenceGranularity: { m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; } case LineGranularity: { m_start = startOfLine(VisiblePosition(m_start, m_affinity)).deepEquivalent(); VisiblePosition end = endOfLine(VisiblePosition(m_end, m_affinity)); // If the end of this line is at the end of a paragraph, include the space // after the end of the line in the selection. if (isEndOfParagraph(end)) { VisiblePosition next = end.next(); if (next.isNotNull()) end = next; } m_end = end.deepEquivalent(); break; } case LineBoundary: m_start = startOfLine(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfLine(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; case ParagraphGranularity: { VisiblePosition pos(m_start, m_affinity); if (isStartOfLine(pos) && isEndOfEditableOrNonEditableContent(pos)) pos = pos.previous(); m_start = startOfParagraph(pos).deepEquivalent(); VisiblePosition visibleParagraphEnd = endOfParagraph(VisiblePosition(m_end, m_affinity)); // Include the "paragraph break" (the space from the end of this paragraph to the start // of the next one) in the selection. VisiblePosition end(visibleParagraphEnd.next()); if (Element* table = isFirstPositionAfterTable(end)) { // The paragraph break after the last paragraph in the last cell of a block table ends // at the start of the paragraph after the table, not at the position just after the table. if (isBlock(table)) end = end.next(CannotCrossEditingBoundary); // There is no parargraph break after the last paragraph in the last cell of an inline table. else end = visibleParagraphEnd; } if (end.isNull()) end = visibleParagraphEnd; m_end = end.deepEquivalent(); break; } case DocumentBoundary: m_start = startOfDocument(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfDocument(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; case ParagraphBoundary: m_start = startOfParagraph(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfParagraph(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; case SentenceBoundary: m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; } // Make sure we do not have a dangling start or end. if (m_start.isNull()) m_start = m_end; if (m_end.isNull()) m_end = m_start; }
PassRefPtrWillBeRawPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const HTMLQualifiedName& listTag) { VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBoundary); VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary); if (start.isNull() || end.isNull()) return nullptr; // Check for adjoining lists. RefPtrWillBeRawPtr<HTMLElement> listItemElement = createListItemElement(document()); RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document()); appendNode(placeholder, listItemElement); // Place list item into adjoining lists. HTMLElement* previousList = adjacentEnclosingList(start, previousPositionOf(start, CannotCrossEditingBoundary), listTag); HTMLElement* nextList = adjacentEnclosingList(start, nextPositionOf(end, CannotCrossEditingBoundary), listTag); RefPtrWillBeRawPtr<HTMLElement> listElement = nullptr; if (previousList) { appendNode(listItemElement, previousList); } else if (nextList) { insertNodeAt(listItemElement, positionBeforeNode(nextList)); } else { // Create the list. listElement = createHTMLElement(document(), listTag); appendNode(listItemElement, listElement); if (start.deepEquivalent() == end.deepEquivalent() && isEnclosingBlock(start.deepEquivalent().anchorNode())) { // Inserting the list into an empty paragraph that isn't held open // by a br or a '\n', will invalidate start and end. Insert // a placeholder and then recompute start and end. RefPtrWillBeRawPtr<HTMLBRElement> placeholder = insertBlockPlaceholder(start.deepEquivalent()); start = createVisiblePosition(positionBeforeNode(placeholder.get())); end = start; } // Insert the list at a position visually equivalent to start of the // paragraph that is being moved into the list. // Try to avoid inserting it somewhere where it will be surrounded by // inline ancestors of start, since it is easier for editing to produce // clean markup when inline elements are pushed down as far as possible. Position insertionPos(mostBackwardCaretPosition(start.deepEquivalent())); // Also avoid the containing list item. Node* listChild = enclosingListChild(insertionPos.anchorNode()); if (isHTMLLIElement(listChild)) insertionPos = positionInParentBeforeNode(*listChild); insertNodeAt(listElement, insertionPos); // We inserted the list at the start of the content we're about to move // Update the start of content, so we don't try to move the list into itself. bug 19066 // Layout is necessary since start's node's inline layoutObjects may have been destroyed by the insertion // The end of the content may have changed after the insertion and layout so update it as well. if (insertionPos == start.deepEquivalent()) start = originalStart; } // Inserting list element and list item list may change start of pargraph // to move. We calculate start of paragraph again. document().updateLayoutIgnorePendingStylesheets(); start = startOfParagraph(start, CanSkipOverEditingBoundary); end = endOfParagraph(start, CanSkipOverEditingBoundary); moveParagraph(start, end, createVisiblePosition(positionBeforeNode(placeholder.get())), true); if (listElement) return mergeWithNeighboringLists(listElement); if (canMergeLists(previousList, nextList)) mergeIdenticalElements(previousList, nextList); return listElement; }
VisiblePosition endOfDocument(const VisiblePosition &c) { return endOfDocument(c.deepEquivalent().node()); }