WIP: mejoras de fidelidad, editor, lógica y suite e2e (checkpoint tras reinicio)
This commit is contained in:
@@ -0,0 +1,824 @@
|
||||
/* =====================================================================
|
||||
js/ux.js — Capa de experiencia de la app (editor, C). No toca el DOM
|
||||
al cargar: WAM.initUX() se ejecuta una sola vez desde WAM.renderEditor().
|
||||
· Historial deshacer / rehacer (WAM.puntoDeshacer / deshacer / rehacer)
|
||||
· Indicador de guardado en la toolbar (#estado-guardado)
|
||||
· Confirmación de Restablecer / Importar (WAM.confirmarReemplazo)
|
||||
· Diálogo «Demos»: escenarios por industria + demos guardadas (wam-demos)
|
||||
· Atajos de teclado globales y diálogo de ayuda (?)
|
||||
· Pestañas Editor / Vista previa en ventana estrecha y menú «⋯»
|
||||
· Enlace vista previa → tarjeta (Alt+clic o doble clic en una burbuja)
|
||||
· Región sr-only que anuncia los mensajes nuevos del chat
|
||||
===================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
window.WAM = window.WAM || {};
|
||||
|
||||
var LIMITE_HISTORIAL = 30;
|
||||
var CLAVE_DEMOS = "wam-demos";
|
||||
var PREFIJO_DEMO = "wam-demo-";
|
||||
var CLAVE_ACTIVA = "wam-demo-activa";
|
||||
var CLAVE_VISTA = "wam-ui-vista";
|
||||
var MQ_ESTRECHA = "(max-width: 900px)";
|
||||
|
||||
var pilaDeshacer = [];
|
||||
var pilaRehacer = [];
|
||||
var iniciado = false;
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Utilidades
|
||||
------------------------------------------------------------------ */
|
||||
function porId(id) { return document.getElementById(id); }
|
||||
|
||||
function h(etiqueta, attrs, hijos) {
|
||||
var el = document.createElement(etiqueta);
|
||||
Object.keys(attrs || {}).forEach(function (k) {
|
||||
var v = attrs[k];
|
||||
if (v === null || v === undefined || v === false) return;
|
||||
if (k === "class") el.className = v;
|
||||
else if (k === "text") el.textContent = v;
|
||||
else if (k === "value") el.value = v;
|
||||
else el.setAttribute(k, v === true ? "" : v);
|
||||
});
|
||||
(Array.isArray(hijos) ? hijos : (hijos ? [hijos] : [])).forEach(function (x) {
|
||||
if (x) el.appendChild(typeof x === "string" ? document.createTextNode(x) : x);
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
function toast(texto, opts) {
|
||||
if (typeof WAM.toast === "function") return WAM.toast(texto, opts);
|
||||
return function () {};
|
||||
}
|
||||
|
||||
function lsGet(k) { try { return window.localStorage.getItem(k); } catch (e) { return null; } }
|
||||
function lsSet(k, v) { try { window.localStorage.setItem(k, v); return true; } catch (e) { return false; } }
|
||||
function lsDel(k) { try { window.localStorage.removeItem(k); } catch (e) { /* nada */ } }
|
||||
|
||||
function esEstrecha() {
|
||||
return !!(window.matchMedia && window.matchMedia(MQ_ESTRECHA).matches);
|
||||
}
|
||||
|
||||
function enCampoTexto(t) {
|
||||
if (!t || !t.tagName) return false;
|
||||
if (t.isContentEditable) return true;
|
||||
if (t.tagName === "TEXTAREA" || t.tagName === "SELECT") return true;
|
||||
if (t.tagName === "INPUT") {
|
||||
var tipo = (t.type || "text").toLowerCase();
|
||||
return ["checkbox", "radio", "button", "submit", "reset", "file", "range", "color"].indexOf(tipo) === -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function esInteractivo(t) {
|
||||
if (!t || !t.closest) return false;
|
||||
return !!t.closest("button, a[href], input, textarea, select, summary, [role='button'], [role='tab'], [tabindex]:not([tabindex='-1'])");
|
||||
}
|
||||
|
||||
function nMensajes() {
|
||||
return (WAM.estado && Array.isArray(WAM.estado.mensajes)) ? WAM.estado.mensajes.length : 0;
|
||||
}
|
||||
|
||||
// Copia profunda estructural: los strings (dataURL incluidos) se comparten
|
||||
// por referencia, así 30 instantáneas no multiplican los MB en memoria.
|
||||
function clonar(v) {
|
||||
if (Array.isArray(v)) return v.map(clonar);
|
||||
if (v && typeof v === "object") {
|
||||
var o = {};
|
||||
for (var k in v) if (Object.prototype.hasOwnProperty.call(v, k)) o[k] = clonar(v[k]);
|
||||
return o;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function reRenderTotal() {
|
||||
if (typeof WAM.setTema === "function") WAM.setTema(WAM.estado.tema);
|
||||
if (typeof WAM.render === "function") WAM.render();
|
||||
if (typeof WAM.renderEditor === "function") WAM.renderEditor();
|
||||
if (typeof WAM.onDemoChange === "function") { try { WAM.onDemoChange(false); } catch (e) { /* opcional */ } }
|
||||
}
|
||||
|
||||
function guardarYa() {
|
||||
if (typeof WAM.guardarYa === "function") return WAM.guardarYa();
|
||||
if (typeof WAM.guardar === "function") WAM.guardar();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sustituye el estado entero (undo, escenario, demo guardada)
|
||||
function sustituirEstado(nuevo) {
|
||||
if (typeof WAM.demoActiva === "function" && WAM.demoActiva() && typeof WAM.detenerDemo === "function") WAM.detenerDemo();
|
||||
if (typeof WAM.cerrarSheet === "function") WAM.cerrarSheet();
|
||||
WAM.estado = nuevo;
|
||||
guardarYa();
|
||||
reRenderTotal();
|
||||
marcarEditado();
|
||||
}
|
||||
|
||||
function normalizado(crudo) {
|
||||
var previo = WAM.estado;
|
||||
WAM.estado = null; // los IDs empiezan en m1 (el estado se sustituye entero)
|
||||
try {
|
||||
return (typeof WAM.normalizar === "function") ? WAM.normalizar(crudo) : crudo;
|
||||
} finally {
|
||||
WAM.estado = previo;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Historial deshacer / rehacer
|
||||
------------------------------------------------------------------ */
|
||||
WAM.puntoDeshacer = function () {
|
||||
if (!WAM.estado) return;
|
||||
pilaDeshacer.push(clonar(WAM.estado));
|
||||
if (pilaDeshacer.length > LIMITE_HISTORIAL) pilaDeshacer.shift();
|
||||
pilaRehacer = [];
|
||||
pintarBotonesHistorial();
|
||||
};
|
||||
|
||||
WAM.puedeDeshacer = function () { return pilaDeshacer.length > 0; };
|
||||
|
||||
WAM.deshacer = function () {
|
||||
if (!pilaDeshacer.length) { toast("No hay nada que deshacer"); return false; }
|
||||
pilaRehacer.push(clonar(WAM.estado));
|
||||
sustituirEstado(pilaDeshacer.pop());
|
||||
pintarBotonesHistorial();
|
||||
toast("Cambio deshecho", { accion: { texto: "Rehacer", fn: WAM.rehacer } });
|
||||
return true;
|
||||
};
|
||||
|
||||
WAM.rehacer = function () {
|
||||
if (!pilaRehacer.length) { toast("No hay nada que rehacer"); return false; }
|
||||
pilaDeshacer.push(clonar(WAM.estado));
|
||||
sustituirEstado(pilaRehacer.pop());
|
||||
pintarBotonesHistorial();
|
||||
toast("Cambio rehecho");
|
||||
return true;
|
||||
};
|
||||
|
||||
function pintarBotonesHistorial() {
|
||||
var d = porId("btn-deshacer"), r = porId("btn-rehacer");
|
||||
if (d) d.disabled = !pilaDeshacer.length;
|
||||
if (r) r.disabled = !pilaRehacer.length;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Indicador de guardado (#estado-guardado)
|
||||
------------------------------------------------------------------ */
|
||||
var ultimoGuardado = 0;
|
||||
var tComprobar = 0;
|
||||
var errorGuardado = "";
|
||||
|
||||
function pintarGuardado(estadoTxt, clase, titulo) {
|
||||
var el = porId("estado-guardado");
|
||||
if (!el) return;
|
||||
el.setAttribute("data-estado", clase);
|
||||
var txt = el.querySelector(".tb-guardado-txt");
|
||||
if (txt) txt.textContent = estadoTxt;
|
||||
el.title = titulo || estadoTxt;
|
||||
pintarRelativo();
|
||||
}
|
||||
|
||||
function formatoTamano(bytes) {
|
||||
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + " KB";
|
||||
return (bytes / (1024 * 1024)).toFixed(1).replace(".", ",") + " MB";
|
||||
}
|
||||
|
||||
function pintarRelativo() {
|
||||
var el = porId("estado-guardado");
|
||||
var rel = el && el.querySelector(".tb-guardado-rel");
|
||||
if (!rel) return;
|
||||
if (el.getAttribute("data-estado") !== "ok" || !ultimoGuardado) { rel.textContent = ""; return; }
|
||||
var s = Math.max(0, Math.round((Date.now() - ultimoGuardado) / 1000));
|
||||
rel.textContent = s < 5 ? "· ahora" : (s < 60 ? "· hace " + s + " s" : "· hace " + Math.round(s / 60) + " min");
|
||||
}
|
||||
|
||||
function comprobarGuardado() {
|
||||
tComprobar = 0;
|
||||
var ok = guardarYa();
|
||||
if (ok === false) {
|
||||
pintarGuardado("No se pudo guardar", "error", errorGuardado || "No se pudo guardar en este navegador: exporta el JSON.");
|
||||
return;
|
||||
}
|
||||
errorGuardado = "";
|
||||
ultimoGuardado = Date.now();
|
||||
var bruto = lsGet(WAM.CLAVE || "wam-mockup-v1");
|
||||
var bytes = bruto ? bruto.length * 2 : 0; // UTF-16 en memoria del navegador
|
||||
var extra = bytes > 3 * 1024 * 1024 ? " · " + formatoTamano(bytes) : "";
|
||||
pintarGuardado("Guardado" + extra, "ok", "Guardado en este navegador" + (bytes ? " (" + formatoTamano(bytes) + ")" : "") +
|
||||
(bytes > 4 * 1024 * 1024 ? ". Te acercas al límite (~5 MB): usa URLs en vez de subir archivos pesados." : ""));
|
||||
}
|
||||
|
||||
// El editor la llama tras cada mutación: «Guardando…» y comprobación diferida
|
||||
function marcarEditado() {
|
||||
if (errorGuardado) return; // mantener el aviso en rojo hasta que vuelva a funcionar
|
||||
pintarGuardado("Guardando…", "pendiente");
|
||||
clearTimeout(tComprobar);
|
||||
tComprobar = setTimeout(comprobarGuardado, 700);
|
||||
}
|
||||
WAM.marcarEditado = marcarEditado;
|
||||
|
||||
function alErrorGuardar(texto) {
|
||||
errorGuardado = texto || "No se pudo guardar";
|
||||
pintarGuardado("No se pudo guardar", "error", errorGuardado);
|
||||
toast(errorGuardado, {
|
||||
tipo: "error", ms: 12000,
|
||||
accion: { texto: "Exportar JSON", fn: function () { if (typeof WAM.exportarJSON === "function") WAM.exportarJSON(); } }
|
||||
});
|
||||
// Reintento: al siguiente guardado correcto el indicador vuelve a verde
|
||||
setTimeout(function () { errorGuardado = ""; }, 4000);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Diálogos (<dialog> nativo; reserva con window.confirm)
|
||||
------------------------------------------------------------------ */
|
||||
var soportaDialog = typeof window.HTMLDialogElement === "function";
|
||||
|
||||
function crearDialogo(id, titulo, clase) {
|
||||
var viejo = porId(id);
|
||||
if (viejo) viejo.remove();
|
||||
var cerrar = h("button", { type: "button", class: "wam-dlg-cerrar", "aria-label": "Cerrar", text: "×" });
|
||||
var cuerpo = h("div", { class: "wam-dlg-cuerpo" });
|
||||
var pie = h("div", { class: "wam-dlg-pie" });
|
||||
var dlg = h("dialog", { id: id, class: "wam-dlg" + (clase ? " " + clase : ""), "aria-labelledby": id + "-titulo" }, [
|
||||
h("div", { class: "wam-dlg-cab" }, [h("h2", { id: id + "-titulo", class: "wam-dlg-titulo", text: titulo }), cerrar]),
|
||||
cuerpo,
|
||||
pie
|
||||
]);
|
||||
cerrar.addEventListener("click", function () { dlg.close(); });
|
||||
// Clic en el fondo (fuera de la caja) cierra
|
||||
dlg.addEventListener("click", function (e) {
|
||||
if (e.target !== dlg) return;
|
||||
var r = dlg.getBoundingClientRect();
|
||||
var dentro = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
|
||||
if (!dentro) dlg.close();
|
||||
});
|
||||
var foco = null;
|
||||
dlg.addEventListener("close", function () {
|
||||
setTimeout(function () { dlg.remove(); }, 0);
|
||||
if (foco && foco.isConnected && typeof foco.focus === "function") foco.focus();
|
||||
});
|
||||
document.body.appendChild(dlg);
|
||||
return {
|
||||
dlg: dlg, cuerpo: cuerpo, pie: pie,
|
||||
abrir: function () { foco = document.activeElement; dlg.showModal(); }
|
||||
};
|
||||
}
|
||||
|
||||
function boton(texto, clase, fn) {
|
||||
var b = h("button", { type: "button", class: "wam-dlg-btn " + (clase || ""), text: texto });
|
||||
if (fn) b.addEventListener("click", fn);
|
||||
return b;
|
||||
}
|
||||
|
||||
/* Confirmación de acciones que sustituyen la demo entera.
|
||||
tipo: "reset" | "importar". ejecutar(): la acción real. */
|
||||
WAM.confirmarReemplazo = function (tipo, ejecutar) {
|
||||
if (typeof ejecutar !== "function") return;
|
||||
var esReset = tipo === "reset";
|
||||
var hacer = function () {
|
||||
if (esReset) {
|
||||
WAM.puntoDeshacer();
|
||||
lsDel(CLAVE_ACTIVA);
|
||||
ejecutar();
|
||||
marcarEditado();
|
||||
toast("Demo de ejemplo restablecida", { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 });
|
||||
} else {
|
||||
ejecutar(); // el punto de deshacer lo toma importarJSON al aplicar el archivo
|
||||
}
|
||||
};
|
||||
if (!nMensajes()) { hacer(); return; }
|
||||
var nombre = (WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "sin nombre";
|
||||
var resumen = "«" + nombre + "», " + nMensajes() + " mensaje" + (nMensajes() === 1 ? "" : "s");
|
||||
if (!soportaDialog) {
|
||||
if (window.confirm(esReset ? "¿Restablecer la demo de ejemplo? Se sustituirá " + resumen + "."
|
||||
: "¿Importar un JSON? Se sustituirá " + resumen + ".")) hacer();
|
||||
return;
|
||||
}
|
||||
var d = crearDialogo("dlg-confirmar", esReset ? "¿Restablecer la demo de ejemplo?" : "¿Importar una demo desde JSON?", "wam-dlg-chico");
|
||||
d.cuerpo.appendChild(h("p", { text: (esReset
|
||||
? "La demo actual (" + resumen + ") se sustituirá por la demo de ejemplo «Café Aurora»."
|
||||
: "El archivo que elijas sustituirá la demo actual (" + resumen + ").") }));
|
||||
d.cuerpo.appendChild(h("p", { class: "wam-dlg-nota", text: "Puedes exportarla antes. Mientras no recargues la página, también puedes deshacer con Ctrl+Z." }));
|
||||
var bExp = boton("Exportar antes", "", function () {
|
||||
if (typeof WAM.exportarJSON === "function") WAM.exportarJSON();
|
||||
bExp.textContent = "Exportada ✓";
|
||||
bExp.disabled = true;
|
||||
});
|
||||
var bOk = boton(esReset ? "Restablecer" : "Elegir archivo…", esReset ? "wam-dlg-peligro" : "wam-dlg-primario", function () {
|
||||
d.dlg.close();
|
||||
hacer();
|
||||
});
|
||||
d.pie.appendChild(boton("Cancelar", "", function () { d.dlg.close(); }));
|
||||
d.pie.appendChild(bExp);
|
||||
d.pie.appendChild(bOk);
|
||||
d.abrir();
|
||||
bOk.focus();
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Demos: escenarios + demos guardadas en localStorage
|
||||
------------------------------------------------------------------ */
|
||||
function indiceDemos() {
|
||||
var bruto = lsGet(CLAVE_DEMOS);
|
||||
try {
|
||||
var arr = JSON.parse(bruto || "[]");
|
||||
return Array.isArray(arr) ? arr.filter(function (x) { return x && typeof x.id === "string"; }) : [];
|
||||
} catch (e) { return []; }
|
||||
}
|
||||
|
||||
function guardarIndice(arr) { return lsSet(CLAVE_DEMOS, JSON.stringify(arr)); }
|
||||
|
||||
function nuevoIdDemo() { return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
|
||||
|
||||
function demoActiva() {
|
||||
var id = lsGet(CLAVE_ACTIVA);
|
||||
if (!id) return null;
|
||||
var idx = indiceDemos();
|
||||
for (var i = 0; i < idx.length; i++) if (idx[i].id === id) return idx[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
WAM.nombreDemoActiva = function () {
|
||||
var d = demoActiva();
|
||||
return d ? d.nombre : "";
|
||||
};
|
||||
|
||||
function guardarDemoComo(nombre, id) {
|
||||
guardarYa();
|
||||
var idx = indiceDemos();
|
||||
var ahora = new Date().toISOString();
|
||||
var nuevoId = id || nuevoIdDemo();
|
||||
if (!lsSet(PREFIJO_DEMO + nuevoId, JSON.stringify(WAM.estado))) {
|
||||
toast("No se pudo guardar la demo: almacenamiento lleno. Exporta a JSON o borra demos guardadas.", { tipo: "error", ms: 9000 });
|
||||
return null;
|
||||
}
|
||||
var entrada = { id: nuevoId, nombre: nombre || "Demo sin nombre", fecha: ahora, n: nMensajes() };
|
||||
var pos = -1;
|
||||
for (var i = 0; i < idx.length; i++) if (idx[i].id === nuevoId) pos = i;
|
||||
if (pos >= 0) idx[pos] = entrada; else idx.unshift(entrada);
|
||||
guardarIndice(idx);
|
||||
lsSet(CLAVE_ACTIVA, nuevoId);
|
||||
return entrada;
|
||||
}
|
||||
|
||||
function cargarDemo(id) {
|
||||
var bruto = lsGet(PREFIJO_DEMO + id);
|
||||
var datos = null;
|
||||
try { datos = JSON.parse(bruto || "null"); } catch (e) { datos = null; }
|
||||
if (!datos) { toast("No se encontró esa demo guardada", { tipo: "error" }); return false; }
|
||||
WAM.puntoDeshacer();
|
||||
sustituirEstado(normalizado(datos));
|
||||
lsSet(CLAVE_ACTIVA, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
function cargarEscenario(esc) {
|
||||
var crudo = WAM.datosEscenario ? WAM.datosEscenario(esc.id) : null;
|
||||
if (!crudo) return;
|
||||
var habiaAlgo = nMensajes() > 0;
|
||||
if (habiaAlgo) WAM.puntoDeshacer();
|
||||
sustituirEstado(normalizado(crudo));
|
||||
lsDel(CLAVE_ACTIVA);
|
||||
toast("Escenario «" + esc.nombre + "» cargado", habiaAlgo ? { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 } : null);
|
||||
}
|
||||
|
||||
function fechaCorta(iso) {
|
||||
var d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
try {
|
||||
return d.toLocaleString("es", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
|
||||
} catch (e) { return d.toISOString().slice(0, 16).replace("T", " "); }
|
||||
}
|
||||
|
||||
WAM.abrirDemos = function (seccion) {
|
||||
if (!soportaDialog) { toast("Tu navegador no admite diálogos nativos", { tipo: "error" }); return; }
|
||||
var d = crearDialogo("dlg-demos", "Demos", "wam-dlg-grande");
|
||||
|
||||
// --- Escenarios ---
|
||||
var gridEsc = h("div", { class: "wam-esc-grid" });
|
||||
(WAM.ESCENARIOS || []).forEach(function (esc) {
|
||||
var n = 0;
|
||||
try { n = (esc.datos().mensajes || []).length; } catch (e) { n = 0; }
|
||||
var b = h("button", { type: "button", class: "wam-esc", "data-esc": esc.id }, [
|
||||
h("span", { class: "wam-esc-nombre", text: esc.nombre }),
|
||||
h("span", { class: "wam-esc-desc", text: esc.descripcion }),
|
||||
h("span", { class: "wam-esc-meta", text: n ? n + " mensajes" : "Vacía" })
|
||||
]);
|
||||
b.addEventListener("click", function () { d.dlg.close(); cargarEscenario(esc); });
|
||||
gridEsc.appendChild(b);
|
||||
});
|
||||
var bAurora = h("button", { type: "button", class: "wam-esc" }, [
|
||||
h("span", { class: "wam-esc-nombre", text: "Catálogo · Café Aurora" }),
|
||||
h("span", { class: "wam-esc-desc", text: "Los 13 tipos de mensaje de la Cloud API. Útil como referencia." }),
|
||||
h("span", { class: "wam-esc-meta", text: "Demo de ejemplo" })
|
||||
]);
|
||||
bAurora.addEventListener("click", function () {
|
||||
d.dlg.close();
|
||||
if (typeof WAM.datosDemo !== "function") return;
|
||||
var habiaAlgo = nMensajes() > 0;
|
||||
if (habiaAlgo) WAM.puntoDeshacer();
|
||||
sustituirEstado(normalizado(WAM.datosDemo()));
|
||||
lsDel(CLAVE_ACTIVA);
|
||||
toast("Demo «Café Aurora» cargada", habiaAlgo ? { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 } : null);
|
||||
});
|
||||
gridEsc.appendChild(bAurora);
|
||||
|
||||
var secEsc = h("section", { class: "wam-dlg-sec", "aria-labelledby": "dlg-sec-esc" }, [
|
||||
h("h3", { id: "dlg-sec-esc", class: "wam-dlg-subtitulo", text: "Empezar desde un escenario" }),
|
||||
h("p", { class: "wam-dlg-nota", text: "Sustituye la demo actual (puedes deshacer). Los textos son de ejemplo y ficticios: revísalos y adáptalos antes de enseñarlos a un cliente." }),
|
||||
gridEsc
|
||||
]);
|
||||
|
||||
// --- Demos guardadas ---
|
||||
var lista = h("ul", { class: "wam-demos-lista" });
|
||||
var activa = demoActiva();
|
||||
var inputNombre = h("input", {
|
||||
type: "text", class: "wam-dlg-input", id: "dlg-demo-nombre", maxlength: "60",
|
||||
value: activa ? activa.nombre : ((WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "Mi demo"),
|
||||
"aria-label": "Nombre de la demo"
|
||||
});
|
||||
var bGuardar = boton(activa ? "Actualizar" : "Guardar la actual", "wam-dlg-primario", function () {
|
||||
var nombre = String(inputNombre.value || "").trim() || "Mi demo";
|
||||
var misma = activa && activa.nombre === nombre;
|
||||
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.textContent = (activa && activa.nombre === String(inputNombre.value).trim()) ? "Actualizar" : "Guardar como nueva";
|
||||
});
|
||||
|
||||
function pintarLista() {
|
||||
while (lista.firstChild) lista.removeChild(lista.firstChild);
|
||||
var idx = indiceDemos();
|
||||
var actId = lsGet(CLAVE_ACTIVA);
|
||||
if (!idx.length) {
|
||||
lista.appendChild(h("li", { class: "wam-demos-vacio", text: "Aún no has guardado ninguna demo. Guarda la actual para tener varias (una por cliente) sin exportar e importar." }));
|
||||
return;
|
||||
}
|
||||
idx.forEach(function (it) {
|
||||
var nombreEl = h("span", { class: "wam-demo-nombre", text: it.nombre });
|
||||
var li = h("li", { class: "wam-demo" + (it.id === actId ? " wam-demo-activa" : "") }, [
|
||||
h("div", { class: "wam-demo-info" }, [
|
||||
nombreEl,
|
||||
h("span", { class: "wam-demo-meta", text: (it.n || 0) + " mensajes · " + fechaCorta(it.fecha) + (it.id === actId ? " · en uso" : "") })
|
||||
])
|
||||
]);
|
||||
var acc = h("div", { class: "wam-demo-acc" });
|
||||
acc.appendChild(boton("Abrir", "wam-dlg-mini", function () {
|
||||
d.dlg.close();
|
||||
if (cargarDemo(it.id)) toast("Demo «" + it.nombre + "» abierta", { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 });
|
||||
}));
|
||||
acc.appendChild(boton("Renombrar", "wam-dlg-mini", function () {
|
||||
var inp = h("input", { type: "text", class: "wam-dlg-input", value: it.nombre, maxlength: "60", "aria-label": "Nuevo nombre" });
|
||||
nombreEl.replaceWith(inp);
|
||||
inp.focus(); inp.select();
|
||||
var hecho = false;
|
||||
var fin = function (guardar) {
|
||||
if (hecho) return; hecho = true;
|
||||
var v = String(inp.value || "").trim();
|
||||
if (guardar && v) {
|
||||
var arr = indiceDemos();
|
||||
arr.forEach(function (x) { if (x.id === it.id) x.nombre = v; });
|
||||
guardarIndice(arr);
|
||||
}
|
||||
pintarLista();
|
||||
};
|
||||
inp.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") { e.preventDefault(); fin(true); }
|
||||
if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); fin(false); }
|
||||
});
|
||||
inp.addEventListener("blur", function () { fin(true); });
|
||||
}));
|
||||
acc.appendChild(boton("Duplicar", "wam-dlg-mini", function () {
|
||||
var bruto = lsGet(PREFIJO_DEMO + it.id);
|
||||
var nid = nuevoIdDemo();
|
||||
if (!bruto || !lsSet(PREFIJO_DEMO + nid, bruto)) { toast("No se pudo duplicar (almacenamiento lleno)", { tipo: "error" }); return; }
|
||||
var arr = indiceDemos();
|
||||
arr.unshift({ id: nid, nombre: it.nombre + " (copia)", fecha: new Date().toISOString(), n: it.n || 0 });
|
||||
guardarIndice(arr);
|
||||
pintarLista();
|
||||
}));
|
||||
var bBorrar = boton("Borrar", "wam-dlg-mini wam-dlg-peligro", function () {
|
||||
if (bBorrar.getAttribute("data-confirmar") !== "1") {
|
||||
bBorrar.setAttribute("data-confirmar", "1");
|
||||
bBorrar.textContent = "¿Seguro?";
|
||||
setTimeout(function () {
|
||||
if (bBorrar.isConnected) { bBorrar.removeAttribute("data-confirmar"); bBorrar.textContent = "Borrar"; }
|
||||
}, 3500);
|
||||
return;
|
||||
}
|
||||
lsDel(PREFIJO_DEMO + it.id);
|
||||
guardarIndice(indiceDemos().filter(function (x) { return x.id !== it.id; }));
|
||||
if (lsGet(CLAVE_ACTIVA) === it.id) lsDel(CLAVE_ACTIVA);
|
||||
activa = demoActiva();
|
||||
pintarLista();
|
||||
});
|
||||
acc.appendChild(bBorrar);
|
||||
li.appendChild(acc);
|
||||
lista.appendChild(li);
|
||||
});
|
||||
}
|
||||
pintarLista();
|
||||
|
||||
var secGuardadas = h("section", { class: "wam-dlg-sec", "aria-labelledby": "dlg-sec-guardadas" }, [
|
||||
h("h3", { id: "dlg-sec-guardadas", class: "wam-dlg-subtitulo", text: "Mis demos guardadas" }),
|
||||
h("div", { class: "wam-dlg-fila" }, [inputNombre, bGuardar]),
|
||||
lista,
|
||||
h("p", { class: "wam-dlg-nota", text: "Se guardan solo en este navegador. Para compartir una demo o hacer copia de seguridad, usa Exportar." })
|
||||
]);
|
||||
|
||||
d.cuerpo.appendChild(seccion === "guardadas" ? secGuardadas : secEsc);
|
||||
d.cuerpo.appendChild(seccion === "guardadas" ? secEsc : secGuardadas);
|
||||
d.pie.appendChild(boton("Cerrar", "", function () { d.dlg.close(); }));
|
||||
d.abrir();
|
||||
var primero = d.cuerpo.querySelector(".wam-esc, .wam-dlg-input");
|
||||
if (primero) primero.focus();
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Atajos de teclado
|
||||
------------------------------------------------------------------ */
|
||||
var ATAJOS = [
|
||||
["P", "Modo presentación"],
|
||||
["Espacio", "Reproducir / detener la demo"],
|
||||
["T", "Tema claro / oscuro"],
|
||||
["F", "Pantalla completa"],
|
||||
["N", "Demos y escenarios"],
|
||||
["Esc", "Cerrar la lista del chat o salir de la presentación"],
|
||||
["Ctrl + S", "Exportar JSON"],
|
||||
["Ctrl + Z", "Deshacer (fuera de un campo de texto)"],
|
||||
["Ctrl + Mayús + Z", "Rehacer"],
|
||||
["Alt + ↑ / ↓", "Mover el mensaje o elemento con el foco"],
|
||||
["Alt + clic en una burbuja", "Abrir su tarjeta en el editor (también doble clic)"],
|
||||
["?", "Esta ayuda"]
|
||||
];
|
||||
|
||||
WAM.abrirAtajos = function () {
|
||||
if (!soportaDialog) return;
|
||||
var d = crearDialogo("dlg-atajos", "Atajos de teclado", "wam-dlg-chico");
|
||||
var tabla = h("dl", { class: "wam-atajos" });
|
||||
ATAJOS.forEach(function (a) {
|
||||
tabla.appendChild(h("dt", {}, a[0].split(" + ").map(function (k, i, arr) {
|
||||
var frag = document.createDocumentFragment();
|
||||
frag.appendChild(h("kbd", { text: k }));
|
||||
if (i < arr.length - 1) frag.appendChild(document.createTextNode(" + "));
|
||||
return frag;
|
||||
})));
|
||||
tabla.appendChild(h("dd", { text: a[1] }));
|
||||
});
|
||||
d.cuerpo.appendChild(tabla);
|
||||
d.cuerpo.appendChild(h("p", { class: "wam-dlg-nota", text: "Los atajos de una letra no actúan mientras escribes en un campo." }));
|
||||
d.pie.appendChild(boton("Cerrar", "wam-dlg-primario", function () { d.dlg.close(); }));
|
||||
d.abrir();
|
||||
};
|
||||
|
||||
function clicEn(id) { var b = porId(id); if (b && !b.disabled) b.click(); }
|
||||
|
||||
function alternarPantallaCompleta() {
|
||||
var doc = document;
|
||||
try {
|
||||
if (doc.fullscreenElement) { var p = doc.exitFullscreen(); if (p && p.catch) p.catch(function () {}); }
|
||||
else if (doc.documentElement.requestFullscreen) {
|
||||
var q = doc.documentElement.requestFullscreen();
|
||||
if (q && q.catch) q.catch(function () { toast("El navegador no permitió la pantalla completa", { tipo: "error" }); });
|
||||
}
|
||||
} catch (e) { /* sin soporte */ }
|
||||
}
|
||||
|
||||
function sheetAbierto() {
|
||||
var s = porId("sheet");
|
||||
return !!(s && !s.hidden && s.offsetParent !== null);
|
||||
}
|
||||
|
||||
function alTecla(e) {
|
||||
if (e.defaultPrevented || e.isComposing) return;
|
||||
var k = e.key;
|
||||
var mod = e.ctrlKey || e.metaKey;
|
||||
var hayDialogo = !!document.querySelector("dialog[open]");
|
||||
|
||||
if (mod && !e.altKey && (k === "s" || k === "S")) {
|
||||
e.preventDefault();
|
||||
if (!hayDialogo && typeof WAM.exportarJSON === "function") WAM.exportarJSON();
|
||||
return;
|
||||
}
|
||||
if (hayDialogo) return; // el <dialog> gestiona Esc y el foco
|
||||
if (mod && !e.altKey && (k === "z" || k === "Z") && !enCampoTexto(e.target)) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) WAM.rehacer(); else WAM.deshacer();
|
||||
return;
|
||||
}
|
||||
if (mod && !e.altKey && (k === "y" || k === "Y") && !enCampoTexto(e.target)) {
|
||||
e.preventDefault();
|
||||
WAM.rehacer();
|
||||
return;
|
||||
}
|
||||
if (k === "Escape") {
|
||||
if (cerrarMenu()) return;
|
||||
if (sheetAbierto() && typeof WAM.cerrarSheet === "function") { WAM.cerrarSheet(); return; }
|
||||
if (document.body.classList.contains("presentacion") && !enCampoTexto(e.target)) clicEn("btn-presentacion");
|
||||
return;
|
||||
}
|
||||
if (mod || e.altKey || enCampoTexto(e.target)) return;
|
||||
switch (k) {
|
||||
case "p": case "P": e.preventDefault(); clicEn("btn-presentacion"); break;
|
||||
case "t": case "T": e.preventDefault(); clicEn("btn-tema"); break;
|
||||
case "f": case "F": e.preventDefault(); alternarPantallaCompleta(); break;
|
||||
case "n": case "N": e.preventDefault(); WAM.abrirDemos(); break;
|
||||
case "?": e.preventDefault(); WAM.abrirAtajos(); break;
|
||||
case " ":
|
||||
if (esInteractivo(e.target)) return; // Espacio activa el botón con foco
|
||||
e.preventDefault();
|
||||
clicEn("btn-reproducir");
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Menú «⋯» de la toolbar (ventana estrecha) y pestañas Editor / Vista
|
||||
------------------------------------------------------------------ */
|
||||
function cerrarMenu() {
|
||||
var tb = porId("toolbar");
|
||||
var mas = porId("btn-mas");
|
||||
if (!tb || !tb.classList.contains("tb-menu-abierto")) return false;
|
||||
tb.classList.remove("tb-menu-abierto");
|
||||
if (mas) mas.setAttribute("aria-expanded", "false");
|
||||
return true;
|
||||
}
|
||||
|
||||
function enlazarMenu() {
|
||||
var tb = porId("toolbar"), mas = porId("btn-mas"), menu = porId("tb-menu");
|
||||
if (!tb || !mas || !menu) return;
|
||||
mas.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
var abierto = tb.classList.toggle("tb-menu-abierto");
|
||||
mas.setAttribute("aria-expanded", abierto ? "true" : "false");
|
||||
if (abierto) { var b = menu.querySelector("button:not([disabled])"); if (b) b.focus(); }
|
||||
});
|
||||
menu.addEventListener("click", function (e) {
|
||||
if (e.target.closest && e.target.closest("button")) cerrarMenu();
|
||||
});
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!tb.classList.contains("tb-menu-abierto")) return;
|
||||
if (menu.contains(e.target) || mas.contains(e.target)) return;
|
||||
cerrarMenu();
|
||||
});
|
||||
}
|
||||
|
||||
var ultimoIdEditado = null;
|
||||
WAM.recordarEnfoque = function (id) { ultimoIdEditado = id || null; };
|
||||
|
||||
WAM.setVistaMovil = function (vista) {
|
||||
var previa = vista === "vista";
|
||||
document.body.classList.toggle("vista-movil", previa);
|
||||
var te = porId("tab-editor"), tv = porId("tab-vista");
|
||||
if (te) { te.setAttribute("aria-selected", previa ? "false" : "true"); te.tabIndex = previa ? -1 : 0; }
|
||||
if (tv) { tv.setAttribute("aria-selected", previa ? "true" : "false"); tv.tabIndex = previa ? 0 : -1; }
|
||||
lsSet(CLAVE_VISTA, previa ? "vista" : "editor");
|
||||
if (previa && ultimoIdEditado && typeof WAM.enfocarEnChat === "function") {
|
||||
var id = ultimoIdEditado;
|
||||
requestAnimationFrame(function () { requestAnimationFrame(function () { WAM.enfocarEnChat(id); }); });
|
||||
}
|
||||
};
|
||||
|
||||
function enlazarPestanas() {
|
||||
var te = porId("tab-editor"), tv = porId("tab-vista");
|
||||
if (te) te.addEventListener("click", function () { WAM.setVistaMovil("editor"); });
|
||||
if (tv) tv.addEventListener("click", function () { WAM.setVistaMovil("vista"); });
|
||||
[te, tv].forEach(function (t) {
|
||||
if (!t) return;
|
||||
t.addEventListener("keydown", function (e) {
|
||||
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
||||
e.preventDefault();
|
||||
var otra = t === te ? tv : te;
|
||||
if (otra) { otra.click(); otra.focus(); }
|
||||
});
|
||||
});
|
||||
var ver = porId("btn-ver-telefono");
|
||||
if (ver) ver.addEventListener("click", function () { WAM.setVistaMovil("vista"); });
|
||||
var guardada = lsGet(CLAVE_VISTA);
|
||||
WAM.setVistaMovil(guardada === "editor" ? "editor" : "vista");
|
||||
}
|
||||
|
||||
// Altura real de la toolbar → variable CSS para el cálculo de escala
|
||||
function observarToolbar() {
|
||||
var tb = porId("toolbar");
|
||||
if (!tb) return;
|
||||
var fijar = function () {
|
||||
document.documentElement.style.setProperty("--wam-toolbar-alto", Math.ceil(tb.getBoundingClientRect().height) + "px");
|
||||
};
|
||||
fijar();
|
||||
if (typeof ResizeObserver === "function") new ResizeObserver(fijar).observe(tb);
|
||||
else window.addEventListener("resize", fijar);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Vista previa → tarjeta del editor
|
||||
------------------------------------------------------------------ */
|
||||
function editorOculto() {
|
||||
return document.body.classList.contains("presentacion");
|
||||
}
|
||||
|
||||
function abrirDesdeChat(id) {
|
||||
if (!id || editorOculto() || typeof WAM.abrirTarjeta !== "function") return false;
|
||||
if (esEstrecha()) WAM.setVistaMovil("editor");
|
||||
return WAM.abrirTarjeta(id);
|
||||
}
|
||||
|
||||
function enlazarChat() {
|
||||
var pantalla = porId("pantalla"), cuerpo = porId("chat-cuerpo");
|
||||
if (pantalla) {
|
||||
// Captura: Alt+clic no dispara el flujo del botón (lo gestiona main.js)
|
||||
pantalla.addEventListener("click", function (e) {
|
||||
if (!e.altKey) return;
|
||||
var fila = e.target.closest && e.target.closest("#chat-cuerpo .wa-row[data-id]");
|
||||
if (!fila || editorOculto()) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
abrirDesdeChat(fila.getAttribute("data-id"));
|
||||
}, true);
|
||||
}
|
||||
if (cuerpo) {
|
||||
cuerpo.addEventListener("dblclick", function (e) {
|
||||
if (!e.target.closest || e.target.closest("[data-flujo], a, button, video, audio, input")) return;
|
||||
var fila = e.target.closest(".wa-row[data-id]");
|
||||
if (!fila || editorOculto()) return;
|
||||
if (window.getSelection) { try { window.getSelection().removeAllRanges(); } catch (err) { /* nada */ } }
|
||||
abrirDesdeChat(fila.getAttribute("data-id"));
|
||||
});
|
||||
// Las altas y re-render parciales no deben releer todo el chat
|
||||
cuerpo.removeAttribute("aria-live");
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Anuncios para lectores de pantalla (mensajes nuevos del chat)
|
||||
------------------------------------------------------------------ */
|
||||
function anunciar(msg) {
|
||||
var reg = porId("wam-anuncios");
|
||||
if (!reg || !msg) return;
|
||||
var nombre = (WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "el negocio";
|
||||
var txt = msg.texto || msg.body || msg.caption || msg.filename || msg.nombre || "";
|
||||
txt = String(txt).replace(/\s+/g, " ").slice(0, 140);
|
||||
reg.textContent = (msg.dir === "out" ? "Tú: " : "Nuevo mensaje de " + nombre + ": ") + (txt || "(" + (msg.tipo || "mensaje") + ")");
|
||||
}
|
||||
|
||||
function envolverAnuncios() {
|
||||
if (!porId("wam-anuncios")) {
|
||||
document.body.appendChild(h("div", { id: "wam-anuncios", class: "wam-sr", "aria-live": "polite", "aria-atomic": "true" }));
|
||||
}
|
||||
var orig = WAM.anadirMensajeDOM;
|
||||
if (typeof orig !== "function" || orig.__wamAnuncio) return;
|
||||
var envuelta = function (msg) {
|
||||
var r = orig.apply(this, arguments);
|
||||
try { anunciar(msg); } catch (e) { /* nunca rompe el flujo */ }
|
||||
return r;
|
||||
};
|
||||
envuelta.__wamAnuncio = true;
|
||||
WAM.anadirMensajeDOM = envuelta;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Estado de los botones de la toolbar
|
||||
------------------------------------------------------------------ */
|
||||
function sincronizarTema() {
|
||||
var b = porId("btn-tema");
|
||||
if (b) b.setAttribute("aria-pressed", document.body.getAttribute("data-tema") === "oscuro" ? "true" : "false");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Inicialización (una sola vez)
|
||||
------------------------------------------------------------------ */
|
||||
WAM.initUX = function () {
|
||||
if (iniciado) return;
|
||||
iniciado = true;
|
||||
|
||||
// Sustituye al aviso genérico: indicador rojo en la toolbar + aviso con «Exportar JSON»
|
||||
WAM.onErrorGuardar = function (texto) { alErrorGuardar(texto); };
|
||||
|
||||
document.addEventListener("keydown", alTecla);
|
||||
var bd = porId("btn-deshacer"), br = porId("btn-rehacer"), bdemos = porId("btn-demos"), bat = porId("btn-atajos");
|
||||
if (bd) bd.addEventListener("click", function () { WAM.deshacer(); });
|
||||
if (br) br.addEventListener("click", function () { WAM.rehacer(); });
|
||||
if (bdemos) bdemos.addEventListener("click", function () { WAM.abrirDemos(); });
|
||||
if (bat) bat.addEventListener("click", function () { WAM.abrirAtajos(); });
|
||||
pintarBotonesHistorial();
|
||||
|
||||
enlazarMenu();
|
||||
enlazarPestanas();
|
||||
observarToolbar();
|
||||
enlazarChat();
|
||||
envolverAnuncios();
|
||||
|
||||
sincronizarTema();
|
||||
if (typeof MutationObserver === "function") {
|
||||
new MutationObserver(sincronizarTema).observe(document.body, { attributes: true, attributeFilter: ["data-tema"] });
|
||||
}
|
||||
|
||||
// Estado inicial del indicador y refresco del «hace N s»
|
||||
ultimoGuardado = Date.now();
|
||||
pintarGuardado("Guardado", "ok", "Guardado en este navegador");
|
||||
setInterval(pintarRelativo, 5000);
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user