/* jQuery.wizard v1.1.3 https://github.com/kflorence/jquery-wizard/ An asynchronous form wizard that supports branching. Requires: - jQuery 1.6.0+ - jQuery UI widget 1.9.0+ Copyright (c) 2017 Kyle Florence Dual licensed under the MIT and GPLv2 licenses. */ (function( $, undefined ) { var count = 0, selector = {}, className = {}, // Reference to commonly used methods aps = Array.prototype.slice, // Used to normalize function arguments that can be either // an array of values or a single value arr = function( obj ) { return $.isArray( obj ) ? obj : [ obj ]; }, // Commonly used strings id = "id", form = "form", click = "click", submit = "submit", disabled = "disabled", namespace = "kf-wizard", wizard = "wizard", def = "default", num = "number", obj = "object", str = "string", bool = "boolean", // Events afterBackward = "afterBackward", afterDestroy = "afterDestroy", afterForward = "afterForward", afterSelect = "afterSelect", beforeBackward = "beforeBackward", beforeDestroy = "beforeDestroy", beforeForward = "beforeForward", beforeSelect = "beforeSelect", beforeSubmit = "beforeSubmit"; // Generate selectors and class names for common wizard elements $.each( "branch form header step wrapper".split( " " ), function() { selector[ this ] = "." + ( className[ this ] = wizard + "-" + this ); }); $.widget( "kf." + wizard, { version: "1.1.3", options: { animations: { show: { options: { duration: 0 }, properties: { opacity: "show" } }, hide: { options: { duration: 0 }, properties: { opacity: "hide" } } }, backward: ".backward", branches: ".branch", disabled: false, enableSubmit: false, forward: ".forward", header: ":header:first", initialStep: 0, stateAttribute: "data-state", stepClasses: { current: "current", exclude: "exclude", stop: "stop", submit: "submit", unidirectional: "unidirectional" }, steps: ".step", submit: ":submit", transitions: {}, unidirectional: false, /* callbacks */ afterBackward: null, afterDestroy: null, afterForward: null, afterSelect: null, beforeBackward: null, beforeDestroy: null, beforeForward: null, beforeSelect: null, create: null }, _create: function() { var $form, $header, self = this, o = self.options, $element = self.element, $steps = $element.find( o.steps ), $stepsWrapper = $steps.eq( 0 ).parent(); if ( $element[ 0 ].elements ) { $form = $element; // If element isn't form, look inside and outside element } else if ( !( $form = $element.find( form ) ).length ) { $form = $element.closest( form ); } // If header isn't found in element, look in form scope if ( !( $header = $element.find( o.header ) ).length ) { $header = $form.find( o.header ); } self.elements = { form: $form.addClass( className.form ), submit: $form.find( o.submit ), forward: $form.find( o.forward ), backward: $form.find( o.backward ), header: $header.addClass( className.header ), steps: $element.find( o.steps ).hide().addClass( className.step ), branches: $element.find( o.branches ).add( $stepsWrapper ).addClass( className.branch ), stepsWrapper: $stepsWrapper.addClass( className.wrapper ), wizard: $element.addClass( wizard ) }; if ( !$stepsWrapper.attr( id ) ) { // stepsWrapper must have an ID as it also functions as the default branch $stepsWrapper.attr( id, wizard + "-" + ( ++count ) ); } self.elements.forward.on( "click." + namespace, function( event ) { event.preventDefault(); self.forward( event ); }); self.elements.backward.on( "click." + namespace, function( event ) { event.preventDefault(); self.backward( event ); }); self._currentState = { branchesActivated: [], stepsActivated: [] }; self._stepCount = self.elements.steps.length; self._lastStepIndex = self._stepCount - 1; // Cache branch labels for quick access later self._branchLabels = []; self.elements.steps.each(function( i ) { self._branchLabels[ i ] = $( this ).parent().attr( id ); }); // Called in the context of jQuery's .filter() method in _state() self._excludesFilter = function() { return !$( this ).hasClass( o.stepClasses.exclude ); }; // Add default transition function if one wasn't defined if ( !o.transitions[ def ] ) { o.transitions[ def ] = function( state ) { return self.stepIndex( state.step.nextAll( selector.step ) ); }; } // Select initial step self.select.apply( self, arr( o.initialStep ) ); }, _fastForward: function( toIndex, relative, callback ) { var i = 0, self = this, stepIndex = self._currentState.stepIndex, stepsTaken = [ stepIndex ]; if ( $.isFunction( relative ) ) { callback = relative; relative = undefined; } (function next() { self._transition( self._state( stepIndex, stepsTaken ), function( step, branch ) { if ( ( stepIndex = self.stepIndex( step, branch ) ) === -1 ) { throw new Error( '[_fastForward]: Invalid step "' + step + '"' ); } else if ( $.inArray( stepIndex, stepsTaken ) >= 0 ) { throw new Error( '[_fastForward]: Recursion detected on step "' + step + '"' ); } else { stepsTaken.push( stepIndex ); if ( stepIndex === self._lastStepIndex || ( relative ? ++i : stepIndex ) === toIndex ) { callback.call( self, stepIndex, stepsTaken ); } else { next(); } } }); })(); }, _find: function( needles, haystack, wrap ) { var element, i, l, needle, type, found = [], $haystack = haystack instanceof jQuery ? haystack : $( haystack ); function matchElement( i, current ) { if ( current === needle ) { element = current; // Break from .each loop return false; } } if ( needles !== null && $haystack.length ) { needles = arr( needles ); for ( i = 0, l = needles.length; i < l; i++ ) { element = null; needle = needles[ i ]; type = typeof needle; if ( type === num ) { element = $haystack.get( needle ); } else if ( type === str ) { element = document.getElementById( needle.replace( '#', '' ) ); } else if ( type === obj ) { if ( needle instanceof jQuery && needle.length ) { needle = needle[ 0 ]; } if ( needle.nodeType ) { $haystack.each( matchElement ); } } if ( element ) { found.push( element ); } } } // Returns a jQuery object by default. If the wrap argument is // false, it will return an array of elements instead. return wrap === false ? found : $( found ); }, _move: function( step, branch, relative, history, callback ) { var self = this, current = self._currentState; if ( typeof branch === bool ) { callback = history; history = relative; relative = branch; branch = undefined; } function move( stepIndex, stepsTaken ) { callback.call( self, stepIndex, $.isArray( history ) ? history : history !== false ? stepsTaken : undefined ); } if ( relative === true ) { if ( step > 0 ) { self._fastForward( step, relative, move ); } else { callback.call( self, current.stepsActivated[ // Normalize to zero if negative Math.max( 0, step + ( current.stepsActivated.length - 1 ) ) ] ); } // Don't attempt to move to invalid steps } else if ( ( step = self.stepIndex( step, branch ) ) !== -1 ) { if ( step > current.stepIndex ) { self._fastForward( step, move ); } else { move.call( self, step ); } } }, _state: function( stepIndex, stepsTaken ) { if ( !this.isValidStepIndex( stepIndex ) ) { return null; } var o = this.options, state = $.extend( true, {}, this._currentState ); // stepsTaken must be an array of at least one step stepsTaken = arr( stepsTaken || stepIndex ); state.step = this.elements.steps.eq( stepIndex ); state.branch = state.step.parent(); state.branchStepCount = state.branch.children( selector.step ).length; state.isMovingForward = stepIndex > state.stepIndex; state.stepIndexInBranch = state.branch.children( selector.step ).index( state.step ); var branchLabel, indexOfBranch, indexOfStep, i = 0, l = stepsTaken.length; for ( ; i < l; i++ ) { stepIndex = stepsTaken[ i ]; branchLabel = this._branchLabels[ stepIndex ]; // Going forward if ( !state.stepIndex || state.stepIndex < stepIndex ) { // No duplicate steps if ( $.inArray( stepIndex, state.stepsActivated ) < 0 ) { state.stepsActivated.push( stepIndex ); // No duplicate branch labels if ( $.inArray( branchLabel, state.branchesActivated ) < 0 ) { state.branchesActivated.push( branchLabel ); } } // Going backward } else if ( state.stepIndex > stepIndex ) { indexOfBranch = $.inArray( branchLabel, state.branchesActivated ) + 1; indexOfStep = $.inArray( stepIndex, state.stepsActivated ) + 1; // Don't remove initial branch if ( indexOfBranch > 0 ) { state.branchesActivated.splice( indexOfBranch, // IE requires this argument state.branchesActivated.length - 1 ); } // Don't remove the initial step if ( indexOfStep > 0 ) { state.stepsActivated.splice( indexOfStep, // IE requires this argument state.stepsActivated.length - 1 ); } } state.stepIndex = stepIndex; state.branchLabel = branchLabel; } // Steps completed: the number of steps we have visited state.stepsComplete = Math.max( 0, this._find( state.stepsActivated, this.elements.steps ).filter( this._excludesFilter ).length - 1 ); // Steps possible: the number of steps in all of the branches we have visited state.stepsPossible = Math.max( 0, this._find( state.branchesActivated, this.elements.branches ).children( selector.step ).filter( this._excludesFilter ).length - 1 ); $.extend( state, { branchLabel: this._branchLabels[ stepIndex ], isFirstStep: stepIndex === 0, isFirstStepInBranch: state.stepIndexInBranch === 0, isLastStep: stepIndex === this._lastStepIndex, isLastStepInBranch: state.stepIndexInBranch === state.branchStepCount - 1, percentComplete: ( 100 * state.stepsComplete / state.stepsPossible ), stepsRemaining: ( state.stepsPossible - state.stepsComplete ) }); return state; }, _transition: function( state, action ) { var response, self = this, o = self.options, stateName = state.step.attr( o.stateAttribute ), transitionFunc = stateName ? o.transitions[ stateName ] : o.transitions[ def ]; if ( $.isFunction( transitionFunc ) ) { response = transitionFunc.call( self, state, function() { return action.apply( self, aps.call( arguments ) ); }); } else { response = stateName; } // A response of 'undefined' or 'false' will halt immediate action // waiting instead for the transition function to handle the call if ( response !== undefined && response !== false ) { // Response could be array like [ step, branch ] action.apply( self, arr( response ) ); } }, _update: function( event, state, force ) { var self = this, current = self._currentState, data = [ state, function( response ) { self._update( event, state, response !== false ); } ], o = self.options; if ( current.step ) { if ( !state || o.disabled || state.stepIndex === current.stepIndex || force !== true && ( !this._trigger( beforeSelect, event, data ) || ( state.isMovingForward && !this._trigger( beforeForward, event, data ) ) || ( !state.isMovingForward && !this._trigger( beforeBackward, event, data ) ) ) ) { return; } current.step.removeClass( o.stepClasses.current ) .animate( o.animations.hide.properties, // Fixes #3583 - http://bugs.jquery.com/ticket/3583 $.extend( {}, o.animations.hide.options ) ); } // Note that this does not affect the value of 'current' this._currentState = state; state.step.addClass( o.stepClasses.current ) .animate( o.animations.show.properties, // Fixes #3583 - http://bugs.jquery.com/ticket/3583 $.extend( {}, o.animations.show.options ) ); if ( state.isFirstStep || o.unidirectional || state.step.hasClass( o.stepClasses.unidirectional ) ) { this.elements.backward.attr( disabled, true ); } else { this.elements.backward.removeAttr( disabled ); } if ( ( state.isLastStepInBranch && !state.step.attr( o.stateAttribute ) ) || state.step.hasClass( o.stepClasses.stop ) ) { this.elements.forward.attr( disabled, true ); } else { this.elements.forward.removeAttr( disabled ); } if ( o.enableSubmit || state.step.hasClass( o.stepClasses.submit ) ) { this.elements.submit.removeAttr( disabled ); } else { this.elements.submit.attr( disabled, true ); } if ( current.step ) { this._trigger( afterSelect, event, state ); this._trigger( state.isMovingForward ? afterForward : afterBackward, event, state ); } }, backward: function( event, howMany ) { if ( typeof event === num ) { howMany = event; event = undefined; } if ( howMany === undefined ) { howMany = 1; } if ( this._currentState.isFirstStep || typeof howMany !== num ) { return; } this._move( -howMany, true, false, function( stepIndex, stepsTaken ) { this._update( event, this._state( stepIndex, stepsTaken ) ); }); }, branch: function( branch ) { return arguments.length ? this._find( branch, this.elements.branches ) : this._currentState.branch; }, branches: function( branch ) { return arguments.length ? this.branch( branch ).children( selector.branch ) : this.elements.branches; }, branchesActivated: function() { return this._find( this._currentState.branchesActivated, this.elements.branches ); }, destroy: function( event, force ) { var self = this, $elements = self.elements, data = [ self.state(), function( response ) { return self.destroy( event, response !== false ); } ]; // args: force if ( typeof event === bool ) { force = event; event = undefined; } if ( force !== true && !self._trigger( beforeDestroy, event, data ) ) { return; } self.elements.backward.off( "." + namespace ); self.elements.forward.off( "." + namespace ); self.element.removeClass( wizard ); $elements.form.removeClass( className.form ); $elements.header.removeClass( className.header ); $elements.steps.show().removeClass( className.step ); $elements.stepsWrapper.removeClass( className.wrapper ); $elements.branches.removeClass( className.branch ); $.Widget.prototype.destroy.call( self ); self._trigger( afterDestroy ); }, form: function() { return this.elements.form; }, forward: function( event, howMany, history ) { if ( typeof event === num ) { history = howMany; howMany = event; event = undefined; } if ( howMany === undefined ) { howMany = 1; } if ( this._currentState.isLastStep || typeof howMany !== num ) { return; } this._move( howMany, true, history, function( stepIndex, stepsTaken ) { this._update( event, this._state( stepIndex, stepsTaken ) ); }); }, isValidStep: function( step, branch ) { return this.isValidStepIndex( this.stepIndex( step, branch ) ); }, isValidStepIndex: function( stepIndex ) { return typeof stepIndex === num && stepIndex >= 0 && stepIndex <= this._lastStepIndex; }, stepCount: function() { return this._stepCount; }, select: function( event, step, branch, relative, history ) { // args: step, branch, relative, history if ( !( event instanceof $.Event ) ) { history = relative; relative = branch; branch = step; step = event; event = undefined; } if ( step === undefined ) { return; } // args: [ step, branch ], relative, history if ( $.isArray( step ) ) { history = relative; relative = branch; branch = step[ 1 ]; step = step[ 0 ]; // args: step, relative, history } else if ( typeof branch === bool ) { history = relative; relative = branch; branch = undefined; // args: step, history } else if ( $.isArray( branch ) ) { history = branch; branch = undefined; } this._move( step, branch, relative, history, function( stepIndex, stepsTaken ) { this._update( event, this._state( stepIndex, stepsTaken ) ); }); }, state: function( step, branch, stepsTaken ) { if ( !arguments.length ) { return this._currentState; } // args: [ step, branch ], stepsTaken if ( $.isArray( step ) ) { stepsTaken = branch; branch = step[ 1 ]; step = step[ 0 ]; // args: step, stepsTaken } else if ( $.isArray( branch ) ) { stepsTaken = branch; branch = undefined; } return this._state( this.stepIndex( step, branch ), stepsTaken ); }, step: function( step, branch ) { if ( !arguments.length ) { return this._currentState.step; } // args: [ step, branch ] if ( $.isArray( step ) ) { branch = step[ 1 ]; step = step[ 0 ]; } var $step, type = typeof step; // Searching for a step by index if ( type === num ) { $step = this._find( step, // Search within branch, if defined, otherwise search all steps branch !== undefined ? this.steps( branch ) : this.elements.steps ); // Searching for a step or branch by string ID, DOM element or jQuery object } else { $step = this._find( step, this.elements.steps.add( this.elements.branches ) ); if ( $step && $step.hasClass( className.branch ) ) { // If a branch is found, the arguments are essentially flip-flopped $step = this._find( branch || 0, this.steps( $step ) ); } } return $step; }, stepIndex: function( step, branch, relative ) { if ( !arguments.length ) { return this._currentState.stepIndex; } var $step; // args: [ step, branch ], relative if ( $.isArray( step ) ) { relative = branch; branch = step[ 1 ]; step = step[ 0 ]; // args: step, relative } else if ( typeof branch === bool ) { relative = branch; branch = undefined; } return ( $step = this.step( step, branch ) ) ? // The returned index can be relative to a branch, or to all steps ( relative ? $step.siblings( selector.step ).andSelf() : this.elements.steps ).index( $step ) : -1; }, steps: function( branch ) { return arguments.length ? this.branch( branch ).children( selector.step ) : this.elements.steps; }, stepsActivated: function() { return this._find( this._currentState.stepsActivated, this.elements.steps ); }, submit: function() { this.elements.form.submit(); } }); })( jQuery );