summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/dialogs/calendar-creation.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/base/content/dialogs/calendar-creation.js
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/calendar/base/content/dialogs/calendar-creation.js')
-rw-r--r--comm/calendar/base/content/dialogs/calendar-creation.js836
1 files changed, 836 insertions, 0 deletions
diff --git a/comm/calendar/base/content/dialogs/calendar-creation.js b/comm/calendar/base/content/dialogs/calendar-creation.js
new file mode 100644
index 0000000000..b4d2a7c2e4
--- /dev/null
+++ b/comm/calendar/base/content/dialogs/calendar-creation.js
@@ -0,0 +1,836 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+var { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "MsgAuthPrompt", "resource:///modules/MsgAsyncPrompter.jsm");
+
+/* exported checkRequired, fillLocationPlaceholder, selectProvider, updateNoCredentials, */
+
+/* import-globals-from calendar-identity-utils.js */
+
+/**
+ * For managing dialog button handler state. Stores the current handlers so we
+ * can remove them with removeEventListener. Provides a way to look up the
+ * button handler functions to be used with a given panel.
+ */
+var gButtonHandlers = {
+ accept: null,
+ extra2: null,
+
+ // Maps a panel DOM node ID to the button handlers to use for that panel.
+ forNodeId: {
+ "panel-select-calendar-type": {
+ accept: selectCalendarType,
+ },
+ "panel-local-calendar-settings": {
+ accept: registerLocalCalendar,
+ extra2: () => selectPanel("panel-select-calendar-type"),
+ },
+ "panel-network-calendar-settings": {
+ accept: event => {
+ event.preventDefault();
+ event.stopPropagation();
+ findCalendars();
+ },
+ extra2: () => selectPanel("panel-select-calendar-type"),
+ },
+ "panel-select-calendars": {
+ accept: createNetworkCalendars,
+ extra2: () => selectPanel("panel-network-calendar-settings"),
+ },
+ "panel-addon-calendar-settings": {
+ extra2: () => selectPanel("panel-select-calendar-type"),
+ // This 'accept' is set dynamically when the calendar type is selected.
+ accept: null,
+ },
+ },
+};
+
+/** @type {calICalendar | null} */
+var gLocalCalendar = null;
+
+/**
+ * A type of calendar that can be created with this dialog.
+ *
+ * @typedef {CalendarType}
+ * @property {string} id A unique ID for this type, e.g. "local" or
+ * "network" for built-in types and
+ * "3" or "4" for add-on types.
+ * @property {boolean} builtIn Whether this is a built in type.
+ * @property {Function} onSelected The "accept" button handler to call when
+ * the type is selected.
+ * @property {string} [label] Text to use in calendar type selection UI.
+ * @property {string} [panelSrc] The "src" property for the <browser> for
+ * this type's settings panel, typically a
+ * path to an html document. Only needed
+ * for types registered by add-ons.
+ * @property {Function} [onCreated] The "accept" button handler for this
+ * type's settings panel. Only needed for
+ * types registered by add-ons.
+ */
+
+/**
+ * Registry of calendar types. The key should match the type's `id` property.
+ * Add-ons may register additional types.
+ *
+ * @type {Map<string, CalendarType>}
+ */
+var gCalendarTypes = new Map([
+ [
+ "local",
+ {
+ id: "local",
+ builtIn: true,
+ onSelected: () => {
+ // Create a local calendar to use, so we can share code with the calendar
+ // preferences dialog.
+ if (!gLocalCalendar) {
+ gLocalCalendar = cal.manager.createCalendar(
+ "storage",
+ Services.io.newURI("moz-storage-calendar://")
+ );
+
+ initMailIdentitiesRow(gLocalCalendar);
+ notifyOnIdentitySelection(gLocalCalendar);
+ }
+ selectPanel("panel-local-calendar-settings");
+ },
+ },
+ ],
+ [
+ "network",
+ {
+ id: "network",
+ builtIn: true,
+ onSelected: () => selectPanel("panel-network-calendar-settings"),
+ },
+ ],
+]);
+
+/** @type {CalendarType | null} */
+var gSelectedCalendarType = null;
+
+/**
+ * Register a calendar type to offer in the dialog. For add-ons to use. Add-on
+ * code should store the returned ID and use it for unregistering the type.
+ *
+ * @param {CalendarType} type - The type object to register.
+ * @returns {string} The generated ID for the type.
+ */
+function registerCalendarType(type) {
+ type.id = String(gCalendarTypes.size + 1);
+ type.builtIn = false;
+
+ if (!type.onSelected) {
+ type.onSelected = () => selectPanel("panel-addon-calendar-settings");
+ }
+ gCalendarTypes.set(type.id, type);
+
+ // Add an option for this type to the "select calendar type" panel.
+ let radiogroup = document.getElementById("calendar-type");
+ let radio = document.createXULElement("radio");
+ radio.setAttribute("value", type.id);
+ radio.setAttribute("label", type.label);
+ radiogroup.appendChild(radio);
+
+ return type.id;
+}
+
+/**
+ * Unregister a calendar type. For add-ons to use.
+ *
+ * @param {string} id - The ID of the type to unregister.
+ */
+function unregisterCalendarType(id) {
+ // Don't allow unregistration of built-in types.
+ if (gCalendarTypes.get(id)?.builtIn) {
+ cal.WARN(
+ `calendar creation dialog: unregistering calendar type "${id}"` +
+ " failed because it is a built in type"
+ );
+ return;
+ }
+ // We are using the size of gCalendarTypes to generate unique IDs for
+ // registered types, so don't fully remove the type.
+ gCalendarTypes.set(id, undefined);
+
+ // Remove the option for this type from the "select calendar type" panel.
+ let radiogroup = document.getElementById("calendar-type");
+ let radio = radiogroup.querySelector(`[value="${id}"]`);
+ if (radio) {
+ radiogroup.removeChild(radio);
+ }
+}
+
+/**
+ * Tools for managing how providers are used for calendar detection. May be used
+ * by add-ons to modify which providers are used and which results are preferred.
+ */
+var gProviderUsage = {
+ /**
+ * A function that returns a list of provider types to filter out and not use
+ * to detect calendars, for a given location and username. The providers are
+ * filtered out before calendar detection. For example, the "Provider for
+ * Google Calendar" add-on might filter out the "caldav" provider:
+ *
+ * (providers, location, username) => {
+ * domain = username.split("@")[1];
+ * if (providers.includes("gdata") && (domain == "googlemail.com" || domain == "gmail.com")) {
+ * return ["caldav"];
+ * }
+ * return [];
+ * }
+ *
+ * @callback ProviderFilter
+ * @param {string[]} providers - Array of provider types to be used (if not filtered out).
+ * @param {string} location - Location to use for calendar detection.
+ * @param {string} username - Username to use for calendar detection.
+ * @returns {string[]} Array of provider types to be filtered out.
+ */
+
+ /** @type {ProviderFilter[]} */
+ _preDetectFilters: [],
+
+ /**
+ * A mapping from a less preferred provider type to a set of more preferred
+ * provider types. Used after calendar detection to default to a more
+ * preferred provider when there are results from more than one provider.
+ *
+ * @typedef {Map<string, Set<string>>} ProviderPreferences
+ */
+
+ /**
+ * @type {ProviderPreferences}
+ */
+ _postDetectPreferences: new Map(),
+
+ get preDetectFilters() {
+ return this._preDetectFilters;
+ },
+
+ get postDetectPreferences() {
+ return this._postDetectPreferences;
+ },
+
+ /**
+ * Add a new provider filter function.
+ *
+ * @param {ProviderFilter} providerFilter
+ */
+ addPreDetectFilter(providerFilter) {
+ this._preDetectFilters.push(providerFilter);
+ },
+
+ /**
+ * Add a preference for one provider type over another provider type.
+ *
+ * @param {string} preferredType - The preferred provider type.
+ * @param {string} nonPreferredType - The non-preferred provider type.
+ */
+ addPostDetectPreference(preferredType, nonPreferredType) {
+ let prefs = this._postDetectPreferences;
+
+ if (this.detectPreferenceCycle(prefs, preferredType, nonPreferredType)) {
+ cal.WARN(
+ `Adding a preference for provider type "${preferredType}" over ` +
+ `type "${nonPreferredType}" would cause a preference cycle, ` +
+ `not adding this preference to prevent a cycle`
+ );
+ } else {
+ let current = prefs.get(nonPreferredType);
+ if (current) {
+ current.add(preferredType);
+ } else {
+ prefs.set(nonPreferredType, new Set([preferredType]));
+ }
+ }
+ },
+
+ /**
+ * Check whether adding a preference for one provider type over another would
+ * cause a cycle in the order of preferences. We assume that the preferences
+ * do not contain any cycles already.
+ *
+ * @param {ProviderPreferences} prefs - The current preferences.
+ * @param {string} preferred - Potential preferred provider.
+ * @param {string} nonPreferred - Potential non-preferred provider.
+ * @returns {boolean} True if it would cause a cycle.
+ */
+ detectPreferenceCycle(prefs, preferred, nonPreferred) {
+ let cycle = false;
+
+ let innerDetect = preferredSet => {
+ if (cycle) {
+ // Bail out, a cycle has already been detected.
+ return;
+ } else if (preferredSet.has(nonPreferred)) {
+ // A cycle! We have arrived back at the nonPreferred provider type.
+ cycle = true;
+ return;
+ }
+ // Recursively check each preferred type.
+ for (let item of preferredSet) {
+ let nextPreferredSet = prefs.get(item);
+ if (nextPreferredSet) {
+ innerDetect(nextPreferredSet);
+ }
+ }
+ };
+
+ innerDetect(new Set([preferred]));
+ return cycle;
+ },
+};
+
+// If both ics and caldav results exist, default to the caldav results.
+gProviderUsage.addPostDetectPreference("caldav", "ics");
+
+/**
+ * Select a specific panel in the dialog. Used to move from one panel to another.
+ *
+ * @param {string} id - The id of the panel node to select.
+ */
+function selectPanel(id) {
+ for (let element of document.getElementById("calendar-creation-dialog").children) {
+ element.hidden = element.id != id;
+ }
+ let panel = document.getElementById(id);
+ updateButton("accept", panel);
+ updateButton("extra2", panel);
+ selectNetworkStatus("none");
+ checkRequired();
+
+ let firstInput = panel.querySelector("input");
+ if (firstInput) {
+ firstInput.focus();
+ }
+}
+
+/**
+ * Set a specific network loading status for the network settings panel.
+ * See the CSS file for appropriate values to set.
+ *
+ * @param {string} status - The status to set.
+ */
+function selectNetworkStatus(status) {
+ for (let row of document.querySelectorAll(".network-status-row")) {
+ row.setAttribute("status", status);
+ }
+}
+
+/**
+ * Update the label, accesskey, and event listener for a dialog button.
+ *
+ * @param {string} name - The dialog button name, e.g. 'accept', 'extra2'.
+ * @param {Element} sourceNode - The source node to take attribute values from.
+ */
+function updateButton(name, sourceNode) {
+ let dialog = document.getElementById("calendar-creation-dialog");
+ let button = dialog.getButton(name);
+ let label = sourceNode.getAttribute("buttonlabel" + name);
+ let accesskey = sourceNode.getAttribute("buttonaccesskey" + name);
+
+ let handler = gButtonHandlers.forNodeId[sourceNode.id][name];
+
+ if (label) {
+ button.setAttribute("label", label);
+ button.hidden = false;
+ } else {
+ button.hidden = true;
+ }
+
+ button.setAttribute("accesskey", accesskey || "");
+
+ // 'dialogaccept', 'dialogextra2', etc.
+ let eventName = "dialog" + name;
+
+ document.removeEventListener(eventName, gButtonHandlers[name]);
+ if (handler) {
+ document.addEventListener(eventName, handler);
+ // Store a reference to the current handler, to allow removing it later.
+ gButtonHandlers[name] = handler;
+ }
+}
+
+/**
+ * Update the disabled state of the accept button by checking the values of
+ * required fields, based on the current panel.
+ */
+function checkRequired() {
+ let dialog = document.getElementById("calendar-creation-dialog");
+ let selectedPanel = null;
+ for (let element of dialog.children) {
+ if (!element.hidden) {
+ selectedPanel = element;
+ }
+ }
+ if (!selectedPanel) {
+ dialog.setAttribute("buttondisabledaccept", "true");
+ return;
+ }
+
+ let disabled = false;
+ switch (selectedPanel.id) {
+ case "panel-local-calendar-settings":
+ disabled = !selectedPanel.querySelector("form").checkValidity();
+ break;
+ case "panel-network-calendar-settings": {
+ let location = document.getElementById("network-location-input");
+ let username = document.getElementById("network-username-input");
+
+ disabled = !location.value && !username.value.split("@")[1];
+ break;
+ }
+ }
+
+ if (disabled) {
+ dialog.setAttribute("buttondisabledaccept", "true");
+ } else {
+ dialog.removeAttribute("buttondisabledaccept");
+ }
+}
+
+/**
+ * Update the placeholder text for the network location field. If the username
+ * is a valid email address use the domain part of the username, otherwise use
+ * the default placeholder.
+ */
+function fillLocationPlaceholder() {
+ let location = document.getElementById("network-location-input");
+ let userval = document.getElementById("network-username-input").value;
+ let parts = userval.split("@");
+ let domain = parts.length == 2 && parts[1] ? parts[1] : null;
+
+ if (domain) {
+ location.setAttribute("placeholder", domain);
+ } else {
+ location.setAttribute("placeholder", location.getAttribute("default-placeholder"));
+ }
+}
+
+/**
+ * Update the select network calendar panel to show or hide the provider
+ * selection dropdown.
+ *
+ * @param {boolean} isSingle - If true, there is just one matching provider.
+ */
+function setSingleProvider(isSingle) {
+ document.getElementById("network-selectcalendar-description-single").hidden = !isSingle;
+ document.getElementById("network-selectcalendar-description-multiple").hidden = isSingle;
+ document.getElementById("network-selectcalendar-providertype-box").hidden = isSingle;
+}
+
+/**
+ * Fill the providers menulist with the given provider types. The types must
+ * correspond to the providers that detected calendars.
+ *
+ * @param {string[]} providerTypes - An array of provider types.
+ * @returns {Element} The selected menuitem.
+ */
+function fillProviders(providerTypes) {
+ let menulist = document.getElementById("network-selectcalendar-providertype-menulist");
+ let popup = menulist.menupopup;
+ while (popup.lastChild) {
+ popup.removeChild(popup.lastChild);
+ }
+
+ let providers = cal.provider.detection.providers;
+
+ for (let type of providerTypes) {
+ let provider = providers.get(type);
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.value = type;
+ menuitem.setAttribute("label", provider.displayName || type);
+ popup.appendChild(menuitem);
+ }
+
+ // Select a provider menu item based on provider preferences.
+ let preferredTypes = new Set(providerTypes);
+
+ for (let [nonPreferred, preferredSet] of gProviderUsage.postDetectPreferences) {
+ if (preferredTypes.has(nonPreferred) && setsIntersect(preferredSet, preferredTypes)) {
+ preferredTypes.delete(nonPreferred);
+ }
+ }
+ let preferredIndex = providerTypes.findIndex(type => preferredTypes.has(type));
+ menulist.selectedIndex = preferredIndex == -1 ? 0 : preferredIndex;
+
+ return menulist.selectedItem;
+}
+
+/**
+ * Return true if the intersection of two sets contains at least one item.
+ *
+ * @param {Set} setA - A set.
+ * @param {Set} setB - A set.
+ * @returns {boolean}
+ */
+function setsIntersect(setA, setB) {
+ for (let item of setA) {
+ if (setB.has(item)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Select the given provider and update the calendar list to fill the
+ * corresponding calendars. Will use the results from the last findCalendars
+ * response.
+ *
+ * @param {string} type - The provider type to select.
+ */
+function selectProvider(type) {
+ let providerMap = findCalendars.lastResult;
+ let calendarList = document.getElementById("network-calendar-list");
+
+ let calendars = providerMap.get(type) || [];
+ renderCalendarList(calendarList, calendars);
+}
+
+/**
+ * Empty a calendar list and then fill it with calendars.
+ *
+ * @param {Element} calendarList - A richlistbox element for listing calendars.
+ * @param {calICalendar[]} calendars - An array of calendars to display in the list.
+ */
+function renderCalendarList(calendarList, calendars) {
+ while (calendarList.hasChildNodes()) {
+ calendarList.lastChild.remove();
+ }
+ let propertiesButtonLabel = calendarList.getAttribute("propertiesbuttonlabel");
+ calendars.forEach((calendar, index) => {
+ let item = document.createXULElement("richlistitem");
+ item.calendar = calendar;
+
+ let checkbox = document.createXULElement("checkbox");
+ let checkboxId = "checkbox" + index;
+ checkbox.id = checkboxId;
+ checkbox.classList.add("calendar-selected");
+ item.appendChild(checkbox);
+
+ let colorMarker = document.createElement("div");
+ colorMarker.classList.add("calendar-color");
+ colorMarker.style.backgroundColor = calendar.getProperty("color");
+ item.appendChild(colorMarker);
+
+ let label = document.createXULElement("label");
+ label.classList.add("calendar-name");
+ label.value = calendar.name;
+ label.control = checkboxId;
+ item.appendChild(label);
+
+ let propertiesButton = document.createXULElement("button");
+ propertiesButton.classList.add("calendar-edit-button");
+ propertiesButton.label = propertiesButtonLabel;
+ propertiesButton.addEventListener("command", openCalendarPropertiesFromEvent);
+ item.appendChild(propertiesButton);
+
+ if (calendar.getProperty("disabled")) {
+ item.disabled = true;
+ item.toggleAttribute("calendar-disabled", true);
+ checkbox.disabled = true;
+ propertiesButton.disabled = true;
+ } else {
+ checkbox.checked = true;
+ }
+ calendarList.appendChild(item);
+ });
+}
+
+/**
+ * Update dialog fields based on the value of the "no credentials" checkbox.
+ *
+ * @param {boolean} noCredentials - True, if "no credentials" is checked.
+ */
+function updateNoCredentials(noCredentials) {
+ if (noCredentials) {
+ document.getElementById("network-username-input").setAttribute("disabled", "true");
+ document.getElementById("network-username-input").value = "";
+ } else {
+ document.getElementById("network-username-input").removeAttribute("disabled");
+ }
+}
+
+/**
+ * The accept button event listener for the "select calendar type" panel.
+ *
+ * @param {Event} event
+ */
+function selectCalendarType(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ let radiogroup = document.getElementById("calendar-type");
+ let calendarType = gCalendarTypes.get(radiogroup.value);
+
+ if (!calendarType.builtIn && calendarType !== gSelectedCalendarType) {
+ setUpAddonCalendarSettingsPanel(calendarType);
+ }
+ gSelectedCalendarType = calendarType;
+ calendarType.onSelected();
+}
+
+/**
+ * Set up the settings panel for calendar types registered by addons.
+ *
+ * @param {CalendarType} calendarType - The calendar type.
+ */
+function setUpAddonCalendarSettingsPanel(calendarType) {
+ function setUpBrowser(browser, src) {
+ // Allow keeping dialog background color without jumping through hoops.
+ browser.setAttribute("transparent", "true");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("src", src);
+ }
+ let panel = document.getElementById("panel-addon-calendar-settings");
+ let browser = panel.lastElementChild;
+
+ if (browser) {
+ setUpBrowser(browser, calendarType.panelSrc);
+ } else {
+ browser = document.createXULElement("browser");
+ setUpBrowser(browser, calendarType.panelSrc);
+
+ panel.appendChild(browser);
+ // The following emit is needed for the browser to work with addon content.
+ ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
+ }
+
+ // Set up the accept button handler for the panel.
+ gButtonHandlers.forNodeId["panel-addon-calendar-settings"].accept = calendarType.onCreated;
+}
+
+/**
+ * Handle change of the email (identity) menu for local calendar creation.
+ * Show a notification when "none" is selected.
+ *
+ * @param {Event} event - The menu selection event.
+ */
+function onChangeIdentity(event) {
+ notifyOnIdentitySelection(gLocalCalendar);
+}
+
+/**
+ * Prepare the local storage calendar with the information from the dialog.
+ * This can be monkeypatched to add additional values.
+ *
+ * @param {calICalendar} calendar - The calendar to prepare.
+ * @returns {calICalendar} The same calendar, prepared with any
+ * extra values.
+ */
+function prepareLocalCalendar(calendar) {
+ calendar.name = document.getElementById("local-calendar-name-input").value;
+ calendar.setProperty("color", document.getElementById("local-calendar-color-picker").value);
+
+ if (!document.getElementById("local-fire-alarms-checkbox").checked) {
+ calendar.setProperty("suppressAlarms", true);
+ }
+
+ saveMailIdentitySelection(calendar);
+ return calendar;
+}
+
+/**
+ * The accept button event listener for the "local calendar settings" panel.
+ * Registers the local storage calendar and closes the dialog.
+ */
+function registerLocalCalendar() {
+ cal.manager.registerCalendar(prepareLocalCalendar(gLocalCalendar));
+}
+
+/**
+ * Start detection and find any calendars using the information from the
+ * network settings panel.
+ *
+ * @param {string} [password] - The password for this attempt, if any.
+ * @param {boolean} [savePassword] - Whether to save the password in the
+ * password manager.
+ */
+function findCalendars(password, savePassword = false) {
+ selectNetworkStatus("loading");
+ let username = document.getElementById("network-username-input");
+ let location = document.getElementById("network-location-input");
+ let locationValue = location.value || username.value.split("@")[1] || "";
+
+ // webcal(s): doesn't work with content principal.
+ locationValue = locationValue.replace(/^webcal(s)?(:.*)/, "http$1$2").trim();
+ cal.provider.detection
+ .detect(
+ username.value,
+ password,
+ locationValue,
+ savePassword,
+ gProviderUsage.preDetectFilters,
+ {}
+ )
+ .then(onDetectionSuccess, onDetectionError.bind(null, password, locationValue));
+}
+
+/**
+ * Called when detection successfully finds calendars. Displays the UI for
+ * selecting calendars to subscribe to.
+ *
+ * @param {Map<string, calICalendar[]>} providerMap Map from provider type
+ * (e.g. "ics", "caldav")
+ * to an array of calendars.
+ */
+function onDetectionSuccess(providerMap) {
+ // Disable the calendars the user has already subscribed to. In the future
+ // we should show a string when all calendars are already subscribed.
+ let existing = new Set(cal.manager.getCalendars({}).map(calendar => calendar.uri.spec));
+
+ let calendarsMap = new Map();
+ for (let [provider, calendars] of providerMap.entries()) {
+ let newCalendars = calendars.map(calendar => {
+ let newCalendar = prepareNetworkCalendar(calendar);
+ if (existing.has(calendar.uri.spec)) {
+ newCalendar.setProperty("disabled", true);
+ }
+ return newCalendar;
+ });
+
+ calendarsMap.set(provider.type, newCalendars);
+ }
+
+ if (!calendarsMap.size) {
+ selectNetworkStatus("notfound");
+ return;
+ }
+
+ // Update the panel with the results from the provider map.
+ setSingleProvider(calendarsMap.size <= 1);
+ findCalendars.lastResult = calendarsMap;
+
+ let selectedItem = fillProviders([...calendarsMap.keys()]);
+ selectProvider(selectedItem.value);
+
+ // Select the panel and validate the fields.
+ selectPanel("panel-select-calendars");
+ checkRequired();
+}
+
+/**
+ * Called when detection fails to find any calendars. Show an appropriate
+ * error message, or if the error is an authentication error and no password
+ * was entered for this attempt, prompt the user to enter a password.
+ *
+ * @param {string} [password] - The password entered, if any.
+ * @param {string} [location] - The location input from the dialog.
+ * @param {Error} error - An error object.
+ */
+function onDetectionError(password, location, error) {
+ if (error instanceof cal.provider.detection.AuthFailedError) {
+ if (password) {
+ selectNetworkStatus("authfail");
+ } else {
+ findCalendarsWithPassword(location);
+ return;
+ }
+ } else if (error instanceof cal.provider.detection.CanceledError) {
+ selectNetworkStatus("none");
+ } else {
+ selectNetworkStatus("notfound");
+ }
+ cal.ERROR(
+ "Error during calendar detection: " +
+ `${error.fileName || error.filename}:${error.lineNumber}: ${error}\n${error.stack}`
+ );
+}
+
+/**
+ * Prompt the user for a password and attempt to find calendars with it.
+ *
+ * @param {string} location - The location input from the dialog.
+ */
+function findCalendarsWithPassword(location) {
+ let password = { value: "" };
+ let savePassword = { value: 1 };
+
+ let okWasClicked = new MsgAuthPrompt().promptPassword2(
+ null,
+ cal.l10n.getAnyString("messenger-mapi", "mapi", "loginText", [location]),
+ password,
+ cal.l10n.getAnyString("passwordmgr", "passwordmgr", "rememberPassword"),
+ savePassword
+ );
+
+ if (okWasClicked) {
+ findCalendars(password.value, savePassword.value);
+ } else {
+ selectNetworkStatus("authfail");
+ }
+}
+
+/**
+ * Make preparations on the given calendar (a detected calendar). This
+ * function can be monkeypatched to make general preparations, e.g. for values
+ * from additional form fields.
+ *
+ * @param {calICalendar} calendar - The calendar to prepare.
+ * @returns {calICalendar} The same calendar, prepared with
+ * any extra values.
+ */
+function prepareNetworkCalendar(calendar) {
+ let cached = document.getElementById("network-cache-checkbox").checked;
+
+ if (!calendar.getProperty("cache.always")) {
+ let cacheSupported = calendar.getProperty("cache.supported") !== false;
+ calendar.setProperty("cache.enabled", cacheSupported ? cached : false);
+ }
+
+ return calendar;
+}
+
+/**
+ * The accept button handler for the 'select network calendars' panel.
+ * Subscribes to all of the selected network calendars and allows the dialog to
+ * close.
+ */
+function createNetworkCalendars() {
+ for (let listItem of document.getElementById("network-calendar-list").children) {
+ if (listItem.querySelector(".calendar-selected").checked) {
+ cal.manager.registerCalendar(listItem.calendar);
+ }
+ }
+}
+
+/**
+ * Open the calendar properties dialog for a calendar in the calendar list.
+ *
+ * @param {Event} event - The triggering event.
+ */
+function openCalendarPropertiesFromEvent(event) {
+ let listItem = event.target.closest("richlistitem");
+ if (listItem) {
+ let calendar = listItem.calendar;
+ if (calendar && !calendar.getProperty("disabled")) {
+ cal.window.openCalendarProperties(window, { calendar, canDisable: false });
+
+ // Update the calendar list item.
+ listItem.querySelector(".calendar-name").value = calendar.name;
+ listItem.querySelector(".calendar-color").style.backgroundColor =
+ calendar.getProperty("color");
+ }
+ }
+}
+
+window.addEventListener("load", () => {
+ fillLocationPlaceholder();
+ selectPanel("panel-select-calendar-type");
+ if (window.arguments[0]) {
+ let spec = window.arguments[0].spec;
+ if (/^webcals?:\/\//.test(spec)) {
+ selectPanel("panel-network-calendar-settings");
+ document.getElementById("network-location-input").value = spec;
+ checkRequired();
+ }
+ }
+});