Bug 1683303 - Remove XUL equalsize from event attendee box. r=darktrojan,mkmelin

Replace the dynamic grid behaviour for the XUL box with a single column list.

Now, both the organizer labels and attendee labels use the same constructor, to keep styling consistent. And the list of attendees is represented by an unordered list <ul>, which is more semantically correct.

Also, calendar editing, summaries and email invitations all use the same constructors for attendees, which removes code duplication and keeps styling consistent. They also share the same styling sheets and localization.

In addition, the invitation document (lightning-invitation.xhtml) was rewritten to be more semantically correct and therefore more accessible. In particular, we no longer embed a table within another table's cell for the organizer row and attendees row (the unordered list is used in the latter case). Unnecessary paragraph <p> elements were removed from the cells. Row headers were marked as <th>. And the table header was moved to a <caption>.

Differential Revision: https://phabricator.services.mozilla.com/D111411

--HG--
extra : amend_source : aee820ea0e2a8c8d66cd9ca6045f1a825e3dc383
This commit is contained in:
Henry Wilkes 2021-04-28 13:56:33 +03:00
Родитель 8c2714ef44
Коммит 483cb63834
19 изменённых файлов: 681 добавлений и 918 удалений

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

@ -4,7 +4,7 @@
/* exported gInTab, gMainWindow, gTabmail, intializeTabOrWindowVariables, /* exported gInTab, gMainWindow, gTabmail, intializeTabOrWindowVariables,
* dispose, setDialogId, loadReminders, saveReminder, * dispose, setDialogId, loadReminders, saveReminder,
* commonUpdateReminder, updateLink, rearrangeAttendees, * commonUpdateReminder, updateLink,
* adaptScheduleAgent, sendMailToOrganizer, * adaptScheduleAgent, sendMailToOrganizer,
* openAttachmentFromItemSummary, * openAttachmentFromItemSummary,
*/ */
@ -572,168 +572,6 @@ function updateLink(itemUrlString, linkRow, urlLink) {
} }
} }
/**
* Set up attendees in event and summary dialog.
*
* @param {calIAttendee[]} attendees - The attendees.
* @param {Element} container - Element containing attendees rows, template, etc.
* @param {number} attendeesInRow - The number of attendees that can fit in each row.
* @param {number} maxLabelWidth - Maximum width of the label.
* @return {{attendeesInRow: number, maxLabelWidth: number}} The new values.
*/
function setupAttendees(attendees, container, attendeesInRow, maxLabelWidth) {
let attBox = container.querySelector(".item-attendees-box");
let attBoxRows = attBox.getElementsByClassName("item-attendees-row");
let newAttendeesInRow = attendeesInRow;
let newMaxLabelWidth = maxLabelWidth;
if (attendees && attendees.length > 0) {
// cloning of the template nodes
let row = container.querySelector(".item-attendees-box-template .item-attendees-row");
let clonedRow = row.cloneNode(false);
let clonedCell = row.querySelector("box:nth-of-type(1)").cloneNode(true);
let clonedSpacer = row.querySelector("box:nth-of-type(2)").cloneNode(false);
// determining of attendee box setup
let inRow = attendeesInRow || -1;
if (inRow == -1) {
inRow = determineAttendeesInRow(maxLabelWidth);
newAttendeesInRow = inRow;
} else {
while (attBoxRows.length > 0) {
attBox.removeChild(attBoxRows[0]);
}
}
// set up of the required nodes
let maxRows = Math.ceil(attendees.length / inRow);
let inLastRow = attendees.length - (maxRows - 1) * inRow;
let attCount = 0;
while (attBox.getElementsByClassName("item-attendees-row").length < maxRows) {
let newRow = clonedRow.cloneNode(false);
let row = attBox.appendChild(newRow);
row.removeAttribute("hidden");
let rowCount = attBox.getElementsByClassName("item-attendees-row").length;
let reqAtt = rowCount == maxRows ? inLastRow : inRow;
// we add as many attendee cells as required
while (row.children.length < reqAtt) {
let newCell = clonedCell.cloneNode(true);
let cell = row.appendChild(newCell);
let icon = cell.getElementsByTagName("img")[0];
let text = cell.getElementsByTagName("label")[0];
let attendee = attendees[attCount];
let label =
attendee.commonName && attendee.commonName.length
? attendee.commonName
: attendee.toString();
let userType = attendee.userType || "INDIVIDUAL";
let role = attendee.role || "REQ-PARTICIPANT";
let partstat = attendee.participationStatus || "NEEDS-ACTION";
icon.setAttribute("partstat", partstat);
icon.setAttribute("usertype", userType);
icon.setAttribute("role", role);
cell.setAttribute("attendeeid", attendee.id);
cell.removeAttribute("hidden");
let userTypeString = cal.l10n.getCalString("dialog.tooltip.attendeeUserType2." + userType, [
attendee.toString(),
]);
let roleString = cal.l10n.getCalString("dialog.tooltip.attendeeRole2." + role, [
userTypeString,
]);
let partstatString = cal.l10n.getCalString("dialog.tooltip.attendeePartStat2." + partstat, [
label,
]);
let tooltip = cal.l10n.getCalString("dialog.tooltip.attendee.combined", [
roleString,
partstatString,
]);
let del = cal.itip.resolveDelegation(attendee, attendees);
if (del.delegators != "") {
del.delegators = cal.l10n.getCalString("dialog.attendee.append.delegatedFrom", [
del.delegators,
]);
label += " " + del.delegators;
tooltip += " " + del.delegators;
}
if (del.delegatees != "") {
del.delegatees = cal.l10n.getCalString("dialog.attendee.append.delegatedTo", [
del.delegatees,
]);
tooltip += " " + del.delegatees;
}
text.setAttribute("value", label);
cell.setAttribute("tooltiptext", tooltip);
attCount++;
}
// we fill the row with placeholders if required
if (attBox.getElementsByClassName("item-attendees-row").length > 1 && inRow > 1) {
while (row.children.length < inRow) {
let newSpacer = clonedSpacer.cloneNode(true);
newSpacer.removeAttribute("hidden");
row.appendChild(newSpacer);
}
}
}
// determining of the max width of an attendee label - this needs to
// be done only once and is obsolete in case of resizing
if (!maxLabelWidth) {
let maxWidth = 0;
for (let cell of attBox.getElementsByClassName("item-attendees-cell")) {
cell = cell.cloneNode(true);
cell.removeAttribute("flex");
cell.getElementsByTagName("label")[0].removeAttribute("flex");
maxWidth = cell.clientWidth > maxWidth ? cell.clientWidth : maxWidth;
}
newMaxLabelWidth = maxWidth;
}
} else {
while (attBoxRows.length > 0) {
attBox.removeChild(attBoxRows[0]);
}
}
return { attendeesInRow: newAttendeesInRow, maxLabelWidth: newMaxLabelWidth };
}
/**
* Re-arranges the attendees on dialog resizing in event and summary dialog
*
* @param {calIAttendee[]} attendees - The attendees.
* @param {Element} parent - Element containing attendees rows, template, etc.
* @param {number} attendeesInRow - The number of attendees that can fit in each row.
* @param {number} maxLabelWidth - Maximum width of the label.
* @return {{attendeesInRow: number, maxLabelWidth: number}} The new values.
*/
function rearrangeAttendees(attendees, parent, attendeesInRow, maxLabelWidth) {
if (attendees && attendees.length > 0 && attendeesInRow) {
let inRow = determineAttendeesInRow(maxLabelWidth);
if (inRow != attendeesInRow) {
return setupAttendees(attendees, parent, inRow, maxLabelWidth);
}
}
return { attendeesInRow, maxLabelWidth };
}
/**
* Calculates the number of columns to distribute attendees for event and summary dialog
*
* @param {number} maxLabelWidth - The maximum width for the label.
* @return {number} The number of attendees that can fit in a row.
*/
function determineAttendeesInRow(maxLabelWidth) {
// as default value a reasonable high value is appropriate
// it will be recalculated anyway.
let minWidth = maxLabelWidth || 200;
let inRow = Math.floor(document.documentElement.clientWidth / minWidth);
return inRow > 1 ? inRow : 1;
}
/** /**
* Adapts the scheduling responsibility for caldav servers according to RfC 6638 * Adapts the scheduling responsibility for caldav servers according to RfC 6638
* based on forceEmailScheduling preference for the respective calendar * based on forceEmailScheduling preference for the respective calendar

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

@ -81,12 +81,6 @@ async function onWindowLoad() {
loadingMessage.remove(); loadingMessage.remove();
document.addEventListener("dialogaccept", importRemainingItems); document.addEventListener("dialogaccept", importRemainingItems);
window.addEventListener("resize", () => {
for (let summary of gModel.itemSummaries.values()) {
summary.onWindowResize();
}
});
}); });
} }
window.addEventListener("load", onWindowLoad); window.addEventListener("load", onWindowLoad);

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

@ -68,10 +68,6 @@ function onLoad() {
// Finish setting up the item summary custom element. // Finish setting up the item summary custom element.
itemSummary.updateItemDetails(); itemSummary.updateItemDetails();
window.addEventListener("resize", () => {
itemSummary.onWindowResize();
});
updateToolbar(); updateToolbar();
updateDialogButtons(item); updateDialogButtons(item);

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

@ -30,7 +30,10 @@
margin-inline: 0px; margin-inline: 0px;
} }
.calendar-summary-table .organizer-label,
.calendar-summary-table .attachments-label { .calendar-summary-table .attachments-label {
vertical-align: top; vertical-align: top;
} }
.item-attendees .item-attendees-list-container {
min-height: 48px;
}

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

@ -12,6 +12,7 @@
// Wrap in a block to prevent leaking to window scope. // Wrap in a block to prevent leaking to window scope.
{ {
var { ltn } = ChromeUtils.import("resource:///modules/calendar/ltnInvitationUtils.jsm");
var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
var { recurrenceStringFromItem } = ChromeUtils.import( var { recurrenceStringFromItem } = ChromeUtils.import(
"resource:///modules/calendar/calRecurrenceUtils.jsm" "resource:///modules/calendar/calRecurrenceUtils.jsm"
@ -82,17 +83,11 @@
<html:td class="item-category"> <html:td class="item-category">
</html:td> </html:td>
</html:tr> </html:tr>
<html:tr class="organizer-row item-attendees-row" hidden="hidden"> <html:tr class="item-organizer-row" hidden="hidden">
<html:th class="organizer-label"> <html:th>
&read.only.organizer.label; &read.only.organizer.label;
</html:th> </html:th>
<html:td> <html:td class="item-organizer-cell">
<hbox class="item-organizer-cell">
<img class="itip-icon"/>
<label class="item-organizer-label text-link item-attendees-cell-label"
crop="end"/>
<spacer flex="1"/>
</hbox>
</html:td> </html:td>
</html:tr> </html:tr>
<html:tr class="status-row" hidden="hidden"> <html:tr class="status-row" hidden="hidden">
@ -138,18 +133,6 @@
</html:td> </html:td>
</html:tr> </html:tr>
</html:table> </html:table>
<!-- attendee box template -->
<vbox class="item-attendees-box-template">
<hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true">
<box class="item-attendees-cell" hidden="true" flex="1">
<img class="itip-icon"/>
<label class="item-attendees-cell-label" crop="end" flex="1"/>
</box>
<box hidden="true" flex="1"/>
</hbox>
</vbox>
<!-- Attendees --> <!-- Attendees -->
<box class="item-attendees" orient="vertical" hidden="true" flex="1"> <box class="item-attendees" orient="vertical" hidden="true" flex="1">
<spacer class="default-spacer"/> <spacer class="default-spacer"/>
@ -158,7 +141,8 @@
class="header"/> class="header"/>
<separator class="groove" flex="1"/> <separator class="groove" flex="1"/>
</hbox> </hbox>
<vbox class="item-attendees-box" flex="1" /> <vbox class="item-attendees-list-container" flex="1">
</vbox>
</box> </box>
<!-- Description --> <!-- Description -->
@ -303,15 +287,8 @@
this.mReadOnly = true; this.mReadOnly = true;
this.mIsInvitation = false; this.mIsInvitation = false;
this.mAttendeesInRow = null;
this.mMaxLabelWidth = null;
this.mIsToDoItem = null; this.mIsToDoItem = null;
this.querySelector(".item-organizer-label").addEventListener("click", () => {
sendMailToOrganizer(this.mItem);
});
let urlLink = this.querySelector(".url-link"); let urlLink = this.querySelector(".url-link");
urlLink.addEventListener("click", event => { urlLink.addEventListener("click", event => {
launchBrowser(urlLink.getAttribute("href"), event); launchBrowser(urlLink.getAttribute("href"), event);
@ -491,7 +468,7 @@
} }
if (item.organizer && item.organizer.id) { if (item.organizer && item.organizer.id) {
this.updateOrganizer(item.organizer); this.updateOrganizer(item);
} }
let status = item.getProperty("STATUS"); let status = item.getProperty("STATUS");
@ -577,16 +554,9 @@
let attendees = item.getAttendees(); let attendees = item.getAttendees();
if (attendees && attendees.length) { if (attendees && attendees.length) {
this.querySelector(".item-attendees").removeAttribute("hidden"); this.querySelector(".item-attendees").removeAttribute("hidden");
this.querySelector(".item-attendees-list-container").appendChild(
let { attendeesInRow, maxLabelWidth } = setupAttendees( ltn.invitation.createAttendeesList(document, attendees)
attendees,
this.querySelector(".item-summary-box"),
this.mAttendeesInRow,
this.mMaxLabelWidth
); );
this.mAttendeesInRow = attendeesInRow;
this.mMaxLabelWidth = maxLabelWidth;
} }
} }
@ -618,60 +588,22 @@
} }
} }
/**
* Handle window resize event. Rearrange attendees.
*/
onWindowResize() {
let attendees = this.mItem.getAttendees();
if (attendees.length) {
let { attendeesInRow, maxLabelWidth } = rearrangeAttendees(
attendees,
this.querySelector(".item-summary-box"),
this.mAttendeesInRow,
this.mMaxLabelWidth
);
this.mAttendeesInRow = attendeesInRow;
this.mMaxLabelWidth = maxLabelWidth;
}
}
/** /**
* Update the organizer part of the UI. * Update the organizer part of the UI.
* *
* @param {calIAttendee} organizer - The organizer of the calendar item. * @param {calIItemBase} item - The calendar item.
*/ */
updateOrganizer(organizer) { updateOrganizer(item) {
this.querySelector(".organizer-row").removeAttribute("hidden"); this.querySelector(".item-organizer-row").removeAttribute("hidden");
let cell = this.querySelector(".item-organizer-cell"); let organizerLabel = ltn.invitation.createAttendeeLabel(
let text = cell.querySelector("label"); document,
let icon = cell.querySelector("img"); item.organizer,
item.getAttendees()
let role = organizer.role || "REQ-PARTICIPANT"; );
let userType = organizer.userType || "INDIVIDUAL"; let organizerName = organizerLabel.querySelector(".attendee-name");
let partstat = organizer.participationStatus || "NEEDS-ACTION"; organizerName.classList.add("text-link");
let orgName = organizerName.addEventListener("click", () => sendMailToOrganizer(this.mItem));
organizer.commonName && organizer.commonName.length this.querySelector(".item-organizer-cell").appendChild(organizerLabel);
? organizer.commonName
: organizer.toString();
let userTypeString = cal.l10n.getCalString("dialog.tooltip.attendeeUserType2." + userType, [
organizer.toString(),
]);
let roleString = cal.l10n.getCalString("dialog.tooltip.attendeeRole2." + role, [
userTypeString,
]);
let partstatString = cal.l10n.getCalString("dialog.tooltip.attendeePartStat2." + partstat, [
orgName,
]);
let tooltip = cal.l10n.getCalString("dialog.tooltip.attendee.combined", [
roleString,
partstatString,
]);
text.setAttribute("value", orgName);
cell.setAttribute("tooltiptext", tooltip);
icon.setAttribute("partstat", partstat);
icon.setAttribute("usertype", userType);
icon.setAttribute("role", role);
} }
/** /**

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

@ -15,15 +15,14 @@ html|input.textbox-addressingWidget:disabled {
opacity: 0.5; opacity: 0.5;
} }
.item-attendees-box { .item-attendees-list-container {
appearance: auto; appearance: auto;
-moz-default-appearance: listbox; -moz-default-appearance: listbox;
margin: 2px 4px 0; margin: 2px 4px 0;
overflow-y: auto; overflow-y: auto;
min-height: 54px; /*at least two rows - otherwise a scrollbar (if required) wouldn't appear*/
} }
:root[lwt-tree] .item-attendees-box { :root[lwt-tree] .item-attendees-list-container {
appearance: none; appearance: none;
background-color: var(--field-background-color); background-color: var(--field-background-color);
color: var(--field-text-color); color: var(--field-text-color);
@ -31,40 +30,36 @@ html|input.textbox-addressingWidget:disabled {
scrollbar-color: rgba(204, 204, 204, 0.5) rgba(230, 230, 235, 0.5); scrollbar-color: rgba(204, 204, 204, 0.5) rgba(230, 230, 235, 0.5);
} }
:root[lwt-tree-brighttext] .item-attendees-box { :root[lwt-tree-brighttext] .item-attendees-list-container {
scrollbar-color: rgba(249, 249, 250, 0.4) rgba(20, 20, 25, 0.3); scrollbar-color: rgba(249, 249, 250, 0.4) rgba(20, 20, 25, 0.3);
} }
#calendar-summary-dialog .item-attendees, .attendee-list {
#calendar-event-summary-dialog .item-attendees, display: block;
#calendar-task-summary-dialog .item-attendees { padding: 0;
max-height: 135px; /* displays up to four rows of attendees*/ margin: 0;
} }
.item-attendees-cell { .attendee-list-item {
padding: 2px; display: contents;
} }
#calendar-event-dialog-inner .item-attendees-cell { .attendee-label {
-moz-user-focus: normal; padding: 2px;
margin-bottom: 1px; display: flex;
margin-inline-end: 1px; align-items: baseline;
} }
#calendar-event-dialog-inner .item-attendees-cell:focus { .itip-icon {
background-color: Highlight; flex: 0 0 auto;
color: Highlighttext;
} }
.item-attendees-cell-label { .attendee-name {
border: 0; margin: 0 3px;
margin: 0 3px; flex: 0 1 auto;
padding: 0; overflow: hidden;
} text-overflow: ellipsis;
white-space: nowrap;
.item-organizer-cell {
padding: 0;
margin-left: 6px;
} }
/* this is for the itip icon setup in calendar */ /* this is for the itip icon setup in calendar */
@ -75,7 +70,6 @@ html|input.textbox-addressingWidget:disabled {
--itip-icon-usertype: -32px; /* default: INDIVIDUAL */ --itip-icon-usertype: -32px; /* default: INDIVIDUAL */
width: 16px; width: 16px;
height: 16px; height: 16px;
max-height: 16px;
background-image: url(chrome://calendar/skin/shared/calendar-itip-icons.svg), background-image: url(chrome://calendar/skin/shared/calendar-itip-icons.svg),
url(chrome://calendar/skin/shared/calendar-itip-icons.svg); url(chrome://calendar/skin/shared/calendar-itip-icons.svg);
background-position: var(--itip-icon-partstat), var(--itip-icon-usertype) var(--itip-icon-role); background-position: var(--itip-icon-partstat), var(--itip-icon-usertype) var(--itip-icon-role);

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

@ -322,8 +322,9 @@ label.label {
margin: 2px 4px; margin: 2px 4px;
} }
#event-grid-tabpanel-attendees > vbox > hbox > .item-attendees-box { #calendar-event-dialog-inner .attendee-label:focus {
margin: 2px 4px; background-color: Highlight;
color: Highlighttext;
} }
/*-------------------------------------------------------------------- /*--------------------------------------------------------------------

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

@ -2,115 +2,67 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
.invitation-table .itip-icon { .invitation-border {
--itip-icon-partstat: -16px -16px; /* default: NEEDS-ACTION */ border: 3px solid -moz-default-color;
--itip-icon-role: 0px; /* default: REQ-PARTICIPANT */ margin-inline-start: auto;
--itip-icon-usertype: -32px; /* default: INDIVIDUAL */ margin-inline-end: auto;
width: 16px; width: -moz-fit-content;
height: 16px; height: -moz-fit-content;
background-image: url(chrome://calendar/skin/shared/calendar-itip-icons.svg),
url(chrome://calendar/skin/shared/calendar-itip-icons.svg);
background-position: var(--itip-icon-partstat), var(--itip-icon-usertype) var(--itip-icon-role);
}
.invitation-table .itip-icon[partstat="ACCEPTED"] {
--itip-icon-partstat: 0px 0px;
}
.invitation-table .itip-icon[partstat="DECLINED"] {
--itip-icon-partstat: 0px -16px;
}
.invitation-table .itip-icon[partstat="DELEGATED"] {
--itip-icon-partstat: 0px -32px;
}
.invitation-table .itip-icon[partstat="TENTATIVE"] {
--itip-icon-partstat: -16px 0px;
}
.invitation-table .itip-icon[usertype="INDIVIDUAL"] {
--itip-icon-usertype: -32px;
}
.invitation-table .itip-icon[usertype="GROUP"] {
--itip-icon-usertype: -48px;
}
.invitation-table .itip-icon[usertype="RESOURCE"] {
--itip-icon-usertype: -64px;
}
.invitation-table .itip-icon[usertype="ROOM"] {
--itip-icon-usertype: -80px;
}
.invitation-table .itip-icon[usertype="UNKNOWN"] {
--itip-icon-usertype: -96px;
}
.invitation-table .itip-icon[role="REQ-PARTICIPANT"] {
--itip-icon-role: 0px;
}
.invitation-table .itip-icon[role="OPT-PARTICIPANT"] {
--itip-icon-role: -16px;
}
.invitation-table .itip-icon[role="NON-PARTICIPANT"] {
--itip-icon-role: -32px;
}
.invitation-table .itip-icon[role="CHAIR"] {
--itip-icon-role: -32px;
--itip-icon-usertype: -16px;
}
#imipHtml-attendees-row > .content,
#imipHtml-organizer-row > .content,
#attendee-table > tbody > tr > td,
#organizer-table > tbody > tr > td {
padding: 0
} }
.invitation-table { .invitation-table {
border: 3px solid -moz-default-color;
border-collapse: collapse; border-collapse: collapse;
width: 40em; width: 40em;
margin-inline-start: auto;
margin-inline-end: auto;
}
.invitation-table > tbody > tr > td {
padding: 3px;
vertical-align: top;
width: 2em;
text-align: left;
} }
.invitation-table .header { .invitation-table .header {
padding: 3px;
color: HighlightText; color: HighlightText;
font-size: 1em; font-size: 1em;
font-weight: bold; font-weight: bold;
background-color: Highlight; background-color: Highlight;
} }
.invitation-table :is(td, th) {
padding: 3px;
vertical-align: baseline;
}
.invitation-table .description { .invitation-table .description {
width: 9em; width: 9em;
text-align: right; text-align: end;
font-weight: normal;
border-inline-end: 1px solid hsla(0, 0%, 50%, .2); border-inline-end: 1px solid hsla(0, 0%, 50%, .2);
background-color: hsla(0, 0%, 50%, .2); background-color: hsla(0, 0%, 50%, .2);
vertical-align: top;
} }
.invitation-table .content { .invitation-table .content {
width: 29em; width: 29em;
} }
.invitation-table .content p {
white-space: pre-wrap;
}
.invitation-table .added { .invitation-table .added {
color: rgb(255, 0, 0); color: rgb(255, 0, 0);
text-decoration-line: none;
} }
.invitation-table .added[systemcolors] { .invitation-table .added[systemcolors] {
color: currentColor; color: currentColor;
font-weight: bold; font-weight: bold;
} }
.invitation-table .modified { .invitation-table .modified {
color: rgb(255, 0, 0); color: rgb(255, 0, 0);
font-style: italic; font-style: italic;
} }
.invitation-table .modified[systemcolors] { .invitation-table .modified[systemcolors] {
color: currentColor; color: currentColor;
} }
.invitation-table .removed { .invitation-table .removed {
color: rgb(125, 125, 125); color: rgb(125, 125, 125);
text-decoration: line-through;
} }
.invitation-table .removed[systemcolors] { .invitation-table .removed[systemcolors] {
color: currentColor; color: currentColor;
} }

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

