PageSourceSearch

https://vibe-coding-educativo.github.io/vibe-responsable/recursos/guia.js?v=bee98de690

js vibe-coding-educativo.github.io collected 2026-10-03 10:08:14 UTC 19,401 bytes, 378 lines download raw bytes

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.