--- a/calendar/base/content/calendar-task-view.js
+++ b/calendar/base/content/calendar-task-view.js
@@ -1,12 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
var taskDetailsView = {
/**
* Task Details Events
*
* XXXberend Please document this function, possibly also consolidate since
* its the only function in taskDetailsView.
@@ -108,19 +109,20 @@ var taskDetailsView = {
parentItem = parentItem.parentItem;
}
var recurrenceInfo = parentItem.recurrenceInfo;
var recurStart = parentItem.recurrenceStartDate;
if (displayElement("calendar-task-details-repeat-row", recurrenceInfo && recurStart)) {
var kDefaultTimezone = calendarDefaultTimezone();
var startDate = recurStart.getInTimezone(kDefaultTimezone);
var endDate = item.dueDate ? item.dueDate.getInTimezone(kDefaultTimezone) : null;
- var detailsString = recurrenceRule2String(recurrenceInfo,startDate,endDate,startDate.isDate);
+ var detailsString = recurrenceRule2String(recurrenceInfo, startDate, endDate, startDate.isDate);
if (detailsString) {
- document.getElementById("calendar-task-details-repeat").value = detailsString.split("\n").join(" ");
+ let rpv = document.getElementById("calendar-task-details-repeat");
+ rpv.value = detailsString.split("\n").join(" ");
}
}
var textbox = document.getElementById("calendar-task-details-description");
var description = item.hasProperty("DESCRIPTION") ? item.getProperty("DESCRIPTION") : null;
textbox.value = description;
textbox.inputField.readOnly = true;
let attachmentRows = document.getElementById("calendar-task-details-attachment-rows");
removeChildren(attachmentRows);
--- a/calendar/base/content/dialogs/calendar-dialog-utils.js
+++ b/calendar/base/content/dialogs/calendar-dialog-utils.js
@@ -1,368 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/PluralForm.jsm");
Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
-
-/**
- * This function takes the recurrence info passed as argument and creates a
- * literal string representing the repeat pattern in natural language.
- *
- * @param recurrenceInfo An item's recurrence info to parse.
- * @param startDate The start date to base rules on.
- * @param endDate The end date to base rules on.
- * @param allDay If true, the pattern should assume an allday item.
- * @return A human readable string describing the recurrence.
- */
-function recurrenceRule2String(recurrenceInfo, startDate, endDate, allDay) {
- function getRString(name, args) calGetString("calendar-event-dialog", name, args);
-
- // Retrieve a valid recurrence rule from the currently
- // set recurrence info. Bail out if there's more
- // than a single rule or something other than a rule.
- recurrenceInfo = recurrenceInfo.clone();
- let rrules = splitRecurrenceRules(recurrenceInfo);
- if (rrules[0].length == 1) {
- let rule = rrules[0][0];
- // currently we don't allow for any BYxxx-rules.
- if (calInstanceOf(rule, Components.interfaces.calIRecurrenceRule) &&
- !checkRecurrenceRule(rule, ['BYSECOND',
- 'BYMINUTE',
- //'BYDAY',
- 'BYHOUR',
- //'BYMONTHDAY',
- 'BYYEARDAY',
- 'BYWEEKNO',
- //'BYMONTH',
- 'BYSETPOS'])) {
- function day_of_week(day) {
- return Math.abs(day) % 8;
- }
- function day_position(day) {
- let dow = day_of_week(day);
- return (Math.abs(day) - dow) / 8 * (day < 0 ? -1 : 1);
- }
- function nounClass(aDayString, aRuleString) {
- // Select noun class (grammatical gender) for rule string
- let nounClass = getRString(aDayString + "Nounclass");
- return aRuleString + nounClass.substr(0, 1).toUpperCase() +
- nounClass.substr(1);
- }
- function pluralWeekday(aDayString) {
- let plural = getRString("pluralForWeekdays") == "true";
- return (plural ? aDayString + "Plural" : aDayString);
- }
-
- let ruleString;
- if (rule.type == 'DAILY') {
- if (checkRecurrenceRule(rule, ['BYDAY'])) {
- let days = rule.getComponent("BYDAY", {});
- let weekdays = [2, 3, 4, 5, 6];
- if (weekdays.length == days.length) {
- let i;
- for (i = 0; i < weekdays.length; i++) {
- if (weekdays[i] != days[i]) {
- break;
- }
- }
- if (i == weekdays.length) {
- ruleString = getRString("repeatDetailsRuleDaily4");
- }
- } else {
- return getRString("ruleTooComplex");
- }
- } else {
- let dailyString = getRString("dailyEveryNth");
- ruleString = PluralForm.get(rule.interval, dailyString)
- .replace("#1", rule.interval);
- }
- } else if (rule.type == 'WEEKLY') {
- // weekly recurrence, currently we
- // support a single 'BYDAY'-rule only.
- if (checkRecurrenceRule(rule, ['BYDAY'])) {
- // create a string like 'Monday, Tuesday and Wednesday'
- let days = rule.getComponent("BYDAY", {});
- let weekdays = "";
- // select noun class (grammatical gender) according to the
- // first day of the list
- let weeklyString = nounClass("repeatDetailsDay" + days[0], "weeklyNthOn");
- for (let i = 0; i < days.length; i++) {
- if (rule.interval == 1) {
- weekdays += getRString(pluralWeekday("repeatDetailsDay" + days[i]));
- } else {
- weekdays += getRString("repeatDetailsDay" + days[i]);
- }
- if (days.length > 1 && i == (days.length - 2)) {
- weekdays += ' ' + getRString("repeatDetailsAnd") + ' ';
- } else if (i < days.length - 1) {
- weekdays += ', ';
- }
- }
-
- weeklyString = getRString(weeklyString, [weekdays]);
- ruleString= PluralForm.get(rule.interval, weeklyString)
- .replace("#2", rule.interval);
-
- } else {
- let weeklyString = getRString("weeklyEveryNth");
- ruleString = PluralForm.get(rule.interval, weeklyString)
- .replace("#1", rule.interval);
- }
- } else if (rule.type == 'MONTHLY') {
- if (checkRecurrenceRule(rule, ['BYDAY'])) {
- let weekdaysString_every = "";
- let weekdaysString_position = "";
- let byday = rule.getComponent("BYDAY", {});
- let firstDay = byday[0];
- // build two strings for weekdays with and without
- // "position" prefix, then join these strings
- for (let i = 0 ; i < byday.length; i++) {
- if (day_position(byday[i]) == 0) {
- if (!weekdaysString_every) {
- firstDay = byday[i];
- }
- weekdaysString_every += getRString(pluralWeekday("repeatDetailsDay" + byday[i])) + ", ";
- } else {
- if (day_position(byday[i]) < -1 || day_position(byday[i]) > 5) {
- // we support only weekdays with -1 as negative
- // position ('THE LAST ...')
- return getRString("ruleTooComplex");
- }
- if (byday.some(function(element) {
- return (day_position(element) == 0 &&
- day_of_week(byday[i]) == day_of_week(element));
- })) {
- // prevent to build strings such as for example:
- // "every Monday and the second Monday..."
- continue;
- }
- let ordinalString = "repeatOrdinal" + day_position(byday[i]);
- let dayString = "repeatDetailsDay" + day_of_week(byday[i]);
- ordinalString = nounClass(dayString, ordinalString);
- ordinalString = getRString(ordinalString);
- dayString = getRString(dayString);
- let stringOrdinalWeekday = getRString("ordinalWeekdayOrder",
- [ordinalString, dayString]);
- weekdaysString_position += stringOrdinalWeekday + ", ";
- }
- }
- let weekdaysString = weekdaysString_every + weekdaysString_position;
- weekdaysString = weekdaysString.slice(0,-2).
- replace(/,(?= [^,]*$)/, ' ' + getRString("repeatDetailsAnd"));
-
- let monthlyString = weekdaysString_every ? "monthlyEveryOfEvery" : "monthlyRuleNthOfEvery";
- monthlyString = nounClass("repeatDetailsDay" + day_of_week(firstDay), monthlyString);
- monthlyString = getRString(monthlyString, [weekdaysString]);
- ruleString = PluralForm.get(rule.interval, monthlyString).
- replace("#2", rule.interval);
- } else if (checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
- let component = rule.getComponent("BYMONTHDAY", {});
-
- // First, find out if the 'BYMONTHDAY' component contains
- // any elements with a negative value. If so we currently
- // don't support anything but the 'last day of the month' rule.
- if (component.some(function(element, index, array) {
- return element < 0;
- })) {
- if (component.length == 1 && component[0] == -1) {
- let monthlyString = getRString("monthlyLastDayOfNth");
- ruleString = PluralForm.get(rule.interval, monthlyString)
- .replace("#1", rule.interval);
- } else {
- // we don't support any other combination for now...
- return getRString("ruleTooComplex");
- }
- } else {
- if (component.length == 31 &&
- component.every(function (element, index, array) {
- for (let i = 0; i < array.length; i++) {
- if ((index + 1) == array[i]) {
- return true;
- }
- }
- return false;
- })) {
- // i.e. every day every N months
- ruleString = getRString("monthlyEveryDayOfNth");
- ruleString = PluralForm.get(rule.interval, ruleString)
- .replace("#2", rule.interval);
- } else {
- // i.e. one or more monthdays every N months
- let day_string = "";
- for (let i = 0; i < component.length; i++) {
- day_string += component[i];
- if (component.length > 1 &&
- i == (component.length - 2)) {
- day_string += ' ' + getRString("repeatDetailsAnd") + ' ';
- } else if (i < component.length-1) {
- day_string += ', ';
- }
- }
- let monthlyString = getRString("monthlyDayOfNth", [day_string]);
- ruleString = PluralForm.get(rule.interval, monthlyString)
- .replace("#2", rule.interval);
- }
- }
- } else {
- let monthlyString = getRString("monthlyDayOfNth", [startDate.day]);
- ruleString = PluralForm.get(rule.interval, monthlyString)
- .replace("#2", rule.interval);
- }
- } else if (rule.type == 'YEARLY') {
- let bymonth = rule.getComponent("BYMONTH", {});
- if (checkRecurrenceRule(rule, ['BYMONTH']) &&
- checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
- let bymonthday = rule.getComponent("BYMONTHDAY", {});
-
- if (bymonth.length == 1 && bymonthday.length == 1) {
- let monthNameString = getRString("repeatDetailsMonth" + bymonth[0]);
-
- let yearlyString = getRString("yearlyNthOn",
- [monthNameString, bymonthday[0]]);
- ruleString = PluralForm.get(rule.interval, yearlyString)
- .replace("#3", rule.interval);
- }
- } else if (checkRecurrenceRule(rule, ['BYMONTH']) &&
- checkRecurrenceRule(rule, ['BYDAY'])) {
- let byday = rule.getComponent("BYDAY", {});
-
- if (bymonth.length == 1 && byday.length == 1) {
- let dayString = "repeatDetailsDay" + day_of_week(byday[0]);
- let month = getRString("repeatDetailsMonth" + bymonth[0]);
- if (day_position(byday[0]) == 0) {
- let yearlyString = "yearlyOnEveryNthOfNth";
- yearlyString = nounClass(dayString, yearlyString);
- let day = getRString(pluralWeekday(dayString));
- yearlyString = getRString(yearlyString, [day, month]);
- ruleString = PluralForm.get(rule.interval, yearlyString)
- .replace("#3", rule.interval);
- } else {
- let yearlyString = "yearlyNthOnNthOf";
- let ordinalString = "repeatOrdinal" + day_position(byday[0])
- yearlyString = nounClass(dayString, yearlyString);
- ordinalString = nounClass(dayString, ordinalString);
- let ordinal = getRString(ordinalString);
- let day = getRString(dayString);
- yearlyString = getRString(yearlyString, [ordinal, day, month]);
- ruleString = PluralForm.get(rule.interval, yearlyString)
- .replace("#4", rule.interval);
- }
- } else {
- return getRString("ruleTooComplex");
- }
- } else {
- let monthNameString = getRString("repeatDetailsMonth" + (startDate.month + 1));
-
- let yearlyString = getRString("yearlyNthOn",
- [monthNameString, startDate.day]);
- ruleString = PluralForm.get(rule.interval, yearlyString)
- .replace("#3", rule.interval);
- }
- }
-
- let kDefaultTimezone = cal.calendarDefaultTimezone();
- let dateFormatter = cal.getDateFormatter();
-
- let detailsString;
- if (!endDate || allDay) {
- if (rule.isFinite) {
- if (rule.isByCount) {
- let countString = getRString("repeatCountAllDay",
- [ruleString,
- dateFormatter.formatDateShort(startDate)]);
- detailsString = PluralForm.get(rule.count, countString)
- .replace("#3", rule.count);
- } else {
- let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
- detailsString = getRString("repeatDetailsUntilAllDay",
- [ruleString,
- dateFormatter.formatDateShort(startDate),
- dateFormatter.formatDateShort(untilDate)]);
- }
- } else {
- detailsString = getRString("repeatDetailsInfiniteAllDay",
- [ruleString,
- dateFormatter.formatDateShort(startDate)]);
- }
- } else {
- if (rule.isFinite) {
- if (rule.isByCount) {
- let countString = getRString("repeatCount",
- [ruleString,
- dateFormatter.formatDateShort(startDate),
- dateFormatter.formatTime(startDate),
- dateFormatter.formatTime(endDate) ]);
- detailsString = PluralForm.get(rule.count, countString)
- .replace("#5", rule.count);
- } else {
- let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
- detailsString = getRString("repeatDetailsUntil",
- [ruleString,
- dateFormatter.formatDateShort(startDate),
- dateFormatter.formatDateShort(untilDate),
- dateFormatter.formatTime(startDate),
- dateFormatter.formatTime(endDate)]);
- }
- } else {
- detailsString = getRString("repeatDetailsInfinite",
- [ruleString,
- dateFormatter.formatDateShort(startDate),
- dateFormatter.formatTime(startDate),
- dateFormatter.formatTime(endDate) ]);
- }
- }
- return detailsString;
- }
- }
- return getRString("ruleTooComplex");
-}
-
-/**
- * Split rules into negative and positive rules.
- *
- * @param recurrenceInfo An item's recurrence info to parse.
- * @return An array with two elements: an array of positive
- * rules and an array of negative rules.
- */
-function splitRecurrenceRules(recurrenceInfo) {
- var ritems = recurrenceInfo.getRecurrenceItems({});
- var rules = [];
- var exceptions = [];
- for each (var r in ritems) {
- if (r.isNegative) {
- exceptions.push(r);
- } else {
- rules.push(r);
- }
- }
- return [rules, exceptions];
-}
-
-/**
- * Check if a recurrence rule's component is valid.
- *
- * @see calIRecurrenceRule
- * @param aRule The recurrence rule to check.
- * @param aArray An array of component names to check.
- * @return Returns true if the rule is valid.
- */
-function checkRecurrenceRule(aRule, aArray) {
- for each (var comp in aArray) {
- var ruleComp = aRule.getComponent(comp, {});
- if (ruleComp && ruleComp.length > 0) {
- return true;
- }
- }
- return false;
-}
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
/**
* Dispose of controlling operations of this event dialog. Uses
* window.arguments[0].job.dispose()
*/
function dispose() {
var args = window.arguments[0];
if (args.job && args.job.dispose) {
--- a/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
+++ b/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
@@ -1,12 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+
var gIsReadOnly = false;
var gStartTime = null;
var gEndTime = null;
var gUntilDate = null;
/**
* Sets up the recurrence dialog from the window arguments. Takes care of filling
* the dialog controls with the recurrence information for this window.
--- a/calendar/base/content/dialogs/calendar-event-dialog.js
+++ b/calendar/base/content/dialogs/calendar-event-dialog.js
@@ -1,14 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://calendar/modules/calUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
// the following variables are constructed if the jsContext this file
// belongs to gets constructed. all those variables are meant to be accessed
// from within this file only.
var gStartTime = null;
var gEndTime = null;
var gItemDuration = null;
var gStartTimezone = null;
@@ -2996,37 +2997,39 @@ function updateRepeatDetails() {
let event = cal.isEvent(item);
let startDate = getElementValue(event ? "event-starttime" : "todo-entrydate");
let endDate = getElementValue(event ? "event-endtime" : "todo-duedate");
startDate = jsDateToDateTime(startDate, kDefaultTimezone);
endDate = jsDateToDateTime(endDate, kDefaultTimezone);
let allDay = getElementValue("event-all-day", "checked");
- let detailsString = recurrenceRule2String(
- recurrenceInfo, startDate, endDate, allDay);
+ let detailsString = recurrenceRule2String(recurrenceInfo, startDate,
+ endDate, allDay);
+
+ if (!detailsString) {
+ detailsString = cal.calGetString("calendar-event-dialog", "ruleTooComplex");
+ }
// Now display the string...
- if (detailsString) {
- let lines = detailsString.split("\n");
- repeatDetails.removeAttribute("collapsed");
- while (repeatDetails.childNodes.length > lines.length) {
- repeatDetails.removeChild(repeatDetails.lastChild);
+ let lines = detailsString.split("\n");
+ repeatDetails.removeAttribute("collapsed");
+ while (repeatDetails.childNodes.length > lines.length) {
+ repeatDetails.removeChild(repeatDetails.lastChild);
+ }
+ let numChilds = repeatDetails.childNodes.length;
+ for (let i = 0; i < lines.length; i++) {
+ if (i >= numChilds) {
+ var newNode = repeatDetails.childNodes[0]
+ .cloneNode(true);
+ repeatDetails.appendChild(newNode);
}
- let numChilds = repeatDetails.childNodes.length;
- for (let i = 0; i < lines.length; i++) {
- if (i >= numChilds) {
- var newNode = repeatDetails.childNodes[0]
- .cloneNode(true);
- repeatDetails.appendChild(newNode);
- }
- repeatDetails.childNodes[i].value = lines[i];
- repeatDetails.childNodes[i].setAttribute("tooltiptext",
- detailsString);
- }
+ repeatDetails.childNodes[i].value = lines[i];
+ repeatDetails.childNodes[i].setAttribute("tooltiptext",
+ detailsString);
}
} else {
let repeatDetails = document.getElementById("repeat-details");
repeatDetails.setAttribute("collapsed", "true");
}
}
/**
--- a/calendar/base/content/dialogs/calendar-summary-dialog.js
+++ b/calendar/base/content/dialogs/calendar-summary-dialog.js
@@ -1,15 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://calendar/modules/calUtils.jsm");
Components.utils.import("resource://calendar/modules/calItipUtils.jsm");
Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
/**
* Sets up the summary dialog, setting all needed fields on the dialog from the
* item received in the window arguments.
*/
function onLoad() {
var args = window.arguments[0];
var item = args.calendarEvent;
@@ -257,37 +258,38 @@ function updateRepeatDetails() {
repeatDetails.setAttribute("collapsed", "true");
// Try to create a descriptive string from the rule(s).
var kDefaultTimezone = calendarDefaultTimezone();
var startDate = item.startDate || item.entryDate;
var endDate = item.endDate || item.dueDate;
startDate = startDate ? startDate.getInTimezone(kDefaultTimezone) : null;
endDate = endDate ? endDate.getInTimezone(kDefaultTimezone) : null;
- var detailsString = recurrenceRule2String(
- recurrenceInfo, startDate, endDate, startDate.isDate);
+ var detailsString = recurrenceRule2String(recurrenceInfo, startDate,
+ endDate, startDate.isDate);
+
+ if (!detailsString) {
+ detailsString = cal.calGetString("calendar-event-dialog", "ruleTooComplexSummary");
+ }
// Now display the string...
- if (detailsString) {
- var lines = detailsString.split("\n");
- repeatDetails.removeAttribute("collapsed");
- while (repeatDetails.childNodes.length > lines.length) {
- repeatDetails.removeChild(repeatDetails.lastChild);
+ var lines = detailsString.split("\n");
+ repeatDetails.removeAttribute("collapsed");
+ while (repeatDetails.childNodes.length > lines.length) {
+ repeatDetails.removeChild(repeatDetails.lastChild);
+ }
+ var numChilds = repeatDetails.childNodes.length;
+ for (var i = 0; i < lines.length; i++) {
+ if (i >= numChilds) {
+ var newNode = repeatDetails.childNodes[0]
+ .cloneNode(true);
+ repeatDetails.appendChild(newNode);
}
- var numChilds = repeatDetails.childNodes.length;
- for (var i = 0; i < lines.length; i++) {
- if (i >= numChilds) {
- var newNode = repeatDetails.childNodes[0]
- .cloneNode(true);
- repeatDetails.appendChild(newNode);
- }
- repeatDetails.childNodes[i].value = lines[i];
- repeatDetails.childNodes[i].setAttribute("tooltiptext",
- detailsString);
- }
+ repeatDetails.childNodes[i].value = lines[i];
+ repeatDetails.childNodes[i].setAttribute("tooltiptext", detailsString);
}
}
/**
* Updates the attendee listbox, displaying all attendees invited to the
* window's item.
*/
function updateAttendees() {
--- a/calendar/base/modules/Makefile.in
+++ b/calendar/base/modules/Makefile.in
@@ -15,13 +15,14 @@ EXTRA_JS_MODULES = \
calAlarmUtils.jsm \
calAuthUtils.jsm \
calHashedArray.jsm \
calItemUtils.jsm \
calIteratorUtils.jsm \
calItipUtils.jsm \
calPrintUtils.jsm \
calProviderUtils.jsm \
+ calRecurrenceUtils.jsm \
calUtils.jsm \
calXMLUtils.jsm \
$(NULL)
include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/calendar/base/modules/calRecurrenceUtils.jsm
@@ -0,0 +1,359 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+EXPORTED_SYMBOLS = ["recurrenceRule2String", "splitRecurrenceRules", "checkRecurrenceRule"];
+
+/**
+ * This function takes the recurrence info passed as argument and creates a
+ * literal string representing the repeat pattern in natural language.
+ *
+ * @param recurrenceInfo An item's recurrence info to parse.
+ * @param startDate The start date to base rules on.
+ * @param endDate The end date to base rules on.
+ * @param allDay If true, the pattern should assume an allday item.
+ * @return A human readable string describing the recurrence.
+ */
+function recurrenceRule2String(recurrenceInfo, startDate, endDate, allDay) {
+ function getRString(name, args) cal.calGetString("calendar-event-dialog", name, args);
+
+ // Retrieve a valid recurrence rule from the currently
+ // set recurrence info. Bail out if there's more
+ // than a single rule or something other than a rule.
+ recurrenceInfo = recurrenceInfo.clone();
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ if (rrules[0].length == 1) {
+ let rule = rrules[0][0];
+ // currently we don't allow for any BYxxx-rules.
+ if (cal.calInstanceOf(rule, Components.interfaces.calIRecurrenceRule) &&
+ !checkRecurrenceRule(rule, ['BYSECOND',
+ 'BYMINUTE',
+ //'BYDAY',
+ 'BYHOUR',
+ //'BYMONTHDAY',
+ 'BYYEARDAY',
+ 'BYWEEKNO',
+ //'BYMONTH',
+ 'BYSETPOS'])) {
+ function day_of_week(day) {
+ return Math.abs(day) % 8;
+ }
+ function day_position(day) {
+ let dow = day_of_week(day);
+ return (Math.abs(day) - dow) / 8 * (day < 0 ? -1 : 1);
+ }
+ function nounClass(aDayString, aRuleString) {
+ // Select noun class (grammatical gender) for rule string
+ let nounClass = getRString(aDayString + "Nounclass");
+ return aRuleString + nounClass.substr(0, 1).toUpperCase() +
+ nounClass.substr(1);
+ }
+ function pluralWeekday(aDayString) {
+ let plural = getRString("pluralForWeekdays") == "true";
+ return (plural ? aDayString + "Plural" : aDayString);
+ }
+
+ let ruleString;
+ if (rule.type == 'DAILY') {
+ if (checkRecurrenceRule(rule, ['BYDAY'])) {
+ let days = rule.getComponent("BYDAY", {});
+ let weekdays = [2, 3, 4, 5, 6];
+ if (weekdays.length == days.length) {
+ let i;
+ for (i = 0; i < weekdays.length; i++) {
+ if (weekdays[i] != days[i]) {
+ break;
+ }
+ }
+ if (i == weekdays.length) {
+ ruleString = getRString("repeatDetailsRuleDaily4");
+ }
+ } else {
+ return null;
+ }
+ } else {
+ let dailyString = getRString("dailyEveryNth");
+ ruleString = PluralForm.get(rule.interval, dailyString)
+ .replace("#1", rule.interval);
+ }
+ } else if (rule.type == 'WEEKLY') {
+ // weekly recurrence, currently we
+ // support a single 'BYDAY'-rule only.
+ if (checkRecurrenceRule(rule, ['BYDAY'])) {
+ // create a string like 'Monday, Tuesday and Wednesday'
+ let days = rule.getComponent("BYDAY", {});
+ let weekdays = "";
+ // select noun class (grammatical gender) according to the
+ // first day of the list
+ let weeklyString = nounClass("repeatDetailsDay" + days[0], "weeklyNthOn");
+ for (let i = 0; i < days.length; i++) {
+ if (rule.interval == 1) {
+ weekdays += getRString(pluralWeekday("repeatDetailsDay" + days[i]));
+ } else {
+ weekdays += getRString("repeatDetailsDay" + days[i]);
+ }
+ if (days.length > 1 && i == (days.length - 2)) {
+ weekdays += ' ' + getRString("repeatDetailsAnd") + ' ';
+ } else if (i < days.length - 1) {
+ weekdays += ', ';
+ }
+ }
+
+ weeklyString = getRString(weeklyString, [weekdays]);
+ ruleString= PluralForm.get(rule.interval, weeklyString)
+ .replace("#2", rule.interval);
+
+ } else {
+ let weeklyString = getRString("weeklyEveryNth");
+ ruleString = PluralForm.get(rule.interval, weeklyString)
+ .replace("#1", rule.interval);
+ }
+ } else if (rule.type == 'MONTHLY') {
+ if (checkRecurrenceRule(rule, ['BYDAY'])) {
+ let weekdaysString_every = "";
+ let weekdaysString_position = "";
+ let byday = rule.getComponent("BYDAY", {});
+ let firstDay = byday[0];
+ // build two strings for weekdays with and without
+ // "position" prefix, then join these strings
+ for (let i = 0 ; i < byday.length; i++) {
+ if (day_position(byday[i]) == 0) {
+ if (!weekdaysString_every) {
+ firstDay = byday[i];
+ }
+ weekdaysString_every += getRString(pluralWeekday("repeatDetailsDay" + byday[i])) + ", ";
+ } else {
+ if (day_position(byday[i]) < -1 || day_position(byday[i]) > 5) {
+ // we support only weekdays with -1 as negative
+ // position ('THE LAST ...')
+ return null;
+ }
+ if (byday.some(function(element) {
+ return (day_position(element) == 0 &&
+ day_of_week(byday[i]) == day_of_week(element));
+ })) {
+ // prevent to build strings such as for example:
+ // "every Monday and the second Monday..."
+ continue;
+ }
+ let ordinalString = "repeatOrdinal" + day_position(byday[i]);
+ let dayString = "repeatDetailsDay" + day_of_week(byday[i]);
+ ordinalString = nounClass(dayString, ordinalString);
+ ordinalString = getRString(ordinalString);
+ dayString = getRString(dayString);
+ let stringOrdinalWeekday = getRString("ordinalWeekdayOrder",
+ [ordinalString, dayString]);
+ weekdaysString_position += stringOrdinalWeekday + ", ";
+ }
+ }
+ let weekdaysString = weekdaysString_every + weekdaysString_position;
+ weekdaysString = weekdaysString.slice(0,-2).
+ replace(/,(?= [^,]*$)/, ' ' + getRString("repeatDetailsAnd"));
+
+ let monthlyString = weekdaysString_every ? "monthlyEveryOfEvery" : "monthlyRuleNthOfEvery";
+ monthlyString = nounClass("repeatDetailsDay" + day_of_week(firstDay), monthlyString);
+ monthlyString = getRString(monthlyString, [weekdaysString]);
+ ruleString = PluralForm.get(rule.interval, monthlyString).
+ replace("#2", rule.interval);
+ } else if (checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
+ let component = rule.getComponent("BYMONTHDAY", {});
+
+ // First, find out if the 'BYMONTHDAY' component contains
+ // any elements with a negative value. If so we currently
+ // don't support anything but the 'last day of the month' rule.
+ if (component.some(function(element, index, array) {
+ return element < 0;
+ })) {
+ if (component.length == 1 && component[0] == -1) {
+ let monthlyString = getRString("monthlyLastDayOfNth");
+ ruleString = PluralForm.get(rule.interval, monthlyString)
+ .replace("#1", rule.interval);
+ } else {
+ // we don't support any other combination for now...
+ return null;
+ }
+ } else {
+ if (component.length == 31 &&
+ component.every(function (element, index, array) {
+ for (let i = 0; i < array.length; i++) {
+ if ((index + 1) == array[i]) {
+ return true;
+ }
+ }
+ return false;
+ })) {
+ // i.e. every day every N months
+ ruleString = getRString("monthlyEveryDayOfNth");
+ ruleString = PluralForm.get(rule.interval, ruleString)
+ .replace("#2", rule.interval);
+ } else {
+ // i.e. one or more monthdays every N months
+ let day_string = "";
+ for (let i = 0; i < component.length; i++) {
+ day_string += component[i];
+ if (component.length > 1 &&
+ i == (component.length - 2)) {
+ day_string += ' ' + getRString("repeatDetailsAnd") + ' ';
+ } else if (i < component.length-1) {
+ day_string += ', ';
+ }
+ }
+ let monthlyString = getRString("monthlyDayOfNth", [day_string]);
+ ruleString = PluralForm.get(rule.interval, monthlyString)
+ .replace("#2", rule.interval);
+ }
+ }
+ } else {
+ let monthlyString = getRString("monthlyDayOfNth", [startDate.day]);
+ ruleString = PluralForm.get(rule.interval, monthlyString)
+ .replace("#2", rule.interval);
+ }
+ } else if (rule.type == 'YEARLY') {
+ let bymonth = rule.getComponent("BYMONTH", {});
+ if (checkRecurrenceRule(rule, ['BYMONTH']) &&
+ checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
+ let bymonthday = rule.getComponent("BYMONTHDAY", {});
+
+ if (bymonth.length == 1 && bymonthday.length == 1) {
+ let monthNameString = getRString("repeatDetailsMonth" + bymonth[0]);
+
+ let yearlyString = getRString("yearlyNthOn",
+ [monthNameString, bymonthday[0]]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#3", rule.interval);
+ }
+ } else if (checkRecurrenceRule(rule, ['BYMONTH']) &&
+ checkRecurrenceRule(rule, ['BYDAY'])) {
+ let byday = rule.getComponent("BYDAY", {});
+
+ if (bymonth.length == 1 && byday.length == 1) {
+ let dayString = "repeatDetailsDay" + day_of_week(byday[0]);
+ let month = getRString("repeatDetailsMonth" + bymonth[0]);
+ if (day_position(byday[0]) == 0) {
+ let yearlyString = "yearlyOnEveryNthOfNth";
+ yearlyString = nounClass(dayString, yearlyString);
+ let day = getRString(pluralWeekday(dayString));
+ yearlyString = getRString(yearlyString, [day, month]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#3", rule.interval);
+ } else {
+ let yearlyString = "yearlyNthOnNthOf";
+ let ordinalString = "repeatOrdinal" + day_position(byday[0])
+ yearlyString = nounClass(dayString, yearlyString);
+ ordinalString = nounClass(dayString, ordinalString);
+ let ordinal = getRString(ordinalString);
+ let day = getRString(dayString);
+ yearlyString = getRString(yearlyString, [ordinal, day, month]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#4", rule.interval);
+ }
+ } else {
+ return null;
+ }
+ } else {
+ let monthNameString = getRString("repeatDetailsMonth" + (startDate.month + 1));
+
+ let yearlyString = getRString("yearlyNthOn",
+ [monthNameString, startDate.day]);
+ ruleString = PluralForm.get(rule.interval, yearlyString)
+ .replace("#3", rule.interval);
+ }
+ }
+
+ let kDefaultTimezone = cal.calendarDefaultTimezone();
+ let dateFormatter = cal.getDateFormatter();
+
+ let detailsString;
+ if (!endDate || allDay) {
+ if (rule.isFinite) {
+ if (rule.isByCount) {
+ let countString = getRString("repeatCountAllDay",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate)]);
+ detailsString = PluralForm.get(rule.count, countString)
+ .replace("#3", rule.count);
+ } else {
+ let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
+ detailsString = getRString("repeatDetailsUntilAllDay",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatDateShort(untilDate)]);
+ }
+ } else {
+ detailsString = getRString("repeatDetailsInfiniteAllDay",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate)]);
+ }
+ } else {
+ if (rule.isFinite) {
+ if (rule.isByCount) {
+ let countString = getRString("repeatCount",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatTime(startDate),
+ dateFormatter.formatTime(endDate) ]);
+ detailsString = PluralForm.get(rule.count, countString)
+ .replace("#5", rule.count);
+ } else {
+ let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
+ detailsString = getRString("repeatDetailsUntil",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatDateShort(untilDate),
+ dateFormatter.formatTime(startDate),
+ dateFormatter.formatTime(endDate)]);
+ }
+ } else {
+ detailsString = getRString("repeatDetailsInfinite",
+ [ruleString,
+ dateFormatter.formatDateShort(startDate),
+ dateFormatter.formatTime(startDate),
+ dateFormatter.formatTime(endDate) ]);
+ }
+ }
+ return detailsString;
+ }
+ }
+ return null;
+}
+
+/**
+ * Split rules into negative and positive rules.
+ *
+ * @param recurrenceInfo An item's recurrence info to parse.
+ * @return An array with two elements: an array of positive
+ * rules and an array of negative rules.
+ */
+function splitRecurrenceRules(recurrenceInfo) {
+ var ritems = recurrenceInfo.getRecurrenceItems({});
+ var rules = [];
+ var exceptions = [];
+ for each (var r in ritems) {
+ if (r.isNegative) {
+ exceptions.push(r);
+ } else {
+ rules.push(r);
+ }
+ }
+ return [rules, exceptions];
+}
+
+/**
+ * Check if a recurrence rule's component is valid.
+ *
+ * @see calIRecurrenceRule
+ * @param aRule The recurrence rule to check.
+ * @param aArray An array of component names to check.
+ * @return Returns true if the rule is valid.
+ */
+function checkRecurrenceRule(aRule, aArray) {
+ for each (var comp in aArray) {
+ var ruleComp = aRule.getComponent(comp, {});
+ if (ruleComp && ruleComp.length > 0) {
+ return true;
+ }
+ }
+ return false;
+}
--- a/calendar/lightning/components/lightningTextCalendarConverter.js
+++ b/calendar/lightning/components/lightningTextCalendarConverter.js
@@ -1,16 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://calendar/modules/calUtils.jsm");
Components.utils.import("resource://calendar/modules/calXMLUtils.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
function ltnMimeConverter() {
}
ltnMimeConverter.prototype = {
classID: Components.ID("{c70acb08-464e-4e55-899d-b2c84c5409fa}"),
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISimpleMimeConverter]),
@@ -58,22 +59,75 @@ ltnMimeConverter.prototype = {
node.appendChild(a);
localText = localText.substr(endPos);
}
},
/**
+ * Returns a header title for an ITIP item depending on the response method
+ * @param aItipItem the event
+ * @return string the header title
+ */
+ getItipHeader: function getItipHeader(aItipItem) {
+ let header;
+
+ if (aItipItem) {
+ let item = aItipItem.getItemList({})[0];
+ let summary = item.getProperty("SUMMARY") || "";
+ let organizer = item.organizer;
+ let organizerString = organizer.toString();
+ if (organizer.commonName) {
+ organizerString = organizer.commonName;
+ }
+
+ switch (aItipItem.responseMethod) {
+ case "REQUEST":
+ header = cal.calGetString("lightning",
+ "itipRequestBody",
+ [organizerString, summary],
+ "lightning");
+ break;
+ case "CANCEL":
+ header = cal.calGetString("lightning",
+ "itipCancelBody",
+ [organizerString, summary],
+ "lightning");
+ break;
+ case "REPLY": {
+ // Generate proper body from my participation status
+ let sender = cal.getInvitedAttendee(item);
+ let statusString = (sender.participationStatus == "DECLINED" ?
+ "itipReplyBodyDecline": "itipReplyBodyAccept");
+
+ header = cal.calGetString("lightning",
+ statusString,
+ [sender],
+ "lightning");
+ break;
+ }
+ }
+ }
+
+ if (!header) {
+ header = cal.calGetString("lightning", "imipHtml.header", null, "lightning");
+ }
+
+ return header;
+ },
+
+ /**
* Returns the html representation of the event as a DOM document.
*
- * @param event The calIItemBase to parse into html.
- * @return The DOM document with values filled in.
+ * @param event The calIItemBase to parse into html.
+ * @param aNewItipItem The parsed itip item.
+ * @return The DOM document with values filled in.
*/
- createHtml: function createHtml(event) {
+ createHtml: function createHtml(event, aNewItipItem) {
// Creates HTML using the Node strings in the properties file
let doc = cal.xml.parseFile("chrome://lightning/content/lightning-invitation.xhtml");
let self = this;
function field(field, contentText, linkify) {
let descr = doc.getElementById("imipHtml-" + field + "-descr");
if (descr) {
let labelText = cal.calGetString("lightning", "imipHtml." + field, null, "lightning");
descr.textContent = labelText;
@@ -86,20 +140,40 @@ ltnMimeConverter.prototype = {
self.linkifyText(contentText, content);
} else {
content.textContent = contentText;
}
}
}
// Simple fields
- field("header", null);
+ let headerDescr = doc.getElementById("imipHtml-header-descr");
+ if (headerDescr) {
+ headerDescr.textContent = this.getItipHeader(aNewItipItem);
+ }
+
field("summary", event.title);
field("location", event.getProperty("LOCATION"));
- field("when", cal.getDateFormatter().formatItemInterval(event));
+
+ let dateString = cal.getDateFormatter().formatItemInterval(event);
+
+ if (event.recurrenceInfo) {
+ let kDefaultTimezone = cal.calendarDefaultTimezone();
+ let startDate = event.startDate;
+ let endDate = event.endDate;
+ startDate = startDate ? startDate.getInTimezone(kDefaultTimezone) : null;
+ endDate = endDate ? endDate.getInTimezone(kDefaultTimezone) : null;
+ let repeatString = recurrenceRule2String(event.recurrenceInfo, startDate,
+ endDate, startDate.isDate);
+ if (repeatString) {
+ dateString = repeatString;
+ }
+ }
+
+ field("when", dateString);
field("comment", event.getProperty("COMMENT"), true);
// DESCRIPTION field
let eventDescription = (event.getProperty("DESCRIPTION") || "")
/* Remove the useless "Outlookism" squiggle. */
.replace("*~*~*~*~*~*~*~*~*~*", "");
field("description", eventDescription, true);
@@ -154,51 +228,49 @@ ltnMimeConverter.prototype = {
item = exc;
}
}
event = item;
break;
}
}
if (!event) {
- return;
+ return '';
}
- // Create the HTML string for display
- let serializer = Components.classes["@mozilla.org/xmlextras/xmlserializer;1"]
- .createInstance(Components.interfaces.nsIDOMSerializer);
- let html = serializer.serializeToString(this.createHtml(event));
+ let itipItem = null;
try {
// this.uri is the message URL that we are processing.
// We use it to get the nsMsgHeaderSink to store the calItipItem.
if (this.uri) {
let msgWindow = null;
try {
let msgUrl = this.uri.QueryInterface(Components.interfaces.nsIMsgMailNewsUrl);
// msgWindow is optional in some scenarios
// (e.g. gloda in action, throws NS_ERROR_INVALID_POINTER then)
msgWindow = msgUrl.msgWindow;
} catch (exc) {
}
if (msgWindow) {
- let itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"]
+ itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"]
.createInstance(Components.interfaces.calIItipItem);
itipItem.init(data);
let sinkProps = msgWindow.msgHeaderSink.properties;
sinkProps.setPropertyAsInterface("itipItem", itipItem);
// Notify the observer that the itipItem is available
let observer = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
observer.notifyObservers(null, "onItipItemCreation", 0);
}
}
} catch (e) {
cal.ERROR("[ltnMimeConverter] convertToHTML: " + e);
}
- return html;
+ // Create the HTML string for display
+ return cal.xml.serializeDOM(this.createHtml(event, itipItem));
}
};
var NSGetFactory = XPCOMUtils.generateNSGetFactory([ltnMimeConverter]);
--- a/calendar/locales/en-US/chrome/calendar/calendar-event-dialog.properties
+++ b/calendar/locales/en-US/chrome/calendar/calendar-event-dialog.properties
@@ -281,20 +281,25 @@ repeatDetailsInfinite=Occurs %1$S\neffec
# %1%$ - A rule string (see above). This is the first line of the link
# %2%$ - event start date (e.g. mm/gg/yyyy)
# e.g. with monthlyDayOfNth and all day event:
# "Occurs day 3 of every 5 month
# effective 1/1/2009"
repeatDetailsInfiniteAllDay=Occurs %1$S\neffective %2$S.
# LOCALIZATION NOTE (ruleTooComplex):
-# This string is shown in the reminder details area if our code can't handle the
+# This string is shown in the repeat details area if our code can't handle the
# complexity of the recurrence rule yet.
ruleTooComplex=Click here for details
+# LOCALIZATION NOTE (ruleTooComplexSummary):
+# This string is shown in the event summary dialog if our code can't handle the
+# complexity of the recurrence rule yet.
+ruleTooComplexSummary=Repeat details unknown
+
# differences between the dialog for an Event or a Task
newEvent=New Event
newTask=New Task
itemMenuLabelEvent=Event
itemMenuAccesskeyEvent2=T
itemMenuLabelTask=Task
itemMenuAccesskeyTask2=T