UNPKG

smartwizard

Version:

A modern and accessible step wizard plugin for jQuery

1,190 lines (1,185 loc) 52.8 kB
/*! * jQuery SmartWizard v7.0.1 * A modern and accessible step wizard plugin for jQuery * http://www.techlaboratory.net/jquery-smartwizard * * 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: [37], // Left key codes right: [39] // Right key codes } }, localization: { buttons: { next: 'Next', previous: 'Previous' } }, styles: { baseClass: 'sw', navigation: { container: 'nav', link: 'nav-link', }, content: { container: 'tab-content', panel: '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: 'nav-scroll-btn', scrollNext: 'nav-scroll-btn-right', scrollPrevious: 'nav-scroll-btn-left', }, loader: 'sw-loading', progressBar: { container: 'progress', bar: 'progress-bar' }, toolbar: { base: 'toolbar', prefix: '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: false, // Scroll the active step anchor into view on step change contentLoader: null // Callback function for dynamically loading content }; // 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) { const result = { ...base }; for (const key of Object.keys(override)) { const baseVal = base[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] = 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 $(window).on(EVENTS.RESIZE, () => { this.fixHeight(this.currentStepIndex); }); // 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); // Add scroll buttons for nav bar this.createAnchorScroll(); } 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) { let si = -1; const elmList = (dir == STEP_DIRECTION.Backward) ? $(this.steps.slice(0, idx).get().reverse()) : this.steps.slice(idx + 1); // Find the next showable step in the direction elmList.each((i, elm) => { if (this.isEnabled($(elm))) { si = (dir == STEP_DIRECTION.Backward) ? idx - (i + 1) : i + idx + 1; return false; } }); return si; } isShowable(elm) { if (!this.isEnabled(elm)) { return false; } const isCompleted = elm.hasClass(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) { return (elm.hasClass(this.options.styles.anchorStates.disabled) || elm.hasClass(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); }); } createAnchorScroll() { // Remove existing scroll buttons if any this.nav.find('.' + this.options.styles.buttons.scroll).remove(); // Check if the nav bar is scrollable const navElement = this.nav.get(0); if (!navElement) return; const isScrollable = navElement.scrollWidth > navElement.clientWidth; // Only add scroll buttons if the nav bar is scrollable if (!isScrollable) return; // Create the scroll buttons const btnNext = $('<button></button>').addClass(this.options.styles.buttons.scroll + ' ' + this.options.styles.buttons.scrollNext).attr('type', 'button'); const btnPrevious = $('<button></button>').addClass(this.options.styles.buttons.scroll + ' ' + this.options.styles.buttons.scrollPrevious).attr('type', 'button'); btnNext.height(this.nav.height() ?? 0); btnPrevious.height(this.nav.height() ?? 0); return this.nav.append(btnPrevious, btnNext); } 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) { this.showStep(this.getShowable(this.currentStepIndex, dir)); } scrollAnchor(dir) { let scrollLeft = this.nav.scrollLeft() ?? 0; if (dir == 'left') { if (scrollLeft == 0) return; scrollLeft = scrollLeft - 200; } else { const navWidth = this.nav.width() ?? 0; const scrollWidth = this.nav?.get(0)?.scrollWidth ?? 0; if (scrollLeft + navWidth >= scrollWidth) return; scrollLeft = scrollLeft + 200; } if (isFunction(this.nav.animate)) { this.nav.animate({ scrollLeft: scrollLeft }, this.options.transition.speed, this.options.transition.easing); } else { this.nav.scrollLeft(scrollLeft); } } scrollCheck() { let hasScroll = false; let canScrollLeft = false; let canScrollRight = false; const scrollLeft = this.nav.scrollLeft() ?? 0; const scrollWidth = this.nav?.get(0)?.scrollWidth ?? 0; const width = this.nav.outerWidth() ?? 0; if (width < scrollWidth) { hasScroll = true; } $(this.options.styles.buttons.scroll).toggle(hasScroll); if (!hasScroll) return; if (scrollLeft > 0) { canScrollLeft = true; } if (Math.ceil(width + scrollLeft) < scrollWidth) { canScrollRight = true; } $(this.options.styles.buttons.scrollPrevious).toggle(canScrollLeft); $(this.options.styles.buttons.scrollNext).toggle(canScrollRight); $(this.options.styles.buttons.scrollPrevious).toggleClass('nav-scroll-btn-visible', canScrollLeft); $(this.options.styles.buttons.scrollNext).toggleClass('nav-scroll-btn-visible', canScrollRight); } keyNav(e) { if (!this.options.keyboardNavigation.enabled) { return; } // Keyboard navigation if ($.inArray(e.which, this.options.keyboardNavigation.keys.left) > -1) { // left this.navigate(STEP_DIRECTION.Backward); e.preventDefault(); } else if ($.inArray(e.which, this.options.keyboardNavigation.keys.right) > -1) { // right this.navigate(STEP_DIRECTION.Forward); e.preventDefault(); } else { return; // exit this handler for other keys } } 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) { selStep && scrollToView(selStep); } // 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) 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