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