3e9de4ab4ffebfbcf72dd6d35943ebd513753a5b
[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 "visible_units.h"
38
39 namespace WebCore {
40
41 using namespace HTMLNames;
42
43 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
44     : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
45 {}
46
47 static Node* enclosingListOrBlockquote(Node* node)
48 {
49     if (!node)
50         return 0;
51     Node* root = (node->inDocument()) ? node->rootEditableElement() : highestAncestor(node);
52     ASSERT(root);
53     for (Node* n = node->parentNode(); n && (n == root || n->isAncestor(root)); n = n->parentNode())
54         if (n->hasTagName(ulTag) || n->hasTagName(olTag) || n->hasTagName(blockquoteTag))
55             return n;
56             
57     return 0;
58 }
59
60 // This function is a workaround for moveParagraph's tendency to strip blockquotes. It updates lastBlockquote to point to the
61 // correct level for the current paragraph, and returns a pointer to a placeholder br where the insertion should be performed.
62 Node* IndentOutdentCommand::prepareBlockquoteLevelForInsertion(VisiblePosition& currentParagraph, Node** lastBlockquote)
63 {
64     int currentBlockquoteLevel = 0;
65     int lastBlockquoteLevel = 0;
66     Node* node = currentParagraph.deepEquivalent().node();
67     while ((node = enclosingNodeWithTag(node, blockquoteTag)))
68         currentBlockquoteLevel++;
69     node = *lastBlockquote;
70     while ((node = enclosingNodeWithTag(node, blockquoteTag)))
71         lastBlockquoteLevel++;
72     while (currentBlockquoteLevel > lastBlockquoteLevel) {
73         RefPtr<Node> newBlockquote = createElement(document(), "blockquote");
74         appendNode(newBlockquote.get(), *lastBlockquote);
75         *lastBlockquote = newBlockquote.get();
76         lastBlockquoteLevel++;
77     }
78     while (currentBlockquoteLevel < lastBlockquoteLevel) {
79         *lastBlockquote = enclosingNodeWithTag(*lastBlockquote, blockquoteTag);
80         lastBlockquoteLevel--;
81     }
82     RefPtr<Node> placeholder = createBreakElement(document());
83     if ((*lastBlockquote)->firstChild() && !(*lastBlockquote)->lastChild()->hasTagName(brTag)) {
84         RefPtr<Node> collapsedPlaceholder = createBreakElement(document());
85         appendNode(collapsedPlaceholder.get(), (*lastBlockquote));
86     }
87     appendNode(placeholder.get(), *lastBlockquote);
88     return placeholder.get();
89 }
90
91 // Splits the tree parent by parent until we reach the specified ancestor. We use VisiblePositions
92 // to determine if the split is necessary. Returns the last split node.
93 Node* IndentOutdentCommand::splitTreeToNode(Node* start, Node* end, bool splitAncestor)
94 {
95     Node* node;
96     for (node = start; node && node->parent() != end; node = node->parent()) {
97         VisiblePosition positionInParent(Position(node->parent(), 0), DOWNSTREAM);
98         VisiblePosition positionInNode(Position(node, 0), DOWNSTREAM);
99         if (positionInParent != positionInNode) {
100             EditCommandPtr cmd(new SplitElementCommand(document(), static_cast<Element*>(node->parent()), node));
101             applyCommandToComposite(cmd);
102         }
103     }
104     if (splitAncestor)
105         return splitTreeToNode(end, end->parent());
106     return node;
107 }
108
109 void IndentOutdentCommand::indentRegion()
110 {
111     VisiblePosition startOfSelection = endingSelection().visibleStart();
112     VisiblePosition endOfSelection = endingSelection().visibleEnd();
113
114     ASSERT(!startOfSelection.isNull());
115     ASSERT(!endOfSelection.isNull());
116     
117     Node* previousListNode = 0;
118     Node* newListNode = 0;
119     Node* newBlockquote = 0;
120     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
121     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
122     while (endOfCurrentParagraph != endAfterSelection) {
123         // Iterate across the selected paragraphs...
124         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
125         Node* listNode = enclosingList(endOfCurrentParagraph.deepEquivalent().node());
126         Node* insertionPoint;
127         if (listNode) {
128             RefPtr<Node> placeholder = createBreakElement(document());
129             insertionPoint = placeholder.get();
130             newBlockquote = 0;
131             RefPtr<Node> listItem = createListItemElement(document());
132             if (listNode == previousListNode) {
133                 // The previous paragraph was inside the same list, so add this list item to the list we already created
134                 appendNode(listItem.get(), newListNode);
135                 appendNode(placeholder.get(), listItem.get());
136             } else {
137                 // Clone the list element, insert it before the current paragraph, and move the paragraph into it.
138                 RefPtr<Node> clonedList = static_cast<Element*>(listNode)->cloneNode(false);
139                 insertNodeBefore(clonedList.get(), enclosingListChild(endOfCurrentParagraph.deepEquivalent().node()));
140                 appendNode(listItem.get(), clonedList.get());
141                 appendNode(placeholder.get(), listItem.get());
142                 newListNode = clonedList.get();
143                 previousListNode = listNode;
144             }
145         } else if (newBlockquote)
146             // The previous paragraph was put into a new blockquote, so move this paragraph there as well
147             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
148         else {
149             // Create a new blockquote and insert it as a child of the root editable element. We accomplish
150             // this by splitting all parents of the current paragraph up to that point.
151             RefPtr<Node> blockquote = createElement(document(), "blockquote");
152             Node* startNode = startOfParagraph(endOfCurrentParagraph).deepEquivalent().node();
153             Node* startOfNewBlock = splitTreeToNode(startNode, startNode->rootEditableElement());
154             insertNodeBefore(blockquote.get(), startOfNewBlock);
155             newBlockquote = blockquote.get();
156             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
157         }
158         moveParagraph(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, VisiblePosition(Position(insertionPoint, 0)), true);
159         endOfCurrentParagraph = endOfNextParagraph;
160     }
161 }
162
163 void IndentOutdentCommand::outdentParagraph()
164 {
165     VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
166     VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
167
168     Node* enclosingNode = enclosingListOrBlockquote(visibleStartOfParagraph.deepEquivalent().node());
169     if (!enclosingNode)
170         return;
171
172     // Handle the list case
173     bool inList = false;
174     InsertListCommand::EListType typeOfList;
175     if (enclosingNode->hasTagName(olTag)) {
176         inList = true;
177         typeOfList = InsertListCommand::OrderedListType;
178     } else if (enclosingNode->hasTagName(ulTag)) {
179         inList = true;
180         typeOfList = InsertListCommand::UnorderedListType;
181     }
182     if (inList) {
183         // Use InsertListCommand to remove the selection from the list
184         EditCommandPtr cmd(new InsertListCommand(document(), typeOfList, ""));
185         applyCommandToComposite(cmd);
186         return;
187     }
188     // The selection is inside a blockquote
189     VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
190     VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
191     VisiblePosition endOfEnclosingBlock = endOfBlock(positionInEnclosingBlock);
192     if (visibleStartOfParagraph == startOfEnclosingBlock &&
193         visibleEndOfParagraph == endOfEnclosingBlock) {
194         // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
195         removeNodePreservingChildren(enclosingNode);
196         return;
197     }
198     Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph);
199     Node* splitBlockquoteNode = enclosingNode;
200     if (enclosingBlockFlow != enclosingNode)
201         splitBlockquoteNode = splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph), enclosingNode, true);
202     RefPtr<Node> placeholder = createBreakElement(document());
203     insertNodeBefore(placeholder.get(), splitBlockquoteNode);
204     moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
205 }
206
207 void IndentOutdentCommand::outdentRegion()
208 {
209     VisiblePosition startOfSelection = endingSelection().visibleStart();
210     VisiblePosition endOfSelection = endingSelection().visibleEnd();
211     VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
212
213     ASSERT(!startOfSelection.isNull());
214     ASSERT(!endOfSelection.isNull());
215
216     if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
217         outdentParagraph();
218         return;
219     }
220
221     Position originalSelectionEnd = endingSelection().end();
222     setEndingSelection(endingSelection().visibleStart());
223     outdentParagraph();
224     Position originalSelectionStart = endingSelection().start();
225     VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
226     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
227     while (endOfCurrentParagraph != endAfterSelection) {
228         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
229         if (endOfCurrentParagraph == endOfLastParagraph)
230             setEndingSelection(originalSelectionEnd, DOWNSTREAM);
231         else
232             setEndingSelection(endOfCurrentParagraph);
233         outdentParagraph();
234         endOfCurrentParagraph = endOfNextParagraph;
235     }
236     setEndingSelection(Selection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
237 }
238
239 void IndentOutdentCommand::doApply()
240 {
241     if (endingSelection().isNone())
242         return;
243
244     if (!endingSelection().rootEditableElement())
245         return;
246
247     if (m_typeOfAction == Indent)
248         indentRegion();
249     else
250         outdentRegion();
251 }
252
253 }