2017-03-22 23:56:25 +03:00
|
|
|
/* - 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/. */
|
|
|
|
|
2020-07-09 15:04:47 +03:00
|
|
|
"use strict";
|
2020-07-09 13:10:18 +03:00
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
var EXPORTED_SYMBOLS = ["SubDialog", "SubDialogManager"];
|
|
|
|
|
|
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The SubDialog resize callback.
|
|
|
|
* @callback SubDialog~resizeCallback
|
|
|
|
* @param {DOMNode} title - The title element of the dialog.
|
|
|
|
* @param {xul:browser} frame - The browser frame of the dialog.
|
|
|
|
*/
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
/**
|
|
|
|
* SubDialog constructor creates a new subdialog from a template and appends
|
|
|
|
* it to the parentElement.
|
2020-07-10 03:01:50 +03:00
|
|
|
* @param {DOMNode} template - The template is copied to create a new dialog.
|
|
|
|
* @param {DOMNode} parentElement - New dialog is appended onto parentElement.
|
|
|
|
* @param {String} id - A unique identifier for the dialog.
|
|
|
|
* @param {Object} dialogOptions - Dialog options object.
|
|
|
|
* @param {String[]} [dialogOptions.styleSheets] - An array of URLs to additional
|
|
|
|
* stylesheets to inject into the frame.
|
|
|
|
* @param {Boolean} [consumeOutsideClicks] - Whether to close the dialog when
|
|
|
|
* its background overlay is clicked.
|
|
|
|
* @param {SubDialog~resizeCallback} [resizeCallback] - Function to be called on
|
|
|
|
* dialog resize.
|
2017-06-05 12:00:47 +03:00
|
|
|
*/
|
2020-07-10 03:01:50 +03:00
|
|
|
function SubDialog({
|
|
|
|
template,
|
|
|
|
parentElement,
|
|
|
|
id,
|
|
|
|
dialogOptions: {
|
|
|
|
styleSheets = [],
|
|
|
|
consumeOutsideClicks = true,
|
|
|
|
resizeCallback,
|
|
|
|
} = {},
|
|
|
|
}) {
|
2017-06-05 12:00:47 +03:00
|
|
|
this._id = id;
|
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
this._injectedStyleSheets = this._injectedStyleSheets.concat(styleSheets);
|
|
|
|
this._consumeOutsideClicks = consumeOutsideClicks;
|
|
|
|
this._resizeCallback = resizeCallback;
|
2017-06-05 12:00:47 +03:00
|
|
|
this._overlay = template.cloneNode(true);
|
|
|
|
this._box = this._overlay.querySelector(".dialogBox");
|
2018-10-04 14:21:03 +03:00
|
|
|
this._titleBar = this._overlay.querySelector(".dialogTitleBar");
|
2017-06-05 12:00:47 +03:00
|
|
|
this._titleElement = this._overlay.querySelector(".dialogTitle");
|
2018-10-04 14:21:03 +03:00
|
|
|
this._closeButton = this._overlay.querySelector(".dialogClose");
|
|
|
|
this._frame = this._overlay.querySelector(".dialogFrame");
|
2017-06-05 12:00:47 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
this._overlay.classList.add(`dialogOverlay-${id}`);
|
2017-06-05 12:00:47 +03:00
|
|
|
this._frame.setAttribute("name", `dialogFrame-${id}`);
|
|
|
|
this._frameCreated = new Promise(resolve => {
|
2021-04-08 19:50:01 +03:00
|
|
|
this._frame.addEventListener(
|
|
|
|
"load",
|
|
|
|
() => {
|
|
|
|
// We intentionally avoid handling or passing the event to the
|
|
|
|
// resolve method to avoid shutdown window leaks. See bug 1686743.
|
|
|
|
resolve();
|
|
|
|
},
|
|
|
|
{
|
|
|
|
once: true,
|
|
|
|
capture: true,
|
|
|
|
}
|
|
|
|
);
|
2017-06-05 12:00:47 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
parentElement.appendChild(this._overlay);
|
|
|
|
this._overlay.hidden = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
SubDialog.prototype = {
|
2017-03-22 23:56:25 +03:00
|
|
|
_closingCallback: null,
|
|
|
|
_closingEvent: null,
|
|
|
|
_isClosing: false,
|
|
|
|
_frame: null,
|
2017-06-05 12:00:47 +03:00
|
|
|
_frameCreated: null,
|
2017-03-22 23:56:25 +03:00
|
|
|
_overlay: null,
|
|
|
|
_box: null,
|
|
|
|
_openedURL: null,
|
2020-07-10 03:01:50 +03:00
|
|
|
_injectedStyleSheets: ["chrome://global/skin/in-content/common.css"],
|
2017-03-22 23:56:25 +03:00
|
|
|
_resizeObserver: null,
|
2017-06-05 12:00:47 +03:00
|
|
|
_template: null,
|
|
|
|
_id: null,
|
|
|
|
_titleElement: null,
|
|
|
|
_closeButton: null,
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2021-04-08 19:50:01 +03:00
|
|
|
get frameContentWindow() {
|
|
|
|
return this._frame?.contentWindow;
|
|
|
|
},
|
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
get _window() {
|
|
|
|
return this._overlay?.ownerGlobal;
|
|
|
|
},
|
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
updateTitle(aEvent) {
|
2017-06-05 12:00:47 +03:00
|
|
|
if (aEvent.target != this._frame.contentDocument) {
|
2017-03-22 23:56:25 +03:00
|
|
|
return;
|
2019-07-05 10:53:32 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
this._titleElement.textContent = this._frame.contentDocument.title;
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
injectXMLStylesheet(aStylesheetURL) {
|
2020-04-23 22:29:48 +03:00
|
|
|
const doc = this._frame.contentDocument;
|
|
|
|
if ([...doc.styleSheets].find(s => s.href === aStylesheetURL)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let contentStylesheet = doc.createProcessingInstruction(
|
2017-03-22 23:56:25 +03:00
|
|
|
"xml-stylesheet",
|
|
|
|
'href="' + aStylesheetURL + '" type="text/css"'
|
|
|
|
);
|
2020-04-23 22:29:48 +03:00
|
|
|
doc.insertBefore(contentStylesheet, doc.documentElement);
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
async open(
|
|
|
|
aURL,
|
2020-09-02 17:24:47 +03:00
|
|
|
{ features, closingCallback, closedCallback, sizeTo } = {},
|
|
|
|
...aParams
|
2020-08-17 13:26:22 +03:00
|
|
|
) {
|
2021-03-05 13:50:52 +03:00
|
|
|
if (["available", "limitheight"].includes(sizeTo)) {
|
|
|
|
this._box.setAttribute("sizeto", sizeTo);
|
2020-08-25 00:00:36 +03:00
|
|
|
}
|
|
|
|
|
2020-08-01 18:13:24 +03:00
|
|
|
// Create a promise so consumers can tell when we're done setting up.
|
|
|
|
this._dialogReady = new Promise(resolve => {
|
|
|
|
this._resolveDialogReady = resolve;
|
|
|
|
});
|
2020-09-01 05:26:04 +03:00
|
|
|
this._frame._dialogReady = this._dialogReady;
|
2020-08-01 18:13:24 +03:00
|
|
|
|
2021-03-19 15:56:22 +03:00
|
|
|
// Assign close callbacks sync to ensure we can always callback even if the
|
|
|
|
// SubDialog is closed directly after opening.
|
|
|
|
let dialog = null;
|
|
|
|
|
|
|
|
if (closingCallback) {
|
|
|
|
this._closingCallback = (...args) => {
|
|
|
|
closingCallback.apply(dialog, args);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (closedCallback) {
|
|
|
|
this._closedCallback = (...args) => {
|
|
|
|
closedCallback.apply(dialog, args);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
// Wait until frame is ready to prevent browser crash in tests
|
|
|
|
await this._frameCreated;
|
2018-02-10 05:59:57 +03:00
|
|
|
|
|
|
|
if (!this._frame.contentWindow) {
|
|
|
|
// Given the binding constructor execution is asynchronous, and "load"
|
|
|
|
// event can be dispatched before the browser element is shown, the
|
|
|
|
// browser binding might not be constructed at this point. Forcibly
|
|
|
|
// construct the frame and construct the binding.
|
|
|
|
// FIXME: Remove this (bug 1437247)
|
|
|
|
this._frame.getBoundingClientRect();
|
|
|
|
}
|
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
// If we're open on some (other) URL or we're closing, open when closing has finished.
|
|
|
|
if (this._openedURL || this._isClosing) {
|
|
|
|
if (!this._isClosing) {
|
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
let args = Array.from(arguments);
|
|
|
|
this._closingPromise.then(() => {
|
|
|
|
this.open.apply(this, args);
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._addDialogEventListeners();
|
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
// If the parent is chrome we also need open the dialog as chrome, otherwise
|
|
|
|
// the openDialog call will fail.
|
2020-09-02 17:24:47 +03:00
|
|
|
let dialogFeatures = `resizable,dialog=no,centerscreen,chrome=${
|
2020-07-10 03:01:50 +03:00
|
|
|
this._window?.isChromeWindow ? "yes" : "no"
|
|
|
|
}`;
|
2020-09-02 17:24:47 +03:00
|
|
|
if (features) {
|
|
|
|
dialogFeatures = `${features},${dialogFeatures}`;
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
|
|
|
|
2021-03-19 15:56:22 +03:00
|
|
|
dialog = this._window.openDialog(
|
2017-06-05 12:00:47 +03:00
|
|
|
aURL,
|
|
|
|
`dialogFrame-${this._id}`,
|
2020-09-02 17:24:47 +03:00
|
|
|
dialogFeatures,
|
|
|
|
...aParams
|
2017-06-05 12:00:47 +03:00
|
|
|
);
|
2017-03-22 23:56:25 +03:00
|
|
|
|
|
|
|
this._closingEvent = null;
|
|
|
|
this._isClosing = false;
|
|
|
|
this._openedURL = aURL;
|
|
|
|
|
2020-09-02 17:24:47 +03:00
|
|
|
dialogFeatures = dialogFeatures.replace(/,/g, "&");
|
|
|
|
let featureParams = new URLSearchParams(dialogFeatures.toLowerCase());
|
2017-03-22 23:56:25 +03:00
|
|
|
this._box.setAttribute(
|
|
|
|
"resizable",
|
|
|
|
featureParams.has("resizable") &&
|
|
|
|
featureParams.get("resizable") != "no" &&
|
|
|
|
featureParams.get("resizable") != "0"
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
/**
|
|
|
|
* Close the dialog and mark it as aborted.
|
|
|
|
*/
|
|
|
|
abort() {
|
|
|
|
this._closingEvent = new CustomEvent("dialogclosing", {
|
|
|
|
bubbles: true,
|
|
|
|
detail: { dialog: this, abort: true },
|
|
|
|
});
|
|
|
|
this._frame.contentWindow.close();
|
2021-04-13 18:51:49 +03:00
|
|
|
// It's possible that we're aborting this dialog before we've had a
|
|
|
|
// chance to set up the contentWindow.close function override in
|
|
|
|
// _onContentLoaded. If so, call this.close() directly to clean things
|
|
|
|
// up. That'll be a no-op if the contentWindow.close override had been
|
|
|
|
// set up, since this.close is idempotent.
|
|
|
|
this.close(this._closingEvent);
|
2020-08-17 13:26:22 +03:00
|
|
|
},
|
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
close(aEvent = null) {
|
|
|
|
if (this._isClosing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._isClosing = true;
|
|
|
|
this._closingPromise = new Promise(resolve => {
|
|
|
|
this._resolveClosePromise = resolve;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (this._closingCallback) {
|
|
|
|
try {
|
|
|
|
this._closingCallback.call(null, aEvent);
|
|
|
|
} catch (ex) {
|
|
|
|
Cu.reportError(ex);
|
|
|
|
}
|
|
|
|
this._closingCallback = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._removeDialogEventListeners();
|
|
|
|
|
|
|
|
this._overlay.style.visibility = "";
|
|
|
|
// Clear the sizing inline styles.
|
|
|
|
this._frame.removeAttribute("style");
|
|
|
|
// Clear the sizing attributes
|
|
|
|
this._box.removeAttribute("width");
|
|
|
|
this._box.removeAttribute("height");
|
|
|
|
this._box.style.removeProperty("min-height");
|
|
|
|
this._box.style.removeProperty("min-width");
|
Bug 1699430 - allow window-modal dialogs to overlap the URL bar and tabstrip if the window is not tall enough, r=Mardak
There are a few disparate changes in this commit that combine to fix the bug.
In no particular order:
- set a min-height on windows with toolbars. This extends the minimum
content size from toolbarless windows to ones with toolbars, on the
assumption that the overhead from the toolbar and tabs is always
going to be at least 25px, even in compact mode (it's significantly
more at the moment). This is also conveniently *just* enough for
dialogs with a title, body and checkbox, at the default OS font size,
to be usable (though the bottom can still get a little cut-off).
- stop assuming there's 30px frame overhead on top of the size of the
browser in which the dialog is displayed in SubDialog.jsm. This is
perhaps true in prefs where we display a titlebar outside of the
browser, but we don't do this for content/tab/window-modal dialogs
shown in browser.xhtml so the code shouldn't assume. Without this,
when the window starts off not being tall enough to fit, we were
losing an additional 30px for no reason.
- instead of subtracting the 1em padding on the <dialog> that the
default styling provides (https://searchfox.org/mozilla-central/rev/2f109387cc6886859d3f985ed9aca352fff653b8/layout/style/res/html.css#815 ) just reset it to 0 and stop subtracting it.
- remove the CSS rule for tab and window-modal dialogs that depends on
`--doc-height-px`. It is never set, because it is only set for the
`limitheight` sizeto value in SubDialog.jsm, and the only
consumer that sets that is at
https://searchfox.org/mozilla-central/rev/2f109387cc6886859d3f985ed9aca352fff653b8/browser/base/content/browser.js#8988
for content dialogs.
- set the margin-top for the window-modal-dialog element from CSS
instead of from the gDialogBox code in browser.css (now without the 1em
subtraction, see above).
- expose the height of the dialog to the parent of the dialog overlay
from SubDialog.jsm as --inner-height
- use CSS to ensure the dialog is off-set to be just below chrome
when its size allows this, and otherwise move it up until it
fits. There's a code comment explaining this.
Differential Revision: https://phabricator.services.mozilla.com/D114292
2021-05-07 14:49:39 +03:00
|
|
|
this._overlay.parentNode.style.removeProperty("--inner-height");
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
let onClosed = () => {
|
|
|
|
this._openedURL = null;
|
|
|
|
|
|
|
|
this._resolveClosePromise();
|
|
|
|
|
|
|
|
if (this._closedCallback) {
|
|
|
|
try {
|
|
|
|
this._closedCallback.call(null, aEvent);
|
|
|
|
} catch (ex) {
|
|
|
|
Cu.reportError(ex);
|
|
|
|
}
|
|
|
|
this._closedCallback = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Wait for the frame to unload before running the closed callback.
|
|
|
|
if (this._frame.contentWindow) {
|
|
|
|
this._frame.contentWindow.addEventListener("unload", onClosed, {
|
|
|
|
once: true,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
onClosed();
|
|
|
|
}
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
this._overlay.dispatchEvent(
|
|
|
|
new CustomEvent("dialogclose", {
|
|
|
|
bubbles: true,
|
|
|
|
detail: { dialog: this },
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
// Defer removing the overlay so the frame content window can unload.
|
2021-03-11 14:34:35 +03:00
|
|
|
Services.tm.dispatchToMainThread(() => {
|
2020-08-17 13:26:22 +03:00
|
|
|
this._overlay.remove();
|
2021-03-11 14:34:35 +03:00
|
|
|
});
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
handleEvent(aEvent) {
|
|
|
|
switch (aEvent.type) {
|
2017-04-30 05:43:50 +03:00
|
|
|
case "click":
|
|
|
|
// Close the dialog if the user clicked the overlay background, just
|
|
|
|
// like when the user presses the ESC key (case "command" below).
|
2020-08-17 13:26:22 +03:00
|
|
|
if (aEvent.target !== this._overlay) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (this._consumeOutsideClicks) {
|
2017-04-30 05:43:50 +03:00
|
|
|
this._frame.contentWindow.close();
|
2020-08-17 13:26:22 +03:00
|
|
|
break;
|
2017-04-30 05:43:50 +03:00
|
|
|
}
|
2020-08-17 13:26:22 +03:00
|
|
|
this._frame.focus();
|
2017-04-30 05:43:50 +03:00
|
|
|
break;
|
2017-03-22 23:56:25 +03:00
|
|
|
case "command":
|
|
|
|
this._frame.contentWindow.close();
|
|
|
|
break;
|
|
|
|
case "dialogclosing":
|
|
|
|
this._onDialogClosing(aEvent);
|
|
|
|
break;
|
|
|
|
case "DOMTitleChanged":
|
|
|
|
this.updateTitle(aEvent);
|
|
|
|
break;
|
|
|
|
case "DOMFrameContentLoaded":
|
|
|
|
this._onContentLoaded(aEvent);
|
|
|
|
break;
|
|
|
|
case "load":
|
|
|
|
this._onLoad(aEvent);
|
|
|
|
break;
|
|
|
|
case "unload":
|
|
|
|
this._onUnload(aEvent);
|
|
|
|
break;
|
|
|
|
case "keydown":
|
|
|
|
this._onKeyDown(aEvent);
|
|
|
|
break;
|
|
|
|
case "focus":
|
|
|
|
this._onParentWinFocus(aEvent);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Private methods */
|
|
|
|
|
|
|
|
_onUnload(aEvent) {
|
2020-08-17 13:26:22 +03:00
|
|
|
if (
|
|
|
|
aEvent.target !== this._frame?.contentDocument ||
|
|
|
|
aEvent.target.location.href !== this._openedURL
|
|
|
|
) {
|
|
|
|
return;
|
2017-03-22 23:56:25 +03:00
|
|
|
}
|
2020-08-17 13:26:22 +03:00
|
|
|
this.abort();
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_onContentLoaded(aEvent) {
|
|
|
|
if (
|
|
|
|
aEvent.target != this._frame ||
|
|
|
|
aEvent.target.contentWindow.location == "about:blank"
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let styleSheetURL of this._injectedStyleSheets) {
|
|
|
|
this.injectXMLStylesheet(styleSheetURL);
|
|
|
|
}
|
|
|
|
|
2021-05-10 16:56:33 +03:00
|
|
|
let { contentDocument } = this._frame;
|
2017-03-22 23:56:25 +03:00
|
|
|
// Provide the ability for the dialog to know that it is being loaded "in-content".
|
2021-05-10 16:56:33 +03:00
|
|
|
for (let dialog of contentDocument.querySelectorAll("dialog")) {
|
2019-12-10 21:25:59 +03:00
|
|
|
dialog.setAttribute("subdialog", "true");
|
|
|
|
}
|
2021-05-10 16:56:33 +03:00
|
|
|
// Used by CSS to give the appropriate background colour in dark mode.
|
|
|
|
contentDocument.documentElement.setAttribute("dialogroot", "true");
|
2017-03-22 23:56:25 +03:00
|
|
|
|
|
|
|
this._frame.contentWindow.addEventListener("dialogclosing", this);
|
|
|
|
|
|
|
|
let oldResizeBy = this._frame.contentWindow.resizeBy;
|
2017-06-05 12:00:47 +03:00
|
|
|
this._frame.contentWindow.resizeBy = (resizeByWidth, resizeByHeight) => {
|
2017-03-22 23:56:25 +03:00
|
|
|
// Only handle resizeByHeight currently.
|
2021-05-13 18:05:24 +03:00
|
|
|
let frameHeight = this._overlay.parentNode.style.getPropertyValue(
|
|
|
|
"--inner-height"
|
|
|
|
);
|
|
|
|
if (frameHeight) {
|
|
|
|
frameHeight = parseFloat(frameHeight, 10);
|
|
|
|
} else {
|
|
|
|
frameHeight = this._frame.clientHeight;
|
|
|
|
}
|
2020-07-10 03:01:50 +03:00
|
|
|
let boxMinHeight = parseFloat(
|
|
|
|
this._window.getComputedStyle(this._box).minHeight,
|
|
|
|
10
|
|
|
|
);
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
this._box.style.minHeight = boxMinHeight + resizeByHeight + "px";
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2021-05-07 14:49:39 +03:00
|
|
|
this._overlay.parentNode.style.setProperty(
|
|
|
|
"--inner-height",
|
|
|
|
frameHeight + resizeByHeight + "px"
|
|
|
|
);
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
oldResizeBy.call(
|
|
|
|
this._frame.contentWindow,
|
|
|
|
resizeByWidth,
|
|
|
|
resizeByHeight
|
|
|
|
);
|
2017-03-22 23:56:25 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
// Make window.close calls work like dialog closing.
|
|
|
|
let oldClose = this._frame.contentWindow.close;
|
2017-06-05 12:00:47 +03:00
|
|
|
this._frame.contentWindow.close = () => {
|
|
|
|
var closingEvent = this._closingEvent;
|
2020-08-17 13:26:22 +03:00
|
|
|
// If this._closingEvent is set, the dialog is closed externally
|
|
|
|
// (dialog.js) and "dialogclosing" has already been dispatched.
|
2017-03-22 23:56:25 +03:00
|
|
|
if (!closingEvent) {
|
2020-08-17 13:26:22 +03:00
|
|
|
// If called without closing event, we need to create and dispatch it.
|
|
|
|
// This is the case for any external close calls not going through
|
|
|
|
// dialog.js.
|
2017-03-22 23:56:25 +03:00
|
|
|
closingEvent = new CustomEvent("dialogclosing", {
|
|
|
|
bubbles: true,
|
|
|
|
detail: { button: null },
|
|
|
|
});
|
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
this._frame.contentWindow.dispatchEvent(closingEvent);
|
|
|
|
} else if (this._closingEvent.detail?.abort) {
|
|
|
|
// If the dialog is aborted (SubDialog#abort) we need to dispatch the
|
|
|
|
// "dialogclosing" event ourselves.
|
2017-06-05 12:00:47 +03:00
|
|
|
this._frame.contentWindow.dispatchEvent(closingEvent);
|
2017-03-22 23:56:25 +03:00
|
|
|
}
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
this.close(closingEvent);
|
|
|
|
oldClose.call(this._frame.contentWindow);
|
2017-03-22 23:56:25 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
// XXX: Hack to make focus during the dialog's load functions work. Make the element visible
|
|
|
|
// sooner in DOMContentLoaded but mostly invisible instead of changing visibility just before
|
|
|
|
// the dialog's load event.
|
2020-11-04 14:03:48 +03:00
|
|
|
// Note that this needs to inherit so that hideDialog() works as expected.
|
|
|
|
this._overlay.style.visibility = "inherit";
|
2017-03-22 23:56:25 +03:00
|
|
|
this._overlay.style.opacity = "0.01";
|
2020-08-24 15:07:50 +03:00
|
|
|
|
|
|
|
// Ensure the document gets an a11y role of dialog.
|
2021-05-10 16:56:33 +03:00
|
|
|
const a11yDoc = contentDocument.body || contentDocument.documentElement;
|
2020-08-24 15:07:50 +03:00
|
|
|
a11yDoc.setAttribute("role", "dialog");
|
2021-03-25 20:28:56 +03:00
|
|
|
|
|
|
|
Services.obs.notifyObservers(this._frame.contentWindow, "subdialog-loaded");
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
2018-05-01 02:58:22 +03:00
|
|
|
async _onLoad(aEvent) {
|
2020-07-10 03:01:50 +03:00
|
|
|
let target = aEvent.currentTarget;
|
|
|
|
if (target.contentWindow.location == "about:blank") {
|
2017-03-22 23:56:25 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-05-01 02:58:22 +03:00
|
|
|
// In order to properly calculate the sizing of the subdialog, we need to
|
|
|
|
// ensure that all of the l10n is done.
|
2020-07-10 03:01:50 +03:00
|
|
|
if (target.contentDocument.l10n) {
|
|
|
|
await target.contentDocument.l10n.ready;
|
2018-05-01 02:58:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Some subdialogs may want to perform additional, asynchronous steps during initializations.
|
|
|
|
//
|
|
|
|
// In that case, we expect them to define a Promise which will delay measuring
|
|
|
|
// until the promise is fulfilled.
|
2020-07-10 03:01:50 +03:00
|
|
|
if (target.contentDocument.mozSubdialogReady) {
|
|
|
|
await target.contentDocument.mozSubdialogReady;
|
2018-05-01 02:58:22 +03:00
|
|
|
}
|
|
|
|
|
2018-11-30 22:37:41 +03:00
|
|
|
await this.resizeDialog();
|
2020-08-01 18:13:24 +03:00
|
|
|
this._resolveDialogReady();
|
2018-11-30 22:37:41 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
async resizeDialog() {
|
2017-03-22 23:56:25 +03:00
|
|
|
// Do this on load to wait for the CSS to load and apply before calculating the size.
|
|
|
|
let docEl = this._frame.contentDocument.documentElement;
|
|
|
|
|
|
|
|
// These are deduced from styles which we don't change, so it's safe to get them now:
|
|
|
|
let boxHorizontalBorder =
|
2020-07-10 03:01:50 +03:00
|
|
|
2 * parseFloat(this._window.getComputedStyle(this._box).borderLeftWidth);
|
2018-10-04 14:21:03 +03:00
|
|
|
let frameHorizontalMargin =
|
2020-07-10 03:01:50 +03:00
|
|
|
2 * parseFloat(this._window.getComputedStyle(this._frame).marginLeft);
|
2017-03-22 23:56:25 +03:00
|
|
|
|
|
|
|
// Then determine and set a bunch of width stuff:
|
2019-11-18 23:44:33 +03:00
|
|
|
let { scrollWidth } = docEl.ownerDocument.body || docEl;
|
|
|
|
let frameMinWidth = docEl.style.width || scrollWidth + "px";
|
2017-03-22 23:56:25 +03:00
|
|
|
let frameWidth = docEl.getAttribute("width")
|
|
|
|
? docEl.getAttribute("width") + "px"
|
|
|
|
: frameMinWidth;
|
2020-08-25 00:00:36 +03:00
|
|
|
|
|
|
|
if (this._box.getAttribute("sizeto") != "available") {
|
|
|
|
this._frame.style.width = frameWidth;
|
|
|
|
}
|
2020-08-17 13:26:22 +03:00
|
|
|
|
|
|
|
let boxMinWidth = `calc(${boxHorizontalBorder +
|
|
|
|
frameHorizontalMargin}px + ${frameMinWidth})`;
|
|
|
|
|
|
|
|
// Temporary fix to allow parent chrome to collapse properly to min width.
|
|
|
|
// See Bug 1658722.
|
|
|
|
if (this._window.isChromeWindow) {
|
|
|
|
boxMinWidth = `min(80vw, ${boxMinWidth})`;
|
|
|
|
}
|
|
|
|
this._box.style.minWidth = boxMinWidth;
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2019-05-08 18:49:25 +03:00
|
|
|
this.resizeVertically();
|
|
|
|
|
|
|
|
this._overlay.dispatchEvent(
|
|
|
|
new CustomEvent("dialogopen", {
|
|
|
|
bubbles: true,
|
|
|
|
detail: { dialog: this },
|
|
|
|
})
|
|
|
|
);
|
2020-11-04 14:03:48 +03:00
|
|
|
this._overlay.style.visibility = "inherit";
|
2019-05-08 18:49:25 +03:00
|
|
|
this._overlay.style.opacity = ""; // XXX: focus hack continued from _onContentLoaded
|
|
|
|
|
|
|
|
if (this._box.getAttribute("resizable") == "true") {
|
|
|
|
this._onResize = this._onResize.bind(this);
|
2020-07-10 03:01:50 +03:00
|
|
|
this._resizeObserver = new this._window.MutationObserver(this._onResize);
|
2019-05-08 18:49:25 +03:00
|
|
|
this._resizeObserver.observe(this._box, { attributes: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
this._trapFocus();
|
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
this._resizeCallback?.({
|
|
|
|
title: this._titleElement,
|
|
|
|
frame: this._frame,
|
|
|
|
});
|
2019-05-08 18:49:25 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
resizeVertically() {
|
|
|
|
let docEl = this._frame.contentDocument.documentElement;
|
2021-03-05 13:50:52 +03:00
|
|
|
function getDocHeight() {
|
|
|
|
let { scrollHeight } = docEl.ownerDocument.body || docEl;
|
|
|
|
return docEl.style.height || scrollHeight + "px";
|
|
|
|
}
|
2019-05-08 18:49:25 +03:00
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
// If the title bar is disabled (not in the template),
|
|
|
|
// set its height to 0 for the calculation.
|
|
|
|
let titleBarHeight = 0;
|
|
|
|
if (this._titleBar) {
|
|
|
|
titleBarHeight =
|
|
|
|
this._titleBar.clientHeight +
|
|
|
|
parseFloat(
|
|
|
|
this._window.getComputedStyle(this._titleBar).borderBottomWidth
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-05-08 18:49:25 +03:00
|
|
|
let boxVerticalBorder =
|
2020-07-10 03:01:50 +03:00
|
|
|
2 * parseFloat(this._window.getComputedStyle(this._box).borderTopWidth);
|
2019-05-08 18:49:25 +03:00
|
|
|
let frameVerticalMargin =
|
2020-07-10 03:01:50 +03:00
|
|
|
2 * parseFloat(this._window.getComputedStyle(this._frame).marginTop);
|
2019-05-08 18:49:25 +03:00
|
|
|
|
|
|
|
// The difference between the frame and box shouldn't change, either:
|
|
|
|
let boxRect = this._box.getBoundingClientRect();
|
|
|
|
let frameRect = this._frame.getBoundingClientRect();
|
|
|
|
let frameSizeDifference =
|
|
|
|
frameRect.top - boxRect.top + (boxRect.bottom - frameRect.bottom);
|
|
|
|
|
Bug 1693277 - fix sizing of dialogs with various content sizes, r=mtigley
This uses the new sizeTo value supported by SubDialog.jsm to also impact
how we size things inside the dialog. It's a bit complicated because we
need the dialog to support 3 layouts:
1. the window-modal setup (which we're trying not to touch)
2. the non-proton modal setup (which uses grid layout to have one column with
the image and the username/password fields, where applicable)
3. the proton modal setup (where we don't want the grid layout, as
the labels for username/password go above their text fields and there's no
big icon on the left, so there's no point)
Finally, for content modal prompts, we need to effectively determine the size
of the content and whether we need to overflow. We can't just always allow
flexbox to shrink everything, or we end up with overflow even for dialogs
with just 2 lines of text.
So with this patch, we determine the ideal height of the dialog document from
SubDialog.jsm, before setting the `.sizeDetermined` class there, thus allowing
the #infoBody part to overflow. The SubDialog.jsm code will ensure the
embedding browser is large enough to allow not overflowing/scrolling the
content.
In terms of the 3 layouts, we select for "not 1" by using `dialog[subdialog]`
(an attribute set by the SubDialog.jsm code anyway). We then use proton
pref-based @supports queries to pick between layouts (2) and (3).
Differential Revision: https://phabricator.services.mozilla.com/D107111
2021-03-05 13:50:53 +03:00
|
|
|
let contentPane =
|
|
|
|
this._frame.contentDocument.querySelector(".contentPane") ||
|
|
|
|
this._frame.contentDocument.querySelector("dialog");
|
|
|
|
|
2021-03-05 13:50:52 +03:00
|
|
|
let sizeTo = this._box.getAttribute("sizeto");
|
|
|
|
if (["available", "limitheight"].includes(sizeTo)) {
|
|
|
|
if (sizeTo == "limitheight") {
|
|
|
|
this._overlay.style.setProperty("--doc-height-px", getDocHeight());
|
Bug 1693277 - fix sizing of dialogs with various content sizes, r=mtigley
This uses the new sizeTo value supported by SubDialog.jsm to also impact
how we size things inside the dialog. It's a bit complicated because we
need the dialog to support 3 layouts:
1. the window-modal setup (which we're trying not to touch)
2. the non-proton modal setup (which uses grid layout to have one column with
the image and the username/password fields, where applicable)
3. the proton modal setup (where we don't want the grid layout, as
the labels for username/password go above their text fields and there's no
big icon on the left, so there's no point)
Finally, for content modal prompts, we need to effectively determine the size
of the content and whether we need to overflow. We can't just always allow
flexbox to shrink everything, or we end up with overflow even for dialogs
with just 2 lines of text.
So with this patch, we determine the ideal height of the dialog document from
SubDialog.jsm, before setting the `.sizeDetermined` class there, thus allowing
the #infoBody part to overflow. The SubDialog.jsm code will ensure the
embedding browser is large enough to allow not overflowing/scrolling the
content.
In terms of the 3 layouts, we select for "not 1" by using `dialog[subdialog]`
(an attribute set by the SubDialog.jsm code anyway). We then use proton
pref-based @supports queries to pick between layouts (2) and (3).
Differential Revision: https://phabricator.services.mozilla.com/D107111
2021-03-05 13:50:53 +03:00
|
|
|
contentPane?.classList.add("sizeDetermined");
|
2021-03-05 13:50:52 +03:00
|
|
|
} else {
|
|
|
|
// Inform the CSS of the toolbar height so the bottom padding can be
|
|
|
|
// correctly calculated.
|
|
|
|
this._box.style.setProperty("--box-top-px", `${boxRect.top}px`);
|
|
|
|
}
|
2020-08-25 00:00:36 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
// Now do the same but for the height. We need to do this afterwards because otherwise
|
|
|
|
// XUL assumes we'll optimize for height and gives us "wrong" values which then are no
|
|
|
|
// longer correct after we set the width:
|
2021-03-05 13:50:52 +03:00
|
|
|
let frameMinHeight = getDocHeight();
|
2017-03-22 23:56:25 +03:00
|
|
|
let frameHeight = docEl.getAttribute("height")
|
|
|
|
? docEl.getAttribute("height") + "px"
|
|
|
|
: frameMinHeight;
|
|
|
|
|
|
|
|
// Now check if the frame height we calculated is possible at this window size,
|
|
|
|
// accounting for titlebar, padding/border and some spacing.
|
2021-06-11 23:02:37 +03:00
|
|
|
let frameOverhead = frameSizeDifference + titleBarHeight;
|
2021-05-13 18:05:24 +03:00
|
|
|
let maxHeight = this._window.innerHeight - frameOverhead;
|
2017-03-22 23:56:25 +03:00
|
|
|
// Do this with a frame height in pixels...
|
|
|
|
let comparisonFrameHeight;
|
|
|
|
if (frameHeight.endsWith("em")) {
|
2020-07-10 03:01:50 +03:00
|
|
|
let fontSize = parseFloat(
|
|
|
|
this._window.getComputedStyle(this._frame).fontSize
|
|
|
|
);
|
2017-03-22 23:56:25 +03:00
|
|
|
comparisonFrameHeight = parseFloat(frameHeight, 10) * fontSize;
|
|
|
|
} else if (frameHeight.endsWith("px")) {
|
|
|
|
comparisonFrameHeight = parseFloat(frameHeight, 10);
|
|
|
|
} else {
|
|
|
|
Cu.reportError(
|
|
|
|
"This dialog (" +
|
|
|
|
this._frame.contentWindow.location.href +
|
|
|
|
") " +
|
|
|
|
"set a height in non-px-non-em units ('" +
|
|
|
|
frameHeight +
|
|
|
|
"'), " +
|
|
|
|
"which is likely to lead to bad sizing in in-content preferences. " +
|
|
|
|
"Please consider changing this."
|
|
|
|
);
|
|
|
|
comparisonFrameHeight = parseFloat(frameHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (comparisonFrameHeight > maxHeight) {
|
2018-10-15 21:01:59 +03:00
|
|
|
// If the height is bigger than that of the window, we should let the
|
|
|
|
// contents scroll. The class is set on the "dialog" element, unless a
|
|
|
|
// content pane exists, which is usually the case when the "window"
|
|
|
|
// element is used to implement the subdialog instead.
|
2017-03-22 23:56:25 +03:00
|
|
|
frameMinHeight = maxHeight + "px";
|
Bug 1693277 - fix sizing of dialogs with various content sizes, r=mtigley
This uses the new sizeTo value supported by SubDialog.jsm to also impact
how we size things inside the dialog. It's a bit complicated because we
need the dialog to support 3 layouts:
1. the window-modal setup (which we're trying not to touch)
2. the non-proton modal setup (which uses grid layout to have one column with
the image and the username/password fields, where applicable)
3. the proton modal setup (where we don't want the grid layout, as
the labels for username/password go above their text fields and there's no
big icon on the left, so there's no point)
Finally, for content modal prompts, we need to effectively determine the size
of the content and whether we need to overflow. We can't just always allow
flexbox to shrink everything, or we end up with overflow even for dialogs
with just 2 lines of text.
So with this patch, we determine the ideal height of the dialog document from
SubDialog.jsm, before setting the `.sizeDetermined` class there, thus allowing
the #infoBody part to overflow. The SubDialog.jsm code will ensure the
embedding browser is large enough to allow not overflowing/scrolling the
content.
In terms of the 3 layouts, we select for "not 1" by using `dialog[subdialog]`
(an attribute set by the SubDialog.jsm code anyway). We then use proton
pref-based @supports queries to pick between layouts (2) and (3).
Differential Revision: https://phabricator.services.mozilla.com/D107111
2021-03-05 13:50:53 +03:00
|
|
|
|
2020-01-16 01:18:09 +03:00
|
|
|
if (contentPane) {
|
|
|
|
// There are also instances where the subdialog is neither implemented
|
|
|
|
// using a content pane, nor a <dialog> (such as manageAddresses.xhtml)
|
|
|
|
// so make sure to check that we actually got a contentPane before we
|
|
|
|
// use it.
|
|
|
|
contentPane.classList.add("doScroll");
|
|
|
|
}
|
2017-03-22 23:56:25 +03:00
|
|
|
}
|
|
|
|
|
Bug 1699430 - allow window-modal dialogs to overlap the URL bar and tabstrip if the window is not tall enough, r=Mardak
There are a few disparate changes in this commit that combine to fix the bug.
In no particular order:
- set a min-height on windows with toolbars. This extends the minimum
content size from toolbarless windows to ones with toolbars, on the
assumption that the overhead from the toolbar and tabs is always
going to be at least 25px, even in compact mode (it's significantly
more at the moment). This is also conveniently *just* enough for
dialogs with a title, body and checkbox, at the default OS font size,
to be usable (though the bottom can still get a little cut-off).
- stop assuming there's 30px frame overhead on top of the size of the
browser in which the dialog is displayed in SubDialog.jsm. This is
perhaps true in prefs where we display a titlebar outside of the
browser, but we don't do this for content/tab/window-modal dialogs
shown in browser.xhtml so the code shouldn't assume. Without this,
when the window starts off not being tall enough to fit, we were
losing an additional 30px for no reason.
- instead of subtracting the 1em padding on the <dialog> that the
default styling provides (https://searchfox.org/mozilla-central/rev/2f109387cc6886859d3f985ed9aca352fff653b8/layout/style/res/html.css#815 ) just reset it to 0 and stop subtracting it.
- remove the CSS rule for tab and window-modal dialogs that depends on
`--doc-height-px`. It is never set, because it is only set for the
`limitheight` sizeto value in SubDialog.jsm, and the only
consumer that sets that is at
https://searchfox.org/mozilla-central/rev/2f109387cc6886859d3f985ed9aca352fff653b8/browser/base/content/browser.js#8988
for content dialogs.
- set the margin-top for the window-modal-dialog element from CSS
instead of from the gDialogBox code in browser.css (now without the 1em
subtraction, see above).
- expose the height of the dialog to the parent of the dialog overlay
from SubDialog.jsm as --inner-height
- use CSS to ensure the dialog is off-set to be just below chrome
when its size allows this, and otherwise move it up until it
fits. There's a code comment explaining this.
Differential Revision: https://phabricator.services.mozilla.com/D114292
2021-05-07 14:49:39 +03:00
|
|
|
this._overlay.parentNode.style.setProperty("--inner-height", frameHeight);
|
2021-05-13 18:05:24 +03:00
|
|
|
this._frame.style.height = `min(
|
|
|
|
calc(100vh - ${frameOverhead}px),
|
|
|
|
var(--inner-height, ${frameHeight})
|
|
|
|
)`;
|
|
|
|
this._box.style.minHeight = `calc(
|
|
|
|
${boxVerticalBorder + titleBarHeight + frameVerticalMargin}px +
|
|
|
|
${frameMinHeight}
|
|
|
|
)`;
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_onResize(mutations) {
|
2017-06-05 12:00:47 +03:00
|
|
|
let frame = this._frame;
|
2017-03-22 23:56:25 +03:00
|
|
|
// The width and height styles are needed for the initial
|
|
|
|
// layout of the frame, but afterward they need to be removed
|
|
|
|
// or their presence will restrict the contents of the <browser>
|
|
|
|
// from resizing to a smaller size.
|
|
|
|
frame.style.removeProperty("width");
|
|
|
|
frame.style.removeProperty("height");
|
|
|
|
|
|
|
|
let docEl = frame.contentDocument.documentElement;
|
|
|
|
let persistedAttributes = docEl.getAttribute("persist");
|
|
|
|
if (
|
|
|
|
!persistedAttributes ||
|
|
|
|
(!persistedAttributes.includes("width") &&
|
|
|
|
!persistedAttributes.includes("height"))
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let mutation of mutations) {
|
|
|
|
if (mutation.attributeName == "width") {
|
|
|
|
docEl.setAttribute("width", docEl.scrollWidth);
|
|
|
|
} else if (mutation.attributeName == "height") {
|
|
|
|
docEl.setAttribute("height", docEl.scrollHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_onDialogClosing(aEvent) {
|
|
|
|
this._frame.contentWindow.removeEventListener("dialogclosing", this);
|
|
|
|
this._closingEvent = aEvent;
|
|
|
|
},
|
|
|
|
|
|
|
|
_onKeyDown(aEvent) {
|
2020-08-17 13:26:22 +03:00
|
|
|
// Close on ESC key if target is SubDialog
|
|
|
|
// If we're in the parent window, we need to check if the SubDialogs
|
|
|
|
// frame is targeted, so we don't close the wrong dialog.
|
|
|
|
if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE && !aEvent.defaultPrevented) {
|
|
|
|
if (
|
2020-08-19 23:04:52 +03:00
|
|
|
(this._window.isChromeWindow && aEvent.currentTarget == this._box) ||
|
2020-08-17 13:26:22 +03:00
|
|
|
(!this._window.isChromeWindow && aEvent.currentTarget == this._window)
|
|
|
|
) {
|
2020-09-21 16:25:16 +03:00
|
|
|
// Prevent ESC on SubDialog from cancelling page load (Bug 1665339).
|
|
|
|
aEvent.preventDefault();
|
2020-08-19 23:04:52 +03:00
|
|
|
this._frame.contentWindow.close();
|
2020-08-17 13:26:22 +03:00
|
|
|
return;
|
|
|
|
}
|
2017-03-22 23:56:25 +03:00
|
|
|
}
|
2020-08-17 13:26:22 +03:00
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
if (
|
2020-08-17 13:26:22 +03:00
|
|
|
this._window.isChromeWindow ||
|
2017-03-22 23:56:25 +03:00
|
|
|
aEvent.keyCode != aEvent.DOM_VK_TAB ||
|
|
|
|
aEvent.ctrlKey ||
|
|
|
|
aEvent.altKey ||
|
|
|
|
aEvent.metaKey
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let fm = Services.focus;
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
let isLastFocusableElement = el => {
|
2017-03-22 23:56:25 +03:00
|
|
|
// XXXgijs unfortunately there is no way to get the last focusable element without asking
|
|
|
|
// the focus manager to move focus to it.
|
2017-06-05 12:00:47 +03:00
|
|
|
let rv =
|
|
|
|
el ==
|
|
|
|
fm.moveFocus(this._frame.contentWindow, null, fm.MOVEFOCUS_LAST, 0);
|
2017-03-22 23:56:25 +03:00
|
|
|
fm.setFocus(el, 0);
|
|
|
|
return rv;
|
2017-06-05 12:00:47 +03:00
|
|
|
};
|
2017-03-22 23:56:25 +03:00
|
|
|
|
|
|
|
let forward = !aEvent.shiftKey;
|
|
|
|
// check if focus is leaving the frame (incl. the close button):
|
|
|
|
if (
|
|
|
|
(aEvent.target == this._closeButton && !forward) ||
|
|
|
|
(isLastFocusableElement(aEvent.originalTarget) && forward)
|
|
|
|
) {
|
|
|
|
aEvent.preventDefault();
|
|
|
|
aEvent.stopImmediatePropagation();
|
2020-08-17 13:26:22 +03:00
|
|
|
|
|
|
|
let parentWin = this._window.docShell.chromeEventHandler.ownerGlobal;
|
2017-03-22 23:56:25 +03:00
|
|
|
if (forward) {
|
|
|
|
fm.moveFocus(parentWin, null, fm.MOVEFOCUS_FIRST, fm.FLAG_BYKEY);
|
|
|
|
} else {
|
|
|
|
// Somehow, moving back 'past' the opening doc is not trivial. Cheat by doing it in 2 steps:
|
2020-07-10 03:01:50 +03:00
|
|
|
fm.moveFocus(this._window, null, fm.MOVEFOCUS_ROOT, fm.FLAG_BYKEY);
|
2017-03-22 23:56:25 +03:00
|
|
|
fm.moveFocus(parentWin, null, fm.MOVEFOCUS_BACKWARD, fm.FLAG_BYKEY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_onParentWinFocus(aEvent) {
|
|
|
|
// Explicitly check for the focus target of |window| to avoid triggering this when the window
|
|
|
|
// is refocused
|
2020-07-10 03:01:50 +03:00
|
|
|
if (
|
|
|
|
this._closeButton &&
|
|
|
|
aEvent.target != this._closeButton &&
|
|
|
|
aEvent.target != this._window
|
|
|
|
) {
|
2017-03-22 23:56:25 +03:00
|
|
|
this._closeButton.focus();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
/**
|
|
|
|
* Setup dialog event listeners.
|
|
|
|
* @param {Boolean} [includeLoad] - Whether to register load/unload listeners.
|
|
|
|
*/
|
|
|
|
_addDialogEventListeners(includeLoad = true) {
|
|
|
|
if (this._window.isChromeWindow) {
|
|
|
|
// Only register an event listener if we have a title to show.
|
|
|
|
if (this._titleBar) {
|
|
|
|
this._frame.addEventListener("DOMTitleChanged", this, true);
|
|
|
|
}
|
2020-08-13 21:43:49 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
if (includeLoad) {
|
|
|
|
this._window.addEventListener("unload", this, true);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let chromeBrowser = this._window.docShell.chromeEventHandler;
|
2020-08-14 14:31:04 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
if (includeLoad) {
|
|
|
|
// For content windows we listen for unload of the browser
|
|
|
|
chromeBrowser.addEventListener("unload", this, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._titleBar) {
|
|
|
|
chromeBrowser.addEventListener("DOMTitleChanged", this, true);
|
|
|
|
}
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
// Make the close button work.
|
|
|
|
this._closeButton?.addEventListener("command", this);
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
if (includeLoad) {
|
|
|
|
// DOMFrameContentLoaded only fires on the top window
|
|
|
|
this._window.addEventListener("DOMFrameContentLoaded", this, true);
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
// Wait for the stylesheets injected during DOMContentLoaded to load before showing the dialog
|
|
|
|
// otherwise there is a flicker of the stylesheet applying.
|
|
|
|
this._frame.addEventListener("load", this, true);
|
|
|
|
}
|
2017-04-30 05:43:50 +03:00
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
// Ensure we get <esc> keypresses even if nothing in the subdialog is focusable
|
|
|
|
// (happens on OS X when only text inputs and lists are focusable, and
|
|
|
|
// the subdialog only has checkboxes/radiobuttons/buttons)
|
2020-08-17 13:26:22 +03:00
|
|
|
if (!this._window.isChromeWindow) {
|
|
|
|
this._window.addEventListener("keydown", this, true);
|
|
|
|
}
|
2017-04-30 05:43:50 +03:00
|
|
|
|
|
|
|
this._overlay.addEventListener("click", this, true);
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
/**
|
|
|
|
* Remove dialog event listeners.
|
|
|
|
* @param {Boolean} [includeLoad] - Whether to remove load/unload listeners.
|
|
|
|
*/
|
|
|
|
_removeDialogEventListeners(includeLoad = true) {
|
|
|
|
if (this._window.isChromeWindow) {
|
|
|
|
this._frame.removeEventListener("DOMTitleChanged", this, true);
|
|
|
|
|
|
|
|
if (includeLoad) {
|
|
|
|
this._window.removeEventListener("unload", this, true);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let chromeBrowser = this._window.docShell.chromeEventHandler;
|
|
|
|
if (includeLoad) {
|
|
|
|
chromeBrowser.removeEventListener("unload", this, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
chromeBrowser.removeEventListener("DOMTitleChanged", this, true);
|
|
|
|
}
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
this._closeButton?.removeEventListener("command", this);
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
if (includeLoad) {
|
|
|
|
this._window.removeEventListener("DOMFrameContentLoaded", this, true);
|
|
|
|
this._frame.removeEventListener("load", this, true);
|
|
|
|
this._frame.contentWindow.removeEventListener("dialogclosing", this);
|
|
|
|
}
|
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
this._window.removeEventListener("keydown", this, true);
|
2017-04-30 05:43:50 +03:00
|
|
|
|
|
|
|
this._overlay.removeEventListener("click", this, true);
|
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
if (this._resizeObserver) {
|
|
|
|
this._resizeObserver.disconnect();
|
|
|
|
this._resizeObserver = null;
|
|
|
|
}
|
2020-08-17 13:26:22 +03:00
|
|
|
|
2017-03-22 23:56:25 +03:00
|
|
|
this._untrapFocus();
|
|
|
|
},
|
|
|
|
|
2021-01-27 14:33:57 +03:00
|
|
|
/**
|
|
|
|
* Focus the dialog content.
|
|
|
|
* If the embedded document defines a custom focus handler it will be called.
|
|
|
|
* Otherwise we will focus the first focusable element in the content window.
|
|
|
|
* @param {boolean} [isInitialFocus] - Whether the dialog is focused for the
|
|
|
|
* first time after opening.
|
|
|
|
*/
|
|
|
|
focus(isInitialFocus = false) {
|
|
|
|
// If the content window has its own focus logic, hand off the focus call.
|
|
|
|
let focusHandler = this._frame?.contentDocument?.subDialogSetDefaultFocus;
|
|
|
|
if (focusHandler) {
|
|
|
|
focusHandler(isInitialFocus);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Handle focus ourselves. Try to move the focus to the first element in
|
|
|
|
// the content window.
|
2017-03-22 23:56:25 +03:00
|
|
|
let fm = Services.focus;
|
2021-04-28 21:09:47 +03:00
|
|
|
|
|
|
|
// We're intentionally hiding the focus ring here for now per bug 1704882,
|
|
|
|
// but we aim to have a better fix that retains the focus ring for users
|
|
|
|
// that had brought up the dialog by keyboard in bug 1708261.
|
2020-09-01 05:26:04 +03:00
|
|
|
let focusedElement = fm.moveFocus(
|
|
|
|
this._frame.contentWindow,
|
|
|
|
null,
|
|
|
|
fm.MOVEFOCUS_FIRST,
|
2021-04-28 21:09:47 +03:00
|
|
|
fm.FLAG_NOSHOWRING
|
2020-09-01 05:26:04 +03:00
|
|
|
);
|
|
|
|
if (!focusedElement) {
|
|
|
|
// Ensure the focus is pulled out of the content document even if there's
|
|
|
|
// nothing focusable in the dialog.
|
|
|
|
this._frame.contentWindow.focus();
|
|
|
|
}
|
2020-08-28 15:36:00 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_trapFocus() {
|
2020-11-19 21:20:36 +03:00
|
|
|
// Attach a system event listener so the dialog can cancel keydown events.
|
|
|
|
// See Bug 1669990.
|
|
|
|
this._box.addEventListener("keydown", this, { mozSystemGroup: true });
|
2020-07-10 03:01:50 +03:00
|
|
|
this._closeButton?.addEventListener("keydown", this);
|
2017-03-22 23:56:25 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
if (!this._window.isChromeWindow) {
|
|
|
|
this._window.addEventListener("focus", this, true);
|
|
|
|
}
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_untrapFocus() {
|
2020-11-19 21:20:36 +03:00
|
|
|
this._box.removeEventListener("keydown", this, { mozSystemGroup: true });
|
2020-07-10 03:01:50 +03:00
|
|
|
this._closeButton?.removeEventListener("keydown", this);
|
2020-08-01 18:13:24 +03:00
|
|
|
this._window.removeEventListener("focus", this, true);
|
2017-03-22 23:56:25 +03:00
|
|
|
},
|
|
|
|
};
|
2017-06-05 12:00:47 +03:00
|
|
|
|
2020-07-09 21:09:25 +03:00
|
|
|
/**
|
|
|
|
* Manages multiple SubDialogs in a dialog stack element.
|
|
|
|
*/
|
2020-07-10 03:01:50 +03:00
|
|
|
class SubDialogManager {
|
2020-07-09 21:09:25 +03:00
|
|
|
/**
|
|
|
|
* @param {Object} options - Dialog manager options.
|
|
|
|
* @param {DOMNode} options.dialogStack - Container element for all dialogs
|
|
|
|
* this instance manages.
|
|
|
|
* @param {DOMNode} options.dialogTemplate - Element to use as template for
|
|
|
|
* constructing new dialogs.
|
|
|
|
* @param {Number} [options.orderType] - Whether dialogs should be ordered as
|
|
|
|
* a stack or a queue.
|
|
|
|
* @param {Boolean} [options.allowDuplicateDialogs] - Whether to allow opening
|
|
|
|
* duplicate dialogs (same URI) at the same time. If disabled, opening a
|
|
|
|
* dialog with the same URI as an existing dialog will be a no-op.
|
|
|
|
* @param {Object} options.dialogOptions - Options passed to every
|
|
|
|
* SubDialog instance.
|
|
|
|
* @see {@link SubDialog} for a list of dialog options.
|
|
|
|
*/
|
2020-07-10 03:01:50 +03:00
|
|
|
constructor({
|
|
|
|
dialogStack,
|
|
|
|
dialogTemplate,
|
2020-07-09 21:09:25 +03:00
|
|
|
orderType = SubDialogManager.ORDER_STACK,
|
2020-07-10 03:01:50 +03:00
|
|
|
allowDuplicateDialogs = false,
|
|
|
|
dialogOptions,
|
|
|
|
}) {
|
|
|
|
/**
|
2020-07-09 21:09:25 +03:00
|
|
|
* New dialogs are pushed to the end of the _dialogs array.
|
|
|
|
* Depending on the orderType either the last element (stack) or the first
|
|
|
|
* element (queue) in the array will be the top and visible.
|
|
|
|
* @type {SubDialog[]}
|
2020-07-10 03:01:50 +03:00
|
|
|
*/
|
|
|
|
this._dialogs = [];
|
|
|
|
this._dialogStack = dialogStack;
|
|
|
|
this._dialogTemplate = dialogTemplate;
|
|
|
|
this._topLevelPrevActiveElement = null;
|
2020-07-09 21:09:25 +03:00
|
|
|
this._orderType = orderType;
|
2020-07-10 03:01:50 +03:00
|
|
|
this._allowDuplicateDialogs = allowDuplicateDialogs;
|
|
|
|
this._dialogOptions = dialogOptions;
|
2017-06-05 12:00:47 +03:00
|
|
|
|
|
|
|
this._preloadDialog = new SubDialog({
|
|
|
|
template: this._dialogTemplate,
|
|
|
|
parentElement: this._dialogStack,
|
2020-08-17 13:26:22 +03:00
|
|
|
id: SubDialogManager._nextDialogID++,
|
2020-07-10 03:01:50 +03:00
|
|
|
dialogOptions: this._dialogOptions,
|
2017-06-05 12:00:47 +03:00
|
|
|
});
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
|
|
|
|
2020-07-09 21:09:25 +03:00
|
|
|
/**
|
|
|
|
* Get the dialog which is currently on top. This depends on whether the
|
|
|
|
* dialogs are in a stack or a queue.
|
|
|
|
*/
|
2020-07-10 03:01:50 +03:00
|
|
|
get _topDialog() {
|
2020-07-09 21:09:25 +03:00
|
|
|
if (!this._dialogs.length) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
if (this._orderType === SubDialogManager.ORDER_STACK) {
|
|
|
|
return this._dialogs[this._dialogs.length - 1];
|
|
|
|
}
|
|
|
|
return this._dialogs[0];
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
open(
|
|
|
|
aURL,
|
2020-09-02 17:24:47 +03:00
|
|
|
{
|
|
|
|
features,
|
|
|
|
closingCallback,
|
|
|
|
closedCallback,
|
|
|
|
allowDuplicateDialogs,
|
|
|
|
sizeTo,
|
|
|
|
} = {},
|
|
|
|
...aParams
|
2020-08-17 13:26:22 +03:00
|
|
|
) {
|
2020-09-02 17:24:47 +03:00
|
|
|
let allowDuplicates =
|
|
|
|
allowDuplicateDialogs != null
|
|
|
|
? allowDuplicateDialogs
|
|
|
|
: this._allowDuplicateDialogs;
|
2017-06-05 12:00:47 +03:00
|
|
|
// If we're already open/opening on this URL, do nothing.
|
2020-09-02 17:24:47 +03:00
|
|
|
if (
|
|
|
|
!allowDuplicates &&
|
|
|
|
this._dialogs.some(dialog => dialog._openedURL == aURL)
|
|
|
|
) {
|
2020-09-02 17:24:58 +03:00
|
|
|
return undefined;
|
2017-06-05 12:00:47 +03:00
|
|
|
}
|
|
|
|
|
2020-07-10 03:01:50 +03:00
|
|
|
let doc = this._dialogStack.ownerDocument;
|
|
|
|
|
2020-07-09 21:09:25 +03:00
|
|
|
// For dialog stacks, remember the last active element before opening the
|
|
|
|
// next dialog. This allows us to restore focus on dialog close.
|
|
|
|
if (
|
|
|
|
this._orderType === SubDialogManager.ORDER_STACK &&
|
|
|
|
this._dialogs.length
|
|
|
|
) {
|
2020-07-10 03:01:50 +03:00
|
|
|
this._topDialog._prevActiveElement = doc.activeElement;
|
2020-07-09 21:09:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!this._dialogs.length) {
|
2020-07-10 03:01:50 +03:00
|
|
|
// When opening the first dialog, show the dialog stack.
|
2018-02-10 05:59:57 +03:00
|
|
|
this._dialogStack.hidden = false;
|
2020-11-04 14:03:48 +03:00
|
|
|
this._dialogStack.classList.remove("temporarilyHidden");
|
2020-07-10 03:01:50 +03:00
|
|
|
this._topLevelPrevActiveElement = doc.activeElement;
|
2018-02-10 05:59:57 +03:00
|
|
|
}
|
|
|
|
|
2021-01-27 14:33:57 +03:00
|
|
|
this._dialogs.push(this._preloadDialog);
|
2020-08-17 13:26:22 +03:00
|
|
|
this._preloadDialog.open(
|
|
|
|
aURL,
|
2021-01-27 14:33:57 +03:00
|
|
|
{
|
|
|
|
features,
|
|
|
|
closingCallback,
|
|
|
|
closedCallback,
|
|
|
|
sizeTo,
|
|
|
|
},
|
2020-09-02 17:24:47 +03:00
|
|
|
...aParams
|
2020-08-17 13:26:22 +03:00
|
|
|
);
|
|
|
|
|
2020-09-02 17:24:58 +03:00
|
|
|
let openedDialog = this._preloadDialog;
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
this._preloadDialog = new SubDialog({
|
|
|
|
template: this._dialogTemplate,
|
|
|
|
parentElement: this._dialogStack,
|
2020-08-17 13:26:22 +03:00
|
|
|
id: SubDialogManager._nextDialogID++,
|
2020-07-10 03:01:50 +03:00
|
|
|
dialogOptions: this._dialogOptions,
|
2017-06-05 12:00:47 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
if (this._dialogs.length == 1) {
|
|
|
|
this._ensureStackEventListeners();
|
|
|
|
}
|
2020-09-02 17:24:58 +03:00
|
|
|
|
|
|
|
return openedDialog;
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
|
|
|
|
close() {
|
|
|
|
this._topDialog.close();
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
|
2020-09-17 05:18:50 +03:00
|
|
|
/**
|
2020-11-04 14:03:48 +03:00
|
|
|
* Hides the dialog stack for a specific browser, without actually destroying
|
|
|
|
* frames for stuff within it.
|
|
|
|
*
|
2020-09-17 05:18:50 +03:00
|
|
|
* @param aBrowser - The browser associated with the tab dialog.
|
|
|
|
*/
|
|
|
|
hideDialog(aBrowser) {
|
|
|
|
aBrowser.removeAttribute("tabDialogShowing");
|
2020-11-04 14:03:48 +03:00
|
|
|
this._dialogStack.classList.add("temporarilyHidden");
|
2020-09-17 05:18:50 +03:00
|
|
|
}
|
|
|
|
|
2020-09-02 17:24:58 +03:00
|
|
|
/**
|
|
|
|
* Abort open dialogs.
|
|
|
|
* @param {function} [filterFn] - Function which should return true for
|
|
|
|
* dialogs that should be aborted and false for dialogs that should remain
|
|
|
|
* open. Defaults to aborting all dialogs.
|
|
|
|
*/
|
|
|
|
abortDialogs(filterFn = () => true) {
|
|
|
|
this._dialogs.filter(filterFn).forEach(dialog => dialog.abort());
|
2020-08-17 13:26:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get hasDialogs() {
|
|
|
|
if (!this._dialogs.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return this._dialogs.some(dialog => !dialog._isClosing);
|
|
|
|
}
|
|
|
|
|
2021-04-08 19:50:01 +03:00
|
|
|
get dialogs() {
|
|
|
|
return [...this._dialogs];
|
|
|
|
}
|
|
|
|
|
2020-08-28 15:36:00 +03:00
|
|
|
focusTopDialog() {
|
|
|
|
this._topDialog?.focus();
|
|
|
|
}
|
|
|
|
|
2017-06-05 12:00:47 +03:00
|
|
|
handleEvent(aEvent) {
|
|
|
|
switch (aEvent.type) {
|
|
|
|
case "dialogopen": {
|
2020-08-17 13:26:22 +03:00
|
|
|
this._onDialogOpen(aEvent.detail.dialog);
|
2017-06-05 12:00:47 +03:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "dialogclose": {
|
|
|
|
this._onDialogClose(aEvent.detail.dialog);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
_onDialogOpen(dialog) {
|
|
|
|
let lowerDialogs = [];
|
2021-01-27 14:33:57 +03:00
|
|
|
if (dialog == this._topDialog) {
|
|
|
|
dialog.focus(true);
|
|
|
|
} else {
|
2020-08-17 13:26:22 +03:00
|
|
|
// Opening dialog is not on top, hide it
|
|
|
|
lowerDialogs.push(dialog);
|
|
|
|
}
|
|
|
|
|
|
|
|
// For stack order, hide the previous top
|
2021-01-27 14:33:57 +03:00
|
|
|
if (
|
|
|
|
this._dialogs.length &&
|
|
|
|
this._orderType === SubDialogManager.ORDER_STACK
|
|
|
|
) {
|
2020-08-17 13:26:22 +03:00
|
|
|
let index = this._dialogs.indexOf(dialog);
|
|
|
|
if (index > 0) {
|
|
|
|
lowerDialogs.push(this._dialogs[index - 1]);
|
|
|
|
}
|
2020-07-09 13:10:26 +03:00
|
|
|
}
|
2020-07-09 21:09:25 +03:00
|
|
|
|
2020-08-17 13:26:22 +03:00
|
|
|
lowerDialogs.forEach(d => {
|
|
|
|
if (d._overlay.hasAttribute("topmost")) {
|
|
|
|
d._overlay.removeAttribute("topmost");
|
|
|
|
d._removeDialogEventListeners(false);
|
|
|
|
}
|
|
|
|
});
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
|
|
|
|
_onDialogClose(dialog) {
|
2020-08-17 13:26:22 +03:00
|
|
|
this._dialogs.splice(this._dialogs.indexOf(dialog), 1);
|
2017-06-05 12:00:47 +03:00
|
|
|
|
|
|
|
if (this._topDialog) {
|
2020-07-09 21:09:25 +03:00
|
|
|
// The prevActiveElement is only set for stacked dialogs
|
2021-01-27 14:33:57 +03:00
|
|
|
if (this._topDialog._prevActiveElement) {
|
|
|
|
this._topDialog._prevActiveElement.focus();
|
|
|
|
} else {
|
|
|
|
this._topDialog.focus(true);
|
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
this._topDialog._overlay.setAttribute("topmost", true);
|
2020-08-17 13:26:22 +03:00
|
|
|
this._topDialog._addDialogEventListeners(false);
|
2020-09-17 05:18:50 +03:00
|
|
|
this._dialogStack.hidden = false;
|
2020-11-04 14:03:48 +03:00
|
|
|
this._dialogStack.classList.remove("temporarilyHidden");
|
2017-06-05 12:00:47 +03:00
|
|
|
} else {
|
2020-07-10 03:01:50 +03:00
|
|
|
// We have closed the last dialog, do cleanup.
|
2019-12-17 21:19:34 +03:00
|
|
|
this._topLevelPrevActiveElement.focus();
|
2017-06-05 12:00:47 +03:00
|
|
|
this._dialogStack.hidden = true;
|
|
|
|
this._removeStackEventListeners();
|
|
|
|
}
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
|
|
|
|
_ensureStackEventListeners() {
|
|
|
|
this._dialogStack.addEventListener("dialogopen", this);
|
|
|
|
this._dialogStack.addEventListener("dialogclose", this);
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
2017-06-05 12:00:47 +03:00
|
|
|
|
|
|
|
_removeStackEventListeners() {
|
|
|
|
this._dialogStack.removeEventListener("dialogopen", this);
|
|
|
|
this._dialogStack.removeEventListener("dialogclose", this);
|
2020-07-10 03:01:50 +03:00
|
|
|
}
|
|
|
|
}
|
2020-07-09 21:09:25 +03:00
|
|
|
|
|
|
|
// Used for the SubDialogManager orderType option.
|
|
|
|
SubDialogManager.ORDER_STACK = 0;
|
|
|
|
SubDialogManager.ORDER_QUEUE = 1;
|
2020-08-17 13:26:22 +03:00
|
|
|
|
|
|
|
SubDialogManager._nextDialogID = 0;
|