@ -304,22 +304,13 @@ var ltnImipBar = {
ltnImipBar.itipItem ltnImipBar.itipItem
); );
let serializedOverlay = cal.xml.serializeDOM(foundOverlay); let serializedOverlay = cal.xml.serializeDOM(foundOverlay);
let organizerId = ltnImipBar.itipItem.targetCalendar.getProperty("organizerId");
if (diff == 1) { if (diff == 1) {
// this is an update to previously accepted invitation // this is an update to previously accepted invitation
msgOverlay = ltn.invitation.compareInvitationOverlay( msgOverlay = ltn.invitation.compareInvitationOverlay(serializedOverlay, msgOverlay);
serializedOverlay,
msgOverlay,
organizerId
);
} else { } else {
// this is a copy of a previously sent out invitation or a previous revision of a // this is a copy of a previously sent out invitation or a previous revision of a
// meanwhile accepted invitation, so we flip comparison order // meanwhile accepted invitation, so we flip comparison order
msgOverlay = ltn.invitation.compareInvitationOverlay( msgOverlay = ltn.invitation.compareInvitationOverlay(msgOverlay, serializedOverlay);
msgOverlay,
serializedOverlay,
organizerId
);
} }
} }
msgWindow.displayHTMLInMessagePane("", msgOverlay, false); msgWindow.displayHTMLInMessagePane("", msgOverlay, false);

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

