2011-02-09 Yael Aharon <yael.aharon@nokia.com>
[WebKit-https.git] / Source / WebCore / editing / InsertListCommand.cpp
1 /*
2  * Copyright (C) 2006, 2010 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 (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 "TextIterator.h"
34 #include "visible_units.h"
35
36 namespace WebCore {
37
38 using namespace HTMLNames;
39
40 static Node* enclosingListChild(Node* node, Node* listNode)
41 {
42     Node* listChild = enclosingListChild(node);
43     while (listChild && enclosingList(listChild) != listNode)
44         listChild = enclosingListChild(listChild->parentNode());
45     return listChild;
46 }
47
48 PassRefPtr<HTMLElement> InsertListCommand::insertList(Document* document, Type type)
49 {
50     RefPtr<InsertListCommand> insertCommand = create(document, type);
51     insertCommand->apply();
52     return insertCommand->m_listElement;
53 }
54
55 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node)
56 {
57     RefPtr<HTMLElement> listElement = createUnorderedListElement(document());
58     insertNodeBefore(listElement, node);
59     removeNode(node);
60     appendNode(node, listElement);
61     m_listElement = listElement;
62     return listElement.get();
63 }
64
65 PassRefPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtr<HTMLElement> passedList)
66 {
67     RefPtr<HTMLElement> list = passedList;
68     Element* previousList = list->previousElementSibling();
69     if (canMergeLists(previousList, list.get()))
70         mergeIdenticalElements(previousList, list);
71
72     if (!list || !list->nextElementSibling() || !list->nextElementSibling()->isHTMLElement())
73         return list.release();
74
75     RefPtr<HTMLElement> nextList = toHTMLElement(list->nextElementSibling());
76     if (canMergeLists(list.get(), nextList.get())) {
77         mergeIdenticalElements(list, nextList);
78         return nextList.release();
79     }
80     return list.release();
81 }
82
83 bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag)
84 {
85     VisiblePosition start = selection.visibleStart();
86
87     if (!enclosingList(start.deepEquivalent().node()))
88         return false;
89
90     VisiblePosition end = selection.visibleEnd();
91     while (start.isNotNull() && start != end) {
92         Element* listNode = enclosingList(start.deepEquivalent().node());
93         if (!listNode || !listNode->hasTagName(listTag))
94             return false;
95         start = startOfNextParagraph(start);
96     }
97
98     return true;
99 }
100
101 InsertListCommand::InsertListCommand(Document* document, Type type) 
102     : CompositeEditCommand(document), m_type(type)
103 {
104 }
105
106 void InsertListCommand::doApply()
107 {
108     if (!endingSelection().isNonOrphanedCaretOrRange())
109         return;
110
111     if (!endingSelection().rootEditableElement())
112         return;
113     
114     VisiblePosition visibleEnd = endingSelection().visibleEnd();
115     VisiblePosition visibleStart = endingSelection().visibleStart();
116     // When a selection ends at the start of a paragraph, we rarely paint 
117     // the selection gap before that paragraph, because there often is no gap.  
118     // In a case like this, it's not obvious to the user that the selection 
119     // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List 
120     // operated on that paragraph.
121     // FIXME: We paint the gap before some paragraphs that are indented with left 
122     // margin/padding, but not others.  We should make the gap painting more consistent and 
123     // then use a left margin/padding rule here.
124     if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
125         setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true)));
126
127     const QualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag;
128     if (endingSelection().isRange()) {
129         VisibleSelection selection = selectionForParagraphIteration(endingSelection());
130         ASSERT(selection.isRange());
131         VisiblePosition startOfSelection = selection.visibleStart();
132         VisiblePosition endOfSelection = selection.visibleEnd();
133         VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection);
134
135         if (startOfParagraph(startOfSelection) != startOfLastParagraph) {
136             bool forceCreateList = !selectionHasListOfType(selection, listTag);
137
138             RefPtr<Range> currentSelection = endingSelection().firstRange();
139             VisiblePosition startOfCurrentParagraph = startOfSelection;
140             while (startOfCurrentParagraph != startOfLastParagraph) {
141                 // doApply() may operate on and remove the last paragraph of the selection from the document 
142                 // if it's in the same list item as startOfCurrentParagraph.  Return early to avoid an 
143                 // infinite loop and because there is no more work to be done.
144                 // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here.  Compute 
145                 // the new location of endOfSelection and use it as the end of the new selection.
146                 if (!startOfLastParagraph.deepEquivalent().node()->inDocument())
147                     return;
148                 setEndingSelection(startOfCurrentParagraph);
149
150                 // Save and restore endOfSelection and startOfLastParagraph when necessary
151                 // since moveParagraph and movePragraphWithClones can remove nodes.
152                 // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from
153                 // the beginning of the document to the endOfSelection everytime this code is executed.
154                 // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph.
155                 int indexForEndOfSelection = indexForVisiblePosition(endOfSelection);
156                 doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get());
157                 if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) {
158                     RefPtr<Range> lastSelectionRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), indexForEndOfSelection, 0, true);
159                     // If lastSelectionRange is null, then some contents have been deleted from the document.
160                     // This should never happen and if it did, exit early immediately because we've lost the loop invariant.
161                     ASSERT(lastSelectionRange);
162                     if (!lastSelectionRange)
163                         return;
164                     endOfSelection = lastSelectionRange->startPosition();
165                     startOfLastParagraph = startOfParagraph(endOfSelection);
166                 }
167
168                 // Fetch the start of the selection after moving the first paragraph,
169                 // because moving the paragraph will invalidate the original start.  
170                 // We'll use the new start to restore the original selection after 
171                 // we modified all selected paragraphs.
172                 if (startOfCurrentParagraph == startOfSelection)
173                     startOfSelection = endingSelection().visibleStart();
174
175                 startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
176             }
177             setEndingSelection(endOfSelection);
178             doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get());
179             // Fetch the end of the selection, for the reason mentioned above.
180             endOfSelection = endingSelection().visibleEnd();
181             setEndingSelection(VisibleSelection(startOfSelection, endOfSelection));
182             return;
183         }
184     }
185
186     doApplyForSingleParagraph(false, listTag, endingSelection().firstRange().get());
187 }
188
189 void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const QualifiedName& listTag, Range* currentSelection)
190 {
191     // FIXME: This will produce unexpected results for a selection that starts just before a
192     // table and ends inside the first cell, selectionForParagraphIteration should probably
193     // be renamed and deployed inside setEndingSelection().
194     Node* selectionNode = endingSelection().start().node();
195     Node* listChildNode = enclosingListChild(selectionNode);
196     bool switchListType = false;
197     if (listChildNode) {
198         // Remove the list chlild.
199         RefPtr<HTMLElement> listNode = enclosingList(listChildNode);
200         if (!listNode) {
201             listNode = fixOrphanedListChild(listChildNode);
202             listNode = mergeWithNeighboringLists(listNode);
203         }
204         if (!listNode->hasTagName(listTag))
205             // listChildNode will be removed from the list and a list of type m_type will be created.
206             switchListType = true;
207
208         // If the list is of the desired type, and we are not removing the list, then exit early.
209         if (!switchListType && forceCreateList)
210             return;
211
212         // If the entire list is selected, then convert the whole list.
213         if (switchListType && isNodeVisiblyContainedWithin(listNode.get(), currentSelection)) {
214             bool rangeStartIsInList = visiblePositionBeforeNode(listNode.get()) == currentSelection->startPosition();
215             bool rangeEndIsInList = visiblePositionAfterNode(listNode.get()) == currentSelection->endPosition();
216
217             RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag);
218             insertNodeBefore(newList, listNode);
219
220             Node* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().node(), listNode.get());
221             Node* outerBlock = firstChildInList->isBlockFlow() ? firstChildInList : listNode.get();
222             
223             moveParagraphWithClones(firstPositionInNode(listNode.get()), lastPositionInNode(listNode.get()), newList.get(), outerBlock);
224
225             // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document.
226             // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html.
227             // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection.
228             if (listNode && listNode->inDocument())
229                 removeNode(listNode);
230
231             newList = mergeWithNeighboringLists(newList);
232
233             // Restore the start and the end of current selection if they started inside listNode
234             // because moveParagraphWithClones could have removed them.
235             ExceptionCode ec;
236             if (rangeStartIsInList && newList)
237                 currentSelection->setStart(newList, 0, ec);
238             if (rangeEndIsInList && newList)
239                 currentSelection->setEnd(newList, lastOffsetInNode(newList.get()), ec);
240
241             setEndingSelection(VisiblePosition(firstPositionInNode(newList.get())));
242
243             return;
244         }
245         
246         unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode);
247     }
248
249     if (!listChildNode || switchListType || forceCreateList)
250         m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag);
251 }
252
253 void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode)
254 {
255     Node* nextListChild;
256     Node* previousListChild;
257     VisiblePosition start;
258     VisiblePosition end;
259     if (listChildNode->hasTagName(liTag)) {
260         start = firstDeepEditingPositionForNode(listChildNode);
261         end = lastDeepEditingPositionForNode(listChildNode);
262         nextListChild = listChildNode->nextSibling();
263         previousListChild = listChildNode->previousSibling();
264     } else {
265         // A paragraph is visually a list item minus a list marker.  The paragraph will be moved.
266         start = startOfParagraph(originalStart);
267         end = endOfParagraph(start);
268         nextListChild = enclosingListChild(end.next().deepEquivalent().node(), listNode);
269         ASSERT(nextListChild != listChildNode);
270         previousListChild = enclosingListChild(start.previous().deepEquivalent().node(), listNode);
271         ASSERT(previousListChild != listChildNode);
272     }
273     // When removing a list, we must always create a placeholder to act as a point of insertion
274     // for the list content being removed.
275     RefPtr<Element> placeholder = createBreakElement(document());
276     RefPtr<Element> nodeToInsert = placeholder;
277     // If the content of the list item will be moved into another list, put it in a list item
278     // so that we don't create an orphaned list child.
279     if (enclosingList(listNode)) {
280         nodeToInsert = createListItemElement(document());
281         appendNode(placeholder, nodeToInsert);
282     }
283
284     if (nextListChild && previousListChild) {
285         // We want to pull listChildNode out of listNode, and place it before nextListChild 
286         // and after previousListChild, so we split listNode and insert it between the two lists.  
287         // But to split listNode, we must first split ancestors of listChildNode between it and listNode,
288         // if any exist.
289         // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove
290         // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is 
291         // unrendered. But we ought to remove nextListChild too, if it is unrendered.
292         splitElement(listNode, splitTreeToNode(nextListChild, listNode));
293         insertNodeBefore(nodeToInsert, listNode);
294     } else if (nextListChild || listChildNode->parentNode() != listNode) {
295         // Just because listChildNode has no previousListChild doesn't mean there isn't any content
296         // in listNode that comes before listChildNode, as listChildNode could have ancestors
297         // between it and listNode. So, we split up to listNode before inserting the placeholder
298         // where we're about to move listChildNode to.
299         if (listChildNode->parentNode() != listNode)
300             splitElement(listNode, splitTreeToNode(listChildNode, listNode).get());
301         insertNodeBefore(nodeToInsert, listNode);
302     } else
303         insertNodeAfter(nodeToInsert, listNode);
304
305     VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.get()));
306     moveParagraphs(start, end, insertionPoint, true);
307 }
308
309 static Element* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag)
310 {
311     Element* listNode = outermostEnclosingList(adjacentPos.deepEquivalent().node());
312
313     if (!listNode)
314         return 0;
315
316     Node* previousCell = enclosingTableCell(pos.deepEquivalent());
317     Node* currentCell = enclosingTableCell(adjacentPos.deepEquivalent());
318
319     if (!listNode->hasTagName(listTag)
320         || listNode->contains(pos.deepEquivalent().node())
321         || previousCell != currentCell
322         || enclosingList(listNode) != enclosingList(pos.deepEquivalent().node()))
323         return 0;
324
325     return listNode;
326 }
327
328 PassRefPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag)
329 {
330     VisiblePosition start = startOfParagraph(originalStart);
331     VisiblePosition end = endOfParagraph(start);
332     
333     if (start.isNull() || end.isNull())
334         return 0;
335
336     // Check for adjoining lists.
337     RefPtr<HTMLElement> listItemElement = createListItemElement(document());
338     RefPtr<HTMLElement> placeholder = createBreakElement(document());
339     appendNode(placeholder, listItemElement);
340
341     // Place list item into adjoining lists.
342     Element* previousList = adjacentEnclosingList(start.deepEquivalent(), start.previous(true), listTag);
343     Element* nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(true), listTag);
344     RefPtr<HTMLElement> listElement;
345     if (previousList)
346         appendNode(listItemElement, previousList);
347     else if (nextList)
348         insertNodeAt(listItemElement, positionBeforeNode(nextList));
349     else {
350         // Create the list.
351         listElement = createHTMLElement(document(), listTag);
352         appendNode(listItemElement, listElement);
353
354         if (start == end && isBlock(start.deepEquivalent().node())) {
355             // Inserting the list into an empty paragraph that isn't held open 
356             // by a br or a '\n', will invalidate start and end.  Insert 
357             // a placeholder and then recompute start and end.
358             RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent());
359             start = positionBeforeNode(placeholder.get());
360             end = start;
361         }
362
363         // Insert the list at a position visually equivalent to start of the
364         // paragraph that is being moved into the list. 
365         // Try to avoid inserting it somewhere where it will be surrounded by 
366         // inline ancestors of start, since it is easier for editing to produce 
367         // clean markup when inline elements are pushed down as far as possible.
368         Position insertionPos(start.deepEquivalent().upstream());
369         // Also avoid the containing list item.
370         Node* listChild = enclosingListChild(insertionPos.node());
371         if (listChild && listChild->hasTagName(liTag))
372             insertionPos = positionInParentBeforeNode(listChild);
373
374         insertNodeAt(listElement, insertionPos);
375
376         // We inserted the list at the start of the content we're about to move
377         // Update the start of content, so we don't try to move the list into itself.  bug 19066
378         if (insertionPos == start.deepEquivalent())
379             start = startOfParagraph(originalStart);
380     }
381
382     moveParagraph(start, end, positionBeforeNode(placeholder.get()), true);
383
384     if (listElement)
385         return mergeWithNeighboringLists(listElement);
386
387     if (canMergeLists(previousList, nextList))
388         mergeIdenticalElements(previousList, nextList);
389
390     return listElement;
391 }
392
393 }