From 9203770d2a3564bcb79749d9e32ea7f0d574e7b8 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 2 Jan 2024 12:20:38 -0800 Subject: [PATCH 01/12] Add simple disabled input demo --- Pages/Demos/DisabledInputs.cshtml | 80 ++++++++++++++++++++++++++++ Pages/Demos/DisabledInputs.cshtml.cs | 34 ++++++++++++ Pages/Index.cshtml | 1 + 3 files changed, 115 insertions(+) create mode 100644 Pages/Demos/DisabledInputs.cshtml create mode 100644 Pages/Demos/DisabledInputs.cshtml.cs diff --git a/Pages/Demos/DisabledInputs.cshtml b/Pages/Demos/DisabledInputs.cshtml new file mode 100644 index 0000000..be1bcee --- /dev/null +++ b/Pages/Demos/DisabledInputs.cshtml @@ -0,0 +1,80 @@ +@page +@model DemoWeb.Pages.Demos.DisabledInputs + +@{ + Layout = "Shared/_Layout"; +} + + + +
+ Disabled inputs + +
+
+

+ This simple test demonstrates that we don't validate disabled inputs. +

+ +
+ +
+ +
+ +
+ +
+ + +
+ + +
+
+
+ +
+ Disabled fieldset + +
+
+

+ This simple test demonstrates that we don't validate disabled fieldsets. +