@ -8,67 +8,74 @@
<head> <head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'/> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
<link rel='stylesheet' type='text/css' href='chrome://messagebody/skin/imip.css'/> <link rel='stylesheet' type='text/css' href='chrome://messagebody/skin/imip.css'/>
<link rel='stylesheet' type='text/css' href='chrome://messagebody/skin/calendar-attendees.css'/>
</head> </head>
<body> <body>
<table class="invitation-table"> <div class="invitation-border">
<tr id="imipHtml-header-row"> <table class="invitation-table">
<th colspan="2" class="header"> <caption id="imipHtml-header" class="header"></caption>
<p id="imipHtml-header-descr" class="header"/> <tr id="imipHtml-summary-row" hidden="hidden">
</th> <th id="imipHtml-summary-descr" class="description" scope="row"></th>
</tr> <td id="imipHtml-summary-content" class="content"></td>
<tr id="imipHtml-summary-row" hidden="true"> </tr>
<td class="description"><p id="imipHtml-summary-descr"/></td> <tr id="imipHtml-location-row" hidden="hidden">
<td class="content"><p id="imipHtml-summary-content"/></td> <th id="imipHtml-location-descr" class="description" scope="row"></th>
</tr> <td id="imipHtml-location-content" class="content"></td>
<tr id="imipHtml-location-row" hidden="true"> </tr>
<td class="description"><p id="imipHtml-location-descr"/></td> <tr id="imipHtml-when-row" hidden="hidden">
<td class="content"><p id="imipHtml-location-content"/></td> <th id="imipHtml-when-descr" class="description" scope="row"></th>
</tr> <td id="imipHtml-when-content" class="content"></td>
<tr id="imipHtml-when-row" hidden="true"> </tr>
<td class="description"><p id="imipHtml-when-descr"/></td> <tr id="imipHtml-canceledOccurrences-row" hidden="hidden">
<td class="content"><p id="imipHtml-when-content"/></td> <th id="imipHtml-canceledOccurrences-descr"
</tr> class="description"
<tr id="imipHtml-canceledOccurrences-row" hidden="true"> scope="row">
<td class="description"><p id="imipHtml-canceledOccurrences-descr"/></td> </th>
<td class="content"><p id="imipHtml-canceledOccurrences-content"/></td> <td id="imipHtml-canceledOccurrences-content" class="content"></td>
</tr> </tr>
<tr id="imipHtml-modifiedOccurrences-row" hidden="true"> <tr id="imipHtml-modifiedOccurrences-row" hidden="hidden">
<td class="description"><p id="imipHtml-modifiedOccurrences-descr"/></td> <th id="imipHtml-modifiedOccurrences-descr"
<td class="content"><p id="imipHtml-modifiedOccurrences-content"/></td> class="description"
</tr> scope="row">
<tr id="imipHtml-organizer-row" hidden="true"> </th>
<td class="description"><p id="imipHtml-organizer-descr"/></td> <td id="imipHtml-modifiedOccurrences-content" class="content"></td>
<td class="content"> </tr>
<table id="organizer-table"/> <tr id="imipHtml-organizer-row" hidden="hidden">
</td> <th id="imipHtml-organizer-descr"
</tr> class="description"
<tr id="imipHtml-description-row" hidden="true"> scope="row">
<td class="description"><p id="imipHtml-description-descr"/></td> </th>
<td class="content"><p id="imipHtml-description-content"/></td> <td id="imipHtml-organizer-cell" class="content"></td>
</tr> </tr>
<tr id="imipHtml-attachments-row" hidden="true"> <tr id="imipHtml-description-row" hidden="hidden">
<td class="description"><p id="imipHtml-attachments-descr"/></td> <th id="imipHtml-description-descr"
<td class="content"><p id="imipHtml-attachments-content"/></td> class="description"
</tr> scope="row">
<tr id="imipHtml-comment-row" hidden="true"> </th>
<td class="description"><p id="imipHtml-comment-descr"/></td> <td id="imipHtml-description-content" class="content"></td>
<td class="content"><p id="imipHtml-comment-content"/></td> </tr>
</tr> <tr id="imipHtml-attachments-row" hidden="hidden">
<tr id="imipHtml-attendees-row" hidden="true"> <th id="imipHtml-attachments-descr"
<td class="description"><p id="imipHtml-attendees-descr"/></td> class="description"
<td class="content"> scope="row"></th>
<table id="attendee-table"> <td id="imipHtml-attachments-content" class="content"></td>
<tr id="attendee-template" hidden="true"> </tr>
<td><p class="itip-icon"/></td> <tr id="imipHtml-comment-row" hidden="hidden">
<td class="attendee-name"/> <th id="imipHtml-comment-descr" class="description" scope="row"></th>
</tr> <td id="imipHtml-comment-content" class="content"></td>
</table> </tr>
</td> <tr id="imipHtml-attendees-row" hidden="hidden">
</tr> <th id="imipHtml-attendees-descr"
<tr id="imipHtml-url-row" hidden="true"> class="description"
<td class="description"><p id="imipHtml-url-descr"/></td> scope="row">
<td class="content"><p id="imipHtml-url-content"/></td> </th>
</tr> <td id="imipHtml-attendees-cell" class="content"></td>
</table> </tr>
<tr id="imipHtml-url-row" hidden="hidden">
<th id="imipHtml-url-descr" class="description" scope="row"></th>
<td id="imipHtml-url-content" class="content"></td>
</tr>
</table>
</div>
</body> </body>
</html> </html>

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

