206 lines
5.3 KiB
JavaScript
206 lines
5.3 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 { 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);
|
|
}
|
|
}
|