1 | /* |
2 | * Copyright (C) 2005, 2006, 2007, 2008 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 "CompositeEditCommand.h" |
28 | |
29 | #include "AXObjectCache.h" |
30 | #include "AppendNodeCommand.h" |
31 | #include "ApplyStyleCommand.h" |
32 | #include "BreakBlockquoteCommand.h" |
33 | #include "DataTransfer.h" |
34 | #include "DeleteFromTextNodeCommand.h" |
35 | #include "DeleteSelectionCommand.h" |
36 | #include "Document.h" |
37 | #include "DocumentFragment.h" |
38 | #include "DocumentMarkerController.h" |
39 | #include "Editing.h" |
40 | #include "Editor.h" |
41 | #include "EditorInsertAction.h" |
42 | #include "ElementTraversal.h" |
43 | #include "Event.h" |
44 | #include "Frame.h" |
45 | #include "HTMLBRElement.h" |
46 | #include "HTMLDivElement.h" |
47 | #include "HTMLLIElement.h" |
48 | #include "HTMLNames.h" |
49 | #include "HTMLSpanElement.h" |
50 | #include "InlineTextBox.h" |
51 | #include "InsertIntoTextNodeCommand.h" |
52 | #include "InsertLineBreakCommand.h" |
53 | #include "InsertNodeBeforeCommand.h" |
54 | #include "InsertParagraphSeparatorCommand.h" |
55 | #include "InsertTextCommand.h" |
56 | #include "MergeIdenticalElementsCommand.h" |
57 | #include "NodeTraversal.h" |
58 | #include "Range.h" |
59 | #include "RemoveNodeCommand.h" |
60 | #include "RemoveNodePreservingChildrenCommand.h" |
61 | #include "RenderBlockFlow.h" |
62 | #include "RenderText.h" |
63 | #include "RenderedDocumentMarker.h" |
64 | #include "ReplaceNodeWithSpanCommand.h" |
65 | #include "ReplaceSelectionCommand.h" |
66 | #include "ScopedEventQueue.h" |
67 | #include "SetNodeAttributeCommand.h" |
68 | #include "SplitElementCommand.h" |
69 | #include "SplitTextNodeCommand.h" |
70 | #include "SplitTextNodeContainingElementCommand.h" |
71 | #include "StaticRange.h" |
72 | #include "Text.h" |
73 | #include "TextIterator.h" |
74 | #include "VisibleUnits.h" |
75 | #include "WrapContentsInDummySpanCommand.h" |
76 | #include "markup.h" |
77 | |
78 | namespace WebCore { |
79 | |
80 | using namespace HTMLNames; |
81 | |
82 | int AccessibilityUndoReplacedText::indexForVisiblePosition(const VisiblePosition& position, RefPtr<ContainerNode>& scope) const |
83 | { |
84 | if (position.deepEquivalent().isNull()) |
85 | return -1; |
86 | return WebCore::indexForVisiblePosition(position, scope); |
87 | } |
88 | |
89 | void AccessibilityUndoReplacedText::configureRangeDeletedByReapplyWithEndingSelection(const VisibleSelection& selection) |
90 | { |
91 | if (!AXObjectCache::accessibilityEnabled()) |
92 | return; |
93 | if (selection.isNone()) |
94 | return; |
95 | m_rangeDeletedByReapply.endIndex.value = indexForVisiblePosition(selection.end(), m_rangeDeletedByReapply.endIndex.scope); |
96 | } |
97 | |
98 | void AccessibilityUndoReplacedText::configureRangeDeletedByReapplyWithStartingSelection(const VisibleSelection& selection) |
99 | { |
100 | if (!AXObjectCache::accessibilityEnabled()) |
101 | return; |
102 | if (selection.isNone()) |
103 | return; |
104 | if (m_rangeDeletedByReapply.startIndex.value == -1) |
105 | m_rangeDeletedByReapply.startIndex.value = indexForVisiblePosition(selection.start(), m_rangeDeletedByReapply.startIndex.scope); |
106 | } |
107 | |
108 | void AccessibilityUndoReplacedText::setRangeDeletedByUnapply(const VisiblePositionIndexRange& range) |
109 | { |
110 | if (m_rangeDeletedByUnapply.isNull()) |
111 | m_rangeDeletedByUnapply = range; |
112 | } |
113 | |
114 | void AccessibilityUndoReplacedText::captureTextForUnapply() |
115 | { |
116 | if (!AXObjectCache::accessibilityEnabled()) |
117 | return; |
118 | m_replacedText = textDeletedByReapply(); |
119 | } |
120 | |
121 | void AccessibilityUndoReplacedText::captureTextForReapply() |
122 | { |
123 | if (!AXObjectCache::accessibilityEnabled()) |
124 | return; |
125 | m_replacedText = textDeletedByUnapply(); |
126 | } |
127 | |
128 | static String stringForVisiblePositionIndexRange(const VisiblePositionIndexRange& range) |
129 | { |
130 | if (range.isNull()) |
131 | return String(); |
132 | VisiblePosition start = visiblePositionForIndex(range.startIndex.value, range.startIndex.scope.get()); |
133 | VisiblePosition end = visiblePositionForIndex(range.endIndex.value, range.endIndex.scope.get()); |
134 | return AccessibilityObject::stringForVisiblePositionRange(VisiblePositionRange(start, end)); |
135 | } |
136 | |
137 | String AccessibilityUndoReplacedText::textDeletedByUnapply() |
138 | { |
139 | if (!AXObjectCache::accessibilityEnabled()) |
140 | return String(); |
141 | return stringForVisiblePositionIndexRange(m_rangeDeletedByUnapply); |
142 | } |
143 | |
144 | String AccessibilityUndoReplacedText::textDeletedByReapply() |
145 | { |
146 | if (!AXObjectCache::accessibilityEnabled()) |
147 | return String(); |
148 | return stringForVisiblePositionIndexRange(m_rangeDeletedByReapply); |
149 | } |
150 | |
151 | static void postTextStateChangeNotification(AXObjectCache* cache, const VisiblePosition& position, const String& deletedText, const String& insertedText) |
152 | { |
153 | ASSERT(cache); |
154 | auto* node = highestEditableRoot(position.deepEquivalent(), HasEditableAXRole); |
155 | if (!node) |
156 | return; |
157 | if (insertedText.length() && deletedText.length()) |
158 | cache->postTextReplacementNotification(node, AXTextEditTypeDelete, insertedText, AXTextEditTypeInsert, deletedText, position); |
159 | else if (deletedText.length()) |
160 | cache->postTextStateChangeNotification(node, AXTextEditTypeInsert, deletedText, position); |
161 | else if (insertedText.length()) |
162 | cache->postTextStateChangeNotification(node, AXTextEditTypeDelete, insertedText, position); |
163 | } |
164 | |
165 | void AccessibilityUndoReplacedText::postTextStateChangeNotificationForUnapply(AXObjectCache* cache) |
166 | { |
167 | if (!cache) |
168 | return; |
169 | if (!AXObjectCache::accessibilityEnabled()) |
170 | return; |
171 | if (m_rangeDeletedByUnapply.isNull()) |
172 | return; |
173 | VisiblePosition position = visiblePositionForIndex(m_rangeDeletedByUnapply.endIndex.value, m_rangeDeletedByUnapply.endIndex.scope.get()); |
174 | if (position.isNull()) |
175 | return; |
176 | postTextStateChangeNotification(cache, position, textDeletedByUnapply(), m_replacedText); |
177 | m_replacedText = String(); |
178 | } |
179 | |
180 | void AccessibilityUndoReplacedText::postTextStateChangeNotificationForReapply(AXObjectCache* cache) |
181 | { |
182 | if (!cache) |
183 | return; |
184 | if (!AXObjectCache::accessibilityEnabled()) |
185 | return; |
186 | if (m_rangeDeletedByReapply.isNull()) |
187 | return; |
188 | VisiblePosition position = visiblePositionForIndex(m_rangeDeletedByReapply.startIndex.value, m_rangeDeletedByReapply.startIndex.scope.get()); |
189 | if (position.isNull()) |
190 | return; |
191 | postTextStateChangeNotification(cache, position, textDeletedByReapply(), m_replacedText); |
192 | m_replacedText = String(); |
193 | } |
194 | |
195 | Ref<EditCommandComposition> EditCommandComposition::create(Document& document, |
196 | const VisibleSelection& startingSelection, const VisibleSelection& endingSelection, EditAction editAction) |
197 | { |
198 | return adoptRef(*new EditCommandComposition(document, startingSelection, endingSelection, editAction)); |
199 | } |
200 | |
201 | EditCommandComposition::EditCommandComposition(Document& document, const VisibleSelection& startingSelection, const VisibleSelection& endingSelection, EditAction editAction) |
202 | : m_document(&document) |
203 | , m_startingSelection(startingSelection) |
204 | , m_endingSelection(endingSelection) |
205 | , m_startingRootEditableElement(startingSelection.rootEditableElement()) |
206 | , m_endingRootEditableElement(endingSelection.rootEditableElement()) |
207 | , m_editAction(editAction) |
208 | { |
209 | m_replacedText.configureRangeDeletedByReapplyWithStartingSelection(startingSelection); |
210 | } |
211 | |
212 | void EditCommandComposition::unapply() |
213 | { |
214 | ASSERT(m_document); |
215 | RefPtr<Frame> frame = m_document->frame(); |
216 | if (!frame) |
217 | return; |
218 | |
219 | m_replacedText.captureTextForUnapply(); |
220 | |
221 | // Changes to the document may have been made since the last editing operation that require a layout, as in <rdar://problem/5658603>. |
222 | // Low level operations, like RemoveNodeCommand, don't require a layout because the high level operations that use them perform one |
223 | // if one is necessary (like for the creation of VisiblePositions). |
224 | m_document->updateLayoutIgnorePendingStylesheets(); |
225 | #if PLATFORM(IOS_FAMILY) |
226 | // FIXME: Where should iPhone code deal with the composition? |
227 | // Since editing commands don't save/restore the composition, undoing without fixing |
228 | // up the composition will leave a stale, invalid composition, as in <rdar://problem/6831637>. |
229 | // Desktop handles this in -[WebHTMLView _updateSelectionForInputManager], but the phone |
230 | // goes another route. |
231 | frame->editor().cancelComposition(); |
232 | #endif |
233 | |
234 | if (!frame->editor().willUnapplyEditing(*this)) |
235 | return; |
236 | |
237 | size_t size = m_commands.size(); |
238 | for (size_t i = size; i; --i) |
239 | m_commands[i - 1]->doUnapply(); |
240 | |
241 | frame->editor().unappliedEditing(*this); |
242 | |
243 | if (AXObjectCache::accessibilityEnabled()) |
244 | m_replacedText.postTextStateChangeNotificationForUnapply(m_document->existingAXObjectCache()); |
245 | } |
246 | |
247 | void EditCommandComposition::reapply() |
248 | { |
249 | ASSERT(m_document); |
250 | RefPtr<Frame> frame = m_document->frame(); |
251 | if (!frame) |
252 | return; |
253 | |
254 | m_replacedText.captureTextForReapply(); |
255 | |
256 | // Changes to the document may have been made since the last editing operation that require a layout, as in <rdar://problem/5658603>. |
257 | // Low level operations, like RemoveNodeCommand, don't require a layout because the high level operations that use them perform one |
258 | // if one is necessary (like for the creation of VisiblePositions). |
259 | m_document->updateLayoutIgnorePendingStylesheets(); |
260 | |
261 | if (!frame->editor().willReapplyEditing(*this)) |
262 | return; |
263 | |
264 | for (auto& command : m_commands) |
265 | command->doReapply(); |
266 | |
267 | frame->editor().reappliedEditing(*this); |
268 | |
269 | if (AXObjectCache::accessibilityEnabled()) |
270 | m_replacedText.postTextStateChangeNotificationForReapply(m_document->existingAXObjectCache()); |
271 | } |
272 | |
273 | void EditCommandComposition::append(SimpleEditCommand* command) |
274 | { |
275 | m_commands.append(command); |
276 | } |
277 | |
278 | void EditCommandComposition::setStartingSelection(const VisibleSelection& selection) |
279 | { |
280 | m_startingSelection = selection; |
281 | m_startingRootEditableElement = selection.rootEditableElement(); |
282 | m_replacedText.configureRangeDeletedByReapplyWithStartingSelection(selection); |
283 | } |
284 | |
285 | void EditCommandComposition::setEndingSelection(const VisibleSelection& selection) |
286 | { |
287 | m_endingSelection = selection; |
288 | m_endingRootEditableElement = selection.rootEditableElement(); |
289 | m_replacedText.configureRangeDeletedByReapplyWithEndingSelection(selection); |
290 | } |
291 | |
292 | void EditCommandComposition::setRangeDeletedByUnapply(const VisiblePositionIndexRange& range) |
293 | { |
294 | m_replacedText.setRangeDeletedByUnapply(range); |
295 | } |
296 | |
297 | #ifndef NDEBUG |
298 | void EditCommandComposition::getNodesInCommand(HashSet<Node*>& nodes) |
299 | { |
300 | for (auto& command : m_commands) |
301 | command->getNodesInCommand(nodes); |
302 | } |
303 | #endif |
304 | |
305 | String EditCommandComposition::label() const |
306 | { |
307 | return undoRedoLabel(m_editAction); |
308 | } |
309 | |
310 | CompositeEditCommand::CompositeEditCommand(Document& document, EditAction editingAction) |
311 | : EditCommand(document, editingAction) |
312 | { |
313 | } |
314 | |
315 | CompositeEditCommand::~CompositeEditCommand() |
316 | { |
317 | ASSERT(isTopLevelCommand() || !m_composition); |
318 | } |
319 | |
320 | bool CompositeEditCommand::willApplyCommand() |
321 | { |
322 | return frame().editor().willApplyEditing(*this, targetRangesForBindings()); |
323 | } |
324 | |
325 | void CompositeEditCommand::apply() |
326 | { |
327 | if (!endingSelection().isContentRichlyEditable()) { |
328 | switch (editingAction()) { |
329 | case EditAction::TypingDeleteSelection: |
330 | case EditAction::TypingDeleteBackward: |
331 | case EditAction::TypingDeleteForward: |
332 | case EditAction::TypingDeleteWordBackward: |
333 | case EditAction::TypingDeleteWordForward: |
334 | case EditAction::TypingDeleteLineBackward: |
335 | case EditAction::TypingDeleteLineForward: |
336 | case EditAction::TypingDeletePendingComposition: |
337 | case EditAction::TypingDeleteFinalComposition: |
338 | case EditAction::TypingInsertText: |
339 | case EditAction::TypingInsertLineBreak: |
340 | case EditAction::TypingInsertParagraph: |
341 | case EditAction::TypingInsertPendingComposition: |
342 | case EditAction::TypingInsertFinalComposition: |
343 | case EditAction::Paste: |
344 | case EditAction::DeleteByDrag: |
345 | case EditAction::SetInlineWritingDirection: |
346 | case EditAction::SetBlockWritingDirection: |
347 | case EditAction::Cut: |
348 | case EditAction::Unspecified: |
349 | case EditAction::Insert: |
350 | case EditAction::InsertReplacement: |
351 | case EditAction::InsertFromDrop: |
352 | case EditAction::Delete: |
353 | case EditAction::Dictation: |
354 | break; |
355 | default: |
356 | ASSERT_NOT_REACHED(); |
357 | return; |
358 | } |
359 | } |
360 | ensureComposition(); |
361 | |
362 | // Changes to the document may have been made since the last editing operation that require a layout, as in <rdar://problem/5658603>. |
363 | // Low level operations, like RemoveNodeCommand, don't require a layout because the high level operations that use them perform one |
364 | // if one is necessary (like for the creation of VisiblePositions). |
365 | document().updateLayoutIgnorePendingStylesheets(); |
366 | |
367 | if (!willApplyCommand()) |
368 | return; |
369 | |
370 | { |
371 | EventQueueScope eventQueueScope; |
372 | doApply(); |
373 | } |
374 | |
375 | didApplyCommand(); |
376 | setShouldRetainAutocorrectionIndicator(false); |
377 | } |
378 | |
379 | void CompositeEditCommand::didApplyCommand() |
380 | { |
381 | frame().editor().appliedEditing(*this); |
382 | } |
383 | |
384 | Vector<RefPtr<StaticRange>> CompositeEditCommand::targetRanges() const |
385 | { |
386 | ASSERT(!isEditingTextAreaOrTextInput()); |
387 | auto firstRange = frame().selection().selection().firstRange(); |
388 | if (!firstRange) |
389 | return { }; |
390 | |
391 | return { 1, StaticRange::createFromRange(*firstRange) }; |
392 | } |
393 | |
394 | Vector<RefPtr<StaticRange>> CompositeEditCommand::targetRangesForBindings() const |
395 | { |
396 | if (isEditingTextAreaOrTextInput()) |
397 | return { }; |
398 | |
399 | return targetRanges(); |
400 | } |
401 | |
402 | RefPtr<DataTransfer> CompositeEditCommand::inputEventDataTransfer() const |
403 | { |
404 | return nullptr; |
405 | } |
406 | |
407 | EditCommandComposition* CompositeEditCommand::composition() const |
408 | { |
409 | for (auto* command = this; command; command = command->parent()) { |
410 | if (auto composition = command->m_composition) { |
411 | ASSERT(!command->parent()); |
412 | return composition.get(); |
413 | } |
414 | } |
415 | return nullptr; |
416 | } |
417 | |
418 | EditCommandComposition& CompositeEditCommand::ensureComposition() |
419 | { |
420 | auto* command = this; |
421 | while (auto* parent = command->parent()) |
422 | command = parent; |
423 | if (!command->m_composition) |
424 | command->m_composition = EditCommandComposition::create(document(), startingSelection(), endingSelection(), editingAction()); |
425 | return *command->m_composition; |
426 | } |
427 | |
428 | bool CompositeEditCommand::isCreateLinkCommand() const |
429 | { |
430 | return false; |
431 | } |
432 | |
433 | bool CompositeEditCommand::preservesTypingStyle() const |
434 | { |
435 | return false; |
436 | } |
437 | |
438 | bool CompositeEditCommand::isTypingCommand() const |
439 | { |
440 | return false; |
441 | } |
442 | |
443 | bool CompositeEditCommand::shouldRetainAutocorrectionIndicator() const |
444 | { |
445 | return false; |
446 | } |
447 | |
448 | void CompositeEditCommand::setShouldRetainAutocorrectionIndicator(bool) |
449 | { |
450 | } |
451 | |
452 | String CompositeEditCommand::inputEventTypeName() const |
453 | { |
454 | return inputTypeNameForEditingAction(editingAction()); |
455 | } |
456 | |
457 | // |
458 | // sugary-sweet convenience functions to help create and apply edit commands in composite commands |
459 | // |
460 | void CompositeEditCommand::applyCommandToComposite(Ref<EditCommand>&& command) |
461 | { |
462 | command->setParent(this); |
463 | command->doApply(); |
464 | if (command->isSimpleEditCommand()) { |
465 | command->setParent(nullptr); |
466 | ensureComposition().append(toSimpleEditCommand(command.ptr())); |
467 | } |
468 | m_commands.append(WTFMove(command)); |
469 | } |
470 | |
471 | void CompositeEditCommand::applyCommandToComposite(Ref<CompositeEditCommand>&& command, const VisibleSelection& selection) |
472 | { |
473 | command->setParent(this); |
474 | if (selection != command->endingSelection()) { |
475 | command->setStartingSelection(selection); |
476 | command->setEndingSelection(selection); |
477 | } |
478 | command->doApply(); |
479 | m_commands.append(WTFMove(command)); |
480 | } |
481 | |
482 | void CompositeEditCommand::applyStyle(const EditingStyle* style, EditAction editingAction) |
483 | { |
484 | applyCommandToComposite(ApplyStyleCommand::create(document(), style, editingAction)); |
485 | } |
486 | |
487 | void CompositeEditCommand::applyStyle(const EditingStyle* style, const Position& start, const Position& end, EditAction editingAction) |
488 | { |
489 | applyCommandToComposite(ApplyStyleCommand::create(document(), style, start, end, editingAction)); |
490 | } |
491 | |
492 | void CompositeEditCommand::applyStyledElement(Ref<Element>&& element) |
493 | { |
494 | applyCommandToComposite(ApplyStyleCommand::create(WTFMove(element), false)); |
495 | } |
496 | |
497 | void CompositeEditCommand::removeStyledElement(Ref<Element>&& element) |
498 | { |
499 | applyCommandToComposite(ApplyStyleCommand::create(WTFMove(element), true)); |
500 | } |
501 | |
502 | void CompositeEditCommand::insertParagraphSeparator(bool useDefaultParagraphElement, bool pasteBlockqutoeIntoUnquotedArea) |
503 | { |
504 | applyCommandToComposite(InsertParagraphSeparatorCommand::create(document(), useDefaultParagraphElement, pasteBlockqutoeIntoUnquotedArea, editingAction())); |
505 | } |
506 | |
507 | void CompositeEditCommand::insertLineBreak() |
508 | { |
509 | applyCommandToComposite(InsertLineBreakCommand::create(document())); |
510 | } |
511 | |
512 | bool CompositeEditCommand::isRemovableBlock(const Node* node) |
513 | { |
514 | ASSERT(node); |
515 | if (!is<HTMLDivElement>(*node)) |
516 | return false; |
517 | |
518 | Node* parentNode = node->parentNode(); |
519 | if (parentNode && parentNode->firstChild() != parentNode->lastChild()) |
520 | return false; |
521 | |
522 | if (!downcast<HTMLDivElement>(*node).hasAttributes()) |
523 | return true; |
524 | |
525 | return false; |
526 | } |
527 | |
528 | void CompositeEditCommand::insertNodeBefore(Ref<Node>&& insertChild, Node& refChild, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable) |
529 | { |
530 | applyCommandToComposite(InsertNodeBeforeCommand::create(WTFMove(insertChild), refChild, shouldAssumeContentIsAlwaysEditable, editingAction())); |
531 | } |
532 | |
533 | void CompositeEditCommand::insertNodeAfter(Ref<Node>&& insertChild, Node& refChild) |
534 | { |
535 | ContainerNode* parent = refChild.parentNode(); |
536 | if (!parent) |
537 | return; |
538 | |
539 | ASSERT(!parent->isShadowRoot()); |
540 | if (parent->lastChild() == &refChild) |
541 | appendNode(WTFMove(insertChild), *parent); |
542 | else { |
543 | ASSERT(refChild.nextSibling()); |
544 | insertNodeBefore(WTFMove(insertChild), *refChild.nextSibling()); |
545 | } |
546 | } |
547 | |
548 | void CompositeEditCommand::insertNodeAt(Ref<Node>&& insertChild, const Position& editingPosition) |
549 | { |
550 | ASSERT(isEditablePosition(editingPosition)); |
551 | // For editing positions like [table, 0], insert before the table, |
552 | // likewise for replaced elements, brs, etc. |
553 | Position p = editingPosition.parentAnchoredEquivalent(); |
554 | Node* refChild = p.deprecatedNode(); |
555 | int offset = p.deprecatedEditingOffset(); |
556 | |
557 | if (canHaveChildrenForEditing(*refChild)) { |
558 | Node* child = refChild->firstChild(); |
559 | for (int i = 0; child && i < offset; i++) |
560 | child = child->nextSibling(); |
561 | if (child) |
562 | insertNodeBefore(WTFMove(insertChild), *child); |
563 | else |
564 | appendNode(WTFMove(insertChild), downcast<ContainerNode>(*refChild)); |
565 | } else if (caretMinOffset(*refChild) >= offset) |
566 | insertNodeBefore(WTFMove(insertChild), *refChild); |
567 | else if (is<Text>(*refChild) && caretMaxOffset(*refChild) > offset) { |
568 | splitTextNode(downcast<Text>(*refChild), offset); |
569 | |
570 | // Mutation events (bug 22634) from the text node insertion may have removed the refChild |
571 | if (!refChild->isConnected()) |
572 | return; |
573 | insertNodeBefore(WTFMove(insertChild), *refChild); |
574 | } else |
575 | insertNodeAfter(WTFMove(insertChild), *refChild); |
576 | } |
577 | |
578 | void CompositeEditCommand::appendNode(Ref<Node>&& node, Ref<ContainerNode>&& parent) |
579 | { |
580 | ASSERT(canHaveChildrenForEditing(parent)); |
581 | applyCommandToComposite(AppendNodeCommand::create(WTFMove(parent), WTFMove(node), editingAction())); |
582 | } |
583 | |
584 | void CompositeEditCommand::removeChildrenInRange(Node& node, unsigned from, unsigned to) |
585 | { |
586 | Vector<Ref<Node>> children; |
587 | Node* child = node.traverseToChildAt(from); |
588 | for (unsigned i = from; child && i < to; i++, child = child->nextSibling()) |
589 | children.append(*child); |
590 | |
591 | for (auto& child : children) |
592 | removeNode(child); |
593 | } |
594 | |
595 | void CompositeEditCommand::removeNode(Node& node, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable) |
596 | { |
597 | if (!node.nonShadowBoundaryParentNode()) |
598 | return; |
599 | applyCommandToComposite(RemoveNodeCommand::create(node, shouldAssumeContentIsAlwaysEditable, editingAction())); |
600 | } |
601 | |
602 | void CompositeEditCommand::removeNodePreservingChildren(Node& node, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable) |
603 | { |
604 | applyCommandToComposite(RemoveNodePreservingChildrenCommand::create(node, shouldAssumeContentIsAlwaysEditable, editingAction())); |
605 | } |
606 | |
607 | void CompositeEditCommand::removeNodeAndPruneAncestors(Node& node) |
608 | { |
609 | RefPtr<ContainerNode> parent = node.parentNode(); |
610 | removeNode(node); |
611 | prune(parent.get()); |
612 | } |
613 | |
614 | void CompositeEditCommand::moveRemainingSiblingsToNewParent(Node* node, Node* pastLastNodeToMove, Element& newParent) |
615 | { |
616 | NodeVector nodesToRemove; |
617 | Ref<Element> protectedNewParent = newParent; |
618 | |
619 | for (; node && node != pastLastNodeToMove; node = node->nextSibling()) |
620 | nodesToRemove.append(*node); |
621 | |
622 | for (auto& nodeToRemove : nodesToRemove) { |
623 | removeNode(nodeToRemove); |
624 | appendNode(WTFMove(nodeToRemove), newParent); |
625 | } |
626 | } |
627 | |
628 | void CompositeEditCommand::updatePositionForNodeRemovalPreservingChildren(Position& position, Node& node) |
629 | { |
630 | int offset = (position.anchorType() == Position::PositionIsOffsetInAnchor) ? position.offsetInContainerNode() : 0; |
631 | updatePositionForNodeRemoval(position, node); |
632 | if (offset) |
633 | position.moveToOffset(offset); |
634 | } |
635 | |
636 | HTMLElement* CompositeEditCommand::replaceElementWithSpanPreservingChildrenAndAttributes(HTMLElement& element) |
637 | { |
638 | // It would also be possible to implement all of ReplaceNodeWithSpanCommand |
639 | // as a series of existing smaller edit commands. Someone who wanted to |
640 | // reduce the number of edit commands could do so here. |
641 | auto command = ReplaceNodeWithSpanCommand::create(element); |
642 | auto* commandPtr = command.ptr(); |
643 | applyCommandToComposite(WTFMove(command)); |
644 | // Returning a raw pointer here is OK because the command is retained by |
645 | // applyCommandToComposite (thus retaining the span), and the span is also |
646 | // in the DOM tree, and thus alive whie it has a parent. |
647 | ASSERT(commandPtr->spanElement()->isConnected()); |
648 | return commandPtr->spanElement(); |
649 | } |
650 | |
651 | void CompositeEditCommand::prune(Node* node) |
652 | { |
653 | if (auto* highestNodeToRemove = highestNodeToRemoveInPruning(node)) |
654 | removeNode(*highestNodeToRemove); |
655 | } |
656 | |
657 | void CompositeEditCommand::splitTextNode(Text& node, unsigned offset) |
658 | { |
659 | applyCommandToComposite(SplitTextNodeCommand::create(node, offset)); |
660 | } |
661 | |
662 | void CompositeEditCommand::splitElement(Element& element, Node& atChild) |
663 | { |
664 | applyCommandToComposite(SplitElementCommand::create(element, atChild)); |
665 | } |
666 | |
667 | void CompositeEditCommand::mergeIdenticalElements(Element& first, Element& second) |
668 | { |
669 | Ref<Element> protectedFirst = first; |
670 | Ref<Element> protectedSecond = second; |
671 | ASSERT(!first.isDescendantOf(&second) && &second != &first); |
672 | if (first.nextSibling() != &second) { |
673 | removeNode(second); |
674 | insertNodeAfter(second, first); |
675 | } |
676 | applyCommandToComposite(MergeIdenticalElementsCommand::create(first, second)); |
677 | } |
678 | |
679 | void CompositeEditCommand::wrapContentsInDummySpan(Element& element) |
680 | { |
681 | applyCommandToComposite(WrapContentsInDummySpanCommand::create(element)); |
682 | } |
683 | |
684 | void CompositeEditCommand::splitTextNodeContainingElement(Text& text, unsigned offset) |
685 | { |
686 | applyCommandToComposite(SplitTextNodeContainingElementCommand::create(text, offset)); |
687 | } |
688 | |
689 | void CompositeEditCommand::inputText(const String& text, bool selectInsertedText) |
690 | { |
691 | unsigned offset = 0; |
692 | unsigned length = text.length(); |
693 | |
694 | RefPtr<ContainerNode> scope; |
695 | unsigned startIndex = indexForVisiblePosition(endingSelection().visibleStart(), scope); |
696 | |
697 | size_t newline; |
698 | do { |
699 | newline = text.find('\n', offset); |
700 | if (newline != offset) { |
701 | int substringLength = newline == notFound ? length - offset : newline - offset; |
702 | applyCommandToComposite(InsertTextCommand::create(document(), text.substring(offset, substringLength), false)); |
703 | } |
704 | if (newline != notFound) { |
705 | VisiblePosition caret(endingSelection().visibleStart()); |
706 | if (enclosingNodeOfType(caret.deepEquivalent(), &isMailBlockquote)) { |
707 | // FIXME: Breaking a blockquote when the caret is just after a space will collapse the |
708 | // space. Modify startIndex or length to compensate for this so that the ending selection |
709 | // will be positioned correctly. |
710 | // <rdar://problem/9914462> breaking a Mail blockquote just after a space collapses the space |
711 | if (caret.previous().characterAfter() == ' ') { |
712 | if (!offset && !startIndex) |
713 | startIndex--; |
714 | else if (!length) |
715 | length--; |
716 | } |
717 | applyCommandToComposite(BreakBlockquoteCommand::create(document())); |
718 | } else |
719 | insertLineBreak(); |
720 | } |
721 | |
722 | offset = newline + 1; |
723 | } while (newline != notFound && offset != length); |
724 | |
725 | if (selectInsertedText) |
726 | setEndingSelection(VisibleSelection(visiblePositionForIndex(startIndex, scope.get()), visiblePositionForIndex(startIndex + length, scope.get()))); |
727 | } |
728 | |
729 | void CompositeEditCommand::insertTextIntoNode(Text& node, unsigned offset, const String& text) |
730 | { |
731 | if (!text.isEmpty()) |
732 | applyCommandToComposite(InsertIntoTextNodeCommand::create(node, offset, text, editingAction())); |
733 | } |
734 | |
735 | void CompositeEditCommand::deleteTextFromNode(Text& node, unsigned offset, unsigned count) |
736 | { |
737 | applyCommandToComposite(DeleteFromTextNodeCommand::create(node, offset, count, editingAction())); |
738 | } |
739 | |
740 | void CompositeEditCommand::replaceTextInNode(Text& node, unsigned offset, unsigned count, const String& replacementText) |
741 | { |
742 | applyCommandToComposite(DeleteFromTextNodeCommand::create(node, offset, count)); |
743 | if (!replacementText.isEmpty()) |
744 | applyCommandToComposite(InsertIntoTextNodeCommand::create(node, offset, replacementText, editingAction())); |
745 | } |
746 | |
747 | Position CompositeEditCommand::replaceSelectedTextInNode(const String& text) |
748 | { |
749 | Position start = endingSelection().start(); |
750 | Position end = endingSelection().end(); |
751 | if (start.containerNode() != end.containerNode() || !start.containerNode()->isTextNode() || isTabSpanTextNode(start.containerNode())) |
752 | return Position(); |
753 | |
754 | RefPtr<Text> textNode = start.containerText(); |
755 | replaceTextInNode(*textNode, start.offsetInContainerNode(), end.offsetInContainerNode() - start.offsetInContainerNode(), text); |
756 | |
757 | return Position(textNode.get(), start.offsetInContainerNode() + text.length()); |
758 | } |
759 | |
760 | static Vector<RenderedDocumentMarker> copyMarkers(const Vector<RenderedDocumentMarker*>& markerPointers) |
761 | { |
762 | Vector<RenderedDocumentMarker> markers; |
763 | markers.reserveInitialCapacity(markerPointers.size()); |
764 | for (auto& markerPointer : markerPointers) |
765 | markers.uncheckedAppend(*markerPointer); |
766 | |
767 | return markers; |
768 | } |
769 | |
770 | void CompositeEditCommand::replaceTextInNodePreservingMarkers(Text& node, unsigned offset, unsigned count, const String& replacementText) |
771 | { |
772 | Ref<Text> protectedNode(node); |
773 | DocumentMarkerController& markerController = document().markers(); |
774 | auto markers = copyMarkers(markerController.markersInRange(Range::create(document(), &node, offset, &node, offset + count), DocumentMarker::allMarkers())); |
775 | replaceTextInNode(node, offset, count, replacementText); |
776 | auto newRange = Range::create(document(), &node, offset, &node, offset + replacementText.length()); |
777 | for (const auto& marker : markers) { |
778 | #if PLATFORM(IOS_FAMILY) |
779 | if (marker.isDictation()) { |
780 | markerController.addMarker(newRange, marker.type(), marker.description(), marker.alternatives(), marker.metadata()); |
781 | continue; |
782 | } |
783 | #endif |
784 | #if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING) |
785 | if (marker.type() == DocumentMarker::PlatformTextChecking) { |
786 | if (!WTF::holds_alternative<DocumentMarker::PlatformTextCheckingData>(marker.data())) { |
787 | ASSERT_NOT_REACHED(); |
788 | continue; |
789 | } |
790 | |
791 | auto& textCheckingData = WTF::get<DocumentMarker::PlatformTextCheckingData>(marker.data()); |
792 | markerController.addPlatformTextCheckingMarker(newRange, textCheckingData.key, textCheckingData.value); |
793 | continue; |
794 | } |
795 | #endif |
796 | markerController.addMarker(newRange, marker.type(), marker.description()); |
797 | } |
798 | } |
799 | |
800 | Position CompositeEditCommand::positionOutsideTabSpan(const Position& position) |
801 | { |
802 | if (!isTabSpanTextNode(position.anchorNode())) |
803 | return position; |
804 | |
805 | switch (position.anchorType()) { |
806 | case Position::PositionIsBeforeChildren: |
807 | case Position::PositionIsAfterChildren: |
808 | ASSERT_NOT_REACHED(); |
809 | return position; |
810 | case Position::PositionIsOffsetInAnchor: |
811 | break; |
812 | case Position::PositionIsBeforeAnchor: |
813 | return positionInParentBeforeNode(position.anchorNode()); |
814 | case Position::PositionIsAfterAnchor: |
815 | return positionInParentAfterNode(position.anchorNode()); |
816 | } |
817 | |
818 | auto* tabSpan = tabSpanNode(position.containerNode()); |
819 | |
820 | if (position.offsetInContainerNode() <= caretMinOffset(*position.containerNode())) |
821 | return positionInParentBeforeNode(tabSpan); |
822 | |
823 | if (position.offsetInContainerNode() >= caretMaxOffset(*position.containerNode())) |
824 | return positionInParentAfterNode(tabSpan); |
825 | |
826 | splitTextNodeContainingElement(downcast<Text>(*position.containerNode()), position.offsetInContainerNode()); |
827 | return positionInParentBeforeNode(tabSpan); |
828 | } |
829 | |
830 | void CompositeEditCommand::insertNodeAtTabSpanPosition(Ref<Node>&& node, const Position& pos) |
831 | { |
832 | // insert node before, after, or at split of tab span |
833 | insertNodeAt(WTFMove(node), positionOutsideTabSpan(pos)); |
834 | } |
835 | |
836 | static EditAction deleteSelectionEditingActionForEditingAction(EditAction editingAction) |
837 | { |
838 | switch (editingAction) { |
839 | case EditAction::Cut: |
840 | return EditAction::Cut; |
841 | default: |
842 | return EditAction::Delete; |
843 | } |
844 | } |
845 | |
846 | void CompositeEditCommand::deleteSelection(bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements, bool sanitizeMarkup) |
847 | { |
848 | if (endingSelection().isRange()) |
849 | applyCommandToComposite(DeleteSelectionCommand::create(document(), smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements, sanitizeMarkup, deleteSelectionEditingActionForEditingAction(editingAction()))); |
850 | } |
851 | |
852 | void CompositeEditCommand::deleteSelection(const VisibleSelection &selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements, bool sanitizeMarkup) |
853 | { |
854 | if (selection.isRange()) |
855 | applyCommandToComposite(DeleteSelectionCommand::create(selection, smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements, sanitizeMarkup)); |
856 | } |
857 | |
858 | void CompositeEditCommand::removeNodeAttribute(Element& element, const QualifiedName& attribute) |
859 | { |
860 | setNodeAttribute(element, attribute, nullAtom()); |
861 | } |
862 | |
863 | void CompositeEditCommand::setNodeAttribute(Element& element, const QualifiedName& attribute, const AtomicString& value) |
864 | { |
865 | applyCommandToComposite(SetNodeAttributeCommand::create(element, attribute, value)); |
866 | } |
867 | |
868 | static inline bool containsOnlyDeprecatedEditingWhitespace(const String& text) |
869 | { |
870 | for (unsigned i = 0; i < text.length(); ++i) { |
871 | if (!deprecatedIsEditingWhitespace(text[i])) |
872 | return false; |
873 | } |
874 | return true; |
875 | } |
876 | |
877 | bool CompositeEditCommand::shouldRebalanceLeadingWhitespaceFor(const String& text) const |
878 | { |
879 | return containsOnlyDeprecatedEditingWhitespace(text); |
880 | } |
881 | |
882 | bool CompositeEditCommand::canRebalance(const Position& position) const |
883 | { |
884 | Node* node = position.containerNode(); |
885 | if (position.anchorType() != Position::PositionIsOffsetInAnchor || !is<Text>(node)) |
886 | return false; |
887 | |
888 | Text& textNode = downcast<Text>(*node); |
889 | if (!textNode.length()) |
890 | return false; |
891 | |
892 | node->document().updateStyleIfNeeded(); |
893 | |
894 | RenderObject* renderer = textNode.renderer(); |
895 | if (renderer && !renderer->style().collapseWhiteSpace()) |
896 | return false; |
897 | |
898 | return true; |
899 | } |
900 | |
901 | // FIXME: Doesn't go into text nodes that contribute adjacent text (siblings, cousins, etc). |
902 | void CompositeEditCommand::rebalanceWhitespaceAt(const Position& position) |
903 | { |
904 | Node* node = position.containerNode(); |
905 | if (!canRebalance(position)) |
906 | return; |
907 | |
908 | // If the rebalance is for the single offset, and neither text[offset] nor text[offset - 1] are some form of whitespace, do nothing. |
909 | int offset = position.deprecatedEditingOffset(); |
910 | String text = downcast<Text>(*node).data(); |
911 | if (!deprecatedIsEditingWhitespace(text[offset])) { |
912 | offset--; |
913 | if (offset < 0 || !deprecatedIsEditingWhitespace(text[offset])) |
914 | return; |
915 | } |
916 | |
917 | rebalanceWhitespaceOnTextSubstring(downcast<Text>(*node), position.offsetInContainerNode(), position.offsetInContainerNode()); |
918 | } |
919 | |
920 | void CompositeEditCommand::rebalanceWhitespaceOnTextSubstring(Text& textNode, int startOffset, int endOffset) |
921 | { |
922 | String text = textNode.data(); |
923 | ASSERT(!text.isEmpty()); |
924 | |
925 | // Set upstream and downstream to define the extent of the whitespace surrounding text[offset]. |
926 | int upstream = startOffset; |
927 | while (upstream > 0 && deprecatedIsEditingWhitespace(text[upstream - 1])) |
928 | upstream--; |
929 | |
930 | int downstream = endOffset; |
931 | while ((unsigned)downstream < text.length() && deprecatedIsEditingWhitespace(text[downstream])) |
932 | downstream++; |
933 | |
934 | int length = downstream - upstream; |
935 | if (!length) |
936 | return; |
937 | |
938 | VisiblePosition visibleUpstreamPos(Position(&textNode, upstream)); |
939 | VisiblePosition visibleDownstreamPos(Position(&textNode, downstream)); |
940 | |
941 | String string = text.substring(upstream, length); |
942 | String rebalancedString = stringWithRebalancedWhitespace(string, |
943 | // FIXME: Because of the problem mentioned at the top of this function, we must also use nbsps at the start/end of the string because |
944 | // this function doesn't get all surrounding whitespace, just the whitespace in the current text node. |
945 | isStartOfParagraph(visibleUpstreamPos) || upstream == 0, |
946 | isEndOfParagraph(visibleDownstreamPos) || (unsigned)downstream == text.length()); |
947 | |
948 | if (string != rebalancedString) |
949 | replaceTextInNodePreservingMarkers(textNode, upstream, length, rebalancedString); |
950 | } |
951 | |
952 | void CompositeEditCommand::prepareWhitespaceAtPositionForSplit(Position& position) |
953 | { |
954 | Node* node = position.deprecatedNode(); |
955 | if (!is<Text>(node)) |
956 | return; |
957 | Text& textNode = downcast<Text>(*node); |
958 | |
959 | if (!textNode.length()) |
960 | return; |
961 | RenderObject* renderer = textNode.renderer(); |
962 | if (renderer && !renderer->style().collapseWhiteSpace()) |
963 | return; |
964 | |
965 | // Delete collapsed whitespace so that inserting nbsps doesn't uncollapse it. |
966 | Position upstreamPos = position.upstream(); |
967 | deleteInsignificantText(position.upstream(), position.downstream()); |
968 | position = upstreamPos.downstream(); |
969 | |
970 | VisiblePosition visiblePos(position); |
971 | VisiblePosition previousVisiblePos(visiblePos.previous()); |
972 | Position previous(previousVisiblePos.deepEquivalent()); |
973 | |
974 | if (deprecatedIsCollapsibleWhitespace(previousVisiblePos.characterAfter()) && is<Text>(*previous.deprecatedNode()) && !is<HTMLBRElement>(*previous.deprecatedNode())) |
975 | replaceTextInNodePreservingMarkers(downcast<Text>(*previous.deprecatedNode()), previous.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); |
976 | if (deprecatedIsCollapsibleWhitespace(visiblePos.characterAfter()) && is<Text>(*position.deprecatedNode()) && !is<HTMLBRElement>(*position.deprecatedNode())) |
977 | replaceTextInNodePreservingMarkers(downcast<Text>(*position.deprecatedNode()), position.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); |
978 | } |
979 | |
980 | void CompositeEditCommand::rebalanceWhitespace() |
981 | { |
982 | VisibleSelection selection = endingSelection(); |
983 | if (selection.isNone()) |
984 | return; |
985 | |
986 | rebalanceWhitespaceAt(selection.start()); |
987 | if (selection.isRange()) |
988 | rebalanceWhitespaceAt(selection.end()); |
989 | } |
990 | |
991 | void CompositeEditCommand::deleteInsignificantText(Text& textNode, unsigned start, unsigned end) |
992 | { |
993 | if (start >= end) |
994 | return; |
995 | |
996 | document().updateLayout(); |
997 | |
998 | RenderText* textRenderer = textNode.renderer(); |
999 | if (!textRenderer) |
1000 | return; |
1001 | |
1002 | Vector<InlineTextBox*> sortedTextBoxes; |
1003 | size_t sortedTextBoxesPosition = 0; |
1004 | |
1005 | for (InlineTextBox* textBox = textRenderer->firstTextBox(); textBox; textBox = textBox->nextTextBox()) |
1006 | sortedTextBoxes.append(textBox); |
1007 | |
1008 | // If there is mixed directionality text, the boxes can be out of order, |
1009 | // (like Arabic with embedded LTR), so sort them first. |
1010 | if (textRenderer->containsReversedText()) |
1011 | std::sort(sortedTextBoxes.begin(), sortedTextBoxes.end(), InlineTextBox::compareByStart); |
1012 | InlineTextBox* box = sortedTextBoxes.isEmpty() ? 0 : sortedTextBoxes[sortedTextBoxesPosition]; |
1013 | |
1014 | if (!box) { |
1015 | // whole text node is empty |
1016 | removeNode(textNode); |
1017 | return; |
1018 | } |
1019 | |
1020 | unsigned length = textNode.length(); |
1021 | if (start >= length || end > length) |
1022 | return; |
1023 | |
1024 | unsigned removed = 0; |
1025 | InlineTextBox* prevBox = nullptr; |
1026 | String str; |
1027 | |
1028 | // This loop structure works to process all gaps preceding a box, |
1029 | // and also will look at the gap after the last box. |
1030 | while (prevBox || box) { |
1031 | unsigned gapStart = prevBox ? prevBox->start() + prevBox->len() : 0; |
1032 | if (end < gapStart) |
1033 | // No more chance for any intersections |
1034 | break; |
1035 | |
1036 | unsigned gapEnd = box ? box->start() : length; |
1037 | bool indicesIntersect = start <= gapEnd && end >= gapStart; |
1038 | int gapLen = gapEnd - gapStart; |
1039 | if (indicesIntersect && gapLen > 0) { |
1040 | gapStart = std::max(gapStart, start); |
1041 | gapEnd = std::min(gapEnd, end); |
1042 | if (str.isNull()) |
1043 | str = textNode.data().substring(start, end - start); |
1044 | // remove text in the gap |
1045 | str.remove(gapStart - start - removed, gapLen); |
1046 | removed += gapLen; |
1047 | } |
1048 | |
1049 | prevBox = box; |
1050 | if (box) { |
1051 | if (++sortedTextBoxesPosition < sortedTextBoxes.size()) |
1052 | box = sortedTextBoxes[sortedTextBoxesPosition]; |
1053 | else |
1054 | box = nullptr; |
1055 | } |
1056 | } |
1057 | |
1058 | if (!str.isNull()) { |
1059 | // Replace the text between start and end with our pruned version. |
1060 | if (!str.isEmpty()) |
1061 | replaceTextInNode(textNode, start, end - start, str); |
1062 | else { |
1063 | // Assert that we are not going to delete all of the text in the node. |
1064 | // If we were, that should have been done above with the call to |
1065 | // removeNode and return. |
1066 | ASSERT(start > 0 || end - start < textNode.length()); |
1067 | deleteTextFromNode(textNode, start, end - start); |
1068 | } |
1069 | } |
1070 | } |
1071 | |
1072 | void CompositeEditCommand::deleteInsignificantText(const Position& start, const Position& end) |
1073 | { |
1074 | if (start.isNull() || end.isNull()) |
1075 | return; |
1076 | |
1077 | if (comparePositions(start, end) >= 0) |
1078 | return; |
1079 | |
1080 | Vector<Ref<Text>> nodes; |
1081 | for (Node* node = start.deprecatedNode(); node; node = NodeTraversal::next(*node)) { |
1082 | if (is<Text>(*node)) |
1083 | nodes.append(downcast<Text>(*node)); |
1084 | if (node == end.deprecatedNode()) |
1085 | break; |
1086 | } |
1087 | |
1088 | for (auto& textNode : nodes) { |
1089 | int startOffset = textNode.ptr() == start.deprecatedNode() ? start.deprecatedEditingOffset() : 0; |
1090 | int endOffset = textNode.ptr() == end.deprecatedNode() ? end.deprecatedEditingOffset() : static_cast<int>(textNode->length()); |
1091 | deleteInsignificantText(textNode, startOffset, endOffset); |
1092 | } |
1093 | } |
1094 | |
1095 | void CompositeEditCommand::deleteInsignificantTextDownstream(const Position& pos) |
1096 | { |
1097 | Position end = VisiblePosition(pos, VP_DEFAULT_AFFINITY).next().deepEquivalent().downstream(); |
1098 | deleteInsignificantText(pos, end); |
1099 | } |
1100 | |
1101 | Ref<Element> CompositeEditCommand::appendBlockPlaceholder(Ref<Element>&& container) |
1102 | { |
1103 | document().updateLayoutIgnorePendingStylesheets(); |
1104 | |
1105 | // Should assert isBlockFlow || isInlineFlow when deletion improves. See 4244964. |
1106 | ASSERT(container->renderer()); |
1107 | |
1108 | auto placeholder = createBlockPlaceholderElement(document()); |
1109 | appendNode(placeholder.copyRef(), WTFMove(container)); |
1110 | return placeholder; |
1111 | } |
1112 | |
1113 | RefPtr<Node> CompositeEditCommand::insertBlockPlaceholder(const Position& pos) |
1114 | { |
1115 | if (pos.isNull()) |
1116 | return nullptr; |
1117 | |
1118 | // Should assert isBlockFlow || isInlineFlow when deletion improves. See 4244964. |
1119 | ASSERT(pos.deprecatedNode()->renderer()); |
1120 | |
1121 | auto placeholder = createBlockPlaceholderElement(document()); |
1122 | insertNodeAt(placeholder.copyRef(), pos); |
1123 | return placeholder; |
1124 | } |
1125 | |
1126 | RefPtr<Node> CompositeEditCommand::addBlockPlaceholderIfNeeded(Element* container) |
1127 | { |
1128 | if (!container) |
1129 | return nullptr; |
1130 | |
1131 | document().updateLayoutIgnorePendingStylesheets(); |
1132 | |
1133 | auto* renderer = container->renderer(); |
1134 | if (!is<RenderBlockFlow>(renderer)) |
1135 | return nullptr; |
1136 | |
1137 | // Append the placeholder to make sure it follows any unrendered blocks. |
1138 | auto& blockFlow = downcast<RenderBlockFlow>(*renderer); |
1139 | if (!blockFlow.height() || (blockFlow.isListItem() && !blockFlow.firstChild())) |
1140 | return appendBlockPlaceholder(*container); |
1141 | |
1142 | return nullptr; |
1143 | } |
1144 | |
1145 | // Assumes that the position is at a placeholder and does the removal without much checking. |
1146 | void CompositeEditCommand::removePlaceholderAt(const Position& p) |
1147 | { |
1148 | ASSERT(lineBreakExistsAtPosition(p)); |
1149 | |
1150 | // We are certain that the position is at a line break, but it may be a br or a preserved newline. |
1151 | if (is<HTMLBRElement>(*p.anchorNode())) { |
1152 | removeNode(*p.anchorNode()); |
1153 | return; |
1154 | } |
1155 | |
1156 | deleteTextFromNode(downcast<Text>(*p.anchorNode()), p.offsetInContainerNode(), 1); |
1157 | } |
1158 | |
1159 | Ref<HTMLElement> CompositeEditCommand::insertNewDefaultParagraphElementAt(const Position& position) |
1160 | { |
1161 | auto paragraphElement = createDefaultParagraphElement(document()); |
1162 | paragraphElement->appendChild(HTMLBRElement::create(document())); |
1163 | insertNodeAt(paragraphElement.copyRef(), position); |
1164 | return paragraphElement; |
1165 | } |
1166 | |
1167 | // If the paragraph is not entirely within it's own block, create one and move the paragraph into |
1168 | // it, and return that block. Otherwise return 0. |
1169 | RefPtr<Node> CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessary(const Position& pos) |
1170 | { |
1171 | if (pos.isNull()) |
1172 | return nullptr; |
1173 | |
1174 | document().updateLayoutIgnorePendingStylesheets(); |
1175 | |
1176 | // It's strange that this function is responsible for verifying that pos has not been invalidated |
1177 | // by an earlier call to this function. The caller, applyBlockStyle, should do this. |
1178 | VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY); |
1179 | VisiblePosition visibleParagraphStart(startOfParagraph(visiblePos)); |
1180 | VisiblePosition visibleParagraphEnd = endOfParagraph(visiblePos); |
1181 | VisiblePosition next = visibleParagraphEnd.next(); |
1182 | VisiblePosition visibleEnd = next.isNotNull() ? next : visibleParagraphEnd; |
1183 | |
1184 | Position upstreamStart = visibleParagraphStart.deepEquivalent().upstream(); |
1185 | Position upstreamEnd = visibleEnd.deepEquivalent().upstream(); |
1186 | |
1187 | // If there are no VisiblePositions in the same block as pos then |
1188 | // upstreamStart will be outside the paragraph |
1189 | if (comparePositions(pos, upstreamStart) < 0) |
1190 | return nullptr; |
1191 | |
1192 | // Perform some checks to see if we need to perform work in this function. |
1193 | if (isBlock(upstreamStart.deprecatedNode())) { |
1194 | // If the block is the root editable element, always move content to a new block, |
1195 | // since it is illegal to modify attributes on the root editable element for editing. |
1196 | if (upstreamStart.deprecatedNode() == editableRootForPosition(upstreamStart)) { |
1197 | // If the block is the root editable element and it contains no visible content, create a new |
1198 | // block but don't try and move content into it, since there's nothing for moveParagraphs to move. |
1199 | if (!Position::hasRenderedNonAnonymousDescendantsWithHeight(downcast<RenderElement>(*upstreamStart.deprecatedNode()->renderer()))) |
1200 | return insertNewDefaultParagraphElementAt(upstreamStart); |
1201 | } else if (isBlock(upstreamEnd.deprecatedNode())) { |
1202 | if (!upstreamEnd.deprecatedNode()->isDescendantOf(upstreamStart.deprecatedNode())) { |
1203 | // If the paragraph end is a descendant of paragraph start, then we need to run |
1204 | // the rest of this function. If not, we can bail here. |
1205 | return nullptr; |
1206 | } |
1207 | } else if (enclosingBlock(upstreamEnd.deprecatedNode()) != upstreamStart.deprecatedNode()) { |
1208 | // The visibleEnd. If it is an ancestor of the paragraph start, then |
1209 | // we can bail as we have a full block to work with. |
1210 | if (upstreamStart.deprecatedNode()->isDescendantOf(enclosingBlock(upstreamEnd.deprecatedNode()))) |
1211 | return nullptr; |
1212 | } else if (isEndOfEditableOrNonEditableContent(visibleEnd)) { |
1213 | // At the end of the editable region. We can bail here as well. |
1214 | return nullptr; |
1215 | } |
1216 | } |
1217 | |
1218 | // If upstreamStart is not editable, then we can bail here. |
1219 | if (!isEditablePosition(upstreamStart)) |
1220 | return nullptr; |
1221 | auto newBlock = insertNewDefaultParagraphElementAt(upstreamStart); |
1222 | |
1223 | bool endWasBr = visibleParagraphEnd.deepEquivalent().deprecatedNode()->hasTagName(brTag); |
1224 | |
1225 | moveParagraphs(visibleParagraphStart, visibleParagraphEnd, VisiblePosition(firstPositionInNode(newBlock.ptr()))); |
1226 | |
1227 | if (newBlock->lastChild() && newBlock->lastChild()->hasTagName(brTag) && !endWasBr) |
1228 | removeNode(*newBlock->lastChild()); |
1229 | |
1230 | return newBlock; |
1231 | } |
1232 | |
1233 | void CompositeEditCommand::pushAnchorElementDown(Element& anchorElement) |
1234 | { |
1235 | ASSERT(anchorElement.isLink()); |
1236 | |
1237 | setEndingSelection(VisibleSelection::selectionFromContentsOfNode(&anchorElement)); |
1238 | applyStyledElement(anchorElement); |
1239 | // Clones of anchorElement have been pushed down, now remove it. |
1240 | if (anchorElement.isConnected()) |
1241 | removeNodePreservingChildren(anchorElement); |
1242 | } |
1243 | |
1244 | // Clone the paragraph between start and end under blockElement, |
1245 | // preserving the hierarchy up to outerNode. |
1246 | |
1247 | void CompositeEditCommand::cloneParagraphUnderNewElement(const Position& start, const Position& end, Node* passedOuterNode, Element* blockElement) |
1248 | { |
1249 | ASSERT(comparePositions(start, end) <= 0); |
1250 | |
1251 | // First we clone the outerNode |
1252 | RefPtr<Node> lastNode; |
1253 | RefPtr<Node> outerNode = passedOuterNode; |
1254 | |
1255 | if (outerNode->isRootEditableElement()) { |
1256 | lastNode = blockElement; |
1257 | } else { |
1258 | lastNode = outerNode->cloneNode(isRenderedTable(outerNode.get())); |
1259 | appendNode(*lastNode, *blockElement); |
1260 | } |
1261 | |
1262 | if (start.deprecatedNode() != outerNode && lastNode->isElementNode() && start.anchorNode()->isDescendantOf(outerNode.get())) { |
1263 | Vector<RefPtr<Node>> ancestors; |
1264 | |
1265 | // Insert each node from innerNode to outerNode (excluded) in a list. |
1266 | for (Node* n = start.deprecatedNode(); n && n != outerNode; n = n->parentNode()) |
1267 | ancestors.append(n); |
1268 | |
1269 | // Clone every node between start.deprecatedNode() and outerBlock. |
1270 | |
1271 | for (size_t i = ancestors.size(); i != 0; --i) { |
1272 | Node* item = ancestors[i - 1].get(); |
1273 | auto child = item->cloneNode(isRenderedTable(item)); |
1274 | appendNode(child.copyRef(), downcast<Element>(*lastNode)); |
1275 | lastNode = WTFMove(child); |
1276 | } |
1277 | } |
1278 | |
1279 | // Handle the case of paragraphs with more than one node, |
1280 | // cloning all the siblings until end.deprecatedNode() is reached. |
1281 | |
1282 | if (start.deprecatedNode() != end.deprecatedNode() && !start.deprecatedNode()->isDescendantOf(end.deprecatedNode())) { |
1283 | // If end is not a descendant of outerNode we need to |
1284 | // find the first common ancestor to increase the scope |
1285 | // of our nextSibling traversal. |
1286 | while (!end.deprecatedNode()->isDescendantOf(outerNode.get())) { |
1287 | outerNode = outerNode->parentNode(); |
1288 | } |
1289 | |
1290 | RefPtr<Node> startNode = start.deprecatedNode(); |
1291 | for (RefPtr<Node> node = NodeTraversal::nextSkippingChildren(*startNode, outerNode.get()); node; node = NodeTraversal::nextSkippingChildren(*node, outerNode.get())) { |
1292 | // Move lastNode up in the tree as much as node was moved up in the |
1293 | // tree by NodeTraversal::nextSkippingChildren, so that the relative depth between |
1294 | // node and the original start node is maintained in the clone. |
1295 | while (startNode->parentNode() != node->parentNode()) { |
1296 | startNode = startNode->parentNode(); |
1297 | lastNode = lastNode->parentNode(); |
1298 | } |
1299 | |
1300 | auto clonedNode = node->cloneNode(true); |
1301 | insertNodeAfter(clonedNode.copyRef(), *lastNode); |
1302 | lastNode = WTFMove(clonedNode); |
1303 | if (node == end.deprecatedNode() || end.deprecatedNode()->isDescendantOf(*node)) |
1304 | break; |
1305 | } |
1306 | } |
1307 | } |
1308 | |
1309 | |
1310 | // There are bugs in deletion when it removes a fully selected table/list. |
1311 | // It expands and removes the entire table/list, but will let content |
1312 | // before and after the table/list collapse onto one line. |
1313 | // Deleting a paragraph will leave a placeholder. Remove it (and prune |
1314 | // empty or unrendered parents). |
1315 | |
1316 | void CompositeEditCommand::cleanupAfterDeletion(VisiblePosition destination) |
1317 | { |
1318 | VisiblePosition caretAfterDelete = endingSelection().visibleStart(); |
1319 | if (!caretAfterDelete.equals(destination) && isStartOfParagraph(caretAfterDelete) && isEndOfParagraph(caretAfterDelete)) { |
1320 | // Note: We want the rightmost candidate. |
1321 | Position position = caretAfterDelete.deepEquivalent().downstream(); |
1322 | Node* node = position.deprecatedNode(); |
1323 | ASSERT(node); |
1324 | // Normally deletion will leave a br as a placeholder. |
1325 | if (is<HTMLBRElement>(*node)) |
1326 | removeNodeAndPruneAncestors(*node); |
1327 | // If the selection to move was empty and in an empty block that |
1328 | // doesn't require a placeholder to prop itself open (like a bordered |
1329 | // div or an li), remove it during the move (the list removal code |
1330 | // expects this behavior). |
1331 | else if (isBlock(node)) { |
1332 | // If caret position after deletion and destination position coincides, |
1333 | // node should not be removed. |
1334 | if (!position.rendersInDifferentPosition(destination.deepEquivalent())) { |
1335 | prune(node); |
1336 | return; |
1337 | } |
1338 | removeNodeAndPruneAncestors(*node); |
1339 | } |
1340 | else if (lineBreakExistsAtPosition(position)) { |
1341 | // There is a preserved '\n' at caretAfterDelete. |
1342 | // We can safely assume this is a text node. |
1343 | Text& textNode = downcast<Text>(*node); |
1344 | if (textNode.length() == 1) |
1345 | removeNodeAndPruneAncestors(textNode); |
1346 | else |
1347 | deleteTextFromNode(textNode, position.deprecatedEditingOffset(), 1); |
1348 | } |
1349 | } |
1350 | } |
1351 | |
1352 | // This is a version of moveParagraph that preserves style by keeping the original markup |
1353 | // It is currently used only by IndentOutdentCommand but it is meant to be used in the |
1354 | // future by several other commands such as InsertList and the align commands. |
1355 | // The blockElement parameter is the element to move the paragraph to, |
1356 | // outerNode is the top element of the paragraph hierarchy. |
1357 | |
1358 | void CompositeEditCommand::moveParagraphWithClones(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, Element* blockElement, Node* outerNode) |
1359 | { |
1360 | if (startOfParagraphToMove.isNull() || endOfParagraphToMove.isNull()) |
1361 | return; |
1362 | |
1363 | ASSERT(outerNode); |
1364 | ASSERT(blockElement); |
1365 | |
1366 | VisiblePosition beforeParagraph = startOfParagraphToMove.previous(); |
1367 | VisiblePosition afterParagraph(endOfParagraphToMove.next()); |
1368 | |
1369 | // We upstream() the end and downstream() the start so that we don't include collapsed whitespace in the move. |
1370 | // When we paste a fragment, spaces after the end and before the start are treated as though they were rendered. |
1371 | Position start = startOfParagraphToMove.deepEquivalent().downstream(); |
1372 | Position end = startOfParagraphToMove == endOfParagraphToMove ? start : endOfParagraphToMove.deepEquivalent().upstream(); |
1373 | |
1374 | cloneParagraphUnderNewElement(start, end, outerNode, blockElement); |
1375 | |
1376 | setEndingSelection(VisibleSelection(start, end, DOWNSTREAM)); |
1377 | deleteSelection(false, false, false, false); |
1378 | |
1379 | // There are bugs in deletion when it removes a fully selected table/list. |
1380 | // It expands and removes the entire table/list, but will let content |
1381 | // before and after the table/list collapse onto one line. |
1382 | |
1383 | cleanupAfterDeletion(); |
1384 | |
1385 | // Add a br if pruning an empty block level element caused a collapse. For example: |
1386 | // foo^ |
1387 | // <div>bar</div> |
1388 | // baz |
1389 | // Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That would |
1390 | // cause 'baz' to collapse onto the line with 'foobar' unless we insert a br. |
1391 | // Must recononicalize these two VisiblePositions after the pruning above. |
1392 | beforeParagraph = VisiblePosition(beforeParagraph.deepEquivalent()); |
1393 | afterParagraph = VisiblePosition(afterParagraph.deepEquivalent()); |
1394 | |
1395 | if (beforeParagraph.isNotNull() && !isRenderedTable(beforeParagraph.deepEquivalent().deprecatedNode()) |
1396 | && ((!isEndOfParagraph(beforeParagraph) && !isStartOfParagraph(beforeParagraph)) || beforeParagraph == afterParagraph) |
1397 | && isEditablePosition(beforeParagraph.deepEquivalent())) { |
1398 | // FIXME: Trim text between beforeParagraph and afterParagraph if they aren't equal. |
1399 | insertNodeAt(HTMLBRElement::create(document()), beforeParagraph.deepEquivalent()); |
1400 | } |
1401 | } |
1402 | |
1403 | |
1404 | // This moves a paragraph preserving its style. |
1405 | void CompositeEditCommand::moveParagraph(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, const VisiblePosition& destination, bool preserveSelection, bool preserveStyle) |
1406 | { |
1407 | ASSERT(isStartOfParagraph(startOfParagraphToMove)); |
1408 | ASSERT(isEndOfParagraph(endOfParagraphToMove)); |
1409 | moveParagraphs(startOfParagraphToMove, endOfParagraphToMove, destination, preserveSelection, preserveStyle); |
1410 | } |
1411 | |
1412 | void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, const VisiblePosition& destination, bool preserveSelection, bool preserveStyle) |
1413 | { |
1414 | if (startOfParagraphToMove == destination) |
1415 | return; |
1416 | |
1417 | int startIndex = -1; |
1418 | int endIndex = -1; |
1419 | int destinationIndex = -1; |
1420 | bool originalIsDirectional = endingSelection().isDirectional(); |
1421 | if (preserveSelection && !endingSelection().isNone()) { |
1422 | VisiblePosition visibleStart = endingSelection().visibleStart(); |
1423 | VisiblePosition visibleEnd = endingSelection().visibleEnd(); |
1424 | |
1425 | bool startAfterParagraph = comparePositions(visibleStart, endOfParagraphToMove) > 0; |
1426 | bool endBeforeParagraph = comparePositions(visibleEnd, startOfParagraphToMove) < 0; |
1427 | |
1428 | if (!startAfterParagraph && !endBeforeParagraph) { |
1429 | bool startInParagraph = comparePositions(visibleStart, startOfParagraphToMove) >= 0; |
1430 | bool endInParagraph = comparePositions(visibleEnd, endOfParagraphToMove) <= 0; |
1431 | |
1432 | startIndex = 0; |
1433 | if (startInParagraph) { |
1434 | auto startRange = Range::create(document(), startOfParagraphToMove.deepEquivalent().parentAnchoredEquivalent(), visibleStart.deepEquivalent().parentAnchoredEquivalent()); |
1435 | startIndex = TextIterator::rangeLength(startRange.ptr(), true); |
1436 | } |
1437 | |
1438 | endIndex = 0; |
1439 | if (endInParagraph) { |
1440 | auto endRange = Range::create(document(), startOfParagraphToMove.deepEquivalent().parentAnchoredEquivalent(), visibleEnd.deepEquivalent().parentAnchoredEquivalent()); |
1441 | endIndex = TextIterator::rangeLength(endRange.ptr(), true); |
1442 | } |
1443 | } |
1444 | } |
1445 | |
1446 | VisiblePosition beforeParagraph = startOfParagraphToMove.previous(CannotCrossEditingBoundary); |
1447 | VisiblePosition afterParagraph(endOfParagraphToMove.next(CannotCrossEditingBoundary)); |
1448 | |
1449 | // We upstream() the end and downstream() the start so that we don't include collapsed whitespace in the move. |
1450 | // When we paste a fragment, spaces after the end and before the start are treated as though they were rendered. |
1451 | Position start = startOfParagraphToMove.deepEquivalent().downstream(); |
1452 | Position end = endOfParagraphToMove.deepEquivalent().upstream(); |
1453 | |
1454 | // start and end can't be used directly to create a Range; they are "editing positions" |
1455 | Position startRangeCompliant = start.parentAnchoredEquivalent(); |
1456 | Position endRangeCompliant = end.parentAnchoredEquivalent(); |
1457 | auto range = Range::create(document(), startRangeCompliant.deprecatedNode(), startRangeCompliant.deprecatedEditingOffset(), endRangeCompliant.deprecatedNode(), endRangeCompliant.deprecatedEditingOffset()); |
1458 | |
1459 | // FIXME: This is an inefficient way to preserve style on nodes in the paragraph to move. It |
1460 | // shouldn't matter though, since moved paragraphs will usually be quite small. |
1461 | RefPtr<DocumentFragment> fragment; |
1462 | // This used to use a ternary for initialization, but that confused some versions of GCC, see bug 37912 |
1463 | if (startOfParagraphToMove != endOfParagraphToMove) |
1464 | fragment = createFragmentFromMarkup(document(), serializePreservingVisualAppearance(range.get(), nullptr, AnnotateForInterchange::No, ConvertBlocksToInlines::Yes), emptyString()); |
1465 | |
1466 | // A non-empty paragraph's style is moved when we copy and move it. We don't move |
1467 | // anything if we're given an empty paragraph, but an empty paragraph can have style |
1468 | // too, <div><b><br></b></div> for example. Save it so that we can preserve it later. |
1469 | RefPtr<EditingStyle> styleInEmptyParagraph; |
1470 | #if !PLATFORM(IOS_FAMILY) |
1471 | if (startOfParagraphToMove == endOfParagraphToMove && preserveStyle) { |
1472 | #else |
1473 | if (startOfParagraphToMove == endOfParagraphToMove && preserveStyle && isRichlyEditablePosition(destination.deepEquivalent())) { |
1474 | #endif |
1475 | styleInEmptyParagraph = EditingStyle::create(startOfParagraphToMove.deepEquivalent()); |
1476 | styleInEmptyParagraph->mergeTypingStyle(document()); |
1477 | // The moved paragraph should assume the block style of the destination. |
1478 | styleInEmptyParagraph->removeBlockProperties(); |
1479 | } |
1480 | |
1481 | // FIXME (5098931): We should add a new insert action "WebViewInsertActionMoved" and call shouldInsertFragment here. |
1482 | |
1483 | setEndingSelection(VisibleSelection(start, end, DOWNSTREAM)); |
1484 | frame().editor().clearMisspellingsAndBadGrammar(endingSelection()); |
1485 | deleteSelection(false, false, false, false); |
1486 | |
1487 | ASSERT(destination.deepEquivalent().anchorNode()->isConnected()); |
1488 | cleanupAfterDeletion(destination); |
1489 | ASSERT(destination.deepEquivalent().anchorNode()->isConnected()); |
1490 | |
1491 | // Add a br if pruning an empty block level element caused a collapse. For example: |
1492 | // foo^ |
1493 | // <div>bar</div> |
1494 | // baz |
1495 | // Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That would |
1496 | // cause 'baz' to collapse onto the line with 'foobar' unless we insert a br. |
1497 | // Must recononicalize these two VisiblePositions after the pruning above. |
1498 | beforeParagraph = VisiblePosition(beforeParagraph.deepEquivalent()); |
1499 | afterParagraph = VisiblePosition(afterParagraph.deepEquivalent()); |
1500 | if (beforeParagraph.isNotNull() && ((!isStartOfParagraph(beforeParagraph) && !isEndOfParagraph(beforeParagraph)) || beforeParagraph == afterParagraph)) { |
1501 | // FIXME: Trim text between beforeParagraph and afterParagraph if they aren't equal. |
1502 | insertNodeAt(HTMLBRElement::create(document()), beforeParagraph.deepEquivalent()); |
1503 | // Need an updateLayout here in case inserting the br has split a text node. |
1504 | document().updateLayoutIgnorePendingStylesheets(); |
1505 | } |
1506 | |
1507 | RefPtr<ContainerNode> editableRoot = destination.rootEditableElement(); |
1508 | if (!editableRoot) |
1509 | editableRoot = &document(); |
1510 | |
1511 | auto startToDestinationRange = Range::create(document(), firstPositionInNode(editableRoot.get()), destination.deepEquivalent().parentAnchoredEquivalent()); |
1512 | destinationIndex = TextIterator::rangeLength(startToDestinationRange.ptr(), true); |
1513 | |
1514 | setEndingSelection(VisibleSelection(destination, originalIsDirectional)); |
1515 | ASSERT(endingSelection().isCaretOrRange()); |
1516 | OptionSet<ReplaceSelectionCommand::CommandOption> options { ReplaceSelectionCommand::SelectReplacement, ReplaceSelectionCommand::MovingParagraph }; |
1517 | if (!preserveStyle) |
1518 | options.add(ReplaceSelectionCommand::MatchStyle); |
1519 | applyCommandToComposite(ReplaceSelectionCommand::create(document(), WTFMove(fragment), options)); |
1520 | |
1521 | frame().editor().markMisspellingsAndBadGrammar(endingSelection()); |
1522 | |
1523 | // If the selection is in an empty paragraph, restore styles from the old empty paragraph to the new empty paragraph. |
1524 | bool selectionIsEmptyParagraph = endingSelection().isCaret() && isStartOfParagraph(endingSelection().visibleStart()) && isEndOfParagraph(endingSelection().visibleStart()); |
1525 | if (styleInEmptyParagraph && selectionIsEmptyParagraph) |
1526 | applyStyle(styleInEmptyParagraph.get()); |
1527 | |
1528 | if (preserveSelection && startIndex != -1) { |
1529 | // Fragment creation (using createMarkup) incorrectly uses regular |
1530 | // spaces instead of nbsps for some spaces that were rendered (11475), which |
1531 | // causes spaces to be collapsed during the move operation. This results |
1532 | // in a call to rangeFromLocationAndLength with a location past the end |
1533 | // of the document (which will return null). |
1534 | RefPtr<Range> start = TextIterator::rangeFromLocationAndLength(editableRoot.get(), destinationIndex + startIndex, 0, true); |
1535 | RefPtr<Range> end = TextIterator::rangeFromLocationAndLength(editableRoot.get(), destinationIndex + endIndex, 0, true); |
1536 | if (start && end) |
1537 | setEndingSelection(VisibleSelection(start->startPosition(), end->startPosition(), DOWNSTREAM, originalIsDirectional)); |
1538 | } |
1539 | } |
1540 | |
1541 | Optional<VisibleSelection> CompositeEditCommand::shouldBreakOutOfEmptyListItem() const |
1542 | { |
1543 | auto emptyListItem = enclosingEmptyListItem(endingSelection().visibleStart()); |
1544 | if (!emptyListItem) |
1545 | return WTF::nullopt; |
1546 | |
1547 | auto listNode = emptyListItem->parentNode(); |
1548 | // FIXME: Can't we do something better when the immediate parent wasn't a list node? |
1549 | if (!listNode |
1550 | || (!listNode->hasTagName(ulTag) && !listNode->hasTagName(olTag)) |
1551 | || !listNode->hasEditableStyle() |
1552 | || listNode == emptyListItem->rootEditableElement()) |
1553 | return WTF::nullopt; |
1554 | |
1555 | return VisibleSelection(endingSelection().start().previous(BackwardDeletion), endingSelection().end()); |
1556 | } |
1557 | |
1558 | // FIXME: Send an appropriate shouldDeleteRange call. |
1559 | bool CompositeEditCommand::breakOutOfEmptyListItem() |
1560 | { |
1561 | if (!shouldBreakOutOfEmptyListItem()) |
1562 | return false; |
1563 | |
1564 | auto emptyListItem = enclosingEmptyListItem(endingSelection().visibleStart()); |
1565 | auto listNode = emptyListItem->parentNode(); |
1566 | auto style = EditingStyle::create(endingSelection().start()); |
1567 | style->mergeTypingStyle(document()); |
1568 | |
1569 | RefPtr<Element> newBlock; |
1570 | if (ContainerNode* blockEnclosingList = listNode->parentNode()) { |
1571 | if (is<HTMLLIElement>(*blockEnclosingList)) { // listNode is inside another list item |
1572 | if (visiblePositionAfterNode(*blockEnclosingList) == visiblePositionAfterNode(*listNode)) { |
1573 | // If listNode appears at the end of the outer list item, then move listNode outside of this list item |
1574 | // e.g. <ul><li>hello <ul><li><br></li></ul> </li></ul> should become <ul><li>hello</li> <ul><li><br></li></ul> </ul> after this section |
1575 | // If listNode does NOT appear at the end, then we should consider it as a regular paragraph. |
1576 | // e.g. <ul><li> <ul><li><br></li></ul> hello</li></ul> should become <ul><li> <div><br></div> hello</li></ul> at the end |
1577 | splitElement(downcast<HTMLLIElement>(*blockEnclosingList), *listNode); |
1578 | removeNodePreservingChildren(*listNode->parentNode()); |
1579 | newBlock = HTMLLIElement::create(document()); |
1580 | } |
1581 | // If listNode does NOT appear at the end of the outer list item, then behave as if in a regular paragraph. |
1582 | } else if (blockEnclosingList->hasTagName(olTag) || blockEnclosingList->hasTagName(ulTag)) |
1583 | newBlock = HTMLLIElement::create(document()); |
1584 | } |
1585 | if (!newBlock) |
1586 | newBlock = createDefaultParagraphElement(document()); |
1587 | |
1588 | RefPtr<Node> previousListNode = emptyListItem->isElementNode() ? ElementTraversal::previousSibling(*emptyListItem): emptyListItem->previousSibling(); |
1589 | RefPtr<Node> nextListNode = emptyListItem->isElementNode() ? ElementTraversal::nextSibling(*emptyListItem): emptyListItem->nextSibling(); |
1590 | if (isListItem(nextListNode.get()) || isListHTMLElement(nextListNode.get())) { |
1591 | // If emptyListItem follows another list item or nested list, split the list node. |
1592 | if (isListItem(previousListNode.get()) || isListHTMLElement(previousListNode.get())) |
1593 | splitElement(downcast<Element>(*listNode), *emptyListItem); |
1594 | |
1595 | // If emptyListItem is followed by other list item or nested list, then insert newBlock before the list node. |
1596 | // Because we have splitted the element, emptyListItem is the first element in the list node. |
1597 | // i.e. insert newBlock before ul or ol whose first element is emptyListItem |
1598 | insertNodeBefore(*newBlock, *listNode); |
1599 | removeNode(*emptyListItem); |
1600 | } else { |
1601 | // When emptyListItem does not follow any list item or nested list, insert newBlock after the enclosing list node. |
1602 | // Remove the enclosing node if emptyListItem is the only child; otherwise just remove emptyListItem. |
1603 | insertNodeAfter(*newBlock, *listNode); |
1604 | removeNode(isListItem(previousListNode.get()) || isListHTMLElement(previousListNode.get()) ? *emptyListItem : *listNode); |
1605 | } |
1606 | |
1607 | appendBlockPlaceholder(*newBlock); |
1608 | setEndingSelection(VisibleSelection(firstPositionInNode(newBlock.get()), DOWNSTREAM, endingSelection().isDirectional())); |
1609 | |
1610 | style->prepareToApplyAt(endingSelection().start()); |
1611 | if (!style->isEmpty()) |
1612 | applyStyle(style.ptr()); |
1613 | |
1614 | return true; |
1615 | } |
1616 | |
1617 | // If the caret is in an empty quoted paragraph, and either there is nothing before that |
1618 | // paragraph, or what is before is unquoted, and the user presses delete, unquote that paragraph. |
1619 | bool CompositeEditCommand::breakOutOfEmptyMailBlockquotedParagraph() |
1620 | { |
1621 | if (!endingSelection().isCaret()) |
1622 | return false; |
1623 | |
1624 | VisiblePosition caret(endingSelection().visibleStart()); |
1625 | Node* highestBlockquote = highestEnclosingNodeOfType(caret.deepEquivalent(), &isMailBlockquote); |
1626 | if (!highestBlockquote) |
1627 | return false; |
1628 | |
1629 | if (!isStartOfParagraph(caret) || !isEndOfParagraph(caret)) |
1630 | return false; |
1631 | |
1632 | VisiblePosition previous(caret.previous(CannotCrossEditingBoundary)); |
1633 | // Only move forward if there's nothing before the caret, or if there's unquoted content before it. |
1634 | if (enclosingNodeOfType(previous.deepEquivalent(), &isMailBlockquote)) |
1635 | return false; |
1636 | |
1637 | auto br = HTMLBRElement::create(document()); |
1638 | auto* brPtr = br.ptr(); |
1639 | // We want to replace this quoted paragraph with an unquoted one, so insert a br |
1640 | // to hold the caret before the highest blockquote. |
1641 | insertNodeBefore(WTFMove(br), *highestBlockquote); |
1642 | VisiblePosition atBR(positionBeforeNode(brPtr)); |
1643 | // If the br we inserted collapsed, for example foo<br><blockquote>...</blockquote>, insert |
1644 | // a second one. |
1645 | if (!isStartOfParagraph(atBR)) |
1646 | insertNodeBefore(HTMLBRElement::create(document()), *brPtr); |
1647 | setEndingSelection(VisibleSelection(atBR, endingSelection().isDirectional())); |
1648 | |
1649 | // If this is an empty paragraph there must be a line break here. |
1650 | if (!lineBreakExistsAtVisiblePosition(caret)) |
1651 | return false; |
1652 | |
1653 | Position caretPos(caret.deepEquivalent().downstream()); |
1654 | // A line break is either a br or a preserved newline. |
1655 | ASSERT(caretPos.deprecatedNode()->hasTagName(brTag) || (caretPos.deprecatedNode()->isTextNode() && caretPos.deprecatedNode()->renderer()->style().preserveNewline())); |
1656 | |
1657 | if (caretPos.deprecatedNode()->hasTagName(brTag)) |
1658 | removeNodeAndPruneAncestors(*caretPos.deprecatedNode()); |
1659 | else if (is<Text>(*caretPos.deprecatedNode())) { |
1660 | ASSERT(caretPos.deprecatedEditingOffset() == 0); |
1661 | Text& textNode = downcast<Text>(*caretPos.deprecatedNode()); |
1662 | ContainerNode* parentNode = textNode.parentNode(); |
1663 | // The preserved newline must be the first thing in the node, since otherwise the previous |
1664 | // paragraph would be quoted, and we verified that it wasn't above. |
1665 | deleteTextFromNode(textNode, 0, 1); |
1666 | prune(parentNode); |
1667 | } |
1668 | |
1669 | return true; |
1670 | } |
1671 | |
1672 | // Operations use this function to avoid inserting content into an anchor when at the start or the end of |
1673 | // that anchor, as in NSTextView. |
1674 | // FIXME: This is only an approximation of NSTextViews insertion behavior, which varies depending on how |
1675 | // the caret was made. |
1676 | Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Position& original) |
1677 | { |
1678 | if (original.isNull()) |
1679 | return original; |
1680 | |
1681 | VisiblePosition visiblePos(original); |
1682 | Element* enclosingAnchor = enclosingAnchorElement(original); |
1683 | Position result = original; |
1684 | |
1685 | if (!enclosingAnchor) |
1686 | return result; |
1687 | |
1688 | // Don't avoid block level anchors, because that would insert content into the wrong paragraph. |
1689 | if (enclosingAnchor && !isBlock(enclosingAnchor)) { |
1690 | VisiblePosition firstInAnchor(firstPositionInNode(enclosingAnchor)); |
1691 | VisiblePosition lastInAnchor(lastPositionInNode(enclosingAnchor)); |
1692 | // If visually just after the anchor, insert *inside* the anchor unless it's the last |
1693 | // VisiblePosition in the document, to match NSTextView. |
1694 | if (visiblePos == lastInAnchor) { |
1695 | // Make sure anchors are pushed down before avoiding them so that we don't |
1696 | // also avoid structural elements like lists and blocks (5142012). |
1697 | if (original.deprecatedNode() != enclosingAnchor && original.deprecatedNode()->parentNode() != enclosingAnchor) { |
1698 | pushAnchorElementDown(*enclosingAnchor); |
1699 | enclosingAnchor = enclosingAnchorElement(original); |
1700 | if (!enclosingAnchor) |
1701 | return original; |
1702 | } |
1703 | // Don't insert outside an anchor if doing so would skip over a line break. It would |
1704 | // probably be safe to move the line break so that we could still avoid the anchor here. |
1705 | Position downstream(visiblePos.deepEquivalent().downstream()); |
1706 | if (lineBreakExistsAtVisiblePosition(visiblePos) && downstream.deprecatedNode()->isDescendantOf(enclosingAnchor)) |
1707 | return original; |
1708 | |
1709 | result = positionInParentAfterNode(enclosingAnchor); |
1710 | } |
1711 | // If visually just before an anchor, insert *outside* the anchor unless it's the first |
1712 | // VisiblePosition in a paragraph, to match NSTextView. |
1713 | if (visiblePos == firstInAnchor) { |
1714 | // Make sure anchors are pushed down before avoiding them so that we don't |
1715 | // also avoid structural elements like lists and blocks (5142012). |
1716 | if (original.deprecatedNode() != enclosingAnchor && original.deprecatedNode()->parentNode() != enclosingAnchor) { |
1717 | pushAnchorElementDown(*enclosingAnchor); |
1718 | enclosingAnchor = enclosingAnchorElement(original); |
1719 | } |
1720 | if (!enclosingAnchor) |
1721 | return original; |
1722 | |
1723 | result = positionInParentBeforeNode(enclosingAnchor); |
1724 | } |
1725 | } |
1726 | |
1727 | if (result.isNull() || !editableRootForPosition(result)) |
1728 | result = original; |
1729 | |
1730 | return result; |
1731 | } |
1732 | |
1733 | // Splits the tree parent by parent until we reach the specified ancestor. We use VisiblePositions |
1734 | // to determine if the split is necessary. Returns the last split node. |
1735 | RefPtr<Node> CompositeEditCommand::splitTreeToNode(Node& start, Node& end, bool shouldSplitAncestor) |
1736 | { |
1737 | ASSERT(&start != &end); |
1738 | |
1739 | RefPtr<Node> adjustedEnd = &end; |
1740 | if (shouldSplitAncestor && adjustedEnd->parentNode()) |
1741 | adjustedEnd = adjustedEnd->parentNode(); |
1742 | |
1743 | ASSERT(adjustedEnd); |
1744 | RefPtr<Node> node; |
1745 | for (node = &start; node && node->parentNode() != adjustedEnd; node = node->parentNode()) { |
1746 | if (!is<Element>(*node->parentNode())) |
1747 | break; |
1748 | // Do not split a node when doing so introduces an empty node. |
1749 | VisiblePosition positionInParent = firstPositionInNode(node->parentNode()); |
1750 | VisiblePosition positionInNode = firstPositionInOrBeforeNode(node.get()); |
1751 | if (positionInParent != positionInNode) |
1752 | splitElement(downcast<Element>(*node->parentNode()), *node); |
1753 | } |
1754 | |
1755 | return node; |
1756 | } |
1757 | |
1758 | Ref<Element> createBlockPlaceholderElement(Document& document) |
1759 | { |
1760 | return HTMLBRElement::create(document); |
1761 | } |
1762 | |
1763 | } // namespace WebCore |
1764 | |