WebCore:
[WebKit-https.git] / WebCore / editing / InsertListCommand.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 (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 "Element.h"
28 #include "InsertListCommand.h"
29 #include "DocumentFragment.h"
30 #include "htmlediting.h"
31 #include "HTMLElement.h"
32 #include "HTMLNames.h"
33 #include "visible_units.h"
34
35 namespace WebCore {
36
37 using namespace HTMLNames;
38
39 PassRefPtr<Node> InsertListCommand::insertList(Document* document, Type type)
40 {
41     RefPtr<InsertListCommand> insertCommand = new InsertListCommand(document, type, "");
42     insertCommand->apply();
43     return insertCommand->m_listElement;
44 }
45
46 Node* InsertListCommand::fixOrphanedListChild(Node* node)
47 {
48     RefPtr<Element> listElement = createUnorderedListElement(document());
49     insertNodeBefore(listElement.get(), node);
50     removeNode(node);
51     appendNode(node, listElement.get());
52     m_listElement = listElement;
53     return listElement.get();
54 }
55
56 InsertListCommand::InsertListCommand(Document* document, Type type, const String& id) 
57     : CompositeEditCommand(document), m_type(type), m_id(id), m_forceCreateList(false)
58 {
59 }
60
61 bool InsertListCommand::modifyRange()
62 {
63     ASSERT(endingSelection().isRange());
64     VisiblePosition visibleStart = endingSelection().visibleStart();
65     VisiblePosition visibleEnd = endingSelection().visibleEnd();
66     VisiblePosition startOfLastParagraph = startOfParagraph(visibleEnd);
67     
68     // If the end of the selection to modify is just after a table, and
69     // if the start of the selection is inside that table, the last paragraph
70     // that we'll want modify is the last one inside the table, not the table itself.
71     // Adjust startOfLastParagraph and visibleEnd here to avoid infinite recursion.
72     if (Node* table = isFirstPositionAfterTable(visibleEnd))
73         if (visibleStart.deepEquivalent().node()->isDescendantOf(table)) {
74             visibleEnd = visibleEnd.previous(true);
75             startOfLastParagraph = startOfParagraph(visibleEnd);
76         }
77         
78     if (startOfParagraph(visibleStart) == startOfLastParagraph)
79         return false;
80     
81     Node* startList = enclosingList(visibleStart.deepEquivalent().node());
82     Node* endList = enclosingList(visibleEnd.deepEquivalent().node());
83     if (!startList || startList != endList)
84         m_forceCreateList = true;
85
86     setEndingSelection(visibleStart);
87     doApply();
88     visibleStart = endingSelection().visibleStart();
89     VisiblePosition nextParagraph = endOfParagraph(visibleStart).next();
90     while (nextParagraph.isNotNull() && nextParagraph != startOfLastParagraph) {
91         setEndingSelection(nextParagraph);
92         doApply();
93         nextParagraph = endOfParagraph(endingSelection().visibleStart()).next();
94     }
95     setEndingSelection(visibleEnd);
96     doApply();
97     visibleEnd = endingSelection().visibleEnd();
98     setEndingSelection(Selection(visibleStart.deepEquivalent(), visibleEnd.deepEquivalent(), DOWNSTREAM));
99     m_forceCreateList = false;
100     
101     return true;
102 }
103
104 void InsertListCommand::doApply()
105 {
106     if (endingSelection().isNone())
107         return;
108     
109     if (!endingSelection().rootEditableElement())
110         return;
111     
112     VisiblePosition visibleEnd = endingSelection().visibleEnd();
113     VisiblePosition visibleStart = endingSelection().visibleStart();
114     // When a selection ends at the start of a paragraph, we rarely paint 
115     // the selection gap before that paragraph, because there often is no gap.  
116     // In a case like this, it's not obvious to the user that the selection 
117     // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List 
118     // operated on that paragraph.
119     // FIXME: We paint the gap before some paragraphs that are indented with left 
120     // margin/padding, but not others.  We should make the gap painting more consistent and 
121     // then use a left margin/padding rule here.
122     if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
123         setEndingSelection(Selection(visibleStart, visibleEnd.previous(true)));
124
125     if (endingSelection().isRange() && modifyRange())
126         return;
127     
128     Node* selectionNode = endingSelection().start().node();
129     const QualifiedName listTag = (m_type == OrderedList) ? olTag : ulTag;
130     Node* listChildNode = enclosingListChild(selectionNode);
131     bool switchListType = false;
132     if (listChildNode) {
133         // Remove the list chlild.
134         Node* listNode = enclosingList(listChildNode);
135         if (!listNode)
136             listNode = fixOrphanedListChild(listChildNode);
137         if (!listNode->hasTagName(listTag))
138             // listChildNode will be removed from the list and a list of type m_type will be created.
139             switchListType = true;
140         Node* nextListChild;
141         Node* previousListChild;
142         VisiblePosition start;
143         VisiblePosition end;
144         if (listChildNode->hasTagName(liTag)) {
145             start = VisiblePosition(Position(listChildNode, 0));
146             end = VisiblePosition(Position(listChildNode, maxDeepOffset(listChildNode)));
147             nextListChild = listChildNode->nextSibling();
148             previousListChild = listChildNode->previousSibling();
149         } else {
150             // A paragraph is visually a list item minus a list marker.  The paragraph will be moved.
151             start = startOfParagraph(endingSelection().visibleStart());
152             end = endOfParagraph(endingSelection().visibleEnd());
153             nextListChild = enclosingListChild(end.next().deepEquivalent().node());
154             ASSERT(nextListChild != listChildNode);
155             if (enclosingList(nextListChild) != listNode)
156                 nextListChild = 0;
157             previousListChild = enclosingListChild(start.previous().deepEquivalent().node());
158             ASSERT(previousListChild != listChildNode);
159             if (enclosingList(previousListChild) != listNode)
160                 previousListChild = 0;
161         }
162         // When removing a list, we must always create a placeholder to act as a point of insertion
163         // for the list content being removed.
164         RefPtr<Element> placeholder = createBreakElement(document());
165         RefPtr<Node> nodeToInsert = placeholder;
166         // If the content of the list item will be moved into another list, put it in a list item
167         // so that we don't create an orphaned list child.
168         if (enclosingList(listNode)) {
169             nodeToInsert = createListItemElement(document());
170             appendNode(placeholder.get(), nodeToInsert.get());
171         }
172         if (nextListChild && previousListChild) {
173             splitElement(static_cast<Element *>(listNode), nextListChild);
174             insertNodeBefore(nodeToInsert.get(), listNode);
175         } else if (nextListChild)
176             insertNodeBefore(nodeToInsert.get(), listNode);
177         else
178             insertNodeAfter(nodeToInsert.get(), listNode);
179         VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0));
180         moveParagraphs(start, end, insertionPoint, true);
181     }
182     if (!listChildNode || switchListType || m_forceCreateList) {
183         // Create list.
184         VisiblePosition start = startOfParagraph(endingSelection().visibleStart());
185         VisiblePosition end = endOfParagraph(endingSelection().visibleEnd());
186         
187         // Check for adjoining lists.
188         VisiblePosition previousPosition = start.previous(true);
189         VisiblePosition nextPosition = end.next(true);
190         RefPtr<Element> listItemElement = createListItemElement(document());
191         RefPtr<Element> placeholder = createBreakElement(document());
192         appendNode(placeholder.get(), listItemElement.get());
193         Node* previousList = outermostEnclosingList(previousPosition.deepEquivalent().node());
194         Node* nextList = outermostEnclosingList(nextPosition.deepEquivalent().node());
195         Node* startNode = start.deepEquivalent().node();
196         Node* previousCell = enclosingTableCell(previousPosition.deepEquivalent());
197         Node* nextCell = enclosingTableCell(nextPosition.deepEquivalent());
198         Node* currentCell = enclosingTableCell(start.deepEquivalent());
199         if (previousList && (!previousList->hasTagName(listTag) || startNode->isDescendantOf(previousList) || previousCell != currentCell))
200             previousList = 0;
201         if (nextList && (!nextList->hasTagName(listTag) || startNode->isDescendantOf(nextList) || nextCell != currentCell))
202             nextList = 0;
203         // Place list item into adjoining lists.
204         if (previousList)
205             appendNode(listItemElement.get(), previousList);
206         else if (nextList)
207             insertNodeAt(listItemElement.get(), Position(nextList, 0));
208         else {
209             // Create the list.
210             RefPtr<Element> listElement = m_type == OrderedList ? createOrderedListElement(document()) : createUnorderedListElement(document());
211             m_listElement = listElement;
212             if (!m_id.isEmpty())
213                 static_cast<HTMLElement*>(listElement.get())->setId(m_id);
214             appendNode(listItemElement.get(), listElement.get());
215             
216             if (start == end && isBlock(start.deepEquivalent().node())) {
217                 // Inserting the list into an empty paragraph that isn't held open 
218                 // by a br or a '\n', will invalidate start and end.  Insert 
219                 // a placeholder and then recompute start and end.
220                 Node* placeholder = insertBlockPlaceholder(start.deepEquivalent());
221                 start = VisiblePosition(Position(placeholder, 0));
222                 end = start;
223             }
224             
225             // Insert the list at a position visually equivalent to start of the
226             // paragraph that is being moved into the list. 
227             // Try to avoid inserting it somewhere where it will be surrounded by 
228             // inline ancestors of start, since it is easier for editing to produce 
229             // clean markup when inline elements are pushed down as far as possible.
230             Position insertionPos(start.deepEquivalent().upstream());
231             // Also avoid the containing list item.
232             Node* listChild = enclosingListChild(insertionPos.node());
233             if (listChild && listChild->hasTagName(liTag))
234                 insertionPos = positionBeforeNode(listChild);
235                 
236             insertNodeAt(listElement.get(), insertionPos);
237         }
238         moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true);
239         if (nextList && previousList)
240             mergeIdenticalElements(static_cast<Element*>(previousList), static_cast<Element*>(nextList));
241     }
242 }
243
244 }