From bd871717d01b773748270e3d1bbe26603e1caa5d Mon Sep 17 00:00:00 2001 From: Andrew Stanton-Nurse Date: Thu, 22 Sep 2022 12:16:55 -0700 Subject: [PATCH 1/4] add a 'watch' option to wire up a mutation observer --- README.MD | 29 +++++++++ dist/aspnet-validation.js | 94 +++++++++++++++++++++++---- dist/aspnet-validation.min.js | 2 +- dist/aspnet-validation.min.js.map | 2 +- docs/DEVELOPMENT.md | 2 +- package-lock.json | 4 +- package.json | 2 +- script/build | 1 + src/index.ts | 103 ++++++++++++++++++++++++++---- types/index.d.ts | 26 +++++++- yarn.lock | 10 +++ 11 files changed, 242 insertions(+), 33 deletions(-) diff --git a/README.MD b/README.MD index 76b6205..8325c95 100644 --- a/README.MD +++ b/README.MD @@ -221,7 +221,36 @@ v.isFieldValid(document.getElementById('field'), true, myCallbackFn) ``` ## Hidden fields validation + By default validation is skipped for hidden fields. To enable validation for hidden fields validation use: ```ts v.allowHiddenFields = true; +``` + +## Monitoring the DOM for changes + +If configured, aspnet-client-validation can monitor the DOM for changes using [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver), if the browser supports it. +As new elements are added/modified, the library will automatically wire up validation directives found. +This can be very useful when using frameworks that modify the DOM, such as [Turbo](http://turbo.hotwired.dev). +To configure this, set the `watch` option to `true` when calling `bootstrap`: + +```js +let v = new aspnetValidation.ValidationService(); +v.bootstrap({ watch: true }); +``` + +## Logging + +There is a rudimentary logging infrastructure in place if you want to get more insight into what the library is doing. +To enable logging, pass an object that implements the `Logger` interface (see below) in to the `ValidationService` constructor. +The `window.console` object satisfies this interface automatically: + +```typescript +// The Logger interface, for reference +export interface Logger { + log(message: string, ...args: any[]): void; +} + +let v = new aspnetValidation.ValidationService(console); +v.bootstrap(); ``` \ No newline at end of file diff --git a/dist/aspnet-validation.js b/dist/aspnet-validation.js index 189549d..51c4a2a 100644 --- a/dist/aspnet-validation.js +++ b/dist/aspnet-validation.js @@ -143,6 +143,17 @@ var __generator = (undefined && undefined.__generator) || function (thisArg, bod if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; +var nullLogger = new (/** @class */ (function () { + function class_1() { + } + class_1.prototype.log = function (_) { + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + }; + return class_1; +}()))(); /** * Resolves and returns the element referred by original element using ASP.NET selector logic. * @param elementName @@ -396,7 +407,7 @@ var MvcValidationProviders = /** @class */ (function () { * Responsibles for managing the DOM elements and running the validation providers. */ var ValidationService = /** @class */ (function () { - function ValidationService() { + function ValidationService(logger) { var _this = this; /** * A key-value collection of loaded validation plugins. @@ -500,6 +511,7 @@ var ValidationService = /** @class */ (function () { var fieldUID = _this.getElementUID(field); return _this.summary[fieldUID] != null; }; + this.logger = logger || nullLogger; } /** * Registers a new validation plugin of the given name, if not registered yet. @@ -513,6 +525,7 @@ var ValidationService = /** @class */ (function () { // Allows developers to override the default MVC Providers by adding custom providers BEFORE bootstrap() is called! return; } + this.logger.log("Registered provider: " + name); this.providers[name] = callback; }; /** @@ -546,8 +559,13 @@ var ValidationService = /** @class */ (function () { /** * Scans document for all validation message generated by ASP.NET Core MVC, then tracks them. */ - ValidationService.prototype.scanMessages = function () { - var validationMessageElements = document.querySelectorAll('[data-valmsg-for]'); + ValidationService.prototype.scanMessages = function (root) { + var validationMessageElements = Array.prototype.slice.call(root.querySelectorAll('[data-valmsg-for]')); + // querySelectorAll does not include the root element itself. + // we could use 'matches', but that's newer than querySelectorAll so we'll keep it simple and compatible. + if (root.hasAttribute("data-valmsg-for")) { + validationMessageElements.push(root); + } for (var i = 0; i < validationMessageElements.length; i++) { var e = validationMessageElements[i]; var name_1 = e.getAttribute('data-valmsg-for'); @@ -767,8 +785,13 @@ var ValidationService = /** @class */ (function () { /** * Scans the entire document for input elements to be validated. */ - ValidationService.prototype.scanInputs = function () { - var inputs = document.querySelectorAll('[data-val="true"]'); + ValidationService.prototype.scanInputs = function (root) { + var inputs = Array.prototype.slice.call(root.querySelectorAll('[data-val="true"]')); + // querySelectorAll does not include the root element itself. + // we could use 'matches', but that's newer than querySelectorAll so we'll keep it simple and compatible. + if (root.getAttribute("data-val") === "true") { + inputs.push(root); + } for (var i = 0; i < inputs.length; i++) { var input = inputs[i]; this.addInput(input); @@ -882,6 +905,7 @@ var ValidationService = /** @class */ (function () { console.log('aspnet-validation provider not implemented: ' + key); return [3 /*break*/, 6]; } + this.logger.log("Running " + key + " validator on element", input); result = provider(input.value, input, directive.params); valid = false; error = directive.error; @@ -930,23 +954,67 @@ var ValidationService = /** @class */ (function () { }; /** * Load default validation providers and scans the entire document when ready. + * @param options.watch If set to true, a MutationObserver will be used to continuously watch for new elements that provide validation directives. */ - ValidationService.prototype.bootstrap = function () { + ValidationService.prototype.bootstrap = function (options) { var _this = this; + options = options || {}; + options.root = options.root || window.document.body; this.addMvcProviders(); - var document = window.document; - if (document.readyState === 'complete') { - this.scan(); + // If the document is done loading, scan it now. + if (window.document.readyState !== 'loading') { + this.scan(options.root); } else { + // Otherwise wait until the document is done loading. window.document.addEventListener('DOMContentLoaded', function (event) { - _this.scan(); + _this.scan(options.root); }); } + // Watch for further mutations + if (options.watch) { + this.watch(options.root); + } }; - ValidationService.prototype.scan = function () { - this.scanMessages(); - this.scanInputs(); + /** + * Scans the provided root element for any validation directives and attaches behavior to them. + */ + ValidationService.prototype.scan = function (root) { + this.scanMessages(root); + this.scanInputs(root); + }; + /** + * Watches the provided root element for mutations, and scans for new validation directives to attach behavior. + * @param root The root element to use, defaults to the document.documentElement. + */ + ValidationService.prototype.watch = function (root) { + var _this = this; + this.observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + _this.observed(mutation); + }); + }); + this.observer.observe(root, { + attributes: true, + childList: true, + subtree: true + }); + this.logger.log("Watching for mutations"); + }; + ValidationService.prototype.observed = function (mutation) { + if (mutation.type === 'childList') { + for (var i = 0; i < mutation.addedNodes.length; i++) { + var node = mutation.addedNodes[i]; + if (node instanceof HTMLElement) { + this.scan(node); + } + } + } + else if (mutation.type === 'attributes') { + if (mutation.target instanceof HTMLElement) { + this.scan(mutation.target); + } + } }; return ValidationService; }()); diff --git a/dist/aspnet-validation.min.js b/dist/aspnet-validation.min.js index 96a5dc9..99fa3e7 100644 --- a/dist/aspnet-validation.min.js +++ b/dist/aspnet-validation.min.js @@ -1,2 +1,2 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.aspnetValidation=t():e.aspnetValidation=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t),r.d(t,"MvcValidationProviders",(function(){return o})),r.d(t,"ValidationService",(function(){return s}));var n=function(e,t,r,n){return new(r||(r=Promise))((function(i,a){function o(e){try{u(n.next(e))}catch(e){a(e)}}function s(e){try{u(n.throw(e))}catch(e){a(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(o,s)}u((n=n.apply(e,t||[])).next())}))},i=function(e,t){var r,n,i,a,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(a){return function(s){return function(a){if(r)throw new TypeError("Generator is already executing.");for(;o;)try{if(r=1,n&&(i=2&a[0]?n.return:a[0]?n.throw||((i=n.return)&&i.call(n),0):n.next)&&!(i=i.call(n,a[1])).done)return i;switch(n=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,n=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!(i=o.trys,(i=i.length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]-1){var i=e.substr(0,n)+"."+r,a=document.getElementsByName(i)[0];if(a)return a}return document.getElementsByName(r)[0]}var o=function(){this.required=function(e,t,r){return Boolean(e)},this.stringLength=function(e,t,r){if(!e)return!0;if(r.min){var n=parseInt(r.min);if(e.lengthi)return!1}return!0},this.compare=function(e,t,r){if(!r.other)return!0;var n=a(t.name,r.other);return!n||n.value===e},this.range=function(e,t,r){if(!e)return!0;var n=parseFloat(e);return!isNaN(n)&&(!(r.min&&nparseFloat(r.max)))},this.regex=function(e,t,r){return!e||!r.pattern||new RegExp(r.pattern).test(e)},this.email=function(e,t,r){return!e||/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$/.test(e)},this.creditcard=function(e,t,r){if(!e)return!0;if(/[^0-9 \-]+/.test(e))return!1;var n,i,a=0,o=0,s=!1;if((e=e.replace(/\D/g,"")).length<13||e.length>19)return!1;for(n=e.length-1;n>=0;n--)i=e.charAt(n),o=parseInt(i,10),s&&(o*=2)>9&&(o-=9),a+=o,s=!s;return a%10==0},this.url=function(e,t,r){if(!e)return!0;var n=e.toLowerCase();return n.indexOf("http://")>-1||n.indexOf("https://")>-1||n.indexOf("ftp://")>-1},this.phone=function(e,t,r){return!e||!/[\+\-\s][\-\s]/g.test(e)&&/^\+?[0-9\-\s]+$/.test(e)},this.remote=function(e,t,r){if(!e)return!0;for(var n=r.additionalfields.split(","),i={},o=0,s=n;o=200&&n.status<300){var i=JSON.parse(n.responseText);e(i)}else t({status:n.status,statusText:n.statusText,data:n.responseText})},n.onerror=function(e){t({status:n.status,statusText:n.statusText,data:n.responseText})}}))}},s=function(){function e(){var e=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.elementEvents={},this.summary={},this.debounce=300,this.allowHiddenFields=!1,this.validateForm=function(t,r){var n=e.getElementUID(t),i=e.elementEvents[n];i&&i(null,r)},this.focusFirstInvalid=function(t){var r=e.getElementUID(t),n=e.formInputs[r].filter((function(t){return e.summary[t]}));if(n.length>0){var i=e.elementByUID[n[0]];i&&i.focus()}},this.isValid=function(t,r,n){void 0===r&&(r=!0),r&&e.validateForm(t,n);var i=e.getElementUID(t);return 0==e.formInputs[i].filter((function(t){return e.summary[t]})).length},this.isFieldValid=function(t,r,n){if(void 0===r&&(r=!0),r){var i=t.closest("form");null!=i&&e.validateForm(i,n)}var a=e.getElementUID(t);return null!=e.summary[a]}}return e.prototype.addProvider=function(e,t){this.providers[e]||(this.providers[e]=t)},e.prototype.addMvcProviders=function(){var e=new o;this.addProvider("required",e.required),this.addProvider("length",e.stringLength),this.addProvider("maxlength",e.stringLength),this.addProvider("minlength",e.stringLength),this.addProvider("equalto",e.compare),this.addProvider("range",e.range),this.addProvider("regex",e.regex),this.addProvider("creditcard",e.creditcard),this.addProvider("email",e.email),this.addProvider("url",e.url),this.addProvider("phone",e.phone),this.addProvider("remote",e.remote)},e.prototype.scanMessages=function(){for(var e=document.querySelectorAll("[data-valmsg-for]"),t=0;t0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]-1){var i=t.substr(0,n)+"."+r,a=document.getElementsByName(i)[0];if(a)return a}return document.getElementsByName(r)[0]}var s=function(){this.required=function(t,e,r){return Boolean(t)},this.stringLength=function(t,e,r){if(!t)return!0;if(r.min){var n=parseInt(r.min);if(t.lengthi)return!1}return!0},this.compare=function(t,e,r){if(!r.other)return!0;var n=o(e.name,r.other);return!n||n.value===t},this.range=function(t,e,r){if(!t)return!0;var n=parseFloat(t);return!isNaN(n)&&(!(r.min&&nparseFloat(r.max)))},this.regex=function(t,e,r){return!t||!r.pattern||new RegExp(r.pattern).test(t)},this.email=function(t,e,r){return!t||/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$/.test(t)},this.creditcard=function(t,e,r){if(!t)return!0;if(/[^0-9 \-]+/.test(t))return!1;var n,i,a=0,o=0,s=!1;if((t=t.replace(/\D/g,"")).length<13||t.length>19)return!1;for(n=t.length-1;n>=0;n--)i=t.charAt(n),o=parseInt(i,10),s&&(o*=2)>9&&(o-=9),a+=o,s=!s;return a%10==0},this.url=function(t,e,r){if(!t)return!0;var n=t.toLowerCase();return n.indexOf("http://")>-1||n.indexOf("https://")>-1||n.indexOf("ftp://")>-1},this.phone=function(t,e,r){return!t||!/[\+\-\s][\-\s]/g.test(t)&&/^\+?[0-9\-\s]+$/.test(t)},this.remote=function(t,e,r){if(!t)return!0;for(var n=r.additionalfields.split(","),i={},a=0,s=n;a=200&&n.status<300){var i=JSON.parse(n.responseText);t(i)}else e({status:n.status,statusText:n.statusText,data:n.responseText})},n.onerror=function(t){e({status:n.status,statusText:n.statusText,data:n.responseText})}}))}},u=function(){function t(t){var e=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.elementEvents={},this.summary={},this.debounce=300,this.allowHiddenFields=!1,this.validateForm=function(t,r){var n=e.getElementUID(t),i=e.elementEvents[n];i&&i(null,r)},this.focusFirstInvalid=function(t){var r=e.getElementUID(t),n=e.formInputs[r].filter((function(t){return e.summary[t]}));if(n.length>0){var i=e.elementByUID[n[0]];i&&i.focus()}},this.isValid=function(t,r,n){void 0===r&&(r=!0),r&&e.validateForm(t,n);var i=e.getElementUID(t);return 0==e.formInputs[i].filter((function(t){return e.summary[t]})).length},this.isFieldValid=function(t,r,n){if(void 0===r&&(r=!0),r){var i=t.closest("form");null!=i&&e.validateForm(i,n)}var a=e.getElementUID(t);return null!=e.summary[a]},this.logger=t||a}return t.prototype.addProvider=function(t,e){this.providers[t]||(this.logger.log("Registered provider: "+t),this.providers[t]=e)},t.prototype.addMvcProviders=function(){var t=new s;this.addProvider("required",t.required),this.addProvider("length",t.stringLength),this.addProvider("maxlength",t.stringLength),this.addProvider("minlength",t.stringLength),this.addProvider("equalto",t.compare),this.addProvider("range",t.range),this.addProvider("regex",t.regex),this.addProvider("creditcard",t.creditcard),this.addProvider("email",t.email),this.addProvider("url",t.url),this.addProvider("phone",t.phone),this.addProvider("remote",t.remote)},t.prototype.scanMessages=function(t){var e=Array.prototype.slice.call(t.querySelectorAll("[data-valmsg-for]"));t.hasAttribute("data-valmsg-for")&&e.push(t);for(var r=0;r\n */\nexport interface StringKeyValuePair {\n [key: string]: string\n}\n\n/**\n * A duplex key-value pair for an element, by GUID or its DOM object reference.\n */\ninterface ElementUID {\n node: Element,\n uid: string;\n}\n\n/**\n * Parameters passed into validation providers from the element attributes.\n * error property is read from data-val-[Provider Name] attribute.\n * params property is populated from data-val-[Provider Name]-[Parameter Name] attributes.\n */\nexport interface ValidationDirectiveBindings {\n error: string,\n params: StringKeyValuePair\n}\n\n/**\n * A key-value pair describing what validations to enforce to an input element, with respective parameters.\n */\nexport type ValidationDirective = {\n [key: string]: ValidationDirectiveBindings\n};\n\n/**\n * Validation plugin signature with multitype return.\n * Boolean return signifies the validation result, which uses the default validation error message read from the element attribute.\n * String return signifies failed validation, which then will be used as the validation error message.\n * Promise return signifies asynchronous plugin behavior, with same behavior as Boolean or String.\n */\nexport type ValidationProvider = (value: string, element: HTMLInputElement, params: StringKeyValuePair) => boolean | string | Promise;\n\n/**\n * A callback method signature that kickstarts a new validation task for an input element, as a Boolean Promise.\n */\ntype Validator = () => Promise;\n\n/**\n * Resolves and returns the element referred by original element using ASP.NET selector logic.\n * @param elementName\n */\nfunction getRelativeFormElement(elementName: string, selector: string) {\n // example elementName: Form.PasswordConfirm, Form.Email\n // example selector (dafuq): *.Password, *.__RequestVerificationToken\n // example result element name: Form.Password, __RequestVerificationToken\n\n let realSelector = selector.substr(2); // Password, __RequestVerificationToken\n let objectName = '';\n\n let dotLocation = elementName.lastIndexOf('.');\n if (dotLocation > -1) {\n // Form\n objectName = elementName.substr(0, dotLocation);\n\n // Form.Password\n let relativeElementName = objectName + '.' + realSelector;\n let relativeElement = document.getElementsByName(relativeElementName)[0];\n if (relativeElement) {\n return relativeElement;\n }\n }\n\n // __RequestVerificationToken\n return document.getElementsByName(realSelector)[0];\n}\n\n/**\n * Contains default implementations for ASP.NET Core MVC validation attributes.\n */\nexport class MvcValidationProviders {\n /**\n * Validates whether the input has a value.\n */\n required: ValidationProvider = (value, element, params) => {\n return Boolean(value);\n }\n\n /**\n * Validates whether the input value satisfies the length contstraint.\n */\n stringLength: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n if (params.min) {\n let min = parseInt(params.min);\n if (value.length < min) {\n return false;\n }\n }\n\n if (params.max) {\n let max = parseInt(params.max);\n if (value.length > max) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Validates whether the input value is equal to another input value.\n */\n compare: ValidationProvider = (value, element, params) => {\n if (!params.other) {\n return true;\n }\n\n let otherElement = getRelativeFormElement(element.name, params.other) as HTMLInputElement;\n if (!otherElement) {\n return true;\n }\n\n return (otherElement.value === value);\n }\n\n /**\n * Validates whether the input value is a number within a given range.\n */\n range: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n let val = parseFloat(value);\n if (isNaN(val)) {\n return false;\n }\n\n if (params.min) {\n let min = parseFloat(params.min);\n if (val < min) {\n return false;\n }\n }\n\n if (params.max) {\n let max = parseFloat(params.max);\n if (val > max) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Validates whether the input value satisfies a regular expression pattern.\n */\n regex: ValidationProvider = (value, element, params) => {\n if (!value || !params.pattern) {\n return true;\n }\n\n let r = new RegExp(params.pattern);\n return r.test(value);\n }\n\n /**\n * Validates whether the input value is an email in accordance to RFC822 specification, with a top level domain.\n */\n email: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // RFC822 email address with .TLD validation\n // (c) Richard Willis, Chris Ferdinandi, MIT Licensed\n // https://gist.github.com/badsyntax/719800\n // https://gist.github.com/cferdinandi/d04aad4ce064b8da3edf21e26f8944c4\n\n let r = /^([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22))*\\x40([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d))*(\\.\\w{2,})+$/;\n return r.test(value);\n }\n\n /**\n * Validates whether the input value is a credit card number, with Luhn's Algorithm.\n */\n creditcard: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // (c) jquery-validation, MIT Licensed\n // https://github.com/jquery-validation/jquery-validation/blob/master/src/additional/creditcard.js\n // based on https://en.wikipedia.org/wiki/Luhn_algorithm\n\n // Accept only spaces, digits and dashes\n if (/[^0-9 \\-]+/.test(value)) {\n return false;\n }\n\n var nCheck = 0,\n nDigit = 0,\n bEven = false,\n n, cDigit;\n\n value = value.replace(/\\D/g, \"\");\n\n // Basing min and max length on https://developer.ean.com/general_info/Valid_Credit_Card_Types\n if (value.length < 13 || value.length > 19) {\n return false;\n }\n\n for (n = value.length - 1; n >= 0; n--) {\n cDigit = value.charAt(n);\n nDigit = parseInt(cDigit, 10);\n if (bEven) {\n if ((nDigit *= 2) > 9) {\n nDigit -= 9;\n }\n }\n\n nCheck += nDigit;\n bEven = !bEven;\n }\n\n return (nCheck % 10) === 0;\n }\n\n /**\n * Validates whether the input value is a URL.\n */\n url: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n let lowerCaseValue = value.toLowerCase();\n\n // Match the logic in `UrlAttribute`\n return lowerCaseValue.indexOf('http://') > -1\n || lowerCaseValue.indexOf('https://') > -1\n || lowerCaseValue.indexOf('ftp://') > -1;\n }\n\n /**\n * Validates whether the input value is a phone number.\n */\n phone: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // Allows whitespace or dash as number separator because some people like to do that...\n let consecutiveSeparator = /[\\+\\-\\s][\\-\\s]/g;\n if (consecutiveSeparator.test(value)) {\n return false;\n }\n\n let r = /^\\+?[0-9\\-\\s]+$/;\n return r.test(value);\n }\n\n /**\n * Asynchronously validates the input value to a JSON GET API endpoint.\n */\n remote: ValidationProvider = (value, element, params) => {\n if (!value) {\n return true;\n }\n\n // params.additionalfields: *.Email,*.Username\n let fieldSelectors: string[] = (params.additionalfields as string).split(',');\n let fields: StringKeyValuePair = {};\n\n for (let fieldSelector of fieldSelectors) {\n let fieldName = fieldSelector.substr(2);\n let fieldElement = getRelativeFormElement(element.name, fieldSelector) as HTMLInputElement;\n\n let hasValue = Boolean(fieldElement && fieldElement.value);\n if (!hasValue) {\n continue;\n }\n\n fields[fieldName] = fieldElement.value;\n }\n\n let url: string = params['url'];\n // console.log(fields);\n\n let encodedParams: string[] = [];\n for (let fieldName in fields) {\n let encodedParam = encodeURIComponent(fieldName) + '=' + encodeURIComponent(fields[fieldName]);\n encodedParams.push(encodedParam);\n }\n let payload = encodedParams.join('&');\n // console.log(payload);\n\n return new Promise((ok, reject) => {\n let request = new XMLHttpRequest();\n\n if (params.type === 'Post') {\n let postData = new FormData();\n for (let fieldName in fields) {\n postData.append(fieldName, fields[fieldName]);\n }\n request.open('post', url);\n request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n request.send(payload);\n } else {\n request.open('get', url + '?' + payload);\n request.send();\n }\n\n request.onload = e => {\n if (request.status >= 200 && request.status < 300) {\n let data = JSON.parse(request.responseText);\n ok(data);\n } else {\n reject({\n status: request.status,\n statusText: request.statusText,\n data: request.responseText\n });\n }\n };\n\n request.onerror = e => {\n reject({\n status: request.status,\n statusText: request.statusText,\n data: request.responseText\n });\n };\n });\n }\n}\n\n/**\n * Responsibles for managing the DOM elements and running the validation providers.\n */\nexport class ValidationService {\n /**\n * A key-value collection of loaded validation plugins.\n */\n private providers: { [name: string]: ValidationProvider } = {};\n\n /**\n * A key-value collection of elements for displaying validation messages for an input (by DOM ID).\n */\n private messageFor: { [id: string]: Element[] } = {};\n\n /**\n * A list of managed elements, each having a randomly assigned unique identifier (UID).\n */\n private elementUIDs: ElementUID[] = [];\n\n /**\n * A key-value collection of UID to Element for quick lookup.\n */\n private elementByUID: { [uid: string]: Element } = {};\n\n /**\n * A key-value collection of input UIDs for a
UID.\n */\n private formInputs: { [formUID: string]: string[] } = {};\n\n /**\n * A key-value map for input UID to its validator factory.\n */\n private validators: { [inputUID: string]: Validator } = {};\n\n /**\n * A key-value map for element UID to its trigger element (submit event for , input event for