Files
whatsapp-api-mockup/js/ux.js
T

825 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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);
};
})();