forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1178 lines
		
	
	
	
		
			37 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1178 lines
		
	
	
	
		
			37 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* - 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| let lazy = {};
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   lazy,
 | |
|   "dragService",
 | |
|   "@mozilla.org/widget/dragservice;1",
 | |
|   "nsIDragService"
 | |
| );
 | |
| 
 | |
| /**
 | |
|  * The SubDialog resize callback.
 | |
|  * @callback SubDialog~resizeCallback
 | |
|  * @param {DOMNode} title - The title element of the dialog.
 | |
|  * @param {xul:browser} frame - The browser frame of the dialog.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * SubDialog constructor creates a new subdialog from a template and appends
 | |
|  * it to the parentElement.
 | |
|  * @param {DOMNode} template - The template is copied to create a new dialog.
 | |
|  * @param {DOMNode} parentElement - New dialog is appended onto parentElement.
 | |
|  * @param {String}  id - A unique identifier for the dialog.
 | |
|  * @param {Object}  dialogOptions - Dialog options object.
 | |
|  * @param {String[]} [dialogOptions.styleSheets] - An array of URLs to additional
 | |
|  * stylesheets to inject into the frame.
 | |
|  * @param {Boolean} [consumeOutsideClicks] - Whether to close the dialog when
 | |
|  * its background overlay is clicked.
 | |
|  * @param {SubDialog~resizeCallback} [resizeCallback] - Function to be called on
 | |
|  * dialog resize.
 | |
|  */
 | |
