WIP: mejoras de fidelidad, editor, lógica y suite e2e (checkpoint tras reinicio)
This commit is contained in:
+560
-114
@@ -1,7 +1,15 @@
|
||||
/* 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. */
|
||||
"reproducir demo" paso a paso (con autoPulsar y velocidad). No toca el DOM
|
||||
al cargar.
|
||||
|
||||
Guion frente a sesión: WAM.estado.mensajes es el GUION (persistente, lo que
|
||||
edita el editor y reproduce la demo). Lo que ocurre al pulsar botones o
|
||||
escribir en el composer es la SESIÓN (WAM.sesion, solo en memoria): se pinta
|
||||
con anadirMensajeDOM sin tocar el estado ni localStorage. Cualquier
|
||||
WAM.render() vuelve al guion: vacía la sesión, cierra la hoja de lista y
|
||||
cancela flujos y demo en curso. */
|
||||
window.WAM = window.WAM || {};
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -11,42 +19,127 @@ window.WAM = window.WAM || {};
|
||||
"text", "image", "video", "audio", "document", "sticker", "location",
|
||||
"contact", "template", "buttons", "list", "cta_url", "carousel"
|
||||
];
|
||||
var CLAVE_VELOCIDAD = "wam-mockup-v1-velocidad";
|
||||
var VELOCIDADES = [0.5, 1, 2];
|
||||
|
||||
/* 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 nada() { /* noop */ }
|
||||
|
||||
function quitar(fn) {
|
||||
var i = cancelaciones.indexOf(fn);
|
||||
if (i !== -1) cancelaciones.splice(i, 1);
|
||||
/* Mensajes de la sesión en curso (efímeros). */
|
||||
WAM.sesion = [];
|
||||
var contSesion = 0;
|
||||
function idSesion() { contSesion++; return "s" + contSesion; }
|
||||
|
||||
/* ————————————————— temporizadores cancelables por grupo ————————————————— */
|
||||
|
||||
/* Dos grupos: "demo" (reproducción) y "flujo" (clics, composer, ticks).
|
||||
Cancelar un grupo sube su generación: los pasos pendientes de esa
|
||||
generación se rechazan con CANCELADO y ya no pueden programar más. */
|
||||
var grupos = {
|
||||
demo: { gen: 0, cancelar: [] },
|
||||
flujo: { gen: 0, cancelar: [] }
|
||||
};
|
||||
|
||||
function grupoDe(nombre) { return grupos[nombre] ? nombre : "flujo"; }
|
||||
|
||||
var velocidad = 1;
|
||||
try {
|
||||
var guardada = parseFloat(window.localStorage.getItem(CLAVE_VELOCIDAD));
|
||||
if (VELOCIDADES.indexOf(guardada) !== -1) velocidad = guardada;
|
||||
} catch (e) { /* sin localStorage */ }
|
||||
|
||||
WAM.getVelocidad = function () { return velocidad; };
|
||||
WAM.setVelocidad = function (v) {
|
||||
var n = parseFloat(v);
|
||||
if (VELOCIDADES.indexOf(n) === -1) n = 1;
|
||||
velocidad = n;
|
||||
try { window.localStorage.setItem(CLAVE_VELOCIDAD, String(n)); } catch (e) { /* idem */ }
|
||||
return velocidad;
|
||||
};
|
||||
|
||||
function esperaEn(nombre, ms, gen) {
|
||||
var g = grupos[grupoDe(nombre)];
|
||||
return new Promise(function (resolver, rechazar) {
|
||||
if (gen !== undefined && gen !== g.gen) { rechazar(CANCELADO); return; }
|
||||
var dur = Math.max(0, (Number(ms) || 0) / velocidad);
|
||||
var id = setTimeout(function () {
|
||||
quitar();
|
||||
if (gen !== undefined && gen !== g.gen) rechazar(CANCELADO);
|
||||
else resolver();
|
||||
}, dur);
|
||||
function cancelar() {
|
||||
clearTimeout(id);
|
||||
quitar();
|
||||
rechazar(CANCELADO);
|
||||
}
|
||||
function quitar() {
|
||||
var i = g.cancelar.indexOf(cancelar);
|
||||
if (i !== -1) g.cancelar.splice(i, 1);
|
||||
}
|
||||
g.cancelar.push(cancelar);
|
||||
});
|
||||
}
|
||||
|
||||
/* Promise cancelable (SPEC): todo temporizador pasa por aquí.
|
||||
grupo opcional: "flujo" (por defecto) o "demo". La duración se divide
|
||||
por la velocidad elegida (0,5x / 1x / 2x). */
|
||||
WAM.espera = function (ms, grupo) { return esperaEn(grupo, ms); };
|
||||
|
||||
/* Contexto de una secuencia: fija grupo y generación al empezar. */
|
||||
function contexto(nombre) {
|
||||
var g = grupoDe(nombre);
|
||||
var gen = grupos[g].gen;
|
||||
return {
|
||||
grupo: g,
|
||||
vivo: function () { return grupos[g].gen === gen; },
|
||||
esperar: function (ms) { return esperaEn(g, ms, gen); }
|
||||
};
|
||||
}
|
||||
|
||||
var colas = { demo: Promise.resolve(), flujo: Promise.resolve() };
|
||||
|
||||
function cancelarGrupo(nombre) {
|
||||
var g = grupos[grupoDe(nombre)];
|
||||
g.gen++;
|
||||
var copia = g.cancelar.slice();
|
||||
g.cancelar = [];
|
||||
for (var i = 0; i < copia.length; i++) copia[i]();
|
||||
colas[grupoDe(nombre)] = Promise.resolve();
|
||||
}
|
||||
|
||||
function cancelarTodo() {
|
||||
var copia = cancelaciones.slice();
|
||||
cancelaciones = [];
|
||||
for (var i = 0; i < copia.length; i++) copia[i]();
|
||||
cancelarGrupo("demo");
|
||||
cancelarGrupo("flujo");
|
||||
escritores.clear();
|
||||
pintarEscribiendo();
|
||||
}
|
||||
|
||||
function escribiendo(visible) {
|
||||
if (typeof WAM.setEscribiendo === "function") WAM.setEscribiendo(visible);
|
||||
/* ————————————————— indicador "escribiendo…" (contador de referencias) ————————————————— */
|
||||
|
||||
var escritores = new Set();
|
||||
|
||||
function pintarEscribiendo() {
|
||||
if (typeof WAM.setEscribiendo === "function") WAM.setEscribiendo(escritores.size > 0);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
});
|
||||
};
|
||||
function empezarEscribir() {
|
||||
var ficha = {};
|
||||
escritores.add(ficha);
|
||||
pintarEscribiendo();
|
||||
return ficha;
|
||||
}
|
||||
|
||||
function dejarEscribir(ficha) {
|
||||
if (escritores.delete(ficha)) pintarEscribiendo();
|
||||
}
|
||||
|
||||
async function escribirDurante(ctx, ms) {
|
||||
var ficha = empezarEscribir();
|
||||
try { await ctx.esperar(ms); } finally { dejarEscribir(ficha); }
|
||||
}
|
||||
|
||||
/* ————————————————— estado de la demo ————————————————— */
|
||||
|
||||
var demoAndando = false;
|
||||
|
||||
WAM.demoActiva = function () { return demoAndando; };
|
||||
|
||||
@@ -56,23 +149,83 @@ window.WAM = window.WAM || {};
|
||||
}
|
||||
}
|
||||
|
||||
/* ————————————————— alta de mensajes (mutación + guardar + DOM incremental) ————————————————— */
|
||||
/* ————————————————— búsqueda de mensajes y filas ————————————————— */
|
||||
|
||||
function anadir(msg) {
|
||||
WAM.estado.mensajes.push(msg);
|
||||
WAM.guardar();
|
||||
if (typeof WAM.anadirMensajeDOM === "function") WAM.anadirMensajeDOM(msg);
|
||||
function cuerpoChat() { return document.getElementById("chat-cuerpo"); }
|
||||
|
||||
function selectorId(id) {
|
||||
return '.wa-row[data-id="' + String(id).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"]';
|
||||
}
|
||||
|
||||
function mensajeOut(texto) {
|
||||
return {
|
||||
id: WAM.generarId(),
|
||||
function filaDe(msg) {
|
||||
var cuerpo = cuerpoChat();
|
||||
if (!cuerpo || !msg || typeof msg.id !== "string" || !msg.id) return null;
|
||||
var filas = cuerpo.querySelectorAll(selectorId(msg.id));
|
||||
return filas.length ? filas[filas.length - 1] : null;
|
||||
}
|
||||
|
||||
function buscarPorId(id) {
|
||||
if (!id) return null;
|
||||
var guion = (WAM.estado && WAM.estado.mensajes) || [];
|
||||
for (var i = 0; i < guion.length; i++) if (guion[i] && guion[i].id === id) return guion[i];
|
||||
for (var j = WAM.sesion.length - 1; j >= 0; j--) if (WAM.sesion[j] && WAM.sesion[j].id === id) return WAM.sesion[j];
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Mensaje de la hoja de lista abierta (guion o sesión). */
|
||||
var hojaMsg = null;
|
||||
|
||||
/* Mensaje al que pertenece un elemento clicable del chat o de la hoja.
|
||||
Se resuelve por el data-id de su fila (inmune a índices obsoletos y
|
||||
válido para mensajes de sesión); el índice data-flujo queda de respaldo. */
|
||||
WAM.mensajeDeElemento = function (el) {
|
||||
if (!el || !el.closest) return null;
|
||||
var hoja = el.closest("#sheet");
|
||||
if (hoja) return hojaMsg;
|
||||
var fila = el.closest(".wa-row[data-id]");
|
||||
var msg = fila ? buscarPorId(fila.getAttribute("data-id")) : null;
|
||||
if (msg) return msg;
|
||||
var indice = parseInt(el.getAttribute("data-flujo"), 10);
|
||||
var guion = (WAM.estado && WAM.estado.mensajes) || [];
|
||||
return isNaN(indice) ? null : (guion[indice] || null);
|
||||
};
|
||||
|
||||
/* Abre la hoja de lista de un mensaje (del guion o de la sesión). */
|
||||
WAM.abrirLista = function (msg) {
|
||||
if (!msg || typeof WAM.abrirSheet !== "function") return;
|
||||
hojaMsg = msg;
|
||||
var guion = (WAM.estado && WAM.estado.mensajes) || [];
|
||||
var i = guion.indexOf(msg);
|
||||
WAM.abrirSheet(i >= 0 ? i : msg);
|
||||
};
|
||||
|
||||
/* ————————————————— pintado incremental ————————————————— */
|
||||
|
||||
/* grupo / cola (.grupo / .ultimo) los recalcula anadirMensajeDOM según el DOM. */
|
||||
function pintar(msg) {
|
||||
if (typeof WAM.anadirMensajeDOM !== "function") return null;
|
||||
WAM.anadirMensajeDOM(msg);
|
||||
var fila = filaDe(msg);
|
||||
if (fila && msg.cita) pintarCita(fila, msg.cita);
|
||||
return fila;
|
||||
}
|
||||
|
||||
function anadirSesion(msg) {
|
||||
WAM.sesion.push(msg);
|
||||
return pintar(msg);
|
||||
}
|
||||
|
||||
function mensajeOut(texto, cita) {
|
||||
var msg = {
|
||||
id: idSesion(),
|
||||
dir: "out",
|
||||
tipo: "text",
|
||||
hora: WAM.horaAhora(),
|
||||
estado: "sent",
|
||||
texto: String(texto == null ? "" : texto)
|
||||
};
|
||||
if (cita) msg.cita = cita;
|
||||
return msg;
|
||||
}
|
||||
|
||||
/* Copia una respuesta anidada (Mensaje completo) para poder reutilizarla
|
||||
@@ -88,68 +241,188 @@ window.WAM = window.WAM || {};
|
||||
}
|
||||
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.id = idSesion();
|
||||
copia.dir = "in";
|
||||
copia.hora = WAM.horaAhora();
|
||||
copia.estado = "sent";
|
||||
delete copia.cita;
|
||||
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. */
|
||||
/* ————————————————— cita de la respuesta (quoted reply) ————————————————— */
|
||||
|
||||
function vacio(v) { return v === undefined || v === null || String(v).trim() === ""; }
|
||||
|
||||
function sinFormato(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/```([\s\S]*?)```/g, "$1")
|
||||
.replace(/`([^`\n]+)`/g, "$1")
|
||||
.replace(/\*([^*\n]+)\*/g, "$1")
|
||||
.replace(/(^|[^\w])_([^_\n]+)_(?=[^\w]|$)/g, "$1$2")
|
||||
.replace(/(^|[^\w])~([^~\n]+)~(?=[^\w]|$)/g, "$1$2")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function miniaturaSegura(u) {
|
||||
if (vacio(u)) return "";
|
||||
var s = String(u);
|
||||
if (/^data:video\//i.test(s)) return "";
|
||||
if (typeof WAM.urlSegura === "function" && !WAM.urlSegura(s, "media")) return "";
|
||||
return s;
|
||||
}
|
||||
|
||||
/* { nombre, titulo, extracto, miniatura, foto } del mensaje original. */
|
||||
function citaDe(msg, opciones) {
|
||||
if (!msg) return null;
|
||||
var cuenta = (WAM.estado && WAM.estado.cuenta) || {};
|
||||
var cita = {
|
||||
nombre: vacio(cuenta.nombre) ? "Chat" : String(cuenta.nombre),
|
||||
titulo: "",
|
||||
extracto: "",
|
||||
miniatura: "",
|
||||
foto: false
|
||||
};
|
||||
var op = opciones || {};
|
||||
var partes = [];
|
||||
if (msg.tipo === "list") {
|
||||
cita.titulo = sinFormato(msg.header);
|
||||
partes = [msg.body, msg.footer];
|
||||
} else if (msg.tipo === "carousel") {
|
||||
var card = (msg.cards || [])[op.card] || {};
|
||||
partes = [vacio(card.texto) ? msg.body : card.texto];
|
||||
cita.miniatura = miniaturaSegura(card.media);
|
||||
if (!cita.miniatura && vacio(card.media) && typeof WAM.svgPlaceholder === "function") {
|
||||
cita.miniatura = WAM.svgPlaceholder("");
|
||||
}
|
||||
cita.foto = true;
|
||||
} else {
|
||||
if (msg.headerTipo === "texto") cita.titulo = sinFormato(msg.headerTexto);
|
||||
partes = [msg.body];
|
||||
if (msg.headerTipo === "imagen") {
|
||||
cita.miniatura = vacio(msg.headerMedia)
|
||||
? (typeof WAM.svgPlaceholder === "function" ? WAM.svgPlaceholder("") : "")
|
||||
: miniaturaSegura(msg.headerMedia);
|
||||
cita.foto = true;
|
||||
} else if (msg.headerTipo === "video") {
|
||||
cita.foto = true;
|
||||
}
|
||||
}
|
||||
cita.extracto = partes.filter(function (p) { return !vacio(p); }).map(sinFormato).join("\n");
|
||||
if (!cita.extracto && !cita.titulo) cita.extracto = cita.foto ? "Foto" : "Mensaje";
|
||||
return cita;
|
||||
}
|
||||
|
||||
var SVG_CAMARA = '<svg class="wa-cita-cam" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">' +
|
||||
'<path fill="currentColor" d="M5.6 2.5h4.8l1 1.5H13a1.5 1.5 0 0 1 1.5 1.5v6A1.5 1.5 0 0 1 13 12.5H3A1.5 1.5 0 0 1 1.5 11V5.5A1.5 1.5 0 0 1 3 4h1.6l1-1.5Zm2.4 3a2.75 2.75 0 1 0 0 5.5 2.75 2.75 0 0 0 0-5.5Z"/></svg>';
|
||||
|
||||
/* Bloque de cita dentro de la burbuja OUT; todo por textContent (sin XSS). */
|
||||
function pintarCita(fila, cita) {
|
||||
var burbuja = fila.querySelector(".bubble");
|
||||
if (!burbuja || burbuja.querySelector(".wa-cita")) return;
|
||||
var caja = document.createElement("div");
|
||||
caja.className = "wa-cita";
|
||||
var txt = document.createElement("div");
|
||||
txt.className = "wa-cita-txt";
|
||||
var nombre = document.createElement("div");
|
||||
nombre.className = "wa-cita-nombre";
|
||||
nombre.textContent = cita.nombre;
|
||||
txt.appendChild(nombre);
|
||||
if (cita.titulo) {
|
||||
var tit = document.createElement("div");
|
||||
tit.className = "wa-cita-titulo";
|
||||
tit.textContent = cita.titulo;
|
||||
txt.appendChild(tit);
|
||||
}
|
||||
if (cita.extracto) {
|
||||
var ext = document.createElement("div");
|
||||
ext.className = "wa-cita-extracto";
|
||||
if (cita.foto) {
|
||||
var cam = document.createElement("span");
|
||||
cam.innerHTML = SVG_CAMARA; /* constante propia, sin datos del usuario */
|
||||
if (cam.firstChild) ext.appendChild(cam.firstChild);
|
||||
}
|
||||
ext.appendChild(document.createTextNode(cita.extracto));
|
||||
txt.appendChild(ext);
|
||||
}
|
||||
caja.appendChild(txt);
|
||||
if (cita.miniatura) {
|
||||
var img = document.createElement("img");
|
||||
img.className = "wa-cita-mini";
|
||||
img.alt = "";
|
||||
img.draggable = false;
|
||||
img.src = cita.miniatura;
|
||||
caja.appendChild(img);
|
||||
}
|
||||
burbuja.insertBefore(caja, burbuja.firstChild);
|
||||
burbuja.classList.add("wa-con-cita");
|
||||
}
|
||||
|
||||
/* ————————————————— ticks automáticos ————————————————— */
|
||||
|
||||
/* Ticks sent → delivered → read sobre la burbuja OUT indicada (por data-id,
|
||||
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 (typeof WAM.tick !== "function") return;
|
||||
var fila = filaDe(msg);
|
||||
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). */
|
||||
/* WAM.tick devuelve un <svg> completo: se REEMPLAZA el nodo. */
|
||||
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");
|
||||
/* Los mensajes de sesión no se guardan; si se cancelan los ticks, la
|
||||
burbuja salta a "read" para no quedar congelada en ✓ / ✓✓ gris. */
|
||||
async function animarTics(msg, ctx) {
|
||||
try {
|
||||
await ctx.esperar(800);
|
||||
msg.estado = "delivered";
|
||||
repintarTickDe(msg, "delivered");
|
||||
await ctx.esperar(800);
|
||||
msg.estado = "read";
|
||||
repintarTickDe(msg, "read");
|
||||
} catch (e) {
|
||||
if (msg.estado !== "read") {
|
||||
msg.estado = "read";
|
||||
repintarTickDe(msg, "read");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/* ————————————————— 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) {
|
||||
/* Respuestas encadenadas: typing ~900 ms + burbuja IN por cada una.
|
||||
En la demo (enDemo) cada respuesta con autoPulsar se pulsa sola. */
|
||||
async function responder(lista, ctx, enDemo) {
|
||||
for (var i = 0; i < lista.length; i++) {
|
||||
await escribirDurante(ctx, 900);
|
||||
if (!ctx.vivo()) throw CANCELADO;
|
||||
var r = mensajeDesde(lista[i]);
|
||||
anadirSesion(r);
|
||||
if (enDemo) await autoPulsar(r, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/* Burbuja OUT con el texto (y cita opcional {nombre, extracto, miniatura}),
|
||||
ticks auto y respuestas. La burbuja sale al instante; las respuestas de
|
||||
flujos simultáneos se encolan para no intercalarse. Cancelable. */
|
||||
WAM.flujo = function (texto, respuestas, cita) {
|
||||
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;
|
||||
var ctx = contexto("flujo");
|
||||
var out = mensajeOut(texto, cita || null);
|
||||
anadirSesion(out);
|
||||
var tics = animarTics(out, ctx);
|
||||
tics.catch(nada);
|
||||
var turno = colas.flujo.then(function () {
|
||||
if (!ctx.vivo()) throw CANCELADO;
|
||||
return tics.then(function () { return responder(lista, ctx, false); });
|
||||
});
|
||||
colas.flujo = turno.catch(nada);
|
||||
return turno.catch(nada);
|
||||
};
|
||||
|
||||
/* Envío libre del composer: burbuja OUT con hora actual y ticks automáticos. */
|
||||
@@ -157,69 +430,213 @@ window.WAM = window.WAM || {};
|
||||
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 */ });
|
||||
anadirSesion(out);
|
||||
animarTics(out, contexto("flujo")).catch(nada);
|
||||
};
|
||||
|
||||
/* 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). */
|
||||
/* Resuelve { texto, respuestas, cita, msg, tipo } desde los data-* que
|
||||
estampa render.js: data-btn (template/buttons), data-sec + data-fila
|
||||
(lista) o data-card (carousel). Etiquetas vacías = las del render. */
|
||||
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 vacioR = { texto: "", respuestas: [], cita: null, msg: null, tipo: "" };
|
||||
if (!el || typeof el.getAttribute !== "function") return vacioR;
|
||||
var msg = WAM.mensajeDeElemento(el);
|
||||
if (!msg) return vacioR;
|
||||
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 (!fila) return vacioR;
|
||||
var titulo = vacio(fila.titulo) ? "Opción" : String(fila.titulo);
|
||||
return {
|
||||
texto: titulo + (vacio(fila.descripcion) ? "" : "\n" + fila.descripcion),
|
||||
respuestas: fila.respuestas || [],
|
||||
cita: citaDe(msg),
|
||||
msg: msg,
|
||||
tipo: "fila"
|
||||
};
|
||||
}
|
||||
if (ds.btn !== undefined) {
|
||||
var boton = (msg.botones || [])[parseInt(ds.btn, 10)];
|
||||
return boton ? { texto: boton.texto || "", respuestas: boton.respuestas || [] } : vacio;
|
||||
var n = parseInt(ds.btn, 10);
|
||||
var boton = (msg.botones || [])[n];
|
||||
if (!boton) return vacioR;
|
||||
return {
|
||||
texto: vacio(boton.texto) ? "Botón " + (n + 1) : String(boton.texto),
|
||||
respuestas: boton.respuestas || [],
|
||||
cita: citaDe(msg),
|
||||
msg: msg,
|
||||
tipo: "boton"
|
||||
};
|
||||
}
|
||||
if (ds.card !== undefined) {
|
||||
var card = (msg.cards || [])[parseInt(ds.card, 10)];
|
||||
return card ? { texto: card.textoBoton || "", respuestas: card.respuestas || [] } : vacio;
|
||||
var c = parseInt(ds.card, 10);
|
||||
var card = (msg.cards || [])[c];
|
||||
if (!card) return vacioR;
|
||||
return {
|
||||
texto: vacio(card.textoBoton) ? "Ver más" : String(card.textoBoton),
|
||||
respuestas: card.respuestas || [],
|
||||
cita: citaDe(msg, { card: c }),
|
||||
msg: msg,
|
||||
tipo: "card"
|
||||
};
|
||||
}
|
||||
/* 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;
|
||||
/* Elemento plano de un mensaje list: es el botón que abre la hoja. */
|
||||
return vacioR;
|
||||
};
|
||||
|
||||
/* Deja el botón pulsado en gris y sin respuesta a más clics (como iOS). */
|
||||
WAM.marcarUsado = function (el) {
|
||||
if (!el || !el.classList) return;
|
||||
el.classList.add("usado");
|
||||
el.setAttribute("aria-disabled", "true");
|
||||
el.setAttribute("tabindex", "-1");
|
||||
};
|
||||
|
||||
/* Guarda la sesión actual como parte del guion (acción explícita). */
|
||||
WAM.sesionAGuion = function () {
|
||||
if (!WAM.sesion.length || !WAM.estado) return 0;
|
||||
var tomar = typeof WAM.crearAlocador === "function" ? WAM.crearAlocador() : WAM.generarId;
|
||||
var nuevos = WAM.sesion.map(function (m) {
|
||||
var copia = JSON.parse(JSON.stringify(m));
|
||||
delete copia.cita;
|
||||
(function reasignar(o) {
|
||||
if (!o || typeof o !== "object") return;
|
||||
if (Array.isArray(o)) { o.forEach(reasignar); return; }
|
||||
if (typeof o.id === "string") o.id = tomar();
|
||||
["respuestas", "botones", "secciones", "filas", "cards"].forEach(function (k) { reasignar(o[k]); });
|
||||
})(copia);
|
||||
return copia;
|
||||
});
|
||||
var n = nuevos.length;
|
||||
WAM.estado = WAM.normalizar ? WAM.normalizar({
|
||||
version: WAM.estado.version, tema: WAM.estado.tema, cuenta: WAM.estado.cuenta,
|
||||
mensajes: WAM.estado.mensajes.concat(nuevos)
|
||||
}) : WAM.estado;
|
||||
if (typeof WAM.guardar === "function") WAM.guardar();
|
||||
if (typeof WAM.render === "function") WAM.render();
|
||||
if (typeof WAM.renderEditor === "function") WAM.renderEditor();
|
||||
return n;
|
||||
};
|
||||
|
||||
/* ————————————————— reproducción de la demo ————————————————— */
|
||||
|
||||
function pulsacion(el, ctx) {
|
||||
el.classList.add("wam-pulsando");
|
||||
return ctx.esperar(260).then(function () { el.classList.remove("wam-pulsando"); },
|
||||
function (e) { el.classList.remove("wam-pulsando"); throw e; });
|
||||
}
|
||||
|
||||
function centrarCard(el) {
|
||||
var car = el.closest(".carousel");
|
||||
var card = el.closest(".card");
|
||||
if (!car || !card) return;
|
||||
var delta = card.getBoundingClientRect().left - car.getBoundingClientRect().left;
|
||||
try { car.scrollTo({ left: car.scrollLeft + delta - 8, behavior: "smooth" }); }
|
||||
catch (e) { car.scrollLeft += delta - 8; }
|
||||
}
|
||||
|
||||
/* Pulsa sola la opción msg.autoPulsar (índice de botón, card o fila
|
||||
aplanada de la lista) 1 s después de que aparezca el mensaje. */
|
||||
async function autoPulsar(msg, ctx) {
|
||||
if (!msg || ["template", "buttons", "list", "carousel"].indexOf(msg.tipo) === -1) return;
|
||||
var idx = msg.autoPulsar;
|
||||
if (idx === null || idx === undefined || idx === "") return;
|
||||
idx = Number(idx);
|
||||
if (!isFinite(idx) || idx < 0) return;
|
||||
var fila = filaDe(msg);
|
||||
if (!fila) return;
|
||||
await ctx.esperar(1000);
|
||||
var el = null;
|
||||
|
||||
if (msg.tipo === "list") {
|
||||
var s, f, cuenta = 0, destino = null;
|
||||
var secs = msg.secciones || [];
|
||||
for (s = 0; s < secs.length && !destino; s++) {
|
||||
var filas = (secs[s] && secs[s].filas) || [];
|
||||
for (f = 0; f < filas.length; f++) {
|
||||
if (cuenta === idx) { destino = [s, f]; break; }
|
||||
cuenta++;
|
||||
}
|
||||
}
|
||||
if (!destino) return;
|
||||
var abridor = fila.querySelector("[data-flujo]:not([data-btn]):not([data-card])");
|
||||
if (abridor) await pulsacion(abridor, ctx);
|
||||
WAM.abrirLista(msg);
|
||||
await ctx.esperar(900);
|
||||
var hoja = document.getElementById("sheet");
|
||||
el = hoja ? hoja.querySelector('[data-sec="' + destino[0] + '"][data-fila="' + destino[1] + '"]') : null;
|
||||
if (!el) { if (WAM.cerrarSheet) WAM.cerrarSheet(); return; }
|
||||
await pulsacion(el, ctx);
|
||||
var selL = WAM.resolverFlujo(el);
|
||||
if (WAM.cerrarSheet) WAM.cerrarSheet();
|
||||
await flujoEnDemo(selL, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
el = fila.querySelector(msg.tipo === "carousel" ? '[data-card="' + idx + '"]' : '[data-btn="' + idx + '"]');
|
||||
if (!el) return;
|
||||
if (el.hidden) {
|
||||
var plegado = fila.querySelector("[data-ver-todas]");
|
||||
if (plegado) { plegado.click(); await ctx.esperar(400); }
|
||||
/* B abre la hoja "Todas las opciones" (render.js): se pulsa su fila */
|
||||
var enHoja = document.querySelector('#sheet [data-opcion="' + idx + '"]');
|
||||
if (!enHoja) { if (WAM.cerrarSheet) WAM.cerrarSheet(); return; }
|
||||
await pulsacion(enHoja, ctx);
|
||||
if (WAM.cerrarSheet) WAM.cerrarSheet();
|
||||
}
|
||||
if (msg.tipo === "carousel") { centrarCard(el); await ctx.esperar(500); }
|
||||
await pulsacion(el, ctx);
|
||||
var sel = WAM.resolverFlujo(el);
|
||||
WAM.marcarUsado(el);
|
||||
await flujoEnDemo(sel, ctx);
|
||||
}
|
||||
|
||||
async function flujoEnDemo(sel, ctx) {
|
||||
if (!sel || !sel.msg) return;
|
||||
var out = mensajeOut(sel.texto, sel.cita);
|
||||
anadirSesion(out);
|
||||
await animarTics(out, ctx);
|
||||
await responder(Array.isArray(sel.respuestas) ? sel.respuestas : [], ctx, true);
|
||||
}
|
||||
|
||||
function vaciarChat() {
|
||||
var cuerpo = cuerpoChat();
|
||||
if (!cuerpo) return;
|
||||
var filas = cuerpo.querySelectorAll(".wa-row");
|
||||
for (var i = 0; i < filas.length; i++) {
|
||||
if (filas[i].id !== "chat-escribiendo") cuerpo.removeChild(filas[i]);
|
||||
}
|
||||
}
|
||||
|
||||
WAM.reproducirDemo = function () {
|
||||
if (demoAndando) return;
|
||||
/* aborta flujos, ticks y typing en curso: no se mezclan con la demo */
|
||||
cancelarTodo();
|
||||
if (typeof WAM.cerrarSheet === "function") WAM.cerrarSheet();
|
||||
hojaMsg = null;
|
||||
WAM.sesion.length = 0;
|
||||
demoAndando = true;
|
||||
avisarDemo();
|
||||
var cuerpo = document.getElementById("chat-cuerpo");
|
||||
if (cuerpo) cuerpo.innerHTML = "";
|
||||
vaciarChat();
|
||||
var ctx = contexto("demo");
|
||||
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);
|
||||
if (msg.dir === "in") await escribirDurante(ctx, 700);
|
||||
if (!ctx.vivo()) throw CANCELADO;
|
||||
pintar(msg);
|
||||
await ctx.esperar(600);
|
||||
await autoPulsar(msg, ctx);
|
||||
}
|
||||
demoAndando = false;
|
||||
escribiendo(false);
|
||||
avisarDemo();
|
||||
} catch (e) {
|
||||
/* cancelado: detenerDemo() / render() ya dejaron el chat coherente */
|
||||
}
|
||||
if (ctx.vivo()) {
|
||||
demoAndando = false;
|
||||
escribiendo(false);
|
||||
avisarDemo();
|
||||
}
|
||||
})();
|
||||
@@ -228,9 +645,38 @@ window.WAM = window.WAM || {};
|
||||
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();
|
||||
cancelarGrupo("demo"); /* solo el grupo de la demo */
|
||||
if (estaba) {
|
||||
/* vuelve al guion completo (y descarta la sesión: ver reiniciarInteraccion) */
|
||||
if (typeof WAM.render === "function") WAM.render();
|
||||
avisarDemo();
|
||||
}
|
||||
};
|
||||
|
||||
/* Vuelve al guion: cancela todo, vacía la sesión y cierra la hoja. */
|
||||
function reiniciarInteraccion() {
|
||||
var estaba = demoAndando;
|
||||
demoAndando = false;
|
||||
cancelarTodo();
|
||||
WAM.sesion.length = 0;
|
||||
hojaMsg = null;
|
||||
if (typeof WAM.cerrarSheet === "function") WAM.cerrarSheet();
|
||||
if (estaba) avisarDemo();
|
||||
}
|
||||
|
||||
WAM.reiniciarChat = function () {
|
||||
if (typeof WAM.render === "function") WAM.render();
|
||||
};
|
||||
|
||||
/* WAM.render() repinta el guion entero: antes se descarta la sesión y se
|
||||
cancelan demo y flujos para que nada añada filas sobre el repintado
|
||||
(evita data-id duplicados y la hoja con índices obsoletos). */
|
||||
if (typeof WAM.render === "function" && !WAM.render.__wamEnvuelto) {
|
||||
var renderBase = WAM.render;
|
||||
WAM.render = function () {
|
||||
reiniciarInteraccion();
|
||||
return renderBase.apply(this, arguments);
|
||||
};
|
||||
WAM.render.__wamEnvuelto = true;
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user