PageSourceSearch

https://recursos.qloudea.com/eventos/modal-nakivo.js

js qloudea.com collected 2026-09-25 01:18:34 UTC 17,605 bytes, 355 lines download raw bytes

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">&times;</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.