releases-comm-central/calendar/base/content/calendar-dialog-utils.js

706 строки
31 KiB
JavaScript

/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Oracle Corporation code.
*
* The Initial Developer of the Original Code is Oracle Corporation
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Stuart Parmenter <stuart.parmenter@oracle.com>
* Michael Buettner <michael.buettner@sun.com>
* Stefan Sitter <ssitter@gmail.com>
* Philipp Kewisch <mozilla@kewis.ch>
* Martin Schroeder <mschroeder@mozilla.x-home.org>
* Hubert Gajewski <hubert@hubertgajewski.com>, Aviary.pl
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
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);
}
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", {});
var weekdays = "";
for (let i = 0; i < days.length; i++) {
weekdays += getRString("repeatDetailsDay" + days[i])
if (days.length > 1 && i == (days.length - 2)) {
weekdays += ' ' + getRString("repeatDetailsAnd") + ' ';
} else if (i < days.length - 1) {
weekdays += ', ';
}
}
let weeklyString = getRString("weeklyNthOn", [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 byday = rule.getComponent("BYDAY", {});
if (day_position(byday[0]) == 0) {
// i.e every MONDAY of every N months
let day = getRString("repeatDetailsDay" + day_of_week(byday[0]));
ruleString = getRString("monthlyEveryOfEvery", [day]);
ruleString = PluralForm.get(rule.interval, ruleString)
.replace("#2", rule.interval);
} else {
// i.e the FIRST MONDAY of every N months
let ordinal = getRString("repeatDetailsOrdinal" + day_position(byday[0]));
let day = getRString("repeatDetailsDay" + day_of_week(byday[0]));
ruleString = getRString("monthlyNthOfEvery", [ordinal, day]);
ruleString = PluralForm.get(rule.interval, ruleString)
.replace("#3", 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 {
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') {
if (checkRecurrenceRule(rule, ['BYMONTH']) &&
checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
bymonth = rule.getComponent("BYMONTH", {});
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'])) {
bymonth = rule.getComponent("BYMONTH", {});
byday = rule.getComponent("BYDAY", {});
if (bymonth.length == 1 && byday.length == 1) {
let dayString = getRString("repeatDetailsDay" + day_of_week(byday[0]));
let monthString = getRString("repeatDetailsMonth" + bymonth[0]);
if (day_position(byday[0]) == 0) {
let yearlyString = getRString("yearlyOnEveryNthOfNth",
[dayString, monthString]);
ruleString = PluralForm.get(rule.interval, yearlyString)
.replace("#3", rule.interval);
} else {
let ordinalString = getRString("repeatDetailsOrdinal" +
day_position(byday[0]));
let yearlyString = getRString("yearlyNthOnNthOf",
[ordinalString,
dayString,
monthString]);
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.endDate.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.endDate.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;
}
/**
* 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) {
args.job.dispose();
}
}
/**
* Create a calIAlarm from the given menuitem. The menuitem must have the
* following attributes: unit, length, origin, relation.
*
* @param menuitem The menuitem to create the alarm from.
* @return The calIAlarm with information from the menuitem.
*/
function createReminderFromMenuitem(aMenuitem) {
let reminder = cal.createAlarm();
let offset = cal.createDuration();
offset[aMenuitem.getAttribute("unit")] = aMenuitem.getAttribute("length");
offset.normalize();
offset.isNegative = (aMenuitem.getAttribute("origin") == "before");
reminder.related = (aMenuitem.getAttribute("relation") == "START" ?
reminder.ALARM_RELATED_START : reminder.ALARM_RELATED_END);
reminder.offset = offset;
return reminder;
}
/**
* This function opens the needed dialogs to edit the reminder. Note however
* that calling this function from an extension is not recommended. To allow an
* extension to open the reminder dialog, set the menulist "item-alarm" to the
* custom menuitem and call updateReminder().
*/
function editReminder() {
let customItem = document.getElementById("reminder-custom-menuitem");
let args = {};
args.reminders = customItem.reminders;
args.item = window.calendarItem;
args.timezone = gStartTimezone || gEndTimezone;
let calendarNode = document.getElementById("item-calendar");
args.calendar = (calendarNode && calendarNode.selectedItem ?
calendarNode.selectedItem.calendar :
window.calendarItem.calendar);
let savedWindow = window;
// While these are "just" callbacks, the dialog is opened modally, so aside
// from whats needed to set up the reminders, nothing else needs to be done.
args.onOk = function(reminders) {
customItem.reminders = reminders;
};
args.onCancel = function() {
document.getElementById("item-alarm").selectedIndex = gLastAlarmSelection;
};
window.setCursor("wait");
// open the dialog modally
openDialog(
"chrome://calendar/content/calendar-event-dialog-reminder.xul",
"_blank",
"chrome,titlebar,modal,resizable",
args);
}
/**
* Update the reminder details from the selected alarm. This shows a string
* describing the reminder set, or nothing in case a preselected reminder was
* chosen.
*/
function updateReminderDetails() {
// find relevant elements in the document
let reminderList = document.getElementById("item-alarm");
let reminderDetails = document.getElementById("reminder-details");
let reminderMultipleLabel = document.getElementById("reminder-multiple-alarms-label");
let reminderSingleLabel = document.getElementById("reminder-single-alarms-label");
let reminders = document.getElementById("reminder-custom-menuitem").reminders || [];
let calendar = document.getElementById("item-calendar")
.selectedItem.calendar;
let actionValues = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
let actionMap = {};
for each (var action in actionValues) {
actionMap[action] = true;
}
// Filter out any unsupported action types.
reminders = reminders.filter(function(x) x.action in actionMap);
if (reminderList.value != "custom" || !reminders.length) {
// Don't try to show the details text for anything but a custom
// recurrence rule.
hideElement(reminderDetails);
return;
} else {
showElement(reminderDetails);
}
// Depending on how many alarms we have, show either the "Multiple Alarms"
// label or the single reminder label.
setElementValue(reminderMultipleLabel,
reminders.length < 2 && "true",
"hidden");
setElementValue(reminderSingleLabel,
reminders.length > 1 && "true",
"hidden");
cal.alarms.addReminderImages(document.getElementById("reminder-icon-box"),
reminders);
// If there is only one reminder, display the reminder string
if (reminders.length == 1) {
setElementValue(reminderSingleLabel,
reminders[0].toString(window.calendarItem));
}
}
var gLastAlarmSelection = 0;
/**
* Load an item's reminders into the dialog
*
* @param reminders An array of calIAlarms to load.
*/
function loadReminders(reminders) {
// select 'no reminder' by default
let reminderList = document.getElementById("item-alarm");
let reminderPopup = reminderList.firstChild;
let customItem = document.getElementById("reminder-custom-menuitem");
reminderList.selectedIndex = 0;
gLastAlarmSelection = 0;
if (!reminders || !reminders.length) {
// No reminders selected, we are done
return;
}
if (reminders.length == 1 &&
reminders[0].related != Components.interfaces.calIAlarm.ALARM_RELATED_ABSOLUTE &&
reminders[0].offset &&
reminders[0].action == "DISPLAY") {
// Exactly one reminder thats not absolute, we may be able to match up
// popup items. The reminder should also be a DISPLAY alarm
let reminder = reminders[0];
let relation = (reminder.related == reminder.ALARM_RELATED_START ? "START" : "END");
let origin = (reminder.offset.isNegative ? "before" : "after");
let unitMap = {
days: 86400,
hours: 3600,
minutes: 60
};
for each (let menuitem in Array.slice(reminderPopup.childNodes)) {
if (menuitem.localName == "menuitem" &&
menuitem.hasAttribute("length") &&
menuitem.getAttribute("origin") == origin &&
menuitem.getAttribute("relation") == relation) {
let unitMult = unitMap[menuitem.getAttribute("unit")] || 1;
let length = menuitem.getAttribute("length") * unitMult;
if (Math.abs(reminder.offset.inSeconds) == length) {
reminderList.selectedItem = menuitem;
// We've selected an item, so we are done here.
return;
}
}
}
}
// If more than one alarm is selected, or we didn't find a matching item
// above, then select the "custom" item and attach the item's reminders to
// it.
if (reminderList.selectedIndex < 1) {
reminderList.value = 'custom';
customItem.reminders = reminders;
}
// remember the selected index
gLastAlarmSelection = reminderList.selectedIndex;
}
/**
* Save the selected reminder into the passed item.
*
* @param item The item save the reminder into.
*/
function saveReminder(item) {
// Clear alarms, we'll need to remove alarms later anyway.
item.clearAlarms();
let reminderList = document.getElementById("item-alarm");
if (reminderList.value != 'none') {
let menuitem = reminderList.selectedItem;
let reminders;
if (menuitem.reminders) {
// Custom reminder entries carry their own reminder object with
// them. Make sure to clone in case these are the original item's
// reminders.
// XXX do we need to clone here?
reminders = menuitem.reminders.map(function(x) x.clone());
} else {
// Pre-defined entries specify the necessary information
// as attributes attached to the menuitem elements.
reminders = [createReminderFromMenuitem(menuitem)];
}
let alarmCaps = item.calendar.getProperty("capabilities.alarms.actionValues") ||
["DISPLAY"];
let alarmActions = {};
for each (let action in alarmCaps) {
alarmActions[action] = true;
}
// Make sure only alarms are saved that work in the given calendar.
reminders.filter(function(x) x.action in alarmActions)
.forEach(item.addAlarm, item);
}
}
/**
* Common update functions for both event dialogs. Called when a reminder has
* been selected from the menulist.
*/
function commonUpdateReminder() {
// if a custom reminder has been selected, we show the appropriate
// dialog in order to allow the user to specify the details.
// the result will be placed in the 'reminder-custom-menuitem' tag.
let reminderList = document.getElementById("item-alarm");
if (reminderList.value == 'custom') {
// show the dialog. This call blocks until the dialog is closed. Don't
// pop up the dialog if this happens during initialization of the dialog
if (reminderList.hasAttribute("last-value")) {
editReminder();
}
if (reminderList.value == 'custom') {
// Only do this if the 'custom' item is still selected. If the edit
// reminder dialog was canceled then the previously selected
// menuitem is selected, which may not be the custom menuitem.
// If one or no reminders were selected, we have a chance of mapping
// them to the existing elements in the dropdown.
let customItem = reminderList.selectedItem;
if (customItem.reminders.length == 0) {
// No reminder was selected
reminderList.value = "none";
} else if (customItem.reminders.length == 1 &&
customItem.reminders[0].action == "DISPLAY") {
// TODO This can be taken care of in a different bug. What needs to
// be done is to go through the menuitems in item-alarm and check if
// customItem.reminders[0] matches with that.
}
}
}
// remember the current reminder drop down selection index.
gLastAlarmSelection = reminderList.selectedIndex;
reminderList.setAttribute("last-value", reminderList.value);
// possibly the selected reminder conflicts with the item.
// for example an end-relation combined with a task without duedate
// is an invalid state we need to take care of. we take the same
// approach as with recurring tasks. in case the reminder is related
// to the entry date we check the entry date automatically and disable
// the checkbox. the same goes for end related reminder and the due date.
if (isToDo(window.calendarItem)) {
// In general, (re-)enable the due/entry checkboxes. This will be
// changed in case the alarms are related to START/END below.
enableElementWithLock("todo-has-duedate", "reminder-lock");
enableElementWithLock("todo-has-entrydate", "reminder-lock");
let menuitem = reminderList.selectedItem;
if (menuitem.value != 'none') {
// In case a reminder is selected, retrieve the array of alarms from
// it, or create one from the currently selected menuitem.
let reminders = menuitem.reminders || [createReminderFromMenuitem(menuitem)];
// If a reminder is related to the entry date...
if (reminders.some(function(x) x.related == x.ALARM_RELATED_START)) {
// ...automatically check 'has entrydate'.
if (!getElementValue("todo-has-entrydate", "checked")) {
setElementValue("todo-has-entrydate", "true", "checked");
// Make sure gStartTime is properly initialized
updateEntryDate();
}
// Disable the checkbox to indicate that we need the entry-date.
disableElementWithLock("todo-has-entrydate", "reminder-lock");
}
// If a reminder is related to the due date...
if (reminders.some(function(x) x.related == x.ALARM_RELATED_END)) {
// ...automatically check 'has duedate'.
if (!getElementValue("todo-has-duedate", "checked")) {
setElementValue("todo-has-duedate", "true", "checked");
// Make sure gStartTime is properly initialized
updateDueDate();
}
// Disable the checkbox to indicate that we need the entry-date.
disableElementWithLock("todo-has-duedate", "reminder-lock");
}
}
}
updateReminderDetails();
}
/**
* Updates the related link on the dialog
*/
function updateLink() {
var itemUrlString = (window.calendarItem || window.item).getProperty("URL") || "";
var linkCommand = document.getElementById("cmd_toggle_link");
function hideOrShow(aBool) {
setElementValue("event-grid-link-row", !aBool && "true", "hidden");
var separator = document.getElementById("event-grid-link-separator");
if (separator) {
// The separator is not there in the summary dialog
setElementValue("event-grid-link-separator", !aBool && "true", "hidden");
}
}
if (linkCommand) {
// Disable if there is no url
setElementValue(linkCommand,
!itemUrlString.length && "true",
"disabled");
}
if ((linkCommand && linkCommand.getAttribute("checked") != "true") ||
!itemUrlString.length) {
// Hide if there is no url, or the menuitem was chosen so that the url
// should be hidden
hideOrShow(false);
} else {
var handler, uri;
try {
uri = makeURL(itemUrlString);
handler = getIOService().getProtocolHandler(uri.scheme);
} catch (e) {
// No protocol handler for the given protocol, or invalid uri
hideOrShow(false);
return;
}
// Only show if its either an internal protcol handler, or its external
// and there is an external app for the scheme
hideOrShow(!calInstanceOf(handler, Components.interfaces.nsIExternalProtocolHandler) ||
handler.externalAppExistsForScheme(uri.scheme));
setTimeout(function() {
// HACK the url-link doesn't crop when setting the value in onLoad
setElementValue("url-link", itemUrlString);
setElementValue("url-link", itemUrlString, "href");
}, 0);
}
}