903 lines
40 KiB
C++
903 lines
40 KiB
C++
/*
|
|
* Copyright (C) 2005-2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "TypingCommand.h"
|
|
|
|
#include "AXObjectCache.h"
|
|
#include "BreakBlockquoteCommand.h"
|
|
#include "DataTransfer.h"
|
|
#include "DeleteSelectionCommand.h"
|
|
#include "Document.h"
|
|
#include "Editing.h"
|
|
#include "Editor.h"
|
|
#include "Element.h"
|
|
#include "Frame.h"
|
|
#include "HTMLElement.h"
|
|
#include "HTMLNames.h"
|
|
#include "InsertLineBreakCommand.h"
|
|
#include "InsertParagraphSeparatorCommand.h"
|
|
#include "InsertTextCommand.h"
|
|
#include "Logging.h"
|
|
#include "MarkupAccumulator.h"
|
|
#include "MathMLElement.h"
|
|
#include "Range.h"
|
|
#include "RenderElement.h"
|
|
#include "StaticRange.h"
|
|
#include "TextIterator.h"
|
|
#include "VisibleUnits.h"
|
|
|
|
namespace WebCore {
|
|
|
|
using namespace HTMLNames;
|
|
|
|
class TypingCommandLineOperation
|
|
{
|
|
public:
|
|
TypingCommandLineOperation(TypingCommand* typingCommand, bool selectInsertedText, const String& text)
|
|
: m_typingCommand(typingCommand)
|
|
, m_selectInsertedText(selectInsertedText)
|
|
, m_text(text)
|
|
{ }
|
|
|
|
void operator()(size_t lineOffset, size_t lineLength, bool isLastLine) const
|
|
{
|
|
if (isLastLine) {
|
|
if (!lineOffset || lineLength > 0)
|
|
m_typingCommand->insertTextRunWithoutNewlines(m_text.substring(lineOffset, lineLength), m_selectInsertedText);
|
|
} else {
|
|
if (lineLength > 0)
|
|
m_typingCommand->insertTextRunWithoutNewlines(m_text.substring(lineOffset, lineLength), false);
|
|
m_typingCommand->insertParagraphSeparator();
|
|
}
|
|
}
|
|
|
|
private:
|
|
TypingCommand* m_typingCommand;
|
|
bool m_selectInsertedText;
|
|
const String& m_text;
|
|
};
|
|
|
|
static inline EditAction editActionForTypingCommand(TypingCommand::ETypingCommand command, TextGranularity granularity, TypingCommand::TextCompositionType compositionType, bool isAutocompletion)
|
|
{
|
|
if (compositionType == TypingCommand::TextCompositionPending) {
|
|
if (command == TypingCommand::InsertText)
|
|
return EditAction::TypingInsertPendingComposition;
|
|
if (command == TypingCommand::DeleteSelection)
|
|
return EditAction::TypingDeletePendingComposition;
|
|
ASSERT_NOT_REACHED();
|
|
}
|
|
|
|
if (compositionType == TypingCommand::TextCompositionFinal) {
|
|
if (command == TypingCommand::InsertText)
|
|
return EditAction::TypingInsertFinalComposition;
|
|
if (command == TypingCommand::DeleteSelection)
|
|
return EditAction::TypingDeleteFinalComposition;
|
|
ASSERT_NOT_REACHED();
|
|
}
|
|
|
|
switch (command) {
|
|
case TypingCommand::DeleteSelection:
|
|
return EditAction::TypingDeleteSelection;
|
|
case TypingCommand::DeleteKey: {
|
|
if (granularity == TextGranularity::WordGranularity)
|
|
return EditAction::TypingDeleteWordBackward;
|
|
if (granularity == TextGranularity::LineBoundary)
|
|
return EditAction::TypingDeleteLineBackward;
|
|
return EditAction::TypingDeleteBackward;
|
|
}
|
|
case TypingCommand::ForwardDeleteKey:
|
|
if (granularity == TextGranularity::WordGranularity)
|
|
return EditAction::TypingDeleteWordForward;
|
|
if (granularity == TextGranularity::LineBoundary)
|
|
return EditAction::TypingDeleteLineForward;
|
|
return EditAction::TypingDeleteForward;
|
|
case TypingCommand::InsertText:
|
|
return isAutocompletion ? EditAction::InsertReplacement : EditAction::TypingInsertText;
|
|
case TypingCommand::InsertLineBreak:
|
|
return EditAction::TypingInsertLineBreak;
|
|
case TypingCommand::InsertParagraphSeparator:
|
|
case TypingCommand::InsertParagraphSeparatorInQuotedContent:
|
|
return EditAction::TypingInsertParagraph;
|
|
default:
|
|
return EditAction::Unspecified;
|
|
}
|
|
}
|
|
|
|
static inline bool editActionIsDeleteByTyping(EditAction action)
|
|
{
|
|
switch (action) {
|
|
case EditAction::TypingDeleteSelection:
|
|
case EditAction::TypingDeleteBackward:
|
|
case EditAction::TypingDeleteWordBackward:
|
|
case EditAction::TypingDeleteLineBackward:
|
|
case EditAction::TypingDeleteForward:
|
|
case EditAction::TypingDeleteWordForward:
|
|
case EditAction::TypingDeleteLineForward:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
TypingCommand::TypingCommand(Document& document, ETypingCommand commandType, const String &textToInsert, Options options, TextGranularity granularity, TextCompositionType compositionType)
|
|
: TextInsertionBaseCommand(document, editActionForTypingCommand(commandType, granularity, compositionType, options & IsAutocompletion))
|
|
, m_commandType(commandType)
|
|
, m_textToInsert(textToInsert)
|
|
, m_currentTextToInsert(textToInsert)
|
|
, m_openForMoreTyping(true)
|
|
, m_selectInsertedText(options & SelectInsertedText)
|
|
, m_smartDelete(options & SmartDelete)
|
|
, m_granularity(granularity)
|
|
, m_compositionType(compositionType)
|
|
, m_shouldAddToKillRing(options & AddsToKillRing)
|
|
, m_isAutocompletion(options & IsAutocompletion)
|
|
, m_openedByBackwardDelete(false)
|
|
, m_shouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator)
|
|
, m_shouldPreventSpellChecking(options & PreventSpellChecking)
|
|
{
|
|
m_currentTypingEditAction = editingAction();
|
|
updatePreservesTypingStyle(m_commandType);
|
|
}
|
|
|
|
void TypingCommand::deleteSelection(Document& document, Options options, TextCompositionType compositionType)
|
|
{
|
|
if (!document.selection().isRange())
|
|
return;
|
|
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
|
|
lastTypingCommand->setCompositionType(compositionType);
|
|
lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
|
|
lastTypingCommand->deleteSelection(options & SmartDelete);
|
|
return;
|
|
}
|
|
|
|
TypingCommand::create(document, DeleteSelection, emptyString(), options, compositionType)->apply();
|
|
}
|
|
|
|
void TypingCommand::deleteKeyPressed(Document& document, Options options, TextGranularity granularity)
|
|
{
|
|
if (granularity == TextGranularity::CharacterGranularity) {
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand.get(), document);
|
|
lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
|
|
lastTypingCommand->setCompositionType(TextCompositionNone);
|
|
lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
|
|
lastTypingCommand->deleteKeyPressed(granularity, options & AddsToKillRing);
|
|
return;
|
|
}
|
|
}
|
|
|
|
TypingCommand::create(document, DeleteKey, emptyString(), options, granularity)->apply();
|
|
}
|
|
|
|
void TypingCommand::forwardDeleteKeyPressed(Document& document, Options options, TextGranularity granularity)
|
|
{
|
|
// FIXME: Forward delete in TextEdit appears to open and close a new typing command.
|
|
if (granularity == TextGranularity::CharacterGranularity) {
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand.get(), document);
|
|
lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
|
|
lastTypingCommand->setCompositionType(TextCompositionNone);
|
|
lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
|
|
lastTypingCommand->forwardDeleteKeyPressed(granularity, options & AddsToKillRing);
|
|
return;
|
|
}
|
|
}
|
|
|
|
TypingCommand::create(document, ForwardDeleteKey, emptyString(), options, granularity)->apply();
|
|
}
|
|
|
|
void TypingCommand::updateSelectionIfDifferentFromCurrentSelection(TypingCommand* typingCommand, Document& document)
|
|
{
|
|
VisibleSelection currentSelection = document.selection().selection();
|
|
if (currentSelection == typingCommand->endingSelection())
|
|
return;
|
|
|
|
typingCommand->setStartingSelection(currentSelection);
|
|
typingCommand->setEndingSelection(currentSelection);
|
|
}
|
|
|
|
void TypingCommand::insertText(Document& document, const String& text, Options options, TextCompositionType composition)
|
|
{
|
|
if (!text.isEmpty())
|
|
document.editor().updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text[0]));
|
|
|
|
insertText(document, text, document.selection().selection(), options, composition);
|
|
}
|
|
|
|
// FIXME: We shouldn't need to take selectionForInsertion. It should be identical to FrameSelection's current selection.
|
|
void TypingCommand::insertText(Document& document, const String& text, const VisibleSelection& selectionForInsertion, Options options, TextCompositionType compositionType)
|
|
{
|
|
LOG(Editing, "TypingCommand::insertText (text %s)", text.utf8().data());
|
|
|
|
VisibleSelection currentSelection = document.selection().selection();
|
|
|
|
String newText = dispatchBeforeTextInsertedEvent(text, selectionForInsertion, compositionType == TextCompositionPending);
|
|
|
|
// Set the starting and ending selection appropriately if we are using a selection
|
|
// that is different from the current selection. In the future, we should change EditCommand
|
|
// to deal with custom selections in a general way that can be used by all of the commands.
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
if (lastTypingCommand->endingSelection() != selectionForInsertion) {
|
|
lastTypingCommand->setStartingSelection(selectionForInsertion);
|
|
lastTypingCommand->setEndingSelection(selectionForInsertion);
|
|
}
|
|
|
|
lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
|
|
lastTypingCommand->setCompositionType(compositionType);
|
|
lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator);
|
|
lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking);
|
|
lastTypingCommand->insertTextAndNotifyAccessibility(newText, options & SelectInsertedText);
|
|
return;
|
|
}
|
|
|
|
auto cmd = TypingCommand::create(document, InsertText, newText, options, compositionType);
|
|
applyTextInsertionCommand(document.frame(), cmd.get(), selectionForInsertion, currentSelection);
|
|
}
|
|
|
|
void TypingCommand::insertLineBreak(Document& document, Options options)
|
|
{
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
|
|
lastTypingCommand->setCompositionType(TextCompositionNone);
|
|
lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator);
|
|
lastTypingCommand->insertLineBreakAndNotifyAccessibility();
|
|
return;
|
|
}
|
|
|
|
TypingCommand::create(document, InsertLineBreak, emptyString(), options)->apply();
|
|
}
|
|
|
|
void TypingCommand::insertParagraphSeparatorInQuotedContent(Document& document)
|
|
{
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
lastTypingCommand->setIsAutocompletion(false);
|
|
lastTypingCommand->setCompositionType(TextCompositionNone);
|
|
lastTypingCommand->insertParagraphSeparatorInQuotedContentAndNotifyAccessibility();
|
|
return;
|
|
}
|
|
|
|
TypingCommand::create(document, InsertParagraphSeparatorInQuotedContent)->apply();
|
|
}
|
|
|
|
void TypingCommand::insertParagraphSeparator(Document& document, Options options)
|
|
{
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
lastTypingCommand->setIsAutocompletion(options & IsAutocompletion);
|
|
lastTypingCommand->setCompositionType(TextCompositionNone);
|
|
lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator);
|
|
lastTypingCommand->insertParagraphSeparatorAndNotifyAccessibility();
|
|
return;
|
|
}
|
|
|
|
TypingCommand::create(document, InsertParagraphSeparator, emptyString(), options)->apply();
|
|
}
|
|
|
|
RefPtr<TypingCommand> TypingCommand::lastTypingCommandIfStillOpenForTyping(Document& document)
|
|
{
|
|
RefPtr<CompositeEditCommand> lastEditCommand = document.editor().lastEditCommand();
|
|
if (!lastEditCommand || !lastEditCommand->isTypingCommand() || !static_cast<TypingCommand*>(lastEditCommand.get())->isOpenForMoreTyping())
|
|
return nullptr;
|
|
|
|
return static_cast<TypingCommand*>(lastEditCommand.get());
|
|
}
|
|
|
|
bool TypingCommand::shouldDeferWillApplyCommandUntilAddingTypingCommand() const
|
|
{
|
|
return !m_isHandlingInitialTypingCommand || editActionIsDeleteByTyping(editingAction());
|
|
}
|
|
|
|
void TypingCommand::closeTyping(Document& document)
|
|
{
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document))
|
|
lastTypingCommand->closeTyping();
|
|
}
|
|
|
|
#if PLATFORM(IOS_FAMILY)
|
|
void TypingCommand::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping(Document& document, const VisibleSelection& newSelection)
|
|
{
|
|
if (RefPtr<TypingCommand> lastTypingCommand = lastTypingCommandIfStillOpenForTyping(document)) {
|
|
lastTypingCommand->setEndingSelection(newSelection);
|
|
lastTypingCommand->setEndingSelectionOnLastInsertCommand(newSelection);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void TypingCommand::postTextStateChangeNotificationForDeletion(const VisibleSelection& selection)
|
|
{
|
|
if (!AXObjectCache::accessibilityEnabled())
|
|
return;
|
|
postTextStateChangeNotification(AXTextEditTypeDelete, AccessibilityObject::stringForVisiblePositionRange(selection), selection.start());
|
|
VisiblePositionIndexRange range;
|
|
range.startIndex.value = indexForVisiblePosition(selection.visibleStart(), range.startIndex.scope);
|
|
range.endIndex.value = indexForVisiblePosition(selection.visibleEnd(), range.endIndex.scope);
|
|
composition()->setRangeDeletedByUnapply(range);
|
|
}
|
|
|
|
bool TypingCommand::willApplyCommand()
|
|
{
|
|
if (shouldDeferWillApplyCommandUntilAddingTypingCommand()) {
|
|
// The TypingCommand will handle the willApplyCommand logic separately in TypingCommand::willAddTypingToOpenCommand.
|
|
return true;
|
|
}
|
|
|
|
return CompositeEditCommand::willApplyCommand();
|
|
}
|
|
|
|
void TypingCommand::doApply()
|
|
{
|
|
if (endingSelection().isNoneOrOrphaned())
|
|
return;
|
|
|
|
if (m_commandType == DeleteKey)
|
|
if (m_commands.isEmpty())
|
|
m_openedByBackwardDelete = true;
|
|
|
|
switch (m_commandType) {
|
|
case DeleteSelection:
|
|
deleteSelection(m_smartDelete);
|
|
return;
|
|
case DeleteKey:
|
|
deleteKeyPressed(m_granularity, m_shouldAddToKillRing);
|
|
return;
|
|
case ForwardDeleteKey:
|
|
forwardDeleteKeyPressed(m_granularity, m_shouldAddToKillRing);
|
|
return;
|
|
case InsertLineBreak:
|
|
insertLineBreakAndNotifyAccessibility();
|
|
return;
|
|
case InsertParagraphSeparator:
|
|
insertParagraphSeparatorAndNotifyAccessibility();
|
|
return;
|
|
case InsertParagraphSeparatorInQuotedContent:
|
|
insertParagraphSeparatorInQuotedContentAndNotifyAccessibility();
|
|
return;
|
|
case InsertText:
|
|
insertTextAndNotifyAccessibility(m_textToInsert, m_selectInsertedText);
|
|
return;
|
|
}
|
|
|
|
ASSERT_NOT_REACHED();
|
|
}
|
|
|
|
String TypingCommand::inputEventTypeName() const
|
|
{
|
|
return inputTypeNameForEditingAction(m_currentTypingEditAction);
|
|
}
|
|
|
|
bool TypingCommand::isBeforeInputEventCancelable() const
|
|
{
|
|
return m_currentTypingEditAction != EditAction::TypingInsertPendingComposition && m_currentTypingEditAction != EditAction::TypingDeletePendingComposition;
|
|
}
|
|
|
|
String TypingCommand::inputEventData() const
|
|
{
|
|
switch (m_currentTypingEditAction) {
|
|
case EditAction::TypingInsertText:
|
|
case EditAction::TypingInsertPendingComposition:
|
|
case EditAction::TypingInsertFinalComposition:
|
|
return m_currentTextToInsert;
|
|
case EditAction::InsertReplacement:
|
|
return isEditingTextAreaOrTextInput() ? m_currentTextToInsert : String();
|
|
default:
|
|
return CompositeEditCommand::inputEventData();
|
|
}
|
|
}
|
|
|
|
RefPtr<DataTransfer> TypingCommand::inputEventDataTransfer() const
|
|
{
|
|
if (m_currentTypingEditAction != EditAction::InsertReplacement || isEditingTextAreaOrTextInput())
|
|
return nullptr;
|
|
|
|
StringBuilder htmlText;
|
|
MarkupAccumulator::appendCharactersReplacingEntities(htmlText, m_currentTextToInsert, 0, m_currentTextToInsert.length(), EntityMaskInHTMLPCDATA);
|
|
return DataTransfer::createForInputEvent(m_currentTextToInsert, htmlText.toString());
|
|
}
|
|
|
|
void TypingCommand::didApplyCommand()
|
|
{
|
|
// TypingCommands handle applied editing separately (see TypingCommand::typingAddedToOpenCommand).
|
|
m_isHandlingInitialTypingCommand = false;
|
|
}
|
|
|
|
void TypingCommand::markMisspellingsAfterTyping(ETypingCommand commandType)
|
|
{
|
|
#if PLATFORM(MAC)
|
|
if (!document().editor().isContinuousSpellCheckingEnabled()
|
|
&& !document().editor().isAutomaticQuoteSubstitutionEnabled()
|
|
&& !document().editor().isAutomaticLinkDetectionEnabled()
|
|
&& !document().editor().isAutomaticDashSubstitutionEnabled()
|
|
&& !document().editor().isAutomaticTextReplacementEnabled())
|
|
return;
|
|
if (document().editor().isHandlingAcceptedCandidate())
|
|
return;
|
|
#else
|
|
if (!document().editor().isContinuousSpellCheckingEnabled())
|
|
return;
|
|
#endif
|
|
// Take a look at the selection that results after typing and determine whether we need to spellcheck.
|
|
// Since the word containing the current selection is never marked, this does a check to
|
|
// see if typing made a new word that is not in the current selection. Basically, you
|
|
// get this by being at the end of a word and typing a space.
|
|
VisiblePosition start(endingSelection().start(), endingSelection().affinity());
|
|
VisiblePosition previous = start.previous();
|
|
if (previous.isNotNull()) {
|
|
#if !PLATFORM(IOS_FAMILY)
|
|
VisiblePosition p1 = startOfWord(previous, LeftWordIfOnBoundary);
|
|
VisiblePosition p2 = startOfWord(start, LeftWordIfOnBoundary);
|
|
if (p1 != p2) {
|
|
auto range = makeSimpleRange(p1, p2);
|
|
String strippedPreviousWord;
|
|
if (range && (commandType == TypingCommand::InsertText || commandType == TypingCommand::InsertLineBreak || commandType == TypingCommand::InsertParagraphSeparator || commandType == TypingCommand::InsertParagraphSeparatorInQuotedContent))
|
|
strippedPreviousWord = plainText(*range).stripWhiteSpace();
|
|
document().editor().markMisspellingsAfterTypingToWord(p1, endingSelection(), !strippedPreviousWord.isEmpty());
|
|
} else if (commandType == TypingCommand::InsertText)
|
|
document().editor().startAlternativeTextUITimer();
|
|
#else
|
|
UNUSED_PARAM(commandType);
|
|
// If this bug gets fixed, this PLATFORM(IOS_FAMILY) code could be removed:
|
|
// <rdar://problem/7259611> Word boundary code on iPhone gives different results than desktop
|
|
EWordSide startWordSide = LeftWordIfOnBoundary;
|
|
UChar32 c = previous.characterAfter();
|
|
// FIXME: VisiblePosition::characterAfter() and characterBefore() do not emit newlines the same
|
|
// way as TextIterator, so we do an isEndOfParagraph check here.
|
|
if (isSpaceOrNewline(c) || c == noBreakSpace || isEndOfParagraph(previous)) {
|
|
startWordSide = RightWordIfOnBoundary;
|
|
}
|
|
VisiblePosition p1 = startOfWord(previous, startWordSide);
|
|
VisiblePosition p2 = startOfWord(start, startWordSide);
|
|
if (p1 != p2)
|
|
document().editor().markMisspellingsAfterTypingToWord(p1, endingSelection(), false);
|
|
#endif // !PLATFORM(IOS_FAMILY)
|
|
}
|
|
}
|
|
|
|
bool TypingCommand::willAddTypingToOpenCommand(ETypingCommand commandType, TextGranularity granularity, const String& text, const std::optional<SimpleRange>& range)
|
|
{
|
|
m_currentTextToInsert = text;
|
|
m_currentTypingEditAction = editActionForTypingCommand(commandType, granularity, m_compositionType, m_isAutocompletion);
|
|
|
|
if (!shouldDeferWillApplyCommandUntilAddingTypingCommand())
|
|
return true;
|
|
|
|
if (!range || isEditingTextAreaOrTextInput())
|
|
return document().editor().willApplyEditing(*this, CompositeEditCommand::targetRangesForBindings());
|
|
|
|
return document().editor().willApplyEditing(*this, { 1, StaticRange::create(*range) });
|
|
}
|
|
|
|
void TypingCommand::typingAddedToOpenCommand(ETypingCommand commandTypeForAddedTyping)
|
|
{
|
|
RefPtr<Frame> protector(document().frame());
|
|
|
|
updatePreservesTypingStyle(commandTypeForAddedTyping);
|
|
|
|
#if PLATFORM(COCOA)
|
|
document().editor().appliedEditing(*this);
|
|
// Since the spellchecking code may also perform corrections and other replacements, it should happen after the typing changes.
|
|
if (!m_shouldPreventSpellChecking)
|
|
markMisspellingsAfterTyping(commandTypeForAddedTyping);
|
|
#else
|
|
// The old spellchecking code requires that checking be done first, to prevent issues like that in 6864072, where <doesn't> is marked as misspelled.
|
|
markMisspellingsAfterTyping(commandTypeForAddedTyping);
|
|
document().editor().appliedEditing(*this);
|
|
#endif
|
|
}
|
|
|
|
void TypingCommand::insertText(const String &text, bool selectInsertedText)
|
|
{
|
|
// FIXME: Need to implement selectInsertedText for cases where more than one insert is involved.
|
|
// This requires support from insertTextRunWithoutNewlines and insertParagraphSeparator for extending
|
|
// an existing selection; at the moment they can either put the caret after what's inserted or
|
|
// select what's inserted, but there's no way to "extend selection" to include both an old selection
|
|
// that ends just before where we want to insert text and the newly inserted text.
|
|
TypingCommandLineOperation operation(this, selectInsertedText, text);
|
|
forEachLineInString(text, operation);
|
|
}
|
|
|
|
void TypingCommand::insertTextAndNotifyAccessibility(const String &text, bool selectInsertedText)
|
|
{
|
|
LOG(Editing, "TypingCommand %p insertTextAndNotifyAccessibility (text %s, selectInsertedText %d)", this, text.utf8().data(), selectInsertedText);
|
|
|
|
AccessibilityReplacedText replacedText(document().selection().selection());
|
|
insertText(text, selectInsertedText);
|
|
replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, text, document().selection().selection());
|
|
composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
|
|
}
|
|
|
|
void TypingCommand::insertTextRunWithoutNewlines(const String &text, bool selectInsertedText)
|
|
{
|
|
if (!willAddTypingToOpenCommand(InsertText, TextGranularity::CharacterGranularity, text))
|
|
return;
|
|
|
|
auto command = InsertTextCommand::create(document(), text, selectInsertedText,
|
|
m_compositionType == TextCompositionNone ? InsertTextCommand::RebalanceLeadingAndTrailingWhitespaces : InsertTextCommand::RebalanceAllWhitespaces, EditAction::TypingInsertText);
|
|
|
|
applyCommandToComposite(WTFMove(command), endingSelection());
|
|
typingAddedToOpenCommand(InsertText);
|
|
}
|
|
|
|
void TypingCommand::insertLineBreak()
|
|
{
|
|
if (!canAppendNewLineFeedToSelection(endingSelection()))
|
|
return;
|
|
|
|
if (!willAddTypingToOpenCommand(InsertLineBreak, TextGranularity::LineGranularity))
|
|
return;
|
|
|
|
applyCommandToComposite(InsertLineBreakCommand::create(document()));
|
|
typingAddedToOpenCommand(InsertLineBreak);
|
|
}
|
|
|
|
void TypingCommand::insertLineBreakAndNotifyAccessibility()
|
|
{
|
|
AccessibilityReplacedText replacedText(document().selection().selection());
|
|
insertLineBreak();
|
|
replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, "\n", document().selection().selection());
|
|
composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
|
|
}
|
|
|
|
void TypingCommand::insertParagraphSeparator()
|
|
{
|
|
if (!canAppendNewLineFeedToSelection(endingSelection()))
|
|
return;
|
|
|
|
if (!willAddTypingToOpenCommand(InsertParagraphSeparator, TextGranularity::ParagraphGranularity))
|
|
return;
|
|
|
|
applyCommandToComposite(InsertParagraphSeparatorCommand::create(document(), false, false, EditAction::TypingInsertParagraph));
|
|
typingAddedToOpenCommand(InsertParagraphSeparator);
|
|
}
|
|
|
|
void TypingCommand::insertParagraphSeparatorAndNotifyAccessibility()
|
|
{
|
|
AccessibilityReplacedText replacedText(document().selection().selection());
|
|
insertParagraphSeparator();
|
|
replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, "\n", document().selection().selection());
|
|
composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
|
|
}
|
|
|
|
void TypingCommand::insertParagraphSeparatorInQuotedContent()
|
|
{
|
|
if (!willAddTypingToOpenCommand(InsertParagraphSeparatorInQuotedContent, TextGranularity::ParagraphGranularity))
|
|
return;
|
|
|
|
// If the selection starts inside a table, just insert the paragraph separator normally
|
|
// Breaking the blockquote would also break apart the table, which is unecessary when inserting a newline
|
|
if (enclosingNodeOfType(endingSelection().start(), &isTableStructureNode)) {
|
|
insertParagraphSeparator();
|
|
return;
|
|
}
|
|
|
|
applyCommandToComposite(BreakBlockquoteCommand::create(document()));
|
|
typingAddedToOpenCommand(InsertParagraphSeparatorInQuotedContent);
|
|
}
|
|
|
|
void TypingCommand::insertParagraphSeparatorInQuotedContentAndNotifyAccessibility()
|
|
{
|
|
AccessibilityReplacedText replacedText(document().selection().selection());
|
|
insertParagraphSeparatorInQuotedContent();
|
|
replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypeTyping, "\n", document().selection().selection());
|
|
composition()->setRangeDeletedByUnapply(replacedText.replacedRange());
|
|
}
|
|
|
|
bool TypingCommand::makeEditableRootEmpty()
|
|
{
|
|
Element* root = endingSelection().rootEditableElement();
|
|
if (!root || !root->firstChild())
|
|
return false;
|
|
|
|
if (root->firstChild() == root->lastChild() && root->firstElementChild() && root->firstElementChild()->hasTagName(brTag)) {
|
|
// If there is a single child and it could be a placeholder, leave it alone.
|
|
if (root->renderer() && root->renderer()->isRenderBlockFlow())
|
|
return false;
|
|
}
|
|
|
|
while (Node* child = root->firstChild())
|
|
removeNode(*child);
|
|
|
|
addBlockPlaceholderIfNeeded(root);
|
|
setEndingSelection(VisibleSelection(firstPositionInNode(root), Affinity::Downstream, endingSelection().isDirectional()));
|
|
|
|
return true;
|
|
}
|
|
|
|
void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool shouldAddToKillRing)
|
|
{
|
|
RefPtr<Frame> protector(document().frame());
|
|
|
|
document().editor().updateMarkersForWordsAffectedByEditing(false);
|
|
|
|
VisibleSelection selectionToDelete;
|
|
VisibleSelection selectionAfterUndo;
|
|
bool expandForSpecialElements = false;
|
|
|
|
ASSERT(endingSelection().isCaretOrRange());
|
|
|
|
if (endingSelection().isRange()) {
|
|
selectionToDelete = endingSelection();
|
|
selectionAfterUndo = selectionToDelete;
|
|
expandForSpecialElements = true;
|
|
} else {
|
|
// After breaking out of an empty mail blockquote, we still want continue with the deletion
|
|
// so actual content will get deleted, and not just the quote style.
|
|
if (breakOutOfEmptyMailBlockquotedParagraph())
|
|
typingAddedToOpenCommand(DeleteKey);
|
|
|
|
m_smartDelete = false;
|
|
|
|
FrameSelection selection;
|
|
selection.setSelection(endingSelection());
|
|
selection.modify(FrameSelection::AlterationExtend, SelectionDirection::Backward, granularity);
|
|
if (shouldAddToKillRing && selection.isCaret() && granularity != TextGranularity::CharacterGranularity)
|
|
selection.modify(FrameSelection::AlterationExtend, SelectionDirection::Backward, TextGranularity::CharacterGranularity);
|
|
|
|
const VisiblePosition& visibleStart = endingSelection().visibleStart();
|
|
const VisiblePosition& previousPosition = visibleStart.previous(CannotCrossEditingBoundary);
|
|
Node* enclosingTableCell = enclosingNodeOfType(visibleStart.deepEquivalent(), &isTableCell);
|
|
const Node* enclosingTableCellForPreviousPosition = enclosingNodeOfType(previousPosition.deepEquivalent(), &isTableCell);
|
|
if (previousPosition.isNull() || enclosingTableCell != enclosingTableCellForPreviousPosition) {
|
|
// When the caret is at the start of the editable area in an empty list item, break out of the list item.
|
|
if (auto deleteListSelection = shouldBreakOutOfEmptyListItem()) {
|
|
if (willAddTypingToOpenCommand(DeleteKey, granularity, { }, *deleteListSelection.value().firstRange())) {
|
|
breakOutOfEmptyListItem();
|
|
typingAddedToOpenCommand(DeleteKey);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (previousPosition.isNull()) {
|
|
// When there are no visible positions in the editing root, delete its entire contents.
|
|
// FIXME: Dispatch a `beforeinput` event here and bail if preventDefault() was invoked.
|
|
if (visibleStart.next(CannotCrossEditingBoundary).isNull() && makeEditableRootEmpty()) {
|
|
typingAddedToOpenCommand(DeleteKey);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If we have a caret selection at the beginning of a cell, we have nothing to do.
|
|
if (enclosingTableCell && visibleStart == firstPositionInNode(enclosingTableCell))
|
|
return;
|
|
|
|
// If the caret is at the start of a paragraph after a table, move content into the last table cell.
|
|
if (isStartOfParagraph(visibleStart) && isFirstPositionAfterTable(visibleStart.previous(CannotCrossEditingBoundary))) {
|
|
// Unless the caret is just before a table. We don't want to move a table into the last table cell.
|
|
if (isLastPositionBeforeTable(visibleStart))
|
|
return;
|
|
// Extend the selection backward into the last cell, then deletion will handle the move.
|
|
selection.modify(FrameSelection::AlterationExtend, SelectionDirection::Backward, granularity);
|
|
// If the caret is just after a table, select the table and don't delete anything.
|
|
} else if (Node* table = isFirstPositionAfterTable(visibleStart)) {
|
|
setEndingSelection(VisibleSelection(positionBeforeNode(table), endingSelection().start(), Affinity::Downstream, endingSelection().isDirectional()));
|
|
typingAddedToOpenCommand(DeleteKey);
|
|
return;
|
|
}
|
|
|
|
selectionToDelete = selection.selection();
|
|
|
|
if (granularity == TextGranularity::CharacterGranularity && selectionToDelete.end().containerNode() == selectionToDelete.start().containerNode()
|
|
&& selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode() > 1) {
|
|
// If there are multiple Unicode code points to be deleted, adjust the range to match platform conventions.
|
|
selectionToDelete.setWithoutValidation(selectionToDelete.end(), selectionToDelete.end().previous(BackwardDeletion));
|
|
}
|
|
|
|
if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
|
|
selectionAfterUndo = selectionToDelete;
|
|
else
|
|
// It's a little tricky to compute what the starting selection would have been in the original document.
|
|
// We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on
|
|
// the current state of the document and we'll get the wrong result.
|
|
selectionAfterUndo.setWithoutValidation(startingSelection().end(), selectionToDelete.extent());
|
|
}
|
|
|
|
ASSERT(!selectionToDelete.isNone());
|
|
if (selectionToDelete.isNone()) {
|
|
#if PLATFORM(IOS_FAMILY)
|
|
// Workaround for this bug:
|
|
// <rdar://problem/4653755> UIKit text widgets should use WebKit editing API to manipulate text
|
|
setEndingSelection(document().selection().selection());
|
|
closeTyping(document());
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if (selectionToDelete.isCaret() || !document().selection().shouldDeleteSelection(selectionToDelete))
|
|
return;
|
|
|
|
if (!willAddTypingToOpenCommand(DeleteKey, granularity, { }, selectionToDelete.firstRange()))
|
|
return;
|
|
|
|
if (shouldAddToKillRing)
|
|
document().editor().addRangeToKillRing(*selectionToDelete.toNormalizedRange(), Editor::KillRingInsertionMode::PrependText);
|
|
|
|
// Post the accessibility notification before actually deleting the content while selectionToDelete is still valid
|
|
postTextStateChangeNotificationForDeletion(selectionToDelete);
|
|
|
|
// Make undo select everything that has been deleted, unless an undo will undo more than just this deletion.
|
|
// FIXME: This behaves like TextEdit except for the case where you open with text insertion and then delete
|
|
// more text than you insert. In that case all of the text that was around originally should be selected.
|
|
if (m_openedByBackwardDelete)
|
|
setStartingSelection(selectionAfterUndo);
|
|
CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete, /* mergeBlocksAfterDelete*/ true, /* replace*/ false, expandForSpecialElements, /*sanitizeMarkup*/ true);
|
|
setSmartDelete(false);
|
|
typingAddedToOpenCommand(DeleteKey);
|
|
}
|
|
|
|
void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool shouldAddToKillRing)
|
|
{
|
|
RefPtr<Frame> protector(document().frame());
|
|
|
|
document().editor().updateMarkersForWordsAffectedByEditing(false);
|
|
|
|
VisibleSelection selectionToDelete;
|
|
VisibleSelection selectionAfterUndo;
|
|
bool expandForSpecialElements = false;
|
|
|
|
ASSERT(endingSelection().isCaretOrRange());
|
|
|
|
if (endingSelection().isRange()) {
|
|
selectionToDelete = endingSelection();
|
|
selectionAfterUndo = selectionToDelete;
|
|
expandForSpecialElements = true;
|
|
} else {
|
|
m_smartDelete = false;
|
|
|
|
// Handle delete at beginning-of-block case.
|
|
// Do nothing in the case that the caret is at the start of a
|
|
// root editable element or at the start of a document.
|
|
FrameSelection selection;
|
|
selection.setSelection(endingSelection());
|
|
selection.modify(FrameSelection::AlterationExtend, SelectionDirection::Forward, granularity);
|
|
if (selection.isNone())
|
|
return;
|
|
if (shouldAddToKillRing && selection.isCaret() && granularity != TextGranularity::CharacterGranularity)
|
|
selection.modify(FrameSelection::AlterationExtend, SelectionDirection::Forward, TextGranularity::CharacterGranularity);
|
|
|
|
Position downstreamEnd = endingSelection().end().downstream();
|
|
VisiblePosition visibleEnd = endingSelection().visibleEnd();
|
|
Node* enclosingTableCell = enclosingNodeOfType(visibleEnd.deepEquivalent(), &isTableCell);
|
|
if (enclosingTableCell && visibleEnd == lastPositionInNode(enclosingTableCell))
|
|
return;
|
|
if (visibleEnd == endOfParagraph(visibleEnd))
|
|
downstreamEnd = visibleEnd.next(CannotCrossEditingBoundary).deepEquivalent().downstream();
|
|
// When deleting tables: Select the table first, then perform the deletion
|
|
if (downstreamEnd.containerNode() && downstreamEnd.containerNode()->renderer() && downstreamEnd.containerNode()->renderer()->isTable()
|
|
&& downstreamEnd.computeOffsetInContainerNode() <= caretMinOffset(*downstreamEnd.containerNode())) {
|
|
setEndingSelection(VisibleSelection(endingSelection().end(), positionAfterNode(downstreamEnd.containerNode()), Affinity::Downstream, endingSelection().isDirectional()));
|
|
typingAddedToOpenCommand(ForwardDeleteKey);
|
|
return;
|
|
}
|
|
|
|
// deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any)
|
|
if (granularity == TextGranularity::ParagraphBoundary && selection.selection().isCaret() && isEndOfParagraph(selection.selection().visibleEnd()))
|
|
selection.modify(FrameSelection::AlterationExtend, SelectionDirection::Forward, TextGranularity::CharacterGranularity);
|
|
|
|
selectionToDelete = selection.selection();
|
|
if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
|
|
selectionAfterUndo = selectionToDelete;
|
|
else {
|
|
// It's a little tricky to compute what the starting selection would have been in the original document.
|
|
// We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on
|
|
// the current state of the document and we'll get the wrong result.
|
|
Position extent = startingSelection().end();
|
|
if (extent.containerNode() != selectionToDelete.end().containerNode())
|
|
extent = selectionToDelete.extent();
|
|
else {
|
|
int extraCharacters;
|
|
if (selectionToDelete.start().containerNode() == selectionToDelete.end().containerNode())
|
|
extraCharacters = selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode();
|
|
else
|
|
extraCharacters = selectionToDelete.end().computeOffsetInContainerNode();
|
|
extent = Position(extent.containerNode(), extent.computeOffsetInContainerNode() + extraCharacters, Position::PositionIsOffsetInAnchor);
|
|
}
|
|
selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent);
|
|
}
|
|
}
|
|
|
|
ASSERT(!selectionToDelete.isNone());
|
|
if (selectionToDelete.isNone()) {
|
|
#if PLATFORM(IOS_FAMILY)
|
|
// Workaround for this bug:
|
|
// <rdar://problem/4653755> UIKit text widgets should use WebKit editing API to manipulate text
|
|
setEndingSelection(document().selection().selection());
|
|
closeTyping(document());
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if (selectionToDelete.isCaret() || !document().selection().shouldDeleteSelection(selectionToDelete))
|
|
return;
|
|
|
|
if (!willAddTypingToOpenCommand(ForwardDeleteKey, granularity, { }, selectionToDelete.firstRange()))
|
|
return;
|
|
|
|
// Post the accessibility notification before actually deleting the content while selectionToDelete is still valid
|
|
postTextStateChangeNotificationForDeletion(selectionToDelete);
|
|
|
|
if (shouldAddToKillRing)
|
|
document().editor().addRangeToKillRing(*selectionToDelete.toNormalizedRange(), Editor::KillRingInsertionMode::AppendText);
|
|
// make undo select what was deleted
|
|
setStartingSelection(selectionAfterUndo);
|
|
CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete, /* mergeBlocksAfterDelete*/ true, /* replace*/ false, expandForSpecialElements, /*sanitizeMarkup*/ true);
|
|
setSmartDelete(false);
|
|
typingAddedToOpenCommand(ForwardDeleteKey);
|
|
}
|
|
|
|
void TypingCommand::deleteSelection(bool smartDelete)
|
|
{
|
|
if (!willAddTypingToOpenCommand(DeleteSelection, TextGranularity::CharacterGranularity))
|
|
return;
|
|
|
|
CompositeEditCommand::deleteSelection(smartDelete);
|
|
typingAddedToOpenCommand(DeleteSelection);
|
|
}
|
|
|
|
#if PLATFORM(IOS_FAMILY)
|
|
class FriendlyEditCommand : public EditCommand {
|
|
public:
|
|
void setEndingSelection(const VisibleSelection& selection)
|
|
{
|
|
EditCommand::setEndingSelection(selection);
|
|
}
|
|
};
|
|
|
|
void TypingCommand::setEndingSelectionOnLastInsertCommand(const VisibleSelection& selection)
|
|
{
|
|
if (!m_commands.isEmpty()) {
|
|
EditCommand* lastCommand = m_commands.last().get();
|
|
if (lastCommand->isInsertTextCommand())
|
|
static_cast<FriendlyEditCommand*>(lastCommand)->setEndingSelection(selection);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void TypingCommand::updatePreservesTypingStyle(ETypingCommand commandType)
|
|
{
|
|
switch (commandType) {
|
|
case DeleteSelection:
|
|
case DeleteKey:
|
|
case ForwardDeleteKey:
|
|
case InsertParagraphSeparator:
|
|
case InsertLineBreak:
|
|
m_preservesTypingStyle = true;
|
|
return;
|
|
case InsertParagraphSeparatorInQuotedContent:
|
|
case InsertText:
|
|
m_preservesTypingStyle = false;
|
|
return;
|
|
}
|
|
ASSERT_NOT_REACHED();
|
|
m_preservesTypingStyle = false;
|
|
}
|
|
|
|
bool TypingCommand::isTypingCommand() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
} // namespace WebCore
|