Home | History | Annotate | Download | only in editing
      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 
     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 
     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 
     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 
     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 
     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 
    102 InsertListCommand::InsertListCommand(Document* document, Type type)
    103     : CompositeEditCommand(document), m_type(type)
    104 {
    105 }
    106 
    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             bool forceCreateList = !selectionHasListOfType(selection, listTag);
    138 
    139             RefPtr<Range> currentSelection = endingSelection().firstRange();
    140             VisiblePosition startOfCurrentParagraph = startOfSelection;
    141             while (!inSameParagraph(startOfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) {
    142                 // doApply() may operate on and remove the last paragraph of the selection from the document
    143                 // if it's in the same list item as startOfCurrentParagraph.  Return early to avoid an
    144                 // infinite loop and because there is no more work to be done.
    145                 // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here.  Compute
    146                 // the new location of endOfSelection and use it as the end of the new selection.
    147                 if (!startOfLastParagraph.deepEquivalent().anchorNode()->inDocument())
    148                     return;
    149                 setEndingSelection(startOfCurrentParagraph);
    150 
    151                 // Save and restore endOfSelection and startOfLastParagraph when necessary
    152                 // since moveParagraph and movePragraphWithClones can remove nodes.
    153                 // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from
    154                 // the beginning of the document to the endOfSelection everytime this code is executed.
    155                 // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph.
    156                 RefPtr<ContainerNode> scope;
    157                 int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scope);
    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             endOfSelection = endingSelection().visibleEnd();
    182             setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional()));
    183             return;
    184         }
    185     }
    186 
    187     doApplyForSingleParagraph(false, listTag, endingSelection().firstRange().get());
    188 }
    189 
    190 void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const QualifiedName& listTag, Range* currentSelection)
    191 {
    192     // FIXME: This will produce unexpected results for a selection that starts just before a
    193     // table and ends inside the first cell, selectionForParagraphIteration should probably
    194     // be renamed and deployed inside setEndingSelection().
    195     Node* selectionNode = endingSelection().start().deprecatedNode();
    196     Node* listChildNode = enclosingListChild(selectionNode);
    197     bool switchListType = false;
    198     if (listChildNode) {
    199         // Remove the list chlild.
    200         RefPtr<HTMLElement> listNode = enclosingList(listChildNode);
    201         if (!listNode) {
    202             listNode = fixOrphanedListChild(listChildNode);
    203             listNode = mergeWithNeighboringLists(listNode);
    204         }
    205         if (!listNode->hasTagName(listTag))
    206             // listChildNode will be removed from the list and a list of type m_type will be created.
    207             switchListType = true;
    208 
    209         // If the list is of the desired type, and we are not removing the list, then exit early.
    210         if (!switchListType && forceCreateList)
    211             return;
    212 
    213         // If the entire list is selected, then convert the whole list.
    214         if (switchListType && isNodeVisiblyContainedWithin(listNode.get(), currentSelection)) {
    215             bool rangeStartIsInList = visiblePositionBeforeNode(listNode.get()) == currentSelection->startPosition();
    216             bool rangeEndIsInList = visiblePositionAfterNode(listNode.get()) == currentSelection->endPosition();
    217 
    218             RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag);
    219             insertNodeBefore(newList, listNode);
    220 
    221             Node* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().deprecatedNode(), listNode.get());
    222             Node* outerBlock = firstChildInList->isBlockFlowElement() ? firstChildInList : listNode.get();
    223 
    224             moveParagraphWithClones(firstPositionInNode(listNode.get()), lastPositionInNode(listNode.get()), newList.get(), outerBlock);
    225 
    226             // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document.
    227             // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html.
    228             // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection.
    229             if (listNode && listNode->inDocument())
    230                 removeNode(listNode);
    231 
    232             newList = mergeWithNeighboringLists(newList);
    233 
    234             // Restore the start and the end of current selection if they started inside listNode
    235             // because moveParagraphWithClones could have removed them.
    236             if (rangeStartIsInList && newList)
    237                 currentSelection->setStart(newList, 0, IGNORE_EXCEPTION);
    238             if (rangeEndIsInList && newList)
    239                 currentSelection->setEnd(newList, lastOffsetInNode(newList.get()), IGNORE_EXCEPTION);
    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 = firstPositionInNode(listChildNode);
    261         end = lastPositionInNode(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, CanSkipOverEditingBoundary);
    267         end = endOfParagraph(start, CanSkipOverEditingBoundary);
    268         nextListChild = enclosingListChild(end.next().deepEquivalent().deprecatedNode(), listNode);
    269         ASSERT(nextListChild != listChildNode);
    270         previousListChild = enclosingListChild(start.previous().deepEquivalent().deprecatedNode(), 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, /* preserveSelection */ true, /* preserveStyle */ true, listChildNode);
    307 }
    308 
    309 static Element* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag)
    310 {
    311     Element* listNode = outermostEnclosingList(adjacentPos.deepEquivalent().deprecatedNode());
    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().deprecatedNode())
    321         || previousCell != currentCell
    322         || enclosingList(listNode) != enclosingList(pos.deepEquivalent().deprecatedNode()))
    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, CanSkipOverEditingBoundary);
    331     VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary);
    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(CannotCrossEditingBoundary), listTag);
    343     Element* nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(CannotCrossEditingBoundary), 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().deprecatedNode())) {
    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.deprecatedNode());
    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         // Layout is necessary since start's node's inline renderers may have been destroyed by the insertion
    379         // The end of the content may have changed after the insertion and layout so update it as well.
    380         if (insertionPos == start.deepEquivalent()) {
    381             listElement->document()->updateLayoutIgnorePendingStylesheets();
    382             start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
    383             end = endOfParagraph(start, CanSkipOverEditingBoundary);
    384         }
    385     }
    386 
    387     moveParagraph(start, end, positionBeforeNode(placeholder.get()), true);
    388 
    389     if (listElement)
    390         return mergeWithNeighboringLists(listElement);
    391 
    392     if (canMergeLists(previousList, nextList))
    393         mergeIdenticalElements(previousList, nextList);
    394 
    395     return listElement;
    396 }
    397 
    398 }
    399