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.
- package/README_NEXTJS.md +90 -0
- package/kilnextvalidation-react.js +42 -0
- package/kilnextvalidation.js +1736 -0
- package/kilnextvalidation.min.js +2 -1
- package/package.json +12 -3
|
@@ -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
|
+
|