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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompter");
export class GeckoViewPrompter {
constructor(aParent) {
this.id = Services.uuid.generateUUID().toString().slice(1, -1); // Discard surrounding braces
if (aParent) {
if (Window.isInstance(aParent)) {
this._domWin = aParent;
} else if (aParent.window) {
this._domWin = aParent.window;
} else {
this._domWin =
aParent.embedderElement && aParent.embedderElement.ownerGlobal;
}
}
if (!this._domWin) {
this._domWin = Services.wm.getMostRecentWindow("navigator:geckoview");
}
this._innerWindowId =
this._domWin?.browsingContext.currentWindowContext.innerWindowId;
}
get domWin() {
return this._domWin;
}
get prompterActor() {
const actor = this.domWin?.windowGlobalChild.getActor("GeckoViewPrompter");
return actor;
}
_changeModalState(aEntering) {
if (!this._domWin) {
// Allow not having a DOM window.
return true;
}
// Accessing the document object can throw if this window no longer exists. See bug 789888.
try {
const winUtils = this._domWin.windowUtils;
if (!aEntering) {
winUtils.leaveModalState();
}
const event = this._domWin.document.createEvent("Events");
event.initEvent(
aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed",
true,
true
);
winUtils.dispatchEventToChromeOnly(this._domWin, event);
if (aEntering) {
winUtils.enterModalState();
}
return true;
} catch (ex) {
console.error("Failed to change modal state:", ex);
}
return false;
}
_dismissUi() {
this.prompterActor?.dismissPrompt(this);
}
accept(aInputText = this.inputText) {
if (this.callback) {
let acceptMsg = {};
switch (this.message.type) {
case "alert":
acceptMsg = null;
break;
case "button":
acceptMsg.button = 0;
break;
case "text":
acceptMsg.text = aInputText;
break;
default:
acceptMsg = null;
break;
}
this.callback(acceptMsg);
// Notify the UI that this prompt should be hidden.
this._dismissUi();
}
}
dismiss() {
this.callback(null);
// Notify the UI that this prompt should be hidden.
this._dismissUi();
}
getPromptType() {
switch (this.message.type) {
case "alert":
return this.message.checkValue ? "alertCheck" : "alert";
case "button":
return this.message.checkValue ? "confirmCheck" : "confirm";
case "text":
return this.message.checkValue ? "promptCheck" : "prompt";
default:
return this.message.type;
}
}
getPromptText() {
return this.message.msg;
}
getInputText() {
return this.inputText;
}
setInputText(aInput) {
this.inputText = aInput;
}
/**
* Shows a native prompt, and then spins the event loop for this thread while we wait
* for a response
*/
showPrompt(aMsg) {
let result = undefined;
if (!this._domWin || !this._changeModalState(/* aEntering */ true)) {
return result;
}
try {
this.asyncShowPrompt(aMsg, res => (result = res));
// Spin this thread while we wait for a result
Services.tm.spinEventLoopUntil(
"GeckoViewPrompter.sys.mjs:showPrompt",
() => this._domWin.closed || result !== undefined
);
} finally {
this._changeModalState(/* aEntering */ false);
}
return result;
}
checkInnerWindow() {
// Checks that the innerWindow where this prompt was created still matches
// the current innerWindow.
// This checks will fail if the page navigates away, making this prompt
// obsolete.
return (
this._innerWindowId ===
this._domWin.browsingContext.currentWindowContext.innerWindowId
);
}
asyncShowPromptPromise(aMsg) {
return new Promise(resolve => {
this.asyncShowPrompt(aMsg, resolve);
});
}
async asyncShowPrompt(aMsg, aCallback) {
this.message = aMsg;
this.inputText = aMsg.value;
this.callback = aCallback;
aMsg.id = this.id;
let response = null;
try {
if (this.checkInnerWindow()) {
response = await this.prompterActor.prompt(this, aMsg);
}
} catch (error) {
// Nothing we can do really, we will treat this as a dismiss.
warn`Error while prompting: ${error}`;
}
if (!this.checkInnerWindow()) {
// Page has navigated away, let's dismiss the prompt
aCallback(null);
} else {
aCallback(response);
}
// This callback object is tied to the Java garbage collector because
// it is invoked from Java. Manually release the target callback
// here; otherwise we may hold onto resources for too long, because
// we would be relying on both the Java and the JS garbage collectors
// to run.
aMsg = undefined;
aCallback = undefined;
}
update(aMsg) {
this.message = aMsg;
aMsg.id = this.id;
this.prompterActor?.updatePrompt(aMsg);
}
}