blob: bc7089a4c7d75c08e07719e983aa56cbe0539cf3 [file] [log] [blame]
/*
* 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