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