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
JavaScript
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()) {