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