Bug 1637133 - Refactor calendar summary dialog to use a custom element. r=darktrojan
authorPaul Morris <paul@thunderbird.net>
Wed, 20 May 2020 14:11:19 +0000
changeset 39202 c0c2cc7c1b7dcb7549c07a8dbe67e5ab0de2e259
parent 39201 cddacd682d3be5e327eed527cf36695ad7b1c375
child 39203 82280bdfe344e5616c30aea33935cf4f2bcc32e3
push id402
push userclokep@gmail.com
push dateMon, 29 Jun 2020 20:48:04 +0000
reviewersdarktrojan
bugs1637133
Bug 1637133 - Refactor calendar summary dialog to use a custom element. r=darktrojan Move the part of the dialog that displays a summary of a calendar item into a custom element so it can be reused more than once in a given scope/dialog/ window. This will be soon be used to show summaries of multiple events in the ICS file import dialog. Update the code to support multiple event summaries in a given scope. Differential Revision: https://phabricator.services.mozilla.com/D74797
calendar/base/content/dialogs/calendar-dialog-utils.js
calendar/base/content/dialogs/calendar-summary-dialog.css
calendar/base/content/dialogs/calendar-summary-dialog.js
calendar/base/content/dialogs/calendar-summary-dialog.xhtml
calendar/base/content/widgets/calendar-item-summary.js
calendar/base/jar.mn
calendar/base/themes/common/calendar-attendees.css
calendar/base/themes/common/dialogs/calendar-event-dialog.css
calendar/lightning/content/lightning-item-iframe.js
calendar/lightning/content/lightning-item-iframe.xhtml
calendar/test/browser/preferences/browser_alarmDefaultValue.js
calendar/test/modules/ItemEditingHelpers.jsm
--- a/calendar/base/content/dialogs/calendar-dialog-utils.js
+++ b/calendar/base/content/dialogs/calendar-dialog-utils.js
@@ -1,16 +1,17 @@
 /* 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/. */
 
 /* exported gInTab, gMainWindow, gTabmail, intializeTabOrWindowVariables,
  *          dispose, setDialogId, loadReminders, saveReminder,
  *          commonUpdateReminder, updateLink, rearrangeAttendees,
- *          adaptScheduleAgent
+ *          adaptScheduleAgent, sendMailToOrganizer,
+ *          openAttachmentFromItemSummary, getRecurrenceString,
  */
 
 /* import-globals-from ../../../lightning/content/lightning-item-iframe.js */
 /* import-globals-from ../calendar-ui-utils.js */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
@@ -95,65 +96,78 @@ function applyPersistedProperties(aDialo
     aDialog.ownerGlobal.resizeTo(width, height);
   }
 }
 
 /**
  * Create a calIAlarm from the given menuitem. The menuitem must have the
  * following attributes: unit, length, origin, relation.
  *
- * @param menuitem      The menuitem to create the alarm from.
- * @return              The calIAlarm with information from the menuitem.
+ * @param {Element} aMenuitem          The menuitem to create the alarm from.
+ * @param {calICalendar} aCalendar     The calendar for getting the default alarm type.
+ * @return                             The calIAlarm with information from the menuitem.
  */
