Home | History | Annotate | Download | only in editing
      1 /*
      2  * Copyright (C) 2006 Apple Computer, 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 "Element.h"
     28 #include "InsertListCommand.h"
     29 #include "DocumentFragment.h"
     30 #include "htmlediting.h"
     31 #include "HTMLElement.h"
     32 #include "HTMLNames.h"
     33 #include "TextIterator.h"
     34 #include "visible_units.h"
     35 
     36 namespace WebCore {
     37 
     38 using namespace HTMLNames;
     39 
     40 PassRefPtr<HTMLElement> InsertListCommand::insertList(Document* document, Type type)
     41 {
     42     RefPtr<InsertListCommand> insertCommand = new InsertListCommand(document, type);
     43     insertCommand->apply();
     44     return insertCommand->m_listElement;
     45 }
     46 
     47 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node)
     48 {
     49     RefPtr<HTMLElement> listElement = createUnorderedListElement(document());
     50     insertNodeBefore(listElement, node);
     51     removeNode(node);
     52     appendNode(node, listElement);
     53     m_listElement = listElement;
     54     return listElement.get();
     55 }
     56 
     57 InsertListCommand::InsertListCommand(Document* document, Type type)
     58     : CompositeEditCommand(document), m_type(type), m_forceCreateList(false)
     59 {
     60 }
     61 
     62 bool InsertListCommand::modifyRange()
     63 {
     64     VisibleSelection selection = selectionForParagraphIteration(endingSelection());
     65     ASSERT(selection.isRange());
     66     VisiblePosition startOfSelection = selection.visibleStart();
     67     VisiblePosition endOfSelection = selection.visibleEnd();
     68     VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection);
     69 
     70     if (startOfParagraph(startOfSelection) == startOfLastParagraph)
     71         return false;
     72 
     73     Node* startList = enclosingList(startOfSelection.deepEquivalent().node());
     74     Node* endList = enclosingList(endOfSelection.deepEquivalent().node());
     75     if (!startList || startList != endList)
     76         m_forceCreateList = true;
     77 
     78     setEndingSelection(startOfSelection);
     79     doApply();
     80     // Fetch the start of the selection after moving the first paragraph,
     81     // because moving the paragraph will invalidate the original start.
     82     // We'll use the new start to restore the original selection after
     83     // we modified all selected paragraphs.
     84     startOfSelection = endingSelection().visibleStart();
     85     VisiblePosition startOfCurrentParagraph = startOfNextParagraph(startOfSelection);
     86     while (startOfCurrentParagraph != startOfLastParagraph) {
     87         // doApply() may operate on and remove the last paragraph of the selection from the document
     88         // if it's in the same list item as startOfCurrentParagraph.  Return early to avoid an
     89         // infinite loop and because there is no more work to be done.
     90         // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here.  Compute
     91         // the new location of endOfSelection and use it as the end of the new selection.
     92         if (!startOfLastParagraph.deepEquivalent().node()->inDocument())
     93             return true;
     94         setEndingSelection(startOfCurrentParagraph);
     95         doApply();
     96         startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
     97     }
     98     setEndingSelection(endOfSelection);
     99     doApply();
    100     // Fetch the end of the selection, for the reason mentioned above.
    101     endOfSelection = endingSelection().visibleEnd();
    102     setEndingSelection(VisibleSelection(startOfSelection, endOfSelection));
    103     m_forceCreateList = false;
    104     return true;
    105 }
    106 
    107 void InsertListCommand::doApply()
    108 {
    109     if (endingSelection().isNone())
    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))
    126         setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true)));
    127 
    128     if (endingSelection().isRange() && modifyRange())
    129         return;
    130 
    131     // FIXME: This will produce unexpected results for a selection that starts just before a
    132     // table and ends inside the first cell, selectionForParagraphIteration should probably
    133     // be renamed and deployed inside setEndingSelection().
    134     Node* selectionNode = endingSelection().start().node();
    135     const QualifiedName listTag = (m_type == OrderedList) ? olTag : ulTag;
    136     Node* listChildNode = enclosingListChild(selectionNode);
    137     bool switchListType = false;
    138     if (listChildNode) {
    139         // Remove the list chlild.
    140         HTMLElement* listNode = enclosingList(listChildNode);
    141         if (!listNode)
    142             listNode = fixOrphanedListChild(listChildNode);
    143         if (!listNode->hasTagName(listTag))
    144             // listChildNode will be removed from the list and a list of type m_type will be created.
    145             switchListType = true;
    146         Node* nextListChild;
    147         Node* previousListChild;
    148         VisiblePosition start;
    149         VisiblePosition end;
    150         if (listChildNode->hasTagName(liTag)) {
    151             start = firstDeepEditingPositionForNode(listChildNode);
    152             end = lastDeepEditingPositionForNode(listChildNode);
    153             nextListChild = listChildNode->nextSibling();
    154             previousListChild = listChildNode->previousSibling();
    155         } else {
    156             // A paragraph is visually a list item minus a list marker.  The paragraph will be moved.
    157             start = startOfParagraph(endingSelection().visibleStart());
    158             end = endOfParagraph(endingSelection().visibleEnd());
    159             nextListChild = enclosingListChild(end.next().deepEquivalent().node());
    160             ASSERT(nextListChild != listChildNode);
    161             if (enclosingList(nextListChild) != listNode)
    162                 nextListChild = 0;
    163             previousListChild = enclosingListChild(start.previous().deepEquivalent().node());
    164             ASSERT(previousListChild != listChildNode);
    165             if (enclosingList(previousListChild) != listNode)
    166                 previousListChild = 0;
    167         }
    168         // When removing a list, we must always create a placeholder to act as a point of insertion
    169         // for the list content being removed.
    170         RefPtr<Element> placeholder = createBreakElement(document());
    171         RefPtr<Element> nodeToInsert = placeholder;
    172         // If the content of the list item will be moved into another list, put it in a list item
    173         // so that we don't create an orphaned list child.
    174         if (enclosingList(listNode)) {
    175             nodeToInsert = createListItemElement(document());
    176             appendNode(placeholder, nodeToInsert);
    177         }
    178 
    179         if (nextListChild && previousListChild) {
    180             // We want to pull listChildNode out of listNode, and place it before nextListChild
    181             // and after previousListChild, so we split listNode and insert it between the two lists.
    182             // But to split listNode, we must first split ancestors of listChildNode between it and listNode,
    183             // if any exist.
    184             // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove
    185             // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is
    186             // unrendered. But we ought to remove nextListChild too, if it is unrendered.
    187             splitElement(listNode, splitTreeToNode(nextListChild, listNode));
    188             insertNodeBefore(nodeToInsert, listNode);
    189         } else if (nextListChild || listChildNode->parentNode() != listNode) {
    190             // Just because listChildNode has no previousListChild doesn't mean there isn't any content
    191             // in listNode that comes before listChildNode, as listChildNode could have ancestors
    192             // between it and listNode. So, we split up to listNode before inserting the placeholder
    193             // where we're about to move listChildNode to.
    194             if (listChildNode->parentNode() != listNode)
    195                 splitElement(listNode, splitTreeToNode(listChildNode, listNode).get());
    196             insertNodeBefore(nodeToInsert, listNode);
    197         } else
    198             insertNodeAfter(nodeToInsert, listNode);
    199 
    200         VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0));
    201         moveParagraphs(start, end, insertionPoint, true);
    202     }
    203     if (!listChildNode || switchListType || m_forceCreateList) {
    204         // Create list.
    205         VisiblePosition originalStart = endingSelection().visibleStart();
    206         VisiblePosition start = startOfParagraph(originalStart);
    207         VisiblePosition end = endOfParagraph(endingSelection().visibleEnd());
    208 
    209         // Check for adjoining lists.
    210         VisiblePosition previousPosition = start.previous(true);
    211         VisiblePosition nextPosition = end.next(true);
    212         RefPtr<HTMLElement> listItemElement = createListItemElement(document());
    213         RefPtr<HTMLElement> placeholder = createBreakElement(document());
    214         appendNode(placeholder, listItemElement);
    215         Element* previousList = outermostEnclosingList(previousPosition.deepEquivalent().node());
    216         Element* nextList = outermostEnclosingList(nextPosition.deepEquivalent().node());
    217         Node* startNode = start.deepEquivalent().node();
    218         Node* previousCell = enclosingTableCell(previousPosition.deepEquivalent());
    219         Node* nextCell = enclosingTableCell(nextPosition.deepEquivalent());
    220         Node* currentCell = enclosingTableCell(start.deepEquivalent());
    221         if (previousList && (!previousList->hasTagName(listTag) || startNode->isDescendantOf(previousList) || previousCell != currentCell))
    222             previousList = 0;
    223         if (nextList && (!nextList->hasTagName(listTag) || startNode->isDescendantOf(nextList) || nextCell != currentCell))
    224             nextList = 0;
    225         // Place list item into adjoining lists.
    226         if (previousList)
    227             appendNode(listItemElement, previousList);
    228         else if (nextList)
    229             insertNodeAt(listItemElement, Position(nextList, 0));
    230         else {
    231             // Create the list.
    232             RefPtr<HTMLElement> listElement = m_type == OrderedList ? createOrderedListElement(document()) : createUnorderedListElement(document());
    233             m_listElement = listElement;
    234             appendNode(listItemElement, listElement);
    235 
    236             if (start == end && isBlock(start.deepEquivalent().node())) {
    237                 // Inserting the list into an empty paragraph that isn't held open
    238                 // by a br or a '\n', will invalidate start and end.  Insert
    239                 // a placeholder and then recompute start and end.
    240                 RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent());
    241                 start = VisiblePosition(Position(placeholder.get(), 0));
    242                 end = start;
    243             }
    244 
    245             // Insert the list at a position visually equivalent to start of the
    246             // paragraph that is being moved into the list.
    247             // Try to avoid inserting it somewhere where it will be surrounded by
    248             // inline ancestors of start, since it is easier for editing to produce
    249             // clean markup when inline elements are pushed down as far as possible.
    250             Position insertionPos(start.deepEquivalent().upstream());
    251             // Also avoid the containing list item.
    252             Node* listChild = enclosingListChild(insertionPos.node());
    253             if (listChild && listChild->hasTagName(liTag))
    254                 insertionPos = positionInParentBeforeNode(listChild);
    255 
    256             insertNodeAt(listElement, insertionPos);
    257 
    258             // We inserted the list at the start of the content we're about to move
    259             // Update the start of content, so we don't try to move the list into itself.  bug 19066
    260             if (insertionPos == start.deepEquivalent())
    261                 start = startOfParagraph(originalStart);
    262         }
    263         moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true);
    264         if (nextList && previousList)
    265             mergeIdenticalElements(previousList, nextList);
    266     }
    267 }
    268 
    269 }
    270