UNPKG

just-validate

Version:

Modern, simple, lightweight (~5kb gzip) form validation library written in Typescript, with no dependencies (no JQuery!). Support a wide range of predefined rules, async, files, dates validation, custom error messages and styles, localization. Support wri

1,506 lines (1,505 loc) 52 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const EMAIL_REGEXP = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const INTEGER_REGEXP = /^-?[0-9]\d*$/; const PASSWORD_REGEXP = /^(?=.*[A-Za-z])(?=.*\d).{8,}$/; const STRONG_PASSWORD_REGEXP = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; const isEmpty = (value) => { let newVal = value; if (typeof value === "string") { newVal = value.trim(); } return !newVal; }; const isEmail = (value) => { return EMAIL_REGEXP.test(value); }; const isLengthMoreThanMax = (value, len) => { return value.length > len; }; const isLengthLessThanMin = (value, len) => { return value.length < len; }; const isNumber = (value) => { if (typeof value !== "string") { return false; } return !isNaN(+value) && !isNaN(parseFloat(value)); }; const isInteger = (value) => { return INTEGER_REGEXP.test(value); }; const isPassword = (value) => { return PASSWORD_REGEXP.test(value); }; const isStrongPassword = (value) => { return STRONG_PASSWORD_REGEXP.test(value); }; const isNumberMoreThanMax = (value, len) => { return value > len; }; const isNumberLessThanMin = (value, len) => { return value < len; }; const isInvalidOrEmptyString = (value) => { return typeof value !== "string" || value === ""; }; var Rules = /* @__PURE__ */ ((Rules2) => { Rules2["Required"] = "required"; Rules2["Email"] = "email"; Rules2["MinLength"] = "minLength"; Rules2["MaxLength"] = "maxLength"; Rules2["Password"] = "password"; Rules2["Number"] = "number"; Rules2["Integer"] = "integer"; Rules2["MaxNumber"] = "maxNumber"; Rules2["MinNumber"] = "minNumber"; Rules2["StrongPassword"] = "strongPassword"; Rules2["CustomRegexp"] = "customRegexp"; Rules2["MinFilesCount"] = "minFilesCount"; Rules2["MaxFilesCount"] = "maxFilesCount"; Rules2["Files"] = "files"; return Rules2; })(Rules || {}); var GroupRules = /* @__PURE__ */ ((GroupRules2) => { GroupRules2["Required"] = "required"; return GroupRules2; })(GroupRules || {}); var CustomStyleTagIds = /* @__PURE__ */ ((CustomStyleTagIds2) => { CustomStyleTagIds2["Label"] = "label"; CustomStyleTagIds2["LabelArrow"] = "labelArrow"; return CustomStyleTagIds2; })(CustomStyleTagIds || {}); const defaultDictionary = [ { key: Rules.Required, dict: { en: "The field is required" } }, { key: Rules.Email, dict: { en: "Email has invalid format" } }, { key: Rules.MaxLength, dict: { en: "The field must contain a maximum of :value characters" } }, { key: Rules.MinLength, dict: { en: "The field must contain a minimum of :value characters" } }, { key: Rules.Password, dict: { en: "Password must contain minimum eight characters, at least one letter and one number" } }, { key: Rules.StrongPassword, dict: { en: "Password should contain minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character" } }, { key: Rules.Number, dict: { en: "Value should be a number" } }, { key: Rules.MaxNumber, dict: { en: "Number should be less or equal than :value" } }, { key: Rules.MinNumber, dict: { en: "Number should be more or equal than :value" } }, { key: Rules.MinFilesCount, dict: { en: "Files count should be more or equal than :value" } }, { key: Rules.MaxFilesCount, dict: { en: "Files count should be less or equal than :value" } }, { key: Rules.Files, dict: { en: "Uploaded files have one or several invalid properties (extension/size/type etc)." } } ]; const DEFAULT_ERROR_FIELD_MESSAGE = "Value is incorrect"; const isPromise = (val) => typeof val === "object" && val !== null && "then" in val && typeof val.then === "function"; const getNodeParents = (el) => { let elem = el; const els = []; while (elem) { els.unshift(elem); elem = elem.parentNode; } return els; }; const getClosestParent = (groups, parents) => { const reversedParents = [...parents].reverse(); for (let i = 0, len = reversedParents.length; i < len; ++i) { const parent = reversedParents[i]; for (const key in groups) { const group = groups[key]; if (group.groupElem === parent) { return [key, group]; } } } return null; }; const getClassList = (classList) => { if (Array.isArray(classList)) { return classList.filter((cls) => cls.length > 0); } if (typeof classList === "string" && classList.trim()) { return [...classList.split(" ").filter((cls) => cls.length > 0)]; } return []; }; const isElement = (element) => { return element instanceof Element || element instanceof HTMLDocument; }; const errorLabelCss = `.just-validate-error-label[data-tooltip=true]{position:fixed;padding:4px 8px;background:#423f3f;color:#fff;white-space:nowrap;z-index:10;border-radius:4px;transform:translateY(-5px)}.just-validate-error-label[data-tooltip=true]:before{content:'';width:0;height:0;border-left:solid 5px transparent;border-right:solid 5px transparent;border-bottom:solid 5px #423f3f;position:absolute;z-index:3;display:block;bottom:-5px;transform:rotate(180deg);left:calc(50% - 5px)}.just-validate-error-label[data-tooltip=true][data-direction=left]{transform:translateX(-5px)}.just-validate-error-label[data-tooltip=true][data-direction=left]:before{right:-7px;bottom:auto;left:auto;top:calc(50% - 2px);transform:rotate(90deg)}.just-validate-error-label[data-tooltip=true][data-direction=right]{transform:translateX(5px)}.just-validate-error-label[data-tooltip=true][data-direction=right]:before{right:auto;bottom:auto;left:-7px;top:calc(50% - 2px);transform:rotate(-90deg)}.just-validate-error-label[data-tooltip=true][data-direction=bottom]{transform:translateY(5px)}.just-validate-error-label[data-tooltip=true][data-direction=bottom]:before{right:auto;bottom:auto;left:calc(50% - 5px);top:-5px;transform:rotate(0)}`; const TOOLTIP_ARROW_HEIGHT = 5; const defaultGlobalConfig = { errorFieldStyle: { color: "#b81111", border: "1px solid #B81111" }, errorFieldCssClass: "just-validate-error-field", successFieldCssClass: "just-validate-success-field", errorLabelStyle: { color: "#b81111" }, errorLabelCssClass: "just-validate-error-label", successLabelCssClass: "just-validate-success-label", focusInvalidField: true, lockForm: true, testingMode: false, validateBeforeSubmitting: false, submitFormAutomatically: false }; class JustValidate { constructor(form, globalConfig, dictLocale) { __publicField(this, "form", null); __publicField(this, "fields", {}); __publicField(this, "groupFields", {}); __publicField(this, "errors", {}); __publicField(this, "isValid", false); __publicField(this, "isSubmitted", false); __publicField(this, "globalConfig", defaultGlobalConfig); __publicField(this, "errorLabels", {}); __publicField(this, "successLabels", {}); __publicField(this, "eventListeners", []); __publicField(this, "dictLocale", defaultDictionary); __publicField(this, "currentLocale", "en"); __publicField(this, "customStyleTags", {}); __publicField(this, "onSuccessCallback"); __publicField(this, "onFailCallback"); __publicField(this, "onValidateCallback"); __publicField(this, "tooltips", []); __publicField(this, "lastScrollPosition"); __publicField(this, "isScrollTick"); __publicField(this, "fieldIds", /* @__PURE__ */ new Map()); __publicField(this, "getKeyByFieldSelector", (field) => { return this.fieldIds.get(field); }); __publicField(this, "getFieldSelectorByKey", (key) => { for (const [fieldSelector, k] of this.fieldIds) { if (key === k) { return fieldSelector; } } return void 0; }); __publicField(this, "getCompatibleFields", () => { const fields = {}; Object.keys(this.fields).forEach((key) => { let newKey = key; const fieldSelector = this.getFieldSelectorByKey(key); if (typeof fieldSelector === "string") { newKey = fieldSelector; } fields[newKey] = { ...this.fields[key] }; }); return fields; }); __publicField(this, "setKeyByFieldSelector", (field) => { if (this.fieldIds.has(field)) { return this.fieldIds.get(field); } const key = String(this.fieldIds.size + 1); this.fieldIds.set(field, key); return key; }); __publicField(this, "refreshAllTooltips", () => { this.tooltips.forEach((item) => { item.refresh(); }); }); __publicField(this, "handleDocumentScroll", () => { this.lastScrollPosition = window.scrollY; if (!this.isScrollTick) { window.requestAnimationFrame(() => { this.refreshAllTooltips(); this.isScrollTick = false; }); this.isScrollTick = true; } }); __publicField(this, "formSubmitHandler", (ev) => { ev.preventDefault(); this.isSubmitted = true; this.validateHandler(ev); }); __publicField(this, "handleFieldChange", (target) => { let foundKey; for (const key in this.fields) { const field = this.fields[key]; if (field.elem === target) { foundKey = key; break; } } if (!foundKey) { return; } this.fields[foundKey].touched = true; this.validateField(foundKey, true); }); __publicField(this, "handleGroupChange", (target) => { let foundKey; for (const key in this.groupFields) { const group = this.groupFields[key]; if (group.elems.find((elem) => elem === target)) { foundKey = key; break; } } if (!foundKey) { return; } this.groupFields[foundKey].touched = true; this.validateGroup(foundKey, true); }); __publicField(this, "handlerChange", (ev) => { if (!ev.target) { return; } this.handleFieldChange(ev.target); this.handleGroupChange(ev.target); this.renderErrors(); }); this.initialize(form, globalConfig, dictLocale); } initialize(form, globalConfig, dictLocale) { this.form = null; this.errors = {}; this.isValid = false; this.isSubmitted = false; this.globalConfig = defaultGlobalConfig; this.errorLabels = {}; this.successLabels = {}; this.eventListeners = []; this.customStyleTags = {}; this.tooltips = []; this.currentLocale = "en"; if (typeof form === "string") { const elem = document.querySelector(form); if (!elem) { throw Error( `Form with ${form} selector not found! Please check the form selector` ); } this.setForm(elem); } else if (form instanceof HTMLFormElement) { this.setForm(form); } else { throw Error( `Form selector is not valid. Please specify a string selector or a DOM element.` ); } this.globalConfig = { ...defaultGlobalConfig, ...globalConfig }; if (dictLocale) { this.dictLocale = [...dictLocale, ...defaultDictionary]; } if (this.isTooltip()) { const styleTag = document.createElement("style"); styleTag.textContent = errorLabelCss; this.customStyleTags[CustomStyleTagIds.Label] = document.head.appendChild(styleTag); this.addListener("scroll", document, this.handleDocumentScroll); } } getLocalisedString(rule, ruleValue, customMsg) { var _a; const search = customMsg != null ? customMsg : rule; let localisedStr = (_a = this.dictLocale.find((item) => item.key === search)) == null ? void 0 : _a.dict[this.currentLocale]; if (!localisedStr) { if (customMsg) { localisedStr = customMsg; } } if (localisedStr && ruleValue !== void 0) { switch (rule) { case Rules.MaxLength: case Rules.MinLength: case Rules.MaxNumber: case Rules.MinNumber: case Rules.MinFilesCount: case Rules.MaxFilesCount: localisedStr = localisedStr.replace(":value", String(ruleValue)); } } return localisedStr || customMsg || DEFAULT_ERROR_FIELD_MESSAGE; } getFieldErrorMessage(fieldRule, elem) { const msg = typeof fieldRule.errorMessage === "function" ? fieldRule.errorMessage(this.getElemValue(elem), this.fields) : fieldRule.errorMessage; return this.getLocalisedString(fieldRule.rule, fieldRule.value, msg); } getFieldSuccessMessage(successMessage, elem) { const msg = typeof successMessage === "function" ? successMessage(this.getElemValue(elem), this.fields) : successMessage; return this.getLocalisedString(void 0, void 0, msg); } getGroupErrorMessage(groupRule) { return this.getLocalisedString( groupRule.rule, void 0, groupRule.errorMessage ); } getGroupSuccessMessage(groupRule) { if (!groupRule.successMessage) { return void 0; } return this.getLocalisedString( void 0, void 0, groupRule.successMessage ); } setFieldInvalid(key, fieldRule) { this.fields[key].isValid = false; this.fields[key].errorMessage = this.getFieldErrorMessage( fieldRule, this.fields[key].elem ); } setFieldValid(key, successMessage) { this.fields[key].isValid = true; if (successMessage !== void 0) { this.fields[key].successMessage = this.getFieldSuccessMessage( successMessage, this.fields[key].elem ); } } setGroupInvalid(key, groupRule) { this.groupFields[key].isValid = false; this.groupFields[key].errorMessage = this.getGroupErrorMessage(groupRule); } setGroupValid(key, groupRule) { this.groupFields[key].isValid = true; this.groupFields[key].successMessage = this.getGroupSuccessMessage(groupRule); } getElemValue(elem) { switch (elem.type) { case "checkbox": return elem.checked; case "file": return elem.files; default: return elem.value; } } validateGroupRule(key, elems, groupRule) { switch (groupRule.rule) { case GroupRules.Required: { if (elems.every((elem) => !elem.checked)) { this.setGroupInvalid(key, groupRule); } else { this.setGroupValid(key, groupRule); } } } } validateFieldRule(key, elem, fieldRule, afterInputChanged = false) { const ruleValue = fieldRule.value; const elemValue = this.getElemValue(elem); if (fieldRule.plugin) { const result = fieldRule.plugin( elemValue, this.getCompatibleFields() ); if (!result) { this.setFieldInvalid(key, fieldRule); } return; } switch (fieldRule.rule) { case Rules.Required: { if (isEmpty(elemValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.Email: { if (isInvalidOrEmptyString(elemValue)) { break; } if (!isEmail(elemValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.MaxLength: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (typeof ruleValue !== "number") { console.error( `Value for ${fieldRule.rule} rule for [${key}] should be a number. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (isInvalidOrEmptyString(elemValue)) { break; } if (isLengthMoreThanMax(elemValue, ruleValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.MinLength: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (typeof ruleValue !== "number") { console.error( `Value for ${fieldRule.rule} rule for [${key}] should be a number. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (isInvalidOrEmptyString(elemValue)) { break; } if (isLengthLessThanMin(elemValue, ruleValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.Password: { if (isInvalidOrEmptyString(elemValue)) { break; } if (!isPassword(elemValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.StrongPassword: { if (isInvalidOrEmptyString(elemValue)) { break; } if (!isStrongPassword(elemValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.Number: { if (isInvalidOrEmptyString(elemValue)) { break; } if (!isNumber(elemValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.Integer: { if (isInvalidOrEmptyString(elemValue)) { break; } if (!isInteger(elemValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.MaxNumber: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (typeof ruleValue !== "number") { console.error( `Value for ${fieldRule.rule} rule for [${key}] field should be a number. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (isInvalidOrEmptyString(elemValue)) { break; } const num = +elemValue; if (Number.isNaN(num) || isNumberMoreThanMax(num, ruleValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.MinNumber: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (typeof ruleValue !== "number") { console.error( `Value for ${fieldRule.rule} rule for [${key}] field should be a number. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (isInvalidOrEmptyString(elemValue)) { break; } const num = +elemValue; if (Number.isNaN(num) || isNumberLessThanMin(num, ruleValue)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.CustomRegexp: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); return; } let regexp; try { regexp = new RegExp(ruleValue); } catch (e) { console.error( `Value for ${fieldRule.rule} rule for [${key}] should be a valid regexp. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } const str = String(elemValue); if (str !== "" && !regexp.test(str)) { this.setFieldInvalid(key, fieldRule); } break; } case Rules.MinFilesCount: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (typeof ruleValue !== "number") { console.error( `Value for ${fieldRule.rule} rule for [${key}] field should be a number. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (Number.isFinite(elemValue == null ? void 0 : elemValue.length) && elemValue.length < ruleValue) { this.setFieldInvalid(key, fieldRule); break; } break; } case Rules.MaxFilesCount: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (typeof ruleValue !== "number") { console.error( `Value for ${fieldRule.rule} rule for [${key}] field should be a number. The field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); break; } if (Number.isFinite(elemValue == null ? void 0 : elemValue.length) && elemValue.length > ruleValue) { this.setFieldInvalid(key, fieldRule); break; } break; } case Rules.Files: { if (ruleValue === void 0) { console.error( `Value for ${fieldRule.rule} rule for [${key}] field is not defined. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); return; } if (typeof ruleValue !== "object") { console.error( `Value for ${fieldRule.rule} rule for [${key}] field should be an object. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); return; } const filesConfig = ruleValue.files; if (typeof filesConfig !== "object") { console.error( `Value for ${fieldRule.rule} rule for [${key}] field should be an object with files array. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); return; } const isFilePropsInvalid = (file, fileConfig) => { const minSizeInvalid = Number.isFinite(fileConfig.minSize) && file.size < fileConfig.minSize; const maxSizeInvalid = Number.isFinite(fileConfig.maxSize) && file.size > fileConfig.maxSize; const nameInvalid = Array.isArray(fileConfig.names) && !fileConfig.names.includes(file.name); const extInvalid = Array.isArray(fileConfig.extensions) && !fileConfig.extensions.includes( file.name.split(".")[file.name.split(".").length - 1] ); const typeInvalid = Array.isArray(fileConfig.types) && !fileConfig.types.includes(file.type); return minSizeInvalid || maxSizeInvalid || nameInvalid || extInvalid || typeInvalid; }; if (typeof elemValue === "object" && elemValue !== null) { for (let fileIdx = 0, len = elemValue.length; fileIdx < len; ++fileIdx) { const file = elemValue.item(fileIdx); if (!file) { this.setFieldInvalid(key, fieldRule); break; } const filesInvalid = isFilePropsInvalid(file, filesConfig); if (filesInvalid) { this.setFieldInvalid(key, fieldRule); break; } } } break; } default: { if (typeof fieldRule.validator !== "function") { console.error( `Validator for custom rule for [${key}] field should be a function. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); return; } const result = fieldRule.validator( elemValue, this.getCompatibleFields() ); if (typeof result !== "boolean" && typeof result !== "function") { console.error( `Validator return value for [${key}] field should be boolean or function. It will be cast to boolean.` ); } if (typeof result === "function") { if (afterInputChanged) { this.fields[key].asyncCheckPending = true; } else { this.fields[key].asyncCheckPending = false; const promise = result(); if (!isPromise(promise)) { console.error( `Validator function for custom rule for [${key}] field should return a Promise. This field will be always invalid.` ); this.setFieldInvalid(key, fieldRule); return; } return promise.then((resp) => { if (!resp) { this.setFieldInvalid(key, fieldRule); } }).catch(() => { this.setFieldInvalid(key, fieldRule); }); } } if (!result) { this.setFieldInvalid(key, fieldRule); } } } } isFormValid() { let isValid = true; for (let i = 0, len = Object.values(this.fields).length; i < len; ++i) { const item = Object.values(this.fields)[i]; if (item.isValid === void 0) { isValid = void 0; break; } if (item.isValid === false) { isValid = false; break; } } for (let i = 0, len = Object.values(this.groupFields).length; i < len; ++i) { const item = Object.values(this.groupFields)[i]; if (item.isValid === void 0) { isValid = void 0; break; } if (item.isValid === false) { isValid = false; break; } } return isValid; } validateField(key, afterInputChanged = false) { var _a; const field = this.fields[key]; field.isValid = true; const promises = []; [...field.rules].reverse().forEach((rule) => { const res = this.validateFieldRule( key, field.elem, rule, afterInputChanged ); if (isPromise(res)) { promises.push(res); } }); if (field.isValid) { this.setFieldValid(key, (_a = field.config) == null ? void 0 : _a.successMessage); } return Promise.allSettled(promises).finally(() => { var _a2; if (afterInputChanged) { (_a2 = this.onValidateCallback) == null ? void 0 : _a2.call(this, { isValid: this.isFormValid(), isSubmitted: this.isSubmitted, fields: this.getCompatibleFields(), groups: { ...this.groupFields } }); } }); } revalidateField(fieldSelector) { if (typeof fieldSelector !== "string" && !isElement(fieldSelector)) { throw Error( `Field selector is not valid. Please specify a string selector or a valid DOM element.` ); } const key = this.getKeyByFieldSelector(fieldSelector); if (!key || !this.fields[key]) { console.error(`Field not found. Check the field selector.`); return Promise.reject(); } return new Promise((resolve) => { this.validateField(key, true).finally(() => { this.clearFieldStyle(key); this.clearFieldLabel(key); this.renderFieldError(key, true); resolve(!!this.fields[key].isValid); }); }); } revalidateGroup(groupSelector) { if (typeof groupSelector !== "string" && !isElement(groupSelector)) { throw Error( `Group selector is not valid. Please specify a string selector or a valid DOM element.` ); } const key = this.getKeyByFieldSelector(groupSelector); if (!key || !this.groupFields[key]) { console.error(`Group not found. Check the group selector.`); return Promise.reject(); } return new Promise((resolve) => { this.validateGroup(key).finally(() => { this.clearFieldLabel(key); this.renderGroupError(key, true); resolve(!!this.groupFields[key].isValid); }); }); } validateGroup(key, afterInputChanged = false) { const group = this.groupFields[key]; const promises = []; [...group.rules].reverse().forEach((rule) => { const res = this.validateGroupRule(key, group.elems, rule); if (isPromise(res)) { promises.push(res); } }); return Promise.allSettled(promises).finally(() => { var _a; if (afterInputChanged) { (_a = this.onValidateCallback) == null ? void 0 : _a.call(this, { isValid: this.isFormValid(), isSubmitted: this.isSubmitted, fields: this.getCompatibleFields(), groups: { ...this.groupFields } }); } }); } focusInvalidField() { for (const key in this.fields) { const field = this.fields[key]; if (!field.isValid) { setTimeout(() => field.elem.focus(), 0); break; } } } afterSubmitValidation(forceRevalidation = false) { this.renderErrors(forceRevalidation); if (this.globalConfig.focusInvalidField) { this.focusInvalidField(); } } validate(forceRevalidation = false) { return new Promise((resolve) => { const promises = []; Object.keys(this.fields).forEach((key) => { const promise = this.validateField(key); if (isPromise(promise)) { promises.push(promise); } }); Object.keys(this.groupFields).forEach((key) => { const promise = this.validateGroup(key); if (isPromise(promise)) { promises.push(promise); } }); Promise.allSettled(promises).then(() => { var _a; this.afterSubmitValidation(forceRevalidation); (_a = this.onValidateCallback) == null ? void 0 : _a.call(this, { isValid: this.isFormValid(), isSubmitted: this.isSubmitted, fields: this.getCompatibleFields(), groups: { ...this.groupFields } }); resolve(!!promises.length); }); }); } revalidate() { return new Promise((resolve) => { this.validateHandler(void 0, true).finally(() => { if (this.globalConfig.focusInvalidField) { this.focusInvalidField(); } resolve(this.isValid); }); }); } validateHandler(ev, forceRevalidation = false) { if (this.globalConfig.lockForm) { this.lockForm(); } return this.validate(forceRevalidation).finally(() => { var _a, _b, _c; if (this.globalConfig.lockForm) { this.unlockForm(); } if (this.isValid) { (_a = this.onSuccessCallback) == null ? void 0 : _a.call(this, ev); if (this.globalConfig.submitFormAutomatically) { (_b = ev == null ? void 0 : ev.currentTarget) == null ? void 0 : _b.submit(); } } else { (_c = this.onFailCallback) == null ? void 0 : _c.call(this, this.getCompatibleFields(), this.groupFields); } }); } setForm(form) { this.form = form; this.form.setAttribute("novalidate", "novalidate"); this.removeListener("submit", this.form, this.formSubmitHandler); this.addListener("submit", this.form, this.formSubmitHandler); } addListener(type, elem, handler) { elem.addEventListener(type, handler); this.eventListeners.push({ type, elem, func: handler }); } removeListener(type, elem, handler) { elem.removeEventListener(type, handler); this.eventListeners = this.eventListeners.filter( (item) => item.type !== type || item.elem !== elem ); } addField(fieldSelector, rules, config) { if (typeof fieldSelector !== "string" && !isElement(fieldSelector)) { throw Error( `Field selector is not valid. Please specify a string selector or a valid DOM element.` ); } let elem; if (typeof fieldSelector === "string") { elem = this.form.querySelector(fieldSelector); } else { elem = fieldSelector; } if (!elem) { throw Error( `Field doesn't exist in the DOM! Please check the field selector.` ); } if (!Array.isArray(rules) || !rules.length) { throw Error( `Rules argument should be an array and should contain at least 1 element.` ); } rules.forEach((item) => { if (!("rule" in item || "validator" in item || "plugin" in item)) { throw Error( `Rules argument must contain at least one rule or validator property.` ); } if (!item.validator && !item.plugin && (!item.rule || !Object.values(Rules).includes(item.rule))) { throw Error( `Rule should be one of these types: ${Object.values(Rules).join( ", " )}. Provided value: ${item.rule}` ); } }); const key = this.setKeyByFieldSelector(fieldSelector); this.fields[key] = { elem, rules, isValid: void 0, touched: false, config }; this.setListeners(elem); if (this.isSubmitted || this.globalConfig.validateBeforeSubmitting) { this.validateField(key); } return this; } removeField(fieldSelector) { if (typeof fieldSelector !== "string" && !isElement(fieldSelector)) { throw Error( `Field selector is not valid. Please specify a string selector or a valid DOM element.` ); } const key = this.getKeyByFieldSelector(fieldSelector); if (!key || !this.fields[key]) { console.error(`Field not found. Check the field selector.`); return this; } const type = this.getListenerType(this.fields[key].elem.type); this.removeListener(type, this.fields[key].elem, this.handlerChange); this.clearErrors(); delete this.fields[key]; return this; } removeGroup(group) { if (typeof group !== "string") { throw Error( `Group selector is not valid. Please specify a string selector.` ); } const key = this.getKeyByFieldSelector(group); if (!key || !this.groupFields[key]) { console.error(`Group not found. Check the group selector.`); return this; } this.groupFields[key].elems.forEach((elem) => { const type = this.getListenerType(elem.type); this.removeListener(type, elem, this.handlerChange); }); this.clearErrors(); delete this.groupFields[key]; return this; } addRequiredGroup(groupField, errorMessage, config, successMessage) { if (typeof groupField !== "string" && !isElement(groupField)) { throw Error( `Group selector is not valid. Please specify a string selector or a valid DOM element.` ); } let elem; if (typeof groupField === "string") { elem = this.form.querySelector(groupField); } else { elem = groupField; } if (!elem) { throw Error(`Group selector not found! Please check the group selector.`); } const inputs = elem.querySelectorAll("input"); const childrenInputs = Array.from(inputs).filter((input) => { const parent = getClosestParent(this.groupFields, getNodeParents(input)); if (!parent) { return true; } return parent[1].elems.find((elem2) => elem2 !== input); }); const key = this.setKeyByFieldSelector(groupField); this.groupFields[key] = { rules: [ { rule: GroupRules.Required, errorMessage, successMessage } ], groupElem: elem, elems: childrenInputs, touched: false, isValid: void 0, config }; inputs.forEach((input) => { this.setListeners(input); }); return this; } getListenerType(type) { switch (type) { case "checkbox": case "select-one": case "file": case "radio": { return "change"; } default: { return "input"; } } } setListeners(elem) { const type = this.getListenerType(elem.type); this.removeListener(type, elem, this.handlerChange); this.addListener(type, elem, this.handlerChange); } clearFieldLabel(key) { var _a, _b; (_a = this.errorLabels[key]) == null ? void 0 : _a.remove(); (_b = this.successLabels[key]) == null ? void 0 : _b.remove(); } clearFieldStyle(key) { var _a, _b, _c, _d; const field = this.fields[key]; const errorStyle = ((_a = field.config) == null ? void 0 : _a.errorFieldStyle) || this.globalConfig.errorFieldStyle; Object.keys(errorStyle).forEach((key2) => { field.elem.style[key2] = ""; }); const successStyle = ((_b = field.config) == null ? void 0 : _b.successFieldStyle) || this.globalConfig.successFieldStyle || {}; Object.keys(successStyle).forEach((key2) => { field.elem.style[key2] = ""; }); field.elem.classList.remove( ...getClassList( ((_c = field.config) == null ? void 0 : _c.errorFieldCssClass) || this.globalConfig.errorFieldCssClass ), ...getClassList( ((_d = field.config) == null ? void 0 : _d.successFieldCssClass) || this.globalConfig.successFieldCssClass ) ); } clearErrors() { var _a, _b; Object.keys(this.errorLabels).forEach( (key) => this.errorLabels[key].remove() ); Object.keys(this.successLabels).forEach( (key) => this.successLabels[key].remove() ); for (const key in this.fields) { this.clearFieldStyle(key); } for (const key in this.groupFields) { const group = this.groupFields[key]; const errorStyle = ((_a = group.config) == null ? void 0 : _a.errorFieldStyle) || this.globalConfig.errorFieldStyle; Object.keys(errorStyle).forEach((key2) => { group.elems.forEach((elem) => { var _a2; elem.style[key2] = ""; elem.classList.remove( ...getClassList( ((_a2 = group.config) == null ? void 0 : _a2.errorFieldCssClass) || this.globalConfig.errorFieldCssClass ) ); }); }); const successStyle = ((_b = group.config) == null ? void 0 : _b.successFieldStyle) || this.globalConfig.successFieldStyle || {}; Object.keys(successStyle).forEach((key2) => { group.elems.forEach((elem) => { var _a2; elem.style[key2] = ""; elem.classList.remove( ...getClassList( ((_a2 = group.config) == null ? void 0 : _a2.successFieldCssClass) || this.globalConfig.successFieldCssClass ) ); }); }); } this.tooltips = []; } isTooltip() { return !!this.globalConfig.tooltip; } lockForm() { const elems = this.form.querySelectorAll( "input, textarea, button, select" ); for (let i = 0, len = elems.length; i < len; ++i) { elems[i].setAttribute( "data-just-validate-fallback-disabled", elems[i].disabled ? "true" : "false" ); elems[i].setAttribute("disabled", "disabled"); elems[i].style.pointerEvents = "none"; elems[i].style.webkitFilter = "grayscale(100%)"; elems[i].style.filter = "grayscale(100%)"; } } unlockForm() { const elems = this.form.querySelectorAll( "input, textarea, button, select" ); for (let i = 0, len = elems.length; i < len; ++i) { if (elems[i].getAttribute("data-just-validate-fallback-disabled") !== "true") { elems[i].removeAttribute("disabled"); } elems[i].style.pointerEvents = ""; elems[i].style.webkitFilter = ""; elems[i].style.filter = ""; } } renderTooltip(elem, errorLabel, position) { var _a; const { top, left, width, height } = elem.getBoundingClientRect(); const errorLabelRect = errorLabel.getBoundingClientRect(); const pos = position || ((_a = this.globalConfig.tooltip) == null ? void 0 : _a.position); switch (pos) { case "left": { errorLabel.style.top = `${top + height / 2 - errorLabelRect.height / 2}px`; errorLabel.style.left = `${left - errorLabelRect.width - TOOLTIP_ARROW_HEIGHT}px`; break; } case "top": { errorLabel.style.top = `${top - errorLabelRect.height - TOOLTIP_ARROW_HEIGHT}px`; errorLabel.style.left = `${left + width / 2 - errorLabelRect.width / 2}px`; break; } case "right": { errorLabel.style.top = `${top + height / 2 - errorLabelRect.height / 2}px`; errorLabel.style.left = `${left + width + TOOLTIP_ARROW_HEIGHT}px`; break; } case "bottom": { errorLabel.style.top = `${top + height + TOOLTIP_ARROW_HEIGHT}px`; errorLabel.style.left = `${left + width / 2 - errorLabelRect.width / 2}px`; break; } } errorLabel.dataset.direction = pos; const refresh = () => { this.renderTooltip(elem, errorLabel, position); }; return { refresh }; } createErrorLabelElem(key, errorMessage, config) { const errorLabel = document.createElement("div"); errorLabel.innerHTML = errorMessage; const customErrorLabelStyle = this.isTooltip() ? config == null ? void 0 : config.errorLabelStyle : (config == null ? void 0 : config.errorLabelStyle) || this.globalConfig.errorLabelStyle; Object.assign(errorLabel.style, customErrorLabelStyle); errorLabel.classList.add( ...getClassList( (config == null ? void 0 : config.errorLabelCssClass) || this.globalConfig.errorLabelCssClass ), "just-validate-error-label" ); if (this.isTooltip()) { errorLabel.dataset.tooltip = "true"; } if (this.globalConfig.testingMode) { errorLabel.dataset.testId = `error-label-${key}`; } this.errorLabels[key] = errorLabel; return errorLabel; } createSuccessLabelElem(key, successMessage, config) { if (successMessage === void 0) { return null; } const successLabel = document.createElement("div"); successLabel.innerHTML = successMessage; const customSuccessLabelStyle = (config == null ? void 0 : config.successLabelStyle) || this.globalConfig.successLabelStyle; Object.assign(successLabel.style, customSuccessLabelStyle); successLabel.classList.add( ...getClassList( (config == null ? void 0 : config.successLabelCssClass) || this.globalConfig.successLabelCssClass ), "just-validate-success-label" ); if (this.globalConfig.testingMode) { successLabel.dataset.testId = `success-label-${key}`; } this.successLabels[key] = successLabel; return successLabel; } renderErrorsContainer(label, errorsContainer) { const container = errorsContainer || this.globalConfig.errorsContainer; if (typeof container === "string") { const elem = this.form.querySelector(container); if (elem) { elem.appendChild(label); return true; } else { console.error( `Error container with ${container} selector not found. Errors will be rendered as usual` ); } } if (container instanceof Element) { container.appendChild(label); return true; } if (container !== void 0) { console.error( `Error container not found. It should be a string or existing Element. Errors will be rendered as usual` ); } return false; } renderGroupLabel(elem, label, errorsContainer, isSuccess) { if (!isSuccess) { const renderedInErrorsContainer = this.renderErrorsContainer( label, errorsContainer ); if (renderedInErrorsContainer) { return; } } elem.appendChild(label); } renderFieldLabel(elem, label, errorsContainer, isSuccess) { var _a, _b, _c, _d, _e, _f, _g; if (!isSuccess) { const renderedInErrorsContainer = this.renderErrorsContainer( label, errorsContainer ); if (renderedInErrorsContainer) { return; } } if (elem.type === "checkbox" || elem.type === "radio") { const labelElem = document.querySelector( `label[for="${elem.getAttribute("id")}"]` ); if (((_b = (_a = elem.parentElement) == null ? void 0 : _a.tagName) == null ? void 0 : _b.toLowerCase()) === "label") { (_d = (_c = elem.parentElement) == null ? void 0 : _c.parentElement) == null ? void 0 : _d.appendChild(label); } else if (labelElem) { (_e = labelElem.parentElement) == null ? void 0 : _e.appendChild(label); } else { (_f = elem.parentElement) == null ? void 0 : _f.appendChild(label); } } else { (_g = elem.parentElement) == null ? void 0 : _g.appendChild(label); } } showLabels(fields, isError) { Object.keys(fields).forEach((fieldName, i) => { const error = fields[fieldName]; const key = this.getKeyByFieldSelector(fieldName); if (!key || !this.fields[key]) { console.error(`Field not found. Check the field selector.`); return; } const field = this.fields[key]; field.isValid = !isError; this.clearFieldStyle(key); this.clearFieldLabel(key); this.renderFieldError(key, false, error); if (i === 0 && this.globalConfig.focusInvalidField) { setTimeout(() => field.elem.focus(), 0); } }); } showErrors(fields) { if (typeof fields !== "object") { throw Error( "[showErrors]: Errors should be an object with key: value format" ); } this.showLabels(fields, true); } showSuccessLabels(fields) { if (typeof fields !== "object") { throw Error( "[showSuccessLabels]: Labels should be an object with key: value format" ); } this.showLabels(fields, false); } renderFieldError(key, forced = false, message) { var _a, _b, _c, _d, _e, _f; const field = this.fields[key]; if (field.isValid === false) { this.isValid = false; } if (field.isValid === void 0 || !forced && !this.isSubmitted && !field.touched && message === void 0) { return; } if (field.isValid) { if (!field.asyncCheckPending) { const successLabel = this.createSuccessLabelElem( key, message !== void 0 ? message : field.successMessage, field.config ); if (successLabel) { this.renderFieldLabel( field.elem, successLabel, (_a = field.config) == null ? void 0 : _a.errorsContainer, true ); } field.elem.classList.add( ...getClassList( ((_b = field.config) == null ? void 0 : _b.successFieldCssClass) || this.globalConfig.successFieldCssClass ) ); } return; } field.elem.classList.add( ...getClassList( ((_c = field.config) == null ? void 0 : _c.errorFieldCssClass) || this.globalConfig.errorFieldCssClass ) ); const errorLabel = this.createErrorLabelElem( key, message !== void 0 ? message : field.errorMessage, field.config ); this.renderFieldLabel( field.elem, errorLabel, (_d = field.config) == null ? void 0 : _d.errorsContainer ); if (this.isTooltip()) { this.tooltips.push( this.renderTooltip( field.elem, errorLabel, (_f = (_e = field.config) == null ? void 0 : _e.tooltip) == null ? void 0 : _f.position ) ); } } renderGroupError(key, force = true) { var _a, _b, _c, _d; const group = this.groupFields[key]; if (group.isValid === false) { this.isValid = false; } if (group.isValid === void 0 || !force && !this.isSubmitted && !group.touched) { return; } if (group.isValid) { group.elems.forEach((elem) => { var _a2, _b2; Object.assign( elem.style, ((_a2 = group.config) == null ? void 0 : _a2.successFieldStyle) || this.globalConfig.successFieldStyle ); elem.classList.add( ...getClassList( ((_b2 = group.config) == null ? void 0 : _b2.successFieldCssClass) || this.globalConfig.successFieldCssClass ) ); }); const successLabel = this.createSuccessLabelElem( key, group.successMessage, group.config ); if (successLabel) { this.renderGroupLabel( group.groupElem, successLabel, (_a = group.config) == null ? void 0 : _a.errorsContainer, true ); } return; } this.isValid = false; group.elems.forEach((elem) => { var _a2, _b2; Object.assign( elem.style, ((_a2 = group.config) == null ? void 0 : _a2.errorFieldStyle) || this.globalConfig.errorFieldStyle ); elem.classList.add( ...getClassList( ((_b2 = group.config) == null ? void 0 : _b2.errorFieldCssClass) || this.globalConfig.errorFieldCssClass ) ); }); const errorLabel = this.createErrorLabelElem( key, group.errorMessage, group.config ); this.renderGroupLabel( group.groupElem, errorLabel, (_b = group.config) == null ? void 0 : _b.errorsContainer ); if (this.isTooltip()) {