/*!
* wizard
* https://github.com/Voliware/Template
* Licensed under the MIT license.
*/
/**
* Templates, serializes, submits,
* and controls a form wizard
* @extends Form
*/
class Wizard extends Form {
/**
* Constructor
* @param {object} [options]
* @param {object} [options.struct]
* @param {string} [options.struct.$wrapper='.wizard'] - wizard wrapper
* @param {string} [options.struct.$nav='.nav'] - navigation list
* @param {string} [options.struct.$navs='.nav > li'] - navigation links
* @param {string} [options.struct.$tabs='.tab-pane'] - tab container
* @param {string} [options.struct.$next='.pager .next'] - next button
* @param {string} [options.struct.$pager='.pager'] - pager container
* @param {string} [options.struct.$previous='.pager .previous'] - previous button
* @returns {Wizard}
*/
constructor(options){
var defaults = {
struct : {
$wrapper : '.wizard',
$nav : '.nav',
$navs : '.nav > li',
$tabs : '.tab-pane',
$next : '.pager .next',
$pager : '.pager',
$previous : '.pager .previous'
}
};
super($Util.opts(defaults, options));
this.stepCount = this.$tabs.length;
this.step = 0;
// show or hide pagination and form buttons
this.toggleSubmitButton(this.stepCount === 1);
this.togglePreviousButton(false);
this.toggleNextButton(this.stepCount > 1);
this._setHandlers();
return this;
}
/**
* Clear all handlers. Useful if
* the wizard DOM is being re-used.
* @private
*/
_clearHandlers(){
this.$next.off('click.wizard');
this.$previous.off('click.wizard');
this.$submit.off('click.wizard');
this.$navs.each(function(i, e) {
$(e).off('click.wizard');
});
}
/**
* Set pagination and form button handlers
* @returns {Wizard}
* @private
*/
_setHandlers(){
var self = this;
this._clearHandlers();
// next
this.$next.on('click.wizard', function(){
self._getNextNav().find('a').click();
//self.validatePreviousTab();
});
// prev
this.$previous.on('click.wizard', function(){
self.validateTab(self._getTab(self.step));
self._getPreviousNav().find('a').click();
});
// submit
this.$submit.on('click.wizard', function(){
self.validateAllTabs();
});
// navs
this.$navs.each(function(i, e){
$(e).on('click.wizard', function(){
self._setPagination(i);
var x = i;
// nav clicked is ahead
if(i > self.step){
for(x = x - 1; x >= 0; x--){
self.validateTab(self._getTab(x))
}
}
// nav clicked is behind
else if (i < self.step){
for(x; x < self.step + 1; x++){
self.validateTab(self._getTab(x))
}
}
self.step = i;
// reset nav status when going to a tab
self._toggleNavInvalid($(this), false);
});
});
return this;
}
/**
* Create an empty wizard
* @returns {Wizard}
* @private
*/
_useDefaultTemplate(){
super._useDefaultTemplate();
// to avoid duplicate $wrapper's (Wizard inherits Form)
// set this.$form to Form's $wrapper
this.$form = this.$wrapper;
// components
this.$wrapper = $('<div class="wizard"></div>');
this.$nav = $('<ul class="nav"></ul>');
this.$tabs = $('<div class="tab-pane"></div>');
this.$pager = $('<ul class="pager"></ul>');
this.$next = $('<li class="next"><a href="#">Next</a></li>');
this.$previous = $('<li class="previous"><a href="#">Previous</a></li>');
// build
this.$pager.append(this.$previous, this.$next, this.$submit);
this.$footer.append(this.$pager);
this.$form.append(this.$tabs, this.$footer);
this.$wrapper.append(this.$nav, this.$form);
return this;
}
/**
* Attaches a validator to the form
* @returns {Form}
* @private
*/
_setupValidator(){
var v = this.settings.validator;
switch(v.api){
case 'formValidation':
// clone to not affect Form refs
var options = $.extend(true, {}, v.options);
// must validate hidden tabs
options.excluded = [':disabled'];
Wizard.validators.formValidation.setup(this, this.$form, options);
break;
}
return this;
}
/**
* Setup the feedback
* @returns {Form}
* @private
*/
_setupFeedback(){
this.feedback = new Feedback();
if(!this.$feedback.length){
this.$feedback = $('<div class="form-feedback"></div>');
this.$wrapper.prepend(this.$feedback);
}
this.$feedback.html(this.feedback.$wrapper);
return this;
}
// control
/**
* Show or hide pagination
* buttons according to step
* @param {number} step - the step
* @private
*/
_setPagination(step){
// simply hide everything first
this.togglePreviousButton(false);
this.toggleNextButton(false);
this.toggleSubmitButton(false);
switch(step){
// first step
case 0:
this.togglePreviousButton(false);
if(this.stepCount === 1)
this.toggleSubmitButton();
else if(this.stepCount > 1)
this.toggleNextButton();
break;
// last step
case this.stepCount - 1:
this.toggleSubmitButton();
if(this.stepCount > 1)
this.togglePreviousButton();
break;
// inbetween steps
default:
if(this.stepCount > 1){
this.toggleNextButton();
this.togglePreviousButton();
}
break;
}
}
// navs
/**
* Get a nav element by index
* @param {number} index
* @returns {jQuery}
* @private
*/
_getNav(index){
return $(this.$navs.get(index));
}
/**
* Get a nav from a tab element
* @param {jQuery} $tab
* @returns {jQuery}
* @private
*/
_getNavFromTab($tab){
var index = this.$tabs.index($tab);
return this._getNav(index);
}
/**
* Get the previous nav
* @returns {jQuery}
* @private
*/
_getPreviousNav(){
return $(this.$navs.get(this.step - 1));
}
/**
* Get the current nav
* @returns {jQuery}
* @private
*/
_getCurrentNav(){
return $(this.$navs.get(this.step));
}
/**
* Get the next nav
* @returns {jQuery}
* @private
*/
_getNextNav(){
return $(this.$navs.get(this.step + 1));
}
/**
* Toggle a nav as invalid
* @param {jQuery} $nav
* @param {boolean} state
* @returns {Wizard}
* @private
*/
_toggleNavInvalid($nav, state = true){
$nav.toggleClass('wizard-tab invalid', state);
return this;
}
// tabs
/**
* Get a tab based on index
* @param {number} index
* @returns {jQuery}
* @private
*/
_getTab(index){
return $(this.$tabs.get(index));
}
/**
* Get the current tab
* @returns {jQuery}
* @private
*/
_getCurrentTab(){
return $(this.$tabs.get(this.step));
}
/**
* Get the next tab
* @returns {jQuery|null}
* @private
*/
_getNextTab(){
return this.step !== this.stepCount
? $(this.$tabs.get(this.step + 1))
: null;
}
/**
* Get the previous tab
* @returns {jQuery|null}
* @private
*/
_getPreviousTab(){
return this.step > 0
? $(this.$tabs.get(this.step - 1))
: null;
}
// validation
/**
* Validate a tab
* @param {jQuery} $tab
* @returns {boolean}
*/
validateTab($tab){
var api = this.settings.validator.api;
var valid = true;
// todo: add support for other validators
switch(api){
case 'formValidation':
this.validator.validateContainer($tab);
valid = this.validator.isValidContainer($tab);
break;
}
var $nav = this._getNavFromTab($tab);
this._toggleNavInvalid($nav, !valid);
return valid;
}
/**
* Validate the current tab
* @returns {boolean}
*/
validateCurrentTab(){
var $tab = this._getCurrentTab();
return this.validateTab($tab);
}
/**
* Validate the previous tab
* @returns {boolean}
*/
validatePreviousTab(){
var $tab = this._getPreviousTab();
return this.validateTab($tab);
}
/**
* Validate the next tab
* @returns {boolean}
*/
validateNextTab(){
var $tab = this._getNextTab();
return this.validateTab($tab);
}
/**
* Validate all tabs
* @returns {boolean}
*/
validateAllTabs(){
var self = this;
var valid = true;
$.each(this.$tabs, function(i, e){
var $tab = $(e);
self.validator.validateContainer($tab);
var validTab = self.validator.isValidContainer($tab);
self._toggleNavInvalid(self._getNav(i), !validTab);
// set overal validity
// should be invalid if any tab is invalid
if(!validTab){
valid = false;
}
});
return valid;
}
// buttons
/**
* Toggle the next button
* @param {boolean} state
* @returns {Wizard}
*/
toggleNextButton(state = true){
this.$next.toggle(state);
return this;
}
/**
* Toggle the previous button
* @param {boolean} state
* @returns {Wizard}
*/
togglePreviousButton(state = true){
this.$previous.toggle(state);
return this;
}
/**
* Toggle the submit button
* @param {boolean} state
* @returns {Wizard}
*/
toggleSubmitButton(state = true){
this.$submit.toggle(state);
return this;
}
/**
* Toggle wizard components
* @param {boolean} state
* @returns {Wizard}
*/
toggleForm(state){
super.toggleForm(state);
this.$nav.toggle(state);
return this;
}
/**
* Toggle wizard components
* @param {boolean} state
* @returns {Wizard}
*/
slideToggleForm(state){
super.slideToggleForm(state);
this.$nav.slideToggleState(state);
return this;
}
// resets
/**
* Reset nav validation
* @returns {Wizard}
*/
resetNavValidation(){
for(var i = 0; i < this.$navs.length; i++){
var $nav = $(this.$navs[i]);
this._toggleNavInvalid($nav, false);
}
return this;
}
/**
* Reset the form
* @returns {Wizard}
*/
resetForm(){
var $nav = $(this.$navs[0]);
$nav.find('a').click();
this.resetNavValidation();
super.resetForm();
return this;
}
}