diff --git a/calendar/base/content/item-editing/calendar-item-iframe.js b/calendar/base/content/item-editing/calendar-item-iframe.js index 82f9df50f2..bdabd21356 100644 --- a/calendar/base/content/item-editing/calendar-item-iframe.js +++ b/calendar/base/content/item-editing/calendar-item-iframe.js @@ -1560,8 +1560,9 @@ function saveDialog(item) { // The editor gives us output wrapped in a body tag. We don't really want // that, so strip it. (Yes, it's a regex with HTML, but a _very_ specific - // one.) - item.descriptionHTML = editorOutput.replace(/^(.+)<\/body>$/, "$1"); + // one.) We use the `s` flag to match across newlines in case there's a + //
 tag, in which case 
will not be inserted. + item.descriptionHTML = editorOutput.replace(/^(.+)<\/body>$/s, "$1"); } // Event Status diff --git a/calendar/base/src/calItemBase.js b/calendar/base/src/calItemBase.js index f87b61c3ce..20c245df8b 100644 --- a/calendar/base/src/calItemBase.js +++ b/calendar/base/src/calItemBase.js @@ -421,13 +421,13 @@ calItemBase.prototype = { set descriptionHTML(html) { if (html) { // We need to output a plaintext version of the description, even if we're - // using the ALTREP parameter. + // using the ALTREP parameter. We use the "preformatted" option in case + // the HTML contains a
 tag with newlines.
       let mode =
         Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
-        Ci.nsIDocumentEncoder.OutputWrap |
         Ci.nsIDocumentEncoder.OutputLFLineBreak |
-        Ci.nsIDocumentEncoder.OutputBodyOnly;
-      let text = gParserUtils.convertToPlainText(html, mode, 80);
+        Ci.nsIDocumentEncoder.OutputPreformatted;
+      let text = gParserUtils.convertToPlainText(html, mode, 0);
 
       this.setProperty("DESCRIPTION", text);
 
