mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			725 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			725 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | 
						|
/* vim: set sts=2 sw=2 et tw=80: */
 | 
						|
/* 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/. */
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
 | 
						|
  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
 | 
						|
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
 | 
						|
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | 
						|
 | 
						|
var { DefaultWeakMap, promiseEvent } = ExtensionUtils;
 | 
						|
 | 
						|
const { makeWidgetId } = ExtensionCommon;
 | 
						|
 | 
						|
const POPUP_LOAD_TIMEOUT_MS = 200;
 | 
						|
 | 
						|
function promisePopupShown(popup) {
 | 
						|
  return new Promise(resolve => {
 | 
						|
    if (popup.state == "open") {
 | 
						|
      resolve();
 | 
						|
    } else {
 | 
						|
      popup.addEventListener(
 | 
						|
        "popupshown",
 | 
						|
        function () {
 | 
						|
          resolve();
 | 
						|
        },
 | 
						|
        { once: true }
 | 
						|
      );
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
const REMOTE_PANEL_ID = "webextension-remote-preload-panel";
 | 
						|
 | 
						|
export class BasePopup {
 | 
						|
  constructor(
 | 
						|
    extension,
 | 
						|
    viewNode,
 | 
						|
    popupURL,
 | 
						|
    browserStyle,
 | 
						|
    fixedWidth = false,
 | 
						|
    blockParser = false
 | 
						|
  ) {
 | 
						|
    this.extension = extension;
 | 
						|
    this.popupURL = popupURL;
 | 
						|
    this.viewNode = viewNode;
 | 
						|
    this.browserStyle = browserStyle;
 | 
						|
    this.window = viewNode.ownerGlobal;
 | 
						|
    this.destroyed = false;
 | 
						|
    this.fixedWidth = fixedWidth;
 | 
						|
    this.blockParser = blockParser;
 | 
						|
 | 
						|
    extension.callOnClose(this);
 | 
						|
 | 
						|
    this.contentReady = new Promise(resolve => {
 | 
						|
      this._resolveContentReady = resolve;
 | 
						|
    });
 | 
						|
 | 
						|
    this.window.addEventListener("unload", this);
 | 
						|
    this.viewNode.addEventListener(this.DESTROY_EVENT, this);
 | 
						|
    this.panel.addEventListener("popuppositioned", this, {
 | 
						|
      once: true,
 | 
						|
      capture: true,
 | 
						|
    });
 | 
						|
 | 
						|
    this.browser = null;
 | 
						|
    this.browserLoaded = new Promise((resolve, reject) => {
 | 
						|
      this.browserLoadedDeferred = { resolve, reject };
 | 
						|
    });
 | 
						|
    this.browserReady = this.createBrowser(viewNode, popupURL);
 | 
						|
 | 
						|
    BasePopup.instances.get(this.window).set(extension, this);
 | 
						|
  }
 | 
						|
 | 
						|
  static for(extension, window) {
 | 
						|
    return BasePopup.instances.get(window).get(extension);
 | 
						|
  }
 | 
						|
 | 
						|
  close() {
 | 
						|
    this.closePopup();
 | 
						|
  }
 | 
						|
 | 
						|
  destroy() {
 | 
						|
    this.extension.forgetOnClose(this);
 | 
						|
 | 
						|
    this.window.removeEventListener("unload", this);
 | 
						|
 | 
						|
    this.destroyed = true;
 | 
						|
    this.browserLoadedDeferred.reject(new Error("Popup destroyed"));
 | 
						|
    // Ignore unhandled rejections if the "attach" method is not called.
 | 
						|
    this.browserLoaded.catch(() => {});
 | 
						|
 | 
						|
    BasePopup.instances.get(this.window).delete(this.extension);
 | 
						|
 | 
						|
    return this.browserReady.then(() => {
 | 
						|
      if (this.browser) {
 | 
						|
        this.destroyBrowser(this.browser, true);
 | 
						|
        this.browser.parentNode.remove();
 | 
						|
      }
 | 
						|
      if (this.stack) {
 | 
						|
        this.stack.remove();
 | 
						|
      }
 | 
						|
 | 
						|
      if (this.viewNode) {
 | 
						|
        this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
 | 
						|
        delete this.viewNode.customRectGetter;
 | 
						|
      }
 | 
						|
 | 
						|
      let { panel } = this;
 | 
						|
      if (panel) {
 | 
						|
        panel.removeEventListener("popuppositioned", this, { capture: true });
 | 
						|
      }
 | 
						|
      if (panel && panel.id !== REMOTE_PANEL_ID) {
 | 
						|
        panel.style.removeProperty("--arrowpanel-background");
 | 
						|
        panel.style.removeProperty("--arrowpanel-border-color");
 | 
						|
        panel.removeAttribute("remote");
 | 
						|
      }
 | 
						|
 | 
						|
      this.browser = null;
 | 
						|
      this.stack = null;
 | 
						|
      this.viewNode = null;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  destroyBrowser(browser, finalize = false) {
 | 
						|
    let mm = browser.messageManager;
 | 
						|
    // If the browser has already been removed from the document, because the
 | 
						|
    // popup was closed externally, there will be no message manager here, so
 | 
						|
    // just replace our receiveMessage method with a stub.
 | 
						|
    if (mm) {
 | 
						|
      mm.removeMessageListener("Extension:BrowserBackgroundChanged", this);
 | 
						|
      mm.removeMessageListener("Extension:BrowserContentLoaded", this);
 | 
						|
      mm.removeMessageListener("Extension:BrowserResized", this);
 | 
						|
    } else if (finalize) {
 | 
						|
      this.receiveMessage = () => {};
 | 
						|
    }
 | 
						|
    browser.removeEventListener("pagetitlechanged", this);
 | 
						|
    browser.removeEventListener("DOMWindowClose", this);
 | 
						|
    browser.removeEventListener("DoZoomEnlargeBy10", this);
 | 
						|
    browser.removeEventListener("DoZoomReduceBy10", this);
 | 
						|
  }
 | 
						|
 | 
						|
  // Returns the name of the event fired on `viewNode` when the popup is being
 | 
						|
  // destroyed. This must be implemented by every subclass.
 | 
						|
  get DESTROY_EVENT() {
 | 
						|
    throw new Error("Not implemented");
 | 
						|
  }
 | 
						|
 | 
						|
  get STYLESHEETS() {
 | 
						|
    let sheets = [];
 | 
						|
 | 
						|
    if (this.browserStyle) {
 | 
						|
      sheets.push("chrome://browser/content/extension.css");
 | 
						|
    }
 | 
						|
    if (!this.fixedWidth) {
 | 
						|
      sheets.push("chrome://browser/content/extension-popup-panel.css");
 | 
						|
    }
 | 
						|
 | 
						|
    return sheets;
 | 
						|
  }
 | 
						|
 | 
						|
  get panel() {
 | 
						|
    let panel = this.viewNode;
 | 
						|
    while (panel && panel.localName != "panel") {
 | 
						|
      panel = panel.parentNode;
 | 
						|
    }
 | 
						|
    return panel;
 | 
						|
  }
 | 
						|
 | 
						|
  receiveMessage({ name, data }) {
 | 
						|
    switch (name) {
 | 
						|
      case "Extension:BrowserBackgroundChanged":
 | 
						|
        this.setBackground(data.background);
 | 
						|
        break;
 | 
						|
 | 
						|
      case "Extension:BrowserContentLoaded":
 | 
						|
        this.browserLoadedDeferred.resolve();
 | 
						|
        break;
 | 
						|
 | 
						|
      case "Extension:BrowserResized":
 | 
						|
        this._resolveContentReady();
 | 
						|
        if (this.ignoreResizes) {
 | 
						|
          this.dimensions = data;
 | 
						|
        } else {
 | 
						|
          this.resizeBrowser(data);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  handleEvent(event) {
 | 
						|
    switch (event.type) {
 | 
						|
      case "unload":
 | 
						|
      case this.DESTROY_EVENT:
 | 
						|
        if (!this.destroyed) {
 | 
						|
          this.destroy();
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case "popuppositioned":
 | 
						|
        if (!this.destroyed) {
 | 
						|
          this.browserLoaded
 | 
						|
            .then(() => {
 | 
						|
              if (this.destroyed) {
 | 
						|
                return;
 | 
						|
              }
 | 
						|
              // Wait the reflow before asking the popup panel to grab the focus, otherwise
 | 
						|
              // `nsFocusManager::SetFocus` may ignore out request because the panel view
 | 
						|
              // visibility is still set to `ViewVisibility::Hide` (waiting the document
 | 
						|
              // to be fully flushed makes us sure that when the popup panel grabs the focus
 | 
						|
              // nsMenuPopupFrame::LayoutPopup has already been colled and set the frame
 | 
						|
              // visibility to `ViewVisibility::Show`).
 | 
						|
              this.browser.ownerGlobal.promiseDocumentFlushed(() => {
 | 
						|
                if (this.destroyed) {
 | 
						|
                  return;
 | 
						|
                }
 | 
						|
                this.browser.messageManager.sendAsyncMessage(
 | 
						|
                  "Extension:GrabFocus",
 | 
						|
                  {}
 | 
						|
                );
 | 
						|
              });
 | 
						|
            })
 | 
						|
            .catch(() => {
 | 
						|
              // If the panel closes too fast an exception is raised here and tests will fail.
 | 
						|
            });
 | 
						|
        }
 | 
						|
        break;
 | 
						|
 | 
						|
      case "pagetitlechanged":
 | 
						|
        this.viewNode.setAttribute("aria-label", this.browser.contentTitle);
 | 
						|
        break;
 | 
						|
 | 
						|
      case "DOMWindowClose":
 | 
						|
        this.closePopup();
 | 
						|
        break;
 | 
						|
 | 
						|
      case "DoZoomEnlargeBy10": {
 | 
						|
        const browser = event.target;
 | 
						|
        let { ZoomManager } = browser.ownerGlobal;
 | 
						|
        let zoom = this.browser.fullZoom;
 | 
						|
        zoom += 0.1;
 | 
						|
        if (zoom > ZoomManager.MAX) {
 | 
						|
          zoom = ZoomManager.MAX;
 | 
						|
        }
 | 
						|
        browser.fullZoom = zoom;
 | 
						|
        break;
 | 
						|
      }
 | 
						|
 | 
						|
      case "DoZoomReduceBy10": {
 | 
						|
        const browser = event.target;
 | 
						|
        let { ZoomManager } = browser.ownerGlobal;
 | 
						|
        let zoom = browser.fullZoom;
 | 
						|
        zoom -= 0.1;
 | 
						|
        if (zoom < ZoomManager.MIN) {
 | 
						|
          zoom = ZoomManager.MIN;
 | 
						|
        }
 | 
						|
        browser.fullZoom = zoom;
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  createBrowser(viewNode, popupURL = null) {
 | 
						|
    let document = viewNode.ownerDocument;
 | 
						|
 | 
						|
    let stack = document.createXULElement("stack");
 | 
						|
    stack.setAttribute("class", "webextension-popup-stack");
 | 
						|
 | 
						|
    let browser = document.createXULElement("browser");
 | 
						|
    browser.setAttribute("type", "content");
 | 
						|
    browser.setAttribute("disableglobalhistory", "true");
 | 
						|
    browser.setAttribute("messagemanagergroup", "webext-browsers");
 | 
						|
    browser.setAttribute("class", "webextension-popup-browser");
 | 
						|
    browser.setAttribute("webextension-view-type", "popup");
 | 
						|
    browser.setAttribute("tooltip", "aHTMLTooltip");
 | 
						|
    browser.setAttribute("contextmenu", "contentAreaContextMenu");
 | 
						|
    browser.setAttribute("autocompletepopup", "PopupAutoComplete");
 | 
						|
    browser.setAttribute("constrainpopups", "false");
 | 
						|
 | 
						|
    // Ensure the browser will initially load in the same group as other
 | 
						|
    // browsers from the same extension.
 | 
						|
    browser.setAttribute(
 | 
						|
      "initialBrowsingContextGroupId",
 | 
						|
      this.extension.policy.browsingContextGroupId
 | 
						|
    );
 | 
						|
 | 
						|
    if (this.extension.remote) {
 | 
						|
      browser.setAttribute("remote", "true");
 | 
						|
      browser.setAttribute("remoteType", this.extension.remoteType);
 | 
						|
      browser.setAttribute("maychangeremoteness", "true");
 | 
						|
    }
 | 
						|
 | 
						|
    // We only need flex sizing for the sake of the slide-in sub-views of the
 | 
						|
    // main menu panel, so that the browser occupies the full width of the view,
 | 
						|
    // and also takes up any extra height that's available to it.
 | 
						|
    browser.setAttribute("flex", "1");
 | 
						|
    stack.setAttribute("flex", "1");
 | 
						|
 | 
						|
    // Note: When using noautohide panels, the popup manager will add width and
 | 
						|
    // height attributes to the panel, breaking our resize code, if the browser
 | 
						|
    // starts out smaller than 30px by 10px. This isn't an issue now, but it
 | 
						|
    // will be if and when we popup debugging.
 | 
						|
 | 
						|
    this.browser = browser;
 | 
						|
    this.stack = stack;
 | 
						|
 | 
						|
    let readyPromise;
 | 
						|
    if (this.extension.remote) {
 | 
						|
      readyPromise = promiseEvent(browser, "XULFrameLoaderCreated");
 | 
						|
    } else {
 | 
						|
      readyPromise = promiseEvent(browser, "load");
 | 
						|
    }
 | 
						|
 | 
						|
    stack.appendChild(browser);
 | 
						|
    viewNode.appendChild(stack);
 | 
						|
 | 
						|
    if (!this.extension.remote) {
 | 
						|
      // FIXME: bug 1494029 - this code used to rely on the browser binding
 | 
						|
      // accessing browser.contentWindow. This is a stopgap to continue doing
 | 
						|
      // that, but we should get rid of it in the long term.
 | 
						|
      browser.contentWindow; // eslint-disable-line no-unused-expressions
 | 
						|
    }
 | 
						|
 | 
						|
    let setupBrowser = browser => {
 | 
						|
      let mm = browser.messageManager;
 | 
						|
      mm.addMessageListener("Extension:BrowserBackgroundChanged", this);
 | 
						|
      mm.addMessageListener("Extension:BrowserContentLoaded", this);
 | 
						|
      mm.addMessageListener("Extension:BrowserResized", this);
 | 
						|
      browser.addEventListener("pagetitlechanged", this);
 | 
						|
      browser.addEventListener("DOMWindowClose", this);
 | 
						|
      browser.addEventListener("DoZoomEnlargeBy10", this, true); // eslint-disable-line mozilla/balanced-listeners
 | 
						|
      browser.addEventListener("DoZoomReduceBy10", this, true); // eslint-disable-line mozilla/balanced-listeners
 | 
						|
 | 
						|
      lazy.ExtensionParent.apiManager.emit(
 | 
						|
        "extension-browser-inserted",
 | 
						|
        browser
 | 
						|
      );
 | 
						|
      return browser;
 | 
						|
    };
 | 
						|
 | 
						|
    const initBrowser = () => {
 | 
						|
      setupBrowser(browser);
 | 
						|
      let mm = browser.messageManager;
 | 
						|
 | 
						|
      mm.loadFrameScript(
 | 
						|
        "chrome://extensions/content/ext-browser-content.js",
 | 
						|
        false,
 | 
						|
        true
 | 
						|
      );
 | 
						|
 | 
						|
      mm.sendAsyncMessage("Extension:InitBrowser", {
 | 
						|
        allowScriptsToClose: true,
 | 
						|
        blockParser: this.blockParser,
 | 
						|
        fixedWidth: this.fixedWidth,
 | 
						|
        maxWidth: 800,
 | 
						|
        maxHeight: 600,
 | 
						|
        stylesheets: this.STYLESHEETS,
 | 
						|
      });
 | 
						|
    };
 | 
						|
 | 
						|
    browser.addEventListener("DidChangeBrowserRemoteness", initBrowser); // eslint-disable-line mozilla/balanced-listeners
 | 
						|
 | 
						|
    if (!popupURL) {
 | 
						|
      // For remote browsers, we can't do any setup until the frame loader is
 | 
						|
      // created. Non-remote browsers get a message manager immediately, so
 | 
						|
      // there's no need to wait for the load event.
 | 
						|
      if (this.extension.remote) {
 | 
						|
        return readyPromise.then(() => setupBrowser(browser));
 | 
						|
      }
 | 
						|
      return setupBrowser(browser);
 | 
						|
    }
 | 
						|
 | 
						|
    return readyPromise.then(() => {
 | 
						|
      initBrowser();
 | 
						|
      browser.fixupAndLoadURIString(popupURL, {
 | 
						|
        triggeringPrincipal: this.extension.principal,
 | 
						|
      });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  unblockParser() {
 | 
						|
    this.browserReady.then(() => {
 | 
						|
      if (this.destroyed) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // Only block the parser for the preloaded browser, initBrowser will be
 | 
						|
      // called again when the browserAction popup is navigated and we should
 | 
						|
      // not block the parser in that case, otherwise the navigating the popup
 | 
						|
      // to another extension page will never complete and the popup will
 | 
						|
      // stay stuck on the previous extension page. See Bug 1747813.
 | 
						|
      this.blockParser = false;
 | 
						|
      this.browser.messageManager.sendAsyncMessage("Extension:UnblockParser");
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  resizeBrowser({ width, height, detail }) {
 | 
						|
    if (this.fixedWidth) {
 | 
						|
      // Figure out how much extra space we have on the side of the panel
 | 
						|
      // opposite the arrow.
 | 
						|
      let side = this.panel.getAttribute("side") == "top" ? "bottom" : "top";
 | 
						|
      let maxHeight = this.viewHeight + this.extraHeight[side];
 | 
						|
 | 
						|
      height = Math.min(height, maxHeight);
 | 
						|
      this.browser.style.height = `${height}px`;
 | 
						|
 | 
						|
      // Used by the panelmultiview code to figure out sizing without reparenting
 | 
						|
      // (which would destroy the browser and break us).
 | 
						|
      this.lastCalculatedInViewHeight = Math.max(height, this.viewHeight);
 | 
						|
    } else {
 | 
						|
      this.browser.style.width = `${width}px`;
 | 
						|
      this.browser.style.minWidth = `${width}px`;
 | 
						|
      this.browser.style.height = `${height}px`;
 | 
						|
      this.browser.style.minHeight = `${height}px`;
 | 
						|
    }
 | 
						|
 | 
						|
    let event = new this.window.CustomEvent("WebExtPopupResized", { detail });
 | 
						|
    this.browser.dispatchEvent(event);
 | 
						|
  }
 | 
						|
 | 
						|
  setBackground(background) {
 | 
						|
    // Panels inherit the applied theme (light, dark, etc) and there is a high
 | 
						|
    // likelihood that most extension authors will not have tested with a dark theme.
 | 
						|
    // If they have not set a background-color, we force it to white to ensure visibility
 | 
						|
    // of the extension content. Passing `null` should be treated the same as no argument,
 | 
						|
    // which is why we can't use default parameters here.
 | 
						|
    if (!background) {
 | 
						|
      background = "#fff";
 | 
						|
    }
 | 
						|
    if (this.panel.id != "widget-overflow") {
 | 
						|
      this.panel.style.setProperty("--arrowpanel-background", background);
 | 
						|
    }
 | 
						|
    if (background == "#fff") {
 | 
						|
      // Set a usable default color that work with the default background-color.
 | 
						|
      this.panel.style.setProperty(
 | 
						|
        "--arrowpanel-border-color",
 | 
						|
        "hsla(210,4%,10%,.15)"
 | 
						|
      );
 | 
						|
    }
 | 
						|
    this.background = background;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * A map of active popups for a given browser window.
 | 
						|
 *
 | 
						|
 * WeakMap[window -> WeakMap[Extension -> BasePopup]]
 | 
						|
 */
 | 
						|
BasePopup.instances = new DefaultWeakMap(() => new WeakMap());
 | 
						|
 | 
						|
export class PanelPopup extends BasePopup {
 | 
						|
  constructor(extension, document, popupURL, browserStyle) {
 | 
						|
    let panel = document.createXULElement("panel");
 | 
						|
    panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
 | 
						|
    panel.setAttribute("class", "browser-extension-panel panel-no-padding");
 | 
						|
    panel.setAttribute("tabspecific", "true");
 | 
						|
    panel.setAttribute("type", "arrow");
 | 
						|
    panel.setAttribute("role", "group");
 | 
						|
    if (extension.remote) {
 | 
						|
      panel.setAttribute("remote", "true");
 | 
						|
    }
 | 
						|
    panel.setAttribute("neverhidden", "true");
 | 
						|
 | 
						|
    document.getElementById("mainPopupSet").appendChild(panel);
 | 
						|
 | 
						|
    panel.addEventListener(
 | 
						|
      "popupshowing",
 | 
						|
      () => {
 | 
						|
        let event = new this.window.CustomEvent("WebExtPopupLoaded", {
 | 
						|
          bubbles: true,
 | 
						|
          detail: { extension },
 | 
						|
        });
 | 
						|
        this.browser.dispatchEvent(event);
 | 
						|
      },
 | 
						|
      { once: true }
 | 
						|
    );
 | 
						|
 | 
						|
    super(extension, panel, popupURL, browserStyle);
 | 
						|
  }
 | 
						|
 | 
						|
  get DESTROY_EVENT() {
 | 
						|
    return "popuphidden";
 | 
						|
  }
 | 
						|
 | 
						|
  destroy() {
 | 
						|
    super.destroy();
 | 
						|
    this.viewNode.remove();
 | 
						|
    this.viewNode = null;
 | 
						|
  }
 | 
						|
 | 
						|
  closePopup() {
 | 
						|
    promisePopupShown(this.viewNode).then(() => {
 | 
						|
      // Make sure we're not already destroyed, or removed from the DOM.
 | 
						|
      if (this.viewNode && this.viewNode.hidePopup) {
 | 
						|
        this.viewNode.hidePopup();
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class ViewPopup extends BasePopup {
 | 
						|
  constructor(
 | 
						|
    extension,
 | 
						|
    window,
 | 
						|
    popupURL,
 | 
						|
    browserStyle,
 | 
						|
    fixedWidth,
 | 
						|
    blockParser
 | 
						|
  ) {
 | 
						|
    let document = window.document;
 | 
						|
 | 
						|
    let createPanel = remote => {
 | 
						|
      let panel = document.createXULElement("panel");
 | 
						|
      panel.setAttribute("type", "arrow");
 | 
						|
      if (remote) {
 | 
						|
        panel.setAttribute("remote", "true");
 | 
						|
      }
 | 
						|
      panel.setAttribute("neverhidden", "true");
 | 
						|
 | 
						|
      document.getElementById("mainPopupSet").appendChild(panel);
 | 
						|
      return panel;
 | 
						|
    };
 | 
						|
 | 
						|
    // Create a temporary panel to hold the browser while it pre-loads its
 | 
						|
    // content. This panel will never be shown, but the browser's docShell will
 | 
						|
    // be swapped with the browser in the real panel when it's ready. For remote
 | 
						|
    // extensions, this popup is shared between all extensions.
 | 
						|
    let panel;
 | 
						|
    if (extension.remote) {
 | 
						|
      panel = document.getElementById(REMOTE_PANEL_ID);
 | 
						|
      if (!panel) {
 | 
						|
        panel = createPanel(true);
 | 
						|
        panel.id = REMOTE_PANEL_ID;
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      panel = createPanel();
 | 
						|
    }
 | 
						|
 | 
						|
    super(extension, panel, popupURL, browserStyle, fixedWidth, blockParser);
 | 
						|
 | 
						|
    this.ignoreResizes = true;
 | 
						|
 | 
						|
    this.attached = false;
 | 
						|
    this.shown = false;
 | 
						|
    this.tempPanel = panel;
 | 
						|
    this.tempBrowser = this.browser;
 | 
						|
 | 
						|
    // NOTE: this class is added to the preload browser and never removed because
 | 
						|
    // the preload browser is then switched with a new browser once we are about to
 | 
						|
    // make the popup visible (this class is not actually used anywhere but it may
 | 
						|
    // be useful to keep it around to be able to identify the preload buffer while
 | 
						|
    // investigating issues).
 | 
						|
    this.browser.classList.add("webextension-preload-browser");
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Attaches the pre-loaded browser to the given view node, and reserves a
 | 
						|
   * promise which resolves when the browser is ready.
 | 
						|
   *
 | 
						|
   * @param {Element} viewNode
 | 
						|
   *        The node to attach the browser to.
 | 
						|
   * @returns {Promise<boolean>}
 | 
						|
   *        Resolves when the browser is ready. Resolves to `false` if the
 | 
						|
   *        browser was destroyed before it was fully loaded, and the popup
 | 
						|
   *        should be closed, or `true` otherwise.
 | 
						|
   */
 | 
						|
  async attach(viewNode) {
 | 
						|
    if (this.destroyed) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
 | 
						|
    this.panel.removeEventListener("popuppositioned", this, {
 | 
						|
      once: true,
 | 
						|
      capture: true,
 | 
						|
    });
 | 
						|
 | 
						|
    this.viewNode = viewNode;
 | 
						|
    this.viewNode.addEventListener(this.DESTROY_EVENT, this);
 | 
						|
    this.viewNode.setAttribute("closemenu", "none");
 | 
						|
 | 
						|
    this.panel.addEventListener("popuppositioned", this, {
 | 
						|
      once: true,
 | 
						|
      capture: true,
 | 
						|
    });
 | 
						|
    if (this.extension.remote) {
 | 
						|
      this.panel.setAttribute("remote", "true");
 | 
						|
    }
 | 
						|
 | 
						|
    // Wait until the browser element is fully initialized, and give it at least
 | 
						|
    // a short grace period to finish loading its initial content, if necessary.
 | 
						|
    //
 | 
						|
    // In practice, the browser that was created by the mousdown handler should
 | 
						|
    // nearly always be ready by this point.
 | 
						|
    await Promise.all([
 | 
						|
      this.browserReady,
 | 
						|
      Promise.race([
 | 
						|
        // This promise may be rejected if the popup calls window.close()
 | 
						|
        // before it has fully loaded.
 | 
						|
        this.browserLoaded.catch(() => {}),
 | 
						|
        new Promise(resolve => lazy.setTimeout(resolve, POPUP_LOAD_TIMEOUT_MS)),
 | 
						|
      ]),
 | 
						|
    ]);
 | 
						|
 | 
						|
    const { panel } = this;
 | 
						|
 | 
						|
    if (!this.destroyed && !panel) {
 | 
						|
      this.destroy();
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.destroyed) {
 | 
						|
      lazy.CustomizableUI.hidePanelForNode(viewNode);
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    this.attached = true;
 | 
						|
 | 
						|
    this.setBackground(this.background);
 | 
						|
 | 
						|
    let flushPromise = this.window.promiseDocumentFlushed(() => {
 | 
						|
      let win = this.window;
 | 
						|
 | 
						|
      // Calculate the extra height available on the screen above and below the
 | 
						|
      // menu panel. Use that to calculate the how much the sub-view may grow.
 | 
						|
      let popupRect = panel.getBoundingClientRect();
 | 
						|
      let screenBottom = win.screen.availTop + win.screen.availHeight;
 | 
						|
      let popupBottom = win.mozInnerScreenY + popupRect.bottom;
 | 
						|
      let popupTop = win.mozInnerScreenY + popupRect.top;
 | 
						|
 | 
						|
      // Store the initial height of the view, so that we never resize menu panel
 | 
						|
      // sub-views smaller than the initial height of the menu.
 | 
						|
      this.viewHeight = viewNode.getBoundingClientRect().height;
 | 
						|
 | 
						|
      this.extraHeight = {
 | 
						|
        bottom: Math.max(0, screenBottom - popupBottom),
 | 
						|
        top: Math.max(0, popupTop - win.screen.availTop),
 | 
						|
      };
 | 
						|
    });
 | 
						|
 | 
						|
    // Create a new browser in the real popup.
 | 
						|
    let browser = this.browser;
 | 
						|
    await this.createBrowser(this.viewNode);
 | 
						|
 | 
						|
    this.browser.swapDocShells(browser);
 | 
						|
    this.destroyBrowser(browser);
 | 
						|
 | 
						|
    await flushPromise;
 | 
						|
 | 
						|
    // Check if the popup has been destroyed while we were waiting for the
 | 
						|
    // document flush promise to be resolve.
 | 
						|
    if (this.destroyed) {
 | 
						|
      this.closePopup();
 | 
						|
      this.destroy();
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.dimensions) {
 | 
						|
      if (this.fixedWidth) {
 | 
						|
        delete this.dimensions.width;
 | 
						|
      }
 | 
						|
      this.resizeBrowser(this.dimensions);
 | 
						|
    }
 | 
						|
 | 
						|
    this.ignoreResizes = false;
 | 
						|
 | 
						|
    this.viewNode.customRectGetter = () => {
 | 
						|
      return { height: this.lastCalculatedInViewHeight || this.viewHeight };
 | 
						|
    };
 | 
						|
 | 
						|
    this.removeTempPanel();
 | 
						|
 | 
						|
    this.shown = true;
 | 
						|
 | 
						|
    if (this.destroyed) {
 | 
						|
      this.closePopup();
 | 
						|
      this.destroy();
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    let event = new this.window.CustomEvent("WebExtPopupLoaded", {
 | 
						|
      bubbles: true,
 | 
						|
      detail: { extension: this.extension },
 | 
						|
    });
 | 
						|
    this.browser.dispatchEvent(event);
 | 
						|
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  removeTempPanel() {
 | 
						|
    if (this.tempPanel) {
 | 
						|
      if (this.tempPanel.id !== REMOTE_PANEL_ID) {
 | 
						|
        this.tempPanel.remove();
 | 
						|
      }
 | 
						|
      this.tempPanel = null;
 | 
						|
    }
 | 
						|
    if (this.tempBrowser) {
 | 
						|
      this.tempBrowser.parentNode.remove();
 | 
						|
      this.tempBrowser = null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  destroy() {
 | 
						|
    return super.destroy().then(() => {
 | 
						|
      this.removeTempPanel();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  get DESTROY_EVENT() {
 | 
						|
    return "ViewHiding";
 | 
						|
  }
 | 
						|
 | 
						|
  closePopup() {
 | 
						|
    if (this.shown) {
 | 
						|
      lazy.CustomizableUI.hidePanelForNode(this.viewNode);
 | 
						|
    } else if (this.attached) {
 | 
						|
      this.destroyed = true;
 | 
						|
    } else {
 | 
						|
      this.destroy();
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |