/* * Copyright (C) 2006, 2007, 2008, 2011 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 * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, 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 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "core/editing/Editor.h" #include "CSSPropertyNames.h" #include "HTMLNames.h" #include "bindings/v8/ExceptionStatePlaceholder.h" #include "core/accessibility/AXObjectCache.h" #include "core/css/CSSComputedStyleDeclaration.h" #include "core/css/StylePropertySet.h" #include "core/dom/Clipboard.h" #include "core/dom/ClipboardEvent.h" #include "core/dom/DocumentFragment.h" #include "core/dom/DocumentMarkerController.h" #include "core/dom/EventNames.h" #include "core/dom/KeyboardEvent.h" #include "core/dom/NodeList.h" #include "core/dom/NodeTraversal.h" #include "core/dom/Text.h" #include "core/dom/TextEvent.h" #include "core/editing/ApplyStyleCommand.h" #include "core/editing/DeleteSelectionCommand.h" #include "core/editing/IndentOutdentCommand.h" #include "core/editing/InputMethodController.h" #include "core/editing/InsertListCommand.h" #include "core/editing/ModifySelectionListLevel.h" #include "core/editing/RemoveFormatCommand.h" #include "core/editing/RenderedPosition.h" #include "core/editing/ReplaceSelectionCommand.h" #include "core/editing/SimplifyMarkupCommand.h" #include "core/editing/SpellChecker.h" #include "core/editing/TextCheckingHelper.h" #include "core/editing/TextIterator.h" #include "core/editing/TypingCommand.h" #include "core/editing/VisibleUnits.h" #include "core/editing/htmlediting.h" #include "core/editing/markup.h" #include "core/html/HTMLImageElement.h" #include "core/html/HTMLInputElement.h" #include "core/html/HTMLTextAreaElement.h" #include "core/loader/cache/ResourceFetcher.h" #include "core/page/EditorClient.h" #include "core/page/EventHandler.h" #include "core/page/FocusController.h" #include "core/page/Frame.h" #include "core/page/FrameView.h" #include "core/page/Page.h" #include "core/page/Settings.h" #include "core/platform/KillRing.h" #include "core/platform/Pasteboard.h" #include "core/platform/Sound.h" #include "core/platform/text/TextCheckerClient.h" #include "core/rendering/HitTestResult.h" #include "core/rendering/RenderBlock.h" #include "core/rendering/RenderTextControl.h" #include "wtf/unicode/CharacterNames.h" namespace WebCore { using namespace std; using namespace HTMLNames; using namespace WTF; using namespace Unicode; Editor::RevealSelectionScope::RevealSelectionScope(Editor* editor) : m_editor(editor) { ++m_editor->m_preventRevealSelection; } Editor::RevealSelectionScope::~RevealSelectionScope() { ASSERT(m_editor->m_preventRevealSelection); --m_editor->m_preventRevealSelection; if (!m_editor->m_preventRevealSelection) m_editor->m_frame->selection()->revealSelection(ScrollAlignment::alignToEdgeIfNeeded, RevealExtent); } namespace { bool isSelectionInTextField(const VisibleSelection& selection) { HTMLTextFormControlElement* textControl = enclosingTextFormControl(selection.start()); return textControl && textControl->hasTagName(inputTag) && toHTMLInputElement(textControl)->isTextField(); } } // namespace // 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) { VisibleSelection selection = m_frame->selection()->selection(); if (!event) return selection; // 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()) : 0; if (textFromControlOfTarget && (selection.start().isNull() || textFromControlOfTarget != textFormControlOfSelectionStart)) { if (RefPtr range = textFromControlOfTarget->selection()) return VisibleSelection(range.get(), DOWNSTREAM, selection.isDirectional()); } return selection; } // Function considers Mac editing behavior a fallback when Page or Settings is not available. EditingBehavior Editor::behavior() const { if (!m_frame || !m_frame->settings()) return EditingBehavior(EditingMacBehavior); return EditingBehavior(m_frame->settings()->editingBehaviorType()); } EditorClient* Editor::client() const { if (Page* page = m_frame->page()) return page->editorClient(); return 0; } TextCheckerClient* Editor::textChecker() const { if (EditorClient* owner = client()) return owner->textChecker(); return 0; } void Editor::handleKeyboardEvent(KeyboardEvent* event) { if (EditorClient* c = client()) c->handleKeyboardEvent(event); } bool Editor::handleTextEvent(TextEvent* event) { // Default event handling for Drag and Drop will be handled by DragController // so we leave the event for it. if (event->isDrop()) return false; if (event->isPaste()) { if (event->pastingFragment()) replaceSelectionWithFragment(event->pastingFragment(), false, event->shouldSmartReplace(), event->shouldMatchStyle()); else replaceSelectionWithText(event->data(), false, event->shouldSmartReplace()); return true; } String data = event->data(); if (data == "\n") { if (event->isLineBreak()) return insertLineBreak(); return insertParagraphSeparator(); } return insertTextWithoutSendingTextEvent(data, false, event); } bool Editor::canEdit() const { return m_frame->selection()->rootEditableElement(); } bool Editor::canEditRichly() const { return m_frame->selection()->isContentRichlyEditable(); } // WinIE uses onbeforecut and onbeforepaste to enables the cut and paste menu items. They // also send onbeforecopy, apparently for symmetry, but it doesn't affect the menu items. // We need to use onbeforecopy as a real menu enabler because we allow elements that are not // normally selectable to implement copy/paste (like divs, or a document body). bool Editor::canDHTMLCut() { return !m_frame->selection()->isInPasswordField() && !dispatchCPPEvent(eventNames().beforecutEvent, ClipboardNumb); } bool Editor::canDHTMLCopy() { return !m_frame->selection()->isInPasswordField() && !dispatchCPPEvent(eventNames().beforecopyEvent, ClipboardNumb); } bool Editor::canDHTMLPaste() { return !dispatchCPPEvent(eventNames().beforepasteEvent, ClipboardNumb); } bool Editor::canCut() const { return canCopy() && canDelete(); } static HTMLImageElement* imageElementFromImageDocument(Document* document) { if (!document) return 0; if (!document->isImageDocument()) return 0; HTMLElement* body = document->body(); if (!body) return 0; Node* node = body->firstChild(); if (!node) return 0; if (!node->hasTagName(imgTag)) return 0; return toHTMLImageElement(node); } bool Editor::canCopy() const { if (imageElementFromImageDocument(m_frame->document())) return true; FrameSelection* selection = m_frame->selection(); return selection->isRange() && !selection->isInPasswordField(); } bool Editor::canPaste() const { return canEdit(); } bool Editor::canDelete() const { FrameSelection* selection = m_frame->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; if (!startContainer->rendererIsEditable() || !endContainer->rendererIsEditable()) return false; if (range->collapsed(IGNORE_EXCEPTION)) { 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()) return false; } return true; } bool Editor::smartInsertDeleteEnabled() { return client() && client()->smartInsertDeleteEnabled(); } bool Editor::canSmartCopyOrDelete() { return client() && client()->smartInsertDeleteEnabled() && m_frame->selection()->granularity() == WordGranularity; } bool Editor::isSelectTrailingWhitespaceEnabled() { return client() && client()->isSelectTrailingWhitespaceEnabled(); } bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity granularity, bool killRing, bool isTypingAction) { if (!canEdit()) return false; if (m_frame->selection()->isRange()) { if (isTypingAction) { TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete() ? TypingCommand::SmartDelete : 0, granularity); revealSelectionAfterEditingOperation(); } else { if (killRing) addToKillRing(selectedRange().get(), false); deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); // Implicitly calls revealSelectionAfterEditingOperation(). } } else { TypingCommand::Options options = 0; if (canSmartCopyOrDelete()) options |= TypingCommand::SmartDelete; if (killRing) options |= TypingCommand::KillRing; switch (direction) { case DirectionForward: case DirectionRight: TypingCommand::forwardDeleteKeyPressed(m_frame->document(), options, granularity); break; case DirectionBackward: case DirectionLeft: TypingCommand::deleteKeyPressed(m_frame->document(), options, granularity); break; } revealSelectionAfterEditingOperation(); } // 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) setStartNewKillRingSequence(false); return true; } void Editor::deleteSelectionWithSmartDelete(bool smartDelete) { if (m_frame->selection()->isNone()) return; applyCommand(DeleteSelectionCommand::create(m_frame->document(), smartDelete)); } void Editor::pasteAsPlainText(const String& pastingText, bool smartReplace) { Node* target = findEventTargetFromSelection(); if (!target) return; target->dispatchEvent(TextEvent::createForPlainTextPaste(m_frame->domWindow(), pastingText, smartReplace), IGNORE_EXCEPTION); } void Editor::pasteAsFragment(PassRefPtr pastingFragment, bool smartReplace, bool matchStyle) { Node* target = findEventTargetFromSelection(); if (!target) return; target->dispatchEvent(TextEvent::createForFragmentPaste(m_frame->domWindow(), pastingFragment, smartReplace, matchStyle), IGNORE_EXCEPTION); } void Editor::pasteAsPlainTextBypassingDHTML() { pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); } void Editor::pasteAsPlainTextWithPasteboard(Pasteboard* pasteboard) { String text = pasteboard->plainText(m_frame); if (client() && client()->shouldInsertText(text, selectedRange().get(), EditorInsertActionPasted)) pasteAsPlainText(text, canSmartReplaceWithPasteboard(pasteboard)); } void Editor::pasteWithPasteboard(Pasteboard* pasteboard, bool allowPlainText) { RefPtr range = selectedRange(); bool chosePlainText; RefPtr fragment = pasteboard->documentFragment(m_frame, range, allowPlainText, chosePlainText); if (fragment && shouldInsertFragment(fragment, range, EditorInsertActionPasted)) pasteAsFragment(fragment, canSmartReplaceWithPasteboard(pasteboard), chosePlainText); } bool Editor::canSmartReplaceWithPasteboard(Pasteboard* pasteboard) { return client() && client()->smartInsertDeleteEnabled() && pasteboard->canSmartReplace(); } bool Editor::shouldInsertFragment(PassRefPtr fragment, PassRefPtr replacingDOMRange, EditorInsertAction givenAction) { if (!client()) return false; if (fragment) { Node* child = fragment->firstChild(); if (child && fragment->lastChild() == child && child->isCharacterDataNode()) return client()->shouldInsertText(static_cast(child)->data(), replacingDOMRange.get(), givenAction); } return client()->shouldInsertNode(fragment.get(), replacingDOMRange.get(), givenAction); } void Editor::replaceSelectionWithFragment(PassRefPtr fragment, bool selectReplacement, bool smartReplace, bool matchStyle) { if (m_frame->selection()->isNone() || !m_frame->selection()->isContentEditable() || !fragment) return; ReplaceSelectionCommand::CommandOptions options = ReplaceSelectionCommand::PreventNesting | ReplaceSelectionCommand::SanitizeFragment; if (selectReplacement) options |= ReplaceSelectionCommand::SelectReplacement; if (smartReplace) options |= ReplaceSelectionCommand::SmartReplace; if (matchStyle) options |= ReplaceSelectionCommand::MatchStyle; applyCommand(ReplaceSelectionCommand::create(m_frame->document(), fragment, options, EditActionPaste)); revealSelectionAfterEditingOperation(); if (m_frame->selection()->isInPasswordField() || !isContinuousSpellCheckingEnabled()) return; Node* nodeToCheck = m_frame->selection()->rootEditableElement(); if (!nodeToCheck) return; RefPtr rangeToCheck = Range::create(m_frame->document(), firstPositionInNode(nodeToCheck), lastPositionInNode(nodeToCheck)); m_spellChecker->requestCheckingFor(SpellCheckRequest::create(resolveTextCheckingTypeMask(TextCheckingTypeSpelling | TextCheckingTypeGrammar), TextCheckingProcessBatch, rangeToCheck, rangeToCheck)); } void Editor::replaceSelectionWithText(const String& text, bool selectReplacement, bool smartReplace) { replaceSelectionWithFragment(createFragmentFromText(selectedRange().get(), text), selectReplacement, smartReplace, true); } PassRefPtr Editor::selectedRange() { if (!m_frame) return 0; return m_frame->selection()->toNormalizedRange(); } bool Editor::shouldDeleteRange(Range* range) const { if (!range || range->collapsed(IGNORE_EXCEPTION)) return false; if (!canDeleteRange(range)) return false; return client() && client()->shouldDeleteRange(range); } bool Editor::tryDHTMLCopy() { if (m_frame->selection()->isInPasswordField()) return false; return !dispatchCPPEvent(eventNames().copyEvent, ClipboardWritable); } bool Editor::tryDHTMLCut() { if (m_frame->selection()->isInPasswordField()) return false; return !dispatchCPPEvent(eventNames().cutEvent, ClipboardWritable); } bool Editor::tryDHTMLPaste() { return !dispatchCPPEvent(eventNames().pasteEvent, ClipboardReadable); } 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 (client()) client()->respondToChangedSelection(m_frame); setStartNewKillRingSequence(true); } void Editor::respondToChangedContents(const VisibleSelection& endingSelection) { if (AXObjectCache::accessibilityEnabled()) { Node* node = endingSelection.start().deprecatedNode(); if (AXObjectCache* cache = m_frame->document()->existingAXObjectCache()) cache->postNotification(node, AXObjectCache::AXValueChanged, false); } updateMarkersForWordsAffectedByEditing(true); if (client()) client()->respondToChangedContents(); } TriState Editor::selectionUnorderedListState() const { if (m_frame->selection()->isCaret()) { if (enclosingNodeWithTag(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); if (startNode && endNode && startNode == endNode) return TrueTriState; } return FalseTriState; } TriState Editor::selectionOrderedListState() const { if (m_frame->selection()->isCaret()) { if (enclosingNodeWithTag(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); if (startNode && endNode && startNode == endNode) return TrueTriState; } return FalseTriState; } PassRefPtr Editor::insertOrderedList() { if (!canEditRichly()) return 0; RefPtr newList = InsertListCommand::insertList(m_frame->document(), InsertListCommand::OrderedList); revealSelectionAfterEditingOperation(); return newList; } PassRefPtr Editor::insertUnorderedList() { if (!canEditRichly()) return 0; RefPtr newList = InsertListCommand::insertList(m_frame->document(), InsertListCommand::UnorderedList); revealSelectionAfterEditingOperation(); return newList; } bool Editor::canIncreaseSelectionListLevel() { return canEditRichly() && IncreaseSelectionListLevelCommand::canIncreaseSelectionListLevel(m_frame->document()); } bool Editor::canDecreaseSelectionListLevel() { return canEditRichly() && DecreaseSelectionListLevelCommand::canDecreaseSelectionListLevel(m_frame->document()); } PassRefPtr Editor::increaseSelectionListLevel() { if (!canEditRichly() || m_frame->selection()->isNone()) return 0; RefPtr newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevel(m_frame->document()); revealSelectionAfterEditingOperation(); return newList; } PassRefPtr Editor::increaseSelectionListLevelOrdered() { if (!canEditRichly() || m_frame->selection()->isNone()) return 0; RefPtr newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(m_frame->document()); revealSelectionAfterEditingOperation(); return newList.release(); } PassRefPtr Editor::increaseSelectionListLevelUnordered() { if (!canEditRichly() || m_frame->selection()->isNone()) return 0; RefPtr newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(m_frame->document()); revealSelectionAfterEditingOperation(); return newList.release(); } void Editor::decreaseSelectionListLevel() { if (!canEditRichly() || m_frame->selection()->isNone()) return; DecreaseSelectionListLevelCommand::decreaseSelectionListLevel(m_frame->document()); revealSelectionAfterEditingOperation(); } void Editor::removeFormattingAndStyle() { applyCommand(RemoveFormatCommand::create(m_frame->document())); } void Editor::clearLastEditCommand() { m_lastEditCommand.clear(); } // 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) { Node* target = findEventTargetFromSelection(); if (!target) return true; RefPtr clipboard = newGeneralClipboard(policy, m_frame); RefPtr evt = ClipboardEvent::create(eventType, true, true, clipboard); target->dispatchEvent(evt, IGNORE_EXCEPTION); bool noDefaultProcessing = evt->defaultPrevented(); if (noDefaultProcessing && policy == ClipboardWritable) { Pasteboard* pasteboard = Pasteboard::generalPasteboard(); pasteboard->clear(); pasteboard->writeClipboard(clipboard.get()); } // invalidate clipboard here for security clipboard->setAccessPolicy(ClipboardNumb); return !noDefaultProcessing; } Node* Editor::findEventTargetFrom(const VisibleSelection& selection) const { Node* target = selection.start().element(); if (!target) target = m_frame->document()->body(); if (!target) return 0; return target; } Node* Editor::findEventTargetFromSelection() const { return findEventTargetFrom(m_frame->selection()->selection()); } void Editor::applyStyle(StylePropertySet* style, EditAction editingAction) { switch (m_frame->selection()->selectionType()) { case VisibleSelection::NoSelection: // do nothing break; case VisibleSelection::CaretSelection: computeAndSetTypingStyle(style, editingAction); break; case VisibleSelection::RangeSelection: if (style) applyCommand(ApplyStyleCommand::create(m_frame->document(), EditingStyle::create(style).get(), editingAction)); break; } } bool Editor::shouldApplyStyle(StylePropertySet* style, Range* range) { return client()->shouldApplyStyle(style, range); } void Editor::applyParagraphStyle(StylePropertySet* 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(m_frame->document(), EditingStyle::create(style).get(), editingAction, ApplyStyleCommand::ForceBlockProperties)); break; } } void Editor::applyStyleToSelection(StylePropertySet* style, EditAction editingAction) { if (!style || style->isEmpty() || !canEditRichly()) return; if (client() && client()->shouldApplyStyle(style, m_frame->selection()->toNormalizedRange().get())) applyStyle(style, editingAction); } void Editor::applyParagraphStyleToSelection(StylePropertySet* style, EditAction editingAction) { if (!style || style->isEmpty() || !canEditRichly()) return; if (client() && client()->shouldApplyStyle(style, m_frame->selection()->toNormalizedRange().get())) applyParagraphStyle(style, editingAction); } bool Editor::selectionStartHasStyle(CSSPropertyID propertyID, const String& value) const { return EditingStyle::create(propertyID, value)->triStateOfStyle( EditingStyle::styleAtSelectionStart(m_frame->selection()->selection(), propertyID == CSSPropertyBackgroundColor).get()); } TriState Editor::selectionHasStyle(CSSPropertyID propertyID, const String& value) const { return EditingStyle::create(propertyID, value)->triStateOfStyle(m_frame->selection()->selection()); } String Editor::selectionStartCSSPropertyValue(CSSPropertyID propertyID) { RefPtr selectionStyle = EditingStyle::styleAtSelectionStart(m_frame->selection()->selection(), propertyID == CSSPropertyBackgroundColor); if (!selectionStyle || !selectionStyle->style()) return String(); if (propertyID == CSSPropertyFontSize) return String::number(selectionStyle->legacyFontSize(m_frame->document())); return selectionStyle->style()->getPropertyValue(propertyID); } void Editor::indent() { applyCommand(IndentOutdentCommand::create(m_frame->document(), IndentOutdentCommand::Indent)); } void Editor::outdent() { applyCommand(IndentOutdentCommand::create(m_frame->document(), IndentOutdentCommand::Outdent)); } static void dispatchEditableContentChangedEvents(PassRefPtr startRoot, PassRefPtr endRoot) { if (startRoot) startRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), IGNORE_EXCEPTION); if (endRoot && endRoot != startRoot) endRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), IGNORE_EXCEPTION); } void Editor::appliedEditing(PassRefPtr cmd) { m_frame->document()->updateLayout(); EditCommandComposition* composition = cmd->composition(); ASSERT(composition); VisibleSelection newSelection(cmd->endingSelection()); // Don't clear the typing style with this selection change. We do those things elsewhere if necessary. changeSelectionAfterCommand(newSelection, 0); dispatchEditableContentChangedEvents(composition->startingRootEditableElement(), composition->endingRootEditableElement()); 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); } void Editor::unappliedEditing(PassRefPtr cmd) { m_frame->document()->updateLayout(); VisibleSelection newSelection(cmd->startingSelection()); changeSelectionAfterCommand(newSelection, FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle); dispatchEditableContentChangedEvents(cmd->startingRootEditableElement(), cmd->endingRootEditableElement()); m_lastEditCommand = 0; if (client()) client()->registerRedoStep(cmd); respondToChangedContents(newSelection); } void Editor::reappliedEditing(PassRefPtr cmd) { m_frame->document()->updateLayout(); VisibleSelection newSelection(cmd->endingSelection()); changeSelectionAfterCommand(newSelection, FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle); dispatchEditableContentChangedEvents(cmd->startingRootEditableElement(), cmd->endingRootEditableElement()); m_lastEditCommand = 0; if (client()) client()->registerUndoStep(cmd); respondToChangedContents(newSelection); } Editor::Editor(Frame* frame) : FrameDestructionObserver(frame) , m_preventRevealSelection(0) , 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_areMarkedTextMatchesHighlighted(false) , m_defaultParagraphSeparator(EditorParagraphSeparatorIsDiv) , m_overwriteModeEnabled(false) { } Editor::~Editor() { } void Editor::clear() { m_frame->inputMethodController().clear(); m_shouldStyleWithCSS = false; m_defaultParagraphSeparator = EditorParagraphSeparatorIsDiv; } bool Editor::insertText(const String& text, Event* triggeringEvent) { return m_frame->eventHandler()->handleTextInputEvent(text, triggeringEvent); } bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectInsertedText, TextEvent* triggeringEvent) { if (text.isEmpty()) return false; VisibleSelection selection = selectionForCommand(triggeringEvent); if (!selection.isContentEditable()) return false; RefPtr range = selection.toNormalizedRange(); if (!shouldInsertText(text, range.get(), EditorInsertActionTyped)) return true; if (!text.isEmpty()) updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text[0])); // Get the selection to use for the event that triggered this insertText. // If the event handler changed the selection, we may want to use a different selection // that is contained in the event target. selection = selectionForCommand(triggeringEvent); if (selection.isContentEditable()) { if (Node* selectionStart = selection.start().deprecatedNode()) { RefPtr document = selectionStart->document(); // Insert the text TypingCommand::Options options = 0; if (selectInsertedText) options |= TypingCommand::SelectInsertedText; TypingCommand::insertText(document.get(), text, selection, options, triggeringEvent && triggeringEvent->isComposition() ? TypingCommand::TextCompositionConfirm : TypingCommand::TextCompositionNone); // Reveal the current selection if (Frame* editedFrame = document->frame()) { if (Page* page = editedFrame->page()) page->focusController().focusedOrMainFrame()->selection()->revealSelection(ScrollAlignment::alignCenterIfNeeded); } } } return true; } bool Editor::insertLineBreak() { if (!canEdit()) return false; if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) return true; VisiblePosition caret = m_frame->selection()->selection().visibleStart(); bool alignToEdge = isEndOfEditableOrNonEditableContent(caret); TypingCommand::insertLineBreak(m_frame->document(), 0); revealSelectionAfterEditingOperation(alignToEdge ? ScrollAlignment::alignToEdgeIfNeeded : ScrollAlignment::alignCenterIfNeeded); return true; } bool Editor::insertParagraphSeparator() { if (!canEdit()) return false; if (!canEditRichly()) return insertLineBreak(); if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) return true; VisiblePosition caret = m_frame->selection()->selection().visibleStart(); bool alignToEdge = isEndOfEditableOrNonEditableContent(caret); TypingCommand::insertParagraphSeparator(m_frame->document(), 0); revealSelectionAfterEditingOperation(alignToEdge ? ScrollAlignment::alignToEdgeIfNeeded : ScrollAlignment::alignCenterIfNeeded); return true; } void Editor::cut() { if (tryDHTMLCut()) return; // DHTML did the whole operation if (!canCut()) { systemBeep(); return; } RefPtr selection = selectedRange(); if (shouldDeleteRange(selection.get())) { updateMarkersForWordsAffectedByEditing(true); if (enclosingTextFormControl(m_frame->selection()->start())) { Pasteboard::generalPasteboard()->writePlainText(m_frame->selectedTextForClipboard(), canSmartCopyOrDelete() ? Pasteboard::CanSmartReplace : Pasteboard::CannotSmartReplace); } else Pasteboard::generalPasteboard()->writeSelection(selection.get(), canSmartCopyOrDelete(), m_frame, IncludeImageAltTextForClipboard); deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); } } void Editor::copy() { if (tryDHTMLCopy()) return; // DHTML did the whole operation if (!canCopy()) { systemBeep(); return; } if (enclosingTextFormControl(m_frame->selection()->start())) { Pasteboard::generalPasteboard()->writePlainText(m_frame->selectedTextForClipboard(), canSmartCopyOrDelete() ? Pasteboard::CanSmartReplace : Pasteboard::CannotSmartReplace); } else { Document* document = m_frame->document(); if (HTMLImageElement* imageElement = imageElementFromImageDocument(document)) Pasteboard::generalPasteboard()->writeImage(imageElement, document->url(), document->title()); else Pasteboard::generalPasteboard()->writeSelection(selectedRange().get(), canSmartCopyOrDelete(), m_frame, IncludeImageAltTextForClipboard); } } void Editor::paste() { ASSERT(m_frame->document()); if (tryDHTMLPaste()) return; // DHTML did the whole operation if (!canPaste()) return; updateMarkersForWordsAffectedByEditing(false); ResourceFetcher* loader = m_frame->document()->fetcher(); ResourceCacheValidationSuppressor validationSuppressor(loader); if (m_frame->selection()->isContentRichlyEditable()) pasteWithPasteboard(Pasteboard::generalPasteboard(), true); else pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); } void Editor::pasteAsPlainText() { if (tryDHTMLPaste()) return; if (!canPaste()) return; updateMarkersForWordsAffectedByEditing(false); pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); } void Editor::performDelete() { if (!canDelete()) { systemBeep(); return; } addToKillRing(selectedRange().get(), false); deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); // clear the "start new kill ring sequence" setting, because it was set to true // when the selection was updated by deleting the range setStartNewKillRingSequence(false); } void Editor::simplifyMarkup(Node* startNode, Node* endNode) { if (!startNode) return; if (endNode) { if (startNode->document() != endNode->document()) return; // check if start node is before endNode Node* node = startNode; while (node && node != endNode) node = NodeTraversal::next(node); if (!node) return; } applyCommand(SimplifyMarkupCommand::create(m_frame->document(), startNode, (endNode) ? NodeTraversal::next(endNode) : 0)); } void Editor::copyURL(const KURL& url, const String& title) { Pasteboard::generalPasteboard()->writeURL(url, title, m_frame); } void Editor::copyImage(const HitTestResult& result) { KURL url = result.absoluteLinkURL(); if (url.isEmpty()) url = result.absoluteImageURL(); Pasteboard::generalPasteboard()->writeImage(result.innerNonSharedNode(), url, result.altDisplayString()); } bool Editor::isContinuousSpellCheckingEnabled() const { return client() && client()->isContinuousSpellCheckingEnabled(); } void Editor::toggleContinuousSpellChecking() { if (client()) client()->toggleContinuousSpellChecking(); } bool Editor::isGrammarCheckingEnabled() { return client() && client()->isGrammarCheckingEnabled(); } bool Editor::shouldEndEditing(Range* range) { return client() && client()->shouldEndEditing(range); } bool Editor::shouldBeginEditing(Range* range) { return client() && client()->shouldBeginEditing(range); } void Editor::clearUndoRedoOperations() { if (client()) client()->clearUndoRedoOperations(); } bool Editor::canUndo() { return client() && client()->canUndo(); } void Editor::undo() { if (client()) client()->undo(); } bool Editor::canRedo() { return client() && client()->canRedo(); } void Editor::redo() { if (client()) client()->redo(); } void Editor::didBeginEditing() { if (client()) client()->didBeginEditing(); } void Editor::didEndEditing() { if (client()) client()->didEndEditing(); } void Editor::setBaseWritingDirection(WritingDirection direction) { Node* focusedElement = frame()->document()->focusedElement(); if (focusedElement && isHTMLTextFormControlElement(focusedElement)) { if (direction == NaturalWritingDirection) return; toHTMLElement(focusedElement)->setAttribute(dirAttr, direction == LeftToRightWritingDirection ? "ltr" : "rtl"); focusedElement->dispatchInputEvent(); frame()->document()->updateStyleIfNeeded(); return; } RefPtr style = MutableStylePropertySet::create(); style->setProperty(CSSPropertyDirection, direction == LeftToRightWritingDirection ? "ltr" : direction == RightToLeftWritingDirection ? "rtl" : "inherit", false); applyParagraphStyleToSelection(style.get(), EditActionSetWritingDirection); } WritingDirection Editor::baseWritingDirectionForSelectionStart() const { WritingDirection result = LeftToRightWritingDirection; Position pos = m_frame->selection()->selection().visibleStart().deepEquivalent(); Node* node = pos.deprecatedNode(); if (!node) return result; RenderObject* renderer = node->renderer(); if (!renderer) return result; if (!renderer->isBlockFlow()) { renderer = renderer->containingBlock(); if (!renderer) return result; } RenderStyle* style = renderer->style(); if (!style) return result; switch (style->direction()) { case LTR: return LeftToRightWritingDirection; case RTL: return RightToLeftWritingDirection; } return result; } void Editor::ignoreSpelling() { if (!client()) return; RefPtr selectedRange = frame()->selection()->toNormalizedRange(); if (selectedRange) frame()->document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::Spelling); } void Editor::advanceToNextMisspelling(bool startBeforeSelection) { // 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. // Start at the end of the selection, search to edge of document. Starting at the selection end makes // repeated "check spelling" commands work. VisibleSelection selection(frame()->selection()->selection()); RefPtr spellingSearchRange(rangeOfContents(frame()->document())); bool startedWithSelection = false; if (selection.start().deprecatedNode()) { startedWithSelection = true; if (startBeforeSelection) { VisiblePosition start(selection.visibleStart()); // We match AppKit's rule: Start 1 character before the selection. VisiblePosition oneBeforeStart = start.previous(); setStart(spellingSearchRange.get(), oneBeforeStart.isNotNull() ? oneBeforeStart : start); } else setStart(spellingSearchRange.get(), selection.visibleEnd()); } Position position = spellingSearchRange->startPosition(); if (!isEditablePosition(position)) { // This shouldn't happen in very often because the Spelling menu items aren't enabled unless the // selection is editable. // This can happen in Mail for a mix of non-editable and editable content (like Stationary), // 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, frame()->document()->documentElement()).deepEquivalent(); if (position.isNull()) return; Position rangeCompliantPosition = position.parentAnchoredEquivalent(); spellingSearchRange->setStart(rangeCompliantPosition.deprecatedNode(), rangeCompliantPosition.deprecatedEditingOffset(), IGNORE_EXCEPTION); startedWithSelection = false; // won't need to wrap } // topNode defines the whole range we want to operate on Node* topNode = highestEditableRoot(position); // FIXME: lastOffsetForEditing() is wrong here if editingIgnoresContent(highestEditableRoot()) returns true (e.g. a ) spellingSearchRange->setEnd(topNode, lastOffsetForEditing(topNode), IGNORE_EXCEPTION); // 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. if (startedWithSelection) { VisiblePosition oneBeforeStart = startVisiblePosition(spellingSearchRange.get(), DOWNSTREAM).previous(); if (oneBeforeStart.isNotNull()) setStart(spellingSearchRange.get(), endOfWord(oneBeforeStart)); // else we were already at the start of the editable node } if (spellingSearchRange->collapsed(IGNORE_EXCEPTION)) return; // nothing to search in // Get the spell checker if it is available if (!client()) return; // 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(); int searchEndOffsetAfterWrap = spellingSearchRange->endOffset(); int misspellingOffset = 0; GrammarDetail grammarDetail; int grammarPhraseOffset = 0; RefPtr grammarSearchRange; String badGrammarPhrase; String misspelledWord; bool isSpelling = true; int foundOffset = 0; String foundItem; RefPtr firstMisspellingRange; if (unifiedTextCheckerEnabled()) { grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); foundItem = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); if (isSpelling) { misspelledWord = foundItem; misspellingOffset = foundOffset; } else { badGrammarPhrase = foundItem; grammarPhraseOffset = foundOffset; } } else { misspelledWord = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); if (!misspelledWord.isEmpty()) { // Stop looking at start of next misspelled word CharacterIterator chars(grammarSearchRange.get()); chars.advance(misspellingOffset); grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset(), IGNORE_EXCEPTION); } if (isGrammarCheckingEnabled()) badGrammarPhrase = TextCheckingHelper(client(), grammarSearchRange).findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false); } // 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); // going until the end of the very first chunk we tested is far enough spellingSearchRange->setEnd(searchEndNodeAfterWrap, searchEndOffsetAfterWrap, IGNORE_EXCEPTION); if (unifiedTextCheckerEnabled()) { grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); foundItem = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); if (isSpelling) { misspelledWord = foundItem; misspellingOffset = foundOffset; } else { badGrammarPhrase = foundItem; grammarPhraseOffset = foundOffset; } } else { misspelledWord = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); if (!misspelledWord.isEmpty()) { // Stop looking at start of next misspelled word CharacterIterator chars(grammarSearchRange.get()); chars.advance(misspellingOffset); grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset(), IGNORE_EXCEPTION); } if (isGrammarCheckingEnabled()) badGrammarPhrase = TextCheckingHelper(client(), grammarSearchRange).findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false); } } if (!badGrammarPhrase.isEmpty()) { // We found bad grammar. Since we only searched for bad grammar up to the first misspelled word, the bad grammar // takes precedence and we ignore any potential misspelled word. Select the grammar detail, update the spelling // panel, and store a marker so we draw the green squiggle later. ASSERT(badGrammarPhrase.length() > 0); ASSERT(grammarDetail.location != -1 && grammarDetail.length > 0); // FIXME 4859190: This gets confused with doubled punctuation at the end of a paragraph RefPtr badGrammarRange = TextIterator::subrange(grammarSearchRange.get(), grammarPhraseOffset + grammarDetail.location, grammarDetail.length); frame()->selection()->setSelection(VisibleSelection(badGrammarRange.get(), SEL_DEFAULT_AFFINITY)); frame()->selection()->revealSelection(); frame()->document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, grammarDetail.userDescription); } else if (!misspelledWord.isEmpty()) { // We found a misspelling, but not any earlier bad grammar. Select the misspelling, update the spelling panel, and store // a marker so we draw the red squiggle later. RefPtr misspellingRange = TextIterator::subrange(spellingSearchRange.get(), misspellingOffset, misspelledWord.length()); frame()->selection()->setSelection(VisibleSelection(misspellingRange.get(), DOWNSTREAM)); frame()->selection()->revealSelection(); client()->updateSpellingUIWithMisspelledWord(misspelledWord); frame()->document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); } } String Editor::misspelledWordAtCaretOrRange(Node* clickedNode) const { if (!isContinuousSpellCheckingEnabled() || !clickedNode || !isSpellCheckingEnabledFor(clickedNode)) return String(); VisibleSelection selection = m_frame->selection()->selection(); if (!selection.isContentEditable() || selection.isNone()) return String(); VisibleSelection wordSelection(selection.base()); wordSelection.expandUsingGranularity(WordGranularity); RefPtr wordRange = wordSelection.toNormalizedRange(); // In compliance with GTK+ applications, additionally allow to provide suggestions when the current // selection exactly match the word selection. if (selection.isRange() && !areRangesEqual(wordRange.get(), selection.toNormalizedRange().get())) return String(); String word = wordRange->text(); if (word.isEmpty() || !client()) return String(); int wordLength = word.length(); int misspellingLocation = -1; int misspellingLength = 0; textChecker()->checkSpellingOfString(word, &misspellingLocation, &misspellingLength); return misspellingLength == wordLength ? word : String(); } void Editor::showSpellingGuessPanel() { if (!client()) { LOG_ERROR("No NSSpellChecker"); return; } if (client()->spellingUIIsShowing()) { client()->showSpellingUI(false); return; } advanceToNextMisspelling(true); client()->showSpellingUI(true); } void Editor::clearMisspellingsAndBadGrammar(const VisibleSelection &movingSelection) { RefPtr selectedRange = movingSelection.toNormalizedRange(); if (selectedRange) { frame()->document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::Spelling); frame()->document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::Grammar); } } void Editor::markMisspellingsAndBadGrammar(const VisibleSelection &movingSelection) { markMisspellingsAndBadGrammar(movingSelection, isContinuousSpellCheckingEnabled() && isGrammarCheckingEnabled(), movingSelection); } void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping) { if (unifiedTextCheckerEnabled()) { TextCheckingTypeMask textCheckingOptions = 0; if (isContinuousSpellCheckingEnabled()) textCheckingOptions |= TextCheckingTypeSpelling; if (!(textCheckingOptions & TextCheckingTypeSpelling)) return; if (isGrammarCheckingEnabled()) textCheckingOptions |= TextCheckingTypeGrammar; VisibleSelection adjacentWords = VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary)); if (textCheckingOptions & TextCheckingTypeGrammar) { VisibleSelection selectedSentence = VisibleSelection(startOfSentence(wordStart), endOfSentence(wordStart)); markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWords.toNormalizedRange().get(), selectedSentence.toNormalizedRange().get()); } else markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWords.toNormalizedRange().get(), adjacentWords.toNormalizedRange().get()); return; } if (!isContinuousSpellCheckingEnabled()) return; // Check spelling of one word RefPtr misspellingRange; markMisspellings(VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary)), misspellingRange); // Autocorrect the misspelled word. if (!misspellingRange) return; // Get the misspelled word. const String misspelledWord = plainText(misspellingRange.get()); String autocorrectedString = textChecker()->getAutoCorrectSuggestionForMisspelledWord(misspelledWord); // If autocorrected word is non empty, replace the misspelled word by this word. if (!autocorrectedString.isEmpty()) { VisibleSelection newSelection(misspellingRange.get(), DOWNSTREAM); if (newSelection != frame()->selection()->selection()) { if (!frame()->selection()->shouldChangeSelection(newSelection)) return; frame()->selection()->setSelection(newSelection); } if (!frame()->editor()->shouldInsertText(autocorrectedString, misspellingRange.get(), EditorInsertActionTyped)) return; frame()->editor()->replaceSelectionWithText(autocorrectedString, false, false); // Reset the charet one character further. frame()->selection()->moveTo(frame()->selection()->end()); frame()->selection()->modify(FrameSelection::AlterationMove, DirectionForward, CharacterGranularity); } if (!isGrammarCheckingEnabled()) return; // Check grammar of entire sentence markBadGrammar(VisibleSelection(startOfSentence(wordStart), endOfSentence(wordStart))); } void Editor::markMisspellingsOrBadGrammar(const VisibleSelection& selection, bool checkSpelling, RefPtr& firstMisspellingRange) { // This function is called with a selection already expanded to word boundaries. // Might be nice to assert that here. // This function is used only for as-you-type checking, so if that's off we do nothing. Note that // grammar checking can only be on if spell checking is also on. if (!isContinuousSpellCheckingEnabled()) return; RefPtr searchRange(selection.toNormalizedRange()); if (!searchRange) return; // If we're not in an editable node, bail. Node* editableNode = searchRange->startContainer(); if (!editableNode || !editableNode->rendererIsEditable()) return; if (!isSpellCheckingEnabledFor(editableNode)) return; // Get the spell checker if it is available if (!client()) return; TextCheckingHelper checker(client(), searchRange); if (checkSpelling) checker.markAllMisspellings(firstMisspellingRange); else if (isGrammarCheckingEnabled()) checker.markAllBadGrammar(); } bool Editor::isSpellCheckingEnabledFor(Node* node) const { if (!node) return false; const Element* focusedElement = node->isElementNode() ? toElement(node) : node->parentElement(); if (!focusedElement) return false; return focusedElement->isSpellCheckingEnabled(); } bool Editor::isSpellCheckingEnabledInFocusedNode() const { return isSpellCheckingEnabledFor(m_frame->selection()->start().deprecatedNode()); } void Editor::markMisspellings(const VisibleSelection& selection, RefPtr& firstMisspellingRange) { markMisspellingsOrBadGrammar(selection, true, firstMisspellingRange); } void Editor::markBadGrammar(const VisibleSelection& selection) { RefPtr firstMisspellingRange; markMisspellingsOrBadGrammar(selection, false, firstMisspellingRange); } void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask textCheckingOptions, Range* spellingRange, Range* grammarRange) { ASSERT(m_frame); ASSERT(unifiedTextCheckerEnabled()); bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; // This function is called with selections already expanded to word boundaries. if (!client() || !spellingRange || (shouldMarkGrammar && !grammarRange)) return; // If we're not in an editable node, bail. Node* editableNode = spellingRange->startContainer(); if (!editableNode || !editableNode->rendererIsEditable()) return; if (!isSpellCheckingEnabledFor(editableNode)) return; Range* rangeToCheck = shouldMarkGrammar ? grammarRange : spellingRange; TextCheckingParagraph paragraphToCheck(rangeToCheck); if (paragraphToCheck.isRangeEmpty() || paragraphToCheck.isEmpty()) return; RefPtr paragraphRange = paragraphToCheck.paragraphRange(); bool asynchronous = m_frame && m_frame->settings() && m_frame->settings()->asynchronousSpellCheckingEnabled(); // In asynchronous mode, we intentionally check paragraph-wide sentence. RefPtr request = SpellCheckRequest::create(resolveTextCheckingTypeMask(textCheckingOptions), TextCheckingProcessIncremental, asynchronous ? paragraphRange : rangeToCheck, paragraphRange); if (asynchronous) { m_spellChecker->requestCheckingFor(request); return; } Vector results; checkTextOfParagraph(textChecker(), paragraphToCheck.text(), resolveTextCheckingTypeMask(textCheckingOptions), results); markAndReplaceFor(request, results); } void Editor::markAndReplaceFor(PassRefPtr request, const Vector& results) { ASSERT(request); TextCheckingTypeMask textCheckingOptions = request->data().mask(); TextCheckingParagraph paragraph(request->checkingRange(), request->paragraphRange()); bool shouldMarkSpelling = textCheckingOptions & TextCheckingTypeSpelling; bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; // Expand the range to encompass entire paragraphs, since text checking needs that much context. int selectionOffset = 0; int ambiguousBoundaryOffset = -1; bool selectionChanged = false; bool restoreSelectionAfterChange = false; bool adjustSelectionForParagraphBoundaries = false; if (shouldMarkSpelling) { if (m_frame->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); restoreSelectionAfterChange = true; if (selectionOffset > 0 && (static_cast(selectionOffset) > paragraph.text().length() || paragraph.textCharAt(selectionOffset - 1) == newlineCharacter)) adjustSelectionForParagraphBoundaries = true; if (selectionOffset > 0 && static_cast(selectionOffset) <= paragraph.text().length() && isAmbiguousBoundaryCharacter(paragraph.textCharAt(selectionOffset - 1))) ambiguousBoundaryOffset = selectionOffset - 1; } } for (unsigned i = 0; i < results.size(); i++) { int spellingRangeEndOffset = paragraph.checkingEnd(); const TextCheckingResult* result = &results[i]; int resultLocation = result->location; int resultLength = result->length; bool resultEndsAtAmbiguousBoundary = ambiguousBoundaryOffset >= 0 && resultLocation + resultLength == ambiguousBoundaryOffset; // Only mark misspelling if: // 1. Current text checking isn't done for autocorrection, in which case shouldMarkSpelling is false. // 2. Result falls within spellingRange. // 3. The word in question doesn't end at an ambiguous boundary. For instance, we would not mark // "wouldn'" as misspelled right after apostrophe is typed. if (shouldMarkSpelling && result->type == TextCheckingTypeSpelling && resultLocation >= paragraph.checkingStart() && resultLocation + resultLength <= spellingRangeEndOffset && !resultEndsAtAmbiguousBoundary) { ASSERT(resultLength > 0 && resultLocation >= 0); RefPtr misspellingRange = paragraph.subrange(resultLocation, resultLength); misspellingRange->startContainer()->document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling, result->replacement, result->hash); } else if (shouldMarkGrammar && result->type == TextCheckingTypeGrammar && paragraph.checkingRangeCovers(resultLocation, resultLength)) { ASSERT(resultLength > 0 && resultLocation >= 0); for (unsigned j = 0; j < result->details.size(); j++) { const GrammarDetail* detail = &result->details[j]; ASSERT(detail->length > 0 && detail->location >= 0); if (paragraph.checkingRangeCovers(resultLocation + detail->location, detail->length)) { RefPtr badGrammarRange = paragraph.subrange(resultLocation + detail->location, detail->length); badGrammarRange->startContainer()->document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription, result->hash); } } } } if (selectionChanged) { TextCheckingParagraph extendedParagraph(paragraph); // Restore the caret position if we have made any replacements extendedParagraph.expandRangeToNextEnd(); if (restoreSelectionAfterChange && selectionOffset >= 0 && selectionOffset <= extendedParagraph.rangeLength()) { RefPtr selectionRange = extendedParagraph.subrange(0, selectionOffset); m_frame->selection()->moveTo(selectionRange->endPosition(), DOWNSTREAM); if (adjustSelectionForParagraphBoundaries) 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()->modify(FrameSelection::AlterationMove, DirectionForward, CharacterGranularity); } } } void Editor::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection) { if (unifiedTextCheckerEnabled()) { if (!isContinuousSpellCheckingEnabled()) return; // markMisspellingsAndBadGrammar() is triggered by selection change, in which case we check spelling and grammar, but don't autocorrect misspellings. TextCheckingTypeMask textCheckingOptions = TextCheckingTypeSpelling; if (markGrammar && isGrammarCheckingEnabled()) textCheckingOptions |= TextCheckingTypeGrammar; markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellingSelection.toNormalizedRange().get(), grammarSelection.toNormalizedRange().get()); return; } RefPtr firstMisspellingRange; markMisspellings(spellingSelection, firstMisspellingRange); if (markGrammar) markBadGrammar(grammarSelection); } void Editor::updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionAtWordBoundary) { if (!textChecker() || textChecker()->shouldEraseMarkersAfterChangeSelection(TextCheckingTypeSpelling)) return; // We want to remove the markers from a word if an editing command will change the word. This can happen in one of // several scenarios: // 1. Insert in the middle of a word. // 2. Appending non whitespace at the beginning of word. // 3. Appending non whitespace at the end of word. // Note that, appending only whitespaces at the beginning or end of word won't change the word, so we don't need to // remove the markers on that word. // Of course, if current selection is a range, we potentially will edit two words that fall on the boundaries of // selection, and remove words between the selection boundaries. // VisiblePosition startOfSelection = frame()->selection()->selection().start(); VisiblePosition endOfSelection = frame()->selection()->selection().end(); if (startOfSelection.isNull()) return; // First word is the word that ends after or on the start of selection. VisiblePosition startOfFirstWord = startOfWord(startOfSelection, LeftWordIfOnBoundary); VisiblePosition endOfFirstWord = endOfWord(startOfSelection, LeftWordIfOnBoundary); // Last word is the word that begins before or on the end of selection VisiblePosition startOfLastWord = startOfWord(endOfSelection, RightWordIfOnBoundary); VisiblePosition endOfLastWord = endOfWord(endOfSelection, RightWordIfOnBoundary); if (startOfFirstWord.isNull()) { startOfFirstWord = startOfWord(startOfSelection, RightWordIfOnBoundary); endOfFirstWord = endOfWord(startOfSelection, RightWordIfOnBoundary); } if (endOfLastWord.isNull()) { startOfLastWord = startOfWord(endOfSelection, LeftWordIfOnBoundary); endOfLastWord = endOfWord(endOfSelection, LeftWordIfOnBoundary); } // If doNotRemoveIfSelectionAtWordBoundary is true, and first word ends at the start of selection, // we choose next word as the first word. if (doNotRemoveIfSelectionAtWordBoundary && endOfFirstWord == startOfSelection) { startOfFirstWord = nextWordPosition(startOfFirstWord); endOfFirstWord = endOfWord(startOfFirstWord, RightWordIfOnBoundary); if (startOfFirstWord == endOfSelection) return; } // If doNotRemoveIfSelectionAtWordBoundary is true, and last word begins at the end of selection, // we choose previous word as the last word. if (doNotRemoveIfSelectionAtWordBoundary && startOfLastWord == endOfSelection) { startOfLastWord = previousWordPosition(startOfLastWord); endOfLastWord = endOfWord(startOfLastWord, RightWordIfOnBoundary); if (endOfLastWord == startOfSelection) return; } if (startOfFirstWord.isNull() || endOfFirstWord.isNull() || startOfLastWord.isNull() || endOfLastWord.isNull()) return; // Now we remove markers on everything between startOfFirstWord and endOfLastWord. // However, if an autocorrection change a single word to multiple words, we want to remove correction mark from all the // resulted words even we only edit one of them. For example, assuming autocorrection changes "avantgarde" to "avant // garde", we will have CorrectionIndicator marker on both words and on the whitespace between them. If we then edit garde, // we would like to remove the marker from word "avant" and whitespace as well. So we need to get the continous range of // of marker that contains the word in question, and remove marker on that whole range. Document* document = m_frame->document(); RefPtr wordRange = Range::create(document, startOfFirstWord.deepEquivalent(), endOfLastWord.deepEquivalent()); document->markers()->removeMarkers(wordRange.get(), DocumentMarker::Spelling | DocumentMarker::Grammar, DocumentMarkerController::RemovePartiallyOverlappingMarker); } PassRefPtr Editor::rangeForPoint(const IntPoint& windowPoint) { Document* document = m_frame->documentAtPoint(windowPoint); if (!document) return 0; Frame* frame = document->frame(); ASSERT(frame); FrameView* frameView = frame->view(); if (!frameView) return 0; IntPoint framePoint = frameView->windowToContents(windowPoint); VisibleSelection selection(frame->visiblePositionForPoint(framePoint)); return selection.toNormalizedRange().get(); } void Editor::revealSelectionAfterEditingOperation(const ScrollAlignment& alignment, RevealExtentOption revealExtentOption) { if (m_preventRevealSelection) return; m_frame->selection()->revealSelection(alignment, revealExtentOption); } bool Editor::setSelectionOffsets(int selectionStart, int selectionEnd) { Element* rootEditableElement = m_frame->selection()->rootEditableElement(); if (!rootEditableElement) return false; RefPtr range = TextIterator::rangeFromLocationAndLength(rootEditableElement, selectionStart, selectionEnd - selectionStart); if (!range) return false; return m_frame->selection()->setSelectedRange(range.get(), VP_DEFAULT_AFFINITY, true); } void Editor::transpose() { if (!canEdit()) return; VisibleSelection selection = m_frame->selection()->selection(); if (!selection.isCaret()) return; // Make a selection that goes back one character and forward two characters. VisiblePosition caret = selection.visibleStart(); VisiblePosition next = isEndOfParagraph(caret) ? caret : caret.next(); VisiblePosition previous = next.previous(); if (next == previous) return; previous = previous.previous(); if (!inSameParagraph(next, previous)) return; RefPtr range = makeRange(previous, next); if (!range) return; VisibleSelection newSelection(range.get(), DOWNSTREAM); // Transpose the two characters. String text = plainText(range.get()); if (text.length() != 2) return; String transposed = text.right(1) + text.left(1); // Select the two characters. if (newSelection != m_frame->selection()->selection()) { if (!m_frame->selection()->shouldChangeSelection(newSelection)) return; m_frame->selection()->setSelection(newSelection); } // Insert the transposed characters. if (!shouldInsertText(transposed, range.get(), EditorInsertActionTyped)) return; replaceSelectionWithText(transposed, false, false); } void Editor::addToKillRing(Range* range, bool prepend) { if (m_shouldStartNewKillRingSequence) killRing()->startNewSequence(); String text = plainText(range); if (prepend) killRing()->prepend(text); else killRing()->append(text); m_shouldStartNewKillRingSequence = false; } void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, FrameSelection::SetSelectionOptions options) { // If the new selection is orphaned, then don't update the selection. if (newSelection.start().isOrphan() || newSelection.end().isOrphan()) return; // If there is no selection change, don't bother sending shouldChangeSelection, but still call setSelection, // because there is work that it must do in this situation. // The old selection can be invalid here and calling shouldChangeSelection can produce some strange calls. // See Some shouldChangeSelectedDOMRange contain Ranges for selections that are no longer valid bool selectionDidNotChangeDOMPosition = newSelection == m_frame->selection()->selection(); if (selectionDidNotChangeDOMPosition || m_frame->selection()->shouldChangeSelection(newSelection)) m_frame->selection()->setSelection(newSelection, options); // Some editing operations change the selection visually without affecting its position within the DOM. // For example when you press return in the following (the caret is marked by ^): //
^Hello
// WebCore inserts

*before* the current block, which correctly moves the paragraph down but which doesn't // change the caret's DOM position (["hello", 0]). In these situations the above FrameSelection::setSelection call // 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 (selectionDidNotChangeDOMPosition && client()) client()->respondToChangedSelection(m_frame); } IntRect Editor::firstRectForRange(Range* range) const { LayoutUnit extraWidthToEndOfLine = 0; ASSERT(range->startContainer()); ASSERT(range->endContainer()); IntRect startCaretRect = RenderedPosition(VisiblePosition(range->startPosition()).deepEquivalent(), DOWNSTREAM).absoluteRect(&extraWidthToEndOfLine); if (startCaretRect == LayoutRect()) return IntRect(); IntRect endCaretRect = RenderedPosition(VisiblePosition(range->endPosition()).deepEquivalent(), UPSTREAM).absoluteRect(); if (endCaretRect == LayoutRect()) return IntRect(); if (startCaretRect.y() == endCaretRect.y()) { // start and end are on the same line return IntRect(min(startCaretRect.x(), endCaretRect.x()), startCaretRect.y(), abs(endCaretRect.x() - startCaretRect.x()), max(startCaretRect.height(), endCaretRect.height())); } // start and end aren't on the same line, so go from start to the end of its line return IntRect(startCaretRect.x(), startCaretRect.y(), startCaretRect.width() + extraWidthToEndOfLine, startCaretRect.height()); } bool Editor::shouldChangeSelection(const VisibleSelection& oldSelection, const VisibleSelection& newSelection, EAffinity affinity, bool stillSelecting) const { return client() && client()->shouldChangeSelectedRange(oldSelection.toNormalizedRange().get(), newSelection.toNormalizedRange().get(), affinity, stillSelecting); } void Editor::computeAndSetTypingStyle(StylePropertySet* style, EditAction editingAction) { if (!style || style->isEmpty()) { m_frame->selection()->clearTypingStyle(); return; } // Calculate the current typing style. RefPtr 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); // Handle block styles, substracting these from the typing style. RefPtr blockStyle = typingStyle->extractAndRemoveBlockProperties(); if (!blockStyle->isEmpty()) applyCommand(ApplyStyleCommand::create(m_frame->document(), blockStyle.get(), editingAction)); // Set the remaining style as the typing style. m_frame->selection()->setTypingStyle(typingStyle); } void Editor::textFieldDidBeginEditing(Element* e) { if (isContinuousSpellCheckingEnabled()) { Element* element = toHTMLTextFormControlElement(e)->innerTextElement(); VisibleSelection selection = VisibleSelection::selectionFromContentsOfNode(element); markMisspellingsAndBadGrammar(selection); } } void Editor::textFieldDidEndEditing(Element* e) { // Remove markers when deactivating a selection in an . // Prevent new ones from appearing too. m_spellChecker->cancelCheck(); HTMLTextFormControlElement* textFormControlElement = toHTMLTextFormControlElement(e); HTMLElement* innerText = textFormControlElement->innerTextElement(); DocumentMarker::MarkerTypes markerTypes(DocumentMarker::Spelling); if (isGrammarCheckingEnabled() || unifiedTextCheckerEnabled()) markerTypes.add(DocumentMarker::Grammar); for (Node* node = innerText; node; node = NodeTraversal::next(node, innerText)) { m_frame->document()->markers()->removeMarkers(node, markerTypes); } if (client()) client()->textFieldDidEndEditing(e); } void Editor::textDidChangeInTextField(Element* e) { if (client()) client()->textDidChangeInTextField(e); } bool Editor::doTextFieldCommandFromEvent(Element* e, KeyboardEvent* ke) { if (client()) return client()->doTextFieldCommandFromEvent(e, ke); return false; } void Editor::applyEditingStyleToBodyElement() const { RefPtr list = m_frame->document()->getElementsByTagName("body"); unsigned len = list->length(); for (unsigned i = 0; i < len; i++) applyEditingStyleToElement(toElement(list->item(i))); } void Editor::applyEditingStyleToElement(Element* element) const { if (!element) return; ASSERT(element->isStyledElement()); if (!element->isStyledElement()) return; // Mutate using the CSSOM wrapper so we get the same event behavior as a script. CSSStyleDeclaration* style = element->style(); style->setPropertyInternal(CSSPropertyWordWrap, "break-word", false, IGNORE_EXCEPTION); style->setPropertyInternal(CSSPropertyWebkitLineBreak, "after-white-space", false, IGNORE_EXCEPTION); } // Searches from the beginning of the document if nothing is selected. bool Editor::findString(const String& target, bool forward, bool caseFlag, bool wrapFlag, bool startInSelection) { FindOptions options = (forward ? 0 : Backwards) | (caseFlag ? 0 : CaseInsensitive) | (wrapFlag ? WrapAround : 0) | (startInSelection ? StartInSelection : 0); return findString(target, options); } bool Editor::findString(const String& target, FindOptions options) { VisibleSelection selection = m_frame->selection()->selection(); RefPtr resultRange = rangeOfString(target, selection.firstRange().get(), options); if (!resultRange) return false; m_frame->selection()->setSelection(VisibleSelection(resultRange.get(), DOWNSTREAM)); m_frame->selection()->revealSelection(); return true; } PassRefPtr Editor::findStringAndScrollToVisible(const String& target, Range* previousMatch, FindOptions options) { RefPtr nextMatch = rangeOfString(target, previousMatch, options); if (!nextMatch) return 0; nextMatch->firstNode()->renderer()->scrollRectToVisible(nextMatch->boundingBox(), ScrollAlignment::alignCenterIfNeeded, ScrollAlignment::alignCenterIfNeeded); return nextMatch.release(); } PassRefPtr Editor::rangeOfString(const String& target, Range* referenceRange, FindOptions options) { if (target.isEmpty()) return 0; // 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. RefPtr searchRange(rangeOfContents(m_frame->document())); bool forward = !(options & Backwards); bool startInReferenceRange = referenceRange && (options & StartInSelection); if (referenceRange) { if (forward) searchRange->setStart(startInReferenceRange ? referenceRange->startPosition() : referenceRange->endPosition()); else searchRange->setEnd(startInReferenceRange ? referenceRange->endPosition() : referenceRange->startPosition()); } RefPtr shadowTreeRoot = referenceRange && referenceRange->startContainer() ? referenceRange->startContainer()->nonBoundaryShadowTreeRootNode() : 0; if (shadowTreeRoot) { if (forward) searchRange->setEnd(shadowTreeRoot.get(), shadowTreeRoot->childNodeCount()); else searchRange->setStart(shadowTreeRoot.get(), 0); } RefPtr resultRange(findPlainText(searchRange.get(), 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)) { searchRange = rangeOfContents(m_frame->document()); if (forward) searchRange->setStart(referenceRange->endPosition()); else searchRange->setEnd(referenceRange->startPosition()); if (shadowTreeRoot) { if (forward) searchRange->setEnd(shadowTreeRoot.get(), shadowTreeRoot->childNodeCount()); else searchRange->setStart(shadowTreeRoot.get(), 0); } resultRange = findPlainText(searchRange.get(), 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) { searchRange = rangeOfContents(m_frame->document()); if (forward) searchRange->setStartAfter(shadowTreeRoot->shadowHost()); else searchRange->setEndBefore(shadowTreeRoot->shadowHost()); resultRange = findPlainText(searchRange.get(), 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) { searchRange = rangeOfContents(m_frame->document()); resultRange = findPlainText(searchRange.get(), 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(); } void Editor::setMarkedTextMatchesAreHighlighted(bool flag) { if (flag == m_areMarkedTextMatchesHighlighted) return; m_areMarkedTextMatchesHighlighted = flag; m_frame->document()->markers()->repaintMarkers(DocumentMarker::TextMatch); } void Editor::respondToChangedSelection(const VisibleSelection& oldSelection, FrameSelection::SetSelectionOptions options) { bool closeTyping = options & FrameSelection::CloseTyping; bool isContinuousSpellCheckingEnabled = this->isContinuousSpellCheckingEnabled(); bool isContinuousGrammarCheckingEnabled = isContinuousSpellCheckingEnabled && isGrammarCheckingEnabled(); if (isContinuousSpellCheckingEnabled) { VisibleSelection newAdjacentWords; VisibleSelection newSelectedSentence; bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); if (m_frame->selection()->selection().isContentEditable() || caretBrowsing) { VisiblePosition newStart(m_frame->selection()->selection().visibleStart()); newAdjacentWords = VisibleSelection(startOfWord(newStart, LeftWordIfOnBoundary), endOfWord(newStart, RightWordIfOnBoundary)); if (isContinuousGrammarCheckingEnabled) 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() && !isSelectionInTextField(oldSelection)) { spellCheckOldSelection(oldSelection, newAdjacentWords, newSelectedSentence); } if (!textChecker() || textChecker()->shouldEraseMarkersAfterChangeSelection(TextCheckingTypeSpelling)) { if (RefPtr wordRange = newAdjacentWords.toNormalizedRange()) m_frame->document()->markers()->removeMarkers(wordRange.get(), DocumentMarker::Spelling); } if (!textChecker() || textChecker()->shouldEraseMarkersAfterChangeSelection(TextCheckingTypeGrammar)) { if (RefPtr sentenceRange = newSelectedSentence.toNormalizedRange()) m_frame->document()->markers()->removeMarkers(sentenceRange.get(), DocumentMarker::Grammar); } } // When continuous spell checking is off, existing markers disappear after the selection changes. if (!isContinuousSpellCheckingEnabled) m_frame->document()->markers()->removeMarkers(DocumentMarker::Spelling); if (!isContinuousGrammarCheckingEnabled) m_frame->document()->markers()->removeMarkers(DocumentMarker::Grammar); m_frame->inputMethodController().cancelCompositionIfSelectionIsInvalid(); notifyComponentsOnChangedSelection(oldSelection, options); } void Editor::spellCheckAfterBlur() { if (!m_frame->selection()->selection().isContentEditable()) return; if (isSelectionInTextField(m_frame->selection()->selection())) { // textFieldDidEndEditing() and textFieldDidBeginEditing() handle this. return; } VisibleSelection empty; spellCheckOldSelection(m_frame->selection()->selection(), empty, empty); } void Editor::spellCheckOldSelection(const VisibleSelection& oldSelection, const VisibleSelection& newAdjacentWords, const VisibleSelection& newSelectedSentence) { VisiblePosition oldStart(oldSelection.visibleStart()); VisibleSelection oldAdjacentWords = VisibleSelection(startOfWord(oldStart, LeftWordIfOnBoundary), endOfWord(oldStart, RightWordIfOnBoundary)); if (oldAdjacentWords != newAdjacentWords) { if (isContinuousSpellCheckingEnabled() && isGrammarCheckingEnabled()) { VisibleSelection selectedSentence = VisibleSelection(startOfSentence(oldStart), endOfSentence(oldStart)); markMisspellingsAndBadGrammar(oldAdjacentWords, selectedSentence != newSelectedSentence, selectedSentence); } else { markMisspellingsAndBadGrammar(oldAdjacentWords, false, oldAdjacentWords); } } } static Node* findFirstMarkable(Node* node) { while (node) { if (!node->renderer()) return 0; if (node->renderer()->isText()) return node; if (node->renderer()->isTextControl()) node = toRenderTextControl(node->renderer())->textFormControlElement()->visiblePositionForIndex(1).deepEquivalent().deprecatedNode(); else if (node->firstChild()) node = node->firstChild(); else node = node->nextSibling(); } return 0; } bool Editor::selectionStartHasMarkerFor(DocumentMarker::MarkerType markerType, int from, int length) const { Node* node = findFirstMarkable(m_frame->selection()->start().deprecatedNode()); if (!node) return false; unsigned int startOffset = static_cast(from); unsigned int endOffset = static_cast(from + length); Vector markers = m_frame->document()->markers()->markersFor(node); for (size_t i = 0; i < markers.size(); ++i) { DocumentMarker* marker = markers[i]; if (marker->startOffset() <= startOffset && endOffset <= marker->endOffset() && marker->type() == markerType) return true; } return false; } TextCheckingTypeMask Editor::resolveTextCheckingTypeMask(TextCheckingTypeMask textCheckingOptions) { bool shouldMarkSpelling = textCheckingOptions & TextCheckingTypeSpelling; bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; TextCheckingTypeMask checkingTypes = 0; if (shouldMarkSpelling) checkingTypes |= TextCheckingTypeSpelling; if (shouldMarkGrammar) checkingTypes |= TextCheckingTypeGrammar; return checkingTypes; } bool Editor::unifiedTextCheckerEnabled() const { return WebCore::unifiedTextCheckerEnabled(m_frame); } void Editor::toggleOverwriteModeEnabled() { m_overwriteModeEnabled = !m_overwriteModeEnabled; frame()->selection()->setShouldShowBlockCursor(m_overwriteModeEnabled); }; } // namespace WebCore