1/*
2 * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "IndentOutdentCommand.h"
28
29#include "Document.h"
30#include "Editing.h"
31#include "ElementTraversal.h"
32#include "HTMLBRElement.h"
33#include "HTMLNames.h"
34#include "HTMLOListElement.h"
35#include "HTMLUListElement.h"
36#include "InsertLineBreakCommand.h"
37#include "InsertListCommand.h"
38#include "RenderElement.h"
39#include "SplitElementCommand.h"
40#include "Text.h"
41#include "VisibleUnits.h"
42
43namespace WebCore {
44
45using namespace HTMLNames;
46
47static bool isListOrIndentBlockquote(const Node* node)
48{
49 return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(blockquoteTag));
50}
51
52IndentOutdentCommand::IndentOutdentCommand(Document& document, EIndentType typeOfAction)
53 : ApplyBlockElementCommand(document, blockquoteTag, "margin: 0 0 0 40px; border: none; padding: 0px;")
54 , m_typeOfAction(typeOfAction)
55{
56}
57
58bool IndentOutdentCommand::tryIndentingAsListItem(const Position& start, const Position& end)
59{
60 // If our selection is not inside a list, bail out.
61 Node* lastNodeInSelectedParagraph = start.deprecatedNode();
62 RefPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph);
63 if (!listNode)
64 return false;
65
66 // 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.
67 RefPtr<Element> selectedListItem = enclosingBlock(lastNodeInSelectedParagraph);
68
69 if (!selectedListItem || !selectedListItem->hasTagName(liTag))
70 return false;
71
72 // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>. Should we?
73 RefPtr<Element> previousList = ElementTraversal::previousSibling(*selectedListItem);
74 RefPtr<Element> nextList = ElementTraversal::nextSibling(*selectedListItem);
75
76 RefPtr<Element> newList;
77 if (is<HTMLUListElement>(*listNode))
78 newList = HTMLUListElement::create(document());
79 else
80 newList = HTMLOListElement::create(document());
81 insertNodeBefore(*newList, *selectedListItem);
82
83 moveParagraphWithClones(start, end, newList.get(), selectedListItem.get());
84
85 if (canMergeLists(previousList.get(), newList.get()))
86 mergeIdenticalElements(*previousList, *newList);
87 if (canMergeLists(newList.get(), nextList.get()))
88 mergeIdenticalElements(*newList, *nextList);
89
90 return true;
91}
92
93void IndentOutdentCommand::indentIntoBlockquote(const Position& start, const Position& end, RefPtr<Element>& targetBlockquote)
94{
95 Node* enclosingCell = enclosingNodeOfType(start, &isTableCell);
96 Node* nodeToSplitTo;
97 if (enclosingCell)
98 nodeToSplitTo = enclosingCell;
99 else if (enclosingList(start.containerNode()))
100 nodeToSplitTo = enclosingBlock(start.containerNode());
101 else
102 nodeToSplitTo = editableRootForPosition(start);
103
104 if (!nodeToSplitTo)
105 return;
106
107 RefPtr<Node> nodeAfterStart = start.computeNodeAfterPosition();
108 RefPtr<Node> outerBlock = (start.containerNode() == nodeToSplitTo) ? start.containerNode() : splitTreeToNode(*start.containerNode(), *nodeToSplitTo);
109
110 VisiblePosition startOfContents = start;
111 if (!targetBlockquote) {
112 // Create a new blockquote and insert it as a child of the root editable element. We accomplish
113 // this by splitting all parents of the current paragraph up to that point.
114 targetBlockquote = createBlockElement();
115 if (outerBlock == nodeToSplitTo)
116 insertNodeAt(*targetBlockquote, start);
117 else
118 insertNodeBefore(*targetBlockquote, *outerBlock);
119 startOfContents = positionInParentAfterNode(targetBlockquote.get());
120 }
121
122 moveParagraphWithClones(startOfContents, end, targetBlockquote.get(), outerBlock.get());
123}
124
125void IndentOutdentCommand::outdentParagraph()
126{
127 VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
128 VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
129
130 auto* enclosingNode = downcast<HTMLElement>(enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote));
131 if (!enclosingNode || !enclosingNode->parentNode()->hasEditableStyle()) // We can't outdent if there is no place to go!
132 return;
133
134 // Use InsertListCommand to remove the selection from the list
135 if (enclosingNode->hasTagName(olTag)) {
136 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::Type::OrderedList));
137 return;
138 }
139 if (enclosingNode->hasTagName(ulTag)) {
140 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::Type::UnorderedList));
141 return;
142 }
143
144 // The selection is inside a blockquote i.e. enclosingNode is a blockquote
145 VisiblePosition positionInEnclosingBlock = VisiblePosition(firstPositionInNode(enclosingNode));
146 // If the blockquote is inline, the start of the enclosing block coincides with
147 // positionInEnclosingBlock.
148 VisiblePosition startOfEnclosingBlock = (enclosingNode->renderer() && enclosingNode->renderer()->isInline()) ? positionInEnclosingBlock : startOfBlock(positionInEnclosingBlock);
149 VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(lastPositionInNode(enclosingNode));
150 VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock);
151 if (visibleStartOfParagraph == startOfEnclosingBlock &&
152 visibleEndOfParagraph == endOfEnclosingBlock) {
153 // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
154 Node* splitPoint = enclosingNode->nextSibling();
155 removeNodePreservingChildren(*enclosingNode);
156 // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've
157 // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true
158 if (splitPoint) {
159 if (ContainerNode* splitPointParent = splitPoint->parentNode()) {
160 if (splitPointParent->hasTagName(blockquoteTag)
161 && !splitPoint->hasTagName(blockquoteTag)
162 && splitPointParent->parentNode()->hasEditableStyle()) // We can't outdent if there is no place to go!
163 splitElement(downcast<Element>(*splitPointParent), *splitPoint);
164 }
165 }
166
167 document().updateLayoutIgnorePendingStylesheets();
168 visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
169 visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
170 if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
171 insertNodeAt(HTMLBRElement::create(document()), visibleStartOfParagraph.deepEquivalent());
172 if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
173 insertNodeAt(HTMLBRElement::create(document()), visibleEndOfParagraph.deepEquivalent());
174
175 return;
176 }
177
178 auto* startOfParagraphNode = visibleStartOfParagraph.deepEquivalent().deprecatedNode();
179 auto* enclosingBlockFlow = enclosingBlock(startOfParagraphNode);
180 RefPtr<Node> splitBlockquoteNode = enclosingNode;
181 if (enclosingBlockFlow != enclosingNode)
182 splitBlockquoteNode = splitTreeToNode(*startOfParagraphNode, *enclosingNode, true);
183 else {
184 // We split the blockquote at where we start outdenting.
185 auto* highestInlineNode = highestEnclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), isInline, CannotCrossEditingBoundary, enclosingBlockFlow);
186 splitElement(*enclosingNode, highestInlineNode ? *highestInlineNode : *visibleStartOfParagraph.deepEquivalent().deprecatedNode());
187 }
188 auto placeholder = HTMLBRElement::create(document());
189 auto* placeholderPtr = placeholder.ptr();
190 insertNodeBefore(WTFMove(placeholder), *splitBlockquoteNode);
191 moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), positionBeforeNode(placeholderPtr), true);
192}
193
194// FIXME: We should merge this function with ApplyBlockElementCommand::formatSelection
195void IndentOutdentCommand::outdentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
196{
197 VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
198
199 if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
200 outdentParagraph();
201 return;
202 }
203
204 Position originalSelectionEnd = endingSelection().end();
205 VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
206 VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
207
208 while (endOfCurrentParagraph != endAfterSelection) {
209 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
210 if (endOfCurrentParagraph == endOfLastParagraph)
211 setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM));
212 else
213 setEndingSelection(endOfCurrentParagraph);
214
215 outdentParagraph();
216
217 // outdentParagraph could move more than one paragraph if the paragraph
218 // is in a list item. As a result, endAfterSelection and endOfNextParagraph
219 // could refer to positions no longer in the document.
220 if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().anchorNode()->isConnected())
221 break;
222
223 if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().anchorNode()->isConnected()) {
224 endOfCurrentParagraph = endingSelection().end();
225 endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
226 }
227 endOfCurrentParagraph = endOfNextParagraph;
228
229 if (endOfCurrentParagraph.isNull()) {
230 // If the end of the current paragraph is null, we'll end up looping infinitely, since the end of the next paragraph
231 // (and the paragraph after that, and so on) will always be null. To avoid this infinite loop, just bail.
232 break;
233 }
234 }
235}
236
237void IndentOutdentCommand::formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
238{
239 if (m_typeOfAction == Indent)
240 ApplyBlockElementCommand::formatSelection(startOfSelection, endOfSelection);
241 else
242 outdentRegion(startOfSelection, endOfSelection);
243}
244
245void IndentOutdentCommand::formatRange(const Position& start, const Position& end, const Position&, RefPtr<Element>& blockquoteForNextIndent)
246{
247 if (tryIndentingAsListItem(start, end))
248 blockquoteForNextIndent = nullptr;
249 else
250 indentIntoBlockquote(start, end, blockquoteForNextIndent);
251}
252
253}
254