summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/item-editing
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/item-editing/calendar-item-editing.js849
-rw-r--r--comm/calendar/base/content/item-editing/calendar-item-iframe.js4302
-rw-r--r--comm/calendar/base/content/item-editing/calendar-item-iframe.xhtml1225
-rw-r--r--comm/calendar/base/content/item-editing/calendar-item-panel.inc.xhtml130
-rw-r--r--comm/calendar/base/content/item-editing/calendar-item-panel.js1143
-rw-r--r--comm/calendar/base/content/item-editing/calendar-item-toolbar.inc.xhtml164
-rw-r--r--comm/calendar/base/content/item-editing/calendar-task-editing.js181
7 files changed, 7994 insertions, 0 deletions
diff --git a/comm/calendar/base/content/item-editing/calendar-item-editing.js b/comm/calendar/base/content/item-editing/calendar-item-editing.js
new file mode 100644
index 0000000000..a280e62f48
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-item-editing.js
@@ -0,0 +1,849 @@
+/* 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/. */
+
+/* import-globals-from ../calendar-management.js */
+/* import-globals-from ../calendar-views-utils.js */
+
+/* globals goUpdateCommand */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+var { CalTransactionManager } = ChromeUtils.import("resource:///modules/CalTransactionManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalAddTransaction: "resource:///modules/CalTransactionManager.jsm",
+ CalDeleteTransaction: "resource:///modules/CalTransactionManager.jsm",
+ CalEvent: "resource:///modules/CalEvent.jsm",
+ CalModifyTransaction: "resource:///modules/CalTransactionManager.jsm",
+ CalTodo: "resource:///modules/CalTodo.jsm",
+});
+
+/* exported modifyEventWithDialog, undo, redo, setContextPartstat */
+
+/**
+ * The global calendar transaction manager.
+ *
+ * @type {CalTransactionManager}
+ */
+var gCalTransactionMgr = CalTransactionManager.getInstance();
+
+/**
+ * If a batch transaction is active, it is stored here.
+ *
+ * @type {CalBatchTransaction?}
+ */
+var gCalBatchTransaction = null;
+
+/**
+ * Sets the default values for new items, taking values from either the passed
+ * parameters or the preferences.
+ *
+ * @param {calIItemBase} aItem - The item to set up.
+ * @param {?calICalendar} aCalendar - The calendar to apply.
+ * @param {?calIDateTime} aStartDate - The start date to set.
+ * @param {?calIDateTime} aEndDate - The end date/due date to set.
+ * @param {?calIDateTime} aInitialDate - The reference date for the date pickers.
+ * @param {boolean} [aForceAllday=false] - Force the event/task to be an all-day item.
+ * @param {calIAttendee[]} aAttendees - Attendees to add, if `aItem` is an event.
+ */
+function setDefaultItemValues(
+ aItem,
+ aCalendar = null,
+ aStartDate = null,
+ aEndDate = null,
+ aInitialDate = null,
+ aForceAllday = false,
+ aAttendees = []
+) {
+ function endOfDay(aDate) {
+ let eod = aDate ? aDate.clone() : cal.dtz.now();
+ eod.hour = Services.prefs.getIntPref("calendar.view.dayendhour", 19);
+ eod.minute = 0;
+ eod.second = 0;
+ return eod;
+ }
+ function startOfDay(aDate) {
+ let sod = aDate ? aDate.clone() : cal.dtz.now();
+ sod.hour = Services.prefs.getIntPref("calendar.view.daystarthour", 8);
+ sod.minute = 0;
+ sod.second = 0;
+ return sod;
+ }
+
+ let initialDate = aInitialDate ? aInitialDate.clone() : cal.dtz.now();
+ initialDate.isDate = true;
+
+ if (aItem.isEvent()) {
+ if (aStartDate) {
+ aItem.startDate = aStartDate.clone();
+ if (aStartDate.isDate && !aForceAllday) {
+ // This is a special case where the date is specified, but the
+ // time is not. To take care, we setup up the time to our
+ // default event start time.
+ aItem.startDate = cal.dtz.getDefaultStartDate(aItem.startDate);
+ } else if (aForceAllday) {
+ // If the event should be forced to be allday, then don't set up
+ // any default hours and directly make it allday.
+ aItem.startDate.isDate = true;
+ aItem.startDate.timezone = cal.dtz.floating;
+ }
+ } else {
+ // If no start date was passed, then default to the next full hour
+ // of today, but with the date of the selected day
+ aItem.startDate = cal.dtz.getDefaultStartDate(initialDate);
+ }
+
+ if (aEndDate) {
+ aItem.endDate = aEndDate.clone();
+ if (aForceAllday) {
+ // XXX it is currently not specified, how callers that force all
+ // day should pass the end date. Right now, they should make
+ // sure that the end date is 00:00:00 of the day after.
+ aItem.endDate.isDate = true;
+ aItem.endDate.timezone = cal.dtz.floating;
+ }
+ } else {
+ aItem.endDate = aItem.startDate.clone();
+ if (aForceAllday) {
+ // All day events need to go to the beginning of the next day.
+ aItem.endDate.day++;
+ } else {
+ // If the event is not all day, then add the default event
+ // length.
+ aItem.endDate.minute += Services.prefs.getIntPref("calendar.event.defaultlength", 60);
+ }
+ }
+
+ // Free/busy status is only valid for events, must not be set for tasks.
+ aItem.setProperty("TRANSP", cal.item.getEventDefaultTransparency(aForceAllday));
+
+ for (let attendee of aAttendees) {
+ aItem.addAttendee(attendee);
+ }
+ } else if (aItem.isTodo()) {
+ let now = cal.dtz.now();
+ let initDate = initialDate ? initialDate.clone() : now;
+ initDate.isDate = false;
+ initDate.hour = now.hour;
+ initDate.minute = now.minute;
+ initDate.second = now.second;
+
+ if (aStartDate) {
+ aItem.entryDate = aStartDate.clone();
+ } else {
+ let defaultStart = Services.prefs.getStringPref("calendar.task.defaultstart", "none");
+ if (
+ Services.prefs.getIntPref("calendar.alarms.onfortodos", 0) == 1 &&
+ defaultStart == "none"
+ ) {
+ // start date is required if we want to set an alarm
+ defaultStart = "offsetcurrent";
+ }
+
+ let units = Services.prefs.getStringPref("calendar.task.defaultstartoffsetunits", "minutes");
+ if (!["days", "hours", "minutes"].includes(units)) {
+ units = "minutes";
+ }
+ let startOffset = cal.createDuration();
+ startOffset[units] = Services.prefs.getIntPref("calendar.task.defaultstartoffset", 0);
+ let start;
+
+ switch (defaultStart) {
+ case "none":
+ break;
+ case "startofday":
+ start = startOfDay(initDate);
+ break;
+ case "tomorrow":
+ start = startOfDay(initDate);
+ start.day++;
+ break;
+ case "nextweek":
+ start = startOfDay(initDate);
+ start.day += 7;
+ break;
+ case "offsetcurrent":
+ start = initDate.clone();
+ start.addDuration(startOffset);
+ break;
+ case "offsetnexthour":
+ start = initDate.clone();
+ start.second = 0;
+ start.minute = 0;
+ start.hour++;
+ start.addDuration(startOffset);
+ break;
+ }
+
+ if (start) {
+ aItem.entryDate = start;
+ }
+ }
+
+ if (aEndDate) {
+ aItem.dueDate = aEndDate.clone();
+ } else {
+ let defaultDue = Services.prefs.getStringPref("calendar.task.defaultdue", "none");
+
+ let units = Services.prefs.getStringPref("calendar.task.defaultdueoffsetunits", "minutes");
+ if (!["days", "hours", "minutes"].includes(units)) {
+ units = "minutes";
+ }
+ let dueOffset = cal.createDuration();
+ dueOffset[units] = Services.prefs.getIntPref("calendar.task.defaultdueoffset", 0);
+
+ let start = aItem.entryDate ? aItem.entryDate.clone() : initDate.clone();
+ let due;
+
+ switch (defaultDue) {
+ case "none":
+ break;
+ case "endofday":
+ due = endOfDay(start);
+ // go to tomorrow if we're past the end of today
+ if (start.compare(due) > 0) {
+ due.day++;
+ }
+ break;
+ case "tomorrow":
+ due = endOfDay(start);
+ due.day++;
+ break;
+ case "nextweek":
+ due = endOfDay(start);
+ due.day += 7;
+ break;
+ case "offsetcurrent":
+ due = start.clone();
+ due.addDuration(dueOffset);
+ break;
+ case "offsetnexthour":
+ due = start.clone();
+ due.second = 0;
+ due.minute = 0;
+ due.hour++;
+ due.addDuration(dueOffset);
+ break;
+ }
+
+ if (aItem.entryDate && due && aItem.entryDate.compare(due) > 0) {
+ // due can't be earlier than start date.
+ due = aItem.entryDate;
+ }
+
+ if (due) {
+ aItem.dueDate = due;
+ }
+ }
+ }
+
+ // Calendar
+ aItem.calendar = aCalendar || getSelectedCalendar();
+
+ // Alarms
+ cal.alarms.setDefaultValues(aItem);
+}
+
+/**
+ * Creates an event with the calendar event dialog.
+ *
+ * @param {?calICalendar} calendar - The calendar to create the event in
+ * @param {?calIDateTime} startDate - The event's start date.
+ * @param {?calIDateTime} endDate - The event's end date.
+ * @param {?string} summary - The event's title.
+ * @param {?calIEvent} event - A template event to show in the dialog
+ * @param {?boolean} forceAllDay - Make sure the event shown in the dialog is an all-day event.
+ * @param {?calIAttendee} attendees - Attendees to add to the event.
+ */
+function createEventWithDialog(
+ calendar,
+ startDate,
+ endDate,
+ summary,
+ event,
+ forceAllDay,
+ attendees
+) {
+ let onNewEvent = function (item, opcalendar, originalItem, listener, extresponse = null) {
+ if (item.id) {
+ // If the item already has an id, then this is the result of
+ // saving the item without closing, and then saving again.
+ doTransaction("modify", item, opcalendar, originalItem, listener, extresponse);
+ } else {
+ // Otherwise, this is an addition
+ doTransaction("add", item, opcalendar, null, listener, extresponse);
+ }
+ };
+
+ if (event) {
+ if (!event.isMutable) {
+ event = event.clone();
+ }
+ // If the event should be created from a template, then make sure to
+ // remove the id so that the item obtains a new id when doing the
+ // transaction
+ event.id = null;
+
+ if (forceAllDay) {
+ event.startDate.isDate = true;
+ event.endDate.isDate = true;
+ if (event.startDate.compare(event.endDate) == 0) {
+ // For a one day all day event, the end date must be 00:00:00 of
+ // the next day.
+ event.endDate.day++;
+ }
+ }
+
+ if (!event.calendar) {
+ event.calendar = calendar || getSelectedCalendar();
+ }
+ } else {
+ event = new CalEvent();
+
+ let refDate = currentView().selectedDay?.clone();
+ setDefaultItemValues(event, calendar, startDate, endDate, refDate, forceAllDay, attendees);
+ if (summary) {
+ event.title = summary;
+ }
+ }
+ openEventDialog(event, event.calendar, "new", onNewEvent);
+}
+
+/**
+ * Creates a task with the calendar event dialog.
+ *
+ * @param calendar (optional) The calendar to create the task in
+ * @param dueDate (optional) The task's due date.
+ * @param summary (optional) The task's title.
+ * @param todo (optional) A template task to show in the dialog.
+ * @param initialDate (optional) The initial date for new task datepickers
+ */
+function createTodoWithDialog(calendar, dueDate, summary, todo, initialDate) {
+ let onNewItem = function (item, opcalendar, originalItem, listener, extresponse = null) {
+ if (item.id) {
+ // If the item already has an id, then this is the result of
+ // saving the item without closing, and then saving again.
+ doTransaction("modify", item, opcalendar, originalItem, listener, extresponse);
+ } else {
+ // Otherwise, this is an addition
+ doTransaction("add", item, opcalendar, null, listener, extresponse);
+ }
+ };
+
+ if (todo) {
+ // If the todo should be created from a template, then make sure to
+ // remove the id so that the item obtains a new id when doing the
+ // transaction
+ if (todo.id) {
+ todo = todo.clone();
+ todo.id = null;
+ }
+
+ if (!todo.calendar) {
+ todo.calendar = calendar || getSelectedCalendar();
+ }
+ } else {
+ todo = new CalTodo();
+ setDefaultItemValues(todo, calendar, null, dueDate, initialDate);
+
+ if (summary) {
+ todo.title = summary;
+ }
+ }
+
+ openEventDialog(todo, calendar, "new", onNewItem, initialDate);
+}
+
+/**
+ * Opens the passed event item for viewing. This enables the modify callback in
+ * openEventDialog so invitation responses can be edited.
+ *
+ * @param {calIItemBase} item - The calendar item to view.
+ */
+function openEventDialogForViewing(item) {
+ function onDialogComplete(newItem, calendar, originalItem, listener, extresponse) {
+ doTransaction("modify", newItem, calendar, originalItem, listener, extresponse);
+ }
+ openEventDialog(item, item.calendar, "view", onDialogComplete);
+}
+
+/**
+ * Modifies the passed event in the event dialog.
+ *
+ * @param aItem The item to modify.
+ * @param aPromptOccurrence If the user should be prompted to select if the
+ * parent item or occurrence should be modified.
+ * @param initialDate (optional) The initial date for new task datepickers
+ * @param aCounterProposal (optional) An object representing the counterproposal
+ * {
+ * {JsObject} result: {
+ * type: {String} "OK"|"OUTDATED"|"NOTLATESTUPDATE"|"ERROR"|"NODIFF"
+ * descr: {String} a technical description of the problem if type is ERROR or NODIFF,
+ * otherwise an empty string
+ * },
+ * (empty if result.type = "ERROR"|"NODIFF"){Array} differences: [{
+ * property: {String} a property that is subject to the proposal
+ * proposed: {String} the proposed value
+ * original: {String} the original value
+ * }]
+ * }
+ */
+function modifyEventWithDialog(aItem, aPromptOccurrence, initialDate = null, aCounterProposal) {
+ let dlg = cal.item.findWindow(aItem);
+ if (dlg) {
+ dlg.focus();
+ return;
+ }
+
+ let onModifyItem = function (item, calendar, originalItem, listener, extresponse = null) {
+ doTransaction("modify", item, calendar, originalItem, listener, extresponse);
+ };
+
+ let item = aItem;
+ let response;
+ if (aPromptOccurrence !== false) {
+ [item, , response] = promptOccurrenceModification(aItem, true, "edit");
+ }
+
+ if (item && (response || response === undefined)) {
+ openEventDialog(item, item.calendar, "modify", onModifyItem, initialDate, aCounterProposal);
+ }
+}
+
+/**
+ * @callback onDialogComplete
+ *
+ * @param {calIItemBase} newItem
+ * @param {calICalendar} calendar
+ * @param {calIItemBase} originalItem
+ * @param {?calIOperationListener} listener
+ * @param {?object} extresponse
+ */
+
+/**
+ * Opens the event dialog with the given item (task OR event).
+ *
+ * @param {calIItemBase} calendarItem - The item to open the dialog with.
+ * @param {calICalendar} calendar - The calendar to open the dialog with.
+ * @param {string} mode - The operation the dialog should do
+ * ("new", "view", "modify").
+ * @param {onDialogComplete} callback - The callback to call when the dialog
+ * has completed.
+ * @param {?calIDateTime} initialDate - The initial date for new task
+ * datepickers.
+ * @param {?object} counterProposal - An object representing the
+ * counterproposal - see description
+ * for modifyEventWithDialog().
+ */
+function openEventDialog(
+ calendarItem,
+ calendar,
+ mode,
+ callback,
+ initialDate = null,
+ counterProposal
+) {
+ let dlg = cal.item.findWindow(calendarItem);
+ if (dlg) {
+ dlg.focus();
+ return;
+ }
+
+ // Set up some defaults
+ mode = mode || "new";
+ calendar = calendar || getSelectedCalendar();
+ let calendars = cal.manager.getCalendars();
+ calendars = calendars.filter(cal.acl.isCalendarWritable);
+
+ let isItemSupported;
+ if (calendarItem.isTodo()) {
+ isItemSupported = function (aCalendar) {
+ return aCalendar.getProperty("capabilities.tasks.supported") !== false;
+ };
+ } else if (calendarItem.isEvent()) {
+ isItemSupported = function (aCalendar) {
+ return aCalendar.getProperty("capabilities.events.supported") !== false;
+ };
+ }
+
+ // Filter out calendars that don't support the given calendar item
+ calendars = calendars.filter(isItemSupported);
+
+ // Filter out calendar/items that we cannot write to/modify
+ if (mode == "new") {
+ calendars = calendars.filter(cal.acl.userCanAddItemsToCalendar);
+ } else if (mode == "modify") {
+ calendars = calendars.filter(aCalendar => {
+ /* If the calendar is the item calendar, we check that the item
+ * can be modified. If the calendar is NOT the item calendar, we
+ * check that the user can remove items from that calendar and
+ * add items to the current one.
+ */
+ let isSameCalendar = calendarItem.calendar == aCalendar;
+ let canModify = cal.acl.userCanModifyItem(calendarItem);
+ let canMoveItems =
+ cal.acl.userCanDeleteItemsFromCalendar(calendarItem.calendar) &&
+ cal.acl.userCanAddItemsToCalendar(aCalendar);
+
+ return isSameCalendar ? canModify : canMoveItems;
+ });
+ }
+
+ if (
+ mode == "new" &&
+ (!cal.acl.isCalendarWritable(calendar) ||
+ !cal.acl.userCanAddItemsToCalendar(calendar) ||
+ !isItemSupported(calendar))
+ ) {
+ if (calendars.length < 1) {
+ // There are no writable calendars or no calendar supports the given
+ // item. Don't show the dialog.
+ return;
+ }
+ // Pick the first calendar that supports the item and is writable
+ calendar = calendars[0];
+ if (calendarItem) {
+ // XXX The dialog currently uses the items calendar as a first
+ // choice. Since we are shortly before a release to keep
+ // regression risk low, explicitly set the item's calendar here.
+ calendarItem.calendar = calendars[0];
+ }
+ }
+
+ // Setup the window arguments
+ let args = {};
+ args.calendarEvent = calendarItem;
+ args.calendar = calendar;
+ args.mode = mode;
+ args.onOk = callback;
+ args.initialStartDateValue = initialDate || cal.dtz.getDefaultStartDate();
+ args.counterProposal = counterProposal;
+ args.inTab = Services.prefs.getBoolPref("calendar.item.editInTab", false);
+ // this will be called if file->new has been selected from within the dialog
+ args.onNewEvent = function (opcalendar) {
+ createEventWithDialog(opcalendar, null, null);
+ };
+ args.onNewTodo = function (opcalendar) {
+ createTodoWithDialog(opcalendar);
+ };
+
+ // the dialog will reset this to auto when it is done loading.
+ window.setCursor("wait");
+
+ // Ask the provider if this item is an invitation. If this is the case,
+ // we'll open the summary dialog since the user is not allowed to change
+ // the details of the item.
+ let isInvitation =
+ calendar.supportsScheduling && calendar.getSchedulingSupport().isInvitation(calendarItem);
+
+ // open the dialog modeless
+ let url;
+ let isEditable = mode == "modify" && !isInvitation && cal.acl.userCanModifyItem(calendarItem);
+
+ if (cal.acl.isCalendarWritable(calendar) && (mode == "new" || isEditable)) {
+ // Currently the read-only summary dialog is never opened in a tab.
+ if (args.inTab) {
+ url = "chrome://calendar/content/calendar-item-iframe.xhtml";
+ } else {
+ url = "chrome://calendar/content/calendar-event-dialog.xhtml";
+ }
+ } else {
+ url = "chrome://calendar/content/calendar-summary-dialog.xhtml";
+ args.inTab = false;
+ args.isInvitation = isInvitation;
+ }
+
+ if (args.inTab) {
+ args.url = url;
+ let tabmail = document.getElementById("tabmail");
+ let tabtype = args.calendarEvent.isEvent() ? "calendarEvent" : "calendarTask";
+ tabmail.openTab(tabtype, args);
+ } else {
+ // open in a window
+ openDialog(url, "_blank", "chrome,titlebar,toolbar,resizable", args);
+ }
+}
+
+/**
+ * Prompts the user how the passed item should be modified. If the item is an
+ * exception or already a parent item, the item is returned without prompting.
+ * If "all occurrences" is specified, the parent item is returned. If "this
+ * occurrence only" is specified, then aItem is returned. If "this and following
+ * occurrences" is selected, aItem's parentItem is modified so that the
+ * recurrence rules end (UNTIL) just before the given occurrence. If
+ * aNeedsFuture is specified, a new item is made from the part that was stripped
+ * off the passed item.
+ *
+ * EXDATEs and RDATEs that do not fit into the items recurrence are removed. If
+ * the modified item or the future item only consist of a single occurrence,
+ * they are changed to be single items.
+ *
+ * @param aItem The item or array of items to check.
+ * @param aNeedsFuture If true, the future item is parsed.
+ * This parameter can for example be
+ * false if a deletion is being made.
+ * @param aAction Either "edit" or "delete". Sets up
+ * the labels in the occurrence prompt
+ * @returns [modifiedItem, futureItem, promptResponse]
+ * modifiedItem is a single item or array
+ * of items depending on the past aItem
+ *
+ * If "this and all following" was chosen,
+ * an array containing the item *until*
+ * the given occurrence (modifiedItem),
+ * and the item *after* the given
+ * occurrence (futureItem).
+ *
+ * If any other option was chosen,
+ * futureItem is null and the
+ * modifiedItem is either the parent item
+ * or the passed occurrence, or null if
+ * the dialog was canceled.
+ *
+ * The promptResponse parameter gives the
+ * response of the dialog as a constant.
+ */
+function promptOccurrenceModification(aItem, aNeedsFuture, aAction) {
+ const CANCEL = 0;
+ const MODIFY_OCCURRENCE = 1;
+ const MODIFY_FOLLOWING = 2;
+ const MODIFY_PARENT = 3;
+
+ let futureItems = false;
+ let pastItems = [];
+ let returnItem = null;
+ let type = CANCEL;
+ let items = Array.isArray(aItem) ? aItem : [aItem];
+
+ // Check if this actually is an instance of a recurring event
+ if (items.every(item => item == item.parentItem)) {
+ type = MODIFY_PARENT;
+ } else if (aItem && items.length) {
+ // Prompt the user. Setting modal blocks the dialog until it is closed. We
+ // use rv to pass our return value.
+ let rv = { value: CANCEL, items, action: aAction };
+ window.openDialog(
+ "chrome://calendar/content/calendar-occurrence-prompt.xhtml",
+ "PromptOccurrenceModification",
+ "centerscreen,chrome,modal,titlebar",
+ rv
+ );
+ type = rv.value;
+ }
+
+ switch (type) {
+ case MODIFY_PARENT:
+ pastItems = items.map(item => item.parentItem);
+ break;
+ case MODIFY_FOLLOWING:
+ // TODO tbd in a different bug
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ case MODIFY_OCCURRENCE:
+ pastItems = items;
+ break;
+ case CANCEL:
+ // Since we have not set past or futureItem, the return below will
+ // take care.
+ break;
+ }
+ if (aItem) {
+ returnItem = Array.isArray(aItem) ? pastItems : pastItems[0];
+ }
+ return [returnItem, futureItems, type];
+}
+
+// Undo/Redo code
+
+/**
+ * Create and commit a transaction with the given arguments to the transaction
+ * manager. Also updates the undo/redo menu.
+ *
+ * @param action The action to do.
+ * @param item The new item to add/modify/delete
+ * @param calendar The calendar to do the transaction on
+ * @param oldItem (optional) some actions require an old item
+ * @param observer (optional) the observer to call when complete.
+ * @param extResponse (optional) JS object with additional parameters for sending itip messages
+ * (see also description of checkAndSend in calItipUtils.jsm)
+ */
+async function doTransaction(action, item, calendar, oldItem, observer, extResponse = null) {
+ // This is usually a user-initiated transaction, so make sure the calendar
+ // this transaction is happening on is visible.
+ top.ensureCalendarVisible(calendar);
+
+ let manager = gCalBatchTransaction || gCalTransactionMgr;
+ let trn;
+ switch (action) {
+ case "add":
+ trn = new CalAddTransaction(item, calendar, oldItem, extResponse);
+ break;
+ case "modify":
+ trn = new CalModifyTransaction(item, calendar, oldItem, extResponse);
+ break;
+ case "delete":
+ trn = new CalDeleteTransaction(item, calendar, oldItem, extResponse);
+ break;
+ default:
+ throw new Components.Exception(
+ `Invalid action specified "${action}"`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ await manager.commit(trn);
+
+ // If a batch transaction is active, do not update the menu as
+ // endBatchTransaction() will take care of that.
+ if (gCalBatchTransaction) {
+ return;
+ }
+
+ observer?.onTransactionComplete(trn.item, trn.oldItem);
+ updateUndoRedoMenu();
+}
+
+/**
+ * Undo the last operation done through the transaction manager.
+ */
+function undo() {
+ if (canUndo()) {
+ gCalTransactionMgr.undo();
+ updateUndoRedoMenu();
+ }
+}
+
+/**
+ * Redo the last undone operation in the transaction manager.
+ */
+function redo() {
+ if (canRedo()) {
+ gCalTransactionMgr.redo();
+ updateUndoRedoMenu();
+ }
+}
+
+/**
+ * Start a batch transaction on the transaction manager.
+ */
+function startBatchTransaction() {
+ gCalBatchTransaction = gCalTransactionMgr.beginBatch();
+}
+
+/**
+ * End a previously started batch transaction. NOTE: be sure to call this in a
+ * try-catch-finally-block in case you have code that could fail between
+ * startBatchTransaction and this call.
+ */
+function endBatchTransaction() {
+ gCalBatchTransaction = null;
+ updateUndoRedoMenu();
+}
+
+/**
+ * Checks if the last operation can be undone (or if there is a last operation
+ * at all).
+ */
+function canUndo() {
+ return gCalTransactionMgr.canUndo();
+}
+
+/**
+ * Checks if the last undone operation can be redone.
+ */
+function canRedo() {
+ return gCalTransactionMgr.canRedo();
+}
+
+/**
+ * Update the undo and redo commands.
+ */
+function updateUndoRedoMenu() {
+ goUpdateCommand("cmd_undo");
+ goUpdateCommand("cmd_redo");
+}
+
+/**
+ * Updates the partstat of the calendar owner for specified items triggered by a
+ * context menu operation
+ *
+ * For a documentation of the expected bahaviours for different use cases of
+ * dealing with context menu partstat actions, see also setupAttendanceMenu(...)
+ * in calendar-ui-utils.js
+ *
+ * @param {EventTarget} aTarget the target of the triggering event
+ * @param {Array} aItems an array of calEvent or calIToDo items
+ */
+function setContextPartstat(aTarget, aItems) {
+ /**
+ * Provides the participation representing the user for a provided item
+ *
+ * @param {calEvent|calTodo} aItem The calendar item to inspect
+ * @returns {?calIAttendee} An calIAttendee object or null if no
+ * participant was detected
+ */
+ function getParticipant(aItem) {
+ let party = null;
+ if (cal.itip.isInvitation(aItem)) {
+ party = cal.itip.getInvitedAttendee(aItem);
+ } else if (aItem.organizer && aItem.getAttendees().length) {
+ let calOrgId = aItem.calendar.getProperty("organizerId");
+ if (calOrgId.toLowerCase() == aItem.organizer.id.toLowerCase()) {
+ party = aItem.organizer;
+ }
+ }
+ return party;
+ }
+
+ startBatchTransaction();
+ try {
+ // TODO: make sure we overwrite the partstat of all occurrences in
+ // the selection, if the partstat of the respective master item is
+ // changed - see matrix in the doc block of setupAttendanceMenu(...)
+ // in calendar-ui-utils.js
+
+ for (let oldItem of aItems) {
+ // Skip this item if its calendar is read only.
+ if (oldItem.calendar.readOnly) {
+ continue;
+ }
+ if (aTarget.getAttribute("scope") == "all-occurrences") {
+ oldItem = oldItem.parentItem;
+ }
+ let attendee = getParticipant(oldItem);
+ if (attendee) {
+ // skip this item if the partstat for the participant hasn't
+ // changed. otherwise we would always perform update operations
+ // for recurring events on both, the master and the occurrence
+ // item
+ let partStat = aTarget.getAttribute("respvalue");
+ if (attendee.participationStatus == partStat) {
+ continue;
+ }
+
+ let newItem = oldItem.clone();
+ let newAttendee = attendee.clone();
+ newAttendee.participationStatus = partStat;
+ if (newAttendee.isOrganizer) {
+ newItem.organizer = newAttendee;
+ } else {
+ newItem.removeAttendee(attendee);
+ newItem.addAttendee(newAttendee);
+ }
+
+ let extResponse = null;
+ if (aTarget.hasAttribute("respmode")) {
+ let mode = aTarget.getAttribute("respmode");
+ let itipMode = Ci.calIItipItem[mode];
+ extResponse = { responseMode: itipMode };
+ }
+
+ doTransaction("modify", newItem, newItem.calendar, oldItem, null, extResponse);
+ }
+ }
+ } catch (e) {
+ cal.ERROR("Error setting partstat: " + e + "\r\n");
+ } finally {
+ endBatchTransaction();
+ }
+}
diff --git a/comm/calendar/base/content/item-editing/calendar-item-iframe.js b/comm/calendar/base/content/item-editing/calendar-item-iframe.js
new file mode 100644
index 0000000000..bdabd21356
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-item-iframe.js
@@ -0,0 +1,4302 @@
+/* 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/. */
+
+/* exported onEventDialogUnload, changeUndiscloseCheckboxStatus,
+ * categoryPopupHiding, categoryTextboxKeypress,
+ * toggleKeepDuration, dateTimeControls2State, onUpdateAllDay,
+ * openNewEvent, openNewTask, openNewMessage,
+ * deleteAllAttachments, copyAttachment, attachmentLinkKeyPress,
+ * attachmentDblClick, attachmentClick, notifyUser,
+ * removeNotification, chooseRecentTimezone, showTimezonePopup,
+ * attendeeDblClick, setAttendeeContext, removeAttendee,
+ * removeAllAttendees, sendMailToUndecidedAttendees, checkUntilDate,
+ * applyValues
+ */
+
+/* global MozElements */
+
+/* import-globals-from ../../../../mail/components/compose/content/editor.js */
+/* import-globals-from ../../../../mail/components/compose/content/editorUtilities.js */
+/* import-globals-from ../calendar-ui-utils.js */
+/* import-globals-from ../dialogs/calendar-dialog-utils.js */
+/* globals gTimezonesEnabled */ // Set by calendar-item-panel.js.
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var {
+ recurrenceRule2String,
+ splitRecurrenceRules,
+ checkRecurrenceRule,
+ countOccurrences,
+ hasUnsupported,
+} = ChromeUtils.import("resource:///modules/calendar/calRecurrenceUtils.jsm");
+var { PluralForm } = ChromeUtils.importESModule("resource://gre/modules/PluralForm.sys.mjs");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalAttachment: "resource:///modules/CalAttachment.jsm",
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm",
+});
+
+window.addEventListener("load", onLoad);
+window.addEventListener("unload", onEventDialogUnload);
+
+var cloudFileAccounts;
+try {
+ ({ cloudFileAccounts } = ChromeUtils.import("resource:///modules/cloudFileAccounts.jsm"));
+} catch (e) {
+ // This will fail on Seamonkey, but that's ok since the pref for cloudfiles
+ // is false, which means the UI will not be shown
+}
+
+// the following variables are constructed if the jsContext this file
+// belongs to gets constructed. all those variables are meant to be accessed
+// from within this file only.
+var gStartTime = null;
+var gEndTime = null;
+var gItemDuration = null;
+var gStartTimezone = null;
+var gEndTimezone = null;
+var gUntilDate = null;
+var gIsReadOnly = false;
+var gAttachMap = {};
+var gConfirmCancel = true;
+var gLastRepeatSelection = 0;
+var gIgnoreUpdate = false;
+var gWarning = false;
+var gPreviousCalendarId = null;
+var gTabInfoObject;
+var gLastAlarmSelection = 0;
+var gConfig = {
+ priority: 0,
+ privacy: null,
+ status: "NONE",
+ showTimeAs: null,
+ percentComplete: 0,
+};
+// The following variables are set by the load handler function of the
+// parent context, so that they are already set before iframe content load:
+// - gTimezoneEnabled
+
+XPCOMUtils.defineLazyGetter(this, "gEventNotification", () => {
+ return new MozElements.NotificationBox(element => {
+ document.getElementById("event-dialog-notifications").append(element);
+ });
+});
+
+var eventDialogRequestObserver = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(aSubject, aTopic, aData) {
+ if (
+ aTopic == "http-on-modify-request" &&
+ aSubject instanceof Ci.nsIChannel &&
+ aSubject.loadInfo &&
+ aSubject.loadInfo.loadingDocument &&
+ aSubject.loadInfo.loadingDocument ==
+ document.getElementById("item-description").contentDocument
+ ) {
+ aSubject.cancel(Cr.NS_ERROR_ABORT);
+ }
+ },
+};
+
+var eventDialogQuitObserver = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ observe(aSubject, aTopic, aData) {
+ // Check whether or not we want to veto the quit request (unless another
+ // observer already did.
+ if (
+ aTopic == "quit-application-requested" &&
+ aSubject instanceof Ci.nsISupportsPRBool &&
+ !aSubject.data
+ ) {
+ aSubject.data = !onCancel();
+ }
+ },
+};
+
+var eventDialogCalendarObserver = {
+ QueryInterface: ChromeUtils.generateQI(["calIObserver"]),
+
+ target: null,
+ isObserving: false,
+
+ onModifyItem(aNewItem, aOldItem) {
+ if (
+ this.isObserving &&
+ "calendarItem" in window &&
+ window.calendarItem &&
+ window.calendarItem.id == aOldItem.id
+ ) {
+ let doUpdate = true;
+
+ // The item has been modified outside the dialog. We only need to
+ // prompt if there have been local changes also.
+ if (isItemChanged()) {
+ let promptTitle = cal.l10n.getCalString("modifyConflictPromptTitle");
+ let promptMessage = cal.l10n.getCalString("modifyConflictPromptMessage");
+ let promptButton1 = cal.l10n.getCalString("modifyConflictPromptButton1");
+ let promptButton2 = cal.l10n.getCalString("modifyConflictPromptButton2");
+ let flags =
+ Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_0 +
+ Ci.nsIPromptService.BUTTON_TITLE_IS_STRING * Ci.nsIPromptService.BUTTON_POS_1;
+
+ let choice = Services.prompt.confirmEx(
+ window,
+ promptTitle,
+ promptMessage,
+ flags,
+ promptButton1,
+ promptButton2,
+ null,
+ null,
+ {}
+ );
+ if (!choice) {
+ doUpdate = false;
+ }
+ }
+
+ let item = aNewItem;
+ if (window.calendarItem.recurrenceId && aNewItem.recurrenceInfo) {
+ item = aNewItem.recurrenceInfo.getOccurrenceFor(window.calendarItem.recurrenceId) || item;
+ }
+ window.calendarItem = item;
+
+ if (doUpdate) {
+ loadDialog(window.calendarItem);
+ }
+ }
+ },
+
+ onDeleteItem(aDeletedItem) {
+ if (
+ this.isObserving &&
+ "calendarItem" in window &&
+ window.calendarItem &&
+ window.calendarItem.id == aDeletedItem.id
+ ) {
+ cancelItem();
+ }
+ },
+
+ onStartBatch() {},
+ onEndBatch() {},
+ onLoad() {},
+ onAddItem() {},
+ onError() {},
+ onPropertyChanged() {},
+ onPropertyDeleting() {},
+
+ observe(aCalendar) {
+ // use the new calendar if one was passed, otherwise use the last one
+ this.target = aCalendar || this.target;
+ if (this.target) {
+ this.cancel();
+ this.target.addObserver(this);
+ this.isObserving = true;
+ }
+ },
+
+ cancel() {
+ if (this.isObserving && this.target) {
+ this.target.removeObserver(this);
+ this.isObserving = false;
+ }
+ },
+};
+
+/**
+ * Checks if the given calendar supports notifying attendees. The item is needed
+ * since calendars may support notifications for only some types of items.
+ *
+ * @param {calICalendar} aCalendar - The calendar to check
+ * @param {calIItemBase} aItem - The item to check support for
+ */
+function canNotifyAttendees(aCalendar, aItem) {
+ try {
+ let calendar = aCalendar.QueryInterface(Ci.calISchedulingSupport);
+ return calendar.canNotify("REQUEST", aItem) && calendar.canNotify("CANCEL", aItem);
+ } catch (exc) {
+ return false;
+ }
+}
+
+/**
+ * Sends an asynchronous message to the parent context that contains the
+ * iframe. Additional properties of aMessage are generally arguments
+ * that will be passed to the function named in aMessage.command.
+ *
+ * @param {object} aMessage - The message to pass to the parent context
+ * @param {string} aMessage.command - The name of a function to call
+ */
+function sendMessage(aMessage) {
+ parent.postMessage(aMessage, "*");
+}
+
+/**
+ * Receives asynchronous messages from the parent context that contains the iframe.
+ *
+ * @param {MessageEvent} aEvent - Contains the message being received
+ */
+function receiveMessage(aEvent) {
+ let validOrigin = gTabmail ? "chrome://messenger" : "chrome://calendar";
+ if (aEvent.origin !== validOrigin) {
+ return;
+ }
+ switch (aEvent.data.command) {
+ case "editAttendees":
+ editAttendees();
+ break;
+ case "attachURL":
+ attachURL();
+ break;
+ case "onCommandDeleteItem":
+ onCommandDeleteItem();
+ break;
+ case "onCommandSave":
+ onCommandSave(aEvent.data.isClosing);
+ break;
+ case "onAccept":
+ onAccept();
+ break;
+ case "onCancel":
+ onCancel(aEvent.data.iframeId);
+ break;
+ case "openNewEvent":
+ openNewEvent();
+ break;
+ case "openNewTask":
+ openNewTask();
+ break;
+ case "editConfigState": {
+ Object.assign(gConfig, aEvent.data.argument);
+ updateConfigState(aEvent.data.argument);
+ break;
+ }
+ case "editToDoStatus": {
+ let textbox = document.getElementById("percent-complete-textbox");
+ textbox.value = aEvent.data.value;
+ updateToDoStatus("percent-changed");
+ break;
+ }
+ case "postponeTask":
+ postponeTask(aEvent.data.value);
+ break;
+ case "toggleTimezoneLinks":
+ gTimezonesEnabled = aEvent.data.checked; // eslint-disable-line
+ updateDateTime();
+ break;
+ case "closingWindowWithTabs": {
+ let response = onCancel(aEvent.data.id, true);
+ sendMessage({
+ command: "replyToClosingWindowWithTabs",
+ response,
+ });
+ break;
+ }
+ case "attachFileByAccountKey":
+ attachFileByAccountKey(aEvent.data.accountKey);
+ break;
+ case "triggerUpdateSaveControls":
+ updateParentSaveControls();
+ break;
+ }
+}
+
+/**
+ * Sets up the event dialog from the window arguments, also setting up all
+ * dialog controls from the window's item.
+ */
+function onLoad() {
+ window.addEventListener("message", receiveMessage);
+
+ // first of all retrieve the array of
+ // arguments this window has been called with.
+ let args = window.arguments[0];
+
+ intializeTabOrWindowVariables();
+
+ // Needed so we can call switchToTab for the prompt about saving
+ // unsaved changes, to show the tab that the prompt is for.
+ if (gInTab) {
+ gTabInfoObject = gTabmail.currentTabInfo;
+ }
+
+ // the most important attribute we expect from the
+ // arguments is the item we'll edit in the dialog.
+ let item = args.calendarEvent;
+
+ // set the iframe's top level id for event vs task
+ if (item.isTodo()) {
+ setDialogId(document.documentElement, "calendar-task-dialog-inner");
+ }
+
+ document.getElementById("item-title").placeholder = cal.l10n.getString(
+ "calendar-event-dialog",
+ item.isEvent() ? "newEvent" : "newTask"
+ );
+
+ window.onAcceptCallback = args.onOk;
+ window.mode = args.mode;
+
+ // we store the item in the window to be able
+ // to access this from any location. please note
+ // that the item is either an occurrence [proxy]
+ // or the stand-alone item [single occurrence item].
+ window.calendarItem = item;
+ // store the initial date value for datepickers in New Task dialog
+ window.initialStartDateValue = args.initialStartDateValue;
+
+ window.attendeeTabLabel = document.getElementById("event-grid-tab-attendees").label;
+ window.attachmentTabLabel = document.getElementById("event-grid-tab-attachments").label;
+
+ // Store the array of attendees on the window for later retrieval. Clone each
+ // existing attendee to prevent modifying objects referenced elsewhere.
+ const attendees = item.getAttendees() ?? [];
+ window.attendees = attendees.map(attendee => attendee.clone());
+
+ window.organizer = null;
+ if (item.organizer) {
+ window.organizer = item.organizer.clone();
+ } else if (attendees.length > 0) {
+ // Previous versions of calendar may not have set the organizer correctly.
+ let organizerId = item.calendar.getProperty("organizerId");
+ if (organizerId) {
+ let organizer = new CalAttendee();
+ organizer.id = cal.email.removeMailTo(organizerId);
+ organizer.commonName = item.calendar.getProperty("organizerCN");
+ organizer.isOrganizer = true;
+ window.organizer = organizer;
+ }
+ }
+
+ // we store the recurrence info in the window so it
+ // can be accessed from any location. since the recurrence
+ // info is a property of the parent item we need to check
+ // whether or not this item is a proxy or a parent.
+ let parentItem = item;
+ if (parentItem.parentItem != parentItem) {
+ parentItem = parentItem.parentItem;
+ }
+
+ window.recurrenceInfo = null;
+ if (parentItem.recurrenceInfo) {
+ window.recurrenceInfo = parentItem.recurrenceInfo.clone();
+ }
+
+ // Set initial values for datepickers in New Tasks dialog
+ if (item.isTodo()) {
+ let initialDatesValue = cal.dtz.dateTimeToJsDate(args.initialStartDateValue);
+ document.getElementById("completed-date-picker").value = initialDatesValue;
+ document.getElementById("todo-entrydate").value = initialDatesValue;
+ document.getElementById("todo-duedate").value = initialDatesValue;
+ }
+ loadDialog(window.calendarItem);
+
+ if (args.counterProposal) {
+ window.counterProposal = args.counterProposal;
+ displayCounterProposal();
+ }
+
+ gMainWindow.setCursor("auto");
+
+ document.getElementById("item-title").select();
+
+ // This causes the app to ask if the window should be closed when the
+ // application is closed.
+ Services.obs.addObserver(eventDialogQuitObserver, "quit-application-requested");
+
+ // This stops the editor from loading remote HTTP(S) content.
+ Services.obs.addObserver(eventDialogRequestObserver, "http-on-modify-request");
+
+ // Normally, Enter closes a <dialog>. We want this to rather on Ctrl+Enter.
+ // Stopping event propagation doesn't seem to work, so just overwrite the
+ // function that does this.
+ if (!gInTab) {
+ document.documentElement._hitEnter = function () {};
+ }
+
+ // set up our calendar event observer
+ eventDialogCalendarObserver.observe(item.calendar);
+
+ // Disable save and save close buttons and menuitems if the item
+ // title is empty.
+ updateTitle();
+
+ cal.view.colorTracker.registerWindow(window);
+
+ top.document.commandDispatcher.addCommandUpdater(
+ document.getElementById("styleMenuItems"),
+ "style",
+ "*"
+ );
+ EditorSharedStartup();
+
+ // We want to keep HTML output as simple as possible, so don't try to use divs
+ // as separators. As a bonus, this avoids a bug in the editor which sometimes
+ // causes the user to have to hit enter twice for it to take effect.
+ const editor = GetCurrentEditor();
+ editor.document.execCommand("defaultparagraphseparator", false, "br");
+
+ onLoad.hasLoaded = true;
+}
+// Set a variable to allow or prevent actions before the dialog is done loading.
+onLoad.hasLoaded = false;
+
+function onEventDialogUnload() {
+ Services.obs.removeObserver(eventDialogRequestObserver, "http-on-modify-request");
+ Services.obs.removeObserver(eventDialogQuitObserver, "quit-application-requested");
+ eventDialogCalendarObserver.cancel();
+}
+
+/**
+ * Handler function to be called when the accept button is pressed.
+ *
+ * @returns Returns true if the window should be closed
+ */
+function onAccept() {
+ dispose();
+ onCommandSave(true);
+ if (!gWarning) {
+ sendMessage({ command: "closeWindowOrTab" });
+ }
+ return !gWarning;
+}
+
+/**
+ * Asks the user if the item should be saved and does so if requested. If the
+ * user cancels, the window should stay open.
+ *
+ * XXX Could possibly be consolidated into onCancel()
+ *
+ * @returns Returns true if the window should be closed.
+ */
+function onCommandCancel() {
+ // Allow closing if the item has not changed and no warning dialog has to be showed.
+ if (!isItemChanged() && !gWarning) {
+ return true;
+ }
+
+ if (gInTab && gTabInfoObject) {
+ // Switch to the tab that the prompt refers to.
+ gTabmail.switchToTab(gTabInfoObject);
+ }
+
+ let promptTitle = cal.l10n.getCalString(
+ window.calendarItem.isEvent() ? "askSaveTitleEvent" : "askSaveTitleTask"
+ );
+ let promptMessage = cal.l10n.getCalString(
+ window.calendarItem.isEvent() ? "askSaveMessageEvent" : "askSaveMessageTask"
+ );
+
+ let flags =
+ Ci.nsIPromptService.BUTTON_TITLE_SAVE * Ci.nsIPromptService.BUTTON_POS_0 +
+ Ci.nsIPromptService.BUTTON_TITLE_CANCEL * Ci.nsIPromptService.BUTTON_POS_1 +
+ Ci.nsIPromptService.BUTTON_TITLE_DONT_SAVE * Ci.nsIPromptService.BUTTON_POS_2;
+
+ let choice = Services.prompt.confirmEx(
+ null,
+ promptTitle,
+ promptMessage,
+ flags,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+ switch (choice) {
+ case 0: // Save
+ let itemTitle = document.getElementById("item-title");
+ if (!itemTitle.value) {
+ itemTitle.value = cal.l10n.getCalString("eventUntitled");
+ }
+ onCommandSave(true);
+ return true;
+ case 2: // Don't save
+ // Don't show any warning dialog when closing without saving.
+ gWarning = false;
+ return true;
+ default:
+ // Cancel
+ return false;
+ }
+}
+
+/**
+ * Handler function to be called when the cancel button is pressed.
+ * aPreventClose is true when closing the main window but leaving the tab open.
+ *
+ * @param {string} aIframeId (optional) iframe id of the tab to be closed
+ * @param {boolean} aPreventClose (optional) True means don't close, just ask about saving
+ * @returns {boolean} True if the tab or window should be closed
+ */
+function onCancel(aIframeId, aPreventClose) {
+ // The datepickers need to remove the focus in order to trigger the
+ // validation of the values just edited, with the keyboard, but not yet
+ // confirmed (i.e. not followed by a click, a tab or enter keys pressure).
+ document.documentElement.focus();
+
+ if (!gConfirmCancel || (gConfirmCancel && onCommandCancel())) {
+ dispose();
+ // Don't allow closing the dialog when the user inputs a wrong
+ // date then closes the dialog and answers with "Save" in
+ // the "Save Event" dialog. Don't allow closing the dialog if
+ // the main window is being closed but the tabs in it are not.
+
+ if (!gWarning && !aPreventClose) {
+ sendMessage({ command: "closeWindowOrTab", iframeId: aIframeId });
+ }
+ return !gWarning;
+ }
+ return false;
+}
+
+/**
+ * Cancels (closes) either the window or the tab, for example when the
+ * item is being deleted.
+ */
+function cancelItem() {
+ gConfirmCancel = false;
+ if (gInTab) {
+ onCancel();
+ } else {
+ sendMessage({ command: "cancelDialog" });
+ }
+}
+
+/**
+ * Get the currently selected calendar from the menulist of calendars.
+ *
+ * @returns The currently selected calendar.
+ */
+function getCurrentCalendar() {
+ return document.getElementById("item-calendar").selectedItem.calendar;
+}
+
+/**
+ * Sets up all dialog controls from the information of the passed item.
+ *
+ * @param aItem The item to parse information out of.
+ */
+function loadDialog(aItem) {
+ loadDateTime(aItem);
+
+ document.getElementById("item-title").value = aItem.title;
+ document.getElementById("item-location").value = aItem.getProperty("LOCATION");
+
+ // add calendars to the calendar menulist
+ let calendarList = document.getElementById("item-calendar");
+ let indexToSelect = appendCalendarItems(
+ aItem,
+ calendarList,
+ aItem.calendar || window.arguments[0].calendar
+ );
+ if (indexToSelect > -1) {
+ calendarList.selectedIndex = indexToSelect;
+ }
+
+ // Categories
+ loadCategories(aItem);
+
+ // Attachment
+ loadCloudProviders();
+
+ let hasAttachments = capSupported("attachments");
+ let attachments = aItem.getAttachments();
+ if (hasAttachments && attachments && attachments.length > 0) {
+ for (let attachment of attachments) {
+ addAttachment(attachment);
+ }
+ } else {
+ updateAttachment();
+ }
+
+ // URL link
+ let itemUrl = window.calendarItem.getProperty("URL")?.trim() || "";
+ let showLink = showOrHideItemURL(itemUrl);
+ updateItemURL(showLink, itemUrl);
+
+ // Description
+ let editorElement = document.getElementById("item-description");
+ let editor = editorElement.getHTMLEditor(editorElement.contentWindow);
+
+ let link = editorElement.contentDocument.createElement("link");
+ link.rel = "stylesheet";
+ link.href = "chrome://messenger/skin/shared/editorContent.css";
+ editorElement.contentDocument.head.appendChild(link);
+
+ try {
+ let checker = editor.getInlineSpellChecker(true);
+ checker.enableRealTimeSpell = Services.prefs.getBoolPref("mail.spellcheck.inline", true);
+ } catch (ex) {
+ // No dictionaries.
+ }
+
+ if (aItem.descriptionText) {
+ let docFragment = cal.view.textToHtmlDocumentFragment(
+ aItem.descriptionText,
+ editorElement.contentDocument,
+ aItem.descriptionHTML
+ );
+ editor.flags =
+ editor.eEditorMailMask | editor.eEditorNoCSSMask | editor.eEditorAllowInteraction;
+ editor.enableUndo(false);
+ editor.forceCompositionEnd();
+ editor.rootElement.replaceChildren(docFragment);
+ // This reinitialises the editor after we replaced its contents.
+ editor.insertText("");
+ editor.enableUndo(true);
+ }
+
+ editor.resetModificationCount();
+
+ if (aItem.isTodo()) {
+ // Task completed date
+ if (aItem.completedDate) {
+ updateToDoStatus(aItem.status, cal.dtz.dateTimeToJsDate(aItem.completedDate));
+ } else {
+ updateToDoStatus(aItem.status);
+ }
+
+ // Task percent complete
+ let percentCompleteInteger = 0;
+ let percentCompleteProperty = aItem.getProperty("PERCENT-COMPLETE");
+ if (percentCompleteProperty != null) {
+ percentCompleteInteger = parseInt(percentCompleteProperty, 10);
+ }
+ if (percentCompleteInteger < 0) {
+ percentCompleteInteger = 0;
+ } else if (percentCompleteInteger > 100) {
+ percentCompleteInteger = 100;
+ }
+ gConfig.percentComplete = percentCompleteInteger;
+ document.getElementById("percent-complete-textbox").value = percentCompleteInteger;
+ }
+
+ // When in a window, set Item-Menu label to Event or Task
+ if (!gInTab) {
+ let isEvent = aItem.isEvent();
+
+ let labelString = isEvent ? "itemMenuLabelEvent" : "itemMenuLabelTask";
+ let label = cal.l10n.getString("calendar-event-dialog", labelString);
+
+ let accessKeyString = isEvent ? "itemMenuAccesskeyEvent2" : "itemMenuAccesskeyTask2";
+ let accessKey = cal.l10n.getString("calendar-event-dialog", accessKeyString);
+ sendMessage({
+ command: "initializeItemMenu",
+ label,
+ accessKey,
+ });
+ }
+
+ // Repeat details
+ let [repeatType, untilDate] = getRepeatTypeAndUntilDate(aItem);
+ loadRepeat(repeatType, untilDate, aItem);
+
+ // load reminders details
+ let alarmsMenu = document.querySelector(".item-alarm");
+ window.gLastAlarmSelection = loadReminders(aItem.getAlarms(), alarmsMenu, getCurrentCalendar());
+
+ // Synchronize link-top-image with keep-duration-button status
+ let keepAttribute = document.getElementById("keepduration-button").getAttribute("keep") == "true";
+ document.getElementById("link-image-top").setAttribute("keep", keepAttribute);
+
+ updateDateTime();
+
+ updateCalendar();
+
+ let notifyCheckbox = document.getElementById("notify-attendees-checkbox");
+ let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox");
+ let disallowcounterCheckbox = document.getElementById("disallow-counter-checkbox");
+ if (canNotifyAttendees(aItem.calendar, aItem)) {
+ // visualize that the server will send out mail:
+ notifyCheckbox.checked = true;
+ // hide these controls as this a client only feature
+ undiscloseCheckbox.disabled = true;
+ } else {
+ let itemProp = aItem.getProperty("X-MOZ-SEND-INVITATIONS");
+ notifyCheckbox.checked =
+ aItem.calendar.getProperty("imip.identity") &&
+ (itemProp === null
+ ? Services.prefs.getBoolPref("calendar.itip.notify", true)
+ : itemProp == "TRUE");
+ let undiscloseProp = aItem.getProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED");
+ undiscloseCheckbox.checked =
+ undiscloseProp === null
+ ? Services.prefs.getBoolPref("calendar.itip.separateInvitationPerAttendee")
+ : undiscloseProp == "TRUE";
+ // disable checkbox, if notifyCheckbox is not checked
+ undiscloseCheckbox.disabled = !notifyCheckbox.checked;
+ }
+ // this may also be a server exposed calendar property from exchange servers - if so, this
+ // probably should overrule the client-side config option
+ let disallowCounterProp = aItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER");
+ disallowcounterCheckbox.checked = disallowCounterProp == "TRUE";
+ // if we're in reschedule mode, it's pointless to enable the control
+ disallowcounterCheckbox.disabled = !!window.counterProposal;
+
+ updateAttendeeInterface();
+ updateRepeat(true);
+ updateReminder(true);
+
+ // Status
+ if (aItem.isEvent()) {
+ gConfig.status = aItem.hasProperty("STATUS") ? aItem.getProperty("STATUS") : "NONE";
+ if (gConfig.status == "NONE") {
+ sendMessage({ command: "showCmdStatusNone" });
+ }
+ updateConfigState({ status: gConfig.status });
+ } else {
+ let itemStatus = aItem.getProperty("STATUS");
+ let todoStatus = document.getElementById("todo-status");
+ todoStatus.value = itemStatus;
+ if (!todoStatus.selectedItem) {
+ // No selected item means there was no <menuitem> that matches the
+ // value given. Select the "NONE" item by default.
+ todoStatus.value = "NONE";
+ }
+ }
+
+ // Priority, Privacy, Transparency
+ gConfig.priority = parseInt(aItem.priority, 10);
+ gConfig.privacy = aItem.privacy;
+ gConfig.showTimeAs = aItem.getProperty("TRANSP");
+
+ // update in outer parent context
+ updateConfigState(gConfig);
+
+ if (aItem.getAttendees().length && !aItem.descriptionText) {
+ let tabs = document.getElementById("event-grid-tabs");
+ let attendeeTab = document.getElementById("event-grid-tab-attendees");
+ tabs.selectedItem = attendeeTab;
+ }
+}
+
+/**
+ * Enables/disables undiscloseCheckbox on (un)checking notifyCheckbox
+ */
+function changeUndiscloseCheckboxStatus() {
+ let notifyCheckbox = document.getElementById("notify-attendees-checkbox");
+ let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox");
+ undiscloseCheckbox.disabled = !notifyCheckbox.checked;
+ updateParentSaveControls();
+}
+
+/**
+ * Loads the item's categories into the category panel
+ *
+ * @param aItem The item to load into the category panel
+ */
+function loadCategories(aItem) {
+ let itemCategories = aItem.getCategories();
+ let categoryList = cal.category.fromPrefs();
+ for (let cat of itemCategories) {
+ if (!categoryList.includes(cat)) {
+ categoryList.push(cat);
+ }
+ }
+ cal.l10n.sortArrayByLocaleCollator(categoryList);
+
+ // Make sure the maximum number of categories is applied to the listbox
+ let calendar = getCurrentCalendar();
+ let maxCount = calendar.getProperty("capabilities.categories.maxCount");
+
+ let categoryPopup = document.getElementById("item-categories-popup");
+ if (maxCount == 1) {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("class", "menuitem-iconic");
+ item.setAttribute("label", cal.l10n.getCalString("None"));
+ item.setAttribute("type", "radio");
+ if (itemCategories.length === 0) {
+ item.setAttribute("checked", "true");
+ }
+ categoryPopup.appendChild(item);
+ }
+ for (let cat of categoryList) {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("class", "menuitem-iconic calendar-category");
+ item.setAttribute("label", cat);
+ item.setAttribute("value", cat);
+ item.setAttribute("type", maxCount === null || maxCount > 1 ? "checkbox" : "radio");
+ if (itemCategories.includes(cat)) {
+ item.setAttribute("checked", "true");
+ }
+ let cssSafeId = cal.view.formatStringForCSSRule(cat);
+ item.style.setProperty("--item-color", `var(--category-${cssSafeId}-color)`);
+ categoryPopup.appendChild(item);
+ }
+
+ updateCategoryMenulist();
+}
+
+/**
+ * Updates the category menulist to show the correct label, depending on the
+ * selected categories in the category panel
+ */
+function updateCategoryMenulist() {
+ let categoryMenulist = document.getElementById("item-categories");
+ let categoryPopup = document.getElementById("item-categories-popup");
+
+ // Make sure the maximum number of categories is applied to the listbox
+ let calendar = getCurrentCalendar();
+ let maxCount = calendar.getProperty("capabilities.categories.maxCount");
+
+ // Hide the categories listbox and label in case categories are not
+ // supported
+ document.getElementById("event-grid-category-row").toggleAttribute("hidden", maxCount === 0);
+
+ let label;
+ let categoryList = categoryPopup.querySelectorAll("menuitem.calendar-category[checked]");
+ if (categoryList.length > 1) {
+ label = cal.l10n.getCalString("multipleCategories");
+ } else if (categoryList.length == 1) {
+ label = categoryList[0].getAttribute("label");
+ } else {
+ label = cal.l10n.getCalString("None");
+ }
+ categoryMenulist.setAttribute("label", label);
+
+ let labelBox = categoryMenulist.shadowRoot.querySelector("#label-box");
+ let labelLabel = labelBox.querySelector("#label");
+ for (let box of labelBox.querySelectorAll("box")) {
+ box.remove();
+ }
+ for (let i = 0; i < categoryList.length; i++) {
+ let box = labelBox.insertBefore(document.createXULElement("box"), labelLabel);
+ // Normal CSS selectors like :first-child don't work on shadow DOM items,
+ // so we have to set up something they do work on.
+ let parts = ["color"];
+ if (i == 0) {
+ parts.push("first");
+ }
+ if (i == categoryList.length - 1) {
+ parts.push("last");
+ }
+ box.setAttribute("part", parts.join(" "));
+ box.style.setProperty("--item-color", categoryList[i].style.getPropertyValue("--item-color"));
+ }
+}
+
+/**
+ * Updates the categories menulist label and decides if the popup should close
+ *
+ * @param aItem The popuphiding event
+ * @returns Whether the popup should close
+ */
+function categoryPopupHiding(event) {
+ updateCategoryMenulist();
+ let calendar = getCurrentCalendar();
+ let maxCount = calendar.getProperty("capabilities.categories.maxCount");
+ if (maxCount === null || maxCount > 1) {
+ return event.target.localName != "menuitem";
+ }
+ return true;
+}
+
+/**
+ * Prompts for a new category name, then adds it to the list
+ */
+function categoryTextboxKeypress(event) {
+ let category = event.target.value;
+ let categoryPopup = document.getElementById("item-categories-popup");
+ switch (event.key) {
+ case "Tab":
+ case "ArrowDown":
+ case "ArrowUp": {
+ event.target.blur();
+ event.preventDefault();
+
+ let keyCode = event.key == "ArrowUp" ? KeyboardEvent.DOM_VK_UP : KeyboardEvent.DOM_VK_DOWN;
+ categoryPopup.dispatchEvent(new KeyboardEvent("keydown", { keyCode }));
+ categoryPopup.dispatchEvent(new KeyboardEvent("keyup", { keyCode }));
+ return;
+ }
+ case "Escape":
+ if (category) {
+ event.target.value = "";
+ } else {
+ categoryPopup.hidePopup();
+ }
+ event.preventDefault();
+ return;
+ case "Enter":
+ category = category.trim();
+ if (category != "") {
+ break;
+ }
+ return;
+ default:
+ return;
+ }
+ event.preventDefault();
+
+ let categoryList = categoryPopup.querySelectorAll("menuitem.calendar-category");
+ let categories = Array.from(categoryList, cat => cat.getAttribute("value"));
+
+ let newIndex = categories.indexOf(category);
+ if (newIndex > -1) {
+ categoryList[newIndex].setAttribute("checked", true);
+ } else {
+ const localeCollator = new Intl.Collator();
+ let compare = localeCollator.compare;
+ newIndex = cal.data.binaryInsert(categories, category, compare, true);
+
+ let calendar = getCurrentCalendar();
+ let maxCount = calendar.getProperty("capabilities.categories.maxCount");
+
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("class", "menuitem-iconic calendar-category");
+ item.setAttribute("label", category);
+ item.setAttribute("value", category);
+ item.setAttribute("type", maxCount === null || maxCount > 1 ? "checkbox" : "radio");
+ item.setAttribute("checked", true);
+ categoryPopup.insertBefore(item, categoryList[newIndex]);
+ }
+
+ event.target.value = "";
+ // By pushing this to the end of the event loop, the other checked items in the list
+ // are cleared, where only one category is allowed.
+ setTimeout(updateCategoryMenulist, 0);
+}
+
+/**
+ * Saves the selected categories into the passed item
+ *
+ * @param aItem The item to set the categories on
+ */
+function saveCategories(aItem) {
+ let categoryPopup = document.getElementById("item-categories-popup");
+ let categoryList = Array.from(
+ categoryPopup.querySelectorAll("menuitem.calendar-category[checked]"),
+ cat => cat.getAttribute("label")
+ );
+ aItem.setCategories(categoryList);
+}
+
+/**
+ * Sets up all date related controls from the passed item
+ *
+ * @param item The item to parse information out of.
+ */
+function loadDateTime(item) {
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+ if (item.isEvent()) {
+ let startTime = item.startDate;
+ let endTime = item.endDate;
+ let duration = endTime.subtractDate(startTime);
+
+ // Check if an all-day event has been passed in (to adapt endDate).
+ if (startTime.isDate) {
+ startTime = startTime.clone();
+ endTime = endTime.clone();
+
+ endTime.day--;
+ duration.days--;
+ }
+
+ // store the start/end-times as calIDateTime-objects
+ // converted to the default timezone. store the timezones
+ // separately.
+ gStartTimezone = startTime.timezone;
+ gEndTimezone = endTime.timezone;
+ gStartTime = startTime.getInTimezone(kDefaultTimezone);
+ gEndTime = endTime.getInTimezone(kDefaultTimezone);
+ gItemDuration = duration;
+ }
+
+ if (item.isTodo()) {
+ let startTime = null;
+ let endTime = null;
+ let duration = null;
+
+ let hasEntryDate = item.entryDate != null;
+ if (hasEntryDate) {
+ startTime = item.entryDate;
+ gStartTimezone = startTime.timezone;
+ startTime = startTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gStartTimezone = kDefaultTimezone;
+ }
+ let hasDueDate = item.dueDate != null;
+ if (hasDueDate) {
+ endTime = item.dueDate;
+ gEndTimezone = endTime.timezone;
+ endTime = endTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gEndTimezone = kDefaultTimezone;
+ }
+ if (hasEntryDate && hasDueDate) {
+ duration = endTime.subtractDate(startTime);
+ }
+ document.getElementById("cmd_attendees").setAttribute("disabled", true);
+ document.getElementById("keepduration-button").disabled = !(hasEntryDate && hasDueDate);
+ sendMessage({
+ command: "updateConfigState",
+ argument: { attendeesCommand: false },
+ });
+ gStartTime = startTime;
+ gEndTime = endTime;
+ gItemDuration = duration;
+ } else {
+ sendMessage({
+ command: "updateConfigState",
+ argument: { attendeesCommand: true },
+ });
+ }
+}
+
+/**
+ * Toggles the "keep" attribute every time the keepduration-button is pressed.
+ */
+function toggleKeepDuration() {
+ let kdb = document.getElementById("keepduration-button");
+ let keepAttribute = kdb.getAttribute("keep") == "true";
+ // To make the "keep" attribute persistent, it mustn't be removed when in
+ // false state (bug 15232).
+ kdb.setAttribute("keep", keepAttribute ? "false" : "true");
+ document.getElementById("link-image-top").setAttribute("keep", !keepAttribute);
+}
+
+/**
+ * Handler function to be used when the Start time or End time of the event have
+ * changed.
+ * When changing the Start date, the End date changes automatically so the
+ * event/task's duration stays the same. Instead the End date is not linked
+ * to the Start date unless the the keepDurationButton has the "keep" attribute
+ * set to true. In this case modifying the End date changes the Start date in
+ * order to keep the same duration.
+ *
+ * @param aStartDatepicker If true the Start or Entry datepicker has changed,
+ * otherwise the End or Due datepicker has changed.
+ */
+function dateTimeControls2State(aStartDatepicker) {
+ if (gIgnoreUpdate) {
+ return;
+ }
+ let keepAttribute = document.getElementById("keepduration-button").getAttribute("keep") == "true";
+ let allDay = document.getElementById("event-all-day").checked;
+ let startWidgetId;
+ let endWidgetId;
+ if (window.calendarItem.isEvent()) {
+ startWidgetId = "event-starttime";
+ endWidgetId = "event-endtime";
+ } else {
+ if (!document.getElementById("todo-has-entrydate").checked) {
+ gItemDuration = null;
+ }
+ if (!document.getElementById("todo-has-duedate").checked) {
+ gItemDuration = null;
+ }
+ startWidgetId = "todo-entrydate";
+ endWidgetId = "todo-duedate";
+ }
+
+ let saveStartTime = gStartTime;
+ let saveEndTime = gEndTime;
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+
+ if (gStartTime) {
+ // jsDate is always in OS timezone, thus we create a calIDateTime
+ // object from the jsDate representation then we convert the timezone
+ // in order to keep gStartTime in default timezone.
+ if (gTimezonesEnabled || allDay) {
+ gStartTime = cal.dtz.jsDateToDateTime(
+ document.getElementById(startWidgetId).value,
+ gStartTimezone
+ );
+ gStartTime = gStartTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gStartTime = cal.dtz.jsDateToDateTime(
+ document.getElementById(startWidgetId).value,
+ kDefaultTimezone
+ );
+ }
+ gStartTime.isDate = allDay;
+ }
+ if (gEndTime) {
+ if (aStartDatepicker) {
+ // Change the End date in order to keep the duration.
+ gEndTime = gStartTime.clone();
+ if (gItemDuration) {
+ gEndTime.addDuration(gItemDuration);
+ }
+ } else {
+ let timezone = gEndTimezone;
+ if (timezone.isUTC) {
+ if (gStartTime && !cal.data.compareObjects(gStartTimezone, gEndTimezone)) {
+ timezone = gStartTimezone;
+ }
+ }
+ if (gTimezonesEnabled || allDay) {
+ gEndTime = cal.dtz.jsDateToDateTime(document.getElementById(endWidgetId).value, timezone);
+ gEndTime = gEndTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gEndTime = cal.dtz.jsDateToDateTime(
+ document.getElementById(endWidgetId).value,
+ kDefaultTimezone
+ );
+ }
+ gEndTime.isDate = allDay;
+ if (keepAttribute && gItemDuration) {
+ // Keepduration-button links the the Start to the End date. We
+ // have to change the Start date in order to keep the duration.
+ let fduration = gItemDuration.clone();
+ fduration.isNegative = true;
+ gStartTime = gEndTime.clone();
+ gStartTime.addDuration(fduration);
+ }
+ }
+ }
+
+ if (allDay) {
+ gStartTime.isDate = true;
+ gEndTime.isDate = true;
+ gItemDuration = gEndTime.subtractDate(gStartTime);
+ }
+
+ // calculate the new duration of start/end-time.
+ // don't allow for negative durations.
+ let warning = false;
+ let stringWarning = "";
+ if (!aStartDatepicker && gStartTime && gEndTime) {
+ if (gEndTime.compare(gStartTime) >= 0) {
+ gItemDuration = gEndTime.subtractDate(gStartTime);
+ } else {
+ gStartTime = saveStartTime;
+ gEndTime = saveEndTime;
+ warning = true;
+ stringWarning = cal.l10n.getCalString("warningEndBeforeStart");
+ }
+ }
+
+ let startChanged = false;
+ if (gStartTime && saveStartTime) {
+ startChanged = gStartTime.compare(saveStartTime) != 0;
+ }
+ // Preset the date in the until-datepicker's minimonth to the new start
+ // date if it has changed.
+ if (startChanged) {
+ let startDate = cal.dtz.dateTimeToJsDate(gStartTime.getInTimezone(cal.dtz.floating));
+ document.getElementById("repeat-until-datepicker").extraDate = startDate;
+ }
+
+ // Sort out and verify the until date if the start date has changed.
+ if (gUntilDate && startChanged) {
+ // Make the time part of the until date equal to the time of start date.
+ updateUntildateRecRule();
+
+ // Don't allow for until date earlier than the start date.
+ if (gUntilDate.compare(gStartTime) < 0) {
+ // We have to restore valid dates. Since the user has intentionally
+ // changed the start date, it looks reasonable to restore a valid
+ // until date equal to the start date.
+ gUntilDate = gStartTime.clone();
+ // Update the until-date-picker. In case of "custom" rule, the
+ // recurrence string is going to be changed by updateDateTime() below.
+ if (
+ !document.getElementById("repeat-untilDate").hidden &&
+ document.getElementById("repeat-details").hidden
+ ) {
+ document.getElementById("repeat-until-datepicker").value = cal.dtz.dateTimeToJsDate(
+ gUntilDate.getInTimezone(cal.dtz.floating)
+ );
+ }
+
+ warning = true;
+ stringWarning = cal.l10n.getCalString("warningUntilDateBeforeStart");
+ }
+ }
+
+ updateDateTime();
+ updateTimezone();
+ updateAccept();
+
+ if (warning) {
+ // Disable the "Save" and "Save and Close" commands as long as the
+ // warning dialog is showed.
+ enableAcceptCommand(false);
+ gWarning = true;
+ let callback = function () {
+ Services.prompt.alert(null, document.title, stringWarning);
+ gWarning = false;
+ updateAccept();
+ };
+ setTimeout(callback, 1);
+ }
+}
+
+/**
+ * Updates the entry date checkboxes, used for example when choosing an alarm:
+ * the entry date needs to be checked in that case.
+ */
+function updateEntryDate() {
+ updateDateCheckboxes("todo-entrydate", "todo-has-entrydate", {
+ isValid() {
+ return gStartTime != null;
+ },
+ setDateTime(date) {
+ gStartTime = date;
+ },
+ });
+}
+
+/**
+ * Updates the due date checkboxes.
+ */
+function updateDueDate() {
+ updateDateCheckboxes("todo-duedate", "todo-has-duedate", {
+ isValid() {
+ return gEndTime != null;
+ },
+ setDateTime(date) {
+ gEndTime = date;
+ },
+ });
+}
+
+/**
+ * Common function used by updateEntryDate and updateDueDate to set up the
+ * checkboxes correctly.
+ *
+ * @param aDatePickerId The XUL id of the datepicker to update.
+ * @param aCheckboxId The XUL id of the corresponding checkbox.
+ * @param aDateTime An object implementing the isValid and setDateTime
+ * methods. XXX explain.
+ */
+function updateDateCheckboxes(aDatePickerId, aCheckboxId, aDateTime) {
+ if (gIgnoreUpdate) {
+ return;
+ }
+
+ if (!window.calendarItem.isTodo()) {
+ return;
+ }
+
+ // force something to get set if there was nothing there before
+ aDatePickerId.value = document.getElementById(aDatePickerId).value;
+
+ // first of all disable the datetime picker if we don't have a date
+ let hasDate = document.getElementById(aCheckboxId).checked;
+ aDatePickerId.disabled = !hasDate;
+
+ // create a new datetime object if date is now checked for the first time
+ if (hasDate && !aDateTime.isValid()) {
+ let date = cal.dtz.jsDateToDateTime(
+ document.getElementById(aDatePickerId).value,
+ cal.dtz.defaultTimezone
+ );
+ aDateTime.setDateTime(date);
+ } else if (!hasDate && aDateTime.isValid()) {
+ aDateTime.setDateTime(null);
+ }
+
+ // calculate the duration if possible
+ let hasEntryDate = document.getElementById("todo-has-entrydate").checked;
+ let hasDueDate = document.getElementById("todo-has-duedate").checked;
+ if (hasEntryDate && hasDueDate) {
+ let start = cal.dtz.jsDateToDateTime(document.getElementById("todo-entrydate").value);
+ let end = cal.dtz.jsDateToDateTime(document.getElementById("todo-duedate").value);
+ gItemDuration = end.subtractDate(start);
+ } else {
+ gItemDuration = null;
+ }
+ document.getElementById("keepduration-button").disabled = !(hasEntryDate && hasDueDate);
+ updateDateTime();
+ updateTimezone();
+}
+
+/**
+ * Get the item's recurrence information for displaying in dialog controls.
+ *
+ * @param {object} aItem - The calendar item
+ * @returns {string[]} An array of two strings: [repeatType, untilDate]
+ */
+function getRepeatTypeAndUntilDate(aItem) {
+ let recurrenceInfo = window.recurrenceInfo;
+ let repeatType = "none";
+ let untilDate = "forever";
+
+ /**
+ * Updates the until date (locally and globally).
+ *
+ * @param aRule The recurrence rule
+ */
+ let updateUntilDate = aRule => {
+ if (!aRule.isByCount) {
+ if (aRule.isFinite) {
+ gUntilDate = aRule.untilDate.clone().getInTimezone(cal.dtz.defaultTimezone);
+ untilDate = cal.dtz.dateTimeToJsDate(gUntilDate.getInTimezone(cal.dtz.floating));
+ } else {
+ gUntilDate = null;
+ }
+ }
+ };
+
+ if (recurrenceInfo) {
+ repeatType = "custom";
+ let ritems = recurrenceInfo.getRecurrenceItems();
+ let rules = [];
+ let exceptions = [];
+ for (let ritem of ritems) {
+ if (ritem.isNegative) {
+ exceptions.push(ritem);
+ } else {
+ rules.push(ritem);
+ }
+ }
+ if (rules.length == 1) {
+ let rule = cal.wrapInstance(rules[0], Ci.calIRecurrenceRule);
+ if (rule) {
+ switch (rule.type) {
+ case "DAILY": {
+ let byparts = [
+ "BYSECOND",
+ "BYMINUTE",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS",
+ ];
+ if (!checkRecurrenceRule(rule, byparts)) {
+ let ruleComp = rule.getComponent("BYDAY");
+ if (rule.interval == 1) {
+ if (ruleComp.length > 0) {
+ if (ruleComp.length == 5) {
+ let found = false;
+ for (let i = 0; i < 5; i++) {
+ if (ruleComp[i] != i + 2) {
+ found = true;
+ break;
+ }
+ }
+ if (!found && (!rule.isFinite || !rule.isByCount)) {
+ repeatType = "every.weekday";
+ updateUntilDate(rule);
+ }
+ }
+ } else if (!rule.isFinite || !rule.isByCount) {
+ repeatType = "daily";
+ updateUntilDate(rule);
+ }
+ }
+ }
+ break;
+ }
+ case "WEEKLY": {
+ let byparts = [
+ "BYSECOND",
+ "BYMINUTE",
+ "BYDAY",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS",
+ ];
+ if (!checkRecurrenceRule(rule, byparts)) {
+ let weekType = ["weekly", "bi.weekly"];
+ if (
+ (rule.interval == 1 || rule.interval == 2) &&
+ (!rule.isFinite || !rule.isByCount)
+ ) {
+ repeatType = weekType[rule.interval - 1];
+ updateUntilDate(rule);
+ }
+ }
+ break;
+ }
+ case "MONTHLY": {
+ let byparts = [
+ "BYSECOND",
+ "BYMINUTE",
+ "BYDAY",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS",
+ ];
+ if (!checkRecurrenceRule(rule, byparts)) {
+ if (rule.interval == 1 && (!rule.isFinite || !rule.isByCount)) {
+ repeatType = "monthly";
+ updateUntilDate(rule);
+ }
+ }
+ break;
+ }
+ case "YEARLY": {
+ let byparts = [
+ "BYSECOND",
+ "BYMINUTE",
+ "BYDAY",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS",
+ ];
+ if (!checkRecurrenceRule(rule, byparts)) {
+ if (rule.interval == 1 && (!rule.isFinite || !rule.isByCount)) {
+ repeatType = "yearly";
+ updateUntilDate(rule);
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ return [repeatType, untilDate];
+}
+
+/**
+ * Updates the XUL UI with the repeat type and the until date.
+ *
+ * @param {string} aRepeatType - The type of repeat
+ * @param {string} aUntilDate - The until date
+ * @param {object} aItem - The calendar item
+ */
+function loadRepeat(aRepeatType, aUntilDate, aItem) {
+ document.getElementById("item-repeat").value = aRepeatType;
+ let repeatMenu = document.getElementById("item-repeat");
+ gLastRepeatSelection = repeatMenu.selectedIndex;
+
+ if (aItem.parentItem != aItem) {
+ document.getElementById("item-repeat").setAttribute("disabled", "true");
+ document.getElementById("repeat-until-datepicker").setAttribute("disabled", "true");
+ }
+ // Show the repeat-until-datepicker and set its date
+ document.getElementById("repeat-untilDate").hidden = false;
+ document.getElementById("repeat-details").hidden = true;
+ document.getElementById("repeat-until-datepicker").value = aUntilDate;
+}
+
+/**
+ * Update reminder related elements on the dialog.
+ *
+ * @param aSuppressDialogs If true, controls are updated without prompting
+ * for changes with the custom dialog
+ */
+function updateReminder(aSuppressDialogs) {
+ window.gLastAlarmSelection = commonUpdateReminder(
+ document.querySelector(".item-alarm"),
+ window.calendarItem,
+ window.gLastAlarmSelection,
+ getCurrentCalendar(),
+ document.querySelector(".reminder-details"),
+ window.gStartTimezone || window.gEndTimezone,
+ aSuppressDialogs
+ );
+ updateAccept();
+}
+
+/**
+ * Saves all values the user chose on the dialog to the passed item
+ *
+ * @param item The item to save to.
+ */
+function saveDialog(item) {
+ // Calendar
+ item.calendar = getCurrentCalendar();
+
+ cal.item.setItemProperty(item, "title", document.getElementById("item-title").value);
+ cal.item.setItemProperty(item, "LOCATION", document.getElementById("item-location").value);
+
+ saveDateTime(item);
+
+ if (item.isTodo()) {
+ let percentCompleteInteger = 0;
+ if (document.getElementById("percent-complete-textbox").value != "") {
+ percentCompleteInteger = parseInt(
+ document.getElementById("percent-complete-textbox").value,
+ 10
+ );
+ }
+ if (percentCompleteInteger < 0) {
+ percentCompleteInteger = 0;
+ } else if (percentCompleteInteger > 100) {
+ percentCompleteInteger = 100;
+ }
+ cal.item.setItemProperty(item, "PERCENT-COMPLETE", percentCompleteInteger);
+ }
+
+ // Categories
+ saveCategories(item);
+
+ // Attachment
+ // We want the attachments to be up to date, remove all first.
+ item.removeAllAttachments();
+
+ // Now add back the new ones
+ for (let hashId in gAttachMap) {
+ let att = gAttachMap[hashId];
+ item.addAttachment(att);
+ }
+
+ // Description
+ let editorElement = document.getElementById("item-description");
+ let editor = editorElement.getHTMLEditor(editorElement.contentWindow);
+ if (editor.documentModified) {
+ // Get editor output as HTML. We request raw output to avoid any
+ // pretty-printing which may cause issues with Google Calendar (see comments
+ // in calViewUtils.fixGoogleCalendarDescription() for more information).
+ let mode =
+ Ci.nsIDocumentEncoder.OutputRaw |
+ Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
+ Ci.nsIDocumentEncoder.OutputBodyOnly;
+
+ const editorOutput = editor.outputToString("text/html", mode);
+
+ // The editor gives us output wrapped in a body tag. We don't really want
+ // that, so strip it. (Yes, it's a regex with HTML, but a _very_ specific
+ // one.) We use the `s` flag to match across newlines in case there's a
+ // <pre/> tag, in which case <br/> will not be inserted.
+ item.descriptionHTML = editorOutput.replace(/^<body>(.+)<\/body>$/s, "$1");
+ }
+
+ // Event Status
+ if (item.isEvent()) {
+ if (gConfig.status && gConfig.status != "NONE") {
+ item.setProperty("STATUS", gConfig.status);
+ } else {
+ item.deleteProperty("STATUS");
+ }
+ } else {
+ let status = document.getElementById("todo-status").value;
+ if (status != "COMPLETED") {
+ item.completedDate = null;
+ }
+ cal.item.setItemProperty(item, "STATUS", status == "NONE" ? null : status);
+ }
+
+ // set the "PRIORITY" property if a valid priority has been
+ // specified (any integer value except *null*) OR the item
+ // already specifies a priority. in any other case we don't
+ // need this property and can safely delete it. we need this special
+ // handling since the WCAP provider always includes the priority
+ // with value *null* and we don't detect changes to this item if
+ // we delete this property.
+ if (capSupported("priority") && (gConfig.priority || item.hasProperty("PRIORITY"))) {
+ item.setProperty("PRIORITY", gConfig.priority);
+ } else {
+ item.deleteProperty("PRIORITY");
+ }
+
+ // Transparency
+ if (gConfig.showTimeAs) {
+ item.setProperty("TRANSP", gConfig.showTimeAs);
+ } else {
+ item.deleteProperty("TRANSP");
+ }
+
+ // Privacy
+ cal.item.setItemProperty(item, "CLASS", gConfig.privacy, "privacy");
+
+ if (item.status == "COMPLETED" && item.isTodo()) {
+ let elementValue = document.getElementById("completed-date-picker").value;
+ item.completedDate = cal.dtz.jsDateToDateTime(elementValue);
+ }
+
+ saveReminder(item, getCurrentCalendar(), document.querySelector(".item-alarm"));
+}
+
+/**
+ * Save date and time related values from the dialog to the passed item.
+ *
+ * @param item The item to save to.
+ */
+function saveDateTime(item) {
+ // Changes to the start date don't have to change the until date.
+ untilDateCompensation(item);
+
+ if (item.isEvent()) {
+ let startTime = gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime.getInTimezone(gEndTimezone);
+ let isAllDay = document.getElementById("event-all-day").checked;
+ if (isAllDay) {
+ startTime = startTime.clone();
+ endTime = endTime.clone();
+ startTime.isDate = true;
+ endTime.isDate = true;
+ endTime.day += 1;
+ } else {
+ startTime = startTime.clone();
+ startTime.isDate = false;
+ endTime = endTime.clone();
+ endTime.isDate = false;
+ }
+ cal.item.setItemProperty(item, "startDate", startTime);
+ cal.item.setItemProperty(item, "endDate", endTime);
+ }
+ if (item.isTodo()) {
+ let startTime = gStartTime && gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime && gEndTime.getInTimezone(gEndTimezone);
+ cal.item.setItemProperty(item, "entryDate", startTime);
+ cal.item.setItemProperty(item, "dueDate", endTime);
+ }
+}
+
+/**
+ * Changes the until date in the rule in order to compensate the automatic
+ * correction caused by the function onStartDateChange() when saving the
+ * item.
+ * It allows to keep the until date set in the dialog irrespective of the
+ * changes that the user has done to the start date.
+ */
+function untilDateCompensation(aItem) {
+ // The current start date in the item is always the date that we get
+ // when opening the dialog or after the last save.
+ let startDate = aItem[cal.dtz.startDateProp(aItem)];
+
+ if (aItem.recurrenceInfo) {
+ let rrules = splitRecurrenceRules(aItem.recurrenceInfo);
+ let rule = rrules[0][0];
+ if (!rule.isByCount && rule.isFinite && startDate) {
+ let compensation = startDate.subtractDate(gStartTime);
+ if (compensation != "PT0S") {
+ let untilDate = rule.untilDate.clone();
+ untilDate.addDuration(compensation);
+ rule.untilDate = untilDate;
+ }
+ }
+ }
+}
+
+/**
+ * Updates the dialog title based on item type and if the item is new or to be
+ * modified.
+ */
+function updateTitle() {
+ let strName;
+ if (window.calendarItem.isEvent()) {
+ strName = window.mode == "new" ? "newEventDialog" : "editEventDialog";
+ } else if (window.calendarItem.isTodo()) {
+ strName = window.mode == "new" ? "newTaskDialog" : "editTaskDialog";
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+ sendMessage({
+ command: "updateTitle",
+ prefix: cal.l10n.getCalString(strName),
+ title: document.getElementById("item-title").value,
+ });
+}
+
+/**
+ * Update the disabled status of the accept button. The button is enabled if all
+ * parts of the dialog have options selected that make sense.
+ * constraining factors like
+ */
+function updateAccept() {
+ let enableAccept = true;
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+ let startDate;
+ let endDate;
+ let isEvent = window.calendarItem.isEvent();
+
+ // don't allow for end dates to be before start dates
+ if (isEvent) {
+ startDate = cal.dtz.jsDateToDateTime(document.getElementById("event-starttime").value);
+ endDate = cal.dtz.jsDateToDateTime(document.getElementById("event-endtime").value);
+ } else {
+ startDate = document.getElementById("todo-has-entrydate").checked
+ ? cal.dtz.jsDateToDateTime(document.getElementById("todo-entrydate").value)
+ : null;
+ endDate = document.getElementById("todo-has-duedate").checked
+ ? cal.dtz.jsDateToDateTime(document.getElementById("todo-duedate").value)
+ : null;
+ }
+
+ if (startDate && endDate) {
+ if (gTimezonesEnabled) {
+ let startTimezone = gStartTimezone;
+ let endTimezone = gEndTimezone;
+ if (endTimezone.isUTC) {
+ if (!cal.data.compareObjects(gStartTimezone, gEndTimezone)) {
+ endTimezone = gStartTimezone;
+ }
+ }
+
+ startDate = startDate.getInTimezone(kDefaultTimezone);
+ endDate = endDate.getInTimezone(kDefaultTimezone);
+
+ startDate.timezone = startTimezone;
+ endDate.timezone = endTimezone;
+ }
+
+ startDate = startDate.getInTimezone(kDefaultTimezone);
+ endDate = endDate.getInTimezone(kDefaultTimezone);
+
+ // For all-day events we are not interested in times and compare only
+ // dates.
+ if (isEvent && document.getElementById("event-all-day").checked) {
+ // jsDateToDateTime returns the values in UTC. Depending on the
+ // local timezone and the values selected in datetimepicker the date
+ // in UTC might be shifted to the previous or next day.
+ // For example: The user (with local timezone GMT+05) selected
+ // Feb 10 2006 00:00:00. The corresponding value in UTC is
+ // Feb 09 2006 19:00:00. If we now set isDate to true we end up with
+ // a date of Feb 09 2006 instead of Feb 10 2006 resulting in errors
+ // during the following comparison.
+ // Calling getInTimezone() ensures that we use the same dates as
+ // displayed to the user in datetimepicker for comparison.
+ startDate.isDate = true;
+ endDate.isDate = true;
+ }
+ }
+
+ if (endDate && startDate && endDate.compare(startDate) == -1) {
+ enableAccept = false;
+ }
+
+ enableAcceptCommand(enableAccept);
+
+ return enableAccept;
+}
+
+/**
+ * Enables/disables the commands cmd_accept and cmd_save related to the
+ * save operation.
+ *
+ * @param aEnable true: enables the command
+ */
+function enableAcceptCommand(aEnable) {
+ sendMessage({ command: "enableAcceptCommand", argument: aEnable });
+}
+
+// Global variables used to restore start and end date-time when changing the
+// "all day" status in the onUpdateAllday() function.
+var gOldStartTime = null;
+var gOldEndTime = null;
+var gOldStartTimezone = null;
+var gOldEndTimezone = null;
+
+/**
+ * Handler function to update controls and state in consequence of the "all
+ * day" checkbox being clicked.
+ */
+function onUpdateAllDay() {
+ if (!window.calendarItem.isEvent()) {
+ return;
+ }
+ let allDay = document.getElementById("event-all-day").checked;
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+
+ if (allDay) {
+ // Store date-times and related timezones so we can restore
+ // if the user unchecks the "all day" checkbox.
+ gOldStartTime = gStartTime.clone();
+ gOldEndTime = gEndTime.clone();
+ gOldStartTimezone = gStartTimezone;
+ gOldEndTimezone = gEndTimezone;
+ // When events that end at 0:00 become all-day events, we need to
+ // subtract a day from the end date because the real end is midnight.
+ if (gEndTime.hour == 0 && gEndTime.minute == 0) {
+ let tempStartTime = gStartTime.clone();
+ let tempEndTime = gEndTime.clone();
+ tempStartTime.isDate = true;
+ tempEndTime.isDate = true;
+ tempStartTime.day++;
+ if (tempEndTime.compare(tempStartTime) >= 0) {
+ gEndTime.day--;
+ }
+ }
+ } else {
+ gStartTime.isDate = false;
+ gEndTime.isDate = false;
+ if (!gOldStartTime && !gOldEndTime) {
+ // The checkbox has been unchecked for the first time, the event
+ // was an "All day" type, so we have to set default values.
+ gStartTime.hour = cal.dtz.getDefaultStartDate(window.initialStartDateValue).hour;
+ gEndTime.hour = gStartTime.hour;
+ gEndTime.minute += Services.prefs.getIntPref("calendar.event.defaultlength", 60);
+ gOldStartTimezone = kDefaultTimezone;
+ gOldEndTimezone = kDefaultTimezone;
+ } else {
+ // Restore date-times previously stored.
+ gStartTime.hour = gOldStartTime.hour;
+ gStartTime.minute = gOldStartTime.minute;
+ gEndTime.hour = gOldEndTime.hour;
+ gEndTime.minute = gOldEndTime.minute;
+ // When we restore 0:00 as end time, we need to add one day to
+ // the end date in order to include the last day until midnight.
+ if (gEndTime.hour == 0 && gEndTime.minute == 0) {
+ gEndTime.day++;
+ }
+ }
+ }
+ gStartTimezone = allDay ? cal.dtz.floating : gOldStartTimezone;
+ gEndTimezone = allDay ? cal.dtz.floating : gOldEndTimezone;
+ setShowTimeAs(allDay);
+
+ updateAllDay();
+}
+
+/**
+ * This function sets the enabled/disabled state of the following controls:
+ * - 'event-starttime'
+ * - 'event-endtime'
+ * - 'timezone-starttime'
+ * - 'timezone-endtime'
+ * the state depends on whether or not the event is configured as 'all-day' or not.
+ */
+function updateAllDay() {
+ if (gIgnoreUpdate) {
+ return;
+ }
+
+ if (!window.calendarItem.isEvent()) {
+ return;
+ }
+
+ let allDay = document.getElementById("event-all-day").checked;
+ if (allDay) {
+ document.getElementById("event-starttime").setAttribute("timepickerdisabled", true);
+ document.getElementById("event-endtime").setAttribute("timepickerdisabled", true);
+ } else {
+ document.getElementById("event-starttime").removeAttribute("timepickerdisabled");
+ document.getElementById("event-endtime").removeAttribute("timepickerdisabled");
+ }
+
+ gStartTime.isDate = allDay;
+ gEndTime.isDate = allDay;
+ gItemDuration = gEndTime.subtractDate(gStartTime);
+
+ updateDateTime();
+ updateUntildateRecRule();
+ updateRepeatDetails();
+ updateAccept();
+}
+
+/**
+ * Use the window arguments to cause the opener to create a new event on the
+ * item's calendar
+ */
+function openNewEvent() {
+ let item = window.calendarItem;
+ let args = window.arguments[0];
+ args.onNewEvent(item.calendar);
+}
+
+/**
+ * Use the window arguments to cause the opener to create a new event on the
+ * item's calendar
+ */
+function openNewTask() {
+ let item = window.calendarItem;
+ let args = window.arguments[0];
+ args.onNewTodo(item.calendar);
+}
+
+/**
+ * Update the transparency status of this dialog, depending on if the event
+ * is all-day or not.
+ *
+ * @param allDay If true, the event is all-day
+ */
+function setShowTimeAs(allDay) {
+ gConfig.showTimeAs = cal.item.getEventDefaultTransparency(allDay);
+ updateConfigState({ showTimeAs: gConfig.showTimeAs });
+}
+
+function editAttendees() {
+ let savedWindow = window;
+ let calendar = getCurrentCalendar();
+
+ let callback = function (attendees, organizer, startTime, endTime) {
+ savedWindow.attendees = attendees;
+ savedWindow.organizer = organizer;
+
+ // if a participant was added or removed we switch to the attendee
+ // tab, so the user can see the change directly
+ let tabs = document.getElementById("event-grid-tabs");
+ let attendeeTab = document.getElementById("event-grid-tab-attendees");
+ tabs.selectedItem = attendeeTab;
+
+ let duration = endTime.subtractDate(startTime);
+ startTime = startTime.clone();
+ endTime = endTime.clone();
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+ gStartTimezone = startTime.timezone;
+ gEndTimezone = endTime.timezone;
+ gStartTime = startTime.getInTimezone(kDefaultTimezone);
+ gEndTime = endTime.getInTimezone(kDefaultTimezone);
+ gItemDuration = duration;
+ updateAttendeeInterface();
+ updateDateTime();
+ updateAllDay();
+
+ if (isAllDay != gStartTime.isDate) {
+ setShowTimeAs(gStartTime.isDate);
+ }
+ };
+
+ let startTime = gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime.getInTimezone(gEndTimezone);
+
+ let isAllDay = document.getElementById("event-all-day").checked;
+ if (isAllDay) {
+ startTime.isDate = true;
+ endTime.isDate = true;
+ endTime.day += 1;
+ } else {
+ startTime.isDate = false;
+ endTime.isDate = false;
+ }
+ let args = {};
+ args.startTime = startTime;
+ args.endTime = endTime;
+ args.displayTimezone = gTimezonesEnabled;
+ args.attendees = window.attendees;
+ args.organizer = window.organizer && window.organizer.clone();
+ args.calendar = calendar;
+ args.item = window.calendarItem;
+ args.onOk = callback;
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-attendees.xhtml",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args
+ );
+}
+
+/**
+ * Updates the UI outside of the iframe (toolbar, menu, statusbar, etc.)
+ * for changes in priority, privacy, status, showTimeAs/transparency,
+ * and/or other properties. This function should be called any time that
+ * gConfig.privacy, gConfig.priority, etc. are updated.
+ *
+ * Privacy and priority updates depend on the selected calendar. If the
+ * selected calendar does not support them, or only supports certain
+ * values, these are removed from the UI.
+ *
+ * @param {object} aArg - Container
+ * @param {string} aArg.privacy - (optional) The new privacy value
+ * @param {short} aArg.priority - (optional) The new priority value
+ * @param {string} aArg.status - (optional) The new status value
+ * @param {string} aArg.showTimeAs - (optional) The new transparency value
+ */
+function updateConfigState(aArg) {
+ // We include additional info for priority and privacy.
+ if (aArg.hasOwnProperty("priority")) {
+ aArg.hasPriority = capSupported("priority");
+ }
+ if (aArg.hasOwnProperty("privacy")) {
+ Object.assign(aArg, {
+ hasPrivacy: capSupported("privacy"),
+ calendarType: getCurrentCalendar().type,
+ privacyValues: capValues("privacy", ["PUBLIC", "CONFIDENTIAL", "PRIVATE"]),
+ });
+ }
+
+ // For tasks, do not include showTimeAs
+ if (aArg.hasOwnProperty("showTimeAs") && window.calendarItem.isTodo()) {
+ delete aArg.showTimeAs;
+ if (Object.keys(aArg).length == 0) {
+ return;
+ }
+ }
+
+ sendMessage({ command: "updateConfigState", argument: aArg });
+}
+
+/**
+ * Add menu items to the UI for attaching files using cloud providers.
+ */
+function loadCloudProviders() {
+ let cloudFileEnabled = Services.prefs.getBoolPref("mail.cloud_files.enabled", false);
+ let cmd = document.getElementById("cmd_attach_cloud");
+ let message = {
+ command: "setElementAttribute",
+ argument: { id: "cmd_attach_cloud", attribute: "hidden", value: null },
+ };
+
+ if (!cloudFileEnabled) {
+ // If cloud file support is disabled, just hide the attach item
+ cmd.hidden = true;
+ message.argument.value = true;
+ sendMessage(message);
+ return;
+ }
+
+ let isHidden = cloudFileAccounts.configuredAccounts.length == 0;
+ cmd.hidden = isHidden;
+ message.argument.value = isHidden;
+ sendMessage(message);
+
+ let itemObjects = [];
+
+ for (let cloudProvider of cloudFileAccounts.configuredAccounts) {
+ // Create a serializable object to pass in a message outside the iframe
+ let itemObject = {};
+ itemObject.displayName = cloudFileAccounts.getDisplayName(cloudProvider);
+ itemObject.label = cal.l10n.getString("calendar-event-dialog", "attachViaFilelink", [
+ itemObject.displayName,
+ ]);
+ itemObject.cloudProviderAccountKey = cloudProvider.accountKey;
+ if (cloudProvider.iconURL) {
+ itemObject.class = "menuitem-iconic";
+ itemObject.image = cloudProvider.iconURL;
+ }
+
+ itemObjects.push(itemObject);
+
+ // Create a menu item from the serializable object
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("label", itemObject.label);
+ item.setAttribute("observes", "cmd_attach_cloud");
+ item.setAttribute(
+ "oncommand",
+ "attachFile(event.target.cloudProvider); event.stopPropagation();"
+ );
+
+ if (itemObject.class) {
+ item.setAttribute("class", itemObject.class);
+ item.setAttribute("image", itemObject.image);
+ }
+
+ // Add the menu item to places inside the iframe where we advertise cloud providers
+ let attachmentPopup = document.getElementById("attachment-popup");
+ attachmentPopup.appendChild(item).cloudProvider = cloudProvider;
+ }
+
+ // Add the items to places outside the iframe where we advertise cloud providers
+ sendMessage({ command: "loadCloudProviders", items: itemObjects });
+}
+
+/**
+ * Prompts the user to attach an url to this item.
+ */
+function attachURL() {
+ if (Services.prompt) {
+ // ghost in an example...
+ let result = { value: "http://" };
+ let confirm = Services.prompt.prompt(
+ window,
+ cal.l10n.getString("calendar-event-dialog", "specifyLinkLocation"),
+ cal.l10n.getString("calendar-event-dialog", "enterLinkLocation"),
+ result,
+ null,
+ { value: 0 }
+ );
+
+ if (confirm) {
+ try {
+ // If something bogus was entered, Services.io.newURI may fail.
+ let attachment = new CalAttachment();
+ attachment.uri = Services.io.newURI(result.value);
+ addAttachment(attachment);
+ // we switch to the attachment tab if it is not already displayed
+ // to allow the user to see the attachment was added
+ let tabs = document.getElementById("event-grid-tabs");
+ let attachTab = document.getElementById("event-grid-tab-attachments");
+ tabs.selectedItem = attachTab;
+ } catch (e) {
+ // TODO We might want to show a warning instead of just not
+ // adding the file
+ }
+ }
+ }
+}
+
+/**
+ * Attach a file using a cloud provider, identified by its accountKey.
+ *
+ * @param {string} aAccountKey - The accountKey for a cloud provider
+ */
+function attachFileByAccountKey(aAccountKey) {
+ for (let cloudProvider of cloudFileAccounts.configuredAccounts) {
+ if (aAccountKey == cloudProvider.accountKey) {
+ attachFile(cloudProvider);
+ return;
+ }
+ }
+}
+
+/**
+ * Attach a file to the item. Not passing a cloud provider is currently unsupported.
+ *
+ * @param cloudProvider If set, the cloud provider will be used for attaching
+ */
+function attachFile(cloudProvider) {
+ if (!cloudProvider) {
+ cal.ERROR(
+ "[calendar-event-dialog] Could not attach file without cloud provider" + cal.STACK(10)
+ );
+ }
+
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ filePicker.init(
+ window,
+ cal.l10n.getString("calendar-event-dialog", "selectAFile"),
+ Ci.nsIFilePicker.modeOpenMultiple
+ );
+
+ // Check for the last directory
+ let lastDir = lastDirectory();
+ if (lastDir) {
+ filePicker.displayDirectory = lastDir;
+ }
+
+ filePicker.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !filePicker.files) {
+ return;
+ }
+
+ // Create the attachment
+ for (let file of filePicker.files) {
+ let fileHandler = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ let uriSpec = fileHandler.getURLSpecFromActualFile(file);
+
+ if (!(uriSpec in gAttachMap)) {
+ // If the attachment hasn't been added, then set the last display
+ // directory.
+ lastDirectory(uriSpec);
+
+ // ... and add the attachment.
+ let attachment = new CalAttachment();
+ if (cloudProvider) {
+ attachment.uri = Services.io.newURI(uriSpec);
+ } else {
+ // TODO read file into attachment
+ }
+ addAttachment(attachment, cloudProvider);
+ }
+ }
+ });
+}
+
+/**
+ * Helper function to remember the last directory chosen when attaching files.
+ *
+ * @param aFileUri (optional) If passed, the last directory will be set and
+ * returned. If null, the last chosen directory
+ * will be returned.
+ * @returns The last directory that was set with this function.
+ */
+function lastDirectory(aFileUri) {
+ if (aFileUri) {
+ // Act similar to a setter, save the passed uri.
+ let uri = Services.io.newURI(aFileUri);
+ let file = uri.QueryInterface(Ci.nsIFileURL).file;
+ lastDirectory.mValue = file.parent.QueryInterface(Ci.nsIFile);
+ }
+
+ // In any case, return the value
+ return lastDirectory.mValue === undefined ? null : lastDirectory.mValue;
+}
+
+/**
+ * Turns an url into a string that can be used in UI.
+ * - For a file:// url, shows the filename.
+ * - For a http:// url, removes protocol and trailing slash
+ *
+ * @param aUri The uri to parse.
+ * @returns A string that can be used in UI.
+ */
+function makePrettyName(aUri) {
+ let name = aUri.spec;
+
+ if (aUri.schemeIs("file")) {
+ name = aUri.spec.split("/").pop();
+ } else if (aUri.schemeIs("http")) {
+ name = aUri.spec.replace(/\/$/, "").replace(/^http:\/\//, "");
+ }
+ return name;
+}
+
+/**
+ * Asynchronously uploads the given attachment to the cloud provider, updating
+ * the passed listItem as things progress.
+ *
+ * @param attachment A calIAttachment to upload.
+ * @param cloudFileAccount The cloud file account used for uploading.
+ * @param listItem The listitem in attachment-link listbox to update.
+ */
+function uploadCloudAttachment(attachment, cloudFileAccount, listItem) {
+ let file = attachment.uri.QueryInterface(Ci.nsIFileURL).file;
+ let image = listItem.querySelector("img");
+ listItem.attachCloudFileAccount = cloudFileAccount;
+ image.setAttribute("src", "chrome://global/skin/icons/loading.png");
+ // WebExtension APIs do not support calendar tabs.
+ cloudFileAccount.uploadFile(null, file, attachment.name).then(
+ upload => {
+ delete gAttachMap[attachment.hashId];
+ attachment.uri = Services.io.newURI(upload.url);
+ attachment.setParameter("FILENAME", file.leafName);
+ attachment.setParameter("X-SERVICE-ICONURL", upload.serviceIcon);
+ listItem.setAttribute("label", file.leafName);
+ gAttachMap[attachment.hashId] = attachment;
+ image.setAttribute("src", upload.serviceIcon);
+ listItem.attachCloudFileUpload = upload;
+ updateAttachment();
+ },
+ statusCode => {
+ cal.ERROR(
+ "[calendar-event-dialog] Uploading cloud attachment failed. Status code: " +
+ statusCode.result
+ );
+
+ // Uploading failed. First of all, show an error icon. Also,
+ // delete it from the attach map now, this will make sure it is
+ // not serialized if the user saves.
+ image.setAttribute("src", "chrome://messenger/skin/icons/error.png");
+ delete gAttachMap[attachment.hashId];
+
+ // Keep the item for a while so the user can see something failed.
+ // When we have a nice notification bar, we can show more info
+ // about the failure.
+ setTimeout(() => {
+ listItem.remove();
+ updateAttachment();
+ }, 5000);
+ }
+ );
+}
+
+/**
+ * Adds the given attachment to dialog controls.
+ *
+ * @param attachment The calIAttachment object to add
+ * @param cloudFileAccount (optional) If set, the given cloud file account will be used.
+ */
+function addAttachment(attachment, cloudFileAccount) {
+ if (!attachment || !attachment.hashId || attachment.hashId in gAttachMap) {
+ return;
+ }
+
+ // We currently only support uri attachments
+ if (attachment.uri) {
+ let documentLink = document.getElementById("attachment-link");
+ let listItem = document.createXULElement("richlistitem");
+ let image = document.createElement("img");
+ image.setAttribute("alt", "");
+ image.width = "24";
+ image.height = "24";
+ // Allow the moz-icon src to be invalid.
+ image.classList.add("invisible-on-broken");
+ listItem.appendChild(image);
+ let label = document.createXULElement("label");
+ label.setAttribute("value", makePrettyName(attachment.uri));
+ label.setAttribute("crop", "end");
+ listItem.appendChild(label);
+ listItem.setAttribute("tooltiptext", attachment.uri.spec);
+ if (cloudFileAccount) {
+ if (attachment.uri.schemeIs("file")) {
+ // Its still a local url, needs to be uploaded
+ image.setAttribute("src", "chrome://messenger/skin/icons/connecting.png");
+ uploadCloudAttachment(attachment, cloudFileAccount, listItem);
+ } else {
+ let cloudFileIconURL = attachment.getParameter("X-SERVICE-ICONURL");
+ image.setAttribute("src", cloudFileIconURL);
+ let leafName = attachment.getParameter("FILENAME");
+ if (leafName) {
+ listItem.setAttribute("label", leafName);
+ }
+ }
+ } else if (attachment.uri.schemeIs("file")) {
+ image.setAttribute("src", "moz-icon://" + attachment.uri.spec);
+ } else {
+ let leafName = attachment.getParameter("FILENAME");
+ let cloudFileIconURL = attachment.getParameter("X-SERVICE-ICONURL");
+ let cloudFileEnabled = Services.prefs.getBoolPref("mail.cloud_files.enabled", false);
+
+ if (leafName) {
+ // TODO security issues?
+ listItem.setAttribute("label", leafName);
+ }
+ if (cloudFileIconURL && cloudFileEnabled) {
+ image.setAttribute("src", cloudFileIconURL);
+ } else {
+ let iconSrc = attachment.uri.spec.length ? attachment.uri.spec : "dummy.html";
+ if (attachment.formatType) {
+ iconSrc = "goat?contentType=" + attachment.formatType;
+ } else {
+ // let's try to auto-detect
+ let parts = iconSrc.substr(attachment.uri.scheme.length + 2).split("/");
+ if (parts.length) {
+ iconSrc = parts[parts.length - 1];
+ }
+ }
+ image.setAttribute("src", "moz-icon://" + iconSrc);
+ }
+ }
+
+ // Now that everything is set up, add it to the attachment box.
+ documentLink.appendChild(listItem);
+
+ // full attachment object is stored here
+ listItem.attachment = attachment;
+
+ // Update the number of rows and save our attachment globally
+ documentLink.rows = documentLink.getRowCount();
+ }
+
+ gAttachMap[attachment.hashId] = attachment;
+ updateAttachment();
+}
+
+/**
+ * Removes the currently selected attachment from the dialog controls.
+ *
+ * XXX This could use a dialog maybe?
+ */
+function deleteAttachment() {
+ let documentLink = document.getElementById("attachment-link");
+ let item = documentLink.selectedItem;
+ delete gAttachMap[item.attachment.hashId];
+
+ if (item.attachCloudFileAccount && item.attachCloudFileUpload) {
+ try {
+ // WebExtension APIs do not support calendar tabs.
+ item.attachCloudFileAccount
+ .deleteFile(null, item.attachCloudFileUpload.id)
+ .catch(statusCode => {
+ // TODO With a notification bar, we could actually show this error.
+ cal.ERROR(
+ "[calendar-event-dialog] Deleting cloud attachment " +
+ "failed, file will remain on server. " +
+ " Status code: " +
+ statusCode
+ );
+ });
+ } catch (e) {
+ cal.ERROR(
+ "[calendar-event-dialog] Deleting cloud attachment " +
+ "failed, file will remain on server. " +
+ "Exception: " +
+ e
+ );
+ }
+ }
+ item.remove();
+
+ updateAttachment();
+}
+
+/**
+ * Removes all attachments from the dialog controls.
+ */
+function deleteAllAttachments() {
+ let documentLink = document.getElementById("attachment-link");
+ let itemCount = documentLink.getRowCount();
+ let canRemove = itemCount < 2;
+
+ if (itemCount > 1) {
+ let removeText = PluralForm.get(
+ itemCount,
+ cal.l10n.getString("calendar-event-dialog", "removeAttachmentsText")
+ );
+ let removeTitle = cal.l10n.getString("calendar-event-dialog", "removeCalendarsTitle");
+ canRemove = Services.prompt.confirm(
+ window,
+ removeTitle,
+ removeText.replace("#1", itemCount),
+ {}
+ );
+ }
+
+ if (canRemove) {
+ while (documentLink.lastChild) {
+ documentLink.lastChild.attachment = null;
+ documentLink.lastChild.remove();
+ }
+ gAttachMap = {};
+ }
+ updateAttachment();
+}
+
+/**
+ * Opens the selected attachment using the external protocol service.
+ *
+ * @see nsIExternalProtocolService
+ */
+function openAttachment() {
+ // Only one file has to be selected and we don't handle base64 files at all
+ let documentLink = document.getElementById("attachment-link");
+ if (documentLink.selectedItem) {
+ let attURI = documentLink.selectedItem.attachment.uri;
+ let externalLoader = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(
+ Ci.nsIExternalProtocolService
+ );
+ // TODO There should be a nicer dialog
+ externalLoader.loadURI(attURI);
+ }
+}
+
+/**
+ * Copies the link location of the first selected attachment to the clipboard
+ */
+function copyAttachment() {
+ let documentLink = document.getElementById("attachment-link");
+ if (documentLink.selectedItem) {
+ let attURI = documentLink.selectedItem.attachment.uri.spec;
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(attURI);
+ }
+}
+
+/**
+ * Handler function to handle pressing keys in the attachment listbox.
+ *
+ * @param aEvent The DOM event caused by the key press.
+ */
+function attachmentLinkKeyPress(aEvent) {
+ switch (aEvent.key) {
+ case "Backspace":
+ case "Delete":
+ deleteAttachment();
+ break;
+ case "Enter":
+ openAttachment();
+ aEvent.preventDefault();
+ break;
+ }
+}
+
+/**
+ * Handler function to take care of double clicking on an attachment
+ *
+ * @param aEvent The DOM event caused by the clicking.
+ */
+function attachmentDblClick(aEvent) {
+ let item = aEvent.target;
+ while (item && item.localName != "richlistbox" && item.localName != "richlistitem") {
+ item = item.parentNode;
+ }
+
+ // left double click on a list item
+ if (item.localName == "richlistitem" && aEvent.button == 0) {
+ openAttachment();
+ }
+}
+
+/**
+ * Handler function to take care of right clicking on an attachment or the attachment list
+ *
+ * @param aEvent The DOM event caused by the clicking.
+ */
+function attachmentClick(aEvent) {
+ let item = aEvent.target.triggerNode;
+ while (item && item.localName != "richlistbox" && item.localName != "richlistitem") {
+ item = item.parentNode;
+ }
+
+ for (let node of aEvent.target.children) {
+ if (item.localName == "richlistitem" || node.id == "attachment-popup-attachPage") {
+ node.removeAttribute("hidden");
+ } else {
+ node.setAttribute("hidden", "true");
+ }
+ }
+}
+
+/**
+ * Helper function to show a notification in the event-dialog's notificationbox
+ *
+ * @param aMessage the message text to show
+ * @param aValue string identifying the notification
+ * @param aPriority (optional) the priority of the warning (info, critical), default is 'warn'
+ * @param aImage (optional) URL of image to appear on the notification
+ * @param aButtonset (optional) array of button descriptions to appear on the notification
+ * @param aCallback (optional) a function to handle events from the notificationbox
+ */
+function notifyUser(aMessage, aValue, aPriority, aImage, aButtonset, aCallback) {
+ // only append, if the notification does not already exist
+ if (gEventNotification.getNotificationWithValue(aValue) == null) {
+ const prioMap = {
+ info: gEventNotification.PRIORITY_INFO_MEDIUM,
+ critical: gEventNotification.PRIORITY_CRITICAL_MEDIUM,
+ };
+ let prio = prioMap[aPriority] || gEventNotification.PRIORITY_WARNING_MEDIUM;
+ gEventNotification.appendNotification(
+ aValue,
+ {
+ label: aMessage,
+ image: aImage,
+ priority: prio,
+ eventCallback: aCallback,
+ },
+ aButtonset
+ );
+ }
+}
+
+/**
+ * Remove a notification from the notifiactionBox
+ *
+ * @param {string} aValue - string identifying the notification to remove
+ */
+function removeNotification(aValue) {
+ let notification = gEventNotification.getNotificationWithValue(aValue);
+ if (notification) {
+ gEventNotification.removeNotification(notification);
+ }
+}
+
+/**
+ * Update the dialog controls related to the item's calendar.
+ */
+function updateCalendar() {
+ let item = window.calendarItem;
+ let calendar = getCurrentCalendar();
+
+ let cssSafeId = cal.view.formatStringForCSSRule(calendar.id);
+ document
+ .getElementById("item-calendar")
+ .style.setProperty("--item-color", `var(--calendar-${cssSafeId}-backcolor)`);
+
+ gIsReadOnly = calendar.readOnly;
+
+ if (!gPreviousCalendarId) {
+ gPreviousCalendarId = item.calendar.id;
+ }
+
+ // We might have to change the organizer, let's see
+ let calendarOrgId = calendar.getProperty("organizerId");
+ if (window.organizer && calendarOrgId && calendar.id != gPreviousCalendarId) {
+ window.organizer.id = calendarOrgId;
+ window.organizer.commonName = calendar.getProperty("organizerCN");
+ gPreviousCalendarId = calendar.id;
+ updateAttendeeInterface();
+ }
+
+ if (!canNotifyAttendees(calendar, item) && calendar.getProperty("imip.identity")) {
+ document.getElementById("notify-attendees-checkbox").removeAttribute("disabled");
+ document.getElementById("undisclose-attendees-checkbox").removeAttribute("disabled");
+ } else {
+ document.getElementById("notify-attendees-checkbox").setAttribute("disabled", "true");
+ document.getElementById("undisclose-attendees-checkbox").setAttribute("disabled", "true");
+ }
+
+ // update the accept button
+ updateAccept();
+
+ // TODO: the code above decided about whether or not the item is readonly.
+ // below we enable/disable all controls based on this decision.
+ // unfortunately some controls need to be disabled based on some other
+ // criteria. this is why we enable all controls in case the item is *not*
+ // readonly and run through all those updateXXX() functions to disable
+ // them again based on the specific logic build into those function. is this
+ // really a good idea?
+ if (gIsReadOnly) {
+ let disableElements = document.getElementsByAttribute("disable-on-readonly", "true");
+ for (let element of disableElements) {
+ if (element.namespaceURI == "http://www.w3.org/1999/xhtml") {
+ element.setAttribute("disabled", "disabled");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+
+ // we mark link-labels with the hyperlink attribute, since we need
+ // to remove their class in case they get disabled. TODO: it would
+ // be better to create a small binding for those link-labels
+ // instead of adding those special stuff.
+ if (element.hasAttribute("hyperlink")) {
+ element.removeAttribute("class");
+ element.removeAttribute("onclick");
+ }
+ }
+
+ let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true");
+ for (let element of collapseElements) {
+ element.setAttribute("collapsed", "true");
+ }
+ } else {
+ sendMessage({ command: "removeDisableAndCollapseOnReadonly" });
+
+ let enableElements = document.getElementsByAttribute("disable-on-readonly", "true");
+ for (let element of enableElements) {
+ element.removeAttribute("disabled");
+ if (element.hasAttribute("hyperlink")) {
+ element.classList.add("text-link");
+ }
+ }
+
+ let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true");
+ for (let element of collapseElements) {
+ element.removeAttribute("collapsed");
+ }
+
+ if (item.isTodo()) {
+ // Task completed date
+ if (item.completedDate) {
+ updateToDoStatus(item.status, cal.dtz.dateTimeToJsDate(item.completedDate));
+ } else {
+ updateToDoStatus(item.status);
+ }
+ }
+
+ // disable repeat menupopup if this is an occurrence
+ item = window.calendarItem;
+ if (item.parentItem != item) {
+ document.getElementById("item-repeat").setAttribute("disabled", "true");
+ document.getElementById("repeat-until-datepicker").setAttribute("disabled", "true");
+ let repeatDetails = document.getElementById("repeat-details");
+ let numChilds = repeatDetails.children.length;
+ for (let i = 0; i < numChilds; i++) {
+ let node = repeatDetails.children[i];
+ node.setAttribute("disabled", "true");
+ node.removeAttribute("class");
+ node.removeAttribute("onclick");
+ }
+ }
+
+ // If the item is a proxy occurrence/instance, a few things aren't
+ // valid.
+ if (item.parentItem != item) {
+ document.getElementById("item-calendar").setAttribute("disabled", "true");
+
+ // don't allow to revoke the entrydate of recurring todo's.
+ disableElementWithLock("todo-has-entrydate", "permanent-lock");
+ }
+
+ // update datetime pickers, disable checkboxes if dates are required by
+ // recurrence or reminders.
+ updateRepeat(true);
+ updateReminder(true);
+ updateAllDay();
+ }
+
+ // Make sure capabilities are reflected correctly
+ updateCapabilities();
+}
+
+/**
+ * Opens the recurrence dialog modally to allow the user to edit the recurrence
+ * rules.
+ */
+function editRepeat() {
+ let args = {};
+ args.calendarEvent = window.calendarItem;
+ args.recurrenceInfo = window.recurrenceInfo;
+ args.startTime = gStartTime;
+ args.endTime = gEndTime;
+
+ let savedWindow = window;
+ args.onOk = function (recurrenceInfo) {
+ savedWindow.recurrenceInfo = recurrenceInfo;
+ };
+
+ window.setCursor("wait");
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-recurrence.xhtml",
+ "_blank",
+ "chrome,titlebar,modal,resizable,centerscreen",
+ args
+ );
+}
+
+/**
+ * This function is responsible for propagating UI state to controls
+ * depending on the repeat setting of an item. This functionality is used
+ * after the dialog has been loaded as well as if the repeat pattern has
+ * been changed.
+ *
+ * @param aSuppressDialogs If true, controls are updated without prompting
+ * for changes with the recurrence dialog
+ * @param aItemRepeatCall True when the function is being called from
+ * the item-repeat menu list. It allows to detect
+ * a change from the "custom" option.
+ */
+function updateRepeat(aSuppressDialogs, aItemRepeatCall) {
+ function setUpEntrydateForTask(item) {
+ // if this item is a task, we need to make sure that it has
+ // an entry-date, otherwise we can't create a recurrence.
+ if (item.isTodo()) {
+ // automatically check 'has entrydate' if needed.
+ if (!document.getElementById("todo-has-entrydate").checked) {
+ document.getElementById("todo-has-entrydate").checked = true;
+
+ // make sure gStartTime is properly initialized
+ updateEntryDate();
+ }
+
+ // disable the checkbox to indicate that we need
+ // the entry-date. the 'disabled' state will be
+ // revoked if the user turns off the repeat pattern.
+ disableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+ }
+
+ let repeatMenu = document.getElementById("item-repeat");
+ let repeatValue = repeatMenu.selectedItem.getAttribute("value");
+ let repeatUntilDate = document.getElementById("repeat-untilDate");
+ let repeatDetails = document.getElementById("repeat-details");
+
+ if (repeatValue == "none") {
+ repeatUntilDate.hidden = true;
+ repeatDetails.hidden = true;
+ window.recurrenceInfo = null;
+ let item = window.calendarItem;
+ if (item.isTodo()) {
+ enableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+ } else if (repeatValue == "custom") {
+ // the user selected custom repeat pattern. we now need to bring
+ // up the appropriate dialog in order to let the user specify the
+ // new rule. First of all, retrieve the item we want to specify
+ // the custom repeat pattern for.
+ let item = window.calendarItem;
+
+ setUpEntrydateForTask(item);
+
+ // retrieve the current recurrence info, we need this
+ // to find out whether or not the user really created
+ // a new repeat pattern.
+ let recurrenceInfo = window.recurrenceInfo;
+
+ // now bring up the recurrence dialog.
+ // don't pop up the dialog if aSuppressDialogs was specified or if
+ // called during initialization of the dialog.
+ if (!aSuppressDialogs && repeatMenu.hasAttribute("last-value")) {
+ editRepeat();
+ }
+
+ // Assign gUntilDate on the first run or when returning from the
+ // edit recurrence dialog.
+ if (window.recurrenceInfo) {
+ let rrules = splitRecurrenceRules(window.recurrenceInfo);
+ let rule = rrules[0][0];
+ gUntilDate = null;
+ if (!rule.isByCount && rule.isFinite && rule.untilDate) {
+ gUntilDate = rule.untilDate.clone().getInTimezone(cal.dtz.defaultTimezone);
+ }
+ }
+
+ // we need to address two separate cases here.
+ // 1)- We need to revoke the selection of the repeat
+ // drop down list in case the user didn't specify
+ // a new repeat pattern (i.e. canceled the dialog);
+ // - re-enable the 'has entrydate' option in case
+ // we didn't end up with a recurrence rule.
+ // 2) Check whether the new recurrence rule needs the
+ // recurrence details text or it can be displayed
+ // only with the repeat-until-datepicker.
+ if (recurrenceInfo == window.recurrenceInfo) {
+ repeatMenu.selectedIndex = gLastRepeatSelection;
+ if (item.isTodo()) {
+ if (!window.recurrenceInfo) {
+ enableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+ }
+ } else {
+ repeatUntilDate.hidden = true;
+ repeatDetails.hidden = false;
+ // From the Edit Recurrence dialog, the rules "every day" and
+ // "every weekday" don't need the recurrence details text when they
+ // have only the until date. The getRepeatTypeAndUntilDate()
+ // function verifies whether this is the case.
+ let [repeatType, untilDate] = getRepeatTypeAndUntilDate(item);
+ loadRepeat(repeatType, untilDate, window.calendarItem);
+ }
+ } else {
+ let item = window.calendarItem;
+ let recurrenceInfo = window.recurrenceInfo || item.recurrenceInfo;
+ let proposedUntilDate = (gStartTime || window.initialStartDateValue).clone();
+
+ if (recurrenceInfo) {
+ recurrenceInfo = recurrenceInfo.clone();
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ let rule = rrules[0][0];
+
+ // If the previous rule was "custom" we have to recover the until
+ // date, or the last occurrence's date in order to set the
+ // repeat-until-datepicker with the same date.
+ if (aItemRepeatCall && repeatUntilDate.hidden && !repeatDetails.hidden) {
+ let repeatDate;
+ if (!rule.isByCount || !rule.isFinite) {
+ if (rule.isFinite) {
+ repeatDate = rule.untilDate.getInTimezone(cal.dtz.floating);
+ repeatDate = cal.dtz.dateTimeToJsDate(repeatDate);
+ } else {
+ repeatDate = "forever";
+ }
+ } else {
+ // Try to recover the last occurrence in 10(?) years.
+ let endDate = gStartTime.clone();
+ endDate.year += 10;
+ let lastOccurrenceDate = null;
+ let dates = recurrenceInfo.getOccurrenceDates(gStartTime, endDate, 0);
+ if (dates) {
+ lastOccurrenceDate = dates[dates.length - 1];
+ }
+ repeatDate = (lastOccurrenceDate || proposedUntilDate).getInTimezone(cal.dtz.floating);
+ repeatDate = cal.dtz.dateTimeToJsDate(repeatDate);
+ }
+ document.getElementById("repeat-until-datepicker").value = repeatDate;
+ }
+ if (rrules[0].length > 0) {
+ recurrenceInfo.deleteRecurrenceItem(rule);
+ }
+ } else {
+ // New event proposes "forever" as default until date.
+ recurrenceInfo = new CalRecurrenceInfo(item);
+ document.getElementById("repeat-until-datepicker").value = "forever";
+ }
+
+ repeatUntilDate.hidden = false;
+ repeatDetails.hidden = true;
+
+ let recRule = cal.createRecurrenceRule();
+ recRule.interval = 1;
+ switch (repeatValue) {
+ case "daily":
+ recRule.type = "DAILY";
+ break;
+ case "weekly":
+ recRule.type = "WEEKLY";
+ break;
+ case "every.weekday":
+ recRule.type = "DAILY";
+ recRule.setComponent("BYDAY", [2, 3, 4, 5, 6]);
+ break;
+ case "bi.weekly":
+ recRule.type = "WEEKLY";
+ recRule.interval = 2;
+ break;
+ case "monthly":
+ recRule.type = "MONTHLY";
+ break;
+ case "yearly":
+ recRule.type = "YEARLY";
+ break;
+ }
+
+ setUpEntrydateForTask(item);
+ updateUntildateRecRule(recRule);
+
+ recurrenceInfo.insertRecurrenceItemAt(recRule, 0);
+ window.recurrenceInfo = recurrenceInfo;
+
+ if (item.isTodo()) {
+ if (!document.getElementById("todo-has-entrydate").checked) {
+ document.getElementById("todo-has-entrydate").checked = true;
+ }
+ disableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+
+ // Preset the until-datepicker's minimonth to the start date.
+ let startDate = cal.dtz.dateTimeToJsDate(gStartTime.getInTimezone(cal.dtz.floating));
+ document.getElementById("repeat-until-datepicker").extraDate = startDate;
+ }
+
+ gLastRepeatSelection = repeatMenu.selectedIndex;
+ repeatMenu.setAttribute("last-value", repeatValue);
+
+ updateRepeatDetails();
+ updateEntryDate();
+ updateDueDate();
+ updateAccept();
+}
+
+/**
+ * Update the until date in the recurrence rule in order to set
+ * the same time of the start date.
+ *
+ * @param recRule (optional) The recurrence rule
+ */
+function updateUntildateRecRule(recRule) {
+ if (!recRule) {
+ let recurrenceInfo = window.recurrenceInfo;
+ if (!recurrenceInfo) {
+ return;
+ }
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ recRule = rrules[0][0];
+ }
+ let defaultTimezone = cal.dtz.defaultTimezone;
+ let repeatUntilDate = null;
+
+ let itemRepeat = document.getElementById("item-repeat").selectedItem.value;
+ if (itemRepeat == "none") {
+ return;
+ } else if (itemRepeat == "custom") {
+ repeatUntilDate = gUntilDate;
+ } else {
+ let untilDatepickerDate = document.getElementById("repeat-until-datepicker").value;
+ if (untilDatepickerDate != "forever") {
+ repeatUntilDate = cal.dtz.jsDateToDateTime(untilDatepickerDate, defaultTimezone);
+ }
+ }
+
+ if (repeatUntilDate) {
+ if (onLoad.hasLoaded) {
+ repeatUntilDate.isDate = gStartTime.isDate; // Enforce same value type as DTSTART
+ if (!gStartTime.isDate) {
+ repeatUntilDate.hour = gStartTime.hour;
+ repeatUntilDate.minute = gStartTime.minute;
+ repeatUntilDate.second = gStartTime.second;
+ }
+ }
+ recRule.untilDate = repeatUntilDate.clone();
+ gUntilDate = repeatUntilDate.clone().getInTimezone(defaultTimezone);
+ } else {
+ // Rule that recurs forever or with a "count" number of recurrences.
+ gUntilDate = null;
+ }
+}
+
+/**
+ * Updates the UI controls related to a task's completion status.
+ *
+ * @param {string} aStatus - The item's completion status or a string
+ * that allows to identify a change in the
+ * percent-complete's textbox.
+ * @param {Date} aCompletedDate - The item's completed date (as a JSDate).
+ */
+function updateToDoStatus(aStatus, aCompletedDate = null) {
+ // RFC2445 doesn't support completedDates without the todo's status
+ // being "COMPLETED", however twiddling the status menulist shouldn't
+ // destroy that information at this point (in case you change status
+ // back to COMPLETED). When we go to store this VTODO as .ics the
+ // date will get lost.
+
+ // remember the original values
+ let oldPercentComplete = parseInt(document.getElementById("percent-complete-textbox").value, 10);
+ let oldCompletedDate = document.getElementById("completed-date-picker").value;
+
+ // If the percent completed has changed to 100 or from 100 to another
+ // value, the status must change.
+ if (aStatus == "percent-changed") {
+ let selectedIndex = document.getElementById("todo-status").selectedIndex;
+ let menuItemCompleted = selectedIndex == 3;
+ let menuItemNotSpecified = selectedIndex == 0;
+ if (oldPercentComplete == 100) {
+ aStatus = "COMPLETED";
+ } else if (menuItemCompleted || menuItemNotSpecified) {
+ aStatus = "IN-PROCESS";
+ }
+ }
+
+ switch (aStatus) {
+ case null:
+ case "":
+ case "NONE":
+ oldPercentComplete = 0;
+ document.getElementById("todo-status").selectedIndex = 0;
+ document.getElementById("percent-complete-textbox").setAttribute("disabled", "true");
+ document.getElementById("percent-complete-label").setAttribute("disabled", "true");
+ break;
+ case "CANCELLED":
+ document.getElementById("todo-status").selectedIndex = 4;
+ document.getElementById("percent-complete-textbox").setAttribute("disabled", "true");
+ document.getElementById("percent-complete-label").setAttribute("disabled", "true");
+ break;
+ case "COMPLETED":
+ document.getElementById("todo-status").selectedIndex = 3;
+ document.getElementById("percent-complete-textbox").removeAttribute("disabled");
+ document.getElementById("percent-complete-label").removeAttribute("disabled");
+ // if there is no aCompletedDate, set it to the previous value
+ if (!aCompletedDate) {
+ aCompletedDate = oldCompletedDate;
+ }
+ break;
+ case "IN-PROCESS":
+ document.getElementById("todo-status").selectedIndex = 2;
+ document.getElementById("completed-date-picker").setAttribute("disabled", "true");
+ document.getElementById("percent-complete-textbox").removeAttribute("disabled");
+ document.getElementById("percent-complete-label").removeAttribute("disabled");
+ break;
+ case "NEEDS-ACTION":
+ document.getElementById("todo-status").selectedIndex = 1;
+ document.getElementById("percent-complete-textbox").removeAttribute("disabled");
+ document.getElementById("percent-complete-label").removeAttribute("disabled");
+ break;
+ }
+
+ let newPercentComplete;
+ if ((aStatus == "IN-PROCESS" || aStatus == "NEEDS-ACTION") && oldPercentComplete == 100) {
+ newPercentComplete = 0;
+ document.getElementById("completed-date-picker").value = oldCompletedDate;
+ document.getElementById("completed-date-picker").setAttribute("disabled", "true");
+ } else if (aStatus == "COMPLETED") {
+ newPercentComplete = 100;
+ document.getElementById("completed-date-picker").value = aCompletedDate;
+ document.getElementById("completed-date-picker").removeAttribute("disabled");
+ } else {
+ newPercentComplete = oldPercentComplete;
+ document.getElementById("completed-date-picker").value = oldCompletedDate;
+ document.getElementById("completed-date-picker").setAttribute("disabled", "true");
+ }
+
+ gConfig.percentComplete = newPercentComplete;
+ document.getElementById("percent-complete-textbox").value = newPercentComplete;
+ if (gInTab) {
+ sendMessage({
+ command: "updateConfigState",
+ argument: { percentComplete: newPercentComplete },
+ });
+ }
+}
+
+/**
+ * Saves all dialog controls back to the item.
+ *
+ * @returns a copy of the original item with changes made.
+ */
+function saveItem() {
+ // we need to clone the item in order to apply the changes.
+ // it is important to not apply the changes to the original item
+ // (even if it happens to be mutable) in order to guarantee
+ // that providers see a proper oldItem/newItem pair in case
+ // they rely on this fact (e.g. WCAP does).
+ let originalItem = window.calendarItem;
+ let item = originalItem.clone();
+
+ // override item's recurrenceInfo *before* serializing date/time-objects.
+ if (!item.recurrenceId) {
+ item.recurrenceInfo = window.recurrenceInfo;
+ }
+
+ // serialize the item
+ saveDialog(item);
+
+ item.organizer = window.organizer;
+
+ item.removeAllAttendees();
+ if (window.attendees && window.attendees.length > 0) {
+ for (let attendee of window.attendees) {
+ item.addAttendee(attendee);
+ }
+
+ let notifyCheckbox = document.getElementById("notify-attendees-checkbox");
+ if (notifyCheckbox.disabled) {
+ item.deleteProperty("X-MOZ-SEND-INVITATIONS");
+ } else {
+ item.setProperty("X-MOZ-SEND-INVITATIONS", notifyCheckbox.checked ? "TRUE" : "FALSE");
+ }
+ let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox");
+ if (undiscloseCheckbox.disabled) {
+ item.deleteProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED");
+ } else {
+ item.setProperty(
+ "X-MOZ-SEND-INVITATIONS-UNDISCLOSED",
+ undiscloseCheckbox.checked ? "TRUE" : "FALSE"
+ );
+ }
+ let disallowcounterCheckbox = document.getElementById("disallow-counter-checkbox");
+ let xProp = window.calendarItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER");
+ // we want to leave an existing x-prop in case the checkbox is disabled as we need to
+ // roundtrip x-props that are not exclusively under our control
+ if (!disallowcounterCheckbox.disabled) {
+ // we only set the prop if we need to
+ if (disallowcounterCheckbox.checked) {
+ item.setProperty("X-MICROSOFT-DISALLOW-COUNTER", "TRUE");
+ } else if (xProp) {
+ item.setProperty("X-MICROSOFT-DISALLOW-COUNTER", "FALSE");
+ }
+ }
+ }
+
+ // We check if the organizerID is different from our
+ // calendar-user-address-set. The organzerID is the owner of the calendar.
+ // If it's different, that is because someone is acting on behalf of
+ // the organizer.
+ if (item.organizer && item.calendar.aclEntry) {
+ let userAddresses = item.calendar.aclEntry.getUserAddresses();
+ if (
+ userAddresses.length > 0 &&
+ !cal.email.attendeeMatchesAddresses(item.organizer, userAddresses)
+ ) {
+ let organizer = item.organizer.clone();
+ organizer.setProperty("SENT-BY", "mailto:" + userAddresses[0]);
+ item.organizer = organizer;
+ }
+ }
+ return item;
+}
+
+/**
+ * Action to take when the user chooses to save. This can happen either by
+ * saving directly or the user selecting to save after being prompted when
+ * closing the dialog.
+ *
+ * This function also takes care of notifying this dialog's caller that the item
+ * is saved.
+ *
+ * @param aIsClosing If true, the save action originates from the
+ * save prompt just before the window is closing.
+ */
+function onCommandSave(aIsClosing) {
+ // The datepickers need to remove the focus in order to trigger the
+ // validation of the values just edited, with the keyboard, but not yet
+ // confirmed (i.e. not followed by a click, a tab or enter keys pressure).
+ document.documentElement.focus();
+
+ // Don't save if a warning dialog about a wrong input date must be showed.
+ if (gWarning) {
+ return;
+ }
+
+ eventDialogCalendarObserver.cancel();
+
+ let originalItem = window.calendarItem;
+ let item = saveItem();
+ let calendar = getCurrentCalendar();
+ adaptScheduleAgent(item);
+
+ item.makeImmutable();
+ // Set the item for now, the callback below will set the full item when the
+ // call succeeded
+ window.calendarItem = item;
+
+ // When the call is complete, we need to set the new item, so that the
+ // dialog is up to date.
+
+ // XXX Do we want to disable the dialog or at least the save button until
+ // the call is complete? This might help when the user tries to save twice
+ // before the call is complete. In that case, we do need a progress bar and
+ // the ability to cancel the operation though.
+ let listener = {
+ onTransactionComplete(aItem) {
+ let aId = aItem.id;
+ let aCalendar = aItem.calendar;
+ // Check if the current window has a calendarItem first, because in case of undo
+ // window refers to the main window and we would get a 'calendarItem is undefined' warning.
+ if (!aIsClosing && "calendarItem" in window) {
+ // If we changed the calendar of the item, onOperationComplete will be called multiple
+ // times. We need to make sure we're receiving the update on the right calendar.
+ if (
+ (!window.calendarItem.id || aId == window.calendarItem.id) &&
+ aCalendar.id == window.calendarItem.calendar.id
+ ) {
+ if (window.calendarItem.recurrenceId) {
+ // TODO This workaround needs to be removed in bug 396182
+ // We are editing an occurrence. Make sure that the returned
+ // item is the same occurrence, not its parent item.
+ let occ = aItem.recurrenceInfo.getOccurrenceFor(window.calendarItem.recurrenceId);
+ window.calendarItem = occ;
+ } else {
+ // We are editing the parent item, no workarounds needed
+ window.calendarItem = aItem;
+ }
+
+ // We now have an item, so we must change to an edit.
+ window.mode = "modify";
+ updateTitle();
+ eventDialogCalendarObserver.observe(window.calendarItem.calendar);
+ }
+ }
+ // this triggers the update of the imipbar in case this is a rescheduling case
+ if (window.counterProposal && window.counterProposal.onReschedule) {
+ window.counterProposal.onReschedule();
+ }
+ },
+ onGetResult(calendarItem, status, itemType, detail, items) {},
+ };
+ let resp = document.getElementById("notify-attendees-checkbox").checked
+ ? Ci.calIItipItem.AUTO
+ : Ci.calIItipItem.NONE;
+ let extResponse = { responseMode: resp };
+ window.onAcceptCallback(item, calendar, originalItem, listener, extResponse);
+}
+
+/**
+ * This function is called when the user chooses to delete an Item
+ * from the Event/Task dialog
+ *
+ */
+function onCommandDeleteItem() {
+ // only ask for confirmation, if the User changed anything on a new item or we modify an existing item
+ if (isItemChanged() || window.mode != "new") {
+ if (!cal.window.promptDeleteItems(window.calendarItem, true)) {
+ return;
+ }
+ }
+
+ if (window.mode == "new") {
+ cancelItem();
+ } else {
+ let deleteListener = {
+ // when deletion of item is complete, close the dialog
+ onTransactionComplete(item) {
+ // Check if the current window has a calendarItem first, because in case of undo
+ // window refers to the main window and we would get a 'calendarItem is undefined' warning.
+ if ("calendarItem" in window) {
+ if (item.id == window.calendarItem.id) {
+ cancelItem();
+ } else {
+ eventDialogCalendarObserver.observe(window.calendarItem.calendar);
+ }
+ }
+ },
+ };
+
+ eventDialogCalendarObserver.cancel();
+ if (window.calendarItem.parentItem.recurrenceInfo && window.calendarItem.recurrenceId) {
+ // if this is a single occurrence of a recurring item
+ if (countOccurrences(window.calendarItem) == 1) {
+ // this is the last occurrence, hence we delete the parent item
+ // to not leave a parent item without children in the calendar
+ gMainWindow.doTransaction(
+ "delete",
+ window.calendarItem.parentItem,
+ window.calendarItem.calendar,
+ null,
+ deleteListener
+ );
+ } else {
+ // we just need to remove the occurrence
+ let newItem = window.calendarItem.parentItem.clone();
+ newItem.recurrenceInfo.removeOccurrenceAt(window.calendarItem.recurrenceId);
+ gMainWindow.doTransaction(
+ "modify",
+ newItem,
+ newItem.calendar,
+ window.calendarItem.parentItem,
+ deleteListener
+ );
+ }
+ } else {
+ gMainWindow.doTransaction(
+ "delete",
+ window.calendarItem,
+ window.calendarItem.calendar,
+ null,
+ deleteListener
+ );
+ }
+ }
+}
+
+/**
+ * Postpone the task's start date/time and due date/time. ISO 8601
+ * format: "PT1H", "P1D", and "P1W" are 1 hour, 1 day, and 1 week. (We
+ * use this format intentionally instead of a calIDuration object because
+ * those objects cannot be serialized for message passing with iframes.)
+ *
+ * @param {string} aDuration - A duration in ISO 8601 format
+ */
+function postponeTask(aDuration) {
+ let duration = cal.createDuration(aDuration);
+ if (gStartTime != null) {
+ gStartTime.addDuration(duration);
+ }
+ if (gEndTime != null) {
+ gEndTime.addDuration(duration);
+ }
+ updateDateTime();
+}
+
+/**
+ * Prompts the user to change the start timezone.
+ */
+function editStartTimezone() {
+ editTimezone(
+ "timezone-starttime",
+ gStartTime.getInTimezone(gStartTimezone),
+ editStartTimezone.complete
+ );
+}
+editStartTimezone.complete = function (datetime) {
+ let equalTimezones = false;
+ if (gStartTimezone && gEndTimezone) {
+ if (gStartTimezone == gEndTimezone) {
+ equalTimezones = true;
+ }
+ }
+ gStartTimezone = datetime.timezone;
+ if (equalTimezones) {
+ gEndTimezone = datetime.timezone;
+ }
+ updateDateTime();
+};
+
+/**
+ * Prompts the user to change the end timezone.
+ */
+function editEndTimezone() {
+ editTimezone("timezone-endtime", gEndTime.getInTimezone(gEndTimezone), editEndTimezone.complete);
+}
+editEndTimezone.complete = function (datetime) {
+ gEndTimezone = datetime.timezone;
+ updateDateTime();
+};
+
+/**
+ * Called to choose a recent timezone from the timezone popup.
+ *
+ * @param event The event with a target that holds the timezone id value.
+ */
+function chooseRecentTimezone(event) {
+ let tzid = event.target.value;
+ let timezonePopup = document.getElementById("timezone-popup");
+
+ if (tzid != "custom") {
+ let zone = cal.timezoneService.getTimezone(tzid);
+ let datetime = timezonePopup.dateTime.getInTimezone(zone);
+ timezonePopup.editTimezone.complete(datetime);
+ }
+}
+
+/**
+ * Opens the timezone popup on the node the event target points at.
+ *
+ * @param event The event causing the popup to open
+ * @param dateTime The datetime for which the timezone should be modified
+ * @param editFunc The function to be called when the custom menuitem is clicked.
+ */
+function showTimezonePopup(event, dateTime, editFunc) {
+ // Don't do anything for right/middle-clicks. Also, don't show the popup if
+ // the opening node is disabled.
+ if (event.button != 0 || event.target.disabled) {
+ return;
+ }
+
+ let timezonePopup = document.getElementById("timezone-popup");
+ let timezoneDefaultItem = document.getElementById("timezone-popup-defaulttz");
+ let timezoneSeparator = document.getElementById("timezone-popup-menuseparator");
+ let defaultTimezone = cal.dtz.defaultTimezone;
+ let recentTimezones = cal.dtz.getRecentTimezones(true);
+
+ // Set up the right editTimezone function, so the custom item can use it.
+ timezonePopup.editTimezone = editFunc;
+ timezonePopup.dateTime = dateTime;
+
+ // Set up the default timezone item
+ timezoneDefaultItem.value = defaultTimezone.tzid;
+ timezoneDefaultItem.label = defaultTimezone.displayName;
+
+ // Clear out any old recent timezones
+ while (timezoneDefaultItem.nextElementSibling != timezoneSeparator) {
+ timezoneDefaultItem.nextElementSibling.remove();
+ }
+
+ // Fill in the new recent timezones
+ for (let timezone of recentTimezones) {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("value", timezone.tzid);
+ menuItem.setAttribute("label", timezone.displayName);
+ timezonePopup.insertBefore(menuItem, timezoneDefaultItem.nextElementSibling);
+ }
+
+ // Show the popup
+ timezonePopup.openPopup(event.target, "after_start", 0, 0, true);
+}
+
+/**
+ * Common function of edit(Start|End)Timezone() to prompt the user for a
+ * timezone change.
+ *
+ * @param aElementId The XUL element id of the timezone label.
+ * @param aDateTime The Date/Time of the time to change zone on.
+ * @param aCallback What to do when the user has chosen a zone.
+ */
+function editTimezone(aElementId, aDateTime, aCallback) {
+ if (document.getElementById(aElementId).hasAttribute("disabled")) {
+ return;
+ }
+
+ // prepare the arguments that will be passed to the dialog
+ let args = {};
+ args.time = aDateTime;
+ args.calendar = getCurrentCalendar();
+ args.onOk = function (datetime) {
+ cal.dtz.saveRecentTimezone(datetime.timezone.tzid);
+ return aCallback(datetime);
+ };
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-timezone.xhtml",
+ "_blank",
+ "chrome,titlebar,modal,resizable,centerscreen",
+ args
+ );
+}
+
+/**
+ * This function initializes the following controls:
+ * - 'event-starttime'
+ * - 'event-endtime'
+ * - 'event-all-day'
+ * - 'todo-has-entrydate'
+ * - 'todo-entrydate'
+ * - 'todo-has-duedate'
+ * - 'todo-duedate'
+ * The date/time-objects are either displayed in their respective
+ * timezone or in the default timezone. This decision is based
+ * on whether or not 'cmd_timezone' is checked.
+ * the necessary information is taken from the following variables:
+ * - 'gStartTime'
+ * - 'gEndTime'
+ * - 'window.calendarItem' (used to decide about event/task)
+ */
+function updateDateTime() {
+ gIgnoreUpdate = true;
+
+ let item = window.calendarItem;
+ // Convert to default timezone if the timezone option
+ // is *not* checked, otherwise keep the specific timezone
+ // and display the labels in order to modify the timezone.
+ if (gTimezonesEnabled) {
+ if (item.isEvent()) {
+ let startTime = gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime.getInTimezone(gEndTimezone);
+
+ document.getElementById("event-all-day").checked = startTime.isDate;
+
+ // In the case where the timezones are different but
+ // the timezone of the endtime is "UTC", we convert
+ // the endtime into the timezone of the starttime.
+ if (startTime && endTime) {
+ if (!cal.data.compareObjects(startTime.timezone, endTime.timezone)) {
+ if (endTime.timezone.isUTC) {
+ endTime = endTime.getInTimezone(startTime.timezone);
+ }
+ }
+ }
+
+ // before feeding the date/time value into the control we need
+ // to set the timezone to 'floating' in order to avoid the
+ // automatic conversion back into the OS timezone.
+ startTime.timezone = cal.dtz.floating;
+ endTime.timezone = cal.dtz.floating;
+
+ document.getElementById("event-starttime").value = cal.dtz.dateTimeToJsDate(startTime);
+ document.getElementById("event-endtime").value = cal.dtz.dateTimeToJsDate(endTime);
+ }
+
+ if (item.isTodo()) {
+ let startTime = gStartTime && gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime && gEndTime.getInTimezone(gEndTimezone);
+ let hasEntryDate = startTime != null;
+ let hasDueDate = endTime != null;
+
+ if (hasEntryDate && hasDueDate) {
+ document.getElementById("todo-has-entrydate").checked = hasEntryDate;
+ startTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(startTime);
+
+ document.getElementById("todo-has-duedate").checked = hasDueDate;
+ endTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(endTime);
+ } else if (hasEntryDate) {
+ document.getElementById("todo-has-entrydate").checked = hasEntryDate;
+ startTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(startTime);
+
+ startTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(startTime);
+ } else if (hasDueDate) {
+ endTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(endTime);
+
+ document.getElementById("todo-has-duedate").checked = hasDueDate;
+ endTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(endTime);
+ } else {
+ startTime = window.initialStartDateValue;
+ startTime.timezone = cal.dtz.floating;
+ endTime = startTime.clone();
+
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(startTime);
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(endTime);
+ }
+ }
+ } else {
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+
+ if (item.isEvent()) {
+ let startTime = gStartTime.getInTimezone(kDefaultTimezone);
+ let endTime = gEndTime.getInTimezone(kDefaultTimezone);
+ document.getElementById("event-all-day").checked = startTime.isDate;
+
+ // before feeding the date/time value into the control we need
+ // to set the timezone to 'floating' in order to avoid the
+ // automatic conversion back into the OS timezone.
+ startTime.timezone = cal.dtz.floating;
+ endTime.timezone = cal.dtz.floating;
+ document.getElementById("event-starttime").value = cal.dtz.dateTimeToJsDate(startTime);
+ document.getElementById("event-endtime").value = cal.dtz.dateTimeToJsDate(endTime);
+ }
+
+ if (item.isTodo()) {
+ let startTime = gStartTime && gStartTime.getInTimezone(kDefaultTimezone);
+ let endTime = gEndTime && gEndTime.getInTimezone(kDefaultTimezone);
+ let hasEntryDate = startTime != null;
+ let hasDueDate = endTime != null;
+
+ if (hasEntryDate && hasDueDate) {
+ document.getElementById("todo-has-entrydate").checked = hasEntryDate;
+ startTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(startTime);
+
+ document.getElementById("todo-has-duedate").checked = hasDueDate;
+ endTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(endTime);
+ } else if (hasEntryDate) {
+ document.getElementById("todo-has-entrydate").checked = hasEntryDate;
+ startTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(startTime);
+
+ startTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(startTime);
+ } else if (hasDueDate) {
+ endTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(endTime);
+
+ document.getElementById("todo-has-duedate").checked = hasDueDate;
+ endTime.timezone = cal.dtz.floating;
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(endTime);
+ } else {
+ startTime = window.initialStartDateValue;
+ startTime.timezone = cal.dtz.floating;
+ endTime = startTime.clone();
+
+ document.getElementById("todo-entrydate").value = cal.dtz.dateTimeToJsDate(startTime);
+ document.getElementById("todo-duedate").value = cal.dtz.dateTimeToJsDate(endTime);
+ }
+ }
+ }
+
+ updateTimezone();
+ updateAllDay();
+ updateRepeatDetails();
+
+ gIgnoreUpdate = false;
+}
+
+/**
+ * This function initializes the following controls:
+ * - 'timezone-starttime'
+ * - 'timezone-endtime'
+ * the timezone-links show the corrosponding names of the
+ * start/end times. If 'cmd_timezone' is not checked
+ * the links will be collapsed.
+ */
+function updateTimezone() {
+ function updateTimezoneElement(aTimezone, aId, aDateTime) {
+ let element = document.getElementById(aId);
+ if (!element) {
+ return;
+ }
+
+ if (aTimezone) {
+ element.removeAttribute("collapsed");
+ element.value = aTimezone.displayName || aTimezone.tzid;
+ if (!aDateTime || !aDateTime.isValid || gIsReadOnly || aDateTime.isDate) {
+ if (element.hasAttribute("class")) {
+ element.setAttribute("class-on-enabled", element.getAttribute("class"));
+ element.removeAttribute("class");
+ }
+ if (element.hasAttribute("onclick")) {
+ element.setAttribute("onclick-on-enabled", element.getAttribute("onclick"));
+ element.removeAttribute("onclick");
+ }
+ element.setAttribute("disabled", "true");
+ } else {
+ if (element.hasAttribute("class-on-enabled")) {
+ element.setAttribute("class", element.getAttribute("class-on-enabled"));
+ element.removeAttribute("class-on-enabled");
+ }
+ if (element.hasAttribute("onclick-on-enabled")) {
+ element.setAttribute("onclick", element.getAttribute("onclick-on-enabled"));
+ element.removeAttribute("onclick-on-enabled");
+ }
+ element.removeAttribute("disabled");
+ }
+ } else {
+ element.setAttribute("collapsed", "true");
+ }
+ }
+
+ // convert to default timezone if the timezone option
+ // is *not* checked, otherwise keep the specific timezone
+ // and display the labels in order to modify the timezone.
+ if (gTimezonesEnabled) {
+ updateTimezoneElement(gStartTimezone, "timezone-starttime", gStartTime);
+ updateTimezoneElement(gEndTimezone, "timezone-endtime", gEndTime);
+ } else {
+ document.getElementById("timezone-starttime").setAttribute("collapsed", "true");
+ document.getElementById("timezone-endtime").setAttribute("collapsed", "true");
+ }
+}
+
+/**
+ * Updates dialog controls related to item attachments
+ */
+function updateAttachment() {
+ let hasAttachments = capSupported("attachments");
+ document.getElementById("cmd_attach_url").setAttribute("disabled", !hasAttachments);
+
+ // update the attachment tab label to make the number of (uri) attachments visible
+ // even if another tab is displayed
+ let attachments = Object.values(gAttachMap).filter(aAtt => aAtt.uri);
+ let attachmentTab = document.getElementById("event-grid-tab-attachments");
+ if (attachments.length) {
+ attachmentTab.label = cal.l10n.getString("calendar-event-dialog", "attachmentsTabLabel", [
+ attachments.length,
+ ]);
+ } else {
+ attachmentTab.label = window.attachmentTabLabel;
+ }
+
+ sendMessage({
+ command: "updateConfigState",
+ argument: { attachUrlCommand: hasAttachments },
+ });
+}
+
+/**
+ * Returns whether to show or hide the related link on the dialog
+ * (rfc2445 URL property).
+ *
+ * @param {string} aUrl - The url in question.
+ * @returns {boolean} true for show and false for hide
+ */
+function showOrHideItemURL(url) {
+ if (!url) {
+ return false;
+ }
+ let handler;
+ let uri;
+ try {
+ uri = Services.io.newURI(url);
+ handler = Services.io.getProtocolHandler(uri.scheme);
+ } catch (e) {
+ // No protocol handler for the given protocol, or invalid uri
+ // hideOrShow(false);
+ return false;
+ }
+ // Only show if its either an internal protocol handler, or its external
+ // and there is an external app for the scheme
+ handler = cal.wrapInstance(handler, Ci.nsIExternalProtocolHandler);
+ return !handler || handler.externalAppExistsForScheme(uri.scheme);
+}
+
+/**
+ * Updates the related link on the dialog (rfc2445 URL property).
+ *
+ * @param {boolean} aShow - Show the link (true) or not (false)
+ * @param {string} aUrl - The url
+ */
+function updateItemURL(aShow, aUrl) {
+ // Hide or show the link
+ document.getElementById("event-grid-link-separator").toggleAttribute("hidden", !aShow);
+ document.getElementById("event-grid-link-row").toggleAttribute("hidden", !aShow);
+
+ // Set the url for the link
+ if (aShow && aUrl.length) {
+ setTimeout(() => {
+ // HACK the url-link doesn't crop when setting the value in onLoad
+ let label = document.getElementById("url-link");
+ label.setAttribute("value", aUrl);
+ label.setAttribute("href", aUrl);
+ }, 0);
+ }
+}
+
+/**
+ * This function updates dialog controls related to attendees.
+ */
+function updateAttendeeInterface() {
+ // sending email invitations currently only supported for events
+ let attendeeTab = document.getElementById("event-grid-tab-attendees");
+ let attendeePanel = document.getElementById("event-grid-tabpanel-attendees");
+ let notifyOptions = document.getElementById("notify-options");
+ if (window.calendarItem.isEvent()) {
+ attendeeTab.removeAttribute("collapsed");
+ attendeePanel.removeAttribute("collapsed");
+ notifyOptions.removeAttribute("collapsed");
+
+ let organizerRow = document.getElementById("item-organizer-row");
+ if (window.organizer && window.organizer.id) {
+ let existingLabel = organizerRow.querySelector(":scope > .attendee-label");
+ if (existingLabel) {
+ organizerRow.removeChild(existingLabel);
+ }
+ organizerRow.appendChild(
+ cal.invitation.createAttendeeLabel(document, window.organizer, window.attendees)
+ );
+ organizerRow.hidden = false;
+ } else {
+ organizerRow.hidden = true;
+ }
+
+ let attendeeContainer = document.querySelector(".item-attendees-list-container");
+ if (attendeeContainer.firstChild) {
+ attendeeContainer.firstChild.remove();
+ }
+ attendeeContainer.appendChild(cal.invitation.createAttendeesList(document, window.attendees));
+ for (let label of attendeeContainer.querySelectorAll(".attendee-label")) {
+ label.addEventListener("dblclick", attendeeDblClick);
+ label.setAttribute("tabindex", "0");
+ }
+
+ // update the attendee tab label to make the number of attendees
+ // visible even if another tab is displayed
+ if (window.attendees.length) {
+ attendeeTab.label = cal.l10n.getString("calendar-event-dialog", "attendeesTabLabel", [
+ window.attendees.length,
+ ]);
+ } else {
+ attendeeTab.label = window.attendeeTabLabel;
+ }
+ } else {
+ attendeeTab.setAttribute("collapsed", "true");
+ attendeePanel.setAttribute("collapsed", "true");
+ }
+ updateParentSaveControls();
+}
+
+/**
+ * Update the save controls in parent context depending on the whether attendees
+ * exist for this event and notifying is enabled
+ */
+function updateParentSaveControls() {
+ let mode =
+ window.calendarItem.isEvent() &&
+ window.organizer &&
+ window.organizer.id &&
+ window.attendees &&
+ window.attendees.length > 0 &&
+ document.getElementById("notify-attendees-checkbox").checked;
+
+ sendMessage({
+ command: "updateSaveControls",
+ argument: { sendNotSave: mode },
+ });
+}
+
+/**
+ * This function updates dialog controls related to recurrence, in this case the
+ * text describing the recurrence rule.
+ */
+function updateRepeatDetails() {
+ // Don't try to show the details text for
+ // anything but a custom recurrence rule.
+ let recurrenceInfo = window.recurrenceInfo;
+ let itemRepeat = document.getElementById("item-repeat");
+ let repeatDetails = document.getElementById("repeat-details");
+ if (itemRepeat.value == "custom" && recurrenceInfo && !hasUnsupported(recurrenceInfo)) {
+ let item = window.calendarItem;
+ document.getElementById("repeat-untilDate").hidden = true;
+ // Try to create a descriptive string from the rule(s).
+ let kDefaultTimezone = cal.dtz.defaultTimezone;
+ let event = item.isEvent();
+
+ let startDate = document.getElementById(event ? "event-starttime" : "todo-entrydate").value;
+ let endDate = document.getElementById(event ? "event-endtime" : "todo-duedate").value;
+ startDate = cal.dtz.jsDateToDateTime(startDate, kDefaultTimezone);
+ endDate = cal.dtz.jsDateToDateTime(endDate, kDefaultTimezone);
+
+ let allDay = document.getElementById("event-all-day").checked;
+ let detailsString = recurrenceRule2String(recurrenceInfo, startDate, endDate, allDay);
+
+ if (!detailsString) {
+ detailsString = cal.l10n.getString("calendar-event-dialog", "ruleTooComplex");
+ }
+ repeatDetails.hidden = false;
+
+ // Now display the string.
+ let lines = detailsString.split("\n");
+ while (repeatDetails.children.length > lines.length) {
+ repeatDetails.lastChild.remove();
+ }
+ let numChilds = repeatDetails.children.length;
+ for (let i = 0; i < lines.length; i++) {
+ if (i >= numChilds) {
+ let newNode = repeatDetails.children[0].cloneNode(true);
+ repeatDetails.appendChild(newNode);
+ }
+ repeatDetails.children[i].value = lines[i];
+ repeatDetails.children[i].setAttribute("tooltiptext", detailsString);
+ }
+ } else {
+ repeatDetails.hidden = true;
+ }
+}
+
+/**
+ * This function does not strictly check if the given attendee has the status
+ * TENTATIVE, but also if he hasn't responded.
+ *
+ * @param aAttendee The attendee to check.
+ * @returns True, if the attendee hasn't responded.
+ */
+function isAttendeeUndecided(aAttendee) {
+ return (
+ aAttendee.participationStatus != "ACCEPTED" &&
+ aAttendee.participationStatus != "DECLINED" &&
+ aAttendee.participationStatus != "DELEGATED"
+ );
+}
+
+/**
+ * Event handler for dblclick on attendee items.
+ *
+ * @param aEvent The popupshowing event
+ */
+function attendeeDblClick(aEvent) {
+ // left mouse button
+ if (aEvent.button == 0) {
+ editAttendees();
+ }
+}
+
+/**
+ * Event handler to set up the attendee-popup. This builds the popup menuitems.
+ *
+ * @param aEvent The popupshowing event
+ */
+function setAttendeeContext(aEvent) {
+ if (window.attendees.length == 0) {
+ // we just need the option to open the attendee dialog in this case
+ let popup = document.getElementById("attendee-popup");
+ let invite = document.getElementById("attendee-popup-invite-menuitem");
+ for (let node of popup.children) {
+ if (node == invite) {
+ node.removeAttribute("hidden");
+ } else {
+ node.setAttribute("hidden", "true");
+ }
+ }
+ } else {
+ if (window.attendees.length > 1) {
+ let removeall = document.getElementById("attendee-popup-removeallattendees-menuitem");
+ removeall.removeAttribute("hidden");
+ }
+ document.getElementById("attendee-popup-sendemail-menuitem").removeAttribute("hidden");
+ document.getElementById("attendee-popup-sendtentativeemail-menuitem").removeAttribute("hidden");
+ document.getElementById("attendee-popup-first-separator").removeAttribute("hidden");
+
+ // setup attendee specific menu items if appropriate otherwise hide respective menu items
+ let mailto = document.getElementById("attendee-popup-emailattendee-menuitem");
+ let remove = document.getElementById("attendee-popup-removeattendee-menuitem");
+ let secondSeparator = document.getElementById("attendee-popup-second-separator");
+ let attId =
+ aEvent.target.getAttribute("attendeeid") ||
+ aEvent.target.parentNode.getAttribute("attendeeid");
+ let attendee = window.attendees.find(aAtt => aAtt.id == attId);
+ if (attendee) {
+ mailto.removeAttribute("hidden");
+ remove.removeAttribute("hidden");
+ secondSeparator.removeAttribute("hidden");
+
+ mailto.setAttribute("label", attendee.toString());
+ mailto.attendee = attendee;
+ remove.attendee = attendee;
+ } else {
+ mailto.setAttribute("hidden", "true");
+ remove.setAttribute("hidden", "true");
+ secondSeparator.setAttribute("hidden", "true");
+ }
+
+ if (window.attendees.some(isAttendeeUndecided)) {
+ document.getElementById("cmd_email_undecided").removeAttribute("disabled");
+ } else {
+ document.getElementById("cmd_email_undecided").setAttribute("disabled", "true");
+ }
+ }
+}
+
+/**
+ * Removes the selected attendee from the window
+ *
+ * @param aAttendee
+ */
+function removeAttendee(aAttendee) {
+ if (aAttendee) {
+ window.attendees = window.attendees.filter(aAtt => aAtt != aAttendee);
+ updateAttendeeInterface();
+ }
+}
+
+/**
+ * Removes all attendees from the window
+ */
+function removeAllAttendees() {
+ window.attendees = [];
+ window.organizer = null;
+ updateAttendeeInterface();
+}
+
+/**
+ * Send Email to all attendees that haven't responded or are tentative.
+ *
+ * @param aAttendees The attendees to check.
+ */
+function sendMailToUndecidedAttendees(aAttendees) {
+ let targetAttendees = aAttendees.filter(isAttendeeUndecided);
+ sendMailToAttendees(targetAttendees);
+}
+
+/**
+ * Send Email to all given attendees.
+ *
+ * @param aAttendees The attendees to send mail to.
+ */
+function sendMailToAttendees(aAttendees) {
+ let toList = cal.email.createRecipientList(aAttendees);
+ let item = saveItem();
+ let emailSubject = cal.l10n.getString("calendar-event-dialog", "emailSubjectReply", [item.title]);
+ let identity = window.calendarItem.calendar.getProperty("imip.identity");
+ cal.email.sendTo(toList, emailSubject, null, identity);
+}
+
+/**
+ * Make sure all fields that may have calendar specific capabilities are updated
+ */
+function updateCapabilities() {
+ updateAttachment();
+ updateConfigState({
+ priority: gConfig.priority,
+ privacy: gConfig.privacy,
+ });
+ updateReminderDetails(
+ document.querySelector(".reminder-details"),
+ document.querySelector(".item-alarm"),
+ getCurrentCalendar()
+ );
+ updateCategoryMenulist();
+}
+
+/**
+ * find out if the User already changed values in the Dialog
+ *
+ * @return: true if the values in the Dialog have changed. False otherwise.
+ */
+function isItemChanged() {
+ let newItem = saveItem();
+ let oldItem = window.calendarItem;
+
+ if (newItem.calendar.id == oldItem.calendar.id && cal.item.compareContent(newItem, oldItem)) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Test if a specific capability is supported
+ *
+ * @param aCap The capability from "capabilities.<aCap>.supported"
+ */
+function capSupported(aCap) {
+ let calendar = getCurrentCalendar();
+ return calendar.getProperty("capabilities." + aCap + ".supported") !== false;
+}
+
+/**
+ * Return the values for a certain capability.
+ *
+ * @param aCap The capability from "capabilities.<aCap>.values"
+ * @returns The values for this capability
+ */
+function capValues(aCap, aDefault) {
+ let calendar = getCurrentCalendar();
+ let vals = calendar.getProperty("capabilities." + aCap + ".values");
+ return vals === null ? aDefault : vals;
+}
+
+/**
+ * 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; sets the warning flag to prevent closing
+ * the dialog when the user enters a wrong until date.
+ */
+function checkUntilDate() {
+ let repeatUntilDate = document.getElementById("repeat-until-datepicker").value;
+ if (repeatUntilDate == "forever") {
+ updateRepeat();
+ // "forever" is never earlier than another date.
+ return;
+ }
+
+ // Check whether the date is valid. Set the correct time just in this case.
+ let untilDate = cal.dtz.jsDateToDateTime(repeatUntilDate, gStartTime.timezone);
+ let startDate = gStartTime.clone();
+ startDate.isDate = true;
+ if (untilDate.compare(startDate) < 0) {
+ // Invalid date: restore the previous date. Since we are checking an
+ // until date, a null value for gUntilDate means repeat "forever".
+ document.getElementById("repeat-until-datepicker").value = gUntilDate
+ ? cal.dtz.dateTimeToJsDate(gUntilDate.getInTimezone(cal.dtz.floating))
+ : "forever";
+ gWarning = true;
+ let callback = function () {
+ // Disable the "Save" and "Save and Close" commands as long as the
+ // warning dialog is showed.
+ enableAcceptCommand(false);
+
+ Services.prompt.alert(
+ null,
+ document.title,
+ cal.l10n.getCalString("warningUntilDateBeforeStart")
+ );
+ enableAcceptCommand(true);
+ gWarning = false;
+ };
+ setTimeout(callback, 1);
+ } else {
+ // Valid date: set the time equal to start date time.
+ gUntilDate = untilDate;
+ updateUntildateRecRule();
+ }
+}
+
+/**
+ * Displays a counterproposal if any
+ */
+function displayCounterProposal() {
+ if (
+ !window.counterProposal ||
+ !window.counterProposal.attendee ||
+ !window.counterProposal.proposal
+ ) {
+ return;
+ }
+
+ let propLabels = document.getElementById("counter-proposal-property-labels");
+ let propValues = document.getElementById("counter-proposal-property-values");
+ let idCounter = 0;
+ let comment;
+
+ for (let proposal of window.counterProposal.proposal) {
+ if (proposal.property == "COMMENT") {
+ if (proposal.proposed && !proposal.original) {
+ comment = proposal.proposed;
+ }
+ } else {
+ let label = lookupCounterLabel(proposal);
+ let value = formatCounterValue(proposal);
+ if (label && value) {
+ // setup label node
+ let propLabel = propLabels.firstElementChild.cloneNode(false);
+ propLabel.id = propLabel.id + "-" + idCounter;
+ propLabel.control = propLabel.control + "-" + idCounter;
+ propLabel.removeAttribute("collapsed");
+ propLabel.value = label;
+ // setup value node
+ let propValue = propValues.firstElementChild.cloneNode(false);
+ propValue.id = propLabel.control;
+ propValue.removeAttribute("collapsed");
+ propValue.value = value;
+ // append nodes
+ propLabels.appendChild(propLabel);
+ propValues.appendChild(propValue);
+ idCounter++;
+ }
+ }
+ }
+
+ let attendeeId =
+ window.counterProposal.attendee.CN ||
+ cal.email.removeMailTo(window.counterProposal.attendee.id || "");
+ let partStat = window.counterProposal.attendee.participationStatus;
+ if (partStat == "DECLINED") {
+ partStat = "counterSummaryDeclined";
+ } else if (partStat == "TENTATIVE") {
+ partStat = "counterSummaryTentative";
+ } else if (partStat == "ACCEPTED") {
+ partStat = "counterSummaryAccepted";
+ } else if (partStat == "DELEGATED") {
+ partStat = "counterSummaryDelegated";
+ } else if (partStat == "NEEDS-ACTION") {
+ partStat = "counterSummaryNeedsAction";
+ } else {
+ cal.LOG("Unexpected partstat " + partStat + " detected.");
+ // we simply reset partStat not display the summary text of the counter box
+ // to avoid the window of death
+ partStat = null;
+ }
+
+ if (idCounter > 0) {
+ if (partStat && attendeeId.length) {
+ document.getElementById("counter-proposal-summary").value = cal.l10n.getString(
+ "calendar-event-dialog",
+ partStat,
+ [attendeeId]
+ );
+ document.getElementById("counter-proposal-summary").removeAttribute("collapsed");
+ }
+ if (comment) {
+ document.getElementById("counter-proposal-comment").value = comment;
+ document.getElementById("counter-proposal-box").removeAttribute("collapsed");
+ }
+ document.getElementById("counter-proposal-box").removeAttribute("collapsed");
+
+ if (window.counterProposal.oldVersion) {
+ // this is a counterproposal to a previous version of the event - we should notify the
+ // user accordingly
+ notifyUser(
+ "counterProposalOnPreviousVersion",
+ cal.l10n.getString("calendar-event-dialog", "counterOnPreviousVersionNotification"),
+ "warn"
+ );
+ }
+ if (window.calendarItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER") == "TRUE") {
+ // this is a counterproposal although the user disallowed countering when sending the
+ // invitation, so we notify the user accordingly
+ notifyUser(
+ "counterProposalOnCounteringDisallowed",
+ cal.l10n.getString("calendar-event-dialog", "counterOnCounterDisallowedNotification"),
+ "warn"
+ );
+ }
+ }
+}
+
+/**
+ * Get the property label to display for a counterproposal based on the respective label used in
+ * the dialog
+ *
+ * @param {JSObject} aProperty The property to check for a label
+ * @returns {string | null} The label to display or null if no such label
+ */
+function lookupCounterLabel(aProperty) {
+ let nodeIds = getPropertyMap();
+ let labels =
+ nodeIds.has(aProperty.property) &&
+ document.getElementsByAttribute("control", nodeIds.get(aProperty.property));
+ let labelValue;
+ if (labels && labels.length) {
+ // as label control assignment should be unique, we can just take the first result
+ labelValue = labels[0].value;
+ } else {
+ cal.LOG(
+ "Unsupported property " +
+ aProperty.property +
+ " detected when setting up counter " +
+ "box labels."
+ );
+ }
+ return labelValue;
+}
+
+/**
+ * Get the property value to display for a counterproposal as currently supported
+ *
+ * @param {JSObject} aProperty The property to check for a label
+ * @returns {string | null} The value to display or null if the property is not supported
+ */
+function formatCounterValue(aProperty) {
+ const dateProps = ["DTSTART", "DTEND"];
+ const stringProps = ["SUMMARY", "LOCATION"];
+
+ let val;
+ if (dateProps.includes(aProperty.property)) {
+ let localTime = aProperty.proposed.getInTimezone(cal.dtz.defaultTimezone);
+ val = cal.dtz.formatter.formatDateTime(localTime);
+ if (gTimezonesEnabled) {
+ let tzone = localTime.timezone.displayName || localTime.timezone.tzid;
+ val += " " + tzone;
+ }
+ } else if (stringProps.includes(aProperty.property)) {
+ val = aProperty.proposed;
+ } else {
+ cal.LOG(
+ "Unsupported property " + aProperty.property + " detected when setting up counter box values."
+ );
+ }
+ return val;
+}
+
+/**
+ * Get a map of property names and labels of currently supported properties
+ *
+ * @returns {Map}
+ */
+function getPropertyMap() {
+ let map = new Map();
+ map.set("SUMMARY", "item-title");
+ map.set("LOCATION", "item-location");
+ map.set("DTSTART", "event-starttime");
+ map.set("DTEND", "event-endtime");
+ return map;
+}
+
+/**
+ * Applies the proposal or original data to the respective dialog fields
+ *
+ * @param {string} aType Either 'proposed' or 'original'
+ */
+function applyValues(aType) {
+ if (!window.counterProposal || (aType != "proposed" && aType != "original")) {
+ return;
+ }
+ let originalBtn = document.getElementById("counter-original-btn");
+ if (originalBtn.disabled) {
+ // The button is disabled when opening the dialog/tab, which makes it more obvious to the
+ // user that he/she needs to apply the proposal values prior to saving & sending.
+ // Once that happened, we leave both options to the user without toggling the button states
+ // to avoid needing to listen to manual changes to do that correctly
+ originalBtn.removeAttribute("disabled");
+ }
+ let nodeIds = getPropertyMap();
+ window.counterProposal.proposal.forEach(aProperty => {
+ if (aProperty.property != "COMMENT") {
+ let valueNode =
+ nodeIds.has(aProperty.property) && document.getElementById(nodeIds.get(aProperty.property));
+ if (valueNode) {
+ if (["DTSTART", "DTEND"].includes(aProperty.property)) {
+ valueNode.value = cal.dtz.dateTimeToJsDate(aProperty[aType]);
+ } else {
+ valueNode.value = aProperty[aType];
+ }
+ }
+ }
+ });
+}
+
+/**
+ * Opens the context menu for the editor element.
+ *
+ * Since its content is, well, content, its contextmenu event is
+ * eaten by the context menu actor before the element's default
+ * context menu processing. Since we know that the editor runs
+ * in the parent process, we can just listen directly to the event.
+ */
+function openEditorContextMenu(event) {
+ let popup = document.getElementById("editorContext");
+ popup.openPopupAtScreen(event.screenX, event.screenY, true, event);
+ event.preventDefault();
+}
+
+// Thunderbird's dialog is mail-centric, but we just want a lightweight prompt.
+function insertLink() {
+ let href = { value: "" };
+ let editor = GetCurrentEditor();
+ let existingLink = editor.getSelectedElement("href");
+ if (existingLink) {
+ editor.selectElement(existingLink);
+ href.value = existingLink.getAttribute("href");
+ }
+ let text = GetSelectionAsText().trim() || href.value || GetString("EmptyHREFError");
+ let title = GetString("Link");
+ if (Services.prompt.prompt(window, title, text, href, null, {})) {
+ if (!href.value) {
+ // Remove the link
+ EditorRemoveTextProperty("href", "");
+ } else if (editor.selection.isCollapsed) {
+ // Insert a link with its href as the text
+ let link = editor.createElementWithDefaults("a");
+ link.setAttribute("href", href.value);
+ link.textContent = href.value;
+ editor.insertElementAtSelection(link, false);
+ } else {
+ // Change the href of the selection
+ let link = editor.createElementWithDefaults("a");
+ link.setAttribute("href", href.value);
+ editor.insertLinkAroundSelection(link);
+ }
+ }
+}
diff --git a/comm/calendar/base/content/item-editing/calendar-item-iframe.xhtml b/comm/calendar/base/content/item-editing/calendar-item-iframe.xhtml
new file mode 100644
index 0000000000..60180a0e4b
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-item-iframe.xhtml
@@ -0,0 +1,1225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<!-- XXX some of these css files may not be needed here. -->
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/calendar-alarms.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/widgets/minimonth.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/calendar-attendees.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/datetimepickers.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/primaryToolbar.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/messenger.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/contextMenu.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/shared/dialogs/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/themeableDialog.css"?>
+
+<!DOCTYPE html [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+<!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd">
+<!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+<!ENTITY % messengercomposeDTD SYSTEM "chrome://messenger/locale/messengercompose/messengercompose.dtd" >
+<!ENTITY % editorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/editorOverlay.dtd">
+%brandDTD; %globalDTD; %calendarDTD; %eventDialogDTD; %messengercomposeDTD; %editorOverlayDTD; ]>
+<html
+ id="calendar-event-dialog-inner"
+ xmlns="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title></title>
+ <link rel="localization" href="toolkit/global/textActions.ftl" />
+ <link rel="localization" href="calendar/calendar-editable-item.ftl" />
+ <link rel="localization" href="calendar/calendar-widgets.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://calendar/content/calendar-dialog-utils.js"></script>
+ <script defer="defer" src="chrome://calendar/content/calendar-ui-utils.js"></script>
+ <script defer="defer" src="chrome://calendar/content/calApplicationUtils.js"></script>
+ <script defer="defer" src="chrome://calendar/content/calendar-statusbar.js"></script>
+ <script defer="defer" src="chrome://calendar/content/widgets/calendar-minimonth.js"></script>
+ <script defer="defer" src="chrome://calendar/content/widgets/datetimepickers.js"></script>
+ <script defer="defer" src="chrome://messenger/content/messengercompose/editor.js"></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/messengercompose/editorUtilities.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/messengercompose/ComposerCommands.js"
+ ></script>
+ <script defer="defer" src="chrome://calendar/content/calendar-item-iframe.js"></script>
+ </head>
+ <html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <commandset id="">
+ <command id="cmd_recurrence" oncommand="editRepeat();" />
+ <command id="cmd_attendees" oncommand="editAttendees();" />
+ <command id="cmd_email" oncommand="sendMailToAttendees(window.attendees);" />
+ <command
+ id="cmd_email_undecided"
+ oncommand="sendMailToUndecidedAttendees(window.attendees);"
+ />
+ <command id="cmd_attach_url" disable-on-readonly="true" oncommand="attachURL()" />
+ <command id="cmd_attach_cloud" disable-on-readonly="true" />
+ <command id="cmd_openAttachment" oncommand="openAttachment()" />
+ <command id="cmd_copyAttachment" oncommand="copyAttachment()" />
+ <command
+ id="cmd_deleteAttachment"
+ disable-on-readonly="true"
+ oncommand="deleteAttachment()"
+ />
+ <command
+ id="cmd_deleteAllAttachments"
+ disable-on-readonly="true"
+ oncommand="deleteAllAttachments()"
+ />
+ <command
+ id="cmd_applyProposal"
+ disable-on-readonly="true"
+ oncommand="applyValues('proposed')"
+ />
+ <command
+ id="cmd_applyOriginal"
+ disable-on-readonly="true"
+ oncommand="applyValues('original')"
+ />
+ </commandset>
+
+ <!-- style related commands that update on creation, and on selection change -->
+ <!-- not using commandupdater directly, as it has to listen to the parent -->
+ <commandset id="styleMenuItems" oncommandupdate="goUpdateComposerMenuItems(this)">
+ <command id="cmd_renderedHTMLEnabler" disabled="true" />
+ <command
+ id="cmd_bold"
+ state="false"
+ oncommand="doStyleUICommand('cmd_bold')"
+ disabled="true"
+ />
+ <command
+ id="cmd_italic"
+ state="false"
+ oncommand="doStyleUICommand('cmd_italic')"
+ disabled="true"
+ />
+ <command
+ id="cmd_underline"
+ state="false"
+ oncommand="doStyleUICommand('cmd_underline')"
+ disabled="true"
+ />
+
+ <command id="cmd_ul" state="false" oncommand="doStyleUICommand('cmd_ul')" disabled="true" />
+ <command id="cmd_ol" state="false" oncommand="doStyleUICommand('cmd_ol')" disabled="true" />
+
+ <command id="cmd_indent" oncommand="goDoCommand('cmd_indent')" disabled="true" />
+ <command id="cmd_outdent" oncommand="goDoCommand('cmd_outdent')" disabled="true" />
+
+ <command id="cmd_align" state="" disabled="true" />
+ </commandset>
+
+ <keyset id="editorKeys">
+ <key id="boldkb" key="&styleBoldCmd.key;" observes="cmd_bold" modifiers="accel" />
+ <key id="italickb" key="&styleItalicCmd.key;" observes="cmd_italic" modifiers="accel" />
+ <key
+ id="underlinekb"
+ key="&styleUnderlineCmd.key;"
+ observes="cmd_underline"
+ modifiers="accel"
+ />
+ <key
+ id="increaseindentkb"
+ key="&increaseIndent.key;"
+ observes="cmd_indent"
+ modifiers="accel"
+ />
+ <key
+ id="decreaseindentkb"
+ key="&decreaseIndent.key;"
+ observes="cmd_outdent"
+ modifiers="accel"
+ />
+ </keyset>
+
+ <menupopup id="editorContext" onpopupshowing="goUpdateGlobalEditMenuItems(true);">
+ <menuitem data-l10n-id="text-action-undo" command="cmd_undo" />
+ <menuseparator />
+ <menuitem data-l10n-id="text-action-cut" command="cmd_cut" />
+ <menuitem data-l10n-id="text-action-copy" command="cmd_copy" />
+ <menuitem data-l10n-id="text-action-paste" command="cmd_paste" />
+ <menuitem data-l10n-id="text-action-delete" command="cmd_item_delete" />
+ <menuseparator />
+ <menuitem data-l10n-id="text-action-select-all" command="cmd_selectAll" />
+ </menupopup>
+
+ <!-- Counter information section -->
+ <hbox id="counter-proposal-box" collapsed="true">
+ <vbox>
+ <description id="counter-proposal-summary" collapsed="true" crop="end" />
+ <hbox id="counter-proposal">
+ <vbox id="counter-proposal-property-labels">
+ <label
+ id="counter-proposal-property-label"
+ control="counter-proposal-property-value"
+ collapsed="true"
+ value=""
+ />
+ </vbox>
+ <vbox id="counter-proposal-property-values">
+ <description
+ id="counter-proposal-property-value"
+ crop="end"
+ collapsed="true"
+ value=""
+ />
+ </vbox>
+ </hbox>
+ <description id="counter-proposal-comment" collapsed="true" crop="end" />
+ </vbox>
+ <spacer flex="1" />
+ <vbox id="counter-buttons">
+ <button
+ id="counter-proposal-btn"
+ label="&counter.button.proposal.label;"
+ crop="end"
+ command="cmd_applyProposal"
+ orient="horizontal"
+ class="counter-buttons"
+ accesskey="&counter.button.proposal.accesskey;"
+ tooltip="&counter.button.proposal.tooltip2;"
+ />
+ <button
+ id="counter-original-btn"
+ label="&counter.button.original.label;"
+ crop="end"
+ command="cmd_applyOriginal"
+ orient="horizontal"
+ disabled="true"
+ class="counter-buttons"
+ accesskey="&counter.button.original.accesskey;"
+ tooltip="&counter.button.original.tooltip2;"
+ />
+ </vbox>
+ </hbox>
+
+ <vbox id="event-dialog-notifications">
+ <!-- notificationbox will be added here lazily. -->
+ </vbox>
+
+ <html:table id="event-grid">
+ <!-- Calendar -->
+ <html:tr>
+ <html:th>
+ <label
+ id="item-calendar-label"
+ value="&event.calendar.label;"
+ accesskey="&event.calendar.accesskey;"
+ control="item-calendar"
+ disable-on-readonly="true"
+ />
+ </html:th>
+ <html:td id="event-grid-item-calendar-td">
+ <menulist id="item-calendar" disable-on-readonly="true" oncommand="updateCalendar();" />
+ </html:td>
+ </html:tr>
+
+ <!-- Title -->
+ <html:tr id="event-grid-title-row">
+ <html:th>
+ <label
+ id="item-title-label"
+ value="&event.title.textbox.label;"
+ accesskey="&event.title.textbox.accesskey;"
+ control="item-title"
+ disable-on-readonly="true"
+ />
+ </html:th>
+ <html:td class="event-input-td">
+ <html:input
+ id="item-title"
+ disable-on-readonly="true"
+ oninput="updateTitle()"
+ aria-labelledby="item-title-label"
+ />
+ </html:td>
+ </html:tr>
+
+ <!-- Location -->
+ <html:tr id="event-grid-location-row">
+ <html:th>
+ <label
+ id="item-location-label"
+ value="&event.location.label;"
+ accesskey="&event.location.accesskey;"
+ control="item-location"
+ disable-on-readonly="true"
+ />
+ </html:th>
+ <html:td class="event-input-td">
+ <html:input
+ id="item-location"
+ disable-on-readonly="true"
+ aria-labelledby="item-location-label"
+ />
+ </html:td>
+ </html:tr>
+
+ <!-- Category -->
+ <html:tr id="event-grid-category-row">
+ <html:th>
+ <hbox id="event-grid-category-labels-box">
+ <label
+ id="item-categories-label"
+ value="&event.categories.label;"
+ accesskey="&event.categories.accesskey;"
+ control="item-categories"
+ disable-on-readonly="true"
+ />
+ </hbox>
+ </html:th>
+ <html:td id="event-grid-category-td">
+ <menulist id="item-categories" type="panel-menulist" disable-on-readonly="true">
+ <menupopup
+ id="item-categories-popup"
+ onpopuphiding="return categoryPopupHiding(event);"
+ >
+ <html:input
+ id="item-categories-textbox"
+ placeholder="&event.categories.textbox.label;"
+ onblur="this.parentNode.removeAttribute('ignorekeys');"
+ onfocus="this.parentNode.setAttribute('ignorekeys', 'true');"
+ onkeypress="categoryTextboxKeypress(event);"
+ />
+ <menuseparator />
+ </menupopup>
+ </menulist>
+ </html:td>
+ </html:tr>
+
+ <html:tr class="separator">
+ <html:td colspan="2"></html:td>
+ </html:tr>
+
+ <!-- All-Day -->
+ <html:tr id="event-grid-allday-row" class="event-only">
+ <html:th> </html:th>
+ <html:td>
+ <checkbox
+ id="event-all-day"
+ disable-on-readonly="true"
+ label="&event.alldayevent.label;"
+ accesskey="&event.alldayevent.accesskey;"
+ oncommand="onUpdateAllDay();"
+ />
+ </html:td>
+ </html:tr>
+
+ <!-- StartDate -->
+ <html:tr id="event-grid-startdate-row">
+ <html:th id="event-grid-startdate-th">
+ <hbox id="event-grid-startdate-label-box" align="center">
+ <label
+ value="&event.from.label;"
+ accesskey="&event.from.accesskey;"
+ control="event-starttime"
+ class="event-only"
+ disable-on-readonly="true"
+ />
+ <label
+ value="&task.from.label;"
+ accesskey="&task.from.accesskey;"
+ control="todo-has-entrydate"
+ class="todo-only"
+ disable-on-readonly="true"
+ />
+ </hbox>
+ </html:th>
+ <html:td id="event-grid-startdate-td">
+ <hbox id="event-grid-startdate-picker-box">
+ <datetimepicker
+ id="event-starttime"
+ class="event-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(true);"
+ />
+ <checkbox
+ id="todo-has-entrydate"
+ class="todo-only checkbox-no-label"
+ disable-on-readonly="true"
+ oncommand="updateEntryDate();"
+ />
+ <datetimepicker
+ id="todo-entrydate"
+ class="todo-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(true);"
+ />
+ <vbox>
+ <hbox>
+ <html:img
+ id="link-image-top"
+ src="chrome://calendar/skin/shared/link-image-top.svg"
+ alt=""
+ class="keepduration-link-image"
+ keep="true"
+ />
+ </hbox>
+ <spacer flex="1" />
+ <toolbarbutton
+ id="keepduration-button"
+ accesskey="&event.dialog.keepDurationButton.accesskey;"
+ oncommand="toggleKeepDuration();"
+ persist="keep"
+ keep="false"
+ tooltiptext="&event.dialog.keepDurationButton.tooltip;"
+ />
+ </vbox>
+ <hbox align="center">
+ <label
+ id="timezone-starttime"
+ class="text-link"
+ collapsed="true"
+ crop="end"
+ disable-on-readonly="true"
+ hyperlink="true"
+ onclick="showTimezonePopup(event, gStartTime.getInTimezone(gStartTimezone), editStartTimezone)"
+ />
+ </hbox>
+ </hbox>
+ </html:td>
+ </html:tr>
+
+ <!-- EndDate -->
+ <html:tr id="event-grid-enddate-row">
+ <html:th>
+ <hbox id="event-grid-enddate-label-box" align="center">
+ <label
+ value="&event.to.label;"
+ accesskey="&event.to.accesskey;"
+ control="event-endtime"
+ class="event-only"
+ disable-on-readonly="true"
+ />
+ <label
+ value="&task.to.label;"
+ accesskey="&task.to.accesskey;"
+ control="todo-has-duedate"
+ class="todo-only"
+ disable-on-readonly="true"
+ />
+ </hbox>
+ </html:th>
+ <html:td id="event-grid-enddate-td">
+ <vbox id="event-grid-enddate-vbox">
+ <hbox id="event-grid-enddate-picker-box">
+ <datetimepicker
+ id="event-endtime"
+ class="event-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(false);"
+ />
+ <checkbox
+ id="todo-has-duedate"
+ class="todo-only checkbox-no-label"
+ disable-on-readonly="true"
+ oncommand="updateDueDate();"
+ />
+ <datetimepicker
+ id="todo-duedate"
+ class="todo-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(false);"
+ />
+ <vbox pack="end">
+ <html:img
+ id="link-image-bottom"
+ alt=""
+ src="chrome://calendar/skin/shared/link-image-bottom.svg"
+ class="keepduration-link-image"
+ />
+ </vbox>
+ <hbox align="center">
+ <label
+ id="timezone-endtime"
+ class="text-link"
+ collapsed="true"
+ crop="end"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="showTimezonePopup(event, gEndTime.getInTimezone(gEndTimezone), editEndTimezone)"
+ />
+ </hbox>
+ </hbox>
+ </vbox>
+ </html:td>
+ </html:tr>
+
+ <html:tr id="event-grid-todo-status-row" class="todo-only">
+ <html:th>
+ <label
+ id="todo-status-label"
+ value="&task.status.label;"
+ accesskey="&task.status.accesskey;"
+ control="todo-status"
+ disable-on-readonly="true"
+ />
+ </html:th>
+ <html:td id="event-grid-todo-status-td">
+ <hbox id="event-grid-todo-status-picker-box" align="center">
+ <menulist
+ id="todo-status"
+ class="todo-only"
+ disable-on-readonly="true"
+ oncommand="updateToDoStatus(this.value);"
+ >
+ <menupopup id="todo-status-menupopup">
+ <menuitem
+ id="todo-status-none-menuitem"
+ label="&newevent.todoStatus.none.label;"
+ value="NONE"
+ />
+ <menuitem
+ id="todo-status-needsaction-menuitem"
+ label="&newevent.status.needsaction.label;"
+ value="NEEDS-ACTION"
+ />
+ <menuitem
+ id="todo-status-inprogress-menuitem"
+ label="&newevent.status.inprogress.label;"
+ value="IN-PROCESS"
+ />
+ <menuitem
+ id="todo-status-completed-menuitem"
+ label="&newevent.status.completed.label;"
+ value="COMPLETED"
+ />
+ <menuitem
+ id="todo-status-canceled-menuitem"
+ label="&newevent.todoStatus.cancelled.label;"
+ value="CANCELLED"
+ />
+ </menupopup>
+ </menulist>
+ <datepicker
+ id="completed-date-picker"
+ class="todo-only"
+ disable-on-readonly="true"
+ disabled="true"
+ value=""
+ />
+ <html:input
+ id="percent-complete-textbox"
+ type="number"
+ class="size3 input-inline"
+ min="0"
+ max="100"
+ disable-on-readonly="true"
+ oninput="updateToDoStatus('percent-changed')"
+ onselect="updateToDoStatus('percent-changed')"
+ />
+ <label
+ id="percent-complete-label"
+ class="todo-only"
+ disable-on-readonly="true"
+ value="&newtodo.percentcomplete.label;"
+ />
+ </hbox>
+ </html:td>
+ </html:tr>
+
+ <!-- Recurrence -->
+ <html:tr id="event-grid-recurrence-row">
+ <html:th>
+ <label
+ value="&event.repeat.label;"
+ accesskey="&event.repeat.accesskey;"
+ control="item-repeat"
+ disable-on-readonly="true"
+ />
+ </html:th>
+ <html:td id="event-grid-recurrence-td">
+ <hbox id="event-grid-recurrence-picker-box" align="center" flex="1">
+ <menulist
+ id="item-repeat"
+ disable-on-readonly="true"
+ oncommand="updateRepeat(null, true)"
+ >
+ <menupopup id="item-repeat-menupopup">
+ <menuitem
+ id="repeat-none-menuitem"
+ label="&event.repeat.does.not.repeat.label;"
+ selected="true"
+ value="none"
+ />
+ <menuitem
+ id="repeat-daily-menuitem"
+ label="&event.repeat.daily.label;"
+ value="daily"
+ />
+ <menuitem
+ id="repeat-weekly-menuitem"
+ label="&event.repeat.weekly.label;"
+ value="weekly"
+ />
+ <menuitem
+ id="repeat-weekday-menuitem"
+ label="&event.repeat.every.weekday.label;"
+ value="every.weekday"
+ />
+ <menuitem
+ id="repeat-biweekly-menuitem"
+ label="&event.repeat.bi.weekly.label;"
+ value="bi.weekly"
+ />
+ <menuitem
+ id="repeat-monthly-menuitem"
+ label="&event.repeat.monthly.label;"
+ value="monthly"
+ />
+ <menuitem
+ id="repeat-yearly-menuitem"
+ label="&event.repeat.yearly.label;"
+ value="yearly"
+ />
+ <menuseparator id="item-repeat-separator" />
+ <menuitem
+ id="repeat-custom-menuitem"
+ label="&event.repeat.custom.label;"
+ value="custom"
+ />
+ </menupopup>
+ </menulist>
+ <hbox id="repeat-untilDate" align="center" hidden="true">
+ <label
+ value="&event.until.label;"
+ accesskey="&event.until.accesskey;"
+ control="repeat-until-datepicker"
+ disable-on-readonly="true"
+ />
+ <datepicker
+ id="repeat-until-datepicker"
+ flex="1"
+ type="forever"
+ disable-on-readonly="true"
+ onchange="if (onLoad.hasLoaded) { checkUntilDate(); }"
+ value=""
+ />
+ </hbox>
+ <vbox id="repeat-details" flex="1" hidden="true">
+ <label
+ id="repeat-details-label"
+ class="text-link"
+ crop="end"
+ disable-on-readonly="true"
+ hyperlink="true"
+ flex="1"
+ onclick="if (onLoad.hasLoaded) { updateRepeat(); }"
+ />
+ </vbox>
+ </hbox>
+ </html:td>
+ </html:tr>
+
+ <html:tr class="separator">
+ <html:td colspan="2"></html:td>
+ </html:tr>
+
+ <!-- Reminder (Alarm) -->
+ <html:tr id="event-grid-alarm-row">
+ <html:th>
+ <label
+ value="&event.reminder.label;"
+ accesskey="&event.reminder.accesskey;"
+ control="item-alarm"
+ disable-on-readonly="true"
+ />
+ </html:th>
+ <html:td>
+ <hbox id="event-grid-alarm-picker-box" align="center">
+ <menulist
+ id="item-alarm"
+ class="item-alarm"
+ disable-on-readonly="true"
+ oncommand="updateReminder()"
+ >
+ <menupopup id="item-alarm-menupopup">
+ <menuitem
+ id="reminder-none-menuitem"
+ label="&event.reminder.none.label;"
+ selected="true"
+ value="none"
+ />
+ <menuseparator id="reminder-none-separator" />
+ <menuitem
+ id="reminder-0minutes-menuitem"
+ label="&event.reminder.0minutes.before.label;"
+ length="0"
+ origin="before"
+ relation="START"
+ unit="minutes"
+ />
+ <menuitem
+ id="reminder-5minutes-menuitem"
+ label="&event.reminder.5minutes.before.label;"
+ length="5"
+ origin="before"
+ relation="START"
+ unit="minutes"
+ />
+ <menuitem
+ id="reminder-15minutes-menuitem"
+ label="&event.reminder.15minutes.before.label;"
+ length="15"
+ origin="before"
+ relation="START"
+ unit="minutes"
+ />
+ <menuitem
+ id="reminder-30minutes-menuitem"
+ label="&event.reminder.30minutes.before.label;"
+ length="30"
+ origin="before"
+ relation="START"
+ unit="minutes"
+ />
+ <menuseparator id="reminder-minutes-separator" />
+ <menuitem
+ id="reminder-1hour-menuitem"
+ label="&event.reminder.1hour.before.label;"
+ length="1"
+ origin="before"
+ relation="START"
+ unit="hours"
+ />
+ <menuitem
+ id="reminder-2hours-menuitem"
+ label="&event.reminder.2hours.before.label;"
+ length="2"
+ origin="before"
+ relation="START"
+ unit="hours"
+ />
+ <menuitem
+ id="reminder-12hours-menuitem"
+ label="&event.reminder.12hours.before.label;"
+ length="12"
+ origin="before"
+ relation="START"
+ unit="hours"
+ />
+ <menuseparator id="reminder-hours-separator" />
+ <menuitem
+ id="reminder-1day-menuitem"
+ label="&event.reminder.1day.before.label;"
+ length="1"
+ origin="before"
+ relation="START"
+ unit="days"
+ />
+ <menuitem
+ id="reminder-2days-menuitem"
+ label="&event.reminder.2days.before.label;"
+ length="2"
+ origin="before"
+ relation="START"
+ unit="days"
+ />
+ <menuitem
+ id="reminder-1week-menuitem"
+ label="&event.reminder.1week.before.label;"
+ length="7"
+ origin="before"
+ relation="START"
+ unit="days"
+ />
+ <menuseparator id="reminder-custom-separator" />
+ <menuitem
+ class="reminder-custom-menuitem"
+ label="&event.reminder.custom.label;"
+ value="custom"
+ />
+ </menupopup>
+ </menulist>
+ <hbox class="reminder-details">
+ <hbox class="alarm-icons-box" align="center" />
+ <!-- TODO oncommand? onkeypress? -->
+ <label
+ class="reminder-multiple-alarms-label text-link"
+ hidden="true"
+ value="&event.reminder.multiple.label;"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="updateReminder()"
+ />
+ <label
+ class="reminder-single-alarms-label text-link"
+ hidden="true"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="updateReminder()"
+ />
+ </hbox>
+ </hbox>
+ </html:td>
+ </html:tr>
+
+ <html:tr id="event-grid-link-separator" class="separator" hidden="hidden">
+ <html:td colspan="2"></html:td>
+ </html:tr>
+
+ <html:tr id="event-grid-link-row" hidden="hidden">
+ <html:th>
+ <label value="&event.url.label;" control="url-link" />
+ </html:th>
+ <html:td>
+ <label
+ id="url-link"
+ class="text-link"
+ onclick="launchBrowser(this.getAttribute('href'), event)"
+ oncommand="launchBrowser(this.getAttribute('href'), event)"
+ crop="end"
+ />
+ </html:td>
+ </html:tr>
+
+ <html:tr class="separator">
+ <html:td colspan="2"></html:td>
+ </html:tr>
+ </html:table>
+
+ <vbox id="event-grid-tab-vbox" flex="1">
+ <!-- Multi purpose tab box -->
+ <hbox id="event-grid-tab-box-row">
+ <tabbox id="event-grid-tabbox" selectedIndex="0" flex="1">
+ <tabs id="event-grid-tabs">
+ <tab
+ id="event-grid-tab-description"
+ label="&event.description.label;"
+ accesskey="&event.description.accesskey;"
+ />
+ <tab
+ id="event-grid-tab-attachments"
+ label="&event.attachments.label;"
+ accesskey="&event.attachments.accesskey;"
+ />
+ <tab
+ id="event-grid-tab-attendees"
+ label="&event.attendees.label;"
+ accesskey="&event.attendees.accesskey;"
+ collapsed="true"
+ />
+ </tabs>
+ <tabpanels id="event-grid-tabpanels" flex="1">
+ <tabpanel id="event-grid-tabpanel-description" orient="vertical">
+ <toolbox id="FormatToolbox" mode="icons">
+ <toolbar
+ id="FormatToolbar"
+ class="inline-toolbar chromeclass-toolbar themeable-full"
+ nowindowdrag="true"
+ >
+ <toolbarbutton
+ id="paragraphButton"
+ type="menu"
+ wantdropmarker="true"
+ class="formatting-button"
+ tooltiptext="&ParagraphSelect.tooltip;"
+ oncommand="goDoCommandParams('cmd_paragraphState', event.target.value);"
+ observes="cmd_renderedHTMLEnabler"
+ >
+ <menupopup id="paragraphPopup">
+ <menuitem id="toolbarmenu_bodyText" label="&bodyTextCmd.label;" value="" />
+ <menuitem id="toolbarmenu_h1" label="&heading1Cmd.label;" value="h1" />
+ <menuitem id="toolbarmenu_h2" label="&heading2Cmd.label;" value="h2" />
+ <menuitem id="toolbarmenu_h3" label="&heading3Cmd.label;" value="h3" />
+ <menuitem id="toolbarmenu_h4" label="&heading4Cmd.label;" value="h4" />
+ <menuitem id="toolbarmenu_h5" label="&heading5Cmd.label;" value="h5" />
+ <menuitem id="toolbarmenu_h6" label="&heading6Cmd.label;" value="h6" />
+ <menuitem
+ id="toolbarmenu_pre"
+ label="&paragraphPreformatCmd.label;"
+ value="pre"
+ />
+ </menupopup>
+ </toolbarbutton>
+ <toolbarseparator class="toolbarseparator-standard" />
+ <toolbarbutton
+ id="boldButton"
+ class="formatting-button"
+ tooltiptext="&boldToolbarCmd.tooltip;"
+ type="checkbox"
+ autoCheck="false"
+ observes="cmd_bold"
+ />
+ <toolbarbutton
+ id="italicButton"
+ class="formatting-button"
+ tooltiptext="&italicToolbarCmd.tooltip;"
+ type="checkbox"
+ autoCheck="false"
+ observes="cmd_italic"
+ />
+ <toolbarbutton
+ id="underlineButton"
+ class="formatting-button"
+ tooltiptext="&underlineToolbarCmd.tooltip;"
+ type="checkbox"
+ autoCheck="false"
+ observes="cmd_underline"
+ />
+ <toolbarseparator class="toolbarseparator-standard" />
+ <toolbarbutton
+ id="ulButton"
+ class="formatting-button"
+ tooltiptext="&bulletListToolbarCmd.tooltip;"
+ type="radio"
+ group="lists"
+ autoCheck="false"
+ observes="cmd_ul"
+ />
+ <toolbarbutton
+ id="olButton"
+ class="formatting-button"
+ tooltiptext="&numberListToolbarCmd.tooltip;"
+ type="radio"
+ group="lists"
+ autoCheck="false"
+ observes="cmd_ol"
+ />
+ <toolbarbutton
+ id="outdentButton"
+ class="formatting-button"
+ tooltiptext="&outdentToolbarCmd.tooltip;"
+ observes="cmd_outdent"
+ />
+ <toolbarbutton
+ id="indentButton"
+ class="formatting-button"
+ tooltiptext="&indentToolbarCmd.tooltip;"
+ observes="cmd_indent"
+ />
+ <toolbarseparator class="toolbarseparator-standard" />
+ <toolbarbutton
+ id="AlignPopupButton"
+ type="menu"
+ wantdropmarker="true"
+ class="formatting-button"
+ tooltiptext="&AlignPopupButton.tooltip;"
+ observes="cmd_align"
+ >
+ <menupopup id="AlignPopup">
+ <menuitem
+ id="AlignLeftItem"
+ class="menuitem-iconic"
+ label="&alignLeft.label;"
+ oncommand="doStatefulCommand('cmd_align', 'left')"
+ tooltiptext="&alignLeftButton.tooltip;"
+ />
+ <menuitem
+ id="AlignCenterItem"
+ class="menuitem-iconic"
+ label="&alignCenter.label;"
+ oncommand="doStatefulCommand('cmd_align', 'center')"
+ tooltiptext="&alignCenterButton.tooltip;"
+ />
+ <menuitem
+ id="AlignRightItem"
+ class="menuitem-iconic"
+ label="&alignRight.label;"
+ oncommand="doStatefulCommand('cmd_align', 'right')"
+ tooltiptext="&alignRightButton.tooltip;"
+ />
+ <menuitem
+ id="AlignJustifyItem"
+ class="menuitem-iconic"
+ label="&alignJustify.label;"
+ oncommand="doStatefulCommand('cmd_align', 'justify')"
+ tooltiptext="&alignJustifyButton.tooltip;"
+ />
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton
+ id="linkButton"
+ class="formatting-button"
+ tooltiptext="&linkToolbarCmd.label;"
+ oncommand="insertLink();"
+ observes="cmd_renderedHTMLEnabler"
+ />
+ <toolbarbutton
+ id="smileButtonMenu"
+ type="menu"
+ wantdropmarker="true"
+ class="formatting-button"
+ tooltiptext="&SmileButton.tooltip;"
+ observes="cmd_renderedHTMLEnabler"
+ >
+ <menupopup id="smileyPopup" class="no-icon-menupopup">
+ <menuitem
+ id="smileySmile"
+ class="menuitem-iconic"
+ label="&#128578; &smiley1Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128578;')"
+ />
+ <menuitem
+ id="smileyFrown"
+ class="menuitem-iconic"
+ label="&#128577; &smiley2Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128577;')"
+ />
+ <menuitem
+ id="smileyWink"
+ class="menuitem-iconic"
+ label="&#128521; &smiley3Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128521;')"
+ />
+ <menuitem
+ id="smileyTongue"
+ class="menuitem-iconic"
+ label="&#128539; &smiley4Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128539;')"
+ />
+ <menuitem
+ id="smileyLaughing"
+ class="menuitem-iconic"
+ label="&#128514; &smiley5Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128514;')"
+ />
+ <menuitem
+ id="smileyEmbarassed"
+ class="menuitem-iconic"
+ label="&#128563; &smiley6Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128563;')"
+ />
+ <menuitem
+ id="smileyUndecided"
+ class="menuitem-iconic"
+ label="&#128533; &smiley7Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128533;')"
+ />
+ <menuitem
+ id="smileySurprise"
+ class="menuitem-iconic"
+ label="&#128558; &smiley8Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128558;')"
+ />
+ <menuitem
+ id="smileyKiss"
+ class="menuitem-iconic"
+ label="&#128536; &smiley9Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128536;')"
+ />
+ <menuitem
+ id="smileyYell"
+ class="menuitem-iconic"
+ label="&#128544; &smiley10Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128544;')"
+ />
+ <menuitem
+ id="smileyCool"
+ class="menuitem-iconic"
+ label="&#128526; &smiley11Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128526;')"
+ />
+ <menuitem
+ id="smileyMoney"
+ class="menuitem-iconic"
+ label="&#129297; &smiley12Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#129297;')"
+ />
+ <menuitem
+ id="smileyFoot"
+ class="menuitem-iconic"
+ label="&#128556; &smiley13Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128556;')"
+ />
+ <menuitem
+ id="smileyInnocent"
+ class="menuitem-iconic"
+ label="&#128519; &smiley14Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128519;')"
+ />
+ <menuitem
+ id="smileyCry"
+ class="menuitem-iconic"
+ label="&#128557; &smiley15Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#128557;')"
+ />
+ <menuitem
+ id="smileySealed"
+ class="menuitem-iconic"
+ label="&#129296; &smiley16Cmd.label;"
+ oncommand="goDoCommandParams('cmd_smiley', '&#129296;')"
+ />
+ </menupopup>
+ </toolbarbutton>
+ </toolbar>
+ </toolbox>
+ <editor
+ id="item-description"
+ type="content"
+ primary="true"
+ editortype="html"
+ oncontextmenu="openEditorContextMenu(event);"
+ disable-on-readonly="true"
+ flex="1"
+ />
+ </tabpanel>
+ <tabpanel id="event-grid-tabpanel-attachments">
+ <vbox flex="1">
+ <richlistbox
+ id="attachment-link"
+ seltype="single"
+ context="attachment-popup"
+ rows="3"
+ flex="1"
+ disable-on-readonly="true"
+ onkeypress="attachmentLinkKeyPress(event)"
+ ondblclick="attachmentDblClick(event);"
+ />
+ </vbox>
+ </tabpanel>
+ <tabpanel id="event-grid-tabpanel-attendees" collapsed="true">
+ <vbox flex="1">
+ <hbox id="item-organizer-row" hidden="true" align="start">
+ <label value="&read.only.organizer.label;" />
+ </hbox>
+ <vbox
+ class="item-attendees-list-container"
+ dialog-type="event"
+ flex="1"
+ context="attendee-popup"
+ oncontextmenu="setAttendeeContext(event)"
+ disable-on-readonly="true"
+ />
+ </vbox>
+ </tabpanel>
+ </tabpanels>
+ <hbox
+ id="notify-options"
+ dialog-type="event"
+ align="center"
+ collapsed="true"
+ disable-on-readonly="true"
+ >
+ <checkbox
+ id="notify-attendees-checkbox"
+ label="&event.attendees.notify.label;"
+ accesskey="&event.attendees.notify.accesskey;"
+ oncommand="changeUndiscloseCheckboxStatus();"
+ pack="start"
+ />
+ <checkbox
+ id="undisclose-attendees-checkbox"
+ label="&event.attendees.notifyundisclosed.label;"
+ accesskey="&event.attendees.notifyundisclosed.accesskey;"
+ tooltiptext="&event.attendees.notifyundisclosed.tooltip;"
+ pack="start"
+ />
+ <checkbox
+ id="disallow-counter-checkbox"
+ label="&event.attendees.disallowcounter.label;"
+ accesskey="&event.attendees.disallowcounter.accesskey;"
+ tooltiptext="&event.attendees.disallowcounter.tooltip;"
+ pack="start"
+ />
+ </hbox>
+ </tabbox>
+ </hbox>
+ </vbox>
+
+ <popupset id="event-dialog-popupset">
+ <menupopup id="attendee-popup">
+ <menuitem
+ id="attendee-popup-invite-menuitem"
+ label="&event.invite.attendees.label;"
+ accesskey="&event.invite.attendees.accesskey;"
+ command="cmd_attendees"
+ disable-on-readonly="true"
+ />
+ <menuitem
+ id="attendee-popup-removeallattendees-menuitem"
+ label="&event.remove.attendees.label2;"
+ accesskey="&event.remove.attendees.accesskey;"
+ oncommand="removeAllAttendees()"
+ disable-on-readonly="true"
+ crop="end"
+ />
+ <menuitem
+ id="attendee-popup-removeattendee-menuitem"
+ label="&event.remove.attendee.label;"
+ accesskey="&event.remove.attendee.accesskey;"
+ oncommand="removeAttendee(event.target.attendee)"
+ crop="end"
+ />
+ <menuseparator id="attendee-popup-first-separator" />
+ <menuitem
+ id="attendee-popup-sendemail-menuitem"
+ label="&event.email.attendees.label;"
+ accesskey="&event.email.attendees.accesskey;"
+ command="cmd_email"
+ />
+ <menuitem
+ id="attendee-popup-sendtentativeemail-menuitem"
+ label="&event.email.tentative.attendees.label;"
+ accesskey="&event.email.tentative.attendees.accesskey;"
+ command="cmd_email_undecided"
+ />
+ <menuseparator id="attendee-popup-second-separator" />
+ <menuitem
+ id="attendee-popup-emailattendee-menuitem"
+ oncommand="sendMailToAttendees([event.target.attendee])"
+ crop="end"
+ />
+ </menupopup>
+ <menupopup id="attachment-popup" onpopupshowing="attachmentClick(event)">
+ <menuitem
+ id="attachment-popup-open"
+ label="&event.attachments.popup.open.label;"
+ accesskey="&event.attachments.popup.open.accesskey;"
+ command="cmd_openAttachment"
+ />
+ <menuitem
+ id="attachment-popup-copy"
+ label="&calendar.copylink.label;"
+ accesskey="&calendar.copylink.accesskey;"
+ command="cmd_copyAttachment"
+ />
+ <menuitem
+ id="attachment-popup-delete"
+ label="&event.attachments.popup.remove.label;"
+ accesskey="&event.attachments.popup.remove.accesskey;"
+ command="cmd_deleteAttachment"
+ />
+ <menuitem
+ id="attachment-popup-deleteAll"
+ label="&event.attachments.popup.removeAll.label;"
+ accesskey="&event.attachments.popup.removeAll.accesskey;"
+ command="cmd_deleteAllAttachments"
+ />
+ <menuseparator />
+ <menuitem
+ id="attachment-popup-attachPage"
+ label="&event.attachments.popup.attachPage.label;"
+ accesskey="&event.attachments.popup.attachPage.accesskey;"
+ command="cmd_attach_url"
+ />
+ </menupopup>
+ <menupopup id="timezone-popup" position="after_start" oncommand="chooseRecentTimezone(event)">
+ <menuitem id="timezone-popup-defaulttz" />
+ <menuseparator id="timezone-popup-menuseparator" />
+ <menuitem
+ id="timezone-custom-menuitem"
+ label="&event.timezone.custom.label;"
+ value="custom"
+ oncommand="this.parentNode.editTimezone()"
+ />
+ </menupopup>
+ </popupset>
+ </html:body>
+</html>
diff --git a/comm/calendar/base/content/item-editing/calendar-item-panel.inc.xhtml b/comm/calendar/base/content/item-editing/calendar-item-panel.inc.xhtml
new file mode 100644
index 0000000000..c567053ee6
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-item-panel.inc.xhtml
@@ -0,0 +1,130 @@
+# 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/.
+
+# This file requires the following localization files:
+# chrome://calendar/locale/global.dtd
+# chrome://calendar/locale/calendar.dtd
+# chrome://calendar/locale/calendar-event-dialog.dtd
+# chrome://lightning/locale/lightning-toolbar.dtd
+
+ <vbox id="calendarItemPanel" collapsed="true">
+
+ <!-- The id of the inner vbox and the iframe are set dynamically
+ when a tab is created. -->
+ <vbox flex="1"
+ id="dummy-calendar-event-dialog-tab"
+ class="calendar-event-dialog-tab">
+
+ <!-- Commands -->
+ <commandset id="itemCommands">
+ <command id="cmd_save"
+ disable-on-readonly="true"
+ oncommand="onCommandSave()"/>
+ <command id="cmd_item_delete"
+ disable-on-readonly="true"
+ oncommand="onCommandDeleteItem()"/>
+
+ <!-- View menu -->
+ <command id="cmd_customize"
+ oncommand="onCommandCustomize()"/>
+
+ <!-- status -->
+ <command id="cmd_status_none"
+ oncommand="editStatus(event.target)"
+ hidden="true"
+ value="NONE"/>
+ <command id="cmd_status_tentative"
+ oncommand="editStatus(event.target)"
+ value="TENTATIVE"/>
+ <command id="cmd_status_confirmed"
+ oncommand="editStatus(event.target)"
+ value="CONFIRMED"/>
+ <command id="cmd_status_cancelled"
+ oncommand="editStatus(event.target)"
+ value="CANCELLED"/>
+
+ <!-- priority -->
+ <command id="cmd_priority_none"
+ oncommand="editPriority(event.target)"
+ value="0"/>
+ <command id="cmd_priority_low"
+ oncommand="editPriority(event.target)"
+ value="9"/>
+ <command id="cmd_priority_normal"
+ oncommand="editPriority(event.target)"
+ value="5"/>
+ <command id="cmd_priority_high"
+ oncommand="editPriority(event.target)"
+ value="1"/>
+
+ <!-- freebusy -->
+ <command id="cmd_showtimeas_busy"
+ oncommand="editShowTimeAs(event.target)"
+ value="OPAQUE"/>
+ <command id="cmd_showtimeas_free"
+ oncommand="editShowTimeAs(event.target)"
+ value="TRANSPARENT"/>
+
+ <!-- attendees -->
+ <command id="cmd_attendees"
+ oncommand="editAttendees();"/>
+
+ <!-- accept, attachments, timezone -->
+ <command id="cmd_accept"
+ disable-on-readonly="true"
+ oncommand="sendMessage({ command: 'onAccept' });"/>
+ <command id="cmd_attach_url"
+ disable-on-readonly="true"
+ oncommand="attachURL()"/>
+ <command id="cmd_attach_cloud"
+ disable-on-readonly="true"/>
+ <command id="cmd_timezone"
+ persist="checked"
+ checked="false"
+ oncommand="toggleTimezoneLinks()"/>
+ </commandset>
+
+ <keyset id="calendar-event-dialog-keyset">
+ <key id="save-key"
+ modifiers="accel, shift"
+ key="&event.dialog.save.key;"
+ command="cmd_save"/>
+ <key id="saveandclose-key"
+ modifiers="accel"
+ key="&event.dialog.saveandclose.key;"
+ command="cmd_accept"/>
+ <key id="saveandclose-key2"
+ modifiers="accel"
+ keycode="VK_RETURN"
+ command="cmd_accept"/>
+ </keyset>
+
+ <toolbox id="event-toolbox"
+ class="mail-toolbox"
+ mode="full"
+ defaultmode="full"
+ iconsize="small"
+ defaulticonsize="small"
+ labelalign="end"
+ defaultlabelalign="end">
+ <toolbarpalette id="event-toolbarpalette">
+#include calendar-item-toolbar.inc.xhtml
+ </toolbarpalette>
+ <!-- toolboxid is set here since we move the toolbar around for tabs -->
+ <toolbar is="customizable-toolbar" id="event-tab-toolbar"
+ toolbarname="&event.menu.view.toolbars.event.label;"
+ accesskey="&event.menu.view.toolbars.event.accesskey;"
+ toolboxid="event-toolbox"
+ class="chromeclass-toolbar inline-toolbar themeable-full"
+ customizable="true"
+ labelalign="end"
+ defaultlabelalign="end"
+ context="event-dialog-toolbar-context-menu"
+ defaultset="button-saveandclose,button-attendees,button-privacy,button-url,button-priority,button-status,button-freebusy,button-delete,spring"/>
+ </toolbox>
+
+ <iframe id="calendar-item-panel-iframe" flex="1"/>
+
+ </vbox>
+ </vbox>
diff --git a/comm/calendar/base/content/item-editing/calendar-item-panel.js b/comm/calendar/base/content/item-editing/calendar-item-panel.js
new file mode 100644
index 0000000000..f2550d509a
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-item-panel.js
@@ -0,0 +1,1143 @@
+/* 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/. */
+
+/* exported onLoadCalendarItemPanel, onCancel, onCommandSave,
+ * onCommandDeleteItem, editAttendees, editPrivacy, editPriority,
+ * editStatus, editShowTimeAs, updateShowTimeAs, editToDoStatus,
+ * postponeTask, toggleTimezoneLinks, attachURL,
+ * onCommandViewToolbar, onCommandCustomize, attachFileByAccountKey,
+ * onUnloadCalendarItemPanel, openNewEvent, openNewTask,
+ * openNewMessage
+ */
+
+/* import-globals-from ../../../../mail/base/content/globalOverlay.js */
+/* import-globals-from ../dialogs/calendar-dialog-utils.js */
+/* import-globals-from ../calendar-ui-utils.js */
+
+// XXX Need to determine which of these we really need here.
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var gTabmail;
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ // gTabmail is null if we are in a dialog window and not in a tab.
+ gTabmail = document.getElementById("tabmail") || null;
+
+ if (!gTabmail) {
+ // In a dialog window the following menu item functions need to be
+ // defined. In a tab they are defined elsewhere. To prevent errors in
+ // the log they are defined here (before the onLoad function is called).
+ /**
+ * Update menu items that rely on focus.
+ */
+ window.goUpdateGlobalEditMenuItems = () => {
+ goUpdateCommand("cmd_undo");
+ goUpdateCommand("cmd_redo");
+ goUpdateCommand("cmd_cut");
+ goUpdateCommand("cmd_copy");
+ goUpdateCommand("cmd_paste");
+ goUpdateCommand("cmd_selectAll");
+ };
+ /**
+ * Update menu items that rely on the current selection.
+ */
+ window.goUpdateSelectEditMenuItems = () => {
+ goUpdateCommand("cmd_cut");
+ goUpdateCommand("cmd_copy");
+ goUpdateCommand("cmd_delete");
+ goUpdateCommand("cmd_selectAll");
+ };
+ /**
+ * Update menu items that relate to undo/redo.
+ */
+ window.goUpdateUndoEditMenuItems = () => {
+ goUpdateCommand("cmd_undo");
+ goUpdateCommand("cmd_redo");
+ };
+ /**
+ * Update menu items that depend on clipboard contents.
+ */
+ window.goUpdatePasteMenuItems = () => {
+ goUpdateCommand("cmd_paste");
+ };
+ }
+ },
+ { once: true }
+);
+
+// Stores the ids of the iframes of currently open event/task tabs, used
+// when window is closed to prompt for saving changes.
+var gItemTabIds = [];
+var gItemTabIdsCopy;
+
+// gConfig is used when switching tabs to restore the state of
+// toolbar, statusbar, and menubar for the current tab.
+var gConfig = {
+ isEvent: null,
+ privacy: null,
+ hasPrivacy: null,
+ calendarType: null,
+ privacyValues: null,
+ priority: null,
+ hasPriority: null,
+ status: null,
+ percentComplete: null,
+ showTimeAs: null,
+ // whether commands are enabled or disabled
+ attendeesCommand: null, // cmd_attendees
+ attachUrlCommand: null, // cmd_attach_url
+ timezonesEnabled: false, // cmd_timezone
+};
+
+/**
+ * Receive an asynchronous message from the iframe.
+ *
+ * @param {MessageEvent} aEvent - Contains the message being received
+ */
+function receiveMessage(aEvent) {
+ if (aEvent.origin !== "chrome://calendar") {
+ return;
+ }
+ switch (aEvent.data.command) {
+ case "initializeItemMenu":
+ initializeItemMenu(aEvent.data.label, aEvent.data.accessKey);
+ break;
+ case "cancelDialog":
+ document.querySelector("dialog").cancelDialog();
+ break;
+ case "closeWindowOrTab":
+ closeWindowOrTab(aEvent.data.iframeId);
+ break;
+ case "showCmdStatusNone":
+ document.getElementById("cmd_status_none").removeAttribute("hidden");
+ break;
+ case "updateTitle":
+ updateTitle(aEvent.data.prefix, aEvent.data.title);
+ break;
+ case "updateConfigState":
+ updateItemTabState(aEvent.data.argument);
+ Object.assign(gConfig, aEvent.data.argument);
+ break;
+ case "enableAcceptCommand":
+ enableAcceptCommand(aEvent.data.argument);
+ break;
+ case "replyToClosingWindowWithTabs":
+ handleWindowClose(aEvent.data.response);
+ break;
+ case "removeDisableAndCollapseOnReadonly":
+ removeDisableAndCollapseOnReadonly();
+ break;
+ case "setElementAttribute": {
+ let arg = aEvent.data.argument;
+ document.getElementById(arg.id)[arg.attribute] = arg.value;
+ break;
+ }
+ case "loadCloudProviders": {
+ loadCloudProviders(aEvent.data.items);
+ break;
+ }
+ case "updateSaveControls": {
+ updateSaveControls(aEvent.data.argument.sendNotSave);
+ break;
+ }
+ }
+}
+
+window.addEventListener("message", receiveMessage);
+
+/**
+ * Send an asynchronous message to an iframe. Additional properties of
+ * aMessage are generally arguments that will be passed to the function
+ * named in aMessage.command. If aIframeId is omitted, the message will
+ * be sent to the iframe of the current tab.
+ *
+ * @param {object} aMessage - Contains the message being sent
+ * @param {string} aMessage.command - The name of a function to call
+ * @param {string} aIframeId - (optional) id of an iframe to send the message to
+ */
+function sendMessage(aMessage, aIframeId) {
+ let iframeId = gTabmail
+ ? aIframeId || gTabmail.currentTabInfo.iframe.id
+ : "calendar-item-panel-iframe";
+ let iframe = document.getElementById(iframeId);
+ iframe.contentWindow.postMessage(aMessage, "*");
+}
+
+/**
+ * When the user closes the window, this function handles prompting them
+ * to save any unsaved changes for any open item tabs, before closing the
+ * window, or not if 'cancel' was clicked. Requires sending and receiving
+ * async messages from the iframes of all open item tabs.
+ *
+ * @param {boolean} aResponse - The response from the tab's iframe
+ */
+function handleWindowClose(aResponse) {
+ if (!aResponse) {
+ // Cancel was clicked, just leave the window open. We're done.
+ } else if (gItemTabIdsCopy.length > 0) {
+ // There are more unsaved changes in tabs to prompt the user about.
+ let nextId = gItemTabIdsCopy.shift();
+ sendMessage({ command: "closingWindowWithTabs", id: nextId }, nextId);
+ } else {
+ // Close the window, there are no more unsaved changes in tabs.
+ window.removeEventListener("close", windowCloseListener);
+ window.close();
+ }
+}
+
+/**
+ * Listener function for window close. We prevent the window from
+ * closing, then for each open tab we prompt the user to save any
+ * unsaved changes with handleWindowClose.
+ *
+ * @param {object} aEvent - The window close event
+ */
+function windowCloseListener(aEvent) {
+ aEvent.preventDefault();
+ gItemTabIdsCopy = gItemTabIds.slice();
+ handleWindowClose(true);
+}
+
+/**
+ * Load handler for the outer parent context that contains the iframe.
+ *
+ * @param {string} aIframeId - (optional) Id of the iframe in this tab
+ * @param {string} aUrl - (optional) The url to load in the iframe
+ */
+function onLoadCalendarItemPanel(aIframeId, aUrl) {
+ let iframe;
+ let iframeSrc;
+ let dialog = document.querySelector("dialog");
+
+ if (!gTabmail) {
+ gTabmail = document.getElementById("tabmail") || null;
+ // This should not happen.
+ if (gTabmail) {
+ console.warn(
+ "gTabmail was undefined on document load and is defined now, that should not happen."
+ );
+ }
+ }
+ if (gTabmail) {
+ // tab case
+ let iframeId = aIframeId || gTabmail.currentTabInfo.iframe.id;
+ iframe = document.getElementById(iframeId);
+ iframeSrc = aUrl;
+
+ // Add a listener to detect close events, prompt user about saving changes.
+ window.addEventListener("close", windowCloseListener);
+ } else {
+ // window dialog case
+ iframe = document.createXULElement("iframe");
+ iframeSrc = "chrome://calendar/content/calendar-item-iframe.xhtml";
+
+ iframe.setAttribute("id", "calendar-item-panel-iframe");
+ iframe.setAttribute("flex", "1");
+
+ // Note: iframe.contentWindow is undefined before the iframe is inserted here.
+ dialog.insertBefore(iframe, document.getElementById("status-bar"));
+
+ iframe.contentWindow.addEventListener(
+ "load",
+ () => {
+ // Push setting dimensions to the end of the event queue.
+ setTimeout(() => {
+ let body = iframe.contentDocument.body;
+ // Make sure the body does not exceed its content's size.
+ body.style.width = "fit-content";
+ body.style.height = "fit-content";
+ let { scrollHeight, scrollWidth } = body;
+ iframe.style.minHeight = `${scrollHeight}px`;
+ iframe.style.minWidth = `${scrollWidth}px`;
+ // Reset the body.
+ body.style.width = null;
+ body.style.height = null;
+ });
+ },
+ { once: true }
+ );
+
+ // Move the args so they are positioned relative to the iframe,
+ // for the window dialog just as they are for the tab.
+ // XXX Should we delete the arguments here in the parent context
+ // so they are only accessible in one place?
+ iframe.contentWindow.arguments = [window.arguments[0]];
+
+ // hide the ok and cancel dialog buttons
+ let accept = dialog.getButton("accept");
+ let cancel = dialog.getButton("cancel");
+ accept.setAttribute("collapsed", "true");
+ cancel.setAttribute("collapsed", "true");
+ cancel.parentNode.setAttribute("collapsed", "true");
+
+ document.addEventListener("dialogaccept", event => {
+ let itemTitle = iframe.contentDocument.documentElement.querySelector("#item-title");
+ // Prevent dialog from saving if title is empty.
+ if (!itemTitle.value) {
+ event.preventDefault();
+ return;
+ }
+ sendMessage({ command: "onAccept" });
+ event.preventDefault();
+ });
+
+ document.addEventListener("dialogcancel", event => {
+ sendMessage({ command: "onCancel" });
+ event.preventDefault();
+ });
+
+ // set toolbar icon color for light or dark themes
+ if (typeof window.ToolbarIconColor !== "undefined") {
+ window.ToolbarIconColor.init();
+ }
+ }
+
+ // event or task
+ let calendarItem = iframe.contentWindow.arguments[0].calendarEvent;
+ gConfig.isEvent = calendarItem.isEvent();
+
+ // for tasks in a window dialog, set the dialog id for CSS selection.
+ if (!gTabmail) {
+ if (gConfig.isEvent) {
+ setDialogId(dialog, "calendar-event-dialog");
+ } else {
+ setDialogId(dialog, "calendar-task-dialog");
+ }
+ }
+
+ // timezones enabled
+ gConfig.timezonesEnabled = getTimezoneCommandState();
+ iframe.contentWindow.gTimezonesEnabled = gConfig.timezonesEnabled;
+
+ // set the iframe src, which loads the iframe's contents
+ iframe.setAttribute("src", iframeSrc);
+}
+
+/**
+ * Unload handler for the outer parent context that contains the iframe.
+ * Currently only called for windows and not tabs.
+ */
+function onUnloadCalendarItemPanel() {
+ if (!gTabmail) {
+ // window dialog case
+ if (typeof window.ToolbarIconColor !== "undefined") {
+ window.ToolbarIconColor.uninit();
+ }
+ }
+}
+
+/**
+ * Updates the UI. Called when a user makes a change and when an
+ * event/task tab is shown. When a tab is shown aArg contains the gConfig
+ * data for that event/task. We pass the full tab state object to the
+ * update functions and they just use the properties they need from it.
+ *
+ * @param {object} aArg - Its properties hold data about the event/task
+ */
+function updateItemTabState(aArg) {
+ const lookup = {
+ privacy: updatePrivacy,
+ priority: updatePriority,
+ status: updateStatus,
+ showTimeAs: updateShowTimeAs,
+ percentComplete: updateMarkCompletedMenuItem,
+ attendeesCommand: updateAttendeesCommand,
+ attachUrlCommand: updateAttachment,
+ timezonesEnabled: updateTimezoneCommand,
+ };
+ for (let key of Object.keys(aArg)) {
+ let procedure = lookup[key];
+ if (procedure) {
+ procedure(aArg);
+ }
+ }
+}
+
+/**
+ * When in a window, set Item-Menu label to Event or Task.
+ *
+ * @param {string} aLabel - The new name for the menu
+ * @param {string} aAccessKey - The access key for the menu
+ */
+function initializeItemMenu(aLabel, aAccessKey) {
+ let menuItem = document.getElementById("item-menu");
+ menuItem.setAttribute("label", aLabel);
+ menuItem.setAttribute("accesskey", aAccessKey);
+}
+
+/**
+ * Handler for when tab is cancelled. (calendar.item.editInTab = true)
+ *
+ * @param {string} aIframeId - The id of the iframe
+ */
+function onCancel(aIframeId) {
+ sendMessage({ command: "onCancel", iframeId: aIframeId }, aIframeId);
+ // We return false to prevent closing of a window until we
+ // can ask the user about saving any unsaved changes.
+ return false;
+}
+
+/**
+ * Closes tab or window. Called after prompting to save any unsaved changes.
+ *
+ * @param {string} aIframeId - The id of the iframe
+ */
+function closeWindowOrTab(iframeId) {
+ if (gTabmail) {
+ if (iframeId) {
+ // Find the tab associated with this iframeId, and close it.
+ let myTabInfo = gTabmail.tabInfo.filter(x => "iframe" in x && x.iframe.id == iframeId)[0];
+ myTabInfo.allowTabClose = true;
+ gTabmail.closeTab(myTabInfo);
+ } else {
+ gTabmail.currentTabInfo.allowTabClose = true;
+ gTabmail.removeCurrentTab();
+ }
+ } else {
+ window.close();
+ }
+}
+
+/**
+ * Handler for saving the event or task.
+ *
+ * @param {boolean} aIsClosing - Is the tab or window closing
+ */
+function onCommandSave(aIsClosing) {
+ sendMessage({ command: "onCommandSave", isClosing: aIsClosing });
+}
+
+/**
+ * Handler for deleting the event or task.
+ */
+function onCommandDeleteItem() {
+ sendMessage({ command: "onCommandDeleteItem" });
+}
+
+/**
+ * Disable the saving options according to the item title.
+ *
+ * @param {boolean} disabled - True if the save options needs to be disabled else false.
+ */
+function disableSaving(disabled) {
+ let cmdSave = document.getElementById("cmd_save");
+ if (cmdSave) {
+ cmdSave.setAttribute("disabled", disabled);
+ }
+ let cmdAccept = document.getElementById("cmd_accept");
+ if (cmdAccept) {
+ cmdAccept.setAttribute("disabled", disabled);
+ }
+}
+
+/**
+ * Update the title of the tab or window.
+ *
+ * @param {string} prefix - The prefix string according to the item.
+ * @param {string} title - The item title.
+ */
+function updateTitle(prefix, title) {
+ disableSaving(!title);
+ let newTitle = prefix + ": " + title;
+ if (gTabmail) {
+ gTabmail.currentTabInfo.title = newTitle;
+ gTabmail.setTabTitle(gTabmail.currentTabInfo);
+ } else {
+ document.title = newTitle;
+ }
+}
+
+/**
+ * Open a new event.
+ */
+function openNewEvent() {
+ sendMessage({ command: "openNewEvent" });
+}
+
+/**
+ * Open a new task.
+ */
+function openNewTask() {
+ sendMessage({ command: "openNewTask" });
+}
+
+/**
+ * Open a new Thunderbird compose window.
+ */
+function openNewMessage() {
+ MailServices.compose.OpenComposeWindow(
+ null,
+ null,
+ null,
+ Ci.nsIMsgCompType.New,
+ Ci.nsIMsgCompFormat.Default,
+ null,
+ null,
+ null
+ );
+}
+
+/**
+ * Handler for edit attendees command.
+ */
+function editAttendees() {
+ sendMessage({ command: "editAttendees" });
+}
+
+/**
+ * Sends a message to set the gConfig values in the iframe.
+ *
+ * @param {object} aArg - Container
+ * @param {string} aArg.privacy - (optional) New privacy value
+ * @param {short} aArg.priority - (optional) New priority value
+ * @param {string} aArg.status - (optional) New status value
+ * @param {string} aArg.showTimeAs - (optional) New showTimeAs / transparency value
+ */
+function editConfigState(aArg) {
+ sendMessage({ command: "editConfigState", argument: aArg });
+}
+
+/**
+ * Handler for changing privacy. aEvent is used for the popup menu
+ * event-privacy-menupopup in the Privacy toolbar button.
+ *
+ * @param {Node} aTarget Has the new privacy in its "value" attribute
+ * @param {XULCommandEvent} aEvent - (optional) the UI element selection event
+ */
+function editPrivacy(aTarget, aEvent) {
+ if (aEvent) {
+ aEvent.stopPropagation();
+ }
+ // "privacy" is indeed the correct attribute to use here
+ let newPrivacy = aTarget.getAttribute("privacy");
+ editConfigState({ privacy: newPrivacy });
+}
+
+/**
+ * Updates the UI according to the privacy setting and the selected
+ * calendar. If the selected calendar does not support privacy or only
+ * certain values, these are removed from the UI. This function should
+ * be called any time that privacy setting is updated.
+ *
+ * @param {object} aArg Contains privacy properties
+ * @param {string} aArg.privacy The new privacy value
+ * @param {boolean} aArg.hasPrivacy Whether privacy is supported
+ * @param {string} aArg.calendarType The type of calendar
+ * @param {string[]} aArg.privacyValues The possible privacy values
+ */
+function updatePrivacy(aArg) {
+ if (aArg.hasPrivacy) {
+ // Update privacy capabilities (toolbar)
+ let menupopup = document.getElementById("event-privacy-menupopup");
+ if (menupopup) {
+ // Only update the toolbar if the button is actually there
+ for (let node of menupopup.children) {
+ let currentProvider = node.getAttribute("provider");
+ if (node.hasAttribute("privacy")) {
+ let currentPrivacyValue = node.getAttribute("privacy");
+ // Collapsed state
+
+ // Hide the toolbar if the value is unsupported or is for a
+ // specific provider and doesn't belong to the current provider.
+ if (
+ !aArg.privacyValues.includes(currentPrivacyValue) ||
+ (currentProvider && currentProvider != aArg.calendarType)
+ ) {
+ node.setAttribute("collapsed", "true");
+ } else {
+ node.removeAttribute("collapsed");
+ }
+
+ // Checked state
+ if (aArg.privacy == currentPrivacyValue) {
+ node.setAttribute("checked", "true");
+ } else {
+ node.removeAttribute("checked");
+ }
+ }
+ }
+ }
+
+ // Update privacy capabilities (menu) but only if we are not in a tab.
+ if (!gTabmail) {
+ menupopup = document.getElementById("options-privacy-menupopup");
+ for (let node of menupopup.children) {
+ let currentProvider = node.getAttribute("provider");
+ if (node.hasAttribute("privacy")) {
+ let currentPrivacyValue = node.getAttribute("privacy");
+ // Collapsed state
+
+ // Hide the menu if the value is unsupported or is for a
+ // specific provider and doesn't belong to the current provider.
+ if (
+ !aArg.privacyValues.includes(currentPrivacyValue) ||
+ (currentProvider && currentProvider != aArg.calendarType)
+ ) {
+ node.setAttribute("collapsed", "true");
+ } else {
+ node.removeAttribute("collapsed");
+ }
+
+ // Checked state
+ if (aArg.privacy == currentPrivacyValue) {
+ node.setAttribute("checked", "true");
+ } else {
+ node.removeAttribute("checked");
+ }
+ }
+ }
+ }
+
+ // Update privacy capabilities (statusbar)
+ let privacyPanel = document.getElementById("status-privacy");
+ let hasAnyPrivacyValue = false;
+ for (let node of privacyPanel.children) {
+ let currentProvider = node.getAttribute("provider");
+ if (node.hasAttribute("privacy")) {
+ let currentPrivacyValue = node.getAttribute("privacy");
+
+ // Hide the panel if the value is unsupported or is for a
+ // specific provider and doesn't belong to the current provider,
+ // or is not the items privacy value
+ if (
+ !aArg.privacyValues.includes(currentPrivacyValue) ||
+ (currentProvider && currentProvider != aArg.calendarType) ||
+ aArg.privacy != currentPrivacyValue
+ ) {
+ node.setAttribute("collapsed", "true");
+ } else {
+ node.removeAttribute("collapsed");
+ hasAnyPrivacyValue = true;
+ }
+ }
+ }
+
+ // Don't show the status panel if no valid privacy value is selected
+ if (hasAnyPrivacyValue) {
+ privacyPanel.removeAttribute("collapsed");
+ } else {
+ privacyPanel.setAttribute("collapsed", "true");
+ }
+ } else {
+ // aArg.hasPrivacy is false
+ document.getElementById("button-privacy").disabled = true;
+ document.getElementById("status-privacy").collapsed = true;
+ // in the tab case the menu item does not exist
+ let privacyMenuItem = document.getElementById("options-privacy-menu");
+ if (privacyMenuItem) {
+ document.getElementById("options-privacy-menu").disabled = true;
+ }
+ }
+}
+
+/**
+ * Handler to change the priority.
+ *
+ * @param {Node} aTarget - Has the new priority in its "value" attribute
+ */
+function editPriority(aTarget) {
+ let newPriority = parseInt(aTarget.getAttribute("value"), 10);
+ editConfigState({ priority: newPriority });
+}
+
+/**
+ * Updates the dialog controls related to priority.
+ *
+ * @param {object} aArg Contains priority properties
+ * @param {string} aArg.priority The new priority value
+ * @param {boolean} aArg.hasPriority - Whether priority is supported
+ */
+function updatePriority(aArg) {
+ // Set up capabilities
+ if (document.getElementById("button-priority")) {
+ document.getElementById("button-priority").disabled = !aArg.hasPriority;
+ }
+ if (!gTabmail && document.getElementById("options-priority-menu")) {
+ document.getElementById("options-priority-menu").disabled = !aArg.hasPriority;
+ }
+ document.getElementById("status-priority").collapsed = !aArg.hasPriority;
+
+ if (aArg.hasPriority) {
+ let priorityLevel = "none";
+ if (aArg.priority >= 1 && aArg.priority <= 4) {
+ priorityLevel = "high";
+ } else if (aArg.priority == 5) {
+ priorityLevel = "normal";
+ } else if (aArg.priority >= 6 && aArg.priority <= 9) {
+ priorityLevel = "low";
+ }
+
+ let priorityNone = document.getElementById("cmd_priority_none");
+ let priorityLow = document.getElementById("cmd_priority_low");
+ let priorityNormal = document.getElementById("cmd_priority_normal");
+ let priorityHigh = document.getElementById("cmd_priority_high");
+
+ priorityNone.setAttribute("checked", priorityLevel == "none" ? "true" : "false");
+ priorityLow.setAttribute("checked", priorityLevel == "low" ? "true" : "false");
+ priorityNormal.setAttribute("checked", priorityLevel == "normal" ? "true" : "false");
+ priorityHigh.setAttribute("checked", priorityLevel == "high" ? "true" : "false");
+
+ // Status bar panel
+ let priorityPanel = document.getElementById("status-priority");
+ let image = priorityPanel.querySelector("img");
+ if (priorityLevel === "none") {
+ // If the priority is none, don't show the status bar panel
+ priorityPanel.setAttribute("collapsed", "true");
+ image.removeAttribute("data-l10n-id");
+ image.setAttribute("alt", "");
+ image.removeAttribute("src");
+ } else {
+ priorityPanel.removeAttribute("collapsed");
+ image.setAttribute("alt", cal.l10n.getString("calendar", `${priorityLevel}Priority`));
+ image.setAttribute(
+ "src",
+ `chrome://calendar/skin/shared/statusbar-priority-${priorityLevel}.svg`
+ );
+ }
+ }
+}
+
+/**
+ * Handler for changing the status.
+ *
+ * @param {Node} aTarget - Has the new status in its "value" attribute
+ */
+function editStatus(aTarget) {
+ let newStatus = aTarget.getAttribute("value");
+ editConfigState({ status: newStatus });
+}
+
+/**
+ * Update the dialog controls related to status.
+ *
+ * @param {object} aArg - Contains the new status value
+ * @param {string} aArg.status - The new status value
+ */
+function updateStatus(aArg) {
+ const statusLabels = [
+ "status-status-tentative-label",
+ "status-status-confirmed-label",
+ "status-status-cancelled-label",
+ ];
+ const commands = [
+ "cmd_status_none",
+ "cmd_status_tentative",
+ "cmd_status_confirmed",
+ "cmd_status_cancelled",
+ ];
+ let found = false;
+ document.getElementById("status-status").collapsed = true;
+ commands.forEach((aElement, aIndex, aArray) => {
+ let node = document.getElementById(aElement);
+ let matches = node.getAttribute("value") == aArg.status;
+ found = found || matches;
+
+ node.setAttribute("checked", matches ? "true" : "false");
+
+ if (aIndex > 0) {
+ statusLabels[aIndex - 1].hidden = !matches;
+ if (matches) {
+ document.getElementById("status-status").collapsed = false;
+ }
+ }
+ });
+ if (!found) {
+ // The current Status value is invalid. Change the status to
+ // "not specified" and update the status again.
+ sendMessage({ command: "editStatus", value: "NONE" });
+ }
+}
+
+/**
+ * Handler for changing the transparency.
+ *
+ * @param {Node} aTarget - Has the new transparency in its "value" attribute
+ */
+function editShowTimeAs(aTarget) {
+ let newValue = aTarget.getAttribute("value");
+ editConfigState({ showTimeAs: newValue });
+}
+
+/**
+ * Update the dialog controls related to transparency.
+ *
+ * @param {object} aArg - Contains the new transparency value
+ * @param {string} aArg.showTimeAs - The new transparency value
+ */
+function updateShowTimeAs(aArg) {
+ let showAsBusy = document.getElementById("cmd_showtimeas_busy");
+ let showAsFree = document.getElementById("cmd_showtimeas_free");
+
+ showAsBusy.setAttribute("checked", aArg.showTimeAs == "OPAQUE" ? "true" : "false");
+ showAsFree.setAttribute("checked", aArg.showTimeAs == "TRANSPARENT" ? "true" : "false");
+
+ document.getElementById("status-freebusy").collapsed =
+ aArg.showTimeAs != "OPAQUE" && aArg.showTimeAs != "TRANSPARENT";
+ document.getElementById("status-freebusy-free-label").hidden = aArg.showTimeAs == "OPAQUE";
+ document.getElementById("status-freebusy-busy-label").hidden = aArg.showTimeAs == "TRANSPARENT";
+}
+
+/**
+ * Change the task percent complete (and thus task status).
+ *
+ * @param {short} aPercentComplete - The new percent complete value
+ */
+function editToDoStatus(aPercentComplete) {
+ sendMessage({ command: "editToDoStatus", value: aPercentComplete });
+}
+
+/**
+ * Check or uncheck the "Mark updated" menu item in "Events and Tasks"
+ * menu based on the percent complete value.
+ *
+ * @param {object} aArg - Container
+ * @param {short} aArg.percentComplete - The percent complete value
+ */
+function updateMarkCompletedMenuItem(aArg) {
+ // Command only for tab case, function only to be executed in dialog windows.
+ if (gTabmail) {
+ let completedCommand = document.getElementById("calendar_toggle_completed_command");
+ let isCompleted = aArg.percentComplete == 100;
+ completedCommand.setAttribute("checked", isCompleted);
+ }
+}
+
+/**
+ * Postpone the task's start date/time and due date/time. ISO 8601
+ * format: "PT1H", "P1D", and "P1W" are 1 hour, 1 day, and 1 week. (We
+ * use this format intentionally instead of a calIDuration object because
+ * those objects cannot be serialized for message passing with iframes.)
+ *
+ * @param {string} aDuration - A duration in ISO 8601 format
+ */
+function postponeTask(aDuration) {
+ sendMessage({ command: "postponeTask", value: aDuration });
+}
+
+/**
+ * Get the timezone button state.
+ *
+ * @returns {boolean} True is active/checked and false is inactive/unchecked
+ */
+function getTimezoneCommandState() {
+ let cmdTimezone = document.getElementById("cmd_timezone");
+ return cmdTimezone.getAttribute("checked") == "true";
+}
+
+/**
+ * Set the timezone button state. Used to keep the toolbar button in
+ * sync when switching tabs.
+ *
+ * @param {object} aArg - Contains timezones property
+ * @param {boolean} aArg.timezonesEnabled - Are timezones enabled?
+ */
+function updateTimezoneCommand(aArg) {
+ let cmdTimezone = document.getElementById("cmd_timezone");
+ cmdTimezone.setAttribute("checked", aArg.timezonesEnabled);
+ gConfig.timezonesEnabled = aArg.timezonesEnabled;
+}
+
+/**
+ * Toggles the command that allows enabling the timezone links in the dialog.
+ */
+function toggleTimezoneLinks() {
+ let cmdTimezone = document.getElementById("cmd_timezone");
+ let currentState = getTimezoneCommandState();
+ cmdTimezone.setAttribute("checked", currentState ? "false" : "true");
+ gConfig.timezonesEnabled = !currentState;
+ sendMessage({ command: "toggleTimezoneLinks", checked: !currentState });
+}
+
+/**
+ * Prompts the user to attach an url to this item.
+ */
+function attachURL() {
+ sendMessage({ command: "attachURL" });
+}
+
+/**
+ * Updates dialog controls related to item attachments.
+ *
+ * @param {object} aArg Container
+ * @param {boolean} aArg.attachUrlCommand - Enable the attach url command?
+ */
+function updateAttachment(aArg) {
+ document.getElementById("cmd_attach_url").setAttribute("disabled", !aArg.attachUrlCommand);
+}
+
+/**
+ * Updates attendees command enabled/disabled state.
+ *
+ * @param {object} aArg Container
+ * @param {boolean} aArg.attendeesCommand - Enable the attendees command?
+ */
+function updateAttendeesCommand(aArg) {
+ document.getElementById("cmd_attendees").setAttribute("disabled", !aArg.attendeesCommand);
+}
+
+/**
+ * Enables/disables the commands cmd_accept and cmd_save related to the
+ * save operation.
+ *
+ * @param {boolean} aEnable - Enable the commands?
+ */
+function enableAcceptCommand(aEnable) {
+ document.getElementById("cmd_accept").setAttribute("disabled", !aEnable);
+ document.getElementById("cmd_save").setAttribute("disabled", !aEnable);
+}
+
+/**
+ * Enable and un-collapse all elements that are disable-on-readonly and
+ * collapse-on-readonly.
+ */
+function removeDisableAndCollapseOnReadonly() {
+ let enableElements = document.getElementsByAttribute("disable-on-readonly", "true");
+ for (let element of enableElements) {
+ element.removeAttribute("disabled");
+ }
+ let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true");
+ for (let element of collapseElements) {
+ element.removeAttribute("collapsed");
+ }
+}
+
+/**
+ * Handler to toggle toolbar visibility.
+ *
+ * @param {string} aToolbarId - The id of the toolbar node to toggle
+ * @param {string} aMenuitemId - The corresponding menuitem in the view menu
+ */
+function onCommandViewToolbar(aToolbarId, aMenuItemId) {
+ let toolbar = document.getElementById(aToolbarId);
+ let menuItem = document.getElementById(aMenuItemId);
+
+ if (!toolbar || !menuItem) {
+ return;
+ }
+
+ let toolbarCollapsed = toolbar.collapsed;
+
+ // toggle the checkbox
+ menuItem.setAttribute("checked", toolbarCollapsed);
+
+ // toggle visibility of the toolbar
+ toolbar.collapsed = !toolbarCollapsed;
+
+ Services.xulStore.persist(toolbar, "collapsed");
+ Services.xulStore.persist(menuItem, "checked");
+}
+
+/**
+ * Called after the customize toolbar dialog has been closed by the
+ * user. We need to restore the state of all buttons and commands of
+ * all customizable toolbars.
+ *
+ * @param {boolean} aToolboxChanged - When true the toolbox has changed
+ */
+function dialogToolboxCustomizeDone(aToolboxChanged) {
+ // Re-enable menu items (disabled during toolbar customization).
+ let menubarId = gTabmail ? "mail-menubar" : "event-menubar";
+ let menubar = document.getElementById(menubarId);
+ for (let menuitem of menubar.children) {
+ menuitem.removeAttribute("disabled");
+ }
+
+ // make sure our toolbar buttons have the correct enabled state restored to them...
+ document.commandDispatcher.updateCommands("itemCommands");
+
+ // Enable the toolbar context menu items
+ document.getElementById("cmd_customize").removeAttribute("disabled");
+
+ // Update privacy items to make sure the toolbarbutton's menupopup is set
+ // correctly
+ updatePrivacy(gConfig);
+}
+
+/**
+ * Handler to start the customize toolbar dialog for the event dialog's toolbar.
+ */
+function onCommandCustomize() {
+ // install the callback that handles what needs to be
+ // done after a toolbar has been customized.
+ let toolboxId = "event-toolbox";
+
+ let toolbox = document.getElementById(toolboxId);
+ toolbox.customizeDone = dialogToolboxCustomizeDone;
+
+ // Disable menu items during toolbar customization.
+ let menubarId = gTabmail ? "mail-menubar" : "event-menubar";
+ let menubar = document.getElementById(menubarId);
+ for (let menuitem of menubar.children) {
+ menuitem.setAttribute("disabled", true);
+ }
+
+ // Disable the toolbar context menu items
+ document.getElementById("cmd_customize").setAttribute("disabled", "true");
+
+ let wintype = document.documentElement.getAttribute("windowtype");
+ wintype = wintype.replace(/:/g, "");
+
+ window.openDialog(
+ "chrome://messenger/content/customizeToolbar.xhtml",
+ "CustomizeToolbar" + wintype,
+ "chrome,all,dependent",
+ document.getElementById(toolboxId), // toolbox dom node
+ false, // is mode toolbar yes/no?
+ null, // callback function
+ "dialog"
+ ); // name of this mode
+}
+
+/**
+ * Add menu items to the UI for attaching files using a cloud provider.
+ *
+ * @param {object[]} aItemObjects - Array of objects that each contain
+ * data to create a menuitem
+ */
+function loadCloudProviders(aItemObjects) {
+ /**
+ * Deletes any existing menu items in aParentNode that have a
+ * cloudProviderAccountKey attribute.
+ *
+ * @param {Node} aParentNode - A menupopup containing menu items
+ */
+ function deleteAlreadyExisting(aParentNode) {
+ for (let node of aParentNode.children) {
+ if (node.cloudProviderAccountKey) {
+ aParentNode.removeChild(node);
+ }
+ }
+ }
+
+ // Delete any existing menu items with a cloudProviderAccountKey,
+ // needed for the tab case to prevent duplicate menu items, and
+ // helps keep the menu items current.
+ let toolbarPopup = document.getElementById("button-attach-menupopup");
+ if (toolbarPopup) {
+ deleteAlreadyExisting(toolbarPopup);
+ }
+ let optionsPopup = document.getElementById("options-attachments-menupopup");
+ if (optionsPopup) {
+ deleteAlreadyExisting(optionsPopup);
+ }
+
+ for (let itemObject of aItemObjects) {
+ // Create a menu item.
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("label", itemObject.label);
+ item.setAttribute("observes", "cmd_attach_cloud");
+ item.setAttribute(
+ "oncommand",
+ "attachFileByAccountKey(event.target.cloudProviderAccountKey); event.stopPropagation();"
+ );
+
+ if (itemObject.class) {
+ item.setAttribute("class", itemObject.class);
+ item.setAttribute("image", itemObject.image);
+ }
+
+ // Add the menu item to the UI.
+ if (toolbarPopup) {
+ toolbarPopup.appendChild(item.cloneNode(true)).cloudProviderAccountKey =
+ itemObject.cloudProviderAccountKey;
+ }
+ if (optionsPopup) {
+ // This one doesn't need to clone, just use the item itself.
+ optionsPopup.appendChild(item).cloudProviderAccountKey = itemObject.cloudProviderAccountKey;
+ }
+ }
+}
+
+/**
+ * Send a message to attach a file using a given cloud provider,
+ * to be identified by the cloud provider's accountKey.
+ *
+ * @param {string} aAccountKey - The accountKey for a cloud provider
+ */
+function attachFileByAccountKey(aAccountKey) {
+ sendMessage({ command: "attachFileByAccountKey", accountKey: aAccountKey });
+}
+
+/**
+ * Updates the save controls depending on whether the event has attendees
+ *
+ * @param {boolean} aSendNotSave
+ */
+function updateSaveControls(aSendNotSave) {
+ if (window.calItemSaveControls && window.calItemSaveControls.state == aSendNotSave) {
+ return;
+ }
+
+ let saveBtn = document.getElementById("button-save");
+ let saveandcloseBtn = document.getElementById("button-saveandclose");
+ let saveMenu =
+ document.getElementById("item-save-menuitem") ||
+ document.getElementById("calendar-save-menuitem");
+ let saveandcloseMenu =
+ document.getElementById("item-saveandclose-menuitem") ||
+ document.getElementById("calendar-save-and-close-menuitem");
+
+ // we store the initial label and tooltip values to be able to reset later
+ if (!window.calItemSaveControls) {
+ window.calItemSaveControls = {
+ state: false,
+ saveMenu: { label: saveMenu.label },
+ saveandcloseMenu: { label: saveandcloseMenu.label },
+ saveBtn: null,
+ saveandcloseBtn: null,
+ };
+ // we need to check for each button whether it exists since toolbarbuttons
+ // can be removed by customizing
+ if (saveBtn) {
+ window.window.calItemSaveControls.saveBtn = {
+ label: saveBtn.label,
+ tooltiptext: saveBtn.tooltip,
+ };
+ }
+ if (saveandcloseBtn) {
+ window.window.calItemSaveControls.saveandcloseBtn = {
+ label: saveandcloseBtn.label,
+ tooltiptext: saveandcloseBtn.tooltip,
+ };
+ }
+ }
+
+ // we update labels and tooltips but leave accesskeys as they are
+ window.calItemSaveControls.state = aSendNotSave;
+ if (aSendNotSave) {
+ if (saveBtn) {
+ saveBtn.label = cal.l10n.getString("calendar-event-dialog", "saveandsendButtonLabel");
+ saveBtn.tooltiptext = cal.l10n.getString("calendar-event-dialog", "saveandsendButtonTooltip");
+ saveBtn.setAttribute("mode", "send");
+ }
+ if (saveandcloseBtn) {
+ saveandcloseBtn.label = cal.l10n.getString(
+ "calendar-event-dialog",
+ "sendandcloseButtonLabel"
+ );
+ saveandcloseBtn.tooltiptext = cal.l10n.getString(
+ "calendar-event-dialog",
+ "sendandcloseButtonTooltip"
+ );
+ saveandcloseBtn.setAttribute("mode", "send");
+ }
+ saveMenu.label = cal.l10n.getString("calendar-event-dialog", "saveandsendMenuLabel");
+ saveandcloseMenu.label = cal.l10n.getString("calendar-event-dialog", "sendandcloseMenuLabel");
+ } else {
+ if (saveBtn) {
+ saveBtn.label = window.calItemSaveControls.saveBtn.label;
+ saveBtn.tooltiptext = window.calItemSaveControls.saveBtn.tooltip;
+ saveBtn.removeAttribute("mode");
+ }
+ if (saveandcloseBtn) {
+ saveandcloseBtn.label = window.calItemSaveControls.saveandcloseBtn.label;
+ saveandcloseBtn.tooltiptext = window.calItemSaveControls.saveandcloseBtn.tooltip;
+ saveandcloseBtn.removeAttribute("mode");
+ }
+ saveMenu.label = window.calItemSaveControls.saveMenu.label;
+ saveandcloseMenu.label = window.calItemSaveControls.saveandcloseMenu.label;
+ }
+}
diff --git a/comm/calendar/base/content/item-editing/calendar-item-toolbar.inc.xhtml b/comm/calendar/base/content/item-editing/calendar-item-toolbar.inc.xhtml
new file mode 100644
index 0000000000..5d0c744086
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-item-toolbar.inc.xhtml
@@ -0,0 +1,164 @@
+# 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/.
+
+ <toolbarbutton id="button-save"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.toolbar.save.label2;"
+ tooltiptext="&event.toolbar.save.tooltip2;"
+ command="cmd_save"/>
+ <toolbarbutton id="button-saveandclose"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.toolbar.saveandclose.label;"
+ tooltiptext="&event.toolbar.saveandclose.tooltip;"
+ command="cmd_accept"/>
+ <toolbarbutton id="button-attendees"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1 event-only"
+ disable-on-readonly="true"
+ label="&event.toolbar.attendees.label;"
+ tooltiptext="&event.toolbar.attendees.tooltip;"
+ command="cmd_attendees"/>
+ <toolbarbutton id="button-privacy"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ type="menu"
+ wantdropmarker="true"
+ disable-on-readonly="true"
+ label="&event.toolbar.privacy.label;"
+ tooltiptext="&event.toolbar.privacy.tooltip;">
+ <menupopup id="event-privacy-menupopup">
+ <menuitem id="event-privacy-public-menuitem"
+ name="event-privacy-group"
+ label="&event.menu.options.privacy.public.label;"
+ type="radio"
+ privacy="PUBLIC"
+ oncommand="editPrivacy(this, event)"/>
+ <menuitem id="event-privacy-confidential-menuitem"
+ name="event-privacy-group"
+ label="&event.menu.options.privacy.confidential.label;"
+ type="radio"
+ privacy="CONFIDENTIAL"
+ oncommand="editPrivacy(this, event)"/>
+ <menuitem id="event-privacy-private-menuitem"
+ name="event-privacy-group"
+ label="&event.menu.options.privacy.private.label;"
+ type="radio"
+ privacy="PRIVATE"
+ oncommand="editPrivacy(this, event)"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-url"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ type="menu"
+ wantdropmarker="true"
+ label="&event.attachments.menubutton.label;"
+ tooltiptext="&event.toolbar.attachments.tooltip;"
+ disable-on-readonly="true">
+ <menupopup id="button-attach-menupopup">
+ <menuitem id="button-attach-url"
+ label="&event.attachments.url.label;"
+ command="cmd_attach_url"/>
+ <!-- Additional items are added here in loadCloudProviders(). -->
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-delete"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.toolbar.delete.label;"
+ tooltiptext="&event.toolbar.delete.tooltip;"
+ command="cmd_item_delete"
+ disable-on-readonly="true"/>
+ <toolbarbutton id="button-priority"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ type="menu"
+ wantdropmarker="true"
+ disable-on-readonly="true"
+ label="&event.menu.options.priority2.label;"
+ tooltiptext="&event.toolbar.priority.tooltip;">
+ <menupopup id="event-priority-menupopup">
+ <menuitem id="event-priority-none-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.notspecified.label;"
+ type="radio"
+ command="cmd_priority_none"/>
+ <menuitem id="event-priority-low-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.low.label;"
+ type="radio"
+ command="cmd_priority_low"/>
+ <menuitem id="event-priority-normal-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.normal.label;"
+ type="radio"
+ command="cmd_priority_normal"/>
+ <menuitem id="event-priority-high-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.high.label;"
+ type="radio"
+ command="cmd_priority_high"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-status"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1 event-only"
+ type="menu"
+ wantdropmarker="true"
+ disable-on-readonly="true"
+ label="&newevent.status.label;"
+ tooltiptext="&event.toolbar.status.tooltip;">
+ <menupopup id="event-status-menupopup">
+ <menuitem id="event-status-none-menuitem"
+ name="event-status-group"
+ label="&newevent.eventStatus.none.label;"
+ type="radio"
+ command="cmd_status_none"/>
+ <menuitem id="event-status-tentative-menuitem"
+ name="event-status-group"
+ label="&newevent.status.tentative.label;"
+ type="radio"
+ command="cmd_status_tentative"/>
+ <menuitem id="event-status-confirmed-menuitem"
+ name="event-status-group"
+ label="&newevent.status.confirmed.label;"
+ type="radio"
+ command="cmd_status_confirmed"/>
+ <menuitem id="event-status-cancelled-menuitem"
+ name="event-status-group"
+ label="&newevent.eventStatus.cancelled.label;"
+ type="radio"
+ command="cmd_status_cancelled"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-freebusy"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1 event-only"
+ type="menu"
+ wantdropmarker="true"
+ disable-on-readonly="true"
+ label="&event.menu.options.show.time.label;"
+ tooltiptext="&event.toolbar.freebusy.tooltip;">
+ <menupopup id="event-freebusy-menupopup">
+ <menuitem id="event-freebusy-busy-menuitem"
+ name="event-freebusy-group"
+ label="&event.menu.options.show.time.busy.label;"
+ type="radio"
+ command="cmd_showtimeas_busy"/>
+ <menuitem id="event-freebusy-free-menuitem"
+ name="event-freebusy-group"
+ label="&event.menu.options.show.time.free.label;"
+ type="radio"
+ command="cmd_showtimeas_free"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-timezones"
+ mode="dialog"
+ type="checkbox"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.menu.options.timezone2.label;"
+ tooltiptext="&event.menu.options.timezone2.label;"
+ command="cmd_timezone"/>
diff --git a/comm/calendar/base/content/item-editing/calendar-task-editing.js b/comm/calendar/base/content/item-editing/calendar-task-editing.js
new file mode 100644
index 0000000000..33f67f81d7
--- /dev/null
+++ b/comm/calendar/base/content/item-editing/calendar-task-editing.js
@@ -0,0 +1,181 @@
+/* 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/. */
+
+/* import-globals-from ../calendar-management.js */
+/* import-globals-from ../calendar-ui-utils.js */
+/* import-globals-from calendar-item-editing.js */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalTodo: "resource:///modules/CalTodo.jsm",
+});
+
+/**
+ * Used by the "quick add" feature for tasks, for example in the task view or
+ * the uniinder-todo.
+ *
+ * NOTE: many of the following methods are called without taskEdit being the
+ * |this| object.
+ */
+
+var taskEdit = {
+ /**
+ * Helper function to set readonly and aria-disabled states and the value
+ * for a given target.
+ *
+ * @param aTarget The ID or XUL node to set the value
+ * @param aDisable A boolean if the target should be disabled.
+ * @param aValue The value that should be set on the target.
+ */
+ setupTaskField(aTarget, aDisable, aValue) {
+ aTarget.value = aValue;
+ aTarget.readOnly = aDisable;
+ aTarget.ariaDisabled = aDisable;
+ },
+
+ /**
+ * Handler function to call when the quick-add input gains focus.
+ *
+ * @param aEvent The DOM focus event
+ */
+ onFocus(aEvent) {
+ let edit = aEvent.target;
+ let calendar = getSelectedCalendar();
+ edit.showsInstructions = true;
+
+ if (calendar.getProperty("capabilities.tasks.supported") === false) {
+ taskEdit.setupTaskField(edit, true, cal.l10n.getCalString("taskEditInstructionsCapability"));
+ } else if (cal.acl.isCalendarWritable(calendar)) {
+ edit.showsInstructions = false;
+ taskEdit.setupTaskField(edit, false, edit.savedValue || "");
+ } else {
+ taskEdit.setupTaskField(edit, true, cal.l10n.getCalString("taskEditInstructionsReadonly"));
+ }
+ },
+
+ /**
+ * Handler function to call when the quick-add input loses focus.
+ *
+ * @param aEvent The DOM blur event
+ */
+ onBlur(aEvent) {
+ let edit = aEvent.target;
+ let calendar = getSelectedCalendar();
+ if (!calendar) {
+ // this must be a first run, we don't have a calendar yet
+ return;
+ }
+
+ if (calendar.getProperty("capabilities.tasks.supported") === false) {
+ taskEdit.setupTaskField(edit, true, cal.l10n.getCalString("taskEditInstructionsCapability"));
+ } else if (cal.acl.isCalendarWritable(calendar)) {
+ if (!edit.showsInstructions) {
+ edit.savedValue = edit.value || "";
+ }
+ taskEdit.setupTaskField(edit, false, cal.l10n.getCalString("taskEditInstructions"));
+ } else {
+ taskEdit.setupTaskField(edit, true, cal.l10n.getCalString("taskEditInstructionsReadonly"));
+ }
+
+ edit.showsInstructions = true;
+ },
+
+ /**
+ * Handler function to call on keypress for the quick-add input.
+ *
+ * @param aEvent The DOM keypress event
+ */
+ onKeyPress(aEvent) {
+ if (aEvent.key == "Enter") {
+ let edit = aEvent.target;
+ if (edit.value && edit.value.length > 0) {
+ let item = new CalTodo();
+ setDefaultItemValues(item);
+ item.title = edit.value;
+
+ edit.value = "";
+ doTransaction("add", item, item.calendar, null, null);
+ }
+ }
+ },
+
+ /**
+ * Helper function to call onBlur for all fields with class name
+ * "task-edit-field".
+ */
+ callOnBlurForAllTaskFields() {
+ let taskEditFields = document.getElementsByClassName("task-edit-field");
+ for (let i = 0; i < taskEditFields.length; i++) {
+ taskEdit.onBlur({ target: taskEditFields[i] });
+ }
+ },
+
+ /**
+ * Load function to set up all quick-add inputs. The input must
+ * have the class "task-edit-field".
+ */
+ onLoad(aEvent) {
+ cal.view.getCompositeCalendar(window).addObserver(taskEdit.compositeObserver);
+ taskEdit.callOnBlurForAllTaskFields();
+ },
+
+ /**
+ * Window load function to clean up all quick-add fields.
+ */
+ onUnload() {
+ cal.view.getCompositeCalendar(window).removeObserver(taskEdit.compositeObserver);
+ },
+
+ /**
+ * Observer to watch for changes to the selected calendar.
+ *
+ * @see calIObserver
+ * @see calICompositeObserver
+ */
+ compositeObserver: {
+ QueryInterface: ChromeUtils.generateQI(["calIObserver", "calICompositeObserver"]),
+
+ // calIObserver:
+ onStartBatch() {},
+ onEndBatch() {},
+ onLoad(aCalendar) {},
+ onAddItem(aItem) {},
+ onModifyItem(aNewItem, aOldItem) {},
+ onDeleteItem(aDeletedItem) {},
+ onError(aCalendar, aErrNo, aMessage) {},
+
+ onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
+ if (aCalendar.id != getSelectedCalendar().id) {
+ // Optimization: if the given calendar isn't the selected calendar,
+ // then we don't need to change any readonly/disabled states.
+ return;
+ }
+
+ switch (aName) {
+ case "readOnly":
+ case "disabled": {
+ taskEdit.callOnBlurForAllTaskFields();
+ break;
+ }
+ }
+ },
+
+ onPropertyDeleting(aCalendar, aName) {
+ // Since the old value is not used directly in onPropertyChanged,
+ // but should not be the same as the value, set it to a different
+ // value.
+ this.onPropertyChanged(aCalendar, aName, null, null);
+ },
+
+ // calICompositeObserver:
+ onCalendarAdded(aCalendar) {},
+ onCalendarRemoved(aCalendar) {},
+ onDefaultCalendarChanged(aNewDefault) {
+ taskEdit.callOnBlurForAllTaskFields();
+ },
+ },
+};