preferences.js (26895B)
1 /* - This Source Code Form is subject to the terms of the Mozilla Public 2 - License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 - You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // Import globals from the files imported by the .xul files. 6 /* import-globals-from main.js */ 7 /* import-globals-from home.js */ 8 /* import-globals-from search.js */ 9 /* import-globals-from containers.js */ 10 /* import-globals-from privacy.js */ 11 /* import-globals-from sync.js */ 12 /* import-globals-from experimental.js */ 13 /* import-globals-from moreFromMozilla.js */ 14 /* import-globals-from findInPage.js */ 15 /* import-globals-from /browser/base/content/utilityOverlay.js */ 16 /* import-globals-from /toolkit/content/preferencesBindings.js */ 17 /* import-globals-from ../torpreferences/content/connectionPane.js */ 18 19 /** @import MozButton from "chrome://global/content/elements/moz-button.mjs" */ 20 /** @import {SettingConfig, SettingEmitChange} from "chrome://global/content/preferences/Setting.mjs" */ 21 /** @import {SettingControlConfig} from "chrome://browser/content/preferences/widgets/setting-control.mjs" */ 22 /** @import {SettingGroup} from "chrome://browser/content/preferences/widgets/setting-group.mjs" */ 23 /** @import {SettingPane, SettingPaneConfig} from "chrome://browser/content/preferences/widgets/setting-pane.mjs" */ 24 25 "use strict"; 26 27 var { AppConstants } = ChromeUtils.importESModule( 28 "resource://gre/modules/AppConstants.sys.mjs" 29 ); 30 31 var { Downloads } = ChromeUtils.importESModule( 32 "resource://gre/modules/Downloads.sys.mjs" 33 ); 34 var { Integration } = ChromeUtils.importESModule( 35 "resource://gre/modules/Integration.sys.mjs" 36 ); 37 /* global DownloadIntegration */ 38 Integration.downloads.defineESModuleGetter( 39 this, 40 "DownloadIntegration", 41 "resource://gre/modules/DownloadIntegration.sys.mjs" 42 ); 43 44 var { PrivateBrowsingUtils } = ChromeUtils.importESModule( 45 "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" 46 ); 47 48 var { Weave } = ChromeUtils.importESModule( 49 "resource://services-sync/main.sys.mjs" 50 ); 51 52 var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule( 53 "resource://gre/modules/FxAccounts.sys.mjs" 54 ); 55 var fxAccounts = getFxAccountsSingleton(); 56 57 XPCOMUtils.defineLazyServiceGetters(this, { 58 gApplicationUpdateService: [ 59 "@mozilla.org/updates/update-service;1", 60 Ci.nsIApplicationUpdateService, 61 ], 62 63 listManager: [ 64 "@mozilla.org/url-classifier/listmanager;1", 65 Ci.nsIUrlListManager, 66 ], 67 gHandlerService: [ 68 "@mozilla.org/uriloader/handler-service;1", 69 Ci.nsIHandlerService, 70 ], 71 gMIMEService: ["@mozilla.org/mime;1", Ci.nsIMIMEService], 72 }); 73 74 if (Cc["@mozilla.org/gio-service;1"]) { 75 XPCOMUtils.defineLazyServiceGetter( 76 this, 77 "gGIOService", 78 "@mozilla.org/gio-service;1", 79 Ci.nsIGIOService 80 ); 81 } else { 82 this.gGIOService = null; 83 } 84 85 ChromeUtils.defineESModuleGetters(this, { 86 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 87 ContextualIdentityService: 88 "resource://gre/modules/ContextualIdentityService.sys.mjs", 89 DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", 90 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", 91 ExtensionPreferencesManager: 92 "resource://gre/modules/ExtensionPreferencesManager.sys.mjs", 93 ExtensionSettingsStore: 94 "resource://gre/modules/ExtensionSettingsStore.sys.mjs", 95 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 96 FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs", 97 HomePage: "resource:///modules/HomePage.sys.mjs", 98 LangPackMatcher: "resource://gre/modules/LangPackMatcher.sys.mjs", 99 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", 100 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 101 OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", 102 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 103 Region: "resource://gre/modules/Region.sys.mjs", 104 SelectionChangedMenulist: 105 "resource:///modules/SelectionChangedMenulist.sys.mjs", 106 ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", 107 SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs", 108 TransientPrefs: "resource:///modules/TransientPrefs.sys.mjs", 109 UIState: "resource://services-sync/UIState.sys.mjs", 110 UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", 111 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 112 }); 113 114 ChromeUtils.defineLazyGetter(this, "gSubDialog", function () { 115 const { SubDialogManager } = ChromeUtils.importESModule( 116 "resource://gre/modules/SubDialog.sys.mjs" 117 ); 118 return new SubDialogManager({ 119 dialogStack: document.getElementById("dialogStack"), 120 dialogTemplate: document.getElementById("dialogTemplate"), 121 dialogOptions: { 122 styleSheets: [ 123 "chrome://browser/skin/preferences/dialog.css", 124 "chrome://browser/skin/preferences/preferences.css", 125 ], 126 resizeCallback: async ({ title, frame }) => { 127 // Search within main document and highlight matched keyword. 128 await gSearchResultsPane.searchWithinNode( 129 title, 130 gSearchResultsPane.query 131 ); 132 133 // Search within sub-dialog document and highlight matched keyword. 134 await gSearchResultsPane.searchWithinNode( 135 frame.contentDocument.firstElementChild, 136 gSearchResultsPane.query 137 ); 138 139 // Creating tooltips for all the instances found 140 for (let node of gSearchResultsPane.listSearchTooltips) { 141 if (!node.tooltipNode) { 142 gSearchResultsPane.createSearchTooltip( 143 node, 144 gSearchResultsPane.query 145 ); 146 } 147 } 148 }, 149 }, 150 }); 151 }); 152 153 /** @type {Record<string, boolean>} */ 154 const srdSectionPrefs = {}; 155 XPCOMUtils.defineLazyPreferenceGetter( 156 srdSectionPrefs, 157 "all", 158 "browser.settings-redesign.enabled", 159 false 160 ); 161 162 /** 163 * @param {string} section 164 */ 165 function srdSectionEnabled(section) { 166 if (!(section in srdSectionPrefs)) { 167 XPCOMUtils.defineLazyPreferenceGetter( 168 srdSectionPrefs, 169 section, 170 `browser.settings-redesign.${section}.enabled`, 171 false 172 ); 173 } 174 return srdSectionPrefs.all || srdSectionPrefs[section]; 175 } 176 177 var SettingPaneManager = { 178 /** @type {Map<string, SettingPaneConfig>} */ 179 _data: new Map(), 180 181 /** 182 * @param {string} id 183 */ 184 get(id) { 185 if (!this._data.has(id)) { 186 throw new Error(`Setting pane "${id}" not found`); 187 } 188 return this._data.get(id); 189 }, 190 191 /** 192 * @param {string} id 193 * @param {SettingPaneConfig} config 194 */ 195 registerPane(id, config) { 196 if (this._data.has(id)) { 197 throw new Error(`Setting pane "${id}" already registered`); 198 } 199 this._data.set(id, config); 200 let subPane = friendlyPrefCategoryNameToInternalName(id); 201 let settingPane = /** @type {SettingPane} */ ( 202 document.createElement("setting-pane") 203 ); 204 settingPane.name = subPane; 205 settingPane.config = config; 206 settingPane.isSubPane = !!config.parent; 207 document.getElementById("mainPrefPane").append(settingPane); 208 register_module(subPane, { 209 init() { 210 settingPane.init(); 211 }, 212 }); 213 }, 214 215 /** 216 * @param {Record<string, SettingPaneConfig>} paneConfigs 217 */ 218 registerPanes(paneConfigs) { 219 for (let id in paneConfigs) { 220 this.registerPane(id, paneConfigs[id]); 221 } 222 }, 223 }; 224 225 var SettingGroupManager = ChromeUtils.importESModule( 226 "chrome://browser/content/preferences/config/SettingGroupManager.mjs", 227 { 228 global: "current", 229 } 230 ).SettingGroupManager; 231 232 /** 233 * Register initial config-based setting panes here. If you need to register a 234 * pane elsewhere, use {@link SettingPaneManager['registerPane']}. 235 * 236 * @type {Record<string, SettingPaneConfig>} 237 */ 238 const CONFIG_PANES = Object.freeze({ 239 containers2: { 240 parent: "general", 241 l10nId: "containers-section-header", 242 groupIds: ["containers"], 243 }, 244 dnsOverHttps: { 245 skip: true, // Skip DNS over HTTPS (DoH). tor-browser#41906. 246 parent: "privacy", 247 l10nId: "preferences-doh-header2", 248 groupIds: ["dnsOverHttpsAdvanced"], 249 }, 250 managePayments: { 251 skip: true, 252 parent: "privacy", 253 l10nId: "autofill-payment-methods-manage-payments-title", 254 groupIds: ["managePayments"], 255 }, 256 paneProfiles: { 257 parent: "general", 258 l10nId: "preferences-profiles-group-header", 259 groupIds: ["profilePane"], 260 }, 261 etp: { 262 skip: true, // Skip enhanced tracking protection. tor-browser#33848. 263 parent: "privacy", 264 l10nId: "preferences-etp-header", 265 groupIds: ["etpBanner", "etpAdvanced"], 266 }, 267 etpCustomize: { 268 parent: "etp", 269 l10nId: "preferences-etp-customize-header", 270 groupIds: ["etpReset", "etpCustomize"], 271 }, 272 manageAddresses: { 273 skip: true, 274 parent: "privacy", 275 l10nId: "autofill-addresses-manage-addresses-title", 276 groupIds: ["manageAddresses"], 277 }, 278 translations: { 279 skip: true, // Skip translations. tor-browser#44710. 280 parent: "general", 281 l10nId: "settings-translations-subpage-header", 282 groupIds: [ 283 "translationsAutomaticTranslation", 284 "translationsDownloadLanguages", 285 ], 286 iconSrc: "chrome://browser/skin/translations.svg", 287 }, 288 aiFeatures: { 289 skip: true, // Skip AI pane. tor-browser#44709. 290 l10nId: "preferences-ai-features-header", 291 groupIds: ["debugModelManagement", "aiFeatures", "aiWindowFeatures"], 292 module: "chrome://browser/content/preferences/config/aiFeatures.mjs", 293 visible: () => srdSectionEnabled("aiFeatures"), 294 }, 295 history: { 296 parent: "privacy", 297 l10nId: "history-header2", 298 groupIds: ["historyAdvanced"], 299 }, 300 }); 301 302 var gLastCategory = { category: undefined, subcategory: undefined }; 303 const gXULDOMParser = new DOMParser(); 304 var gCategoryModules = new Map(); 305 var gCategoryInits = new Map(); 306 307 function register_module(categoryName, categoryObject) { 308 gCategoryModules.set(categoryName, categoryObject); 309 gCategoryInits.set(categoryName, { 310 _initted: false, 311 init() { 312 let startTime = ChromeUtils.now(); 313 if (this._initted) { 314 return; 315 } 316 this._initted = true; 317 let template = document.getElementById("template-" + categoryName); 318 if (template) { 319 // Replace the template element with the nodes inside of it. 320 template.replaceWith(template.content); 321 322 // We've inserted elements that rely on 'preference' attributes. 323 // So we need to update those by reading from the prefs. 324 // The bindings will do this using idle dispatch and avoid 325 // repeated runs if called multiple times before the task runs. 326 Preferences.queueUpdateOfAllElements(); 327 } 328 329 categoryObject.init(); 330 ChromeUtils.addProfilerMarker( 331 "Preferences", 332 { startTime }, 333 categoryName + " init" 334 ); 335 }, 336 }); 337 } 338 339 document.addEventListener("DOMContentLoaded", init_all, { once: true }); 340 341 function init_all() { 342 Preferences.forceEnableInstantApply(); 343 344 // Asks Preferences to queue an update of the attribute values of 345 // the entire document. 346 Preferences.queueUpdateOfAllElements(); 347 348 register_module("paneGeneral", gMainPane); 349 register_module("paneHome", gHomePane); 350 register_module("paneSearch", gSearchPane); 351 register_module("panePrivacy", gPrivacyPane); 352 register_module("paneContainers", gContainersPane); 353 354 for (let [id, config] of Object.entries(CONFIG_PANES)) { 355 // Skip over configs we do not want, including all its children. 356 // See tor-browser#44711. 357 let skip = false; 358 let parentConfig = config; 359 while (parentConfig) { 360 skip = parentConfig.skip; 361 if (skip) { 362 break; 363 } 364 parentConfig = parentConfig.parent 365 ? CONFIG_PANES[parentConfig.parent] 366 : undefined; 367 } 368 if (skip) { 369 continue; 370 } 371 SettingPaneManager.registerPane(id, config); 372 } 373 374 if (ExperimentAPI.labsEnabled) { 375 // Set hidden based on previous load's hidden value or if Nimbus is 376 // disabled. 377 document.getElementById("category-experimental").hidden = 378 Services.prefs.getBoolPref( 379 "browser.preferences.experimental.hidden", 380 false 381 ); 382 register_module("paneExperimental", gExperimentalPane); 383 } else { 384 document.getElementById("category-experimental").hidden = true; 385 } 386 387 NimbusFeatures.moreFromMozilla.recordExposureEvent({ once: true }); 388 if (NimbusFeatures.moreFromMozilla.getVariable("enabled")) { 389 document.getElementById("category-more-from-mozilla").hidden = false; 390 gMoreFromMozillaPane.option = 391 NimbusFeatures.moreFromMozilla.getVariable("template"); 392 register_module("paneMoreFromMozilla", gMoreFromMozillaPane); 393 } 394 // The Sync category needs to be the last of the "real" categories 395 // registered and inititalized since many tests wait for the 396 // "sync-pane-loaded" observer notification before starting the test. 397 if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) { 398 document.getElementById("category-sync").hidden = false; 399 register_module("paneSync", gSyncPane); 400 } 401 register_module("paneSearchResults", gSearchResultsPane); 402 if (gConnectionPane.enabled) { 403 document.getElementById("category-connection").hidden = false; 404 register_module("paneConnection", gConnectionPane); 405 } else { 406 // Remove the pane from the DOM so it doesn't get incorrectly included in search results. 407 document.getElementById("template-paneConnection").remove(); 408 } 409 410 gSearchResultsPane.init(); 411 gMainPane.preInit(); 412 413 let categories = document.getElementById("categories"); 414 categories.addEventListener("select", event => gotoPref(event.target.value)); 415 416 document.documentElement.addEventListener("keydown", function (event) { 417 if (event.keyCode == KeyEvent.DOM_VK_TAB) { 418 categories.setAttribute("keyboard-navigation", "true"); 419 } 420 }); 421 categories.addEventListener("mousedown", function () { 422 this.removeAttribute("keyboard-navigation"); 423 }); 424 425 maybeDisplayPoliciesNotice(); 426 427 window.addEventListener("hashchange", onHashChange); 428 429 document.getElementById("focusSearch1").addEventListener("command", () => { 430 gSearchResultsPane.searchInput.focus(); 431 }); 432 433 gotoPref().then(() => { 434 document.getElementById("addonsButton").addEventListener("click", e => { 435 e.preventDefault(); 436 if (e.button >= 2) { 437 // Ignore right clicks. 438 return; 439 } 440 let mainWindow = window.browsingContext.topChromeWindow; 441 mainWindow.BrowserAddonUI.openAddonsMgr(); 442 }); 443 444 document.dispatchEvent( 445 new CustomEvent("Initialized", { 446 bubbles: true, 447 cancelable: true, 448 }) 449 ); 450 }); 451 } 452 453 function onHashChange() { 454 gotoPref(null, "Hash"); 455 } 456 457 /** 458 * @param {string} [aCategory] The pane to show, defaults to the hash of URL or general 459 * @param {"Click"|"Initial"|"Hash"} [aShowReason] 460 * What triggered the navigation. Defaults to "Click" if aCategory is provided, 461 * otherwise "Initial". 462 */ 463 async function gotoPref( 464 aCategory, 465 aShowReason = aCategory ? "Click" : "Initial" 466 ) { 467 let categories = document.getElementById("categories"); 468 const kDefaultCategoryInternalName = "paneGeneral"; 469 const kDefaultCategory = "general"; 470 let hash = document.location.hash; 471 let category = aCategory || hash.substring(1) || kDefaultCategoryInternalName; 472 473 let breakIndex = category.indexOf("-"); 474 // Subcategories allow for selecting smaller sections of the preferences 475 // until proper search support is enabled (bug 1353954). 476 let subcategory = breakIndex != -1 && category.substring(breakIndex + 1); 477 if (subcategory) { 478 category = category.substring(0, breakIndex); 479 } 480 category = friendlyPrefCategoryNameToInternalName(category); 481 if (category != "paneSearchResults") { 482 gSearchResultsPane.query = null; 483 gSearchResultsPane.searchInput.value = ""; 484 gSearchResultsPane.removeAllSearchIndicators(window, true); 485 } else if (!gSearchResultsPane.searchInput.value) { 486 // Something tried to send us to the search results pane without 487 // a query string. Default to the General pane instead. 488 category = kDefaultCategoryInternalName; 489 document.location.hash = kDefaultCategory; 490 gSearchResultsPane.query = null; 491 } 492 493 // Updating the hash (below) or changing the selected category 494 // will re-enter gotoPref. 495 if (gLastCategory.category == category && !subcategory) { 496 return; 497 } 498 499 let item; 500 let unknownCategory = false; 501 if (category != "paneSearchResults") { 502 // Hide second level headers in normal view 503 for (let element of document.querySelectorAll(".search-header")) { 504 element.hidden = true; 505 } 506 507 item = /** @type {HTMLElement} */ ( 508 categories.querySelector(".category[value=" + CSS.escape(category) + "]") 509 ); 510 if (!item || item.hidden) { 511 unknownCategory = true; 512 category = kDefaultCategoryInternalName; 513 item = categories.querySelector(".category[value=" + category + "]"); 514 } 515 } 516 517 if ( 518 gLastCategory.category || 519 unknownCategory || 520 category != kDefaultCategoryInternalName || 521 subcategory 522 ) { 523 let friendlyName = internalPrefCategoryNameToFriendlyName(category); 524 // Overwrite the hash, unless there is no hash and we're switching to the 525 // default category, e.g. by using the 'back' button after navigating to 526 // a different category. 527 528 // Note: Bug 1983388 - If there is an element in the DOM that has the same 529 // ID as the `friendlyName`, then focus will be lost when navigating the 530 // category navigation via keyboard when that `friendlyName` category is selected. 531 if ( 532 !(!document.location.hash && category == kDefaultCategoryInternalName) 533 ) { 534 document.location.hash = friendlyName; 535 } 536 } 537 // Need to set the gLastCategory before setting categories.selectedItem since 538 // the categories 'select' event will re-enter the gotoPref codepath. 539 gLastCategory.category = category; 540 gLastCategory.subcategory = subcategory; 541 if (item) { 542 // @ts-ignore MozElements.RichListBox 543 categories.selectedItem = item; 544 } else { 545 // @ts-ignore MozElements.RichListBox 546 categories.clearSelection(); 547 } 548 window.history.replaceState(category, document.title); 549 550 let categoryInfo = gCategoryInits.get(category); 551 if (!categoryInfo) { 552 let err = new Error( 553 "Unknown in-content prefs category! Can't init " + category 554 ); 555 console.error(err); 556 throw err; 557 } 558 categoryInfo.init(); 559 560 if (document.hasPendingL10nMutations) { 561 await new Promise(r => 562 document.addEventListener("L10nMutationsFinished", r, { once: true }) 563 ); 564 // Bail out of this goToPref if the category 565 // or subcategory changed during async operation. 566 if ( 567 gLastCategory.category !== category || 568 gLastCategory.subcategory !== subcategory 569 ) { 570 return; 571 } 572 } 573 574 search(category, "data-category"); 575 576 if (aShowReason != "Initial") { 577 document.querySelector(".main-content").scrollTop = 0; 578 } 579 580 // Check to see if the category module wants to do any special 581 // handling of the subcategory - for example, opening a SubDialog. 582 // 583 // If not, just do a normal spotlight on the subcategory. 584 let categoryModule = gCategoryModules.get(category); 585 if (!categoryModule.handleSubcategory?.(subcategory)) { 586 spotlight(subcategory, category); 587 } 588 589 // Handle any visibility changes that are controlled by pref logic. 590 // 591 // Take caution when trying to flip the hidden state to true since the 592 // element might show up unexpectedly on different pages in about:preferences. 593 // 594 // See Bug 1999032 to remove this in favor of config-based prefs. 595 categoryModule.handlePrefControlledSection?.(); 596 597 // Record which category is shown 598 let gleanId = /** @type {"showClick" | "showHash" | "showInitial"} */ ( 599 "show" + aShowReason 600 ); 601 Glean.aboutpreferences[gleanId].record({ value: category }); 602 603 document.dispatchEvent( 604 new CustomEvent("paneshown", { 605 bubbles: true, 606 cancelable: true, 607 detail: { 608 category, 609 }, 610 }) 611 ); 612 } 613 614 /** 615 * @param {string} aQuery 616 * @param {string} aAttribute 617 */ 618 function search(aQuery, aAttribute) { 619 let mainPrefPane = document.getElementById("mainPrefPane"); 620 let elements = /** @type {HTMLElement[]} */ ( 621 Array.from(mainPrefPane.children) 622 ); 623 for (let element of elements) { 624 // If the "data-hidden-from-search" is "true", the 625 // element will not get considered during search. 626 if ( 627 element.getAttribute("data-hidden-from-search") != "true" || 628 element.getAttribute("data-subpanel") == "true" 629 ) { 630 let attributeValue = element.getAttribute(aAttribute); 631 if (attributeValue == aQuery) { 632 element.hidden = false; 633 } else { 634 element.hidden = true; 635 } 636 } else if ( 637 element.getAttribute("data-hidden-from-search") == "true" && 638 !element.hidden 639 ) { 640 element.hidden = true; 641 } 642 element.classList.remove("visually-hidden"); 643 } 644 } 645 646 function spotlight(subcategory, category) { 647 let highlightedElements = document.querySelectorAll(".spotlight"); 648 if (highlightedElements.length) { 649 for (let element of highlightedElements) { 650 element.classList.remove("spotlight"); 651 } 652 } 653 if (subcategory) { 654 scrollAndHighlight(subcategory, category); 655 } 656 } 657 658 function scrollAndHighlight(subcategory) { 659 let element = document.querySelector(`[data-subcategory="${subcategory}"]`); 660 if (!element) { 661 return; 662 } 663 664 // We assign a tabindex=-1 to the element so that we can focus it. This allows 665 // us to move screen reader's focus to an arbitrary position on the page. 666 // See tor-browser#41454 and mozilla bug 1799153. 667 const doFocus = () => { 668 element.setAttribute("tabindex", "-1"); 669 Services.focus.setFocus(element, Services.focus.FLAG_NOSCROLL); 670 // Immediately remove again now that it has focus. 671 element.removeAttribute("tabindex"); 672 }; 673 // The element is not always immediately focusable, so we wait until document 674 // load. 675 if (document.readyState === "complete") { 676 doFocus(); 677 } else { 678 // Wait until document load to move focus. 679 // NOTE: This should be called after DOMContentLoaded, where the searchInput 680 // is focused. 681 window.addEventListener("load", doFocus, { once: true }); 682 } 683 684 element.scrollIntoView({ 685 behavior: "smooth", 686 block: "center", 687 }); 688 element.classList.add("spotlight"); 689 } 690 691 function friendlyPrefCategoryNameToInternalName(aName) { 692 if (aName.startsWith("pane")) { 693 return aName; 694 } 695 return "pane" + aName.substring(0, 1).toUpperCase() + aName.substr(1); 696 } 697 698 // This function is duplicated inside of utilityOverlay.js's openPreferences. 699 function internalPrefCategoryNameToFriendlyName(aName) { 700 return (aName || "").replace(/^pane./, function (toReplace) { 701 return toReplace[4].toLowerCase(); 702 }); 703 } 704 705 // Put up a confirm dialog with "ok to restart", "revert without restarting" 706 // and "restart later" buttons and returns the index of the button chosen. 707 // We can choose not to display the "restart later", or "revert" buttons, 708 // altough the later still lets us revert by using the escape key. 709 // 710 // The constants are useful to interpret the return value of the function. 711 const CONFIRM_RESTART_PROMPT_RESTART_NOW = 0; 712 const CONFIRM_RESTART_PROMPT_CANCEL = 1; 713 const CONFIRM_RESTART_PROMPT_RESTART_LATER = 2; 714 async function confirmRestartPrompt( 715 aRestartToEnable, 716 aDefaultButtonIndex, 717 aWantRevertAsCancelButton, 718 aWantRestartLaterButton 719 ) { 720 let [ 721 msg, 722 title, 723 restartButtonText, 724 noRestartButtonText, 725 restartLaterButtonText, 726 ] = await document.l10n.formatValues([ 727 { 728 id: aRestartToEnable 729 ? "feature-enable-requires-restart" 730 : "feature-disable-requires-restart", 731 }, 732 { id: "should-restart-title" }, 733 { id: "should-restart-ok" }, 734 { id: "cancel-no-restart-button" }, 735 { id: "restart-later" }, 736 ]); 737 738 // Set up the first (index 0) button: 739 let buttonFlags = 740 Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING; 741 742 // Set up the second (index 1) button: 743 if (aWantRevertAsCancelButton) { 744 buttonFlags += 745 Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING; 746 } else { 747 noRestartButtonText = null; 748 buttonFlags += 749 Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL; 750 } 751 752 // Set up the third (index 2) button: 753 if (aWantRestartLaterButton) { 754 buttonFlags += 755 Services.prompt.BUTTON_POS_2 * Services.prompt.BUTTON_TITLE_IS_STRING; 756 } else { 757 restartLaterButtonText = null; 758 } 759 760 switch (aDefaultButtonIndex) { 761 case 0: 762 buttonFlags += Services.prompt.BUTTON_POS_0_DEFAULT; 763 break; 764 case 1: 765 buttonFlags += Services.prompt.BUTTON_POS_1_DEFAULT; 766 break; 767 case 2: 768 buttonFlags += Services.prompt.BUTTON_POS_2_DEFAULT; 769 break; 770 default: 771 break; 772 } 773 774 let button = await Services.prompt.asyncConfirmEx( 775 window.browsingContext, 776 Ci.nsIPrompt.MODAL_TYPE_CONTENT, 777 title, 778 msg, 779 buttonFlags, 780 restartButtonText, 781 noRestartButtonText, 782 restartLaterButtonText, 783 null, 784 {} 785 ); 786 787 let buttonIndex = button.get("buttonNumClicked"); 788 789 // If we have the second confirmation dialog for restart, see if the user 790 // cancels out at that point. 791 if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) { 792 let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( 793 Ci.nsISupportsPRBool 794 ); 795 Services.obs.notifyObservers( 796 cancelQuit, 797 "quit-application-requested", 798 "restart" 799 ); 800 if (cancelQuit.data) { 801 buttonIndex = CONFIRM_RESTART_PROMPT_CANCEL; 802 } 803 } 804 return buttonIndex; 805 } 806 807 // This function is used to append search keywords found 808 // in the related subdialog to the button that will activate the subdialog. 809 function appendSearchKeywords(aId, keywords) { 810 let element = document.getElementById(aId); 811 let searchKeywords = element.getAttribute("searchkeywords"); 812 if (searchKeywords) { 813 keywords.push(searchKeywords); 814 } 815 element.setAttribute("searchkeywords", keywords.join(" ")); 816 } 817 818 async function ensureScrollPadding() { 819 let stickyContainer = document.querySelector(".sticky-container"); 820 let height = await window.browsingContext.topChromeWindow 821 .promiseDocumentFlushed(() => stickyContainer.clientHeight) 822 .catch(console.error); // Can reject if the window goes away. 823 824 // Make it a bit more, to ensure focus rectangles etc. don't get cut off. 825 // This being 8px causes us to end up with 90px if the policies container 826 // is not visible (the common case), which matches the CSS and thus won't 827 // cause a style change, repaint, or other changes. 828 height += 8; 829 stickyContainer 830 .closest(".main-content") 831 .style.setProperty("scroll-padding-top", height + "px"); 832 } 833 834 function maybeDisplayPoliciesNotice() { 835 if (Services.policies.status == Services.policies.ACTIVE) { 836 document.getElementById("policies-container").removeAttribute("hidden"); 837 } 838 ensureScrollPadding(); 839 }