2 * Copyright (C) 2006, 2010 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
27 #include "core/editing/InsertListCommand.h"
29 #include "HTMLNames.h"
30 #include "bindings/v8/ExceptionStatePlaceholder.h"
31 #include "core/dom/Element.h"
32 #include "core/dom/ElementTraversal.h"
33 #include "core/editing/TextIterator.h"
34 #include "core/editing/VisibleUnits.h"
35 #include "core/editing/htmlediting.h"
36 #include "core/html/HTMLElement.h"
40 using namespace HTMLNames;
42 static Node* enclosingListChild(Node* node, Node* listNode)
44 Node* listChild = enclosingListChild(node);
45 while (listChild && enclosingList(listChild) != listNode)
46 listChild = enclosingListChild(listChild->parentNode());
50 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node)
52 RefPtr<HTMLElement> listElement = createUnorderedListElement(document());
53 insertNodeBefore(listElement, node);
55 appendNode(node, listElement);
56 m_listElement = listElement;
57 return listElement.get();
60 PassRefPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtr<HTMLElement> passedList)
62 RefPtr<HTMLElement> list = passedList;
63 Element* previousList = ElementTraversal::previousSibling(*list);
64 if (canMergeLists(previousList, list.get()))
65 mergeIdenticalElements(previousList, list);
70 Element* nextSibling = ElementTraversal::nextSibling(*list);
71 if (!nextSibling || !nextSibling->isHTMLElement())
72 return list.release();
74 RefPtr<HTMLElement> nextList = toHTMLElement(nextSibling);
75 if (canMergeLists(list.get(), nextList.get())) {
76 mergeIdenticalElements(list, nextList);
77 return nextList.release();
79 return list.release();
82 bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag)
84 VisiblePosition start = selection.visibleStart();
86 if (!enclosingList(start.deepEquivalent().deprecatedNode()))
89 VisiblePosition end = startOfParagraph(selection.visibleEnd());
90 while (start.isNotNull() && start != end) {
91 Element* listNode = enclosingList(start.deepEquivalent().deprecatedNode());
92 if (!listNode || !listNode->hasTagName(listTag))
94 start = startOfNextParagraph(start);
100 InsertListCommand::InsertListCommand(Document& document, Type type)
101 : CompositeEditCommand(document), m_type(type)
105 void InsertListCommand::doApply()
107 if (!endingSelection().isNonOrphanedCaretOrRange())
110 if (!endingSelection().rootEditableElement())
113 VisiblePosition visibleEnd = endingSelection().visibleEnd();
114 VisiblePosition visibleStart = endingSelection().visibleStart();
115 // When a selection ends at the start of a paragraph, we rarely paint
116 // the selection gap before that paragraph, because there often is no gap.
117 // In a case like this, it's not obvious to the user that the selection
118 // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List
119 // operated on that paragraph.
120 // FIXME: We paint the gap before some paragraphs that are indented with left
121 // margin/padding, but not others. We should make the gap painting more consistent and
122 // then use a left margin/padding rule here.
123 if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd, CanSkipOverEditingBoundary)) {
124 setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional()));
125 if (!endingSelection().rootEditableElement())
129 const QualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag;
130 if (endingSelection().isRange()) {
131 VisibleSelection selection = selectionForParagraphIteration(endingSelection());
132 ASSERT(selection.isRange());
133 VisiblePosition startOfSelection = selection.visibleStart();
134 VisiblePosition endOfSelection = selection.visibleEnd();
135 VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
137 if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary) != startOfLastParagraph) {
138 RefPtr<ContainerNode> scope;
139 int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scope);
140 bool forceCreateList = !selectionHasListOfType(selection, listTag);
142 RefPtrWillBeRawPtr<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().inDocument())
152 setEndingSelection(startOfCurrentParagraph);
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 doApplyForSingleParagraph(forceCreateList, listTag, *currentSelection);
160 if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) {
161 endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.get());
162 // If endOfSelection is null, then some contents have been deleted from the document.
163 // This should never happen and if it did, exit early immediately because we've lost the loop invariant.
164 ASSERT(endOfSelection.isNotNull());
165 if (endOfSelection.isNull())
167 startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
170 // Fetch the start of the selection after moving the first paragraph,
171 // because moving the paragraph will invalidate the original start.
172 // We'll use the new start to restore the original selection after
173 // we modified all selected paragraphs.
174 if (startOfCurrentParagraph == startOfSelection)
175 startOfSelection = endingSelection().visibleStart();
177 startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
179 setEndingSelection(endOfSelection);
180 doApplyForSingleParagraph(forceCreateList, listTag, *currentSelection);
181 // Fetch the end of the selection, for the reason mentioned above.
182 if (endOfSelection.isNull() || endOfSelection.isOrphan()) {
183 endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.get());
184 if (endOfSelection.isNull())
187 setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional()));
192 ASSERT(endingSelection().firstRange());
193 doApplyForSingleParagraph(false, listTag, *endingSelection().firstRange());
196 void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const QualifiedName& listTag, Range& currentSelection)
198 // FIXME: This will produce unexpected results for a selection that starts just before a
199 // table and ends inside the first cell, selectionForParagraphIteration should probably
200 // be renamed and deployed inside setEndingSelection().
201 Node* selectionNode = endingSelection().start().deprecatedNode();
202 Node* listChildNode = enclosingListChild(selectionNode);
203 bool switchListType = false;
205 // Remove the list chlild.
206 RefPtr<HTMLElement> listNode = enclosingList(listChildNode);
208 listNode = fixOrphanedListChild(listChildNode);
209 listNode = mergeWithNeighboringLists(listNode);
211 if (!listNode->hasTagName(listTag))
212 // listChildNode will be removed from the list and a list of type m_type will be created.
213 switchListType = true;
215 // If the list is of the desired type, and we are not removing the list, then exit early.
216 if (!switchListType && forceCreateList)
219 // If the entire list is selected, then convert the whole list.
220 if (switchListType && isNodeVisiblyContainedWithin(*listNode, currentSelection)) {
221 bool rangeStartIsInList = visiblePositionBeforeNode(*listNode) == VisiblePosition(currentSelection.startPosition());
222 bool rangeEndIsInList = visiblePositionAfterNode(*listNode) == VisiblePosition(currentSelection.endPosition());
224 RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag);
225 insertNodeBefore(newList, listNode);
227 Node* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().deprecatedNode(), listNode.get());
228 Node* outerBlock = firstChildInList && firstChildInList->isBlockFlowElement() ? firstChildInList : listNode.get();
230 moveParagraphWithClones(VisiblePosition(firstPositionInNode(listNode.get())), VisiblePosition(lastPositionInNode(listNode.get())), newList.get(), outerBlock);
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);
238 newList = mergeWithNeighboringLists(newList);
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);
247 setEndingSelection(VisiblePosition(firstPositionInNode(newList.get())));
252 unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode);
255 if (!listChildNode || switchListType || forceCreateList)
256 m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag);
259 void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode)
262 Node* previousListChild;
263 VisiblePosition start;
265 ASSERT(listChildNode);
266 if (isHTMLLIElement(*listChildNode)) {
267 start = VisiblePosition(firstPositionInNode(listChildNode));
268 end = VisiblePosition(lastPositionInNode(listChildNode));
269 nextListChild = listChildNode->nextSibling();
270 previousListChild = listChildNode->previousSibling();
272 // A paragraph is visually a list item minus a list marker. The paragraph will be moved.
273 start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
274 end = endOfParagraph(start, CanSkipOverEditingBoundary);
275 nextListChild = enclosingListChild(end.next().deepEquivalent().deprecatedNode(), listNode);
276 ASSERT(nextListChild != listChildNode);
277 previousListChild = enclosingListChild(start.previous().deepEquivalent().deprecatedNode(), listNode);
278 ASSERT(previousListChild != listChildNode);
280 // When removing a list, we must always create a placeholder to act as a point of insertion
281 // for the list content being removed.
282 RefPtr<Element> placeholder = createBreakElement(document());
283 RefPtr<Element> nodeToInsert = placeholder;
284 // If the content of the list item will be moved into another list, put it in a list item
285 // so that we don't create an orphaned list child.
286 if (enclosingList(listNode)) {
287 nodeToInsert = createListItemElement(document());
288 appendNode(placeholder, nodeToInsert);
291 if (nextListChild && previousListChild) {
292 // We want to pull listChildNode out of listNode, and place it before nextListChild
293 // and after previousListChild, so we split listNode and insert it between the two lists.
294 // But to split listNode, we must first split ancestors of listChildNode between it and listNode,
296 // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove
297 // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is
298 // unrendered. But we ought to remove nextListChild too, if it is unrendered.
299 splitElement(listNode, splitTreeToNode(nextListChild, listNode));
300 insertNodeBefore(nodeToInsert, listNode);
301 } else if (nextListChild || listChildNode->parentNode() != listNode) {
302 // Just because listChildNode has no previousListChild doesn't mean there isn't any content
303 // in listNode that comes before listChildNode, as listChildNode could have ancestors
304 // between it and listNode. So, we split up to listNode before inserting the placeholder
305 // where we're about to move listChildNode to.
306 if (listChildNode->parentNode() != listNode)
307 splitElement(listNode, splitTreeToNode(listChildNode, listNode).get());
308 insertNodeBefore(nodeToInsert, listNode);
310 insertNodeAfter(nodeToInsert, listNode);
312 VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.get()));
313 moveParagraphs(start, end, insertionPoint, /* preserveSelection */ true, /* preserveStyle */ true, listChildNode);
316 static Element* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag)
318 Element* listNode = outermostEnclosingList(adjacentPos.deepEquivalent().deprecatedNode());
323 Node* previousCell = enclosingTableCell(pos.deepEquivalent());
324 Node* currentCell = enclosingTableCell(adjacentPos.deepEquivalent());
326 if (!listNode->hasTagName(listTag)
327 || listNode->contains(pos.deepEquivalent().deprecatedNode())
328 || previousCell != currentCell
329 || enclosingList(listNode) != enclosingList(pos.deepEquivalent().deprecatedNode()))
335 PassRefPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag)
337 VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
338 VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary);
340 if (start.isNull() || end.isNull())
343 // Check for adjoining lists.
344 RefPtr<HTMLElement> listItemElement = createListItemElement(document());
345 RefPtr<HTMLElement> placeholder = createBreakElement(document());
346 appendNode(placeholder, listItemElement);
348 // Place list item into adjoining lists.
349 Element* previousList = adjacentEnclosingList(start, start.previous(CannotCrossEditingBoundary), listTag);
350 Element* nextList = adjacentEnclosingList(start, end.next(CannotCrossEditingBoundary), listTag);
351 RefPtr<HTMLElement> listElement;
353 appendNode(listItemElement, previousList);
355 insertNodeAt(listItemElement, positionBeforeNode(nextList));
358 listElement = createHTMLElement(document(), listTag);
359 appendNode(listItemElement, listElement);
361 if (start == end && isBlock(start.deepEquivalent().deprecatedNode())) {
362 // Inserting the list into an empty paragraph that isn't held open
363 // by a br or a '\n', will invalidate start and end. Insert
364 // a placeholder and then recompute start and end.
365 RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent());
366 start = VisiblePosition(positionBeforeNode(placeholder.get()));
370 // Insert the list at a position visually equivalent to start of the
371 // paragraph that is being moved into the list.
372 // Try to avoid inserting it somewhere where it will be surrounded by
373 // inline ancestors of start, since it is easier for editing to produce
374 // clean markup when inline elements are pushed down as far as possible.
375 Position insertionPos(start.deepEquivalent().upstream());
376 // Also avoid the containing list item.
377 Node* listChild = enclosingListChild(insertionPos.deprecatedNode());
378 if (isHTMLLIElement(listChild))
379 insertionPos = positionInParentBeforeNode(*listChild);
381 insertNodeAt(listElement, insertionPos);
383 // We inserted the list at the start of the content we're about to move
384 // Update the start of content, so we don't try to move the list into itself. bug 19066
385 // Layout is necessary since start's node's inline renderers may have been destroyed by the insertion
386 // The end of the content may have changed after the insertion and layout so update it as well.
387 if (insertionPos == start.deepEquivalent()) {
388 listElement->document().updateLayoutIgnorePendingStylesheets();
389 start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
390 end = endOfParagraph(start, CanSkipOverEditingBoundary);
394 moveParagraph(start, end, VisiblePosition(positionBeforeNode(placeholder.get())), true);
397 return mergeWithNeighboringLists(listElement);
399 if (canMergeLists(previousList, nextList))
400 mergeIdenticalElements(previousList, nextList);