1/* eslint-disable */ 2/** 3 * Auth Overlay for Zemexx ÐÐ 4 * -------------------------- 5 * ÐакладÑÐ²Ð°ÐµÑ UX-иÑпÑÐ°Ð²Ð»ÐµÐ½Ð¸Ñ (P0âP3 по аÑдиÑÑ 2026-05-25) 6 * на ÑÑÑаниÑÑ /my/auth/* без пеÑеÑбоÑки Vue-бандла. 7 * 8 * ÐокÑÑÑие: 9 * P0 â autocomplete, label[for]/input[id], focus-visible, конÑÑаÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ¸ 10 * P1 â strength meter, конÑÑаÑÑ Ð¿Ð¾Ð´Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²ÐºÐ°, placeholder [email protected] 11 * P2 â Remember me, inline валидаÑÐ¸Ñ email, live-ÑвеÑка паÑолей, 12 * каÑÑоÑка, ÑаÑÑÑоÑние логоÑип/ÑоÑма, "ÐабÑли паÑолÑ" underline 13 * P3 â inputmode=numeric Ð´Ð»Ñ tel, dynamic title, hints, * меÑка обÑзаÑелÑноÑÑи 14 * 15 * Ðе ÑÑÐ¾Ð³Ð°ÐµÑ Ð´ÑÑгие overlay (office / chat-admin) и не подклÑÑаеÑÑÑ Ð²Ð½Ðµ /my/auth/. 16 */ 17(function () { 18 'use strict'; 19 20 // ---- Guard: запÑÑкаемÑÑ Ð´Ð»Ñ Ð²Ñего /my, но акÑÐ¸Ð²Ð½Ñ ÑолÑко на /my/auth/* ---- 21 if (!location.pathname.startsWith('/my')) return; 22 function isAuthArea() { return location.pathname.startsWith('/my/auth'); } 23 function syncBodyClass() { 24 document.body.classList.toggle('auth-ov', isAuthArea()); 25 } 26 syncBodyClass(); 27 28 // ---- ÐонÑиги ---------------------------------------------------------- 29 30 /** СопоÑÑавление пÑÑи â меÑаданнÑе ÑÑÑаниÑÑ Ð´Ð»Ñ title и конÑекÑÑа */ 31 const ROUTE_META = [ 32 { test: /\/auth\/login(?:\/|$)/, title: 'ÐÑ Ð¾Ð´ â Zemexx', kind: 'login' }, 33 { test: /\/auth\/register(?:\/|$)/, title: 'РегиÑÑÑаÑÐ¸Ñ â Zemexx', kind: 'register' }, 34 { test: /\/auth\/forgot/, title: 'ÐоÑÑÑановление паÑÐ¾Ð»Ñ â Zemexx', kind: 'forgot' }, 35 { test: /\/auth\/reset/, title: 'ÐовÑй паÑÐ¾Ð»Ñ â Zemexx', kind: 'reset' }, 36 { test: /\/auth\/verify/, title: 'ÐодÑвеÑждение â Zemexx', kind: 'verify' }, 37 { test: /\/auth\/otp/, title: 'Ðод подÑвеÑÐ¶Ð´ÐµÐ½Ð¸Ñ â Zemexx', kind: 'otp' }, 38 ]; 39 40 /** ÐвÑиÑÑика: ÑÑо ÑÑо за input по подÑказкам в label/placeholder/name/type */ 41 function detectInputKind(input, labelText) { 42 const type = (input.getAttribute('type') || 'text').toLowerCase(); 43 const ph = (input.getAttribute('placeholder') || '').toLowerCase(); 44 const nm = (input.getAttribute('name') || '').toLowerCase(); 45 const lbl = (labelText || '').toLowerCase(); 46 const hay = `${ph} ${nm} ${lbl}`; 47 48 if (type === 'password') return 'password'; 49 if (type === 'email' || /email|e-?mail|поÑÑ/.test(hay)) return 'email'; 50 if (type === 'tel' || /ÑелеÑон|phone|tel/.test(hay)) return 'tel'; 51 if (/код|sms|otp|подÑвеÑжд/.test(hay) && (type === 'text' || type === 'number')) return 'otp'; 52 if (/ÑамилиÑ|surname|last\s*name/.test(hay)) return 'family-name'; 53 if (/имÑ\b|first\s*name|name(?!.*last)/.test(hay)) return 'given-name'; 54 if (/оÑÑеÑÑво|middle|patronymic/.test(hay)) return 'additional-name'; 55 return null; 56 } 57 58 function currentRouteMeta() { 59 for (const r of ROUTE_META) { 60 if (r.test.test(location.pathname)) return r; 61 } 62 return { title: 'ÐиÑнÑй ÐºÐ°Ð±Ð¸Ð½ÐµÑ â Zemexx', kind: 'other' }; 63 } 64 65 // ---- Title + favicon -------------------------------------------------- 66 67 function applyTitle() { 68 const meta = currentRouteMeta(); 69 if (document.title !== meta.title) document.title = meta.title; 70 } 71 72 function ensureFavicon() { 73 if (document.querySelector('link[rel="icon"], link[rel="shortcut icon"]')) return; 74 const link = document.createElement('link'); 75 link.rel = 'icon'; 76 link.type = 'image/svg+xml'; 77 link.href = '/my/logo.svg'; 78 document.head.appendChild(link); 79 } 80 81 // ---- УникалÑнÑй ID Ð´Ð»Ñ Ð¿Ð°Ñ label/input -------------------------------- 82 83 let _uid = 0; 84 function nextId(prefix) { 85 _uid += 1; 86 return `${prefix}-ov-${_uid}`; 87 } 88 89 // ---- ÐбÑабоÑка одного input ------------------------------------------ 90 91 function enhanceInput(input) { 92 if (input.dataset.ovDone === '1') return; 93 94 const group = input.closest('.form-group'); 95 const label = group ? group.querySelector('label') : null; 96 const labelText = label ? label.textContent.trim() : '';
97 const kind = detectInputKind(input, labelText); 98 const route = currentRouteMeta(); 99 100 // 1) label[for] â input[id] association (P0) 101 if (label && input) { 102 if (!input.id) input.id = nextId(kind || 'field'); 103 if (!label.getAttribute('for')) label.setAttribute('for', input.id); 104 } 105 106 // 2) autocomplete + inputmode (P0/P3) 107 if (kind === 'email') { 108 if (!input.getAttribute('autocomplete')) { 109 input.setAttribute('autocomplete', route.kind === 'register' ? 'email' : 'username'); 110 } 111 if (!input.getAttribute('inputmode')) input.setAttribute('inputmode', 'email'); 112 if (!input.getAttribute('placeholder') || /введиÑе/i.test(input.getAttribute('placeholder') || '')) { 113 input.setAttribute('placeholder', '[email protected]'); 114 } 115 input.setAttribute('spellcheck', 'false'); 116 input.setAttribute('autocapitalize', 'off'); 117 } else if (kind === 'tel') { 118 if (!input.getAttribute('autocomplete')) input.setAttribute('autocomplete', 'tel'); 119 if (!input.getAttribute('inputmode')) input.setAttribute('inputmode', 'tel'); 120 } else if (kind === 'otp') { 121 if (!input.getAttribute('autocomplete')) input.setAttribute('autocomplete', 'one-time-code'); 122 if (!input.getAttribute('inputmode')) input.setAttribute('inputmode', 'numeric'); 123 input.setAttribute('pattern', '[0-9]*'); 124 } else if (kind === 'password') { 125 const isNew = route.kind === 'register' || route.kind === 'reset'; 126 if (!input.getAttribute('autocomplete')) { 127 input.setAttribute('autocomplete', isNew ? 'new-password' : 'current-password'); 128 } 129 } else if (kind === 'family-name' || kind === 'given-name' || kind === 'additional-name') { 130 if (!input.getAttribute('autocomplete')) input.setAttribute('autocomplete', kind); 131 } 132 133 // 3) required asterisk visual marker (P3) 134 if (label && input.required && !label.querySelector('.req-mark')) { 135 const star = document.createElement('span'); 136 star.className = 'req-mark'; 137 star.textContent = ' *'; 138 star.setAttribute('aria-hidden', 'true'); 139 label.appendChild(star); 140 if (!input.getAttribute('aria-required')) input.setAttribute('aria-required', 'true'); 141 } 142 143 // 4) inline валидаÑÐ¸Ñ email на blur (P2) 144 if (kind === 'email') { 145 input.addEventListener('blur', () => validateEmailField(input), { passive: true }); 146 input.addEventListener('input', () => clearOvError(input), { passive: true }); 147 } 148 149 // 5) password strength meter (P1) â ÑолÑко новÑе паÑоли 150 if (kind === 'password' && (route.kind === 'register' || route.kind === 'reset')) { 151 const isConfirm = /повÑоÑ|подÑвеÑ|confirm/i.test(labelText); 152 if (!isConfirm) attachStrengthMeter(input); 153 else attachMatchHint(input); 154 } 155 156 // 6) hint texts (P3) 157 addFieldHint(input, kind, route, group); 158 159 input.dataset.ovDone = '1'; 160 } 161 162 // ---- Email validation ------------------------------------------------- 163 164 const EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; 165 166 function validateEmailField(input) { 167 const val = (input.value || '').trim(); 168 if (!val) return clearOvError(input); 169 if (!EMAIL_RE.test(val)) { 170 showOvError(input, 'ÐÐ¾Ñ Ð¾Ð¶Ðµ, в адÑеÑе опеÑаÑка. ÐÑимеÑ: [email protected]'); 171 } else { 172 clearOvError(input); 173 } 174 } 175 176 function showOvError(input, msg) { 177 clearOvError(input); 178 const group = input.closest('.form-group'); 179 if (!group) return; 180 const div = document.createElement('div'); 181 div.className = 'field-error ov-inline-error'; 182 div.textContent = msg; 183 group.appendChild(div); 184 input.setAttribute('aria-invalid', 'true'); 185 } 186 187 function clearOvError(input) { 188 const group = input.closest('.form-group'); 189 if (!group) return; 190 group.querySelectorAll('.ov-inline-error').forEach((n) => n.remove()); 191 input.removeAttribute('aria-invalid'); 192 } 193 194 // ---- Password strength meter ------------------------------------------ 195 196 function scorePassword(pwd) { 197 if (!pwd) return { score: 0, label: '' }; 198 let s = 0; 199 if (pwd.length >= 8) s += 1; 200 if (pwd.length >= 12) s += 1; 201 if (/[a-zа-ÑÑ]/i.test(pwd) && /[A-ZÐ-ЯÐ]/.test(pwd)) s += 1;
202 if (/\d/.test(pwd) && /[^A-Za-zÐ-Яа-ÑÐÑ0-9]/.test(pwd)) s += 1; 203 if (s > 4) s = 4; 204 const labels = ['', 'СлабÑй', 'Так Ñебе', 'ХоÑоÑий', 'ÐадÑжнÑй']; 205 const colors = ['#9ca3af', '#dc2626', '#d97706', '#059669', '#047857']; 206 return { score: s, label: labels[s], color: colors[s] }; 207 } 208 209 function attachStrengthMeter(input) { 210 const group = input.closest('.form-group'); 211 if (!group || group.querySelector('.ov-pwd-meter')) return; 212 213 const meter = document.createElement('div'); 214 meter.className = 'ov-pwd-meter'; 215 meter.setAttribute('aria-live', 'polite'); 216 meter.dataset.score = '0'; 217 meter.innerHTML = ` 218 <div class="ov-pwd-meter-bars" aria-hidden="true"> 219 <div class="ov-pwd-meter-bar"></div> 220 <div class="ov-pwd-meter-bar"></div> 221 <div class="ov-pwd-meter-bar"></div> 222 <div class="ov-pwd-meter-bar"></div> 223 </div> 224 <div class="ov-pwd-meter-label"> 225 <span>ÐадÑжноÑÑÑ Ð¿Ð°ÑолÑ</span><strong>â</strong> 226 </div>`; 227 group.appendChild(meter); 228 229 const update = () => { 230 const r = scorePassword(input.value || ''); 231 meter.dataset.score = String(r.score); 232 meter.style.setProperty('--ov-pwd-color', r.color); 233 const strong = meter.querySelector('strong'); 234 if (strong) strong.textContent = r.label || 'â'; 235 // Trigger any attached match hint update 236 document.dispatchEvent(new CustomEvent('ov:pwd-changed')); 237 }; 238 input.addEventListener('input', update, { passive: true }); 239 } 240 241 function attachMatchHint(confirmInput) { 242 const group = confirmInput.closest('.form-group'); 243 if (!group || group.querySelector('.ov-match-hint')) return; 244 245 const hint = document.createElement('div'); 246 hint.className = 'ov-match-hint'; 247 hint.setAttribute('aria-live', 'polite'); 248 group.appendChild(hint); 249 250 const findFirstPwd = () => { 251 const all = document.querySelectorAll('.auth-page input[type="password"]'); 252 for (const el of all) { 253 if (el !== confirmInput) return el; 254 } 255 return null; 256 }; 257 258 const update = () => { 259 const first = findFirstPwd(); 260 const a = first ? first.value : ''; 261 const b = confirmInput.value || ''; 262 if (!b) { 263 hint.textContent = ''; 264 hint.removeAttribute('data-state'); 265 return; 266 } 267 if (a && a === b) { 268 hint.textContent = 'â ÐаÑоли ÑовпадаÑÑ'; 269 hint.setAttribute('data-state', 'ok'); 270 } else { 271 hint.textContent = 'â ÐаÑоли не ÑовпадаÑÑ'; 272 hint.setAttribute('data-state', 'no'); 273 } 274 }; 275 confirmInput.addEventListener('input', update, { passive: true }); 276 document.addEventListener('ov:pwd-changed', update); 277 } 278 279 // ---- Field hint helper ------------------------------------------------ 280 281 function addFieldHint(input, kind, route, group) { 282 if (!group || group.querySelector('.ov-field-hint')) return; 283 let hint = null; 284 if (kind === 'email' && route.kind === 'register') { 285 hint = 'Ðа него пÑидÑÑ ÑÑÑлка Ð´Ð»Ñ Ð¿Ð¾Ð´ÑвеÑÐ¶Ð´ÐµÐ½Ð¸Ñ ÑегиÑÑÑаÑии.'; 286 } else if (kind === 'tel' && route.kind === 'register') { 287 hint = 'ÐÑжен, еÑли поÑÑебÑеÑÑÑ ÑвÑзаÑÑÑÑ Ð¿Ð¾ Ñделке.'; 288 } else if (kind === 'password' && (route.kind === 'register' || route.kind === 'reset')) { 289 const isConfirm = /повÑоÑ|подÑвеÑ|confirm/i.test((group.querySelector('label') || {}).textContent || ''); 290 if (!isConfirm) hint = 'ÐинимÑм 8 Ñимволов и Ñ Ð¾ÑÑ Ð±Ñ Ð¾Ð´Ð¸Ð½ ÑпеÑÑимвол (! @ # $ % &).'; 291 } 292 if (!hint) return; 293 const div = document.createElement('div'); 294 div.className = 'ov-field-hint'; 295 div.textContent = hint; 296 group.appendChild(div); 297 const helpId = nextId('hint'); 298 div.id = helpId; 299 const prev = input.getAttribute('aria-describedby'); 300 input.setAttribute('aria-describedby', prev ? `${prev} ${helpId}` : helpId); 301 } 302 303 // ---- "Required" note ÑвеÑÑ Ñ ÑоÑÐ¼Ñ (P3) -------------------------------- 304 305 function addRequiredNote(form) { 306 if (!form || form.dataset.ovReqNote === '1') return; 307 if (!form.querySelector('input[required]')) return; 308 const note = document.createElement('div'); 309 note.className = 'ov-required-note'; 310 note.innerHTML = 'ÐолÑ, оÑмеÑеннÑе <span class="req-mark">*</span>, обÑзаÑелÑÐ½Ñ Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ.'; 311 form.insertBefore(note, form.firstChild); 312 form.dataset.ovReqNote = '1'; 313 } 314 315 // ---- Remember me (P2, ÑолÑко login) ----------------------------------- 316 317 function addRememberMe(form) { 318 if (!form || form.dataset.ovRemember === '1') return; 319 const route = currentRouteMeta(); 320 if (route.kind !== 'login') return; 321 const submit = form.querySelector('.btn-primary'); 322 if (!submit) return; 323
324 const row = document.createElement('div'); 325 row.className = 'ov-remember-row'; 326 const id = nextId('remember'); 327 row.innerHTML = ` 328 <input type="checkbox" id="${id}" checked /> 329 <label for="${id}">ÐапомниÑÑ Ð¼ÐµÐ½Ñ Ð½Ð° ÑÑом ÑÑÑÑойÑÑве</label>`; 330 submit.parentNode.insertBefore(row, submit); 331 332 const cb = row.querySelector('input'); 333 try { 334 cb.checked = localStorage.getItem('ov_remember_me') !== '0'; 335 } catch (_) { /* noop */ } 336 cb.addEventListener('change', () => { 337 try { localStorage.setItem('ov_remember_me', cb.checked ? '1' : '0'); } catch (_) { /* noop */ } 338 }); 339 340 form.dataset.ovRemember = '1'; 341 } 342 343 // ---- ÐлавнÑй пÑÐ¾Ñ Ð¾Ð´ по DOM -------------------------------------------- 344 345 function enhanceForm() { 346 const form = document.querySelector('.auth-page .auth-form'); 347 if (!form) return; 348 form.querySelectorAll('input').forEach(enhanceInput); 349 addRequiredNote(form); 350 addRememberMe(form); 351 } 352 353 // ÐнÑоÑмаÑионнÑй Ð±Ð°Ð½Ð½ÐµÑ Ð½Ð° ÑÑÑаниÑе ÐÐ¥ÐÐÐ: пÑедÑпÑеждаем пÑо ÑоÑÑ ÑÐ¼ÐµÐ½Ñ 354 // паÑÐ¾Ð»Ñ Ð¿Ð¾Ñле Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ ÐÐ (безопаÑноÑÑÑ). ТолÑко на /auth/login. 355 function ensureLoginInfoStyle() { 356 if (document.getElementById('ov-login-info-style')) return; 357 const s = document.createElement('style'); 358 s.id = 'ov-login-info-style'; 359 s.textContent = 360 '.ov-login-info{margin:0 0 18px;padding:14px 16px;border-radius:14px;background:rgba(0,191,63,.08);border:1px solid rgba(0,191,63,.25);color:inherit;text-align:left}' + 361 '.ov-login-info-title{font-weight:700;font-size:14px;margin-bottom:6px;color:#06912f;display:flex;gap:6px}' + 362 '.ov-login-info-text{font-size:13px;line-height:1.5;opacity:.88}' + 363 '[data-theme="dark"] .ov-login-info{background:rgba(0,191,63,.12);border-color:rgba(0,191,63,.35)}' + 364 '[data-theme="dark"] .ov-login-info-title{color:#4ade80}'; 365 document.head.appendChild(s); 366 } 367 368 function addLoginInfoBanner() { 369 const existing = document.querySelector('.ov-login-info'); 370 if (currentRouteMeta().kind !== 'login') { if (existing) existing.remove(); return; } 371 if (existing) return; 372 const form = document.querySelector('.auth-page .auth-form'); 373 if (!form || !form.parentNode) return; 374 ensureLoginInfoStyle(); 375 const banner = document.createElement('div'); 376 banner.className = 'ov-login-info'; 377 banner.setAttribute('role', 'note'); 378 banner.innerHTML = 379 '<div class="ov-login-info-title"><span aria-hidden="true">ð¡</span><span>ÐÑли Ð²Ñ ÑвлÑеÑеÑÑ ÐºÐ»Ð¸ÐµÐ½Ñом компании</span></div>' + 380 '<div class="ov-login-info-text">Ñо пÑи пеÑвом Ð²Ñ Ð¾Ð´Ðµ в лиÑнÑй кабинеÑ: <strong>Ðогин</strong> â Ð²Ð°Ñ email из договоÑа. <strong>ÐаÑолÑ</strong> â Ð²Ð°Ñ Ð½Ð¾Ð¼ÐµÑ ÑелеÑона из договоÑа (напÑимеÑ, 89001234567). ÐÑи Ð²Ñ Ð¾Ð´Ðµ ÑиÑÑема попÑоÑÐ¸Ñ Ð·Ð°Ð´Ð°ÑÑ Ð½Ð¾Ð²Ñй поÑÑоÑннÑй паÑÐ¾Ð»Ñ â ÑÑо в ÑелÑÑ Ð±ÐµÐ·Ð¾Ð¿Ð°ÑноÑÑи.</div>'; 381 form.parentNode.insertBefore(banner, form); 382 } 383 384 // УбиÑаем подпиÑÑ Â«ÐоÑÑÑÐ¿Ñ Ð²ÑдаÑÑÑÑ Ð¿Ñи заклÑÑении договоÑа» под заголовком 385 // Ð²Ñ Ð¾Ð´Ð° (по запÑоÑÑ). ÐаÑÑим по ÑекÑÑÑ â заÑÑÐ°Ð³Ð¸Ð²Ð°ÐµÑ ÑолÑко ÑÑÑ Ð¿Ð¾Ð´Ð¿Ð¸ÑÑ. 386 function removeAccessSubtitle() { 387 document.querySelectorAll('p.auth-subtitle').forEach(function (el) { 388 if ((el.textContent || '').trim().indexOf('ÐоÑÑÑÐ¿Ñ Ð²ÑдаÑÑÑÑ Ð¿Ñи заклÑÑении договоÑа') === 0) { 389 el.remove(); 390 } 391 }); 392 } 393 394 function tick() { 395 syncBodyClass(); 396 if (!isAuthArea()) return; // не auth â пÑÑÑÑ app-overlay ÑабоÑÐ°ÐµÑ 397 applyTitle(); 398 enhanceForm(); 399 addLoginInfoBanner(); 400 removeAccessSubtitle(); 401 } 402 403 // ---- Init ------------------------------------------------------------- 404 405 ensureFavicon(); 406 407 // ÐеÑÐµÑ Ð²Ð°ÑÑваем SPA-навигаÑÐ¸Ñ 408 const pushState = history.pushState; 409 history.pushState = function () { 410 pushState.apply(this, arguments); 411 queueMicrotask(tick); 412 }; 413 const replaceState = history.replaceState; 414 history.replaceState = function () { 415 replaceState.apply(this, arguments); 416 queueMicrotask(tick); 417 }; 418 window.addEventListener('popstate', tick); 419 420 // Mutation observer Ð»Ð¾Ð²Ð¸Ñ ÐºÐ°Ð¶Ð´Ð¾Ðµ пеÑеÑиÑовÑвание Vue (rAF-debounce) 421 let _tickPending = false;
422 function scheduleTick() { 423 if (_tickPending) return; 424 _tickPending = true; 425 requestAnimationFrame(() => { 426 _tickPending = false; 427 try { tick(); } catch (_) {} 428 }); 429 } 430 const obs = new MutationObserver(scheduleTick); 431 function start() { 432 const app = document.getElementById('app') || document.body; 433 obs.observe(app, { childList: true, subtree: true }); 434 tick(); 435 } 436 if (document.readyState === 'loading') { 437 document.addEventListener('DOMContentLoaded', start, { once: true }); 438 } else { 439 start(); 440 } 441})();
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.