Created
August 9, 2013 00:43
-
-
Save flxa/6190240 to your computer and use it in GitHub Desktop.
Parsley.js
From http://assets.atjayjo.com/js/parsley.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Parsley.js allows you to verify your form inputs frontend side, without writing a line of javascript. Or so.. | |
| * | |
| * Author: Guillaume Potier - @guillaumepotier | |
| */ | |
| !function ($) { | |
| 'use strict'; | |
| /** | |
| * Validator class stores all constraints functions and associated messages. | |
| * Provides public interface to add, remove or modify them | |
| * | |
| * @class Validator | |
| * @constructor | |
| */ | |
| var Validator = function ( options ) { | |
| /** | |
| * Error messages | |
| * | |
| * @property messages | |
| * @type {Object} | |
| */ | |
| this.messages = { | |
| defaultMessage: "This value seems to be invalid." | |
| , type: { | |
| email: "This value should be a valid email." | |
| , url: "This value should be a valid url." | |
| , urlstrict: "This value should be a valid url." | |
| , number: "This value should be a valid number." | |
| , digits: "This value should be digits." | |
| , dateIso: "This value should be a valid date (YYYY-MM-DD)." | |
| , alphanum: "This value should be alphanumeric." | |
| , phone: "This value should be a valid phone number." | |
| } | |
| , notnull: "This value should not be null." | |
| , notblank: "This value should not be blank." | |
| , required: "This value is required." | |
| , regexp: "This value seems to be invalid." | |
| , min: "This value should be greater than or equal to %s." | |
| , max: "This value should be lower than or equal to %s." | |
| , range: "This value should be between %s and %s." | |
| , minlength: "This value is too short. It should have %s characters or more." | |
| , maxlength: "This value is too long. It should have %s characters or less." | |
| , rangelength: "This value length is invalid. It should be between %s and %s characters long." | |
| , mincheck: "You must select at least %s choices." | |
| , maxcheck: "You must select %s choices or less." | |
| , rangecheck: "You must select between %s and %s choices." | |
| , equalto: "This value should be the same." | |
| }, | |
| this.init( options ); | |
| }; | |
| Validator.prototype = { | |
| constructor: Validator | |
| /** | |
| * Validator list. Built-in validators functions | |
| * | |
| * @property validators | |
| * @type {Object} | |
| */ | |
| , validators: { | |
| notnull: function ( val ) { | |
| return val.length > 0; | |
| } | |
| , notblank: function ( val ) { | |
| return 'string' === typeof val && '' !== val.replace( /^\s+/g, '' ).replace( /\s+$/g, '' ); | |
| } | |
| // Works on all inputs. val is object for checkboxes | |
| , required: function ( val ) { | |
| // for checkboxes and select multiples. Check there is at least one required value | |
| if ( 'object' === typeof val ) { | |
| for ( var i in val ) { | |
| if ( this.required( val[ i ] ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| return this.notnull( val ) && this.notblank( val ); | |
| } | |
| , type: function ( val, type ) { | |
| var regExp; | |
| switch ( type ) { | |
| case 'number': | |
| regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/; | |
| break; | |
| case 'digits': | |
| regExp = /^\d+$/; | |
| break; | |
| case 'alphanum': | |
| regExp = /^\w+$/; | |
| break; | |
| case 'email': | |
| regExp = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; | |
| break; | |
| case 'url': | |
| val = new RegExp( '(https?|s?ftp|git)', 'i' ).test( val ) ? val : 'http://' + val; | |
| /* falls through */ | |
| case 'urlstrict': | |
| regExp = /^(https?|s?ftp|git):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; | |
| break; | |
| case 'dateIso': | |
| regExp = /^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$/; | |
| break; | |
| case 'phone': | |
| regExp = /^((\+\d{1,3}(-| )?\(?\d\)?(-| )?\d{1,5})|(\(?\d{2,6}\)?))(-| )?(\d{3,4})(-| )?(\d{4})(( x| ext)\d{1,5}){0,1}$/; | |
| break; | |
| default: | |
| return false; | |
| } | |
| // test regExp if not null | |
| return '' !== val ? regExp.test( val ) : false; | |
| } | |
| , regexp: function ( val, regExp, self ) { | |
| return new RegExp( regExp, self.options.regexpFlag || '' ).test( val ); | |
| } | |
| , minlength: function ( val, min ) { | |
| return val.length >= min; | |
| } | |
| , maxlength: function ( val, max ) { | |
| return val.length <= max; | |
| } | |
| , rangelength: function ( val, arrayRange ) { | |
| return this.minlength( val, arrayRange[ 0 ] ) && this.maxlength( val, arrayRange[ 1 ] ); | |
| } | |
| , min: function ( val, min ) { | |
| return Number( val ) >= min; | |
| } | |
| , max: function ( val, max ) { | |
| return Number( val ) <= max; | |
| } | |
| , range: function ( val, arrayRange ) { | |
| return val >= arrayRange[ 0 ] && val <= arrayRange[ 1 ]; | |
| } | |
| , equalto: function ( val, elem, self ) { | |
| self.options.validateIfUnchanged = true; | |
| return val === $( elem ).val(); | |
| } | |
| , remote: function ( val, url, self ) { | |
| var result = null | |
| , data = {} | |
| , dataType = {}; | |
| data[ self.$element.attr( 'name' ) ] = val; | |
| if ( 'undefined' !== typeof self.options.remoteDatatype ) { | |
| dataType = { dataType: self.options.remoteDatatype }; | |
| } | |
| var manage = function ( isConstraintValid, message ) { | |
| // remove error message if we got a server message, different from previous message | |
| if ( 'undefined' !== typeof message && 'undefined' !== typeof self.Validator.messages.remote && message !== self.Validator.messages.remote ) { | |
| $( self.ulError + ' .remote' ).remove(); | |
| } | |
| self.updtConstraint( { name: 'remote', valid: isConstraintValid }, message ); | |
| self.manageValidationResult(); | |
| }; | |
| // transform string response into object | |
| var handleResponse = function ( response ) { | |
| if ( 'object' === typeof response ) { | |
| return response; | |
| } | |
| try { | |
| response = $.parseJSON( response ); | |
| } catch ( err ) {} | |
| return response; | |
| } | |
| var manageErrorMessage = function ( response ) { | |
| return 'object' === typeof response && null !== response ? ( 'undefined' !== typeof response.error ? response.error : ( 'undefined' !== typeof response.message ? response.message : null ) ) : null; | |
| } | |
| $.ajax( $.extend( {}, { | |
| url: url | |
| , data: data | |
| , type: self.options.remoteMethod || 'GET' | |
| , success: function ( response ) { | |
| response = handleResponse( response ); | |
| manage( 1 === response || true === response || ( 'object' === typeof response && null !== response && 'undefined' !== typeof response.success ), manageErrorMessage( response ) | |
| ); | |
| } | |
| , error: function ( response ) { | |
| response = handleResponse( response ); | |
| manage( false, manageErrorMessage( response ) ); | |
| } | |
| }, dataType ) ); | |
| return result; | |
| } | |
| /** | |
| * Aliases for checkboxes constraints | |
| */ | |
| , mincheck: function ( obj, val ) { | |
| return this.minlength( obj, val ); | |
| } | |
| , maxcheck: function ( obj, val ) { | |
| return this.maxlength( obj, val); | |
| } | |
| , rangecheck: function ( obj, arrayRange ) { | |
| return this.rangelength( obj, arrayRange ); | |
| } | |
| } | |
| /* | |
| * Register custom validators and messages | |
| */ | |
| , init: function ( options ) { | |
| var customValidators = options.validators | |
| , customMessages = options.messages; | |
| var key; | |
| for ( key in customValidators ) { | |
| this.addValidator(key, customValidators[ key ]); | |
| } | |
| for ( key in customMessages ) { | |
| this.addMessage(key, customMessages[ key ]); | |
| } | |
| } | |
| /** | |
| * Replace %s placeholders by values | |
| * | |
| * @method formatMesssage | |
| * @param {String} message Message key | |
| * @param {Mixed} args Args passed by validators functions. Could be string, number or object | |
| * @return {String} Formatted string | |
| */ | |
| , formatMesssage: function ( message, args ) { | |
| if ( 'object' === typeof args ) { | |
| for ( var i in args ) { | |
| message = this.formatMesssage( message, args[ i ] ); | |
| } | |
| return message; | |
| } | |
| return 'string' === typeof message ? message.replace( new RegExp( '%s', 'i' ), args ) : ''; | |
| } | |
| /** | |
| * Add / override a validator in validators list | |
| * | |
| * @method addValidator | |
| * @param {String} name Validator name. Will automatically bindable through data-name='' | |
| * @param {Function} fn Validator function. Must return {Boolean} | |
| */ | |
| , addValidator: function ( name, fn ) { | |
| this.validators[ name ] = fn; | |
| } | |
| /** | |
| * Add / override error message | |
| * | |
| * @method addMessage | |
| * @param {String} name Message name. Will automatically be binded to validator with same name | |
| * @param {String} message Message | |
| */ | |
| , addMessage: function ( key, message, type ) { | |
| if ( 'undefined' !== typeof type && true === type ) { | |
| this.messages.type[ key ] = message; | |
| return; | |
| } | |
| // custom types messages are a bit tricky cuz' nested ;) | |
| if ( 'type' === key ) { | |
| for ( var i in message ) { | |
| this.messages.type[ i ] = message[ i ]; | |
| } | |
| return; | |
| } | |
| this.messages[ key ] = message; | |
| } | |
| }; | |
| /** | |
| * ParsleyField class manage each form field inside a validated Parsley form. | |
| * Returns if field valid or not depending on its value and constraints | |
| * Manage field error display and behavior, event triggers and more | |
| * | |
| * @class ParsleyField | |
| * @constructor | |
| */ | |
| var ParsleyField = function ( element, options, type ) { | |
| this.options = options; | |
| this.Validator = new Validator( options ); | |
| // if type is ParsleyFieldMultiple, just return this. used for clone | |
| if ( type === 'ParsleyFieldMultiple' ) { | |
| return this; | |
| } | |
| this.init( element, type || 'ParsleyField' ); | |
| }; | |
| ParsleyField.prototype = { | |
| constructor: ParsleyField | |
| /** | |
| * Set some properties, bind constraint validators and validation events | |
| * | |
| * @method init | |
| * @param {Object} element | |
| * @param {Object} options | |
| */ | |
| , init: function ( element, type ) { | |
| this.type = type; | |
| this.valid = true; | |
| this.element = element; | |
| this.validatedOnce = false; | |
| this.$element = $( element ); | |
| this.val = this.$element.val(); | |
| this.isRequired = false; | |
| this.constraints = {}; | |
| // overriden by ParsleyItemMultiple if radio or checkbox input | |
| if ( 'undefined' === typeof this.isRadioOrCheckbox ) { | |
| this.isRadioOrCheckbox = false; | |
| this.hash = this.generateHash(); | |
| this.errorClassHandler = this.options.errors.classHandler( element, this.isRadioOrCheckbox ) || this.$element; | |
| } | |
| // error ul dom management done only once at init | |
| this.ulErrorManagement(); | |
| // bind some html5 properties | |
| this.bindHtml5Constraints(); | |
| // bind validators to field | |
| this.addConstraints(); | |
| // bind parsley events if validators have been registered | |
| if ( this.hasConstraints() ) { | |
| this.bindValidationEvents(); | |
| } | |
| } | |
| , setParent: function ( elem ) { | |
| this.$parent = $( elem ); | |
| } | |
| , getParent: function () { | |
| return this.$parent; | |
| } | |
| /** | |
| * Bind some extra html5 types / validators | |
| * | |
| * @private | |
| * @method bindHtml5Constraints | |
| */ | |
| , bindHtml5Constraints: function () { | |
| // add html5 required support + class required support | |
| if ( this.$element.hasClass( 'required' ) || this.$element.prop( 'required' ) ) { | |
| this.options.required = true; | |
| } | |
| // add html5 supported types & options | |
| if ( 'undefined' !== typeof this.$element.attr( 'type' ) && new RegExp( this.$element.attr( 'type' ), 'i' ).test( 'email url number range' ) ) { | |
| this.options.type = this.$element.attr( 'type' ); | |
| // number and range types could have min and/or max values | |
| if ( new RegExp( this.options.type, 'i' ).test( 'number range' ) ) { | |
| this.options.type = 'number'; | |
| // double condition to support jQuery and Zepto.. :( | |
| if ( 'undefined' !== typeof this.$element.attr( 'min' ) && this.$element.attr( 'min' ).length ) { | |
| this.options.min = this.$element.attr( 'min' ); | |
| } | |
| if ( 'undefined' !== typeof this.$element.attr( 'max' ) && this.$element.attr( 'max' ).length ) { | |
| this.options.max = this.$element.attr( 'max' ); | |
| } | |
| } | |
| } | |
| if ( 'string' === typeof this.$element.attr( 'pattern' ) && this.$element.attr( 'pattern' ).length ) { | |
| this.options.regexp = this.$element.attr( 'pattern' ); | |
| } | |
| } | |
| /** | |
| * Attach field validators functions passed through data-api | |
| * | |
| * @private | |
| * @method addConstraints | |
| */ | |
| , addConstraints: function () { | |
| for ( var constraint in this.options ) { | |
| var addConstraint = {}; | |
| addConstraint[ constraint ] = this.options[ constraint ]; | |
| this.addConstraint( addConstraint, true ); | |
| } | |
| } | |
| /** | |
| * Dynamically add a new constraint to a field | |
| * | |
| * @method addConstraint | |
| * @param {Object} constraint { name: requirements } | |
| */ | |
| , addConstraint: function ( constraint, doNotUpdateValidationEvents ) { | |
| for ( var name in constraint ) { | |
| name = name.toLowerCase(); | |
| if ( 'function' === typeof this.Validator.validators[ name ] ) { | |
| this.constraints[ name ] = { | |
| name: name | |
| , requirements: constraint[ name ] | |
| , valid: null | |
| } | |
| if ( name === 'required' ) { | |
| this.isRequired = true; | |
| } | |
| this.addCustomConstraintMessage( name ); | |
| } | |
| } | |
| // force field validation next check and reset validation events | |
| if ( 'undefined' === typeof doNotUpdateValidationEvents ) { | |
| this.bindValidationEvents(); | |
| } | |
| } | |
| /** | |
| * Dynamically update an existing constraint to a field. | |
| * Simple API: { name: requirements } | |
| * | |
| * @method updtConstraint | |
| * @param {Object} constraint | |
| */ | |
| , updateConstraint: function ( constraint, message ) { | |
| for ( var name in constraint ) { | |
| this.updtConstraint( { name: name, requirements: constraint[ name ], valid: null }, message ); | |
| } | |
| } | |
| /** | |
| * Dynamically update an existing constraint to a field. | |
| * Complex API: { name: name, requirements: requirements, valid: boolean } | |
| * | |
| * @method updtConstraint | |
| * @param {Object} constraint | |
| */ | |
| , updtConstraint: function ( constraint, message ) { | |
| this.constraints[ constraint.name ] = $.extend( true, this.constraints[ constraint.name ], constraint ); | |
| if ( 'string' === typeof message ) { | |
| this.Validator.messages[ constraint.name ] = message ; | |
| } | |
| // force field validation next check and reset validation events | |
| this.bindValidationEvents(); | |
| } | |
| /** | |
| * Dynamically remove an existing constraint to a field. | |
| * | |
| * @method removeConstraint | |
| * @param {String} constraintName | |
| */ | |
| , removeConstraint: function ( constraintName ) { | |
| var constraintName = constraintName.toLowerCase(); | |
| delete this.constraints[ constraintName ]; | |
| if ( constraintName === 'required' ) { | |
| this.isRequired = false; | |
| } | |
| // if there are no more constraint, destroy parsley instance for this field | |
| if ( !this.hasConstraints() ) { | |
| // in a form context, remove item from parent | |
| if ( 'ParsleyForm' === typeof this.getParent() ) { | |
| this.getParent().removeItem( this.$element ); | |
| return; | |
| } | |
| this.destroy(); | |
| return; | |
| } | |
| this.bindValidationEvents(); | |
| } | |
| /** | |
| * Add custom constraint message, passed through data-API | |
| * | |
| * @private | |
| * @method addCustomConstraintMessage | |
| * @param constraint | |
| */ | |
| , addCustomConstraintMessage: function ( constraint ) { | |
| // custom message type data-type-email-message -> typeEmailMessage | data-minlength-error => minlengthMessage | |
| var customMessage = constraint | |
| + ( 'type' === constraint && 'undefined' !== typeof this.options[ constraint ] ? this.options[ constraint ].charAt( 0 ).toUpperCase() + this.options[ constraint ].substr( 1 ) : '' ) | |
| + 'Message'; | |
| if ( 'undefined' !== typeof this.options[ customMessage ] ) { | |
| this.Validator.addMessage( 'type' === constraint ? this.options[ constraint ] : constraint, this.options[ customMessage ], 'type' === constraint ); | |
| } | |
| } | |
| /** | |
| * Bind validation events on a field | |
| * | |
| * @private | |
| * @method bindValidationEvents | |
| */ | |
| , bindValidationEvents: function () { | |
| // this field has validation events, that means it has to be validated | |
| this.valid = null; | |
| this.$element.addClass( 'parsley-validated' ); | |
| // remove eventually already binded events | |
| this.$element.off( '.' + this.type ); | |
| // force add 'change' event if async remote validator here to have result before form submitting | |
| if ( this.options.remote && !new RegExp( 'change', 'i' ).test( this.options.trigger ) ) { | |
| this.options.trigger = !this.options.trigger ? 'change' : ' change'; | |
| } | |
| // alaways bind keyup event, for better UX when a field is invalid | |
| var triggers = ( !this.options.trigger ? '' : this.options.trigger ) | |
| + ( new RegExp( 'key', 'i' ).test( this.options.trigger ) ? '' : ' keyup' ); | |
| // alaways bind change event, for better UX when a select is invalid | |
| if ( this.$element.is( 'select' ) ) { | |
| triggers += new RegExp( 'change', 'i' ).test( triggers ) ? '' : ' change'; | |
| } | |
| // trim triggers to bind them correctly with .on() | |
| triggers = triggers.replace( /^\s+/g , '' ).replace( /\s+$/g , '' ); | |
| this.$element.on( ( triggers + ' ' ).split( ' ' ).join( '.' + this.type + ' ' ), false, $.proxy( this.eventValidation, this ) ); | |
| } | |
| /** | |
| * Hash management. Used for ul error | |
| * | |
| * @method generateHash | |
| * @returns {String} 5 letters unique hash | |
| */ | |
| , generateHash: function () { | |
| return 'parsley-' + ( Math.random() + '' ).substring( 2 ); | |
| } | |
| /** | |
| * Public getHash accessor | |
| * | |
| * @method getHash | |
| * @returns {String} hash | |
| */ | |
| , getHash: function () { | |
| return this.hash; | |
| } | |
| /** | |
| * Returns field val needed for validation | |
| * Special treatment for radio & checkboxes | |
| * | |
| * @method getVal | |
| * @returns {String} val | |
| */ | |
| , getVal: function () { | |
| return this.$element.data('value') || this.$element.val(); | |
| } | |
| /** | |
| * Called when validation is triggered by an event | |
| * Do nothing if val.length < this.options.validationMinlength | |
| * | |
| * @method eventValidation | |
| * @param {Object} event jQuery event | |
| */ | |
| , eventValidation: function ( event ) { | |
| var val = this.getVal(); | |
| // do nothing on keypress event if not explicitely passed as data-trigger and if field has not already been validated once | |
| if ( event.type === 'keyup' && !/keyup/i.test( this.options.trigger ) && !this.validatedOnce ) { | |
| return true; | |
| } | |
| // do nothing on change event if not explicitely passed as data-trigger and if field has not already been validated once | |
| if ( event.type === 'change' && !/change/i.test( this.options.trigger ) && !this.validatedOnce ) { | |
| return true; | |
| } | |
| // start validation process only if field has enough chars and validation never started | |
| if ( !this.isRadioOrCheckbox && val.length < this.options.validationMinlength && !this.validatedOnce ) { | |
| return true; | |
| } | |
| this.validate(); | |
| } | |
| /** | |
| * Return if field verify its constraints | |
| * | |
| * @method isValid | |
| * @return {Boolean} Is field valid or not | |
| */ | |
| , isValid: function () { | |
| return this.validate( false ); | |
| } | |
| /** | |
| * Return if field has constraints | |
| * | |
| * @method hasConstraints | |
| * @return {Boolean} Is field has constraints or not | |
| */ | |
| , hasConstraints: function () { | |
| for ( var constraint in this.constraints ) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Validate a field & display errors | |
| * | |
| * @method validate | |
| * @param {Boolean} errorBubbling set to false if you just want valid boolean without error bubbling next to fields | |
| * @return {Boolean} Is field valid or not | |
| */ | |
| , validate: function ( errorBubbling ) { | |
| var val = this.getVal() | |
| , valid = null; | |
| // do not even bother trying validating a field w/o constraints | |
| if ( !this.hasConstraints() ) { | |
| return null; | |
| } | |
| // reset Parsley validation if onFieldValidate returns true, or if field is empty and not required | |
| if ( this.options.listeners.onFieldValidate( this.element, this ) || ( '' === val && !this.isRequired ) ) { | |
| this.reset(); | |
| return null; | |
| } | |
| // do not validate a field already validated and unchanged ! | |
| if ( !this.needsValidation( val ) ) { | |
| return this.valid; | |
| } | |
| valid = this.applyValidators(); | |
| if ( 'undefined' !== typeof errorBubbling ? errorBubbling : this.options.showErrors ) { | |
| this.manageValidationResult(); | |
| } | |
| return valid; | |
| } | |
| /** | |
| * Check if value has changed since previous validation | |
| * | |
| * @method needsValidation | |
| * @param value | |
| * @return {Boolean} | |
| */ | |
| , needsValidation: function ( val ) { | |
| if ( !this.options.validateIfUnchanged && this.valid !== null && this.val === val && this.validatedOnce ) { | |
| return false; | |
| } | |
| this.val = val; | |
| return this.validatedOnce = true; | |
| } | |
| /** | |
| * Loop through every fields validators | |
| * Adds errors after unvalid fields | |
| * | |
| * @method applyValidators | |
| * @return {Mixed} {Boolean} If field valid or not, null if not validated | |
| */ | |
| , applyValidators: function () { | |
| var valid = null; | |
| for ( var constraint in this.constraints ) { | |
| var result = this.Validator.validators[ this.constraints[ constraint ].name ]( this.val, this.constraints[ constraint ].requirements, this ); | |
| if ( false === result ) { | |
| valid = false; | |
| this.constraints[ constraint ].valid = valid; | |
| this.options.listeners.onFieldError( this.element, this.constraints, this ); | |
| } else if ( true === result ) { | |
| this.constraints[ constraint ].valid = true; | |
| valid = false !== valid; | |
| this.options.listeners.onFieldSuccess( this.element, this.constraints, this ); | |
| } | |
| } | |
| return valid; | |
| } | |
| /** | |
| * Fired when all validators have be executed | |
| * Returns true or false if field is valid or not | |
| * Display errors messages below failed fields | |
| * Adds parsley-success or parsley-error class on fields | |
| * | |
| * @method manageValidationResult | |
| * @return {Boolean} Is field valid or not | |
| */ | |
| , manageValidationResult: function () { | |
| var valid = null; | |
| for ( var constraint in this.constraints ) { | |
| if ( false === this.constraints[ constraint ].valid ) { | |
| this.manageError( this.constraints[ constraint ] ); | |
| valid = false; | |
| } else if ( true === this.constraints[ constraint ].valid ) { | |
| this.removeError( this.constraints[ constraint ].name ); | |
| valid = false !== valid; | |
| } | |
| } | |
| this.valid = valid; | |
| if ( true === this.valid ) { | |
| this.removeErrors(); | |
| this.errorClassHandler.removeClass( this.options.errorClass ).addClass( this.options.successClass ); | |
| return true; | |
| } else if ( false === this.valid ) { | |
| this.errorClassHandler.removeClass( this.options.successClass ).addClass( this.options.errorClass ); | |
| return false; | |
| } | |
| return valid; | |
| } | |
| /** | |
| * Manage ul error Container | |
| * | |
| * @private | |
| * @method ulErrorManagement | |
| */ | |
| , ulErrorManagement: function () { | |
| this.ulError = '#' + this.hash; | |
| this.ulTemplate = $( this.options.errors.errorsWrapper ).attr( 'id', this.hash ).addClass( 'parsley-error-list' ); | |
| } | |
| /** | |
| * Remove li / ul error | |
| * | |
| * @method removeError | |
| * @param {String} constraintName Method Name | |
| */ | |
| , removeError: function ( constraintName ) { | |
| var liError = this.ulError + ' .' + constraintName | |
| , that = this; | |
| this.options.animate ? $( liError ).fadeOut( this.options.animateDuration, function () { | |
| $( this ).remove(); | |
| if ( that.ulError && $( that.ulError ).children().length === 0 ) { | |
| that.removeErrors(); | |
| } } ) : $( liError ).remove(); | |
| // remove li error, and ul error if no more li inside | |
| if ( this.ulError && $( this.ulError ).children().length === 0 ) { | |
| this.removeErrors(); | |
| } | |
| } | |
| /** | |
| * Add li error | |
| * | |
| * @method addError | |
| * @param {Object} { minlength: "error message for minlength constraint" } | |
| */ | |
| , addError: function ( error ) { | |
| for ( var constraint in error ) { | |
| var liTemplate = $( this.options.errors.errorElem ).addClass( constraint ); | |
| $( this.ulError ).append( this.options.animate ? $( liTemplate ).html( error[ constraint ] ).hide().fadeIn( this.options.animateDuration ) : $( liTemplate ).html( error[ constraint ] ) ); | |
| } | |
| } | |
| /** | |
| * Remove all ul / li errors | |
| * | |
| * @method removeErrors | |
| */ | |
| , removeErrors: function () { | |
| this.options.animate ? $( this.ulError ).fadeOut( this.options.animateDuration, function () { $( this ).remove(); } ) : $( this.ulError ).remove(); | |
| } | |
| /** | |
| * Remove ul errors and parsley error or success classes | |
| * | |
| * @method reset | |
| */ | |
| , reset: function () { | |
| this.valid = null; | |
| this.removeErrors(); | |
| this.validatedOnce = false; | |
| this.errorClassHandler.removeClass( this.options.successClass ).removeClass( this.options.errorClass ); | |
| for ( var constraint in this.constraints ) { | |
| this.constraints[ constraint ].valid = null; | |
| } | |
| return this; | |
| } | |
| /** | |
| * Add li / ul errors messages | |
| * | |
| * @method manageError | |
| * @param {Object} constraint | |
| */ | |
| , manageError: function ( constraint ) { | |
| // display ulError container if it has been removed previously (or never shown) | |
| if ( !$( this.ulError ).length ) { | |
| this.manageErrorContainer(); | |
| } | |
| // TODO: refacto properly | |
| // if required constraint but field is not null, do not display | |
| if ( 'required' === constraint.name && null !== this.getVal() && this.getVal().length > 0 ) { | |
| return; | |
| // if empty required field and non required constraint fails, do not display | |
| } else if ( this.isRequired && 'required' !== constraint.name && ( null === this.getVal() || 0 === this.getVal().length ) ) { | |
| return; | |
| } | |
| // TODO: refacto error name w/ proper & readable function | |
| var constraintName = constraint.name | |
| , liClass = false !== this.options.errorMessage ? 'custom-error-message' : constraintName | |
| , liError = {} | |
| , message = false !== this.options.errorMessage ? this.options.errorMessage : ( constraint.name === 'type' ? | |
| this.Validator.messages[ constraintName ][ constraint.requirements ] : ( 'undefined' === typeof this.Validator.messages[ constraintName ] ? | |
| this.Validator.messages.defaultMessage : this.Validator.formatMesssage( this.Validator.messages[ constraintName ], constraint.requirements ) ) ); | |
| // add liError if not shown. Do not add more than once custom errorMessage if exist | |
| if ( !$( this.ulError + ' .' + liClass ).length ) { | |
| liError[ liClass ] = message; | |
| this.addError( liError ); | |
| } | |
| } | |
| /** | |
| * Create ul error container | |
| * | |
| * @method manageErrorContainer | |
| */ | |
| , manageErrorContainer: function () { | |
| var errorContainer = this.options.errorContainer || this.options.errors.container( this.element, this.isRadioOrCheckbox ) | |
| , ulTemplate = this.options.animate ? this.ulTemplate.show() : this.ulTemplate; | |
| if ( 'undefined' !== typeof errorContainer ) { | |
| $( errorContainer ).append( ulTemplate ); | |
| return; | |
| } | |
| !this.isRadioOrCheckbox ? this.$element.after( ulTemplate ) : this.$element.parent().after( ulTemplate ); | |
| } | |
| /** | |
| * Add custom listeners | |
| * | |
| * @param {Object} { listener: function () {} }, eg { onFormSubmit: function ( valid, event, focus ) { ... } } | |
| */ | |
| , addListener: function ( object ) { | |
| for ( var listener in object ) { | |
| this.options.listeners[ listener ] = object[ listener ]; | |
| } | |
| } | |
| /** | |
| * Destroy parsley field instance | |
| * | |
| * @private | |
| * @method destroy | |
| */ | |
| , destroy: function () { | |
| this.$element.removeClass( 'parsley-validated' ); | |
| this.reset().$element.off( '.' + this.type ).removeData( this.type ); | |
| } | |
| }; | |
| /** | |
| * ParsleyFieldMultiple override ParsleyField for checkbox and radio inputs | |
| * Pseudo-heritance to manage divergent behavior from ParsleyItem in dedicated methods | |
| * | |
| * @class ParsleyFieldMultiple | |
| * @constructor | |
| */ | |
| var ParsleyFieldMultiple = function ( element, options, type ) { | |
| this.initMultiple( element, options ); | |
| this.inherit( element, options ); | |
| this.Validator = new Validator( options ); | |
| // call ParsleyField constructor | |
| this.init( element, type || 'ParsleyFieldMultiple' ); | |
| }; | |
| ParsleyFieldMultiple.prototype = { | |
| constructor: ParsleyFieldMultiple | |
| /** | |
| * Set some specific properties, call some extra methods to manage radio / checkbox | |
| * | |
| * @method init | |
| * @param {Object} element | |
| * @param {Object} options | |
| */ | |
| , initMultiple: function ( element, options ) { | |
| this.element = element; | |
| this.$element = $( element ); | |
| this.group = options.group || false; | |
| this.hash = this.getName(); | |
| this.siblings = this.group ? '[data-group="' + this.group + '"]' : 'input[name="' + this.$element.attr( 'name' ) + '"]'; | |
| this.isRadioOrCheckbox = true; | |
| this.isRadio = this.$element.is( 'input[type=radio]' ); | |
| this.isCheckbox = this.$element.is( 'input[type=checkbox]' ); | |
| this.errorClassHandler = options.errors.classHandler( element, this.isRadioOrCheckbox ) || this.$element.parent(); | |
| } | |
| /** | |
| * Set specific constraints messages, do pseudo-heritance | |
| * | |
| * @private | |
| * @method inherit | |
| * @param {Object} element | |
| * @param {Object} options | |
| */ | |
| , inherit: function ( element, options ) { | |
| var clone = new ParsleyField( element, options, 'ParsleyFieldMultiple' ); | |
| for ( var property in clone ) { | |
| if ( 'undefined' === typeof this[ property ] ) { | |
| this[ property ] = clone [ property ]; | |
| } | |
| } | |
| } | |
| /** | |
| * Set specific constraints messages, do pseudo-heritance | |
| * | |
| * @method getName | |
| * @returns {String} radio / checkbox hash is cleaned 'name' or data-group property | |
| */ | |
| , getName: function () { | |
| if ( this.group ) { | |
| return 'parsley-' + this.group; | |
| } | |
| if ( 'undefined' === typeof this.$element.attr( 'name' ) ) { | |
| throw "A radio / checkbox input must have a data-group attribute or a name to be Parsley validated !"; | |
| } | |
| return 'parsley-' + this.$element.attr( 'name' ).replace( /(:|\.|\[|\])/g, '' ); | |
| } | |
| /** | |
| * Special treatment for radio & checkboxes | |
| * Returns checked radio or checkboxes values | |
| * | |
| * @method getVal | |
| * @returns {String} val | |
| */ | |
| , getVal: function () { | |
| if ( this.isRadio ) { | |
| return $( this.siblings + ':checked' ).val() || ''; | |
| } | |
| if ( this.isCheckbox ) { | |
| var values = []; | |
| $( this.siblings + ':checked' ).each( function () { | |
| values.push( $( this ).val() ); | |
| } ); | |
| return values; | |
| } | |
| } | |
| /** | |
| * Bind validation events on a field | |
| * | |
| * @private | |
| * @method bindValidationEvents | |
| */ | |
| , bindValidationEvents: function () { | |
| // this field has validation events, that means it has to be validated | |
| this.valid = null; | |
| this.$element.addClass( 'parsley-validated' ); | |
| // remove eventually already binded events | |
| this.$element.off( '.' + this.type ); | |
| // alaways bind keyup event, for better UX when a field is invalid | |
| var self = this | |
| , triggers = ( !this.options.trigger ? '' : this.options.trigger ) | |
| + ( new RegExp( 'change', 'i' ).test( this.options.trigger ) ? '' : ' change' ); | |
| // trim triggers to bind them correctly with .on() | |
| triggers = triggers.replace( /^\s+/g , '' ).replace( /\s+$/g ,'' ); | |
| // bind trigger event on every siblings | |
| $( this.siblings ).each(function () { | |
| $( this ).on( triggers.split( ' ' ).join( '.' + self.type + ' ' ) , false, $.proxy( self.eventValidation, self ) ); | |
| } ) | |
| } | |
| }; | |
| /** | |
| * ParsleyForm class manage Parsley validated form. | |
| * Manage its fields and global validation | |
| * | |
| * @class ParsleyForm | |
| * @constructor | |
| */ | |
| var ParsleyForm = function ( element, options, type ) { | |
| this.init( element, options, type || 'parsleyForm' ); | |
| }; | |
| ParsleyForm.prototype = { | |
| constructor: ParsleyForm | |
| /* init data, bind jQuery on() actions */ | |
| , init: function ( element, options, type ) { | |
| this.type = type; | |
| this.items = []; | |
| this.$element = $( element ); | |
| this.options = options; | |
| var self = this; | |
| this.$element.find( options.inputs ).each( function () { | |
| self.addItem( this ); | |
| }); | |
| this.$element.on( 'submit.' + this.type , false, $.proxy( this.validate, this ) ); | |
| } | |
| /** | |
| * Add custom listeners | |
| * | |
| * @param {Object} { listener: function () {} }, eg { onFormSubmit: function ( valid, event, focus ) { ... } } | |
| */ | |
| , addListener: function ( object ) { | |
| for ( var listener in object ) { | |
| if ( new RegExp( 'Field' ).test( listener ) ) { | |
| for ( var item = 0; item < this.items.length; item++ ) { | |
| this.items[ item ].addListener( object ); | |
| } | |
| } else { | |
| this.options.listeners[ listener ] = object[ listener ]; | |
| } | |
| } | |
| } | |
| /** | |
| * Adds a new parsleyItem child to ParsleyForm | |
| * | |
| * @method addItem | |
| * @param elem | |
| */ | |
| , addItem: function ( elem ) { | |
| if ( $( elem ).is( this.options.excluded ) ) { | |
| return false; | |
| } | |
| var ParsleyField = $( elem ).parsley( this.options ); | |
| ParsleyField.setParent( this ); | |
| this.items.push( ParsleyField ); | |
| } | |
| /** | |
| * Removes a parsleyItem child from ParsleyForm | |
| * | |
| * @method removeItem | |
| * @param elem | |
| * @return {Boolean} | |
| */ | |
| , removeItem: function ( elem ) { | |
| var parsleyItem = $( elem ).parsley(); | |
| // identify & remove item if same Parsley hash | |
| for ( var i = 0; i < this.items.length; i++ ) { | |
| if ( this.items[ i ].hash === parsleyItem.hash ) { | |
| this.items[ i ].destroy(); | |
| this.items.splice( i, 1 ); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Process each form field validation | |
| * Display errors, call custom onFormSubmit() function | |
| * | |
| * @method validate | |
| * @param {Object} event jQuery Event | |
| * @return {Boolean} Is form valid or not | |
| */ | |
| , validate: function ( event ) { | |
| var valid = true; | |
| this.focusedField = false; | |
| for ( var item = 0; item < this.items.length; item++ ) { | |
| if ( 'undefined' !== typeof this.items[ item ] && false === this.items[ item ].validate() ) { | |
| valid = false; | |
| if ( !this.focusedField && 'first' === this.options.focus || 'last' === this.options.focus ) { | |
| this.focusedField = this.items[ item ].$element; | |
| } | |
| } | |
| } | |
| // form is invalid, focus an error field depending on focus policy | |
| if ( this.focusedField && !valid ) { | |
| this.focusedField.focus(); | |
| } | |
| this.options.listeners.onFormSubmit( valid, event, this ); | |
| return valid; | |
| } | |
| , isValid: function () { | |
| for ( var item = 0; item < this.items.length; item++ ) { | |
| if ( false === this.items[ item ].isValid() ) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * Remove all errors ul under invalid fields | |
| * | |
| * @method removeErrors | |
| */ | |
| , removeErrors: function () { | |
| for ( var item = 0; item < this.items.length; item++ ) { | |
| this.items[ item ].parsley( 'reset' ); | |
| } | |
| } | |
| /** | |
| * destroy Parsley binded on the form and its fields | |
| * | |
| * @method destroy | |
| */ | |
| , destroy: function () { | |
| for ( var item = 0; item < this.items.length; item++ ) { | |
| this.items[ item ].destroy(); | |
| } | |
| this.$element.off( '.' + this.type ).removeData( this.type ); | |
| } | |
| /** | |
| * reset Parsley binded on the form and its fields | |
| * | |
| * @method reset | |
| */ | |
| , reset: function () { | |
| for ( var item = 0; item < this.items.length; item++ ) { | |
| this.items[ item ].reset(); | |
| } | |
| } | |
| }; | |
| /** | |
| * Parsley plugin definition | |
| * Provides an interface to access public Validator, ParsleyForm and ParsleyField functions | |
| * | |
| * @class Parsley | |
| * @constructor | |
| * @param {Mixed} Options. {Object} to configure Parsley or {String} method name to call a public class method | |
| * @param {Function} Callback function | |
| * @return {Mixed} public class method return | |
| */ | |
| $.fn.parsley = function ( option, fn ) { | |
| var options = $.extend( true, {}, $.fn.parsley.defaults, 'undefined' !== typeof window.ParsleyConfig ? window.ParsleyConfig : {}, option, this.data() ) | |
| , newInstance = null; | |
| function bind ( self, type ) { | |
| var parsleyInstance = $( self ).data( type ); | |
| // if data never binded or we want to clone a build (for radio & checkboxes), bind it right now! | |
| if ( !parsleyInstance ) { | |
| switch ( type ) { | |
| case 'parsleyForm': | |
| parsleyInstance = new ParsleyForm( self, options, 'parsleyForm' ); | |
| break; | |
| case 'parsleyField': | |
| parsleyInstance = new ParsleyField( self, options, 'parsleyField' ); | |
| break; | |
| case 'parsleyFieldMultiple': | |
| parsleyInstance = new ParsleyFieldMultiple( self, options, 'parsleyFieldMultiple' ); | |
| break; | |
| default: | |
| return; | |
| } | |
| $( self ).data( type, parsleyInstance ); | |
| } | |
| // here is our parsley public function accessor | |
| if ( 'string' === typeof option && 'function' === typeof parsleyInstance[ option ] ) { | |
| var response = parsleyInstance[ option ]( fn ); | |
| return 'undefined' !== typeof response ? response : $( self ); | |
| } | |
| return parsleyInstance; | |
| } | |
| // if a form elem is given, bind all its input children | |
| if ( $( this ).is( 'form' ) ) { | |
| newInstance = bind ( $( this ), 'parsleyForm' ); | |
| // if it is a Parsley supported single element, bind it too, except inputs type hidden | |
| // add here a return instance, cuz' we could call public methods on single elems with data[ option ]() above | |
| } else if ( $( this ).is( options.inputs ) && !$( this ).is( options.excluded ) ) { | |
| newInstance = bind( $( this ), !$( this ).is( 'input[type=radio], input[type=checkbox]' ) ? 'parsleyField' : 'parsleyFieldMultiple' ); | |
| } | |
| return 'function' === typeof fn ? fn() : newInstance; | |
| }; | |
| $.fn.parsley.Constructor = ParsleyForm; | |
| /** | |
| * Parsley plugin configuration | |
| * | |
| * @property $.fn.parsley.defaults | |
| * @type {Object} | |
| */ | |
| $.fn.parsley.defaults = { | |
| // basic data-api overridable properties here.. | |
| inputs: 'input, textarea, select' // Default supported inputs. | |
| , excluded: 'input[type=hidden], :disabled' // Do not validate input[type=hidden] & :disabled. | |
| , trigger: false // $.Event() that will trigger validation. eg: keyup, change.. | |
| , animate: true // fade in / fade out error messages | |
| , animateDuration: 300 // fadein/fadout ms time | |
| , focus: 'first' // 'fist'|'last'|'none' which error field would have focus first on form validation | |
| , validationMinlength: 3 // If trigger validation specified, only if value.length > validationMinlength | |
| , successClass: 'parsley-success' // Class name on each valid input | |
| , errorClass: 'parsley-error' // Class name on each invalid input | |
| , errorMessage: false // Customize an unique error message showed if one constraint fails | |
| , validators: {} // Add your custom validators functions | |
| , showErrors: true // Set to false if you don't want Parsley to display error messages | |
| , messages: {} // Add your own error messages here | |
| //some quite advanced configuration here.. | |
| , validateIfUnchanged: false // false: validate once by field value change | |
| , errors: { | |
| classHandler: function ( elem, isRadioOrCheckbox ) {} // specify where parsley error-success classes are set | |
| , container: function ( elem, isRadioOrCheckbox ) {} // specify an elem where errors will be **apened** | |
| , errorsWrapper: '<ul></ul>' // do not set an id for this elem, it would have an auto-generated id | |
| , errorElem: '<li></li>' // each field constraint fail in an li | |
| } | |
| , listeners: { | |
| onFieldValidate: function ( elem, ParsleyForm ) { return false; } // Executed on validation. Return true to ignore field validation | |
| , onFormSubmit: function ( isFormValid, event, ParsleyForm ) {} // Executed once on form validation | |
| , onFieldError: function ( elem, constraints, ParsleyField ) {} // Executed when a field is detected as invalid | |
| , onFieldSuccess: function ( elem, constraints, ParsleyField ) {} // Executed when a field passes validation | |
| } | |
| }; | |
| /* PARSLEY auto-bind DATA-API + Global config retrieving | |
| * =================================================== */ | |
| $( window ).on( 'load', function () { | |
| $( '[data-validate="parsley"]' ).each( function () { | |
| $( this ).parsley(); | |
| } ); | |
| } ); | |
| // This plugin works with jQuery or Zepto (with data extension built for Zepto.) | |
| }(window.jQuery || window.Zepto); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment