1 /*
2 * Copyright (C) 2006 Apple Computer, 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
insertList(Document * document,Type type)40 PassRefPtr<HTMLElement> InsertListCommand::insertList(Document* document, Type type)
41 {
42 RefPtr<InsertListCommand> insertCommand = new InsertListCommand(document, type);
43 insertCommand->apply();
44 return insertCommand->m_listElement;
45 }
46
fixOrphanedListChild(Node * node)47 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node)
48 {
49 RefPtr<HTMLElement> listElement = createUnorderedListElement(document());
50 insertNodeBefore(listElement, node);
51 removeNode(node);
52 appendNode(node, listElement);
53 m_listElement = listElement;
54 return listElement.get();
55 }
56
InsertListCommand(Document * document,Type type)57 InsertListCommand::InsertListCommand(Document* document, Type type)
58 : CompositeEditCommand(document), m_type(type), m_forceCreateList(false)
59 {
60 }
61
modifyRange()62 bool InsertListCommand::modifyRange()
63 {
64 VisibleSelection selection = selectionForParagraphIteration(endingSelection());
65 ASSERT(selection.isRange());
66 VisiblePosition startOfSelection = selection.visibleStart();
67 VisiblePosition endOfSelection = selection.visibleEnd();
68 VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection);
69
70 if (startOfParagraph(startOfSelection) == startOfLastParagraph)
71 return false;
72
73 Node* startList = enclosingList(startOfSelection.deepEquivalent().node());
74 Node* endList = enclosingList(endOfSelection.deepEquivalent().node());
75 if (!startList || startList != endList)
76 m_forceCreateList = true;
77
78 setEndingSelection(startOfSelection);
79 doApply();
80 // Fetch the start of the selection after moving the first paragraph,
81 // because moving the paragraph will invalidate the original start.
82 // We'll use the new start to restore the original selection after
83 // we modified all selected paragraphs.
84 startOfSelection = endingSelection().visibleStart();
85 VisiblePosition startOfCurrentParagraph = startOfNextParagraph(startOfSelection);
86 while (startOfCurrentParagraph != startOfLastParagraph) {
87 // doApply() may operate on and remove the last paragraph of the selection from the document
88 // if it's in the same list item as startOfCurrentParagraph. Return early to avoid an
89 // infinite loop and because there is no more work to be done.
90 // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here. Compute
91 // the new location of endOfSelection and use it as the end of the new selection.
92 if (!startOfLastParagraph.deepEquivalent().node()->inDocument())
93 return true;
94 setEndingSelection(startOfCurrentParagraph);
95 doApply();
96 startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
97 }
98 setEndingSelection(endOfSelection);
99 doApply();
100 // Fetch the end of the selection, for the reason mentioned above.
101 endOfSelection = endingSelection().visibleEnd();
102 setEndingSelection(VisibleSelection(startOfSelection, endOfSelection));
103 m_forceCreateList = false;
104 return true;
105 }
106
doApply()107 void InsertListCommand::doApply()
108 {
109 if (endingSelection().isNone())
110 return;
111
112 if (!endingSelection().rootEditableElement())
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))
126 setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true)));
127
128 if (endingSelection().isRange() && modifyRange())
129 return;
130
131 // FIXME: This will produce unexpected results for a selection that starts just before a
132 // table and ends inside the first cell, selectionForParagraphIteration should probably
133 // be renamed and deployed inside setEndingSelection().
134 Node* selectionNode = endingSelection().start().node();
135 const QualifiedName listTag = (m_type == OrderedList) ? olTag : ulTag;
136 Node* listChildNode = enclosingListChild(selectionNode);
137 bool switchListType = false;
138 if (listChildNode) {
139 // Remove the list chlild.
140 HTMLElement* listNode = enclosingList(listChildNode);
141 if (!listNode)
142 listNode = fixOrphanedListChild(listChildNode);
143 if (!listNode->hasTagName(listTag))
144 // listChildNode will be removed from the list and a list of type m_type will be created.
145 switchListType = true;
146 Node* nextListChild;
147 Node* previousListChild;
148 VisiblePosition start;
149 VisiblePosition end;
150 if (listChildNode->hasTagName(liTag)) {
151 start = firstDeepEditingPositionForNode(listChildNode);
152 end = lastDeepEditingPositionForNode(listChildNode);
153 nextListChild = listChildNode->nextSibling();
154 previousListChild = listChildNode->previousSibling();
155 } else {
156 // A paragraph is visually a list item minus a list marker. The paragraph will be moved.
157 start = startOfParagraph(endingSelection().visibleStart());
158 end = endOfParagraph(endingSelection().visibleEnd());
159 nextListChild = enclosingListChild(end.next().deepEquivalent().node());
160 ASSERT(nextListChild != listChildNode);
161 if (enclosingList(nextListChild) != listNode)
162 nextListChild = 0;
163 previousListChild = enclosingListChild(start.previous().deepEquivalent().node());
164 ASSERT(previousListChild != listChildNode);
165 if (enclosingList(previousListChild) != listNode)
166 previousListChild = 0;
167 }
168 // When removing a list, we must always create a placeholder to act as a point of insertion
169 // for the list content being removed.
170 RefPtr<Element> placeholder = createBreakElement(document());
171 RefPtr<Element> nodeToInsert = placeholder;
172 // If the content of the list item will be moved into another list, put it in a list item
173 // so that we don't create an orphaned list child.
174 if (enclosingList(listNode)) {
175 nodeToInsert = createListItemElement(document());
176 appendNode(placeholder, nodeToInsert);
177 }
178
179 if (nextListChild && previousListChild) {
180 // We want to pull listChildNode out of listNode, and place it before nextListChild
181 // and after previousListChild, so we split listNode and insert it between the two lists.
182 // But to split listNode, we must first split ancestors of listChildNode between it and listNode,
183 // if any exist.
184 // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove
185 // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is
186 // unrendered. But we ought to remove nextListChild too, if it is unrendered.
187 splitElement(listNode, splitTreeToNode(nextListChild, listNode));
188 insertNodeBefore(nodeToInsert, listNode);
189 } else if (nextListChild || listChildNode->parentNode() != listNode) {
190 // Just because listChildNode has no previousListChild doesn't mean there isn't any content
191 // in listNode that comes before listChildNode, as listChildNode could have ancestors
192 // between it and listNode. So, we split up to listNode before inserting the placeholder
193 // where we're about to move listChildNode to.
194 if (listChildNode->parentNode() != listNode)
195 splitElement(listNode, splitTreeToNode(listChildNode, listNode).get());
196 insertNodeBefore(nodeToInsert, listNode);
197 } else
198 insertNodeAfter(nodeToInsert, listNode);
199
200 VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0));
201 moveParagraphs(start, end, insertionPoint, true);
202 }
203 if (!listChildNode || switchListType || m_forceCreateList) {
204 // Create list.
205 VisiblePosition originalStart = endingSelection().visibleStart();
206 VisiblePosition start = startOfParagraph(originalStart);
207 VisiblePosition end = endOfParagraph(endingSelection().visibleEnd());
208
209 // Check for adjoining lists.
210 VisiblePosition previousPosition = start.previous(true);
211 VisiblePosition nextPosition = end.next(true);
212 RefPtr<HTMLElement> listItemElement = createListItemElement(document());
213 RefPtr<HTMLElement> placeholder = createBreakElement(document());
214 appendNode(placeholder, listItemElement);
215 Element* previousList = outermostEnclosingList(previousPosition.deepEquivalent().node());
216 Element* nextList = outermostEnclosingList(nextPosition.deepEquivalent().node());
217 Node* startNode = start.deepEquivalent().node();
218 Node* previousCell = enclosingTableCell(previousPosition.deepEquivalent());
219 Node* nextCell = enclosingTableCell(nextPosition.deepEquivalent());
220 Node* currentCell = enclosingTableCell(start.deepEquivalent());
221 if (previousList && (!previousList->hasTagName(listTag) || startNode->isDescendantOf(previousList) || previousCell != currentCell))
222 previousList = 0;
223 if (nextList && (!nextList->hasTagName(listTag) || startNode->isDescendantOf(nextList) || nextCell != currentCell))
224 nextList = 0;
225 // Place list item into adjoining lists.
226 if (previousList)
227 appendNode(listItemElement, previousList);
228 else if (nextList)
229 insertNodeAt(listItemElement, Position(nextList, 0));
230 else {
231 // Create the list.
232 RefPtr<HTMLElement> listElement = m_type == OrderedList ? createOrderedListElement(document()) : createUnorderedListElement(document());
233 m_listElement = listElement;
234 appendNode(listItemElement, listElement);
235
236 if (start == end && isBlock(start.deepEquivalent().node())) {
237 // Inserting the list into an empty paragraph that isn't held open
238 // by a br or a '\n', will invalidate start and end. Insert
239 // a placeholder and then recompute start and end.
240 RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent());
241 start = VisiblePosition(Position(placeholder.get(), 0));
242 end = start;
243 }
244
245 // Insert the list at a position visually equivalent to start of the
246 // paragraph that is being moved into the list.
247 // Try to avoid inserting it somewhere where it will be surrounded by
248 // inline ancestors of start, since it is easier for editing to produce
249 // clean markup when inline elements are pushed down as far as possible.
250 Position insertionPos(start.deepEquivalent().upstream());
251 // Also avoid the containing list item.
252 Node* listChild = enclosingListChild(insertionPos.node());
253 if (listChild && listChild->hasTagName(liTag))
254 insertionPos = positionBeforeNode(listChild);
255
256 insertNodeAt(listElement, insertionPos);
257
258 // We inserted the list at the start of the content we're about to move
259 // Update the start of content, so we don't try to move the list into itself. bug 19066
260 if (insertionPos == start.deepEquivalent())
261 start = startOfParagraph(originalStart);
262 }
263 moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true);
264 if (nextList && previousList)
265 mergeIdenticalElements(previousList, nextList);
266 }
267 }
268
269 }
270