• 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 "bindings/v8/ExceptionStatePlaceholder.h"
30 #include "core/HTMLNames.h"
31 #include "core/dom/Document.h"
32 #include "core/dom/Element.h"
33 #include "core/dom/ElementTraversal.h"
34 #include "core/editing/TextIterator.h"
35 #include "core/editing/VisibleUnits.h"
36 #include "core/editing/htmlediting.h"
37 #include "core/html/HTMLElement.h"
38 
39 namespace WebCore {
40 
41 using namespace HTMLNames;
42 
enclosingListChild(Node * node,Node * listNode)43 static Node* enclosingListChild(Node* node, Node* listNode)
44 {
45     Node* listChild = enclosingListChild(node);
46     while (listChild && enclosingList(listChild) != listNode)
47         listChild = enclosingListChild(listChild->parentNode());
48     return listChild;
49 }
50 
fixOrphanedListChild(Node * node)51 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node)
52 {
53     RefPtrWillBeRawPtr<HTMLElement> listElement = createUnorderedListElement(document());
54     insertNodeBefore(listElement, node);
55     removeNode(node);
56     appendNode(node, listElement);
57     m_listElement = listElement;
58     return listElement.get();
59 }
60 
mergeWithNeighboringLists(PassRefPtrWillBeRawPtr<HTMLElement> passedList)61 PassRefPtrWillBeRawPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtrWillBeRawPtr<HTMLElement> passedList)
62 {
63     RefPtrWillBeRawPtr<HTMLElement> list = passedList;
64     Element* previousList = ElementTraversal::previousSibling(*list);
65     if (canMergeLists(previousList, list.get()))
66         mergeIdenticalElements(previousList, list);
67 
68     if (!list)
69         return nullptr;
70 
71     Element* nextSibling = ElementTraversal::nextSibling(*list);
72     if (!nextSibling || !nextSibling->isHTMLElement())
73         return list.release();
74 
75     RefPtrWillBeRawPtr<HTMLElement> nextList = toHTMLElement(nextSibling);
76     if (canMergeLists(list.get(), nextList.get())) {
77         mergeIdenticalElements(list, nextList);
78         return nextList.release();
79     }
80     return list.release();
81 }
82 
selectionHasListOfType(const VisibleSelection & selection,const QualifiedName & listTag)83 bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag)
84 {
85     VisiblePosition start = selection.visibleStart();
86 
87     if (!enclosingList(start.deepEquivalent().deprecatedNode()))
88         return false;
89 
90     VisiblePosition end = startOfParagraph(selection.visibleEnd());
91     while (start.isNotNull() && start != end) {
92         Element* listNode = enclosingList(start.deepEquivalent().deprecatedNode());
93         if (!listNode || !listNode->hasTagName(listTag))
94             return false;
95         start = startOfNextParagraph(start);
96     }
97 
98     return true;
99 }
100 
InsertListCommand(Document & document,Type type)101 InsertListCommand::InsertListCommand(Document& document, Type type)
102     : CompositeEditCommand(document), m_type(type)
103 {
104 }
105 
doApply()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, CanSkipOverEditingBoundary)) {
125         setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional()));
126         if (!endingSelection().rootEditableElement())
127             return;
128     }
129 
130     const QualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag;
131     if (endingSelection().isRange()) {
132         VisibleSelection selection = selectionForParagraphIteration(endingSelection());
133         ASSERT(selection.isRange());
134         VisiblePosition startOfSelection = selection.visibleStart();
135         VisiblePosition endOfSelection = selection.visibleEnd();
136         VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
137 
138         if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary) != startOfLastParagraph) {
139             RefPtrWillBeRawPtr<ContainerNode> scope = nullptr;
140             int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scope);
141             bool forceCreateList = !selectionHasListOfType(selection, listTag);
142 
143             RefPtrWillBeRawPtr<Range> currentSelection = endingSelection().firstRange();
144             VisiblePosition startOfCurrentParagraph = startOfSelection;
145             while (startOfCurrentParagraph.isNotNull() && !inSameParagraph(startOfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) {
146                 // doApply() may operate on and remove the last paragraph of the selection from the document
147                 // if it's in the same list item as startOfCurrentParagraph.  Return early to avoid an
148                 // infinite loop and because there is no more work to be done.
149                 // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here.  Compute
150                 // the new location of endOfSelection and use it as the end of the new selection.
151                 if (!startOfLastParagraph.deepEquivalent().inDocument())
152                     return;
153                 setEndingSelection(startOfCurrentParagraph);
154 
155                 // Save and restore endOfSelection and startOfLastParagraph when necessary
156                 // since moveParagraph and movePragraphWithClones can remove nodes.
157                 // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from
158                 // the beginning of the document to the endOfSelection everytime this code is executed.
159                 // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph.
160                 doApplyForSingleParagraph(forceCreateList, listTag, *currentSelection);
161                 if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) {
162                     endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.get());
163                     // If endOfSelection is null, then some contents have been deleted from the document.
164                     // This should never happen and if it did, exit early immediately because we've lost the loop invariant.
165                     ASSERT(endOfSelection.isNotNull());
166                     if (endOfSelection.isNull())
167                         return;
168                     startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
169                 }
170 
171                 // Fetch the start of the selection after moving the first paragraph,
172                 // because moving the paragraph will invalidate the original start.
173                 // We'll use the new start to restore the original selection after
174                 // we modified all selected paragraphs.
175                 if (startOfCurrentParagraph == startOfSelection)
176                     startOfSelection = endingSelection().visibleStart();
177 
178                 startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
179             }
180             setEndingSelection(endOfSelection);
181             doApplyForSingleParagraph(forceCreateList, listTag, *currentSelection);
182             // Fetch the end of the selection, for the reason mentioned above.
183             if (endOfSelection.isNull() || endOfSelection.isOrphan()) {
184                 endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.get());
185                 if (endOfSelection.isNull())
186                     return;
187             }
188             setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional()));
189             return;
190         }
191     }
192 
193     ASSERT(endingSelection().firstRange());
194     doApplyForSingleParagraph(false, listTag, *endingSelection().firstRange());
195 }
196 
doApplyForSingleParagraph(bool forceCreateList,const QualifiedName & listTag,Range & currentSelection)197 void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const QualifiedName& listTag, Range& currentSelection)
198 {
199     // FIXME: This will produce unexpected results for a selection that starts just before a
200     // table and ends inside the first cell, selectionForParagraphIteration should probably
201     // be renamed and deployed inside setEndingSelection().
202     Node* selectionNode = endingSelection().start().deprecatedNode();
203     Node* listChildNode = enclosingListChild(selectionNode);
204     bool switchListType = false;
205     if (listChildNode) {
206         // Remove the list chlild.
207         RefPtrWillBeRawPtr<HTMLElement> listNode = enclosingList(listChildNode);
208         if (!listNode) {
209             listNode = fixOrphanedListChild(listChildNode);
210             listNode = mergeWithNeighboringLists(listNode);
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         // If the list is of the desired type, and we are not removing the list, then exit early.
217         if (!switchListType && forceCreateList)
218             return;
219 
220         // If the entire list is selected, then convert the whole list.
221         if (switchListType && isNodeVisiblyContainedWithin(*listNode, currentSelection)) {
222             bool rangeStartIsInList = visiblePositionBeforeNode(*listNode) == VisiblePosition(currentSelection.startPosition());
223             bool rangeEndIsInList = visiblePositionAfterNode(*listNode) == VisiblePosition(currentSelection.endPosition());
224 
225             RefPtrWillBeRawPtr<HTMLElement> newList = createHTMLElement(document(), listTag);
226             insertNodeBefore(newList, listNode);
227 
228             Node* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().deprecatedNode(), listNode.get());
229             Node* outerBlock = firstChildInList && firstChildInList->isBlockFlowElement() ? firstChildInList : listNode.get();
230 
231             moveParagraphWithClones(VisiblePosition(firstPositionInNode(listNode.get())), VisiblePosition(lastPositionInNode(listNode.get())), newList.get(), outerBlock);
232 
233             // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document.
234             // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html.
235             // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection.
236             if (listNode && listNode->inDocument())
237                 removeNode(listNode);
238 
239             newList = mergeWithNeighboringLists(newList);
240 
241             // Restore the start and the end of current selection if they started inside listNode
242             // because moveParagraphWithClones could have removed them.
243             if (rangeStartIsInList && newList)
244                 currentSelection.setStart(newList, 0, IGNORE_EXCEPTION);
245             if (rangeEndIsInList && newList)
246                 currentSelection.setEnd(newList, lastOffsetInNode(newList.get()), IGNORE_EXCEPTION);
247 
248             setEndingSelection(VisiblePosition(firstPositionInNode(newList.get())));
249 
250             return;
251         }
252 
253         unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode);
254     }
255 
256     if (!listChildNode || switchListType || forceCreateList)
257         m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag);
258 }
259 
unlistifyParagraph(const VisiblePosition & originalStart,HTMLElement * listNode,Node * listChildNode)260 void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode)
261 {
262     Node* nextListChild;
263     Node* previousListChild;
264     VisiblePosition start;
265     VisiblePosition end;
266     ASSERT(listChildNode);
267     if (isHTMLLIElement(*listChildNode)) {
268         start = VisiblePosition(firstPositionInNode(listChildNode));
269         end = VisiblePosition(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     RefPtrWillBeRawPtr<Element> placeholder = createBreakElement(document());
284     RefPtrWillBeRawPtr<Element> nodeToInsert = placeholder;
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 = createListItemElement(document());
289         appendNode(placeholder, 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, 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, listNode);
310     } else
311         insertNodeAfter(nodeToInsert, listNode);
312 
313     VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.get()));
314     moveParagraphs(start, end, insertionPoint, /* preserveSelection */ true, /* preserveStyle */ true, listChildNode);
315 }
316 
adjacentEnclosingList(const VisiblePosition & pos,const VisiblePosition & adjacentPos,const QualifiedName & listTag)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 
listifyParagraph(const VisiblePosition & originalStart,const QualifiedName & listTag)336 PassRefPtrWillBeRawPtr<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 nullptr;
343 
344     // Check for adjoining lists.
345     RefPtrWillBeRawPtr<HTMLElement> listItemElement = createListItemElement(document());
346     RefPtrWillBeRawPtr<HTMLElement> placeholder = createBreakElement(document());
347     appendNode(placeholder, listItemElement);
348 
349     // Place list item into adjoining lists.
350     Element* previousList = adjacentEnclosingList(start, start.previous(CannotCrossEditingBoundary), listTag);
351     Element* nextList = adjacentEnclosingList(start, end.next(CannotCrossEditingBoundary), listTag);
352     RefPtrWillBeRawPtr<HTMLElement> listElement = nullptr;
353     if (previousList)
354         appendNode(listItemElement, previousList);
355     else if (nextList)
356         insertNodeAt(listItemElement, positionBeforeNode(nextList));
357     else {
358         // Create the list.
359         listElement = createHTMLElement(document(), listTag);
360         appendNode(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             RefPtrWillBeRawPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent());
367             start = VisiblePosition(positionBeforeNode(placeholder.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 (isHTMLLIElement(listChild))
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             start = originalStart;
390     }
391 
392     // Inserting list element and list item list may change start of pargraph
393     // to move. We calculate start of paragraph again.
394     document().updateLayoutIgnorePendingStylesheets();
395     start = startOfParagraph(start, CanSkipOverEditingBoundary);
396     end = endOfParagraph(start, CanSkipOverEditingBoundary);
397     moveParagraph(start, end, VisiblePosition(positionBeforeNode(placeholder.get())), true);
398 
399     if (listElement)
400         return mergeWithNeighboringLists(listElement);
401 
402     if (canMergeLists(previousList, nextList))
403         mergeIdenticalElements(previousList, nextList);
404 
405     return listElement;
406 }
407 
trace(Visitor * visitor)408 void InsertListCommand::trace(Visitor* visitor)
409 {
410     visitor->trace(m_listElement);
411     CompositeEditCommand::trace(visitor);
412 }
413 
414 }
415