diff options
Diffstat (limited to 'comm/calendar/base/content/calendar-management.js')
-rw-r--r-- | comm/calendar/base/content/calendar-management.js | 721 |
1 files changed, 721 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-management.js b/comm/calendar/base/content/calendar-management.js new file mode 100644 index 0000000000..351c44b184 --- /dev/null +++ b/comm/calendar/base/content/calendar-management.js @@ -0,0 +1,721 @@ +/* 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"); + } + }, +}; |