summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js1237
1 files changed, 1237 insertions, 0 deletions
diff --git a/comm/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js b/comm/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
new file mode 100644
index 0000000000..379e5e387e
--- /dev/null
+++ b/comm/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js
@@ -0,0 +1,1237 @@
+/* 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/. */
+
+var { splitRecurrenceRules } = ChromeUtils.import(
+ "resource:///modules/calendar/calRecurrenceUtils.jsm"
+);
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm",
+});
+
+var gIsReadOnly = false;
+var gStartTime = null;
+var gEndTime = null;
+var gUntilDate = null;
+
+window.addEventListener("load", onLoad);
+
+/**
+ * Object wrapping the methods and properties of recurrencePreview binding.
+ */
+const RecurrencePreview = {
+ /**
+ * Initializes some properties and adds event listener to the #recurrencePreview node.
+ */
+ init() {
+ this.node = document.getElementById("recurrencePreview");
+ this.mRecurrenceInfo = null;
+ this.mResizeHandler = null;
+ this.mDateTime = null;
+ document.getElementById("recurrencePrevious").addEventListener("click", () => {
+ this.showPreviousMonth();
+ });
+ document.getElementById("recurrenceNext").addEventListener("click", () => {
+ this.showNextMonth();
+ });
+ document.getElementById("recurrenceToday").addEventListener("click", () => {
+ this.jumpToToday();
+ });
+ this.togglePreviousMonthButton();
+ },
+ /**
+ * Setter for mDateTime property.
+ *
+ * @param {Date} val - The date value that is to be set.
+ */
+ set dateTime(val) {
+ this.mDateTime = val.clone();
+ },
+ /**
+ * Getter for mDateTime property.
+ */
+ get dateTime() {
+ if (this.mDateTime == null) {
+ this.mDateTime = cal.dtz.now();
+ }
+ return this.mDateTime;
+ },
+ /**
+ * Updates content of #recurrencePreview node.
+ */
+ updateContent() {
+ let date = cal.dtz.dateTimeToJsDate(this.dateTime);
+ for (let minimonth of this.node.children) {
+ minimonth.showMonth(date);
+ date.setMonth(date.getMonth() + 1);
+ }
+ },
+ /**
+ * Updates preview of #recurrencePreview node.
+ */
+ updatePreview(recurrenceInfo) {
+ let minimonth = this.node.querySelector("calendar-minimonth");
+ this.node.style.minHeight = minimonth.getBoundingClientRect().height + "px";
+
+ this.mRecurrenceInfo = recurrenceInfo;
+ let start = this.dateTime.clone();
+ start.day = 1;
+ start.hour = 0;
+ start.minute = 0;
+ start.second = 0;
+ let end = start.clone();
+ end.month++;
+
+ for (let minimonth of this.node.children) {
+ // we now have one of the minimonth controls while 'start'
+ // and 'end' are set to the interval this minimonth shows.
+ minimonth.showMonth(cal.dtz.dateTimeToJsDate(start));
+ if (recurrenceInfo) {
+ // retrieve an array of dates that represents all occurrences
+ // that fall into this time interval [start,end[.
+ // note: the following loop assumes that this array contains
+ // dates that are strictly monotonically increasing.
+ // should getOccurrenceDates() not enforce this assumption we
+ // need to fall back to some different algorithm.
+ let dates = recurrenceInfo.getOccurrenceDates(start, end, 0);
+
+ // now run through all days of this month and set the
+ // 'busy' attribute with respect to the occurrence array.
+ let index = 0;
+ let occurrence = null;
+ if (index < dates.length) {
+ occurrence = dates[index++].getInTimezone(start.timezone);
+ }
+ let current = start.clone();
+ while (current.compare(end) < 0) {
+ let box = minimonth.getBoxForDate(current);
+ if (box) {
+ if (
+ occurrence &&
+ occurrence.day == current.day &&
+ occurrence.month == current.month &&
+ occurrence.year == current.year
+ ) {
+ box.setAttribute("busy", 1);
+ if (index < dates.length) {
+ occurrence = dates[index++].getInTimezone(start.timezone);
+ // take into account that the very next occurrence
+ // can happen at the same day as the previous one.
+ if (
+ occurrence.day == current.day &&
+ occurrence.month == current.month &&
+ occurrence.year == current.year
+ ) {
+ continue;
+ }
+ } else {
+ occurrence = null;
+ }
+ } else {
+ box.removeAttribute("busy");
+ }
+ }
+ current.day++;
+ }
+ }
+ start.month++;
+ end.month++;
+ }
+ },
+ /**
+ * Shows the previous month in the recurrence preview.
+ */
+ showPreviousMonth() {
+ let prevMinimonth = this.node.querySelector(`calendar-minimonth[active-month="true"]`);
+
+ let activeDate = this.previousMonthDate(
+ prevMinimonth.getAttribute("year"),
+ prevMinimonth.getAttribute("month")
+ );
+
+ if (activeDate) {
+ this.resetDisplayOfMonths();
+ this.displayCurrentMonths(activeDate);
+ this.togglePreviousMonthButton();
+ }
+ },
+ /**
+ * Shows the next month in the recurrence preview.
+ */
+ showNextMonth() {
+ let prevMinimonth = this.node.querySelector(`calendar-minimonth[active-month="true"]`);
+
+ let activeDate = this.nextMonthDate(
+ prevMinimonth.getAttribute("year"),
+ prevMinimonth.getAttribute("month")
+ );
+
+ if (activeDate) {
+ this.resetDisplayOfMonths();
+ this.displayCurrentMonths(activeDate);
+ this.togglePreviousMonthButton();
+ }
+ },
+ /**
+ * Shows the current day's month in the recurrence preview.
+ */
+ jumpToToday() {
+ let activeDate = new Date();
+ this.resetDisplayOfMonths();
+ this.displayCurrentMonths(activeDate);
+ this.togglePreviousMonthButton();
+ },
+ /**
+ * Selects the minimonth element belonging to a year and month.
+ */
+ selectMinimonth(year, month) {
+ let minimonthIdentifier = `calendar-minimonth[year="${year}"][month="${month}"]`;
+ let selectedMinimonth = this.node.querySelector(minimonthIdentifier);
+
+ if (selectedMinimonth) {
+ return selectedMinimonth;
+ }
+
+ selectedMinimonth = document.createXULElement("calendar-minimonth");
+ this.node.appendChild(selectedMinimonth);
+
+ selectedMinimonth.setAttribute("readonly", "true");
+ selectedMinimonth.setAttribute("month", month);
+ selectedMinimonth.setAttribute("year", year);
+ selectedMinimonth.hidden = true;
+
+ if (this.mRecurrenceInfo) {
+ this.updatePreview(this.mRecurrenceInfo);
+ }
+
+ return selectedMinimonth;
+ },
+ /**
+ * Returns the next month's first day when given a year and month.
+ */
+ nextMonthDate(currentYear, currentMonth) {
+ // If month is December, select first day of January
+ if (currentMonth == 11) {
+ return new Date(parseInt(currentYear) + 1, 0, 1);
+ }
+ return new Date(parseInt(currentYear), parseInt(currentMonth) + 1, 1);
+ },
+ /**
+ * Returns the previous month's first day when given a year and month.
+ */
+ previousMonthDate(currentYear, currentMonth) {
+ // If month is January, select first day of December.
+ if (currentMonth == 0) {
+ return new Date(parseInt(currentYear) - 1, 11, 1);
+ }
+ return new Date(parseInt(currentYear), parseInt(currentMonth) - 1, 1);
+ },
+ /**
+ * Reset the recurrence preview months, making all hidden and none set to active.
+ */
+ resetDisplayOfMonths() {
+ let calContainer = this.node;
+ for (let minimonth of calContainer.children) {
+ minimonth.hidden = true;
+ minimonth.setAttribute("active-month", false);
+ }
+ },
+ /**
+ * Display the active month and the next two months in the recurrence preview.
+ */
+ displayCurrentMonths(activeDate) {
+ let activeMonth = activeDate.getMonth();
+ let activeYear = activeDate.getFullYear();
+
+ let month1Date = this.nextMonthDate(activeYear, activeMonth);
+ let month2Date = this.nextMonthDate(month1Date.getFullYear(), month1Date.getMonth());
+
+ let activeMinimonth = this.selectMinimonth(activeYear, activeMonth);
+ let minimonth1 = this.selectMinimonth(month1Date.getFullYear(), month1Date.getMonth());
+ let minimonth2 = this.selectMinimonth(month2Date.getFullYear(), month2Date.getMonth());
+
+ activeMinimonth.setAttribute("active-month", true);
+ activeMinimonth.removeAttribute("hidden");
+ minimonth1.removeAttribute("hidden");
+ minimonth2.removeAttribute("hidden");
+ },
+ /**
+ * Disable previous month button when the active month is the first month of the event.
+ */
+ togglePreviousMonthButton() {
+ let activeMinimonth = this.node.querySelector(`calendar-minimonth[active-month="true"]`);
+
+ if (activeMinimonth.getAttribute("initial-month") == "true") {
+ document.getElementById("recurrencePrevious").setAttribute("disabled", "true");
+ } else {
+ document.getElementById("recurrencePrevious").removeAttribute("disabled");
+ }
+ },
+};
+
+/**
+ * An object containing the daypicker-weekday binding functionalities.
+ */
+const DaypickerWeekday = {
+ /**
+ * Method intitializing DaypickerWeekday.
+ */
+ init() {
+ this.weekStartOffset = Services.prefs.getIntPref("calendar.week.start", 0);
+
+ let mainbox = document.getElementById("daypicker-weekday");
+ let numChilds = mainbox.children.length;
+ for (let i = 0; i < numChilds; i++) {
+ let child = mainbox.children[i];
+ let dow = i + this.weekStartOffset;
+ if (dow >= 7) {
+ dow -= 7;
+ }
+ let day = cal.l10n.getString("dateFormat", `day.${dow + 1}.Mmm`);
+ child.label = day;
+ child.calendar = mainbox;
+ }
+ },
+ /**
+ * Getter for days property.
+ */
+ get days() {
+ let mainbox = document.getElementById("daypicker-weekday");
+ let numChilds = mainbox.children.length;
+ let days = [];
+ for (let i = 0; i < numChilds; i++) {
+ let child = mainbox.children[i];
+ if (child.getAttribute("checked") == "true") {
+ let index = i + this.weekStartOffset;
+ if (index >= 7) {
+ index -= 7;
+ }
+ days.push(index + 1);
+ }
+ }
+ return days;
+ },
+ /**
+ * The weekday-picker manages an array of selected days of the week and
+ * the 'days' property is the interface to this array. the expected argument is
+ * an array containing integer elements, where each element represents a selected
+ * day of the week, starting with SUNDAY=1.
+ */
+ set days(val) {
+ let mainbox = document.getElementById("daypicker-weekday");
+ for (let child of mainbox.children) {
+ child.removeAttribute("checked");
+ }
+ for (let i in val) {
+ let index = val[i] - 1 - this.weekStartOffset;
+ if (index < 0) {
+ index += 7;
+ }
+ mainbox.children[index].setAttribute("checked", "true");
+ }
+ },
+};
+
+/**
+ * An object containing the daypicker-monthday binding functionalities.
+ */
+const DaypickerMonthday = {
+ /**
+ * Method intitializing DaypickerMonthday.
+ */
+ init() {
+ let mainbox = document.querySelector(".daypicker-monthday-mainbox");
+ let child = null;
+ for (let row of mainbox.children) {
+ for (child of row.children) {
+ child.calendar = mainbox;
+ }
+ }
+ let labelLastDay = cal.l10n.getString(
+ "calendar-event-dialog",
+ "eventRecurrenceMonthlyLastDayLabel"
+ );
+ child.setAttribute("label", labelLastDay);
+ },
+ /**
+ * Setter for days property.
+ */
+ set days(val) {
+ let mainbox = document.querySelector(".daypicker-monthday-mainbox");
+ let days = [];
+ for (let row of mainbox.children) {
+ for (let child of row.children) {
+ child.removeAttribute("checked");
+ days.push(child);
+ }
+ }
+ for (let i in val) {
+ let lastDayOffset = val[i] == -1 ? 0 : -1;
+ let index = val[i] < 0 ? val[i] + days.length + lastDayOffset : val[i] - 1;
+ days[index].setAttribute("checked", "true");
+ }
+ },
+ /**
+ * Getter for days property.
+ */
+ get days() {
+ let mainbox = document.querySelector(".daypicker-monthday-mainbox");
+ let days = [];
+ for (let row of mainbox.children) {
+ for (let child of row.children) {
+ if (child.getAttribute("checked") == "true") {
+ days.push(Number(child.label) ? Number(child.label) : -1);
+ }
+ }
+ }
+ return days;
+ },
+ /**
+ * Disables daypicker elements.
+ */
+ disable() {
+ let mainbox = document.querySelector(".daypicker-monthday-mainbox");
+ for (let row of mainbox.children) {
+ for (let child of row.children) {
+ child.setAttribute("disabled", "true");
+ }
+ }
+ },
+ /**
+ * Enables daypicker elements.
+ */
+ enable() {
+ let mainbox = document.querySelector(".daypicker-monthday-mainbox");
+ for (let row of mainbox.children) {
+ for (let child of row.children) {
+ child.removeAttribute("disabled");
+ }
+ }
+ },
+};
+
+/**
+ * Sets up the recurrence dialog from the window arguments. Takes care of filling
+ * the dialog controls with the recurrence information for this window.
+ */
+function onLoad() {
+ RecurrencePreview.init();
+ DaypickerWeekday.init();
+ DaypickerMonthday.init();
+ changeWidgetsOrder();
+
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ let calendar = item.calendar;
+ let recinfo = args.recurrenceInfo;
+
+ gStartTime = args.startTime;
+ gEndTime = args.endTime;
+ RecurrencePreview.dateTime = gStartTime.getInTimezone(cal.dtz.defaultTimezone);
+
+ onChangeCalendar(calendar);
+
+ // Set starting value for 'repeat until' rule and highlight the start date.
+ let repeatDate = cal.dtz.dateTimeToJsDate(gStartTime.getInTimezone(cal.dtz.floating));
+ document.getElementById("repeat-until-date").value = repeatDate;
+ document.getElementById("repeat-until-date").extraDate = repeatDate;
+
+ if (item.parentItem != item) {
+ item = item.parentItem;
+ }
+ let rule = null;
+ if (recinfo) {
+ // Split out rules and exceptions
+ try {
+ let rrules = splitRecurrenceRules(recinfo);
+ let rules = rrules[0];
+ // Deal with the rules
+ if (rules.length > 0) {
+ // We only handle 1 rule currently
+ rule = cal.wrapInstance(rules[0], Ci.calIRecurrenceRule);
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ if (!rule) {
+ rule = cal.createRecurrenceRule();
+ rule.type = "DAILY";
+ rule.interval = 1;
+ rule.count = -1;
+
+ // We don't let the user set the week start day for a given rule, but we
+ // want to default to the user's week start so rules behave as expected
+ let weekStart = Services.prefs.getIntPref("calendar.week.start", 0);
+ rule.weekStart = weekStart;
+ }
+ initializeControls(rule);
+
+ // Update controls
+ updateRecurrenceBox();
+
+ opener.setCursor("auto");
+ self.focus();
+}
+
+/**
+ * Initialize the dialog controls according to the passed rule
+ *
+ * @param rule The recurrence rule to parse.
+ */
+function initializeControls(rule) {
+ function getOrdinalAndWeekdayOfRule(aByDayRuleComponent) {
+ return {
+ ordinal: (aByDayRuleComponent - (aByDayRuleComponent % 8)) / 8,
+ weekday: Math.abs(aByDayRuleComponent % 8),
+ };
+ }
+
+ function setControlsForByMonthDay_YearlyRule(aDate, aByMonthDay) {
+ if (aByMonthDay == -1) {
+ // The last day of the month.
+ document.getElementById("yearly-group").selectedIndex = 1;
+ document.getElementById("yearly-ordinal").value = -1;
+ document.getElementById("yearly-weekday").value = -1;
+ } else {
+ if (aByMonthDay < -1) {
+ // The UI doesn't manage negative days apart from -1 but we can
+ // display in the controls the day from the start of the month.
+ aByMonthDay += aDate.endOfMonth.day + 1;
+ }
+ document.getElementById("yearly-group").selectedIndex = 0;
+ document.getElementById("yearly-days").value = aByMonthDay;
+ }
+ }
+
+ function everyWeekDay(aByDay) {
+ // Checks if aByDay contains only values from 1 to 7 with any order.
+ let mask = aByDay.reduce((value, item) => value | (1 << item), 1);
+ return aByDay.length == 7 && mask == Math.pow(2, 8) - 1;
+ }
+
+ document.getElementById("week-start").value = rule.weekStart;
+
+ switch (rule.type) {
+ case "DAILY":
+ document.getElementById("period-list").selectedIndex = 0;
+ document.getElementById("daily-days").value = rule.interval;
+ break;
+ case "WEEKLY":
+ document.getElementById("weekly-weeks").value = rule.interval;
+ document.getElementById("period-list").selectedIndex = 1;
+ break;
+ case "MONTHLY":
+ document.getElementById("monthly-interval").value = rule.interval;
+ document.getElementById("period-list").selectedIndex = 2;
+ break;
+ case "YEARLY":
+ document.getElementById("yearly-interval").value = rule.interval;
+ document.getElementById("period-list").selectedIndex = 3;
+ break;
+ default:
+ document.getElementById("period-list").selectedIndex = 0;
+ dump("unable to handle your rule type!\n");
+ break;
+ }
+
+ let byDayRuleComponent = rule.getComponent("BYDAY");
+ let byMonthDayRuleComponent = rule.getComponent("BYMONTHDAY");
+ let byMonthRuleComponent = rule.getComponent("BYMONTH");
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+ let startDate = gStartTime.getInTimezone(kDefaultTimezone);
+
+ // "DAILY" ruletype
+ // byDayRuleComponents may have been set priorily by "MONTHLY"- ruletypes
+ // where they have a different context-
+ // that's why we also query the current rule-type
+ if (byDayRuleComponent.length == 0 || rule.type != "DAILY") {
+ document.getElementById("daily-group").selectedIndex = 0;
+ } else {
+ document.getElementById("daily-group").selectedIndex = 1;
+ }
+
+ // "WEEKLY" ruletype
+ if (byDayRuleComponent.length == 0 || rule.type != "WEEKLY") {
+ DaypickerWeekday.days = [startDate.weekday + 1];
+ } else {
+ DaypickerWeekday.days = byDayRuleComponent;
+ }
+
+ // "MONTHLY" ruletype
+ let ruleComponentsEmpty = byDayRuleComponent.length == 0 && byMonthDayRuleComponent.length == 0;
+ if (ruleComponentsEmpty || rule.type != "MONTHLY") {
+ document.getElementById("monthly-group").selectedIndex = 1;
+ DaypickerMonthday.days = [startDate.day];
+ let day = Math.floor((startDate.day - 1) / 7) + 1;
+ document.getElementById("monthly-ordinal").value = day;
+ document.getElementById("monthly-weekday").value = startDate.weekday + 1;
+ } else if (everyWeekDay(byDayRuleComponent)) {
+ // Every day of the month.
+ document.getElementById("monthly-group").selectedIndex = 0;
+ document.getElementById("monthly-ordinal").value = 0;
+ document.getElementById("monthly-weekday").value = -1;
+ } else if (byDayRuleComponent.length > 0) {
+ // One of the first five days or weekdays of the month.
+ document.getElementById("monthly-group").selectedIndex = 0;
+ let ruleInfo = getOrdinalAndWeekdayOfRule(byDayRuleComponent[0]);
+ document.getElementById("monthly-ordinal").value = ruleInfo.ordinal;
+ document.getElementById("monthly-weekday").value = ruleInfo.weekday;
+ } else if (byMonthDayRuleComponent.length == 1 && byMonthDayRuleComponent[0] == -1) {
+ // The last day of the month.
+ document.getElementById("monthly-group").selectedIndex = 0;
+ document.getElementById("monthly-ordinal").value = byMonthDayRuleComponent[0];
+ document.getElementById("monthly-weekday").value = byMonthDayRuleComponent[0];
+ } else if (byMonthDayRuleComponent.length > 0) {
+ document.getElementById("monthly-group").selectedIndex = 1;
+ DaypickerMonthday.days = byMonthDayRuleComponent;
+ }
+
+ // "YEARLY" ruletype
+ if (byMonthRuleComponent.length == 0 || rule.type != "YEARLY") {
+ document.getElementById("yearly-month-rule").value = startDate.month + 1;
+ document.getElementById("yearly-month-ordinal").value = startDate.month + 1;
+ if (byMonthDayRuleComponent.length > 0) {
+ setControlsForByMonthDay_YearlyRule(startDate, byMonthDayRuleComponent[0]);
+ } else {
+ document.getElementById("yearly-days").value = startDate.day;
+ let ordinalDay = Math.floor((startDate.day - 1) / 7) + 1;
+ document.getElementById("yearly-ordinal").value = ordinalDay;
+ document.getElementById("yearly-weekday").value = startDate.weekday + 1;
+ }
+ } else {
+ document.getElementById("yearly-month-rule").value = byMonthRuleComponent[0];
+ document.getElementById("yearly-month-ordinal").value = byMonthRuleComponent[0];
+ if (byMonthDayRuleComponent.length > 0) {
+ let date = startDate.clone();
+ date.month = byMonthRuleComponent[0] - 1;
+ setControlsForByMonthDay_YearlyRule(date, byMonthDayRuleComponent[0]);
+ } else if (byDayRuleComponent.length > 0) {
+ document.getElementById("yearly-group").selectedIndex = 1;
+ if (everyWeekDay(byDayRuleComponent)) {
+ // Every day of the month.
+ document.getElementById("yearly-ordinal").value = 0;
+ document.getElementById("yearly-weekday").value = -1;
+ } else {
+ let yearlyRuleInfo = getOrdinalAndWeekdayOfRule(byDayRuleComponent[0]);
+ document.getElementById("yearly-ordinal").value = yearlyRuleInfo.ordinal;
+ document.getElementById("yearly-weekday").value = yearlyRuleInfo.weekday;
+ }
+ } else if (byMonthRuleComponent.length > 0) {
+ document.getElementById("yearly-group").selectedIndex = 0;
+ document.getElementById("yearly-days").value = startDate.day;
+ }
+ }
+
+ /* load up the duration of the event radiogroup */
+ if (rule.isByCount) {
+ if (rule.count == -1) {
+ document.getElementById("recurrence-duration").value = "forever";
+ } else {
+ document.getElementById("recurrence-duration").value = "ntimes";
+ document.getElementById("repeat-ntimes-count").value = rule.count;
+ }
+ } else {
+ let untilDate = rule.untilDate;
+ if (untilDate) {
+ gUntilDate = untilDate.getInTimezone(gStartTime.timezone); // calIRecurrenceRule::untilDate is always UTC or floating
+ // Change the until date to start date if the rule has a forbidden
+ // value (earlier than the start date).
+ if (gUntilDate.compare(gStartTime) < 0) {
+ gUntilDate = gStartTime.clone();
+ }
+ let repeatDate = cal.dtz.dateTimeToJsDate(gUntilDate.getInTimezone(cal.dtz.floating));
+ document.getElementById("recurrence-duration").value = "until";
+ document.getElementById("repeat-until-date").value = repeatDate;
+ } else {
+ document.getElementById("recurrence-duration").value = "forever";
+ }
+ }
+}
+
+/**
+ * Save the recurrence information selected in the dialog back to the given
+ * item.
+ *
+ * @param item The item to save back to.
+ * @returns The saved recurrence info.
+ */
+function onSave(item) {
+ // Always return 'null' if this item is an occurrence.
+ if (!item || item.parentItem != item) {
+ return null;
+ }
+
+ // This works, but if we ever support more complex recurrence,
+ // e.g. recurrence for Martians, then we're going to want to
+ // not clone and just recreate the recurrenceInfo each time.
+ // The reason is that the order of items (rules/dates/datesets)
+ // matters, so we can't always just append at the end. This
+ // code here always inserts a rule first, because all our
+ // exceptions should come afterward.
+ let periodNumber = Number(document.getElementById("period-list").value);
+
+ let args = window.arguments[0];
+ let recurrenceInfo = args.recurrenceInfo;
+ if (recurrenceInfo) {
+ recurrenceInfo = recurrenceInfo.clone();
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ if (rrules[0].length > 0) {
+ recurrenceInfo.deleteRecurrenceItem(rrules[0][0]);
+ }
+ recurrenceInfo.item = item;
+ } else {
+ recurrenceInfo = new CalRecurrenceInfo(item);
+ }
+
+ let recRule = cal.createRecurrenceRule();
+
+ // We don't let the user edit the start of the week for a given rule, but we
+ // want to preserve the value set
+ let weekStart = Number(document.getElementById("week-start").value);
+ recRule.weekStart = weekStart;
+
+ const ALL_WEEKDAYS = [2, 3, 4, 5, 6, 7, 1]; // The sequence MO,TU,WE,TH,FR,SA,SU.
+ switch (periodNumber) {
+ case 0: {
+ recRule.type = "DAILY";
+ let dailyGroup = document.getElementById("daily-group");
+ if (dailyGroup.selectedIndex == 0) {
+ let ndays = Math.max(1, Number(document.getElementById("daily-days").value));
+ recRule.interval = ndays;
+ } else {
+ recRule.interval = 1;
+ let onDays = [2, 3, 4, 5, 6];
+ recRule.setComponent("BYDAY", onDays);
+ }
+ break;
+ }
+ case 1: {
+ recRule.type = "WEEKLY";
+ let ndays = Number(document.getElementById("weekly-weeks").value);
+ recRule.interval = ndays;
+ let onDays = DaypickerWeekday.days;
+ if (onDays.length > 0) {
+ recRule.setComponent("BYDAY", onDays);
+ }
+ break;
+ }
+ case 2: {
+ recRule.type = "MONTHLY";
+ let monthInterval = Number(document.getElementById("monthly-interval").value);
+ recRule.interval = monthInterval;
+ let monthlyGroup = document.getElementById("monthly-group");
+ if (monthlyGroup.selectedIndex == 0) {
+ let monthlyOrdinal = Number(document.getElementById("monthly-ordinal").value);
+ let monthlyDOW = Number(document.getElementById("monthly-weekday").value);
+ if (monthlyDOW < 0) {
+ if (monthlyOrdinal == 0) {
+ // Monthly rule "Every day of the month".
+ recRule.setComponent("BYDAY", ALL_WEEKDAYS);
+ } else {
+ // One of the first five days or the last day of the month.
+ recRule.setComponent("BYMONTHDAY", [monthlyOrdinal]);
+ }
+ } else {
+ let sign = monthlyOrdinal < 0 ? -1 : 1;
+ let onDays = [(Math.abs(monthlyOrdinal) * 8 + monthlyDOW) * sign];
+ recRule.setComponent("BYDAY", onDays);
+ }
+ } else {
+ let monthlyDays = DaypickerMonthday.days;
+ if (monthlyDays.length > 0) {
+ recRule.setComponent("BYMONTHDAY", monthlyDays);
+ }
+ }
+ break;
+ }
+ case 3: {
+ recRule.type = "YEARLY";
+ let yearInterval = Number(document.getElementById("yearly-interval").value);
+ recRule.interval = yearInterval;
+ let yearlyGroup = document.getElementById("yearly-group");
+ if (yearlyGroup.selectedIndex == 0) {
+ let yearlyByMonth = [Number(document.getElementById("yearly-month-ordinal").value)];
+ recRule.setComponent("BYMONTH", yearlyByMonth);
+ let yearlyByDay = [Number(document.getElementById("yearly-days").value)];
+ recRule.setComponent("BYMONTHDAY", yearlyByDay);
+ } else {
+ let yearlyByMonth = [Number(document.getElementById("yearly-month-rule").value)];
+ recRule.setComponent("BYMONTH", yearlyByMonth);
+ let yearlyOrdinal = Number(document.getElementById("yearly-ordinal").value);
+ let yearlyDOW = Number(document.getElementById("yearly-weekday").value);
+ if (yearlyDOW < 0) {
+ if (yearlyOrdinal == 0) {
+ // Yearly rule "Every day of a month".
+ recRule.setComponent("BYDAY", ALL_WEEKDAYS);
+ } else {
+ // One of the first five days or the last of a month.
+ recRule.setComponent("BYMONTHDAY", [yearlyOrdinal]);
+ }
+ } else {
+ let sign = yearlyOrdinal < 0 ? -1 : 1;
+ let onDays = [(Math.abs(yearlyOrdinal) * 8 + yearlyDOW) * sign];
+ recRule.setComponent("BYDAY", onDays);
+ }
+ }
+ break;
+ }
+ }
+
+ // Figure out how long this event is supposed to last
+ switch (document.getElementById("recurrence-duration").selectedItem.value) {
+ case "forever": {
+ recRule.count = -1;
+ break;
+ }
+ case "ntimes": {
+ recRule.count = Math.max(1, document.getElementById("repeat-ntimes-count").value);
+ break;
+ }
+ case "until": {
+ let untilDate = cal.dtz.jsDateToDateTime(
+ document.getElementById("repeat-until-date").value,
+ gStartTime.timezone
+ );
+ untilDate.isDate = gStartTime.isDate; // enforce same value type as DTSTART
+ if (!gStartTime.isDate) {
+ // correct UNTIL to exactly match start date's hour, minute, second:
+ untilDate.hour = gStartTime.hour;
+ untilDate.minute = gStartTime.minute;
+ untilDate.second = gStartTime.second;
+ }
+ recRule.untilDate = untilDate;
+ break;
+ }
+ }
+
+ if (recRule.interval < 1) {
+ return null;
+ }
+
+ recurrenceInfo.insertRecurrenceItemAt(recRule, 0);
+ return recurrenceInfo;
+}
+
+/**
+ * Handler function to be called when the accept button is pressed.
+ */
+document.addEventListener("dialogaccept", event => {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ args.onOk(onSave(item));
+ // Don't close the dialog if a warning must be showed.
+ if (checkUntilDate.warning) {
+ event.preventDefault();
+ }
+});
+
+/**
+ * Handler function to be called when the Cancel button is pressed.
+ */
+document.addEventListener("dialogcancel", () => {
+ // Don't show any warning if the dialog must be closed.
+ checkUntilDate.warning = false;
+});
+
+/**
+ * Handler function called when the calendar is changed (also for initial
+ * setup).
+ *
+ * XXX we don't change the calendar in this dialog, this function should be
+ * consolidated or renamed.
+ *
+ * @param calendar The calendar to use for setup.
+ */
+function onChangeCalendar(calendar) {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+
+ // Set 'gIsReadOnly' if the calendar is read-only
+ gIsReadOnly = false;
+ if (calendar && calendar.readOnly) {
+ gIsReadOnly = true;
+ }
+
+ // Disable or enable controls based on a set or rules
+ // - whether this item is a stand-alone item or an occurrence
+ // - whether or not this item is read-only
+ // - whether or not the state of the item allows recurrence rules
+ // - tasks without an entrydate are invalid
+ disableOrEnable(item);
+
+ updateRecurrenceControls();
+}
+
+/**
+ * Disable or enable certain controls based on the given item:
+ * Uses the following attribute:
+ *
+ * - disable-on-occurrence
+ * - disable-on-readonly
+ *
+ * A task without a start time is also considered readonly.
+ *
+ * @param item The item to check.
+ */
+function disableOrEnable(item) {
+ if (item.parentItem != item) {
+ disableRecurrenceFields("disable-on-occurrence");
+ } else if (gIsReadOnly) {
+ disableRecurrenceFields("disable-on-readonly");
+ } else if (item.isTodo() && !gStartTime) {
+ disableRecurrenceFields("disable-on-readonly");
+ } else {
+ enableRecurrenceFields("disable-on-readonly");
+ }
+}
+
+/**
+ * Disables all fields that have an attribute that matches the argument and is
+ * set to "true".
+ *
+ * @param aAttributeName The attribute to search for.
+ */
+function disableRecurrenceFields(aAttributeName) {
+ let disableElements = document.getElementsByAttribute(aAttributeName, "true");
+ for (let i = 0; i < disableElements.length; i++) {
+ disableElements[i].setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Enables all fields that have an attribute that matches the argument and is
+ * set to "true".
+ *
+ * @param aAttributeName The attribute to search for.
+ */
+function enableRecurrenceFields(aAttributeName) {
+ let enableElements = document.getElementsByAttribute(aAttributeName, "true");
+ for (let i = 0; i < enableElements.length; i++) {
+ enableElements[i].removeAttribute("disabled");
+ }
+}
+
+/**
+ * Handler function to update the period-box when an item from the period-list
+ * is selected. Also updates the controls on that period-box.
+ */
+function updateRecurrenceBox() {
+ let periodBox = document.getElementById("period-box");
+ let periodNumber = Number(document.getElementById("period-list").value);
+ for (let i = 0; i < periodBox.children.length; i++) {
+ periodBox.children[i].hidden = i != periodNumber;
+ }
+ updateRecurrenceControls();
+}
+
+/**
+ * Updates the controls regarding ranged controls (i.e repeat forever, repeat
+ * until, repeat n times...)
+ */
+function updateRecurrenceRange() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ if (item.parentItem != item || gIsReadOnly) {
+ return;
+ }
+
+ let radioRangeForever = document.getElementById("recurrence-range-forever");
+ let radioRangeFor = document.getElementById("recurrence-range-for");
+ let radioRangeUntil = document.getElementById("recurrence-range-until");
+ let rangeTimesCount = document.getElementById("repeat-ntimes-count");
+ let rangeUntilDate = document.getElementById("repeat-until-date");
+ let rangeAppointmentsLabel = document.getElementById("repeat-appointments-label");
+
+ radioRangeForever.removeAttribute("disabled");
+ radioRangeFor.removeAttribute("disabled");
+ radioRangeUntil.removeAttribute("disabled");
+ rangeAppointmentsLabel.removeAttribute("disabled");
+
+ let durationSelection = document.getElementById("recurrence-duration").selectedItem.value;
+
+ if (durationSelection == "ntimes") {
+ rangeTimesCount.removeAttribute("disabled");
+ } else {
+ rangeTimesCount.setAttribute("disabled", "true");
+ }
+
+ if (durationSelection == "until") {
+ rangeUntilDate.removeAttribute("disabled");
+ } else {
+ rangeUntilDate.setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Updates the recurrence preview calendars using the window's item.
+ */
+function updatePreview() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ if (item.parentItem != item) {
+ item = item.parentItem;
+ }
+
+ // TODO: We should better start the whole dialog with a newly cloned item
+ // and always pump changes immediately into it. This would eliminate the
+ // need to break the encapsulation, as we do it here. But we need the item
+ // to contain the startdate in order to calculate the recurrence preview.
+ item = item.clone();
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+ if (item.isEvent()) {
+ let startDate = gStartTime.getInTimezone(kDefaultTimezone);
+ let endDate = gEndTime.getInTimezone(kDefaultTimezone);
+ if (startDate.isDate) {
+ endDate.day--;
+ }
+
+ item.startDate = startDate;
+ item.endDate = endDate;
+ }
+ if (item.isTodo()) {
+ let entryDate = gStartTime;
+ if (entryDate) {
+ entryDate = entryDate.getInTimezone(kDefaultTimezone);
+ } else {
+ item.recurrenceInfo = null;
+ }
+ item.entryDate = entryDate;
+ let dueDate = gEndTime;
+ if (dueDate) {
+ dueDate = dueDate.getInTimezone(kDefaultTimezone);
+ }
+ item.dueDate = dueDate;
+ }
+
+ let recInfo = onSave(item);
+ RecurrencePreview.updatePreview(recInfo);
+}
+
+/**
+ * Checks the until date just entered in the datepicker in order to avoid
+ * setting a date earlier than the start date.
+ * Restores the previous correct date, shows a warning and prevents to close the
+ * dialog when the user enters a wrong until date.
+ */
+function checkUntilDate() {
+ if (!gStartTime) {
+ // This function shouldn't run before onLoad.
+ return;
+ }
+
+ let untilDate = cal.dtz.jsDateToDateTime(
+ document.getElementById("repeat-until-date").value,
+ gStartTime.timezone
+ );
+ let startDate = gStartTime.clone();
+ startDate.isDate = true;
+ if (untilDate.compare(startDate) < 0) {
+ let repeatDate = cal.dtz.dateTimeToJsDate(
+ (gUntilDate || gStartTime).getInTimezone(cal.dtz.floating)
+ );
+ document.getElementById("repeat-until-date").value = repeatDate;
+ checkUntilDate.warning = true;
+ let callback = function () {
+ // No warning when the dialog is being closed with the Cancel button.
+ if (!checkUntilDate.warning) {
+ return;
+ }
+ Services.prompt.alert(
+ null,
+ document.title,
+ cal.l10n.getCalString("warningUntilDateBeforeStart")
+ );
+ checkUntilDate.warning = false;
+ };
+ setTimeout(callback, 1);
+ } else {
+ gUntilDate = untilDate;
+ updateRecurrenceControls();
+ }
+}
+
+/**
+ * Checks the date entered for a yearly absolute rule (i.e. every 12 of January)
+ * in order to avoid creating a rule on an invalid date.
+ */
+function checkYearlyAbsoluteDate() {
+ if (!gStartTime) {
+ // This function shouldn't run before onLoad.
+ return;
+ }
+
+ const MONTH_LENGTHS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+ let dayOfMonth = document.getElementById("yearly-days").value;
+ let month = document.getElementById("yearly-month-ordinal").value;
+ document.getElementById("yearly-days").max = MONTH_LENGTHS[month - 1];
+ // Check if the day value is too high.
+ if (dayOfMonth > MONTH_LENGTHS[month - 1]) {
+ document.getElementById("yearly-days").value = MONTH_LENGTHS[month - 1];
+ } else {
+ updateRecurrenceControls();
+ }
+ // Check if the day value is too low.
+ if (dayOfMonth < 1) {
+ document.getElementById("yearly-days").value = 1;
+ } else {
+ updateRecurrenceControls();
+ }
+}
+
+/**
+ * Update all recurrence controls on the dialog.
+ */
+function updateRecurrenceControls() {
+ updateRecurrencePattern();
+ updateRecurrenceRange();
+ updatePreview();
+ window.sizeToContent();
+}
+
+/**
+ * Disables/enables controls related to the recurrence pattern.
+ * the status of the controls depends on which period entry is selected
+ * and which form of pattern rule is selected.
+ */
+function updateRecurrencePattern() {
+ let args = window.arguments[0];
+ let item = args.calendarEvent;
+ if (item.parentItem != item || gIsReadOnly) {
+ return;
+ }
+
+ switch (Number(document.getElementById("period-list").value)) {
+ // daily
+ case 0: {
+ let dailyGroup = document.getElementById("daily-group");
+ let dailyDays = document.getElementById("daily-days");
+ dailyDays.removeAttribute("disabled");
+ if (dailyGroup.selectedIndex == 1) {
+ dailyDays.setAttribute("disabled", "true");
+ }
+ break;
+ }
+ // weekly
+ case 1: {
+ break;
+ }
+ // monthly
+ case 2: {
+ let monthlyGroup = document.getElementById("monthly-group");
+ let monthlyOrdinal = document.getElementById("monthly-ordinal");
+ let monthlyWeekday = document.getElementById("monthly-weekday");
+ let monthlyDays = DaypickerMonthday;
+ monthlyOrdinal.removeAttribute("disabled");
+ monthlyWeekday.removeAttribute("disabled");
+ monthlyDays.enable();
+ if (monthlyGroup.selectedIndex == 0) {
+ monthlyDays.disable();
+ } else {
+ monthlyOrdinal.setAttribute("disabled", "true");
+ monthlyWeekday.setAttribute("disabled", "true");
+ }
+ break;
+ }
+ // yearly
+ case 3: {
+ let yearlyGroup = document.getElementById("yearly-group");
+ let yearlyDays = document.getElementById("yearly-days");
+ let yearlyMonthOrdinal = document.getElementById("yearly-month-ordinal");
+ let yearlyPeriodOfMonthLabel = document.getElementById("yearly-period-of-month-label");
+ let yearlyOrdinal = document.getElementById("yearly-ordinal");
+ let yearlyWeekday = document.getElementById("yearly-weekday");
+ let yearlyMonthRule = document.getElementById("yearly-month-rule");
+ let yearlyPeriodOfLabel = document.getElementById("yearly-period-of-label");
+ yearlyDays.removeAttribute("disabled");
+ yearlyMonthOrdinal.removeAttribute("disabled");
+ yearlyOrdinal.removeAttribute("disabled");
+ yearlyWeekday.removeAttribute("disabled");
+ yearlyMonthRule.removeAttribute("disabled");
+ yearlyPeriodOfLabel.removeAttribute("disabled");
+ yearlyPeriodOfMonthLabel.removeAttribute("disabled");
+ if (yearlyGroup.selectedIndex == 0) {
+ yearlyOrdinal.setAttribute("disabled", "true");
+ yearlyWeekday.setAttribute("disabled", "true");
+ yearlyMonthRule.setAttribute("disabled", "true");
+ yearlyPeriodOfLabel.setAttribute("disabled", "true");
+ } else {
+ yearlyDays.setAttribute("disabled", "true");
+ yearlyMonthOrdinal.setAttribute("disabled", "true");
+ yearlyPeriodOfMonthLabel.setAttribute("disabled", "true");
+ }
+ break;
+ }
+ }
+}
+
+/**
+ * This function changes the order for certain elements using a locale string.
+ * This is needed for some locales that expect a different wording order.
+ *
+ * @param aPropKey The locale property key to get the order from
+ * @param aPropParams An array of ids to be passed to the locale property.
+ * These should be the ids of the elements to change
+ * the order for.
+ */
+function changeOrderForElements(aPropKey, aPropParams) {
+ let localeOrder;
+ let parents = {};
+
+ for (let key in aPropParams) {
+ // Save original parents so that the nodes to reorder get appended to
+ // the correct parent nodes.
+ parents[key] = document.getElementById(aPropParams[key]).parentNode;
+ }
+
+ try {
+ localeOrder = cal.l10n.getString("calendar-event-dialog", aPropKey, aPropParams).split(" ");
+ } catch (ex) {
+ let msg =
+ "The key " +
+ aPropKey +
+ " in calendar-event-dialog.prop" +
+ "erties has incorrect number of params. Expected " +
+ aPropParams.length +
+ " params.";
+ console.error(msg + " " + ex);
+ return;
+ }
+
+ // Add elements in the right order, removing them from their old parent
+ for (let i = 0; i < aPropParams.length; i++) {
+ let newEl = document.getElementById(localeOrder[i]);
+ if (newEl) {
+ parents[i].appendChild(newEl);
+ } else {
+ cal.ERROR(
+ "Localization error, could not find node '" +
+ localeOrder[i] +
+ "'. Please have your localizer check the string '" +
+ aPropKey +
+ "'"
+ );
+ }
+ }
+}
+
+/**
+ * Change locale-specific widget order for Edit Recurrence window
+ */
+function changeWidgetsOrder() {
+ changeOrderForElements("monthlyOrder", ["monthly-ordinal", "monthly-weekday"]);
+ changeOrderForElements("yearlyOrder", [
+ "yearly-days",
+ "yearly-period-of-month-label",
+ "yearly-month-ordinal",
+ ]);
+ changeOrderForElements("yearlyOrder2", [
+ "yearly-ordinal",
+ "yearly-weekday",
+ "yearly-period-of-label",
+ "yearly-month-rule",
+ ]);
+}