Deshacer con demo activa, avisos obsoletos, hoja de opciones y foco en hojas

- Deshacer/rehacer guardan y reponen «wam-demo-activa» con cada instantánea
  ({estado, activa}); «Actualizar» pide un segundo clic si el contenido cambió
  de cuenta o de número de mensajes respecto a la demo guardada.
- WAM.puntoDeshacer cierra los avisos con acción: su «Deshacer» ya no
  desharía el cambio que anuncian.
- «Todas las opciones» replica botonAccion: URL bloqueada o no válida se
  pinta como fila inerte (aria-disabled, sin data-opcion) en el grupo de
  enlaces.
- Hojas inferiores: enfocan la primera fila (o la X), retienen Tab dentro del
  panel y devuelven el foco al cerrar (origen, fila repintada o
  #chat-cuerpo); no roban el foco durante «Reproducir demo».
- Tests e2e de regresión en tests/e2e/regresiones.spec.mjs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
urieljareth
2026-09-27 22:08:46 -06:00
co-authored by Claude Opus 5.5
parent 1176003b42
commit 875ae9ad67
4 changed files with 255 additions and 7 deletions
+54 -5
View File
@@ -128,9 +128,32 @@
/* ------------------------------------------------------------------
Historial deshacer / rehacer
------------------------------------------------------------------ */
/* Cada instantánea guarda también qué demo guardada estaba en uso
(«wam-demo-activa»): al deshacer o rehacer se repone, para que
«Actualizar» nunca escriba el contenido de otra demo en su hueco. */
function instantanea() {
return { estado: clonar(WAM.estado), activa: lsGet(CLAVE_ACTIVA) };
}
function reponerActiva(id) {
if (id) lsSet(CLAVE_ACTIVA, id); else lsDel(CLAVE_ACTIVA);
}
/* Un cambio nuevo deja obsoletos los avisos con acción («Deshacer» /
«Rehacer»): su botón ya no desharía lo que anuncian. */
function cerrarAvisosConAccion() {
var ts = document.querySelectorAll("#ed-toasts .ed-toast");
for (var i = 0; i < ts.length; i++) {
if (!ts[i].querySelector(".ed-toast-accion")) continue;
if (typeof ts[i].__cerrar === "function") ts[i].__cerrar();
else if (ts[i].parentNode) ts[i].parentNode.removeChild(ts[i]);
}
}
WAM.puntoDeshacer = function () {
if (!WAM.estado) return;
pilaDeshacer.push(clonar(WAM.estado));
cerrarAvisosConAccion();
pilaDeshacer.push(instantanea());
if (pilaDeshacer.length > LIMITE_HISTORIAL) pilaDeshacer.shift();
pilaRehacer = [];
pintarBotonesHistorial();
@@ -147,8 +170,10 @@
WAM.deshacer = function () {
if (!pilaDeshacer.length) { toast("No hay nada que deshacer"); return false; }
pilaRehacer.push(clonar(WAM.estado));
sustituirEstado(conTemaActual(pilaDeshacer.pop()));
var previo = pilaDeshacer.pop();
pilaRehacer.push(instantanea());
sustituirEstado(conTemaActual(previo.estado));
reponerActiva(previo.activa);
pintarBotonesHistorial();
toast("Cambio deshecho", { accion: { texto: "Rehacer", fn: WAM.rehacer } });
return true;
@@ -156,8 +181,10 @@
WAM.rehacer = function () {
if (!pilaRehacer.length) { toast("No hay nada que rehacer"); return false; }
pilaDeshacer.push(clonar(WAM.estado));
sustituirEstado(conTemaActual(pilaRehacer.pop()));
var siguiente = pilaRehacer.pop();
pilaDeshacer.push(instantanea());
sustituirEstado(conTemaActual(siguiente.estado));
reponerActiva(siguiente.activa);
pintarBotonesHistorial();
toast("Cambio rehecho");
return true;
@@ -372,6 +399,18 @@
return entrada;
}
/* ¿El estado actual cambió de cuenta o de número de mensajes respecto a
lo guardado en el hueco «id»? */
function cambioSustancial(id) {
var guardada = null;
try { guardada = JSON.parse(lsGet(PREFIJO_DEMO + id) || "null"); } catch (e) { guardada = null; }
if (!guardada || typeof guardada !== "object") return false;
var nomG = (guardada.cuenta && guardada.cuenta.nombre) || "";
var nomA = (WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "";
var nG = Array.isArray(guardada.mensajes) ? guardada.mensajes.length : 0;
return nomG !== nomA || nG !== nMensajes();
}
function cargarDemo(id) {
var bruto = lsGet(PREFIJO_DEMO + id);
var datos = null;
@@ -499,11 +538,21 @@
var bGuardar = boton(activa ? "Actualizar" : "Guardar la actual", "wam-dlg-primario", function () {
var nombre = String(inputNombre.value || "").trim() || nombreCuenta || "Mi demo";
var misma = activa && activa.nombre === nombre;
// Defensa: si lo que se va a escribir sobre una demo existente tiene
// otra cuenta u otro número de mensajes, se pide un segundo clic.
if (misma && bGuardar.getAttribute("data-confirmar") !== "1" && cambioSustancial(activa.id)) {
bGuardar.setAttribute("data-confirmar", "1");
bGuardar.textContent = "¿Sobrescribir «" + activa.nombre + "»?";
toast("El contenido actual no se parece al de «" + activa.nombre + "» (otra cuenta u otro número de mensajes). Pulsa otra vez para sobrescribirla o cambia el nombre para guardarla como nueva.", { ms: 7000 });
return;
}
bGuardar.removeAttribute("data-confirmar");
var e = guardarDemoComo(nombre, misma ? activa.id : null);
if (e) { toast("Demo «" + e.nombre + "» guardada"); pintarLista(); activa = demoActiva(); }
});
inputNombre.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); bGuardar.click(); } });
inputNombre.addEventListener("input", function () {
bGuardar.removeAttribute("data-confirmar");
bGuardar.textContent = (activa && activa.nombre === String(inputNombre.value).trim()) ? "Actualizar" : "Guardar como nueva";
});