/*! * Rails 3 Client Side Validations - v3.0.1 * https://github.com/bcardarlela/client_side_validations * * Copyright (c) 2011 Brian Cardarella * Licensed under the MIT license * http://www.opensource.org/licenses/mit-license.php */ (function($) { $.fn.validate = function() { return this.filter('form[data-validate]').each(function() { var form = $(this); var settings = window[form.attr('id')]; // Set up the events for the form form .submit(function() { return form.isValid(settings.validators); }) .bind('ajax:beforeSend', function() { return form.isValid(settings.validators); }) // Callbacks .bind('form:validate:after', function(eventData) { clientSideValidations.callbacks.form.after( form, eventData); }) .bind('form:validate:before', function(eventData) { clientSideValidations.callbacks.form.before(form, eventData); }) .bind('form:validate:fail', function(eventData) { clientSideValidations.callbacks.form.fail( form, eventData); }) .bind('form:validate:pass', function(eventData) { clientSideValidations.callbacks.form.pass( form, eventData); }) // Set up the events for each validatable form element .find('[data-validate]:input') .live('focusout', function() { $(this).isValid(settings.validators); }) .live('change', function() { $(this).data('changed', true); }) // Callbacks .live('element:validate:after', function(eventData) { clientSideValidations.callbacks.element.after( $(this), eventData); }) .live('element:validate:before', function(eventData) { clientSideValidations.callbacks.element.before($(this), eventData); }) .live('element:validate:fail', function(eventData, message) { var element = $(this); clientSideValidations.callbacks.element.fail(element, message, function() { addError(element, message); }, eventData) }) .live('element:validate:pass', function(eventData) { var element = $(this); clientSideValidations.callbacks.element.pass(element, function() { removeError(element); }, eventData) }) // Checkboxes - Live events don't support filter .end().find('[data-validate]:checkbox') .live('click', function() { $(this).isValid(settings.validators); }) // Inputs for confirmations .end().find('[id*=_confirmation]').each(function() { var confirmationElement = $(this), element = form.find('#' + this.id.match(/(.+)_confirmation/)[1] + '[data-validate]:input'); $('#' + confirmationElement.attr('id')) .live('focusout', function() { element.data('changed', true).isValid(settings.validators); }) .live('keyup', function() { element.data('changed', true).isValid(settings.validators); }) }); var addError = function(element, message) { clientSideValidations.formBuilders[settings.type].add(element, settings, message); } var removeError = function(element) { clientSideValidations.formBuilders[settings.type].remove(element, settings); } }); } $.fn.isValid = function(validators) { if ($(this[0]).is('form')) { return validateForm($(this[0]), validators); } else { return validateElement($(this[0]), validators[this[0].name]); } } var validateForm = function(form, validators) { var valid = true; form.trigger('form:validate:before').find('[data-validate]:input').each(function() { if (!$(this).isValid(validators)) { valid = false; } }); if (valid) { form.trigger('form:validate:pass'); } else { form.trigger('form:validate:fail'); } form.trigger('form:validate:after'); return valid; } var validateElement = function(element, validators) { element.trigger('element:validate:before'); if (element.data('changed') !== false) { var valid = true; element.data('changed', false); // Because 'length' is defined on the list of validators we cannot call jQuery.each on // the clientSideValidations.validators.all() object for (kind in clientSideValidations.validators.all()) { if (validators[kind] && (message = clientSideValidations.validators.all()[kind](element, validators[kind]))) { element.trigger('element:validate:fail', message).data('valid', false); valid = false; break; } } if (valid) { element.data('valid', null); element.trigger('element:validate:pass'); } } element.trigger('element:validate:after'); return element.data('valid') === false ? false : true; } // Main hook // If new forms are dynamically introduced into the DOM the .validate() method // must be invoked on that form $(function() { $('form[data-validate]').validate(); }) })(jQuery); var clientSideValidations = { validators: { all: function() { return jQuery.extend({}, clientSideValidations.validators.local, clientSideValidations.validators.remote) }, local: { presence: function(element, options) { if (/^\s*$/.test(element.val())) { return options.message; } }, acceptance: function(element, options) { switch (element.attr('type')) { case 'checkbox': if (!element.attr('checked')) { return options.message; } break; case 'text': if (element.val() != (options.accept || '1')) { return options.message; } break; } }, format: function(element, options) { if ((message = this.presence(element, options)) && options.allow_blank == true) { return; } else if (message) { return message; } else { if (options['with'] && !options['with'].test(element.val())) { return options.message; } else if (options['without'] && options['without'].test(element.val())) { return options.message; } } }, numericality: function(element, options) { if (!/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d*)?$/.test(element.val())) { return options.messages.numericality; } if (options.only_integer && !/^\d+$/.test(element.val())) { return options.messages.only_integer; } var CHECKS = { greater_than: '>', greater_than_or_equal_to: '>=', equal_to: '==', less_than: '<', less_than_or_equal_to: '<=' } for (var check in CHECKS) { if (options[check] && !(new Function("return " + element.val() + CHECKS[check] + options[check])())) { return options.messages[check]; } } if (options.odd && !(parseInt(element.val()) % 2)) { return options.messages.odd; } if (options.even && (parseInt(element.val()) % 2)) { return options.messages.even; } }, length: function(element, options) { var blankOptions = {}; if (options.is) { blankOptions.message = options.messages.is; } else if (options.minimum) { blankOptions.message = options.messages.minimum; } if ((message = this.presence(element, blankOptions)) && options.allow_blank == true && !options.maximum) { return; } else if (message) { return message; } else { var CHECKS = { is: '==', minimum: '>=', maximum: '<=' } var tokenizer = options.js_tokenizer || "split('')"; var tokenized_length = new Function("element", "return (element.val()." + tokenizer + " || '').length;")(element); for (var check in CHECKS) { if (options[check] && !(new Function("return " + tokenized_length + CHECKS[check] + options[check])())) { return options.messages[check]; } } } }, exclusion: function(element, options) { if ((message = this.presence(element, options)) && options.allow_blank == true) { return; } else if (message) { return message; } else { if (options['in']) { for (var i = 0; i < options['in'].length; i++) { if (options['in'][i] == element.val()) { return options.message; } } } else if (options['range']) { var lower = options['range'][0], upper = options['range'][1]; if (element.val() >= lower && element.val() <= upper) { return options.message; } } } }, inclusion: function(element, options) { if ((message = this.presence(element, options)) && options.allow_blank == true) { return; } else if (message) { return message; } else { if (options['in']) { for (var i = 0; i < options['in'].length; i++) { if (options['in'][i] == element.val()) { return; } } return options.message; } else if (options['range']) { var lower = options['range'][0], upper = options['range'][1]; if (element.val() >= lower && element.val() <= upper) { return; } else { return options.message; } } } }, confirmation: function(element, options) { if (element.val() != jQuery('#' + element.attr('id') + '_confirmation').val()) { return options.message; } } }, remote: { uniqueness: function(element, options) { var data = {}; data['case_sensitive'] = !!options.case_sensitive; if (options.id) { data['id'] = options.id; } if (options.scope) { data.scope = {} for (key in options.scope) { var scoped_element = jQuery('[name="' + element.attr('name').replace(/\[\w+]$/, '[' + key + ']' + '"]')); if (scoped_element[0] && scoped_element.val() != options.scope[key]) { data.scope[key] = scoped_element.val(); scoped_element.unbind('change.' + element.id).bind('change.' + element.id, function() { element.trigger('change'); element.trigger('focusout'); }); } else { data.scope[key] = options.scope[key]; } } } // Kind of a hack but this will isolate the resource name and attribute. // e.g. user[records_attributes][0][title] => records[title] // e.g. user[record_attributes][title] => record[title] // Server side handles classifying the resource properly if (/_attributes]/.test(element.attr('name'))) { var name = element.attr('name').match(/\[\w+_attributes]/g).pop().match(/\[(\w+)_attributes]/).pop(); name += /(\[\w+])$/.exec(element.attr('name'))[1]; } else { var name = element.attr('name'); } data[name] = element.val(); if (jQuery.ajax({ url: '/validators/uniqueness.json', data: data, async: false }).status == 200) { return options.message; } } } }, formBuilders: { 'ActionView::Helpers::FormBuilder': { add: function(element, settings, message) { if (element.data('valid') !== false) { var inputErrorField = jQuery(settings.input_tag), labelErrorField = jQuery(settings.label_tag), label = jQuery('label[for="' + element.attr('id') + '"]:not(.message)'); if (element.attr('autofocus')) { element.attr('autofocus', false) }; element.before(inputErrorField); inputErrorField.find('span#input_tag').replaceWith(element); inputErrorField.find('label.message').attr('for', element.attr('id')); labelErrorField.find('label.message').attr('for', element.attr('id')); label.replaceWith(labelErrorField); labelErrorField.find('label#label_tag').replaceWith(label); } jQuery('label.message[for="' + element.attr('id') + '"]').text(message); }, remove: function(element, settings) { var errorFieldClass = jQuery(settings.input_tag).attr('class'), inputErrorField = element.closest('.' + errorFieldClass), label = jQuery('label[for="' + element.attr('id') + '"]:not(.message)'), labelErrorField = label.closest('.' + errorFieldClass); if (inputErrorField[0]) { inputErrorField.find('#' + element.attr('id')).detach(); inputErrorField.replaceWith(element); label.detach(); labelErrorField.replaceWith(label); } } }, 'SimpleForm::FormBuilder': { add: function(element, settings, message) { if (element.data('valid') !== false) { var wrapper = element.closest(settings.wrapper_tag); wrapper.addClass(settings.wrapper_error_class); var errorElement = $('<' + settings.error_tag + ' class="' + settings.error_class + '">' + message + ''); wrapper.append(errorElement); } else { element.parent().find(settings.error_tag + '.' + settings.error_class).text(message); } }, remove: function(element, settings) { var wrapper = element.closest(settings.wrapper_tag + '.' + settings.wrapper_error_class); wrapper.removeClass(settings.wrapper_error_class); var errorElement = wrapper.find(settings.error_tag + '.' + settings.error_class); errorElement.remove(); } }, 'Formtastic::SemanticFormBuilder': { add: function(element, settings, message) { if (element.data('valid') !== false) { var wrapper = element.closest('li'); wrapper.addClass('error'); var errorElement = $('

' + message + '

'); wrapper.append(errorElement); } else { element.parent().find('p.' + settings.inline_error_class).text(message); } }, remove: function(element, settings) { var wrapper = element.closest('li.error'); wrapper.removeClass('error'); var errorElement = wrapper.find('p.' + settings.inline_error_class); errorElement.remove(); } } }, callbacks: { element: { after: function(element, eventData) { }, before: function(element, eventData) { }, fail: function(element, message, addError, eventData) { addError() }, pass: function(element, removeError, eventData) { removeError() } }, form: { after: function(form, eventData) { }, before: function(form, eventData) { }, fail: function(form, eventData) { }, pass: function(form, eventData) { } } } }