This commit is contained in:
Geoff Lankow 2022-12-08 16:00:11 +13:00
Родитель cef4e5fac7 a274baf1aa
Коммит ab26990946
8 изменённых файлов: 731 добавлений и 316 удалений

Просмотреть файл

@ -13,6 +13,13 @@
* invitation panel when an email contains an embedded invitation.
*/
const CalInvitationDisplay = {
/**
* The itipItem currently displayed.
*
* @type {calIItipItem}
*/
currentItipItem: null,
/**
* The XUL element that wraps the invitation.
*
@ -91,28 +98,45 @@
* @param {calIItipItem} itipItem
*/
async show(itipItem) {
this.currentItipItem = itipItem;
this.display.replaceChildren();
let [, rc, actionFunc, foundItems] = await new Promise(resolve =>
cal.itip.processItipItem(itipItem, (targetItipItem, rc, actionFunc, foundItems) =>
resolve([targetItipItem, rc, actionFunc, foundItems])
)
);
if (this.currentItipItem != itipItem || !Components.isSuccessCode(rc)) {
return;
}
let [item] = itipItem.getItemList();
let foundItem = await itipItem.targetCalendar.getItem(item.id);
let [foundItem] = foundItems;
let panel = document.createElement("calendar-invitation-panel");
let { mode } = panel;
switch (itipItem.receivedMethod) {
let method = actionFunc ? actionFunc.method : itipItem.receivedMethod;
switch (method) {
case "REQUEST:UPDATE":
panel.mode = panel.constructor.MODE_UPDATE_MAJOR;
break;
case "REQUEST:UPDATE-MINOR":
panel.mode = panel.constructor.MODE_UPDATE_MINOR;
break;
case "REQUEST":
if (foundItem) {
if (cal.itip.compareSequence(foundItem, item) == 1) {
mode = panel.MODE_UPDATE_MAJOR;
} else if (cal.itip.compareStamp(foundItem, item) == 1) {
mode = panel.MODE_UPDATE_MINOR;
} else {
mode = panel.MODE_ALREADY_PROCESSED;
}
}
panel.mode = foundItem
? panel.constructor.MODE_ALREADY_PROCESSED
: panel.constructor.MODE_NEW;
break;
case "CANCEL":
mode = foundItem ? panel.MODE_CANCELLED : panel.MODE_CANCELLED_NOT_FOUND;
panel.mode = foundItem
? panel.constructor.MODE_CANCELLED
: panel.constructor.MODE_CANCELLED_NOT_FOUND;
break;
default:
panel.mode = panel.mode = panel.constructor.MODE_NEW;
break;
}
panel.mode = mode;
panel.foundItem = foundItem;
panel.item = item;
this.display.appendChild(panel);
this.body.hidden = true;

Просмотреть файл

@ -2,7 +2,7 @@
* 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 */
/* globals cal, openLinkExternally, MozXULElement, MozElements */
"use strict";
@ -12,12 +12,20 @@
"resource:///modules/calendar/calRecurrenceUtils.jsm"
);
let l10n = new DOMLocalization(["calendar/calendar-invitation-panel.ftl"]);
// calendar-invitation-panel.ftl is not globally loaded until now.
MozXULElement.insertFTLIfNeeded("calendar/calendar-invitation-panel.ftl");
/**
* Base element providing boilerplate for shadow root initialisation.
*/
class BaseInvitationElement extends HTMLElement {
/**
* A previous copy of the event item if found on an existing calendar.
*
* @type {calIEvent?}
*/
foundItem;
/**
* The id of the <template> tag to initialize the element with.
*
@ -26,7 +34,7 @@
constructor(id) {
super();
this.attachShadow({ mode: "open" });
l10n.connectRoot(this.shadowRoot);
document.l10n.connectRoot(this.shadowRoot);
let link = document.createElement("link");
link.rel = "stylesheet";
@ -39,7 +47,7 @@
}
disconnectedCallback() {
l10n.disconnectRoot(this.shadowRoot);
document.l10n.disconnectRoot(this.shadowRoot);
}
}
@ -48,12 +56,12 @@
* interactive panel.
*/
class InvitationPanel extends BaseInvitationElement {
MODE_NEW = "New";
MODE_ALREADY_PROCESSED = "Processed";
MODE_UPDATE_MAJOR = "UpdateMajor";
MODE_UPDATE_MINOR = "UpdateMinor";
MODE_CANCELLED = "Cancelled";
MODE_CANCELLED_NOT_FOUND = "CancelledNotFound";
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";
/**
* mode determines how the UI should display the received invitation. It
@ -61,7 +69,7 @@
*
* @type {string}
*/
mode = this.MODE_NEW;
mode = InvitationPanel.MODE_NEW;
/**
* The event item to be displayed.
@ -72,78 +80,205 @@
connectedCallback() {
if (this.item && this.mode) {
let template = document.getElementById(`calendarInvitationPanel${this.mode}`);
let template = document.getElementById(`calendarInvitationPanel`);
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.shadowRoot.getElementById("wrapper").item = this.item;
this.shadowRoot.getElementById("header").item = this.item;
let title = this.shadowRoot.querySelector("calendar-invitation-panel-title");
title.foundItem = this.foundItem;
title.item = this.item;
let statusBar = this.shadowRoot.querySelector("calendar-invitation-panel-status-bar");
statusBar.status = this.mode;
this.shadowRoot.querySelector("calendar-minidate").date = this.item.startDate;
let props = this.shadowRoot.querySelector("calendar-invitation-panel-properties");
props.foundItem = this.foundItem;
props.item = this.item;
}
}
}
customElements.define("calendar-invitation-panel", InvitationPanel);
/**
* InvitationPanelWrapper wraps the contents of the panel for formatting and
* provides the minidate to the left of the details.
* InvitationPanelTitle displays the title of the event in a header element.
*/
class InvitationPanelWrapper extends BaseInvitationElement {
constructor() {
super("calendarInvitationPanelWrapper");
}
set item(value) {
this.shadowRoot.getElementById("minidate").date = value.startDate;
this.shadowRoot.getElementById("properties").item = value;
}
}
customElements.define("calendar-invitation-panel-wrapper", InvitationPanelWrapper);
/**
* InvitationPanelHeader renders the header part of the invitation panel.
*/
class InvitationPanelHeader extends BaseInvitationElement {
constructor() {
super("calendarInvitationPanelHeader");
}
class InvitationPanelTitle extends HTMLElement {
/**
* A previous copy of the event, if specified it is used to determine if
* the title of the invitation has changed.
*
* @type {calIEvent?}
*/
foundItem;
/**
* Setting the item will populate the header with information.
* Setting this value will render the title as well as a change indicator
* if a change in title is detected.
*
* @type {calIEvent}
*/
set item(item) {
let l10nArgs = JSON.stringify({
summary: item.getProperty("SUMMARY") || "",
organizer: item.organizer ? item.organizer?.commonName || item.organizer.toString() : "",
});
let action = this.getAttribute("actionType");
if (action) {
this.shadowRoot
.getElementById("intro")
.setAttribute("data-l10n-id", `calendar-invitation-panel-intro-${action}`);
set item(value) {
let node = document.getElementById("calendarInvitationPanelTitle").content.cloneNode(true);
if (this.foundItem && this.foundItem.title != value.title) {
let indicator = node.querySelector("calendar-invitation-change-indicator");
indicator.status = PROPERTY_MODIFIED;
indicator.hidden = false;
}
for (let id of ["intro", "title"]) {
this.shadowRoot.getElementById(id).setAttribute("data-l10n-args", l10nArgs);
node.querySelector("h1").textContent = value.title;
this.append(node);
}
}
customElements.define("calendar-invitation-panel-title", InvitationPanelTitle);
/**
* 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;
}
/**
* Provides the value of the title displayed as a string.
* 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 {string}
* @type {Object.<string, InvitationStatusBarDescriptor>
*/
get fullTitle() {
return [
...this.shadowRoot.querySelectorAll(
".calendar-invitation-panel-intro, .calendar-invitation-panel-title"
),
]
.map(node => node.textContent)
.join(" ");
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",
},
]),
},
],
},
[InvitationPanel.MODE_ALREADY_PROCESSED]: {
label: "calendar-invitation-panel-status-processed",
buttons: [
{
"l10n-id": "calendar-invitation-panel-view-button",
},
],
},
[InvitationPanel.MODE_UPDATE_MINOR]: {
label: "calendar-invitation-panel-status-updateminor",
priority: this.notificationBox.PRIORITY_WARNING_LOW,
buttons: [
{ "l10n-id": "calendar-invitation-panel-update-button" },
{
"l10n-id": "calendar-invitation-panel-more-button",
callback: (notification, opts, button, event) =>
this._showMoreMenu(event, [
{
type: "checkbox",
l10nId: "calendar-invitation-panel-menu-item-toggle-changes",
},
]),
},
],
},
[InvitationPanel.MODE_UPDATE_MAJOR]: {
label: "calendar-invitation-panel-status-updatemajor",
priority: this.notificationBox.PRIORITY_WARNING_LOW,
buttons: [
{
"l10n-id": "calendar-invitation-panel-more-button",
callback: (notification, opts, button, event) =>
this._showMoreMenu(event, [
{
type: "checkbox",
l10nId: "calendar-invitation-panel-menu-item-toggle-changes",
},
]),
},
],
},
[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, command } of menuitems) {
let menuitem = document.createXULElement("menuitem");
if (type) {
menuitem.type = type;
}
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-header", InvitationPanelHeader);
customElements.define("calendar-invitation-panel-status-bar", InvitationPanelStatusBar);
const PROPERTY_REMOVED = -1;
const PROPERTY_UNCHANGED = 0;
const PROPERTY_ADDED = 1;
const PROPERTY_MODIFIED = 2;
/**
* InvitationPanelProperties renders the details of the most useful properties
@ -154,6 +289,90 @@
super("calendarInvitationPanelProperties");
}
/**
* 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 {PropertyShow} show - Function to use to display the property
* value.
*/
/**
* A static list of objects used in determining how to display each of the
* properties.
*
* @type {PropertyDescriptor[]}
*/
static propertyDescriptors = [
{
id: "interval",
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: "description",
getValue(item) {
return item.descriptionText;
},
show(description, value) {
description.appendChild(cal.view.textToHtmlDocumentFragment(value, document));
},
},
];
/**
* Setting the item will populate the table that displays the event
* properties.
@ -161,30 +380,59 @@
* @type {calIEvent}
*/
set item(item) {
let interval = this.shadowRoot.getElementById("interval");
interval.item = item;
if (item.recurrenceInfo || item.parentItem.recurrenceInfo) {
let parent = item.parentItem;
this.shadowRoot.getElementById("recurrence").textContent = recurrenceRule2String(
parent.recurrenceInfo,
parent.recurrenceStartDate
);
for (let prop of InvitationPanelProperties.propertyDescriptors) {
let el = this.shadowRoot.getElementById(prop.id);
let value = prop.getValue(item);
let oldValue;
let result = PROPERTY_UNCHANGED;
if (this.foundItem) {
oldValue = prop.getValue(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) {
prop.show(el, value, oldValue, item, this.foundItem, result);
}
}
this.shadowRoot
.getElementById("location")
.appendChild(cal.view.textToHtmlDocumentFragment(item.getProperty("LOCATION"), document));
let attendeeValues = item.getAttendees();
this.shadowRoot.getElementById("summary").attendees = attendeeValues;
let attendees = item.getAttendees();
this.shadowRoot.getElementById("summary").attendees = attendees;
this.shadowRoot.getElementById("attendees").attendees = attendees;
let attendees = this.shadowRoot.getElementById("attendees");
if (this.foundItem) {
attendees.oldValue = this.foundItem.getAttendees();
}
attendees.value = attendeeValues;
this.shadowRoot
.getElementById("description")
.appendChild(cal.view.textToHtmlDocumentFragment(item.descriptionText, document));
let attachments = this.shadowRoot.getElementById("attachments");
if (this.foundItem) {
attachments.oldValue = this.foundItem.getAttachments();
}
attachments.value = item.getAttachments();
}
this.shadowRoot.getElementById("attachments").attachments = item.getAttachments();
/**
* 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;
}
}
customElements.define("calendar-invitation-panel-properties", InvitationPanelProperties);
@ -207,7 +455,7 @@
let [startDate, endDate] = cal.dtz.formatter.getItemDates(value);
let timezone = startDate.timezone.displayName;
let parts = cal.dtz.formatter.formatIntervalParts(startDate, endDate);
l10n.setAttributes(
document.l10n.setAttributes(
this.shadowRoot.getElementById("interval"),
`calendar-invitation-interval-${parts.type}`,
{ ...parts, timezone }
@ -249,7 +497,7 @@
counts.OTHER++;
}
}
l10n.setAttributes(
document.l10n.setAttributes(
this.shadowRoot.getElementById("total"),
"calendar-invitation-panel-partstat-total",
{ count: counts.TOTAL }
@ -265,9 +513,13 @@
// calendar-invitation-panel-partstat-declined
// calendar-invitation-panel-partstat-tentative
// calendar-invitation-panel-partstat-needs-action
l10n.setAttributes(span, `calendar-invitation-panel-partstat-${partStat.toLowerCase()}`, {
count: counts[partStat],
});
document.l10n.setAttributes(
span,
`calendar-invitation-panel-partstat-${partStat.toLowerCase()}`,
{
count: counts[partStat],
}
);
breakdown.appendChild(span);
}
}
@ -275,80 +527,232 @@
customElements.define("calendar-invitation-partstat-summary", InvitationPartStatSummary);
/**
* InvitationAttendeeList displays a list of all the attendees on
* an event's attendee list.
* BaseInvitationChangeList is a <ul> element that can visually show changes
* between elements of a list value.
*
* @template T
*/
class InvitationAttendeeList extends BaseInvitationElement {
constructor() {
super("calendarInvitationAttendeesList");
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 attendees list.
* Setting this property will trigger rendering of the list. If no prior
* values are detected, change indicators are not touched.
*
* @type {calIAttendee[]}
* @type {T[]}
*/
set attendees(value) {
let ul = this.shadowRoot.getElementById("list");
for (let att of value) {
let li = document.createElement("li");
let span = document.createElement("span");
span.textContent = att;
li.appendChild(span);
ul.appendChild(li);
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);
}
}
customElements.define("calendar-invitation-attendee-list", InvitationAttendeeList);
/**
* InvitationAttachmentList displays a list of all attachments in the
* invitation that have URIs. Binary attachments are not supported.
* BaseInvitationChangeListItem is the <li> element used for change lists.
*
* @template {T}
*/
class InvitationAttachmentList extends BaseInvitationElement {
constructor() {
super("calendarInvitationAttachmentList");
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);
}
}
/**
* Setting this property will trigger rendering of the attachments list.
* Implemented by sub-classes to build the <li> inner DOM structure.
*
* @type {calIAttachment[]}
* @param {T} value
* @abstract
*/
set attachments(value) {
let ul = this.shadowRoot.getElementById("list");
for (let attachment of value) {
if (attachment.uri) {
let item = document.createElement("li", { is: "calendar-invitation-attachment-item" });
item.attachment = attachment;
ul.appendChild(item);
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-attachment-list", InvitationAttachmentList);
customElements.define("calendar-invitation-panel-attendee-list", InvitationAttendeeList, {
extends: "ul",
});
/**
* InvitationAttachmentItem displays a link to an attachment attached to the
* 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 InvitationAttachmentItem extends HTMLLIElement {
class InvitationAttachmentListItem extends BaseInvitationChangeListItem {
/**
* Settings this property will set up the attachment to be displayed as a
* link with appropriate icon. Links are opened externally.
* Indicates whether the attachment has changed and should be displayed as
* such. Its value is one of the PROPERTY_* constants.
*
* @type {calIAttachment[]}
* @type {number}
*/
set attachment(value) {
let title = value.getParameter("FILENAME") || value.uri.spec;
let link = document.createElement("a");
link.textContent = title;
link.setAttribute("href", value.uri.spec);
link.addEventListener("click", event => {
event.preventDefault();
openLinkExternally(event.target.href);
});
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")) {
@ -364,12 +768,59 @@
}
}
icon.setAttribute("src", "moz-icon://" + iconSrc);
this.append(icon, link);
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-attachment-item", InvitationAttachmentItem, {
extends: "li",
});
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}
*/
type = PROPERTY_MODIFIED;
connectedCallback() {
let key = this._typeMap[this.type];
document.l10n.setAttributes(this, `calendar-invitation-change-indicator-${key}`);
}
}
customElements.define("calendar-invitation-change-indicator", InvitationChangeIndicator);
/**
* InvitationPanelFooter renders the footer for the details section of
@ -379,13 +830,6 @@
constructor() {
super("calendarInvitationPanelFooter");
}
connectedCallback() {
l10n.setAttributes(
this.shadowRoot.getElementById("status"),
"calendar-invitation-panel-reply-status"
);
}
}
customElements.define("calendar-invitation-panel-footer", InvitationPanelFooter);
}

Просмотреть файл

@ -2,108 +2,25 @@
# 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/.
<!-- Template used by <calendar-invitation-panel/> when an item is first
encountered -->
<template id="calendarInvitationPanelNew" xmlns="http://www.w3.org/1999/xhtml">
<calendar-invitation-panel-wrapper id="wrapper">
<calendar-invitation-panel-header id="header" slot="header">
<button id="actionButton"
class="calendar-invitation-panel-action-button"
data-l10n-id="calendar-invitation-panel-save-button"
slot="action"></button>
</calendar-invitation-panel-header>
</calendar-invitation-panel-wrapper>
<calendar-invitation-panel-footer/>
</template>
<!-- Template for <calendar-invitation-panel/> when the invitation has already
been processed. -->
<template id="calendarInvitationPanelProcessed" xmlns="http://www.w3.org/1999/xhtml">
<calendar-invitation-panel-wrapper id="wrapper">
<calendar-invitation-panel-header id="header" slot="header">
<button id="actionButton"
class="calendar-invitation-panel-action-button"
data-l10n-id="calendar-invitation-panel-view-button"
slot="action"></button>
</calendar-invitation-panel-header>
</calendar-invitation-panel-wrapper>
<calendar-invitation-panel-footer/>
</template>
<!-- Template for <calendar-invitation-panel/> when the invitation is a major
update. -->
<template id="calendarInvitationPanelUpdateMajor" xmlns="http://www.w3.org/1999/xhtml">
<calendar-invitation-panel-wrapper id="wrapper">
<calendar-invitation-panel-header id="header" slot="header" actionType="update">
<button id="actionButton"
class="calendar-invitation-panel-action-button"
data-l10n-id="calendar-invitation-panel-view-button"
slot="action"></button>
</calendar-invitation-panel-header>
</calendar-invitation-panel-wrapper>
<calendar-invitation-panel-footer/>
</template>
<!-- Template for <calendar-invitation-panel/> when the invitation is a minor
update. -->
<template id="calendarInvitationPanelUpdateMinor" xmlns="http://www.w3.org/1999/xhtml">
<calendar-invitation-panel-wrapper id="wrapper">
<calendar-invitation-panel-header id="header" slot="header" actionType="update">
<button id="actionButton"
class="calendar-invitation-panel-action-button"
data-l10n-id="calendar-invitation-panel-update-button"
slot="action"></button>
</calendar-invitation-panel-header>
</calendar-invitation-panel-wrapper>
<calendar-invitation-panel-footer/>
</template>
<!-- Template for <calendar-invitation-panel/> when the invitation is a
cancellation of an existing event. -->
<template id="calendarInvitationPanelCancelled" xmlns="http://www.w3.org/1999/xhtml">
<calendar-invitation-panel-wrapper id="wrapper">
<calendar-invitation-panel-header id="header" slot="header" actionType="cancel">
<button id="actionButton"
class="calendar-invitation-panel-action-button"
data-l10n-id="calendar-invitation-panel-delete-button"
slot="action"></button>
</calendar-invitation-panel-header>
</calendar-invitation-panel-wrapper>
<calendar-invitation-panel-footer/>
</template>
<!-- Template for <calendar-invitation-panel/> when the invitation is a
cancellation of an unprocessed event. -->
<template id="calendarInvitationPanelCancelledNotFound" xmlns="http://www.w3.org/1999/xhtml">
<calendar-invitation-panel-wrapper id="wrapper">
<calendar-invitation-panel-header id="header" slot="header" actionType="cancel"/>
</calendar-invitation-panel-wrapper>
<calendar-invitation-panel-footer/>
</template>
<!-- Template for <calendar-invitation-panel-wrapper/> -->
<template id="calendarInvitationPanelWrapper" xmlns="http://www.w3.org/1999/xhtml">
<!-- Template for <calendar-invitation-panel/> -->
<template id="calendarInvitationPanel" xmlns="http://www.w3.org/1999/xhtml">
<calendar-invitation-panel-status-bar/>
<div class="calendar-invitation-panel-wrapper">
<div class="calendar-invitation-panel-preview">
<calendar-minidate id="minidate"/>
<calendar-minidate/>
</div>
<div class="calendar-invitation-panel-details">
<slot name="header"></slot>
<calendar-invitation-panel-properties id="properties"/>
<calendar-invitation-panel-title/>
<calendar-invitation-panel-properties/>
</div>
</div>
<calendar-invitation-panel-footer/>
</template>
<!-- Template for <calendar-invitation-panel-header/> -->
<template id="calendarInvitationPanelHeader" xmlns="http://www.w3.org/1999/xhtml">
<div class="calendar-invitation-panel-details-header">
<div id="intro"
class="calendar-invitation-panel-intro"
data-l10n-id="calendar-invitation-panel-intro"></div>
<div class="calendar-invitation-panel-summary">
<h1 id="title"
class="calendar-invitation-panel-title"
data-l10n-id="calendar-invitation-panel-title"></h1>
<slot name="action"></slot>
</div>
<!-- Template for <calendar-invitation-panel-title/> -->
<template id="calendarInvitationPanelTitle" xmlns="http://www.w3.org/1999/xhtml">
<div class="calendar-invitation-panel-title">
<calendar-invitation-change-indicator hidden="hidden"/><h1></h1>
</div>
</template>
@ -113,32 +30,45 @@
<tbody>
<tr>
<th data-l10n-id="calendar-invitation-panel-prop-title-when"></th>
<td id="when" class="calendar-invitation-when">
<td class="calendar-invitation-when">
<calendar-invitation-change-indicator id="intervalChangeIndicator" hidden="hidden"/>
<calendar-invitation-interval id="interval"/>
</td>
</tr>
<tr>
<th data-l10n-id="calendar-invitation-panel-prop-title-recurrence"></th>
<td id="recurrence" class="calendar-invitation-recurrence"></td>
<td id="recurrence" class="calendar-invitation-recurrence">
<calendar-invitation-change-indicator id="recurrenceChangeIndicator" hidden="hidden"/>
</td>
</tr>
<tr>
<th data-l10n-id="calendar-invitation-panel-prop-title-location"></th>
<td id="location" class="content"></td>
<td id="location" class="content">
<calendar-invitation-change-indicator id="locationChangeIndicator" hidden="hidden"/>
</td>
</tr>
<tr>
<th data-l10n-id="calendar-invitation-panel-prop-title-attendees"></th>
<td>
<calendar-invitation-partstat-summary id="summary"/>
<calendar-invitation-attendee-list id="attendees"/>
<ul id="attendees"
is="calendar-invitation-panel-attendee-list"
class="calendar-invitation-panel-list"></ul>
</td>
</tr>
<tr>
<th data-l10n-id="calendar-invitation-panel-prop-title-description"></th>
<td id="description" class="content"></td>
<td id="description" class="content">
<calendar-invitation-change-indicator id="descriptionChangeIndicator" hidden="hidden"/>
</td>
</tr>
<tr>
<th data-l10n-id="calendar-invitation-panel-prop-title-attachments"></th>
<td class="content"><calendar-invitation-panel-attachment-list id="attachments"/></td>
<td class="content">
<ul id="attachments"
is="calendar-invitation-panel-attachment-list"
class="calendar-invitation-panel-list"></ul>
</td>
</tr>
</tbody>
</table>
@ -158,21 +88,9 @@
</div>
</template>
<!-- Template for <calendar-invitation-attendees-list/> -->
<template id="calendarInvitationAttendeesList" xmlns="http://www.w3.org/1999/xhtml">
<ul id="list" class="calendar-invitation-list"></ul>
</template>
<!-- Template for <calendar-invitation-attachment-list/> -->
<template id="calendarInvitationAttachmentList" xmlns="http://www.w3.org/1999/xhtml">
<ul id="list" class="calendar-invitation-list"></ul>
</template>
<!-- Template for <calendar-invitation-panel-footer/> -->
<template id="calendarInvitationPanelFooter" xmlns="http://www.w3.org/1999/xhtml">
<div class="calendar-invitation-panel-details-footer">
<div id="status"
class="calendar-invitation-panel-reply-status"></div>
<div id="actionButtons" class="calendar-invitation-panel-response-buttons">
<button id="acceptButton"
name="accept"
@ -187,3 +105,6 @@
</div>
</div>
</template>
<!-- Menu for the "More" button in the invitation panel. Populated via JavaScript.-->
<menupopup id="calendarInvitationPanelMoreMenu"></menupopup>

Просмотреть файл

@ -9,7 +9,16 @@
border: 1px solid var(--splitter-color);
}
:host(calendar-invitation-panel-wrapper) {
:host(calendar-invitation-panel) .notificationbox-stack {
width: 100%;
padding: 10px 20px;
}
:host(calendar-invitation-panel) .notificationbox-stack > notification-message {
margin: 0;
}
.calendar-invitation-panel-wrapper {
display: flex;
padding: 10px 20px;
}
@ -20,32 +29,22 @@
flex-direction: column;
}
.calendar-invitation-panel-details-header {
.calendar-invitation-panel-title {
margin-bottom: 5px;
text-align: center;
}
.calendar-invitation-panel-summary {
display: flex;
align-items: baseline;
justify-content: center;
}
.calendar-invitation-panel-title {
.calendar-invitation-panel-title > h1 {
font-size: 1.4rem;
font-weight: 800;
margin: 0 1em;
flex-grow: 2;
align-self: flex-end;
display: inline-block;
margin: 0;
}
.calendar-invitation-panel-action-button {
margin: 0;
.calendar-invitation-panel-response-buttons {
margin-inline-start: auto;
}
.calendar-invitation-panel-action-button,
.calendar-invitation-panel-response-buttons button {
min-height: 2.5em;
font-size: .8em;
@ -58,15 +57,6 @@
padding: 10px 20px;
}
.calendar-invitation-panel-reply-status {
margin-inline-end: 2rem;
font-size: .8rem;
}
.calendar-invitation-panel-response-buttons {
margin-inline-start: auto;
}
.calendar-invitation-panel-props {
margin: 1.5em 0px;
}
@ -78,6 +68,7 @@
.calendar-invitation-panel-props th, .calendar-invitation-panel-props td {
vertical-align: top;
padding: 3px 0;
}
.calendar-invitation-panel-props th {
@ -91,9 +82,8 @@
word-break: break-word;
}
.calendar-invitation-when {
display: flex;
flex-direction: column;
calendar-invitation-interval {
display: inline-block;
}
.calendar-invitation-panel-partstat-breakdown:before {
@ -113,19 +103,33 @@
margin-inline-end: 0.5em;
}
.calendar-invitation-list {
.calendar-invitation-panel-list {
list-style: none;
padding: 0;
margin: 0;
}
.calendar-invitation-list li {
.calendar-invitation-panel-list li {
display: flex;
align-items: center;
gap: 3px;
}
.calendar-invitation-list li img {
.calendar-invitation-panel-list li img {
width: 16px;
height: 16px;
}
.calendar-invitation-panel-list li span.removed {
text-decoration: line-through;
}
calendar-invitation-change-indicator:not([hidden]) {
border-radius: var(--toolbarbutton-border-radius);
margin-inline-end: 3px;
margin-block: 2px;
padding-inline: 7px;
background-color: rgba(0, 0, 0, 0.1);
box-shadow: inset 0 0 0 1px transparent;
display: inline-block;
}

Просмотреть файл

@ -2,13 +2,17 @@
# 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/.
# Variables:
# $organizer (String) - The participant that created the original invitation.
calendar-invitation-panel-intro = { $organizer } has invited you to:
calendar-invitation-panel-status-new = You have been invited to this event.
# Variables:
# $organizer (String) - The participant that updated the original invitation.
calendar-invitation-panel-intro-update = { $organizer } has updated:
calendar-invitation-panel-status-processed = This event has already been added your calendar.
calendar-invitation-panel-status-updateminor = This message contains an update for this event.
calendar-invitation-panel-status-updatemajor = This message contains an update for this event. You should re-confirm your attendance.
calendar-invitation-panel-status-cancelled = This message contains a cancellation for this event.
calendar-invitation-panel-status-cancelled-notfound = This message contains a cancellation for an event not found on your calendar.
# Variables:
# $organizer (String) - The participant that cancelled the invitation.
@ -18,8 +22,6 @@ calendar-invitation-panel-intro-cancel = { $organizer } has cancelled:
# $summary (String) - A short summary or title of the event.
calendar-invitation-panel-title = { $summary }
calendar-invitation-panel-save-button = Save
calendar-invitation-panel-view-button = View
calendar-invitation-panel-update-button = Update
@ -32,7 +34,13 @@ calendar-invitation-panel-decline-button = No
calendar-invitation-panel-tentative-button = Maybe
calendar-invitation-panel-reply-status = * You have not decided or responded yet
calendar-invitation-panel-more-button = More
calendar-invitation-panel-menu-item-save =
.label = Save to calendar
calendar-invitation-panel-menu-item-toggle-changes=
.label = Show Changes
calendar-invitation-panel-prop-title-when = When:
@ -114,3 +122,9 @@ calendar-invitation-panel-partstat-needs-action = { $count } pending
calendar-invitation-panel-partstat-total = { $count } participants
calendar-invitation-panel-prop-title-attachments = Attachments:
calendar-invitation-change-indicator-removed = Removed
calendar-invitation-change-indicator-added = New
calendar-invitation-change-indicator-modified = Changed

Просмотреть файл

@ -35,13 +35,12 @@ async function checkABrowser(browser) {
let picker = doc.getElementById(browser.getAttribute("datetimepicker"));
Assert.ok(picker, "date/time picker exists");
// Click on the input box to open the popup.
// Open the popup.
let shownPromise = BrowserTestUtils.waitForEvent(picker, "popupshown");
await BrowserTestUtils.synthesizeMouseAtCenter(
`input[type="date"]`,
{},
browser
);
await SpecialPowers.spawn(browser, [], function() {
content.document.notifyUserGestureActivation();
content.document.querySelector(`input[type="date"]`).showPicker();
});
await shownPromise;
// Allow the picker time to initialise.
@ -50,7 +49,12 @@ async function checkABrowser(browser) {
// Click in the middle of the picker. This should always land on a date and
// close the picker.
let hiddenPromise = BrowserTestUtils.waitForEvent(picker, "popuphidden");
EventUtils.synthesizeMouseAtCenter(picker, {}, win);
let frame = picker.querySelector("#dateTimePopupFrame");
EventUtils.synthesizeMouseAtCenter(
frame.contentDocument.querySelector(".days-view td"),
{},
frame.contentWindow
);
await hiddenPromise;
// Check the date was assigned to the input.

Просмотреть файл

@ -1316,6 +1316,10 @@ toolbarpaletteitem[type="spring"][place="toolbar"] {
-moz-box-align: center;
}
toolbarpaletteitem[type="spring"][place="toolbar"] {
-moz-box-flex: 1000;
}
toolbarpaletteitem[type="spacer"][place="toolbar"] toolbarspacer,
toolbarpaletteitem[type="spring"][place="toolbar"] toolbarspring {
height: 16px;

Просмотреть файл

@ -1051,7 +1051,7 @@ calendar-notifications-setting .remove-button {
.synced-account .config-list {
gap: 0;
margin-inline-start: 21px
margin-inline-start: 21px;
}
.config-item {
@ -1061,7 +1061,7 @@ calendar-notifications-setting .remove-button {
}
.config-item label::before {
display: inline-block
display: inline-block;
height: 16px;
width: 16px;
margin-inline-end: 6px;