/* 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/. */ /* globals sortCalendarArray, gDataMigrator, calendarUpdateNewItemsCommand, currentView */ /* exported promptDeleteCalendar, loadCalendarManager, unloadCalendarManager, * calendarListTooltipShowing, calendarListSetupContextMenu, * ensureCalendarVisible, toggleCalendarVisible, showAllCalendars, * showOnlyCalendar, calendarOfflineManager, openLocalCalendar, */ var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); /** * Get this window's currently selected calendar. * * @returns The currently selected calendar. */ function getSelectedCalendar() { return cal.view.getCompositeCalendar(window).defaultCalendar; } /** * Deletes the passed calendar, prompting the user if he really wants to do * this. If there is only one calendar left, no calendar is removed and the user * is not prompted. * * @param aCalendar The calendar to delete. */ function promptDeleteCalendar(aCalendar) { let calendars = cal.manager.getCalendars(); if (calendars.length <= 1) { // If this is the last calendar, don't delete it. return; } let modes = new Set(aCalendar.getProperty("capabilities.removeModes") || ["unsubscribe"]); let title = cal.l10n.getCalString("removeCalendarTitle"); let textKey, b0text, b2text; let removeFlags = 0; let promptFlags = Ci.nsIPromptService.BUTTON_POS_0 * Ci.nsIPromptService.BUTTON_TITLE_IS_STRING + Ci.nsIPromptService.BUTTON_POS_1 * Ci.nsIPromptService.BUTTON_TITLE_CANCEL; if (modes.has("delete") && !modes.has("unsubscribe")) { textKey = "removeCalendarMessageDelete"; promptFlags += Ci.nsIPromptService.BUTTON_DELAY_ENABLE; b0text = cal.l10n.getCalString("removeCalendarButtonDelete"); } else if (modes.has("delete")) { textKey = "removeCalendarMessageDeleteOrUnsubscribe"; promptFlags += Ci.nsIPromptService.BUTTON_POS_2 * Ci.nsIPromptService.BUTTON_TITLE_IS_STRING; b0text = cal.l10n.getCalString("removeCalendarButtonUnsubscribe"); b2text = cal.l10n.getCalString("removeCalendarButtonDelete"); } else if (modes.has("unsubscribe")) { textKey = "removeCalendarMessageUnsubscribe"; removeFlags |= Ci.calICalendarManager.REMOVE_NO_DELETE; b0text = cal.l10n.getCalString("removeCalendarButtonUnsubscribe"); } else { return; } let text = cal.l10n.getCalString(textKey, [aCalendar.name]); let res = Services.prompt.confirmEx( window, title, text, promptFlags, b0text, null, b2text, null, {} ); if (res != 1) { // Not canceled if (textKey == "removeCalendarMessageDeleteOrUnsubscribe" && res == 0) { // Both unsubscribing and deleting is possible, but unsubscribing was // requested. Make sure no delete is executed. removeFlags |= Ci.calICalendarManager.REMOVE_NO_DELETE; } cal.manager.removeCalendar(aCalendar, removeFlags); } } /** * Call to refresh the status image of a calendar item when the * calendar-readfailed or calendar-readonly attributes are added or removed. * * @param {MozRichlistitem} item - The calendar item to update. */ function updateCalendarStatusIndicators(item) { let calendarName = item.querySelector(".calendar-name").textContent; let image = item.querySelector("img.calendar-readstatus"); if (item.hasAttribute("calendar-readfailed")) { image.setAttribute("src", "chrome://messenger/skin/icons/new/compact/warning.svg"); let tooltip = cal.l10n.getCalString("tooltipCalendarDisabled", [calendarName]); image.setAttribute("title", tooltip); } else if (item.hasAttribute("calendar-readonly")) { image.setAttribute("src", "chrome://messenger/skin/icons/new/compact/lock.svg"); let tooltip = cal.l10n.getCalString("tooltipCalendarReadOnly", [calendarName]); image.setAttribute("title", tooltip); } else { image.removeAttribute("src"); image.removeAttribute("title"); } } /** * Called to initialize the calendar manager for a window. */ async function loadCalendarManager() { let calendarList = document.getElementById("calendar-list"); // Set up the composite calendar in the calendar list widget. let compositeCalendar = cal.view.getCompositeCalendar(window); // Initialize our composite observer compositeCalendar.addObserver(compositeObserver); // Create the home calendar if no calendar exists. let calendars = cal.manager.getCalendars(); if (calendars.length) { // migration code to make sure calendars, which do not support caching have cache enabled // required to further clean up on top of bug 1182264 for (let calendar of calendars) { if ( calendar.getProperty("cache.supported") === false && calendar.getProperty("cache.enabled") === true ) { calendar.deleteProperty("cache.enabled"); } } } else { initHomeCalendar(); } for (let calendar of sortCalendarArray(cal.manager.getCalendars())) { addCalendarItem(calendar); } function addCalendarItem(calendar) { let item = document .getElementById("calendar-list-item") .content.firstElementChild.cloneNode(true); let forceDisabled = calendar.getProperty("force-disabled"); item.id = `calendar-listitem-${calendar.id}`; item.searchLabel = calendar.name; item.setAttribute("aria-label", calendar.name); item.setAttribute("calendar-id", calendar.id); item.toggleAttribute("calendar-disabled", calendar.getProperty("disabled")); item.toggleAttribute( "calendar-readfailed", !Components.isSuccessCode(calendar.getProperty("currentStatus")) || forceDisabled ); item.toggleAttribute("calendar-readonly", calendar.readOnly); item.toggleAttribute("calendar-muted", calendar.getProperty("suppressAlarms")); document.l10n.setAttributes( item.querySelector(".calendar-mute-status"), "calendar-no-reminders-tooltip", { calendarName: calendar.name } ); document.l10n.setAttributes( item.querySelector(".calendar-more-button"), "calendar-list-item-context-button", { calendarName: calendar.name } ); let cssSafeId = cal.view.formatStringForCSSRule(calendar.id); let colorMarker = item.querySelector(".calendar-color"); if (calendar.getProperty("disabled")) { colorMarker.style.backgroundColor = "transparent"; colorMarker.style.border = `2px solid var(--calendar-${cssSafeId}-backcolor)`; } else { colorMarker.style.backgroundColor = `var(--calendar-${cssSafeId}-backcolor)`; } let label = item.querySelector(".calendar-name"); label.textContent = calendar.name; updateCalendarStatusIndicators(item); let enable = item.querySelector(".calendar-enable-button"); document.l10n.setAttributes(enable, "calendar-enable-button"); enable.hidden = forceDisabled || !calendar.getProperty("disabled"); let displayedCheckbox = item.querySelector(".calendar-displayed"); displayedCheckbox.checked = calendar.getProperty("calendar-main-in-composite"); displayedCheckbox.hidden = calendar.getProperty("disabled"); let stringName = cal.view.getCompositeCalendar(window).getCalendarById(calendar.id) ? "hideCalendar" : "showCalendar"; displayedCheckbox.setAttribute("title", cal.l10n.getCalString(stringName, [calendar.name])); calendarList.appendChild(item); if (calendar.getProperty("calendar-main-default")) { // The list needs to handle the addition of the row before we can select it. setTimeout(() => { calendarList.selectedIndex = calendarList.rows.indexOf(item); }); } } function saveSortOrder() { let order = [...calendarList.children].map(i => i.getAttribute("calendar-id")); Services.prefs.setStringPref("calendar.list.sortOrder", order.join(" ")); try { Services.prefs.savePrefFile(null); } catch (ex) { cal.ERROR(ex); } } calendarList.addEventListener("click", event => { if (event.target.matches(".calendar-enable-button")) { let calendar = cal.manager.getCalendarById( event.target.closest("li").getAttribute("calendar-id") ); calendar.setProperty("disabled", false); calendarList.focus(); return; } if (!event.target.matches(".calendar-displayed")) { return; } let item = event.target.closest("li"); let calendarId = item.getAttribute("calendar-id"); let calendar = cal.manager.getCalendarById(calendarId); if (event.target.checked) { compositeCalendar.addCalendar(calendar); } else { compositeCalendar.removeCalendar(calendar); } let stringName = event.target.checked ? "hideCalendar" : "showCalendar"; event.target.setAttribute("title", cal.l10n.getCalString(stringName, [calendar.name])); calendarList.focus(); }); calendarList.addEventListener("dblclick", event => { if ( event.target.matches(".calendar-displayed") || event.target.matches(".calendar-enable-button") ) { return; } let item = event.target.closest("li"); if (!item) { // Click on an empty part of the richlistbox. cal.window.openCalendarWizard(window); return; } let calendarId = item.getAttribute("calendar-id"); let calendar = cal.manager.getCalendarById(calendarId); cal.window.openCalendarProperties(window, { calendar }); }); calendarList.addEventListener("ordered", event => { saveSortOrder(); calendarList.selectedIndex = calendarList.rows.indexOf(event.detail); }); calendarList.addEventListener("keypress", event => { let item = calendarList.rows[calendarList.selectedIndex]; let calendarId = item.getAttribute("calendar-id"); let calendar = cal.manager.getCalendarById(calendarId); switch (event.key) { case "Delete": promptDeleteCalendar(calendar); break; case " ": { if (item.querySelector(".calendar-displayed").checked) { compositeCalendar.removeCalendar(calendar); } else { compositeCalendar.addCalendar(calendar); } let stringName = item.querySelector(".calendar-displayed").checked ? "hideCalendar" : "showCalendar"; item .querySelector(".calendar-displayed") .setAttribute("title", cal.l10n.getCalString(stringName, [calendar.name])); break; } } }); calendarList.addEventListener("select", event => { let item = calendarList.rows[calendarList.selectedIndex]; let calendarId = item.getAttribute("calendar-id"); let calendar = cal.manager.getCalendarById(calendarId); compositeCalendar.defaultCalendar = calendar; }); calendarList._calendarObserver = { QueryInterface: ChromeUtils.generateQI(["calIObserver"]), onStartBatch() {}, onEndBatch() {}, onLoad() {}, onAddItem(item) {}, onModifyItem(newItem, oldItem) {}, onDeleteItem(deletedItem) {}, onError(calendar, errNo, message) {}, onPropertyChanged(calendar, name, value, oldValue) { let item = calendarList.getElementsByAttribute("calendar-id", calendar.id)[0]; if (!item) { return; } switch (name) { case "disabled": item.toggleAttribute("calendar-disabled", value); item.querySelector(".calendar-displayed").hidden = value; // Update the "ENABLE" button. let enableButton = item.querySelector(".calendar-enable-button"); enableButton.hidden = !value; // Update the color preview. let cssSafeId = cal.view.formatStringForCSSRule(calendar.id); let colorMarker = item.querySelector(".calendar-color"); colorMarker.style.backgroundColor = value ? "transparent" : `var(--calendar-${cssSafeId}-backcolor)`; colorMarker.style.border = value ? `2px solid var(--calendar-${cssSafeId}-backcolor)` : "none"; break; case "calendar-main-default": if (value) { calendarList.selectedIndex = calendarList.rows.indexOf(item); } break; case "calendar-main-in-composite": item.querySelector(".calendar-displayed").checked = value; break; case "name": item.searchLabel = calendar.name; item.querySelector(".calendar-name").textContent = value; break; case "currentStatus": case "force-disabled": item.toggleAttribute( "calendar-readfailed", name == "currentStatus" ? !Components.isSuccessCode(value) : value ); updateCalendarStatusIndicators(item); break; case "readOnly": item.toggleAttribute("calendar-readonly", value); updateCalendarStatusIndicators(item); break; case "suppressAlarms": item.toggleAttribute("calendar-muted", value); break; } }, onPropertyDeleting(calendar, name) { // 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(calendar, name, null, null); }, }; cal.manager.addCalendarObserver(calendarList._calendarObserver); calendarList._calendarManagerObserver = { QueryInterface: ChromeUtils.generateQI(["calICalendarManagerObserver"]), onCalendarRegistered(calendar) { addCalendarItem(calendar); saveSortOrder(); }, onCalendarUnregistering(calendar) { let item = calendarList.getElementsByAttribute("calendar-id", calendar.id)[0]; item.remove(); saveSortOrder(); }, onCalendarDeleting(calendar) {}, }; cal.manager.addObserver(calendarList._calendarManagerObserver); } /** * Creates the initial "Home" calendar if no calendar exists. */ function initHomeCalendar() { let composite = cal.view.getCompositeCalendar(window); let url = Services.io.newURI("moz-storage-calendar://"); let homeCalendar = cal.manager.createCalendar("storage", url); homeCalendar.name = cal.l10n.getCalString("homeCalendarName"); homeCalendar.setProperty("disabled", true); cal.manager.registerCalendar(homeCalendar); Services.prefs.setStringPref("calendar.list.sortOrder", homeCalendar.id); composite.addCalendar(homeCalendar); // Wrapping this in a try/catch block, as if any of the migration code // fails, the app may not load. if (Services.prefs.getBoolPref("calendar.migrator.enabled", true)) { try { gDataMigrator.checkAndMigrate(); } catch (e) { console.error("Migrator error: " + e); } } return homeCalendar; } /** * Called to clean up the calendar manager for a window. */ function unloadCalendarManager() { let compositeCalendar = cal.view.getCompositeCalendar(window); compositeCalendar.setStatusObserver(null, null); compositeCalendar.removeObserver(compositeObserver); let calendarList = document.getElementById("calendar-list"); cal.manager.removeCalendarObserver(calendarList._calendarObserver); cal.manager.removeObserver(calendarList._calendarManagerObserver); } /** * A handler called to set up the context menu on the calendar list. * * @param {Event} event - The click DOMEvent. */ function calendarListSetupContextMenu(event) { let calendar; let composite = cal.view.getCompositeCalendar(window); if (event.target.matches(".calendar-displayed")) { return; } let item = event.target.closest("li"); if (item) { let calendarList = document.getElementById("calendar-list"); calendarList.selectedIndex = calendarList.rows.indexOf(item); let calendarId = item.getAttribute("calendar-id"); calendar = cal.manager.getCalendarById(calendarId); } document.getElementById("list-calendars-context-menu").contextCalendar = calendar; for (let elem of document.querySelectorAll("#list-calendars-context-menu .needs-calendar")) { elem.hidden = !calendar; } if (calendar) { let stringName = composite.getCalendarById(calendar.id) ? "hideCalendar" : "showCalendar"; document.getElementById("list-calendars-context-togglevisible").label = cal.l10n.getCalString( stringName, [calendar.name] ); let accessKey = document .getElementById("list-calendars-context-togglevisible") .getAttribute(composite.getCalendarById(calendar.id) ? "accesskeyhide" : "accesskeyshow"); document.getElementById("list-calendars-context-togglevisible").accessKey = accessKey; document.getElementById("list-calendars-context-showonly").label = cal.l10n.getCalString( "showOnlyCalendar", [calendar.name] ); setupDeleteMenuitem("list-calendars-context-delete", calendar); document.getElementById("list-calendar-context-reload").hidden = !calendar.canRefresh; document.getElementById("list-calendars-context-reload-menuseparator").hidden = !calendar.canRefresh; } } /** * Trigger the opening of the calendar list item context menu. * * @param {Event} event - The click DOMEvent. */ function openCalendarListItemContext(event) { calendarListSetupContextMenu(event); let popUpCalListMenu = document.getElementById("list-calendars-context-menu"); if (event.type == "contextmenu" && event.button == 2) { // This is a right-click. Open where it happened. popUpCalListMenu.openPopupAtScreen(event.screenX, event.screenY, true); return; } popUpCalListMenu.openPopup(event.target, "after_start", 0, 0, true); } /** * Changes the "delete calendar" menuitem to have the right label based on the * removeModes. The menuitem must have the attributes "labelremove", * "labeldelete" and "labelunsubscribe". * * @param aDeleteId The id of the menuitem to delete the calendar */ function setupDeleteMenuitem(aDeleteId, aCalendar) { let calendar = aCalendar === undefined ? getSelectedCalendar() : aCalendar; let modes = new Set( calendar ? calendar.getProperty("capabilities.removeModes") || ["unsubscribe"] : [] ); let type = "remove"; if (modes.has("delete") && !modes.has("unsubscribe")) { type = "delete"; } else if (modes.has("unsubscribe") && !modes.has("delete")) { type = "unsubscribe"; } let deleteItem = document.getElementById(aDeleteId); // Dynamically set labelremove, labeldelete, labelunsubscribe deleteItem.label = deleteItem.getAttribute("label" + type); // Dynamically set accesskeyremove, accesskeydelete, accesskeyunsubscribe deleteItem.accessKey = deleteItem.getAttribute("accesskey" + type); } /** * Makes sure the passed calendar is visible to the user * * @param aCalendar The calendar to make visible. */ function ensureCalendarVisible(aCalendar) { // We use the main window's calendar list to ensure that the calendar is visible. // If the main window has been closed this function may still be called, // like when an event/task window is still open and the user clicks 'save', // thus we have the extra checks. let calendarList = document.getElementById("calendar-list"); if (calendarList) { let compositeCalendar = cal.view.getCompositeCalendar(window); compositeCalendar.addCalendar(aCalendar); } } /** * Hides the specified calendar if it is visible, or shows it if it is hidden. * * @param aCalendar The calendar to show or hide */ function toggleCalendarVisible(aCalendar) { let composite = cal.view.getCompositeCalendar(window); if (composite.getCalendarById(aCalendar.id)) { composite.removeCalendar(aCalendar); } else { composite.addCalendar(aCalendar); } } /** * Shows all hidden calendars. */ function showAllCalendars() { let composite = cal.view.getCompositeCalendar(window); let cals = cal.manager.getCalendars(); composite.startBatch(); for (let calendar of cals) { if (!composite.getCalendarById(calendar.id)) { composite.addCalendar(calendar); } } composite.endBatch(); } /** * Shows only the specified calendar, and hides all others. * * @param aCalendar The calendar to show as the only visible calendar */ function showOnlyCalendar(aCalendar) { let composite = cal.view.getCompositeCalendar(window); let cals = composite.getCalendars() || []; composite.startBatch(); for (let calendar of cals) { if (calendar.id != aCalendar.id) { composite.removeCalendar(calendar); } } composite.addCalendar(aCalendar); composite.endBatch(); } var compositeObserver = { QueryInterface: ChromeUtils.generateQI(["calIObserver", "calICompositeObserver"]), onStartBatch() {}, onEndBatch() {}, onLoad() { calendarUpdateNewItemsCommand(); document.commandDispatcher.updateCommands("calendar_commands"); }, onAddItem() {}, onModifyItem() {}, onDeleteItem() {}, onError() {}, onPropertyChanged(calendar, name, value, oldValue) { if (name == "disabled" || name == "readOnly") { // Update commands when a calendar has been enabled or disabled. calendarUpdateNewItemsCommand(); document.commandDispatcher.updateCommands("calendar_commands"); } }, onPropertyDeleting() {}, onCalendarAdded(aCalendar) { // Update the calendar commands for number of remote calendars and for // more than one calendar. calendarUpdateNewItemsCommand(); document.commandDispatcher.updateCommands("calendar_commands"); }, onCalendarRemoved(aCalendar) { // Update commands to disallow deleting the last calendar and only // allowing reload remote calendars when there are remote calendars. calendarUpdateNewItemsCommand(); document.commandDispatcher.updateCommands("calendar_commands"); }, onDefaultCalendarChanged(aNewCalendar) { // A new default calendar may mean that the new calendar has different // ACLs. Make sure the commands are updated. calendarUpdateNewItemsCommand(); document.commandDispatcher.updateCommands("calendar_commands"); }, }; /** * Shows the filepicker and creates a new calendar with a local file using the ICS * provider. */ function openLocalCalendar() { let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); picker.init(window, cal.l10n.getCalString("Open"), Ci.nsIFilePicker.modeOpen); let wildmat = "*.ics"; let description = cal.l10n.getCalString("filterIcs", [wildmat]); picker.appendFilter(description, wildmat); picker.appendFilters(Ci.nsIFilePicker.filterAll); picker.open(rv => { if (rv != Ci.nsIFilePicker.returnOK || !picker.file) { return; } let calendars = cal.manager.getCalendars(); let calendar = calendars.find(x => x.uri.equals(picker.fileURL)); if (!calendar) { calendar = cal.manager.createCalendar("ics", picker.fileURL); // Strip ".ics" from filename for use as calendar name. let prettyName = picker.fileURL.spec.match(/([^/:]+)\.ics$/); if (prettyName) { calendar.name = decodeURIComponent(prettyName[1]); } else { calendar.name = cal.l10n.getCalString("untitledCalendarName"); } cal.manager.registerCalendar(calendar); } let calendarList = document.getElementById("calendar-list"); for (let index = 0; index < calendarList.rowCount; index++) { if (calendarList.rows[index].getAttribute("calendar-id") == calendar.id) { calendarList.selectedIndex = index; break; } } }); } /** * Calendar Offline Manager */ var calendarOfflineManager = { QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), init() { if (this.initialized) { throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED); } Services.obs.addObserver(this, "network:offline-status-changed"); this.updateOfflineUI(!this.isOnline()); this.initialized = true; }, uninit() { if (!this.initialized) { throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED); } Services.obs.removeObserver(this, "network:offline-status-changed"); this.initialized = false; }, isOnline() { return !Services.io.offline; }, updateOfflineUI(aIsOffline) { // Refresh the current view currentView().goToDay(currentView().selectedDay); // Set up disabled locks for offline document.commandDispatcher.updateCommands("calendar_commands"); }, observe(aSubject, aTopic, aState) { if (aTopic == "network:offline-status-changed") { this.updateOfflineUI(aState == "offline"); } }, };