1077 lines
46 KiB
JavaScript
1077 lines
46 KiB
JavaScript
/* =====================================================================
|
||
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 (?); G / F6 y el enlace
|
||
«Saltar a la vista previa» mueven el foco entre editor y teléfono
|
||
· 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
|
||
------------------------------------------------------------------ */
|
||
/* 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;
|
||
cerrarAvisosConAccion();
|
||
pilaDeshacer.push(instantanea());
|
||
if (pilaDeshacer.length > LIMITE_HISTORIAL) pilaDeshacer.shift();
|
||
pilaRehacer = [];
|
||
pintarBotonesHistorial();
|
||
};
|
||
|
||
WAM.puedeDeshacer = function () { return pilaDeshacer.length > 0; };
|
||
|
||
// El tema es una preferencia de visualización, no contenido: deshacer y
|
||
// rehacer conservan el tema actual en vez del de la instantánea.
|
||
function conTemaActual(x) {
|
||
if (x && WAM.estado) x.tema = WAM.estado.tema;
|
||
return x;
|
||
}
|
||
|
||
WAM.deshacer = function () {
|
||
if (!pilaDeshacer.length) { toast("No hay nada que deshacer"); return false; }
|
||
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;
|
||
};
|
||
|
||
WAM.rehacer = function () {
|
||
if (!pilaRehacer.length) { toast("No hay nada que rehacer"); return false; }
|
||
var siguiente = pilaRehacer.pop();
|
||
pilaDeshacer.push(instantanea());
|
||
sustituirEstado(conTemaActual(siguiente.estado));
|
||
reponerActiva(siguiente.activa);
|
||
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;
|
||
var restaurar = true;
|
||
dlg.addEventListener("close", function () {
|
||
setTimeout(function () { dlg.remove(); }, 0);
|
||
if (restaurar && 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(); },
|
||
// La acción elegida mueve el foco a otro sitio (p. ej. el nombre de la cuenta)
|
||
sinRestaurar: function () { restaurar = false; }
|
||
};
|
||
}
|
||
|
||
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 «Café Aurora» 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 ? "¿Volver al ejemplo Café Aurora? Se sustituirá " + resumen + "."
|
||
: "¿Importar un JSON? Se sustituirá " + resumen + ".")) hacer();
|
||
return;
|
||
}
|
||
var d = crearDialogo("dlg-confirmar", esReset ? "¿Volver al ejemplo Café Aurora?" : "¿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;
|
||
}
|
||
|
||
/* ¿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;
|
||
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;
|
||
}
|
||
|
||
/* Tras cargar un escenario lo primero es poner el nombre del cliente:
|
||
se despliega «Cuenta» y se enfoca (y selecciona) su nombre. */
|
||
function enfocarNombreCuenta() {
|
||
if (esEstrecha() && document.body.classList.contains("vista-movil")) return;
|
||
if (document.body.classList.contains("presentacion")) return;
|
||
var det = porId("bloque-cuenta");
|
||
if (det && !det.open) det.open = true;
|
||
var inp = porId("cuenta-nombre");
|
||
if (!inp) return;
|
||
try { inp.focus({ preventScroll: true }); } catch (e) { inp.focus(); }
|
||
if (inp.scrollIntoView) inp.scrollIntoView({ block: "nearest" });
|
||
if (typeof inp.select === "function") inp.select();
|
||
}
|
||
|
||
/* Siempre deja un punto de deshacer (también desde una demo vacía) y un
|
||
único aviso con «Deshacer»: WAM.toast retira los avisos con acción
|
||
anteriores, que ya no deshacen lo que dicen. */
|
||
function aplicarDemoCompleta(crudo, texto) {
|
||
WAM.puntoDeshacer();
|
||
sustituirEstado(normalizado(crudo));
|
||
lsDel(CLAVE_ACTIVA);
|
||
toast(texto, { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 });
|
||
}
|
||
|
||
function cargarEscenario(esc) {
|
||
var crudo = WAM.datosEscenario ? WAM.datosEscenario(esc.id) : null;
|
||
if (!crudo) return;
|
||
aplicarDemoCompleta(crudo, "Escenario «" + esc.nombre + "» cargado. Cambia el nombre del negocio en «Cuenta».");
|
||
enfocarNombreCuenta();
|
||
}
|
||
|
||
// Tipos de mensaje que usa un escenario (chips del diálogo Demos)
|
||
var NOMBRE_TIPO = {
|
||
text: "Texto", image: "Imagen", video: "Vídeo", audio: "Audio", document: "Documento",
|
||
sticker: "Sticker", location: "Ubicación", contact: "Contacto", template: "Plantilla",
|
||
buttons: "Botones", list: "Lista", cta_url: "Botón URL", carousel: "Carrusel"
|
||
};
|
||
// Los más distintivos primero (texto casi siempre está)
|
||
var ORDEN_TIPOS = ["template", "list", "carousel", "buttons", "cta_url", "location", "document",
|
||
"contact", "image", "video", "audio", "sticker", "text"];
|
||
|
||
function chipsTipos(mensajes) {
|
||
var usados = Object.create(null);
|
||
(mensajes || []).forEach(function (m) { if (m && m.tipo) usados[m.tipo] = true; });
|
||
var lista = ORDEN_TIPOS.filter(function (t) { return usados[t]; });
|
||
if (!lista.length) return null;
|
||
var max = 4;
|
||
var cont = h("span", { class: "wam-esc-tipos", "aria-label": "Incluye: " + lista.map(function (t) { return NOMBRE_TIPO[t]; }).join(", ") });
|
||
lista.slice(0, max).forEach(function (t) {
|
||
cont.appendChild(h("span", { class: "wam-esc-tipo", "aria-hidden": "true", text: NOMBRE_TIPO[t] || t }));
|
||
});
|
||
if (lista.length > max) cont.appendChild(h("span", { class: "wam-esc-tipo", "aria-hidden": "true", text: "+" + (lista.length - max) }));
|
||
return cont;
|
||
}
|
||
|
||
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 ms = [];
|
||
try { ms = esc.datos().mensajes || []; } catch (e) { ms = []; }
|
||
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 }),
|
||
chipsTipos(ms),
|
||
h("span", { class: "wam-esc-meta", text: ms.length ? ms.length + " mensajes" : "Vacía" })
|
||
]);
|
||
b.addEventListener("click", function () { d.sinRestaurar(); d.dlg.close(); cargarEscenario(esc); });
|
||
gridEsc.appendChild(b);
|
||
});
|
||
var msAurora = [];
|
||
try { msAurora = (typeof WAM.datosDemo === "function" && WAM.datosDemo().mensajes) || []; } catch (e) { msAurora = []; }
|
||
var bAurora = h("button", { type: "button", class: "wam-esc", "data-esc": "aurora" }, [
|
||
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" + (msAurora.length ? " · " + msAurora.length + " mensajes" : "") })
|
||
]);
|
||
bAurora.addEventListener("click", function () {
|
||
d.dlg.close();
|
||
if (typeof WAM.datosDemo !== "function") return;
|
||
aplicarDemoCompleta(WAM.datosDemo(), "Demo «Café Aurora» cargada");
|
||
});
|
||
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();
|
||
// Sin nombre precargado: el consultor escribe el del cliente (si lo deja
|
||
// vacío se usa el nombre de la cuenta).
|
||
var nombreCuenta = (WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "";
|
||
var inputNombre = h("input", {
|
||
type: "text", class: "wam-dlg-input", id: "dlg-demo-nombre", maxlength: "60",
|
||
value: activa ? activa.nombre : "",
|
||
placeholder: "Nombre de la demo (p. ej. el cliente)" + (nombreCuenta ? " · si lo dejas vacío: «" + nombreCuenta + "»" : ""),
|
||
"aria-label": "Nombre de la demo"
|
||
});
|
||
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";
|
||
});
|
||
|
||
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
|
||
------------------------------------------------------------------ */
|
||
// «Ctrl» se pinta como ⌘ en Mac (los atajos aceptan ctrlKey o metaKey)
|
||
var ATAJOS = [
|
||
["N", "Nueva demo: escenarios y demos guardadas"],
|
||
["P", "Modo presentación"],
|
||
["Espacio", "Reproducir / detener la demo"],
|
||
["G", "Saltar entre el editor y el teléfono (también F6)"],
|
||
["T", "Tema claro / oscuro"],
|
||
["F", "Pantalla completa"],
|
||
["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", "Sobre una burbuja: abre su tarjeta en el editor (también doble clic)"],
|
||
["?", "Esta ayuda"]
|
||
];
|
||
|
||
function esMac() {
|
||
var nav = window.navigator || {};
|
||
var plat = (nav.userAgentData && nav.userAgentData.platform) || nav.platform || nav.userAgent || "";
|
||
return /mac|iphone|ipad|ipod/i.test(String(plat));
|
||
}
|
||
|
||
function teclaVisible(k) {
|
||
if (!esMac()) return k;
|
||
if (k === "Ctrl") return "⌘";
|
||
if (k === "Alt") return "⌥";
|
||
if (k === "Mayús") return "⇧";
|
||
return k;
|
||
}
|
||
|
||
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(teclaVisible).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);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Saltar entre paneles: enlace «Saltar a la vista previa» y G / F6
|
||
------------------------------------------------------------------ */
|
||
var ultimoFocoEditor = null;
|
||
|
||
function enfocar(el) {
|
||
if (!el) return false;
|
||
try { el.focus({ preventScroll: true }); } catch (e) { el.focus(); }
|
||
return document.activeElement === el;
|
||
}
|
||
|
||
function irAVistaPrevia() {
|
||
if (esEstrecha() && !document.body.classList.contains("presentacion")) WAM.setVistaMovil("vista");
|
||
var cuerpo = porId("chat-cuerpo");
|
||
if (!cuerpo) return;
|
||
// tras el cambio de pestaña (display) el nodo ya es enfocable
|
||
requestAnimationFrame(function () { enfocar(cuerpo); });
|
||
}
|
||
|
||
function irAlEditor() {
|
||
if (document.body.classList.contains("presentacion")) return false;
|
||
if (esEstrecha()) WAM.setVistaMovil("editor");
|
||
requestAnimationFrame(function () {
|
||
var lista = porId("lista-mensajes");
|
||
var previo = ultimoFocoEditor && ultimoFocoEditor.isConnected && ultimoFocoEditor.getClientRects().length ? ultimoFocoEditor : null;
|
||
if (previo && enfocar(previo)) return;
|
||
var cab = lista && lista.querySelector(".ed-cab-principal");
|
||
if (cab && enfocar(cab)) { if (cab.scrollIntoView) cab.scrollIntoView({ block: "nearest" }); return; }
|
||
var pal = document.querySelector('#nuevo-tipo [data-tipo][tabindex="0"]') || document.querySelector("#nuevo-tipo [data-tipo]");
|
||
enfocar(pal || lista);
|
||
});
|
||
return true;
|
||
}
|
||
|
||
function alternarPanel() {
|
||
var movil = porId("panel-movil");
|
||
var a = document.activeElement;
|
||
if (movil && a && movil.contains(a)) irAlEditor();
|
||
else irAVistaPrevia();
|
||
}
|
||
|
||
function enlazarSaltos() {
|
||
var enlace = porId("saltar-vista");
|
||
if (enlace) enlace.addEventListener("click", function (e) { e.preventDefault(); irAVistaPrevia(); });
|
||
var ed = porId("panel-editor");
|
||
if (ed) ed.addEventListener("focusin", function (e) { ultimoFocoEditor = e.target; });
|
||
}
|
||
|
||
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 (k === "F6" && !mod && !e.altKey && !hayDialogo) {
|
||
e.preventDefault();
|
||
alternarPanel();
|
||
return;
|
||
}
|
||
|
||
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 "g": case "G": e.preventDefault(); alternarPanel(); 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(devolverFoco) {
|
||
var tb = porId("toolbar");
|
||
var mas = porId("btn-mas");
|
||
if (!tb || !tb.classList.contains("tb-menu-abierto")) return false;
|
||
var menu = porId("tb-menu");
|
||
var dentro = menu && menu.contains(document.activeElement);
|
||
tb.classList.remove("tb-menu-abierto");
|
||
if (mas) {
|
||
mas.setAttribute("aria-expanded", "false");
|
||
if (devolverFoco !== false && dentro) mas.focus();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Opciones visibles del menú «⋯» abierto (en escritorio solo «Volver al ejemplo»)
|
||
function opcionesMenu() {
|
||
var menu = porId("tb-menu");
|
||
if (!menu) return [];
|
||
var bs = esEstrecha() ? menu.querySelectorAll("button:not([disabled])") : menu.querySelectorAll("#btn-reset");
|
||
return Array.prototype.filter.call(bs, function (b) { return b.getClientRects().length > 0; });
|
||
}
|
||
|
||
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 ops = opcionesMenu(); if (ops[0]) ops[0].focus(); }
|
||
});
|
||
// Flechas dentro del menú abierto
|
||
tb.addEventListener("keydown", function (e) {
|
||
if (!tb.classList.contains("tb-menu-abierto") || (e.key !== "ArrowDown" && e.key !== "ArrowUp")) return;
|
||
var ops = opcionesMenu();
|
||
var i = ops.indexOf(document.activeElement);
|
||
if (i < 0 && document.activeElement !== mas) return;
|
||
e.preventDefault();
|
||
var j = i < 0 ? 0 : (i + (e.key === "ArrowDown" ? 1 : -1) + ops.length) % ops.length;
|
||
if (ops[j]) ops[j].focus();
|
||
});
|
||
// Al salir con Tab del menú abierto, se cierra
|
||
tb.addEventListener("focusout", function (e) {
|
||
if (!tb.classList.contains("tb-menu-abierto")) return;
|
||
var sig = e.relatedTarget;
|
||
if (sig && (menu.contains(sig) || sig === mas)) return;
|
||
if (sig) cerrarMenu(false);
|
||
});
|
||
menu.addEventListener("click", function (e) {
|
||
if (e.target.closest && e.target.closest("button")) cerrarMenu(false);
|
||
});
|
||
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");
|
||
};
|
||
var barra = porId("barra-movil");
|
||
var fijarBarra = function () {
|
||
if (!barra) return;
|
||
var r = barra.getBoundingClientRect();
|
||
if (r.height) document.documentElement.style.setProperty("--wam-barra-alto", Math.ceil(r.height + 14) + "px");
|
||
};
|
||
fijar();
|
||
fijarBarra();
|
||
if (typeof ResizeObserver === "function") {
|
||
new ResizeObserver(fijar).observe(tb);
|
||
if (barra) new ResizeObserver(fijarBarra).observe(barra);
|
||
} else window.addEventListener("resize", function () { fijar(); fijarBarra(); });
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
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
|
||
------------------------------------------------------------------ */
|
||
var temaPintado = null;
|
||
function sincronizarTema() {
|
||
var tema = document.body.getAttribute("data-tema");
|
||
var b = porId("btn-tema");
|
||
if (b) {
|
||
b.setAttribute("aria-pressed", tema === "oscuro" ? "true" : "false");
|
||
b.setAttribute("aria-label", "Tema oscuro");
|
||
}
|
||
// El cambio de tema (botón o T) también pasa por «Guardando…»
|
||
if (temaPintado !== null && tema !== temaPintado) marcarEditado();
|
||
temaPintado = tema;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Zoom manual del teléfono (Ajustar / 100 % / 125 %). El CSS vive en
|
||
iphone.css (body[data-wam-zoom]); sin atributo = ajustar al hueco.
|
||
------------------------------------------------------------------ */
|
||
var CLAVE_ZOOM = "wam-zoom";
|
||
function aplicarZoom(v) {
|
||
v = v === "100" || v === "125" ? v : "";
|
||
if (v) document.body.setAttribute("data-wam-zoom", v);
|
||
else document.body.removeAttribute("data-wam-zoom");
|
||
var sel = porId("sel-zoom");
|
||
if (sel && sel.value !== v) sel.value = v;
|
||
return v;
|
||
}
|
||
WAM.setZoom = function (v) {
|
||
v = aplicarZoom(String(v == null ? "" : v));
|
||
if (v) lsSet(CLAVE_ZOOM, v); else lsDel(CLAVE_ZOOM);
|
||
return v;
|
||
};
|
||
function enlazarZoom() {
|
||
aplicarZoom(lsGet(CLAVE_ZOOM) || "");
|
||
var sel = porId("sel-zoom");
|
||
if (sel) sel.addEventListener("change", function () { WAM.setZoom(sel.value); });
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
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();
|
||
enlazarSaltos();
|
||
enlazarZoom();
|
||
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);
|
||
};
|
||
})();
|