@ -22,6 +22,7 @@
/* globals gTimezonesEnabled, gShowLink */ // Set by lightning-item-panel.js. /* globals gTimezonesEnabled, gShowLink */ // Set by lightning-item-panel.js.
var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
var { ltn } = ChromeUtils.import("resource:///modules/calendar/ltnInvitationUtils.jsm");
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { var {
recurrenceRule2String, recurrenceRule2String,
@ -387,18 +388,6 @@ function onLoad() {
} }
} }
window.addEventListener("resize", () => {
let { attendeesInRow, maxLabelWidth } = rearrangeAttendees(
window.attendees,
document,
window.attendeesInRow,
window.maxLabelWidth
);
window.attendeesInRow = attendeesInRow;
window.maxLabelWidth = maxLabelWidth;
});
// we store the recurrence info in the window so it // we store the recurrence info in the window so it
// can be accessed from any location. since the recurrence // can be accessed from any location. since the recurrence
// info is a property of the parent item we need to check // info is a property of the parent item we need to check
@ -3860,53 +3849,29 @@ function updateAttendees() {
attendeePanel.removeAttribute("collapsed"); attendeePanel.removeAttribute("collapsed");
notifyOptions.removeAttribute("collapsed"); notifyOptions.removeAttribute("collapsed");
let organizerRow = document.getElementById("item-organizer-row");
if (window.organizer && window.organizer.id) { if (window.organizer && window.organizer.id) {
let organizer = window.organizer; let existingLabel = organizerRow.querySelector(":scope > .attendee-label");
document.getElementById("item-organizer-row").removeAttribute("collapsed"); if (existingLabel) {
let cell = document.querySelector(".item-organizer-cell"); organizerRow.removeChild(existingLabel);
let icon = cell.querySelector("img:nth-of-type(1)"); }
let text = cell.querySelector("label:nth-of-type(1)"); organizerRow.appendChild(
ltn.invitation.createAttendeeLabel(document, window.organizer, window.attendees)
let role = organizer.role || "REQ-PARTICIPANT"; );
let userType = organizer.userType || "INDIVIDUAL"; organizerRow.hidden = false;
let partStat = organizer.participationStatus || "NEEDS-ACTION";
let orgName =
organizer.commonName && organizer.commonName.length
? organizer.commonName
: organizer.toString();
let userTypeString = cal.l10n.getCalString("dialog.tooltip.attendeeUserType2." + userType, [
organizer.toString(),
]);
let roleString = cal.l10n.getCalString("dialog.tooltip.attendeeRole2." + role, [
userTypeString,
]);
let partStatString = cal.l10n.getCalString("dialog.tooltip.attendeePartStat2." + partStat, [
orgName,
]);
let tooltip = cal.l10n.getCalString("dialog.tooltip.attendee.combined", [
roleString,
partStatString,
]);
text.setAttribute("value", orgName);
cell.setAttribute("tooltiptext", tooltip);
icon.setAttribute("partstat", partStat);
icon.setAttribute("usertype", userType);
icon.setAttribute("role", role);
} else { } else {
document.getElementById("item-organizer-row").collapsed = true; organizerRow.hidden = true;
} }
let { attendeesInRow, maxLabelWidth } = setupAttendees( let attendeeContainer = document.querySelector(".item-attendees-list-container");
window.attendees, if (attendeeContainer.firstChild) {
document, attendeeContainer.firstChild.remove();
window.attendeesInRow, }
window.maxLabelWidth attendeeContainer.appendChild(ltn.invitation.createAttendeesList(document, window.attendees));
); for (let label of attendeeContainer.querySelectorAll(".attendee-label")) {
label.addEventListener("dblclick", attendeeDblClick);
window.attendeesInRow = attendeesInRow; label.setAttribute("tabindex", "0");
window.maxLabelWidth = maxLabelWidth; }
// update the attendee tab label to make the number of attendees // update the attendee tab label to make the number of attendees
// visible even if another tab is displayed // visible even if another tab is displayed

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

@ -642,26 +642,15 @@
<tabpanel id="event-grid-tabpanel-attendees" <tabpanel id="event-grid-tabpanel-attendees"
collapsed="true"> collapsed="true">
<vbox flex="1"> <vbox flex="1">
<hbox id="item-organizer-row" <hbox id="item-organizer-row" hidden="true" align="start">
collapsed="true"
align="start"
class="item-attendees-row">
<label value="&read.only.organizer.label;"/> <label value="&read.only.organizer.label;"/>
<hbox class="item-organizer-cell">
<img class="itip-icon"/>
<label id="item-organizer"
class="item-attendees-cell-label"
crop="right"/>
</hbox>
</hbox>
<hbox flex="1">
<vbox class="item-attendees-box"
dialog-type="event"
flex="1"
context="attendee-popup"
oncontextmenu="setAttendeeContext(event)"
disable-on-readonly="true"/>
</hbox> </hbox>
<vbox class="item-attendees-list-container"
dialog-type="event"
flex="1"
context="attendee-popup"
oncontextmenu="setAttendeeContext(event)"
disable-on-readonly="true"/>
</vbox> </vbox>
</tabpanel> </tabpanel>
</tabpanels> </tabpanels>
@ -756,23 +745,4 @@
oncommand="this.parentNode.editTimezone()"/> oncommand="this.parentNode.editTimezone()"/>
</menupopup> </menupopup>
</popupset> </popupset>
<!-- attendee box template -->
<vbox class="item-attendees-box-template"
hidden="true">
<hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true">
<box class="item-attendees-cell"
hidden="true"
flex="1"
context="attendee-popup"
ondblclick="attendeeDblClick(event)"
oncontextmenu="setAttendeeContext(event)">
<img class="itip-icon"/>
<label class="item-attendees-cell-label"
crop="end"
flex="1"/>
</box>
<box hidden="true" flex="1"/>
</hbox>
</vbox>
</window> </window>

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

@ -5,6 +5,7 @@
lightning.jar: lightning.jar:
% override chrome://messagebody/skin/imip.css chrome://calendar/skin/imip.css % override chrome://messagebody/skin/imip.css chrome://calendar/skin/imip.css
% override chrome://messagebody/skin/calendar-attendees.css chrome://calendar/skin/shared/calendar-attendees.css
% override chrome://messagebody/skin/calendar-event-dialog-attendees.png chrome://calendar/skin/shared/calendar-event-dialog-attendees.png % override chrome://messagebody/skin/calendar-event-dialog-attendees.png chrome://calendar/skin/shared/calendar-event-dialog-attendees.png
% content lightning %content/ % content lightning %content/
content/html-item-editing/lightning-item-iframe.html (content/html-item-editing/lightning-item-iframe.html) content/html-item-editing/lightning-item-iframe.html (content/html-item-editing/lightning-item-iframe.html)

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

@ -67,6 +67,100 @@ ltn.invitation = {
return header; return header;
}, },
/**
* Creates new icon and text label for the given event attendee.
*
* @param {Document} doc - The document the new label will belong to.
* @param {calIAttendee} attendee - The attendee to create the label for.
* @param {calIAttendee[]} attendees - The full list of attendees for the
* event.
*
* @return {HTMLDivElement} - The new attendee label.
*/
createAttendeeLabel(doc, attendee, attendees) {
let userType = attendee.userType || "INDIVIDUAL";
let role = attendee.role || "REQ-PARTICIPANT";
let partstat = attendee.participationStatus || "NEEDS-ACTION";
// resolve delegatees/delegators to display also the CN
let del = cal.itip.resolveDelegation(attendee, attendees);
let userTypeString = cal.l10n.getLtnString("imipHtml.attendeeUserType2." + userType, [
attendee.toString(),
]);
let roleString = cal.l10n.getLtnString("imipHtml.attendeeRole2." + role, [userTypeString]);
let partstatString = cal.l10n.getLtnString("imipHtml.attendeePartStat2." + partstat, [
attendee.commonName || attendee.toString(),
del.delegatees,
]);
let tooltip = cal.l10n.getLtnString("imipHtml.attendee.combined", [roleString, partstatString]);
let name = attendee.toString();
if (del.delegators) {
name += " " + cal.l10n.getLtnString("imipHtml.attendeeDelegatedFrom", [del.delegators]);
}
let attendeeLabel = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
attendeeLabel.classList.add("attendee-label");
// NOTE: tooltip will not appear when the top level is XUL.
attendeeLabel.setAttribute("title", tooltip);
attendeeLabel.setAttribute("attendeeid", attendee.id);
// FIXME: Replace icon with an img element with src and alt. The current
// problem is that the icon image is set in CSS on the itip-icon class
// with a background image that changes with the role attribute. This is
// generally inaccessible (see Bug 1702560).
let icon = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
icon.classList.add("itip-icon");
icon.setAttribute("partstat", partstat);
icon.setAttribute("usertype", userType);
icon.setAttribute("role", role);
attendeeLabel.appendChild(icon);
let text = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
text.classList.add("attendee-name");
text.appendChild(doc.createTextNode(name));
attendeeLabel.appendChild(text);
return attendeeLabel;
},
/**
* Create an new list item element for an attendee, to be used as a child of
* an "attendee-list" element.
* @param {Document} doc - The document the new list item will belong to.
* @param {Element} attendeeLabel - The attendee label to place within the
* list item.
*
* return {HTMLLIElement} - The attendee list item.
*/
createAttendeeListItem(doc, attendeeLabel) {
let listItem = doc.createElementNS("http://www.w3.org/1999/xhtml", "li");
listItem.classList.add("attendee-list-item");
listItem.appendChild(attendeeLabel);
return listItem;
},
/**
* Creates a new element that lists the given attendees.
*
* @param {Document} doc - The document the new list will belong to.
* @param {calIAttendee[]} attendees - The attendees to create the list for.
*
* @return {HTMLUListElement} - The list of attendees.
*/
createAttendeesList(doc, attendees) {
let list = doc.createElementNS("http://www.w3.org/1999/xhtml", "ul");
list.classList.add("attendee-list");
for (let attendee of attendees) {
list.appendChild(
this.createAttendeeListItem(doc, this.createAttendeeLabel(doc, attendee, attendees))
);
}
return list;
},
/** /**
* Returns the html representation of the event as a DOM document. * Returns the html representation of the event as a DOM document.
* *
@ -98,7 +192,7 @@ ltn.invitation = {
}; };
// Simple fields // Simple fields
let headerDescr = doc.getElementById("imipHtml-header-descr"); let headerDescr = doc.getElementById("imipHtml-header");
if (headerDescr) { if (headerDescr) {
headerDescr.textContent = ltn.invitation.getItipHeader(aItipItem); headerDescr.textContent = ltn.invitation.getItipHeader(aItipItem);
} }
@ -204,66 +298,20 @@ ltn.invitation = {
field("attachments", links.join("<br>"), true); field("attachments", links.join("<br>"), true);
// ATTENDEE and ORGANIZER fields // ATTENDEE and ORGANIZER fields
let organizerCell = doc.getElementById("imipHtml-organizer-cell");
let attendeeCell = doc.getElementById("imipHtml-attendees-cell");
let attendees = aEvent.getAttendees(); let attendees = aEvent.getAttendees();
let attendeeTemplate = doc.getElementById("attendee-template");
let attendeeTable = doc.getElementById("attendee-table");
let organizerTable = doc.getElementById("organizer-table");
doc.getElementById("imipHtml-attendees-row").hidden = attendees.length < 1; doc.getElementById("imipHtml-attendees-row").hidden = attendees.length < 1;
doc.getElementById("imipHtml-organizer-row").hidden = !aEvent.organizer; doc.getElementById("imipHtml-organizer-row").hidden = !aEvent.organizer;
let setupAttendee = function(aAttendee) { field("organizer");
let row = attendeeTemplate.cloneNode(true); if (aEvent.organizer) {
row.removeAttribute("id"); organizerCell.appendChild(this.createAttendeeLabel(doc, aEvent.organizer, attendees));
row.removeAttribute("hidden"); }
// resolve delegatees/delegators to display also the CN
let del = cal.itip.resolveDelegation(aAttendee, attendees);
if (del.delegators != "") {
del.delegators =
" " + cal.l10n.getLtnString("imipHtml.attendeeDelegatedFrom", [del.delegators]);
}
// display itip icon
let role = aAttendee.role || "REQ-PARTICIPANT";
let partstat = aAttendee.participationStatus || "NEEDS-ACTION";
let userType = aAttendee.userType || "INDIVIDUAL";
let itipIcon = row.getElementsByClassName("itip-icon")[0];
itipIcon.setAttribute("role", role);
itipIcon.setAttribute("usertype", userType);
itipIcon.setAttribute("partstat", partstat);
let attName =
aAttendee.commonName && aAttendee.commonName.length
? aAttendee.commonName
: aAttendee.toString();
let userTypeString = cal.l10n.getLtnString("imipHtml.attendeeUserType2." + userType, [
aAttendee.toString(),
]);
let roleString = cal.l10n.getLtnString("imipHtml.attendeeRole2." + role, [userTypeString]);
let partstatString = cal.l10n.getLtnString("imipHtml.attendeePartStat2." + partstat, [
attName,
del.delegatees,
]);
let itipTooltip = cal.l10n.getLtnString("imipHtml.attendee.combined", [
roleString,
partstatString,
]);
row.setAttribute("title", itipTooltip);
// display attendee
row.getElementsByClassName("attendee-name")[0].textContent =
aAttendee.toString() + del.delegators;
return row;
};
// Fill rows for attendees and organizer // Fill rows for attendees and organizer
field("attendees"); field("attendees");
for (let attendee of attendees) { attendeeCell.appendChild(this.createAttendeesList(doc, attendees));
attendeeTable.appendChild(setupAttendee(attendee));
}
field("organizer");
if (aEvent.organizer) {
organizerTable.appendChild(setupAttendee(aEvent.organizer));
}
return doc; return doc;
}, },
@ -272,143 +320,268 @@ ltn.invitation = {
* Expects and return a serialized DOM - use cal.xml.serializeDOM(aDOM) * Expects and return a serialized DOM - use cal.xml.serializeDOM(aDOM)
* @param {String} aOldDoc serialized DOM of the the old document * @param {String} aOldDoc serialized DOM of the the old document
* @param {String} aNewDoc serialized DOM of the the new document * @param {String} aNewDoc serialized DOM of the the new document
* @param {String} aIgnoreId attendee id to ignore, usually the organizer
* @return {String} updated serialized DOM of the new document * @return {String} updated serialized DOM of the new document
*/ */
compareInvitationOverlay(aOldDoc, aNewDoc, aIgnoreId) { compareInvitationOverlay(aOldDoc, aNewDoc) {
let systemColors = Services.prefs.getBoolPref("calendar.view.useSystemColors", false);
/** /**
* Transforms text node content to formatted child nodes. Decorations are defined in imip.css * Add a styling class to the given element.
* @param {Node} aToNode text node to change *
* @param {String} aType use 'newline' for the same, 'added' or 'removed' for decoration * @param {Element} el - The element to add the class to.
* @param {String} aText [optional] * @param {string} className - The name of the styling class to add.
* @param {Boolean} aClear [optional] for consecutive changes on the same node, set to false
*/ */
function _content2Child(aToNode, aType, aText = "", aClear = true) { function _addStyleClass(el, className) {
let nodeDoc = aToNode.ownerDocument; el.classList.add(className);
if (aClear) { el.toggleAttribute("systemcolors", systemColors);
while (aToNode.lastChild) { }
aToNode.lastChild.remove();
/**
* Extract the elements from an element and place them within a new element
* that represents a change in content.
*
* @param {Element} el - The element to extract content from. This will be
* empty after the method returns.
* @param {string} change - The change that the returned element should
* represent.
*
* @return {HTMLModElement} - A new container for the previous content of
* the element. It will be styled and semantically tagged according to the
* given change.
*/
function _extractChangedContent(el, change) {
// Static list of children, including text nodes.
let nodeDoc = el.ownerDocument;
let children = Array.from(el.childNodes);
let wrapper;
if (change === "removed") {
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "del");
} else {
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "ins");
}
_addStyleClass(wrapper, change);
for (let child of children) {
el.removeChild(child);
wrapper.appendChild(child);
}
return wrapper;
}
/**
* Compares a row across the two documents. The row in the new document will
* be shown if the row was shown in either document. Otherwise, it will
* remain hidden.
*
* @param {Document} doc - The current document.
* @param {Document} oldDoc - The old document to compare against.
* @param {String} rowId - The id for the row to compare.
* @param {Function} removedCallback - Method to call if the row is hidden
* in the current document, but shown in the old document.
* @param {Function} addedCallback - Method to call if the row is shown
* in the current document, but hidden in the old document.
* @param {Function} modifiedCallback - Method to call if the row is shown
* in both documents.
*/
function _compareRows(doc, oldDoc, rowId, removedCallback, addedCallback, modifiedCallback) {
let oldRow = oldDoc.getElementById(rowId);
let row = doc.getElementById(rowId);
if (row.hidden && !oldRow.hidden) {
removedCallback();
row.hidden = false;
} else if (!row.hidden && oldRow.hidden) {
addedCallback();
} else if (!row.hidden && !oldRow.hidden) {
modifiedCallback();
}
}
/**
* Compares content across the two documents. The content of the new
* document will be modified to reflect the changes.
*
* @param {Document} doc - The current document (which will be modified).
* @param {Document} oldDoc - The old document to compare against.
* @param {String} rowId - The id for the row that contains the content.
* @param {String} contentId - The id for the content element.
*/
function _compareContent(doc, oldDoc, rowId, contentId) {
let content = doc.getElementById(contentId);
let oldContent = oldDoc.getElementById(contentId);
_compareRows(
doc,
oldDoc,
rowId,
// Removed row.
() => {
let removed = _extractChangedContent(oldContent, "removed");
while (content.lastChild) {
content.lastChild.remove();
}
content.appendChild(removed);
},
// Added row.
() => {
let added = _extractChangedContent(content, "added");
content.appendChild(added);
},
// Modified row.
() => {
if (content.textContent !== oldContent.textContent) {
let added = _extractChangedContent(content, "added");
let removed = _extractChangedContent(oldContent, "removed");
content.appendChild(added);
content.appendChild(doc.createElementNS("http://www.w3.org/1999/xhtml", "br"));
content.appendChild(removed);
}
}
);
}
let oldDoc = cal.xml.parseString(aOldDoc);
let doc = cal.xml.parseString(aNewDoc);
// elements to consider for comparison
[
["imipHtml-summary-row", "imipHtml-summary-content"],
["imipHtml-location-row", "imipHtml-location-content"],
["imipHtml-when-row", "imipHtml-when-content"],
["imipHtml-canceledOccurrences-row", "imipHtml-canceledOccurrences-content"],
["imipHtml-modifiedOccurrences-row", "imipHtml-modifiedOccurrences-content"],
].forEach(ids => _compareContent(doc, oldDoc, ids[0], ids[1]));
/**
* Relate two attendee labels.
*
* @param {Element} attendeeLabel - An attendee label.
* @param {Element} otherAttendeeLabel - Another attendee label to compare
* against.
*
* @return {string} - The relation between the two labels:
* "different" if the attendee names differ,
* "modified" if the attendance details differ,
* "same" otherwise.
*/
function _attendeeDiff(attendeeLabel, otherAttendeeLabel) {
if (attendeeLabel.textContent !== otherAttendeeLabel.textContent) {
return "different";
}
let otherIcon = otherAttendeeLabel.querySelector(".itip-icon");
let icon = attendeeLabel.querySelector(".itip-icon");
for (let attr of ["role", "partstat", "usertype"]) {
if (icon.getAttribute(attr) !== otherIcon.getAttribute(attr)) {
return "modified";
} }
} }
return "same";
}
let n = nodeDoc.createElement(aType.toLowerCase() == "newline" ? "br" : "span"); /**
switch (aType) { * Wrap the given element in-place to describe the given change.
case "added": * The wrapper will semantically and/or stylistically describe the change.
case "modified": *
* @param {Element} - The element to wrap. The new wrapper will take its
* place in the parent container.
* @param {string} - The change that the wrapper should represent.
*/
function _wrapChanged(el, change) {
let nodeDoc = el.ownerDocument;
let wrapper;
switch (change) {
case "removed": case "removed":
n.className = aType; wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "del");
if (Services.prefs.getBoolPref("calendar.view.useSystemColors", false)) { break;
n.setAttribute("systemcolors", true); case "added":
} wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "ins");
break; break;
} }
n.textContent = aText; if (wrapper) {
aToNode.appendChild(n); el.replaceWith(wrapper);
} wrapper.appendChild(el);
/** el = wrapper;
* Extracts attendees from the given document
* @param {Node} aDoc document to search in
* @param {String} aElement element name as used in _compareElement()
* @returns {Array} attendee nodes
*/
function _getAttendees(aDoc, aElement) {
let attendees = [];
for (let att of aDoc.getElementsByClassName("attendee-name")) {
if (!att.parentNode.hidden && att.parentNode.parentNode.id == aElement + "-table") {
attendees[att.textContent] = att;
}
} }
return attendees; _addStyleClass(el, change);
} }
/**
* Compares both documents for elements related to the given name
* @param {String} aElement part of the element id within the html template
*/
function _compareElement(aElement) {
let element = aElement == "attendee" ? aElement + "s" : aElement;
let oldRow = aOldDoc.getElementById("imipHtml-" + element + "-row");
let newRow = aNewDoc.getElementById("imipHtml-" + element + "-row");
let row = doc.getElementById("imipHtml-" + element + "-row");
let oldContent = aOldDoc.getElementById("imipHtml-" + aElement + "-content");
let content = doc.getElementById("imipHtml-" + aElement + "-content");
if (newRow.hidden && !oldRow.hidden) { let organizerCell = doc.querySelector("#imipHtml-organizer-cell");
// element was removed let organizerLabel = organizerCell.querySelector(".attendee-label");
// we only need to check for simple elements here: attendee or organizer row let oldOrganizerLabel = oldDoc.querySelector("#imipHtml-organizer-cell .attendee-label");
// cannot be removed _compareRows(
if (oldContent) { doc,
_content2Child(content, "removed", oldContent.textContent); oldDoc,
row.hidden = false; "imipHtml-organizer-row",
// Removed row.
() => {
oldOrganizerLabel.remove();
if (organizerLabel) {
organizerLabel.remove();
} }
} else if (!newRow.hidden && oldRow.hidden) { organizerCell.appendChild(oldOrganizerLabel);
// the element was added _wrapChanged(oldOrganizerLabel, "removed");
// we only need to check for simple elements here: attendee or organizer row },
// must have been there before // Added row.
if (content) { () => _wrapChanged(organizerLabel, "added"),
_content2Child(content, "added", content.textContent); // Modified row.
} () => {
} else if (!newRow.hidden && !oldRow.hidden) { switch (_attendeeDiff(organizerLabel, oldOrganizerLabel)) {
// the element may have been modified case "different":
if (content) { _wrapChanged(organizerLabel, "added");
if (content.textContent != oldContent.textContent) { oldOrganizerLabel.remove();
_content2Child(content, "added", content.textContent); organizerCell.appendChild(oldOrganizerLabel);
_content2Child(content, "newline", null, false); _wrapChanged(oldOrganizerLabel, "removed");
_content2Child(content, "removed", oldContent.textContent, false); break;
} case "modified":
} else { _wrapChanged(organizerLabel, "modified");
content = doc.getElementById(aElement + "-table"); break;
oldContent = aOldDoc.getElementById(aElement + "-table");
let excludeAddress = cal.email.removeMailTo(aIgnoreId);
if (content && oldContent && !content.isEqualNode(oldContent)) {
// extract attendees
let attendees = _getAttendees(doc, aElement);
let oldAttendees = _getAttendees(aOldDoc, aElement);
// decorate newly added attendees
for (let att of Object.keys(attendees)) {
if (!(att in oldAttendees)) {
_content2Child(attendees[att], "added", att);
}
}
for (let att of Object.keys(oldAttendees)) {
// if att is the user his/herself, who accepted an invitation he/she was
// not invited to, we exclude him/her from decoration
let notExcluded = excludeAddress == "" || !att.includes(excludeAddress);
// decorate removed attendees
if (!(att in attendees) && notExcluded) {
_content2Child(oldAttendees[att], "removed", att);
content.appendChild(oldAttendees[att].parentNode.cloneNode(true));
} else if (att in attendees && notExcluded) {
// highlight partstat, role or usertype changes
let oldAtts = oldAttendees[att].parentNode.getElementsByClassName("itip-icon")[0]
.attributes;
let newAtts = attendees[att].parentNode.getElementsByClassName("itip-icon")[0]
.attributes;
let hasChanged = function(name) {
return oldAtts.getNamedItem(name).value != newAtts.getNamedItem(name).value;
};
if (["role", "partstat", "usertype"].some(hasChanged)) {
_content2Child(attendees[att], "modified", att);
}
}
}
}
} }
} }
} );
aOldDoc = cal.xml.parseString(aOldDoc);
aNewDoc = cal.xml.parseString(aNewDoc); let attendeeCell = doc.querySelector("#imipHtml-attendees-cell");
let doc = aNewDoc.cloneNode(true); let attendeeList = attendeeCell.querySelector(".attendee-list");
// elements to consider for comparison let oldAttendeeList = oldDoc.querySelector("#imipHtml-attendees-cell .attendee-list");
let elements = [ _compareRows(
"summary", doc,
"location", oldDoc,
"when", "imipHtml-attendees-row",
"canceledOccurrences", // Removed row.
"modifiedOccurrences", () => {
"organizer", oldAttendeeList.remove();
"attendee", if (attendeeList) {
]; attendeeList.remove();
elements.forEach(_compareElement); }
attendeeCell.appendChild(oldAttendeeList);
_wrapChanged(oldAttendeeList, "removed");
},
// Added row.
() => _wrapChanged(attendeeList, "added"),
// Modified row.
() => {
let oldAttendees = Array.from(oldAttendeeList.querySelectorAll(".attendee-label"));
for (let attendeeLabel of attendeeList.querySelectorAll(".attendee-label")) {
let added = true;
for (let i = 0; added && i < oldAttendees.length; i++) {
switch (_attendeeDiff(attendeeLabel, oldAttendees[i])) {
case "different":
break;
case "modified":
_wrapChanged(attendeeLabel, "modified");
// Fallthrough.
case "same":
oldAttendees.splice(i, 1);
added = false;
break;
}
}
if (added) {
_wrapChanged(attendeeLabel, "added");
}
}
for (let oldAttendeeLabel of oldAttendees) {
oldAttendeeLabel.remove();
attendeeList.appendChild(this.createAttendeeListItem(doc, oldAttendeeLabel));
_wrapChanged(oldAttendeeLabel, "removed");
}
}
);
return cal.xml.serializeDOM(doc); return cal.xml.serializeDOM(doc);
}, },

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

