UNPKG

aspnet-client-validation

Version:

Enables ASP.NET MVC client-side validation, without jQuery!

1,123 lines (1,122 loc) 65.8 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["aspnetValidation"] = factory(); else root["aspnetValidation"] = factory(); })(self, () => { return /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ // The require scope /******/ var __webpack_require__ = {}; /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; /*!**********************!*\ !*** ./src/index.ts ***! \**********************/ __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ MvcValidationProviders: () => (/* binding */ MvcValidationProviders), /* harmony export */ ValidationService: () => (/* binding */ ValidationService), /* harmony export */ isValidatable: () => (/* binding */ isValidatable) /* harmony export */ }); var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (undefined && undefined.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var nullLogger = new (/** @class */ (function () { function class_1() { this.warn = globalThis.console.warn; } class_1.prototype.log = function (_) { var _args = []; for (var _i = 1; _i < arguments.length; _i++) { _args[_i - 1] = arguments[_i]; } }; return class_1; }()))(); /** * Checks if `element` is validatable (`input`, `select`, `textarea`). * @param element The element to check. * @returns `true` if validatable, otherwise `false`. */ var isValidatable = function (element) { return element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement; }; var validatableElementTypes = ['input', 'select', 'textarea']; /** * Generates a selector to match validatable elements (`input`, `select`, `textarea`). * @param selector An optional selector to apply to the valid input types, e.g. `[data-val="true"]`. * @returns The validatable elements. */ var validatableSelector = function (selector) { return validatableElementTypes.map(function (t) { return "".concat(t).concat(selector || ''); }).join(','); }; /** * Resolves and returns the element referred by original element using ASP.NET selector logic. * @param element - The input to validate * @param selector - Used to find the field. Ex. *.Password where * replaces whatever prefixes asp.net might add. */ function getRelativeFormElement(element, selector) { // example elementName: Form.PasswordConfirm, Form.Email // example selector (dafuq): *.Password, *.__RequestVerificationToken // example result element name: Form.Password, __RequestVerificationToken var elementName = element.name; var selectedName = selector.substring(2); // Password, __RequestVerificationToken var objectName = ''; var dotLocation = elementName.lastIndexOf('.'); if (dotLocation > -1) { // Form objectName = elementName.substring(0, dotLocation); // Form.Password var relativeElementName = objectName + '.' + selectedName; var relativeElement = document.getElementsByName(relativeElementName)[0]; if (isValidatable(relativeElement)) { return relativeElement; } } // __RequestVerificationToken return element.form.querySelector(validatableSelector("[name=".concat(selectedName, "]"))); } /** * Contains default implementations for ASP.NET Core MVC validation attributes. */ var MvcValidationProviders = /** @class */ (function () { function MvcValidationProviders() { /** * Validates whether the input has a value. */ this.required = function (value, element, params) { // Handle single and multiple checkboxes/radio buttons. var elementType = element.type.toLowerCase(); if (elementType === "checkbox" || elementType === "radio") { var allElementsOfThisName = Array.from(element.form.querySelectorAll(validatableSelector("[name='".concat(element.name, "'][type='").concat(elementType, "']")))); for (var _i = 0, allElementsOfThisName_1 = allElementsOfThisName; _i < allElementsOfThisName_1.length; _i++) { var element_1 = allElementsOfThisName_1[_i]; if (element_1 instanceof HTMLInputElement && element_1.checked === true) { return true; } } // Checkboxes do not submit a value when unchecked. To work around this, platforms such as ASP.NET render a // hidden input with the same name as the checkbox so that a value ("false") is still submitted even when // the checkbox is not checked. We check this special case here. if (elementType === "checkbox") { var checkboxHiddenInput = element.form.querySelector("input[name='".concat(element.name, "'][type='hidden']")); if (checkboxHiddenInput instanceof HTMLInputElement && checkboxHiddenInput.value === "false") { return true; } } return false; } // Default behavior otherwise (trim ensures whitespace only is not seen as valid). return Boolean(value === null || value === void 0 ? void 0 : value.trim()); }; /** * Validates whether the input value satisfies the length contstraint. */ this.stringLength = function (value, element, params) { if (!value) { return true; } if (params.min) { var min = parseInt(params.min); if (value.length < min) { return false; } } if (params.max) { var max = parseInt(params.max); if (value.length > max) { return false; } } return true; }; /** * Validates whether the input value is equal to another input value. */ this.compare = function (value, element, params) { if (!params.other) { return true; } var otherElement = getRelativeFormElement(element, params.other); if (!otherElement) { return true; } return (otherElement.value === value); }; /** * Validates whether the input value is a number within a given range. */ this.range = function (value, element, params) { if (!value) { return true; } var val = parseFloat(value); if (isNaN(val)) { return false; } if (params.min) { var min = parseFloat(params.min); if (val < min) { return false; } } if (params.max) { var max = parseFloat(params.max); if (val > max) { return false; } } return true; }; /** * Validates whether the input value satisfies a regular expression pattern. */ this.regex = function (value, element, params) { if (!value || !params.pattern) { return true; } var r = new RegExp(params.pattern); return r.test(value); }; /** * Validates whether the input value is an email in accordance to RFC822 specification, with a top level domain. */ this.email = function (value, element, params) { if (!value) { return true; } // RFC822 email address with .TLD validation // (c) Richard Willis, Chris Ferdinandi, MIT Licensed // https://gist.github.com/badsyntax/719800 // https://gist.github.com/cferdinandi/d04aad4ce064b8da3edf21e26f8944c4 var 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,})+$/; return r.test(value); }; /** * Validates whether the input value is a credit card number, with Luhn's Algorithm. */ this.creditcard = function (value, element, params) { if (!value) { return true; } // (c) jquery-validation, MIT Licensed // https://github.com/jquery-validation/jquery-validation/blob/master/src/additional/creditcard.js // based on https://en.wikipedia.org/wiki/Luhn_algorithm // Accept only spaces, digits and dashes if (/[^0-9 \-]+/.test(value)) { return false; } var nCheck = 0, nDigit = 0, bEven = false, n, cDigit; value = value.replace(/\D/g, ""); // Basing min and max length on https://developer.ean.com/general_info/Valid_Credit_Card_Types if (value.length < 13 || value.length > 19) { return false; } for (n = value.length - 1; n >= 0; n--) { cDigit = value.charAt(n); nDigit = parseInt(cDigit, 10); if (bEven) { if ((nDigit *= 2) > 9) { nDigit -= 9; } } nCheck += nDigit; bEven = !bEven; } return (nCheck % 10) === 0; }; /** * Validates whether the input value is a URL. */ this.url = function (value, element, params) { if (!value) { return true; } var lowerCaseValue = value.toLowerCase(); // Match the logic in `UrlAttribute` return lowerCaseValue.indexOf('http://') > -1 || lowerCaseValue.indexOf('https://') > -1 || lowerCaseValue.indexOf('ftp://') > -1; }; /** * Validates whether the input value is a phone number. */ this.phone = function (value, element, params) { if (!value) { return true; } // Allows whitespace or dash as number separator because some people like to do that... var consecutiveSeparator = /[\+\-\s][\-\s]/g; if (consecutiveSeparator.test(value)) { return false; } var r = /^\+?[0-9\-\s]+$/; return r.test(value); }; /** * Asynchronously validates the input value to a JSON GET API endpoint. */ this.remote = function (value, element, params) { if (!value) { return true; } // params.additionalfields: *.Email,*.Username var fieldSelectors = params.additionalfields.split(','); var fields = {}; for (var _i = 0, fieldSelectors_1 = fieldSelectors; _i < fieldSelectors_1.length; _i++) { var fieldSelector = fieldSelectors_1[_i]; var fieldName = fieldSelector.substr(2); var fieldElement = getRelativeFormElement(element, fieldSelector); var hasValue = Boolean(fieldElement && fieldElement.value); if (!hasValue) { continue; } 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 = []; for (var fieldName in fields) { var encodedParam = encodeURIComponent(fieldName) + '=' + encodeURIComponent(fields[fieldName]); encodedParams.push(encodedParam); } var payload = encodedParams.join('&'); return new Promise(function (ok, reject) { var request = new XMLHttpRequest(); if (params.type && params.type.toLowerCase() === 'post') { var postData = new FormData(); for (var fieldName in fields) { postData.append(fieldName, fields[fieldName]); } request.open('post', url); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); request.send(payload); } else { request.open('get', url + '?' + payload); request.send(); } request.onload = function (e) { if (request.status >= 200 && request.status < 300) { var data = JSON.parse(request.responseText); ok(data); } else { reject({ status: request.status, statusText: request.statusText, data: request.responseText }); } }; request.onerror = function (e) { reject({ status: request.status, statusText: request.statusText, data: request.responseText }); }; }); }; } return MvcValidationProviders; }()); /** * Responsible for managing the DOM elements and running the validation providers. */ var ValidationService = /** @class */ (function () { function ValidationService(logger) { var _this = this; /** * A key-value collection of loaded validation plugins. */ this.providers = {}; /** * A key-value collection of form UIDs and their <span> elements for displaying validation messages for an input (by DOM name). */ this.messageFor = {}; /** * A list of managed elements, each having a randomly assigned unique identifier (UID). */ this.elementUIDs = []; /** * A key-value collection of UID to Element for quick lookup. */ this.elementByUID = {}; /** * A key-value collection of input UIDs for a <form> UID. */ this.formInputs = {}; /** * A key-value map for input UID to its validator factory. */ this.validators = {}; /** * A key-value map for form UID to its trigger element (submit event for <form>). */ this.formEvents = {}; /** * A key-value map for element UID to its trigger element (input event for <textarea> and <input>, change event for <select>). */ this.inputEvents = {}; /** * A key-value map of input UID to its validation error message. */ this.summary = {}; /** * In milliseconds, the rate of fire of the input validation. */ this.debounce = 300; /** * Allow hidden fields validation */ this.allowHiddenFields = false; /** * Fires off validation for elements within the provided form and then calls the callback * @param form The form to validate. * @param callback Receives true or false indicating validity after all validation is complete. * @returns Promise that resolves to true or false indicating validity after all validation is complete. */ this.validateForm = function (form, callback) { return __awaiter(_this, void 0, void 0, function () { var formUID, formValidationEvent, _a; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!(form instanceof HTMLFormElement)) { throw new Error('validateForm() can only be called on <form> elements'); } formUID = this.getElementUID(form); formValidationEvent = this.formEvents[formUID]; _a = !formValidationEvent; if (_a) return [3 /*break*/, 2]; return [4 /*yield*/, formValidationEvent(undefined, callback)]; case 1: _a = (_b.sent()); _b.label = 2; case 2: return [2 /*return*/, _a]; } }); }); }; /** * Fires off validation for the provided element and then calls the callback * @param field The element to validate. * @param callback Receives true or false indicating validity after all validation is complete. * @returns Promise that resolves to true or false indicating validity after all validation is complete */ this.validateField = function (field, callback) { return __awaiter(_this, void 0, void 0, function () { var fieldUID, fieldValidationEvent, _a; return __generator(this, function (_b) { switch (_b.label) { case 0: fieldUID = this.getElementUID(field); fieldValidationEvent = this.inputEvents[fieldUID]; _a = !fieldValidationEvent; if (_a) return [3 /*break*/, 2]; return [4 /*yield*/, fieldValidationEvent(undefined, callback)]; case 1: _a = (_b.sent()); _b.label = 2; case 2: return [2 /*return*/, _a]; } }); }); }; /** * Called before validating form submit events. * Default calls `preventDefault()` and `stopImmediatePropagation()`. * @param submitEvent The `SubmitEvent`. */ this.preValidate = function (submitEvent) { submitEvent.preventDefault(); submitEvent.stopImmediatePropagation(); }; /** * Handler for validated form submit events. * Default calls `submitValidForm(form, submitEvent)` on success * and `focusFirstInvalid(form)` on failure. * @param form The form that has been validated. * @param success The validation result. * @param submitEvent The `SubmitEvent`. */ this.handleValidated = function (form, success, submitEvent) { if (!(form instanceof HTMLFormElement)) { throw new Error('handleValidated() can only be called on <form> elements'); } if (success) { if (submitEvent) { _this.submitValidForm(form, submitEvent); } } else { _this.focusFirstInvalid(form); } }; /** * Dispatches a new `SubmitEvent` on the provided form, * then calls `form.submit()` unless `submitEvent` is cancelable * and `preventDefault()` was called by a handler that received the new event. * * This is equivalent to `form.requestSubmit()`, but more flexible. * @param form The validated form to submit * @param submitEvent The `SubmitEvent`. */ this.submitValidForm = function (form, submitEvent) { if (!(form instanceof HTMLFormElement)) { throw new Error('submitValidForm() can only be called on <form> 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 submitterInput = null; var initialFormAction = form.action; if (submitter) { var name_1 = submitter.getAttribute('name'); // If name is null, a submit button is not submitted. if (name_1) { submitterInput = document.createElement('input'); submitterInput.type = 'hidden'; submitterInput.name = name_1; submitterInput.value = submitter.getAttribute('value'); form.appendChild(submitterInput); } var formAction = submitter.getAttribute('formaction'); if (formAction) { form.action = formAction; } } try { form.submit(); } finally { if (submitterInput) { // Important to clean up the submit input we created. form.removeChild(submitterInput); } form.action = initialFormAction; } } }; /** * Focuses the first invalid element within the provided form * @param form */ this.focusFirstInvalid = function (form) { if (!(form instanceof HTMLFormElement)) { throw new Error('focusFirstInvalid() can only be called on <form> elements'); } var formUID = _this.getElementUID(form); var formInputUIDs = _this.formInputs[formUID]; var invalidFormInputUID = formInputUIDs === null || formInputUIDs === void 0 ? void 0 : formInputUIDs.find(function (uid) { return _this.summary[uid]; }); if (invalidFormInputUID) { var firstInvalid = _this.elementByUID[invalidFormInputUID]; if (firstInvalid instanceof HTMLElement) { firstInvalid.focus(); } } }; /** * Returns true if the provided form is currently valid. * The form will be validated unless prevalidate is set to false. * @param form The form to validate. * @param prevalidate Whether the form should be validated before returning. * @param callback A callback that receives true or false indicating validity after all validation is complete. Ignored if prevalidate is false. * @returns The current state of the form. May be inaccurate if any validation is asynchronous (e.g. remote); consider using `callback` instead. */ 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 <form> elements'); } if (prevalidate) { _this.validateForm(form, callback); } var formUID = _this.getElementUID(form); var formInputUIDs = _this.formInputs[formUID]; var formIsInvalid = (formInputUIDs === null || formInputUIDs === void 0 ? void 0 : formInputUIDs.some(function (uid) { return _this.summary[uid]; })) === true; return !formIsInvalid; }; /** * Returns true if the provided field is currently valid. * The field will be validated unless prevalidate is set to false. * @param field The field to validate. * @param prevalidate Whether the field should be validated before returning. * @param callback A callback that receives true or false indicating validity after all validation is complete. Ignored if prevalidate is false. * @returns The current state of the field. May be inaccurate if any validation is asynchronous (e.g. remote); consider using `callback` instead. */ this.isFieldValid = function (field, prevalidate, callback) { if (prevalidate === void 0) { prevalidate = true; } if (prevalidate) { _this.validateField(field, callback); } var fieldUID = _this.getElementUID(field); return _this.summary[fieldUID] === undefined; }; /** * Options for this instance of @type {ValidationService}. */ this.options = { root: document.body, watch: false, addNoValidate: true, }; /** * Override CSS class name for input validation error. Default: 'input-validation-error' */ this.ValidationInputCssClassName = "input-validation-error"; /** * Override CSS class name for valid input validation. Default: 'input-validation-valid' */ this.ValidationInputValidCssClassName = "input-validation-valid"; /** * Override CSS class name for field validation error. Default: 'field-validation-error' */ this.ValidationMessageCssClassName = "field-validation-error"; /** * Override CSS class name for valid field validation. Default: 'field-validation-valid' */ this.ValidationMessageValidCssClassName = "field-validation-valid"; /** * Override CSS class name for validation summary error. Default: 'validation-summary-errors' */ this.ValidationSummaryCssClassName = "validation-summary-errors"; /** * Override CSS class name for valid validation summary. Default: 'validation-summary-valid' */ this.ValidationSummaryValidCssClassName = "validation-summary-valid"; this.logger = logger || nullLogger; } /** * Registers a new validation plugin of the given name, if not registered yet. * Registered plugin validates inputs with data-val-[name] attribute, used as error message. * @param name * @param callback */ ValidationService.prototype.addProvider = function (name, callback) { if (this.providers[name]) { // First-Come-First-Serve validation plugin design. // Allows developers to override the default MVC Providers by adding custom providers BEFORE bootstrap() is called! return; } this.logger.log("Registered provider: %s", name); this.providers[name] = callback; }; /** * Registers the default providers for enabling ASP.NET Core MVC client-side validation. */ ValidationService.prototype.addMvcProviders = function () { var mvc = new MvcValidationProviders(); // [Required] this.addProvider('required', mvc.required); // [StringLength], [MinLength], [MaxLength] this.addProvider('length', mvc.stringLength); this.addProvider('maxlength', mvc.stringLength); this.addProvider('minlength', mvc.stringLength); // [Compare] this.addProvider('equalto', mvc.compare); // [Range] this.addProvider('range', mvc.range); // [RegularExpression] this.addProvider('regex', mvc.regex); // [CreditCard] this.addProvider('creditcard', mvc.creditcard); // [EmailAddress] this.addProvider('email', mvc.email); // [Url] this.addProvider('url', mvc.url); // [Phone] this.addProvider('phone', mvc.phone); // [Remote] this.addProvider('remote', mvc.remote); }; /** * Scans `root` for all validation message <span> generated by ASP.NET Core MVC, then calls `cb` for each. * @param root The root node to scan * @param cb The callback to invoke with each form and span */ ValidationService.prototype.scanMessages = function (root, cb) { /* If a validation span explicitly declares a form, we group the span with that form. */ var validationMessageElements = Array.from(root.querySelectorAll('span[form]')); for (var _i = 0, validationMessageElements_1 = validationMessageElements; _i < validationMessageElements_1.length; _i++) { var span = validationMessageElements_1[_i]; var form = document.getElementById(span.getAttribute('form')); if (form instanceof HTMLFormElement) { cb.call(this, form, span); } } // Otherwise if a validation message span is inside a form, we group the span with the form it's inside. var forms = Array.from(root.querySelectorAll('form')); if (root instanceof HTMLFormElement) { // querySelectorAll does not include the root element itself. // we could use 'matches', but that's newer than querySelectorAll so we'll keep it simple and compatible. forms.push(root); } // If root is the descendant of a form, we want to include that form too. var containingForm = (root instanceof Element) ? root.closest('form') : null; if (containingForm) { forms.push(containingForm); } for (var _a = 0, forms_1 = forms; _a < forms_1.length; _a++) { var form = forms_1[_a]; var validationMessageElements_3 = Array.from(form.querySelectorAll('[data-valmsg-for]')); for (var _b = 0, validationMessageElements_2 = validationMessageElements_3; _b < validationMessageElements_2.length; _b++) { var span = validationMessageElements_2[_b]; cb.call(this, form, span); } } }; ValidationService.prototype.pushValidationMessageSpan = function (form, span) { var _a, _b; var _c; var formId = this.getElementUID(form); var formSpans = (_a = (_c = this.messageFor)[formId]) !== null && _a !== void 0 ? _a : (_c[formId] = {}); var messageForId = span.getAttribute('data-valmsg-for'); if (!messageForId) return; var spans = (_b = formSpans[messageForId]) !== null && _b !== void 0 ? _b : (formSpans[messageForId] = []); if (spans.indexOf(span) < 0) { spans.push(span); } else { this.logger.log("Validation element for '%s' is already tracked", name, span); } }; ValidationService.prototype.removeValidationMessageSpan = function (form, span) { var formId = this.getElementUID(form); var formSpans = this.messageFor[formId]; if (!formSpans) return; var messageForId = span.getAttribute('data-valmsg-for'); if (!messageForId) return; var spans = formSpans[messageForId]; if (!spans) { return; } var index = spans.indexOf(span); if (index >= 0) { spans.splice(index, 1); } else { this.logger.log("Validation element for '%s' was already removed", name, span); } }; /** * Given attribute map for an HTML input, returns the validation directives to be executed. * @param attributes */ ValidationService.prototype.parseDirectives = function (attributes) { var directives = {}; var validationAtributes = {}; var cut = 'data-val-'.length; for (var i = 0; i < attributes.length; i++) { var a = attributes[i]; if (a.name.indexOf('data-val-') === 0) { var key = a.name.substr(cut); validationAtributes[key] = a.value; } } var _loop_1 = function (key) { if (key.indexOf('-') === -1) { var parameters = Object.keys(validationAtributes).filter(function (Q) { return (Q !== key) && (Q.indexOf(key) === 0); }); var directive = { error: validationAtributes[key], params: {} }; var pcut = (key + '-').length; for (var i = 0; i < parameters.length; i++) { var pvalue = validationAtributes[parameters[i]]; var pkey = parameters[i].substr(pcut); directive.params[pkey] = pvalue; } directives[key] = directive; } }; for (var key in validationAtributes) { _loop_1(key); } return directives; }; /** * Returns an RFC4122 version 4 compliant GUID. */ ValidationService.prototype.guid4 = function () { // (c) broofa, MIT Licensed // 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); return v.toString(16); }); }; /** * Gets a UID for an DOM element. * @param node */ ValidationService.prototype.getElementUID = function (node) { var x = this.elementUIDs.filter(function (e) { return e.node === node; })[0]; if (x) { return x.uid; } var uid = this.guid4(); this.elementUIDs.push({ node: node, uid: uid }); this.elementByUID[uid] = node; return uid; }; /** * Returns a Promise that returns validation result for each and every inputs within the form. * @param formUID */ ValidationService.prototype.getFormValidationTask = function (formUID) { var formInputUIDs = this.formInputs[formUID]; if (!formInputUIDs || formInputUIDs.length === 0) { return Promise.resolve(true); } var formValidators = []; for (var _i = 0, formInputUIDs_1 = formInputUIDs; _i < formInputUIDs_1.length; _i++) { var inputUID = formInputUIDs_1[_i]; var validator = this.validators[inputUID]; if (validator) { formValidators.push(validator); } } var tasks = formValidators.map(function (factory) { return factory(); }); return Promise.all(tasks).then(function (result) { return result.every(function (e) { return e; }); }); }; // Retrieves the validation span for the input. ValidationService.prototype.getMessageFor = function (input) { var _a; if (!input.form) { return undefined; } var formId = this.getElementUID(input.form); return (_a = this.messageFor[formId]) === null || _a === void 0 ? void 0 : _a[input.name]; }; /** * Returns true if the event triggering the form submission indicates we should validate the form. * @param e */ ValidationService.prototype.shouldValidate = function (e) { // Skip client-side validation if the form has been submitted via a button that has the "formnovalidate" attribute. return !(e && e['submitter'] && e['submitter']['formNoValidate']); }; /** * Tracks a <form> element as parent of an input UID. When the form is submitted, attempts to validate the said input asynchronously. * @param form * @param inputUID */ ValidationService.prototype.trackFormInput = function (form, inputUID) { var _this = this; var _a; var _b; var formUID = this.getElementUID(form); var formInputUIDs = (_a = (_b = this.formInputs)[formUID]) !== null && _a !== void 0 ? _a : (_b[formUID] = []); var add = formInputUIDs.indexOf(inputUID) === -1; if (add) { formInputUIDs.push(inputUID); if (this.options.addNoValidate) { this.logger.log('Setting novalidate on form', form); form.setAttribute('novalidate', 'novalidate'); } else { this.logger.log('Not setting novalidate on form', form); } } else { this.logger.log("Form input for UID '%s' is already tracked", inputUID); } if (this.formEvents[formUID]) { return; } var validationTask = null; var cb = function (e, callback) { // Prevent recursion if (validationTask) { return validationTask; } if (!_this.shouldValidate(e)) { return Promise.resolve(true); } validationTask = _this.getFormValidationTask(formUID); //`preValidate` typically prevents submit before validation if (e) { _this.preValidate(e); } _this.logger.log('Validating', form); return validationTask.then(function (success) { return __awaiter(_this, void 0, void 0, function () { var validationEvent; return __generator(this, function (_a) { switch (_a.label) { case 0: this.logger.log('Validated (success = %s)', success, form); if (callback) { callback(success); return [2 /*return*/, success]; } validationEvent = new CustomEvent('validation', { detail: { valid: success } }); form.dispatchEvent(validationEvent); // Firefox fix: redispatch 'submit' after finished handling this event return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 0); })]; case 1: // Firefox fix: redispatch 'submit' after finished handling this event _a.sent(); this.handleValidated(form, success, e); return [2 /*return*/, success]; } }); }); }).catch(function (error) { _this.logger.log('Validation error', error); return false; }).finally(function () { validationTask = null; }); }; form.addEventListener('submit', cb); var cbReset = function (e) { var formInputUIDs = _this.formInputs[formUID]; for (var _i = 0, formInputUIDs_2 = formInputUIDs; _i < formInputUIDs_2.length; _i++) { var inputUID_1 = formInputUIDs_2[_i]; _this.resetField(inputUID_1); } _this.renderSummary(); }; form.addEventListener('reset', cbReset); cb.remove = function () { form.removeEventListener('submit', cb); form.removeEventListener('reset', cbReset); }; 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]; this.swapClasses(input, '', this.ValidationInputCssClassName); this.swapClasses(input, '', this.ValidationInputValidCssClassName); var spans = isValidatable(input) && this.getMessageFor(input); if (spans) { for (var i = 0; i < spans.length; i++) { spans[i].innerHTML = ''; this.swapClasses(spans[i], '', this.ValidationMessageCssClassName); this.swapClasses(spans[i], '', this.ValidationMessageValidCssClassName); } } delete this.summary[inputUID]; }; ValidationService.prototype.untrackFormInput = function (form, inputUID) { var _a; var formUID = this.getElementUID(form); var formInputUIDs = this.formInputs[formUID]; if (!formInputUIDs) { return; } var indexToRemove = formInputUIDs.indexOf(inputUID); if (indexToRemove >= 0) { formInputUIDs.splice(indexToRemove, 1); if (!formInputUIDs.length) { (_a = this.formEvents[formUID]) === null || _a === void 0 ? void 0 : _a.remove(); delete this.formEvents[formUID]; delete this.formInputs[formUID]; delete this.messageFor[formUID]; } } else { this.logger.log("Form input for UID '%s' was already removed", inputUID); } }; /** * Adds an input element to be managed and validated by the service. * Triggers a debounced live validation when input value changes. * @param input */ ValidationService.prototype.addInput = function (input) { var _this = this; var _a; var uid = this.getElementUID(input); var directives = this.parseDirectives(input.attributes); this.validators[uid] = this.createValidator(input, directives); if (input.form) { this.trackFormInput(input.form, uid); } if (this.inputEvents[uid]) { return; } var cb = function (event, callback) { return __awaiter(_this, void 0, void 0, function () { var validate, success, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: validate = this.validators[uid]; if (!validate) return [2 /*return*/, true]; if (!input.dataset.valEvent && event && event.type === 'input' && !input.classList.contains(this.ValidationInputCssClassName)) { // When no data-val-event specified on a field, "input" event only takes it back to valid. "Change" event can make it invalid. return [2 /*return*/, true]; } this.logger.log('Validating', { event: event }); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, validate()]; case 2: success = _a.sent(); callback(success); return [2 /*return*/, success]; case 3: error_1 = _a.sent(); this.logger.log('Validation error', error_1); return [2 /*return*/, false]; case 4: return [2 /*return*/]; } }); }); }; var debounceTimeoutID = null; cb.debounced = function (event, callback) { if (debounceTimeoutID !== null) { clearTimeout(debounceTimeoutID); } debounceTimeoutID = setTimeout(function () { cb(event, callback); }, _this.debounce); }; var defaultEvent = input instanceof HTMLSelectElement ? 'change' : 'input change'; var validateEvent = (_a = input.dataset.valEvent) !== null && _a !== void 0 ? _a : defaultEvent; var events = validateEvent.split(' '); events.forEach(function (eventName) { input.addEventListener(eventName, cb.debounced); }); cb.remove = function () { events.forEach(function (eventName) { input.removeEventListener(eventName, cb.debounced); }); }; this.inputEvents[uid] = cb; }; ValidationService.prototype.removeInput = function (input) { var uid = this.getElementUID(input); // Clean up event listener var cb = this.inputEvents[uid]; if (cb === null || cb === void 0 ? void 0 : cb.remove) { cb.remove(); delete cb.remove; } delete this.summary[uid]; delete this.inputEvents[uid]; delete this.validators[uid]; if (input.form) { this.untrackFormInput(input.form, uid); } }; /** * Scans `root` for input elements to be validated, then calls `cb` for each. * @param root The root node to scan * @param cb The callback to invoke with each input */ ValidationService.prototype.scanInputs = function (root, cb) { var inputs = Array.from(root.querySelectorAll(validatableSelector('[data-val="true"]'))); // querySelectorAll does not include the root element itself. // we could use 'matches', but that's newer than querySelectorAll so we'll keep it simple and compatible. if (isValidatable(root) && root.getAttribute("data-val") === "true") { inputs.push(root); } for (var i = 0; i < inputs.length; i++) { var input = inputs[i]; cb.call(this, input); } }; /** * Returns a <ul> element as a validation error