1 | /* |
2 | * Copyright (C) 2005 Apple 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 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 INC. OR |
17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
18 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
20 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
21 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
24 | */ |
25 | |
26 | #include "config.h" |
27 | #include "DeleteSelectionCommand.h" |
28 | |
29 | #include "Document.h" |
30 | #include "DocumentMarkerController.h" |
31 | #include "Editing.h" |
32 | #include "Editor.h" |
33 | #include "EditorClient.h" |
34 | #include "ElementIterator.h" |
35 | #include "Frame.h" |
36 | #include "HTMLBRElement.h" |
37 | #include "HTMLLinkElement.h" |
38 | #include "HTMLNames.h" |
39 | #include "HTMLStyleElement.h" |
40 | #include "HTMLTableElement.h" |
41 | #include "NodeTraversal.h" |
42 | #include "RenderTableCell.h" |
43 | #include "RenderText.h" |
44 | #include "RenderedDocumentMarker.h" |
45 | #include "Text.h" |
46 | #include "VisibleUnits.h" |
47 | |
48 | namespace WebCore { |
49 | |
50 | using namespace HTMLNames; |
51 | |
52 | static bool isTableRow(const Node* node) |
53 | { |
54 | return node && node->hasTagName(trTag); |
55 | } |
56 | |
57 | static bool isTableCellEmpty(Node* cell) |
58 | { |
59 | ASSERT(isTableCell(cell)); |
60 | return VisiblePosition(firstPositionInNode(cell)) == VisiblePosition(lastPositionInNode(cell)); |
61 | } |
62 | |
63 | static bool isTableRowEmpty(Node* row) |
64 | { |
65 | if (!isTableRow(row)) |
66 | return false; |
67 | |
68 | for (Node* child = row->firstChild(); child; child = child->nextSibling()) |
69 | if (isTableCell(child) && !isTableCellEmpty(child)) |
70 | return false; |
71 | |
72 | return true; |
73 | } |
74 | |
75 | DeleteSelectionCommand::DeleteSelectionCommand(Document& document, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements, bool sanitizeMarkup, EditAction editingAction) |
76 | : CompositeEditCommand(document, editingAction) |
77 | , m_hasSelectionToDelete(false) |
78 | , m_smartDelete(smartDelete) |
79 | , m_mergeBlocksAfterDelete(mergeBlocksAfterDelete) |
80 | , m_needPlaceholder(false) |
81 | , m_replace(replace) |
82 | , m_expandForSpecialElements(expandForSpecialElements) |
83 | , m_pruneStartBlockIfNecessary(false) |
84 | , m_startsAtEmptyLine(false) |
85 | , m_sanitizeMarkup(sanitizeMarkup) |
86 | { |
87 | } |
88 | |
89 | DeleteSelectionCommand::DeleteSelectionCommand(const VisibleSelection& selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements, bool sanitizeMarkup, EditAction editingAction) |
90 | : CompositeEditCommand(selection.start().anchorNode()->document(), editingAction) |
91 | , m_hasSelectionToDelete(true) |
92 | , m_smartDelete(smartDelete) |
93 | , m_mergeBlocksAfterDelete(mergeBlocksAfterDelete) |
94 | , m_needPlaceholder(false) |
95 | , m_replace(replace) |
96 | , m_expandForSpecialElements(expandForSpecialElements) |
97 | , m_pruneStartBlockIfNecessary(false) |
98 | , m_startsAtEmptyLine(false) |
99 | , m_sanitizeMarkup(sanitizeMarkup) |
100 | , m_selectionToDelete(selection) |
101 | { |
102 | } |
103 | |
104 | void DeleteSelectionCommand::initializeStartEnd(Position& start, Position& end) |
105 | { |
106 | HTMLElement* startSpecialContainer = nullptr; |
107 | HTMLElement* endSpecialContainer = nullptr; |
108 | |
109 | start = m_selectionToDelete.start(); |
110 | end = m_selectionToDelete.end(); |
111 | |
112 | // For HRs, we'll get a position at (HR,1) when hitting delete from the beginning of the previous line, or (HR,0) when forward deleting, |
113 | // but in these cases, we want to delete it, so manually expand the selection |
114 | if (start.deprecatedNode()->hasTagName(hrTag)) |
115 | start = positionBeforeNode(start.deprecatedNode()); |
116 | else if (end.deprecatedNode()->hasTagName(hrTag)) |
117 | end = positionAfterNode(end.deprecatedNode()); |
118 | |
119 | // FIXME: This is only used so that moveParagraphs can avoid the bugs in special element expansion. |
120 | if (!m_expandForSpecialElements) |
121 | return; |
122 | |
123 | while (1) { |
124 | startSpecialContainer = nullptr; |
125 | endSpecialContainer = nullptr; |
126 | |
127 | Position s = positionBeforeContainingSpecialElement(start, &startSpecialContainer); |
128 | Position e = positionAfterContainingSpecialElement(end, &endSpecialContainer); |
129 | |
130 | if (!startSpecialContainer && !endSpecialContainer) |
131 | break; |
132 | |
133 | m_mergeBlocksAfterDelete = false; |
134 | |
135 | if (VisiblePosition(start) != m_selectionToDelete.visibleStart() || VisiblePosition(end) != m_selectionToDelete.visibleEnd()) |
136 | break; |
137 | |
138 | // If we're going to expand to include the startSpecialContainer, it must be fully selected. |
139 | if (startSpecialContainer && !endSpecialContainer && comparePositions(positionInParentAfterNode(startSpecialContainer), end) > -1) |
140 | break; |
141 | |
142 | // If we're going to expand to include the endSpecialContainer, it must be fully selected. |
143 | if (endSpecialContainer && !startSpecialContainer && comparePositions(start, positionInParentBeforeNode(endSpecialContainer)) > -1) |
144 | break; |
145 | |
146 | if (startSpecialContainer && startSpecialContainer->isDescendantOf(endSpecialContainer)) |
147 | // Don't adjust the end yet, it is the end of a special element that contains the start |
148 | // special element (which may or may not be fully selected). |
149 | start = s; |
150 | else if (endSpecialContainer && endSpecialContainer->isDescendantOf(startSpecialContainer)) |
151 | // Don't adjust the start yet, it is the start of a special element that contains the end |
152 | // special element (which may or may not be fully selected). |
153 | end = e; |
154 | else { |
155 | start = s; |
156 | end = e; |
157 | } |
158 | } |
159 | } |
160 | |
161 | void DeleteSelectionCommand::setStartingSelectionOnSmartDelete(const Position& start, const Position& end) |
162 | { |
163 | VisiblePosition newBase; |
164 | VisiblePosition newExtent; |
165 | if (startingSelection().isBaseFirst()) { |
166 | newBase = start; |
167 | newExtent = end; |
168 | } else { |
169 | newBase = end; |
170 | newExtent = start; |
171 | } |
172 | setStartingSelection(VisibleSelection(newBase, newExtent, startingSelection().isDirectional())); |
173 | } |
174 | |
175 | bool DeleteSelectionCommand::shouldSmartDeleteParagraphSpacers() |
176 | { |
177 | return document().editingBehavior().shouldSmartInsertDeleteParagraphs(); |
178 | } |
179 | |
180 | void DeleteSelectionCommand::smartDeleteParagraphSpacers() |
181 | { |
182 | VisiblePosition visibleStart { m_upstreamStart }; |
183 | VisiblePosition visibleEnd { m_downstreamEnd }; |
184 | bool selectionEndsInParagraphSeperator = isEndOfParagraph(visibleEnd); |
185 | bool selectionEndIsEndOfContent = endOfEditableContent(visibleEnd) == visibleEnd; |
186 | bool startAndEndInSameUnsplittableElement = unsplittableElementForPosition(visibleStart.deepEquivalent()) == unsplittableElementForPosition(visibleEnd.deepEquivalent()); |
187 | visibleStart = visibleStart.previous(CannotCrossEditingBoundary); |
188 | visibleEnd = visibleEnd.next(CannotCrossEditingBoundary); |
189 | bool previousPositionIsBlankParagraph = isBlankParagraph(visibleStart); |
190 | bool endPositonIsBlankParagraph = isBlankParagraph(visibleEnd); |
191 | bool hasBlankParagraphAfterEndOrIsEndOfContent = !selectionEndIsEndOfContent && (endPositonIsBlankParagraph || selectionEndsInParagraphSeperator); |
192 | if (startAndEndInSameUnsplittableElement && previousPositionIsBlankParagraph && hasBlankParagraphAfterEndOrIsEndOfContent) { |
193 | m_needPlaceholder = false; |
194 | Position position; |
195 | if (endPositonIsBlankParagraph) |
196 | position = startOfNextParagraph(startOfNextParagraph(m_downstreamEnd)).deepEquivalent(); |
197 | else |
198 | position = VisiblePosition(m_downstreamEnd).next().deepEquivalent(); |
199 | m_upstreamEnd = position.upstream(); |
200 | m_downstreamEnd = position.downstream(); |
201 | m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY); |
202 | setStartingSelectionOnSmartDelete(m_upstreamStart, m_downstreamEnd); |
203 | } |
204 | if (startAndEndInSameUnsplittableElement && selectionEndIsEndOfContent && previousPositionIsBlankParagraph && selectionEndsInParagraphSeperator) { |
205 | m_needPlaceholder = false; |
206 | VisiblePosition endOfParagraphBeforeStart = endOfParagraph(VisiblePosition { m_upstreamStart }.previous().previous()); |
207 | Position position = endOfParagraphBeforeStart.deepEquivalent(); |
208 | m_upstreamStart = position.upstream(); |
209 | m_downstreamStart = position.downstream(); |
210 | m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(DOWNSTREAM); |
211 | setStartingSelectionOnSmartDelete(m_upstreamStart, m_upstreamEnd); |
212 | } |
213 | } |
214 | |
215 | bool DeleteSelectionCommand::initializePositionData() |
216 | { |
217 | Position start, end; |
218 | initializeStartEnd(start, end); |
219 | |
220 | if (!isEditablePosition(start, ContentIsEditable)) |
221 | start = firstEditablePositionAfterPositionInRoot(start, highestEditableRoot(start)); |
222 | if (!isEditablePosition(end, ContentIsEditable)) |
223 | end = lastEditablePositionBeforePositionInRoot(end, highestEditableRoot(start)); |
224 | |
225 | if (start.isNull() || end.isNull()) |
226 | return false; |
227 | |
228 | m_upstreamStart = start.upstream(); |
229 | m_downstreamStart = start.downstream(); |
230 | m_upstreamEnd = end.upstream(); |
231 | m_downstreamEnd = end.downstream(); |
232 | |
233 | m_startRoot = editableRootForPosition(start); |
234 | m_endRoot = editableRootForPosition(end); |
235 | |
236 | m_startTableRow = enclosingNodeOfType(start, &isTableRow); |
237 | m_endTableRow = enclosingNodeOfType(end, &isTableRow); |
238 | |
239 | // Don't move content out of a table cell. |
240 | // If the cell is non-editable, enclosingNodeOfType won't return it by default, so |
241 | // tell that function that we don't care if it returns non-editable nodes. |
242 | Node* startCell = enclosingNodeOfType(m_upstreamStart, &isTableCell, CanCrossEditingBoundary); |
243 | Node* endCell = enclosingNodeOfType(m_downstreamEnd, &isTableCell, CanCrossEditingBoundary); |
244 | // FIXME: This isn't right. A borderless table with two rows and a single column would appear as two paragraphs. |
245 | if (endCell && endCell != startCell) |
246 | m_mergeBlocksAfterDelete = false; |
247 | |
248 | // Usually the start and the end of the selection to delete are pulled together as a result of the deletion. |
249 | // Sometimes they aren't (like when no merge is requested), so we must choose one position to hold the caret |
250 | // and receive the placeholder after deletion. |
251 | VisiblePosition visibleEnd(m_downstreamEnd); |
252 | if (m_mergeBlocksAfterDelete && !isEndOfParagraph(visibleEnd)) |
253 | m_endingPosition = m_downstreamEnd; |
254 | else |
255 | m_endingPosition = m_downstreamStart; |
256 | |
257 | // We don't want to merge into a block if it will mean changing the quote level of content after deleting |
258 | // selections that contain a whole number paragraphs plus a line break, since it is unclear to most users |
259 | // that such a selection actually ends at the start of the next paragraph. This matches TextEdit behavior |
260 | // for indented paragraphs. |
261 | // Only apply this rule if the endingSelection is a range selection. If it is a caret, then other operations have created |
262 | // the selection we're deleting (like the process of creating a selection to delete during a backspace), and the user isn't in the situation described above. |
263 | if (numEnclosingMailBlockquotes(start) != numEnclosingMailBlockquotes(end) |
264 | && isStartOfParagraph(visibleEnd) && isStartOfParagraph(VisiblePosition(start)) |
265 | && endingSelection().isRange()) { |
266 | m_mergeBlocksAfterDelete = false; |
267 | m_pruneStartBlockIfNecessary = true; |
268 | } |
269 | |
270 | // Handle leading and trailing whitespace, as well as smart delete adjustments to the selection |
271 | m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.affinity()); |
272 | m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY); |
273 | |
274 | if (m_smartDelete) { |
275 | |
276 | // skip smart delete if the selection to delete already starts or ends with whitespace |
277 | Position pos = VisiblePosition(m_upstreamStart, m_selectionToDelete.affinity()).deepEquivalent(); |
278 | bool skipSmartDelete = pos.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull(); |
279 | if (!skipSmartDelete) |
280 | skipSmartDelete = m_downstreamEnd.leadingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull(); |
281 | |
282 | // extend selection upstream if there is whitespace there |
283 | bool hasLeadingWhitespaceBeforeAdjustment = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.affinity(), true).isNotNull(); |
284 | if (!skipSmartDelete && hasLeadingWhitespaceBeforeAdjustment) { |
285 | VisiblePosition visiblePos = VisiblePosition(m_upstreamStart, VP_DEFAULT_AFFINITY).previous(); |
286 | pos = visiblePos.deepEquivalent(); |
287 | // Expand out one character upstream for smart delete and recalculate |
288 | // positions based on this change. |
289 | m_upstreamStart = pos.upstream(); |
290 | m_downstreamStart = pos.downstream(); |
291 | m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(visiblePos.affinity()); |
292 | |
293 | setStartingSelectionOnSmartDelete(m_upstreamStart, m_upstreamEnd); |
294 | } |
295 | |
296 | // trailing whitespace is only considered for smart delete if there is no leading |
297 | // whitespace, as in the case where you double-click the first word of a paragraph. |
298 | if (!skipSmartDelete && !hasLeadingWhitespaceBeforeAdjustment && m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull()) { |
299 | // Expand out one character downstream for smart delete and recalculate |
300 | // positions based on this change. |
301 | pos = VisiblePosition(m_downstreamEnd, VP_DEFAULT_AFFINITY).next().deepEquivalent(); |
302 | m_upstreamEnd = pos.upstream(); |
303 | m_downstreamEnd = pos.downstream(); |
304 | m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY); |
305 | |
306 | setStartingSelectionOnSmartDelete(m_downstreamStart, m_downstreamEnd); |
307 | } |
308 | |
309 | if (shouldSmartDeleteParagraphSpacers()) |
310 | smartDeleteParagraphSpacers(); |
311 | } |
312 | |
313 | // We must pass call parentAnchoredEquivalent on the positions since some editing positions |
314 | // that appear inside their nodes aren't really inside them. [hr, 0] is one example. |
315 | // FIXME: parentAnchoredEquivalent should eventually be moved into enclosing element getters |
316 | // like the one below, since editing functions should obviously accept editing positions. |
317 | // FIXME: Passing false to enclosingNodeOfType tells it that it's OK to return a non-editable |
318 | // node. This was done to match existing behavior, but it seems wrong. |
319 | m_startBlock = enclosingNodeOfType(m_downstreamStart.parentAnchoredEquivalent(), &isBlock, CanCrossEditingBoundary); |
320 | m_endBlock = enclosingNodeOfType(m_upstreamEnd.parentAnchoredEquivalent(), &isBlock, CanCrossEditingBoundary); |
321 | |
322 | return true; |
323 | } |
324 | |
325 | void DeleteSelectionCommand::saveTypingStyleState() |
326 | { |
327 | // A common case is deleting characters that are all from the same text node. In |
328 | // that case, the style at the start of the selection before deletion will be the |
329 | // same as the style at the start of the selection after deletion (since those |
330 | // two positions will be identical). Therefore there is no need to save the |
331 | // typing style at the start of the selection, nor is there a reason to |
332 | // compute the style at the start of the selection after deletion (see the |
333 | // early return in calculateTypingStyleAfterDelete). |
334 | // However, if typing style was previously set from another text node at the previous |
335 | // position (now deleted), we need to clear that style as well. |
336 | if (m_upstreamStart.deprecatedNode() == m_downstreamEnd.deprecatedNode() && m_upstreamStart.deprecatedNode()->isTextNode()) { |
337 | frame().selection().clearTypingStyle(); |
338 | return; |
339 | } |
340 | |
341 | // Figure out the typing style in effect before the delete is done. |
342 | m_typingStyle = EditingStyle::create(m_selectionToDelete.start(), EditingStyle::EditingPropertiesInEffect); |
343 | m_typingStyle->removeStyleAddedByNode(enclosingAnchorElement(m_selectionToDelete.start())); |
344 | |
345 | // If we're deleting into a Mail blockquote, save the style at end() instead of start() |
346 | // We'll use this later in computeTypingStyleAfterDelete if we end up outside of a Mail blockquote |
347 | if (enclosingNodeOfType(m_selectionToDelete.start(), isMailBlockquote)) |
348 | m_deleteIntoBlockquoteStyle = EditingStyle::create(m_selectionToDelete.end()); |
349 | else |
350 | m_deleteIntoBlockquoteStyle = nullptr; |
351 | } |
352 | |
353 | bool DeleteSelectionCommand::handleSpecialCaseBRDelete() |
354 | { |
355 | Node* nodeAfterUpstreamStart = m_upstreamStart.computeNodeAfterPosition(); |
356 | Node* nodeAfterDownstreamStart = m_downstreamStart.computeNodeAfterPosition(); |
357 | // Upstream end will appear before BR due to canonicalization |
358 | Node* nodeAfterUpstreamEnd = m_upstreamEnd.computeNodeAfterPosition(); |
359 | |
360 | if (!nodeAfterUpstreamStart || !nodeAfterDownstreamStart) |
361 | return false; |
362 | |
363 | // Check for special-case where the selection contains only a BR on a line by itself after another BR. |
364 | bool upstreamStartIsBR = nodeAfterUpstreamStart->hasTagName(brTag); |
365 | bool downstreamStartIsBR = nodeAfterDownstreamStart->hasTagName(brTag); |
366 | // We should consider that the BR is on a line by itself also when we have <br><br>. This test should be true only |
367 | // when the two elements are siblings and should be false in a case like <div><br></div><br>. |
368 | bool isBROnLineByItself = upstreamStartIsBR && downstreamStartIsBR && ((nodeAfterDownstreamStart == nodeAfterUpstreamEnd) || (nodeAfterUpstreamEnd && nodeAfterUpstreamEnd->hasTagName(brTag) && nodeAfterUpstreamStart->nextSibling() == nodeAfterUpstreamEnd)); |
369 | |
370 | if (isBROnLineByItself) { |
371 | removeNode(*nodeAfterDownstreamStart); |
372 | return true; |
373 | } |
374 | |
375 | // FIXME: This code doesn't belong in here. |
376 | // We detect the case where the start is an empty line consisting of BR not wrapped in a block element. |
377 | if (upstreamStartIsBR && downstreamStartIsBR |
378 | && !(isStartOfBlock(positionBeforeNode(nodeAfterUpstreamStart)) && isEndOfBlock(positionAfterNode(nodeAfterDownstreamStart))) |
379 | && (!nodeAfterUpstreamEnd || nodeAfterUpstreamEnd->hasTagName(brTag) || nodeAfterUpstreamEnd->previousSibling() != nodeAfterUpstreamStart)) { |
380 | m_startsAtEmptyLine = true; |
381 | m_endingPosition = m_downstreamEnd; |
382 | } |
383 | |
384 | return false; |
385 | } |
386 | |
387 | static Position firstEditablePositionInNode(Node* node) |
388 | { |
389 | ASSERT(node); |
390 | Node* next = node; |
391 | while (next && !next->hasEditableStyle()) |
392 | next = NodeTraversal::next(*next, node); |
393 | return next ? firstPositionInOrBeforeNode(next) : Position(); |
394 | } |
395 | |
396 | void DeleteSelectionCommand::insertBlockPlaceholderForTableCellIfNeeded(Element& element) |
397 | { |
398 | // Make sure empty cell has some height. |
399 | auto* renderer = element.renderer(); |
400 | if (!is<RenderTableCell>(renderer)) |
401 | return; |
402 | if (downcast<RenderTableCell>(*renderer).contentHeight() > 0) |
403 | return; |
404 | insertBlockPlaceholder(firstEditablePositionInNode(&element)); |
405 | } |
406 | |
407 | void DeleteSelectionCommand::removeNodeUpdatingStates(Node& node, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable) |
408 | { |
409 | if (&node == m_startBlock && !isEndOfBlock(VisiblePosition(firstPositionInNode(m_startBlock.get())).previous())) |
410 | m_needPlaceholder = true; |
411 | else if (&node == m_endBlock && !isStartOfBlock(VisiblePosition(lastPositionInNode(m_startBlock.get())).next())) |
412 | m_needPlaceholder = true; |
413 | |
414 | // FIXME: Update the endpoints of the range being deleted. |
415 | updatePositionForNodeRemoval(m_endingPosition, node); |
416 | updatePositionForNodeRemoval(m_leadingWhitespace, node); |
417 | updatePositionForNodeRemoval(m_trailingWhitespace, node); |
418 | |
419 | CompositeEditCommand::removeNode(node, shouldAssumeContentIsAlwaysEditable); |
420 | } |
421 | |
422 | static inline bool shouldRemoveContentOnly(const Node& node) |
423 | { |
424 | return isTableStructureNode(&node) || node.isRootEditableElement(); |
425 | } |
426 | |
427 | void DeleteSelectionCommand::removeNode(Node& node, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable) |
428 | { |
429 | Ref<Node> protectedNode = node; |
430 | if (m_startRoot != m_endRoot && !(node.isDescendantOf(m_startRoot.get()) && node.isDescendantOf(m_endRoot.get()))) { |
431 | // If a node is not in both the start and end editable roots, remove it only if its inside an editable region. |
432 | if (!node.parentNode()->hasEditableStyle()) { |
433 | // Don't remove non-editable atomic nodes. |
434 | if (!node.firstChild()) |
435 | return; |
436 | // Search this non-editable region for editable regions to empty. |
437 | RefPtr<Node> child = node.firstChild(); |
438 | while (child) { |
439 | RefPtr<Node> nextChild = child->nextSibling(); |
440 | removeNode(*child, shouldAssumeContentIsAlwaysEditable); |
441 | // Bail if nextChild is no longer node's child. |
442 | if (nextChild && nextChild->parentNode() != &node) |
443 | return; |
444 | child = nextChild; |
445 | } |
446 | |
447 | // Don't remove editable regions that are inside non-editable ones, just clear them. |
448 | return; |
449 | } |
450 | } |
451 | |
452 | if (shouldRemoveContentOnly(node)) { |
453 | // Do not remove an element of table structure; remove its contents. |
454 | // Likewise for the root editable element. |
455 | auto* child = NodeTraversal::next(node, &node); |
456 | while (child) { |
457 | if (shouldRemoveContentOnly(*child)) { |
458 | child = NodeTraversal::next(*child, &node); |
459 | continue; |
460 | } |
461 | auto* remove = child; |
462 | child = NodeTraversal::nextSkippingChildren(*child, &node); |
463 | removeNodeUpdatingStates(*remove, shouldAssumeContentIsAlwaysEditable); |
464 | } |
465 | |
466 | ASSERT(is<Element>(node)); |
467 | auto& element = downcast<Element>(node); |
468 | document().updateLayoutIgnorePendingStylesheets(); |
469 | // Check if we need to insert a placeholder for descendant table cells. |
470 | auto* descendant = ElementTraversal::next(element, &element); |
471 | while (descendant) { |
472 | auto* placeholderCandidate = descendant; |
473 | descendant = ElementTraversal::next(*descendant, &element); |
474 | insertBlockPlaceholderForTableCellIfNeeded(*placeholderCandidate); |
475 | } |
476 | insertBlockPlaceholderForTableCellIfNeeded(element); |
477 | return; |
478 | } |
479 | removeNodeUpdatingStates(node, shouldAssumeContentIsAlwaysEditable); |
480 | } |
481 | |
482 | static void updatePositionForTextRemoval(Node* node, int offset, int count, Position& position) |
483 | { |
484 | if (position.anchorType() != Position::PositionIsOffsetInAnchor || position.containerNode() != node) |
485 | return; |
486 | |
487 | if (position.offsetInContainerNode() > offset + count) |
488 | position.moveToOffset(position.offsetInContainerNode() - count); |
489 | else if (position.offsetInContainerNode() > offset) |
490 | position.moveToOffset(offset); |
491 | } |
492 | |
493 | void DeleteSelectionCommand::deleteTextFromNode(Text& node, unsigned offset, unsigned count) |
494 | { |
495 | // FIXME: Update the endpoints of the range being deleted. |
496 | updatePositionForTextRemoval(&node, offset, count, m_endingPosition); |
497 | updatePositionForTextRemoval(&node, offset, count, m_leadingWhitespace); |
498 | updatePositionForTextRemoval(&node, offset, count, m_trailingWhitespace); |
499 | updatePositionForTextRemoval(&node, offset, count, m_downstreamEnd); |
500 | |
501 | CompositeEditCommand::deleteTextFromNode(node, offset, count); |
502 | } |
503 | |
504 | void DeleteSelectionCommand::makeStylingElementsDirectChildrenOfEditableRootToPreventStyleLoss() |
505 | { |
506 | RefPtr<Range> range = m_selectionToDelete.toNormalizedRange(); |
507 | RefPtr<Node> node = range ? range->firstNode() : nullptr; |
508 | while (node && node != range->pastLastNode()) { |
509 | RefPtr<Node> nextNode = NodeTraversal::next(*node); |
510 | if ((is<HTMLStyleElement>(*node) && !downcast<HTMLStyleElement>(*node).hasAttributeWithoutSynchronization(scopedAttr)) || is<HTMLLinkElement>(*node)) { |
511 | nextNode = NodeTraversal::nextSkippingChildren(*node); |
512 | RefPtr<ContainerNode> rootEditableElement = node->rootEditableElement(); |
513 | if (rootEditableElement) { |
514 | removeNode(*node); |
515 | appendNode(*node, *rootEditableElement); |
516 | } |
517 | } |
518 | node = nextNode; |
519 | } |
520 | } |
521 | |
522 | void DeleteSelectionCommand::handleGeneralDelete() |
523 | { |
524 | if (m_upstreamStart.isNull()) |
525 | return; |
526 | |
527 | int startOffset = m_upstreamStart.deprecatedEditingOffset(); |
528 | Node* startNode = m_upstreamStart.deprecatedNode(); |
529 | |
530 | makeStylingElementsDirectChildrenOfEditableRootToPreventStyleLoss(); |
531 | |
532 | // Never remove the start block unless it's a table, in which case we won't merge content in. |
533 | if (startNode == m_startBlock && !startOffset && canHaveChildrenForEditing(*startNode) && !is<HTMLTableElement>(*startNode)) { |
534 | startOffset = 0; |
535 | startNode = NodeTraversal::next(*startNode); |
536 | if (!startNode) |
537 | return; |
538 | } |
539 | |
540 | int startNodeCaretMaxOffset = caretMaxOffset(*startNode); |
541 | if (startOffset >= startNodeCaretMaxOffset && is<Text>(*startNode)) { |
542 | Text& text = downcast<Text>(*startNode); |
543 | if (text.length() > static_cast<unsigned>(startNodeCaretMaxOffset)) |
544 | deleteTextFromNode(text, startNodeCaretMaxOffset, text.length() - startNodeCaretMaxOffset); |
545 | } |
546 | |
547 | if (startOffset >= lastOffsetForEditing(*startNode)) { |
548 | startNode = NodeTraversal::nextSkippingChildren(*startNode); |
549 | startOffset = 0; |
550 | } |
551 | |
552 | // Done adjusting the start. See if we're all done. |
553 | if (!startNode) |
554 | return; |
555 | |
556 | if (startNode == m_downstreamEnd.deprecatedNode()) { |
557 | if (m_downstreamEnd.deprecatedEditingOffset() - startOffset > 0) { |
558 | if (is<Text>(*startNode)) { |
559 | // in a text node that needs to be trimmed |
560 | Text& text = downcast<Text>(*startNode); |
561 | deleteTextFromNode(text, startOffset, m_downstreamEnd.deprecatedEditingOffset() - startOffset); |
562 | } else { |
563 | removeChildrenInRange(*startNode, startOffset, m_downstreamEnd.deprecatedEditingOffset()); |
564 | m_endingPosition = m_upstreamStart; |
565 | } |
566 | } |
567 | |
568 | // The selection to delete is all in one node. |
569 | if (!startNode->renderer() || (!startOffset && m_downstreamEnd.atLastEditingPositionForNode())) |
570 | removeNode(*startNode); |
571 | } |
572 | else { |
573 | bool startNodeWasDescendantOfEndNode = m_upstreamStart.deprecatedNode()->isDescendantOf(m_downstreamEnd.deprecatedNode()); |
574 | // The selection to delete spans more than one node. |
575 | RefPtr<Node> node(startNode); |
576 | |
577 | if (startOffset > 0) { |
578 | if (is<Text>(*startNode)) { |
579 | // in a text node that needs to be trimmed |
580 | Text& text = downcast<Text>(*node); |
581 | deleteTextFromNode(text, startOffset, text.length() - startOffset); |
582 | node = NodeTraversal::next(*node); |
583 | } else { |
584 | node = startNode->traverseToChildAt(startOffset); |
585 | } |
586 | } else if (startNode == m_upstreamEnd.deprecatedNode() && is<Text>(*startNode)) { |
587 | Text& text = downcast<Text>(*m_upstreamEnd.deprecatedNode()); |
588 | deleteTextFromNode(text, 0, m_upstreamEnd.deprecatedEditingOffset()); |
589 | } |
590 | |
591 | // handle deleting all nodes that are completely selected |
592 | while (node && node != m_downstreamEnd.deprecatedNode()) { |
593 | if (comparePositions(firstPositionInOrBeforeNode(node.get()), m_downstreamEnd) >= 0) { |
594 | // NodeTraversal::nextSkippingChildren just blew past the end position, so stop deleting |
595 | node = nullptr; |
596 | } else if (!m_downstreamEnd.deprecatedNode()->isDescendantOf(*node)) { |
597 | RefPtr<Node> nextNode = NodeTraversal::nextSkippingChildren(*node); |
598 | // if we just removed a node from the end container, update end position so the |
599 | // check above will work |
600 | updatePositionForNodeRemoval(m_downstreamEnd, *node); |
601 | removeNode(*node); |
602 | node = nextNode.get(); |
603 | } else { |
604 | Node* n = node->lastDescendant(); |
605 | if (m_downstreamEnd.deprecatedNode() == n && m_downstreamEnd.deprecatedEditingOffset() >= caretMaxOffset(*n)) { |
606 | removeNode(*node); |
607 | node = nullptr; |
608 | } else |
609 | node = NodeTraversal::next(*node); |
610 | } |
611 | } |
612 | |
613 | if (m_downstreamEnd.deprecatedNode() != startNode && !m_upstreamStart.deprecatedNode()->isDescendantOf(m_downstreamEnd.deprecatedNode()) && m_downstreamEnd.anchorNode()->isConnected() && m_downstreamEnd.deprecatedEditingOffset() >= caretMinOffset(*m_downstreamEnd.deprecatedNode())) { |
614 | if (m_downstreamEnd.atLastEditingPositionForNode() && !canHaveChildrenForEditing(*m_downstreamEnd.deprecatedNode())) { |
615 | // The node itself is fully selected, not just its contents. Delete it. |
616 | removeNode(*m_downstreamEnd.deprecatedNode()); |
617 | } else { |
618 | if (is<Text>(*m_downstreamEnd.deprecatedNode())) { |
619 | // in a text node that needs to be trimmed |
620 | Text& text = downcast<Text>(*m_downstreamEnd.deprecatedNode()); |
621 | if (m_downstreamEnd.deprecatedEditingOffset() > 0) { |
622 | deleteTextFromNode(text, 0, m_downstreamEnd.deprecatedEditingOffset()); |
623 | } |
624 | // Remove children of m_downstreamEnd.deprecatedNode() that come after m_upstreamStart. |
625 | // Don't try to remove children if m_upstreamStart was inside m_downstreamEnd.deprecatedNode() |
626 | // and m_upstreamStart has been removed from the document, because then we don't |
627 | // know how many children to remove. |
628 | // FIXME: Make m_upstreamStart a position we update as we remove content, then we can |
629 | // always know which children to remove. |
630 | } else if (!(startNodeWasDescendantOfEndNode && !m_upstreamStart.anchorNode()->isConnected())) { |
631 | unsigned offset = 0; |
632 | if (m_upstreamStart.deprecatedNode()->isDescendantOf(m_downstreamEnd.deprecatedNode())) { |
633 | Node* n = m_upstreamStart.deprecatedNode(); |
634 | while (n && n->parentNode() != m_downstreamEnd.deprecatedNode()) |
635 | n = n->parentNode(); |
636 | if (n) |
637 | offset = n->computeNodeIndex() + 1; |
638 | } |
639 | removeChildrenInRange(*m_downstreamEnd.deprecatedNode(), offset, m_downstreamEnd.deprecatedEditingOffset()); |
640 | m_downstreamEnd = createLegacyEditingPosition(m_downstreamEnd.deprecatedNode(), offset); |
641 | } |
642 | } |
643 | } |
644 | } |
645 | } |
646 | |
647 | void DeleteSelectionCommand::fixupWhitespace() |
648 | { |
649 | document().updateLayoutIgnorePendingStylesheets(); |
650 | // FIXME: isRenderedCharacter should be removed, and we should use VisiblePosition::characterAfter and VisiblePosition::characterBefore |
651 | if (m_leadingWhitespace.isNotNull() && !m_leadingWhitespace.isRenderedCharacter() && is<Text>(*m_leadingWhitespace.deprecatedNode())) { |
652 | Text& textNode = downcast<Text>(*m_leadingWhitespace.deprecatedNode()); |
653 | ASSERT(!textNode.renderer() || textNode.renderer()->style().collapseWhiteSpace()); |
654 | replaceTextInNodePreservingMarkers(textNode, m_leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); |
655 | } |
656 | if (m_trailingWhitespace.isNotNull() && !m_trailingWhitespace.isRenderedCharacter() && is<Text>(*m_trailingWhitespace.deprecatedNode())) { |
657 | Text& textNode = downcast<Text>(*m_trailingWhitespace.deprecatedNode()); |
658 | ASSERT(!textNode.renderer() || textNode.renderer()->style().collapseWhiteSpace()); |
659 | replaceTextInNodePreservingMarkers(textNode, m_trailingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); |
660 | } |
661 | } |
662 | |
663 | // If a selection starts in one block and ends in another, we have to merge to bring content before the |
664 | // start together with content after the end. |
665 | void DeleteSelectionCommand::mergeParagraphs() |
666 | { |
667 | if (!m_mergeBlocksAfterDelete) { |
668 | if (m_pruneStartBlockIfNecessary) { |
669 | // We aren't going to merge into the start block, so remove it if it's empty. |
670 | prune(m_startBlock.get()); |
671 | // Removing the start block during a deletion is usually an indication that we need |
672 | // a placeholder, but not in this case. |
673 | m_needPlaceholder = false; |
674 | } |
675 | return; |
676 | } |
677 | |
678 | // It shouldn't have been asked to both try and merge content into the start block and prune it. |
679 | ASSERT(!m_pruneStartBlockIfNecessary); |
680 | |
681 | // FIXME: Deletion should adjust selection endpoints as it removes nodes so that we never get into this state (4099839). |
682 | if (!m_downstreamEnd.anchorNode()->isConnected() || !m_upstreamStart.anchorNode()->isConnected()) |
683 | return; |
684 | |
685 | // FIXME: The deletion algorithm shouldn't let this happen. |
686 | if (comparePositions(m_upstreamStart, m_downstreamEnd) > 0) |
687 | return; |
688 | |
689 | // There's nothing to merge. |
690 | if (m_upstreamStart == m_downstreamEnd) |
691 | return; |
692 | |
693 | VisiblePosition startOfParagraphToMove(m_downstreamEnd); |
694 | VisiblePosition mergeDestination(m_upstreamStart); |
695 | |
696 | // m_downstreamEnd's block has been emptied out by deletion. There is no content inside of it to |
697 | // move, so just remove it. |
698 | Element* endBlock = enclosingBlock(m_downstreamEnd.deprecatedNode()); |
699 | if (!endBlock) |
700 | return; |
701 | |
702 | if (!endBlock->contains(startOfParagraphToMove.deepEquivalent().deprecatedNode()) || !startOfParagraphToMove.deepEquivalent().deprecatedNode()) { |
703 | removeNode(*endBlock); |
704 | return; |
705 | } |
706 | |
707 | // We need to merge into m_upstreamStart's block, but it's been emptied out and collapsed by deletion. |
708 | if (!mergeDestination.deepEquivalent().deprecatedNode() || !mergeDestination.deepEquivalent().deprecatedNode()->isDescendantOf(enclosingBlock(m_upstreamStart.containerNode())) || m_startsAtEmptyLine) { |
709 | insertNodeAt(HTMLBRElement::create(document()), m_upstreamStart); |
710 | mergeDestination = VisiblePosition(m_upstreamStart); |
711 | } |
712 | |
713 | if (mergeDestination == startOfParagraphToMove) |
714 | return; |
715 | |
716 | VisiblePosition endOfParagraphToMove = endOfParagraph(startOfParagraphToMove, CanSkipOverEditingBoundary); |
717 | |
718 | if (mergeDestination == endOfParagraphToMove) |
719 | return; |
720 | |
721 | // The rule for merging into an empty block is: only do so if its farther to the right. |
722 | // FIXME: Consider RTL. |
723 | if (!m_startsAtEmptyLine && isStartOfParagraph(mergeDestination) && startOfParagraphToMove.absoluteCaretBounds().x() > mergeDestination.absoluteCaretBounds().x()) { |
724 | if (mergeDestination.deepEquivalent().downstream().deprecatedNode()->hasTagName(brTag)) { |
725 | removeNodeAndPruneAncestors(*mergeDestination.deepEquivalent().downstream().deprecatedNode()); |
726 | m_endingPosition = startOfParagraphToMove.deepEquivalent(); |
727 | return; |
728 | } |
729 | } |
730 | |
731 | // Block images, tables and horizontal rules cannot be made inline with content at mergeDestination. If there is |
732 | // any (!isStartOfParagraph(mergeDestination)), don't merge, just move the caret to just before the selection we deleted. |
733 | // See https://bugs.webkit.org/show_bug.cgi?id=25439 |
734 | if (isRenderedAsNonInlineTableImageOrHR(startOfParagraphToMove.deepEquivalent().deprecatedNode()) && !isStartOfParagraph(mergeDestination)) { |
735 | m_endingPosition = m_upstreamStart; |
736 | return; |
737 | } |
738 | |
739 | auto range = Range::create(document(), startOfParagraphToMove.deepEquivalent().parentAnchoredEquivalent(), endOfParagraphToMove.deepEquivalent().parentAnchoredEquivalent()); |
740 | auto rangeToBeReplaced = Range::create(document(), mergeDestination.deepEquivalent().parentAnchoredEquivalent(), mergeDestination.deepEquivalent().parentAnchoredEquivalent()); |
741 | if (!frame().editor().client()->shouldMoveRangeAfterDelete(range.ptr(), rangeToBeReplaced.ptr())) |
742 | return; |
743 | |
744 | // moveParagraphs will insert placeholders if it removes blocks that would require their use, don't let block |
745 | // removals that it does cause the insertion of *another* placeholder. |
746 | bool needPlaceholder = m_needPlaceholder; |
747 | bool paragraphToMergeIsEmpty = (startOfParagraphToMove == endOfParagraphToMove); |
748 | moveParagraph(startOfParagraphToMove, endOfParagraphToMove, mergeDestination, false, !paragraphToMergeIsEmpty); |
749 | m_needPlaceholder = needPlaceholder; |
750 | // The endingPosition was likely clobbered by the move, so recompute it (moveParagraph selects the moved paragraph). |
751 | m_endingPosition = endingSelection().start(); |
752 | } |
753 | |
754 | void DeleteSelectionCommand::removePreviouslySelectedEmptyTableRows() |
755 | { |
756 | if (m_endTableRow && m_endTableRow->isConnected() && m_endTableRow != m_startTableRow) { |
757 | Node* row = m_endTableRow->previousSibling(); |
758 | while (row && row != m_startTableRow) { |
759 | RefPtr<Node> previousRow = row->previousSibling(); |
760 | if (isTableRowEmpty(row)) |
761 | // Use a raw removeNode, instead of DeleteSelectionCommand's, because |
762 | // that won't remove rows, it only empties them in preparation for this function. |
763 | CompositeEditCommand::removeNode(*row); |
764 | row = previousRow.get(); |
765 | } |
766 | } |
767 | |
768 | // Remove empty rows after the start row. |
769 | if (m_startTableRow && m_startTableRow->isConnected() && m_startTableRow != m_endTableRow) { |
770 | Node* row = m_startTableRow->nextSibling(); |
771 | while (row && row != m_endTableRow) { |
772 | RefPtr<Node> nextRow = row->nextSibling(); |
773 | if (isTableRowEmpty(row)) |
774 | CompositeEditCommand::removeNode(*row); |
775 | row = nextRow.get(); |
776 | } |
777 | } |
778 | |
779 | if (m_endTableRow && m_endTableRow->isConnected() && m_endTableRow != m_startTableRow) { |
780 | if (isTableRowEmpty(m_endTableRow.get())) { |
781 | // Don't remove m_endTableRow if it's where we're putting the ending selection. |
782 | if (!m_endingPosition.deprecatedNode()->isDescendantOf(*m_endTableRow)) { |
783 | // FIXME: We probably shouldn't remove m_endTableRow unless it's fully selected, even if it is empty. |
784 | // We'll need to start adjusting the selection endpoints during deletion to know whether or not m_endTableRow |
785 | // was fully selected here. |
786 | CompositeEditCommand::removeNode(*m_endTableRow); |
787 | } |
788 | } |
789 | } |
790 | } |
791 | |
792 | void DeleteSelectionCommand::calculateTypingStyleAfterDelete() |
793 | { |
794 | if (!m_typingStyle) |
795 | return; |
796 | |
797 | // Compute the difference between the style before the delete and the style now |
798 | // after the delete has been done. Set this style on the frame, so other editing |
799 | // commands being composed with this one will work, and also cache it on the command, |
800 | // so the Frame::appliedEditing can set it after the whole composite command |
801 | // has completed. |
802 | |
803 | // If we deleted into a blockquote, but are now no longer in a blockquote, use the alternate typing style |
804 | if (m_deleteIntoBlockquoteStyle && !enclosingNodeOfType(m_endingPosition, isMailBlockquote, CanCrossEditingBoundary)) |
805 | m_typingStyle = m_deleteIntoBlockquoteStyle; |
806 | m_deleteIntoBlockquoteStyle = nullptr; |
807 | |
808 | m_typingStyle->prepareToApplyAt(m_endingPosition); |
809 | if (m_typingStyle->isEmpty()) |
810 | m_typingStyle = nullptr; |
811 | // This is where we've deleted all traces of a style but not a whole paragraph (that's handled above). |
812 | // In this case if we start typing, the new characters should have the same style as the just deleted ones, |
813 | // but, if we change the selection, come back and start typing that style should be lost. Also see |
814 | // preserveTypingStyle() below. |
815 | frame().selection().setTypingStyle(m_typingStyle.copyRef()); |
816 | } |
817 | |
818 | void DeleteSelectionCommand::clearTransientState() |
819 | { |
820 | m_selectionToDelete = VisibleSelection(); |
821 | m_upstreamStart.clear(); |
822 | m_downstreamStart.clear(); |
823 | m_upstreamEnd.clear(); |
824 | m_downstreamEnd.clear(); |
825 | m_endingPosition.clear(); |
826 | m_leadingWhitespace.clear(); |
827 | m_trailingWhitespace.clear(); |
828 | } |
829 | |
830 | String DeleteSelectionCommand::originalStringForAutocorrectionAtBeginningOfSelection() |
831 | { |
832 | if (!m_selectionToDelete.isRange()) |
833 | return String(); |
834 | |
835 | VisiblePosition startOfSelection = m_selectionToDelete.start(); |
836 | if (!isStartOfWord(startOfSelection)) |
837 | return String(); |
838 | |
839 | VisiblePosition nextPosition = startOfSelection.next(); |
840 | if (nextPosition.isNull()) |
841 | return String(); |
842 | |
843 | auto rangeOfFirstCharacter = Range::create(document(), startOfSelection.deepEquivalent(), nextPosition.deepEquivalent()); |
844 | for (auto* marker : document().markers().markersInRange(rangeOfFirstCharacter, DocumentMarker::Autocorrected)) { |
845 | int startOffset = marker->startOffset(); |
846 | if (startOffset == startOfSelection.deepEquivalent().offsetInContainerNode()) |
847 | return marker->description(); |
848 | } |
849 | return String(); |
850 | } |
851 | |
852 | // This method removes div elements with no attributes that have only one child or no children at all. |
853 | void DeleteSelectionCommand::removeRedundantBlocks() |
854 | { |
855 | Node* node = m_endingPosition.containerNode(); |
856 | Node* rootNode = node->rootEditableElement(); |
857 | |
858 | while (node != rootNode) { |
859 | if (isRemovableBlock(node)) { |
860 | if (node == m_endingPosition.anchorNode()) |
861 | updatePositionForNodeRemovalPreservingChildren(m_endingPosition, *node); |
862 | |
863 | CompositeEditCommand::removeNodePreservingChildren(*node); |
864 | node = m_endingPosition.anchorNode(); |
865 | } else |
866 | node = node->parentNode(); |
867 | } |
868 | } |
869 | |
870 | void DeleteSelectionCommand::doApply() |
871 | { |
872 | // If selection has not been set to a custom selection when the command was created, |
873 | // use the current ending selection. |
874 | if (!m_hasSelectionToDelete) |
875 | m_selectionToDelete = endingSelection(); |
876 | |
877 | if (!m_selectionToDelete.isNonOrphanedRange()) |
878 | return; |
879 | |
880 | String originalString = originalStringForAutocorrectionAtBeginningOfSelection(); |
881 | |
882 | // If the deletion is occurring in a text field, and we're not deleting to replace the selection, then let the frame call across the bridge to notify the form delegate. |
883 | if (!m_replace) { |
884 | Element* textControl = enclosingTextFormControl(m_selectionToDelete.start()); |
885 | if (textControl && textControl->focused()) |
886 | frame().editor().textWillBeDeletedInTextField(textControl); |
887 | } |
888 | |
889 | // save this to later make the selection with |
890 | EAffinity affinity = m_selectionToDelete.affinity(); |
891 | |
892 | Position downstreamEnd = m_selectionToDelete.end().downstream(); |
893 | m_needPlaceholder = isStartOfParagraph(m_selectionToDelete.visibleStart(), CanCrossEditingBoundary) |
894 | && isEndOfParagraph(m_selectionToDelete.visibleEnd(), CanCrossEditingBoundary) |
895 | && !lineBreakExistsAtVisiblePosition(m_selectionToDelete.visibleEnd()); |
896 | if (m_needPlaceholder) { |
897 | // Don't need a placeholder when deleting a selection that starts just before a table |
898 | // and ends inside it (we do need placeholders to hold open empty cells, but that's |
899 | // handled elsewhere). |
900 | if (auto* table = isLastPositionBeforeTable(m_selectionToDelete.visibleStart())) { |
901 | if (m_selectionToDelete.end().deprecatedNode()->isDescendantOf(*table)) |
902 | m_needPlaceholder = false; |
903 | } |
904 | } |
905 | |
906 | |
907 | // set up our state |
908 | if (!initializePositionData()) |
909 | return; |
910 | |
911 | // Delete any text that may hinder our ability to fixup whitespace after the delete |
912 | deleteInsignificantTextDownstream(m_trailingWhitespace); |
913 | |
914 | saveTypingStyleState(); |
915 | |
916 | // deleting just a BR is handled specially, at least because we do not |
917 | // want to replace it with a placeholder BR! |
918 | if (handleSpecialCaseBRDelete()) { |
919 | calculateTypingStyleAfterDelete(); |
920 | setEndingSelection(VisibleSelection(m_endingPosition, affinity, endingSelection().isDirectional())); |
921 | clearTransientState(); |
922 | rebalanceWhitespace(); |
923 | return; |
924 | } |
925 | |
926 | handleGeneralDelete(); |
927 | |
928 | fixupWhitespace(); |
929 | |
930 | mergeParagraphs(); |
931 | |
932 | removePreviouslySelectedEmptyTableRows(); |
933 | |
934 | if (m_needPlaceholder) { |
935 | if (m_sanitizeMarkup) |
936 | removeRedundantBlocks(); |
937 | insertNodeAt(HTMLBRElement::create(document()), m_endingPosition); |
938 | } |
939 | |
940 | bool shouldRebalaceWhiteSpace = true; |
941 | if (!frame().editor().behavior().shouldRebalanceWhiteSpacesInSecureField()) { |
942 | Node* node = m_endingPosition.deprecatedNode(); |
943 | if (is<Text>(node)) { |
944 | Text& textNode = downcast<Text>(*node); |
945 | if (textNode.length() && textNode.renderer()) |
946 | shouldRebalaceWhiteSpace = textNode.renderer()->style().textSecurity() == TextSecurity::None; |
947 | } |
948 | } |
949 | if (shouldRebalaceWhiteSpace) |
950 | rebalanceWhitespaceAt(m_endingPosition); |
951 | |
952 | calculateTypingStyleAfterDelete(); |
953 | |
954 | if (!originalString.isEmpty()) |
955 | frame().editor().deletedAutocorrectionAtPosition(m_endingPosition, originalString); |
956 | |
957 | setEndingSelection(VisibleSelection(m_endingPosition, affinity, endingSelection().isDirectional())); |
958 | clearTransientState(); |
959 | } |
960 | |
961 | // Normally deletion doesn't preserve the typing style that was present before it. For example, |
962 | // type a character, Bold, then delete the character and start typing. The Bold typing style shouldn't |
963 | // stick around. Deletion should preserve a typing style that *it* sets, however. |
964 | bool DeleteSelectionCommand::preservesTypingStyle() const |
965 | { |
966 | return m_typingStyle; |
967 | } |
968 | |
969 | } // namespace WebCore |
970 | |