/** * 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 );