@ -190,75 +190,6 @@ tooltipPriority=Priority:
tooltipPercent=% Complete: tooltipPercent=% Complete:
tooltipCompleted=Completed: tooltipCompleted=Completed:
# Tooltips for attendees and organizer in event and summary dialog
# LOCALIZATION_NOTE(dialog.tooltip.attendee.combined): tooltip for itip icon in summary/event dialog.
# Given an attendee loungeexample.org of type room is a mandatory participant and has accepted the
# invitation, the tooltip would be:
# lounge@example.org (room) is a required participant. lounge@example.org has confirmed attendance.
# %1$S - value of dialog.tooltip.attendeeRole2.*
# %2$S - value of dialog.tooltip.attendeePartStat2.*
dialog.tooltip.attendee.combined=%1$S %2$S
# LOCALIZATION_NOTE(dialog.tooltip.attendeeRole2.CHAIR): used to compose
# dialog.tooltip.attendee.combined
# %1$S - value of dialog.tooltip.attendeeUserType2.*
dialog.tooltip.attendeeRole2.CHAIR=%1$S chairs the event.
# LOCALIZATION_NOTE(dialog.tooltip.attendeeRole2.NON-PARTICIPANT): used to compose
# dialog.tooltip.attendee.combined
# %1$S - value of dialog.tooltip.attendeeUserType2.*
dialog.tooltip.attendeeRole2.NON-PARTICIPANT=%1$S is a non-participant.
# LOCALIZATION_NOTE(dialog.tooltip.attendeeRole2.OPT-PARTICIPANT): used to compose
# dialog.tooltip.attendee.combined
# %1$S - value of dialog.tooltip.attendeeUserType2.*
dialog.tooltip.attendeeRole2.OPT-PARTICIPANT=%1$S is an optional participant.
# LOCALIZATION_NOTE(dialog.tooltip.attendeeRole2.REQ-PARTICIPANT): used to compose
# dialog.tooltip.attendee.combined
# %1$S - value of dialog.tooltip.attendeeUserType2.*
dialog.tooltip.attendeeRole2.REQ-PARTICIPANT=%1$S is a required participant.
# LOCALIZATION_NOTE(dialog.tooltip.attendeePartStat2.ACCEPTED): used to compose
# dialog.tooltip.attendee.combined
# %1$S - common name or email address of the attendee
dialog.tooltip.attendeePartStat2.ACCEPTED=%1$S has confirmed attendance.
# LOCALIZATION_NOTE(dialog.tooltip.attendeePartStat2.DECLINED): used to compose
# dialog.tooltip.attendee.combined
# %1$S - common name or email address of the attendee
dialog.tooltip.attendeePartStat2.DECLINED=%1$S has declined attendance.
# LOCALIZATION_NOTE(dialog.tooltip.attendeePartStat2.DELEGATED): used to compose
# dialog.tooltip.attendee.combined
# %1$S - common name or email address of the attendee
dialog.tooltip.attendeePartStat2.DELEGATED=%1$S has delegated attendance.
# LOCALIZATION_NOTE(dialog.tooltip.attendeePartStat2.NEEDS-ACTION): used to compose
# dialog.tooltip.attendee.combined
# %1$S - common name or email address of the attendee
dialog.tooltip.attendeePartStat2.NEEDS-ACTION=%1$S still needs to reply.
# LOCALIZATION_NOTE(dialog.tooltip.attendeePartStat2.TENTATIVE): used to compose
# dialog.tooltip.attendee.combined
# %1$S - common name or email address of the attendee
dialog.tooltip.attendeePartStat2.TENTATIVE=%1$S has confirmed attendance tentatively.
# LOCALIZATION_NOTE(dialog.tooltip.attendeeUserType2.INDIVIDUAL): used to compose
# dialog.tooltip.attendeeRole2.*
# %1$S - email address or common name <email address> representing individual attendee
dialog.tooltip.attendeeUserType2.INDIVIDUAL=%1$S
# LOCALIZATION_NOTE(dialog.tooltip.attendeeUserType2.GROUP): used to compose
# dialog.tooltip.attendeeRole2.*
# %1$S - email address or common name <email address> representing a group (e.g. a distribution list)
dialog.tooltip.attendeeUserType2.GROUP=%1$S (group)
# LOCALIZATION_NOTE(dialog.tooltip.attendeeUserType2.RESOURCE): used to compose
# dialog.tooltip.attendeeRole2.*
# %1$S - email address or common name <email address> representing a resource (e.g. projector)
dialog.tooltip.attendeeUserType2.RESOURCE=%1$S (resource)
# LOCALIZATION_NOTE(dialog.tooltip.attendeeUserType2.ROOM): used to compose
# dialog.tooltip.attendeeRole2.*
# %1$S - email address or common name <email address> representing a room
dialog.tooltip.attendeeUserType2.ROOM=%1$S (room)
# LOCALIZATION_NOTE(dialog.tooltip.attendeeUserType2.UNKNOWN): used to compose
# dialog.tooltip.attendeeRole2.*
# %1$S - email address or common name <email address> representing an attendee of unknown type
dialog.tooltip.attendeeUserType2.UNKNOWN=%1$S
#File commands and dialogs #File commands and dialogs
New=New New=New
Open=Open Open=Open
@ -776,19 +707,5 @@ modifyConflictPromptMessage=The item being edited in the dialog has been modifie
modifyConflictPromptButton1=Overwrite the other changes modifyConflictPromptButton1=Overwrite the other changes
modifyConflictPromptButton2=Discard these changes modifyConflictPromptButton2=Discard these changes
# LOCALIZATION_NOTE(dialog.attendee.append.delegatedFrom): this is appended behind an attendee name
# in the tooltip and the visible name for an attendee in the event summary dialog - don't add
# leading or trailing whitespaces here
# %1$S - a single delegatee or a comma separated list of delegatees
# delegation is different from simple invitation forwarding - in case of delegation the original
# invited attendee gets replaced
dialog.attendee.append.delegatedFrom=(delegated from %1$S)
# LOCALIZATION_NOTE(dialog.attendee.append.delegatedTo): this is appended behind an attendee name
# in the tooltip for an attendee in the event summary dialog - don't add leading or trailing
# whitespaces here
# delegation is different from simple invitation forwarding - in case of delegation the original
# invited attendee gets replaced
dialog.attendee.append.delegatedTo=(delegated to %1$S)
# Accessible description of a grid calendar with no selected date # Accessible description of a grid calendar with no selected date
minimonthNoSelectedDate=No date selected minimonthNoSelectedDate=No date selected

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

