From 4d939dbefdca5037575b563ded9976fc656ebc36 Mon Sep 17 00:00:00 2001 From: Marco Castelluccio Date: Wed, 9 Oct 2024 16:37:28 +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 UltraBlame original commit: 2e3f866560e2c750fe1e4469b81d89f10bffc6a1 --- editor/libeditor/HTMLEditor.h | 24 +- editor/libeditor/HTMLEditorDataTransfer.cpp | 320 +++++++++++------- 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, 721 insertions(+), 125 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 890adf6c09c9..8dbcd657b820 100644 --- a/editor/libeditor/HTMLEditor.h +++ b/editor/libeditor/HTMLEditor.h @@ -3143,8 +3143,9 @@ class HTMLEditor final : public EditorBase, - 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, - MOZ_CAN_RUN_SCRIPT nsresult - InsertTextWithQuotationsInternal(const nsAString& aStringToInsert); + MOZ_CAN_RUN_SCRIPT nsresult InsertTextWithQuotationsInternal( + const nsAString& aStringToInsert, const Element& aEditingHost); @@ -3722,9 +3723,10 @@ class HTMLEditor final : public EditorBase, [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetSelectionAtDocumentStart(); - 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 }; @@ -3734,8 +3736,10 @@ class HTMLEditor final : public EditorBase, + [[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); @@ -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 b79ffdfd6748..33332ab24cd5 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( - 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; + } + nsresult rv = NS_OK; nsCOMPtr clipboard = @@ -2314,7 +2347,7 @@ nsresult HTMLEditor::PasteInternal(nsIClipboard::ClipboardType aClipboardType) { nsCOMPtr transferable; - rv = PrepareHTMLTransferable(getter_AddRefs(transferable)); + rv = PrepareHTMLTransferable(getter_AddRefs(transferable), &aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::PrepareHTMLTransferable() failed"); return rv; @@ -2336,11 +2369,6 @@ nsresult HTMLEditor::PasteInternal(nsIClipboard::ClipboardType aClipboardType) { } - if (!IsModifiable()) { - return NS_OK; - } - - 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) { @@ -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; + } nsCOMPtr clipboard( @@ -2582,10 +2619,6 @@ nsresult HTMLEditor::PasteNoFormattingAsAction( return NS_OK; } - if (!IsModifiable()) { - return NS_OK; - } - 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 { } - 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) { return true; @@ -2664,7 +2710,8 @@ bool HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable) { 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) { 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; + } + 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()); @@ -3052,10 +3112,8 @@ nsresult HTMLEditor::InsertTextWithQuotationsInternal( 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, 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, } } - - - - - - - Result, nsresult> spanElementOrError = - DeleteSelectionAndCreateElement( - *nsGkAtoms::span, [](HTMLEditor&, Element& aSpanElement, - const EditorDOMPoint& aPointToInsert) { - - 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()); - - if (aPointToInsert.IsContainerHTMLElement(nsGkAtoms::body)) { + RefPtr containerSpanElement; + if (!aEditingHost.IsContentEditablePlainTextOnly()) { + + + + + + + + + Result, nsresult> spanElementOrError = + DeleteSelectionAndCreateElement( + *nsGkAtoms::span, [](HTMLEditor&, Element& aSpanElement, + const EditorDOMPoint& aPointToInsert) { + + 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 (spanElementOrError.isOk()) { + nsPrintfCString( + "Element::SetAttr(nsGkAtoms::mozquote, \"true\", %s) " + "failed", + aSpanElement.IsInComposedDoc() ? "true" : "false") + .get()); + + + 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; + } + + + + MOZ_ASSERT(spanElementOrError.inspect()); - rv = CollapseSelectionToStartOf( + nsresult rv = CollapseSelectionToStartOf( MOZ_KnownLive(*spanElementOrError.inspect())); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( @@ -3246,18 +3321,19 @@ nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText, NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed, but ignored"); + containerSpanElement = spanElementOrError.unwrap(); } - 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; @@ -3265,33 +3341,31 @@ nsresult HTMLEditor::InsertAsPlaintextQuotation(const nsAString& aQuotedText, } - if (spanElementOrError.isErr()) { - return NS_OK; - } - - 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; - } + 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"); + } - - - - if (aNodeInserted) { - spanElementOrError.unwrap().forget(aNodeInserted); + + + + 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; + } + 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; + } + - 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 253775fd5d68..11e29d3b091b 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 000000000000..16cf684c7e2e --- /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 000000000000..c8931bda040a --- /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 000000000000..f4c7a0ef4fea --- /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 94aa1e6e0529..798d315ec6d7 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 + ); + } + 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 000000000000..611c39f8bf3c --- /dev/null +++ b/testing/web-platform/tests/editing/plaintext-only/paste.https.html @@ -0,0 +1,264 @@ + + + + + + + + + +Pasting rich text into contenteditable=plaintext-only + + + + + + + + + +