| export function SubDialog({
 | |
|   template,
 | |
|   parentElement,
 | |
|   id,
 | |
|   dialogOptions: {
 | |
|     styleSheets = [],
 | |
|     consumeOutsideClicks = true,
 | |
|     resizeCallback,
 | |
|   } = {},
 | |
| }) {
 | |
|   this._id = id;
 | |
| 
 | |
|   this._injectedStyleSheets = this._injectedStyleSheets.concat(styleSheets);
 | |
|   this._consumeOutsideClicks = consumeOutsideClicks;
 | |
|   this._resizeCallback = resizeCallback;
 | |
|   this._overlay = template.cloneNode(true);
 | |
|   this._box = this._overlay.querySelector(".dialogBox");
 | |
|   this._titleBar = this._overlay.querySelector(".dialogTitleBar");
 | |
|   this._titleElement = this._overlay.querySelector(".dialogTitle");
 | |
|   this._closeButton = this._overlay.querySelector(".dialogClose");
 | |
|   this._frame = this._overlay.querySelector(".dialogFrame");
 | |
| 
 | |
|   this._overlay.classList.add(`dialogOverlay-${id}`);
 | |
|   this._frame.setAttribute("name", `dialogFrame-${id}`);
 | |
|   this._frameCreated = new Promise(resolve => {
 | |
|     this._frame.addEventListener(
 | |
|       "load",
 | |
|       () => {
 | |
|         // We intentionally avoid handling or passing the event to the
 | |
|         // resolve method to avoid shutdown window leaks. See bug 1686743.
 | |
|         resolve();
 | |
|       },
 | |
|       {
 | |
|         once: true,
 | |
|         capture: true,
 | |
|       }
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   parentElement.appendChild(this._overlay);
 | |
|   this._overlay.hidden = false;
 | |
| }
 | |
| 
 | |
| SubDialog.prototype = {
 | |
|   _closingCallback: null,
 | |
|   _closingEvent: null,
 | |
|   _isClosing: false,
 | |
|   _frame: null,
 | |
|   _frameCreated: null,
 | |
|   _overlay: null,
 | |
|   _box: null,
 | |
|   _openedURL: null,
 | |
|   _injectedStyleSheets: ["chrome://global/skin/in-content/common.css"],
 | |
|   _resizeObserver: null,
 | |
|   _template: null,
 | |
|   _id: null,
 | |
|   _titleElement: null,
 | |
|   _closeButton: null,
 | |
| 
 | |
|   get frameContentWindow() {
 | |
|     return this._frame?.contentWindow;
 | |
|   },
 | |
| 
 | |
|   get _window() {
 | |
|     return this._overlay?.ownerGlobal;
 | |
|   },
 | |
| 
 | |
|   updateTitle(aEvent) {
 | |
|     if (aEvent.target != this._frame.contentDocument) {
 | |
|       return;
 | |
|     }
 | |
|     this._titleElement.textContent = this._frame.contentDocument.title;
 | |
|   },
 | |
| 
 | |
|   injectXMLStylesheet(aStylesheetURL) {
 | |
|     const doc = this._frame.contentDocument;
 | |
|     if ([...doc.styleSheets].find(s => s.href === aStylesheetURL)) {
 | |
|       return;
 | |
|     }
 | |
|     let contentStylesheet = doc.createProcessingInstruction(
 | |
|       "xml-stylesheet",
 | |
|       'href="' + aStylesheetURL + '" type="text/css"'
 | |
|     );
 | |
|     doc.insertBefore(contentStylesheet, doc.documentElement);
 | |
|   },
 | |
| 
 | |
|   async open(
 | |
|     aURL,
 | |
|     { features, closingCallback, closedCallback, sizeTo } = {},
 | |
|     ...aParams
 | |
|   ) {
 | |
|     if (["available", "limitheight"].includes(sizeTo)) {
 | |
|       this._box.setAttribute("sizeto", sizeTo);
 | |
|     }
 | |
| 
 | |
|     // Create a promise so consumers can tell when we're done setting up.
 | |
|     this._dialogReady = new Promise(resolve => {
 | |
|       this._resolveDialogReady = resolve;
 | |
|     });
 | |
|     this._frame._dialogReady = this._dialogReady;
 | |
| 
 | |
|     // Assign close callbacks sync to ensure we can always callback even if the
 | |
|     // SubDialog is closed directly after opening.
 | |
|     let dialog = null;
 | |
| 
 | |
|     if (closingCallback) {
 | |
|       this._closingCallback = (...args) => {
 | |
|         closingCallback.apply(dialog, args);
 | |
|       };
 | |
|     }
 | |
|     if (closedCallback) {
 | |
|       this._closedCallback = (...args) => {
 | |
|         closedCallback.apply(dialog, args);
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     // Wait until frame is ready to prevent browser crash in tests
 | |
|     await this._frameCreated;
 | |
| 
 | |
|     if (!this._frame.contentWindow) {
 | |
|       // Given the binding constructor execution is asynchronous, and "load"
 | |
|       // event can be dispatched before the browser element is shown, the
 | |
|       // browser binding might not be constructed at this point.  Forcibly
 | |
|       // construct the frame and construct the binding.
 | |
|       // FIXME: Remove this (bug 1437247)
 | |
|       this._frame.getBoundingClientRect();
 | |
|     }
 | |
| 
 | |
|     // If we're open on some (other) URL or we're closing, open when closing has finished.
 | |
|     if (this._openedURL || this._isClosing) {
 | |
|       if (!this._isClosing) {
 | |
|         this.close();
 | |
|       }
 | |
|       let args = Array.from(arguments);
 | |
|       this._closingPromise.then(() => {
 | |
|         this.open.apply(this, args);
 | |
|       });
 | |
|       return;
 | |
|     }
 | |
|     this._addDialogEventListeners();
 | |
| 
 | |
|     // Ensure we end any pending drag sessions:
 | |
|     try {
 | |
|       // The drag service getService call fails in puppeteer tests on Linux,
 | |
|       // so this is in a try...catch as it shouldn't stop us from opening the
 | |
|       // dialog. Bug 1806870 tracks fixing this.
 | |
|       if (lazy.dragService.getCurrentSession()) {
 | |
|         lazy.dragService.endDragSession(true);
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       console.error(ex);
 | |
|     }
 | |
| 
 | |
|     // If the parent is chrome we also need open the dialog as chrome, otherwise
 | |
|     // the openDialog call will fail.
 | |
|     let dialogFeatures = `resizable,dialog=no,centerscreen,chrome=${
 | |
|       this._window?.isChromeWindow ? "yes" : "no"
 | |
|     }`;
 | |
|     if (features) {
 | |
|       dialogFeatures = `${features},${dialogFeatures}`;
 | |
|     }
 | |
| 
 | |
|     dialog = this._window.openDialog(
 | |
|       aURL,
 | |
|       `dialogFrame-${this._id}`,
 | |
|       dialogFeatures,
 | |
|       ...aParams
 | |
|     );
 | |
| 
 | |
|     this._closingEvent = null;
 | |
|     this._isClosing = false;
 | |
|     this._openedURL = aURL;
 | |
| 
 | |
|     dialogFeatures = dialogFeatures.replace(/,/g, "&");
 | |
|     let featureParams = new URLSearchParams(dialogFeatures.toLowerCase());
 | |
|     this._box.setAttribute(
 | |
|       "resizable",
 | |
|       featureParams.has("resizable") &&
 | |
|         featureParams.get("resizable") != "no" &&
 | |
|         featureParams.get("resizable") != "0"
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Close the dialog and mark it as aborted.
 | |
|    */
 | |
|   abort() {
 | |
|     this._closingEvent = new CustomEvent("dialogclosing", {
 | |
|       bubbles: true,
 | |
|       detail: { dialog: this, abort: true },
 | |
|     });
 | |
|     this._frame.contentWindow.close();
 | |
|     // It's possible that we're aborting this dialog before we've had a
 | |
|     // chance to set up the contentWindow.close function override in
 | |
|     // _onContentLoaded. If so, call this.close() directly to clean things
 | |
|     // up. That'll be a no-op if the contentWindow.close override had been
 | |
|     // set up, since this.close is idempotent.
 | |
|     this.close(this._closingEvent);
 | |
|   },
 | |
| 
 | |
|   close(aEvent = null) {
 | |
|     if (this._isClosing) {
 | |
|       return;
 | |
|     }
 | |
|     this._isClosing = true;
 | |
|     this._closingPromise = new Promise(resolve => {
 | |
|       this._resolveClosePromise = resolve;
 | |
|     });
 | |
| 
 | |
|     if (this._closingCallback) {
 | |
|       try {
 | |
|         this._closingCallback.call(null, aEvent);
 | |
|       } catch (ex) {
 | |
|         console.error(ex);
 | |
|       }
 | |
|       this._closingCallback = null;
 | |
|     }
 | |
| 
 | |
|     this._removeDialogEventListeners();
 | |
| 
 | |
|     this._overlay.style.visibility = "";
 | |
|     // Clear the sizing inline styles.
 | |
|     this._frame.removeAttribute("style");
 | |
|     // Clear the sizing attributes
 | |
|     this._box.removeAttribute("width");
 | |
|     this._box.removeAttribute("height");
 | |
|     this._box.style.removeProperty("--box-max-height-requested");
 | |
|     this._box.style.removeProperty("--box-max-width-requested");
 | |
|     this._box.style.removeProperty("min-height");
 | |
|     this._box.style.removeProperty("min-width");
 | |
|     this._overlay.style.removeProperty("--subdialog-inner-height");
 | |
| 
 | |
|     let onClosed = () => {
 | |
|       this._openedURL = null;
 | |
| 
 | |
|       this._resolveClosePromise();
 | |
| 
 | |
|       if (this._closedCallback) {
 | |
|         try {
 | |
|           this._closedCallback.call(null, aEvent);
 | |
|         } catch (ex) {
 | |
|           console.error(ex);
 | |
|         }
 | |
|         this._closedCallback = null;
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // Wait for the frame to unload before running the closed callback.
 | |
|     if (this._frame.contentWindow) {
 | |
|       this._frame.contentWindow.addEventListener("unload", onClosed, {
 | |
|         once: true,
 | |
|       });
 | |
|     } else {
 | |
|       onClosed();
 | |
|     }
 | |
| 
 | |
|     this._overlay.dispatchEvent(
 | |
|       new CustomEvent("dialogclose", {
 | |
|         bubbles: true,
 | |
|         detail: { dialog: this },
 | |
|       })
 | |
|     );
 | |
| 
 | |
|     // Defer removing the overlay so the frame content window can unload.
 | |
|     Services.tm.dispatchToMainThread(() => {
 | |
|       this._overlay.remove();
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   handleEvent(aEvent) {
 | |
|     switch (aEvent.type) {
 | |
|       case "click":
 | |
|         // Close the dialog if the user clicked the overlay background, just
 | |
|         // like when the user presses the ESC key (case "command" below).
 | |
|         if (aEvent.target !== this._overlay) {
 | |
|           break;
 | |
|         }
 | |
|         if (this._consumeOutsideClicks) {
 | |
|           this._frame.contentWindow.close();
 | |
|           break;
 | |
|         }
 | |
|         this._frame.focus();
 | |
|         break;
 | |
|       case "command":
 | |
|         this._frame.contentWindow.close();
 | |
|         break;
 | |
|       case "dialogclosing":
 | |
|         this._onDialogClosing(aEvent);
 | |
|         break;
 | |
|       case "DOMTitleChanged":
 | |
|         this.updateTitle(aEvent);
 | |
|         break;
 | |
|       case "DOMFrameContentLoaded":
 | |
|         this._onContentLoaded(aEvent);
 | |
|         break;
 | |
|       case "load":
 | |
|         this._onLoad(aEvent);
 | |
|         break;
 | |
|       case "unload":
 | |
|         this._onUnload(aEvent);
 | |
|         break;
 | |
|       case "keydown":
 | |
|         this._onKeyDown(aEvent);
 | |
|         break;
 | |
|       case "focus":
 | |
|         this._onParentWinFocus(aEvent);
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /* Private methods */
 | |
| 
 | |
|   _onUnload(aEvent) {
 | |
|     if (
 | |
|       aEvent.target !== this._frame?.contentDocument ||
 | |
|       aEvent.target.location.href !== this._openedURL
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
|     this.abort();
 | |
|   },
 | |
| 
 | |
|   _onContentLoaded(aEvent) {
 | |
|     if (
 | |
|       aEvent.target != this._frame ||
 | |
|       aEvent.target.contentWindow.location == "about:blank"
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     for (let styleSheetURL of this._injectedStyleSheets) {
 | |
|       this.injectXMLStylesheet(styleSheetURL);
 | |
|     }
 | |
| 
 | |
|     let { contentDocument } = this._frame;
 | |
|     // Provide the ability for the dialog to know that it is loaded in a frame
 | |
|     // rather than as a top-level window.
 | |
|     for (let dialog of contentDocument.querySelectorAll("dialog")) {
 | |
|       dialog.setAttribute("subdialog", "true");
 | |
|     }
 | |
|     // Sub-dialogs loaded in a chrome window should use the system font size so
 | |
|     // that the user has a way to increase or decrease it via system settings.
 | |
|     // Sub-dialogs loaded in the content area, on the other hand, can be zoomed
 | |
|     // like web content.
 | |
|     if (this._window.isChromeWindow) {
 | |
|       contentDocument.documentElement.classList.add("system-font-size");
 | |
|     }
 | |
|     // Used by CSS to give the appropriate background colour in dark mode.
 | |
|     contentDocument.documentElement.setAttribute("dialogroot", "true");
 | |
| 
 | |
|     this._frame.contentWindow.addEventListener("dialogclosing", this);
 | |
| 
 | |
|     let oldResizeBy = this._frame.contentWindow.resizeBy;
 | |
|     this._frame.contentWindow.resizeBy = (resizeByWidth, resizeByHeight) => {
 | |
|       // Only handle resizeByHeight currently.
 | |
|       let frameHeight = this._overlay.style.getPropertyValue(
 | |
|         "--subdialog-inner-height"
 | |
|       );
 | |
|       if (frameHeight) {
 | |
|         frameHeight = parseFloat(frameHeight);
 | |
|       } else {
 | |
|         frameHeight = this._frame.clientHeight;
 | |
|       }
 | |
|       let boxMinHeight = parseFloat(
 | |
|         this._window.getComputedStyle(this._box).minHeight
 | |
|       );
 | |
| 
 | |
|       this._box.style.minHeight = boxMinHeight + resizeByHeight + "px";
 | |
| 
 | |
|       this._overlay.style.setProperty(
 | |
|         "--subdialog-inner-height",
 | |
|         frameHeight + resizeByHeight + "px"
 | |
|       );
 | |
| 
 | |
|       oldResizeBy.call(
 | |
|         this._frame.contentWindow,
 | |
|         resizeByWidth,
 | |
|         resizeByHeight
 | |
|       );
 | |
|     };
 | |
| 
 | |
|     // Make window.close calls work like dialog closing.
 | |
|     let oldClose = this._frame.contentWindow.close;
 | |
|     this._frame.contentWindow.close = () => {
 | |
|       var closingEvent = this._closingEvent;
 | |
|       // If this._closingEvent is set, the dialog is closed externally
 | |
|       // (dialog.js) and "dialogclosing" has already been dispatched.
 | |
|       if (!closingEvent) {
 | |
|         // If called without closing event, we need to create and dispatch it.
 | |
|         // This is the case for any external close calls not going through
 | |
|         // dialog.js.
 | |
|         closingEvent = new CustomEvent("dialogclosing", {
 | |
|           bubbles: true,
 | |
|           detail: { button: null },
 | |
|         });
 | |
| 
 | |
|         this._frame.contentWindow.dispatchEvent(closingEvent);
 | |
|       } else if (this._closingEvent.detail?.abort) {
 | |
|         // If the dialog is aborted (SubDialog#abort) we need to dispatch the
 | |
|         // "dialogclosing" event ourselves.
 | |
|         this._frame.contentWindow.dispatchEvent(closingEvent);
 | |
|       }
 | |
| 
 | |
|       this.close(closingEvent);
 | |
|       oldClose.call(this._frame.contentWindow);
 | |
|     };
 | |
| 
 | |
|     // XXX: Hack to make focus during the dialog's load functions work. Make the element visible
 | |
|     // sooner in DOMContentLoaded but mostly invisible instead of changing visibility just before
 | |
|     // the dialog's load event.
 | |
|     // Note that this needs to inherit so that hideDialog() works as expected.
 | |
|     this._overlay.style.visibility = "inherit";
 | |
|     this._overlay.style.opacity = "0.01";
 | |
| 
 | |
|     // Ensure the document gets an a11y role of dialog.
 | |
|     const a11yDoc = contentDocument.body || contentDocument.documentElement;
 | |
|     a11yDoc.setAttribute("role", "dialog");
 | |
| 
 | |
|     Services.obs.notifyObservers(this._frame.contentWindow, "subdialog-loaded");
 | |
|   },
 | |
| 
 | |
|   async _onLoad(aEvent) {
 | |
|     let target = aEvent.currentTarget;
 | |
|     if (target.contentWindow.location == "about:blank") {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // In order to properly calculate the sizing of the subdialog, we need to
 | |
|     // ensure that all of the l10n is done.
 | |
|     if (target.contentDocument.l10n) {
 | |
|       await target.contentDocument.l10n.ready;
 | |
|     }
 | |
| 
 | |
|     // Some subdialogs may want to perform additional, asynchronous steps during initializations.
 | |
|     //
 | |
|     // In that case, we expect them to define a Promise which will delay measuring
 | |
|     // until the promise is fulfilled.
 | |
|     if (target.contentDocument.mozSubdialogReady) {
 | |
|       await target.contentDocument.mozSubdialogReady;
 | |
|     }
 | |
| 
 | |
|     await this.resizeDialog();
 | |
|     this._resolveDialogReady();
 | |
|   },
 | |
| 
 | |
|   async resizeDialog() {
 | |
|     // Do this on load to wait for the CSS to load and apply before calculating the size.
 | |
|     let docEl = this._frame.contentDocument.documentElement;
 | |
| 
 | |
|     // These are deduced from styles which we don't change, so it's safe to get them now:
 | |
|     let boxHorizontalBorder =
 | |
|       2 * parseFloat(this._window.getComputedStyle(this._box).borderLeftWidth);
 | |
|     let frameHorizontalMargin =
 | |
|       2 * parseFloat(this._window.getComputedStyle(this._frame).marginLeft);
 | |
| 
 | |
|     // Then determine and set a bunch of width stuff:
 | |
|     let { scrollWidth } = docEl.ownerDocument.body || docEl;
 | |
|     // We need to convert em to px because an em value from the dialog window could
 | |
|     // translate to something else in the host window, as font sizes may vary.
 | |
|     let frameMinWidth =
 | |
|       this._emToPx(docEl.style.minWidth) ||
 | |
|       this._emToPx(docEl.style.width) ||
 | |
|       scrollWidth + "px";
 | |
|     let frameWidth = docEl.getAttribute("width")
 | |
|       ? docEl.getAttribute("width") + "px"
 | |
|       : scrollWidth + "px";
 | |
|     if (
 | |
|       this._box.getAttribute("sizeto") == "available" &&
 | |
|       docEl.style.maxWidth
 | |
|     ) {
 | |
|       this._box.style.setProperty(
 | |
|         "--box-max-width-requested",
 | |
|         this._emToPx(docEl.style.maxWidth)
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (this._box.getAttribute("sizeto") != "available") {
 | |
|       this._frame.style.width = frameWidth;
 | |
|       this._frame.style.minWidth = frameMinWidth;
 | |
|     }
 | |
| 
 | |
|     let boxMinWidth = `calc(${boxHorizontalBorder +
 | |
|       frameHorizontalMargin}px + ${frameMinWidth})`;
 | |
| 
 | |
|     // Temporary fix to allow parent chrome to collapse properly to min width.
 | |
|     // See Bug 1658722.
 | |
|     if (this._window.isChromeWindow) {
 | |
|       boxMinWidth = `min(80vw, ${boxMinWidth})`;
 | |
|     }
 | |
|     this._box.style.minWidth = boxMinWidth;
 | |
| 
 | |
|     this.resizeVertically();
 | |
| 
 | |
|     this._overlay.dispatchEvent(
 | |
|       new CustomEvent("dialogopen", {
 | |
|         bubbles: true,
 | |
|         detail: { dialog: this },
 | |
|       })
 | |
|     );
 | |
|     this._overlay.style.visibility = "inherit";
 | |
|     this._overlay.style.opacity = ""; // XXX: focus hack continued from _onContentLoaded
 | |
| 
 | |
|     if (this._box.getAttribute("resizable") == "true") {
 | |
|       this._onResize = this._onResize.bind(this);
 | |
|       this._resizeObserver = new this._window.MutationObserver(this._onResize);
 | |
|       this._resizeObserver.observe(this._box, { attributes: true });
 | |
|     }
 | |
| 
 | |
|     this._trapFocus();
 | |
| 
 | |
|     this._resizeCallback?.({
 | |
|       title: this._titleElement,
 | |
|       frame: this._frame,
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   resizeVertically() {
 | |
|     let docEl = this._frame.contentDocument.documentElement;
 | |
|     let getDocHeight = () => {
 | |
|       let { scrollHeight } = docEl.ownerDocument.body || docEl;
 | |
|       // We need to convert em to px because an em value from the dialog window could
 | |
|       // translate to something else in the host window, as font sizes may vary.
 | |
|       return this._emToPx(docEl.style.height) || scrollHeight + "px";
 | |
|     };
 | |
| 
 | |
|     // If the title bar is disabled (not in the template),
 | |
|     // set its height to 0 for the calculation.
 | |
|     let titleBarHeight = 0;
 | |
|     if (this._titleBar) {
 | |
|       titleBarHeight =
 | |
|         this._titleBar.clientHeight +
 | |
|         parseFloat(
 | |
|           this._window.getComputedStyle(this._titleBar).borderBottomWidth
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     let boxVerticalBorder =
 | |
|       2 * parseFloat(this._window.getComputedStyle(this._box).borderTopWidth);
 | |
|     let frameVerticalMargin =
 | |
|       2 * parseFloat(this._window.getComputedStyle(this._frame).marginTop);
 | |
| 
 | |
|     // The difference between the frame and box shouldn't change, either:
 | |
|     let boxRect = this._box.getBoundingClientRect();
 | |
|     let frameRect = this._frame.getBoundingClientRect();
 | |
|     let frameSizeDifference =
 | |
|       frameRect.top - boxRect.top + (boxRect.bottom - frameRect.bottom);
 | |
| 
 | |
|     let contentPane =
 | |
|       this._frame.contentDocument.querySelector(".contentPane") ||
 | |
|       this._frame.contentDocument.querySelector("dialog");
 | |
| 
 | |
|     let sizeTo = this._box.getAttribute("sizeto");
 | |
|     if (["available", "limitheight"].includes(sizeTo)) {
 | |
|       if (sizeTo == "limitheight") {
 | |
|         this._overlay.style.setProperty("--doc-height-px", getDocHeight());
 | |
|         contentPane?.classList.add("sizeDetermined");
 | |
|       } else {
 | |
|         if (docEl.style.maxHeight) {
 | |
|           this._box.style.setProperty(
 | |
|             "--box-max-height-requested",
 | |
|             this._emToPx(docEl.style.maxHeight)
 | |
|           );
 | |
|         }
 | |
|         // Inform the CSS of the toolbar height so the bottom padding can be
 | |
|         // correctly calculated.
 | |
|         this._box.style.setProperty("--box-top-px", `${boxRect.top}px`);
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Now do the same but for the height. We need to do this afterwards because otherwise
 | |
|     // XUL assumes we'll optimize for height and gives us "wrong" values which then are no
 | |
|     // longer correct after we set the width:
 | |
|     let frameMinHeight = getDocHeight();
 | |
|     let frameHeight = docEl.getAttribute("height")
 | |
|       ? docEl.getAttribute("height") + "px"
 | |
|       : frameMinHeight;
 | |
| 
 | |
|     // Now check if the frame height we calculated is possible at this window size,
 | |
|     // accounting for titlebar, padding/border and some spacing.
 | |
|     let frameOverhead = frameSizeDifference + titleBarHeight;
 | |
|     let maxHeight = this._window.innerHeight - frameOverhead;
 | |
|     // Do this with a frame height in pixels...
 | |
|     if (!frameHeight.endsWith("px")) {
 | |
|       console.error(
 | |
|         "This dialog (",
 | |
|         this._frame.contentWindow.location.href,
 | |
|         ") set a height in non-px-non-em units ('",
 | |
|         frameHeight,
 | |
|         "'), " +
 | |
|           "which is likely to lead to bad sizing in in-content preferences. " +
 | |
|           "Please consider changing this."
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       parseFloat(frameMinHeight) > maxHeight ||
 | |
|       parseFloat(frameHeight) > maxHeight
 | |
|     ) {
 | |
|       // If the height is bigger than that of the window, we should let the
 | |
|       // contents scroll. The class is set on the "dialog" element, unless a
 | |
|       // content pane exists, which is usually the case when the "window"
 | |
|       // element is used to implement the subdialog instead.
 | |
|       frameMinHeight = maxHeight + "px";
 | |
|       // There also instances where the subdialog is neither implemented using
 | |
|       // a content pane, nor a <dialog> (such as manageAddresses.xhtml)
 | |
|       // so make sure to check that we actually got a contentPane before we
 | |
|       // use it.
 | |
|       contentPane?.classList.add("doScroll");
 | |
|     }
 | |
| 
 | |
|     this._overlay.style.setProperty("--subdialog-inner-height", frameHeight);
 | |
|     this._frame.style.height = `min(
 | |
|       calc(100vh - ${frameOverhead}px),
 | |
|       var(--subdialog-inner-height, ${frameHeight})
 | |
|     )`;
 | |
|     this._box.style.minHeight = `calc(
 | |
|       ${boxVerticalBorder + titleBarHeight + frameVerticalMargin}px +
 | |
|       ${frameMinHeight}
 | |
|     )`;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Helper for converting em to px because an em value from the dialog window could
 | |
|    * translate to something else in the host window, as font sizes may vary.
 | |
|    *
 | |
|    * @param {String} val
 | |
|    *                 A CSS length value.
 | |
|    * @return {String} The converted CSS length value, or the original value if
 | |
|    *                  no conversion took place.
 | |
|    */
 | |
|   _emToPx(val) {
 | |
|     if (val && val.endsWith("em")) {
 | |
|       let { fontSize } = this.frameContentWindow.getComputedStyle(
 | |
|         this._frame.contentDocument.documentElement
 | |
|       );
 | |
|       return parseFloat(val) * parseFloat(fontSize) + "px";
 | |
|     }
 | |
|     return val;
 | |
|   },
 | |
| 
 | |
|   _onResize(mutations) {
 | |
|     let frame = this._frame;
 | |
|     // The width and height styles are needed for the initial
 | |
|     // layout of the frame, but afterward they need to be removed
 | |
|     // or their presence will restrict the contents of the <browser>
 | |
|     // from resizing to a smaller size.
 | |
|     frame.style.removeProperty("width");
 | |
|     frame.style.removeProperty("height");
 | |
| 
 | |
|     let docEl = frame.contentDocument.documentElement;
 | |
|     let persistedAttributes = docEl.getAttribute("persist");
 | |
|     if (
 | |
|       !persistedAttributes ||
 | |
|       (!persistedAttributes.includes("width") &&
 | |
|         !persistedAttributes.includes("height"))
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     for (let mutation of mutations) {
 | |
|       if (mutation.attributeName == "width") {
 | |
|         docEl.setAttribute("width", docEl.scrollWidth);
 | |
|       } else if (mutation.attributeName == "height") {
 | |
|         docEl.setAttribute("height", docEl.scrollHeight);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onDialogClosing(aEvent) {
 | |
|     this._frame.contentWindow.removeEventListener("dialogclosing", this);
 | |
|     this._closingEvent = aEvent;
 | |
|   },
 | |
| 
 | |
|   _onKeyDown(aEvent) {
 | |
|     // Close on ESC key if target is SubDialog
 | |
|     // If we're in the parent window, we need to check if the SubDialogs
 | |
|     // frame is targeted, so we don't close the wrong dialog.
 | |
|     if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE && !aEvent.defaultPrevented) {
 | |
|       if (
 | |
|         (this._window.isChromeWindow && aEvent.currentTarget == this._box) ||
 | |
|         (!this._window.isChromeWindow && aEvent.currentTarget == this._window)
 | |
|       ) {
 | |
|         // Prevent ESC on SubDialog from cancelling page load (Bug 1665339).
 | |
|         aEvent.preventDefault();
 | |
|         this._frame.contentWindow.close();
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       this._window.isChromeWindow ||
 | |
|       aEvent.keyCode != aEvent.DOM_VK_TAB ||
 | |
|       aEvent.ctrlKey ||
 | |
|       aEvent.altKey ||
 | |
|       aEvent.metaKey
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let fm = Services.focus;
 | |
| 
 | |
|     let isLastFocusableElement = el => {
 | |
|       // XXXgijs unfortunately there is no way to get the last focusable element without asking
 | |
|       // the focus manager to move focus to it.
 | |
|       let rv =
 | |
|         el ==
 | |
|         fm.moveFocus(this._frame.contentWindow, null, fm.MOVEFOCUS_LAST, 0);
 | |
|       fm.setFocus(el, 0);
 | |
|       return rv;
 | |
|     };
 | |
| 
 | |
|     let forward = !aEvent.shiftKey;
 | |
|     // check if focus is leaving the frame (incl. the close button):
 | |
|     if (
 | |
|       (aEvent.target == this._closeButton && !forward) ||
 | |
|       (isLastFocusableElement(aEvent.originalTarget) && forward)
 | |
|     ) {
 | |
|       aEvent.preventDefault();
 | |
|       aEvent.stopImmediatePropagation();
 | |
| 
 | |
|       let parentWin = this._window.docShell.chromeEventHandler.ownerGlobal;
 | |
|       if (forward) {
 | |
|         fm.moveFocus(parentWin, null, fm.MOVEFOCUS_FIRST, fm.FLAG_BYKEY);
 | |
|       } else {
 | |
|         // Somehow, moving back 'past' the opening doc is not trivial. Cheat by doing it in 2 steps:
 | |
|         fm.moveFocus(this._window, null, fm.MOVEFOCUS_ROOT, fm.FLAG_BYKEY);
 | |
|         fm.moveFocus(parentWin, null, fm.MOVEFOCUS_BACKWARD, fm.FLAG_BYKEY);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onParentWinFocus(aEvent) {
 | |
|     // Explicitly check for the focus target of |window| to avoid triggering this when the window
 | |
|     // is refocused
 | |
|     if (
 | |
|       this._closeButton &&
 | |
|       aEvent.target != this._closeButton &&
 | |
|       aEvent.target != this._window
 | |
|     ) {
 | |
|       this._closeButton.focus();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Setup dialog event listeners.
 | |
|    * @param {Boolean} [includeLoad] - Whether to register load/unload listeners.
 | |
|    */
 | |
|   _addDialogEventListeners(includeLoad = true) {
 | |
|     if (this._window.isChromeWindow) {
 | |
|       // Only register an event listener if we have a title to show.
 | |
|       if (this._titleBar) {
 | |
|         this._frame.addEventListener("DOMTitleChanged", this, true);
 | |
|       }
 | |
| 
 | |
|       if (includeLoad) {
 | |
|         this._window.addEventListener("unload", this, true);
 | |
|       }
 | |
|     } else {
 | |
|       let chromeBrowser = this._window.docShell.chromeEventHandler;
 | |
| 
 | |
|       if (includeLoad) {
 | |
|         // For content windows we listen for unload of the browser
 | |
|         chromeBrowser.addEventListener("unload", this, true);
 | |
|       }
 | |
| 
 | |
|       if (this._titleBar) {
 | |
|         chromeBrowser.addEventListener("DOMTitleChanged", this, true);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Make the close button work.
 | |
|     this._closeButton?.addEventListener("command", this);
 | |
| 
 | |
|     if (includeLoad) {
 | |
|       // DOMFrameContentLoaded only fires on the top window
 | |
|       this._window.addEventListener("DOMFrameContentLoaded", this, true);
 | |
| 
 | |
|       // Wait for the stylesheets injected during DOMContentLoaded to load before showing the dialog
 | |
|       // otherwise there is a flicker of the stylesheet applying.
 | |
|       this._frame.addEventListener("load", this, true);
 | |
|     }
 | |
| 
 | |
|     // Ensure we get <esc> keypresses even if nothing in the subdialog is focusable
 | |
|     // (happens on OS X when only text inputs and lists are focusable, and
 | |
|     //  the subdialog only has checkboxes/radiobuttons/buttons)
 | |
|     if (!this._window.isChromeWindow) {
 | |
|       this._window.addEventListener("keydown", this, true);
 | |
|     }
 | |
| 
 | |
|     this._overlay.addEventListener("click", this, true);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove dialog event listeners.
 | |
|    * @param {Boolean} [includeLoad] - Whether to remove load/unload listeners.
 | |
|    */
 | |
|   _removeDialogEventListeners(includeLoad = true) {
 | |
|     if (this._window.isChromeWindow) {
 | |
|       this._frame.removeEventListener("DOMTitleChanged", this, true);
 | |
| 
 | |
|       if (includeLoad) {
 | |
|         this._window.removeEventListener("unload", this, true);
 | |
|       }
 | |
|     } else {
 | |
|       let chromeBrowser = this._window.docShell.chromeEventHandler;
 | |
|       if (includeLoad) {
 | |
|         chromeBrowser.removeEventListener("unload", this, true);
 | |
|       }
 | |
| 
 | |
|       chromeBrowser.removeEventListener("DOMTitleChanged", this, true);
 | |
|     }
 | |
| 
 | |
|     this._closeButton?.removeEventListener("command", this);
 | |
| 
 | |
|     if (includeLoad) {
 | |
|       this._window.removeEventListener("DOMFrameContentLoaded", this, true);
 | |
|       this._frame.removeEventListener("load", this, true);
 | |
|       this._frame.contentWindow.removeEventListener("dialogclosing", this);
 | |
|     }
 | |
| 
 | |
|     this._window.removeEventListener("keydown", this, true);
 | |
| 
 | |
|     this._overlay.removeEventListener("click", this, true);
 | |
| 
 | |
|     if (this._resizeObserver) {
 | |
|       this._resizeObserver.disconnect();
 | |
|       this._resizeObserver = null;
 | |
|     }
 | |
| 
 | |
|     this._untrapFocus();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Focus the dialog content.
 | |
|    * If the embedded document defines a custom focus handler it will be called.
 | |
|    * Otherwise we will focus the first focusable element in the content window.
 | |
|    * @param {boolean} [isInitialFocus] - Whether the dialog is focused for the
 | |
|    * first time after opening.
 | |
|    */
 | |
|   focus(isInitialFocus = false) {
 | |
|     // If the content window has its own focus logic, hand off the focus call.
 | |
|     let focusHandler = this._frame?.contentDocument?.subDialogSetDefaultFocus;
 | |
|     if (focusHandler) {
 | |
|       focusHandler(isInitialFocus);
 | |
|       return;
 | |
|     }
 | |
|     // Handle focus ourselves. Try to move the focus to the first element in
 | |
|     // the content window.
 | |
|     let fm = Services.focus;
 | |
| 
 | |
|     // We're intentionally hiding the focus ring here for now per bug 1704882,
 | |
|     // but we aim to have a better fix that retains the focus ring for users
 | |
|     // that had brought up the dialog by keyboard in bug 1708261.
 | |
|     let focusedElement = fm.moveFocus(
 | |
|       this._frame.contentWindow,
 | |
|       null,
 | |
|       fm.MOVEFOCUS_FIRST,
 | |
|       fm.FLAG_NOSHOWRING
 | |
|     );
 | |
|     if (!focusedElement) {
 | |
|       // Ensure the focus is pulled out of the content document even if there's
 | |
|       // nothing focusable in the dialog.
 | |
|       this._frame.focus();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _trapFocus() {
 | |
|     // Attach a system event listener so the dialog can cancel keydown events.
 | |
|     // See Bug 1669990.
 | |
|     this._box.addEventListener("keydown", this, { mozSystemGroup: true });
 | |
|     this._closeButton?.addEventListener("keydown", this);
 | |
| 
 | |
|     if (!this._window.isChromeWindow) {
 | |
|       this._window.addEventListener("focus", this, true);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _untrapFocus() {
 | |
|     this._box.removeEventListener("keydown", this, { mozSystemGroup: true });
 | |
|     this._closeButton?.removeEventListener("keydown", this);
 | |
|     this._window.removeEventListener("focus", this, true);
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Manages multiple SubDialogs in a dialog stack element.
 | |
|  */
 | |
| export class SubDialogManager {
 | |
|   /**
 | |
|    * @param {Object} options - Dialog manager options.
 | |
|    * @param {DOMNode} options.dialogStack - Container element for all dialogs
 | |
|    * this instance manages.
 | |
|    * @param {DOMNode} options.dialogTemplate - Element to use as template for
 | |
|    * constructing new dialogs.
 | |
|    * @param {Number} [options.orderType] - Whether dialogs should be ordered as
 | |
|    * a stack or a queue.
 | |
|    * @param {Boolean} [options.allowDuplicateDialogs] - Whether to allow opening
 | |
|    * duplicate dialogs (same URI) at the same time. If disabled, opening a
 | |
|    * dialog with the same URI as an existing dialog will be a no-op.
 | |
|    * @param {Object} options.dialogOptions - Options passed to every
 | |
|    * SubDialog instance.
 | |
|    * @see {@link SubDialog} for a list of dialog options.
 | |
|    */
 | |
|   constructor({
 | |
|     dialogStack,
 | |
|     dialogTemplate,
 | |
|     orderType = SubDialogManager.ORDER_STACK,
 | |
|     allowDuplicateDialogs = false,
 | |
|     dialogOptions,
 | |
|   }) {
 | |
|     /**
 | |
|      * New dialogs are pushed to the end of the _dialogs array.
 | |
|      * Depending on the orderType either the last element (stack) or the first
 | |
|      * element (queue) in the array will be the top and visible.
 | |
|      * @type {SubDialog[]}
 | |
|      */
 | |
|     this._dialogs = [];
 | |
|     this._dialogStack = dialogStack;
 | |
|     this._dialogTemplate = dialogTemplate;
 | |
|     this._topLevelPrevActiveElement = null;
 | |
|     this._orderType = orderType;
 | |
|     this._allowDuplicateDialogs = allowDuplicateDialogs;
 | |
|     this._dialogOptions = dialogOptions;
 | |
| 
 | |
|     this._preloadDialog = new SubDialog({
 | |
|       template: this._dialogTemplate,
 | |
|       parentElement: this._dialogStack,
 | |
|       id: SubDialogManager._nextDialogID++,
 | |
|       dialogOptions: this._dialogOptions,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the dialog which is currently on top. This depends on whether the
 | |
|    * dialogs are in a stack or a queue.
 | |
|    */
 | |
|   get _topDialog() {
 | |
|     if (!this._dialogs.length) {
 | |
|       return undefined;
 | |
|     }
 | |
|     if (this._orderType === SubDialogManager.ORDER_STACK) {
 | |
|       return this._dialogs[this._dialogs.length - 1];
 | |
|     }
 | |
|     return this._dialogs[0];
 | |
|   }
 | |
| 
 | |
|   open(
 | |
|     aURL,
 | |
|     {
 | |
|       features,
 | |
|       closingCallback,
 | |
|       closedCallback,
 | |
|       allowDuplicateDialogs,
 | |
|       sizeTo,
 | |
|       hideContent,
 | |
|     } = {},
 | |
|     ...aParams
 | |
|   ) {
 | |
|     let allowDuplicates =
 | |
|       allowDuplicateDialogs != null
 | |
|         ? allowDuplicateDialogs
 | |
|         : this._allowDuplicateDialogs;
 | |
|     // If we're already open/opening on this URL, do nothing.
 | |
|     if (
 | |
|       !allowDuplicates &&
 | |
|       this._dialogs.some(dialog => dialog._openedURL == aURL)
 | |
|     ) {
 | |
|       return undefined;
 | |
|     }
 | |
| 
 | |
|     let doc = this._dialogStack.ownerDocument;
 | |
| 
 | |
|     // For dialog stacks, remember the last active element before opening the
 | |
|     // next dialog. This allows us to restore focus on dialog close.
 | |
|     if (
 | |
|       this._orderType === SubDialogManager.ORDER_STACK &&
 | |
|       this._dialogs.length
 | |
|     ) {
 | |
|       this._topDialog._prevActiveElement = doc.activeElement;
 | |
|     }
 | |
| 
 | |
|     if (!this._dialogs.length) {
 | |
|       // When opening the first dialog, show the dialog stack.
 | |
|       this._dialogStack.hidden = false;
 | |
|       this._dialogStack.classList.remove("temporarilyHidden");
 | |
|       this._topLevelPrevActiveElement = doc.activeElement;
 | |
|     }
 | |
| 
 | |
|     // Consumers may pass this flag to make the dialog overlay background opaque,
 | |
|     // effectively hiding the content behind it. For example,
 | |
|     // this is used by the prompt code to prevent certain http authentication spoofing scenarios.
 | |
|     if (hideContent) {
 | |
|       this._preloadDialog._overlay.setAttribute("hideContent", true);
 | |
|     }
 | |
|     this._dialogs.push(this._preloadDialog);
 | |
|     this._preloadDialog.open(
 | |
|       aURL,
 | |
|       {
 | |
|         features,
 | |
|         closingCallback,
 | |
|         closedCallback,
 | |
|         sizeTo,
 | |
|       },
 | |
|       ...aParams
 | |
|     );
 | |
| 
 | |
|     let openedDialog = this._preloadDialog;
 | |
| 
 | |
|     this._preloadDialog = new SubDialog({
 | |
|       template: this._dialogTemplate,
 | |
|       parentElement: this._dialogStack,
 | |
|       id: SubDialogManager._nextDialogID++,
 | |
|       dialogOptions: this._dialogOptions,
 | |
|     });
 | |
| 
 | |
|     if (this._dialogs.length == 1) {
 | |
|       this._ensureStackEventListeners();
 | |
|     }
 | |
| 
 | |
|     return openedDialog;
 | |
|   }
 | |
| 
 | |
|   close() {
 | |
|     this._topDialog.close();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hides the dialog stack for a specific browser, without actually destroying
 | |
|    * frames for stuff within it.
 | |
|    *
 | |
|    * @param aBrowser - The browser associated with the tab dialog.
 | |
|    */
 | |
|   hideDialog(aBrowser) {
 | |
|     aBrowser.removeAttribute("tabDialogShowing");
 | |
|     this._dialogStack.classList.add("temporarilyHidden");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Abort open dialogs.
 | |
|    * @param {function} [filterFn] - Function which should return true for
 | |
|    * dialogs that should be aborted and false for dialogs that should remain
 | |
|    * open. Defaults to aborting all dialogs.
 | |
|    */
 | |
|   abortDialogs(filterFn = () => true) {
 | |
|     this._dialogs.filter(filterFn).forEach(dialog => dialog.abort());
 | |
|   }
 | |
| 
 | |
|   get hasDialogs() {
 | |
|     if (!this._dialogs.length) {
 | |
|       return false;
 | |
|     }
 | |
|     return this._dialogs.some(dialog => !dialog._isClosing);
 | |
|   }
 | |
| 
 | |
|   get dialogs() {
 | |
|     return [...this._dialogs];
 | |
|   }
 | |
| 
 | |
|   focusTopDialog() {
 | |
|     this._topDialog?.focus();
 | |
|   }
 | |
| 
 | |
|   handleEvent(aEvent) {
 | |
|     switch (aEvent.type) {
 | |
|       case "dialogopen": {
 | |
|         this._onDialogOpen(aEvent.detail.dialog);
 | |
|         break;
 | |
|       }
 | |
|       case "dialogclose": {
 | |
|         this._onDialogClose(aEvent.detail.dialog);
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _onDialogOpen(dialog) {
 | |
|     let lowerDialogs = [];
 | |
|     if (dialog == this._topDialog) {
 | |
|       dialog.focus(true);
 | |
|     } else {
 | |
|       // Opening dialog is not on top, hide it
 | |
|       lowerDialogs.push(dialog);
 | |
|     }
 | |
| 
 | |
|     // For stack order, hide the previous top
 | |
|     if (
 | |
|       this._dialogs.length &&
 | |
|       this._orderType === SubDialogManager.ORDER_STACK
 | |
|     ) {
 | |
|       let index = this._dialogs.indexOf(dialog);
 | |
|       if (index > 0) {
 | |
|         lowerDialogs.push(this._dialogs[index - 1]);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     lowerDialogs.forEach(d => {
 | |
|       if (d._overlay.hasAttribute("topmost")) {
 | |
|         d._overlay.removeAttribute("topmost");
 | |
|         d._removeDialogEventListeners(false);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _onDialogClose(dialog) {
 | |
|     this._dialogs.splice(this._dialogs.indexOf(dialog), 1);
 | |
| 
 | |
|     if (this._topDialog) {
 | |
|       // The prevActiveElement is only set for stacked dialogs
 | |
|       if (this._topDialog._prevActiveElement) {
 | |
|         this._topDialog._prevActiveElement.focus();
 | |
|       } else {
 | |
|         this._topDialog.focus(true);
 | |
|       }
 | |
|       this._topDialog._overlay.setAttribute("topmost", true);
 | |
|       this._topDialog._addDialogEventListeners(false);
 | |
|       this._dialogStack.hidden = false;
 | |
|       this._dialogStack.classList.remove("temporarilyHidden");
 | |
|     } else {
 | |
|       // We have closed the last dialog, do cleanup.
 | |
|       this._topLevelPrevActiveElement.focus();
 | |
|       this._dialogStack.hidden = true;
 | |
|       this._removeStackEventListeners();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _ensureStackEventListeners() {
 | |
|     this._dialogStack.addEventListener("dialogopen", this);
 | |
|     this._dialogStack.addEventListener("dialogclose", this);
 | |
|   }
 | |
| 
 | |
|   _removeStackEventListeners() {
 | |
|     this._dialogStack.removeEventListener("dialogopen", this);
 | |
|     this._dialogStack.removeEventListener("dialogclose", this);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Used for the SubDialogManager orderType option.
 | |
| SubDialogManager.ORDER_STACK = 0;
 | |
| SubDialogManager.ORDER_QUEUE = 1;
 | |
| 
 | |
| SubDialogManager._nextDialogID = 0;
 | 
