summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/wizard.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/wizard.js')
-rw-r--r--toolkit/content/widgets/wizard.js651
1 files changed, 651 insertions, 0 deletions
diff --git a/toolkit/content/widgets/wizard.js b/toolkit/content/widgets/wizard.js
new file mode 100644
index 0000000000..6eb4bcb517
--- /dev/null
+++ b/toolkit/content/widgets/wizard.js
@@ -0,0 +1,651 @@
+/* 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 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 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(aPage) {
+ 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 == "" ? "sequential" : "random";
+ }
+ 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);
+}