PageSourceSearch

https://www.mirela.bg/js/form-validation.js?v=1790088865

js mirela.bg collected 2026-09-24 18:48:14 UTC 14,817 bytes, 370 lines download raw bytes

1/**
2 * Shared form validation utilities.
3 * Loaded once globally; individual forms call window.FormValidation.* helpers.
4 */
5(function () {
6    'use strict';
7
8    /* ── validators ─────────────────────────────────────────── */
9
10    function validateName(name) {
11        var s = String(name || '').trim();
12        return /^([a-zA-Zа-яА-ЯёЁ'-]{2,})(\s+([a-zA-Zа-яА-ЯёЁ'-]{2,}))+\s*$/.test(s);
13    }
14
15    function validatePhone(phone) {
16        var clean = String(phone || '').replace(/\D/g, '');
17        return clean.length >= 8 && clean.length <= 12;
18    }
19
20    function validateEmail(email) {
21        return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(String(email || '').trim());
22    }
23
24    function validateEGN(egn) {
25        var s = String(egn || '').trim();
26        if (!/^\d{10}$/.test(s)) return false;
27        var yy = parseInt(s.slice(0, 2), 10);
28        var mm = parseInt(s.slice(2, 4), 10);
29        var dd = parseInt(s.slice(4, 6), 10);
30        var year = 1900 + yy;
31        if (mm >= 21 && mm <= 32) { year = 1800 + yy; mm -= 20; }
32        else if (mm >= 41 && mm <= 52) { year = 2000 + yy; mm -= 40; }
33        else if (mm < 1 || mm > 12) { return false; }
34        var d = new Date(year, mm - 1, dd);
35        if (d.getFullYear() !== year || (d.getMonth() + 1) !== mm || d.getDate() !== dd) return false;
36        var w = [2, 4, 8, 5, 10, 9, 7, 3, 6], sum = 0;
37        for (var i = 0; i < 9; i++) sum += parseInt(s[i], 10) * w[i];
38        var chk = sum % 11; if (chk === 10) chk = 0;
39        return chk === parseInt(s[9], 10);
40    }
41
42    /* ── UI helpers ─────────────────────────────────────────── */
43
44    function clearErrors(form) {
45        form.querySelectorAll('.is-invalid').forEach(function (el) { el.classList.remove('is-invalid'); });
46        form.querySelectorAll('.invalid-feedback').forEach(function (el) { el.remove(); });
47        form.querySelectorAll('.fv-pulsate').forEach(function (el) { el.classList.remove('fv-pulsate'); });
48    }
49
50    function showFieldError(el, message, opts) {
51        if (!el) return;
52        opts = opts || {};
53        if (!opts.skipInvalidClass) el.classList.add('is-invalid');
54        var fb = document.createElement('div');
55        fb.className = 'invalid-feedback' + (opts.block ? ' d-block' : '');
56        if ((el.type === 'checkbox' || el.type === 'radio') && opts.block) {
57            fb.classList.add('fv-consent-feedback');
58        }
59        fb.textContent = message || '';
60        var target = opts.appendTo || el.parentElement;
61        if (target) target.appendChild(fb);
62    }
63
64    function getScrollParent(el) {
65        var node = el ? el.parentElement : null;
66        while (node) {
67            if (node.classList && node.classList.contains('modal-body')) return node;
68            var style = window.getComputedStyle(node);
69            var overflowY = style.overflowY;
70            if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight) {
71                return node;
72            }
73            node = node.parentElement;
74        }
75        return null;
76    }
77
78    function scrollElementIntoView(el, offset) {
79        if (!el || !el.getBoundingClientRect) return;
80        offset = offset != null ? offset : 120;
81        var scrollParent = getScrollParent(el);
82
83        if (scrollParent && scrollParent !== document.documentElement && scrollParent !== document.body) {
84            var parentRect = scrollParent.getBoundingClientRect();
85            var elRect = el.getBoundingClientRect();
86            var targetTop = scrollParent.scrollTop + (elRect.top - parentRect.top) - offset;
87            try {
88                scrollParent.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
89            } catch (_e) {
90                scrollParent.scrollTop = Math.max(0, targetTop);
91            }
92            return;
93        }
94
95        var rect = el.getBoundingClientRect();
96        var top = rect.top + (window.pageYOffset || document.documentElement.scrollTop || 0) - offset;
97        try {
98            window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
99        } catch (_e) {
100            window.scrollTo(0, Math.max(0, top));
101        }
102    }
103
104    function fieldGroupForError(el) {
105        if (!el) return null;
106        if (el.classList && el.classList.contains('invalid-feedback')) {
107            return el.closest('.ip-group, .mb-3, .form-group, .form-check, .manage-search-terms-row, .manage-search-email-row, .subscribe-terms-row, .subscribe-email-row') || el.parentElement;
108        }
109        if (el.classList && el.classList.contains('fv-pulsate')) {
110            return el.closest('.ip-group, .mb-3, .form-group, .manage-search-terms-row, .manage-search-email-row, .subscribe-terms-row, .subscribe-email-row') || el;
111        }
112        return el;
113    }
114
115    function findFirstInvalidTarget(form) {
116        if (!form) return null;
117
118        function topOf(el) {
119            var rect = el.getBoundingClientRect();
120            if (!rect || rect.height === 0) return Infinity;
121            return rect.top + (window.pageYOffset || document.documentElement.scrollTop || 0);
122        }
123
124        var candidates = [];
125
126        form.querySelectorAll('.is-invalid').forEach(function (el) {
127            candidates.push({ top: topOf(el), el: el });
128        });
129
130        if (!candidates.length) {
131            form.querySelectorAll('.invalid-feedback').forEach(function (fb) {
132                var group = fieldGroupForError(fb);
133                if (group) candidates.push({ top: topOf(group), el: group });
134            });
135        }
136
137        if (!candidates.length) {
138            form.querySelectorAll('.fv-pulsate').forEach(function (el) {
139                var group = fieldGroupForError(el);
140                if (group) candidates.push({ top: topOf(group), el: group });
141            });
142        }
143
144        if (!candidates.length) return null;
145        candidates.sort(function (a, b) { return a.top - b.top; });
146        return candidates[0].el;
147    }
148
149    function focusFirstInvalid(form) {
150        var target = findFirstInvalidTarget(form);
151        if (!target) return;
152
153        scrollElementIntoView(target);
154
155        var focusEl = (target.matches && target.matches('input, textarea, select, button'))
156            ? target
157            : (target.querySelector ? target.querySelector('input:not([type="hidden"]), textarea, select, button') : null);
158        if (focusEl && focusEl.focus) {
159            try {
160                focusEl.focus({ preventScroll: true });
161            } catch (_e) {
162                try { focusEl.focus(); } catch (_e2) {}
163            }
164        }
165    }
166
167    /**
168     * Legacy-style pulsation: briefly scales the target element to draw attention.
169     * Used for consent checkbox / reCAPTCHA where no text message is shown.
170     */
171    function pulsateElement(el) {
172        if (!el) return;
173        el.classList.remove('fv-pulsate');
174        void el.offsetWidth;
175        el.classList.add('fv-pulsate');
176    }
177
178    function getErrorContainers(el) {
179        if (!el) return [];
180        var containers = [];
181        var parent = el.parentElement;
182        if (parent) containers.push(parent);
183        var scoped = el.closest('.ip-group, .mb-3, .form-group, .form-check, .manage-search-terms-row, .manage-search-email-row, .subscribe-terms-row, .subscribe-email-row');
184        if (scoped && containers.indexOf(scoped) === -1) containers.push(scoped);
185        return containers;
186    }
187
188    /** Clear error state from a single element. */
189    function clearFieldError(el) {
190        if (!el) return;
191        el.classList.remove('is-invalid');
192        getErrorContainers(el).forEach(function (container) {
193            container.querySelectorAll('.invalid-feedback').forEach(function (fb) { fb.remove(); });
194            container.classList.remove('fv-pulsate');
195        });
196    }
197
198    /**
199     * Attach live (blur / input) validation to specific fields.
200     *
201     * @param {Array} configs – each entry: { el, rules }
202     *   rules is an ordered array of { test: fn(value)->bool, msg: string }
203     *   The first failing rule wins.
204     *
205     * On blur  : validate and show error if invalid.
206     * On input : if an error is visible, re-validate so it clears immediately.
207     */
208    function setupLiveValidation(configs) {
209        configs.forEach(function (cfg) {
210            if (!cfg.el) return;
211
212            function getValue() {
213                if (typeof cfg.getValue === 'function') return cfg.getValue(cfg.el);
214                if (cfg.el.type === 'checkbox') return cfg.el.checked ? (cfg.el.value || '1') : '';
215                if (cfg.el.type === 'radio') return cfg.el.checked ? (cfg.el.value || '1') : '';
216                return (cfg.el.value || '').trim();
217            }
218
219            function hasVisibleError() {
220                if (cfg.el.classList.contains('is-invalid')) return true;
221                return getErrorContainers(cfg.el).some(function (container) {
222                    return !!(container.querySelector('.invalid-feedback') || container.classList.contains('fv-pulsate'));
223                });
224            }
225
226            function runCheck() {
227                clearFieldError(cfg.el);
228                var val = getValue();
229                for (var i = 0; i < cfg.rules.length; i++) {
230                    if (!cfg.rules[i].test(val)) {
231                        showFieldError(cfg.el, cfg.rules[i].msg, cfg.errorOpts || {});
232                        return false;
233                    }
234                }
235                return true;
236            }
237
238            cfg.el.addEventListener('blur', function () { runCheck(); });
239            cfg.el.addEventListener('input', function () {
240                if (hasVisibleError()) runCheck();
241            });
242            cfg.el.addEventListener('change', function () {
243                if (hasVisibleError()) runCheck();
244            });
245        });
246    }
247
248    function getAjaxErrorMessage(response, data, fallback) {
249        if (response && response.status === 413) {
250            return (window.__mirelaAjaxMessages && window.__mirelaAjaxMessages.payloadTooLarge)
251                || fallback
252                || 'The uploaded file is too large. Please choose a smaller file and try again.';
253        }
254
255        if (data && data.message) {
256            return data.message;
257        }
258
259        return fallback || '';
260    }
261
262    /* ── mobile keyboard: prefer Latin / email layout (iOS, Samsung) ── */
263
264    function isEmailInput(el) {
265        if (!el || el.tagName !== 'INPUT') return false;
266        if (String(el.getAttribute('data-role') || '').toLowerCase() === 'email') return true;
267        var type = String(el.getAttribute('type') || '').toLowerCase();
268        if (type === 'email') return true;
269        var name = String(el.getAttribute('name') || '').toLowerCase();
270        return name === 'email' || name.indexOf('email') !== -1;
271    }
272
273    function isPhoneInput(el) {
274        if (!el || el.tagName !== 'INPUT') return false;
275        if (String(el.getAttribute('data-role') || '').toLowerCase() === 'phone') return true;
276        var type = String(el.getAttribute('type') || '').toLowerCase();
277        if (type === 'tel') return true;
278        var name = String(el.getAttribute('name') || '').toLowerCase();
279        return name === 'phone' || name.indexOf('phone') !== -1;
280    }
281
282    function applyLatinKeyboardHints(el) {
283        if (!el || (!isEmailInput(el) && !isPhoneInput(el))) return;
284        el.setAttribute('lang', 'en');
285        el.setAttribute('dir', 'ltr');
286        if (isEmailInput(el)) {
287            if (!el.getAttribute('inputmode')) el.setAttribute('inputmode', 'email');
288            el.setAttribute('autocapitalize', 'off');
289            el.setAttribute('autocorrect', 'off');
290            el.setAttribute('spellcheck', 'false');
291        } else if (isPhoneInput(el) && !el.getAttribute('inputmode')) {
292            el.setAttribute('inputmode', 'tel');
293        }
294    }
295
296    function initLatinKeyboardInputs(root) {
297        var scope = root || document;
298        if (!scope.querySelectorAll) return;
299        scope.querySelectorAll('input').forEach(function (el) {
300            applyLatinKeyboardHints(el);
301        });
302    }
303
304    /* ── inject pulsation CSS once ─────────────────────────── */
305    (function () {
306        if (document.getElementById('fv-pulsate-style')) return;
307        var style = document.createElement('style');
308        style.id = 'fv-pulsate-style';
309        style.textContent =
310            '@keyframes fvPulsate{0%{transform:scale(1)}50%{transform:scale(1.15)}100%{transform:scale(1)}}' +
311            '.fv-pulsate{animation:fvPulsate .35s ease-in-out 3}';
312        document.head.appendChild(style);
313    })();
314
315    document.addEventListener('DOMContentLoaded', function () {
316        initLatinKeyboardInputs(document);
317    });
318
319    document.addEventListener('focusin', function (e) {
320        applyLatinKeyboardHints(e.target);
321    });
322
323    /* ── in-flight submit lock (double-click / Enter while sending) ── */
324
325    var SUBMITTING_ATTR = 'data-fv-submitting';
326
327    function isSubmitting(form) {
328        return !!(form && form.getAttribute(SUBMITTING_ATTR) === '1');
329    }
330
331    function beginSubmit(form) {
332        if (!form) return false;
333        if (isSubmitting(form)) return false;
334        form.setAttribute(SUBMITTING_ATTR, '1');
335        return true;
336    }
337
338    function endSubmit(form) {
339        if (form) form.removeAttribute(SUBMITTING_ATTR);
340    }
341
342    document.addEventListener('submit', function (e) {
343        var el = e.target;
344        var form = (el && el.nodeName === 'FORM') ? el : (el && el.closest ? el.closest('form') : null);
345        if (!form || !isSubmitting(form)) return;
346        e.preventDefault();
347        e.stopImmediatePropagation();
348    }, true);
349
350    /* ── public API ─────────────────────────────────────────── */
351
352    window.FormValidation = {
353        validateName: validateName,
354        validatePhone: validatePhone,
355        validateEmail: validateEmail,
356        validateEGN: validateEGN,
357        clearErrors: clearErrors,
358        clearFieldError: clearFieldError,
359        showFieldError: showFieldError,
360        focusFirstInvalid: focusFirstInvalid,
361        pulsateElement: pulsateElement,
362        setupLiveValidation: setupLiveValidation,
363        getAjaxErrorMessage: getAjaxErrorMessage,
364        applyLatinKeyboardHints: applyLatinKeyboardHints,
365        initLatinKeyboardInputs: initLatinKeyboardInputs,
366        isSubmitting: isSubmitting,
367        beginSubmit: beginSubmit,
368        endSubmit: endSubmit
369    };
370})();

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.