summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/dialog.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/dialog.js')
-rw-r--r--toolkit/content/widgets/dialog.js517
1 files changed, 517 insertions, 0 deletions
diff --git a/toolkit/content/widgets/dialog.js b/toolkit/content/widgets/dialog.js
new file mode 100644
index 0000000000..e6d70934eb
--- /dev/null
+++ b/toolkit/content/widgets/dialog.js
@@ -0,0 +1,517 @@
+/* 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" });
+
+ 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();
+ }
+ });
+
+ // 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" hidden="true"/>
+ <button dlgtype="help" hidden="true"/>
+ <button dlgtype="extra2" hidden="true"/>
+ <button dlgtype="extra1" hidden="true"/>
+ <spacer class="button-spacer" part="button-spacer" flex="1"/>
+ <button dlgtype="cancel"/>
+ <button dlgtype="accept"/>
+ </hbox>`
+ : `
+ <hbox class="dialog-button-box" pack="end">
+ <button dlgtype="extra2" hidden="true"/>
+ <spacer class="button-spacer" part="button-spacer" flex="1" hidden="true"/>
+ <button dlgtype="accept"/>
+ <button dlgtype="extra1" hidden="true"/>
+ <button dlgtype="cancel"/>
+ <button dlgtype="help" hidden="true"/>
+ <button dlgtype="disclosure" 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/skin/button.css"/>
+ <html:link rel="stylesheet" href="chrome://global/skin/dialog.css"/>
+ ${this.hasAttribute("subdialog") ? this.inContentStyle : ""}
+ <vbox class="box-inherit dialog-content-box" part="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);
+ }
+ }
+ }
+ }
+
+ // 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,
+ 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(".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) {
+ 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);
+}