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
+
+
+
+
+
+
+
+
+
+