diff --git a/calendar/test/browser/eventDialog/browser.ini b/calendar/test/browser/eventDialog/browser.ini
index 3d1d8d63fd..85f569c0cc 100644
--- a/calendar/test/browser/eventDialog/browser.ini
+++ b/calendar/test/browser/eventDialog/browser.ini
@@ -21,6 +21,7 @@ support-files = data/**
 [browser_attendeesDialogRemove.js]
 [browser_attendeesDialogUpdate.js]
 [browser_eventDialog.js]
+[browser_eventDialogDescriptionEditor.js]
 [browser_eventDialogEditButton.js]
 [browser_eventDialogModificationPrompt.js]
 [browser_utf8.js]
diff --git a/calendar/test/browser/eventDialog/browser_eventDialogDescriptionEditor.js b/calendar/test/browser/eventDialog/browser_eventDialogDescriptionEditor.js
new file mode 100644
index 0000000000..6cf0f7cf37
--- /dev/null
+++ b/calendar/test/browser/eventDialog/browser_eventDialogDescriptionEditor.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm");
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+add_setup(async function() {
+  await CalendarTestUtils.setCalendarView(window, "day");
+  CalendarTestUtils.goToDate(window, 2023, 2, 18);
+});
+
+add_task(async function testPastePreformattedWithLinebreak() {
+  const calendar = CalendarTestUtils.createCalendar();
+
+  // Create an event which currently has no description.
+  const event = await calendar.addItem(
+    new CalEvent(CalendarTestUtils.dedent`
+      BEGIN:VEVENT
+      SUMMARY:An event
+      DTSTART:20230218T100000Z
+      DTEND:20230218T110000Z
+      END:VEVENT
+    `)
+  );
+
+  // Remember event details so we can refetch it after editing.
+  const eventId = event.id;
+  const eventModified = event.lastModifiedTime;
+
+  // Sanity check.
+  Assert.equal(event.descriptionHTML, null, "event should not have an HTML description");
+  Assert.equal(event.descriptionText, null, "event should not have a text description");
+
+  // Open our event for editing.
+  const { dialogWindow: eventWindow, iframeDocument } = await CalendarTestUtils.dayView.editEventAt(
+    window,
+    1
+  );
+
+  const editor = iframeDocument.getElementById("item-description");
+  editor.focus();
+
+  const expectedHTML =
+    "
This event is one which includes\nan explicit linebreak inside a pre tag.
"; + + // Create a paste which includes HTML data, which the editor will recognize as + // HTML and paste with formatting by default. + const stringData = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + stringData.data = expectedHTML; + + const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + transferable.init(null); + transferable.addDataFlavor("text/html"); + transferable.setTransferData("text/html", stringData); + Services.clipboard.setData(transferable, null, Ci.nsIClipboard.kGlobalClipboard); + + // Paste. + EventUtils.synthesizeKey("v", { accelKey: true }, eventWindow); + + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the description has been set appropriately. There should be no + // change to the HTML, which is preformatted, and the text description should + // include a linebreak in the same place as the HTML. + Assert.equal(editedEvent.descriptionHTML, expectedHTML, "HTML description should match input"); + Assert.equal( + editedEvent.descriptionText, + "This event is one which includes\nan explicit linebreak inside a pre tag.", + "text description should include linebreak" + ); + + CalendarTestUtils.removeCalendar(calendar); +}); + +add_task(async function testTypeLongTextWithLinebreaks() { + const calendar = CalendarTestUtils.createCalendar(); + + // Create an event which currently has no description. + const event = await calendar.addItem( + new CalEvent(CalendarTestUtils.dedent` + BEGIN:VEVENT + SUMMARY:An event + DTSTART:20230218T100000Z + DTEND:20230218T110000Z + END:VEVENT + `) + ); + + // Remember event details so we can refetch it after editing. + const eventId = event.id; + const eventModified = event.lastModifiedTime; + + // Sanity check. + Assert.equal(event.descriptionHTML, null, "event should not have an HTML description"); + Assert.equal(event.descriptionText, null, "event should not have a text description"); + + // Open our event for editing. + const { + dialogWindow: eventWindow, + iframeDocument, + iframeWindow, + } = await CalendarTestUtils.dayView.editEventAt(window, 1); + + const editor = iframeDocument.getElementById("item-description"); + editor.focus(); + + // Insert text with several long lines and explicit linebreaks. + const firstLine = + "This event is pretty much just plain text, albeit it has some pretty long lines so that we can ensure that we don't accidentally wrap it during conversion."; + EventUtils.sendString(firstLine, iframeWindow); + EventUtils.sendKey("RETURN", iframeWindow); + + const secondLine = "This line follows immediately after a linebreak."; + EventUtils.sendString(secondLine, iframeWindow); + EventUtils.sendKey("RETURN", iframeWindow); + EventUtils.sendKey("RETURN", iframeWindow); + + const thirdLine = + "And one after a couple more linebreaks, for good measure. It might as well be a fairly long string as well, just so we're certain."; + EventUtils.sendString(thirdLine, iframeWindow); + + await CalendarTestUtils.items.saveAndCloseItemDialog(eventWindow); + + await TestUtils.waitForCondition(async () => { + const item = await calendar.getItem(eventId); + return item.lastModifiedTime != eventModified; + }); + + const editedEvent = await calendar.getItem(eventId); + + // Verify that the description has been set appropriately. The HTML should + // match the input and use
as a linebreak, while the text should not be + // wrapped and should use \n as a linebreak. + Assert.equal( + editedEvent.descriptionHTML, + `${firstLine}
${secondLine}

${thirdLine}`, + "HTML description should match input with
for linebreaks" + ); + Assert.equal( + editedEvent.descriptionText, + `${firstLine}\n${secondLine}\n\n${thirdLine}`, + "text description should match input with linebreaks" + ); + + CalendarTestUtils.removeCalendar(calendar); +});