1(function () { 2 'use strict'; 3 const english = document.documentElement.lang === 'en'; 4 const copy = { 5 invalid: english ? 'Check the highlighted fields.' : 'Revisa los campos indicados.', 6 sending: english ? 'Sending⦠Please wait.' : 'Enviando⦠Espera un momento.', 7 }; 8 9 function initForms() { 10 document.querySelectorAll('form[data-zento-form]').forEach(function (form) { 11 const status = document.createElement('div'); 12 status.className = 'zento-form-status'; 13 status.setAttribute('role', 'status'); 14 status.setAttribute('aria-live', 'polite'); 15 form.appendChild(status); 16 let busy = false; 17 const markedButtons = []; 18 function feedback(field) { 19 if (!field.id) field.id = 'zento-field-' + Math.random().toString(36).slice(2); 20 const described = (field.getAttribute('aria-describedby') || '').split(/\s+/); 21 let message = described.map(id => document.getElementById(id)) 22 .find(el => el && el.classList.contains('invalid-feedback')); 23 if (!message) { 24 message = document.createElement('div'); 25 message.id = field.id + '-feedback'; 26 message.className = 'invalid-feedback'; 27 const group = field.closest('.input-group'); 28 (group || field).insertAdjacentElement('afterend', message); 29 field.setAttribute('aria-describedby', described.filter(Boolean).concat(message.id).join(' ')); 30 } 31 return message; 32 } 33 function mark(field, message) { 34 const el = feedback(field); 35 el.textContent = message; 36 el.classList.toggle('d-block', !!message); 37 field.setAttribute('aria-invalid', message ? 'true' : 'false'); 38 } 39 form.addEventListener('invalid', function (event) { 40 mark(event.target, event.target.validationMessage); 41 status.textContent = copy.invalid; 42 }, true); 43 form.addEventListener('input', function (event) { 44 if (event.target.matches('input, select, textarea') && event.target.getAttribute('aria-invalid') === 'true') { 45 mark(event.target, event.target.validity.valid ? '' : event.target.validationMessage); 46 } 47 }); 48 form.addEventListener('change', function (event) { 49 if (event.target.matches('input, select, textarea') && event.target.validity.valid) mark(event.target, ''); 50 }); 51 let firstError = null; 52 form.querySelectorAll('input, select, textarea').forEach(function (field) { 53 const ids = (field.getAttribute('aria-describedby') || '').split(/\s+/); 54 const error = ids.map(id => document.getElementById(id)) 55 .find(el => el && el.classList.contains('invalid-feedback') && el.textContent.trim()); 56 if (error) { field.setAttribute('aria-invalid', 'true'); firstError = firstError || field; } 57 }); 58 if (firstError) firstError.focus(); 59 form.addEventListener('submit', function (event) { 60 if (busy) { event.preventDefault(); return; } 61 // Run after other handlers. Payment modals and AJAX flows retain their own lifecycle. 62 queueMicrotask(function () { 63 if (event.defaultPrevented || !form.hasAttribute('data-zento-submit-lock')) return; 64 busy = true; 65 form.setAttribute('aria-busy', 'true'); 66 status.textContent = copy.sending; 67 form.querySelectorAll('button[type="submit"], input[type="submit"]').forEach(function (button) { 68 button.setAttribute('aria-disabled', 'true'); 69 markedButtons.push(button); 70 }); 71 }); 72 }); 73 window.addEventListener('pageshow', function () { 74 busy = false; 75 form.removeAttribute('aria-busy'); 76 status.textContent = ''; 77 markedButtons.forEach(button => button.removeAttribute('aria-disabled')); 78 }); 79 }); 80 } 81 82 function initNavigation() { 83 function sync() { 84 const links = Array.from(document.querySelectorAll('.ab-mobile-bottom-nav a')); 85 const exact = links.find(link => { 86 const url = new URL(link.href, location.href); 87 return url.pathname === location.pathname && url.hash === location.hash && !!url.hash; 88 }); 89 if (!exact && !document.getElementById('mis-citas')) return; 90 links.forEach(function (link) { 91 const active = exact ? link === exact : new URL(link.href).pathname === location.pathname && !new URL(link.href).hash; 92 link.classList.toggle('active', active); 93 link.classList.toggle('ab-mobile-nav-active', active); 94 if (active) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current'); 95 }); 96 } 97 sync(); 98 window.addEventListener('hashchange', sync); 99 } 100 101 function initOnboarding() { 102 const config = window.ZentoUX || {}; 103 if (!config.employeeId) return;
104 const key = 'zento:onboarding:' + config.employeeId; 105 let saved = null; 106 try { 107 saved = JSON.parse(localStorage.getItem(key) || 'null'); 108 if (!saved || !Number.isInteger(saved.step) || saved.step < 1 || saved.step > 6 || !Number.isFinite(saved.expires) || saved.expires < Date.now()) saved = null; 109 if (config.complete) { localStorage.removeItem(key); saved = null; } 110 if (config.step) { 111 saved = { step: config.step, expires: Date.now() + 30 * 86400000 }; 112 localStorage.setItem(key, JSON.stringify(saved)); 113 } 114 } catch (_) { /* Server session remains the fallback when local storage is unavailable. */ } 115 const step = config.resumeStep || (saved ? saved.step : 0); 116 if (step && !config.complete && !config.step) { 117 try { localStorage.setItem(key, JSON.stringify({ step: step, expires: Date.now() + 30 * 86400000 })); } catch (_) {} 118 } 119 document.querySelectorAll('[data-zento-resume]').forEach(function (link) { 120 if (step && !config.complete) { 121 const url = new URL(link.href, location.href); 122 url.searchParams.set('step', String(step)); 123 link.href = url.href; 124 const card = link.closest('[data-zento-resume-card]'); 125 if (card) card.hidden = false; 126 } 127 }); 128 // Store only the checkpoint. Names, phones, passwords and form drafts never go here. 129 } 130 131 function init() { initForms(); initNavigation(); initOnboarding(); } 132 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 133 else init(); 134})();
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.