diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebCore/editing/Editor.cpp | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-master.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebCore/editing/Editor.cpp')
-rw-r--r-- | Source/WebCore/editing/Editor.cpp | 1775 |
1 files changed, 1027 insertions, 748 deletions
diff --git a/Source/WebCore/editing/Editor.cpp b/Source/WebCore/editing/Editor.cpp index e599dbd8b..458c33bf0 100644 --- a/Source/WebCore/editing/Editor.cpp +++ b/Source/WebCore/editing/Editor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006, 2007, 2008, 2011, 2013 Apple Inc. All rights reserved. + * Copyright (C) 2006-2016 Apple Inc. All rights reserved. * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) * * Redistribution and use in source and binary forms, with or without @@ -11,10 +11,10 @@ * 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 COMPUTER, INC. ``AS IS'' AND ANY + * 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 COMPUTER, INC. OR + * 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 @@ -33,10 +33,10 @@ #include "CSSComputedStyleDeclaration.h" #include "CSSPropertyNames.h" #include "CachedResourceLoader.h" -#include "Clipboard.h" #include "ClipboardEvent.h" #include "CompositionEvent.h" #include "CreateLinkCommand.h" +#include "DataTransfer.h" #include "DeleteSelectionCommand.h" #include "DictationAlternative.h" #include "DictationCommand.h" @@ -45,31 +45,39 @@ #include "EditorClient.h" #include "EventHandler.h" #include "EventNames.h" -#include "ExceptionCodePlaceholder.h" #include "FocusController.h" #include "Frame.h" #include "FrameTree.h" #include "FrameView.h" #include "GraphicsContext.h" +#include "HTMLCollection.h" #include "HTMLFormControlElement.h" #include "HTMLFrameOwnerElement.h" #include "HTMLImageElement.h" +#include "HTMLInputElement.h" #include "HTMLNames.h" -#include "HTMLTextAreaElement.h" +#include "HTMLSpanElement.h" #include "HitTestResult.h" #include "IndentOutdentCommand.h" +#include "InputEvent.h" #include "InsertListCommand.h" +#include "InsertTextCommand.h" #include "KeyboardEvent.h" #include "KillRing.h" +#include "Logging.h" +#include "MainFrame.h" #include "ModifySelectionListLevel.h" #include "NodeList.h" #include "NodeTraversal.h" #include "Page.h" #include "Pasteboard.h" +#include "Range.h" #include "RemoveFormatCommand.h" #include "RenderBlock.h" #include "RenderTextControl.h" +#include "RenderedDocumentMarker.h" #include "RenderedPosition.h" +#include "ReplaceRangeWithTextCommand.h" #include "ReplaceSelectionCommand.h" #include "Settings.h" #include "ShadowRoot.h" @@ -78,6 +86,7 @@ #include "SpellChecker.h" #include "SpellingCorrectionCommand.h" #include "StyleProperties.h" +#include "TelephoneNumberDetector.h" #include "Text.h" #include "TextCheckerClient.h" #include "TextCheckingHelper.h" @@ -89,28 +98,60 @@ #include "htmlediting.h" #include "markup.h" #include <wtf/unicode/CharacterNames.h> -#include <wtf/unicode/Unicode.h> -#if ENABLE(DELETION_UI) -#include "DeleteButtonController.h" -#endif - -#if PLATFORM(IOS) -#include "DictationCommandIOS.h" -#include <wtf/text/StringBuilder.h> -#include <wtf/text/WTFString.h> +#if PLATFORM(MAC) +#include "ServicesOverlayController.h" #endif namespace WebCore { -#if PLATFORM(IOS) +static bool dispatchBeforeInputEvent(Element& element, const AtomicString& inputType, const String& data = { }, RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<RefPtr<StaticRange>>& targetRanges = { }, bool cancelable = true) +{ + if (!element.document().settings().inputEventsEnabled()) + return true; + + return element.dispatchEvent(InputEvent::create(eventNames().beforeinputEvent, inputType, true, cancelable, element.document().defaultView(), data, WTFMove(dataTransfer), targetRanges, 0)); +} + +static void dispatchInputEvent(Element& element, const AtomicString& inputType, const String& data = { }, RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<RefPtr<StaticRange>>& targetRanges = { }) +{ + if (element.document().settings().inputEventsEnabled()) { + // FIXME: We should not be dispatching to the scoped queue here. Normally, input events are dispatched in CompositeEditCommand::apply after the end of the scope, + // but TypingCommands are special in that existing TypingCommands that are applied again fire input events *from within* the scope by calling typingAddedToOpenCommand. + // Instead, TypingCommands should always dispatch events synchronously after the end of the scoped queue in CompositeEditCommand::apply. To work around this for the + // time being, just revert back to calling dispatchScopedEvent. + element.dispatchScopedEvent(InputEvent::create(eventNames().inputEvent, inputType, true, false, element.document().defaultView(), data, WTFMove(dataTransfer), targetRanges, 0)); + } else + element.dispatchInputEvent(); +} + +static String inputEventDataForEditingStyleAndAction(const StyleProperties* style, EditAction action) +{ + if (!style) + return { }; + + switch (action) { + case EditActionSetColor: + return style->getPropertyValue(CSSPropertyColor); + case EditActionSetWritingDirection: + return style->getPropertyValue(CSSPropertyDirection); + default: + return { }; + } +} + +static String inputEventDataForEditingStyleAndAction(EditingStyle& style, EditAction action) +{ + return inputEventDataForEditingStyleAndAction(style.style(), action); +} + class ClearTextCommand : public DeleteSelectionCommand { public: ClearTextCommand(Document& document); static void CreateAndApply(const RefPtr<Frame> frame); private: - virtual EditAction editingAction() const; + EditAction editingAction() const override; }; ClearTextCommand::ClearTextCommand(Document& document) @@ -133,73 +174,15 @@ void ClearTextCommand::CreateAndApply(const RefPtr<Frame> frame) const VisibleSelection oldSelection = frame->selection().selection(); frame->selection().selectAll(); - RefPtr<ClearTextCommand> clearCommand = adoptRef(new ClearTextCommand(*frame->document())); + auto clearCommand = adoptRef(*new ClearTextCommand(*frame->document())); clearCommand->setStartingSelection(oldSelection); - applyCommand(clearCommand.release()); + applyCommand(WTFMove(clearCommand)); } -#endif using namespace HTMLNames; using namespace WTF; using namespace Unicode; -#if ENABLE(DELETION_UI) - -PassRefPtr<Range> Editor::avoidIntersectionWithDeleteButtonController(const Range* range) const -{ - if (!range) - return 0; - - Node* startContainer = range->startContainer(); - int startOffset = range->startOffset(); - Node* endContainer = range->endContainer(); - int endOffset = range->endOffset(); - - if (!startContainer) - return 0; - - ASSERT(endContainer); - - Element* element = m_deleteButtonController->containerElement(); - if (startContainer == element || startContainer->isDescendantOf(element)) { - ASSERT(element->parentNode()); - startContainer = element->parentNode(); - startOffset = element->nodeIndex(); - } - if (endContainer == element || endContainer->isDescendantOf(element)) { - ASSERT(element->parentNode()); - endContainer = element->parentNode(); - endOffset = element->nodeIndex(); - } - - return Range::create(range->ownerDocument(), startContainer, startOffset, endContainer, endOffset); -} - -VisibleSelection Editor::avoidIntersectionWithDeleteButtonController(const VisibleSelection& selection) const -{ - if (selection.isNone()) - return selection; - - Element* element = m_deleteButtonController->containerElement(); - if (!element) - return selection; - VisibleSelection updatedSelection = selection; - - Position updatedBase = selection.base(); - updatePositionForNodeRemoval(updatedBase, element); - if (updatedBase != selection.base()) - updatedSelection.setBase(updatedBase); - - Position updatedExtent = selection.extent(); - updatePositionForNodeRemoval(updatedExtent, element); - if (updatedExtent != selection.extent()) - updatedSelection.setExtent(updatedExtent); - - return updatedSelection; -} - -#endif - // When an event handler has moved the selection outside of a text control // we should use the target control's selection for this editing operation. VisibleSelection Editor::selectionForCommand(Event* event) @@ -210,10 +193,10 @@ VisibleSelection Editor::selectionForCommand(Event* event) // If the target is a text control, and the current selection is outside of its shadow tree, // then use the saved selection for that text control. HTMLTextFormControlElement* textFormControlOfSelectionStart = enclosingTextFormControl(selection.start()); - HTMLTextFormControlElement* textFromControlOfTarget = isHTMLTextFormControlElement(*event->target()->toNode()) ? toHTMLTextFormControlElement(event->target()->toNode()) : nullptr; + HTMLTextFormControlElement* textFromControlOfTarget = is<HTMLTextFormControlElement>(*event->target()->toNode()) ? downcast<HTMLTextFormControlElement>(event->target()->toNode()) : nullptr; if (textFromControlOfTarget && (selection.start().isNull() || textFromControlOfTarget != textFormControlOfSelectionStart)) { if (RefPtr<Range> range = textFromControlOfTarget->selection()) - return VisibleSelection(range.get(), DOWNSTREAM, selection.isDirectional()); + return VisibleSelection(*range, DOWNSTREAM, selection.isDirectional()); } return selection; } @@ -227,11 +210,10 @@ EditingBehavior Editor::behavior() const EditorClient* Editor::client() const { if (Page* page = m_frame.page()) - return page->editorClient(); - return 0; + return &page->editorClient(); + return nullptr; } - TextCheckerClient* Editor::textChecker() const { if (EditorClient* owner = client()) @@ -239,59 +221,57 @@ TextCheckerClient* Editor::textChecker() const return 0; } -void Editor::handleKeyboardEvent(KeyboardEvent* event) +void Editor::handleKeyboardEvent(KeyboardEvent& event) { if (EditorClient* c = client()) - c->handleKeyboardEvent(event); + c->handleKeyboardEvent(&event); } -void Editor::handleInputMethodKeydown(KeyboardEvent* event) +void Editor::handleInputMethodKeydown(KeyboardEvent& event) { if (EditorClient* c = client()) - c->handleInputMethodKeydown(event); + c->handleInputMethodKeydown(&event); } -bool Editor::handleTextEvent(TextEvent* event) +bool Editor::handleTextEvent(TextEvent& event) { + LOG(Editing, "Editor %p handleTextEvent (data %s)", this, event.data().utf8().data()); + // Default event handling for Drag and Drop will be handled by DragController // so we leave the event for it. - if (event->isDrop()) + if (event.isDrop()) return false; - if (event->isPaste()) { - if (event->pastingFragment()) + if (event.isPaste()) { + if (event.pastingFragment()) { #if PLATFORM(IOS) - { - if (client()->performsTwoStepPaste(event->pastingFragment())) + if (client()->performsTwoStepPaste(event.pastingFragment())) return true; #endif - replaceSelectionWithFragment(event->pastingFragment(), false, event->shouldSmartReplace(), event->shouldMatchStyle()); -#if PLATFORM(IOS) - } -#endif - else - replaceSelectionWithText(event->data(), false, event->shouldSmartReplace()); + replaceSelectionWithFragment(event.pastingFragment(), false, event.shouldSmartReplace(), event.shouldMatchStyle(), EditActionPaste, event.mailBlockquoteHandling()); + } else + replaceSelectionWithText(event.data(), false, event.shouldSmartReplace(), EditActionPaste); return true; } - String data = event->data(); + String data = event.data(); if (data == "\n") { - if (event->isLineBreak()) + if (event.isLineBreak()) return insertLineBreak(); return insertParagraphSeparator(); } - return insertTextWithoutSendingTextEvent(data, false, event); + return insertTextWithoutSendingTextEvent(data, false, &event); } bool Editor::canEdit() const { - return m_frame.selection().rootEditableElement(); + return m_frame.selection().selection().rootEditableElement(); } bool Editor::canEditRichly() const { - return m_frame.selection().isContentRichlyEditable(); + return m_frame.selection().selection().isContentRichlyEditable(); } // WinIE uses onbeforecut and onbeforepaste to enables the cut and paste menu items. They @@ -301,17 +281,17 @@ bool Editor::canEditRichly() const bool Editor::canDHTMLCut() { - return !m_frame.selection().isInPasswordField() && !dispatchCPPEvent(eventNames().beforecutEvent, ClipboardNumb); + return !m_frame.selection().selection().isInPasswordField() && !dispatchCPPEvent(eventNames().beforecutEvent, DataTransferAccessPolicy::Numb); } bool Editor::canDHTMLCopy() { - return !m_frame.selection().isInPasswordField() && !dispatchCPPEvent(eventNames().beforecopyEvent, ClipboardNumb); + return !m_frame.selection().selection().isInPasswordField() && !dispatchCPPEvent(eventNames().beforecopyEvent, DataTransferAccessPolicy::Numb); } bool Editor::canDHTMLPaste() { - return !dispatchCPPEvent(eventNames().beforepasteEvent, ClipboardNumb); + return !dispatchCPPEvent(eventNames().beforepasteEvent, DataTransferAccessPolicy::Numb); } bool Editor::canCut() const @@ -322,25 +302,23 @@ bool Editor::canCut() const static HTMLImageElement* imageElementFromImageDocument(Document& document) { if (!document.isImageDocument()) - return 0; + return nullptr; - HTMLElement* body = document.body(); + HTMLElement* body = document.bodyOrFrameset(); if (!body) - return 0; + return nullptr; Node* node = body->firstChild(); - if (!node) - return 0; - if (!isHTMLImageElement(node)) - return 0; - return toHTMLImageElement(node); + if (!is<HTMLImageElement>(node)) + return nullptr; + return downcast<HTMLImageElement>(node); } bool Editor::canCopy() const { if (imageElementFromImageDocument(document())) return true; - FrameSelection& selection = m_frame.selection(); + const VisibleSelection& selection = m_frame.selection().selection(); return selection.isRange() && !selection.isInPasswordField(); } @@ -351,25 +329,23 @@ bool Editor::canPaste() const bool Editor::canDelete() const { - FrameSelection& selection = m_frame.selection(); + const VisibleSelection& selection = m_frame.selection().selection(); return selection.isRange() && selection.rootEditableElement(); } bool Editor::canDeleteRange(Range* range) const { - Node* startContainer = range->startContainer(); - Node* endContainer = range->endContainer(); - if (!startContainer || !endContainer) - return false; + Node& startContainer = range->startContainer(); + Node& endContainer = range->endContainer(); - if (!startContainer->hasEditableStyle() || !endContainer->hasEditableStyle()) + if (!startContainer.hasEditableStyle() || !endContainer.hasEditableStyle()) return false; - if (range->collapsed(IGNORE_EXCEPTION)) { + if (range->collapsed()) { VisiblePosition start(range->startPosition(), DOWNSTREAM); VisiblePosition previous = start.previous(); // FIXME: We sometimes allow deletions at the start of editable roots, like when the caret is in an empty list item. - if (previous.isNull() || previous.deepEquivalent().deprecatedNode()->rootEditableElement() != startContainer->rootEditableElement()) + if (previous.isNull() || previous.deepEquivalent().deprecatedNode()->rootEditableElement() != startContainer.rootEditableElement()) return false; } return true; @@ -390,7 +366,7 @@ bool Editor::isSelectTrailingWhitespaceEnabled() return client() && client()->isSelectTrailingWhitespaceEnabled(); } -bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity granularity, bool killRing, bool isTypingAction) +bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity granularity, bool shouldAddToKillRing, bool isTypingAction) { if (!canEdit()) return false; @@ -400,8 +376,8 @@ bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity g TypingCommand::deleteKeyPressed(document(), canSmartCopyOrDelete() ? TypingCommand::SmartDelete : 0, granularity); revealSelectionAfterEditingOperation(); } else { - if (killRing) - addToKillRing(selectedRange().get(), false); + if (shouldAddToKillRing) + addRangeToKillRing(*selectedRange().get(), KillRingInsertionMode::AppendText); deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); // Implicitly calls revealSelectionAfterEditingOperation(). } @@ -409,8 +385,8 @@ bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity g TypingCommand::Options options = 0; if (canSmartCopyOrDelete()) options |= TypingCommand::SmartDelete; - if (killRing) - options |= TypingCommand::KillRing; + if (shouldAddToKillRing) + options |= TypingCommand::AddsToKillRing; switch (direction) { case DirectionForward: case DirectionRight: @@ -427,112 +403,39 @@ bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity g // FIXME: We should to move this down into deleteKeyPressed. // clear the "start new kill ring sequence" setting, because it was set to true // when the selection was updated by deleting the range - if (killRing) + if (shouldAddToKillRing) setStartNewKillRingSequence(false); return true; } -void Editor::deleteSelectionWithSmartDelete(bool smartDelete) +void Editor::deleteSelectionWithSmartDelete(bool smartDelete, EditAction editingAction) { if (m_frame.selection().isNone()) return; - applyCommand(DeleteSelectionCommand::create(document(), smartDelete)); + applyCommand(DeleteSelectionCommand::create(document(), smartDelete, true, false, false, true, editingAction)); } -#if PLATFORM(IOS) void Editor::clearText() { ClearTextCommand::CreateAndApply(&m_frame); } -void Editor::insertDictationPhrases(PassOwnPtr<Vector<Vector<String> > > dictationPhrases, RetainPtr<id> metadata) -{ - if (m_frame.selection().isNone()) - return; - - if (dictationPhrases->isEmpty()) - return; - - applyCommand(DictationCommandIOS::create(document(), dictationPhrases, metadata)); -} - -void Editor::setDictationPhrasesAsChildOfElement(PassOwnPtr<Vector<Vector<String> > > dictationPhrases, RetainPtr<id> metadata, Element* element) -{ - // Clear the composition. - clear(); - - // Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack. Some day - // we could make them Undoable, and let callers clear the Undo stack explicitly if they wish. - clearUndoRedoOperations(); - - m_frame.selection().clear(); - - element->removeChildren(); - - if (dictationPhrases->isEmpty()) { - client()->respondToChangedContents(); - return; - } - - ExceptionCode ec; - RefPtr<Range> context = document().createRange(); - context->selectNodeContents(element, ec); - - StringBuilder dictationPhrasesBuilder; - size_t dictationPhraseCount = dictationPhrases->size(); - for (size_t i = 0; i < dictationPhraseCount; i++) { - const String& firstInterpretation = dictationPhrases->at(i)[0]; - dictationPhrasesBuilder.append(firstInterpretation); - } - String serializedDictationPhrases = dictationPhrasesBuilder.toString(); - - element->appendChild(createFragmentFromText(*context.get(), serializedDictationPhrases), ec); - - // We need a layout in order to add markers below. - document().updateLayout(); - - if (!element->firstChild()->isTextNode()) { - // Shouldn't happen. - ASSERT(element->firstChild()->isTextNode()); - return; - } - - Text* textNode = static_cast<Text*>(element->firstChild()); - int previousDictationPhraseStart = 0; - for (size_t i = 0; i < dictationPhraseCount; i++) { - const Vector<String>& interpretations = dictationPhrases->at(i); - int dictationPhraseLength = interpretations[0].length(); - int dictationPhraseEnd = previousDictationPhraseStart + dictationPhraseLength; - if (interpretations.size() > 1) { - RefPtr<Range> dictationPhraseRange = Range::create(document(), textNode, previousDictationPhraseStart, textNode, dictationPhraseEnd); - document().markers().addDictationPhraseWithAlternativesMarker(dictationPhraseRange.get(), interpretations); - } - previousDictationPhraseStart = dictationPhraseEnd; - } - - RefPtr<Range> resultRange = Range::create(document(), textNode, 0, textNode, textNode->length()); - document().markers().addDictationResultMarker(resultRange.get(), metadata); - - client()->respondToChangedContents(); -} -#endif - void Editor::pasteAsPlainText(const String& pastingText, bool smartReplace) { Node* target = findEventTargetFromSelection(); if (!target) return; - target->dispatchEvent(TextEvent::createForPlainTextPaste(document().domWindow(), pastingText, smartReplace), IGNORE_EXCEPTION); + target->dispatchEvent(TextEvent::createForPlainTextPaste(document().domWindow(), pastingText, smartReplace)); } -void Editor::pasteAsFragment(PassRefPtr<DocumentFragment> pastingFragment, bool smartReplace, bool matchStyle) +void Editor::pasteAsFragment(Ref<DocumentFragment>&& pastingFragment, bool smartReplace, bool matchStyle, MailBlockquoteHandling respectsMailBlockquote) { Node* target = findEventTargetFromSelection(); if (!target) return; - target->dispatchEvent(TextEvent::createForFragmentPaste(document().domWindow(), pastingFragment, smartReplace, matchStyle), IGNORE_EXCEPTION); + target->dispatchEvent(TextEvent::createForFragmentPaste(document().domWindow(), WTFMove(pastingFragment), smartReplace, matchStyle, respectsMailBlockquote)); } void Editor::pasteAsPlainTextBypassingDHTML() @@ -543,7 +446,7 @@ void Editor::pasteAsPlainTextBypassingDHTML() void Editor::pasteAsPlainTextWithPasteboard(Pasteboard& pasteboard) { String text = readPlainTextFromPasteboard(pasteboard); - if (client() && client()->shouldInsertText(text, selectedRange().get(), EditorInsertActionPasted)) + if (client() && client()->shouldInsertText(text, selectedRange().get(), EditorInsertAction::Pasted)) pasteAsPlainText(text, canSmartReplaceWithPasteboard(pasteboard)); } @@ -554,7 +457,7 @@ String Editor::readPlainTextFromPasteboard(Pasteboard& pasteboard) return plainTextFromPasteboard(text); } -#if !(PLATFORM(MAC) && !PLATFORM(IOS)) +#if !PLATFORM(MAC) String Editor::plainTextFromPasteboard(const PasteboardPlainText& text) { @@ -563,20 +466,6 @@ String Editor::plainTextFromPasteboard(const PasteboardPlainText& text) #endif -#if !PLATFORM(MAC) && !PLATFORM(EFL) -void Editor::pasteWithPasteboard(Pasteboard* pasteboard, bool allowPlainText) -{ - RefPtr<Range> range = selectedRange(); - if (!range) - return; - - bool chosePlainText; - RefPtr<DocumentFragment> fragment = pasteboard->documentFragment(m_frame, *range, allowPlainText, chosePlainText); - if (fragment && shouldInsertFragment(fragment, range, EditorInsertActionPasted)) - pasteAsFragment(fragment, canSmartReplaceWithPasteboard(*pasteboard), chosePlainText); -} -#endif - bool Editor::canSmartReplaceWithPasteboard(Pasteboard& pasteboard) { return client() && client()->smartInsertDeleteEnabled() && pasteboard.canSmartReplace(); @@ -589,18 +478,23 @@ bool Editor::shouldInsertFragment(PassRefPtr<DocumentFragment> fragment, PassRef if (fragment) { Node* child = fragment->firstChild(); - if (child && fragment->lastChild() == child && child->isCharacterDataNode()) - return client()->shouldInsertText(toCharacterData(child)->data(), replacingDOMRange.get(), givenAction); + if (is<CharacterData>(child) && fragment->lastChild() == child) + return client()->shouldInsertText(downcast<CharacterData>(*child).data(), replacingDOMRange.get(), givenAction); } return client()->shouldInsertNode(fragment.get(), replacingDOMRange.get(), givenAction); } -void Editor::replaceSelectionWithFragment(PassRefPtr<DocumentFragment> fragment, bool selectReplacement, bool smartReplace, bool matchStyle) +void Editor::replaceSelectionWithFragment(PassRefPtr<DocumentFragment> fragment, bool selectReplacement, bool smartReplace, bool matchStyle, EditAction editingAction, MailBlockquoteHandling mailBlockquoteHandling) { - if (m_frame.selection().isNone() || !m_frame.selection().isContentEditable() || !fragment) + VisibleSelection selection = m_frame.selection().selection(); + if (selection.isNone() || !selection.isContentEditable() || !fragment) return; + AccessibilityReplacedText replacedText; + if (AXObjectCache::accessibilityEnabled() && editingAction == EditActionPaste) + replacedText = AccessibilityReplacedText(selection); + ReplaceSelectionCommand::CommandOptions options = ReplaceSelectionCommand::PreventNesting | ReplaceSelectionCommand::SanitizeFragment; if (selectReplacement) options |= ReplaceSelectionCommand::SelectReplacement; @@ -608,116 +502,51 @@ void Editor::replaceSelectionWithFragment(PassRefPtr<DocumentFragment> fragment, options |= ReplaceSelectionCommand::SmartReplace; if (matchStyle) options |= ReplaceSelectionCommand::MatchStyle; - applyCommand(ReplaceSelectionCommand::create(document(), fragment, options, EditActionPaste)); + if (mailBlockquoteHandling == MailBlockquoteHandling::IgnoreBlockquote) + options |= ReplaceSelectionCommand::IgnoreMailBlockquote; + + RefPtr<ReplaceSelectionCommand> command = ReplaceSelectionCommand::create(document(), fragment, options, editingAction); + applyCommand(command); revealSelectionAfterEditingOperation(); - if (m_frame.selection().isInPasswordField() || !isContinuousSpellCheckingEnabled()) + selection = m_frame.selection().selection(); + if (selection.isInPasswordField()) return; - Node* nodeToCheck = m_frame.selection().rootEditableElement(); + + if (AXObjectCache::accessibilityEnabled() && editingAction == EditActionPaste) { + String text = AccessibilityObject::stringForVisiblePositionRange(command->visibleSelectionForInsertedText()); + replacedText.postTextStateChangeNotification(document().existingAXObjectCache(), AXTextEditTypePaste, text, m_frame.selection().selection()); + command->composition()->setRangeDeletedByUnapply(replacedText.replacedRange()); + } + + if (!isContinuousSpellCheckingEnabled()) + return; + + Node* nodeToCheck = selection.rootEditableElement(); if (!nodeToCheck) return; RefPtr<Range> rangeToCheck = Range::create(document(), firstPositionInNode(nodeToCheck), lastPositionInNode(nodeToCheck)); - m_spellChecker->requestCheckingFor(SpellCheckRequest::create(resolveTextCheckingTypeMask(TextCheckingTypeSpelling | TextCheckingTypeGrammar), TextCheckingProcessBatch, rangeToCheck, rangeToCheck)); + m_spellChecker->requestCheckingFor(SpellCheckRequest::create(resolveTextCheckingTypeMask(*nodeToCheck, TextCheckingTypeSpelling | TextCheckingTypeGrammar), TextCheckingProcessBatch, rangeToCheck, rangeToCheck)); } -void Editor::replaceSelectionWithText(const String& text, bool selectReplacement, bool smartReplace) +void Editor::replaceSelectionWithText(const String& text, bool selectReplacement, bool smartReplace, EditAction editingAction) { RefPtr<Range> range = selectedRange(); if (!range) return; - replaceSelectionWithFragment(createFragmentFromText(*range, text), selectReplacement, smartReplace, true); + replaceSelectionWithFragment(createFragmentFromText(*range, text), selectReplacement, smartReplace, true, editingAction); } -PassRefPtr<Range> Editor::selectedRange() +RefPtr<Range> Editor::selectedRange() { return m_frame.selection().toNormalizedRange(); } -#if PLATFORM(IOS) -void Editor::confirmMarkedText() -{ - // FIXME: This is a hacky workaround for the keyboard calling this method too late - - // after the selection and focus have already changed. See <rdar://problem/5975559> - Element* focused = document().focusedElement(); - Node* composition = compositionNode(); - - if (composition && focused && focused != composition && !composition->isDescendantOrShadowDescendantOf(focused)) { - cancelComposition(); - document().setFocusedElement(focused); - } else - confirmComposition(); -} - -void Editor::setTextAsChildOfElement(const String& text, Element* elem) -{ - // Clear the composition - clear(); - - // Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack. Some day - // we could make them Undoable, and let callers clear the Undo stack explicitly if they wish. - clearUndoRedoOperations(); - - // If the element is empty already and we're not adding text, we can early return and avoid clearing/setting - // a selection at [0, 0] and the expense involved in creation VisiblePositions. - if (!elem->firstChild() && text.isEmpty()) - return; - - // As a side effect this function sets a caret selection after the inserted content. Much of what - // follows is more expensive if there is a selection, so clear it since it's going to change anyway. - m_frame.selection().clear(); - - // clear out all current children of element - elem->removeChildren(); - - if (text.length()) { - // insert new text - // remove element from tree while doing it - // FIXME: The element we're inserting into is often the body element. It seems strange to be removing it - // (even if it is only temporary). ReplaceSelectionCommand doesn't bother doing this when it inserts - // content, why should we here? - ExceptionCode ec; - RefPtr<Node> parent = elem->parentNode(); - RefPtr<Node> siblingAfter = elem->nextSibling(); - if (parent) - elem->remove(ec); - - RefPtr<Range> context = document().createRange(); - context->selectNodeContents(elem, ec); - RefPtr<DocumentFragment> fragment = createFragmentFromText(*context.get(), text); - elem->appendChild(fragment, ec); - - // restore element to document - if (parent) { - if (siblingAfter) - parent->insertBefore(elem, siblingAfter.get(), ec); - else - parent->appendChild(elem, ec); - } - } - - // set the selection to the end - VisibleSelection selection; - - Position pos = createLegacyEditingPosition(elem, elem->childNodeCount()); - - VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY); - if (visiblePos.isNull()) - return; - - selection.setBase(visiblePos); - selection.setExtent(visiblePos); - - m_frame.selection().setSelection(selection); - - client()->respondToChangedContents(); -} -#endif - bool Editor::shouldDeleteRange(Range* range) const { - if (!range || range->collapsed(IGNORE_EXCEPTION)) + if (!range || range->collapsed()) return false; if (!canDeleteRange(range)) @@ -728,45 +557,31 @@ bool Editor::shouldDeleteRange(Range* range) const bool Editor::tryDHTMLCopy() { - if (m_frame.selection().isInPasswordField()) + if (m_frame.selection().selection().isInPasswordField()) return false; - return !dispatchCPPEvent(eventNames().copyEvent, ClipboardWritable); + return !dispatchCPPEvent(eventNames().copyEvent, DataTransferAccessPolicy::Writable); } bool Editor::tryDHTMLCut() { - if (m_frame.selection().isInPasswordField()) + if (m_frame.selection().selection().isInPasswordField()) return false; - return !dispatchCPPEvent(eventNames().cutEvent, ClipboardWritable); + return !dispatchCPPEvent(eventNames().cutEvent, DataTransferAccessPolicy::Writable); } bool Editor::tryDHTMLPaste() { - return !dispatchCPPEvent(eventNames().pasteEvent, ClipboardReadable); + return !dispatchCPPEvent(eventNames().pasteEvent, DataTransferAccessPolicy::Readable); } bool Editor::shouldInsertText(const String& text, Range* range, EditorInsertAction action) const { - return client() && client()->shouldInsertText(text, range, action); -} - -void Editor::notifyComponentsOnChangedSelection(const VisibleSelection& oldSelection, FrameSelection::SetSelectionOptions options) -{ -#if PLATFORM(IOS) - // FIXME: Merge this to open source https://bugs.webkit.org/show_bug.cgi?id=38830 - if (m_ignoreCompositionSelectionChange) - return; -#endif + if (m_frame.mainFrame().loader().shouldSuppressKeyboardInput() && action == EditorInsertAction::Typed) + return false; - if (client()) - client()->respondToChangedSelection(&m_frame); - setStartNewKillRingSequence(true); -#if ENABLE(DELETION_UI) - m_deleteButtonController->respondToChangedSelection(oldSelection); -#endif - m_alternativeTextController->respondToChangedSelection(oldSelection, options); + return client() && client()->shouldInsertText(text, range, action); } void Editor::respondToChangedContents(const VisibleSelection& endingSelection) @@ -797,8 +612,11 @@ bool Editor::hasBidiSelection() const } else startNode = m_frame.selection().selection().visibleStart().deepEquivalent().deprecatedNode(); + if (!startNode) + return false; + auto renderer = startNode->renderer(); - while (renderer && !renderer->isRenderBlockFlow()) + while (renderer && !is<RenderBlockFlow>(*renderer)) renderer = renderer->parent(); if (!renderer) @@ -807,17 +625,17 @@ bool Editor::hasBidiSelection() const if (!renderer->style().isLeftToRightDirection()) return true; - return toRenderBlockFlow(renderer)->containsNonZeroBidiLevel(); + return downcast<RenderBlockFlow>(*renderer).containsNonZeroBidiLevel(); } TriState Editor::selectionUnorderedListState() const { if (m_frame.selection().isCaret()) { - if (enclosingNodeWithTag(m_frame.selection().selection().start(), ulTag)) + if (enclosingElementWithTag(m_frame.selection().selection().start(), ulTag)) return TrueTriState; } else if (m_frame.selection().isRange()) { - Node* startNode = enclosingNodeWithTag(m_frame.selection().selection().start(), ulTag); - Node* endNode = enclosingNodeWithTag(m_frame.selection().selection().end(), ulTag); + auto* startNode = enclosingElementWithTag(m_frame.selection().selection().start(), ulTag); + auto* endNode = enclosingElementWithTag(m_frame.selection().selection().end(), ulTag); if (startNode && endNode && startNode == endNode) return TrueTriState; } @@ -828,11 +646,11 @@ TriState Editor::selectionUnorderedListState() const TriState Editor::selectionOrderedListState() const { if (m_frame.selection().isCaret()) { - if (enclosingNodeWithTag(m_frame.selection().selection().start(), olTag)) + if (enclosingElementWithTag(m_frame.selection().selection().start(), olTag)) return TrueTriState; } else if (m_frame.selection().isRange()) { - Node* startNode = enclosingNodeWithTag(m_frame.selection().selection().start(), olTag); - Node* endNode = enclosingNodeWithTag(m_frame.selection().selection().end(), olTag); + auto* startNode = enclosingElementWithTag(m_frame.selection().selection().start(), olTag); + auto* endNode = enclosingElementWithTag(m_frame.selection().selection().end(), olTag); if (startNode && endNode && startNode == endNode) return TrueTriState; } @@ -870,34 +688,34 @@ bool Editor::canDecreaseSelectionListLevel() return canEditRichly() && DecreaseSelectionListLevelCommand::canDecreaseSelectionListLevel(&document()); } -PassRefPtr<Node> Editor::increaseSelectionListLevel() +RefPtr<Node> Editor::increaseSelectionListLevel() { if (!canEditRichly() || m_frame.selection().isNone()) - return 0; + return nullptr; RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevel(&document()); revealSelectionAfterEditingOperation(); return newList; } -PassRefPtr<Node> Editor::increaseSelectionListLevelOrdered() +RefPtr<Node> Editor::increaseSelectionListLevelOrdered() { if (!canEditRichly() || m_frame.selection().isNone()) - return 0; + return nullptr; RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(&document()); revealSelectionAfterEditingOperation(); - return newList.release(); + return newList; } -PassRefPtr<Node> Editor::increaseSelectionListLevelUnordered() +RefPtr<Node> Editor::increaseSelectionListLevelUnordered() { if (!canEditRichly() || m_frame.selection().isNone()) - return 0; + return nullptr; RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(&document()); revealSelectionAfterEditingOperation(); - return newList.release(); + return newList; } void Editor::decreaseSelectionListLevel() @@ -916,38 +734,34 @@ void Editor::removeFormattingAndStyle() void Editor::clearLastEditCommand() { - m_lastEditCommand.clear(); + m_lastEditCommand = nullptr; } -#if PLATFORM(IOS) -// If the selection is adjusted from UIKit without closing the typing, the typing command may -// have a stale selection. -void Editor::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping() -{ - TypingCommand::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping(&m_frame, m_frame.selection().selection()); -} -#endif // Returns whether caller should continue with "the default processing", which is the same as // the event handler NOT setting the return value to false -bool Editor::dispatchCPPEvent(const AtomicString& eventType, ClipboardAccessPolicy policy) +bool Editor::dispatchCPPEvent(const AtomicString& eventType, DataTransferAccessPolicy policy) { Node* target = findEventTargetFromSelection(); if (!target) return true; - RefPtr<Clipboard> clipboard = Clipboard::createForCopyAndPaste(policy); + auto dataTransfer = DataTransfer::createForCopyAndPaste(policy); - RefPtr<Event> event = ClipboardEvent::create(eventType, true, true, clipboard); - target->dispatchEvent(event, IGNORE_EXCEPTION); + ClipboardEvent::Init init; + init.bubbles = true; + init.cancelable = true; + init.clipboardData = dataTransfer.ptr(); + auto event = ClipboardEvent::create(eventType, init, Event::IsTrusted::Yes); + target->dispatchEvent(event); bool noDefaultProcessing = event->defaultPrevented(); - if (noDefaultProcessing && policy == ClipboardWritable) { - OwnPtr<Pasteboard> pasteboard = Pasteboard::createForCopyAndPaste(); + if (noDefaultProcessing && policy == DataTransferAccessPolicy::Writable) { + auto pasteboard = Pasteboard::createForCopyAndPaste(); pasteboard->clear(); - pasteboard->writePasteboard(clipboard->pasteboard()); + pasteboard->writePasteboard(dataTransfer->pasteboard()); } - // invalidate clipboard here for security - clipboard->setAccessPolicy(ClipboardNumb); + // invalidate dataTransfer here for security + dataTransfer->setAccessPolicy(DataTransferAccessPolicy::Numb); return !noDefaultProcessing; } @@ -956,9 +770,9 @@ Node* Editor::findEventTargetFrom(const VisibleSelection& selection) const { Node* target = selection.start().element(); if (!target) - target = document().body(); + target = document().bodyOrFrameset(); if (!target) - return 0; + return nullptr; return target; } @@ -970,18 +784,39 @@ Node* Editor::findEventTargetFromSelection() const void Editor::applyStyle(StyleProperties* style, EditAction editingAction) { - switch (m_frame.selection().selectionType()) { - case VisibleSelection::NoSelection: - // do nothing - break; + if (style) + applyStyle(EditingStyle::create(style), editingAction); +} + +void Editor::applyStyle(RefPtr<EditingStyle>&& style, EditAction editingAction) +{ + if (!style) + return; + + auto selectionType = m_frame.selection().selection().selectionType(); + if (selectionType == VisibleSelection::NoSelection) + return; + + String inputTypeName = inputTypeNameForEditingAction(editingAction); + String inputEventData = inputEventDataForEditingStyleAndAction(*style, editingAction); + RefPtr<Element> element = m_frame.selection().selection().rootEditableElement(); + if (element && !dispatchBeforeInputEvent(*element, inputTypeName, inputEventData)) + return; + + switch (selectionType) { case VisibleSelection::CaretSelection: - computeAndSetTypingStyle(style, editingAction); + computeAndSetTypingStyle(*style, editingAction); break; case VisibleSelection::RangeSelection: - if (style) - applyCommand(ApplyStyleCommand::create(document(), EditingStyle::create(style).get(), editingAction)); + applyCommand(ApplyStyleCommand::create(document(), style.get(), editingAction)); + break; + default: break; } + + client()->didApplyStyle(); + if (element) + dispatchInputEvent(*element, inputTypeName, inputEventData); } bool Editor::shouldApplyStyle(StyleProperties* style, Range* range) @@ -991,16 +826,23 @@ bool Editor::shouldApplyStyle(StyleProperties* style, Range* range) void Editor::applyParagraphStyle(StyleProperties* style, EditAction editingAction) { - switch (m_frame.selection().selectionType()) { - case VisibleSelection::NoSelection: - // do nothing - break; - case VisibleSelection::CaretSelection: - case VisibleSelection::RangeSelection: - if (style) - applyCommand(ApplyStyleCommand::create(document(), EditingStyle::create(style).get(), editingAction, ApplyStyleCommand::ForceBlockProperties)); - break; - } + if (!style) + return; + + auto selectionType = m_frame.selection().selection().selectionType(); + if (selectionType == VisibleSelection::NoSelection) + return; + + String inputTypeName = inputTypeNameForEditingAction(editingAction); + String inputEventData = inputEventDataForEditingStyleAndAction(style, editingAction); + RefPtr<Element> element = m_frame.selection().selection().rootEditableElement(); + if (element && !dispatchBeforeInputEvent(*element, inputTypeName, inputEventData)) + return; + + applyCommand(ApplyStyleCommand::create(document(), EditingStyle::create(style).ptr(), editingAction, ApplyStyleCommand::ForceBlockProperties)); + client()->didApplyStyle(); + if (element) + dispatchInputEvent(*element, inputTypeName, inputEventData); } void Editor::applyStyleToSelection(StyleProperties* style, EditAction editingAction) @@ -1008,8 +850,21 @@ void Editor::applyStyleToSelection(StyleProperties* style, EditAction editingAct if (!style || style->isEmpty() || !canEditRichly()) return; - if (client() && client()->shouldApplyStyle(style, m_frame.selection().toNormalizedRange().get())) - applyStyle(style, editingAction); + if (!client() || !client()->shouldApplyStyle(style, m_frame.selection().toNormalizedRange().get())) + return; + applyStyle(style, editingAction); +} + +void Editor::applyStyleToSelection(Ref<EditingStyle>&& style, EditAction editingAction) +{ + if (style->isEmpty() || !canEditRichly()) + return; + + // FIXME: This is wrong for text decorations since m_mutableStyle is empty. + if (!client() || !client()->shouldApplyStyle(style->styleWithResolvedTextDecorations().ptr(), m_frame.selection().toNormalizedRange().get())) + return; + + applyStyle(WTFMove(style), editingAction); } void Editor::applyParagraphStyleToSelection(StyleProperties* style, EditAction editingAction) @@ -1054,93 +909,147 @@ void Editor::outdent() applyCommand(IndentOutdentCommand::create(document(), IndentOutdentCommand::Outdent)); } -static void dispatchEditableContentChangedEvents(PassRefPtr<Element> prpStartRoot, PassRefPtr<Element> prpEndRoot) +static void notifyTextFromControls(Element* startRoot, Element* endRoot) +{ + HTMLTextFormControlElement* startingTextControl = enclosingTextFormControl(firstPositionInOrBeforeNode(startRoot)); + HTMLTextFormControlElement* endingTextControl = enclosingTextFormControl(firstPositionInOrBeforeNode(endRoot)); + if (startingTextControl) + startingTextControl->didEditInnerTextValue(); + if (endingTextControl && startingTextControl != endingTextControl) + endingTextControl->didEditInnerTextValue(); +} + +static bool dispatchBeforeInputEvents(RefPtr<Element> startRoot, RefPtr<Element> endRoot, const AtomicString& inputTypeName, const String& data = { }, RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<RefPtr<StaticRange>>& targetRanges = { }, bool cancelable = true) { - RefPtr<Element> startRoot = prpStartRoot; - RefPtr<Element> endRoot = prpEndRoot; + bool continueWithDefaultBehavior = true; if (startRoot) - startRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), IGNORE_EXCEPTION); + continueWithDefaultBehavior &= dispatchBeforeInputEvent(*startRoot, inputTypeName, data, WTFMove(dataTransfer), targetRanges, cancelable); if (endRoot && endRoot != startRoot) - endRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), IGNORE_EXCEPTION); + continueWithDefaultBehavior &= dispatchBeforeInputEvent(*endRoot, inputTypeName, data, WTFMove(dataTransfer), targetRanges, cancelable); + return continueWithDefaultBehavior; +} + +static void dispatchInputEvents(RefPtr<Element> startRoot, RefPtr<Element> endRoot, const AtomicString& inputTypeName, const String& data = { }, RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<RefPtr<StaticRange>>& targetRanges = { }) +{ + if (startRoot) + dispatchInputEvent(*startRoot, inputTypeName, data, WTFMove(dataTransfer), targetRanges); + if (endRoot && endRoot != startRoot) + dispatchInputEvent(*endRoot, inputTypeName, data, WTFMove(dataTransfer), targetRanges); +} + +bool Editor::willApplyEditing(CompositeEditCommand& command, Vector<RefPtr<StaticRange>>&& targetRanges) const +{ + if (!command.shouldDispatchInputEvents()) + return true; + + auto* composition = command.composition(); + if (!composition) + return true; + + return dispatchBeforeInputEvents(composition->startingRootEditableElement(), composition->endingRootEditableElement(), command.inputEventTypeName(), command.inputEventData(), command.inputEventDataTransfer(), targetRanges, command.isBeforeInputEventCancelable()); } void Editor::appliedEditing(PassRefPtr<CompositeEditCommand> cmd) { + LOG(Editing, "Editor %p appliedEditing", this); + document().updateLayout(); EditCommandComposition* composition = cmd->composition(); ASSERT(composition); VisibleSelection newSelection(cmd->endingSelection()); - m_alternativeTextController->respondToAppliedEditing(cmd.get()); + notifyTextFromControls(composition->startingRootEditableElement(), composition->endingRootEditableElement()); - // Don't clear the typing style with this selection change. We do those things elsewhere if necessary. - FrameSelection::SetSelectionOptions options = cmd->isDictationCommand() ? FrameSelection::DictationTriggered : 0; - changeSelectionAfterCommand(newSelection, options); - dispatchEditableContentChangedEvents(composition->startingRootEditableElement(), composition->endingRootEditableElement()); + if (cmd->isTopLevelCommand()) { + // Don't clear the typing style with this selection change. We do those things elsewhere if necessary. + FrameSelection::SetSelectionOptions options = cmd->isDictationCommand() ? FrameSelection::DictationTriggered : 0; - if (!cmd->preservesTypingStyle()) - m_frame.selection().clearTypingStyle(); + changeSelectionAfterCommand(newSelection, options); + } - // Command will be equal to last edit command only in the case of typing - if (m_lastEditCommand.get() == cmd) - ASSERT(cmd->isTypingCommand()); - else { - // Only register a new undo command if the command passed in is - // different from the last command - m_lastEditCommand = cmd; - if (client()) - client()->registerUndoStep(m_lastEditCommand->ensureComposition()); + if (cmd->shouldDispatchInputEvents()) + dispatchInputEvents(composition->startingRootEditableElement(), composition->endingRootEditableElement(), cmd->inputEventTypeName(), cmd->inputEventData(), cmd->inputEventDataTransfer()); + + if (cmd->isTopLevelCommand()) { + updateEditorUINowIfScheduled(); + + m_alternativeTextController->respondToAppliedEditing(cmd.get()); + + if (!cmd->preservesTypingStyle()) + m_frame.selection().clearTypingStyle(); + + // Command will be equal to last edit command only in the case of typing + if (m_lastEditCommand.get() == cmd) + ASSERT(cmd->isTypingCommand()); + else { + // Only register a new undo command if the command passed in is + // different from the last command + m_lastEditCommand = cmd; + if (client()) + client()->registerUndoStep(m_lastEditCommand->ensureComposition()); + } + respondToChangedContents(newSelection); } +} - respondToChangedContents(newSelection); +bool Editor::willUnapplyEditing(const EditCommandComposition& composition) const +{ + return dispatchBeforeInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyUndo"); } -void Editor::unappliedEditing(PassRefPtr<EditCommandComposition> cmd) +void Editor::unappliedEditing(EditCommandComposition& composition) { document().updateLayout(); - VisibleSelection newSelection(cmd->startingSelection()); - changeSelectionAfterCommand(newSelection, FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle); - dispatchEditableContentChangedEvents(cmd->startingRootEditableElement(), cmd->endingRootEditableElement()); + notifyTextFromControls(composition.startingRootEditableElement(), composition.endingRootEditableElement()); - m_alternativeTextController->respondToUnappliedEditing(cmd.get()); + VisibleSelection newSelection(composition.startingSelection()); + changeSelectionAfterCommand(newSelection, FrameSelection::defaultSetSelectionOptions()); + dispatchInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyUndo"); - m_lastEditCommand = 0; - if (client()) - client()->registerRedoStep(cmd); + updateEditorUINowIfScheduled(); + + m_alternativeTextController->respondToUnappliedEditing(&composition); + + m_lastEditCommand = nullptr; + if (auto* client = this->client()) + client->registerRedoStep(composition); respondToChangedContents(newSelection); } -void Editor::reappliedEditing(PassRefPtr<EditCommandComposition> cmd) +bool Editor::willReapplyEditing(const EditCommandComposition& composition) const +{ + return dispatchBeforeInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyRedo"); +} + +void Editor::reappliedEditing(EditCommandComposition& composition) { document().updateLayout(); - VisibleSelection newSelection(cmd->endingSelection()); - changeSelectionAfterCommand(newSelection, FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle); - dispatchEditableContentChangedEvents(cmd->startingRootEditableElement(), cmd->endingRootEditableElement()); + notifyTextFromControls(composition.startingRootEditableElement(), composition.endingRootEditableElement()); - m_lastEditCommand = 0; - if (client()) - client()->registerUndoStep(cmd); + VisibleSelection newSelection(composition.endingSelection()); + changeSelectionAfterCommand(newSelection, FrameSelection::defaultSetSelectionOptions()); + dispatchInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyRedo"); + + updateEditorUINowIfScheduled(); + + m_lastEditCommand = nullptr; + if (auto* client = this->client()) + client->registerUndoStep(composition); respondToChangedContents(newSelection); } Editor::Editor(Frame& frame) : m_frame(frame) -#if ENABLE(DELETION_UI) - , m_deleteButtonController(adoptPtr(new DeleteButtonController(frame))) + , m_killRing(std::make_unique<KillRing>()) + , m_spellChecker(std::make_unique<SpellChecker>(frame)) + , m_alternativeTextController(std::make_unique<AlternativeTextController>(frame)) + , m_editorUIUpdateTimer(*this, &Editor::editorUIUpdateTimerFired) +#if ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS) + , m_telephoneNumberDetectionUpdateTimer(*this, &Editor::scanSelectionForTelephoneNumbers) #endif - , m_ignoreCompositionSelectionChange(false) - , m_shouldStartNewKillRingSequence(false) - // This is off by default, since most editors want this behavior (this matches IE but not FF). - , m_shouldStyleWithCSS(false) - , m_killRing(adoptPtr(new KillRing)) - , m_spellChecker(adoptPtr(new SpellChecker(frame))) - , m_alternativeTextController(adoptPtr(new AlternativeTextController(frame))) - , m_areMarkedTextMatchesHighlighted(false) - , m_defaultParagraphSeparator(EditorParagraphSeparatorIsDiv) - , m_overwriteModeEnabled(false) { } @@ -1150,19 +1059,19 @@ Editor::~Editor() void Editor::clear() { - m_compositionNode = 0; + if (m_compositionNode) { + m_compositionNode = nullptr; + if (EditorClient* client = this->client()) + client->discardedComposition(&m_frame); + } m_customCompositionUnderlines.clear(); m_shouldStyleWithCSS = false; m_defaultParagraphSeparator = EditorParagraphSeparatorIsDiv; - -#if ENABLE(DELETION_UI) - m_deleteButtonController = adoptPtr(new DeleteButtonController(m_frame)); -#endif } -bool Editor::insertText(const String& text, Event* triggeringEvent) +bool Editor::insertText(const String& text, Event* triggeringEvent, TextEventInputType inputType) { - return m_frame.eventHandler().handleTextInputEvent(text, triggeringEvent); + return m_frame.eventHandler().handleTextInputEvent(text, triggeringEvent, inputType); } bool Editor::insertTextForConfirmedComposition(const String& text) @@ -1185,7 +1094,7 @@ bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectIn return false; RefPtr<Range> range = selection.toNormalizedRange(); - if (!shouldInsertText(text, range.get(), EditorInsertActionTyped)) + if (!shouldInsertText(text, range.get(), EditorInsertAction::Typed)) return true; updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text[0])); @@ -1209,20 +1118,28 @@ bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectIn // Insert the text if (triggeringEvent && triggeringEvent->isDictation()) - DictationCommand::insertText(&document.get(), text, triggeringEvent->dictationAlternatives(), selection); + DictationCommand::insertText(document, text, triggeringEvent->dictationAlternatives(), selection); else { TypingCommand::Options options = 0; if (selectInsertedText) options |= TypingCommand::SelectInsertedText; if (autocorrectionWasApplied) options |= TypingCommand::RetainAutocorrectionIndicator; - TypingCommand::insertText(document.get(), text, selection, options, triggeringEvent && triggeringEvent->isComposition() ? TypingCommand::TextCompositionConfirm : TypingCommand::TextCompositionNone); + if (triggeringEvent && triggeringEvent->isAutocompletion()) + options |= TypingCommand::IsAutocompletion; + TypingCommand::insertText(document, text, selection, options, triggeringEvent && triggeringEvent->isComposition() ? TypingCommand::TextCompositionFinal : TypingCommand::TextCompositionNone); } // Reveal the current selection if (Frame* editedFrame = document->frame()) - if (Page* page = editedFrame->page()) - page->focusController().focusedOrMainFrame().selection().revealSelection(ScrollAlignment::alignCenterIfNeeded); + if (Page* page = editedFrame->page()) { +#if PLATFORM(IOS) + SelectionRevealMode revealMode = SelectionRevealMode::RevealUpToMainFrame; +#else + SelectionRevealMode revealMode = SelectionRevealMode::Reveal; +#endif + page->focusController().focusedOrMainFrame().selection().revealSelection(revealMode, ScrollAlignment::alignCenterIfNeeded); + } } } @@ -1234,7 +1151,7 @@ bool Editor::insertLineBreak() if (!canEdit()) return false; - if (!shouldInsertText("\n", m_frame.selection().toNormalizedRange().get(), EditorInsertActionTyped)) + if (!shouldInsertText("\n", m_frame.selection().toNormalizedRange().get(), EditorInsertAction::Typed)) return true; VisiblePosition caret = m_frame.selection().selection().visibleStart(); @@ -1254,7 +1171,7 @@ bool Editor::insertParagraphSeparator() if (!canEditRichly()) return insertLineBreak(); - if (!shouldInsertText("\n", m_frame.selection().toNormalizedRange().get(), EditorInsertActionTyped)) + if (!shouldInsertText("\n", m_frame.selection().toNormalizedRange().get(), EditorInsertAction::Typed)) return true; VisiblePosition caret = m_frame.selection().selection().visibleStart(); @@ -1266,6 +1183,14 @@ bool Editor::insertParagraphSeparator() return true; } +bool Editor::insertParagraphSeparatorInQuotedContent() +{ + // FIXME: Why is this missing calls to canEdit, canEditRichly, etc.? + TypingCommand::insertParagraphSeparatorInQuotedContent(document()); + revealSelectionAfterEditingOperation(); + return true; +} + void Editor::cut() { if (tryDHTMLCut()) @@ -1275,24 +1200,7 @@ void Editor::cut() return; } - // FIXME: This should share more code with the copy function; there is a lot of overlap. - RefPtr<Range> selection = selectedRange(); - willWriteSelectionToPasteboard(selection); - if (shouldDeleteRange(selection.get())) { - updateMarkersForWordsAffectedByEditing(true); - if (enclosingTextFormControl(m_frame.selection().start())) - Pasteboard::createForCopyAndPaste()->writePlainText(selectedTextForClipboard(), canSmartCopyOrDelete() ? Pasteboard::CanSmartReplace : Pasteboard::CannotSmartReplace); - else { -#if PLATFORM(MAC) || PLATFORM(EFL) - writeSelectionToPasteboard(*Pasteboard::createForCopyAndPaste()); -#else - // FIXME: Convert all other platforms to match Mac and delete this. - Pasteboard::createForCopyAndPaste()->writeSelection(*selection, canSmartCopyOrDelete(), m_frame, IncludeImageAltTextForClipboard); -#endif - } - didWriteSelectionToPasteboard(); - deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); - } + performCutOrCopy(CutAction); } void Editor::copy() @@ -1304,28 +1212,64 @@ void Editor::copy() return; } - willWriteSelectionToPasteboard(selectedRange()); - if (enclosingTextFormControl(m_frame.selection().start())) { - Pasteboard::createForCopyAndPaste()->writePlainText(selectedTextForClipboard(), - canSmartCopyOrDelete() ? Pasteboard::CanSmartReplace : Pasteboard::CannotSmartReplace); - } else { - if (HTMLImageElement* imageElement = imageElementFromImageDocument(document())) { -#if PLATFORM(MAC) || PLATFORM(EFL) + performCutOrCopy(CopyAction); +} + +void Editor::postTextStateChangeNotificationForCut(const String& text, const VisibleSelection& selection) +{ + if (!AXObjectCache::accessibilityEnabled()) + return; + if (!text.length()) + return; + AXObjectCache* cache = document().existingAXObjectCache(); + if (!cache) + return; + cache->postTextStateChangeNotification(selection.start().anchorNode(), AXTextEditTypeCut, text, selection.start()); +} + +void Editor::performCutOrCopy(EditorActionSpecifier action) +{ + RefPtr<Range> selection = selectedRange(); + willWriteSelectionToPasteboard(selection); + if (action == CutAction) { + if (!shouldDeleteRange(selection.get())) + return; + + updateMarkersForWordsAffectedByEditing(true); + } + + if (enclosingTextFormControl(m_frame.selection().selection().start())) + Pasteboard::createForCopyAndPaste()->writePlainText(selectedTextForDataTransfer(), canSmartCopyOrDelete() ? Pasteboard::CanSmartReplace : Pasteboard::CannotSmartReplace); + else { + HTMLImageElement* imageElement = nullptr; + if (action == CopyAction) + imageElement = imageElementFromImageDocument(document()); + + if (imageElement) { +#if PLATFORM(COCOA) || PLATFORM(GTK) writeImageToPasteboard(*Pasteboard::createForCopyAndPaste(), *imageElement, document().url(), document().title()); #else Pasteboard::createForCopyAndPaste()->writeImage(*imageElement, document().url(), document().title()); #endif } else { -#if PLATFORM(MAC) || PLATFORM(EFL) +#if PLATFORM(COCOA) || PLATFORM(GTK) writeSelectionToPasteboard(*Pasteboard::createForCopyAndPaste()); #else // FIXME: Convert all other platforms to match Mac and delete this. - Pasteboard::createForCopyAndPaste()->writeSelection(*selectedRange(), canSmartCopyOrDelete(), m_frame, IncludeImageAltTextForClipboard); + Pasteboard::createForCopyAndPaste()->writeSelection(*selection, canSmartCopyOrDelete(), m_frame, IncludeImageAltTextForDataTransfer); #endif } } didWriteSelectionToPasteboard(); + if (action == CutAction) { + String text; + if (AXObjectCache::accessibilityEnabled()) + text = AccessibilityObject::stringForVisiblePositionRange(m_frame.selection().selection()); + deleteSelectionWithSmartDelete(canSmartCopyOrDelete(), EditActionCut); + if (AXObjectCache::accessibilityEnabled()) + postTextStateChangeNotificationForCut(text, m_frame.selection().selection()); + } } void Editor::paste() @@ -1340,9 +1284,8 @@ void Editor::paste(Pasteboard& pasteboard) if (!canPaste()) return; updateMarkersForWordsAffectedByEditing(false); - CachedResourceLoader* loader = document().cachedResourceLoader(); - ResourceCacheValidationSuppressor validationSuppressor(loader); - if (m_frame.selection().isContentRichlyEditable()) + ResourceCacheValidationSuppressor validationSuppressor(document().cachedResourceLoader()); + if (m_frame.selection().selection().isContentRichlyEditable()) pasteWithPasteboard(&pasteboard, true); else pasteAsPlainTextWithPasteboard(pasteboard); @@ -1365,7 +1308,7 @@ void Editor::performDelete() return; } - addToKillRing(selectedRange().get(), false); + addRangeToKillRing(*selectedRange().get(), KillRingInsertionMode::AppendText); deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); // clear the "start new kill ring sequence" setting, because it was set to true @@ -1383,12 +1326,12 @@ void Editor::simplifyMarkup(Node* startNode, Node* endNode) // check if start node is before endNode Node* node = startNode; while (node && node != endNode) - node = NodeTraversal::next(node); + node = NodeTraversal::next(*node); if (!node) return; } - applyCommand(SimplifyMarkupCommand::create(document(), startNode, (endNode) ? NodeTraversal::next(endNode) : 0)); + applyCommand(SimplifyMarkupCommand::create(document(), startNode, endNode ? NodeTraversal::next(*endNode) : nullptr)); } void Editor::copyURL(const URL& url, const String& title) @@ -1402,7 +1345,7 @@ void Editor::copyURL(const URL& url, const String& title, Pasteboard& pasteboard pasteboardURL.url = url; pasteboardURL.title = title; -#if PLATFORM(MAC) && !PLATFORM(IOS) +#if PLATFORM(MAC) fillInUserVisibleForm(pasteboardURL); #endif @@ -1410,6 +1353,7 @@ void Editor::copyURL(const URL& url, const String& title, Pasteboard& pasteboard } #if !PLATFORM(IOS) + void Editor::copyImage(const HitTestResult& result) { Element* element = result.innerNonSharedElement(); @@ -1420,12 +1364,13 @@ void Editor::copyImage(const HitTestResult& result) if (url.isEmpty()) url = result.absoluteImageURL(); -#if PLATFORM(MAC) || PLATFORM(EFL) +#if PLATFORM(COCOA) || PLATFORM(GTK) writeImageToPasteboard(*Pasteboard::createForCopyAndPaste(), *element, url, result.altDisplayString()); #else Pasteboard::createForCopyAndPaste()->writeImage(*element, url, result.altDisplayString()); #endif } + #endif bool Editor::isContinuousSpellCheckingEnabled() const @@ -1644,11 +1589,18 @@ void Editor::setBaseWritingDirection(WritingDirection direction) #endif Element* focusedElement = document().focusedElement(); - if (focusedElement && isHTMLTextFormControlElement(*focusedElement)) { + if (is<HTMLTextFormControlElement>(focusedElement)) { if (direction == NaturalWritingDirection) return; - toHTMLElement(focusedElement)->setAttribute(dirAttr, direction == LeftToRightWritingDirection ? "ltr" : "rtl"); - focusedElement->dispatchInputEvent(); + + auto& focusedFormElement = downcast<HTMLTextFormControlElement>(*focusedElement); + auto directionValue = direction == LeftToRightWritingDirection ? "ltr" : "rtl"; + auto writingDirectionInputTypeName = inputTypeNameForEditingAction(EditActionSetWritingDirection); + if (!dispatchBeforeInputEvent(focusedFormElement, writingDirectionInputTypeName, directionValue)) + return; + + focusedFormElement.setAttributeWithoutSynchronization(dirAttr, directionValue); + dispatchInputEvent(focusedFormElement, writingDirectionInputTypeName, directionValue); document().updateStyleIfNeeded(); return; } @@ -1742,29 +1694,25 @@ void Editor::setComposition(const String& text, SetCompositionMode mode) else selectComposition(); + m_compositionNode = nullptr; + m_customCompositionUnderlines.clear(); + if (m_frame.selection().isNone()) { setIgnoreCompositionSelectionChange(false); return; } - - // Dispatch a compositionend event to the focused node. - // We should send this event before sending a TextEvent as written in Section 6.2.2 and 6.2.3 of - // the DOM Event specification. - if (Element* target = document().focusedElement()) { - RefPtr<CompositionEvent> event = CompositionEvent::create(eventNames().compositionendEvent, document().domWindow(), text); - target->dispatchEvent(event.release(), IGNORE_EXCEPTION); - } - - // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input - // will delete the old composition with an optimized replace operation. - if (text.isEmpty() && mode != CancelComposition) - TypingCommand::deleteSelection(document(), 0); - m_compositionNode = 0; - m_customCompositionUnderlines.clear(); + // Always delete the current composition before inserting the finalized composition text if we're confirming our composition. + // Our default behavior (if the beforeinput event is not prevented) is to insert the finalized composition text back in. + // We pass TypingCommand::TextCompositionPending here to indicate that we are deleting the pending composition. + if (mode != CancelComposition) + TypingCommand::deleteSelection(document(), 0, TypingCommand::TextCompositionPending); insertTextForConfirmedComposition(text); + if (auto* target = document().focusedElement()) + target->dispatchEvent(CompositionEvent::create(eventNames().compositionendEvent, document().domWindow(), text)); + if (mode == CancelComposition) { // An open typing command that disagrees about current selection would cause issues with typing later on. TypingCommand::closeTyping(&m_frame); @@ -1775,6 +1723,8 @@ void Editor::setComposition(const String& text, SetCompositionMode mode) void Editor::setComposition(const String& text, const Vector<CompositionUnderline>& underlines, unsigned selectionStart, unsigned selectionEnd) { + Ref<Frame> protection(m_frame); + UserTypingGestureIndicator typingGestureIndicator(m_frame); setIgnoreCompositionSelectionChange(true); @@ -1791,6 +1741,18 @@ void Editor::setComposition(const String& text, const Vector<CompositionUnderlin return; } + String originalText = selectedText(); + bool isStartingToRecomposeExistingRange = !text.isEmpty() && selectionStart < selectionEnd && !hasComposition(); + if (isStartingToRecomposeExistingRange) { + // We pass TypingCommand::TextCompositionFinal here to indicate that we are removing composition text that has been finalized. + TypingCommand::deleteSelection(document(), 0, TypingCommand::TextCompositionFinal); + const VisibleSelection& currentSelection = m_frame.selection().selection(); + if (currentSelection.isRange()) { + // If deletion was prevented, then we need to collapse the selection to the end so that the original text will not be recomposed. + m_frame.selection().setSelection({ currentSelection.end(), currentSelection.end() }); + } + } + #if PLATFORM(IOS) client()->startDelayingAndCoalescingContentChangeNotifications(); #endif @@ -1817,47 +1779,46 @@ void Editor::setComposition(const String& text, const Vector<CompositionUnderlin // We should send a compositionstart event only when the given text is not empty because this // function doesn't create a composition node when the text is empty. if (!text.isEmpty()) { - target->dispatchEvent(CompositionEvent::create(eventNames().compositionstartEvent, document().domWindow(), selectedText())); + target->dispatchEvent(CompositionEvent::create(eventNames().compositionstartEvent, document().domWindow(), originalText)); event = CompositionEvent::create(eventNames().compositionupdateEvent, document().domWindow(), text); } - } else { - if (!text.isEmpty()) - event = CompositionEvent::create(eventNames().compositionupdateEvent, document().domWindow(), text); - else - event = CompositionEvent::create(eventNames().compositionendEvent, document().domWindow(), text); - } - if (event.get()) - target->dispatchEvent(event, IGNORE_EXCEPTION); + } else if (!text.isEmpty()) + event = CompositionEvent::create(eventNames().compositionupdateEvent, document().domWindow(), text); + + if (event) + target->dispatchEvent(*event); } // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input // will delete the old composition with an optimized replace operation. - if (text.isEmpty()) - TypingCommand::deleteSelection(document(), TypingCommand::PreventSpellChecking); + if (text.isEmpty()) { + TypingCommand::deleteSelection(document(), TypingCommand::PreventSpellChecking, TypingCommand::TextCompositionPending); + if (target) + target->dispatchEvent(CompositionEvent::create(eventNames().compositionendEvent, document().domWindow(), text)); + } - m_compositionNode = 0; + m_compositionNode = nullptr; m_customCompositionUnderlines.clear(); if (!text.isEmpty()) { - TypingCommand::insertText(document(), text, TypingCommand::SelectInsertedText | TypingCommand::PreventSpellChecking, TypingCommand::TextCompositionUpdate); + TypingCommand::insertText(document(), text, TypingCommand::SelectInsertedText | TypingCommand::PreventSpellChecking, TypingCommand::TextCompositionPending); // Find out what node has the composition now. - Position base = m_frame.selection().base().downstream(); - Position extent = m_frame.selection().extent(); + Position base = m_frame.selection().selection().base().downstream(); + Position extent = m_frame.selection().selection().extent(); Node* baseNode = base.deprecatedNode(); unsigned baseOffset = base.deprecatedEditingOffset(); Node* extentNode = extent.deprecatedNode(); unsigned extentOffset = extent.deprecatedEditingOffset(); - if (baseNode && baseNode == extentNode && baseNode->isTextNode() && baseOffset + text.length() == extentOffset) { - m_compositionNode = toText(baseNode); + if (is<Text>(baseNode) && baseNode == extentNode && baseOffset + text.length() == extentOffset) { + m_compositionNode = downcast<Text>(baseNode); m_compositionStart = baseOffset; m_compositionEnd = extentOffset; m_customCompositionUnderlines = underlines; - size_t numUnderlines = m_customCompositionUnderlines.size(); - for (size_t i = 0; i < numUnderlines; ++i) { - m_customCompositionUnderlines[i].startOffset += baseOffset; - m_customCompositionUnderlines[i].endOffset += baseOffset; + for (auto& underline : m_customCompositionUnderlines) { + underline.startOffset += baseOffset; + underline.endOffset += baseOffset; } if (baseNode->renderer()) baseNode->renderer()->repaint(); @@ -1907,8 +1868,11 @@ void Editor::learnSpelling() } #if !PLATFORM(IOS) + void Editor::advanceToNextMisspelling(bool startBeforeSelection) { + Ref<Frame> protection(m_frame); + // The basic approach is to search in two phases - from the selection end to the end of the doc, and // then we wrap and search from the doc start to (approximately) where we started. @@ -1937,19 +1901,21 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) // when spell checking the whole document before sending the message. // In that case the document might not be editable, but there are editable pockets that need to be spell checked. - position = firstEditablePositionAfterPositionInRoot(position, document().documentElement()).deepEquivalent(); + position = VisiblePosition(firstEditablePositionAfterPositionInRoot(position, document().documentElement())).deepEquivalent(); if (position.isNull()) return; Position rangeCompliantPosition = position.parentAnchoredEquivalent(); - spellingSearchRange->setStart(rangeCompliantPosition.deprecatedNode(), rangeCompliantPosition.deprecatedEditingOffset(), IGNORE_EXCEPTION); + if (rangeCompliantPosition.deprecatedNode()) + spellingSearchRange->setStart(*rangeCompliantPosition.deprecatedNode(), rangeCompliantPosition.deprecatedEditingOffset()); startedWithSelection = false; // won't need to wrap } // topNode defines the whole range we want to operate on - Node* topNode = highestEditableRoot(position); + auto* topNode = highestEditableRoot(position); // FIXME: lastOffsetForEditing() is wrong here if editingIgnoresContent(highestEditableRoot()) returns true (e.g. a <table>) - spellingSearchRange->setEnd(topNode, lastOffsetForEditing(topNode), IGNORE_EXCEPTION); + if (topNode) + spellingSearchRange->setEnd(*topNode, lastOffsetForEditing(*topNode)); // If spellingSearchRange starts in the middle of a word, advance to the next word so we start checking // at a word boundary. Going back by one char and then forward by a word does the trick. @@ -1960,7 +1926,7 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) // else we were already at the start of the editable node } - if (spellingSearchRange->collapsed(IGNORE_EXCEPTION)) + if (spellingSearchRange->collapsed()) return; // nothing to search in // Get the spell checker if it is available @@ -1970,7 +1936,7 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) // We go to the end of our first range instead of the start of it, just to be sure // we don't get foiled by any word boundary problems at the start. It means we might // do a tiny bit more searching. - Node* searchEndNodeAfterWrap = spellingSearchRange->endContainer(); + Node& searchEndNodeAfterWrap = spellingSearchRange->endContainer(); int searchEndOffsetAfterWrap = spellingSearchRange->endOffset(); int misspellingOffset = 0; @@ -1985,7 +1951,7 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) String foundItem; RefPtr<Range> firstMisspellingRange; if (unifiedTextCheckerEnabled()) { - grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); + grammarSearchRange = spellingSearchRange->cloneRange(); foundItem = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); if (isSpelling) { misspelledWord = foundItem; @@ -1998,12 +1964,12 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) misspelledWord = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); #if USE(GRAMMAR_CHECKING) - grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); + grammarSearchRange = spellingSearchRange->cloneRange(); if (!misspelledWord.isEmpty()) { // Stop looking at start of next misspelled word - CharacterIterator chars(grammarSearchRange.get()); + CharacterIterator chars(*grammarSearchRange); chars.advance(misspellingOffset); - grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset(), IGNORE_EXCEPTION); + grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset()); } if (isGrammarCheckingEnabled()) @@ -2014,12 +1980,13 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) // If we found neither bad grammar nor a misspelled word, wrap and try again (but don't bother if we started at the beginning of the // block rather than at a selection). if (startedWithSelection && !misspelledWord && !badGrammarPhrase) { - spellingSearchRange->setStart(topNode, 0, IGNORE_EXCEPTION); + if (topNode) + spellingSearchRange->setStart(*topNode, 0); // going until the end of the very first chunk we tested is far enough - spellingSearchRange->setEnd(searchEndNodeAfterWrap, searchEndOffsetAfterWrap, IGNORE_EXCEPTION); + spellingSearchRange->setEnd(searchEndNodeAfterWrap, searchEndOffsetAfterWrap); if (unifiedTextCheckerEnabled()) { - grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); + grammarSearchRange = spellingSearchRange->cloneRange(); foundItem = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); if (isSpelling) { misspelledWord = foundItem; @@ -2032,12 +1999,12 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) misspelledWord = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); #if USE(GRAMMAR_CHECKING) - grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); + grammarSearchRange = spellingSearchRange->cloneRange(); if (!misspelledWord.isEmpty()) { // Stop looking at start of next misspelled word - CharacterIterator chars(grammarSearchRange.get()); + CharacterIterator chars(*grammarSearchRange); chars.advance(misspellingOffset); - grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset(), IGNORE_EXCEPTION); + grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset()); } if (isGrammarCheckingEnabled()) @@ -2060,7 +2027,7 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) // FIXME 4859190: This gets confused with doubled punctuation at the end of a paragraph RefPtr<Range> badGrammarRange = TextIterator::subrange(grammarSearchRange.get(), grammarPhraseOffset + grammarDetail.location, grammarDetail.length); - m_frame.selection().setSelection(VisibleSelection(badGrammarRange.get(), SEL_DEFAULT_AFFINITY)); + m_frame.selection().setSelection(VisibleSelection(*badGrammarRange, SEL_DEFAULT_AFFINITY)); m_frame.selection().revealSelection(); client()->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail); @@ -2072,13 +2039,14 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) // a marker so we draw the red squiggle later. RefPtr<Range> misspellingRange = TextIterator::subrange(spellingSearchRange.get(), misspellingOffset, misspelledWord.length()); - m_frame.selection().setSelection(VisibleSelection(misspellingRange.get(), DOWNSTREAM)); + m_frame.selection().setSelection(VisibleSelection(*misspellingRange, DOWNSTREAM)); m_frame.selection().revealSelection(); client()->updateSpellingUIWithMisspelledWord(misspelledWord); document().markers().addMarker(misspellingRange.get(), DocumentMarker::Spelling); } } + #endif // !PLATFORM(IOS) String Editor::misspelledWordAtCaretOrRange(Node* clickedNode) const @@ -2106,7 +2074,7 @@ String Editor::misspelledWordAtCaretOrRange(Node* clickedNode) const int wordLength = word.length(); int misspellingLocation = -1; int misspellingLength = 0; - textChecker()->checkSpellingOfString(word.deprecatedCharacters(), wordLength, &misspellingLocation, &misspellingLength); + textChecker()->checkSpellingOfString(word, &misspellingLocation, &misspellingLength); return misspellingLength == wordLength ? word : String(); } @@ -2120,7 +2088,7 @@ String Editor::misspelledSelectionString() const int misspellingLocation = -1; int misspellingLength = 0; - textChecker()->checkSpellingOfString(selectedString.deprecatedCharacters(), length, &misspellingLocation, &misspellingLength); + textChecker()->checkSpellingOfString(selectedString, &misspellingLocation, &misspellingLength); // The selection only counts as misspelled if the selected text is exactly one misspelled word if (misspellingLength != length) @@ -2153,7 +2121,7 @@ Vector<String> Editor::guessesForMisspelledWord(const String& word) const Vector<String> guesses; if (client()) - textChecker()->getGuessesForWord(word, String(), guesses); + textChecker()->getGuessesForWord(word, String(), m_frame.selection().selection(), guesses); return guesses; } @@ -2161,13 +2129,13 @@ Vector<String> Editor::guessesForMisspelledOrUngrammatical(bool& misspelled, boo { if (unifiedTextCheckerEnabled()) { RefPtr<Range> range; - FrameSelection& frameSelection = m_frame.selection(); - if (frameSelection.isCaret() && behavior().shouldAllowSpellingSuggestionsWithoutSelection()) { - VisibleSelection wordSelection = VisibleSelection(frameSelection.base()); + VisibleSelection selection = m_frame.selection().selection(); + if (selection.isCaret() && behavior().shouldAllowSpellingSuggestionsWithoutSelection()) { + VisibleSelection wordSelection = VisibleSelection(selection.base()); wordSelection.expandUsingGranularity(WordGranularity); range = wordSelection.toNormalizedRange(); } else - range = frameSelection.toNormalizedRange(); + range = selection.toNormalizedRange(); if (!range) return Vector<String>(); return TextCheckingHelper(client(), range).guessesForMisspelledOrUngrammaticalRange(isGrammarCheckingEnabled(), misspelled, ungrammatical); @@ -2224,6 +2192,8 @@ void Editor::markMisspellingsAndBadGrammar(const VisibleSelection &movingSelecti void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping, bool doReplacement) { + Ref<Frame> protection(m_frame); + #if PLATFORM(IOS) UNUSED_PARAM(selectionAfterTyping); UNUSED_PARAM(doReplacement); @@ -2289,19 +2259,19 @@ void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, // If autocorrected word is non empty, replace the misspelled word by this word. if (!autocorrectedString.isEmpty()) { - VisibleSelection newSelection(misspellingRange.get(), DOWNSTREAM); + VisibleSelection newSelection(*misspellingRange, DOWNSTREAM); if (newSelection != m_frame.selection().selection()) { if (!m_frame.selection().shouldChangeSelection(newSelection)) return; m_frame.selection().setSelection(newSelection); } - if (!m_frame.editor().shouldInsertText(autocorrectedString, misspellingRange.get(), EditorInsertActionTyped)) + if (!m_frame.editor().shouldInsertText(autocorrectedString, misspellingRange.get(), EditorInsertAction::Typed)) return; - m_frame.editor().replaceSelectionWithText(autocorrectedString, false, false); + m_frame.editor().replaceSelectionWithText(autocorrectedString, false, false, EditActionInsert); // Reset the charet one character further. - m_frame.selection().moveTo(m_frame.selection().end()); + m_frame.selection().moveTo(m_frame.selection().selection().end()); m_frame.selection().modify(FrameSelection::AlterationMove, DirectionForward, CharacterGranularity); } @@ -2329,11 +2299,11 @@ void Editor::markMisspellingsOrBadGrammar(const VisibleSelection& selection, boo return; // If we're not in an editable node, bail. - Node* editableNode = searchRange->startContainer(); - if (!editableNode || !editableNode->hasEditableStyle()) + Node& editableNode = searchRange->startContainer(); + if (!editableNode.hasEditableStyle()) return; - if (!isSpellCheckingEnabledFor(editableNode)) + if (!isSpellCheckingEnabledFor(&editableNode)) return; // Get the spell checker if it is available @@ -2362,15 +2332,19 @@ bool Editor::isSpellCheckingEnabledFor(Node* node) const { if (!node) return false; - const Element* focusedElement = node->isElementNode() ? toElement(node) : node->parentElement(); - if (!focusedElement) + Element* element = is<Element>(*node) ? downcast<Element>(node) : node->parentElement(); + if (!element) return false; - return focusedElement->isSpellCheckingEnabled(); + if (element->isInUserAgentShadowTree()) { + if (HTMLTextFormControlElement* textControl = enclosingTextFormControl(firstPositionInOrBeforeNode(element))) + return textControl->isSpellCheckingEnabled(); + } + return element->isSpellCheckingEnabled(); } bool Editor::isSpellCheckingEnabledInFocusedNode() const { - return isSpellCheckingEnabledFor(m_frame.selection().start().deprecatedNode()); + return isSpellCheckingEnabledFor(m_frame.selection().selection().start().deprecatedNode()); } void Editor::markMisspellings(const VisibleSelection& selection, RefPtr<Range>& firstMisspellingRange) @@ -2403,33 +2377,33 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask textC return; // If we're not in an editable node, bail. - Node* editableNode = spellingRange->startContainer(); - if (!editableNode || !editableNode->hasEditableStyle()) + Node& editableNode = spellingRange->startContainer(); + if (!editableNode.hasEditableStyle()) return; - if (!isSpellCheckingEnabledFor(editableNode)) + if (!isSpellCheckingEnabledFor(&editableNode)) return; Range* rangeToCheck = shouldMarkGrammar ? grammarRange : spellingRange; TextCheckingParagraph paragraphToCheck(rangeToCheck); - if (paragraphToCheck.isRangeEmpty() || paragraphToCheck.isEmpty()) + if (paragraphToCheck.isEmpty()) return; RefPtr<Range> paragraphRange = paragraphToCheck.paragraphRange(); bool asynchronous = m_frame.settings().asynchronousSpellCheckingEnabled() && !shouldShowCorrectionPanel; // In asynchronous mode, we intentionally check paragraph-wide sentence. - RefPtr<SpellCheckRequest> request = SpellCheckRequest::create(resolveTextCheckingTypeMask(textCheckingOptions), TextCheckingProcessIncremental, asynchronous ? paragraphRange : rangeToCheck, paragraphRange); + const auto resolvedOptions = resolveTextCheckingTypeMask(editableNode, textCheckingOptions); + auto request = SpellCheckRequest::create(resolvedOptions, TextCheckingProcessIncremental, asynchronous ? paragraphRange : rangeToCheck, paragraphRange); if (asynchronous) { - m_spellChecker->requestCheckingFor(request); + m_spellChecker->requestCheckingFor(WTFMove(request)); return; } Vector<TextCheckingResult> results; - checkTextOfParagraph(textChecker(), paragraphToCheck.textDeprecatedCharacters(), paragraphToCheck.textLength(), - resolveTextCheckingTypeMask(textCheckingOptions), results); - markAndReplaceFor(request, results); + checkTextOfParagraph(*textChecker(), paragraphToCheck.text(), resolvedOptions, results, m_frame.selection().selection()); + markAndReplaceFor(WTFMove(request), results); } static bool isAutomaticTextReplacementType(TextCheckingType type) @@ -2453,24 +2427,25 @@ static bool isAutomaticTextReplacementType(TextCheckingType type) static void correctSpellcheckingPreservingTextCheckingParagraph(TextCheckingParagraph& paragraph, PassRefPtr<Range> rangeToReplace, const String& replacement, int resultLocation, int resultLength) { - ContainerNode* scope = toContainerNode(highestAncestor(paragraph.paragraphRange()->startContainer())); + auto& scope = downcast<ContainerNode>(paragraph.paragraphRange()->startContainer().rootNode()); size_t paragraphLocation; size_t paragraphLength; - TextIterator::getLocationAndLengthFromRange(scope, paragraph.paragraphRange().get(), paragraphLocation, paragraphLength); + TextIterator::getLocationAndLengthFromRange(&scope, paragraph.paragraphRange().get(), paragraphLocation, paragraphLength); applyCommand(SpellingCorrectionCommand::create(rangeToReplace, replacement)); // TextCheckingParagraph may be orphaned after SpellingCorrectionCommand mutated DOM. // See <rdar://10305315>, http://webkit.org/b/89526. - RefPtr<Range> newParagraphRange = TextIterator::rangeFromLocationAndLength(scope, paragraphLocation, paragraphLength + replacement.length() - resultLength); + RefPtr<Range> newParagraphRange = TextIterator::rangeFromLocationAndLength(&scope, paragraphLocation, paragraphLength + replacement.length() - resultLength); paragraph = TextCheckingParagraph(TextIterator::subrange(newParagraphRange.get(), resultLocation, replacement.length()), newParagraphRange); } void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vector<TextCheckingResult>& results) { + Ref<Frame> protection(m_frame); ASSERT(request); TextCheckingTypeMask textCheckingOptions = request->data().mask(); @@ -2479,7 +2454,7 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect const bool shouldMarkSpelling = textCheckingOptions & TextCheckingTypeSpelling; const bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; const bool shouldMarkLink = textCheckingOptions & TextCheckingTypeLink; - const bool shouldPerformReplacement = textCheckingOptions & TextCheckingTypeReplacement; + const bool shouldPerformReplacement = textCheckingOptions & (TextCheckingTypeQuote | TextCheckingTypeDash | TextCheckingTypeReplacement); const bool shouldShowCorrectionPanel = textCheckingOptions & TextCheckingTypeShowCorrectionPanel; const bool shouldCheckForCorrection = shouldShowCorrectionPanel || (textCheckingOptions & TextCheckingTypeCorrection); #if !USE(AUTOCORRECTION_PANEL) @@ -2494,10 +2469,10 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect bool adjustSelectionForParagraphBoundaries = false; if (shouldPerformReplacement || shouldMarkSpelling || shouldCheckForCorrection) { - if (m_frame.selection().selectionType() == VisibleSelection::CaretSelection) { + if (m_frame.selection().selection().selectionType() == VisibleSelection::CaretSelection) { // Attempt to save the caret position so we can restore it later if needed - Position caretPosition = m_frame.selection().end(); - selectionOffset = paragraph.offsetTo(caretPosition, ASSERT_NO_EXCEPTION); + Position caretPosition = m_frame.selection().selection().end(); + selectionOffset = paragraph.offsetTo(caretPosition).releaseReturnValue(); restoreSelectionAfterChange = true; if (selectionOffset > 0 && (selectionOffset > paragraph.textLength() || paragraph.textCharAt(selectionOffset - 1) == newlineCharacter)) adjustSelectionForParagraphBoundaries = true; @@ -2515,7 +2490,7 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect const int resultLength = results[i].length; const int resultEndLocation = resultLocation + resultLength; const String& replacement = results[i].replacement; - const bool resultEndsAtAmbiguousBoundary = useAmbiguousBoundaryOffset && resultEndLocation == selectionOffset - 1; + const bool resultEndsAtAmbiguousBoundary = useAmbiguousBoundaryOffset && selectionOffset - 1 <= resultEndLocation; // Only mark misspelling if: // 1. Current text checking isn't done for autocorrection, in which case shouldMarkSpelling is false. @@ -2528,16 +2503,14 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect RefPtr<Range> misspellingRange = paragraph.subrange(resultLocation, resultLength); if (!m_alternativeTextController->isSpellingMarkerAllowed(misspellingRange)) continue; - misspellingRange->startContainer()->document().markers().addMarker(misspellingRange.get(), DocumentMarker::Spelling, replacement); + misspellingRange->startContainer().document().markers().addMarker(misspellingRange.get(), DocumentMarker::Spelling, replacement); } else if (shouldMarkGrammar && resultType == TextCheckingTypeGrammar && paragraph.checkingRangeCovers(resultLocation, resultLength)) { ASSERT(resultLength > 0 && resultLocation >= 0); - const Vector<GrammarDetail>& details = results[i].details; - for (unsigned j = 0; j < details.size(); j++) { - const GrammarDetail& detail = details[j]; + for (auto& detail : results[i].details) { ASSERT(detail.length > 0 && detail.location >= 0); if (paragraph.checkingRangeCovers(resultLocation + detail.location, detail.length)) { RefPtr<Range> badGrammarRange = paragraph.subrange(resultLocation + detail.location, detail.length); - badGrammarRange->startContainer()->document().markers().addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail.userDescription); + badGrammarRange->startContainer().document().markers().addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail.userDescription); } } } else if (resultEndLocation <= spellingRangeEndOffset && resultEndLocation >= paragraph.checkingStart() @@ -2578,7 +2551,7 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect continue; } - VisibleSelection selectionToReplace(rangeToReplace.get(), DOWNSTREAM); + VisibleSelection selectionToReplace(*rangeToReplace, DOWNSTREAM); if (selectionToReplace != m_frame.selection().selection()) { if (!m_frame.selection().shouldChangeSelection(selectionToReplace)) continue; @@ -2590,7 +2563,7 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect restoreSelectionAfterChange = false; if (canEditRichly()) applyCommand(CreateLinkCommand::create(document(), replacement)); - } else if (canEdit() && shouldInsertText(replacement, rangeToReplace.get(), EditorInsertActionTyped)) { + } else if (canEdit() && shouldInsertText(replacement, rangeToReplace.get(), EditorInsertAction::Typed)) { correctSpellcheckingPreservingTextCheckingParagraph(paragraph, rangeToReplace, replacement, resultLocation, resultLength); if (AXObjectCache* cache = document().existingAXObjectCache()) { @@ -2607,9 +2580,13 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect if (resultLocation < selectionOffset) selectionOffset += replacement.length() - resultLength; - // Add a marker so that corrections can easily be undone and won't be re-corrected. - if (resultType == TextCheckingTypeCorrection) - m_alternativeTextController->markCorrection(paragraph.subrange(resultLocation, replacement.length()), replacedString); + if (resultType == TextCheckingTypeCorrection) { + RefPtr<Range> replacementRange = paragraph.subrange(resultLocation, replacement.length()); + m_alternativeTextController->recordAutocorrectionResponse(AutocorrectionResponse::Accepted, replacedString, replacementRange); + + // Add a marker so that corrections can easily be undone and won't be re-corrected. + m_alternativeTextController->markCorrection(WTFMove(replacementRange), replacedString); + } } } } @@ -2625,7 +2602,7 @@ void Editor::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vect m_frame.selection().modify(FrameSelection::AlterationMove, DirectionForward, CharacterGranularity); } else { // If this fails for any reason, the fallback is to go one position beyond the last replacement - m_frame.selection().moveTo(m_frame.selection().end()); + m_frame.selection().moveTo(m_frame.selection().selection().end()); m_frame.selection().modify(FrameSelection::AlterationMove, DirectionForward, CharacterGranularity); } } @@ -2640,14 +2617,14 @@ void Editor::changeBackToReplacedString(const String& replacedString) return; RefPtr<Range> selection = selectedRange(); - if (!shouldInsertText(replacedString, selection.get(), EditorInsertActionPasted)) + if (!shouldInsertText(replacedString, selection.get(), EditorInsertAction::Pasted)) return; - m_alternativeTextController->recordAutocorrectionResponseReversed(replacedString, selection); + m_alternativeTextController->recordAutocorrectionResponse(AutocorrectionResponse::Reverted, replacedString, selection); TextCheckingParagraph paragraph(selection); - replaceSelectionWithText(replacedString, false, false); + replaceSelectionWithText(replacedString, false, false, EditActionInsert); RefPtr<Range> changedRange = paragraph.subrange(paragraph.checkingStart(), replacedString.length()); - changedRange->startContainer()->document().markers().addMarker(changedRange.get(), DocumentMarker::Replacement, String()); + changedRange->startContainer().document().markers().addMarker(changedRange.get(), DocumentMarker::Replacement, String()); m_alternativeTextController->markReversed(changedRange.get()); #else ASSERT_NOT_REACHED(); @@ -2749,9 +2726,9 @@ void Editor::updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionA // of marker that contains the word in question, and remove marker on that whole range. RefPtr<Range> wordRange = Range::create(document(), startOfFirstWord.deepEquivalent(), endOfLastWord.deepEquivalent()); - Vector<DocumentMarker*> markers = document().markers().markersInRange(wordRange.get(), DocumentMarker::DictationAlternatives); - for (size_t i = 0; i < markers.size(); ++i) - m_alternativeTextController->removeDictationAlternativesForMarker(markers[i]); + Vector<RenderedDocumentMarker*> markers = document().markers().markersInRange(wordRange.get(), DocumentMarker::DictationAlternatives); + for (auto* marker : markers) + m_alternativeTextController->removeDictationAlternativesForMarker(*marker); #if PLATFORM(IOS) document().markers().removeMarkers(wordRange.get(), DocumentMarker::Spelling | DocumentMarker::CorrectionIndicator | DocumentMarker::SpellCheckingExemption | DocumentMarker::DictationAlternatives | DocumentMarker::DictationPhraseWithAlternatives, DocumentMarkerController::RemovePartiallyOverlappingMarker); @@ -2780,7 +2757,7 @@ PassRefPtr<Range> Editor::rangeForPoint(const IntPoint& windowPoint) IntPoint framePoint = frameView->windowToContents(windowPoint); VisibleSelection selection(frame->visiblePositionForPoint(framePoint)); - return avoidIntersectionWithDeleteButtonController(selection.toNormalizedRange().get()); + return selection.toNormalizedRange(); } void Editor::revealSelectionAfterEditingOperation(const ScrollAlignment& alignment, RevealExtentOption revealExtentOption) @@ -2788,33 +2765,39 @@ void Editor::revealSelectionAfterEditingOperation(const ScrollAlignment& alignme if (m_ignoreCompositionSelectionChange) return; - m_frame.selection().revealSelection(alignment, revealExtentOption); +#if PLATFORM(IOS) + SelectionRevealMode revealMode = SelectionRevealMode::RevealUpToMainFrame; +#else + SelectionRevealMode revealMode = SelectionRevealMode::Reveal; +#endif + + m_frame.selection().revealSelection(revealMode, alignment, revealExtentOption); } -void Editor::setIgnoreCompositionSelectionChange(bool ignore) +void Editor::setIgnoreCompositionSelectionChange(bool ignore, RevealSelection shouldRevealExistingSelection) { if (m_ignoreCompositionSelectionChange == ignore) return; m_ignoreCompositionSelectionChange = ignore; #if PLATFORM(IOS) - // FIXME: Merge this to open source https://bugs.webkit.org/show_bug.cgi?id=38830 + // FIXME: Should suppress selection change notifications during a composition change <https://webkit.org/b/38830> if (!ignore) - notifyComponentsOnChangedSelection(m_frame.selection().selection(), 0); + respondToChangedSelection(m_frame.selection().selection(), 0); #endif - if (!ignore) + if (!ignore && shouldRevealExistingSelection == RevealSelection::Yes) revealSelectionAfterEditingOperation(ScrollAlignment::alignToEdgeIfNeeded, RevealExtent); } -PassRefPtr<Range> Editor::compositionRange() const +RefPtr<Range> Editor::compositionRange() const { if (!m_compositionNode) - return 0; + return nullptr; unsigned length = m_compositionNode->length(); unsigned start = std::min(m_compositionStart, length); unsigned end = std::min(std::max(start, m_compositionEnd), length); if (start >= end) - return 0; + return nullptr; return Range::create(m_compositionNode->document(), m_compositionNode.get(), start, m_compositionNode.get(), end); } @@ -2822,10 +2805,11 @@ bool Editor::getCompositionSelection(unsigned& selectionStart, unsigned& selecti { if (!m_compositionNode) return false; - Position start = m_frame.selection().start(); + const VisibleSelection& selection = m_frame.selection().selection(); + Position start = selection.start(); if (start.deprecatedNode() != m_compositionNode) return false; - Position end = m_frame.selection().end(); + Position end = selection.end(); if (end.deprecatedNode() != m_compositionNode) return false; @@ -2860,7 +2844,7 @@ void Editor::transpose() RefPtr<Range> range = makeRange(previous, next); if (!range) return; - VisibleSelection newSelection(range.get(), DOWNSTREAM); + VisibleSelection newSelection(*range, DOWNSTREAM); // Transpose the two characters. String text = plainText(range.get()); @@ -2876,22 +2860,34 @@ void Editor::transpose() } // Insert the transposed characters. - if (!shouldInsertText(transposed, range.get(), EditorInsertActionTyped)) + if (!shouldInsertText(transposed, range.get(), EditorInsertAction::Typed)) return; - replaceSelectionWithText(transposed, false, false); + replaceSelectionWithText(transposed, false, false, EditActionInsert); +} + +void Editor::addRangeToKillRing(const Range& range, KillRingInsertionMode mode) +{ + addTextToKillRing(plainText(&range), mode); } -void Editor::addToKillRing(Range* range, bool prepend) +void Editor::addTextToKillRing(const String& text, KillRingInsertionMode mode) { if (m_shouldStartNewKillRingSequence) killRing().startNewSequence(); - String text = plainText(range); - if (prepend) + m_shouldStartNewKillRingSequence = false; + + // If the kill was from a backwards motion, prepend to the kill ring. + // This will ensure that alternating forward and backward kills will + // build up the original string in the kill ring without permuting it. + switch (mode) { + case KillRingInsertionMode::PrependText: killRing().prepend(text); - else + break; + case KillRingInsertionMode::AppendText: killRing().append(text); - m_shouldStartNewKillRingSequence = false; + break; + } } void Editor::startAlternativeTextUITimer() @@ -2910,8 +2906,10 @@ void Editor::dismissCorrectionPanelAsIgnored() m_alternativeTextController->dismiss(ReasonForDismissingAlternativeTextIgnored); } -void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, FrameSelection::SetSelectionOptions options) +void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, FrameSelection::SetSelectionOptions options) { + Ref<Frame> protection(m_frame); + // If the new selection is orphaned, then don't update the selection. if (newSelection.start().isOrphan() || newSelection.end().isOrphan()) return; @@ -2932,7 +2930,7 @@ void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, // does not call EditorClient::respondToChangedSelection(), which, on the Mac, sends selection change notifications and // starts a new kill ring sequence, but we want to do these things (matches AppKit). #if PLATFORM(IOS) - // FIXME: Merge this to open source https://bugs.webkit.org/show_bug.cgi?id=38830 + // FIXME: Should suppress selection change notifications during a composition change <https://webkit.org/b/38830> if (m_ignoreCompositionSelectionChange) return; #endif @@ -2945,11 +2943,9 @@ String Editor::selectedText() const return selectedText(TextIteratorDefaultBehavior); } -String Editor::selectedTextForClipboard() const +String Editor::selectedTextForDataTransfer() const { - if (m_frame.settings().selectionIncludesAltImageText()) - return selectedText(TextIteratorEmitsImageAltText); - return selectedText(); + return selectedText(TextIteratorEmitsImageAltText); } String Editor::selectedText(TextIteratorBehavior behavior) const @@ -2969,12 +2965,9 @@ static inline void collapseCaretWidth(IntRect& rect) IntRect Editor::firstRectForRange(Range* range) const { - ASSERT(range->startContainer()); - ASSERT(range->endContainer()); - VisiblePosition startVisiblePosition(range->startPosition(), DOWNSTREAM); - if (range->collapsed(ASSERT_NO_EXCEPTION)) { + if (range->collapsed()) { // FIXME: Getting caret rect and removing caret width is a very roundabout way to get collapsed range location. // In particular, width adjustment doesn't work for rotated text. IntRect startCaretRect = RenderedPosition(startVisiblePosition).absoluteRect(); @@ -3014,22 +3007,20 @@ bool Editor::shouldChangeSelection(const VisibleSelection& oldSelection, const V return client() && client()->shouldChangeSelectedRange(oldSelection.toNormalizedRange().get(), newSelection.toNormalizedRange().get(), affinity, stillSelecting); } -void Editor::computeAndSetTypingStyle(StyleProperties* style, EditAction editingAction) +void Editor::computeAndSetTypingStyle(EditingStyle& style, EditAction editingAction) { - if (!style || style->isEmpty()) { + if (style.isEmpty()) { m_frame.selection().clearTypingStyle(); return; } // Calculate the current typing style. RefPtr<EditingStyle> typingStyle; - if (m_frame.selection().typingStyle()) { - typingStyle = m_frame.selection().typingStyle()->copy(); - typingStyle->overrideWithStyle(style); - } else - typingStyle = EditingStyle::create(style); - - typingStyle->prepareToApplyAt(m_frame.selection().selection().visibleStart().deepEquivalent(), EditingStyle::PreserveWritingDirection); + if (auto existingTypingStyle = m_frame.selection().typingStyle()) + typingStyle = existingTypingStyle->copy(); + else + typingStyle = EditingStyle::create(); + typingStyle->overrideTypingStyleAt(style, m_frame.selection().selection().visibleStart().deepEquivalent()); // Handle block styles, substracting these from the typing style. RefPtr<EditingStyle> blockStyle = typingStyle->extractAndRemoveBlockProperties(); @@ -3040,6 +3031,11 @@ void Editor::computeAndSetTypingStyle(StyleProperties* style, EditAction editing m_frame.selection().setTypingStyle(typingStyle); } +void Editor::computeAndSetTypingStyle(StyleProperties& properties, EditAction editingAction) +{ + return computeAndSetTypingStyle(EditingStyle::create(&properties), editingAction); +} + void Editor::textFieldDidBeginEditing(Element* e) { if (client()) @@ -3081,29 +3077,29 @@ void Editor::textDidChangeInTextArea(Element* e) void Editor::applyEditingStyleToBodyElement() const { - RefPtr<NodeList> list = document().getElementsByTagName("body"); - unsigned len = list->length(); - for (unsigned i = 0; i < len; i++) - applyEditingStyleToElement(toElement(list->item(i))); + auto collection = document().getElementsByTagName(HTMLNames::bodyTag.localName()); + unsigned length = collection->length(); + for (unsigned i = 0; i < length; ++i) + applyEditingStyleToElement(collection->item(i)); } void Editor::applyEditingStyleToElement(Element* element) const { - if (!element) - return; - ASSERT(element->isStyledElement()); - if (!element->isStyledElement()) + ASSERT(!element || is<StyledElement>(*element)); + if (!is<StyledElement>(element)) return; // Mutate using the CSSOM wrapper so we get the same event behavior as a script. - CSSStyleDeclaration* style = toStyledElement(element)->style(); - style->setPropertyInternal(CSSPropertyWordWrap, "break-word", false, IGNORE_EXCEPTION); - style->setPropertyInternal(CSSPropertyWebkitNbspMode, "space", false, IGNORE_EXCEPTION); - style->setPropertyInternal(CSSPropertyWebkitLineBreak, "after-white-space", false, IGNORE_EXCEPTION); + CSSStyleDeclaration* style = downcast<StyledElement>(*element).cssomStyle(); + style->setPropertyInternal(CSSPropertyWordWrap, "break-word", false); + style->setPropertyInternal(CSSPropertyWebkitNbspMode, "space", false); + style->setPropertyInternal(CSSPropertyWebkitLineBreak, "after-white-space", false); } bool Editor::findString(const String& target, FindOptions options) { + Ref<Frame> protection(m_frame); + VisibleSelection selection = m_frame.selection().selection(); RefPtr<Range> resultRange = rangeOfString(target, selection.firstRange().get(), options); @@ -3111,27 +3107,18 @@ bool Editor::findString(const String& target, FindOptions options) if (!resultRange) return false; - m_frame.selection().setSelection(VisibleSelection(resultRange.get(), DOWNSTREAM)); - m_frame.selection().revealSelection(); - return true; -} - -PassRefPtr<Range> Editor::findStringAndScrollToVisible(const String& target, Range* previousMatch, FindOptions options) -{ - RefPtr<Range> nextMatch = rangeOfString(target, previousMatch, options); - if (!nextMatch) - return 0; + m_frame.selection().setSelection(VisibleSelection(*resultRange, DOWNSTREAM)); - nextMatch->firstNode()->renderer()->scrollRectToVisible(nextMatch->boundingBox(), - ScrollAlignment::alignCenterIfNeeded, ScrollAlignment::alignCenterIfNeeded); + if (!(options & DoNotRevealSelection)) + m_frame.selection().revealSelection(); - return nextMatch.release(); + return true; } -PassRefPtr<Range> Editor::rangeOfString(const String& target, Range* referenceRange, FindOptions options) +RefPtr<Range> Editor::rangeOfString(const String& target, Range* referenceRange, FindOptions options) { if (target.isEmpty()) - return 0; + return nullptr; // Start from an edge of the reference range, if there's a reference range that's not in shadow content. Which edge // is used depends on whether we're searching forward or backward, and whether startInSelection is set. @@ -3146,19 +3133,19 @@ PassRefPtr<Range> Editor::rangeOfString(const String& target, Range* referenceRa searchRange->setEnd(startInReferenceRange ? referenceRange->endPosition() : referenceRange->startPosition()); } - RefPtr<Node> shadowTreeRoot = referenceRange && referenceRange->startContainer() ? referenceRange->startContainer()->nonBoundaryShadowTreeRootNode() : 0; + RefPtr<ShadowRoot> shadowTreeRoot = referenceRange ? referenceRange->startContainer().containingShadowRoot() : nullptr; if (shadowTreeRoot) { if (forward) - searchRange->setEnd(shadowTreeRoot.get(), shadowTreeRoot->childNodeCount()); + searchRange->setEnd(*shadowTreeRoot, shadowTreeRoot->countChildNodes()); else - searchRange->setStart(shadowTreeRoot.get(), 0); + searchRange->setStart(*shadowTreeRoot, 0); } - RefPtr<Range> resultRange(findPlainText(searchRange.get(), target, options)); + RefPtr<Range> resultRange = findPlainText(*searchRange, target, options); // If we started in the reference range and the found range exactly matches the reference range, find again. // Build a selection with the found range to remove collapsed whitespace. // Compare ranges instead of selection objects to ignore the way that the current selection was made. - if (startInReferenceRange && areRangesEqual(VisibleSelection(resultRange.get()).toNormalizedRange().get(), referenceRange)) { + if (startInReferenceRange && areRangesEqual(VisibleSelection(*resultRange).toNormalizedRange().get(), referenceRange)) { searchRange = rangeOfContents(document()); if (forward) searchRange->setStart(referenceRange->endPosition()); @@ -3167,48 +3154,49 @@ PassRefPtr<Range> Editor::rangeOfString(const String& target, Range* referenceRa if (shadowTreeRoot) { if (forward) - searchRange->setEnd(shadowTreeRoot.get(), shadowTreeRoot->childNodeCount()); + searchRange->setEnd(*shadowTreeRoot, shadowTreeRoot->countChildNodes()); else - searchRange->setStart(shadowTreeRoot.get(), 0); + searchRange->setStart(*shadowTreeRoot, 0); } - resultRange = findPlainText(searchRange.get(), target, options); + resultRange = findPlainText(*searchRange, target, options); } // If nothing was found in the shadow tree, search in main content following the shadow tree. - if (resultRange->collapsed(ASSERT_NO_EXCEPTION) && shadowTreeRoot) { + if (resultRange->collapsed() && shadowTreeRoot) { searchRange = rangeOfContents(document()); - if (forward) - searchRange->setStartAfter(shadowTreeRoot->shadowHost()); - else - searchRange->setEndBefore(shadowTreeRoot->shadowHost()); + if (shadowTreeRoot->shadowHost()) { + if (forward) + searchRange->setStartAfter(*shadowTreeRoot->shadowHost()); + else + searchRange->setEndBefore(*shadowTreeRoot->shadowHost()); + } - resultRange = findPlainText(searchRange.get(), target, options); + resultRange = findPlainText(*searchRange, target, options); } // If we didn't find anything and we're wrapping, search again in the entire document (this will // redundantly re-search the area already searched in some cases). - if (resultRange->collapsed(ASSERT_NO_EXCEPTION) && options & WrapAround) { + if (resultRange->collapsed() && options & WrapAround) { searchRange = rangeOfContents(document()); - resultRange = findPlainText(searchRange.get(), target, options); + resultRange = findPlainText(*searchRange, target, options); // We used to return false here if we ended up with the same range that we started with // (e.g., the reference range was already the only instance of this text). But we decided that // this should be a success case instead, so we'll just fall through in that case. } - return resultRange->collapsed(ASSERT_NO_EXCEPTION) ? 0 : resultRange.release(); + return resultRange->collapsed() ? nullptr : resultRange; } -static bool isFrameInRange(Frame* frame, Range* range) +static bool isFrameInRange(Frame& frame, Range& range) { - bool inRange = false; - for (HTMLFrameOwnerElement* ownerElement = frame->ownerElement(); ownerElement; ownerElement = ownerElement->document().ownerElement()) { - if (&ownerElement->document() == &range->ownerDocument()) { - inRange = range->intersectsNode(ownerElement, IGNORE_EXCEPTION); - break; + for (auto* ownerElement = frame.ownerElement(); ownerElement; ownerElement = ownerElement->document().ownerElement()) { + if (&ownerElement->document() == &range.ownerDocument()) { + auto result = range.intersectsNode(*ownerElement); + return !result.hasException() && result.releaseReturnValue(); } } - return inRange; + return false; } unsigned Editor::countMatchesForText(const String& target, Range* range, FindOptions options, unsigned limit, bool markMatches, Vector<RefPtr<Range>>* matches) @@ -3220,24 +3208,24 @@ unsigned Editor::countMatchesForText(const String& target, Range* range, FindOpt if (range) { if (&range->ownerDocument() == &document()) searchRange = range; - else if (!isFrameInRange(&m_frame, range)) + else if (!isFrameInRange(m_frame, *range)) return 0; } if (!searchRange) searchRange = rangeOfContents(document()); - Node* originalEndContainer = searchRange->endContainer(); + Node& originalEndContainer = searchRange->endContainer(); int originalEndOffset = searchRange->endOffset(); unsigned matchCount = 0; do { - RefPtr<Range> resultRange(findPlainText(searchRange.get(), target, options & ~Backwards)); - if (resultRange->collapsed(IGNORE_EXCEPTION)) { - if (!resultRange->startContainer()->isInShadowTree()) + RefPtr<Range> resultRange(findPlainText(*searchRange, target, options & ~Backwards)); + if (resultRange->collapsed()) { + if (!resultRange->startContainer().isInShadowTree()) break; - searchRange->setStartAfter(resultRange->startContainer()->shadowHost(), IGNORE_EXCEPTION); - searchRange->setEnd(originalEndContainer, originalEndOffset, IGNORE_EXCEPTION); + searchRange->setStartAfter(*resultRange->startContainer().shadowHost()); + searchRange->setEnd(originalEndContainer, originalEndOffset); continue; } @@ -3256,32 +3244,13 @@ unsigned Editor::countMatchesForText(const String& target, Range* range, FindOpt // result range. There is no need to use a VisiblePosition here, // since findPlainText will use a TextIterator to go over the visible // text nodes. - searchRange->setStart(resultRange->endContainer(IGNORE_EXCEPTION), resultRange->endOffset(IGNORE_EXCEPTION), IGNORE_EXCEPTION); + searchRange->setStart(resultRange->endContainer(), resultRange->endOffset()); Node* shadowTreeRoot = searchRange->shadowRoot(); - if (searchRange->collapsed(IGNORE_EXCEPTION) && shadowTreeRoot) - searchRange->setEnd(shadowTreeRoot, shadowTreeRoot->childNodeCount(), IGNORE_EXCEPTION); + if (searchRange->collapsed() && shadowTreeRoot) + searchRange->setEnd(*shadowTreeRoot, shadowTreeRoot->countChildNodes()); } while (true); - if (markMatches || matches) { - // Do a "fake" paint in order to execute the code that computes the rendered rect for each text match. - if (m_frame.view() && m_frame.contentRenderer()) { - document().updateLayout(); // Ensure layout is up to date. - // FIXME: unclear if we need LegacyIOSDocumentVisibleRect. - // FIXME: this should probably look at paintsEntireContents() - LayoutRect visibleRect = m_frame.view()->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect); - if (!visibleRect.isEmpty()) { - GraphicsContext context((PlatformGraphicsContext*)0); - context.setPaintingDisabled(true); - - PaintBehavior oldBehavior = m_frame.view()->paintBehavior(); - m_frame.view()->setPaintBehavior(oldBehavior | PaintBehaviorFlattenCompositingLayers); - m_frame.view()->paintContents(&context, enclosingIntRect(visibleRect)); - m_frame.view()->setPaintBehavior(oldBehavior); - } - } - } - return matchCount; } @@ -3294,11 +3263,149 @@ void Editor::setMarkedTextMatchesAreHighlighted(bool flag) document().markers().repaintMarkers(DocumentMarker::TextMatch); } -void Editor::respondToChangedSelection(const VisibleSelection& oldSelection, FrameSelection::SetSelectionOptions options) +#if !PLATFORM(MAC) +void Editor::selectionWillChange() { - m_alternativeTextController->stopPendingCorrection(oldSelection); +} +#endif + +void Editor::respondToChangedSelection(const VisibleSelection&, FrameSelection::SetSelectionOptions options) +{ +#if PLATFORM(IOS) + // FIXME: Should suppress selection change notifications during a composition change <https://webkit.org/b/38830> + if (m_ignoreCompositionSelectionChange) + return; +#endif + + if (client()) + client()->respondToChangedSelection(&m_frame); + +#if ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS) + if (shouldDetectTelephoneNumbers()) + m_telephoneNumberDetectionUpdateTimer.startOneShot(0); +#endif + + setStartNewKillRingSequence(true); - bool closeTyping = options & FrameSelection::CloseTyping; + if (m_editorUIUpdateTimer.isActive()) + return; + + // Don't check spelling and grammar if the change of selection is triggered by spelling correction itself. + m_editorUIUpdateTimerShouldCheckSpellingAndGrammar = options & FrameSelection::CloseTyping + && !(options & FrameSelection::SpellCorrectionTriggered); + m_editorUIUpdateTimerWasTriggeredByDictation = options & FrameSelection::DictationTriggered; + m_editorUIUpdateTimer.startOneShot(0); +} + +#if ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS) + +bool Editor::shouldDetectTelephoneNumbers() +{ + if (!m_frame.document()) + return false; + return document().isTelephoneNumberParsingEnabled() && TelephoneNumberDetector::isSupported(); +} + +void Editor::scanSelectionForTelephoneNumbers() +{ + if (!shouldDetectTelephoneNumbers() || !client()) + return; + + m_detectedTelephoneNumberRanges.clear(); + + Vector<RefPtr<Range>> markedRanges; + + FrameSelection& frameSelection = m_frame.selection(); + if (!frameSelection.isRange()) { + m_frame.mainFrame().servicesOverlayController().selectedTelephoneNumberRangesChanged(); + return; + } + RefPtr<Range> selectedRange = frameSelection.toNormalizedRange(); + + // Extend the range a few characters in each direction to detect incompletely selected phone numbers. + static const int charactersToExtend = 15; + const VisibleSelection& visibleSelection = frameSelection.selection(); + Position start = visibleSelection.start(); + Position end = visibleSelection.end(); + for (int i = 0; i < charactersToExtend; ++i) { + start = start.previous(Character); + end = end.next(Character); + } + + FrameSelection extendedSelection; + extendedSelection.setStart(start); + extendedSelection.setEnd(end); + RefPtr<Range> extendedRange = extendedSelection.toNormalizedRange(); + + if (!extendedRange) { + m_frame.mainFrame().servicesOverlayController().selectedTelephoneNumberRangesChanged(); + return; + } + + scanRangeForTelephoneNumbers(*extendedRange, extendedRange->text(), markedRanges); + + // Only consider ranges with a detected telephone number if they overlap with the actual selection range. + for (auto& range : markedRanges) { + if (rangesOverlap(range.get(), selectedRange.get())) + m_detectedTelephoneNumberRanges.append(range); + } + + m_frame.mainFrame().servicesOverlayController().selectedTelephoneNumberRangesChanged(); +} + +void Editor::scanRangeForTelephoneNumbers(Range& range, const StringView& stringView, Vector<RefPtr<Range>>& markedRanges) +{ + // Don't scan for phone numbers inside editable regions. + Node& startNode = range.startContainer(); + if (startNode.hasEditableStyle()) + return; + + // relativeStartPosition and relativeEndPosition are the endpoints of the phone number range, + // relative to the scannerPosition + unsigned length = stringView.length(); + unsigned scannerPosition = 0; + int relativeStartPosition = 0; + int relativeEndPosition = 0; + + auto characters = stringView.upconvertedCharacters(); + + while (scannerPosition < length && TelephoneNumberDetector::find(&characters[scannerPosition], length - scannerPosition, &relativeStartPosition, &relativeEndPosition)) { + // The convention in the Data Detectors framework is that the end position is the first character NOT in the phone number + // (that is, the length of the range is relativeEndPosition - relativeStartPosition). So subtract 1 to get the same + // convention as the old WebCore phone number parser (so that the rest of the code is still valid if we want to go back + // to the old parser). + --relativeEndPosition; + + ASSERT(scannerPosition + relativeEndPosition < length); + + unsigned subrangeOffset = scannerPosition + relativeStartPosition; + unsigned subrangeLength = relativeEndPosition - relativeStartPosition + 1; + + RefPtr<Range> subrange = TextIterator::subrange(&range, subrangeOffset, subrangeLength); + + markedRanges.append(subrange); + range.ownerDocument().markers().addMarker(subrange.get(), DocumentMarker::TelephoneNumber); + + scannerPosition += relativeEndPosition + 1; + } +} + +#endif // ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS) + +void Editor::updateEditorUINowIfScheduled() +{ + if (!m_editorUIUpdateTimer.isActive()) + return; + m_editorUIUpdateTimer.stop(); + editorUIUpdateTimerFired(); +} + +void Editor::editorUIUpdateTimerFired() +{ + VisibleSelection oldSelection = m_oldSelectionForEditorUIUpdate; + + m_alternativeTextController->stopPendingCorrection(oldSelection); + bool isContinuousSpellCheckingEnabled = this->isContinuousSpellCheckingEnabled(); bool isContinuousGrammarCheckingEnabled = isContinuousSpellCheckingEnabled && isGrammarCheckingEnabled(); if (isContinuousSpellCheckingEnabled) { @@ -3325,13 +3432,10 @@ void Editor::respondToChangedSelection(const VisibleSelection& oldSelection, Fra newSelectedSentence = VisibleSelection(startOfSentence(newStart), endOfSentence(newStart)); } - // Don't check spelling and grammar if the change of selection is triggered by spelling correction itself. - bool shouldCheckSpellingAndGrammar = !(options & FrameSelection::SpellCorrectionTriggered); - // When typing we check spelling elsewhere, so don't redo it here. // If this is a change in selection resulting from a delete operation, // oldSelection may no longer be in the document. - if (shouldCheckSpellingAndGrammar && closeTyping && oldSelection.isContentEditable() && oldSelection.start().deprecatedNode() && oldSelection.start().anchorNode()->inDocument()) { + if (m_editorUIUpdateTimerShouldCheckSpellingAndGrammar && oldSelection.isContentEditable() && oldSelection.start().deprecatedNode() && oldSelection.start().anchorNode()->isConnected()) { VisiblePosition oldStart(oldSelection.visibleStart()); VisibleSelection oldAdjacentWords = VisibleSelection(startOfWord(oldStart, LeftWordIfOnBoundary), endOfWord(oldStart, RightWordIfOnBoundary)); if (oldAdjacentWords != newAdjacentWords) { @@ -3359,38 +3463,40 @@ void Editor::respondToChangedSelection(const VisibleSelection& oldSelection, Fra if (!isContinuousGrammarCheckingEnabled) document().markers().removeMarkers(DocumentMarker::Grammar); - notifyComponentsOnChangedSelection(oldSelection, options); + if (!m_editorUIUpdateTimerWasTriggeredByDictation) + m_alternativeTextController->respondToChangedSelection(oldSelection); + + m_oldSelectionForEditorUIUpdate = m_frame.selection().selection(); } static Node* findFirstMarkable(Node* node) { while (node) { if (!node->renderer()) - return 0; + return nullptr; if (node->renderer()->isTextOrLineBreak()) return node; - if (isHTMLTextFormControlElement(*node)) - node = toHTMLTextFormControlElement(node)->visiblePositionForIndex(1).deepEquivalent().deprecatedNode(); + if (is<HTMLTextFormControlElement>(*node)) + node = downcast<HTMLTextFormControlElement>(*node).visiblePositionForIndex(1).deepEquivalent().deprecatedNode(); else if (node->firstChild()) node = node->firstChild(); else node = node->nextSibling(); } - return 0; + return nullptr; } bool Editor::selectionStartHasMarkerFor(DocumentMarker::MarkerType markerType, int from, int length) const { - Node* node = findFirstMarkable(m_frame.selection().start().deprecatedNode()); + Node* node = findFirstMarkable(m_frame.selection().selection().start().deprecatedNode()); if (!node) return false; unsigned int startOffset = static_cast<unsigned int>(from); unsigned int endOffset = static_cast<unsigned int>(from + length); - Vector<DocumentMarker*> markers = document().markers().markersFor(node); - for (size_t i = 0; i < markers.size(); ++i) { - DocumentMarker* marker = markers[i]; + Vector<RenderedDocumentMarker*> markers = document().markers().markersFor(node); + for (auto* marker : markers) { if (marker->startOffset() <= startOffset && endOffset <= marker->endOffset() && marker->type() == markerType) return true; } @@ -3398,8 +3504,18 @@ bool Editor::selectionStartHasMarkerFor(DocumentMarker::MarkerType markerType, i return false; } -TextCheckingTypeMask Editor::resolveTextCheckingTypeMask(TextCheckingTypeMask textCheckingOptions) +TextCheckingTypeMask Editor::resolveTextCheckingTypeMask(const Node& rootEditableElement, TextCheckingTypeMask textCheckingOptions) { +#if USE(AUTOMATIC_TEXT_REPLACEMENT) && !PLATFORM(IOS) + bool onlyAllowsTextReplacement = false; + if (auto* host = rootEditableElement.shadowHost()) + onlyAllowsTextReplacement = is<HTMLInputElement>(host) && downcast<HTMLInputElement>(*host).isSpellcheckDisabledExceptTextReplacement(); + if (onlyAllowsTextReplacement) + textCheckingOptions &= TextCheckingTypeReplacement; +#else + UNUSED_PARAM(rootEditableElement); +#endif + bool shouldMarkSpelling = textCheckingOptions & TextCheckingTypeSpelling; bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; #if !PLATFORM(IOS) @@ -3421,16 +3537,18 @@ TextCheckingTypeMask Editor::resolveTextCheckingTypeMask(TextCheckingTypeMask te #if USE(AUTOMATIC_TEXT_REPLACEMENT) bool shouldPerformReplacement = textCheckingOptions & TextCheckingTypeReplacement; if (shouldPerformReplacement) { - if (isAutomaticLinkDetectionEnabled()) - checkingTypes |= TextCheckingTypeLink; - if (isAutomaticQuoteSubstitutionEnabled()) - checkingTypes |= TextCheckingTypeQuote; - if (isAutomaticDashSubstitutionEnabled()) - checkingTypes |= TextCheckingTypeDash; + if (!onlyAllowsTextReplacement) { + if (isAutomaticLinkDetectionEnabled()) + checkingTypes |= TextCheckingTypeLink; + if (isAutomaticQuoteSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeQuote; + if (isAutomaticDashSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeDash; + if (shouldMarkSpelling && isAutomaticSpellingCorrectionEnabled()) + checkingTypes |= TextCheckingTypeCorrection; + } if (isAutomaticTextReplacementEnabled()) checkingTypes |= TextCheckingTypeReplacement; - if (shouldMarkSpelling && isAutomaticSpellingCorrectionEnabled()) - checkingTypes |= TextCheckingTypeCorrection; } #endif #endif // !PLATFORM(IOS) @@ -3438,11 +3556,68 @@ TextCheckingTypeMask Editor::resolveTextCheckingTypeMask(TextCheckingTypeMask te return checkingTypes; } -void Editor::deviceScaleFactorChanged() +static RefPtr<Range> candidateRangeForSelection(Frame& frame) { -#if ENABLE(DELETION_UI) - m_deleteButtonController->deviceScaleFactorChanged(); -#endif + const VisibleSelection& selection = frame.selection().selection(); + return selection.isCaret() ? wordRangeFromPosition(selection.start()) : frame.selection().toNormalizedRange(); +} + +static bool candidateWouldReplaceText(const VisibleSelection& selection) +{ + // If the character behind the caret in the current selection is anything but a space or a newline then we should + // replace the whole current word with the candidate. + UChar32 characterAfterSelection, characterBeforeSelection, twoCharacterBeforeSelection = 0; + charactersAroundPosition(selection.visibleStart(), characterAfterSelection, characterBeforeSelection, twoCharacterBeforeSelection); + return !(characterBeforeSelection == '\0' || characterBeforeSelection == '\n' || characterBeforeSelection == ' '); +} + +String Editor::stringForCandidateRequest() const +{ + const VisibleSelection& selection = m_frame.selection().selection(); + RefPtr<Range> rangeForCurrentlyTypedString = candidateRangeForSelection(m_frame); + if (rangeForCurrentlyTypedString && candidateWouldReplaceText(selection)) + return plainText(rangeForCurrentlyTypedString.get()); + + return String(); +} + +RefPtr<Range> Editor::contextRangeForCandidateRequest() const +{ + const VisibleSelection& selection = m_frame.selection().selection(); + return makeRange(startOfParagraph(selection.visibleStart()), endOfParagraph(selection.visibleEnd())); +} + +RefPtr<Range> Editor::rangeForTextCheckingResult(const TextCheckingResult& result) const +{ + if (!result.length) + return nullptr; + + RefPtr<Range> contextRange = contextRangeForCandidateRequest(); + if (!contextRange) + return nullptr; + + return TextIterator::subrange(contextRange.get(), result.location, result.length); +} + +void Editor::handleAcceptedCandidate(TextCheckingResult acceptedCandidate) +{ + const VisibleSelection& selection = m_frame.selection().selection(); + + m_isHandlingAcceptedCandidate = true; + + if (auto range = rangeForTextCheckingResult(acceptedCandidate)) { + if (shouldInsertText(acceptedCandidate.replacement, range.get(), EditorInsertAction::Typed)) { + Ref<ReplaceRangeWithTextCommand> replaceCommand = ReplaceRangeWithTextCommand::create(range.get(), acceptedCandidate.replacement); + applyCommand(replaceCommand.ptr()); + } + } else + insertText(acceptedCandidate.replacement, nullptr); + + RefPtr<Range> insertedCandidateRange = rangeExpandedByCharactersInDirectionAtWordBoundary(selection.visibleStart(), acceptedCandidate.replacement.length(), DirectionBackward); + if (insertedCandidateRange) + insertedCandidateRange->startContainer().document().markers().addMarker(insertedCandidateRange.get(), DocumentMarker::AcceptedCandidate, acceptedCandidate.replacement); + + m_isHandlingAcceptedCandidate = false; } bool Editor::unifiedTextCheckerEnabled() const @@ -3450,7 +3625,7 @@ bool Editor::unifiedTextCheckerEnabled() const return WebCore::unifiedTextCheckerEnabled(&m_frame); } -Vector<String> Editor::dictationAlternativesForMarker(const DocumentMarker* marker) +Vector<String> Editor::dictationAlternativesForMarker(const DocumentMarker& marker) { return m_alternativeTextController->dictationAlternativesForMarker(marker); } @@ -3472,4 +3647,108 @@ Document& Editor::document() const return *m_frame.document(); } +RefPtr<Range> Editor::adjustedSelectionRange() +{ + // FIXME: Why do we need to adjust the selection to include the anchor tag it's in? + // Whoever wrote this code originally forgot to leave us a comment explaining the rationale. + RefPtr<Range> range = selectedRange(); + Node* commonAncestor = range->commonAncestorContainer(); + ASSERT(commonAncestor); + auto* enclosingAnchor = enclosingElementWithTag(firstPositionInNode(commonAncestor), HTMLNames::aTag); + if (enclosingAnchor && comparePositions(firstPositionInOrBeforeNode(range->startPosition().anchorNode()), range->startPosition()) >= 0) + range->setStart(*enclosingAnchor, 0); + return range; +} + +// FIXME: This figures out the current style by inserting a <span>! +const RenderStyle* Editor::styleForSelectionStart(Frame* frame, Node*& nodeToRemove) +{ + nodeToRemove = nullptr; + + if (frame->selection().isNone()) + return nullptr; + + Position position = adjustedSelectionStartForStyleComputation(frame->selection().selection()); + if (!position.isCandidate() || position.isNull()) + return nullptr; + + RefPtr<EditingStyle> typingStyle = frame->selection().typingStyle(); + if (!typingStyle || !typingStyle->style()) + return &position.deprecatedNode()->renderer()->style(); + + auto styleElement = HTMLSpanElement::create(*frame->document()); + + String styleText = typingStyle->style()->asText() + " display: inline"; + styleElement->setAttribute(HTMLNames::styleAttr, styleText); + + styleElement->appendChild(frame->document()->createEditingTextNode(emptyString())); + + auto positionNode = position.deprecatedNode(); + if (!positionNode || !positionNode->parentNode() || positionNode->parentNode()->appendChild(styleElement).hasException()) + return nullptr; + + nodeToRemove = styleElement.ptr(); + + frame->document()->updateStyleIfNeeded(); + return styleElement->renderer() ? &styleElement->renderer()->style() : nullptr; +} + +const Font* Editor::fontForSelection(bool& hasMultipleFonts) const +{ + hasMultipleFonts = false; + + if (!m_frame.selection().isRange()) { + Node* nodeToRemove; + auto* style = styleForSelectionStart(&m_frame, nodeToRemove); // sets nodeToRemove + + const Font* font = nullptr; + if (style) { + font = &style->fontCascade().primaryFont(); + if (nodeToRemove) + nodeToRemove->remove(); + } + + return font; + } + + RefPtr<Range> range = m_frame.selection().toNormalizedRange(); + if (!range) + return nullptr; + + Node* startNode = adjustedSelectionStartForStyleComputation(m_frame.selection().selection()).deprecatedNode(); + if (!startNode) + return nullptr; + + const Font* font = nullptr; + Node* pastEnd = range->pastLastNode(); + // In the loop below, node should eventually match pastEnd and not become null, but we've seen at least one + // unreproducible case where this didn't happen, so check for null also. + for (Node* node = startNode; node && node != pastEnd; node = NodeTraversal::next(*node)) { + auto renderer = node->renderer(); + if (!renderer) + continue; + // FIXME: Are there any node types that have renderers, but that we should be skipping? + const Font& primaryFont = renderer->style().fontCascade().primaryFont(); + if (!font) + font = &primaryFont; + else if (font != &primaryFont) { + hasMultipleFonts = true; + break; + } + } + + return font; +} + +Ref<DocumentFragment> Editor::createFragmentForImageAndURL(const String& url) +{ + auto imageElement = HTMLImageElement::create(*m_frame.document()); + imageElement->setAttributeWithoutSynchronization(HTMLNames::srcAttr, url); + + auto fragment = document().createDocumentFragment(); + fragment->appendChild(imageElement); + + return fragment; +} + } // namespace WebCore |