@ -189,12 +189,12 @@ add_task(async () => {
Assert.equal(eventEndTime.value.toISOString(), times.FOUR.toISOString()); Assert.equal(eventEndTime.value.toISOString(), times.FOUR.toISOString());
function checkAttendeeCells(organizer, ...expected) { function checkAttendeeCells(organizer, ...expected) {
Assert.equal(iframeDocument.getElementById("item-organizer").getAttribute("value"), organizer); Assert.equal(iframeDocument.getElementById("item-organizer-row").textContent, organizer);
let cells = iframeDocument.querySelectorAll(".item-attendees-box .item-attendees-cell"); let attendeeItems = iframeDocument.querySelectorAll(".attendee-list .attendee-label");
Assert.equal(cells.length, expected.length); Assert.equal(attendeeItems.length, expected.length);
for (let i = 0; i < expected.length; i++) { for (let i = 0; i < expected.length; i++) {
Assert.equal(cells[i].getAttribute("attendeeid"), expected[i]); Assert.equal(attendeeItems[i].getAttribute("attendeeid"), expected[i]);
} }
} }

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

@ -120,10 +120,10 @@ add_task(async function testEventDialog() {
); );
let attendeesTab = iframeDocument.getElementById("event-grid-tabpanel-attendees"); let attendeesTab = iframeDocument.getElementById("event-grid-tabpanel-attendees");
let attendee = attendeesTab.querySelector(".item-attendees-cell"); let attendeeName = attendeesTab.querySelector(".attendee-list .attendee-name");
Assert.ok(attendee); Assert.ok(attendeeName);
Assert.equal(attendee.querySelector(".item-attendees-cell-label").value, EVENTATTENDEE); Assert.equal(attendeeName.textContent, EVENTATTENDEE);
Assert.ok(!iframeDocument.getElementById("notify-attendees-checkbox").checked); Assert.ok(!iframeDocument.getElementById("notify-attendees-checkbox").checked);
// Verify private label visible. // Verify private label visible.

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

@ -492,7 +492,7 @@ async function deleteAttendees(iframeWindow, attendeesString) {
let attendees = attendeesString.split(","); let attendees = attendeesString.split(",");
for (let attendee of attendees) { for (let attendee of attendees) {
let attendeeToDelete = iframeDocument.querySelector( let attendeeToDelete = iframeDocument.querySelector(
`.item-attendees-row [attendeeid="mailto:${attendee}"]` `.attendee-list [attendeeid="mailto:${attendee}"]`
); );
if (attendeeToDelete) { if (attendeeToDelete) {
attendeeToDelete.focus(); attendeeToDelete.focus();

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

@ -339,46 +339,88 @@ add_task(async function createInvitationOverlay_test() {
"ATTENDEE:mailto:attendee7@example.net\r\n", "ATTENDEE:mailto:attendee7@example.net\r\n",
}, },
expected: { expected: {
node: "attendee-table", node: "imipHtml-attendees-cell",
value: values: [
'<tr xmlns="http://www.w3.org/1999/xhtml" id="attendee-template" hidden="' + {
'true"><td><p class="itip-icon"></p></td><td class="attendee-name"></td><' + name: "Attendee 1 <attendee1@example.net>",
"/tr>" + title:
'<tr xmlns="http://www.w3.org/1999/xhtml" title="Attendee 1 &lt;attendee1@e' + "Attendee 1 <attendee1@example.net> is an optional " +
'xample.net&gt; is an optional participant. Attendee 1 still needs to reply."' + "participant. Attendee 1 still needs to reply.",
'><td><p class="itip-icon" role="OPT-PARTICIPANT" usertype="INDIVIDUAL" ' + icon: {
'partstat="NEEDS-ACTION"></p></td><td class="attendee-name">Attendee 1 &lt' + role: "OPT-PARTICIPANT",
";attendee1@example.net&gt;</td></tr>" + usertype: "INDIVIDUAL",
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee2@example.net (gro' + partstat: "NEEDS-ACTION",
'up) is a non-participant. attendee2@example.net has confirmed attendance."><' + },
'td><p class="itip-icon" role="NON-PARTICIPANT" usertype="GROUP" partsta' + },
't="ACCEPTED"></p></td><td class="attendee-name">attendee2@example.net</td' + {
"></tr>" + name: "attendee2@example.net",
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee3@example.net (res' + title:
"ource) is a required participant. attendee3@example.net has confirmed attenda" + "attendee2@example.net (group) is a non-participant. " +
'nce tentatively."><td><p class="itip-icon" role="REQ-PARTICIPANT" userty' + "attendee2@example.net has confirmed attendance.",
'pe="RESOURCE" partstat="TENTATIVE"></p></td><td class="attendee-name">a' + icon: {
"ttendee3@example.net</td></tr>" + role: "NON-PARTICIPANT",
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee4@example.net (roo' + usertype: "GROUP",
"m) is an optional participant. attendee4@example.net has declined attendance." + partstat: "ACCEPTED",
'"><td><p class="itip-icon" role="OPT-PARTICIPANT" usertype="ROOM" part' + },
'stat="DECLINED"></p></td><td class="attendee-name">attendee4@example.net ' + },
"(delegated from attendee5@example.net)</td></tr>" + {
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee5@example.net is a' + name: "attendee3@example.net",
"n optional participant. attendee5@example.net has delegated attendance to att" + title:
'endee4@example.net."><td><p class="itip-icon" role="OPT-PARTICIPANT" use' + "attendee3@example.net (resource) is a required " +
'rtype="UNKNOWN" partstat="DELEGATED"></p></td><td class="attendee-name"' + "participant. attendee3@example.net has confirmed attendance " +
">attendee5@example.net</td></tr>" + "tentatively.",
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee6@example.net is a' + icon: {
' required participant. attendee6@example.net still needs to reply."><td><p c' + role: "REQ-PARTICIPANT",
'lass="itip-icon" role="REQ-PARTICIPANT" usertype="INDIVIDUAL" partstat=' + usertype: "RESOURCE",
'"NEEDS-ACTION"></p></td><td class="attendee-name">attendee6@example.net</' + partstat: "TENTATIVE",
"td></tr>" + },
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee7@example.net is a' + },
' required participant. attendee7@example.net still needs to reply."><td><p c' + {
'lass="itip-icon" role="REQ-PARTICIPANT" usertype="INDIVIDUAL" partstat=' + name: "attendee4@example.net (delegated from attendee5@example.net)",
'"NEEDS-ACTION"></p></td><td class="attendee-name">attendee7@example.net</' + title:
"td></tr>", "attendee4@example.net (room) is an optional participant. " +
"attendee4@example.net has declined attendance.",
icon: {
role: "OPT-PARTICIPANT",
usertype: "ROOM",
partstat: "DECLINED",
},
},
{
name: "attendee5@example.net",
title:
"attendee5@example.net is an optional participant. " +
"attendee5@example.net has delegated attendance to " +
"attendee4@example.net.",
icon: {
role: "OPT-PARTICIPANT",
usertype: "UNKNOWN",
partstat: "DELEGATED",
},
},
{
name: "attendee6@example.net",
title:
"attendee6@example.net is a required participant. " +
"attendee6@example.net still needs to reply.",
icon: {
role: "REQ-PARTICIPANT",
usertype: "INDIVIDUAL",
partstat: "NEEDS-ACTION",
},
},
{
name: "attendee7@example.net",
title:
"attendee7@example.net is a required participant. " +
"attendee7@example.net still needs to reply.",
icon: {
role: "REQ-PARTICIPANT",
usertype: "INDIVIDUAL",
partstat: "NEEDS-ACTION",
},
},
],
}, },
}, },
{ {
@ -388,13 +430,20 @@ add_task(async function createInvitationOverlay_test() {
'anizer":mailto:organizer@example.net\r\n', 'anizer":mailto:organizer@example.net\r\n',
}, },
expected: { expected: {
node: "organizer-table", node: "imipHtml-organizer-cell",
value: values: [
'<tr xmlns="http://www.w3.org/1999/xhtml" title="The Organizer &lt;organize' + {
'r@example.net&gt; chairs the event. The Organizer has confirmed attendance."' + name: "The Organizer <organizer@example.net>",
'><td><p class="itip-icon" role="CHAIR" usertype="INDIVIDUAL" partstat="' + title:
'ACCEPTED"></p></td><td class="attendee-name">The Organizer &lt;organizer@e' + "The Organizer <organizer@example.net> chairs the event. " +
"xample.net&gt;</td></tr>", "The Organizer has confirmed attendance.",
icon: {
role: "CHAIR",
usertype: "INDIVIDUAL",
partstat: "ACCEPTED",
},
},
],
}, },
}, },
]; ];
@ -426,13 +475,32 @@ add_task(async function createInvitationOverlay_test() {
let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser); let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser);
parser.parseString(item); parser.parseString(item);
let dom = ltn.invitation.createInvitationOverlay(parser.getItems()[0], itipItem); let dom = ltn.invitation.createInvitationOverlay(parser.getItems()[0], itipItem);
let observed = dom.getElementById(test.expected.node).innerHTML;
// we remove line-breaks and leading white spaces here so we can keep expected test results // we remove line-breaks and leading white spaces here so we can keep expected test results
// above more comprehensive // above more comprehensive
if (test.expected.node.endsWith("-table")) { switch (test.expected.node) {
observed = observed.replace(/(?:\n|\r\n|\r)[ ]{2,}/g, ""); case "imipHtml-organizer-cell":
case "imipHtml-attendees-cell":
let attendeeNodes = Array.from(
dom.querySelectorAll(`#${test.expected.node} .attendee-label`)
);
equal(attendeeNodes.length, test.expected.values.length);
for (let { name, title, icon } of test.expected.values) {
let index = attendeeNodes.findIndex(el => el.textContent === name);
ok(index !== -1, `Attendee with name ${name}`);
let node = attendeeNodes.splice(index, 1)[0];
equal(node.getAttribute("title"), title, `Title for ${name}`);
let nodeIcon = node.querySelector(".itip-icon");
ok(nodeIcon, `icon for ${name}`);
for (let attr in icon) {
equal(nodeIcon.getAttribute(attr), icon[attr], `${attr} for icon for ${name}`);
}
}
break;
default:
let observed = dom.getElementById(test.expected.node).innerHTML;
equal(observed, test.expected.value, "(test #" + i + ")");
break;
} }
equal(observed, test.expected.value, "(test #" + i + ")");
} }
}); });
@ -461,26 +529,24 @@ add_task(async function compareInvitationOverlay_test() {
input: { input: {
previous: { location: "LOCATION:This place\r\n" }, previous: { location: "LOCATION:This place\r\n" },
current: { location: "LOCATION:Another location\r\n" }, current: { location: "LOCATION:Another location\r\n" },
ignore: "",
}, },
expected: { expected: {
node: "imipHtml-location-content", node: "imipHtml-location-content",
value: ins: ["Another location"],
'<span xmlns="" class="added">Another location</span><br xmlns=""/>' + del: ["This place"],
'<span xmlns="" class="removed">This place</span>', mod: [],
}, },
}, },
{ {
input: { input: {
previous: { summary: "SUMMARY:My invitation\r\n" }, previous: { summary: "SUMMARY:My invitation\r\n" },
current: { summary: "SUMMARY:My new invitation\r\n" }, current: { summary: "SUMMARY:My new invitation\r\n" },
ignore: "",
}, },
expected: { expected: {
node: "imipHtml-summary-content", node: "imipHtml-summary-content",
value: ins: ["My new invitation"],
'<span xmlns="" class="added">My new invitation</span><br xmlns=""/>' + del: ["My invitation"],
'<span xmlns="" class="removed">My invitation</span>', mod: [],
}, },
}, },
{ {
@ -493,46 +559,28 @@ add_task(async function compareInvitationOverlay_test() {
dtstart: "DTSTART;TZID=Europe/Berlin:20150909T140000\r\n", dtstart: "DTSTART;TZID=Europe/Berlin:20150909T140000\r\n",
dtend: "DTEND;TZID=Europe/Berlin:20150909T150000\r\n", dtend: "DTEND;TZID=Europe/Berlin:20150909T150000\r\n",
}, },
ignore: "",
}, },
expected: { expected: {
// Time format is platform dependent, so we use alternative result sets here. // Time format is platform dependent, so we use alternative result sets here.
// The first two are configurations running for automated tests. // The first two are configurations running for automated tests.
// If you get a failure for this test, add your pattern here. // If you get a failure for this test, add your pattern here.
node: "imipHtml-when-content", node: "imipHtml-when-content",
some: [ // For Windows.
// For Windows. ins: [/^Wednesday, (September 0?9,|0?9 September) 2015 (2:00 PM – 3:00 PM|14:00 – 15:00)$/],
'<span xmlns="" class="added">Wednesday, September 09, 2015 2:00 PM – 3:00 PM</span>' + del: [/^Wednesday, (September 0?9,|0?9 September) 2015 (1:00 PM – 2:00 PM|13:00 – 14:00)$/],
'<br xmlns=""/>' + mod: [],
'<span xmlns="" class="removed">Wednesday, September 09, 2015 1:00 PM – 2:00 PM</span>',
'<span xmlns="" class="added">Wednesday, September 09, 2015 14:00 – 15:00</span>' +
'<br xmlns=""/>' +
'<span xmlns="" class="removed">Wednesday, September 09, 2015 13:00 – 14:00</span>',
// For Linux and Mac: The same but without 2-digit day.
'<span xmlns="" class="added">Wednesday, September 9, 2015 2:00 PM – 3:00 PM</span>' +
'<br xmlns=""/>' +
'<span xmlns="" class="removed">Wednesday, September 9, 2015 1:00 PM – 2:00 PM</span>',
'<span xmlns="" class="added">Wednesday, September 9, 2015 14:00 – 15:00</span>' +
'<br xmlns=""/>' +
'<span xmlns="" class="removed">Wednesday, September 9, 2015 13:00 – 14:00</span>',
],
}, },
}, },
{ {
input: { input: {
previous: { organizer: "ORGANIZER:mailto:organizer1@example.net\r\n" }, previous: { organizer: "ORGANIZER:mailto:organizer1@example.net\r\n" },
current: { organizer: "ORGANIZER:mailto:organizer2@example.net\r\n" }, current: { organizer: "ORGANIZER:mailto:organizer2@example.net\r\n" },
ignore: "",
}, },
expected: { expected: {
node: "organizer-table", node: "imipHtml-organizer-cell",
each: [ ins: ["organizer2@example.net"],
'<span xmlns="" class="added">organizer2@example.net</span>', del: ["organizer1@example.net"],
'<span xmlns="" class="removed">organizer1@example.net</span>', mod: [],
],
}, },
}, },
{ {
@ -555,16 +603,12 @@ add_task(async function compareInvitationOverlay_test() {
"ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:" + "ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:" +
"mailto:attendee4@example.net\r\n", "mailto:attendee4@example.net\r\n",
}, },
ignore: "",
}, },
expected: { expected: {
node: "attendee-table", node: "imipHtml-attendees-cell",
each: [ ins: ["attendee4@example.net"],
'<span xmlns="" class="modified">attendee2@example.net</span>', del: ["attendee1@example.net"],
"attendee3@example.net", mod: ["attendee2@example.net"],
'<span xmlns="" class="added">attendee4@example.net</span>',
'<span xmlns="" class="removed">attendee1@example.net</span>',
],
}, },
}, },
]; ];
@ -579,46 +623,31 @@ add_task(async function compareInvitationOverlay_test() {
Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", false); Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", false);
Services.prefs.setIntPref("calendar.date.format", 0); Services.prefs.setIntPref("calendar.date.format", 0);
Services.prefs.setStringPref("calendar.timezone.local", "Europe/Berlin"); Services.prefs.setStringPref("calendar.timezone.local", "Europe/Berlin");
let i = 0;
for (let test of data) { for (let test of data) {
i++;
let dom1 = getDom(test.input.previous); let dom1 = getDom(test.input.previous);
let dom2 = getDom(test.input.current); let dom2 = getDom(test.input.current);
let result = ltn.invitation.compareInvitationOverlay(dom1, dom2, test.input.ignore); let result = ltn.invitation.compareInvitationOverlay(dom1, dom2);
let dom = cal.xml.parseString(result); let dom = cal.xml.parseString(result);
if (test.expected.node.startsWith("imipHtml")) { let id = test.expected.node;
if ("value" in test.expected && test.expected.value) {
equal( function assertChanges(name, nodes, expectedText) {
dom.getElementById(test.expected.node).innerHTML, equal(nodes.length, expectedText.length, `Equal number of ${name} for ${id}`);
test.expected.value, for (let text of expectedText) {
"(test #" + i + "): " + test.expected.node let index;
); if (text instanceof RegExp) {
} else if ("some" in test.expected && test.expected.some) { index = nodes.findIndex(el => text.test(el.textContent));
ok( } else {
test.expected.some.includes(dom.getElementById(test.expected.node).innerHTML), index = nodes.findIndex(el => el.textContent === text);
"(test #" + i + "): " + test.expected.node
);
}
} else {
// this is for testing of an attendee or organizer
let nodes = dom.getElementById(test.expected.node).getElementsByClassName("attendee-name");
let j = 0;
for (let node of nodes) {
if (node.parentNode.id != "attendee-template") {
j++;
equal(
node.innerHTML,
test.expected.each[j - 1],
"(test #" + i + "): " + test.expected.node + "(entry #" + j + ")"
);
} }
ok(index !== -1, `${name} node with text ${text} for ${id}`);
nodes.splice(index, 1);
} }
equal(
test.expected.each.length,
j,
"(test #" + i + "): completeness check " + test.expected.node
);
} }
let node = dom.getElementById(id);
ok(node, `Element with id ${id}`);
assertChanges("<ins>", Array.from(node.querySelectorAll("ins.added")), test.expected.ins);
assertChanges("<del>", Array.from(node.querySelectorAll("del.removed")), test.expected.del);
assertChanges("modified", Array.from(node.querySelectorAll(".modified")), test.expected.mod);
} }
// let's reset setting // let's reset setting
Services.prefs.setIntPref("calendar.date.format", dateformat); Services.prefs.setIntPref("calendar.date.format", dateformat);