Reviewed by Hyatt.
[WebKit-https.git] / WebCore / editing / IndentOutdentCommand.cpp
1 /*
2  * Copyright (C) 2006 Apple Computer, 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 "Element.h"
28 #include "IndentOutdentCommand.h"
29 #include "InsertListCommand.h"
30 #include "Document.h"
31 #include "htmlediting.h"
32 #include "HTMLElement.h"
33 #include "HTMLNames.h"
34 #include "InsertLineBreakCommand.h"
35 #include "Range.h"
36 #include "SplitElementCommand.h"
37 #include "TextIterator.h"
38 #include "visible_units.h"
39
40 namespace WebCore {
41
42 using namespace HTMLNames;
43
44 static String indentBlockquoteString()
45 {
46     static String string = "webkit-indent-blockquote";
47     return string;
48 }
49
50 static PassRefPtr<Element> createIndentBlockquoteElement(Document* document)
51 {
52     RefPtr<Element> indentBlockquoteElement = createElement(document, "blockquote");
53     indentBlockquoteElement->setAttribute(classAttr, indentBlockquoteString());
54     return indentBlockquoteElement.release();
55 }
56
57 static bool isIndentBlockquote(Node* node)
58 {
59     if (!node || !node->hasTagName(blockquoteTag) || !node->isElementNode())
60         return false;
61
62     Element* elem = static_cast<Element*>(node);
63     return elem->getAttribute(classAttr) == indentBlockquoteString();
64 }
65
66 static bool isListOrIndentBlockquote(Node* node)
67 {
68     return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || isIndentBlockquote(node));
69 }
70
71 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
72     : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
73 {}
74
75 // This function is a workaround for moveParagraph's tendency to strip blockquotes. It updates lastBlockquote to point to the
76 // correct level for the current paragraph, and returns a pointer to a placeholder br where the insertion should be performed.
77 Node* IndentOutdentCommand::prepareBlockquoteLevelForInsertion(VisiblePosition& currentParagraph, Node** lastBlockquote)
78 {
79     int currentBlockquoteLevel = 0;
80     int lastBlockquoteLevel = 0;
81     Node* node = currentParagraph.deepEquivalent().node();
82     while ((node = enclosingNodeOfType(node, &isIndentBlockquote)))
83         currentBlockquoteLevel++;
84     node = *lastBlockquote;
85     while ((node = enclosingNodeOfType(node, &isIndentBlockquote)))
86         lastBlockquoteLevel++;
87     while (currentBlockquoteLevel > lastBlockquoteLevel) {
88         RefPtr<Node> newBlockquote = createIndentBlockquoteElement(document());
89         appendNode(newBlockquote.get(), *lastBlockquote);
90         *lastBlockquote = newBlockquote.get();
91         lastBlockquoteLevel++;
92     }
93     while (currentBlockquoteLevel < lastBlockquoteLevel) {
94         *lastBlockquote = enclosingNodeOfType(*lastBlockquote, &isIndentBlockquote);
95         lastBlockquoteLevel--;
96     }
97     RefPtr<Node> placeholder = createBreakElement(document());
98     appendNode(placeholder.get(), *lastBlockquote);
99     // Add another br before the placeholder if it collapsed.
100     VisiblePosition visiblePos(Position(placeholder.get(), 0));
101     if (!isStartOfParagraph(visiblePos))
102         insertNodeBefore(createBreakElement(document()).get(), placeholder.get());
103     return placeholder.get();
104 }
105
106 // Splits the tree parent by parent until we reach the specified ancestor. We use VisiblePositions
107 // to determine if the split is necessary. Returns the last split node.
108 Node* IndentOutdentCommand::splitTreeToNode(Node* start, Node* end, bool splitAncestor)
109 {
110     Node* node;
111     for (node = start; node && node->parent() != end; node = node->parent()) {
112         VisiblePosition positionInParent(Position(node->parent(), 0), DOWNSTREAM);
113         VisiblePosition positionInNode(Position(node, 0), DOWNSTREAM);
114         if (positionInParent != positionInNode)
115             applyCommandToComposite(new SplitElementCommand(static_cast<Element*>(node->parent()), node));
116     }
117     if (splitAncestor)
118         return splitTreeToNode(end, end->parent());
119     return node;
120 }
121
122 static int indexForVisiblePosition(VisiblePosition& visiblePosition)
123 {
124     if (visiblePosition.isNull())
125         return 0;
126     Position p(visiblePosition.deepEquivalent());
127     RefPtr<Range> range = new Range(p.node()->document(), Position(p.node()->document(), 0), p);
128     return TextIterator::rangeLength(range.get());
129 }
130
131 void IndentOutdentCommand::indentRegion()
132 {
133     VisiblePosition startOfSelection = endingSelection().visibleStart();
134     VisiblePosition endOfSelection = endingSelection().visibleEnd();
135     int startIndex = indexForVisiblePosition(startOfSelection);
136     int endIndex = indexForVisiblePosition(endOfSelection);
137
138     ASSERT(!startOfSelection.isNull());
139     ASSERT(!endOfSelection.isNull());
140     
141     // Special case empty root editable elements because there's nothing to split
142     // and there's nothing to move.
143     Node* startNode = startOfSelection.deepEquivalent().downstream().node();
144     if (startNode == startNode->rootEditableElement()) {
145         RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
146         insertNodeAt(blockquote.get(), startNode, 0);
147         RefPtr<Node> placeholder = createBreakElement(document());
148         appendNode(placeholder.get(), blockquote.get());
149         setEndingSelection(Selection(Position(placeholder.get(), 0), DOWNSTREAM));
150         return;
151     }
152     
153     Node* previousListNode = 0;
154     Node* newListNode = 0;
155     Node* newBlockquote = 0;
156     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
157     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
158     while (endOfCurrentParagraph != endAfterSelection) {
159         // Iterate across the selected paragraphs...
160         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
161         Node* listNode = enclosingList(endOfCurrentParagraph.deepEquivalent().node());
162         Node* insertionPoint;
163         if (listNode) {
164             RefPtr<Node> placeholder = createBreakElement(document());
165             insertionPoint = placeholder.get();
166             newBlockquote = 0;
167             RefPtr<Node> listItem = createListItemElement(document());
168             if (listNode == previousListNode) {
169                 // The previous paragraph was inside the same list, so add this list item to the list we already created
170                 appendNode(listItem.get(), newListNode);
171                 appendNode(placeholder.get(), listItem.get());
172             } else {
173                 // Clone the list element, insert it before the current paragraph, and move the paragraph into it.
174                 RefPtr<Node> clonedList = static_cast<Element*>(listNode)->cloneNode(false);
175                 insertNodeBefore(clonedList.get(), enclosingListChild(endOfCurrentParagraph.deepEquivalent().node()));
176                 appendNode(listItem.get(), clonedList.get());
177                 appendNode(placeholder.get(), listItem.get());
178                 newListNode = clonedList.get();
179                 previousListNode = listNode;
180             }
181         } else if (newBlockquote)
182             // The previous paragraph was put into a new blockquote, so move this paragraph there as well
183             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
184         else {
185             // Create a new blockquote and insert it as a child of the root editable element. We accomplish
186             // this by splitting all parents of the current paragraph up to that point.
187             RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
188             Node* startNode = startOfParagraph(endOfCurrentParagraph).deepEquivalent().node();
189             Node* startOfNewBlock = splitTreeToNode(startNode, startNode->rootEditableElement());
190             insertNodeBefore(blockquote.get(), startOfNewBlock);
191             newBlockquote = blockquote.get();
192             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
193         }
194         moveParagraph(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, VisiblePosition(Position(insertionPoint, 0)), true);
195         endOfCurrentParagraph = endOfNextParagraph;
196     }
197     
198     RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), 0, startIndex);
199     RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), 0, endIndex);
200     setEndingSelection(Selection(startRange->endPosition(), endRange->endPosition(), DOWNSTREAM));
201 }
202
203 void IndentOutdentCommand::outdentParagraph()
204 {
205     VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
206     VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
207
208     Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent().node(), &isListOrIndentBlockquote);
209     if (!enclosingNode)
210         return;
211
212     // Use InsertListCommand to remove the selection from the list
213     if (enclosingNode->hasTagName(olTag)) {
214         applyCommandToComposite(new InsertListCommand(document(), InsertListCommand::OrderedList, ""));
215         return;        
216     } else if (enclosingNode->hasTagName(ulTag)) {
217         applyCommandToComposite(new InsertListCommand(document(), InsertListCommand::UnorderedList, ""));
218         return;
219     }
220     
221     // The selection is inside a blockquote
222     VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
223     VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
224     VisiblePosition endOfEnclosingBlock = endOfBlock(positionInEnclosingBlock);
225     if (visibleStartOfParagraph == startOfEnclosingBlock &&
226         visibleEndOfParagraph == endOfEnclosingBlock) {
227         // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
228         removeNodePreservingChildren(enclosingNode);
229         updateLayout();
230         if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
231             insertNodeAt(createBreakElement(document()).get(), visibleStartOfParagraph.deepEquivalent().node(), visibleStartOfParagraph.deepEquivalent().offset());
232         if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
233             insertNodeAt(createBreakElement(document()).get(), visibleEndOfParagraph.deepEquivalent().node(), visibleEndOfParagraph.deepEquivalent().offset());
234         return;
235     }
236     Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph);
237     Node* splitBlockquoteNode = enclosingNode;
238     if (enclosingBlockFlow != enclosingNode)
239         splitBlockquoteNode = splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph), enclosingNode, true);
240     RefPtr<Node> placeholder = createBreakElement(document());
241     insertNodeBefore(placeholder.get(), splitBlockquoteNode);
242     moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
243 }
244
245 void IndentOutdentCommand::outdentRegion()
246 {
247     VisiblePosition startOfSelection = endingSelection().visibleStart();
248     VisiblePosition endOfSelection = endingSelection().visibleEnd();
249     VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
250
251     ASSERT(!startOfSelection.isNull());
252     ASSERT(!endOfSelection.isNull());
253
254     if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
255         outdentParagraph();
256         return;
257     }
258
259     Position originalSelectionEnd = endingSelection().end();
260     setEndingSelection(endingSelection().visibleStart());
261     outdentParagraph();
262     Position originalSelectionStart = endingSelection().start();
263     VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
264     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
265     while (endOfCurrentParagraph != endAfterSelection) {
266         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
267         if (endOfCurrentParagraph == endOfLastParagraph)
268             setEndingSelection(Selection(originalSelectionEnd, DOWNSTREAM));
269         else
270             setEndingSelection(endOfCurrentParagraph);
271         outdentParagraph();
272         endOfCurrentParagraph = endOfNextParagraph;
273     }
274     setEndingSelection(Selection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
275 }
276
277 void IndentOutdentCommand::doApply()
278 {
279     if (endingSelection().isNone())
280         return;
281
282     if (!endingSelection().rootEditableElement())
283         return;
284         
285     VisiblePosition visibleEnd = endingSelection().visibleEnd();
286     VisiblePosition visibleStart = endingSelection().visibleStart();
287     // When a selection ends at the start of a paragraph, we rarely paint 
288     // the selection gap before that paragraph, because there often is no gap.  
289     // In a case like this, it's not obvious to the user that the selection 
290     // ends "inside" that paragraph, so it would be confusing if Indent/Outdent 
291     // operated on that paragraph.
292     // FIXME: We paint the gap before some paragraphs that are indented with left 
293     // margin/padding, but not others.  We should make the gap painting more consistent and 
294     // then use a left margin/padding rule here.
295     if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
296         setEndingSelection(Selection(visibleStart, visibleEnd.previous(true)));
297
298     if (m_typeOfAction == Indent)
299         indentRegion();
300     else
301         outdentRegion();
302 }
303
304 }