﻿/*!
 * jQuery bValidator plugin
 *
 * http://code.google.com/p/bvalidator/
 *
 * Copyright (c) 2011 Bojan Mauser
 *
 * $Id: jquery.bvalidator.js 55 2011-02-02 01:29:04Z bmauser $
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 */

(function($){

	// constructor
	$.fn.bValidator = function(overrideOptions){
		return new bValidator(this, overrideOptions);
	};

	// bValidator class
	bValidator = function(mainElement, overrideOptions){

		// default options
		var options = {

			singleError:         false,		// validate all inputs at once
			offset:              {x:-25, y:-3},	// offset position for error message tooltip
			position:            {x:'right', y:'top'}, // error message placement x:left|center|right  y:top|center|bottom
			template:            '<div class="{errMsgClass}"><em/>{message}</div>', // template for error message
			templateCloseIcon:   '<div style="display:table"><div style="display:table-cell">{message}</div><div style="display:table-cell"><div class="{closeIconClass}" onclick="{closeErrMsg}">x</div></div></div>', // template for error message container when showCloseIcon option is true
			showCloseIcon:       true,	// put close icon on error message
			showErrMsgSpeed:    'normal',	// message's fade-in speed 'fast', 'normal', 'slow' or number of milliseconds
			scrollToError:       true,	// scroll to first error
			// css class names
			closeIconClass:      'bvalidator_close_icon',	// close error message icon class
			errMsgClass:         'bvalidator_errmsg',	// error message class
			errorClass:          'bvalidator_invalid',	// input field class name in case of validation error
			validClass:          '',			// input field class name in case of valid value

			lang: 'en', 				// default language for error messages 
			errorMessageAttr:    'data-bvalidator-msg',// name of the attribute for overridden error message
			validateActionsAttr: 'data-bvalidator', // name of the attribute which stores info what validation actions to do
			paramsDelimiter:     ':',		// delimiter for validator action options inside []
			validatorsDelimiter: ',',		// delimiter for validator actions

			// when to validate
			validateOn:          null,		// null, 'change', 'blur', 'keyup'
			errorValidateOn:     'keyup',		// null, 'change', 'blur', 'keyup'

			// callback functions
			onBeforeValidate:    null,
			onAfterValidate:     null,
			onValidateFail:      null,
			onValidateSuccess:   null,

			// default error messages
			errorMessages: {
				en: {
					'default':    'Please correct this value.',
					'equalto':    'Please enter the same value again.',
					'differs':    'Please enter a different value.',
					'minlength':  'The length must be at least {0} characters',
					'maxlength':  'The length must be at max {0} characters',
					'rangelength':'The length must be between {0} and {1}',
					'min':        'Please enter a number greater than or equal to {0}.',
					'max':        'Please enter a number less than or equal to {0}.',
					'between':    'Please enter a number between {0} and {1}.',
					'required':   'This field is required.',
					'alpha':      'Please enter alphabetic characters only.',
					'alphanum':   'Please enter alphanumeric characters only.',
					'digit':      'Please enter only digits.',
					'number':     'Please enter a valid number.',
					'email':      'Please enter a valid email address.',
					'image':      'This field should only contain image types',
					'url':        'Please enter a valid URL.',
					'ip4':        'Please enter a valid IP address.',
					'date':       'Please enter a valid date in format {0}.'
				}
			},

			// regular expressions used by validator methods
			regex: {
				alpha:    /^[a-z ._\-]+$/i,
				alphanum: /^[a-z0-9 ._\-]+$/i,
				digit:    /^\d+$/,
				number:   /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
				email:    /^([a-zA-Z0-9_\.\-\+%])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,
				image:    /\.(jpg|jpeg|png|gif|bmp)$/i,
				url:      /^(http|https|ftp)\:\/\/[a-z0-9\-\.]+\.[a-z]{2,3}(:[a-z0-9]*)?\/?([a-z0-9\-\._\?\,\'\/\\\+&amp;%\$#\=~])*$/i
			}
		},

		// returns all inputs
		_getElementsForValidation = function(element){
			// skip hidden and input fields witch we do not want to validate
			return element.is(':input') ? element : element.find(':input[' + options.validateActionsAttr + ']').not(":button, :image, :reset, :submit, :hidden, :disabled");
		},

		// binds validateOn event
		_bindValidateOn = function(elements){
			elements.bind(options.validateOn + '.bV', {'bVInstance': instance}, function(event){
				event.data.bVInstance.validate(false, $(this));
			});
		},

		// displays error message
		_showErrMsg = function(element, messages){

			// if error message already exists remove it from DOM
			_removeErrMsg(element);

			msg_container = $('<div class="bVErrMsgContainer"></div>').css('position','absolute');
			element.data("errMsg.bV", msg_container);
			msg_container.insertAfter(element);

			var messagesHtml = '';

			for(var i in messages)
				messagesHtml += '<div>' + messages[i] + '</div>\n';

			if(options.showCloseIcon)
				messagesHtml = options.templateCloseIcon.replace('{message}', messagesHtml).replace('{closeIconClass}', options.closeIconClass).replace('{closeErrMsg}', '$(this).closest(\'.'+ options.errMsgClass +'\').css(\'visibility\', \'hidden\');');

			// make tooltip from template
			var tooltip = $(options.template.replace('{errMsgClass}', options.errMsgClass).replace('{message}', messagesHtml));
			tooltip.appendTo(msg_container);

			var pos = _getErrMsgPosition(element, tooltip); 

			tooltip.css({visibility: 'visible', position: 'absolute', top: pos.top, left: pos.left}).fadeIn(options.showErrMsgSpeed);

			if(options.scrollToError){
				// get most top tolltip
				var tot = tooltip.offset().top;
				if(scroll_to === null || tot < scroll_to)
					scroll_to = tot;
			}
		},

		// removes error message from DOM
		_removeErrMsg = function(element){
			var existingMsg = element.data("errMsg.bV")
			if(existingMsg){
				existingMsg.remove();
			}
		},

		// calculates error message position
		_getErrMsgPosition = function(input, tooltip){

		        var tooltipContainer = input.data("errMsg.bV"),
		         top  = - ((tooltipContainer.offset().top - input.offset().top) + tooltip.outerHeight() - options.offset.y),
		         left = (input.offset().left + input.outerWidth()) - tooltipContainer.offset().left + options.offset.x,
			 x = options.position.x,
			 y = options.position.y;

			// adjust Y
			if(y == 'center' || y == 'bottom'){
				var height = tooltip.outerHeight() + input.outerHeight();
				if (y == 'center') 	{top += height / 2;}
				if (y == 'bottom') 	{top += height;}
			}

			// adjust X
			if(x == 'center' || x == 'left'){
				var width = input.outerWidth();
				if (x == 'center') 	{left -= width / 2;}
				if (x == 'left')  	{left -= width;}
			}

			return {top: top, left: left};
		},

		// calls callback functions
		_callBack = function(type, param1, param2, param3){
		        if($.isFunction(options[type])){
		        	return options[type](param1, param2, param3);
			}
		},
		
		// gets element value	
		_getValue = function(element){

			var ret = {};

			// checkbox
			if(element.is('input:checkbox')){
				ret['value'] = element.attr('name') ? ret['selectedInGroup'] = $('input:checkbox[name="' + element.attr('name') + '"]:checked').length : element.attr('checked');
			}
			else if(element.is('input:radio')){
				ret['value'] = element.attr('name') ? ret['value'] = $('input:radio[name="' + element.attr('name') + '"]:checked').length : element.val();
			}
			else if(element.is('select')){
				ret['selectedInGroup'] =  $("option:selected", element).length;
				ret['value'] = element.val();
			}
			else if(element.is(':input')){
				ret['value'] = element.val();
			}

			return ret;
		},

		// object with validator functions
		validator = {

			equalto: function(v, elementId){
				return v.value == $('#' + elementId).val();
			},

			differs: function(v, elementId){
				return v.value != $('#' + elementId).val();
			},

			minlength: function(v, minlength){
				return (v.value.length >= parseInt(minlength))
			},

			maxlength: function(v, maxlength){
				return (v.value.length <= parseInt(maxlength))
			},

			rangelength: function(v, minlength, maxlength){		
				return (v.value.length >= parseInt(minlength) && v.value.length <= parseInt(maxlength))
			},

			min: function(v, min){		
				if(v.selectedInGroup)
					return v.selectedInGroup >= parseFloat(min)
				else{
					if(!this.number(v))
			 			return false;
			 		return (parseFloat(v.value) >= parseFloat(min))
				}
			},

			max: function(v, max){		
				if(v.selectedInGroup)
					return v.selectedInGroup <= parseFloat(max)
				else{
					if(!this.number(v))
			 			return false;
			 		return (parseFloat(v.value) <= parseFloat(max))
				}
			},

			between: function(v, min, max){
				if(v.selectedInGroup)
					return (v.selectedInGroup >= parseFloat(min) && v.selectedInGroup <= parseFloat(max))
			   	if(!this.number(v))
			 		return false;
				var va = parseFloat(v.value);
				return (va >= parseFloat(min) && va <= parseFloat(max))
			},

			required: function(v){
				if(!v.value || !$.trim(v.value))
					return false
				return true
			},

			alpha: function(v){
				return this.regex(v, options.regex.alpha);
			},

			alphanum: function(v){
				return this.regex(v, options.regex.alphanum);
			},

			digit: function(v){
				return this.regex(v, options.regex.digit);
			},

			number: function(v){
				return this.regex(v, options.regex.number);
			},

			email: function(v){
				return this.regex(v, options.regex.email);
			},

			image: function(v){
				return this.regex(v, options.regex.image);
			},

			url: function(v){
				return this.regex(v, options.regex.url);
			},

			regex: function(v, regex, mod){
				if(typeof regex === "string")
					regex = new RegExp(regex, mod);
				return regex.test(v.value);
			},

			ip4: function(v){
				var r = /^(([01]?\d\d?|2[0-4]\d|25[0-5])\.){3}([01]?\d\d?|25[0-5]|2[0-4]\d)$/;
				if (!r.test(v.value) || v.value == "0.0.0.0" || v.value == "255.255.255.255")
					return false
				return true;
			},

			date: function(v, format){ // format can be any combination of mm,dd,yyyy with separator between. Example: 'mm.dd.yyyy' or 'yyyy-mm-dd'
				if(v.value.length == 10 && format.length == 10){
					var s = format.match(/[^mdy]+/g);
					if(s.length == 2 && s[0].length == 1 && s[0] == s[1]){

						var d = v.value.split(s[0]),
						 f = format.split(s[0]);

						for(var i=0; i<3; i++){
							if(f[i] == 'dd') var day = d[i];
							else if(f[i] == 'mm') var month = d[i];
							else if(f[i] == 'yyyy') var year = d[i];
						}

						var dobj = new Date(year, month-1, day)
						if ((dobj.getMonth()+1!=month) || (dobj.getDate()!=day) || (dobj.getFullYear()!=year))
							return false

						return true
					}
				}
				return false;
			},

			extension: function(){
				var v = arguments[0],
				 r = '';
				if(!arguments[1])
					return false
				for(var i=1; i<arguments.length; i++){
					r += arguments[i];
					if(i != arguments.length-1)
						r += '|';
				}
				return this.regex(v, '\\.(' +  r  + ')$', 'i');
			}
		},

		// validator instance, scroll position flag
		instance = this, scroll_to;

		// global options
		if(window['bValidatorOptions']){
			$.extend(true, options, window['bValidatorOptions']);
		}

		// passed options
		if(overrideOptions)
			$.extend(true, options, overrideOptions);

		// return existing instance
		if(mainElement.data("bValidator"))
			return mainElement.data("bValidator");

		mainElement.data("bValidator", this);

		// if selector is a form
		if(mainElement.is('form')){
			// bind validation on form submit
			mainElement.bind('submit.bV', function(event){
				if(instance.validate())
					return true;
				else{
					event.stopImmediatePropagation();
					return false;
				}
			});

			// bind reset on form reset
			mainElement.bind("reset.bV", function(){
				instance.reset();			
			});
		}

		// bind validateOn event
		if(options.validateOn)
			_bindValidateOn(_getElementsForValidation(mainElement));


		// API functions:

		// validation function
		this.validate = function(doNotshowMessages, elementsOverride){

			// return value, elements to validate
			var ret = true, 
			 elementsl = elementsOverride ? elementsOverride : _getElementsForValidation(mainElement);

			scroll_to = null;

			// validate each element
			elementsl.each(function(){

				// value of validateActionsAttr input attribute
				var actionsStr = $.trim($(this).attr(options.validateActionsAttr).replace(new RegExp('\\s*\\' + options.validatorsDelimiter + '\\s*', 'g'), options.validatorsDelimiter)),
				 is_valid = 0;

				if(!actionsStr)
					return true;

				var actions = actionsStr.split(options.validatorsDelimiter), // all validation actions
				 inputValue = _getValue($(this)), // value of input field for validation
				 errorMessages = [];

				// if value is not required and is empty
				if(jQuery.inArray('required',actions) == -1 && !validator.required(inputValue)){
					is_valid = 1;
				}

				if(!is_valid){

					// get error message from attribute
					var errMsg = $(this).attr(options.errorMessageAttr),
					 skip_messages = 0;

					// for each validation action
					for(var i in actions){

						actions[i] = $.trim(actions[i]);

						if(!actions[i])
							continue;

						if(_callBack('onBeforeValidate', $(this), actions[i]) === false)
							continue;

						// check if we have some parameters for validator
						var validatorParams = actions[i].match(/^(.*?)\[(.*?)\]/);

						if(validatorParams && validatorParams.length == 3){
							var validatorName = validatorParams[1];
							validatorParams = validatorParams[2].split(options.paramsDelimiter);
						}
						else{
							validatorParams = [];
							var validatorName = actions[i];
						}

						// if validator exists
						if(typeof validator[validatorName] == 'function'){
							validatorParams.unshift(inputValue); // add input value to beginning of validatorParams
							var validationResult = validator[validatorName].apply(validator, validatorParams); // call validator function
						}
						// call custom user dafined function
						else if(typeof window[validatorName] == 'function'){
							validatorParams.unshift(inputValue.value);
							var validationResult = window[validatorName].apply(validator, validatorParams);
						}

						if(_callBack('onAfterValidate', $(this), actions[i], validationResult) === false)
							continue;

						// if validation failed
						if(!validationResult){
							if(!doNotshowMessages){

								if(!skip_messages){
									if(!errMsg){

										if(options.errorMessages[options.lang] && options.errorMessages[options.lang][validatorName])
											errMsg = options.errorMessages[options.lang][validatorName];
										else if(options.errorMessages.en[validatorName])
											errMsg = options.errorMessages.en[validatorName];
										else if(options.errorMessages[options.lang] && options.errorMessages[options.lang]['default'])
											errMsg = options.errorMessages[options.lang]['default'];
										else
											errMsg = options.errorMessages.en['default'];
									}
									else{
										skip_messages = 1;
									}

									// replace values in braces
									if(errMsg.indexOf('{')){
										for(var j=0; j<validatorParams.length-1; j++)
											errMsg = errMsg.replace(new RegExp("\\{" + j + "\\}", "g"), validatorParams[j+1]);
									}

									if(!(errorMessages.length && validatorName == 'required'))
										errorMessages[errorMessages.length] = errMsg;

									errMsg = null;
								}
							}
							else
								errorMessages[errorMessages.length] = '';

							ret = false;

							if(_callBack('onValidateFail', $(this), actions[i], errorMessages) === false)
								continue;
						}
						else{
							if(_callBack('onValidateSuccess', $(this), actions[i]) === false)
								continue;
						}
					}
				}

				if(!doNotshowMessages){

					var chk_rad = $(this).is('input:checkbox,input:radio') ? 1 : 0;

					// if validation failed
					if(errorMessages.length){

						_showErrMsg($(this), errorMessages)

						if(!chk_rad){
							$(this).removeClass(options.validClass);
							if(options.errorClass)
								$(this).addClass(options.errorClass);
						}
		
						// input validation event
						if (options.errorValidateOn){
							if(options.validateOn)
								$(this).unbind(options.validateOn + '.bV');

							var evt = chk_rad || $(this).is('select,input:file') ? 'change' : options.errorValidateOn;

							if(chk_rad){
								var group = $(this).is('input:checkbox') ? $('input:checkbox[name="' + $(this).attr('name') + '"]') : $('input:radio[name="' + $(this).attr('name') + '"]');
								$(group).unbind('.bVerror');
								$(group).bind('change.bVerror', {'bVInstance': instance, 'groupLeader': $(this)}, function(event){
									event.data.bVInstance.validate(false, event.data.groupLeader);
								});
							}
							else{
								$(this).unbind('.bVerror');
								$(this).bind(evt + '.bVerror', {'bVInstance': instance}, function(event){
									event.data.bVInstance.validate(false, $(this));
								});
							}
						}

						if (options.singleError)
							return false;
					}
					else{
						_removeErrMsg($(this));

						if(!chk_rad){
							$(this).removeClass(options.errorClass);
							if(options.validClass)
								$(this).addClass(options.validClass);
						}

						//if (options.errorValidateOn)
						//	$(this).unbind('.bVerror');
						if (options.validateOn){
							$(this).unbind(options.validateOn + '.bV');
							_bindValidateOn($(this));
						}
					}
				}
			});

			// scroll to error
			if(scroll_to && !elementsOverride && ($(window).scrollTop() > scroll_to || $(window).scrollTop()+$(window).height() < scroll_to)){
				var ua = navigator.userAgent.toLowerCase();			
				$(ua.indexOf('chrome')>-1 || ua.indexOf('safari')>-1 ? 'body' : 'html').animate({scrollTop: scroll_to - 10}, {duration: 'slow'});
			}

			return ret;
		}

		// returns options object
		this.getOptions = function(){
			return options;
		}

		// chechs validity
		this.isValid = function(){
			return this.validate(true);
		}

		// deletes error message
		this.removeErrMsg = function(element){
			_removeErrMsg(element);
		}

		// returns all inputs
		this.getInputs = function(){
			return _getElementsForValidation(mainElement);
		}

		// binds validateOn event
		this.bindValidateOn = function(element){
			_bindValidateOn(element);
		}

		// resets validation
		this.reset = function(){
			elements = _getElementsForValidation(mainElement);
			if (options.validateOn)
				_bindValidateOn(elements);
			elements.each(function(){
				_removeErrMsg($(this));
				$(this).unbind('.bVerror');
				$(this).removeClass(options.errorClass);
				$(this).removeClass(options.validClass);
			});
		}

		this.destroy = function(){
			if (mainElement.is('form'))
				mainElement.unbind('.bV');
			
			this.reset();
			
			mainElement.removeData("bValidator");
		}
	}

})(jQuery);

