blob: 6a3e65805574e00ee39eb84da610c2d8e4ccd25d [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
mjsb64c50a2005-10-03 21:13:12 +000026#include "config.h"
mjsa0fe4042005-05-13 08:37:15 +000027#include "delete_selection_command.h"
28
29#include "css/css_computedstyle.h"
30#include "htmlediting.h"
31#include "khtml_part.h"
hyatt59136b72005-07-09 20:19:28 +000032#include "htmlnames.h"
mjsa0fe4042005-05-13 08:37:15 +000033#include "rendering/render_line.h"
34#include "rendering/render_object.h"
35#include "visible_text.h"
36#include "visible_units.h"
37#include "xml/dom2_rangeimpl.h"
38#include "xml/dom_position.h"
39#include "xml/dom_textimpl.h"
40
41
mjscff5e5e2005-09-27 22:37:33 +000042#include <kxmlcore/Assertions.h>
mjsa0fe4042005-05-13 08:37:15 +000043#include "KWQLogging.h"
mjsa0fe4042005-05-13 08:37:15 +000044
darinedbc5e42005-08-25 23:13:58 +000045using namespace DOM::HTMLNames;
46
mjsa0fe4042005-05-13 08:37:15 +000047using DOM::CSSComputedStyleDeclarationImpl;
justingd8043b62005-07-28 05:15:45 +000048using DOM::CSSMutableStyleDeclarationImpl;
mjsa0fe4042005-05-13 08:37:15 +000049using DOM::DOMString;
50using DOM::DocumentImpl;
51using DOM::NodeImpl;
52using DOM::Position;
53using DOM::RangeImpl;
54using DOM::TextImpl;
55
56namespace khtml {
57
58static bool isListStructureNode(const NodeImpl *node)
59{
60 // FIXME: Irritating that we can get away with just going at the render tree for isTableStructureNode,
61 // but here we also have to peek at the type of DOM node?
62 RenderObject *r = node->renderer();
mjsa0fe4042005-05-13 08:37:15 +000063 return (r && r->isListItem())
mjs76582fb2005-07-30 02:33:26 +000064 || node->hasTagName(olTag)
65 || node->hasTagName(ulTag)
66 || node->hasTagName(ddTag)
67 || node->hasTagName(dtTag)
68 || node->hasTagName(dirTag)
69 || node->hasTagName(menuTag);
mjsa0fe4042005-05-13 08:37:15 +000070}
71
72static int maxDeepOffset(NodeImpl *n)
73{
74 if (n->isAtomicNode())
75 return n->caretMaxOffset();
76
77 if (n->isElementNode())
78 return n->childNodeCount();
79
80 return 1;
81}
82
83static void debugPosition(const char *prefix, const Position &pos)
84{
85 if (!prefix)
86 prefix = "";
87 if (pos.isNull())
88 LOG(Editing, "%s <null>", prefix);
89 else
darinca8c31572005-08-25 17:47:26 +000090 LOG(Editing, "%s%s %p : %d", prefix, pos.node()->nodeName().qstring().latin1(), pos.node(), pos.offset());
mjsa0fe4042005-05-13 08:37:15 +000091}
92
93static void debugNode(const char *prefix, const NodeImpl *node)
94{
95 if (!prefix)
96 prefix = "";
97 if (!node)
98 LOG(Editing, "%s <null>", prefix);
99 else
darinca8c31572005-08-25 17:47:26 +0000100 LOG(Editing, "%s%s %p", prefix, node->nodeName().qstring().latin1(), node);
mjsa0fe4042005-05-13 08:37:15 +0000101}
102
103static Position positionBeforePossibleContainingSpecialElement(const Position &pos)
104{
105 if (isFirstVisiblePositionInSpecialElement(pos)) {
106 return positionBeforeContainingSpecialElement(pos);
107 }
108
109 return pos;
110}
111
112static Position positionAfterPossibleContainingSpecialElement(const Position &pos)
113{
114 if (isLastVisiblePositionInSpecialElement(pos)) {
115 return positionAfterContainingSpecialElement(pos);
116 }
117
118 return pos;
119}
120
121DeleteSelectionCommand::DeleteSelectionCommand(DocumentImpl *document, bool smartDelete, bool mergeBlocksAfterDelete)
122 : CompositeEditCommand(document),
123 m_hasSelectionToDelete(false),
124 m_smartDelete(smartDelete),
125 m_mergeBlocksAfterDelete(mergeBlocksAfterDelete),
126 m_startBlock(0),
127 m_endBlock(0),
128 m_startNode(0),
justing503ccf12005-07-29 22:50:47 +0000129 m_typingStyle(0),
130 m_deleteIntoBlockquoteStyle(0)
mjsa0fe4042005-05-13 08:37:15 +0000131{
132}
133
darina63b5f52005-09-24 01:19:14 +0000134DeleteSelectionCommand::DeleteSelectionCommand(DocumentImpl *document, const SelectionController &selection, bool smartDelete, bool mergeBlocksAfterDelete)
mjsa0fe4042005-05-13 08:37:15 +0000135 : CompositeEditCommand(document),
136 m_hasSelectionToDelete(true),
137 m_smartDelete(smartDelete),
138 m_mergeBlocksAfterDelete(mergeBlocksAfterDelete),
139 m_selectionToDelete(selection),
140 m_startBlock(0),
141 m_endBlock(0),
142 m_startNode(0),
justing503ccf12005-07-29 22:50:47 +0000143 m_typingStyle(0),
144 m_deleteIntoBlockquoteStyle(0)
mjsa0fe4042005-05-13 08:37:15 +0000145{
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();
mjsa0fe4042005-05-13 08:37:15 +0000207 m_endBlock = m_upstreamEnd.node()->enclosingBlockFlowElement();
mjsa0fe4042005-05-13 08:37:15 +0000208 m_startNode = m_upstreamStart.node();
mjsa0fe4042005-05-13 08:37:15 +0000209
210 //
211 // Handle detecting if the line containing the selection end is itself fully selected.
212 // This is one of the tests that determines if block merging of content needs to be done.
213 //
214 VisiblePosition visibleEnd(end, m_selectionToDelete.endAffinity());
harrisonbe11d7e2005-11-14 19:53:48 +0000215 if (isEndOfParagraph(visibleEnd)) {
mjsa0fe4042005-05-13 08:37:15 +0000216 Position previousLineStart = previousLinePosition(visibleEnd, 0).deepEquivalent();
217 if (previousLineStart.isNull() || RangeImpl::compareBoundaryPoints(previousLineStart, m_downstreamStart) >= 0)
218 m_mergeBlocksAfterDelete = false;
219 }
220
221 debugPosition("m_upstreamStart ", m_upstreamStart);
222 debugPosition("m_downstreamStart ", m_downstreamStart);
223 debugPosition("m_upstreamEnd ", m_upstreamEnd);
224 debugPosition("m_downstreamEnd ", m_downstreamEnd);
225 debugPosition("m_leadingWhitespace ", m_leadingWhitespace);
226 debugPosition("m_trailingWhitespace ", m_trailingWhitespace);
eseidelef508982006-01-03 09:19:17 +0000227 debugNode( "m_startBlock ", m_startBlock.get());
228 debugNode( "m_endBlock ", m_endBlock.get());
229 debugNode( "m_startNode ", m_startNode.get());
mjsa0fe4042005-05-13 08:37:15 +0000230}
231
232void DeleteSelectionCommand::insertPlaceholderForAncestorBlockContent()
233{
234 // This code makes sure a line does not disappear when deleting in this case:
235 // <p>foo</p>bar<p>baz</p>
236 // Select "bar" and hit delete. If nothing is done, the line containing bar will disappear.
237 // It needs to be held open by inserting a placeholder.
238 // Also see:
239 // <rdar://problem/3928305> selecting an entire line and typing over causes new inserted text at top of document
240 //
241 // The checks below detect the case where the selection contains content in an ancestor block
242 // surrounded by child blocks.
243 //
244 VisiblePosition visibleStart(m_upstreamStart, VP_DEFAULT_AFFINITY);
245 VisiblePosition beforeStart = visibleStart.previous();
246 NodeImpl *startBlock = enclosingBlockFlowElement(visibleStart);
247 NodeImpl *beforeStartBlock = enclosingBlockFlowElement(beforeStart);
248
249 if (!beforeStart.isNull() &&
250 !inSameBlock(visibleStart, beforeStart) &&
251 beforeStartBlock->isAncestor(startBlock) &&
252 startBlock != m_upstreamStart.node()) {
253
254 VisiblePosition visibleEnd(m_downstreamEnd, VP_DEFAULT_AFFINITY);
255 VisiblePosition afterEnd = visibleEnd.next();
256
257 if ((!afterEnd.isNull() && !inSameBlock(afterEnd, visibleEnd) && !inSameBlock(afterEnd, visibleStart)) ||
harrison17bab062005-10-08 00:50:30 +0000258 (m_downstreamEnd == m_selectionToDelete.end() && isEndOfParagraph(visibleEnd) && !m_downstreamEnd.node()->hasTagName(brTag))) {
mjsa0fe4042005-05-13 08:37:15 +0000259 NodeImpl *block = createDefaultParagraphElement(document());
260 insertNodeBefore(block, m_upstreamStart.node());
261 addBlockPlaceholderIfNeeded(block);
262 m_endingPosition = Position(block, 0);
263 }
264 }
265}
266
267void DeleteSelectionCommand::saveTypingStyleState()
268{
269 // Figure out the typing style in effect before the delete is done.
270 // FIXME: Improve typing style.
271 // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
eseidelef508982006-01-03 09:19:17 +0000272 RefPtr<CSSComputedStyleDeclarationImpl> computedStyle = positionBeforeTabSpan(m_selectionToDelete.start()).computedStyle();
mjsa0fe4042005-05-13 08:37:15 +0000273 m_typingStyle = computedStyle->copyInheritableProperties();
justing503ccf12005-07-29 22:50:47 +0000274
275 // If we're deleting into a Mail blockquote, save the style at end() instead of start()
276 // We'll use this later in computeTypingStyleAfterDelete if we end up outside of a Mail blockquote
277 if (nearestMailBlockquote(m_selectionToDelete.start().node())) {
278 computedStyle = m_selectionToDelete.end().computedStyle();
justing503ccf12005-07-29 22:50:47 +0000279 m_deleteIntoBlockquoteStyle = computedStyle->copyInheritableProperties();
justing503ccf12005-07-29 22:50:47 +0000280 } else
281 m_deleteIntoBlockquoteStyle = 0;
mjsa0fe4042005-05-13 08:37:15 +0000282}
283
284bool DeleteSelectionCommand::handleSpecialCaseBRDelete()
285{
286 // Check for special-case where the selection contains only a BR on a line by itself after another BR.
mjs76582fb2005-07-30 02:33:26 +0000287 bool upstreamStartIsBR = m_startNode->hasTagName(brTag);
288 bool downstreamStartIsBR = m_downstreamStart.node()->hasTagName(brTag);
mjsa0fe4042005-05-13 08:37:15 +0000289 bool isBROnLineByItself = upstreamStartIsBR && downstreamStartIsBR && m_downstreamStart.node() == m_upstreamEnd.node();
290 if (isBROnLineByItself) {
harrisone4b84c52005-07-18 18:14:59 +0000291 m_endingPosition = Position(m_downstreamStart.node()->parentNode(), m_downstreamStart.node()->nodeIndex());
mjsa0fe4042005-05-13 08:37:15 +0000292 removeNode(m_downstreamStart.node());
harrisone4b84c52005-07-18 18:14:59 +0000293 m_endingPosition = m_endingPosition.equivalentDeepPosition();
mjsa0fe4042005-05-13 08:37:15 +0000294 m_mergeBlocksAfterDelete = false;
295 return true;
296 }
297
298 // Not a special-case delete per se, but we can detect that the merging of content between blocks
299 // should not be done.
300 if (upstreamStartIsBR && downstreamStartIsBR)
301 m_mergeBlocksAfterDelete = false;
302
303 return false;
304}
305
mjsa0fe4042005-05-13 08:37:15 +0000306void DeleteSelectionCommand::handleGeneralDelete()
307{
308 int startOffset = m_upstreamStart.offset();
309 VisiblePosition visibleEnd = VisiblePosition(m_downstreamEnd, m_selectionToDelete.endAffinity());
310 bool endAtEndOfBlock = isEndOfBlock(visibleEnd);
311
312 // Handle some special cases where the selection begins and ends on specific visible units.
313 // Sometimes a node that is actually selected needs to be retained in order to maintain
314 // user expectations for the delete operation. Here is an example:
315 // 1. Open a new Blot or Mail document
316 // 2. hit Return ten times or so
317 // 3. Type a letter (do not hit Return after it)
318 // 4. Type shift-up-arrow to select the line containing the letter and the previous blank line
319 // 5. Hit Delete
320 // You expect the insertion point to wind up at the start of the line where your selection began.
321 // Because of the nature of HTML, the editing code needs to perform a special check to get
322 // this behavior. So:
323 // If the entire start block is selected, and the selection does not extend to the end of the
324 // end of a block other than the block containing the selection start, then do not delete the
325 // start block, otherwise delete the start block.
mjs76582fb2005-07-30 02:33:26 +0000326 if (startOffset == 1 && m_startNode && m_startNode->hasTagName(brTag)) {
eseidelef508982006-01-03 09:19:17 +0000327 m_startNode = m_startNode->traverseNextNode();
mjsa0fe4042005-05-13 08:37:15 +0000328 startOffset = 0;
329 }
harrison17bab062005-10-08 00:50:30 +0000330 if (m_startBlock != m_endBlock && isStartOfBlock(VisiblePosition(m_upstreamStart, m_selectionToDelete.startAffinity()))) {
eseidelef508982006-01-03 09:19:17 +0000331 if (!m_startBlock->isAncestor(m_endBlock.get()) && !isStartOfBlock(visibleEnd) && endAtEndOfBlock) {
mjsa0fe4042005-05-13 08:37:15 +0000332 // Delete all the children of the block, but not the block itself.
eseidelef508982006-01-03 09:19:17 +0000333 m_startNode = m_startBlock->firstChild();
mjsa0fe4042005-05-13 08:37:15 +0000334 startOffset = 0;
335 }
336 }
337 else if (startOffset >= m_startNode->caretMaxOffset() &&
338 (m_startNode->isAtomicNode() || startOffset == 0)) {
339 // Move the start node to the next node in the tree since the startOffset is equal to
340 // or beyond the start node's caretMaxOffset This means there is nothing visible to delete.
341 // But don't do this if the node is not atomic - we don't want to move into the first child.
342
343 // Also, before moving on, delete any insignificant text that may be present in a text node.
344 if (m_startNode->isTextNode()) {
345 // Delete any insignificant text from this node.
eseidelef508982006-01-03 09:19:17 +0000346 TextImpl *text = static_cast<TextImpl *>(m_startNode.get());
mjsa0fe4042005-05-13 08:37:15 +0000347 if (text->length() > (unsigned)m_startNode->caretMaxOffset())
348 deleteTextFromNode(text, m_startNode->caretMaxOffset(), text->length() - m_startNode->caretMaxOffset());
349 }
350
351 // shift the start node to the next
eseidelef508982006-01-03 09:19:17 +0000352 m_startNode = m_startNode->traverseNextNode();
mjsa0fe4042005-05-13 08:37:15 +0000353 startOffset = 0;
354 }
355
356 // Done adjusting the start. See if we're all done.
357 if (!m_startNode)
358 return;
359
360 if (m_startNode == m_downstreamEnd.node()) {
361 // The selection to delete is all in one node.
362 if (!m_startNode->renderer() ||
eseidelef508982006-01-03 09:19:17 +0000363 (startOffset == 0 && m_downstreamEnd.offset() >= maxDeepOffset(m_startNode.get()))) {
mjsa0fe4042005-05-13 08:37:15 +0000364 // just delete
eseidelef508982006-01-03 09:19:17 +0000365 removeFullySelectedNode(m_startNode.get());
mjsa0fe4042005-05-13 08:37:15 +0000366 } else if (m_downstreamEnd.offset() - startOffset > 0) {
367 if (m_startNode->isTextNode()) {
368 // in a text node that needs to be trimmed
eseidelef508982006-01-03 09:19:17 +0000369 TextImpl *text = static_cast<TextImpl *>(m_startNode.get());
mjsa0fe4042005-05-13 08:37:15 +0000370 deleteTextFromNode(text, startOffset, m_downstreamEnd.offset() - startOffset);
371 m_trailingWhitespaceValid = false;
372 } else {
eseidelef508982006-01-03 09:19:17 +0000373 removeChildrenInRange(m_startNode.get(), startOffset, m_downstreamEnd.offset());
mjsa0fe4042005-05-13 08:37:15 +0000374 m_endingPosition = m_upstreamStart;
375 }
376 }
377 }
378 else {
379 // The selection to delete spans more than one node.
eseidelef508982006-01-03 09:19:17 +0000380 NodeImpl *node = m_startNode.get();
mjsa0fe4042005-05-13 08:37:15 +0000381
382 if (startOffset > 0) {
383 if (m_startNode->isTextNode()) {
384 // in a text node that needs to be trimmed
385 TextImpl *text = static_cast<TextImpl *>(node);
386 deleteTextFromNode(text, startOffset, text->length() - startOffset);
387 node = node->traverseNextNode();
388 } else {
389 node = m_startNode->childNode(startOffset);
390 }
391 }
392
393 // handle deleting all nodes that are completely selected
394 while (node && node != m_downstreamEnd.node()) {
395 if (RangeImpl::compareBoundaryPoints(Position(node, 0), m_downstreamEnd) >= 0) {
396 // traverseNextSibling just blew past the end position, so stop deleting
397 node = 0;
398 } else if (!m_downstreamEnd.node()->isAncestor(node)) {
399 NodeImpl *nextNode = node->traverseNextSibling();
400 // if we just removed a node from the end container, update end position so the
401 // check above will work
402 if (node->parentNode() == m_downstreamEnd.node()) {
403 ASSERT(node->nodeIndex() < (unsigned)m_downstreamEnd.offset());
404 m_downstreamEnd = Position(m_downstreamEnd.node(), m_downstreamEnd.offset() - 1);
405 }
406 removeFullySelectedNode(node);
407 node = nextNode;
408 } else {
409 NodeImpl *n = node->lastChild();
410 while (n && n->lastChild())
411 n = n->lastChild();
412 if (n == m_downstreamEnd.node() && m_downstreamEnd.offset() >= m_downstreamEnd.node()->caretMaxOffset()) {
413 removeFullySelectedNode(node);
414 m_trailingWhitespaceValid = false;
415 node = 0;
416 }
417 else {
418 node = node->traverseNextNode();
419 }
420 }
421 }
422
423
424 if (m_downstreamEnd.node() != m_startNode && !m_upstreamStart.node()->isAncestor(m_downstreamEnd.node()) && m_downstreamEnd.node()->inDocument() && m_downstreamEnd.offset() >= m_downstreamEnd.node()->caretMinOffset()) {
425 if (m_downstreamEnd.offset() >= maxDeepOffset(m_downstreamEnd.node())) {
426 // need to delete whole node
427 // we can get here if this is the last node in the block
428 // remove an ancestor of m_downstreamEnd.node(), and thus m_downstreamEnd.node() itself
429 if (!m_upstreamStart.node()->inDocument() ||
430 m_upstreamStart.node() == m_downstreamEnd.node() ||
431 m_upstreamStart.node()->isAncestor(m_downstreamEnd.node())) {
432 m_upstreamStart = Position(m_downstreamEnd.node()->parentNode(), m_downstreamEnd.node()->nodeIndex());
433 }
434
435 removeFullySelectedNode(m_downstreamEnd.node());
436 m_trailingWhitespaceValid = false;
437 } else {
438 if (m_downstreamEnd.node()->isTextNode()) {
439 // in a text node that needs to be trimmed
440 TextImpl *text = static_cast<TextImpl *>(m_downstreamEnd.node());
441 if (m_downstreamEnd.offset() > 0) {
442 deleteTextFromNode(text, 0, m_downstreamEnd.offset());
443 m_downstreamEnd = Position(text, 0);
444 m_trailingWhitespaceValid = false;
445 }
446 } else {
447 int offset = 0;
448 if (m_upstreamStart.node()->isAncestor(m_downstreamEnd.node())) {
449 NodeImpl *n = m_upstreamStart.node();
450 while (n && n->parentNode() != m_downstreamEnd.node())
451 n = n->parentNode();
452 if (n)
453 offset = n->nodeIndex() + 1;
454 }
455 removeChildrenInRange(m_downstreamEnd.node(), offset, m_downstreamEnd.offset());
456 m_downstreamEnd = Position(m_downstreamEnd.node(), offset);
457 }
458 }
459 }
460 }
461}
462
mjsa0fe4042005-05-13 08:37:15 +0000463// FIXME: Can't really determine this without taking white-space mode into account.
464static inline bool nextCharacterIsCollapsibleWhitespace(const Position &pos)
465{
466 if (!pos.node())
467 return false;
468 if (!pos.node()->isTextNode())
469 return false;
470 return isCollapsibleWhitespace(static_cast<TextImpl *>(pos.node())->data()[pos.offset()]);
471}
472
473void DeleteSelectionCommand::fixupWhitespace()
474{
darin26f5b362005-12-22 04:11:39 +0000475 updateLayout();
mjsa0fe4042005-05-13 08:37:15 +0000476 if (m_leadingWhitespace.isNotNull() && (m_trailingWhitespace.isNotNull() || !m_leadingWhitespace.isRenderedCharacter())) {
477 LOG(Editing, "replace leading");
478 TextImpl *textNode = static_cast<TextImpl *>(m_leadingWhitespace.node());
479 replaceTextInNode(textNode, m_leadingWhitespace.offset(), 1, nonBreakingSpaceString());
480 }
481 else if (m_trailingWhitespace.isNotNull()) {
482 if (m_trailingWhitespaceValid) {
483 if (!m_trailingWhitespace.isRenderedCharacter()) {
484 LOG(Editing, "replace trailing [valid]");
485 TextImpl *textNode = static_cast<TextImpl *>(m_trailingWhitespace.node());
486 replaceTextInNode(textNode, m_trailingWhitespace.offset(), 1, nonBreakingSpaceString());
487 }
488 }
489 else {
490 Position pos = m_endingPosition.downstream();
491 pos = Position(pos.node(), pos.offset() - 1);
492 if (nextCharacterIsCollapsibleWhitespace(pos) && !pos.isRenderedCharacter()) {
493 LOG(Editing, "replace trailing [invalid]");
494 TextImpl *textNode = static_cast<TextImpl *>(pos.node());
495 replaceTextInNode(textNode, pos.offset(), 1, nonBreakingSpaceString());
496 // need to adjust ending position since the trailing position is not valid.
497 m_endingPosition = pos;
498 }
499 }
500 }
501}
502
503// This function moves nodes in the block containing startNode to dstBlock, starting
504// from startNode and proceeding to the end of the paragraph. Nodes in the block containing
505// startNode that appear in document order before startNode are not moved.
506// This function is an important helper for deleting selections that cross paragraph
507// boundaries.
508void DeleteSelectionCommand::moveNodesAfterNode()
509{
510 if (!m_mergeBlocksAfterDelete)
511 return;
512
513 if (m_endBlock == m_startBlock)
514 return;
515
516 NodeImpl *startNode = m_downstreamEnd.node();
517 NodeImpl *dstNode = m_upstreamStart.node();
518
519 if (!startNode->inDocument() || !dstNode->inDocument())
520 return;
521
522 NodeImpl *startBlock = startNode->enclosingBlockFlowElement();
523 if (isTableStructureNode(startBlock) || isListStructureNode(startBlock))
524 // Do not move content between parts of a table or list.
525 return;
526
527 // Now that we are about to add content, check to see if a placeholder element
528 // can be removed.
529 removeBlockPlaceholder(startBlock);
530
531 // Move the subtree containing node
532 NodeImpl *node = startNode->enclosingInlineElement();
533
534 // Insert after the subtree containing destNode
535 NodeImpl *refNode = dstNode->enclosingInlineElement();
536
537 // Nothing to do if start is already at the beginning of dstBlock
538 NodeImpl *dstBlock = refNode->enclosingBlockFlowElement();
539 if (startBlock == dstBlock->firstChild())
540 return;
541
542 // Do the move.
543 NodeImpl *rootNode = refNode->rootEditableElement();
544 while (node && node->isAncestor(startBlock)) {
545 NodeImpl *moveNode = node;
546 node = node->nextSibling();
547 removeNode(moveNode);
mjs76582fb2005-07-30 02:33:26 +0000548 if (moveNode->hasTagName(brTag) && !moveNode->renderer()) {
mjsa0fe4042005-05-13 08:37:15 +0000549 // Just remove this node, and don't put it back.
550 // If the BR was not rendered (since it was at the end of a block, for instance),
551 // putting it back in the document might make it appear, and that is not desirable.
552 break;
553 }
554 if (refNode == rootNode)
555 insertNodeAt(moveNode, refNode, 0);
556 else
557 insertNodeAfter(moveNode, refNode);
558 refNode = moveNode;
mjs76582fb2005-07-30 02:33:26 +0000559 if (moveNode->hasTagName(brTag))
mjsa0fe4042005-05-13 08:37:15 +0000560 break;
561 }
562
563 // If the startBlock no longer has any kids, we may need to deal with adding a BR
564 // to make the layout come out right. Consider this document:
565 //
566 // One
567 // <div>Two</div>
568 // Three
569 //
570 // Placing the insertion before before the 'T' of 'Two' and hitting delete will
571 // move the contents of the div to the block containing 'One' and delete the div.
572 // This will have the side effect of moving 'Three' on to the same line as 'One'
573 // and 'Two'. This is undesirable. We fix this up by adding a BR before the 'Three'.
574 // This may not be ideal, but it is better than nothing.
darin26f5b362005-12-22 04:11:39 +0000575 updateLayout();
mjsa0fe4042005-05-13 08:37:15 +0000576 if (!startBlock->renderer() || !startBlock->renderer()->firstChild()) {
577 removeNode(startBlock);
darin26f5b362005-12-22 04:11:39 +0000578 updateLayout();
mjsa0fe4042005-05-13 08:37:15 +0000579 if (refNode->renderer() && refNode->renderer()->inlineBox() && refNode->renderer()->inlineBox()->nextOnLineExists()) {
580 insertNodeAfter(createBreakElement(document()), refNode);
581 }
582 }
583}
584
585void DeleteSelectionCommand::calculateEndingPosition()
586{
587 if (m_endingPosition.isNotNull() && m_endingPosition.node()->inDocument())
588 return;
589
590 m_endingPosition = m_upstreamStart;
591 if (m_endingPosition.node()->inDocument())
592 return;
593
594 m_endingPosition = m_downstreamEnd;
595 if (m_endingPosition.node()->inDocument())
596 return;
597
eseidelef508982006-01-03 09:19:17 +0000598 m_endingPosition = Position(m_startBlock.get(), 0);
mjsa0fe4042005-05-13 08:37:15 +0000599 if (m_endingPosition.node()->inDocument())
600 return;
601
eseidelef508982006-01-03 09:19:17 +0000602 m_endingPosition = Position(m_endBlock.get(), 0);
mjsa0fe4042005-05-13 08:37:15 +0000603 if (m_endingPosition.node()->inDocument())
604 return;
605
606 m_endingPosition = Position(document()->documentElement(), 0);
607}
608
609void DeleteSelectionCommand::calculateTypingStyleAfterDelete(NodeImpl *insertedPlaceholder)
610{
611 // Compute the difference between the style before the delete and the style now
612 // after the delete has been done. Set this style on the part, so other editing
613 // commands being composed with this one will work, and also cache it on the command,
614 // so the KHTMLPart::appliedEditing can set it after the whole composite command
615 // has completed.
616 // FIXME: Improve typing style.
617 // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
justing503ccf12005-07-29 22:50:47 +0000618
eseidelef508982006-01-03 09:19:17 +0000619 // If we deleted into a blockquote, but are now no longer in a blockquote, use the alternate typing style
620 if (m_deleteIntoBlockquoteStyle && !nearestMailBlockquote(m_endingPosition.node()))
621 m_typingStyle = m_deleteIntoBlockquoteStyle;
622 m_deleteIntoBlockquoteStyle = 0;
justing503ccf12005-07-29 22:50:47 +0000623
eseidelef508982006-01-03 09:19:17 +0000624 RefPtr<CSSComputedStyleDeclarationImpl> endingStyle = new CSSComputedStyleDeclarationImpl(m_endingPosition.node());
625 endingStyle->diff(m_typingStyle.get());
626 if (!m_typingStyle->length())
mjsa0fe4042005-05-13 08:37:15 +0000627 m_typingStyle = 0;
mjsa0fe4042005-05-13 08:37:15 +0000628 if (insertedPlaceholder && m_typingStyle) {
629 // Apply style to the placeholder. This makes sure that the single line in the
630 // paragraph has the right height, and that the paragraph takes on the style
631 // of the preceding line and retains it even if you click away, click back, and
632 // then start typing. In this case, the typing style is applied right now, and
633 // is not retained until the next typing action.
634
justing140053e2005-11-07 19:59:22 +0000635 setEndingSelection(SelectionController(Position(insertedPlaceholder, 0), DOWNSTREAM));
eseidelef508982006-01-03 09:19:17 +0000636 applyStyle(m_typingStyle.get(), EditActionUnspecified);
mjsa0fe4042005-05-13 08:37:15 +0000637 m_typingStyle = 0;
638 }
639 // Set m_typingStyle as the typing style.
640 // It's perfectly OK for m_typingStyle to be null.
eseidelef508982006-01-03 09:19:17 +0000641 document()->part()->setTypingStyle(m_typingStyle.get());
642 setTypingStyle(m_typingStyle.get());
mjsa0fe4042005-05-13 08:37:15 +0000643}
644
645void DeleteSelectionCommand::clearTransientState()
646{
647 m_selectionToDelete.clear();
648 m_upstreamStart.clear();
649 m_downstreamStart.clear();
650 m_upstreamEnd.clear();
651 m_downstreamEnd.clear();
652 m_endingPosition.clear();
653 m_leadingWhitespace.clear();
654 m_trailingWhitespace.clear();
mjsa0fe4042005-05-13 08:37:15 +0000655}
656
657void DeleteSelectionCommand::doApply()
658{
659 // If selection has not been set to a custom selection when the command was created,
660 // use the current ending selection.
661 if (!m_hasSelectionToDelete)
662 m_selectionToDelete = endingSelection();
663
664 if (!m_selectionToDelete.isRange())
665 return;
666
667 // save this to later make the selection with
668 EAffinity affinity = m_selectionToDelete.startAffinity();
669
670 // set up our state
671 initializePositionData();
mjsa0fe4042005-05-13 08:37:15 +0000672 if (!m_startBlock || !m_endBlock) {
673 // Can't figure out what blocks we're in. This can happen if
674 // the document structure is not what we are expecting, like if
675 // the document has no body element, or if the editable block
676 // has been changed to display: inline. Some day it might
677 // be nice to be able to deal with this, but for now, bail.
678 clearTransientState();
679 return;
680 }
harrisondff01cd2005-07-18 23:21:19 +0000681
682 // if all we are deleting is complete paragraph(s), we need to make
683 // sure a blank paragraph remains when we are done
684 bool forceBlankParagraph = isStartOfParagraph(VisiblePosition(m_upstreamStart, VP_DEFAULT_AFFINITY)) &&
685 isEndOfParagraph(VisiblePosition(m_downstreamEnd, VP_DEFAULT_AFFINITY));
686
687 // Delete any text that may hinder our ability to fixup whitespace after the detele
688 deleteInsignificantTextDownstream(m_trailingWhitespace);
689
690 saveTypingStyleState();
mjsa0fe4042005-05-13 08:37:15 +0000691
harrisone4b84c52005-07-18 18:14:59 +0000692 // deleting just a BR is handled specially, at least because we do not
693 // want to replace it with a placeholder BR!
694 if (handleSpecialCaseBRDelete()) {
695 calculateTypingStyleAfterDelete(false);
696 debugPosition("endingPosition ", m_endingPosition);
darina63b5f52005-09-24 01:19:14 +0000697 setEndingSelection(SelectionController(m_endingPosition, affinity));
harrisone4b84c52005-07-18 18:14:59 +0000698 clearTransientState();
699 rebalanceWhitespace();
700 return;
701 }
mjsa0fe4042005-05-13 08:37:15 +0000702
harrisone4b84c52005-07-18 18:14:59 +0000703 insertPlaceholderForAncestorBlockContent();
704 handleGeneralDelete();
mjsa0fe4042005-05-13 08:37:15 +0000705
706 // Do block merge if start and end of selection are in different blocks.
707 moveNodesAfterNode();
708
709 calculateEndingPosition();
710 fixupWhitespace();
711
712 // if the m_endingPosition is already a blank paragraph, there is
713 // no need to force a new one
714 if (forceBlankParagraph &&
715 isStartOfParagraph(VisiblePosition(m_endingPosition, VP_DEFAULT_AFFINITY)) &&
716 isEndOfParagraph(VisiblePosition(m_endingPosition, VP_DEFAULT_AFFINITY))) {
717 forceBlankParagraph = false;
718 }
719
720 NodeImpl *addedPlaceholder = forceBlankParagraph ? insertBlockPlaceholder(m_endingPosition) :
721 addBlockPlaceholderIfNeeded(m_endingPosition.node());
722
723 calculateTypingStyleAfterDelete(addedPlaceholder);
harrisone4b84c52005-07-18 18:14:59 +0000724
mjsa0fe4042005-05-13 08:37:15 +0000725 debugPosition("endingPosition ", m_endingPosition);
darina63b5f52005-09-24 01:19:14 +0000726 setEndingSelection(SelectionController(m_endingPosition, affinity));
mjsa0fe4042005-05-13 08:37:15 +0000727 clearTransientState();
728 rebalanceWhitespace();
729}
730
731EditAction DeleteSelectionCommand::editingAction() const
732{
733 // Note that DeleteSelectionCommand is also used when the user presses the Delete key,
734 // but in that case there's a TypingCommand that supplies the editingAction(), so
735 // the Undo menu correctly shows "Undo Typing"
736 return EditActionCut;
737}
738
739bool DeleteSelectionCommand::preservesTypingStyle() const
740{
741 return true;
742}
743
744} // namespace khtml