Source code
Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
modal: "chrome://remote/content/shared/Prompt.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
/**
* The PromptListener listens to the DialogObserver events.
*
* Example:
* ```
* const listener = new PromptListener();
* listener.on("opened", onPromptOpened);
* listener.startListening();
*
* const onPromptOpened = (eventName, data = {}) => {
* const { contentBrowser, prompt } = data;
* ...
* };
* ```
*
* @fires message
* The PromptListener emits "opened" events,
* with the following object as payload:
* - {XULBrowser} contentBrowser
* The <xul:browser> which hold the <var>prompt</var>.
* - {modal.Dialog} prompt
* Returns instance of the Dialog class.
*
* The PromptListener emits "closed" events,
* with the following object as payload:
* - {XULBrowser} contentBrowser
* The <xul:browser> which is the target of the event.
* - {object} detail
* {boolean=} detail.accepted
* Returns true if a user prompt was accepted
* and false if it was dismissed.
* {string=} detail.userText
* The user text specified in a prompt.
*/
export class PromptListener {
#curBrowserFn;
#listening;
constructor(curBrowserFn) {
lazy.EventEmitter.decorate(this);
// curBrowserFn is used only for Marionette (WebDriver classic).
this.#curBrowserFn = curBrowserFn;
this.#listening = false;
}
destroy() {
this.stopListening();
}
/**
* Waits for the prompt to be closed.
*
* @returns {Promise}
* Promise that resolves when the prompt is closed.
*/
async dialogClosed() {
return new Promise(resolve => {
const dialogClosed = () => {
this.off("closed", dialogClosed);
resolve();
};
this.on("closed", dialogClosed);
});
}
/**
* Handles `DOMModalDialogClosed` events.
*/
handleEvent(event) {
const chromeWin = event.target.opener
? event.target.opener.ownerGlobal
: event.target.ownerGlobal;
const curBrowser = this.#curBrowserFn && this.#curBrowserFn();
// For Marionette (WebDriver classic) we only care about events which come
// the currently selected browser.
if (curBrowser && chromeWin != curBrowser.window) {
return;
}
let contentBrowser;
if (lazy.AppInfo.isAndroid) {
const tabBrowser = lazy.TabManager.getTabBrowser(event.target);
// Since on Android we always have only one tab we can just check
// the selected tab.
const tab = tabBrowser.selectedTab;
contentBrowser = lazy.TabManager.getBrowserForTab(tab);
} else {
contentBrowser = event.target;
}
const detail = {};
// At the moment the event details are present for GeckoView and on desktop
// only for Services.prompt.MODAL_TYPE_CONTENT prompts.
if (event.detail) {
const { areLeaving, owningBrowsingContext, promptType, value } =
event.detail;
// `areLeaving` returns undefined for alerts, for confirms and prompts
// it returns true if a user prompt was accepted and false if it was dismissed.
detail.accepted = areLeaving === undefined ? true : areLeaving;
detail.promptType = promptType;
if (value) {
detail.userText = value;
}
detail.browsingContext = owningBrowsingContext;
}
this.emit("closed", {
contentBrowser,
detail,
});
}
/**
* Observes the following notifications:
* `common-dialog-loaded` - when a modal dialog loaded on desktop,
* `domwindowopened` - when a new chrome window opened,
* `geckoview-prompt-show` - when a modal dialog opened on Android.
*/
async observe(subject, topic) {
let curBrowser = this.#curBrowserFn && this.#curBrowserFn();
switch (topic) {
case "common-dialog-loaded": {
const browsingContext = subject.args.owningBrowsingContext;
if (curBrowser) {
if (
!this.#hasCommonDialog(
curBrowser.contentBrowser,
curBrowser.window,
subject
)
) {
return;
}
} else {
curBrowser = { contentBrowser: browsingContext.embedderElement };
}
this.emit(
"opened",
await this.#getOpenedEventDetail(
browsingContext,
curBrowser.contentBrowser,
subject
)
);
break;
}
case "domwindowopened": {
subject.addEventListener("DOMModalDialogClosed", this);
break;
}
case "geckoview-prompt-show": {
for (let win of Services.wm.getEnumerator(null)) {
const subjectObject = subject.wrappedJSObject;
const prompt = win
.prompts()
.find(item => item.getPromptId() == subjectObject.id);
if (prompt) {
const tabBrowser = lazy.TabManager.getTabBrowser(win);
// Since on Android we always have only one tab we can just check
// the selected tab.
const tab = tabBrowser.selectedTab;
const contentBrowser = lazy.TabManager.getBrowserForTab(tab);
// Do not send the event if the curBrowser is specified,
// and it's different from prompt browser.
if (curBrowser && contentBrowser !== curBrowser.contentBrowser) {
continue;
}
this.emit(
"opened",
await this.#getOpenedEventDetail(
subjectObject.owningBrowsingContext,
contentBrowser,
prompt
)
);
return;
}
}
break;
}
}
}
startListening() {
if (this.#listening) {
return;
}
this.#register();
this.#listening = true;
}
stopListening() {
if (!this.#listening) {
return;
}
this.#unregister();
this.#listening = false;
}
async #getOpenedEventDetail(browsingContext, contentBrowser, dialog) {
const prompt = new lazy.modal.Dialog(dialog);
return {
browsingContext,
contentBrowser,
prompt,
// Resolve prompt details here to avoid sending an open event
// with the data that is resolved after a prompt is handled.
promptDetails: {
defaultValue:
prompt.promptType === "prompt" ? await prompt.getInputText() : null,
message: await prompt.getText(),
},
};
}
#hasCommonDialog(contentBrowser, window, prompt) {
const modalType = prompt.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 content browser.
const container = contentBrowser.closest(".browserSidebarContainer");
return container.contains(prompt.docShell.chromeEventHandler);
}
return prompt.ownerGlobal == window || prompt.opener?.ownerGlobal == window;
}
#register() {
for (const observerName of [
"common-dialog-loaded",
"domwindowopened",
"geckoview-prompt-show",
]) {
Services.obs.addObserver(this, observerName);
}
// Register event listener and save already open prompts for all already open windows.
for (const win of Services.wm.getEnumerator(null)) {
win.addEventListener("DOMModalDialogClosed", this);
}
}
#unregister() {
[
"common-dialog-loaded",
"domwindowopened",
"geckoview-prompt-show",
].forEach(observerName => {
try {
Services.obs.removeObserver(this, observerName);
} catch (e) {
lazy.logger.debug(
`${this.constructor.name}: Failed to remove observer "${observerName}"`
);
}
});
// Unregister event listener for all open windows
for (const win of Services.wm.getEnumerator(null)) {
win.removeEventListener("DOMModalDialogClosed", this);
}
}
}