Source

client/ui/containers/wizardPanel.js

/**
 * 
 * The Wizard Panel derives from [Panel](kiss.ui.Panel.html).
 * 
 * It's a panel where items are displayed one at a time (each wizard page) with helper buttons (next, previous) to navigate through the pages.
 * The panel title is updated with the current page number.
 * 
 * @param {object} config
 * @param {function} config.action - Action triggered when the last page of the wizard is validated. The function is called with the wizard panel as context, so that this.getData() can be used to get the data of all fields of the wizard.
 * @param {object} [config.actionText] - Text of the action button of the last page, like "Done", "Proceed", "Let's go". Default = "OK"
 * @param {boolean} [config.pageValidation] - If true, validate each page when navigating next/previous. Default = false
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-wizardpanel class="a-panel">
 *  <div class="panel-header">
 *      <span class="panel-icon"></span>
 *      <span class="panel-title"></span>
 *      <span class="panel-custom-buttons"></span>
 *      <span class="panel-button-expand-collapse"></span>
 *      <span class="panel-button-maximize"></span>
 *      <span class="panel-button-close"></span>
 *  </div>
 *  <div class="panel-body">
 *      <!-- Panel items are inserted here -->
 *  </div>
 * </a-wizardpanel>
 * ```
 * 
 */
