Files
whatsapp-api-mockup/js/interactivity.js
T
2026-09-27 19:17:33 -06:00

237 lines
8.3 KiB
JavaScript

/* 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 <svg> 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();
};
})();