| 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 "Editing.h" |
| 30 | #include "ElementTraversal.h" |
| 31 | #include "HTMLBRElement.h" |
| 32 | #include "HTMLLIElement.h" |
| 33 | #include "HTMLNames.h" |
| 34 | #include "HTMLUListElement.h" |
| 35 | #include "Range.h" |
| 36 | #include "VisibleUnits.h" |
| 37 | |
| 38 | namespace WebCore { |
| 39 | |
| 40 | using namespace HTMLNames; |
| 41 | |
| 42 | static 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 | |
| 50 | RefPtr<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 | |
| 57 | HTMLElement& InsertListCommand::fixOrphanedListChild(Node& node) |
| 58 | { |
| 59 | auto listElement = HTMLUListElement::create(document()); |
| 60 | insertNodeBefore(listElement.copyRef(), node); |
| 61 | removeNode(node); |
| 62 | appendNode(node, listElement.copyRef()); |
| 63 | m_listElement = WTFMove(listElement); |
| 64 | return *m_listElement; |
| 65 | } |
| 66 | |
| 67 | Ref<HTMLElement> InsertListCommand::mergeWithNeighboringLists(HTMLElement& list) |
| 68 | { |
| 69 | Ref<HTMLElement> protectedList = list; |
| 70 | Element* previousList = list.previousElementSibling(); |
| 71 | if (canMergeLists(previousList, &list)) |
| 72 | mergeIdenticalElements(*previousList, list); |
| 73 | |
| 74 | Element* sibling = ElementTraversal::nextSibling(list); |
| 75 | if (!is<HTMLElement>(sibling)) |
| 76 | return protectedList; |
| 77 | |
| 78 | Ref<HTMLElement> nextList = downcast<HTMLElement>(*sibling); |
| 79 | if (canMergeLists(&list, nextList.ptr())) { |
| 80 | mergeIdenticalElements(list, nextList); |
| 81 | return nextList; |
| 82 | } |
| 83 | return protectedList; |
| 84 | } |
| 85 | |
| 86 | bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& 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 | Element* listNode = enclosingList(start.deepEquivalent().deprecatedNode()); |
| 96 | if (!listNode || !listNode->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) |
| 106 | , m_type(type) |
| 107 | { |
| 108 | } |
| 109 | |
| 110 | void InsertListCommand::doApply() |
| 111 | { |
| 112 | if (endingSelection().isNoneOrOrphaned() || !endingSelection().isContentRichlyEditable()) |
| 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 | if (!endingSelection().rootEditableElement()) |
| 128 | return; |
| 129 | } |
| 130 | |
| 131 | auto& listTag = (m_type == Type::OrderedList) ? olTag : ulTag; |
| 132 | if (endingSelection().isRange()) { |
| 133 | VisibleSelection selection = selectionForParagraphIteration(endingSelection()); |
| 134 | ASSERT(selection.isRange()); |
| 135 | VisiblePosition startOfSelection = selection.visibleStart(); |
| 136 | VisiblePosition endOfSelection = selection.visibleEnd(); |
| 137 | VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary); |
| 138 | |
| 139 | if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary) != startOfLastParagraph) { |
| 140 | bool forceCreateList = !selectionHasListOfType(selection, listTag); |
| 141 | |
| 142 | RefPtr<Range> currentSelection = endingSelection().firstRange(); |
| 143 | VisiblePosition startOfCurrentParagraph = startOfSelection; |
| 144 | while (!inSameParagraph(startOfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) { |
| 145 | // doApply() may operate on and remove the last paragraph of the selection from the document |
| 146 | // if it's in the same list item as startOfCurrentParagraph. Return early to avoid an |
| 147 | // infinite loop and because there is no more work to be done. |
| 148 | // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here. Compute |
| 149 | // the new location of endOfSelection and use it as the end of the new selection. |
| 150 | if (!startOfLastParagraph.deepEquivalent().anchorNode()->isConnected()) |
| 151 | return; |
| 152 | setEndingSelection(startOfCurrentParagraph); |
| 153 | |
| 154 | // Save and restore endOfSelection and startOfLastParagraph when necessary |
| 155 | // since moveParagraph and movePragraphWithClones can remove nodes. |
| 156 | // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from |
| 157 | // the beginning of the document to the endOfSelection everytime this code is executed. |
| 158 | // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph. |
| 159 | RefPtr<ContainerNode> scope; |
| 160 | int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scope); |
| 161 | doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); |
| 162 | if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) { |
| 163 | endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.get()); |
| 164 | // If endOfSelection is null, then some contents have been deleted from the document. |
| 165 | // This should never happen and if it did, exit early immediately because we've lost the loop invariant. |
| 166 | ASSERT(endOfSelection.isNotNull()); |
| 167 | if (endOfSelection.isNull()) |
| 168 | return; |
| 169 | startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary); |
| 170 | } |
| 171 | |
| 172 | // Fetch the start of the selection after moving the first paragraph, |
| 173 | // because moving the paragraph will invalidate the original start. |
| 174 | // We'll use the new start to restore the original selection after |
| 175 | // we modified all selected paragraphs. |
| 176 | if (startOfCurrentParagraph == startOfSelection) |
| 177 | startOfSelection = endingSelection().visibleStart(); |
| 178 | |
| 179 | startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart()); |
| 180 | } |
| 181 | setEndingSelection(endOfSelection); |
| 182 | doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); |
| 183 | // Fetch the end of the selection, for the reason mentioned above. |
| 184 | endOfSelection = endingSelection().visibleEnd(); |
| 185 | setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional())); |
| 186 | return; |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | doApplyForSingleParagraph(false, listTag, endingSelection().firstRange().get()); |
| 191 | } |
| 192 | |
| 193 | EditAction InsertListCommand::editingAction() const |
| 194 | { |
| 195 | return m_type == Type::OrderedList ? EditAction::InsertOrderedList : EditAction::InsertUnorderedList; |
| 196 | } |
| 197 | |
| 198 | void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const HTMLQualifiedName& listTag, Range* currentSelection) |
| 199 | { |
| 200 | // FIXME: This will produce unexpected results for a selection that starts just before a |
| 201 | // table and ends inside the first cell, selectionForParagraphIteration should probably |
| 202 | // be renamed and deployed inside setEndingSelection(). |
| 203 | Node* selectionNode = endingSelection().start().deprecatedNode(); |
| 204 | Node* listChildNode = enclosingListChild(selectionNode); |
| 205 | bool switchListType = false; |
| 206 | if (listChildNode) { |
| 207 | // Remove the list chlild. |
| 208 | RefPtr<HTMLElement> listNode = enclosingList(listChildNode); |
| 209 | if (!listNode) |
| 210 | listNode = mergeWithNeighboringLists(fixOrphanedListChild(*listChildNode)); |
| 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 | |
| 217 | // If the list is of the desired type, and we are not removing the list, then exit early. |
| 218 | if (!switchListType && forceCreateList) |
| 219 | return; |
| 220 | |
| 221 | // If the entire list is selected, then convert the whole list. |
| 222 | if (switchListType && isNodeVisiblyContainedWithin(*listNode, *currentSelection)) { |
| 223 | bool rangeStartIsInList = visiblePositionBeforeNode(*listNode) == currentSelection->startPosition(); |
| 224 | bool rangeEndIsInList = visiblePositionAfterNode(*listNode) == currentSelection->endPosition(); |
| 225 | |
| 226 | RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag); |
| 227 | insertNodeBefore(*newList, *listNode); |
| 228 | |
| 229 | auto* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().deprecatedNode(), listNode.get()); |
| 230 | Node* outerBlock = firstChildInList && isBlockFlowElement(*firstChildInList) ? firstChildInList : listNode.get(); |
| 231 | |
| 232 | moveParagraphWithClones(firstPositionInNode(listNode.get()), lastPositionInNode(listNode.get()), newList.get(), outerBlock); |
| 233 | |
| 234 | // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document. |
| 235 | // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html. |
| 236 | // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection. |
| 237 | if (listNode && listNode->isConnected()) |
| 238 | removeNode(*listNode); |
| 239 | |
| 240 | newList = mergeWithNeighboringLists(*newList); |
| 241 | |
| 242 | // Restore the start and the end of current selection if they started inside listNode |
| 243 | // because moveParagraphWithClones could have removed them. |
| 244 | if (rangeStartIsInList && newList) |
| 245 | currentSelection->setStart(*newList, 0); |
| 246 | if (rangeEndIsInList && newList) |
| 247 | currentSelection->setEnd(*newList, lastOffsetInNode(newList.get())); |
| 248 | |
| 249 | setEndingSelection(VisiblePosition(firstPositionInNode(newList.get()))); |
| 250 | |
| 251 | return; |
| 252 | } |
| 253 | |
| 254 | unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode); |
| 255 | } |
| 256 | |
| 257 | if (!listChildNode || switchListType || forceCreateList) |
| 258 | m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag); |
| 259 | } |
| 260 | |
| 261 | void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode) |
| 262 | { |
| 263 | Node* nextListChild; |
| 264 | Node* previousListChild; |
| 265 | VisiblePosition start; |
| 266 | VisiblePosition end; |
| 267 | if (listChildNode->hasTagName(liTag)) { |
| 268 | start = firstPositionInNode(listChildNode); |
| 269 | end = 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 | auto placeholder = HTMLBRElement::create(document()); |
| 284 | RefPtr<Element> nodeToInsert = placeholder.copyRef(); |
| 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 = HTMLLIElement::create(document()); |
| 289 | appendNode(placeholder.copyRef(), *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.releaseNonNull(), *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.releaseNonNull(), *listNode); |
| 310 | } else |
| 311 | insertNodeAfter(nodeToInsert.releaseNonNull(), *listNode); |
| 312 | |
| 313 | VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.ptr())); |
| 314 | moveParagraphs(start, end, insertionPoint, true); |
| 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 | RefPtr<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 0; |
| 343 | |
| 344 | // Check for adjoining lists. |
| 345 | auto listItemElement = HTMLLIElement::create(document()); |
| 346 | auto placeholder = HTMLBRElement::create(document()); |
| 347 | appendNode(placeholder.copyRef(), listItemElement.copyRef()); |
| 348 | |
| 349 | // Place list item into adjoining lists. |
| 350 | Element* previousList = adjacentEnclosingList(start.deepEquivalent(), start.previous(CannotCrossEditingBoundary), listTag); |
| 351 | Element* nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(CannotCrossEditingBoundary), listTag); |
| 352 | RefPtr<HTMLElement> listElement; |
| 353 | if (previousList) |
| 354 | appendNode(WTFMove(listItemElement), *previousList); |
| 355 | else if (nextList) |
| 356 | insertNodeAt(WTFMove(listItemElement), positionBeforeNode(nextList)); |
| 357 | else { |
| 358 | // Create the list. |
| 359 | listElement = createHTMLElement(document(), listTag); |
| 360 | appendNode(WTFMove(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 | auto blockPlaceholder = insertBlockPlaceholder(start.deepEquivalent()); |
| 367 | start = positionBeforeNode(blockPlaceholder.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 (listChild && listChild->hasTagName(liTag)) |
| 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 | listElement->document().updateLayoutIgnorePendingStylesheets(); |
| 390 | start = startOfParagraph(originalStart, CanSkipOverEditingBoundary); |
| 391 | end = endOfParagraph(start, CanSkipOverEditingBoundary); |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | moveParagraph(start, end, positionBeforeNode(placeholder.ptr()), true); |
| 396 | |
| 397 | if (listElement) |
| 398 | return mergeWithNeighboringLists(*listElement); |
| 399 | |
| 400 | if (canMergeLists(previousList, nextList)) |
| 401 | mergeIdenticalElements(*previousList, *nextList); |
| 402 | |
| 403 | return listElement; |
| 404 | } |
| 405 | |
| 406 | } |
| 407 | |