1/** 2 * Archivo: assets/js/form.js 3 * Ubicación: /wp-content/plugins/wti-mesas-vip/assets/js/ 4 * Creado: 2026-08-02 12:40 5 * 6 * Intercepta el botón MESAS VIP, abre el formulario y, al enviar, abre 7 * WhatsApp con la solicitud ya escrita. No se guarda nada en el servidor. 8 * 9 * No modifica wti-core: la detección se hace sobre el enlace ya renderizado. 10 * 11 * Idioma: español para quien navega en español, inglés para el resto. 12 * Se decide AQUÃ, en el navegador, no en el servidor: la web va cacheada 13 * con WP Rocket y el servidor entrega la misma página a todo el mundo. 14 * 15 * Este archivo se carga en todas las páginas, esté el formulario activo o 16 * no, y no hace NADA hasta que alguien toca el botón MESAS VIP. Entonces 17 * pregunta al servidor si tiene que abrirse. Va asà porque la web está 18 * cacheada: si la decisión viniera dentro de la página, las copias viejas 19 * seguirÃan con el comportamiento antiguo (incidencia 2026-08-02, con la 20 * mitad de la web ây todo el móvilâ sirviendo la versión sin formulario). 21 */ 22(function () { 23 'use strict'; 24 25 if (typeof WTI_MVIP_BOOT === 'undefined' || !WTI_MVIP_BOOT.configUrl) { 26 return; 27 } 28 29 var BOOT = WTI_MVIP_BOOT; 30 31 // Se rellenan cuando llega la configuración del servidor. 32 var CFG = null; 33 34 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 35 // 0. Idioma 36 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 37 38 /** Prefijos de idioma que la web usa en la URL (GTranslate / Polylang). */ 39 var URL_LANGS = ['es', 'en', 'fr', 'de', 'it', 'nl', 'pt', 'ca', 'ru', 40 'pl', 'sv', 'da', 'no', 'fi', 'ja', 'zh', 'ar']; 41 42 /** 43 * Español solo si el visitante navega en español. Cualquier otro idioma 44 * ve el inglés. 45 * 46 * Manda el idioma elegido en la URL (/en/, /fr/â¦) porque es una decisión 47 * expresa del visitante. Si no hay prefijo, manda el navegador â no el 48 * "lang" del HTML, que en la web base siempre dice es-ES y dejarÃa a los 49 * extranjeros con el formulario en español. 50 */ 51 function detectLang() { 52 var m = /^\/([a-z]{2})(?:[-_][a-z]{2})?(?:\/|$)/i.exec(window.location.pathname); 53 if (m && URL_LANGS.indexOf(m[1].toLowerCase()) !== -1) { 54 return 'es' === m[1].toLowerCase() ? 'es' : 'en'; 55 } 56 57 var nav = (navigator.language || navigator.userLanguage || '').toLowerCase(); 58 if (!nav && navigator.languages && navigator.languages.length) { 59 nav = String(navigator.languages[0]).toLowerCase(); 60 } 61 if (nav) { 62 return 0 === nav.indexOf('es') ? 'es' : 'en'; 63 } 64 65 // Sin dato ninguno: nos quedamos con el idioma de la página. 66 var html = (document.documentElement.getAttribute('lang') || '').toLowerCase(); 67 return 0 === html.indexOf('es') ? 'es' : 'en'; 68 } 69 70 var LANG = detectLang(); 71 var T = null; 72 var AGES = []; 73 var GROUPS = []; 74 var MESES = []; 75 76 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 77 // 0.bis Configuración: se pide al servidor, no viene en la página 78 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 79 80 var configPromise = null; 81 82 function loadConfig() { 83 if (configPromise) { return configPromise; } 84 85 configPromise = new Promise(function (resolve) { 86 var done = false; 87 function finish(cfg) { 88 if (done) { return; } 89 done = true; 90 resolve(cfg); 91 } 92 93 // Si el servidor tarda o falla, el botón se comporta como siempre. 94 var timer = setTimeout(function () { finish(null); }, 4000); 95 96 // El servidor le pone caducidad de un año a TODAS las respuestas, 97 // también a esta. Con una dirección distinta cada vez, ninguna 98 // caché puede devolver una configuración vieja. 99 var url = BOOT.configUrl 100 + (BOOT.configUrl.indexOf('?') === -1 ? '?' : '&') 101 + '_=' + Date.now(); 102 103 var req = new XMLHttpRequest(); 104 req.open('GET', url, true); 105 req.setRequestHeader('Accept', 'application/json'); 106 req.setRequestHeader('Cache-Control', 'no-cache'); 107 req.onload = function () { 108 clearTimeout(timer);
109 try { 110 finish(JSON.parse(req.responseText)); 111 } catch (e) { 112 finish(null); 113 } 114 }; 115 req.onerror = function () { clearTimeout(timer); finish(null); }; 116 req.send(); 117 }).then(function (cfg) { 118 if (cfg && applyConfig(cfg)) { return cfg; } 119 return null; 120 }); 121 122 return configPromise; 123 } 124 125 /** ¿Está el formulario activo para quien está viendo esto ahora mismo? */ 126 function applyConfig(cfg) { 127 var on = ('form_all' === cfg.mode) 128 || ('form_admin' === cfg.mode && BOOT.isAdmin); 129 if (!on || !cfg.phone || !cfg.texts) { return false; } 130 131 CFG = cfg; 132 T = cfg.texts[LANG] || cfg.texts.es; 133 AGES = (cfg.ageRanges && cfg.ageRanges[LANG]) || []; 134 GROUPS = (cfg.groupTypes && cfg.groupTypes[LANG]) || []; 135 MESES = (T && T.months) || []; 136 return true; 137 } 138 139 /** 140 * El CSS se carga la primera vez que se abre el formulario, no en cada 141 * página: en la inmensa mayorÃa nadie llega a pulsar el botón. 142 */ 143 var stylesPromise = null; 144 function loadStyles() { 145 if (stylesPromise) { return stylesPromise; } 146 147 stylesPromise = new Promise(function (resolve) { 148 if (!CFG || !CFG.cssUrl) { resolve(); return; } 149 150 var link = document.createElement('link'); 151 link.rel = 'stylesheet'; 152 link.href = CFG.cssUrl; 153 link.onload = resolve; 154 link.onerror = resolve; 155 document.head.appendChild(link); 156 157 // Nunca dejar al cliente esperando por una hoja de estilos. 158 setTimeout(resolve, 1500); 159 }); 160 161 return stylesPromise; 162 } 163 164 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 165 // 1. Detección del botón 166 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 167 168 /** 169 * Solo mesas VIP. Deja fuera los botones de barcos y de cena + fiesta, 170 * que comparten la clase wti-button-vip pero llevan otro texto. 171 */ 172 var RE_IS_VIP = /reservas\s*V\.?\s*I\.?\s*P/i; 173 174 function isWhatsAppLink(href) { 175 return /(?:wa\.me|api\.whatsapp\.com|web\.whatsapp\.com)/i.test(href || ''); 176 } 177 178 /** Texto prerellenado del enlace de WhatsApp, ya decodificado. */ 179 function readMessage(href) { 180 var m = /[?&]text=([^&]*)/i.exec(href || ''); 181 if (!m) { return ''; } 182 try { 183 return decodeURIComponent(m[1].replace(/\+/g, ' ')); 184 } catch (e) { 185 return m[1]; 186 } 187 } 188 189 /** 190 * Saca evento, fecha y sala del mensaje del enlace. 191 * Formato de origen: "⦠el dÃa 01-07-2026 en NOMBRE FIESTA (SALA)" 192 * 193 * Si un dÃa wti-core añade atributos de datos limpios (data-wti-event, 194 * data-wti-date, data-wti-venue), se usan esos con preferencia. 195 */ 196 function parseContext(link) { 197 var ctx = { event: '', date: '', dateRaw: '', venue: '' }; 198 199 if (link.dataset && link.dataset.wtiEvent) { 200 ctx.event = link.dataset.wtiEvent; 201 ctx.dateRaw = link.dataset.wtiDate || ''; 202 ctx.venue = link.dataset.wtiVenue || ''; 203 } else { 204 var msg = readMessage(link.getAttribute('href')); 205 var m = /el\s+d[Ãi]a\s+(\d{1,2}[-/]\d{1,2}[-/]\d{4})\s+en\s+(.+)$/i.exec(msg); 206 if (m) { 207 ctx.dateRaw = m[1]; 208 var rest = m[2].trim(); 209 // La sala va entre paréntesis al final. A veces vienen dobles. 210 var v = /^(.*?)\s*\(+\s*([^()]+?)\s*\)+\s*$/.exec(rest); 211 if (v) { 212 ctx.event = v[1].trim(); 213 ctx.venue = v[2].trim(); 214 } else { 215 ctx.event = rest; 216 } 217 } 218 } 219 220 ctx.date = prettyDate(ctx.dateRaw); 221 return ctx; 222 } 223 224 /** "01-07-2026" â "1 de julio de 2026" / "July 1, 2026" */ 225 function prettyDate(raw) { 226 var m = /^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/.exec(raw || ''); 227 if (!m) { return raw || ''; } 228 var d = parseInt(m[1], 10); 229 var mo = parseInt(m[2], 10); 230 if (mo < 1 || mo > 12 || !MESES[mo - 1]) { return raw; } 231 232 return String(T.dateFormat || '%1$d de %2$s de %3$d') 233 .replace('%1$d', d) 234 .replace('%2$s', MESES[mo - 1]) 235 .replace('%3$d', m[3]); 236 } 237 238 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 239 // 2. Modal 240 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 241
242 var overlay = null; 243 var lastFocus = null; 244 245 function el(tag, cls, text) { 246 var n = document.createElement(tag); 247 if (cls) { n.className = cls; } 248 if (text !== undefined) { n.textContent = text; } 249 return n; 250 } 251 252 function field(labelText, control, hintText) { 253 var wrap = el('div', 'wti-mvip-field'); 254 var lab = el('label', 'wti-mvip-label', labelText); 255 256 // Un grupo de botones no es un campo etiquetable: se enlaza por 257 // aria-labelledby en vez de por "for", que ahà no vale. 258 if ('DIV' === control.tagName) { 259 lab.id = control.id + '-label'; 260 control.setAttribute('aria-labelledby', lab.id); 261 } else { 262 lab.setAttribute('for', control.id); 263 } 264 265 wrap.appendChild(lab); 266 wrap.appendChild(control); 267 if (hintText) { wrap.appendChild(el('p', 'wti-mvip-hint', hintText)); } 268 wrap.appendChild(el('p', 'wti-mvip-error')); 269 return wrap; 270 } 271 272 /** 273 * Grupo de botones excluyentes (chicos / chicas / mixto). 274 * Devuelve { wrap, first, value() }. 275 */ 276 function pills(id, options) { 277 var wrap = el('div', 'wti-mvip-pills'); 278 wrap.id = id; 279 wrap.setAttribute('role', 'radiogroup'); 280 281 var chosen = ''; 282 var buttons = []; 283 284 options.forEach(function (label) { 285 var b = el('button', 'wti-mvip-pill', label); 286 b.type = 'button'; 287 b.setAttribute('role', 'radio'); 288 b.setAttribute('aria-checked', 'false'); 289 b.addEventListener('click', function () { 290 chosen = label; 291 buttons.forEach(function (other) { 292 var on = other === b; 293 other.classList.toggle('is-on', on); 294 other.setAttribute('aria-checked', on ? 'true' : 'false'); 295 }); 296 }); 297 buttons.push(b); 298 wrap.appendChild(b); 299 }); 300 301 return { 302 wrap: wrap, 303 first: buttons[0], 304 value: function () { return chosen; } 305 }; 306 } 307 308 function input(id, type, attrs) { 309 var i = document.createElement(type === 'textarea' ? 'textarea' : 'input'); 310 i.id = id; 311 i.className = 'wti-mvip-input'; 312 if (type !== 'textarea') { i.type = type; } 313 Object.keys(attrs || {}).forEach(function (k) { i.setAttribute(k, attrs[k]); }); 314 return i; 315 } 316 317 function buildModal(ctx) { 318 overlay = el('div', 'wti-mvip-overlay'); 319 overlay.setAttribute('role', 'dialog'); 320 overlay.setAttribute('aria-modal', 'true'); 321 overlay.setAttribute('aria-label', T.title); 322 overlay.setAttribute('lang', LANG); 323 324 var panel = el('div', 'wti-mvip-panel'); 325 326 // ââ Cabecera 327 var head = el('div', 'wti-mvip-head'); 328 head.appendChild(el('h2', 'wti-mvip-title', T.title)); 329 var close = el('button', 'wti-mvip-close'); 330 close.type = 'button'; 331 close.setAttribute('aria-label', T.close); 332 close.innerHTML = '×'; 333 close.addEventListener('click', closeModal); 334 head.appendChild(close); 335 panel.appendChild(head); 336 337 var body = el('div', 'wti-mvip-body'); 338 339 if (CFG.isPreview) { 340 body.appendChild(el('div', 'wti-mvip-preview', T.preview)); 341 } 342 343 // ââ Resumen del evento (viene del botón, no se pide) 344 if (ctx.event || ctx.date || ctx.venue) { 345 var sum = el('div', 'wti-mvip-summary'); 346 [[T.event, ctx.event], [T.date, ctx.date], [T.venue, ctx.venue]].forEach(function (row) { 347 if (!row[1]) { return; } 348 var line = el('div', 'wti-mvip-summary-row'); 349 line.appendChild(el('span', 'wti-mvip-summary-key', row[0])); 350 line.appendChild(el('span', 'wti-mvip-summary-val', row[1])); 351 sum.appendChild(line); 352 }); 353 body.appendChild(sum); 354 } 355 356 if (T.intro) { body.appendChild(el('p', 'wti-mvip-intro', T.intro)); } 357 358 var form = el('form', 'wti-mvip-form'); 359 form.noValidate = true; 360 361 // ââ Personas + edades (en dos columnas) 362 var people = input('wti-mvip-people', 'number', { 363 min: '1', max: String(CFG.maxPeople), inputmode: 'numeric', placeholder: '2' 364 }); 365 var ages = document.createElement('select'); 366 ages.id = 'wti-mvip-ages'; 367 ages.className = 'wti-mvip-input'; 368 var ph = el('option', null, T.agesPick); 369 ph.value = ''; 370 ages.appendChild(ph); 371 AGES.forEach(function (r) { 372 var o = el('option', null, r); 373 o.value = r; 374 ages.appendChild(o); 375 }); 376 377 var rowA = el('div', 'wti-mvip-row'); 378 rowA.appendChild(field(T.people, people)); 379 rowA.appendChild(field(T.ages, ages)); 380 form.appendChild(rowA); 381 382 // ââ Tipo de grupo: chicos / chicas / mixto. 383 // Son tres opciones y van en botones, no en desplegable: en el móvil 384 // es un toque en vez de tres, y se ve de un vistazo lo que hay. 385 var group = pills('wti-mvip-group', GROUPS); 386 form.appendChild(field(T.group, group.wrap)); 387 388 // ââ Nombre + apellido 389 var first = input('wti-mvip-first', 'text', { autocomplete: 'given-name' }); 390 var last = input('wti-mvip-last', 'text', { autocomplete: 'family-name' }); 391 var rowB = el('div', 'wti-mvip-row'); 392 rowB.appendChild(field(T.firstName, first)); 393 rowB.appendChild(field(T.lastName, last)); 394 form.appendChild(rowB); 395 396 // ââ Email 397 var email = input('wti-mvip-email', 'email', { 398 autocomplete: 'email', placeholder: T.emailPh || '[email protected]' 399 }); 400 form.appendChild(field(T.email, email)); 401 402 // ââ Teléfono 403 // 404 // Hay que preguntarlo: el mensaje lo escribe el navegador y NO puede
405 // saber desde qué número lo enviará después el cliente. Sin este campo 406 // la solicitud llegarÃa sin teléfono en el texto. 407 var phone = input('wti-mvip-phone-num', 'tel', { 408 autocomplete: 'tel', placeholder: T.phonePh || '' 409 }); 410 form.appendChild(field(T.phone, phone)); 411 412 // ââ Comentario 413 var comment = input('wti-mvip-comment', 'textarea', { 414 rows: '3', maxlength: String(CFG.commentMax) 415 }); 416 form.appendChild(field(T.comment, comment)); 417 418 // ââ Aviso de consumo mÃnimo 419 if (T.note) { form.appendChild(el('p', 'wti-mvip-note', T.note)); } 420 421 // ââ Privacidad 422 // 423 // Dos formas, según los ajustes: 424 // · casilla obligatoria (hay que marcarla para enviar); 425 // · aviso informativo con enlace, sin marcar nada. 426 // 427 // Gestionar la solicitud se apoya en la propia petición del cliente, 428 // no en su consentimiento, asà que la casilla no es obligatoria por 429 // ley; informar sÃ. La casilla de novedades es aparte y esa sà es 430 // consentimiento, por eso nunca bloquea. 431 var privacy; 432 if (CFG.privacyMode === 'notice') { 433 form.appendChild(privacyNotice()); 434 privacy = { box: { checked: true }, notice: true }; 435 } else { 436 privacy = checkRow('wti-mvip-privacy', T.privacy, T.privacyUrl); 437 form.appendChild(privacy.wrap); 438 } 439 440 var marketing = null; 441 if (T.marketing) { 442 marketing = checkRow('wti-mvip-marketing', T.marketing, ''); 443 form.appendChild(marketing.wrap); 444 } 445 446 // ââ Botón de envÃo 447 var submit = el('button', 'wti-mvip-submit', T.submit); 448 submit.type = 'submit'; 449 form.appendChild(submit); 450 451 var done = el('p', 'wti-mvip-done', T.success); 452 form.appendChild(done); 453 454 form.addEventListener('submit', function (ev) { 455 ev.preventDefault(); 456 var data = { 457 people: people.value.trim(), 458 ages: ages.value, 459 group: group.value(), 460 first: first.value.trim(), 461 last: last.value.trim(), 462 email: email.value.trim(), 463 phone: phone.value.trim(), 464 comment: comment.value.trim(), 465 privacy: privacy.box.checked, 466 marketing: marketing ? marketing.box.checked : null 467 }; 468 var controls = { 469 people: people, ages: ages, group: group.first, 470 first: first, last: last, email: email, phone: phone 471 }; 472 if (!validate(form, data, controls, privacy)) { 473 return; 474 } 475 openWhatsApp(ctx, data); 476 overlay.classList.add('is-sent'); 477 done.classList.add('is-visible'); 478 }); 479 480 body.appendChild(form); 481 panel.appendChild(body); 482 overlay.appendChild(panel); 483 484 overlay.addEventListener('mousedown', function (ev) { 485 if (ev.target === overlay) { closeModal(); } 486 }); 487 488 return overlay; 489 } 490 491 /** Aviso de privacidad con enlace, sin casilla que marcar. */ 492 function privacyNotice() { 493 var p = el('p', 'wti-mvip-privacy-notice', T.privacyNotice || ''); 494 if (T.privacyUrl) { 495 p.appendChild(document.createTextNode(' ')); 496 var a = document.createElement('a'); 497 a.href = T.privacyUrl; 498 a.target = '_blank'; 499 a.rel = 'noopener'; 500 a.textContent = T.privacyLink || 'PolÃtica de Privacidad'; 501 p.appendChild(a); 502 } 503 return p; 504 } 505 506 function checkRow(id, labelText, url) { 507 var wrap = el('div', 'wti-mvip-check'); 508 var box = document.createElement('input'); 509 box.type = 'checkbox'; 510 box.id = id; 511 var lab = document.createElement('label'); 512 lab.setAttribute('for', id); 513 lab.textContent = labelText; 514 if (url) { 515 lab.appendChild(document.createTextNode(' ')); 516 var a = document.createElement('a'); 517 a.href = url; 518 a.target = '_blank'; 519 a.rel = 'noopener'; 520 a.textContent = 'â'; 521 lab.appendChild(a); 522 } 523 wrap.appendChild(box); 524 wrap.appendChild(lab); 525 wrap.appendChild(el('p', 'wti-mvip-error')); 526 return { wrap: wrap, box: box }; 527 } 528 529 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 530 // 3. Validación 531 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 532 533 function markError(control, message) { 534 var wrap = control.closest('.wti-mvip-field') || control.closest('.wti-mvip-check'); 535 if (!wrap) { return; } 536 wrap.classList.add('has-error'); 537 var p = wrap.querySelector('.wti-mvip-error'); 538 if (p) { p.textContent = message; } 539 } 540 541 function clearErrors(form) { 542 form.querySelectorAll('.has-error').forEach(function (n) { 543 n.classList.remove('has-error'); 544 var p = n.querySelector('.wti-mvip-error'); 545 if (p) { p.textContent = ''; } 546 }); 547 } 548 549 function validate(form, data, controls, privacy) { 550 clearErrors(form); 551 var ok = true; 552 var firstBad = null; 553 554 function fail(control, msg) { 555 markError(control, msg); 556 if (!firstBad) { firstBad = control; } 557 ok = false;
558 } 559 560 var n = parseInt(data.people, 10); 561 if (!data.people || isNaN(n) || n < 1 || n > CFG.maxPeople) { 562 fail(controls.people, T.peopleBad); 563 } 564 if (!data.ages) { fail(controls.ages, T.required); } 565 if (GROUPS.length && !data.group) { fail(controls.group, T.required); } 566 if (!data.first) { fail(controls.first, T.required); } 567 if (!data.last) { fail(controls.last, T.required); } 568 if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(data.email)) { 569 fail(controls.email, T.emailBad); 570 } 571 // Teléfono: al menos 8 cifras. No se valida el paÃs â la mitad de los 572 // clientes son extranjeros y cada paÃs tiene su formato. 573 if (String(data.phone).replace(/\D+/g, '').length < 8) { 574 fail(controls.phone, T.phoneBad); 575 } 576 // Con el aviso informativo no hay casilla que marcar: nada que validar. 577 if (!privacy.notice && !data.privacy) { fail(privacy.box, T.privacyBad); } 578 579 if (firstBad) { firstBad.focus(); } 580 return ok; 581 } 582 583 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 584 // 4. Mensaje de WhatsApp 585 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 586 587 /** 588 * Mismo formato que la notificación del asistente, para que las dos vÃas 589 * lleguen igual y se puedan reenviar a la sala sin retocar nada. 590 */ 591 function buildMessage(ctx, data) { 592 var lines = []; 593 594 function add(key, val) { 595 if (val !== '' && val !== null && val !== undefined) { 596 lines.push(key + ': ' + val); 597 } 598 } 599 600 if (T.greeting) { lines.push(T.greeting, ''); } 601 602 add(T.msgName || T.firstName, data.first + ' ' + data.last); 603 add(T.email, data.email); 604 add(T.msgPhone || T.phone, data.phone); 605 add(T.event, ctx.event); 606 add(T.date, ctx.date); 607 add(T.venue, ctx.venue); 608 add(T.people, data.people); 609 610 // "10 chicos de 25-35" â una sola lÃnea, como la del asistente. 611 if (data.group || data.ages) { 612 add(T.msgGroup, String(T.groupFormat || '%1$s %2$s de %3$s') 613 .replace('%1$s', data.people) 614 .replace('%2$s', String(data.group || '').toLowerCase()) 615 .replace('%3$s', data.ages) 616 .replace(/\s{2,}/g, ' ') 617 .trim()); 618 } 619 620 if (data.comment) { add(T.msgComment || T.commentKey, data.comment); } 621 if (data.marketing !== null) { 622 add(T.msgOptIn, data.marketing ? T.msgYes : T.msgNo); 623 } 624 625 if (T.closing) { lines.push('', T.closing); } 626 627 return lines.join('\n'); 628 } 629 630 function openWhatsApp(ctx, data) { 631 var url = 'https://wa.me/' + CFG.phone + '?text=' + encodeURIComponent(buildMessage(ctx, data)); 632 // Dentro del gesto de clic: el navegador no lo bloquea. 633 window.open(url, '_blank', 'noopener'); 634 } 635 636 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 637 // 5. Abrir / cerrar 638 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 639 640 function openModal(ctx) { 641 closeModal(); 642 lastFocus = document.activeElement; 643 document.body.appendChild(buildModal(ctx)); 644 document.body.classList.add('wti-mvip-open'); 645 requestAnimationFrame(function () { overlay.classList.add('is-visible'); }); 646 var first = overlay.querySelector('input, select, textarea, button'); 647 if (first) { first.focus(); } 648 } 649 650 function closeModal() { 651 if (!overlay) { return; } 652 overlay.remove(); 653 overlay = null; 654 document.body.classList.remove('wti-mvip-open'); 655 if (lastFocus && lastFocus.focus) { lastFocus.focus(); } 656 } 657 658 document.addEventListener('keydown', function (ev) { 659 if ('Escape' === ev.key && overlay) { closeModal(); } 660 }); 661 662 /** ¿Es este enlace el botón de MESAS VIP? */ 663 function isVipLink(link) { 664 if (!link) { return false; } 665 var href = link.getAttribute('href') || ''; 666 return isWhatsAppLink(href) && RE_IS_VIP.test(readMessage(href)); 667 } 668 669 function closestLink(target) { 670 return target && target.closest ? target.closest('a') : null; 671 } 672 673 // Al acercarse al botón se va pidiendo la configuración, para que al 674 // pulsar no haya que esperar a nada. Quien no lo toca, no gasta nada. 675 ['pointerenter', 'touchstart'].forEach(function (evName) { 676 document.addEventListener(evName, function (ev) { 677 if (isVipLink(closestLink(ev.target))) { loadConfig(); } 678 }, { capture: true, passive: true }); 679 }); 680 681 // Delegación: el calendario puede repintar los botones por AJAX. 682 document.addEventListener('click', function (ev) { 683 var link = closestLink(ev.target); 684 if (!isVipLink(link)) { return; } 685 686 // Se para el enlace SIEMPRE y se decide después. Si resulta que el 687 // formulario no está activo âo el servidor no contestaâ se sigue al 688 // WhatsApp de siempre, que es lo que el enlace hacÃa. 689 ev.preventDefault(); 690 ev.stopPropagation(); 691 692 var href = link.getAttribute('href'); 693 694 loadConfig().then(function (cfg) { 695 if (!cfg) { 696 window.location.href = href; 697 return; 698 } 699 // El contexto se lee DESPUÃS de tener la configuración: la fecha 700 // se escribe en el idioma que toque, y los meses vienen con ella. 701 var ctx = parseContext(link); 702 loadStyles().then(function () { openModal(ctx); }); 703 }); 704 }, true); 705 706})();
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.