Source code

Revision control

Copy as Markdown

Other Tools

/* 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 =
// 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);
}