| /* |
| * 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 "third_party/blink/renderer/core/editing/commands/typing_command.h" |
| |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/element_traversal.h" |
| #include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h" |
| #include "third_party/blink/renderer/core/editing/commands/break_blockquote_command.h" |
| #include "third_party/blink/renderer/core/editing/commands/delete_selection_command.h" |
| #include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h" |
| #include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h" |
| #include "third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h" |
| #include "third_party/blink/renderer/core/editing/commands/insert_line_break_command.h" |
| #include "third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h" |
| #include "third_party/blink/renderer/core/editing/commands/insert_text_command.h" |
| #include "third_party/blink/renderer/core/editing/editing_behavior.h" |
| #include "third_party/blink/renderer/core/editing/editing_utilities.h" |
| #include "third_party/blink/renderer/core/editing/editor.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/frame_selection.h" |
| #include "third_party/blink/renderer/core/editing/ime/input_method_controller.h" |
| #include "third_party/blink/renderer/core/editing/plain_text_range.h" |
| #include "third_party/blink/renderer/core/editing/selection_modifier.h" |
| #include "third_party/blink/renderer/core/editing/selection_template.h" |
| #include "third_party/blink/renderer/core/editing/visible_position.h" |
| #include "third_party/blink/renderer/core/editing/visible_selection.h" |
| #include "third_party/blink/renderer/core/editing/visible_units.h" |
| #include "third_party/blink/renderer/core/events/before_text_inserted_event.h" |
| #include "third_party/blink/renderer/core/events/text_event.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/html/html_br_element.h" |
| #include "third_party/blink/renderer/core/html_names.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| bool IsValidDocument(const Document& document) { |
| return document.GetFrame() && document.GetFrame()->GetDocument() == &document; |
| } |
| |
| String DispatchBeforeTextInsertedEvent(const String& text, |
| const SelectionInDOMTree& selection, |
| EditingState* editing_state) { |
| // We use SelectionForUndoStep because it is resilient to DOM |
| // mutation. |
| const SelectionForUndoStep& selection_as_undo_step = |
| SelectionForUndoStep::From(selection); |
| Node* start_node = selection_as_undo_step.Start().ComputeContainerNode(); |
| if (!start_node || !RootEditableElement(*start_node)) |
| return text; |
| |
| // Send BeforeTextInsertedEvent. The event handler will update text if |
| // necessary. |
| const Document& document = start_node->GetDocument(); |
| auto* evt = MakeGarbageCollected<BeforeTextInsertedEvent>(text); |
| RootEditableElement(*start_node)->DispatchEvent(*evt); |
| if (IsValidDocument(document) && selection_as_undo_step.IsValidFor(document)) |
| return evt->GetText(); |
| // editing/inserting/webkitBeforeTextInserted-removes-frame.html |
| // and |
| // editing/inserting/webkitBeforeTextInserted-disconnects-selection.html |
| // reaches here. |
| editing_state->Abort(); |
| return String(); |
| } |
| |
| DispatchEventResult DispatchTextInputEvent(LocalFrame* frame, |
| const String& text, |
| EditingState* editing_state) { |
| const Document& document = *frame->GetDocument(); |
| Element* target = document.FocusedElement(); |
| if (!target) |
| return DispatchEventResult::kCanceledBeforeDispatch; |
| |
| // Send TextInputEvent. Unlike BeforeTextInsertedEvent, there is no need to |
| // update text for TextInputEvent as it doesn't have the API to modify text. |
| TextEvent* event = TextEvent::Create(frame->DomWindow(), text, |
| kTextEventInputIncrementalInsertion); |
| event->SetUnderlyingEvent(nullptr); |
| DispatchEventResult result = target->DispatchEvent(*event); |
| if (IsValidDocument(document)) |
| return result; |
| // editing/inserting/insert-text-remove-iframe-on-textInput-event.html |
| // reaches here. |
| editing_state->Abort(); |
| return result; |
| } |
| |
| PlainTextRange GetSelectionOffsets(const SelectionInDOMTree& selection) { |
| const EphemeralRange range = selection.ComputeRange(); |
| if (range.IsNull()) |
| return PlainTextRange(); |
| ContainerNode* const editable = |
| RootEditableElementOrTreeScopeRootNodeOf(selection.Base()); |
| DCHECK(editable); |
| return PlainTextRange::Create(*editable, range); |
| } |
| |
| SelectionInDOMTree CreateSelection(const wtf_size_t start, |
| const wtf_size_t end, |
| Element* element) { |
| const EphemeralRange& start_range = |
| PlainTextRange(0, static_cast<int>(start)).CreateRange(*element); |
| DCHECK(start_range.IsNotNull()); |
| const Position& start_position = start_range.EndPosition(); |
| |
| const EphemeralRange& end_range = |
| PlainTextRange(0, static_cast<int>(end)).CreateRange(*element); |
| DCHECK(end_range.IsNotNull()); |
| const Position& end_position = end_range.EndPosition(); |
| |
| const SelectionInDOMTree& selection = |
| SelectionInDOMTree::Builder() |
| .SetBaseAndExtent(start_position, end_position) |
| .Build(); |
| return selection; |
| } |
| |
| bool CanAppendNewLineFeedToSelection(const SelectionInDOMTree& selection, |
| EditingState* editing_state) { |
| // We use SelectionForUndoStep because it is resilient to DOM |
| // mutation. |
| const SelectionForUndoStep& selection_as_undo_step = |
| SelectionForUndoStep::From(selection); |
| Element* element = selection_as_undo_step.RootEditableElement(); |
| if (!element) |
| return false; |
| |
| const Document& document = element->GetDocument(); |
| auto* event = MakeGarbageCollected<BeforeTextInsertedEvent>(String("\n")); |
| element->DispatchEvent(*event); |
| // event may invalidate frame or selection |
| if (IsValidDocument(document) && selection_as_undo_step.IsValidFor(document)) |
| return event->GetText().length(); |
| // editing/inserting/webkitBeforeTextInserted-removes-frame.html |
| // and |
| // editing/inserting/webkitBeforeTextInserted-disconnects-selection.html |
| // reaches here. |
| editing_state->Abort(); |
| return false; |
| } |
| |
| // Example: <div><img style="display:block">|<br></p> |
| // See "editing/deleting/delete_after_block_image.html" |
| Position AfterBlockIfBeforeAnonymousPlaceholder(const Position& position) { |
| if (!position.IsBeforeAnchor()) |
| return Position(); |
| const LayoutObject* const layout_object = |
| position.AnchorNode()->GetLayoutObject(); |
| if (!layout_object || !layout_object->IsBR() || |
| layout_object->NextSibling() || layout_object->PreviousSibling()) |
| return Position(); |
| const LayoutObject* const parent = layout_object->Parent(); |
| if (!parent || !parent->IsAnonymous()) |
| return Position(); |
| const LayoutObject* const previous = parent->PreviousSibling(); |
| if (!previous || !previous->NonPseudoNode()) |
| return Position(); |
| return Position::AfterNode(*previous->NonPseudoNode()); |
| } |
| |
| } // anonymous namespace |
| |
| TypingCommand::TypingCommand(Document& document, |
| CommandType command_type, |
| const String& text_to_insert, |
| Options options, |
| TextGranularity granularity, |
| TextCompositionType composition_type) |
| : CompositeEditCommand(document), |
| command_type_(command_type), |
| text_to_insert_(text_to_insert), |
| open_for_more_typing_(true), |
| select_inserted_text_(options & kSelectInsertedText), |
| smart_delete_(options & kSmartDelete), |
| granularity_(granularity), |
| composition_type_(composition_type), |
| kill_ring_(options & kKillRing), |
| opened_by_backward_delete_(false) { |
| UpdatePreservesTypingStyle(command_type_); |
| } |
| |
| void TypingCommand::DeleteSelection(Document& document, Options options) { |
| LocalFrame* frame = document.GetFrame(); |
| DCHECK(frame); |
| |
| if (!frame->Selection() |
| .ComputeVisibleSelectionInDOMTreeDeprecated() |
| .IsRange()) |
| return; |
| |
| if (TypingCommand* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(frame)) { |
| UpdateSelectionIfDifferentFromCurrentSelection(last_typing_command, frame); |
| |
| // InputMethodController uses this function to delete composition |
| // selection. It won't be aborted. |
| last_typing_command->DeleteSelection(options & kSmartDelete, |
| ASSERT_NO_EDITING_ABORT); |
| return; |
| } |
| |
| MakeGarbageCollected<TypingCommand>(document, kDeleteSelection, "", options) |
| ->Apply(); |
| } |
| |
| void TypingCommand::DeleteSelectionIfRange( |
| const SelectionForUndoStep& selection, |
| EditingState* editing_state) { |
| if (!selection.IsRange()) |
| return; |
| // Although the 'selection' to delete is indeed a Range, it may have been |
| // built from a Caret selection; in that case we don't want to expand so that |
| // the table structure is deleted as well. |
| bool expand_for_special = EndingSelection().IsRange(); |
| ApplyCommandToComposite( |
| MakeGarbageCollected<DeleteSelectionCommand>( |
| selection, DeleteSelectionOptions::Builder() |
| .SetSmartDelete(smart_delete_) |
| .SetMergeBlocksAfterDelete(true) |
| .SetExpandForSpecialElements(expand_for_special) |
| .SetSanitizeMarkup(true) |
| .Build()), |
| editing_state); |
| } |
| |
| void TypingCommand::DeleteKeyPressed(Document& document, |
| Options options, |
| TextGranularity granularity) { |
| if (granularity == TextGranularity::kCharacter) { |
| LocalFrame* frame = document.GetFrame(); |
| if (TypingCommand* last_typing_command = |
| 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 (last_typing_command->CommandTypeOfOpenCommand() == kDeleteKey) { |
| UpdateSelectionIfDifferentFromCurrentSelection(last_typing_command, |
| frame); |
| EditingState editing_state; |
| last_typing_command->DeleteKeyPressed(granularity, options & kKillRing, |
| &editing_state); |
| return; |
| } |
| } |
| } |
| |
| MakeGarbageCollected<TypingCommand>(document, kDeleteKey, "", options, |
| granularity) |
| ->Apply(); |
| } |
| |
| void TypingCommand::ForwardDeleteKeyPressed(Document& document, |
| EditingState* editing_state, |
| Options options, |
| TextGranularity granularity) { |
| // FIXME: Forward delete in TextEdit appears to open and close a new typing |
| // command. |
| if (granularity == TextGranularity::kCharacter) { |
| LocalFrame* frame = document.GetFrame(); |
| if (TypingCommand* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(frame)) { |
| UpdateSelectionIfDifferentFromCurrentSelection(last_typing_command, |
| frame); |
| last_typing_command->ForwardDeleteKeyPressed( |
| granularity, options & kKillRing, editing_state); |
| return; |
| } |
| } |
| |
| MakeGarbageCollected<TypingCommand>(document, kForwardDeleteKey, "", options, |
| granularity) |
| ->Apply(); |
| } |
| |
| String TypingCommand::TextDataForInputEvent() const { |
| if (commands_.IsEmpty() || IsIncrementalInsertion()) |
| return text_to_insert_; |
| return commands_.back()->TextDataForInputEvent(); |
| } |
| |
| void TypingCommand::UpdateSelectionIfDifferentFromCurrentSelection( |
| TypingCommand* typing_command, |
| LocalFrame* frame) { |
| DCHECK(frame); |
| const SelectionInDOMTree& current_selection = |
| frame->Selection().GetSelectionInDOMTree(); |
| if (current_selection == typing_command->EndingSelection().AsSelection()) |
| return; |
| |
| typing_command->SetStartingSelection( |
| SelectionForUndoStep::From(current_selection)); |
| typing_command->SetEndingSelection( |
| SelectionForUndoStep::From(current_selection)); |
| } |
| |
| void TypingCommand::InsertText(Document& document, |
| const String& text, |
| Options options, |
| TextCompositionType composition, |
| const bool is_incremental_insertion) { |
| LocalFrame* frame = document.GetFrame(); |
| DCHECK(frame); |
| EditingState editing_state; |
| InsertText(document, text, frame->Selection().GetSelectionInDOMTree(), |
| options, &editing_state, composition, is_incremental_insertion); |
| } |
| |
| void TypingCommand::AdjustSelectionAfterIncrementalInsertion( |
| LocalFrame* frame, |
| const wtf_size_t selection_start, |
| const wtf_size_t text_length, |
| EditingState* editing_state) { |
| if (!IsIncrementalInsertion()) |
| return; |
| |
| // TODO(editing-dev): The use of UpdateStyleAndLayout |
| // needs to be audited. see http://crbug.com/590369 for more details. |
| frame->GetDocument()->UpdateStyleAndLayout(DocumentUpdateReason::kEditing); |
| |
| Element* element = frame->Selection() |
| .ComputeVisibleSelectionInDOMTreeDeprecated() |
| .RootEditableElement(); |
| |
| // TODO(editing-dev): The text insertion should probably always leave the |
| // selection in an editable region, but we know of at least one case where it |
| // doesn't (see test case in crbug.com/767599). Return early in this case to |
| // avoid a crash. |
| if (!element) { |
| editing_state->Abort(); |
| return; |
| } |
| |
| const wtf_size_t new_end = selection_start + text_length; |
| const SelectionInDOMTree& selection = |
| CreateSelection(new_end, new_end, element); |
| SetEndingSelection(SelectionForUndoStep::From(selection)); |
| } |
| |
| // 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 SelectionInDOMTree& passed_selection_for_insertion, |
| Options options, |
| EditingState* editing_state, |
| TextCompositionType composition_type, |
| const bool is_incremental_insertion, |
| InputEvent::InputType input_type) { |
| DCHECK(!document.NeedsLayoutTreeUpdate()); |
| LocalFrame* frame = document.GetFrame(); |
| DCHECK(frame); |
| |
| // We use SelectionForUndoStep because it is resilient to DOM |
| // mutation. |
| const SelectionForUndoStep& passed_selection_for_insertion_as_undo_step = |
| SelectionForUndoStep::From(passed_selection_for_insertion); |
| |
| String new_text = text; |
| if (composition_type != kTextCompositionUpdate) { |
| new_text = DispatchBeforeTextInsertedEvent( |
| text, passed_selection_for_insertion, editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| ABORT_EDITING_COMMAND_IF( |
| !passed_selection_for_insertion_as_undo_step.IsValidFor(document)); |
| } |
| |
| if (composition_type == kTextCompositionConfirm) { |
| if (DispatchTextInputEvent(frame, new_text, editing_state) != |
| DispatchEventResult::kNotCanceled) |
| return; |
| // event handler might destroy document. |
| if (editing_state->IsAborted()) |
| return; |
| // editing/inserting/insert-text-nodes-disconnect-on-textinput-event.html |
| // hits true for ABORT_EDITING_COMMAND_IF macro. |
| ABORT_EDITING_COMMAND_IF( |
| !passed_selection_for_insertion_as_undo_step.IsValidFor(document)); |
| } |
| |
| // Do nothing if no need to delete and insert. |
| if (passed_selection_for_insertion_as_undo_step.IsCaret() && |
| new_text.IsEmpty()) |
| return; |
| |
| // TODO(editing-dev): The use of UpdateStyleAndLayout |
| // needs to be audited. see http://crbug.com/590369 for more details. |
| document.UpdateStyleAndLayout(DocumentUpdateReason::kEditing); |
| |
| const PlainTextRange selection_offsets = GetSelectionOffsets( |
| passed_selection_for_insertion_as_undo_step.AsSelection()); |
| if (selection_offsets.IsNull()) |
| return; |
| const wtf_size_t selection_start = selection_offsets.Start(); |
| |
| // 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* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(frame)) { |
| if (last_typing_command->EndingSelection() != |
| passed_selection_for_insertion_as_undo_step) { |
| last_typing_command->SetStartingSelection( |
| passed_selection_for_insertion_as_undo_step); |
| last_typing_command->SetEndingSelection( |
| passed_selection_for_insertion_as_undo_step); |
| } |
| |
| last_typing_command->SetCompositionType(composition_type); |
| last_typing_command->is_incremental_insertion_ = is_incremental_insertion; |
| last_typing_command->selection_start_ = selection_start; |
| last_typing_command->input_type_ = input_type; |
| |
| EventQueueScope event_queue_scope; |
| last_typing_command->InsertTextInternal( |
| new_text, options & kSelectInsertedText, editing_state); |
| return; |
| } |
| |
| TypingCommand* command = MakeGarbageCollected<TypingCommand>( |
| document, kInsertText, new_text, options, TextGranularity::kCharacter, |
| composition_type); |
| const SelectionInDOMTree& current_selection = |
| frame->Selection().GetSelectionInDOMTree(); |
| bool change_selection = |
| current_selection != |
| passed_selection_for_insertion_as_undo_step.AsSelection(); |
| if (change_selection) { |
| command->SetStartingSelection(passed_selection_for_insertion_as_undo_step); |
| command->SetEndingSelection(passed_selection_for_insertion_as_undo_step); |
| } |
| command->is_incremental_insertion_ = is_incremental_insertion; |
| command->selection_start_ = selection_start; |
| command->input_type_ = input_type; |
| ABORT_EDITING_COMMAND_IF(!command->Apply()); |
| |
| if (change_selection) { |
| const SelectionInDOMTree& current_selection_as_dom = |
| frame->Selection().GetSelectionInDOMTree(); |
| command->SetEndingSelection( |
| SelectionForUndoStep::From(current_selection_as_dom)); |
| frame->Selection().SetSelection( |
| current_selection_as_dom, |
| SetSelectionOptions::Builder() |
| .SetIsDirectional(frame->Selection().IsDirectional()) |
| .Build()); |
| } |
| } |
| |
| bool TypingCommand::InsertLineBreak(Document& document) { |
| if (TypingCommand* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(document.GetFrame())) { |
| EditingState editing_state; |
| EventQueueScope event_queue_scope; |
| last_typing_command->InsertLineBreak(&editing_state); |
| return !editing_state.IsAborted(); |
| } |
| |
| return MakeGarbageCollected<TypingCommand>(document, kInsertLineBreak, "", 0) |
| ->Apply(); |
| } |
| |
| bool TypingCommand::InsertParagraphSeparatorInQuotedContent( |
| Document& document) { |
| if (TypingCommand* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(document.GetFrame())) { |
| EditingState editing_state; |
| EventQueueScope event_queue_scope; |
| last_typing_command->InsertParagraphSeparatorInQuotedContent( |
| &editing_state); |
| return !editing_state.IsAborted(); |
| } |
| |
| return MakeGarbageCollected<TypingCommand>( |
| document, kInsertParagraphSeparatorInQuotedContent) |
| ->Apply(); |
| } |
| |
| bool TypingCommand::InsertParagraphSeparator(Document& document) { |
| if (TypingCommand* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(document.GetFrame())) { |
| EditingState editing_state; |
| EventQueueScope event_queue_scope; |
| last_typing_command->InsertParagraphSeparator(&editing_state); |
| return !editing_state.IsAborted(); |
| } |
| |
| return MakeGarbageCollected<TypingCommand>(document, |
| kInsertParagraphSeparator, "", 0) |
| ->Apply(); |
| } |
| |
| TypingCommand* TypingCommand::LastTypingCommandIfStillOpenForTyping( |
| LocalFrame* frame) { |
| DCHECK(frame); |
| |
| CompositeEditCommand* last_edit_command = |
| frame->GetEditor().LastEditCommand(); |
| if (!last_edit_command || !last_edit_command->IsTypingCommand() || |
| !static_cast<TypingCommand*>(last_edit_command)->IsOpenForMoreTyping()) |
| return nullptr; |
| |
| return static_cast<TypingCommand*>(last_edit_command); |
| } |
| |
| void TypingCommand::CloseTyping(LocalFrame* frame) { |
| if (TypingCommand* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(frame)) |
| last_typing_command->CloseTyping(); |
| } |
| |
| void TypingCommand::CloseTypingIfNeeded(LocalFrame* frame) { |
| if (frame->GetDocument()->IsRunningExecCommand() || |
| frame->GetInputMethodController().HasComposition()) |
| return; |
| if (TypingCommand* last_typing_command = |
| LastTypingCommandIfStillOpenForTyping(frame)) |
| last_typing_command->CloseTyping(); |
| } |
| |
| void TypingCommand::DoApply(EditingState* editing_state) { |
| if (EndingSelection().IsNone() || |
| !EndingSelection().IsValidFor(GetDocument())) |
| return; |
| |
| if (command_type_ == kDeleteKey) { |
| if (commands_.IsEmpty()) |
| opened_by_backward_delete_ = true; |
| } |
| |
| switch (command_type_) { |
| case kDeleteSelection: |
| DeleteSelection(smart_delete_, editing_state); |
| return; |
| case kDeleteKey: |
| DeleteKeyPressed(granularity_, kill_ring_, editing_state); |
| return; |
| case kForwardDeleteKey: |
| ForwardDeleteKeyPressed(granularity_, kill_ring_, editing_state); |
| return; |
| case kInsertLineBreak: |
| InsertLineBreak(editing_state); |
| return; |
| case kInsertParagraphSeparator: |
| InsertParagraphSeparator(editing_state); |
| return; |
| case kInsertParagraphSeparatorInQuotedContent: |
| InsertParagraphSeparatorInQuotedContent(editing_state); |
| return; |
| case kInsertText: |
| InsertTextInternal(text_to_insert_, select_inserted_text_, editing_state); |
| return; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| InputEvent::InputType TypingCommand::GetInputType() const { |
| using InputType = InputEvent::InputType; |
| |
| if (composition_type_ != kTextCompositionNone) |
| return InputType::kInsertCompositionText; |
| |
| if (input_type_ != InputType::kNone) |
| return input_type_; |
| |
| switch (command_type_) { |
| // TODO(editing-dev): |DeleteSelection| is used by IME but we don't have |
| // direction info. |
| case kDeleteSelection: |
| return InputType::kDeleteContentBackward; |
| case kDeleteKey: |
| return DeletionInputTypeFromTextGranularity(DeleteDirection::kBackward, |
| granularity_); |
| case kForwardDeleteKey: |
| return DeletionInputTypeFromTextGranularity(DeleteDirection::kForward, |
| granularity_); |
| case kInsertText: |
| return InputType::kInsertText; |
| case kInsertLineBreak: |
| return InputType::kInsertLineBreak; |
| case kInsertParagraphSeparator: |
| case kInsertParagraphSeparatorInQuotedContent: |
| return InputType::kInsertParagraph; |
| default: |
| return InputType::kNone; |
| } |
| } |
| |
| void TypingCommand::TypingAddedToOpenCommand( |
| CommandType command_type_for_added_typing) { |
| LocalFrame* frame = GetDocument().GetFrame(); |
| if (!frame) |
| return; |
| |
| UpdatePreservesTypingStyle(command_type_for_added_typing); |
| UpdateCommandTypeOfOpenCommand(command_type_for_added_typing); |
| |
| AppliedEditing(); |
| } |
| |
| void TypingCommand::InsertTextInternal(const String& text, |
| bool select_inserted_text, |
| EditingState* editing_state) { |
| text_to_insert_ = text; |
| |
| if (text.IsEmpty()) { |
| InsertTextRunWithoutNewlines(text, editing_state); |
| return; |
| } |
| wtf_size_t selection_start = selection_start_; |
| unsigned offset = 0; |
| wtf_size_t newline; |
| while ((newline = text.find('\n', offset)) != kNotFound) { |
| if (newline > offset) { |
| const wtf_size_t insertion_length = newline - offset; |
| InsertTextRunWithoutNewlines(text.Substring(offset, insertion_length), |
| editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| |
| AdjustSelectionAfterIncrementalInsertion(GetDocument().GetFrame(), |
| selection_start, |
| insertion_length, editing_state); |
| selection_start += insertion_length; |
| } |
| |
| InsertParagraphSeparator(editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| |
| offset = newline + 1; |
| ++selection_start; |
| } |
| |
| if (text.length() > offset) { |
| const wtf_size_t insertion_length = text.length() - offset; |
| InsertTextRunWithoutNewlines(text.Substring(offset, insertion_length), |
| editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| |
| AdjustSelectionAfterIncrementalInsertion(GetDocument().GetFrame(), |
| selection_start, insertion_length, |
| editing_state); |
| } |
| |
| if (!select_inserted_text) |
| return; |
| |
| // If the caller wants the newly-inserted text to be selected, we select from |
| // the plain text offset corresponding to the beginning of the range (possibly |
| // collapsed) being replaced by the text insert, to wherever the selection was |
| // left after the final run of text was inserted. |
| ContainerNode* const editable = |
| RootEditableElementOrTreeScopeRootNodeOf(EndingSelection().Base()); |
| |
| const EphemeralRange new_selection_start_collapsed_range = |
| PlainTextRange(selection_start_, selection_start_).CreateRange(*editable); |
| const Position current_selection_end = EndingSelection().End(); |
| |
| const SelectionInDOMTree& new_selection = |
| SelectionInDOMTree::Builder() |
| .SetBaseAndExtent(new_selection_start_collapsed_range.StartPosition(), |
| current_selection_end) |
| .Build(); |
| |
| SetEndingSelection(SelectionForUndoStep::From(new_selection)); |
| } |
| |
| void TypingCommand::InsertTextRunWithoutNewlines(const String& text, |
| EditingState* editing_state) { |
| CompositeEditCommand* command; |
| if (IsIncrementalInsertion()) { |
| command = MakeGarbageCollected<InsertIncrementalTextCommand>( |
| GetDocument(), text, |
| composition_type_ == kTextCompositionNone |
| ? InsertIncrementalTextCommand:: |
| kRebalanceLeadingAndTrailingWhitespaces |
| : InsertIncrementalTextCommand::kRebalanceAllWhitespaces); |
| } else { |
| command = MakeGarbageCollected<InsertTextCommand>( |
| GetDocument(), text, |
| composition_type_ == kTextCompositionNone |
| ? InsertTextCommand::kRebalanceLeadingAndTrailingWhitespaces |
| : InsertTextCommand::kRebalanceAllWhitespaces); |
| } |
| |
| command->SetStartingSelection(EndingSelection()); |
| command->SetEndingSelection(EndingSelection()); |
| ApplyCommandToComposite(command, editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| |
| TypingAddedToOpenCommand(kInsertText); |
| } |
| |
| void TypingCommand::InsertLineBreak(EditingState* editing_state) { |
| if (!CanAppendNewLineFeedToSelection(EndingSelection().AsSelection(), |
| editing_state)) |
| return; |
| |
| ApplyCommandToComposite( |
| MakeGarbageCollected<InsertLineBreakCommand>(GetDocument()), |
| editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| TypingAddedToOpenCommand(kInsertLineBreak); |
| } |
| |
| void TypingCommand::InsertParagraphSeparator(EditingState* editing_state) { |
| if (!CanAppendNewLineFeedToSelection(EndingSelection().AsSelection(), |
| editing_state)) |
| return; |
| |
| ApplyCommandToComposite( |
| MakeGarbageCollected<InsertParagraphSeparatorCommand>(GetDocument()), |
| editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| TypingAddedToOpenCommand(kInsertParagraphSeparator); |
| } |
| |
| void TypingCommand::InsertParagraphSeparatorInQuotedContent( |
| EditingState* editing_state) { |
| // 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(editing_state); |
| return; |
| } |
| |
| ApplyCommandToComposite( |
| MakeGarbageCollected<BreakBlockquoteCommand>(GetDocument()), |
| editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| TypingAddedToOpenCommand(kInsertParagraphSeparatorInQuotedContent); |
| } |
| |
| bool TypingCommand::MakeEditableRootEmpty(EditingState* editing_state) { |
| DCHECK(!GetDocument().NeedsLayoutTreeUpdate()); |
| Element* root = RootEditableElementOf(EndingSelection().Base()); |
| if (!root || !root->HasChildren()) |
| return false; |
| |
| if (root->firstChild() == root->lastChild()) { |
| if (IsA<HTMLBRElement>(root->firstChild())) { |
| // If there is a single child and it could be a placeholder, leave it |
| // alone. |
| if (root->GetLayoutObject() && |
| root->GetLayoutObject()->IsLayoutBlockFlow()) |
| return false; |
| } |
| } |
| |
| RemoveAllChildrenIfPossible(root, editing_state); |
| if (editing_state->IsAborted() || root->firstChild()) |
| return false; |
| |
| AddBlockPlaceholderIfNeeded(root, editing_state); |
| if (editing_state->IsAborted()) |
| return false; |
| const SelectionInDOMTree& selection = |
| SelectionInDOMTree::Builder() |
| .Collapse(Position::FirstPositionInNode(*root)) |
| .Build(); |
| SetEndingSelection(SelectionForUndoStep::From(selection)); |
| |
| return true; |
| } |
| |
| // If there are multiple Unicode code points to be deleted, adjust the |
| // range to match platform conventions. |
| static SelectionForUndoStep AdjustSelectionForBackwardDelete( |
| const SelectionInDOMTree& selection) { |
| const Position& base = selection.Base(); |
| if (selection.IsCaret()) { |
| // TODO(yosin): We should make |DeleteSelectionCommand| to work with |
| // anonymous placeholder. |
| if (Position after_block = AfterBlockIfBeforeAnonymousPlaceholder(base)) { |
| // We remove a anonymous placeholder <br> in <div> like <div><br></div>: |
| // <div><img style="display:block"><br></div> |
| // |selection_to_delete| is Before:<br> |
| // as |
| // <div><img style="display:block"><div><br></div></div>. |
| // |selection_to_delete| is <div>@0, After:<img> |
| // See "editing/deleting/delete_after_block_image.html" |
| return SelectionForUndoStep::Builder() |
| .SetBaseAndExtentAsBackwardSelection(base, after_block) |
| .Build(); |
| } |
| return SelectionForUndoStep::From(selection); |
| } |
| if (base.ComputeContainerNode() != selection.Extent().ComputeContainerNode()) |
| return SelectionForUndoStep::From(selection); |
| if (base.ComputeOffsetInContainerNode() - |
| selection.Extent().ComputeOffsetInContainerNode() <= |
| 1) |
| return SelectionForUndoStep::From(selection); |
| const Position& end = selection.ComputeEndPosition(); |
| return SelectionForUndoStep::Builder() |
| .SetBaseAndExtentAsBackwardSelection( |
| end, PreviousPositionOf(end, PositionMoveType::kBackwardDeletion)) |
| .Build(); |
| } |
| |
| void TypingCommand::DeleteKeyPressed(TextGranularity granularity, |
| bool kill_ring, |
| EditingState* editing_state) { |
| LocalFrame* frame = GetDocument().GetFrame(); |
| if (!frame) |
| return; |
| |
| if (EndingSelection().IsRange()) { |
| DeleteKeyPressedInternal(EndingSelection(), EndingSelection(), kill_ring, |
| editing_state); |
| return; |
| } |
| |
| if (!EndingSelection().IsCaret()) { |
| NOTREACHED(); |
| return; |
| } |
| |
| // 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. |
| const bool break_out_result = |
| BreakOutOfEmptyMailBlockquotedParagraph(editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| if (break_out_result) |
| TypingAddedToOpenCommand(kDeleteKey); |
| |
| smart_delete_ = false; |
| GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing); |
| |
| SelectionModifier selection_modifier(*frame, EndingSelection().AsSelection()); |
| selection_modifier.SetSelectionIsDirectional(SelectionIsDirectional()); |
| selection_modifier.Modify(SelectionModifyAlteration::kExtend, |
| SelectionModifyDirection::kBackward, granularity); |
| if (kill_ring && selection_modifier.Selection().IsCaret() && |
| granularity != TextGranularity::kCharacter) { |
| selection_modifier.Modify(SelectionModifyAlteration::kExtend, |
| SelectionModifyDirection::kBackward, |
| TextGranularity::kCharacter); |
| } |
| |
| const VisiblePosition& visible_start(EndingVisibleSelection().VisibleStart()); |
| const VisiblePosition& previous_position = |
| PreviousPositionOf(visible_start, kCannotCrossEditingBoundary); |
| const Node* enclosing_table_cell = |
| EnclosingNodeOfType(visible_start.DeepEquivalent(), &IsTableCell); |
| const Node* enclosing_table_cell_for_previous_position = |
| EnclosingNodeOfType(previous_position.DeepEquivalent(), &IsTableCell); |
| if (previous_position.IsNull() || |
| enclosing_table_cell != enclosing_table_cell_for_previous_position) { |
| // When the caret is at the start of the editable area, or cell, in an |
| // empty list item, break out of the list item. |
| const bool break_out_of_empty_list_item_result = |
| BreakOutOfEmptyListItem(editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| if (break_out_of_empty_list_item_result) { |
| TypingAddedToOpenCommand(kDeleteKey); |
| return; |
| } |
| } |
| if (previous_position.IsNull()) { |
| // When there are no visible positions in the editing root, delete its |
| // entire contents. |
| if (NextPositionOf(visible_start, kCannotCrossEditingBoundary).IsNull() && |
| MakeEditableRootEmpty(editing_state)) { |
| TypingAddedToOpenCommand(kDeleteKey); |
| return; |
| } |
| if (editing_state->IsAborted()) |
| return; |
| } |
| |
| // If we have a caret selection at the beginning of a cell, we have |
| // nothing to do. |
| if (enclosing_table_cell && visible_start.DeepEquivalent() == |
| VisiblePosition::FirstPositionInNode( |
| *const_cast<Node*>(enclosing_table_cell)) |
| .DeepEquivalent()) |
| return; |
| |
| // If the caret is at the start of a paragraph after a table, move content |
| // into the last table cell (this is done to follows macOS' behavior). |
| if (frame->GetEditor().Behavior().ShouldMergeContentWithTablesOnBackspace() && |
| IsStartOfParagraph(visible_start) && |
| TableElementJustBefore( |
| PreviousPositionOf(visible_start, kCannotCrossEditingBoundary))) { |
| // Unless the caret is just before a table. We don't want to move a |
| // table into the last table cell. |
| if (TableElementJustAfter(visible_start)) |
| return; |
| // Extend the selection backward into the last cell, then deletion will |
| // handle the move. |
| selection_modifier.Modify(SelectionModifyAlteration::kExtend, |
| SelectionModifyDirection::kBackward, granularity); |
| // If the caret is just after a table, select the table and don't delete |
| // anything. |
| } else if (Element* table = TableElementJustBefore(visible_start)) { |
| const SelectionInDOMTree& selection = |
| SelectionInDOMTree::Builder() |
| .Collapse(Position::BeforeNode(*table)) |
| .Extend(EndingSelection().Start()) |
| .Build(); |
| SetEndingSelection(SelectionForUndoStep::From(selection)); |
| TypingAddedToOpenCommand(kDeleteKey); |
| return; |
| } |
| |
| const SelectionForUndoStep& selection_to_delete = |
| granularity == TextGranularity::kCharacter |
| ? AdjustSelectionForBackwardDelete( |
| selection_modifier.Selection().AsSelection()) |
| : SelectionForUndoStep::From( |
| selection_modifier.Selection().AsSelection()); |
| |
| if (!StartingSelection().IsRange() || |
| selection_to_delete.Base() != StartingSelection().Start()) { |
| DeleteKeyPressedInternal(selection_to_delete, selection_to_delete, |
| kill_ring, editing_state); |
| return; |
| } |
| // Note: |StartingSelection().End()| can be disconnected. |
| // See editing/deleting/delete_list_item.html on MacOS. |
| const SelectionForUndoStep selection_after_undo = |
| SelectionForUndoStep::Builder() |
| .SetBaseAndExtentAsBackwardSelection( |
| StartingSelection().End(), |
| CreateVisiblePosition(selection_to_delete.Extent()) |
| .DeepEquivalent()) |
| .Build(); |
| DeleteKeyPressedInternal(selection_to_delete, selection_after_undo, kill_ring, |
| editing_state); |
| } |
| |
| void TypingCommand::DeleteKeyPressedInternal( |
| const SelectionForUndoStep& selection_to_delete, |
| const SelectionForUndoStep& selection_after_undo, |
| bool kill_ring, |
| EditingState* editing_state) { |
| DCHECK(!selection_to_delete.IsNone()); |
| if (selection_to_delete.IsNone()) |
| return; |
| |
| if (selection_to_delete.IsCaret()) |
| return; |
| |
| LocalFrame* frame = GetDocument().GetFrame(); |
| DCHECK(frame); |
| |
| if (kill_ring) { |
| frame->GetEditor().AddToKillRing(CreateVisibleSelection(selection_to_delete) |
| .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->GetEditor().Behavior().ShouldUndoOfDeleteSelectText() && |
| opened_by_backward_delete_) |
| SetStartingSelection(selection_after_undo); |
| DeleteSelectionIfRange(selection_to_delete, editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| SetSmartDelete(false); |
| TypingAddedToOpenCommand(kDeleteKey); |
| } |
| |
| static Position ComputeExtentForForwardDeleteUndo( |
| const VisibleSelection& selection, |
| const Position& extent) { |
| if (extent.ComputeContainerNode() != selection.End().ComputeContainerNode()) |
| return selection.Extent(); |
| const int extra_characters = |
| selection.Start().ComputeContainerNode() == |
| selection.End().ComputeContainerNode() |
| ? selection.End().ComputeOffsetInContainerNode() - |
| selection.Start().ComputeOffsetInContainerNode() |
| : selection.End().ComputeOffsetInContainerNode(); |
| return Position::CreateWithoutValidation( |
| *extent.ComputeContainerNode(), |
| extent.ComputeOffsetInContainerNode() + extra_characters); |
| } |
| |
| void TypingCommand::ForwardDeleteKeyPressed(TextGranularity granularity, |
| bool kill_ring, |
| EditingState* editing_state) { |
| LocalFrame* frame = GetDocument().GetFrame(); |
| if (!frame) |
| return; |
| |
| if (EndingSelection().IsRange()) { |
| ForwardDeleteKeyPressedInternal(EndingSelection(), EndingSelection(), |
| kill_ring, editing_state); |
| return; |
| } |
| |
| if (!EndingSelection().IsCaret()) { |
| NOTREACHED(); |
| return; |
| } |
| |
| smart_delete_ = false; |
| GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing); |
| |
| // 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 selection_modifier(*frame, EndingSelection().AsSelection()); |
| selection_modifier.SetSelectionIsDirectional(SelectionIsDirectional()); |
| selection_modifier.Modify(SelectionModifyAlteration::kExtend, |
| SelectionModifyDirection::kForward, granularity); |
| if (kill_ring && selection_modifier.Selection().IsCaret() && |
| granularity != TextGranularity::kCharacter) { |
| selection_modifier.Modify(SelectionModifyAlteration::kExtend, |
| SelectionModifyDirection::kForward, |
| TextGranularity::kCharacter); |
| } |
| |
| Position downstream_end = MostForwardCaretPosition(EndingSelection().End()); |
| VisiblePosition visible_end = EndingVisibleSelection().VisibleEnd(); |
| Node* enclosing_table_cell = |
| EnclosingNodeOfType(visible_end.DeepEquivalent(), &IsTableCell); |
| if (enclosing_table_cell && |
| visible_end.DeepEquivalent() == |
| VisiblePosition::LastPositionInNode(*enclosing_table_cell) |
| .DeepEquivalent()) |
| return; |
| if (visible_end.DeepEquivalent() == |
| EndOfParagraph(visible_end).DeepEquivalent()) { |
| downstream_end = MostForwardCaretPosition( |
| NextPositionOf(visible_end, kCannotCrossEditingBoundary) |
| .DeepEquivalent()); |
| } |
| // When deleting tables: Select the table first, then perform the deletion |
| if (IsDisplayInsideTable(downstream_end.ComputeContainerNode()) && |
| downstream_end.ComputeOffsetInContainerNode() <= |
| CaretMinOffset(downstream_end.ComputeContainerNode())) { |
| const SelectionInDOMTree& selection = |
| SelectionInDOMTree::Builder() |
| .SetBaseAndExtentDeprecated( |
| EndingSelection().End(), |
| Position::AfterNode(*downstream_end.ComputeContainerNode())) |
| .Build(); |
| SetEndingSelection(SelectionForUndoStep::From(selection)); |
| TypingAddedToOpenCommand(kForwardDeleteKey); |
| return; |
| } |
| |
| // deleting to end of paragraph when at end of paragraph needs to merge |
| // the next paragraph (if any) |
| if (granularity == TextGranularity::kParagraphBoundary && |
| selection_modifier.Selection().IsCaret() && |
| IsEndOfParagraph(selection_modifier.Selection().VisibleEnd())) { |
| selection_modifier.Modify(SelectionModifyAlteration::kExtend, |
| SelectionModifyDirection::kForward, |
| TextGranularity::kCharacter); |
| } |
| |
| const VisibleSelection& selection_to_delete = selection_modifier.Selection(); |
| if (!StartingSelection().IsRange() || |
| MostBackwardCaretPosition(selection_to_delete.Base()) != |
| StartingSelection().Start()) { |
| ForwardDeleteKeyPressedInternal( |
| SelectionForUndoStep::From(selection_to_delete.AsSelection()), |
| SelectionForUndoStep::From(selection_to_delete.AsSelection()), |
| kill_ring, editing_state); |
| return; |
| } |
| // Note: |StartingSelection().Start()| can be disconnected. |
| const SelectionForUndoStep selection_after_undo = |
| SelectionForUndoStep::Builder() |
| .SetBaseAndExtentAsForwardSelection( |
| StartingSelection().Start(), |
| ComputeExtentForForwardDeleteUndo(selection_to_delete, |
| StartingSelection().End())) |
| .Build(); |
| ForwardDeleteKeyPressedInternal( |
| SelectionForUndoStep::From(selection_to_delete.AsSelection()), |
| selection_after_undo, kill_ring, editing_state); |
| } |
| |
| void TypingCommand::ForwardDeleteKeyPressedInternal( |
| const SelectionForUndoStep& selection_to_delete, |
| const SelectionForUndoStep& selection_after_undo, |
| bool kill_ring, |
| EditingState* editing_state) { |
| DCHECK(!selection_to_delete.IsNone()); |
| if (selection_to_delete.IsNone()) |
| return; |
| |
| if (selection_to_delete.IsCaret()) |
| return; |
| |
| LocalFrame* frame = GetDocument().GetFrame(); |
| DCHECK(frame); |
| |
| if (kill_ring) { |
| frame->GetEditor().AddToKillRing(CreateVisibleSelection(selection_to_delete) |
| .ToNormalizedEphemeralRange()); |
| } |
| // Make undo select what was deleted on Mac alone |
| if (frame->GetEditor().Behavior().ShouldUndoOfDeleteSelectText()) |
| SetStartingSelection(selection_after_undo); |
| DeleteSelectionIfRange(selection_to_delete, editing_state); |
| if (editing_state->IsAborted()) |
| return; |
| SetSmartDelete(false); |
| TypingAddedToOpenCommand(kForwardDeleteKey); |
| } |
| |
| void TypingCommand::DeleteSelection(bool smart_delete, |
| EditingState* editing_state) { |
| if (!CompositeEditCommand::DeleteSelection( |
| editing_state, smart_delete ? DeleteSelectionOptions::SmartDelete() |
| : DeleteSelectionOptions::NormalDelete())) |
| return; |
| TypingAddedToOpenCommand(kDeleteSelection); |
| } |
| |
| void TypingCommand::UpdatePreservesTypingStyle(CommandType command_type) { |
| switch (command_type) { |
| case kDeleteSelection: |
| case kDeleteKey: |
| case kForwardDeleteKey: |
| case kInsertParagraphSeparator: |
| case kInsertLineBreak: |
| preserves_typing_style_ = true; |
| return; |
| case kInsertParagraphSeparatorInQuotedContent: |
| case kInsertText: |
| preserves_typing_style_ = false; |
| return; |
| } |
| NOTREACHED(); |
| preserves_typing_style_ = false; |
| } |
| |
| bool TypingCommand::IsTypingCommand() const { |
| return true; |
| } |
| |
| } // namespace blink |