diff options
Diffstat (limited to '')
-rw-r--r-- | comm/calendar/base/content/dialogs/calendar-event-dialog-recurrence.js | 1237 |
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", + ]); +} |