1// Comportamiento de la guÃa. No envÃa ningún dato; lo único que guarda en el 2// navegador es el tema claro u oscuro, y solo si se elige uno distinto al del 3// dispositivo. Sin este script la página se lee entera, con cada recomendación 4// desplegada bajo su tÃtulo. 5(function () { 6 "use strict"; 7 document.documentElement.classList.add("js"); 8 var ESCRITORIO = window.matchMedia("(min-width: 62rem)"); 9 var REDUCIDO = window.matchMedia("(prefers-reduced-motion: reduce)"); 10 11 /* ---------- Cabecera fija ---------- */ 12 // La cabecera se queda arriba al bajar. Su altura se guarda en --alto-cabecera 13 // para que los enlaces internos no dejen el destino debajo de ella. En el 14 // móvil, donde ocupa mucho, se oculta al bajar y vuelve en cuanto se sube. 15 var cabecera = document.querySelector(".pizarra"); 16 if (cabecera) { 17 var MOVIL = window.matchMedia("(max-width: 40rem)"); 18 var ultima = window.scrollY; 19 var medir = function () { 20 document.documentElement.style.setProperty("--alto-cabecera", cabecera.offsetHeight + "px"); 21 }; 22 medir(); 23 window.addEventListener("resize", medir); 24 window.addEventListener("scroll", function () { 25 var y = window.scrollY; 26 if (Math.abs(y - ultima) < 8) { return; } 27 var menuAbierto = cabecera.querySelector('[aria-expanded="true"]'); 28 var ocultar = MOVIL.matches && y > ultima && y > cabecera.offsetHeight && !menuAbierto; 29 cabecera.classList.toggle("escondida", ocultar); 30 ultima = y; 31 }, { passive: true }); 32 // Con el teclado, la cabecera reaparece al llegar a uno de sus controles. 33 cabecera.addEventListener("focusin", function () { cabecera.classList.remove("escondida"); }); 34 } 35 36 /* ---------- Tema claro u oscuro ---------- */ 37 // El aspecto sigue al del dispositivo mientras no se elija otra cosa. Si se 38 // elige justo el que ya trae el dispositivo, se vuelve a seguirlo. La clave 39 // es la misma que usa el arranque en <head> (CLAVE_TEMA en construir.py). 40 var CLAVE_TEMA = "vibe-responsable:tema"; 41 var sistemaOscuro = window.matchMedia("(prefers-color-scheme: dark)"); 42 var guardar = function (valor) { 43 try { if (valor === null) { localStorage.removeItem(CLAVE_TEMA); } else { localStorage.setItem(CLAVE_TEMA, valor); } } catch (e) {} 44 }; 45 var leer = function () { try { return localStorage.getItem(CLAVE_TEMA); } catch (e) { return null; } }; 46 var aplicarTema = function (oscuro, manual) { 47 document.documentElement.dataset.theme = oscuro ? "dark" : "light"; 48 if (manual) { guardar(oscuro === sistemaOscuro.matches ? null : (oscuro ? "dark" : "light")); } 49 }; 50 var tema = document.querySelector(".tema"); 51 if (tema) { 52 tema.addEventListener("click", function () { aplicarTema(document.documentElement.dataset.theme !== "dark", true); }); 53 } 54 sistemaOscuro.addEventListener("change", function (ev) { if (leer() === null) { aplicarTema(ev.matches); } }); 55 56 /* ---------- Menús de la cabecera ---------- */ 57 // El botón de idiomas despliega los idiomas de la guÃa, que llevan a la misma página 58 // en cada uno; el de la impresora, imprimir la página o descargar la guÃa completa en 59 // PDF. Cada menú se cierra al elegir, al pulsar fuera o con Escape. 60 document.querySelectorAll(".desplegable").forEach(function (caja) { 61 var boton = caja.querySelector("button[aria-haspopup]"); 62 var menu = caja.querySelector(".menu"); 63 if (!boton || !menu) { return; } 64 var abrirMenu = function (abierto) { 65 menu.hidden = !abierto; 66 boton.setAttribute("aria-expanded", abierto ? "true" : "false"); 67 if (abierto) { (menu.querySelector("[aria-current]") || menu.querySelector("[role=menuitem]")).focus(); } 68 }; 69 boton.addEventListener("click", function () { abrirMenu(menu.hidden); }); 70 var imprimir = menu.querySelector(".menu-imprimir"); 71 if (imprimir) { imprimir.addEventListener("click", function () { abrirMenu(false); window.print(); }); } 72 menu.querySelectorAll("a").forEach(function (a) { a.addEventListener("click", function () { abrirMenu(false); }); }); 73 document.addEventListener("click", function (ev) { 74 if (!menu.hidden && !boton.contains(ev.target) && !menu.contains(ev.target)) { abrirMenu(false); } 75 }); 76 document.addEventListener("keydown", function (ev) { 77 if (ev.key === "Escape" && !menu.hidden) { abrirMenu(false); boton.focus(); } 78 }); 79 }); 80 81 /* ---------- Lista y panel ---------- */ 82 var panel = document.querySelector(".panel"); 83 var puntos = Array.prototype.slice.call(document.querySelectorAll(".punto")); 84 var detalles = {}; 85 var activo = 0; 86 87 puntos.forEach(function (li) { 88 var d = li.querySelector(".detalle") || document.getElementById("detalle-" + li.id.split("-")[1]); 89 detalles[li.id.split("-")[1]] = d; 90 }); 91 92 // En escritorio las explicaciones viven en el panel; en móvil, bajo su fila. 93 function colocar() { 94 puntos.forEach(function (li) { 95 var n = li.id.split("-")[1], d = detalles[n]; 96 if (ESCRITORIO.matches) { if (d.parentNode !== panel) { panel.appendChild(d); } } 97 else if (d.parentNode !== li) { li.appendChild(d); } 98 }); 99 // En escritorio el panel muestra siempre una recomendación; en móvil empiezan todas plegadas 100 if (ESCRITORIO.matches && !activo) { activo = 1; } 101 pintar(); 102 } 103 104 function pintar() { 105 puntos.forEach(function (li) { 106 var n = +li.id.split("-")[1], es = n === activo; 107 li.classList.toggle("activo", es); 108 li.querySelector(".abrir").setAttribute("aria-expanded", es ? "true" : "false"); 109 detalles[n].classList.toggle("visible", es); 110 }); 111 } 112 113 function seleccionar(n, opciones) { 114 opciones = opciones || {}; 115 n = +n || 0; 116 if (n === activo && opciones.alternar && !ESCRITORIO.matches) { n = 0; } 117 activo = n; 118 pintar(); 119 var destino = n ? "#recomendacion-" + n : location.pathname + location.search;
120 if (opciones.historial !== false) { try { history.replaceState(null, "", destino); } catch (e) { /* file:// */ } } 121 if (n && opciones.foco) { 122 var t = detalles[n].querySelector("h2"); 123 if (ESCRITORIO.matches) { t.focus({ preventScroll: true }); } 124 else { setTimeout(function () { puntos[n - 1].scrollIntoView({ block: "nearest", behavior: REDUCIDO.matches ? "auto" : "smooth" }); }, 340); } 125 } 126 } 127 128 if (panel && puntos.length) { 129 puntos.forEach(function (li, i) { 130 var enlace = li.querySelector(".abrir"); 131 enlace.setAttribute("role", "button"); 132 enlace.addEventListener("click", function (ev) { ev.preventDefault(); seleccionar(i + 1, { alternar: true, foco: true }); }); 133 enlace.addEventListener("keydown", function (ev) { 134 if (ev.key === " ") { ev.preventDefault(); enlace.click(); } 135 if (ev.key === "ArrowDown" || ev.key === "ArrowUp") { 136 ev.preventDefault(); 137 var j = Math.max(0, Math.min(puntos.length - 1, i + (ev.key === "ArrowDown" ? 1 : -1))); 138 puntos[j].querySelector(".abrir").focus(); 139 } 140 }); 141 }); 142 document.querySelectorAll(".paso").forEach(function (b) { 143 b.addEventListener("click", function () { seleccionar(b.dataset.ir, { foco: true }); }); 144 }); 145 var desdeHash = function () { 146 var m = /^#(?:recomendacion|detalle)-(\d+)$/.exec(location.hash); 147 if (m && detalles[m[1]]) { seleccionar(m[1], { historial: false }); } 148 }; 149 window.addEventListener("hashchange", desdeHash); 150 if (ESCRITORIO.addEventListener) { ESCRITORIO.addEventListener("change", colocar); } else { ESCRITORIO.addListener(colocar); } 151 colocar(); 152 desdeHash(); 153 window.addEventListener("beforeprint", function () { document.documentElement.classList.remove("js"); }); 154 window.addEventListener("afterprint", function () { document.documentElement.classList.add("js"); }); 155 } 156 157 /* ---------- Archivos para la IA plegados ---------- */ 158 // Al imprimir se despliegan, para que el papel lleve el texto; después vuelven como estaban 159 var plegados = []; 160 window.addEventListener("beforeprint", function () { 161 plegados = Array.prototype.filter.call(document.querySelectorAll("details.archivo-ia"), function (d) { return !d.open; }); 162 plegados.forEach(function (d) { d.open = true; }); 163 }); 164 window.addEventListener("afterprint", function () { plegados.forEach(function (d) { d.open = false; }); }); 165 166 // Abrir y cerrar con un movimiento suave de la altura, salvo con movimiento reducido 167 document.querySelectorAll("details.archivo-ia").forEach(function (d) { 168 var resumen = d.querySelector("summary"), animacion = null; 169 resumen.addEventListener("click", function (ev) { 170 if (REDUCIDO.matches || !d.animate) { return; } 171 ev.preventDefault(); 172 var abrir = !d.open || (animacion && d.dataset.cerrando === "1"); 173 var desde = d.getBoundingClientRect().height; 174 if (animacion) { animacion.cancel(); } 175 if (abrir) { d.open = true; } 176 d.dataset.cerrando = abrir ? "0" : "1"; 177 var hasta = abrir ? d.scrollHeight : resumen.getBoundingClientRect().height + (d.offsetHeight - d.clientHeight); 178 d.style.overflow = "hidden"; 179 animacion = d.animate({ height: [desde + "px", hasta + "px"] }, 180 { duration: Math.min(450, 180 + Math.abs(hasta - desde) / 6), easing: "cubic-bezier(0.2, 0, 0, 1)" }); 181 animacion.onfinish = function () { 182 if (!abrir) { d.open = false; } 183 d.style.overflow = ""; d.dataset.cerrando = ""; animacion = null; 184 }; 185 animacion.oncancel = function () { d.style.overflow = ""; }; 186 }); 187 }); 188 189 /* ---------- Copiar los textos para la IA ---------- */ 190 document.querySelectorAll(".copiar").forEach(function (boton) { 191 boton.addEventListener("click", function () { 192 var texto = boton.closest(".copiable").querySelector("pre").textContent; 193 var rotulo = boton.querySelector("span") || boton; // con icono, cambia solo el rótulo 194 var hecho = function () { 195 var antes = rotulo.textContent; 196 rotulo.textContent = boton.dataset.hecho; 197 setTimeout(function () { rotulo.textContent = antes; }, 1800); 198 }; 199 var respaldo = function () { 200 var a = document.createElement("textarea"); 201 a.value = texto; a.setAttribute("readonly", ""); a.style.position = "fixed"; a.style.opacity = "0"; 202 document.body.appendChild(a); a.select(); 203 try { document.execCommand("copy"); } catch (e) { /* nada que hacer */ } 204 document.body.removeChild(a); 205 }; 206 if (navigator.clipboard && navigator.clipboard.writeText) { 207 navigator.clipboard.writeText(texto).then(hecho, function () { respaldo(); hecho(); }); 208 } else { respaldo(); hecho(); } 209 }); 210 }); 211 212 /* ---------- Portada: la infografÃa, como mucho, del alto del texto ---------- */ 213 // El CSS le da el alto que deja la pantalla; si asà queda más alta que el texto de al lado, 214 // se reduce lo que sobra, para que el espacio libre quede al final de la página y no en medio. 215 // Al estrecharse la imagen se ensancha el texto y baja su alto, asà que se repite hasta ajustar. 216 var entrada = document.querySelector(".pagina-presentacion .entrada"); 217 if (entrada) { 218 var tarjeta = entrada.querySelector(".tarjeta"); 219 var miniatura = entrada.querySelector(".tarjeta .miniatura"); 220 var ancha = window.matchMedia("(min-width: 62rem)"); 221 var ajustarImagen = function () { 222 tarjeta.style.removeProperty("--mini-al"); 223 if (!ancha.matches) { return; } 224 entrada.classList.add("midiendo"); 225 for (var i = 0; i < 4; i++) { 226 var pasos = entrada.querySelector(".utiliza").getBoundingClientRect().bottom; 227 var boton = entrada.querySelector(".ir-guia").getBoundingClientRect().bottom; 228 var sobra = boton - pasos; 229 if (sobra < 2) { break; } 230 tarjeta.style.setProperty("--mini-al", (miniatura.getBoundingClientRect().height - sobra) + "px"); 231 } 232 entrada.classList.remove("midiendo"); 233 }; 234 ajustarImagen(); 235 window.addEventListener("resize", ajustarImagen); 236 if (document.fonts && document.fonts.ready) { document.fonts.ready.then(ajustarImagen); } 237 } 238 239 /* ---------- Ventanas de la portada con cada archivo para la IA ---------- */ 240 // Los enlaces de «Cómo empezar» abren su ventana; sin JavaScript o sin <dialog>, llevan a la 241 // página de instrucciones. Se cierran con la X, con Escape o al pulsar fuera. 242 document.querySelectorAll("a.abrir-ventana").forEach(function (enlace) { 243 var ventana = document.getElementById(enlace.dataset.ventana); 244 if (!ventana || typeof ventana.showModal !== "function") { return; } 245 // La ventana de la animación carga el vÃdeo al abrirse y lo pone en marcha con la misma pulsación, 246 // que es lo que piden los navegadores para que suene; al cerrarse, lo detiene. 247 var video = ventana.querySelector("video[data-src]"); 248 var otraVez = ventana.querySelector(".volver-a-ver"); 249 function desdeElPrincipio() { 250 if (otraVez) { otraVez.hidden = true; } 251 video.currentTime = 0; 252 var marcha = video.play(); 253 if (marcha && marcha.catch) { marcha.catch(function () {}); } 254 } 255 // Al terminar, un botón bien visible para volver a verla: el del reproductor apenas se distingue. 256 if (video && otraVez) { 257 video.addEventListener("ended", function () { otraVez.hidden = false; otraVez.focus(); }); 258 video.addEventListener("play", function () { otraVez.hidden = true; }); 259 otraVez.addEventListener("click", desdeElPrincipio); 260 } 261 enlace.addEventListener("click", function (ev) { 262 ev.preventDefault(); 263 ventana.showModal(); 264 if (video) { 265 if (!video.getAttribute("src")) { video.src = video.dataset.src; } 266 desdeElPrincipio(); 267 } 268 }); 269 ventana.querySelector(".ventana-cerrar").addEventListener("click", function () { ventana.close(); }); 270 ventana.addEventListener("click", function (ev) { if (ev.target === ventana) { ventana.close(); } }); 271 ventana.addEventListener("close", function () { 272 if (video) { video.pause(); } 273 enlace.focus(); 274 }); 275 }); 276 277 /* ---------- Visor de la infografÃa ---------- */ 278 var visor = document.querySelector("dialog.visor"); 279 if (visor && typeof visor.showModal === "function") { 280 var imagen = visor.querySelector("img"); 281 var zoom = visor.querySelector(".visor-zoom"); 282 var lienzo = visor.querySelector(".visor-lienzo"); 283 var origen = null; 284 285 var alternar = function () { 286 var acercado = visor.classList.toggle("acercado"); 287 zoom.textContent = acercado ? zoom.dataset.alejar : zoom.dataset.acercar; 288 if (!acercado) { lienzo.scrollTo({ top: 0, behavior: REDUCIDO.matches ? "auto" : "smooth" }); } 289 }; 290 var cerrar = function () { 291 if (!visor.open || visor.classList.contains("saliendo")) { return; } 292 if (REDUCIDO.matches) { visor.close(); return; } 293 visor.classList.add("saliendo"); 294 var fin = function () { visor.classList.remove("saliendo"); visor.close(); }; 295 visor.addEventListener("animationend", fin, { once: true }); 296 setTimeout(function () { if (visor.classList.contains("saliendo")) { fin(); } }, 320); 297 }; 298 299 document.querySelectorAll("a.ampliar").forEach(function (enlace) { 300 enlace.addEventListener("click", function (ev) { 301 ev.preventDefault(); 302 origen = enlace; 303 imagen.src = enlace.getAttribute("href"); 304 visor.classList.remove("acercado"); 305 zoom.textContent = zoom.dataset.acercar; 306 visor.showModal(); 307 lienzo.scrollTop = 0; 308 }); 309 }); 310 zoom.addEventListener("click", alternar); 311 imagen.addEventListener("click", alternar); 312 visor.querySelector(".visor-cerrar").addEventListener("click", cerrar); 313 lienzo.addEventListener("click", function (ev) { if (ev.target === lienzo) { cerrar(); } }); 314 visor.addEventListener("cancel", function (ev) { ev.preventDefault(); cerrar(); }); 315 visor.addEventListener("close", function () { if (origen) { origen.focus(); } }); 316 } 317 318 /* ---------- Apartados que han cambiado de página ---------- */ 319 // Un enlace antiguo a un apartado que ya no está en esta página (data-mudado, 320 // con el ancla antigua y su destino nuevo) lleva a su sitio actual. 321 var mudado = document.querySelector("[data-mudado]"); 322 if (mudado) { 323 var destinos = JSON.parse(mudado.getAttribute("data-mudado")); 324 var mudar = function () { 325 var ancla = decodeURIComponent(window.location.hash.slice(1)); 326 if (Object.prototype.hasOwnProperty.call(destinos, ancla)) { window.location.replace(destinos[ancla]); } 327 }; 328 mudar(); 329 window.addEventListener("hashchange", mudar); 330 } 331 332 /* ---------- Resultado de la evaluación VCER ---------- */ 333 // La mención del pie de un recurso evaluado enlaza a vcer.html con su resultado 334 // en la dirección: r (recomendable, mejorable o no-recomendable), p (el 335 // porcentaje, opcional), f (año y mes, 2026-10), v (la versión), t (el tÃtulo) 336 // y u (la dirección del recurso). Nada se envÃa ni se guarda: los datos solo 337 // se leen de la dirección y se escriben como texto. Si r no es válido, la 338 // página se muestra sin el recuadro; cualquier otro dato que no lo sea se omite. 339 var recuadro = document.querySelector(".resultado-vcer"); 340 if (recuadro) { 341 var datos = new URLSearchParams(window.location.search); 342 var fila = /^(recomendable|mejorable|no-recomendable)$/.test(datos.get("r") || "") && 343 document.querySelector('.resultados-vcer tr[data-resultado="' + datos.get("r") + '"]'); 344 if (fila) { 345 var textos = JSON.parse(recuadro.querySelector(".rv-textos").textContent); 346 var limpio = function (nombre, max) { 347 var valor = (datos.get(nombre) || "").replace(/\s+/g, " ").trim(); 348 return valor.length <= max ? valor : ""; 349 }; 350 var poner = function (plantilla, valores) { 351 return plantilla.replace(/\{([^}]+)\}/g, function (_, clave) { return valores[clave]; }); 352 }; 353 var titulo = limpio("t", 200); 354 var version = limpio("v", 30); 355 var direccion = limpio("u", 300); 356 if (!/^https?:\/\/[^\s]+$/i.test(direccion)) { direccion = ""; } 357 var porcentaje = /^\d{1,3}$/.test(datos.get("p") || "") && Number(datos.get("p")) <= 100 ? Number(datos.get("p")) : null; 358 var mes = /^(\d{4})-(\d{2})$/.exec(datos.get("f") || ""); 359 var fecha = mes && Number(mes[2]) >= 1 && Number(mes[2]) <= 12 ? 360 poner(textos.fecha, { "mes": textos.meses[Number(mes[2]) - 1], "año": mes[1] }) : ""; 361 var recurso = poner(textos.recurso[(titulo ? "t" : "") + (version ? "v" : "")], { t: titulo, v: version }); 362 recuadro.querySelector(".rv-frase").textContent = poner(textos.frase, { 363 recurso: recurso, u: direccion ? " (" + direccion + ")" : "", fecha: fecha }); 364 recuadro.querySelector(".rv-nombre").textContent = fila.cells[0].textContent.trim(); 365 recuadro.querySelector(".rv-significado").textContent = fila.cells[2].textContent.trim(); 366 recuadro.querySelector(".rv-nota").textContent = textos.nota; 367 recuadro.dataset.resultado = datos.get("r"); 368 if (porcentaje !== null) { 369 recuadro.querySelector(".rv-pct").textContent = poner(textos.pct, { p: porcentaje }); 370 recuadro.querySelector(".rv-relleno").style.width = porcentaje + "%"; 371 } else { 372 recuadro.querySelector(".rv-barra").hidden = true; 373 } 374 fila.classList.add("actual"); 375 recuadro.hidden = false;
376 } 377 } 378})();
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.