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