/* 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" ); ChromeUtils.defineESModuleGetters(this, { SubDialogManager: "resource://gre/modules/SubDialog.sys.mjs", }); var { MailE10SUtils } = ChromeUtils.import( "resource:///modules/MailE10SUtils.jsm" ); // Load PrintUtils lazily and modify it to suit. XPCOMUtils.defineLazyGetter(this, "PrintUtils", () => { let scope = {}; Services.scriptloader.loadSubScript( "chrome://global/content/printUtils.js", scope ); scope.PrintUtils.getTabDialogBox = function (browser) { if (!browser.tabDialogBox) { browser.tabDialogBox = new TabDialogBox(browser); } return browser.tabDialogBox; }; scope.PrintUtils.createBrowser = function ({ remoteType, initialBrowsingContextGroupId, userContextId, skipLoad, initiallyActive, } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the // permanentKey is held by something after this window closes, it // doesn't keep the window alive. b.permanentKey = new (Cu.getGlobalForObject(Services).Object)(); const defaultBrowserAttributes = { maychangeremoteness: "true", messagemanagergroup: "browsers", type: "content", }; for (let attribute in defaultBrowserAttributes) { b.setAttribute(attribute, defaultBrowserAttributes[attribute]); } if (userContextId) { b.setAttribute("usercontextid", userContextId); } if (remoteType) { b.setAttribute("remoteType", remoteType); b.setAttribute("remote", "true"); } // Ensure that the browser will be created in a specific initial // BrowsingContextGroup. This may change the process selection behaviour // of the newly created browser, and is often used in combination with // "remoteType" to ensure that the initial about:blank load occurs // within the same process as another window. if (initialBrowsingContextGroupId) { b.setAttribute( "initialBrowsingContextGroupId", initialBrowsingContextGroupId ); } // We set large flex on both containers to allow the devtools toolbox to // set a flex attribute. We don't want the toolbox to actually take up free // space, but we do want it to collapse when the window shrinks, and with // flex=0 it can't. When the toolbox is on the bottom it's a sibling of // browserStack, and when it's on the side it's a sibling of // browserContainer. let stack = document.createXULElement("stack"); stack.className = "browserStack"; stack.appendChild(b); let browserContainer = document.createXULElement("vbox"); browserContainer.className = "browserContainer"; browserContainer.appendChild(stack); let browserSidebarContainer = document.createXULElement("hbox"); browserSidebarContainer.className = "browserSidebarContainer"; browserSidebarContainer.appendChild(browserContainer); // Prevent the superfluous initial load of a blank document // if we're going to load something other than about:blank. if (skipLoad) { b.setAttribute("nodefaultsrc", "true"); } return b; }; scope.PrintUtils.__defineGetter__("printBrowser", () => document.getElementById("hiddenPrintContent") ); scope.PrintUtils.loadPrintBrowser = async function (url) { let printBrowser = this.printBrowser; if (printBrowser.currentURI?.spec == url) { return; } // The template page hasn't been loaded yet. Do that now. await new Promise(resolve => { // Store a strong reference to this progress listener. printBrowser.progressListener = { QueryInterface: ChromeUtils.generateQI([ "nsIWebProgressListener", "nsISupportsWeakReference", ]), /** nsIWebProgressListener */ onStateChange(webProgress, request, stateFlags, status) { if ( stateFlags & Ci.nsIWebProgressListener.STATE_STOP && printBrowser.currentURI.spec != "about:blank" ) { printBrowser.webProgress.removeProgressListener(this); delete printBrowser.progressListener; resolve(); } }, }; printBrowser.webProgress.addProgressListener( printBrowser.progressListener, Ci.nsIWebProgress.NOTIFY_STATE_ALL ); MailE10SUtils.loadURI(printBrowser, url); }); }; return scope.PrintUtils; }); /** * The TabDialogBox supports opening window dialogs as SubDialogs on the tab and content * level. Both tab and content dialogs have their own separate managers. * Dialogs will be queued FIFO and cover the web content. * Dialogs are closed when the user reloads or leaves the page. * While a dialog is open PopupNotifications, such as permission prompts, are * suppressed. */ class TabDialogBox { constructor(browser) { this._weakBrowserRef = Cu.getWeakReference(browser); // Create parent element for tab dialogs let template = document.getElementById("dialogStackTemplate"); this.dialogStack = template.content.cloneNode(true).firstElementChild; this.dialogStack.classList.add("tab-prompt-dialog"); while (browser.ownerDocument != document) { // Find an ancestor in this document so that we can locate the // print preview appropriately. browser = browser.ownerGlobal.browsingContext.embedderElement; } // This differs from Firefox by using a specific ancestor rather // than the parent of the , so that a larger area of the screen // is used for the preview. this.printPreviewStack = document.querySelector(".printPreviewStack"); if (this.printPreviewStack && this.printPreviewStack.contains(browser)) { this.printPreviewStack.appendChild(this.dialogStack); } else { this.printPreviewStack = this.browser.parentNode; this.browser.parentNode.insertBefore( this.dialogStack, this.browser.nextElementSibling ); } // Initially the stack only contains the template let dialogTemplate = this.dialogStack.firstElementChild; // Create dialog manager for prompts at the tab level. this._tabDialogManager = new SubDialogManager({ dialogStack: this.dialogStack, dialogTemplate, orderType: SubDialogManager.ORDER_QUEUE, allowDuplicateDialogs: true, dialogOptions: { consumeOutsideClicks: false, }, }); } /** * Open a dialog on tab or content level. * * @param {string} aURL - URL of the dialog to load in the tab box. * @param {object} [aOptions] * @param {string} [aOptions.features] - Comma separated list of window * features. * @param {boolean} [aOptions.allowDuplicateDialogs] - Whether to allow * showing multiple dialogs with aURL at the same time. If false calls for * duplicate dialogs will be dropped. * @param {string} [aOptions.sizeTo] - Pass "available" to stretch dialog to * roughly content size. * @param {boolean} [aOptions.keepOpenSameOriginNav] - By default dialogs are * aborted on any navigation. * Set to true to keep the dialog open for same origin navigation. * @param {number} [aOptions.modalType] - The modal type to create the dialog for. * By default, we show the dialog for tab prompts. * @returns {object} [result] Returns an object { closedPromise, dialog }. * @returns {Promise} [result.closedPromise] Resolves once the dialog has been closed. * @returns {SubDialog} [result.dialog] A reference to the opened SubDialog. */ open( aURL, { features = null, allowDuplicateDialogs = true, sizeTo, keepOpenSameOriginNav, modalType = null, allowFocusCheckbox = false, } = {}, ...aParams ) { let resolveClosed; let closedPromise = new Promise(resolve => (resolveClosed = resolve)); // Get the dialog manager to open the prompt with. let dialogManager = modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT ? this.getContentDialogManager() : this._tabDialogManager; let hasDialogs = this._tabDialogManager.hasDialogs || this._contentDialogManager?.hasDialogs; if (!hasDialogs) { this._onFirstDialogOpen(); } let closingCallback = event => { if (!hasDialogs) { this._onLastDialogClose(); } if (allowFocusCheckbox && !event.detail?.abort) { this.maybeSetAllowTabSwitchPermission(event.target); } }; if (modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT) { sizeTo = "limitheight"; } // Open dialog and resolve once it has been closed let dialog = dialogManager.open( aURL, { features, allowDuplicateDialogs, sizeTo, closingCallback, closedCallback: resolveClosed, }, ...aParams ); // Marking the dialog externally, instead of passing it as an option. // The SubDialog(Manager) does not care about navigation. // dialog can be null here if allowDuplicateDialogs = false. if (dialog) { dialog._keepOpenSameOriginNav = keepOpenSameOriginNav; } return { closedPromise, dialog }; } _onFirstDialogOpen() { for (let element of this.printPreviewStack.children) { if (element != this.dialogStack) { element.setAttribute("tabDialogShowing", true); } } // Register listeners this._lastPrincipal = this.browser.contentPrincipal; if ("addProgressListener" in this.browser) { this.browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); } } _onLastDialogClose() { for (let element of this.printPreviewStack.children) { if (element != this.dialogStack) { element.removeAttribute("tabDialogShowing"); } } // Clean up listeners if ("removeProgressListener" in this.browser) { this.browser.removeProgressListener(this); } this._lastPrincipal = null; } _buildContentPromptDialog() { let template = document.getElementById("dialogStackTemplate"); let contentDialogStack = template.content.cloneNode(true).firstElementChild; contentDialogStack.classList.add("content-prompt-dialog"); // Create a dialog manager for content prompts. let tabPromptDialog = this.browser.parentNode.querySelector(".tab-prompt-dialog"); this.browser.parentNode.insertBefore(contentDialogStack, tabPromptDialog); let contentDialogTemplate = contentDialogStack.firstElementChild; this._contentDialogManager = new SubDialogManager({ dialogStack: contentDialogStack, dialogTemplate: contentDialogTemplate, orderType: SubDialogManager.ORDER_QUEUE, allowDuplicateDialogs: true, dialogOptions: { consumeOutsideClicks: false, }, }); } handleEvent(event) { if (event.type !== "TabClose") { return; } this.abortAllDialogs(); } abortAllDialogs() { this._tabDialogManager.abortDialogs(); this._contentDialogManager?.abortDialogs(); } focus() { // Prioritize focusing the dialog manager for tab prompts if (this._tabDialogManager._dialogs.length) { this._tabDialogManager.focusTopDialog(); return; } this._contentDialogManager?.focusTopDialog(); } /** * If the user navigates away or refreshes the page, close all dialogs for * the current browser. */ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { if ( !aWebProgress.isTopLevel || aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT ) { return; } // Dialogs can be exempt from closing on same origin location change. let filterFn; // Test for same origin location change if ( this._lastPrincipal?.isSameOrigin( aLocation, this.browser.browsingContext.usePrivateBrowsing ) ) { filterFn = dialog => !dialog._keepOpenSameOriginNav; } this._lastPrincipal = this.browser.contentPrincipal; this._tabDialogManager.abortDialogs(filterFn); this._contentDialogManager?.abortDialogs(filterFn); } get tab() { return document.getElementById("tabmail").getTabForBrowser(this.browser); } get browser() { let browser = this._weakBrowserRef.get(); if (!browser) { throw new Error("Stale dialog box! The associated browser is gone."); } return browser; } getTabDialogManager() { return this._tabDialogManager; } getContentDialogManager() { if (!this._contentDialogManager) { this._buildContentPromptDialog(); } return this._contentDialogManager; } onNextPromptShowAllowFocusCheckboxFor(principal) { this._allowTabFocusByPromptPrincipal = principal; } /** * Sets the "focus-tab-by-prompt" permission for the dialog. */ maybeSetAllowTabSwitchPermission(dialog) { let checkbox = dialog.querySelector("checkbox"); if (checkbox.checked) { Services.perms.addFromPrincipal( this._allowTabFocusByPromptPrincipal, "focus-tab-by-prompt", Services.perms.ALLOW_ACTION ); } // Don't show the "allow tab switch checkbox" for subsequent prompts. this._allowTabFocusByPromptPrincipal = null; } } TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([ "nsIWebProgressListener", "nsISupportsWeakReference", ]);