/** * jquery form validation * * @author tom bertrand * @version 1.3.5 (2014-09-10) * * @copyright * copyright (c) 2014 runningcoder. * * @link * http://www.runningcoder.org/jqueryvalidation/ * * @license * licensed under the mit license. * * @note * remove debug code: //\s?\{debug\}[\s\s]*?\{/debug\} */ (function (window, document, $, undefined) { window.validation = { form: [], messages: null, labels: {}, hasscrolled: false }; /** * fail-safe preventextensions function for older browsers */ if (typeof object.preventextensions !== "function") { object.preventextensions = function (obj) { return obj; } } // not using strict to avoid throwing a window error on bad config extend. // console.debug is used instead to debug validation //"use strict"; // ================================================================================================================= /** * @private * regexp rules */ var _rules = { // validate not empty notempty: /./, // validate a numeric numeric: /^[0-9]+$/, // validate an alphanumeric string (no special chars) mixed: /^[\w\s-]+$/, // validate a spaceless string nospace: /^[^\s]+$/, // validate a spaceless string at start or end trim: /^[^\s].*[^\s]$/, // validate a date yyyy-mm-dd date: /^\d{4}-\d{2}-\d{2}(\s\d{2}:\d{2}(:\d{2})?)?$/, // validate an email email: /^([^@]+?)@(([a-z0-9]-*)*[a-z0-9]+\.)+([a-z0-9]+)$/i, // validate an url url: /^(https?:\/\/)?((([a-z0-9]-*)*[a-z0-9]+\.?)*([a-z0-9]+))(\/[\w?=\.-]*)*$/, // validate a north american phone number phone: /^(\()?\d{3}(\))?(-|\s)?\d{3}(-|\s)\d{4}$/, // validate value if it is not empty optional: /^.*$/, // validate values or length by comparison comparison: /^\s*([lv])\s*([<>]=?|==|!=)\s*([^<>=!]+?)\s*$/ }; /** * @private * error messages */ var _messages = object.preventextensions({ 'default': '$ contain error(s).', 'notempty': '$ must not be empty.', 'numeric': '$ must be numeric.', 'string': '$ must be a string.', 'nospace': '$ must not contain spaces.', 'trim': '$ must not start or end with space character.', 'mixed': '$ must be letters or numbers (no special characters).', 'date': '$ is not a valid with format yyyy-mm-dd.', 'email': '$ is not valid.', 'url': '$ is not valid.', 'phone': '$ is not a valid phone number.', //'inarray': '$ is not a valid option.', '<': '$ must be less than % characters.', '<=': '$ must be less or equal to % characters.', '>': '$ must be greater than % characters.', '>=': '$ must be greater or equal to % characters.', '==': '$ must be equal to %', '!=': '$ must be different than %' }), _extendedmessages = false; /** * @private * html5 data attributes */ var _data = { validation: 'data-validation', validationmessage: 'data-validation-message', regex: 'data-validation-regex', regexmessage: 'data-validation-regex-message', group: 'data-validation-group', label: 'data-validation-label', errorlist: 'data-error-list' }; /** * @private * default options * * @link http://www.runningcoder.org/jqueryvalidation/documentation/ */ var _options = { submit: { settings: { form: null, display: "inline", insertion: "append", allerrors: false, trigger: "click", button: "input[type='submit']", errorclass: "error", errorlistclass: "error-list", inputcontainer: null, clear: "focusin", scrolltoerror: false }, callback: { oninit: null, onvalidate: null, onerror: null, onbeforesubmit: null, onsubmit: null, onaftersubmit: null } }, dynamic: { settings: { trigger: null, delay: 300 }, callback: { onsuccess: null, onerror: null, oncomplete: null } }, messages: {}, labels: {}, debug: false }; /** * @private * limit the supported options on matching keys */ var _supported = { submit: { settings: { display: ["inline", "block"], insertion: ["append", "prepend"], //"before", "insertbefore", "after", "insertafter" allerrors: [true, false], clear: ["focusin", "keypress", false], trigger: [ "click", "dblclick", "focusout", "hover", "mousedown", "mouseenter", "mouseleave", "mousemove", "mouseout", "mouseover", "mouseup", "toggle" ] } }, dynamic: { settings: { trigger: ["focusout", "keydown", "keypress", "keyup"] } }, debug: [true, false] }; // ================================================================================================================= /** * @constructor * validation class * * @param {object} node jquery form object * @param {object} options user defined options */ var validation = function (node, options) { var errors = []; window.validation.hasscrolled = false; /** * extends user-defined "message" into the default validation "_message". * notes: * - preventextensions prevents from modifying the validation "_message" object structure */ function extendmessages () { if (!window.validation.messages || _extendedmessages) { return false; } _messages = $.extend(_messages, window.validation.messages); _extendedmessages = true; } /** * extends user-defined "options" into the default validation "_options". * notes: * - preventextensions prevents from modifying the validation "_options" object structure * - filter through the "_supported" to delete unsupported "options" */ function extendoptions () { if (!(options instanceof object)) { options = {}; } var tpmoptions = object.preventextensions($.extend(true, {}, _options)), tmpmessages = object.preventextensions($.extend(true, {}, _messages)); tpmoptions.messages = $.extend(tmpmessages, options.messages || {}); for (var method in options) { if (!options.hasownproperty(method) || method === "debug" || method === "messages") { continue; } if (method === "labels" && options[method] instanceof object) { tpmoptions[method] = options[method]; continue; } if (!_options[method] || !(options[method] instanceof object)) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'extendoptions()', 'arguments': '{' + method + ': ' + json.stringify(options[method]) + '}', 'message': 'warning - ' + method + ' - invalid option' }); // {/debug} continue; } for (var type in options[method]) { if (!options[method].hasownproperty(type)) { continue; } if (!_options[method][type] || !(options[method][type] instanceof object)) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'extendoptions()', 'arguments': '{' + type + ': ' + json.stringify(options[method][type]) + '}', 'message': 'warning - ' + type + ' - invalid option' }); // {/debug} continue; } for (var option in options[method][type]) { if (!options[method][type].hasownproperty(option)) { continue; } if (_supported[method] && _supported[method][type] && _supported[method][type][option] && $.inarray(options[method][type][option], _supported[method][type][option]) === -1) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'extendoptions()', 'arguments': '{' + option + ': ' + json.stringify(options[method][type][option]) + '}', 'message': 'warning - ' + option.tostring() + ': ' + json.stringify(options[method][type][option]) + ' - unsupported option' }); // {/debug} delete options[method][type][option]; } } if (tpmoptions[method] && tpmoptions[method][type]) { tpmoptions[method][type] = $.extend(object.preventextensions(tpmoptions[method][type]), options[method][type]); } } } // {debug} if (options.debug && $.inarray(options.debug, _supported['debug'] !== -1)) { tpmoptions.debug = options.debug; } // {/debug} // @todo would there be a better fix to solve event conflict? if (tpmoptions.dynamic.settings.trigger) { if (tpmoptions.dynamic.settings.trigger === "keypress" && tpmoptions.submit.settings.clear === "keypress") { tpmoptions.dynamic.settings.trigger = "keydown"; } } options = tpmoptions; } /** * delegates the dynamic validation on data-validation and data-validation-regex attributes based on trigger. * * @returns {boolean} false if the option is not set */ function delegatedynamicvalidation() { if (!options.dynamic.settings.trigger) { return false; } // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'delegatedynamicvalidation()', 'arguments': json.stringify(options), 'message': 'ok - dynamic validation activated on ' + $(node).length + ' form(s)' }); // {/debug} if ( !$(node).find('[' + _data.validation + '],[' + _data.regex + ']')[0]) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'delegatedynamicvalidation()', 'arguments': '$(node).find([' + _data.validation + '],[' + _data.regex + '])', 'message': 'error - [' + _data.validation + '] not found' }); // {/debug} return false; } var namespace = ".vd", // validation.delegate event = options.dynamic.settings.trigger + namespace; if (options.dynamic.settings.trigger !== "focusout") { event += " change" + namespace + " paste" + namespace; } $.each( $(node).find('[' + _data.validation + '],[' + _data.regex + ']'), function (index, input) { $(input).unbind(event).on(event, function (e) { if ($(this).is(':disabled')) { return false; } //e.preventdefault(); var input = this, keycode = e.keycode || null; _typewatch(function () { if (!validateinput(input)) { displayoneerror(input.name); _executecallback(options.dynamic.callback.onerror, [node, input, keycode, errors[input.name]]); } else { _executecallback(options.dynamic.callback.onsuccess, [node, input, keycode]); } _executecallback(options.dynamic.callback.oncomplete, [node, input, keycode]); }, options.dynamic.settings.delay); }); } ) } var _typewatch = (function(){ var timer = 0; return function(callback, ms){ cleartimeout (timer); timer = settimeout(callback, ms); } })(); /** * delegates the submit validation on data-validation and data-validation-regex attributes based on trigger. * note: disable the form submit function so the callbacks are not by-passed */ function delegatevalidation () { _executecallback(options.submit.callback.oninit, [node]); var event = options.submit.settings.trigger + '.vd'; // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'delegatevalidation()', 'arguments': json.stringify(options), 'message': 'ok - validation activated on ' + $(node).length + ' form(s)' }); // {/debug} if (!$(node).find(options.submit.settings.button)[0]) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'delegatedynamicvalidation()', 'arguments': '$(node).find(' + options.submit.settings.button + ')', 'message': 'error - ' + options.submit.settings.button + ' not found' }); // {/debug} return false; } $(node).on("submit", false ); $(node).find(options.submit.settings.button).unbind(event).on(event, function (e) { e.preventdefault(); reseterrors(); _executecallback(options.submit.callback.onvalidate, [node]); if (!validateform()) { // onerror function receives the "errors" object as the last "extraparam" _executecallback(options.submit.callback.onerror, [node, errors]); displayerrors(); } else { _executecallback(options.submit.callback.onbeforesubmit, [node]); (options.submit.callback.onsubmit) ? _executecallback(options.submit.callback.onsubmit, [node]) : submitform(); _executecallback(options.submit.callback.onaftersubmit, [node]); } // {debug} options.debug && window.debug.print(); // {/debug} return false; }); } /** * for every "data-validation" & "data-pattern" attributes that are not disabled inside the jquery "node" object * the "validateinput" function will be called. * * @returns {boolean} true if no error(s) were found (valid form) */ function validateform () { var isvalid = true; $.each( $(node).find('[' + _data.validation + '],[' + _data.regex + ']'), function (index, input) { if ($(this).is(':disabled')) { return false; } if (!validateinput(input)) { isvalid = false; } } ); return isvalid; } /** * prepare the information from the data attributes * and call the "validaterule" function. * * @param {object} input reference of the input element * * @returns {boolean} true if no error(s) were found (valid input) */ function validateinput (input) { var inputname = $(input).attr('name'); if (!inputname) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'validateinput()', 'arguments': '$(input).attr("name")', 'message': 'error - missing input [name]' }); // {/debug} return false; } var value = _getinputvalue(input), matches = inputname.replace(/]$/, '').split(/]\[|[[\]]/g), inputshortname = window.validation.labels[inputname] || options.labels[inputname] || $(input).attr(_data.label) || matches[matches.length - 1], validationarray = $(input).attr(_data.validation), validationmessage = $(input).attr(_data.validationmessage), validationregex = $(input).attr(_data.regex), validationregexmessage = $(input).attr(_data.regexmessage), validateonce = false; if (validationarray) { validationarray = _api._splitvalidation(validationarray); } // validates the "data-validation" if (validationarray instanceof array && validationarray.length > 0) { // "optional" input will not be validated if it's empty if (value === '' && $.inarray('optional', validationarray) !== -1) { return true; } $.each(validationarray, function (i, rule) { if (validateonce === true) { return true; } try { validaterule(value, rule); } catch (error) { if (validationmessage || !options.submit.settings.allerrors) { validateonce = true; } error[0] = validationmessage || error[0]; registererror(inputname, error[0].replace('$', inputshortname).replace('%', error[1])); } }); } // validates the "data-validation-regex" if (validationregex) { var pattern = validationregex.split('/'); if (pattern.length > 1) { var tmppattern = ""; // do not loop through the last item knowing its a potential modifier for (var k = 0; k < pattern.length - 1; k++) { if (pattern[k] !== "") { tmppattern += pattern[k] + '/'; } } // remove last added "/" tmppattern = tmppattern.slice(0, -1); // test the last item for modifier(s) if (/[gimsxeu]+/.test(pattern[pattern.length - 1])) { var patternmodifier = pattern[pattern.length - 1]; } pattern = tmppattern; } else { pattern = pattern[0]; } // validate the regex try { var rule = new regexp(pattern, patternmodifier); } catch (error) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'validateinput()', 'arguments': '{pattern: {' + pattern + '}, modifier: {' + patternmodifier+ '}', 'message': 'warning - invalid [data-validation-regex] on input ' + inputname }); // {/debug} // do not block validation if a regexp is bad, only skip it return true; } try { validaterule(value, rule); } catch (error) { error[0] = validationregexmessage || error[0]; registererror(inputname, error[0].replace('$', inputshortname)); } } return !errors[inputname] || errors[inputname] instanceof array && errors[inputname].length === 0; } /** * validate an input value against one rule. * if a "value-rule" mismatch occurs, an error is thrown to the caller function. * * @param {string} value * @param rule * * @returns {*} error if a mismatch occurred. */ function validaterule (value, rule) { // validate for custom "data-validation-regex" if (rule instanceof regexp) { if (rule.test(value)) { throw [options.messages['default'], '']; } return; } // validate for predefined "data-validation" _rules if (_rules[rule]) { if (!_rules[rule].test(value)) { throw [options.messages[rule], '']; } return; } // validate for comparison "data-validation" var comparison = rule.match(_rules['comparison']); if (!comparison || comparison.length !== 4) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'validaterule()', 'arguments': 'value: ' + value + ' rule: ' + rule, 'message': 'warning - invalid comparison' }); // {/debug} return; } var type = comparison[1], operator = comparison[2], compared = comparison[3], comparedvalue; switch (type) { // compare input "length" case "l": // only numeric value for "l" are allowed if (isnan(compared)) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'validaterule()', 'arguments': 'compare: ' + compared + ' rule: ' + rule, 'message': 'warning - invalid rule, "l" compare must be numeric' }); // {/debug} return false; } else { if (!value || eval(value.length + operator + parsefloat(compared)) == false) { throw [options.messages[operator], compared]; } } break; // compare input "value" case "v": default: // compare field values if (isnan(compared)) { comparedvalue = $(node).find('[name="' + compared + '"]').val(); if (!comparedvalue) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'validaterule()', 'arguments': 'compare: ' + compared + ' rule: ' + rule, 'message': 'warning - unable to find compared field [name="' + compared + '"]' }); // {/debug} return false; } if (!value || !eval('"' + encodeuricomponent(value) + '"' + operator + '"' + encodeuricomponent(comparedvalue) + '"')) { throw [options.messages[operator].replace(' characters', ''), compared]; } // compare numeric value } else { if (!value || isnan(value) || !eval(value + operator + parsefloat(compared))) { throw [options.messages[operator].replace(' characters', ''), compared]; } } break; } } /** * register an error into the global "error" variable. * * @param {string} inputname input where the error occurred * @param {string} error description of the error to be displayed */ function registererror (inputname, error) { if (!errors[inputname]) { errors[inputname] = []; } error = error.capitalize(); var haserror = false; for (var i = 0; i < errors[inputname].length; i++) { if (errors[inputname][i] === error) { haserror = true; break; } } if (!haserror) { errors[inputname].push(error); } } /** * display a single error based on "inputname" key inside the "errors" global array. * the input, the label and the "inputcontainer" will be given the "errorclass" and other * settings will be considered. * * @param {string} inputname key used for search into "errors" * * @returns {boolean} false if an unwanted behavior occurs */ function displayoneerror (inputname) { var input, inputid, errorcontainer, label, html = '
', group, groupinput; if (!errors.hasownproperty(inputname)) { return false; } input = $(node).find('[name="' + inputname + '"]'); label = null; if (!input[0]) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'displayoneerror()', 'arguments': '[name="' + inputname + '"]', 'message': 'error - unable to find input by name "' + inputname + '"' }); // {/debug} return false; } group = input.attr(_data.group); if (group) { groupinput = $(node).find('[name="' + inputname + '"]'); label = $(node).find('[id="' + group + '"]'); if (label[0]) { label.addclass(options.submit.settings.errorclass); errorcontainer = label; } //$(node).find('[' + _data.group + '="' + group + '"]').addclass(options.submit.settings.errorclass) } else { input.addclass(options.submit.settings.errorclass); if (options.submit.settings.inputcontainer) { input.parentsuntil(node, options.submit.settings.inputcontainer).addclass(options.submit.settings.errorclass) } inputid = input.attr('id'); if (inputid) { label = $(node).find('label[for="' + inputid + '"]')[0]; } if (!label) { label = input.parentsuntil(node, 'label')[0]; } if (label) { label = $(label); label.addclass(options.submit.settings.errorclass); } } if (options.submit.settings.display === 'inline') { errorcontainer = errorcontainer || input.parent(); } else if (options.submit.settings.display === 'block') { errorcontainer = $(node); } // prevent double error list if the previous one has not been cleared. if (options.submit.settings.display === 'inline' && errorcontainer.find('[' + _data.errorlist + ']')[0]) { return false; } if (options.submit.settings.display === "inline" || (options.submit.settings.display === "block" && !errorcontainer.find('[' + _data.errorlist + ']')[0]) ) { if (options.submit.settings.insertion === 'append') { errorcontainer.append(html); } else if (options.submit.settings.insertion === 'prepend') { errorcontainer.prepend(html); } } for (var i = 0; i < errors[inputname].length; i++) { errorcontainer.find('ul').append('