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