1/** 2 * modal-nakivo.js â Ventana modal del evento NAKIVO x Qloudea Tech Session 3 * 4 * Destino: https://recursos.qloudea.com/eventos/modal-nakivo.js 5 * 6 * Se muestra en cualquier página de recursos.qloudea.com cuya URL contenga 7 * alguna de las palabras de CFG.palabras (sin distinguir mayúsculas), salvo en la propia landing 8 * /eventos/nakivo.html. Enlaza con esa landing. 9 * 10 * - Autónomo, sin dependencias, ES5 (vale también como HTML personalizado de GTM). 11 * - Encapsulado en Shadow DOM para no chocar con los estilos de cada página. 12 * - Aparece a los 4 s o al pasar del 35 % de scroll, lo que ocurra antes. 13 * - Si se cierra, no vuelve a salir en 3 dÃas. Si se pulsa el botón, no vuelve a salir. 14 * - Páginas «sà o sû (CFG.siempre o data-siempre): sale siempre, sin mirar la URL ni el cierre previo. 15 * - Se desactiva solo cuando empieza el evento (30/09/2026 16:30, Madrid). 16 * - Accesible: role="dialog", foco atrapado, cierre con Esc y clic fuera. 17 * - Eventos al dataLayer: modal_nakivo_mostrado, modal_nakivo_clic, modal_nakivo_cerrado. 18 * 19 * Forzar la aparición para pruebas: añade ?modal-nakivo=1 a la URL (en ese modo 20 * no se guarda el cierre ni el clic, asà las pruebas no bloquean la modal). 21 * Desbloquear un navegador: QloudeaModalNakivo.reiniciar() en la consola. 22 */ 23(function () { 24'use strict'; 25 26// ============================================================================ 27// CONFIGURACIÃN 28// ============================================================================ 29 30var CFG = { 31 host: 'recursos.qloudea.com', 32 // Subcadenas que activan la modal si aparecen en la URL (ruta + parámetros), 33 // sin distinguir mayúsculas. Basta con que aparezca una. 34 palabras: ['nakivo', 'virtualizacion', 'proxmox', 'virtuales'], 35 excluir: ['/eventos/nakivo.html', '/eventos/nakivo', '/eventos/nakivo/'], 36 37 // Páginas donde sale Sà O SÃ, aunque su URL no contenga «nakivo» y aunque 38 // el visitante la haya cerrado antes. Rutas sin dominio. Una ruta que acaba 39 // en «*» vale para todo lo que empiece asÃ: 40 // '/blog/backup-proxmox.html' â solo esa página 41 // '/kb/virtualizacion/*' â todo lo que cuelgue de esa carpeta 42 // También se puede activar desde el propio HTML de una página: 43 // <script src="/eventos/modal-nakivo.js" defer data-siempre></script> 44 siempre: [ 45 // '/blog/ejemplo.html', 46 ], 47 destino: 'https://recursos.qloudea.com/eventos/nakivo.html', 48 // Imagen servida desde recursos: la URL /web/image/ de Odoo lleva un hash que 49 // cambia al editar la imagen y no siempre admite enlazarse desde otro dominio. 50 imagen: 'https://recursos.qloudea.com/eventos/nakivo-evento.webp', 51 finEvento: '2026-09-30T16:30:00+02:00', 52 retrasoMs: 2000, 53 scrollPct: 0.35, 54 diasSilencio: 3, 55 claveCierre: 'qld_modal_nakivo_cerrado', 56 claveClic: 'qld_modal_nakivo_clic' 57}; 58 59// ============================================================================ 60// ¿TOCA MOSTRARLO? 61// ============================================================================ 62 63var forzado = /[?&]modal-nakivo=1\b/.test(location.search); 64var etiqueta = document.currentScript; 65 66function esPaginaSiempre() { 67 if (etiqueta && etiqueta.hasAttribute('data-siempre')) return true; 68 var ruta = location.pathname.toLowerCase(); 69 for (var i = 0; i < CFG.siempre.length; i++) { 70 var r = String(CFG.siempre[i]).toLowerCase(); 71 if (r.charAt(r.length - 1) === '*') { 72 if (ruta.indexOf(r.slice(0, -1)) === 0) return true; 73 } else if (ruta === r) { 74 return true; 75 } 76 } 77 return false; 78} 79var siempre = esPaginaSiempre(); 80 81function leer(clave) { 82 try { return window.localStorage.getItem(clave); } catch (e) { return null; } 83} 84function guardar(clave, valor) { 85 try { window.localStorage.setItem(clave, valor); } catch (e) { /* modo privado: se ignora */ } 86} 87 88// Minúsculas, sin %-codificación y sin tildes: «Virtualizaci%C3%B3n» â «virtualizacion». 89function normalizar(texto) { 90 var t = String(texto); 91 try { t = decodeURIComponent(t); } catch (e) { /* URL mal codificada: se usa tal cual */ } 92 t = t.toLowerCase(); 93 if (t.normalize) t = t.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 94 return t; 95} 96 97// Devuelve '' si hay que mostrarla, o el motivo por el que no se muestra.
98function motivoParaNoMostrar() { 99 if (forzado) return ''; 100 if (Date.now() >= new Date(CFG.finEvento).getTime()) return 'el evento ya ha empezado'; 101 if (document.querySelector('[data-qloudea-modal-nakivo]')) return 'ya está montada en la página'; 102 if (siempre) return ''; 103 104 if (location.hostname !== CFG.host) return 'el dominio es ' + location.hostname + ' y no ' + CFG.host; 105 106 var ruta = location.pathname.toLowerCase(); 107 var url = normalizar(location.pathname + location.search); 108 var coincide = false; 109 for (var j = 0; j < CFG.palabras.length; j++) { 110 if (url.indexOf(normalizar(CFG.palabras[j])) !== -1) { coincide = true; break; } 111 } 112 if (!coincide) return 'la URL no contiene ninguna de: ' + CFG.palabras.join(', '); 113 for (var i = 0; i < CFG.excluir.length; i++) { 114 if (ruta === CFG.excluir[i]) return 'es la propia landing (' + ruta + ')'; 115 } 116 117 if (leer(CFG.claveClic)) return 'este navegador ya pulsó el botón (localStorage ' + CFG.claveClic + ')'; 118 119 var cerrado = parseInt(leer(CFG.claveCierre), 10); 120 if (cerrado && Date.now() - cerrado < CFG.diasSilencio * 864e5) { 121 return 'este navegador la cerró hace menos de ' + CFG.diasSilencio + ' dÃas (localStorage ' + CFG.claveCierre + ')'; 122 } 123 124 return ''; 125} 126 127var motivo = motivoParaNoMostrar(); 128if (motivo) { 129 if (window.console && console.info) console.info('[modal-nakivo] No se muestra: ' + motivo + '.'); 130 window.QloudeaModalNakivo = { 131 motivo: motivo, 132 reiniciar: function () { 133 try { localStorage.removeItem(CFG.claveCierre); localStorage.removeItem(CFG.claveClic); } catch (e) {} 134 console.info('[modal-nakivo] Reiniciado. Recarga la página.'); 135 } 136 }; 137 return; 138} 139if (window.console && console.info) console.info('[modal-nakivo] Cargado' + (siempre ? ' (página «sà o sû)' : '') + ': se abrirá a los ' + (CFG.retrasoMs / 1000) + ' s o al ' + Math.round(CFG.scrollPct * 100) + ' % de scroll.'); 140 141// ============================================================================ 142// MEDICIÃN 143// ============================================================================ 144 145function medir(evento, extra) { 146 window.dataLayer = window.dataLayer || []; 147 var datos = { event: evento, modal_pagina: location.pathname }; 148 if (extra) for (var k in extra) if (extra.hasOwnProperty(k)) datos[k] = extra[k]; 149 window.dataLayer.push(datos); 150} 151 152// ============================================================================ 153// TIPOGRAFÃA (las @font-face deben vivir en el documento, no en el shadow root) 154// ============================================================================ 155 156if (!document.getElementById('qld-fuentes-modal-nakivo')) { 157 var fuentes = document.createElement('style'); 158 fuentes.id = 'qld-fuentes-modal-nakivo'; 159 fuentes.textContent = 160 '@font-face{font-family:"Kanit";font-weight:700;font-display:swap;src:url("https://recursos.qloudea.com/fonts/kanit-700.woff2") format("woff2")}' + 161 '@font-face{font-family:"Poppins";font-weight:400;font-display:swap;src:url("https://recursos.qloudea.com/fonts/poppins-400.woff2") format("woff2")}' + 162 '@font-face{font-family:"Poppins";font-weight:600;font-display:swap;src:url("https://recursos.qloudea.com/fonts/poppins-600.woff2") format("woff2")}' + 163 '@font-face{font-family:"Poppins";font-weight:700;font-display:swap;src:url("https://recursos.qloudea.com/fonts/poppins-700.woff2") format("woff2")}'; 164 document.head.appendChild(fuentes); 165} 166 167// ============================================================================ 168// ESTILOS Y MARCADO 169// ============================================================================ 170 171var CSS = 172':host{all:initial}' + 173'*{box-sizing:border-box}' + 174'.fondo{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:16px;' + 175 'background:rgba(7,27,42,.72);opacity:0;transition:opacity .25s ease;font-family:Poppins,Arial,sans-serif;line-height:1.5}' + 176'.fondo.abierto{opacity:1}' + 177'.caja{position:relative;width:min(560px,100%);max-height:calc(100vh - 32px);overflow:auto;border-radius:26px;background:#0d2330;color:#fff;' + 178 'box-shadow:0 30px 80px rgba(0,0,0,.45);transform:translateY(18px) scale(.98);transition:transform .3s cubic-bezier(.2,.8,.2,1)}' + 179'.fondo.abierto .caja{transform:none}' + 180'.cabecera{position:relative;height:190px}' + 181'.imagen{position:absolute;inset:0;overflow:hidden;border-radius:26px 26px 0 0;background:#006b82}' + 182'.imagen img{width:100%;height:100%;object-fit:cover;display:block}' + 183'.imagen.sin-foto{background:#006b82}' + 184'.imagen.sin-foto img{display:none}' + 185'.sello{position:absolute;z-index:1;left:24px;bottom:-30px;display:flex;flex-direction:column;align-items:center;justify-content:center;' + 186 'width:84px;height:84px;border-radius:50%;background:#b7ff17;color:#0d2330;box-shadow:0 10px 25px rgba(0,0,0,.3);font-family:Kanit,Poppins,sans-serif;font-weight:700;line-height:1}' + 187'.sello b{font-size:32px}' + 188'.sello span{font-size:13px;text-transform:uppercase;letter-spacing:.06em;margin-top:2px}' + 189'.cuerpo{padding:48px 32px 30px;
189display:flex;flex-direction:column;gap:16px}' + 190'.etiqueta{align-self:flex-start;display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:999px;' + 191 'background:rgba(183,255,23,.14);color:#b7ff17;font-size:13px;font-weight:600}' + 192'.punto{width:8px;height:8px;border-radius:50%;background:#b7ff17;animation:latido 1.6s ease-in-out infinite}' + 193'@keyframes latido{0%,100%{opacity:1}50%{opacity:.25}}' + 194'h2{margin:0;font-family:Kanit,Poppins,sans-serif;font-weight:700;font-size:clamp(26px,5vw,34px);line-height:1.05;letter-spacing:-.01em;color:#fff}' + 195'h2 span{color:#b7ff17}' + 196'p{margin:0;font-size:15.5px;color:rgba(255,255,255,.82)}' + 197'.datos{display:flex;flex-wrap:wrap;gap:8px}' + 198'.dato{padding:8px 12px;border-radius:12px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);font-size:13.5px;font-weight:600}' + 199'.dato.gratis{color:#b7ff17}' + 200'.cta{display:flex;align-items:center;justify-content:center;gap:10px;min-height:56px;padding:0 28px;border-radius:999px;background:#b7ff17;' + 201 'color:#0d2330;font-weight:700;font-size:16.5px;text-decoration:none;transition:background .2s,transform .2s;margin-top:6px}' + 202'.cta:hover{background:#a5e800;transform:translateY(-1px)}' + 203'.luego{align-self:center;background:none;border:0;padding:8px 12px;color:rgba(255,255,255,.7);font:inherit;font-size:14px;cursor:pointer;text-decoration:underline}' + 204'.luego:hover{color:#fff}' + 205'.cerrar{position:absolute;top:12px;right:12px;z-index:2;width:44px;height:44px;border-radius:50%;border:0;cursor:pointer;' + 206 'background:rgba(13,35,48,.75);color:#fff;font-size:22px;line-height:1;display:grid;place-items:center;transition:background .2s}' + 207'.cerrar:hover{background:#0d2330}' + 208':focus-visible{outline:3px solid #b7ff17;outline-offset:3px}' + 209'@media (max-width:480px){.cabecera{height:150px}.cuerpo{padding:44px 22px 24px}.sello{width:72px;height:72px}.sello b{font-size:27px}}' + 210'@media (prefers-reduced-motion:reduce){.fondo,.caja,.cta{transition:none}.punto{animation:none}}'; 211 212var HTML = 213'<div class="fondo" part="fondo">' + 214 '<div class="caja" role="dialog" aria-modal="true" aria-labelledby="mn-titulo" aria-describedby="mn-texto">' + 215 '<button class="cerrar" type="button" aria-label="Cerrar">×</button>' + 216 '<div class="cabecera">' + 217 '<div class="imagen"><img src="' + CFG.imagen + '" alt="" width="1920" height="720"></div>' + 218 '<div class="sello" aria-hidden="true"><b>30</b><span>sept</span></div>' + 219 '</div>' + 220 '<div class="cuerpo">' + 221 '<span class="etiqueta"><span class="punto" aria-hidden="true"></span>Plazas limitadas · Madrid</span>' + 222 '<h2 id="mn-titulo">Tu NAS ya guarda tus VMs. <span>¿SabrÃas recuperarlas?</span></h2>' + 223 '<p id="mn-texto">Ven a la NAKIVO x Qloudea Tech Session: demo en directo de backup y recuperación sobre Proxmox, ' + 224 'tus dudas resueltas por los equipos técnicos y una promoción solo para asistentes.</p>' + 225 '<div class="datos">' + 226 '<span class="dato">30 de septiembre</span>' + 227 '<span class="dato">16:30 â 18:30</span>' + 228 '<span class="dato">Eurostars Madrid Tower</span>' + 229 '<span class="dato gratis">Gratis</span>' + 230 '</div>' + 231 '<a class="cta" href="' + CFG.destino + '">Quiero mi plaza gratis ' + 232 '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>' + 233 '</a>' + 234 '<button class="luego" type="button">Ahora no</button>' + 235 '</div>' + 236 '</div>' + 237'</div>'; 238 239// ============================================================================ 240// COMPORTAMIENTO 241// ============================================================================ 242 243var anfitrion, raiz, fondo, focoPrevio, mostrado = false, temporizador; 244 245function montar() { 246 anfitrion = document.createElement('div'); 247 anfitrion.setAttribute('data-qloudea-modal-nakivo', ''); 248 document.body.appendChild(anfitrion); 249 250 raiz = anfitrion.attachShadow ? anfitrion.attachShadow({ mode: 'open' }) : anfitrion; 251 raiz.innerHTML = '<style>' + CSS + '</style>' + HTML; 252 fondo = raiz.querySelector('.fondo'); 253 254 // Si la imagen no carga, la cabecera se queda en azul corporativo en vez 255 // de enseñar el icono de imagen rota. 256 var foto = raiz.querySelector('.imagen img'); 257 foto.addEventListener('error', function () { 258 raiz.querySelector('.imagen').classList.add('sin-foto'); 259 if (window.console && console.warn) console.warn('[modal-nakivo] No se pudo cargar la
259imagen: ' + CFG.imagen); 260 }); 261 262 raiz.querySelector('.cerrar').addEventListener('click', function () { cerrar('boton_x'); }); 263 raiz.querySelector('.luego').addEventListener('click', function () { cerrar('ahora_no'); }); 264 fondo.addEventListener('click', function (e) { if (e.target === fondo) cerrar('fondo'); }); 265 266 raiz.querySelector('.cta').addEventListener('click', function () { 267 if (!forzado) guardar(CFG.claveClic, String(Date.now())); 268 medir('modal_nakivo_clic', { modal_destino: CFG.destino }); 269 }); 270 271 document.addEventListener('keydown', teclado, true); 272} 273 274function focables() { 275 return Array.prototype.slice.call(raiz.querySelectorAll('a[href], button')); 276} 277 278function teclado(e) { 279 if (!mostrado) return; 280 if (e.key === 'Escape' || e.keyCode === 27) { cerrar('escape'); return; } 281 if (e.key !== 'Tab' && e.keyCode !== 9) return; 282 283 var lista = focables(); 284 var primero = lista[0], ultimo = lista[lista.length - 1]; 285 var activo = raiz.activeElement || document.activeElement; 286 if (e.shiftKey && activo === primero) { e.preventDefault(); ultimo.focus(); } 287 else if (!e.shiftKey && activo === ultimo) { e.preventDefault(); primero.focus(); } 288 else if (lista.indexOf(activo) === -1) { e.preventDefault(); primero.focus(); } 289} 290 291function mostrar() { 292 if (mostrado) return; 293 mostrado = true; 294 clearTimeout(temporizador); 295 window.removeEventListener('scroll', alHacerScroll); 296 297 montar(); 298 focoPrevio = document.activeElement; 299 document.documentElement.style.overflow = 'hidden'; 300 301 // Doble rAF: primero se pinta a opacidad 0 y luego se anima la entrada. 302 requestAnimationFrame(function () { 303 requestAnimationFrame(function () { 304 fondo.classList.add('abierto'); 305 raiz.querySelector('.cta').focus(); 306 }); 307 }); 308 309 medir('modal_nakivo_mostrado'); 310} 311 312function cerrar(motivo) { 313 if (!mostrado) return; 314 mostrado = false; 315 // En modo prueba (?modal-nakivo=1) no se recuerda el cierre: si no, cada 316 // prueba dejarÃa la modal bloqueada 3 dÃas en ese navegador. 317 if (!forzado) guardar(CFG.claveCierre, String(Date.now())); 318 medir('modal_nakivo_cerrado', { modal_motivo: motivo }); 319 320 fondo.classList.remove('abierto'); 321 document.removeEventListener('keydown', teclado, true); 322 document.documentElement.style.overflow = ''; 323 324 setTimeout(function () { 325 if (anfitrion && anfitrion.parentNode) anfitrion.parentNode.removeChild(anfitrion); 326 }, 300); 327 328 if (focoPrevio && focoPrevio.focus) { 329 try { focoPrevio.focus(); } catch (e) { /* elemento ya no existe */ } 330 } 331} 332 333function alHacerScroll() { 334 var alto = document.documentElement.scrollHeight - window.innerHeight; 335 if (alto > 0 && window.scrollY / alto >= CFG.scrollPct) mostrar(); 336} 337 338function iniciar() { 339 temporizador = setTimeout(mostrar, CFG.retrasoMs); 340 window.addEventListener('scroll', alHacerScroll, { passive: true }); 341} 342 343if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', iniciar); 344else iniciar(); 345 346// API mÃnima para pruebas desde la consola 347window.QloudeaModalNakivo = { 348 abrir: mostrar, 349 cerrar: function () { cerrar('api'); }, 350 reiniciar: function () { 351 try { localStorage.removeItem(CFG.claveCierre); localStorage.removeItem(CFG.claveClic); } catch (e) {} 352 } 353}; 354 355})();
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.