/*
	Copyright (c) 2007-2010 JB Interactive Pty. Ltd.
	All Rights Reserved
	http://www.jbinteractive.com.au/
*/

/*
JQUERY ERRORS PLUGIN
Requires jQuery 1.4

--------------------------- USAGE ---------------------------
$(form).errors(messages)

Where form is the selector of the form you would like error messages attached
and messages is an object of error messages in the format:

	{ "field_name" : "Message string",
	  "field_2"    : "This is an error" };

--------------------------- FUTURE FEATURES ---------------------------
- Icon can be specified on a per error basis, eg.
{ "field" : { "message" : "Field must not be empty", "icon" : "error" } }

*/

// Preloading of images on document load
$(function(){
    $("<img>").attr("src", "http://connectbooks.com.au/img/layout/error_sprite.png");
});

(function($) {
	
	// Errors.element is literally the element (input, textarea) the error is
	// being applied to
	
	// Errors.field is the div.field element that contains the label, form
	// element and errorContainer (if an error exists for the contained element)
	
	$.fn.errors = function(o) {	

		if(!$.fn.errors.opts)
			$.fn.errors.config();

		$.fn.errors.addErrors(o);
		if($.fn.errors.opts.mode == "ajax") {
			$.fn.errors.removeErrors(o);
		}
		$.fn.errors.focusError();
		
		$(window).resize(function(){
			$.fn.errors.updatePositions();
		});
		
		return this;
	};
	
	$.fn.errors.config = function(o) {
		$.fn.errors.opts = $.extend({}, $.fn.errors.defaults, o);
	};

	$.fn.errors.defaults = {
		width:  29,		// "rolled-in" state width
		height: 38,     
		icon:   "error", // "error" is default, "accept", "warning" and "info" are alternatives
		containerName: ".field", // enclosing div as described above
		mode: "normal" // ajax or normal mode
	};
	
	// Generate position of error on window resize, so to appear attached to form element
	// Use of outerWidth requires jQuery 1.4, faciliatates accurate positioning
	$.fn.errors.updatePositions = function() {
		$('.errorContainer').each(function(){
			var element = $.fn.errors.getElementByError($(this));
			
			// Double check corresponding element is visible
			if(element.is(':hidden'))
				$(this).css('display','none');
			else
				$(this).css('display','block');
				
			// Double check element still exists, otherwise remove error
			if(!element)
				$.fn.errors.removeError($(this));
			
			$(this).css({
				'left' : element.offset().left + element.outerWidth(),
				'top' : element.offset().top
			});
		})
	};
	
	// Focus the first error on screen
	$.fn.errors.focusError = function() {
		
		var error = $('.errorContainer').eq(0);
		var element = $.fn.errors.getElementByError(error);
		
		element.focus();
	};
	
	// Takes a JSON object of errors and appends them to elements on screen specified by keys
	$.fn.errors.addErrors = function(errors) {
		
		var self = this;
		$.each(errors, function(key, val) {
			
			var element = $('[name="' + key + '"]').filter('input, textarea, select');
			var field = $.fn.errors.getField(element);
			var error = $.fn.errors.getErrorByKey(key);

			// If error doesn't already exist, create error
			if(!error)
			{
				var errorFull = $.fn.errors.createError(key, val, element);
				$('body').append(errorFull);	
			
			// Update error message if changed	
			} else if(errors[key] != error.find('span').text() && fn.errors.opts.mode == "ajax") {
				error.remove();
			}
			
		});
		
		$('.errorContainer').fadeIn();
	};
	
	// Takes a JSON object of errors and removes them from the screen
	// They are removed on the condition they no longer exist in the JSON object
	$.fn.errors.removeErrors = function(errors) {
		
		$('.errorContainer').each(function(){ 
			
			var name = $(this).data('key');
			if(errors[name] == null) {
				$(this).remove();
			}
			
		});
	};
	
	// Remove a single error passed as a parameter
	$.fn.errors.removeError = function(item) { 
		if($(item).length == 0)
			return false;
		
		$(item).fadeOut('fast', function(e){ $(this).remove() });
	}
	
	// Takes an error string (val) and creates markup for an error
	// Size/padding/margin info is taken from element+label to determine positioning
	// Binds error so that mouseover/out or field (de)activation extends/contracts it
	$.fn.errors.createError = function(key, val, element) {
		
		var field = $.fn.errors.getField(element);
		
		var isValidInput = function(e) { // TAB, CTRL, SHIFT, ALT, CAPS LOCK, COMMAND - invalid
			
			if(e.keyCode != 9 && e.keyCode != 224 && (e.keyCode < 16 || e.keyCode > 20)) {
				return true;
			}
			return false;
		};
		
		// Create elements
		var $errorContainer = $('<div />').attr('class','errorContainer'); // Outer container
		var $errorText = $('<div />').attr('class','errorText'); // Inner container
		var $errorIcon = $('<div />').attr('class', 'icon ' + this.opts.icon); // Icon - *configurable*
		var $errorSpan = $('<span />').append(val); // Error message
		
		// Animation init and binding
		$errorText.append($errorIcon).append($errorSpan);

		// Take into account vertical space added by container div
		var docOffset = element.offset();

		$errorContainer // Mouse moves over/out actual error container
		    .css({'left': docOffset.left + element.outerWidth(), 'margin-left':'-3px', 'top': docOffset.top })
			.width(this.opts.width)
			.height(this.opts.height)
			.data('open','false')
			.data('key', key) // stored so the associated field can be located at a later point
			.bind('mouseenter', function(e){$.fn.errors.errorOpen(this, 'over')})
			.bind('mouseleave', function(e){$.fn.errors.errorClose(this, 'over')})
			.hide();
		
		$errorText.css('height', this.opts.height).appendTo($errorContainer); // Explicit height statement for IE
		
		element // User selects/deselects erroneous text field 	
			.bind('focus', function(e){$.fn.errors.errorOpen($($errorContainer), 'field')})
			.bind('blur', function(e){$.fn.errors.errorClose($($errorContainer), 'field')})
			.bind('keydown', function(e){
				if(isValidInput(e)) { 
					$.fn.errors.removeError($($errorContainer))
				}
			});

		if(element.children().length > 0) { // If select, removal condition is change rather than blur
			element.bind('change', function(e){
				if(element.val() != 0) {
					$.fn.errors.removeError($($errorContainer))
				}
			});
		}
		
		return $errorContainer;
	};
	
	$.fn.errors.reset = function() {
		$('.errorContainer').each(function() {
			$(this).remove();
		});
	}
	
	// Rollout error to full extension
	// Source: valid source strings include 'field' and 'over'
	// Item: div.errorContainer
	$.fn.errors.errorOpen = function(item, source) {
		if($(item).data('open') == 'false')
		{
			var itemWidth = this.opts.width 
							+$(item).children('.errorText').children('span').width() // Width of text
							+19; // Additional padding
							
			$(item).animate({
						width: itemWidth
					}, 100)
					.css('z-index','11')
				    .data('open','true')
					.data('source', source);
		}
	}
	
	// Roll-in error to minimum size (icon only visible)
	$.fn.errors.errorClose = function(item, source) {
		var self = this;
		if(source == $(item).data('source') || source == 'field')
		{
			$(item).animate({
				width: self.opts.width
			}, 100, function(e){
				$(this).data('open','false')
			})
			.css('z-index','5');
		}
	}
	
	// Returns the enclosing field of the element
	$.fn.errors.getField = function(element) {
		var field = element.parents($.fn.errors.opts.containerName);

		if(field.length == 0) // If specific enclosing field is not present, return parent
			field = element.parent();
			
		return field;
	}
	
	// Returns error specified by key (associated element name property)
	$.fn.errors.getErrorByKey = function(key) {
		var element = null;
		
		$('.errorContainer').each(function(){
			if($(this).data('key') == key) {
				element = $(this);
				return false;
			}
		});
		
		return element;
	};
	
	// Returns element from error
	$.fn.errors.getElementByError = function(error) {
		var name = error.data('key');
		var element = $('[name="' + name + '"]').filter('input, textarea, select');
		
		return element;
	};
	
})(jQuery);	