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 | |
223 // |m_type| will be created. | |
224 switchListType = true; | |
225 } | |
226 | |
227 // If the list is of the desired type, and we are not removing the list, | |
228 // then exit early. | |
229 if (!switchListType && forceCreateList) | |
230 return true; | |
231 | |
232 // If the entire list is selected, then convert the whole list. | |
233 if (switchListType && isNodeVisiblyContainedWithin(*listElement, current
Selection)) { | |
234 bool rangeStartIsInList = visiblePositionBeforeNode(*listElement).de
epEquivalent() == VisiblePosition(currentSelection.startPosition()).deepEquivale
nt(); | |
235 bool rangeEndIsInList = visiblePositionAfterNode(*listElement).deepE
quivalent() == VisiblePosition(currentSelection.endPosition()).deepEquivalent(); | |
236 | |
237 RefPtrWillBeRawPtr<HTMLElement> newList = createHTMLElement(document
(), listTag); | |
238 insertNodeBefore(newList, listElement); | |
239 | |
240 Node* firstChildInList = enclosingListChild(VisiblePosition(firstPos
itionInNode(listElement.get())).deepEquivalent().anchorNode(), listElement.get()
); | |
241 Element* outerBlock = firstChildInList && isBlockFlowElement(*firstC
hildInList) ? toElement(firstChildInList) : listElement.get(); | |
242 | |
243 moveParagraphWithClones(VisiblePosition(firstPositionInNode(listElem
ent.get())), VisiblePosition(lastPositionInNode(listElement.get())), newList.get
(), outerBlock); | |
244 | |
245 // Manually remove listNode because moveParagraphWithClones sometime
s leaves it behind in the document. | |
246 // See the bug 33668 and editing/execCommand/insert-list-orphaned-it
em-with-nested-lists.html. | |
247 // FIXME: This might be a bug in moveParagraphWithClones or deleteSe
lection. | |
248 if (listElement && listElement->inDocument()) | |
249 removeNode(listElement); | |
250 | |
251 newList = mergeWithNeighboringLists(newList); | |
252 | |
253 // Restore the start and the end of current selection if they starte
d inside listNode | |
254 // because moveParagraphWithClones could have removed them. | |
255 if (rangeStartIsInList && newList) | |
256 currentSelection.setStart(newList, 0, IGNORE_EXCEPTION); | |
257 if (rangeEndIsInList && newList) | |
258 currentSelection.setEnd(newList, lastOffsetInNode(newList.get())
, IGNORE_EXCEPTION); | |
259 | |
260 setEndingSelection(VisiblePosition(firstPositionInNode(newList.get()
))); | |
261 | |
262 return true; | |
263 } | |
264 | |
265 unlistifyParagraph(endingSelection().visibleStart(), listElement.get(),
listChildNode); | |
266 } | |
267 | |
268 if (!listChildNode || switchListType || forceCreateList) | |
269 m_listElement = listifyParagraph(endingSelection().visibleStart(), listT
ag); | |
270 | |
271 return true; | |
272 } | |
273 | |
274 void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart,
HTMLElement* listElement, Node* listChildNode) | |
275 { | |
276 Node* nextListChild; | |
277 Node* previousListChild; | |
278 VisiblePosition start; | |
279 VisiblePosition end; | |
280 ASSERT(listChildNode); | |
281 if (isHTMLLIElement(*listChildNode)) { | |
282 start = VisiblePosition(firstPositionInNode(listChildNode)); | |
283 end = VisiblePosition(lastPositionInNode(listChildNode)); | |
284 nextListChild = listChildNode->nextSibling(); | |
285 previousListChild = listChildNode->previousSibling(); | |
286 } else { | |
287 // A paragraph is visually a list item minus a list marker. The paragra
ph will be moved. | |
288 start = startOfParagraph(originalStart, CanSkipOverEditingBoundary); | |
289 end = endOfParagraph(start, CanSkipOverEditingBoundary); | |
290 nextListChild = enclosingListChild(end.next().deepEquivalent().anchorNod
e(), listElement); | |
291 ASSERT(nextListChild != listChildNode); | |
292 previousListChild = enclosingListChild(start.previous().deepEquivalent()
.anchorNode(), listElement); | |
293 ASSERT(previousListChild != listChildNode); | |
294 } | |
295 // When removing a list, we must always create a placeholder to act as a poi
nt of insertion | |
296 // for the list content being removed. | |
297 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document(
)); | |
298 RefPtrWillBeRawPtr<HTMLElement> elementToInsert = placeholder; | |
299 // If the content of the list item will be moved into another list, put it i
n a list item | |
300 // so that we don't create an orphaned list child. | |
301 if (enclosingList(listElement)) { | |
302 elementToInsert = createListItemElement(document()); | |
303 appendNode(placeholder, elementToInsert); | |
304 } | |
305 | |
306 if (nextListChild && previousListChild) { | |
307 // We want to pull listChildNode out of listNode, and place it before ne
xtListChild | |
308 // and after previousListChild, so we split listNode and insert it betwe
en the two lists. | |
309 // But to split listNode, we must first split ancestors of listChildNode
between it and listNode, | |
310 // if any exist. | |
311 // FIXME: We appear to split at nextListChild as opposed to listChildNod
e so that when we remove | |
312 // listChildNode below in moveParagraphs, previousListChild will be remo
ved along with it if it is | |
313 // unrendered. But we ought to remove nextListChild too, if it is unrend
ered. | |
314 splitElement(listElement, splitTreeToNode(nextListChild, listElement)); | |
315 insertNodeBefore(elementToInsert, listElement); | |
316 } else if (nextListChild || listChildNode->parentNode() != listElement) { | |
317 // Just because listChildNode has no previousListChild doesn't mean ther
e isn't any content | |
318 // in listNode that comes before listChildNode, as listChildNode could h
ave ancestors | |
319 // between it and listNode. So, we split up to listNode before inserting
the placeholder | |
320 // where we're about to move listChildNode to. | |
321 if (listChildNode->parentNode() != listElement) | |
322 splitElement(listElement, splitTreeToNode(listChildNode, listElement
).get()); | |
323 insertNodeBefore(elementToInsert, listElement); | |
324 } else { | |
325 insertNodeAfter(elementToInsert, listElement); | |
326 } | |
327 | |
328 VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placehol
der.get())); | |
329 moveParagraphs(start, end, insertionPoint, /* preserveSelection */ true, /*
preserveStyle */ true, listChildNode); | |
330 } | |
331 | |
332 static HTMLElement* adjacentEnclosingList(const VisiblePosition& pos, const Visi
blePosition& adjacentPos, const HTMLQualifiedName& listTag) | |
333 { | |
334 HTMLElement* listElement = outermostEnclosingList(adjacentPos.deepEquivalent
().anchorNode()); | |
335 | |
336 if (!listElement) | |
337 return 0; | |
338 | |
339 Element* previousCell = enclosingTableCell(pos.deepEquivalent()); | |
340 Element* currentCell = enclosingTableCell(adjacentPos.deepEquivalent()); | |
341 | |
342 if (!listElement->hasTagName(listTag) | |
343 || listElement->contains(pos.deepEquivalent().anchorNode()) | |
344 || previousCell != currentCell | |
345 || enclosingList(listElement) != enclosingList(pos.deepEquivalent().anch
orNode())) | |
346 return 0; | |
347 | |
348 return listElement; | |
349 } | |
350 | |
351 PassRefPtrWillBeRawPtr<HTMLElement> InsertListCommand::listifyParagraph(const Vi
siblePosition& originalStart, const HTMLQualifiedName& listTag) | |
352 { | |
353 VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBo
undary); | |
354 VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary); | |
355 | |
356 if (start.isNull() || end.isNull()) | |
357 return nullptr; | |
358 | |
359 // Check for adjoining lists. | |
360 RefPtrWillBeRawPtr<HTMLElement> listItemElement = createListItemElement(docu
ment()); | |
361 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document(
)); | |
362 appendNode(placeholder, listItemElement); | |
363 | |
364 // Place list item into adjoining lists. | |
365 HTMLElement* previousList = adjacentEnclosingList(start, start.previous(Cann
otCrossEditingBoundary), listTag); | |
366 HTMLElement* nextList = adjacentEnclosingList(start, end.next(CannotCrossEdi
tingBoundary), listTag); | |
367 RefPtrWillBeRawPtr<HTMLElement> listElement = nullptr; | |
368 if (previousList) { | |
369 appendNode(listItemElement, previousList); | |
370 } else if (nextList) { | |
371 insertNodeAt(listItemElement, positionBeforeNode(nextList)); | |
372 } else { | |
373 // Create the list. | |
374 listElement = createHTMLElement(document(), listTag); | |
375 appendNode(listItemElement, listElement); | |
376 | |
377 if (start.deepEquivalent() == end.deepEquivalent() && isBlock(start.deep
Equivalent().anchorNode())) { | |
378 // Inserting the list into an empty paragraph that isn't held open | |
379 // by a br or a '\n', will invalidate start and end. Insert | |
380 // a placeholder and then recompute start and end. | |
381 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = insertBlockPlacehold
er(start.deepEquivalent()); | |
382 start = VisiblePosition(positionBeforeNode(placeholder.get())); | |
383 end = start; | |
384 } | |
385 | |
386 // Insert the list at a position visually equivalent to start of the | |
387 // paragraph that is being moved into the list. | |
388 // Try to avoid inserting it somewhere where it will be surrounded by | |
389 // inline ancestors of start, since it is easier for editing to produce | |
390 // clean markup when inline elements are pushed down as far as possible. | |
391 Position insertionPos(start.deepEquivalent().upstream()); | |
392 // Also avoid the containing list item. | |
393 Node* listChild = enclosingListChild(insertionPos.anchorNode()); | |
394 if (isHTMLLIElement(listChild)) | |
395 insertionPos = positionInParentBeforeNode(*listChild); | |
396 | |
397 insertNodeAt(listElement, insertionPos); | |
398 | |
399 // We inserted the list at the start of the content we're about to move | |
400 // Update the start of content, so we don't try to move the list into it
self. bug 19066 | |
401 // Layout is necessary since start's node's inline layoutObjects may hav
e been destroyed by the insertion | |
402 // The end of the content may have changed after the insertion and layou
t so update it as well. | |
403 if (insertionPos == start.deepEquivalent()) | |
404 start = originalStart; | |
405 } | |
406 | |
407 // Inserting list element and list item list may change start of pargraph | |
408 // to move. We calculate start of paragraph again. | |
409 document().updateLayoutIgnorePendingStylesheets(); | |
410 start = startOfParagraph(start, CanSkipOverEditingBoundary); | |
411 end = endOfParagraph(start, CanSkipOverEditingBoundary); | |
412 moveParagraph(start, end, VisiblePosition(positionBeforeNode(placeholder.get
())), true); | |
413 | |
414 if (listElement) | |
415 return mergeWithNeighboringLists(listElement); | |
416 | |
417 if (canMergeLists(previousList, nextList)) | |
418 mergeIdenticalElements(previousList, nextList); | |
419 | |
420 return listElement; | |
421 } | |
422 | |
423 DEFINE_TRACE(InsertListCommand) | |
424 { | |
425 visitor->trace(m_listElement); | |
426 CompositeEditCommand::trace(visitor); | |
427 } | |
428 | |
429 } | |
OLD | NEW |