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