From bf71ea467343b2ae6a95637f68fc0970913636aa Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Thu, 15 Jun 2023 18:37:16 -0500 Subject: [PATCH 1/6] More precise types --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3d4a6e9..d60cc45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -494,8 +494,8 @@ export class ValidationService { } // Otherwise if a validation message span is inside a form, we group the span with the form it's inside. - let forms = Array.from(root.querySelectorAll('form')); - if (root.tagName === 'form') { + let forms = Array.from(root.querySelectorAll('form')); + if (root instanceof HTMLFormElement) { // 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. forms.push(root); From 4ca83df59db33d96132554022fae7b53667cd3ae Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Thu, 15 Jun 2023 18:39:52 -0500 Subject: [PATCH 2/6] Allow ParentNode, to include DocumentFragment --- src/index.ts | 12 ++++++------ types/index.d.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index d60cc45..0928d5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -483,7 +483,7 @@ export class ValidationService { /** * Scans document for all validation message generated by ASP.NET Core MVC, then tracks them. */ - private scanMessages(root: HTMLElement) { + private scanMessages(root: ParentNode) { /* If a validation span explicitly declares a form, we group the span with that form. */ let validationMessageElements = Array.from(root.querySelectorAll('span[form]')); for (let span of validationMessageElements) { @@ -884,12 +884,12 @@ export class ValidationService { /** * Scans the entire document for input elements to be validated. */ - private scanInputs(root: HTMLElement) { + private scanInputs(root: ParentNode) { let inputs = Array.from(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") { + if (root instanceof HTMLElement && root.getAttribute("data-val") === "true") { inputs.push(root); } @@ -1097,7 +1097,7 @@ export class ValidationService { * 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. */ - bootstrap(options?: { watch?: boolean, root?: HTMLElement }) { + bootstrap(options?: { watch?: boolean, root?: ParentNode }) { options = options || {}; this.addMvcProviders(); @@ -1125,7 +1125,7 @@ export class ValidationService { /** * Scans the provided root element for any validation directives and attaches behavior to them. */ - scan(root: HTMLElement) { + scan(root: ParentNode) { this.logger.log('Scanning', root); this.scanMessages(root); this.scanInputs(root); @@ -1135,7 +1135,7 @@ export class ValidationService { * 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. */ - watch(root: HTMLElement) { + watch(root: ParentNode) { this.observer = new MutationObserver(mutations => { mutations.forEach(mutation => { this.observed(mutation); diff --git a/types/index.d.ts b/types/index.d.ts index e49b35d..ab83d3b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -286,17 +286,17 @@ export declare class ValidationService { */ bootstrap(options?: { watch?: boolean; - root?: HTMLElement; + root?: ParentNode; }): void; /** * Scans the provided root element for any validation directives and attaches behavior to them. */ - scan(root: HTMLElement): void; + scan(root: ParentNode): void; /** * 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. */ - watch(root: HTMLElement): void; + watch(root: ParentNode): void; private observed; /** * Override CSS class name for input validation error. Default: 'input-validation-error' From 0fd170010762344a8bd51dac2f8d23a83ab5f693 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Mon, 19 Jun 2023 23:55:26 -0500 Subject: [PATCH 3/6] Rename realSelector; it's a name --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0928d5a..319e705 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,7 +72,7 @@ function getRelativeFormElement(element: HTMLInputElement, selector: string) { // example result element name: Form.Password, __RequestVerificationToken let elementName = element.name; - let realSelector = selector.substring(2); // Password, __RequestVerificationToken + let selectedName = selector.substring(2); // Password, __RequestVerificationToken let objectName = ''; let dotLocation = elementName.lastIndexOf('.'); @@ -81,7 +81,7 @@ function getRelativeFormElement(element: HTMLInputElement, selector: string) { objectName = elementName.substring(0, dotLocation); // Form.Password - let relativeElementName = objectName + '.' + realSelector; + let relativeElementName = objectName + '.' + selectedName; let relativeElement = document.getElementsByName(relativeElementName)[0]; if (relativeElement) { return relativeElement; @@ -89,7 +89,7 @@ function getRelativeFormElement(element: HTMLInputElement, selector: string) { } // __RequestVerificationToken - return element.form.querySelector(`[name=${realSelector}]`); + return element.form.querySelector(`[name=${selectedName}]`); } /** From 1394fe2374fc6b8758cddd496d96d87dfb5ba292 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Mon, 19 Jun 2023 23:55:26 -0500 Subject: [PATCH 4/6] ValidatableElement = input|select|textarea --- src/index.ts | 55 +++++++++++++++++++++++++++++++++++------------- types/index.d.ts | 20 +++++++++++++----- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index 319e705..28e6464 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,31 @@ const nullLogger = new (class implements Logger { warn = globalThis.console.warn; })(); +/** + * An `HTMLElement` that can be validated (`input`, `select`, `textarea`). + */ +export type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; + +/** + * Checks if `element` is validatable (`input`, `select`, `textarea`). + * @param element The element to check. + * @returns `true` if validatable, otherwise `false`. + */ +export const isValidatable = (element: Node): element is ValidatableElement => + element instanceof HTMLInputElement + || element instanceof HTMLSelectElement + || element instanceof HTMLTextAreaElement; + +const validatableElementTypes = ['input', 'select', 'textarea']; + +/** + * Generates a selector to match validatable elements (`input`, `select`, `textarea`). + * @param selector An optional selector to apply to the valid input types, e.g. `[data-val="true"]`. + * @returns The validatable elements. + */ +const validatableSelector = (selector?: string) => + validatableElementTypes.map(t => `${t}${selector || ''}`).join(','); + /** * Parameters passed into validation providers from the element attributes. * error property is read from data-val-[Provider Name] attribute. @@ -49,7 +74,7 @@ export type ValidationDirective = { * String return signifies failed validation, which then will be used as the validation error message. * Promise return signifies asynchronous plugin behavior, with same behavior as Boolean or String. */ -export type ValidationProvider = (value: string, element: HTMLInputElement, params: StringKeyValuePair) => boolean | string | Promise; +export type ValidationProvider = (value: string, element: ValidatableElement, params: StringKeyValuePair) => boolean | string | Promise; /** * Callback to receive the result of validating a form. @@ -66,7 +91,7 @@ type Validator = () => Promise; * @param element - The input to validate * @param selector - Used to find the field. Ex. *.Password where * replaces whatever prefixes asp.net might add. */ -function getRelativeFormElement(element: HTMLInputElement, selector: string) { +function getRelativeFormElement(element: ValidatableElement, selector: string): ValidatableElement { // example elementName: Form.PasswordConfirm, Form.Email // example selector (dafuq): *.Password, *.__RequestVerificationToken // example result element name: Form.Password, __RequestVerificationToken @@ -83,13 +108,13 @@ function getRelativeFormElement(element: HTMLInputElement, selector: string) { // Form.Password let relativeElementName = objectName + '.' + selectedName; let relativeElement = document.getElementsByName(relativeElementName)[0]; - if (relativeElement) { + if (isValidatable(relativeElement)) { return relativeElement; } } // __RequestVerificationToken - return element.form.querySelector(`[name=${selectedName}]`); + return element.form.querySelector(validatableSelector(`[name=${selectedName}]`)); } /** @@ -149,7 +174,7 @@ export class MvcValidationProviders { return true; } - let otherElement = getRelativeFormElement(element, params.other) as HTMLInputElement; + let otherElement = getRelativeFormElement(element, params.other); if (!otherElement) { return true; } @@ -309,7 +334,7 @@ export class MvcValidationProviders { for (let fieldSelector of fieldSelectors) { let fieldName = fieldSelector.substr(2); - let fieldElement = getRelativeFormElement(element, fieldSelector) as HTMLInputElement; + let fieldElement = getRelativeFormElement(element, fieldSelector); let hasValue = Boolean(fieldElement && fieldElement.value); if (!hasValue) { @@ -622,7 +647,7 @@ export class ValidationService { } // Retrieves the validation span for the input. - private getMessageFor(input: HTMLInputElement) { + private getMessageFor(input: ValidatableElement) { let formId = this.getElementUID(input.form); let name = `${formId}:${input.name}`; return this.messageFor[name]; @@ -819,7 +844,7 @@ export class ValidationService { let uids = this.formInputs[formUID]; for (let uid of uids) { - let input = this.elementByUID[uid] as HTMLInputElement; + let input = this.elementByUID[uid] as ValidatableElement; if (input.classList.contains(this.ValidationInputCssClassName)) { input.classList.remove(this.ValidationInputCssClassName); } @@ -846,7 +871,7 @@ export class ValidationService { * Triggers a debounced live validation when input value changes. * @param input */ - addInput(input: HTMLInputElement) { + addInput(input: ValidatableElement) { let uid = this.getElementUID(input); let directives = this.parseDirectives(input.attributes); @@ -885,16 +910,16 @@ export class ValidationService { * Scans the entire document for input elements to be validated. */ private scanInputs(root: ParentNode) { - let inputs = Array.from(root.querySelectorAll('[data-val="true"]')); + let inputs = Array.from(root.querySelectorAll(validatableSelector('[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 instanceof HTMLElement && root.getAttribute("data-val") === "true") { + if (isValidatable(root) && root.getAttribute("data-val") === "true") { inputs.push(root); } for (let i = 0; i < inputs.length; i++) { - let input = inputs[i] as HTMLInputElement; + let input = inputs[i]; this.addInput(input); } } @@ -956,7 +981,7 @@ export class ValidationService { * @param input * @param message */ - addError(input: HTMLInputElement, message: string) { + addError(input: ValidatableElement, message: string) { let spans = this.getMessageFor(input); if (spans) { for (let i = 0; i < spans.length; i++) { @@ -989,7 +1014,7 @@ export class ValidationService { * Removes an error message from an input element, which also updates the validation message elements and validation summary elements. * @param input */ - removeError(input: HTMLInputElement) { + removeError(input: ValidatableElement) { let spans = this.getMessageFor(input); if (spans) { for (let i = 0; i < spans.length; i++) { @@ -1022,7 +1047,7 @@ export class ValidationService { * @param input * @param directives */ - createValidator(input: HTMLInputElement, directives: ValidationDirective) { + createValidator(input: ValidatableElement, directives: ValidationDirective) { return async () => { // only validate visible fields diff --git a/types/index.d.ts b/types/index.d.ts index ab83d3b..3aec904 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -11,6 +11,16 @@ export interface Logger { log(message: string, ...args: any[]): void; warn(message: string, ...args: any[]): void; } +/** + * An `HTMLElement` that can be validated (`input`, `select`, `textarea`). + */ +export type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; +/** + * Checks if `element` is validatable (`input`, `select`, `textarea`). + * @param element The element to check. + * @returns `true` if validatable, otherwise `false`. + */ +export declare const isValidatable: (element: Node) => element is ValidatableElement; /** * Parameters passed into validation providers from the element attributes. * error property is read from data-val-[Provider Name] attribute. @@ -32,7 +42,7 @@ export type ValidationDirective = { * String return signifies failed validation, which then will be used as the validation error message. * Promise return signifies asynchronous plugin behavior, with same behavior as Boolean or String. */ -export type ValidationProvider = (value: string, element: HTMLInputElement, params: StringKeyValuePair) => boolean | string | Promise; +export type ValidationProvider = (value: string, element: ValidatableElement, params: StringKeyValuePair) => boolean | string | Promise; /** * Callback to receive the result of validating a form. */ @@ -237,7 +247,7 @@ export declare class ValidationService { * Triggers a debounced live validation when input value changes. * @param input */ - addInput(input: HTMLInputElement): void; + addInput(input: ValidatableElement): void; /** * Scans the entire document for input elements to be validated. */ @@ -255,18 +265,18 @@ export declare class ValidationService { * @param input * @param message */ - addError(input: HTMLInputElement, message: string): void; + addError(input: ValidatableElement, message: string): void; /** * Removes an error message from an input element, which also updates the validation message elements and validation summary elements. * @param input */ - removeError(input: HTMLInputElement): void; + removeError(input: ValidatableElement): void; /** * Returns a validation Promise factory for an input element, using given validation directives. * @param input * @param directives */ - createValidator(input: HTMLInputElement, directives: ValidationDirective): () => Promise; + createValidator(input: ValidatableElement, directives: ValidationDirective): () => Promise; /** * Checks if the provided input is hidden from the browser * @param input From 2a561c91a3fe99183f85e504799c5fd29750fa7b Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Wed, 21 Jun 2023 08:06:29 -0700 Subject: [PATCH 5/6] Add a textarea demo --- Pages/Index.cshtml | 6 ++++++ Pages/Index.cshtml.cs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/Pages/Index.cshtml b/Pages/Index.cshtml index 13c13ba..e245f7b 100644 --- a/Pages/Index.cshtml +++ b/Pages/Index.cshtml @@ -50,6 +50,12 @@ +
+ + + +
+
@foreach (var animal in Model.Animals) { diff --git a/Pages/Index.cshtml.cs b/Pages/Index.cshtml.cs index fd99930..3e34824 100644 --- a/Pages/Index.cshtml.cs +++ b/Pages/Index.cshtml.cs @@ -19,6 +19,10 @@ public class IndexModel : PageModel [Required] public string? Control { get; set; } + [BindProperty] + [Required] + public string? TextArea { get; set; } + [BindProperty] [Required] public List? SelectedAnimals { get; set; } From ea86e21bc5593d7998bd39fc50f3aa451d2cc3e4 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Wed, 21 Jun 2023 08:13:46 -0700 Subject: [PATCH 6/6] Bump version to 0.8.10 --- dist/aspnet-validation.js | 36 ++++++++++++++++++++++++------- dist/aspnet-validation.min.js | 2 +- dist/aspnet-validation.min.js.map | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/dist/aspnet-validation.js b/dist/aspnet-validation.js index d8704e8..7e47443 100644 --- a/dist/aspnet-validation.js +++ b/dist/aspnet-validation.js @@ -100,11 +100,12 @@ return /******/ (function(modules) { // webpackBootstrap /*!**********************!*\ !*** ./src/index.ts ***! \**********************/ -/*! exports provided: MvcValidationProviders, ValidationService */ +/*! exports provided: isValidatable, MvcValidationProviders, ValidationService */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isValidatable", function() { return isValidatable; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MvcValidationProviders", function() { return MvcValidationProviders; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ValidationService", function() { return ValidationService; }); var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { @@ -155,6 +156,25 @@ var nullLogger = new (/** @class */ (function () { }; return class_1; }()))(); +/** + * Checks if `element` is validatable (`input`, `select`, `textarea`). + * @param element The element to check. + * @returns `true` if validatable, otherwise `false`. + */ +var isValidatable = function (element) { + return element instanceof HTMLInputElement + || element instanceof HTMLSelectElement + || element instanceof HTMLTextAreaElement; +}; +var validatableElementTypes = ['input', 'select', 'textarea']; +/** + * Generates a selector to match validatable elements (`input`, `select`, `textarea`). + * @param selector An optional selector to apply to the valid input types, e.g. `[data-val="true"]`. + * @returns The validatable elements. + */ +var validatableSelector = function (selector) { + return validatableElementTypes.map(function (t) { return "".concat(t).concat(selector || ''); }).join(','); +}; /** * Resolves and returns the element referred by original element using ASP.NET selector logic. * @param element - The input to validate @@ -165,21 +185,21 @@ function getRelativeFormElement(element, selector) { // example selector (dafuq): *.Password, *.__RequestVerificationToken // example result element name: Form.Password, __RequestVerificationToken var elementName = element.name; - var realSelector = selector.substring(2); // Password, __RequestVerificationToken + var selectedName = selector.substring(2); // Password, __RequestVerificationToken var objectName = ''; var dotLocation = elementName.lastIndexOf('.'); if (dotLocation > -1) { // Form objectName = elementName.substring(0, dotLocation); // Form.Password - var relativeElementName = objectName + '.' + realSelector; + var relativeElementName = objectName + '.' + selectedName; var relativeElement = document.getElementsByName(relativeElementName)[0]; - if (relativeElement) { + if (isValidatable(relativeElement)) { return relativeElement; } } // __RequestVerificationToken - return element.form.querySelector("[name=".concat(realSelector, "]")); + return element.form.querySelector(validatableSelector("[name=".concat(selectedName, "]"))); } /** * Contains default implementations for ASP.NET Core MVC validation attributes. @@ -649,7 +669,7 @@ var ValidationService = /** @class */ (function () { } // Otherwise if a validation message span is inside a form, we group the span with the form it's inside. var forms = Array.from(root.querySelectorAll('form')); - if (root.tagName === 'form') { + if (root instanceof HTMLFormElement) { // 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. forms.push(root); @@ -893,10 +913,10 @@ var ValidationService = /** @class */ (function () { * Scans the entire document for input elements to be validated. */ ValidationService.prototype.scanInputs = function (root) { - var inputs = Array.from(root.querySelectorAll('[data-val="true"]')); + var inputs = Array.from(root.querySelectorAll(validatableSelector('[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") { + if (isValidatable(root) && root.getAttribute("data-val") === "true") { inputs.push(root); } for (var i = 0; i < inputs.length; i++) { diff --git a/dist/aspnet-validation.min.js b/dist/aspnet-validation.min.js index eae46e2..022cf91 100644 --- a/dist/aspnet-validation.min.js +++ b/dist/aspnet-validation.min.js @@ -1,2 +1,2 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.aspnetValidation=e():t.aspnetValidation=e()}(window,(function(){return function(t){var e={};function r(n){if(e[n])return e[n].exports;var a=e[n]={i:n,l:!1,exports:{}};return t[n].call(a.exports,a,a.exports,r),a.l=!0,a.exports}return r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var a in t)r.d(n,a,function(e){return t[e]}.bind(null,a));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,r){"use strict";r.r(e),r.d(e,"MvcValidationProviders",(function(){return o})),r.d(e,"ValidationService",(function(){return l}));var n=function(t,e,r,n){return new(r||(r=Promise))((function(a,i){function s(t){try{l(n.next(t))}catch(t){i(t)}}function o(t){try{l(n.throw(t))}catch(t){i(t)}}function l(t){var e;t.done?a(t.value):(e=t.value,e instanceof r?e:new r((function(t){t(e)}))).then(s,o)}l((n=n.apply(t,e||[])).next())}))},a=function(t,e){var r,n,a,i,s={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return i={next:o(0),throw:o(1),return:o(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function o(o){return function(l){return function(o){if(r)throw new TypeError("Generator is already executing.");for(;i&&(i=0,o[0]&&(s=0)),s;)try{if(r=1,n&&(a=2&o[0]?n.return:o[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,o[1])).done)return a;switch(n=0,a&&(o=[2&o[0],a.value]),o[0]){case 0:case 1:a=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,n=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(a=s.trys,(a=a.length>0&&a[a.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!a||o[1]>a[0]&&o[1]-1){var i=r.substring(0,a)+"."+n,s=document.getElementsByName(i)[0];if(s)return s}return t.form.querySelector("[name=".concat(n,"]"))}var o=function(){this.required=function(t,e,r){var n=e.type.toLowerCase();if("checkbox"===n||"radio"===n){for(var a=0,i=Array.from(e.form.querySelectorAll("input[name='".concat(e.name,"'][type='").concat(n,"']")));aa)return!1}return!0},this.compare=function(t,e,r){if(!r.other)return!0;var n=s(e,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,a,i=0,s=0,o=!1;if((t=t.replace(/\D/g,"")).length<13||t.length>19)return!1;for(n=t.length-1;n>=0;n--)a=t.charAt(n),s=parseInt(a,10),o&&(s*=2)>9&&(s-=9),i+=s,o=!o;return i%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(","),a={},i=0,o=n;i=200&&n.status<300){var a=JSON.parse(n.responseText);t(a)}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})}}))}},l=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),a=e.elementEvents[n];a&&a(void 0,r)},this.preValidate=function(t){t.preventDefault(),t.stopImmediatePropagation()},this.handleValidated=function(t,r,n){r?e.submitValidForm(t,n):e.focusFirstInvalid(t)},this.submitValidForm=function(t,e){var r=new SubmitEvent("submit",e);t.dispatchEvent(r)&&t.submit()},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 a=e.elementByUID[n[0]];a&&a.focus()}},this.isValid=function(t,r,n){void 0===r&&(r=!0),r&&e.validateForm(t,n);var a=e.getElementUID(t);return 0==e.formInputs[a].filter((function(t){return e.summary[t]})).length},this.isFieldValid=function(t,r,n){if(void 0===r&&(r=!0),r){var a=t.closest("form");null!=a&&e.validateForm(a,n)}var i=e.getElementUID(t);return null!=e.summary[i]},this.ValidationInputCssClassName="input-validation-error",this.ValidationInputValidCssClassName="input-validation-valid",this.ValidationMessageCssClassName="field-validation-error",this.ValidationMessageValidCssClassName="field-validation-valid",this.ValidationSummaryCssClassName="validation-summary-errors",this.ValidationSummaryValidCssClassName="validation-summary-valid",this.logger=t||i}return t.prototype.addProvider=function(t,e){this.providers[t]||(this.logger.log("Registered provider: %s",t),this.providers[t]=e)},t.prototype.addMvcProviders=function(){var t=new o;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){for(var e=0,r=Array.from(t.querySelectorAll("span[form]"));e0&&a[a.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!a||o[1]>a[0]&&o[1]-1){var i=r.substring(0,a)+"."+n,o=document.getElementsByName(i)[0];if(s(o))return o}return t.form.querySelector(l("[name=".concat(n,"]")))}var d=function(){this.required=function(t,e,r){var n=e.type.toLowerCase();if("checkbox"===n||"radio"===n){for(var a=0,i=Array.from(e.form.querySelectorAll("input[name='".concat(e.name,"'][type='").concat(n,"']")));aa)return!1}return!0},this.compare=function(t,e,r){if(!r.other)return!0;var n=u(e,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,a,i=0,s=0,o=!1;if((t=t.replace(/\D/g,"")).length<13||t.length>19)return!1;for(n=t.length-1;n>=0;n--)a=t.charAt(n),s=parseInt(a,10),o&&(s*=2)>9&&(s-=9),i+=s,o=!o;return i%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(","),a={},i=0,s=n;i=200&&n.status<300){var a=JSON.parse(n.responseText);t(a)}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})}}))}},c=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),a=e.elementEvents[n];a&&a(void 0,r)},this.preValidate=function(t){t.preventDefault(),t.stopImmediatePropagation()},this.handleValidated=function(t,r,n){r?e.submitValidForm(t,n):e.focusFirstInvalid(t)},this.submitValidForm=function(t,e){var r=new SubmitEvent("submit",e);t.dispatchEvent(r)&&t.submit()},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 a=e.elementByUID[n[0]];a&&a.focus()}},this.isValid=function(t,r,n){void 0===r&&(r=!0),r&&e.validateForm(t,n);var a=e.getElementUID(t);return 0==e.formInputs[a].filter((function(t){return e.summary[t]})).length},this.isFieldValid=function(t,r,n){if(void 0===r&&(r=!0),r){var a=t.closest("form");null!=a&&e.validateForm(a,n)}var i=e.getElementUID(t);return null!=e.summary[i]},this.ValidationInputCssClassName="input-validation-error",this.ValidationInputValidCssClassName="input-validation-valid",this.ValidationMessageCssClassName="field-validation-error",this.ValidationMessageValidCssClassName="field-validation-valid",this.ValidationSummaryCssClassName="validation-summary-errors",this.ValidationSummaryValidCssClassName="validation-summary-valid",this.logger=t||i}return t.prototype.addProvider=function(t,e){this.providers[t]||(this.logger.log("Registered provider: %s",t),this.providers[t]=e)},t.prototype.addMvcProviders=function(){var t=new d;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){for(var e=0,r=Array.from(t.querySelectorAll("span[form]"));e\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 * A simple logging interface that mirrors the Console object.\n */\nexport interface Logger {\n log(message: string, ...args: any[]): void;\n warn(message: string, ...args: any[]): void;\n}\n\nconst nullLogger = new (class implements Logger {\n log(_: string, ..._args: any[]): void { }\n warn = globalThis.console.warn;\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 * Callback to receive the result of validating a form.\n */\nexport type ValidatedCallback = (success: boolean) => void;\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 element - The input to validate\n * @param selector - Used to find the field. Ex. *.Password where * replaces whatever prefixes asp.net might add.\n */\nfunction getRelativeFormElement(element: HTMLInputElement, 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 elementName = element.name;\n let realSelector = selector.substring(2); // Password, __RequestVerificationToken\n let objectName = '';\n\n let dotLocation = elementName.lastIndexOf('.');\n if (dotLocation > -1) {\n // Form\n objectName = elementName.substring(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 element.form.querySelector(`[name=${realSelector}]`);\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 // Handle single and multiple checkboxes/radio buttons.\n const elementType = element.type.toLowerCase();\n if (elementType === \"checkbox\" || elementType === \"radio\") {\n const allElementsOfThisName = Array.from(element.form.querySelectorAll(`input[name='${element.name}'][type='${elementType}']`));\n for (let element of allElementsOfThisName) {\n if (element instanceof HTMLInputElement && element.checked === true) {\n return true;\n }\n }\n\n return false;\n }\n // Default behavior otherwise.\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, 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, 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\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\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 * Responsible 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