Files
whatsapp-api-mockup/js/main.js
T
urieljarethandClaude Opus 5.5 4fe8f73959 Aplicar pendientes tras la integración
- render.js: botones y cards con urlBloqueada se pintan inertes.
- interactivity.js: sinFormato con la misma regla de marcadores que render.js; cita de documento con icono y nombre.
- Zoom manual (Ajustar / 100 % / 125 %) en #barra-movil, persistido en localStorage.
- Contador «Paso N/M» con estilo en iphone.css en lugar de estilos en línea.
- store/editor: campos opcionales tamano y headerTamano; editar la URL retira urlBloqueada;
  urlValida acepta URLs con forma de dominio; importar avisa de lo recortado.
- README y SPEC actualizados.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 21:39:09 -06:00

346 lines
12 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.
/* 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;
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();
})();