/* 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(` `) ); this.initializeAttributeInheritance(); this._wizardButtons = this.shadowRoot.querySelector(".wizard-buttons"); this._wizardHeader = this.shadowRoot.querySelector(".wizard-header"); this._wizardHeader.appendChild( MozXULElement.parseXULToFragment( AppConstants.platform == "macosx" ? ` ` : ` ` ) ); } 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 `