(function () { 'use strict'; angular.module('unobtrusive.validation', []) .provider('validation', [function () { var validationTypes = {}; function getValidationType(validatorName) { return validationTypes[validatorName]; }; this.$get = ['$injector', '$sce', function ($injector, $sce) { function startsWith(string, start) { return string.slice(0, start.length) == start; }; function camelCase(string) { return string.charAt(0).toLowerCase() + string.slice(1); }; // Aggregate our attributes for validation parameters. // For example, valRegexPattern is a parameter of valRegex called "pattern". function buildValidatorsFromAttributes(attrs, tools, scope, ngModel) { var keys = Object.keys(attrs).sort(); var result = {}; angular.forEach(keys, function (key) { if (key == 'val' || key == 'valIf' || key == 'valRealtime' || !startsWith(key, 'val')) return; var handled = false; if (key.substr(3).charAt(0).toLowerCase() == key.substr(3).charAt(0)) { // Check to make sure the next character is an upper-case character... keeps us from capturing data-value and things like that. return; } var keyName = camelCase(key.substr(3)); angular.forEach(result, function (validator, validatorName) { if (startsWith(keyName, validatorName)) { validator.parameters[camelCase(keyName.substr(validatorName.length))] = attrs[key]; handled = true; return; } }); if (handled) return; var validate = getValidationType(keyName); if (validate) { result[keyName] = { name: keyName, validate: validate.validate, message: $sce.trustAsHtml(attrs[key]), parameters: [], injected: {}, attributes: attrs, scope: scope, ngModel: ngModel, fail: function (message) { tools.fail(keyName, message); }, pass: function () { tools.pass(keyName); } }; if (validate.inject) { angular.forEach(validate.inject, function (name) { result[keyName].injected[name] = $injector.get(name); }); } } else { console.log('WARNING: Unhandled validation attribute: ' + keyName); } }); return result; }; var svc = { ensureValidation: function (scope) { scope['$$ validation'] = scope['$$ validation'] || { cancelSuppress: false, messages: {}, data: {} }; return scope['$$ validation']; }, buildValidation: function (scope, element, attrs, ngModelController) { var validationEnabled = true; var validationFor = attrs['name']; ngModelController.suppressValidationMessages = true; ngModelController.validationMessages = {}; var validators; var result = { enable: function () { validationEnabled = true; result.runValidations(svc.dataValue(scope, validationFor)); result.populateMessages(); }, disable: function () { validationEnabled = false; ngModelController.validationMessages = {}; angular.forEach(validators, function (value, key) { result.pass(key); }) result.populateMessages(); }, populateMessages: function () { if (!ngModelController.suppressValidationMessages) { svc.messageArray(scope, validationFor, ngModelController.validationMessages); } }, runValidations: function (newValue) { svc.dataValue(scope, validationFor, newValue); if (validationEnabled) { ngModelController.validationMessages = {}; // Run validations for all of our client-side validation and store in a local array. angular.forEach(validators, function (value, key) { if (!value.validate(newValue, value)) value.fail(); else value.pass(); }); result.populateMessages(); } return newValue; }, cancelSuppress: function () { ngModelController.suppressValidationMessages = false; result.populateMessages(); }, enableSuppress: function () { ngModelController.suppressValidationMessages = true; // don't re-populate the messages here }, fail: function (key, message) { if (validationEnabled) { ngModelController.$setValidity(key, false); ngModelController.validationMessages[key] = message ? $sce.trustAsHtml(message) : (validators[key].message); } }, pass: function (key) { ngModelController.$setValidity(key, true); }, showValidationSummary: false }; validators = buildValidatorsFromAttributes(attrs, result, scope, ngModelController); return result; }, messageArray: function (scope, dotNetName, setter) { if (dotNetName) { if (setter !== undefined) svc.ensureValidation(scope).messages[dotNetName] = setter; return svc.ensureValidation(scope).messages[dotNetName]; } return svc.ensureValidation(scope).messages; }, dataValue: function (scope, dotNetName, setter) { if (dotNetName) { if (setter !== undefined) svc.ensureValidation(scope).data[dotNetName] = setter; return svc.ensureValidation(scope).data[dotNetName]; } return svc.ensureValidation(scope).data; }, hasCancelledSuppress: function (scope) { return svc.ensureValidation(scope).cancelSuppress; }, cancelSuppress: function (scope) { svc.ensureValidation(scope).cancelSuppress = true; }, clearDotNetName: function (scope, dotNetName) { var validation = svc.ensureValidation(scope); delete svc.ensureValidation(scope).messages[dotNetName]; delete svc.ensureValidation(scope).data[dotNetName]; } }; return svc; }]; this.getValidationType = getValidationType; this.addValidator = function (validatorName, validate, inject) { validationTypes[validatorName] = { validate: validate, inject: inject }; }; }]) .directive('val', ['validation', function (validation) { // Attribute to run validation on an element var link = function (scope, element, attrs, ngModelController) { if (attrs['val'] != 'true') return; var validationFor = attrs['name']; // If suppress is true, don't actually display any validation messages. var validators = validation.buildValidation(scope, element, attrs, ngModelController); ngModelController.$parsers.unshift(validators.runValidations); ngModelController.$formatters.unshift(validators.runValidations); var watches = [ // Watch to see if the hasCancelledSuppress is set to true and, if it is, cancel our own suppression. scope.$watch(validation.hasCancelledSuppress, function (newValue) { if (newValue) validators.cancelSuppress(); }) ]; if (attrs['valIf']) { // watch our "valIf" expression and, if it becomse falsy, turn off all of our validations. watches.push(scope.$watch(attrs['valIf'], function (newValue, oldValue) { if (newValue) validators.enable(); else validators.disable(); })); } else { validators.enable(); } // Make sure we dispose all our element.on('$destroy', function () { delete validation.clearDotNetName(scope, validationFor); for (var key in watches) watches[key](); }); // Cancel suppression of error messages for this element on blur element.on('blur', function () { validators.cancelSuppress(); scope.$digest(); }); if (!attrs.hasOwnProperty('valRealtime')) { element.on('focus', function () { validators.enableSuppress(); }); } else { element.on('focus', function () { validators.cancelSuppress(); scope.$digest(); }); } }; return { restrict: 'A', require: 'ngModel', link: link }; }]) .directive('form', ['validation', function (validation) { return { restrict: 'E', link: function (scope) { // Add the $$validation object at the form level so that we don't end up adding it // at an inner level, such as an ng-if. validation.ensureValidation(scope); } }; }]) .directive('valSubmit', ['validation', function (validation) { return { restrict: 'A', require: '^?form', link: function (scope, element, attrs, ctrl) { element.on('click', function ($event) { if (ctrl.$invalid) { $event.preventDefault(); validation.showValidationSummary = true; // Cancels the suppression of validation messages, which reveals error classes, validation summaries, etc. validation.cancelSuppress(scope); scope.$digest(); } }); var watches = [ scope.$watch(function () { return ctrl.$invalid }, function (newValue) { if (newValue) element.addClass('disabled'); else element.removeClass('disabled'); }) ]; element.on('$destroy', function () { for (var key in watches) watches[key](); }); } } }]) .directive('valmsgSummary', ['validation', function (validation) { return { restrict: 'A', scope: {}, template: '
' + ' ' + '
' + '
', transclude: true, link: function (scope, element) { scope.started = false; scope.validationSummary = []; // Here we don't need to dispose our watch because we have an isolated scope that goes away when the element does. var watch = scope.$parent.$watchCollection(validation.messageArray, function (newValue) { if (!validation.showValidationSummary) return; var merged = []; // flatten the nested arrays into "merged" var obj = newValue; angular.forEach(obj, function (value, key) { if (obj.hasOwnProperty(key)) { scope.started = true; angular.forEach(value, function (innerValue) { if (innerValue && merged.indexOf(innerValue) == -1) { merged.push(innerValue); } }); } }); scope.validationSummary = merged; if (scope.started) { if (!merged.length) { element.addClass('validation-summary-valid'); element.removeClass('validation-summary-errors'); } else { element.removeClass('validation-summary-valid'); element.addClass('validation-summary-errors'); } } }); element.on('$destroy', function () { watch(); }); } }; }]) .directive('valBindMessages', ['validation', '$parse', '$sce', function (validation, $parse, $sce) { return { restrict: 'A', link: function (scope, element, attrs) { var model = $parse(attrs.valBindMessages); var disposeWatch = [ scope.$watchCollection(attrs.valBindMessages, function (newValue) { var target = validation.ensureValidation(scope).messages = {}; angular.forEach(newValue, function (entry) { target[entry.memberName] = target[entry.memberName] || []; target[entry.memberName].push($sce.trustAsHtml(entry.text)); }); }) ]; element.on('$destroy', function () { angular.forEach(disposeWatch, function (d) { d(); }); }); } }; }]) .directive('valError', ['validation', function (validation) { return { restrict: 'A', link: function (scope, element, attrs) { var disposeWatch = scope.$watchCollection(function () { return validation.messageArray(scope, attrs['valError']) }, function (newValue) { if (newValue && Object.keys(newValue).length) { element.addClass('error'); } else { element.removeClass('error'); } }); element.on('$destroy', function () { disposeWatch(); }); } }; }]) .directive('valmsgFor', ['validation', function (validation) { return { restrict: 'A', scope: { valmsgFor: '@' }, template: '', transclude: true, link: function (scope, element) { scope.validationSummary = []; // Here we don't need to dispose our watch because we have an isolated scope that goes away when the element does. var watch = scope.$parent.$watchCollection(function () { return validation.messageArray(scope.$parent, scope.valmsgFor) }, function (newValue) { scope.messages = newValue; if (newValue !== undefined) { scope.started = true; } if (scope.started) { if (newValue && !Object.keys(newValue).length) { element.addClass('field-validation-valid'); element.removeClass('field-validation-error'); } else { element.removeClass('field-validation-valid'); element.addClass('field-validation-error'); } } }); element.on('$destroy', function () { watch(); }); } }; }]) .config(['validationProvider', function (validationProvider) { function getModelPrefix(fieldName) { return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); } function appendModelPrefix(value, prefix) { if (value.indexOf("*.") === 0) { value = value.replace("*.", prefix); } return value; } validationProvider.addValidator('required', function (val) { return !!val; }); validationProvider.addValidator('regex', function (val, options) { return !val || new RegExp(options.parameters['pattern']).exec(val); }); validationProvider.addValidator('email', function (val) { // regex taken from jquery.validate v1.12.0 // From http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#e-mail-state-%28type=email%29 // Retrieved 2014-01-14 // If you have a problem with this implementation, report a bug against the above spec // Or use custom methods to implement your own email validation return !val || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(val); }); validationProvider.addValidator("creditcard", function (value) { if (!value) return true; // regex taken from jquery.validate v1.12.0 // accept only spaces, digits and dashes if (/[^0-9 \-]+/.test(value)) { return false; } var nCheck = 0, nDigit = 0, bEven = false, n, cDigit; value = value.replace(/\D/g, ""); // Basing min and max length on // http://developer.ean.com/general_info/Valid_Credit_Card_Types if (value.length < 13 || value.length > 19) { return false; } for (n = value.length - 1; n >= 0; n--) { cDigit = value.charAt(n); nDigit = parseInt(cDigit, 10); if (bEven) { if ((nDigit *= 2) > 9) { nDigit -= 9; } } nCheck += nDigit; bEven = !bEven; } return (nCheck % 10) === 0; }); validationProvider.addValidator("date", function (val) { if (!val) return true; return !/Invalid|NaN/.test(new Date(val).toString()); }); validationProvider.addValidator("digits", function (val) { if (!val) return true; return /^\d+$/.test(val); }); validationProvider.addValidator("number", function (val) { if (!val) return true; return /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(val); }); validationProvider.addValidator("url", function (val) { if (!val) return true; // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ return /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(val); }); validationProvider.addValidator("minlength", function (val, options) { if (!val) return true; return val.length >= parseInt(options.parameters.min); }); validationProvider.addValidator("maxlength", function (val, options) { if (!val) return true; return val.length <= parseInt(options.parameters.max); }); validationProvider.addValidator("length", function (val, options) { if (!val) return true; return (!options.parameters.min || val.length >= parseInt(options.parameters.min)) && (!options.parameters.max || val.length <= parseInt(options.parameters.max)); }); validationProvider.addValidator("range", function (val, options) { if (!val) return true; var value = parseFloat(val); return value <= parseFloat(options.parameters.max) && value >= parseFloat(options.parameters.min); }); validationProvider.addValidator("password", function (val, options) { function nonalphamin(value, min) { var match = value.match(/\W/g); return match && match.length >= min; } if (!val) return true; return (!options.parameters.min || val.length >= options.parameters.min) && (!options.parameters.nonalphamin || nonalphamin(val, options.parameters.nonalphamin)) && (!options.parameters.regex || !!(new RegExp(options.parameters.regex).exec(val))); }); validationProvider.addValidator("equalto", function (val, options) { var prefix = getModelPrefix(options.attributes.name), other = options.parameters.other, fullOtherName = appendModelPrefix(other, prefix), element = options.injected.validation.dataValue(options.scope, fullOtherName); return element == val; }, ['validation']); validationProvider.addValidator("extension", function (val, options) { if (!val) return true; var param = typeof options.parameters.extension == "string" ? options.parameters.extension.replace(/,/g, '|') : "png|jpe?g|gif"; return val.match(new RegExp("\\.(" + param + ")$", "i")); }); validationProvider.addValidator("remote", function (val, options) { if (options.ngModel.remoteTimeout) options.ngModel.remoteTimeout.resolve(); if (!val) return true; var prefix = getModelPrefix(options.attributes.name); var data = {}; data[options.attributes.name] = val; angular.forEach((options.parameters.additionalfields || '').split(','), function (fieldName) { var dataName = appendModelPrefix(fieldName, prefix); data[dataName] = options.injected.validation.dataValue(options.scope, dataName); }); var timeout = options.injected.$q.defer(); options.ngModel.remoteTimeout = timeout; options.injected.$http({ method: options.parameters.type, url: options.parameters.url, data: data, cache: true, // we may want this off... but it should save repeated calls to our back-end timeout: timeout.promise, responseType: "json" }).success(function (response, status) { if (response !== true && response !== "true") { options.fail(response); } }); return true; }, ['validation', '$http', '$q']); }]); })();