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 | |
| 26 | #include "delete_selection_command.h" |
| 27 | |
| 28 | #include "css/css_computedstyle.h" |
| 29 | #include "htmlediting.h" |
| 30 | #include "khtml_part.h" |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 31 | #include "htmlnames.h" |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 32 | #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 | |
| 49 | using DOM::CSSComputedStyleDeclarationImpl; |
| 50 | using DOM::DOMString; |
| 51 | using DOM::DocumentImpl; |
| 52 | using DOM::NodeImpl; |
| 53 | using DOM::Position; |
| 54 | using DOM::RangeImpl; |
| 55 | using DOM::TextImpl; |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 56 | using DOM::HTMLNames; |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 57 | |
| 58 | namespace khtml { |
| 59 | |
| 60 | static 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 65 | return (r && r->isListItem()) |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 66 | || 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()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 72 | } |
| 73 | |
| 74 | static 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 | |
| 85 | static 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 | |
| 95 | static 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 | |
| 105 | static Position positionBeforePossibleContainingSpecialElement(const Position &pos) |
| 106 | { |
| 107 | if (isFirstVisiblePositionInSpecialElement(pos)) { |
| 108 | return positionBeforeContainingSpecialElement(pos); |
| 109 | } |
| 110 | |
| 111 | return pos; |
| 112 | } |
| 113 | |
| 114 | static Position positionAfterPossibleContainingSpecialElement(const Position &pos) |
| 115 | { |
| 116 | if (isLastVisiblePositionInSpecialElement(pos)) { |
| 117 | return positionAfterContainingSpecialElement(pos); |
| 118 | } |
| 119 | |
| 120 | return pos; |
| 121 | } |
| 122 | |
| 123 | DeleteSelectionCommand::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 | |
| 135 | DeleteSelectionCommand::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 | |
| 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(); |
| 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 | |
| 235 | void 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 | |
| 270 | void 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 |
adele | 945819d | 2005-07-05 23:21:31 +0000 | [diff] [blame] | 275 | CSSComputedStyleDeclarationImpl *computedStyle = m_selectionToDelete.start().computedStyle(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 276 | computedStyle->ref(); |
| 277 | m_typingStyle = computedStyle->copyInheritableProperties(); |
| 278 | m_typingStyle->ref(); |
| 279 | computedStyle->deref(); |
| 280 | } |
| 281 | |
| 282 | bool DeleteSelectionCommand::handleSpecialCaseBRDelete() |
| 283 | { |
| 284 | // Check for special-case where the selection contains only a BR on a line by itself after another BR. |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 285 | bool upstreamStartIsBR = m_startNode->hasTagName(HTMLNames::br()); |
| 286 | bool downstreamStartIsBR = m_downstreamStart.node()->hasTagName(HTMLNames::br()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 287 | bool isBROnLineByItself = upstreamStartIsBR && downstreamStartIsBR && m_downstreamStart.node() == m_upstreamEnd.node(); |
| 288 | if (isBROnLineByItself) { |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame^] | 289 | m_endingPosition = Position(m_downstreamStart.node()->parentNode(), m_downstreamStart.node()->nodeIndex()); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 290 | removeNode(m_downstreamStart.node()); |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame^] | 291 | m_endingPosition = m_endingPosition.equivalentDeepPosition(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 292 | 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 | |
| 304 | void 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 | |
| 314 | void 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. |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 335 | if (startOffset == 1 && m_startNode && m_startNode->hasTagName(HTMLNames::br())) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 336 | setStartNode(m_startNode->traverseNextNode()); |
| 337 | startOffset = 0; |
| 338 | } |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 339 | if (m_startBlock != m_endBlock && startOffset == 0 && m_startNode && m_startNode->hasTagName(HTMLNames::br()) && endAtEndOfBlock) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 340 | // 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 | |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 476 | // FIXME: Can't really determine this without taking white-space mode into account. |
| 477 | static 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 | |
| 486 | void 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. |
| 521 | void 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); |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 561 | if (moveNode->hasTagName(HTMLNames::br()) && !moveNode->renderer()) { |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 562 | // 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; |
hyatt | 59136b7 | 2005-07-09 20:19:28 +0000 | [diff] [blame] | 572 | if (moveNode->hasTagName(HTMLNames::br())) |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 573 | 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 | |
| 598 | void 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 | |
| 622 | void 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 | |
| 662 | void 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 | |
| 691 | void 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 706 | 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 | |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame^] | 716 | // 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 | |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 727 | // 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(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 736 | |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame^] | 737 | insertPlaceholderForAncestorBlockContent(); |
| 738 | handleGeneralDelete(); |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 739 | |
| 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); |
harrison | e4b84c5 | 2005-07-18 18:14:59 +0000 | [diff] [blame^] | 758 | |
mjs | a0fe404 | 2005-05-13 08:37:15 +0000 | [diff] [blame] | 759 | debugPosition("endingPosition ", m_endingPosition); |
| 760 | setEndingSelection(Selection(m_endingPosition, affinity)); |
| 761 | clearTransientState(); |
| 762 | rebalanceWhitespace(); |
| 763 | } |
| 764 | |
| 765 | EditAction 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 | |
| 773 | bool DeleteSelectionCommand::preservesTypingStyle() const |
| 774 | { |
| 775 | return true; |
| 776 | } |
| 777 | |
| 778 | } // namespace khtml |