blob: da3fcad95c01743ef83e2ec5ffef891d4516c685 [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 "core/editing/commands/TypingCommand.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/events/ScopedEventQueue.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/Editor.h"
#include "core/editing/EphemeralRange.h"
#include "core/editing/FrameSelection.h"
#include "core/editing/PlainTextRange.h"
#include "core/editing/SelectionModifier.h"
#include "core/editing/SelectionTemplate.h"
#include "core/editing/VisiblePosition.h"
#include "core/editing/VisibleSelection.h"
#include "core/editing/VisibleUnits.h"
#include "core/editing/commands/BreakBlockquoteCommand.h"
#include "core/editing/commands/DeleteSelectionCommand.h"
#include "core/editing/commands/DeleteSelectionOptions.h"
#include "core/editing/commands/EditingCommandsUtilities.h"
#include "core/editing/commands/InsertIncrementalTextCommand.h"
#include "core/editing/commands/InsertLineBreakCommand.h"
#include "core/editing/commands/InsertParagraphSeparatorCommand.h"
#include "core/editing/commands/InsertTextCommand.h"
#include "core/events/BeforeTextInsertedEvent.h"
#include "core/events/TextEvent.h"
#include "core/frame/LocalFrame.h"
#include "core/html/HTMLBRElement.h"
#include "core/html_names.h"
#include "core/layout/LayoutObject.h"
namespace blink {
namespace {
bool IsValidDocument(const Document& document) {
return document.GetFrame() && document.GetFrame()->GetDocument() == &document;
}
String DispatchBeforeTextInsertedEvent(const String& text,
const VisibleSelection& selection,
EditingState* editing_state) {
Node* start_node = selection.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();
BeforeTextInsertedEvent* evt = BeforeTextInsertedEvent::Create(text);
RootEditableElement(*start_node)->DispatchEvent(evt);
if (IsValidDocument(document) && selection.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 VisibleSelection visible_selection = CreateVisibleSelection(selection);
const EphemeralRange range = FirstEphemeralRangeOf(visible_selection);
if (range.IsNull())
return PlainTextRange();
ContainerNode* const editable =
RootEditableElementOrTreeScopeRootNodeOf(selection.Base());
DCHECK(editable);
return PlainTextRange::Create(*editable, range);
}
SelectionInDOMTree CreateSelection(const size_t start,
const 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 VisibleSelection& selection,
EditingState* editing_state) {
Element* element = selection.RootEditableElement();
if (!element)
return false;
const Document& document = element->GetDocument();
BeforeTextInsertedEvent* event =
BeforeTextInsertedEvent::Create(String("\n"));
element->DispatchEvent(event);
// event may invalidate frame or selection
if (IsValidDocument(document) && selection.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;
}
} // anonymous namespace
using namespace HTMLNames;
TypingCommand::TypingCommand(Document& document,
ETypingCommand 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;
}
TypingCommand::Create(document, kDeleteSelection, "", options)->Apply();
}
void TypingCommand::DeleteSelectionIfRange(const VisibleSelection& selection,
EditingState* editing_state) {
if (!selection.IsRange())
return;
ApplyCommandToComposite(DeleteSelectionCommand::Create(
selection, DeleteSelectionOptions::Builder()
.SetSmartDelete(smart_delete_)
.SetMergeBlocksAfterDelete(true)
.SetExpandForSpecialElements(true)
.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;
}
}
}
TypingCommand::Create(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;
}
}
TypingCommand::Create(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 size_t selection_start,
const size_t text_length,
EditingState* editing_state) {
if (!IsIncrementalInsertion())
return;
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. see http://crbug.com/590369 for more details.
frame->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
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 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);
const VisibleSelection& current_selection =
frame->Selection().ComputeVisibleSelectionInDOMTree();
const VisibleSelection& selection_for_insertion =
CreateVisibleSelection(passed_selection_for_insertion);
String new_text = text;
if (composition_type != kTextCompositionUpdate) {
new_text = DispatchBeforeTextInsertedEvent(text, selection_for_insertion,
editing_state);
if (editing_state->IsAborted())
return;
}
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(!selection_for_insertion.IsValidFor(document));
}
// Do nothing if no need to delete and insert.
if (selection_for_insertion.IsCaret() && new_text.IsEmpty())
return;
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. see http://crbug.com/590369 for more details.
document.UpdateStyleAndLayoutIgnorePendingStylesheets();
const PlainTextRange selection_offsets =
GetSelectionOffsets(selection_for_insertion.AsSelection());
if (selection_offsets.IsNull())
return;
const 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->EndingVisibleSelection() !=
selection_for_insertion) {
const SelectionForUndoStep& selection_for_insertion_as_undo_step =
SelectionForUndoStep::From(selection_for_insertion.AsSelection());
last_typing_command->SetStartingSelection(
selection_for_insertion_as_undo_step);
last_typing_command->SetEndingSelection(
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 = TypingCommand::Create(
document, kInsertText, new_text, options, composition_type);
bool change_selection = selection_for_insertion != current_selection;
if (change_selection) {
const SelectionForUndoStep& selection_for_insertion_as_undo_step =
SelectionForUndoStep::From(selection_for_insertion.AsSelection());
command->SetStartingSelection(selection_for_insertion_as_undo_step);
command->SetEndingSelection(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) {
ABORT_EDITING_COMMAND_IF(!current_selection.IsValidFor(document));
const SelectionInDOMTree& current_selection_as_dom =
current_selection.AsSelection();
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 TypingCommand::Create(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 TypingCommand::Create(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 TypingCommand::Create(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::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(chongz): |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(
ETypingCommand 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;
}
size_t selection_start = selection_start_;
unsigned offset = 0;
size_t newline;
while ((newline = text.find('\n', offset)) != kNotFound) {
if (newline > offset) {
const 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 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 = InsertIncrementalTextCommand::Create(
GetDocument(), text,
composition_type_ == kTextCompositionNone
? InsertIncrementalTextCommand::
kRebalanceLeadingAndTrailingWhitespaces
: InsertIncrementalTextCommand::kRebalanceAllWhitespaces);
} else {
command = InsertTextCommand::Create(
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(EndingVisibleSelection(), editing_state))
return;
ApplyCommandToComposite(InsertLineBreakCommand::Create(GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
TypingAddedToOpenCommand(kInsertLineBreak);
}
void TypingCommand::InsertParagraphSeparator(EditingState* editing_state) {
if (!CanAppendNewLineFeedToSelection(EndingVisibleSelection(), editing_state))
return;
ApplyCommandToComposite(
InsertParagraphSeparatorCommand::Create(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(BreakBlockquoteCommand::Create(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 (IsHTMLBRElement(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;
}
}
while (Node* child = root->firstChild()) {
RemoveNode(child, editing_state);
if (editing_state->IsAborted())
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 VisibleSelection AdjustSelectionForBackwardDelete(
const VisibleSelection& selection) {
if (selection.End().ComputeContainerNode() !=
selection.Start().ComputeContainerNode())
return selection;
if (selection.End().ComputeOffsetInContainerNode() -
selection.Start().ComputeOffsetInContainerNode() <=
1)
return selection;
return VisibleSelection::CreateWithoutValidationDeprecated(
selection.End(),
PreviousPositionOf(selection.End(), PositionMoveType::kBackwardDeletion),
selection.Affinity());
}
void TypingCommand::DeleteKeyPressed(TextGranularity granularity,
bool kill_ring,
EditingState* editing_state) {
LocalFrame* frame = GetDocument().GetFrame();
if (!frame)
return;
if (EndingSelection().IsRange()) {
DeleteKeyPressedInternal(EndingVisibleSelection(), 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().UpdateStyleAndLayoutIgnorePendingStylesheets();
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 VisibleSelection& selection_to_delete =
granularity == TextGranularity::kCharacter
? AdjustSelectionForBackwardDelete(selection_modifier.Selection())
: selection_modifier.Selection();
if (!StartingSelection().IsRange() ||
selection_to_delete.Base() != StartingSelection().Start()) {
DeleteKeyPressedInternal(
selection_to_delete,
SelectionForUndoStep::From(selection_to_delete.AsSelection()),
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 VisibleSelection& 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(
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(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(EndingVisibleSelection(), EndingSelection(),
kill_ring, editing_state);
return;
}
if (!EndingSelection().IsCaret()) {
NOTREACHED();
return;
}
smart_delete_ = false;
GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
// 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(
selection_to_delete,
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(selection_to_delete, selection_after_undo,
kill_ring, editing_state);
}
void TypingCommand::ForwardDeleteKeyPressedInternal(
const VisibleSelection& 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(
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(ETypingCommand 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