UNPKG

smartwizard

Version:

A modern and accessible step wizard plugin for jQuery

1,317 lines • 50.1 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
 */
/**
 * 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;

export { CONTENT_DIRECTION, CSS_PROPERTIES, DATA_ATTRIBUTES, EVENTS, EVENT_NAMESPACE, SELECTORS, STEP_DIRECTION, STEP_POSITION, TOOLBAR_POSITION, Wizard, deepMerge, Wizard as default, defaults, getContentDirection, getFirstDescendant, getUrlHash, isFunction, scrollToView, setContentDirection, setUrlHash, stopAnimations, transitions, triggerEvent };
//# sourceMappingURL=jquery.smartWizard.esm.js.map