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