351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
/* WAM · main.js — dueño D (núcleo).
|
||
Bootstrap: carga el estado, aplica tema, renderiza chat y editor,
|
||
y enlaza la delegación de clics del chat, el composer, la reproducción
|
||
de la demo (velocidad, reiniciar chat) y la toolbar. Único script con
|
||
ejecución al cargar. */
|
||
window.WAM = window.WAM || {};
|
||
(function () {
|
||
"use strict";
|
||
|
||
function porId(id) {
|
||
return document.getElementById(id);
|
||
}
|
||
|
||
function al(id, evento, manejador) {
|
||
var el = porId(id);
|
||
if (el) el.addEventListener(evento, manejador);
|
||
}
|
||
|
||
/* ————————————————— aviso visible (toast) ————————————————— */
|
||
|
||
/* Aviso flotante accesible. Estilos por CSSOM (compatibles con una CSP
|
||
style-src 'self'); no depende de ningún CSS ajeno. */
|
||
var avisoTimer = null;
|
||
WAM.avisar = function (texto, opciones) {
|
||
var op = opciones || {};
|
||
var caja = porId("wam-aviso");
|
||
if (!caja) {
|
||
caja = document.createElement("div");
|
||
caja.id = "wam-aviso";
|
||
caja.setAttribute("role", "alert");
|
||
caja.setAttribute("aria-live", "assertive");
|
||
var s = caja.style;
|
||
s.position = "fixed";
|
||
s.left = "50%";
|
||
s.bottom = "20px";
|
||
s.transform = "translateX(-50%)";
|
||
s.zIndex = "9999";
|
||
s.maxWidth = "min(560px, calc(100vw - 32px))";
|
||
s.padding = "12px 40px 12px 16px";
|
||
s.borderRadius = "12px";
|
||
s.font = "500 14px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif";
|
||
s.boxShadow = "0 8px 28px rgba(0,0,0,.28)";
|
||
var cerrar = document.createElement("button");
|
||
cerrar.type = "button";
|
||
cerrar.setAttribute("aria-label", "Cerrar aviso");
|
||
cerrar.textContent = "×";
|
||
var c = cerrar.style;
|
||
c.position = "absolute";
|
||
c.top = "6px";
|
||
c.right = "8px";
|
||
c.border = "0";
|
||
c.background = "transparent";
|
||
c.color = "inherit";
|
||
c.font = "600 20px/1 Arial, sans-serif";
|
||
c.cursor = "pointer";
|
||
cerrar.addEventListener("click", function () { caja.hidden = true; });
|
||
var txt = document.createElement("span");
|
||
txt.className = "wam-aviso-texto";
|
||
caja.appendChild(txt);
|
||
caja.appendChild(cerrar);
|
||
document.body.appendChild(caja);
|
||
}
|
||
var error = op.tipo !== "info";
|
||
caja.style.background = error ? "#b3261e" : "#1f2c33";
|
||
caja.style.color = "#fff";
|
||
caja.querySelector(".wam-aviso-texto").textContent = String(texto == null ? "" : texto);
|
||
caja.hidden = false;
|
||
if (avisoTimer) clearTimeout(avisoTimer);
|
||
avisoTimer = null;
|
||
if (op.duracion) {
|
||
avisoTimer = setTimeout(function () { caja.hidden = true; }, op.duracion);
|
||
}
|
||
};
|
||
|
||
/* ————————————————— botón "Reproducir demo" ————————————————— */
|
||
|
||
var SVG_STOP = '<rect x="6.5" y="6.5" width="11" height="11" rx="2"/>';
|
||
|
||
/* Contador discreto «Paso 3/12» junto al botón (se crea una vez). */
|
||
function contadorDemo(btn) {
|
||
var c = porId("demo-paso");
|
||
if (c) return c;
|
||
c = document.createElement("span");
|
||
c.id = "demo-paso";
|
||
c.className = "demo-paso";
|
||
c.setAttribute("aria-live", "polite");
|
||
c.setAttribute("role", "status");
|
||
c.hidden = true;
|
||
/* estilo mínimo en línea (tabular) mientras no haya regla en iphone.css */
|
||
c.style.fontSize = "12px";
|
||
c.style.fontVariantNumeric = "tabular-nums";
|
||
c.style.opacity = "0.75";
|
||
c.style.whiteSpace = "nowrap";
|
||
btn.parentNode.insertBefore(c, btn);
|
||
return c;
|
||
}
|
||
|
||
function pintarBotonDemo() {
|
||
var btn = porId("btn-reproducir");
|
||
if (!btn) return;
|
||
var activa = WAM.demoActiva();
|
||
btn.setAttribute("aria-pressed", activa ? "true" : "false");
|
||
var svg = btn.querySelector("svg");
|
||
if (svg) {
|
||
if (!svg.dataset.play) svg.dataset.play = svg.innerHTML;
|
||
var deseado = activa ? SVG_STOP : svg.dataset.play;
|
||
if (svg.innerHTML !== deseado) svg.innerHTML = deseado;
|
||
}
|
||
var span = btn.querySelector("span");
|
||
if (span) {
|
||
if (!span.dataset.etiqueta) span.dataset.etiqueta = span.textContent;
|
||
span.textContent = activa ? "Detener" : span.dataset.etiqueta;
|
||
}
|
||
if (btn.parentNode) {
|
||
var c = contadorDemo(btn);
|
||
var p = typeof WAM.progresoDemo === "function" ? WAM.progresoDemo() : null;
|
||
if (activa && p && p.total > 0 && p.paso > 0) {
|
||
var txt = "Paso " + p.paso + "/" + p.total;
|
||
if (c.textContent !== txt) c.textContent = txt;
|
||
c.hidden = false;
|
||
} else {
|
||
c.hidden = true;
|
||
c.textContent = "";
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Evita que un doble clic arranque y detenga la demo en el mismo gesto. */
|
||
var ultimoReproducir = 0;
|
||
function alReproducir() {
|
||
var ahora = Date.now();
|
||
if (ahora - ultimoReproducir < 350) return;
|
||
ultimoReproducir = ahora;
|
||
if (WAM.demoActiva()) WAM.detenerDemo();
|
||
else WAM.reproducirDemo();
|
||
pintarBotonDemo();
|
||
}
|
||
|
||
function alReiniciarChat() {
|
||
if (typeof WAM.reiniciarChat === "function") WAM.reiniciarChat();
|
||
else WAM.render();
|
||
pintarBotonDemo();
|
||
}
|
||
|
||
function alVelocidad(e) {
|
||
var v = WAM.setVelocidad(e.currentTarget.value);
|
||
e.currentTarget.value = String(v);
|
||
}
|
||
|
||
/* ————————————————— toolbar ————————————————— */
|
||
|
||
function alTema() {
|
||
WAM.estado.tema = WAM.estado.tema === "oscuro" ? "claro" : "oscuro";
|
||
WAM.guardar();
|
||
WAM.setTema(WAM.estado.tema);
|
||
}
|
||
|
||
function alPresentacion() {
|
||
document.body.classList.toggle("presentacion");
|
||
var btn = porId("btn-presentacion");
|
||
if (btn) {
|
||
btn.setAttribute("aria-pressed",
|
||
document.body.classList.contains("presentacion") ? "true" : "false");
|
||
}
|
||
}
|
||
|
||
function reRenderTotal() {
|
||
paso(function () { WAM.setTema(WAM.estado.tema); });
|
||
paso(function () { WAM.render(); });
|
||
paso(function () { WAM.renderEditor(); });
|
||
pintarBotonDemo();
|
||
}
|
||
|
||
function alExportar() {
|
||
if (typeof WAM.guardarYa === "function") WAM.guardarYa();
|
||
if (typeof WAM.exportarJSON === "function") WAM.exportarJSON();
|
||
}
|
||
|
||
function alImportar() {
|
||
var abrir = function () {
|
||
var archivo = porId("importar-archivo");
|
||
if (archivo) archivo.click();
|
||
};
|
||
/* Hook opcional de js/ux.js: <dialog> con «Exportar antes» */
|
||
if (typeof WAM.confirmarReemplazo === "function") { WAM.confirmarReemplazo("importar", abrir); return; }
|
||
abrir();
|
||
}
|
||
|
||
function alArchivoImport(e) {
|
||
var input = e.currentTarget;
|
||
var archivo = input.files && input.files[0];
|
||
input.value = ""; /* permite reimportar el mismo archivo */
|
||
if (!archivo || typeof WAM.importarJSON !== "function") return;
|
||
WAM.detenerDemo();
|
||
var promesa;
|
||
try { promesa = WAM.importarJSON(archivo); } catch (err) { promesa = Promise.reject(err); }
|
||
if (promesa && typeof promesa.then === "function") {
|
||
promesa.then(reRenderTotal, function () {
|
||
reRenderTotal();
|
||
WAM.avisar("No se pudo importar: el archivo no es un JSON válido de la demo.", { duracion: 6000 });
|
||
});
|
||
} else {
|
||
reRenderTotal();
|
||
}
|
||
}
|
||
|
||
function alReset() {
|
||
var ejecutar = function () {
|
||
WAM.detenerDemo();
|
||
WAM.resetear();
|
||
reRenderTotal();
|
||
};
|
||
/* Hook opcional de js/ux.js: <dialog> con «Exportar antes» + deshacer */
|
||
if (typeof WAM.confirmarReemplazo === "function") { WAM.confirmarReemplazo("reset", ejecutar); return; }
|
||
var nombre = (WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "la demo";
|
||
if (!window.confirm("¿Restablecer la demo «Café Aurora»? Se perderán los cambios no exportados de «" +
|
||
nombre + "».")) return;
|
||
WAM.detenerDemo();
|
||
WAM.resetear();
|
||
reRenderTotal();
|
||
}
|
||
|
||
/* ————————————————— chat: delegación de clics (contrato B→D) ————————————————— */
|
||
|
||
/* Enlaces del chat con esquema no permitido (javascript:, data:…) no se abren. */
|
||
function bloquearEnlaceInseguro(e) {
|
||
var a = (e.target && e.target.closest) ? e.target.closest("a[href]") : null;
|
||
if (!a) return false;
|
||
var href = a.getAttribute("href");
|
||
if (typeof WAM.urlSegura === "function" && !WAM.urlSegura(href, "enlace")) {
|
||
e.preventDefault();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function alClicChat(e) {
|
||
if (bloquearEnlaceInseguro(e)) return;
|
||
var objetivo = e.target;
|
||
var el = (objetivo && objetivo.closest) ? objetivo.closest("[data-flujo]") : null;
|
||
if (!el) return;
|
||
e.preventDefault(); /* protege frente a botones montados sobre <a href="#"> */
|
||
var ds = el.dataset || {};
|
||
if (ds.fila !== undefined) return; /* filas del sheet: se atienden en #pantalla */
|
||
if (el.classList.contains("usado") || el.getAttribute("aria-disabled") === "true") return;
|
||
|
||
var msg = WAM.mensajeDeElemento(el);
|
||
var plano = ds.btn === undefined && ds.sec === undefined && ds.card === undefined;
|
||
|
||
if (plano) {
|
||
if (msg && msg.tipo === "list") WAM.abrirLista(msg);
|
||
return;
|
||
}
|
||
|
||
var flujo = WAM.resolverFlujo(el);
|
||
if (!flujo || !flujo.msg) return;
|
||
WAM.marcarUsado(el); /* gris + bloquea el doble clic */
|
||
WAM.flujo(flujo.texto, flujo.respuestas, flujo.cita);
|
||
}
|
||
|
||
/* Opciones y cierre de la hoja de lista (vive en #pantalla, fuera de #chat-cuerpo). */
|
||
function alClicPantalla(e) {
|
||
var objetivo = e.target;
|
||
if (!objetivo || !objetivo.closest) return;
|
||
var hoja = porId("sheet");
|
||
if (!hoja || !hoja.contains(objetivo)) return;
|
||
if (objetivo.closest(".sheet-cerrar")) {
|
||
e.preventDefault();
|
||
WAM.cerrarSheet();
|
||
return;
|
||
}
|
||
var fila = objetivo.closest("[data-fila]");
|
||
if (!fila) return;
|
||
e.preventDefault();
|
||
var flujo = WAM.resolverFlujo(fila);
|
||
WAM.cerrarSheet();
|
||
if (flujo && flujo.msg) WAM.flujo(flujo.texto, flujo.respuestas, flujo.cita);
|
||
}
|
||
|
||
/* Teclado: Enter / Espacio activan los botones del teléfono (span role=button);
|
||
Escape cierra la hoja de lista. */
|
||
function alTeclaPantalla(e) {
|
||
if (e.key === "Escape") {
|
||
if (porId("sheet")) { e.preventDefault(); WAM.cerrarSheet(); }
|
||
return;
|
||
}
|
||
if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return;
|
||
var t = e.target;
|
||
if (!t || !t.closest || t.tagName === "A" || t.tagName === "BUTTON" ||
|
||
t.tagName === "INPUT" || t.tagName === "TEXTAREA") return;
|
||
var el = t.closest("[data-flujo],[data-ver-todas],.sheet-cerrar");
|
||
if (!el || el !== t) return;
|
||
e.preventDefault();
|
||
el.click();
|
||
}
|
||
|
||
/* ————————————————— composer ————————————————— */
|
||
|
||
function alComposer(e) {
|
||
e.preventDefault();
|
||
var input = porId("composer-input");
|
||
var texto = input ? input.value : "";
|
||
WAM.enviarLibre(texto);
|
||
if (input) {
|
||
input.value = "";
|
||
input.focus();
|
||
}
|
||
}
|
||
|
||
/* ————————————————— bootstrap ————————————————— */
|
||
|
||
/* Un fallo en un módulo (render o editor) no debe dejar sin enlazar el
|
||
resto: cada paso del arranque se aísla. */
|
||
function paso(fn) {
|
||
try { fn(); } catch (e) {
|
||
setTimeout(function () { throw e; }, 0); /* visible en consola, sin cortar init */
|
||
}
|
||
}
|
||
|
||
WAM.init = function () {
|
||
WAM.onErrorGuardar = function (texto) { WAM.avisar(texto); };
|
||
paso(function () { WAM.estado = WAM.cargar(); });
|
||
if (!WAM.estado) WAM.estado = WAM.normalizar(WAM.datosDemo());
|
||
paso(function () { WAM.setTema(WAM.estado.tema); });
|
||
paso(function () { WAM.render(); });
|
||
paso(function () { WAM.renderEditor(); });
|
||
|
||
WAM.onDemoChange = pintarBotonDemo;
|
||
al("chat-cuerpo", "click", alClicChat);
|
||
al("pantalla", "click", alClicPantalla);
|
||
al("pantalla", "keydown", alTeclaPantalla);
|
||
al("composer", "submit", alComposer);
|
||
al("btn-reproducir", "click", alReproducir);
|
||
al("btn-reiniciar", "click", alReiniciarChat);
|
||
var vel = porId("sel-velocidad");
|
||
if (vel) {
|
||
vel.value = String(WAM.getVelocidad());
|
||
vel.addEventListener("change", alVelocidad);
|
||
}
|
||
al("btn-tema", "click", alTema);
|
||
al("btn-presentacion", "click", alPresentacion);
|
||
al("btn-exportar", "click", alExportar);
|
||
al("btn-importar", "click", alImportar);
|
||
al("importar-archivo", "change", alArchivoImport);
|
||
al("btn-reset", "click", alReset);
|
||
pintarBotonDemo();
|
||
};
|
||
|
||
WAM.init();
|
||
})();
|