/*!
 * plum.Form v1.4: Styling web forms
 *
 * Copyright 2011 RoboCréatif, LLC
 * <http://robocreatif.com>
 *
 * Date: 13 October, 2011
 */

var plum = plum || {};

String.prototype.plum = Number.prototype.plum = jQuery.fn.plum = function(callback, options)
{
	var action = callback.split('.'), secondary;
	callback = action[0];
	if (action.length > 1) {
		secondary = options;
		options = action[1];
	}
	return typeof plum[callback] === 'function' ? plum[callback].call(this, options, secondary) : this;
};

/**
 * DOMNodeInserted emulation (custom "plum" event)
 *
 * Plum plugins love AJAX, and it's always awesome to not have the need to call a
 * plugin again when a new page has been loaded with AJAX. This can be accomplished
 * by listening to DOM mutation events. Unfortunately, DOMNodeInserted is not
 * supported in IE 7 and 8, and has been deprecated by the W3C (meaning future
 * browsers may also not support it). A forceful workaround is to play with jQuery's
 * DOM mutation methods to trigger a custom event.
 *
 * @package  Plum
 * @since    1.3
 */
(function() {
	$.each([ 'html', 'append' ], function (k, v) {
		k = $.fn[v];
		$.fn[v] = function () {
			var result = k.apply(this, arguments);
			this.trigger('plum', [ result ]);
			return result;
		};
	});
}(jQuery));
(function ($) {

	// Microsoft, you will one day leave this world
	var ie7 = $.browser.msie && parseFloat($.browser.version) < 8;

	// Checks for browser support for uploading files, plum containers
	// and invalid fields
	$.support.file = window.File && window.FileList;
	$.support.filexhr = window.XMLHttpRequestUpload;
	$.expr[':'].plum = function (a) { return !!$(a).data('plum-form-options'); };
	$.expr[':'].invalid = function (a) { return !!$(a).data('invalid'); };

	// Older browsers need to submit forms to a hidden iframe to support
	// file uploads via ajax. What a shame
	if ((!$.support.file || !$.support.filexhr) && !$('iframe[name="plum-form"]').length) {
		var iframe;
		$(function () {
			iframe = $('<iframe name="plum-form">').attr('src', 'about:blank').css({
				border: 0,
				left: '-9999em',
				height: 0,
				position: 'absolute',
				top: '-9999em',
				width: 0
			}).appendTo('body');
		});
	}

	plum.form = function (options, method) {

		var prototype = plum.form.prototype, selector = this.selector, forms = $();

		if (typeof options === 'string') {
			return this.each(function () {
				if (typeof prototype.modules[options] === 'function') {
					var elem = $(this);
					elem.selector = selector;
					prototype.modules[options].call(elem, method);
				}
			});
			return;
		}

		// Loop through the matched elements and find a form related to each one.
		// If a form exists, add the element as a jQuery object to the form's
		// "plum-form" data cache.
		this.each(function () {
			var elem = $(this),
			form = this.nodeName.toLowerCase() === 'form',
			input = $(':input:not(:hidden)', this);
			if (form) {
				form = elem;
			} else {
				form = elem.closest('form');
				form = form.length ? form : elem.find('form');
			}
			prototype.add(form, elem.is(':input') ? elem : input, options);
			forms = forms.add(form);
		});

		// Run plum.Form on each matched form
		forms.each(prototype.init());

		// Add the selector to the cache to listen for DOM changes
		prototype.listen(this.selector);

		return this;

	};
	plum.form.fields = {

		/**
		 * Handles checkboxes
		 *
		 * @since  1.0
		 */
		checkbox: function () {
			var c = this.data('plum-form-options').classes,
			form = this.closest('form'),
			elem = this.bind('click', function () {
				var boxes,
				group = elem.hasClass('check-all') && elem.attr('class').match(/group-([^\s]+)/)[1];
				if (!group) {
					this.wrapper.toggleClass(c.checked);
					plum.form.prototype.checkboxes.call(elem, this.name);
				} else {
					form.data('plum-form').filter(function () {
						return this.type === 'checkbox'
							&& !this.disabled
							&& (this.name === group || $(this).hasClass('group-' + group));
					}).each(function () {
						if (elem[0].checked || this.wrapper.hasClass(c.mixed)) {
							this.checked = true;
							this.wrapper.removeClass(c.mixed).addClass(c.checked);
						} else {
							this.checked = false;
							this.wrapper.removeClass(c.checked + ' ' + c.mixed);
						}
					});
				}
			}).css({
				left: '50%',
				marginLeft: -this[0].offsetWidth / 2,
				marginTop: -this[0].offsetHeight / 2,
				opacity: 0,
				position: 'absolute',
				top: '50%'
			});
			this[0].wrapper.css({ verticalAlign: '' });
		},

		/**
		 * Handles file uploads
		 *
		 * @since  1.0
		 */
		file: function () {

			var options = this.data('plum-form-options'),
			a = options.ajax,
			c = options.classes,
			f = options.file,
			form = this.closest('form'),

			file = {

				/**
				 * Creates a more human-friendly version of a file's size. This takes
				 * a byte-integer and converts it to Gb, Mb, Kb or bytes.
				 *
				 * @since   1.1
				 * @param   int     size  File size in bytes
				 * @return  string  A human-friendly file size
				 */
				size: function (size) {
					size   = { B: size };
					size.K = size.B / 1024;
					size.M = size.K / 1024;
					size.G = size.M / 1024;
					return size.G > 1 ? Math.round(size.G) + ' GB'
						: size.M > 1 ? Math.round(size.M) + ' MB'
						: size.K > 1 ? Math.round(size.K) + ' KB'
						: size.B > 0 ? size.B + ' bytes'
						: '';
				},

				/**
				 * Adds a file to the upload queue
				 *
				 * A new list item is appended to the file list, which contains details
				 * about the current file, such as error status, loading progress and
				 * file information. If an HTML tag in the item HTML contains a "remove"
				 * class, and that tag is clicked, the file is removed from the queue.
				 *
				 * @since  1.0
				 */
				add: function () {

					var queue = form.data('plum-form-queue'),
					li = $('<li>', {
						'class': (this.error ? c.error : c.waiting)
							+ ' plum-upload-' + (index - 1),
						css: { display: 'none' },
						html: f.html
							.replace(/\{filename\}/g, this.name)
							.replace(/\{filesize\}/g, file.size(this.size))
							.replace(/\{filetype\}/g, this.type)
							+ this.error
							+ '<div style="clear:both"></div>'
					}).appendTo(filelist).fadeIn(300);

					$('.' + c.remove, li).bind('click', function () {
						var i = li.attr('class').match(/(?:(?:.+\s+)?)?plum-upload-([\d]+)/)[1],
						file = $(':file.plum-upload-' + i);
						li.fadeOut(300, function () { li.remove(); });
						if (typeof queue[i] !== 'undefined') {
							queue.splice(i, 1);
							form.data('plum-form-queue', queue);
						}
						!$.support.file && file.remove();
					});

					// If an error occurred, or if the browser doesn't support uploading
					// files via AJAX, remove the progress bar. Otherwise, make sure the
					// progress bar is empty.
					if (this.error || !$.support.filexhr) {
						$('.' + c.progress, li).remove();
					} else {
						$('.' + c.progress, li).children().css({ width: 0 });
					}

				},

				/**
				 * Listen for changes in the original file input field
				 *
				 * For modern browsers that support AJAX uploads and the File API, file
				 * objects are added to the upload queue and the field is emptied. In
				 * older browsers, multiple file support is accomplished by creating
				 * additional file fields and stacking them next to each other in the
				 * plum.Form wrapper.
				 *
				 * @since  1.0
				 */
				change: function () {

					var i = 0, l,
					elem = $(this),
					queue = form.data('plum-form-queue'),
					files,
					props = { name: this.value, size: '', type: '', error: '' };

					// Hooray, you're using an awesome browser
					if ($.support.file) {
						if (!options.ajax) {
							queue = [];
							filelist.children().remove();
						}
						for (l = this.files.length; i < l; i++) {
							// Too many files, stop adding
							if (f.files && queue.length === f.files) {
								break;
							}
							// Set the file properties
							files = this.files[i];
							props.name = files.name || files.fileName;
							props.size = files.size || files.fileSize;
							props.type = files.type || files.fileType;
							if (f.types.length && $.inArray(props.type, f.types) < 0) {
								props.error = '<div>' + f.errortype + '</div>';
							} else if (f.size && props.size > f.size) {
								props.error = '<div>' + f.errorsize + '</div>';
							}
							// No errors occured, so the file can be queued
							if (!props.error) {
								index++;
								queue.push(files);
							}
							// Add the file to the file list
							form.data('plum-form-queue', queue);
							file.add.call(props);
						}

					// Stupid browser and too many files
					} else if (f.files && queue.length === f.files) {
						return false;

					// Stupid browser, so no informational errors can be given
					} else {
						index++;
						queue.push(props.name);
						form.data('plum-form-queue', queue);
						file.add.call(props);

						// Hide the previous file field and create a new one
						elem = elem.css({ zIndex: -998 })
							.unbind('change', file.change)
							.after(elem.clone().val(''))
							.next()
							.data('plum', true)
							.removeClass('plum-upload-' + (index - 1))
							.addClass('plum-upload-' + index)
							.css({ zIndex: 998 })
							.bind('mousedown mouseup', function () {
								$(this).parent().toggleClass(c.active);
							})
							.bind('change', file.change);

					}

				}

			},

			index = 0,
			maxSize = file.size(f.size),
			filelist = $('<ul>', { 'class': c.filelist }).insertAfter(this),

			// The original file field is wrapped with a new plum and given
			// a pseudo-button
			mouse,
			button = $('<button type="button">' + f.button + '</button>').appendTo('body'),
			original = $(this)
				.attr('multiple', true)
				.addClass('plum-upload-' + index)
				.css({
					opacity: 0,
					position: 'absolute',
					width: 50
				})
				.wrap($('<div>', {
					'class': 'plum-form ' + c.input + ' ' + c.button,
					css: {
						cssFloat: $(this).css('float'),
						display: 'inline-block',
						overflow: 'hidden',
						position: 'relative',
						verticalAlign: 'top',
						width: ie7 ? button.css('width') : '',
						zoom: ie7 ? 1 : ''
					}
				}))
				.before(button)
				.unbind('mousedown mouseup')
				.bind({
					change: file.change,
					mousedown: function () {
						mouse = true;
						this.wrapper.addClass(c.active);
					},
					mouseup: function () {
						mouse = false;
						this.wrapper.removeClass(c.active);
					},
					focus: function () {
						this.wrapper.removeClass(c.focus);
						$(this).parent().addClass(c.focus);
					},
					blur: function () {
						this.wrapper.removeClass(c.focus);
						$(this).parent().removeClass(c.focus);
					}
				});
			original.parent().bind({
				mouseover: function () {
					$(this).toggleClass(c.hover);
					if (mouse) {
						$(this).addClass(c.active);
					}
				},
				mouseout: function () {
					$(this).toggleClass(c.hover).removeClass(c.active);
				},
				mousemove: function (e) {
					var elem = $(this), file = $(':file', this).eq(-1);
					file.css({
						left: -file.outerWidth(),
						marginLeft: e.pageX - elem.offset().left + 25,
						top: e.pageY - elem.offset().top - 10
					});
				}
			});

		},

		/**
		 * Handles all unmatched form elements
		 *
		 * @since  1.0
		 */
		input: function () {
			var c = this.data('plum-form-options').classes;
			switch (this[0].type) {
				case 'textarea':
					return this.css({ resize: 'none', verticalAlign: 'bottom' });
				case 'button':
				case 'submit':
					return this.attr('formnovalidate', true);
				default:
					return this.css('verticalAlign', 'bottom').each(function () {
						this.wrapper.addClass(c.text);
					});
			}
		},

		/**
		 * Handles radio buttons
		 *
		 * @since  1.0
		 */
		radio: function () {
			var c = this.data('plum-form-options').classes,
			form = this.closest('form'),
			elem = this.bind('click', function () {
				form.data('plum-form').filter(function () {
					return this.type === 'radio' && this.name === elem[0].name;
				}).each(function () {
					this.wrapper.removeClass(c.checked);
				});
				this.wrapper.addClass(c.checked);
			}).css({
				left: '50%',
				marginLeft: -this[0].offsetWidth / 2,
				marginTop: -this[0].offsetHeight / 2,
				opacity: 0,
				position: 'absolute',
				top: '50%'
			});
			this[0].wrapper.css({ verticalAlign: '' });
		},

		/**
		 * Handles reset buttons
		 *
		 * @since  1.1
		 */
		reset: function () {
			this.bind('click', function (e) {
				e.preventDefault();
				plum.form.prototype.reset.call($(this).closest('form'));
			});
		},

		/**
		 * Handles select-one and select-multiple drop down menus
		 *
		 * @since  1.0
		 */
		select: function () {

			var elem = this,
			c = this.data('plum-form-options').classes,
			index = 0,
			i = 0,
			wrapper = this[0].wrapper,
			disabled = false,
			multiple = this[0].multiple,
			size = this[0].size || (multiple ? 5 : 10),
			open = !!multiple,
			closing = false,
			closed = !multiple,
			optionslist,
			search = '',
			escKey = false,

			// Event methods
			select = {

				/**
				 * Listening for clicks on options in the menu.
				 *
				 * @since  1.1
				 * @param  object  e  "click" event object
				 */
				click: function (e) {
					i = menuoptions.index(this);
					e && e.preventDefault();
					if (multiple) {
						elem.trigger('focus');
						select[e.shiftKey ? 'shift' : e.ctrlKey ? 'ctrl' : 'one'](this);
					} else {
						select.one(this);
					}
					if ((closed || (multiple && !e.shiftKey)) && index !== i) {
						index = i;
						elem.trigger('change');
					}
				},

				/**
				 * Applicable only to select-one menus, a menu is closed when an
				 * option is clicked or the menu defocused.
				 *
				 * @since  1.1
				 */
				close: function () {
					if (multiple || closing) {
						return;
					}
					closing = true;
					if (!escKey && index !== i) {
						index = i;
						elem.trigger('change');
					} else {
						select.click.call(menuoptions.eq(index));
					}
					closed = true;
					open = false;
					escKey = false;
					elem.trigger('close');
					return menu.stop(true, true).slideUp(150, function () {
						closing = false;
						wrapper.css('zIndex', '').addClass(c.closed).removeClass(c.open);
						menu.css('marginTop', 0);
					});
				},

				/**
				 * Applicable only to select-one menus, a menu is opened when the
				 * selected value text or the arrow is clicked.
				 *
				 * To prevent the menu from doing stupid things to the document's
				 * global height, the top margin of the menu container should be
				 * calculated to keep the top and bottom of the menu at least 25
				 * pixels from the top and bottom of the document. Opened menus
				 * are given a z-index of 9999.
				 *
				 * @since  1.1
				 */
				open: function () {

					open = true;
					closed = false;
					elem.trigger('focus').trigger('open');

					// Calculate the top margin to stay within document constraints
					var docHeight = $(document).height(),
					offsetTop = wrapper.offset().top,
					marginTop = -value.outerHeight(true)
						- parseInt(elem[0].wrapper.css('borderTopWidth'), 10);
					if (offsetTop + height + 50 > docHeight) {
						marginTop = docHeight - (offsetTop + height) - 50;
					}
					if (marginTop * -1 > offsetTop + 25) {
						marginTop = -offsetTop + 25;
					}

					// Animate the menu and put the wrapper above everything
					wrapper.css({ zIndex: 9999 }).addClass(c.open).removeClass(c.closed);
					menu.stop(true, true)
						.animate({ marginTop: marginTop }, 150)
						.slideDown(150, function () {
							menu.scrollTop(index * maxHeight / size);
						});

				},

				/**
				 * Handles keydown events on a menu (select-one and -mutiple).
				 *
				 * @since  1.1
				 * @param  object  e  "keydown" event object
				 */
				keydown: function (e) {
					switch (e.which) {
						// While typing in a menu, the backspace key will remove one
						// character from the search string.
						case 8:
							e.preventDefault();
							search = !search ? '' : search.substring(0, search.length - 1);
							select.search(e);
							break;
						// Pressing the tab key closes a select-one menu.
						case 9:
							search = '';
							if (!multiple) {
								select.close();
							}
							break;
						// Pressing the escape key closes a menu.
						case 27:
							search = '';
							if (!multiple) {
								escKey = true;
								select.close();
							}
							break;
						// The up and down arrow keys can browse through the available options
						// in a menu.
						case 38:
						case 40:
							e.ctrlKey = false;
							i = e.which === 38
								? (i - 1 < 0 ? 0 : i - 1)
								: (i + 1 >= menuoptions.length ? menuoptions.length - 1 : i + 1);
							select.click.call(menuoptions.eq(i)[0], e);
							break;
						default:
							break;
					}
				},

				/**
				 * Keyboard handler for the enter key when focused on a menu option.
				 *
				 * @since  1.1
				 * @param  object  e  "keypress" event object
				 */
				keypress: function (e) {
					var i = 0, l;
					if (!e.which) {
						search = '';
						return this;
					}
					if (e.which === 13) {
						search = '';
						if (!multiple) {
							select[wrapper.hasClass(c.open) ? 'close' : 'open']();
						}
						return this;
					}
					e.preventDefault();
					search += String.fromCharCode(e.which);
					select.search(e);
				},

				/**
				 * Keyboard handler when holding the control/command key inside a
				 * select-multiple menu
				 *
				 * @since  1.1
				 */
				ctrl: function () {
					original[i].selected = !original[i].selected;
					menuoptions.eq(i).toggleClass(c.selected);
				},

				/**
				 * Filters the available text values in an option list to match that
				 * of the search string while type-selecting options.
				 *
				 * @since  1.1
				 * @param  string  e  The value to find in the option list
				 */
				search: function (e) {
					var j = 0, l = optionvalues.length;
					for (; j < l; j++) {
						if (optionvalues[j].substring(0, search.length) === search) {
							return select.click.call(menuoptions.eq(j)[0], e);
						}
					}
				},

				/**
				 * Keyboard handler when holding the shift key inside a select-
				 * multiple menu.
				 *
				 * @since  1.1
				 */
				shift: function () {
					var s = index > i ? i : index, e = index > i ? index : i;
					original.each(function () { this.selected = false; });
					menuoptions.removeClass(c.selected);
					for (; s <= e;) {
						original[s].selected = true;
						menuoptions.eq(s++).addClass(c.selected);
					}
				},

				/**
				 * Actionable event for selecting an option in a select-one menu.
				 *
				 * @since  1.1
				 * @param  object  elem  The selected option in the original menu
				 */
				one: function (elem) {
					original.each(function () { this.selected = false; })[i].selected = true;
					menuoptions.removeClass(c.selected).eq(i).addClass(c.selected);
					selected.text($(elem).text());
					menu.scrollTop(i * maxHeight / size);
				},

				/**
				 * Rebuilds a Plum menu to allow for changes to the original menu
				 *
				 * @since  1.3
				 */
				rebuild: function () {
					optionslist = '';
					search = '';
					optionvalues = [];
					disabled = elem[0].disabled;
					elem.children().each(compile);
					menu.show().html(optionslist).css({ minWidth: elem.width(), width: '' });
					maxHeight = $('li.' + c.option + ':eq(0)', menu).outerHeight(true) * size;
					menu.css({
						display: multiple ? 'block' : 'none',
						maxHeight: maxHeight
					});
					height = menu.outerHeight(true);
					original = $('option:not(:disabled)', elem);
					menuoptions = menu.find('li.' + c.option + ':not(.' + c.disabled + ')')
						.bind('click', select.click)
						.each(function () {
							optionvalues.push($(this).text().toLowerCase())
						});
					if (!multiple && !selected.text()) {
						selected.text(
							elem.find('option[selected]').text() ||
							elem.find('option:eq(0)').text()
						);
					}
					wrapper
						[disabled ? 'addClass' : 'removeClass'](c.disabled)
						.css({ width: menu.outerWidth(true) });
					return true;
				}

			},

			// The menu's internal wrapper
			innerwrapper = $('<div>', { 'class': c.wrapper }).prependTo(wrapper),

			// The selected value of select-one menus
			value = multiple ? $() : $('<div>', {
					'class': c.value,
					css: { position: 'relative' },
					html: '<div></div><div class="' + c.arrow + '"></div>'
				})
				.appendTo(innerwrapper)
				.css({ verticalAlign: 'bottom' }),

			// The text of the selected value
			selected = value.children('div:first-child'),

			// Menu dimensions
			height = 0,
			width = elem[0].offsetWidth,
			maxHeight = 0,

			// The list of menu options and groups
			menu = $('<ul>', {
				'class': c.container,
				css: {
					overflowX: 'hidden',
					overflowY: 'scroll',
					position: 'relative',
					whiteSpace: 'nowrap',
					width: width
				}
			}).appendTo(innerwrapper),

			// Builds the menu
			original = null,
			menuoptions = null,
			optionvalues = [],
			compile = function () {
				var node = this.nodeName.toLowerCase(),
				text = this.label || this.textContent || this.innerText;
				optionslist += '<li class="' + c[node]
					+ ' ' + (this.disabled && c.disabled || '')
					+ ' ' + (this.selected && c.selected || '')
					+ '">';
				if (node === 'option') {
					optionslist += text;
					if (this.selected) {
						selected.text(text);
					}
				} else {
					optionslist += '<label>' + text + '</label><ul>';
					$(this).children().each(compile);
					optionslist += '</ul>';
				}
				optionslist += '</li>';
			};

			// Hide that pesky old menu, listen for keyboard activity, and adjust
			// the wrapper to its proper state
			this.css({
				opacity: 0,
				position: 'absolute',
				top: 0,
				zIndex: -999
			}).bind({
				change: select.close,
				keydown: select.keydown,
				keypress: select.keypress,
				rebuild: select.rebuild
			});
			wrapper
				.addClass(multiple ? c.multiple + ' ' + c.open : c.single + ' ' + c.closed)
				.bind('mousedown', false);

			// Build the menu
			this.trigger('rebuild');

			// If the menu is not disabled, we can determine the selected index.
			!disabled && menuoptions.each(function (k) {
				if ($(this).hasClass(c.selected)) {
					index = i;
					i = k;
					return false;
				}
			});

			// Select-multiple menus can stop processing here
			if (multiple) {
				return;
			}

			/**
			 * For select-one menus, the menu wrapper needs to have an absolute position
			 * to allow for overflow. When the document is clicked, Plum should determine
			 * whether the menu needs to be opened or closed by checking for an existing
			 * class in the wrapper. Clicks on disabled menus, disabled options and option
			 * groups can of course be ignored.
			 */
			menu.css({ position: 'absolute' });
			$(document).bind('click', function (e) {
				e = $(e.target);
				if (e.closest('div.plum-form.' + c.select)[0] !== wrapper[0]) {
					open && select.close();
					return this;
				}
				if (disabled || e.hasClass(c.disabled) || e.is('label')) {
					return this;
				}
				select[open ? 'close' : 'open']();
			});

		}

	};
	plum.form.prototype = {

		/**
		 * Default configuration options
		 *
		 * @since  1.0
		 */
		defaults: {
			action: null,
			ajax: false,
			complete: function () { },
			classes: {
				active: 'active',
				arrow: 'select-arrow',
				button: 'button',
				checkbox: 'checkbox',
				checked: 'checked',
				closed: 'closed',
				color: 'color',
				container: 'select-container',
				date: 'date',
				datetime: 'datetime',
				disabled: 'disabled',
				file: 'file',
				filelist: 'filelist',
				focus: 'focus',
				email: 'email',
				error: 'error',
				hover: 'hover',
				info: 'info',
				input: 'input',
				label: 'label',
				loading: 'loading',
				mixed: 'mixed',
				month: 'month',
				multiple: 'multiple',
				number: 'number',
				open: 'open',
				optgroup: 'optgroup',
				option: 'option',
				password: 'password',
				progress: 'progress',
				radio: 'radio',
				range: 'range',
				remove: 'remove',
				reset: 'reset',
				submit: 'submit',
				text: 'text',
				textarea: 'textarea',
				select: 'select',
				search: 'search',
				selected: 'selected',
				single: 'single',
				success: 'success',
				tel: 'tel',
				url: 'url',
				value: 'select-value',
				waiting: 'waiting',
				week: 'week',
				wrapper: 'select-wrapper'
			},
			file: {
				button: 'Choose a file...',
				complete: function () { },
				errorsize: 'Please choose a file smaller than {filesize}.',
				errortype: 'This file type is not allowed.',
				files: 0,
				html: '<span class="filename">{filename}</span>'
					+ '<span class="remove">&times;</span>'
					+ '<span class="filesize">{filesize}</span>'
					+ '<div class="progress"><div></div></div>',
				progress: function (e) {
					e.progressbar.children().stop(true, true)
						.animate({ width: e.percent + '%' }, 150);
				},
				size: 0,
				start: function () { },
				types: []
			},
			json: false,
			labels: false,
			reset: false,
			shake: true,
			submit: function () { }
		},

		/**
		 * Adds an element to a form's data cache
		 *
		 * @since  1.4
		 * @param  object  form     A jQuery object containing any forms
		 * @param  object  fields   One or more form fields to add
		 * @param  object  options  The list of configuration options
		 */
		add: function (form, fields, options) {
			var defaults = this.defaults;
			if (form.length) {
				if (!form.data('plum-form')) {
					form.data('plum-form', $());
				}
				fields.each(function () {
					var field = $(this), o;
					if (!field.data('plum-form-options')) {
						o = $.extend(true, {}, defaults, options);
						field.data('plum-form-options', o);
						form.data('plum-form', form.data('plum-form').add(this))
							.data('plum-form-queue', [])
							.data('plum-form-options', o);
					}
				});
			};
		},

		/**
		 * Initiates plum.Form on form elements
		 *
		 * @since  1.4
		 */
		init: function () {
			var prototype = this;
			return function () {
				var form = $(this), i = 0;
				form
				.unbind('submit', prototype.submit)
				.bind('submit', prototype.submit)
				.data('plum-form').each(function() {

					var elem = $(this), options = elem.data('plum-form-options');

					if (elem.data('plum-form') || !options) {
						return;
					}

					var c = options.classes,
					display = elem.css('display'),
					pos = elem.css('position'),
					node = this.nodeName.toLowerCase(),
					type = (elem.attr('type') || this.type || this.nodeName).toLowerCase(),
					label = elem.parent().is('label') ? elem.parent() : $('label[for="' + this.id + '"]'),
					mouse;

					// The relevant form's action URL should be updated according to
					// the current element's options. It should be noted that if plum.Form
					// is initiated separately on various elements in the same form, the
					// last matched element's action will take priority.
					form[0].action = options.action || form[0].action || window.location.href;

					// Create a wrapper for each element based on its properties and styles
					this.wrapper = elem.data('plum-form', true)
						.css({ position: 'relative', overflow: 'visible' })
						.wrap($('<div>', {
							'class': 'plum-form'
								+ ' ' + (c[type] || '')
								+ ' ' + (c[node] || '')
								+ ' ' + (this.disabled && c.disabled || '')
								+ ' ' + (this.checked && c.checked || ''),
							title: this.title,
							dir: this.dir,
							css: {
								cssFloat: elem.css('float'),
								display: display === 'inline' && !ie7 ? 'inline-block' : display,
								position: pos === 'static' ? 'relative' : pos,
								verticalAlign: 'top',
								width: ie7 || /^(?:button|checkbox|file|submit|reset|radio)$/.test(type) ? '' : elem.width(),
								zoom: ie7 ? 1 : ''
							}
						}))
						// Add or remove the "focus" class
						.bind('focus blur', function (e) {
							this.wrapper[e === 'focus' ? 'addClass' : 'removeClass'](c.focus);
						})
						.parent()
						// Add or remove the "active" class
						.bind({
							mousedown: function () { mouse = true; $(this).addClass(c.active); },
							mouseup: function () { mouse = false; $(this).removeClass(c.active); },
							mouseenter: function () { mouse && $(this).addClass(c.active); },
							mouseleave: function () { $(this).removeClass(c.active); }
						});

					elem.css({ width: this.wrapper.css('width') });
					plum.form.fields[typeof plum.form.fields[type] === 'function' ? type
						: typeof plum.form.fields[node] === 'function' ? node
						: 'input'].call(elem);
					label.length && prototype.labels.call(this, label);
					i++;

				});

				// Set the check state for checkbox group handlers
				i && prototype.checkboxes.call(form);

			};
		},

		/**
		 * Counts the number of boxes in a group and checks the group handler
		 * accordingly
		 *
		 * @since  1.0
		 * @param  string  group  The name of the group
		 */
		checkboxes: function (group) {

			var c = this.data('plum-form-options').classes, form = this.closest('form');
			form.data('plum-form').filter(function () {
				return this.type === 'checkbox'
					&& !this.disabled
					&& $(this).hasClass('check-all')
					&& (!group || $(this).hasClass('group-' + group));
			}).each(function () {
				var elem = $(this),
				wrapper = elem.parent(),
				group = elem.attr('class').match(/(?:(?:.+\s+)+)?group-([^\s]+)/)[1],
				boxes = form.data('plum-form').filter(function () {
					return this.type === 'checkbox'
						&& !this.disabled
						&& this.name === group
						&& !$(this).hasClass('check-all');
				}),
				checked = boxes.filter(function () { return this.checked; });
				if (checked.length === 0) {
					this.checked = false;
					wrapper.removeClass(c.checked + ' ' + c.mixed);
				} else {
					this.checked = true;
					if (checked.length === boxes.length) {
						wrapper.removeClass(c.mixed).addClass(c.checked);
					} else {
						wrapper.addClass(c.checked + ' ' + c.mixed);
					}
				}
			});

		},

		/**
		 * Creates in-field labels that fade when the field is clicked
		 *
		 * @since  1.1
		 * @param  object  elem   The original form element with an attached label
		 * @param  object  label  The HTML object containing the original form
		 *                        element's label
		 */
		labels: function (label) {

			var elem = $(this),
			options = elem.data('plum-form-options'),
			inner;

			// Bind hover events to the label
			label.bind('mouseover mouseout', function () {
				elem[0].wrapper.toggleClass(options.classes.hover);
			});

			// These types don't support in-field labels
			if (
				!options.labels ||
				/^(?:button|checkbox|file|select-one|select-multiple|submit|radio|reset)$/
					.test(this.type.toLowerCase() || this.nodeName.toLowerCase())
			) {
				return;
			}

			// Create the new label
			inner = $('<label>', {
				'class': options.classes.label,
				css: {
					display: 'block',
					height: this.clientHeight,
					left: -parseInt(elem.css('borderLeftWidth'), 10)
						+ parseInt(elem.css('paddingLeft'), 10),
					overflow: 'hidden',
					position: 'absolute',
					top: -parseInt(elem.css('borderTopWidth'), 10),
					whiteSpace: this.nodeName.toLowerCase() === 'textarea' ? 'normal' : 'nowrap',
					maxWidth: this.clientWidth
						- parseInt(elem.css('borderRightWidth'), 10)
						- parseInt(elem.css('paddingRight'), 10)
				},
				text: label.text()
			})
			.appendTo(elem.parent())
			.bind('mousedown', function () {
				elem.trigger('focus');
				return false;
			});

			// Get rid of the original label
			if (elem.parent().parent().is('label')) {
				this.wrapper.insertAfter(label);
			}
			label.remove();

			// Check for an initial value
			this.value && inner.hide().css({ opacity: 0 });

			// Listen for activity on the field, and adjust the visibility
			// of the label accordingly
			elem.bind('focus blur', function (e) {
				if (!this.value) {
					inner.show().stop().animate({
						opacity: e.type === 'focus' ? 0.3 : 1
					}, 250);
				} else {
					inner.hide().css({ opacity: 0 });
				}
			}).bind('keypress', function (e) {
				if (e.which) {
					inner.hide().css({ opacity: 0 });
				}
			});

		},

		/**
		 * Listens for new form elements added to the DOM using Plum's custom event
		 *
		 * @since  1.4
		 * @param  string  selector  A selector to listen for
		 */
		listen: function (selector) {
			var form;
			$('body').bind('plum', function (e, html) {
				$(':input', html[0]).each(function () {
					var elem = $(this);
					if (elem.closest(selector).length && !elem.data('plum-form')) {
						form = elem.closest('form');
						plum.form.prototype.add(form, elem, $(selector).data('plum-form-options'));
					}
				});
				form && plum.form.prototype.init().call(form);
			});
		},

		/**
		 * Listens for form submissions and handles the form based on AJAX configuration
		 *
		 * @since   1.0
		 * @param   object  e  The submit event object
		 * @return  object  The form HTML object
		 */
		submit: function (e) {

			var form = $(this),
			prototype = plum.form.prototype,
			options = form.data('plum-form-options'),
			files = $(':file:plum', this).parent().parent(),
			filelist = files.find('li'),
			invalid,
			c = options.classes;

			// Validate the form and run the submit callback
			form.data('plum-form').trigger('blur');
			invalid = $(':input:plum:invalid', this);
			if (invalid.length || options.submit.call(this) === false) {
				options.shake && invalid.plum('form.shake');
				return false;
			}

			// No AJAX, submit the form normally
			if (!options.ajax) {
				return this;
			}
			$(':submit', this).attr('disabled', true);

			// Modern browsers rock
			if ($.support.filexhr) {
				e.preventDefault();
				files = files.find(':file').data('plum-form-options');
				prototype.upload.call(form, files, filelist, function () {
					$.ajax(form[0].action, {
						type: form[0].method || 'GET',
						data: form.serialize(),
						dataType: options.json ? 'json' : 'html',
						success: function (e) {
							$(':submit', form).attr('disabled', false);
							options.reset && prototype.reset.call(form);
							options.complete.call(form[0], e);
						}
					});
				});
				return this;
			}

			// Old browsers do not rock
			this.target = 'plum-form';
			filelist
				.filter(function () { return !$(this).hasClass(c.error); })
				.toggleClass(c.waiting + ' ' + c.loading);
			iframe.one('load', function () {
				$(':submit', form).attr('disabled', false);
				options.reset && prototype.reset.call(form);
				options.complete.call(form[0], $(this).contents().find('body').html());
				files.each(function () {
					$(':file', this).slice(0, -1).remove();
					$('ul.' + c.filelist + ' li', this).fadeOut(300, function () {
						$(this).remove();
					});
				});
			});
			return this;

		},

		/**
		 * Resets a form to its original state
		 *
		 * @since  1.1
		 */
		reset: function () {

			var c = this.data('plum-form-options').classes;
			this[0].reset();
			this.data('plum-form-queue', []);
			this.data('plum-form').each(function () {

				var e = $(this), w = e.parent(), files, menu;
				switch (this.type) {
					case 'checkbox':
					case 'radio':
						if (this.checked) {
							w.addClass(c.checked);
						} else {
							w.removeClass(c.checked + ' ' + c.mixed);
						}
						break;
					case 'file':
						files = w.find(':file');
						files.each(function (k) {
							if (k < files.length - 1) {
								$(this).remove();
							}
						});
						$('li', w.next()).each(function () {
							var li = $(this).fadeOut(300, function () { li.remove(); });
						});
						break;
					case 'reset':
						break;
					case 'select-multiple':
					case 'select-one':
						menu = w.find('li.' + c.option).removeClass(c.selected);
						$('option', this).each(function (i) {
							var s = $(this), m = menu.eq(i);
							if (this.selected) {
								m.addClass(c.selected);
								if (e[0].type === 'select-one') {
									s.closest('div.plum-form')
										.find('div.' + c.value + ' div:first-child')
										.text(s.text());
								}
							}
						});
						break;
					default:
						e.trigger('blur');
						break;
				}
				w.children('div.' + c.info).removeClass(c.error + ' ' + c.success);

			});
			plum.form.prototype.checkboxes.call(this);

		},

		/**
		 * Uploads a file via XMLHttpRequest to support the progress event
		 *
		 * @since   1.1
		 * @param   object    options   The options for uploading files
		 * @param   object    filelist  The list of files in queue
		 * @param   function  callback  The function to execute when all files
		 *                              have been uploaded
		 * @return  mixed     The return value of the callback function
		 */
		upload: function (options, filelist, callback) {

			if (!filelist.length) {
				return callback();
			}

			var form = this,
			c = options.classes,
			li = filelist.eq(0),
			queue = form.data('plum-form-queue'),
			file = queue.shift(),
			progressbar = $('.' + c.progress, li).slideDown(300),
			xhr = new XMLHttpRequest();

			form.data('plum-form-queue', queue);

			// Uploading has begun
			xhr.upload.addEventListener('loadstart', function (e) {

				li.toggleClass(c.waiting + ' ' + c.loading);
				options.file.start.call(li, $.extend(e, {
					progressbar: progressbar,
					percent: e.loaded / e.total * 100
				}));

			}, false);

			// File is uploading
			xhr.upload.addEventListener('progress', function (e) {

				options.file.progress.call(li, $.extend(e, {
					progressbar: progressbar,
					percent: e.loaded / e.total * 100
				}));

			}, false);

			// File has finished uploading
			xhr.onreadystatechange = function (e) {

				if (xhr.readyState === 4) {
					options.file.complete.call(li, $.extend(e, {
						progressbar: progressbar,
						percent: 100
					}), options.json ? $.parseJSON(xhr.responseText) : xhr.responseText);
					li.fadeOut(300, function () {
						if (queue.length) {
							plum.form.prototype.upload.call(form, options, filelist.slice(1), callback);
						} else {
							callback();
						}
					});
				}

			};

			xhr.open('POST', form[0].action, true);
			xhr.setRequestHeader('Content-Type', file.type);
			xhr.setRequestHeader('X-File-Name', file.name);
			xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
			xhr.send(file);

		},

		/**
		 * plum.Form modular expressions and methods
		 *
		 * @since  1.2
		 */
		modules: {

			/**
			 * Form validation expressions
			 *
			 * @since  1.2
			 */
			methods: {
				email: /^(?:"[\w!#$%&'*+\-\/=?\^_`{|}~]+"|[\w!#$%&'*+\-\/=?\^_`{|}~]+)@(?:\w(?:\-?[\w]+)?\.)*?\w+(?:\.[a-z]{2})?\.[a-z]{2,6}$/,
				tel: /^(?:(?:\+?1\s*(?:[\.\-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[\.\-]\s*)?)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[\.\-]\s*)?([0-9]{4})(?:\s*(?:#|x\.?|ext\.?|extension)\s*(\d+))?$/,
				url: /^(?:https?:\/\/)?(?:[\w](?:\-?[\w]+)?\.)*?[\w]+(?:\.[a-z]{2})?\.[a-z]{2,4}(\/.+)?$/
			},

			/**
			 * Applies a "shaking" effect to invalid form fields
			 *
			 * @since  1.2
			 */
			shake: function () {
				var elem = this[0].wrapper, i = 0;
				if (typeof elem.data('plum-form-shaking') !== 'undefined') {
					elem.css('left', elem.data('plum-form-shaking')).removeData('plum-form-shaking');
				}
				elem.data('plum-form-shaking', elem.css('left')).stop(true);
				for (; i < 5; i++) {
					elem.animate({ left: '-=15' }, 50).animate({ left: '+=15' }, 50);
				}
			},

			/**
			 * Verifies the value of a field
			 *
			 * @since  1.2
			 * @param  object  elem     The HTML form field object
			 * @param  object  options  Methods to check validation against
			 */
			verify: function (options) {
				var prototype = plum.form.prototype.modules,
				elem = this,
				o = this.data('plum-form-options'),
				c,
				info;
				if (!o) {
					return;
				}
				c = this.data('plum-form-options').classes;
				info = $('<div class="' + c.info + '">').insertAfter(this);
				this.bind('blur', function () {
					var valid = true;
					if (typeof options === 'string') {
						valid = this.value === options;
					} else if (typeof options === 'function') {
						valid = !!options.call(this);
					} else {
						'min' in options && (valid = valid && this.value.length >= options.min);
						'max' in options && (valid = valid && this.value.length <= options.max);
						'method' in options && (valid = 'min' in options && !options.min && !this.value ? true
							: valid && prototype.methods[options.method].test(this.value));
					}
					elem.data('invalid', !valid);
					info.removeClass(valid ? c.error : c.success).addClass(valid ? c.success : c.error);
				});
			}

		}

	};

}(jQuery));
