// This avoids the expense of a full fledged delete operation, and avoids a layout that typically results // from text removal. bool InsertTextCommand::performTrivialReplace(const String& text, bool selectInsertedText) { if (!endingSelection().isRange()) return false; if (text.contains('\t') || text.contains(' ') || text.contains('\n')) return false; Position start = endingSelection().start(); Position end = endingSelection().end(); if (start.node() != end.node() || !start.node()->isTextNode() || isTabSpanTextNode(start.node())) return false; replaceTextInNode(static_cast<Text*>(start.node()), start.offset(), end.offset() - start.offset(), text); Position endPosition(start.node(), start.offset() + text.length()); // We could have inserted a part of composed character sequence, // so we are basically treating ending selection as a range to avoid validation. // <http://bugs.webkit.org/show_bug.cgi?id=15781> Selection forcedEndingSelection; forcedEndingSelection.setWithoutValidation(start, endPosition); setEndingSelection(forcedEndingSelection); if (!selectInsertedText) setEndingSelection(Selection(endingSelection().visibleEnd())); return true; }
// This avoids the expense of a full fledged delete operation, and avoids a layout that typically results // from text removal. bool InsertTextCommand::performTrivialReplace(const String& text, bool selectInsertedText) { if (!endingSelection().isRange()) return false; if (text.contains('\t') || text.contains(' ') || text.contains('\n')) return false; Position start = endingSelection().start(); Position endPosition = replaceSelectedTextInNode(text); if (endPosition.isNull()) return false; // We could have inserted a part of composed character sequence, // so we are basically treating ending selection as a range to avoid validation. // <http://bugs.webkit.org/show_bug.cgi?id=15781> VisibleSelection forcedEndingSelection; forcedEndingSelection.setWithoutValidation(start, endPosition); forcedEndingSelection.setIsDirectional(endingSelection().isDirectional()); setEndingSelection(forcedEndingSelection); if (!selectInsertedText) setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional())); return true; }
void InsertTextCommand::input(const String &text, bool selectInsertedText) { assert(text.find('\n') == -1); if (endingSelection().isNone()) return; // Delete the current selection. if (endingSelection().isRange()) deleteSelection(false, true, true); // Insert the character at the leftmost candidate. Position startPosition = endingSelection().start().upstream(); deleteInsignificantText(startPosition.upstream(), startPosition.downstream()); if (!startPosition.inRenderedContent()) startPosition = startPosition.downstream(); startPosition = positionAvoidingSpecialElementBoundary(startPosition); Position endPosition; if (text == "\t") { endPosition = insertTab(startPosition); startPosition = endPosition.previous(); removeBlockPlaceholder(VisiblePosition(startPosition)); m_charactersAdded += 1; } else { // Make sure the document is set up to receive text startPosition = prepareForTextInsertion(startPosition); removeBlockPlaceholder(VisiblePosition(startPosition)); Text *textNode = static_cast<Text *>(startPosition.node()); int offset = startPosition.offset(); insertTextIntoNode(textNode, offset, text); endPosition = Position(textNode, offset + text.length()); // The insertion may require adjusting adjacent whitespace, if it is present. rebalanceWhitespaceAt(endPosition); // Rebalancing on both sides isn't necessary if we've inserted a space. if (text != " ") rebalanceWhitespaceAt(startPosition); m_charactersAdded += text.length(); } setEndingSelection(Selection(startPosition, endPosition, DOWNSTREAM)); // Handle the case where there is a typing style. // FIXME: Improve typing style. // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement CSSMutableStyleDeclaration* typingStyle = document()->frame()->typingStyle(); RefPtr<CSSComputedStyleDeclaration> endingStyle = endPosition.computedStyle(); endingStyle->diff(typingStyle); if (typingStyle && typingStyle->length() > 0) applyStyle(typingStyle); if (!selectInsertedText) setEndingSelection(endingSelection().end(), endingSelection().affinity()); }
void MoveSelectionCommand::doApply() { Selection selection = endingSelection(); ASSERT(selection.isRange()); Position pos = m_position; if (pos.isNull()) return; // Update the position otherwise it may become invalid after the selection is deleted. Node *positionNode = m_position.node(); int positionOffset = m_position.offset(); Position selectionEnd = selection.end(); int selectionEndOffset = selectionEnd.offset(); if (selectionEnd.node() == positionNode && selectionEndOffset < positionOffset) { positionOffset -= selectionEndOffset; Position selectionStart = selection.start(); if (selectionStart.node() == positionNode) { positionOffset += selectionStart.offset(); } pos = Position(positionNode, positionOffset); } deleteSelection(m_smartMove); // If the node for the destination has been removed as a result of the deletion, // set the destination to the ending point after the deletion. // Fixes: <rdar://problem/3910425> REGRESSION (Mail): Crash in ReplaceSelectionCommand; // selection is empty, leading to null deref if (!pos.node()->inDocument()) pos = endingSelection().start(); setEndingSelection(Selection(pos, endingSelection().affinity())); applyCommandToComposite(ReplaceSelectionCommand::create(positionNode->document(), m_fragment, true, m_smartMove)); }
void CreateLinkCommand::doApply(EditingState* editingState) { if (endingSelection().isNone()) return; HTMLAnchorElement* anchorElement = HTMLAnchorElement::create(document()); anchorElement->setHref(AtomicString(m_url)); if (endingSelection().isRange()) { applyStyledElement(anchorElement, editingState); if (editingState->isAborted()) return; } else { insertNodeAt(anchorElement, endingSelection().start(), editingState); if (editingState->isAborted()) return; Text* textNode = Text::create(document(), m_url); appendNode(textNode, anchorElement, editingState); if (editingState->isAborted()) return; document().updateStyleAndLayoutIgnorePendingStylesheets(); setEndingSelection(createVisibleSelection( Position::inParentBeforeNode(*anchorElement), Position::inParentAfterNode(*anchorElement), TextAffinity::Downstream, endingSelection().isDirectional())); } }
EditCommand::EditCommand(Document& document, const VisibleSelection& startingSelection, const VisibleSelection& endingSelection) : m_document(document) { ASSERT(document.frame()); setStartingSelection(startingSelection); setEndingSelection(endingSelection); }
EditCommand::EditCommand(Document& document, EditAction editingAction) : m_document(document) , m_editingAction(editingAction) { ASSERT(document.frame()); setStartingSelection(m_document->frame()->selection().selection()); setEndingSelection(m_startingSelection); }
EditCommand::EditCommand(Document* document) : m_document(document) , m_parent(0) { ASSERT(m_document); ASSERT(m_document->frame()); setStartingSelection(avoidIntersectionWithNode(m_document->frame()->selection()->selection(), m_document->frame()->editor()->deleteButtonController()->containerElement())); setEndingSelection(m_startingSelection); }
EditCommand::EditCommand(Document& document) : m_document(&document) , m_parent(nullptr) { ASSERT(m_document); ASSERT(m_document->frame()); setStartingSelection(m_document->frame()->selection().selection()); setEndingSelection(m_startingSelection); }
EditCommand::EditCommand(Document* document, const VisibleSelection& startingSelection, const VisibleSelection& endingSelection) : m_document(document) , m_parent(nullptr) { ASSERT(m_document); ASSERT(m_document->frame()); setStartingSelection(startingSelection); setEndingSelection(endingSelection); }
void InsertTextCommand::setEndingSelectionWithoutValidation(const Position& startPosition, const Position& endPosition) { // We could have inserted a part of composed character sequence, // so we are basically treating ending selection as a range to avoid validation. // <http://bugs.webkit.org/show_bug.cgi?id=15781> VisibleSelection forcedEndingSelection; forcedEndingSelection.setWithoutValidation(startPosition, endPosition); forcedEndingSelection.setIsDirectional(endingSelection().isDirectional()); setEndingSelection(forcedEndingSelection); }
void MoveSelectionCommand::doApply() { ASSERT(endingSelection().isNonOrphanedRange()); Position pos = m_position; if (pos.isNull()) return; // Update the position otherwise it may become invalid after the selection is deleted. Position selectionEnd = endingSelection().end(); if (pos.anchorType() == Position::PositionIsOffsetInAnchor && selectionEnd.anchorType() == Position::PositionIsOffsetInAnchor && selectionEnd.containerNode() == pos.containerNode() && selectionEnd.offsetInContainerNode() < pos.offsetInContainerNode()) { pos.moveToOffset(pos.offsetInContainerNode() - selectionEnd.offsetInContainerNode()); Position selectionStart = endingSelection().start(); if (selectionStart.anchorType() == Position::PositionIsOffsetInAnchor && selectionStart.containerNode() == pos.containerNode()) pos.moveToOffset(pos.offsetInContainerNode() + selectionStart.offsetInContainerNode()); } { auto deleteSelection = DeleteSelectionCommand::create(document(), m_smartDelete, true, false, true, true, EditActionDeleteByDrag); deleteSelection->setParent(this); deleteSelection->apply(); m_commands.append(WTFMove(deleteSelection)); } // If the node for the destination has been removed as a result of the deletion, // set the destination to the ending point after the deletion. // Fixes: <rdar://problem/3910425> REGRESSION (Mail): Crash in ReplaceSelectionCommand; // selection is empty, leading to null deref if (!pos.anchorNode()->inDocument()) pos = endingSelection().start(); cleanupAfterDeletion(pos); setEndingSelection(VisibleSelection(pos, endingSelection().affinity(), endingSelection().isDirectional())); setStartingSelection(endingSelection()); if (!pos.anchorNode()->inDocument()) { // Document was modified out from under us. return; } ReplaceSelectionCommand::CommandOptions options = ReplaceSelectionCommand::SelectReplacement | ReplaceSelectionCommand::PreventNesting; if (m_smartInsert) options |= ReplaceSelectionCommand::SmartReplace; { auto replaceSelection = ReplaceSelectionCommand::create(document(), WTFMove(m_fragment), options, EditActionInsertFromDrop); replaceSelection->setParent(this); replaceSelection->apply(); m_commands.append(WTFMove(replaceSelection)); } }
void CreateLinkCommand::doApply() { if (endingSelection().isNone()) return; RefPtr<HTMLAnchorElement> anchorElement = HTMLAnchorElement::create(document()); anchorElement->setHref(m_url); if (endingSelection().isRange()) applyStyledElement(anchorElement.get()); else { insertNodeAt(anchorElement.get(), endingSelection().start()); RefPtr<Text> textNode = Text::create(document(), m_url); appendNode(textNode.get(), anchorElement.get()); setEndingSelection(VisibleSelection(positionInParentBeforeNode(anchorElement.get()), positionInParentAfterNode(anchorElement.get()), DOWNSTREAM)); } }
void CreateLinkCommand::doApply() { if (endingSelection().isNone()) return; RefPtrWillBeRawPtr<HTMLAnchorElement> anchorElement = HTMLAnchorElement::create(document()); anchorElement->setHref(AtomicString(m_url)); if (endingSelection().isRange()) { applyStyledElement(anchorElement.get()); } else { insertNodeAt(anchorElement.get(), endingSelection().start()); RefPtrWillBeRawPtr<Text> textNode = Text::create(document(), m_url); appendNode(textNode.get(), anchorElement.get()); setEndingSelection(VisibleSelection(positionInParentBeforeNode(*anchorElement), positionInParentAfterNode(*anchorElement), TextAffinity::Downstream, endingSelection().isDirectional())); } }
void CreateLinkCommand::doApply() { if (endingSelection().isNone()) return; RefPtr<HTMLAnchorElement> anchorElement = new HTMLAnchorElement(document()); anchorElement->setHref(m_url); if (endingSelection().isRange()) { pushPartiallySelectedAnchorElementsDown(); applyStyledElement(anchorElement.get()); } else { insertNodeAt(anchorElement.get(), endingSelection().start()); RefPtr<Text> textNode = new Text(document(), m_url); appendNode(textNode.get(), anchorElement.get()); setEndingSelection(Selection(positionBeforeNode(anchorElement.get()), positionAfterNode(anchorElement.get()), DOWNSTREAM)); } }
// This avoids the expense of a full fledged delete operation, and avoids a layout that typically results // from text removal. bool InsertTextCommand::performTrivialReplace(const String& text, bool selectInsertedText) { if (!endingSelection().isRange()) return false; if (text.contains('\t') || text.contains(' ') || text.contains('\n')) return false; Position start = endingSelection().start(); Position endPosition = replaceSelectedTextInNode(text); if (endPosition.isNull()) return false; setEndingSelectionWithoutValidation(start, endPosition); if (!selectInsertedText) setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional())); return true; }
bool InsertTextCommand::performOverwrite(const String& text, bool selectInsertedText) { Position start = endingSelection().start(); RefPtrWillBeRawPtr<Text> textNode = start.containerText(); if (!textNode) return false; unsigned count = std::min(text.length(), textNode->length() - start.offsetInContainerNode()); if (!count) return false; replaceTextInNode(textNode, start.offsetInContainerNode(), count, text); Position endPosition = Position(textNode.release(), start.offsetInContainerNode() + text.length()); setEndingSelectionWithoutValidation(start, endPosition); if (!selectInsertedText) setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional())); return true; }
void MoveSelectionCommand::doApply() { ASSERT(endingSelection().isNonOrphanedRange()); Position pos = m_position; if (pos.isNull()) return; // Update the position otherwise it may become invalid after the selection is deleted. Position selectionEnd = endingSelection().end(); if (pos.isOffsetInAnchor() && selectionEnd.isOffsetInAnchor() && selectionEnd.computeContainerNode() == pos.computeContainerNode() && selectionEnd.offsetInContainerNode() < pos.offsetInContainerNode()) { pos = Position(pos.computeContainerNode(), pos.offsetInContainerNode() - selectionEnd.offsetInContainerNode()); Position selectionStart = endingSelection().start(); if (selectionStart.isOffsetInAnchor() && selectionStart.computeContainerNode() == pos.computeContainerNode()) pos = Position(pos.computeContainerNode(), pos.offsetInContainerNode() + selectionStart.offsetInContainerNode()); } deleteSelection(m_smartDelete); // If the node for the destination has been removed as a result of the deletion, // set the destination to the ending point after the deletion. // Fixes: <rdar://problem/3910425> REGRESSION (Mail): Crash in ReplaceSelectionCommand; // selection is empty, leading to null deref if (!pos.inDocument()) pos = endingSelection().start(); cleanupAfterDeletion(VisiblePosition(pos)); setEndingSelection(VisibleSelection(pos, endingSelection().affinity(), endingSelection().isDirectional())); if (!pos.inDocument()) { // Document was modified out from under us. return; } ReplaceSelectionCommand::CommandOptions options = ReplaceSelectionCommand::SelectReplacement | ReplaceSelectionCommand::PreventNesting; if (m_smartInsert) options |= ReplaceSelectionCommand::SmartReplace; applyCommandToComposite(ReplaceSelectionCommand::create(document(), m_fragment, options)); }
void TypingCommand::deleteKeyPressed(TextGranularity granularity) { Selection selectionToDelete; Selection selectionAfterUndo; switch (endingSelection().state()) { case Selection::RANGE: selectionToDelete = endingSelection(); selectionAfterUndo = selectionToDelete; break; case Selection::CARET: { m_smartDelete = false; SelectionController selection; selection.setSelection(endingSelection()); selection.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity); // When the caret is at the start of the editable area in an empty list item, break out of the list item. if (endingSelection().visibleStart().previous(true).isNull()) { if (breakOutOfEmptyListItem()) { typingAddedToOpenCommand(); return; } } VisiblePosition visibleStart(endingSelection().visibleStart()); // If the caret is at the start of a paragraph after a table, move content into the last table cell. if (isStartOfParagraph(visibleStart) && isFirstPositionAfterTable(visibleStart.previous(true))) { // Unless the caret is just before a table. We don't want to move a table into the last table cell. if (isLastPositionBeforeTable(visibleStart)) return; // Extend the selection backward into the last cell, then deletion will handle the move. selection.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity); // If the caret is just after a table, select the table and don't delete anything. } else if (Node* table = isFirstPositionAfterTable(visibleStart)) { setEndingSelection(Selection(Position(table, 0), endingSelection().start(), DOWNSTREAM)); typingAddedToOpenCommand(); return; } 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 Selection 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. selectionAfterUndo.setWithoutValidation(startingSelection().end(), selectionToDelete.extent()); break; } case Selection::NONE: ASSERT_NOT_REACHED(); break; } if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) { // Make undo select everything that has been deleted, unless an undo will undo more than just this deletion. // FIXME: This behaves like TextEdit except for the case where you open with text insertion and then delete // more text than you insert. In that case all of the text that was around originally should be selected. if (m_openedByBackwardDelete) setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); typingAddedToOpenCommand(); } }
void EditCommand::setEndingSelection(const VisiblePosition& position) { setEndingSelection(VisibleSelection(position)); }
void InsertTextCommand::input(const String& text, bool selectInsertedText, RebalanceType whitespaceRebalance) { ASSERT(text.find('\n') == notFound); if (!endingSelection().isNonOrphanedCaretOrRange()) return; // Delete the current selection. // FIXME: This delete operation blows away the typing style. if (endingSelection().isRange()) { if (performTrivialReplace(text, selectInsertedText)) return; deleteSelection(false, true, true, false); } Position startPosition(endingSelection().start()); Position placeholder; // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content // is inserted just before them. // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661. // If the caret is just before a placeholder, downstream will normalize the caret to it. Position downstream(startPosition.downstream()); if (lineBreakExistsAtPosition(downstream)) { // FIXME: This doesn't handle placeholders at the end of anonymous blocks. VisiblePosition caret(startPosition); if (isEndOfBlock(caret) && isStartOfParagraph(caret)) placeholder = downstream; // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before // we get a chance to insert into it. We check for a placeholder now, though, because doing so requires // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout. } // Insert the character at the leftmost candidate. startPosition = startPosition.upstream(); // It is possible for the node that contains startPosition to contain only unrendered whitespace, // and so deleteInsignificantText could remove it. Save the position before the node in case that happens. Position positionBeforeStartNode(positionInParentBeforeNode(startPosition.containerNode())); deleteInsignificantText(startPosition.upstream(), startPosition.downstream()); if (!startPosition.anchorNode()->inDocument()) startPosition = positionBeforeStartNode; if (!startPosition.isCandidate()) startPosition = startPosition.downstream(); startPosition = positionAvoidingSpecialElementBoundary(startPosition); Position endPosition; if (text == "\t") { endPosition = insertTab(startPosition); startPosition = endPosition.previous(); if (placeholder.isNotNull()) removePlaceholderAt(placeholder); } else { // Make sure the document is set up to receive text startPosition = positionInsideTextNode(startPosition); ASSERT(startPosition.anchorType() == Position::PositionIsOffsetInAnchor); ASSERT(startPosition.containerNode()); ASSERT(startPosition.containerNode()->isTextNode()); if (placeholder.isNotNull()) removePlaceholderAt(placeholder); RefPtr<Text> textNode = static_cast<Text*>(startPosition.containerNode()); const unsigned offset = startPosition.offsetInContainerNode(); insertTextIntoNode(textNode, offset, text); endPosition = Position(textNode, offset + text.length()); if (whitespaceRebalance == RebalanceLeadingAndTrailingWhitespaces) { // The insertion may require adjusting adjacent whitespace, if it is present. rebalanceWhitespaceAt(endPosition); // Rebalancing on both sides isn't necessary if we've inserted only spaces. if (!shouldRebalanceLeadingWhitespaceFor(text)) rebalanceWhitespaceAt(startPosition); } else { ASSERT(whitespaceRebalance == RebalanceAllWhitespaces); if (canRebalance(startPosition) && canRebalance(endPosition)) rebalanceWhitespaceOnTextSubstring(textNode, startPosition.offsetInContainerNode(), endPosition.offsetInContainerNode()); } } // We could have inserted a part of composed character sequence, // so we are basically treating ending selection as a range to avoid validation. // <http://bugs.webkit.org/show_bug.cgi?id=15781> VisibleSelection forcedEndingSelection; forcedEndingSelection.setWithoutValidation(startPosition, endPosition); setEndingSelection(forcedEndingSelection); // Handle the case where there is a typing style. if (RefPtr<EditingStyle> typingStyle = document()->frame()->selection()->typingStyle()) { typingStyle->prepareToApplyAt(endPosition, EditingStyle::PreserveWritingDirection); if (!typingStyle->isEmpty()) applyStyle(typingStyle.get()); } if (!selectInsertedText) setEndingSelection(VisibleSelection(endingSelection().end(), endingSelection().affinity())); }
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() // 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() && isRenderedTableElement(canonicalPos.deprecatedNode()))) { 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 = nullptr; 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<HTMLElement> extraBlock = createDefaultParagraphElement(document()); appendNode(extraBlock, startBlock); } appendNode(blockToInsert, startBlock); } else { // 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* siblingElement = startBlock.get(); insertNodeAfter(blockToInsert, siblingElement); } // 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); 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 = 0; 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->hasChildren()); refNode = startBlock->firstChild(); } else if (insertionPosition.deprecatedNode() == startBlock && nestNewBlock) { refNode = NodeTraversal::childAt(*startBlock, 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(); if (refNode) insertNodeBefore(blockToInsert, refNode); // Recreate the same structure in the new paragraph. Vector<RefPtr<Element> > ancestors; getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(positionOutsideTabSpan(insertionPosition)).deprecatedNode(), startBlock.get(), ancestors); // In this case, we need to set the new ending selection. setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM, endingSelection().isDirectional())); return; } //--------------------------------------------------------------------- // Handle the (more complicated) general case, // 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 = leadingWhitespacePosition(insertionPosition, 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()); 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(); // 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())); }
void InsertTextCommand::input(const String& text, bool selectInsertedText) { ASSERT(text.find('\n') == notFound); if (!endingSelection().isNonOrphanedCaretOrRange()) return; // Delete the current selection. // FIXME: This delete operation blows away the typing style. if (endingSelection().isRange()) { if (performTrivialReplace(text, selectInsertedText)) return; deleteSelection(false, true, true, false); } Position startPosition(endingSelection().start()); Position placeholder; // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content // is inserted just before them. // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661. // If the caret is just before a placeholder, downstream will normalize the caret to it. Position downstream(startPosition.downstream()); if (lineBreakExistsAtPosition(downstream)) { // FIXME: This doesn't handle placeholders at the end of anonymous blocks. VisiblePosition caret(startPosition); if (isEndOfBlock(caret) && isStartOfParagraph(caret)) placeholder = downstream; // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before // we get a chance to insert into it. We check for a placeholder now, though, because doing so requires // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout. } // Insert the character at the leftmost candidate. startPosition = startPosition.upstream(); // It is possible for the node that contains startPosition to contain only unrendered whitespace, // and so deleteInsignificantText could remove it. Save the position before the node in case that happens. Position positionBeforeStartNode(positionInParentBeforeNode(startPosition.node())); deleteInsignificantText(startPosition.upstream(), startPosition.downstream()); if (!startPosition.node()->inDocument()) startPosition = positionBeforeStartNode; if (!startPosition.isCandidate()) startPosition = startPosition.downstream(); startPosition = positionAvoidingSpecialElementBoundary(startPosition); Position endPosition; if (text == "\t") { endPosition = insertTab(startPosition); startPosition = endPosition.previous(); if (placeholder.isNotNull()) removePlaceholderAt(placeholder); m_charactersAdded += 1; } else { // Make sure the document is set up to receive text startPosition = prepareForTextInsertion(startPosition); if (placeholder.isNotNull()) removePlaceholderAt(placeholder); Text *textNode = static_cast<Text *>(startPosition.node()); int offset = startPosition.deprecatedEditingOffset(); insertTextIntoNode(textNode, offset, text); endPosition = Position(textNode, offset + text.length()); // The insertion may require adjusting adjacent whitespace, if it is present. rebalanceWhitespaceAt(endPosition); // Rebalancing on both sides isn't necessary if we've inserted a space. if (text != " ") rebalanceWhitespaceAt(startPosition); m_charactersAdded += text.length(); } // We could have inserted a part of composed character sequence, // so we are basically treating ending selection as a range to avoid validation. // <http://bugs.webkit.org/show_bug.cgi?id=15781> VisibleSelection forcedEndingSelection; forcedEndingSelection.setWithoutValidation(startPosition, endPosition); setEndingSelection(forcedEndingSelection); // Handle the case where there is a typing style. CSSMutableStyleDeclaration* typingStyle = document()->frame()->selection()->typingStyle(); RefPtr<CSSComputedStyleDeclaration> endingStyle = endPosition.computedStyle(); RefPtr<CSSValue> unicodeBidi; RefPtr<CSSValue> direction; if (typingStyle) { unicodeBidi = typingStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi); direction = typingStyle->getPropertyCSSValue(CSSPropertyDirection); } endingStyle->diff(typingStyle); if (typingStyle && unicodeBidi) { ASSERT(unicodeBidi->isPrimitiveValue()); typingStyle->setProperty(CSSPropertyUnicodeBidi, static_cast<CSSPrimitiveValue*>(unicodeBidi.get())->getIdent()); if (direction) { ASSERT(direction->isPrimitiveValue()); typingStyle->setProperty(CSSPropertyDirection, static_cast<CSSPrimitiveValue*>(direction.get())->getIdent()); } } if (typingStyle && typingStyle->length()) applyStyle(typingStyle); if (!selectInsertedText) setEndingSelection(VisibleSelection(endingSelection().end(), endingSelection().affinity())); }
void InsertTextCommand::input(const String& originalText, bool selectInsertedText) { String text = originalText; ASSERT(text.find('\n') == -1); if (endingSelection().isNone()) return; if (RenderObject* renderer = endingSelection().start().node()->renderer()) if (renderer->style()->collapseWhiteSpace()) // Turn all spaces into non breaking spaces, to make sure that they are treated // literally, and aren't collapsed after insertion. They will be rebalanced // (turned into a sequence of regular and non breaking spaces) below. text.replace(' ', noBreakSpace); // Delete the current selection. // FIXME: This delete operation blows away the typing style. if (endingSelection().isRange()) { if (performTrivialReplace(text, selectInsertedText)) return; deleteSelection(false, true, true, false); } // Insert the character at the leftmost candidate. Position startPosition = endingSelection().start().upstream(); // It is possible for the node that contains startPosition to contain only unrendered whitespace, // and so deleteInsignificantText could remove it. Save the position before the node in case that happens. Position positionBeforeStartNode(positionBeforeNode(startPosition.node())); deleteInsignificantText(startPosition.upstream(), startPosition.downstream()); if (!startPosition.node()->inDocument()) startPosition = positionBeforeStartNode; if (!startPosition.isCandidate()) startPosition = startPosition.downstream(); startPosition = positionAvoidingSpecialElementBoundary(startPosition); Position endPosition; if (text == "\t") { endPosition = insertTab(startPosition); startPosition = endPosition.previous(); removePlaceholderAt(VisiblePosition(startPosition)); m_charactersAdded += 1; } else { // Make sure the document is set up to receive text startPosition = prepareForTextInsertion(startPosition); removePlaceholderAt(VisiblePosition(startPosition)); Text *textNode = static_cast<Text *>(startPosition.node()); int offset = startPosition.offset(); insertTextIntoNode(textNode, offset, text); endPosition = Position(textNode, offset + text.length()); // The insertion may require adjusting adjacent whitespace, if it is present. rebalanceWhitespaceAt(endPosition); // Rebalancing on both sides isn't necessary if we've inserted a space. if (originalText != " ") rebalanceWhitespaceAt(startPosition); m_charactersAdded += text.length(); } // We could have inserted a part of composed character sequence, // so we are basically treating ending selection as a range to avoid validation. // <http://bugs.webkit.org/show_bug.cgi?id=15781> Selection forcedEndingSelection; forcedEndingSelection.setWithoutValidation(startPosition, endPosition); setEndingSelection(forcedEndingSelection); // Handle the case where there is a typing style. CSSMutableStyleDeclaration* typingStyle = document()->frame()->typingStyle(); RefPtr<CSSComputedStyleDeclaration> endingStyle = endPosition.computedStyle(); RefPtr<CSSValue> unicodeBidi; RefPtr<CSSValue> direction; if (typingStyle) { unicodeBidi = typingStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi); direction = typingStyle->getPropertyCSSValue(CSSPropertyDirection); } endingStyle->diff(typingStyle); if (typingStyle && unicodeBidi) { ASSERT(unicodeBidi->isPrimitiveValue()); typingStyle->setProperty(CSSPropertyUnicodeBidi, static_cast<CSSPrimitiveValue*>(unicodeBidi.get())->getIdent()); if (direction) { ASSERT(direction->isPrimitiveValue()); typingStyle->setProperty(CSSPropertyDirection, static_cast<CSSPrimitiveValue*>(direction.get())->getIdent()); } } if (typingStyle && typingStyle->length()) applyStyle(typingStyle); if (!selectInsertedText) setEndingSelection(Selection(endingSelection().end(), endingSelection().affinity())); }
void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity) { Selection selectionToDelete; Selection selectionAfterUndo; switch (endingSelection().state()) { case Selection::RANGE: selectionToDelete = endingSelection(); selectionAfterUndo = selectionToDelete; break; case Selection::CARET: { 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. SelectionController selection; selection.setSelection(endingSelection()); selection.modify(SelectionController::EXTEND, SelectionController::FORWARD, granularity); Position downstreamEnd = endingSelection().end().downstream(); VisiblePosition visibleEnd = endingSelection().visibleEnd(); if (visibleEnd == endOfParagraph(visibleEnd)) downstreamEnd = visibleEnd.next(true).deepEquivalent().downstream(); // When deleting tables: Select the table first, then perform the deletion if (downstreamEnd.node() && downstreamEnd.node()->renderer() && downstreamEnd.node()->renderer()->isTable() && downstreamEnd.offset() == 0) { setEndingSelection(Selection(endingSelection().end(), Position(downstreamEnd.node(), maxDeepOffset(downstreamEnd.node())), DOWNSTREAM)); typingAddedToOpenCommand(); 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(SelectionController::EXTEND, SelectionController::FORWARD, 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 Selection 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.node() != selectionToDelete.end().node()) extent = selectionToDelete.extent(); else { int extraCharacters; if (selectionToDelete.start().node() == selectionToDelete.end().node()) extraCharacters = selectionToDelete.end().offset() - selectionToDelete.start().offset(); else extraCharacters = selectionToDelete.end().offset(); extent = Position(extent.node(), extent.offset() + extraCharacters); } selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent); } break; } case Selection::NONE: ASSERT_NOT_REACHED(); break; } if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) { // make undo select what was deleted setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); typingAddedToOpenCommand(); } }
void InsertTextCommand::doApply() { ASSERT(m_text.find('\n') == kNotFound); if (!endingSelection().isNonOrphanedCaretOrRange()) return; // Delete the current selection. // FIXME: This delete operation blows away the typing style. if (endingSelection().isRange()) { if (performTrivialReplace(m_text, m_selectInsertedText)) return; bool endOfSelectionWasAtStartOfBlock = isStartOfBlock(endingSelection().visibleEnd()); deleteSelection(false, true, false, false); // deleteSelection eventually makes a new endingSelection out of a Position. If that Position doesn't have // a renderer (e.g. it is on a <frameset> in the DOM), the VisibleSelection cannot be canonicalized to // anything other than NoSelection. The rest of this function requires a real endingSelection, so bail out. if (endingSelection().isNone()) return; if (endOfSelectionWasAtStartOfBlock) { if (EditingStyle* typingStyle = document().frame()->selection().typingStyle()) typingStyle->removeBlockProperties(); } } else if (document().frame()->editor().isOverwriteModeEnabled()) { if (performOverwrite(m_text, m_selectInsertedText)) return; } Position startPosition(endingSelection().start()); Position placeholder; // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content // is inserted just before them. // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661. // If the caret is just before a placeholder, downstream will normalize the caret to it. Position downstream(startPosition.downstream()); if (lineBreakExistsAtPosition(downstream)) { // FIXME: This doesn't handle placeholders at the end of anonymous blocks. VisiblePosition caret(startPosition); if (isEndOfBlock(caret) && isStartOfParagraph(caret)) placeholder = downstream; // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before // we get a chance to insert into it. We check for a placeholder now, though, because doing so requires // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout. } // Insert the character at the leftmost candidate. startPosition = startPosition.upstream(); // It is possible for the node that contains startPosition to contain only unrendered whitespace, // and so deleteInsignificantText could remove it. Save the position before the node in case that happens. ASSERT(startPosition.containerNode()); Position positionBeforeStartNode(positionInParentBeforeNode(*startPosition.containerNode())); deleteInsignificantText(startPosition, startPosition.downstream()); if (!startPosition.inDocument()) startPosition = positionBeforeStartNode; if (!startPosition.isCandidate()) startPosition = startPosition.downstream(); startPosition = positionAvoidingSpecialElementBoundary(startPosition); Position endPosition; if (m_text == "\t") { endPosition = insertTab(startPosition); startPosition = endPosition.previous(); if (placeholder.isNotNull()) removePlaceholderAt(placeholder); } else { // Make sure the document is set up to receive m_text startPosition = positionInsideTextNode(startPosition); ASSERT(startPosition.anchorType() == Position::PositionIsOffsetInAnchor); ASSERT(startPosition.containerNode()); ASSERT(startPosition.containerNode()->isTextNode()); if (placeholder.isNotNull()) removePlaceholderAt(placeholder); RefPtrWillBeRawPtr<Text> textNode = startPosition.containerText(); const unsigned offset = startPosition.offsetInContainerNode(); insertTextIntoNode(textNode, offset, m_text); endPosition = Position(textNode, offset + m_text.length()); if (m_rebalanceType == RebalanceLeadingAndTrailingWhitespaces) { // The insertion may require adjusting adjacent whitespace, if it is present. rebalanceWhitespaceAt(endPosition); // Rebalancing on both sides isn't necessary if we've inserted only spaces. if (!shouldRebalanceLeadingWhitespaceFor(m_text)) rebalanceWhitespaceAt(startPosition); } else { ASSERT(m_rebalanceType == RebalanceAllWhitespaces); if (canRebalance(startPosition) && canRebalance(endPosition)) rebalanceWhitespaceOnTextSubstring(textNode, startPosition.offsetInContainerNode(), endPosition.offsetInContainerNode()); } } setEndingSelectionWithoutValidation(startPosition, endPosition); // Handle the case where there is a typing style. if (RefPtrWillBeRawPtr<EditingStyle> typingStyle = document().frame()->selection().typingStyle()) { typingStyle->prepareToApplyAt(endPosition, EditingStyle::PreserveWritingDirection); if (!typingStyle->isEmpty()) applyStyle(typingStyle.get()); } if (!m_selectInsertedText) setEndingSelection(VisibleSelection(endingSelection().end(), endingSelection().affinity(), endingSelection().isDirectional())); }