mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 16:22:00 +00:00
7ccbffb9f7
Differential Revision: https://phabricator.services.mozilla.com/D174738
378 lines
10 KiB
JavaScript
378 lines
10 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
|
|
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
|
|
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
|
|
);
|
|
|
|
const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml";
|
|
|
|
/** @namespace */
|
|
export const modal = {
|
|
ACTION_CLOSED: "closed",
|
|
ACTION_OPENED: "opened",
|
|
};
|
|
|
|
/**
|
|
* Check for already existing modal or tab modal dialogs
|
|
*
|
|
* @param {browser.Context} context
|
|
* Reference to the browser context to check for existent dialogs.
|
|
*
|
|
* @returns {modal.Dialog}
|
|
* Returns instance of the Dialog class, or `null` if no modal dialog
|
|
* is present.
|
|
*/
|
|
modal.findModalDialogs = function(context) {
|
|
// First check if there is a modal dialog already present for the
|
|
// current browser window.
|
|
for (let win of Services.wm.getEnumerator(null)) {
|
|
// TODO: Use BrowserWindowTracker.getTopWindow for modal dialogs without
|
|
// an opener.
|
|
if (
|
|
win.document.documentURI === COMMON_DIALOG &&
|
|
win.opener &&
|
|
win.opener === context.window
|
|
) {
|
|
lazy.logger.trace("Found open window modal prompt");
|
|
return new modal.Dialog(() => context, win);
|
|
}
|
|
}
|
|
|
|
if (lazy.AppInfo.isAndroid) {
|
|
const geckoViewPrompts = context.window.prompts();
|
|
if (geckoViewPrompts.length) {
|
|
lazy.logger.trace("Found open GeckoView prompt");
|
|
const prompt = geckoViewPrompts[0];
|
|
return new modal.Dialog(() => context, prompt);
|
|
}
|
|
}
|
|
|
|
const contentBrowser = context.contentBrowser;
|
|
|
|
// If no modal dialog has been found yet, also check for tab and content modal
|
|
// dialogs for the current tab.
|
|
//
|
|
// TODO: Find an adequate implementation for Firefox on Android (bug 1708105)
|
|
if (contentBrowser?.tabDialogBox) {
|
|
let dialogs = contentBrowser.tabDialogBox.getTabDialogManager().dialogs;
|
|
if (dialogs.length) {
|
|
lazy.logger.trace("Found open tab modal prompt");
|
|
return new modal.Dialog(() => context, dialogs[0].frameContentWindow);
|
|
}
|
|
|
|
dialogs = contentBrowser.tabDialogBox.getContentDialogManager().dialogs;
|
|
|
|
// Even with the dialog manager handing back a dialog, the `Dialog` property
|
|
// gets lazily added. If it's not set yet, ignore the dialog for now.
|
|
if (dialogs.length && dialogs[0].frameContentWindow.Dialog) {
|
|
lazy.logger.trace("Found open content prompt");
|
|
return new modal.Dialog(() => context, dialogs[0].frameContentWindow);
|
|
}
|
|
}
|
|
|
|
// If no modal dialog has been found yet, check for old non SubDialog based
|
|
// content modal dialogs. Even with those deprecated in Firefox 89 we should
|
|
// keep supporting applications that don't have them implemented yet.
|
|
if (contentBrowser?.tabModalPromptBox) {
|
|
const prompts = contentBrowser.tabModalPromptBox.listPrompts();
|
|
if (prompts.length) {
|
|
lazy.logger.trace("Found open old-style content prompt");
|
|
return new modal.Dialog(() => context, null);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Observer for modal and tab modal dialogs.
|
|
*
|
|
* @param {function(): browser.Context} curBrowserFn
|
|
* Function that returns the current |browser.Context|.
|
|
*
|
|
* @returns {modal.DialogObserver}
|
|
* Returns instance of the DialogObserver class.
|
|
*/
|
|
modal.DialogObserver = class {
|
|
constructor(curBrowserFn) {
|
|
this._curBrowserFn = curBrowserFn;
|
|
|
|
this.callbacks = new Set();
|
|
this.register();
|
|
}
|
|
|
|
register() {
|
|
Services.obs.addObserver(this, "common-dialog-loaded");
|
|
Services.obs.addObserver(this, "domwindowopened");
|
|
Services.obs.addObserver(this, "geckoview-prompt-show");
|
|
Services.obs.addObserver(this, "tabmodal-dialog-loaded");
|
|
|
|
// Register event listener for all already open windows
|
|
for (let win of Services.wm.getEnumerator(null)) {
|
|
win.addEventListener("DOMModalDialogClosed", this);
|
|
}
|
|
}
|
|
|
|
unregister() {
|
|
Services.obs.removeObserver(this, "common-dialog-loaded");
|
|
Services.obs.removeObserver(this, "domwindowopened");
|
|
Services.obs.removeObserver(this, "geckoview-prompt-show");
|
|
Services.obs.removeObserver(this, "tabmodal-dialog-loaded");
|
|
|
|
// Unregister event listener for all open windows
|
|
for (let win of Services.wm.getEnumerator(null)) {
|
|
win.removeEventListener("DOMModalDialogClosed", this);
|
|
}
|
|
}
|
|
|
|
cleanup() {
|
|
this.callbacks.clear();
|
|
this.unregister();
|
|
}
|
|
|
|
handleEvent(event) {
|
|
lazy.logger.trace(`Received event ${event.type}`);
|
|
|
|
const chromeWin = event.target.opener
|
|
? event.target.opener.ownerGlobal
|
|
: event.target.ownerGlobal;
|
|
|
|
if (chromeWin != this._curBrowserFn().window) {
|
|
return;
|
|
}
|
|
|
|
this.callbacks.forEach(callback => {
|
|
callback(modal.ACTION_CLOSED, event.target);
|
|
});
|
|
}
|
|
|
|
observe(subject, topic) {
|
|
lazy.logger.trace(`Received observer notification ${topic}`);
|
|
|
|
const curBrowser = this._curBrowserFn();
|
|
|
|
switch (topic) {
|
|
// This topic is only used by the old-style content modal dialogs like
|
|
// alert, confirm, and prompt. It can be removed when only the new
|
|
// subdialog based content modals remain. Those will be made default in
|
|
// Firefox 89, and this case is deprecated.
|
|
case "tabmodal-dialog-loaded":
|
|
const container = curBrowser.contentBrowser.closest(
|
|
".browserSidebarContainer"
|
|
);
|
|
if (!container.contains(subject)) {
|
|
return;
|
|
}
|
|
this.callbacks.forEach(callback =>
|
|
callback(modal.ACTION_OPENED, subject)
|
|
);
|
|
break;
|
|
|
|
case "common-dialog-loaded":
|
|
const modalType = subject.Dialog.args.modalType;
|
|
|
|
if (
|
|
modalType === Services.prompt.MODAL_TYPE_TAB ||
|
|
modalType === Services.prompt.MODAL_TYPE_CONTENT
|
|
) {
|
|
// Find the container of the dialog in the parent document, and ensure
|
|
// it is a descendant of the same container as the current browser.
|
|
const container = curBrowser.contentBrowser.closest(
|
|
".browserSidebarContainer"
|
|
);
|
|
if (!container.contains(subject.docShell.chromeEventHandler)) {
|
|
return;
|
|
}
|
|
} else if (
|
|
subject.ownerGlobal != curBrowser.window &&
|
|
subject.opener?.ownerGlobal != curBrowser.window
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.callbacks.forEach(callback =>
|
|
callback(modal.ACTION_OPENED, subject)
|
|
);
|
|
break;
|
|
|
|
case "domwindowopened":
|
|
subject.addEventListener("DOMModalDialogClosed", this);
|
|
break;
|
|
|
|
case "geckoview-prompt-show":
|
|
for (let win of Services.wm.getEnumerator(null)) {
|
|
const prompt = win.prompts().find(item => item.id == subject.id);
|
|
if (prompt) {
|
|
this.callbacks.forEach(callback =>
|
|
callback(modal.ACTION_OPENED, prompt)
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add dialog handler by function reference.
|
|
*
|
|
* @param {Function} callback
|
|
* The handler to be added.
|
|
*/
|
|
add(callback) {
|
|
if (this.callbacks.has(callback)) {
|
|
return;
|
|
}
|
|
this.callbacks.add(callback);
|
|
}
|
|
|
|
/**
|
|
* Remove dialog handler by function reference.
|
|
*
|
|
* @param {Function} callback
|
|
* The handler to be removed.
|
|
*/
|
|
remove(callback) {
|
|
if (!this.callbacks.has(callback)) {
|
|
return;
|
|
}
|
|
this.callbacks.delete(callback);
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that waits for the dialog to be closed.
|
|
*/
|
|
async dialogClosed() {
|
|
return new Promise(resolve => {
|
|
const dialogClosed = (action, dialog) => {
|
|
if (action == modal.ACTION_CLOSED) {
|
|
this.remove(dialogClosed);
|
|
resolve();
|
|
}
|
|
};
|
|
|
|
this.add(dialogClosed);
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Represents a modal dialog.
|
|
*
|
|
* @param {function(): browser.Context} curBrowserFn
|
|
* Function that returns the current |browser.Context|.
|
|
* @param {DOMWindow} dialog
|
|
* DOMWindow of the dialog.
|
|
*/
|
|
modal.Dialog = class {
|
|
constructor(curBrowserFn, dialog) {
|
|
this.curBrowserFn_ = curBrowserFn;
|
|
this.win_ = Cu.getWeakReference(dialog);
|
|
}
|
|
|
|
get args() {
|
|
if (lazy.AppInfo.isAndroid) {
|
|
return this.window.args;
|
|
}
|
|
let tm = this.tabModal;
|
|
return tm ? tm.args : null;
|
|
}
|
|
|
|
get curBrowser_() {
|
|
return this.curBrowserFn_();
|
|
}
|
|
|
|
get isOpen() {
|
|
if (lazy.AppInfo.isAndroid) {
|
|
return this.window !== null;
|
|
}
|
|
if (!this.ui) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
get isWindowModal() {
|
|
return [
|
|
Services.prompt.MODAL_TYPE_WINDOW,
|
|
Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
|
|
].includes(this.args.modalType);
|
|
}
|
|
|
|
get tabModal() {
|
|
let win = this.window;
|
|
if (win) {
|
|
return win.Dialog;
|
|
}
|
|
return this.curBrowser_.getTabModal();
|
|
}
|
|
|
|
get text() {
|
|
if (lazy.AppInfo.isAndroid) {
|
|
return this.window.getPromptText();
|
|
}
|
|
return this.ui.infoBody.textContent;
|
|
}
|
|
|
|
get ui() {
|
|
let tm = this.tabModal;
|
|
return tm ? tm.ui : null;
|
|
}
|
|
|
|
/**
|
|
* For Android, this returns a GeckoViewPrompter, which can be used to control prompts.
|
|
* Otherwise, this returns the ChromeWindow associated with an open dialog window if
|
|
* it is currently attached to the DOM.
|
|
*/
|
|
get window() {
|
|
if (this.win_) {
|
|
let win = this.win_.get();
|
|
if (win && (lazy.AppInfo.isAndroid || win.parent)) {
|
|
return win;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
set text(inputText) {
|
|
if (lazy.AppInfo.isAndroid) {
|
|
this.window.setInputText(inputText);
|
|
} else {
|
|
// see toolkit/components/prompts/content/commonDialog.js
|
|
let { loginTextbox } = this.ui;
|
|
loginTextbox.value = inputText;
|
|
}
|
|
}
|
|
|
|
accept() {
|
|
if (lazy.AppInfo.isAndroid) {
|
|
// GeckoView does not have a UI, so the methods are called directly
|
|
this.window.acceptPrompt();
|
|
} else {
|
|
const { button0 } = this.ui;
|
|
button0.click();
|
|
}
|
|
}
|
|
|
|
dismiss() {
|
|
if (lazy.AppInfo.isAndroid) {
|
|
// GeckoView does not have a UI, so the methods are called directly
|
|
this.window.dismissPrompt();
|
|
} else {
|
|
const { button0, button1 } = this.ui;
|
|
(button1 ? button1 : button0).click();
|
|
}
|
|
}
|
|
};
|