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