From 36901d5978062185e12722900baae13d3e51e735 Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Wed, 2 Oct 2024 21:42:09 +0000 Subject: [PATCH] Bug 1920646 - part 1: Make `HTMLEditor` paste/drop things as plaintext when `contenteditable=plaintext-only` r=m_kato Chrome sets `beforeinput.data` instead of `beforeinput.dataTransfer`, but Input Events Level 2 spec defines that browsers should set `dataTransfer` when **contenteditable** [1]. Therefore, the new WPT expects `dataTransfer`. However, it's unclear that the `dataTransfer` should have `text/html` or only `text/plain`. From web apps point of view, `text/html` data may make them serialize the rich text format to plaintext without any dependencies of browsers and OS. On the other hand, they cannot distinguish whether the user tries to paste with or without formatting when `contenteditable=true`. Therefore, I filed a spec issue for this. We need to be back later about this issue. 1. https://w3c.github.io/input-events/#overview 2. https://github.com/w3c/input-events/issues/162 Differential Revision: https://phabricator.services.mozilla.com/D223908 --- editor/libeditor/HTMLEditor.h | 24 +- editor/libeditor/HTMLEditorDataTransfer.cpp | 324 +++++++++++------- editor/libeditor/tests/test_dragdrop.html | 68 ++++ .../meta/editor/plaintext-only/__dir__.ini | 1 + .../plaintext-only/special-paste.html.ini | 22 ++ .../editor/plaintext-only/special-paste.html | 129 +++++++ .../editing/include/editor-test-utils.js | 18 + .../editing/plaintext-only/paste.https.html | 264 ++++++++++++++ 8 files changed, 723 insertions(+), 127 deletions(-) create mode 100644 testing/web-platform/mozilla/meta/editor/plaintext-only/__dir__.ini create mode 100644 testing/web-platform/mozilla/meta/editor/plaintext-only/special-paste.html.ini create mode 100644 testing/web-platform/mozilla/tests/editor/plaintext-only/special-paste.html create mode 100644 testing/web-platform/tests/editing/plaintext-only/paste.https.html 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 + + + + + + + + + +