|
1 /* |
|
2 * Copyright (C) 2005, 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 "InsertParagraphSeparatorCommand.h" |
|
28 |
|
29 #include "CSSComputedStyleDeclaration.h" |
|
30 #include "CSSMutableStyleDeclaration.h" |
|
31 #include "CSSPropertyNames.h" |
|
32 #include "Document.h" |
|
33 #include "HTMLElement.h" |
|
34 #include "HTMLNames.h" |
|
35 #include "InsertLineBreakCommand.h" |
|
36 #include "Logging.h" |
|
37 #include "RenderObject.h" |
|
38 #include "Text.h" |
|
39 #include "htmlediting.h" |
|
40 #include "visible_units.h" |
|
41 #include "ApplyStyleCommand.h" |
|
42 |
|
43 namespace WebCore { |
|
44 |
|
45 using namespace HTMLNames; |
|
46 |
|
47 // When inserting a new line, we want to avoid nesting empty divs if we can. Otherwise, when |
|
48 // pasting, it's easy to have each new line be a div deeper than the previous. E.g., in the case |
|
49 // below, we want to insert at ^ instead of |. |
|
50 // <div>foo<div>bar</div>|</div>^ |
|
51 static Element* highestVisuallyEquivalentDivBelowRoot(Element* startBlock) |
|
52 { |
|
53 Element* curBlock = startBlock; |
|
54 // We don't want to return a root node (if it happens to be a div, e.g., in a document fragment) because there are no |
|
55 // siblings for us to append to. |
|
56 while (!curBlock->nextSibling() && curBlock->parentElement()->hasTagName(divTag) && curBlock->parentElement()->parentElement()) { |
|
57 NamedNodeMap* attributes = curBlock->parentElement()->attributes(true); |
|
58 if (attributes && !attributes->isEmpty()) |
|
59 break; |
|
60 curBlock = curBlock->parentElement(); |
|
61 } |
|
62 return curBlock; |
|
63 } |
|
64 |
|
65 InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(Document *document, bool mustUseDefaultParagraphElement) |
|
66 : CompositeEditCommand(document) |
|
67 , m_mustUseDefaultParagraphElement(mustUseDefaultParagraphElement) |
|
68 { |
|
69 } |
|
70 |
|
71 bool InsertParagraphSeparatorCommand::preservesTypingStyle() const |
|
72 { |
|
73 return true; |
|
74 } |
|
75 |
|
76 void InsertParagraphSeparatorCommand::calculateStyleBeforeInsertion(const Position &pos) |
|
77 { |
|
78 // It is only important to set a style to apply later if we're at the boundaries of |
|
79 // a paragraph. Otherwise, content that is moved as part of the work of the command |
|
80 // will lend their styles to the new paragraph without any extra work needed. |
|
81 VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY); |
|
82 if (!isStartOfParagraph(visiblePos) && !isEndOfParagraph(visiblePos)) |
|
83 return; |
|
84 |
|
85 m_style = ApplyStyleCommand::editingStyleAtPosition(pos, IncludeTypingStyle); |
|
86 } |
|
87 |
|
88 void InsertParagraphSeparatorCommand::applyStyleAfterInsertion(Node* originalEnclosingBlock) |
|
89 { |
|
90 // Not only do we break out of header tags, but we also do not preserve the typing style, |
|
91 // in order to match other browsers. |
|
92 if (originalEnclosingBlock->hasTagName(h1Tag) || |
|
93 originalEnclosingBlock->hasTagName(h2Tag) || |
|
94 originalEnclosingBlock->hasTagName(h3Tag) || |
|
95 originalEnclosingBlock->hasTagName(h4Tag) || |
|
96 originalEnclosingBlock->hasTagName(h5Tag)) |
|
97 return; |
|
98 |
|
99 if (!m_style) |
|
100 return; |
|
101 |
|
102 prepareEditingStyleToApplyAt(m_style.get(), endingSelection().start()); |
|
103 |
|
104 if (m_style->length() > 0) |
|
105 applyStyle(m_style.get()); |
|
106 } |
|
107 |
|
108 bool InsertParagraphSeparatorCommand::shouldUseDefaultParagraphElement(Node* enclosingBlock) const |
|
109 { |
|
110 if (m_mustUseDefaultParagraphElement) |
|
111 return true; |
|
112 |
|
113 // Assumes that if there was a range selection, it was already deleted. |
|
114 if (!isEndOfBlock(endingSelection().visibleStart())) |
|
115 return false; |
|
116 |
|
117 return enclosingBlock->hasTagName(h1Tag) || |
|
118 enclosingBlock->hasTagName(h2Tag) || |
|
119 enclosingBlock->hasTagName(h3Tag) || |
|
120 enclosingBlock->hasTagName(h4Tag) || |
|
121 enclosingBlock->hasTagName(h5Tag); |
|
122 } |
|
123 |
|
124 void InsertParagraphSeparatorCommand::getAncestorsInsideBlock(const Node* insertionNode, Element* outerBlock, Vector<Element*>& ancestors) |
|
125 { |
|
126 ancestors.clear(); |
|
127 |
|
128 // Build up list of ancestors elements between the insertion node and the outer block. |
|
129 if (insertionNode != outerBlock) { |
|
130 for (Element* n = insertionNode->parentElement(); n && n != outerBlock; n = n->parentElement()) |
|
131 ancestors.append(n); |
|
132 } |
|
133 } |
|
134 |
|
135 PassRefPtr<Element> InsertParagraphSeparatorCommand::cloneHierarchyUnderNewBlock(const Vector<Element*>& ancestors, PassRefPtr<Element> blockToInsert) |
|
136 { |
|
137 // Make clones of ancestors in between the start node and the start block. |
|
138 RefPtr<Element> parent = blockToInsert; |
|
139 for (size_t i = ancestors.size(); i != 0; --i) { |
|
140 RefPtr<Element> child = ancestors[i - 1]->cloneElementWithoutChildren(); |
|
141 appendNode(child, parent); |
|
142 parent = child.release(); |
|
143 } |
|
144 |
|
145 return parent.release(); |
|
146 } |
|
147 |
|
148 void InsertParagraphSeparatorCommand::doApply() |
|
149 { |
|
150 bool splitText = false; |
|
151 if (endingSelection().isNone()) |
|
152 return; |
|
153 |
|
154 Position insertionPosition = endingSelection().start(); |
|
155 |
|
156 EAffinity affinity = endingSelection().affinity(); |
|
157 |
|
158 // Delete the current selection. |
|
159 if (endingSelection().isRange()) { |
|
160 calculateStyleBeforeInsertion(insertionPosition); |
|
161 deleteSelection(false, true); |
|
162 insertionPosition = endingSelection().start(); |
|
163 affinity = endingSelection().affinity(); |
|
164 } |
|
165 |
|
166 // FIXME: The rangeCompliantEquivalent conversion needs to be moved into enclosingBlock. |
|
167 Node* startBlockNode = enclosingBlock(rangeCompliantEquivalent(insertionPosition).node()); |
|
168 Position canonicalPos = VisiblePosition(insertionPosition).deepEquivalent(); |
|
169 Element* startBlock = static_cast<Element*>(startBlockNode); |
|
170 if (!startBlockNode |
|
171 || !startBlockNode->isElementNode() |
|
172 || !startBlock->parentNode() |
|
173 || isTableCell(startBlock) |
|
174 || startBlock->hasTagName(formTag) |
|
175 // FIXME: If the node is hidden, we don't have a canonical position so we will do the wrong thing for tables and <hr>. https://bugs.webkit.org/show_bug.cgi?id=40342 |
|
176 || (!canonicalPos.isNull() && canonicalPos.node()->renderer() && canonicalPos.node()->renderer()->isTable()) |
|
177 || (!canonicalPos.isNull() && canonicalPos.node()->hasTagName(hrTag))) { |
|
178 applyCommandToComposite(InsertLineBreakCommand::create(document())); |
|
179 return; |
|
180 } |
|
181 |
|
182 // Use the leftmost candidate. |
|
183 insertionPosition = insertionPosition.upstream(); |
|
184 if (!insertionPosition.isCandidate()) |
|
185 insertionPosition = insertionPosition.downstream(); |
|
186 |
|
187 // Adjust the insertion position after the delete |
|
188 insertionPosition = positionAvoidingSpecialElementBoundary(insertionPosition); |
|
189 VisiblePosition visiblePos(insertionPosition, affinity); |
|
190 calculateStyleBeforeInsertion(insertionPosition); |
|
191 |
|
192 //--------------------------------------------------------------------- |
|
193 // Handle special case of typing return on an empty list item |
|
194 if (breakOutOfEmptyListItem()) |
|
195 return; |
|
196 |
|
197 //--------------------------------------------------------------------- |
|
198 // Prepare for more general cases. |
|
199 |
|
200 bool isFirstInBlock = isStartOfBlock(visiblePos); |
|
201 bool isLastInBlock = isEndOfBlock(visiblePos); |
|
202 bool nestNewBlock = false; |
|
203 |
|
204 // Create block to be inserted. |
|
205 RefPtr<Element> blockToInsert; |
|
206 if (startBlock == startBlock->rootEditableElement()) { |
|
207 blockToInsert = createDefaultParagraphElement(document()); |
|
208 nestNewBlock = true; |
|
209 } else if (shouldUseDefaultParagraphElement(startBlock)) |
|
210 blockToInsert = createDefaultParagraphElement(document()); |
|
211 else |
|
212 blockToInsert = startBlock->cloneElementWithoutChildren(); |
|
213 |
|
214 //--------------------------------------------------------------------- |
|
215 // Handle case when position is in the last visible position in its block, |
|
216 // including when the block is empty. |
|
217 if (isLastInBlock) { |
|
218 if (nestNewBlock) { |
|
219 if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) { |
|
220 // The block is empty. Create an empty block to |
|
221 // represent the paragraph that we're leaving. |
|
222 RefPtr<Element> extraBlock = createDefaultParagraphElement(document()); |
|
223 appendNode(extraBlock, startBlock); |
|
224 appendBlockPlaceholder(extraBlock); |
|
225 } |
|
226 appendNode(blockToInsert, startBlock); |
|
227 } else { |
|
228 // We can get here if we pasted a copied portion of a blockquote with a newline at the end and are trying to paste it |
|
229 // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted. |
|
230 if (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) |
|
231 startBlock = static_cast<Element*>(highestBlockquote); |
|
232 |
|
233 // Most of the time we want to stay at the nesting level of the startBlock (e.g., when nesting within lists). However, |
|
234 // for div nodes, this can result in nested div tags that are hard to break out of. |
|
235 Element* siblingNode = startBlock; |
|
236 if (blockToInsert->hasTagName(divTag)) |
|
237 siblingNode = highestVisuallyEquivalentDivBelowRoot(startBlock); |
|
238 insertNodeAfter(blockToInsert, siblingNode); |
|
239 } |
|
240 |
|
241 // Recreate the same structure in the new paragraph. |
|
242 |
|
243 Vector<Element*> ancestors; |
|
244 getAncestorsInsideBlock(insertionPosition.node(), startBlock, ancestors); |
|
245 RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); |
|
246 |
|
247 appendBlockPlaceholder(parent); |
|
248 |
|
249 setEndingSelection(VisibleSelection(Position(parent.get(), 0), DOWNSTREAM)); |
|
250 return; |
|
251 } |
|
252 |
|
253 |
|
254 //--------------------------------------------------------------------- |
|
255 // Handle case when position is in the first visible position in its block, and |
|
256 // similar case where previous position is in another, presumeably nested, block. |
|
257 if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) { |
|
258 Node *refNode; |
|
259 if (isFirstInBlock && !nestNewBlock) |
|
260 refNode = startBlock; |
|
261 else if (insertionPosition.node() == startBlock && nestNewBlock) { |
|
262 refNode = startBlock->childNode(insertionPosition.deprecatedEditingOffset()); |
|
263 ASSERT(refNode); // must be true or we'd be in the end of block case |
|
264 } else |
|
265 refNode = insertionPosition.node(); |
|
266 |
|
267 // find ending selection position easily before inserting the paragraph |
|
268 insertionPosition = insertionPosition.downstream(); |
|
269 |
|
270 insertNodeBefore(blockToInsert, refNode); |
|
271 |
|
272 // Recreate the same structure in the new paragraph. |
|
273 |
|
274 Vector<Element*> ancestors; |
|
275 getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(insertionPosition).node(), startBlock, ancestors); |
|
276 |
|
277 appendBlockPlaceholder(cloneHierarchyUnderNewBlock(ancestors, blockToInsert)); |
|
278 |
|
279 // In this case, we need to set the new ending selection. |
|
280 setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM)); |
|
281 return; |
|
282 } |
|
283 |
|
284 //--------------------------------------------------------------------- |
|
285 // Handle the (more complicated) general case, |
|
286 |
|
287 // All of the content in the current block after visiblePos is |
|
288 // about to be wrapped in a new paragraph element. Add a br before |
|
289 // it if visiblePos is at the start of a paragraph so that the |
|
290 // content will move down a line. |
|
291 if (isStartOfParagraph(visiblePos)) { |
|
292 RefPtr<Element> br = createBreakElement(document()); |
|
293 insertNodeAt(br.get(), insertionPosition); |
|
294 insertionPosition = positionInParentAfterNode(br.get()); |
|
295 } |
|
296 |
|
297 // Move downstream. Typing style code will take care of carrying along the |
|
298 // style of the upstream position. |
|
299 insertionPosition = insertionPosition.downstream(); |
|
300 |
|
301 // At this point, the insertionPosition's node could be a container, and we want to make sure we include |
|
302 // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position |
|
303 // before we walk the DOM tree. |
|
304 insertionPosition = VisiblePosition(insertionPosition).deepEquivalent(); |
|
305 |
|
306 // Build up list of ancestors in between the start node and the start block. |
|
307 Vector<Element*> ancestors; |
|
308 getAncestorsInsideBlock(insertionPosition.node(), startBlock, ancestors); |
|
309 |
|
310 // Make sure we do not cause a rendered space to become unrendered. |
|
311 // FIXME: We need the affinity for pos, but pos.downstream() does not give it |
|
312 Position leadingWhitespace = insertionPosition.leadingWhitespacePosition(VP_DEFAULT_AFFINITY); |
|
313 // FIXME: leadingWhitespacePosition is returning the position before preserved newlines for positions |
|
314 // after the preserved newline, causing the newline to be turned into a nbsp. |
|
315 if (leadingWhitespace.isNotNull() && leadingWhitespace.node()->isTextNode()) { |
|
316 Text* textNode = static_cast<Text*>(leadingWhitespace.node()); |
|
317 ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); |
|
318 replaceTextInNode(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); |
|
319 } |
|
320 |
|
321 // Split at pos if in the middle of a text node. |
|
322 if (insertionPosition.node()->isTextNode()) { |
|
323 Text* textNode = static_cast<Text*>(insertionPosition.node()); |
|
324 bool atEnd = (unsigned)insertionPosition.deprecatedEditingOffset() >= textNode->length(); |
|
325 if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) { |
|
326 splitTextNode(textNode, insertionPosition.deprecatedEditingOffset()); |
|
327 insertionPosition.moveToOffset(0); |
|
328 visiblePos = VisiblePosition(insertionPosition); |
|
329 splitText = true; |
|
330 } |
|
331 } |
|
332 |
|
333 // Put the added block in the tree. |
|
334 if (nestNewBlock) |
|
335 appendNode(blockToInsert.get(), startBlock); |
|
336 else |
|
337 insertNodeAfter(blockToInsert.get(), startBlock); |
|
338 |
|
339 updateLayout(); |
|
340 |
|
341 // Make clones of ancestors in between the start node and the outer block. |
|
342 RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); |
|
343 |
|
344 // If the paragraph separator was inserted at the end of a paragraph, an empty line must be |
|
345 // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph |
|
346 // element. If the first node to be inserted won't be one that will hold an empty line open, add a br. |
|
347 if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) |
|
348 appendNode(createBreakElement(document()).get(), blockToInsert.get()); |
|
349 |
|
350 // Move the start node and the siblings of the start node. |
|
351 if (insertionPosition.node() != startBlock) { |
|
352 Node* n = insertionPosition.node(); |
|
353 if (insertionPosition.deprecatedEditingOffset() >= caretMaxOffset(n)) |
|
354 n = n->nextSibling(); |
|
355 |
|
356 while (n && n != blockToInsert) { |
|
357 Node *next = n->nextSibling(); |
|
358 removeNode(n); |
|
359 appendNode(n, parent.get()); |
|
360 n = next; |
|
361 } |
|
362 } |
|
363 |
|
364 // Move everything after the start node. |
|
365 if (!ancestors.isEmpty()) { |
|
366 Element* leftParent = ancestors.first(); |
|
367 while (leftParent && leftParent != startBlock) { |
|
368 parent = parent->parentElement(); |
|
369 if (!parent) |
|
370 break; |
|
371 Node* n = leftParent->nextSibling(); |
|
372 while (n && n != blockToInsert) { |
|
373 Node* next = n->nextSibling(); |
|
374 removeNode(n); |
|
375 appendNode(n, parent.get()); |
|
376 n = next; |
|
377 } |
|
378 leftParent = leftParent->parentElement(); |
|
379 } |
|
380 } |
|
381 |
|
382 // Handle whitespace that occurs after the split |
|
383 if (splitText) { |
|
384 updateLayout(); |
|
385 insertionPosition = Position(insertionPosition.node(), 0); |
|
386 if (!insertionPosition.isRenderedCharacter()) { |
|
387 // Clear out all whitespace and insert one non-breaking space |
|
388 ASSERT(!insertionPosition.node()->renderer() || insertionPosition.node()->renderer()->style()->collapseWhiteSpace()); |
|
389 deleteInsignificantTextDownstream(insertionPosition); |
|
390 if (insertionPosition.node()->isTextNode()) |
|
391 insertTextIntoNode(static_cast<Text*>(insertionPosition.node()), 0, nonBreakingSpaceString()); |
|
392 } |
|
393 } |
|
394 |
|
395 setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM)); |
|
396 applyStyleAfterInsertion(startBlock); |
|
397 } |
|
398 |
|
399 } // namespace WebCore |