Home | History | Annotate | Download | only in editing
      1 /*
      2  * Copyright (C) 2006, 2008 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 (IndentOutdentCommandINCLUDING, 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 "IndentOutdentCommand.h"
     28 
     29 #include "Document.h"
     30 #include "Element.h"
     31 #include "HTMLBlockquoteElement.h"
     32 #include "HTMLNames.h"
     33 #include "InsertLineBreakCommand.h"
     34 #include "InsertListCommand.h"
     35 #include "Range.h"
     36 #include "SplitElementCommand.h"
     37 #include "TextIterator.h"
     38 #include "htmlediting.h"
     39 #include "visible_units.h"
     40 #include <wtf/StdLibExtras.h>
     41 
     42 namespace WebCore {
     43 
     44 using namespace HTMLNames;
     45 
     46 static String indentBlockquoteString()
     47 {
     48     DEFINE_STATIC_LOCAL(String, string, ("webkit-indent-blockquote"));
     49     return string;
     50 }
     51 
     52 static PassRefPtr<HTMLBlockquoteElement> createIndentBlockquoteElement(Document* document)
     53 {
     54     RefPtr<HTMLBlockquoteElement> element = new HTMLBlockquoteElement(blockquoteTag, document);
     55     element->setAttribute(classAttr, indentBlockquoteString());
     56     element->setAttribute(styleAttr, "margin: 0 0 0 40px; border: none; padding: 0px;");
     57     return element.release();
     58 }
     59 
     60 static bool isListOrIndentBlockquote(const Node* node)
     61 {
     62     return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(blockquoteTag));
     63 }
     64 
     65 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
     66     : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
     67 {
     68 }
     69 
     70 bool IndentOutdentCommand::tryIndentingAsListItem(const VisiblePosition& endOfCurrentParagraph)
     71 {
     72     // If our selection is not inside a list, bail out.
     73     Node* lastNodeInSelectedParagraph = endOfCurrentParagraph.deepEquivalent().node();
     74     RefPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph);
     75     if (!listNode)
     76         return false;
     77 
     78     // Find the list item enclosing the current paragraph
     79     Element* selectedListItem = static_cast<Element*>(enclosingBlock(lastNodeInSelectedParagraph));
     80     // FIXME: enclosingBlock shouldn't return the passed in element.  See the
     81     // comment on the function about how to fix rather than having to adjust here.
     82     if (selectedListItem == lastNodeInSelectedParagraph)
     83         selectedListItem = static_cast<Element*>(enclosingBlock(lastNodeInSelectedParagraph->parentNode()));
     84 
     85     // FIXME: we need to deal with the case where there is no li (malformed HTML)
     86     if (!selectedListItem->hasTagName(liTag))
     87         return false;
     88 
     89     // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>.  Should we?
     90     Element* previousList = selectedListItem->previousElementSibling();
     91     Element* nextList = selectedListItem->nextElementSibling();
     92 
     93     RefPtr<Element> newList = document()->createElement(listNode->tagQName(), false);
     94     insertNodeBefore(newList, selectedListItem);
     95 
     96     moveParagraphWithClones(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, newList.get(), selectedListItem);
     97 
     98     if (canMergeLists(previousList, newList.get()))
     99         mergeIdenticalElements(previousList, newList);
    100     if (canMergeLists(newList.get(), nextList))
    101         mergeIdenticalElements(newList, nextList);
    102 
    103     return true;
    104 }
    105 
    106 void IndentOutdentCommand::indentIntoBlockquote(const VisiblePosition& endOfCurrentParagraph, const VisiblePosition& endOfNextParagraph, RefPtr<Element>& targetBlockquote)
    107 {
    108     Node* enclosingCell = 0;
    109 
    110     Position start = startOfParagraph(endOfCurrentParagraph).deepEquivalent();
    111     enclosingCell = enclosingNodeOfType(start, &isTableCell);
    112     Node* nodeToSplitTo;
    113     if (enclosingCell)
    114         nodeToSplitTo = enclosingCell;
    115     else if (enclosingList(start.node()))
    116         nodeToSplitTo = enclosingBlock(start.node());
    117     else
    118         nodeToSplitTo = editableRootForPosition(start);
    119 
    120     RefPtr<Node> outerBlock = splitTreeToNode(start.node(), nodeToSplitTo);
    121 
    122     if (!targetBlockquote) {
    123         // Create a new blockquote and insert it as a child of the root editable element. We accomplish
    124         // this by splitting all parents of the current paragraph up to that point.
    125         targetBlockquote = createIndentBlockquoteElement(document());
    126         insertNodeBefore(targetBlockquote, outerBlock);
    127     }
    128 
    129     moveParagraphWithClones(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, targetBlockquote.get(), outerBlock.get());
    130 
    131     // Don't put the next paragraph in the blockquote we just created for this paragraph unless
    132     // the next paragraph is in the same cell.
    133     if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell))
    134         targetBlockquote = 0;
    135 }
    136 
    137 void IndentOutdentCommand::indentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
    138 {
    139     // Special case empty unsplittable elements because there's nothing to split
    140     // and there's nothing to move.
    141     Position start = startOfSelection.deepEquivalent().downstream();
    142     if (isAtUnsplittableElement(start)) {
    143         RefPtr<Element> blockquote = createIndentBlockquoteElement(document());
    144         insertNodeAt(blockquote, start);
    145         RefPtr<Element> placeholder = createBreakElement(document());
    146         appendNode(placeholder, blockquote);
    147         setEndingSelection(VisibleSelection(Position(placeholder.get(), 0), DOWNSTREAM));
    148         return;
    149     }
    150 
    151     RefPtr<Element> blockquoteForNextIndent;
    152     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
    153     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
    154     while (endOfCurrentParagraph != endAfterSelection) {
    155         // Iterate across the selected paragraphs...
    156         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
    157         if (tryIndentingAsListItem(endOfCurrentParagraph))
    158             blockquoteForNextIndent = 0;
    159         else
    160             indentIntoBlockquote(endOfCurrentParagraph, endOfNextParagraph, blockquoteForNextIndent);
    161 
    162         // indentIntoBlockquote could move more than one paragraph if the paragraph
    163         // is in a list item or a table. As a result, endAfterSelection could refer to a position
    164         // no longer in the document.
    165         if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().node()->inDocument())
    166             break;
    167         // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().node()
    168         // If somehow we did, return to prevent crashes.
    169         if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) {
    170             ASSERT_NOT_REACHED();
    171             return;
    172         }
    173         endOfCurrentParagraph = endOfNextParagraph;
    174     }
    175 }
    176 
    177 void IndentOutdentCommand::outdentParagraph()
    178 {
    179     VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
    180     VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
    181 
    182     Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote);
    183     if (!enclosingNode || !enclosingNode->parentNode()->isContentEditable())  // We can't outdent if there is no place to go!
    184         return;
    185 
    186     // Use InsertListCommand to remove the selection from the list
    187     if (enclosingNode->hasTagName(olTag)) {
    188         applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList));
    189         return;
    190     }
    191     if (enclosingNode->hasTagName(ulTag)) {
    192         applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList));
    193         return;
    194     }
    195 
    196     // The selection is inside a blockquote i.e. enclosingNode is a blockquote
    197     VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
    198     VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
    199     VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(Position(enclosingNode, enclosingNode->childNodeCount()));
    200     VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock);
    201     if (visibleStartOfParagraph == startOfEnclosingBlock &&
    202         visibleEndOfParagraph == endOfEnclosingBlock) {
    203         // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
    204         Node* splitPoint = enclosingNode->nextSibling();
    205         removeNodePreservingChildren(enclosingNode);
    206         // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've
    207         // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true
    208         if (splitPoint) {
    209             if (Node* splitPointParent = splitPoint->parentNode()) {
    210                 if (splitPointParent->hasTagName(blockquoteTag)
    211                     && !splitPoint->hasTagName(blockquoteTag)
    212                     && splitPointParent->parentNode()->isContentEditable()) // We can't outdent if there is no place to go!
    213                     splitElement(static_cast<Element*>(splitPointParent), splitPoint);
    214             }
    215         }
    216 
    217         updateLayout();
    218         visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
    219         visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
    220         if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
    221             insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent());
    222         if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
    223             insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent());
    224 
    225         return;
    226     }
    227     Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().node());
    228     RefPtr<Node> splitBlockquoteNode = enclosingNode;
    229     if (enclosingBlockFlow != enclosingNode)
    230         splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true);
    231     else {
    232         // We split the blockquote at where we start outdenting.
    233         splitElement(static_cast<Element*>(enclosingNode), visibleStartOfParagraph.deepEquivalent().node());
    234     }
    235     RefPtr<Node> placeholder = createBreakElement(document());
    236     insertNodeBefore(placeholder, splitBlockquoteNode);
    237     moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
    238 }
    239 
    240 void IndentOutdentCommand::outdentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
    241 {
    242     VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
    243 
    244     if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
    245         outdentParagraph();
    246         return;
    247     }
    248 
    249     Position originalSelectionEnd = endingSelection().end();
    250     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
    251     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
    252 
    253     while (endOfCurrentParagraph != endAfterSelection) {
    254         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
    255         if (endOfCurrentParagraph == endOfLastParagraph)
    256             setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM));
    257         else
    258             setEndingSelection(endOfCurrentParagraph);
    259 
    260         outdentParagraph();
    261 
    262         // outdentParagraph could move more than one paragraph if the paragraph
    263         // is in a list item. As a result, endAfterSelection and endOfNextParagraph
    264         // could refer to positions no longer in the document.
    265         if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().node()->inDocument())
    266             break;
    267 
    268         if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) {
    269             endOfCurrentParagraph = endingSelection().end();
    270             endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
    271         }
    272         endOfCurrentParagraph = endOfNextParagraph;
    273     }
    274 }
    275 
    276 void IndentOutdentCommand::doApply()
    277 {
    278     if (endingSelection().isNone())
    279         return;
    280 
    281     if (!endingSelection().rootEditableElement())
    282         return;
    283 
    284     VisiblePosition visibleEnd = endingSelection().visibleEnd();
    285     VisiblePosition visibleStart = endingSelection().visibleStart();
    286     // When a selection ends at the start of a paragraph, we rarely paint
    287     // the selection gap before that paragraph, because there often is no gap.
    288     // In a case like this, it's not obvious to the user that the selection
    289     // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
    290     // operated on that paragraph.
    291     // FIXME: We paint the gap before some paragraphs that are indented with left
    292     // margin/padding, but not others.  We should make the gap painting more consistent and
    293     // then use a left margin/padding rule here.
    294     if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
    295         setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true)));
    296 
    297     VisibleSelection selection = selectionForParagraphIteration(endingSelection());
    298     VisiblePosition startOfSelection = selection.visibleStart();
    299     VisiblePosition endOfSelection = selection.visibleEnd();
    300 
    301     int startIndex = indexForVisiblePosition(startOfSelection);
    302     int endIndex = indexForVisiblePosition(endOfSelection);
    303 
    304     ASSERT(!startOfSelection.isNull());
    305     ASSERT(!endOfSelection.isNull());
    306 
    307     if (m_typeOfAction == Indent)
    308         indentRegion(startOfSelection, endOfSelection);
    309     else
    310         outdentRegion(startOfSelection, endOfSelection);
    311 
    312     updateLayout();
    313 
    314     RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, 0, true);
    315     RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex, 0, true);
    316     if (startRange && endRange)
    317         setEndingSelection(VisibleSelection(startRange->startPosition(), endRange->startPosition(), DOWNSTREAM));
    318 }
    319 
    320 }
    321