easy-forms-core 1.1.3 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/easy-form.d.ts +79 -2
- package/dist/easy-form.js +469 -13
- package/dist/easy-form.js.map +1 -1
- package/dist/index.d.ts +146 -3
- package/dist/index.js +473 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -265,6 +265,13 @@ function getBaseStyles(colors) {
|
|
|
265
265
|
.easy-form-submit:active {
|
|
266
266
|
transform: scale(0.98);
|
|
267
267
|
}
|
|
268
|
+
.easy-form-submit-wrapper {
|
|
269
|
+
margin-top: 1rem;
|
|
270
|
+
margin-bottom: 0.5rem;
|
|
271
|
+
}
|
|
272
|
+
.easy-form-submit-wrapper .easy-form-submit {
|
|
273
|
+
min-width: 100px;
|
|
274
|
+
}
|
|
268
275
|
input:not([type="checkbox"]):not([type="radio"]), textarea, select {
|
|
269
276
|
width: 100%;
|
|
270
277
|
padding: 0.5rem;
|
|
@@ -710,6 +717,28 @@ function getBaseStyles(colors) {
|
|
|
710
717
|
transform: rotate(360deg);
|
|
711
718
|
}
|
|
712
719
|
}
|
|
720
|
+
/* Lock Overlay (bloqueo por intentos) */
|
|
721
|
+
.easy-form-lock-overlay {
|
|
722
|
+
position: absolute;
|
|
723
|
+
top: 0;
|
|
724
|
+
left: 0;
|
|
725
|
+
right: 0;
|
|
726
|
+
bottom: 0;
|
|
727
|
+
background: rgba(255, 255, 255, 0.9);
|
|
728
|
+
display: flex;
|
|
729
|
+
align-items: center;
|
|
730
|
+
justify-content: center;
|
|
731
|
+
z-index: 1001;
|
|
732
|
+
backdrop-filter: blur(4px);
|
|
733
|
+
border-radius: inherit;
|
|
734
|
+
text-align: center;
|
|
735
|
+
padding: 1.5rem;
|
|
736
|
+
}
|
|
737
|
+
.easy-form-lock-message {
|
|
738
|
+
font-size: 1rem;
|
|
739
|
+
color: var(--easy-form-text);
|
|
740
|
+
max-width: 280px;
|
|
741
|
+
}
|
|
713
742
|
/* Disabled State */
|
|
714
743
|
.easy-form-disabled,
|
|
715
744
|
.easy-form-disabled * {
|
|
@@ -1635,6 +1664,191 @@ function getPredefinedMask(type) {
|
|
|
1635
1664
|
return PREDEFINED_MASKS[type];
|
|
1636
1665
|
}
|
|
1637
1666
|
|
|
1667
|
+
// src/utils/injection-validation.ts
|
|
1668
|
+
var INJECTION_VALIDATION_MESSAGE = "El valor contiene caracteres o patrones no permitidos";
|
|
1669
|
+
var INJECTION_PATTERNS = [
|
|
1670
|
+
// SQL Injection
|
|
1671
|
+
/\b(union|select|insert|update|delete|drop|exec|execute|declare)\s+(all\s+)?(select|from|into|table)/i,
|
|
1672
|
+
/\b(or|and)\s+['"]?\d+['"]?\s*=\s*['"]?\d+/i,
|
|
1673
|
+
/;\s*(drop|delete|truncate|alter)\s+/i,
|
|
1674
|
+
/--\s*$/,
|
|
1675
|
+
// SQL comment
|
|
1676
|
+
/\/\*[\s\S]*\*\//,
|
|
1677
|
+
// Block comment
|
|
1678
|
+
/'\s*or\s+'1'\s*=\s*'1/i,
|
|
1679
|
+
/"\s*or\s+"1"\s*=\s*"1/i,
|
|
1680
|
+
/\bexec\s*\(/i,
|
|
1681
|
+
/\bxp_\w+/i,
|
|
1682
|
+
// SQL Server extended procedures
|
|
1683
|
+
// XSS / Scripting
|
|
1684
|
+
/<script\b[\s\S]*?>[\s\S]*?<\/script>/i,
|
|
1685
|
+
/<script\b/i,
|
|
1686
|
+
/javascript\s*:/i,
|
|
1687
|
+
/vbscript\s*:/i,
|
|
1688
|
+
/on\w+\s*=\s*["'][^"']*["']/i,
|
|
1689
|
+
// onclick=, onerror=, onload=, etc.
|
|
1690
|
+
/on\w+\s*=\s*[^\s>]+/i,
|
|
1691
|
+
/<iframe\b/i,
|
|
1692
|
+
/<object\b/i,
|
|
1693
|
+
/<embed\b/i,
|
|
1694
|
+
/\beval\s*\(/i,
|
|
1695
|
+
/document\.(cookie|write|location)/i,
|
|
1696
|
+
/window\.(location|open|eval)/i,
|
|
1697
|
+
// Command injection (shell)
|
|
1698
|
+
/[;&|]\s*(ls|cat|rm|wget|curl|nc|bash|sh|python|perl)\s/i,
|
|
1699
|
+
/\$\s*\([^)]+\)/,
|
|
1700
|
+
// $(...)
|
|
1701
|
+
/`[^`]+`/,
|
|
1702
|
+
// Backtick command substitution
|
|
1703
|
+
/\|\s*\w+/,
|
|
1704
|
+
// Pipe to command (with word after)
|
|
1705
|
+
// NoSQL / Template injection
|
|
1706
|
+
/\$\s*where\b/i,
|
|
1707
|
+
/\$\s*gt\b|\$\s*ne\b|\$\s*regex\b/i,
|
|
1708
|
+
/\{\{[^}]*\}\}/,
|
|
1709
|
+
// Template literals
|
|
1710
|
+
/\$\{[^}]*\}/
|
|
1711
|
+
// JS template literals
|
|
1712
|
+
];
|
|
1713
|
+
function containsInjection(value) {
|
|
1714
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
const normalized = value.trim();
|
|
1718
|
+
if (normalized.length === 0) return false;
|
|
1719
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
1720
|
+
if (pattern.test(value)) {
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return false;
|
|
1725
|
+
}
|
|
1726
|
+
function isSafeFromInjection(value) {
|
|
1727
|
+
if (value === null || value === void 0) {
|
|
1728
|
+
return true;
|
|
1729
|
+
}
|
|
1730
|
+
if (typeof value === "string") {
|
|
1731
|
+
return !containsInjection(value);
|
|
1732
|
+
}
|
|
1733
|
+
if (Array.isArray(value)) {
|
|
1734
|
+
return value.every((item) => isSafeFromInjection(item));
|
|
1735
|
+
}
|
|
1736
|
+
if (typeof value === "object") {
|
|
1737
|
+
return Object.values(value).every(
|
|
1738
|
+
(v) => isSafeFromInjection(v)
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
return true;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// src/utils/attempts-lock.ts
|
|
1745
|
+
var AttemptsLock = class {
|
|
1746
|
+
constructor(options) {
|
|
1747
|
+
this.attempts = 0;
|
|
1748
|
+
this.lockedUntil = null;
|
|
1749
|
+
this.unlockCheckInterval = null;
|
|
1750
|
+
this.maxAttempts = Math.max(1, options.maxAttempts);
|
|
1751
|
+
this.blockDurationMs = (options.blockDurationMinutes ?? 5) * 60 * 1e3;
|
|
1752
|
+
this.storageKey = options.storageKey;
|
|
1753
|
+
this.onLocked = options.onLocked;
|
|
1754
|
+
this.onUnlocked = options.onUnlocked;
|
|
1755
|
+
if (this.storageKey && typeof sessionStorage !== "undefined") {
|
|
1756
|
+
this.loadFromStorage();
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
loadFromStorage() {
|
|
1760
|
+
if (!this.storageKey || typeof sessionStorage === "undefined") return;
|
|
1761
|
+
try {
|
|
1762
|
+
const stored = sessionStorage.getItem(this.storageKey);
|
|
1763
|
+
if (stored) {
|
|
1764
|
+
const data = JSON.parse(stored);
|
|
1765
|
+
this.attempts = data.attempts ?? 0;
|
|
1766
|
+
this.lockedUntil = data.lockedUntil ?? null;
|
|
1767
|
+
this.checkExpiration();
|
|
1768
|
+
}
|
|
1769
|
+
} catch {
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
saveToStorage() {
|
|
1773
|
+
if (!this.storageKey || typeof sessionStorage === "undefined") return;
|
|
1774
|
+
try {
|
|
1775
|
+
const data = {
|
|
1776
|
+
attempts: this.attempts,
|
|
1777
|
+
lockedUntil: this.lockedUntil ?? void 0
|
|
1778
|
+
};
|
|
1779
|
+
sessionStorage.setItem(this.storageKey, JSON.stringify(data));
|
|
1780
|
+
} catch {
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
checkExpiration() {
|
|
1784
|
+
if (this.lockedUntil === null) return false;
|
|
1785
|
+
if (Date.now() >= this.lockedUntil) {
|
|
1786
|
+
this.reset();
|
|
1787
|
+
this.onUnlocked?.();
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
startUnlockCheck() {
|
|
1793
|
+
if (this.unlockCheckInterval) return;
|
|
1794
|
+
this.unlockCheckInterval = setInterval(() => {
|
|
1795
|
+
if (this.checkExpiration()) {
|
|
1796
|
+
this.stopUnlockCheck();
|
|
1797
|
+
}
|
|
1798
|
+
}, 1e3);
|
|
1799
|
+
}
|
|
1800
|
+
stopUnlockCheck() {
|
|
1801
|
+
if (this.unlockCheckInterval) {
|
|
1802
|
+
clearInterval(this.unlockCheckInterval);
|
|
1803
|
+
this.unlockCheckInterval = null;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Incrementa el contador de intentos. Si alcanza maxAttempts, bloquea.
|
|
1808
|
+
*/
|
|
1809
|
+
incrementAttempts() {
|
|
1810
|
+
this.attempts++;
|
|
1811
|
+
if (this.attempts >= this.maxAttempts) {
|
|
1812
|
+
this.lockedUntil = Date.now() + this.blockDurationMs;
|
|
1813
|
+
this.saveToStorage();
|
|
1814
|
+
this.onLocked?.(this.blockDurationMs);
|
|
1815
|
+
this.startUnlockCheck();
|
|
1816
|
+
} else {
|
|
1817
|
+
this.saveToStorage();
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Retorna true si el lock está activo (aún dentro del período de bloqueo)
|
|
1822
|
+
*/
|
|
1823
|
+
isLocked() {
|
|
1824
|
+
if (this.checkExpiration()) return false;
|
|
1825
|
+
return this.lockedUntil !== null && Date.now() < this.lockedUntil;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado
|
|
1829
|
+
*/
|
|
1830
|
+
getRemainingBlockTimeMs() {
|
|
1831
|
+
if (this.checkExpiration()) return 0;
|
|
1832
|
+
if (this.lockedUntil === null) return 0;
|
|
1833
|
+
return Math.max(0, this.lockedUntil - Date.now());
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Resetea el contador de intentos y el bloqueo
|
|
1837
|
+
*/
|
|
1838
|
+
reset() {
|
|
1839
|
+
this.attempts = 0;
|
|
1840
|
+
this.lockedUntil = null;
|
|
1841
|
+
this.stopUnlockCheck();
|
|
1842
|
+
this.saveToStorage();
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Retorna el número actual de intentos
|
|
1846
|
+
*/
|
|
1847
|
+
getAttempts() {
|
|
1848
|
+
return this.attempts;
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1638
1852
|
// src/utils/index.ts
|
|
1639
1853
|
function attributeValue(value) {
|
|
1640
1854
|
if (value === null || value === void 0) {
|
|
@@ -1728,6 +1942,8 @@ var ValidationEngine = class {
|
|
|
1728
1942
|
return this.validatePattern(value, validation.value);
|
|
1729
1943
|
case "custom":
|
|
1730
1944
|
return await this.validateCustom(value, validation);
|
|
1945
|
+
case "noInjection":
|
|
1946
|
+
return this.validateNoInjection(value, validation);
|
|
1731
1947
|
default:
|
|
1732
1948
|
return { isValid: true };
|
|
1733
1949
|
}
|
|
@@ -1838,6 +2054,19 @@ var ValidationEngine = class {
|
|
|
1838
2054
|
message: isValid ? void 0 : "El formato no es v\xE1lido"
|
|
1839
2055
|
};
|
|
1840
2056
|
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Valida que el valor no contenga patrones de inyección (SQL, XSS, etc.)
|
|
2059
|
+
*/
|
|
2060
|
+
validateNoInjection(value, validation) {
|
|
2061
|
+
if (value === null || value === void 0 || value === "") {
|
|
2062
|
+
return { isValid: true };
|
|
2063
|
+
}
|
|
2064
|
+
const isValid = isSafeFromInjection(value);
|
|
2065
|
+
return {
|
|
2066
|
+
isValid,
|
|
2067
|
+
message: isValid ? void 0 : validation.message || INJECTION_VALIDATION_MESSAGE
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
1841
2070
|
/**
|
|
1842
2071
|
* Valida con función personalizada
|
|
1843
2072
|
*/
|
|
@@ -1876,6 +2105,8 @@ var ValidationEngine = class {
|
|
|
1876
2105
|
return "El formato no es v\xE1lido";
|
|
1877
2106
|
case "custom":
|
|
1878
2107
|
return "Validaci\xF3n fallida";
|
|
2108
|
+
case "noInjection":
|
|
2109
|
+
return INJECTION_VALIDATION_MESSAGE;
|
|
1879
2110
|
default:
|
|
1880
2111
|
return "Campo inv\xE1lido";
|
|
1881
2112
|
}
|
|
@@ -2354,6 +2585,11 @@ var StateManager = class {
|
|
|
2354
2585
|
*/
|
|
2355
2586
|
getActiveValidations(field) {
|
|
2356
2587
|
let validations = [...field.validations || []];
|
|
2588
|
+
const textFieldTypes = ["text", "email", "password", "textarea"];
|
|
2589
|
+
const skipInjection = field.skipInjectionValidation ?? field.props?.skipInjectionValidation;
|
|
2590
|
+
if (textFieldTypes.includes(field.type) && !skipInjection && !validations.some((v) => v.type === "noInjection")) {
|
|
2591
|
+
validations = [{ type: "noInjection" }, ...validations];
|
|
2592
|
+
}
|
|
2357
2593
|
const isRequired = this.getFieldRequired(field.name);
|
|
2358
2594
|
const hasRequiredValidation = validations.some((v) => v.type === "required");
|
|
2359
2595
|
if (isRequired && !hasRequiredValidation) {
|
|
@@ -4534,12 +4770,27 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4534
4770
|
super();
|
|
4535
4771
|
this.customComponents = {};
|
|
4536
4772
|
this.isRendering = false;
|
|
4773
|
+
this.attemptsLock = null;
|
|
4774
|
+
this.lockCountdownInterval = null;
|
|
4537
4775
|
this.dependencyRenderTimeout = null;
|
|
4538
4776
|
this.stateManager = new StateManager();
|
|
4539
4777
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
4540
4778
|
}
|
|
4541
4779
|
static get observedAttributes() {
|
|
4542
|
-
return [
|
|
4780
|
+
return [
|
|
4781
|
+
"schema",
|
|
4782
|
+
"template",
|
|
4783
|
+
"template-extend",
|
|
4784
|
+
"theme",
|
|
4785
|
+
"colors",
|
|
4786
|
+
"initialData",
|
|
4787
|
+
"loading",
|
|
4788
|
+
"disabled",
|
|
4789
|
+
"max-attempts",
|
|
4790
|
+
"block-duration-minutes",
|
|
4791
|
+
"attempts-storage-key",
|
|
4792
|
+
"submit-button"
|
|
4793
|
+
];
|
|
4543
4794
|
}
|
|
4544
4795
|
/**
|
|
4545
4796
|
* Obtiene el schema
|
|
@@ -4613,10 +4864,91 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4613
4864
|
this.removeAttribute("template-extend");
|
|
4614
4865
|
}
|
|
4615
4866
|
}
|
|
4867
|
+
/**
|
|
4868
|
+
* Máximo de intentos antes de bloquear (para AttemptsLock)
|
|
4869
|
+
*/
|
|
4870
|
+
get maxAttempts() {
|
|
4871
|
+
const attr = this.getAttribute("max-attempts");
|
|
4872
|
+
if (!attr) return null;
|
|
4873
|
+
const n = parseInt(attr, 10);
|
|
4874
|
+
return isNaN(n) ? null : n;
|
|
4875
|
+
}
|
|
4876
|
+
set maxAttempts(value) {
|
|
4877
|
+
if (value != null && value >= 1) {
|
|
4878
|
+
this.setAttribute("max-attempts", String(value));
|
|
4879
|
+
} else {
|
|
4880
|
+
this.removeAttribute("max-attempts");
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
/**
|
|
4884
|
+
* Duración del bloqueo en minutos (default: 5)
|
|
4885
|
+
*/
|
|
4886
|
+
get blockDurationMinutes() {
|
|
4887
|
+
const attr = this.getAttribute("block-duration-minutes");
|
|
4888
|
+
if (!attr) return null;
|
|
4889
|
+
const n = parseInt(attr, 10);
|
|
4890
|
+
return isNaN(n) ? null : n;
|
|
4891
|
+
}
|
|
4892
|
+
set blockDurationMinutes(value) {
|
|
4893
|
+
if (value != null && value >= 1) {
|
|
4894
|
+
this.setAttribute("block-duration-minutes", String(value));
|
|
4895
|
+
} else {
|
|
4896
|
+
this.removeAttribute("block-duration-minutes");
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
/**
|
|
4900
|
+
* Clave para persistir intentos en sessionStorage
|
|
4901
|
+
*/
|
|
4902
|
+
get attemptsStorageKey() {
|
|
4903
|
+
return this.getAttribute("attempts-storage-key");
|
|
4904
|
+
}
|
|
4905
|
+
set attemptsStorageKey(value) {
|
|
4906
|
+
if (value) {
|
|
4907
|
+
this.setAttribute("attempts-storage-key", value);
|
|
4908
|
+
} else {
|
|
4909
|
+
this.removeAttribute("attempts-storage-key");
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
/**
|
|
4913
|
+
* Configuración del botón de submit (desde atributo o schema)
|
|
4914
|
+
*/
|
|
4915
|
+
get submitButton() {
|
|
4916
|
+
const attr = this.getAttribute("submit-button");
|
|
4917
|
+
if (attr) {
|
|
4918
|
+
try {
|
|
4919
|
+
return parseAttributeValue(attr);
|
|
4920
|
+
} catch {
|
|
4921
|
+
return null;
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
return null;
|
|
4925
|
+
}
|
|
4926
|
+
set submitButton(value) {
|
|
4927
|
+
if (value && typeof value === "object") {
|
|
4928
|
+
this.setAttribute("submit-button", attributeValue(value));
|
|
4929
|
+
} else {
|
|
4930
|
+
this.removeAttribute("submit-button");
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Obtiene la configuración efectiva del botón submit (atributo > schema > defaults)
|
|
4935
|
+
*/
|
|
4936
|
+
getSubmitButtonConfig(schema) {
|
|
4937
|
+
const fromAttr = this.submitButton;
|
|
4938
|
+
const fromSchema = schema?.submitButton;
|
|
4939
|
+
const merged = { ...fromSchema, ...fromAttr };
|
|
4940
|
+
return {
|
|
4941
|
+
visible: merged.visible ?? true,
|
|
4942
|
+
text: merged.text ?? "Enviar",
|
|
4943
|
+
width: merged.width ?? "auto",
|
|
4944
|
+
align: merged.align ?? "left"
|
|
4945
|
+
};
|
|
4946
|
+
}
|
|
4616
4947
|
/**
|
|
4617
4948
|
* Se llama cuando el componente se conecta al DOM
|
|
4618
4949
|
*/
|
|
4619
4950
|
connectedCallback() {
|
|
4951
|
+
this.setupAttemptsLock();
|
|
4620
4952
|
this.setupStyles();
|
|
4621
4953
|
this.render();
|
|
4622
4954
|
}
|
|
@@ -4663,6 +4995,36 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4663
4995
|
if (name === "disabled" && newValue !== oldValue) {
|
|
4664
4996
|
this.render();
|
|
4665
4997
|
}
|
|
4998
|
+
if ((name === "max-attempts" || name === "block-duration-minutes" || name === "attempts-storage-key") && newValue !== oldValue) {
|
|
4999
|
+
this.setupAttemptsLock();
|
|
5000
|
+
this.updateLockOverlay();
|
|
5001
|
+
}
|
|
5002
|
+
if (name === "submit-button" && newValue !== oldValue) {
|
|
5003
|
+
this.render();
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
/**
|
|
5007
|
+
* Configura el AttemptsLock según los atributos actuales
|
|
5008
|
+
*/
|
|
5009
|
+
setupAttemptsLock() {
|
|
5010
|
+
const maxAttempts = this.maxAttempts;
|
|
5011
|
+
if (maxAttempts == null || maxAttempts < 1) {
|
|
5012
|
+
this.attemptsLock = null;
|
|
5013
|
+
return;
|
|
5014
|
+
}
|
|
5015
|
+
this.attemptsLock = new AttemptsLock({
|
|
5016
|
+
maxAttempts,
|
|
5017
|
+
blockDurationMinutes: this.blockDurationMinutes ?? 5,
|
|
5018
|
+
storageKey: this.attemptsStorageKey ?? void 0,
|
|
5019
|
+
onLocked: () => {
|
|
5020
|
+
this.updateLockOverlay();
|
|
5021
|
+
},
|
|
5022
|
+
onUnlocked: () => {
|
|
5023
|
+
this.stopLockCountdown();
|
|
5024
|
+
this.updateLockOverlay();
|
|
5025
|
+
this.render();
|
|
5026
|
+
}
|
|
5027
|
+
});
|
|
4666
5028
|
}
|
|
4667
5029
|
/**
|
|
4668
5030
|
* Maneja el cambio de schema
|
|
@@ -4753,17 +5115,25 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4753
5115
|
newFormElement.classList.add("easy-form-disabled");
|
|
4754
5116
|
}
|
|
4755
5117
|
if (finalWizardState) {
|
|
4756
|
-
this.renderWizard(newFormElement);
|
|
5118
|
+
this.renderWizard(newFormElement, schema);
|
|
4757
5119
|
} else {
|
|
4758
5120
|
this.renderFields(newFormElement, schema.fields || []);
|
|
4759
|
-
const
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
submitButton
|
|
5121
|
+
const submitConfig = this.getSubmitButtonConfig(schema);
|
|
5122
|
+
if (submitConfig.visible) {
|
|
5123
|
+
const submitWrapper = document.createElement("div");
|
|
5124
|
+
submitWrapper.className = "easy-form-submit-wrapper";
|
|
5125
|
+
submitWrapper.style.textAlign = submitConfig.align;
|
|
5126
|
+
const submitButton = document.createElement("button");
|
|
5127
|
+
submitButton.type = "submit";
|
|
5128
|
+
submitButton.textContent = submitConfig.text;
|
|
5129
|
+
submitButton.className = "easy-form-submit";
|
|
5130
|
+
submitButton.style.width = submitConfig.width;
|
|
5131
|
+
if (this.disabled || this.loading) {
|
|
5132
|
+
submitButton.disabled = true;
|
|
5133
|
+
}
|
|
5134
|
+
submitWrapper.appendChild(submitButton);
|
|
5135
|
+
newFormElement.appendChild(submitWrapper);
|
|
4765
5136
|
}
|
|
4766
|
-
newFormElement.appendChild(submitButton);
|
|
4767
5137
|
}
|
|
4768
5138
|
const oldForm = this.shadow.querySelector("form");
|
|
4769
5139
|
if (oldForm && oldForm.parentNode === this.shadow && oldForm !== newFormElement) {
|
|
@@ -4777,10 +5147,50 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4777
5147
|
if (this.loading) {
|
|
4778
5148
|
this.updateLoadingOverlay(newFormElement);
|
|
4779
5149
|
}
|
|
5150
|
+
this.updateLockOverlay(newFormElement);
|
|
4780
5151
|
} finally {
|
|
4781
5152
|
this.isRendering = false;
|
|
4782
5153
|
}
|
|
4783
5154
|
}
|
|
5155
|
+
/**
|
|
5156
|
+
* Actualiza el overlay de bloqueo por intentos
|
|
5157
|
+
*/
|
|
5158
|
+
updateLockOverlay(formElement) {
|
|
5159
|
+
const existingOverlay = this.shadow.querySelector(".easy-form-lock-overlay");
|
|
5160
|
+
if (existingOverlay) {
|
|
5161
|
+
existingOverlay.remove();
|
|
5162
|
+
}
|
|
5163
|
+
this.stopLockCountdown();
|
|
5164
|
+
if (!this.attemptsLock?.isLocked()) return;
|
|
5165
|
+
const form = formElement || this.shadow.querySelector("form");
|
|
5166
|
+
if (!form) return;
|
|
5167
|
+
const overlay = document.createElement("div");
|
|
5168
|
+
overlay.className = "easy-form-lock-overlay";
|
|
5169
|
+
const message = document.createElement("div");
|
|
5170
|
+
message.className = "easy-form-lock-message";
|
|
5171
|
+
message.setAttribute("role", "alert");
|
|
5172
|
+
const updateCountdown = () => {
|
|
5173
|
+
const remainingMs = this.attemptsLock.getRemainingBlockTimeMs();
|
|
5174
|
+
if (remainingMs <= 0) {
|
|
5175
|
+
this.stopLockCountdown();
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
const minutes = Math.floor(remainingMs / 6e4);
|
|
5179
|
+
const seconds = Math.floor(remainingMs % 6e4 / 1e3);
|
|
5180
|
+
const timeStr = minutes > 0 ? `${minutes} min ${seconds} s` : `${seconds} segundos`;
|
|
5181
|
+
message.textContent = `Demasiados intentos. Intenta de nuevo en ${timeStr}.`;
|
|
5182
|
+
};
|
|
5183
|
+
updateCountdown();
|
|
5184
|
+
overlay.appendChild(message);
|
|
5185
|
+
form.appendChild(overlay);
|
|
5186
|
+
this.lockCountdownInterval = setInterval(updateCountdown, 1e3);
|
|
5187
|
+
}
|
|
5188
|
+
stopLockCountdown() {
|
|
5189
|
+
if (this.lockCountdownInterval) {
|
|
5190
|
+
clearInterval(this.lockCountdownInterval);
|
|
5191
|
+
this.lockCountdownInterval = null;
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
4784
5194
|
/**
|
|
4785
5195
|
* Actualiza el overlay de loading sobre el formulario
|
|
4786
5196
|
*/
|
|
@@ -5085,14 +5495,13 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
5085
5495
|
/**
|
|
5086
5496
|
* Renderiza wizard
|
|
5087
5497
|
*/
|
|
5088
|
-
renderWizard(container) {
|
|
5498
|
+
renderWizard(container, schema) {
|
|
5089
5499
|
const wizardState = this.stateManager.getWizardState();
|
|
5090
5500
|
if (!wizardState) return;
|
|
5091
5501
|
const wizardContainer = document.createElement("div");
|
|
5092
5502
|
wizardContainer.className = "easy-form-wizard";
|
|
5093
5503
|
const stepsIndicator = document.createElement("div");
|
|
5094
5504
|
stepsIndicator.className = "easy-form-wizard-steps";
|
|
5095
|
-
const schema = this.schema;
|
|
5096
5505
|
if (schema?.steps) {
|
|
5097
5506
|
for (let i = 0; i < schema.steps.length; i++) {
|
|
5098
5507
|
const stepEl = document.createElement("div");
|
|
@@ -5177,11 +5586,13 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
5177
5586
|
}
|
|
5178
5587
|
navContainer.appendChild(nextButton);
|
|
5179
5588
|
}
|
|
5180
|
-
|
|
5589
|
+
const submitConfig = this.getSubmitButtonConfig(schema);
|
|
5590
|
+
if (wizardState.currentStep === wizardState.totalSteps - 1 && submitConfig.visible) {
|
|
5181
5591
|
const submitButton = document.createElement("button");
|
|
5182
5592
|
submitButton.type = "button";
|
|
5183
|
-
submitButton.textContent =
|
|
5593
|
+
submitButton.textContent = submitConfig.text;
|
|
5184
5594
|
submitButton.className = "easy-form-wizard-next";
|
|
5595
|
+
submitButton.style.width = submitConfig.width;
|
|
5185
5596
|
if (this.disabled || this.loading) {
|
|
5186
5597
|
submitButton.disabled = true;
|
|
5187
5598
|
} else {
|
|
@@ -5337,6 +5748,9 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
5337
5748
|
*/
|
|
5338
5749
|
async handleSubmit(event) {
|
|
5339
5750
|
event.preventDefault();
|
|
5751
|
+
if (this.attemptsLock?.isLocked()) {
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
5340
5754
|
const errors = await this.stateManager.validateForm();
|
|
5341
5755
|
const state = this.stateManager.getState();
|
|
5342
5756
|
if (Object.keys(errors).length > 0) {
|
|
@@ -5402,6 +5816,48 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
5402
5816
|
this.stateManager.reset();
|
|
5403
5817
|
this.render();
|
|
5404
5818
|
}
|
|
5819
|
+
/**
|
|
5820
|
+
* Incrementa el contador de intentos (para bloqueo por intentos fallidos).
|
|
5821
|
+
* El consumidor debe llamar esto cuando la API/login falle.
|
|
5822
|
+
*/
|
|
5823
|
+
incrementAttempts() {
|
|
5824
|
+
this.attemptsLock?.incrementAttempts();
|
|
5825
|
+
if (this.attemptsLock?.isLocked()) {
|
|
5826
|
+
this.updateLockOverlay();
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
/**
|
|
5830
|
+
* Resetea el contador de intentos y desbloquea el formulario.
|
|
5831
|
+
*/
|
|
5832
|
+
resetAttempts() {
|
|
5833
|
+
this.attemptsLock?.reset();
|
|
5834
|
+
this.stopLockCountdown();
|
|
5835
|
+
this.updateLockOverlay();
|
|
5836
|
+
this.render();
|
|
5837
|
+
}
|
|
5838
|
+
/**
|
|
5839
|
+
* Retorna true si el formulario está bloqueado por intentos.
|
|
5840
|
+
*/
|
|
5841
|
+
isLocked() {
|
|
5842
|
+
return this.attemptsLock?.isLocked() ?? false;
|
|
5843
|
+
}
|
|
5844
|
+
/**
|
|
5845
|
+
* Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado.
|
|
5846
|
+
*/
|
|
5847
|
+
getRemainingBlockTimeMs() {
|
|
5848
|
+
return this.attemptsLock?.getRemainingBlockTimeMs() ?? 0;
|
|
5849
|
+
}
|
|
5850
|
+
/**
|
|
5851
|
+
* Dispara el submit del formulario programáticamente.
|
|
5852
|
+
* Útil cuando el botón submit está oculto (visible: false).
|
|
5853
|
+
*/
|
|
5854
|
+
requestSubmit() {
|
|
5855
|
+
const form = this.shadow.querySelector("form");
|
|
5856
|
+
if (form && typeof form.requestSubmit === "function") {
|
|
5857
|
+
;
|
|
5858
|
+
form.requestSubmit();
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5405
5861
|
/**
|
|
5406
5862
|
* Limpia todos los valores del formulario
|
|
5407
5863
|
*/
|
|
@@ -5609,14 +6065,17 @@ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !c
|
|
|
5609
6065
|
customElements.define("easy-form", EasyForm);
|
|
5610
6066
|
}
|
|
5611
6067
|
export {
|
|
6068
|
+
AttemptsLock,
|
|
5612
6069
|
ConditionEngine,
|
|
5613
6070
|
EasyForm,
|
|
6071
|
+
INJECTION_VALIDATION_MESSAGE,
|
|
5614
6072
|
MaskEngine,
|
|
5615
6073
|
PREDEFINED_MASKS,
|
|
5616
6074
|
SchemaParser,
|
|
5617
6075
|
StateManager,
|
|
5618
6076
|
ValidationEngine,
|
|
5619
6077
|
attributeValue,
|
|
6078
|
+
containsInjection,
|
|
5620
6079
|
createInput,
|
|
5621
6080
|
extendTemplate,
|
|
5622
6081
|
generateId,
|
|
@@ -5627,6 +6086,7 @@ export {
|
|
|
5627
6086
|
getPredefinedMask,
|
|
5628
6087
|
getTemplate,
|
|
5629
6088
|
getThemeStyles,
|
|
6089
|
+
isSafeFromInjection,
|
|
5630
6090
|
isValidEmail,
|
|
5631
6091
|
parseAttributeValue,
|
|
5632
6092
|
registerComponent,
|