blob: 7e9b7124a9ec22066be8635c66f78a41e257ed2d [file] [log] [blame]
/*
* Copyright (C) 2006, 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/indent_outdent_command.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
#include "third_party/blink/renderer/core/editing/commands/insert_list_command.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/visible_position.h"
#include "third_party/blink/renderer/core/editing/visible_selection.h"
#include "third_party/blink/renderer/core/editing/visible_units.h"
#include "third_party/blink/renderer/core/html/html_br_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
namespace blink {
using namespace html_names;
// Returns true if |node| is UL, OL, or BLOCKQUOTE with "display:block".
// "Outdent" command considers <BLOCKQUOTE style="display:inline"> makes
// indentation.
static bool IsHTMLListOrBlockquoteElement(const Node* node) {
const auto* element = DynamicTo<HTMLElement>(node);
if (!element)
return false;
if (!node->GetLayoutObject() || !node->GetLayoutObject()->IsLayoutBlock())
return false;
// TODO(yosin): We should check OL/UL element has "list-style-type" CSS
// property to make sure they layout contents as list.
return IsHTMLUListElement(*element) || IsHTMLOListElement(*element) ||
element->HasTagName(kBlockquoteTag);
}
IndentOutdentCommand::IndentOutdentCommand(Document& document,
IndentType type_of_action)
: ApplyBlockElementCommand(
document,
kBlockquoteTag,
"margin: 0 0 0 40px; border: none; padding: 0px;"),
type_of_action_(type_of_action) {}
bool IndentOutdentCommand::TryIndentingAsListItem(const Position& start,
const Position& end,
EditingState* editing_state) {
// If our selection is not inside a list, bail out.
Node* last_node_in_selected_paragraph = start.AnchorNode();
HTMLElement* list_element = EnclosingList(last_node_in_selected_paragraph);
if (!list_element)
return false;
// Find the block that we want to indent. If it's not a list item (e.g., a
// div inside a list item), we bail out.
Element* selected_list_item = EnclosingBlock(last_node_in_selected_paragraph);
// FIXME: we need to deal with the case where there is no li (malformed HTML)
if (!IsA<HTMLLIElement>(selected_list_item))
return false;
// FIXME: previousElementSibling does not ignore non-rendered content like
// <span></span>. Should we?
Element* previous_list =
ElementTraversal::PreviousSibling(*selected_list_item);
Element* next_list = ElementTraversal::NextSibling(*selected_list_item);
// We should calculate visible range in list item because inserting new
// list element will change visibility of list item, e.g. :first-child
// CSS selector.
auto* new_list = To<HTMLElement>(GetDocument().CreateElement(
list_element->TagQName(), CreateElementFlags::ByCloneNode(),
g_null_atom));
InsertNodeBefore(new_list, selected_list_item, editing_state);
if (editing_state->IsAborted())
return false;
GetDocument().UpdateStyleAndLayout();
// We should clone all the children of the list item for indenting purposes.
// However, in case the current selection does not encompass all its children,
// we need to explicitally handle the same. The original list item too would
// require proper deletion in that case.
const bool should_keep_selected_list =
end.AnchorNode() == selected_list_item ||
end.AnchorNode()->IsDescendantOf(selected_list_item->lastChild());
const VisiblePosition& start_of_paragraph_to_move =
CreateVisiblePosition(start);
const VisiblePosition& end_of_paragraph_to_move =
should_keep_selected_list
? CreateVisiblePosition(end)
: VisiblePosition::AfterNode(*selected_list_item->lastChild());
// The insertion of |newList| may change the computed style of other
// elements, resulting in failure in visible canonicalization.
if (start_of_paragraph_to_move.IsNull() ||
end_of_paragraph_to_move.IsNull()) {
editing_state->Abort();
return false;
}
MoveParagraphWithClones(start_of_paragraph_to_move, end_of_paragraph_to_move,
new_list, selected_list_item, editing_state);
if (editing_state->IsAborted())
return false;
if (!should_keep_selected_list) {
RemoveNode(selected_list_item, editing_state);
if (editing_state->IsAborted())
return false;
}
GetDocument().UpdateStyleAndLayout();
DCHECK(new_list);
if (previous_list && CanMergeLists(*previous_list, *new_list)) {
MergeIdenticalElements(previous_list, new_list, editing_state);
if (editing_state->IsAborted())
return false;
}
GetDocument().UpdateStyleAndLayout();
if (next_list && CanMergeLists(*new_list, *next_list)) {
MergeIdenticalElements(new_list, next_list, editing_state);
if (editing_state->IsAborted())
return false;
}
return true;
}
void IndentOutdentCommand::IndentIntoBlockquote(const Position& start,
const Position& end,
HTMLElement*& target_blockquote,
EditingState* editing_state) {
auto* enclosing_cell = To<Element>(EnclosingNodeOfType(start, &IsTableCell));
Element* element_to_split_to;
if (enclosing_cell)
element_to_split_to = enclosing_cell;
else if (EnclosingList(start.ComputeContainerNode()))
element_to_split_to = EnclosingBlock(start.ComputeContainerNode());
else
element_to_split_to = RootEditableElementOf(start);
if (!element_to_split_to)
return;
Node* outer_block =
(start.ComputeContainerNode() == element_to_split_to)
? start.ComputeContainerNode()
: SplitTreeToNode(start.ComputeContainerNode(), element_to_split_to);
GetDocument().UpdateStyleAndLayout();
VisiblePosition start_of_contents = CreateVisiblePosition(start);
if (!target_blockquote) {
// Create a new blockquote and insert it as a child of the root editable
// element. We accomplish this by splitting all parents of the current
// paragraph up to that point.
target_blockquote = CreateBlockElement();
if (outer_block == start.ComputeContainerNode()) {
// When we apply indent to an empty <blockquote>, we should call
// insertNodeAfter(). See http://crbug.com/625802 for more details.
if (outer_block->HasTagName(kBlockquoteTag))
InsertNodeAfter(target_blockquote, outer_block, editing_state);
else
InsertNodeAt(target_blockquote, start, editing_state);
} else
InsertNodeBefore(target_blockquote, outer_block, editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout();
start_of_contents = VisiblePosition::InParentAfterNode(*target_blockquote);
}
VisiblePosition end_of_contents = CreateVisiblePosition(end);
if (start_of_contents.IsNull() || end_of_contents.IsNull())
return;
MoveParagraphWithClones(start_of_contents, end_of_contents, target_blockquote,
outer_block, editing_state);
}
void IndentOutdentCommand::OutdentParagraph(EditingState* editing_state) {
VisiblePosition visible_start_of_paragraph =
StartOfParagraph(EndingVisibleSelection().VisibleStart());
VisiblePosition visible_end_of_paragraph =
EndOfParagraph(visible_start_of_paragraph);
auto* enclosing_element = To<HTMLElement>(
EnclosingNodeOfType(visible_start_of_paragraph.DeepEquivalent(),
&IsHTMLListOrBlockquoteElement));
// We can't outdent if there is no place to go!
if (!enclosing_element || !HasEditableStyle(*enclosing_element->parentNode()))
return;
// Use InsertListCommand to remove the selection from the list
if (IsHTMLOListElement(*enclosing_element)) {
ApplyCommandToComposite(MakeGarbageCollected<InsertListCommand>(
GetDocument(), InsertListCommand::kOrderedList),
editing_state);
return;
}
if (IsHTMLUListElement(*enclosing_element)) {
ApplyCommandToComposite(
MakeGarbageCollected<InsertListCommand>(
GetDocument(), InsertListCommand::kUnorderedList),
editing_state);
return;
}
// The selection is inside a blockquote i.e. enclosingNode is a blockquote
VisiblePosition position_in_enclosing_block =
VisiblePosition::FirstPositionInNode(*enclosing_element);
// If the blockquote is inline, the start of the enclosing block coincides
// with positionInEnclosingBlock.
VisiblePosition start_of_enclosing_block =
(enclosing_element->GetLayoutObject() &&
enclosing_element->GetLayoutObject()->IsInline())
? position_in_enclosing_block
: StartOfBlock(position_in_enclosing_block);
VisiblePosition last_position_in_enclosing_block =
VisiblePosition::LastPositionInNode(*enclosing_element);
VisiblePosition end_of_enclosing_block =
EndOfBlock(last_position_in_enclosing_block);
if (visible_start_of_paragraph.DeepEquivalent() ==
start_of_enclosing_block.DeepEquivalent() &&
visible_end_of_paragraph.DeepEquivalent() ==
end_of_enclosing_block.DeepEquivalent()) {
// The blockquote doesn't contain anything outside the paragraph, so it can
// be totally removed.
Node* split_point = enclosing_element->nextSibling();
RemoveNodePreservingChildren(enclosing_element, editing_state);
if (editing_state->IsAborted())
return;
// outdentRegion() assumes it is operating on the first paragraph of an
// enclosing blockquote, but if there are multiply nested blockquotes and
// we've just removed one, then this assumption isn't true. By splitting the
// next containing blockquote after this node, we keep this assumption true
if (split_point) {
if (Element* split_point_parent = split_point->parentElement()) {
// We can't outdent if there is no place to go!
if (split_point_parent->HasTagName(kBlockquoteTag) &&
!split_point->HasTagName(kBlockquoteTag) &&
HasEditableStyle(*split_point_parent->parentNode()))
SplitElement(split_point_parent, split_point);
}
}
GetDocument().UpdateStyleAndLayout();
visible_start_of_paragraph =
CreateVisiblePosition(visible_start_of_paragraph.DeepEquivalent());
if (visible_start_of_paragraph.IsNotNull() &&
!IsStartOfParagraph(visible_start_of_paragraph)) {
InsertNodeAt(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
visible_start_of_paragraph.DeepEquivalent(), editing_state);
if (editing_state->IsAborted())
return;
}
GetDocument().UpdateStyleAndLayout();
visible_end_of_paragraph =
CreateVisiblePosition(visible_end_of_paragraph.DeepEquivalent());
if (visible_end_of_paragraph.IsNotNull() &&
!IsEndOfParagraph(visible_end_of_paragraph))
InsertNodeAt(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
visible_end_of_paragraph.DeepEquivalent(), editing_state);
return;
}
Node* split_blockquote_node = enclosing_element;
if (Element* enclosing_block_flow = EnclosingBlock(
visible_start_of_paragraph.DeepEquivalent().AnchorNode())) {
if (enclosing_block_flow != enclosing_element) {
// We should check if the blockquotes are nested, as nested blockquotes
// may be at different indentations.
const Position& previous_element =
PreviousCandidate(visible_start_of_paragraph.DeepEquivalent());
auto* const previous_element_is_blockquote =
To<HTMLElement>(EnclosingNodeOfType(previous_element,
&IsHTMLListOrBlockquoteElement));
const bool is_previous_blockquote_same =
!previous_element_is_blockquote ||
(enclosing_element == previous_element_is_blockquote);
const bool split_ancestor = true;
if (is_previous_blockquote_same) {
split_blockquote_node = SplitTreeToNode(
enclosing_block_flow, enclosing_element, split_ancestor);
} else {
SplitTreeToNode(
visible_start_of_paragraph.DeepEquivalent().AnchorNode(),
enclosing_element, split_ancestor);
}
} else {
// We split the blockquote at where we start outdenting.
Node* highest_inline_node = HighestEnclosingNodeOfType(
visible_start_of_paragraph.DeepEquivalent(), IsInline,
kCannotCrossEditingBoundary, enclosing_block_flow);
SplitElement(
enclosing_element,
highest_inline_node
? highest_inline_node
: visible_start_of_paragraph.DeepEquivalent().AnchorNode());
}
GetDocument().UpdateStyleAndLayout();
// Re-canonicalize visible{Start,End}OfParagraph, make them valid again
// after DOM change.
// TODO(editing-dev): We should not store a VisiblePosition and later
// inspect its properties when it is already invalidated.
// See crbug.com/648949 for details.
visible_start_of_paragraph = CreateVisiblePosition(
visible_start_of_paragraph.ToPositionWithAffinity());
visible_end_of_paragraph = CreateVisiblePosition(
visible_end_of_paragraph.ToPositionWithAffinity());
}
// TODO(editing-dev): We should not store a VisiblePosition and later
// inspect its properties when it is already invalidated.
// See crbug.com/648949 for details.
VisiblePosition start_of_paragraph_to_move =
StartOfParagraph(visible_start_of_paragraph);
VisiblePosition end_of_paragraph_to_move =
EndOfParagraph(visible_end_of_paragraph);
if (start_of_paragraph_to_move.IsNull() || end_of_paragraph_to_move.IsNull())
return;
auto* placeholder = MakeGarbageCollected<HTMLBRElement>(GetDocument());
InsertNodeBefore(placeholder, split_blockquote_node, editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout();
start_of_paragraph_to_move = CreateVisiblePosition(
start_of_paragraph_to_move.ToPositionWithAffinity());
end_of_paragraph_to_move =
CreateVisiblePosition(end_of_paragraph_to_move.ToPositionWithAffinity());
MoveParagraph(start_of_paragraph_to_move, end_of_paragraph_to_move,
VisiblePosition::BeforeNode(*placeholder), editing_state,
kPreserveSelection);
}
// FIXME: We should merge this function with
// ApplyBlockElementCommand::formatSelection
void IndentOutdentCommand::OutdentRegion(
const VisiblePosition& start_of_selection,
const VisiblePosition& end_of_selection,
EditingState* editing_state) {
VisiblePosition end_of_current_paragraph = EndOfParagraph(start_of_selection);
VisiblePosition end_of_last_paragraph = EndOfParagraph(end_of_selection);
if (end_of_current_paragraph.DeepEquivalent() ==
end_of_last_paragraph.DeepEquivalent()) {
OutdentParagraph(editing_state);
return;
}
Position original_selection_end = EndingVisibleSelection().End();
Position end_after_selection =
EndOfParagraph(NextPositionOf(end_of_last_paragraph)).DeepEquivalent();
while (end_of_current_paragraph.DeepEquivalent() != end_after_selection) {
PositionWithAffinity end_of_next_paragraph =
EndOfParagraph(NextPositionOf(end_of_current_paragraph))
.ToPositionWithAffinity();
if (end_of_current_paragraph.DeepEquivalent() ==
end_of_last_paragraph.DeepEquivalent()) {
SelectionInDOMTree::Builder builder;
if (original_selection_end.IsNotNull())
builder.Collapse(original_selection_end);
SetEndingSelection(SelectionForUndoStep::From(builder.Build()));
} else {
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(end_of_current_paragraph.DeepEquivalent())
.Build()));
}
OutdentParagraph(editing_state);
if (editing_state->IsAborted())
return;
// outdentParagraph could move more than one paragraph if the paragraph
// is in a list item. As a result, endAfterSelection and endOfNextParagraph
// could refer to positions no longer in the document.
if (end_after_selection.IsNotNull() && !end_after_selection.IsConnected())
break;
GetDocument().UpdateStyleAndLayout();
if (end_of_next_paragraph.IsNotNull() &&
!end_of_next_paragraph.IsConnected()) {
end_of_current_paragraph =
CreateVisiblePosition(EndingVisibleSelection().End());
end_of_next_paragraph =
EndOfParagraph(NextPositionOf(end_of_current_paragraph))
.ToPositionWithAffinity();
}
end_of_current_paragraph = CreateVisiblePosition(end_of_next_paragraph);
}
}
void IndentOutdentCommand::FormatSelection(
const VisiblePosition& start_of_selection,
const VisiblePosition& end_of_selection,
EditingState* editing_state) {
if (type_of_action_ == kIndent)
ApplyBlockElementCommand::FormatSelection(start_of_selection,
end_of_selection, editing_state);
else
OutdentRegion(start_of_selection, end_of_selection, editing_state);
}
void IndentOutdentCommand::FormatRange(const Position& start,
const Position& end,
const Position&,
HTMLElement*& blockquote_for_next_indent,
EditingState* editing_state) {
bool indenting_as_list_item_result =
TryIndentingAsListItem(start, end, editing_state);
if (editing_state->IsAborted())
return;
if (indenting_as_list_item_result)
blockquote_for_next_indent = nullptr;
else
IndentIntoBlockquote(start, end, blockquote_for_next_indent, editing_state);
}
InputEvent::InputType IndentOutdentCommand::GetInputType() const {
return type_of_action_ == kIndent ? InputEvent::InputType::kFormatIndent
: InputEvent::InputType::kFormatOutdent;
}
} // namespace blink