/* WAM · interactivity.js — dueño D (núcleo). Flujos de clic (quick replies, filas de lista, botones de plantilla y de card), composer libre, indicador "escribiendo…", ticks automáticos y modo "reproducir demo" paso a paso. No toca el DOM al cargar. */ window.WAM = window.WAM || {}; (function () { "use strict"; var CANCELADO = "wam:cancelado"; var TIPOS_VALIDOS = [ "text", "image", "video", "audio", "document", "sticker", "location", "contact", "template", "buttons", "list", "cta_url", "carousel" ]; /* Todo temporizador pasa por WAM.espera(); cancelarTodo() aborta los flujos y la reproducción de la demo en curso. */ var cancelaciones = []; var demoAndando = false; function quitar(fn) { var i = cancelaciones.indexOf(fn); if (i !== -1) cancelaciones.splice(i, 1); } function cancelarTodo() { var copia = cancelaciones.slice(); cancelaciones = []; for (var i = 0; i < copia.length; i++) copia[i](); } function escribiendo(visible) { if (typeof WAM.setEscribiendo === "function") WAM.setEscribiendo(visible); } /* Promise cancelable: se rechaza con CANCELADO si detenerDemo() la aborta. */ WAM.espera = function (ms) { return new Promise(function (resolver, rechazar) { var id = setTimeout(function () { quitar(cancelar); resolver(); }, ms); function cancelar() { clearTimeout(id); quitar(cancelar); rechazar(CANCELADO); } cancelaciones.push(cancelar); }); }; WAM.demoActiva = function () { return demoAndando; }; function avisarDemo() { if (typeof WAM.onDemoChange === "function") { try { WAM.onDemoChange(demoAndando); } catch (e) { /* el hook es opcional */ } } } /* ————————————————— alta de mensajes (mutación + guardar + DOM incremental) ————————————————— */ function anadir(msg) { WAM.estado.mensajes.push(msg); WAM.guardar(); if (typeof WAM.anadirMensajeDOM === "function") WAM.anadirMensajeDOM(msg); } function mensajeOut(texto) { return { id: WAM.generarId(), dir: "out", tipo: "text", hora: WAM.horaAhora(), estado: "sent", texto: String(texto == null ? "" : texto) }; } /* Copia una respuesta anidada (Mensaje completo) para poder reutilizarla cada vez que se vuelve a pulsar el mismo botón. */ function mensajeDesde(origen) { var copia; try { copia = JSON.parse(JSON.stringify( (origen && typeof origen === "object") ? origen : { tipo: "text", texto: String(origen == null ? "" : origen) } )); } catch (e) { copia = { tipo: "text", texto: "" }; } if (!copia || typeof copia !== "object" || Array.isArray(copia)) copia = { tipo: "text", texto: "" }; if (TIPOS_VALIDOS.indexOf(copia.tipo) === -1) copia.tipo = "text"; copia.id = WAM.generarId(); copia.dir = "in"; copia.hora = WAM.horaAhora(); copia.estado = "sent"; return copia; } /* Ticks automáticos sent → delivered → read sobre la burbuja OUT indicada. Se localiza por data-id del msg (no "la última fila") para que dos envíos solapados avancen cada uno su propio tick. */ function repintarTickDe(msg, estado) { var cuerpo = document.getElementById("chat-cuerpo"); if (!cuerpo || typeof WAM.tick !== "function") return; if (!msg || typeof msg.id !== "string" || !msg.id) return; var seguro = msg.id.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); var fila = cuerpo.querySelector('.wa-row[data-id="' + seguro + '"]'); if (!fila || !fila.classList.contains("out")) return; var tick = fila.querySelector(".tick"); if (tick && tick.parentNode) { /* WAM.tick devuelve un completo: se REEMPLAZA el nodo para no anidar un svg dentro de otro (se recortaría el segundo tick). */ var aux = document.createElement("span"); aux.innerHTML = WAM.tick(estado); if (aux.firstChild) tick.parentNode.replaceChild(aux.firstChild, tick); } } async function animarTics(msg) { await WAM.espera(800); msg.estado = "delivered"; WAM.guardar(); repintarTickDe(msg, "delivered"); await WAM.espera(800); msg.estado = "read"; WAM.guardar(); repintarTickDe(msg, "read"); } /* ————————————————— flujos ————————————————— */ /* Burbuja OUT con el texto (ticks auto), typing ~900 ms y, por cada respuesta, typing ~900 ms + anadirMensajeDOM (con scroll). Cancelable por detenerDemo(). */ WAM.flujo = function (texto, respuestas) { var lista = Array.isArray(respuestas) ? respuestas : []; var secuencia = (async function () { try { var out = mensajeOut(texto); anadir(out); await animarTics(out); for (var i = 0; i < lista.length; i++) { escribiendo(true); await WAM.espera(900); escribiendo(false); anadir(mensajeDesde(lista[i])); } escribiendo(false); } catch (e) { escribiendo(false); if (e !== CANCELADO) { /* el mockup nunca ensucia la consola */ } } })(); return secuencia; }; /* Envío libre del composer: burbuja OUT con hora actual y ticks automáticos. */ WAM.enviarLibre = function (texto) { var limpio = String(texto == null ? "" : texto).trim(); if (!limpio) return; var out = mensajeOut(limpio); anadir(out); var tics = animarTics(out); if (typeof tics.catch === "function") tics.catch(function () { /* cancelado */ }); }; /* Resuelve { texto, respuestas } desde los data-* que estampa render.js: data-flujo (índice del mensaje) + data-btn (template/buttons), data-sec + data-fila (lista) o data-card (carousel). */ WAM.resolverFlujo = function (el) { var vacio = { texto: "", respuestas: [] }; if (!el || typeof el.getAttribute !== "function") return vacio; var indice = parseInt(el.getAttribute("data-flujo"), 10); var mensajes = (WAM.estado && WAM.estado.mensajes) ? WAM.estado.mensajes : []; var msg = isNaN(indice) ? null : mensajes[indice]; if (!msg) return vacio; var ds = el.dataset || {}; if (ds.sec !== undefined || ds.fila !== undefined) { var seccion = (msg.secciones || [])[parseInt(ds.sec, 10)]; var fila = ((seccion && seccion.filas) || [])[parseInt(ds.fila, 10)]; return fila ? { texto: fila.titulo || "", respuestas: fila.respuestas || [] } : vacio; } if (ds.btn !== undefined) { var boton = (msg.botones || [])[parseInt(ds.btn, 10)]; return boton ? { texto: boton.texto || "", respuestas: boton.respuestas || [] } : vacio; } if (ds.card !== undefined) { var card = (msg.cards || [])[parseInt(ds.card, 10)]; return card ? { texto: card.textoBoton || "", respuestas: card.respuestas || [] } : vacio; } /* Elemento plano de un mensaje list: es el botón que abre la sheet (main.js lo detecta y llama WAM.abrirSheet); sin flujo directo. */ return vacio; }; /* ————————————————— reproducción de la demo ————————————————— */ WAM.reproducirDemo = function () { if (demoAndando) return; demoAndando = true; avisarDemo(); var cuerpo = document.getElementById("chat-cuerpo"); if (cuerpo) cuerpo.innerHTML = ""; var secuencia = ((WAM.estado && WAM.estado.mensajes) || []).slice(); (async function () { try { for (var i = 0; i < secuencia.length; i++) { var msg = secuencia[i]; if (!msg) continue; if (msg.dir === "in") { escribiendo(true); await WAM.espera(700); escribiendo(false); } if (typeof WAM.anadirMensajeDOM === "function") WAM.anadirMensajeDOM(msg); await WAM.espera(600); } demoAndando = false; escribiendo(false); avisarDemo(); } catch (e) { demoAndando = false; escribiendo(false); avisarDemo(); } })(); }; WAM.detenerDemo = function () { var estaba = demoAndando; demoAndando = false; cancelarTodo(); /* aborta también flujos y typing en curso */ escribiendo(false); if (estaba && typeof WAM.render === "function") WAM.render(); if (estaba) avisarDemo(); }; })();