diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h index 6458280a2f504..ff7969f9469bf 100644 --- a/editor/libeditor/HTMLEditor.h +++ b/editor/libeditor/HTMLEditor.h @@ -3142,9 +3142,10 @@ class HTMLEditor final : public EditorBase, * * @param aClipboardType nsIClipboard::kGlobalClipboard or * nsIClipboard::kSelectionClipboard. + * @param aEditingHost The editing host. */ - MOZ_CAN_RUN_SCRIPT nsresult - PasteInternal(nsIClipboard::ClipboardType aClipboardType); + MOZ_CAN_RUN_SCRIPT nsresult PasteInternal( + nsIClipboard::ClipboardType aClipboardType, const Element& aEditingHost); [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertWithQuotationsAsSubAction(const nsAString& aQuotedText) final; @@ -3198,8 +3199,8 @@ class HTMLEditor final : public EditorBase, * with , and each chunk not starting with ">" is * inserted as normal text. */ - MOZ_CAN_RUN_SCRIPT nsresult - InsertTextWithQuotationsInternal(const nsAString& aStringToInsert); + MOZ_CAN_RUN_SCRIPT nsresult InsertTextWithQuotationsInternal( + const nsAString& aStringToInsert, const Element& aEditingHost); /** * ReplaceContainerWithTransactionInternal() is implementation of @@ -3722,20 +3723,23 @@ class HTMLEditor final : public EditorBase, [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetSelectionAtDocumentStart(); // Methods for handling plaintext quotations - MOZ_CAN_RUN_SCRIPT nsresult - PasteAsPlaintextQuotation(nsIClipboard::ClipboardType aSelectionType); + MOZ_CAN_RUN_SCRIPT nsresult PasteAsPlaintextQuotation( + nsIClipboard::ClipboardType aSelectionType, const Element& aEditingHost); + enum class AddCites { No, Yes }; /** * Insert a string as quoted text, replacing the selected text (if any). * @param aQuotedText The string to insert. * @param aAddCites Whether to prepend extra ">" to each line * (usually true, unless those characters * have already been added.) + * @param aEditingHost The editing host. * @return aNodeInserted The node spanning the insertion, if applicable. * If aAddCites is false, this will be null. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InsertAsPlaintextQuotation( - const nsAString& aQuotedText, bool aAddCites, nsINode** aNodeInserted); + const nsAString& aQuotedText, AddCites aAddCites, + const Element& aEditingHost, nsINode** aNodeInserted = nullptr); /** * InsertObject() inserts given object at aPointToInsert. @@ -3749,7 +3753,8 @@ class HTMLEditor final : public EditorBase, DeleteSelectedContent aDeleteSelectedContent); class HTMLTransferablePreparer; - nsresult PrepareHTMLTransferable(nsITransferable** aTransferable) const; + nsresult PrepareHTMLTransferable(nsITransferable** aTransferable, + const Element* aEditingHost) const; enum class HavePrivateHTMLFlavor { No, Yes }; MOZ_CAN_RUN_SCRIPT nsresult InsertFromTransferableAtSelection( @@ -3765,7 +3770,8 @@ class HTMLEditor final : public EditorBase, MOZ_CAN_RUN_SCRIPT nsresult InsertFromDataTransfer( const dom::DataTransfer* aDataTransfer, uint32_t aIndex, nsIPrincipal* aSourcePrincipal, const EditorDOMPoint& aDroppedAt, - DeleteSelectedContent aDeleteSelectedContent); + DeleteSelectedContent aDeleteSelectedContent, + const Element& aEditingHost); static HavePrivateHTMLFlavor ClipboardHasPrivateHTMLFlavor( nsIClipboard* clipboard); diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp index 8ba0ca1893ae7..bcae0bd0742c8 100644 --- a/editor/libeditor/HTMLEditorDataTransfer.cpp +++ b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -27,6 +27,8 @@ #include "mozilla/dom/DOMException.h" #include "mozilla/dom/DOMStringList.h" #include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ElementInlines.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/FileBlobImpl.h" #include "mozilla/dom/FileReader.h" @@ -130,11 +132,24 @@ nsresult HTMLEditor::InsertDroppedDataTransferAsAction( "MaybeDispatchBeforeInputEvent() failed"); return rv; } + + if (MOZ_UNLIKELY(!aDroppedAt.IsInContentNode())) { + NS_WARNING("Dropped into non-content node"); + return NS_OK; + } + + const RefPtr editingHost = ComputeEditingHost( + *aDroppedAt.ContainerAs(), LimitInBodyElement::No); + if (MOZ_UNLIKELY(!editingHost)) { + NS_WARNING("Dropped onto non-editable node"); + return NS_OK; + } + uint32_t numItems = aDataTransfer.MozItemCount(); for (uint32_t i = 0; i < numItems; ++i) { DebugOnly rvIgnored = InsertFromDataTransfer(&aDataTransfer, i, aSourcePrincipal, aDroppedAt, - DeleteSelectedContent::No); + DeleteSelectedContent::No, *editingHost); if (NS_WARN_IF(Destroyed())) { return NS_OK; } @@ -1359,7 +1374,8 @@ nsresult HTMLEditor::HTMLWithContextInserter::FragmentFromPasteCreator:: class MOZ_STACK_CLASS HTMLEditor::HTMLTransferablePreparer { public: HTMLTransferablePreparer(const HTMLEditor& aHTMLEditor, - nsITransferable** aTransferable); + nsITransferable** aTransferable, + const Element* aEditingHost); nsresult Run(); @@ -1367,19 +1383,24 @@ class MOZ_STACK_CLASS HTMLEditor::HTMLTransferablePreparer { void AddDataFlavorsInBestOrder(nsITransferable& aTransferable) const; const HTMLEditor& mHTMLEditor; + const Element* const mEditingHost; nsITransferable** mTransferable; }; HTMLEditor::HTMLTransferablePreparer::HTMLTransferablePreparer( - const HTMLEditor& aHTMLEditor, nsITransferable** aTransferable) - : mHTMLEditor{aHTMLEditor}, mTransferable{aTransferable} { + const HTMLEditor& aHTMLEditor, nsITransferable** aTransferable, + const Element* aEditingHost) + : mHTMLEditor{aHTMLEditor}, + mEditingHost(aEditingHost), + mTransferable{aTransferable} { MOZ_ASSERT(mTransferable); MOZ_ASSERT(!*mTransferable); } nsresult HTMLEditor::PrepareHTMLTransferable( - nsITransferable** aTransferable) const { - HTMLTransferablePreparer htmlTransferablePreparer{*this, aTransferable}; + nsITransferable** aTransferable, const Element* aEditingHost) const { + HTMLTransferablePreparer htmlTransferablePreparer{*this, aTransferable, + aEditingHost}; return htmlTransferablePreparer.Run(); } @@ -1418,7 +1439,8 @@ void HTMLEditor::HTMLTransferablePreparer::AddDataFlavorsInBestOrder( // Create the desired DataFlavor for the type of data // we want to get out of the transferable // This should only happen in html editors, not plaintext - if (!mHTMLEditor.IsPlaintextMailComposer()) { + if (!mHTMLEditor.IsPlaintextMailComposer() && + !(mEditingHost && mEditingHost->IsContentEditablePlainTextOnly())) { DebugOnly rvIgnored = aTransferable.AddDataFlavor(kNativeHTMLMime); NS_WARNING_ASSERTION( @@ -2130,7 +2152,7 @@ static void GetStringFromDataTransfer(const DataTransfer* aDataTransfer, nsresult HTMLEditor::InsertFromDataTransfer( const DataTransfer* aDataTransfer, uint32_t aIndex, nsIPrincipal* aSourcePrincipal, const EditorDOMPoint& aDroppedAt, - DeleteSelectedContent aDeleteSelectedContent) { + DeleteSelectedContent aDeleteSelectedContent, const Element& aEditingHost) { MOZ_ASSERT(GetEditAction() == EditAction::eDrop || GetEditAction() == EditAction::ePaste); MOZ_ASSERT(mPlaceholderBatch, @@ -2149,7 +2171,8 @@ nsresult HTMLEditor::InsertFromDataTransfer( const bool hasPrivateHTMLFlavor = types->Contains(NS_LITERAL_STRING_FROM_CSTRING(kHTMLContext)); - const bool isPlaintextEditor = IsPlaintextMailComposer(); + const bool isPlaintextEditor = IsPlaintextMailComposer() || + aEditingHost.IsContentEditablePlainTextOnly(); const SafeToInsertData safeToInsertData = IsSafeToInsertData(aSourcePrincipal); @@ -2295,14 +2318,24 @@ nsresult HTMLEditor::HandlePaste(AutoEditActionDataSetter& aEditActionData, "CanHandleAndMaybeDispatchBeforeInputEvent() failed"); return rv; } - rv = PasteInternal(aClipboardType); + const RefPtr editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (NS_WARN_IF(!editingHost)) { + return NS_ERROR_FAILURE; + } + rv = PasteInternal(aClipboardType, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::PasteInternal() failed"); return rv; } -nsresult HTMLEditor::PasteInternal(nsIClipboard::ClipboardType aClipboardType) { +nsresult HTMLEditor::PasteInternal(nsIClipboard::ClipboardType aClipboardType, + const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); + if (MOZ_UNLIKELY(!IsModifiable())) { + return NS_OK; + } + // Get Clipboard Service nsresult rv = NS_OK; nsCOMPtr clipboard = @@ -2314,7 +2347,7 @@ nsresult HTMLEditor::PasteInternal(nsIClipboard::ClipboardType aClipboardType) { // Get the nsITransferable interface for getting the data from the clipboard nsCOMPtr transferable; - rv = PrepareHTMLTransferable(getter_AddRefs(transferable)); + rv = PrepareHTMLTransferable(getter_AddRefs(transferable), &aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::PrepareHTMLTransferable() failed"); return rv; @@ -2335,11 +2368,6 @@ nsresult HTMLEditor::PasteInternal(nsIClipboard::ClipboardType aClipboardType) { return rv; } - // XXX Why don't you check this first? - if (!IsModifiable()) { - return NS_OK; - } - // also get additional html copy hints, if present nsAutoString contextStr, infoStr; @@ -2433,6 +2461,12 @@ nsresult HTMLEditor::HandlePasteTransferable( return rv; } + const RefPtr editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (NS_WARN_IF(!editingHost)) { + return NS_ERROR_FAILURE; + } + RefPtr dataTransfer = GetInputEventDataTransfer(); if (dataTransfer->HasFile() && dataTransfer->MozItemCount() > 0) { // Now aTransferable has moved to DataTransfer. Use DataTransfer. @@ -2440,7 +2474,7 @@ nsresult HTMLEditor::HandlePasteTransferable( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); rv = InsertFromDataTransfer(dataTransfer, 0, nullptr, EditorDOMPoint(), - DeleteSelectedContent::Yes); + DeleteSelectedContent::Yes, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertFromDataTransfer(" "DeleteSelectedContent::Yes) failed"); @@ -2549,6 +2583,9 @@ nsresult HTMLEditor::PasteNoFormattingAsAction( } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CommitComposition() failed, but ignored"); + if (MOZ_UNLIKELY(!IsModifiable())) { + return NS_OK; + } // Get Clipboard Service nsCOMPtr clipboard( @@ -2582,10 +2619,6 @@ nsresult HTMLEditor::PasteNoFormattingAsAction( return NS_OK; } - if (!IsModifiable()) { - return NS_OK; - } - // Get the Data from the clipboard rv = clipboard->GetData(transferable, aClipboardType, windowContext); if (NS_FAILED(rv)) { @@ -2619,6 +2652,12 @@ bool HTMLEditor::CanPaste(nsIClipboard::ClipboardType aClipboardType) const { return false; } + const RefPtr editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (!editingHost) { + return false; + } + nsresult rv; nsCOMPtr clipboard( do_GetService("@mozilla.org/widget/clipboard;1", &rv)); @@ -2628,7 +2667,8 @@ bool HTMLEditor::CanPaste(nsIClipboard::ClipboardType aClipboardType) const { } // Use the flavors depending on the current editor mask - if (IsPlaintextMailComposer()) { + if (IsPlaintextMailComposer() || + editingHost->IsContentEditablePlainTextOnly()) { AutoTArray flavors; flavors.AppendElements(Span(textEditorFlavors)); bool haveFlavors; @@ -2654,6 +2694,12 @@ bool HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable) { return false; } + const RefPtr editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (!editingHost) { + return false; + } + // If |aTransferable| is null, assume that a paste will succeed. if (!aTransferable) { return true; @@ -2664,7 +2710,8 @@ bool HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable) { // Use the flavors depending on the current editor mask const char** flavors; size_t length; - if (IsPlaintextMailComposer()) { + if (IsPlaintextMailComposer() || + editingHost->IsContentEditablePlainTextOnly()) { flavors = textEditorFlavors; length = ArrayLength(textEditorFlavors); } else { @@ -2702,8 +2749,15 @@ nsresult HTMLEditor::HandlePasteAsQuotation( return rv; } - if (IsPlaintextMailComposer()) { - nsresult rv = PasteAsPlaintextQuotation(aClipboardType); + const RefPtr editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (!editingHost) { + return NS_ERROR_FAILURE; + } + + if (IsPlaintextMailComposer() || + editingHost->IsContentEditablePlainTextOnly()) { + nsresult rv = PasteAsPlaintextQuotation(aClipboardType, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::PasteAsPlaintextQuotation() failed"); return rv; @@ -2804,13 +2858,13 @@ nsresult HTMLEditor::HandlePasteAsQuotation( return rv; } - rv = PasteInternal(aClipboardType); + rv = PasteInternal(aClipboardType, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::PasteInternal() failed"); return rv; } nsresult HTMLEditor::PasteAsPlaintextQuotation( - nsIClipboard::ClipboardType aSelectionType) { + nsIClipboard::ClipboardType aSelectionType, const Element& aEditingHost) { // Get Clipboard Service nsresult rv; nsCOMPtr clipboard = @@ -2877,7 +2931,7 @@ nsresult HTMLEditor::PasteAsPlaintextQuotation( AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); - rv = InsertAsPlaintextQuotation(stuffToPaste, true, 0); + rv = InsertAsPlaintextQuotation(stuffToPaste, AddCites::Yes, aEditingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertAsPlaintextQuotation() failed"); return rv; @@ -2969,20 +3023,26 @@ NS_IMETHODIMP HTMLEditor::InsertTextWithQuotations( return NS_OK; } + const RefPtr editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (NS_WARN_IF(!editingHost)) { + return NS_ERROR_FAILURE; + } + // The whole operation should be undoable in one transaction: // XXX Why isn't enough to use only AutoPlaceholderBatch here? AutoTransactionBatch bundleAllTransactions(*this, __FUNCTION__); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); - rv = InsertTextWithQuotationsInternal(aStringToInsert); + rv = InsertTextWithQuotationsInternal(aStringToInsert, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertTextWithQuotationsInternal() failed"); return EditorBase::ToGenericNSResult(rv); } nsresult HTMLEditor::InsertTextWithQuotationsInternal( - const nsAString& aStringToInsert) { + const nsAString& aStringToInsert, const Element& aEditingHost) { MOZ_ASSERT(!aStringToInsert.IsEmpty()); // We're going to loop over the string, collecting up a "hunk" // that's all the same type (quoted or not), @@ -3052,10 +3112,8 @@ nsresult HTMLEditor::InsertTextWithQuotationsInternal( // If no newline found, lineStart is now strEnd and we can finish up, // inserting from curHunk to lineStart then returning. const nsAString& curHunk = Substring(hunkStart, lineStart); - nsCOMPtr dummyNode; if (curHunkIsQuoted) { - rv = - InsertAsPlaintextQuotation(curHunk, false, getter_AddRefs(dummyNode)); + rv = InsertAsPlaintextQuotation(curHunk, AddCites::No, aEditingHost); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } @@ -3082,7 +3140,14 @@ nsresult HTMLEditor::InsertTextWithQuotationsInternal( nsresult HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText, nsINode** aNodeInserted) { - if (IsPlaintextMailComposer()) { + const RefPtr editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (NS_WARN_IF(!editingHost)) { + return NS_ERROR_FAILURE; + } + + if (IsPlaintextMailComposer() || + editingHost->IsContentEditablePlainTextOnly()) { AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText); MOZ_ASSERT(!aQuotedText.IsVoid()); editActionData.SetData(aQuotedText); @@ -3095,7 +3160,8 @@ nsresult HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText, } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); - rv = InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted); + rv = InsertAsPlaintextQuotation(aQuotedText, AddCites::Yes, *editingHost, + aNodeInserted); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::InsertAsPlaintextQuotation() failed"); return EditorBase::ToGenericNSResult(rv); @@ -3125,7 +3191,8 @@ nsresult HTMLEditor::InsertAsQuotation(const nsAString& aQuotedText, // in that here, quoted material is enclosed in a
 tag
 // in order to preserve the original line wrapping.
 nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
-                                                bool aAddCites,
+                                                AddCites aAddCites,
+                                                const Element& aEditingHost,
                                                 nsINode** aNodeInserted) {
   MOZ_ASSERT(IsEditActionDataAvailable());
 
@@ -3183,59 +3250,67 @@ nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
     }
   }
 
-  // Wrap the inserted quote in a  so we can distinguish it. If we're
-  // inserting into the , we use a  which is displayed as a block
-  // and sized to the screen using 98 viewport width units.
-  // We could use 100vw, but 98vw avoids a horizontal scroll bar where possible.
-  // All this is done to wrap overlong lines to the screen and not to the
-  // container element, the width-restricted body.
-  Result, nsresult> spanElementOrError =
-      DeleteSelectionAndCreateElement(
-          *nsGkAtoms::span, [](HTMLEditor&, Element& aSpanElement,
-                               const EditorDOMPoint& aPointToInsert) {
-            // Add an attribute on the pre node so we'll know it's a quotation.
-            DebugOnly rvIgnored = aSpanElement.SetAttr(
-                kNameSpaceID_None, nsGkAtoms::mozquote, u"true"_ns,
-                aSpanElement.IsInComposedDoc());
-            NS_WARNING_ASSERTION(
-                NS_SUCCEEDED(rvIgnored),
-                nsPrintfCString(
-                    "Element::SetAttr(nsGkAtoms::mozquote, \"true\", %s) "
-                    "failed",
-                    aSpanElement.IsInComposedDoc() ? "true" : "false")
-                    .get());
-            // Allow wrapping on spans so long lines get wrapped to the screen.
-            if (aPointToInsert.IsContainerHTMLElement(nsGkAtoms::body)) {
+  RefPtr containerSpanElement;
+  if (!aEditingHost.IsContentEditablePlainTextOnly()) {
+    // Wrap the inserted quote in a  so we can distinguish it. If we're
+    // inserting into the , we use a  which is displayed as a block
+    // and sized to the screen using 98 viewport width units.
+    // We could use 100vw, but 98vw avoids a horizontal scroll bar where
+    // possible. All this is done to wrap overlong lines to the screen and not
+    // to the container element, the width-restricted body.
+    // XXX I think that we don't need to do this in the web.  This should be
+    // done only for Thunderbird.
+    Result, nsresult> spanElementOrError =
+        DeleteSelectionAndCreateElement(
+            *nsGkAtoms::span, [](HTMLEditor&, Element& aSpanElement,
+                                 const EditorDOMPoint& aPointToInsert) {
+              // Add an attribute on the pre node so we'll know it's a
+              // quotation.
               DebugOnly rvIgnored = aSpanElement.SetAttr(
-                  kNameSpaceID_None, nsGkAtoms::style,
-                  nsLiteralString(u"white-space: pre-wrap; display: block; "
-                                  u"width: 98vw;"),
-                  false);
+                  kNameSpaceID_None, nsGkAtoms::mozquote, u"true"_ns,
+                  aSpanElement.IsInComposedDoc());
               NS_WARNING_ASSERTION(
                   NS_SUCCEEDED(rvIgnored),
-                  "Element::SetAttr(nsGkAtoms::style, \"pre-wrap, block\", "
-                  "false) failed, but ignored");
-            } else {
-              DebugOnly rvIgnored =
-                  aSpanElement.SetAttr(kNameSpaceID_None, nsGkAtoms::style,
-                                       u"white-space: pre-wrap;"_ns, false);
-              NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
-                                   "Element::SetAttr(nsGkAtoms::style, "
-                                   "\"pre-wrap\", false) failed, but ignored");
-            }
-            return NS_OK;
-          });
-  NS_WARNING_ASSERTION(spanElementOrError.isOk(),
-                       "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::"
-                       "span) failed, but ignored");
-
-  // If this succeeded, then set selection inside the pre
-  // so the inserted text will end up there.
-  // If it failed, we don't care what the return value was,
-  // but we'll fall through and try to insert the text anyway.
-  if (spanElementOrError.isOk()) {
+                  nsPrintfCString(
+                      "Element::SetAttr(nsGkAtoms::mozquote, \"true\", %s) "
+                      "failed",
+                      aSpanElement.IsInComposedDoc() ? "true" : "false")
+                      .get());
+              // Allow wrapping on spans so long lines get wrapped to the
+              // screen.
+              if (aPointToInsert.IsContainerHTMLElement(nsGkAtoms::body)) {
+                DebugOnly rvIgnored = aSpanElement.SetAttr(
+                    kNameSpaceID_None, nsGkAtoms::style,
+                    nsLiteralString(u"white-space: pre-wrap; display: block; "
+                                    u"width: 98vw;"),
+                    false);
+                NS_WARNING_ASSERTION(
+                    NS_SUCCEEDED(rvIgnored),
+                    "Element::SetAttr(nsGkAtoms::style, \"pre-wrap, block\", "
+                    "false) failed, but ignored");
+              } else {
+                DebugOnly rvIgnored =
+                    aSpanElement.SetAttr(kNameSpaceID_None, nsGkAtoms::style,
+                                         u"white-space: pre-wrap;"_ns, false);
+                NS_WARNING_ASSERTION(
+                    NS_SUCCEEDED(rvIgnored),
+                    "Element::SetAttr(nsGkAtoms::style, "
+                    "\"pre-wrap\", false) failed, but ignored");
+              }
+              return NS_OK;
+            });
+    if (MOZ_UNLIKELY(spanElementOrError.isErr())) {
+      NS_WARNING(
+          "HTMLEditor::DeleteSelectionAndCreateElement(nsGkAtoms::span) "
+          "failed");
+      return NS_OK;
+    }
+    // If this succeeded, then set selection inside the pre
+    // so the inserted text will end up there.
+    // If it failed, we don't care what the return value was,
+    // but we'll fall through and try to insert the text anyway.
     MOZ_ASSERT(spanElementOrError.inspect());
-    rv = CollapseSelectionToStartOf(
+    nsresult rv = CollapseSelectionToStartOf(
         MOZ_KnownLive(*spanElementOrError.inspect()));
     if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
       NS_WARNING(
@@ -3246,52 +3321,51 @@ nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText,
     NS_WARNING_ASSERTION(
         NS_SUCCEEDED(rv),
         "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+    containerSpanElement = spanElementOrError.unwrap();
   }
 
   // TODO: We should insert text at specific point rather than at selection.
   //       Then, we can do this before inserting the  element.
-  if (aAddCites) {
-    rv = InsertWithQuotationsAsSubAction(aQuotedText);
+  if (aAddCites == AddCites::Yes) {
+    nsresult rv = InsertWithQuotationsAsSubAction(aQuotedText);
     if (NS_FAILED(rv)) {
       NS_WARNING("HTMLEditor::InsertWithQuotationsAsSubAction() failed");
       return rv;
     }
   } else {
-    rv = InsertTextAsSubAction(aQuotedText, SelectionHandling::Delete);
+    nsresult rv = InsertTextAsSubAction(aQuotedText, SelectionHandling::Delete);
     if (NS_FAILED(rv)) {
       NS_WARNING("EditorBase::InsertTextAsSubAction() failed");
       return rv;
     }
   }
 
-  // XXX Why don't we check this before inserting the quoted text?
-  if (spanElementOrError.isErr()) {
-    return NS_OK;
-  }
-
-  // Set the selection to just after the inserted node:
-  EditorRawDOMPoint afterNewSpanElement(
-      EditorRawDOMPoint::After(*spanElementOrError.inspect()));
-  NS_WARNING_ASSERTION(
-      afterNewSpanElement.IsSet(),
-      "Failed to set after the new  element, but ignored");
-  if (afterNewSpanElement.IsSet()) {
-    nsresult rv = CollapseSelectionTo(afterNewSpanElement);
-    if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
-      NS_WARNING(
-          "EditorBase::CollapseSelectionTo() caused destroying the editor");
-      return NS_ERROR_EDITOR_DESTROYED;
-    }
+  // Set the selection to after the  if and only if we wrap the text into
+  // it.
+  if (containerSpanElement) {
+    EditorRawDOMPoint afterNewSpanElement(
+        EditorRawDOMPoint::After(*containerSpanElement));
     NS_WARNING_ASSERTION(
-        NS_SUCCEEDED(rv),
-        "EditorBase::CollapseSelectionTo() failed, but ignored");
-  }
+        afterNewSpanElement.IsSet(),
+        "Failed to set after the new  element, but ignored");
+    if (afterNewSpanElement.IsSet()) {
+      nsresult rv = CollapseSelectionTo(afterNewSpanElement);
+      if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+        NS_WARNING(
+            "EditorBase::CollapseSelectionTo() caused destroying the editor");
+        return NS_ERROR_EDITOR_DESTROYED;
+      }
+      NS_WARNING_ASSERTION(
+          NS_SUCCEEDED(rv),
+          "EditorBase::CollapseSelectionTo() failed, but ignored");
+    }
 
-  // Note that if !aAddCites, aNodeInserted isn't set.
-  // That's okay because the routines that use aAddCites
-  // don't need to know the inserted node.
-  if (aNodeInserted) {
-    spanElementOrError.unwrap().forget(aNodeInserted);
+    // Note that if !aAddCites, aNodeInserted isn't set.
+    // That's okay because the routines that use aAddCites
+    // don't need to know the inserted node.
+    if (aNodeInserted) {
+      containerSpanElement.forget(aNodeInserted);
+    }
   }
 
   return NS_OK;
@@ -3306,6 +3380,12 @@ NS_IMETHODIMP HTMLEditor::Rewrap(bool aRespectNewlines) {
     return EditorBase::ToGenericNSResult(rv);
   }
 
+  const RefPtr editingHost =
+      ComputeEditingHost(LimitInBodyElement::No);
+  if (NS_WARN_IF(!editingHost)) {
+    return NS_ERROR_FAILURE;
+  }
+
   // Rewrap makes no sense if there's no wrap column; default to 72.
   int32_t wrapWidth = WrapWidth();
   if (wrapWidth <= 0) {
@@ -3351,7 +3431,7 @@ NS_IMETHODIMP HTMLEditor::Rewrap(bool aRespectNewlines) {
   AutoTransactionBatch bundleAllTransactions(*this, __FUNCTION__);
   AutoPlaceholderBatch treatAsOneTransaction(
       *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
-  rv = InsertTextWithQuotationsInternal(wrapped);
+  rv = InsertTextWithQuotationsInternal(wrapped, *editingHost);
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                        "HTMLEditor::InsertTextWithQuotationsInternal() failed");
   return EditorBase::ToGenericNSResult(rv);
@@ -3361,8 +3441,15 @@ NS_IMETHODIMP HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
                                                  const nsAString& aCitation,
                                                  bool aInsertHTML,
                                                  nsINode** aNodeInserted) {
+  const RefPtr editingHost =
+      ComputeEditingHost(LimitInBodyElement::No);
+  if (NS_WARN_IF(!editingHost)) {
+    return NS_ERROR_FAILURE;
+  }
+
   // Don't let anyone insert HTML when we're in plaintext mode.
-  if (IsPlaintextMailComposer()) {
+  if (IsPlaintextMailComposer() ||
+      editingHost->IsContentEditablePlainTextOnly()) {
     NS_ASSERTION(
         !aInsertHTML,
         "InsertAsCitedQuotation: trying to insert html into plaintext editor");
@@ -3380,7 +3467,8 @@ NS_IMETHODIMP HTMLEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
 
     AutoPlaceholderBatch treatAsOneTransaction(
         *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
-    rv = InsertAsPlaintextQuotation(aQuotedText, true, aNodeInserted);
+    rv = InsertAsPlaintextQuotation(aQuotedText, AddCites::Yes, *editingHost,
+                                    aNodeInserted);
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                          "HTMLEditor::InsertAsPlaintextQuotation() failed");
     return EditorBase::ToGenericNSResult(rv);
diff --git a/editor/libeditor/tests/test_dragdrop.html b/editor/libeditor/tests/test_dragdrop.html
index 253775fd5d68e..11e29d3b091be 100644
--- a/editor/libeditor/tests/test_dragdrop.html
+++ b/editor/libeditor/tests/test_dragdrop.html
@@ -70,6 +70,12 @@
 
 // eslint-disable-next-line complexity
 async function doTest() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["dom.element.contenteditable.plaintext-only.enabled", true],
+    ],
+  });
+
   const container = document.getElementById("container");
   const dropZone = document.getElementById("dropZone");
 
@@ -3462,6 +3468,68 @@
     document.removeEventListener("dragend", onDragEnd, {capture: true});
   })();
 
+  // -------- Test dragging contenteditable to contenteditable=plaintext-only
+  await (async function test_dragging_from_contenteditable_to_contenteditable_plaintext_only() {
+    const description = "dragging text in contenteditable to contenteditable=plaintext-only";
+    container.innerHTML = '
bold

'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "ol", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, "bd", + `${description}: dragged range should be removed from contenteditable`); + is(otherContenteditable.innerHTML, "ol", + `${description}: dragged content should be inserted into other contenteditable without formatting`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "ol"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "ol"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // We need to clean up contenteditable=plaintext-only before the pref enabling it is cleared. + container.innerHTML = ""; + document.removeEventListener("beforeinput", onBeforeinput); document.removeEventListener("input", onInput); SimpleTest.finish(); diff --git a/testing/web-platform/mozilla/meta/editor/plaintext-only/__dir__.ini b/testing/web-platform/mozilla/meta/editor/plaintext-only/__dir__.ini new file mode 100644 index 0000000000000..16cf684c7e2ea --- /dev/null +++ b/testing/web-platform/mozilla/meta/editor/plaintext-only/__dir__.ini @@ -0,0 +1 @@ +prefs: [dom.element.contenteditable.plaintext-only.enabled:true] diff --git a/testing/web-platform/mozilla/meta/editor/plaintext-only/special-paste.html.ini b/testing/web-platform/mozilla/meta/editor/plaintext-only/special-paste.html.ini new file mode 100644 index 0000000000000..c8931bda040ab --- /dev/null +++ b/testing/web-platform/mozilla/meta/editor/plaintext-only/special-paste.html.ini @@ -0,0 +1,22 @@ +[special-paste.html?white-space=pre-line] + prefs: [middlemouse.paste:true,general.autoScroll:false,middlemouse.contentLoadURL:false] + [Pasting without format: beforeinput] + expected: FAIL + + +[special-paste.html?white-space=pre] + prefs: [middlemouse.paste:true,general.autoScroll:false,middlemouse.contentLoadURL:false] + [Pasting without format: beforeinput] + expected: FAIL + + +[special-paste.html?white-space=normal] + prefs: [middlemouse.paste:true,general.autoScroll:false,middlemouse.contentLoadURL:false] + [Pasting without format: beforeinput] + expected: FAIL + + +[special-paste.html?white-space=pre-wrap] + prefs: [middlemouse.paste:true,general.autoScroll:false,middlemouse.contentLoadURL:false] + [Pasting without format: beforeinput] + expected: FAIL diff --git a/testing/web-platform/mozilla/tests/editor/plaintext-only/special-paste.html b/testing/web-platform/mozilla/tests/editor/plaintext-only/special-paste.html new file mode 100644 index 0000000000000..f4c7a0ef4fea6 --- /dev/null +++ b/testing/web-platform/mozilla/tests/editor/plaintext-only/special-paste.html @@ -0,0 +1,129 @@ + + + + + + + + + +Pasting rich text into contenteditable=plaintext-only + + + + + + + + + + diff --git a/testing/web-platform/tests/editing/include/editor-test-utils.js b/testing/web-platform/tests/editing/include/editor-test-utils.js index b180f3343fde2..b302d19a11750 100644 --- a/testing/web-platform/tests/editing/include/editor-test-utils.js +++ b/testing/web-platform/tests/editing/include/editor-test-utils.js @@ -100,6 +100,24 @@ class EditorTestUtils { ); } + sendCopyShortcutKey() { + return this.sendKey( + "c", + this.window.navigator.platform.includes("Mac") + ? this.kMeta + : this.kControl + ); + } + + sendPasteShortcutKey() { + return this.sendKey( + "v", + this.window.navigator.platform.includes("Mac") + ? this.kMeta + : this.kControl + ); + } + // Similar to `setupDiv` in editing/include/tests.js, this method sets // innerHTML value of this.editingHost, and sets multiple selection ranges // specified with the markers. diff --git a/testing/web-platform/tests/editing/plaintext-only/paste.https.html b/testing/web-platform/tests/editing/plaintext-only/paste.https.html new file mode 100644 index 0000000000000..611c39f8bf3c8 --- /dev/null +++ b/testing/web-platform/tests/editing/plaintext-only/paste.https.html @@ -0,0 +1,264 @@ + + + + + + + + + +Pasting rich text into contenteditable=plaintext-only + + + + + + + + + +