blob: f5dd5ef44f3e73de7810653ceb5a8e812445dc64 [file] [log] [blame]
mjsa0fe4042005-05-13 08:37:15 +00001/*
2 * Copyright (C) 2005 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 "delete_selection_command.h"
27
28#include "css/css_computedstyle.h"
29#include "htmlediting.h"
30#include "khtml_part.h"
hyatt59136b72005-07-09 20:19:28 +000031#include "htmlnames.h"
mjsa0fe4042005-05-13 08:37:15 +000032#include "rendering/render_line.h"
33#include "rendering/render_object.h"
34#include "visible_text.h"
35#include "visible_units.h"
36#include "xml/dom2_rangeimpl.h"
37#include "xml/dom_position.h"
38#include "xml/dom_textimpl.h"
39
40
41#if APPLE_CHANGES
42#include "KWQAssertions.h"
43#include "KWQLogging.h"
44#else
45#define ASSERT(assertion) assert(assertion)
46#define LOG(channel, formatAndArgs...) ((void)0)
47#endif
48
49using DOM::CSSComputedStyleDeclarationImpl;
50using DOM::DOMString;
51using DOM::DocumentImpl;
52using DOM::NodeImpl;
53using DOM::Position;
54using DOM::RangeImpl;
55using DOM::TextImpl;
hyatt59136b72005-07-09 20:19:28 +000056using DOM::HTMLNames;
mjsa0fe4042005-05-13 08:37:15 +000057
58namespace khtml {
59
60static bool isListStructureNode(const NodeImpl *node)
61{
62 // FIXME: Irritating that we can get away with just going at the render tree for isTableStructureNode,
63 // but here we also have to peek at the type of DOM node?
64 RenderObject *r = node->renderer();
mjsa0fe4042005-05-13 08:37:15 +000065 return (r && r->isListItem())
hyatt59136b72005-07-09 20:19:28 +000066 || node->hasTagName(HTMLNames::ol())
67 || node->hasTagName(HTMLNames::ul())
68 || node->hasTagName(HTMLNames::dd())
69 || node->hasTagName(HTMLNames::dt())
70 || node->hasTagName(HTMLNames::dir())
71 || node->hasTagName(HTMLNames::menu());
mjsa0fe4042005-05-13 08:37:15 +000072}
73
74static int maxDeepOffset(NodeImpl *n)
75{
76 if (n->isAtomicNode())
77 return n->caretMaxOffset();
78
79 if (n->isElementNode())
80 return n->childNodeCount();
81
82 return 1;
83}
84
85static void debugPosition(const char *prefix, const Position &pos)
86{
87 if (!prefix)
88 prefix = "";
89 if (pos.isNull())
90 LOG(Editing, "%s <null>", prefix);
91 else
92 LOG(Editing, "%s%s %p : %d", prefix, pos.node()->nodeName().string().latin1(), pos.node(), pos.offset());
93}
94
95static void debugNode(const char *prefix, const NodeImpl *node)
96{
97 if (!prefix)
98 prefix = "";
99 if (!node)
100 LOG(Editing, "%s <null>", prefix);
101 else
102 LOG(Editing, "%s%s %p", prefix, node->nodeName().string().latin1(), node);
103}
104
105static Position positionBeforePossibleContainingSpecialElement(const Position &pos)
106{
107 if (isFirstVisiblePositionInSpecialElement(pos)) {
108 return positionBeforeContainingSpecialElement(pos);
109 }
110
111 return pos;
112}
113
114static Position positionAfterPossibleContainingSpecialElement(const Position &pos)
115{
116 if (isLastVisiblePositionInSpecialElement(pos)) {
117 return positionAfterContainingSpecialElement(pos);
118 }
119
120 return pos;
121}
122
123DeleteSelectionCommand::DeleteSelectionCommand(DocumentImpl *document, bool smartDelete, bool mergeBlocksAfterDelete)
124 : CompositeEditCommand(document),
125 m_hasSelectionToDelete(false),
126 m_smartDelete(smartDelete),
127 m_mergeBlocksAfterDelete(mergeBlocksAfterDelete),
128 m_startBlock(0),
129 m_endBlock(0),
130 m_startNode(0),
131 m_typingStyle(0)
132{
133}
134
135DeleteSelectionCommand::DeleteSelectionCommand(DocumentImpl *document, const Selection &selection, bool smartDelete, bool mergeBlocksAfterDelete)
136 : CompositeEditCommand(document),
137 m_hasSelectionToDelete(true),
138 m_smartDelete(smartDelete),
139 m_mergeBlocksAfterDelete(mergeBlocksAfterDelete),
140 m_selectionToDelete(selection),
141 m_startBlock(0),
142 m_endBlock(0),
143 m_startNode(0),
144 m_typingStyle(0)
145{
146}
147
148void DeleteSelectionCommand::initializePositionData()
149{
150 //
151 // Handle setting some basic positions
152 //
153 Position start = m_selectionToDelete.start();
154 start = positionOutsideContainingSpecialElement(start);
155 Position end = m_selectionToDelete.end();
156 end = positionOutsideContainingSpecialElement(end);
157
158 m_upstreamStart = positionBeforePossibleContainingSpecialElement(start.upstream());
159 m_downstreamStart = positionBeforePossibleContainingSpecialElement(start.downstream());
160 m_upstreamEnd = positionAfterPossibleContainingSpecialElement(end.upstream());
161 m_downstreamEnd = positionAfterPossibleContainingSpecialElement(end.downstream());
162
163 //
164 // Handle leading and trailing whitespace, as well as smart delete adjustments to the selection
165 //
166 m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.startAffinity());
167 m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY);
168
169 if (m_smartDelete) {
170
171 // skip smart delete if the selection to delete already starts or ends with whitespace
172 Position pos = VisiblePosition(m_upstreamStart, m_selectionToDelete.startAffinity()).deepEquivalent();
173 bool skipSmartDelete = pos.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull();
174 if (!skipSmartDelete)
175 skipSmartDelete = m_downstreamEnd.leadingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull();
176
177 // extend selection upstream if there is whitespace there
178 bool hasLeadingWhitespaceBeforeAdjustment = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.startAffinity(), true).isNotNull();
179 if (!skipSmartDelete && hasLeadingWhitespaceBeforeAdjustment) {
180 VisiblePosition visiblePos = VisiblePosition(start, m_selectionToDelete.startAffinity()).previous();
181 pos = visiblePos.deepEquivalent();
182 // Expand out one character upstream for smart delete and recalculate
183 // positions based on this change.
184 m_upstreamStart = pos.upstream();
185 m_downstreamStart = pos.downstream();
186 m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(visiblePos.affinity());
187 }
188
189 // trailing whitespace is only considered for smart delete if there is no leading
190 // whitespace, as in the case where you double-click the first word of a paragraph.
191 if (!skipSmartDelete && !hasLeadingWhitespaceBeforeAdjustment && m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull()) {
192 // Expand out one character downstream for smart delete and recalculate
193 // positions based on this change.
194 pos = VisiblePosition(end, m_selectionToDelete.endAffinity()).next().deepEquivalent();
195 m_upstreamEnd = pos.upstream();
196 m_downstreamEnd = pos.downstream();
197 m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY);
198 }
199 }
200
201 m_trailingWhitespaceValid = true;
202
203 //
204 // Handle setting start and end blocks and the start node.
205 //
206 m_startBlock = m_downstreamStart.node()->enclosingBlockFlowElement();
207 m_startBlock->ref();
208 m_endBlock = m_upstreamEnd.node()->enclosingBlockFlowElement();
209 m_endBlock->ref();
210 m_startNode = m_upstreamStart.node();
211 m_startNode->ref();
212
213 //
214 // Handle detecting if the line containing the selection end is itself fully selected.
215 // This is one of the tests that determines if block merging of content needs to be done.
216 //
217 VisiblePosition visibleEnd(end, m_selectionToDelete.endAffinity());
218 if (isStartOfParagraph(visibleEnd) || isEndOfParagraph(visibleEnd)) {
219 Position previousLineStart = previousLinePosition(visibleEnd, 0).deepEquivalent();
220 if (previousLineStart.isNull() || RangeImpl::compareBoundaryPoints(previousLineStart, m_downstreamStart) >= 0)
221 m_mergeBlocksAfterDelete = false;
222 }
223
224 debugPosition("m_upstreamStart ", m_upstreamStart);
225 debugPosition("m_downstreamStart ", m_downstreamStart);
226 debugPosition("m_upstreamEnd ", m_upstreamEnd);
227 debugPosition("m_downstreamEnd ", m_downstreamEnd);
228 debugPosition("m_leadingWhitespace ", m_leadingWhitespace);
229 debugPosition("m_trailingWhitespace ", m_trailingWhitespace);
230 debugNode( "m_startBlock ", m_startBlock);
231 debugNode( "m_endBlock ", m_endBlock);
232 debugNode( "m_startNode ", m_startNode);
233}
234
235void DeleteSelectionCommand::insertPlaceholderForAncestorBlockContent()
236{
237 // This code makes sure a line does not disappear when deleting in this case:
238 // <p>foo</p>bar<p>baz</p>
239 // Select "bar" and hit delete. If nothing is done, the line containing bar will disappear.
240 // It needs to be held open by inserting a placeholder.
241 // Also see:
242 // <rdar://problem/3928305> selecting an entire line and typing over causes new inserted text at top of document
243 //
244 // The checks below detect the case where the selection contains content in an ancestor block
245 // surrounded by child blocks.
246 //
247 VisiblePosition visibleStart(m_upstreamStart, VP_DEFAULT_AFFINITY);
248 VisiblePosition beforeStart = visibleStart.previous();
249 NodeImpl *startBlock = enclosingBlockFlowElement(visibleStart);
250 NodeImpl *beforeStartBlock = enclosingBlockFlowElement(beforeStart);
251
252 if (!beforeStart.isNull() &&
253 !inSameBlock(visibleStart, beforeStart) &&
254 beforeStartBlock->isAncestor(startBlock) &&
255 startBlock != m_upstreamStart.node()) {
256
257 VisiblePosition visibleEnd(m_downstreamEnd, VP_DEFAULT_AFFINITY);
258 VisiblePosition afterEnd = visibleEnd.next();
259
260 if ((!afterEnd.isNull() && !inSameBlock(afterEnd, visibleEnd) && !inSameBlock(afterEnd, visibleStart)) ||
261 (m_downstreamEnd == m_selectionToDelete.end() && isEndOfParagraph(visibleEnd))) {
262 NodeImpl *block = createDefaultParagraphElement(document());
263 insertNodeBefore(block, m_upstreamStart.node());
264 addBlockPlaceholderIfNeeded(block);
265 m_endingPosition = Position(block, 0);
266 }
267 }
268}
269
270void DeleteSelectionCommand::saveTypingStyleState()
271{
272 // Figure out the typing style in effect before the delete is done.
273 // FIXME: Improve typing style.
274 // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
adele945819d2005-07-05 23:21:31 +0000275 CSSComputedStyleDeclarationImpl *computedStyle = m_selectionToDelete.start().computedStyle();
mjsa0fe4042005-05-13 08:37:15 +0000276 computedStyle->ref();
277 m_typingStyle = computedStyle->copyInheritableProperties();
278 m_typingStyle->ref();
279 computedStyle->deref();
280}
281
282bool DeleteSelectionCommand::handleSpecialCaseBRDelete()
283{
284 // Check for special-case where the selection contains only a BR on a line by itself after another BR.
hyatt59136b72005-07-09 20:19:28 +0000285 bool upstreamStartIsBR = m_startNode->hasTagName(HTMLNames::br());
286 bool downstreamStartIsBR = m_downstreamStart.node()->hasTagName(HTMLNames::br());
mjsa0fe4042005-05-13 08:37:15 +0000287 bool isBROnLineByItself = upstreamStartIsBR && downstreamStartIsBR && m_downstreamStart.node() == m_upstreamEnd.node();
288 if (isBROnLineByItself) {
harrisone4b84c52005-07-18 18:14:59 +0000289 m_endingPosition = Position(m_downstreamStart.node()->parentNode(), m_downstreamStart.node()->nodeIndex());
mjsa0fe4042005-05-13 08:37:15 +0000290 removeNode(m_downstreamStart.node());
harrisone4b84c52005-07-18 18:14:59 +0000291 m_endingPosition = m_endingPosition.equivalentDeepPosition();
mjsa0fe4042005-05-13 08:37:15 +0000292 m_mergeBlocksAfterDelete = false;
293 return true;
294 }
295
296 // Not a special-case delete per se, but we can detect that the merging of content between blocks
297 // should not be done.
298 if (upstreamStartIsBR && downstreamStartIsBR)
299 m_mergeBlocksAfterDelete = false;
300
301 return false;
302}
303
304void DeleteSelectionCommand::setStartNode(NodeImpl *node)
305{
306 NodeImpl *old = m_startNode;
307 m_startNode = node;
308 if (m_startNode)
309 m_startNode->ref();
310 if (old)
311 old->deref();
312}
313
314void DeleteSelectionCommand::handleGeneralDelete()
315{
316 int startOffset = m_upstreamStart.offset();
317 VisiblePosition visibleEnd = VisiblePosition(m_downstreamEnd, m_selectionToDelete.endAffinity());
318 bool endAtEndOfBlock = isEndOfBlock(visibleEnd);
319
320 // Handle some special cases where the selection begins and ends on specific visible units.
321 // Sometimes a node that is actually selected needs to be retained in order to maintain
322 // user expectations for the delete operation. Here is an example:
323 // 1. Open a new Blot or Mail document
324 // 2. hit Return ten times or so
325 // 3. Type a letter (do not hit Return after it)
326 // 4. Type shift-up-arrow to select the line containing the letter and the previous blank line
327 // 5. Hit Delete
328 // You expect the insertion point to wind up at the start of the line where your selection began.
329 // Because of the nature of HTML, the editing code needs to perform a special check to get
330 // this behavior. So:
331 // If the entire start block is selected, and the selection does not extend to the end of the
332 // end of a block other than the block containing the selection start, then do not delete the
333 // start block, otherwise delete the start block.
334 // A similar case is provided to cover selections starting in BR elements.
hyatt59136b72005-07-09 20:19:28 +0000335 if (startOffset == 1 && m_startNode && m_startNode->hasTagName(HTMLNames::br())) {
mjsa0fe4042005-05-13 08:37:15 +0000336 setStartNode(m_startNode->traverseNextNode());
337 startOffset = 0;
338 }
hyatt59136b72005-07-09 20:19:28 +0000339 if (m_startBlock != m_endBlock && startOffset == 0 && m_startNode && m_startNode->hasTagName(HTMLNames::br()) && endAtEndOfBlock) {
mjsa0fe4042005-05-13 08:37:15 +0000340 // Don't delete the BR element
341 setStartNode(m_startNode->traverseNextNode());
342 }
343 else if (m_startBlock != m_endBlock && isStartOfBlock(VisiblePosition(m_upstreamStart, m_selectionToDelete.startAffinity()))) {
344 if (!m_startBlock->isAncestor(m_endBlock) && !isStartOfBlock(visibleEnd) && endAtEndOfBlock) {
345 // Delete all the children of the block, but not the block itself.
346 setStartNode(m_startBlock->firstChild());
347 startOffset = 0;
348 }
349 }
350 else if (startOffset >= m_startNode->caretMaxOffset() &&
351 (m_startNode->isAtomicNode() || startOffset == 0)) {
352 // Move the start node to the next node in the tree since the startOffset is equal to
353 // or beyond the start node's caretMaxOffset This means there is nothing visible to delete.
354 // But don't do this if the node is not atomic - we don't want to move into the first child.
355
356 // Also, before moving on, delete any insignificant text that may be present in a text node.
357 if (m_startNode->isTextNode()) {
358 // Delete any insignificant text from this node.
359 TextImpl *text = static_cast<TextImpl *>(m_startNode);
360 if (text->length() > (unsigned)m_startNode->caretMaxOffset())
361 deleteTextFromNode(text, m_startNode->caretMaxOffset(), text->length() - m_startNode->caretMaxOffset());
362 }
363
364 // shift the start node to the next
365 setStartNode(m_startNode->traverseNextNode());
366 startOffset = 0;
367 }
368
369 // Done adjusting the start. See if we're all done.
370 if (!m_startNode)
371 return;
372
373 if (m_startNode == m_downstreamEnd.node()) {
374 // The selection to delete is all in one node.
375 if (!m_startNode->renderer() ||
376 (startOffset == 0 && m_downstreamEnd.offset() >= maxDeepOffset(m_startNode))) {
377 // just delete
378 removeFullySelectedNode(m_startNode);
379 } else if (m_downstreamEnd.offset() - startOffset > 0) {
380 if (m_startNode->isTextNode()) {
381 // in a text node that needs to be trimmed
382 TextImpl *text = static_cast<TextImpl *>(m_startNode);
383 deleteTextFromNode(text, startOffset, m_downstreamEnd.offset() - startOffset);
384 m_trailingWhitespaceValid = false;
385 } else {
386 removeChildrenInRange(m_startNode, startOffset, m_downstreamEnd.offset());
387 m_endingPosition = m_upstreamStart;
388 }
389 }
390 }
391 else {
392 // The selection to delete spans more than one node.
393 NodeImpl *node = m_startNode;
394
395 if (startOffset > 0) {
396 if (m_startNode->isTextNode()) {
397 // in a text node that needs to be trimmed
398 TextImpl *text = static_cast<TextImpl *>(node);
399 deleteTextFromNode(text, startOffset, text->length() - startOffset);
400 node = node->traverseNextNode();
401 } else {
402 node = m_startNode->childNode(startOffset);
403 }
404 }
405
406 // handle deleting all nodes that are completely selected
407 while (node && node != m_downstreamEnd.node()) {
408 if (RangeImpl::compareBoundaryPoints(Position(node, 0), m_downstreamEnd) >= 0) {
409 // traverseNextSibling just blew past the end position, so stop deleting
410 node = 0;
411 } else if (!m_downstreamEnd.node()->isAncestor(node)) {
412 NodeImpl *nextNode = node->traverseNextSibling();
413 // if we just removed a node from the end container, update end position so the
414 // check above will work
415 if (node->parentNode() == m_downstreamEnd.node()) {
416 ASSERT(node->nodeIndex() < (unsigned)m_downstreamEnd.offset());
417 m_downstreamEnd = Position(m_downstreamEnd.node(), m_downstreamEnd.offset() - 1);
418 }
419 removeFullySelectedNode(node);
420 node = nextNode;
421 } else {
422 NodeImpl *n = node->lastChild();
423 while (n && n->lastChild())
424 n = n->lastChild();
425 if (n == m_downstreamEnd.node() && m_downstreamEnd.offset() >= m_downstreamEnd.node()->caretMaxOffset()) {
426 removeFullySelectedNode(node);
427 m_trailingWhitespaceValid = false;
428 node = 0;
429 }
430 else {
431 node = node->traverseNextNode();
432 }
433 }
434 }
435
436
437 if (m_downstreamEnd.node() != m_startNode && !m_upstreamStart.node()->isAncestor(m_downstreamEnd.node()) && m_downstreamEnd.node()->inDocument() && m_downstreamEnd.offset() >= m_downstreamEnd.node()->caretMinOffset()) {
438 if (m_downstreamEnd.offset() >= maxDeepOffset(m_downstreamEnd.node())) {
439 // need to delete whole node
440 // we can get here if this is the last node in the block
441 // remove an ancestor of m_downstreamEnd.node(), and thus m_downstreamEnd.node() itself
442 if (!m_upstreamStart.node()->inDocument() ||
443 m_upstreamStart.node() == m_downstreamEnd.node() ||
444 m_upstreamStart.node()->isAncestor(m_downstreamEnd.node())) {
445 m_upstreamStart = Position(m_downstreamEnd.node()->parentNode(), m_downstreamEnd.node()->nodeIndex());
446 }
447
448 removeFullySelectedNode(m_downstreamEnd.node());
449 m_trailingWhitespaceValid = false;
450 } else {
451 if (m_downstreamEnd.node()->isTextNode()) {
452 // in a text node that needs to be trimmed
453 TextImpl *text = static_cast<TextImpl *>(m_downstreamEnd.node());
454 if (m_downstreamEnd.offset() > 0) {
455 deleteTextFromNode(text, 0, m_downstreamEnd.offset());
456 m_downstreamEnd = Position(text, 0);
457 m_trailingWhitespaceValid = false;
458 }
459 } else {
460 int offset = 0;
461 if (m_upstreamStart.node()->isAncestor(m_downstreamEnd.node())) {
462 NodeImpl *n = m_upstreamStart.node();
463 while (n && n->parentNode() != m_downstreamEnd.node())
464 n = n->parentNode();
465 if (n)
466 offset = n->nodeIndex() + 1;
467 }
468 removeChildrenInRange(m_downstreamEnd.node(), offset, m_downstreamEnd.offset());
469 m_downstreamEnd = Position(m_downstreamEnd.node(), offset);
470 }
471 }
472 }
473 }
474}
475
mjsa0fe4042005-05-13 08:37:15 +0000476// FIXME: Can't really determine this without taking white-space mode into account.
477static inline bool nextCharacterIsCollapsibleWhitespace(const Position &pos)
478{
479 if (!pos.node())
480 return false;
481 if (!pos.node()->isTextNode())
482 return false;
483 return isCollapsibleWhitespace(static_cast<TextImpl *>(pos.node())->data()[pos.offset()]);
484}
485
486void DeleteSelectionCommand::fixupWhitespace()
487{
488 document()->updateLayout();
489 if (m_leadingWhitespace.isNotNull() && (m_trailingWhitespace.isNotNull() || !m_leadingWhitespace.isRenderedCharacter())) {
490 LOG(Editing, "replace leading");
491 TextImpl *textNode = static_cast<TextImpl *>(m_leadingWhitespace.node());
492 replaceTextInNode(textNode, m_leadingWhitespace.offset(), 1, nonBreakingSpaceString());
493 }
494 else if (m_trailingWhitespace.isNotNull()) {
495 if (m_trailingWhitespaceValid) {
496 if (!m_trailingWhitespace.isRenderedCharacter()) {
497 LOG(Editing, "replace trailing [valid]");
498 TextImpl *textNode = static_cast<TextImpl *>(m_trailingWhitespace.node());
499 replaceTextInNode(textNode, m_trailingWhitespace.offset(), 1, nonBreakingSpaceString());
500 }
501 }
502 else {
503 Position pos = m_endingPosition.downstream();
504 pos = Position(pos.node(), pos.offset() - 1);
505 if (nextCharacterIsCollapsibleWhitespace(pos) && !pos.isRenderedCharacter()) {
506 LOG(Editing, "replace trailing [invalid]");
507 TextImpl *textNode = static_cast<TextImpl *>(pos.node());
508 replaceTextInNode(textNode, pos.offset(), 1, nonBreakingSpaceString());
509 // need to adjust ending position since the trailing position is not valid.
510 m_endingPosition = pos;
511 }
512 }
513 }
514}
515
516// This function moves nodes in the block containing startNode to dstBlock, starting
517// from startNode and proceeding to the end of the paragraph. Nodes in the block containing
518// startNode that appear in document order before startNode are not moved.
519// This function is an important helper for deleting selections that cross paragraph
520// boundaries.
521void DeleteSelectionCommand::moveNodesAfterNode()
522{
523 if (!m_mergeBlocksAfterDelete)
524 return;
525
526 if (m_endBlock == m_startBlock)
527 return;
528
529 NodeImpl *startNode = m_downstreamEnd.node();
530 NodeImpl *dstNode = m_upstreamStart.node();
531
532 if (!startNode->inDocument() || !dstNode->inDocument())
533 return;
534
535 NodeImpl *startBlock = startNode->enclosingBlockFlowElement();
536 if (isTableStructureNode(startBlock) || isListStructureNode(startBlock))
537 // Do not move content between parts of a table or list.
538 return;
539
540 // Now that we are about to add content, check to see if a placeholder element
541 // can be removed.
542 removeBlockPlaceholder(startBlock);
543
544 // Move the subtree containing node
545 NodeImpl *node = startNode->enclosingInlineElement();
546
547 // Insert after the subtree containing destNode
548 NodeImpl *refNode = dstNode->enclosingInlineElement();
549
550 // Nothing to do if start is already at the beginning of dstBlock
551 NodeImpl *dstBlock = refNode->enclosingBlockFlowElement();
552 if (startBlock == dstBlock->firstChild())
553 return;
554
555 // Do the move.
556 NodeImpl *rootNode = refNode->rootEditableElement();
557 while (node && node->isAncestor(startBlock)) {
558 NodeImpl *moveNode = node;
559 node = node->nextSibling();
560 removeNode(moveNode);
hyatt59136b72005-07-09 20:19:28 +0000561 if (moveNode->hasTagName(HTMLNames::br()) && !moveNode->renderer()) {
mjsa0fe4042005-05-13 08:37:15 +0000562 // Just remove this node, and don't put it back.
563 // If the BR was not rendered (since it was at the end of a block, for instance),
564 // putting it back in the document might make it appear, and that is not desirable.
565 break;
566 }
567 if (refNode == rootNode)
568 insertNodeAt(moveNode, refNode, 0);
569 else
570 insertNodeAfter(moveNode, refNode);
571 refNode = moveNode;
hyatt59136b72005-07-09 20:19:28 +0000572 if (moveNode->hasTagName(HTMLNames::br()))
mjsa0fe4042005-05-13 08:37:15 +0000573 break;
574 }
575
576 // If the startBlock no longer has any kids, we may need to deal with adding a BR
577 // to make the layout come out right. Consider this document:
578 //
579 // One
580 // <div>Two</div>
581 // Three
582 //
583 // Placing the insertion before before the 'T' of 'Two' and hitting delete will
584 // move the contents of the div to the block containing 'One' and delete the div.
585 // This will have the side effect of moving 'Three' on to the same line as 'One'
586 // and 'Two'. This is undesirable. We fix this up by adding a BR before the 'Three'.
587 // This may not be ideal, but it is better than nothing.
588 document()->updateLayout();
589 if (!startBlock->renderer() || !startBlock->renderer()->firstChild()) {
590 removeNode(startBlock);
591 document()->updateLayout();
592 if (refNode->renderer() && refNode->renderer()->inlineBox() && refNode->renderer()->inlineBox()->nextOnLineExists()) {
593 insertNodeAfter(createBreakElement(document()), refNode);
594 }
595 }
596}
597
598void DeleteSelectionCommand::calculateEndingPosition()
599{
600 if (m_endingPosition.isNotNull() && m_endingPosition.node()->inDocument())
601 return;
602
603 m_endingPosition = m_upstreamStart;
604 if (m_endingPosition.node()->inDocument())
605 return;
606
607 m_endingPosition = m_downstreamEnd;
608 if (m_endingPosition.node()->inDocument())
609 return;
610
611 m_endingPosition = Position(m_startBlock, 0);
612 if (m_endingPosition.node()->inDocument())
613 return;
614
615 m_endingPosition = Position(m_endBlock, 0);
616 if (m_endingPosition.node()->inDocument())
617 return;
618
619 m_endingPosition = Position(document()->documentElement(), 0);
620}
621
622void DeleteSelectionCommand::calculateTypingStyleAfterDelete(NodeImpl *insertedPlaceholder)
623{
624 // Compute the difference between the style before the delete and the style now
625 // after the delete has been done. Set this style on the part, so other editing
626 // commands being composed with this one will work, and also cache it on the command,
627 // so the KHTMLPart::appliedEditing can set it after the whole composite command
628 // has completed.
629 // FIXME: Improve typing style.
630 // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
631 CSSComputedStyleDeclarationImpl endingStyle(m_endingPosition.node());
632 endingStyle.diff(m_typingStyle);
633 if (!m_typingStyle->length()) {
634 m_typingStyle->deref();
635 m_typingStyle = 0;
636 }
637 if (insertedPlaceholder && m_typingStyle) {
638 // Apply style to the placeholder. This makes sure that the single line in the
639 // paragraph has the right height, and that the paragraph takes on the style
640 // of the preceding line and retains it even if you click away, click back, and
641 // then start typing. In this case, the typing style is applied right now, and
642 // is not retained until the next typing action.
643
644 // FIXME: is this even right? I don't think post-deletion typing style is supposed
645 // to be saved across clicking away and clicking back, it certainly isn't in TextEdit
646
647 Position pastPlaceholder(insertedPlaceholder, 1);
648
649 setEndingSelection(Selection(m_endingPosition, m_selectionToDelete.endAffinity(), pastPlaceholder, DOWNSTREAM));
650
651 applyStyle(m_typingStyle, EditActionUnspecified);
652
653 m_typingStyle->deref();
654 m_typingStyle = 0;
655 }
656 // Set m_typingStyle as the typing style.
657 // It's perfectly OK for m_typingStyle to be null.
658 document()->part()->setTypingStyle(m_typingStyle);
659 setTypingStyle(m_typingStyle);
660}
661
662void DeleteSelectionCommand::clearTransientState()
663{
664 m_selectionToDelete.clear();
665 m_upstreamStart.clear();
666 m_downstreamStart.clear();
667 m_upstreamEnd.clear();
668 m_downstreamEnd.clear();
669 m_endingPosition.clear();
670 m_leadingWhitespace.clear();
671 m_trailingWhitespace.clear();
672
673 if (m_startBlock) {
674 m_startBlock->deref();
675 m_startBlock = 0;
676 }
677 if (m_endBlock) {
678 m_endBlock->deref();
679 m_endBlock = 0;
680 }
681 if (m_startNode) {
682 m_startNode->deref();
683 m_startNode = 0;
684 }
685 if (m_typingStyle) {
686 m_typingStyle->deref();
687 m_typingStyle = 0;
688 }
689}
690
691void DeleteSelectionCommand::doApply()
692{
693 // If selection has not been set to a custom selection when the command was created,
694 // use the current ending selection.
695 if (!m_hasSelectionToDelete)
696 m_selectionToDelete = endingSelection();
697
698 if (!m_selectionToDelete.isRange())
699 return;
700
701 // save this to later make the selection with
702 EAffinity affinity = m_selectionToDelete.startAffinity();
703
704 // set up our state
705 initializePositionData();
mjsa0fe4042005-05-13 08:37:15 +0000706 if (!m_startBlock || !m_endBlock) {
707 // Can't figure out what blocks we're in. This can happen if
708 // the document structure is not what we are expecting, like if
709 // the document has no body element, or if the editable block
710 // has been changed to display: inline. Some day it might
711 // be nice to be able to deal with this, but for now, bail.
712 clearTransientState();
713 return;
714 }
715
harrisone4b84c52005-07-18 18:14:59 +0000716 // deleting just a BR is handled specially, at least because we do not
717 // want to replace it with a placeholder BR!
718 if (handleSpecialCaseBRDelete()) {
719 calculateTypingStyleAfterDelete(false);
720 debugPosition("endingPosition ", m_endingPosition);
721 setEndingSelection(Selection(m_endingPosition, affinity));
722 clearTransientState();
723 rebalanceWhitespace();
724 return;
725 }
726
mjsa0fe4042005-05-13 08:37:15 +0000727 // if all we are deleting is complete paragraph(s), we need to make
728 // sure a blank paragraph remains when we are done
729 bool forceBlankParagraph = isStartOfParagraph(VisiblePosition(m_upstreamStart, VP_DEFAULT_AFFINITY)) &&
730 isEndOfParagraph(VisiblePosition(m_downstreamEnd, VP_DEFAULT_AFFINITY));
731
732 // Delete any text that may hinder our ability to fixup whitespace after the detele
733 deleteInsignificantTextDownstream(m_trailingWhitespace);
734
735 saveTypingStyleState();
mjsa0fe4042005-05-13 08:37:15 +0000736
harrisone4b84c52005-07-18 18:14:59 +0000737 insertPlaceholderForAncestorBlockContent();
738 handleGeneralDelete();
mjsa0fe4042005-05-13 08:37:15 +0000739
740 // Do block merge if start and end of selection are in different blocks.
741 moveNodesAfterNode();
742
743 calculateEndingPosition();
744 fixupWhitespace();
745
746 // if the m_endingPosition is already a blank paragraph, there is
747 // no need to force a new one
748 if (forceBlankParagraph &&
749 isStartOfParagraph(VisiblePosition(m_endingPosition, VP_DEFAULT_AFFINITY)) &&
750 isEndOfParagraph(VisiblePosition(m_endingPosition, VP_DEFAULT_AFFINITY))) {
751 forceBlankParagraph = false;
752 }
753
754 NodeImpl *addedPlaceholder = forceBlankParagraph ? insertBlockPlaceholder(m_endingPosition) :
755 addBlockPlaceholderIfNeeded(m_endingPosition.node());
756
757 calculateTypingStyleAfterDelete(addedPlaceholder);
harrisone4b84c52005-07-18 18:14:59 +0000758
mjsa0fe4042005-05-13 08:37:15 +0000759 debugPosition("endingPosition ", m_endingPosition);
760 setEndingSelection(Selection(m_endingPosition, affinity));
761 clearTransientState();
762 rebalanceWhitespace();
763}
764
765EditAction DeleteSelectionCommand::editingAction() const
766{
767 // Note that DeleteSelectionCommand is also used when the user presses the Delete key,
768 // but in that case there's a TypingCommand that supplies the editingAction(), so
769 // the Undo menu correctly shows "Undo Typing"
770 return EditActionCut;
771}
772
773bool DeleteSelectionCommand::preservesTypingStyle() const
774{
775 return true;
776}
777
778} // namespace khtml