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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EnableDelayHelper: "resource://gre/modules/PromptUtils.sys.mjs",
});
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
export class CommonDialog {
constructor(args, ui) {
this.args = args;
this.ui = ui;
this.initialFocusPromise = new Promise(resolve => {
this.initialFocusResolver = resolve;
});
}
static DEFAULT_APP_ICON_CSS = `image-set(url("chrome://branding/content/icon16.png") 1x,
url("chrome://branding/content/icon32.png") 2x,
url("chrome://branding/content/icon64.png") 4x)`;
args = null;
ui = null;
hasInputField = true;
numButtons = undefined;
iconClass = undefined;
soundID = undefined;
focusTimer = null;
initialFocusPromise = null;
initialFocusResolver = null;
/**
* @param [commonDialogEl] - Dialog element from commonDialog.xhtml.
*/
async onLoad(commonDialogEl) {
let isEmbedded = !!commonDialogEl.ownerGlobal.docShell.chromeEventHandler;
switch (this.args.promptType) {
case "alert":
case "alertCheck":
this.hasInputField = false;
this.numButtons = 1;
this.iconClass = ["alert-icon"];
this.soundID = Ci.nsISound.EVENT_ALERT_DIALOG_OPEN;
break;
case "confirmCheck":
case "confirm":
this.hasInputField = false;
this.numButtons = 2;
this.iconClass = ["question-icon"];
this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
break;
case "confirmEx":
var numButtons = 0;
if (this.args.button0Label) {
numButtons++;
}
if (this.args.button1Label) {
numButtons++;
}
if (this.args.button2Label) {
numButtons++;
}
if (this.args.button3Label) {
numButtons++;
}
if (numButtons == 0 && !this.args.allowNoButtons) {
throw new Error(
"A dialog with no buttons requires the allowNoButtons argument"
);
}
this.numButtons = numButtons;
this.hasInputField = false;
this.iconClass = ["question-icon"];
this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
break;
case "prompt":
this.numButtons = 2;
this.iconClass = ["question-icon"];
this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
this.initTextbox("login", this.args.value);
// Clear the label, since this isn't really a username prompt.
this.ui.loginLabel.setAttribute("value", "");
// Ensure the labeling for the prompt is correct.
this.ui.loginTextbox.setAttribute("aria-labelledby", "infoBody");
break;
case "promptUserAndPass":
this.numButtons = 2;
this.iconClass = ["authentication-icon", "question-icon"];
this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
this.initTextbox("login", this.args.user);
this.initTextbox("password1", this.args.pass);
break;
case "promptPassword":
this.numButtons = 2;
this.iconClass = ["authentication-icon", "question-icon"];
this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
this.initTextbox("password1", this.args.pass);
// Clear the label, since the message presumably indicates its purpose.
this.ui.password1Label.setAttribute("value", "");
break;
default:
console.error(
"commonDialog opened for unknown type: ",
this.args.promptType
);
throw new Error("unknown dialog type");
}
commonDialogEl.setAttribute("windowtype", "prompt:" + this.args.promptType);
// set the document title
let title = this.args.title;
let infoTitle = this.ui.infoTitle;
infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title));
// After making these preventative checks, we can determine to show it if we're on
// macOS (where there is no titlebar) or if the prompt is a common dialog document
// and has been embedded (has a chromeEventHandler).
infoTitle.hidden = !(AppConstants.platform === "macosx" || isEmbedded);
commonDialogEl.ownerDocument.title = title;
// Set button labels and visibility
//
// This assumes that button0 defaults to a visible "ok" button, and
// button1 defaults to a visible "cancel" button. The other 2 buttons
// have no default labels (and are hidden).
switch (this.numButtons) {
case 4:
this.setLabelForNode(this.ui.button3, this.args.button3Label);
this.ui.button3.hidden = false;
// fall through
case 3:
this.setLabelForNode(this.ui.button2, this.args.button2Label);
this.ui.button2.hidden = false;
// fall through
case 2:
// Defaults to a visible "cancel" button
if (this.args.button1Label) {
this.setLabelForNode(this.ui.button1, this.args.button1Label);
}
break;
case 0:
this.ui.button0.hidden = true;
// fall through
case 1:
this.ui.button1.hidden = true;
break;
}
// Defaults to a visible "ok" button
if (this.args.button0Label) {
this.setLabelForNode(this.ui.button0, this.args.button0Label);
}
// display the main text
let croppedMessage = "";
if (this.args.text) {
// Bug 317334 - crop string length as a workaround.
croppedMessage = this.args.text.substr(0, 10000);
// TabModalPrompts don't have an infoRow to hide / not hide here, so
// guard on that here so long as they are in use.
if (this.ui.infoRow) {
this.ui.infoRow.hidden = false;
}
}
let infoBody = this.ui.infoBody;
infoBody.appendChild(infoBody.ownerDocument.createTextNode(croppedMessage));
let label = this.args.checkLabel;
if (label) {
// Only show the checkbox if label has a value.
this.ui.checkboxContainer.hidden = false;
this.ui.checkboxContainer.clientTop; // style flush to assure binding is attached
this.setLabelForNode(this.ui.checkbox, label);
this.ui.checkbox.checked = this.args.checked;
}
// set the icon
let icon = this.ui.infoIcon;
if (icon) {
this.iconClass.forEach(el => icon.classList.add(el));
}
// set default result to cancelled
this.args.ok = false;
this.args.buttonNumClicked = 1;
// Set the default button
let b = this.args.defaultButtonNum || 0;
commonDialogEl.defaultButton = ["accept", "cancel", "extra1", "extra2"][b];
if (!isEmbedded && !this.ui.promptContainer?.hidden) {
// Set default focus and select textbox contents if applicable. If we're
// embedded SubDialogManager will call setDefaultFocus for us.
this.setDefaultFocus(true);
}
if (this.args.enableDelay) {
this.delayHelper = new lazy.EnableDelayHelper({
disableDialog: () => this.setButtonsEnabledState(false),
enableDialog: () => this.setButtonsEnabledState(true),
focusTarget: this.ui.focusTarget,
});
}
// Play a sound (unless we're showing a content prompt -- don't want those
// to feel like OS prompts).
try {
if (this.soundID && !this.args.openedWithTabDialog) {
Cc["@mozilla.org/sound;1"]
.getService(Ci.nsISound)
.playEventSound(this.soundID);
}
} catch (e) {
console.error("Couldn't play common dialog event sound: ", e);
}
if (isEmbedded) {
// If we delayed default focus above, wait for it to be ready before
// sending the notification.
await this.initialFocusPromise;
}
Services.obs.notifyObservers(this.ui.prompt, "common-dialog-loaded");
}
setLabelForNode(aNode, aLabel) {
// This is for labels which may contain embedded access keys.
// If we end in (&X) where X represents the access key, optionally preceded
// by spaces and/or followed by the ':' character, store the access key and
// remove the access key placeholder + leading spaces from the label.
// Otherwise a character preceded by one but not two &s is the access key.
// Store it and remove the &.
// Note that if you change the following code, see the comment of
// nsTextBoxFrame::UpdateAccessTitle.
var accessKey = null;
if (/ *\(\&([^&])\)(:?)$/.test(aLabel)) {
aLabel = RegExp.leftContext + RegExp.$2;
accessKey = RegExp.$1;
} else if (/^([^&]*)\&(([^&]).*$)/.test(aLabel)) {
aLabel = RegExp.$1 + RegExp.$2;
accessKey = RegExp.$3;
}
// && is the magic sequence to embed an & in your label.
aLabel = aLabel.replace(/\&\&/g, "&");
aNode.label = aLabel;
// XXXjag bug 325251
// Need to set this after aNode.setAttribute("value", aLabel);
if (accessKey) {
aNode.accessKey = accessKey;
}
}
initTextbox(aName, aValue) {
this.ui[aName + "Container"].hidden = false;
this.ui[aName + "Textbox"].setAttribute(
"value",
aValue !== null ? aValue : ""
);
}
setButtonsEnabledState(enabled) {
this.ui.button0.disabled = !enabled;
// button1 (cancel) remains enabled.
this.ui.button2.disabled = !enabled;
this.ui.button3.disabled = !enabled;
}
setDefaultFocus(isInitialLoad) {
let b = this.args.defaultButtonNum || 0;
let button = this.ui["button" + b];
if (!this.hasInputField) {
let isOSX = "nsILocalFileMac" in Ci;
// If the infoRow exists and is is hidden, then the infoBody is also hidden,
// which means it can't be focused. At that point, we fall back to focusing
// the default button, regardless of platform.
if (isOSX && !(this.ui.infoRow && this.ui.infoRow.hidden)) {
this.ui.infoBody.focus();
} else {
button.focus({ focusVisible: false });
}
} else if (this.args.promptType == "promptPassword") {
// When the prompt is initialized, focus and select the textbox
// contents. Afterwards, only focus the textbox.
if (isInitialLoad) {
this.ui.password1Textbox.select();
} else {
this.ui.password1Textbox.focus();
}
} else if (isInitialLoad) {
this.ui.loginTextbox.select();
} else {
this.ui.loginTextbox.focus();
}
if (isInitialLoad) {
this.initialFocusResolver();
}
}
onCheckbox() {
this.args.checked = this.ui.checkbox.checked;
}
onButton0() {
this.args.promptActive = false;
this.args.ok = true;
this.args.buttonNumClicked = 0;
let username = this.ui.loginTextbox.value;
let password = this.ui.password1Textbox.value;
// Return textfield values
switch (this.args.promptType) {
case "prompt":
this.args.value = username;
break;
case "promptUserAndPass":
this.args.user = username;
this.args.pass = password;
break;
case "promptPassword":
this.args.pass = password;
break;
}
}
onButton1() {
this.args.promptActive = false;
this.args.buttonNumClicked = 1;
}
onButton2() {
this.args.promptActive = false;
this.args.buttonNumClicked = 2;
}
onButton3() {
this.args.promptActive = false;
this.args.buttonNumClicked = 3;
}
abortPrompt() {
this.args.promptActive = false;
this.args.promptAborted = true;
}
}