1/* XPATAS â interacciones. Sin dependencias salvo jQuery (lo exige WooCommerce para sus eventos). */ 2(function ($) { 3 'use strict'; 4 5 var cfg = window.XPATAS || {}; 6 var body = document.body; 7 var scrim = document.querySelector('.scrim'); 8 var nav = document.getElementById('site-nav'); 9 var drawer = document.getElementById('cart-drawer'); 10 11 /* ---------- Paneles (menú móvil y carrito) ---------- */ 12 function openPanel(el) { 13 if (!el) return; 14 closePanels(); 15 el.classList.add('is-open'); 16 el.setAttribute('aria-hidden', 'false'); 17 if (scrim) scrim.classList.add('is-on'); 18 // El menú vive dentro de la cabecera: hay que subirla por encima del fondo oscuro o no se puede tocar. 19 body.classList.toggle('nav-open', el === nav); 20 body.style.overflow = 'hidden'; 21 var focusable = el.querySelector('button, a, input'); 22 if (focusable) focusable.focus({ preventScroll: true }); 23 } 24 function closePanels() { 25 [nav, drawer].forEach(function (el) { 26 if (el) { el.classList.remove('is-open'); } 27 }); 28 if (drawer) drawer.setAttribute('aria-hidden', 'true'); 29 if (scrim) scrim.classList.remove('is-on'); 30 body.classList.remove('nav-open'); 31 body.style.overflow = ''; 32 document.querySelectorAll('[data-open="nav"]').forEach(function (b) { b.setAttribute('aria-expanded', 'false'); }); 33 } 34 35 document.addEventListener('click', function (e) { 36 var opener = e.target.closest('[data-open]'); 37 if (opener) { 38 var which = opener.getAttribute('data-open'); 39 if (which === 'cart' && drawer) { e.preventDefault(); openPanel(drawer); } 40 if (which === 'nav' && nav) { opener.setAttribute('aria-expanded', 'true'); openPanel(nav); } 41 return; 42 } 43 if (e.target.closest('[data-close]')) { closePanels(); return; } 44 45 var st = e.target.closest('[data-toggle-search]'); 46 if (st) { 47 var panel = document.getElementById('header-search'); 48 var open = panel.classList.toggle('is-open'); 49 st.setAttribute('aria-expanded', open ? 'true' : 'false'); 50 if (open) panel.querySelector('input[type=search]').focus(); 51 } 52 }); 53 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePanels(); }); 54 55 /* ---------- Mega menú ---------- 56 Escritorio: se abre al pasar el ratón o con el teclado (CSS :hover/:focus-within) y también al pulsar 57 la flecha (pantallas táctiles). Móvil: acordeón. */ 58 document.querySelectorAll('.submenu-toggle').forEach(function (btn) { 59 btn.addEventListener('click', function () { 60 var li = btn.closest('li'); 61 var open = !li.classList.contains('is-open'); 62 li.parentElement.querySelectorAll(':scope > li.is-open').forEach(function (o) { if (o !== li) { o.classList.remove('is-open'); o.querySelector('.submenu-toggle').setAttribute('aria-expanded', 'false'); } }); 63 li.classList.toggle('is-open', open); 64 btn.setAttribute('aria-expanded', open ? 'true' : 'false'); 65 }); 66 }); 67 document.addEventListener('click', function (e) { 68 if (!e.target.closest('.has-mega')) document.querySelectorAll('.has-mega.is-open').forEach(function (o) { o.classList.remove('is-open'); }); 69 }); 70 71 /* ---------- Carrito: abrir al añadir ---------- */ 72 if ($ && drawer) { 73 $(document.body).on('added_to_cart', function () { openPanel(drawer); }); 74 } 75 76 /* ---------- Añadir al carrito sin recargar en la ficha ---------- 77 Enviamos el formulario tal cual a la misma URL (lo procesa WooCommerce como siempre, 78 con todas sus validaciones y extensiones) y refrescamos los fragments. Si algo falla, 79 se hace el envÃo normal. */ 80 document.addEventListener('submit', function (e) { 81 var form = e.target; 82 if (!form.matches || !form.matches('form.cart') || !drawer || !window.fetch || form.hasAttribute('data-no-ajax')) return; 83 if (form.closest('.product-type-external') || form.querySelector('input[type=file]')) return; 84 var btn = form.querySelector('.single_add_to_cart_button'); 85 if (!btn || btn.classList.contains('disabled')) return; 86 87 e.preventDefault(); 88 var data = new FormData(form); 89 if (!data.has('add-to-cart') && btn.name === 'add-to-cart') data.append('add-to-cart', btn.value); 90 var label = btn.innerHTML; 91 btn.classList.add('loading'); 92 btn.innerHTML = cfg.i18n ? cfg.i18n.adding : 'â¦'; 93 94 fetch(form.action || window.location.href, { method: 'POST', body: data, credentials: 'same-origin' }) 95 .then(function (r) { return r.text(); }) 96 .then(function (html) { 97 var doc = new DOMParser().parseFromString(html, 'text/html'); 98 var err = doc.querySelector('.woocommerce-error, .wc-block-components-notice-banner.is-error'); 99 if (err) { 100 var holder = document.querySelector('.woocommerce-notices-wrapper') || form.parentElement; 101 holder.innerHTML = err.outerHTML; 102 holder.scrollIntoView({ behavior: 'smooth', block: 'center' }); 103 return; 104 } 105 $(document.body).one('wc_fragments_refreshed', function () { openPanel
105(drawer); }); 106 $(document.body).trigger('wc_fragment_refresh'); 107 if (window.dataLayer) window.dataLayer.push({ event: 'xpatas_add_to_cart' }); 108 }) 109 .catch(function () { form.setAttribute('data-no-ajax', ''); form.submit(); }) 110 .finally(function () { btn.classList.remove('loading'); btn.innerHTML = label; }); 111 }); 112 113 /* ---------- Cantidad +/- ---------- */ 114 document.querySelectorAll('form.cart .quantity').forEach(function (q) { 115 var input = q.querySelector('.qty'); 116 if (!input || input.type === 'hidden') return; 117 function mk(txt, delta, label) { 118 var b = document.createElement('button'); 119 b.type = 'button'; b.className = 'qty-btn'; b.textContent = txt; b.setAttribute('aria-label', label); 120 b.addEventListener('click', function () { 121 var min = parseFloat(input.min) || 1, max = parseFloat(input.max) || Infinity, step = parseFloat(input.step) || 1; 122 var v = Math.min(max, Math.max(min, (parseFloat(input.value) || min) + delta * step)); 123 input.value = v; 124 input.dispatchEvent(new Event('change', { bubbles: true })); 125 }); 126 return b; 127 } 128 q.insertBefore(mk('â', -1, 'Quitar uno'), input); 129 q.appendChild(mk('+', 1, 'Añadir uno')); 130 }); 131 132 /* ---------- Variaciones como botones ---------- */ 133 function buildSwatches(form) { 134 form.querySelectorAll('.variations select').forEach(function (select) { 135 var td = select.closest('td'); 136 if (!td || td.querySelector('.swatches') || select.options.length > 13) return; 137 td.classList.add('has-swatches'); 138 var wrap = document.createElement('div'); 139 wrap.className = 'swatches'; 140 wrap.setAttribute('role', 'radiogroup'); 141 Array.prototype.forEach.call(select.options, function (opt) { 142 if (!opt.value) return; 143 var b = document.createElement('button'); 144 b.type = 'button'; b.className = 'swatch'; b.textContent = opt.text; b.dataset.value = opt.value; 145 b.setAttribute('role', 'radio'); 146 b.addEventListener('click', function () { 147 if (b.classList.contains('is-disabled')) return; 148 $(select).val(select.value === opt.value ? '' : opt.value).trigger('change'); 149 }); 150 wrap.appendChild(b); 151 }); 152 td.insertBefore(wrap, select); 153 }); 154 syncSwatches(form); 155 } 156 function syncSwatches(form) { 157 form.querySelectorAll('.variations select').forEach(function (select) { 158 var wrap = select.closest('td').querySelector('.swatches'); 159 if (!wrap) return; 160 var enabled = {}; 161 Array.prototype.forEach.call(select.options, function (o) { if (o.value && !o.disabled) enabled[o.value] = true; }); 162 wrap.querySelectorAll('.swatch').forEach(function (b) { 163 var sel = b.dataset.value === select.value; 164 b.classList.toggle('is-selected', sel); 165 b.setAttribute('aria-checked', sel ? 'true' : 'false'); 166 b.classList.toggle('is-disabled', !enabled[b.dataset.value]); 167 }); 168 }); 169 } 170 if ($) { 171 $('form.variations_form').each(function () { buildSwatches(this); }); 172 $(document.body).on('woocommerce_update_variation_values check_variations reset_data found_variation', 'form.variations_form', function () { syncSwatches(this); }); 173 $(document.body).on('change', 'form.variations_form .variations select', function () { syncSwatches(this.form); }); 174 } 175 176 /* ---------- Fecha estimada de entrega ---------- 177 Se calcula en el navegador para que la caché de página no muestre fechas viejas. */ 178 function madridNow() { 179 try { 180 var s = new Date().toLocaleString('en-US', { timeZone: 'Europe/Madrid' }); 181 return new Date(s); 182 } catch (e) { return new Date(); } 183 } 184 function isWorkday(d) { 185 var iso = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); 186 return d.getDay() !== 0 && d.getDay() !== 6 && (cfg.holidays || []).indexOf(iso) === -1; 187 } 188 function addWorkdays(d, n) { 189 var r = new Date(d); 190 while (n > 0) { r.setDate(r.getDate() + 1); if (isWorkday(r)) n--; } 191 return r; 192 } 193 /* Cada producto trae su plazo (data-min/max en dÃas laborables) según su clase de envÃo. 194 La hora de corte solo se anuncia en los de entrega rápida. */ 195 document.querySelectorAll('[data-eta]').forEach(function (el) { 196 var min = parseInt(el.dataset.min, 10), max = parseInt(el.dataset.max, 10); 197 if (!min || !max) return; 198 var fast = el.dataset.national === '1'; 199 var days = (el.dataset.days || '1,2,3,4,5').split(',').map(Number); 200 var now = madridNow(); 201 var cutoff = parseInt(cfg.cutoffHour, 10) || 13; 202 var isShipDay = function (d) { return isWorkday(d) && days.indexOf(d.getDay()) !== -1; }; 203 var shipsToday = isShipDay(now) && now.getHours() < cutoff; 204 var start = new Date(now); 205 if (!shipsToday) { do { start.setDate(start.getDate() + 1); }
205 while (!isShipDay(start)); } 206 var everyDay = days.length >= 5; 207 var from = addWorkdays(start, min), to = addWorkdays(start, max); 208 var fmt = new Intl.DateTimeFormat('es-ES', fast ? { weekday: 'long', day: 'numeric', month: 'long' } : { day: 'numeric', month: 'long' }); 209 var when = from.toDateString() === to.toDateString() ? 'el ' + fmt.format(from) : 'entre el ' + fmt.format(from) + ' y el ' + fmt.format(to); 210 var dayFmt = new Intl.DateTimeFormat('es-ES', { weekday: 'long', day: 'numeric' }); 211 var txt; 212 if (fast && shipsToday) txt = 'PÃdelo antes de las ' + cutoff + ':00 y recÃbelo ' + when; 213 else if (fast && !everyDay) txt = 'Sale el ' + dayFmt.format(start) + ' · recÃbelo ' + when; 214 else txt = 'RecÃbelo ' + when; 215 el.innerHTML = '<span class="eta"></span>'; 216 el.firstChild.textContent = txt; 217 if (!fast) el.firstChild.classList.add('eta--slow'); 218 }); 219 220 /* ---------- Barra fija de compra (móvil) ---------- */ 221 var sticky = document.querySelector('.sticky-atc'); 222 var mainBtn = document.querySelector('.summary .single_add_to_cart_button'); 223 if (sticky && mainBtn && 'IntersectionObserver' in window) { 224 new IntersectionObserver(function (entries) { 225 var e = entries[0]; 226 var past = !e.isIntersecting && e.boundingClientRect.top < 0; 227 sticky.classList.toggle('is-visible', past); 228 sticky.setAttribute('aria-hidden', past ? 'false' : 'true'); 229 }).observe(mainBtn); 230 sticky.querySelector('[data-sticky-atc]').addEventListener('click', function () { 231 var form = mainBtn.form; 232 if (form && form.classList.contains('variations_form') && mainBtn.classList.contains('disabled')) { 233 form.scrollIntoView({ behavior: 'smooth', block: 'center' }); 234 } else { 235 mainBtn.click(); 236 } 237 }); 238 } 239 document.querySelectorAll('[data-scroll-buy]').forEach(function (a) { 240 a.addEventListener('click', function (ev) { 241 var target = mainBtn && (mainBtn.form || mainBtn); 242 if (!target) return; 243 ev.preventDefault(); 244 target.scrollIntoView({ behavior: 'smooth', block: 'center' }); 245 }); 246 }); 247})(window.jQuery); 248 249/* Checkout: añadir un producto de "Completa tu pedido" sin salir de la página */ 250(function ($) { 251 if (!$ || typeof wc_checkout_params === 'undefined') return; 252 $(document.body).on('click', '[data-co-add]', function () { 253 var btn = $(this); 254 btn.prop('disabled', true).text('Añadiendoâ¦'); 255 $.post(wc_checkout_params.wc_ajax_url.toString().replace('%%endpoint%%', 'add_to_cart'), { product_id: btn.data('co-add'), quantity: 1 }) 256 .always(function () { $(document.body).trigger('update_checkout'); }); 257 }); 258})(window.jQuery);
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.