kilvalidate 1.0.71 → 3.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.
@@ -0,0 +1,1736 @@
1
+ /*! kilvalidate | Author: Kilvish (Vasu Birla) | MIT */
2
+ (function (root, factory) {
3
+ if (typeof module === 'object' && module.exports) {
4
+ module.exports = factory(root);
5
+ } else if (typeof define === 'function' && define.amd) {
6
+ define(function () { return factory(root); });
7
+ } else if (root) {
8
+ root.kilvalidate = factory(root);
9
+ }
10
+ })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : undefined), function (window) {
11
+ if (!window || !window.document) {
12
+ const noop = () => {};
13
+ const noopFalse = () => false;
14
+ return {
15
+ validateRequiredFields: noopFalse,
16
+ addRealTimeValidation: noop,
17
+ validateForm: noopFalse,
18
+ addAsteriskToRequiredFields: noop,
19
+ attachInputEvents: noop,
20
+ addValidateFormToSubmitButtons: noop,
21
+ validateKilvishInput: noop,
22
+ addErrorMessage: noop,
23
+ init: noop,
24
+ initPasswordStrengthMeter: noop
25
+ };
26
+ }
27
+
28
+ const document = window.document;
29
+
30
+ //==================== Comman Kilvalidation start =====================
31
+
32
+
33
+ // Minimal CSS once (prevents layout jump + smooth reveal)
34
+ (function kilErrCSS(){
35
+ if (document.getElementById('kil-error-style')) return;
36
+ const style = document.createElement('style');
37
+ style.id = 'kil-error-style';
38
+ style.textContent = `
39
+ .error-message {
40
+ display:block;
41
+ min-height:16px; /* reserves space even when empty */
42
+ margin-top:4px;
43
+ font-size:12px;
44
+ color:#d32f2f;
45
+ line-height:1.25;
46
+ opacity:1;
47
+ transition: opacity .12s ease;
48
+ }
49
+ .error-message.is-hidden{ opacity:0; }
50
+ `;
51
+ document.head.appendChild(style);
52
+ })();
53
+
54
+ // Safe key for IDs/data-attrs
55
+ const _escKey = s => String(s || '').replace(/[^a-zA-Z0-9_\-]/g, '_');
56
+ const _cssEscape = s => {
57
+ if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(s));
58
+ return String(s).replace(/["\\]/g, '\\$&');
59
+ };
60
+
61
+ function resolveForm(kil) {
62
+ if (!kil) return null;
63
+ if (kil.tagName && String(kil.tagName).toUpperCase() === 'FORM') return kil;
64
+ if (typeof kil === 'string') {
65
+ const isSelector = /^[#.\[]/.test(kil) || kil.includes(' ') || kil.includes('[');
66
+ if (isSelector) return document.querySelector(kil);
67
+ return document.getElementById(kil) || document.querySelector(kil);
68
+ }
69
+ if (kil.querySelector) return kil;
70
+ return null;
71
+ }
72
+
73
+ // Find the visual anchor where the message should appear
74
+ function getErrorAnchor(target) {
75
+ // If an input/select/textarea is passed, work from it; if a container is passed,
76
+ // use the last field inside as the anchor (fallback to container itself).
77
+ let field = null;
78
+
79
+ if (target && target.tagName && /^(INPUT|SELECT|TEXTAREA)$/.test(target.tagName)) {
80
+ field = target;
81
+ } else if (target && target.querySelector) {
82
+ const fields = target.querySelectorAll('input,select,textarea');
83
+ field = fields.length ? fields[fields.length - 1] : null;
84
+ }
85
+
86
+ // if we still don't have a field, use the target itself
87
+ if (!field) return { anchor: target, mode: 'beforeend', key: 'container' };
88
+
89
+ // select2: show under the rendered container, not the <select>
90
+ const sib = field.nextElementSibling;
91
+ if (field.tagName === 'SELECT' && sib && (sib.classList.contains('select2') || sib.classList.contains('select2-container'))) {
92
+ return { anchor: sib, mode: 'afterend', key: field.name || field.id || 'select' };
93
+ }
94
+
95
+ // bootstrap input-group: show under the group wrapper
96
+ if (field.parentElement && field.parentElement.classList.contains('input-group')) {
97
+ return { anchor: field.parentElement, mode: 'afterend', key: field.name || field.id || 'inputgroup' };
98
+ }
99
+
100
+ // default: directly under the field
101
+ return { anchor: field, mode: 'afterend', key: field.name || field.id || 'field' };
102
+ }
103
+
104
+ // Create (once) or reuse a persistent error node right under the anchor
105
+ function getOrCreateErrorNode(form, anchor, mode, key) {
106
+ const id = `kil_err_${_escKey(key)}`;
107
+
108
+ // Try to find existing
109
+ let node = form.querySelector(`#${id}`);
110
+ if (!node) {
111
+ node = document.createElement('div');
112
+ node.id = id;
113
+ node.className = 'error-message is-hidden';
114
+ if (mode === 'beforeend') {
115
+ anchor.insertAdjacentElement('beforeend', node);
116
+ } else {
117
+ anchor.insertAdjacentElement('afterend', node);
118
+ }
119
+ }
120
+ return node;
121
+ }
122
+
123
+
124
+
125
+
126
+ (function kilReqCSS(){
127
+ if (document.getElementById('kil-req-style')) return;
128
+ const style = document.createElement('style');
129
+ style.id = 'kil-req-style';
130
+ style.textContent = `
131
+ .kil-required-star{ color:#d32f2f; margin-left:2px; }
132
+ .error-message{ display:block; min-height:16px; margin-top:4px; font-size:12px; color:#d32f2f; line-height:1.25; opacity:1; transition:opacity .12s ease; }
133
+ .error-message.is-hidden{ opacity:0; }
134
+ `;
135
+ document.head.appendChild(style);
136
+ })()
137
+
138
+ // ----- label resolution: prefer "for=[id]" then fallback to name, fieldset > legend, or closest label wrapper -----
139
+ function findLabelFor(input){
140
+ if (!input) return null;
141
+ const form = input.form || document;
142
+ let label = null;
143
+
144
+ if (input.id) label = form.querySelector(`label[for="${_cssEscape(input.id)}"]`);
145
+ if (!label && input.name) label = form.querySelector(`label[for="${_cssEscape(input.name)}"]`); // your legacy pattern
146
+ if (!label) {
147
+ // label wrapping input
148
+ label = input.closest('label');
149
+ }
150
+ if (!label) {
151
+ // fieldset legend (radios/checkbox groups)
152
+ const fs = input.closest('fieldset');
153
+ if (fs) label = fs.querySelector('legend');
154
+ }
155
+ return label;
156
+ }
157
+
158
+ // ----- star helpers -----
159
+ function ensureStarVisibleFor(input){
160
+ const label = findLabelFor(input);
161
+ if (!label) return;
162
+
163
+ let star = label.querySelector('.kil-required-star');
164
+ if (!star) {
165
+ star = document.createElement('span');
166
+ star.className = 'kil-required-star';
167
+ star.textContent = ' *';
168
+ label.appendChild(star);
169
+ }
170
+ star.style.visibility = 'visible';
171
+ }
172
+
173
+ function hideStarFor(input){
174
+ const label = findLabelFor(input);
175
+ if (!label) return;
176
+ const star = label.querySelector('.kil-required-star');
177
+ if (star) star.style.visibility = 'hidden';
178
+ }
179
+
180
+
181
+ // 👉 helper: decide if a TEL (kiltel) field is effectively empty
182
+ function isKiltelEmpty(telInput) {
183
+ // try the sibling hidden “contact” (most reliable if your kiltel populates it)
184
+ const wrap = telInput.closest('.kiltel-phone-wrap') || telInput.parentElement;
185
+ const hiddenContact = wrap && (wrap.querySelector('input[name="contact"]') || wrap.querySelector('#contact'));
186
+ if (hiddenContact) {
187
+ const digits = (hiddenContact.value || '').replace(/\D/g, '');
188
+ return digits.length < 7; // treat <7 digits as “no real number”
189
+ }
190
+
191
+ // fallback: inspect the tel input itself
192
+ const raw = (telInput.value || '').trim().replace(/\s|-/g, '');
193
+ if (!raw) return true;
194
+ if (/^\+?$/.test(raw)) return true; // only plus
195
+ if (/^\+?\d{1,4}$/.test(raw)) return true; // looks like only country code
196
+ const totalDigits = raw.replace(/\D/g, '').length;
197
+ return totalDigits < 7; // not enough digits to be a number
198
+ }
199
+
200
+
201
+ function validateRequiredFields(kil) {
202
+
203
+ const form = resolveForm(kil);
204
+ if (!form) {
205
+ console.warn('Form not found for validation:', kil);
206
+ return false;
207
+ }
208
+
209
+ const inputs = form.querySelectorAll('input[required], select[required], textarea[required]'); // Get only required inputs
210
+ const radioGroups = form.querySelectorAll('fieldset.radios');
211
+
212
+
213
+ const radios = form.querySelectorAll('input[type="radio"]');
214
+
215
+ // const checkboxes = form.querySelectorAll('input[type="checkbox"][required]');
216
+
217
+
218
+ // Validate checkboxes with the same name (at least one should be checked)
219
+
220
+ const checkboxGroups = {};
221
+ const requiredCheckboxes = new Set();
222
+ form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
223
+ const name = checkbox.name;
224
+ if (!checkboxGroups[name]) {
225
+ checkboxGroups[name] = [];
226
+ }
227
+ checkboxGroups[name].push(checkbox);
228
+
229
+ if (checkbox.hasAttribute('required')) {
230
+ requiredCheckboxes.add(name);
231
+ }
232
+ });
233
+
234
+ const select2 = form.querySelectorAll('.select2');
235
+
236
+
237
+ let isValid = true;
238
+ let kilerror = 'This Field is Required.'
239
+
240
+
241
+
242
+ // Remove previous error messages and borders
243
+ // Old -> form.querySelectorAll('.error-message').forEach(errorMsg => errorMsg.remove());
244
+
245
+ // NEW (keep slots, just hide/clear)
246
+ form.querySelectorAll('.error-message').forEach(node => {
247
+ node.textContent = '';
248
+ node.classList.add('is-hidden'); // keep space, fade out
249
+ });
250
+ inputs.forEach(input => {
251
+
252
+ input.style.border = ''; // Reset borders
253
+ });
254
+
255
+ inputs.forEach(input => {
256
+
257
+ const inputGroup = input.closest('.form-group') || input.parentNode;
258
+
259
+ const inputGroup1 = input.parentNode;
260
+
261
+ if (input.type == 'date') {
262
+
263
+
264
+ kilerror = "Please Select Date";
265
+ // Reset error message and border before validating
266
+ input.style.border = ''; // Reset border
267
+ const dateValue = input.value.trim();
268
+
269
+ if (!dateValue) {
270
+ isValid = false;
271
+ input.style.border = '1px solid red'; // Highlight empty date field
272
+ addErrorMessage(inputGroup, kilerror); // Add error message for empty date
273
+ } else {
274
+ // Additional date-specific validation logic can go here
275
+ }
276
+
277
+ // kilerror = "Please Select Date"
278
+ // input.style.border = '1px solid red';
279
+
280
+
281
+
282
+ } else if (input.name == 'comments') {
283
+
284
+ kilerror = 'Please Enter Comments'
285
+ } else if (input.type == 'email') {
286
+
287
+ kilerror = 'Please Enter Email'
288
+ } else if (input.type == 'password') {
289
+
290
+ kilerror = 'Please Enter Password'
291
+ } else {
292
+ kilerror = 'This Field is Required.'
293
+ }
294
+
295
+ const inputname = input.getAttribute('data-kilvish-name');
296
+ kilerror = `${inputname || 'This'} Field is Required.`;
297
+
298
+
299
+
300
+ // ---------- REQUIRED CHECK (with kiltel awareness) ----------
301
+ let isEmpty;
302
+ if (input.type === 'tel' || input.hasAttribute('data-kilvish-tel')) {
303
+
304
+ isEmpty = isKiltelEmpty(input);
305
+ if (isEmpty) {
306
+ kilerror = `${inputname || 'Phone'} is required (enter full number, not just country code).`;
307
+ }
308
+ }
309
+ // else {
310
+ // isEmpty = !(input.value || '').trim();
311
+ // }
312
+
313
+ if (isEmpty) {
314
+ isValid = false;
315
+ input.style.border = '1px solid red';
316
+ // pass the INPUT itself so your addErrorMessage anchors under the field;
317
+ // it will auto-adjust to .input-group when present.
318
+ addErrorMessage(input, kilerror);
319
+ return; // continue next input
320
+ }
321
+
322
+ // ---------- REQUIRED CHECK (with kiltel awareness) ----------
323
+
324
+
325
+ if (!input.value.trim()) { // If the required field is empty
326
+ isValid = false;
327
+ input.style.border = '1px solid red'; // Set border to red for empty required fields
328
+ addErrorMessage(inputGroup, kilerror); // Add error message
329
+
330
+ //addErrorMessage(inputGroup, kilerror);
331
+ }
332
+ });
333
+
334
+
335
+
336
+
337
+ //--------- For Radio button validation ----------
338
+ if (radios.length > 0) {
339
+
340
+
341
+ const radioGroups = {}; // Store radio button groups
342
+
343
+
344
+ radios.forEach(radio => {
345
+ const group = radio.name; // Group by name attribute
346
+ if (!radioGroups[group]) {
347
+ // radioGroups[group] = { isSelected: false, groupElement: radio.closest('.form-check') || radio.closest('.radios') || radio.closest('.col-md-6') };
348
+ radioGroups[group] = {
349
+ isSelected: false,
350
+ groupElement: radio.closest('.form-group') || radio.closest('.mb-3') ||
351
+ radio.closest('.form-check') ||
352
+ radio.closest('.radios') ||
353
+ radio.closest('.col-md-6')
354
+ };
355
+ }
356
+
357
+ if (radio.checked) {
358
+ radioGroups[group].isSelected = true;
359
+ }
360
+ });
361
+
362
+ // Check if each radio group has at least one selection
363
+ Object.entries(radioGroups).forEach(([group, data]) => {
364
+ if (!data.isSelected) {
365
+ isValid = false;
366
+ const message = "Please select one option.";
367
+
368
+ // Check if data.groupElement is valid
369
+ if (!data.groupElement) {
370
+ console.error("Invalid group element for radio group:", group);
371
+ return; // Skip this iteration if the parent is not found
372
+ }
373
+
374
+ // Proceed to add error message if the parent exists
375
+ addErrorMessage(data.groupElement, message);
376
+ }
377
+ });
378
+
379
+
380
+
381
+
382
+ }
383
+
384
+
385
+
386
+
387
+
388
+
389
+ //-------- for radio buttons checks only
390
+
391
+ //-------- For Multiple checkboxes without puttin require
392
+
393
+
394
+ Object.keys(checkboxGroups).forEach(name => {
395
+ const checkboxes = checkboxGroups[name];
396
+
397
+ // Skip validation if any checkbox in the group has data-ignore-kilvish attribute
398
+ const shouldIgnoreGroup = checkboxes.some(checkbox => checkbox.hasAttribute('data-ignore-kilvish'));
399
+ if (shouldIgnoreGroup) return;
400
+
401
+ const isChecked = checkboxes.some(checkbox => checkbox.checked);
402
+
403
+ if (!isChecked) {
404
+ isValid = false;
405
+ const message = requiredCheckboxes.has(name)
406
+ ? "Please check this box to proceed"
407
+ : "Please select at least one option.";
408
+
409
+ // const parentElement = checkboxes[0].closest('.mb-3') || form.querySelector(`input[name="${name}"]`)?.closest('.mb-3') || form;
410
+
411
+ // Use the closest('.form-check') for checkboxes to match previous behavior
412
+
413
+ // Find the closest parent container that holds the checkbox
414
+ const parentElement = checkboxes[0].closest('.form-group') || checkboxes[0].closest('.form-check') || checkboxes[0].closest('.checkbox-container') || checkboxes[0].closest('.some-other-container');
415
+ addErrorMessage(parentElement, message);
416
+
417
+ // addErrorMessage(checkboxes[0].closest('.form-check'), message);
418
+ }
419
+ });
420
+
421
+
422
+
423
+
424
+ // Validate checkboxes in select2 dropdown or similar groups
425
+ // select2.forEach(dropdown => {
426
+ // const checkboxesInDropdown = dropdown.querySelectorAll('input[type="checkbox"]:checked');
427
+ // const isValidCheckboxSelected = checkboxesInDropdown.length > 0;
428
+
429
+ // if (!isValidCheckboxSelected) {
430
+ // isValid = false;
431
+ // addErrorMessage(dropdown.closest('.form-group'), "Please select at least one option");
432
+ // }
433
+ // });
434
+
435
+
436
+ //============ select2 checkboxes ========================
437
+
438
+ return isValid;
439
+ }
440
+
441
+
442
+
443
+ function addRealTimeValidation(kil) {
444
+ const form = resolveForm(kil);
445
+ if (!form) return;
446
+ const inputs = form.querySelectorAll('input[required], select[required], textarea[required]');
447
+
448
+ const updateAsterisk = (input, add = true) => {
449
+ if (add) ensureStarVisibleFor(input);
450
+ else hideStarFor(input);
451
+ };
452
+
453
+
454
+ function removeError(input) {
455
+ const form = input.form || input.closest('form');
456
+ if (!form) return;
457
+
458
+ // resolve the same anchor key
459
+ const sib = input.nextElementSibling;
460
+ let anchor = input, mode = 'afterend', key = input.name || input.id || 'field';
461
+ if (input.tagName === 'SELECT' && sib && (sib.classList.contains('select2') || sib.classList.contains('select2-container'))) {
462
+ anchor = sib; mode = 'afterend'; key = input.name || input.id || 'select';
463
+ } else if (input.parentElement && input.parentElement.classList.contains('input-group')) {
464
+ anchor = input.parentElement; mode = 'afterend'; key = input.name || input.id || 'inputgroup';
465
+ }
466
+
467
+ const node = form.querySelector(`#kil_err_${String(key).replace(/[^a-zA-Z0-9_\-]/g,'_')}`);
468
+ if (node) {
469
+ node.textContent = '';
470
+ node.classList.add('is-hidden');
471
+ }
472
+ input.style.border = '';
473
+
474
+ // Hide star when valid (toggle look). If you prefer to always keep stars, comment this line.
475
+ hideStarFor(input);
476
+ }
477
+
478
+
479
+
480
+
481
+ // Handle standard required input/textarea/select fields
482
+ inputs.forEach(input => {
483
+ if (input.dataset.kilRealtimeBound === '1') return;
484
+ const validate = () => {
485
+ if (input.value.trim()) {
486
+ removeError(input);
487
+ updateAsterisk(input, false);
488
+ } else {
489
+ updateAsterisk(input, true);
490
+ }
491
+ };
492
+
493
+ input.addEventListener('input', validate);
494
+ input.addEventListener('change', validate);
495
+ input.dataset.kilRealtimeBound = '1';
496
+ });
497
+
498
+ // Handle radio groups
499
+ const radios = form.querySelectorAll('input[type="radio"]');
500
+ const radioNames = [...new Set(Array.from(radios).map(r => r.name))];
501
+
502
+ radioNames.forEach(name => {
503
+ const group = form.querySelectorAll(`input[type="radio"][name="${name}"]`);
504
+ group.forEach(radio => {
505
+ if (radio.dataset.kilRadioBound === '1') return;
506
+ radio.addEventListener('change', () => {
507
+ const isChecked = Array.from(group).some(r => r.checked);
508
+ if (isChecked) {
509
+ removeError(radio);
510
+ }
511
+ });
512
+ radio.dataset.kilRadioBound = '1';
513
+ });
514
+ });
515
+
516
+ // Handle checkbox groups
517
+ const checkboxes = form.querySelectorAll('input[type="checkbox"]');
518
+ const checkboxNames = [...new Set(Array.from(checkboxes).map(c => c.name))];
519
+
520
+ checkboxNames.forEach(name => {
521
+ const group = form.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
522
+ group.forEach(checkbox => {
523
+ if (checkbox.dataset.kilCheckboxBound === '1') return;
524
+ checkbox.addEventListener('change', () => {
525
+ const isChecked = Array.from(group).some(c => c.checked);
526
+ if (isChecked) {
527
+ removeError(checkbox);
528
+ }
529
+ });
530
+ checkbox.dataset.kilCheckboxBound = '1';
531
+ });
532
+ });
533
+ }
534
+
535
+
536
+
537
+
538
+ // Combined validation function
539
+ function validateForm(kil, evt) {
540
+ const ok = validateRequiredFields(kil);
541
+ if (!ok) {
542
+ const e = evt || window.event;
543
+ if (e && e.preventDefault) e.preventDefault();
544
+ }
545
+ return ok;
546
+ }
547
+
548
+
549
+
550
+
551
+ function normalizeRequiredStars(scope=document) {
552
+ scope.querySelectorAll('label, legend').forEach(label => {
553
+ // remove legacy inline red-star spans or bare * text nodes
554
+ label.querySelectorAll('span').forEach(s => {
555
+ if (!s.classList.contains('kil-required-star') && s.textContent.trim() === '*') s.remove();
556
+ });
557
+ // de-dupe our own stars
558
+ const stars = label.querySelectorAll('.kil-required-star');
559
+ for (let i = 1; i < stars.length; i++) stars[i].remove();
560
+ });
561
+ }
562
+
563
+ // Function to add red asterisk (*) to required fields
564
+ function addAsteriskToRequiredFields_working() {
565
+ const inputs = document.querySelectorAll('input, select, textarea'); // Select all input, select, and textarea elements
566
+ inputs.forEach(input => {
567
+ if (input.required) { // Check if the field is required
568
+
569
+
570
+ let label;
571
+ // Try to find label by 'for' attribute first (common case)
572
+ label = document.querySelector(`label[for='${input.name}']`);
573
+
574
+ // If not found, try to find the legend element (fieldset case)
575
+ if (!label) {
576
+ const fieldset = input.closest('fieldset');
577
+ if (fieldset) {
578
+ label = fieldset.querySelector('legend');
579
+ }
580
+ }
581
+
582
+ if (label && !label.innerHTML.includes('*')) {
583
+ // Append asterisk only if not already there
584
+ label.innerHTML += ' <span style="color:red;">*</span>';
585
+ }
586
+ }
587
+ });
588
+ }
589
+
590
+
591
+ // single-source: always use ensureStarVisibleFor()
592
+ function addAsteriskToRequiredFields() {
593
+ document.querySelectorAll('input[required], select[required], textarea[required]')
594
+ .forEach(ensureStarVisibleFor);
595
+ }
596
+
597
+ function attachInputEvents() {
598
+ const inputs = document.querySelectorAll('input, select, textarea'); // Select all input, select, and textarea elements
599
+ inputs.forEach(input => {
600
+ if (input.dataset.kilInputBound === '1') return;
601
+ if (!input.hasAttribute('data-ignore-kilvish')) { // Skip inputs with the custom attribute
602
+ input.addEventListener('keyup', validateKilvishInput);
603
+ input.addEventListener('change', validateKilvishInput);
604
+ input.dataset.kilInputBound = '1';
605
+ }
606
+ });
607
+ }
608
+
609
+
610
+
611
+ // Function to automatically add validateForm to form submit buttons
612
+ function addValidateFormToSubmitButtons() {
613
+ const forms = document.querySelectorAll('form'); // Get all forms on the page
614
+ forms.forEach(form => {
615
+ const formId = form.id;
616
+ if (form.dataset.kilFormSubmitBound !== '1') {
617
+ form.addEventListener('submit', (e) => validateForm(form, e));
618
+ form.dataset.kilFormSubmitBound = '1';
619
+ }
620
+ const target = formId ? `#${formId}` : form;
621
+ const submitButtons = form.querySelectorAll('button[type="submit"], input[type="submit"]');
622
+ submitButtons.forEach(button => {
623
+ if (button.dataset.kilSubmitBound === '1') return;
624
+ button.addEventListener('click', (e) => validateForm(target, e));
625
+ button.dataset.kilSubmitBound = '1';
626
+ });
627
+ });
628
+ }
629
+
630
+ let _kilEyeObserver = null;
631
+
632
+ function startEyeObserver() {
633
+ if (_kilEyeObserver || !window.MutationObserver) return;
634
+ _kilEyeObserver = new MutationObserver(muts => {
635
+ for (const m of muts) {
636
+ m.addedNodes && m.addedNodes.forEach(node => {
637
+ if (!(node instanceof HTMLElement)) return;
638
+ if (node.matches && node.matches('input[type="password"]')) ensureEyeForPassword(node);
639
+ node.querySelectorAll && node.querySelectorAll('input[type="password"]').forEach(ensureEyeForPassword);
640
+ });
641
+ }
642
+ });
643
+ _kilEyeObserver.observe(document.documentElement, { childList:true, subtree:true });
644
+ }
645
+
646
+ function initKilvalidate() {
647
+ normalizeRequiredStars();
648
+ addAsteriskToRequiredFields();
649
+
650
+ const forms = document.querySelectorAll('form'); // Get all forms on the page
651
+ forms.forEach(form => {
652
+ const formId = form.id;
653
+ if (formId) {
654
+ addRealTimeValidation(formId);
655
+ } else {
656
+ addRealTimeValidation(form);
657
+ }
658
+ });
659
+
660
+ // Attach events to all input fields
661
+ attachInputEvents();
662
+
663
+ // Add validation to submit buttons
664
+ addValidateFormToSubmitButtons();
665
+
666
+ // Password helpers
667
+ initPasswordStrengthMeter();
668
+ document.querySelectorAll('input[type="password"]').forEach(ensureEyeForPassword);
669
+ startEyeObserver();
670
+ }
671
+
672
+ if (document.readyState === 'loading') {
673
+ document.addEventListener('DOMContentLoaded', initKilvalidate);
674
+ } else {
675
+ initKilvalidate();
676
+ }
677
+
678
+
679
+
680
+ function validateKilvishInput(event) {
681
+ const inputField = event.target;
682
+ if (inputField.hasAttribute('data-ignore-kilvish')) return;
683
+
684
+ // --- local helper: clear this field's error slot without relying on wrapper classes
685
+ function clearFieldError(field) {
686
+ const form = field.form || field.closest('form');
687
+ if (!form) return;
688
+ const sib = field.nextElementSibling;
689
+ let key = field.name || field.id || 'field';
690
+ if (field.tagName === 'SELECT' && sib && (sib.classList.contains('select2') || sib.classList.contains('select2-container'))) {
691
+ key = field.name || field.id || 'select';
692
+ } else if (field.parentElement && field.parentElement.classList.contains('input-group')) {
693
+ key = field.name || field.id || 'inputgroup';
694
+ }
695
+ const node = form.querySelector(`#kil_err_${String(key).replace(/[^a-zA-Z0-9_\-]/g,'_')}`);
696
+ if (node) { node.textContent = ''; node.classList.add('is-hidden'); }
697
+ field.style.border = '';
698
+ }
699
+
700
+ // --- General valid character sets
701
+ let validCharacters = /^[a-zA-Z\s]*$/; // only char, no special char no numbers
702
+
703
+ // Define allowed image/file types
704
+ const validFilesTypes = {
705
+ image: ['image/jpeg','image/png','image/bmp','image/tiff','image/webp','image/svg+xml'],
706
+ doc: ['application/pdf','application/msword','application/vnd.openxmlformats-officedocument.wordprocessingml.document','application/vnd.ms-excel','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
707
+ pdf: ['application/pdf'],
708
+ mix: ['image/jpeg','image/png','image/bmp','image/tiff','image/webp','image/svg+xml','application/pdf','application/msword','application/vnd.openxmlformats-officedocument.wordprocessingml.document','application/vnd.ms-excel','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
709
+ only_pdf_image: ['application/pdf','image/jpeg','image/png','image/bmp','image/tiff','image/webp','image/svg+xml'],
710
+ only_doc_image: ['application/pdf','application/msword','application/vnd.openxmlformats-officedocument.wordprocessingml.document','application/vnd.ms-excel','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','image/jpeg','image/png','image/bmp','image/tiff','image/webp','image/svg+xml']
711
+ };
712
+
713
+ const fileTypeDisplayNames = {
714
+ image: 'Image',
715
+ doc: 'Document',
716
+ pdf: 'PDF',
717
+ mix: 'Image & Document',
718
+ only_pdf_image: 'PDF & Image',
719
+ only_doc_image: 'Document & Image'
720
+ };
721
+
722
+ // --- class-agnostic cleanup (no container lookups)
723
+ clearFieldError(inputField);
724
+
725
+ // --- Custom validations by field/name/type
726
+ if (inputField.name === 'description' || inputField.name === 'address') {
727
+ validCharacters = /^[a-zA-Z0-9@,._\s-'"*]*$/;
728
+ if (inputField.value.length > 400) {
729
+ addErrorMessage(inputField, 'Input exceeds the maximum length of 400 characters!');
730
+ inputField.style.border = '1px solid red';
731
+ inputField.value = inputField.value.substring(0, 400);
732
+ return;
733
+ }
734
+
735
+ } else if (inputField.name === 'firstname' || inputField.name === 'lastname' || inputField.name === 'appointment_by') {
736
+ validCharacters = /^[a-zA-Z\s-]*$/; // allow hyphen
737
+ const allowedChars = /^[a-zA-Z0-9._@-\s]+$/;
738
+ const inputValue = inputField.value;
739
+ if (allowedChars.test(inputValue)) {
740
+ if (inputValue.length > 20 || inputValue.length < 2) {
741
+ addErrorMessage(inputField, 'This Field Must Be Between 2 And 20 Characters.');
742
+ inputField.style.border = '1px solid red';
743
+ if (event.type === 'change') inputField.value = '';
744
+ return;
745
+ }
746
+ }
747
+ inputField.style.border = '';
748
+
749
+ } else if (inputField.name === 'otp') {
750
+ validCharacters = /^\d{6}$/;
751
+ const numericOnlyPattern = /^\d+$/;
752
+ const inputValue = inputField.value;
753
+ if (numericOnlyPattern.test(inputValue) && inputValue.length !== 6) {
754
+ addErrorMessage(inputField, 'OTP must be exactly 6 digits');
755
+ inputField.style.border = '1px solid red';
756
+ if (event.type === 'change') inputField.value = '';
757
+ return;
758
+ }
759
+ inputField.style.border = '';
760
+
761
+ } else if (inputField.name === 'number_of_items') {
762
+ validCharacters = /^\d{0,6}(\.\d{0,2})?$/;
763
+ const numericOnlyPattern = /^\d+$/;
764
+ const inputValue = inputField.value;
765
+ if (numericOnlyPattern.test(inputValue) && inputValue.length > 3) {
766
+ addErrorMessage(inputField, 'Pacakge Number Cannot Be Exceed 3 Digits');
767
+ inputField.style.border = '2px solid red';
768
+ if (event.type === 'change') inputField.value = '';
769
+ return;
770
+ }
771
+ inputField.style.border = '';
772
+
773
+ } else if (inputField.name === 'contact' || inputField.id === 'agent_contact' || inputField.name === 'fullcontact' ) {
774
+ validCharacters = /^[\d\-\s]{0,20}$/;
775
+
776
+ } else if (inputField.type === 'number') {
777
+ validCharacters = /^\d{0,20}$/;
778
+ const numericOnlyPattern = /^\d+$/;
779
+ const inputValue = inputField.value;
780
+ if (numericOnlyPattern.test(inputValue) && (inputValue.length > 20 || inputValue.length < 1)) {
781
+ addErrorMessage(inputField, 'This Must Be Between 1 And 20 Digits.');
782
+ inputField.style.border = '2px solid red';
783
+ if (event.type === 'change') inputField.value = '';
784
+ return;
785
+ }
786
+ inputField.style.border = '';
787
+
788
+ } else if (inputField.type === 'tel') {
789
+ let inputValue = inputField.value.trim().replace(/\s+/g, '');
790
+ inputField.value = inputValue;
791
+ const phonePattern = /^\+?\d{7,15}$/;
792
+ validCharacters = /^\+?\d{7,15}$/;
793
+ if (!phonePattern.test(inputValue)) {
794
+ addErrorMessage(inputField, 'Enter a valid phone number (7–15 digits, optional +).');
795
+ inputField.style.border = '2px solid red';
796
+ if (event.type === 'change') inputField.value = '';
797
+ return;
798
+ }
799
+ inputField.style.border = '';
800
+
801
+ } else if (inputField.name === 'kildate') {
802
+ validCharacters = /^(?:\d{4}[-\/]\d{2}[-\/]\d{2})$/;
803
+ const inputValue = inputField.value.trim();
804
+ const inputDate = new Date(inputValue.replace(/[-\/]/g, '/'));
805
+ const today = new Date(); today.setHours(0,0,0,0);
806
+ const dateRange = inputField.getAttribute('data-kilvish-date');
807
+ if (dateRange) {
808
+ const [minAge, maxAge] = dateRange.replace('_','').split('_').map(Number);
809
+ const minDate = new Date(today), maxDate = new Date(today);
810
+ minDate.setFullYear(today.getFullYear() - minAge);
811
+ maxDate.setFullYear(today.getFullYear() - maxAge);
812
+ if (inputDate > minDate || inputDate < maxDate) {
813
+ addErrorMessage(inputField, `Birthday cannot be less than ${minAge} years ago or more than ${maxAge} years in the future.`);
814
+ inputField.style.border = '2px solid red';
815
+ if (event.type === 'change') inputField.value = '';
816
+ return;
817
+ }
818
+ } else {
819
+ if (inputDate < today) {
820
+ addErrorMessage(inputField, 'The date cannot be in the past.');
821
+ inputField.style.border = '2px solid red';
822
+ if (event.type === 'change') inputField.value = '';
823
+ return;
824
+ }
825
+ }
826
+ inputField.style.border = '';
827
+
828
+ } else if (inputField.type === 'date' || inputField.type === 'month') {
829
+ validCharacters = /^(?:\d{4}[-\/]\d{2}[-\/]\d{2})$/;
830
+ const inputValue = inputField.value.trim();
831
+ const inputDate = (inputField.type === 'month')
832
+ ? new Date(inputValue + '-01')
833
+ : new Date(inputValue.replace(/[-\/]/g, '/'));
834
+ const today = new Date(); today.setHours(0,0,0,0);
835
+ const dateRange = inputField.getAttribute('data-kilvish-date');
836
+
837
+ if (dateRange) {
838
+ if (/^_(\d+)_(\d+)$/.test(dateRange)) {
839
+ const [minAge, maxAge] = dateRange.replace('_','').split('_').map(Number);
840
+ const minDate = new Date(today), maxDate = new Date(today);
841
+ minDate.setFullYear(today.getFullYear() - minAge);
842
+ maxDate.setFullYear(today.getFullYear() - maxAge);
843
+ if (inputDate > minDate || inputDate < maxDate) {
844
+ addErrorMessage(inputField, `Age must be between ${minAge} and ${maxAge} years.`);
845
+ inputField.style.border = '2px solid red';
846
+ if (event.type === 'change') inputField.value = '';
847
+ return;
848
+ }
849
+ } else if (/^_(\d+)$/.test(dateRange)) {
850
+ const exactAge = Number(dateRange.replace('_',''));
851
+ const exactDate = new Date(today); exactDate.setFullYear(today.getFullYear() - exactAge);
852
+ if (inputDate.getTime() !== exactDate.getTime()) {
853
+ addErrorMessage(inputField, `You must be exactly ${exactAge} years old.`);
854
+ inputField.style.border = '2px solid red';
855
+ if (event.type === 'change') inputField.value = '';
856
+ return;
857
+ }
858
+ } else if (/future/.test(dateRange)) {
859
+ if (dateRange === 'future' && inputDate <= today) {
860
+ addErrorMessage(inputField, 'The selected date must be in the future.');
861
+ inputField.style.border = '2px solid red';
862
+ if (event.type === 'change') inputField.value = '';
863
+ return;
864
+ } else if (dateRange === 'today_future' && inputDate < today) {
865
+ addErrorMessage(inputField, 'The selected date cannot be in the past.');
866
+ inputField.style.border = '2px solid red';
867
+ if (event.type === 'change') inputField.value = '';
868
+ return;
869
+ } else if (/(\d+)_days_future/.test(dateRange)) {
870
+ const days = Number(dateRange.match(/(\d+)_days_future/)[1]);
871
+ const maxFutureDate = new Date(today); maxFutureDate.setDate(today.getDate() + days);
872
+ if (inputDate > maxFutureDate || inputDate < today) {
873
+ addErrorMessage(inputField, `The selected date must be within the next ${days} days.`);
874
+ inputField.style.border = '2px solid red';
875
+ if (event.type === 'change') inputField.value = '';
876
+ return;
877
+ }
878
+ } else if (/^(\d+)_years_future$/.test(dateRange)) {
879
+ const yearsFuture = Number(dateRange.match(/^(\d+)_years_future$/)[1]);
880
+ const maxFutureDate = new Date(today); maxFutureDate.setFullYear(today.getFullYear() + yearsFuture);
881
+ if (inputDate < today || inputDate > maxFutureDate) {
882
+ addErrorMessage(inputField, `The date must be within the next ${yearsFuture} years.`);
883
+ inputField.style.border = '2px solid red';
884
+ if (event.type === 'change') inputField.value = '';
885
+ return;
886
+ }
887
+ } else if (/^(\d+)_years_after$/.test(dateRange)) {
888
+ const yearsAfter = Number(dateRange.match(/^(\d+)_years_after$/)[1]);
889
+ const afterDate = new Date(today); afterDate.setFullYear(today.getFullYear() + yearsAfter);
890
+ if (inputDate.getTime() !== afterDate.getTime()) {
891
+ addErrorMessage(inputField, `The date must be exactly ${yearsAfter} years after today.`);
892
+ inputField.style.border = '2px solid red';
893
+ if (event.type === 'change') inputField.value = '';
894
+ return;
895
+ }
896
+ }
897
+ } else if (/past/.test(dateRange)) {
898
+ if (dateRange === 'past' && inputDate >= today) {
899
+ addErrorMessage(inputField, 'The selected date must be in the past.');
900
+ inputField.style.border = '2px solid red';
901
+ if (event.type === 'change') inputField.value = '';
902
+ return;
903
+ } else if (/^(\d+)_years_past$/.test(dateRange)) {
904
+ const yearsPast = Number(dateRange.match(/^(\d+)_years_past$/)[1]);
905
+ const maxPastDate = new Date(today); maxPastDate.setFullYear(today.getFullYear() - yearsPast);
906
+ if (inputDate > today || inputDate < maxPastDate) {
907
+ addErrorMessage(inputField, `The date must be within the past ${yearsPast} years.`);
908
+ inputField.style.border = '2px solid red';
909
+ if (event.type === 'change') inputField.value = '';
910
+ return;
911
+ }
912
+ }
913
+ } else if (/^(\d+)_years_before$/.test(dateRange)) {
914
+ const yearsBefore = Number(dateRange.match(/^(\d+)_years_before$/)[1]);
915
+ const beforeDate = new Date(today); beforeDate.setFullYear(today.getFullYear() - yearsBefore);
916
+ if (inputDate >= beforeDate) {
917
+ addErrorMessage(inputField, `The date must be before ${yearsBefore} years ago.`);
918
+ inputField.style.border = '2px solid red';
919
+ if (event.type === 'change') inputField.value = '';
920
+ return;
921
+ }
922
+ } else if (/^(\d+)_years_after$/.test(dateRange)) {
923
+ const yearsAfter = Number(dateRange.match(/^(\d+)_years_after$/)[1]);
924
+ const afterDate = new Date(today); afterDate.setFullYear(today.getFullYear() + yearsAfter);
925
+ if (inputDate <= afterDate) {
926
+ addErrorMessage(inputField, `The date must be after ${yearsAfter} years from today.`);
927
+ inputField.style.border = '2px solid red';
928
+ if (event.type === 'change') inputField.value = '';
929
+ return;
930
+ }
931
+ } else if (/Between_(\d+)_(\d+)/.test(dateRange)) {
932
+ const [startYear, endYear] = dateRange.match(/Between_(\d+)_(\d+)/).slice(1,3).map(Number);
933
+ const minRangeDate = new Date(startYear,0,1);
934
+ const maxRangeDate = new Date(endYear,11,31);
935
+ if (inputDate < minRangeDate || inputDate > maxRangeDate) {
936
+ addErrorMessage(inputField, `The date must be between ${startYear} and ${endYear}.`);
937
+ inputField.style.border = '2px solid red';
938
+ if (event.type === 'change') inputField.value = '';
939
+ return;
940
+ }
941
+ } else if (/from_(\d+)/.test(dateRange)) {
942
+ const fromYear = Number(dateRange.match(/from_(\d+)/)[1]);
943
+ const fromDate = new Date(fromYear,0,1);
944
+ if (inputDate < fromDate) {
945
+ addErrorMessage(inputField, `The date must be from the year ${fromYear} onwards.`);
946
+ inputField.style.border = '2px solid red';
947
+ if (event.type === 'change') inputField.value = '';
948
+ return;
949
+ }
950
+ } else if (/till_(\d+)/.test(dateRange)) {
951
+ const tillYear = Number(dateRange.match(/till_(\d+)/)[1]);
952
+ const tillDate = new Date(tillYear,11,31);
953
+ if (inputDate > tillDate) {
954
+ addErrorMessage(inputField, `The date must be till the year ${tillYear}.`);
955
+ inputField.style.border = '2px solid red';
956
+ if (event.type === 'change') inputField.value = '';
957
+ return;
958
+ }
959
+ } else if (dateRange === 'not_today' && inputDate.getTime() === today.getTime()) {
960
+ addErrorMessage(inputField, "Today's date is not allowed.");
961
+ inputField.style.border = '2px solid red';
962
+ if (event.type === 'change') inputField.value = '';
963
+ return;
964
+ } else if (dateRange === 'weekday' && (inputDate.getDay() === 0 || inputDate.getDay() === 6)) {
965
+ addErrorMessage(inputField, 'Only weekdays (Monday to Friday) are allowed.');
966
+ inputField.style.border = '2px solid red';
967
+ if (event.type === 'change') inputField.value = '';
968
+ return;
969
+ } else if (dateRange === 'weekend' && (inputDate.getDay() >= 1 && inputDate.getDay() <= 5)) {
970
+ addErrorMessage(inputField, 'Only weekends (Saturday and Sunday) are allowed.');
971
+ inputField.style.border = '2px solid red';
972
+ if (event.type === 'change') inputField.value = '';
973
+ return;
974
+ } else if (dateRange === 'first_of_month' && inputDate.getDate() !== 1) {
975
+ addErrorMessage(inputField, 'The date must be the first day of the month.');
976
+ inputField.style.border = '2px solid red';
977
+ if (event.type === 'change') inputField.value = '';
978
+ return;
979
+ } else if (/^exact_(\d+)$/.test(dateRange)) {
980
+ const exactAge = Number(dateRange.match(/^exact_(\d+)$/)[1]);
981
+ const exactDate = new Date(today); exactDate.setFullYear(today.getFullYear() - exactAge);
982
+ if (inputDate.getTime() !== exactDate.getTime()) {
983
+ addErrorMessage(inputField, `You must be exactly ${exactAge} years old.`);
984
+ inputField.style.border = '2px solid red';
985
+ if (event.type === 'change') inputField.value = '';
986
+ return;
987
+ }
988
+ } else if (dateRange === 'last_of_month') {
989
+ const lastDay = new Date(inputDate.getFullYear(), inputDate.getMonth()+1, 0).getDate();
990
+ if (inputDate.getDate() !== lastDay) {
991
+ addErrorMessage(inputField, 'The date must be the last day of the month.');
992
+ inputField.style.border = '2px solid red';
993
+ if (event.type === 'change') inputField.value = '';
994
+ return;
995
+ }
996
+ }
997
+ }
998
+
999
+ // grouped date validation (FROM..TO style)
1000
+ const dateGroupAttr = inputField.getAttribute('data-kilvish-dategroup');
1001
+ if (dateGroupAttr && /.+_[12]$/.test(dateGroupAttr)) {
1002
+ const [groupKey, groupIndex] = dateGroupAttr.split('_');
1003
+ const counterpartIndex = groupIndex === '1' ? '2' : '1';
1004
+ const counterpart = document.querySelector(`[data-kilvish-dategroup="${groupKey}_${counterpartIndex}"]`);
1005
+ if (counterpart && counterpart.value) {
1006
+ const d1 = (inputField.type === 'month') ? new Date(inputField.value + '-01') : new Date(inputField.value.replace(/[-\/]/g,'/'));
1007
+ const d2 = (counterpart.type === 'month') ? new Date(counterpart.value + '-01') : new Date(counterpart.value.replace(/[-\/]/g,'/'));
1008
+ const ok = (groupIndex === '1' && d1 <= d2) || (groupIndex === '2' && d1 >= d2);
1009
+ if (!ok) {
1010
+ const thisName = inputField.dataset.kilvishDate_name || `Field (${groupKey}_${groupIndex})`;
1011
+ const otherName = counterpart.dataset.kilvishDate_name || `Field (${groupKey}_${counterpartIndex})`;
1012
+ addErrorMessage(inputField, `${thisName} must be ${groupIndex === '1' ? 'before or same as' : 'after or same as'} ${otherName}.`);
1013
+ inputField.style.border = '2px solid red';
1014
+ if (event.type === 'change') inputField.value = '';
1015
+ return;
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ inputField.style.border = '';
1021
+ return;
1022
+
1023
+ } else if (inputField.name === 'age') {
1024
+ const inputValue = inputField.value.trim();
1025
+ const ageValue = parseInt(inputValue, 10);
1026
+ if (isNaN(ageValue)) {
1027
+ addErrorMessage(inputField, 'Please enter a valid number for age.');
1028
+ inputField.style.border = '2px solid red';
1029
+ if (event.type === 'change') inputField.value = '';
1030
+ return;
1031
+ }
1032
+ const ageRule = inputField.getAttribute('data-kilvish-age') || '18';
1033
+ if (/^(\d+)$/.test(ageRule)) {
1034
+ const requiredAge = Number(ageRule);
1035
+ if (ageValue !== requiredAge) {
1036
+ addErrorMessage(inputField, `Age must be exactly ${requiredAge} years.`);
1037
+ inputField.style.border = '2px solid red';
1038
+ if (event.type === 'change') inputField.value = '';
1039
+ return;
1040
+ }
1041
+ } else if (/^(\d+)_(\d+)$/.test(ageRule)) {
1042
+ const [minAge, maxAge] = ageRule.split('_').map(Number);
1043
+ if (ageValue < minAge) {
1044
+ addErrorMessage(inputField, `Minimum age must be ${minAge} years.`);
1045
+ inputField.style.border = '2px solid red';
1046
+ if (event.type === 'change') inputField.value = '';
1047
+ return;
1048
+ } else if (ageValue > maxAge) {
1049
+ addErrorMessage(inputField, `Age cannot exceed ${maxAge} years.`);
1050
+ inputField.style.border = '2px solid red';
1051
+ if (event.type === 'change') inputField.value = '';
1052
+ return;
1053
+ }
1054
+ }
1055
+ inputField.style.border = '';
1056
+ return;
1057
+ }
1058
+
1059
+ // model_name / models[]
1060
+ if (inputField.name === 'model_name' || inputField.name === 'models[]') {
1061
+ validCharacters = /^[a-zA-Z0-9]{4,20}$/;
1062
+ } else if (inputField.type === 'email') {
1063
+ validCharacters = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
1064
+ const allowedEmailChars = /^[a-zA-Z0-9._@-]+$/;
1065
+ const inputValue = inputField.value;
1066
+ if (allowedEmailChars.test(inputValue) && !validCharacters.test(inputValue)) {
1067
+ addErrorMessage(inputField, 'Invalid Email Format.');
1068
+ inputField.style.border = '1px solid red';
1069
+ if (event.type === 'change') inputField.value = '';
1070
+ return;
1071
+ }
1072
+ inputField.style.border = '';
1073
+ }
1074
+
1075
+ // data-kilvish-num
1076
+ if (inputField.hasAttribute('data-kilvish-num')) {
1077
+ const inputValue = inputField.value;
1078
+ const kilvishType = inputField.getAttribute('data-kilvish-num');
1079
+ validCharacters = /^[0-9]*$/;
1080
+ let min = null, max = null;
1081
+ const m = kilvishType.match(/^_(\d+)_(\d+)$/);
1082
+ if (m) { min = parseInt(m[1],10); max = parseInt(m[2],10); }
1083
+ const exactM = kilvishType.match(/^exact_(\d+)$/);
1084
+ const exactLength = exactM ? parseInt(exactM[1],10) : null;
1085
+
1086
+ if (!/^\d*$/.test(inputValue)) {
1087
+ addErrorMessage(inputField, 'Only numerical characters are allowed.');
1088
+ inputField.style.border = '2px solid red';
1089
+ } else if (exactLength !== null && inputValue.length !== exactLength) {
1090
+ addErrorMessage(inputField, `This field must contain exactly ${exactLength} digits.`);
1091
+ inputField.style.border = '2px solid red';
1092
+ if (event.type === 'change') inputField.value = '';
1093
+ } else if ((min && inputValue.length < min) || (max && inputValue.length > max)) {
1094
+ addErrorMessage(inputField, `This Field Must be between ${min} and ${max} digits.`);
1095
+ inputField.style.border = '2px solid red';
1096
+ if (event.type === 'change') inputField.value = '';
1097
+ } else {
1098
+ inputField.style.border = '';
1099
+ }
1100
+ }
1101
+
1102
+ // data-kilvish-num11
1103
+ if (inputField.hasAttribute('data-kilvish-num11')) {
1104
+ const inputValue = inputField.value;
1105
+ const kilvishType = inputField.getAttribute('data-kilvish-num');
1106
+ validCharacters = /^[0-9]*$/;
1107
+ let min = null, max = null;
1108
+ const m = kilvishType ? kilvishType.match(/^_(\d+)_(\d+)$/) : null;
1109
+ if (m) { min = parseInt(m[1],10); max = parseInt(m[2],10); }
1110
+ if (kilvishType === 'exact_20') validCharacters = /^[0-9]{20}$/;
1111
+
1112
+ if (!validCharacters.test(inputValue)) {
1113
+ addErrorMessage(inputField, kilvishType === 'exact_20' ? 'This field must contain exactly 20 digits.' : 'Only numerical characters are allowed.');
1114
+ inputField.style.border = '2px solid red';
1115
+ } else if ((min && inputValue.length < min) || (max && inputValue.length > max)) {
1116
+ addErrorMessage(inputField, `This Field Must be between ${min} and ${max} digits.`);
1117
+ inputField.style.border = '2px solid red';
1118
+ if (event.type === 'change') inputField.value = '';
1119
+ } else {
1120
+ inputField.style.border = '';
1121
+ }
1122
+ }
1123
+
1124
+ // data-kilvish-char
1125
+ if (inputField.hasAttribute('data-kilvish-char')) {
1126
+ const inputValue = inputField.value;
1127
+ const charType = inputField.getAttribute('data-kilvish-char');
1128
+ if (charType.startsWith('Yes') || charType.startsWith('mix')) {
1129
+ if (charType === 'mix') validCharacters = /^[a-zA-Z0-9]*$/;
1130
+ else if (charType === 'Yes') validCharacters = /^[a-zA-Z]*$/;
1131
+ const specialChars = charType.split('_').slice(1).join('');
1132
+ if (specialChars) validCharacters = new RegExp(`^[a-zA-Z0-9${specialChars}]*$`);
1133
+ if (!validCharacters.test(inputValue)) {
1134
+ addErrorMessage(inputField, 'Invalid character entered!');
1135
+ inputField.style.border = '2px solid red';
1136
+ if (event.type === 'change') inputField.value = '';
1137
+ return;
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ // data-kilvish-amount
1143
+ if (inputField.hasAttribute('data-kilvish-amount')) {
1144
+ const kilvishType = inputField.getAttribute('data-kilvish-amount');
1145
+ validCharacters = /^\d+(\.\d{0,2})?$/;
1146
+ let currency = 'USD', minAmount = null, maxAmount = null;
1147
+ const m = kilvishType.match(/^([A-Z]{3})(?:_(\d+)_(\d+))?$/);
1148
+ if (m) { currency = m[1]; minAmount = m[2] ? parseFloat(m[2]) : null; maxAmount = m[3] ? parseFloat(m[3]) : null; }
1149
+
1150
+ const formatAmount = () => {
1151
+ let caret = inputField.selectionStart;
1152
+ let raw = inputField.value;
1153
+ if (!validCharacters.test(raw)) {
1154
+ raw = raw.replace(/[^0-9.]/g,'').replace(/(\..*?)\..*/,'$1').replace(/(\.\d{2}).*/,'$1');
1155
+ }
1156
+ if (raw && !raw.includes('.') && !raw.endsWith('.')) raw = parseFloat(raw).toFixed(2);
1157
+ inputField.value = raw;
1158
+ const di = raw.indexOf('.'); if (di !== -1 && caret > di) caret = Math.min(caret, raw.length);
1159
+ inputField.setSelectionRange(caret, caret);
1160
+
1161
+ let display = inputField.nextElementSibling;
1162
+ if (!display || !display.classList.contains('amount-display')) {
1163
+ display = document.createElement('div');
1164
+ display.className = 'amount-display';
1165
+ display.style.color = 'green';
1166
+ inputField.parentNode.insertBefore(display, inputField.nextSibling);
1167
+ }
1168
+ display.textContent = `${currency} ${raw || '0.00'} .`;
1169
+ };
1170
+
1171
+ const validateAmountOnBlur = () => {
1172
+ if (inputField.readOnly) return;
1173
+ let finalValue = parseFloat(inputField.value || '0').toFixed(2);
1174
+ let display = inputField.nextElementSibling;
1175
+ if (minAmount !== null && finalValue < minAmount) {
1176
+ addErrorMessage(inputField, `Amount must be at least ${minAmount.toFixed(2)} ${currency}.`);
1177
+ inputField.style.border = '2px solid red';
1178
+ inputField.value = '';
1179
+ if (display) display.textContent = '';
1180
+ return;
1181
+ }
1182
+ if (maxAmount !== null && finalValue > maxAmount) {
1183
+ addErrorMessage(inputField, `Amount must not exceed ${maxAmount.toFixed(2)} ${currency}.`);
1184
+ inputField.style.border = '2px solid red';
1185
+ inputField.value = '';
1186
+ if (display) display.textContent = '';
1187
+ return;
1188
+ }
1189
+ inputField.style.border = '';
1190
+ inputField.value = finalValue;
1191
+ };
1192
+
1193
+ if (inputField.dataset.kilAmountBound !== '1') {
1194
+ inputField.addEventListener('input', formatAmount);
1195
+ inputField.addEventListener('blur', validateAmountOnBlur);
1196
+ inputField.dataset.kilAmountBound = '1';
1197
+ }
1198
+ if (event.type === 'keyup' || event.type === 'change') {
1199
+ formatAmount();
1200
+ }
1201
+ }
1202
+
1203
+
1204
+
1205
+
1206
+ if (inputField.type === 'password') {
1207
+ // Always render UI directly *under* the field (or its .input-group)
1208
+ const { bars, label: strengthLabel, rulesUl } = ensurePassUI(inputField);
1209
+
1210
+ // Read constraints like data-kilvish-password="12_64" (min_max). Defaults to 12–128.
1211
+ const constraintAttr = inputField.getAttribute("data-kilvish-password") || "12";
1212
+ const [minStr, maxStr] = constraintAttr.split("_");
1213
+ const minLength = parseInt(minStr, 10) || 12;
1214
+ const maxLength = parseInt(maxStr || "128", 10);
1215
+
1216
+ // Overall regex (lower, upper, digit, special, within length)
1217
+ const regexPattern = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_]).{${minLength},${maxLength}}$`;
1218
+ validCharacters = new RegExp(regexPattern);
1219
+
1220
+ // Rules list (shown under field)
1221
+ const rules = {
1222
+ length: `Password must be between ${minLength} and ${maxLength} characters long`,
1223
+ uppercase: "At least one uppercase letter (A-Z)",
1224
+ lowercase: "At least one lowercase letter (a-z)",
1225
+ number: "At least one number (0-9)",
1226
+ special: "At least one special character (!@#$%^&*)",
1227
+ sequential: "No sequential characters (e.g., 1234, abcd)"
1228
+ };
1229
+
1230
+ // Ensure LI items exist for each rule under this field
1231
+ const ruleElements = {};
1232
+ Object.keys(rules).forEach((key) => {
1233
+ let li = rulesUl.querySelector(`li[data-rule="${key}"]`);
1234
+ if (!li) {
1235
+ li = document.createElement("li");
1236
+ li.setAttribute("data-rule", key);
1237
+ li.textContent = `❌ ${rules[key]}`;
1238
+ li.style.color = "red";
1239
+ li.style.fontSize = "12px";
1240
+ rulesUl.appendChild(li);
1241
+ }
1242
+ ruleElements[key] = li;
1243
+ });
1244
+
1245
+ // Helper: detect sequential runs (abcd / 1234 or reverse)
1246
+ const hasSequentialChars = (password) => {
1247
+ for (let i = 0; i < password.length - 3; i++) {
1248
+ if (
1249
+ password.charCodeAt(i + 1) === password.charCodeAt(i) + 1 &&
1250
+ password.charCodeAt(i + 2) === password.charCodeAt(i) + 2 &&
1251
+ password.charCodeAt(i + 3) === password.charCodeAt(i) + 3
1252
+ ) return true;
1253
+ if (
1254
+ password.charCodeAt(i + 1) === password.charCodeAt(i) - 1 &&
1255
+ password.charCodeAt(i + 2) === password.charCodeAt(i) - 2 &&
1256
+ password.charCodeAt(i + 3) === password.charCodeAt(i) - 3
1257
+ ) return true;
1258
+ }
1259
+ return false;
1260
+ };
1261
+
1262
+ // Strength scoring (simple, consistent with rules)
1263
+ const checkStrength = (pwd) => {
1264
+ let s = 0;
1265
+ if (pwd.length >= minLength) s += 2;
1266
+ if (/[A-Z]/.test(pwd)) s += 1;
1267
+ if (/[a-z]/.test(pwd)) s += 1;
1268
+ if (/\d/.test(pwd)) s += 1;
1269
+ if (/[\W_]/.test(pwd)) s += 1;
1270
+ if (hasSequentialChars(pwd)) s -= 2;
1271
+ return Math.max(0, s);
1272
+ };
1273
+
1274
+ const password = inputField.value || "";
1275
+
1276
+ // Evaluate rule-by-rule
1277
+ const validations = {
1278
+ length: password.length >= minLength && password.length <= maxLength,
1279
+ uppercase: /[A-Z]/.test(password),
1280
+ lowercase: /[a-z]/.test(password),
1281
+ number: /\d/.test(password),
1282
+ special: /[\W_]/.test(password),
1283
+ sequential: !hasSequentialChars(password)
1284
+ };
1285
+
1286
+ // Update rule list UI
1287
+ Object.keys(validations).forEach((key) => {
1288
+ const ok = validations[key];
1289
+ ruleElements[key].textContent = `${ok ? "✅" : "❌"} ${rules[key]}`;
1290
+ ruleElements[key].style.color = ok ? "green" : "red";
1291
+ });
1292
+
1293
+ // Update meter bars + label
1294
+ const strength = checkStrength(password);
1295
+ bars.forEach(b => b.style.background = "lightgray");
1296
+ if (strength <= 2) {
1297
+ if (bars[0]) bars[0].style.background = "red";
1298
+ strengthLabel.textContent = "Weak";
1299
+ strengthLabel.style.color = "red";
1300
+ } else if (strength <= 4) {
1301
+ if (bars[0]) bars[0].style.background = "yellow";
1302
+ if (bars[1]) bars[1].style.background = "yellow";
1303
+ strengthLabel.textContent = "Medium";
1304
+ strengthLabel.style.color = "orange";
1305
+ } else {
1306
+ if (bars[0]) bars[0].style.background = "green";
1307
+ if (bars[1]) bars[1].style.background = "green";
1308
+ if (bars[2]) bars[2].style.background = "green";
1309
+ strengthLabel.textContent = "Strong";
1310
+ strengthLabel.style.color = "green";
1311
+ }
1312
+
1313
+ // Final validity & border color
1314
+ const isValid = validCharacters.test(password) &&
1315
+ Object.values(validations).every(Boolean);
1316
+
1317
+ inputField.style.border = isValid ? "1px solid green" : "1px solid red";
1318
+
1319
+ // On commit (change), clear invalid
1320
+ if (!isValid && event.type === "change") {
1321
+ inputField.value = "";
1322
+ }
1323
+
1324
+ // Important: stop further generic validation for this field
1325
+ return;
1326
+ }
1327
+
1328
+
1329
+
1330
+ // Files
1331
+ if (inputField.type === 'file') {
1332
+ const kilvishFileType = inputField.getAttribute('data-kilvish-file') || 'image';
1333
+ const [type, ...minMax] = kilvishFileType.split('_');
1334
+ const validTypes = validFilesTypes[kilvishFileType] || validFilesTypes[type] || validFilesTypes.image;
1335
+ const files = inputField.files;
1336
+
1337
+ let min = 1, max = Infinity;
1338
+ if (minMax.length >= 2) { min = parseInt(minMax[0],10); max = parseInt(minMax[1],10); }
1339
+
1340
+ if (files.length < min || files.length > max) {
1341
+ addErrorMessage(inputField, `Please select between ${min} and ${max} valid file(s).`);
1342
+ inputField.value = '';
1343
+ inputField.style.border = '1px solid red';
1344
+ return false;
1345
+ }
1346
+
1347
+ const displayType = fileTypeDisplayNames[kilvishFileType] || type.toUpperCase();
1348
+ for (const file of files) {
1349
+ if (!validTypes.includes(file.type)) {
1350
+ addErrorMessage(inputField, `Invalid file type. Allowed types: ${displayType}.`);
1351
+ inputField.value = '';
1352
+ inputField.style.border = '1px solid red';
1353
+ return false;
1354
+ }
1355
+ }
1356
+ } else {
1357
+ // Generic typing/commit validation
1358
+ if (event.type === 'keyup') {
1359
+ if (!validCharacters.test(inputField.value)) {
1360
+ if (inputField.type !== 'password') addErrorMessage(inputField, 'Invalid character entered!');
1361
+ inputField.style.border = '1px solid red';
1362
+ }
1363
+ }
1364
+ if (event.type === 'change') {
1365
+ if (!validCharacters.test(inputField.value)) {
1366
+ inputField.value = '';
1367
+ addErrorMessage(inputField, 'Invalid character or format!');
1368
+ inputField.style.border = '1px solid red';
1369
+ } else {
1370
+ inputField.style.border = '';
1371
+ clearFieldError(inputField); // class-agnostic clear on valid
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+
1378
+ function initPasswordStrengthMeter() {
1379
+ document.querySelectorAll("input[type='password']").forEach((inputField) => {
1380
+ if (inputField.hasAttribute('data-ignore-kilvish')) return;
1381
+ if (inputField.dataset.kilPassStrengthBound === '1') return;
1382
+
1383
+ const { bars, label: strengthLabel } = ensurePassUI(inputField);
1384
+
1385
+ const passwordConstraints = inputField.getAttribute("data-kilvish-password") || "12";
1386
+ const [minLength] = passwordConstraints.split("_").map(Number);
1387
+
1388
+ const hasSequentialChars = (password) => {
1389
+ for (let i = 0; i < password.length - 3; i++) {
1390
+ if (password.charCodeAt(i+1) === password.charCodeAt(i)+1 &&
1391
+ password.charCodeAt(i+2) === password.charCodeAt(i)+2 &&
1392
+ password.charCodeAt(i+3) === password.charCodeAt(i)+3) return true;
1393
+ if (password.charCodeAt(i+1) === password.charCodeAt(i)-1 &&
1394
+ password.charCodeAt(i+2) === password.charCodeAt(i)-2 &&
1395
+ password.charCodeAt(i+3) === password.charCodeAt(i)-3) return true;
1396
+ }
1397
+ return false;
1398
+ };
1399
+
1400
+ const checkStrength = (password) => {
1401
+ let s = 0;
1402
+ if (password.length >= minLength) s += 2;
1403
+ if (/[A-Z]/.test(password)) s += 1;
1404
+ if (/[a-z]/.test(password)) s += 1;
1405
+ if (/\d/.test(password)) s += 1;
1406
+ if (/[\W_]/.test(password)) s += 1;
1407
+ if (hasSequentialChars(password)) s -= 2;
1408
+ return Math.max(0, s);
1409
+ };
1410
+
1411
+ inputField.addEventListener("input", (e) => {
1412
+ const pwd = e.target.value;
1413
+ const strength = checkStrength(pwd);
1414
+
1415
+ // reset
1416
+ bars.forEach(b => b.style.background = 'lightgray');
1417
+
1418
+ if (strength <= 2) {
1419
+ bars[0].style.background = 'red';
1420
+ strengthLabel.textContent = 'Weak';
1421
+ strengthLabel.style.color = 'red';
1422
+ } else if (strength <= 4) {
1423
+ bars[0].style.background = 'yellow';
1424
+ bars[1].style.background = 'yellow';
1425
+ strengthLabel.textContent = 'Medium';
1426
+ strengthLabel.style.color = 'orange';
1427
+ } else {
1428
+ bars[0].style.background = 'green';
1429
+ bars[1].style.background = 'green';
1430
+ bars[2].style.background = 'green';
1431
+ strengthLabel.textContent = 'Strong';
1432
+ strengthLabel.style.color = 'green';
1433
+ }
1434
+ });
1435
+
1436
+ inputField.dataset.kilPassStrengthBound = '1';
1437
+ });
1438
+ }
1439
+
1440
+
1441
+
1442
+
1443
+ // Function to display error messages under the input field or container for checkboxes
1444
+ function addErrorMessage_working(parent, message) {
1445
+ if (!parent.querySelector('.error-message')) {
1446
+ const errorMessage = document.createElement('div');
1447
+ errorMessage.className = 'error-message';
1448
+ errorMessage.style.color = 'red';
1449
+ errorMessage.style.fontSize = '12px';
1450
+ errorMessage.innerText = message;
1451
+ parent.appendChild(errorMessage);
1452
+ }
1453
+ }
1454
+
1455
+
1456
+ function addErrorMessage_working2(target /* can be input or container */, message) {
1457
+ // Find anchor & unique key
1458
+ const form = (target.form) ? target.form : target.closest('form');
1459
+ if (!form) return;
1460
+
1461
+ const { anchor, mode, key } = getErrorAnchor(target);
1462
+
1463
+ // Persistent node under anchor
1464
+ const node = getOrCreateErrorNode(form, anchor, mode, key);
1465
+
1466
+ // Update text and reveal smoothly (no layout jump)
1467
+ node.textContent = message || '';
1468
+ node.classList.remove('is-hidden');
1469
+ }
1470
+
1471
+
1472
+ function addErrorMessage(target /* input or container */, message) {
1473
+ const form = (target.form) ? target.form : target.closest('form');
1474
+ if (!form) return;
1475
+
1476
+ // ------- find visual anchor for error placement -------
1477
+ let field = null;
1478
+ if (target && target.tagName && /^(INPUT|SELECT|TEXTAREA)$/.test(target.tagName)) {
1479
+ field = target;
1480
+ } else if (target && target.querySelector) {
1481
+ const fields = target.querySelectorAll('input,select,textarea');
1482
+ field = fields.length ? fields[fields.length - 1] : null;
1483
+ }
1484
+
1485
+ let anchor = target, mode = 'beforeend', key = 'container';
1486
+ if (field) {
1487
+ // select2 rendered node
1488
+ const sib = field.nextElementSibling;
1489
+ if (field.tagName === 'SELECT' && sib && (sib.classList.contains('select2') || sib.classList.contains('select2-container'))) {
1490
+ anchor = sib; mode = 'afterend'; key = field.name || field.id || 'select';
1491
+ }
1492
+ // bootstrap input-group
1493
+ else if (field.parentElement && field.parentElement.classList.contains('input-group')) {
1494
+ anchor = field.parentElement; mode = 'afterend'; key = field.name || field.id || 'inputgroup';
1495
+ }
1496
+ else {
1497
+ anchor = field; mode = 'afterend'; key = field.name || field.id || 'field';
1498
+ }
1499
+ }
1500
+
1501
+ const id = `kil_err_${String(key).replace(/[^a-zA-Z0-9_\-]/g,'_')}`;
1502
+ let node = form.querySelector(`#${id}`);
1503
+ if (!node) {
1504
+ node = document.createElement('div');
1505
+ node.id = id;
1506
+ node.className = 'error-message is-hidden';
1507
+ if (mode === 'beforeend') anchor.insertAdjacentElement('beforeend', node);
1508
+ else anchor.insertAdjacentElement('afterend', node);
1509
+ }
1510
+
1511
+ node.textContent = message || '';
1512
+ node.classList.remove('is-hidden');
1513
+
1514
+ // ------- re-show star on error -------
1515
+ if (field) {
1516
+ ensureStarVisibleFor(field);
1517
+ } else {
1518
+ const any = target.querySelector('input[required],select[required],textarea[required]') ||
1519
+ target.querySelector('input,select,textarea');
1520
+ if (any) ensureStarVisibleFor(any);
1521
+ }
1522
+ }
1523
+
1524
+
1525
+
1526
+
1527
+
1528
+ //====================== Command Kilvish validation End ===============================
1529
+
1530
+
1531
+ // --- Password UI helpers (ALWAYS render below field or its input-group) ---
1532
+ (function kilPassCSS(){
1533
+ if (document.getElementById('kil-pass-style')) return;
1534
+ const s = document.createElement('style');
1535
+ s.id = 'kil-pass-style';
1536
+ s.textContent = `
1537
+ .kil-passui { display:block; width:100%; margin-top:6px; }
1538
+ .kil-passui .password-strength { display:flex; align-items:center; gap:10px; }
1539
+ .kil-passui .password-strength .bars { display:flex; gap:4px; }
1540
+ .kil-passui .password-strength .bar { height:5px; width:30px; border-radius:3px; background:lightgray; }
1541
+ .kil-passui .password-validation { list-style:none; padding:0; margin:6px 0 0; }
1542
+ .kil-passui .password-validation li { font-size:12px; margin:2px 0; }
1543
+ `;
1544
+ document.head.appendChild(s);
1545
+ })();
1546
+
1547
+ function getFieldAnchorForUI(input) {
1548
+ // If wrapped in Bootstrap input-group, insert after the group
1549
+ if (input.parentElement && input.parentElement.classList.contains('input-group')) {
1550
+ return input.parentElement;
1551
+ }
1552
+ return input;
1553
+ }
1554
+
1555
+ // Creates (or reuses) a single container with bars + <ul class="password-validation">
1556
+ function ensurePassUI(input) {
1557
+ const anchor = getFieldAnchorForUI(input);
1558
+ const key = input.name || input.id || 'password';
1559
+ const id = `kil_passui_${String(key).replace(/[^a-zA-Z0-9_-]/g,'_')}`;
1560
+
1561
+ let wrap = anchor.parentNode.querySelector(`#${id}`);
1562
+ if (!wrap) {
1563
+ wrap = document.createElement('div');
1564
+ wrap.id = id;
1565
+ wrap.className = 'kil-passui';
1566
+
1567
+ // Strength row
1568
+ const strength = document.createElement('div');
1569
+ strength.className = 'password-strength';
1570
+
1571
+ const barsWrap = document.createElement('div');
1572
+ barsWrap.className = 'bars';
1573
+ const bar1 = document.createElement('div'); bar1.className = 'bar';
1574
+ const bar2 = document.createElement('div'); bar2.className = 'bar';
1575
+ const bar3 = document.createElement('div'); bar3.className = 'bar';
1576
+ barsWrap.append(bar1, bar2, bar3);
1577
+
1578
+ const label = document.createElement('span');
1579
+ label.className = 'label';
1580
+ label.textContent = 'Weak';
1581
+ label.style.fontSize = '12px';
1582
+
1583
+ strength.append(barsWrap, label);
1584
+
1585
+ // Rules list
1586
+ const rules = document.createElement('ul');
1587
+ rules.className = 'password-validation';
1588
+
1589
+ wrap.append(strength, rules);
1590
+ anchor.insertAdjacentElement('afterend', wrap);
1591
+ }
1592
+
1593
+ const bars = wrap.querySelectorAll('.bar');
1594
+ const label = wrap.querySelector('.password-strength .label');
1595
+ const rulesUl = wrap.querySelector('.password-validation');
1596
+ return { wrap, bars, label, rulesUl };
1597
+ }
1598
+
1599
+
1600
+ // ========== Password Eye (icon-inside input, sticky, colorable) ==========
1601
+ (function kilEyeCSS(){
1602
+ if (document.getElementById('kil-eye-style')) return;
1603
+ const s = document.createElement('style');
1604
+ s.id = 'kil-eye-style';
1605
+ s.textContent = `
1606
+ .kil-eye-rel { position: relative !important; overflow: visible !important; }
1607
+ .kil-eye-wrap{ position:relative; display:block; }
1608
+ .kil-eye-btn{
1609
+ position:absolute; top:50%; right:10px; transform:translateY(-50%);
1610
+ display:inline-flex; align-items:center; justify-content:center;
1611
+ background:transparent; border:0; padding:0; margin:0;
1612
+ height:auto; width:auto; line-height:1; cursor:pointer; outline:none;
1613
+ z-index: 10; /* <-- stays above focused input */
1614
+ pointer-events: auto;
1615
+ }
1616
+ .kil-eye-btn i{ font-size:16px; }
1617
+ .kil-eye-pad { padding-right: 38px !important; }
1618
+ .kil-eye-btn { flex: 0 0 auto; }
1619
+ `;
1620
+ document.head.appendChild(s);
1621
+ })();
1622
+
1623
+ /** Find an anchor to absolutely-position the icon.
1624
+ * - If inside .input-group ⇒ use that as relative container (we add .kil-eye-rel)
1625
+ * - Else wrap the <input> with a relative .kil-eye-wrap
1626
+ * Always adds padding-right to the specific input so text never overlaps the eye.
1627
+ */
1628
+ function getEyeAnchor(input){
1629
+ const ig = input.closest('.input-group');
1630
+ if (ig) {
1631
+ ig.classList.add('kil-eye-rel');
1632
+ input.classList.add('kil-eye-pad');
1633
+ return { anchor: ig };
1634
+ }
1635
+ // no input-group: wrap input so we can position inside
1636
+ if (!input.parentElement.classList.contains('kil-eye-wrap')) {
1637
+ const wrap = document.createElement('span');
1638
+ wrap.className = 'kil-eye-wrap';
1639
+ input.parentNode.insertBefore(wrap, input);
1640
+ wrap.appendChild(input);
1641
+ }
1642
+ input.classList.add('kil-eye-pad');
1643
+ return { anchor: input.parentElement }; // .kil-eye-wrap
1644
+ }
1645
+
1646
+
1647
+ function ensureEyeForPassword(input){
1648
+ if (input.dataset.kileyeAttached === '1') return;
1649
+ if (input.hasAttribute('data-kileye-false')) return;
1650
+
1651
+ const { anchor } = getEyeAnchor(input);
1652
+ // safety: prevent clipping when input is focused
1653
+ anchor.style.overflow = 'visible';
1654
+
1655
+ // Reuse if exists near this input
1656
+ let btn = anchor.querySelector('.kil-eye-btn');
1657
+ if (!btn) {
1658
+ btn = document.createElement('button');
1659
+ btn.type = 'button';
1660
+ btn.className = 'kil-eye-btn';
1661
+ btn.setAttribute('tabindex', '-1');
1662
+ btn.setAttribute('aria-label', 'Show/Hide password');
1663
+ btn.innerHTML = `<i class="fa fa-eye-slash" aria-hidden="true"></i>`;
1664
+ anchor.appendChild(btn);
1665
+ }
1666
+
1667
+ // Color support (per-field)
1668
+ const color = input.getAttribute('data-kileye-color');
1669
+ if (color) btn.style.color = color;
1670
+
1671
+ const icon = () => btn.querySelector('i');
1672
+
1673
+ function show(){
1674
+ input.type = 'text';
1675
+ if (icon()) { icon().classList.remove('fa-eye-slash'); icon().classList.add('fa-eye'); }
1676
+ }
1677
+ function hide(){
1678
+ input.type = 'password';
1679
+ if (icon()) { icon().classList.remove('fa-eye'); icon().classList.add('fa-eye-slash'); }
1680
+ }
1681
+ function toggle(){ (input.type === 'password') ? show() : hide(); }
1682
+
1683
+ // ── mode config: 'hold' | 'toggle' | 'fix' ────────────────────────────
1684
+ const modeAttr = (input.getAttribute('data-kileye-show') || 'hold').toLowerCase();
1685
+ const initialAttr = (input.getAttribute('data-kileye-initial') || 'auto').toLowerCase(); // 'auto'|'on'|'off'
1686
+
1687
+ // initial state: 'fix' => on, else respect data-kileye-initial, else off
1688
+ if (modeAttr === 'fix') {
1689
+ show();
1690
+ } else if (initialAttr === 'on') {
1691
+ show();
1692
+ } else if (initialAttr === 'off' || initialAttr === 'auto') {
1693
+ hide();
1694
+ }
1695
+
1696
+ // clear old listeners
1697
+ btn.onmousedown = btn.onmouseup = btn.onmouseleave = btn.onclick = null;
1698
+ btn.ontouchstart = btn.ontouchend = btn.ontouchcancel = null;
1699
+
1700
+ if (modeAttr === 'toggle' || modeAttr === 'fix') {
1701
+ // click/tap toggles visibility
1702
+ btn.onclick = (e)=>{ e.preventDefault(); toggle(); };
1703
+ btn.addEventListener('touchstart', (e)=>{ e.preventDefault(); toggle(); }, {passive:false});
1704
+ } else {
1705
+ // HOLD mode (default)
1706
+ btn.onmousedown = (e)=>{ e.preventDefault(); show(); };
1707
+ btn.onmouseup = ()=> hide();
1708
+ btn.onmouseleave= ()=> hide();
1709
+ btn.onblur = ()=> hide();
1710
+ // mobile hold
1711
+ btn.addEventListener('touchstart', (e)=>{ e.preventDefault(); show(); }, {passive:false});
1712
+ btn.addEventListener('touchend', ()=> hide());
1713
+ btn.addEventListener('touchcancel',()=> hide());
1714
+ }
1715
+
1716
+ // If input loses focus in HOLD mode, ensure hide
1717
+ input.addEventListener('blur', ()=> { if (modeAttr === 'hold') hide(); });
1718
+
1719
+ input.dataset.kileyeAttached = '1';
1720
+ }
1721
+
1722
+ return {
1723
+ validateRequiredFields,
1724
+ addRealTimeValidation,
1725
+ validateForm,
1726
+ addAsteriskToRequiredFields,
1727
+ attachInputEvents,
1728
+ addValidateFormToSubmitButtons,
1729
+ validateKilvishInput,
1730
+ addErrorMessage,
1731
+ init: initKilvalidate,
1732
+ initPasswordStrengthMeter,
1733
+ normalizeRequiredStars
1734
+ };
1735
+ });
1736
+