kiss.ui.WizardPanel = class WizardPanel extends kiss.ui.Panel {
    /**
     * Its a Custom Web Component. Do not use the constructor directly with the **new** keyword.
     * Instead, use one of the 3 following methods:
     * 
     * Create the Web Component and call its **init** method:
     * ```
     * const myWizardPanel = document.createElement("a-wizardpanel").init(config)
     * ```
     * 
     * Or use the shorthand for it:
     * ```
     * const myWizardPanel = createWizardPanel({
     * 
     *   // Can have the same config properties as a panel
     *   title: "Setup"
     *   icon: "fas fa-wrench",
     *   headerBackgroundColor: "#00aaee",
     *   closable: true,
     *   draggable: true,
     *   modal: true,
     *   display: "flex"
     *   flexFlow: "column",
     *   padding: "10px",
     * 
     *   // Wizard pages
     *   items: [
     *      wizardPage1,
     *      wizardPage2,
     *      wizardPage3
     *   ],
     *   actionText: "Proceed",
     *   action: function() {
     *      // Get the data of all fields of the wizard
     *      const data = this.getData()
     *     // Do something with the data
     *   }
     * })
     * 
     * myWizardPanel.render()
     * ```
     * 
     * Or directly declare the config inside a container component:
     * ```
     * const myBlock = createBlock({
     *   items: [
     *       {
     *           type: "wizardpanel",
     *           title: "Foo",
     *           items: [
     *               wizardPage1,
     *               wizardPage2,
     *               wizardPage3
     *           ],
     *           actionText: "Proceed",
     *           action: function() {
     *              // Get the data of all fields of the wizard
     *              const data = this.getData()
     *              // Do something with the data
     *           }
     *       }
     *   ]
     * })
     * myBlock.render()
     * ```
     * 
     * If you need to validate a page before navigating to the next one, you can add a **validate** method to the page:
     * ```
     * const wizardPage1 = {
     *  type: "panel", // or "block"
     *  items: [
     *      // Page items
     *  ],
     *  methods: {
     *     validate: function() {
     *       // Validate the page
     *       return true // or false
     *     }
     *  }
     * }
     * 
     * Use this in combination with "pageValidation" property in the wizard panel config.
     * If you don't need a specific validation, "pageValidation" will validate all the pages as normal forms, checking for validation rules of each field.
     * ```
     * 
     */
    constructor() {
        super()
    }

    /**
     * Generates a Wizard Panel from a JSON config
     * 
     * @ignore
     * @param {object} config - JSON config
     * @returns {HTMLElement}
     */
    init(config) {
        config.id = config.id || "cmp-" + (kiss.global.componentCount++).toString()
        this.id = config.id
        this.currentPage = 0
        this.numberOfPages = config.items.length
        this.pageValidation = !!config.pageValidation

        this._initButtons(config)
        config.items = this._initStructure(config)

        super.init(config)
        this._updateTitle()

        this.classList.add("a-panel")
        return this
    }

    /**
     * Manage click event in the panel's header to perform various actions like "close", "expand", "collapse"...
     * 
     * @private
     * @ignore
     */
    _initHeaderClickEvent() {
        this.panelHeader.onclick = function(event) {
            const element = event.target
            let panel = element.closest("a-wizardpanel")

            if (element.classList.contains("panel-button-close")) {
                panel.close()
            }
            else if (element.classList.contains("panel-button-expand")) {
                panel.maximize(20)
            }
            else if (element.classList.contains("panel-button-expand-collapse") || element.classList.contains("panel-header-collapsible")) {
                panel.expandCollapse()
            }
            else if ((element.classList.contains("panel-title") || element.classList.contains("panel-icon")) && panel.config.collapsible === true && panel.config.draggable !== true) {
                panel.expandCollapse()
            }
        }
    }    

    /**
     * Initialize the DOM structure of the wizard panel:
     * - original items are inserted into "pages" block
     * - a button bar is added to the bottom of the panel to navigate between pages
     * 
     * @private
     * @ignore
     * @param {object} config 
     * @returns {object} The final structure
     */
    _initStructure(config) {
        const items = [
            {
                id: this.id + "-pages",
                multiview: true,
                items: config.items
            },
            {
                id: this.id + "-buttons",
                layout: "horizontal",
                defaultConfig: {
                    type: "button",
                    margin: "1rem 0.5rem 0 0",
                    height: "4rem",
                    flex: 1
                },
                items: [
                    this.buttonCancel,
                    (this.numberOfPages > 1) ? this.buttonNext : this.buttonOK
                ]
            }
        ]
        return items
    }

    /**
     * Initialize the buttons of the wizard panel:
     * - cancel
     * - previous / next
     * - validate
     * 
     * @private
     * @ignore
     * @param {object} config 
     */
    _initButtons(config) {
        this.buttonCancel = {
            icon: "fas fa-times",
            text: txtTitleCase("cancel"),
            action: function () {
                this.closest("a-wizardpanel").close()
            }
        }

        this.buttonPrevious = {
            icon: "fas fa-chevron-left",
            text: txtTitleCase("previous"),
            action: function () {
                this.closest("a-wizardpanel").previous()
            }
        }

        this.buttonNext = {
            icon: "fas fa-chevron-right",
            iconPosition: "right",
            text: txtTitleCase("next"),
            action: function () {
                this.closest("a-wizardpanel").next()
            }
        }             

        this.buttonOK = {
            icon: "fas fa-check",
            text: config.actionText || "OK",
            action: () => {
                if (this.pageValidation && !this.validatePage()) return

                // this = wizard panel, which allows to reference the wizard panel inside the action
                config.action.bind(this)() 
            }
        }     
    }

    /**
     * Update the buttons when navigating between pages
     * 
     * @private
     * @ignore
     */
    _updateButtons() {
        let buttons
        if (this.currentPage == 0) {
            buttons = [this.buttonCancel, (this.numberOfPages > 1) ? this.buttonNext : this.buttonOK]
        }
        else if (this.currentPage == this.numberOfPages - 1) {
            buttons = [this.buttonPrevious, this.buttonOK]
        }
        else {
            buttons = [this.buttonPrevious, this.buttonNext]
        }
        $(this.id + "-buttons").setItems(buttons)
    }

    /**
     * Update the title of the wizard panel with the current page number
     * 
     * @private
     * @ignore
     */
    _updateTitle() {
        this.setTitle((this.currentPage + 1) + "/" + this.numberOfPages + " - " + this.config.title)
    }

    /**
     * Validates the form of a wizard page.
     * Prevents from navigating to the next page if the form is not validated.
     * 
     * @param {number} [pageIndex] - Optional wizard's page to validate. If not specified, tries to validate the current page.
     */
    validatePage(pageIndex) {
        this.pages = $(this.id + "-pages").children
        if (!this.pages) return true
        const currentPage = this.pages[pageIndex || this.currentPage]
        return (currentPage.validate) ? currentPage.validate() : true
    }

    /**
     * Navigate to the next wizard page
     */
    next() {
        if (this.pageValidation && !this.validatePage()) return

        this.currentPage++
        this._updateButtons()
        this._updateTitle()

        $(this.id + "-pages").showItem(this.currentPage, {
            name: "slideInRight",
            speed: "faster"
        })
    }

    /**
     * Navigate to the previous wizard page
     */
    previous() {
        this.currentPage--
        this._updateButtons()
        this._updateTitle()
        
        $(this.id + "-pages").showItem(this.currentPage, {
            name: "slideInLeft",
            speed: "faster"
        })
    }
}

// Create a Custom Element and add a shortcut to create it
customElements.define("a-wizardpanel", kiss.ui.WizardPanel)

/**
 * Shorthand to create a new Wizard Panel. See [kiss.ui.WizardPanel](kiss.ui.WizardPanel.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createWizardPanel = (config) => document.createElement("a-wizardpanel").init(config)

;