/* autoValidate() jQuery plugin This plugin binds handlers to the keyup and blur events of the elements upon which it is applied. When these events detect a change in the 'value' of the element (value attribute or another data-* attribute can be used) it flags the element as invalid, applies a special class to the element and possibly a parent of the element, and sets the title text of the element to some error text. Much of this behavoir is customizable. You can use built in validation tests or supply your own, run your own functions after testing, control what classes are applied, control whether the a parent element (direct parent or one found by a jquery selector) is styled as well, turn automatic validation on/off, trigger validation manually, and customize what data is used for the test. Apply to jquery elements like so (second arg overrides defaults): $elements.autoValidate('init', {optionName:value,...}); Call other methods the same way: $elements.autoValidate('trigger'); $elements.autoValidate('changeTitles','foobar') .autoValidate('off'); var myvalue = $element.autoValidate('getValue'); The current values of all options can be accessed with 'getOption'. If multiple elements are used, only the first is used: var myClass = $element.autoValidate('getOption','invalidClass'); Options can be changed for an element or set of elements using the 'setOptions' method. Pass the options in an object: $elements.autoValidate('setOptions',{'type':'integer'}) .autoValidate('trigger'); The following options are flags (boolean), defaults in (): validateOnBind (false) trigger autovalidation upon initialization styleParentOfInvalid (true) apply invalidParentClass to parent element if invalid setTitleOfParent (true) also change the title of the parent element valid (true) set by validation routine, false if test is failed active (true) validate when bound events are triggered The following options are also available: userTestFunction (function) custom function to test the 'value' of the element specificParentSelector ('') jquery selector used to find a specific parent element valueAttribute ('value') what attribute to test, if not 'value', it is assumed that the attribute exists in data-* attribute. invalidClass ('avInvalid') name of class to apply to invalid element invalidParentClass ('avInvalidParent') class to apply to parent of invalid element followUpOptions ({'errorText':''}) options passed to followUp function substituteFollowUp (function) user specified followUp function testFunction (function) set internally, the function used to test the 'value' The following methods are available: init(options{}) apply plugin to elements trigger(substituteFollowUp) manually trigger validation (don't wait for events), then do substituteFollowUp validate(substituteFollowUp) called by 'trigger' and event handlers, 'trigger' should be used by users getValue() returns value that is tested for element reset() clears styling, etc., resets options to values at 'init' off(bool) turn off trigger on events (bool=true) on(bool) turn on trigger on events (bool=true) getParent() return the parent that will be styled changeTitles('text') set 'title' attr for elements and possibly parents styleValid() style element (and maybe parent) as though validation passed styleInvalid() style element (and maybe parent) as though validation failed followUp() this is the default followUp function setErrorText('text') change the error text that will appear in the element's 'title' attribute if invalid */ // plugin definition // function is not assigned to a variable, therefore it is executed // immediately notice that the jQuery object is passed as $ (function( $ ){ // main function call passes to other methods in namespace $.fn.autoValidate = function(method, args ) { if (typeof(methods[method]) != 'undefined') { return methods[method].apply( this, [args]); } else{ return this; } }; // these are the properties that will be stored for each element var defaults = { 'type' : 'basicText', 'userTestFunction' : function(testValue){return true;}, 'validateOnBind' : false, 'styleParentOfInvalid' : true, 'setTitleOfParent' : true, 'specificParentSelector': '', 'valueAttribute' : 'value', 'invalidClass' : 'avInvalid', 'invalidParentClass' : 'avInvalidParent', 'followUpOptions' : { 'errorText':''}, 'substituteFollowUp' : null, 'valid' : true, 'active' : true, 'oldValue' : null, 'testFunction' : function (testValue){return true;} } // define methods /// var avInit = function( userOptions ) { // returns jQuery chain var elementOptions = $.extend({},defaults,userOptions); var testType = elementOptions['type']; var userTestFunction = elementOptions['userTestFunction']; var testFunction = getTest(testType, userTestFunction); elementOptions['testFunction'] = testFunction; var initOptions = $.extend({},elementOptions); return this.each( function(){ var $element = $(this); setData($element,'options',elementOptions); // an entire copy of the options object is stored in the data // namespace of the plugin so we can use it to reset the element later setData($element,'initOptions',initOptions); // perform any initial validation if (elementOptions['avValidateOnBind']){ // validate element $element.autoValidate('trigger'); } // clear any previous formatting $element.autoValidate('styleValid'); $element.autoValidate('changeTitles',''); attachHandlers($element); }); } var avGetOption = function(optionName){ // breaks jquery chain // this is a wrapper for getData, to get an option's value from the // plugin's data store var $element = this; var allOptions = {}; var option = null; allOptions = getData($element,'options'); if (typeof(allOptions[optionName]) != 'undefined'){ option = allOptions[optionName]; } return option; } var avSetOptions = function(options){ // returns jQuery chain // this is a wrapper for setData, so options can be altered after init return this.each( function(){ var $element = $(this); var allOptions = {}; allOptions = getData($element,'options'); if (typeof(options) === 'object'){ var newOptions = $.extend(allOptions,options); setData($element,'options',newOptions); } }); } var avTrigger = function(substituteFollowUp){ // returns jQuery chain return this.each( function(){ var $element = $(this); $element.autoValidate('validate', substituteFollowUp); }); } var avValidate = function(substituteFollowUp){ // returns jQuery chain return this.each( function(){ var $element = $(this); var testValue = $element.autoValidate('getValue'); var testResult = $element.autoValidate('getOption','testFunction').apply(this, [testValue]); // should return bool $element.autoValidate('setOptions', {'valid': testResult}); // store test result // a substitute followup can be specified at call time, or store in the element if (typeof(substituteFollowUp) == 'function'){ substituteFollowUp(this); } else if (typeof($element.autoValidate('getOption','substituteFollowUp')) == 'function') { $element.autoValidate('getOption','substituteFollowUp')(this); } else{ // no substitute, do default $element.autoValidate('followUp'); } }); } var avGetValue = function(){ // breaks jQuery chain var $element = this; var testValue = null; var valueAttribute = $element.autoValidate('getOption','valueAttribute'); if (valueAttribute == 'value'){ // for 'value' check if we can use .val(), which is the ideal way if ($element.filter('select,option,input,button,param,textarea').length){ // this is an element that has the standard value attribute testValue = $element.val(); } else{ // this element doesn't have a standard value attribute // assume this is set in data-myvalue testValue = $element.data(valueAttribute); } } else{ // any other 'attribute' we're assuming is set in data-* attribute testValue = $element.data(valueAttribute); } return testValue; } var avReset = function(){ // returns jquery chain return this.each( function(){ var $element = $(this); var initOptions = getData($element,'initOptions'); setData($element,'options',initOptions); $element.autoValidate('styleValid'); $element.autoValidate('changeTitles',''); }); } var avOff = function(state){ // returns jquery chain return this.each( function(){ var $element = $(this); if (typeof(state) != 'undefined'){ state = Boolean(state); } else { state = true; } state = !state; $element.autoValidate('setOptions', {'active': state}); }); } var avOn = function(state){ return this.each( function(){ var $element = $(this); if (typeof(state) != 'undefined'){ state = Boolean(state); } else { state = true; } $element.autoValidate('setOptions', {'active': state}); }); } var avGetParent = function(){ // returns jquery chain // this returns a set of elements matching the parent of each jquery // object in the set, or a set of elements representing the first // parent of each that matches the selector var $allParents = $(); this.each( function(){ var $element = $(this); var $parentElement = $element.parent(); var parentSelector = $element.autoValidate('getOption','specificParentSelector'); if (parentSelector != '' && parentSelector !== null){ $parentElement = $element.closest(parentSelector).not($element); } $allParents = $allParents.add($parentElement); }); return $allParents; } var avChangeTitles = function(titleText){ // returns jquery chain return this.each( function(){ var $element = $(this); var $parentElement = ''; var setTitleOfParent = $element.autoValidate('getOption','setTitleOfParent'); if (setTitleOfParent){ $parentElement = $element.autoValidate('getParent'); } var $elements = $element.add($parentElement); if (titleText == ""){ $elements.prop('title', ''); } else{ $elements.prop('title', titleText); } }); } var avStyleValid = function(){ // returns jquery chain return this.each( function(){ var $element = $(this); var styleParentOfInvalid = $element.autoValidate('getOption','styleParentOfInvalid'); if (styleParentOfInvalid){ var $parentElement = $element.autoValidate('getParent'); var invalidParentClass = $element.autoValidate('getOption','invalidParentClass'); $parentElement.removeClass(invalidParentClass); } var invalidClass = $element.autoValidate('getOption','invalidClass'); $element.removeClass(invalidClass); }); } var avStyleInvalid = function(){ // returns jquery chain return this.each( function(){ var $element = $(this); var styleParentOfInvalid = $element.autoValidate('getOption','styleParentOfInvalid'); if (styleParentOfInvalid){ var $parentElement = $element.autoValidate('getParent'); var invalidParentClass = $element.autoValidate('getOption', 'invalidParentClass'); $parentElement.addClass(invalidParentClass); } var invalidClass = $element.autoValidate('getOption','invalidClass'); $element.addClass(invalidClass); }); } var avFollowUp = function(){ // returns jquery chain return this.each( function(){ var $element = $(this); var errorText = ""; var followUpOptions = $element.autoValidate('getOption','followUpOptions'); if (typeof(followUpOptions['errorText']) != "undefined"){ errorText = followUpOptions['errorText']; } $element.autoValidate('changeTitles', errorText); if ($element.autoValidate('getOption','valid') != true){ $element.autoValidate('styleInvalid'); } else { $element.autoValidate('styleValid'); } }); } var avSetErrorText = function(errorText){ // returns jquery chain return this.each( function(){ var $element = $(this); var newErrorText = {'errorText' : errorText}; var currentFOptions = $element.autoValidate('getOption','followUpOptions'); var newFOptions = $.extend(currentFOptions, newErrorText); $element.autoValidate('setOptions', {'followUpOptions' : newFOptions}); }); } // Any functions in the methods array are accessible to the user var methods = { 'init' : avInit, 'getOption' : avGetOption, 'setOptions': avSetOptions, 'trigger' : avTrigger, 'validate' : avValidate, 'getValue' : avGetValue, 'reset' : avReset, 'off' : avOff, 'on' : avOn, 'getParent' : avGetParent, 'changeTitles' : avChangeTitles, 'styleValid' : avStyleValid, 'styleInvalid' : avStyleInvalid, 'followUp' : avFollowUp, 'setErrorText' : avSetErrorText } // These functions are not in the methods list and are unaccessible to the user // this is to help prevent users from setting data anywhere but in the // avData namespace var setData = function($elements, dataName, dataValue) { // this is not a jquery object method, but will return the set of // jquery objects passed in for the first attribute, so it can be // chained like this: setData($elems,dObj).filter('#id').etc... return $elements.each(function(){ var $this = $(this); var currentData = $this.data('avData'); var newPiece = {}; newPiece[dataName] = dataValue; var newData = $.extend(currentData,newPiece); // this is setting the entire avData object which includes options{} and initOptions{} $this.data('avData',newData); }); } var getData = function($elements, dataName){ // for getting stored data in the namespace // but only for the first jquery element in the array var $this = $elements.first(); var dataValue = null; if (typeof($this.data('avData')) != 'undefined' && typeof($this.data('avData')[dataName]) != 'undefined'){ if (typeof($this.data('avData')[dataName] == 'object')){ // ojects need to be cloned dataValue = $.extend({},$this.data('avData')[dataName]); } else{ dataValue = $this.data('avData')[dataName]; } } return dataValue; // probably an object, options{} or initOptions{} } var getTest = function(testType, userTestFunction){ // choose validation function based on avType var avTest = null; switch (testType){ case 'userDefined': if (typeof(userTestFunction) === 'function'){ avTest = userTestFunction; } else{ avTest = function(testValue){return true;} } break; case 'integer': avTest = function(testValue){ var testExpression = /^[0-9]+$/; return testExpression.test(testValue); } break; case 'alpha': avTest = function(testValue){ var testExpression = /^[a-zA-Z]+$/; return testExpression.test(testValue); } break; case 'alphaNum': avTest = function(testValue){ var testExpression = /^[a-zA-Z0-9]+$/; return testExpression.test(testValue); } break; case 'alphaNumUnderscore': avTest = function(testValue){ var testExpression = /^[_]*[a-zA-Z0-9][a-zA-Z0-9_]*$/; return testExpression.test(testValue); } break; case 'basicText': avTest = function(testValue){ var testExpression = /^[a-zA-Z0-9_\-.]*$/; return testExpression.test(testValue); } break; default: avTest = function(testValue){return true;} break; } return avTest; } var attachHandlers = function($element){ // attach handlers $element.blur( function(eventObject){ // check if value has changed var $element = $(this); var newValue = $element.autoValidate('getValue'); var oldValue = $element.autoValidate('getOption','oldValue'); if (newValue != oldValue){ if ($element.autoValidate('getOption','active') == true){ $element.autoValidate('setOptions', { 'oldValue' : newValue}); $element.autoValidate('trigger'); } } }); $element.keyup( function(eventObject){ // respond if key is enter key var keyPressed = eventObject.keyCode; var carriageReturn = 13; var newValue = $element.autoValidate('getValue'); if (keyPressed == carriageReturn){ if ($element.autoValidate('getOption','active') == true){ $element.autoValidate('setOptions', { 'oldValue' : newValue}); $element.autoValidate('trigger'); } } }); return $element; } })( jQuery ); // here is where the jQuery object is passed into the function