1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 // This is loaded into all XUL windows. Wrap in a block to prevent
8 // leaking to window scope.
10 const { AppConstants } = ChromeUtils.importESModule(
11 "resource://gre/modules/AppConstants.sys.mjs"
14 class MozDialog extends MozXULElement {
19 static get observedAttributes() {
20 return super.observedAttributes.concat("subdialog");
23 attributeChangedCallback(name, oldValue, newValue) {
24 if (name == "subdialog") {
27 `Turning off subdialog style is not supported`
29 if (this.isConnectedAndReady && !oldValue && newValue) {
30 this.shadowRoot.appendChild(
31 MozXULElement.parseXULToFragment(this.inContentStyle)
36 super.attributeChangedCallback(name, oldValue, newValue);
39 static get inheritedAttributes() {
42 "pack=buttonpack,align=buttonalign,dir=buttondir,orient=buttonorient",
43 "[dlgtype='accept']": "disabled=buttondisabledaccept",
47 get inContentStyle() {
49 <html:link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
54 let buttons = AppConstants.XP_UNIX
56 <hbox class="dialog-button-box">
57 <button dlgtype="disclosure" hidden="true"/>
58 <button dlgtype="extra2" hidden="true"/>
59 <button dlgtype="extra1" hidden="true"/>
60 <spacer class="button-spacer" part="button-spacer" flex="1"/>
61 <button dlgtype="cancel"/>
62 <button dlgtype="accept"/>
65 <hbox class="dialog-button-box" pack="end">
66 <button dlgtype="extra2" hidden="true"/>
67 <spacer class="button-spacer" part="button-spacer" flex="1" hidden="true"/>
68 <button dlgtype="accept"/>
69 <button dlgtype="extra1" hidden="true"/>
70 <button dlgtype="cancel"/>
71 <button dlgtype="disclosure" hidden="true"/>
75 <html:link rel="stylesheet" href="chrome://global/skin/button.css"/>
76 <html:link rel="stylesheet" href="chrome://global/skin/dialog.css"/>
77 ${this.hasAttribute("subdialog") ? this.inContentStyle : ""}
78 <vbox class="box-inherit" part="content-box">
79 <html:slot></html:slot>
85 if (this.delayConnectedCallback()) {
88 if (this.hasConnected) {
91 this.hasConnected = true;
92 this.attachShadow({ mode: "open" });
94 document.documentElement.setAttribute("role", "dialog");
95 document.l10n?.connectRoot(this.shadowRoot);
97 this.shadowRoot.textContent = "";
98 this.shadowRoot.appendChild(
99 MozXULElement.parseXULToFragment(this._markup)
101 this.initializeAttributeInheritance();
103 this._configureButtons(this.buttons);
105 window.moveToAlertPosition = this.moveToAlertPosition;
106 window.centerWindowOnScreen = this.centerWindowOnScreen;
108 document.addEventListener(
111 if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
112 this._hitEnter(event);
114 event.keyCode == KeyEvent.DOM_VK_ESCAPE &&
115 !event.defaultPrevented
120 { mozSystemGroup: true }
123 if (AppConstants.platform == "macosx") {
124 document.addEventListener(
127 if (event.key == "." && event.metaKey) {
134 this.addEventListener("focus", this, true);
135 this.shadowRoot.addEventListener("focus", this, true);
138 // listen for when window is closed via native close buttons
139 window.addEventListener("close", event => {
140 if (!this.cancelDialog()) {
141 event.preventDefault();
145 // Call postLoadInit for things that we need to initialize after onload.
146 if (document.readyState == "complete") {
147 this._postLoadInit();
149 window.addEventListener("load", event => this._postLoadInit());
154 this._configureButtons(val);
158 return this.getAttribute("buttons");
161 set defaultButton(val) {
162 this._setDefaultButton(val);
165 get defaultButton() {
166 if (this.hasAttribute("defaultButton")) {
167 return this.getAttribute("defaultButton");
169 return "accept"; // default to the accept button
173 if (!this.__stringBundle) {
174 this.__stringBundle = Services.strings.createBundle(
175 "chrome://global/locale/dialog.properties"
178 return this.__stringBundle;
182 return this._doButtonCommand("accept");
186 return this._doButtonCommand("cancel");
189 getButton(aDlgType) {
190 return this._buttons[aDlgType];
194 return this.shadowRoot.querySelector(".dialog-button-box");
197 // NOTE(emilio): This has to match AppWindow::IntrinsicallySizeShell, to
198 // prevent flickering, see bug 1799394.
199 _sizeToPreferredSize() {
200 const docEl = document.documentElement;
201 const prefWidth = (() => {
202 if (docEl.hasAttribute("width")) {
203 return parseInt(docEl.getAttribute("width"));
205 let prefWidthProp = docEl.getAttribute("prefwidth");
207 let minWidth = parseFloat(
208 getComputedStyle(docEl).getPropertyValue(prefWidthProp)
210 if (isFinite(minWidth)) {
216 window.sizeToContentConstrained({ prefWidth });
219 moveToAlertPosition() {
220 // hack. we need this so the window has something like its final size
221 if (window.outerWidth == 1) {
223 "Trying to position a sizeless window; caller should have called sizeToContent() or sizeTo(). See bug 75649.\n"
225 this._sizeToPreferredSize();
229 var xOffset = (opener.outerWidth - window.outerWidth) / 2;
230 var yOffset = opener.outerHeight / 5;
232 var newX = opener.screenX + xOffset;
233 var newY = opener.screenY + yOffset;
235 newX = (screen.availWidth - window.outerWidth) / 2;
236 newY = (screen.availHeight - window.outerHeight) / 2;
239 // ensure the window is fully onscreen (if smaller than the screen)
240 if (newX < screen.availLeft) {
241 newX = screen.availLeft + 20;
243 if (newX + window.outerWidth > screen.availLeft + screen.availWidth) {
244 newX = screen.availLeft + screen.availWidth - window.outerWidth - 20;
247 if (newY < screen.availTop) {
248 newY = screen.availTop + 20;
250 if (newY + window.outerHeight > screen.availTop + screen.availHeight) {
251 newY = screen.availTop + screen.availHeight - window.outerHeight - 60;
254 window.moveTo(newX, newY);
257 centerWindowOnScreen() {
258 var xOffset = screen.availWidth / 2 - window.outerWidth / 2;
259 var yOffset = screen.availHeight / 2 - window.outerHeight / 2;
261 xOffset = xOffset > 0 ? xOffset : 0;
262 yOffset = yOffset > 0 ? yOffset : 0;
263 window.moveTo(xOffset, yOffset);
266 // Give focus to the first focusable element in the dialog
267 _setInitialFocusIfNeeded() {
268 let focusedElt = document.commandDispatcher.focusedElement;
273 const defaultButton = this.getButton(this.defaultButton);
274 Services.focus.moveFocus(
277 Services.focus.MOVEFOCUS_FORWARD,
278 Services.focus.FLAG_NOPARENTFRAME
281 focusedElt = document.commandDispatcher.focusedElement;
283 return; // No focusable element?
286 let firstFocusedElt = focusedElt;
288 focusedElt.localName == "tab" ||
289 focusedElt.getAttribute("noinitialfocus") == "true"
291 Services.focus.moveFocus(
294 Services.focus.MOVEFOCUS_FORWARD,
295 Services.focus.FLAG_NOPARENTFRAME
297 focusedElt = document.commandDispatcher.focusedElement;
298 if (focusedElt == firstFocusedElt) {
299 if (focusedElt.getAttribute("noinitialfocus") == "true") {
302 // Didn't find anything else to focus, we're done.
307 if (firstFocusedElt.localName == "tab") {
308 if (focusedElt.hasAttribute("dlgtype")) {
309 // We don't want to focus on anonymous OK, Cancel, etc. buttons,
310 // so return focus to the tab itself
311 firstFocusedElt.focus();
314 AppConstants.platform != "macosx" &&
315 focusedElt.hasAttribute("dlgtype") &&
316 focusedElt != defaultButton
318 defaultButton.focus();
319 if (document.commandDispatcher.focusedElement != defaultButton) {
320 // If the default button is not focusable, then return focus to the
321 // initial element if possible, or blur otherwise.
322 if (firstFocusedElt.getAttribute("noinitialfocus") == "true") {
325 firstFocusedElt.focus();
331 async _postLoadInit() {
332 this._setInitialFocusIfNeeded();
333 let finalStep = () => {
334 this._sizeToPreferredSize();
335 this._snapCursorToDefaultButtonIfNeeded();
337 // As a hack to ensure Windows sizes the window correctly,
338 // _sizeToPreferredSize() needs to happen after
339 // AppWindow::OnChromeLoaded. That one is called right after the load
340 // event dispatch but within the same task. Using direct dispatch let's
341 // all this code run before the next task (which might be a task to
342 // paint the window).
343 // But, MacOS doesn't like resizing after window/dialog becoming visible.
344 // Linux seems to be able to handle both cases.
345 if (Services.appinfo.OS == "Darwin") {
348 Services.tm.dispatchDirectTaskToCurrentThread(finalStep);
352 // This snaps the cursor to the default button rect on windows, when
353 // SPI_GETSNAPTODEFBUTTON is set.
354 async _snapCursorToDefaultButtonIfNeeded() {
355 const defaultButton = this.getButton(this.defaultButton);
356 if (!defaultButton) {
360 // FIXME(emilio, bug 1797624): This setTimeout() ensures enough time
361 // has passed so that the dialog vertical margin has been set by the
362 // front-end. For subdialogs, cursor positioning should probably be
363 // done by the opener instead, once the dialog is positioned.
364 await new Promise(r => setTimeout(r, 0));
365 await window.promiseDocumentFlushed(() => {});
366 window.notifyDefaultButtonLoaded(defaultButton);
370 _configureButtons(aButtons) {
371 // by default, get all the anonymous button elements
373 this._buttons = buttons;
375 for (let type of ["accept", "cancel", "extra1", "extra2", "disclosure"]) {
376 buttons[type] = this.shadowRoot.querySelector(`[dlgtype="${type}"]`);
379 // look for any overriding explicit button elements
380 var exBtns = this.getElementsByAttribute("dlgtype", "*");
382 for (let i = 0; i < exBtns.length; ++i) {
383 dlgtype = exBtns[i].getAttribute("dlgtype");
384 buttons[dlgtype].hidden = true; // hide the anonymous button
385 buttons[dlgtype] = exBtns[i];
388 // add the label and oncommand handler to each button
389 for (dlgtype in buttons) {
390 var button = buttons[dlgtype];
391 button.addEventListener(
393 this._handleButtonCommand.bind(this),
397 // don't override custom labels with pre-defined labels on explicit buttons
398 if (!button.hasAttribute("label")) {
399 // dialog attributes override the default labels in dialog.properties
400 if (this.hasAttribute("buttonlabel" + dlgtype)) {
403 this.getAttribute("buttonlabel" + dlgtype)
405 if (this.hasAttribute("buttonaccesskey" + dlgtype)) {
408 this.getAttribute("buttonaccesskey" + dlgtype)
411 } else if (this.hasAttribute("buttonid" + dlgtype)) {
412 document.l10n.setAttributes(
414 this.getAttribute("buttonid" + dlgtype)
416 } else if (dlgtype != "extra1" && dlgtype != "extra2") {
419 this._strBundle.GetStringFromName("button-" + dlgtype)
421 var accessKey = this._strBundle.GetStringFromName(
422 "accesskey-" + dlgtype
425 button.setAttribute("accesskey", accessKey);
431 // ensure that hitting enter triggers the default button command
432 // eslint-disable-next-line no-self-assign
433 this.defaultButton = this.defaultButton;
435 // if there is a special button configuration, use it
437 // expect a comma delimited list of dlgtype values
438 var list = aButtons.split(",");
440 // mark shown dlgtypes as true
448 for (let i = 0; i < list.length; ++i) {
449 shown[list[i].replace(/ /g, "")] = true;
452 // hide/show the buttons we want
453 for (dlgtype in buttons) {
454 buttons[dlgtype].hidden = !shown[dlgtype];
457 // show the spacer on Windows only when the extra2 button is present
458 if (AppConstants.platform == "win") {
459 let spacer = this.shadowRoot.querySelector(".button-spacer");
460 spacer.removeAttribute("hidden");
461 spacer.setAttribute("flex", shown.extra2 ? "1" : "0");
466 _setDefaultButton(aNewDefault) {
467 // remove the default attribute from the previous default button, if any
468 var oldDefaultButton = this.getButton(this.defaultButton);
469 if (oldDefaultButton) {
470 oldDefaultButton.removeAttribute("default");
473 var newDefaultButton = this.getButton(aNewDefault);
474 if (newDefaultButton) {
475 this.setAttribute("defaultButton", aNewDefault);
476 newDefaultButton.setAttribute("default", "true");
478 this.setAttribute("defaultButton", "none");
479 if (aNewDefault != "none") {
481 "invalid new default button: " + aNewDefault + ", assuming: none\n"
487 _handleButtonCommand(aEvent) {
488 return this._doButtonCommand(aEvent.target.getAttribute("dlgtype"));
491 _doButtonCommand(aDlgType) {
492 var button = this.getButton(aDlgType);
493 if (!button.disabled) {
494 var noCancel = this._fireButtonEvent(aDlgType);
496 if (aDlgType == "accept" || aDlgType == "cancel") {
497 var closingEvent = new CustomEvent("dialogclosing", {
499 detail: { button: aDlgType },
501 this.dispatchEvent(closingEvent);
510 _fireButtonEvent(aDlgType) {
511 var event = document.createEvent("Events");
512 event.initEvent("dialog" + aDlgType, true, true);
514 // handle dom event handlers
515 return this.dispatchEvent(event);
519 if (evt.defaultPrevented) {
523 var btn = this.getButton(this.defaultButton);
525 this._doButtonCommand(this.defaultButton);
530 let btn = this.getButton(this.defaultButton);
534 event.originalTarget == btn ||
536 event.originalTarget.localName == "button" ||
537 event.originalTarget.localName == "toolbarbutton"
544 customElements.define("dialog", MozDialog);