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 "core/editing/IndentOutdentCommand.h"
     28 
     29 #include "core/HTMLNames.h"
     30 #include "core/dom/Document.h"
     31 #include "core/dom/ElementTraversal.h"
     32 #include "core/editing/InsertListCommand.h"
     33 #include "core/editing/VisibleUnits.h"
     34 #include "core/editing/htmlediting.h"
     35 #include "core/html/HTMLElement.h"
     36 #include "core/rendering/RenderObject.h"
     37 
     38 namespace WebCore {
     39 
     40 using namespace HTMLNames;
     41 
     42 static bool isListOrIndentBlockquote(const Node* node)
     43 {
     44     return node && (isHTMLUListElement(*node) || isHTMLOListElement(*node) || node->hasTagName(blockquoteTag));
     45 }
     46 
     47 IndentOutdentCommand::IndentOutdentCommand(Document& document, EIndentType typeOfAction)
     48     : ApplyBlockElementCommand(document, blockquoteTag, "margin: 0 0 0 40px; border: none; padding: 0px;")
     49     , m_typeOfAction(typeOfAction)
     50 {
     51 }
     52 
     53 bool IndentOutdentCommand::tryIndentingAsListItem(const Position& start, const Position& end)
     54 {
     55     // If our selection is not inside a list, bail out.
     56     RefPtrWillBeRawPtr<Node> lastNodeInSelectedParagraph = start.deprecatedNode();
     57     RefPtrWillBeRawPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph.get());
     58     if (!listNode)
     59         return false;
     60 
     61     // Find the block that we want to indent.  If it's not a list item (e.g., a div inside a list item), we bail out.
     62     RefPtrWillBeRawPtr<Element> selectedListItem = enclosingBlock(lastNodeInSelectedParagraph.get());
     63 
     64     // FIXME: we need to deal with the case where there is no li (malformed HTML)
     65     if (!selectedListItem || !isHTMLLIElement(*selectedListItem))
     66         return false;
     67 
     68     // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>.  Should we?
     69     RefPtrWillBeRawPtr<Element> previousList = ElementTraversal::previousSibling(*selectedListItem);
     70     RefPtrWillBeRawPtr<Element> nextList = ElementTraversal::nextSibling(*selectedListItem);
     71 
     72     // We should calculate visible range in list item because inserting new
     73     // list element will change visibility of list item, e.g. :first-child
     74     // CSS selector.
     75     RefPtrWillBeRawPtr<Element> newList = document().createElement(listNode->tagQName(), false);
     76     insertNodeBefore(newList, selectedListItem.get());
     77 
     78     // We should clone all the children of the list item for indenting purposes. However, in case the current
     79     // selection does not encompass all its children, we need to explicitally handle the same. The original
     80     // list item too would require proper deletion in that case.
     81     if (end.anchorNode() == selectedListItem.get() || end.anchorNode()->isDescendantOf(selectedListItem->lastChild())) {
     82         moveParagraphWithClones(VisiblePosition(start), VisiblePosition(end), newList.get(), selectedListItem.get());
     83     } else {
     84         moveParagraphWithClones(VisiblePosition(start), VisiblePosition(positionAfterNode(selectedListItem->lastChild())), newList.get(), selectedListItem.get());
     85         removeNode(selectedListItem.get());
     86     }
     87 
     88     if (canMergeLists(previousList.get(), newList.get()))
     89         mergeIdenticalElements(previousList.get(), newList.get());
     90     if (canMergeLists(newList.get(), nextList.get()))
     91         mergeIdenticalElements(newList.get(), nextList.get());
     92 
     93     return true;
     94 }
     95 
     96 void IndentOutdentCommand::indentIntoBlockquote(const Position& start, const Position& end, RefPtrWillBeRawPtr<Element>& targetBlockquote)
     97 {
     98     Node* enclosingCell = enclosingNodeOfType(start, &isTableCell);
     99     Node* nodeToSplitTo;
    100     if (enclosingCell)
    101         nodeToSplitTo = enclosingCell;
    102     else if (enclosingList(start.containerNode()))
    103         nodeToSplitTo = enclosingBlock(start.containerNode());
    104     else
    105         nodeToSplitTo = editableRootForPosition(start);
    106 
    107     if (!nodeToSplitTo)
    108         return;
    109 
    110     RefPtrWillBeRawPtr<Node> outerBlock = (start.containerNode() == nodeToSplitTo) ? start.containerNode() : splitTreeToNode(start.containerNode(), nodeToSplitTo).get();
    111 
    112     VisiblePosition startOfContents(start);
    113     if (!targetBlockquote) {
    114         // Create a new blockquote and insert it as a child of the root editable element. We accomplish
    115         // this by splitting all parents of the current paragraph up to that point.
    116         targetBlockquote = createBlockElement();
    117         if (outerBlock == start.containerNode())
    118             insertNodeAt(targetBlockquote, start);
    119         else
    120             insertNodeBefore(targetBlockquote, outerBlock);
    121         startOfContents = VisiblePosition(positionInParentAfterNode(*targetBlockquote));
    122     }
    123 
    124     moveParagraphWithClones(startOfContents, VisiblePosition(end), targetBlockquote.get(), outerBlock.get());
    125 }
    126 
    127 void IndentOutdentCommand::outdentParagraph()
    128 {
    129     VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
    130     VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
    131 
    132     Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote);
    133     if (!enclosingNode || !enclosingNode->parentNode()->rendererIsEditable()) // We can't outdent if there is no place to go!
    134         return;
    135 
    136     // Use InsertListCommand to remove the selection from the list
    137     if (isHTMLOListElement(*enclosingNode)) {
    138         applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList));
    139         return;
    140     }
    141     if (isHTMLUListElement(*enclosingNode)) {
    142         applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList));
    143         return;
    144     }
    145 
    146     // The selection is inside a blockquote i.e. enclosingNode is a blockquote
    147     VisiblePosition positionInEnclosingBlock = VisiblePosition(firstPositionInNode(enclosingNode));
    148     // If the blockquote is inline, the start of the enclosing block coincides with
    149     // positionInEnclosingBlock.
    150     VisiblePosition startOfEnclosingBlock = (enclosingNode->renderer() && enclosingNode->renderer()->isInline()) ? positionInEnclosingBlock : startOfBlock(positionInEnclosingBlock);
    151     VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(lastPositionInNode(enclosingNode));
    152     VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock);
    153     if (visibleStartOfParagraph == startOfEnclosingBlock &&
    154         visibleEndOfParagraph == endOfEnclosingBlock) {
    155         // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
    156         Node* splitPoint = enclosingNode->nextSibling();
    157         removeNodePreservingChildren(enclosingNode);
    158         // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've
    159         // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true
    160         if (splitPoint) {
    161             if (ContainerNode* splitPointParent = splitPoint->parentNode()) {
    162                 if (splitPointParent->hasTagName(blockquoteTag)
    163                     && !splitPoint->hasTagName(blockquoteTag)
    164                     && splitPointParent->parentNode()->rendererIsEditable()) // We can't outdent if there is no place to go!
    165                     splitElement(toElement(splitPointParent), splitPoint);
    166             }
    167         }
    168 
    169         document().updateLayoutIgnorePendingStylesheets();
    170         visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
    171         visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
    172         if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
    173             insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent());
    174         if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
    175             insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent());
    176 
    177         return;
    178     }
    179     RefPtrWillBeRawPtr<Node> splitBlockquoteNode = enclosingNode;
    180     if (Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().deprecatedNode())) {
    181         if (enclosingBlockFlow != enclosingNode) {
    182             splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true);
    183         } else {
    184             // We split the blockquote at where we start outdenting.
    185             Node* highestInlineNode = highestEnclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), isInline, CannotCrossEditingBoundary, enclosingBlockFlow);
    186             splitElement(toElement(enclosingNode), (highestInlineNode) ? highestInlineNode : visibleStartOfParagraph.deepEquivalent().deprecatedNode());
    187         }
    188     }
    189     VisiblePosition startOfParagraphToMove(startOfParagraph(visibleStartOfParagraph));
    190     VisiblePosition endOfParagraphToMove(endOfParagraph(visibleEndOfParagraph));
    191     if (startOfParagraphToMove.isNull() || endOfParagraphToMove.isNull())
    192         return;
    193     RefPtrWillBeRawPtr<Node> placeholder = createBreakElement(document());
    194     insertNodeBefore(placeholder, splitBlockquoteNode);
    195     moveParagraph(startOfParagraphToMove, endOfParagraphToMove, VisiblePosition(positionBeforeNode(placeholder.get())), true);
    196 }
    197 
    198 // FIXME: We should merge this function with ApplyBlockElementCommand::formatSelection
    199 void IndentOutdentCommand::outdentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
    200 {
    201     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
    202     VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
    203 
    204     if (endOfCurrentParagraph == endOfLastParagraph) {
    205         outdentParagraph();
    206         return;
    207     }
    208 
    209     Position originalSelectionEnd = endingSelection().end();
    210     VisiblePosition endAfterSelection = endOfParagraph(endOfLastParagraph.next());
    211 
    212     while (endOfCurrentParagraph != endAfterSelection) {
    213         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
    214         if (endOfCurrentParagraph == endOfLastParagraph)
    215             setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM));
    216         else
    217             setEndingSelection(endOfCurrentParagraph);
    218 
    219         outdentParagraph();
    220 
    221         // outdentParagraph could move more than one paragraph if the paragraph
    222         // is in a list item. As a result, endAfterSelection and endOfNextParagraph
    223         // could refer to positions no longer in the document.
    224         if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().inDocument())
    225             break;
    226 
    227         if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().inDocument()) {
    228             endOfCurrentParagraph = VisiblePosition(endingSelection().end());
    229             endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
    230         }
    231         endOfCurrentParagraph = endOfNextParagraph;
    232     }
    233 }
    234 
    235 void IndentOutdentCommand::formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
    236 {
    237     if (m_typeOfAction == Indent)
    238         ApplyBlockElementCommand::formatSelection(startOfSelection, endOfSelection);
    239     else
    240         outdentRegion(startOfSelection, endOfSelection);
    241 }
    242 
    243 void IndentOutdentCommand::formatRange(const Position& start, const Position& end, const Position&, RefPtrWillBeRawPtr<Element>& blockquoteForNextIndent)
    244 {
    245     if (tryIndentingAsListItem(start, end))
    246         blockquoteForNextIndent = nullptr;
    247     else
    248         indentIntoBlockquote(start, end, blockquoteForNextIndent);
    249 }
    250 
    251 }
    252