﻿/*
 * jQuery bValidator plugin
 *
 * http://code.google.com/p/bvalidator/
 *
 * Copyright (c) 2011 Bojan Mauser
 *
 * $Id: jquery.bvalidator.js 44 2011-01-16 20:13:25Z 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
			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
			}
		};
		
		// validator instance
		var 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();			
			});
		}
		
		// returns all inputs
		var _getElementsForValidation = function(element){
		
			if(element.is(':input'))
				var elements = element;
			else{
				//skip hidden and input fields witch we do not want to validate
				var elements = element.find(':input').not(":button, :image, :reset, :submit, :hidden, :disabled");
			}
			
			return elements;
		}
		
		// binds validateOn event
		var _bindValidateOn = function(elements){
			elements.bind(options.validateOn + '.bV', {'bValidatorInstance': instance}, function(event) {
				event.data.bValidatorInstance.validate(false, $(this));
			});
		}
		
		// displays error message
		var _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){
				var closeiconTpl = '<div style="display:table"><div style="display:table-cell">{message}</div><div style="display:table-cell"><div class="'+options.closeIconClass+'" onclick="$(this).closest(\'.'+ options.errMsgClass +'\').css(\'visibility\', \'hidden\');">x</div></div></div>';
				messagesHtml = closeiconTpl.replace('{message}', messagesHtml);
			}
			
			// 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
		var _removeErrMsg = function(element){
			var existingMsg = element.data("errMsg.bV")
			if(existingMsg){
				existingMsg.remove();
			}
		}
		
		// calculates error message position
		var _getErrMsgPosition = function(input, tooltip) {
		        
		        var tooltipContainer = input.data("errMsg.bV");
		        var top  = - ((tooltipContainer.offset().top - input.offset().top) + tooltip.outerHeight() - options.offset.y);
		        var left = (input.offset().left + input.outerWidth()) - tooltipContainer.offset().left + options.offset.x;
			
			var x = options.position.x;
			var 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
		var _callBack = function(type, param1, param2, param3) {
		        if($.isFunction(options[type])){
		        	return options[type](param1, param2, param3);
		        }
		}
		
		// gets element value	
		var _getValue = function(element) {
			
			var ret = {};
	
			// checkbox
			if(element.is('input:checkbox')){
				if(element.attr('name'))
					ret['selectedInGroup'] = $('input:checkbox[name=' + element.attr('name') + ']:checked').length;
				ret['value'] = element.attr('checked');
			}
			else if(element.is('input:radio')){
				if(element.attr('name'))
					ret['value'] = $('input:radio[name=' + element.attr('name') + ']:checked').length;
				else
					ret['value'] = 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
		var 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 >= minlength)
			},
			
			maxlength: function(v, maxlength){
				return (v.value.length <= maxlength)
			},
			
			rangelength: function(v, minlength, maxlength){		
				return (v.value.length >= minlength && v.value.length <= maxlength)
			},
			
			min: function(v, min){		
				if(v.selectedInGroup)
					return v.selectedInGroup >= min
				else{
					if(!this.number(v))
			 			return false;
			 		return (parseFloat(v.value) >= parseFloat(min))
				}
			},
			
			max: function(v, max){		
				if(v.selectedInGroup)
					return v.selectedInGroup <= max
				else{
					if(!this.number(v))
			 			return false;
			 		return (parseFloat(v.value) <= parseFloat(min))
				}
			},
			
			between: function(v, min, 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]);
						var 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];
				var 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');
			}
		};
		
		// bind validateOn event
		if(options.validateOn)
			_bindValidateOn(_getElementsForValidation(mainElement));
		
		
		// API functions:
		
		
		// validation function
		this.validate = function(doNotshowMessages, elementsOverride) {
			
			if(elementsOverride)
				var elementsl = elementsOverride;
			else
				var elementsl = _getElementsForValidation(mainElement);
			
			// return value
			var ret = true;
			
			scroll_to = null;
			
			// validate each element
			elementsl.each(function() {
				
				// value of validateActionsAttr input attribute
				var actionsStr = $.trim($(this).attr(options.validateActionsAttr));
				var is_valid = 0;
				
				if(!actionsStr)
					return true;
				
				// get all validation actions
				var actions = actionsStr.split(options.validatorsDelimiter);
				
				// value of input field for validation
				var inputValue = _getValue($(this));
				
				// if value is not required and is empty
				if(jQuery.inArray('required',actions) == -1 && !validator.required(inputValue)){
					is_valid = 1;
				}
				
				var errorMessages = [];
				
				if(!is_valid){
					
					// get error message from attribute
					var errMsg = $(this).attr(options.errorMessageAttr);
					var 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 = $.trim(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 i=0; i<validatorParams.length-1; i++)
											errMsg = errMsg.replace(new RegExp("\\{" + i + "\\}", "g"), validatorParams[i+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){
					// if validation failed
					if(errorMessages.length){
						
						_showErrMsg($(this), errorMessages)
						
						if(!$(this).is('input:checkbox,input:radio')){
							$(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 = options.errorValidateOn;
							if($(this).is('input:checkbox,input:radio,select,input:file'))
								evt = 'change';
							
							$(this).unbind(evt + '.bVerror');
							$(this).bind(evt + '.bVerror', {'bValidatorInstance': instance}, function(event) {
								event.data.bValidatorInstance.validate(false, $(this));
							});
						}
						
						if (options.singleError)
							return false;
					}
					else{
						_removeErrMsg($(this));
						
						if(!$(this).is('input:checkbox,input:radio')){
							$(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'});
			}

			/* added JR */
			if(scroll_to && !elementsOverride && typeof $.postMessage != 'undefined'){
				$.postMessage({method: 'scroll', top: scroll_to}, decodeURIComponent(document.location.hash.replace( /^#/, '' )), parent);
				//console.log(decodeURIComponent(document.location.hash.replace( /^#/, '' )));
			}
			
			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);

