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,
* dispose, setDialogId, loadReminders, saveReminder,
* commonUpdateReminder, updateLink, rearrangeAttendees,
* commonUpdateReminder, updateLink,
* adaptScheduleAgent, sendMailToOrganizer,
* 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
* based on forceEmailScheduling preference for the respective calendar

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

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

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

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

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

@ -30,7 +30,10 @@
margin-inline: 0px;
}
.calendar-summary-table .organizer-label,
.calendar-summary-table .attachments-label {
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.
{
var { ltn } = ChromeUtils.import("resource:///modules/calendar/ltnInvitationUtils.jsm");
var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
var { recurrenceStringFromItem } = ChromeUtils.import(
"resource:///modules/calendar/calRecurrenceUtils.jsm"
@ -82,17 +83,11 @@
<html:td class="item-category">
</html:td>
</html:tr>
<html:tr class="organizer-row item-attendees-row" hidden="hidden">
<html:th class="organizer-label">
<html:tr class="item-organizer-row" hidden="hidden">
<html:th>
&read.only.organizer.label;
</html:th>
<html:td>
<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 class="item-organizer-cell">
</html:td>
</html:tr>
<html:tr class="status-row" hidden="hidden">
@ -138,18 +133,6 @@
</html:td>
</html:tr>
</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 -->
<box class="item-attendees" orient="vertical" hidden="true" flex="1">
<spacer class="default-spacer"/>
@ -158,7 +141,8 @@
class="header"/>
<separator class="groove" flex="1"/>
</hbox>
<vbox class="item-attendees-box" flex="1" />
<vbox class="item-attendees-list-container" flex="1">
</vbox>
</box>
<!-- Description -->
@ -303,15 +287,8 @@
this.mReadOnly = true;
this.mIsInvitation = false;
this.mAttendeesInRow = null;
this.mMaxLabelWidth = null;
this.mIsToDoItem = null;
this.querySelector(".item-organizer-label").addEventListener("click", () => {
sendMailToOrganizer(this.mItem);
});
let urlLink = this.querySelector(".url-link");
urlLink.addEventListener("click", event => {
launchBrowser(urlLink.getAttribute("href"), event);
@ -491,7 +468,7 @@
}
if (item.organizer && item.organizer.id) {
this.updateOrganizer(item.organizer);
this.updateOrganizer(item);
}
let status = item.getProperty("STATUS");
@ -577,16 +554,9 @@
let attendees = item.getAttendees();
if (attendees && attendees.length) {
this.querySelector(".item-attendees").removeAttribute("hidden");
let { attendeesInRow, maxLabelWidth } = setupAttendees(
attendees,
this.querySelector(".item-summary-box"),
this.mAttendeesInRow,
this.mMaxLabelWidth
this.querySelector(".item-attendees-list-container").appendChild(
ltn.invitation.createAttendeesList(document, attendees)
);
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.
*
* @param {calIAttendee} organizer - The organizer of the calendar item.
* @param {calIItemBase} item - The calendar item.
*/
updateOrganizer(organizer) {
this.querySelector(".organizer-row").removeAttribute("hidden");
let cell = this.querySelector(".item-organizer-cell");
let text = cell.querySelector("label");
let icon = cell.querySelector("img");
let role = organizer.role || "REQ-PARTICIPANT";
let userType = organizer.userType || "INDIVIDUAL";
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);
updateOrganizer(item) {
this.querySelector(".item-organizer-row").removeAttribute("hidden");
let organizerLabel = ltn.invitation.createAttendeeLabel(
document,
item.organizer,
item.getAttendees()
);
let organizerName = organizerLabel.querySelector(".attendee-name");
organizerName.classList.add("text-link");
organizerName.addEventListener("click", () => sendMailToOrganizer(this.mItem));
this.querySelector(".item-organizer-cell").appendChild(organizerLabel);
}
/**

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

@ -15,15 +15,14 @@ html|input.textbox-addressingWidget:disabled {
opacity: 0.5;
}
.item-attendees-box {
.item-attendees-list-container {
appearance: auto;
-moz-default-appearance: listbox;
margin: 2px 4px 0;
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;
background-color: var(--field-background-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);
}
: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);
}
#calendar-summary-dialog .item-attendees,
#calendar-event-summary-dialog .item-attendees,
#calendar-task-summary-dialog .item-attendees {
max-height: 135px; /* displays up to four rows of attendees*/
.attendee-list {
display: block;
padding: 0;
margin: 0;
}
.item-attendees-cell {
padding: 2px;
.attendee-list-item {
display: contents;
}
#calendar-event-dialog-inner .item-attendees-cell {
-moz-user-focus: normal;
margin-bottom: 1px;
margin-inline-end: 1px;
.attendee-label {
padding: 2px;
display: flex;
align-items: baseline;
}
#calendar-event-dialog-inner .item-attendees-cell:focus {
background-color: Highlight;
color: Highlighttext;
.itip-icon {
flex: 0 0 auto;
}
.item-attendees-cell-label {
border: 0;
margin: 0 3px;
padding: 0;
}
.item-organizer-cell {
padding: 0;
margin-left: 6px;
.attendee-name {
margin: 0 3px;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* this is for the itip icon setup in calendar */
@ -75,7 +70,6 @@ html|input.textbox-addressingWidget:disabled {
--itip-icon-usertype: -32px; /* default: INDIVIDUAL */
width: 16px;
height: 16px;
max-height: 16px;
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);

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

@ -322,8 +322,9 @@ label.label {
margin: 2px 4px;
}
#event-grid-tabpanel-attendees > vbox > hbox > .item-attendees-box {
margin: 2px 4px;
#calendar-event-dialog-inner .attendee-label:focus {
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,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
.invitation-table .itip-icon {
--itip-icon-partstat: -16px -16px; /* default: NEEDS-ACTION */
--itip-icon-role: 0px; /* default: REQ-PARTICIPANT */
--itip-icon-usertype: -32px; /* default: INDIVIDUAL */
width: 16px;
height: 16px;
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-border {
border: 3px solid -moz-default-color;
margin-inline-start: auto;
margin-inline-end: auto;
width: -moz-fit-content;
height: -moz-fit-content;
}
.invitation-table {
border: 3px solid -moz-default-color;
border-collapse: collapse;
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 {
padding: 3px;
color: HighlightText;
font-size: 1em;
font-weight: bold;
background-color: Highlight;
}
.invitation-table :is(td, th) {
padding: 3px;
vertical-align: baseline;
}
.invitation-table .description {
width: 9em;
text-align: right;
text-align: end;
font-weight: normal;
border-inline-end: 1px solid hsla(0, 0%, 50%, .2);
background-color: hsla(0, 0%, 50%, .2);
vertical-align: top;
}
.invitation-table .content {
width: 29em;
}
.invitation-table .content p {
white-space: pre-wrap;
}
.invitation-table .added {
color: rgb(255, 0, 0);
text-decoration-line: none;
}
.invitation-table .added[systemcolors] {
color: currentColor;
font-weight: bold;
}
.invitation-table .modified {
color: rgb(255, 0, 0);
font-style: italic;
}
.invitation-table .modified[systemcolors] {
color: currentColor;
}
.invitation-table .removed {
color: rgb(125, 125, 125);
text-decoration: line-through;
}
.invitation-table .removed[systemcolors] {
color: currentColor;
}

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

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

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

@ -8,67 +8,74 @@
<head>
<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/calendar-attendees.css'/>
</head>
<body>
<table class="invitation-table">
<tr id="imipHtml-header-row">
<th colspan="2" class="header">
<p id="imipHtml-header-descr" class="header"/>
</th>
</tr>
<tr id="imipHtml-summary-row" hidden="true">
<td class="description"><p id="imipHtml-summary-descr"/></td>
<td class="content"><p id="imipHtml-summary-content"/></td>
</tr>
<tr id="imipHtml-location-row" hidden="true">
<td class="description"><p id="imipHtml-location-descr"/></td>
<td class="content"><p id="imipHtml-location-content"/></td>
</tr>
<tr id="imipHtml-when-row" hidden="true">
<td class="description"><p id="imipHtml-when-descr"/></td>
<td class="content"><p id="imipHtml-when-content"/></td>
</tr>
<tr id="imipHtml-canceledOccurrences-row" hidden="true">
<td class="description"><p id="imipHtml-canceledOccurrences-descr"/></td>
<td class="content"><p id="imipHtml-canceledOccurrences-content"/></td>
</tr>
<tr id="imipHtml-modifiedOccurrences-row" hidden="true">
<td class="description"><p id="imipHtml-modifiedOccurrences-descr"/></td>
<td class="content"><p id="imipHtml-modifiedOccurrences-content"/></td>
</tr>
<tr id="imipHtml-organizer-row" hidden="true">
<td class="description"><p id="imipHtml-organizer-descr"/></td>
<td class="content">
<table id="organizer-table"/>
</td>
</tr>
<tr id="imipHtml-description-row" hidden="true">
<td class="description"><p id="imipHtml-description-descr"/></td>
<td class="content"><p id="imipHtml-description-content"/></td>
</tr>
<tr id="imipHtml-attachments-row" hidden="true">
<td class="description"><p id="imipHtml-attachments-descr"/></td>
<td class="content"><p id="imipHtml-attachments-content"/></td>
</tr>
<tr id="imipHtml-comment-row" hidden="true">
<td class="description"><p id="imipHtml-comment-descr"/></td>
<td class="content"><p id="imipHtml-comment-content"/></td>
</tr>
<tr id="imipHtml-attendees-row" hidden="true">
<td class="description"><p id="imipHtml-attendees-descr"/></td>
<td class="content">
<table id="attendee-table">
<tr id="attendee-template" hidden="true">
<td><p class="itip-icon"/></td>
<td class="attendee-name"/>
</tr>
</table>
</td>
</tr>
<tr id="imipHtml-url-row" hidden="true">
<td class="description"><p id="imipHtml-url-descr"/></td>
<td class="content"><p id="imipHtml-url-content"/></td>
</tr>
</table>
<div class="invitation-border">
<table class="invitation-table">
<caption id="imipHtml-header" class="header"></caption>
<tr id="imipHtml-summary-row" hidden="hidden">
<th id="imipHtml-summary-descr" class="description" scope="row"></th>
<td id="imipHtml-summary-content" class="content"></td>
</tr>
<tr id="imipHtml-location-row" hidden="hidden">
<th id="imipHtml-location-descr" class="description" scope="row"></th>
<td id="imipHtml-location-content" class="content"></td>
</tr>
<tr id="imipHtml-when-row" hidden="hidden">
<th id="imipHtml-when-descr" class="description" scope="row"></th>
<td id="imipHtml-when-content" class="content"></td>
</tr>
<tr id="imipHtml-canceledOccurrences-row" hidden="hidden">
<th id="imipHtml-canceledOccurrences-descr"
class="description"
scope="row">
</th>
<td id="imipHtml-canceledOccurrences-content" class="content"></td>
</tr>
<tr id="imipHtml-modifiedOccurrences-row" hidden="hidden">
<th id="imipHtml-modifiedOccurrences-descr"
class="description"
scope="row">
</th>
<td id="imipHtml-modifiedOccurrences-content" class="content"></td>
</tr>
<tr id="imipHtml-organizer-row" hidden="hidden">
<th id="imipHtml-organizer-descr"
class="description"
scope="row">
</th>
<td id="imipHtml-organizer-cell" class="content"></td>
</tr>
<tr id="imipHtml-description-row" hidden="hidden">
<th id="imipHtml-description-descr"
class="description"
scope="row">
</th>
<td id="imipHtml-description-content" class="content"></td>
</tr>
<tr id="imipHtml-attachments-row" hidden="hidden">
<th id="imipHtml-attachments-descr"
class="description"
scope="row"></th>
<td id="imipHtml-attachments-content" class="content"></td>
</tr>
<tr id="imipHtml-comment-row" hidden="hidden">
<th id="imipHtml-comment-descr" class="description" scope="row"></th>
<td id="imipHtml-comment-content" class="content"></td>
</tr>
<tr id="imipHtml-attendees-row" hidden="hidden">
<th id="imipHtml-attendees-descr"
class="description"
scope="row">
</th>
<td id="imipHtml-attendees-cell" class="content"></td>
</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>
</html>

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

@ -22,6 +22,7 @@
/* globals gTimezonesEnabled, gShowLink */ // Set by lightning-item-panel.js.
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 {
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
// can be accessed from any location. since the recurrence
// info is a property of the parent item we need to check
@ -3860,53 +3849,29 @@ function updateAttendees() {
attendeePanel.removeAttribute("collapsed");
notifyOptions.removeAttribute("collapsed");
let organizerRow = document.getElementById("item-organizer-row");
if (window.organizer && window.organizer.id) {
let organizer = window.organizer;
document.getElementById("item-organizer-row").removeAttribute("collapsed");
let cell = document.querySelector(".item-organizer-cell");
let icon = cell.querySelector("img:nth-of-type(1)");
let text = cell.querySelector("label:nth-of-type(1)");
let role = organizer.role || "REQ-PARTICIPANT";
let userType = organizer.userType || "INDIVIDUAL";
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);
let existingLabel = organizerRow.querySelector(":scope > .attendee-label");
if (existingLabel) {
organizerRow.removeChild(existingLabel);
}
organizerRow.appendChild(
ltn.invitation.createAttendeeLabel(document, window.organizer, window.attendees)
);
organizerRow.hidden = false;
} else {
document.getElementById("item-organizer-row").collapsed = true;
organizerRow.hidden = true;
}
let { attendeesInRow, maxLabelWidth } = setupAttendees(
window.attendees,
document,
window.attendeesInRow,
window.maxLabelWidth
);
window.attendeesInRow = attendeesInRow;
window.maxLabelWidth = maxLabelWidth;
let attendeeContainer = document.querySelector(".item-attendees-list-container");
if (attendeeContainer.firstChild) {
attendeeContainer.firstChild.remove();
}
attendeeContainer.appendChild(ltn.invitation.createAttendeesList(document, window.attendees));
for (let label of attendeeContainer.querySelectorAll(".attendee-label")) {
label.addEventListener("dblclick", attendeeDblClick);
label.setAttribute("tabindex", "0");
}
// update the attendee tab label to make the number of attendees
// visible even if another tab is displayed

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

@ -642,26 +642,15 @@
<tabpanel id="event-grid-tabpanel-attendees"
collapsed="true">
<vbox flex="1">
<hbox id="item-organizer-row"
collapsed="true"
align="start"
class="item-attendees-row">
<hbox id="item-organizer-row" hidden="true" align="start">
<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>
<vbox class="item-attendees-list-container"
dialog-type="event"
flex="1"
context="attendee-popup"
oncontextmenu="setAttendeeContext(event)"
disable-on-readonly="true"/>
</vbox>
</tabpanel>
</tabpanels>
@ -756,23 +745,4 @@
oncommand="this.parentNode.editTimezone()"/>
</menupopup>
</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>

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

@ -5,6 +5,7 @@
lightning.jar:
% 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
% content lightning %content/
content/html-item-editing/lightning-item-iframe.html (content/html-item-editing/lightning-item-iframe.html)

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

@ -67,6 +67,100 @@ ltn.invitation = {
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.
*
@ -98,7 +192,7 @@ ltn.invitation = {
};
// Simple fields
let headerDescr = doc.getElementById("imipHtml-header-descr");
let headerDescr = doc.getElementById("imipHtml-header");
if (headerDescr) {
headerDescr.textContent = ltn.invitation.getItipHeader(aItipItem);
}
@ -204,66 +298,20 @@ ltn.invitation = {
field("attachments", links.join("<br>"), true);
// ATTENDEE and ORGANIZER fields
let organizerCell = doc.getElementById("imipHtml-organizer-cell");
let attendeeCell = doc.getElementById("imipHtml-attendees-cell");
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-organizer-row").hidden = !aEvent.organizer;
let setupAttendee = function(aAttendee) {
let row = attendeeTemplate.cloneNode(true);
row.removeAttribute("id");
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;
};
field("organizer");
if (aEvent.organizer) {
organizerCell.appendChild(this.createAttendeeLabel(doc, aEvent.organizer, attendees));
}
// Fill rows for attendees and organizer
field("attendees");
for (let attendee of attendees) {
attendeeTable.appendChild(setupAttendee(attendee));
}
field("organizer");
if (aEvent.organizer) {
organizerTable.appendChild(setupAttendee(aEvent.organizer));
}
attendeeCell.appendChild(this.createAttendeesList(doc, attendees));
return doc;
},
@ -272,143 +320,268 @@ ltn.invitation = {
* Expects and return a serialized DOM - use cal.xml.serializeDOM(aDOM)
* @param {String} aOldDoc serialized DOM of the the old 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
*/
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
* @param {Node} aToNode text node to change
* @param {String} aType use 'newline' for the same, 'added' or 'removed' for decoration
* @param {String} aText [optional]
* @param {Boolean} aClear [optional] for consecutive changes on the same node, set to false
* Add a styling class to the given element.
*
* @param {Element} el - The element to add the class to.
* @param {string} className - The name of the styling class to add.
*/
function _content2Child(aToNode, aType, aText = "", aClear = true) {
let nodeDoc = aToNode.ownerDocument;
if (aClear) {
while (aToNode.lastChild) {
aToNode.lastChild.remove();
function _addStyleClass(el, className) {
el.classList.add(className);
el.toggleAttribute("systemcolors", systemColors);
}
/**
* 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) {
case "added":
case "modified":
/**
* Wrap the given element in-place to describe the given change.
* The wrapper will semantically and/or stylistically describe the change.
*
* @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":
n.className = aType;
if (Services.prefs.getBoolPref("calendar.view.useSystemColors", false)) {
n.setAttribute("systemcolors", true);
}
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "del");
break;
case "added":
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "ins");
break;
}
n.textContent = aText;
aToNode.appendChild(n);
}
/**
* 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;
}
if (wrapper) {
el.replaceWith(wrapper);
wrapper.appendChild(el);
el = wrapper;
}
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) {
// element was removed
// we only need to check for simple elements here: attendee or organizer row
// cannot be removed
if (oldContent) {
_content2Child(content, "removed", oldContent.textContent);
row.hidden = false;
let organizerCell = doc.querySelector("#imipHtml-organizer-cell");
let organizerLabel = organizerCell.querySelector(".attendee-label");
let oldOrganizerLabel = oldDoc.querySelector("#imipHtml-organizer-cell .attendee-label");
_compareRows(
doc,
oldDoc,
"imipHtml-organizer-row",
// Removed row.
() => {
oldOrganizerLabel.remove();
if (organizerLabel) {
organizerLabel.remove();
}
} else if (!newRow.hidden && oldRow.hidden) {
// the element was added
// we only need to check for simple elements here: attendee or organizer row
// must have been there before
if (content) {
_content2Child(content, "added", content.textContent);
}
} else if (!newRow.hidden && !oldRow.hidden) {
// the element may have been modified
if (content) {
if (content.textContent != oldContent.textContent) {
_content2Child(content, "added", content.textContent);
_content2Child(content, "newline", null, false);
_content2Child(content, "removed", oldContent.textContent, false);
}
} else {
content = doc.getElementById(aElement + "-table");
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);
}
}
}
}
organizerCell.appendChild(oldOrganizerLabel);
_wrapChanged(oldOrganizerLabel, "removed");
},
// Added row.
() => _wrapChanged(organizerLabel, "added"),
// Modified row.
() => {
switch (_attendeeDiff(organizerLabel, oldOrganizerLabel)) {
case "different":
_wrapChanged(organizerLabel, "added");
oldOrganizerLabel.remove();
organizerCell.appendChild(oldOrganizerLabel);
_wrapChanged(oldOrganizerLabel, "removed");
break;
case "modified":
_wrapChanged(organizerLabel, "modified");
break;
}
}
}
aOldDoc = cal.xml.parseString(aOldDoc);
aNewDoc = cal.xml.parseString(aNewDoc);
let doc = aNewDoc.cloneNode(true);
// elements to consider for comparison
let elements = [
"summary",
"location",
"when",
"canceledOccurrences",
"modifiedOccurrences",
"organizer",
"attendee",
];
elements.forEach(_compareElement);
);
let attendeeCell = doc.querySelector("#imipHtml-attendees-cell");
let attendeeList = attendeeCell.querySelector(".attendee-list");
let oldAttendeeList = oldDoc.querySelector("#imipHtml-attendees-cell .attendee-list");
_compareRows(
doc,
oldDoc,
"imipHtml-attendees-row",
// Removed row.
() => {
oldAttendeeList.remove();
if (attendeeList) {
attendeeList.remove();
}
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);
},

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

@ -190,75 +190,6 @@ tooltipPriority=Priority:
tooltipPercent=% Complete:
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
New=New
Open=Open
@ -776,19 +707,5 @@ modifyConflictPromptMessage=The item being edited in the dialog has been modifie
modifyConflictPromptButton1=Overwrite the other 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
minimonthNoSelectedDate=No date selected

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

@ -189,12 +189,12 @@ add_task(async () => {
Assert.equal(eventEndTime.value.toISOString(), times.FOUR.toISOString());
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");
Assert.equal(cells.length, expected.length);
let attendeeItems = iframeDocument.querySelectorAll(".attendee-list .attendee-label");
Assert.equal(attendeeItems.length, expected.length);
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 attendee = attendeesTab.querySelector(".item-attendees-cell");
let attendeeName = attendeesTab.querySelector(".attendee-list .attendee-name");
Assert.ok(attendee);
Assert.equal(attendee.querySelector(".item-attendees-cell-label").value, EVENTATTENDEE);
Assert.ok(attendeeName);
Assert.equal(attendeeName.textContent, EVENTATTENDEE);
Assert.ok(!iframeDocument.getElementById("notify-attendees-checkbox").checked);
// Verify private label visible.

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

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

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

@ -339,46 +339,88 @@ add_task(async function createInvitationOverlay_test() {
"ATTENDEE:mailto:attendee7@example.net\r\n",
},
expected: {
node: "attendee-table",
value:
'<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><' +
"/tr>" +
'<tr xmlns="http://www.w3.org/1999/xhtml" title="Attendee 1 &lt;attendee1@e' +
'xample.net&gt; is an optional participant. Attendee 1 still needs to reply."' +
'><td><p class="itip-icon" role="OPT-PARTICIPANT" usertype="INDIVIDUAL" ' +
'partstat="NEEDS-ACTION"></p></td><td class="attendee-name">Attendee 1 &lt' +
";attendee1@example.net&gt;</td></tr>" +
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee2@example.net (gro' +
'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>" +
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee3@example.net (res' +
"ource) is a required participant. attendee3@example.net has confirmed attenda" +
'nce tentatively."><td><p class="itip-icon" role="REQ-PARTICIPANT" userty' +
'pe="RESOURCE" partstat="TENTATIVE"></p></td><td class="attendee-name">a' +
"ttendee3@example.net</td></tr>" +
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee4@example.net (roo' +
"m) is an optional participant. attendee4@example.net has declined attendance." +
'"><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' +
"n optional participant. attendee5@example.net has delegated attendance to att" +
'endee4@example.net."><td><p class="itip-icon" role="OPT-PARTICIPANT" use' +
'rtype="UNKNOWN" partstat="DELEGATED"></p></td><td class="attendee-name"' +
">attendee5@example.net</td></tr>" +
'<tr xmlns="http://www.w3.org/1999/xhtml" title="attendee6@example.net is a' +
' required participant. attendee6@example.net still needs to reply."><td><p c' +
'lass="itip-icon" role="REQ-PARTICIPANT" usertype="INDIVIDUAL" partstat=' +
'"NEEDS-ACTION"></p></td><td class="attendee-name">attendee6@example.net</' +
"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=' +
'"NEEDS-ACTION"></p></td><td class="attendee-name">attendee7@example.net</' +
"td></tr>",
node: "imipHtml-attendees-cell",
values: [
{
name: "Attendee 1 <attendee1@example.net>",
title:
"Attendee 1 <attendee1@example.net> is an optional " +
"participant. Attendee 1 still needs to reply.",
icon: {
role: "OPT-PARTICIPANT",
usertype: "INDIVIDUAL",
partstat: "NEEDS-ACTION",
},
},
{
name: "attendee2@example.net",
title:
"attendee2@example.net (group) is a non-participant. " +
"attendee2@example.net has confirmed attendance.",
icon: {
role: "NON-PARTICIPANT",
usertype: "GROUP",
partstat: "ACCEPTED",
},
},
{
name: "attendee3@example.net",
title:
"attendee3@example.net (resource) is a required " +
"participant. attendee3@example.net has confirmed attendance " +
"tentatively.",
icon: {
role: "REQ-PARTICIPANT",
usertype: "RESOURCE",
partstat: "TENTATIVE",
},
},
{
name: "attendee4@example.net (delegated from attendee5@example.net)",
title:
"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',
},
expected: {
node: "organizer-table",
value:
'<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."' +
'><td><p class="itip-icon" role="CHAIR" usertype="INDIVIDUAL" partstat="' +
'ACCEPTED"></p></td><td class="attendee-name">The Organizer &lt;organizer@e' +
"xample.net&gt;</td></tr>",
node: "imipHtml-organizer-cell",
values: [
{
name: "The Organizer <organizer@example.net>",
title:
"The Organizer <organizer@example.net> chairs the event. " +
"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);
parser.parseString(item);
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
// above more comprehensive
if (test.expected.node.endsWith("-table")) {
observed = observed.replace(/(?:\n|\r\n|\r)[ ]{2,}/g, "");
switch (test.expected.node) {
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: {
previous: { location: "LOCATION:This place\r\n" },
current: { location: "LOCATION:Another location\r\n" },
ignore: "",
},
expected: {
node: "imipHtml-location-content",
value:
'<span xmlns="" class="added">Another location</span><br xmlns=""/>' +
'<span xmlns="" class="removed">This place</span>',
ins: ["Another location"],
del: ["This place"],
mod: [],
},
},
{
input: {
previous: { summary: "SUMMARY:My invitation\r\n" },
current: { summary: "SUMMARY:My new invitation\r\n" },
ignore: "",
},
expected: {
node: "imipHtml-summary-content",
value:
'<span xmlns="" class="added">My new invitation</span><br xmlns=""/>' +
'<span xmlns="" class="removed">My invitation</span>',
ins: ["My new invitation"],
del: ["My invitation"],
mod: [],
},
},
{
@ -493,46 +559,28 @@ add_task(async function compareInvitationOverlay_test() {
dtstart: "DTSTART;TZID=Europe/Berlin:20150909T140000\r\n",
dtend: "DTEND;TZID=Europe/Berlin:20150909T150000\r\n",
},
ignore: "",
},
expected: {
// Time format is platform dependent, so we use alternative result sets here.
// The first two are configurations running for automated tests.
// If you get a failure for this test, add your pattern here.
node: "imipHtml-when-content",
some: [
// For Windows.
'<span xmlns="" class="added">Wednesday, September 09, 2015 2:00 PM – 3:00 PM</span>' +
'<br xmlns=""/>' +
'<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>',
],
// For Windows.
ins: [/^Wednesday, (September 0?9,|0?9 September) 2015 (2:00 PM – 3:00 PM|14:00 – 15:00)$/],
del: [/^Wednesday, (September 0?9,|0?9 September) 2015 (1:00 PM – 2:00 PM|13:00 – 14:00)$/],
mod: [],
},
},
{
input: {
previous: { organizer: "ORGANIZER:mailto:organizer1@example.net\r\n" },
current: { organizer: "ORGANIZER:mailto:organizer2@example.net\r\n" },
ignore: "",
},
expected: {
node: "organizer-table",
each: [
'<span xmlns="" class="added">organizer2@example.net</span>',
'<span xmlns="" class="removed">organizer1@example.net</span>',
],
node: "imipHtml-organizer-cell",
ins: ["organizer2@example.net"],
del: ["organizer1@example.net"],
mod: [],
},
},
{
@ -555,16 +603,12 @@ add_task(async function compareInvitationOverlay_test() {
"ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:" +
"mailto:attendee4@example.net\r\n",
},
ignore: "",
},
expected: {
node: "attendee-table",
each: [
'<span xmlns="" class="modified">attendee2@example.net</span>',
"attendee3@example.net",
'<span xmlns="" class="added">attendee4@example.net</span>',
'<span xmlns="" class="removed">attendee1@example.net</span>',
],
node: "imipHtml-attendees-cell",
ins: ["attendee4@example.net"],
del: ["attendee1@example.net"],
mod: ["attendee2@example.net"],
},
},
];
@ -579,46 +623,31 @@ add_task(async function compareInvitationOverlay_test() {
Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", false);
Services.prefs.setIntPref("calendar.date.format", 0);
Services.prefs.setStringPref("calendar.timezone.local", "Europe/Berlin");
let i = 0;
for (let test of data) {
i++;
let dom1 = getDom(test.input.previous);
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);
if (test.expected.node.startsWith("imipHtml")) {
if ("value" in test.expected && test.expected.value) {
equal(
dom.getElementById(test.expected.node).innerHTML,
test.expected.value,
"(test #" + i + "): " + test.expected.node
);
} else if ("some" in test.expected && test.expected.some) {
ok(
test.expected.some.includes(dom.getElementById(test.expected.node).innerHTML),
"(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 + ")"
);
let id = test.expected.node;
function assertChanges(name, nodes, expectedText) {
equal(nodes.length, expectedText.length, `Equal number of ${name} for ${id}`);
for (let text of expectedText) {
let index;
if (text instanceof RegExp) {
index = nodes.findIndex(el => text.test(el.textContent));
} else {
index = nodes.findIndex(el => el.textContent === text);
}
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
Services.prefs.setIntPref("calendar.date.format", dateformat);