From a1d2b9ec53e141016431402d1d3c6b17a738b78f Mon Sep 17 00:00:00 2001 From: David Karlsson Date: Tue, 12 Dec 2023 09:41:30 +0100 Subject: [PATCH 1/3] Only send checkbox/radio values for remote validation if checked Doing remote validation for a checkbox value always sends the input's value whether the checkbox is checked or not and since the value is set to "true" for checkboxes the value will always be true on the server side even if you unchecked the checkbox when the remote validation was triggered. This change fixes this by only sending the value if the checkbox was checked. This is similar to what jquery-validation-unobtrusive does to handle remote checkbox validation. I also added radio buttons to the check but I don't think it will work correctly for them since there will be multiple inputs with the same name selector for radio buttons but only the first one is handled which might not be the one that is clicked. So I'm not sure what the correct approach is for remote validation of radio buttons but this at least seems to be more in line with how jquery-validation-unobtrusive handles them. --- src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 29621f3..8925c69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -351,7 +351,12 @@ export class MvcValidationProviders { continue; } - fields[fieldName] = fieldElement.value; + if (fieldElement instanceof HTMLInputElement && + (fieldElement.type === 'checkbox' || fieldElement.type === 'radio')) { + fields[fieldName] = fieldElement.checked ? fieldElement.value : ''; + } else { + fields[fieldName] = fieldElement.value; + } } let url: string = params['url']; From 15bdd9a9deecc55bbc69ec8382c6a8d30870ebe3 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 12 Dec 2023 11:44:34 -0800 Subject: [PATCH 2/3] Add demo to prove fix works --- Controllers/Validations.cs | 6 ++++++ Pages/Demos/Checkboxes.cshtml | 4 ++-- Pages/Demos/Checkboxes.cshtml.cs | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Controllers/Validations.cs b/Controllers/Validations.cs index 188d29b..28d793e 100644 --- a/Controllers/Validations.cs +++ b/Controllers/Validations.cs @@ -10,6 +10,12 @@ public IActionResult CheckRemote(string id) return Ok(id == "42"); } + [HttpPost] + public IActionResult CheckboxRemote(bool isChecked) + { + return Ok(isChecked); + } + [HttpGet] public IActionResult Test() { diff --git a/Pages/Demos/Checkboxes.cshtml b/Pages/Demos/Checkboxes.cshtml index 3a0a640..e366c99 100644 --- a/Pages/Demos/Checkboxes.cshtml +++ b/Pages/Demos/Checkboxes.cshtml @@ -52,8 +52,8 @@ } - @if (Model.SelectedAnimals.Any()) { - Selected animals: @string.Join(", ", Model.SelectedAnimals) + @if (Model.SelectedAnimals!.Any()) { + Selected animals: @string.Join(", ", Model.SelectedAnimals!) } diff --git a/Pages/Demos/Checkboxes.cshtml.cs b/Pages/Demos/Checkboxes.cshtml.cs index 7e455fc..7501d91 100644 --- a/Pages/Demos/Checkboxes.cshtml.cs +++ b/Pages/Demos/Checkboxes.cshtml.cs @@ -41,6 +41,7 @@ public IActionResult OnPost() public class InputModel { + [Remote("CheckboxRemote", "Validations", HttpMethod = "Post")] public bool IsChecked { get; set; } } @@ -50,6 +51,7 @@ public class InputModel public class Selectable { public required string Name { get; set; } + public bool IsSelected { get; set; } } } \ No newline at end of file From 6bb07bd4968f71dd0e4cacedb5fd0af35ff961b2 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 12 Dec 2023 11:45:25 -0800 Subject: [PATCH 3/3] Bump version to 0.9.3 and rebuild --- dist/aspnet-validation.js | 8 +++++++- dist/aspnet-validation.min.js | 2 +- dist/aspnet-validation.min.js.map | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/dist/aspnet-validation.js b/dist/aspnet-validation.js index b38b15a..d953cd5 100644 --- a/dist/aspnet-validation.js +++ b/dist/aspnet-validation.js @@ -396,7 +396,13 @@ var MvcValidationProviders = /** @class */ (function () { if (!hasValue) { continue; } - fields[fieldName] = fieldElement.value; + if (fieldElement instanceof HTMLInputElement && + (fieldElement.type === 'checkbox' || fieldElement.type === 'radio')) { + fields[fieldName] = fieldElement.checked ? fieldElement.value : ''; + } + else { + fields[fieldName] = fieldElement.value; + } } var url = params['url']; var encodedParams = []; diff --git a/dist/aspnet-validation.min.js b/dist/aspnet-validation.min.js index 825043c..8e1b83a 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=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;n\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 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 * 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