1 /* 2 * Copyright (C) 2006, 2010 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 (INCLUDING, 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/InsertListCommand.h" 28 29 #include "HTMLNames.h" 30 #include "bindings/v8/ExceptionStatePlaceholder.h" 31 #include "core/dom/Element.h" 32 #include "core/editing/TextIterator.h" 33 #include "core/editing/VisibleUnits.h" 34 #include "core/editing/htmlediting.h" 35 #include "core/html/HTMLElement.h" 36 37 namespace WebCore { 38 39 using namespace HTMLNames; 40 41 static Node* enclosingListChild(Node* node, Node* listNode) 42 { 43 Node* listChild = enclosingListChild(node); 44 while (listChild && enclosingList(listChild) != listNode) 45 listChild = enclosingListChild(listChild->parentNode()); 46 return listChild; 47 } 48 49 PassRefPtr<HTMLElement> InsertListCommand::insertList(Document* document, Type type) 50 { 51 RefPtr<InsertListCommand> insertCommand = create(document, type); 52 insertCommand->apply(); 53 return insertCommand->m_listElement; 54 } 55 56 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node) 57 { 58 RefPtr<HTMLElement> listElement = createUnorderedListElement(document()); 59 insertNodeBefore(listElement, node); 60 removeNode(node); 61 appendNode(node, listElement); 62 m_listElement = listElement; 63 return listElement.get(); 64 } 65 66 PassRefPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtr<HTMLElement> passedList) 67 { 68 RefPtr<HTMLElement> list = passedList; 69 Element* previousList = list->previousElementSibling(); 70 if (canMergeLists(previousList, list.get())) 71 mergeIdenticalElements(previousList, list); 72 73 if (!list || !list->nextElementSibling() || !list->nextElementSibling()->isHTMLElement()) 74 return list.release(); 75 76 RefPtr<HTMLElement> nextList = toHTMLElement(list->nextElementSibling()); 77 if (canMergeLists(list.get(), nextList.get())) { 78 mergeIdenticalElements(list, nextList); 79 return nextList.release(); 80 } 81 return list.release(); 82 } 83 84 bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag) 85 { 86 VisiblePosition start = selection.visibleStart(); 87 88 if (!enclosingList(start.deepEquivalent().deprecatedNode())) 89 return false; 90 91 VisiblePosition end = startOfParagraph(selection.visibleEnd()); 92 while (start.isNotNull() && start != end) { 93 Element* listNode = enclosingList(start.deepEquivalent().deprecatedNode()); 94 if (!listNode || !listNode->hasTagName(listTag)) 95 return false; 96 start = startOfNextParagraph(start); 97 } 98 99 return true; 100 } 101 102 InsertListCommand::InsertListCommand(Document* document, Type type) 103 : CompositeEditCommand(document), m_type(type) 104 { 105 } 106 107 void InsertListCommand::doApply() 108 { 109 if (!endingSelection().isNonOrphanedCaretOrRange()) 110 return; 111 112 if (!endingSelection().rootEditableElement()) 113 return; 114 115 VisiblePosition visibleEnd = endingSelection().visibleEnd(); 116 VisiblePosition visibleStart = endingSelection().visibleStart(); 117 // When a selection ends at the start of a paragraph, we rarely paint 118 // the selection gap before that paragraph, because there often is no gap. 119 // In a case like this, it's not obvious to the user that the selection 120 // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List 121 // operated on that paragraph. 122 // FIXME: We paint the gap before some paragraphs that are indented with left 123 // margin/padding, but not others. We should make the gap painting more consistent and 124 // then use a left margin/padding rule here. 125 if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd, CanSkipOverEditingBoundary)) 126 setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional())); 127 128 const QualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag; 129 if (endingSelection().isRange()) { 130 VisibleSelection selection = selectionForParagraphIteration(endingSelection()); 131 ASSERT(selection.isRange()); 132 VisiblePosition startOfSelection = selection.visibleStart(); 133 VisiblePosition endOfSelection = selection.visibleEnd(); 134 VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary); 135 136 if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary) != startOfLastParagraph) { 137 bool forceCreateList = !selectionHasListOfType(selection, listTag); 138 139 RefPtr<Range> currentSelection = endingSelection().firstRange(); 140 VisiblePosition startOfCurrentParagraph = startOfSelection; 141 while (!inSameParagraph(startOfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) { 142 // doApply() may operate on and remove the last paragraph of the selection from the document 143 // if it's in the same list item as startOfCurrentParagraph. Return early to avoid an 144 // infinite loop and because there is no more work to be done. 145 // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here. Compute 146 // the new location of endOfSelection and use it as the end of the new selection. 147 if (!startOfLastParagraph.deepEquivalent().anchorNode()->inDocument()) 148 return; 149 setEndingSelection(startOfCurrentParagraph); 150 151 // Save and restore endOfSelection and startOfLastParagraph when necessary 152 // since moveParagraph and movePragraphWithClones can remove nodes. 153 // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from 154 // the beginning of the document to the endOfSelection everytime this code is executed. 155 // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph. 156 RefPtr<ContainerNode> scope; 157 int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scope); 158 doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); 159 if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) { 160 endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.get()); 161 // If endOfSelection is null, then some contents have been deleted from the document. 162 // This should never happen and if it did, exit early immediately because we've lost the loop invariant. 163 ASSERT(endOfSelection.isNotNull()); 164 if (endOfSelection.isNull()) 165 return; 166 startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary); 167 } 168 169 // Fetch the start of the selection after moving the first paragraph, 170 // because moving the paragraph will invalidate the original start. 171 // We'll use the new start to restore the original selection after 172 // we modified all selected paragraphs. 173 if (startOfCurrentParagraph == startOfSelection) 174 startOfSelection = endingSelection().visibleStart(); 175 176 startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart()); 177 } 178 setEndingSelection(endOfSelection); 179 doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); 180 // Fetch the end of the selection, for the reason mentioned above. 181 endOfSelection = endingSelection().visibleEnd(); 182 setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional())); 183 return; 184 } 185 } 186 187 doApplyForSingleParagraph(false, listTag, endingSelection().firstRange().get()); 188 } 189 190 void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const QualifiedName& listTag, Range* currentSelection) 191 { 192 // FIXME: This will produce unexpected results for a selection that starts just before a 193 // table and ends inside the first cell, selectionForParagraphIteration should probably 194 // be renamed and deployed inside setEndingSelection(). 195 Node* selectionNode = endingSelection().start().deprecatedNode(); 196 Node* listChildNode = enclosingListChild(selectionNode); 197 bool switchListType = false; 198 if (listChildNode) { 199 // Remove the list chlild. 200 RefPtr<HTMLElement> listNode = enclosingList(listChildNode); 201 if (!listNode) { 202 listNode = fixOrphanedListChild(listChildNode); 203 listNode = mergeWithNeighboringLists(listNode); 204 } 205 if (!listNode->hasTagName(listTag)) 206 // listChildNode will be removed from the list and a list of type m_type will be created. 207 switchListType = true; 208 209 // If the list is of the desired type, and we are not removing the list, then exit early. 210 if (!switchListType && forceCreateList) 211 return; 212 213 // If the entire list is selected, then convert the whole list. 214 if (switchListType && isNodeVisiblyContainedWithin(listNode.get(), currentSelection)) { 215 bool rangeStartIsInList = visiblePositionBeforeNode(listNode.get()) == currentSelection->startPosition(); 216 bool rangeEndIsInList = visiblePositionAfterNode(listNode.get()) == currentSelection->endPosition(); 217 218 RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag); 219 insertNodeBefore(newList, listNode); 220 221 Node* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().deprecatedNode(), listNode.get()); 222 Node* outerBlock = firstChildInList->isBlockFlowElement() ? firstChildInList : listNode.get(); 223 224 moveParagraphWithClones(firstPositionInNode(listNode.get()), lastPositionInNode(listNode.get()), newList.get(), outerBlock); 225 226 // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document. 227 // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html. 228 // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection. 229 if (listNode && listNode->inDocument()) 230 removeNode(listNode); 231 232 newList = mergeWithNeighboringLists(newList); 233 234 // Restore the start and the end of current selection if they started inside listNode 235 // because moveParagraphWithClones could have removed them. 236 if (rangeStartIsInList && newList) 237 currentSelection->setStart(newList, 0, IGNORE_EXCEPTION); 238 if (rangeEndIsInList && newList) 239 currentSelection->setEnd(newList, lastOffsetInNode(newList.get()), IGNORE_EXCEPTION); 240 241 setEndingSelection(VisiblePosition(firstPositionInNode(newList.get()))); 242 243 return; 244 } 245 246 unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode); 247 } 248 249 if (!listChildNode || switchListType || forceCreateList) 250 m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag); 251 } 252 253 void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode) 254 { 255 Node* nextListChild; 256 Node* previousListChild; 257 VisiblePosition start; 258 VisiblePosition end; 259 if (listChildNode->hasTagName(liTag)) { 260 start = firstPositionInNode(listChildNode); 261 end = lastPositionInNode(listChildNode); 262 nextListChild = listChildNode->nextSibling(); 263 previousListChild = listChildNode->previousSibling(); 264 } else { 265 // A paragraph is visually a list item minus a list marker. The paragraph will be moved. 266 start = startOfParagraph(originalStart, CanSkipOverEditingBoundary); 267 end = endOfParagraph(start, CanSkipOverEditingBoundary); 268 nextListChild = enclosingListChild(end.next().deepEquivalent().deprecatedNode(), listNode); 269 ASSERT(nextListChild != listChildNode); 270 previousListChild = enclosingListChild(start.previous().deepEquivalent().deprecatedNode(), listNode); 271 ASSERT(previousListChild != listChildNode); 272 } 273 // When removing a list, we must always create a placeholder to act as a point of insertion 274 // for the list content being removed. 275 RefPtr<Element> placeholder = createBreakElement(document()); 276 RefPtr<Element> nodeToInsert = placeholder; 277 // If the content of the list item will be moved into another list, put it in a list item 278 // so that we don't create an orphaned list child. 279 if (enclosingList(listNode)) { 280 nodeToInsert = createListItemElement(document()); 281 appendNode(placeholder, nodeToInsert); 282 } 283 284 if (nextListChild && previousListChild) { 285 // We want to pull listChildNode out of listNode, and place it before nextListChild 286 // and after previousListChild, so we split listNode and insert it between the two lists. 287 // But to split listNode, we must first split ancestors of listChildNode between it and listNode, 288 // if any exist. 289 // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove 290 // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is 291 // unrendered. But we ought to remove nextListChild too, if it is unrendered. 292 splitElement(listNode, splitTreeToNode(nextListChild, listNode)); 293 insertNodeBefore(nodeToInsert, listNode); 294 } else if (nextListChild || listChildNode->parentNode() != listNode) { 295 // Just because listChildNode has no previousListChild doesn't mean there isn't any content 296 // in listNode that comes before listChildNode, as listChildNode could have ancestors 297 // between it and listNode. So, we split up to listNode before inserting the placeholder 298 // where we're about to move listChildNode to. 299 if (listChildNode->parentNode() != listNode) 300 splitElement(listNode, splitTreeToNode(listChildNode, listNode).get()); 301 insertNodeBefore(nodeToInsert, listNode); 302 } else 303 insertNodeAfter(nodeToInsert, listNode); 304 305 VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.get())); 306 moveParagraphs(start, end, insertionPoint, /* preserveSelection */ true, /* preserveStyle */ true, listChildNode); 307 } 308 309 static Element* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag) 310 { 311 Element* listNode = outermostEnclosingList(adjacentPos.deepEquivalent().deprecatedNode()); 312 313 if (!listNode) 314 return 0; 315 316 Node* previousCell = enclosingTableCell(pos.deepEquivalent()); 317 Node* currentCell = enclosingTableCell(adjacentPos.deepEquivalent()); 318 319 if (!listNode->hasTagName(listTag) 320 || listNode->contains(pos.deepEquivalent().deprecatedNode()) 321 || previousCell != currentCell 322 || enclosingList(listNode) != enclosingList(pos.deepEquivalent().deprecatedNode())) 323 return 0; 324 325 return listNode; 326 } 327 328 PassRefPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag) 329 { 330 VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBoundary); 331 VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary); 332 333 if (start.isNull() || end.isNull()) 334 return 0; 335 336 // Check for adjoining lists. 337 RefPtr<HTMLElement> listItemElement = createListItemElement(document()); 338 RefPtr<HTMLElement> placeholder = createBreakElement(document()); 339 appendNode(placeholder, listItemElement); 340 341 // Place list item into adjoining lists. 342 Element* previousList = adjacentEnclosingList(start.deepEquivalent(), start.previous(CannotCrossEditingBoundary), listTag); 343 Element* nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(CannotCrossEditingBoundary), listTag); 344 RefPtr<HTMLElement> listElement; 345 if (previousList) 346 appendNode(listItemElement, previousList); 347 else if (nextList) 348 insertNodeAt(listItemElement, positionBeforeNode(nextList)); 349 else { 350 // Create the list. 351 listElement = createHTMLElement(document(), listTag); 352 appendNode(listItemElement, listElement); 353 354 if (start == end && isBlock(start.deepEquivalent().deprecatedNode())) { 355 // Inserting the list into an empty paragraph that isn't held open 356 // by a br or a '\n', will invalidate start and end. Insert 357 // a placeholder and then recompute start and end. 358 RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent()); 359 start = positionBeforeNode(placeholder.get()); 360 end = start; 361 } 362 363 // Insert the list at a position visually equivalent to start of the 364 // paragraph that is being moved into the list. 365 // Try to avoid inserting it somewhere where it will be surrounded by 366 // inline ancestors of start, since it is easier for editing to produce 367 // clean markup when inline elements are pushed down as far as possible. 368 Position insertionPos(start.deepEquivalent().upstream()); 369 // Also avoid the containing list item. 370 Node* listChild = enclosingListChild(insertionPos.deprecatedNode()); 371 if (listChild && listChild->hasTagName(liTag)) 372 insertionPos = positionInParentBeforeNode(listChild); 373 374 insertNodeAt(listElement, insertionPos); 375 376 // We inserted the list at the start of the content we're about to move 377 // Update the start of content, so we don't try to move the list into itself. bug 19066 378 // Layout is necessary since start's node's inline renderers may have been destroyed by the insertion 379 // The end of the content may have changed after the insertion and layout so update it as well. 380 if (insertionPos == start.deepEquivalent()) { 381 listElement->document()->updateLayoutIgnorePendingStylesheets(); 382 start = startOfParagraph(originalStart, CanSkipOverEditingBoundary); 383 end = endOfParagraph(start, CanSkipOverEditingBoundary); 384 } 385 } 386 387 moveParagraph(start, end, positionBeforeNode(placeholder.get()), true); 388 389 if (listElement) 390 return mergeWithNeighboringLists(listElement); 391 392 if (canMergeLists(previousList, nextList)) 393 mergeIdenticalElements(previousList, nextList); 394 395 return listElement; 396 } 397 398 } 399