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 "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 
     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 
     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 
     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 
     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 
    101 InsertListCommand::InsertListCommand(Document& document, Type type)
    102     : CompositeEditCommand(document), m_type(type)
    103 {
    104 }
    105 
    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 
    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 
    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 
    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 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 
    408 void InsertListCommand::trace(Visitor* visitor)
    409 {
    410     visitor->trace(m_listElement);
    411     CompositeEditCommand::trace(visitor);
    412 }
    413 
    414 }
    415