| /* |
| * Copyright (C) 2005, 2006, 2007, 2008 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "core/editing/commands/TypingCommand.h" |
| |
| #include "core/HTMLNames.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/Element.h" |
| #include "core/dom/ElementTraversal.h" |
| #include "core/editing/EditingUtilities.h" |
| #include "core/editing/Editor.h" |
| #include "core/editing/SelectionModifier.h" |
| #include "core/editing/VisiblePosition.h" |
| #include "core/editing/VisibleUnits.h" |
| #include "core/editing/commands/BreakBlockquoteCommand.h" |
| #include "core/editing/commands/InsertLineBreakCommand.h" |
| #include "core/editing/commands/InsertParagraphSeparatorCommand.h" |
| #include "core/editing/commands/InsertTextCommand.h" |
| #include "core/editing/spellcheck/SpellChecker.h" |
| #include "core/events/BeforeTextInsertedEvent.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/html/HTMLBRElement.h" |
| #include "core/layout/LayoutObject.h" |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| TypingCommand::TypingCommand(Document& document, |
| ETypingCommand commandType, |
| const String& textToInsert, |
| Options options, |
| TextGranularity granularity, |
| TextCompositionType compositionType) |
| : CompositeEditCommand(document), |
| m_commandType(commandType), |
| m_textToInsert(textToInsert), |
| m_openForMoreTyping(true), |
| m_selectInsertedText(options & SelectInsertedText), |
| m_smartDelete(options & SmartDelete), |
| m_granularity(granularity), |
| m_compositionType(compositionType), |
| m_killRing(options & KillRing), |
| m_openedByBackwardDelete(false), |
| m_shouldRetainAutocorrectionIndicator(options & |
| RetainAutocorrectionIndicator), |
| m_shouldPreventSpellChecking(options & PreventSpellChecking) { |
| updatePreservesTypingStyle(m_commandType); |
| } |
| |
| void TypingCommand::deleteSelection(Document& document, Options options) { |
| LocalFrame* frame = document.frame(); |
| DCHECK(frame); |
| |
| if (!frame->selection().isRange()) |
| return; |
| |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(frame)) { |
| updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand, frame); |
| |
| lastTypingCommand->setShouldPreventSpellChecking(options & |
| PreventSpellChecking); |
| // InputMethodController uses this function to delete composition |
| // selection. It won't be aborted. |
| lastTypingCommand->deleteSelection(options & SmartDelete, |
| ASSERT_NO_EDITING_ABORT); |
| return; |
| } |
| |
| TypingCommand::create(document, DeleteSelection, "", options)->apply(); |
| } |
| |
| void TypingCommand::deleteKeyPressed(Document& document, |
| Options options, |
| TextGranularity granularity) { |
| if (granularity == CharacterGranularity) { |
| LocalFrame* frame = document.frame(); |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(frame)) { |
| // If the last typing command is not Delete, open a new typing command. |
| // We need to group continuous delete commands alone in a single typing |
| // command. |
| if (lastTypingCommand->commandTypeOfOpenCommand() == DeleteKey) { |
| updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand, |
| frame); |
| lastTypingCommand->setShouldPreventSpellChecking(options & |
| PreventSpellChecking); |
| EditingState editingState; |
| lastTypingCommand->deleteKeyPressed(granularity, options & KillRing, |
| &editingState); |
| return; |
| } |
| } |
| } |
| |
| TypingCommand::create(document, DeleteKey, "", options, granularity)->apply(); |
| } |
| |
| void TypingCommand::forwardDeleteKeyPressed(Document& document, |
| EditingState* editingState, |
| Options options, |
| TextGranularity granularity) { |
| // FIXME: Forward delete in TextEdit appears to open and close a new typing |
| // command. |
| if (granularity == CharacterGranularity) { |
| LocalFrame* frame = document.frame(); |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(frame)) { |
| updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand, frame); |
| lastTypingCommand->setShouldPreventSpellChecking(options & |
| PreventSpellChecking); |
| lastTypingCommand->forwardDeleteKeyPressed( |
| granularity, options & KillRing, editingState); |
| return; |
| } |
| } |
| |
| TypingCommand::create(document, ForwardDeleteKey, "", options, granularity) |
| ->apply(); |
| } |
| |
| String TypingCommand::textDataForInputEvent() const { |
| if (m_commands.isEmpty()) |
| return m_textToInsert; |
| return m_commands.last()->textDataForInputEvent(); |
| } |
| |
| void TypingCommand::updateSelectionIfDifferentFromCurrentSelection( |
| TypingCommand* typingCommand, |
| LocalFrame* frame) { |
| DCHECK(frame); |
| VisibleSelection currentSelection = frame->selection().selection(); |
| if (currentSelection == typingCommand->endingSelection()) |
| return; |
| |
| typingCommand->setStartingSelection(currentSelection); |
| typingCommand->setEndingSelection(currentSelection); |
| } |
| |
| static String dispatchBeforeTextInsertedEvent( |
| const String& text, |
| const VisibleSelection& selectionForInsertion, |
| bool insertionIsForUpdatingComposition) { |
| if (insertionIsForUpdatingComposition) |
| return text; |
| |
| String newText = text; |
| if (Node* startNode = selectionForInsertion.start().computeContainerNode()) { |
| if (rootEditableElement(*startNode)) { |
| // Send BeforeTextInsertedEvent. The event handler will update text if |
| // necessary. |
| BeforeTextInsertedEvent* evt = BeforeTextInsertedEvent::create(text); |
| rootEditableElement(*startNode)->dispatchEvent(evt); |
| newText = evt->text(); |
| } |
| } |
| return newText; |
| } |
| |
| void TypingCommand::insertText(Document& document, |
| const String& text, |
| Options options, |
| TextCompositionType composition) { |
| LocalFrame* frame = document.frame(); |
| DCHECK(frame); |
| |
| if (!text.isEmpty()) |
| document.frame()->spellChecker().updateMarkersForWordsAffectedByEditing( |
| isSpaceOrNewline(text[0])); |
| |
| insertText(document, text, frame->selection().selection(), options, |
| composition); |
| } |
| |
| // FIXME: We shouldn't need to take selectionForInsertion. It should be |
| // identical to FrameSelection's current selection. |
| void TypingCommand::insertText(Document& document, |
| const String& text, |
| const VisibleSelection& selectionForInsertion, |
| Options options, |
| TextCompositionType compositionType) { |
| LocalFrame* frame = document.frame(); |
| DCHECK(frame); |
| |
| VisibleSelection currentSelection = frame->selection().selection(); |
| |
| String newText = dispatchBeforeTextInsertedEvent( |
| text, selectionForInsertion, compositionType == TextCompositionUpdate); |
| |
| // Set the starting and ending selection appropriately if we are using a |
| // selection that is different from the current selection. In the future, we |
| // should change EditCommand to deal with custom selections in a general way |
| // that can be used by all of the commands. |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(frame)) { |
| if (lastTypingCommand->endingSelection() != selectionForInsertion) { |
| lastTypingCommand->setStartingSelection(selectionForInsertion); |
| lastTypingCommand->setEndingSelection(selectionForInsertion); |
| } |
| |
| lastTypingCommand->setCompositionType(compositionType); |
| lastTypingCommand->setShouldRetainAutocorrectionIndicator( |
| options & RetainAutocorrectionIndicator); |
| lastTypingCommand->setShouldPreventSpellChecking(options & |
| PreventSpellChecking); |
| EditingState editingState; |
| lastTypingCommand->insertText(newText, options & SelectInsertedText, |
| &editingState); |
| // Nothing to do even if the command was aborted. |
| return; |
| } |
| |
| TypingCommand* command = TypingCommand::create(document, InsertText, newText, |
| options, compositionType); |
| bool changeSelection = selectionForInsertion != currentSelection; |
| if (changeSelection) { |
| command->setStartingSelection(selectionForInsertion); |
| command->setEndingSelection(selectionForInsertion); |
| } |
| command->apply(); |
| if (changeSelection) { |
| command->setEndingSelection(currentSelection); |
| frame->selection().setSelection(currentSelection); |
| } |
| } |
| |
| bool TypingCommand::insertLineBreak(Document& document) { |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(document.frame())) { |
| lastTypingCommand->setShouldRetainAutocorrectionIndicator(false); |
| EditingState editingState; |
| lastTypingCommand->insertLineBreak(&editingState); |
| return !editingState.isAborted(); |
| } |
| |
| return TypingCommand::create(document, InsertLineBreak, "", 0)->apply(); |
| } |
| |
| bool TypingCommand::insertParagraphSeparatorInQuotedContent( |
| Document& document) { |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(document.frame())) { |
| EditingState editingState; |
| lastTypingCommand->insertParagraphSeparatorInQuotedContent(&editingState); |
| return !editingState.isAborted(); |
| } |
| |
| return TypingCommand::create(document, |
| InsertParagraphSeparatorInQuotedContent) |
| ->apply(); |
| } |
| |
| bool TypingCommand::insertParagraphSeparator(Document& document) { |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(document.frame())) { |
| lastTypingCommand->setShouldRetainAutocorrectionIndicator(false); |
| EditingState editingState; |
| lastTypingCommand->insertParagraphSeparator(&editingState); |
| return !editingState.isAborted(); |
| } |
| |
| return TypingCommand::create(document, InsertParagraphSeparator, "", 0) |
| ->apply(); |
| } |
| |
| TypingCommand* TypingCommand::lastTypingCommandIfStillOpenForTyping( |
| LocalFrame* frame) { |
| DCHECK(frame); |
| |
| CompositeEditCommand* lastEditCommand = frame->editor().lastEditCommand(); |
| if (!lastEditCommand || !lastEditCommand->isTypingCommand() || |
| !static_cast<TypingCommand*>(lastEditCommand)->isOpenForMoreTyping()) |
| return nullptr; |
| |
| return static_cast<TypingCommand*>(lastEditCommand); |
| } |
| |
| void TypingCommand::closeTyping(LocalFrame* frame) { |
| if (TypingCommand* lastTypingCommand = |
| lastTypingCommandIfStillOpenForTyping(frame)) |
| lastTypingCommand->closeTyping(); |
| } |
| |
| void TypingCommand::doApply(EditingState* editingState) { |
| if (!endingSelection().isNonOrphanedCaretOrRange()) |
| return; |
| |
| if (m_commandType == DeleteKey) { |
| if (m_commands.isEmpty()) |
| m_openedByBackwardDelete = true; |
| } |
| |
| switch (m_commandType) { |
| case DeleteSelection: |
| deleteSelection(m_smartDelete, editingState); |
| return; |
| case DeleteKey: |
| deleteKeyPressed(m_granularity, m_killRing, editingState); |
| return; |
| case ForwardDeleteKey: |
| forwardDeleteKeyPressed(m_granularity, m_killRing, editingState); |
| return; |
| case InsertLineBreak: |
| insertLineBreak(editingState); |
| return; |
| case InsertParagraphSeparator: |
| insertParagraphSeparator(editingState); |
| return; |
| case InsertParagraphSeparatorInQuotedContent: |
| insertParagraphSeparatorInQuotedContent(editingState); |
| return; |
| case InsertText: |
| insertText(m_textToInsert, m_selectInsertedText, editingState); |
| return; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| InputEvent::InputType TypingCommand::inputType() const { |
| using InputType = InputEvent::InputType; |
| |
| switch (m_commandType) { |
| // TODO(chongz): |DeleteSelection| is used by IME but we don't have |
| // direction info. |
| case DeleteSelection: |
| return InputType::DeleteContentBackward; |
| case DeleteKey: |
| if (m_compositionType != TextCompositionNone) |
| return InputType::DeleteComposedCharacterBackward; |
| return deletionInputTypeFromTextGranularity(DeleteDirection::Backward, |
| m_granularity); |
| case ForwardDeleteKey: |
| if (m_compositionType != TextCompositionNone) |
| return InputType::DeleteComposedCharacterForward; |
| return deletionInputTypeFromTextGranularity(DeleteDirection::Forward, |
| m_granularity); |
| case InsertText: |
| return InputType::InsertText; |
| case InsertLineBreak: |
| return InputType::InsertLineBreak; |
| case InsertParagraphSeparator: |
| case InsertParagraphSeparatorInQuotedContent: |
| return InputType::InsertParagraph; |
| default: |
| return InputType::None; |
| } |
| } |
| |
| void TypingCommand::typingAddedToOpenCommand( |
| ETypingCommand commandTypeForAddedTyping) { |
| LocalFrame* frame = document().frame(); |
| if (!frame) |
| return; |
| |
| updatePreservesTypingStyle(commandTypeForAddedTyping); |
| updateCommandTypeOfOpenCommand(commandTypeForAddedTyping); |
| |
| frame->editor().appliedEditing(this); |
| } |
| |
| void TypingCommand::insertText(const String& text, |
| bool selectInsertedText, |
| EditingState* editingState) { |
| if (text.isEmpty()) { |
| insertTextRunWithoutNewlines(text, selectInsertedText, editingState); |
| return; |
| } |
| // FIXME: Need to implement selectInsertedText for cases where more than one |
| // insert is involved. This requires support from insertTextRunWithoutNewlines |
| // and insertParagraphSeparator for extending an existing selection; at the |
| // moment they can either put the caret after what's inserted or select what's |
| // inserted, but there's no way to "extend selection" to include both an old |
| // selection that ends just before where we want to insert text and the newly |
| // inserted text. |
| unsigned offset = 0; |
| size_t newline; |
| while ((newline = text.find('\n', offset)) != kNotFound) { |
| if (newline > offset) { |
| const bool notSelectInsertedText = false; |
| insertTextRunWithoutNewlines(text.substring(offset, newline - offset), |
| notSelectInsertedText, editingState); |
| if (editingState->isAborted()) |
| return; |
| } |
| |
| insertParagraphSeparator(editingState); |
| if (editingState->isAborted()) |
| return; |
| |
| offset = newline + 1; |
| } |
| |
| if (!offset) { |
| insertTextRunWithoutNewlines(text, selectInsertedText, editingState); |
| return; |
| } |
| |
| if (text.length() > offset) |
| insertTextRunWithoutNewlines(text.substring(offset, text.length() - offset), |
| selectInsertedText, editingState); |
| } |
| |
| void TypingCommand::insertTextRunWithoutNewlines(const String& text, |
| bool selectInsertedText, |
| EditingState* editingState) { |
| InsertTextCommand* command = InsertTextCommand::create( |
| document(), text, selectInsertedText, |
| m_compositionType == TextCompositionNone |
| ? InsertTextCommand::RebalanceLeadingAndTrailingWhitespaces |
| : InsertTextCommand::RebalanceAllWhitespaces); |
| |
| applyCommandToComposite(command, endingSelection(), editingState); |
| if (editingState->isAborted()) |
| return; |
| typingAddedToOpenCommand(InsertText); |
| } |
| |
| static bool canAppendNewLineFeedToSelection(const VisibleSelection& selection) { |
| Element* element = selection.rootEditableElement(); |
| if (!element) |
| return false; |
| |
| BeforeTextInsertedEvent* event = |
| BeforeTextInsertedEvent::create(String("\n")); |
| element->dispatchEvent(event); |
| return event->text().length(); |
| } |
| |
| void TypingCommand::insertLineBreak(EditingState* editingState) { |
| if (!canAppendNewLineFeedToSelection(endingSelection())) |
| return; |
| |
| applyCommandToComposite(InsertLineBreakCommand::create(document()), |
| editingState); |
| if (editingState->isAborted()) |
| return; |
| typingAddedToOpenCommand(InsertLineBreak); |
| } |
| |
| void TypingCommand::insertParagraphSeparator(EditingState* editingState) { |
| if (!canAppendNewLineFeedToSelection(endingSelection())) |
| return; |
| |
| applyCommandToComposite(InsertParagraphSeparatorCommand::create(document()), |
| editingState); |
| if (editingState->isAborted()) |
| return; |
| typingAddedToOpenCommand(InsertParagraphSeparator); |
| } |
| |
| void TypingCommand::insertParagraphSeparatorInQuotedContent( |
| EditingState* editingState) { |
| // If the selection starts inside a table, just insert the paragraph separator |
| // normally Breaking the blockquote would also break apart the table, which is |
| // unecessary when inserting a newline |
| if (enclosingNodeOfType(endingSelection().start(), &isTableStructureNode)) { |
| insertParagraphSeparator(editingState); |
| return; |
| } |
| |
| applyCommandToComposite(BreakBlockquoteCommand::create(document()), |
| editingState); |
| if (editingState->isAborted()) |
| return; |
| typingAddedToOpenCommand(InsertParagraphSeparatorInQuotedContent); |
| } |
| |
| bool TypingCommand::makeEditableRootEmpty(EditingState* editingState) { |
| Element* root = endingSelection().rootEditableElement(); |
| if (!root || !root->hasChildren()) |
| return false; |
| |
| if (root->firstChild() == root->lastChild()) { |
| if (isHTMLBRElement(root->firstChild())) { |
| // If there is a single child and it could be a placeholder, leave it |
| // alone. |
| if (root->layoutObject() && root->layoutObject()->isLayoutBlockFlow()) |
| return false; |
| } |
| } |
| |
| while (Node* child = root->firstChild()) { |
| removeNode(child, editingState); |
| if (editingState->isAborted()) |
| return false; |
| } |
| |
| addBlockPlaceholderIfNeeded(root, editingState); |
| if (editingState->isAborted()) |
| return false; |
| setEndingSelection(createVisibleSelectionDeprecated( |
| Position::firstPositionInNode(root), TextAffinity::Downstream, |
| endingSelection().isDirectional())); |
| |
| return true; |
| } |
| |
| void TypingCommand::deleteKeyPressed(TextGranularity granularity, |
| bool killRing, |
| EditingState* editingState) { |
| LocalFrame* frame = document().frame(); |
| if (!frame) |
| return; |
| |
| frame->spellChecker().updateMarkersForWordsAffectedByEditing(false); |
| |
| VisibleSelection selectionToDelete; |
| VisibleSelection selectionAfterUndo; |
| |
| switch (endingSelection().getSelectionType()) { |
| case RangeSelection: |
| selectionToDelete = endingSelection(); |
| selectionAfterUndo = selectionToDelete; |
| break; |
| case CaretSelection: { |
| // After breaking out of an empty mail blockquote, we still want continue |
| // with the deletion so actual content will get deleted, and not just the |
| // quote style. |
| bool breakOutResult = |
| breakOutOfEmptyMailBlockquotedParagraph(editingState); |
| if (editingState->isAborted()) |
| return; |
| if (breakOutResult) |
| typingAddedToOpenCommand(DeleteKey); |
| |
| m_smartDelete = false; |
| |
| SelectionModifier selectionModifier(*frame, endingSelection()); |
| selectionModifier.modify(FrameSelection::AlterationExtend, |
| DirectionBackward, granularity); |
| if (killRing && selectionModifier.selection().isCaret() && |
| granularity != CharacterGranularity) |
| selectionModifier.modify(FrameSelection::AlterationExtend, |
| DirectionBackward, CharacterGranularity); |
| |
| VisiblePosition visibleStart(endingSelection().visibleStartDeprecated()); |
| if (previousPositionOf(visibleStart, CannotCrossEditingBoundary) |
| .isNull()) { |
| // When the caret is at the start of the editable area in an empty list |
| // item, break out of the list item. |
| bool breakOutOfEmptyListItemResult = |
| breakOutOfEmptyListItem(editingState); |
| if (editingState->isAborted()) |
| return; |
| if (breakOutOfEmptyListItemResult) { |
| typingAddedToOpenCommand(DeleteKey); |
| return; |
| } |
| // When there are no visible positions in the editing root, delete its |
| // entire contents. |
| if (nextPositionOf(visibleStart, CannotCrossEditingBoundary).isNull() && |
| makeEditableRootEmpty(editingState)) { |
| typingAddedToOpenCommand(DeleteKey); |
| return; |
| } |
| if (editingState->isAborted()) |
| return; |
| } |
| |
| // If we have a caret selection at the beginning of a cell, we have |
| // nothing to do. |
| Node* enclosingTableCell = |
| enclosingNodeOfType(visibleStart.deepEquivalent(), &isTableCell); |
| if (enclosingTableCell && |
| visibleStart.deepEquivalent() == |
| VisiblePosition::firstPositionInNode(enclosingTableCell) |
| .deepEquivalent()) |
| return; |
| |
| // If the caret is at the start of a paragraph after a table, move content |
| // into the last table cell. |
| if (isStartOfParagraphDeprecated(visibleStart) && |
| tableElementJustBefore( |
| previousPositionOf(visibleStart, CannotCrossEditingBoundary))) { |
| // Unless the caret is just before a table. We don't want to move a |
| // table into the last table cell. |
| if (tableElementJustAfter(visibleStart)) |
| return; |
| // Extend the selection backward into the last cell, then deletion will |
| // handle the move. |
| selectionModifier.modify(FrameSelection::AlterationExtend, |
| DirectionBackward, granularity); |
| // If the caret is just after a table, select the table and don't delete |
| // anything. |
| } else if (Element* table = tableElementJustBefore(visibleStart)) { |
| setEndingSelection(createVisibleSelectionDeprecated( |
| Position::beforeNode(table), endingSelection().start(), |
| TextAffinity::Downstream, endingSelection().isDirectional())); |
| typingAddedToOpenCommand(DeleteKey); |
| return; |
| } |
| |
| selectionToDelete = selectionModifier.selection(); |
| |
| if (granularity == CharacterGranularity && |
| selectionToDelete.end().computeContainerNode() == |
| selectionToDelete.start().computeContainerNode() && |
| selectionToDelete.end().computeOffsetInContainerNode() - |
| selectionToDelete.start().computeOffsetInContainerNode() > |
| 1) { |
| // If there are multiple Unicode code points to be deleted, adjust the |
| // range to match platform conventions. |
| selectionToDelete.setWithoutValidation( |
| selectionToDelete.end(), |
| previousPositionOf(selectionToDelete.end(), |
| PositionMoveType::BackwardDeletion)); |
| } |
| |
| 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. |
| selectionAfterUndo.setWithoutValidation(startingSelection().end(), |
| selectionToDelete.extent()); |
| } |
| break; |
| } |
| case NoSelection: |
| NOTREACHED(); |
| break; |
| } |
| |
| DCHECK(!selectionToDelete.isNone()); |
| if (selectionToDelete.isNone()) |
| return; |
| |
| if (selectionToDelete.isCaret()) |
| return; |
| |
| if (killRing) |
| frame->editor().addToKillRing( |
| selectionToDelete.toNormalizedEphemeralRange()); |
| // On Mac, 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 (frame->editor().behavior().shouldUndoOfDeleteSelectText() && |
| m_openedByBackwardDelete) |
| setStartingSelection(selectionAfterUndo); |
| CompositeEditCommand::deleteSelection(selectionToDelete, editingState, |
| m_smartDelete); |
| if (editingState->isAborted()) |
| return; |
| setSmartDelete(false); |
| typingAddedToOpenCommand(DeleteKey); |
| } |
| |
| void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, |
| bool killRing, |
| EditingState* editingState) { |
| LocalFrame* frame = document().frame(); |
| if (!frame) |
| return; |
| |
| frame->spellChecker().updateMarkersForWordsAffectedByEditing(false); |
| |
| VisibleSelection selectionToDelete; |
| VisibleSelection selectionAfterUndo; |
| |
| switch (endingSelection().getSelectionType()) { |
| 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. |
| SelectionModifier selectionModifier(*frame, endingSelection()); |
| selectionModifier.modify(FrameSelection::AlterationExtend, |
| DirectionForward, granularity); |
| if (killRing && selectionModifier.selection().isCaret() && |
| granularity != CharacterGranularity) |
| selectionModifier.modify(FrameSelection::AlterationExtend, |
| DirectionForward, CharacterGranularity); |
| |
| Position downstreamEnd = |
| mostForwardCaretPosition(endingSelection().end()); |
| VisiblePosition visibleEnd = endingSelection().visibleEndDeprecated(); |
| Node* enclosingTableCell = |
| enclosingNodeOfType(visibleEnd.deepEquivalent(), &isTableCell); |
| if (enclosingTableCell && |
| visibleEnd.deepEquivalent() == |
| VisiblePosition::lastPositionInNode(enclosingTableCell) |
| .deepEquivalent()) |
| return; |
| if (visibleEnd.deepEquivalent() == |
| endOfParagraphDeprecated(visibleEnd).deepEquivalent()) |
| downstreamEnd = mostForwardCaretPosition( |
| nextPositionOf(visibleEnd, CannotCrossEditingBoundary) |
| .deepEquivalent()); |
| // When deleting tables: Select the table first, then perform the deletion |
| if (isDisplayInsideTable(downstreamEnd.computeContainerNode()) && |
| downstreamEnd.computeOffsetInContainerNode() <= |
| caretMinOffset(downstreamEnd.computeContainerNode())) { |
| setEndingSelection(createVisibleSelectionDeprecated( |
| endingSelection().end(), |
| Position::afterNode(downstreamEnd.computeContainerNode()), |
| TextAffinity::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 && |
| selectionModifier.selection().isCaret() && |
| isEndOfParagraphDeprecated( |
| selectionModifier.selection().visibleEndDeprecated())) |
| selectionModifier.modify(FrameSelection::AlterationExtend, |
| DirectionForward, CharacterGranularity); |
| |
| selectionToDelete = selectionModifier.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.computeContainerNode() != |
| selectionToDelete.end().computeContainerNode()) { |
| extent = selectionToDelete.extent(); |
| } else { |
| int extraCharacters; |
| if (selectionToDelete.start().computeContainerNode() == |
| selectionToDelete.end().computeContainerNode()) |
| extraCharacters = |
| selectionToDelete.end().computeOffsetInContainerNode() - |
| selectionToDelete.start().computeOffsetInContainerNode(); |
| else |
| extraCharacters = |
| selectionToDelete.end().computeOffsetInContainerNode(); |
| extent = |
| Position(extent.computeContainerNode(), |
| extent.computeOffsetInContainerNode() + extraCharacters); |
| } |
| selectionAfterUndo.setWithoutValidation(startingSelection().start(), |
| extent); |
| } |
| break; |
| } |
| case NoSelection: |
| NOTREACHED(); |
| break; |
| } |
| |
| DCHECK(!selectionToDelete.isNone()); |
| if (selectionToDelete.isNone()) |
| return; |
| |
| if (selectionToDelete.isCaret()) |
| return; |
| |
| if (killRing) |
| frame->editor().addToKillRing( |
| selectionToDelete.toNormalizedEphemeralRange()); |
| // Make undo select what was deleted on Mac alone |
| if (frame->editor().behavior().shouldUndoOfDeleteSelectText()) |
| setStartingSelection(selectionAfterUndo); |
| CompositeEditCommand::deleteSelection(selectionToDelete, editingState, |
| m_smartDelete); |
| if (editingState->isAborted()) |
| return; |
| setSmartDelete(false); |
| typingAddedToOpenCommand(ForwardDeleteKey); |
| } |
| |
| void TypingCommand::deleteSelection(bool smartDelete, |
| EditingState* editingState) { |
| CompositeEditCommand::deleteSelection(editingState, smartDelete); |
| if (editingState->isAborted()) |
| return; |
| typingAddedToOpenCommand(DeleteSelection); |
| } |
| |
| void TypingCommand::updatePreservesTypingStyle(ETypingCommand commandType) { |
| switch (commandType) { |
| case DeleteSelection: |
| case DeleteKey: |
| case ForwardDeleteKey: |
| case InsertParagraphSeparator: |
| case InsertLineBreak: |
| m_preservesTypingStyle = true; |
| return; |
| case InsertParagraphSeparatorInQuotedContent: |
| case InsertText: |
| m_preservesTypingStyle = false; |
| return; |
| } |
| NOTREACHED(); |
| m_preservesTypingStyle = false; |
| } |
| |
| bool TypingCommand::isTypingCommand() const { |
| return true; |
| } |
| |
| } // namespace blink |