diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mail/base/content/printUtils.js | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/comm/mail/base/content/printUtils.js b/comm/mail/base/content/printUtils.js new file mode 100644 index 0000000000..129cc6a41a --- /dev/null +++ b/comm/mail/base/content/printUtils.js @@ -0,0 +1,428 @@ +/* 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 <browser> 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 <stack> rather + // than the parent of the <browser>, 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", +]); |