summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/content/printUtils.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mail/base/content/printUtils.js428
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",
+]);