From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../content/widgets/calendar-invitation-panel.js | 799 +++++++++++++++++++++ 1 file changed, 799 insertions(+) create mode 100644 comm/calendar/base/content/widgets/calendar-invitation-panel.js (limited to 'comm/calendar/base/content/widgets/calendar-invitation-panel.js') diff --git a/comm/calendar/base/content/widgets/calendar-invitation-panel.js b/comm/calendar/base/content/widgets/calendar-invitation-panel.js new file mode 100644 index 0000000000..aa2be5e29f --- /dev/null +++ b/comm/calendar/base/content/widgets/calendar-invitation-panel.js @@ -0,0 +1,799 @@ +/* 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
    element that can visually show changes + * between elements of a list value. + * + * @template T + */ + class BaseInvitationChangeList extends HTMLUListElement { + /** + * An array containing the old values to be compared against for changes. + * + * @type {T[]} + */ + oldValue = []; + + /** + * String indicating the type of list items to create. This is passed + * directly to the "is" argument of document.createElement(). + * + * @abstract + */ + listItem; + + _createListItem(value, status) { + let li = document.createElement("li", { is: this.listItem }); + li.changeStatus = status; + li.value = value; + return li; + } + + /** + * Setting this property will trigger rendering of the list. If no prior + * values are detected, change indicators are not touched. + * + * @type {T[]} + */ + set value(list) { + if (!this.oldValue.length) { + for (let value of list) { + this.append(this._createListItem(value)); + } + return; + } + for (let [value, status] of this.getChanges(this.oldValue, list)) { + this.appendChild(this._createListItem(value, status)); + } + } + + /** + * Implemented by sub-classes to generate a list of changes for each element + * of the new list. + * + * @param {T[]} oldValue + * @param {T[]} newValue + * @return {[T, number][]} + */ + getChanges(oldValue, newValue) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + } + + /** + * BaseInvitationChangeListItem is the
  • element used for change lists. + * + * @template {T} + */ + class BaseInvitationChangeListItem extends HTMLLIElement { + /** + * Indicates whether the item value has changed and should be displayed as + * such. Its value is one of the PROPERTY_* constants. + * + * @type {number} + */ + changeStatus = PROPERTY_UNCHANGED; + + /** + * Settings this property will render the list item including a change + * indicator if the changeStatus property != PROPERTY_UNCHANGED. + * + * @type {T} + */ + set value(itemValue) { + this.build(itemValue); + if (this.changeStatus) { + let changeIndicator = document.createElement("calendar-invitation-change-indicator"); + changeIndicator.type = this.changeStatus; + this.append(changeIndicator); + } + } + + /** + * Implemented by sub-classes to build the
  • inner DOM structure. + * + * @param {T} value + * @abstract + */ + build(value) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + } + + /** + * InvitationAttendeeList displays a list of all the attendees on an event's + * attendee list. + */ + class InvitationAttendeeList extends BaseInvitationChangeList { + listItem = "calendar-invitation-panel-attendee-list-item"; + + getChanges(oldValue, newValue) { + let diff = []; + for (let att of newValue) { + let oldAtt = oldValue.find(oldAtt => oldAtt.id == att.id); + if (!oldAtt) { + diff.push([att, PROPERTY_ADDED]); // New attendee. + } else if (oldAtt.participationStatus != att.participationStatus) { + diff.push([att, PROPERTY_MODIFIED]); // Participation status changed. + } else { + diff.push([att, PROPERTY_UNCHANGED]); // No change. + } + } + + // Insert removed attendees into the diff. + for (let [idx, att] of oldValue.entries()) { + let found = newValue.find(newAtt => newAtt.id == att.id); + if (!found) { + diff.splice(idx, 0, [att, PROPERTY_REMOVED]); + } + } + return diff; + } + } + customElements.define("calendar-invitation-panel-attendee-list", InvitationAttendeeList, { + extends: "ul", + }); + + /** + * InvitationAttendeeListItem displays a single attendee from the attendee + * list. + */ + class InvitationAttendeeListItem extends BaseInvitationChangeListItem { + build(value) { + let span = document.createElement("span"); + if (this.changeStatus == PROPERTY_REMOVED) { + span.setAttribute("class", "removed"); + } + span.textContent = value; + this.appendChild(span); + } + } + customElements.define( + "calendar-invitation-panel-attendee-list-item", + InvitationAttendeeListItem, + { + extends: "li", + } + ); + + /** + * InvitationAttachmentList displays a list of all attachments in the invitation + * that have URIs. Binary attachments are not supported. + */ + class InvitationAttachmentList extends BaseInvitationChangeList { + listItem = "calendar-invitation-panel-attachment-list-item"; + + getChanges(oldValue, newValue) { + let diff = []; + for (let attch of newValue) { + if (!attch.uri) { + continue; + } + let oldAttch = oldValue.find( + oldAttch => oldAttch.uri && oldAttch.uri.spec == attch.uri.spec + ); + + if (!oldAttch) { + // New attachment. + diff.push([attch, PROPERTY_ADDED]); + continue; + } + if ( + attch.hashId != oldAttch.hashId || + attch.getParameter("FILENAME") != oldAttch.getParameter("FILENAME") + ) { + // Contents changed or renamed. + diff.push([attch, PROPERTY_MODIFIED]); + continue; + } + // No change. + diff.push([attch, PROPERTY_UNCHANGED]); + } + + // Insert removed attachments into the diff. + for (let [idx, attch] of oldValue.entries()) { + if (!attch.uri) { + continue; + } + let found = newValue.find(newAtt => newAtt.uri && newAtt.uri.spec == attch.uri.spec); + if (!found) { + diff.splice(idx, 0, [attch, PROPERTY_REMOVED]); + } + } + return diff; + } + } + customElements.define("calendar-invitation-panel-attachment-list", InvitationAttachmentList, { + extends: "ul", + }); + + /** + * InvitationAttachmentListItem displays a link to an attachment attached to the + * event. + */ + class InvitationAttachmentListItem extends BaseInvitationChangeListItem { + /** + * Indicates whether the attachment has changed and should be displayed as + * such. Its value is one of the PROPERTY_* constants. + * + * @type {number} + */ + changeStatus = PROPERTY_UNCHANGED; + + /** + * Sets up the attachment to be displayed as a link with appropriate icon. + * Links are opened externally. + * + * @param {calIAttachment} + */ + build(value) { + let icon = document.createElement("img"); + let iconSrc = value.uri.spec.length ? value.uri.spec : "dummy.html"; + if (!value.uri.schemeIs("file")) { + // Using an uri directly, with e.g. a http scheme, wouldn't render any icon. + if (value.formatType) { + iconSrc = "goat?contentType=" + value.formatType; + } else { + // Let's try to auto-detect. + let parts = iconSrc.substr(value.uri.scheme.length + 2).split("/"); + if (parts.length) { + iconSrc = parts[parts.length - 1]; + } + } + } + icon.setAttribute("src", "moz-icon://" + iconSrc); + this.append(icon); + + let title = value.getParameter("FILENAME") || value.uri.spec; + if (this.changeStatus == PROPERTY_REMOVED) { + let span = document.createElement("span"); + span.setAttribute("class", "removed"); + span.textContent = title; + this.append(span); + } else { + let link = document.createElement("a"); + link.textContent = title; + link.setAttribute("href", value.uri.spec); + link.addEventListener("click", event => { + event.preventDefault(); + openLinkExternally(event.target.href); + }); + this.append(link); + } + } + } + customElements.define( + "calendar-invitation-panel-attachment-list-item", + InvitationAttachmentListItem, + { + extends: "li", + } + ); + + /** + * InvitationChangeIndicator is a visual indicator for indicating some piece + * of data has changed. + */ + class InvitationChangeIndicator extends HTMLElement { + _typeMap = { + [PROPERTY_REMOVED]: "removed", + [PROPERTY_ADDED]: "added", + [PROPERTY_MODIFIED]: "modified", + }; + + /** + * One of the PROPERTY_* constants that indicates what kind of change we + * are indicating (add/modify/delete) etc. + * + * @type {number} + */ + set type(value) { + let key = this._typeMap[value]; + document.l10n.setAttributes(this, `calendar-invitation-change-indicator-${key}`); + } + } + customElements.define("calendar-invitation-change-indicator", InvitationChangeIndicator); +} -- cgit v1.2.3