UNPKG

smartwizard

Version:

A modern and accessible step wizard plugin for jQuery

1,349 lines • 56.3 kB
/*!
 * jQuery SmartWizard v7.1.2
 * A modern and accessible step wizard plugin for jQuery
 * http://www.techlaboratory.net/jquery-smartwizard
 *
 * See the next-generation [SmartWizard](https://github.com/techlab/smart-wizard)
 * A framework-agnostic wizard component with React & Vue support.
 *
 * Created by Dipu Raj (https://github.com/techlab)
 *
 * Licensed under the terms of the MIT License - Free for personal and open-source projects.
 * For commercial use, please purchase a commercial license: https://techlaboratory.net/jquery-smartwizard#license
 */
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
    typeof define === 'function' && define.amd ? define(['exports'], factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.smartWizard = {}));
})(this, (function (exports) { 'use strict';

    /**
     * Event namespace
     */
    const EVENT_NAMESPACE = '.sw';
    /**
     * Events used on the plugin
     */
    const EVENTS = {
        // DOM Events
        CLICK: `click${EVENT_NAMESPACE}`,
        KEYUP: `keyup${EVENT_NAMESPACE}`,
        HASHCHANGE: `hashchange${EVENT_NAMESPACE}`,
        SCROLLEND: `scrollend${EVENT_NAMESPACE}`,
        WHEEL: `wheel${EVENT_NAMESPACE}`,
        RESIZE: `resize${EVENT_NAMESPACE}`,
        ANIMATIONEND: `animationend${EVENT_NAMESPACE}`,
        ANIMATIONCANCEL: `animationcancel${EVENT_NAMESPACE}`,
        TOUCHSTART: `touchstart${EVENT_NAMESPACE}`,
        TOUCHEND: `touchend${EVENT_NAMESPACE}`,
        // SmartWizard Custom Events
        INITIALIZED: `initialized${EVENT_NAMESPACE}`,
        LOADED: `loaded${EVENT_NAMESPACE}`,
        LEAVESTEP: `leave${EVENT_NAMESPACE}`,
        SHOWSTEP: `shown${EVENT_NAMESPACE}`,
    };
    const STEP_DIRECTION = {
        Forward: "forward",
        Backward: "backward",
    };
    const CONTENT_DIRECTION = {
        LeftToRight: "ltr",
        RightToLeft: "rtl",
    };
    const STEP_POSITION = {
        First: "first",
        Middle: "middle",
        Last: "last",
    };
    const TOOLBAR_POSITION = {
        None: "none",
        Top: "top",
        Bottom: "bottom",
        Both: "both",
    };
    /**
     * CSS Custom Properties
     */
    const CSS_PROPERTIES = {
        PROGRESS_PERCENTAGE: '--sw-progress-percentage',
    };
    /**
     * Selectors used
     */
    const SELECTORS = {
        TOOLBAR_ELM: '.sw-toolbar-elm',
    };
    /**
     * Data attributes used
     */
    const DATA_ATTRIBUTES = {
        THEME: 'data-theme',
    };

    const defaults = {
        initialStep: 0, // Initial selected step (0 = first step)
        theme: 'basic', // Theme name, ensure related CSS is included
        displayMode: 'auto', // Display mode: auto (system preference) | dark | light | none (no class applied)
        behavior: {
            autoHeight: true, // Auto-adjust content height
            useUrlHash: false, // Enable step selection via URL hash
            supportBrowserHistory: false // Enable browser history support
        },
        navigation: {
            enabled: true, // Enable/Disable anchor navigation
            alwaysClickable: false, // Allow clicking on any anchor at any time
            completed: {
                enabled: true, // Mark visited steps as completed
                completeAllPreviousSteps: true, // Mark all previous steps as completed when using URL hash
                clearOnBack: false, // Clear completed state when navigating back
                clickable: true // Allow navigation via completed state steps
            }
        },
        transition: {
            effect: 'default', // Transition type: default|fade|slideHorizontal|slideVertical|slideSwing|css
            speed: 400, // Animation speed (ignored if type is 'css')
            easing: '', // Animation easing (requires a jQuery easing plugin, ignored for 'css')
            css: {
                prefix: '', // CSS animation prefix
                forward: { show: '', hide: '' }, // Forward animation classes
                backward: { show: '', hide: '' } // Backward animation classes
            }
        },
        toolbar: {
            position: 'bottom', // Toolbar position: none|top|bottom|both
            buttons: {
                showNext: true, // Show/hide Next button
                showPrevious: true // Show/hide Previous button
            },
            extraElements: '' // Additional HTML for toolbar
        },
        keyboardNavigation: {
            enabled: true, // Enable/Disable keyboard navigation (left/right keys)
            keys: {
                left: ['ArrowLeft'], // Left keys
                right: ['ArrowRight'] // Right keys
            }
        },
        localization: {
            buttons: {
                next: 'Next',
                previous: 'Previous'
            }
        },
        styles: {
            baseClass: 'sw',
            navigation: {
                container: 'sw-nav',
                link: 'sw-nav-link',
            },
            content: {
                container: 'sw-tab-content',
                panel: 'sw-tab-pane'
            },
            themePrefix: 'sw-theme-',
            anchorStates: {
                default: 'default',
                completed: 'completed',
                active: 'active',
                disabled: 'disabled',
                hidden: 'hidden',
                error: 'error',
                warning: 'warning'
            },
            buttons: {
                base: 'sw-btn',
                next: 'sw-btn-next',
                previous: 'sw-btn-prev',
                reset: 'sw-btn-reset',
                scroll: 'sw-nav-scroll-btn',
                scrollNext: 'sw-nav-scroll-btn-right',
                scrollPrevious: 'sw-nav-scroll-btn-left',
            },
            loader: 'sw-loading',
            progressBar: {
                container: 'sw-progress',
                bar: 'sw-progress-bar'
            },
            toolbar: {
                base: 'sw-toolbar',
                prefix: 'sw-toolbar-'
            }
        },
        stepStates: {
            completed: [], // Completed steps
            disabled: [], // Disabled steps
            hidden: [], // Hidden steps
            error: [], // Steps with errors
            warning: [], // Warning steps
        },
        swipeNavigation: {
            enabled: false, // Enable/Disable swipe navigation on touch devices
            threshold: 50, // Minimum swipe distance in pixels to trigger navigation
        },
        scrollToView: true, // Scroll the active step anchor into view on step change
        contentLoader: null, // Callback function for dynamically loading content
        contentSanitize: true // Sanitize the content loaded by contentLoader to prevent XSS
    };

    // TypeScript Util
    /**
     * Gets the current URL hash (including #).
     */
    function getUrlHash() {
        return window.location.hash || '';
    }
    /**
     * Sets the URL hash without reloading the page.
     */
    function setUrlHash(hash) {
        history.pushState(null, '', hash);
    }
    /**
     * Scrolls an element into view smoothly.
     */
    function scrollToView(element) {
        element[0]?.scrollIntoView({ behavior: "smooth" });
    }
    /**
     * Determines if a value is a function.
     */
    function isFunction(value) {
        return typeof value === "function";
    }
    /**
     * Recursively merges `override` into a deep clone of `base`.
     * Plain-object values are merged key-by-key; all other values are replaced.
     */
    function deepMerge(base, override) {
        // Helper to deep clone objects/arrays
        const deepClone = (val) => {
            if (val === null || typeof val !== 'object') {
                return val;
            }
            if (Array.isArray(val)) {
                return val.map(deepClone);
            }
            const clone = {};
            for (const key of Object.keys(val)) {
                clone[key] = deepClone(val[key]);
            }
            return clone;
        };
        const result = deepClone(base);
        for (const key of Object.keys(override)) {
            const baseVal = result[key];
            const overrideVal = override[key];
            if (overrideVal !== null &&
                typeof overrideVal === 'object' &&
                !Array.isArray(overrideVal) &&
                baseVal !== null &&
                typeof baseVal === 'object' &&
                !Array.isArray(baseVal)) {
                result[key] = deepMerge(baseVal, overrideVal);
            }
            else if (overrideVal !== undefined) {
                result[key] = deepClone(overrideVal);
            }
        }
        return result;
    }
    // jQuery Util
    /**
     * Finds the first matching descendant in one of the first two levels of children.
     */
    function getFirstDescendant(element, selector) {
        // Check for first level element
        const firstLevel = element.children(selector);
        if (firstLevel.length > 0) {
            return firstLevel;
        }
        // Check for second level element
        let result = null;
        element.children().each((_i, node) => {
            const secondLevel = $(node).children(selector);
            if (secondLevel.length > 0) {
                result = secondLevel;
                return false; // Break the loop
            }
        });
        if (result) {
            return result;
        }
        // Element not found
        throw new Error(`Element not found ${selector}`);
    }
    /**
     * Gets the text direction from an element or falls back to document.
     */
    function getContentDirection(element) {
        return element.prop('dir') || document.documentElement.dir || '';
    }
    /**
     * Sets the text direction of an element.
     */
    function setContentDirection(element, direction) {
        element.prop('dir', direction);
    }
    /**
     * Triggers a jQuery event and returns whether default was prevented.
     */
    function triggerEvent(element, name, args = []) {
        // Trigger an event
        const event = $.Event(name);
        element.trigger(event, args);
        return !event.isDefaultPrevented();
    }
    function stopAnimations(...elements) {
        if (!isFunction($.fn.finish)) {
            return;
        }
        $(elements).finish();
    }

    const transitions = {
        /**
         * Default transition handler with simple hide/show steps
         */
        default: function (next, current, _stepDirection, _wizard, callback) {
            current && current.hide();
            next.show();
            callback();
        },
        fade: function (next, current, stepDirection, wizard, callback) {
            // Fallback to default transition if fadeOut is not available
            if (!isFunction(next.fadeOut)) {
                transitions.default(next, current, stepDirection, wizard, callback);
                return;
            }
            const { speed, easing } = wizard.getOptions().transition;
            const show = () => next.fadeIn(speed, easing, callback);
            current ? current.fadeOut(speed, easing, show) : show();
        },
        slideHorizontal: function (next, current, stepDirection, wizard, callback) {
            if (!isFunction(next.animate)) {
                transitions.default(next, current, stepDirection, wizard, callback);
                return;
            }
            const { speed, easing } = wizard.getOptions().transition;
            const containerWidth = wizard.getWidth();
            if (wizard.getCurrentIndex() == -1) {
                // Set container height at page load 
                wizard.resetHeight();
            }
            // Horizontal slide
            const show = (element, initial, final, complete) => {
                element.css({ position: 'absolute', left: initial })
                    .show()
                    .stop(true)
                    .animate({ left: final }, speed, easing, complete);
            };
            if (current) {
                const initialCss = current.css(["position", "left"]);
                const final = containerWidth * (stepDirection == 'backward' ? 1 : -1);
                show(current, 0, final, () => {
                    current.hide().css(initialCss);
                });
            }
            const initialCss = next.css(["position"]);
            const initial = containerWidth * (stepDirection == 'backward' ? -2 : 1);
            show(next, initial, 0, () => {
                next.css(initialCss);
                callback();
            });
        },
        slideVertical: function (next, current, stepDirection, wizard, callback) {
            if (!isFunction(next.animate)) {
                transitions.default(next, current, stepDirection, wizard, callback);
                return;
            }
            const { speed, easing } = wizard.getOptions().transition;
            const containerWidth = wizard.getWidth();
            if (wizard.getCurrentIndex() == -1) {
                // Set container height at page load 
                wizard.resetHeight();
            }
            // Vertical slide
            const show = (element, initial, final, complete) => {
                element.css({ position: 'absolute', top: initial })
                    .show()
                    .stop(true)
                    .animate({ top: final }, speed, easing, complete);
            };
            if (current) {
                const initialCss = current.css(["position", "top"]);
                const final = containerWidth * (stepDirection == 'backward' ? 1 : -1);
                show(current, 0, final, () => {
                    current.hide().css(initialCss);
                });
            }
            const initialCss = next.css(["position"]);
            const initial = containerWidth * (stepDirection == 'backward' ? -2 : 1);
            show(next, initial, 0, () => {
                next.css(initialCss);
                callback();
            });
        },
        slideSwing: function (next, current, stepDirection, wizard, callback) {
            // Fallback to default transition if fadeOut is not available
            if (!isFunction(next.slideDown)) {
                transitions.default(next, current, stepDirection, wizard, callback);
                return;
            }
            const { speed, easing } = wizard.getOptions().transition;
            const show = () => next.slideDown(speed, easing, callback);
            current ? current.slideUp(speed, easing, show) : show();
        },
        css: function (next, current, stepDirection, wizard, callback) {
            const { prefix, forward, backward } = wizard.getOptions().transition.css;
            if (forward.show.length == 0 || backward.show.length == 0) {
                transitions.default(next, current, stepDirection, wizard, callback);
                return;
            }
            // CSS Animation
            const animateCss = (element, animation, complete) => {
                if (!animation || animation.length == 0) {
                    complete();
                    return;
                }
                // Element must be visible before adding animation classes,
                // otherwise the browser won't run the animation and animationend never fires.
                element.show();
                let called = false;
                const done = (reason) => {
                    if (called)
                        return;
                    called = true;
                    element.removeClass(animation);
                    complete(reason);
                };
                element.addClass(animation)
                    .one(EVENTS.ANIMATIONEND, () => done())
                    .one(EVENTS.ANIMATIONCANCEL, () => done('cancel'));
            };
            const show = () => {
                const css = prefix + ' ' + (stepDirection == 'backward' ? backward.show : forward.show);
                animateCss(next, css, () => {
                    callback();
                });
            };
            if (current) {
                const css = prefix + ' ' + (stepDirection == 'backward' ? backward.hide : forward.hide);
                animateCss(current, css, () => {
                    current.hide();
                    show();
                });
            }
            else {
                show();
            }
        }
    };

    class Wizard {
        constructor(element, options) {
            Object.defineProperty(this, "options", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "main", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "container", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "nav", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "steps", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "pages", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "progressbar", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "contentDirection", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: void 0
            });
            Object.defineProperty(this, "isInitialized", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: false
            });
            Object.defineProperty(this, "currentStepIndex", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: -1
            });
            Object.defineProperty(this, "touchStartX", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: 0
            });
            Object.defineProperty(this, "touchStartY", {
                enumerable: true,
                configurable: true,
                writable: true,
                value: 0
            });
            // Merge user settings with default
            this.options = deepMerge(defaults, options ?? {});
            // Main container element
            this.main = $(element);
            // Initial setup
            this.setup();
            // Initialize
            this.init();
            // Load wizard asynchronously
            requestAnimationFrame(() => {
                this.load();
            });
        }
        setup() {
            // Content container
            this.container = getFirstDescendant(this.main, '.' + this.options.styles.content.container);
            // Navigation bar element
            this.nav = getFirstDescendant(this.main, '.' + this.options.styles.navigation.container);
            // Step anchor elements
            this.steps = this.nav.find('.' + this.options.styles.navigation.link);
            // Content pages
            this.pages = this.container.children('.' + this.options.styles.content.panel);
            // Progressbar
            this.progressbar = this.main.find('.' + this.options.styles.progressBar.container);
            // Direction, RTL/LTR
            this.contentDirection = getContentDirection(this.main);
            this.contentDirection ?? setContentDirection(this.main, this.contentDirection);
            // Initial wizard index
            this.currentStepIndex = -1;
            // Is initialiazed
            this.isInitialized = false;
        }
        init() {
            // Set elements
            this.setElements();
            // Add toolbar
            this.setToolbar();
            // Set anchor
            this.setNav();
            // Remove Skip if already init
            if (this.isInitialized === true)
                return;
            // Assign plugin events
            this.setEvents();
            // Trigger the initialized event
            triggerEvent(this.main, EVENTS.INITIALIZED);
            this.isInitialized = true;
        }
        load() {
            // Clean the elements
            this.pages.hide();
            // Initial wizard index
            this.currentStepIndex = -1;
            // Get the initial step index
            const idx = this.getInitialStep();
            // Mark any previous steps as completed
            if (idx > 0 && this.options.navigation.completed.enabled && this.options.navigation.completed.completeAllPreviousSteps) {
                this.steps.slice(0, idx).addClass(this.options.styles.anchorStates.completed);
            }
            // Show the initial step
            this.showStep(idx);
            // Trigger the loaded event
            triggerEvent(this.main, EVENTS.LOADED);
        }
        getInitialStep() {
            // Determine target step from hash if enabled
            let hashIndex = null;
            if (this.options.behavior.useUrlHash) {
                const hash = getUrlHash();
                hashIndex = hash ? this.getStepByAnchor(hash) : null;
            }
            // Determine initial step index
            const initialIndex = hashIndex ?? this.options.initialStep;
            // Find the first showable step starting from the initial index
            const showableIndex = this.getShowable(initialIndex - 1, STEP_DIRECTION.Forward);
            // If invalid or not showable, fallback to the first showable step
            return showableIndex ?? (initialIndex > 0 ? this.getShowable(-1, STEP_DIRECTION.Forward) ?? 0 : initialIndex);
        }
        setElements() {
            // Set the main element classes including theme css
            this.main.removeClass((_i, className) => {
                return (className.match(new RegExp('(^|\\s)' + this.options.styles.themePrefix + '\\S+', 'g')) || []).join(' ');
            }).addClass(this.options.styles.baseClass + ' ' + this.options.styles.themePrefix + this.options.theme);
            // Set display mode
            this.setDisplayMode();
        }
        setDisplayMode() {
            const mode = this.options.displayMode;
            // Remove existing data-theme attribute
            this.main.removeAttr(DATA_ATTRIBUTES.THEME);
            if (mode === 'none') {
                // Do nothing - let user manage display mode manually
                return;
            }
            if (mode === 'dark') {
                this.main.attr(DATA_ATTRIBUTES.THEME, 'dark');
            }
            else if (mode === 'light') {
                this.main.attr(DATA_ATTRIBUTES.THEME, 'light');
            }
            else if (mode === 'auto') {
                // Auto-detect system color scheme preference
                const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
                this.main.attr(DATA_ATTRIBUTES.THEME, prefersDark ? 'dark' : 'light');
                // Listen for changes in color scheme preference
                if (window.matchMedia) {
                    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
                    const handleChange = (e) => {
                        this.main.attr(DATA_ATTRIBUTES.THEME, e.matches ? 'dark' : 'light');
                    };
                    // Modern browsers
                    if (mediaQuery.addEventListener) {
                        mediaQuery.addEventListener('change', handleChange);
                    }
                    else if (mediaQuery.addListener) {
                        // Legacy browsers
                        mediaQuery.addListener(handleChange);
                    }
                }
            }
        }
        setEvents() {
            // Anchor click event
            this.steps.on(EVENTS.CLICK, (e) => {
                e.preventDefault();
                if (this.options.navigation.enabled !== true) {
                    return;
                }
                const elm = $(e.currentTarget);
                if (this.isShowable(elm)) {
                    // Get the step index
                    this.showStep(this.steps.index(elm));
                }
            });
            // Next/Previous button event
            this.main.on(EVENTS.CLICK, (e) => {
                const targetElm = $(e.target);
                if (targetElm.hasClass(this.options.styles.buttons.next)) {
                    e.preventDefault();
                    this.navigate(STEP_DIRECTION.Forward);
                }
                else if (targetElm.hasClass(this.options.styles.buttons.previous)) {
                    e.preventDefault();
                    this.navigate(STEP_DIRECTION.Backward);
                }
                else if (targetElm.hasClass(this.options.styles.buttons.scrollNext)) {
                    e.preventDefault();
                    this.scrollAnchor('right');
                }
                else if (targetElm.hasClass(this.options.styles.buttons.scrollPrevious)) {
                    e.preventDefault();
                    this.scrollAnchor('left');
                }
                return;
            });
            // Scroll event
            $(this.nav).on(EVENTS.SCROLLEND, () => {
                this.scrollCheck();
            });
            // Redirect vertical wheel scroll to horizontal on the nav bar
            this.nav.on(EVENTS.WHEEL, (e) => {
                const navEl = this.nav.get(0);
                if (!navEl || navEl.scrollWidth <= navEl.clientWidth)
                    return;
                const originalEvent = e.originalEvent;
                if (!originalEvent || originalEvent.deltaY === 0)
                    return;
                e.preventDefault();
                navEl.scrollLeft += originalEvent.deltaY;
            });
            // Keyboard navigation event            
            $(document).on(EVENTS.KEYUP, (e) => {
                this.keyNav(e);
            });
            // Back/forward browser button event
            $(window).on(EVENTS.HASHCHANGE, (e) => {
                if (this.options.behavior.supportBrowserHistory !== true) {
                    return;
                }
                const idx = this.getURLHashIndex();
                if (idx !== false && this.isShowable(this.steps.eq(idx))) {
                    e.preventDefault();
                    this.showStep(idx);
                }
            });
            // Fix content height on window resize
            let resizeTimer = null;
            $(window).on(EVENTS.RESIZE, () => {
                // ⚡ Bolt Optimization: Throttle/Debounce layout thrashing during window resize
                // Using requestAnimationFrame to ensure DOM read/writes happen right before repaint
                // This prevents main thread blocking and reduces synchronous reflows
                if (resizeTimer !== null) {
                    cancelAnimationFrame(resizeTimer);
                }
                resizeTimer = requestAnimationFrame(() => {
                    this.fixHeight(this.currentStepIndex);
                    this.scrollCheck();
                });
            });
            // Swipe navigation on touch devices
            this.container.on(EVENTS.TOUCHSTART, (e) => {
                const touch = e.originalEvent.touches[0];
                this.touchStartX = touch.clientX;
                this.touchStartY = touch.clientY;
            });
            this.container.on(EVENTS.TOUCHEND, (e) => {
                if (!this.options.swipeNavigation.enabled)
                    return;
                const touch = e.originalEvent.changedTouches[0];
                const deltaX = touch.clientX - this.touchStartX;
                const deltaY = touch.clientY - this.touchStartY;
                // Ignore mostly-vertical swipes
                if (Math.abs(deltaX) < Math.abs(deltaY))
                    return;
                const { threshold } = this.options.swipeNavigation;
                if (deltaX < -threshold) {
                    this.navigate(STEP_DIRECTION.Forward);
                }
                else if (deltaX > threshold) {
                    this.navigate(STEP_DIRECTION.Backward);
                }
            });
        }
        setNav() {
            // Clear other states from the steps
            this.steps.removeClass([
                this.options.styles.anchorStates.completed,
                this.options.styles.anchorStates.active,
                this.options.styles.anchorStates.default,
                this.options.styles.anchorStates.disabled,
                this.options.styles.anchorStates.error,
                this.options.styles.anchorStates.warning,
                this.options.styles.anchorStates.hidden
            ]);
            // Set the anchor default style
            if (this.options.navigation.alwaysClickable !== true || this.options.navigation.enabled !== true) {
                this.steps.addClass(this.options.styles.anchorStates.default);
            }
            // Completed steps
            this.setStepStyle(this.options.stepStates.completed, this.options.styles.anchorStates.completed);
            // Disabled steps
            this.setStepStyle(this.options.stepStates.disabled, this.options.styles.anchorStates.disabled);
            // Warning steps
            this.setStepStyle(this.options.stepStates.warning, this.options.styles.anchorStates.warning);
            // Error steps
            this.setStepStyle(this.options.stepStates.error, this.options.styles.anchorStates.error);
            // Hidden steps
            this.setStepStyle(this.options.stepStates.hidden, this.options.styles.anchorStates.hidden);
        }
        setAnchor(stepIdx) {
            // Current step anchor > Remove other classes and add done class
            if (this.currentStepIndex !== null && this.currentStepIndex >= 0) {
                let removeCss = this.options.styles.anchorStates.active;
                removeCss += ' ' + this.options.styles.anchorStates.default;
                let addCss = '';
                if (this.options.navigation.completed.enabled !== false) {
                    addCss += this.options.styles.anchorStates.completed;
                    if (this.options.navigation.completed.clearOnBack !== false && this.getStepDirection(stepIdx) === STEP_DIRECTION.Backward) {
                        removeCss += ' ' + this.options.styles.anchorStates.completed;
                    }
                }
                this.steps.eq(this.currentStepIndex)
                    .addClass(addCss)
                    .removeClass(removeCss);
            }
            // Next step anchor > Remove other classes and add active class
            this.steps.eq(stepIdx)
                .removeClass(this.options.styles.anchorStates.completed + ' ' + this.options.styles.anchorStates.default)
                .addClass(this.options.styles.anchorStates.active);
        }
        setButtons(idx) {
            // Previous/Next Button enable/disable based on step
            this.main.find('.' + this.options.styles.buttons.next + ', .' + this.options.styles.buttons.previous).removeClass(this.options.styles.anchorStates.disabled);
            const p = this.getStepPosition(idx);
            if (p === STEP_POSITION.First || p === STEP_POSITION.Last) {
                const c = (p === STEP_POSITION.First) ? '.' + this.options.styles.buttons.previous : '.' + this.options.styles.buttons.next;
                this.main.find(c).addClass(this.options.styles.anchorStates.disabled);
            }
            else {
                if (this.getShowable(idx, STEP_DIRECTION.Forward) === null) {
                    this.main.find('.' + this.options.styles.buttons.next).addClass(this.options.styles.anchorStates.disabled);
                }
                if (this.getShowable(idx, STEP_DIRECTION.Backward) === null) {
                    this.main.find('.' + this.options.styles.buttons.previous).addClass(this.options.styles.anchorStates.disabled);
                }
            }
        }
        setProgressbar(idx) {
            const width = this.nav.width() ?? 0;
            const widthPercentage = (((width / this.steps.length) * (idx + 1) / width) * 100).toFixed(2);
            // Set css variable for supported themes
            document.documentElement.style.setProperty(CSS_PROPERTIES.PROGRESS_PERCENTAGE, widthPercentage + '%');
            if (this.progressbar.length > 0) {
                this.progressbar.find('.' + this.options.styles.progressBar.bar).css('width', widthPercentage + '%');
            }
        }
        getShowable(idx, dir) {
            if (dir == STEP_DIRECTION.Backward) {
                for (let i = idx - 1; i >= 0; i--) {
                    if (this.isEnabled(this.steps[i])) {
                        return i;
                    }
                }
            }
            else {
                const len = this.steps.length;
                for (let i = idx + 1; i < len; i++) {
                    if (this.isEnabled(this.steps[i])) {
                        return i;
                    }
                }
            }
            return null;
        }
        isShowable(elm) {
            if (!elm)
                return false;
            const rawElm = (elm instanceof $) ? elm[0] : elm;
            if (!rawElm)
                return false;
            if (!this.isEnabled(rawElm)) {
                return false;
            }
            const isCompleted = rawElm.classList.contains(this.options.styles.anchorStates.completed);
            if (this.options.navigation.completed.enabled === false && isCompleted) {
                return false;
            }
            if (this.options.navigation.completed.clickable === false && isCompleted) {
                return false;
            }
            if (this.options.navigation.alwaysClickable === false && !isCompleted) {
                return false;
            }
            return true;
        }
        isEnabled(elm) {
            if (!elm)
                return false;
            const rawElm = (elm instanceof $) ? elm[0] : elm;
            if (!rawElm)
                return false;
            return (rawElm.classList.contains(this.options.styles.anchorStates.disabled) || rawElm.classList.contains(this.options.styles.anchorStates.hidden)) ? false : true;
        }
        getStepByAnchor(hash) {
            var elm = this.nav.find("a[href*='" + hash + "']");
            if (elm.length > 0) {
                return this.steps.index(elm);
            }
            return null;
        }
        setStepStyle(stepIndexes, cssClass) {
            $.each(stepIndexes, (_i, n) => {
                this.steps.eq(n).addClass(cssClass);
            });
        }
        setToolbar() {
            // Remove already existing toolbar if any
            this.main.find(SELECTORS.TOOLBAR_ELM).remove();
            const toolbarPosition = this.options.toolbar.position;
            if (toolbarPosition === TOOLBAR_POSITION.None) {
                // Skip right away if the toolbar is not enabled
                return;
            }
            if (toolbarPosition == TOOLBAR_POSITION.Both) {
                this.container.before(this.createToolbar(TOOLBAR_POSITION.Top));
                this.container.after(this.createToolbar(TOOLBAR_POSITION.Bottom));
            }
            else if (toolbarPosition == TOOLBAR_POSITION.Top) {
                this.container.before(this.createToolbar(TOOLBAR_POSITION.Top));
            }
            else {
                this.container.after(this.createToolbar(TOOLBAR_POSITION.Bottom));
            }
        }
        createToolbar(position) {
            const toolbar = $('<div></div>').addClass('sw-toolbar-elm ' + this.options.styles.toolbar.base + ' ' + this.options.styles.toolbar.prefix + position).attr('role', 'toolbar');
            // Create the toolbar buttons
            let buttonArray = [];
            if (this.options.toolbar.buttons.showPrevious !== false) {
                buttonArray.push($('<button></button>').text(this.options.localization.buttons.previous).addClass('btn ' + this.options.styles.buttons.previous + ' ' + this.options.styles.buttons.base).attr('type', 'button'));
            }
            if (this.options.toolbar.buttons.showNext !== false) {
                buttonArray.push($('<button></button>').text(this.options.localization.buttons.next).addClass('btn ' + this.options.styles.buttons.next + ' ' + this.options.styles.buttons.base).attr('type', 'button'));
            }
            if (buttonArray.length > 0) {
                toolbar.append(...buttonArray);
            }
            return toolbar.append(this.options.toolbar.extraElements);
        }
        getURLHashIndex() {
            if (this.options.behavior.useUrlHash) {
                // Get step number from url hash if available
                var hash = getUrlHash();
                if (hash.length > 0) {
                    var elm = this.nav.find("a[href*='" + hash + "']");
                    if (elm.length > 0) {
                        return this.steps.index(elm);
                    }
                }
            }
            return false;
        }
        navigate(dir) {
            const nextStep = this.getShowable(this.currentStepIndex, dir);
            if (nextStep !== null) {
                this.showStep(nextStep);
            }
        }
        scrollAnchor(dir) {
            const navEl = this.nav.get(0);
            if (!navEl)
                return;
            const isHorizontal = window.getComputedStyle(navEl).flexDirection !== 'column';
            const scrollAmount = isHorizontal
                ? navEl.clientWidth / 2
                : navEl.clientHeight / 2;
            const maxScroll = isHorizontal
                ? navEl.scrollWidth - navEl.clientWidth
                : navEl.scrollHeight - navEl.clientHeight;
            let target = isHorizontal ? navEl.scrollLeft : navEl.scrollTop;
            target += (dir === 'right' || dir === 'next') ? scrollAmount : -scrollAmount;
            target = Math.max(0, Math.min(target, maxScroll));
            if (typeof navEl.scrollTo === 'function') {
                navEl.scrollTo({
                    [isHorizontal ? 'left' : 'top']: target,
                    behavior: 'smooth',
                });
            }
            else {
                if (isHorizontal) {
                    navEl.scrollLeft = target;
                }
                else {
                    navEl.scrollTop = target;
                }
            }
        }
        scrollCheck() {
            const navEl = this.nav.get(0);
            if (!navEl)
                return;
            const isHorizontal = window.getComputedStyle(navEl).flexDirection !== 'column';
            const scrollPos = isHorizontal ? navEl.scrollLeft : navEl.scrollTop;
            const scrollSize = isHorizontal ? navEl.scrollWidth : navEl.scrollHeight;
            const clientSize = isHorizontal ? navEl.clientWidth : navEl.clientHeight;
            const hasScroll = scrollSize > clientSize;
            this.nav.find('.' + this.options.styles.buttons.scroll).toggle(hasScroll);
            if (!hasScroll)
                return;
            const canScrollPrev = scrollPos > 0;
            const canScrollNext = Math.ceil(clientSize + scrollPos) < scrollSize;
            this.nav.find('.' + this.options.styles.buttons.scrollPrevious).toggle(canScrollPrev);
            this.nav.find('.' + this.options.styles.buttons.scrollNext).toggle(canScrollNext);
        }
        scrollNavToStep(stepIdx) {
            const stepAnchor = this.steps.eq(stepIdx);
            if (!stepAnchor.length)
                return;
            const navEl = this.nav.get(0);
            if (!navEl)
                return;
            // Resolve the step element (use parent li if present, matching NavScroll step items)
            const stepItem = stepAnchor.closest('li, .sw-nav-item');
            const stepEl = (stepItem.length > 0 ? stepItem.get(0) : stepAnchor.get(0));
            if (!stepEl)
                return;
            const isHorizontal = window.getComputedStyle(navEl).flexDirection !== 'column';
            let itemStart = isHorizontal ? stepEl.offsetLeft : stepEl.offsetTop;
            if (stepEl.offsetParent !== navEl) {
                const navRect = navEl.getBoundingClientRect();
                const stepRect = stepEl.getBoundingClientRect();
                itemStart = isHorizontal
                    ? stepRect.left - navRect.left + navEl.scrollLeft
                    : stepRect.top - navRect.top + navEl.scrollTop;
            }
            const itemSize = isHorizontal ? stepEl.offsetWidth : stepEl.offsetHeight;
            const navSize = isHorizontal ? navEl.clientWidth : navEl.clientHeight;
            const maxScroll = isHorizontal
                ? navEl.scrollWidth - navEl.clientWidth
                : navEl.scrollHeight - navEl.clientHeight;
            const center = itemStart + itemSize / 2 - navSize / 2;
            const clamped = Math.max(0, Math.min(center, maxScroll));
            if (typeof navEl.scrollTo === 'function') {
                navEl.scrollTo({
                    [isHorizontal ? 'left' : 'top']: clamped,
                    behavior: 'smooth',
                });
            }
            else {
                if (isHorizontal) {
                    navEl.scrollLeft = clamped;
                }
                else {
                    navEl.scrollTop = clamped;
                }
            }
        }
        keyNav(e) {
            if (!this.options.keyboardNavigation.enabled)
                return;
            // Ignore key events originating from input fields
            const tag = e.target?.tagName;
            if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
                return;
            const key = e.key;
            if (!key)
                return;
            if (this.options.keyboardNavigation.keys.left.includes(key)) {
                this.navigate(STEP_DIRECTION.Backward);
                e.preventDefault();
            }
            else if (this.options.keyboardNavigation.keys.right.includes(key)) {
                this.navigate(STEP_DIRECTION.Forward);
                e.preventDefault();
            }
        }
        getStepDirection(stepIdx) {
            return stepIdx > this.currentStepIndex ? STEP_DIRECTION.Forward : STEP_DIRECTION.Backward;
        }
        getStepAnchor(stepIdx) {
            if (stepIdx == null || stepIdx == -1)
                return null;
            return this.steps.eq(stepIdx) ?? null;
        }
        getStepPage(idx) {
            if (idx == null || idx == -1)
                return null;
            return this.pages.eq(idx) ?? null;
        }
        transit(next, current, stepDirection, callback) {
            stopAnimations(this.pages, this.container);
            let doTransit = transitions[this.options.transition.effect] ?? null;
            doTransit = isFunction(doTransit) ? doTransit : transitions['default'];
            doTransit(next, current, stepDirection, this, callback);
        }
        fixHeight(idx) {
            if (this.options.behavior.autoHeight === false)
                return;
            const elm = this.getStepPage(idx);
            if (elm == null)
                return;
            // Auto adjust height of the container
            const parentPadding = parseFloat(this.container.css('padding-top')) + parseFloat(this.container.css('padding-bottom'));
            const parentBorders = parseFloat(this.container.css('border-top-width')) + parseFloat(this.container.css('border-bottom-width'));
            const contentHeight = ($(elm).outerHeight(true) ?? 0) + parentPadding + parentBorders;
            if (isFunction(this.container.finish) && isFunction(this.container.animate) && contentHeight > 0) {
                this.container.finish().animate({ height: contentHeight }, this.options.transition.speed);
            }
            else {
                this.container.css({ height: contentHeight > 0 ? contentHeight : 'auto' });
            }
        }
        showStep(stepIdx) {
            if (stepIdx === -1 || stepIdx === null)
                return;
            // If current step is requested again, skip
            if (stepIdx == this.currentStepIndex)
                return;
            // If step not found, skip
            if (!this.steps.eq(stepIdx))
                return;
            // If it is a disabled step, skip
            if (!this.isEnabled(this.steps.eq(stepIdx)))
                return;
            // Get the direction of navigation
            const stepDirection = this.getStepDirection(stepIdx);
            if (this.currentStepIndex !== -1) {
                const leaveStepEventArgs = {
                    stepIndex: this.currentStepIndex,
                    nextStepIndex: stepIdx, // The step being shown/left
                    stepElement: this.getStepAnchor(this.currentStepIndex), // DOM element of the step
                    stepDirection: stepDirection, // or custom enum
                    stepPosition: this.getStepPosition(this.currentStepIndex), // optional helper classification
                };
                // Trigger "leaveStep" event
                if (triggerEvent(this.main, EVENTS.LEAVESTEP, leaveStepEventArgs) === false) {
                    return;
                }
            }
            // Load content
            this.loadContent(stepIdx, () => {
                // Get step to show element
                const selStep = this.getStepAnchor(stepIdx);
                // Change the url hash to new step
                if (this.options.behavior.useUrlHash && this.options.behavior.supportBrowserHistory) {
                    const hash = selStep?.attr("href") ?? '';
                    if (hash) {
                        setUrlHash(hash);
                    }
                }
                // Update controls
                this.setAnchor(stepIdx);
                // Scroll the element into view
                if (this.options.scrollToView) {
                    this.scrollNavToStep(stepIdx);
                }
                // Get current step element
                const curPage = this.getStepPage(this.currentStepIndex);
                // Get next step element
                const selPage = this.getStepPage(stepIdx);
                if (selPage == null)
                    return;
                // transit the step
                this.transit($(selPage), curPage ? $(curPage) : null, stepDirection, () => {
                    // Fix height with content
                    this.fixHeight(stepIdx);
                    // Trigger "showStep" event
                    const stepEventArgs = {
                        stepIndex: stepIdx,
                        stepElement: selStep,
                        stepDirection: stepDirection,
                        stepPosition: this.getStepPosition(stepIdx),
                    };
                    triggerEvent(this.main, EVENTS.SHOWSTEP, stepEventArgs);
                });
                // Update the current index
                this.currentStepIndex = stepIdx;
                // Set the buttons based on the step
                this.setButtons(stepIdx);
                // Set the progressbar based on the step
                this.setProgressbar(stepIdx);
            });
        }
        loadContent(idx, callback) {
            if (!this.options.contentLoader || !isFunction(this.options.contentLoader)) {
                callback();
                return;
            }
            let selPage = this.getStepPage(idx);
            if (!selPage) {
                callback();
                return;
            }
            selPage = $(selPage);
            // Get step direction
            const stepDirection = this.getStepDirection(idx);
            // Get step position
            const stepPosition = this.getStepPosition(idx);
            // Get next step element
            let selStep = this.getStepAnchor(idx);
            if (selStep == null) {
                callback();
                return;
            }
            selStep = $(selStep);
            this.options.contentLoader(idx, stepDirection, stepPosition, selStep, (content) => {
                if (content) {
                    if (this.options.contentSanitize !== false && typeof content === 'string') {
                        // Use $.parseHTML to parse the string safely (avoids executing inline scripts)
                        // The third parameter keepScripts defaults to false
                        selPage.empty().append($.parseHTML(content));
                    }
                    else {
                        selPage.html(content);
                    }
                }
                callback();
            });
        }
        getStepPosition(idx) {
            if (idx === 0) {
                return STEP_POSITION.First;
            }
            else if (idx === this.steps.length - 1) {
                return STEP_POSITION.Last;
            }
            return STEP_POSITION.Middle;
        }
        changeState(stepArray, state, addOrRemove) {
            // addOrRemove: true => Add, otherwise remove 
            addOrRemove = (addOrRemove !== false) ? true : false;
            let css = '';
            if (state == 'default') {
                css = this.options.styles.anchorStates.default;
            }
            else if (state == 'active') {
                css = this.options.styles.anchorStates.active;
            }
            else if (state == 'completed') {
                css = this.options.styles.anchorStates.completed;
            }
            else if (state == 'disable') {
                css = this.options.styles.anchorStates.disabled;
            }
            else if (state == 'hidden') {
                css = this.options.styles.anchorStates.hidden;
            }
            else if (state == 'error') {
                css = this.options.styles.anchorStates.error;
            }
            else if (state == 'warning') {
                css = this.options.styles.anchorStates.warning;
            }
            $.each(stepArray, (_i, n) => {
                this.steps.eq(n).toggleClass(css, addOrRemove);
            });
        }
        // PUBLIC FUNCTIONS
        goToStep(stepIndex, force) {
            force = force === true ? true : false;
            if (!force && !this.isShowable(this.steps.eq(stepIndex))) {
                return;
            }
            // Mark any previous steps done
            if (force && stepIndex > 0 && this.options.navigation.completed.enabled && this.options.navigation.completed.completeAllPreviousSteps) {
                this.steps.slice(0, stepIndex).addClass(this.options.styles.anchorStates.completed);
            }
            this.showStep(stepIndex);
        }
        next() {
            this.navigate(STEP_DIRECTION.Forward);
        }
        prev() {
            this.navigate(STEP_DIRECTION.Backward);
        }
        reset() {
            // Clear css from steps except default, hidden and disabled
            this.steps.removeClass([
                this.options.styles.anchorStates.completed,
                this.options.styles.anchorStates.active,
                this.options.styles.anchorStates.error,
                this.options.styles.anchorStates.warning
            ]);
            // Reset all
            if (this.options.behavior.useUrlHash && this.options.behavior.supportBrowserHistory) {
                setUrlHash('#');
            }
            this.init();
            this.load();
        }
        setState(stepArray, state) {
            this.changeState(stepArray, state, true);
        }
        unsetState(stepArray, state) {
            this.changeState(stepArray, state, false);
        }
        setOptions(options) {
            this.options = deepMerge(this.options, options);
            this.init();
            this.load();
        }
        getOptions() {
            return this.options;
        }
        getContentDirection() {
            return this.contentDirection || CONTENT_DIRECTION.LeftToRight;
        }
        getCurrentIndex() {
            return this.currentStepIndex;
        }
        getStepInfo() {
            return {
                currentStep: this.currentStepIndex ? this.currentStepIndex : 0,
                totalSteps: this.steps ? this.steps.length : 0
            };
        }
        loader(state) {
            this.main.toggleClass(this.options.styles.loader, (state === "show"));
        }
        adjustHeight() {
            this.fixHeight(this.currentStepIndex);
        }
        resetHeight() {
            const elm = this.getStepPage(0);
            if (elm == null)
                return;
            const contentHeight = $(elm).outerHeight(true) ?? 'auto';
            this.container.css({ height: contentHeight });
        }
        getWidth() {
            return this.container.width() ?? 0;
        }
    }

    // Define the plugin
    function smartWizard(options) {
        if (options === undefined || typeof options === 'object') {
            return this.each(function () {
                if (!$.data(this, "smartWizard")) {
                    $.data(this, "smartWizard", new Wizard($(this), options));
                }
            });
        }
        else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
            const instance = $.data(this[0], 'smartWizard');
            if (options === 'destroy') {
                $.data(this, 'smartWizard', null);
                instance?.destroy();
                return this;
            }
            if (instance instanceof Wizard && typeof instance[options] === 'function') {
                return instance[options].apply(instance, Array.prototype.slice.call(arguments, 1));
            }
            else {
                return this;
            }
        }
    }
    // Extend jQuery
    $.fn.extend({
        smartWizard
    });
    // Define plugin defaults and transitions
    $.fn.smartWizard.transitions = transitions;

    exports.CONTENT_DIRECTION = CONTENT_DIRECTION;
    exports.CSS_PROPERTIES = CSS_PROPERTIES;
    exports.DATA_ATTRIBUTES = DATA_ATTRIBUTES;
    exports.EVENTS = EVENTS;
    exports.EVENT_NAMESPACE = EVENT_NAMESPACE;
    exports.SELECTORS = SELECTORS;
    exports.STEP_DIRECTION = STEP_DIRECTION;
    exports.STEP_POSITION = STEP_POSITION;
    exports.TOOLBAR_POSITION = TOOLBAR_POSITION;
    exports.Wizard = Wizard;
    exports.deepMerge = deepMerge;
    exports.default = Wizard;
    exports.defaults = defaults;
    exports.getContentDirection = getContentDirection;
    exports.getFirstDescendant = getFirstDescendant;
    exports.getUrlHash = getUrlHash;
    exports.isFunction = isFunction;
    exports.scrollToView = scrollToView;
    exports.setContentDirection = setContentDirection;
    exports.setUrlHash = setUrlHash;
    exports.stopAnimations = stopAnimations;
    exports.transitions = transitions;
    exports.triggerEvent = triggerEvent;

    Object.defineProperty(exports, '__esModule', { value: true });

}));
//# sourceMappingURL=jquery.smartWizard.js.map