Bug 1385548 - Part 1: Support tab modals in WebExtensions options_ui pages.
This patch introduces a stub gBrowser object which allow a WebExtensions options_ui page
to open a tab modal using alert/prompt/confirm.
The about:addons page is defined at toolkit level but the TabModalPromptBox is defined
at browser level, and so to be able to provide a TabMobalPromptBox from the about:addons
page this patch uses the implementation provided by the window that contains the
about:addons tab, if any.
MozReview-Commit-ID: m6khgJyMs
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -6094,18 +6094,20 @@
// Focus window for beforeunload dialog so it is seen but don't
// steal focus from other applications.
if (event.detail &&
event.detail.tabPrompt &&
event.detail.inPermitUnload &&
Services.focus.activeWindow)
window.focus();
- // Don't need to act if the tab is already selected:
- if (tabForEvent.selected)
+ // Don't need to act if the tab is already selected or if there isn't
+ // a tab for the event (e.g. for the webextensions options_ui remote
+ // browsers embedded in the "about:addons" page):
+ if (!tabForEvent || tabForEvent.selected)
return;
// We always switch tabs for beforeunload tab-modal prompts.
if (event.detail &&
event.detail.tabPrompt &&
!event.detail.inPermitUnload) {
let docPrincipal = targetIsWindow ? event.target.document.nodePrincipal : null;
// At least one of these should/will be non-null:
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -433,20 +433,25 @@ var EventTargetParent = {
if (target instanceof Ci.nsIDOMXULElement) {
if (target.localName == "browser") {
return target;
} else if (target.localName == "tab") {
return target.linkedBrowser;
}
- // Check if |target| is somewhere on the patch from the
+ // Check if |target| is somewhere on the path from the
// <tabbrowser> up to the root element.
let window = target.ownerGlobal;
- if (window && target.contains(window.gBrowser)) {
+
+ // Some non-browser windows define gBrowser globals which are not elements
+ // and can't be passed to target.contains().
+ if (window &&
+ window.gBrowser instanceof Ci.nsIDOMXULElement &&
+ target.contains(window.gBrowser)) {
return window;
}
}
return null;
},
// When a given event fires in the child, we fire it on the
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1,17 +1,17 @@
/* 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";
/* import-globals-from ../../../content/contentAreaUtils.js */
/* globals XMLStylesheetProcessingInstruction */
-/* exported UPDATES_RELEASENOTES_TRANSFORMFILE, XMLURI_PARSE_ERROR, loadView */
+/* exported UPDATES_RELEASENOTES_TRANSFORMFILE, XMLURI_PARSE_ERROR, loadView, gBrowser */
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
@@ -3646,25 +3646,25 @@ var gDetailView = {
var rows = document.getElementById("detail-downloads").parentNode;
try {
if (this._addon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER) {
whenViewLoaded(async () => {
await this._addon.startupPromise;
- let browser = await this.createOptionsBrowser(rows);
+ const browserContainer = await this.createOptionsBrowser(rows);
// Make sure the browser is unloaded as soon as we change views,
// rather than waiting for the next detail view to load.
document.addEventListener("ViewChanged", function() {
- browser.remove();
+ browserContainer.remove();
}, {once: true});
- finish(browser);
+ finish(browserContainer);
});
if (aCallback)
aCallback();
} else {
var xhr = new XMLHttpRequest();
xhr.open("GET", this._addon.optionsURL, true);
xhr.responseType = "xml";
@@ -3729,16 +3729,19 @@ var gDetailView = {
let detailViewBoxObject = gDetailView.node.boxObject;
top -= detailViewBoxObject.y;
detailViewBoxObject.scrollTo(0, top);
}
},
async createOptionsBrowser(parentNode) {
+ let stack = document.createElement("stack");
+ stack.setAttribute("id", "addon-options-prompts-stack");
+
let browser = document.createElement("browser");
browser.setAttribute("type", "content");
browser.setAttribute("disableglobalhistory", "true");
browser.setAttribute("id", "addon-options");
browser.setAttribute("class", "inline-options-browser");
browser.setAttribute("transparent", "true");
browser.setAttribute("forcemessagemanager", "true");
browser.setAttribute("selectmenulist", "ContentSelectDropdown");
@@ -3757,31 +3760,32 @@ var gDetailView = {
if (remote) {
browser.setAttribute("remote", "true");
browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
readyPromise = promiseEvent("XULFrameLoaderCreated", browser);
} else {
readyPromise = promiseEvent("load", browser, true);
}
- parentNode.appendChild(browser);
+ stack.appendChild(browser);
+ parentNode.appendChild(stack);
// Force bindings to apply synchronously.
browser.clientTop;
await readyPromise;
ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
return new Promise(resolve => {
let messageListener = {
receiveMessage({name, data}) {
if (name === "Extension:BrowserResized")
browser.style.height = `${data.height}px`;
else if (name === "Extension:BrowserContentLoaded")
- resolve(browser);
+ resolve(stack);
},
};
let mm = browser.messageManager;
mm.loadFrameScript("chrome://extensions/content/ext-browser-content.js",
false);
mm.addMessageListener("Extension:BrowserContentLoaded", messageListener);
mm.addMessageListener("Extension:BrowserResized", messageListener);
@@ -4127,8 +4131,23 @@ var gDragDrop = {
AddonManager.installAddonFromAOM(browser, document.documentURIObject, install);
}, "application/x-xpinstall");
}
}
aEvent.preventDefault();
}
};
+
+// Stub tabbrowser implementation for use by the tab-modal alert code
+// when an alert/prompt/confirm method is called in a WebExtensions options_ui page
+// (See Bug 1385548 for rationale).
+var gBrowser = {
+ getTabModalPromptBox(browser) {
+ const parentWindow = document.docShell.chromeEventHandler.ownerGlobal;
+
+ if (parentWindow.gBrowser) {
+ return parentWindow.gBrowser.getTabModalPromptBox(browser);
+ }
+
+ return null;
+ }
+};
--- a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
@@ -91,27 +91,29 @@ async function openDetailsBrowser(addonI
await TestUtils.topicObserved(AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
(subject, data) => data == addonId);
is(gManagerWindow.gViewController.currentViewId,
`addons://detail/${encodeURIComponent(addonId)}/preferences`,
"Current view should scroll to preferences");
var browser = gManagerWindow.document.querySelector(
- "#detail-grid > rows > .inline-options-browser");
- var rows = browser.parentNode;
+ "#detail-grid > rows > stack > .inline-options-browser");
+ var rows = browser.parentNode.parentNode;
let url = await ContentTask.spawn(browser, {}, () => content.location.href);
- ok(browser, "Grid should have a browser child");
- is(browser.localName, "browser", "Grid should have a browser child");
+ ok(browser, "Grid should have a browser descendant");
+ is(browser.localName, "browser", "Grid should have a browser descendant");
is(url, addon.mAddon.optionsURL, "Browser has the expected options URL loaded")
+ is(browser.clientWidth, browser.parentNode.clientWidth,
+ "Browser should be the same width as its direct parent");
is(browser.clientWidth, rows.clientWidth,
- "Browser should be the same width as its parent node");
+ "Browser should be the same width as its rows ancestor");
button = gManagerWindow.document.getElementById("detail-prefs-btn");
is_element_hidden(button, "Preferences button should not be visible");
return browser;
}
--- a/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
@@ -25,27 +25,29 @@ async function runTest(installer) {
await TestUtils.topicObserved(AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
(subject, data) => data == id);
is(mgrWindow.gViewController.currentViewId,
`addons://detail/${encodeURIComponent(id)}/preferences`,
"Current view should scroll to preferences");
- var browser = mgrWindow.document.querySelector("#detail-grid > rows > .inline-options-browser");
+ var browser = mgrWindow.document.querySelector("#detail-grid > rows > stack > .inline-options-browser");
var rows = browser.parentNode;
let url = await ContentTask.spawn(browser, {}, () => content.location.href);
- ok(browser, "Grid should have a browser child");
- is(browser.localName, "browser", "Grid should have a browser child");
+ ok(browser, "Grid should have a browser descendant");
+ is(browser.localName, "browser", "Grid should have a browser descendant");
is(url, element.mAddon.optionsURL, "Browser has the expected options URL loaded")
+ is(browser.clientWidth, browser.parentNode.clientWidth,
+ "Browser should be the same width as its direct parent");
is(browser.clientWidth, rows.clientWidth,
- "Browser should be the same width as its parent node");
+ "Browser should be the same width as its rows ancestor");
button = mgrWindow.document.getElementById("detail-prefs-btn");
is_element_hidden(button, "Preferences button should not be visible");
await close_manager(mgrWindow);
addon.uninstall();
}