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 COMPUTER, 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 COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (IndentOutdentCommandINCLUDING, 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 "Element.h"
31 #include "HTMLBlockquoteElement.h"
32 #include "HTMLNames.h"
33 #include "InsertLineBreakCommand.h"
34 #include "InsertListCommand.h"
35 #include "Range.h"
36 #include "DocumentFragment.h"
37 #include "SplitElementCommand.h"
38 #include "TextIterator.h"
39 #include "htmlediting.h"
40 #include "visible_units.h"
41 #include <wtf/StdLibExtras.h>
42
43 namespace WebCore {
44
45 using namespace HTMLNames;
46
indentBlockquoteString()47 static String indentBlockquoteString()
48 {
49 DEFINE_STATIC_LOCAL(String, string, ("webkit-indent-blockquote"));
50 return string;
51 }
52
createIndentBlockquoteElement(Document * document)53 static PassRefPtr<HTMLBlockquoteElement> createIndentBlockquoteElement(Document* document)
54 {
55 RefPtr<HTMLBlockquoteElement> element = new HTMLBlockquoteElement(blockquoteTag, document);
56 element->setAttribute(classAttr, indentBlockquoteString());
57 element->setAttribute(styleAttr, "margin: 0 0 0 40px; border: none; padding: 0px;");
58 return element.release();
59 }
60
isListOrIndentBlockquote(const Node * node)61 static bool isListOrIndentBlockquote(const Node* node)
62 {
63 return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(blockquoteTag));
64 }
65
IndentOutdentCommand(Document * document,EIndentType typeOfAction,int marginInPixels)66 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
67 : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
68 {
69 }
70
tryIndentingAsListItem(const VisiblePosition & endOfCurrentParagraph)71 bool IndentOutdentCommand::tryIndentingAsListItem(const VisiblePosition& endOfCurrentParagraph)
72 {
73 // If our selection is not inside a list, bail out.
74 Node* lastNodeInSelectedParagraph = endOfCurrentParagraph.deepEquivalent().node();
75 RefPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph);
76 if (!listNode)
77 return false;
78
79 // Find the list item enclosing the current paragraph
80 Element* selectedListItem = static_cast<Element*>(enclosingBlock(endOfCurrentParagraph.deepEquivalent().node()));
81 // FIXME: we need to deal with the case where there is no li (malformed HTML)
82 if (!selectedListItem->hasTagName(liTag))
83 return false;
84
85 // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>. Should we?
86 Element* previousList = selectedListItem->previousElementSibling();
87 Element* nextList = selectedListItem->nextElementSibling();
88
89 RefPtr<Element> newList = document()->createElement(listNode->tagQName(), false);
90 insertNodeBefore(newList, selectedListItem);
91 appendParagraphIntoNode(visiblePositionBeforeNode(selectedListItem), visiblePositionAfterNode(selectedListItem), newList.get());
92
93 if (canMergeLists(previousList, newList.get()))
94 mergeIdenticalElements(previousList, newList);
95 if (canMergeLists(newList.get(), nextList))
96 mergeIdenticalElements(newList, nextList);
97
98 return true;
99 }
100
indentIntoBlockquote(const VisiblePosition & startOfCurrentParagraph,const VisiblePosition & endOfCurrentParagraph,RefPtr<Element> & targetBlockquote,Node * nodeToSplitTo)101 void IndentOutdentCommand::indentIntoBlockquote(const VisiblePosition& startOfCurrentParagraph, const VisiblePosition& endOfCurrentParagraph, RefPtr<Element>& targetBlockquote, Node* nodeToSplitTo)
102 {
103 Node* enclosingCell = 0;
104
105 if (!targetBlockquote) {
106 // Create a new blockquote and insert it as a child of the enclosing block element. We accomplish
107 // this by splitting all parents of the current paragraph up to that point.
108 targetBlockquote = createIndentBlockquoteElement(document());
109 if (isTableCell(nodeToSplitTo))
110 enclosingCell = nodeToSplitTo;
111 RefPtr<Node> startOfNewBlock = splitTreeToNode(startOfCurrentParagraph.deepEquivalent().node(), nodeToSplitTo);
112 insertNodeBefore(targetBlockquote, startOfNewBlock);
113 }
114
115 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
116 appendParagraphIntoNode(startOfCurrentParagraph, endOfCurrentParagraph, targetBlockquote.get());
117
118 // Don't put the next paragraph in the blockquote we just created for this paragraph unless
119 // the next paragraph is in the same cell.
120 if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell))
121 targetBlockquote = 0;
122 }
123
isAtUnsplittableElement(const Position & pos) const124 bool IndentOutdentCommand::isAtUnsplittableElement(const Position& pos) const
125 {
126 Node* node = pos.node();
127 return node == editableRootForPosition(pos) || node == enclosingNodeOfType(pos, &isTableCell);
128 }
129
130 // Enclose all nodes between start and end by newParent, which is a sibling node of nodes between start and end
131 // FIXME: moveParagraph is overly complicated. We need to clean up moveParagraph so that it uses appendParagraphIntoNode
132 // or prepare more specialized functions and delete moveParagraph
appendParagraphIntoNode(const VisiblePosition & start,const VisiblePosition & end,Node * newParent)133 void IndentOutdentCommand::appendParagraphIntoNode(const VisiblePosition& start, const VisiblePosition& end, Node* newParent)
134 {
135 ASSERT(newParent);
136 ASSERT(newParent->isContentEditable());
137 ASSERT(isStartOfParagraph(start) && isEndOfParagraph(end));
138
139 Position endOfParagraph = end.deepEquivalent().downstream();
140 Node* insertionPoint = newParent->lastChild();// Remember the place to put br later
141 // Look for the beginning of the last paragraph in newParent
142 Node* startOfLastParagraph = startOfParagraph(Position(newParent, newParent->childNodeCount())).deepEquivalent().node();
143 if (startOfLastParagraph && !startOfLastParagraph->isDescendantOf(newParent))
144 startOfLastParagraph = 0;
145
146 // Extend the range so that we can append wrapping nodes as well if they're containd within the paragraph
147 ExceptionCode ec = 0;
148 RefPtr<Range> selectedRange = createRange(document(), start, end, ec);
149 RefPtr<Range> extendedRange = extendRangeToWrappingNodes(selectedRange, selectedRange.get(), newParent->parentNode());
150 newParent->appendChild(extendedRange->extractContents(ec), ec);
151
152 // If the start of paragraph didn't change by appending nodes, we should insert br to seperate the paragraphs.
153 Node* startOfNewParagraph = startOfParagraph(Position(newParent, newParent->childNodeCount())).deepEquivalent().node();
154 if (startOfNewParagraph == startOfLastParagraph) {
155 if (insertionPoint)
156 newParent->insertBefore(createBreakElement(document()), insertionPoint->nextSibling(), ec);
157 else
158 newParent->appendChild(createBreakElement(document()), ec);
159 }
160
161 // Remove unnecessary br from the place where we moved the paragraph from
162 removeUnnecessaryLineBreakAt(endOfParagraph);
163 }
164
removeUnnecessaryLineBreakAt(const Position & endOfParagraph)165 void IndentOutdentCommand::removeUnnecessaryLineBreakAt(const Position& endOfParagraph)
166 {
167 // If there is something in this paragraph, then don't remove br.
168 if (!isStartOfParagraph(endOfParagraph) || !isEndOfParagraph(endOfParagraph))
169 return;
170
171 // We only care about br at the end of paragraph
172 Node* br = endOfParagraph.node();
173 Node* parentNode = br->parentNode();
174
175 // If the node isn't br or the parent node is empty, then don't remove.
176 if (!br->hasTagName(brTag) || isVisiblyAdjacent(positionBeforeNode(parentNode), positionAfterNode(parentNode)))
177 return;
178
179 removeNodeAndPruneAncestors(br);
180 }
181
indentRegion()182 void IndentOutdentCommand::indentRegion()
183 {
184 VisibleSelection selection = selectionForParagraphIteration(endingSelection());
185 VisiblePosition startOfSelection = selection.visibleStart();
186 VisiblePosition endOfSelection = selection.visibleEnd();
187 RefPtr<Range> selectedRange = selection.firstRange();
188
189 ASSERT(!startOfSelection.isNull());
190 ASSERT(!endOfSelection.isNull());
191
192 // Special case empty unsplittable elements because there's nothing to split
193 // and there's nothing to move.
194 Position start = startOfSelection.deepEquivalent().downstream();
195 if (isAtUnsplittableElement(start)) {
196 RefPtr<Element> blockquote = createIndentBlockquoteElement(document());
197 insertNodeAt(blockquote, start);
198 RefPtr<Element> placeholder = createBreakElement(document());
199 appendNode(placeholder, blockquote);
200 setEndingSelection(VisibleSelection(Position(placeholder.get(), 0), DOWNSTREAM));
201 return;
202 }
203
204 RefPtr<Element> blockquoteForNextIndent;
205 VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
206 VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
207 while (endOfCurrentParagraph != endAfterSelection) {
208 // Iterate across the selected paragraphs...
209 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
210 if (tryIndentingAsListItem(endOfCurrentParagraph))
211 blockquoteForNextIndent = 0;
212 else {
213 VisiblePosition startOfCurrentParagraph = startOfParagraph(endOfCurrentParagraph);
214 Node* blockNode = enclosingBlock(endOfCurrentParagraph.deepEquivalent().node());
215 // extend the region so that it contains all the ancestor blocks within the selection
216 ExceptionCode ec;
217 Element* unsplittableNode = unsplittableElementForPosition(endOfCurrentParagraph.deepEquivalent());
218 RefPtr<Range> originalRange = createRange(document(), endOfCurrentParagraph, endOfCurrentParagraph, ec);
219 RefPtr<Range> extendedRange = extendRangeToWrappingNodes(originalRange, selectedRange.get(), unsplittableNode);
220 if (originalRange != extendedRange) {
221 ExceptionCode ec = 0;
222 endOfCurrentParagraph = endOfParagraph(extendedRange->endPosition().previous());
223 blockNode = enclosingBlock(extendedRange->commonAncestorContainer(ec));
224 }
225
226 endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
227 indentIntoBlockquote(startOfCurrentParagraph, endOfCurrentParagraph, blockquoteForNextIndent, blockNode);
228 // blockquoteForNextIndent will be updated in the function
229 }
230 // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().node()
231 // If somehow we did, return to prevent crashes.
232 if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) {
233 ASSERT_NOT_REACHED();
234 return;
235 }
236 endOfCurrentParagraph = endOfNextParagraph;
237 }
238
239 }
240
outdentParagraph()241 void IndentOutdentCommand::outdentParagraph()
242 {
243 VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
244 VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
245
246 Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote);
247 if (!enclosingNode || !enclosingNode->parentNode()->isContentEditable()) // We can't outdent if there is no place to go!
248 return;
249
250 // Use InsertListCommand to remove the selection from the list
251 if (enclosingNode->hasTagName(olTag)) {
252 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList));
253 return;
254 }
255 if (enclosingNode->hasTagName(ulTag)) {
256 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList));
257 return;
258 }
259
260 // The selection is inside a blockquote i.e. enclosingNode is a blockquote
261 VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
262 VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
263 VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(Position(enclosingNode, enclosingNode->childNodeCount()));
264 VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock);
265 if (visibleStartOfParagraph == startOfEnclosingBlock &&
266 visibleEndOfParagraph == endOfEnclosingBlock) {
267 // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
268 Node* splitPoint = enclosingNode->nextSibling();
269 removeNodePreservingChildren(enclosingNode);
270 // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've
271 // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true
272 if (splitPoint) {
273 if (Node* splitPointParent = splitPoint->parentNode()) {
274 if (splitPointParent->hasTagName(blockquoteTag)
275 && !splitPoint->hasTagName(blockquoteTag)
276 && splitPointParent->parentNode()->isContentEditable()) // We can't outdent if there is no place to go!
277 splitElement(static_cast<Element*>(splitPointParent), splitPoint);
278 }
279 }
280
281 updateLayout();
282 visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
283 visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
284 if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
285 insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent());
286 if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
287 insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent());
288
289 return;
290 }
291 Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().node());
292 RefPtr<Node> splitBlockquoteNode = enclosingNode;
293 if (enclosingBlockFlow != enclosingNode)
294 splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true);
295 else {
296 // We split the blockquote at where we start outdenting.
297 splitElement(static_cast<Element*>(enclosingNode), visibleStartOfParagraph.deepEquivalent().node());
298 }
299 RefPtr<Node> placeholder = createBreakElement(document());
300 insertNodeBefore(placeholder, splitBlockquoteNode);
301 moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
302 }
303
outdentRegion()304 void IndentOutdentCommand::outdentRegion()
305 {
306 VisiblePosition startOfSelection = endingSelection().visibleStart();
307 VisiblePosition endOfSelection = endingSelection().visibleEnd();
308 VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
309
310 ASSERT(!startOfSelection.isNull());
311 ASSERT(!endOfSelection.isNull());
312
313 if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
314 outdentParagraph();
315 return;
316 }
317
318 Position originalSelectionEnd = endingSelection().end();
319 setEndingSelection(endingSelection().visibleStart());
320 outdentParagraph();
321 Position originalSelectionStart = endingSelection().start();
322 VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
323 VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
324 while (endOfCurrentParagraph != endAfterSelection) {
325 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
326 if (endOfCurrentParagraph == endOfLastParagraph)
327 setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM));
328 else
329 setEndingSelection(endOfCurrentParagraph);
330 outdentParagraph();
331 endOfCurrentParagraph = endOfNextParagraph;
332 }
333 setEndingSelection(VisibleSelection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
334 }
335
doApply()336 void IndentOutdentCommand::doApply()
337 {
338 if (endingSelection().isNone())
339 return;
340
341 if (!endingSelection().rootEditableElement())
342 return;
343
344 VisiblePosition visibleEnd = endingSelection().visibleEnd();
345 VisiblePosition visibleStart = endingSelection().visibleStart();
346 // When a selection ends at the start of a paragraph, we rarely paint
347 // the selection gap before that paragraph, because there often is no gap.
348 // In a case like this, it's not obvious to the user that the selection
349 // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
350 // operated on that paragraph.
351 // FIXME: We paint the gap before some paragraphs that are indented with left
352 // margin/padding, but not others. We should make the gap painting more consistent and
353 // then use a left margin/padding rule here.
354 if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
355 setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true)));
356
357 if (m_typeOfAction == Indent)
358 indentRegion();
359 else
360 outdentRegion();
361 }
362
363 }
364