diff --git a/browser/components/extensions/ext-browserAction.js b/browser/components/extensions/ext-browserAction.js index 5ce062aa0be2..f630516fedb6 100644 --- a/browser/components/extensions/ext-browserAction.js +++ b/browser/components/extensions/ext-browserAction.js @@ -87,7 +87,6 @@ BrowserAction.prototype = { onDestroyed: document => { let view = document.getElementById(this.viewId); if (view) { - CustomizableUI.hidePanelForNode(view); view.remove(); } }, diff --git a/browser/components/extensions/ext-utils.js b/browser/components/extensions/ext-utils.js index 47377d98d417..fcc38c192f8d 100644 --- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -28,10 +28,12 @@ const POPUP_LOAD_TIMEOUT_MS = 200; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +// Minimum time between two resizes. +const RESIZE_TIMEOUT = 100; + var { DefaultWeakMap, EventManager, - promiseEvent, } = ExtensionUtils; // This file provides some useful code for the |tabs| and |windows| @@ -57,11 +59,17 @@ function promisePopupShown(popup) { }); } -XPCOMUtils.defineLazyGetter(this, "popupStylesheets", () => { - let stylesheets = ["chrome://browser/content/extension.css"]; +XPCOMUtils.defineLazyGetter(this, "stylesheets", () => { + let styleSheetURI = NetUtil.newURI("chrome://browser/content/extension.css"); + let styleSheet = styleSheetService.preloadSheet(styleSheetURI, + styleSheetService.AGENT_SHEET); + let stylesheets = [styleSheet]; if (AppConstants.platform === "macosx") { - stylesheets.push("chrome://browser/content/extension-mac.css"); + styleSheetURI = NetUtil.newURI("chrome://browser/content/extension-mac.css"); + let macStyleSheet = styleSheetService.preloadSheet(styleSheetURI, + styleSheetService.AGENT_SHEET); + stylesheets.push(macStyleSheet); } return stylesheets; }); @@ -70,10 +78,16 @@ XPCOMUtils.defineLazyGetter(this, "standaloneStylesheets", () => { let stylesheets = []; if (AppConstants.platform === "macosx") { - stylesheets.push("chrome://browser/content/extension-mac-panel.css"); + let styleSheetURI = NetUtil.newURI("chrome://browser/content/extension-mac-panel.css"); + let macStyleSheet = styleSheetService.preloadSheet(styleSheetURI, + styleSheetService.AGENT_SHEET); + stylesheets.push(macStyleSheet); } if (AppConstants.platform === "win") { - stylesheets.push("chrome://browser/content/extension-win-panel.css"); + let styleSheetURI = NetUtil.newURI("chrome://browser/content/extension-win-panel.css"); + let winStyleSheet = styleSheetService.preloadSheet(styleSheetURI, + styleSheetService.AGENT_SHEET); + stylesheets.push(winStyleSheet); } return stylesheets; }); @@ -95,6 +109,7 @@ class BasePopup { this.window = viewNode.ownerGlobal; this.destroyed = false; this.fixedWidth = fixedWidth; + this.ignoreResizes = true; this.contentReady = new Promise(resolve => { this._resolveContentReady = resolve; @@ -142,16 +157,12 @@ class BasePopup { } destroyBrowser(browser) { - 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. - if (mm) { - mm.removeMessageListener("DOMTitleChanged", this); - mm.removeMessageListener("Extension:BrowserBackgroundChanged", this); - mm.removeMessageListener("Extension:BrowserContentLoaded", this); - mm.removeMessageListener("Extension:BrowserResized", this); - mm.removeMessageListener("Extension:DOMWindowClose", this); - } + browser.removeEventListener("DOMWindowCreated", this, true); + browser.removeEventListener("load", this, true); + browser.removeEventListener("DOMContentLoaded", this, true); + browser.removeEventListener("DOMTitleChanged", this, true); + browser.removeEventListener("DOMWindowClose", this, true); + browser.removeEventListener("MozScrolledAreaChanged", this, true); } // Returns the name of the event fired on `viewNode` when the popup is being @@ -160,19 +171,6 @@ class BasePopup { throw new Error("Not implemented"); } - get STYLESHEETS() { - let sheets = []; - - if (this.browserStyle) { - sheets.push(...popupStylesheets); - } - if (!this.fixedWidth) { - sheets.push(...standaloneStylesheets); - } - - return sheets; - } - get panel() { let panel = this.viewNode; while (panel && panel.localName != "panel") { @@ -181,40 +179,70 @@ class BasePopup { return panel; } - receiveMessage({name, data}) { - switch (name) { - case "DOMTitleChanged": - this.viewNode.setAttribute("aria-label", this.browser.contentTitle); - break; - - 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; - - case "Extension:DOMWindowClose": - this.closePopup(); - break; - } - } - handleEvent(event) { switch (event.type) { case this.DESTROY_EVENT: this.destroy(); break; + + case "DOMWindowCreated": + if (event.target === this.browser.contentDocument) { + let winUtils = this.browser.contentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + if (this.browserStyle) { + for (let stylesheet of stylesheets) { + winUtils.addSheet(stylesheet, winUtils.AGENT_SHEET); + } + } + if (!this.fixedWidth) { + for (let stylesheet of standaloneStylesheets) { + winUtils.addSheet(stylesheet, winUtils.AGENT_SHEET); + } + } + } + break; + + case "DOMWindowClose": + if (event.target === this.browser.contentWindow) { + event.preventDefault(); + this.closePopup(); + } + break; + + case "DOMTitleChanged": + this.viewNode.setAttribute("aria-label", this.browser.contentTitle); + break; + + case "DOMContentLoaded": + this.browserLoadedDeferred.resolve(); + this.resizeBrowser(true); + break; + + case "load": + // We use a capturing listener, so we get this event earlier than any + // load listeners in the content page. Resizing after a timeout ensures + // that we calculate the size after the entire event cycle has completed + // (unless someone spins the event loop, anyway), and hopefully after + // the content has made any modifications. + Promise.resolve().then(() => { + this.resizeBrowser(true); + }); + + // Mutation observer to make sure the panel shrinks when the content does. + new this.browser.contentWindow.MutationObserver(this.resizeBrowser.bind(this)).observe( + this.browser.contentDocument.documentElement, { + attributes: true, + characterData: true, + childList: true, + subtree: true, + }); + break; + + case "MozScrolledAreaChanged": + this.resizeBrowser(); + break; } } @@ -241,12 +269,12 @@ class BasePopup { viewNode.appendChild(this.browser); let initBrowser = browser => { - let mm = browser.messageManager; - mm.addMessageListener("DOMTitleChanged", this); - mm.addMessageListener("Extension:BrowserBackgroundChanged", this); - mm.addMessageListener("Extension:BrowserContentLoaded", this); - mm.addMessageListener("Extension:BrowserResized", this); - mm.addMessageListener("Extension:DOMWindowClose", this, true); + browser.addEventListener("DOMWindowCreated", this, true); + browser.addEventListener("load", this, true); + browser.addEventListener("DOMContentLoaded", this, true); + browser.addEventListener("DOMTitleChanged", this, true); + browser.addEventListener("DOMWindowClose", this, true); + browser.addEventListener("MozScrolledAreaChanged", this, true); }; if (!popupURL) { @@ -254,28 +282,82 @@ class BasePopup { return this.browser; } - return promiseEvent(this.browser, "load").then(() => { + return new Promise(resolve => { + // The first load event is for about:blank. + // We can't finish setting up the browser until the binding has fully + // initialized. Waiting for the first load event guarantees that it has. + let loadListener = event => { + this.browser.removeEventListener("load", loadListener, true); + resolve(); + }; + this.browser.addEventListener("load", loadListener, true); + }).then(() => { initBrowser(this.browser); - let mm = this.browser.messageManager; + let {contentWindow} = this.browser; - mm.loadFrameScript( - "chrome://extensions/content/ext-browser-content.js", false); - - mm.sendAsyncMessage("Extension:InitBrowser", { - allowScriptsToClose: true, - fixedWidth: this.fixedWidth, - maxWidth: 800, - maxHeight: 600, - stylesheets: this.STYLESHEETS, - }); + contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .allowScriptsToClose(); this.browser.setAttribute("src", popupURL); }); } - resizeBrowser({width, height, detail}) { + // Resizes the browser to match the preferred size of the content (debounced). + resizeBrowser(ignoreThrottling = false) { + if (this.ignoreResizes) { + return; + } + + if (ignoreThrottling && this.resizeTimeout) { + this.window.clearTimeout(this.resizeTimeout); + this.resizeTimeout = null; + } + + if (this.resizeTimeout == null) { + this.resizeTimeout = this.window.setTimeout(() => { + try { + this._resizeBrowser(); + } finally { + this.resizeTimeout = null; + } + }, RESIZE_TIMEOUT); + + this._resizeBrowser(); + } + } + + _resizeBrowser() { + let doc = this.browser && this.browser.contentDocument; + if (!doc || !doc.documentElement) { + return; + } + + let root = doc.documentElement; + let body = doc.body; + if (!body || doc.compatMode == "BackCompat") { + // In quirks mode, the root element is used as the scroll frame, and the + // body lies about its scroll geometry, and returns the values for the + // root instead. + body = root; + } + + if (this.fixedWidth) { + // If we're in a fixed-width area (namely a slide-in subview of the main + // menu panel), we need to calculate the view height based on the + // preferred height of the content document's root scrollable element at the + // current width, rather than the complete preferred dimensions of the + // content window. + + // Compensate for any offsets (margin, padding, ...) between the scroll + // area of the body and the outer height of the document. + let getHeight = elem => elem.getBoundingClientRect(elem).height; + let bodyPadding = getHeight(root) - getHeight(body); + + let height = Math.ceil(body.scrollHeight + bodyPadding); + // 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"; @@ -292,32 +374,48 @@ class BasePopup { height = Math.max(height, this.viewHeight); this.viewNode.style.maxHeight = `${height}px`; } else { + // Copy the background color of the document's body to the panel if it's + // fully opaque. + let panelBackground = ""; + let panelArrow = ""; + + let background = doc.defaultView.getComputedStyle(body).backgroundColor; + if (background != "transparent") { + let bgColor = colorUtils.colorToRGBA(background); + if (bgColor.a == 1) { + panelBackground = background; + let borderColor = this.borderColor || background; + + panelArrow = `url("data:image/svg+xml,${encodeURIComponent(` + + + + + `)}")`; + } + } + + this.panel.style.setProperty("--arrowpanel-background", panelBackground); + this.panel.style.setProperty("--panel-arrow-image-vertical", panelArrow); + + + // Adjust the size of the browser based on its content's preferred size. + let {contentViewer} = this.browser.docShell; + let ratio = this.window.devicePixelRatio; + + let w = {}, h = {}; + contentViewer.getContentSizeConstrained(800 * ratio, 600 * ratio, w, h); + let width = Math.ceil(w.value / ratio); + let height = Math.ceil(h.value / ratio); + this.browser.style.width = `${width}px`; this.browser.style.height = `${height}px`; } - let event = new this.window.CustomEvent("WebExtPopupResized", {detail}); + let event = new this.window.CustomEvent("WebExtPopupResized"); this.browser.dispatchEvent(event); - } - setBackground(background) { - let panelBackground = ""; - let panelArrow = ""; - - if (background) { - let borderColor = this.borderColor || background; - - panelBackground = background; - panelArrow = `url("data:image/svg+xml,${encodeURIComponent(` - - - - - `)}")`; - } - - this.panel.style.setProperty("--arrowpanel-background", panelBackground); - this.panel.style.setProperty("--panel-arrow-image-vertical", panelArrow); + this._resolveContentReady(); } } @@ -328,7 +426,7 @@ class BasePopup { */ BasePopup.instances = new DefaultWeakMap(() => new WeakMap()); -class PanelPopup extends BasePopup { +global.PanelPopup = class PanelPopup extends BasePopup { constructor(extension, imageNode, popupURL, browserStyle) { let document = imageNode.ownerDocument; @@ -343,6 +441,8 @@ class PanelPopup extends BasePopup { super(extension, panel, popupURL, browserStyle); + this.ignoreResizes = false; + this.contentReady.then(() => { panel.openPopup(imageNode, "bottomcenter topright", 0, 0, false, false); }); @@ -365,9 +465,9 @@ class PanelPopup extends BasePopup { } }); } -} +}; -class ViewPopup extends BasePopup { +global.ViewPopup = class ViewPopup extends BasePopup { constructor(extension, window, popupURL, browserStyle, fixedWidth) { let document = window.document; @@ -380,8 +480,6 @@ class ViewPopup extends BasePopup { super(extension, panel, popupURL, browserStyle, fixedWidth); - this.ignoreResizes = true; - this.attached = false; this.tempPanel = panel; @@ -451,9 +549,7 @@ class ViewPopup extends BasePopup { this.destroyBrowser(browser); this.ignoreResizes = false; - if (this.dimensions) { - this.resizeBrowser(this.dimensions); - } + this.resizeBrowser(true); this.tempPanel.remove(); this.tempPanel = null; @@ -482,9 +578,7 @@ class ViewPopup extends BasePopup { this.destroy(); } } -} - -Object.assign(global, {PanelPopup, ViewPopup}); +}; // Manages tab-specific context data, and dispatching tab select events // across all windows. diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js index 8bda566ee400..d485f9c79143 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js @@ -14,7 +14,8 @@ function* awaitResize(browser) { // looking for, but don't wait longer than a few seconds. return Promise.race([ - BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized", event => event.detail === "delayed"), + BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized") + .then(() => BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized")), new Promise(resolve => setTimeout(resolve, 5000)), ]); } diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js index 78d57a51b4dc..671cfa5d8f00 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js @@ -15,8 +15,6 @@ function* awaitResize(browser) { } add_task(function* testPageActionPopupResize() { - let browser; - let extension = ExtensionTestUtils.loadExtension({ manifest: { "page_action": { @@ -25,7 +23,6 @@ add_task(function* testPageActionPopupResize() { }, }, background: function() { - /* global browser */ browser.tabs.query({active: true, currentWindow: true}, tabs => { const tabId = tabs[0].id; @@ -45,10 +42,12 @@ add_task(function* testPageActionPopupResize() { clickPageAction(extension, window); - browser = yield awaitExtensionPanel(extension); + let {target: panelDocument} = yield BrowserTestUtils.waitForEvent(document, "load", true, (event) => { + info(`Loaded ${event.target.location}`); + return event.target.location && event.target.location.href.endsWith("popup.html"); + }); - let panelWindow = browser.contentWindow; - let panelDocument = panelWindow.document; + let panelWindow = panelDocument.defaultView; let panelBody = panelDocument.body.firstChild; let body = panelDocument.body; let root = panelDocument.documentElement; @@ -70,7 +69,7 @@ add_task(function* testPageActionPopupResize() { panelBody.style.height = `${size}px`; panelBody.style.width = `${size}px`; - return BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized"); + return BrowserTestUtils.waitForEvent(panelWindow, "resize"); } let sizes = [ diff --git a/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js index c20eb3e1b9ed..19035476e46a 100644 --- a/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js +++ b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js @@ -70,7 +70,5 @@ add_task(function* testPageAction() { yield extension.unload(); - yield new Promise(resolve => setTimeout(resolve, 0)); - is(panel.parentNode, null, "Panel should be removed from the document"); }); diff --git a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js index 035375682d67..7c463a91a908 100644 --- a/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js +++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js @@ -10,4 +10,5 @@ // browser_addons_debug_webextension.js function myWebExtensionPopupAddonFunction() { // eslint-disable-line no-unused-vars console.log("Popup page function called", browser.runtime.getManifest()); + window.close(); } diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 6f0db2dbc7c6..f52917637cb7 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -54,13 +54,17 @@ XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Preferences", "resource://gre/modules/Preferences.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "require", - "resource://devtools/shared/Loader.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Schemas", "resource://gre/modules/Schemas.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyGetter(this, "require", () => { + let obj = {}; + Cu.import("resource://devtools/shared/Loader.jsm", obj); + return obj.require; +}); + Cu.import("resource://gre/modules/ExtensionContent.jsm"); Cu.import("resource://gre/modules/ExtensionManagement.jsm"); diff --git a/toolkit/components/extensions/ExtensionUtils.jsm b/toolkit/components/extensions/ExtensionUtils.jsm index f682a8ae469e..bb3890b9395e 100644 --- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -28,8 +28,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Locale", "resource://gre/modules/Locale.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Preferences", "resource://gre/modules/Preferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", @@ -37,10 +35,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", XPCOMUtils.defineLazyModuleGetter(this, "Schemas", "resource://gre/modules/Schemas.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService", - "@mozilla.org/content/style-sheet-service;1", - "nsIStyleSheetService"); - function getConsole() { return new ConsoleAPI({ maxLogLevelPref: "extensions.webextensions.log.level", @@ -154,20 +148,6 @@ class DefaultWeakMap extends WeakMap { } } -class DefaultMap extends Map { - constructor(defaultConstructor, init) { - super(init); - this.defaultConstructor = defaultConstructor; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.defaultConstructor(key)); - } - return super.get(key); - } -} - class SpreadArgs extends Array { constructor(args) { super(); @@ -1137,35 +1117,6 @@ function promiseDocumentLoaded(doc) { }); } -/** - * Returns a Promise which resolves when the given event is dispatched to the - * given element. - * - * @param {Element} element - * The element on which to listen. - * @param {string} eventName - * The event to listen for. - * @param {boolean} [useCapture = true] - * If true, listen for the even in the capturing rather than - * bubbling phase. - * @param {Event} [test] - * An optional test function which, when called with the - * observer's subject and data, should return true if this is the - * expected event, false otherwise. - * @returns {Promise} - */ -function promiseEvent(element, eventName, useCapture = true, test = event => true) { - return new Promise(resolve => { - function listener(event) { - if (test(event)) { - element.removeEventListener(eventName, listener, useCapture); - resolve(event); - } - } - element.addEventListener(eventName, listener, useCapture); - }); -} - /** * Returns a Promise which resolves the given observer topic has been * observed. @@ -2016,11 +1967,6 @@ function normalizeTime(date) { ? parseInt(date, 10) : date); } -const stylesheetMap = new DefaultMap(url => { - let uri = NetUtil.newURI(url); - return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET); -}); - this.ExtensionUtils = { detectLanguage, extend, @@ -2033,13 +1979,11 @@ this.ExtensionUtils = { normalizeTime, promiseDocumentLoaded, promiseDocumentReady, - promiseEvent, promiseObserved, runSafe, runSafeSync, runSafeSyncWithoutClone, runSafeWithoutClone, - stylesheetMap, BaseContext, DefaultWeakMap, EventEmitter, diff --git a/toolkit/components/extensions/ext-browser-content.js b/toolkit/components/extensions/ext-browser-content.js deleted file mode 100644 index e14ca50d6afb..000000000000 --- a/toolkit/components/extensions/ext-browser-content.js +++ /dev/null @@ -1,217 +0,0 @@ -/* 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"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "require", - "resource://devtools/shared/Loader.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); - -XPCOMUtils.defineLazyGetter(this, "colorUtils", () => { - return require("devtools/shared/css/color").colorUtils; -}); - -const { - stylesheetMap, -} = ExtensionUtils; - -/* globals addMessageListener, content, docShell, sendAsyncMessage */ - -// Minimum time between two resizes. -const RESIZE_TIMEOUT = 100; - -const BrowserListener = { - init({allowScriptsToClose, fixedWidth, maxHeight, maxWidth, stylesheets}) { - this.fixedWidth = fixedWidth; - this.stylesheets = stylesheets || []; - - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - - this.oldBackground = null; - - if (allowScriptsToClose) { - content.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .allowScriptsToClose(); - } - - addEventListener("DOMWindowCreated", this, true); - addEventListener("load", this, true); - addEventListener("DOMContentLoaded", this, true); - addEventListener("DOMWindowClose", this, true); - addEventListener("MozScrolledAreaChanged", this, true); - }, - - destroy() { - removeEventListener("DOMWindowCreated", this, true); - removeEventListener("load", this, true); - removeEventListener("DOMContentLoaded", this, true); - removeEventListener("DOMWindowClose", this, true); - removeEventListener("MozScrolledAreaChanged", this, true); - }, - - receiveMessage({name, data}) { - if (name === "Extension:InitBrowser") { - this.init(data); - } - }, - - handleEvent(event) { - switch (event.type) { - case "DOMWindowCreated": - if (event.target === content.document) { - let winUtils = content.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - - for (let url of this.stylesheets) { - winUtils.addSheet(stylesheetMap.get(url), winUtils.AGENT_SHEET); - } - } - break; - - case "DOMWindowClose": - if (event.target === content) { - event.preventDefault(); - - sendAsyncMessage("Extension:DOMWindowClose"); - } - break; - - case "DOMContentLoaded": - if (event.target === content.document) { - sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href}); - this.handleDOMChange(true); - } - break; - - case "load": - if (event.target.contentWindow === content) { - // For about:addons inline , we currently receive a load - // event on the element, but no load or DOMContentLoaded - // events from the content window. - sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href}); - } else if (event.target !== content.document) { - break; - } - - // We use a capturing listener, so we get this event earlier than any - // load listeners in the content page. Resizing after a timeout ensures - // that we calculate the size after the entire event cycle has completed - // (unless someone spins the event loop, anyway), and hopefully after - // the content has made any modifications. - Promise.resolve().then(() => { - this.handleDOMChange(true); - }); - - // Mutation observer to make sure the panel shrinks when the content does. - new content.MutationObserver(this.handleDOMChange.bind(this)).observe( - content.document.documentElement, { - attributes: true, - characterData: true, - childList: true, - subtree: true, - }); - break; - - case "MozScrolledAreaChanged": - this.handleDOMChange(); - break; - } - }, - - // Resizes the browser to match the preferred size of the content (debounced). - handleDOMChange(ignoreThrottling = false) { - if (ignoreThrottling && this.resizeTimeout) { - clearTimeout(this.resizeTimeout); - this.resizeTimeout = null; - } - - if (this.resizeTimeout == null) { - this.resizeTimeout = setTimeout(() => { - try { - if (content) { - this._handleDOMChange("delayed"); - } - } finally { - this.resizeTimeout = null; - } - }, RESIZE_TIMEOUT); - - this._handleDOMChange(); - } - }, - - _handleDOMChange(detail) { - let doc = content.document; - - let body = doc.body; - if (!body || doc.compatMode === "BackCompat") { - // In quirks mode, the root element is used as the scroll frame, and the - // body lies about its scroll geometry, and returns the values for the - // root instead. - body = doc.documentElement; - } - - - let result; - if (this.fixedWidth) { - // If we're in a fixed-width area (namely a slide-in subview of the main - // menu panel), we need to calculate the view height based on the - // preferred height of the content document's root scrollable element at the - // current width, rather than the complete preferred dimensions of the - // content window. - - // Compensate for any offsets (margin, padding, ...) between the scroll - // area of the body and the outer height of the document. - let getHeight = elem => elem.getBoundingClientRect(elem).height; - let bodyPadding = getHeight(doc.documentElement) - getHeight(body); - - let height = Math.ceil(body.scrollHeight + bodyPadding); - - result = {height, detail}; - } else { - let background = doc.defaultView.getComputedStyle(body).backgroundColor; - let bgColor = colorUtils.colorToRGBA(background); - if (bgColor.a !== 1) { - // Ignore non-opaque backgrounds. - background = null; - } - - if (background !== this.oldBackground) { - sendAsyncMessage("Extension:BrowserBackgroundChanged", {background}); - } - this.oldBackground = background; - - - // Adjust the size of the browser based on its content's preferred size. - let {contentViewer} = docShell; - let ratio = content.devicePixelRatio; - - let w = {}, h = {}; - contentViewer.getContentSizeConstrained(this.maxWidth * ratio, - this.maxHeight * ratio, - w, h); - - let width = Math.ceil(w.value / ratio); - let height = Math.ceil(h.value / ratio); - - result = {width, height, detail}; - } - - sendAsyncMessage("Extension:BrowserResized", result); - }, -}; - -addMessageListener("Extension:InitBrowser", BrowserListener); diff --git a/toolkit/components/extensions/jar.mn b/toolkit/components/extensions/jar.mn index fde6a3516762..3d0f1fc905b6 100644 --- a/toolkit/components/extensions/jar.mn +++ b/toolkit/components/extensions/jar.mn @@ -6,7 +6,6 @@ toolkit.jar: % content extensions %content/extensions/ content/extensions/ext-alarms.js content/extensions/ext-backgroundPage.js - content/extensions/ext-browser-content.js content/extensions/ext-cookies.js content/extensions/ext-downloads.js content/extensions/ext-management.js diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js index 7bbcee96505b..fec280ca8acf 100644 --- a/toolkit/mozapps/extensions/content/extensions.js +++ b/toolkit/mozapps/extensions/content/extensions.js @@ -83,79 +83,6 @@ XPCOMUtils.defineLazyGetter(gStrings, "appVersion", function() { document.addEventListener("load", initialize, true); window.addEventListener("unload", shutdown, false); -class MessageDispatcher { - constructor(target) { - this.listeners = new Map(); - this.target = target; - } - - addMessageListener(name, handler) { - if (!this.listeners.has(name)) { - this.listeners.set(name, new Set()); - } - - this.listeners.get(name).add(handler); - } - - removeMessageListener(name, handler) { - if (this.listeners.has(name)) { - this.listeners.get(name).delete(handler); - } - } - - sendAsyncMessage(name, data) { - for (let handler of this.listeners.get(name) || new Set()) { - Promise.resolve().then(() => { - handler.receiveMessage({ - name, - data, - target: this.target, - }); - }); - } - } -} - -/** - * A mock FrameMessageManager global to allow frame scripts to run in - * non-top-level, non-remote s as if they were top-level or - * remote. - * - * @param {Element} browser - * A XUL element. - */ -class FakeFrameMessageManager { - constructor(browser) { - let dispatcher = new MessageDispatcher(browser); - let frameDispatcher = new MessageDispatcher(null); - - this.sendAsyncMessage = frameDispatcher.sendAsyncMessage.bind(frameDispatcher); - this.addMessageListener = dispatcher.addMessageListener.bind(dispatcher); - this.removeMessageListener = dispatcher.removeMessageListener.bind(dispatcher); - - this.frame = { - get content() { - return browser.contentWindow; - }, - - get docShell() { - return browser.docShell; - }, - - addEventListener: browser.addEventListener.bind(browser), - removeEventListener: browser.removeEventListener.bind(browser), - - sendAsyncMessage: dispatcher.sendAsyncMessage.bind(dispatcher), - addMessageListener: frameDispatcher.addMessageListener.bind(frameDispatcher), - removeMessageListener: frameDispatcher.removeMessageListener.bind(frameDispatcher), - } - } - - loadFrameScript(url) { - Services.scriptloader.loadSubScript(url, Object.create(this.frame)); - } -} - var gPendingInitializations = 1; Object.defineProperty(this, "gIsInitializing", { get: () => gPendingInitializations > 0 @@ -3526,30 +3453,72 @@ var gDetailView = { browser.setAttribute("disableglobalhistory", "true"); browser.setAttribute("class", "inline-options-browser"); - return new Promise((resolve, reject) => { - let messageListener = { - receiveMessage({name, data}) { - if (name === "Extension:BrowserResized") - browser.style.height = `${data.height}px`; - else if (name === "Extension:BrowserContentLoaded") - resolve(browser); - }, - }; + // Resize at most 10 times per second. + const TIMEOUT = 100; + let timeout; + function resizeBrowser() { + if (timeout == null) { + _resizeBrowser(); + timeout = setTimeout(_resizeBrowser, TIMEOUT); + } + } + + function _resizeBrowser() { + timeout = null; + + let doc = browser.contentDocument; + if (!doc) { + return; + } + + let body = doc.body || doc.documentElement; + + let docHeight = doc.documentElement.getBoundingClientRect().height; + + let height = Math.ceil(body.scrollHeight + + // Compensate for any offsets between the scroll + // area of the body and the outer height of the + // document. + docHeight - body.clientHeight); + + // Note: This will trigger another MozScrolledAreaChanged event + // if it's different from the previous height. + browser.style.height = `${height}px`; + } + + return new Promise((resolve, reject) => { let onload = () => { browser.removeEventListener("load", onload, true); - let mm = new FakeFrameMessageManager(browser); - mm.loadFrameScript("chrome://extensions/content/ext-browser-content.js", - false); - mm.addMessageListener("Extension:BrowserContentLoaded", messageListener); - mm.addMessageListener("Extension:BrowserResized", messageListener); - mm.sendAsyncMessage("Extension:InitBrowser", {fixedWidth: true}); + browser.addEventListener("error", reject); + browser.addEventListener("load", event => { + // We only get load events targetted at one of these elements. + // If we're running in a tab, it's the . If we're + // running in a dialog, it's the content document. + if (event.target != browser && event.target != browser.contentDocument) + return; + + resolve(browser); + + browser.contentWindow.addEventListener("MozScrolledAreaChanged", event => { + resizeBrowser(); + }, true); + + new browser.contentWindow.MutationObserver(resizeBrowser).observe( + browser.contentDocument.documentElement, { + attributes: true, + characterData: true, + childList: true, + subtree: true, + }); + + resizeBrowser(); + }, true); browser.setAttribute("src", this._addon.optionsURL); }; browser.addEventListener("load", onload, true); - browser.addEventListener("error", reject); parentNode.appendChild(browser); });