/* 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 chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const XUL_NS =
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// Note: MozWizard currently supports adding, but not removing MozWizardPage
// children.
class MozWizard
extends MozXULElement {
constructor() {
super();
// About this._accessMethod:
// There are two possible access methods: "sequential" and "random".
// "sequential" causes the MozWizardPage's to be displayed in the order
// that they are added to the DOM.
// The "random" method name is a bit misleading since the pages aren't
// displayed in a random order. Instead, each MozWizardPage must have
// a "next" attribute containing the id of the MozWizardPage that should
// be loaded next.
this._accessMethod =
null;
this._currentPage =
null;
this._canAdvance =
true;
this._canRewind =
false;
this._hasLoaded =
false;
this._hasStarted =
false;
// Whether any MozWizardPage has been shown yet
this._wizardButtonsReady =
false;
this.pageCount = 0;
this._pageStack = [];
this._bundle = Services.strings.createBundle(
"chrome://global/locale/wizard.properties"
);
this.addEventListener(
"keypress",
event => {
if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
this._hitEnter(event);
}
else if (
event.keyCode == KeyEvent.DOM_VK_ESCAPE &&
!event.defaultPrevented
) {
this.cancel();
}
},
{ mozSystemGroup:
true }
);
/*
XXX(ntim): We import button.css here for the wizard-buttons children
This won't be needed after bug 1624888.
*/
this.attachShadow({ mode:
"open" }).appendChild(
MozXULElement.parseXULToFragment(`
<html:link rel=
"stylesheet" href=
"chrome://global/skin/button.css"/>
<html:link rel=
"stylesheet" href=
"chrome://global/skin/wizard.css"/>
<hbox
class=
"wizard-header"></hbox>
<html:slot name=
"wizardpage" class=
"wizard-page-box" style=
"display: grid; flex: 1;"/>
<html:slot/>
<wizard-buttons
class=
"wizard-buttons"></wizard-buttons>
`)
);
this.initializeAttributeInheritance();
this._wizardButtons =
this.shadowRoot.querySelector(
".wizard-buttons");
this._wizardHeader =
this.shadowRoot.querySelector(
".wizard-header");
this._wizardHeader.appendChild(
MozXULElement.parseXULToFragment(
AppConstants.platform ==
"macosx"
? `<stack
class=
"wizard-header-stack" flex=
"1">
<vbox
class=
"wizard-header-box-1">
<vbox
class=
"wizard-header-box-text">
<label id=
"wizard-header-label" class=
"wizard-header-label"/>
</vbox>
</vbox>
<hbox
class=
"wizard-header-box-icon">
<spacer flex=
"1"/>
<image
class=
"wizard-header-icon"/>
</hbox>
</stack>`
: `<hbox
class=
"wizard-header-box-1" flex=
"1">
<vbox
class=
"wizard-header-box-text" flex=
"1">
<label id=
"wizard-header-label" class=
"wizard-header-label"/>
<label
class=
"wizard-header-description"/>
</vbox>
<image
class=
"wizard-header-icon"/>
</hbox>`
)
);
}
static get inheritedAttributes() {
return {
".wizard-buttons":
"pagestep,firstpage,lastpage",
};
}
connectedCallback() {
if (document.l10n) {
document.l10n.connectRoot(
this.shadowRoot);
}
document.documentElement.setAttribute(
"role",
"dialog");
document.documentElement.classList.add(
"wizard-window");
this._maybeStartWizard();
window.addEventListener(
"close", event => {
if (
this.cancel()) {
event.preventDefault();
}
});
// Give focus to the first focusable element in the wizard, do it after
// onload completes, see bug 103197.
window.addEventListener(
"load", () =>
window.setTimeout(() => {
this._hasLoaded =
true;
if (!document.commandDispatcher.focusedElement) {
document.commandDispatcher.advanceFocusIntoSubtree(
this);
}
try {
let button =
this._wizardButtons.defaultButton;
if (button) {
window.notifyDefaultButtonLoaded(button);
}
}
catch (e) {}
}, 0)
);
}
set title(val) {
document.title = val;
}
get title() {
return document.title;
}
set canAdvance(val) {
this.getButton(
"next").disabled = !val;
this._canAdvance = val;
}
get canAdvance() {
return this._canAdvance;
}
set canRewind(val) {
this.getButton(
"back").disabled = !val;
this._canRewind = val;
}
get canRewind() {
return this._canRewind;
}
get pageStep() {
return this._pageStack.length;
}
get wizardPages() {
return this.getElementsByTagNameNS(XUL_NS,
"wizardpage");
}
set currentPage(val) {
if (!val) {
return;
}
this._currentPage?.classList.remove(
"selected");
val.classList.add(
"selected");
this._currentPage = val;
// Setting this attribute allows wizard's clients to dynamically
// change the styles of each page based on purpose of the page.
this.setAttribute(
"currentpageid", val.pageid);
this._initCurrentPage();
this._advanceFocusToPage(val);
this._fireEvent(val,
"pageshow");
}
get currentPage() {
return this._currentPage;
}
set pageIndex(val) {
if (val < 0 || val >=
this.pageCount) {
return;
}
var page =
this.wizardPages[val];
this._pageStack[
this._pageStack.length - 1] = page;
this.currentPage = page;
}
get pageIndex() {
return this._currentPage ?
this._currentPage.pageIndex : -1;
}
get onFirstPage() {
return this._pageStack.length == 1;
}
get onLastPage() {
var cp =
this.currentPage;
return (
cp &&
((
this._accessMethod ==
"sequential" &&
cp.pageIndex ==
this.pageCount - 1) ||
(
this._accessMethod ==
"random" && !cp.next))
);
}
getButton(aDlgType) {
return this._wizardButtons.getButton(aDlgType);
}
getPageById(aPageId) {
var els =
this.getElementsByAttribute(
"pageid", aPageId);
return els.item(0);
}
extra1() {
if (
this.currentPage) {
this._fireEvent(
this.currentPage,
"extra1");
}
}
extra2() {
if (
this.currentPage) {
this._fireEvent(
this.currentPage,
"extra2");
}
}
rewind() {
if (!
this.canRewind) {
return;
}
if (
this.currentPage && !
this._fireEvent(
this.currentPage,
"pagehide")) {
return;
}
if (
this.currentPage &&
!
this._fireEvent(
this.currentPage,
"pagerewound")
) {
return;
}
if (!
this._fireEvent(
this,
"wizardback")) {
return;
}
this._pageStack.pop();
this.currentPage =
this._pageStack[
this._pageStack.length - 1];
this.setAttribute(
"pagestep",
this._pageStack.length);
}
advance(aPageId) {
if (!
this.canAdvance) {
return;
}
if (
this.currentPage && !
this._fireEvent(
this.currentPage,
"pagehide")) {
return;
}
if (
this.currentPage &&
!
this._fireEvent(
this.currentPage,
"pageadvanced")
) {
return;
}
if (
this.onLastPage && !aPageId) {
if (
this._fireEvent(
this,
"wizardfinish")) {
window.setTimeout(
function () {
window.close();
}, 1);
}
}
else {
if (!
this._fireEvent(
this,
"wizardnext")) {
return;
}
let page;
if (aPageId) {
page =
this.getPageById(aPageId);
}
else if (
this.currentPage) {
if (
this._accessMethod ==
"random") {
page =
this.getPageById(
this.currentPage.next);
}
else {
page =
this.wizardPages[
this.currentPage.pageIndex + 1];
}
}
else {
page =
this.wizardPages[0];
}
if (page) {
this._pageStack.push(page);
this.setAttribute(
"pagestep",
this._pageStack.length);
this.currentPage = page;
}
}
}
goTo(aPageId) {
var page =
this.getPageById(aPageId);
if (page) {
this._pageStack[
this._pageStack.length - 1] = page;
this.currentPage = page;
}
}
cancel() {
if (!
this._fireEvent(
this,
"wizardcancel")) {
return true;
}
window.close();
window.setTimeout(
function () {
window.close();
}, 1);
return false;
}
_initCurrentPage() {
this.canRewind = !
this.onFirstPage;
this.setAttribute(
"firstpage", String(
this.onFirstPage));
if (AppConstants.platform ==
"linux") {
this.getButton(
"back").hidden =
this.onFirstPage;
}
if (
this.onLastPage) {
this.canAdvance =
true;
this.setAttribute(
"lastpage",
"true");
}
else {
this.setAttribute(
"lastpage",
"false");
}
this._adjustWizardHeader();
this._wizardButtons.onPageChange();
}
_advanceFocusToPage() {
if (!
this._hasLoaded) {
return;
}
// XXX: it'd be correct to advance focus into the panel, however we can't do
// it until bug 1558990 is fixed, so moving the focus into a wizard itsef
// as a workaround - it's same behavior but less optimal.
document.commandDispatcher.advanceFocusIntoSubtree(
this);
// if advanceFocusIntoSubtree tries to focus one of our
// dialog buttons, then remove it and put it on the root
var focused = document.commandDispatcher.focusedElement;
if (focused && focused.hasAttribute(
"dlgtype")) {
this.focus();
}
}
_registerPage(aPage) {
aPage.pageIndex =
this.pageCount;
this.pageCount += 1;
if (!
this._accessMethod) {
this._accessMethod = aPage.next ?
"random" :
"sequential";
}
if (!
this._maybeStartWizard() &&
this._hasStarted) {
// If the wizard has already started, adding a page might require
// updating elements to reflect that (ex: changing the Finish button to
// the Next button).
this._initCurrentPage();
}
}
_onWizardButtonsReady() {
this._wizardButtonsReady =
true;
this._maybeStartWizard();
}
_maybeStartWizard() {
if (
!
this._hasStarted &&
this.isConnected &&
this._wizardButtonsReady &&
this.pageCount > 0
) {
this._hasStarted =
true;
this.advance();
return true;
}
return false;
}
_adjustWizardHeader() {
let labelElement =
this._wizardHeader.querySelector(
".wizard-header-label"
);
// First deal with fluent. Ideally, we'd stop supporting anything else,
// but some comm-central consumers still use DTDs. (bug 1627049).
// Removing the DTD support is bug 1627051.
if (
this.currentPage.hasAttribute(
"data-header-label-id")) {
let id =
this.currentPage.getAttribute(
"data-header-label-id");
document.l10n.setAttributes(labelElement, id);
}
else {
// Otherwise, make sure we remove any fluent IDs leftover:
if (labelElement.hasAttribute(
"data-l10n-id")) {
labelElement.removeAttribute(
"data-l10n-id");
}
// And use the label attribute or the default:
var label =
this.currentPage.getAttribute(
"label") ||
"";
if (!label &&
this.onFirstPage &&
this._bundle) {
if (AppConstants.platform ==
"macosx") {
label =
this._bundle.GetStringFromName(
"default-first-title-mac");
}
else {
label =
this._bundle.formatStringFromName(
"default-first-title", [
this.title,
]);
}
}
else if (!label &&
this.onLastPage &&
this._bundle) {
if (AppConstants.platform ==
"macosx") {
label =
this._bundle.GetStringFromName(
"default-last-title-mac");
}
else {
label =
this._bundle.formatStringFromName(
"default-last-title", [
this.title,
]);
}
}
labelElement.textContent = label;
}
let headerDescEl =
this._wizardHeader.querySelector(
".wizard-header-description"
);
if (headerDescEl) {
headerDescEl.textContent =
this.currentPage.getAttribute(
"description");
}
}
_hitEnter(evt) {
if (!evt.defaultPrevented) {
this.advance();
}
}
_fireEvent(aTarget, aType) {
var event = document.createEvent(
"Events");
event.initEvent(aType,
true,
true);
// handle dom event handlers
return aTarget.dispatchEvent(event);
}
}
customElements.define(
"wizard", MozWizard);
class MozWizardPage
extends MozXULElement {
constructor() {
super();
this.pageIndex = -1;
}
connectedCallback() {
this.setAttribute(
"slot",
"wizardpage");
let wizard =
this.closest(
"wizard");
if (wizard) {
wizard._registerPage(
this);
}
}
get pageid() {
return this.getAttribute(
"pageid");
}
set pageid(val) {
this.setAttribute(
"pageid", val);
}
get next() {
return this.getAttribute(
"next");
}
set next(val) {
this.setAttribute(
"next", val);
this.parentNode._accessMethod =
"random";
}
}
customElements.define(
"wizardpage", MozWizardPage);
class MozWizardButtons
extends MozXULElement {
connectedCallback() {
this._wizard =
this.getRootNode().host;
this.textContent =
"";
this.appendChild(
this.constructor.fragment);
MozXULElement.insertFTLIfNeeded(
"toolkit/global/wizard.ftl");
this._wizardButtonDeck =
this.querySelector(
".wizard-next-deck");
this.initializeAttributeInheritance();
const listeners = [
[
"back", () =>
this._wizard.rewind()],
[
"next", () =>
this._wizard.advance()],
[
"finish", () =>
this._wizard.advance()],
[
"cancel", () =>
this._wizard.cancel()],
[
"extra1", () =>
this._wizard.extra1()],
[
"extra2", () =>
this._wizard.extra2()],
];
for (let [name, listener] of listeners) {
let btn =
this.getButton(name);
if (btn) {
btn.addEventListener(
"command", listener);
}
}
this._wizard._onWizardButtonsReady();
}
static get inheritedAttributes() {
return AppConstants.platform ==
"macosx"
? {
"[dlgtype='next']":
"hidden=lastpage",
}
:
null;
}
static get markup() {
if (AppConstants.platform ==
"macosx") {
return `
<vbox flex=
"1">
<hbox
class=
"wizard-buttons-btm">
<button
class=
"wizard-button" dlgtype=
"extra1" hidden=
"true"/>
<button
class=
"wizard-button" dlgtype=
"extra2" hidden=
"true"/>
<button data-l10n-id=
"wizard-macos-button-cancel"
class=
"wizard-button" dlgtype=
"cancel"/>
<spacer flex=
"1"/>
<button data-l10n-id=
"wizard-macos-button-back"
class=
"wizard-button wizard-nav-button" dlgtype=
"back"/>
<button data-l10n-id=
"wizard-macos-button-next"
class=
"wizard-button wizard-nav-button" dlgtype=
"next"
default=
"true" />
<button data-l10n-id=
"wizard-macos-button-finish" class=
"wizard-button"
dlgtype=
"finish" default=
"true" />
</hbox>
</vbox>`;
}
let buttons =
AppConstants.platform ==
"linux"
? `
<button data-l10n-id=
"wizard-linux-button-cancel"
class=
"wizard-button"
dlgtype=
"cancel"/>
<spacer style=
"width: 24px;"/>
<button data-l10n-id=
"wizard-linux-button-back"
class=
"wizard-button" dlgtype=
"back"/>
<deck
class=
"wizard-next-deck">
<hbox>
<button data-l10n-id=
"wizard-linux-button-finish"
class=
"wizard-button"
dlgtype=
"finish" default=
"true" flex=
"1"/>
</hbox>
<hbox>
<button data-l10n-id=
"wizard-linux-button-next"
class=
"wizard-button" dlgtype=
"next"
default=
"true" flex=
"1"/>
</hbox>
</deck>`
: `
<button data-l10n-id=
"wizard-win-button-back"
class=
"wizard-button" dlgtype=
"back"/>
<deck
class=
"wizard-next-deck">
<hbox>
<button data-l10n-id=
"wizard-win-button-finish"
class=
"wizard-button"
dlgtype=
"finish" default=
"true" flex=
"1"/>
</hbox>
<hbox>
<button data-l10n-id=
"wizard-win-button-next"
class=
"wizard-button" dlgtype=
"next"
default=
"true" flex=
"1"/>
</hbox>
</deck>
<button data-l10n-id=
"wizard-win-button-cancel"
class=
"wizard-button"
dlgtype=
"cancel"/>`;
return `
<vbox
class=
"wizard-buttons-box-1" flex=
"1">
<separator
class=
"wizard-buttons-separator groove"/>
<hbox
class=
"wizard-buttons-box-2">
<button
class=
"wizard-button" dlgtype=
"extra1" hidden=
"true"/>
<button
class=
"wizard-button" dlgtype=
"extra2" hidden=
"true"/>
<spacer flex=
"1" anonid=
"spacer"/>
${buttons}
</hbox>
</vbox>`;
}
onPageChange() {
if (AppConstants.platform ==
"macosx") {
this.getButton(
"finish").hidden = !(
this.getAttribute(
"lastpage") ==
"true"
);
}
else if (
this.getAttribute(
"lastpage") ==
"true") {
this._wizardButtonDeck.selectedIndex = 0;
}
else {
this._wizardButtonDeck.selectedIndex = 1;
}
}
getButton(type) {
return this.querySelector(`[dlgtype=
"${type}"]`);
}
get defaultButton() {
let buttons =
this._wizardButtonDeck.selectedPanel.getElementsByTagNameNS(
XUL_NS,
"button"
);
for (let i = 0; i < buttons.length; i++) {
if (
buttons[i].getAttribute(
"default") ==
"true" &&
!buttons[i].hidden &&
!buttons[i].disabled
) {
return buttons[i];
}
}
return null;
}
}
customElements.define(
"wizard-buttons", MozWizardButtons);
}