summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/preferences/preferences.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/preferences/preferences.js')
-rw-r--r--comm/mail/components/preferences/preferences.js453
1 files changed, 453 insertions, 0 deletions
diff --git a/comm/mail/components/preferences/preferences.js b/comm/mail/components/preferences/preferences.js
new file mode 100644
index 0000000000..1a123527ca
--- /dev/null
+++ b/comm/mail/components/preferences/preferences.js
@@ -0,0 +1,453 @@
+/* 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 ../../../../toolkit/content/preferencesBindings.js */
+/* import-globals-from general.js */
+/* import-globals-from compose.js */
+/* import-globals-from downloads.js */
+/* import-globals-from privacy.js */
+/* import-globals-from chat.js */
+/* import-globals-from sync.js */
+/* import-globals-from findInPage.js */
+/* globals gCalendarPane */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+var { calendarDeactivator } = ChromeUtils.import(
+ "resource:///modules/calendar/calCalendarDeactivator.jsm"
+);
+var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm");
+var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm");
+
+var paneDeck = document.getElementById("paneDeck");
+var defaultPane = "paneGeneral";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () {
+ const { SubDialogManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/SubDialog.sys.mjs"
+ );
+ return new SubDialogManager({
+ dialogStack: document.getElementById("dialogStack"),
+ dialogTemplate: document.getElementById("dialogTemplate"),
+ dialogOptions: {
+ styleSheets: [
+ "chrome://messenger/skin/preferences/dialog.css",
+ "chrome://messenger/skin/preferences/preferences.css",
+ ],
+ resizeCallback: ({ title, frame }) => {
+ UIFontSize.registerWindow(frame.contentWindow);
+
+ // Search within main document and highlight matched keyword.
+ gSearchResultsPane.searchWithinNode(title, gSearchResultsPane.query);
+
+ // Search within sub-dialog document and highlight matched keyword.
+ gSearchResultsPane.searchWithinNode(
+ frame.contentDocument.firstElementChild,
+ gSearchResultsPane.query
+ );
+
+ // Creating tooltips for all the instances found
+ for (let node of gSearchResultsPane.listSearchTooltips) {
+ if (!node.tooltipNode) {
+ gSearchResultsPane.createSearchTooltip(
+ node,
+ gSearchResultsPane.query
+ );
+ }
+ }
+
+ // Resize the dialog to fit the content with edited font size.
+ requestAnimationFrame(() => {
+ let dialogs = frame.ownerGlobal.gSubDialog._dialogs;
+ let dialog = dialogs.find(
+ d => d._frame.contentDocument == frame.contentDocument
+ );
+ if (dialog) {
+ UIFontSize.resizeSubDialog(dialog);
+ }
+ });
+ },
+ },
+ });
+});
+
+document.addEventListener("DOMContentLoaded", init, { once: true });
+
+var gCategoryInits = new Map();
+var gLastCategory = { category: undefined, subcategory: undefined };
+
+function init_category_if_required(category) {
+ let categoryInfo = gCategoryInits.get(category);
+ if (!categoryInfo) {
+ throw new Error(
+ "Unknown in-content prefs category! Can't init " + category
+ );
+ }
+ if (categoryInfo.inited) {
+ return null;
+ }
+ return categoryInfo.init();
+}
+
+function register_module(categoryName, categoryObject) {
+ gCategoryInits.set(categoryName, {
+ inited: false,
+ async init() {
+ let template = document.getElementById(categoryName);
+ if (template) {
+ // Replace the template element with the nodes inside of it.
+ let frag = template.content;
+ await document.l10n.translateFragment(frag);
+
+ // Actually insert them into the DOM.
+ document.l10n.pauseObserving();
+ template.replaceWith(frag);
+ document.l10n.resumeObserving();
+
+ // Asks Preferences to update the attribute value of the entire
+ // document again (this can be simplified if we could separate the
+ // preferences of each pane.)
+ Preferences.queueUpdateOfAllElements();
+ }
+ categoryObject.init();
+ this.inited = true;
+ },
+ });
+}
+
+function init() {
+ register_module("paneGeneral", gGeneralPane);
+ register_module("paneCompose", gComposePane);
+ register_module("panePrivacy", gPrivacyPane);
+ register_module("paneCalendar", gCalendarPane);
+ if (AppConstants.NIGHTLY_BUILD) {
+ register_module("paneSync", gSyncPane);
+ }
+ register_module("paneSearchResults", gSearchResultsPane);
+ if (Services.prefs.getBoolPref("mail.chat.enabled")) {
+ register_module("paneChat", gChatPane);
+ } else {
+ // Remove the pane from the DOM so it doesn't get incorrectly included in
+ // the search results.
+ document.getElementById("paneChat").remove();
+ }
+
+ // If no calendar is currently enabled remove it from the DOM so it doesn't
+ // get incorrectly included in the search results.
+ if (!calendarDeactivator.isCalendarActivated) {
+ document.getElementById("paneCalendar").remove();
+ document.getElementById("category-calendar").remove();
+ }
+ gSearchResultsPane.init();
+
+ let categories = document.getElementById("categories");
+ categories.addEventListener("select", event => gotoPref(event.target.value));
+
+ document.documentElement.addEventListener("keydown", event => {
+ if (event.key == "Tab") {
+ categories.setAttribute("keyboard-navigation", "true");
+ } else if ((event.ctrlKey || event.metaKey) && event.key == "f") {
+ document.getElementById("searchInput").focus();
+ event.preventDefault();
+ }
+ });
+
+ categories.addEventListener("mousedown", function () {
+ this.removeAttribute("keyboard-navigation");
+ });
+
+ window.addEventListener("hashchange", onHashChange);
+ let lastSelected = Services.xulStore.getValue(
+ "about:preferences",
+ "paneDeck",
+ "lastSelected"
+ );
+ gotoPref(lastSelected);
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+}
+
+function onHashChange() {
+ gotoPref();
+}
+
+async function gotoPref(aCategory) {
+ let categories = document.getElementById("categories");
+ const kDefaultCategoryInternalName = "paneGeneral";
+ const kDefaultCategory = "general";
+ let hash = document.location.hash;
+
+ let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
+ let breakIndex = category.indexOf("-");
+ // Subcategories allow for selecting smaller sections of the preferences
+ // until proper search support is enabled (bug 1353954).
+ let subcategory = breakIndex != -1 && category.substring(breakIndex + 1);
+ if (subcategory) {
+ category = category.substring(0, breakIndex);
+ }
+ category = friendlyPrefCategoryNameToInternalName(category);
+ if (category != "paneSearchResults") {
+ gSearchResultsPane.query = null;
+ gSearchResultsPane.searchInput.value = "";
+ gSearchResultsPane.getFindSelection(window).removeAllRanges();
+ gSearchResultsPane.removeAllSearchTooltips();
+ gSearchResultsPane.removeAllSearchMenuitemIndicators();
+ } else if (!gSearchResultsPane.searchInput.value) {
+ // Something tried to send us to the search results pane without
+ // a query string. Default to the General pane instead.
+ category = kDefaultCategoryInternalName;
+ document.location.hash = kDefaultCategory;
+ gSearchResultsPane.query = null;
+ }
+
+ // Updating the hash (below) or changing the selected category
+ // will re-enter gotoPref.
+ if (gLastCategory.category == category && !subcategory) {
+ return;
+ }
+
+ let item;
+ if (category != "paneSearchResults") {
+ // Hide second level headers in normal view
+ for (let element of document.querySelectorAll(".search-header")) {
+ element.hidden = true;
+ }
+
+ item = categories.querySelector(".category[value=" + category + "]");
+ if (!item) {
+ category = kDefaultCategoryInternalName;
+ item = categories.querySelector(".category[value=" + category + "]");
+ }
+ }
+
+ if (
+ gLastCategory.category ||
+ category != kDefaultCategoryInternalName ||
+ subcategory
+ ) {
+ let friendlyName = internalPrefCategoryNameToFriendlyName(category);
+ document.location.hash = friendlyName;
+ }
+ // Need to set the gLastCategory before setting categories.selectedItem since
+ // the categories 'select' event will re-enter the gotoPref codepath.
+ gLastCategory.category = category;
+ gLastCategory.subcategory = subcategory;
+ if (item) {
+ categories.selectedItem = item;
+ } else {
+ categories.clearSelection();
+ }
+ window.history.replaceState(category, document.title);
+
+ try {
+ await init_category_if_required(category);
+ } catch (ex) {
+ console.error(
+ new Error(
+ "Error initializing preference category " + category + ": " + ex
+ )
+ );
+ throw ex;
+ }
+
+ // Bail out of this goToPref if the category
+ // or subcategory changed during async operation.
+ if (
+ gLastCategory.category !== category ||
+ gLastCategory.subcategory !== subcategory
+ ) {
+ return;
+ }
+
+ search(category, "data-category");
+
+ let mainContent = document.querySelector(".main-content");
+ mainContent.scrollTop = 0;
+
+ spotlight(subcategory, category);
+
+ document.dispatchEvent(new CustomEvent("paneSelected", { bubbles: true }));
+ document.getElementById("preferencesContainer").scrollTo(0, 0);
+ document.getElementById("paneDeck").setAttribute("lastSelected", category);
+ Services.xulStore.setValue(
+ "about:preferences",
+ "paneDeck",
+ "lastSelected",
+ category
+ );
+}
+
+function friendlyPrefCategoryNameToInternalName(aName) {
+ if (aName.startsWith("pane")) {
+ return aName;
+ }
+ return "pane" + aName.substring(0, 1).toUpperCase() + aName.substr(1);
+}
+
+// This function is duplicated inside of utilityOverlay.js's openPreferences.
+function internalPrefCategoryNameToFriendlyName(aName) {
+ return (aName || "").replace(/^pane./, function (toReplace) {
+ return toReplace[4].toLowerCase();
+ });
+}
+
+function search(aQuery, aAttribute) {
+ let paneDeck = document.getElementById("paneDeck");
+ let elements = paneDeck.children;
+ for (let element of elements) {
+ // If the "data-hidden-from-search" is "true", the
+ // element will not get considered during search.
+ if (
+ element.getAttribute("data-hidden-from-search") != "true" ||
+ element.getAttribute("data-subpanel") == "true"
+ ) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.hidden = false;
+ } else {
+ element.hidden = true;
+ }
+ } else if (
+ element.getAttribute("data-hidden-from-search") == "true" &&
+ !element.hidden
+ ) {
+ element.hidden = true;
+ }
+ element.classList.remove("visually-hidden");
+ }
+
+ let keysets = paneDeck.getElementsByTagName("keyset");
+ for (let element of keysets) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", true);
+ }
+ }
+}
+
+async function spotlight(subcategory, category) {
+ let highlightedElements = document.querySelectorAll(".spotlight");
+ if (highlightedElements.length) {
+ for (let element of highlightedElements) {
+ element.classList.remove("spotlight");
+ }
+ }
+ if (subcategory) {
+ scrollAndHighlight(subcategory, category);
+ }
+}
+
+async function scrollAndHighlight(subcategory, category) {
+ let element = document.querySelector(`[data-subcategory="${subcategory}"]`);
+ if (!element) {
+ return;
+ }
+ let header = getClosestDisplayedHeader(element);
+
+ scrollContentTo(header);
+ element.classList.add("spotlight");
+}
+
+/**
+ * If there is no visible second level header it will return first level header,
+ * otherwise return second level header.
+ *
+ * @returns {Element} The closest displayed header.
+ */
+function getClosestDisplayedHeader(element) {
+ let header = element.closest("groupbox");
+ let searchHeader = header.querySelector(".search-header");
+ if (
+ searchHeader &&
+ searchHeader.hidden &&
+ header.previousElementSibling.classList.contains("subcategory")
+ ) {
+ header = header.previousElementSibling;
+ }
+ return header;
+}
+
+function scrollContentTo(element) {
+ const STICKY_CONTAINER_HEIGHT =
+ document.querySelector(".sticky-container").clientHeight;
+ let mainContent = document.querySelector(".main-content");
+ let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT;
+ mainContent.scroll({
+ top,
+ behavior: "smooth",
+ });
+}
+
+/**
+ * Selects the specified preferences pane
+ *
+ * @param paneID ID of prefpane to select
+ * @param scrollPaneTo ID of the element to scroll into view
+ * @param otherArgs.subdialog ID of button to activate, opening a subdialog
+ */
+function selectPrefPane(paneID, scrollPaneTo, otherArgs) {
+ if (paneID) {
+ if (gLastCategory.category != paneID) {
+ gotoPref(paneID);
+ }
+ if (scrollPaneTo) {
+ showTab(scrollPaneTo, otherArgs ? otherArgs.subdialog : undefined);
+ }
+ }
+}
+
+/**
+ * Select the specified tab
+ *
+ * @param scrollPaneTo ID of the element to scroll into view
+ * @param subdialogID ID of button to activate, opening a subdialog
+ */
+function showTab(scrollPaneTo, subdialogID) {
+ setTimeout(function () {
+ let scrollTarget = document.getElementById(scrollPaneTo);
+ if (scrollTarget.closest("groupbox")) {
+ scrollTarget = scrollTarget.closest("groupbox");
+ }
+ scrollTarget.scrollIntoView();
+ if (subdialogID) {
+ document.getElementById(subdialogID).click();
+ }
+ });
+}
+
+/**
+ * Filter the lastFallbackLocale from availableLocales if it doesn't have all
+ * of the needed strings.
+ *
+ * When the lastFallbackLocale isn't the defaultLocale, then by default only
+ * fluent strings are included. To fully use that locale you need the langpack
+ * to be installed, so if it isn't installed remove it from availableLocales.
+ */
+async function getAvailableLocales() {
+ let { availableLocales, defaultLocale, lastFallbackLocale } = Services.locale;
+ // If defaultLocale isn't lastFallbackLocale, then we still need the langpack
+ // for lastFallbackLocale for it to be useful.
+ if (defaultLocale != lastFallbackLocale) {
+ let lastFallbackId = `langpack-${lastFallbackLocale}@thunderbird.mozilla.org`;
+ let lastFallbackInstalled = await AddonManager.getAddonByID(lastFallbackId);
+ if (!lastFallbackInstalled) {
+ return availableLocales.filter(locale => locale != lastFallbackLocale);
+ }
+ }
+ return availableLocales;
+}