fune/toolkit/content/widgets/dialog.js
Mihai Alexandru Michis 2dab66d06e Backed out 9 changesets (bug 1607172, bug 1609998, bug 1608799) for causing xpcshell failures in test_parser.js
CLOSED TREE

Backed out changeset 7753083b67dd (bug 1609998)
Backed out changeset e6f5aac734ab (bug 1608799)
Backed out changeset 90ea35966b73 (bug 1608799)
Backed out changeset 6fafa451b3f9 (bug 1608799)
Backed out changeset e07a4aea2ae1 (bug 1608799)
Backed out changeset d69d6dfdccad (bug 1607172)
Backed out changeset 93023b1b6153 (bug 1607172)
Backed out changeset 99ce7a56080e (bug 1607172)
Backed out changeset 20aa5934c785 (bug 1607172)
2020-01-21 21:44:03 +02:00

566 lines
17 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/. */
"use strict";
// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
class MozDialog extends MozXULElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.addEventListener(
"keypress",
event => {
if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
this._hitEnter(event);
}
},
{ mozSystemGroup: true }
);
this.addEventListener(
"keypress",
event => {
if (
event.keyCode == KeyEvent.DOM_VK_ESCAPE &&
!event.defaultPrevented
) {
this.cancelDialog();
}
},
{ mozSystemGroup: true }
);
if (AppConstants.platform == "macosx") {
this.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();
}
});
// for things that we need to initialize after onload fires
window.addEventListener("load", event => this.postLoadInit(event));
}
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 `
<html:link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
`;
}
get _markup() {
let buttons = AppConstants.XP_UNIX
? `
<hbox class="dialog-button-box">
<button dlgtype="disclosure" class="dialog-button" hidden="true"/>
<button dlgtype="help" class="dialog-button" hidden="true"/>
<button dlgtype="extra2" class="dialog-button" hidden="true"/>
<button dlgtype="extra1" class="dialog-button" hidden="true"/>
<spacer class="spacer" flex="1"/>
<button dlgtype="cancel" class="dialog-button"/>
<button dlgtype="accept" class="dialog-button"/>
</hbox>`
: `
<hbox class="dialog-button-box" pack="end">
<button dlgtype="extra2" class="dialog-button" hidden="true"/>
<spacer class="spacer" flex="1" hidden="true"/>
<button dlgtype="accept" class="dialog-button"/>
<button dlgtype="extra1" class="dialog-button" hidden="true"/>
<button dlgtype="cancel" class="dialog-button"/>
<button dlgtype="help" class="dialog-button" hidden="true"/>
<button dlgtype="disclosure" class="dialog-button" hidden="true"/>
</hbox>`;
let key =
AppConstants.platform == "macosx"
? `<key phase="capturing"
oncommand="document.querySelector('dialog').openHelp(event)"
key="&openHelpMac.commandkey;" modifiers="accel"/>`
: `<key phase="capturing"
oncommand="document.querySelector('dialog').openHelp(event)"
keycode="&openHelp.commandkey;"/>`;
return `
<html:link rel="stylesheet" href="chrome://global/content/widgets.css" />
${this.hasAttribute("subdialog") ? this.inContentStyle : ""}
<html:style>
:host([nobuttonspacer]) .spacer {
display: none;
}
:host([subdialog]) > .dialog-content-box {
/* This allows the focus ring to display fully when scrolling is enabled.
See matching style in dialog.inc.css.
*/
padding: 4px;
}
:host(.doScroll) > .dialog-content-box {
overflow-y: auto;
}
</html:style>
<vbox class="box-inherit dialog-content-box" flex="1">
<html:slot></html:slot>
</vbox>
${buttons}
<keyset>${key}</keyset>`;
}
connectedCallback() {
if (this.delayConnectedCallback()) {
return;
}
document.documentElement.setAttribute("role", "dialog");
this.shadowRoot.textContent = "";
this.shadowRoot.appendChild(
MozXULElement.parseXULToFragment(this._markup, [
"chrome://global/locale/globalKeys.dtd",
])
);
this.initializeAttributeInheritance();
/**
* Gets populated by elements that are passed to document.l10n.setAttributes
* to localize the dialog buttons. Needed to properly size the dialog after
* the asynchronous translation.
*/
this._l10nButtons = [];
this._configureButtons(this.buttons);
window.moveToAlertPosition = this.moveToAlertPosition;
window.centerWindowOnScreen = this.centerWindowOnScreen;
}
set buttons(val) {
this._configureButtons(val);
return val;
}
get buttons() {
return this.getAttribute("buttons");
}
set defaultButton(val) {
this._setDefaultButton(val);
return 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");
}
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"
);
sizeToContent();
}
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);
}
postLoadInit(aEvent) {
let focusInit = () => {
const defaultButton = this.getButton(this.defaultButton);
// give focus to the first focusable element in the dialog
let focusedElt = document.commandDispatcher.focusedElement;
if (!focusedElt) {
document.commandDispatcher.advanceFocusIntoSubtree(this);
focusedElt = document.commandDispatcher.focusedElement;
if (focusedElt) {
var initialFocusedElt = focusedElt;
while (
focusedElt.localName == "tab" ||
focusedElt.getAttribute("noinitialfocus") == "true"
) {
document.commandDispatcher.advanceFocusIntoSubtree(focusedElt);
focusedElt = document.commandDispatcher.focusedElement;
if (focusedElt) {
if (focusedElt == initialFocusedElt) {
if (focusedElt.getAttribute("noinitialfocus") == "true") {
focusedElt.blur();
}
break;
}
}
}
if (initialFocusedElt.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
initialFocusedElt.focus();
}
} else if (
AppConstants.platform != "macosx" &&
focusedElt.hasAttribute("dlgtype") &&
focusedElt != defaultButton
) {
defaultButton.focus();
}
}
}
try {
if (defaultButton) {
window.notifyDefaultButtonLoaded(defaultButton);
}
} catch (e) {}
};
// Give focus after onload completes, see bug 103197.
setTimeout(focusInit, 0);
if (this._l10nButtons.length) {
document.l10n.translateElements(this._l10nButtons).then(() => {
window.sizeToContent();
});
}
}
openHelp(event) {
var helpButton = this.getButton("help");
if (helpButton.disabled || helpButton.hidden) {
return;
}
this._fireButtonEvent("help");
event.stopPropagation();
event.preventDefault();
}
_configureButtons(aButtons) {
// by default, get all the anonymous button elements
var buttons = {};
this._buttons = buttons;
for (let type of [
"accept",
"cancel",
"extra1",
"extra2",
"help",
"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)
);
this._l10nButtons.push(button);
} 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);
}
}
}
// allow specifying alternate icons in the dialog header
if (!button.hasAttribute("icon")) {
// if there's an icon specified, use that
if (this.hasAttribute("buttonicon" + dlgtype)) {
button.setAttribute(
"icon",
this.getAttribute("buttonicon" + dlgtype)
);
}
// otherwise set defaults
else {
switch (dlgtype) {
case "accept":
button.setAttribute("icon", "accept");
break;
case "cancel":
button.setAttribute("icon", "cancel");
break;
case "disclosure":
button.setAttribute("icon", "properties");
break;
case "help":
button.setAttribute("icon", "help");
break;
default:
break;
}
}
}
}
// ensure that hitting enter triggers the default button command
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,
help: 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(".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) {
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);
}