;(function(factory) { typeof module === 'object' && module.exports ? module.exports = factory( require( 'jquery' ) ) : typeof define === 'function' && define.amd ? define(['jquery'], factory) : factory(jQuery); }(function($, undefined) { 'use strict'; var NS = 'validator', CLS_NS = '.' + NS, CLS_NS_RULE = '.rule', CLS_NS_FIELD = '.field', CLS_NS_FORM = '.form', CLS_WRAPPER = 'nice-' + NS, CLS_MSG_BOX = 'msg-box', ARIA_INVALID = 'aria-invalid', DATA_RULE = 'data-rule', DATA_MSG = 'data-msg', DATA_TIP = 'data-tip', DATA_OK = 'data-ok', DATA_TIMELY = 'data-timely', DATA_TARGET = 'data-target', DATA_DISPLAY = 'data-display', DATA_MUST = 'data-must', NOVALIDATE = 'novalidate', INPUT_SELECTOR = ':verifiable', rRules = /(&)?(!)?\b(\w+)(?:\[\s*(.*?\]?)\s*\]|\(\s*(.*?\)?)\s*\))?\s*(;|\|)?/g, rRule = /(\w+)(?:\[\s*(.*?\]?)\s*\]|\(\s*(.*?\)?)\s*\))?/, rDisplay = /(?:([^:;\(\[]*):)?(.*)/, rDoubleBytes = /[^\x00-\xff]/g, rPos = /top|right|bottom|left/, rAjaxType = /(?:(cors|jsonp):)?(?:(post|get):)?(.+)/i, rUnsafe = /[<>'"`\\]|&#x?\d+[A-F]?;?|%3[A-F]/gmi, noop = $.noop, proxy = $.proxy, trim = $.trim, isFunction = $.isFunction, isString = function(s) { return typeof s === 'string'; }, isObject = function(o) { return o && Object.prototype.toString.call(o) === '[object Object]'; }, isIE = document.documentMode || +(navigator.userAgent.match(/MSIE (\d+)/) && RegExp.$1), attr = function(el, key, value) { if (!el || !el.tagName) return null; if (value !== undefined) { if (value === null) el.removeAttribute(key); else el.setAttribute(key, '' + value); } else { return el.getAttribute(key); } }, novalidateonce, preinitialized = {}, defaults = { debug: 0, theme: 'default', ignore: '', focusInvalid: true, focusCleanup: false, stopOnError: false, beforeSubmit: null, valid: null, invalid: null, validation: null, formClass: 'n-default', validClass: 'n-valid', invalidClass: 'n-invalid', bindClassTo: null, remoteDataType: 'cors' }, fieldDefaults = { timely: 1, display: null, target: null, ignoreBlank: false, showOk: true, // Translate ajax response to validation result dataFilter: function (data) { if ( isString(data) || ( isObject(data) && ('error' in data || 'ok' in data) ) ) { return data; } }, msgMaker: function(opt) { var html; html = '' + opt.arrow; if (opt.result) { $.each(opt.result, function(i, obj){ html += '' + opt.icon + '' + obj.msg + ''; }); } else { html += opt.icon + '' + opt.msg + ''; } html += ''; return html; }, msgWrapper: 'span', msgArrow: '', msgIcon: '', msgClass: 'n-right', msgStyle: '', msgShow: null, msgHide: null }, themes = {}; /** jQuery Plugin * @param {Object} options debug {Boolean} 0 Whether to enable debug mode timely {Number} 1 Whether to enable timely validation theme {String} 'default' Theme name stopOnError {Boolean} false Whether to stop validate when found an error input focusCleanup {Boolean} false Whether to clean up the field message when focus the field focusInvalid {Boolean} true Whether to focus the field that is invalid ignoreBlank {Boolean} false When the field has no value, whether to ignore validation ignore {jqSelector} '' Ignored fields (Using jQuery selector) beforeSubmit {Function} Do something before submit form dataFilter {Function} Convert ajax results valid {Function} Triggered when the form is valid invalid {Function} Triggered when the form is invalid validClass {String} 'n-valid' Add this class name to a valid field invalidClass {String} 'n-invalid' Add this class name to a invalid field bindClassTo {jqSelector} ':verifiable' Which element should the className binding to display {Function} Callback function to get dynamic display target {Function} Callback function to get dynamic target msgShow {Function} Trigger this callback when show message msgHide {Function} Trigger this callback when hide message msgWrapper {String} 'span' Message wrapper tag name msgMaker {Function} Callback function to make message HTML msgArrow {String} Message arrow template msgIcon {String} Message icon template msgStyle {String} Custom message css style msgClass {String} Additional added to the message class names formClass {String} Additional added to the form class names messages {Object} Custom messages for the current instance rules {Object} Custom rules for the current instance fields {Object} Field validation configuration {String} key name|#id {String|Object} value Rule string or an object which can pass more arguments fields[key][rule] {String} Rule string fields[key][display] {String|Function} fields[key][tip] {String} Custom tip message fields[key][ok] {String} Custom success message fields[key][msg] {Object} Custom error message fields[key][msgStyle] {String} Custom message style fields[key][msgClass] {String} A className which added to message placeholder element fields[key][msgWrapper] {String} Tag name of the message placeholder element fields[key][msgMaker] {Function} A function to custom message HTML fields[key][dataFilter] {Function} A function to convert ajax results fields[key][valid] {Function} A function triggered when field is valid fields[key][invalid] {Function} A function triggered when field is invalid fields[key][must] {Boolean} If set true, we always check the field even has remote checking fields[key][timely] {Boolean} Whether to enable timely validation fields[key][target] {jqSelector} Define placement of a message */ $.fn.validator = function(options) { var that = this, args = arguments; if (that.is(INPUT_SELECTOR)) return that; if (!that.is('form')) that = this.find('form'); if (!that.length) that = this; that.each(function() { var instance = $(this).data(NS); if (instance) { if ( isString(options) ) { if ( options.charAt(0) === '_' ) return; instance[options].apply(instance, [].slice.call(args, 1)); } else if (options) { instance._reset(true); instance._init(this, options); } } else { new Validator(this, options); } }); return this; }; // Validate a field, or an area $.fn.isValid = function(callback, hideMsg) { var me = _getInstance(this[0]), hasCallback = isFunction(callback), ret, opt; if (!me) return true; if (!hasCallback && hideMsg === undefined) hideMsg = callback; me.checkOnly = !!hideMsg; opt = me.options; ret = me._multiValidate( this.is(INPUT_SELECTOR) ? this : this.find(INPUT_SELECTOR), function(isValid){ if (!isValid && opt.focusInvalid && !me.checkOnly) { // navigate to the error element me.$el.find('[' + ARIA_INVALID + ']:first').focus(); } if (hasCallback) { if (callback.length) { callback(isValid); } else if (isValid) { callback(); } } me.checkOnly = false; } ); // If you pass a callback, we maintain the jQuery object chain return hasCallback ? this : ret; }; $.extend($.expr.pseudos || $.expr[':'], { // A faster selector than ":input:not(:submit,:button,:reset,:image,:disabled,[contenteditable])" verifiable: function(elem) { var name = elem.nodeName.toLowerCase(); return ( name === 'input' && !({submit: 1, button: 1, reset: 1, image: 1})[elem.type] || name === 'select' || name === 'textarea' || elem.contentEditable === 'true' ) && !elem.disabled; }, // any value, but not only whitespace filled: function(elem) { return !!trim($(elem).val()); } }); /** * Creates a new Validator * * @class * @param {Element} element - form element * @param {Object} options - options for validator */ function Validator(element, options) { var me = this; if ( !(me instanceof Validator) ) { return new Validator(element, options); } if (Validator.pending) { $(window).on('validatorready', init); } else { init(); } function init() { me.$el = $(element); if (me.$el.length) { me._init(me.$el[0], options); } else if (isString(element)) { preinitialized[element] = options; } } } Validator.prototype = { _init: function(element, options) { var me = this, opt, themeOpt, dataOpt; // Initialization options if ( isFunction(options) ) { options = { valid: options }; } options = me._opt = options || {}; dataOpt = attr(element, 'data-'+ NS +'-option'); dataOpt = me._dataOpt = dataOpt && dataOpt.charAt(0) === '{' ? (new Function('return ' + dataOpt))() : {}; themeOpt = me._themeOpt = themes[ options.theme || dataOpt.theme || defaults.theme ]; opt = me.options = $.extend({}, defaults, fieldDefaults, themeOpt, me.options, options, dataOpt); me.rules = new Rules(opt.rules, true); me.messages = new Messages(opt.messages, true); me.Field = _createFieldFactory(me); me.elements = me.elements || {}; me.deferred = {}; me.errors = {}; me.fields = {}; // Initialization fields me._initFields(opt.fields); // Initialization events and make a cache if ( !me.$el.data(NS) ) { me.$el.data(NS, me).addClass(CLS_WRAPPER +' '+ opt.formClass) .on('form-submit-validate', function(e, a, $form, opts, veto) { me.vetoed = veto.veto = !me.isValid; me.ajaxFormOptions = opts; }) .on('submit'+ CLS_NS +' validate'+ CLS_NS, proxy(me, '_submit')) .on('reset'+ CLS_NS, proxy(me, '_reset')) .on('showmsg'+ CLS_NS, proxy(me, '_showmsg')) .on('hidemsg'+ CLS_NS, proxy(me, '_hidemsg')) .on('focusin'+ CLS_NS + ' click'+ CLS_NS, INPUT_SELECTOR, proxy(me, '_focusin')) .on('focusout'+ CLS_NS +' validate'+ CLS_NS, INPUT_SELECTOR, proxy(me, '_focusout')) .on('keyup'+ CLS_NS +' input'+ CLS_NS + ' compositionstart compositionend', INPUT_SELECTOR, proxy(me, '_focusout')) .on('click'+ CLS_NS, ':radio,:checkbox', 'click', proxy(me, '_focusout')) .on('change'+ CLS_NS, 'select,input[type="file"]', 'change', proxy(me, '_focusout')); // cache the novalidate attribute value me._NOVALIDATE = attr(element, NOVALIDATE); // Initialization is complete, stop off default HTML5 form validation // If use "jQuery.attr('novalidate')" in IE7 will complain: "SCRIPT3: Member not found." attr(element, NOVALIDATE, NOVALIDATE); } // Display all messages in target container if ( isString(opt.target) ) { me.$el.find(opt.target).addClass('msg-container'); } }, // Guess whether the form use ajax submit _guessAjax: function(form) { var me = this; if ( !(me.isAjaxSubmit = !!me.options.valid) ) { // if there is a "valid.form" event var events = ($._data || $.data)(form, 'events'); me.isAjaxSubmit = issetEvent(events, 'valid', 'form') || issetEvent(events, 'submit', 'form-plugin'); } function issetEvent(events, name, namespace) { return !!( events && events[name] && $.map(events[name], function(e){ return ~e.namespace.indexOf(namespace) ? 1 : null; }).length ) } }, _initFields: function(fields) { var me = this, k, arr, i, clear = fields === null; // Processing field information if (clear) fields = me.fields; if ( isObject(fields) ) { for (k in fields) { if (~k.indexOf(',')) { arr = k.split(','); i = arr.length; while (i--) { initField(trim(arr[i]), fields[k]); } } else { initField(k, fields[k]); } } } // Parsing DOM rules me.$el.find(INPUT_SELECTOR).each(function() { me._parse(this); }); function initField(k, v) { // delete a field from settings if ( v === null || clear ) { var el = me.elements[k]; if (el) me._resetElement(el, true); delete me.fields[k]; } else { me.fields[k] = new me.Field(k, isString(v) ? {rule: v} : v, me.fields[k]); } } }, // Parsing a field _parse: function(el) { var me = this, field, key = el.name, display, timely, dataRule = attr(el, DATA_RULE); dataRule && attr(el, DATA_RULE, null); // If the field has passed the key as id mode, or it doesn't has a name if ( el.id && ( ('#' + el.id in me.fields) || !key || // If dataRule and element are diffrent from old's, we use ID mode. (dataRule !== null && (field = me.fields[key]) && dataRule !== field.rule && el.id !== field.key) ) ) { key = '#' + el.id; } // Generate id if (!key) { key = '#' + (el.id = 'N' + String(Math.random()).slice(-12)); } field = me.getField(key, true); // The priority of passing parameter by DOM is higher than by JS. field.rule = dataRule || field.rule; if (display = attr(el, DATA_DISPLAY)) { field.display = display; } if (field.rule) { if ( attr(el, DATA_MUST) !== null || /\b(?:match|checked)\b/.test(field.rule) ) { field.must = true; } if ( /\brequired\b/.test(field.rule) ) { field.required = true; } if (timely = attr(el, DATA_TIMELY)) { field.timely = +timely; } else if (field.timely > 3) { attr(el, DATA_TIMELY, field.timely); } me._parseRule(field); field.old = {}; } if ( isString(field.target) ) { attr(el, DATA_TARGET, field.target); } if ( isString(field.tip) ) { attr(el, DATA_TIP, field.tip); } return me.fields[key] = field; }, // Parsing field rules _parseRule: function(field) { var arr = rDisplay.exec(field.rule); if (!arr) return; // current rule index field._i = 0; if (arr[1]) { field.display = arr[1]; } if (arr[2]) { field._rules = []; arr[2].replace(rRules, function(){ var args = arguments; args[4] = args[4] || args[5]; field._rules.push({ and: args[1] === '&', not: args[2] === '!', or: args[6] === '|', method: args[3], params: args[4] ? $.map( args[4].split(', '), trim ) : undefined }); }); } }, // Verify a zone _multiValidate: function($inputs, doneCallback){ var me = this, opt = me.options; me.hasError = false; if (opt.ignore) { $inputs = $inputs.not(opt.ignore); } $inputs.each(function() { me._validate(this); if (me.hasError && opt.stopOnError) { // stop the validation return false; } }); // Need to wait for all fields validation complete, especially asynchronous validation if (doneCallback) { me.validating = true; $.when.apply( null, $.map(me.deferred, function(v){return v;}) ).done(function(){ doneCallback.call(me, !me.hasError); me.validating = false; }); } // If the form does not contain asynchronous validation, the return value is correct. // Otherwise, you should detect form validation result through "doneCallback". return !$.isEmptyObject(me.deferred) ? undefined : !me.hasError; }, // Validate the whole form _submit: function(e) { var me = this, opt = me.options, form = e.target, canSubmit = e.type === 'submit' && form.tagName === 'FORM' && !e.isDefaultPrevented(); e.preventDefault(); if ( novalidateonce && ~(novalidateonce = false) || // Prevent duplicate submission me.submiting || // Receive the "validate" event only from the form. e.type === 'validate' && me.$el[0] !== form || // trigger the beforeSubmit callback. isFunction(opt.beforeSubmit) && opt.beforeSubmit.call(me, form) === false ) { return; } if (me.isAjaxSubmit === undefined) { me._guessAjax(form); } me._debug('log', '\n<<< event: ' + e.type); me._reset(); me.submiting = true; me._multiValidate( me.$el.find(INPUT_SELECTOR), function(isValid){ var ret = (isValid || opt.debug === 2) ? 'valid' : 'invalid', errors; if (!isValid) { if (opt.focusInvalid) { // navigate to the error element me.$el.find('[' + ARIA_INVALID + ']:first').focus(); } errors = $.map(me.errors, function(err){return err;}); } // releasing submit me.submiting = false; me.isValid = isValid; // trigger callback and event isFunction(opt[ret]) && opt[ret].call(me, form, errors); me.$el.trigger(ret + CLS_NS_FORM, [form, errors]); me._debug('log', '>>> ' + ret); if (!isValid) return; // For jquery.form plugin if (me.vetoed) { $(form).ajaxSubmit(me.ajaxFormOptions); } else if (canSubmit && !me.isAjaxSubmit) { document.createElement('form').submit.call(form); } } ); }, _reset: function(e) { var me = this; me.errors = {}; if (e) { me.reseting = true; me.$el.find(INPUT_SELECTOR).each( function(){ me._resetElement(this); }); delete me.reseting; } }, _resetElement: function(el, all) { this._setClass(el, null); this.hideMsg(el); }, // Handle events: "focusin/click" _focusin: function(e) { var me = this, opt = me.options, el = e.target, timely, msg; if ( me.validating || ( e.type==='click' && document.activeElement === el ) ) { return; } if (opt.focusCleanup) { if ( attr(el, ARIA_INVALID) === 'true' ) { me._setClass(el, null); me.hideMsg(el); } } msg = attr(el, DATA_TIP); if (msg) { me.showMsg(el, { type: 'tip', msg: msg }); } else { if (attr(el, DATA_RULE)) { me._parse(el); } if (timely = attr(el, DATA_TIMELY)) { if ( timely === 8 || timely === 9 ) { me._focusout(e); } } } }, // Handle events: "focusout/validate/keyup/click/change/input/compositionstart/compositionend" _focusout: function(e) { var me = this, opt = me.options, el = e.target, etype = e.type, etype0, focusin = etype === 'focusin', special = etype === 'validate', elem, field, old, value, timestamp, key, specialKey, timely, timer = 0; if (etype === 'compositionstart') { me.pauseValidate = true; } if (etype === 'compositionend') { me.pauseValidate = false; } if (me.pauseValidate) { return; } // For checkbox and radio elem = el.name && _checkable(el) ? me.$el.find('input[name="'+ el.name +'"]').get(0) : el; // Get field if (!(field = me.getField(elem)) || !field.rule) { return; } // Cache event type etype0 = field._e; field._e = etype; timely = field.timely; if (!special) { if (!timely || (_checkable(el) && etype !== 'click')) { return; } value = field.getValue(); // not validate field unless fill a value if ( field.ignoreBlank && !value && !focusin ) { me.hideMsg(el); return; } if ( etype === 'focusout' ) { if (etype0 === 'change') { return; } if ( timely === 2 || timely === 8 ) { old = field.old; if (value && old) { if (field.isValid && !old.showOk) { me.hideMsg(el); } else { me._makeMsg(el, field, old); } } else { return; } } } else { if ( timely < 2 && !e.data ) { return; } // mark timestamp to reduce the frequency of the received event timestamp = +new Date(); if ( timestamp - (el._ts || 0) < 100 ) { return; } el._ts = timestamp; // handle keyup if ( etype === 'keyup' ) { if (etype0 === 'input') { return; } key = e.keyCode; specialKey = { 8: 1, // Backspace 9: 1, // Tab 16: 1, // Shift 32: 1, // Space 46: 1 // Delete }; // only gets focus, no validation if ( key === 9 && !value ) { return; } // do not validate, if triggered by these keys if ( key < 48 && !specialKey[key] ) { return; } } if ( !focusin ) { // keyboard events, reducing the frequency of validation timer = timely <100 ? (etype === 'click' || el.tagName === 'SELECT') ? 0 : 400 : timely; } } } // if the current field is ignored if ( opt.ignore && $(el).is(opt.ignore) ) { return; } clearTimeout(field._t); if (timer) { field._t = setTimeout(function() { me._validate(el, field); }, timer); } else { if (special) field.old = {}; me._validate(el, field); } }, _setClass: function(el, isValid) { var $el = $(el), opt = this.options; if (opt.bindClassTo) { $el = $el.closest(opt.bindClassTo); } $el.removeClass( opt.invalidClass + ' ' + opt.validClass ); if (isValid !== null) { $el.addClass( isValid ? opt.validClass : opt.invalidClass ); } }, _showmsg: function(e, type, msg) { var me = this, el = e.target; if ( me.$el.is(el) ) { if (isObject(type)) { me.showMsg(type) } else if ( type === 'tip' ) { me.$el.find(INPUT_SELECTOR +'['+ DATA_TIP +']', el).each(function(){ me.showMsg(this, {type: type, msg: msg}); }); } } else { me.showMsg(el, {type: type, msg: msg}); } }, _hidemsg: function(e) { var $el = $(e.target); if ( $el.is(INPUT_SELECTOR) ) { this.hideMsg($el); } }, // Validated a field _validatedField: function(el, field, ret) { var me = this, opt = me.options, isValid = field.isValid = ret.isValid = !!ret.isValid, callback = isValid ? 'valid' : 'invalid'; ret.key = field.key; ret.ruleName = field._r; ret.id = el.id; ret.value = field.value; me.elements[field.key] = ret.element = el; me.isValid = me.$el[0].isValid = isValid ? me.isFormValid() : isValid; if (isValid) { ret.type = 'ok'; } else { if (me.submiting) { me.errors[field.key] = ret.msg; } me.hasError = true; } // cache result field.old = ret; // trigger callback isFunction(field[callback]) && field[callback].call(me, el, ret); isFunction(opt.validation) && opt.validation.call(me, el, ret); // trigger event $(el).attr( ARIA_INVALID, isValid ? null : true ) .trigger( callback + CLS_NS_FIELD, [ret, me] ); me.$el.triggerHandler('validation', [ret, me]); if (me.checkOnly) return; // set className me._setClass(el, ret.skip || ret.type === 'tip' ? null : isValid); me._makeMsg.apply(me, arguments); }, _makeMsg: function(el, field, ret) { // show or hide the message if (field.msgMaker) { ret = $.extend({}, ret); if (field._e === 'focusin') { ret.type = 'tip'; } this[ ret.showOk || ret.msg || ret.type === 'tip' ? 'showMsg' : 'hideMsg' ](el, ret, field); } }, // Validated a rule _validatedRule: function(el, field, ret, msgOpt) { field = field || me.getField(el); msgOpt = msgOpt || {}; var me = this, msg, rule, method = field._r, timely = field.timely, special = timely === 9 || timely === 8, transfer, temp, isValid = false; // use null to break validation from a field if (ret === null) { me._validatedField(el, field, {isValid: true, skip: true}); field._i = 0; return; } else if (ret === undefined) { transfer = true; } else if (ret === true || ret === '') { isValid = true; } else if (isString(ret)) { msg = ret; } else if (isObject(ret)) { if (ret.error) { msg = ret.error; } else { msg = ret.ok; isValid = true; } } else { isValid = !!ret } rule = field._rules[field._i]; if (rule.not) { msg = undefined; isValid = method === 'required' || !isValid; } if (rule.or) { if (isValid) { while ( field._i < field._rules.length && field._rules[field._i].or ) { field._i++; } } else { transfer = true; } } else if (rule.and) { if (!field.isValid) transfer = true; } if (transfer) { isValid = true; } // message analysis, and throw rule level event else { if (isValid) { if (field.showOk !== false) { temp = attr(el, DATA_OK); msg = temp === null ? isString(field.ok) ? field.ok : msg : temp; if (!isString(msg) && isString(field.showOk)) { msg = field.showOk; } if (isString(msg)) { msgOpt.showOk = isValid; } } } if (!isValid || special) { /* rule message priority: 1. custom DOM message 2. custom field message; 3. global defined message; 4. rule returned message; 5. default message; */ msg = (_getDataMsg(el, field, msg || rule.msg || me.messages[method]) || me.messages.fallback).replace(/\{0\|?([^\}]*)\}/, function(m, defaultDisplay){ return me._getDisplay(el, field.display) || defaultDisplay || me.messages[0]; }); } if (!isValid) field.isValid = isValid; msgOpt.msg = msg; $(el).trigger( (isValid ? 'valid' : 'invalid') + CLS_NS_RULE, [method, msg]); } if (special && (!transfer || rule.and)) { if (!isValid && !field._m) field._m = msg; field._v = field._v || []; field._v.push({ type: isValid ? !transfer ? 'ok' : 'tip' : 'error', msg: msg || rule.msg }); } me._debug('log', ' ' + field._i + ': ' + method + ' => ' + (isValid || msg)); // the current rule has passed, continue to validate if ( (isValid || special) && field._i < field._rules.length - 1) { field._i++; me._checkRule(el, field); } // field was invalid, or all fields was valid else { field._i = 0; if (special) { msgOpt.isValid = field.isValid; msgOpt.result = field._v; msgOpt.msg = field._m || ''; if (!field.value && (field._e === 'focusin')) { msgOpt.type = 'tip'; } } else { msgOpt.isValid = isValid; } me._validatedField(el, field, msgOpt); delete field._m; delete field._v; } }, // Verify a rule form a field _checkRule: function(el, field) { var me = this, ret, fn, old, key = field.key, rule = field._rules[field._i], method = rule.method, params = rule.params; // request has been sent, wait it if (me.submiting && me.deferred[key]) { return; } old = field.old; field._r = method; if (old && !field.must && !rule.must && rule.result !== undefined && old.ruleName === method && old.id === el.id && field.value && old.value === field.value ) { // get result from cache ret = rule.result; } else { // get result from current rule fn = _getDataRule(el, method) || me.rules[method] || noop; ret = fn.call(field, el, params, field); if (fn.msg) rule.msg = fn.msg; } // asynchronous validation if (isObject(ret) && isFunction(ret.then)) { me.deferred[key] = ret; // whether the field valid is unknown field.isValid = undefined; // show loading message !me.checkOnly && me.showMsg(el, { type: 'loading', msg: me.messages.loading }, field); // waiting to parse the response data ret.then( function(d, textStatus, jqXHR) { var data = trim(jqXHR.responseText), result, dataFilter = field.dataFilter; // detect if data is json or jsonp format if (/jsonp?/.test(this.dataType)) { data = d; } else if (data.charAt(0) === '{') { data = $.parseJSON(data); } // filter data result = dataFilter.call(this, data, field); if (result === undefined) result = dataFilter.call(this, data.data, field); rule.data = this.data; rule.result = field.old ? result : undefined; me._validatedRule(el, field, result); }, function(jqXHR, textStatus){ me._validatedRule(el, field, me.messages[textStatus] || textStatus); } ).always(function(){ delete me.deferred[key]; }); } // other result else { me._validatedRule(el, field, ret); } }, // Processing the validation _validate: function(el, field) { var me = this; // doesn't validate the element that has "disabled" or "novalidate" attribute if ( el.disabled || attr(el, NOVALIDATE) !== null ) { return; } field = field || me.getField(el); if (!field) return; if (!field._rules) me._parse(el); if (!field._rules) return; me._debug('info', field.key); field.isValid = true; field.element = el; // Cache the value field.value = field.getValue(); // if the field is not required, and that has a blank value if (!field.required && !field.must && !field.value) { if (!_checkable(el)) { me._validatedField(el, field, {isValid: true}); return true; } } me._checkRule(el, field); return field.isValid; }, _debug: function(type, messages) { if (window.console && this.options.debug) { console[type](messages); } }, /** * Detecting whether the value of an element that matches a rule * * @method test * @param {Element} el - input element * @param {String} rule - rule name */ test: function(el, rule) { var me = this, ret, parts = rRule.exec(rule), field, method, params; if (parts) { method = parts[1]; if (method in me.rules) { params = parts[2] || parts[3]; params = params ? params.split(', ') : undefined; field = me.getField(el, true); field._r = method; field.value = field.getValue(); ret = me.rules[method].call(field, el, params); } } return ret === true || ret === undefined || ret === null; }, _getDisplay: function(el, str) { return !isString(str) ? isFunction(str) ? str.call(this, el) : '' : str; }, _getMsgOpt: function(obj, field) { var opt = field ? field : this.options; return $.extend({ type: 'error', pos: _getPos(opt.msgClass), target: opt.target, wrapper: opt.msgWrapper, style: opt.msgStyle, cls: opt.msgClass, arrow: opt.msgArrow, icon: opt.msgIcon }, isString(obj) ? {msg: obj} : obj); }, _getMsgDOM: function(el, msgOpt) { var $el = $(el), $msgbox, datafor, tgt, container; if ( $el.is(INPUT_SELECTOR) ) { tgt = msgOpt.target || attr(el, DATA_TARGET); if (tgt) { tgt = !isFunction(tgt) ? tgt.charAt(0) === '#' ? $(tgt) : this.$el.find(tgt) : tgt.call(this, el); if (tgt.length) { if ( tgt.is(INPUT_SELECTOR) ) { $el = tgt el = tgt.get(0); } else if ( tgt.hasClass(CLS_MSG_BOX) ) { $msgbox = tgt; } else { container = tgt; } } } if (!$msgbox) { datafor = (!_checkable(el) || !el.name) && el.id ? el.id : el.name; $msgbox = (container || this.$el).find(msgOpt.wrapper + '.' + CLS_MSG_BOX + '[for="' + datafor + '"]'); } } else { $msgbox = $el; } // Create new message box if (!msgOpt.hide && !$msgbox.length) { $msgbox = $('<'+ msgOpt.wrapper + '>').attr({ 'class': CLS_MSG_BOX + (msgOpt.cls ? ' ' + msgOpt.cls : ''), 'style': msgOpt.style || undefined, 'for': datafor }); if (container) { $msgbox.appendTo(container); } else { if ( _checkable(el) ) { var $parent = $el.parent(); $msgbox.appendTo( $parent.is('label') ? $parent.parent() : $parent ); } else { $msgbox[!msgOpt.pos || msgOpt.pos === 'right' ? 'insertAfter' : 'insertBefore']($el); } } } return $msgbox; }, /** * Show validation message * * @method showMsg * @param {Element} el - input element * @param {Object} msgOpt */ showMsg: function(el, msgOpt, /*INTERNAL*/ field) { if (!el) return; var me = this, opt = me.options, msgShow, msgMaker, temp, $msgbox; if (isObject(el) && !el.jquery && !msgOpt) { $.each(el, function(key, msg) { var el = me.elements[key] || me.$el.find(_key2selector(key))[0]; me.showMsg(el, msg); }); return; } if ($(el).is(INPUT_SELECTOR)) { field = field || me.getField(el); } if (!(msgMaker = (field || opt).msgMaker)) { return; } msgOpt = me._getMsgOpt(msgOpt, field); el = (el.name && _checkable(el) ? me.$el.find('input[name="'+ el.name +'"]') : $(el)).get(0); // ok or tip if (!msgOpt.msg && msgOpt.type !== 'error') { temp = attr(el, 'data-' + msgOpt.type); if (temp !== null) msgOpt.msg = temp; } if ( !isString(msgOpt.msg) ) { return; } $msgbox = me._getMsgDOM(el, msgOpt); !rPos.test($msgbox[0].className) && $msgbox.addClass(msgOpt.cls); if ( isIE === 6 && msgOpt.pos === 'bottom' ) { $msgbox[0].style.marginTop = $(el).outerHeight() + 'px'; } $msgbox.html( msgMaker.call(me, msgOpt) )[0].style.display = ''; if (isFunction(msgShow = field && field.msgShow || opt.msgShow)) { msgShow.call(me, $msgbox, msgOpt.type); } }, /** * Hide validation message * * @method hideMsg * @param {Element} el - input element * @param {Object} msgOpt optional */ hideMsg: function(el, msgOpt, /*INTERNAL*/ field) { var me = this, opt = me.options, msgHide, $msgbox; el = $(el).get(0); if ($(el).is(INPUT_SELECTOR)) { field = field || me.getField(el); if (field) { if (field.isValid || me.reseting) attr(el, ARIA_INVALID, null); } } msgOpt = me._getMsgOpt(msgOpt, field); msgOpt.hide = true; $msgbox = me._getMsgDOM(el, msgOpt); if (!$msgbox.length) return; if ( isFunction(msgHide = field && field.msgHide || opt.msgHide) ) { msgHide.call(me, $msgbox, msgOpt.type); } else { $msgbox[0].style.display = 'none'; $msgbox[0].innerHTML = ''; } }, /** * Get field information * * @method getField * @param {Element} - input element * @return {Object} field */ getField: function(el, must) { var me = this, key, field; if (isString(el)) { key = el; el = undefined; } else { if (attr(el, DATA_RULE)) { return me._parse(el); } if (el.id && '#' + el.id in me.fields || !el.name) { key = '#' + el.id; } else { key = el.name; } } if ( (field = me.fields[key]) || must && (field = new me.Field(key)) ) { field.element = el; } return field; }, /** * Config a field * * @method: setField * @param {String} key * @param {Object} obj */ setField: function(key, obj) { var fields = {}; if (!key) return; // update this field if (isString(key)) { fields[key] = obj; } // update fields else { fields = key; } this._initFields(fields); }, /** * Detecting whether the form is valid * * @method isFormValid * @return {Boolean} */ isFormValid: function() { var fields = this.fields, k, field; for (k in fields) { field = fields[k]; if (!field._rules || !field.required && !field.must && !field.value) continue; if (!field.isValid) return false; } return true; }, /** * Prevent submission form * * @method holdSubmit * @param {Boolean} hold - If set to false, will release the hold */ holdSubmit: function(hold) { this.submiting = hold === undefined || hold; }, /** * Clean validation messages * * @method cleanUp */ cleanUp: function() { this._reset(1); }, /** * Destroy the validation * * @method destroy */ destroy: function() { this._reset(1); this.$el.off(CLS_NS).removeData(NS); attr(this.$el[0], NOVALIDATE, this._NOVALIDATE); } }; /** * Create Field Factory * * @class * @param {Object} context * @return {Function} Factory */ function _createFieldFactory(context) { function FieldFactory() { var options = this.options; for (var i in options) { if (i in fieldDefaults) this[i] = options[i]; } $.extend(this, { _valHook: function() { return this.element.contentEditable === 'true' ? 'text' : 'val'; }, getValue: function() { var elem = this.element; if (elem.type === 'number' && elem.validity && elem.validity.badInput) { return 'NaN'; } return $(elem)[this._valHook()](); }, setValue: function(value) { $(this.element)[this._valHook()](this.value = value); }, // Get a range of validation messages getRangeMsg: function(value, params, suffix) { if (!params) return; var me = this, msg = me.messages[me._r] || '', result, p = params[0].split('~'), e = params[1] === 'false', a = p[0], b = p[1], c = 'rg', args = [''], isNumber = trim(value) && +value === +value; function compare(large, small) { return !e ? large >= small : large > small; } if (p.length === 2) { if (a && b) { if (isNumber && compare(value, +a) && compare(+b, value)) { result = true; } args = args.concat(p); c = e ? 'gtlt' : 'rg'; } else if (a && !b) { if (isNumber && compare(value, +a)) { result = true; } args.push(a); c = e ? 'gt' : 'gte'; } else if (!a && b) { if (isNumber && compare(+b, value)) { result = true; } args.push(b); c = e ? 'lt' : 'lte'; } } else { if (value === +a) { result = true; } args.push(a); c = 'eq'; } if (msg) { if (suffix && msg[c + suffix]) { c += suffix; } args[0] = msg[c]; } return result || me._rules && ( me._rules[me._i].msg = me.renderMsg.apply(null, args) ); }, // Render message template renderMsg: function() { var args = arguments, tpl = args[0], i = args.length; if (!tpl) return; while (--i) { tpl = tpl.replace('{' + i + '}', args[i]); } return tpl; } }); } function Field(key, obj, oldField) { this.key = key; this.validator = context; $.extend(this, oldField, obj); } FieldFactory.prototype = context; Field.prototype = new FieldFactory(); return Field; } /** * Create Rules * * @class * @param {Object} obj rules * @param {Object} context context */ function Rules(obj, context) { if (!isObject(obj)) return; var k, that = context ? context === true ? this : context : Rules.prototype; for (k in obj) { if (_checkRuleName(k)) { that[k] = _getRule(obj[k]); } } } /** * Create Messages * * @class * @param {Object} obj rules * @param {Object} context context */ function Messages(obj, context) { if (!isObject(obj)) return; var k, that = context ? context === true ? this : context : Messages.prototype; for (k in obj) { that[k] = obj[k]; } } // Rule converted factory function _getRule(fn) { switch ($.type(fn)) { case 'function': return fn; case 'array': var f = function() { return fn[0].test(this.value) || fn[1] || false; }; f.msg = fn[1]; return f; case 'regexp': return function() { return fn.test(this.value); }; } } // Get instance by an element function _getInstance(el) { var wrap, k, options; if (!el || !el.tagName) return; switch (el.tagName) { case 'INPUT': case 'SELECT': case 'TEXTAREA': case 'BUTTON': case 'FIELDSET': wrap = el.form || $(el).closest('.' + CLS_WRAPPER); break; case 'FORM': wrap = el; break; default: wrap = $(el).closest('.' + CLS_WRAPPER); } for (k in preinitialized) { if ($(wrap).is(k)) { options = preinitialized[k]; break; } } return $(wrap).data(NS) || $(wrap)[NS](options).data(NS); } // Get custom rules on the node function _getDataRule(el, method) { var fn = trim(attr(el, DATA_RULE + '-' + method)); if ( fn && (fn = new Function('return ' + fn)()) ) { return _getRule(fn); } } // Get custom messages on the node function _getDataMsg(el, field, m) { var msg = field.msg, item = field._r; if ( isObject(msg) ) msg = msg[item]; if ( !isString(msg) ) { msg = attr(el, DATA_MSG + '-' + item) || attr(el, DATA_MSG) || ( m ? isString(m) ? m : m[item] : ''); } return msg; } // Get message position function _getPos(str) { var pos; if (str) pos = rPos.exec(str); return pos && pos[0]; } // Check whether the element is checkbox or radio function _checkable(el) { return el.tagName === 'INPUT' && el.type === 'checkbox' || el.type === 'radio'; } // Parse date string to timestamp function _parseDate(str) { return Date.parse(str.replace(/\.|\-/g, '/')); } // Rule name only allows alphanumeric characters and underscores function _checkRuleName(name) { return /^\w+$/.test(name); } // Translate field key to jQuery selector. function _key2selector(key) { var isID = key.charAt(0) === '#'; key = key.replace(/([:.{(|)}/\[\]])/g, '\\$1'); return isID ? key : '[name="'+ key +'"]:first'; } // Fixed a issue cause by refresh page in IE. $(window).on('beforeunload', function(){ this.focus(); }); $(document) .on('click', ':submit', function(){ var input = this, attrNode; if (!input.form) return; // Shim for "formnovalidate" attrNode = input.getAttributeNode('formnovalidate'); if (attrNode && attrNode.nodeValue !== null || attr(input, NOVALIDATE)!== null) { novalidateonce = true; } }) // Automatic initializing form validation .on('focusin submit validate', 'form,.'+CLS_WRAPPER, function(e) { if ( attr(this, NOVALIDATE) !== null ) return; var $form = $(this), me; if ( !$form.data(NS) && (me = _getInstance(this)) ) { if ( !$.isEmptyObject(me.fields) ) { // Execute event handler if (e.type === 'focusin') { me._focusin(e); } else { me._submit(e); } } else { attr(this, NOVALIDATE, NOVALIDATE); $form.off(CLS_NS).removeData(NS); } } }); new Messages({ fallback: 'This field is not valid.', loading: 'Validating...' }); // Built-in rules (global) new Rules({ /** * required * * @example: required required(jqSelector) required(anotherRule) required(not, -1) required(from, .contact) */ required: function(element, params) { var me = this, val = trim(me.value), isValid = true; if (params) { if ( params.length === 1 ) { if ( !_checkRuleName(params[0]) ) { if (!val && !$(params[0], me.$el).length ) { return null; } } else if ( me.rules[params[0]] ) { if ( !val && !me.test(element, params[0]) ) { return null; } me._r = 'required' } } else if ( params[0] === 'not' ) { $.each(params.slice(1), function() { return (isValid = val !== trim(this)); }); } else if ( params[0] === 'from' ) { var $elements = me.$el.find(params[1]), VALIDATED = '_validated_', ret; isValid = $elements.filter(function(){ var field = me.getField(this); return field && !!trim(field.getValue()); }).length >= (params[2] || 1); if (isValid) { if (!val) ret = null; } else { ret = _getDataMsg($elements[0], me) || false; } if ( !$(element).data(VALIDATED) ) { $elements.data(VALIDATED, 1).each(function(){ if (element !== this) { me._validate(this); } }).removeData(VALIDATED); } return ret; } } return isValid && !!val; }, /** * integer * * @example: integer integer[+] integer[+0] integer[-] integer[-0] */ integer: function(element, params) { var re, z = '0|', p = '[1-9]\\d*', key = params ? params[0] : '*'; switch (key) { case '+': re = p; break; case '-': re = '-' + p; break; case '+0': re = z + p; break; case '-0': re = z + '-' + p; break; default: re = z + '-?' + p; } re = '^(?:' + re + ')$'; return new RegExp(re).test(this.value) || (this.messages.integer && this.messages.integer[key]); }, /** * match another field * * @example: match[password] Match the password field (two values ​​must be the same) match[eq, password] Ditto match[neq, count] The value must be not equal to the value of the count field match[lt, count] The value must be less than the value of the count field match[lte, count] The value must be less than or equal to the value of the count field match[gt, count] The value must be greater than the value of the count field match[gte, count] The value must be greater than or equal to the value of the count field match[gte, startDate, date] match[gte, startTime, time] **/ match: function(element, params) { if (!params) return; var me = this, isValid = true, a, b, key, msg, type = 'eq', parser, selector2, elem2, field2; if (params.length === 1) { key = params[0]; } else { type = params[0]; key = params[1]; } selector2 = _key2selector(key); elem2 = me.$el.find(selector2)[0]; // If the compared field is not exist if (!elem2) return; field2 = me.getField(elem2); a = me.value; b = field2.getValue(); if (!me._match) { me.$el.on('valid'+CLS_NS_FIELD+CLS_NS, selector2, function(){ $(element).trigger('validate'); }); me._match = field2._match = 1; } // If both fields are blank if (!me.required && a === '' && b === '') { return null; } parser = params[2]; if (parser) { if (/^date(time)?$/i.test(parser)) { a = _parseDate(a); b = _parseDate(b); } else if (parser === 'time') { a = +a.replace(/:/g, ''); b = +b.replace(/:/g, ''); } } // If the compared field is incorrect, we only ensure that this field is correct. if (type !== 'eq' && !isNaN(+a) && isNaN(+b)) { return true; } switch (type) { case 'lt': isValid = +a < +b; break; case 'lte': isValid = +a <= +b; break; case 'gte': isValid = +a >= +b; break; case 'gt': isValid = +a > +b; break; case 'neq': isValid = a !== b; break; default: isValid = a === b; } return isValid || ( isObject(me.messages.match) && me.messages.match[type].replace( '{1}', me._getDisplay( elem2, field2.display || key ) ) ); }, /** * range numbers * * @example: range[0~99] Number 0-99 range[0~] Number greater than or equal to 0 range[~100] Number less than or equal to 100 **/ range: function(element, params) { return this.getRangeMsg(this.value, params); }, /** * how many checkbox or radio inputs that checked * * @example: checked; no empty, same to required checked[1~3] 1-3 items checked[1~] greater than 1 item checked[~3] less than 3 items checked[3] 3 items **/ checked: function(element, params) { if ( !_checkable(element) ) return; var me = this, elem, count; if (element.name) { count = me.$el.find('input[name="' + element.name + '"]').filter(function() { var el = this; if (!elem && _checkable(el)) elem = el; return !el.disabled && el.checked; }).length; } else { elem = element; count = elem.checked; } if (params) { return me.getRangeMsg(count, params); } else { return !!count || _getDataMsg(elem, me, '') || me.messages.required || false; } }, /** * length of a characters (You can pass the second parameter "true", will calculate the length in bytes) * * @example: length[6~16] 6-16 characters length[6~] Greater than 6 characters length[~16] Less than 16 characters length[~16, true] Less than 16 characters, non-ASCII characters calculating two-character **/ length: function(element, params) { var value = this.value, len = (params[1] === 'true' ? value.replace(rDoubleBytes, 'xx') : value).length; return this.getRangeMsg(len, params, (params[1] ? '_2' : '')); }, /** * remote validation * * @description * remote([get:]url [, name1, [name2 ...]]); * Adaptation three kinds of results (Front for the successful, followed by a failure): 1. text: '' 'Error Message' 2. json: {"ok": ""} {"error": "Error Message"} 3. json wrapper: {"status": 1, "data": {"ok": ""}} {"status": 1, "data": {"error": "Error Message"}} * @example The simplest: remote(path/to/server); With parameters: remote(path/to/server, name1, name2, ...); By GET: remote(get:path/to/server, name1, name2, ...); Name proxy: remote(path/to/server, name1, proxyname2:name2, proxyname3:#id3, ...) Query String remote(path/to/server, foo=1&bar=2, name1, name2, ...) CORS remote(cors:path/to/server) JSONP remote(jsonp:path/to/server) */ remote: function(element, params) { if (!params) return; var me = this, arr = rAjaxType.exec(params[0]), rule = me._rules[me._i], data = {}, queryString = '', url = arr[3], type = arr[2] || 'POST', // GET / POST rType = (arr[1] || this.validator.options.remoteDataType || '').toLowerCase(), // CORS / JSONP dataType; rule.must = true; data[element.name] = me.value; // There are extra fields if (params[1]) { $.map(params.slice(1), function(name) { var arr, key; if (~name.indexOf('=')) { queryString += '&' + name; } else { arr = name.split(':'); name = trim(arr[0]); key = trim(arr[1]) || name; data[ name ] = me.$el.find( _key2selector(key) ).val(); } }); } data = $.param(data) + queryString; if (!me.must && rule.data && rule.data === data) { return rule.result; } // Cross-domain request, force jsonp dataType if (rType !== 'cors' && /^https?:/.test(url) && !~url.indexOf(location.host)) { dataType = 'jsonp'; } // Asynchronous validation need return jqXHR objects return $.ajax({ url: url, type: type, data: data, dataType: dataType }); }, /** * filter characters, direct filtration without prompting error (support custom regular expressions) * * @example * filter filtering unsafe characters * filter(regexp) filtering the "regexp" matched characters */ filter: function(element, params) { var value = this.value, temp = value.replace( params ? (new RegExp('[' + params[0] + ']', 'gm')) : rUnsafe, '' ); if (temp !== value) this.setValue(temp); } }); /** * Config global options * * @static config * @param {Object} options */ Validator.config = function(key, value) { if (isObject(key)) { $.each(key, _config); } else if (isString(key)) { _config(key, value); } function _config(k, o) { if (k === 'rules') { new Rules(o); } else if (k === 'messages') { new Messages(o); } else if (k in fieldDefaults) { fieldDefaults[k] = o; } else { defaults[k] = o; } } }; /** * Config themes * * @static setTheme * @param {String|Object} name * @param {Object} obj * @example .setTheme( themeName, themeOptions ) .setTheme( multiThemes ) */ Validator.setTheme = function(name, obj) { if ( isObject(name) ) { $.extend(true, themes, name); } else if ( isString(name) && isObject(obj) ) { themes[name] = $.extend(themes[name], obj); } }; /** * Resource loader * * @static load * @param {String} str * @example .load('local=zh-CN') // load: local/zh-CN.js and jquery.validator.css .load('local=zh-CN&css=') // load: local/zh-CN.js .load('local&css') // load: local/en.js (set ) and jquery.validator.css .load('local') // dito */ Validator.load = function(str) { if (!str) return; var doc = document, params = {}, node = doc.scripts[0], dir, el, ONLOAD; str.replace(/([^?=&]+)=([^&#]*)/g, function(m, key, value){ params[key] = value; }); dir = params.dir || Validator.dir; if (!Validator.css && params.css !== '') { el = doc.createElement('link'); el.rel = 'stylesheet'; el.href = Validator.css = dir + 'jquery.validator.css'; node.parentNode.insertBefore(el, node); } if (!Validator.local && ~str.indexOf('local') && params.local !== '') { Validator.local = (params.local || doc.documentElement.lang || 'en').replace('_','-'); Validator.pending = 1; el = doc.createElement('script'); el.src = dir + 'local/' + Validator.local + '.js'; ONLOAD = 'onload' in el ? 'onload' : 'onreadystatechange'; el[ONLOAD] = function() { if (!el.readyState || /loaded|complete/.test(el.readyState)) { el = el[ONLOAD] = null; delete Validator.pending; $(window).triggerHandler('validatorready'); } }; node.parentNode.insertBefore(el, node); } }; // Auto loading resources (function(){ var scripts = document.scripts, i = scripts.length, node, arr, re = /(.*validator(?:\.min)?.js)(\?.*(?:local|css|dir)(?:=[\w\-]*)?)?/; while (i-- && !arr) { node = scripts[i]; arr = (node.hasAttribute ? node.src : node.getAttribute('src',4)||'').match(re); } if (!arr) return; Validator.dir = arr[1].split('/').slice(0, -1).join('/')+'/'; Validator.load(arr[2]); })(); return $[NS] = Validator; }));