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