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.