diff --git a/dist/jcs-auto-validate.js b/dist/jcs-auto-validate.js index 21cebc1..c817124 100644 --- a/dist/jcs-auto-validate.js +++ b/dist/jcs-auto-validate.js @@ -1,936 +1,1517 @@ /* - * angular-auto-validate - v1.0.17 - 2014-08-07 + * angular-auto-validate - v1.19.3 - 2015-11-30 * https://github.com/jonsamwell/angular-auto-validate - * Copyright (c) 2014 Jon Samwell (http://www.jonsamwell.com) + * Copyright (c) 2015 Jon Samwell (http://www.jonsamwell.com) */ -(function (angular) { +(function (String, angular) { 'use strict'; - angular.module('jcs-autoValidate', []); -}(angular)); +angular.module('jcs-autoValidate', []); -(function (angular) { - 'use strict'; +function ValidatorFn() { + var elementStateModifiers = {}, + enableValidElementStyling = true, + enableInvalidElementStyling = true, + enableFirstInvalidElementScrollingOnSubmit = false, + validationEnabled = true, - angular.module('jcs-autoValidate') - .provider('validator', [ - - function () { - var elementStateModifiers = {}, - enableValidElementStyling = true, - enableInvalidElementStyling = true, - - toBoolean = function (value) { - var v; - if (value && value.length !== 0) { - v = value.toLowerCase(); - value = !(v === 'f' || v === '0' || v === 'false'); - } else { - value = false; - } - - return value; - }, - - getAttributeValue = function (el, attrName) { - var val; - - if (el !== undefined) { - val = el.attr(attrName) || el.attr('data-' + attrName); - } - - return val; - }, - - getBooleanAttributeValue = function (el, attrName) { - return toBoolean(getAttributeValue(el, attrName)); - }, - - validElementStylingEnabled = function (el) { - return enableValidElementStyling && !getBooleanAttributeValue(el, 'disable-valid-styling'); - }, - - invalidElementStylingEnabled = function (el) { - return enableInvalidElementStyling && !getBooleanAttributeValue(el, 'disable-invalid-styling'); - }; - - /** - * @ngdoc function - * @name validator#setDefaultElementModifier - * @methodOf validator - * - * @description - * Sets the default element modifier that will be used by the validator - * to change an elements UI state. Please ensure the modifier has been registered - * before setting it as default. - * - * Note: this can be changed by setting the - * element modifier attribute on the input element 'data-element-modifier="myCustomModifier"' - * - * Example: - *
-                 *  app.config(function (validator) {
-                 *    validator.setDefaultElementModifier('myCustomModifier');
-                 *  });
-                 * 
- * - * @param {string} key The key name of the modifier. - */ - this.setDefaultElementModifier = function (key) { - if (elementStateModifiers[key] === undefined) { - throw new Error('Element modifier not registered: ' + key); - } - - this.defaultElementModifier = key; - }; + toBoolean = function (value) { + var v; + if (value && value.length !== 0) { + v = value.toLowerCase(); + value = !(v === 'f' || v === '0' || v === 'false'); + } else { + value = false; + } - /** - * @ngdoc function - * @name validator#registerDomModifier - * @methodOf validator - * - * @description - * Registers an object that adheres to the elementModifier interface and is - * able to modifier an elements dom so that appears valid / invalid for a specific - * scenario i.e. the Twitter Bootstrap css framework, Foundation CSS framework etc. - * - * Example: - *
-                 *  app.config(function (validator) {
-                 *    validator.registerDomModifier('customDomModifier', {
-                 *      makeValid: function (el) {
-                 *          el.removeClass(el, 'invalid');
-                 *          el.addClass(el, 'valid');
-                 *      },
-                 *      makeInvalid: function (el, err, domManipulator) {
-                 *          el.removeClass(el, 'valid');
-                 *          el.addClass(el, 'invalid');
-                 *      }
-                 *    });
-                 *  });
-                 * 
- * - * @param {string} key The key name of the modifier - * @param {object} modifier An object which implements the elementModifier interface - */ - this.registerDomModifier = function (key, modifier) { - elementStateModifiers[key] = modifier; - }; + return value; + }, - /** - * @ngdoc function - * @name validator#setErrorMessageResolver - * @methodOf validator - * - * @description - * Registers an object that adheres to the elementModifier interface and is - * able to modifier an elements dom so that appears valid / invalid for a specific - * scenario i.e. the Twitter Bootstrap css framework, Foundation CSS framework etc. - * - * Example: - *
-                 *  app.config(function (validator) {
-                 *    validator.setErrorMessageResolver(function (errorKey, el) {
-                 *      var defer = $q.defer();
-                 *      // resolve the correct error from the given key and resolve the returned promise.
-                 *      return defer.promise();
-                 *    });
-                 *  });
-                 * 
- * - * @param {function} resolver A method that returns a promise with the resolved error message in. - */ - this.setErrorMessageResolver = function (resolver) { - this.errorMessageResolver = resolver; - }; + getAttributeValue = function (el, attrName) { + var val; - /** - * @ngdoc function - * @name validator#getErrorMessage - * @methodOf validator - * - * @description - * Resolves the error message for the given error type. - * - * @param {String} errorKey The error type. - * @param {Element} el The UI element that is the focus of the error. - * It is provided as the error message may need information from the element i.e. ng-min (the min allowed value). - */ - this.getErrorMessage = function (errorKey, el) { - if (this.errorMessageResolver === undefined) { - throw new Error('Please set an error message resolver via the setErrorMessageResolver function before attempting to resolve an error message.'); - } - - return this.errorMessageResolver(errorKey, el); - }; + if (el !== undefined) { + val = el.attr(attrName) || el.attr('data-' + attrName); + } - /** - * @ngdoc function - * @name validator#setValidElementStyling - * @methodOf validator - * - * @description - * Globally enables valid element visual styling. This is enabled by default. - * - * @param {Boolean} enabled True to enable style otherwise false. - */ - this.setValidElementStyling = function (enabled) { - enableValidElementStyling = enabled; - }; + return val; + }, + attributeExists = function (el, attrName) { + var exists; - /** - * @ngdoc function - * @name validator#setInvalidElementStyling - * @methodOf validator - * - * @description - * Globally enables invalid element visual styling. This is enabled by default. - * - * @param {Boolean} enabled True to enable style otherwise false. - */ - this.setInvalidElementStyling = function (enabled) { - enableInvalidElementStyling = enabled; - }; + if (el !== undefined) { + exists = el.attr(attrName) !== undefined || el.attr('data-' + attrName) !== undefined; + } - this.getDomModifier = function (el) { - var modifierKey = (el !== undefined ? el.attr('element-modifier') : this.defaultElementModifier) || - (el !== undefined ? el.attr('data-element-modifier') : this.defaultElementModifier) || - this.defaultElementModifier; + return exists; + }, - if (modifierKey === undefined) { - throw new Error('Please set a default dom modifier via the setDefaultElementModifier method on the validator class.'); - } + getBooleanAttributeValue = function (el, attrName) { + return toBoolean(getAttributeValue(el, attrName)); + }, - return elementStateModifiers[modifierKey]; - }; + validElementStylingEnabled = function (el) { + return enableValidElementStyling && !getBooleanAttributeValue(el, 'disable-valid-styling'); + }, - this.makeValid = function (el) { - if (validElementStylingEnabled(el)) { - this.getDomModifier(el).makeValid(el); - } - }; + autoValidateEnabledOnControl = function (el) { + return !getBooleanAttributeValue(el, 'disable-auto-validate'); + }, - this.makeInvalid = function (el, errorMsg) { - if (invalidElementStylingEnabled(el)) { - this.getDomModifier(el).makeInvalid(el, errorMsg); - } - }; + invalidElementStylingEnabled = function (el) { + return enableInvalidElementStyling && !getBooleanAttributeValue(el, 'disable-invalid-styling'); + }; - this.makeDefault = function (el) { - var dm = this.getDomModifier(el); - if (dm.makeDefault) { - dm.makeDefault(el); - } - }; + /** + * @ngdoc function + * @name validator#enable + * @methodOf validator + * + * @description + * By default auto validate will validate all forms and elements with an ngModel directive on. By + * setting enabled to false you will explicitly have to opt in to enable validation on forms and child + * elements. + * + * Note: this can be overridden by add the 'auto-validate-enabled="true/false' attribute to a form. + * + * Example: + *
+   *  app.config(function (validator) {
+   *    validator.enable(false);
+   *  });
+   * 
+ * + * @param {Boolean} isEnabled true to enable, false to disable. + */ + this.enable = function (isEnabled) { + validationEnabled = isEnabled; + }; + + /** + * @ngdoc function + * @name validator#isEnabled + * @methodOf validator + * + * @description + * Returns true if the library is enabeld. + * + * @return {Boolean} true if enabled, otherwise false. + */ + this.isEnabled = function () { + return validationEnabled; + }; + + /** + * @ngdoc function + * @name validator#setDefaultElementModifier + * @methodOf validator + * + * @description + * Sets the default element modifier that will be used by the validator + * to change an elements UI state. Please ensure the modifier has been registered + * before setting it as default. + * + * Note: this can be changed by setting the + * element modifier attribute on the input element 'data-element-modifier="myCustomModifier"' + * + * Example: + *
+   *  app.config(function (validator) {
+   *    validator.setDefaultElementModifier('myCustomModifier');
+   *  });
+   * 
+ * + * @param {string} key The key name of the modifier. + */ + this.setDefaultElementModifier = function (key) { + if (elementStateModifiers[key] === undefined) { + throw new Error('Element modifier not registered: ' + key); + } - this.$get = [ + this.defaultElementModifier = key; + }; + + /** + * @ngdoc function + * @name validator#registerDomModifier + * @methodOf validator + * + * @description + * Registers an object that adheres to the elementModifier interface and is + * able to modifier an elements dom so that appears valid / invalid for a specific + * scenario i.e. the Twitter Bootstrap css framework, Foundation CSS framework etc. + * + * Example: + *
+   *  app.config(function (validator) {
+   *    validator.registerDomModifier('customDomModifier', {
+   *      makeValid: function (el) {
+   *          el.removeClass(el, 'invalid');
+   *          el.addClass(el, 'valid');
+   *      },
+   *      makeInvalid: function (el, err, domManipulator) {
+   *          el.removeClass(el, 'valid');
+   *          el.addClass(el, 'invalid');
+   *      }
+   *    });
+   *  });
+   * 
+ * + * @param {string} key The key name of the modifier + * @param {object} modifier An object which implements the elementModifier interface + */ + this.registerDomModifier = function (key, modifier) { + elementStateModifiers[key] = modifier; + }; + + /** + * @ngdoc function + * @name validator#setErrorMessageResolver + * @methodOf validator + * + * @description + * Registers an object that adheres to the elementModifier interface and is + * able to modifier an elements dom so that appears valid / invalid for a specific + * scenario i.e. the Twitter Bootstrap css framework, Foundation CSS framework etc. + * + * Example: + *
+   *  app.config(function (validator) {
+   *    validator.setErrorMessageResolver(function (errorKey, el) {
+   *      var defer = $q.defer();
+   *      // resolve the correct error from the given key and resolve the returned promise.
+   *      return defer.promise();
+   *    });
+   *  });
+   * 
+ * + * @param {function} resolver A method that returns a promise with the resolved error message in. + */ + this.setErrorMessageResolver = function (resolver) { + this.errorMessageResolver = resolver; + }; + + /** + * @ngdoc function + * @name validator#getErrorMessage + * @methodOf validator + * + * @description + * Resolves the error message for the given error type. + * + * @param {String} errorKey The error type. + * @param {Element} el The UI element that is the focus of the error. + * It is provided as the error message may need information from the element i.e. ng-min (the min allowed value). + */ + this.getErrorMessage = function (errorKey, el) { + var defer; + if (this.errorMessageResolver === undefined) { + throw new Error('Please set an error message resolver via the setErrorMessageResolver function before attempting to resolve an error message.'); + } - function () { - return this; - } - ]; - } - ]); -}(angular)); + if (attributeExists(el, 'disable-validation-message')) { + defer = angular.injector(['ng']).get('$q').defer(); + defer.resolve(''); + return defer.promise; + } else { + return this.errorMessageResolver(errorKey, el); + } + }; + + /** + * @ngdoc function + * @name validator#setValidElementStyling + * @methodOf validator + * + * @description + * Globally enables valid element visual styling. This is enabled by default. + * + * @param {Boolean} enabled True to enable style otherwise false. + */ + this.setValidElementStyling = function (enabled) { + enableValidElementStyling = enabled; + }; + + /** + * @ngdoc function + * @name validator#setInvalidElementStyling + * @methodOf validator + * + * @description + * Globally enables invalid element visual styling. This is enabled by default. + * + * @param {Boolean} enabled True to enable style otherwise false. + */ + this.setInvalidElementStyling = function (enabled) { + enableInvalidElementStyling = enabled; + }; + + /** + * @ngdoc function + * @name validator#setFirstInvalidElementScrollingOnSubmit + * @methodOf validator + * + * @description + * Globally enables first invalid element scrolling on form submit. This is disabled by default. + * + * @param enabled {Boolean} enabled True to enable scrolling otherwise false. + */ + this.setFirstInvalidElementScrollingOnSubmit = function (enabled) { + enableFirstInvalidElementScrollingOnSubmit = enabled; + }; + + this.firstInvalidElementScrollingOnSubmitEnabled = function () { + return enableFirstInvalidElementScrollingOnSubmit; + }; + + + this.getDomModifier = function (el) { + var modifierKey = (el !== undefined ? el.attr('element-modifier') : this.defaultElementModifier) || + (el !== undefined ? el.attr('data-element-modifier') : this.defaultElementModifier) || + this.defaultElementModifier; + + if (modifierKey === undefined) { + throw new Error('Please set a default dom modifier via the setDefaultElementModifier method on the validator class.'); + } -(function (angular) { - 'use strict'; + return elementStateModifiers[modifierKey]; + }; - angular.module('jcs-autoValidate') - .factory('bootstrap3ElementModifier', [ - - function () { - var reset = function (el) { - angular.forEach(el.find('span'), function (spanEl) { - spanEl = angular.element(spanEl); - if (spanEl.hasClass('error-msg') || spanEl.hasClass('form-control-feedback')) { - spanEl.remove(); - } - }); - - el.removeClass('has-success has-error has-feedback'); - }, - findFormGroupElement = function (el) { - var parent = el; - for (var i = 0; i <= 3; i += 1) { - if (parent !== undefined && parent.hasClass('form-group')) { - break; - } else if (parent !== undefined) { - parent = parent.parent(); - } - } - - return parent; - }, - - insertAfter = function (referenceNode, newNode) { - referenceNode[0].parentNode.insertBefore(newNode[0], referenceNode[0].nextSibling); - }, - - /** - * @ngdoc property - * @name bootstrap3ElementModifier#addValidationStateIcons - * @propertyOf bootstrap3ElementModifier - * @returns {bool} True if an state icon will be added to the element in the valid and invalid control - * states. The default is false. - */ - addValidationStateIcons = false, - - /** - * @ngdoc function - * @name bootstrap3ElementModifier#enableValidationStateIcons - * @methodOf bootstrap3ElementModifier - * - * @description - * Makes an element appear invalid by apply an icon to the input element. - * - * @param {bool} enable - True to enable the icon otherwise false. - */ - enableValidationStateIcons = function (enable) { - addValidationStateIcons = enable; - }, - - /** - * @ngdoc function - * @name bootstrap3ElementModifier#makeValid - * @methodOf bootstrap3ElementModifier - * - * @description - * Makes an element appear valid by apply bootstrap 3 specific styles and child elements. If the service - * property 'addValidationStateIcons' is true it will also append validation glyphicon to the element. - * See: http://getbootstrap.com/css/#forms-control-validation - * - * @param {Element} el - The input control element that is the target of the validation. - */ - makeValid = function (el) { - var frmGroupEl = findFormGroupElement(el); - reset(frmGroupEl); - frmGroupEl.addClass('has-success has-feedback'); - if (addValidationStateIcons) { - insertAfter(el, angular.element('')); - } - }, - - /** - * @ngdoc function - * @name bootstrap3ElementModifier#makeInvalid - * @methodOf bootstrap3ElementModifier - * - * @description - * Makes an element appear invalid by apply bootstrap 3 specific styles and child elements. If the service - * property 'addValidationStateIcons' is true it will also append validation glyphicon to the element. - * See: http://getbootstrap.com/css/#forms-control-validation - * - * @param {Element} el - The input control element that is the target of the validation. - */ - makeInvalid = function (el, errorMsg) { - var frmGroupEl = findFormGroupElement(el), - helpTextEl = angular.element('' + errorMsg + ''); - reset(frmGroupEl); - frmGroupEl.addClass('has-error has-feedback'); - insertAfter(el, helpTextEl); - if (addValidationStateIcons) { - insertAfter(el, angular.element('')); - } - }, - - /** - * @ngdoc function - * @name bootstrap3ElementModifier#makeDefault - * @methodOf bootstrap3ElementModifier - * - * @description - * Makes an element appear in its default visual state by apply bootstrap 3 specific styles and child elements. - * - * @param {Element} el - The input control element that is the target of the validation. - */ - makeDefault = function (el) { - var frmGroupEl = findFormGroupElement(el); - reset(frmGroupEl); - }; - - return { - makeValid: makeValid, - makeInvalid: makeInvalid, - makeDefault: makeDefault, - enableValidationStateIcons: enableValidationStateIcons, - key: 'bs3' - }; - } - ]); -}(angular)); + this.makeValid = function (el) { + if (autoValidateEnabledOnControl(el)) { + if (validElementStylingEnabled(el)) { + this.getDomModifier(el).makeValid(el); + } else { + this.makeDefault(el); + } + } + }; + + this.makeInvalid = function (el, errorMsg) { + if (autoValidateEnabledOnControl(el)) { + if (invalidElementStylingEnabled(el)) { + this.getDomModifier(el).makeInvalid(el, errorMsg); + } else { + this.makeDefault(el); + } + } + }; + + this.makeDefault = function (el) { + if (autoValidateEnabledOnControl(el)) { + var dm = this.getDomModifier(el); + if (dm.makeDefault) { + dm.makeDefault(el); + } + } + }; + + this.waitForAsyncValidators = function (el) { + if (autoValidateEnabledOnControl(el)) { + var dm = this.getDomModifier(el); + if (dm.waitForAsyncValidators) { + dm.waitForAsyncValidators(el); + } + } + }; + + this.defaultFormValidationOptions = { + forceValidation: false, + disabled: false, + validateNonVisibleControls: false, + removeExternalValidationErrorsOnSubmit: true, + validateOnFormSubmit: false, + waitForAsyncValidators: true + }; + + this.$get = [ + function () { + return this; + } + ]; +} + +angular.module('jcs-autoValidate').provider('validator', ValidatorFn); + +function Bootstrap3ElementModifierFn($log) { + var customCss = [ + '' + ].join(''); + + angular.element(document.body).append(angular.element(customCss)); + + var reset = function (el) { + angular.forEach(el.find('span'), function (spanEl) { + spanEl = angular.element(spanEl); + if (spanEl.hasClass('error-msg') || spanEl.hasClass('form-control-feedback') || spanEl.hasClass('control-feedback')) { + spanEl.remove(); + } + }); + + el.removeClass('has-success has-error has-feedback'); + }, + findWithClassElementAsc = function (el, klass) { + var returnEl, + parent = el; + for (var i = 0; i <= 10; i += 1) { + if (parent !== undefined && parent.hasClass(klass)) { + returnEl = parent; + break; + } else if (parent !== undefined) { + parent = parent.parent(); + } + } + + return returnEl; + }, + + findWithClassElementDesc = function (el, klass) { + var child; + for (var i = 0; i < el.children.length; i += 1) { + child = el.children[i]; + if (child !== undefined && angular.element(child).hasClass(klass)) { + break; + } else if (child.children !== undefined) { + child = findWithClassElementDesc(child, klass); + if (child.length > 0) { + break; + } + } + } -(function (angular) { - 'use strict'; + return angular.element(child); + }, + + findFormGroupElement = function (el) { + return findWithClassElementAsc(el, 'form-group'); + }, + + findInputGroupElement = function (el) { + return findWithClassElementDesc(el, 'input-group'); + }, + + insertAfter = function (referenceNode, newNode) { + referenceNode[0].parentNode.insertBefore(newNode[0], referenceNode[0].nextSibling); + }, + + /** + * @ngdoc property + * @name bootstrap3ElementModifier#addValidationStateIcons + * @propertyOf bootstrap3ElementModifier + * @returns {bool} True if an state icon will be added to the element in the valid and invalid control + * states. The default is false. + */ + addValidationStateIcons = false, - /* - * Taken from https://github.com/angular/angular.js/issues/2690#issue-14462164 (with added tests of course!) + /** + * @ngdoc function + * @name bootstrap3ElementModifier#enableValidationStateIcons + * @methodOf bootstrap3ElementModifier + * + * @description + * Makes an element appear invalid by apply an icon to the input element. + * + * @param {bool} enable - True to enable the icon otherwise false. */ - angular.module('jcs-autoValidate').factory('debounce', [ - '$timeout', - function ($timeout) { - var debounce = function (fn, timeout, apply) { - timeout = angular.isUndefined(timeout) ? 0 : timeout; - apply = angular.isUndefined(apply) ? true : apply; // !!default is true! most suitable to my experience - var nthCall = 0; - return function () { // intercepting fn - var that = this; - var argz = arguments; - nthCall += 1; - var later = (function (version) { - return function () { - if (version === nthCall) { - return fn.apply(that, argz); - } - }; - })(nthCall); - - return $timeout(later, timeout, apply); - }; - }; + enableValidationStateIcons = function (enable) { + addValidationStateIcons = enable; + }, - return { - debounce: debounce - }; + /** + * @ngdoc function + * @name bootstrap3ElementModifier#makeValid + * @methodOf bootstrap3ElementModifier + * + * @description + * Makes an element appear valid by apply bootstrap 3 specific styles and child elements. If the service + * property 'addValidationStateIcons' is true it will also append validation glyphicon to the element. + * See: http://getbootstrap.com/css/#forms-control-validation + * + * @param {Element} el - The input control element that is the target of the validation. + */ + makeValid = function (el) { + var frmGroupEl = findFormGroupElement(el), + inputGroupEl; + + if (frmGroupEl) { + reset(frmGroupEl); + inputGroupEl = findInputGroupElement(frmGroupEl[0]); + frmGroupEl.addClass('has-success ' + (inputGroupEl.length > 0 || addValidationStateIcons === false ? '' : 'has-feedback')); + if (addValidationStateIcons) { + var iconElText = ''; + if (inputGroupEl.length > 0) { + iconElText = iconElText.replace('form-', ''); + iconElText = '' + iconElText + '' + errorMsg + ''), + inputGroupEl; + + if (frmGroupEl) { + reset(frmGroupEl); + inputGroupEl = findInputGroupElement(frmGroupEl[0]); + frmGroupEl.addClass('has-error ' + (inputGroupEl.length > 0 || addValidationStateIcons === false ? '' : 'has-feedback')); + insertAfter(inputGroupEl.length > 0 ? inputGroupEl : getCorrectElementToPlaceErrorElementAfter(el), helpTextEl); + if (addValidationStateIcons) { + var iconElText = ''; + if (inputGroupEl.length > 0) { + iconElText = iconElText.replace('form-', ''); + iconElText = '' + iconElText + ''; + } + + insertAfter(getCorrectElementToPlaceErrorElementAfter(el), angular.element(iconElText)); + } + } else { + $log.error('Angular-auto-validate: invalid bs3 form structure elements must be wrapped by a form-group class'); + } + }, + + getCorrectElementToPlaceErrorElementAfter = function (el) { + var correctEl = el, + elType = el[0].type ? el[0].type.toLowerCase() : ''; + + if ((elType === 'checkbox' || elType === 'radio') && el.parent()[0].nodeName.toLowerCase() === 'label') { + correctEl = el.parent(); + } + + return correctEl; + }, /** - * Replaces string placeholders with corresponding template string + * @ngdoc function + * @name bootstrap3ElementModifier#makeDefault + * @methodOf bootstrap3ElementModifier + * + * @description + * Makes an element appear in its default visual state by apply bootstrap 3 specific styles and child elements. + * + * @param {Element} el - The input control element that is the target of the validation. */ - if (!('format' in String.prototype)) { - String.prototype.format = function () { - var args = arguments; - return this.replace(/{(\d+)}/g, function (match, number) { - return typeof args[number] !== undefined ? args[number] : match; - }); - }; - } + makeDefault = function (el) { + var frmGroupEl = findFormGroupElement(el); + if (frmGroupEl) { + reset(frmGroupEl); + } else { + $log.error('Angular-auto-validate: invalid bs3 form structure elements must be wrapped by a form-group class'); + } + }, + + waitForAsyncValidators = function (el) { + var frmGroupEl = findFormGroupElement(el), + inputGroupEl; + + if (frmGroupEl) { + reset(frmGroupEl); + inputGroupEl = findInputGroupElement(frmGroupEl[0]); + frmGroupEl.addClass('has-feedback ' + (inputGroupEl.length > 0 || addValidationStateIcons === false ? '' : 'has-feedback')); + if (addValidationStateIcons) { + var iconElText = ''; + if (inputGroupEl.length > 0) { + iconElText = iconElText.replace('form-', ''); + iconElText = '' + iconElText + ''; + } + + insertAfter(el, angular.element(iconElText)); + } + } else { + $log.error('Angular-auto-validate: invalid bs3 form structure elements must be wrapped by a form-group class'); + } + }; + + return { + makeValid: makeValid, + makeInvalid: makeInvalid, + makeDefault: makeDefault, + waitForAsyncValidators: waitForAsyncValidators, + enableValidationStateIcons: enableValidationStateIcons, + key: 'bs3' + }; +} + +Bootstrap3ElementModifierFn.$inject = [ + '$log' +]; + +angular.module('jcs-autoValidate').factory('bootstrap3ElementModifier', Bootstrap3ElementModifierFn); - angular.autoValidate = angular.autoValidate || { - errorMessages: {} +/* + * Taken from https://github.com/angular/angular.js/issues/2690#issue-14462164 (with added tests of course!) + */ +function JCSDebounceFn($timeout) { + var debounce = function (func, wait, immediate) { + var timeout; + return function () { + var context = this; + var args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + + var callNow = immediate && !timeout; + $timeout.cancel(timeout); + timeout = $timeout(later, wait, false); + if (callNow) { + func.apply(context, args); + } }; + }; + + return { + debounce: debounce + }; +} + +JCSDebounceFn.$inject = [ + '$timeout' +]; + +angular.module('jcs-autoValidate').factory('jcs-debounce', JCSDebounceFn); + +/** + * Replaces string placeholders with corresponding template string + */ +if (!('format' in String.prototype)) { + String.prototype.format = function () { + var args = arguments; + return this.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] !== undefined ? args[number] : match; + }); + }; +} + +angular.autoValidate = angular.autoValidate || { + errorMessages: {} +}; + +angular.autoValidate.errorMessages['default'] = { + defaultMsg: 'Please add error message for {0}', + email: 'Please enter a valid email address', + minlength: 'Please enter at least {0} characters', + maxlength: 'You have entered more than the maximum {0} characters', + min: 'Please enter the minimum number of {0}', + max: 'Please enter the maximum number of {0}', + required: 'This field is required', + date: 'Please enter a valid date', + pattern: 'Please ensure the entered information adheres to this pattern {0}', + number: 'Please enter a valid number', + url: 'Please enter a valid URL in the format of http(s)://www.google.com' +}; + +function DefaultErrorMessageResolverFn($q, $http) { + var currentCulture = 'default', + + i18nFileRootPath = 'js/angular-auto-validate/dist/lang', + + cultureRetrievalPromise, + + loadRemoteCulture = function (culture) { + cultureRetrievalPromise = $http.get('{0}/jcs-auto-validate_{1}.json'.format(i18nFileRootPath, culture.toLowerCase())); + return cultureRetrievalPromise; + }, + + /** + * @ngdoc function + * @name defaultErrorMessageResolver#setI18nFileRootPath + * @methodOf defaultErrorMessageResolver + * + * @description + * Set the root path to the il8n files on the server + * + * @param {String} rootPath - The root path on the server to the il8n file - this defaults + * to 'js/angular-auto-validate/lang/' + */ + setI18nFileRootPath = function (rootPath) { + i18nFileRootPath = rootPath; + }, - angular.autoValidate.errorMessages['en-us'] = angular.autoValidate.errorMessages['en-gb'] = { - defaultMsg: 'Please add error message for {0}', - email: 'Please enter a valid email address', - minlength: 'Please enter at least {0} characters', - maxlength: 'You have entered more than the maximum {0} characters', - min: 'Please enter the minimum number of {0}', - max: 'Please enter the maximum number of {0}', - required: 'This field is required', - date: 'Please enter a valid date', - pattern: 'Please ensure the entered information adheres to this pattern {0}', - number: 'Please enter a valid number', - url: 'Please enter a valid URL in the format of http(s)://wwww.google.com' + /** + * @ngdoc function + * @name defaultErrorMessageResolver#setCulture + * @methodOf defaultErrorMessageResolver + * + * @description + * Set the culture for the error messages by loading an the correct culture resource file. + * + * @param {String} culture - The new culture in the format of 'en-gb' etc. + * @param {Function} cultureLoadingFn - A optional function to load the culture resolve which should + * return a promise which is resolved with the culture errorMessage object. If a function is not specified + * the culture file is loaded from the **i18nFileRootPath**. + * @returns {Promise} - A promise which is resolved with the loaded culture error messages object. + */ + setCulture = function (culture, cultureLoadingFn) { + var defer = $q.defer(); + cultureLoadingFn = cultureLoadingFn || loadRemoteCulture; + currentCulture = culture.toLowerCase(); + if (angular.autoValidate.errorMessages[currentCulture] === undefined) { + cultureRetrievalPromise = cultureLoadingFn(culture); + cultureRetrievalPromise.then(function (response) { + cultureRetrievalPromise = undefined; + angular.autoValidate.errorMessages[currentCulture] = response.data; + defer.resolve(angular.autoValidate.errorMessages[currentCulture]); + }, function (err) { + angular.autoValidate.errorMessages[currentCulture] = { + defaultMsg: 'Loading culture failed!' + }; + cultureRetrievalPromise = null; + defer.reject(err); + }); + } else { + defer.resolve(angular.autoValidate.errorMessages[currentCulture]); + } + + return defer.promise; + }, + + getErrorMessages = function (culture) { + var defer = $q.defer(); + culture = culture === undefined ? currentCulture : culture.toLowerCase(); + if (cultureRetrievalPromise !== undefined) { + cultureRetrievalPromise.then(function () { + defer.resolve(angular.autoValidate.errorMessages[culture]); + }, function (err) { + defer.reject(err); + }); + } else { + defer.resolve(angular.autoValidate.errorMessages[culture]); + } + return defer.promise; + }, + + getMessageTypeOverride = function (errorType, el) { + var overrideKey; + + if (el) { + // try and find an attribute which overrides the given error type in the form of errorType-err-type="someMsgKey" + errorType += '-err-type'; + + + overrideKey = el.attr('ng-' + errorType); + if (overrideKey === undefined) { + overrideKey = el.attr('data-ng-' + errorType) || el.attr(errorType); + } + + if (overrideKey) { + overrideKey = overrideKey.replace(/[\W]/g, ''); + } + } + + return overrideKey; + }, + + /** + * @ngdoc function + * @name defaultErrorMessageResolver#resolve + * @methodOf defaultErrorMessageResolver + * + * @description + * Resolves a validate error type into a user validation error message + * + * @param {String} errorType - The type of validation error that has occurred. + * @param {Element} el - The input element that is the source of the validation error. + * @returns {Promise} A promise that is resolved when the validation message has been produced. + */ + resolve = function (errorType, el) { + var defer = $q.defer(), + errMsg, + parameters = [], + parameter, + messageTypeOverride; + + if (cultureRetrievalPromise !== undefined) { + cultureRetrievalPromise.then(function () { + resolve(errorType, el).then(function (msg) { + defer.resolve(msg); + }); + }); + } else { + errMsg = angular.autoValidate.errorMessages[currentCulture][errorType]; + messageTypeOverride = getMessageTypeOverride(errorType, el); + if (messageTypeOverride) { + errMsg = angular.autoValidate.errorMessages[currentCulture][messageTypeOverride]; + } + + if (errMsg === undefined && messageTypeOverride !== undefined) { + errMsg = angular.autoValidate.errorMessages[currentCulture].defaultMsg.format(messageTypeOverride); + } else if (errMsg === undefined) { + errMsg = angular.autoValidate.errorMessages[currentCulture].defaultMsg.format(errorType); + } + + if (el && el.attr) { + try { + parameter = el.attr('ng-' + errorType); + if (parameter === undefined) { + parameter = el.attr('data-ng-' + errorType) || el.attr(errorType); + } + + parameters.push(parameter || ''); + + errMsg = errMsg.format(parameters); + } catch (e) {} + } + + defer.resolve(errMsg); + } + + return defer.promise; }; - angular.module('jcs-autoValidate') - .factory('defaultErrorMessageResolver', [ - '$q', - '$http', - function ($q, $http) { - var currentCulture = 'en-gb', - - i18nFileRootPath = 'js/angular-auto-validate/dist/lang', - - cultureRetrievalPromise, - - loadRemoteCulture = function (culture) { - cultureRetrievalPromise = $http.get('{0}/jcs-auto-validate_{1}.json'.format(i18nFileRootPath, culture.toLowerCase())); - return cultureRetrievalPromise; - }, - - /** - * @ngdoc function - * @name defaultErrorMessageResolver#setI18nFileRootPath - * @methodOf defaultErrorMessageResolver - * - * @description - * Set the root path to the il8n files on the server - * - * @param {String} rootPath - The root path on the server to the il8n file - this defaults - * to 'js/angular-auto-validate/lang/' - */ - setI18nFileRootPath = function (rootPath) { - i18nFileRootPath = rootPath; - }, - - /** - * @ngdoc function - * @name defaultErrorMessageResolver#setCulture - * @methodOf defaultErrorMessageResolver - * - * @description - * Set the culture for the error messages by loading an the correct culture resource file. - * - * @param {String} culture - The new culture in the format of 'en-gb' etc. - * @param {Function} cultureLoadingFn - A optional function to load the culture resolve which should - * return a promise which is resolved with the culture errorMessage object. If a function is not specified - * the culture file is loaded from the **i18nFileRootPath**. - * @returns {Promise} - A promise which is resolved with the loaded culture error messages object. - */ - setCulture = function (culture, cultureLoadingFn) { - var defer = $q.defer(); - cultureLoadingFn = cultureLoadingFn || loadRemoteCulture; - currentCulture = culture.toLowerCase(); - if (angular.autoValidate.errorMessages[currentCulture] === undefined) { - cultureRetrievalPromise = cultureLoadingFn(culture); - cultureRetrievalPromise.then(function (response) { - cultureRetrievalPromise = undefined; - angular.autoValidate.errorMessages[currentCulture] = response.data; - defer.resolve(angular.autoValidate.errorMessages[currentCulture]); - }, function (err) { - angular.autoValidate.errorMessages[currentCulture] = { - defaultMsg: 'Loading culture failed!' - }; - cultureRetrievalPromise = null; - defer.reject(err); - }); - } else { - defer.resolve(angular.autoValidate.errorMessages[currentCulture]); - } - - return defer.promise; - }, - - getErrorMessages = function (culture) { - var defer = $q.defer(); - culture = culture === undefined ? currentCulture : culture.toLowerCase(); - if (cultureRetrievalPromise !== undefined) { - cultureRetrievalPromise.then(function () { - defer.resolve(angular.autoValidate.errorMessages[culture]); - }, function (err) { - defer.reject(err); - }); - } else { - defer.resolve(angular.autoValidate.errorMessages[culture]); - } - return defer.promise; - }, - - getMessageTypeOverride = function (errorType, el) { - var overrideKey; - - if (el) { - // try and find an attribute which overrides the given error type in the form of errorType-err-type="someMsgKey" - errorType += '-err-type'; - - overrideKey = el.attr(errorType); - if (overrideKey === undefined) { - overrideKey = el.attr('data-ng-' + errorType) || el.attr('ng-' + errorType); - } - } - - return overrideKey; - }, - - /** - * @ngdoc function - * @name defaultErrorMessageResolver#resolve - * @methodOf defaultErrorMessageResolver - * - * @description - * Resolves a validate error type into a user validation error message - * - * @param {String} errorType - The type of validation error that has occurred. - * @param {Element} el - The input element that is the source of the validation error. - * @returns {Promise} A promise that is resolved when the validation message has been produced. - */ - resolve = function (errorType, el) { - var defer = $q.defer(), - errMsg, - parameters = [], - parameter, - messageTypeOverride; - - if (cultureRetrievalPromise !== undefined) { - cultureRetrievalPromise.then(function () { - resolve(errorType, el).then(function (msg) { - defer.resolve(msg); - }); - }); - } else { - errMsg = angular.autoValidate.errorMessages[currentCulture][errorType]; - messageTypeOverride = getMessageTypeOverride(errorType, el); - if (messageTypeOverride) { - errMsg = angular.autoValidate.errorMessages[currentCulture][messageTypeOverride]; - } - - if (errMsg === undefined) { - errMsg = angular.autoValidate.errorMessages[currentCulture].defaultMsg.format(errorType); - } - - if (el && el.attr) { - try { - parameter = el.attr(errorType); - if (parameter === undefined) { - parameter = el.attr('data-ng-' + errorType) || el.attr('ng-' + errorType); - } - - parameters.push(parameter || ''); - - errMsg = errMsg.format(parameters); - } catch (e) {} - } - - defer.resolve(errMsg); - } - - return defer.promise; - }; - - return { - setI18nFileRootPath: setI18nFileRootPath, - setCulture: setCulture, - getErrorMessages: getErrorMessages, - resolve: resolve - }; + return { + setI18nFileRootPath: setI18nFileRootPath, + setCulture: setCulture, + getErrorMessages: getErrorMessages, + resolve: resolve + }; +} + +DefaultErrorMessageResolverFn.$inject = [ + '$q', + '$http' +]; + +angular.module('jcs-autoValidate').factory('defaultErrorMessageResolver', DefaultErrorMessageResolverFn); + +function foundation6ElementModifierFn() { + var reset = function (el, inputEl) { + angular.forEach(el.find('small'), function (smallEl) { + if (angular.element(smallEl).hasClass('form-error is-visible')) { + angular.element(smallEl).remove(); + } + }); + + inputEl.removeClass('alert callout'); + }, + findParentColumn = function (el) { + var parent = el; + for (var i = 0; i <= 3; i += 1) { + if (parent !== undefined && (parent.hasClass('columns') || parent.hasClass('column'))) { + break; + } else if (parent !== undefined) { + parent = parent.parent(); + } + } + + return parent; + }, + + /** + * @ngdoc function + * @name foundation6ElementModifier#makeValid + * @methodOf foundation6ElementModifier + * + * @description + * Makes an element appear valid by apply Foundation 6 specific styles and child elements. + * See: http://foundation.zurb.com/sites/docs/forms.html + * + * @param {Element} el - The input control element that is the target of the validation. + */ + makeValid = function (el) { + var parentColumn = findParentColumn(el); + reset(parentColumn && parentColumn.length > 0 ? parentColumn : el, el); + }, + + /** + * @ngdoc function + * @name foundation6ElementModifier#makeInvalid + * @methodOf foundation6ElementModifier + * + * @description + * Makes an element appear invalid by apply Foundation 6 specific styles and child elements. + * See: http://foundation.zurb.com/sites/docs/forms.html + * + * @param {Element} el - The input control element that is the target of the validation. + */ + makeInvalid = function (el, errorMsg) { + var parentColumn = findParentColumn(el), + helpTextEl; + reset(parentColumn || el, el); + el.addClass('alert callout'); + if (parentColumn) { + helpTextEl = angular.element('' + errorMsg + ''); + parentColumn.append(helpTextEl); + } + }, + + /** + * @ngdoc function + * @name foundation6ElementModifier#makeDefault + * @methodOf foundation6ElementModifier + * + * @description + * Makes an element appear in its default visual state by apply Foundation 6 specific styles and child elements. + * + * @param {Element} el - The input control element that is the target of the validation. + */ + makeDefault = function (el) { + makeValid(el); + }; + + return { + makeValid: makeValid, + makeInvalid: makeInvalid, + makeDefault: makeDefault, + key: 'foundation6' + }; +} + +angular.module('jcs-autoValidate').factory('foundation6ElementModifier', foundation6ElementModifierFn); + +function ElementUtilsFn() { + var isElementVisible = function (el) { + return el[0].offsetWidth > 0 && el[0].offsetHeight > 0; + }; + + return { + isElementVisible: isElementVisible + }; +} + +function ValidationManagerFn(validator, elementUtils, $anchorScroll) { + var elementTypesToValidate = ['input', 'textarea', 'select', 'form'], + + elementIsVisible = function (el) { + return elementUtils.isElementVisible(el); + }, + + getFormOptions = function (el) { + var frmCtrl = angular.element(el).controller('form'), + options; + + if (frmCtrl !== undefined && frmCtrl !== null) { + options = frmCtrl.autoValidateFormOptions; + } else { + options = validator.defaultFormValidationOptions; + } + + return options; + }, + + /** + * Only validate if the element is present, it is visible, if it is not a comment, + * it is either a valid user input control (input, select, textare, form) or + * it is a custom control register by the developer. + * @param el + * @param formOptions The validation options of the parent form + * @returns {boolean} true to indicate it should be validated + */ + shouldValidateElement = function (el, formOptions, formSubmitted) { + var elementExists = el && el.length > 0, + isElementAComment = elementExists && el[0].nodeName.toLowerCase() === '#comment', + correctVisibilityToValidate, + correctTypeToValidate, + correctPhaseToValidate; + + if (elementExists && isElementAComment === false) { + correctVisibilityToValidate = elementIsVisible(el) || formOptions.validateNonVisibleControls; + correctTypeToValidate = elementTypesToValidate.indexOf(el[0].nodeName.toLowerCase()) > -1 || + el[0].hasAttribute('register-custom-form-control'); + correctPhaseToValidate = formOptions.validateOnFormSubmit === false || + (formOptions.validateOnFormSubmit === true && formSubmitted === true); + } + + return elementExists && !isElementAComment && correctVisibilityToValidate && correctTypeToValidate && correctPhaseToValidate; + + }, + + /** + * @ngdoc validateElement + * @name validation#validateElement + * @param {object} modelCtrl holds the information about the element e.g. $invalid, $valid + * @param {options} + * - forceValidation if set to true forces the validation even if the element is pristine + * - disabled if set to true forces the validation is disabled and will return true + * - validateNonVisibleControls if set to true forces the validation of non visible element i.e. display:block + * @description + * Validate the form element and make invalid/valid element model status. + * + * As of v1.17.22: + * BREAKING CHANGE to validateElement on the validationManger. The third parameter is now the parent form's + * autoValidateFormOptions object on the form controller. This can be left blank and will be found by the + * validationManager. + */ + validateElement = function (modelCtrl, el, options) { + var isValid = true, + frmOptions = options || getFormOptions(el), + needsValidation = modelCtrl.$pristine === false || frmOptions.forceValidation, + errorType, + findErrorType = function ($errors) { + var keepGoing = true, + errorTypeToReturn; + angular.forEach($errors, function (status, errortype) { + if (keepGoing && status) { + keepGoing = false; + errorTypeToReturn = errortype; } - ]); -}(String, angular)); + }); -(function (angular) { - 'use strict'; + return errorTypeToReturn; + }; - angular.module('jcs-autoValidate') - .factory('foundation5ElementModifier', [ - - function () { - var reset = function (el, inputEl) { - angular.forEach(el.find('small'), function (smallEl) { - if (angular.element(smallEl).hasClass('error')) { - angular.element(smallEl).remove(); - } - }); - - inputEl.removeClass('error'); - }, - findParentColumn = function (el) { - var parent = el; - for (var i = 0; i <= 3; i += 1) { - if (parent !== undefined && parent.hasClass('columns')) { - break; - } else if (parent !== undefined) { - parent = parent.parent(); - } - } - - return parent; - }, - - /** - * @ngdoc function - * @name foundation5ElementModifier#makeValid - * @methodOf foundation5ElementModifier - * - * @description - * Makes an element appear valid by apply Foundation 5 specific styles and child elements. - * See: http://foundation.zurb.com/docs/components/forms.html - * - * @param {Element} el - The input control element that is the target of the validation. - */ - makeValid = function (el) { - var parentColumn = findParentColumn(el); - reset(parentColumn && parentColumn.length > 0 ? parentColumn : el, el); - }, - - /** - * @ngdoc function - * @name foundation5ElementModifier#makeInvalid - * @methodOf foundation5ElementModifier - * - * @description - * Makes an element appear invalid by apply Foundation 5 specific styles and child elements. - * See: http://foundation.zurb.com/docs/components/forms.html - * - * @param {Element} el - The input control element that is the target of the validation. - */ - makeInvalid = function (el, errorMsg) { - var parentColumn = findParentColumn(el), - helpTextEl; - reset(parentColumn || el, el); - el.addClass('error'); - if (parentColumn) { - helpTextEl = angular.element('' + errorMsg + ''); - parentColumn.append(helpTextEl); - } - }, - - /** - * @ngdoc function - * @name foundation5ElementModifier#makeDefault - * @methodOf foundation5ElementModifier - * - * @description - * Makes an element appear in its default visual state by apply foundation 5 specific styles and child elements. - * - * @param {Element} el - The input control element that is the target of the validation. - */ - makeDefault = function (el) { - makeValid(el); - }; - - return { - makeValid: makeValid, - makeInvalid: makeInvalid, - makeDefault: makeDefault, - key: 'foundation5' - }; + if (frmOptions.disabled === false) { + if ((frmOptions.forceValidation || + (shouldValidateElement(el, frmOptions, frmOptions.getFormController().$submitted) && + modelCtrl && + needsValidation))) { + isValid = !modelCtrl.$invalid; + + if (frmOptions.removeExternalValidationErrorsOnSubmit && modelCtrl.removeAllExternalValidation) { + modelCtrl.removeAllExternalValidation(); + } + + if (modelCtrl.$pending !== undefined && options.waitForAsyncValidators === true) { + // we have pending async validators + validator.waitForAsyncValidators(el); + } else { + if (isValid) { + validator.makeValid(el); + } else { + errorType = findErrorType(modelCtrl.$errors || modelCtrl.$error); + if (errorType === undefined) { + // we have a weird situation some users are encountering where a custom control + // is valid but the ngModel is report it isn't and thus no valid error type can be found + isValid = true; + } else { + validator.getErrorMessage(errorType, el).then(function (errorMsg) { + validator.makeInvalid(el, errorMsg); + }); + } + } + } + } + } + + return isValid; + }, + + resetElement = function (element) { + validator.makeDefault(element); + }, + + resetForm = function (frmElement) { + angular.forEach((frmElement[0].all || frmElement[0].elements) || frmElement[0], function (element) { + var controller, + ctrlElement = angular.element(element); + controller = ctrlElement.controller('ngModel'); + + if (controller !== undefined) { + if (ctrlElement[0].nodeName.toLowerCase() === 'form') { + // we probably have a sub form + resetForm(ctrlElement); + } else { + controller.$setPristine(); + } + } + }); + }, + + validateForm = function (frmElement) { + var frmValid = true, + frmCtrl = frmElement ? angular.element(frmElement).controller('form') : undefined, + processElement = function (ctrlElement, force, formOptions) { + var controller, isValid, ctrlFormOptions, originalForceValue; + + ctrlElement = angular.element(ctrlElement); + controller = ctrlElement.controller('ngModel'); + + if (controller !== undefined && (force || shouldValidateElement(ctrlElement, formOptions, frmCtrl.$submitted))) { + if (ctrlElement[0].nodeName.toLowerCase() === 'form') { + // we probably have a sub form + validateForm(ctrlElement); + } else { + // we need to get the options for the element rather than use the passed in as the + // element could be an ng-form and have different options to the parent form. + ctrlFormOptions = getFormOptions(ctrlElement); + originalForceValue = ctrlFormOptions.forceValidation; + ctrlFormOptions.forceValidation = force; + try { + isValid = validateElement(controller, ctrlElement, ctrlFormOptions); + if (validator.firstInvalidElementScrollingOnSubmitEnabled() && !isValid && frmValid) { + var ctrlElementId = ctrlElement.attr('id'); + if (ctrlElementId) { + $anchorScroll(ctrlElementId); + } + } + frmValid = frmValid && isValid; + } finally { + ctrlFormOptions.forceValidation = originalForceValue; + } } - ]); -}(angular)); + } + }, + clonedOptions; + + if (frmElement === undefined || (frmCtrl !== undefined && frmCtrl.autoValidateFormOptions.disabled)) { + return frmElement !== undefined; + } + + //force the validation of controls + clonedOptions = angular.copy(frmCtrl.autoValidateFormOptions); + clonedOptions.forceValidation = true; + + // IE8 holds the child controls collection in the all property + // Firefox in the elements and chrome as a child iterator + angular.forEach((frmElement[0].elements || frmElement[0].all) || frmElement[0], function (ctrlElement) { + processElement(ctrlElement, true, clonedOptions); + }); + + // If you have a custom form control that should be validated i.e. + // ... it will not be part of the forms + // HTMLFormControlsCollection and thus won't be included in the above element iteration although + // it will be on the Angular FormController (if it has a name attribute). So adding the directive + // register-custom-form-control="" to the control root and autoValidate will include it in this + // iteration. + if (frmElement[0].customHTMLFormControlsCollection) { + angular.forEach(frmElement[0].customHTMLFormControlsCollection, function (ctrlElement) { + // need to force the validation as the element might not be a known form input type + // so the normal validation process will ignore it. + processElement(ctrlElement, true, clonedOptions); + }); + } + + return frmValid; + }, + + setElementValidationError = function (element, errorMsgKey, errorMsg) { + if (errorMsgKey) { + validator.getErrorMessage(errorMsgKey, element).then(function (msg) { + validator.makeInvalid(element, msg); + }); + } else { + validator.makeInvalid(element, errorMsg); + } + }; -(function (angular) { - 'use strict'; + return { + setElementValidationError: setElementValidationError, + validateElement: validateElement, + validateForm: validateForm, + resetElement: resetElement, + resetForm: resetForm + }; +} + +ValidationManagerFn.$inject = [ + 'validator', + 'jcs-elementUtils', + '$anchorScroll' +]; + +angular.module('jcs-autoValidate').factory('jcs-elementUtils', ElementUtilsFn); +angular.module('jcs-autoValidate').factory('validationManager', ValidationManagerFn); + +function parseBooleanAttributeValue(val, defaultValue) { + if ((val === undefined || val === null) && defaultValue !== undefined) { + return defaultValue; + } else { + return val !== 'false'; + } +} + +function parseOptions(ctrl, validator, attrs) { + var opts = ctrl.autoValidateFormOptions = ctrl.autoValidateFormOptions || angular.copy(validator.defaultFormValidationOptions); + + // needed to stop circular ref in json serialisation + opts.getFormController = function () { + return ctrl; + }; + opts.waitForAsyncValidators = parseBooleanAttributeValue(attrs.waitForAsyncValidators, opts.waitForAsyncValidators); + opts.forceValidation = false; + opts.disabled = !validator.isEnabled() || parseBooleanAttributeValue(attrs.disableDynamicValidation, opts.disabled); + opts.validateNonVisibleControls = parseBooleanAttributeValue(attrs.validateNonVisibleControls, opts.validateNonVisibleControls); + opts.validateOnFormSubmit = parseBooleanAttributeValue(attrs.validateOnFormSubmit, opts.validateOnFormSubmit); + opts.removeExternalValidationErrorsOnSubmit = attrs.removeExternalValidationErrorsOnSubmit === undefined ? + opts.removeExternalValidationErrorsOnSubmit : + parseBooleanAttributeValue(attrs.removeExternalValidationErrorsOnSubmit, opts.removeExternalValidationErrorsOnSubmit); + + // the library might be globally disabled but enabled on a particular form so check the + // disableDynamicValidation attribute is on the form + if (validator.isEnabled() === false && attrs.disableDynamicValidation === 'false') { + opts.disabled = false; + } +} + +angular.module('jcs-autoValidate').directive('form', [ + 'validator', + function (validator) { + return { + restrict: 'E', + require: 'form', + priority: 9999, + compile: function () { + return { + pre: function (scope, element, attrs, ctrl) { + parseOptions(ctrl, validator, attrs); + } + }; + } + }; + } +]); + +angular.module('jcs-autoValidate').directive('ngForm', [ + 'validator', + function (validator) { + return { + restrict: 'EA', + require: 'form', + priority: 9999, + compile: function () { + return { + pre: function (scope, element, attrs, ctrl) { + parseOptions(ctrl, validator, attrs); + } + }; + } + }; + } +]); + +function FormResetDirectiveFn(validationManager) { + return { + restrict: 'E', + link: function (scope, el) { + var formController = el.controller('form'); + + function resetFn() { + validationManager.resetForm(el); + if (formController.$setPristine) { + formController.$setPristine(); + } - angular.module('jcs-autoValidate') - .factory('validationManager', [ - 'validator', - function (validator) { - var - /** - * @ngdoc validateElement - * @name validation#validateElement - * @param {object} modelCtrl holds the information about the element e.g. $invalid, $valid - * @param {Boolean} forceValidation if set to true forces the validation even if the element is pristine - * @description - * Validate the form element and make invalid/valid element model status. - */ - validateElement = function (modelCtrl, el, forceValidation) { - var isValid = true, - needsValidation = modelCtrl.$pristine === false || forceValidation, - errorType, - findErrorType = function ($errors) { - var keepGoing = true, - errorTypeToReturn; - angular.forEach($errors, function (status, errortype) { - if (keepGoing && status) { - keepGoing = false; - errorTypeToReturn = errortype; - } - }); - - return errorTypeToReturn; - }; - - if (modelCtrl && needsValidation) { - isValid = !modelCtrl.$invalid; - if (isValid) { - validator.makeValid(el); - } else { - errorType = findErrorType(modelCtrl.$error); - - validator.getErrorMessage(errorType, el).then(function (errorMsg) { - validator.makeInvalid(el, errorMsg); - }); - } - } - - return isValid; - }, - - resetElement = function (element) { - validator.makeDefault(element); - }, - - resetForm = function (frmElement) { - angular.forEach(frmElement[0], function (element) { - var controller, - ctrlElement = angular.element(element); - controller = ctrlElement.controller('ngModel'); - - if (controller !== undefined) { - if (ctrlElement[0].nodeName === 'FORM') { - // we probably have a sub form - resetForm(ctrlElement); - } else { - controller.$setPristine(); - } - } - }); - }, - - validateForm = function (frmElement) { - var frmValid = true; - if (frmElement === undefined) { - return false; - } - - angular.forEach(frmElement[0], function (ctrlElement) { - var controller, isValid; - ctrlElement = angular.element(ctrlElement); - controller = ctrlElement.controller('ngModel'); - - if (controller !== undefined) { - if (ctrlElement[0].nodeName === 'FORM') { - // we probably have a sub form - validateForm(ctrlElement); - } else { - isValid = validateElement(controller, ctrlElement, true); - frmValid = frmValid && isValid; - } - } - }); - - return frmValid; - }; - - return { - validateElement: validateElement, - validateForm: validateForm, - resetElement: resetElement, - resetForm: resetForm - }; + if (formController.$setUntouched) { + formController.$setUntouched(); + } + } + + if (formController !== undefined && + formController.autoValidateFormOptions && + formController.autoValidateFormOptions.disabled === false) { + el.on('reset', resetFn); + + scope.$on('$destroy', function () { + el.off('reset', resetFn); + }); + } + } + }; +} + +FormResetDirectiveFn.$inject = [ + 'validationManager' +]; + +angular.module('jcs-autoValidate').directive('form', FormResetDirectiveFn); + +function RegisterCustomFormControlFn() { + var findParentForm = function (el) { + var parent = el; + for (var i = 0; i <= 50; i += 1) { + if (parent !== undefined && parent.nodeName.toLowerCase() === 'form') { + break; + } else if (parent !== undefined) { + parent = angular.element(parent).parent()[0]; + } + } + + return parent; + }; + + return { + restrict: 'A', + link: function (scope, element) { + var frmEl = findParentForm(element.parent()[0]); + if (frmEl) { + frmEl.customHTMLFormControlsCollection = frmEl.customHTMLFormControlsCollection || []; + frmEl.customHTMLFormControlsCollection.push(element[0]); + } + } + }; +} + +angular.module('jcs-autoValidate').directive('registerCustomFormControl', RegisterCustomFormControlFn); + +function SubmitDecorator($delegate, $parse, validationManager) { + $delegate[0].compile = function ($element, attrs) { + var fn = $parse(attrs.ngSubmit), + force = attrs.ngSubmitForce === 'true'; + + return function (scope, element) { + var formController = element.controller('form'), + resetListenerOffFn; + + function handlerFn(event) { + scope.$apply(function () { + if (formController !== undefined && + formController !== null && + formController.autoValidateFormOptions && + formController.autoValidateFormOptions.disabled === true) { + fn(scope, { + $event: event + }); + } else { + if (formController.$setSubmitted === undefined) { + // we probably have angular <= 1.2 + formController.$submitted = true; } - ]); -}(angular)); -(function (angular) { - 'use strict'; + if (validationManager.validateForm(element) || force === true) { + fn(scope, { + $event: event + }); + } + } + }); + } + + function resetFormFn() { + if (element[0].reset) { + element[0].reset(); + } else { + validationManager.resetForm(element); + } + } + + if (formController && formController.autoValidateFormOptions) { + // allow the form to be reset programatically or via raising the event + // form:formName:reset + formController.autoValidateFormOptions.resetForm = resetFormFn; + if (formController.$name !== undefined && formController.$name !== '') { + resetListenerOffFn = scope.$on('form:' + formController.$name + ':reset', resetFormFn); + } + } - angular.module('jcs-autoValidate').directive('form', [ - 'validationManager', - function (validationManager) { - return { - restrict: 'E', - link: function (scope, el) { - el.on('reset', function () { - validationManager.resetForm(el); - }); - - scope.$on('$destroy', function () { - el.off('reset'); - }); - } - }; + element.on('submit', handlerFn); + scope.$on('$destroy', function () { + element.off('submit', handlerFn); + if (resetListenerOffFn) { + resetListenerOffFn(); } - ]); -}(angular)); + }); + }; + }; + + return $delegate; +} + +SubmitDecorator.$inject = [ + '$delegate', + '$parse', + 'validationManager' +]; + +function ProviderFn($provide) { + $provide.decorator('ngSubmitDirective', SubmitDecorator); +} + +ProviderFn.$inject = [ + '$provide' +]; + +angular.module('jcs-autoValidate').config(ProviderFn); + +angular.module('jcs-autoValidate').config(['$provide', + function ($provide) { + $provide.decorator('ngModelDirective', [ + '$timeout', + '$delegate', + 'validationManager', + 'jcs-debounce', + function ($timeout, $delegate, validationManager, debounce) { + var directive = $delegate[0], + link = directive.link || directive.compile; + + directive.compile = function (el) { + var supportsNgModelOptions = angular.version.major >= 1 && angular.version.minor >= 3, + originalLink = link; + + // in the RC of 1.3 there is no directive.link only the directive.compile which + // needs to be invoked to get at the link functions. + if (supportsNgModelOptions && angular.isFunction(link)) { + originalLink = link(el); + } + + return { + pre: function (scope, element, attrs, ctrls) { + var ngModelCtrl = ctrls[0], + frmCtrl = ctrls[1], + ngModelOptions = attrs.ngModelOptions === undefined ? undefined : scope.$eval(attrs.ngModelOptions), + setValidity = ngModelCtrl.$setValidity, + setPristine = ngModelCtrl.$setPristine, + setValidationState = debounce.debounce(function () { + var validateOptions = frmCtrl !== undefined && frmCtrl !== null ? frmCtrl.autoValidateFormOptions : undefined; + validationManager.validateElement(ngModelCtrl, element, validateOptions); + }, 100); + + if (attrs.formnovalidate === undefined && + (frmCtrl !== undefined && frmCtrl !== null && frmCtrl.autoValidateFormOptions && + frmCtrl.autoValidateFormOptions.disabled === false)) { + // if the version of angular supports ng-model-options let angular handle the element.on bit + // fixes issue with async validators + if (supportsNgModelOptions || + (!supportsNgModelOptions || ngModelOptions === undefined || ngModelOptions.updateOn === undefined || ngModelOptions.updateOn === '')) { + ngModelCtrl.$setValidity = function (validationErrorKey, isValid) { + setValidity.call(ngModelCtrl, validationErrorKey, isValid); + setValidationState(); + }; + } else { + element.on(ngModelOptions.updateOn, function () { + setValidationState(); + }); + + scope.$on('$destroy', function () { + element.off(ngModelOptions.updateOn); + }); + } -(function (angular) { - 'use strict'; + // We override this so we can reset the element state when it is called. + ngModelCtrl.$setPristine = function () { + setPristine.call(ngModelCtrl); + validationManager.resetElement(element); + }; + + ngModelCtrl.autoValidated = true; + } - angular.module('jcs-autoValidate').config(['$provide', - function ($provide) { - $provide.decorator('ngSubmitDirective', [ - '$delegate', - '$parse', - 'validationManager', - function ($delegate, $parse, validationManager) { - $delegate[0].compile = function ($element, attr) { - var fn = $parse(attr.ngSubmit), - force = attr.ngSubmitForce === 'true'; - return function (scope, element) { - element.on('submit', function (event) { - scope.$apply(function () { - if (force === true || validationManager.validateForm(element)) { - fn(scope, { - $event: event - }); - } - }); - }); - }; - }; - - return $delegate; + ngModelCtrl.setExternalValidation = function (errorMsgKey, errorMessage, addToModelErrors) { + if (addToModelErrors) { + var collection = ngModelCtrl.$error || ngModelCtrl.$errors; + collection[errorMsgKey] = false; } - ]); - } - ]); -}(angular)); -(function (angular) { - 'use strict'; + ngModelCtrl.externalErrors = ngModelCtrl.externalErrors || {}; + ngModelCtrl.externalErrors[errorMsgKey] = false; + validationManager.setElementValidationError(element, errorMsgKey, errorMessage); + }; - angular.module('jcs-autoValidate').config(['$provide', - function ($provide) { - $provide.decorator('ngModelDirective', [ - '$timeout', - '$delegate', - 'validationManager', - 'debounce', - function ($timeout, $delegate, validationManager, debounce) { - var directive = $delegate[0], - link = directive.link; - - directive.compile = function () { - return function (scope, element, attrs, ctrls) { - var ngModelCtrl = ctrls[0], - supportsNgModelOptions = angular.version.major >= 1 && angular.version.minor >= 3, - ngModelOptions = attrs.ngModelOptions === undefined ? undefined : scope.$eval(attrs.ngModelOptions), - setValidity = ngModelCtrl.$setValidity, - setPristine = ngModelCtrl.$setPristine, - setValidationState = debounce.debounce(function () { - validationManager.validateElement(ngModelCtrl, element); - }, 100); - - - if (link.pre) { - link.pre.apply(this, arguments); - ngModelOptions = ngModelCtrl.$options === undefined ? undefined : ngModelCtrl.$options; - } - - if (attrs.formnovalidate === undefined) { - if (supportsNgModelOptions || ngModelOptions === undefined || ngModelOptions.updateOn === undefined || ngModelOptions.updateOn === '') { - ngModelCtrl.$setValidity = function (validationErrorKey, isValid) { - setValidity.call(ngModelCtrl, validationErrorKey, isValid); - setValidationState(); - }; - } else { - element.on(ngModelOptions.updateOn, function () { - setValidationState(); - }); - - scope.$on('$destroy', function () { - element.off(ngModelOptions.updateOn); - }); - } - - // We override this so - ngModelCtrl.$setPristine = function () { - setPristine.call(ngModelCtrl); - validationManager.resetElement(element); - }; - - ngModelCtrl.autoValidated = true; - } - - if (link.post) { - link.post.apply(this, arguments); - } else { - link.apply(this, arguments); - } - }; - }; - - return $delegate; + ngModelCtrl.removeExternalValidation = function (errorMsgKey, addToModelErrors) { + if (addToModelErrors) { + var collection = ngModelCtrl.$error || ngModelCtrl.$errors; + delete collection[errorMsgKey]; } - ]); - } - ]); -}(angular)); -(function (angular) { - 'use strict'; + if (ngModelCtrl.externalErrors) { + delete ngModelCtrl.externalErrors[errorMsgKey]; + } + + validationManager.resetElement(element); + }; + + ngModelCtrl.removeAllExternalValidation = function () { + if (ngModelCtrl.externalErrors) { + var errorCollection = ngModelCtrl.$error || ngModelCtrl.$errors; + angular.forEach(ngModelCtrl.externalErrors, function (value, key) { + delete errorCollection[key]; + }); + + ngModelCtrl.externalErrors = {}; + + validationManager.resetElement(element); + } + }; + + if (frmCtrl) { + frmCtrl.setExternalValidation = function (modelProperty, errorMsgKey, errorMessageOverride, addToModelErrors) { + var success = false; + if (frmCtrl[modelProperty]) { + frmCtrl[modelProperty].setExternalValidation(errorMsgKey, errorMessageOverride, addToModelErrors); + success = true; + } + + return success; + }; + + frmCtrl.removeExternalValidation = function (modelProperty, errorMsgKey, errorMessageOverride, addToModelErrors) { + var success = false; + if (frmCtrl[modelProperty]) { + frmCtrl[modelProperty].removeExternalValidation(errorMsgKey, addToModelErrors); + success = true; + } - angular.module('jcs-autoValidate') - .run([ - 'validator', - 'defaultErrorMessageResolver', - 'bootstrap3ElementModifier', - 'foundation5ElementModifier', - function (validator, defaultErrorMessageResolver, bootstrap3ElementModifier, foundation5ElementModifier) { - validator.setErrorMessageResolver(defaultErrorMessageResolver.resolve); - validator.registerDomModifier(bootstrap3ElementModifier.key, bootstrap3ElementModifier); - validator.registerDomModifier(foundation5ElementModifier.key, foundation5ElementModifier); - validator.setDefaultElementModifier(bootstrap3ElementModifier.key); + return success; + }; + } + + return originalLink.pre ? + originalLink.pre.apply(this, arguments) : + this; + }, + post: function (scope, element, attrs, ctrls) { + return originalLink.post ? + originalLink.post.apply(this, arguments) : + originalLink.apply(this, arguments); } - ]); -}(angular)); + }; + }; + + return $delegate; + } + ]); + } +]); + +function AutoValidateRunFn(validator, defaultErrorMessageResolver, bootstrap3ElementModifier, foundation6ElementModifier) { + validator.setErrorMessageResolver(defaultErrorMessageResolver.resolve); + validator.registerDomModifier(bootstrap3ElementModifier.key, bootstrap3ElementModifier); + validator.registerDomModifier(foundation6ElementModifier.key, foundation6ElementModifier); + validator.setDefaultElementModifier(bootstrap3ElementModifier.key); +} + +AutoValidateRunFn.$inject = [ + 'validator', + 'defaultErrorMessageResolver', + 'bootstrap3ElementModifier', + 'foundation6ElementModifier' +]; + +angular.module('jcs-autoValidate').run(AutoValidateRunFn); + +}(String, angular)); diff --git a/dist/jcs-auto-validate.min.js b/dist/jcs-auto-validate.min.js index e812671..a2c8278 100644 --- a/dist/jcs-auto-validate.min.js +++ b/dist/jcs-auto-validate.min.js @@ -1,6 +1 @@ -/* - * angular-auto-validate - v1.0.17 - 2014-08-07 - * https://github.com/jonsamwell/angular-auto-validate - * Copyright (c) 2014 Jon Samwell (http://www.jonsamwell.com) - */ -!function(a){"use strict";a.module("jcs-autoValidate",[])}(angular),function(a){"use strict";a.module("jcs-autoValidate").provider("validator",[function(){var a={},b=!0,c=!0,d=function(a){var b;return a&&0!==a.length?(b=a.toLowerCase(),a=!("f"===b||"0"===b||"false"===b)):a=!1,a},e=function(a,b){var c;return void 0!==a&&(c=a.attr(b)||a.attr("data-"+b)),c},f=function(a,b){return d(e(a,b))},g=function(a){return b&&!f(a,"disable-valid-styling")},h=function(a){return c&&!f(a,"disable-invalid-styling")};this.setDefaultElementModifier=function(b){if(void 0===a[b])throw new Error("Element modifier not registered: "+b);this.defaultElementModifier=b},this.registerDomModifier=function(b,c){a[b]=c},this.setErrorMessageResolver=function(a){this.errorMessageResolver=a},this.getErrorMessage=function(a,b){if(void 0===this.errorMessageResolver)throw new Error("Please set an error message resolver via the setErrorMessageResolver function before attempting to resolve an error message.");return this.errorMessageResolver(a,b)},this.setValidElementStyling=function(a){b=a},this.setInvalidElementStyling=function(a){c=a},this.getDomModifier=function(b){var c=(void 0!==b?b.attr("element-modifier"):this.defaultElementModifier)||(void 0!==b?b.attr("data-element-modifier"):this.defaultElementModifier)||this.defaultElementModifier;if(void 0===c)throw new Error("Please set a default dom modifier via the setDefaultElementModifier method on the validator class.");return a[c]},this.makeValid=function(a){g(a)&&this.getDomModifier(a).makeValid(a)},this.makeInvalid=function(a,b){h(a)&&this.getDomModifier(a).makeInvalid(a,b)},this.makeDefault=function(a){var b=this.getDomModifier(a);b.makeDefault&&b.makeDefault(a)},this.$get=[function(){return this}]}])}(angular),function(a){"use strict";a.module("jcs-autoValidate").factory("bootstrap3ElementModifier",[function(){var b=function(b){a.forEach(b.find("span"),function(b){b=a.element(b),(b.hasClass("error-msg")||b.hasClass("form-control-feedback"))&&b.remove()}),b.removeClass("has-success has-error has-feedback")},c=function(a){for(var b=a,c=0;3>=c&&(void 0===b||!b.hasClass("form-group"));c+=1)void 0!==b&&(b=b.parent());return b},d=function(a,b){a[0].parentNode.insertBefore(b[0],a[0].nextSibling)},e=!1,f=function(a){e=a},g=function(f){var g=c(f);b(g),g.addClass("has-success has-feedback"),e&&d(f,a.element(''))},h=function(f,g){var h=c(f),i=a.element(''+g+"");b(h),h.addClass("has-error has-feedback"),d(f,i),e&&d(f,a.element(''))},i=function(a){var d=c(a);b(d)};return{makeValid:g,makeInvalid:h,makeDefault:i,enableValidationStateIcons:f,key:"bs3"}}])}(angular),function(a){"use strict";a.module("jcs-autoValidate").factory("debounce",["$timeout",function(b){var c=function(c,d,e){d=a.isUndefined(d)?0:d,e=a.isUndefined(e)?!0:e;var f=0;return function(){var a=this,g=arguments;f+=1;var h=function(b){return function(){return b===f?c.apply(a,g):void 0}}(f);return b(h,d,e)}};return{debounce:c}}])}(angular),function(a,b){"use strict";"format"in a.prototype||(a.prototype.format=function(){var a=arguments;return this.replace(/{(\d+)}/g,function(b,c){return void 0!==typeof a[c]?a[c]:b})}),b.autoValidate=b.autoValidate||{errorMessages:{}},b.autoValidate.errorMessages["en-us"]=b.autoValidate.errorMessages["en-gb"]={defaultMsg:"Please add error message for {0}",email:"Please enter a valid email address",minlength:"Please enter at least {0} characters",maxlength:"You have entered more than the maximum {0} characters",min:"Please enter the minimum number of {0}",max:"Please enter the maximum number of {0}",required:"This field is required",date:"Please enter a valid date",pattern:"Please ensure the entered information adheres to this pattern {0}",number:"Please enter a valid number",url:"Please enter a valid URL in the format of http(s)://wwww.google.com"},b.module("jcs-autoValidate").factory("defaultErrorMessageResolver",["$q","$http",function(a,c){var d,e="en-gb",f="js/angular-auto-validate/dist/lang",g=function(a){return d=c.get("{0}/jcs-auto-validate_{1}.json".format(f,a.toLowerCase()))},h=function(a){f=a},i=function(c,f){var h=a.defer();return f=f||g,e=c.toLowerCase(),void 0===b.autoValidate.errorMessages[e]?(d=f(c),d.then(function(a){d=void 0,b.autoValidate.errorMessages[e]=a.data,h.resolve(b.autoValidate.errorMessages[e])},function(a){b.autoValidate.errorMessages[e]={defaultMsg:"Loading culture failed!"},d=null,h.reject(a)})):h.resolve(b.autoValidate.errorMessages[e]),h.promise},j=function(c){var f=a.defer();return c=void 0===c?e:c.toLowerCase(),void 0!==d?d.then(function(){f.resolve(b.autoValidate.errorMessages[c])},function(a){f.reject(a)}):f.resolve(b.autoValidate.errorMessages[c]),f.promise},k=function(a,b){var c;return b&&(a+="-err-type",c=b.attr(a),void 0===c&&(c=b.attr("data-ng-"+a)||b.attr("ng-"+a))),c},l=function(c,f){var g,h,i,j=a.defer(),m=[];if(void 0!==d)d.then(function(){l(c,f).then(function(a){j.resolve(a)})});else{if(g=b.autoValidate.errorMessages[e][c],i=k(c,f),i&&(g=b.autoValidate.errorMessages[e][i]),void 0===g&&(g=b.autoValidate.errorMessages[e].defaultMsg.format(c)),f&&f.attr)try{h=f.attr(c),void 0===h&&(h=f.attr("data-ng-"+c)||f.attr("ng-"+c)),m.push(h||""),g=g.format(m)}catch(n){}j.resolve(g)}return j.promise};return{setI18nFileRootPath:h,setCulture:i,getErrorMessages:j,resolve:l}}])}(String,angular),function(a){"use strict";a.module("jcs-autoValidate").factory("foundation5ElementModifier",[function(){var b=function(b,c){a.forEach(b.find("small"),function(b){a.element(b).hasClass("error")&&a.element(b).remove()}),c.removeClass("error")},c=function(a){for(var b=a,c=0;3>=c&&(void 0===b||!b.hasClass("columns"));c+=1)void 0!==b&&(b=b.parent());return b},d=function(a){var d=c(a);b(d&&d.length>0?d:a,a)},e=function(d,e){var f,g=c(d);b(g||d,d),d.addClass("error"),g&&(f=a.element(''+e+""),g.append(f))},f=function(a){d(a)};return{makeValid:d,makeInvalid:e,makeDefault:f,key:"foundation5"}}])}(angular),function(a){"use strict";a.module("jcs-autoValidate").factory("validationManager",["validator",function(b){var c=function(c,d,e){var f,g=!0,h=c.$pristine===!1||e,i=function(b){var c,d=!0;return a.forEach(b,function(a,b){d&&a&&(d=!1,c=b)}),c};return c&&h&&(g=!c.$invalid,g?b.makeValid(d):(f=i(c.$error),b.getErrorMessage(f,d).then(function(a){b.makeInvalid(d,a)}))),g},d=function(a){b.makeDefault(a)},e=function(b){a.forEach(b[0],function(b){var c,d=a.element(b);c=d.controller("ngModel"),void 0!==c&&("FORM"===d[0].nodeName?e(d):c.$setPristine())})},f=function(b){var d=!0;return void 0===b?!1:(a.forEach(b[0],function(b){var e,g;b=a.element(b),e=b.controller("ngModel"),void 0!==e&&("FORM"===b[0].nodeName?f(b):(g=c(e,b,!0),d=d&&g))}),d)};return{validateElement:c,validateForm:f,resetElement:d,resetForm:e}}])}(angular),function(a){"use strict";a.module("jcs-autoValidate").directive("form",["validationManager",function(a){return{restrict:"E",link:function(b,c){c.on("reset",function(){a.resetForm(c)}),b.$on("$destroy",function(){c.off("reset")})}}}])}(angular),function(a){"use strict";a.module("jcs-autoValidate").config(["$provide",function(a){a.decorator("ngSubmitDirective",["$delegate","$parse","validationManager",function(a,b,c){return a[0].compile=function(a,d){var e=b(d.ngSubmit),f="true"===d.ngSubmitForce;return function(a,b){b.on("submit",function(d){a.$apply(function(){(f===!0||c.validateForm(b))&&e(a,{$event:d})})})}},a}])}])}(angular),function(a){"use strict";a.module("jcs-autoValidate").config(["$provide",function(b){b.decorator("ngModelDirective",["$timeout","$delegate","validationManager","debounce",function(b,c,d,e){var f=c[0],g=f.link;return f.compile=function(){return function(b,c,f,h){var i=h[0],j=a.version.major>=1&&a.version.minor>=3,k=void 0===f.ngModelOptions?void 0:b.$eval(f.ngModelOptions),l=i.$setValidity,m=i.$setPristine,n=e.debounce(function(){d.validateElement(i,c)},100);g.pre&&(g.pre.apply(this,arguments),k=void 0===i.$options?void 0:i.$options),void 0===f.formnovalidate&&(j||void 0===k||void 0===k.updateOn||""===k.updateOn?i.$setValidity=function(a,b){l.call(i,a,b),n()}:(c.on(k.updateOn,function(){n()}),b.$on("$destroy",function(){c.off(k.updateOn)})),i.$setPristine=function(){m.call(i),d.resetElement(c)},i.autoValidated=!0),g.post?g.post.apply(this,arguments):g.apply(this,arguments)}},c}])}])}(angular),function(a){"use strict";a.module("jcs-autoValidate").run(["validator","defaultErrorMessageResolver","bootstrap3ElementModifier","foundation5ElementModifier",function(a,b,c,d){a.setErrorMessageResolver(b.resolve),a.registerDomModifier(c.key,c),a.registerDomModifier(d.key,d),a.setDefaultElementModifier(c.key)}])}(angular); \ No newline at end of file +!function(e,t){"use strict";function r(){var e={},r=!0,o=!0,a=!1,n=!0,i=function(e){var t;return e&&0!==e.length?(t=e.toLowerCase(),e=!("f"===t||"0"===t||"false"===t)):e=!1,e},l=function(e,t){var r;return void 0!==e&&(r=e.attr(t)||e.attr("data-"+t)),r},s=function(e,t){var r;return void 0!==e&&(r=void 0!==e.attr(t)||void 0!==e.attr("data-"+t)),r},d=function(e,t){return i(l(e,t))},u=function(e){return r&&!d(e,"disable-valid-styling")},c=function(e){return!d(e,"disable-auto-validate")},f=function(e){return o&&!d(e,"disable-invalid-styling")};this.enable=function(e){n=e},this.isEnabled=function(){return n},this.setDefaultElementModifier=function(t){if(void 0===e[t])throw new Error("Element modifier not registered: "+t);this.defaultElementModifier=t},this.registerDomModifier=function(t,r){e[t]=r},this.setErrorMessageResolver=function(e){this.errorMessageResolver=e},this.getErrorMessage=function(e,r){var o;if(void 0===this.errorMessageResolver)throw new Error("Please set an error message resolver via the setErrorMessageResolver function before attempting to resolve an error message.");return s(r,"disable-validation-message")?(o=t.injector(["ng"]).get("$q").defer(),o.resolve(""),o.promise):this.errorMessageResolver(e,r)},this.setValidElementStyling=function(e){r=e},this.setInvalidElementStyling=function(e){o=e},this.setFirstInvalidElementScrollingOnSubmit=function(e){a=e},this.firstInvalidElementScrollingOnSubmitEnabled=function(){return a},this.getDomModifier=function(t){var r=(void 0!==t?t.attr("element-modifier"):this.defaultElementModifier)||(void 0!==t?t.attr("data-element-modifier"):this.defaultElementModifier)||this.defaultElementModifier;if(void 0===r)throw new Error("Please set a default dom modifier via the setDefaultElementModifier method on the validator class.");return e[r]},this.makeValid=function(e){c(e)&&(u(e)?this.getDomModifier(e).makeValid(e):this.makeDefault(e))},this.makeInvalid=function(e,t){c(e)&&(f(e)?this.getDomModifier(e).makeInvalid(e,t):this.makeDefault(e))},this.makeDefault=function(e){if(c(e)){var t=this.getDomModifier(e);t.makeDefault&&t.makeDefault(e)}},this.waitForAsyncValidators=function(e){if(c(e)){var t=this.getDomModifier(e);t.waitForAsyncValidators&&t.waitForAsyncValidators(e)}},this.defaultFormValidationOptions={forceValidation:!1,disabled:!1,validateNonVisibleControls:!1,removeExternalValidationErrorsOnSubmit:!0,validateOnFormSubmit:!1,waitForAsyncValidators:!0},this.$get=[function(){return this}]}function o(e){var r=[""].join("");t.element(document.body).append(t.element(r));var o=function(e){t.forEach(e.find("span"),function(e){e=t.element(e),(e.hasClass("error-msg")||e.hasClass("form-control-feedback")||e.hasClass("control-feedback"))&&e.remove()}),e.removeClass("has-success has-error has-feedback")},a=function(e,t){for(var r,o=e,a=0;10>=a;a+=1){if(void 0!==o&&o.hasClass(t)){r=o;break}void 0!==o&&(o=o.parent())}return r},n=function(e,r){for(var o,a=0;a0));a+=1);return t.element(o)},i=function(e){return a(e,"form-group")},l=function(e){return n(e,"input-group")},s=function(e,t){e[0].parentNode.insertBefore(t[0],e[0].nextSibling)},d=!1,u=function(e){d=e},c=function(r){var a=i(r),n;if(a){if(o(a),n=l(a[0]),a.addClass("has-success "+(n.length>0||d===!1?"":"has-feedback")),d){var u='';n.length>0&&(u=u.replace("form-",""),u=''+u+"'+a+""),c;if(n){if(o(n),c=l(n[0]),n.addClass("has-error "+(c.length>0||d===!1?"":"has-feedback")),s(c.length>0?c:m(r),u),d){var f='';c.length>0&&(f=f.replace("form-",""),f=''+f+""),s(m(r),t.element(f))}}else e.error("Angular-auto-validate: invalid bs3 form structure elements must be wrapped by a form-group class")},m=function(e){var t=e,r=e[0].type?e[0].type.toLowerCase():"";return"checkbox"!==r&&"radio"!==r||"label"!==e.parent()[0].nodeName.toLowerCase()||(t=e.parent()),t},v=function(t){var r=i(t);r?o(r):e.error("Angular-auto-validate: invalid bs3 form structure elements must be wrapped by a form-group class")},p=function(r){var a=i(r),n;if(a){if(o(a),n=l(a[0]),a.addClass("has-feedback "+(n.length>0||d===!1?"":"has-feedback")),d){var u='';n.length>0&&(u=u.replace("form-",""),u=''+u+""),s(r,t.element(u))}}else e.error("Angular-auto-validate: invalid bs3 form structure elements must be wrapped by a form-group class")};return{makeValid:c,makeInvalid:f,makeDefault:v,waitForAsyncValidators:p,enableValidationStateIcons:u,key:"bs3"}}function a(e){var t=function(t,r,o){var a;return function(){var n=this,i=arguments,l=function(){a=null,o||t.apply(n,i)},s=o&&!a;e.cancel(a),a=e(l,r,!1),s&&t.apply(n,i)}};return{debounce:t}}function n(e,r){var o="default",a="js/angular-auto-validate/dist/lang",n,i=function(e){return n=r.get("{0}/jcs-auto-validate_{1}.json".format(a,e.toLowerCase()))},l=function(e){a=e},s=function(r,a){var l=e.defer();return a=a||i,o=r.toLowerCase(),void 0===t.autoValidate.errorMessages[o]?(n=a(r),n.then(function(e){n=void 0,t.autoValidate.errorMessages[o]=e.data,l.resolve(t.autoValidate.errorMessages[o])},function(e){t.autoValidate.errorMessages[o]={defaultMsg:"Loading culture failed!"},n=null,l.reject(e)})):l.resolve(t.autoValidate.errorMessages[o]),l.promise},d=function(r){var a=e.defer();return r=void 0===r?o:r.toLowerCase(),void 0!==n?n.then(function(){a.resolve(t.autoValidate.errorMessages[r])},function(e){a.reject(e)}):a.resolve(t.autoValidate.errorMessages[r]),a.promise},u=function(e,t){var r;return t&&(e+="-err-type",r=t.attr("ng-"+e),void 0===r&&(r=t.attr("data-ng-"+e)||t.attr(e)),r&&(r=r.replace(/[\W]/g,""))),r},c=function(r,a){var i=e.defer(),l,s=[],d,f;if(void 0!==n)n.then(function(){c(r,a).then(function(e){i.resolve(e)})});else{if(l=t.autoValidate.errorMessages[o][r],f=u(r,a),f&&(l=t.autoValidate.errorMessages[o][f]),void 0===l&&void 0!==f?l=t.autoValidate.errorMessages[o].defaultMsg.format(f):void 0===l&&(l=t.autoValidate.errorMessages[o].defaultMsg.format(r)),a&&a.attr)try{d=a.attr("ng-"+r),void 0===d&&(d=a.attr("data-ng-"+r)||a.attr(r)),s.push(d||""),l=l.format(s)}catch(m){}i.resolve(l)}return i.promise};return{setI18nFileRootPath:l,setCulture:s,getErrorMessages:d,resolve:c}}function i(){var e=function(e,r){t.forEach(e.find("small"),function(e){t.element(e).hasClass("form-error is-visible")&&t.element(e).remove()}),r.removeClass("alert callout")},r=function(e){for(var t=e,r=0;3>=r&&(void 0===t||!t.hasClass("columns")&&!t.hasClass("column"));r+=1)void 0!==t&&(t=t.parent());return t},o=function(t){var o=r(t);e(o&&o.length>0?o:t,t)},a=function(o,a){var n=r(o),i;e(n||o,o),o.addClass("alert callout"),n&&(i=t.element(''+a+""),n.append(i))},n=function(e){o(e)};return{makeValid:o,makeInvalid:a,makeDefault:n,key:"foundation6"}}function l(){var e=function(e){return e[0].offsetWidth>0&&e[0].offsetHeight>0};return{isElementVisible:e}}function s(e,r,o){var a=["input","textarea","select","form"],n=function(e){return r.isElementVisible(e)},i=function(r){var o=t.element(r).controller("form"),a;return a=void 0!==o&&null!==o?o.autoValidateFormOptions:e.defaultFormValidationOptions},l=function(e,t,r){var o=e&&e.length>0,i=o&&"#comment"===e[0].nodeName.toLowerCase(),l,s,d;return o&&i===!1&&(l=n(e)||t.validateNonVisibleControls,s=a.indexOf(e[0].nodeName.toLowerCase())>-1||e[0].hasAttribute("register-custom-form-control"),d=t.validateOnFormSubmit===!1||t.validateOnFormSubmit===!0&&r===!0),o&&!i&&l&&s&&d},s=function(r,o,a){var n=!0,s=a||i(o),d=r.$pristine===!1||s.forceValidation,u,c=function(e){var r=!0,o;return t.forEach(e,function(e,t){r&&e&&(r=!1,o=t)}),o};return s.disabled===!1&&(s.forceValidation||l(o,s,s.getFormController().$submitted)&&r&&d)&&(n=!r.$invalid,s.removeExternalValidationErrorsOnSubmit&&r.removeAllExternalValidation&&r.removeAllExternalValidation(),void 0!==r.$pending&&a.waitForAsyncValidators===!0?e.waitForAsyncValidators(o):n?e.makeValid(o):(u=c(r.$errors||r.$error),void 0===u?n=!0:e.getErrorMessage(u,o).then(function(t){e.makeInvalid(o,t)}))),n},d=function(t){e.makeDefault(t)},u=function(e){t.forEach(e[0].all||e[0].elements||e[0],function(e){var r,o=t.element(e);r=o.controller("ngModel"),void 0!==r&&("form"===o[0].nodeName.toLowerCase()?u(o):r.$setPristine())})},c=function(r){var a=!0,n=r?t.element(r).controller("form"):void 0,d=function(r,d,u){var f,m,v,p;if(r=t.element(r),f=r.controller("ngModel"),void 0!==f&&(d||l(r,u,n.$submitted)))if("form"===r[0].nodeName.toLowerCase())c(r);else{v=i(r),p=v.forceValidation,v.forceValidation=d;try{if(m=s(f,r,v),e.firstInvalidElementScrollingOnSubmitEnabled()&&!m&&a){var g=r.attr("id");g&&o(g)}a=a&&m}finally{v.forceValidation=p}}},u;return void 0===r||void 0!==n&&n.autoValidateFormOptions.disabled?void 0!==r:(u=t.copy(n.autoValidateFormOptions),u.forceValidation=!0,t.forEach(r[0].elements||r[0].all||r[0],function(e){d(e,!0,u)}),r[0].customHTMLFormControlsCollection&&t.forEach(r[0].customHTMLFormControlsCollection,function(e){d(e,!0,u)}),a)},f=function(t,r,o){r?e.getErrorMessage(r,t).then(function(r){e.makeInvalid(t,r)}):e.makeInvalid(t,o)};return{setElementValidationError:f,validateElement:s,validateForm:c,resetElement:d,resetForm:u}}function d(e,t){return void 0!==e&&null!==e||void 0===t?"false"!==e:t}function u(e,r,o){var a=e.autoValidateFormOptions=e.autoValidateFormOptions||t.copy(r.defaultFormValidationOptions);a.getFormController=function(){return e},a.waitForAsyncValidators=d(o.waitForAsyncValidators,a.waitForAsyncValidators),a.forceValidation=!1,a.disabled=!r.isEnabled()||d(o.disableDynamicValidation,a.disabled),a.validateNonVisibleControls=d(o.validateNonVisibleControls,a.validateNonVisibleControls),a.validateOnFormSubmit=d(o.validateOnFormSubmit,a.validateOnFormSubmit),a.removeExternalValidationErrorsOnSubmit=void 0===o.removeExternalValidationErrorsOnSubmit?a.removeExternalValidationErrorsOnSubmit:d(o.removeExternalValidationErrorsOnSubmit,a.removeExternalValidationErrorsOnSubmit),r.isEnabled()===!1&&"false"===o.disableDynamicValidation&&(a.disabled=!1)}function c(e){return{restrict:"E",link:function(t,r){function o(){e.resetForm(r),a.$setPristine&&a.$setPristine(),a.$setUntouched&&a.$setUntouched()}var a=r.controller("form");void 0!==a&&a.autoValidateFormOptions&&a.autoValidateFormOptions.disabled===!1&&(r.on("reset",o),t.$on("$destroy",function(){r.off("reset",o)}))}}}function f(){var e=function(e){for(var r=e,o=0;50>=o&&(void 0===r||"form"!==r.nodeName.toLowerCase());o+=1)void 0!==r&&(r=t.element(r).parent()[0]);return r};return{restrict:"A",link:function(t,r){var o=e(r.parent()[0]);o&&(o.customHTMLFormControlsCollection=o.customHTMLFormControlsCollection||[],o.customHTMLFormControlsCollection.push(r[0]))}}}function m(e,t,r){return e[0].compile=function(e,o){var a=t(o.ngSubmit),n="true"===o.ngSubmitForce;return function(e,t){function o(o){e.$apply(function(){void 0!==l&&null!==l&&l.autoValidateFormOptions&&l.autoValidateFormOptions.disabled===!0?a(e,{$event:o}):(void 0===l.$setSubmitted&&(l.$submitted=!0),(r.validateForm(t)||n===!0)&&a(e,{$event:o}))})}function i(){t[0].reset?t[0].reset():r.resetForm(t)}var l=t.controller("form"),s;l&&l.autoValidateFormOptions&&(l.autoValidateFormOptions.resetForm=i,void 0!==l.$name&&""!==l.$name&&(s=e.$on("form:"+l.$name+":reset",i))),t.on("submit",o),e.$on("$destroy",function(){t.off("submit",o),s&&s()})}},e}function v(e){e.decorator("ngSubmitDirective",m)}function p(e,t,r,o){e.setErrorMessageResolver(t.resolve),e.registerDomModifier(r.key,r),e.registerDomModifier(o.key,o),e.setDefaultElementModifier(r.key)}t.module("jcs-autoValidate",[]),t.module("jcs-autoValidate").provider("validator",r),o.$inject=["$log"],t.module("jcs-autoValidate").factory("bootstrap3ElementModifier",o),a.$inject=["$timeout"],t.module("jcs-autoValidate").factory("jcs-debounce",a),"format"in e.prototype||(e.prototype.format=function(){var e=arguments;return this.replace(/{(\d+)}/g,function(t,r){return void 0!==typeof e[r]?e[r]:t})}),t.autoValidate=t.autoValidate||{errorMessages:{}},t.autoValidate.errorMessages["default"]={defaultMsg:"Please add error message for {0}",email:"Please enter a valid email address",minlength:"Please enter at least {0} characters",maxlength:"You have entered more than the maximum {0} characters",min:"Please enter the minimum number of {0}",max:"Please enter the maximum number of {0}",required:"This field is required",date:"Please enter a valid date",pattern:"Please ensure the entered information adheres to this pattern {0}",number:"Please enter a valid number",url:"Please enter a valid URL in the format of http(s)://www.google.com"},n.$inject=["$q","$http"],t.module("jcs-autoValidate").factory("defaultErrorMessageResolver",n),t.module("jcs-autoValidate").factory("foundation6ElementModifier",i),s.$inject=["validator","jcs-elementUtils","$anchorScroll"],t.module("jcs-autoValidate").factory("jcs-elementUtils",l),t.module("jcs-autoValidate").factory("validationManager",s),t.module("jcs-autoValidate").directive("form",["validator",function(e){return{restrict:"E",require:"form",priority:9999,compile:function(){return{pre:function(t,r,o,a){u(a,e,o)}}}}}]),t.module("jcs-autoValidate").directive("ngForm",["validator",function(e){return{restrict:"EA",require:"form",priority:9999,compile:function(){return{pre:function(t,r,o,a){u(a,e,o)}}}}}]),c.$inject=["validationManager"],t.module("jcs-autoValidate").directive("form",c),t.module("jcs-autoValidate").directive("registerCustomFormControl",f),m.$inject=["$delegate","$parse","validationManager"],v.$inject=["$provide"],t.module("jcs-autoValidate").config(v),t.module("jcs-autoValidate").config(["$provide",function(e){e.decorator("ngModelDirective",["$timeout","$delegate","validationManager","jcs-debounce",function(e,r,o,a){var n=r[0],i=n.link||n.compile;return n.compile=function(e){var r=t.version.major>=1&&t.version.minor>=3,n=i;return r&&t.isFunction(i)&&(n=i(e)),{pre:function(e,i,l,s){var d=s[0],u=s[1],c=void 0===l.ngModelOptions?void 0:e.$eval(l.ngModelOptions),f=d.$setValidity,m=d.$setPristine,v=a.debounce(function(){var e=void 0!==u&&null!==u?u.autoValidateFormOptions:void 0;o.validateElement(d,i,e)},100);return void 0===l.formnovalidate&&void 0!==u&&null!==u&&u.autoValidateFormOptions&&u.autoValidateFormOptions.disabled===!1&&(r||!r||void 0===c||void 0===c.updateOn||""===c.updateOn?d.$setValidity=function(e,t){f.call(d,e,t),v()}:(i.on(c.updateOn,function(){v()}),e.$on("$destroy",function(){i.off(c.updateOn)})),d.$setPristine=function(){m.call(d),o.resetElement(i)},d.autoValidated=!0),d.setExternalValidation=function(e,t,r){if(r){var a=d.$error||d.$errors;a[e]=!1}d.externalErrors=d.externalErrors||{},d.externalErrors[e]=!1,o.setElementValidationError(i,e,t)},d.removeExternalValidation=function(e,t){if(t){var r=d.$error||d.$errors;delete r[e]}d.externalErrors&&delete d.externalErrors[e],o.resetElement(i)},d.removeAllExternalValidation=function(){if(d.externalErrors){var e=d.$error||d.$errors;t.forEach(d.externalErrors,function(t,r){delete e[r]}),d.externalErrors={},o.resetElement(i)}},u&&(u.setExternalValidation=function(e,t,r,o){var a=!1;return u[e]&&(u[e].setExternalValidation(t,r,o),a=!0),a},u.removeExternalValidation=function(e,t,r,o){var a=!1;return u[e]&&(u[e].removeExternalValidation(t,o),a=!0),a}),n.pre?n.pre.apply(this,arguments):this},post:function(e,t,r,o){return n.post?n.post.apply(this,arguments):n.apply(this,arguments)}}},r}])}]),p.$inject=["validator","defaultErrorMessageResolver","bootstrap3ElementModifier","foundation6ElementModifier"],t.module("jcs-autoValidate").run(p)}(String,angular); \ No newline at end of file diff --git a/index.html b/index.html index a255b82..90411e7 100644 --- a/index.html +++ b/index.html @@ -111,16 +111,16 @@

Getting Started.

bootstrap3ElementModifier.enableValidationStateIcons(true); }]); -

if you are using Foundation 5 you will need to set the Foundation 5 element modifier as the default one

+

if you are using Foundation 6 you will need to set the Foundation 6 element modifier as the default one

angular.module('my-app')
        .run([
        'validator',
-       'foundation5ElementModifier',
-       function (validator, foundation5ElementModifier) {
-           validator.setDefaultElementModifier(foundation5ElementModifier.key);
+       'Foundation6ElementModifier',
+       function (validator, Foundation6ElementModifier) {
+           validator.setDefaultElementModifier(Foundation6ElementModifier.key);
        }]);
-

If you are not using Bootstrap3 or Foundation5 you will need to create a custom element modifier so see the below section

+

If you are not using Bootstrap3 or Foundation6 you will need to create a custom element modifier so see the below section

Your Form

Now that you are using auto-validate you haven't got to worry about putting your error messages in you html @@ -414,7 +414,7 @@

Custom Error Message Resolver

Custom Element Modifier

-

If you are not using Bootstrap3 or Foundation5 CSS libraries you will need to create a custom element modifier +

If you are not using Bootstrap3 or Foundation6 CSS libraries you will need to create a custom element modifier which takes care of updating the visual state of an element, don't worry this is really easy! You can see a few example of what to put in them here and here.

angular.module('my-app')