+ +
+ +
+ +
+ +
+ +
+ + +
+ + +
+
+
+ + diff --git a/Pages/Demos/DisabledInputs.cshtml.cs b/Pages/Demos/DisabledInputs.cshtml.cs new file mode 100644 index 0000000..af60ec3 --- /dev/null +++ b/Pages/Demos/DisabledInputs.cshtml.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace DemoWeb.Pages.Demos; + +public class DisabledInputs : PageModel +{ + [TempData] + public string? StatusMessage { get; set; } + + [BindProperty] + [Required] + public string? Value1 { get; set; } + + [BindProperty] + [Required] + public string? Value2 { get; set; } + + public bool IsChecked { get; set; } + + [Remote("CheckboxRemote", "Validations", HttpMethod = "Post", + ErrorMessage = "Must match other checkbox.", + AdditionalFields = $"{nameof(IsChecked)}" + )] + public bool IsCheckedToo { get; set; } + + public IActionResult OnPost() + { + StatusMessage = "Form was submitted. Any validation errors are due to server side validation"; + + return RedirectToPage(); + } +} \ No newline at end of file diff --git a/Pages/Index.cshtml b/Pages/Index.cshtml index 31996c7..1657624 100644 --- a/Pages/Index.cshtml +++ b/Pages/Index.cshtml @@ -11,6 +11,7 @@
  • Removed Inputs
  • Select Input and Validation Summary
  • Form Action
  • +
  • Disabled inputs
  • @if (Model.StatusMessage != null) { From 5ec330fd694e999ed46e3e4742b7a5d1b5538902 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 2 Jan 2024 12:42:05 -0800 Subject: [PATCH 02/12] Do not validate disabled inputs Fixes #90 --- src/index.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index f7380ac..bedf15f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1244,8 +1244,8 @@ export class ValidationService { */ createValidator(input: ValidatableElement, directives: ValidationDirective) { return async () => { - // only validate visible fields - if (!this.isHidden(input)) { + // only validate visible and enabled fields + if (!this.isHidden(input) && !this.isDisabled(input)) { for (let key in directives) { let directive = directives[key]; let provider = this.providers[key]; @@ -1296,6 +1296,15 @@ export class ValidationService { return !(this.allowHiddenFields || input.offsetWidth || input.offsetHeight || input.getClientRects().length); } + /** + * Checks if the provided input is disabled + * @param input + * @returns + */ + private isDisabled(input: HTMLElement) { + return input.getAttribute('disabled') !== null; + } + /** * Adds addClass and removes removeClass * @param element Element to modify From 6330d4e885798368b1f79a5d6e991403d67b49e7 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Fri, 5 Jan 2024 13:42:16 -0800 Subject: [PATCH 03/12] Use ValidatableElement instead of HTMLElement Co-authored-by: Keith Dahlby --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index bedf15f..5feb17a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1301,8 +1301,8 @@ export class ValidationService { * @param input * @returns */ - private isDisabled(input: HTMLElement) { - return input.getAttribute('disabled') !== null; + private isDisabled(input: ValidatableElement) { + return input.disabled; } /** From 4cfd9b39be4e521f4c684ecabf26496647543cbd Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Fri, 5 Jan 2024 13:57:15 -0800 Subject: [PATCH 04/12] Do not track disabled inputs --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index 5feb17a..d0d3eab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1013,6 +1013,10 @@ export class ValidationService { * @param input */ addInput(input: ValidatableElement) { + if (input.disabled) { + return; + } + let uid = this.getElementUID(input); let directives = this.parseDirectives(input.attributes); From 32a01bcb9cd4db3c3d4d59076af1afa4f41fa8d6 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Fri, 5 Jan 2024 15:05:02 -0800 Subject: [PATCH 05/12] Update Disabled Inputs demo to scan for changes --- Pages/Demos/Checkboxes.cshtml | 6 +++ Pages/Demos/DisabledInputs.cshtml | 30 +++++++++-- Pages/Demos/DisabledInputs.cshtml.cs | 2 +- Pages/Demos/FormAction.cshtml | 9 +++- Pages/Demos/RemovedInputs.cshtml | 61 +++++++++++---------- Pages/Demos/SelectInput.cshtml | 9 +++- Pages/Demos/SubmitButton.cshtml | 79 +++++++++++++++------------- Pages/Shared/_Layout.cshtml | 4 -- 8 files changed, 127 insertions(+), 73 deletions(-) diff --git a/Pages/Demos/Checkboxes.cshtml b/Pages/Demos/Checkboxes.cshtml index c1a6df1..c0ed782 100644 --- a/Pages/Demos/Checkboxes.cshtml +++ b/Pages/Demos/Checkboxes.cshtml @@ -124,3 +124,9 @@ Reset } +@section Scripts { + +} \ No newline at end of file diff --git a/Pages/Demos/DisabledInputs.cshtml b/Pages/Demos/DisabledInputs.cshtml index be1bcee..769247d 100644 --- a/Pages/Demos/DisabledInputs.cshtml +++ b/Pages/Demos/DisabledInputs.cshtml @@ -17,15 +17,17 @@

    -
    -
    @@ -77,4 +79,26 @@ - +@section Scripts { + +} \ No newline at end of file diff --git a/Pages/Demos/DisabledInputs.cshtml.cs b/Pages/Demos/DisabledInputs.cshtml.cs index af60ec3..c5304e2 100644 --- a/Pages/Demos/DisabledInputs.cshtml.cs +++ b/Pages/Demos/DisabledInputs.cshtml.cs @@ -27,7 +27,7 @@ public class DisabledInputs : PageModel public IActionResult OnPost() { - StatusMessage = "Form was submitted. Any validation errors are due to server side validation"; + StatusMessage = "Form was submitted to server. Any validation errors that may be present are due to server side validation, not client."; return RedirectToPage(); } diff --git a/Pages/Demos/FormAction.cshtml b/Pages/Demos/FormAction.cshtml index 98846df..a411b24 100644 --- a/Pages/Demos/FormAction.cshtml +++ b/Pages/Demos/FormAction.cshtml @@ -17,4 +17,11 @@ - \ No newline at end of file + + +@section Scripts { + +} diff --git a/Pages/Demos/RemovedInputs.cshtml b/Pages/Demos/RemovedInputs.cshtml index 70d5a75..ad48e3c 100644 --- a/Pages/Demos/RemovedInputs.cshtml +++ b/Pages/Demos/RemovedInputs.cshtml @@ -8,31 +8,38 @@
    -Required ASP.NET Checkboxes with hidden input - -
    -
    -

    - This simple test demonstrates that we don't validate removed inputs. -

    - -
    - + Required ASP.NET Checkboxes with hidden input + + +
    +

    + This simple test demonstrates that we don't validate removed inputs. +

    + +
    + +
    + +
    + +
    + + + +
    - -
    - -
    - - - - -
    - -
    \ No newline at end of file + + + +@section Scripts { + +} \ No newline at end of file diff --git a/Pages/Demos/SelectInput.cshtml b/Pages/Demos/SelectInput.cshtml index 861f6da..0a8d50c 100644 --- a/Pages/Demos/SelectInput.cshtml +++ b/Pages/Demos/SelectInput.cshtml @@ -23,4 +23,11 @@ - \ No newline at end of file + + +@section Scripts { + +} \ No newline at end of file diff --git a/Pages/Demos/SubmitButton.cshtml b/Pages/Demos/SubmitButton.cshtml index bf5b6fa..ce4956e 100644 --- a/Pages/Demos/SubmitButton.cshtml +++ b/Pages/Demos/SubmitButton.cshtml @@ -6,39 +6,46 @@ }
    -Required ASP.NET Checkboxes with hidden input - -
    -
    -

    - This simple test demonstrates that we ensure that submit buttons. -

    - - - - - - - - - - @if (Model.SubmitButtonValue is not null) { - - The submitted button value is "@Model.SubmitButtonValue". - - -

    The submitted form collection:

    - - @foreach (var item in Request.Form) { - - - - - } -
    @item.Key@item.Value
    - } - - -
    -
    -
    \ No newline at end of file + Required ASP.NET Checkboxes with hidden input + +
    +
    +

    + This simple test demonstrates that we ensure that submit buttons. +

    + + + + + + + + + + @if (Model.SubmitButtonValue is not null) { + + The submitted button value is "@Model.SubmitButtonValue". + + +

    The submitted form collection:

    + + @foreach (var item in Request.Form) { + + + + + } +
    @item.Key@item.Value
    + } + + +
    +
    + + +@section Scripts { + +} \ No newline at end of file diff --git a/Pages/Shared/_Layout.cshtml b/Pages/Shared/_Layout.cshtml index b3abb18..6236ab6 100644 --- a/Pages/Shared/_Layout.cshtml +++ b/Pages/Shared/_Layout.cshtml @@ -10,10 +10,6 @@ @RenderBody() - @RenderSection("scripts", required: false) From 024a02acf52ea0b92b0739da88064450c8cfaaaa Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Fri, 5 Jan 2024 15:05:37 -0800 Subject: [PATCH 06/12] Minor tidying up --- src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index d0d3eab..208c44e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,8 +106,8 @@ function getRelativeFormElement(element: ValidatableElement, selector: string): objectName = elementName.substring(0, dotLocation); // Form.Password - let relativeElementName = objectName + '.' + selectedName; - let relativeElement = document.getElementsByName(relativeElementName)[0]; + const relativeElementName = objectName + '.' + selectedName; + const relativeElement = document.getElementsByName(relativeElementName)[0]; if (isValidatable(relativeElement)) { return relativeElement; } @@ -656,7 +656,7 @@ export class ValidationService { // https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript/2117523#2117523 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } @@ -1336,7 +1336,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. - * @param options.addNoValidate If set to true (the default), a novalidate attribute will be added to the containing form in validate elemets. + * @param options.addNoValidate If set to true (the default), a novalidate attribute will be added to the containing form in validate elements. */ bootstrap(options?: Partial) { Object.assign(this.options, options); From 52446b94e8f8e934a5ef8c5cd1153020a4cdb3d3 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Fri, 5 Jan 2024 19:00:02 -0800 Subject: [PATCH 07/12] Reset field when disabled. --- src/index.ts | 73 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/index.ts b/src/index.ts index 208c44e..e0e558b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -968,31 +968,35 @@ export class ValidationService { form.addEventListener('submit', cb); form.addEventListener('reset', e => { - let uids = this.formInputs[formUID]; + const uids = this.formInputs[formUID]; for (let uid of uids) { - let input = this.elementByUID[uid] as ValidatableElement; - if (input.classList.contains(this.ValidationInputCssClassName)) { - input.classList.remove(this.ValidationInputCssClassName); - } - if (input.classList.contains(this.ValidationInputValidCssClassName)) { - input.classList.remove(this.ValidationInputValidCssClassName); - } - - let spans = this.getMessageFor(input); - if (spans) { - for (let i = 0; i < spans.length; i++) { - spans[i].innerHTML = ''; - } - } - - delete this.summary[uid]; + this.resetField(uid); } this.renderSummary(); }); this.formEvents[formUID] = cb; } + private resetField(inputUID: string) { + let input = this.elementByUID[inputUID] as ValidatableElement; + if (input.classList.contains(this.ValidationInputCssClassName)) { + input.classList.remove(this.ValidationInputCssClassName); + } + if (input.classList.contains(this.ValidationInputValidCssClassName)) { + input.classList.remove(this.ValidationInputValidCssClassName); + } + + let spans = this.getMessageFor(input); + if (spans) { + for (let i = 0; i < spans.length; i++) { + spans[i].innerHTML = ''; + } + } + + delete this.summary[inputUID]; + } + private untrackFormInput(form: HTMLFormElement, inputUID: string) { let formUID = this.getElementUID(form); if (!this.formInputs[formUID]) { @@ -1081,7 +1085,7 @@ export class ValidationService { for (let i = 0; i < inputs.length; i++) { let input = inputs[i]; - if (remove) { + if (remove || input.disabled) { this.removeInput(input); } else { @@ -1417,15 +1421,30 @@ export class ValidationService { } } else if (mutation.type === 'attributes') { if (mutation.target instanceof HTMLElement) { - const oldValue = mutation.oldValue ?? ''; - const newValue = mutation.target.attributes[mutation.attributeName]?.value ?? ''; - this.logger.log("Attribute '%s' changed from '%s' to '%s'", - mutation.attributeName, - oldValue, - newValue, - mutation.target); - if (oldValue !== newValue) { - this.scan(mutation.target); + const attributeName = mutation.attributeName; + + // Special case for disabled. + if (attributeName === 'disabled') { + const target = mutation.target as ValidatableElement; + if (target.disabled) { + this.remove(target); + this.resetField(this.getElementUID(target)); + } + else { + this.scan(target); + } + } + else { + const oldValue = mutation.oldValue ?? ''; + const newValue = mutation.target.attributes[mutation.attributeName]?.value ?? ''; + this.logger.log("Attribute '%s' changed from '%s' to '%s'", + mutation.attributeName, + oldValue, + newValue, + mutation.target); + if (oldValue !== newValue) { + this.scan(mutation.target); + } } } } From ef8ef8cafd4dce0e7ea841a39d73a286c3dda6f7 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Mon, 8 Jan 2024 16:22:07 -0800 Subject: [PATCH 08/12] Add example of handling disabled/enabled with no watch --- Pages/Demos/DisabledInputsNoWatch.cshtml | 107 ++++++++++++++++++ ...tml.cs => DisabledInputsNoWatch.cshtml.cs} | 2 +- ....cshtml => DisabledInputsWithWatch.cshtml} | 2 +- Pages/Demos/DisabledInputsWithWatch.cshtml.cs | 34 ++++++ Pages/Index.cshtml | 3 +- 5 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 Pages/Demos/DisabledInputsNoWatch.cshtml rename Pages/Demos/{DisabledInputs.cshtml.cs => DisabledInputsNoWatch.cshtml.cs} (93%) rename Pages/Demos/{DisabledInputs.cshtml => DisabledInputsWithWatch.cshtml} (98%) create mode 100644 Pages/Demos/DisabledInputsWithWatch.cshtml.cs diff --git a/Pages/Demos/DisabledInputsNoWatch.cshtml b/Pages/Demos/DisabledInputsNoWatch.cshtml new file mode 100644 index 0000000..e871134 --- /dev/null +++ b/Pages/Demos/DisabledInputsNoWatch.cshtml @@ -0,0 +1,107 @@ +@page +@model DemoWeb.Pages.Demos.DisabledInputsNoWatchPageModel + +@{ + Layout = "Shared/_Layout"; +} + + + +
    + Disabled inputs + +
    +
    +

    + This simple test demonstrates that we don't validate disabled inputs. +

    + +
    + +
    + +
    + +
    + +
    + + +
    + + +
    +
    +
    + +
    + Disabled fieldset + +
    +
    +

    + This simple test demonstrates that we don't validate disabled fieldsets. +

    + +
    + +
    + +
    + +
    + +
    + + +
    + + +
    +
    +
    + +@section Scripts { + +} \ No newline at end of file diff --git a/Pages/Demos/DisabledInputs.cshtml.cs b/Pages/Demos/DisabledInputsNoWatch.cshtml.cs similarity index 93% rename from Pages/Demos/DisabledInputs.cshtml.cs rename to Pages/Demos/DisabledInputsNoWatch.cshtml.cs index c5304e2..b0e2cfd 100644 --- a/Pages/Demos/DisabledInputs.cshtml.cs +++ b/Pages/Demos/DisabledInputsNoWatch.cshtml.cs @@ -4,7 +4,7 @@ namespace DemoWeb.Pages.Demos; -public class DisabledInputs : PageModel +public class DisabledInputsNoWatchPageModel : PageModel { [TempData] public string? StatusMessage { get; set; } diff --git a/Pages/Demos/DisabledInputs.cshtml b/Pages/Demos/DisabledInputsWithWatch.cshtml similarity index 98% rename from Pages/Demos/DisabledInputs.cshtml rename to Pages/Demos/DisabledInputsWithWatch.cshtml index 769247d..315916a 100644 --- a/Pages/Demos/DisabledInputs.cshtml +++ b/Pages/Demos/DisabledInputsWithWatch.cshtml @@ -1,5 +1,5 @@ @page -@model DemoWeb.Pages.Demos.DisabledInputs +@model DemoWeb.Pages.Demos.DisabledInputsWithWatchPageModel @{ Layout = "Shared/_Layout"; diff --git a/Pages/Demos/DisabledInputsWithWatch.cshtml.cs b/Pages/Demos/DisabledInputsWithWatch.cshtml.cs new file mode 100644 index 0000000..77c9f70 --- /dev/null +++ b/Pages/Demos/DisabledInputsWithWatch.cshtml.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace DemoWeb.Pages.Demos; + +public class DisabledInputsWithWatchPageModel : PageModel +{ + [TempData] + public string? StatusMessage { get; set; } + + [BindProperty] + [Required] + public string? Value1 { get; set; } + + [BindProperty] + [Required] + public string? Value2 { get; set; } + + public bool IsChecked { get; set; } + + [Remote("CheckboxRemote", "Validations", HttpMethod = "Post", + ErrorMessage = "Must match other checkbox.", + AdditionalFields = $"{nameof(IsChecked)}" + )] + public bool IsCheckedToo { get; set; } + + public IActionResult OnPost() + { + StatusMessage = "Form was submitted to server. Any validation errors that may be present are due to server side validation, not client."; + + return RedirectToPage(); + } +} \ No newline at end of file diff --git a/Pages/Index.cshtml b/Pages/Index.cshtml index 1657624..a8d6477 100644 --- a/Pages/Index.cshtml +++ b/Pages/Index.cshtml @@ -11,7 +11,8 @@
  • Removed Inputs
  • Select Input and Validation Summary
  • Form Action
  • -
  • Disabled inputs
  • +
  • Disabled inputs ({watch: true})
  • +
  • Disabled inputs (no watch)
  • @if (Model.StatusMessage != null) { From 983f1c05d8f8aed001c05b6e63226bb556f662c8 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Mon, 8 Jan 2024 16:22:43 -0800 Subject: [PATCH 09/12] Add a new reset method for resetting a single form input This can be called after changing the state of a form input. --- src/index.ts | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index e0e558b..1a45397 100644 --- a/src/index.ts +++ b/src/index.ts @@ -978,6 +978,18 @@ export class ValidationService { this.formEvents[formUID] = cb; } + /* + Reset the state of a validatable input. This is used when it's enabled or disabled. + */ + reset(input: HTMLElement) { + if (this.isDisabled(input)) { + this.resetField(this.getElementUID(input)); + } + else { + this.scan(input); + } + } + private resetField(inputUID: string) { let input = this.elementByUID[inputUID] as ValidatableElement; if (input.classList.contains(this.ValidationInputCssClassName)) { @@ -1017,7 +1029,7 @@ export class ValidationService { * @param input */ addInput(input: ValidatableElement) { - if (input.disabled) { + if (this.isDisabled(input)) { return; } @@ -1085,7 +1097,7 @@ export class ValidationService { for (let i = 0; i < inputs.length; i++) { let input = inputs[i]; - if (remove || input.disabled) { + if (remove || this.isDisabled(input)) { this.removeInput(input); } else { @@ -1309,8 +1321,10 @@ export class ValidationService { * @param input * @returns */ - private isDisabled(input: ValidatableElement) { - return input.disabled; + private isDisabled(input: Element) { + // Test the the input is validatable and disabled + const validatableElement = input as ValidatableElement; + return validatableElement && validatableElement.disabled; } /** @@ -1320,7 +1334,7 @@ export class ValidationService { * @param removeClass Class to remove */ private swapClasses(element: Element, addClass: string, removeClass: string) { - if (addClass && !element.classList.contains(addClass)) { + if (addClass && !this.isDisabled(element) && !element.classList.contains(addClass)) { element.classList.add(addClass); } if (element.classList.contains(removeClass)) { @@ -1426,13 +1440,7 @@ export class ValidationService { // Special case for disabled. if (attributeName === 'disabled') { const target = mutation.target as ValidatableElement; - if (target.disabled) { - this.remove(target); - this.resetField(this.getElementUID(target)); - } - else { - this.scan(target); - } + this.reset(target); } else { const oldValue = mutation.oldValue ?? ''; From 33728fdda51aab2cd91615fbcdde8805bbde318e Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Mon, 18 Mar 2024 16:29:00 -0700 Subject: [PATCH 10/12] Always track disabled fields This way, if they become enabled, we'll validate them. If they become disabled, we won't validate them. --- Pages/Demos/DisabledInputsNoWatch.cshtml | 7 ++++++- src/index.ts | 6 +----- types/index.d.ts | 12 ++++++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Pages/Demos/DisabledInputsNoWatch.cshtml b/Pages/Demos/DisabledInputsNoWatch.cshtml index e871134..e887ede 100644 --- a/Pages/Demos/DisabledInputsNoWatch.cshtml +++ b/Pages/Demos/DisabledInputsNoWatch.cshtml @@ -96,7 +96,12 @@ inputStatus.innerText = input.disabled ? 'disabled' : 'enabled'; toggleButton.innerText = input.disabled ? 'Enable' : 'Disable'; - service.reset(input); + // If you dynamically disable a field, the next time validation occurs, the field will be ignored. + // If you dynamically enable a field, the next time validation occurs, the field will be validated. + // However, the current validation state will not be updated unless you call reset on the service + // or use the watch: true option in service.bootstrap. + + //service.reset(input); } updateStatus('Value1'); diff --git a/src/index.ts b/src/index.ts index 1a45397..68abb10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1029,10 +1029,6 @@ export class ValidationService { * @param input */ addInput(input: ValidatableElement) { - if (this.isDisabled(input)) { - return; - } - let uid = this.getElementUID(input); let directives = this.parseDirectives(input.attributes); @@ -1097,7 +1093,7 @@ export class ValidationService { for (let i = 0; i < inputs.length; i++) { let input = inputs[i]; - if (remove || this.isDisabled(input)) { + if (remove) { this.removeInput(input); } else { diff --git a/types/index.d.ts b/types/index.d.ts index 76df11e..6654d3e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -263,6 +263,8 @@ export declare class ValidationService { * @param inputUID */ private trackFormInput; + reset(input: HTMLElement): void; + private resetField; private untrackFormInput; /** * Adds an input element to be managed and validated by the service. @@ -306,6 +308,12 @@ export declare class ValidationService { * @returns */ private isHidden; + /** + * Checks if the provided input is disabled + * @param input + * @returns + */ + private isDisabled; /** * Adds addClass and removes removeClass * @param element Element to modify @@ -320,7 +328,7 @@ export declare 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. - * @param options.addNoValidate If set to true (the default), a novalidate attribute will be added to the containing form in validate elemets. + * @param options.addNoValidate If set to true (the default), a novalidate attribute will be added to the containing form in validate elements. */ bootstrap(options?: Partial): void; /** @@ -358,7 +366,7 @@ export declare class ValidationService { */ ValidationSummaryCssClassName: string; /** - * Override CSS class name for valid validation summary. Default: 'field-validation-valid' + * Override CSS class name for valid validation summary. Default: 'validation-summary-valid' */ ValidationSummaryValidCssClassName: string; } From 527fb9ae6e2852df8dff1177de74aa56f97bf10a Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Mon, 18 Mar 2024 16:43:49 -0700 Subject: [PATCH 11/12] Make the code clearer `as` in TypeScript is not the same as in C#. --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 68abb10..f6c9cd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1318,9 +1318,9 @@ export class ValidationService { * @returns */ private isDisabled(input: Element) { - // Test the the input is validatable and disabled - const validatableElement = input as ValidatableElement; - return validatableElement && validatableElement.disabled; + // If the input is validatable, we check the `disabled` property. + // Otherwise the `disabled` property is undefined and this returns false. + return (input as ValidatableElement).disabled; } /** From f17bcee2762f39babde39278926b84ff9811848e Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Mon, 18 Mar 2024 16:44:15 -0700 Subject: [PATCH 12/12] Add the script files --- dist/aspnet-validation.js | 107 +++++++++++++++++++++++------- dist/aspnet-validation.min.js | 2 +- dist/aspnet-validation.min.js.map | 2 +- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/dist/aspnet-validation.js b/dist/aspnet-validation.js index d953cd5..01f99a8 100644 --- a/dist/aspnet-validation.js +++ b/dist/aspnet-validation.js @@ -413,7 +413,7 @@ var MvcValidationProviders = /** @class */ (function () { var payload = encodedParams.join('&'); return new Promise(function (ok, reject) { var request = new XMLHttpRequest(); - if (params.type === 'Post') { + if (params.type && params.type.toLowerCase() === 'post') { var postData = new FormData(); for (var fieldName in fields) { postData.append(fieldName, fields[fieldName]); @@ -508,6 +508,9 @@ var ValidationService = /** @class */ (function () { * @param callback Receives true or false indicating validity after all validation is complete. */ this.validateForm = function (form, callback) { + if (!(form instanceof HTMLFormElement)) { + throw new Error('validateForm() can only be called on
    elements'); + } var formUID = _this.getElementUID(form); var formValidationEvent = _this.formEvents[formUID]; if (formValidationEvent) { @@ -544,6 +547,9 @@ var ValidationService = /** @class */ (function () { * @param submitEvent The `SubmitEvent`. */ this.handleValidated = function (form, success, submitEvent) { + if (!(form instanceof HTMLFormElement)) { + throw new Error('handleValidated() can only be called on elements'); + } if (success) { if (submitEvent) { _this.submitValidForm(form, submitEvent); @@ -563,11 +569,15 @@ var ValidationService = /** @class */ (function () { * @param submitEvent The `SubmitEvent`. */ this.submitValidForm = function (form, submitEvent) { + if (!(form instanceof HTMLFormElement)) { + throw new Error('submitValidForm() can only be called on elements'); + } var newEvent = new SubmitEvent('submit', submitEvent); if (form.dispatchEvent(newEvent)) { // Because the submitter is not propagated when calling // form.submit(), we recreate it here. var submitter = submitEvent.submitter; + var initialFormAction = form.action; if (submitter) { var name_1 = submitter.getAttribute('name'); // If name is null, a submit button is not submitted. @@ -578,8 +588,17 @@ var ValidationService = /** @class */ (function () { submitterInput.value = submitter.getAttribute('value'); form.appendChild(submitterInput); } + var formAction = submitter.getAttribute('formaction'); + if (formAction) { + form.action = formAction; + } + } + try { + form.submit(); + } + finally { + form.action = initialFormAction; } - form.submit(); } }; /** @@ -587,6 +606,9 @@ var ValidationService = /** @class */ (function () { * @param form */ this.focusFirstInvalid = function (form) { + if (!(form instanceof HTMLFormElement)) { + throw new Error('focusFirstInvalid() can only be called on elements'); + } var formUID = _this.getElementUID(form); var formInputUIDs = _this.formInputs[formUID]; var invalidFormInputUIDs = formInputUIDs.filter(function (uid) { return _this.summary[uid]; }); @@ -607,6 +629,9 @@ var ValidationService = /** @class */ (function () { */ this.isValid = function (form, prevalidate, callback) { if (prevalidate === void 0) { prevalidate = true; } + if (!(form instanceof HTMLFormElement)) { + throw new Error('isValid() can only be called on elements'); + } if (prevalidate) { _this.validateForm(form, callback); } @@ -660,7 +685,7 @@ var ValidationService = /** @class */ (function () { */ this.ValidationSummaryCssClassName = "validation-summary-errors"; /** - * Override CSS class name for valid validation summary. Default: 'field-validation-valid' + * Override CSS class name for valid validation summary. Default: 'validation-summary-valid' */ this.ValidationSummaryValidCssClassName = "validation-summary-valid"; this.logger = logger || nullLogger; @@ -965,25 +990,39 @@ var ValidationService = /** @class */ (function () { var uids = _this.formInputs[formUID]; for (var _i = 0, uids_1 = uids; _i < uids_1.length; _i++) { var uid = uids_1[_i]; - var input = _this.elementByUID[uid]; - if (input.classList.contains(_this.ValidationInputCssClassName)) { - input.classList.remove(_this.ValidationInputCssClassName); - } - if (input.classList.contains(_this.ValidationInputValidCssClassName)) { - input.classList.remove(_this.ValidationInputValidCssClassName); - } - var spans = _this.getMessageFor(input); - if (spans) { - for (var i = 0; i < spans.length; i++) { - spans[i].innerHTML = ''; - } - } - delete _this.summary[uid]; + _this.resetField(uid); } _this.renderSummary(); }); this.formEvents[formUID] = cb; }; + /* + Reset the state of a validatable input. This is used when it's enabled or disabled. + */ + ValidationService.prototype.reset = function (input) { + if (this.isDisabled(input)) { + this.resetField(this.getElementUID(input)); + } + else { + this.scan(input); + } + }; + ValidationService.prototype.resetField = function (inputUID) { + var input = this.elementByUID[inputUID]; + if (input.classList.contains(this.ValidationInputCssClassName)) { + input.classList.remove(this.ValidationInputCssClassName); + } + if (input.classList.contains(this.ValidationInputValidCssClassName)) { + input.classList.remove(this.ValidationInputValidCssClassName); + } + var spans = this.getMessageFor(input); + if (spans) { + for (var i = 0; i < spans.length; i++) { + spans[i].innerHTML = ''; + } + } + delete this.summary[inputUID]; + }; ValidationService.prototype.untrackFormInput = function (form, inputUID) { var formUID = this.getElementUID(form); if (!this.formInputs[formUID]) { @@ -1193,7 +1232,7 @@ var ValidationService = /** @class */ (function () { return __generator(this, function (_d) { switch (_d.label) { case 0: - if (!!this.isHidden(input)) return [3 /*break*/, 7]; + if (!(!this.isHidden(input) && !this.isDisabled(input))) return [3 /*break*/, 7]; _a = directives; _b = []; for (_c in _a) @@ -1258,6 +1297,16 @@ var ValidationService = /** @class */ (function () { ValidationService.prototype.isHidden = function (input) { return !(this.allowHiddenFields || input.offsetWidth || input.offsetHeight || input.getClientRects().length); }; + /** + * Checks if the provided input is disabled + * @param input + * @returns + */ + ValidationService.prototype.isDisabled = function (input) { + // If the input is validatable, we check the `disabled` property. + // Otherwise the `disabled` property is undefined and this returns false. + return input.disabled; + }; /** * Adds addClass and removes removeClass * @param element Element to modify @@ -1265,7 +1314,7 @@ var ValidationService = /** @class */ (function () { * @param removeClass Class to remove */ ValidationService.prototype.swapClasses = function (element, addClass, removeClass) { - if (addClass && !element.classList.contains(addClass)) { + if (addClass && !this.isDisabled(element) && !element.classList.contains(addClass)) { element.classList.add(addClass); } if (element.classList.contains(removeClass)) { @@ -1275,7 +1324,7 @@ 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. - * @param options.addNoValidate If set to true (the default), a novalidate attribute will be added to the containing form in validate elemets. + * @param options.addNoValidate If set to true (the default), a novalidate attribute will be added to the containing form in validate elements. */ ValidationService.prototype.bootstrap = function (options) { var _this = this; @@ -1353,11 +1402,19 @@ var ValidationService = /** @class */ (function () { } else if (mutation.type === 'attributes') { if (mutation.target instanceof HTMLElement) { - var oldValue = (_a = mutation.oldValue) !== null && _a !== void 0 ? _a : ''; - var newValue = (_c = (_b = mutation.target.attributes[mutation.attributeName]) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : ''; - this.logger.log("Attribute '%s' changed from '%s' to '%s'", mutation.attributeName, oldValue, newValue, mutation.target); - if (oldValue !== newValue) { - this.scan(mutation.target); + var attributeName = mutation.attributeName; + // Special case for disabled. + if (attributeName === 'disabled') { + var target = mutation.target; + this.reset(target); + } + else { + var oldValue = (_a = mutation.oldValue) !== null && _a !== void 0 ? _a : ''; + var newValue = (_c = (_b = mutation.target.attributes[mutation.attributeName]) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : ''; + this.logger.log("Attribute '%s' changed from '%s' to '%s'", mutation.attributeName, oldValue, newValue, mutation.target); + if (oldValue !== newValue) { + this.scan(mutation.target); + } } } } diff --git a/dist/aspnet-validation.min.js b/dist/aspnet-validation.min.js index 8e1b83a..3805e8b 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,"isValidatable",(function(){return s})),r.d(e,"MvcValidationProviders",(function(){return d})),r.d(e,"ValidationService",(function(){return f}));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,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(l("[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})}}))}},f=function(){function t(t){var e=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.formEvents={},this.inputEvents={},this.summary={},this.debounce=300,this.allowHiddenFields=!1,this.validateForm=function(t,r){var n=e.getElementUID(t),a=e.formEvents[n];a&&a(void 0,r)},this.validateField=function(t,r){var n=e.getElementUID(t),a=e.inputEvents[n];a&&a(void 0,r)},this.preValidate=function(t){t.preventDefault(),t.stopImmediatePropagation()},this.handleValidated=function(t,r,n){r?n&&e.submitValidForm(t,n):e.focusFirstInvalid(t)},this.submitValidForm=function(t,e){var r=new SubmitEvent("submit",e);if(t.dispatchEvent(r)){var n=e.submitter;if(n){var a=n.getAttribute("name");if(a){var i=document.createElement("input");i.type="hidden",i.name=a,i.value=n.getAttribute("value"),t.appendChild(i)}}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){void 0===r&&(r=!0),r&&e.validateField(t,n);var a=e.getElementUID(t);return void 0===e.summary[a]},this.options={root:document.body,watch:!1,addNoValidate:!0},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,e){void 0===e&&(e=!1);for(var r=0,n=Array.from(t.querySelectorAll("span[form]"));r=0?a.splice(i,1):this.logger.log("Validation element for '%s' was already removed",n,e)}},t.prototype.parseDirectives=function(t){for(var e={},r={},n="data-val-".length,a=0;a=0?this.formInputs[r].splice(n,1):this.logger.log("Form input for UID '%s' was already removed",e)}},t.prototype.addInput=function(t){var e=this,r=this.getElementUID(t),n=this.parseDirectives(t.attributes);if(this.validators[r]=this.createValidator(t,n),t.form&&this.trackFormInput(t.form,r),!this.inputEvents[r]){var a=0,i=function(t,n){var i=e.validators[r];clearTimeout(a),a=setTimeout((function(){i().then(n).catch((function(t){e.logger.log("Validation error",t)}))}),e.debounce)},s=t.dataset.valEvent;if(s)t.addEventListener(s,i);else{var o=t instanceof HTMLSelectElement?"change":"input";t.addEventListener(o,i)}this.inputEvents[r]=i}},t.prototype.removeInput=function(t){var e=this.getElementUID(t);delete this.summary[e],delete this.inputEvents[e],delete this.validators[e],t.form&&this.untrackFormInput(t.form,e)},t.prototype.scanInputs=function(t,e){void 0===e&&(e=!1);var r=Array.from(t.querySelectorAll(l('[data-val="true"]')));s(t)&&"true"===t.getAttribute("data-val")&&r.push(t);for(var n=0;n-1)){var a=document.createElement("li");a.innerHTML=this.summary[r],e.appendChild(a),t.push(this.summary[r])}}return e},t.prototype.renderSummary=function(){var t=document.querySelectorAll('[data-valmsg-summary="true"]');if(t.length){var e=JSON.stringify(this.summary,Object.keys(this.summary).sort());if(e!==this.renderedSummaryJSON){this.renderedSummaryJSON=e;for(var r=this.createSummaryDOM(),n=0;n0&&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=n.substring(0,a)+"."+r,o=document.getElementsByName(i)[0];if(s(o))return o}return t.form.querySelector(l("[name=".concat(r,"]")))}var d=function(){this.required=function(t,e,n){var r=e.type.toLowerCase();if("checkbox"===r||"radio"===r){for(var a=0,i=Array.from(e.form.querySelectorAll(l("[name='".concat(e.name,"'][type='").concat(r,"']"))));aa)return!1}return!0},this.compare=function(t,e,n){if(!n.other)return!0;var r=u(e,n.other);return!r||r.value===t},this.range=function(t,e,n){if(!t)return!0;var r=parseFloat(t);return!isNaN(r)&&(!(n.min&&rparseFloat(n.max)))},this.regex=function(t,e,n){return!t||!n.pattern||new RegExp(n.pattern).test(t)},this.email=function(t,e,n){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,n){if(!t)return!0;if(/[^0-9 \-]+/.test(t))return!1;var r,a,i=0,s=0,o=!1;if((t=t.replace(/\D/g,"")).length<13||t.length>19)return!1;for(r=t.length-1;r>=0;r--)a=t.charAt(r),s=parseInt(a,10),o&&(s*=2)>9&&(s-=9),i+=s,o=!o;return i%10==0},this.url=function(t,e,n){if(!t)return!0;var r=t.toLowerCase();return r.indexOf("http://")>-1||r.indexOf("https://")>-1||r.indexOf("ftp://")>-1},this.phone=function(t,e,n){return!t||!/[\+\-\s][\-\s]/g.test(t)&&/^\+?[0-9\-\s]+$/.test(t)},this.remote=function(t,e,n){if(!t)return!0;for(var r=n.additionalfields.split(","),a={},i=0,s=r;i=200&&r.status<300){var a=JSON.parse(r.responseText);t(a)}else e({status:r.status,statusText:r.statusText,data:r.responseText})},r.onerror=function(t){e({status:r.status,statusText:r.statusText,data:r.responseText})}}))}},c=function(){function t(t){var e=this;this.providers={},this.messageFor={},this.elementUIDs=[],this.elementByUID={},this.formInputs={},this.validators={},this.formEvents={},this.inputEvents={},this.summary={},this.debounce=300,this.allowHiddenFields=!1,this.validateForm=function(t,n){if(!(t instanceof HTMLFormElement))throw new Error("validateForm() can only be called on elements");var r=e.getElementUID(t),a=e.formEvents[r];a&&a(void 0,n)},this.validateField=function(t,n){var r=e.getElementUID(t),a=e.inputEvents[r];a&&a(void 0,n)},this.preValidate=function(t){t.preventDefault(),t.stopImmediatePropagation()},this.handleValidated=function(t,n,r){if(!(t instanceof HTMLFormElement))throw new Error("handleValidated() can only be called on elements");n?r&&e.submitValidForm(t,r):e.focusFirstInvalid(t)},this.submitValidForm=function(t,e){if(!(t instanceof HTMLFormElement))throw new Error("submitValidForm() can only be called on elements");var n=new SubmitEvent("submit",e);if(t.dispatchEvent(n)){var r=e.submitter,a=t.action;if(r){var i=r.getAttribute("name");if(i){var s=document.createElement("input");s.type="hidden",s.name=i,s.value=r.getAttribute("value"),t.appendChild(s)}var o=r.getAttribute("formaction");o&&(t.action=o)}try{t.submit()}finally{t.action=a}}},this.focusFirstInvalid=function(t){if(!(t instanceof HTMLFormElement))throw new Error("focusFirstInvalid() can only be called on elements");var n=e.getElementUID(t),r=e.formInputs[n].filter((function(t){return e.summary[t]}));if(r.length>0){var a=e.elementByUID[r[0]];a&&a.focus()}},this.isValid=function(t,n,r){if(void 0===n&&(n=!0),!(t instanceof HTMLFormElement))throw new Error("isValid() can only be called on elements");n&&e.validateForm(t,r);var a=e.getElementUID(t);return 0==e.formInputs[a].filter((function(t){return e.summary[t]})).length},this.isFieldValid=function(t,n,r){void 0===n&&(n=!0),n&&e.validateField(t,r);var a=e.getElementUID(t);return void 0===e.summary[a]},this.options={root:document.body,watch:!1,addNoValidate:!0},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,e){void 0===e&&(e=!1);for(var n=0,r=Array.from(t.querySelectorAll("span[form]"));n=0?a.splice(i,1):this.logger.log("Validation element for '%s' was already removed",r,e)}},t.prototype.parseDirectives=function(t){for(var e={},n={},r="data-val-".length,a=0;a=0?this.formInputs[n].splice(r,1):this.logger.log("Form input for UID '%s' was already removed",e)}},t.prototype.addInput=function(t){var e=this,n=this.getElementUID(t),r=this.parseDirectives(t.attributes);if(this.validators[n]=this.createValidator(t,r),t.form&&this.trackFormInput(t.form,n),!this.inputEvents[n]){var a=0,i=function(t,r){var i=e.validators[n];clearTimeout(a),a=setTimeout((function(){i().then(r).catch((function(t){e.logger.log("Validation error",t)}))}),e.debounce)},s=t.dataset.valEvent;if(s)t.addEventListener(s,i);else{var o=t instanceof HTMLSelectElement?"change":"input";t.addEventListener(o,i)}this.inputEvents[n]=i}},t.prototype.removeInput=function(t){var e=this.getElementUID(t);delete this.summary[e],delete this.inputEvents[e],delete this.validators[e],t.form&&this.untrackFormInput(t.form,e)},t.prototype.scanInputs=function(t,e){void 0===e&&(e=!1);var n=Array.from(t.querySelectorAll(l('[data-val="true"]')));s(t)&&"true"===t.getAttribute("data-val")&&n.push(t);for(var r=0;r-1)){var a=document.createElement("li");a.innerHTML=this.summary[n],e.appendChild(a),t.push(this.summary[n])}}return e},t.prototype.renderSummary=function(){var t=document.querySelectorAll('[data-valmsg-summary="true"]');if(t.length){var e=JSON.stringify(this.summary,Object.keys(this.summary).sort());if(e!==this.renderedSummaryJSON){this.renderedSummaryJSON=e;for(var n=this.createSummaryDOM(),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 * 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 * An `HTMLElement` that can be validated (`input`, `select`, `textarea`).\n */\nexport type ValidatableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/**\n * Checks if `element` is validatable (`input`, `select`, `textarea`).\n * @param element The element to check.\n * @returns `true` if validatable, otherwise `false`.\n */\nexport const isValidatable = (element: Node): element is ValidatableElement =>\n element instanceof HTMLInputElement\n || element instanceof HTMLSelectElement\n || element instanceof HTMLTextAreaElement;\n\nconst validatableElementTypes = ['input', 'select', 'textarea'];\n\n/**\n * Generates a selector to match validatable elements (`input`, `select`, `textarea`).\n * @param selector An optional selector to apply to the valid input types, e.g. `[data-val=\"true\"]`.\n * @returns The validatable elements.\n */\nconst validatableSelector = (selector?: string) =>\n validatableElementTypes.map(t => `${t}${selector || ''}`).join(',');\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: ValidatableElement, 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: ValidatableElement, selector: string): ValidatableElement {\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 selectedName = 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 + '.' + selectedName;\n let relativeElement = document.getElementsByName(relativeElementName)[0];\n if (isValidatable(relativeElement)) {\n return relativeElement;\n }\n }\n\n // __RequestVerificationToken\n return element.form.querySelector(validatableSelector(`[name=${selectedName}]`));\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(validatableSelector(`[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 // Checkboxes do not submit a value when unchecked. To work around this, platforms such as ASP.NET render a\n // hidden input with the same name as the checkbox so that a value (\"false\") is still submitted even when\n // the checkbox is not checked. We check this special case here.\n if (elementType === \"checkbox\") {\n const checkboxHiddenInput = element.form.querySelector(`input[name='${element.name}'][type='hidden']`);\n if (checkboxHiddenInput instanceof HTMLInputElement && checkboxHiddenInput.value === \"false\") {\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);\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);\n\n let hasValue = Boolean(fieldElement && fieldElement.value);\n if (!hasValue) {\n continue;\n }\n\n if (fieldElement instanceof HTMLInputElement &&\n (fieldElement.type === 'checkbox' || fieldElement.type === 'radio')) {\n fields[fieldName] = fieldElement.checked ? fieldElement.value : '';\n } else {\n fields[fieldName] = fieldElement.value;\n }\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 * Configuration for @type {ValidationService}.\n */\nexport interface ValidationServiceOptions {\n watch: boolean;\n root: ParentNode;\n addNoValidate: boolean;\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 form UID to its trigger element (submit event for ).\n */\n private formEvents: { [id: string]: (e?: SubmitEvent, callback?: ValidatedCallback) => void } = {};\n\n /**\n * A key-value map for element UID to its trigger element (input event for