UNPKG

preline

Version:

Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.

948 lines (737 loc) 24.3 kB
/* * HSStepper * @version: 3.2.2 * @author: Preline Labs Ltd. * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html) * Copyright 2024 Preline Labs Ltd. */ import { dispatch } from "../../utils"; import { IStepper, IStepperItem, IStepperOptions } from "./interfaces"; import HSBasePlugin from "../base-plugin"; import { ICollectionItem } from "../../interfaces"; class HSStepper extends HSBasePlugin<{}> implements IStepper { private currentIndex: number | null; private readonly mode: string | null; private isCompleted: boolean | null; private totalSteps: number | null; private navItems: IStepperItem[] | null; private contentItems: IStepperItem[] | null; private backBtn: HTMLElement | null; private nextBtn: HTMLElement | null; private skipBtn: HTMLElement | null; private completeStepBtn: HTMLElement | null; private completeStepBtnDefaultText: string | null; private finishBtn: HTMLElement | null; private resetBtn: HTMLElement | null; private onNavItemClickListener: | { el: HTMLElement; fn: () => void; }[] | null; private onBackClickListener: () => void; private onNextClickListener: () => void; private onSkipClickListener: () => void; private onCompleteStepBtnClickListener: () => void; private onFinishBtnClickListener: () => void; private onResetBtnClickListener: () => void; constructor(el: HTMLElement, options?: IStepperOptions) { super(el, options); const data = el.getAttribute("data-hs-stepper"); const dataOptions: IStepperOptions = data ? JSON.parse(data) : {}; const concatOptions = { ...dataOptions, ...options, }; this.currentIndex = concatOptions?.currentIndex || 1; this.mode = concatOptions?.mode || "linear"; this.isCompleted = typeof concatOptions?.isCompleted !== "undefined" ? concatOptions?.isCompleted : false; this.totalSteps = 1; this.navItems = []; this.contentItems = []; this.onNavItemClickListener = []; this.init(); } private navItemClick(item: IStepperItem) { this.handleNavItemClick(item); } private backClick() { this.handleBackButtonClick(); if (this.mode === "linear") { const currentNavItem = this.navItems.find( ({ index }) => index === this.currentIndex, ); const currentContentItem = this.contentItems.find( ({ index }) => index === this.currentIndex, ); if (!currentNavItem || !currentContentItem) return; if (currentNavItem.isCompleted) { currentNavItem.isCompleted = false; currentNavItem.isSkip = false; currentNavItem.el.classList.remove("success", "skipped"); } if (currentContentItem.isCompleted) { currentContentItem.isCompleted = false; currentContentItem.isSkip = false; currentContentItem.el.classList.remove("success", "skipped"); } if (this.mode === "linear" && this.currentIndex !== this.totalSteps) { if (this.nextBtn) this.nextBtn.style.display = ""; if (this.completeStepBtn) this.completeStepBtn.style.display = ""; } this.showSkipButton(); this.showFinishButton(); this.showCompleteStepButton(); } } private nextClick() { this.fireEvent("beforeNext", this.currentIndex); dispatch("beforeNext.hs.stepper", this.el, this.currentIndex); if (this.getNavItem(this.currentIndex)?.isProcessed) { this.disableAll(); return false; } this.goToNext(); } private skipClick() { this.handleSkipButtonClick(); if (this.mode === "linear" && this.currentIndex === this.totalSteps) { if (this.nextBtn) this.nextBtn.style.display = "none"; if (this.completeStepBtn) this.completeStepBtn.style.display = "none"; if (this.finishBtn) this.finishBtn.style.display = ""; } } private completeStepBtnClick() { this.handleCompleteStepButtonClick(); } private finishBtnClick() { this.fireEvent("beforeFinish", this.currentIndex); dispatch("beforeFinish.hs.stepper", this.el, this.currentIndex); if (this.getNavItem(this.currentIndex)?.isProcessed) { this.disableAll(); return false; } this.handleFinishButtonClick(); } private resetBtnClick() { this.handleResetButtonClick(); } private init() { this.createCollection(window.$hsStepperCollection, this); this.buildNav(); this.buildContent(); this.buildButtons(); this.setTotalSteps(); } private getUncompletedSteps(inIncludedSkipped: boolean = false) { return this.navItems.filter(({ isCompleted, isSkip }) => inIncludedSkipped ? !isCompleted || isSkip : !isCompleted && !isSkip ); } private setTotalSteps() { this.navItems.forEach((item) => { const { index } = item; if (index > this.totalSteps) this.totalSteps = index; }); } // Nav private buildNav() { this.el .querySelectorAll("[data-hs-stepper-nav-item]") .forEach((el) => this.addNavItem(el as HTMLElement)); this.navItems.forEach((item) => this.buildNavItem(item)); } private buildNavItem(item: IStepperItem) { const { index, isDisabled, el } = item; if (index === this.currentIndex) this.setCurrentNavItem(); if (this.mode !== "linear" || isDisabled) { this.onNavItemClickListener.push({ el, fn: () => this.navItemClick(item), }); el.addEventListener( "click", this.onNavItemClickListener.find((navItem) => navItem.el === el).fn, ); } } private addNavItem(el: HTMLElement) { const { index, isFinal = false, isCompleted = false, isSkip = false, isOptional = false, isDisabled = false, isProcessed = false, hasError = false, } = JSON.parse(el.getAttribute("data-hs-stepper-nav-item")); if (isCompleted) el.classList.add("success"); if (isSkip) el.classList.add("skipped"); if (isDisabled) { if (el.tagName === "BUTTON" || el.tagName === "INPUT") { el.setAttribute("disabled", "disabled"); } el.classList.add("disabled"); } if (hasError) el.classList.add("error"); this.navItems.push({ index, isFinal, isCompleted, isSkip, isOptional, isDisabled, isProcessed, hasError, el: el as HTMLElement, }); } private setCurrentNavItem() { this.navItems.forEach((item) => { const { index, el } = item; if (index === this.currentIndex) this.setCurrentNavItemActions(el); else this.unsetCurrentNavItemActions(el); }); } private setCurrentNavItemActions(el: HTMLElement) { el.classList.add("active"); this.fireEvent("active", this.currentIndex); dispatch("active.hs.stepper", this.el, this.currentIndex); } private getNavItem(n = this.currentIndex) { return this.navItems.find(({ index }) => index === n); } private setProcessedNavItemActions(item: IStepperItem) { item.isProcessed = true; item.el.classList.add("processed"); } private setErrorNavItemActions(item: IStepperItem) { item.hasError = true; item.el.classList.add("error"); } private unsetCurrentNavItemActions(el: HTMLElement) { el.classList.remove("active"); } private handleNavItemClick(item: IStepperItem) { const { index } = item; this.currentIndex = index; this.setCurrentNavItem(); this.setCurrentContentItem(); this.checkForTheFirstStep(); } // Content private buildContent() { this.el .querySelectorAll("[data-hs-stepper-content-item]") .forEach((el) => this.addContentItem(el as HTMLElement)); this.navItems.forEach((item) => this.buildContentItem(item)); } private buildContentItem(item: IStepperItem) { const { index } = item; if (index === this.currentIndex) this.setCurrentContentItem(); } private addContentItem(el: HTMLElement) { const { index, isFinal = false, isCompleted = false, isSkip = false, } = JSON.parse(el.getAttribute("data-hs-stepper-content-item")); if (isCompleted) el.classList.add("success"); if (isSkip) el.classList.add("skipped"); this.contentItems.push({ index, isFinal, isCompleted, isSkip, el: el as HTMLElement, }); } private setCurrentContentItem() { if (this.isCompleted) { const finalContentItem = this.contentItems.find(({ isFinal }) => isFinal); const otherContentItems = this.contentItems.filter( ({ isFinal }) => !isFinal, ); finalContentItem.el.style.display = ""; otherContentItems.forEach(({ el }) => (el.style.display = "none")); return false; } this.contentItems.forEach((item) => { const { index, el } = item; if (index === this.currentIndex) this.setCurrentContentItemActions(el); else this.unsetCurrentContentItemActions(el); }); } private hideAllContentItems() { this.contentItems.forEach(({ el }) => (el.style.display = "none")); } private setCurrentContentItemActions(el: HTMLElement) { el.style.display = ""; } private unsetCurrentContentItemActions(el: HTMLElement) { el.style.display = "none"; } private disableAll() { const currentNavItem = this.getNavItem(this.currentIndex); currentNavItem.hasError = false; currentNavItem.isCompleted = false; currentNavItem.isDisabled = false; currentNavItem.el.classList.remove("error", "success"); this.disableButtons(); } private disableNavItemActions(item: IStepperItem) { item.isDisabled = true; item.el.classList.add("disabled"); } private enableNavItemActions(item: IStepperItem) { item.isDisabled = false; item.el.classList.remove("disabled"); } // Buttons private buildButtons() { this.backBtn = this.el.querySelector("[data-hs-stepper-back-btn]"); this.nextBtn = this.el.querySelector("[data-hs-stepper-next-btn]"); this.skipBtn = this.el.querySelector("[data-hs-stepper-skip-btn]"); this.completeStepBtn = this.el.querySelector( "[data-hs-stepper-complete-step-btn]", ); this.finishBtn = this.el.querySelector("[data-hs-stepper-finish-btn]"); this.resetBtn = this.el.querySelector("[data-hs-stepper-reset-btn]"); this.buildBackButton(); this.buildNextButton(); this.buildSkipButton(); this.buildCompleteStepButton(); this.buildFinishButton(); this.buildResetButton(); } // back private buildBackButton() { if (!this.backBtn) return; this.checkForTheFirstStep(); this.onBackClickListener = () => this.backClick(); this.backBtn.addEventListener("click", this.onBackClickListener); } private handleBackButtonClick() { if (this.currentIndex === 1) return; if (this.mode === "linear") { this.removeOptionalClasses(); } this.currentIndex--; if (this.mode === "linear") { this.removeOptionalClasses(); } this.setCurrentNavItem(); this.setCurrentContentItem(); this.checkForTheFirstStep(); if (this.completeStepBtn) { this.changeTextAndDisableCompleteButtonIfStepCompleted(); } this.fireEvent("back", this.currentIndex); dispatch("back.hs.stepper", this.el, this.currentIndex); } private checkForTheFirstStep() { if (this.currentIndex === 1) { this.setToDisabled(this.backBtn); } else { this.setToNonDisabled(this.backBtn); } } private setToDisabled(el: HTMLElement) { if (el.tagName === "BUTTON" || el.tagName === "INPUT") { el.setAttribute("disabled", "disabled"); } el.classList.add("disabled"); } private setToNonDisabled(el: HTMLElement) { if (el.tagName === "BUTTON" || el.tagName === "INPUT") { el.removeAttribute("disabled"); } el.classList.remove("disabled"); } // next private buildNextButton() { if (!this.nextBtn) return; this.onNextClickListener = () => this.nextClick(); this.nextBtn.addEventListener("click", this.onNextClickListener); } private unsetProcessedNavItemActions(item: IStepperItem) { item.isProcessed = false; item.el.classList.remove("processed"); } private handleNextButtonClick(infinite = true) { if (infinite) { if (this.currentIndex === this.totalSteps) this.currentIndex = 1; else this.currentIndex++; } else { const nonCompletedSteps = this.getUncompletedSteps(); if (nonCompletedSteps.length === 1) { const { index } = nonCompletedSteps[0]; this.currentIndex = index; } else { if (this.currentIndex === this.totalSteps) return; this.currentIndex++; } } if (this.mode === "linear") { this.removeOptionalClasses(); } this.setCurrentNavItem(); this.setCurrentContentItem(); this.checkForTheFirstStep(); if (this.completeStepBtn) { this.changeTextAndDisableCompleteButtonIfStepCompleted(); } this.showSkipButton(); this.showFinishButton(); this.showCompleteStepButton(); this.fireEvent("next", this.currentIndex); dispatch("next.hs.stepper", this.el, this.currentIndex); } private removeOptionalClasses() { const currentNavItem = this.navItems.find( ({ index }) => index === this.currentIndex, ); const currentContentItem = this.contentItems.find( ({ index }) => index === this.currentIndex, ); currentNavItem.isSkip = false; currentNavItem.hasError = false; currentNavItem.isDisabled = false; currentContentItem.isSkip = false; currentNavItem.el.classList.remove("skipped", "success", "error"); currentContentItem.el.classList.remove("skipped", "success", "error"); } // skip private buildSkipButton() { if (!this.skipBtn) return; this.showSkipButton(); this.onSkipClickListener = () => this.skipClick(); this.skipBtn.addEventListener("click", this.onSkipClickListener); } private setSkipItem(n?: number) { const targetNavItem = this.navItems.find( ({ index }) => index === (n || this.currentIndex), ); const targetContentItem = this.contentItems.find( ({ index }) => index === (n || this.currentIndex), ); if (!targetNavItem || !targetContentItem) return; this.setSkipItemActions(targetNavItem); this.setSkipItemActions(targetContentItem); } private setSkipItemActions(item: IStepperItem) { item.isSkip = true; item.el.classList.add("skipped"); } private showSkipButton() { if (!this.skipBtn) return; const { isOptional } = this.navItems.find( ({ index }) => index === this.currentIndex, ); if (isOptional) this.skipBtn.style.display = ""; else this.skipBtn.style.display = "none"; } private handleSkipButtonClick() { this.setSkipItem(); this.handleNextButtonClick(); this.fireEvent("skip", this.currentIndex); dispatch("skip.hs.stepper", this.el, this.currentIndex); } // complete private buildCompleteStepButton() { if (!this.completeStepBtn) return; this.completeStepBtnDefaultText = this.completeStepBtn.innerText; this.onCompleteStepBtnClickListener = () => this.completeStepBtnClick(); this.completeStepBtn.addEventListener( "click", this.onCompleteStepBtnClickListener, ); } private changeTextAndDisableCompleteButtonIfStepCompleted() { const currentNavItem = this.navItems.find( ({ index }) => index === this.currentIndex, ); const { completedText } = JSON.parse( this.completeStepBtn.getAttribute("data-hs-stepper-complete-step-btn"), ); if (!currentNavItem) return; if (currentNavItem.isCompleted) { this.completeStepBtn.innerText = completedText || this.completeStepBtnDefaultText; this.completeStepBtn.setAttribute("disabled", "disabled"); this.completeStepBtn.classList.add("disabled"); } else { this.completeStepBtn.innerText = this.completeStepBtnDefaultText; this.completeStepBtn.removeAttribute("disabled"); this.completeStepBtn.classList.remove("disabled"); } } private setCompleteItem(n?: number) { const targetNavItem = this.navItems.find( ({ index }) => index === (n || this.currentIndex), ); const targetContentItem = this.contentItems.find( ({ index }) => index === (n || this.currentIndex), ); if (!targetNavItem || !targetContentItem) return; this.setCompleteItemActions(targetNavItem); this.setCompleteItemActions(targetContentItem); } private setCompleteItemActions(item: IStepperItem) { item.isCompleted = true; item.el.classList.add("success"); } private showCompleteStepButton() { if (!this.completeStepBtn) return; const nonCompletedSteps = this.getUncompletedSteps(); if (nonCompletedSteps.length === 1) { this.completeStepBtn.style.display = "none"; } else this.completeStepBtn.style.display = ""; } private handleCompleteStepButtonClick() { this.setCompleteItem(); this.fireEvent("complete", this.currentIndex); dispatch("complete.hs.stepper", this.el, this.currentIndex); this.handleNextButtonClick(false); this.showFinishButton(); this.showCompleteStepButton(); this.checkForTheFirstStep(); if (this.completeStepBtn) { this.changeTextAndDisableCompleteButtonIfStepCompleted(); } this.showSkipButton(); } // finish private buildFinishButton() { if (!this.finishBtn) return; if (this.isCompleted) { this.setCompleted(); } this.onFinishBtnClickListener = () => this.finishBtnClick(); this.finishBtn.addEventListener("click", this.onFinishBtnClickListener); } private setCompleted() { this.el.classList.add("completed"); } private unsetCompleted() { this.el.classList.remove("completed"); } private showFinishButton() { if (!this.finishBtn) return; const nonCompletedSteps = this.getUncompletedSteps(); if (nonCompletedSteps.length === 1) this.finishBtn.style.display = ""; else this.finishBtn.style.display = "none"; } private handleFinishButtonClick() { const uncompletedSteps = this.getUncompletedSteps(); const uncompletedOrSkipSteps = this.getUncompletedSteps(true); const { el } = this.contentItems.find(({ isFinal }) => isFinal); if (uncompletedSteps.length) { uncompletedSteps.forEach(({ index }) => this.setCompleteItem(index)); } this.currentIndex = this.totalSteps; this.setCurrentNavItem(); this.hideAllContentItems(); const currentNavItem = this.navItems.find( ({ index }) => index === this.currentIndex, ); const currentNavItemEl = currentNavItem ? currentNavItem.el : null; currentNavItemEl.classList.remove("active"); el.style.display = "block"; if (this.backBtn) this.backBtn.style.display = "none"; if (this.nextBtn) this.nextBtn.style.display = "none"; if (this.skipBtn) this.skipBtn.style.display = "none"; if (this.completeStepBtn) this.completeStepBtn.style.display = "none"; if (this.finishBtn) this.finishBtn.style.display = "none"; if (this.resetBtn) this.resetBtn.style.display = ""; if (uncompletedOrSkipSteps.length <= 1) { this.isCompleted = true; this.setCompleted(); } this.fireEvent("finish", this.currentIndex); dispatch("finish.hs.stepper", this.el, this.currentIndex); } // reset private buildResetButton() { if (!this.resetBtn) return; this.onResetBtnClickListener = () => this.resetBtnClick(); this.resetBtn.addEventListener("click", this.onResetBtnClickListener); } private handleResetButtonClick() { if (this.backBtn) this.backBtn.style.display = ""; if (this.nextBtn) this.nextBtn.style.display = ""; if (this.completeStepBtn) { this.completeStepBtn.style.display = ""; this.completeStepBtn.innerText = this.completeStepBtnDefaultText; this.completeStepBtn.removeAttribute("disabled"); this.completeStepBtn.classList.remove("disabled"); } if (this.resetBtn) this.resetBtn.style.display = "none"; this.navItems.forEach((item) => { const { el } = item; item.isSkip = false; item.isCompleted = false; this.unsetCurrentNavItemActions(el); el.classList.remove("success", "skipped"); }); this.contentItems.forEach((item) => { const { el } = item; item.isSkip = false; item.isCompleted = false; this.unsetCurrentContentItemActions(el); el.classList.remove("success", "skipped"); }); this.currentIndex = 1; this.unsetCompleted(); this.isCompleted = false; this.showSkipButton(); this.setCurrentNavItem(); this.setCurrentContentItem(); this.showFinishButton(); this.showCompleteStepButton(); this.checkForTheFirstStep(); this.fireEvent("reset", this.currentIndex); dispatch("reset.hs.stepper", this.el, this.currentIndex); } // Public methods public setProcessedNavItem(n?: number) { const targetNavItem = this.getNavItem(n); if (!targetNavItem) return; this.setProcessedNavItemActions(targetNavItem); } public unsetProcessedNavItem(n?: number) { const targetNavItem = this.getNavItem(n); if (!targetNavItem) return; this.unsetProcessedNavItemActions(targetNavItem); } public goToNext() { if (this.mode === "linear") this.setCompleteItem(); this.handleNextButtonClick(this.mode !== "linear"); if (this.mode === "linear" && this.currentIndex === this.totalSteps) { if (this.nextBtn) this.nextBtn.style.display = "none"; if (this.completeStepBtn) this.completeStepBtn.style.display = "none"; } } public goToFinish() { this.handleFinishButtonClick(); } public disableButtons() { if (this.backBtn) this.setToDisabled(this.backBtn); if (this.nextBtn) this.setToDisabled(this.nextBtn); } public enableButtons() { if (this.backBtn) this.setToNonDisabled(this.backBtn); if (this.nextBtn) this.setToNonDisabled(this.nextBtn); } public setErrorNavItem(n?: number) { const targetNavItem = this.getNavItem(n); if (!targetNavItem) return; this.setErrorNavItemActions(targetNavItem); } public destroy() { // Remove classes this.el.classList.remove("completed"); this.el.querySelectorAll("[data-hs-stepper-nav-item]").forEach((el) => { el.classList.remove("active", "success", "skipped", "disabled", "error"); if (el.tagName === "BUTTON" || el.tagName === "INPUT") { el.removeAttribute("disabled"); } }); this.el.querySelectorAll("[data-hs-stepper-content-item]").forEach((el) => { el.classList.remove("success", "skipped"); }); if (this.backBtn) this.backBtn.classList.remove("disabled"); if (this.nextBtn) this.nextBtn.classList.remove("disabled"); if (this.completeStepBtn) this.completeStepBtn.classList.remove("disabled"); // Remove attributes if (this.backBtn) this.backBtn.style.display = ""; if (this.nextBtn) this.nextBtn.style.display = ""; if (this.skipBtn) this.skipBtn.style.display = ""; if (this.finishBtn) this.finishBtn.style.display = "none"; if (this.resetBtn) this.resetBtn.style.display = "none"; // Remove listeners if (this.onNavItemClickListener.length) { this.onNavItemClickListener.forEach(({ el, fn }) => { el.removeEventListener("click", fn); }); } if (this.backBtn) { this.backBtn.removeEventListener("click", this.onBackClickListener); } if (this.nextBtn) { this.nextBtn.removeEventListener("click", this.onNextClickListener); } if (this.skipBtn) { this.skipBtn.removeEventListener("click", this.onSkipClickListener); } if (this.completeStepBtn) { this.completeStepBtn.removeEventListener( "click", this.onCompleteStepBtnClickListener, ); } if (this.finishBtn) { this.finishBtn.removeEventListener( "click", this.onFinishBtnClickListener, ); } if (this.resetBtn) { this.resetBtn.removeEventListener("click", this.onResetBtnClickListener); } window.$hsStepperCollection = window.$hsStepperCollection.filter( ({ element }) => element.el !== this.el, ); } // Static methods static getInstance(target: HTMLElement | string, isInstance?: boolean) { const elInCollection = window.$hsStepperCollection.find( (el) => el.element.el === (typeof target === "string" ? document.querySelector(target) : target), ); return elInCollection ? isInstance ? elInCollection : elInCollection.element : null; } static autoInit() { if (!window.$hsStepperCollection) window.$hsStepperCollection = []; if (window.$hsStepperCollection) { window.$hsStepperCollection = window.$hsStepperCollection.filter( ({ element }) => document.contains(element.el), ); } document .querySelectorAll("[data-hs-stepper]:not(.--prevent-on-load-init)") .forEach((el: HTMLElement) => { if ( !window.$hsStepperCollection.find( (elC) => (elC?.element?.el as HTMLElement) === el, ) ) { new HSStepper(el); } }); } } declare global { interface Window { HSStepper: Function; $hsStepperCollection: ICollectionItem<HSStepper>[]; } } window.addEventListener("load", () => { HSStepper.autoInit(); // Uncomment for debug // console.log('Stepper collection:', window.$hsStepperCollection); }); if (typeof window !== "undefined") { window.HSStepper = HSStepper; } export default HSStepper;