PageSourceSearch

https://topdentiq.com/public/assets/backend/file-validation/form-advance-validation.js

js topdentiq.com collected 2026-10-06 12:38:24 UTC 19,228 bytes, 520 lines download raw bytes

1"use strict";
2
3/**
4 * FormMagicValidation
5 * Initialize JustValidate on a form with auto rules for inputs, file validation, and helper methods.
6 * @param {HTMLFormElement} form
7 * @returns {Object} { check, errors, destroy, validator }
8 */
9function FormMagicValidation(form) {
10    if (!form) return null;
11
12    // Initialize JustValidate
13    const validator = new window.JustValidate(form, {
14        validateBeforeSubmitting: true,
15        lockForm: false,
16        focusInvalidField: false,
17        errorFieldCssClass: '',
18        errorLabelCssClass: 'just-validate-error-label',
19        errorLabelStyle: { color: 'red', fontSize: '0.85rem' },
20        successFieldCssClass: '',
21    });
22
23    // Remove all error/success labels dynamically
24    const observer = new MutationObserver(() => {
25        form.querySelectorAll('.just-validate-error-label, .just-validate-success-label')
26            .forEach(el => el.remove());
27    });
28    observer.observe(form, { childList: true, subtree: true });
29
30    // Optional: live validation observer
31    validator.onValidate(({ isValid, isSubmitted, fields }) => {
32        // Object.entries(fields)
33        // .filter(([_, f]) => !f.isValid)
34        // .forEach(([name, fieldState]) => {
35        //     if (fieldState.errors && fieldState.errors.length > 0) {
36        //         // Show a toast for each error message
37        //         fieldState.errors.forEach(msg => showErrorToast(msg));
38        //     } else {
39        //         // Fallback message
40        //         showErrorToast(`${name} is invalid`);
41        //     }
42        // });
43
44        // Debug individual field states (optional)
45        let errorsShowIndex = 0;
46        Object.entries(fields).forEach(([name, state]) => {
47            if (!state.isValid && state?.errorMessage) {
48                setTimeout(() => {
49                    // toastMagic.error(state?.errorMessage)
50                }, errorsShowIndex * 500);
51                errorsShowIndex++;
52            }
53        });
54    });
55
56    // Helper: get field label for error messages
57    const getFieldLabel = (input) => input.closest('.form-group')?.querySelector('label')?.textContent?.trim()
58        || input.placeholder || 'This field';
59
60    // Auto-add rules to inputs
61    form.querySelectorAll('input, textarea, select').forEach(input => {
62        if (!input.name || input.type === 'hidden' || input.hasAttribute('hidden')) return;
63
64        const name = input.name;
65        if (!name) return;
66        const rules = [];
67        if (form.classList.contains('form-advance-inputs-validation')) {
68            if (input.required && input.type === 'file') rules.push({ rule: 'required', errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required` });
69            if (input.type === 'email') rules.push({ rule: 'email', errorMessage: input.dataset.emailMsg || 'Please enter a valid email address' });
70            if (input.type === 'number' || input.inputMode === 'numeric') rules.push({ rule: 'number', errorMessage: input.dataset.numberMsg || 'Please enter a valid number' });
71            if (input.type === 'url') rules.push({ rule: 'customRegexp', value: /^https?:\/\/.+/, errorMessage: input.dataset.urlMsg || 'Please enter a valid URL' });
72            if (input.minLength) rules.push({ rule: 'minLength', value: parseInt(input.minLength), errorMessage: input.dataset.minlengthMsg || `Minimum ${input.minLength} characters` });
73            if (input.maxLength && parseInt(input.maxLength) > 0) rules.push({ rule: 'maxLength', value: parseInt(input.maxLength), errorMessage: input.dataset.maxlengthMsg || `Maximum ${input.maxLength} characters` });
74            if (input.min) rules.push({ rule: 'minNumber', value: parseFloat(input.min), errorMessage: input.dataset.minMsg || `Minimum value is ${input.min}` });
75            if (input.max) rules.push({ rule: 'maxNumber', value: parseFloat(input.max), errorMessage: input.dataset.maxMsg || `Maximum value is ${input.max}` });
76            if (input.pattern) rules.push({ rule: 'customRegexp', value: new RegExp(input.pattern), errorMessage: input.dataset.patternMsg || 'Invalid format' });
77
78            // Standard rules
79            // if (input.required && !input.classList.contains('select2-hidden-accessible')) {
80            //     rules.push({
81            //         rule: 'required',
82            //         errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required`
83            //     });
84            // }
85
86            // Select2 auto-detect
87            if (input.classList.contains('select2-hidden-accessible')) {
88                rules.push({
89                    rule: 'function',
90                    validator: (val, field) => {
91                        if (!field) return false;
92                        let value = $(field).val();
93
94                        // Ensure value is never undefined
95                        if (value === undefined || value === null) return false;
96
97                        // If it's an array (multi-select)
98                        if (Array.isArray(value)) return value.length > 0;
99
100                        // If it's a string
101                        if (typeof value === 'string') return value.trim() !== '';
102
103                        return false; // fallback for anything else
104                    },
105                    errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required`
106                });
107            }
108
109            // if (name.endsWith('[]')) {
110            //     // JustValidate will select all matching inputs
111            //     rules.push({
112            //         rule: 'function',
113            //         validator: (val, field) => {
114            //             // Get all inputs with this name
115            //             const inputs = form.querySelectorAll(`input[name="${name}"]`);
116            //             // Required: at least one filled
117            //             return Array.from(inputs).some(i => i.value && i.value.trim() !== '');
118            //         },
119            //         errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required`
120            //     });
121            //     validator.addField(`[name="${name}"]`, rules);
122            // } else {
123            //     validator.addField(`[name="${name}"]`, rules);
124            // }
125
126            if (name.endsWith('[]')) {
127                rules.push({
128                    rule: 'function',
129                    validator: (val, field) => {
130                        const inputs = form.querySelectorAll(`input[name="${name}"]`);
131                        return Array.from(inputs).some(i => i.value && i.value.trim() !== '');
132                    },
133                    errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required`
134                });
135            }
136        }
137
138        if (form.classList.contains('form-advance-file-validation')) {
139            // --- File input rules using JustValidate native 'files' rule ---
140            if (input.type?.toString() === 'file') {
141                // Add required first
142                if (input.required) {
143                    const accept = (input.getAttribute('accept') || '')
144                        .replace(/\|/g, ',')
145                        .split(',')
146                        .map(t => t.trim())
147                        .filter(Boolean);
148
149                    const allowedExtensions = accept
150                        .filter(a => a.startsWith('.'))
151                        .map(a => a.replace('.', ''));
152
153                    const allowedTypes = accept
154                        .filter(a => a.includes('/'))
155                        .map(a => a.toLowerCase());
156
157                    const maxSizeMb = parseInt(input.dataset.maxSize, 10);
158                    const maxSizeBytes = !isNaN(maxSizeMb) ? maxSizeMb * 1024 * 1024 : 5 * 1024 * 1024;
159
160                    rules.push({
161                            rule: 'minFilesCount',
162                            value: 1,
163                            errorMessage: input.dataset.requiredMsg || `${getFieldLabel(input)} is required`,
164                        },
165                        // {
166                        // rule: 'files',
167                        // value: {
168                        //     files: {
169                        //         extensions: allowedExtensions.length ? allowedExtensions : undefined,
170                        //         types: allowedTypes.length ? allowedTypes : undefined,
171                        //         maxSize: maxSizeBytes, // convert MB to bytes
172                        //     },
173                        // },
174                        // errorMessage: input.dataset.fileMsg || `${getFieldLabel(input)} is invalid`,
175                        // }
176                    );
177                }
178
179                // Optional: min/max files count (if you want to add)
180                if (input.dataset.minFiles) {
181                    rules.push({
182                        rule: 'minFilesCount',
183                        value: parseInt(input.dataset.minFiles, 10),
184                        errorMessage: `Select at least ${input.dataset.minFiles} file(s)`
185                    });
186                }
187                if (input.dataset.maxFiles) {
188                    rules.push({
189                        rule: 'maxFilesCount',
190                        value: parseInt(input.dataset.maxFiles, 10),
191                        errorMessage: `Select at most ${input.dataset.maxFiles} file(s)`
192                    });
193                }
194
195            }
196        }
197
198        if (rules.length > 0) {
199            validator.addField(`[name="${name}"]`, rules);
200        }
201
202        if (!rules.length) {
203            console.warn(`Skipping "${name}" — no validation rules detected.`);
204            return;
205        }
206    });
207
208    return {
209        /** Run validation and return boolean */
210        async check() {
211            try {
212                this.lastResult = await validator.validate(); // save it
213                return this.lastResult?.isValid === true;
214            } catch (e) {
215                console.warn(e);
216                return false;
217            }
218        },
219
220        errors() {
221            const list = [];
222
223            // ✅ Get fields from validator instance, not lastResult
224            const fields = validator.fields;
225
226            if (!fields) return list;
227
228            // Convert numeric keys (1, 2, 3) to entries
229            Object.entries(fields).forEach(([id, fieldState]) => {
230                if (!fieldState.isValid) {
231                    const message = fieldState.errorMessage ||
232                        (fieldState.errors?.[0]) ||
233                        `Field is invalid`;
234
235                    list.push({
236                        element: fieldState.elem,
237                        message: message,
238                        fieldName: fieldState.elem?.name || id
239                    });
240                }
241            });
242
243            return list;
244        },
245
246        /** Destroy observer and validator */
247        destroy() {
248            observer.disconnect();
249            validator.destroy?.();
250        },
251
252        /** Expose JustValidate instance */
253        validator
254    };
255}
256
257// --- Global form initialization ---
258const formValidators = new WeakMap();
259const formSubmitting = new WeakSet();
260
261function initFormValidation() {
262    document.querySelectorAll('.form-advance-validation').forEach(form => {
263        if (!formValidators.has(form)) {
264            formValidators.set(form, FormMagicValidation(form));
265        }
266    });
267}
268
269if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initFormValidation);
270else initFormValidation();
271
272if (typeof MutationObserver !== 'undefined') {
273    new MutationObserver(() => initFormValidation()).observe(document.body, { childList: true, subtree: true });
274}
275
276// --- Non-AJAX form submit handling ---
277document.addEventListener('submit', async function (e) {
278    const form = e.target.closest('.form-advance-validation.non-ajax-form-validate');
279    if (!form) return;
280
281    e.preventDefault();
282    if (formSubmitting.has(form)) return false;
283
284    const validator = formValidators.get(form);
285    if (!validator) return true;
286
287    formSubmitting.add(form);
288
289    const valid = await validator.check(); // await result
290    const errors = validator.errors();     // only works if you store lastResult inside check
291
292    if (!valid && errors?.length > 0) {
293        validator.errors().forEach((err, i) => setTimeout(() => showErrorToast(err.message), i * 400));
294        formSubmitting.delete(form);
295        return false;
296    }
297
298    formSubmitting.delete(form);
299    HTMLFormElement.prototype.submit.call(form);
300});
301
302// --- Toast helper ---
303function showErrorToast(msg) {
304    if (typeof toastMagic !== 'undefined'){
305        toastMagic.error(msg, '', true);
306    } else if(typeof toastr !== 'undefined'){
307        toastr.error(msg, '', true);
308    } else{
309        console.warn(msg);
310    }
311}
312
313
314/**
315 * Form validation helper
316 * @param {HTMLFormElement|jQuery} formElement
317 * @returns {Promise<boolean>} true if valid, false if invalid (and shows errors)
318 */
319async function validateFormHelper(formElement) {
320    const form = formElement instanceof jQuery ? formElement : $(formElement);
321    // Prevent double submission
322    if (formSubmitting.has(form)) return false;
323    const validator = formValidators.get(form[0]); // always get native DOM element
324    if (!validator) return true;
325    formSubmitting.add(form);
326
327    const valid = await validator.check();
328
329    if (!valid && validator.errors()?.length > 0) {
330        validator.errors().forEach((err, i) => setTimeout(() => showErrorToast(err.message), i * 400));
331        formSubmitting.delete(form);
332        return false;
333    }
334
335    return true;
336}
337
338
339/**
340 * Form helper to cleanup after submission
341 * @param {jQuery|HTMLFormElement} formElement
342 */
343function formSubmitCleanup(formElement) {
344    const form = formElement instanceof jQuery ? formElement : $(formElement);
345    formSubmitting.delete(form);
346}
347
348
349document.querySelectorAll('.form-advance-validation input[type="file"]').forEach(input => {
350    input.addEventListener('change', function () {
351
352        if (this.type !== 'file') return true;
353
354        const files = this.files;
355        const required = this.required;
356        const maxSizeMb = parseInt(this.dataset.maxSize || 3, 10);
357        const accept = (this.getAttribute('accept') || '')
358            .split(',')
359            .map(t => t.trim().toLowerCase())
360            .filter(Boolean);
361
362        // ✅ Clear any previous state or message if needed
363        let isValid = true;
364
365        // 🧩 Required check
366        if (required && (!files || files.length === 0)) {
367            isValid = false;
368            showErrorToast(`${getFieldLabel(this)} is required`);
369            removeInputConnectedElements(this);
370            return;
371        }
372
373        // Skip if optional and empty
374        if (!files || files.length === 0) return;
375
376        for (const f of files) {
377            // 🧩 Size check
378            if (f.size > maxSizeMb * 1024 * 1024) {
379                isValid = false;
380                showErrorToast(`${f.name} exceeds ${maxSizeMb}MB`);
381                removeInputConnectedElements(this);
382                break;
383            }
384
385            // 🧩 Type check
386            if (
387                accept.length &&
388                !accept.some(a =>
389                    a.startsWith('.')
390                        ? f.name.toLowerCase().endsWith(a)
391                        : f.type.toLowerCase() === a
392                )
393            ) {
394                isValid = false;
395                showErrorToast(`${f.name} has an invalid file type`);
396                removeInputConnectedElements(this);
397                break;
398            }
399        }
400
401        // 🚫 Reset the input if invalid
402        if (!isValid) {
403            this.value = ''; // clear selected file(s)
404        }
405    });
406});
407
408// ✅ Helper: extract label
409function getFieldLabel(input) {
410    return (
411        input.dataset.label ||
412        input.name ||
413        input.id ||
414        'This field'
415    ).replace(/[_\[\]]+/g, ' ').trim();
416}
417
418function removeInputConnectedElements(input) {
419    const container = input.closest('.custom_upload_input');
420    if (container) {
421        const deleteSections = container.querySelectorAll('.delete_file_input_section');
422        deleteSections.forEach(section => section.classList.add('d-none'));
423    }
424
425    const cardContainer = input.closest('.upload-file');
426    if (cardContainer) {
427        setTimeout(() => {
428            const uploadImgs = cardContainer.querySelectorAll('.upload-file-img');
429            const textboxes = cardContainer.querySelectorAll('.upload-file-textbox');
430            const overlays = cardContainer.querySelectorAll('.overlay');
431            const removeBtns = cardContainer.querySelectorAll('.remove_btn');
432
433            uploadImgs.forEach(img => {
434                img.style.display = 'none';
435                img.src = '';
436            });
437
438            textboxes.forEach(box => {
439                box.style.display = '';
440            });
441
442            overlays.forEach(overlay => {
443                overlay.classList.remove('show');
444            });
445
446            removeBtns.forEach(btn => {
447                btn.style.opacity = 0;
448            });
449
450            cardContainer.classList.remove('input-disabled');
451        }, 20)
452    }
453
454    let uploadZone = input.closest('.upload-zone');
455    if (uploadZone) {
456        setTimeout(() => {
457            const textboxes = uploadZone.querySelectorAll('.text-box');
458
459            textboxes.forEach(box => {
460                box.style.display = '';
461                box.classList.remove('d-none');
462            });
463
464        }, 20)
465    }
466}
467
468
469function xhrResponseManager(xhr) {
470    if (xhr?.responseJSON && xhr.responseJSON.error) {
471        toastMagic.error(xhr.responseJSON.error);
472    } else if (xhr?.responseJSON && xhr.responseJSON.errors) {
473        $.each(xhr?.responseJSON.errors, function (key, value) {
474            toastMagic.error(value);
475        });
476    } else {
477        toastMagic.error('An unexpected error occurred.');
478    }
479}
480function ajaxResponseManager(response) {
481    if (response.status === "success") {
482        if (response.message) {
483            toastMagic.success(response.message);
484        }
485
486        setTimeout(() => {
487            if (response?.redirectRoute) {
488                location.href = response.redirectRoute;
489            } else if (response?.redirect_url) {
490                location.href = response?.redirect_url;
491            }
492        }, 500)
493    } else if (response.status === "error") {
494        if (response.message) {
495            toastMagic.error(response.message);
496        }
497    } else if (response.status === "warning") {
498        if (response.message) {
499            toastMagic.warning(response.message);
500        }
501    }
502
503    if (response.errors) {
504        for (let index = 0; index < response.errors.length; index++) {
505            toastMagic.error(response.errors[index].message, {
506                CloseButton: true,
507                ProgressBar: true,
508            });
509        }
510    } else if (response.error) {
511        toastMagic.error(response.error, {
512            CloseButton: true,
513            ProgressBar: true,
514        });
515    }
516
517    if (response?.reload) {
518        location.reload();
519    }
520}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.