278 lines
8.1 KiB
JavaScript
278 lines
8.1 KiB
JavaScript
/**
|
||
* stepsForm.js v1.0.0
|
||
* http://www.codrops.com
|
||
*
|
||
* Licensed under the MIT license.
|
||
* http://www.opensource.org/licenses/mit-license.php
|
||
*
|
||
* Copyright 2014, Codrops
|
||
* http://www.codrops.com
|
||
*/
|
||
;( function( window ) {
|
||
|
||
'use strict';
|
||
|
||
var transEndEventNames = {
|
||
'WebkitTransition': 'webkitTransitionEnd',
|
||
'MozTransition': 'transitionend',
|
||
'OTransition': 'oTransitionEnd',
|
||
'msTransition': 'MSTransitionEnd',
|
||
'transition': 'transitionend'
|
||
},
|
||
transEndEventName = transEndEventNames[ Modernizr.prefixed( 'transition' ) ],
|
||
support = { transitions : Modernizr.csstransitions };
|
||
|
||
function extend( a, b ) {
|
||
for( var key in b ) {
|
||
if( b.hasOwnProperty( key ) ) {
|
||
a[key] = b[key];
|
||
}
|
||
}
|
||
return a;
|
||
}
|
||
|
||
function stepsForm( el, options ) {
|
||
this.el = el;
|
||
this.options = extend( {}, this.options );
|
||
extend( this.options, options );
|
||
this._init();
|
||
}
|
||
|
||
// generates a unique id
|
||
function randomID() {
|
||
var id = Math.random().toString(36).substr(2, 9);
|
||
if (document.getElementById(id)) {
|
||
return randomID();
|
||
}
|
||
return id;
|
||
}
|
||
|
||
stepsForm.prototype.options = {
|
||
onSubmit : function() { return false; }
|
||
};
|
||
|
||
stepsForm.prototype._init = function() {
|
||
// current question
|
||
this.current = 0;
|
||
|
||
// questions
|
||
this.questions = [].slice.call( this.el.querySelectorAll( 'ol.questions > li' ) );
|
||
// total questions
|
||
this.questionsCount = this.questions.length;
|
||
// show first question
|
||
classie.addClass( this.questions[0], 'current' );
|
||
|
||
// next question control
|
||
this.ctrlNext = this.el.querySelector( 'button.next' );
|
||
this.ctrlNext.setAttribute( 'aria-label', 'Next' );
|
||
|
||
// progress bar
|
||
this.progress = this.el.querySelector( 'div.progress' );
|
||
// set progressbar attributes
|
||
this.progress.setAttribute( 'role', 'progressbar' );
|
||
this.progress.setAttribute( 'aria-readonly', 'true' );
|
||
this.progress.setAttribute( 'aria-valuemin', '0' );
|
||
this.progress.setAttribute( 'aria-valuemax', '100' );
|
||
this.progress.setAttribute( 'aria-valuenow', '0' );
|
||
|
||
// question number status
|
||
this.questionStatus = this.el.querySelector( 'span.number' );
|
||
// give the questions status an id
|
||
this.questionStatus.id = this.questionStatus.id || randomID();
|
||
// associate "x / y" with the input via aria-describedby
|
||
for (var i = this.questions.length - 1; i >= 0; i--) {
|
||
var formElement = this.questions[i].querySelector( 'input, textarea, select' );
|
||
formElement.setAttribute( 'aria-describedby', this.questionStatus.id );
|
||
};
|
||
// current question placeholder
|
||
this.currentNum = this.questionStatus.querySelector( 'span.number-current' );
|
||
this.currentNum.innerHTML = Number( this.current + 1 );
|
||
// total questions placeholder
|
||
this.totalQuestionNum = this.questionStatus.querySelector( 'span.number-total' );
|
||
this.totalQuestionNum.innerHTML = this.questionsCount;
|
||
|
||
// error message
|
||
this.error = this.el.querySelector( 'span.error-message' );
|
||
|
||
// checks for HTML5 Form Validation support
|
||
// a cleaner solution might be to add form validation to the custom Modernizr script
|
||
this.supportsHTML5Forms = typeof document.createElement("input").checkValidity === 'function';
|
||
|
||
// init events
|
||
this._initEvents();
|
||
};
|
||
|
||
stepsForm.prototype._initEvents = function() {
|
||
var self = this,
|
||
// first input
|
||
firstElInput = this.questions[ this.current ].querySelector( 'input, textarea, select' ),
|
||
// focus
|
||
onFocusStartFn = function() {
|
||
firstElInput.removeEventListener( 'focus', onFocusStartFn );
|
||
classie.addClass( self.ctrlNext, 'show' );
|
||
};
|
||
|
||
// show the next question control first time the input gets focused
|
||
firstElInput.addEventListener( 'focus', onFocusStartFn );
|
||
|
||
// show next question
|
||
this.ctrlNext.addEventListener( 'click', function( ev ) {
|
||
ev.preventDefault();
|
||
self._nextQuestion();
|
||
} );
|
||
|
||
// pressing enter will jump to next question
|
||
this.el.addEventListener( 'keydown', function( ev ) {
|
||
var keyCode = ev.keyCode || ev.which;
|
||
// enter
|
||
if( keyCode === 13 ) {
|
||
ev.preventDefault();
|
||
self._nextQuestion();
|
||
}
|
||
} );
|
||
};
|
||
|
||
stepsForm.prototype._nextQuestion = function() {
|
||
if( !this._validate() ) {
|
||
return false;
|
||
}
|
||
|
||
// checks HTML5 validation
|
||
if ( this.supportsHTML5Forms ) {
|
||
var input = this.questions[ this.current ].querySelector( 'input, textarea, select' );
|
||
// clear any previous error messages
|
||
input.setCustomValidity( '' );
|
||
|
||
// checks input against the validation constraint
|
||
if ( !input.checkValidity() ) {
|
||
// Optionally, set a custom HTML5 valiation message
|
||
// comment or remove this line to use the browser default message
|
||
input.setCustomValidity( 'Whoops, that\'s not an email address!' );
|
||
// display the HTML5 error message
|
||
this._showError( input.validationMessage );
|
||
// prevent the question from changing
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// check if form is filled
|
||
if( this.current === this.questionsCount - 1 ) {
|
||
this.isFilled = true;
|
||
}
|
||
|
||
// clear any previous error messages
|
||
this._clearError();
|
||
|
||
// current question
|
||
var currentQuestion = this.questions[ this.current ];
|
||
|
||
// increment current question iterator
|
||
++this.current;
|
||
|
||
// update progress bar
|
||
this._progress();
|
||
|
||
if( !this.isFilled ) {
|
||
// change the current question number/status
|
||
this._updateQuestionNumber();
|
||
|
||
// add class "show-next" to form element (start animations)
|
||
classie.addClass( this.el, 'show-next' );
|
||
|
||
// remove class "current" from current question and add it to the next one
|
||
// current question
|
||
var nextQuestion = this.questions[ this.current ];
|
||
classie.removeClass( currentQuestion, 'current' );
|
||
classie.addClass( nextQuestion, 'current' );
|
||
}
|
||
|
||
// after animation ends, remove class "show-next" from form element and change current question placeholder
|
||
var self = this,
|
||
onEndTransitionFn = function( ev ) {
|
||
if( support.transitions ) {
|
||
this.removeEventListener( transEndEventName, onEndTransitionFn );
|
||
}
|
||
if( self.isFilled ) {
|
||
self._submit();
|
||
}
|
||
else {
|
||
classie.removeClass( self.el, 'show-next' );
|
||
self.currentNum.innerHTML = self.nextQuestionNum.innerHTML;
|
||
self.questionStatus.removeChild( self.nextQuestionNum );
|
||
// force the focus on the next input
|
||
nextQuestion.querySelector( 'input, textarea, select' ).focus();
|
||
}
|
||
};
|
||
|
||
if( support.transitions ) {
|
||
this.progress.addEventListener( transEndEventName, onEndTransitionFn );
|
||
}
|
||
else {
|
||
onEndTransitionFn();
|
||
}
|
||
}
|
||
|
||
// updates the progress bar by setting its width
|
||
stepsForm.prototype._progress = function() {
|
||
var currentProgress = this.current * ( 100 / this.questionsCount );
|
||
this.progress.style.width = currentProgress + '%';
|
||
// update the progressbar's aria-valuenow attribute
|
||
this.progress.setAttribute('aria-valuenow', currentProgress);
|
||
}
|
||
|
||
// changes the current question number
|
||
stepsForm.prototype._updateQuestionNumber = function() {
|
||
// first, create next question number placeholder
|
||
this.nextQuestionNum = document.createElement( 'span' );
|
||
this.nextQuestionNum.className = 'number-next';
|
||
this.nextQuestionNum.innerHTML = Number( this.current + 1 );
|
||
// insert it in the DOM
|
||
this.questionStatus.appendChild( this.nextQuestionNum );
|
||
}
|
||
|
||
// submits the form
|
||
stepsForm.prototype._submit = function() {
|
||
this.options.onSubmit( this.el );
|
||
}
|
||
|
||
// TODO (next version..)
|
||
// the validation function
|
||
stepsForm.prototype._validate = function() {
|
||
// current question´s input
|
||
var input = this.questions[ this.current ].querySelector( 'input, textarea, select' ).value;
|
||
if( input === '' ) {
|
||
this._showError( 'EMPTYSTR' );
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// TODO (next version..)
|
||
stepsForm.prototype._showError = function( err ) {
|
||
var message = '';
|
||
switch( err ) {
|
||
case 'EMPTYSTR' :
|
||
message = 'Please fill the field before continuing';
|
||
break;
|
||
case 'INVALIDEMAIL' :
|
||
message = 'Please fill a valid email address';
|
||
break;
|
||
// ...
|
||
default :
|
||
message = err;
|
||
};
|
||
this.error.innerHTML = message;
|
||
classie.addClass( this.error, 'show' );
|
||
}
|
||
|
||
// clears/hides the current error message
|
||
stepsForm.prototype._clearError = function() {
|
||
classie.removeClass( this.error, 'show' );
|
||
}
|
||
|
||
// add to global namespace
|
||
window.stepsForm = stepsForm;
|
||
|
||
})( window );
|