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