/* 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/. */ /* globals cal, openLinkExternally, MozXULElement, MozElements */ "use strict"; // Wrap in a block to prevent leaking to window scope. { var { recurrenceRule2String } = ChromeUtils.import( "resource:///modules/calendar/calRecurrenceUtils.jsm" ); // calendar-invitation-panel.ftl is not globally loaded until now. MozXULElement.insertFTLIfNeeded("calendar/calendar-invitation-panel.ftl"); const PROPERTY_REMOVED = -1; const PROPERTY_UNCHANGED = 0; const PROPERTY_ADDED = 1; const PROPERTY_MODIFIED = 2; /** * InvitationPanel displays the details of an iTIP event invitation in an * interactive panel. */ class InvitationPanel extends HTMLElement { static MODE_NEW = "New"; static MODE_ALREADY_PROCESSED = "Processed"; static MODE_UPDATE_MAJOR = "UpdateMajor"; static MODE_UPDATE_MINOR = "UpdateMinor"; static MODE_CANCELLED = "Cancelled"; static MODE_CANCELLED_NOT_FOUND = "CancelledNotFound"; /** * Used to retrieve a property value from an event. * * @callback GetValue * @param {calIEvent} event * @returns {string} */ /** * A function used to make a property value visible in to the user. * * @callback PropertyShow * @param {HTMLElement} node - The element responsible for displaying the * value. * @param {string} value - The value of property to display. * @param {string} oldValue - The previous value of the property if the * there is a prior copy of the event. * @param {calIEvent} item - The event item the property belongs to. * @param {string} oldItem - The prior version of the event if there is one. */ /** * @typedef {Object} InvitationPropertyDescriptor * @property {string} id - The id of the HTMLElement that displays * the property. * @property {GetValue} getValue - Function used to retrieve the displayed * value of the property from the item. * @property {boolean?} isList - Indicates the value of the property is a * list. * @property {PropertyShow?} show - Function to use to display the property * value if it is not a list. */ /** * A static list of objects used in determining how to display each of the * properties. * * @type {PropertyDescriptor[]} */ static propertyDescriptors = [ { id: "when", getValue(item) { let tz = cal.dtz.defaultTimezone; let startDate = item.startDate?.getInTimezone(tz) ?? null; let endDate = item.endDate?.getInTimezone(tz) ?? null; return `${startDate.icalString}-${endDate?.icalString}`; }, show(intervalNode, newValue, oldValue, item) { intervalNode.item = item; }, }, { id: "recurrence", getValue(item) { let parent = item.parentItem; if (!parent.recurrenceInfo) { return null; } return recurrenceRule2String(parent.recurrenceInfo, parent.recurrenceStartDate); }, show(recurrence, value) { recurrence.appendChild(document.createTextNode(value)); }, }, { id: "location", getValue(item) { return item.getProperty("LOCATION"); }, show(location, value) { location.appendChild(cal.view.textToHtmlDocumentFragment(value, document)); }, }, { id: "summary", getValue(item) { return item.getAttendees(); }, show(summary, value) { summary.attendees = value; }, }, { id: "attendees", isList: true, getValue(item) { return item.getAttendees(); }, }, { id: "attachments", isList: true, getValue(item) { return item.getAttachments(); }, }, { id: "description", getValue(item) { return item.descriptionText; }, show(description, value) { description.appendChild(cal.view.textToHtmlDocumentFragment(value, document)); }, }, ]; /** * mode determines how the UI should display the received invitation. It * must be set to one of the MODE_* constants, defaults to MODE_NEW. * * @type {string} */ mode = InvitationPanel.MODE_NEW; /** * A previous copy of the event item if found on an existing calendar. * * @type {calIEvent?} */ foundItem; /** * The event item to be displayed. * * @type {calIEvent?} */ item; constructor(id) { super(); this.attachShadow({ mode: "open" }); document.l10n.connectRoot(this.shadowRoot); let link = document.createElement("link"); link.rel = "stylesheet"; link.href = "chrome://calendar/skin/shared/widgets/calendar-invitation-panel.css"; this.shadowRoot.appendChild(link); } /** * Compares two like property values, an old and a new one, to determine * what type of change has been made (if any). * * @param {any} oldValue * @param {any} newValue * @returns {number} - One of the PROPERTY_* constants. */ compare(oldValue, newValue) { if (!oldValue && newValue) { return PROPERTY_ADDED; } if (oldValue && !newValue) { return PROPERTY_REMOVED; } return oldValue != newValue ? PROPERTY_MODIFIED : PROPERTY_UNCHANGED; } connectedCallback() { if (this.item && this.mode) { let template = document.getElementById(`calendarInvitationPanel`); this.shadowRoot.appendChild(template.content.cloneNode(true)); if (this.foundItem && this.foundItem.title != this.item.title) { let indicator = this.shadowRoot.getElementById("titleChangeIndicator"); indicator.status = PROPERTY_MODIFIED; indicator.hidden = false; } this.shadowRoot.getElementById("title").textContent = this.item.title; let statusBar = this.shadowRoot.querySelector("calendar-invitation-panel-status-bar"); statusBar.status = this.mode; this.shadowRoot.querySelector("calendar-minidate").date = this.item.startDate; for (let prop of InvitationPanel.propertyDescriptors) { let el = this.shadowRoot.getElementById(prop.id); let value = prop.getValue(this.item); let result = PROPERTY_UNCHANGED; if (prop.isList) { let oldValue = this.foundItem ? prop.getValue(this.foundItem) : []; if (value.length || oldValue.length) { el.oldValue = oldValue; el.value = value; el.closest(".calendar-invitation-row").hidden = false; } continue; } let oldValue = this.foundItem ? prop.getValue(this.foundItem) : null; if (this.foundItem) { result = this.compare(oldValue, value); if (result) { let indicator = this.shadowRoot.getElementById(`${prop.id}ChangeIndicator`); if (indicator) { indicator.type = result; indicator.hidden = false; } } } if (value || oldValue) { prop.show(el, value, oldValue, this.item, this.foundItem, result); el.closest(".calendar-invitation-row").hidden = false; } } if ( this.mode == InvitationPanel.MODE_NEW || this.mode == InvitationPanel.MODE_UPDATE_MAJOR ) { for (let button of this.shadowRoot.querySelectorAll("#actionButtons > button")) { button.addEventListener("click", e => this.dispatchEvent( new CustomEvent("calendar-invitation-panel-action", { detail: { type: button.dataset.action }, }) ) ); } this.shadowRoot.getElementById("footer").hidden = false; } } } } customElements.define("calendar-invitation-panel", InvitationPanel); /** * Object used to describe relevant arguments to MozElements.NotificationBox. * appendNotification(). * @type {Object} InvitationStatusBarDescriptor * @property {string} label - An l10n id used used to generate the notification * bar text. * @property {number} priority - One of the notification box constants that * indicate the priority of a notification. * @property {object[]} buttons - An array of objects corresponding to the * "buttons" argument of MozElements.NotificationBox.appendNotification(). * See that method for details. */ /** * InvitationStatusBar generates a notification bar that informs the user about * the status of the received invitation and possible actions they may take. */ class InvitationPanelStatusBar extends HTMLElement { /** * @type {NotificationBox} */ get notificationBox() { if (!this._notificationBox) { this._notificationBox = new MozElements.NotificationBox(element => { this.append(element); }); } return this._notificationBox; } /** * Map-like object where each key is an InvitationPanel mode and the values * are descriptors used to generate the notification bar for that mode. * * @type {Object. */ notices = { [InvitationPanel.MODE_NEW]: { label: "calendar-invitation-panel-status-new", buttons: [ { "l10n-id": "calendar-invitation-panel-more-button", callback: (notification, opts, button, event) => this._showMoreMenu(event, [ { l10nId: "calendar-invitation-panel-menu-item-save-copy", name: "save", command: e => this.dispatchEvent( new CustomEvent("calendar-invitation-panel-action", { details: { type: "x-savecopy" }, bubbles: true, composed: true, }) ), }, ]), }, ], }, [InvitationPanel.MODE_ALREADY_PROCESSED]: { label: "calendar-invitation-panel-status-processed", buttons: [ { "l10n-id": "calendar-invitation-panel-view-button", callback: () => { this.dispatchEvent( new CustomEvent("calendar-invitation-panel-action", { detail: { type: "x-showdetails" }, bubbles: true, composed: true, }) ); return true; }, }, ], }, [InvitationPanel.MODE_UPDATE_MINOR]: { label: "calendar-invitation-panel-status-updateminor", priority: this.notificationBox.PRIORITY_WARNING_LOW, buttons: [ { "l10n-id": "calendar-invitation-panel-update-button", callback: () => { this.dispatchEvent( new CustomEvent("calendar-invitation-panel-action", { detail: { type: "update" }, bubbles: true, composed: true, }) ); return true; }, }, ], }, [InvitationPanel.MODE_UPDATE_MAJOR]: { label: "calendar-invitation-panel-status-updatemajor", priority: this.notificationBox.PRIORITY_WARNING_LOW, }, [InvitationPanel.MODE_CANCELLED]: { label: "calendar-invitation-panel-status-cancelled", buttons: [{ "l10n-id": "calendar-invitation-panel-delete-button" }], priority: this.notificationBox.PRIORITY_CRITICAL_LOW, }, [InvitationPanel.MODE_CANCELLED_NOT_FOUND]: { label: "calendar-invitation-panel-status-cancelled-notfound", priority: this.notificationBox.PRIORITY_CRITICAL_LOW, }, }; /** * status corresponds to one of the MODE_* constants and will trigger * rendering of the notification box. * * @type {string} status */ set status(value) { let opts = this.notices[value]; let priority = opts.priority || this.notificationBox.PRIORITY_INFO_LOW; let buttons = opts.buttons || []; let notification = this.notificationBox.appendNotification( "invitationStatus", { label: { "l10n-id": opts.label }, priority, }, buttons ); notification.removeAttribute("dismissable"); } _showMoreMenu(event, menuitems) { let menu = document.getElementById("calendarInvitationPanelMoreMenu"); menu.replaceChildren(); for (let { type, l10nId, name, command } of menuitems) { let menuitem = document.createXULElement("menuitem"); if (type) { menuitem.type = type; } if (name) { menuitem.name = name; } if (command) { menuitem.addEventListener("command", command); } document.l10n.setAttributes(menuitem, l10nId); menu.appendChild(menuitem); } menu.openPopup(event.originalTarget, "after_start", 0, 0, false, false, event); return true; } } customElements.define("calendar-invitation-panel-status-bar", InvitationPanelStatusBar); /** * InvitationInterval displays the formatted interval of the event. Formatting * relies on cal.dtz.formatter.formatIntervalParts(). */ class InvitationInterval extends HTMLElement { /** * The item whose interval to show. * * @type {calIEvent} */ set item(value) { let [startDate, endDate] = cal.dtz.formatter.getItemDates(value); let timezone = startDate.timezone.displayName; let parts = cal.dtz.formatter.formatIntervalParts(startDate, endDate); document.l10n.setAttributes(this, `calendar-invitation-interval-${parts.type}`, { ...parts, timezone, }); } } customElements.define("calendar-invitation-interval", InvitationInterval); const partStatOrder = ["ACCEPTED", "DECLINED", "TENTATIVE", "NEEDS-ACTION"]; /** * InvitationPartStatSummary generates text indicating the aggregated * participation status of each attendee in the event's attendees list. */ class InvitationPartStatSummary extends HTMLElement { constructor() { super(); this.appendChild( document.getElementById("calendarInvitationPartStatSummary").content.cloneNode(true) ); } /** * Setting this property will trigger an update of the text displayed. * * @type {calIAttendee[]} */ set attendees(attendees) { let counts = { ACCEPTED: 0, DECLINED: 0, TENTATIVE: 0, "NEEDS-ACTION": 0, TOTAL: attendees.length, OTHER: 0, }; for (let { participationStatus } of attendees) { if (counts.hasOwnProperty(participationStatus)) { counts[participationStatus]++; } else { counts.OTHER++; } } document.l10n.setAttributes( this.querySelector("#partStatTotal"), "calendar-invitation-panel-partstat-total", { count: counts.TOTAL } ); let shownPartStats = partStatOrder.filter(partStat => counts[partStat]); let breakdown = this.querySelector("#partStatBreakdown"); for (let partStat of shownPartStats) { let span = document.createElement("span"); span.setAttribute("class", "calendar-invitation-panel-partstat-summary"); // calendar-invitation-panel-partstat-accepted // calendar-invitation-panel-partstat-declined // calendar-invitation-panel-partstat-tentative // calendar-invitation-panel-partstat-needs-action document.l10n.setAttributes( span, `calendar-invitation-panel-partstat-${partStat.toLowerCase()}`, { count: counts[partStat], } ); breakdown.appendChild(span); } } } customElements.define("calendar-invitation-partstat-summary", InvitationPartStatSummary); /** * BaseInvitationChangeList is a