/* 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/. */
"use strict";
// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
class MozDialog extends MozXULElement {
constructor() {
super();
}
static get observedAttributes() {
return super.observedAttributes.concat("subdialog");
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == "subdialog") {
console.assert(
newValue,
`Turning off subdialog style is not supported`
);
if (this.isConnectedAndReady && !oldValue && newValue) {
this.shadowRoot.appendChild(
MozXULElement.parseXULToFragment(this.inContentStyle)
);
}
return;
}
super.attributeChangedCallback(name, oldValue, newValue);
}
static get inheritedAttributes() {
return {
".dialog-button-box":
"pack=buttonpack,align=buttonalign,dir=buttondir,orient=buttonorient",
"[dlgtype='accept']": "disabled=buttondisabledaccept",
};
}
get inContentStyle() {
return `
`;
}
get _markup() {
let buttons = AppConstants.XP_UNIX
? `
`
: `
`;
return `
${this.hasAttribute("subdialog") ? this.inContentStyle : ""}
${buttons}`;
}
connectedCallback() {
if (this.delayConnectedCallback()) {
return;
}
if (this.hasConnected) {
return;
}
this.hasConnected = true;
this.attachShadow({ mode: "open" });
document.documentElement.setAttribute("role", "dialog");
document.l10n?.connectRoot(this.shadowRoot);
this.shadowRoot.textContent = "";
this.shadowRoot.appendChild(
MozXULElement.parseXULToFragment(this._markup)
);
this.initializeAttributeInheritance();
this._configureButtons(this.buttons);
window.moveToAlertPosition = this.moveToAlertPosition;
window.centerWindowOnScreen = this.centerWindowOnScreen;
document.addEventListener(
"keypress",
event => {
if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
this._hitEnter(event);
} else if (
event.keyCode == KeyEvent.DOM_VK_ESCAPE &&
!event.defaultPrevented
) {
this.cancelDialog();
}
},
{ mozSystemGroup: true }
);
if (AppConstants.platform == "macosx") {
document.addEventListener(
"keypress",
event => {
if (event.key == "." && event.metaKey) {
this.cancelDialog();
}
},
true
);
} else {
this.addEventListener("focus", this, true);
this.shadowRoot.addEventListener("focus", this, true);
}
// listen for when window is closed via native close buttons
window.addEventListener("close", event => {
if (!this.cancelDialog()) {
event.preventDefault();
}
});
// Call postLoadInit for things that we need to initialize after onload.
if (document.readyState == "complete") {
this._postLoadInit();
} else {
window.addEventListener("load", () => this._postLoadInit());
}
}
set buttons(val) {
this._configureButtons(val);
}
get buttons() {
return this.getAttribute("buttons");
}
set defaultButton(val) {
this._setDefaultButton(val);
}
get defaultButton() {
if (this.hasAttribute("defaultButton")) {
return this.getAttribute("defaultButton");
}
return "accept"; // default to the accept button
}
get _strBundle() {
if (!this.__stringBundle) {
this.__stringBundle = Services.strings.createBundle(
"chrome://global/locale/dialog.properties"
);
}
return this.__stringBundle;
}
acceptDialog() {
return this._doButtonCommand("accept");
}
cancelDialog() {
return this._doButtonCommand("cancel");
}
getButton(aDlgType) {
return this._buttons[aDlgType];
}
get buttonBox() {
return this.shadowRoot.querySelector(".dialog-button-box");
}
// NOTE(emilio): This has to match AppWindow::IntrinsicallySizeShell, to
// prevent flickering, see bug 1799394.
_sizeToPreferredSize() {
const docEl = document.documentElement;
const prefWidth = (() => {
if (docEl.hasAttribute("width")) {
return parseInt(docEl.getAttribute("width"));
}
let prefWidthProp = docEl.getAttribute("prefwidth");
if (prefWidthProp) {
let minWidth = parseFloat(
getComputedStyle(docEl).getPropertyValue(prefWidthProp)
);
if (isFinite(minWidth)) {
return minWidth;
}
}
return 0;
})();
window.sizeToContentConstrained({ prefWidth });
}
moveToAlertPosition() {
// hack. we need this so the window has something like its final size
if (window.outerWidth == 1) {
dump(
"Trying to position a sizeless window; caller should have called sizeToContent() or sizeTo(). See bug 75649.\n"
);
this._sizeToPreferredSize();
}
if (opener) {
var xOffset = (opener.outerWidth - window.outerWidth) / 2;
var yOffset = opener.outerHeight / 5;
var newX = opener.screenX + xOffset;
var newY = opener.screenY + yOffset;
} else {
newX = (screen.availWidth - window.outerWidth) / 2;
newY = (screen.availHeight - window.outerHeight) / 2;
}
// ensure the window is fully onscreen (if smaller than the screen)
if (newX < screen.availLeft) {
newX = screen.availLeft + 20;
}
if (newX + window.outerWidth > screen.availLeft + screen.availWidth) {
newX = screen.availLeft + screen.availWidth - window.outerWidth - 20;
}
if (newY < screen.availTop) {
newY = screen.availTop + 20;
}
if (newY + window.outerHeight > screen.availTop + screen.availHeight) {
newY = screen.availTop + screen.availHeight - window.outerHeight - 60;
}
window.moveTo(newX, newY);
}
centerWindowOnScreen() {
var xOffset = screen.availWidth / 2 - window.outerWidth / 2;
var yOffset = screen.availHeight / 2 - window.outerHeight / 2;
xOffset = xOffset > 0 ? xOffset : 0;
yOffset = yOffset > 0 ? yOffset : 0;
window.moveTo(xOffset, yOffset);
}
// Give focus to the first focusable element in the dialog
_setInitialFocusIfNeeded() {
let focusedElt = document.commandDispatcher.focusedElement;
if (focusedElt) {
return;
}
const defaultButton = this.getButton(this.defaultButton);
Services.focus.moveFocus(
window,
null,
Services.focus.MOVEFOCUS_FORWARD,
Services.focus.FLAG_NOPARENTFRAME
);
focusedElt = document.commandDispatcher.focusedElement;
if (!focusedElt) {
return; // No focusable element?
}
let firstFocusedElt = focusedElt;
while (
focusedElt.localName == "tab" ||
focusedElt.getAttribute("noinitialfocus") == "true"
) {
Services.focus.moveFocus(
window,
focusedElt,
Services.focus.MOVEFOCUS_FORWARD,
Services.focus.FLAG_NOPARENTFRAME
);
focusedElt = document.commandDispatcher.focusedElement;
if (focusedElt == firstFocusedElt) {
if (focusedElt.getAttribute("noinitialfocus") == "true") {
focusedElt.blur();
}
// Didn't find anything else to focus, we're done.
return;
}
}
if (firstFocusedElt.localName == "tab") {
if (focusedElt.hasAttribute("dlgtype")) {
// We don't want to focus on anonymous OK, Cancel, etc. buttons,
// so return focus to the tab itself
firstFocusedElt.focus();
}
} else if (
AppConstants.platform != "macosx" &&
focusedElt.hasAttribute("dlgtype") &&
focusedElt != defaultButton
) {
defaultButton.focus();
if (document.commandDispatcher.focusedElement != defaultButton) {
// If the default button is not focusable, then return focus to the
// initial element if possible, or blur otherwise.
if (firstFocusedElt.getAttribute("noinitialfocus") == "true") {
focusedElt.blur();
} else {
firstFocusedElt.focus();
}
}
}
}
async _postLoadInit() {
this._setInitialFocusIfNeeded();
let finalStep = () => {
this._sizeToPreferredSize();
this._snapCursorToDefaultButtonIfNeeded();
};
// As a hack to ensure Windows sizes the window correctly,
// _sizeToPreferredSize() needs to happen after
// AppWindow::OnChromeLoaded. That one is called right after the load
// event dispatch but within the same task. Using direct dispatch let's
// all this code run before the next task (which might be a task to
// paint the window).
// But, MacOS doesn't like resizing after window/dialog becoming visible.
// Linux seems to be able to handle both cases.
if (Services.appinfo.OS == "Darwin") {
finalStep();
} else {
Services.tm.dispatchDirectTaskToCurrentThread(finalStep);
}
}
// This snaps the cursor to the default button rect on windows, when
// SPI_GETSNAPTODEFBUTTON is set.
async _snapCursorToDefaultButtonIfNeeded() {
const defaultButton = this.getButton(this.defaultButton);
if (!defaultButton) {
return;
}
try {
// FIXME(emilio, bug 1797624): This setTimeout() ensures enough time
// has passed so that the dialog vertical margin has been set by the
// front-end. For subdialogs, cursor positioning should probably be
// done by the opener instead, once the dialog is positioned.
await new Promise(r => setTimeout(r, 0));
await window.promiseDocumentFlushed(() => {});
window.notifyDefaultButtonLoaded(defaultButton);
} catch (e) {}
}
_configureButtons(aButtons) {
// by default, get all the anonymous button elements
var buttons = {};
this._buttons = buttons;
for (let type of ["accept", "cancel", "extra1", "extra2", "disclosure"]) {
buttons[type] = this.shadowRoot.querySelector(`[dlgtype="${type}"]`);
}
// look for any overriding explicit button elements
var exBtns = this.getElementsByAttribute("dlgtype", "*");
var dlgtype;
for (let i = 0; i < exBtns.length; ++i) {
dlgtype = exBtns[i].getAttribute("dlgtype");
buttons[dlgtype].hidden = true; // hide the anonymous button
buttons[dlgtype] = exBtns[i];
}
// add the label and oncommand handler to each button
for (dlgtype in buttons) {
var button = buttons[dlgtype];
button.addEventListener(
"command",
this._handleButtonCommand.bind(this),
true
);
// don't override custom labels with pre-defined labels on explicit buttons
if (!button.hasAttribute("label")) {
// dialog attributes override the default labels in dialog.properties
if (this.hasAttribute("buttonlabel" + dlgtype)) {
button.setAttribute(
"label",
this.getAttribute("buttonlabel" + dlgtype)
);
if (this.hasAttribute("buttonaccesskey" + dlgtype)) {
button.setAttribute(
"accesskey",
this.getAttribute("buttonaccesskey" + dlgtype)
);
}
} else if (this.hasAttribute("buttonid" + dlgtype)) {
document.l10n.setAttributes(
button,
this.getAttribute("buttonid" + dlgtype)
);
} else if (dlgtype != "extra1" && dlgtype != "extra2") {
button.setAttribute(
"label",
this._strBundle.GetStringFromName("button-" + dlgtype)
);
var accessKey = this._strBundle.GetStringFromName(
"accesskey-" + dlgtype
);
if (accessKey) {
button.setAttribute("accesskey", accessKey);
}
}
}
}
// ensure that hitting enter triggers the default button command
// eslint-disable-next-line no-self-assign
this.defaultButton = this.defaultButton;
// if there is a special button configuration, use it
if (aButtons) {
// expect a comma delimited list of dlgtype values
var list = aButtons.split(",");
// mark shown dlgtypes as true
var shown = {
accept: false,
cancel: false,
disclosure: false,
extra1: false,
extra2: false,
};
for (let i = 0; i < list.length; ++i) {
shown[list[i].replace(/ /g, "")] = true;
}
// hide/show the buttons we want
for (dlgtype in buttons) {
buttons[dlgtype].hidden = !shown[dlgtype];
}
// show the spacer on Windows only when the extra2 button is present
if (AppConstants.platform == "win") {
let spacer = this.shadowRoot.querySelector(".button-spacer");
spacer.removeAttribute("hidden");
spacer.setAttribute("flex", shown.extra2 ? "1" : "0");
}
}
}
_setDefaultButton(aNewDefault) {
// remove the default attribute from the previous default button, if any
var oldDefaultButton = this.getButton(this.defaultButton);
if (oldDefaultButton) {
oldDefaultButton.removeAttribute("default");
}
var newDefaultButton = this.getButton(aNewDefault);
if (newDefaultButton) {
this.setAttribute("defaultButton", aNewDefault);
newDefaultButton.setAttribute("default", "true");
} else {
this.setAttribute("defaultButton", "none");
if (aNewDefault != "none") {
dump(
"invalid new default button: " + aNewDefault + ", assuming: none\n"
);
}
}
}
_handleButtonCommand(aEvent) {
return this._doButtonCommand(aEvent.target.getAttribute("dlgtype"));
}
_doButtonCommand(aDlgType) {
var button = this.getButton(aDlgType);
if (!button.disabled) {
var noCancel = this._fireButtonEvent(aDlgType);
if (noCancel) {
if (aDlgType == "accept" || aDlgType == "cancel") {
var closingEvent = new CustomEvent("dialogclosing", {
bubbles: true,
detail: { button: aDlgType },
});
this.dispatchEvent(closingEvent);
window.close();
}
}
return noCancel;
}
return true;
}
_fireButtonEvent(aDlgType) {
var event = document.createEvent("Events");
event.initEvent("dialog" + aDlgType, true, true);
// handle dom event handlers
return this.dispatchEvent(event);
}
_hitEnter(evt) {
if (evt.defaultPrevented) {
return;
}
var btn = this.getButton(this.defaultButton);
if (btn && !btn.hidden) {
this._doButtonCommand(this.defaultButton);
}
}
on_focus(event) {
let btn = this.getButton(this.defaultButton);
if (btn) {
btn.setAttribute(
"default",
event.originalTarget == btn ||
!(
event.originalTarget.localName == "button" ||
event.originalTarget.localName == "toolbarbutton"
)
);
}
}
}
customElements.define("dialog", MozDialog);
}