| 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 | |