1/*
2 * Copyright (C) 2005-2008, 2016 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 "TypingCommand.h"
28
29#include "AXObjectCache.h"
30#include "BreakBlockquoteCommand.h"
31#include "DataTransfer.h"
32#include "DeleteSelectionCommand.h"
33#include "Document.h"
34#include "Editing.h"
35#include "Editor.h"
36#include "Element.h"
37#include "Frame.h"
38#include "HTMLElement.h"
39#include "HTMLNames.h"
40#include "InsertLineBreakCommand.h"
41#include "InsertParagraphSeparatorCommand.h"
42#include "InsertTextCommand.h"
43#include "Logging.h"
44#include "MarkupAccumulator.h"
45#include "MathMLElement.h"
46#include "RenderElement.h"
47#include "StaticRange.h"
48#include "TextIterator.h"
49#include "VisibleUnits.h"
50
51namespace WebCore {
52
53using namespace HTMLNames;
54
55class TypingCommandLineOperation
56{
57public:
58 TypingCommandLineOperation(TypingCommand* typingCommand, bool selectInsertedText, const String& text)
59 : m_typingCommand(typingCommand)
60 , m_selectInsertedText(selectInsertedText)
61 , m_text(text)
62 { }
63
64 void operator()(size_t lineOffset, size_t lineLength, bool isLastLine) const
65 {
66 if (isLastLine) {
67 if (!lineOffset || lineLength > 0)
68 m_typingCommand->insertTextRunWithoutNewlines(m_text.substring(lineOffset, lineLength), m_selectInsertedText);
69 } else {
70 if (lineLength > 0)
71 m_typingCommand->insertTextRunWithoutNewlines(m_text.substring(lineOffset, lineLength), false);
72 m_typingCommand->insertParagraphSeparator();
73 }
74 }
75
76private:
77 TypingCommand* m_typingCommand;
78 bool m_selectInsertedText;
79 const String& m_text;
80};
81
82static inline EditAction editActionForTypingCommand(TypingCommand::ETypingCommand command, TextGranularity granularity, TypingCommand::TextCompositionType compositionType, bool isAutocompletion)
83{
84 if (compositionType == TypingCommand::TextCompositionPending) {
85 if (command == TypingCommand::InsertText)
86 return EditAction::TypingInsertPendingComposition;
87 if (command == TypingCommand::DeleteSelection)
88 return EditAction::TypingDeletePendingComposition;
89 ASSERT_NOT_REACHED();
90 }
91
92 if (compositionType == TypingCommand::TextCompositionFinal) {
93 if (command == TypingCommand::InsertText)
94 return EditAction::TypingInsertFinalComposition;
95 if (command == TypingCommand::DeleteSelection)
96 return EditAction::TypingDeleteFinalComposition;
97 ASSERT_NOT_REACHED();
98 }
99
100 switch (command) {
101 case TypingCommand::DeleteSelection:
102 return EditAction::TypingDeleteSelection;
103 case TypingCommand::DeleteKey: {
104 if (granularity == WordGranularity)
105 return EditAction::TypingDeleteWordBackward;
106 if (granularity == LineBoundary)
107 return EditAction::TypingDeleteLineBackward;
108 return EditAction::TypingDeleteBackward;
109 }
110 case TypingCommand::ForwardDeleteKey:
111 if (granularity == WordGranularity)
112 return EditAction::TypingDeleteWordForward;
113 if (granularity == LineBoundary)
114 return EditAction::TypingDeleteLineForward;
115 return EditAction::TypingDeleteForward;
116 case TypingCommand::InsertText:
117 return isAutocompletion ? EditAction::InsertReplacement : EditAction::TypingInsertText;
118 case TypingCommand::InsertLineBreak:
119 return EditAction::TypingInsertLineBreak;
120 case TypingCommand::InsertParagraphSeparator:
121 case TypingCommand::InsertParagraphSeparatorInQuotedContent:
122 return EditAction::TypingInsertParagraph;
123 default:
124 return EditAction::Unspecified;
125 }
126}
127
128static inline bool editActionIsDeleteByTyping(EditAction action)
129{
130 switch (action) {
131 case EditAction::TypingDeleteSelection:
132 case EditAction::TypingDeleteBackward:
133 case EditAction::TypingDeleteWordBackward:
134 case EditAction::TypingDeleteLineBackward:
135 case EditAction::TypingDeleteForward:
136 case EditAction::TypingDeleteWordForward:
137 case EditAction::TypingDeleteLineForward:
138 return true;
139 default:
140 return false;
141 }
142}
143
144TypingCommand::TypingCommand(Document& document, ETypingCommand commandType, const String &textToInsert, Options options, TextGranularity granularity, TextCompositionType compositionType)
145 : TextInsertionBaseCommand(document, editActionForTypingCommand(commandType, granularity, compositionType, options & IsAutocompletion))
146 , m_commandType(commandType)
147 , m_textToInsert(textToInsert)
148 , m_currentTextToInsert(textToInsert)
149 , m_openForMoreTyping(true)
150 , m_selectInsertedText(options & SelectInsertedText)
151 , m_smartDelete(options & SmartDelete)
152 , m_granularity(granularity)
153 , m_compositionType(compositionType)
154 , m_shouldAddToKillRing(options & AddsToKillRing)
155 , m_isAutocompletion(options & IsAutocompletion)
156 , m_openedByBackwardDelete(false)
157 , m_shouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator)
158 , m_shouldPreventSpellChecking(options & PreventSpellChecking)
159{
160 m_currentTypingEditAction = editingAction();
161 updatePreservesTypingStyle(m_commandType);
162}
163
164void TypingCommand::deleteSelection(Document& document, Options options, TextCompositionType compositionType)
165{
166 Frame* frame = document.frame();
167 ASSERT(frame);
168
169 if (!frame->selection().isRange())
170 return;
171
172 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*frame)) {
173 lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
174 lastTypingCommand->setCompositionType(compositionType);
175 lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
176 lastTypingCommand->deleteSelection(options & SmartDelete);
177 return;
178 }
179
180 TypingCommand::create(document, DeleteSelection, emptyString(), options, compositionType)->apply();
181}
182
183void TypingCommand::deleteKeyPressed(Document& document, Options options, TextGranularity granularity)
184{
185 if (granularity == CharacterGranularity) {
186 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*document.frame())) {
187 updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand.get(), document.frame());
188 lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
189 lastTypingCommand->setCompositionType(TextCompositionNone);
190 lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
191 lastTypingCommand->deleteKeyPressed(granularity, options & AddsToKillRing);
192 return;
193 }
194 }
195
196 TypingCommand::create(document, DeleteKey, emptyString(), options, granularity)->apply();
197}
198
199void TypingCommand::forwardDeleteKeyPressed(Document& document, Options options, TextGranularity granularity)
200{
201 // FIXME: Forward delete in TextEdit appears to open and close a new typing command.
202 Frame* frame = document.frame();
203 if (granularity == CharacterGranularity) {
204 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*frame)) {
205 updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand.get(), frame);
206 lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
207 lastTypingCommand->setCompositionType(TextCompositionNone);
208 lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
209 lastTypingCommand->forwardDeleteKeyPressed(granularity, options & AddsToKillRing);
210 return;
211 }
212 }
213
214 TypingCommand::create(document, ForwardDeleteKey, emptyString(), options, granularity)->apply();
215}
216
217void TypingCommand::updateSelectionIfDifferentFromCurrentSelection(TypingCommand* typingCommand, Frame* frame)
218{
219 ASSERT(frame);
220 VisibleSelection currentSelection = frame->selection().selection();
221 if (currentSelection == typingCommand->endingSelection())
222 return;
223
224 typingCommand->setStartingSelection(currentSelection);
225 typingCommand->setEndingSelection(currentSelection);
226}
227
228void TypingCommand::insertText(Document& document, const String& text, Options options, TextCompositionType composition)
229{
230 Frame* frame = document.frame();
231 ASSERT(frame);
232
233 if (!text.isEmpty())
234 frame->editor().updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text[0]));
235
236 insertText(document, text, frame->selection().selection(), options, composition);
237}
238
239// FIXME: We shouldn't need to take selectionForInsertion. It should be identical to FrameSelection's current selection.
240void TypingCommand::insertText(Document& document, const String& text, const VisibleSelection& selectionForInsertion, Options options, TextCompositionType compositionType)
241{
242 RefPtr<Frame> frame = document.frame();
243 ASSERT(frame);
244
245 LOG(Editing, "TypingCommand::insertText (text %s)", text.utf8().data());
246
247 VisibleSelection currentSelection = frame->selection().selection();
248
249 String newText = dispatchBeforeTextInsertedEvent(text, selectionForInsertion, compositionType == TextCompositionPending);
250
251 // Set the starting and ending selection appropriately if we are using a selection
252 // that is different from the current selection. In the future, we should change EditCommand
253 // to deal with custom selections in a general way that can be used by all of the commands.
254 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*frame)) {
255 if (lastTypingCommand->endingSelection() != selectionForInsertion) {
256 lastTypingCommand->setStartingSelection(selectionForInsertion);
257 lastTypingCommand->setEndingSelection(selectionForInsertion);
258 }
259
260 lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
261 lastTypingCommand->setCompositionType(compositionType);
262 lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator);
263 lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
264 lastTypingCommand->insertTextAndNotifyAccessibility(newText, options & SelectInsertedText);
265 return;
266 }
267
268 auto cmd = TypingCommand::create(document, InsertText, newText, options, compositionType);
269 applyTextInsertionCommand(frame.get(), cmd.get(), selectionForInsertion, currentSelection);
270}
271
272void TypingCommand::insertLineBreak(Document& document, Options options)
273{
274 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*document.frame())) {
275 lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
276 lastTypingCommand->setCompositionType(TextCompositionNone);
277 lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator);
278 lastTypingCommand->insertLineBreakAndNotifyAccessibility();
279 return;
280 }
281
282 TypingCommand::create(document, InsertLineBreak, emptyString(), options)->apply();
283}
284
285void TypingCommand::insertParagraphSeparatorInQuotedContent(Document& document)
286{
287 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*document.frame())) {
288 lastTypingCommand->setIsAutocompletion(false);
289 lastTypingCommand->setCompositionType(TextCompositionNone);
290 lastTypingCommand->insertParagraphSeparatorInQuotedContentAndNotifyAccessibility();
291 return;
292 }
293
294 TypingCommand::create(document, InsertParagraphSeparatorInQuotedContent)->apply();
295}
296
297void TypingCommand::insertParagraphSeparator(Document& document, Options options)
298{
299 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*document.frame())) {
300 lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
301 lastTypingCommand->setCompositionType(TextCompositionNone);
302 lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator);
303 lastTypingCommand->insertParagraphSeparatorAndNotifyAccessibility();
304 return;
305 }
306
307 TypingCommand::create(document, InsertParagraphSeparator, emptyString(), options)->apply();
308}
309
310RefPtr<TypingCommand> TypingCommand::lastTypingCommandIfStillOpenForTyping(Frame& frame)
311{
312 RefPtr<CompositeEditCommand> lastEditCommand = frame.editor().lastEditCommand();
313 if (!lastEditCommand || !lastEditCommand->isTypingCommand() || !static_cast<TypingCommand*>(lastEditCommand.get())->isOpenForMoreTyping())
314 return nullptr;
315
316 return static_cast<TypingCommand*>(lastEditCommand.get());
317}
318
319bool TypingCommand::shouldDeferWillApplyCommandUntilAddingTypingCommand() const
320{
321 return !m_isHandlingInitialTypingCommand || editActionIsDeleteByTyping(editingAction());
322}
323
324void TypingCommand::closeTyping(Frame* frame)
325{
326 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*frame))
327 lastTypingCommand->closeTyping();
328}
329
330#if PLATFORM(IOS_FAMILY)
331void TypingCommand::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping(Frame* frame, const VisibleSelection& newSelection)
332{
333 if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*frame)) {
334 lastTypingCommand->setEndingSelection(newSelection);
335 lastTypingCommand->setEndingSelectionOnLastInsertCommand(newSelection);
336 }
337}
338#endif
339
340void TypingCommand::postTextStateChangeNotificationForDeletion(const VisibleSelection& selection)
341{
342 if (!AXObjectCache::accessibilityEnabled())
343 return;
344 postTextStateChangeNotification(AXTextEditTypeDelete, AccessibilityObject::stringForVisiblePositionRange(selection), selection.start());
345 VisiblePositionIndexRange range;
346 range.startIndex.value = indexForVisiblePosition(selection.start(), range.startIndex.scope);
347 range.endIndex.value = indexForVisiblePosition(selection.end(), range.endIndex.scope);
348 composition()->setRangeDeletedByUnapply(range);
349}
350
351bool TypingCommand::willApplyCommand()
352{
353 if (shouldDeferWillApplyCommandUntilAddingTypingCommand()) {
354 // The TypingCommand will handle the willApplyCommand logic separately in TypingCommand::willAddTypingToOpenCommand.
355 return true;
356 }
357
358 return CompositeEditCommand::willApplyCommand();
359}
360
361void TypingCommand::doApply()
362{
363 if (endingSelection().isNoneOrOrphaned())
364 return;
365
366 if (m_commandType == DeleteKey)
367 if (m_commands.isEmpty())
368 m_openedByBackwardDelete = true;
369
370 switch (m_commandType) {
371 case DeleteSelection:
372 deleteSelection(m_smartDelete);
373 return;
374 case DeleteKey:
375 deleteKeyPressed(m_granularity, m_shouldAddToKillRing);
376 return;
377 case ForwardDeleteKey:
378 forwardDeleteKeyPressed(m_granularity, m_shouldAddToKillRing);
379 return;
380 case InsertLineBreak:
381 insertLineBreakAndNotifyAccessibility();
382 return;
383 case InsertParagraphSeparator:
384 insertParagraphSeparatorAndNotifyAccessibility();
385 return;
386 case InsertParagraphSeparatorInQuotedContent:
387 insertParagraphSeparatorInQuotedContentAndNotifyAccessibility();
388 return;
389 case InsertText:
390 insertTextAndNotifyAccessibility(m_textToInsert, m_selectInsertedText);
391 return;
392 }
393
394 ASSERT_NOT_REACHED();
395}
396
397String TypingCommand::inputEventTypeName() const
398{
399 return inputTypeNameForEditingAction(m_currentTypingEditAction);
400}
401
402bool TypingCommand::isBeforeInputEventCancelable() const
403{
404 return m_currentTypingEditAction != EditAction::TypingInsertPendingComposition && m_currentTypingEditAction != EditAction::TypingDeletePendingComposition;
405}
406
407String TypingCommand::inputEventData() const
408{
409 switch (m_currentTypingEditAction) {
410 case EditAction::TypingInsertText:
411 case EditAction::TypingInsertPendingComposition:
412 case EditAction::TypingInsertFinalComposition:
413 return m_currentTextToInsert;
414 case EditAction::InsertReplacement:
415 return isEditingTextAreaOrTextInput() ? m_currentTextToInsert : String();
416 default:
417 return CompositeEditCommand::inputEventData();
418 }
419}
420
421RefPtr<DataTransfer> TypingCommand::inputEventDataTransfer() const
422{
423 if (m_currentTypingEditAction != EditAction::InsertReplacement || isEditingTextAreaOrTextInput())
424 return nullptr;
425
426 StringBuilder htmlText;
427 MarkupAccumulator::appendCharactersReplacingEntities(htmlText, m_currentTextToInsert, 0, m_currentTextToInsert.length(), EntityMaskInHTMLPCDATA);
428 return DataTransfer::createForInputEvent(m_currentTextToInsert, htmlText.toString());
429}
430
431void TypingCommand::didApplyCommand()
432{
433 // TypingCommands handle applied editing separately (see TypingCommand::typingAddedToOpenCommand).
434 m_isHandlingInitialTypingCommand = false;
435}
436
437void TypingCommand::markMisspellingsAfterTyping(ETypingCommand commandType)
438{
439 Frame& frame = this->frame();
440
441#if PLATFORM(MAC)
442 if (!frame.editor().isContinuousSpellCheckingEnabled()
443 && !frame.editor().isAutomaticQuoteSubstitutionEnabled()
444 && !frame.editor().isAutomaticLinkDetectionEnabled()
445 && !frame.editor().isAutomaticDashSubstitutionEnabled()
446 && !frame.editor().isAutomaticTextReplacementEnabled())
447 return;
448 if (frame.editor().isHandlingAcceptedCandidate())
449 return;
450#else
451 if (!frame.editor().isContinuousSpellCheckingEnabled())
452 return;
453#endif
454 // Take a look at the selection that results after typing and determine whether we need to spellcheck.
455 // Since the word containing the current selection is never marked, this does a check to
456 // see if typing made a new word that is not in the current selection. Basically, you
457 // get this by being at the end of a word and typing a space.
458 VisiblePosition start(endingSelection().start(), endingSelection().affinity());
459 VisiblePosition previous = start.previous();
460 if (previous.isNotNull()) {
461#if !PLATFORM(IOS_FAMILY)
462 VisiblePosition p1 = startOfWord(previous, LeftWordIfOnBoundary);
463 VisiblePosition p2 = startOfWord(start, LeftWordIfOnBoundary);
464 if (p1 != p2) {
465 RefPtr<Range> range = makeRange(p1, p2);
466 String strippedPreviousWord;
467 if (range && (commandType == TypingCommand::InsertText || commandType == TypingCommand::InsertLineBreak || commandType == TypingCommand::InsertParagraphSeparator || commandType == TypingCommand::InsertParagraphSeparatorInQuotedContent))
468 strippedPreviousWord = plainText(range.get()).stripWhiteSpace();
469 frame.editor().markMisspellingsAfterTypingToWord(p1, endingSelection(), !strippedPreviousWord.isEmpty());
470 } else if (commandType == TypingCommand::InsertText)
471 frame.editor().startAlternativeTextUITimer();
472#else
473 UNUSED_PARAM(commandType);
474 // If this bug gets fixed, this PLATFORM(IOS_FAMILY) code could be removed:
475 // <rdar://problem/7259611> Word boundary code on iPhone gives different results than desktop
476 EWordSide startWordSide = LeftWordIfOnBoundary;
477 UChar32 c = previous.characterAfter();
478 // FIXME: VisiblePosition::characterAfter() and characterBefore() do not emit newlines the same
479 // way as TextIterator, so we do an isEndOfParagraph check here.
480 if (isSpaceOrNewline(c) || c == noBreakSpace || isEndOfParagraph(previous)) {
481 startWordSide = RightWordIfOnBoundary;
482 }
483 VisiblePosition p1 = startOfWord(previous, startWordSide);
484 VisiblePosition p2 = startOfWord(start, startWordSide);
485 if (p1 != p2)
486 frame.editor().markMisspellingsAfterTypingToWord(p1, endingSelection(), false);
487#endif // !PLATFORM(IOS_FAMILY)
488 }
489}
490
491bool TypingCommand::willAddTypingToOpenCommand(ETypingCommand commandType, TextGranularity granularity, const String& text, RefPtr<Range>&& range)
492{
493 m_currentTextToInsert = text;
494 m_currentTypingEditAction = editActionForTypingCommand(commandType, granularity, m_compositionType, m_isAutocompletion);
495
496 if (!shouldDeferWillApplyCommandUntilAddingTypingCommand())
497 return true;
498
499 if (!range || isEditingTextAreaOrTextInput())
500 return frame().editor().willApplyEditing(*this, CompositeEditCommand::targetRangesForBindings());
501
502 return frame().editor().willApplyEditing(*this, { 1, StaticRange::createFromRange(*range) });
503}
504
505void TypingCommand::typingAddedToOpenCommand(ETypingCommand commandTypeForAddedTyping)
506{
507 Frame& frame = this->frame();
508
509 updatePreservesTypingStyle(commandTypeForAddedTyping);
510
511#if PLATFORM(COCOA)
512 frame.editor().appliedEditing(*this);
513 // Since the spellchecking code may also perform corrections and other replacements, it should happen after the typing changes.
514 if (!m_shouldPreventSpellChecking)
515 markMisspellingsAfterTyping(commandTypeForAddedTyping);
516#else
517 // The old spellchecking code requires that checking be done first, to prevent issues like that in 6864072, where <doesn't> is marked as misspelled.
518 markMisspellingsAfterTyping(commandTypeForAddedTyping);
519 frame.editor().appliedEditing(*this);
520#endif
521}
522
523void TypingCommand::insertText(const String &text, bool selectInsertedText)
524{
525 // FIXME: Need to implement selectInsertedText for cases where more than one insert is involved.
526 // This requires support from insertTextRunWithoutNewlines and insertParagraphSeparator for extending
527 // an existing selection; at the moment they can either put the caret after what's inserted or
528 // select what's inserted, but there's no way to "extend selection" to include both an old selection
529 // that ends just before where we want to insert text and the newly inserted text.
530 TypingCommandLineOperation operation(this, selectInsertedText, text);
531 forEachLineInString(text, operation);
532}
533
534void TypingCommand::insertTextAndNotifyAccessibility(const String &text, bool selectInsertedText)
535{
536 LOG(Editing, "TypingCommand %p insertTextAndNotifyAccessibility (text %s, selectInsertedText %d)", this, text.utf8().data(), selectInsertedText);
537
538 AccessibilityReplacedText replacedText(frame().selection().selection());
539 insertText(text, selectInsertedText);
540 replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, text, frame().selection().selection());
541 composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
542}
543
544void TypingCommand::insertTextRunWithoutNewlines(const String &text, bool selectInsertedText)
545{
546 if (!willAddTypingToOpenCommand(InsertText, CharacterGranularity, text))
547 return;
548
549 auto command = InsertTextCommand::create(document(), text, selectInsertedText,
550 m_compositionType == TextCompositionNone ? InsertTextCommand::RebalanceLeadingAndTrailingWhitespaces : InsertTextCommand::RebalanceAllWhitespaces, EditAction::TypingInsertText);
551
552 applyCommandToComposite(WTFMove(command), endingSelection());
553
554 typingAddedToOpenCommand(InsertText);
555}
556
557void TypingCommand::insertLineBreak()
558{
559 if (!canAppendNewLineFeedToSelection(endingSelection()))
560 return;
561
562 if (!willAddTypingToOpenCommand(InsertLineBreak, LineGranularity))
563 return;
564
565 applyCommandToComposite(InsertLineBreakCommand::create(document()));
566 typingAddedToOpenCommand(InsertLineBreak);
567}
568
569void TypingCommand::insertLineBreakAndNotifyAccessibility()
570{
571 AccessibilityReplacedText replacedText(frame().selection().selection());
572 insertLineBreak();
573 replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, "\n", frame().selection().selection());
574 composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
575}
576
577void TypingCommand::insertParagraphSeparator()
578{
579 if (!canAppendNewLineFeedToSelection(endingSelection()))
580 return;
581
582 if (!willAddTypingToOpenCommand(InsertParagraphSeparator, ParagraphGranularity))
583 return;
584
585 applyCommandToComposite(InsertParagraphSeparatorCommand::create(document(), false, false, EditAction::TypingInsertParagraph));
586 typingAddedToOpenCommand(InsertParagraphSeparator);
587}
588
589void TypingCommand::insertParagraphSeparatorAndNotifyAccessibility()
590{
591 AccessibilityReplacedText replacedText(frame().selection().selection());
592 insertParagraphSeparator();
593 replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, "\n", frame().selection().selection());
594 composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
595}
596
597void TypingCommand::insertParagraphSeparatorInQuotedContent()
598{
599 if (!willAddTypingToOpenCommand(InsertParagraphSeparatorInQuotedContent, ParagraphGranularity))
600 return;
601
602 // If the selection starts inside a table, just insert the paragraph separator normally
603 // Breaking the blockquote would also break apart the table, which is unecessary when inserting a newline
604 if (enclosingNodeOfType(endingSelection().start(), &isTableStructureNode)) {
605 insertParagraphSeparator();
606 return;
607 }
608
609 applyCommandToComposite(BreakBlockquoteCommand::create(document()));
610 typingAddedToOpenCommand(InsertParagraphSeparatorInQuotedContent);
611}
612
613void TypingCommand::insertParagraphSeparatorInQuotedContentAndNotifyAccessibility()
614{
615 AccessibilityReplacedText replacedText(frame().selection().selection());
616 insertParagraphSeparatorInQuotedContent();
617 replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, "\n", frame().selection().selection());
618 composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
619}
620
621bool TypingCommand::makeEditableRootEmpty()
622{
623 Element* root = endingSelection().rootEditableElement();
624 if (!root || !root->firstChild())
625 return false;
626
627 if (root->firstChild() == root->lastChild() && root->firstElementChild() && root->firstElementChild()->hasTagName(brTag)) {
628 // If there is a single child and it could be a placeholder, leave it alone.
629 if (root->renderer() && root->renderer()->isRenderBlockFlow())
630 return false;
631 }
632
633 while (Node* child = root->firstChild())
634 removeNode(*child);
635
636 addBlockPlaceholderIfNeeded(root);
637 setEndingSelection(VisibleSelection(firstPositionInNode(root), DOWNSTREAM, endingSelection().isDirectional()));
638
639 return true;
640}
641
642void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool shouldAddToKillRing)
643{
644 Frame& frame = this->frame();
645 Ref<Frame> protector(frame);
646
647 frame.editor().updateMarkersForWordsAffectedByEditing(false);
648
649 VisibleSelection selectionToDelete;
650 VisibleSelection selectionAfterUndo;
651 bool expandForSpecialElements = !endingSelection().isCaret();
652
653 switch (endingSelection().selectionType()) {
654 case VisibleSelection::RangeSelection:
655 selectionToDelete = endingSelection();
656 selectionAfterUndo = selectionToDelete;
657 break;
658 case VisibleSelection::CaretSelection: {
659 // After breaking out of an empty mail blockquote, we still want continue with the deletion
660 // so actual content will get deleted, and not just the quote style.
661 if (breakOutOfEmptyMailBlockquotedParagraph())
662 typingAddedToOpenCommand(DeleteKey);
663
664 m_smartDelete = false;
665
666 FrameSelection selection;
667 selection.setSelection(endingSelection());
668 selection.modify(FrameSelection::AlterationExtend, DirectionBackward, granularity);
669 if (shouldAddToKillRing && selection.isCaret() && granularity != CharacterGranularity)
670 selection.modify(FrameSelection::AlterationExtend, DirectionBackward, CharacterGranularity);
671
672 const VisiblePosition& visibleStart = endingSelection().visibleStart();
673 const VisiblePosition& previousPosition = visibleStart.previous(CannotCrossEditingBoundary);
674 Node* enclosingTableCell = enclosingNodeOfType(visibleStart.deepEquivalent(), &isTableCell);
675 const Node* enclosingTableCellForPreviousPosition = enclosingNodeOfType(previousPosition.deepEquivalent(), &isTableCell);
676 if (previousPosition.isNull() || enclosingTableCell != enclosingTableCellForPreviousPosition) {
677 // When the caret is at the start of the editable area in an empty list item, break out of the list item.
678 if (auto deleteListSelection = shouldBreakOutOfEmptyListItem()) {
679 if (willAddTypingToOpenCommand(DeleteKey, granularity, { }, Range::create(document(), deleteListSelection.value().start(), deleteListSelection.value().end()))) {
680 breakOutOfEmptyListItem();
681 typingAddedToOpenCommand(DeleteKey);
682 }
683 return;
684 }
685 }
686 if (previousPosition.isNull()) {
687 // When there are no visible positions in the editing root, delete its entire contents.
688 // FIXME: Dispatch a `beforeinput` event here and bail if preventDefault() was invoked.
689 if (visibleStart.next(CannotCrossEditingBoundary).isNull() && makeEditableRootEmpty()) {
690 typingAddedToOpenCommand(DeleteKey);
691 return;
692 }
693 }
694
695 // If we have a caret selection at the beginning of a cell, we have nothing to do.
696 if (enclosingTableCell && visibleStart == firstPositionInNode(enclosingTableCell))
697 return;
698
699 // If the caret is at the start of a paragraph after a table, move content into the last table cell.
700 if (isStartOfParagraph(visibleStart) && isFirstPositionAfterTable(visibleStart.previous(CannotCrossEditingBoundary))) {
701 // Unless the caret is just before a table. We don't want to move a table into the last table cell.
702 if (isLastPositionBeforeTable(visibleStart))
703 return;
704 // Extend the selection backward into the last cell, then deletion will handle the move.
705 selection.modify(FrameSelection::AlterationExtend, DirectionBackward, granularity);
706 // If the caret is just after a table, select the table and don't delete anything.
707 } else if (Node* table = isFirstPositionAfterTable(visibleStart)) {
708 setEndingSelection(VisibleSelection(positionBeforeNode(table), endingSelection().start(), DOWNSTREAM, endingSelection().isDirectional()));
709 typingAddedToOpenCommand(DeleteKey);
710 return;
711 }
712
713 selectionToDelete = selection.selection();
714
715 if (granularity == CharacterGranularity && selectionToDelete.end().containerNode() == selectionToDelete.start().containerNode()
716 && selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode() > 1) {
717 // If there are multiple Unicode code points to be deleted, adjust the range to match platform conventions.
718 selectionToDelete.setWithoutValidation(selectionToDelete.end(), selectionToDelete.end().previous(BackwardDeletion));
719 }
720
721 if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
722 selectionAfterUndo = selectionToDelete;
723 else
724 // It's a little tricky to compute what the starting selection would have been in the original document.
725 // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on
726 // the current state of the document and we'll get the wrong result.
727 selectionAfterUndo.setWithoutValidation(startingSelection().end(), selectionToDelete.extent());
728 break;
729 }
730 case VisibleSelection::NoSelection:
731 ASSERT_NOT_REACHED();
732 break;
733 }
734
735 ASSERT(!selectionToDelete.isNone());
736 if (selectionToDelete.isNone()) {
737#if PLATFORM(IOS_FAMILY)
738 // Workaround for this bug:
739 // <rdar://problem/4653755> UIKit text widgets should use WebKit editing API to manipulate text
740 setEndingSelection(frame.selection().selection());
741 closeTyping(&frame);
742#endif
743 return;
744 }
745
746 if (selectionToDelete.isCaret() || !frame.selection().shouldDeleteSelection(selectionToDelete))
747 return;
748
749 if (!willAddTypingToOpenCommand(DeleteKey, granularity, { }, selectionToDelete.firstRange()))
750 return;
751
752 if (shouldAddToKillRing)
753 frame.editor().addRangeToKillRing(*selectionToDelete.toNormalizedRange().get(), Editor::KillRingInsertionMode::PrependText);
754
755 // Post the accessibility notification before actually deleting the content while selectionToDelete is still valid
756 postTextStateChangeNotificationForDeletion(selectionToDelete);
757
758 // Make undo select everything that has been deleted, unless an undo will undo more than just this deletion.
759 // FIXME: This behaves like TextEdit except for the case where you open with text insertion and then delete
760 // more text than you insert. In that case all of the text that was around originally should be selected.
761 if (m_openedByBackwardDelete)
762 setStartingSelection(selectionAfterUndo);
763 CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete, /* mergeBlocksAfterDelete*/ true, /* replace*/ false, expandForSpecialElements, /*sanitizeMarkup*/ true);
764 setSmartDelete(false);
765 typingAddedToOpenCommand(DeleteKey);
766}
767
768void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool shouldAddToKillRing)
769{
770 Frame& frame = this->frame();
771 Ref<Frame> protector(frame);
772
773 frame.editor().updateMarkersForWordsAffectedByEditing(false);
774
775 VisibleSelection selectionToDelete;
776 VisibleSelection selectionAfterUndo;
777 bool expandForSpecialElements = !endingSelection().isCaret();
778
779 switch (endingSelection().selectionType()) {
780 case VisibleSelection::RangeSelection:
781 selectionToDelete = endingSelection();
782 selectionAfterUndo = selectionToDelete;
783 break;
784 case VisibleSelection::CaretSelection: {
785 m_smartDelete = false;
786
787 // Handle delete at beginning-of-block case.
788 // Do nothing in the case that the caret is at the start of a
789 // root editable element or at the start of a document.
790 FrameSelection selection;
791 selection.setSelection(endingSelection());
792 selection.modify(FrameSelection::AlterationExtend, DirectionForward, granularity);
793 if (shouldAddToKillRing && selection.isCaret() && granularity != CharacterGranularity)
794 selection.modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity);
795
796 Position downstreamEnd = endingSelection().end().downstream();
797 VisiblePosition visibleEnd = endingSelection().visibleEnd();
798 Node* enclosingTableCell = enclosingNodeOfType(visibleEnd.deepEquivalent(), &isTableCell);
799 if (enclosingTableCell && visibleEnd == lastPositionInNode(enclosingTableCell))
800 return;
801 if (visibleEnd == endOfParagraph(visibleEnd))
802 downstreamEnd = visibleEnd.next(CannotCrossEditingBoundary).deepEquivalent().downstream();
803 // When deleting tables: Select the table first, then perform the deletion
804 if (downstreamEnd.containerNode() && downstreamEnd.containerNode()->renderer() && downstreamEnd.containerNode()->renderer()->isTable()
805 && downstreamEnd.computeOffsetInContainerNode() <= caretMinOffset(*downstreamEnd.containerNode())) {
806 setEndingSelection(VisibleSelection(endingSelection().end(), positionAfterNode(downstreamEnd.containerNode()), DOWNSTREAM, endingSelection().isDirectional()));
807 typingAddedToOpenCommand(ForwardDeleteKey);
808 return;
809 }
810
811 // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any)
812 if (granularity == ParagraphBoundary && selection.selection().isCaret() && isEndOfParagraph(selection.selection().visibleEnd()))
813 selection.modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity);
814
815 selectionToDelete = selection.selection();
816 if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
817 selectionAfterUndo = selectionToDelete;
818 else {
819 // It's a little tricky to compute what the starting selection would have been in the original document.
820 // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on
821 // the current state of the document and we'll get the wrong result.
822 Position extent = startingSelection().end();
823 if (extent.containerNode() != selectionToDelete.end().containerNode())
824 extent = selectionToDelete.extent();
825 else {
826 int extraCharacters;
827 if (selectionToDelete.start().containerNode() == selectionToDelete.end().containerNode())
828 extraCharacters = selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode();
829 else
830 extraCharacters = selectionToDelete.end().computeOffsetInContainerNode();
831 extent = Position(extent.containerNode(), extent.computeOffsetInContainerNode() + extraCharacters, Position::PositionIsOffsetInAnchor);
832 }
833 selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent);
834 }
835 break;
836 }
837 case VisibleSelection::NoSelection:
838 ASSERT_NOT_REACHED();
839 break;
840 }
841
842 ASSERT(!selectionToDelete.isNone());
843 if (selectionToDelete.isNone()) {
844#if PLATFORM(IOS_FAMILY)
845 // Workaround for this bug:
846 // <rdar://problem/4653755> UIKit text widgets should use WebKit editing API to manipulate text
847 setEndingSelection(frame.selection().selection());
848 closeTyping(&frame);
849#endif
850 return;
851 }
852
853 if (selectionToDelete.isCaret() || !frame.selection().shouldDeleteSelection(selectionToDelete))
854 return;
855
856 if (!willAddTypingToOpenCommand(ForwardDeleteKey, granularity, { }, selectionToDelete.firstRange()))
857 return;
858
859 // Post the accessibility notification before actually deleting the content while selectionToDelete is still valid
860 postTextStateChangeNotificationForDeletion(selectionToDelete);
861
862 if (shouldAddToKillRing)
863 frame.editor().addRangeToKillRing(*selectionToDelete.toNormalizedRange().get(), Editor::KillRingInsertionMode::AppendText);
864 // make undo select what was deleted
865 setStartingSelection(selectionAfterUndo);
866 CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete, /* mergeBlocksAfterDelete*/ true, /* replace*/ false, expandForSpecialElements, /*sanitizeMarkup*/ true);
867 setSmartDelete(false);
868 typingAddedToOpenCommand(ForwardDeleteKey);
869}
870
871void TypingCommand::deleteSelection(bool smartDelete)
872{
873 if (!willAddTypingToOpenCommand(DeleteSelection, CharacterGranularity))
874 return;
875
876 CompositeEditCommand::deleteSelection(smartDelete);
877 typingAddedToOpenCommand(DeleteSelection);
878}
879
880#if PLATFORM(IOS_FAMILY)
881class FriendlyEditCommand : public EditCommand {
882public:
883 void setEndingSelection(const VisibleSelection& selection)
884 {
885 EditCommand::setEndingSelection(selection);
886 }
887};
888
889void TypingCommand::setEndingSelectionOnLastInsertCommand(const VisibleSelection& selection)
890{
891 if (!m_commands.isEmpty()) {
892 EditCommand* lastCommand = m_commands.last().get();
893 if (lastCommand->isInsertTextCommand())
894 static_cast<FriendlyEditCommand*>(lastCommand)->setEndingSelection(selection);
895 }
896}
897#endif
898
899void TypingCommand::updatePreservesTypingStyle(ETypingCommand commandType)
900{
901 switch (commandType) {
902 case DeleteSelection:
903 case DeleteKey:
904 case ForwardDeleteKey:
905 case InsertParagraphSeparator:
906 case InsertLineBreak:
907 m_preservesTypingStyle = true;
908 return;
909 case InsertParagraphSeparatorInQuotedContent:
910 case InsertText:
911 m_preservesTypingStyle = false;
912 return;
913 }
914 ASSERT_NOT_REACHED();
915 m_preservesTypingStyle = false;
916}
917
918bool TypingCommand::isTypingCommand() const
919{
920 return true;
921}
922
923} // namespace WebCore
924