ctrovalidate-browser 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ctrotech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # ctrovalidate-browser
2
+
3
+ **DOM-first validation for vanilla JavaScript and HTML.**
4
+
5
+ `ctrovalidate-browser` provides a `Ctrovalidate` controller class that bridges [`ctrovalidate-core`](https://www.npmjs.com/package/ctrovalidate-core) validation logic to the DOM. Discover fields via data attributes, manage event listeners, display errors, and handle ARIA accessibility — all without a framework.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/ctrovalidate-browser.svg)](https://www.npmjs.com/package/ctrovalidate-browser)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install ctrovalidate-browser ctrovalidate-core
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Quick Start
21
+
22
+ ### HTML
23
+
24
+ ```html
25
+ <form id="signupForm" novalidate>
26
+ <div>
27
+ <input
28
+ type="email"
29
+ name="email"
30
+ data-ctrovalidate-rules="required|email"
31
+ />
32
+ <div class="error-message"></div>
33
+ </div>
34
+
35
+ <div>
36
+ <input
37
+ type="password"
38
+ name="password"
39
+ data-ctrovalidate-rules="required|minLength:8|strongPassword"
40
+ />
41
+ <div class="error-message"></div>
42
+ </div>
43
+
44
+ <button type="submit">Sign Up</button>
45
+ </form>
46
+ ```
47
+
48
+ ### JavaScript
49
+
50
+ ```javascript
51
+ import { Ctrovalidate } from 'ctrovalidate-browser';
52
+
53
+ const form = document.querySelector('#signupForm');
54
+ const validator = new Ctrovalidate(form, {
55
+ errorClass: 'is-invalid',
56
+ errorMessageClass: 'error-message',
57
+ });
58
+
59
+ form.addEventListener('submit', async (e) => {
60
+ e.preventDefault();
61
+ const isValid = await validator.validate();
62
+ if (isValid) {
63
+ // Submit
64
+ }
65
+ });
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Data Attributes
71
+
72
+ | Attribute | Description |
73
+ |-----------|-------------|
74
+ | `data-ctrovalidate-rules` | Pipe-delimited rule string (e.g. `required\|email\|minLength:8`) |
75
+ | `data-ctrovalidate-if` | Conditional validation dependency (e.g. `agreeTerms:checked`, `country:value=USA`) |
76
+ | `data-ctrovalidate-message` | Catch-all error message for the field |
77
+ | `data-ctrovalidate-{ruleName}-message` | Rule-specific error message |
78
+
79
+ ---
80
+
81
+ ## Configuration
82
+
83
+ ```javascript
84
+ const validator = new Ctrovalidate(form, {
85
+ realTime: true, // Enable blur/input listeners
86
+ errorClass: 'is-invalid border-red-500', // Supports Tailwind space-separated
87
+ errorMessageClass: 'error-message', // Also space-separated
88
+ pendingClass: 'is-validating', // Added during async validation
89
+ logLevel: Ctrovalidate.LogLevel.NONE, // Logging verbosity
90
+ schema: { email: 'required|email' }, // Programmatic rules (merged with HTML)
91
+ aliases: { username: 'required|alphaDash|minLength:3' }, // Instance aliases
92
+ });
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Instance Methods
98
+
99
+ ```javascript
100
+ await validator.validate(); // Validate all fields
101
+ validator.addField(element); // Register a field
102
+ validator.removeField(element); // Unregister a field
103
+ validator.refresh(); // Re-discover fields
104
+ validator.isDirty('email'); // Check if field was interacted with
105
+ validator.getError('email'); // Get current error message
106
+ validator.reset(); // Clear all validation state
107
+ validator.destroy(); // Full cleanup
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Static Methods
113
+
114
+ ```javascript
115
+ Ctrovalidate.defineAlias('password', 'required|minLength:8|strongPassword');
116
+ Ctrovalidate.addRule('isEven', (v) => Number(v) % 2 === 0, 'Must be even.');
117
+ Ctrovalidate.addAsyncRule('checkEmail', async (v, p, el, signal) => { ... });
118
+ Ctrovalidate.setCustomMessages({ required: 'Cannot be empty.' });
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Accessibility
124
+
125
+ The controller automatically manages:
126
+
127
+ - `aria-invalid="true"` on validation failure
128
+ - `aria-describedby` linking to the error container
129
+ - `role="status"` and `aria-live="polite"` on fallback error containers
130
+
131
+ ---
132
+
133
+ ## Related Packages
134
+
135
+ - **[ctrovalidate-core](https://www.npmjs.com/package/ctrovalidate-core)** — Validation engine
136
+ - **[ctrovalidate-react](https://www.npmjs.com/package/ctrovalidate-react)** — React hook
137
+ - **[ctrovalidate-vue](https://www.npmjs.com/package/ctrovalidate-vue)** — Vue composable
138
+ - **[ctrovalidate-svelte](https://www.npmjs.com/package/ctrovalidate-svelte)** — Svelte stores
139
+ - **[ctrovalidate-next](https://www.npmjs.com/package/ctrovalidate-next)** — Next.js server actions
140
+
141
+ ---
142
+
143
+ ## License
144
+
145
+ MIT © [Ctrotech](https://github.com/ctrotech-tutor)
146
+
147
+ Full documentation: https://ctrovalidate.vercel.app
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var Ke=Object.defineProperty;var oe=t=>{throw TypeError(t)};var Qe=(t,e,r)=>e in t?Ke(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var de=(t,e,r)=>Qe(t,typeof e!="symbol"?e+"":e,r),j=(t,e,r)=>e.has(t)||oe("Cannot "+r);var s=(t,e,r)=>(j(t,e,"read from private field"),r?r.call(t):e.get(t)),o=(t,e,r)=>e.has(t)?oe("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),c=(t,e,r,a)=>(j(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),v=(t,e,r)=>(j(t,e,"access private method"),r);var ce=(t,e,r,a)=>({set _(i){c(t,e,i,r)},get _(){return s(t,e,a)}});Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});var ve=Object.defineProperty,be=t=>{throw TypeError(t)},Xe=(t,e,r)=>e in t?ve(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,Ye=(t,e)=>{for(var r in e)ve(t,r,{get:e[r],enumerable:!0})},ie=(t,e,r)=>Xe(t,typeof e!="symbol"?e+"":e,r),ye=(t,e,r)=>e.has(t)||be("Cannot "+r),N=(t,e,r)=>(ye(t,e,"read from private field"),r?r.call(t):e.get(t)),ue=(t,e,r)=>e.has(t)?be("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),he=(t,e,r,a)=>(ye(t,e,"write to private field"),e.set(t,r),r);function fe(t){if(!t||typeof t!="string")return[];const e=[],r=t.split("|");for(const a of r){const i=a.trim();if(i==="")continue;const l=i.indexOf(":");if(l===-1)e.push({name:i,params:[]});else{const n=i.substring(0,l).trim(),d=i.substring(l+1).trim();if(n){const u=d.split(",").map(g=>g.trim());e.push({name:n,params:u})}}}return e}function ee(t,e={},r=new Set){if(!t)return[];let a;if(typeof t=="string")a=fe(t);else if(Array.isArray(t))a=t;else return[];return a.flatMap(i=>{const l=typeof i=="string"?i.split(":")[0]:i.name;if(e[l]&&!r.has(l)){const n=new Set(r);return n.add(l),ee(e[l],e,n)}return typeof i=="string"?fe(i):[i]})}var J=(t=>(t[t.NONE=0]="NONE",t[t.ERROR=1]="ERROR",t[t.WARN=2]="WARN",t[t.INFO=3]="INFO",t[t.DEBUG=4]="DEBUG",t))(J||{}),ne=class Ee{constructor(e=0){ie(this,"level"),this.level=e}setLevel(e){this.level=e}static setLevel(e){this.globalLevel=e}format(e){return`${Ee.prefix} ${e}`}error(e,...r){this.level>=1&&console.error(this.format(e),...r)}warn(e,...r){this.level>=2&&console.warn(this.format(e),...r)}info(e,...r){this.level>=3&&console.info(this.format(e),...r)}debug(e,...r){this.level>=4&&console.debug(this.format(e),...r)}static error(e,...r){this.globalLevel>=1&&console.error(`${this.prefix} ${e}`,...r)}static warn(e,...r){this.globalLevel>=2&&console.warn(`${this.prefix} ${e}`,...r)}static info(e,...r){this.globalLevel>=3&&console.info(`${this.prefix} ${e}`,...r)}static debug(e,...r){this.globalLevel>=4&&console.debug(`${this.prefix} ${e}`,...r)}};ie(ne,"globalLevel",1);ie(ne,"prefix","[Ctrovalidate]");var je=ne,et={};Ye(et,{alpha:()=>xe,alphaDash:()=>$e,alphaNum:()=>Re,alphaSpaces:()=>nt,between:()=>Ve,creditCard:()=>qe,decimal:()=>Te,email:()=>Ae,exactLength:()=>ke,integer:()=>Me,ipAddress:()=>_e,json:()=>Ie,max:()=>we,maxLength:()=>Ne,min:()=>Ce,minLength:()=>Se,numeric:()=>Le,phone:()=>De,required:()=>Fe,sameAs:()=>Be,strongPassword:()=>ze,url:()=>Pe});var Fe=t=>t==null?!1:typeof t=="boolean"?t:String(t).trim()!=="",tt=/^[^\s@]+@[^\s@]+\.[^\s@]+$/,Ae=t=>t?tt.test(String(t)):!0,Ce=(t,e=[])=>{if(t==null||t==="")return!0;if(!e[0])return console.error("[Ctrovalidate] Missing parameter for 'min' rule."),!1;const r=Number(e[0]);return Number(t)>=r},we=(t,e=[])=>{if(t==null||t==="")return!0;if(!e[0])return console.error("[Ctrovalidate] Missing parameter for 'max' rule."),!1;const r=Number(e[0]);return Number(t)<=r},Se=(t,e=[])=>{if(t==null||t==="")return!0;if(!e[0])return console.error("[Ctrovalidate] Missing parameter for 'minLength' rule."),!1;const r=Number(e[0]);return String(t).length>=r},Ne=(t,e=[])=>{if(t==null||t==="")return!0;if(!e[0])return console.error("[Ctrovalidate] Missing parameter for 'maxLength' rule."),!1;const r=Number(e[0]);return String(t).length<=r},rt=/^[a-zA-Z]+$/,xe=t=>t?rt.test(String(t)):!0,st=/^[a-zA-Z0-9]+$/,Re=t=>t?st.test(String(t)):!0,at=/^[a-zA-Z0-9-_]+$/,$e=t=>t?at.test(String(t)):!0,it=/^[a-zA-Z\s]+$/,nt=t=>t?it.test(String(t)):!0,Le=t=>!t&&t!==0?!0:!isNaN(Number(t))&&!isNaN(parseFloat(String(t))),lt=/^-?\d+$/,Me=t=>!t&&t!==0?!0:lt.test(String(t)),ot=/^-?\d+(\.\d+)?$/,Te=t=>!t&&t!==0?!0:ot.test(String(t)),dt=/^(https?:\/\/)([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,Pe=t=>t?dt.test(String(t)):!0,ct=/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,ut=/^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/,_e=t=>{if(!t)return!0;const e=String(t);return ct.test(e)||ut.test(e)},Ie=t=>{if(!t)return!0;try{const e=JSON.parse(String(t));return typeof e=="object"&&e!==null}catch{return!1}},ht=/^[+]?([(]?[0-9]{1,4}[)]?)[-\s.]?[0-9]{3,4}[-\s.]?[0-9]{4,6}$/,De=t=>t?ht.test(String(t)):!0,qe=t=>{if(!t)return!0;const e=String(t).replace(/[- ]/g,"");if(!/^\d+$/.test(e))return!1;let r=0;const a=e.length%2;for(let i=0;i<e.length;i++){let l=parseInt(e[i],10);i%2===a&&(l*=2,l>9&&(l-=9)),r+=l}return r%10===0},ft=/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,ze=t=>t?ft.test(String(t)):!0,ke=(t,e=[])=>{if(t==null||t==="")return!0;if(!e[0])return console.error("[Ctrovalidate] Missing parameter for 'exactLength' rule."),!1;const r=Number(e[0]);return String(t).length===r},Ve=(t,e=[])=>{if(t==null||t==="")return!0;if(e.length<2)return console.error("[Ctrovalidate] Missing parameters for 'between' rule."),!1;const r=Number(e[0]),a=Number(e[1]);if(isNaN(r)||isNaN(a))return!1;if(!isNaN(Number(t))&&typeof t!="string"){const n=Number(t);return n>=r&&n<=a}const i=String(t);if(!isNaN(Number(i))&&i!==""){const n=Number(i);return n>=r&&n<=a}const l=i.length;return l>=r&&l<=a},Be=(t,e=[])=>{if(e.length===0||e[0]===void 0)return console.error("[Ctrovalidate] Missing parameter for 'sameAs' rule."),!1;const r=e[0];return t===r},mt={required:"This field is required.",email:"Please enter a valid email address.",min:"Minimum value is {0}.",max:"Maximum value is {0}.",minLength:"Minimum length is {0} characters.",maxLength:"Maximum length is {0} characters.",alpha:"This field may only contain alphabetic characters.",alphaNum:"This field may only contain alpha-numeric characters.",alphaDash:"This field may only contain alpha-numeric characters as well as dashes and underscores.",alphaSpaces:"This field may only contain alphabetic characters and spaces.",numeric:"This field must be a number.",integer:"This field must be an integer.",decimal:"This field must be a decimal number.",url:"Please enter a valid URL.",ipAddress:"Please enter a valid IP address.",json:"Please enter a valid JSON string.",phone:"Please enter a valid phone number.",creditCard:"Please enter a valid credit card number.",strongPassword:"Password must be at least 8 characters long and include an uppercase letter, a lowercase letter, a number, and a symbol.",exactLength:"This field must be exactly {0} characters long.",between:"This field must be between {0} and {1}.",sameAs:"This field must match {0}."},x,I,gt=class{constructor(){ue(this,x,{en:mt}),ue(this,I,"en")}setLocale(t){if(!N(this,x)[t]){console.warn(`[Ctrovalidate] Locale "${t}" not found. Falling back to "en".`),he(this,I,"en");return}he(this,I,t)}addMessages(t,e){N(this,x)[t]={...N(this,x)[t]||{},...e}}translate(t,e=[],r){const a=r||N(this,I),i=N(this,x)[a]||N(this,x).en;return(i[t]||i.default||"Invalid input.").replace(/{(\d+)}/g,(n,d)=>{const u=parseInt(d,10);return e[u]!==void 0?String(e[u]):n})}get currentLocale(){return N(this,I)}};x=new WeakMap;I=new WeakMap;new gt;var D,B,q,R,U,K,C,We,re,se;const Q=class Q{constructor({errorClass:e="ctrovalidate-error",errorMessageClass:r="ctrovalidate-error-message",pendingClass:a="ctrovalidate-pending"}){o(this,C);o(this,D);o(this,B);o(this,q);o(this,R,new WeakMap);o(this,U,new WeakMap);c(this,D,e),c(this,B,r),c(this,q,a)}displayError(e,r){const a=v(this,C,se).call(this,e);s(this,D).split(/\s+/).filter(Boolean).forEach(i=>e.classList.add(i)),e.setAttribute("aria-invalid","true"),a&&(a.textContent=r,a.style.display="",e.setAttribute("aria-describedby",a.id))}clearError(e){const r=v(this,C,se).call(this,e);s(this,D).split(/\s+/).filter(Boolean).forEach(a=>e.classList.remove(a)),e.removeAttribute("aria-invalid"),r&&(r.textContent="",r.style.display="none",e.getAttribute("aria-describedby")===r.id&&e.removeAttribute("aria-describedby"))}showPending(e){this.clearError(e),s(this,q).split(/\s+/).filter(Boolean).forEach(r=>e.classList.add(r))}hidePending(e){s(this,q).split(/\s+/).filter(Boolean).forEach(r=>e.classList.remove(r))}};D=new WeakMap,B=new WeakMap,q=new WeakMap,R=new WeakMap,U=new WeakMap,K=new WeakMap,C=new WeakSet,We=function(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~[\]])/g,"\\$1")},re=function(e){let r;if((e instanceof HTMLInputElement||e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement)&&(r=e.name),r&&r.trim().length>0)return`ctrovalidate-error-${r.replace(/\s+/g,"-").replace(/[^A-Za-z0-9\-_:.]/g,"")}`;const a=`ctrovalidate-error-${ce(Q,K)._++}`;return s(this,U).set(e,a),a},se=function(e){if(s(this,R).has(e))return s(this,R).get(e)||null;const r=s(this,B).split(/\s+/).filter(Boolean);let a=e.parentElement,i=0;const l=3;for(;a&&i<l;){for(const g of r){const S=`.${v(this,C,We).call(this,g)}`,f=a.querySelector(S);if(f)return f.id||(f.id=v(this,C,re).call(this,e)),s(this,R).set(e,f),f}a=a.parentElement,i++}const n=e.parentElement;if(!n)return null;const d=document.createElement("div");d.className=r.join(" "),d.style.display="none",d.role="status",d.setAttribute("aria-live","polite");const u=v(this,C,re).call(this,e);d.id=u;try{e.insertAdjacentElement("afterend",d)}catch{n.appendChild(d)}return s(this,R).set(e,d),d},o(Q,K,0);let te=Q;var b,w,W,Z,H,O,X,Ze;class pt{constructor(e,{logger:r,uiManager:a,rules:i,asyncRules:l,messages:n}){o(this,X);o(this,b);o(this,w);o(this,W);o(this,Z);o(this,H);o(this,O);c(this,W,e),c(this,b,r),c(this,w,a),c(this,Z,i),c(this,H,l),c(this,O,n)}async validateField(e){const{element:r,rules:a,state:i,dependency:l}=e;if(!v(this,X,Ze).call(this,l))return s(this,b).debug(`[RuleEngine] Dependency for "${r.name}" not met. Skipping validation.`),s(this,w).clearError(r),e.state.lastError=null,!0;const n=r,d=n.type==="checkbox"?n.checked:n.value;i.abortController&&(i.abortController.abort(),s(this,b).debug(`[RuleEngine] Aborted previous validation for "${n.name}".`));for(const u of a){const g=s(this,Z)[u.name],S=s(this,H)[u.name];if(!g&&!S){s(this,b).warn(`[RuleEngine] Unknown rule "${u.name}" used on field:`,r);continue}let f=!1;if(g)f=g(d,u.params,r);else if(S){i.abortController=new AbortController,s(this,w).showPending(r);try{f=await S(d,u.params,r,i.abortController.signal)}catch(p){if(p instanceof Error&&p.name==="AbortError")return s(this,b).debug(`[RuleEngine] Async validation for "${n.name}" was successfully aborted.`),!0;s(this,b).error(`[RuleEngine] Async rule "${u.name}" threw an error.`,[p]),f=!1}finally{s(this,w).hidePending(r),i.abortController=null}}if(!f){let p=s(this,O)[u.name]||"Invalid input.";e.customMessages&&(e.customMessages[u.name]?p=e.customMessages[u.name]:e.customMessages["*"]&&(p=e.customMessages["*"]));const _=p.replace(/{(\d+)}/g,(Je,Ue)=>{const le=parseInt(Ue,10);return u.params[le]!==void 0?String(u.params[le]):Je});return s(this,w).displayError(r,_),e.state.lastError=_,s(this,b).debug(`[RuleEngine] Field validation failed for "${n.name}" on rule "${u.name}".`),!1}}return s(this,w).clearError(r),e.state.lastError=null,s(this,b).debug(`[RuleEngine] Field validation succeeded for "${n.name}".`),!0}}b=new WeakMap,w=new WeakMap,W=new WeakMap,Z=new WeakMap,H=new WeakMap,O=new WeakMap,X=new WeakSet,Ze=function(e){if(!e)return!0;const r=s(this,W).querySelector(`[name="${e.controllerName}"]`);if(!r)return!1;switch(e.type){case"checked":return r.checked;case"value":return r.value===e.value;case"present":return!!r.value;default:return!1}};var $,y,E,m,z,k,L,P,ae,He;class vt{constructor(e,{logger:r,schema:a={},aliases:i={},validationHandler:l}){o(this,P);o(this,$);o(this,y,[]);o(this,E,new Map);o(this,m);o(this,z);o(this,k);o(this,L);c(this,$,e),c(this,m,r),c(this,z,a),c(this,k,i),c(this,L,l)}discoverFields(){c(this,y,[]),s(this,E).clear();const e=s(this,$).querySelectorAll("[data-ctrovalidate-rules]");s(this,m).debug("[FormController] Discovered "+e.length+" fields to validate."),e.forEach(r=>this.addField(r,!1)),Object.keys(s(this,z)).forEach(r=>{const a=s(this,$).querySelector(`[name="${r}"]`);a&&!s(this,E).has(a)&&this.addField(a,!1)})}addField(e,r=!0){if(!e||s(this,E).has(e))return;const a=e.getAttribute("data-ctrovalidate-rules"),i=e.getAttribute("data-ctrovalidate-if"),l=e.name,n=ee(a||"",s(this,k)),d=l?ee(s(this,z)[l]||[],s(this,k)):[],u=[...n,...d];if(u.length===0&&!i)return;const g={},S=e.getAttribute("data-ctrovalidate-message");S&&(g["*"]=S);for(const p of Array.from(e.attributes))if(p.name.startsWith("data-ctrovalidate-")&&p.name.endsWith("-message")){const _=p.name.replace("data-ctrovalidate-","").replace("-message","");_!==""&&_!=="message"&&(g[_]=p.value)}const f={element:e,rules:u,dependency:v(this,P,He).call(this,i),customMessages:Object.keys(g).length>0?g:void 0,state:{isDirty:!1,abortController:null,lastError:null}};s(this,y).push(f),s(this,E).set(e,f),s(this,m).debug("[FormController] Added field:",e),r&&v(this,P,ae).call(this,f)}removeField(e){if(!e||!s(this,E).has(e))return;const r=s(this,E).get(e);if(c(this,y,s(this,y).filter(a=>a.element!==e)),r.listeners){const{onBlur:a,onInput:i,onControllerInput:l,controllerElement:n}=r.listeners;try{e.removeEventListener("blur",a),e.removeEventListener("input",i),n&&l&&n.removeEventListener("input",l)}catch(d){s(this,m).debug("[FormController] Error while removing listeners for",e,d)}delete r.listeners}s(this,E).delete(e),s(this,m).debug("[FormController] Removed field:",e)}attachEventListeners(e){if(!e){s(this,m).info("[FormController] Real-time validation is disabled.");return}s(this,m).info("[FormController] Real-time validation is enabled. Attaching listeners."),s(this,y).forEach(r=>v(this,P,ae).call(this,r))}reset(){s(this,y).forEach(e=>{e.state.isDirty=!1,e.state.lastError=null})}destroy(){[...s(this,y)].forEach(e=>{this.removeField(e.element)})}getFields(){return s(this,y)}}$=new WeakMap,y=new WeakMap,E=new WeakMap,m=new WeakMap,z=new WeakMap,k=new WeakMap,L=new WeakMap,P=new WeakSet,ae=function(e){const{element:r,dependency:a}=e;if(e.listeners)return;const i=()=>{s(this,m).debug(`[FormController] Blur event on "${r.name}". Validating.`),s(this,L).call(this,e),e.state.isDirty=!0},l=()=>{e.state.isDirty&&(s(this,m).debug(`[FormController] Input event on dirty field "${r.name}". Validating.`),s(this,L).call(this,e))};r.addEventListener("blur",i),r.addEventListener("input",l);let n=null,d=null;a&&(n=s(this,$).querySelector(`[name="${a.controllerName}"]`),n?(d=()=>{s(this,m).debug(`[FormController] Controller "${n.name}" changed. Re-validating dependent field "${r.name}".`),s(this,L).call(this,e)},n.addEventListener("input",d)):s(this,m).warn(`[FormController] Could not find controller field with name "${a.controllerName}".`)),e.listeners={onBlur:i,onInput:l,onControllerInput:d,controllerElement:n}},He=function(e){if(!e)return null;const r=e.split(":"),a=r[0],i=r[1]||"present";if(!a)return null;if(i.includes("=")){const[l,n]=i.split("=");return{controllerName:a,type:l,value:n}}else return{controllerName:a,type:i}};const bt=(t,e)=>{const r=t;if(!r.form)return"";const a=r.form.querySelector(`[name="${e}"]`);return a?a.value:""},yt=(t,e=[],r=null)=>{const a=String(e[0]||"");if(a===void 0)return console.error("[Ctrovalidate] Missing parameter for 'sameAs' rule."),!1;if(!r)return!1;const i=bt(r,a);return Be(t,[i])},Et=Object.freeze(Object.defineProperty({__proto__:null,alpha:xe,alphaDash:$e,alphaNum:Re,between:Ve,creditCard:qe,decimal:Te,email:Ae,exactLength:ke,integer:Me,ipAddress:_e,json:Ie,max:we,maxLength:Ne,min:Ce,minLength:Se,numeric:Le,phone:De,required:Fe,sameAs:yt,strongPassword:ze,url:Pe},Symbol.toStringTag,{value:"Module"})),me={...Et},ge={},pe={},Ft={required:"This field is required.",minLength:"This field must be at least {0} characters long.",maxLength:"This field cannot be longer than {0} characters.",between:"This field must be between {0} and {1} characters long.",email:"Please enter a valid email address.",phone:"Please enter a valid phone number.",url:"Please enter a valid URL.",numeric:"This field must contain only numbers.",alpha:"This field must contain only letters.",alphaDash:"This field must contain only letters, numbers, and dashes.",alphaSpaces:"This field must contain only letters and spaces.",alphaNum:"This field must contain only letters and numbers.",decimal:"This field must be a valid decimal number.",integer:"This field must be a valid integer.",min:"This field must be at least {0}.",max:"This field cannot be more than {0}.",exactLength:"This field must be exactly {0} characters long.",format:"This field format is invalid.",sameAs:"This field must match {0}.",strongPassword:"Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.",creditCard:"Please enter a valid credit card number.",ipAddress:"Please enter a valid IP address.",json:"Please enter a valid JSON string."},G={...Ft};function At(t){Object.assign(G,t)}var M,F,A,T,V,h,Y,Ge;class Oe{constructor(e,r={}){o(this,Y);o(this,M);o(this,F);o(this,A);o(this,T);o(this,V);o(this,h);if(!(e instanceof HTMLFormElement))throw new Error("Ctrovalidate requires a valid HTMLFormElement to be initialized.");c(this,M,e),c(this,F,{logLevel:J.NONE,errorClass:"is-invalid",errorMessageClass:"error-message",pendingClass:"ctrovalidate-pending",realTime:!0,schema:{},aliases:{},...r}),c(this,A,new je(s(this,F).logLevel)),c(this,T,new te(s(this,F))),c(this,V,new pt(s(this,M),{logger:s(this,A),uiManager:s(this,T),rules:me,asyncRules:ge,messages:G})),c(this,h,new vt(s(this,M),{logger:s(this,A),schema:s(this,F).schema,aliases:{...pe,...s(this,F).aliases},validationHandler:a=>s(this,V).validateField(a)})),v(this,Y,Ge).call(this)}static defineAlias(e,r){pe[e]=r}async validate(){return this.validateForm()}async validateForm(){s(this,A).info("[Ctrovalidate] Manually triggering form validation.");const e=s(this,h).getFields(),a=(await Promise.all(e.map(i=>s(this,V).validateField(i)))).every(i=>i===!0);return a?s(this,A).info("[Ctrovalidate] Form validation succeeded."):s(this,A).warn("[Ctrovalidate] Form validation failed."),a}static setCustomMessages(e){At(e)}static addRule(e,r,a){if(!e||typeof r!="function"){console.error("[Ctrovalidate] addRule requires a rule name and a logic function.");return}me[e]=r,a&&(G[e]=a)}static addAsyncRule(e,r,a){if(!e||typeof r!="function"){console.error("[Ctrovalidate] addAsyncRule requires a rule name and a logic function.");return}ge[e]=r,a&&(G[e]=a)}addField(e){s(this,h).addField(e)}removeField(e){s(this,h).removeField(e)}refresh(){s(this,h).discoverFields(),s(this,h).attachEventListeners(s(this,F).realTime)}getError(e){const r=s(this,h).getFields().find(a=>a.element.name===e);return r?r.state.lastError:null}isDirty(e){const r=s(this,h).getFields().find(a=>a.element.name===e);return r?r.state.isDirty:!1}reset(){s(this,h).reset(),s(this,h).getFields().forEach(e=>{s(this,T).clearError(e.element)})}destroy(){s(this,h).destroy(),s(this,h).getFields().forEach(e=>{s(this,T).clearError(e.element)})}}M=new WeakMap,F=new WeakMap,A=new WeakMap,T=new WeakMap,V=new WeakMap,h=new WeakMap,Y=new WeakSet,Ge=function(){s(this,A).info("[Ctrovalidate] Initializing library."),s(this,h).discoverFields(),s(this,h).attachEventListeners(s(this,F).realTime),s(this,M).setAttribute("novalidate","true")},de(Oe,"LogLevel",J);exports.Ctrovalidate=Oe;exports.LogLevel=J;
@@ -0,0 +1,137 @@
1
+ import { AsyncRuleLogic as AsyncRuleLogic_2 } from 'ctrovalidate-core';
2
+ import { DependencyDefinition } from 'ctrovalidate-core';
3
+ import { LogLevel } from 'ctrovalidate-core';
4
+ import { RuleDefinition } from 'ctrovalidate-core';
5
+ import { RuleLogic as RuleLogic_2 } from 'ctrovalidate-core';
6
+ import { SchemaRule } from 'ctrovalidate-core';
7
+ import { ValidationResult } from 'ctrovalidate-core';
8
+ import { ValidationSchema } from 'ctrovalidate-core';
9
+
10
+ /**
11
+ * The logic function for an asynchronous validation rule (Browser context).
12
+ */
13
+ export declare type AsyncRuleLogic = AsyncRuleLogic_2<HTMLElement>;
14
+
15
+ /**
16
+ * The main public-facing class for the Ctrovalidate library.
17
+ */
18
+ export declare class Ctrovalidate {
19
+ #private;
20
+ static LogLevel: typeof LogLevel;
21
+ /**
22
+ * @param formElement - The HTML form element to validate.
23
+ * @param options - Custom configuration options.
24
+ */
25
+ constructor(formElement: HTMLFormElement, options?: CtrovalidateOptions);
26
+ /**
27
+ * Defines a rule alias (macro) globally.
28
+ */
29
+ static defineAlias(name: string, rules: SchemaRule): void;
30
+ /**
31
+ * Validates the entire form programmatically.
32
+ */
33
+ validate(): Promise<boolean>;
34
+ /**
35
+ * Validates the entire form programmatically.
36
+ */
37
+ validateForm(): Promise<boolean>;
38
+ /**
39
+ * Allows the user to add custom validation messages globally.
40
+ */
41
+ static setCustomMessages(customMessages: Record<string, string>): void;
42
+ /**
43
+ * Adds a custom synchronous validation rule globally.
44
+ */
45
+ static addRule(name: string, logic: RuleLogic, message?: string): void;
46
+ /**
47
+ * Adds a custom asynchronous validation rule globally.
48
+ */
49
+ static addAsyncRule(name: string, logic: AsyncRuleLogic, message?: string): void;
50
+ /**
51
+ * Manually adds a new field to be validated.
52
+ */
53
+ addField(element: HTMLElement): void;
54
+ /**
55
+ * Manually removes a field from validation.
56
+ */
57
+ removeField(element: HTMLElement): void;
58
+ /**
59
+ * Refreshes the discovered fields. Use if fields are added or removed from the DOM.
60
+ */
61
+ refresh(): void;
62
+ /**
63
+ * Returns the current error for a field by its name.
64
+ */
65
+ getError(fieldName: string): string | null;
66
+ /**
67
+ * Checks if a field has been interacted with.
68
+ */
69
+ isDirty(fieldName: string): boolean;
70
+ /**
71
+ * Resets the validation state of the form.
72
+ */
73
+ reset(): void;
74
+ /**
75
+ * Fully cleans up the validator instance.
76
+ */
77
+ destroy(): void;
78
+ }
79
+
80
+ /**
81
+ * Configuration options for a Ctrovalidate instance.
82
+ */
83
+ export declare interface CtrovalidateOptions {
84
+ logLevel?: number;
85
+ errorClass?: string;
86
+ errorMessageClass?: string;
87
+ pendingClass?: string;
88
+ realTime?: boolean;
89
+ schema?: ValidationSchema;
90
+ aliases?: Record<string, SchemaRule>;
91
+ }
92
+
93
+ export { DependencyDefinition }
94
+
95
+ /**
96
+ * The object representing a field being validated.
97
+ */
98
+ export declare interface FieldObject {
99
+ element: HTMLElement;
100
+ rules: RuleDefinition[];
101
+ state: FieldState;
102
+ dependency: DependencyDefinition | null;
103
+ customMessages?: Record<string, string>;
104
+ listeners?: {
105
+ onBlur: (e: Event) => void;
106
+ onInput: (e: Event) => void;
107
+ onControllerInput: ((e: Event) => void) | null;
108
+ controllerElement: HTMLElement | null;
109
+ };
110
+ }
111
+
112
+ /**
113
+ * The state of a single validatable field.
114
+ */
115
+ export declare interface FieldState {
116
+ isDirty: boolean;
117
+ abortController: AbortController | null;
118
+ lastError: string | null;
119
+ }
120
+
121
+ export { LogLevel }
122
+
123
+ export { RuleDefinition }
124
+
125
+ /**
126
+ * The logic function for a synchronous validation rule (Browser context).
127
+ * Context is strictly HTMLElement here.
128
+ */
129
+ export declare type RuleLogic = RuleLogic_2<HTMLElement>;
130
+
131
+ export { SchemaRule }
132
+
133
+ export { ValidationResult }
134
+
135
+ export { ValidationSchema }
136
+
137
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,861 @@
1
+ var Ue = Object.defineProperty;
2
+ var oe = (t) => {
3
+ throw TypeError(t);
4
+ };
5
+ var Ke = (t, e, r) => e in t ? Ue(t, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : t[e] = r;
6
+ var de = (t, e, r) => Ke(t, typeof e != "symbol" ? e + "" : e, r), Y = (t, e, r) => e.has(t) || oe("Cannot " + r);
7
+ var s = (t, e, r) => (Y(t, e, "read from private field"), r ? r.call(t) : e.get(t)), o = (t, e, r) => e.has(t) ? oe("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, r), c = (t, e, r, a) => (Y(t, e, "write to private field"), a ? a.call(t, r) : e.set(t, r), r), v = (t, e, r) => (Y(t, e, "access private method"), r);
8
+ var ce = (t, e, r, a) => ({
9
+ set _(i) {
10
+ c(t, e, i, r);
11
+ },
12
+ get _() {
13
+ return s(t, e, a);
14
+ }
15
+ });
16
+ var ve = Object.defineProperty, be = (t) => {
17
+ throw TypeError(t);
18
+ }, Qe = (t, e, r) => e in t ? ve(t, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : t[e] = r, Xe = (t, e) => {
19
+ for (var r in e)
20
+ ve(t, r, { get: e[r], enumerable: !0 });
21
+ }, ie = (t, e, r) => Qe(t, typeof e != "symbol" ? e + "" : e, r), ye = (t, e, r) => e.has(t) || be("Cannot " + r), S = (t, e, r) => (ye(t, e, "read from private field"), r ? r.call(t) : e.get(t)), ue = (t, e, r) => e.has(t) ? be("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, r), he = (t, e, r, a) => (ye(t, e, "write to private field"), e.set(t, r), r);
22
+ function fe(t) {
23
+ if (!t || typeof t != "string")
24
+ return [];
25
+ const e = [], r = t.split("|");
26
+ for (const a of r) {
27
+ const i = a.trim();
28
+ if (i === "")
29
+ continue;
30
+ const l = i.indexOf(":");
31
+ if (l === -1)
32
+ e.push({ name: i, params: [] });
33
+ else {
34
+ const n = i.substring(0, l).trim(), d = i.substring(l + 1).trim();
35
+ if (n) {
36
+ const u = d.split(",").map((g) => g.trim());
37
+ e.push({ name: n, params: u });
38
+ }
39
+ }
40
+ }
41
+ return e;
42
+ }
43
+ function j(t, e = {}, r = /* @__PURE__ */ new Set()) {
44
+ if (!t) return [];
45
+ let a;
46
+ if (typeof t == "string")
47
+ a = fe(t);
48
+ else if (Array.isArray(t))
49
+ a = t;
50
+ else
51
+ return [];
52
+ return a.flatMap((i) => {
53
+ const l = typeof i == "string" ? i.split(":")[0] : i.name;
54
+ if (e[l] && !r.has(l)) {
55
+ const n = new Set(r);
56
+ return n.add(l), j(e[l], e, n);
57
+ }
58
+ return typeof i == "string" ? fe(i) : [i];
59
+ });
60
+ }
61
+ var ee = /* @__PURE__ */ ((t) => (t[t.NONE = 0] = "NONE", t[t.ERROR = 1] = "ERROR", t[t.WARN = 2] = "WARN", t[t.INFO = 3] = "INFO", t[t.DEBUG = 4] = "DEBUG", t))(ee || {}), ne = class Ee {
62
+ constructor(e = 0) {
63
+ ie(this, "level"), this.level = e;
64
+ }
65
+ setLevel(e) {
66
+ this.level = e;
67
+ }
68
+ static setLevel(e) {
69
+ this.globalLevel = e;
70
+ }
71
+ format(e) {
72
+ return `${Ee.prefix} ${e}`;
73
+ }
74
+ // Instance methods
75
+ error(e, ...r) {
76
+ this.level >= 1 && console.error(this.format(e), ...r);
77
+ }
78
+ warn(e, ...r) {
79
+ this.level >= 2 && console.warn(this.format(e), ...r);
80
+ }
81
+ info(e, ...r) {
82
+ this.level >= 3 && console.info(this.format(e), ...r);
83
+ }
84
+ debug(e, ...r) {
85
+ this.level >= 4 && console.debug(this.format(e), ...r);
86
+ }
87
+ // Static methods (using global level)
88
+ static error(e, ...r) {
89
+ this.globalLevel >= 1 && console.error(`${this.prefix} ${e}`, ...r);
90
+ }
91
+ static warn(e, ...r) {
92
+ this.globalLevel >= 2 && console.warn(`${this.prefix} ${e}`, ...r);
93
+ }
94
+ static info(e, ...r) {
95
+ this.globalLevel >= 3 && console.info(`${this.prefix} ${e}`, ...r);
96
+ }
97
+ static debug(e, ...r) {
98
+ this.globalLevel >= 4 && console.debug(`${this.prefix} ${e}`, ...r);
99
+ }
100
+ };
101
+ ie(
102
+ ne,
103
+ "globalLevel",
104
+ 1
105
+ /* ERROR */
106
+ );
107
+ ie(ne, "prefix", "[Ctrovalidate]");
108
+ var Ye = ne, je = {};
109
+ Xe(je, {
110
+ alpha: () => xe,
111
+ alphaDash: () => $e,
112
+ alphaNum: () => Re,
113
+ alphaSpaces: () => it,
114
+ between: () => Ve,
115
+ creditCard: () => qe,
116
+ decimal: () => Te,
117
+ email: () => Ae,
118
+ exactLength: () => ke,
119
+ integer: () => Le,
120
+ ipAddress: () => _e,
121
+ json: () => Ie,
122
+ max: () => we,
123
+ maxLength: () => Se,
124
+ min: () => Ce,
125
+ minLength: () => Ne,
126
+ numeric: () => Me,
127
+ phone: () => De,
128
+ required: () => Fe,
129
+ sameAs: () => Be,
130
+ strongPassword: () => ze,
131
+ url: () => Pe
132
+ });
133
+ var Fe = (t) => t == null ? !1 : typeof t == "boolean" ? t : String(t).trim() !== "", et = /^[^\s@]+@[^\s@]+\.[^\s@]+$/, Ae = (t) => t ? et.test(String(t)) : !0, Ce = (t, e = []) => {
134
+ if (t == null || t === "") return !0;
135
+ if (!e[0])
136
+ return console.error("[Ctrovalidate] Missing parameter for 'min' rule."), !1;
137
+ const r = Number(e[0]);
138
+ return Number(t) >= r;
139
+ }, we = (t, e = []) => {
140
+ if (t == null || t === "") return !0;
141
+ if (!e[0])
142
+ return console.error("[Ctrovalidate] Missing parameter for 'max' rule."), !1;
143
+ const r = Number(e[0]);
144
+ return Number(t) <= r;
145
+ }, Ne = (t, e = []) => {
146
+ if (t == null || t === "") return !0;
147
+ if (!e[0])
148
+ return console.error("[Ctrovalidate] Missing parameter for 'minLength' rule."), !1;
149
+ const r = Number(e[0]);
150
+ return String(t).length >= r;
151
+ }, Se = (t, e = []) => {
152
+ if (t == null || t === "") return !0;
153
+ if (!e[0])
154
+ return console.error("[Ctrovalidate] Missing parameter for 'maxLength' rule."), !1;
155
+ const r = Number(e[0]);
156
+ return String(t).length <= r;
157
+ }, tt = /^[a-zA-Z]+$/, xe = (t) => t ? tt.test(String(t)) : !0, rt = /^[a-zA-Z0-9]+$/, Re = (t) => t ? rt.test(String(t)) : !0, st = /^[a-zA-Z0-9-_]+$/, $e = (t) => t ? st.test(String(t)) : !0, at = /^[a-zA-Z\s]+$/, it = (t) => t ? at.test(String(t)) : !0, Me = (t) => !t && t !== 0 ? !0 : !isNaN(Number(t)) && !isNaN(parseFloat(String(t))), nt = /^-?\d+$/, Le = (t) => !t && t !== 0 ? !0 : nt.test(String(t)), lt = /^-?\d+(\.\d+)?$/, Te = (t) => !t && t !== 0 ? !0 : lt.test(String(t)), ot = /^(https?:\/\/)([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/, Pe = (t) => t ? ot.test(String(t)) : !0, dt = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, ct = /^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$/, _e = (t) => {
158
+ if (!t) return !0;
159
+ const e = String(t);
160
+ return dt.test(e) || ct.test(e);
161
+ }, Ie = (t) => {
162
+ if (!t) return !0;
163
+ try {
164
+ const e = JSON.parse(String(t));
165
+ return typeof e == "object" && e !== null;
166
+ } catch {
167
+ return !1;
168
+ }
169
+ }, ut = /^[+]?([(]?[0-9]{1,4}[)]?)[-\s.]?[0-9]{3,4}[-\s.]?[0-9]{4,6}$/, De = (t) => t ? ut.test(String(t)) : !0, qe = (t) => {
170
+ if (!t) return !0;
171
+ const e = String(t).replace(/[- ]/g, "");
172
+ if (!/^\d+$/.test(e)) return !1;
173
+ let r = 0;
174
+ const a = e.length % 2;
175
+ for (let i = 0; i < e.length; i++) {
176
+ let l = parseInt(e[i], 10);
177
+ i % 2 === a && (l *= 2, l > 9 && (l -= 9)), r += l;
178
+ }
179
+ return r % 10 === 0;
180
+ }, ht = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, ze = (t) => t ? ht.test(String(t)) : !0, ke = (t, e = []) => {
181
+ if (t == null || t === "") return !0;
182
+ if (!e[0])
183
+ return console.error("[Ctrovalidate] Missing parameter for 'exactLength' rule."), !1;
184
+ const r = Number(e[0]);
185
+ return String(t).length === r;
186
+ }, Ve = (t, e = []) => {
187
+ if (t == null || t === "") return !0;
188
+ if (e.length < 2)
189
+ return console.error("[Ctrovalidate] Missing parameters for 'between' rule."), !1;
190
+ const r = Number(e[0]), a = Number(e[1]);
191
+ if (isNaN(r) || isNaN(a))
192
+ return !1;
193
+ if (!isNaN(Number(t)) && typeof t != "string") {
194
+ const n = Number(t);
195
+ return n >= r && n <= a;
196
+ }
197
+ const i = String(t);
198
+ if (!isNaN(Number(i)) && i !== "") {
199
+ const n = Number(i);
200
+ return n >= r && n <= a;
201
+ }
202
+ const l = i.length;
203
+ return l >= r && l <= a;
204
+ }, Be = (t, e = []) => {
205
+ if (e.length === 0 || e[0] === void 0)
206
+ return console.error("[Ctrovalidate] Missing parameter for 'sameAs' rule."), !1;
207
+ const r = e[0];
208
+ return t === r;
209
+ }, ft = {
210
+ required: "This field is required.",
211
+ email: "Please enter a valid email address.",
212
+ min: "Minimum value is {0}.",
213
+ max: "Maximum value is {0}.",
214
+ minLength: "Minimum length is {0} characters.",
215
+ maxLength: "Maximum length is {0} characters.",
216
+ alpha: "This field may only contain alphabetic characters.",
217
+ alphaNum: "This field may only contain alpha-numeric characters.",
218
+ alphaDash: "This field may only contain alpha-numeric characters as well as dashes and underscores.",
219
+ alphaSpaces: "This field may only contain alphabetic characters and spaces.",
220
+ numeric: "This field must be a number.",
221
+ integer: "This field must be an integer.",
222
+ decimal: "This field must be a decimal number.",
223
+ url: "Please enter a valid URL.",
224
+ ipAddress: "Please enter a valid IP address.",
225
+ json: "Please enter a valid JSON string.",
226
+ phone: "Please enter a valid phone number.",
227
+ creditCard: "Please enter a valid credit card number.",
228
+ strongPassword: "Password must be at least 8 characters long and include an uppercase letter, a lowercase letter, a number, and a symbol.",
229
+ exactLength: "This field must be exactly {0} characters long.",
230
+ between: "This field must be between {0} and {1}.",
231
+ sameAs: "This field must match {0}."
232
+ }, x, I, mt = class {
233
+ constructor() {
234
+ ue(this, x, { en: ft }), ue(this, I, "en");
235
+ }
236
+ /**
237
+ * Set the current locale.
238
+ */
239
+ setLocale(t) {
240
+ if (!S(this, x)[t]) {
241
+ console.warn(
242
+ `[Ctrovalidate] Locale "${t}" not found. Falling back to "en".`
243
+ ), he(this, I, "en");
244
+ return;
245
+ }
246
+ he(this, I, t);
247
+ }
248
+ /**
249
+ * Add or update messages for a specific locale.
250
+ */
251
+ addMessages(t, e) {
252
+ S(this, x)[t] = {
253
+ ...S(this, x)[t] || {},
254
+ ...e
255
+ };
256
+ }
257
+ /**
258
+ * Translates a rule name with optional parameters.
259
+ */
260
+ translate(t, e = [], r) {
261
+ const a = r || S(this, I), i = S(this, x)[a] || S(this, x).en;
262
+ return (i[t] || i.default || "Invalid input.").replace(/{(\d+)}/g, (n, d) => {
263
+ const u = parseInt(d, 10);
264
+ return e[u] !== void 0 ? String(e[u]) : n;
265
+ });
266
+ }
267
+ /**
268
+ * Get the current active locale.
269
+ */
270
+ get currentLocale() {
271
+ return S(this, I);
272
+ }
273
+ };
274
+ x = /* @__PURE__ */ new WeakMap();
275
+ I = /* @__PURE__ */ new WeakMap();
276
+ new mt();
277
+ var D, B, q, R, J, U, C, We, re, se;
278
+ const K = class K {
279
+ constructor({
280
+ errorClass: e = "ctrovalidate-error",
281
+ errorMessageClass: r = "ctrovalidate-error-message",
282
+ pendingClass: a = "ctrovalidate-pending"
283
+ }) {
284
+ o(this, C);
285
+ o(this, D);
286
+ o(this, B);
287
+ o(this, q);
288
+ // Cache field -> found error element
289
+ o(this, R, /* @__PURE__ */ new WeakMap());
290
+ // Optional map to store generated IDs per field when we create an element
291
+ o(this, J, /* @__PURE__ */ new WeakMap());
292
+ c(this, D, e), c(this, B, r), c(this, q, a);
293
+ }
294
+ /**
295
+ * Display a validation error message for the given field.
296
+ * Applies configured error classes and ARIA attributes.
297
+ */
298
+ displayError(e, r) {
299
+ const a = v(this, C, se).call(this, e);
300
+ s(this, D).split(/\s+/).filter(Boolean).forEach((i) => e.classList.add(i)), e.setAttribute("aria-invalid", "true"), a && (a.textContent = r, a.style.display = "", e.setAttribute("aria-describedby", a.id));
301
+ }
302
+ /**
303
+ * Clear the validation error message and related styling/ARIA attributes.
304
+ */
305
+ clearError(e) {
306
+ const r = v(this, C, se).call(this, e);
307
+ s(this, D).split(/\s+/).filter(Boolean).forEach((a) => e.classList.remove(a)), e.removeAttribute("aria-invalid"), r && (r.textContent = "", r.style.display = "none", e.getAttribute("aria-describedby") === r.id && e.removeAttribute("aria-describedby"));
308
+ }
309
+ /**
310
+ * Show pending state for a field (async validation).
311
+ */
312
+ showPending(e) {
313
+ this.clearError(e), s(this, q).split(/\s+/).filter(Boolean).forEach((r) => e.classList.add(r));
314
+ }
315
+ /**
316
+ * Hide pending state for a field.
317
+ */
318
+ hidePending(e) {
319
+ s(this, q).split(/\s+/).filter(Boolean).forEach((r) => e.classList.remove(r));
320
+ }
321
+ };
322
+ D = new WeakMap(), B = new WeakMap(), q = new WeakMap(), R = new WeakMap(), J = new WeakMap(), U = new WeakMap(), C = new WeakSet(), /**
323
+ * Safely escape a class name for use in querySelector.
324
+ * Uses CSS.escape when available, otherwise falls back to a robust regex.
325
+ */
326
+ We = function(e) {
327
+ return typeof CSS < "u" && typeof CSS.escape == "function" ? CSS.escape(e) : e.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~[\]])/g, "\\$1");
328
+ }, /**
329
+ * Build a DOM id from the field (name if available) or a generated counter.
330
+ */
331
+ re = function(e) {
332
+ let r;
333
+ if ((e instanceof HTMLInputElement || e instanceof HTMLSelectElement || e instanceof HTMLTextAreaElement) && (r = e.name), r && r.trim().length > 0)
334
+ return `ctrovalidate-error-${r.replace(/\s+/g, "-").replace(/[^A-Za-z0-9\-_:.]/g, "")}`;
335
+ const a = `ctrovalidate-error-${ce(K, U)._++}`;
336
+ return s(this, J).set(e, a), a;
337
+ }, /**
338
+ * Finds (or creates) the dedicated error message container for a given input field.
339
+ * Supports multiple classes in `errorMessageClass` by trying each class individually.
340
+ */
341
+ se = function(e) {
342
+ if (s(this, R).has(e))
343
+ return s(this, R).get(e) || null;
344
+ const r = s(this, B).split(/\s+/).filter(Boolean);
345
+ let a = e.parentElement, i = 0;
346
+ const l = 3;
347
+ for (; a && i < l; ) {
348
+ for (const g of r) {
349
+ const N = `.${v(this, C, We).call(this, g)}`, f = a.querySelector(
350
+ N
351
+ );
352
+ if (f)
353
+ return f.id || (f.id = v(this, C, re).call(this, e)), s(this, R).set(e, f), f;
354
+ }
355
+ a = a.parentElement, i++;
356
+ }
357
+ const n = e.parentElement;
358
+ if (!n) return null;
359
+ const d = document.createElement("div");
360
+ d.className = r.join(" "), d.style.display = "none", d.role = "status", d.setAttribute("aria-live", "polite");
361
+ const u = v(this, C, re).call(this, e);
362
+ d.id = u;
363
+ try {
364
+ e.insertAdjacentElement("afterend", d);
365
+ } catch {
366
+ n.appendChild(d);
367
+ }
368
+ return s(this, R).set(e, d), d;
369
+ }, // Global unique id counter for fallback ids
370
+ o(K, U, 0);
371
+ let te = K;
372
+ var b, w, W, Z, H, O, Q, Ze;
373
+ class gt {
374
+ /**
375
+ * @param formElement - The form element.
376
+ * @param dependencies - Core dependencies.
377
+ */
378
+ constructor(e, { logger: r, uiManager: a, rules: i, asyncRules: l, messages: n }) {
379
+ o(this, Q);
380
+ o(this, b);
381
+ o(this, w);
382
+ o(this, W);
383
+ o(this, Z);
384
+ o(this, H);
385
+ o(this, O);
386
+ c(this, W, e), c(this, b, r), c(this, w, a), c(this, Z, i), c(this, H, l), c(this, O, n);
387
+ }
388
+ /**
389
+ * The core logic for validating a single field.
390
+ */
391
+ async validateField(e) {
392
+ const { element: r, rules: a, state: i, dependency: l } = e;
393
+ if (!v(this, Q, Ze).call(this, l))
394
+ return s(this, b).debug(
395
+ `[RuleEngine] Dependency for "${r.name}" not met. Skipping validation.`
396
+ ), s(this, w).clearError(r), e.state.lastError = null, !0;
397
+ const n = r, d = n.type === "checkbox" ? n.checked : n.value;
398
+ i.abortController && (i.abortController.abort(), s(this, b).debug(
399
+ `[RuleEngine] Aborted previous validation for "${n.name}".`
400
+ ));
401
+ for (const u of a) {
402
+ const g = s(this, Z)[u.name], N = s(this, H)[u.name];
403
+ if (!g && !N) {
404
+ s(this, b).warn(
405
+ `[RuleEngine] Unknown rule "${u.name}" used on field:`,
406
+ r
407
+ );
408
+ continue;
409
+ }
410
+ let f = !1;
411
+ if (g)
412
+ f = g(d, u.params, r);
413
+ else if (N) {
414
+ i.abortController = new AbortController(), s(this, w).showPending(r);
415
+ try {
416
+ f = await N(
417
+ d,
418
+ u.params,
419
+ r,
420
+ i.abortController.signal
421
+ );
422
+ } catch (p) {
423
+ if (p instanceof Error && p.name === "AbortError")
424
+ return s(this, b).debug(
425
+ `[RuleEngine] Async validation for "${n.name}" was successfully aborted.`
426
+ ), !0;
427
+ s(this, b).error(
428
+ `[RuleEngine] Async rule "${u.name}" threw an error.`,
429
+ [p]
430
+ ), f = !1;
431
+ } finally {
432
+ s(this, w).hidePending(r), i.abortController = null;
433
+ }
434
+ }
435
+ if (!f) {
436
+ let p = s(this, O)[u.name] || "Invalid input.";
437
+ e.customMessages && (e.customMessages[u.name] ? p = e.customMessages[u.name] : e.customMessages["*"] && (p = e.customMessages["*"]));
438
+ const _ = p.replace(/{(\d+)}/g, (Ge, Je) => {
439
+ const le = parseInt(Je, 10);
440
+ return u.params[le] !== void 0 ? String(u.params[le]) : Ge;
441
+ });
442
+ return s(this, w).displayError(r, _), e.state.lastError = _, s(this, b).debug(
443
+ `[RuleEngine] Field validation failed for "${n.name}" on rule "${u.name}".`
444
+ ), !1;
445
+ }
446
+ }
447
+ return s(this, w).clearError(r), e.state.lastError = null, s(this, b).debug(
448
+ `[RuleEngine] Field validation succeeded for "${n.name}".`
449
+ ), !0;
450
+ }
451
+ }
452
+ b = new WeakMap(), w = new WeakMap(), W = new WeakMap(), Z = new WeakMap(), H = new WeakMap(), O = new WeakMap(), Q = new WeakSet(), /**
453
+ * Checks if a field's dependency condition is currently met.
454
+ */
455
+ Ze = function(e) {
456
+ if (!e) return !0;
457
+ const r = s(this, W).querySelector(
458
+ `[name="${e.controllerName}"]`
459
+ );
460
+ if (!r) return !1;
461
+ switch (e.type) {
462
+ case "checked":
463
+ return r.checked;
464
+ case "value":
465
+ return r.value === e.value;
466
+ case "present":
467
+ return !!r.value;
468
+ default:
469
+ return !1;
470
+ }
471
+ };
472
+ var $, y, E, m, z, k, M, P, ae, He;
473
+ class pt {
474
+ /**
475
+ * @param formElement - The form element to control.
476
+ * @param dependencies - Core dependencies.
477
+ */
478
+ constructor(e, {
479
+ logger: r,
480
+ schema: a = {},
481
+ aliases: i = {},
482
+ validationHandler: l
483
+ }) {
484
+ o(this, P);
485
+ o(this, $);
486
+ o(this, y, []);
487
+ o(this, E, /* @__PURE__ */ new Map());
488
+ o(this, m);
489
+ o(this, z);
490
+ o(this, k);
491
+ o(this, M);
492
+ c(this, $, e), c(this, m, r), c(this, z, a), c(this, k, i), c(this, M, l);
493
+ }
494
+ /**
495
+ * Scans the form to find all fields and prepares them for validation.
496
+ */
497
+ discoverFields() {
498
+ c(this, y, []), s(this, E).clear();
499
+ const e = s(this, $).querySelectorAll(
500
+ "[data-ctrovalidate-rules]"
501
+ );
502
+ s(this, m).debug(
503
+ "[FormController] Discovered " + e.length + " fields to validate."
504
+ ), e.forEach(
505
+ (r) => this.addField(r, !1)
506
+ ), Object.keys(s(this, z)).forEach((r) => {
507
+ const a = s(this, $).querySelector(
508
+ `[name="${r}"]`
509
+ );
510
+ a && !s(this, E).has(a) && this.addField(a, !1);
511
+ });
512
+ }
513
+ /**
514
+ * Adds a single field to the validation controller.
515
+ */
516
+ addField(e, r = !0) {
517
+ if (!e || s(this, E).has(e))
518
+ return;
519
+ const a = e.getAttribute("data-ctrovalidate-rules"), i = e.getAttribute("data-ctrovalidate-if"), l = e.name, n = j(a || "", s(this, k)), d = l ? j(s(this, z)[l] || [], s(this, k)) : [], u = [...n, ...d];
520
+ if (u.length === 0 && !i)
521
+ return;
522
+ const g = {}, N = e.getAttribute("data-ctrovalidate-message");
523
+ N && (g["*"] = N);
524
+ for (const p of Array.from(e.attributes))
525
+ if (p.name.startsWith("data-ctrovalidate-") && p.name.endsWith("-message")) {
526
+ const _ = p.name.replace("data-ctrovalidate-", "").replace("-message", "");
527
+ _ !== "" && _ !== "message" && (g[_] = p.value);
528
+ }
529
+ const f = {
530
+ element: e,
531
+ rules: u,
532
+ dependency: v(this, P, He).call(this, i),
533
+ customMessages: Object.keys(g).length > 0 ? g : void 0,
534
+ state: { isDirty: !1, abortController: null, lastError: null }
535
+ };
536
+ s(this, y).push(f), s(this, E).set(e, f), s(this, m).debug("[FormController] Added field:", e), r && v(this, P, ae).call(this, f);
537
+ }
538
+ /**
539
+ * Removes a single field from the validation controller.
540
+ */
541
+ removeField(e) {
542
+ if (!e || !s(this, E).has(e))
543
+ return;
544
+ const r = s(this, E).get(e);
545
+ if (c(this, y, s(this, y).filter((a) => a.element !== e)), r.listeners) {
546
+ const { onBlur: a, onInput: i, onControllerInput: l, controllerElement: n } = r.listeners;
547
+ try {
548
+ e.removeEventListener("blur", a), e.removeEventListener("input", i), n && l && n.removeEventListener("input", l);
549
+ } catch (d) {
550
+ s(this, m).debug(
551
+ "[FormController] Error while removing listeners for",
552
+ e,
553
+ d
554
+ );
555
+ }
556
+ delete r.listeners;
557
+ }
558
+ s(this, E).delete(e), s(this, m).debug("[FormController] Removed field:", e);
559
+ }
560
+ /**
561
+ * Attaches all necessary event listeners for real-time validation.
562
+ */
563
+ attachEventListeners(e) {
564
+ if (!e) {
565
+ s(this, m).info("[FormController] Real-time validation is disabled.");
566
+ return;
567
+ }
568
+ s(this, m).info(
569
+ "[FormController] Real-time validation is enabled. Attaching listeners."
570
+ ), s(this, y).forEach(
571
+ (r) => v(this, P, ae).call(this, r)
572
+ );
573
+ }
574
+ /**
575
+ * Resets all field states to clean.
576
+ */
577
+ reset() {
578
+ s(this, y).forEach((e) => {
579
+ e.state.isDirty = !1, e.state.lastError = null;
580
+ });
581
+ }
582
+ /**
583
+ * Cleans up all event listeners.
584
+ */
585
+ destroy() {
586
+ [...s(this, y)].forEach((e) => {
587
+ this.removeField(e.element);
588
+ });
589
+ }
590
+ /**
591
+ * Returns all discovered fields.
592
+ */
593
+ getFields() {
594
+ return s(this, y);
595
+ }
596
+ }
597
+ $ = new WeakMap(), y = new WeakMap(), E = new WeakMap(), m = new WeakMap(), z = new WeakMap(), k = new WeakMap(), M = new WeakMap(), P = new WeakSet(), /**
598
+ * Attaches event listeners to a single field object.
599
+ */
600
+ ae = function(e) {
601
+ const { element: r, dependency: a } = e;
602
+ if (e.listeners) return;
603
+ const i = () => {
604
+ s(this, m).debug(
605
+ `[FormController] Blur event on "${r.name}". Validating.`
606
+ ), s(this, M).call(this, e), e.state.isDirty = !0;
607
+ }, l = () => {
608
+ e.state.isDirty && (s(this, m).debug(
609
+ `[FormController] Input event on dirty field "${r.name}". Validating.`
610
+ ), s(this, M).call(this, e));
611
+ };
612
+ r.addEventListener("blur", i), r.addEventListener("input", l);
613
+ let n = null, d = null;
614
+ a && (n = s(this, $).querySelector(
615
+ `[name="${a.controllerName}"]`
616
+ ), n ? (d = () => {
617
+ s(this, m).debug(
618
+ `[FormController] Controller "${n.name}" changed. Re-validating dependent field "${r.name}".`
619
+ ), s(this, M).call(this, e);
620
+ }, n.addEventListener("input", d)) : s(this, m).warn(
621
+ `[FormController] Could not find controller field with name "${a.controllerName}".`
622
+ )), e.listeners = {
623
+ onBlur: i,
624
+ onInput: l,
625
+ onControllerInput: d,
626
+ controllerElement: n
627
+ };
628
+ }, /**
629
+ * Parses the dependency condition string.
630
+ */
631
+ He = function(e) {
632
+ if (!e) return null;
633
+ const r = e.split(":"), a = r[0], i = r[1] || "present";
634
+ if (!a) return null;
635
+ if (i.includes("=")) {
636
+ const [l, n] = i.split("=");
637
+ return { controllerName: a, type: l, value: n };
638
+ } else
639
+ return { controllerName: a, type: i };
640
+ };
641
+ const vt = (t, e) => {
642
+ const r = t;
643
+ if (!r.form)
644
+ return "";
645
+ const a = r.form.querySelector(
646
+ `[name="${e}"]`
647
+ );
648
+ return a ? a.value : "";
649
+ }, bt = (t, e = [], r = null) => {
650
+ const a = String(e[0] || "");
651
+ if (a === void 0)
652
+ return console.error("[Ctrovalidate] Missing parameter for 'sameAs' rule."), !1;
653
+ if (!r) return !1;
654
+ const i = vt(r, a);
655
+ return Be(t, [i]);
656
+ }, yt = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
657
+ __proto__: null,
658
+ alpha: xe,
659
+ alphaDash: $e,
660
+ alphaNum: Re,
661
+ between: Ve,
662
+ creditCard: qe,
663
+ decimal: Te,
664
+ email: Ae,
665
+ exactLength: ke,
666
+ integer: Le,
667
+ ipAddress: _e,
668
+ json: Ie,
669
+ max: we,
670
+ maxLength: Se,
671
+ min: Ce,
672
+ minLength: Ne,
673
+ numeric: Me,
674
+ phone: De,
675
+ required: Fe,
676
+ sameAs: bt,
677
+ strongPassword: ze,
678
+ url: Pe
679
+ }, Symbol.toStringTag, { value: "Module" })), me = {
680
+ ...yt
681
+ }, ge = {}, pe = {}, Et = {
682
+ required: "This field is required.",
683
+ minLength: "This field must be at least {0} characters long.",
684
+ maxLength: "This field cannot be longer than {0} characters.",
685
+ between: "This field must be between {0} and {1} characters long.",
686
+ email: "Please enter a valid email address.",
687
+ phone: "Please enter a valid phone number.",
688
+ url: "Please enter a valid URL.",
689
+ numeric: "This field must contain only numbers.",
690
+ alpha: "This field must contain only letters.",
691
+ alphaDash: "This field must contain only letters, numbers, and dashes.",
692
+ alphaSpaces: "This field must contain only letters and spaces.",
693
+ alphaNum: "This field must contain only letters and numbers.",
694
+ decimal: "This field must be a valid decimal number.",
695
+ integer: "This field must be a valid integer.",
696
+ min: "This field must be at least {0}.",
697
+ max: "This field cannot be more than {0}.",
698
+ exactLength: "This field must be exactly {0} characters long.",
699
+ format: "This field format is invalid.",
700
+ sameAs: "This field must match {0}.",
701
+ strongPassword: "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.",
702
+ creditCard: "Please enter a valid credit card number.",
703
+ ipAddress: "Please enter a valid IP address.",
704
+ json: "Please enter a valid JSON string."
705
+ }, G = {
706
+ ...Et
707
+ };
708
+ function Ft(t) {
709
+ Object.assign(G, t);
710
+ }
711
+ var L, F, A, T, V, h, X, Oe;
712
+ class At {
713
+ /**
714
+ * @param formElement - The HTML form element to validate.
715
+ * @param options - Custom configuration options.
716
+ */
717
+ constructor(e, r = {}) {
718
+ o(this, X);
719
+ o(this, L);
720
+ o(this, F);
721
+ o(this, A);
722
+ o(this, T);
723
+ o(this, V);
724
+ o(this, h);
725
+ if (!(e instanceof HTMLFormElement))
726
+ throw new Error(
727
+ "Ctrovalidate requires a valid HTMLFormElement to be initialized."
728
+ );
729
+ c(this, L, e), c(this, F, {
730
+ logLevel: ee.NONE,
731
+ errorClass: "is-invalid",
732
+ errorMessageClass: "error-message",
733
+ pendingClass: "ctrovalidate-pending",
734
+ realTime: !0,
735
+ schema: {},
736
+ aliases: {},
737
+ ...r
738
+ }), c(this, A, new Ye(s(this, F).logLevel)), c(this, T, new te(s(this, F))), c(this, V, new gt(s(this, L), {
739
+ logger: s(this, A),
740
+ uiManager: s(this, T),
741
+ rules: me,
742
+ asyncRules: ge,
743
+ messages: G
744
+ })), c(this, h, new pt(s(this, L), {
745
+ logger: s(this, A),
746
+ schema: s(this, F).schema,
747
+ aliases: { ...pe, ...s(this, F).aliases },
748
+ validationHandler: (a) => s(this, V).validateField(a)
749
+ })), v(this, X, Oe).call(this);
750
+ }
751
+ /**
752
+ * Defines a rule alias (macro) globally.
753
+ */
754
+ static defineAlias(e, r) {
755
+ pe[e] = r;
756
+ }
757
+ /**
758
+ * Validates the entire form programmatically.
759
+ */
760
+ async validate() {
761
+ return this.validateForm();
762
+ }
763
+ /**
764
+ * Validates the entire form programmatically.
765
+ */
766
+ async validateForm() {
767
+ s(this, A).info("[Ctrovalidate] Manually triggering form validation.");
768
+ const e = s(this, h).getFields(), a = (await Promise.all(
769
+ e.map((i) => s(this, V).validateField(i))
770
+ )).every((i) => i === !0);
771
+ return a ? s(this, A).info("[Ctrovalidate] Form validation succeeded.") : s(this, A).warn("[Ctrovalidate] Form validation failed."), a;
772
+ }
773
+ /**
774
+ * Allows the user to add custom validation messages globally.
775
+ */
776
+ static setCustomMessages(e) {
777
+ Ft(e);
778
+ }
779
+ /**
780
+ * Adds a custom synchronous validation rule globally.
781
+ */
782
+ static addRule(e, r, a) {
783
+ if (!e || typeof r != "function") {
784
+ console.error(
785
+ "[Ctrovalidate] addRule requires a rule name and a logic function."
786
+ );
787
+ return;
788
+ }
789
+ me[e] = r, a && (G[e] = a);
790
+ }
791
+ /**
792
+ * Adds a custom asynchronous validation rule globally.
793
+ */
794
+ static addAsyncRule(e, r, a) {
795
+ if (!e || typeof r != "function") {
796
+ console.error(
797
+ "[Ctrovalidate] addAsyncRule requires a rule name and a logic function."
798
+ );
799
+ return;
800
+ }
801
+ ge[e] = r, a && (G[e] = a);
802
+ }
803
+ /**
804
+ * Manually adds a new field to be validated.
805
+ */
806
+ addField(e) {
807
+ s(this, h).addField(e);
808
+ }
809
+ /**
810
+ * Manually removes a field from validation.
811
+ */
812
+ removeField(e) {
813
+ s(this, h).removeField(e);
814
+ }
815
+ /**
816
+ * Refreshes the discovered fields. Use if fields are added or removed from the DOM.
817
+ */
818
+ refresh() {
819
+ s(this, h).discoverFields(), s(this, h).attachEventListeners(s(this, F).realTime);
820
+ }
821
+ /**
822
+ * Returns the current error for a field by its name.
823
+ */
824
+ getError(e) {
825
+ const r = s(this, h).getFields().find((a) => a.element.name === e);
826
+ return r ? r.state.lastError : null;
827
+ }
828
+ /**
829
+ * Checks if a field has been interacted with.
830
+ */
831
+ isDirty(e) {
832
+ const r = s(this, h).getFields().find((a) => a.element.name === e);
833
+ return r ? r.state.isDirty : !1;
834
+ }
835
+ /**
836
+ * Resets the validation state of the form.
837
+ */
838
+ reset() {
839
+ s(this, h).reset(), s(this, h).getFields().forEach((e) => {
840
+ s(this, T).clearError(e.element);
841
+ });
842
+ }
843
+ /**
844
+ * Fully cleans up the validator instance.
845
+ */
846
+ destroy() {
847
+ s(this, h).destroy(), s(this, h).getFields().forEach((e) => {
848
+ s(this, T).clearError(e.element);
849
+ });
850
+ }
851
+ }
852
+ L = new WeakMap(), F = new WeakMap(), A = new WeakMap(), T = new WeakMap(), V = new WeakMap(), h = new WeakMap(), X = new WeakSet(), /**
853
+ * Initializes the library by discovering fields and attaching listeners.
854
+ */
855
+ Oe = function() {
856
+ s(this, A).info("[Ctrovalidate] Initializing library."), s(this, h).discoverFields(), s(this, h).attachEventListeners(s(this, F).realTime), s(this, L).setAttribute("novalidate", "true");
857
+ }, de(At, "LogLevel", ee);
858
+ export {
859
+ At as Ctrovalidate,
860
+ ee as LogLevel
861
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "ctrovalidate-browser",
3
+ "version": "1.0.0",
4
+ "description": "Browser adapter for Ctrovalidate - Declarative HTML-first form validation with data attributes.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "vite build",
21
+ "test": "vitest run",
22
+ "test:coverage": "vitest run --coverage",
23
+ "lint": "eslint .",
24
+ "format": "prettier . --check"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^9.39.2",
28
+ "ctrovalidate-core": "^1.0.0",
29
+ "eslint": "^9.39.2",
30
+ "eslint-config-prettier": "^9.1.0",
31
+ "globals": "^17.2.0",
32
+ "prettier": "^3.1.1",
33
+ "vite": "^6.0.0",
34
+ "vite-plugin-dts": "^4.0.0",
35
+ "typescript": "^5.9.3",
36
+ "typescript-eslint": "^8.54.0",
37
+ "vitest": "^4.0.18"
38
+ },
39
+ "homepage": "https://ctrovalidate.vercel.app/guide/browser",
40
+ "keywords": [
41
+ "ctrovalidate",
42
+ "validation",
43
+ "form-validation",
44
+ "browser",
45
+ "javascript"
46
+ ],
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/ctrotech-tutor/ctrovalidate-browser.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/ctrotech-tutor/ctrovalidate-browser/issues"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "author": "Ctrotech (https://github.com/ctrotech-tutor)",
58
+ "license": "MIT"
59
+ }