-function createReminderFromMenuitem(aMenuitem) {
+function createReminderFromMenuitem(aMenuitem, aCalendar) {
   let reminder = aMenuitem.reminder || cal.createAlarm();
   // clone immutable reminders if necessary to set default values
   let isImmutable = !reminder.isMutable;
   if (isImmutable) {
     reminder = reminder.clone();
   }
   let offset = cal.createDuration();
   offset[aMenuitem.getAttribute("unit")] = aMenuitem.getAttribute("length");
   offset.normalize();
   offset.isNegative = aMenuitem.getAttribute("origin") == "before";
   reminder.related =
     aMenuitem.getAttribute("relation") == "START"
       ? reminder.ALARM_RELATED_START
       : reminder.ALARM_RELATED_END;
   reminder.offset = offset;
-  reminder.action = getDefaultAlarmType();
+  reminder.action = getDefaultAlarmType(aCalendar);
   // make reminder immutable in case it was before
   if (isImmutable) {
     reminder.makeImmutable();
   }
   return reminder;
 }
 
 /**
  * This function opens the needed dialogs to edit the reminder. Note however
  * that calling this function from an extension is not recommended. To allow an
  * extension to open the reminder dialog, set the menulist "item-alarm" to the
  * custom menuitem and call updateReminder().
+ *
+ * @param {Element} reminderList - The reminder menu element.
+ * @param {calIEvent | calIToDo} calendarItem - The calendar item.
+ * @param {number} lastAlarmSelection - Index of previously selected item in the menu.
+ * @param {calICalendar} calendar - The calendar to use.
+ * @param {calITimezone} [timezone] - Timezone to use.
  */
-function editReminder() {
-  let customItem = document.getElementById("reminder-custom-menuitem");
-  let args = {};
-  args.reminders = customItem.reminders;
-  args.item = window.calendarItem;
-  args.timezone = window.gStartTimezone || window.gEndTimezone || cal.dtz.defaultTimezone;
+function editReminder(
+  reminderList,
+  calendarItem,
+  lastAlarmSelection,
+  calendar,
+  timezone = cal.dtz.defaultTimezone
+) {
+  let customItem = reminderList.querySelector(".reminder-custom-menuitem");
 
-  args.calendar = getCurrentCalendar();
-
-  // While these are "just" callbacks, the dialog is opened modally, so aside
-  // from what's needed to set up the reminders, nothing else needs to be done.
-  args.onOk = function(reminders) {
-    customItem.reminders = reminders;
-  };
-  args.onCancel = function() {
-    document.getElementById("item-alarm").selectedIndex = gLastAlarmSelection;
+  let args = {
+    reminders: customItem.reminders,
+    item: calendarItem,
+    timezone,
+    calendar,
+    // While these are "just" callbacks, the dialog is opened modally, so aside
+    // from what's needed to set up the reminders, nothing else needs to be done.
+    onOk(reminders) {
+      customItem.reminders = reminders;
+    },
+    onCancel() {
+      reminderList.selectedIndex = lastAlarmSelection;
+    },
   };
 
   window.setCursor("wait");
 
   // open the dialog modally
   openDialog(
     "chrome://calendar/content/calendar-event-dialog-reminder.xhtml",
     "_blank",
@@ -161,25 +175,29 @@ function editReminder() {
     args
   );
 }
 
 /**
  * Update the reminder details from the selected alarm. This shows a string
  * describing the reminder set, or nothing in case a preselected reminder was
  * chosen.
+ *
+ * @param {Element} reminderDetails - The reminder details element.
+ * @param {Element} reminderList - The reminder menu element.
+ * @param {calICalendar} calendar - The calendar.
  */
-function updateReminderDetails() {
+function updateReminderDetails(reminderDetails, reminderList, calendar) {
   // find relevant elements in the document
-  let reminderList = document.getElementById("item-alarm");
-  let reminderMultipleLabel = document.getElementById("reminder-multiple-alarms-label");
-  let iconBox = document.getElementById("reminder-icon-box");
-  let reminderSingleLabel = document.getElementById("reminder-single-alarms-label");
-  let reminders = document.getElementById("reminder-custom-menuitem").reminders || [];
-  let calendar = getCurrentCalendar();
+  let reminderMultipleLabel = reminderDetails.querySelector(".reminder-multiple-alarms-label");
+  let iconBox = reminderDetails.querySelector(".reminder-icon-box");
+  let reminderSingleLabel = reminderDetails.querySelector(".reminder-single-alarms-label");
+
+  let reminders = reminderList.querySelector(".reminder-custom-menuitem").reminders || [];
+
   let actionValues = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
   let actionMap = {};
   for (let action of actionValues) {
     actionMap[action] = true;
   }
 
   // Filter out any unsupported action types.
   reminders = reminders.filter(x => x.action in actionMap);
@@ -202,44 +220,44 @@ function updateReminderDetails() {
     if (reminderList.value == "none") {
       // No reminder selected means show no icons.
       removeChildren(iconBox);
     } else {
       // This is one of the predefined dropdown items. We should show a
       // single icon in the icons box to tell the user what kind of alarm
       // this will be.
       let mockAlarm = cal.createAlarm();
-      mockAlarm.action = getDefaultAlarmType();
+      mockAlarm.action = getDefaultAlarmType(calendar);
       cal.alarms.addReminderImages(iconBox, [mockAlarm]);
     }
   }
 }
 
-var gLastAlarmSelection = 0;
-
-function matchCustomReminderToMenuitem(reminder) {
-  let defaultAlarmType = getDefaultAlarmType();
-  let reminderList = document.getElementById("item-alarm");
+/**
+ * Check whether a reminder matches one of the default menu items or not.
+ *
+ * @param {calIAlarm} reminder - The reminder to match to a menu item.
+ * @param {Element} reminderList - The reminder menu element.
+ * @param {calICalendar} calendar - The current calendar, to get the default alarm type.
+ * @return {boolean} True if the reminder matches a menu item, false if not.
+ */
+function matchCustomReminderToMenuitem(reminder, reminderList, calendar) {
+  let defaultAlarmType = getDefaultAlarmType(calendar);
   let reminderPopup = reminderList.menupopup;
   if (
     reminder.related != Ci.calIAlarm.ALARM_RELATED_ABSOLUTE &&
     reminder.offset &&
     reminder.action == defaultAlarmType
   ) {
     // Exactly one reminder that's not absolute, we may be able to match up
     // popup items.
     let relation = reminder.related == reminder.ALARM_RELATED_START ? "START" : "END";
-    let origin;
 
     // If the time duration for offset is 0, means the reminder is '0 minutes before'
-    if (reminder.offset.inSeconds == 0 || reminder.offset.isNegative) {
-      origin = "before";
-    } else {
-      origin = "after";
-    }
+    let origin = reminder.offset.inSeconds == 0 || reminder.offset.isNegative ? "before" : "after";
 
     let unitMap = {
       days: 86400,
       hours: 3600,
       minutes: 60,
     };
 
     for (let menuitem of reminderPopup.children) {
@@ -259,77 +277,82 @@ function matchCustomReminderToMenuitem(r
           return true;
         }
       }
     }
   }
 
   return false;
 }
+
 /**
- * Load an item's reminders into the dialog
+ * Load an item's reminders into the dialog.
  *
- * @param reminders     An array of calIAlarms to load.
+ * @param {calIAlarm[]} reminders     An array of alarms to load.
+ * @param {Element} reminderList      The reminders menulist element.
+ * @param {calICalendar} calendar     The calendar the item belongs to.
+ * @return {number}                   Index of the selected item in reminders menu.
  */
-function loadReminders(reminders) {
-  // select 'no reminder' by default
-  let reminderList = document.getElementById("item-alarm");
-  let customItem = document.getElementById("reminder-custom-menuitem");
+function loadReminders(reminders, reminderList, calendar) {
+  // Select 'no reminder' by default.
   reminderList.selectedIndex = 0;
-  gLastAlarmSelection = 0;
 
   if (!reminders || !reminders.length) {
     // No reminders selected, we are done
-    return;
+    return reminderList.selectedIndex;
   }
 
-  if (reminders.length > 1 || !matchCustomReminderToMenuitem(reminders[0])) {
+  if (
+    reminders.length > 1 ||
+    !matchCustomReminderToMenuitem(reminders[0], reminderList, calendar)
+  ) {
     // If more than one alarm is selected, or we didn't find a matching item
     // above, then select the "custom" item and attach the item's reminders to
     // it.
     reminderList.value = "custom";
-    customItem.reminders = reminders;
+    reminderList.querySelector(".reminder-custom-menuitem").reminders = reminders;
   }
 
-  // remember the selected index
-  gLastAlarmSelection = reminderList.selectedIndex;
+  // Return the selected index so it can be remembered.
+  return reminderList.selectedIndex;
 }
 
 /**
  * Save the selected reminder into the passed item.
  *
- * @param item      The item save the reminder into.
+ * @param {calIEvent | calITodo} item   The calendar item to save the reminder into.
+ * @param {calICalendar} calendar       The current calendar.
+ * @param {Element} reminderList        The reminder menu element.
  */
-function saveReminder(item) {
+function saveReminder(item, calendar, reminderList) {
   // We want to compare the old alarms with the new ones. If these are not
   // the same, then clear the snooze/dismiss times
   let oldAlarmMap = {};
   for (let alarm of item.getAlarms()) {
     oldAlarmMap[alarm.icalString] = true;
   }
 
   // Clear the alarms so we can add our new ones.
   item.clearAlarms();
 
-  let reminderList = document.getElementById("item-alarm");
   if (reminderList.value != "none") {
     let menuitem = reminderList.selectedItem;
     let reminders;
 
     if (menuitem.reminders) {
       // Custom reminder entries carry their own reminder object with
       // them. Make sure to clone in case these are the original item's
       // reminders.
 
       // XXX do we need to clone here?
       reminders = menuitem.reminders.map(x => x.clone());
     } else {
       // Pre-defined entries specify the necessary information
       // as attributes attached to the menuitem elements.
-      reminders = [createReminderFromMenuitem(menuitem)];
+      reminders = [createReminderFromMenuitem(menuitem, calendar)];
     }
 
     let alarmCaps = item.calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
     let alarmActions = {};
     for (let action of alarmCaps) {
       alarmActions[action] = true;
     }
 
@@ -369,103 +392,100 @@ function saveReminder(item) {
   }
 }
 
 /**
  * Get the default alarm type for the currently selected calendar. If the
  * calendar supports DISPLAY alarms, this is the default. Otherwise it is the
  * first alarm action the calendar supports.
  *
- * @return      The default alarm type.
+ * @param {calICalendar} calendar - The calendar to use.
+ * @return {string} The default alarm type.
  */
-function getDefaultAlarmType() {
-  let calendar = getCurrentCalendar();
+function getDefaultAlarmType(calendar) {
   let alarmCaps = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
   return alarmCaps.includes("DISPLAY") ? "DISPLAY" : alarmCaps[0];
 }
 
 /**
- * Get the currently selected calendar. For dialogs with a menulist of
- * calendars, this is the currently chosen calendar, otherwise its the fixed
- * calendar from the window's item.
- *
- * @return      The currently selected calendar.
- */
-function getCurrentCalendar() {
-  let calendarNode = document.getElementById("item-calendar");
-  return calendarNode && calendarNode.selectedItem
-    ? calendarNode.selectedItem.calendar
-    : window.calendarItem.calendar;
-}
-
-/**
  * Common update functions for both event dialogs. Called when a reminder has
  * been selected from the menulist.
  *
- * @param aSuppressDialogs     If true, controls are updated without prompting
- *                               for changes with the dialog
+ * @param {Element} reminderList - The reminders menu element.
+ * @param {calIEvent | calITodo} calendarItem - The calendar item.
+ * @param {number} lastAlarmSelection - Index of the previous selection in the reminders menu.
+ * @param {Element} reminderDetails - The reminder details element.
+ * @param {calITimezone} timezone - The relevant timezone.
+ * @param {boolean} suppressDialogs - If true, controls are updated without prompting
+ *                                    for changes with the dialog
+ * @return {number} Index of the item selected in the reminders menu.
  */
-function commonUpdateReminder(aSuppressDialogs) {
+function commonUpdateReminder(
+  reminderList,
+  calendarItem,
+  lastAlarmSelection,
+  calendar,
+  reminderDetails,
+  timezone,
+  suppressDialogs
+) {
   // if a custom reminder has been selected, we show the appropriate
   // dialog in order to allow the user to specify the details.
   // the result will be placed in the 'reminder-custom-menuitem' tag.
-  let reminderList = document.getElementById("item-alarm");
   if (reminderList.value == "custom") {
     // Clear the reminder icons first, this will make sure that while the
     // dialog is open the default reminder image is not shown which may
     // confuse users.
-    removeChildren("reminder-icon-box");
+    removeChildren(reminderDetails.querySelector(".reminder-icon-box"));
 
     // show the dialog. This call blocks until the dialog is closed. Don't
     // pop up the dialog if aSuppressDialogs was specified or if this
     // happens during initialization of the dialog
-    if (!aSuppressDialogs && reminderList.hasAttribute("last-value")) {
-      editReminder();
+    if (!suppressDialogs && reminderList.hasAttribute("last-value")) {
+      editReminder(reminderList, calendarItem, lastAlarmSelection, calendar, timezone);
     }
 
     if (reminderList.value == "custom") {
       // Only do this if the 'custom' item is still selected. If the edit
       // reminder dialog was canceled then the previously selected
       // menuitem is selected, which may not be the custom menuitem.
 
       // If one or no reminders were selected, we have a chance of mapping
       // them to the existing elements in the dropdown.
       let customItem = reminderList.selectedItem;
       if (customItem.reminders.length == 0) {
         // No reminder was selected
         reminderList.value = "none";
       } else if (customItem.reminders.length == 1) {
         // We might be able to match the custom reminder with one of the
         // default menu items.
-        matchCustomReminderToMenuitem(customItem.reminders[0]);
+        matchCustomReminderToMenuitem(customItem.reminders[0], reminderList, calendar);
       }
     }
   }
 
-  // remember the current reminder drop down selection index.
-  gLastAlarmSelection = reminderList.selectedIndex;
   reminderList.setAttribute("last-value", reminderList.value);
 
   // possibly the selected reminder conflicts with the item.
   // for example an end-relation combined with a task without duedate
   // is an invalid state we need to take care of. we take the same
   // approach as with recurring tasks. in case the reminder is related
   // to the entry date we check the entry date automatically and disable
   // the checkbox. the same goes for end related reminder and the due date.
-  if (cal.item.isToDo(window.calendarItem)) {
+  if (cal.item.isToDo(calendarItem)) {
     // In general, (re-)enable the due/entry checkboxes. This will be
     // changed in case the alarms are related to START/END below.
     enableElementWithLock("todo-has-duedate", "reminder-lock");
     enableElementWithLock("todo-has-entrydate", "reminder-lock");
 
     let menuitem = reminderList.selectedItem;
     if (menuitem.value != "none") {
       // In case a reminder is selected, retrieve the array of alarms from
       // it, or create one from the currently selected menuitem.
-      let reminders = menuitem.reminders || [createReminderFromMenuitem(menuitem)];
+      let reminders = menuitem.reminders || [createReminderFromMenuitem(menuitem, calendar)];
 
       // If a reminder is related to the entry date...
       if (reminders.some(x => x.related == x.ALARM_RELATED_START)) {
         // ...automatically check 'has entrydate'.
         if (!getElementValue("todo-has-entrydate", "checked")) {
           setElementValue("todo-has-entrydate", "true", "checked");
 
           // Make sure gStartTime is properly initialized
@@ -486,113 +506,118 @@ function commonUpdateReminder(aSuppressD
           updateDueDate();
         }
 
         // Disable the checkbox to indicate that we need the entry-date.
         disableElementWithLock("todo-has-duedate", "reminder-lock");
       }
     }
   }
-  updateReminderDetails();
+  updateReminderDetails(reminderDetails, reminderList, calendar);
+
+  // Return the current reminder drop down selection index so it can be remembered.
+  return reminderList.selectedIndex;
 }
 
 /**
  * Updates the related link on the dialog. Currently only used by the
  * read-only summary dialog.
+ *
+ * @param {string} itemUrlString - The calendar item URL as a string.
+ * @param {Element} linkRow - The row containing the link.
+ * @param {Element} urlLink - The link element itself.
  */
-function updateLink() {
-  function hideOrShow(aBool) {
-    setElementValue("event-grid-link-row", !aBool && "true", "hidden");
-    let separator = document.getElementById("event-grid-link-separator");
-    if (separator) {
-      // The separator is not there in the summary dialog
-      setElementValue("event-grid-link-separator", !aBool && "true", "hidden");
-    }
-  }
-
-  let itemUrlString = window.calendarItem.getProperty("URL") || "";
+function updateLink(itemUrlString, linkRow, urlLink) {
   let linkCommand = document.getElementById("cmd_toggle_link");
 
   if (linkCommand) {
-    // Disable if there is no url
-    setElementValue(linkCommand, !itemUrlString.length && "true", "disabled");
+    // Disable if there is no url.
+    setBooleanAttribute(linkCommand, "disabled", !itemUrlString);
   }
 
   if ((linkCommand && linkCommand.getAttribute("checked") != "true") || !itemUrlString.length) {
     // Hide if there is no url, or the menuitem was chosen so that the url
     // should be hidden
-    hideOrShow(false);
+    setBooleanAttribute(linkRow, "hidden", true);
   } else {
     let handler, uri;
     try {
       uri = Services.io.newURI(itemUrlString);
       handler = Services.io.getProtocolHandler(uri.scheme);
     } catch (e) {
       // No protocol handler for the given protocol, or invalid uri
-      hideOrShow(false);
+      setBooleanAttribute(linkRow, "hidden", true);
       return;
     }
 
     // Only show if its either an internal protocol handler, or its external
     // and there is an external app for the scheme
     handler = cal.wrapInstance(handler, Ci.nsIExternalProtocolHandler);
-    hideOrShow(!handler || handler.externalAppExistsForScheme(uri.scheme));
+    let show = !handler || handler.externalAppExistsForScheme(uri.scheme);
+    setBooleanAttribute(linkRow, "hidden", !show);
 
     setTimeout(() => {
-      // HACK the url-link doesn't crop when setting the value in onLoad
-      setElementValue("url-link", itemUrlString);
-      setElementValue("url-link", itemUrlString, "href");
+      // HACK the url link doesn't crop when setting the value in onLoad
+      setElementValue(urlLink, itemUrlString);
+      setElementValue(urlLink, itemUrlString, "href");
     }, 0);
   }
 }
 
-/*
- * setup attendees in event and summary dialog
+/**
+ * Set up attendees in event and summary dialog.
+ *
+ * @param {calIAttendee[]} attendees - The attendees.
+ * @param {Element} container - Element containing attendees rows, template, etc.
+ * @param {number} attendeesInRow - The number of attendees that can fit in each row.
+ * @param {number} maxLabelWidth - Maximum width of the label.
+ * @return {{attendeesInRow: number, maxLabelWidth: number}} The new values.
  */
-function setupAttendees() {
-  let attBox = document.getElementById("item-attendees-box");
+function setupAttendees(attendees, container, attendeesInRow, maxLabelWidth) {
+  let attBox = container.querySelector(".item-attendees-box");
   let attBoxRows = attBox.getElementsByClassName("item-attendees-row");
+  let newAttendeesInRow = attendeesInRow;
+  let newMaxLabelWidth = maxLabelWidth;
 
-  if (window.attendees && window.attendees.length > 0) {
+  if (attendees && attendees.length > 0) {
     // cloning of the template nodes
-    let selector = "#item-attendees-box-template .item-attendees-row";
-    let clonedRow = document.querySelector(selector).cloneNode(false);
-    selector = "#item-attendees-box-template .item-attendees-row box:nth-of-type(1)";
-    let clonedCell = document.querySelector(selector).cloneNode(true);
-    selector = "#item-attendees-box-template .item-attendees-row box:nth-of-type(2)";
-    let clonedSpacer = document.querySelector(selector).cloneNode(false);
+    let row = container.querySelector(".item-attendees-box-template .item-attendees-row");
+
+    let clonedRow = row.cloneNode(false);
+    let clonedCell = row.querySelector("box:nth-of-type(1)").cloneNode(true);
+    let clonedSpacer = row.querySelector("box:nth-of-type(2)").cloneNode(false);
 
     // determining of attendee box setup
-    let inRow = window.attendeesInRow || -1;
+    let inRow = attendeesInRow || -1;
     if (inRow == -1) {
-      inRow = determineAttendeesInRow();
-      window.attendeesInRow = inRow;
+      inRow = determineAttendeesInRow(maxLabelWidth);
+      newAttendeesInRow = inRow;
     } else {
       while (attBoxRows.length > 0) {
         attBox.removeChild(attBoxRows[0]);
       }
     }
 
     // set up of the required nodes
-    let maxRows = Math.ceil(window.attendees.length / inRow);
-    let inLastRow = window.attendees.length - (maxRows - 1) * inRow;
+    let maxRows = Math.ceil(attendees.length / inRow);
+    let inLastRow = attendees.length - (maxRows - 1) * inRow;
     let attCount = 0;
     while (attBox.getElementsByClassName("item-attendees-row").length < maxRows) {
       let newRow = clonedRow.cloneNode(false);
       let row = attBox.appendChild(newRow);
       row.removeAttribute("hidden");
       let rowCount = attBox.getElementsByClassName("item-attendees-row").length;
       let reqAtt = rowCount == maxRows ? inLastRow : inRow;
       // we add as many attendee cells as required
       while (row.children.length < reqAtt) {
         let newCell = clonedCell.cloneNode(true);
         let cell = row.appendChild(newCell);
         let icon = cell.getElementsByTagName("img")[0];
         let text = cell.getElementsByTagName("label")[0];
-        let attendee = window.attendees[attCount];
+        let attendee = attendees[attCount];
 
         let label =
           attendee.commonName && attendee.commonName.length
             ? attendee.commonName
             : attendee.toString();
         let userType = attendee.userType || "INDIVIDUAL";
         let role = attendee.role || "REQ-PARTICIPANT";
         let partstat = attendee.participationStatus || "NEEDS-ACTION";
@@ -612,17 +637,17 @@ function setupAttendees() {
         let partstatString = cal.l10n.getCalString("dialog.tooltip.attendeePartStat2." + partstat, [
           label,
         ]);
         let tooltip = cal.l10n.getCalString("dialog.tooltip.attendee.combined", [
           roleString,
           partstatString,
         ]);
 
-        let del = cal.itip.resolveDelegation(attendee, window.attendees);
+        let del = cal.itip.resolveDelegation(attendee, attendees);
         if (del.delegators != "") {
           del.delegators = cal.l10n.getCalString("dialog.attendee.append.delegatedFrom", [
             del.delegators,
           ]);
           label += " " + del.delegators;
           tooltip += " " + del.delegators;
         }
         if (del.delegatees != "") {
@@ -643,53 +668,63 @@ function setupAttendees() {
           newSpacer.removeAttribute("hidden");
           row.appendChild(newSpacer);
         }
       }
     }
 
     // determining of the max width of an attendee label - this needs to
     // be done only once and is obsolete in case of resizing
-    if (!window.maxLabelWidth) {
+    if (!maxLabelWidth) {
       let maxWidth = 0;
       for (let cell of attBox.getElementsByClassName("item-attendees-cell")) {
         cell = cell.cloneNode(true);
         cell.removeAttribute("flex");
         cell.getElementsByTagName("label")[0].removeAttribute("flex");
         maxWidth = cell.clientWidth > maxWidth ? cell.clientWidth : maxWidth;
       }
-      window.maxLabelWidth = maxWidth;
+      newMaxLabelWidth = maxWidth;
     }
   } else {
     while (attBoxRows.length > 0) {
       attBox.removeChild(attBoxRows[0]);
     }
   }
+  return { attendeesInRow: newAttendeesInRow, maxLabelWidth: newMaxLabelWidth };
 }
 
 /**
  * Re-arranges the attendees on dialog resizing in event and summary dialog
+ *
+ * @param {calIAttendee[]} attendees - The attendees.
+ * @param {Element} parent - Element containing attendees rows, template, etc.
+ * @param {number} attendeesInRow - The number of attendees that can fit in each row.
+ * @param {number} maxLabelWidth - Maximum width of the label.
+ * @return {{attendeesInRow: number, maxLabelWidth: number}} The new values.
  */
-function rearrangeAttendees() {
-  if (window.attendees && window.attendees.length > 0 && window.attendeesInRow) {
-    let inRow = determineAttendeesInRow();
-    if (inRow != window.attendeesInRow) {
-      window.attendeesInRow = inRow;
-      setupAttendees();
+function rearrangeAttendees(attendees, parent, attendeesInRow, maxLabelWidth) {
+  if (attendees && attendees.length > 0 && attendeesInRow) {
+    let inRow = determineAttendeesInRow(maxLabelWidth);
+    if (inRow != attendeesInRow) {
+      return setupAttendees(attendees, parent, inRow, maxLabelWidth);
     }
   }
+  return { attendeesInRow, maxLabelWidth };
 }
 
 /**
  * Calculates the number of columns to distribute attendees for event and summary dialog
+ *
+ * @param {number} maxLabelWidth - The maximum width for the label.
+ * @return {number} The number of attendees that can fit in a row.
  */
-function determineAttendeesInRow() {
+function determineAttendeesInRow(maxLabelWidth) {
   // as default value a reasonable high value is appropriate
   // it will be recalculated anyway.
-  let minWidth = window.maxLabelWidth || 200;
+  let minWidth = maxLabelWidth || 200;
   let inRow = Math.floor(document.documentElement.clientWidth / minWidth);
   return inRow > 1 ? inRow : 1;
 }
 
 /**
  * Adapts the scheduling responsibility for caldav servers according to RfC 6638
  * based on forceEmailScheduling preference for the respective calendar
  *
@@ -740,8 +775,74 @@ function adaptScheduleAgent(aItem) {
           aAttendee.deleteProperty("SCHEDULE-AGENT");
         }
       });
     } else if (aItem.organizer && aItem.organizer.getProperty("SCHEDULE-AGENT") == "CLIENT") {
       aItem.organizer.deleteProperty("SCHEDULE-AGENT");
     }
   }
 }
+
+/**
+ * Extracts the item's organizer and opens a compose window to send the
+ * organizer an email.
+ *
+ * @param {calIEvent | calITodo} item - The calendar item.
+ */
+function sendMailToOrganizer(item) {
+  let organizer = item.organizer;
+  let email = cal.email.getAttendeeEmail(organizer, true);
+  let emailSubject = cal.l10n.getString("calendar-event-dialog", "emailSubjectReply", [item.title]);
+  let identity = item.calendar.getProperty("imip.identity");
+  cal.email.sendTo(email, emailSubject, null, identity);
+}
+
+/**
+ * Opens an attachment.
+ *
+ * @param {AUTF8String}  aAttachmentId   The hashId of the attachment to open.
+ * @param {calIEvent | calITodo} item    The calendar item.
+ */
+function openAttachmentFromItemSummary(aAttachmentId, item) {
+  if (!aAttachmentId) {
+    return;
+  }
+  let attachments = item
+    .getAttachments()
+    .filter(aAttachment => aAttachment.hashId == aAttachmentId);
+
+  if (attachments.length && attachments[0].uri && attachments[0].uri.spec != "about:blank") {
+    Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+      .getService(Ci.nsIExternalProtocolService)
+      .loadURI(attachments[0].uri);
+  }
+}
+
+/**
+ * Given a calendar event or task, return a string that describes the item's
+ * recurrence pattern, or null if there is no recurrence info.
+ *
+ * @param {calIEvent | calITodo} item - A calendar item.
+ * @return {string | null} A string describing the item's recurrence pattern or null.
+ */
+function getRecurrenceString(item) {
+  // Recurrence info is stored on the parent item.
+  let parent = item.parentItem;
+
+  let recurrenceInfo = parent.recurrenceInfo;
+  if (!recurrenceInfo) {
+    return null;
+  }
+
+  let kDefaultTimezone = cal.dtz.defaultTimezone;
+
+  let rawStartDate = parent.startDate || parent.entryDate;
+  let rawEndDate = parent.endDate || parent.dueDate;
+
+  let startDate = rawStartDate ? rawStartDate.getInTimezone(kDefaultTimezone) : null;
+  let endDate = rawEndDate ? rawEndDate.getInTimezone(kDefaultTimezone) : null;
+
+  let details =
+    recurrenceRule2String(recurrenceInfo, startDate, endDate, startDate.isDate) ||
+    cal.l10n.getString("calendar-event-dialog", "ruleTooComplexSummary");
+
+  return details;
+}
--- a/calendar/base/content/dialogs/calendar-summary-dialog.css
+++ b/calendar/base/content/dialogs/calendar-summary-dialog.css
@@ -20,22 +20,22 @@ td {
 .calendar-summary-table {
   width: 100%;
 }
 
 .calendar-summary-table input {
   width: 100%;
 }
 
-#repeat-row > th {
+.repeat-row > th {
   vertical-align: top;
   padding-top: 2px;
 }
 
-#repeat-details {
+.repeat-details {
   width: 100%;
 }
 
 .calendar-summary-table > tr > th > label {
   margin-inline-end: 0;
 }
 
 .calendar-summary-table > tr > td > label {
--- a/calendar/base/content/dialogs/calendar-summary-dialog.js
+++ b/calendar/base/content/dialogs/calendar-summary-dialog.js
@@ -1,15 +1,13 @@
 /* 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/. */
 
-/* exported onLoad, onUnload, updatePartStat, browseDocument,
- *          sendMailToOrganizer, openAttachment, reply
- */
+/* exported onLoad, onUnload, updatePartStat, browseDocument, reply */
 
 /* global MozElements */
 
 /* import-globals-from ../../src/calApplicationUtils.js */
 /* import-globals-from calendar-dialog-utils.js */
 
 var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
 var { recurrenceRule2String } = ChromeUtils.import(
@@ -31,16 +29,18 @@ XPCOMUtils.defineLazyGetter(gNotificatio
  */
 function onLoad() {
   let args = window.arguments[0];
   let item = args.calendarEvent;
   item = item.clone(); // use an own copy of the passed item
   window.calendarItem = item;
   let dialog = document.querySelector("dialog");
 
+  document.title = item.title;
+
   // the calling entity provides us with an object that is responsible
   // for recording details about the initiated modification. the 'finalize'-property
   // is our hook in order to receive a notification in case the operation needs
   // to be terminated prematurely. this function will be called if the calling
   // entity needs to immediately terminate the pending modification. in this
   // case we serialize the item and close the window.
   if (args.job) {
     // store the 'finalize'-functor in the provided job-object.
@@ -59,24 +59,23 @@ function onLoad() {
 
   // set the dialog-id to enable the right window-icon to be loaded.
   if (cal.item.isEvent(item)) {
     setDialogId(dialog, "calendar-event-summary-dialog");
   } else if (cal.item.isToDo(item)) {
     setDialogId(dialog, "calendar-task-summary-dialog");
   }
 
-  window.attendees = item.getAttendees();
+  // Start setting up the item summary custom element.
+  let itemSummary = document.getElementById("calendar-item-summary");
+  itemSummary.item = item;
 
-  let calendar = cal.wrapInstance(item.calendar, Ci.calISchedulingSupport);
-  window.readOnly = !(
-    cal.acl.isCalendarWritable(calendar) &&
-    (cal.acl.userCanModifyItem(item) ||
-      (calendar && item.calendar.isInvitation(item) && cal.acl.userCanRespondToInvitation(item)))
-  );
+  window.readOnly = itemSummary.readOnly;
+  let calendar = itemSummary.calendar;
+
   if (!window.readOnly && calendar) {
     let attendee = calendar.getInvitedAttendee(item);
     if (attendee) {
       // if this is an unresponded invitation, preset our default alarm values:
       if (!item.getAlarms().length && attendee.participationStatus == "NEEDS-ACTION") {
         cal.alarms.setDefaultValues(item);
       }
 
@@ -86,206 +85,23 @@ function onLoad() {
       // (i.e REPLY on a mailing list)
       item.removeAttendee(attendee);
       item.addAttendee(window.attendee);
 
       window.responseMode = "USER";
     }
   }
 
-  document.getElementById("item-title").value = item.title;
-
-  document.getElementById("item-calendar").value = calendar.name;
-
-  let isToDoItem = cal.item.isToDo(item);
-
-  // Show start date.
-  let itemStartDate = item[cal.dtz.startDateProp(item)];
-
-  let itemStartRowLabel = document.getElementById("item-start-row-label");
-  let itemDateRowStartDate = document.getElementById("item-date-row-start-date");
-
-  itemStartRowLabel.style.visibility = itemStartDate ? "visible" : "collapse";
-  itemDateRowStartDate.style.visibility = itemStartDate ? "visible" : "collapse";
-
-  if (itemStartDate) {
-    let itemStartLabelValue = itemStartRowLabel.getAttribute(
-      isToDoItem ? "taskStartLabel" : "eventStartLabel"
-    );
-    itemStartRowLabel.setAttribute("value", itemStartLabelValue);
-    itemDateRowStartDate.value = cal.dtz.getStringForDateTime(itemStartDate);
-  }
-
-  // Show due date / end date.
-  let itemDueDate = item[cal.dtz.endDateProp(item)];
-
-  let itemDueRowLabel = document.getElementById("item-due-row-label");
-  let itemDateRowEndDate = document.getElementById("item-date-row-end-date");
-
-  itemDueRowLabel.style.visibility = itemDueDate ? "visible" : "collapse";
-  itemDateRowEndDate.style.visibility = itemDueDate ? "visible" : "collapse";
-
-  if (itemDueDate) {
-    let itemDueLabelValue = itemDueRowLabel.getAttribute(
-      isToDoItem ? "taskDueLabel" : "eventEndLabel"
-    );
-    itemDueRowLabel.setAttribute("value", itemDueLabelValue);
-    itemDateRowEndDate.value = cal.dtz.getStringForDateTime(itemDueDate);
-  }
-
-  // Show reminder if this item is *not* readonly.
-  // This case happens for example if this is an invitation.
-  if (!window.readOnly) {
-    let argCalendar = window.arguments[0].calendarEvent.calendar;
-    let supportsReminders =
-      argCalendar.getProperty("capabilities.alarms.oninvitations.supported") !== false;
-
-    if (supportsReminders) {
-      document.getElementById("reminder-row").removeAttribute("hidden");
-      loadReminders(window.calendarItem.getAlarms());
-      updateReminder();
-    }
-  }
-
-  updateRecurrenceDetails(getRecurrenceString(item));
-  updateAttendees();
-  updateLink();
-
-  let location = item.getProperty("LOCATION");
-
-  if (location) {
-    document.getElementById("location-row").removeAttribute("hidden");
-    let urlMatch = location.match(/(https?:\/\/[^ ]*)/);
-    let url = urlMatch && urlMatch[1];
-    let itemLocation = document.getElementById("item-location");
-    if (url) {
-      let locationLabel = document.createXULElement("label");
-      locationLabel.setAttribute("id", "item-location-link");
-      locationLabel.setAttribute("context", "location-link-context-menu");
-      locationLabel.setAttribute("class", "text-link");
-      locationLabel.setAttribute("value", url);
-      locationLabel.setAttribute("tooltiptext", url);
-      locationLabel.setAttribute("onclick", "launchBrowser(this.getAttribute('value'), event)");
-      locationLabel.setAttribute("oncommand", "launchBrowser(this.getAttribute('value'), event)");
-      itemLocation.replaceWith(locationLabel);
-    } else {
-      itemLocation.value = location;
-    }
-  }
-
-  let categories = item.getCategories();
-  if (categories.length > 0) {
-    document.getElementById("category-row").removeAttribute("hidden");
-    document.getElementById("item-category").value = categories.join(", "); // TODO l10n-unfriendly
-  }
-
-  let organizer = item.organizer;
-  if (organizer && organizer.id) {
-    document.getElementById("organizer-row").removeAttribute("hidden");
-    let cell = document.getElementsByClassName("item-organizer-cell")[0];
-    let text = cell.getElementsByTagName("label")[0];
-    let icon = cell.getElementsByTagName("img")[0];
+  // Finish setting up the item summary custom element.
+  itemSummary.updateItemDetails();
 
-    let role = organizer.role || "REQ-PARTICIPANT";
-    let userType = organizer.userType || "INDIVIDUAL";
-    let partstat = organizer.participationStatus || "NEEDS-ACTION";
-    let orgName =
-      organizer.commonName && organizer.commonName.length
-        ? organizer.commonName
-        : organizer.toString();
-    let userTypeString = cal.l10n.getCalString("dialog.tooltip.attendeeUserType2." + userType, [
-      organizer.toString(),
-    ]);
-    let roleString = cal.l10n.getCalString("dialog.tooltip.attendeeRole2." + role, [
-      userTypeString,
-    ]);
-    let partstatString = cal.l10n.getCalString("dialog.tooltip.attendeePartStat2." + partstat, [
-      orgName,
-    ]);
-    let tooltip = cal.l10n.getCalString("dialog.tooltip.attendee.combined", [
-      roleString,
-      partstatString,
-    ]);
-
-    text.setAttribute("value", orgName);
-    cell.setAttribute("tooltiptext", tooltip);
-    icon.setAttribute("partstat", partstat);
-    icon.setAttribute("usertype", userType);
-    icon.setAttribute("role", role);
-  }
-
-  let status = item.getProperty("STATUS");
-  if (status && status.length) {
-    let statusRow = document.getElementById("status-row");
-    let statusRowData = document.getElementById("status-row-td");
-    for (let i = 0; i < statusRowData.children.length; i++) {
-      if (statusRowData.children[i].getAttribute("status") == status) {
-        statusRow.removeAttribute("hidden");
-        if (status == "CANCELLED" && cal.item.isToDo(item)) {
-          // There are two labels for CANCELLED, the second one is for
-          // todo items. Increment the counter here.
-          i++;
-        }
-        statusRowData.children[i].removeAttribute("hidden");
-        break;
-      }
-    }
-  }
+  window.addEventListener("resize", () => {
+    itemSummary.onWindowResize();
+  });
 
-  if (item.hasProperty("DESCRIPTION")) {
-    let description = item.getProperty("DESCRIPTION");
-    if (description && description.length) {
-      document.getElementById("item-description-box").removeAttribute("hidden");
-      let textbox = document.getElementById("item-description");
-      textbox.value = description;
-      textbox.readOnly = true;
-    }
-  }
-
-  document.title = item.title;
-
-  let attachments = item.getAttachments();
-  if (attachments.length) {
-    // we only want to display uri type attachments and no ones received inline with the
-    // invitation message (having a CID: prefix results in about:blank) here
-    let attCounter = 0;
-    attachments.forEach(aAttachment => {
-      if (aAttachment.uri && aAttachment.uri.spec != "about:blank") {
-        let attachment = document.getElementById("attachment-template").cloneNode(true);
-        attachment.removeAttribute("id");
-        attachment.removeAttribute("hidden");
-
-        let label = attachment.getElementsByTagName("label")[0];
-        label.setAttribute("value", aAttachment.uri.spec);
-        label.setAttribute("hashid", aAttachment.hashId);
-
-        let icon = attachment.getElementsByTagName("image")[0];
-        let iconSrc = aAttachment.uri.spec.length ? aAttachment.uri.spec : "dummy.html";
-        if (aAttachment.uri && !aAttachment.uri.schemeIs("file")) {
-          // using an uri directly with e.g. a http scheme wouldn't render any icon
-          if (aAttachment.formatType) {
-            iconSrc = "goat?contentType=" + aAttachment.formatType;
-          } else {
-            // let's try to auto-detect
-            let parts = iconSrc.substr(aAttachment.uri.scheme.length + 2).split("/");
-            if (parts.length) {
-              iconSrc = parts[parts.length - 1];
-            }
-          }
-        }
-        icon.setAttribute("src", "moz-icon://" + iconSrc);
-
-        document.getElementById("item-attachment-cell").appendChild(attachment);
-        attCounter++;
-      }
-    });
-    if (attCounter > 0) {
-      document.getElementById("attachments-row").removeAttribute("hidden");
-    }
-  }
   // If this item is read only we remove the 'cancel' button as users
   // can't modify anything, thus we go ahead with an 'ok' button only.
   if (window.readOnly) {
     dialog.getButton("cancel").setAttribute("collapsed", "true");
     dialog.getButton("accept").focus();
   }
 
   // disable default controls
@@ -322,17 +138,17 @@ document.addEventListener("dialogaccept"
   // let's make sure we have a response mode defined
   let resp = window.responseMode || "USER";
   let respMode = { responseMode: Ci.calIItipItem[resp] };
 
   let args = window.arguments[0];
   let oldItem = args.calendarEvent;
   let newItem = window.calendarItem;
   let calendar = newItem.calendar;
-  saveReminder(newItem);
+  saveReminder(newItem, calendar, document.querySelector(".item-alarm"));
   adaptScheduleAgent(newItem);
   args.onOk(newItem, calendar, oldItem, null, respMode);
   window.calendarItem = newItem;
 });
 
 /**
  * Called when closing the dialog and any changes should be thrown away.
  */
@@ -425,153 +241,31 @@ function updateToolbar() {
       gNotification.notificationbox.PRIORITY_INFO_MEDIUM
     );
   } else {
     gNotification.notificationbox.removeAllNotifications();
   }
 }
 
 /**
- * Updates the dialog w.r.t recurrence, i.e shows a text describing the item's
- * recurrence.
- *
- * @param {string} details - Recurrence details as a string.
- */
-function updateRecurrenceDetails(details) {
-  let repeatRow = document.getElementById("repeat-row");
-  let repeatDetails = document.getElementById("repeat-details");
-
-  if (!details) {
-    repeatRow.setAttribute("hidden", "true");
-    repeatDetails.setAttribute("collapsed", "true");
-
-    while (repeatDetails.children.length) {
-      repeatDetails.lastChild.remove();
-    }
-    return;
-  }
-
-  repeatRow.removeAttribute("hidden");
-  repeatDetails.removeAttribute("collapsed");
-
-  let lines = details.split("\n");
-
-  while (repeatDetails.children.length > lines.length) {
-    repeatDetails.lastChild.remove();
-  }
-  while (repeatDetails.children.length < lines.length) {
-    repeatDetails.appendChild(repeatDetails.firstElementChild.cloneNode(true));
-  }
-  for (let i = 0; i < lines.length; i++) {
-    repeatDetails.children[i].value = lines[i];
-    repeatDetails.children[i].setAttribute("tooltiptext", details);
-  }
-}
-
-/**
- * Given a calendar event or task, return a string that describes the item's
- * recurrence pattern, or null if there is no recurrence info.
- *
- * @param {calIItem} item - A calendar item.
- * @return {string | null} A string describing the item's recurrence pattern or null.
- */
-function getRecurrenceString(item) {
-  // Recurrence info is stored on the parent item.
-  let parent = item.parentItem;
-
-  let recurrenceInfo = parent.recurrenceInfo;
-  if (!recurrenceInfo) {
-    return null;
-  }
-
-  let kDefaultTimezone = cal.dtz.defaultTimezone;
-
-  let rawStartDate = parent.startDate || parent.entryDate;
-  let rawEndDate = parent.endDate || parent.dueDate;
-
-  let startDate = rawStartDate ? rawStartDate.getInTimezone(kDefaultTimezone) : null;
-  let endDate = rawEndDate ? rawEndDate.getInTimezone(kDefaultTimezone) : null;
-
-  let details =
-    recurrenceRule2String(recurrenceInfo, startDate, endDate, startDate.isDate) ||
-    cal.l10n.getString("calendar-event-dialog", "ruleTooComplexSummary");
-
-  return details;
-}
-
-/**
- * Updates the attendee listbox, displaying all attendees invited to the
- * window's item.
- */
-function updateAttendees() {
-  if (window.attendees && window.attendees.length) {
-    document.getElementById("item-attendees").removeAttribute("hidden");
-    setupAttendees();
-  }
-}
-
-/**
- * Updates the reminder, called when a reminder has been selected in the
- * menulist.
- */
-function updateReminder() {
-  commonUpdateReminder();
-}
-
-/**
  * Browse the item's attached URL.
  *
  * XXX This function is broken, should be fixed in bug 471967
  */
 function browseDocument() {
   let args = window.arguments[0];
   let item = args.calendarEvent;
   let url = item.getProperty("URL");
   launchBrowser(url);
 }
 
 /**
- * Extracts the item's organizer and opens a compose window to send the
- * organizer an email.
- */
-function sendMailToOrganizer() {
-  let args = window.arguments[0];
-  let item = args.calendarEvent;
-  let organizer = item.organizer;
-  let email = cal.email.getAttendeeEmail(organizer, true);
-  let emailSubject = cal.l10n.getString("calendar-event-dialog", "emailSubjectReply", [item.title]);
-  let identity = item.calendar.getProperty("imip.identity");
-  cal.email.sendTo(email, emailSubject, null, identity);
-}
-
-/**
- * Opens an attachment
- *
- * @param {AUTF8String}  aAttachmentId   The hashId of the attachment to open
- */
-function openAttachment(aAttachmentId) {
-  if (!aAttachmentId) {
-    return;
-  }
-  let args = window.arguments[0];
-  let item = args.calendarEvent;
-  let attachments = item
-    .getAttachments()
-    .filter(aAttachment => aAttachment.hashId == aAttachmentId);
-  if (attachments.length && attachments[0].uri && attachments[0].uri.spec != "about:blank") {
-    Cc["@mozilla.org/uriloader/external-protocol-service;1"]
-      .getService(Ci.nsIExternalProtocolService)
-      .loadURI(attachments[0].uri);
-  }
-}
-
-/**
  * Copy the value of the given link node to the clipboard.
  *
- * @param {string} linkNode The node containing the value to copy to the clipboard.
+ * @param {string} linkNode - The node containing the value to copy to the clipboard.
  */
 function locationCopyLink(linkNode) {
   if (linkNode) {
     let linkAddress = linkNode.value;
     let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
     clipboard.copyString(linkAddress);
   }
 }
--- a/calendar/base/content/dialogs/calendar-summary-dialog.xhtml
+++ b/calendar/base/content/dialogs/calendar-summary-dialog.xhtml
@@ -25,31 +25,31 @@
   %brandDTD;
 ]>
 
 <!-- Dialog id is changed during execution to allow different Window-icons
      on this dialog. document.loadOverlay() will not work on this one. -->
 <window windowtype="Calendar:EventSummaryDialog"
         onload="onLoad()"
         onunload="onUnload()"
-        onresize="rearrangeAttendees();"
         persist="screenX screenY width height"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
 
 <dialog id="calendar-summary-dialog">
 
   <!-- Javascript includes -->
   <script src="chrome://global/content/globalOverlay.js"/>
   <script src="chrome://global/content/editMenuOverlay.js"/>
   <script src="chrome://calendar/content/calendar-summary-dialog.js"/>
   <script src="chrome://calendar/content/calendar-dialog-utils.js"/>
   <script src="chrome://calendar/content/calendar-ui-utils.js"/>
   <script src="chrome://calendar/content/calendar-item-editing.js"/>
   <script src="chrome://calendar/content/calApplicationUtils.js"/>
+  <script src="chrome://calendar/content/widgets/calendar-item-summary.js"/>
 
   <toolbox id="summary-toolbox"
            class="mail-toolbox"
            mode="full"
            defaultmode="full"
            iconsize="small"
            defaulticonsize="small"
            labelalign="end"
@@ -117,308 +117,17 @@
         </menupopup>
       </toolbarbutton>
     </toolbar>
   </toolbox>
 
   <hbox id="status-notifications">
     <!-- notificationbox will be added here lazily. -->
   </hbox>
-
-  <!-- General -->
-  <box id="item-general-box" orient="vertical">
-    <hbox id="item-general-caption" class="calendar-caption" align="center">
-      <label value="&read.only.general.label;" class="header"/>
-      <separator class="groove" flex="1"/>
-    </hbox>
-    <hbox flex="1" class="calendar-summary-box">
-      <html:table class="calendar-summary-table">
-        <html:tr>
-          <html:th>
-            &read.only.title.label;
-          </html:th>
-          <html:td>
-            <html:input id="item-title"
-                        class="selectable-label plain input-inline"
-                        readonly="readonly"/>
-          </html:td>
-        </html:tr>
-        <html:tr>
-          <html:th>
-            &read.only.calendar.label;
-          </html:th>
-          <html:td>
-            <html:input id="item-calendar"
-                        class="selectable-label plain input-inline"
-                        readonly="readonly"/>
-          </html:td>
-        </html:tr>
-        <html:tr class="item-date-row">
-          <html:th>
-            <label id="item-start-row-label"
-                   taskStartLabel="&read.only.task.start.label;"
-                   eventStartLabel="&read.only.event.start.label;"/>
-          </html:th>
-          <html:td>
-            <html:input id="item-date-row-start-date"
-                        class="selectable-label plain input-inline"
-                        readonly="readonly"/>
-          </html:td>
-        </html:tr>
-        <html:tr class="item-date-row">
-          <html:th>
-            <label id="item-due-row-label"
-                   taskDueLabel="&read.only.task.due.label;"
-                   eventEndLabel="&read.only.event.end.label;"/>
-          </html:th>
-          <html:td>
-            <html:input id="item-date-row-end-date"
-                        class="selectable-label plain input-inline"
-                        readonly="readonly"/>
-          </html:td>
-        </html:tr>
-        <html:tr id="repeat-row" hidden="hidden">
-          <html:th>
-            &read.only.repeat.label;
-          </html:th>
-          <html:td>
-            <box id="repeat-details" orient="vertical">
-              <html:input readonly="readonly" class="selectable-label plain input-inline"/>
-            </box>
-          </html:td>
-        </html:tr>
-        <html:tr id="location-row" hidden="hidden">
-          <html:th>
-            &read.only.location.label;
-          </html:th>
-          <html:td>
-            <html:input id="item-location"
-                        class="selectable-label plain input-inline"
-                        readonly="readonly"/>
-          </html:td>
-        </html:tr>
-        <html:tr id="category-row" hidden="hidden">
-          <html:th>
-            &read.only.category.label;
-          </html:th>
-          <html:td>
-            <label id="item-category"/>
-          </html:td>
-        </html:tr>
-        <html:tr id="organizer-row" class="item-attendees-row" hidden="hidden">
-          <html:th>
-            &read.only.organizer.label;
-          </html:th>
-          <html:td>
-            <hbox class="item-organizer-cell">
-              <img class="itip-icon"/>
-              <label id="item-organizer"
-                     class="text-link item-attendees-cell-label"
-                     crop="end"
-                     onclick="sendMailToOrganizer()"/>
-              <spacer flex="1"/>
-            </hbox>
-          </html:td>
-        </html:tr>
-        <html:tr id="status-row" hidden="hidden">
-          <html:th>
-            &task.status.label;
-          </html:th>
-          <html:td id="status-row-td">
-            <label value="&newevent.status.tentative.label;" hidden="true" status="TENTATIVE"/>
-            <label value="&newevent.status.confirmed.label;" hidden="true" status="CONFIRMED"/>
-            <label value="&newevent.eventStatus.cancelled.label;" hidden="true" status="CANCELLED"/>
-            <label value="&newevent.todoStatus.cancelled.label;" hidden="true" status="CANCELLED"/>
-            <label value="&newevent.status.needsaction.label;" hidden="true" status="NEEDS-ACTION"/>
-            <label value="&newevent.status.inprogress.label;" hidden="true" status="IN-PROCESS"/>
-            <label value="&newevent.status.completed.label;" hidden="true" status="COMPLETED"/>
-          </html:td>
-        </html:tr>
-        <separator id="item-main-separator" class="groove" flex="1" hidden="true"/>
-        <html:tr id="reminder-row" hidden="hidden">
-          <html:th>
-            &read.only.reminder.label;
-          </html:th>
-          <html:td>
-            <hbox id="event-grid-alarm-picker-box"
-                  align="center">
-              <menulist id="item-alarm"
-                        disable-on-readonly="true"
-                        oncommand="updateReminder()">
-                <menupopup id="item-alarm-menupopup">
-                  <menuitem id="reminder-none-menuitem"
-                            label="&event.reminder.none.label;"
-                            selected="true"
-                            value="none"/>
-                  <menuseparator id="reminder-none-separator"/>
-                  <menuitem id="reminder-0minutes-menuitem"
-                            label="&event.reminder.0minutes.before.label;"
-                            length="0"
-                            origin="before"
-                            relation="START"
-                            unit="minutes"/>
-                  <menuitem id="reminder-5minutes-menuitem"
-                            label="&event.reminder.5minutes.before.label;"
-                            length="5"
-                            origin="before"
-                            relation="START"
-                            unit="minutes"/>
-                  <menuitem id="reminder-15minutes-menuitem"
-                            label="&event.reminder.15minutes.before.label;"
-                            length="15"
-                            origin="before"
-                            relation="START"
-                            unit="minutes"/>
-                  <menuitem id="reminder-30minutes-menuitem"
-                            label="&event.reminder.30minutes.before.label;"
-                            length="30"
-                            origin="before"
-                            relation="START"
-                            unit="minutes"/>
-                  <menuseparator id="reminder-minutes-separator"/>
-                  <menuitem id="reminder-1hour-menuitem"
-                            label="&event.reminder.1hour.before.label;"
-                            length="1"
-                            origin="before"
-                            relation="START"
-                            unit="hours"/>
-                  <menuitem id="reminder-2hours-menuitem"
-                            label="&event.reminder.2hours.before.label;"
-                            length="2"
-                            origin="before"
-                            relation="START"
-                            unit="hours"/>
-                  <menuitem id="reminder-12hours-menuitem"
-                            label="&event.reminder.12hours.before.label;"
-                            length="12"
-                            origin="before"
-                            relation="START"
-                            unit="hours"/>
-                  <menuseparator id="reminder-hours-separator"/>
-                  <menuitem id="reminder-1day-menuitem"
-                            label="&event.reminder.1day.before.label;"
-                            length="1"
-                            origin="before"
-                            relation="START"
-                            unit="days"/>
-                  <menuitem id="reminder-2days-menuitem"
-                            label="&event.reminder.2days.before.label;"
-                            length="2"
-                            origin="before"
-                            relation="START"
-                            unit="days"/>
-                  <menuitem id="reminder-1week-menuitem"
-                            label="&event.reminder.1week.before.label;"
-                            length="7"
-                            origin="before"
-                            relation="START"
-                            unit="days"/>
-                  <menuseparator id="reminder-custom-separator"/>
-                  <menuitem id="reminder-custom-menuitem"
-                            label="&event.reminder.custom.label;"
-                            value="custom"/>
-                </menupopup>
-              </menulist>
-              <hbox id="reminder-details">
-                <hbox id="reminder-icon-box"
-                      class="alarm-icons-box"
-                      align="center"/>
-                <!-- TODO oncommand? onkeypress? -->
-                <label id="reminder-multiple-alarms-label"
-                       class="text-link"
-                       hidden="true"
-                       value="&event.reminder.multiple.label;"
-                       disable-on-readonly="true"
-                       flex="1"
-                       hyperlink="true"
-                       onclick="updateReminder()"/>
-                <label id="reminder-single-alarms-label"
-                       class="text-link"
-                       hidden="true"
-                       disable-on-readonly="true"
-                       flex="1"
-                       hyperlink="true"
-                       onclick="updateReminder()"/>
-              </hbox>
-            </hbox>
-          </html:td>
-        </html:tr>
-        <html:tr id="attachments-row" class="item-attachments-row" hidden="hidden" >
-          <html:th>
-            &read.only.attachments.label;
-          </html:th>
-          <html:td>
-            <vbox id="item-attachment-cell">
-              <!-- attachment box template -->
-              <hbox id="attachment-template"
-                    hidden="true"
-                    align="center"
-                    disable-on-readonly="true">
-                <image class="attachment-icon"/>
-                <label class="text-link item-attachment-cell-label"
-                       onclick="openAttachment(this.getAttribute('hashid'), event)"
-                       crop="end"
-                       flex="1" />
-              </hbox>
-            </vbox>
-          </html:td>
-        </html:tr>
-      </html:table>
-    </hbox>
-  </box>
-
-  <!-- attendee box template -->
-  <vbox id="item-attendees-box-template">
-    <hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true">
-      <box class="item-attendees-cell" hidden="true" flex="1">
-        <img class="itip-icon"/>
-        <label class="item-attendees-cell-label" crop="end" flex="1"/>
-      </box>
-      <box hidden="true" flex="1"/>
-    </hbox>
-  </vbox>
-
-  <!-- Attendees -->
-  <box id="item-attendees" orient="vertical" hidden="true" flex="1">
-    <spacer class="default-spacer"/>
-    <hbox id="item-attendees-caption" class="calendar-caption" align="center">
-      <label value="&read.only.attendees.label;"
-             control="item-attendees-box" class="header"/>
-      <separator class="groove" flex="1"/>
-    </hbox>
-    <vbox id="item-attendees-box" flex="1" />
-  </box>
-
-  <!-- Description -->
-  <box id="item-description-box" hidden="true" orient="vertical" flex="1">
-    <spacer class="default-spacer"/>
-    <hbox id="item-description-caption" class="calendar-caption" align="center">
-      <label value="&read.only.description.label;"
-             control="item-description" class="header"/>
-      <separator class="groove" flex="1"/>
-    </hbox>
-    <hbox id="item-description-wrapper" flex="1">
-      <html:textarea id="item-description" rows="6" flex="1"/>
-    </hbox>
-  </box>
-
-  <!-- URL link -->
-  <box id="event-grid-link-row" hidden="true" orient="vertical">
-    <spacer class="default-spacer"/>
-    <hbox id="event-grid-link-caption" class="calendar-caption" align="center">
-      <label value="&read.only.link.label;"
-             control="url-link" class="header"/>
-      <separator class="groove" flex="1"/>
-    </hbox>
-    <label id="url-link"
-           class="text-link default-indent"
-           onclick="launchBrowser(this.getAttribute('href'), event)"
-           oncommand="launchBrowser(this.getAttribute('href'), event)"
-           crop="end"/>
-  </box>
+  <calendar-item-summary id="calendar-item-summary"/>
 
   <!-- LOCATION LINK CONTEXT MENU -->
   <menupopup id="location-link-context-menu">
     <menuitem id="location-link-context-menu-copy"
               label="&calendar.copylink.label;"
               accesskey="&calendar.copylink.accesskey;"
               oncommand="locationCopyLink(document.popupNode)"/>
   </menupopup>
new file mode 100644
--- /dev/null
+++ b/calendar/base/content/widgets/calendar-item-summary.js
@@ -0,0 +1,724 @@
+/* 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/. */
+
+"use strict";
+
+/* global MozElements MozXULElement */
+
+/* import-globals-from ../../src/calApplicationUtils.js */
+/* import-globals-from ../dialogs/calendar-summary-dialog.js */
+/* import-globals-from ../dialogs/calendar-dialog-utils.js */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+  var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+  var { recurrenceRule2String } = ChromeUtils.import(
+    "resource:///modules/calendar/calRecurrenceUtils.jsm"
+  );
+
+  /**
+   * Represents a mostly read-only summary of a calendar item. Used in places
+   * like the calendar summary dialog and calendar import dialog. All instances
+   * should have an ID attribute.
+   */
+  class CalendarItemSummary extends MozXULElement {
+    connectedCallback() {
+      if (this.delayConnectedCallback() || this.hasConnected) {
+        return;
+      }
+      this.hasConnected = true;
+
+      // Generate IDs to make sure they are unique when there is more than one
+      // instance of this element in a given scope.
+      let id = this.getAttribute("id");
+      let itemAttendeesBoxId = id + "-item-attendees-box";
+      let itemDescriptionId = id + "-item-description";
+      let urlLinkId = id + "-url-link";
+
+      this.appendChild(
+        MozXULElement.parseXULToFragment(
+          `
+          <vbox class="item-summary-box" flex="1">
+            <!-- General -->
+            <box orient="vertical">
+              <hbox class="calendar-caption" align="center">
+                <label value="&read.only.general.label;" class="header"/>
+                <separator class="groove" flex="1"/>
+              </hbox>
+              <hbox flex="1" class="calendar-summary-box">
+                <html:table class="calendar-summary-table">
+                  <html:tr>
+                    <html:th>
+                      &read.only.title.label;
+                    </html:th>
+                    <html:td>
+                      <html:input class="item-title selectable-label plain input-inline"
+                                  readonly="readonly"/>
+                    </html:td>
+                  </html:tr>
+                  <html:tr>
+                    <html:th>
+                      &read.only.calendar.label;
+                    </html:th>
+                    <html:td>
+                      <html:input class="item-calendar selectable-label plain input-inline"
+                                  readonly="readonly"/>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="item-date-row">
+                    <html:th>
+                      <label class="item-start-row-label"
+                             taskStartLabel="&read.only.task.start.label;"
+                             eventStartLabel="&read.only.event.start.label;"/>
+                    </html:th>
+                    <html:td>
+                      <html:input class="item-date-row-start-date selectable-label plain input-inline"
+                                  readonly="readonly"/>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="item-date-row">
+                    <html:th>
+                      <label class="item-due-row-label"
+                             taskDueLabel="&read.only.task.due.label;"
+                             eventEndLabel="&read.only.event.end.label;"/>
+                    </html:th>
+                    <html:td>
+                      <html:input class="item-date-row-end-date selectable-label plain input-inline"
+                                  readonly="readonly"/>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="repeat-row" hidden="hidden">
+                    <html:th>
+                      &read.only.repeat.label;
+                    </html:th>
+                    <html:td>
+                      <box class="repeat-details" orient="vertical">
+                        <html:input readonly="readonly" class="selectable-label plain input-inline"/>
+                      </box>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="location-row" hidden="hidden">
+                    <html:th>
+                      &read.only.location.label;
+                    </html:th>
+                    <html:td>
+                      <html:input class="item-location selectable-label plain input-inline"
+                                  readonly="readonly"/>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="category-row" hidden="hidden">
+                    <html:th>
+                      &read.only.category.label;
+                    </html:th>
+                    <html:td>
+                      <label class="item-category"/>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="organizer-row item-attendees-row" hidden="hidden">
+                    <html:th>
+                      &read.only.organizer.label;
+                    </html:th>
+                    <html:td>
+                      <hbox class="item-organizer-cell">
+                        <img class="itip-icon"/>
+                        <label class="item-organizer-label text-link item-attendees-cell-label"
+                               crop="end"/>
+                        <spacer flex="1"/>
+                      </hbox>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="status-row" hidden="hidden">
+                    <html:th>
+                      &task.status.label;
+                    </html:th>
+                    <html:td class="status-row-td">
+                      <label value="&newevent.status.tentative.label;" hidden="true" status="TENTATIVE"/>
+                      <label value="&newevent.status.confirmed.label;" hidden="true" status="CONFIRMED"/>
+                      <label value="&newevent.eventStatus.cancelled.label;" hidden="true" status="CANCELLED"/>
+                      <label value="&newevent.todoStatus.cancelled.label;" hidden="true" status="CANCELLED"/>
+                      <label value="&newevent.status.needsaction.label;" hidden="true" status="NEEDS-ACTION"/>
+                      <label value="&newevent.status.inprogress.label;" hidden="true" status="IN-PROCESS"/>
+                      <label value="&newevent.status.completed.label;" hidden="true" status="COMPLETED"/>
+                    </html:td>
+                  </html:tr>
+                  <separator class="groove" flex="1" hidden="true"/>
+                  <html:tr class="reminder-row" hidden="hidden">
+                    <html:th>
+                      &read.only.reminder.label;
+                    </html:th>
+                    <html:td>
+                      <hbox align="center">
+                        <menulist class="item-alarm"
+                                  disable-on-readonly="true">
+                          <menupopup>
+                            <menuitem label="&event.reminder.none.label;"
+                                      selected="true"
+                                      value="none"/>
+                            <menuseparator/>
+                            <menuitem label="&event.reminder.0minutes.before.label;"
+                                      length="0"
+                                      origin="before"
+                                      relation="START"
+                                      unit="minutes"/>
+                            <menuitem label="&event.reminder.5minutes.before.label;"
+                                      length="5"
+                                      origin="before"
+                                      relation="START"
+                                      unit="minutes"/>
+                            <menuitem label="&event.reminder.15minutes.before.label;"
+                                      length="15"
+                                      origin="before"
+                                      relation="START"
+                                      unit="minutes"/>
+                            <menuitem label="&event.reminder.30minutes.before.label;"
+                                      length="30"
+                                      origin="before"
+                                      relation="START"
+                                      unit="minutes"/>
+                            <menuseparator/>
+                            <menuitem label="&event.reminder.1hour.before.label;"
+                                      length="1"
+                                      origin="before"
+                                      relation="START"
+                                      unit="hours"/>
+                            <menuitem label="&event.reminder.2hours.before.label;"
+                                      length="2"
+                                      origin="before"
+                                      relation="START"
+                                      unit="hours"/>
+                            <menuitem label="&event.reminder.12hours.before.label;"
+                                      length="12"
+                                      origin="before"
+                                      relation="START"
+                                      unit="hours"/>
+                            <menuseparator/>
+                            <menuitem label="&event.reminder.1day.before.label;"
+                                      length="1"
+                                      origin="before"
+                                      relation="START"
+                                      unit="days"/>
+                            <menuitem label="&event.reminder.2days.before.label;"
+                                      length="2"
+                                      origin="before"
+                                      relation="START"
+                                      unit="days"/>
+                            <menuitem label="&event.reminder.1week.before.label;"
+                                      length="7"
+                                      origin="before"
+                                      relation="START"
+                                      unit="days"/>
+                            <menuseparator/>
+                            <menuitem class="reminder-custom-menuitem"
+                                      label="&event.reminder.custom.label;"
+                                      value="custom"/>
+                          </menupopup>
+                        </menulist>
+                        <hbox class="reminder-details">
+                          <hbox class="reminder-icon-box alarm-icons-box"
+                                align="center"/>
+                          <!-- TODO oncommand? onkeypress? -->
+                          <label class="reminder-multiple-alarms-label text-link"
+                                 hidden="true"
+                                 value="&event.reminder.multiple.label;"
+                                 disable-on-readonly="true"
+                                 flex="1"
+                                 hyperlink="true"/>
+                          <label class="reminder-single-alarms-label text-link"
+                                 hidden="true"
+                                 disable-on-readonly="true"
+                                 flex="1"
+                                 hyperlink="true"/>
+                        </hbox>
+                      </hbox>
+                    </html:td>
+                  </html:tr>
+                  <html:tr class="attachments-row item-attachments-row" hidden="hidden" >
+                    <html:th>
+                      &read.only.attachments.label;
+                    </html:th>
+                    <html:td>
+                      <vbox class="item-attachment-cell">
+                        <!-- attachment box template -->
+                        <hbox class="attachment-template"
+                              hidden="true"
+                              align="center"
+                              disable-on-readonly="true">
+                          <image class="attachment-icon"/>
+                          <label class="text-link item-attachment-cell-label"
+                                 crop="end"
+                                 flex="1" />
+                        </hbox>
+                      </vbox>
+                    </html:td>
+                  </html:tr>
+                </html:table>
+              </hbox>
+            </box>
+
+            <!-- attendee box template -->
+            <vbox class="item-attendees-box-template">
+              <hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true">
+                <box class="item-attendees-cell" hidden="true" flex="1">
+                  <img class="itip-icon"/>
+                  <label class="item-attendees-cell-label" crop="end" flex="1"/>
+                </box>
+                <box hidden="true" flex="1"/>
+              </hbox>
+            </vbox>
+
+            <!-- Attendees -->
+            <box class="item-attendees" orient="vertical" hidden="true" flex="1">
+              <spacer class="default-spacer"/>
+              <hbox class="calendar-caption" align="center">
+                <label value="&read.only.attendees.label;"
+                       class="header"
+                       control="${itemAttendeesBoxId}"/>
+                <separator class="groove" flex="1"/>
+              </hbox>
+              <vbox id="${itemAttendeesBoxId}" class="item-attendees-box" flex="1" />
+            </box>
+
+            <!-- Description -->
+            <box class="item-description-box" hidden="true" orient="vertical" flex="1">
+              <spacer class="default-spacer"/>
+              <hbox class="calendar-caption" align="center">
+                <label value="&read.only.description.label;"
+                       control="${itemDescriptionId}"
+                       class="header"/>
+                <separator class="groove" flex="1"/>
+              </hbox>
+              <hbox class="item-description-wrapper" flex="1">
+                <html:textarea id="${itemDescriptionId}"
+                               class="item-description"
+                               rows="6"
+                               flex="1"/>
+              </hbox>
+            </box>
+
+            <!-- URL link -->
+            <box class="event-grid-link-row" hidden="true" orient="vertical">
+              <spacer class="default-spacer"/>
+              <hbox class="calendar-caption" align="center">
+                <label value="&read.only.link.label;"
+                       control="${urlLinkId}"
+                       class="header"/>
+                <separator class="groove" flex="1"/>
+              </hbox>
+              <label id="${urlLinkId}"
+                     class="url-link text-link default-indent"
+                     crop="end"/>
+            </box>
+          </vbox>
+          `,
+          [
+            "chrome://calendar/locale/global.dtd",
+            "chrome://calendar/locale/calendar.dtd",
+            "chrome://calendar/locale/calendar-event-dialog.dtd",
+            "chrome://branding/locale/brand.dtd",
+          ]
+        )
+      );
+      this.mItem = null;
+      this.mCalendar = null;
+      this.mReadOnly = true;
+
+      this.mAttendeesInRow = null;
+      this.mMaxLabelWidth = null;
+
+      this.mAlarmsMenu = this.querySelector(".item-alarm");
+      this.mIsToDoItem = null;
+      this.mLastAlarmSelection = 0;
+
+      this.mAlarmsMenu.addEventListener("command", () => {
+        this.updateReminder();
+      });
+
+      this.querySelector(".reminder-multiple-alarms-label").addEventListener("click", () => {
+        this.updateReminder();
+      });
+
+      this.querySelector(".reminder-single-alarms-label").addEventListener("click", () => {
+        this.updateReminder();
+      });
+
+      this.querySelector(".item-organizer-label").addEventListener("click", () => {
+        sendMailToOrganizer(this.mItem);
+      });
+
+      let urlLink = this.querySelector(".url-link");
+      urlLink.addEventListener("click", event => {
+        launchBrowser(urlLink.getAttribute("href"), event);
+      });
+      urlLink.addEventListener("command", event => {
+        launchBrowser(urlLink.getAttribute("href"), event);
+      });
+    }
+
+    set item(item) {
+      this.mItem = item;
+      this.mCalendar = cal.wrapInstance(item.calendar, Ci.calISchedulingSupport);
+      this.mIsToDoItem = cal.item.isToDo(item);
+
+      this.mReadOnly = !(
+        cal.acl.isCalendarWritable(this.mCalendar) &&
+        (cal.acl.userCanModifyItem(item) ||
+          (this.mCalendar &&
+            this.mCalendar.isInvitation(item) &&
+            cal.acl.userCanRespondToInvitation(item)))
+      );
+
+      return item;
+    }
+
+    get item() {
+      return this.mItem;
+    }
+
+    get calendar() {
+      return this.mCalendar;
+    }
+
+    get readOnly() {
+      return this.mReadOnly;
+    }
+
+    /**
+     * Update the item details in the UI. To be called when this element is
+     * first rendered and when the item changes.
+     */
+    updateItemDetails() {
+      if (!this.item) {
+        // Setup not complete, do nothing for now.
+        return;
+      }
+      let item = this.item;
+      let isToDoItem = this.mIsToDoItem;
+
+      this.querySelector(".item-title").value = item.title;
+      this.querySelector(".item-calendar").value = this.calendar.name;
+
+      // Show start date.
+      let itemStartDate = item[cal.dtz.startDateProp(item)];
+
+      let itemStartRowLabel = this.querySelector(".item-start-row-label");
+      let itemDateRowStartDate = this.querySelector(".item-date-row-start-date");
+
+      itemStartRowLabel.style.visibility = itemStartDate ? "visible" : "collapse";
+      itemDateRowStartDate.style.visibility = itemStartDate ? "visible" : "collapse";
+
+      if (itemStartDate) {
+        let itemStartLabelValue = itemStartRowLabel.getAttribute(
+          isToDoItem ? "taskStartLabel" : "eventStartLabel"
+        );
+        itemStartRowLabel.setAttribute("value", itemStartLabelValue);
+        itemDateRowStartDate.value = cal.dtz.getStringForDateTime(itemStartDate);
+      }
+
+      // Show due date / end date.
+      let itemDueDate = item[cal.dtz.endDateProp(item)];
+
+      let itemDueRowLabel = this.querySelector(".item-due-row-label");
+      let itemDateRowEndDate = this.querySelector(".item-date-row-end-date");
+
+      itemDueRowLabel.style.visibility = itemDueDate ? "visible" : "collapse";
+      itemDateRowEndDate.style.visibility = itemDueDate ? "visible" : "collapse";
+
+      if (itemDueDate) {
+        let itemDueLabelValue = itemDueRowLabel.getAttribute(
+          isToDoItem ? "taskDueLabel" : "eventEndLabel"
+        );
+        itemDueRowLabel.setAttribute("value", itemDueLabelValue);
+        itemDateRowEndDate.value = cal.dtz.getStringForDateTime(itemDueDate);
+      }
+
+      // Show reminder if this item is *not* readonly.
+      // This case happens for example if this is an invitation.
+      if (!this.readOnly) {
+        let argCalendar = item.calendar;
+
+        let supportsReminders =
+          argCalendar.getProperty("capabilities.alarms.oninvitations.supported") !== false;
+
+        if (supportsReminders) {
+          this.querySelector(".reminder-row").removeAttribute("hidden");
+          this.mLastAlarmSelection = loadReminders(
+            item.getAlarms(),
+            this.mAlarmsMenu,
+            this.mItem.calendar
+          );
+          this.updateReminder();
+        }
+      }
+
+      this.updateRecurrenceDetails(getRecurrenceString(item));
+      this.updateAttendees(item);
+
+      updateLink(
+        item.getProperty("URL") || "",
+        this.querySelector(".event-grid-link-row"),
+        this.querySelector(".url-link")
+      );
+
+      let location = item.getProperty("LOCATION");
+      if (location) {
+        this.updateLocation(location);
+      }
+
+      let categories = item.getCategories();
+      if (categories.length > 0) {
+        this.querySelector(".category-row").removeAttribute("hidden");
+        // TODO: this join is unfriendly for l10n (categories.join(", ")).
+        this.querySelector(".item-category").value = categories.join(", ");
+      }
+
+      if (item.organizer && item.organizer.id) {
+        this.updateOrganizer(item.organizer);
+      }
+
+      let status = item.getProperty("STATUS");
+      if (status && status.length) {
+        this.updateStatus(status, isToDoItem);
+      }
+
+      if (item.hasProperty("DESCRIPTION")) {
+        let description = item.getProperty("DESCRIPTION");
+        if (description && description.length) {
+          this.querySelector(".item-description-box").removeAttribute("hidden");
+          let textbox = this.querySelector(".item-description");
+          textbox.value = description;
+          textbox.readOnly = true;
+        }
+      }
+
+      let attachments = item.getAttachments();
+      if (attachments.length) {
+        this.updateAttachments(attachments);
+      }
+    }
+
+    /**
+     * Updates the reminder, called when a reminder has been selected in the
+     * menulist.
+     */
+    updateReminder() {
+      this.mLastAlarmSelection = commonUpdateReminder(
+        this.mAlarmsMenu,
+        this.mItem,
+        this.mLastAlarmSelection,
+        this.mItem.calendar,
+        this.querySelector(".reminder-details"),
+        null,
+        false
+      );
+    }
+    /**
+     * Updates the dialog w.r.t recurrence, i.e shows a text describing the item's
+     * recurrence.
+     *
+     * @param {string} details - Recurrence details as a string.
+     */
+    updateRecurrenceDetails(details) {
+      let repeatRow = document.querySelector(".repeat-row");
+      let repeatDetails = document.querySelector(".repeat-details");
+
+      if (!details) {
+        repeatRow.setAttribute("hidden", "true");
+        repeatDetails.setAttribute("collapsed", "true");
+
+        while (repeatDetails.children.length) {
+          repeatDetails.lastChild.remove();
+        }
+        return;
+      }
+
+      repeatRow.removeAttribute("hidden");
+      repeatDetails.removeAttribute("collapsed");
+
+      let lines = details.split("\n");
+
+      while (repeatDetails.children.length > lines.length) {
+        repeatDetails.lastChild.remove();
+      }
+      while (repeatDetails.children.length < lines.length) {
+        repeatDetails.appendChild(repeatDetails.firstElementChild.cloneNode(true));
+      }
+      for (let i = 0; i < lines.length; i++) {
+        repeatDetails.children[i].value = lines[i];
+        repeatDetails.children[i].setAttribute("tooltiptext", details);
+      }
+    }
+
+    /**
+     * Updates the attendee listbox, displaying all attendees invited to the item.
+     */
+    updateAttendees(item) {
+      let attendees = item.getAttendees();
+      if (attendees && attendees.length) {
+        this.querySelector(".item-attendees").removeAttribute("hidden");
+
+        let { attendeesInRow, maxLabelWidth } = setupAttendees(
+          attendees,
+          this.querySelector(".item-summary-box"),
+          this.mAttendeesInRow,
+          this.mMaxLabelWidth
+        );
+
+        this.mAttendeesInRow = attendeesInRow;
+        this.mMaxLabelWidth = maxLabelWidth;
+      }
+    }
+
+    /**
+     * Updates the location, creating a link if the value is a URL.
+     *
+     * @param {string} location - The value of the location property.
+     */
+    updateLocation(location) {
+      this.querySelector(".location-row").removeAttribute("hidden");
+      let urlMatch = location.match(/(https?:\/\/[^ ]*)/);
+      let url = urlMatch && urlMatch[1];
+      let itemLocation = this.querySelector(".item-location");
+      if (url) {
+        let locationLabel = document.createXULElement("label");
+        locationLabel.setAttribute("class", "item-location-link text-link");
+        locationLabel.setAttribute("context", "location-link-context-menu");
+        locationLabel.setAttribute("value", url);
+        locationLabel.setAttribute("tooltiptext", url);
+        locationLabel.setAttribute("onclick", "launchBrowser(this.getAttribute('value'), event)");
+        locationLabel.setAttribute("oncommand", "launchBrowser(this.getAttribute('value'), event)");
+        itemLocation.replaceWith(locationLabel);
+      } else {
+        itemLocation.value = location;
+      }
+    }
+
+    /**
+     * Handle window resize event. Rearrange attendees.
+     */
+    onWindowResize() {
+      let { attendeesInRow, maxLabelWidth } = rearrangeAttendees(
+        this.mItem.getAttendees(),
+        this.querySelector(".item-summary-box"),
+        this.mAttendeesInRow,
+        this.mMaxLabelWidth
+      );
+      this.mAttendeesInRow = attendeesInRow;
+      this.mMaxLabelWidth = maxLabelWidth;
+    }
+
+    /**
+     * Update the organizer part of the UI.
+     *
+     * @param {calIAttendee} organizer - The organizer of the calendar item.
+     */
+    updateOrganizer(organizer) {
+      this.querySelector(".organizer-row").removeAttribute("hidden");
+      let cell = this.querySelector(".item-organizer-cell");
+      let text = cell.querySelector("label");
+      let icon = cell.querySelector("img");
+
+      let role = organizer.role || "REQ-PARTICIPANT";
+      let userType = organizer.userType || "INDIVIDUAL";
+      let partstat = organizer.participationStatus || "NEEDS-ACTION";
+      let orgName =
+        organizer.commonName && organizer.commonName.length
+          ? organizer.commonName
+          : organizer.toString();
+      let userTypeString = cal.l10n.getCalString("dialog.tooltip.attendeeUserType2." + userType, [
+        organizer.toString(),
+      ]);
+      let roleString = cal.l10n.getCalString("dialog.tooltip.attendeeRole2." + role, [
+        userTypeString,
+      ]);
+      let partstatString = cal.l10n.getCalString("dialog.tooltip.attendeePartStat2." + partstat, [
+        orgName,
+      ]);
+      let tooltip = cal.l10n.getCalString("dialog.tooltip.attendee.combined", [
+        roleString,
+        partstatString,
+      ]);
+
+      text.setAttribute("value", orgName);
+      cell.setAttribute("tooltiptext", tooltip);
+      icon.setAttribute("partstat", partstat);
+      icon.setAttribute("usertype", userType);
+      icon.setAttribute("role", role);
+    }
+
+    /**
+     * Update the status part of the UI.
+     *
+     * @param {string} status - The status of the calendar item.
+     * @param {boolean} isToDoItem - True if the calendar item is a todo, false if an event.
+     */
+    updateStatus(status, isToDoItem) {
+      let statusRow = this.querySelector(".status-row");
+      let statusRowData = this.querySelector(".status-row-td");
+
+      for (let i = 0; i < statusRowData.children.length; i++) {
+        if (statusRowData.children[i].getAttribute("status") == status) {
+          statusRow.removeAttribute("hidden");
+
+          if (status == "CANCELLED" && isToDoItem) {
+            // There are two labels for CANCELLED, the second one is for
+            // todo items. Increment the counter here.
+            i++;
+          }
+          statusRowData.children[i].removeAttribute("hidden");
+          break;
+        }
+      }
+    }
+
+    /**
+     * Update the attachments part of the UI.
+     *
+     * @param {calIAttachment[]} attachments - Array of attachment objects.
+     */
+    updateAttachments(attachments) {
+      // We only want to display URI type attachments and no ones received inline with the
+      // invitation message (having a CID: prefix results in about:blank) here.
+      let attCounter = 0;
+      attachments.forEach(aAttachment => {
+        if (aAttachment.uri && aAttachment.uri.spec != "about:blank") {
+          let attachment = this.querySelector(".attachment-template").cloneNode(true);
+          attachment.removeAttribute("id");
+          attachment.removeAttribute("hidden");
+
+          let label = attachment.querySelector("label");
+          label.setAttribute("value", aAttachment.uri.spec);
+
+          label.addEventListener("click", () => {
+            openAttachmentFromItemSummary(aAttachment.hashId, this.mItem);
+          });
+
+          let icon = attachment.querySelector("image");
+          let iconSrc = aAttachment.uri.spec.length ? aAttachment.uri.spec : "dummy.html";
+          if (aAttachment.uri && !aAttachment.uri.schemeIs("file")) {
+            // Using an uri directly, with e.g. a http scheme, wouldn't render any icon.
+            if (aAttachment.formatType) {
+              iconSrc = "goat?contentType=" + aAttachment.formatType;
+            } else {
+              // Let's try to auto-detect.
+              let parts = iconSrc.substr(aAttachment.uri.scheme.length + 2).split("/");
+              if (parts.length) {
+                iconSrc = parts[parts.length - 1];
+              }
+            }
+          }
+          icon.setAttribute("src", "moz-icon://" + iconSrc);
+
+          this.querySelector(".item-attachment-cell").appendChild(attachment);
+          attCounter++;
+        }
+      });
+
+      if (attCounter > 0) {
+        this.querySelector(".attachments-row").removeAttribute("hidden");
+      }
+    }
+  }
+
+  customElements.define("calendar-item-summary", CalendarItemSummary);
+}
--- a/calendar/base/jar.mn
+++ b/calendar/base/jar.mn
@@ -89,14 +89,15 @@ calendar.jar:
     content/preferences/categories.js                           (content/preferences/categories.js)
     content/preferences/editCategory.js                         (content/preferences/editCategory.js)
     content/preferences/editCategory.xhtml                      (content/preferences/editCategory.xhtml)
     content/preferences/general.js                              (content/preferences/general.js)
     content/preferences/views.js                                (content/preferences/views.js)
     content/today-pane.js                                       (content/today-pane.js)
     content/widgets/calendar-alarm-widget.js                    (content/widgets/calendar-alarm-widget.js)
     content/widgets/calendar-dnd-widgets.js                     (content/widgets/calendar-dnd-widgets.js)
+    content/widgets/calendar-item-summary.js                    (content/widgets/calendar-item-summary.js)
     content/widgets/calendar-minimonth.js                       (content/widgets/calendar-minimonth.js)
     content/widgets/calendar-modebox.js                         (content/widgets/calendar-modebox.js)
     content/calendar-subscriptions-list.js                      (content/widgets/calendar-subscriptions-list.js)
     content/calApplicationUtils.js                              (src/calApplicationUtils.js)
     content/calFilter.js                                        (src/calFilter.js)
     content/WindowsNTToZoneInfoTZId.properties                  (src/WindowsNTToZoneInfoTZId.properties)
--- a/calendar/base/themes/common/calendar-attendees.css
+++ b/calendar/base/themes/common/calendar-attendees.css
@@ -11,26 +11,26 @@ html|input.textbox-addressingWidget {
     background-color: transparent !important;
     flex: 1;
 }
 html|input.textbox-addressingWidget:disabled {
     color: inherit;
     opacity: 0.5;
 }
 
-#item-attendees-box {
+.item-attendees-box {
     -moz-appearance: listbox;
     margin: 2px 4px 0;
     overflow-y: auto;
     min-height: 54px; /*at least two rows - otherwise a scrollbar (if required) wouldn't appear*/
 }
 
-#calendar-summary-dialog #item-attendees,
-#calendar-event-summary-dialog #item-attendees,
-#calendar-task-summary-dialog #item-attendees {
+#calendar-summary-dialog .item-attendees,
+#calendar-event-summary-dialog .item-attendees,
+#calendar-task-summary-dialog .item-attendees {
     max-height: 135px; /* displays up to four rows of attendees*/
 }
 
 .item-attendees-cell {
     padding: 2px;
 }
 
 #calendar-event-dialog-inner .item-attendees-cell {
--- a/calendar/base/themes/common/dialogs/calendar-event-dialog.css
+++ b/calendar/base/themes/common/dialogs/calendar-event-dialog.css
@@ -1,15 +1,14 @@
 /* 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/. */
 
 @namespace html url("http://www.w3.org/1999/xhtml");
 
-
 window > dialog {
   --eventBorderColor: #8d8e90;
   --eventWidgetBorderColor: #aaaabc;
   --eventGridStartBorderColor: #bdbec0;
 }
 
 window[systemcolors] > dialog {
   --eventBorderColor: ThreeDShadow;
@@ -121,19 +120,20 @@ window[systemcolors] > dialog {
 }
 
 #yearly-period-of-label,
 label.label {
   text-align: right;
 }
 
 #item-calendar,
+.item-calendar,
 #item-categories,
 #item-repeat,
-#item-alarm,
+.item-alarm,
 .datepicker-text-class {
   min-width: 12em;
 }
 
 .cal-event-toolbarbutton .toolbarbutton-icon {
   width: 18px;
   height: 18px;
 }
@@ -173,16 +173,17 @@ label.label {
   height: 100%;
 }
 
 .event-input-td > input {
   flex: 1;
 }
 
 #item-calendar,
+.item-calendar,
 #item-categories {
   flex: 1;
 }
 
 #event-grid-item-calendar-td,
 #event-grid-category-color-td,
 .event-input-td {
   display: flex;
@@ -194,28 +195,30 @@ label.label {
 #event-grid th,
 #event-grid deck {
   margin: 0;
   padding: 0;
   margin-inline-end: 0;
   margin-inline-start: 0;
 }
 
-#item-location-link,
+.item-location-link,
 #item-location,
-#item-title {
+.item-location,
+#item-title,
+.item-title {
   margin: 0;
   margin-inline-end: 0;
   margin-inline-start: 0;
   padding-inline-start: 4px;
 }
 
 #todo-status,
 #item-repeat,
-#item-alarm {
+.item-alarm {
   margin: 0;
 }
 
 #event-grid td,
 #event-grid th {
   padding: 4px 0px;
 }
 
@@ -226,32 +229,33 @@ label.label {
   padding: 0;
 }
 
 #item-categories-textbox {
   margin: 0px 8px;
 }
 
 #item-calendar,
+.item-calendar,
 #item-categories {
   width: 100%;
 }
 
 .datepicker-menulist {
   margin-left: 0 !important;
 }
 
 #event-grid-tab-vbox {
   padding-bottom: 10px;
   padding-inline-start: 8px;
   padding-inline-end: 10px;
 }
 
 .above-separator {
-  border-bottom: 1px solid #A3A3A3;
+  border-bottom: 1px solid #a3a3a3;
 }
 
 #completed-date-picker {
   margin-inline-start: 4px;
 }
 
 /*--------------------------------------------------------------------
  *   Event dialog tabbox section
@@ -271,17 +275,17 @@ label.label {
 
 #event-grid-tabpanel-description > #item-description {
   width: 100%;
   box-sizing: border-box;
   min-height: 7em;
   margin: 2px 4px;
 }
 
-#event-grid-tabpanel-attendees > vbox > hbox > #item-attendees-box {
+#event-grid-tabpanel-attendees > vbox > hbox > .item-attendees-box {
   margin: 2px 4px;
 }
 
 /*--------------------------------------------------------------------
  *   Event dialog keep duration button
  *-------------------------------------------------------------------*/
 
 #keepduration-button {
@@ -396,17 +400,18 @@ label.label {
 }
 
 #freebusy-grid {
   min-width: 1px;
   -moz-user-focus: normal;
   overflow: hidden;
 }
 
-#freebusy-grid > .dummy-row, #freebusy-grid .dummy-row-cell {
+#freebusy-grid > .dummy-row,
+#freebusy-grid .dummy-row-cell {
   width: 100%;
 }
 
 #calendar-summary-dialog {
   min-width: 35em;
 }
 
 #daypicker-weekday {
@@ -416,18 +421,18 @@ label.label {
 .daypicker-monthday {
   margin-top: 2px;
 }
 
 .headline {
   font-weight: bold;
 }
 
-.headline[align=end],
-.headline[align=right]{
+.headline[align="end"],
+.headline[align="right"] {
   text-align: right;
 }
 
 .default-spacer {
   width: 1em;
   height: 1em;
 }
 
@@ -468,29 +473,29 @@ label.label {
 }
 
 #freebusy-grid > .listbox-noborder {
   border-top-color: transparent;
 }
 
 /* remove on Windows the double border with the splitter */
 @media (-moz-os-version: windows-win7),
-       (-moz-os-version: windows-win8),
-       (-moz-os-version: windows-win10) {
+  (-moz-os-version: windows-win8),
+  (-moz-os-version: windows-win10) {
   #attendees-list > .listbox-noborder {
     border-inline-end-style: none;
   }
   #timebar > .listbox-noborder,
   #freebusy-grid > .listbox-noborder {
     border-inline-start-style: none;
   }
 }
 
 .selection-bar {
-  background-color: rgba(0, 128, 128, .2);
+  background-color: rgba(0, 128, 128, 0.2);
   border: 1px solid #008080;
   border-top-left-radius: 5px;
   border-top-right-radius: 5px;
 }
 
 #freebusy-container {
   overflow: hidden;
   clip: rect(0px 0px 0px 0px);
@@ -560,39 +565,39 @@ calendar-event-freebusy-day > box {
      text-decoration: underline;
   */
 }
 
 .freebusy-grid {
   margin: 0;
   border-inline-start: 1px solid var(--eventGridStartBorderColor);
   padding-inline: 2px;
-  background-color: #E09EBD;
-  color: #E09EBD;
+  background-color: #e09ebd;
+  color: #e09ebd;
   min-height: 16px;
 }
 
 .freebusy-grid[state="busy"] {
-  background-color: #153E7E;
-  color: #153E7E;
+  background-color: #153e7e;
+  color: #153e7e;
 }
 
 .freebusy-grid[state="busy_tentative"] {
-  background-color: #1589FF;
-  color: #1589FF;
+  background-color: #1589ff;
+  color: #1589ff;
 }
 
 .freebusy-grid[state="busy_unavailable"] {
-  background-color: #4E387E;
-  color: #4E387E;
+  background-color: #4e387e;
+  color: #4e387e;
 }
 
 .freebusy-grid[state="free"] {
-  background-color: #EBEBE4;
-  color: #EBEBE4;
+  background-color: #ebebe4;
+  color: #ebebe4;
 }
 
 .freebusy-grid:first-child {
   border-inline-start: 1px solid transparent;
 }
 
 .freebusy-grid.last-in-day {
   border-inline-end: 1px solid var(--eventBorderColor);
@@ -702,45 +707,45 @@ calendar-event-freebusy-day > box {
 .right-icon .button-icon {
   margin-inline-start: 3px;
 }
 
 .legend {
   display: flex;
   width: 3em;
   height: 1em;
-  border-top: 1px solid #A1A1A1;
-  border-right: 1px solid #C3C3C3;
-  border-bottom: 1px solid #DDDDDD;
-  border-left: 1px solid #C3C3C3;
+  border-top: 1px solid #a1a1a1;
+  border-right: 1px solid #c3c3c3;
+  border-bottom: 1px solid #dddddd;
+  border-left: 1px solid #c3c3c3;
 }
 
 .legend[status="FREE"] {
-  background-color: #EBEBE4;
-  color: #EBEBE4;
+  background-color: #ebebe4;
+  color: #ebebe4;
 }
 
 .legend[status="BUSY"] {
-  background-color: #153E7E;
-  color: #153E7E;
+  background-color: #153e7e;
+  color: #153e7e;
 }
 
 .legend[status="BUSY_TENTATIVE"] {
-  background-color: #1589FF;
-  color: #1589FF;
+  background-color: #1589ff;
+  color: #1589ff;
 }
 
 .legend[status="BUSY_UNAVAILABLE"] {
-  background-color: #4E387E;
-  color: #4E387E;
+  background-color: #4e387e;
+  color: #4e387e;
 }
 
 .legend[status="UNKNOWN"] {
-  background-color: #E09EBD;
-  color: #E09EBD;
+  background-color: #e09ebd;
+  color: #e09ebd;
 }
 
 #content-frame {
   border-left: 1px solid ThreeDDarkShadow;
   border-right: 1px solid ThreeDLightShadow;
   min-width: 10px;
   min-height: 10px;
   height: 400px;
@@ -800,37 +805,37 @@ calendar-event-freebusy-day > box {
 }
 
 #calendar-summary-dialog,
 #calendar-event-summary-dialog,
 #calendar-task-summary-dialog {
   min-width: 35em;
 }
 
-#calendar-summary-dialog #item-attachment-cell,
-#calendar-event-summary-dialog #item-attachment-cell,
-#calendar-task-summary-dialog #item-attachment-cell {
+#calendar-summary-dialog .item-attachment-cell,
+#calendar-event-summary-dialog .item-attachment-cell,
+#calendar-task-summary-dialog .item-attachment-cell {
   margin-left: 6px;
 }
 
 #calendar-summary-dialog .item-attachment-cell-label,
 #calendar-event-summary-dialog .item-attachment-cell-label,
 #calendar-task-summary-dialog .item-attachment-cell-label {
   margin-left: 3px;
 }
 
-#calendar-summary-dialog #item-description-wrapper,
-#calendar-event-summary-dialog #item-description-wrapper,
-#calendar-task-summary-dialog #item-description-wrapper {
+#calendar-summary-dialog .item-description-wrapper,
+#calendar-event-summary-dialog .item-description-wrapper,
+#calendar-task-summary-dialog .item-description-wrapper {
   display: flex;
 }
 
-#calendar-summary-dialog #item-description,
-#calendar-event-summary-dialog #item-description,
-#calendar-task-summary-dialog #item-description {
+#calendar-summary-dialog .item-description,
+#calendar-event-summary-dialog .item-description,
+#calendar-task-summary-dialog .item-description {
   width: 100%;
   box-sizing: border-box;
   min-height: 54px;
   margin: 2px 4px 0;
 }
 
 #calendar-summary-dialog .selectable-label,
 #calendar-event-summary-dialog .selectable-label,
--- a/calendar/lightning/content/lightning-item-iframe.js
+++ b/calendar/lightning/content/lightning-item-iframe.js
@@ -55,16 +55,17 @@ var gUntilDate = null;
 var gIsReadOnly = false;
 var gAttachMap = {};
 var gConfirmCancel = true;
 var gLastRepeatSelection = 0;
 var gIgnoreUpdate = false;
 var gWarning = false;
 var gPreviousCalendarId = null;
 var gTabInfoObject;
+var gLastAlarmSelection = 0;
 var gConfig = {
   priority: 0,
   privacy: null,
   status: "NONE",
   showTimeAs: null,
   percentComplete: 0,
 };
 // The following variables are set by the load handler function of the
@@ -260,17 +261,17 @@ function receiveMessage(aEvent) {
       setElementValue(textbox, aEvent.data.value);
       updateToDoStatus("percent-changed");
       break;
     }
     case "postponeTask":
       postponeTask(aEvent.data.value);
       break;
     case "toggleTimezoneLinks":
-            gTimezonesEnabled = aEvent.data.checked; // eslint-disable-line
+      gTimezonesEnabled = aEvent.data.checked; // eslint-disable-line
       updateDateTime();
       /*
             // Not implemented in react-code.js yet
             if (gNewItemUI) {
                 gTopComponent.importState({ timezonesEnabled: aEvent.data.checked });
             }
             */
       break;
@@ -595,16 +596,25 @@ function cancelItem() {
   if (gInTab) {
     onCancel();
   } else {
     sendMessage({ command: "cancelDialog" });
   }
 }
 
 /**
+ * Get the currently selected calendar from the menulist of calendars.
+ *
+ * @return      The currently selected calendar.
+ */
+function getCurrentCalendar() {
+  return document.getElementById("item-calendar").selectedItem.calendar;
+}
+
+/**
  * Sets up all dialog controls from the information of the passed item.
  *
  * @param aItem      The item to parse information out of.
  */
 function loadDialog(aItem) {
   loadDateTime(aItem);
 
   let itemProps;
@@ -781,17 +791,18 @@ function loadDialog(aItem) {
     itemProps.initialRepeatUntilDate = untilDate;
     // XXX more to do, see loadRepeat
   } else {
     loadRepeat(repeatType, untilDate, aItem);
   }
 
   if (!gNewItemUI) {
     // load reminders details
-    loadReminders(aItem.getAlarms());
+    let alarmsMenu = document.querySelector(".item-alarm");
+    window.gLastAlarmSelection = loadReminders(aItem.getAlarms(), alarmsMenu, getCurrentCalendar());
 
     // Synchronize link-top-image with keep-duration-button status
     let keepAttribute =
       document.getElementById("keepduration-button").getAttribute("keep") == "true";
     setBooleanAttribute("link-image-top", "keep", keepAttribute);
 
     updateDateTime();
 
@@ -1576,17 +1587,25 @@ function loadRepeat(aRepeatType, aUntilD
 
 /**
  * Update reminder related elements on the dialog.
  *
  * @param aSuppressDialogs     If true, controls are updated without prompting
  *                               for changes with the custom dialog
  */
 function updateReminder(aSuppressDialogs) {
-  commonUpdateReminder(aSuppressDialogs);
+  window.gLastAlarmSelection = commonUpdateReminder(
+    document.querySelector(".item-alarm"),
+    window.calendarItem,
+    window.gLastAlarmSelection,
+    getCurrentCalendar(),
+    document.querySelector(".reminder-details"),
+    window.gStartTimezone || window.gEndTimezone,
+    aSuppressDialogs
+  );
   updateAccept();
 }
 
 /**
  * Saves all values the user chose on the dialog to the passed item
  *
  * @param item    The item to save to.
  */
@@ -1666,17 +1685,17 @@ function saveDialog(item) {
   // Privacy
   cal.item.setItemProperty(item, "CLASS", gConfig.privacy, "privacy");
 
   if (item.status == "COMPLETED" && cal.item.isToDo(item)) {
     let elementValue = getElementValue("completed-date-picker");
     item.completedDate = cal.dtz.jsDateToDateTime(elementValue);
   }
 
-  saveReminder(item);
+  saveReminder(item, getCurrentCalendar(), document.querySelector(".item-alarm"));
 }
 
 /**
  * Save date and time related values from the dialog to the passed item.
  *
  * @param item    The item to save to.
  */
 function saveDateTime(item) {
@@ -3834,17 +3853,26 @@ function updateAttendees() {
       text.setAttribute("value", orgName);
       cell.setAttribute("tooltiptext", tooltip);
       icon.setAttribute("partstat", partStat);
       icon.setAttribute("usertype", userType);
       icon.setAttribute("role", role);
     } else {
       setBooleanAttribute("item-organizer-row", "collapsed", true);
     }
-    setupAttendees();
+
+    let { attendeesInRow, maxLabelWidth } = setupAttendees(
+      window.attendees,
+      document,
+      window.attendeesInRow,
+      window.maxLabelWidth
+    );
+
+    window.attendeesInRow = attendeesInRow;
+    window.maxLabelWidth = maxLabelWidth;
 
     // update the attendee tab label to make the number of attendees
     // visible even if another tab is displayed
     if (window.attendees.length) {
       attendeeTab.label = cal.l10n.getString("calendar-event-dialog", "attendeesTabLabel", [
         window.attendees.length,
       ]);
     } else {
@@ -4062,17 +4090,21 @@ function sendMailToAttendees(aAttendees)
  * Make sure all fields that may have calendar specific capabilities are updated
  */
 function updateCapabilities() {
   updateAttachment();
   updateConfigState({
     priority: gConfig.priority,
     privacy: gConfig.privacy,
   });
-  updateReminderDetails();
+  updateReminderDetails(
+    document.querySelector(".reminder-details"),
+    document.querySelector(".item-alarm"),
+    getCurrentCalendar()
+  );
   updateCategoryMenulist();
 }
 
 /**
  * find out if the User already changed values in the Dialog
  *
  * @return:    true if the values in the Dialog have changed. False otherwise.
  */
--- a/calendar/lightning/content/lightning-item-iframe.xhtml
+++ b/calendar/lightning/content/lightning-item-iframe.xhtml
@@ -469,16 +469,17 @@
                accesskey="&event.reminder.accesskey;"
                control="item-alarm"
                disable-on-readonly="true"/>
       </html:th>
       <html:td class="above-separator">
         <hbox id="event-grid-alarm-picker-box"
               align="center">
           <menulist id="item-alarm"
+                    class="item-alarm"
                     disable-on-readonly="true"
                     oncommand="updateReminder()">
             <menupopup id="item-alarm-menupopup">
               <menuitem id="reminder-none-menuitem"
                         label="&event.reminder.none.label;"
                         selected="true"
                         value="none"/>
               <menuseparator id="reminder-none-separator"/>
@@ -540,36 +541,33 @@
                         unit="days"/>
               <menuitem id="reminder-1week-menuitem"
                         label="&event.reminder.1week.before.label;"
                         length="7"
                         origin="before"
                         relation="START"
                         unit="days"/>
               <menuseparator id="reminder-custom-separator"/>
-              <menuitem id="reminder-custom-menuitem"
+              <menuitem class="reminder-custom-menuitem"
                         label="&event.reminder.custom.label;"
                         value="custom"/>
             </menupopup>
           </menulist>
-          <hbox id="reminder-details">
-            <hbox id="reminder-icon-box"
-                  class="alarm-icons-box"
+          <hbox class="reminder-details">
+            <hbox class="reminder-icon-box alarm-icons-box"
                   align="center"/>
             <!-- TODO oncommand? onkeypress? -->
-            <label id="reminder-multiple-alarms-label"
-                   class="text-link"
+            <label class="reminder-multiple-alarms-label text-link"
                    hidden="true"
                    value="&event.reminder.multiple.label;"
                    disable-on-readonly="true"
                    flex="1"
                    hyperlink="true"
                    onclick="updateReminder()"/>
-            <label id="reminder-single-alarms-label"
-                   class="text-link"
+            <label class="reminder-single-alarms-label text-link"
                    hidden="true"
                    disable-on-readonly="true"
                    flex="1"
                    hyperlink="true"
                    onclick="updateReminder()"/>
           </hbox>
         </hbox>
       </html:td>
@@ -637,17 +635,17 @@
                 <hbox class="item-organizer-cell">
                   <img class="itip-icon"/>
                   <label id="item-organizer"
                          class="item-attendees-cell-label"
                          crop="right"/>
                 </hbox>
               </hbox>
               <hbox flex="1">
-                <vbox id="item-attendees-box"
+                <vbox class="item-attendees-box"
                       dialog-type="event"
                       flex="1"
                       context="attendee-popup"
                       oncontextmenu="setAttendeeContext(event)"
                       disable-on-readonly="true"/>
               </hbox>
             </vbox>
           </tabpanel>
@@ -740,17 +738,17 @@
       <menuitem id="timezone-custom-menuitem"
                 label="&event.timezone.custom.label;"
                 value="custom"
                 oncommand="this.parentNode.editTimezone()"/>
     </menupopup>
   </popupset>
 
   <!-- attendee box template -->
-  <vbox id="item-attendees-box-template"
+  <vbox class="item-attendees-box-template"
         hidden="true">
     <hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true">
       <box class="item-attendees-cell"
            hidden="true"
            flex="1"
            context="attendee-popup"
            ondblclick="attendeeDblClick(event)"
            oncontextmenu="setAttendeeContext(event)">
--- a/calendar/test/browser/preferences/browser_alarmDefaultValue.js
+++ b/calendar/test/browser/preferences/browser_alarmDefaultValue.js
@@ -49,18 +49,18 @@ add_task(async function testDefaultAlarm
   let eventDialogIframe = eventDialogDocument.getElementById("lightning-item-panel-iframe");
   let iframeWindow = eventDialogIframe.contentWindow;
   if (eventDialogIframe.contentDocument.readyState != "complete") {
     await BrowserTestUtils.waitForEvent(iframeWindow, "load");
   }
   let iframeDocument = iframeWindow.document;
   await new Promise(r => iframeWindow.setTimeout(r));
 
-  Assert.equal(iframeDocument.getElementById("item-alarm").value, "custom");
-  let reminderDetails = iframeDocument.getElementById("reminder-single-alarms-label");
+  Assert.equal(iframeDocument.querySelector(".item-alarm").value, "custom");
+  let reminderDetails = iframeDocument.querySelector(".reminder-single-alarms-label");
   Assert.equal(reminderDetails.value, expectedEventReminder);
 
   let reminderDialogPromise = BrowserTestUtils.promiseAlertDialog(
     null,
     "chrome://calendar/content/calendar-event-dialog-reminder.xhtml",
     handleReminderDialog
   );
   EventUtils.synthesizeMouseAtCenter(reminderDetails, {}, iframeWindow);
@@ -80,18 +80,18 @@ add_task(async function testDefaultAlarm
   let taskDialogIframe = taskDialogDocument.getElementById("lightning-item-panel-iframe");
   iframeWindow = taskDialogIframe.contentWindow;
   if (taskDialogIframe.contentDocument.readyState != "complete") {
     await BrowserTestUtils.waitForEvent(iframeWindow, "load");
   }
   iframeDocument = iframeWindow.document;
   await new Promise(r => iframeWindow.setTimeout(r));
 
-  Assert.equal(iframeDocument.getElementById("item-alarm").value, "custom");
-  reminderDetails = iframeDocument.getElementById("reminder-single-alarms-label");
+  Assert.equal(iframeDocument.querySelector(".item-alarm").value, "custom");
+  reminderDetails = iframeDocument.querySelector(".reminder-single-alarms-label");
   Assert.equal(reminderDetails.value, expectedTaskReminder);
 
   reminderDialogPromise = BrowserTestUtils.promiseAlertDialog(
     null,
     "chrome://calendar/content/calendar-event-dialog-reminder.xhtml",
     handleReminderDialog
   );
   EventUtils.synthesizeMouseAtCenter(reminderDetails, {}, iframeWindow);
--- a/calendar/test/modules/ItemEditingHelpers.jsm
+++ b/calendar/test/modules/ItemEditingHelpers.jsm
@@ -45,17 +45,17 @@ var EVENT_TABPANELS = `
     id("event-grid-tab-vbox")/id("event-grid-tab-box-row")/id("event-grid-tabbox")/
     id("event-grid-tabpanels")
 `;
 var DESCRIPTION_TEXTBOX = `
     ${EVENT_TABPANELS}/id("event-grid-tabpanel-description")/id("item-description")
 `;
 var ATTENDEES_ROW = `
     ${EVENT_TABPANELS}/id("event-grid-tabpanel-attendees")/{"flex":"1"}/
-    {"flex":"1"}/id("item-attendees-box")/{"class":"item-attendees-row"}
+    {"flex":"1"}/{"class":"item-attendees-box"}/{"class":"item-attendees-row"}
 `;
 // Only for Tasks.
 var PERCENT_COMPLETE_INPUT = `
     id("event-grid")/id("event-grid-todo-status-row")/id("event-grid-todo-status-td")/
     id("event-grid-todo-status-picker-box")/id("percent-complete-textbox")
 `;
 
 // To be appended to the path for a date- or timepicker.
@@ -430,17 +430,17 @@ async function setData(dialog, iframe, d
  * Select an item in the reminder menulist.
  * Custom reminders are not supported.
  *
  * @param iframeWindow    The event dialog iframe.
  * @param id              Identifying string of menuitem id.
  */
 async function setReminderMenulist(iframeWindow, id) {
   let iframeDocument = iframeWindow.document;
-  let menulist = iframeDocument.getElementById("item-alarm");
+  let menulist = iframeDocument.querySelector(".item-alarm");
   let menuitem = iframeDocument.getElementById(`reminder-${id}-menuitem`);
 
   synthesizeMouseAtCenter(menulist, {}, iframeWindow);
   await BrowserTestUtils.waitForEvent(menulist, "popupshown");
   synthesizeMouseAtCenter(menuitem, {}, iframeWindow);
   await BrowserTestUtils.waitForEvent(menulist, "popuphidden");
   await sleep(iframeWindow);
 }