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 | |
51 | namespace WebCore { |
52 | |
53 | using namespace HTMLNames; |
54 | |
55 | class TypingCommandLineOperation |
56 | { |
57 | public: |
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 | |
76 | private: |
77 | TypingCommand* m_typingCommand; |
78 | bool m_selectInsertedText; |
79 | const String& m_text; |
80 | }; |
81 | |
82 | static 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 | |
128 | static 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 | |
144 | TypingCommand::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 | |
164 | void 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 | |
183 | void 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 | |
199 | void 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 | |
217 | void 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 | |
228 | void 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. |
240 | void 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 | |
272 | void 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 | |
285 | void 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 | |
297 | void 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 | |
310 | RefPtr<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 | |
319 | bool TypingCommand::shouldDeferWillApplyCommandUntilAddingTypingCommand() const |
320 | { |
321 | return !m_isHandlingInitialTypingCommand || editActionIsDeleteByTyping(editingAction()); |
322 | } |
323 | |
324 | void TypingCommand::closeTyping(Frame* frame) |
325 | { |
326 | if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(*frame)) |
327 | lastTypingCommand->closeTyping(); |
328 | } |
329 | |
330 | #if PLATFORM(IOS_FAMILY) |
331 | void 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 | |
340 | void 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 | |
351 | bool 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 | |
361 | void 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 | |
397 | String TypingCommand::inputEventTypeName() const |
398 | { |
399 | return inputTypeNameForEditingAction(m_currentTypingEditAction); |
400 | } |
401 | |
402 | bool TypingCommand::isBeforeInputEventCancelable() const |
403 | { |
404 | return m_currentTypingEditAction != EditAction::TypingInsertPendingComposition && m_currentTypingEditAction != EditAction::TypingDeletePendingComposition; |
405 | } |
406 | |
407 | String 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 | |
421 | RefPtr<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 | |
431 | void TypingCommand::didApplyCommand() |
432 | { |
433 | // TypingCommands handle applied editing separately (see TypingCommand::typingAddedToOpenCommand). |
434 | m_isHandlingInitialTypingCommand = false; |
435 | } |
436 | |
437 | void 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 | |
491 | bool 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 | |
505 | void 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 | |
523 | void 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 | |
534 | void 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 | |
544 | void 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 | |
557 | void 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 | |
569 | void 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 | |
577 | void 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 | |
589 | void 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 | |
597 | void 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 | |
613 | void 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 | |
621 | bool 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 | |
642 | void 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 | |
768 | void 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 ; |
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 | |
871 | void 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) |
881 | class FriendlyEditCommand : public EditCommand { |
882 | public: |
883 | void setEndingSelection(const VisibleSelection& selection) |
884 | { |
885 | EditCommand::setEndingSelection(selection); |
886 | } |
887 | }; |
888 | |
889 | void 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 | |
899 | void 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 | |
918 | bool TypingCommand::isTypingCommand() const |
919 | { |
920 | return true; |
921 | } |
922 | |
923 | } // namespace WebCore |
924 | |