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