forked from mirrors/gecko-dev
Backed out changeset 8781a0d1254d (bug 1810141) Backed out changeset 131037295784 (bug 1810141) Backed out changeset 3852fbe290f4 (bug 1810141) Backed out changeset 118f131a524a (bug 1810141) Backed out changeset ab5d76846e10 (bug 1810141) Backed out changeset dce3aa683445 (bug 1810141) Backed out changeset 4dc41d90dbb3 (bug 1810141) Backed out changeset 50b57ba1a061 (bug 1810141) Backed out changeset 569de94781e4 (bug 1810141)
763 lines
23 KiB
JavaScript
763 lines
23 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/. */
|
|
|
|
"use strict";
|
|
|
|
/* exported PanelPopup, ViewPopup */
|
|
|
|
var EXPORTED_SYMBOLS = ["BasePopup", "PanelPopup", "ViewPopup"];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"CustomizableUI",
|
|
"resource:///modules/CustomizableUI.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"ExtensionParent",
|
|
"resource://gre/modules/ExtensionParent.jsm"
|
|
);
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
});
|
|
|
|
const { AppConstants } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/AppConstants.sys.mjs"
|
|
);
|
|
const { ExtensionCommon } = ChromeUtils.import(
|
|
"resource://gre/modules/ExtensionCommon.jsm"
|
|
);
|
|
const { ExtensionUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/ExtensionUtils.jsm"
|
|
);
|
|
|
|
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(event) {
|
|
resolve();
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
XPCOMUtils.defineLazyGetter(lazy, "standaloneStylesheets", () => {
|
|
let stylesheets = [];
|
|
|
|
if (AppConstants.platform === "macosx") {
|
|
stylesheets.push("chrome://browser/content/extension-mac-panel.css");
|
|
} else if (AppConstants.platform === "win") {
|
|
stylesheets.push("chrome://browser/content/extension-win-panel.css");
|
|
} else if (AppConstants.platform === "linux") {
|
|
stylesheets.push("chrome://browser/content/extension-linux-panel.css");
|
|
}
|
|
return stylesheets;
|
|
});
|
|
|
|
const REMOTE_PANEL_ID = "webextension-remote-preload-panel";
|
|
|
|
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(...lazy.ExtensionParent.extensionStylesheets);
|
|
}
|
|
if (!this.fixedWidth) {
|
|
sheets.push(...lazy.standaloneStylesheets);
|
|
}
|
|
|
|
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("selectmenuconstrained", "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.loadURI(popupURL, {
|
|
triggeringPrincipal: this.extension.principal,
|
|
});
|
|
});
|
|
}
|
|
|
|
unblockParser() {
|
|
this.browserReady.then(browser => {
|
|
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());
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|