blob: 9c0d976d657fc513d8231c1b993dd0324611b870 [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/composite_edit_command.h"
#include <algorithm>
#include "third_party/blink/renderer/core/accessibility/blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/accessibility/scoped_blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_fragment.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/dom/node_traversal.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/commands/append_node_command.h"
#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
#include "third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.h"
#include "third_party/blink/renderer/core/editing/commands/delete_selection_command.h"
#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
#include "third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.h"
#include "third_party/blink/renderer/core/editing/commands/insert_line_break_command.h"
#include "third_party/blink/renderer/core/editing/commands/insert_node_before_command.h"
#include "third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h"
#include "third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.h"
#include "third_party/blink/renderer/core/editing/commands/remove_css_property_command.h"
#include "third_party/blink/renderer/core/editing/commands/remove_node_command.h"
#include "third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.h"
#include "third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.h"
#include "third_party/blink/renderer/core/editing/commands/replace_selection_command.h"
#include "third_party/blink/renderer/core/editing/commands/set_character_data_command.h"
#include "third_party/blink/renderer/core/editing/commands/set_node_attribute_command.h"
#include "third_party/blink/renderer/core/editing/commands/split_element_command.h"
#include "third_party/blink/renderer/core/editing/commands/split_text_node_command.h"
#include "third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.h"
#include "third_party/blink/renderer/core/editing/commands/undo_stack.h"
#include "third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.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/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/plain_text_range.h"
#include "third_party/blink/renderer/core/editing/relocatable_position.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/serializers/serialization.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/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_br_element.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_li_element.h"
#include "third_party/blink/renderer/core/html/html_quote_element.h"
#include "third_party/blink/renderer/core/html/html_span_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_block.h"
#include "third_party/blink/renderer/core/layout/layout_list_item.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
namespace blink {
CompositeEditCommand::CompositeEditCommand(Document& document)
: EditCommand(document) {
const VisibleSelection& visible_selection =
document.GetFrame()
->Selection()
.ComputeVisibleSelectionInDOMTreeDeprecated();
SetStartingSelection(
SelectionForUndoStep::From(visible_selection.AsSelection()));
SetEndingSelection(starting_selection_);
}
CompositeEditCommand::~CompositeEditCommand() {
DCHECK(IsTopLevelCommand() || !undo_step_);
}
VisibleSelection CompositeEditCommand::EndingVisibleSelection() const {
// TODO(editing-dev): The use of
// |Document::UpdateStyleAndLayout()|
// needs to be audited. See http://crbug.com/590369 for more details.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
return CreateVisibleSelection(ending_selection_);
}
bool CompositeEditCommand::Apply() {
DCHECK(!IsCommandGroupWrapper());
if (!IsRichlyEditablePosition(EndingVisibleSelection().Base())) {
switch (GetInputType()) {
case InputEvent::InputType::kInsertText:
case InputEvent::InputType::kInsertLineBreak:
case InputEvent::InputType::kInsertParagraph:
case InputEvent::InputType::kInsertFromPaste:
case InputEvent::InputType::kInsertFromDrop:
case InputEvent::InputType::kInsertFromYank:
case InputEvent::InputType::kInsertTranspose:
case InputEvent::InputType::kInsertReplacementText:
case InputEvent::InputType::kInsertCompositionText:
case InputEvent::InputType::kDeleteWordBackward:
case InputEvent::InputType::kDeleteWordForward:
case InputEvent::InputType::kDeleteSoftLineBackward:
case InputEvent::InputType::kDeleteSoftLineForward:
case InputEvent::InputType::kDeleteHardLineBackward:
case InputEvent::InputType::kDeleteHardLineForward:
case InputEvent::InputType::kDeleteContentBackward:
case InputEvent::InputType::kDeleteContentForward:
case InputEvent::InputType::kDeleteByCut:
case InputEvent::InputType::kDeleteByDrag:
case InputEvent::InputType::kNone:
break;
default:
return false;
}
}
EnsureUndoStep();
// Changes to the document may have been made since the last editing operation
// that require a layout, as in <rdar://problem/5658603>. Low level
// operations, like RemoveNodeCommand, don't require a layout because the high
// level operations that use them perform one if one is necessary (like for
// the creation of VisiblePositions).
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
LocalFrame* frame = GetDocument().GetFrame();
DCHECK(frame);
// directional is stored at the top level command, so that before and after
// executing command same directional will be there.
SetSelectionIsDirectional(frame->Selection().IsDirectional());
GetUndoStep()->SetSelectionIsDirectional(SelectionIsDirectional());
// Provides details to accessibility about any text change caused by applying
// this command, throughout the current call stack.
ScopedBlinkAXEventIntent scoped_blink_ax_event_intent(
BlinkAXEventIntent::FromEditCommand(*this), &GetDocument());
EditingState editing_state;
EventQueueScope event_queue_scope;
DoApply(&editing_state);
// Only need to call appliedEditing for top-level commands, and TypingCommands
// do it on their own (see TypingCommand::typingAddedToOpenCommand).
if (!IsTypingCommand())
AppliedEditing();
return !editing_state.IsAborted();
}
UndoStep* CompositeEditCommand::EnsureUndoStep() {
CompositeEditCommand* command = this;
while (command && command->Parent())
command = command->Parent();
if (!command->undo_step_) {
command->undo_step_ = MakeGarbageCollected<UndoStep>(
&GetDocument(), StartingSelection(), EndingSelection(), GetInputType());
}
return command->undo_step_.Get();
}
bool CompositeEditCommand::PreservesTypingStyle() const {
return false;
}
bool CompositeEditCommand::IsTypingCommand() const {
return false;
}
bool CompositeEditCommand::IsCommandGroupWrapper() const {
return false;
}
bool CompositeEditCommand::IsDragAndDropCommand() const {
return false;
}
bool CompositeEditCommand::IsReplaceSelectionCommand() const {
return false;
}
//
// sugary-sweet convenience functions to help create and apply edit commands in
// composite commands
//
void CompositeEditCommand::ApplyCommandToComposite(
EditCommand* command,
EditingState* editing_state) {
command->SetParent(this);
command->SetSelectionIsDirectional(SelectionIsDirectional());
command->DoApply(editing_state);
if (editing_state->IsAborted()) {
command->SetParent(nullptr);
return;
}
if (auto* simple_edit_command = DynamicTo<SimpleEditCommand>(command)) {
command->SetParent(nullptr);
EnsureUndoStep()->Append(simple_edit_command);
}
commands_.push_back(command);
}
void CompositeEditCommand::AppendCommandToUndoStep(
CompositeEditCommand* command) {
EnsureUndoStep()->Append(command->EnsureUndoStep());
command->undo_step_ = nullptr;
command->SetParent(this);
commands_.push_back(command);
}
void CompositeEditCommand::ApplyStyle(const EditingStyle* style,
EditingState* editing_state) {
ApplyCommandToComposite(
MakeGarbageCollected<ApplyStyleCommand>(GetDocument(), style,
InputEvent::InputType::kNone),
editing_state);
}
void CompositeEditCommand::ApplyStyle(const EditingStyle* style,
const Position& start,
const Position& end,
EditingState* editing_state) {
ApplyCommandToComposite(
MakeGarbageCollected<ApplyStyleCommand>(GetDocument(), style, start, end),
editing_state);
}
void CompositeEditCommand::ApplyStyledElement(Element* element,
EditingState* editing_state) {
ApplyCommandToComposite(
MakeGarbageCollected<ApplyStyleCommand>(element, false), editing_state);
}
void CompositeEditCommand::RemoveStyledElement(Element* element,
EditingState* editing_state) {
ApplyCommandToComposite(
MakeGarbageCollected<ApplyStyleCommand>(element, true), editing_state);
}
void CompositeEditCommand::InsertParagraphSeparator(
EditingState* editing_state,
bool use_default_paragraph_element,
bool paste_blockqutoe_into_unquoted_area) {
ApplyCommandToComposite(MakeGarbageCollected<InsertParagraphSeparatorCommand>(
GetDocument(), use_default_paragraph_element,
paste_blockqutoe_into_unquoted_area),
editing_state);
}
bool CompositeEditCommand::IsRemovableBlock(const Node* node) {
DCHECK(node);
const auto* element = DynamicTo<HTMLDivElement>(node);
if (!element)
return false;
ContainerNode* parent_node = element->parentNode();
if (parent_node && parent_node->firstChild() != parent_node->lastChild())
return false;
if (!element->hasAttributes())
return true;
return false;
}
void CompositeEditCommand::InsertNodeBefore(
Node* insert_child,
Node* ref_child,
EditingState* editing_state,
ShouldAssumeContentIsAlwaysEditable
should_assume_content_is_always_editable) {
ABORT_EDITING_COMMAND_IF(GetDocument().body() == ref_child);
ABORT_EDITING_COMMAND_IF(!ref_child->parentNode());
// TODO(editing-dev): Use of UpdateStyleAndLayout
// needs to be audited. See http://crbug.com/590369 for more details.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
ABORT_EDITING_COMMAND_IF(!HasEditableStyle(*ref_child->parentNode()) &&
ref_child->parentNode()->InActiveDocument());
ApplyCommandToComposite(
MakeGarbageCollected<InsertNodeBeforeCommand>(
insert_child, ref_child, should_assume_content_is_always_editable),
editing_state);
}
void CompositeEditCommand::InsertNodeAfter(Node* insert_child,
Node* ref_child,
EditingState* editing_state) {
ABORT_EDITING_COMMAND_IF(!ref_child->parentNode());
DCHECK(insert_child);
DCHECK(ref_child);
DCHECK_NE(GetDocument().body(), ref_child);
ContainerNode* parent = ref_child->parentNode();
DCHECK(parent);
DCHECK(!parent->IsShadowRoot()) << parent;
if (parent->lastChild() == ref_child) {
AppendNode(insert_child, parent, editing_state);
} else {
DCHECK(ref_child->nextSibling()) << ref_child;
InsertNodeBefore(insert_child, ref_child->nextSibling(), editing_state);
}
}
void CompositeEditCommand::InsertNodeAt(Node* insert_child,
const Position& editing_position,
EditingState* editing_state) {
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
ABORT_EDITING_COMMAND_IF(!IsEditablePosition(editing_position));
// For editing positions like [table, 0], insert before the table,
// likewise for replaced elements, brs, etc.
Position p = editing_position.ParentAnchoredEquivalent();
Node* ref_child = p.AnchorNode();
int offset = p.OffsetInContainerNode();
auto* ref_child_text_node = DynamicTo<Text>(ref_child);
if (CanHaveChildrenForEditing(ref_child)) {
Node* child = ref_child->firstChild();
for (int i = 0; child && i < offset; i++)
child = child->nextSibling();
if (child)
InsertNodeBefore(insert_child, child, editing_state);
else
AppendNode(insert_child, To<ContainerNode>(ref_child), editing_state);
} else if (CaretMinOffset(ref_child) >= offset) {
InsertNodeBefore(insert_child, ref_child, editing_state);
} else if (ref_child_text_node && CaretMaxOffset(ref_child) > offset) {
SplitTextNode(ref_child_text_node, offset);
// Mutation events (bug 22634) from the text node insertion may have
// removed the refChild
if (!ref_child->isConnected())
return;
InsertNodeBefore(insert_child, ref_child, editing_state);
} else {
InsertNodeAfter(insert_child, ref_child, editing_state);
}
}
void CompositeEditCommand::AppendNode(Node* node,
ContainerNode* parent,
EditingState* editing_state) {
// When cloneParagraphUnderNewElement() clones the fallback content
// of an OBJECT element, the ASSERT below may fire since the return
// value of canHaveChildrenForEditing is not reliable until the layout
// object of the OBJECT is created. Hence we ignore this check for OBJECTs.
// TODO(yosin): We should move following |ABORT_EDITING_COMMAND_IF|s to
// |AppendNodeCommand|.
// TODO(yosin): We should get rid of |canHaveChildrenForEditing()|, since
// |cloneParagraphUnderNewElement()| attempt to clone non-well-formed HTML,
// produced by JavaScript.
auto* parent_element = DynamicTo<Element>(parent);
ABORT_EDITING_COMMAND_IF(!CanHaveChildrenForEditing(parent) &&
!(parent_element && parent_element->TagQName() ==
html_names::kObjectTag));
ABORT_EDITING_COMMAND_IF(!HasEditableStyle(*parent) &&
parent->InActiveDocument());
ApplyCommandToComposite(MakeGarbageCollected<AppendNodeCommand>(parent, node),
editing_state);
}
void CompositeEditCommand::RemoveAllChildrenIfPossible(
ContainerNode* container,
EditingState* editing_state,
ShouldAssumeContentIsAlwaysEditable
should_assume_content_is_always_editable) {
Node* child = container->firstChild();
while (child) {
Node* const next = child->nextSibling();
RemoveNode(child, editing_state, should_assume_content_is_always_editable);
if (editing_state->IsAborted())
return;
if (next && next->parentNode() != container) {
// |RemoveNode()| moves |next| outside |node|.
return;
}
child = next;
}
}
void CompositeEditCommand::RemoveChildrenInRange(Node* node,
unsigned from,
unsigned to,
EditingState* editing_state) {
HeapVector<Member<Node>> children;
Node* child = NodeTraversal::ChildAt(*node, from);
for (unsigned i = from; child && i < to; i++, child = child->nextSibling())
children.push_back(child);
size_t size = children.size();
for (wtf_size_t i = 0; i < size; ++i) {
RemoveNode(children[i].Release(), editing_state);
if (editing_state->IsAborted())
return;
}
}
void CompositeEditCommand::RemoveNode(
Node* node,
EditingState* editing_state,
ShouldAssumeContentIsAlwaysEditable
should_assume_content_is_always_editable) {
if (!node || !node->NonShadowBoundaryParentNode())
return;
ABORT_EDITING_COMMAND_IF(!node->GetDocument().GetFrame());
ApplyCommandToComposite(MakeGarbageCollected<RemoveNodeCommand>(
node, should_assume_content_is_always_editable),
editing_state);
}
void CompositeEditCommand::RemoveNodePreservingChildren(
Node* node,
EditingState* editing_state,
ShouldAssumeContentIsAlwaysEditable
should_assume_content_is_always_editable) {
ABORT_EDITING_COMMAND_IF(!node->GetDocument().GetFrame());
ApplyCommandToComposite(
MakeGarbageCollected<RemoveNodePreservingChildrenCommand>(
node, should_assume_content_is_always_editable),
editing_state);
}
void CompositeEditCommand::RemoveNodeAndPruneAncestors(
Node* node,
EditingState* editing_state,
Node* exclude_node) {
DCHECK_NE(node, exclude_node);
ContainerNode* parent = node->parentNode();
RemoveNode(node, editing_state);
if (editing_state->IsAborted())
return;
Prune(parent, editing_state, exclude_node);
}
void CompositeEditCommand::MoveRemainingSiblingsToNewParent(
Node* node,
Node* past_last_node_to_move,
Element* new_parent,
EditingState* editing_state) {
NodeVector nodes_to_remove;
for (; node && node != past_last_node_to_move; node = node->nextSibling())
nodes_to_remove.push_back(node);
for (unsigned i = 0; i < nodes_to_remove.size(); i++) {
RemoveNode(nodes_to_remove[i], editing_state);
if (editing_state->IsAborted())
return;
AppendNode(nodes_to_remove[i], new_parent, editing_state);
if (editing_state->IsAborted())
return;
}
}
void CompositeEditCommand::UpdatePositionForNodeRemovalPreservingChildren(
Position& position,
Node& node) {
int offset =
position.IsOffsetInAnchor() ? position.OffsetInContainerNode() : 0;
position = ComputePositionForNodeRemoval(position, node);
if (offset == 0)
return;
position = Position::CreateWithoutValidationDeprecated(
*position.ComputeContainerNode(), offset);
}
HTMLSpanElement*
CompositeEditCommand::ReplaceElementWithSpanPreservingChildrenAndAttributes(
HTMLElement* node) {
// It would also be possible to implement all of ReplaceNodeWithSpanCommand
// as a series of existing smaller edit commands. Someone who wanted to
// reduce the number of edit commands could do so here.
auto* command = MakeGarbageCollected<ReplaceNodeWithSpanCommand>(node);
// ReplaceNodeWithSpanCommand is never aborted.
ApplyCommandToComposite(command, ASSERT_NO_EDITING_ABORT);
// Returning a raw pointer here is OK because the command is retained by
// applyCommandToComposite (thus retaining the span), and the span is also
// in the DOM tree, and thus alive whie it has a parent.
DCHECK(command->SpanElement()->isConnected()) << command->SpanElement();
return command->SpanElement();
}
void CompositeEditCommand::Prune(Node* node,
EditingState* editing_state,
Node* exclude_node) {
if (Node* highest_node_to_remove =
HighestNodeToRemoveInPruning(node, exclude_node))
RemoveNode(highest_node_to_remove, editing_state);
}
void CompositeEditCommand::SplitTextNode(Text* node, unsigned offset) {
// SplitTextNodeCommand is never aborted.
ApplyCommandToComposite(
MakeGarbageCollected<SplitTextNodeCommand>(node, offset),
ASSERT_NO_EDITING_ABORT);
}
void CompositeEditCommand::SplitElement(Element* element, Node* at_child) {
// SplitElementCommand is never aborted.
ApplyCommandToComposite(
MakeGarbageCollected<SplitElementCommand>(element, at_child),
ASSERT_NO_EDITING_ABORT);
}
void CompositeEditCommand::MergeIdenticalElements(Element* first,
Element* second,
EditingState* editing_state) {
DCHECK(!first->IsDescendantOf(second)) << first << " " << second;
DCHECK_NE(second, first);
if (first->nextSibling() != second) {
RemoveNode(second, editing_state);
if (editing_state->IsAborted())
return;
InsertNodeAfter(second, first, editing_state);
if (editing_state->IsAborted())
return;
}
ApplyCommandToComposite(
MakeGarbageCollected<MergeIdenticalElementsCommand>(first, second),
editing_state);
}
void CompositeEditCommand::WrapContentsInDummySpan(Element* element) {
// WrapContentsInDummySpanCommand is never aborted.
ApplyCommandToComposite(
MakeGarbageCollected<WrapContentsInDummySpanCommand>(element),
ASSERT_NO_EDITING_ABORT);
}
void CompositeEditCommand::SplitTextNodeContainingElement(Text* text,
unsigned offset) {
// SplitTextNodeContainingElementCommand is never aborted.
ApplyCommandToComposite(
MakeGarbageCollected<SplitTextNodeContainingElementCommand>(text, offset),
ASSERT_NO_EDITING_ABORT);
}
void CompositeEditCommand::InsertTextIntoNode(Text* node,
unsigned offset,
const String& text) {
// InsertIntoTextNodeCommand is never aborted.
if (!text.IsEmpty())
ApplyCommandToComposite(
MakeGarbageCollected<InsertIntoTextNodeCommand>(node, offset, text),
ASSERT_NO_EDITING_ABORT);
}
void CompositeEditCommand::DeleteTextFromNode(Text* node,
unsigned offset,
unsigned count) {
// DeleteFromTextNodeCommand is never aborted.
ApplyCommandToComposite(
MakeGarbageCollected<DeleteFromTextNodeCommand>(node, offset, count),
ASSERT_NO_EDITING_ABORT);
}
void CompositeEditCommand::ReplaceTextInNode(Text* node,
unsigned offset,
unsigned count,
const String& replacement_text) {
// SetCharacterDataCommand is never aborted.
ApplyCommandToComposite(MakeGarbageCollected<SetCharacterDataCommand>(
node, offset, count, replacement_text),
ASSERT_NO_EDITING_ABORT);
}
Position CompositeEditCommand::ReplaceSelectedTextInNode(const String& text) {
const Position& start = EndingSelection().Start();
const Position& end = EndingSelection().End();
auto* text_node = DynamicTo<Text>(start.ComputeContainerNode());
if (!text_node || text_node != end.ComputeContainerNode() ||
IsTabHTMLSpanElementTextNode(text_node))
return Position();
ReplaceTextInNode(text_node, start.OffsetInContainerNode(),
end.OffsetInContainerNode() - start.OffsetInContainerNode(),
text);
return Position(text_node, start.OffsetInContainerNode() + text.length());
}
Position CompositeEditCommand::PositionOutsideTabSpan(const Position& pos) {
if (!IsTabHTMLSpanElementTextNode(pos.AnchorNode()))
return pos;
switch (pos.AnchorType()) {
case PositionAnchorType::kAfterChildren:
NOTREACHED();
return pos;
case PositionAnchorType::kOffsetInAnchor:
break;
case PositionAnchorType::kBeforeAnchor:
return Position::InParentBeforeNode(*pos.AnchorNode());
case PositionAnchorType::kAfterAnchor:
return Position::InParentAfterNode(*pos.AnchorNode());
}
HTMLSpanElement* tab_span = TabSpanElement(pos.ComputeContainerNode());
DCHECK(tab_span);
// TODO(editing-dev): Hoist this UpdateStyleAndLayout
// to the callers. See crbug.com/590369 for details.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
if (pos.OffsetInContainerNode() <= CaretMinOffset(pos.ComputeContainerNode()))
return Position::InParentBeforeNode(*tab_span);
if (pos.OffsetInContainerNode() >= CaretMaxOffset(pos.ComputeContainerNode()))
return Position::InParentAfterNode(*tab_span);
SplitTextNodeContainingElement(To<Text>(pos.ComputeContainerNode()),
pos.OffsetInContainerNode());
return Position::InParentBeforeNode(*tab_span);
}
void CompositeEditCommand::InsertNodeAtTabSpanPosition(
Node* node,
const Position& pos,
EditingState* editing_state) {
// insert node before, after, or at split of tab span
InsertNodeAt(node, PositionOutsideTabSpan(pos), editing_state);
}
bool CompositeEditCommand::DeleteSelection(
EditingState* editing_state,
const DeleteSelectionOptions& options) {
if (!EndingSelection().IsRange())
return true;
ApplyCommandToComposite(
MakeGarbageCollected<DeleteSelectionCommand>(GetDocument(), options),
editing_state);
if (editing_state->IsAborted())
return false;
if (!EndingSelection().IsValidFor(GetDocument())) {
editing_state->Abort();
return false;
}
return true;
}
void CompositeEditCommand::RemoveCSSProperty(Element* element,
CSSPropertyID property) {
// RemoveCSSPropertyCommand is never aborted.
ApplyCommandToComposite(MakeGarbageCollected<RemoveCSSPropertyCommand>(
GetDocument(), element, property),
ASSERT_NO_EDITING_ABORT);
}
void CompositeEditCommand::RemoveElementAttribute(
Element* element,
const QualifiedName& attribute) {
SetNodeAttribute(element, attribute, AtomicString());
}
void CompositeEditCommand::SetNodeAttribute(Element* element,
const QualifiedName& attribute,
const AtomicString& value) {
// SetNodeAttributeCommand is never aborted.
ApplyCommandToComposite(
MakeGarbageCollected<SetNodeAttributeCommand>(element, attribute, value),
ASSERT_NO_EDITING_ABORT);
}
bool CompositeEditCommand::CanRebalance(const Position& position) const {
// TODO(editing-dev): Use of UpdateStyleAndLayout()
// needs to be audited. See http://crbug.com/590369 for more details.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
auto* text_node = DynamicTo<Text>(position.ComputeContainerNode());
if (!position.IsOffsetInAnchor() || !text_node ||
!HasRichlyEditableStyle(*text_node))
return false;
if (text_node->length() == 0)
return false;
LayoutText* layout_text = text_node->GetLayoutObject();
if (layout_text && !layout_text->Style()->CollapseWhiteSpace())
return false;
return true;
}
// FIXME: Doesn't go into text nodes that contribute adjacent text (siblings,
// cousins, etc).
void CompositeEditCommand::RebalanceWhitespaceAt(const Position& position) {
Node* node = position.ComputeContainerNode();
if (!CanRebalance(position))
return;
// If the rebalance is for the single offset, and neither text[offset] nor
// text[offset - 1] are some form of whitespace, do nothing.
int offset = position.ComputeOffsetInContainerNode();
String text = To<Text>(node)->data();
if (!IsWhitespace(text[offset])) {
offset--;
if (offset < 0 || !IsWhitespace(text[offset]))
return;
}
RebalanceWhitespaceOnTextSubstring(To<Text>(node),
position.OffsetInContainerNode(),
position.OffsetInContainerNode());
}
void CompositeEditCommand::RebalanceWhitespaceOnTextSubstring(Text* text_node,
int start_offset,
int end_offset) {
String text = text_node->data();
DCHECK(!text.IsEmpty());
// Set upstream and downstream to define the extent of the whitespace
// surrounding text[offset].
int upstream = start_offset;
while (upstream > 0 && IsWhitespace(text[upstream - 1]))
upstream--;
int downstream = end_offset;
while ((unsigned)downstream < text.length() && IsWhitespace(text[downstream]))
downstream++;
int length = downstream - upstream;
if (!length)
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
VisiblePosition visible_upstream_pos =
CreateVisiblePosition(Position(text_node, upstream));
VisiblePosition visible_downstream_pos =
CreateVisiblePosition(Position(text_node, downstream));
String string = text.Substring(upstream, length);
// FIXME: Because of the problem mentioned at the top of this function, we
// must also use nbsps at the start/end of the string because this function
// doesn't get all surrounding whitespace, just the whitespace in the
// current text node. However, if the next sibling node is a text node
// (not empty, see http://crbug.com/632300), we should use a plain space.
// See http://crbug.com/310149
auto* next_text_node = DynamicTo<Text>(text_node->nextSibling());
const bool next_sibling_is_text_node =
next_text_node && next_text_node->data().length() &&
!IsWhitespace(next_text_node->data()[0]);
const bool should_emit_nbs_pbefore_end =
(IsEndOfParagraph(visible_downstream_pos) ||
(unsigned)downstream == text.length()) &&
!next_sibling_is_text_node;
String rebalanced_string = StringWithRebalancedWhitespace(
string, IsStartOfParagraph(visible_upstream_pos) || !upstream,
should_emit_nbs_pbefore_end);
if (string != rebalanced_string)
ReplaceTextInNode(text_node, upstream, length, rebalanced_string);
}
void CompositeEditCommand::PrepareWhitespaceAtPositionForSplit(
Position& position) {
if (!IsRichlyEditablePosition(position))
return;
auto* text_node = DynamicTo<Text>(position.AnchorNode());
if (!text_node)
return;
if (text_node->length() == 0)
return;
LayoutText* layout_text = text_node->GetLayoutObject();
if (layout_text && !layout_text->Style()->CollapseWhiteSpace())
return;
// Delete collapsed whitespace so that inserting nbsps doesn't uncollapse it.
Position upstream_pos = MostBackwardCaretPosition(position);
DeleteInsignificantText(upstream_pos, MostForwardCaretPosition(position));
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
position = MostForwardCaretPosition(upstream_pos);
VisiblePosition visible_pos = CreateVisiblePosition(position);
VisiblePosition previous_visible_pos = PreviousPositionOf(visible_pos);
ReplaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(
previous_visible_pos);
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
ReplaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(
CreateVisiblePosition(position));
}
void CompositeEditCommand::
ReplaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(
const VisiblePosition& visible_position) {
if (!IsCollapsibleWhitespace(CharacterAfter(visible_position)))
return;
Position pos = MostForwardCaretPosition(visible_position.DeepEquivalent());
auto* container_text_node = DynamicTo<Text>(pos.ComputeContainerNode());
if (!container_text_node)
return;
ReplaceTextInNode(container_text_node, pos.OffsetInContainerNode(), 1,
NonBreakingSpaceString());
}
void CompositeEditCommand::RebalanceWhitespace() {
VisibleSelection selection = EndingVisibleSelection();
if (selection.IsNone())
return;
RebalanceWhitespaceAt(selection.Start());
if (selection.IsRange())
RebalanceWhitespaceAt(selection.End());
}
void CompositeEditCommand::DeleteInsignificantText(Text* text_node,
unsigned start,
unsigned end) {
if (!text_node || start >= end)
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
LayoutText* text_layout_object = text_node->GetLayoutObject();
if (!text_layout_object)
return;
if (!text_layout_object->HasInlineFragments()) {
// whole text node is empty
// Removing a Text node won't dispatch synchronous events.
RemoveNode(text_node, ASSERT_NO_EDITING_ABORT);
return;
}
unsigned length = text_node->length();
if (start >= length || end > length)
return;
if (text_layout_object->IsInLayoutNGInlineFormattingContext()) {
const String string = PlainText(
EphemeralRange(Position(*text_node, start), Position(*text_node, end)));
if (string.IsEmpty())
return DeleteTextFromNode(text_node, start, end - start);
// Replace the text between start and end with collapsed version.
return ReplaceTextInNode(text_node, start, end - start, string);
}
Vector<InlineTextBox*> sorted_text_boxes;
wtf_size_t sorted_text_boxes_position = 0;
for (InlineTextBox* text_box : text_layout_object->TextBoxes())
sorted_text_boxes.push_back(text_box);
// If there is mixed directionality text, the boxes can be out of order,
// (like Arabic with embedded LTR), so sort them first.
if (text_layout_object->ContainsReversedText())
std::sort(sorted_text_boxes.begin(), sorted_text_boxes.end(),
InlineTextBox::CompareByStart);
InlineTextBox* box = sorted_text_boxes.IsEmpty()
? 0
: sorted_text_boxes[sorted_text_boxes_position];
unsigned removed = 0;
InlineTextBox* prev_box = nullptr;
String str;
// This loop structure works to process all gaps preceding a box,
// and also will look at the gap after the last box.
while (prev_box || box) {
unsigned gap_start = prev_box ? prev_box->Start() + prev_box->Len() : 0;
if (end < gap_start) {
// No more chance for any intersections
break;
}
unsigned gap_end = box ? box->Start() : length;
bool indices_intersect = start <= gap_end && end >= gap_start;
int gap_len = gap_end - gap_start;
if (indices_intersect && gap_len > 0) {
gap_start = std::max(gap_start, start);
if (str.IsNull())
str = text_node->data().Substring(start, end - start);
// remove text in the gap
str.Remove(gap_start - start - removed, gap_len);
removed += gap_len;
}
prev_box = box;
if (box) {
if (++sorted_text_boxes_position < sorted_text_boxes.size())
box = sorted_text_boxes[sorted_text_boxes_position];
else
box = nullptr;
}
}
if (!str.IsNull()) {
// Replace the text between start and end with our pruned version.
if (!str.IsEmpty()) {
ReplaceTextInNode(text_node, start, end - start, str);
} else {
// Assert that we are not going to delete all of the text in the node.
// If we were, that should have been done above with the call to
// removeNode and return.
DCHECK(start > 0 || end - start < text_node->length());
DeleteTextFromNode(text_node, start, end - start);
}
}
}
void CompositeEditCommand::DeleteInsignificantText(const Position& start,
const Position& end) {
if (start.IsNull() || end.IsNull())
return;
if (ComparePositions(start, end) >= 0)
return;
HeapVector<Member<Text>> nodes;
for (Node& node : NodeTraversal::StartsAt(*start.AnchorNode())) {
if (auto* text_node = DynamicTo<Text>(&node))
nodes.push_back(text_node);
if (&node == end.AnchorNode())
break;
}
for (const auto& node : nodes) {
Text* text_node = node;
int start_offset = text_node == start.AnchorNode()
? start.ComputeOffsetInContainerNode()
: 0;
int end_offset = text_node == end.AnchorNode()
? end.ComputeOffsetInContainerNode()
: static_cast<int>(text_node->length());
DeleteInsignificantText(text_node, start_offset, end_offset);
}
}
void CompositeEditCommand::DeleteInsignificantTextDownstream(
const Position& pos) {
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
Position end = MostForwardCaretPosition(
NextPositionOf(CreateVisiblePosition(pos)).DeepEquivalent());
DeleteInsignificantText(pos, end);
}
HTMLBRElement* CompositeEditCommand::AppendBlockPlaceholder(
Element* container,
EditingState* editing_state) {
if (!container)
return nullptr;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Should assert isLayoutBlockFlow || isInlineFlow when deletion improves. See
// 4244964.
DCHECK(container->GetLayoutObject()) << container;
auto* placeholder = MakeGarbageCollected<HTMLBRElement>(GetDocument());
AppendNode(placeholder, container, editing_state);
if (editing_state->IsAborted())
return nullptr;
return placeholder;
}
HTMLBRElement* CompositeEditCommand::InsertBlockPlaceholder(
const Position& pos,
EditingState* editing_state) {
if (pos.IsNull())
return nullptr;
// Should assert isLayoutBlockFlow || isInlineFlow when deletion improves. See
// 4244964.
DCHECK(pos.AnchorNode()->GetLayoutObject()) << pos;
auto* placeholder = MakeGarbageCollected<HTMLBRElement>(GetDocument());
InsertNodeAt(placeholder, pos, editing_state);
if (editing_state->IsAborted())
return nullptr;
return placeholder;
}
static bool IsEmptyListItem(const LayoutBlockFlow& block_flow) {
if (block_flow.IsLayoutNGListItem())
return !block_flow.FirstChild();
if (block_flow.IsListItem())
return To<LayoutListItem>(block_flow).IsEmpty();
return false;
}
HTMLBRElement* CompositeEditCommand::AddBlockPlaceholderIfNeeded(
Element* container,
EditingState* editing_state) {
if (!container)
return nullptr;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
auto* block = DynamicTo<LayoutBlockFlow>(container->GetLayoutObject());
if (!block)
return nullptr;
// append the placeholder to make sure it follows
// any unrendered blocks
if (block->Size().Height() == 0 || IsEmptyListItem(*block))
return AppendBlockPlaceholder(container, editing_state);
return nullptr;
}
// Assumes that the position is at a placeholder and does the removal without
// much checking.
void CompositeEditCommand::RemovePlaceholderAt(const Position& p) {
DCHECK(LineBreakExistsAtPosition(p)) << p;
// We are certain that the position is at a line break, but it may be a br or
// a preserved newline.
if (IsA<HTMLBRElement>(*p.AnchorNode())) {
// Removing a BR element won't dispatch synchronous events.
RemoveNode(p.AnchorNode(), ASSERT_NO_EDITING_ABORT);
return;
}
DeleteTextFromNode(To<Text>(p.AnchorNode()), p.OffsetInContainerNode(), 1);
}
HTMLElement* CompositeEditCommand::InsertNewDefaultParagraphElementAt(
const Position& position,
EditingState* editing_state) {
HTMLElement* paragraph_element = CreateDefaultParagraphElement(GetDocument());
paragraph_element->AppendChild(
MakeGarbageCollected<HTMLBRElement>(GetDocument()));
InsertNodeAt(paragraph_element, position, editing_state);
if (editing_state->IsAborted())
return nullptr;
return paragraph_element;
}
// If the paragraph is not entirely within it's own block, create one and move
// the paragraph into it, and return that block. Otherwise return 0.
HTMLElement* CompositeEditCommand::MoveParagraphContentsToNewBlockIfNecessary(
const Position& pos,
EditingState* editing_state) {
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
DCHECK(IsEditablePosition(pos)) << pos;
// It's strange that this function is responsible for verifying that pos has
// not been invalidated by an earlier call to this function. The caller,
// applyBlockStyle, should do this.
VisiblePosition visible_pos = CreateVisiblePosition(pos);
VisiblePosition visible_paragraph_start = StartOfParagraph(visible_pos);
VisiblePosition visible_paragraph_end = EndOfParagraph(visible_pos);
VisiblePosition next = NextPositionOf(visible_paragraph_end);
VisiblePosition visible_end = next.IsNotNull() ? next : visible_paragraph_end;
Position upstream_start =
MostBackwardCaretPosition(visible_paragraph_start.DeepEquivalent());
Position upstream_end =
MostBackwardCaretPosition(visible_end.DeepEquivalent());
// If there are no VisiblePositions in the same block as pos then
// upstreamStart will be outside the paragraph
if (ComparePositions(pos, upstream_start) < 0)
return nullptr;
// Perform some checks to see if we need to perform work in this function.
if (IsEnclosingBlock(upstream_start.AnchorNode())) {
// If the block is the root editable element, always move content to a new
// block, since it is illegal to modify attributes on the root editable
// element for editing.
if (upstream_start.AnchorNode() == RootEditableElementOf(upstream_start)) {
// If the block is the root editable element and it contains no visible
// content, create a new block but don't try and move content into it,
// since there's nothing for moveParagraphs to move.
if (!HasRenderedNonAnonymousDescendantsWithHeight(
upstream_start.AnchorNode()->GetLayoutObject()))
return InsertNewDefaultParagraphElementAt(upstream_start,
editing_state);
} else if (IsEnclosingBlock(upstream_end.AnchorNode())) {
if (!upstream_end.AnchorNode()->IsDescendantOf(
upstream_start.AnchorNode())) {
// If the paragraph end is a descendant of paragraph start, then we need
// to run the rest of this function. If not, we can bail here.
return nullptr;
}
} else if (EnclosingBlock(upstream_end.AnchorNode()) !=
upstream_start.AnchorNode()) {
// It should be an ancestor of the paragraph start.
// We can bail as we have a full block to work with.
return nullptr;
} else if (IsEndOfEditableOrNonEditableContent(visible_end)) {
// At the end of the editable region. We can bail here as well.
return nullptr;
}
}
if (visible_paragraph_end.IsNull())
return nullptr;
HTMLElement* const new_block =
InsertNewDefaultParagraphElementAt(upstream_start, editing_state);
if (editing_state->IsAborted())
return nullptr;
DCHECK(new_block);
bool end_was_br =
IsA<HTMLBRElement>(*visible_paragraph_end.DeepEquivalent().AnchorNode());
// Inserting default paragraph element can change visible position. We
// should update visible positions before use them.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
const VisiblePosition& destination =
VisiblePosition::FirstPositionInNode(*new_block);
if (destination.IsNull()) {
// Reached by CompositeEditingCommandTest
// .MoveParagraphContentsToNewBlockWithNonEditableStyle.
editing_state->Abort();
return nullptr;
}
visible_pos = CreateVisiblePosition(pos);
visible_paragraph_start = StartOfParagraph(visible_pos);
visible_paragraph_end = EndOfParagraph(visible_pos);
DCHECK_LE(visible_paragraph_start.DeepEquivalent(),
visible_paragraph_end.DeepEquivalent());
MoveParagraphs(visible_paragraph_start, visible_paragraph_end, destination,
editing_state);
if (editing_state->IsAborted())
return nullptr;
if (new_block->lastChild() && IsA<HTMLBRElement>(*new_block->lastChild()) &&
!end_was_br) {
RemoveNode(new_block->lastChild(), editing_state);
if (editing_state->IsAborted())
return nullptr;
}
return new_block;
}
void CompositeEditCommand::PushAnchorElementDown(Element* anchor_node,
EditingState* editing_state) {
if (!anchor_node)
return;
DCHECK(anchor_node->IsLink()) << anchor_node;
const VisibleSelection& visible_selection = CreateVisibleSelection(
SelectionInDOMTree::Builder().SelectAllChildren(*anchor_node).Build());
SetEndingSelection(
SelectionForUndoStep::From(visible_selection.AsSelection()));
ApplyStyledElement(anchor_node, editing_state);
if (editing_state->IsAborted())
return;
// Clones of anchorNode have been pushed down, now remove it.
if (anchor_node->isConnected())
RemoveNodePreservingChildren(anchor_node, editing_state);
}
// Clone the paragraph between start and end under blockElement,
// preserving the hierarchy up to outerNode.
void CompositeEditCommand::CloneParagraphUnderNewElement(
const Position& start,
const Position& end,
Node* passed_outer_node,
Element* block_element,
EditingState* editing_state) {
DCHECK_LE(start, end);
DCHECK(passed_outer_node);
DCHECK(block_element);
// First we clone the outerNode
Node* last_node = nullptr;
Node* outer_node = passed_outer_node;
if (IsRootEditableElement(*outer_node)) {
last_node = block_element;
} else {
last_node = outer_node->cloneNode(IsDisplayInsideTable(outer_node));
AppendNode(last_node, block_element, editing_state);
if (editing_state->IsAborted())
return;
}
if (start.AnchorNode() != outer_node && last_node->IsElementNode() &&
start.AnchorNode()->IsDescendantOf(outer_node)) {
HeapVector<Member<Node>> ancestors;
// Insert each node from innerNode to outerNode (excluded) in a list.
for (Node& runner :
NodeTraversal::InclusiveAncestorsOf(*start.AnchorNode())) {
if (runner == outer_node)
break;
ancestors.push_back(runner);
}
// Clone every node between start.anchorNode() and outerBlock.
for (wtf_size_t i = ancestors.size(); i != 0; --i) {
Node* item = ancestors[i - 1].Get();
Node* child = item->cloneNode(IsDisplayInsideTable(item));
AppendNode(child, To<Element>(last_node), editing_state);
if (editing_state->IsAborted())
return;
last_node = child;
}
}
// Scripts specified in javascript protocol may remove |outerNode|
// during insertion, e.g. <iframe src="javascript:...">
if (!outer_node->isConnected())
return;
// Handle the case of paragraphs with more than one node,
// cloning all the siblings until end.anchorNode() is reached.
if (start.AnchorNode() != end.AnchorNode() &&
!start.AnchorNode()->IsDescendantOf(end.AnchorNode())) {
// If end is not a descendant of outerNode we need to
// find the first common ancestor to increase the scope
// of our nextSibling traversal.
while (outer_node && !end.AnchorNode()->IsDescendantOf(outer_node)) {
outer_node = outer_node->parentNode();
}
if (!outer_node)
return;
Node* start_node = start.AnchorNode();
for (Node* node =
NodeTraversal::NextSkippingChildren(*start_node, outer_node);
node; node = NodeTraversal::NextSkippingChildren(*node, outer_node)) {
// Move lastNode up in the tree as much as node was moved up in the tree
// by NodeTraversal::nextSkippingChildren, so that the relative depth
// between node and the original start node is maintained in the clone.
while (start_node && last_node &&
start_node->parentNode() != node->parentNode()) {
start_node = start_node->parentNode();
last_node = last_node->parentNode();
}
if (!last_node || !last_node->parentNode())
return;
Node* cloned_node = node->cloneNode(true);
InsertNodeAfter(cloned_node, last_node, editing_state);
if (editing_state->IsAborted())
return;
last_node = cloned_node;
if (node == end.AnchorNode() || end.AnchorNode()->IsDescendantOf(node))
break;
}
}
}
// There are bugs in deletion when it removes a fully selected table/list.
// It expands and removes the entire table/list, but will let content
// before and after the table/list collapse onto one line.
// Deleting a paragraph will leave a placeholder. Remove it (and prune
// empty or unrendered parents).
void CompositeEditCommand::CleanupAfterDeletion(EditingState* editing_state) {
CleanupAfterDeletion(editing_state, VisiblePosition());
}
void CompositeEditCommand::CleanupAfterDeletion(EditingState* editing_state,
VisiblePosition destination) {
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
VisiblePosition caret_after_delete = EndingVisibleSelection().VisibleStart();
Node* destination_node = destination.DeepEquivalent().AnchorNode();
if (caret_after_delete.DeepEquivalent() != destination.DeepEquivalent() &&
IsStartOfParagraph(caret_after_delete) &&
IsEndOfParagraph(caret_after_delete)) {
// Note: We want the rightmost candidate.
Position position =
MostForwardCaretPosition(caret_after_delete.DeepEquivalent());
Node* node = position.AnchorNode();
// InsertListCommandTest.CleanupNodeSameAsDestinationNode reaches here.
ABORT_EDITING_COMMAND_IF(destination_node == node);
// Bail if we'd remove an ancestor of our destination.
if (destination_node && destination_node->IsDescendantOf(node))
return;
// Normally deletion will leave a br as a placeholder.
if (IsA<HTMLBRElement>(*node)) {
RemoveNodeAndPruneAncestors(node, editing_state, destination_node);
// If the selection to move was empty and in an empty block that
// doesn't require a placeholder to prop itself open (like a bordered
// div or an li), remove it during the move (the list removal code
// expects this behavior).
} else if (IsEnclosingBlock(node)) {
// If caret position after deletion and destination position coincides,
// node should not be removed.
if (!RendersInDifferentPosition(position, destination.DeepEquivalent())) {
Prune(node, editing_state, destination_node);
return;
}
RemoveNodeAndPruneAncestors(node, editing_state, destination_node);
} else if (LineBreakExistsAtPosition(position)) {
// There is a preserved '\n' at caretAfterDelete.
// We can safely assume this is a text node.
auto* text_node = To<Text>(node);
if (text_node->length() == 1)
RemoveNodeAndPruneAncestors(node, editing_state, destination_node);
else
DeleteTextFromNode(text_node, position.ComputeOffsetInContainerNode(),
1);
}
}
}
// This is a version of moveParagraph that preserves style by keeping the
// original markup. It is currently used only by IndentOutdentCommand but it is
// meant to be used in the future by several other commands such as InsertList
// and the align commands.
// The blockElement parameter is the element to move the paragraph to, outerNode
// is the top element of the paragraph hierarchy.
void CompositeEditCommand::MoveParagraphWithClones(
const VisiblePosition& start_of_paragraph_to_move,
const VisiblePosition& end_of_paragraph_to_move,
HTMLElement* block_element,
Node* outer_node,
EditingState* editing_state) {
// InsertListCommandTest.InsertListWithCollapsedVisibility reaches here.
ABORT_EDITING_COMMAND_IF(start_of_paragraph_to_move.IsNull());
ABORT_EDITING_COMMAND_IF(end_of_paragraph_to_move.IsNull());
DCHECK(outer_node);
DCHECK(block_element);
RelocatablePosition relocatable_before_paragraph(
PreviousPositionOf(start_of_paragraph_to_move).DeepEquivalent());
RelocatablePosition relocatable_after_paragraph(
NextPositionOf(end_of_paragraph_to_move).DeepEquivalent());
// We upstream() the end and downstream() the start so that we don't include
// collapsed whitespace in the move. When we paste a fragment, spaces after
// the end and before the start are treated as though they were rendered.
Position start =
MostForwardCaretPosition(start_of_paragraph_to_move.DeepEquivalent());
Position end = start_of_paragraph_to_move.DeepEquivalent() ==
end_of_paragraph_to_move.DeepEquivalent()
? start
: MostBackwardCaretPosition(
end_of_paragraph_to_move.DeepEquivalent());
if (ComparePositions(start, end) > 0)
end = start;
CloneParagraphUnderNewElement(start, end, outer_node, block_element,
editing_state);
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder().Collapse(start).Extend(end).Build()));
if (!DeleteSelection(
editing_state,
DeleteSelectionOptions::Builder().SetSanitizeMarkup(true).Build()))
return;
// There are bugs in deletion when it removes a fully selected table/list.
// It expands and removes the entire table/list, but will let content
// before and after the table/list collapse onto one line.
CleanupAfterDeletion(editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Add a br if pruning an empty block level element caused a collapse. For
// example:
// foo^
// <div>bar</div>
// baz
// Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That
// would cause 'baz' to collapse onto the line with 'foobar' unless we insert
// a br. Must recononicalize these two VisiblePositions after the pruning
// above.
const VisiblePosition& before_paragraph =
CreateVisiblePosition(relocatable_before_paragraph.GetPosition());
const VisiblePosition& after_paragraph =
CreateVisiblePosition(relocatable_after_paragraph.GetPosition());
if (before_paragraph.IsNotNull() &&
!IsDisplayInsideTable(before_paragraph.DeepEquivalent().AnchorNode()) &&
((!IsEndOfParagraph(before_paragraph) &&
!IsStartOfParagraph(before_paragraph)) ||
before_paragraph.DeepEquivalent() == after_paragraph.DeepEquivalent())) {
// FIXME: Trim text between beforeParagraph and afterParagraph if they
// aren't equal.
InsertNodeAt(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
before_paragraph.DeepEquivalent(), editing_state);
}
}
void CompositeEditCommand::MoveParagraph(
const VisiblePosition& start_of_paragraph_to_move,
const VisiblePosition& end_of_paragraph_to_move,
const VisiblePosition& destination,
EditingState* editing_state,
ShouldPreserveSelection should_preserve_selection,
ShouldPreserveStyle should_preserve_style,
Node* constraining_ancestor) {
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
DCHECK(IsStartOfParagraph(start_of_paragraph_to_move))
<< start_of_paragraph_to_move;
DCHECK(IsEndOfParagraph(end_of_paragraph_to_move))
<< end_of_paragraph_to_move;
MoveParagraphs(start_of_paragraph_to_move, end_of_paragraph_to_move,
destination, editing_state, should_preserve_selection,
should_preserve_style, constraining_ancestor);
}
void CompositeEditCommand::MoveParagraphs(
const VisiblePosition& start_of_paragraph_to_move,
const VisiblePosition& end_of_paragraph_to_move,
const VisiblePosition& destination,
EditingState* editing_state,
ShouldPreserveSelection should_preserve_selection,
ShouldPreserveStyle should_preserve_style,
Node* constraining_ancestor) {
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
DCHECK(start_of_paragraph_to_move.IsNotNull());
DCHECK(end_of_paragraph_to_move.IsNotNull());
DCHECK(destination.IsNotNull());
if (start_of_paragraph_to_move.DeepEquivalent() ==
destination.DeepEquivalent() ||
start_of_paragraph_to_move.IsNull())
return;
// Can't move the range to a destination inside itself.
if (destination.DeepEquivalent() >=
start_of_paragraph_to_move.DeepEquivalent() &&
destination.DeepEquivalent() <=
end_of_paragraph_to_move.DeepEquivalent()) {
// Reached by unit test TypingCommandTest.insertLineBreakWithIllFormedHTML
// and ApplyStyleCommandTest.JustifyRightDetachesDestination
editing_state->Abort();
return;
}
int start_index = -1;
int end_index = -1;
int destination_index = -1;
if (should_preserve_selection == kPreserveSelection &&
!EndingSelection().IsNone()) {
VisiblePosition visible_start = EndingVisibleSelection().VisibleStart();
VisiblePosition visible_end = EndingVisibleSelection().VisibleEnd();
bool start_after_paragraph =
ComparePositions(visible_start, end_of_paragraph_to_move) > 0;
bool end_before_paragraph =
ComparePositions(visible_end, start_of_paragraph_to_move) < 0;
if (!start_after_paragraph && !end_before_paragraph) {
bool start_in_paragraph =
ComparePositions(visible_start, start_of_paragraph_to_move) >= 0;
bool end_in_paragraph =
ComparePositions(visible_end, end_of_paragraph_to_move) <= 0;
const TextIteratorBehavior behavior =
TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior();
start_index = 0;
if (start_in_paragraph) {
start_index = TextIterator::RangeLength(
start_of_paragraph_to_move.ToParentAnchoredPosition(),
visible_start.ToParentAnchoredPosition(), behavior);
}
end_index = 0;
if (end_in_paragraph) {
end_index = TextIterator::RangeLength(
start_of_paragraph_to_move.ToParentAnchoredPosition(),
visible_end.ToParentAnchoredPosition(), behavior);
}
}
}
RelocatablePosition before_paragraph_position(
PreviousPositionOf(start_of_paragraph_to_move,
kCannotCrossEditingBoundary)
.DeepEquivalent());
RelocatablePosition after_paragraph_position(
NextPositionOf(end_of_paragraph_to_move, kCannotCrossEditingBoundary)
.DeepEquivalent());
const Position& start_candidate = start_of_paragraph_to_move.DeepEquivalent();
const Position& end_candidate = end_of_paragraph_to_move.DeepEquivalent();
DCHECK_LE(start_candidate, end_candidate);
// We upstream() the end and downstream() the start so that we don't include
// collapsed whitespace in the move. When we paste a fragment, spaces after
// the end and before the start are treated as though they were rendered.
bool equal = start_candidate == end_candidate;
Position start =
equal ? start_candidate : MostForwardCaretPosition(start_candidate);
Position end =
equal ? end_candidate : MostBackwardCaretPosition(end_candidate);
DCHECK_LE(start, end);
// FIXME: This is an inefficient way to preserve style on nodes in the
// paragraph to move. It shouldn't matter though, since moved paragraphs will
// usually be quite small.
DocumentFragment* fragment = nullptr;
if (start_of_paragraph_to_move.DeepEquivalent() !=
end_of_paragraph_to_move.DeepEquivalent()) {
const String paragraphs_markup = CreateMarkup(
start.ParentAnchoredEquivalent(), end.ParentAnchoredEquivalent(),
CreateMarkupOptions::Builder()
.SetShouldConvertBlocksToInlines(true)
.SetConstrainingAncestor(constraining_ancestor)
.Build());
fragment = CreateSanitizedFragmentFromMarkupWithContext(
GetDocument(), paragraphs_markup, 0, paragraphs_markup.length(), "");
}
// A non-empty paragraph's style is moved when we copy and move it. We don't
// move anything if we're given an empty paragraph, but an empty paragraph can
// have style too, <div><b><br></b></div> for example. Save it so that we can
// preserve it later.
EditingStyle* style_in_empty_paragraph = nullptr;
if (start_of_paragraph_to_move.DeepEquivalent() ==
end_of_paragraph_to_move.DeepEquivalent() &&
should_preserve_style == kPreserveStyle) {
style_in_empty_paragraph = MakeGarbageCollected<EditingStyle>(
start_of_paragraph_to_move.DeepEquivalent());
style_in_empty_paragraph->MergeTypingStyle(&GetDocument());
// The moved paragraph should assume the block style of the destination.
style_in_empty_paragraph->RemoveBlockProperties(
GetDocument().GetExecutionContext());
}
// FIXME (5098931): We should add a new insert action
// "WebViewInsertActionMoved" and call shouldInsertFragment here.
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
const VisibleSelection& selection_to_delete = CreateVisibleSelection(
SelectionInDOMTree::Builder().Collapse(start).Extend(end).Build());
SetEndingSelection(
SelectionForUndoStep::From(selection_to_delete.AsSelection()));
if (!DeleteSelection(
editing_state,
DeleteSelectionOptions::Builder().SetSanitizeMarkup(true).Build()))
return;
DCHECK(destination.DeepEquivalent().IsConnected()) << destination;
CleanupAfterDeletion(editing_state, destination);
if (editing_state->IsAborted())
return;
DCHECK(destination.DeepEquivalent().IsConnected()) << destination;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Add a br if pruning an empty block level element caused a collapse. For
// example:
// foo^
// <div>bar</div>
// baz
// Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That
// would cause 'baz' to collapse onto the line with 'foobar' unless we insert
// a br. Must recononicalize these two VisiblePositions after the pruning
// above.
VisiblePosition before_paragraph =
CreateVisiblePosition(before_paragraph_position.GetPosition());
VisiblePosition after_paragraph =
CreateVisiblePosition(after_paragraph_position.GetPosition());
if (before_paragraph.IsNotNull() &&
((!IsStartOfParagraph(before_paragraph) &&
!IsEndOfParagraph(before_paragraph)) ||
before_paragraph.DeepEquivalent() == after_paragraph.DeepEquivalent())) {
// FIXME: Trim text between beforeParagraph and afterParagraph if they
// aren't equal.
InsertNodeAt(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
before_paragraph.DeepEquivalent(), editing_state);
if (editing_state->IsAborted())
return;
}
// TextIterator::rangeLength requires clean layout.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
destination_index = TextIterator::RangeLength(
Position::FirstPositionInNode(*GetDocument().documentElement()),
destination.ToParentAnchoredPosition(),
TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior());
const VisibleSelection& destination_selection =
CreateVisibleSelection(SelectionInDOMTree::Builder()
.Collapse(destination.ToPositionWithAffinity())
.Build());
if (EndingSelection().IsNone()) {
// We abort executing command since |destination| becomes invisible.
editing_state->Abort();
return;
}
SetEndingSelection(
SelectionForUndoStep::From(destination_selection.AsSelection()));
ReplaceSelectionCommand::CommandOptions options =
ReplaceSelectionCommand::kSelectReplacement |
ReplaceSelectionCommand::kMovingParagraph;
if (should_preserve_style == kDoNotPreserveStyle)
options |= ReplaceSelectionCommand::kMatchStyle;
ApplyCommandToComposite(MakeGarbageCollected<ReplaceSelectionCommand>(
GetDocument(), fragment, options),
editing_state);
if (editing_state->IsAborted())
return;
ABORT_EDITING_COMMAND_IF(!EndingSelection().IsValidFor(GetDocument()));
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// If the selection is in an empty paragraph, restore styles from the old
// empty paragraph to the new empty paragraph.
bool selection_is_empty_paragraph =
EndingSelection().IsCaret() &&
IsStartOfParagraph(EndingVisibleSelection().VisibleStart()) &&
IsEndOfParagraph(EndingVisibleSelection().VisibleStart());
if (style_in_empty_paragraph && selection_is_empty_paragraph) {
ApplyStyle(style_in_empty_paragraph, editing_state);
if (editing_state->IsAborted())
return;
}
if (should_preserve_selection == kDoNotPreserveSelection || start_index == -1)
return;
Element* document_element = GetDocument().documentElement();
if (!document_element)
return;
// We need clean layout in order to compute plain-text ranges below.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Fragment creation (using createMarkup) incorrectly uses regular spaces
// instead of nbsps for some spaces that were rendered (11475), which causes
// spaces to be collapsed during the move operation. This results in a call
// to rangeFromLocationAndLength with a location past the end of the
// document (which will return null).
EphemeralRange start_range = PlainTextRange(destination_index + start_index)
.CreateRangeForSelection(*document_element);
if (start_range.IsNull())
return;
EphemeralRange end_range = PlainTextRange(destination_index + end_index)
.CreateRangeForSelection(*document_element);
if (end_range.IsNull())
return;
const VisibleSelection& visible_selection =
CreateVisibleSelection(SelectionInDOMTree::Builder()
.Collapse(start_range.StartPosition())
.Extend(end_range.StartPosition())
.Build());
SetEndingSelection(
SelectionForUndoStep::From(visible_selection.AsSelection()));
}
// FIXME: Send an appropriate shouldDeleteRange call.
bool CompositeEditCommand::BreakOutOfEmptyListItem(
EditingState* editing_state) {
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
Node* empty_list_item =
EnclosingEmptyListItem(EndingVisibleSelection().VisibleStart());
if (!empty_list_item)
return false;
EditingStyle* style =
MakeGarbageCollected<EditingStyle>(EndingSelection().Start());
style->MergeTypingStyle(&GetDocument());
ContainerNode* list_node = empty_list_item->parentNode();
// FIXME: Can't we do something better when the immediate parent wasn't a list
// node?
if (!list_node ||
(!IsA<HTMLUListElement>(*list_node) &&
!IsA<HTMLOListElement>(*list_node)) ||
!HasEditableStyle(*list_node) ||
list_node == RootEditableElement(*empty_list_item))
return false;
HTMLElement* new_block = nullptr;
if (ContainerNode* block_enclosing_list = list_node->parentNode()) {
if (block_enclosing_list->HasTagName(
html_names::kLiTag)) { // listNode is inside another list item
if (CreateVisiblePosition(PositionAfterNode(*block_enclosing_list))
.DeepEquivalent() ==
CreateVisiblePosition(PositionAfterNode(*list_node))
.DeepEquivalent()) {
// If listNode appears at the end of the outer list item, then move
// listNode outside of this list item, e.g.
// <ul><li>hello <ul><li><br></li></ul> </li></ul>
// should become
// <ul><li>hello</li> <ul><li><br></li></ul> </ul>
// after this section.
//
// If listNode does NOT appear at the end, then we should consider it as
// a regular paragraph, e.g.
// <ul><li> <ul><li><br></li></ul> hello</li></ul>
// should become
// <ul><li> <div><br></div> hello</li></ul>
// at the end
SplitElement(To<Element>(block_enclosing_list), list_node);
RemoveNodePreservingChildren(list_node->parentNode(), editing_state);
if (editing_state->IsAborted())
return false;
new_block = MakeGarbageCollected<HTMLLIElement>(GetDocument());
}
// If listNode does NOT appear at the end of the outer list item, then
// behave as if in a regular paragraph.
} else if (block_enclosing_list->HasTagName(html_names::kOlTag) ||
block_enclosing_list->HasTagName(html_names::kUlTag)) {
new_block = MakeGarbageCollected<HTMLLIElement>(GetDocument());
}
}
if (!new_block)
new_block = CreateDefaultParagraphElement(GetDocument());
Node* previous_list_node =
empty_list_item->IsElementNode()
? ElementTraversal::PreviousSibling(*empty_list_item)
: empty_list_item->previousSibling();
Node* next_list_node = empty_list_item->IsElementNode()
? ElementTraversal::NextSibling(*empty_list_item)
: empty_list_item->nextSibling();
if (next_list_node && IsListElementTag(list_node)) {
// If emptyListItem follows another list item or nested list, split the list
// node.
if (IsListItemTag(previous_list_node) ||
IsHTMLListElement(previous_list_node)) {
SplitElement(To<Element>(list_node), empty_list_item);
}
// If emptyListItem is followed by other list item or nested list, then
// insert newBlock before the list node. Because we have split the
// element, emptyListItem is the first element in the list node.
// i.e. insert newBlock before ul or ol whose first element is emptyListItem
InsertNodeBefore(new_block, list_node, editing_state);
if (editing_state->IsAborted())
return false;
RemoveNode(empty_list_item, editing_state);
if (editing_state->IsAborted())
return false;
} else {
// When emptyListItem does not follow any list item or nested list, insert
// newBlock after the enclosing list node. Remove the enclosing node if
// emptyListItem is the only child; otherwise just remove emptyListItem.
// <ul> <ul>
// <li> <li>
// abc abc
// <ul> <ul>
// <li>def</li> <li>def</li>
// <li>{}<br></li> -> </ul>
// </ul> <div>{}<br></div>
// ghi ghi
// </li> </li>
// </ul> </ul>
InsertNodeAfter(new_block, list_node, editing_state);
if (editing_state->IsAborted())
return false;
RemoveNode(previous_list_node ? empty_list_item : list_node, editing_state);
if (editing_state->IsAborted())
return false;
}
AppendBlockPlaceholder(new_block, editing_state);
if (editing_state->IsAborted())
return false;
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(Position::FirstPositionInNode(*new_block))
.Build()));
style->PrepareToApplyAt(EndingSelection().Start());
if (!style->IsEmpty()) {
ApplyStyle(style, editing_state);
if (editing_state->IsAborted())
return false;
}
return true;
}
// If the caret is in an empty quoted paragraph, and either there is nothing
// before that paragraph, or what is before is unquoted, and the user presses
// delete, unquote that paragraph.
bool CompositeEditCommand::BreakOutOfEmptyMailBlockquotedParagraph(
EditingState* editing_state) {
if (!EndingSelection().IsCaret())
return false;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
VisiblePosition caret = EndingVisibleSelection().VisibleStart();
auto* highest_blockquote = To<HTMLQuoteElement>(HighestEnclosingNodeOfType(
caret.DeepEquivalent(), &IsMailHTMLBlockquoteElement));
if (!highest_blockquote)
return false;
if (!IsStartOfParagraph(caret) || !IsEndOfParagraph(caret))
return false;
VisiblePosition previous =
PreviousPositionOf(caret, kCannotCrossEditingBoundary);
// Only move forward if there's nothing before the caret, or if there's
// unquoted content before it.
if (EnclosingNodeOfType(previous.DeepEquivalent(),
&IsMailHTMLBlockquoteElement))
return false;
auto* br = MakeGarbageCollected<HTMLBRElement>(GetDocument());
// We want to replace this quoted paragraph with an unquoted one, so insert a
// br to hold the caret before the highest blockquote.
InsertNodeBefore(br, highest_blockquote, editing_state);
if (editing_state->IsAborted())
return false;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
VisiblePosition at_br = VisiblePosition::BeforeNode(*br);
// If the br we inserted collapsed, for example:
// foo<br><blockquote>...</blockquote>
// insert a second one.
if (!IsStartOfParagraph(at_br)) {
InsertNodeBefore(MakeGarbageCollected<HTMLBRElement>(GetDocument()), br,
editing_state);
if (editing_state->IsAborted())
return false;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
}
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(at_br.ToPositionWithAffinity())
.Build()));
// If this is an empty paragraph there must be a line break here.
if (!LineBreakExistsAtVisiblePosition(caret))
return false;
Position caret_pos(MostForwardCaretPosition(caret.DeepEquivalent()));
// A line break is either a br or a preserved newline.
DCHECK(
IsA<HTMLBRElement>(caret_pos.AnchorNode()) ||
(caret_pos.AnchorNode()->IsTextNode() &&
caret_pos.AnchorNode()->GetLayoutObject()->Style()->PreserveNewline()))
<< caret_pos;
if (IsA<HTMLBRElement>(*caret_pos.AnchorNode())) {
RemoveNodeAndPruneAncestors(caret_pos.AnchorNode(), editing_state);
if (editing_state->IsAborted())
return false;
} else if (auto* text_node = DynamicTo<Text>(caret_pos.AnchorNode())) {
DCHECK_EQ(caret_pos.ComputeOffsetInContainerNode(), 0);
ContainerNode* parent_node = text_node->parentNode();
// The preserved newline must be the first thing in the node, since
// otherwise the previous paragraph would be quoted, and we verified that it
// wasn't above.
DeleteTextFromNode(text_node, 0, 1);
Prune(parent_node, editing_state);
if (editing_state->IsAborted())
return false;
}
return true;
}
// Operations use this function to avoid inserting content into an anchor when
// at the start or the end of that anchor, as in NSTextView.
// FIXME: This is only an approximation of NSTextViews insertion behavior, which
// varies depending on how the caret was made.
Position CompositeEditCommand::PositionAvoidingSpecialElementBoundary(
const Position& original,
EditingState* editing_state) {
if (original.IsNull())
return original;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
VisiblePosition visible_pos = CreateVisiblePosition(original);
Element* enclosing_anchor = EnclosingAnchorElement(original);
Position result = original;
if (!enclosing_anchor)
return result;
// Don't avoid block level anchors, because that would insert content into the
// wrong paragraph.
if (enclosing_anchor && !IsEnclosingBlock(enclosing_anchor)) {
VisiblePosition first_in_anchor =
VisiblePosition::FirstPositionInNode(*enclosing_anchor);
VisiblePosition last_in_anchor =
VisiblePosition::LastPositionInNode(*enclosing_anchor);
// If visually just after the anchor, insert *inside* the anchor unless it's
// the last VisiblePosition in the document, to match NSTextView.
if (visible_pos.DeepEquivalent() == last_in_anchor.DeepEquivalent()) {
// Make sure anchors are pushed down before avoiding them so that we don't
// also avoid structural elements like lists and blocks (5142012).
Element* enclosing_block = EnclosingBlock(original.AnchorNode());
if (enclosing_block &&
enclosing_block->IsDescendantOf(enclosing_anchor)) {
// Only push down anchor element if there are block elements inside it.
PushAnchorElementDown(enclosing_anchor, editing_state);
if (editing_state->IsAborted())
return original;
enclosing_anchor = EnclosingAnchorElement(original);
if (!enclosing_anchor)
return original;
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Don't insert outside an anchor if doing so would skip over a line
// break. It would probably be safe to move the line break so that we
// could still avoid the anchor here.
Position downstream(
MostForwardCaretPosition(visible_pos.DeepEquivalent()));
if (LineBreakExistsAtVisiblePosition(visible_pos) &&
downstream.AnchorNode()->IsDescendantOf(enclosing_anchor))
return original;
result = Position::InParentAfterNode(*enclosing_anchor);
}
// If visually just before an anchor, insert *outside* the anchor unless
// it's the first VisiblePosition in a paragraph, to match NSTextView.
if (visible_pos.DeepEquivalent() == first_in_anchor.DeepEquivalent()) {
// Make sure anchors are pushed down before avoiding them so that we don't
// also avoid structural elements like lists and blocks (5142012).
Element* enclosing_block = EnclosingBlock(original.AnchorNode());
if (enclosing_block &&
enclosing_block->IsDescendantOf(enclosing_anchor)) {
// Only push down anchor element if there are block elements inside it.
PushAnchorElementDown(enclosing_anchor, editing_state);
if (editing_state->IsAborted())
return original;
enclosing_anchor = EnclosingAnchorElement(original);
}
if (!enclosing_anchor)
return original;
result = Position::InParentBeforeNode(*enclosing_anchor);
}
}
if (result.IsNull() || !RootEditableElementOf(result))
result = original;
return result;
}
// Splits the tree parent by parent until we reach the specified ancestor. We
// use VisiblePositions to determine if the split is necessary. Returns the last
// split node.
Node* CompositeEditCommand::SplitTreeToNode(Node* start,
Node* end,
bool should_split_ancestor) {
DCHECK(start);
DCHECK(end);
DCHECK_NE(start, end);
if (should_split_ancestor && end->parentNode())
end = end->parentNode();
if (!start->IsDescendantOf(end))
return end;
Node* end_node = end;
Node* node = nullptr;
for (node = start; node->parentNode() != end_node;
node = node->parentNode()) {
Element* parent_element = node->parentElement();
if (!parent_element)
break;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Do not split a node when doing so introduces an empty node.
VisiblePosition position_in_parent =
VisiblePosition::FirstPositionInNode(*parent_element);
VisiblePosition position_in_node =
CreateVisiblePosition(FirstPositionInOrBeforeNode(*node));
if (position_in_parent.DeepEquivalent() !=
position_in_node.DeepEquivalent())
SplitElement(parent_element, node);
}
return node;
}
void CompositeEditCommand::SetStartingSelection(
const SelectionForUndoStep& selection) {
for (CompositeEditCommand* command = this;; command = command->Parent()) {
if (UndoStep* undo_step = command->GetUndoStep()) {
DCHECK(command->IsTopLevelCommand());
undo_step->SetStartingSelection(selection);
}
command->starting_selection_ = selection;
if (!command->Parent() || command->Parent()->IsFirstCommand(command))
break;
}
}
void CompositeEditCommand::SetEndingSelection(
const SelectionForUndoStep& selection) {
for (CompositeEditCommand* command = this; command;
command = command->Parent()) {
if (UndoStep* undo_step = command->GetUndoStep()) {
DCHECK(command->IsTopLevelCommand());
undo_step->SetEndingSelection(selection);
}
command->ending_selection_ = selection;
}
}
void CompositeEditCommand::SetParent(CompositeEditCommand* parent) {
EditCommand::SetParent(parent);
if (!parent)
return;
starting_selection_ = parent->ending_selection_;
ending_selection_ = parent->ending_selection_;
}
// Determines whether a node is inside a range or visibly starts and ends at the
// boundaries of the range. Call this function to determine whether a node is
// visibly fit inside selectedRange
bool CompositeEditCommand::IsNodeVisiblyContainedWithin(
Node& node,
const EphemeralRange& selected_range) {
DCHECK(!NeedsLayoutTreeUpdate(node));
DocumentLifecycle::DisallowTransitionScope disallow_transition(
node.GetDocument().Lifecycle());
if (IsNodeFullyContained(selected_range, node))
return true;
bool start_is_visually_same =
CreateVisiblePosition(PositionBeforeNode(node)).DeepEquivalent() ==
CreateVisiblePosition(selected_range.StartPosition()).DeepEquivalent();
if (start_is_visually_same &&
ComparePositions(Position::InParentAfterNode(node),
selected_range.EndPosition()) < 0)
return true;
bool end_is_visually_same =
CreateVisiblePosition(PositionAfterNode(node)).DeepEquivalent() ==
CreateVisiblePosition(selected_range.EndPosition()).DeepEquivalent();
if (end_is_visually_same &&
ComparePositions(selected_range.StartPosition(),
Position::InParentBeforeNode(node)) < 0)
return true;
return start_is_visually_same && end_is_visually_same;
}
void CompositeEditCommand::Trace(Visitor* visitor) const {
visitor->Trace(commands_);
visitor->Trace(starting_selection_);
visitor->Trace(ending_selection_);
visitor->Trace(undo_step_);
EditCommand::Trace(visitor);
}
void CompositeEditCommand::AppliedEditing() {
DCHECK(!IsCommandGroupWrapper());
EventQueueScope scope;
const UndoStep& undo_step = *GetUndoStep();
DispatchEditableContentChangedEvents(undo_step.StartingRootEditableElement(),
undo_step.EndingRootEditableElement());
LocalFrame* const frame = GetDocument().GetFrame();
Editor& editor = frame->GetEditor();
// TODO(editing-dev): Filter empty InputType after spec is finalized.
DispatchInputEventEditableContentChanged(
undo_step.StartingRootEditableElement(),
undo_step.EndingRootEditableElement(), GetInputType(),
TextDataForInputEvent(), IsComposingFromCommand(this));
const SelectionInDOMTree& new_selection =
CorrectedSelectionAfterCommand(EndingSelection(), &GetDocument());
// Don't clear the typing style with this selection change. We do those things
// elsewhere if necessary.
ChangeSelectionAfterCommand(frame, new_selection,
SetSelectionOptions::Builder()
.SetIsDirectional(SelectionIsDirectional())
.Build());
if (!PreservesTypingStyle())
editor.ClearTypingStyle();
CompositeEditCommand* const last_edit_command = editor.LastEditCommand();
// Command will be equal to last edit command only in the case of typing
if (last_edit_command == this) {
DCHECK(IsTypingCommand());
} else if (last_edit_command && last_edit_command->IsDragAndDropCommand() &&
(GetInputType() == InputEvent::InputType::kDeleteByDrag ||
GetInputType() == InputEvent::InputType::kInsertFromDrop)) {
// Only register undo entry when combined with other commands.
if (!last_edit_command->GetUndoStep()) {
editor.GetUndoStack().RegisterUndoStep(
last_edit_command->EnsureUndoStep());
}
last_edit_command->EnsureUndoStep()->SetEndingSelection(
EnsureUndoStep()->EndingSelection());
last_edit_command->GetUndoStep()->SetSelectionIsDirectional(
GetUndoStep()->SelectionIsDirectional());
last_edit_command->AppendCommandToUndoStep(this);
} else {
// Only register a new undo command if the command passed in is
// different from the last command
editor.SetLastEditCommand(this);
editor.GetUndoStack().RegisterUndoStep(EnsureUndoStep());
}
editor.RespondToChangedContents(new_selection.Base());
}
} // namespace blink