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 | |