/** * 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('
  • ' + errors[inputname][i] + '
  • '); } if (options.submit.settings.clear || options.dynamic.settings.trigger) { if (group && groupinput) { input = groupinput; } var namespace = ".vr", //validation.reseterror event = "coucou" + namespace; if (options.submit.settings.clear) { event += " " + options.submit.settings.clear + namespace } if (options.dynamic.settings.trigger) { event += " " + options.dynamic.settings.trigger + namespace; if (options.dynamic.settings.trigger !== "focusout") { event += " change" + namespace + " paste" + namespace; } } input.unbind(event).on(event, function (a,b,c,d,e) { return function () { if (e) { if ($(c).hasclass(options.submit.settings.errorclass)) { resetoneerror(a,b,c,d,e); } } else if ($(b).hasclass(options.submit.settings.errorclass)) { resetoneerror(a,b,c,d); } }; }(inputname, input, label, errorcontainer, group)) } if (options.submit.settings.scrolltoerror && !window.validation.hasscrolled) { window.validation.hasscrolled = true; var offset = parsefloat(options.submit.settings.scrolltoerror.offset) || 0, duration = parsefloat(options.submit.settings.scrolltoerror.duration) || 500, handle = (options.submit.settings.display === 'block') ? errorcontainer : input; $('html, body').animate({ scrolltop: handle.offset().top + offset }, duration); } } /** * display all of the errors */ function displayerrors () { for (var inputname in errors) { displayoneerror(inputname); } } /** * remove an input error. * * @param {string} inputname key reference to delete the error from "errors" global variable * @param {object} input jquery object of the input * @param {object} label jquery object of the input's label * @param {object} container jquery object of the "errorlist" * @param {string} [group] name of the group if any (ex: used on input radio) */ function resetoneerror(inputname, input, label, container, group) { delete errors[inputname]; if (container) { //window.validation.hasscrolled = false; if (options.submit.settings.inputcontainer) { (group ? label : input).parentsuntil(node, options.submit.settings.inputcontainer).removeclass(options.submit.settings.errorclass) } label && label.removeclass(options.submit.settings.errorclass); input.removeclass(options.submit.settings.errorclass); if (options.submit.settings.display === 'inline') { container.find('[' + _data.errorlist + ']').remove(); } } else { if (!input) { input = $(node).find('[name="' + inputname + '"]'); if (!input[0]) { // {debug} options.debug && window.debug.log({ 'node': node, 'function': 'resetoneerror()', 'arguments': '[name="' + inputname + '"]', 'message': 'error - unable to find input by name "' + inputname + '"' }); // {/debug} return false; } } //$._data( input[0], "events" ); input.trigger('coucou.vr'); } } /** * remove all of the input error(s) display. */ function reseterrors () { errors = []; window.validation.hasscrolled = false; $(node).find('[' + _data.errorlist + ']').remove(); $(node).find('.' + options.submit.settings.errorclass).removeclass(options.submit.settings.errorclass); } /** * submits the form once it succeeded the validation process. * note: this function will be overridden if "options.submit.settings.onsubmit" is defined */ function submitform () { node.submit(); } /** * @private * helper to get the value of an regular, radio or chackbox input * * @param input * * @returns {string} value */ var _getinputvalue = function (input) { var value; // get the value or state of the input based on its type switch ($(input).attr('type')) { case 'checkbox': value = ($(input).is(':checked')) ? 1 : ''; break; case 'radio': value = $(node).find('input[name="' + $(input).attr('name') + '"]:checked').val() || ''; break; default: value = $(input).val(); break; } return value; }; /** * @private * executes an anonymous function or a string reached from the window scope. * * @example * note: these examples works with every callbacks (oninit, onerror, onsubmit, onbeforesubmit & onaftersubmit) * * // an anonymous function inside the "oninit" option * oninit: function() { console.log(':d'); }; * * * // myfunction() located on window.coucou scope * oninit: 'window.coucou.myfunction' * * // myfunction(a,b) located on window.coucou scope passing 2 parameters * oninit: ['window.coucou.myfunction', [':d', ':)']]; * * // anonymous function to execute a local function * oninit: function () { myfunction(':d'); } * * @param {string|array} callback the function to be called * @param {array} [extraparams] in some cases the function can be called with extra parameters (onerror) * * @returns {boolean} */ var _executecallback = function (callback, extraparams) { if (!callback) { return false; } var _callback; if (typeof callback === "function") { _callback = callback; } else if (typeof callback === "string" || callback instanceof array) { _callback = window; if (typeof callback === "string") { callback = [callback, []]; } var _exploded = callback[0].split('.'), _params = callback[1], _isvalid = true, _splitindex = 0; while (_splitindex < _exploded.length) { if (typeof _callback !== 'undefined') { _callback = _callback[_exploded[_splitindex++]]; } else { _isvalid = false; break; } } if (!_isvalid || typeof _callback !== "function") { // {debug} options.debug && window.debug.log({ 'node': node, 'function': '_executecallback()', 'arguments': json.stringify(callback), 'message': 'warning - invalid callback function"' }); // {/debug} return false; } } _callback.apply(this, $.merge(_params || [], (extraparams) ? extraparams : [])); return true; }; /** * @private * constructs validation */ this.__construct = function () { extendmessages(); extendoptions(); delegatedynamicvalidation(); delegatevalidation(); // {debug} options.debug && window.debug.print(); // {/debug} }(); return { /** * @public * register error * * @param inputname * @param error */ registererror: registererror, /** * @public * display one error * * @param inputname */ displayoneerror: displayoneerror, /** * @public * display all errors */ displayerrors: displayerrors, /** * @public * remove one error */ resetoneerror: resetoneerror, /** * @public * remove all errors */ reseterrors: reseterrors }; }; // ================================================================================================================= /** * @public * jquery public function to implement the validation on the selected node(s). * * @param {object} options to configure the validation class. * * @return {object} modified dom element */ $.fn.validate = $.validate = function (options) { return _api.validate(this, options); }; /** * @public * jquery public function to add one or multiple "data-validation" argument. * * @param {string|array} validation arguments to add in the node's data-validation * * @return {object} modified dom element */ $.fn.addvalidation = function (validation) { return _api.addvalidation(this, validation); }; /** * @public * jquery public function to remove one or multiple "data-validation" argument. * * @param {string|array} validation arguments to remove in the node's data-validation * * @return {object} modified dom element */ $.fn.removevalidation = function (validation) { return _api.removevalidation(this, validation); }; /** * @public * jquery public function to add one or multiple errors. * * @param {object} error object of errors where the keys are the input names * @example * $('form#myform').adderror({ * 'username': 'invalid username, please choose another one.' * }); * * @return {object} modified dom element */ $.fn.adderror = function (error) { return _api.adderror(this, error); }; /** * @public * jquery public function to remove one or multiple errors. * * @param {array} error array of errors where the keys are the input names * @example * $('form#myform').removeerror([ * 'username' * ]); * * @return {object} modified dom element */ $.fn.removeerror = function (error) { return _api.removeerror(this, error); }; // ================================================================================================================= /** * @private * api to handles "addvalidation" and "removevalidation" on attribute "data-validation". * note: contains fail-safe operations to unify the validation parameter. * * @example * $.addvalidation('notempty, l>=6') * $.addvalidation('[notempty, v>=6]') * $.removevalidation(['optional', 'v>=6']) * * @returns {object} updated dom object */ var _api = { /** * @private * this function unifies the data through the validation process. * string, uppercase and spaceless. * * @param {string|array} validation * * @returns {string} */ _formatvalidation: function (validation) { validation = validation.tostring().replace(/\s/g, ''); if (validation.charat(0) === "[" && validation.charat(validation.length - 1) === "]") { validation = validation.replace(/^\[|\]$/g, ''); } return validation; }, /** * @private * splits the validation into an array, uppercase the rules if they are not comparisons * * @param {string|array} validation * * @returns {array} formatted validation keys */ _splitvalidation: function (validation) { var validationarray = this._formatvalidation(validation).split(','), onevalidation; for (var i = 0; i < validationarray.length; i++) { onevalidation = validationarray[i]; if (/^[a-z]+$/i.test(onevalidation)) { validationarray[i] = onevalidation.touppercase(); } } return validationarray; }, /** * @private * joins the validation array to create the "data-validation" value * * @param {array} validation * * @returns {string} */ _joinvalidation: function (validation) { return '[' + validation.join(', ') + ']'; }, /** * api method to attach the submit event type on the specified node. * note: clears the previous event regardless to avoid double submits or unwanted behaviors. * * @param {object} node jquery object(s) * @param {object} options to configure the validation class. * * @returns {*} */ validate: function (node, options) { if (typeof node === "function") { if (!options.submit.settings.form) { // {debug} window.debug.log({ 'node': node, 'function': '$.validate()', 'arguments': '', 'message': 'undefined property "options.submit.settings.form - validation dropped' }); window.debug.print(); // {/debug} return; } node = $(options.submit.settings.form); if (!node[0]) { // {debug} window.debug.log({ 'node': node, 'function': '$.validate()', 'arguments': json.stringify(options.submit.settings.form), 'message': 'unable to find jquery form element - validation dropped' }); window.debug.print(); // {/debug} return; } } else if (typeof node[0] === 'undefined') { // {debug} window.debug.log({ 'node': node, 'function': '$.validate()', 'arguments': '$("' + node['selector'] + '").validate()', 'message': 'unable to find jquery form element - validation dropped' }); window.debug.print(); // {/debug} return; } return node.each(function () { window.validation.form[node.selector] = new validation(this, options); }); }, /** * api method to handle the addition of "data-validation" arguments. * note: only the predefined validation arguments are allowed to be added * inside the "data-validation" attribute (see configuration). * * @param {object} node jquery objects * @param {string|array} validation arguments to add in the node(s) "data-validation" * * @returns {*} */ addvalidation: function (node, validation) { var self = this; validation = self._splitvalidation(validation); if (!validation) { return false; } return node.each( function () { var $this = $(this), validationdata = $this.attr(_data.validation), validationarray = (validationdata && validationdata.length) ? self._splitvalidation(validationdata) : [], onevalidation; for (var i = 0; i < validation.length; i++) { onevalidation = self._formatvalidation(validation[i]); if ($.inarray(onevalidation, validationarray) === -1) { validationarray.push(onevalidation); } } if (validationarray.length) { $this.attr(_data.validation, self._joinvalidation(validationarray)); } }); }, /** * api method to handle the removal of "data-validation" arguments. * * @param {object} node jquery objects * @param {string|array} validation arguments to remove in the node(s) "data-validation" * * @returns {*} */ removevalidation: function (node, validation) { var self = this; validation = self._splitvalidation(validation); if (!validation) { return false; } return node.each( function () { var $this = $(this), validationdata = $this.attr(_data.validation), validationarray = (validationdata && validationdata.length) ? self._splitvalidation(validationdata) : [], onevalidation, validationindex; if (!validationarray.length) { $this.removeattr(_data.validation); return true; } for (var i = 0; i < validation.length; i++) { onevalidation = self._formatvalidation(validation[i]); validationindex = $.inarray(onevalidation, validationarray); if (validationindex !== -1) { validationarray.splice(validationindex, 1); } } if (!validationarray.length) { $this.removeattr(_data.validation); return true; } $this.attr(_data.validation, self._joinvalidation(validationarray)); }); }, /** * api method to manually trigger a form error. * note: the same form jquery selector must be used to recuperate the validation configuration. * * @example * $('#form-signup_v3').adderror({ * 'inputname': 'my error message', * 'inputname2': [ * 'first error message', * 'second error message' * ] * }) * * @param {object} node jquery object * @param {object} error object of errors to add on the node * * @returns {*} */ adderror: function (node, error) { if (!window.validation.form[node.selector]) { // {debug} window.debug.log({ 'node': node, 'function': '$.adderror()', 'arguments': 'window.validation.form[' + json.stringify(node.selector) + ']', 'message': 'error - invalid node selector' }); window.debug.print(); // {/debug} return false; } if (typeof error !== "object" || object.prototype.tostring.call(error) !== "[object object]") { // {debug} window.debug.log({ 'node': node, 'function': '$.adderror()', 'arguments': 'window.validation.form[' + json.stringify(node.selector) + ']', 'message': 'error - invalid argument, must be type object' }); window.debug.print(); // {/debug} return false; } var input, onlyonce = true; for (var inputname in error) { if (!error.hasownproperty(inputname)) { continue; } if (!(error[inputname] instanceof array)) { error[inputname] = [error[inputname]]; } input = $(node.selector).find('[name="'+ inputname + '"]'); if (!input[0]) { // {debug} window.debug.log({ 'node': node, 'function': '$.adderror()', 'arguments': json.stringify(inputname), 'message': 'error - unable to find ' + '$(' + node.selector + ').find("[name="'+ inputname + '"]")' }); window.debug.print(); // {/debug} continue; } if (onlyonce) { window.validation.hasscrolled = false; onlyonce = false; } window.validation.form[node.selector].resetoneerror(inputname, input); for (var i = 0; i < error[inputname].length; i++) { if (typeof error[inputname][i] !== "string") { // {debug} window.debug.log({ 'node': node, 'function': '$.adderror()', 'arguments': json.stringify(error[inputname][i]), 'message': 'error - invalid error object property - accepted format: {"inputname": "errorstring"} or {"inputname": ["errorstring", "errorstring"]}' }); window.debug.print(); // {/debug} continue; } window.validation.form[node.selector].registererror(inputname, error[inputname][i]); } window.validation.form[node.selector].displayoneerror(inputname); } }, /** * api method to manually remove a form error. * note: the same form jquery selector must be used to recuperate the validation configuration. * * @example * $('#form-signin_v2').removeerror([ * 'signin_v2[username]', * 'signin_v2[password]' * ]) * * @param {object} node jquery object * @param {object} inputname object of errors to remove on the node * * @returns {*} */ removeerror: function (node, inputname) { if (!window.validation.form[node.selector]) { // {debug} window.debug.log({ 'node': node, 'function': '$.removeerror()', 'arguments': 'window.validation.form[' + json.stringify(node.selector) + ']', 'message': 'error - invalid node selector' }); window.debug.print(); // {/debug} return false; } if (!inputname) { window.validation.form[node.selector].reseterrors(); return false; } if (typeof inputname === "object" && object.prototype.tostring.call(inputname) !== "[object array]") { // {debug} window.debug.log({ 'node': node, 'function': '$.removeerror()', 'arguments': json.stringify(inputname), 'message': 'error - invalid inputname, must be type string or array' }); window.debug.print(); // {/debug} return false; } if (!(inputname instanceof array)) { inputname = [inputname]; } var input; for (var i = 0; i < inputname.length; i++) { input = $(node.selector).find('[name="'+ inputname[i] + '"]'); if (!input[0]) { // {debug} window.debug.log({ 'node': node, 'function': '$.removeerror()', 'arguments': json.stringify(inputname[i]), 'message': 'error - unable to find ' + '$(' + node.selector + ').find("[name="'+ inputname[i] + '"]")' }); window.debug.print(); // {/debug} continue; } window.validation.form[node.selector].resetoneerror(inputname[i], input); } } }; // {debug} window.debug = { table: {}, log: function (debugobject) { if (!debugobject.message || typeof debugobject.message !== "string") { return false; } this.table[debugobject.message] = $.extend( object.preventextensions( { 'node': '', 'function': '', 'arguments': '' } ), debugobject ) }, print: function () { if ($.isemptyobject(this.table)) { return false; } if (console.group !== undefined || console.table !== undefined) { console.groupcollapsed('--- jquery form validation debug ---'); if (console.table) { console.table(this.table); } else { $.each(this.table, function (index, data) { console.log(data['name'] + ': ' + data['execution time']+'ms'); }); } console.groupend(); } else { console.log('debug is not available on your current browser, try the most recent version of chrome or firefox.'); } this.table = {}; } }; // {/debug} string.prototype.capitalize = function() { return this.charat(0).touppercase() + this.slice(1); }; /** * creates a string from a json object * * @return {string|array} str string or array of strings */ window.json.stringify = json.stringify || function (obj) { var t = typeof (obj); if (t !== "object" || obj === null) { // simple data type if (t === "string") { obj = '"' + obj + '"'; } return string(obj); } else { // recurse array or object var n, v, json = [], arr = (obj && obj.constructor === array); for (n in obj) { // jslint hack to validate for..in if (true) { v = obj[n]; t = typeof(v); if (t === "string") { v = '"' + v + '"'; } else if (t === "object" && v !== null) { v = json.stringify(v); } json.push((arr ? "" : '"' + n + '": ') + string(v)); } } return (arr ? "[" : "{") + string(json) + (arr ? "]" : "}"); } }; }(window, document, window.jquery));