OLD | NEW |
| (Empty) |
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 "core/editing/InsertListCommand.h" | |
28 | |
29 #include "bindings/core/v8/ExceptionStatePlaceholder.h" | |
30 #include "core/HTMLNames.h" | |
31 #include "core/dom/Document.h" | |
32 #include "core/dom/Element.h" | |
33 #include "core/dom/ElementTraversal.h" | |
34 #include "core/editing/EditingUtilities.h" | |
35 #include "core/editing/VisibleUnits.h" | |
36 #include "core/editing/iterators/TextIterator.h" | |
37 #include "core/html/HTMLBRElement.h" | |
38 #include "core/html/HTMLElement.h" | |
39 #include "core/html/HTMLLIElement.h" | |
40 #include "core/html/HTMLUListElement.h" | |
41 | |
42 namespace blink { | |
43 | |
44 using namespace HTMLNames; | |
45 | |
46 static Node* enclosingListChild(Node* node, Node* listNode) | |
47 { | |
48 Node* listChild = enclosingListChild(node); | |
49 while (listChild && enclosingList(listChild) != listNode) | |
50 listChild = enclosingListChild(listChild->parentNode()); | |
51 return listChild; | |
52 } | |
53 | |
54 HTMLUListElement* InsertListCommand::fixOrphanedListChild(Node* node) | |
55 { | |
56 RefPtrWillBeRawPtr<HTMLUListElement> listElement = createUnorderedListElemen
t(document()); | |
57 insertNodeBefore(listElement, node); | |
58 removeNode(node); | |
59 appendNode(node, listElement); | |
60 m_listElement = listElement; | |
61 return listElement.get(); | |
62 } | |
63 | |
64 PassRefPtrWillBeRawPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists
(PassRefPtrWillBeRawPtr<HTMLElement> passedList) | |
65 { | |
66 RefPtrWillBeRawPtr<HTMLElement> list = passedList; | |
67 Element* previousList = ElementTraversal::previousSibling(*list); | |
68 if (canMergeLists(previousList, list.get())) | |
69 mergeIdenticalElements(previousList, list); | |
70 | |
71 if (!list) | |
72 return nullptr; | |
73 | |
74 Element* nextSibling = ElementTraversal::nextSibling(*list); | |
75 if (!nextSibling || !nextSibling->isHTMLElement()) | |
76 return list.release(); | |
77 | |
78 RefPtrWillBeRawPtr<HTMLElement> nextList = toHTMLElement(nextSibling); | |
79 if (canMergeLists(list.get(), nextList.get())) { | |
80 mergeIdenticalElements(list, nextList); | |
81 return nextList.release(); | |
82 } | |
83 return list.release(); | |
84 } | |
85 | |
86 bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection
, const HTMLQualifiedName& listTag) | |
87 { | |
88 VisiblePosition start = selection.visibleStart(); | |
89 | |
90 if (!enclosingList(start.deepEquivalent().anchorNode())) | |
91 return false; | |
92 | |
93 VisiblePosition end = startOfParagraph(selection.visibleEnd()); | |
94 while (start.isNotNull() && start.deepEquivalent() != end.deepEquivalent())
{ | |
95 HTMLElement* listElement = enclosingList(start.deepEquivalent().anchorNo
de()); | |
96 if (!listElement || !listElement->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), m_type(type) | |
106 { | |
107 } | |
108 | |
109 void InsertListCommand::doApply() | |
110 { | |
111 if (!endingSelection().isNonOrphanedCaretOrRange()) | |
112 return; | |
113 | |
114 if (!endingSelection().rootEditableElement()) | |
115 return; | |
116 | |
117 VisiblePosition visibleEnd = endingSelection().visibleEnd(); | |
118 VisiblePosition visibleStart = endingSelection().visibleStart(); | |
119 // When a selection ends at the start of a paragraph, we rarely paint | |
120 // the selection gap before that paragraph, because there often is no gap. | |
121 // In a case like this, it's not obvious to the user that the selection | |
122 // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordere
d}List | |
123 // operated on that paragraph. | |
124 // FIXME: We paint the gap before some paragraphs that are indented with lef
t | |
125 // margin/padding, but not others. We should make the gap painting more con
sistent and | |
126 // then use a left margin/padding rule here. | |
127 if (visibleEnd.deepEquivalent() != visibleStart.deepEquivalent() && isStartO
fParagraph(visibleEnd, CanSkipOverEditingBoundary)) { | |
128 setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(Ca
nnotCrossEditingBoundary), endingSelection().isDirectional())); | |
129 if (!endingSelection().rootEditableElement()) | |
130 return; | |
131 } | |
132 | |
133 const HTMLQualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag; | |
134 if (endingSelection().isRange()) { | |
135 bool forceListCreation = false; | |
136 VisibleSelection selection = selectionForParagraphIteration(endingSelect
ion()); | |
137 ASSERT(selection.isRange()); | |
138 VisiblePosition startOfSelection = selection.visibleStart(); | |
139 VisiblePosition endOfSelection = selection.visibleEnd(); | |
140 VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection,
CanSkipOverEditingBoundary); | |
141 | |
142 RefPtrWillBeRawPtr<Range> currentSelection = endingSelection().firstRang
e(); | |
143 RefPtrWillBeRawPtr<ContainerNode> scopeForStartOfSelection = nullptr; | |
144 RefPtrWillBeRawPtr<ContainerNode> scopeForEndOfSelection = nullptr; | |
145 // FIXME: This is an inefficient way to keep selection alive because | |
146 // indexForVisiblePosition walks from the beginning of the document to t
he | |
147 // endOfSelection everytime this code is executed. But not using index i
s hard | |
148 // because there are so many ways we can los eselection inside doApplyFo
rSingleParagraph. | |
149 int indexForStartOfSelection = indexForVisiblePosition(startOfSelection,
scopeForStartOfSelection); | |
150 int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, sco
peForEndOfSelection); | |
151 | |
152 if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary).deepE
quivalent() != startOfLastParagraph.deepEquivalent()) { | |
153 forceListCreation = !selectionHasListOfType(selection, listTag); | |
154 | |
155 VisiblePosition startOfCurrentParagraph = startOfSelection; | |
156 while (startOfCurrentParagraph.isNotNull() && !inSameParagraph(start
OfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) { | |
157 // doApply() may operate on and remove the last paragraph of the
selection from the document | |
158 // if it's in the same list item as startOfCurrentParagraph. Re
turn early to avoid an | |
159 // infinite loop and because there is no more work to be done. | |
160 // FIXME(<rdar://problem/5983974>): The endingSelection() may be
incorrect here. Compute | |
161 // the new location of endOfSelection and use it as the end of t
he new selection. | |
162 if (!startOfLastParagraph.deepEquivalent().inDocument()) | |
163 return; | |
164 setEndingSelection(startOfCurrentParagraph); | |
165 | |
166 // Save and restore endOfSelection and startOfLastParagraph when
necessary | |
167 // since moveParagraph and movePragraphWithClones can remove nod
es. | |
168 if (!doApplyForSingleParagraph(forceListCreation, listTag, *curr
entSelection)) | |
169 break; | |
170 if (endOfSelection.isNull() || endOfSelection.isOrphan() || star
tOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) { | |
171 endOfSelection = visiblePositionForIndex(indexForEndOfSelect
ion, scopeForEndOfSelection.get()); | |
172 // If endOfSelection is null, then some contents have been d
eleted from the document. | |
173 // This should never happen and if it did, exit early immedi
ately because we've lost the loop invariant. | |
174 ASSERT(endOfSelection.isNotNull()); | |
175 if (endOfSelection.isNull()) | |
176 return; | |
177 startOfLastParagraph = startOfParagraph(endOfSelection, CanS
kipOverEditingBoundary); | |
178 } | |
179 | |
180 startOfCurrentParagraph = startOfNextParagraph(endingSelection()
.visibleStart()); | |
181 } | |
182 setEndingSelection(endOfSelection); | |
183 } | |
184 doApplyForSingleParagraph(forceListCreation, listTag, *currentSelection)
; | |
185 // Fetch the end of the selection, for the reason mentioned above. | |
186 if (endOfSelection.isNull() || endOfSelection.isOrphan()) { | |
187 endOfSelection = visiblePositionForIndex(indexForEndOfSelection, sco
peForEndOfSelection.get()); | |
188 if (endOfSelection.isNull()) | |
189 return; | |
190 } | |
191 if (startOfSelection.isNull() || startOfSelection.isOrphan()) { | |
192 startOfSelection = visiblePositionForIndex(indexForStartOfSelection,
scopeForStartOfSelection.get()); | |
193 if (startOfSelection.isNull()) | |
194 return; | |
195 } | |
196 setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, en
dingSelection().isDirectional())); | |
197 return; | |
198 } | |
199 | |
200 ASSERT(endingSelection().firstRange()); | |
201 doApplyForSingleParagraph(false, listTag, *endingSelection().firstRange()); | |
202 } | |
203 | |
204 bool InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const HT
MLQualifiedName& listTag, Range& currentSelection) | |
205 { | |
206 // FIXME: This will produce unexpected results for a selection that starts j
ust before a | |
207 // table and ends inside the first cell, selectionForParagraphIteration shou
ld probably | |
208 // be renamed and deployed inside setEndingSelection(). | |
209 Node* selectionNode = endingSelection().start().anchorNode(); | |
210 Node* listChildNode = enclosingListChild(selectionNode); | |
211 bool switchListType = false; | |
212 if (listChildNode) { | |
213 if (!listChildNode->parentNode()->hasEditableStyle()) | |
214 return false; | |
215 // Remove the list child. | |
216 RefPtrWillBeRawPtr<HTMLElement> listElement = enclosingList(listChildNod
e); | |
217 if (!listElement) { | |
218 listElement = fixOrphanedListChild(listChildNode); | |
219 listElement = mergeWithNeighboringLists(listElement); | |
220 } | |
221 if (!listElement->hasTagName(listTag)) | |
222 // listChildNode will be removed from the list and a list of type m_
type will be created. | |
223 switchListType = true; | |
224 | |
225 // If the list is of the desired type, and we are not removing the list,
then exit early. | |
226 if (!switchListType && forceCreateList) | |
227 return true; | |
228 | |
229 // If the entire list is selected, then convert the whole list. | |
230 if (switchListType && isNodeVisiblyContainedWithin(*listElement, current
Selection)) { | |
231 bool rangeStartIsInList = visiblePositionBeforeNode(*listElement).de
epEquivalent() == VisiblePosition(currentSelection.startPosition()).deepEquivale
nt(); | |
232 bool rangeEndIsInList = visiblePositionAfterNode(*listElement).deepE
quivalent() == VisiblePosition(currentSelection.endPosition()).deepEquivalent(); | |
233 | |
234 RefPtrWillBeRawPtr<HTMLElement> newList = createHTMLElement(document
(), listTag); | |
235 insertNodeBefore(newList, listElement); | |
236 | |
237 Node* firstChildInList = enclosingListChild(VisiblePosition(firstPos
itionInNode(listElement.get())).deepEquivalent().anchorNode(), listElement.get()
); | |
238 Element* outerBlock = firstChildInList && isBlockFlowElement(*firstC
hildInList) ? toElement(firstChildInList) : listElement.get(); | |
239 | |
240 moveParagraphWithClones(VisiblePosition(firstPositionInNode(listElem
ent.get())), VisiblePosition(lastPositionInNode(listElement.get())), newList.get
(), outerBlock); | |
241 | |
242 // Manually remove listNode because moveParagraphWithClones sometime
s leaves it behind in the document. | |
243 // See the bug 33668 and editing/execCommand/insert-list-orphaned-it
em-with-nested-lists.html. | |
244 // FIXME: This might be a bug in moveParagraphWithClones or deleteSe
lection. | |
245 if (listElement && listElement->inDocument()) | |
246 removeNode(listElement); | |
247 | |
248 newList = mergeWithNeighboringLists(newList); | |
249 | |
250 // Restore the start and the end of current selection if they starte
d inside listNode | |
251 // because moveParagraphWithClones could have removed them. | |
252 if (rangeStartIsInList && newList) | |
253 currentSelection.setStart(newList, 0, IGNORE_EXCEPTION); | |
254 if (rangeEndIsInList && newList) | |
255 currentSelection.setEnd(newList, lastOffsetInNode(newList.get())
, IGNORE_EXCEPTION); | |
256 | |
257 setEndingSelection(VisiblePosition(firstPositionInNode(newList.get()
))); | |
258 | |
259 return true; | |
260 } | |
261 | |
262 unlistifyParagraph(endingSelection().visibleStart(), listElement.get(),
listChildNode); | |
263 } | |
264 | |
265 if (!listChildNode || switchListType || forceCreateList) | |
266 m_listElement = listifyParagraph(endingSelection().visibleStart(), listT
ag); | |
267 | |
268 return true; | |
269 } | |
270 | |
271 void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart,
HTMLElement* listElement, Node* listChildNode) | |
272 { | |
273 Node* nextListChild; | |
274 Node* previousListChild; | |
275 VisiblePosition start; | |
276 VisiblePosition end; | |
277 ASSERT(listChildNode); | |
278 if (isHTMLLIElement(*listChildNode)) { | |
279 start = VisiblePosition(firstPositionInNode(listChildNode)); | |
280 end = VisiblePosition(lastPositionInNode(listChildNode)); | |
281 nextListChild = listChildNode->nextSibling(); | |
282 previousListChild = listChildNode->previousSibling(); | |
283 } else { | |
284 // A paragraph is visually a list item minus a list marker. The paragra
ph will be moved. | |
285 start = startOfParagraph(originalStart, CanSkipOverEditingBoundary); | |
286 end = endOfParagraph(start, CanSkipOverEditingBoundary); | |
287 nextListChild = enclosingListChild(end.next().deepEquivalent().anchorNod
e(), listElement); | |
288 ASSERT(nextListChild != listChildNode); | |
289 previousListChild = enclosingListChild(start.previous().deepEquivalent()
.anchorNode(), listElement); | |
290 ASSERT(previousListChild != listChildNode); | |
291 } | |
292 // When removing a list, we must always create a placeholder to act as a poi
nt of insertion | |
293 // for the list content being removed. | |
294 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document(
)); | |
295 RefPtrWillBeRawPtr<HTMLElement> elementToInsert = placeholder; | |
296 // If the content of the list item will be moved into another list, put it i
n a list item | |
297 // so that we don't create an orphaned list child. | |
298 if (enclosingList(listElement)) { | |
299 elementToInsert = createListItemElement(document()); | |
300 appendNode(placeholder, elementToInsert); | |
301 } | |
302 | |
303 if (nextListChild && previousListChild) { | |
304 // We want to pull listChildNode out of listNode, and place it before ne
xtListChild | |
305 // and after previousListChild, so we split listNode and insert it betwe
en the two lists. | |
306 // But to split listNode, we must first split ancestors of listChildNode
between it and listNode, | |
307 // if any exist. | |
308 // FIXME: We appear to split at nextListChild as opposed to listChildNod
e so that when we remove | |
309 // listChildNode below in moveParagraphs, previousListChild will be remo
ved along with it if it is | |
310 // unrendered. But we ought to remove nextListChild too, if it is unrend
ered. | |
311 splitElement(listElement, splitTreeToNode(nextListChild, listElement)); | |
312 insertNodeBefore(elementToInsert, listElement); | |
313 } else if (nextListChild || listChildNode->parentNode() != listElement) { | |
314 // Just because listChildNode has no previousListChild doesn't mean ther
e isn't any content | |
315 // in listNode that comes before listChildNode, as listChildNode could h
ave ancestors | |
316 // between it and listNode. So, we split up to listNode before inserting
the placeholder | |
317 // where we're about to move listChildNode to. | |
318 if (listChildNode->parentNode() != listElement) | |
319 splitElement(listElement, splitTreeToNode(listChildNode, listElement
).get()); | |
320 insertNodeBefore(elementToInsert, listElement); | |
321 } else { | |
322 insertNodeAfter(elementToInsert, listElement); | |
323 } | |
324 | |
325 VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placehol
der.get())); | |
326 moveParagraphs(start, end, insertionPoint, /* preserveSelection */ true, /*
preserveStyle */ true, listChildNode); | |
327 } | |
328 | |
329 static HTMLElement* adjacentEnclosingList(const VisiblePosition& pos, const Visi
blePosition& adjacentPos, const HTMLQualifiedName& listTag) | |
330 { | |
331 HTMLElement* listElement = outermostEnclosingList(adjacentPos.deepEquivalent
().anchorNode()); | |
332 | |
333 if (!listElement) | |
334 return 0; | |
335 | |
336 Element* previousCell = enclosingTableCell(pos.deepEquivalent()); | |
337 Element* currentCell = enclosingTableCell(adjacentPos.deepEquivalent()); | |
338 | |
339 if (!listElement->hasTagName(listTag) | |
340 || listElement->contains(pos.deepEquivalent().anchorNode()) | |
341 || previousCell != currentCell | |
342 || enclosingList(listElement) != enclosingList(pos.deepEquivalent().anch
orNode())) | |
343 return 0; | |
344 | |
345 return listElement; | |
346 } | |
347 | |
348 PassRefPtrWillBeRawPtr<HTMLElement> InsertListCommand::listifyParagraph(const Vi
siblePosition& originalStart, const HTMLQualifiedName& listTag) | |
349 { | |
350 VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBo
undary); | |
351 VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary); | |
352 | |
353 if (start.isNull() || end.isNull()) | |
354 return nullptr; | |
355 | |
356 // Check for adjoining lists. | |
357 RefPtrWillBeRawPtr<HTMLElement> listItemElement = createListItemElement(docu
ment()); | |
358 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document(
)); | |
359 appendNode(placeholder, listItemElement); | |
360 | |
361 // Place list item into adjoining lists. | |
362 HTMLElement* previousList = adjacentEnclosingList(start, start.previous(Cann
otCrossEditingBoundary), listTag); | |
363 HTMLElement* nextList = adjacentEnclosingList(start, end.next(CannotCrossEdi
tingBoundary), listTag); | |
364 RefPtrWillBeRawPtr<HTMLElement> listElement = nullptr; | |
365 if (previousList) | |
366 appendNode(listItemElement, previousList); | |
367 else if (nextList) | |
368 insertNodeAt(listItemElement, positionBeforeNode(nextList)); | |
369 else { | |
370 // Create the list. | |
371 listElement = createHTMLElement(document(), listTag); | |
372 appendNode(listItemElement, listElement); | |
373 | |
374 if (start.deepEquivalent() == end.deepEquivalent() && isBlock(start.deep
Equivalent().anchorNode())) { | |
375 // Inserting the list into an empty paragraph that isn't held open | |
376 // by a br or a '\n', will invalidate start and end. Insert | |
377 // a placeholder and then recompute start and end. | |
378 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = insertBlockPlacehold
er(start.deepEquivalent()); | |
379 start = VisiblePosition(positionBeforeNode(placeholder.get())); | |
380 end = start; | |
381 } | |
382 | |
383 // Insert the list at a position visually equivalent to start of the | |
384 // paragraph that is being moved into the list. | |
385 // Try to avoid inserting it somewhere where it will be surrounded by | |
386 // inline ancestors of start, since it is easier for editing to produce | |
387 // clean markup when inline elements are pushed down as far as possible. | |
388 Position insertionPos(start.deepEquivalent().upstream()); | |
389 // Also avoid the containing list item. | |
390 Node* listChild = enclosingListChild(insertionPos.anchorNode()); | |
391 if (isHTMLLIElement(listChild)) | |
392 insertionPos = positionInParentBeforeNode(*listChild); | |
393 | |
394 insertNodeAt(listElement, insertionPos); | |
395 | |
396 // We inserted the list at the start of the content we're about to move | |
397 // Update the start of content, so we don't try to move the list into it
self. bug 19066 | |
398 // Layout is necessary since start's node's inline layoutObjects may hav
e been destroyed by the insertion | |
399 // The end of the content may have changed after the insertion and layou
t so update it as well. | |
400 if (insertionPos == start.deepEquivalent()) | |
401 start = originalStart; | |
402 } | |
403 | |
404 // Inserting list element and list item list may change start of pargraph | |
405 // to move. We calculate start of paragraph again. | |
406 document().updateLayoutIgnorePendingStylesheets(); | |
407 start = startOfParagraph(start, CanSkipOverEditingBoundary); | |
408 end = endOfParagraph(start, CanSkipOverEditingBoundary); | |
409 moveParagraph(start, end, VisiblePosition(positionBeforeNode(placeholder.get
())), true); | |
410 | |
411 if (listElement) | |
412 return mergeWithNeighboringLists(listElement); | |
413 | |
414 if (canMergeLists(previousList, nextList)) | |
415 mergeIdenticalElements(previousList, nextList); | |
416 | |
417 return listElement; | |
418 } | |
419 | |
420 DEFINE_TRACE(InsertListCommand) | |
421 { | |
422 visitor->trace(m_listElement); | |
423 CompositeEditCommand::trace(visitor); | |
424 } | |
425 | |
426 } | |
OLD | NEW |