/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/TextEditor.h" #include #include "EditAggregateTransaction.h" #include "HTMLEditUtils.h" #include "InternetCiter.h" #include "PlaceholderTransaction.h" #include "gfxFontUtils.h" #include "mozilla/Assertions.h" #include "mozilla/ContentIterator.h" #include "mozilla/EditAction.h" #include "mozilla/EditorDOMPoint.h" #include "mozilla/HTMLEditor.h" #include "mozilla/IMEStateManager.h" #include "mozilla/LookAndFeel.h" #include "mozilla/mozalloc.h" #include "mozilla/Preferences.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_editor.h" #include "mozilla/TextComposition.h" #include "mozilla/TextEvents.h" #include "mozilla/TextServicesDocument.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/StaticRange.h" #include "nsAString.h" #include "nsCRT.h" #include "nsCaret.h" #include "nsCharTraits.h" #include "nsComponentManagerUtils.h" #include "nsContentCID.h" #include "nsContentList.h" #include "nsCopySupport.h" #include "nsDebug.h" #include "nsDependentSubstring.h" #include "nsError.h" #include "nsGkAtoms.h" #include "nsIClipboard.h" #include "nsIContent.h" #include "nsIDocumentEncoder.h" #include "nsINode.h" #include "nsIPrincipal.h" #include "nsISelectionController.h" #include "nsISupportsPrimitives.h" #include "nsITransferable.h" #include "nsIWeakReferenceUtils.h" #include "nsNameSpaceManager.h" #include "nsLiteralString.h" #include "nsReadableUtils.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsTextFragment.h" #include "nsTextNode.h" #include "nsUnicharUtils.h" #include "nsXPCOM.h" class nsIOutputStream; class nsISupports; namespace mozilla { using namespace dom; using ChildBlockBoundary = HTMLEditUtils::ChildBlockBoundary; TextEditor::TextEditor() : mMaxTextLength(-1), mUnmaskedStart(UINT32_MAX), mUnmaskedLength(0), mIsMaskingPassword(true) { // printf("Size of TextEditor: %zu\n", sizeof(TextEditor)); static_assert( sizeof(TextEditor) <= 512, "TextEditor instance should be allocatable in the quantum class bins"); } TextEditor::~TextEditor() { // Remove event listeners. Note that if we had an HTML editor, // it installed its own instead of these RemoveEventListeners(); } NS_IMPL_CYCLE_COLLECTION_CLASS(TextEditor) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TextEditor, EditorBase) if (tmp->mMaskTimer) { tmp->mMaskTimer->Cancel(); } NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedDocumentEncoder) NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaskTimer) NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedDocumentEncoder) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaskTimer) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase) NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextEditor) NS_INTERFACE_MAP_ENTRY(nsITimerCallback) NS_INTERFACE_MAP_ENTRY(nsINamed) NS_INTERFACE_MAP_END_INHERITING(EditorBase) nsresult TextEditor::Init(Document& aDoc, Element* aRoot, nsISelectionController* aSelCon, uint32_t aFlags, const nsAString& aInitialValue) { MOZ_ASSERT(!AsHTMLEditor()); MOZ_ASSERT(!mInitSucceeded, "TextEditor::Init() called again without calling PreDestroy()?"); // Init the base editor nsresult rv = EditorBase::Init(aDoc, aRoot, aSelCon, aFlags, aInitialValue); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::Init() failed"); return rv; } // XXX `eNotEditing` is a lie since InitEditorContentAndSelection() may // insert padding `
`. AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_FAILURE; } // We set mInitSucceeded here rather than at the end of the function, // since InitEditorContentAndSelection() can perform some transactions // and can warn if mInitSucceeded is still false. MOZ_ASSERT(!mInitSucceeded, "TextEditor::Init() shouldn't be nested"); mInitSucceeded = true; rv = InitEditorContentAndSelection(); if (NS_FAILED(rv)) { NS_WARNING("TextEditor::InitEditorContentAndSelection() failed"); // XXX Sholdn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this // is a public method? mInitSucceeded = false; return EditorBase::ToGenericNSResult(rv); } // Throw away the old transaction manager if this is not the first time that // we're initializing the editor. ClearUndoRedo(); EnableUndoRedo(); return NS_OK; } NS_IMETHODIMP TextEditor::SetDocumentCharacterSet( const nsACString& characterSet) { AutoEditActionDataSetter editActionData(*this, EditAction::eSetCharacterSet); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } rv = EditorBase::SetDocumentCharacterSet(characterSet); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::SetDocumentCharacterSet() failed"); return EditorBase::ToGenericNSResult(rv); } // Update META charset element. RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return NS_ERROR_NOT_INITIALIZED; } if (UpdateMetaCharset(*document, characterSet)) { return NS_OK; } RefPtr headElementList = document->GetElementsByTagName(u"head"_ns); if (NS_WARN_IF(!headElementList)) { return NS_OK; } nsCOMPtr primaryHeadElement = headElementList->Item(0); if (NS_WARN_IF(!primaryHeadElement)) { return NS_OK; } // Create a new meta charset tag RefPtr metaElement = CreateNodeWithTransaction( *nsGkAtoms::meta, EditorDOMPoint(primaryHeadElement, 0)); if (!metaElement) { NS_WARNING( "EditorBase::CreateNodeWithTransaction(nsGkAtoms::meta) failed, but " "ignored"); return NS_OK; } // Set attributes to the created element if (characterSet.IsEmpty()) { return NS_OK; } // not undoable, undo should undo CreateNodeWithTransaction(). DebugOnly rvIgnored = NS_OK; rvIgnored = metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, u"Content-Type"_ns, true); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Element::SetAttr(nsGkAtoms::httpEquiv, Content-Type) " "failed, but ignored"); rvIgnored = metaElement->SetAttr( kNameSpaceID_None, nsGkAtoms::content, u"text/html;charset="_ns + NS_ConvertASCIItoUTF16(characterSet), true); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "Element::SetAttr(nsGkAtoms::content) failed, but ignored"); return NS_OK; } bool TextEditor::UpdateMetaCharset(Document& aDocument, const nsACString& aCharacterSet) { // get a list of META tags RefPtr metaElementList = aDocument.GetElementsByTagName(u"meta"_ns); if (NS_WARN_IF(!metaElementList)) { return false; } for (uint32_t i = 0; i < metaElementList->Length(true); ++i) { RefPtr metaElement = metaElementList->Item(i)->AsElement(); MOZ_ASSERT(metaElement); nsAutoString currentValue; metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue); if (!FindInReadable(u"content-type"_ns, currentValue, nsCaseInsensitiveStringComparator)) { continue; } metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::content, currentValue); constexpr auto charsetEquals = u"charset="_ns; nsAString::const_iterator originalStart, start, end; originalStart = currentValue.BeginReading(start); currentValue.EndReading(end); if (!FindInReadable(charsetEquals, start, end, nsCaseInsensitiveStringComparator)) { continue; } // set attribute to charset=text/html nsresult rv = SetAttributeWithTransaction( *metaElement, *nsGkAtoms::content, Substring(originalStart, start) + charsetEquals + NS_ConvertASCIItoUTF16(aCharacterSet)); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction(nsGkAtoms::content) failed"); return NS_SUCCEEDED(rv); } return false; } nsresult TextEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) { // NOTE: When you change this method, you should also change: // * editor/libeditor/tests/test_texteditor_keyevent_handling.html // * editor/libeditor/tests/test_htmleditor_keyevent_handling.html // // And also when you add new key handling, you need to change the subclass's // HandleKeyPressEvent()'s switch statement. if (IsReadonly()) { // When we're not editable, the events handled on EditorBase. return EditorBase::HandleKeyPressEvent(aKeyboardEvent); } if (NS_WARN_IF(!aKeyboardEvent)) { return NS_ERROR_UNEXPECTED; } MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress, "HandleKeyPressEvent gets non-keypress event"); switch (aKeyboardEvent->mKeyCode) { case NS_VK_META: case NS_VK_WIN: case NS_VK_SHIFT: case NS_VK_CONTROL: case NS_VK_ALT: // These keys are handled on EditorBase return EditorBase::HandleKeyPressEvent(aKeyboardEvent); case NS_VK_BACK: { if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) { return NS_OK; } DebugOnly rvIgnored = DeleteSelectionAsAction(nsIEditor::ePrevious, nsIEditor::eStrip); aKeyboardEvent->PreventDefault(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::DeleteSelectionAsAction() failed, but ignored"); return NS_OK; } case NS_VK_DELETE: { // on certain platforms (such as windows) the shift key // modifies what delete does (cmd_cut in this case). // bailing here to allow the keybindings to do the cut. if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) { return NS_OK; } DebugOnly rvIgnored = DeleteSelectionAsAction(nsIEditor::eNext, nsIEditor::eStrip); aKeyboardEvent->PreventDefault(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "EditorBase::DeleteSelectionAsAction() failed, but ignored"); return NS_OK; } case NS_VK_TAB: { if (IsTabbable()) { return NS_OK; // let it be used for focus switching } if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) { return NS_OK; } // else we insert the tab straight through aKeyboardEvent->PreventDefault(); nsresult rv = OnInputText(u"\t"_ns); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::OnInputText(\\t) failed"); return rv; } case NS_VK_RETURN: { if (!aKeyboardEvent->IsInputtingLineBreak()) { return NS_OK; } if (!IsSingleLineEditor()) { aKeyboardEvent->PreventDefault(); } // We need to dispatch "beforeinput" event at least even if we're a // single line text editor. nsresult rv = InsertLineBreakAsAction(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::InsertLineBreakAsAction() failed"); return rv; } } if (!aKeyboardEvent->IsInputtingText()) { // we don't PreventDefault() here or keybindings like control-x won't work return NS_OK; } // Our widget shouldn't set `\r` to `mCharCode`, but it may be synthesized // keyboard event and its value may be `\r`. In such case, we should treat // it as `\n` for the backward compatibility because we stopped converting // `\r` and `\r\n` to `\n` at getting `HTMLInputElement.value` and // `HTMLTextAreaElement.value` for the performance (i.e., we don't need to // take care in `HTMLEditor`). char16_t charCode = static_cast(aKeyboardEvent->mCharCode) == nsCRT::CR ? nsCRT::LF : static_cast(aKeyboardEvent->mCharCode); aKeyboardEvent->PreventDefault(); nsAutoString str(charCode); nsresult rv = OnInputText(str); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::OnInputText() failed"); return rv; } nsresult TextEditor::OnInputText(const nsAString& aStringToInsert) { AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText); MOZ_ASSERT(!aStringToInsert.IsVoid()); editActionData.SetData(aStringToInsert); // FYI: For conforming to current UI Events spec, we should dispatch // "beforeinput" event before "keypress" event, but here is in a // "keypress" event listener. However, the other browsers dispatch // "beforeinput" event after "keypress" event. Therefore, it makes // sense to follow the other browsers. Spec issue: // https://github.com/w3c/uievents/issues/220 nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, ScrollSelectionIntoView::Yes); rv = InsertTextAsSubAction(aStringToInsert); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::InsertTextAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) { AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak, aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } if (IsSingleLineEditor()) { return NS_OK; } // XXX This may be called by execCommand() with "insertParagraph". // In such case, naming the transaction "TypingTxnName" is odd. AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, ScrollSelectionIntoView::Yes); rv = InsertLineBreakAsSubAction(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::InsertLineBreakAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::SetTextAsAction( const nsAString& aString, AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable, nsIPrincipal* aPrincipal) { MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound); MOZ_ASSERT(!AsHTMLEditor()); AutoEditActionDataSetter editActionData(*this, EditAction::eSetText, aPrincipal); if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) { editActionData.MakeBeforeInputEventNonCancelable(); } nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::Yes); rv = SetTextAsSubAction(aString); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::SetTextAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::ReplaceTextAsAction( const nsAString& aString, nsRange* aReplaceRange, AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable, nsIPrincipal* aPrincipal) { MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound); AutoEditActionDataSetter editActionData(*this, EditAction::eReplaceText, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) { editActionData.MakeBeforeInputEventNonCancelable(); } if (!AsHTMLEditor()) { editActionData.SetData(aString); } else { editActionData.InitializeDataTransfer(aString); RefPtr targetRange; if (aReplaceRange) { // Compute offset of the range before dispatching `beforeinput` event // because it may be referred after the DOM tree is changed and the // range may have not computed the offset yet. targetRange = StaticRange::Create( aReplaceRange->GetStartContainer(), aReplaceRange->StartOffset(), aReplaceRange->GetEndContainer(), aReplaceRange->EndOffset(), IgnoreErrors()); NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(), "StaticRange::Create() failed"); } else { Element* editingHost = AsHTMLEditor()->GetActiveEditingHost(); NS_WARNING_ASSERTION(editingHost, "No active editing host, no target ranges"); if (editingHost) { targetRange = StaticRange::Create( editingHost, 0, editingHost, editingHost->Length(), IgnoreErrors()); NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(), "StaticRange::Create() failed"); } } if (targetRange && targetRange->IsPositioned()) { editActionData.AppendTargetRange(*targetRange); } } nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::Yes); // This should emulates inserting text for better undo/redo behavior. IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditorBase::ToGenericNSResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); if (!aReplaceRange) { nsresult rv = SetTextAsSubAction(aString); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::SetTextAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } if (aString.IsEmpty() && aReplaceRange->Collapsed()) { NS_WARNING("Setting value was empty and replaced range was empty"); return NS_OK; } // Note that do not notify selectionchange caused by selecting all text // because it's preparation of our delete implementation so web apps // shouldn't receive such selectionchange before the first mutation. AutoUpdateViewBatch preventSelectionChangeEvent(*this); // Select the range but as far as possible, we should not create new range // even if it's part of special Selection. ErrorResult error; SelectionRefPtr()->RemoveAllRanges(error); if (error.Failed()) { NS_WARNING("Selection::RemoveAllRanges() failed"); return error.StealNSResult(); } MOZ_KnownLive(SelectionRefPtr()) ->AddRangeAndSelectFramesAndNotifyListeners(*aReplaceRange, error); if (error.Failed()) { NS_WARNING("Selection::AddRangeAndSelectFramesAndNotifyListeners() failed"); return error.StealNSResult(); } rv = ReplaceSelectionAsSubAction(aString); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::ReplaceSelectionAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::SetTextAsSubAction(const nsAString& aString) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(mPlaceholderBatch); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eSetText, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); if (IsPlaintextEditor() && !IsIMEComposing() && !IsUndoRedoEnabled() && GetEditAction() != EditAction::eReplaceText && mMaxTextLength < 0) { EditActionResult result = SetTextWithoutTransaction(aString); if (result.Failed() || result.Canceled() || result.Handled()) { NS_WARNING_ASSERTION(result.Succeeded(), "TextEditor::SetTextWithoutTransaction() failed"); return result.Rv(); } } { // Note that do not notify selectionchange caused by selecting all text // because it's preparation of our delete implementation so web apps // shouldn't receive such selectionchange before the first mutation. AutoUpdateViewBatch preventSelectionChangeEvent(*this); RefPtr rootElement = GetRoot(); if (NS_WARN_IF(!rootElement)) { return NS_ERROR_FAILURE; } // We want to select trailing `
` element to remove all nodes to replace // all, but TextEditor::SelectEntireDocument() doesn't select such `
` // elements. // XXX We should make ReplaceSelectionAsSubAction() take range. Then, // we can saving the expensive cost of modifying `Selection` here. nsresult rv; if (IsEmpty()) { rv = MOZ_KnownLive(SelectionRefPtr())->CollapseInLimiter(rootElement, 0); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "Selection::CollapseInLimiter() failed, but ignored"); } else { // XXX Oh, we shouldn't select padding `
` element for empty last // line here since we will need to recreate it in multiline // text editor. ErrorResult error; SelectionRefPtr()->SelectAllChildren(*rootElement, error); NS_WARNING_ASSERTION( !error.Failed(), "Selection::SelectAllChildren() failed, but ignored"); rv = error.StealNSResult(); } if (NS_SUCCEEDED(rv)) { DebugOnly rvIgnored = ReplaceSelectionAsSubAction(aString); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "TextEditor::ReplaceSelectionAsSubAction() failed, but ignored"); } } // Destroying AutoUpdateViewBatch may cause destroying us. return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; } nsresult TextEditor::ReplaceSelectionAsSubAction(const nsAString& aString) { // TODO: Move this method to `EditorBase`. if (aString.IsEmpty()) { nsresult rv = DeleteSelectionAsSubAction( nsIEditor::eNone, IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::DeleteSelectionAsSubAction(eNone) failed"); return rv; } nsresult rv = InsertTextAsSubAction(aString); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::InsertTextAsSubAction() failed"); return rv; } bool TextEditor::EnsureComposition(WidgetCompositionEvent& aCompositionEvent) { if (mComposition) { return true; } // The compositionstart event must cause creating new TextComposition // instance at being dispatched by IMEStateManager. mComposition = IMEStateManager::GetTextCompositionFor(&aCompositionEvent); if (!mComposition) { // However, TextComposition may be committed before the composition // event comes here. return false; } mComposition->StartHandlingComposition(this); return true; } nsresult TextEditor::OnCompositionStart( WidgetCompositionEvent& aCompositionStartEvent) { if (mComposition) { NS_WARNING("There was a composition at receiving compositionstart event"); return NS_OK; } // "beforeinput" event shouldn't be fired before "compositionstart". AutoEditActionDataSetter editActionData(*this, EditAction::eStartComposition); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } EnsureComposition(aCompositionStartEvent); NS_WARNING_ASSERTION(mComposition, "Failed to get TextComposition instance?"); return NS_OK; } nsresult TextEditor::OnCompositionChange( WidgetCompositionEvent& aCompositionChangeEvent) { MOZ_ASSERT(aCompositionChangeEvent.mMessage == eCompositionChange, "The event should be eCompositionChange"); if (!mComposition) { NS_WARNING( "There is no composition, but receiving compositionchange event"); return NS_ERROR_FAILURE; } AutoEditActionDataSetter editActionData(*this, EditAction::eUpdateComposition); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } // If: // - new composition string is not empty, // - there is no composition string in the DOM tree, // - and there is non-collapsed Selection, // the selected content will be removed by this composition. if (aCompositionChangeEvent.mData.IsEmpty() && mComposition->String().IsEmpty() && !SelectionRefPtr()->IsCollapsed()) { editActionData.UpdateEditAction(EditAction::eDeleteByComposition); } // If Input Events Level 2 is enabled, EditAction::eDeleteByComposition is // mapped to EditorInputType::eDeleteByComposition and it requires null // for InputEvent.data. Therefore, only otherwise, we should set data. if (ToInputType(editActionData.GetEditAction()) != EditorInputType::eDeleteByComposition) { MOZ_ASSERT(ToInputType(editActionData.GetEditAction()) == EditorInputType::eInsertCompositionText); MOZ_ASSERT(!aCompositionChangeEvent.mData.IsVoid()); editActionData.SetData(aCompositionChangeEvent.mData); } // If we're an `HTMLEditor` and this is second or later composition change, // we should set target range to the range of composition string. // Otherwise, set target ranges to selection ranges (will be done by // editActionData itself before dispatching `beforeinput` event). if (AsHTMLEditor() && mComposition->GetContainerTextNode()) { RefPtr targetRange = StaticRange::Create( mComposition->GetContainerTextNode(), mComposition->XPOffsetInTextNode(), mComposition->GetContainerTextNode(), mComposition->XPEndOffsetInTextNode(), IgnoreErrors()); NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(), "StaticRange::Create() failed"); if (targetRange && targetRange->IsPositioned()) { editActionData.AppendTargetRange(*targetRange); } } // TODO: We need to use different EditAction value for beforeinput event // if the event is followed by "compositionend" because corresponding // "input" event will be fired from OnCompositionEnd() later with // different EditAction value. // TODO: If Input Events Level 2 is enabled, "beforeinput" event may be // actually canceled if edit action is eDeleteByComposition. In such // case, we might need to keep selected text, but insert composition // string before or after the selection. However, the spec is still // unstable. We should keep handling the composition since other // parts including widget may not be ready for such complicated // behavior. nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (rv != NS_ERROR_EDITOR_ACTION_CANCELED && NS_FAILED(rv)) { NS_WARNING("MaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } if (!EnsureComposition(aCompositionChangeEvent)) { NS_WARNING("TextEditor::EnsureComposition() failed"); return NS_OK; } if (NS_WARN_IF(!GetPresShell())) { return NS_ERROR_NOT_INITIALIZED; } // NOTE: TextComposition should receive selection change notification before // CompositionChangeEventHandlingMarker notifies TextComposition of the // end of handling compositionchange event because TextComposition may // need to ignore selection changes caused by composition. Therefore, // CompositionChangeEventHandlingMarker must be destroyed after a call // of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or // NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies // TextComposition of a selection change. MOZ_ASSERT( !mPlaceholderBatch, "UpdateIMEComposition() must be called without place holder batch"); TextComposition::CompositionChangeEventHandlingMarker compositionChangeEventHandlingMarker(mComposition, &aCompositionChangeEvent); RefPtr caret = GetCaret(); { AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::IMETxnName, ScrollSelectionIntoView::Yes); MOZ_ASSERT( mIsInEditSubAction, "AutoPlaceholderBatch should've notified the observes of before-edit"); nsString data(aCompositionChangeEvent.mData); if (!AsTextEditor()) { nsContentUtils::PlatformToDOMLineBreaks(data); } rv = InsertTextAsSubAction(data); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::InsertTextAsSubAction() failed"); if (caret) { caret->SetSelection(SelectionRefPtr()); } } // If still composing, we should fire input event via observer. // Note that if the composition will be committed by the following // compositionend event, we don't need to notify editor observes of this // change. // NOTE: We must notify after the auto batch will be gone. if (!aCompositionChangeEvent.IsFollowedByCompositionEnd()) { NotifyEditorObservers(eNotifyEditorObserversOfEnd); } return EditorBase::ToGenericNSResult(rv); } void TextEditor::OnCompositionEnd( WidgetCompositionEvent& aCompositionEndEvent) { if (!mComposition) { NS_WARNING("There is no composition, but receiving compositionend event"); return; } EditAction editAction = aCompositionEndEvent.mData.IsEmpty() ? EditAction::eCancelComposition : EditAction::eCommitComposition; AutoEditActionDataSetter editActionData(*this, editAction); // If Input Events Level 2 is enabled, EditAction::eCancelComposition is // mapped to EditorInputType::eDeleteCompositionText and it requires null // for InputEvent.data. Therefore, only otherwise, we should set data. if (ToInputType(editAction) != EditorInputType::eDeleteCompositionText) { MOZ_ASSERT( ToInputType(editAction) == EditorInputType::eInsertCompositionText || ToInputType(editAction) == EditorInputType::eInsertFromComposition); MOZ_ASSERT(!aCompositionEndEvent.mData.IsVoid()); editActionData.SetData(aCompositionEndEvent.mData); } // commit the IME transaction..we can get at it via the transaction mgr. // Note that this means IME won't work without an undo stack! if (mTransactionManager) { if (nsCOMPtr transaction = mTransactionManager->PeekUndoStack()) { if (RefPtr transactionBase = transaction->GetAsEditTransactionBase()) { if (PlaceholderTransaction* placeholderTransaction = transactionBase->GetAsPlaceholderTransaction()) { placeholderTransaction->Commit(); } } } } // Note that this just marks as that we've already handled "beforeinput" for // preventing assertions in FireInputEvent(). Note that corresponding // "beforeinput" event for the following "input" event should've already // been dispatched from `OnCompositionChange()`. DebugOnly rvIgnored = editActionData.MaybeDispatchBeforeInputEvent(); MOZ_ASSERT(rvIgnored != NS_ERROR_EDITOR_ACTION_CANCELED, "Why beforeinput event was canceled in this case?"); MOZ_ASSERT(NS_SUCCEEDED(rvIgnored), "MaybeDispatchBeforeInputEvent() should just mark the instance as " "handled it"); // Composition string may have hidden the caret. Therefore, we need to // cancel it here. HideCaret(false); // FYI: mComposition still keeps storing container text node of committed // string, its offset and length. However, they will be invalidated // soon since its Destroy() will be called by IMEStateManager. mComposition->EndHandlingComposition(this); mComposition = nullptr; // notify editor observers of action // FYI: With current draft, "input" event should be fired from // OnCompositionChange(), however, it requires a lot of our UI code // change and does not make sense. See spec issue: // https://github.com/w3c/uievents/issues/202 NotifyEditorObservers(eNotifyEditorObserversOfEnd); } already_AddRefed TextEditor::GetInputEventTargetElement() const { nsCOMPtr target = do_QueryInterface(mEventTarget); return target.forget(); } bool TextEditor::IsEmpty() const { if (mPaddingBRElementForEmptyEditor) { return true; } // Even if there is no padding
element for empty editor, we should be // detected as empty editor if all the children are text nodes and these // have no content. Element* anonymousDivElement = GetRoot(); if (!anonymousDivElement) { return true; // Don't warn it, this is possible, e.g., 997805.html } // Only when there is non-empty text node, we are not empty. return !anonymousDivElement->GetFirstChild() || !anonymousDivElement->GetFirstChild()->IsText() || !anonymousDivElement->GetFirstChild()->Length(); } NS_IMETHODIMP TextEditor::GetDocumentIsEmpty(bool* aDocumentIsEmpty) { MOZ_ASSERT(aDocumentIsEmpty); *aDocumentIsEmpty = IsEmpty(); return NS_OK; } NS_IMETHODIMP TextEditor::GetTextLength(int32_t* aCount) { MOZ_ASSERT(aCount); // initialize out params *aCount = 0; // special-case for empty document, to account for the padding
element // for empty editor. // XXX This should be overridden by `HTMLEditor` and we should return the // first text node's length from `TextEditor` instead. The following // code is too expensive. if (IsEmpty()) { return NS_OK; } Element* rootElement = GetRoot(); if (NS_WARN_IF(!rootElement)) { return NS_ERROR_FAILURE; } uint32_t totalLength = 0; PostContentIterator postOrderIter; DebugOnly rvIgnored = postOrderIter.Init(rootElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "PostContentIterator::Init() failed, but ignored"); EditorType editorType = GetEditorType(); for (; !postOrderIter.IsDone(); postOrderIter.Next()) { nsINode* currentNode = postOrderIter.GetCurrentNode(); if (currentNode && currentNode->IsText() && EditorUtils::IsEditableContent(*currentNode->AsText(), editorType)) { totalLength += currentNode->Length(); } } *aCount = totalLength; return NS_OK; } nsresult TextEditor::UndoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) { if (aCount == 0 || IsReadonly()) { return NS_OK; } // If we don't have transaction in the undo stack, we shouldn't notify // anybody of trying to undo since it's not useful notification but we // need to pay some runtime cost. if (!CanUndo()) { return NS_OK; } // If there is composition, we shouldn't allow to undo with committing // composition since Chrome doesn't allow it and it doesn't make sense // because committing composition causes one transaction and Undo(1) // undoes the committing composition. if (GetComposition()) { return NS_OK; } AutoEditActionDataSetter editActionData(*this, EditAction::eUndo, aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } AutoUpdateViewBatch preventSelectionChangeEvent(*this); NotifyEditorObservers(eNotifyEditorObserversOfBefore); if (NS_WARN_IF(!CanUndo()) || NS_WARN_IF(Destroyed())) { return NS_ERROR_FAILURE; } rv = NS_OK; { IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eUndo, nsIEditor::eNone, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditorBase::ToGenericNSResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION(!ignoredError.Failed(), "TextEditor::OnStartToHandleTopLevelEditSubAction() " "failed, but ignored"); RefPtr transactionManager(mTransactionManager); for (uint32_t i = 0; i < aCount; ++i) { if (NS_FAILED(transactionManager->Undo())) { NS_WARNING("TransactionManager::Undo() failed"); break; } DoAfterUndoTransaction(); } if (NS_WARN_IF(!mRootElement)) { NS_WARNING("Failed to handle padding BR Element due to no root element"); rv = NS_ERROR_FAILURE; } else { // The idea here is to see if the magic empty node has suddenly // reappeared as the result of the undo. If it has, set our state // so we remember it. There is a tradeoff between doing here and // at redo, or doing it everywhere else that might care. Since undo // and redo are relatively rare, it makes sense to take the (small) // performance hit here. nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafChild( *mRootElement, ChildBlockBoundary::Ignore); if (firstLeafChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) { mPaddingBRElementForEmptyEditor = static_cast(firstLeafChild); } else { mPaddingBRElementForEmptyEditor = nullptr; } } } NotifyEditorObservers(eNotifyEditorObserversOfEnd); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::RedoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) { if (aCount == 0 || IsReadonly()) { return NS_OK; } // If we don't have transaction in the redo stack, we shouldn't notify // anybody of trying to redo since it's not useful notification but we // need to pay some runtime cost. if (!CanRedo()) { return NS_OK; } // If there is composition, we shouldn't allow to redo with committing // composition since Chrome doesn't allow it and it doesn't make sense // because committing composition causes removing all transactions from // the redo queue. So, it becomes impossible to redo anything. if (GetComposition()) { return NS_OK; } AutoEditActionDataSetter editActionData(*this, EditAction::eRedo, aPrincipal); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } AutoUpdateViewBatch preventSelectionChangeEvent(*this); NotifyEditorObservers(eNotifyEditorObserversOfBefore); if (NS_WARN_IF(!CanRedo()) || NS_WARN_IF(Destroyed())) { return NS_ERROR_FAILURE; } rv = NS_OK; { IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eRedo, nsIEditor::eNone, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION(!ignoredError.Failed(), "TextEditor::OnStartToHandleTopLevelEditSubAction() " "failed, but ignored"); RefPtr transactionManager(mTransactionManager); for (uint32_t i = 0; i < aCount; ++i) { if (NS_FAILED(transactionManager->Redo())) { NS_WARNING("TransactionManager::Redo() failed"); break; } DoAfterRedoTransaction(); } if (NS_WARN_IF(!mRootElement)) { NS_WARNING("Failed to handle padding BR element due to no root element"); rv = NS_ERROR_FAILURE; } else { // We may take empty
element for empty editor back with this redo. // We need to store it again. // XXX Looks like that this is too slow if there are a lot of nodes. // Shouldn't we just scan children in the root? nsCOMPtr nodeList = mRootElement->GetElementsByTagName(u"br"_ns); MOZ_ASSERT(nodeList); Element* brElement = nodeList->Length() == 1 ? nodeList->Item(0) : nullptr; if (brElement && EditorUtils::IsPaddingBRElementForEmptyEditor(*brElement)) { mPaddingBRElementForEmptyEditor = static_cast(brElement); } else { mPaddingBRElementForEmptyEditor = nullptr; } } } NotifyEditorObservers(eNotifyEditorObserversOfEnd); return EditorBase::ToGenericNSResult(rv); } bool TextEditor::IsCopyToClipboardAllowedInternal() const { MOZ_ASSERT(IsEditActionDataAvailable()); if (SelectionRefPtr()->IsCollapsed()) { return false; } if (!IsSingleLineEditor() || !IsPasswordEditor()) { return true; } // If we're a password editor, we should allow selected text to be copied // to the clipboard only when selection range is in unmasked range. if (IsAllMasked() || IsMaskingPassword() || mUnmaskedLength == 0) { return false; } // If there are 2 or more ranges, we don't allow to copy/cut for now since // we need to check whether all ranges are in unmasked range or not. // Anyway, such operation in password field does not make sense. if (SelectionRefPtr()->RangeCount() > 1) { return false; } uint32_t selectionStart = 0, selectionEnd = 0; nsContentUtils::GetSelectionInTextControl(SelectionRefPtr(), mRootElement, selectionStart, selectionEnd); return mUnmaskedStart <= selectionStart && UnmaskedEnd() >= selectionEnd; } bool TextEditor::FireClipboardEvent(EventMessage aEventMessage, int32_t aSelectionType, bool* aActionTaken) { MOZ_ASSERT(IsEditActionDataAvailable()); if (aEventMessage == ePaste) { CommitComposition(); } RefPtr presShell = GetPresShell(); if (NS_WARN_IF(!presShell)) { return false; } RefPtr sel = SelectionRefPtr(); if (IsHTMLEditor() && aEventMessage == eCopy && sel->IsCollapsed()) { // If we don't have a usable selection for copy and we're an HTML editor // (which is global for the document) try to use the last focused selection // instead. sel = nsCopySupport::GetSelectionForCopy(GetDocument()); } const bool clipboardEventCanceled = !nsCopySupport::FireClipboardEvent( aEventMessage, aSelectionType, presShell, sel, aActionTaken); NotifyOfDispatchingClipboardEvent(); // If the event handler caused the editor to be destroyed, return false. // Otherwise return true if the event was not cancelled. return !clipboardEventCanceled && !mDidPreDestroy; } nsresult TextEditor::CutAsAction(nsIPrincipal* aPrincipal) { // TODO: Move this method to `EditorBase`. AutoEditActionDataSetter editActionData(*this, EditAction::eCut, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } bool actionTaken = false; if (!FireClipboardEvent(eCut, nsIClipboard::kGlobalClipboard, &actionTaken)) { return EditorBase::ToGenericNSResult( actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED); } // Dispatch "beforeinput" event after dispatching "cut" event. nsresult rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } // XXX This transaction name is referred by PlaceholderTransaction::Merge() // so that we need to keep using it here. AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::DeleteTxnName, ScrollSelectionIntoView::Yes); rv = DeleteSelectionAsSubAction( eNone, IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::DeleteSelectionAsSubAction(eNone) failed, but ignored"); return EditorBase::ToGenericNSResult(rv); } bool TextEditor::AreClipboardCommandsUnconditionallyEnabled() const { Document* document = GetDocument(); return document && document->AreClipboardCommandsUnconditionallyEnabled(); } bool TextEditor::IsCutCommandEnabled() const { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return false; } if (AreClipboardCommandsUnconditionallyEnabled()) { return true; } return IsModifiable() && IsCopyToClipboardAllowedInternal(); } NS_IMETHODIMP TextEditor::Copy() { AutoEditActionDataSetter editActionData(*this, EditAction::eCopy); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } bool actionTaken = false; FireClipboardEvent(eCopy, nsIClipboard::kGlobalClipboard, &actionTaken); return EditorBase::ToGenericNSResult( actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED); } bool TextEditor::IsCopyCommandEnabled() const { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return false; } if (AreClipboardCommandsUnconditionallyEnabled()) { return true; } return IsCopyToClipboardAllowedInternal(); } bool TextEditor::CanDeleteSelection() const { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return false; } return IsModifiable() && !SelectionRefPtr()->IsCollapsed(); } already_AddRefed TextEditor::GetAndInitDocEncoder( const nsAString& aFormatType, uint32_t aDocumentEncoderFlags, const nsACString& aCharset) const { MOZ_ASSERT(IsEditActionDataAvailable()); nsCOMPtr docEncoder; if (!mCachedDocumentEncoder || !mCachedDocumentEncoderType.Equals(aFormatType)) { nsAutoCString formatType; LossyAppendUTF16toASCII(aFormatType, formatType); docEncoder = do_createDocumentEncoder(PromiseFlatCString(formatType).get()); if (NS_WARN_IF(!docEncoder)) { return nullptr; } mCachedDocumentEncoder = docEncoder; mCachedDocumentEncoderType = aFormatType; } else { docEncoder = mCachedDocumentEncoder; } RefPtr doc = GetDocument(); NS_ASSERTION(doc, "Need a document"); nsresult rv = docEncoder->NativeInit( doc, aFormatType, aDocumentEncoderFlags | nsIDocumentEncoder::RequiresReinitAfterOutput); if (NS_FAILED(rv)) { NS_WARNING("nsIDocumentEncoder::NativeInit() failed"); return nullptr; } if (!aCharset.IsEmpty() && !aCharset.EqualsLiteral("null")) { DebugOnly rvIgnored = docEncoder->SetCharset(aCharset); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsIDocumentEncoder::SetCharset() failed, but ignored"); } const int32_t wrapWidth = std::max(WrapWidth(), 0); DebugOnly rvIgnored = docEncoder->SetWrapColumn(wrapWidth); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsIDocumentEncoder::SetWrapColumn() failed, but ignored"); // Set the selection, if appropriate. // We do this either if the OutputSelectionOnly flag is set, // in which case we use our existing selection ... if (aDocumentEncoderFlags & nsIDocumentEncoder::OutputSelectionOnly) { if (NS_FAILED(docEncoder->SetSelection(SelectionRefPtr()))) { NS_WARNING("nsIDocumentEncoder::SetSelection() failed"); return nullptr; } } // ... or if the root element is not a body, // in which case we set the selection to encompass the root. else { dom::Element* rootElement = GetRoot(); if (NS_WARN_IF(!rootElement)) { return nullptr; } if (!rootElement->IsHTMLElement(nsGkAtoms::body)) { if (NS_FAILED(docEncoder->SetContainerNode(rootElement))) { NS_WARNING("nsIDocumentEncoder::SetContainerNode() failed"); return nullptr; } } } return docEncoder.forget(); } NS_IMETHODIMP TextEditor::OutputToString(const nsAString& aFormatType, uint32_t aDocumentEncoderFlags, nsAString& aOutputString) { AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } nsresult rv = ComputeValueInternal(aFormatType, aDocumentEncoderFlags, aOutputString); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::ComputeValueInternal() failed"); // This is low level API for XUL application. So, we should return raw // error code here. return rv; } nsresult TextEditor::ComputeValueInternal(const nsAString& aFormatType, uint32_t aDocumentEncoderFlags, nsAString& aOutputString) const { MOZ_ASSERT(IsEditActionDataAvailable()); // First, let's try to get the value simply only from text node if the // caller wants plaintext value. if (aFormatType.LowerCaseEqualsLiteral("text/plain")) { // If it's necessary to check selection range or the editor wraps hard, // we need some complicated handling. In such case, we need to use the // expensive path. // XXX Anything else what we cannot return the text node data simply? if (!(aDocumentEncoderFlags & (nsIDocumentEncoder::OutputSelectionOnly | nsIDocumentEncoder::OutputWrap))) { EditActionResult result = ComputeValueFromTextNodeAndPaddingBRElement(aOutputString); if (result.Failed() || result.Canceled() || result.Handled()) { NS_WARNING_ASSERTION( result.Succeeded(), "TextEditor::ComputeValueFromTextNodeAndPaddingBRElement() failed"); return result.Rv(); } } } nsAutoCString charset; nsresult rv = GetDocumentCharsetInternal(charset); if (NS_FAILED(rv) || charset.IsEmpty()) { charset.AssignLiteral("windows-1252"); } nsCOMPtr encoder = GetAndInitDocEncoder(aFormatType, aDocumentEncoderFlags, charset); if (!encoder) { NS_WARNING("TextEditor::GetAndInitDocEncoder() failed"); return NS_ERROR_FAILURE; } rv = encoder->EncodeToString(aOutputString); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsIDocumentEncoder::EncodeToString() failed"); return rv; } nsresult TextEditor::PasteAsQuotationAsAction(int32_t aClipboardType, bool aDispatchPasteEvent, nsIPrincipal* aPrincipal) { MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard || aClipboardType == nsIClipboard::kSelectionClipboard); AutoEditActionDataSetter editActionData(*this, EditAction::ePasteAsQuotation, aPrincipal); if (NS_WARN_IF(!editActionData.CanHandle())) { return NS_ERROR_NOT_INITIALIZED; } // Get Clipboard Service nsresult rv; nsCOMPtr clipboard = do_GetService("@mozilla.org/widget/clipboard;1", &rv); if (NS_FAILED(rv)) { NS_WARNING("Failed to get nsIClipboard service"); return rv; } // XXX Why don't we dispatch ePaste event here? // Get the nsITransferable interface for getting the data from the clipboard nsCOMPtr trans; rv = PrepareTransferable(getter_AddRefs(trans)); if (NS_FAILED(rv)) { NS_WARNING("TextEditor::PrepareTransferable() failed"); return EditorBase::ToGenericNSResult(rv); } if (!trans) { return NS_OK; } // Get the Data from the clipboard clipboard->GetData(trans, aClipboardType); // Now we ask the transferable for the data // it still owns the data, we just have a pointer to it. // If it can't support a "text" output of the data the call will fail nsCOMPtr genericDataObj; nsAutoCString flav; rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj)); if (NS_FAILED(rv)) { NS_WARNING("nsITransferable::GetAnyTransferData() failed"); return EditorBase::ToGenericNSResult(rv); } if (!flav.EqualsLiteral(kUnicodeMime) && !flav.EqualsLiteral(kMozTextInternal)) { return NS_OK; } nsCOMPtr text = do_QueryInterface(genericDataObj); if (!text) { return NS_OK; } nsString stuffToPaste; DebugOnly rvIgnored = text->GetData(stuffToPaste); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "nsISupportsString::GetData() failed, but ignored"); if (stuffToPaste.IsEmpty()) { return NS_OK; } editActionData.SetData(stuffToPaste); if (!stuffToPaste.IsEmpty()) { nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste); } // XXX Perhaps, we should dispatch "paste" event with the pasting text data. editActionData.NotifyOfDispatchingClipboardEvent(); rv = editActionData.MaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "MaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::Yes); rv = InsertWithQuotationsAsSubAction(stuffToPaste); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::InsertWithQuotationsAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::InsertWithQuotationsAsSubAction( const nsAString& aQuotedText) { MOZ_ASSERT(IsEditActionDataAvailable()); if (IsReadonly()) { return NS_OK; } // Let the citer quote it for us: nsString quotedStuff; nsresult rv = InternetCiter::GetCiteString(aQuotedText, quotedStuff); if (NS_FAILED(rv)) { NS_WARNING("InternetCiter::GetCiteString() failed"); return rv; } // It's best to put a blank line after the quoted text so that mails // written without thinking won't be so ugly. if (!aQuotedText.IsEmpty() && (aQuotedText.Last() != char16_t('\n'))) { quotedStuff.Append(char16_t('\n')); } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); // XXX Do we need to support paste-as-quotation in password editor (and // also in single line editor)? MaybeDoAutoPasswordMasking(); rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::EnsureNoPaddingBRElementForEmptyEditor() failed"); return rv; } rv = InsertTextAsSubAction(quotedStuff); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::InsertTextAsSubAction() failed"); return rv; } nsresult TextEditor::SharedOutputString(uint32_t aFlags, bool* aIsCollapsed, nsAString& aResult) const { MOZ_ASSERT(IsEditActionDataAvailable()); *aIsCollapsed = SelectionRefPtr()->IsCollapsed(); if (!*aIsCollapsed) { aFlags |= nsIDocumentEncoder::OutputSelectionOnly; } // If the selection isn't collapsed, we'll use the whole document. nsresult rv = ComputeValueInternal(u"text/plain"_ns, aFlags, aResult); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TextEditor::ComputeValueInternal(text/plain) failed"); return rv; } nsresult TextEditor::SelectEntireDocument() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!AsHTMLEditor()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } RefPtr anonymousDivElement = GetRoot(); if (NS_WARN_IF(!anonymousDivElement)) { return NS_ERROR_NOT_INITIALIZED; } // If we're empty, don't select all children because that would select the // padding
element for empty editor. if (IsEmpty()) { nsresult rv = MOZ_KnownLive(SelectionRefPtr()) ->CollapseInLimiter(anonymousDivElement, 0); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Selection::CollapseInLimiter() failed"); return rv; } // XXX We just need to select all of first text node (if there is). // Why do we do this kind of complicated things? // Don't select the trailing BR node if we have one nsCOMPtr childNode; nsresult rv = EditorBase::GetEndChildNode(*SelectionRefPtr(), getter_AddRefs(childNode)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::GetEndChildNode() failed"); return rv; } if (childNode) { childNode = childNode->GetPreviousSibling(); } if (childNode && EditorUtils::IsPaddingBRElementForEmptyLastLine(*childNode)) { ErrorResult error; MOZ_KnownLive(SelectionRefPtr()) ->SetStartAndEndInLimiter(RawRangeBoundary(anonymousDivElement, 0u), EditorRawDOMPoint(childNode), error); NS_WARNING_ASSERTION(!error.Failed(), "Selection::SetStartAndEndInLimiter() failed"); return error.StealNSResult(); } ErrorResult error; SelectionRefPtr()->SelectAllChildren(*anonymousDivElement, error); NS_WARNING_ASSERTION(!error.Failed(), "Selection::SelectAllChildren() failed"); return error.StealNSResult(); } EventTarget* TextEditor::GetDOMEventTarget() const { return mEventTarget; } nsresult TextEditor::SetAttributeOrEquivalent(Element* aElement, nsAtom* aAttribute, const nsAString& aValue, bool aSuppressTransaction) { if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } rv = SetAttributeWithTransaction(*aElement, *aAttribute, aValue); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::RemoveAttributeOrEquivalent(Element* aElement, nsAtom* aAttribute, bool aSuppressTransaction) { if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) { return NS_ERROR_INVALID_ARG; } AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveAttribute); nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); if (NS_FAILED(rv)) { NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return EditorBase::ToGenericNSResult(rv); } rv = RemoveAttributeWithTransaction(*aElement, *aAttribute); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RemoveAttributeWithTransaction() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult TextEditor::EnsurePaddingBRElementForEmptyEditor() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!AsHTMLEditor()); // If there is padding
element for empty editor, we have no work to do. if (mPaddingBRElementForEmptyEditor) { return NS_OK; } // Likewise, nothing to be done if we could never have inserted a trailing //
element. // XXX Why don't we use same path for