// Abs x/y position of the caret ignoring transforms. // TODO(yosin) navigation with transforms should be smarter. static LayoutUnit lineDirectionPointForBlockDirectionNavigationOf( const VisiblePosition& visiblePosition) { if (visiblePosition.isNull()) return LayoutUnit(); LayoutObject* layoutObject; LayoutRect localRect = localCaretRectOfPosition( visiblePosition.toPositionWithAffinity(), layoutObject); if (localRect.isEmpty() || !layoutObject) return LayoutUnit(); // This ignores transforms on purpose, for now. Vertical navigation is done // without consulting transforms, so that 'up' in transformed text is 'up' // relative to the text, not absolute 'up'. FloatPoint caretPoint = layoutObject->localToAbsolute(FloatPoint(localRect.location())); LayoutObject* containingBlock = layoutObject->containingBlock(); if (!containingBlock) { // Just use ourselves to determine the writing mode if we have no containing // block. containingBlock = layoutObject; } return LayoutUnit(containingBlock->isHorizontalWritingMode() ? caretPoint.x() : caretPoint.y()); }
// TODO(xiaochengh): Stop storing VisiblePositions through mutations. void InsertListCommand::moveParagraphOverPositionIntoEmptyListItem( const VisiblePosition& pos, HTMLLIElement* listItemElement, EditingState* editingState) { DCHECK(!listItemElement->hasChildren()); HTMLBRElement* placeholder = HTMLBRElement::create(document()); appendNode(placeholder, listItemElement, editingState); if (editingState->isAborted()) return; // Inserting list element and list item list may change start of pargraph // to move. We calculate start of paragraph again. document().updateStyleAndLayoutIgnorePendingStylesheets(); const VisiblePosition& validPos = createVisiblePosition(pos.toPositionWithAffinity()); const VisiblePosition& start = startOfParagraph(validPos, CanSkipOverEditingBoundary); const VisiblePosition& end = endOfParagraph(validPos, CanSkipOverEditingBoundary); moveParagraph(start, end, VisiblePosition::beforeNode(placeholder), editingState, PreserveSelection); }
void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listElement, Node* listChildNode, EditingState* editingState) { // Since, unlistify paragraph inserts nodes into parent and removes node // from parent, if parent of |listElement| should be editable. DCHECK(hasEditableStyle(*listElement->parentNode())); Node* nextListChild; Node* previousListChild; VisiblePosition start; VisiblePosition end; DCHECK(listChildNode); if (isHTMLLIElement(*listChildNode)) { start = VisiblePosition::firstPositionInNode(listChildNode); end = VisiblePosition::lastPositionInNode(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(originalStart, CanSkipOverEditingBoundary); end = endOfParagraph(start, CanSkipOverEditingBoundary); nextListChild = enclosingListChild( nextPositionOf(end).deepEquivalent().anchorNode(), listElement); DCHECK_NE(nextListChild, listChildNode); previousListChild = enclosingListChild( previousPositionOf(start).deepEquivalent().anchorNode(), listElement); DCHECK_NE(previousListChild, listChildNode); } // Helpers for making |start| and |end| valid again after DOM changes. PositionWithAffinity startPosition = start.toPositionWithAffinity(); PositionWithAffinity endPosition = end.toPositionWithAffinity(); // When removing a list, we must always create a placeholder to act as a point // of insertion for the list content being removed. HTMLBRElement* placeholder = HTMLBRElement::create(document()); HTMLElement* elementToInsert = 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(listElement)) { elementToInsert = HTMLLIElement::create(document()); appendNode(placeholder, elementToInsert, editingState); if (editingState->isAborted()) return; } 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(listElement, splitTreeToNode(nextListChild, listElement)); insertNodeBefore(elementToInsert, listElement, editingState); } else if (nextListChild || listChildNode->parentNode() != listElement) { // 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() != listElement) splitElement(listElement, splitTreeToNode(listChildNode, listElement)); insertNodeBefore(elementToInsert, listElement, editingState); } else { insertNodeAfter(elementToInsert, listElement, editingState); } if (editingState->isAborted()) return; document().updateStyleAndLayoutIgnorePendingStylesheets(); // Make |start| and |end| valid again. start = createVisiblePosition(startPosition); end = createVisiblePosition(endPosition); VisiblePosition insertionPoint = VisiblePosition::beforeNode(placeholder); moveParagraphs(start, end, insertionPoint, editingState, PreserveSelection, PreserveStyle, listChildNode); }
void InsertListCommand::doApply(EditingState* editingState) { // Only entry points are Editor::Command::execute and // IndentOutdentCommand::outdentParagraph, both of which ensure clean layout. DCHECK(!document().needsLayoutTreeUpdate()); 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(createVisibleSelection( 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()); DCHECK(selection.isRange()); VisiblePosition visibleStartOfSelection = selection.visibleStart(); VisiblePosition visibleEndOfSelection = selection.visibleEnd(); PositionWithAffinity startOfSelection = visibleStartOfSelection.toPositionWithAffinity(); PositionWithAffinity endOfSelection = visibleEndOfSelection.toPositionWithAffinity(); Position startOfLastParagraph = startOfParagraph(visibleEndOfSelection, CanSkipOverEditingBoundary) .deepEquivalent(); Range* currentSelection = firstRangeOf(endingSelection()); ContainerNode* scopeForStartOfSelection = nullptr; ContainerNode* scopeForEndOfSelection = nullptr; // FIXME: This is an inefficient way to keep selection alive because // indexForVisiblePosition walks from the beginning of the document to the // visibleEndOfSelection everytime this code is executed. But not using // index is hard because there are so many ways we can lose selection inside // doApplyForSingleParagraph. int indexForStartOfSelection = indexForVisiblePosition( visibleStartOfSelection, scopeForStartOfSelection); int indexForEndOfSelection = indexForVisiblePosition(visibleEndOfSelection, scopeForEndOfSelection); if (startOfParagraph(visibleStartOfSelection, CanSkipOverEditingBoundary) .deepEquivalent() != startOfLastParagraph) { forceListCreation = !selectionHasListOfType(selection, listTag); VisiblePosition startOfCurrentParagraph = visibleStartOfSelection; while (inSameTreeAndOrdered(startOfCurrentParagraph.deepEquivalent(), startOfLastParagraph) && !inSameParagraph(startOfCurrentParagraph, createVisiblePosition(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 visibleEndOfSelection // and use it as the end of the new selection. if (!startOfLastParagraph.isConnected()) return; setEndingSelection(startOfCurrentParagraph); // Save and restore visibleEndOfSelection and startOfLastParagraph when // necessary since moveParagraph and movePragraphWithClones can remove // nodes. bool singleParagraphResult = doApplyForSingleParagraph( forceListCreation, listTag, *currentSelection, editingState); if (editingState->isAborted()) return; if (!singleParagraphResult) break; document().updateStyleAndLayoutIgnorePendingStylesheets(); // Make |visibleEndOfSelection| valid again. if (!endOfSelection.isConnected() || !startOfLastParagraph.isConnected()) { visibleEndOfSelection = visiblePositionForIndex( indexForEndOfSelection, scopeForEndOfSelection); endOfSelection = visibleEndOfSelection.toPositionWithAffinity(); // If visibleEndOfSelection 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. DCHECK(visibleEndOfSelection.isNotNull()); if (visibleEndOfSelection.isNull() || !rootEditableElementOf(visibleEndOfSelection)) return; startOfLastParagraph = startOfParagraph(visibleEndOfSelection, CanSkipOverEditingBoundary) .deepEquivalent(); } else { visibleEndOfSelection = createVisiblePosition(endOfSelection); } startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart()); } setEndingSelection(visibleEndOfSelection); } doApplyForSingleParagraph(forceListCreation, listTag, *currentSelection, editingState); if (editingState->isAborted()) return; document().updateStyleAndLayoutIgnorePendingStylesheets(); // Fetch the end of the selection, for the reason mentioned above. if (!endOfSelection.isConnected()) { visibleEndOfSelection = visiblePositionForIndex(indexForEndOfSelection, scopeForEndOfSelection); if (visibleEndOfSelection.isNull()) return; } else { visibleEndOfSelection = createVisiblePosition(endOfSelection); } if (!startOfSelection.isConnected()) { visibleStartOfSelection = visiblePositionForIndex( indexForStartOfSelection, scopeForStartOfSelection); if (visibleStartOfSelection.isNull()) return; } else { visibleStartOfSelection = createVisiblePosition(startOfSelection); } setEndingSelection( createVisibleSelection(visibleStartOfSelection, visibleEndOfSelection, endingSelection().isDirectional())); return; } DCHECK(firstRangeOf(endingSelection())); doApplyForSingleParagraph(false, listTag, *firstRangeOf(endingSelection()), editingState); }
void CaretBase::updateCaretRect(const VisiblePosition& caretPosition) { updateCaretRect(caretPosition.toPositionWithAffinity()); }
VisiblePosition VisiblePosition::honorEditingBoundaryAtOrBefore(const VisiblePosition &pos) const { return VisiblePosition(honorEditingBoundaryAtOrBeforeOf(pos.toPositionWithAffinity(), deepEquivalent())); }
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; }
void IndentOutdentCommand::outdentParagraph(EditingState* editingState) { VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart()); VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph); HTMLElement* enclosingElement = toHTMLElement( enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isHTMLListOrBlockquoteElement)); // We can't outdent if there is no place to go! if (!enclosingElement || !hasEditableStyle(*enclosingElement->parentNode())) return; // Use InsertListCommand to remove the selection from the list if (isHTMLOListElement(*enclosingElement)) { applyCommandToComposite( InsertListCommand::create(document(), InsertListCommand::OrderedList), editingState); return; } if (isHTMLUListElement(*enclosingElement)) { applyCommandToComposite( InsertListCommand::create(document(), InsertListCommand::UnorderedList), editingState); 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.deepEquivalent() == startOfEnclosingBlock.deepEquivalent() && visibleEndOfParagraph.deepEquivalent() == endOfEnclosingBlock.deepEquivalent()) { // The blockquote doesn't contain anything outside the paragraph, so it can // be totally removed. Node* splitPoint = enclosingElement->nextSibling(); removeNodePreservingChildren(enclosingElement, editingState); if (editingState->isAborted()) return; // 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()) { // We can't outdent if there is no place to go! if (splitPointParent->hasTagName(blockquoteTag) && !splitPoint->hasTagName(blockquoteTag) && hasEditableStyle(*splitPointParent->parentNode())) splitElement(splitPointParent, splitPoint); } } document().updateStyleAndLayoutIgnorePendingStylesheets(); visibleStartOfParagraph = createVisiblePosition(visibleStartOfParagraph.deepEquivalent()); if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph)) { insertNodeAt(HTMLBRElement::create(document()), visibleStartOfParagraph.deepEquivalent(), editingState); if (editingState->isAborted()) return; } document().updateStyleAndLayoutIgnorePendingStylesheets(); visibleEndOfParagraph = createVisiblePosition(visibleEndOfParagraph.deepEquivalent()); if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph)) insertNodeAt(HTMLBRElement::create(document()), visibleEndOfParagraph.deepEquivalent(), editingState); return; } Node* splitBlockquoteNode = enclosingElement; if (Element* enclosingBlockFlow = enclosingBlock( visibleStartOfParagraph.deepEquivalent().anchorNode())) { 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().anchorNode()); } document().updateStyleAndLayoutIgnorePendingStylesheets(); // Re-canonicalize visible{Start,End}OfParagraph, make them valid again // after DOM change. // TODO(xiaochengh): We should not store a VisiblePosition and later inspect // its properties when it is already invalidated. visibleStartOfParagraph = createVisiblePosition(visibleStartOfParagraph.toPositionWithAffinity()); visibleEndOfParagraph = createVisiblePosition(visibleEndOfParagraph.toPositionWithAffinity()); } // TODO(xiaochengh): We should not store a VisiblePosition and later inspect // its properties when it is already invalidated. VisiblePosition startOfParagraphToMove = startOfParagraph(visibleStartOfParagraph); VisiblePosition endOfParagraphToMove = endOfParagraph(visibleEndOfParagraph); if (startOfParagraphToMove.isNull() || endOfParagraphToMove.isNull()) return; HTMLBRElement* placeholder = HTMLBRElement::create(document()); insertNodeBefore(placeholder, splitBlockquoteNode, editingState); if (editingState->isAborted()) return; document().updateStyleAndLayoutIgnorePendingStylesheets(); startOfParagraphToMove = createVisiblePosition(startOfParagraphToMove.toPositionWithAffinity()); endOfParagraphToMove = createVisiblePosition(endOfParagraphToMove.toPositionWithAffinity()); moveParagraph(startOfParagraphToMove, endOfParagraphToMove, VisiblePosition::beforeNode(placeholder), editingState, PreserveSelection); }