00aacf25ecc8f82bd55d01c08291871e3aa5982b
[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->isDescendantOf(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             applyCommandToComposite(new SplitElementCommand(static_cast<Element*>(node->parent()), node));
101     }
102     if (splitAncestor)
103         return splitTreeToNode(end, end->parent());
104     return node;
105 }
106
107 void IndentOutdentCommand::indentRegion()
108 {
109     VisiblePosition startOfSelection = endingSelection().visibleStart();
110     VisiblePosition endOfSelection = endingSelection().visibleEnd();
111
112     ASSERT(!startOfSelection.isNull());
113     ASSERT(!endOfSelection.isNull());
114     
115     // Special case empty root editable elements because there's nothing to split
116     // and there's nothing to move.
117     Node* startNode = startOfSelection.deepEquivalent().downstream().node();
118     if (startNode == startNode->rootEditableElement()) {
119         RefPtr<Node> blockquote = createElement(document(), "blockquote");
120         insertNodeAt(blockquote.get(), startNode, 0);
121         RefPtr<Node> placeholder = createBreakElement(document());
122         appendNode(placeholder.get(), blockquote.get());
123         setEndingSelection(Selection(Position(placeholder.get(), 0), DOWNSTREAM));
124         return;
125     }
126     
127     Node* previousListNode = 0;
128     Node* newListNode = 0;
129     Node* newBlockquote = 0;
130     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
131     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
132     while (endOfCurrentParagraph != endAfterSelection) {
133         // Iterate across the selected paragraphs...
134         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
135         Node* listNode = enclosingList(endOfCurrentParagraph.deepEquivalent().node());
136         Node* insertionPoint;
137         if (listNode) {
138             RefPtr<Node> placeholder = createBreakElement(document());
139             insertionPoint = placeholder.get();
140             newBlockquote = 0;
141             RefPtr<Node> listItem = createListItemElement(document());
142             if (listNode == previousListNode) {
143                 // The previous paragraph was inside the same list, so add this list item to the list we already created
144                 appendNode(listItem.get(), newListNode);
145                 appendNode(placeholder.get(), listItem.get());
146             } else {
147                 // Clone the list element, insert it before the current paragraph, and move the paragraph into it.
148                 RefPtr<Node> clonedList = static_cast<Element*>(listNode)->cloneNode(false);
149                 insertNodeBefore(clonedList.get(), enclosingListChild(endOfCurrentParagraph.deepEquivalent().node()));
150                 appendNode(listItem.get(), clonedList.get());
151                 appendNode(placeholder.get(), listItem.get());
152                 newListNode = clonedList.get();
153                 previousListNode = listNode;
154             }
155         } else if (newBlockquote)
156             // The previous paragraph was put into a new blockquote, so move this paragraph there as well
157             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
158         else {
159             // Create a new blockquote and insert it as a child of the root editable element. We accomplish
160             // this by splitting all parents of the current paragraph up to that point.
161             RefPtr<Node> blockquote = createElement(document(), "blockquote");
162             Node* startNode = startOfParagraph(endOfCurrentParagraph).deepEquivalent().node();
163             Node* startOfNewBlock = splitTreeToNode(startNode, startNode->rootEditableElement());
164             insertNodeBefore(blockquote.get(), startOfNewBlock);
165             newBlockquote = blockquote.get();
166             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
167         }
168         moveParagraph(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, VisiblePosition(Position(insertionPoint, 0)), true);
169         endOfCurrentParagraph = endOfNextParagraph;
170     }
171 }
172
173 void IndentOutdentCommand::outdentParagraph()
174 {
175     VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
176     VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
177
178     Node* enclosingNode = enclosingListOrBlockquote(visibleStartOfParagraph.deepEquivalent().node());
179     if (!enclosingNode)
180         return;
181
182     // Handle the list case
183     bool inList = false;
184     InsertListCommand::Type typeOfList;
185     if (enclosingNode->hasTagName(olTag)) {
186         inList = true;
187         typeOfList = InsertListCommand::OrderedList;
188     } else if (enclosingNode->hasTagName(ulTag)) {
189         inList = true;
190         typeOfList = InsertListCommand::UnorderedList;
191     }
192     if (inList) {
193         // Use InsertListCommand to remove the selection from the list
194         applyCommandToComposite(new InsertListCommand(document(), typeOfList, ""));
195         return;
196     }
197     // The selection is inside a blockquote
198     VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
199     VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
200     VisiblePosition endOfEnclosingBlock = endOfBlock(positionInEnclosingBlock);
201     if (visibleStartOfParagraph == startOfEnclosingBlock &&
202         visibleEndOfParagraph == endOfEnclosingBlock) {
203         // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
204         removeNodePreservingChildren(enclosingNode);
205         return;
206     }
207     Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph);
208     Node* splitBlockquoteNode = enclosingNode;
209     if (enclosingBlockFlow != enclosingNode)
210         splitBlockquoteNode = splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph), enclosingNode, true);
211     RefPtr<Node> placeholder = createBreakElement(document());
212     insertNodeBefore(placeholder.get(), splitBlockquoteNode);
213     moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
214 }
215
216 void IndentOutdentCommand::outdentRegion()
217 {
218     VisiblePosition startOfSelection = endingSelection().visibleStart();
219     VisiblePosition endOfSelection = endingSelection().visibleEnd();
220     VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
221
222     ASSERT(!startOfSelection.isNull());
223     ASSERT(!endOfSelection.isNull());
224
225     if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
226         outdentParagraph();
227         return;
228     }
229
230     Position originalSelectionEnd = endingSelection().end();
231     setEndingSelection(endingSelection().visibleStart());
232     outdentParagraph();
233     Position originalSelectionStart = endingSelection().start();
234     VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
235     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
236     while (endOfCurrentParagraph != endAfterSelection) {
237         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
238         if (endOfCurrentParagraph == endOfLastParagraph)
239             setEndingSelection(Selection(originalSelectionEnd, DOWNSTREAM));
240         else
241             setEndingSelection(endOfCurrentParagraph);
242         outdentParagraph();
243         endOfCurrentParagraph = endOfNextParagraph;
244     }
245     setEndingSelection(Selection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
246 }
247
248 void IndentOutdentCommand::doApply()
249 {
250     if (endingSelection().isNone())
251         return;
252
253     if (!endingSelection().rootEditableElement())
254         return;
255
256     if (m_typeOfAction == Indent)
257         indentRegion();
258     else
259         outdentRegion();
260 }
261
262 }