smartwizard
Version:
A modern and accessible step wizard plugin for jQuery
1,190 lines (1,185 loc) • 52.8 kB
JavaScript
/*!
* 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