summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-ui-utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/base/content/calendar-ui-utils.js')
-rw-r--r--comm/calendar/base/content/calendar-ui-utils.js596
1 files changed, 596 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-ui-utils.js b/comm/calendar/base/content/calendar-ui-utils.js
new file mode 100644
index 0000000000..11d92ab6da
--- /dev/null
+++ b/comm/calendar/base/content/calendar-ui-utils.js
@@ -0,0 +1,596 @@
+/* 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 disableElementWithLock,
+ * enableElementWithLock,
+ * appendCalendarItems, checkRadioControl,
+ * checkRadioControlAppmenu,
+ * updateUnitLabelPlural, updateMenuLabelsPlural,
+ * getOptimalMinimumWidth, getOptimalMinimumHeight,
+ * setupAttendanceMenu
+ */
+
+/* import-globals-from ../../../mail/base/content/globalOverlay.js */
+/* import-globals-from ../../../mail/base/content/utilityOverlay.js */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { PluralForm } = ChromeUtils.importESModule("resource://gre/modules/PluralForm.sys.mjs");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+/**
+ * This function unconditionally disables the element for
+ * which the id has been passed as argument. Furthermore, it
+ * remembers who was responsible for this action by using
+ * the given key (lockId). In case the control should be
+ * enabled again the lock gets removed, but the control only
+ * gets enabled if *all* possibly held locks have been removed.
+ *
+ * @param elementId The element ID of the element to disable.
+ * @param lockId The ID of the lock to set.
+ */
+function disableElementWithLock(elementId, lockId) {
+ // unconditionally disable the element.
+ document.getElementById(elementId).setAttribute("disabled", "true");
+
+ // remember that this element has been locked with
+ // the key passed as argument. we keep a primitive
+ // form of ref-count in the attribute 'lock'.
+ let element = document.getElementById(elementId);
+ if (element) {
+ if (!element.hasAttribute(lockId)) {
+ element.setAttribute(lockId, "true");
+ let n = parseInt(element.getAttribute("lock") || 0, 10);
+ element.setAttribute("lock", n + 1);
+ }
+ }
+}
+
+/**
+ * This function is intended to be used in tandem with the
+ * above defined function 'disableElementWithLock()'.
+ * See the respective comment for further details.
+ *
+ * @see disableElementWithLock
+ * @param elementId The element ID of the element to enable.
+ * @param lockId The ID of the lock to set.
+ */
+function enableElementWithLock(elementId, lockId) {
+ let element = document.getElementById(elementId);
+ if (!element) {
+ dump("unable to find " + elementId + "\n");
+ return;
+ }
+
+ if (element.hasAttribute(lockId)) {
+ element.removeAttribute(lockId);
+ let n = parseInt(element.getAttribute("lock") || 0, 10) - 1;
+ if (n > 0) {
+ element.setAttribute("lock", n);
+ } else {
+ element.removeAttribute("lock");
+ }
+ if (n <= 0) {
+ element.removeAttribute("disabled");
+ }
+ }
+}
+
+/**
+ * Sorts a sorted array of calendars by pref |calendar.list.sortOrder|.
+ * Repairs that pref if dangling entries exist.
+ *
+ * @param calendars An array of calendars to sort.
+ */
+function sortCalendarArray(calendars) {
+ let ret = calendars.concat([]);
+ let sortOrder = {};
+ let sortOrderPref = Services.prefs.getStringPref("calendar.list.sortOrder", "").split(" ");
+ for (let i = 0; i < sortOrderPref.length; ++i) {
+ sortOrder[sortOrderPref[i]] = i;
+ }
+ function sortFunc(cal1, cal2) {
+ let orderIdx1 = sortOrder[cal1.id] || -1;
+ let orderIdx2 = sortOrder[cal2.id] || -1;
+ if (orderIdx1 < orderIdx2) {
+ return -1;
+ }
+ if (orderIdx1 > orderIdx2) {
+ return 1;
+ }
+ return 0;
+ }
+ ret.sort(sortFunc);
+
+ // check and repair pref when an array of all calendars has been passed:
+ let sortOrderString = Services.prefs.getStringPref("calendar.list.sortOrder", "");
+ let wantedOrderString = ret.map(calendar => calendar.id).join(" ");
+ if (wantedOrderString != sortOrderString && cal.manager.getCalendars().length == ret.length) {
+ Services.prefs.setStringPref("calendar.list.sortOrder", wantedOrderString);
+ }
+
+ return ret;
+}
+
+/**
+ * Fills up a menu - either a menupopup or a menulist - with menuitems that refer
+ * to calendars.
+ *
+ * @param aItem The event or task
+ * @param aCalendarMenuParent The direct parent of the menuitems - either a
+ * menupopup or a menulist
+ * @param aCalendarToUse The default-calendar
+ * @param aOnCommand A string that is applied to the "oncommand"
+ * attribute of each menuitem
+ * @returns The index of the calendar that matches the
+ * default-calendar. By default 0 is returned.
+ */
+function appendCalendarItems(aItem, aCalendarMenuParent, aCalendarToUse, aOnCommand) {
+ let calendarToUse = aCalendarToUse || aItem.calendar;
+ let calendars = sortCalendarArray(cal.manager.getCalendars());
+ let indexToSelect = 0;
+ let index = -1;
+ for (let i = 0; i < calendars.length; ++i) {
+ let calendar = calendars[i];
+ if (
+ calendar.id == calendarToUse.id ||
+ (calendar &&
+ cal.acl.isCalendarWritable(calendar) &&
+ (cal.acl.userCanAddItemsToCalendar(calendar) ||
+ (calendar == aItem.calendar && cal.acl.userCanModifyItem(aItem))) &&
+ cal.item.isItemSupported(aItem, calendar))
+ ) {
+ let menuitem = addMenuItem(aCalendarMenuParent, calendar.name, calendar.name);
+ menuitem.calendar = calendar;
+ index++;
+ if (aOnCommand) {
+ menuitem.setAttribute("oncommand", aOnCommand);
+ }
+ if (aCalendarMenuParent.localName == "menupopup") {
+ menuitem.setAttribute("type", "checkbox");
+ }
+ if (calendarToUse && calendarToUse.id == calendar.id) {
+ indexToSelect = index;
+ }
+ let cssSafeId = cal.view.formatStringForCSSRule(calendar.id);
+ menuitem.style.setProperty("--item-color", `var(--calendar-${cssSafeId}-backcolor)`);
+ menuitem.classList.add("menuitem-iconic");
+ }
+ }
+ return indexToSelect;
+}
+
+/**
+ * Helper function to add a menuitem to a menulist or similar.
+ *
+ * @param aParent The XUL node to add the menuitem to.
+ * @param aLabel The label string of the menuitem.
+ * @param aValue The value attribute of the menuitem.
+ * @param aCommand The oncommand attribute of the menuitem.
+ * @returns The newly created menuitem
+ */
+function addMenuItem(aParent, aLabel, aValue, aCommand) {
+ let item = null;
+ if (aParent.localName == "menupopup") {
+ item = document.createXULElement("menuitem");
+ item.setAttribute("label", aLabel);
+ if (aValue) {
+ item.setAttribute("value", aValue);
+ }
+ if (aCommand) {
+ item.command = aCommand;
+ }
+ aParent.appendChild(item);
+ } else if (aParent.localName == "menulist") {
+ item = aParent.appendItem(aLabel, aValue);
+ }
+ return item;
+}
+
+/**
+ * Gets the correct plural form of a given unit.
+ *
+ * @param aLength The number to use to determine the plural form
+ * @param aUnit The unit to find the plural form of
+ * @param aIncludeLength (optional) If true, the length will be included in the
+ * result. If false, only the pluralized unit is returned.
+ * @returns A string containing the pluralized version of the unit
+ */
+function unitPluralForm(aLength, aUnit, aIncludeLength = true) {
+ let unitProp =
+ {
+ minutes: "unitMinutes",
+ hours: "unitHours",
+ days: "unitDays",
+ weeks: "unitWeeks",
+ }[aUnit] || "unitMinutes";
+
+ return PluralForm.get(aLength, cal.l10n.getCalString(unitProp))
+ .replace("#1", aIncludeLength ? aLength : "")
+ .trim();
+}
+
+/**
+ * Update the given unit label to show the correct plural form.
+ *
+ * @param aLengthFieldId The ID of the element containing the number
+ * @param aLabelId The ID of the label to update.
+ * @param aUnit The unit to use for the label.
+ */
+function updateUnitLabelPlural(aLengthFieldId, aLabelId, aUnit) {
+ let label = document.getElementById(aLabelId);
+ let length = Number(document.getElementById(aLengthFieldId).value);
+
+ label.value = unitPluralForm(length, aUnit, false);
+}
+
+/**
+ * Update the given menu to show the correct plural form in the list.
+ *
+ * @param aLengthFieldId The ID of the element containing the number
+ * @param aMenuId The menu to update labels in.
+ */
+function updateMenuLabelsPlural(aLengthFieldId, aMenuId) {
+ let menu = document.getElementById(aMenuId);
+ let length = Number(document.getElementById(aLengthFieldId).value);
+
+ // update the menu items
+ let items = menu.getElementsByTagName("menuitem");
+ for (let menuItem of items) {
+ menuItem.label = unitPluralForm(length, menuItem.value, false);
+ }
+
+ // force the menu selection to redraw
+ let saveSelectedIndex = menu.selectedIndex;
+ menu.selectedIndex = -1;
+ menu.selectedIndex = saveSelectedIndex;
+}
+
+/**
+ * A helper function to calculate and add up certain css-values of a box.
+ * It is required, that all css values can be converted to integers
+ * see also
+ * http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSview-getComputedStyle
+ *
+ * @param aXULElement The xul element to be inspected.
+ * @param aStyleProps The css style properties for which values are to be retrieved
+ * e.g. 'font-size', 'min-width" etc.
+ * @returns An integer value denoting the optimal minimum width
+ */
+function getSummarizedStyleValues(aXULElement, aStyleProps) {
+ let retValue = 0;
+ let cssStyleDeclares = document.defaultView.getComputedStyle(aXULElement);
+ for (let prop of aStyleProps) {
+ retValue += parseInt(cssStyleDeclares.getPropertyValue(prop), 10);
+ }
+ return retValue;
+}
+
+/**
+ * Calculates the optimal minimum width based on the set css style-rules
+ * by considering the css rules for the min-width, padding, border, margin
+ * and border of the box.
+ *
+ * @param aXULElement The xul element to be inspected.
+ * @returns An integer value denoting the optimal minimum width
+ */
+function getOptimalMinimumWidth(aXULElement) {
+ return getSummarizedStyleValues(aXULElement, [
+ "min-width",
+ "padding-left",
+ "padding-right",
+ "margin-left",
+ "margin-top",
+ "border-left-width",
+ "border-right-width",
+ ]);
+}
+
+/**
+ * Calculates the optimal minimum height based on the set css style-rules
+ * by considering the css rules for the font-size, padding, border, margin
+ * and border of the box. In its current state the line-height is considered
+ * by assuming that it's size is about one third of the size of the font-size
+ *
+ * @param aXULElement The xul-element to be inspected.
+ * @returns An integer value denoting the optimal minimum height
+ */
+function getOptimalMinimumHeight(aXULElement) {
+ // the following line of code presumes that the line-height is set to "normal"
+ // which is supposed to be a "reasonable distance" between the lines
+ let firstEntity = parseInt(1.35 * getSummarizedStyleValues(aXULElement, ["font-size"]), 10);
+ let secondEntity = getSummarizedStyleValues(aXULElement, [
+ "padding-bottom",
+ "padding-top",
+ "margin-bottom",
+ "margin-top",
+ "border-bottom-width",
+ "border-top-width",
+ ]);
+ return firstEntity + secondEntity;
+}
+
+/**
+ * Sets up the attendance context menu, based on the given items
+ *
+ * @param {Node} aMenu The context menu item containing the required
+ * menu or menuitem elements
+ * @param {Array} aItems - An array of the selected calEvent or calTodo
+ * items to display the context menu for
+ */
+function setupAttendanceMenu(aMenu, aItems) {
+ /**
+ * For menu items in scope, a check mark will be annotated corresponding to
+ * the partstat and removed for all others
+ *
+ * The user always selected single items or occurrences of series but never
+ * the master event of a series. That said, for the items in aItems, one of
+ * following scenarios applies:
+ *
+ * A. one none-recurring item which have attendees
+ * B. multiple none-recurring items which have attendees
+ * C. one occurrence of a series which has attendees
+ * D. multiple occurrences of the same series which have attendees
+ * E. multiple occurrences of different series which have attendees
+ * F. mixture of non-recurring and occurrences of one or more series which
+ * have attendees
+ * G. any mixture including a single item or an occurrence which doesn't
+ * have any attendees
+ *
+ * For scenarios A and B, the user will be prompted with a single set of
+ * available partstats and the according options to change it.
+ *
+ * For C, D and E the user was prompted with a set of partstats for both,
+ * the occurrence and the master. In case of E, no partstat information
+ * was annotated.
+ *
+ * For F, only a single set of available partstat options was prompted
+ * without annotating any partstat.
+ *
+ * For G, no context menu would be displayed, so we don't need to deal with
+ * that scenario here.
+ *
+ * Now the following matrix applies to take action of the users choice for
+ * the relevant participant (for columns, see explanation below):
+ * +---+------------------+-------------+--------+-----------------+
+ * | # | SELECTED | DISPLAYED | STATUS | MENU ACTION |
+ * | | CAL ITEMS | SUBMENU | PRESET | APPLIES ON |
+ * +---+------------------+-------------+--------+-----------------+
+ * | | | this-occ* | yes | selected item |
+ * | A | one +-------------+--------+-----------------+
+ * | | single item | all-occ | n/a |
+ * | | | | menu not displayed |
+ * +---+------------------+-------------+--------+-----------------+
+ * | | | this-occ* | no | selected items |
+ * | B | multiple +-------------+--------+-----------------+
+ * | | single items | all-occ | n/a |
+ * | | | | menu not displayed |
+ * +---+------------------+-------------+--------+-----------------+
+ * | | | this-occ | yes | sel. |
+ * | | one | | | occurrences |
+ * | C | occurrence +-------------+--------+-----------------+
+ * | | of a master | all-occ | yes | master of sel. |
+ * | | | | | occurrence |
+ * +---+------------------+-------------+--------+-----------------+
+ * | | | this-occ | no | sel. |
+ * | | multiple | | | occurrences |
+ * | D | occurrences +-------------+--------+-----------------+
+ * | | of one master | all-occ | yes | master of sel. |
+ * | | | | | occurrences |
+ * +---+------------------+-------------+--------+-----------------+
+ * | | | this-occ | no | sel. |
+ * | | multiple | | | occurrences |
+ * | E | occurrences of +-------------+--------+-----------------+
+ * | | multiple masters | all-occ | no | masters of sel. |
+ * | | | | | occurrences |
+ * +---+------------------+-------------+--------+-----------------+
+ * | | multiple single | this-occ* | no | selected items |
+ * | | and occurrences | | | and occurrences |
+ * | F | of multiple +-------------+--------+-----------------+
+ * | | masters | all-occ | n/a |
+ * | | | | menu not displayed |
+ * +---+------------------+-------------+--------------------------+
+ * | | any combination | |
+ * | G | including at | n/a |
+ * | | least one items | no attendance menu displayed |
+ * | | or occurrence | |
+ * | | w/o attendees | |
+ * +---+------------------+----------------------------------------+
+ *
+ * #: scenario as described above
+ * SELECTED CAL ITEMS: item types the user selected to prompt the context
+ * menu for
+ * DISPLAYED SUBMENU: the subbmenu displayed
+ * STATUS PRESET: whether or not a partstat is annotated to the menu
+ * items, if the respective submenu is displayed
+ * MENU ACTION APPLIES ON: the cal item, the respective partstat should be
+ * applied on, if the respective submenu is
+ * displayed
+ *
+ * this-occ* means that in this cases the submenu label is not displayed -
+ * additionally, if status is not preset the menu item for 'NEEDS-ACTIONS'
+ * will not be displayed, if the status is already different (consistent
+ * how we deal with that case at other places)
+ *
+ * @param {NodeList} aMenuItems A list of DOM nodes
+ * @param {string} aScope Either 'this-occurrence' or
+ * 'all-occurrences'
+ * @param {string} aPartStat A valid participation status
+ * as per RfC 5545
+ */
+ function checkMenuItem(aMenuItems, aScope, aPartStat) {
+ let toRemove = [];
+ let toAdd = [];
+ for (let item of aMenuItems) {
+ if (item.getAttribute("scope") == aScope && item.nodeName != "label") {
+ if (item.getAttribute("value") == aPartStat) {
+ switch (item.nodeName) {
+ case "menu": {
+ // Since menu elements cannot have checkmarks,
+ // we add a menuitem for this partstat and hide
+ // the menu element instead
+ let checkedId = "checked-" + item.getAttribute("id");
+ if (!document.getElementById(checkedId)) {
+ let checked = item.ownerDocument.createXULElement("menuitem");
+ checked.setAttribute("type", "checkbox");
+ checked.setAttribute("checked", "true");
+ checked.setAttribute("label", item.getAttribute("label"));
+ checked.setAttribute("value", item.getAttribute("value"));
+ checked.setAttribute("scope", item.getAttribute("scope"));
+ checked.setAttribute("id", checkedId);
+ item.setAttribute("hidden", "true");
+ toAdd.push([item, checked]);
+ }
+ break;
+ }
+ case "menuitem": {
+ item.removeAttribute("hidden");
+ item.setAttribute("checked", "true");
+ break;
+ }
+ }
+ } else if (item.nodeName == "menuitem") {
+ if (item.getAttribute("id").startsWith("checked-")) {
+ // we inserted a menuitem before for this partstat, so
+ // we revert that now
+ let menu = document.getElementById(item.getAttribute("id").substr(8));
+ menu.removeAttribute("hidden");
+ toRemove.push(item);
+ } else {
+ item.removeAttribute("checked");
+ }
+ } else if (item.nodeName == "menu") {
+ item.removeAttribute("hidden");
+ }
+ }
+ }
+ for (let [item, checked] of toAdd) {
+ item.before(checked);
+ }
+ for (let item of toRemove) {
+ item.remove();
+ }
+ }
+
+ /**
+ * Hides the items from the provided node list. If a partstat is provided,
+ * only the matching item will be hidden
+ *
+ * @param {NodeList} aMenuItems A list of DOM nodes
+ * @param {string} aPartStat [optional] A valid participation
+ * status as per RfC 5545
+ */
+ function hideItems(aNodeList, aPartStat = null) {
+ for (let item of aNodeList) {
+ if (aPartStat && aPartStat != item.getAttribute("value")) {
+ continue;
+ }
+ item.setAttribute("hidden", "true");
+ }
+ }
+
+ /**
+ * Provides the user's participation status for a provided item
+ *
+ * @param {calEvent|calTodo} aItem The calendar item to inspect
+ * @returns {?string} The participation status string
+ * as per RfC 5545 or null if no
+ * participant was detected
+ */
+ function getInvitationStatus(aItem) {
+ let party = null;
+ if (cal.itip.isInvitation(aItem)) {
+ party = cal.itip.getInvitedAttendee(aItem);
+ } else if (aItem.organizer && aItem.getAttendees().length) {
+ let calOrgId = aItem.calendar.getProperty("organizerId");
+ if (calOrgId && calOrgId.toLowerCase() == aItem.organizer.id.toLowerCase()) {
+ party = aItem.organizer;
+ }
+ }
+ return party && (party.participationStatus || "NEEDS-ACTION");
+ }
+
+ goUpdateCommand("calendar_attendance_command");
+
+ let singleMenuItems = aMenu.getElementsByAttribute("scope", "this-occurrence");
+ let seriesMenuItems = aMenu.getElementsByAttribute("scope", "all-occurrences");
+ let labels = aMenu.getElementsByAttribute("class", "calendar-context-heading-label");
+
+ if (aItems.length == 1) {
+ // we offer options for both single and recurring items. In case of the
+ // latter and the item is an occurrence, we offer status information and
+ // actions for both, the occurrence and the series
+ let thisPartStat = getInvitationStatus(aItems[0]);
+
+ if (aItems[0].recurrenceId) {
+ // we get the partstat - if this is null, no participant could
+ // be identified, so we bail out
+ let seriesPartStat = getInvitationStatus(aItems[0].parentItem);
+ if (seriesPartStat) {
+ // let's make sure we display the labels to distinguish series
+ // and occurrence
+ for (let label of labels) {
+ label.removeAttribute("hidden");
+ }
+
+ checkMenuItem(seriesMenuItems, "all-occurrences", seriesPartStat);
+
+ if (seriesPartStat != "NEEDS-ACTION") {
+ hideItems(seriesMenuItems, "NEEDS-ACTION");
+ }
+ // until we support actively delegating items, we also only
+ // display this status if it is already set
+ if (seriesPartStat != "DELEGATED") {
+ hideItems(seriesMenuItems, "DELEGATED");
+ }
+ } else {
+ hideItems(seriesMenuItems);
+ }
+ } else {
+ // here we don't need the all-occurrences scope, so let's hide all
+ // labels and related menu items
+ hideItems(labels);
+ hideItems(seriesMenuItems);
+ }
+
+ // also for the single occurrence we check whether there's a partstat
+ // available and bail out otherwise - we also make sure to not display
+ // the NEEDS-ACTION menu item if the current status is already different
+ if (thisPartStat) {
+ checkMenuItem(singleMenuItems, "this-occurrence", thisPartStat);
+ if (thisPartStat != "NEEDS-ACTION") {
+ hideItems(singleMenuItems, "NEEDS-ACTION");
+ }
+ // until we support actively delegating items, we also only display
+ // this status if it is already set (by another client or the server)
+ if (thisPartStat != "DELEGATED") {
+ hideItems(singleMenuItems, "DELEGATED");
+ }
+ } else {
+ // in this case, we hide the entire attendance menu
+ aMenu.setAttribute("hidden", "true");
+ }
+ } else if (aItems.length > 1) {
+ // the user displayed a context menu for multiple selected items.
+ // The selection might comprise single and recurring events, so we need
+ // to deal here with any combination thereof. To do so, we don't display
+ // a partstat control for the entire series but only for the selected
+ // occurrences. As we have a potential mixture of partstat, we also don't
+ // display the current status and no action towards NEEDS-ACTIONS.
+ hideItems(labels);
+ hideItems(seriesMenuItems);
+ hideItems(singleMenuItems, "NEEDS-ACTION");
+ } else {
+ // there seems to be no item passed in, so we don't display anything
+ hideItems(labels);
+ hideItems(seriesMenuItems);
+ hideItems(singleMenuItems);
+ }
+}
+
+/**
+ * Open the calendar settings to define the weekdays.
+ */
+function showCalendarWeekPreferences() {
+ openPreferencesTab("paneCalendar", "calendarPaneCategory");
+}