XH_Digital_Management/static/js/pages/minimal-form/stepsForm.js

278 lines
8.1 KiB
JavaScript
Raw Normal View History

2024-05-29 15:25:17 +08:00
/**
* 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 );