mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 1 | /* |
| 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 | |
mjs | b64c50a | 2005-10-03 21:13:12 +0000 | [diff] [blame] | 26 | #include "config.h" |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 27 | #include "delete_selection_command.h" |
| 28 | |
| 29 | #include "css/css_computedstyle.h" |
| 30 | #include "htmlediting.h" |
| 31 | #include "khtml_part.h" |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 32 | #include "htmlnames.h" |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 33 | #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 | |
mjs | cff5e5e | 2005-09-27 22:37:33 +0000 | [diff] [blame] | 42 | #include <kxmlcore/Assertions.h> |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 43 | #include "KWQLogging.h" |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 44 | |
darin | edbc5e4 | 2005-08-25 23:13:58 +0000 | [diff] [blame] | 45 | using namespace DOM::HTMLNames; |
| 46 | |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 47 | using DOM::CSSComputedStyleDeclarationImpl; |
justing | d8043b6 | 2005-07-28 05:15:45 +0000 | [diff] [blame] | 48 | using DOM::CSSMutableStyleDeclarationImpl; |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 49 | using DOM::DOMString; |
| 50 | using DOM::DocumentImpl; |
| 51 | using DOM::NodeImpl; |
| 52 | using DOM::Position; |
| 53 | using DOM::RangeImpl; |
| 54 | using DOM::TextImpl; |
| 55 | |
| 56 | namespace khtml { |
| 57 | |
| 58 | static 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 63 | return (r && r->isListItem()) |
mjs | 76582fb | 2005-07-30 02:33:26 +0000 | [diff] [blame] | 64 | || node->hasTagName(olTag) |
| 65 | || node->hasTagName(ulTag) |
| 66 | || node->hasTagName(ddTag) |
| 67 | || node->hasTagName(dtTag) |
| 68 | || node->hasTagName(dirTag) |
| 69 | || node->hasTagName(menuTag); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 70 | } |
| 71 | |
| 72 | static 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 | |
| 83 | static 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 |
darin | ca8c3157 | 2005-08-25 17:47:26 +0000 | [diff] [blame] | 90 | LOG(Editing, "%s%s %p : %d", prefix, pos.node()->nodeName().qstring().latin1(), pos.node(), pos.offset()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 91 | } |
| 92 | |
| 93 | static 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 |
darin | ca8c3157 | 2005-08-25 17:47:26 +0000 | [diff] [blame] | 100 | LOG(Editing, "%s%s %p", prefix, node->nodeName().qstring().latin1(), node); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 101 | } |
| 102 | |
| 103 | static Position positionBeforePossibleContainingSpecialElement(const Position &pos) |
| 104 | { |
| 105 | if (isFirstVisiblePositionInSpecialElement(pos)) { |
| 106 | return positionBeforeContainingSpecialElement(pos); |
| 107 | } |
| 108 | |
| 109 | return pos; |
| 110 | } |
| 111 | |
| 112 | static Position positionAfterPossibleContainingSpecialElement(const Position &pos) |
| 113 | { |
| 114 | if (isLastVisiblePositionInSpecialElement(pos)) { |
| 115 | return positionAfterContainingSpecialElement(pos); |
| 116 | } |
| 117 | |
| 118 | return pos; |
| 119 | } |
| 120 | |
| 121 | DeleteSelectionCommand::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), |
justing | 503ccf1 | 2005-07-29 22:50:47 +0000 | [diff] [blame] | 129 | m_typingStyle(0), |
| 130 | m_deleteIntoBlockquoteStyle(0) |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 131 | { |
| 132 | } |
| 133 | |
darin | a63b5f5 | 2005-09-24 01:19:14 +0000 | [diff] [blame] | 134 | DeleteSelectionCommand::DeleteSelectionCommand(DocumentImpl *document, const SelectionController &selection, bool smartDelete, bool mergeBlocksAfterDelete) |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 135 | : 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), |
justing | 503ccf1 | 2005-07-29 22:50:47 +0000 | [diff] [blame] | 143 | m_typingStyle(0), |
| 144 | m_deleteIntoBlockquoteStyle(0) |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 145 | { |
| 146 | } |
| 147 | |
| 148 | void 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 207 | m_endBlock = m_upstreamEnd.node()->enclosingBlockFlowElement(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 208 | m_startNode = m_upstreamStart.node(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 209 | |
| 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()); |
harrison | be11d7e | 2005-11-14 19:53:48 +0000 | [diff] [blame] | 215 | if (isEndOfParagraph(visibleEnd)) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 216 | 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); |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 227 | debugNode( "m_startBlock ", m_startBlock.get()); |
| 228 | debugNode( "m_endBlock ", m_endBlock.get()); |
| 229 | debugNode( "m_startNode ", m_startNode.get()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 230 | } |
| 231 | |
| 232 | void 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)) || |
harrison | 17bab06 | 2005-10-08 00:50:30 +0000 | [diff] [blame] | 258 | (m_downstreamEnd == m_selectionToDelete.end() && isEndOfParagraph(visibleEnd) && !m_downstreamEnd.node()->hasTagName(brTag))) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 259 | NodeImpl *block = createDefaultParagraphElement(document()); |
| 260 | insertNodeBefore(block, m_upstreamStart.node()); |
| 261 | addBlockPlaceholderIfNeeded(block); |
| 262 | m_endingPosition = Position(block, 0); |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | void 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 |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 272 | RefPtr<CSSComputedStyleDeclarationImpl> computedStyle = positionBeforeTabSpan(m_selectionToDelete.start()).computedStyle(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 273 | m_typingStyle = computedStyle->copyInheritableProperties(); |
justing | 503ccf1 | 2005-07-29 22:50:47 +0000 | [diff] [blame] | 274 | |
| 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(); |
justing | 503ccf1 | 2005-07-29 22:50:47 +0000 | [diff] [blame] | 279 | m_deleteIntoBlockquoteStyle = computedStyle->copyInheritableProperties(); |
justing | 503ccf1 | 2005-07-29 22:50:47 +0000 | [diff] [blame] | 280 | } else |
| 281 | m_deleteIntoBlockquoteStyle = 0; |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 282 | } |
| 283 | |
| 284 | bool DeleteSelectionCommand::handleSpecialCaseBRDelete() |
| 285 | { |
| 286 | // Check for special-case where the selection contains only a BR on a line by itself after another BR. |
mjs | 76582fb | 2005-07-30 02:33:26 +0000 | [diff] [blame] | 287 | bool upstreamStartIsBR = m_startNode->hasTagName(brTag); |
| 288 | bool downstreamStartIsBR = m_downstreamStart.node()->hasTagName(brTag); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 289 | bool isBROnLineByItself = upstreamStartIsBR && downstreamStartIsBR && m_downstreamStart.node() == m_upstreamEnd.node(); |
| 290 | if (isBROnLineByItself) { |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame] | 291 | m_endingPosition = Position(m_downstreamStart.node()->parentNode(), m_downstreamStart.node()->nodeIndex()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 292 | removeNode(m_downstreamStart.node()); |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame] | 293 | m_endingPosition = m_endingPosition.equivalentDeepPosition(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 294 | 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 | |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 306 | void 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. |
mjs | 76582fb | 2005-07-30 02:33:26 +0000 | [diff] [blame] | 326 | if (startOffset == 1 && m_startNode && m_startNode->hasTagName(brTag)) { |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 327 | m_startNode = m_startNode->traverseNextNode(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 328 | startOffset = 0; |
| 329 | } |
harrison | 17bab06 | 2005-10-08 00:50:30 +0000 | [diff] [blame] | 330 | if (m_startBlock != m_endBlock && isStartOfBlock(VisiblePosition(m_upstreamStart, m_selectionToDelete.startAffinity()))) { |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 331 | if (!m_startBlock->isAncestor(m_endBlock.get()) && !isStartOfBlock(visibleEnd) && endAtEndOfBlock) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 332 | // Delete all the children of the block, but not the block itself. |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 333 | m_startNode = m_startBlock->firstChild(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 334 | 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. |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 346 | TextImpl *text = static_cast<TextImpl *>(m_startNode.get()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 347 | 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 |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 352 | m_startNode = m_startNode->traverseNextNode(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 353 | 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() || |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 363 | (startOffset == 0 && m_downstreamEnd.offset() >= maxDeepOffset(m_startNode.get()))) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 364 | // just delete |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 365 | removeFullySelectedNode(m_startNode.get()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 366 | } else if (m_downstreamEnd.offset() - startOffset > 0) { |
| 367 | if (m_startNode->isTextNode()) { |
| 368 | // in a text node that needs to be trimmed |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 369 | TextImpl *text = static_cast<TextImpl *>(m_startNode.get()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 370 | deleteTextFromNode(text, startOffset, m_downstreamEnd.offset() - startOffset); |
| 371 | m_trailingWhitespaceValid = false; |
| 372 | } else { |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 373 | removeChildrenInRange(m_startNode.get(), startOffset, m_downstreamEnd.offset()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 374 | m_endingPosition = m_upstreamStart; |
| 375 | } |
| 376 | } |
| 377 | } |
| 378 | else { |
| 379 | // The selection to delete spans more than one node. |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 380 | NodeImpl *node = m_startNode.get(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 381 | |
| 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 | |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 463 | // FIXME: Can't really determine this without taking white-space mode into account. |
| 464 | static 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 | |
| 473 | void DeleteSelectionCommand::fixupWhitespace() |
| 474 | { |
darin | 26f5b36 | 2005-12-22 04:11:39 +0000 | [diff] [blame] | 475 | updateLayout(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 476 | 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. |
| 508 | void 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); |
mjs | 76582fb | 2005-07-30 02:33:26 +0000 | [diff] [blame] | 548 | if (moveNode->hasTagName(brTag) && !moveNode->renderer()) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 549 | // 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; |
mjs | 76582fb | 2005-07-30 02:33:26 +0000 | [diff] [blame] | 559 | if (moveNode->hasTagName(brTag)) |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 560 | 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. |
darin | 26f5b36 | 2005-12-22 04:11:39 +0000 | [diff] [blame] | 575 | updateLayout(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 576 | if (!startBlock->renderer() || !startBlock->renderer()->firstChild()) { |
| 577 | removeNode(startBlock); |
darin | 26f5b36 | 2005-12-22 04:11:39 +0000 | [diff] [blame] | 578 | updateLayout(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 579 | if (refNode->renderer() && refNode->renderer()->inlineBox() && refNode->renderer()->inlineBox()->nextOnLineExists()) { |
| 580 | insertNodeAfter(createBreakElement(document()), refNode); |
| 581 | } |
| 582 | } |
| 583 | } |
| 584 | |
| 585 | void 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 | |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 598 | m_endingPosition = Position(m_startBlock.get(), 0); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 599 | if (m_endingPosition.node()->inDocument()) |
| 600 | return; |
| 601 | |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 602 | m_endingPosition = Position(m_endBlock.get(), 0); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 603 | if (m_endingPosition.node()->inDocument()) |
| 604 | return; |
| 605 | |
| 606 | m_endingPosition = Position(document()->documentElement(), 0); |
| 607 | } |
| 608 | |
| 609 | void 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 |
justing | 503ccf1 | 2005-07-29 22:50:47 +0000 | [diff] [blame] | 618 | |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 619 | // 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; |
justing | 503ccf1 | 2005-07-29 22:50:47 +0000 | [diff] [blame] | 623 | |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 624 | RefPtr<CSSComputedStyleDeclarationImpl> endingStyle = new CSSComputedStyleDeclarationImpl(m_endingPosition.node()); |
| 625 | endingStyle->diff(m_typingStyle.get()); |
| 626 | if (!m_typingStyle->length()) |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 627 | m_typingStyle = 0; |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 628 | 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 | |
justing | 140053e | 2005-11-07 19:59:22 +0000 | [diff] [blame] | 635 | setEndingSelection(SelectionController(Position(insertedPlaceholder, 0), DOWNSTREAM)); |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 636 | applyStyle(m_typingStyle.get(), EditActionUnspecified); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 637 | m_typingStyle = 0; |
| 638 | } |
| 639 | // Set m_typingStyle as the typing style. |
| 640 | // It's perfectly OK for m_typingStyle to be null. |
eseidel | ef50898 | 2006-01-03 09:19:17 +0000 | [diff] [blame^] | 641 | document()->part()->setTypingStyle(m_typingStyle.get()); |
| 642 | setTypingStyle(m_typingStyle.get()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 643 | } |
| 644 | |
| 645 | void 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 655 | } |
| 656 | |
| 657 | void 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 672 | 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 | } |
harrison | dff01cd | 2005-07-18 23:21:19 +0000 | [diff] [blame] | 681 | |
| 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 691 | |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame] | 692 | // 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); |
darin | a63b5f5 | 2005-09-24 01:19:14 +0000 | [diff] [blame] | 697 | setEndingSelection(SelectionController(m_endingPosition, affinity)); |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame] | 698 | clearTransientState(); |
| 699 | rebalanceWhitespace(); |
| 700 | return; |
| 701 | } |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 702 | |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame] | 703 | insertPlaceholderForAncestorBlockContent(); |
| 704 | handleGeneralDelete(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 705 | |
| 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); |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame] | 724 | |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 725 | debugPosition("endingPosition ", m_endingPosition); |
darin | a63b5f5 | 2005-09-24 01:19:14 +0000 | [diff] [blame] | 726 | setEndingSelection(SelectionController(m_endingPosition, affinity)); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 727 | clearTransientState(); |
| 728 | rebalanceWhitespace(); |
| 729 | } |
| 730 | |
| 731 | EditAction 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 | |
| 739 | bool DeleteSelectionCommand::preservesTypingStyle() const |
| 740 | { |
| 741 | return true; |
| 742 | } |
| 743 | |
| 744 | } // namespace khtml |