1759 lines
74 KiB
JavaScript
1759 lines
74 KiB
JavaScript
/* =====================================================================
|
||
js/editor.js — Dueño C (SPEC §2/§4): panel editor.
|
||
Formulario de cuenta, alta por paleta de tarjetas de tipo (estilo Meta),
|
||
tarjetas con TODOS los campos del modelo (§1), contadores de caracteres,
|
||
barra de formato WhatsApp, variables de plantilla, toggle de cabecera,
|
||
origen de media por radios + dropzone (clic o arrastrar-soltar),
|
||
reordenar por drag & drop (mensajes y elementos internos),
|
||
duplicar/borrar, exportar/importar JSON.
|
||
Listeners SOLO dentro de #panel-editor e inputs propios.
|
||
Contratos usados de otros dueños: WAM.estado/guardar/normalizar/
|
||
generarId/nuevoMensaje/horaAhora/svgPlaceholder (store, D),
|
||
WAM.render/setTema (render, B). Toolbar (#btn-exportar, #btn-importar,
|
||
#btn-reset, #btn-tema, #importar-archivo) la enlaza main.js (D).
|
||
===================================================================== */
|
||
(function () {
|
||
"use strict";
|
||
window.WAM = window.WAM || {};
|
||
|
||
/* ------------------------------------------------------------------
|
||
Constantes del editor
|
||
------------------------------------------------------------------ */
|
||
|
||
// Los 13 tipos del modelo (SPEC §1)
|
||
var TIPOS = [
|
||
["text", "Texto"],
|
||
["image", "Imagen"],
|
||
["video", "Vídeo"],
|
||
["audio", "Audio"],
|
||
["document", "Documento"],
|
||
["sticker", "Sticker"],
|
||
["location", "Ubicación"],
|
||
["contact", "Contacto"],
|
||
["template", "Plantilla"],
|
||
["buttons", "Botones rápidos"],
|
||
["list", "Lista"],
|
||
["cta_url", "Botón URL"],
|
||
["carousel", "Carrusel"]
|
||
];
|
||
|
||
// Tipos de cabecera de template/buttons (SPEC §1.9/§1.10)
|
||
var TIPOS_HEADER = [
|
||
["texto", "Texto"],
|
||
["imagen", "Imagen"],
|
||
["video", "Vídeo"],
|
||
["documento", "Documento"]
|
||
];
|
||
|
||
var ESTADOS = [
|
||
["sent", "Enviado ✓"],
|
||
["delivered", "Entregado ✓✓"],
|
||
["read", "Leído ✓✓"]
|
||
];
|
||
|
||
// Límites de docs/research-api.md (§8) que aplica el editor
|
||
var LIMITES = {
|
||
botonesRapidos: 3, // quick replies por mensaje (SPEC §1.10)
|
||
botonesPlantilla: 10, // botones totales por plantilla [20]
|
||
secciones: 10,
|
||
filas: 10,
|
||
tarjetasMax: 5, // carousel 2–5 cards (SPEC §1.13)
|
||
tarjetasMin: 2
|
||
};
|
||
|
||
// Iconos SVG (trazo) de la paleta de tipos, viewBox 0 0 24 24
|
||
var ICONOS_TIPO = {
|
||
text: '<path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H10l-4.3 3.6c-.5.4-1.2.1-1.2-.6z"/><path d="M8 9h8M8 12.2h5"/>',
|
||
image: '<rect x="3.5" y="5" width="17" height="14" rx="2"/><circle cx="9" cy="10" r="1.7"/><path d="M3.8 16.8l4.4-3.9 3.4 3 3.6-3.6 5 4.4"/>',
|
||
video: '<rect x="3" y="6" width="12.5" height="12" rx="2"/><path d="M15.5 10.6l5.5-3.1v9l-5.5-3.1z"/>',
|
||
audio: '<path d="M4 10.2v3.6M8 7.2v9.6M12 4.4v15.2M16 7.2v9.6M20 10.2v3.6"/>',
|
||
document: '<path d="M7 3.5h6.8L18.5 8v11a1.5 1.5 0 0 1-1.5 1.5H7A1.5 1.5 0 0 1 5.5 19V5A1.5 1.5 0 0 1 7 3.5z"/><path d="M13.5 3.8V8h4.6M9 12.2h6M9 15.4h6"/>',
|
||
sticker: '<path d="M5.5 4h13v9.3L14 18H5.5z"/><path d="M18.2 13.5h-4v4.3"/><path d="M9.2 10.4h.01M14.8 10.4h.01"/>',
|
||
location: '<path d="M12 20.6S5.6 15 5.6 10.4a6.4 6.4 0 0 1 12.8 0C18.4 15 12 20.6 12 20.6z"/><circle cx="12" cy="10.4" r="2.2"/>',
|
||
contact: '<rect x="3.5" y="5" width="17" height="14" rx="2"/><circle cx="9.4" cy="10.4" r="1.9"/><path d="M6.4 15.6c.8-1.6 1.9-2.1 3-2.1s2.2.5 3 2.1M15.3 9.6h2.5M15.3 13h2.5"/>',
|
||
template: '<rect x="3.5" y="4" width="17" height="16" rx="2"/><path d="M3.5 8.4h17M7 12.4h10M7 15.6h6.5"/>',
|
||
buttons: '<rect x="4" y="4.4" width="16" height="4.3" rx="2.15"/><rect x="4" y="9.9" width="16" height="4.3" rx="2.15"/><rect x="4" y="15.4" width="11.5" height="4.3" rx="2.15"/>',
|
||
list: '<path d="M8.5 6h11.5M8.5 12h11.5M8.5 18h11.5"/><circle cx="4.6" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="4.6" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="4.6" cy="18" r="1" fill="currentColor" stroke="none"/>',
|
||
cta_url: '<path d="M10.2 13.8l3.6-3.6"/><path d="M9.3 11.9l-2.1 2.1a3.1 3.1 0 0 0 4.4 4.4l2.1-2.1"/><path d="M14.7 12.1l2.1-2.1a3.1 3.1 0 0 0-4.4-4.4l-2.1 2.1"/>',
|
||
carousel: '<rect x="3" y="6" width="12.6" height="12" rx="1.8"/><path d="M18.6 8.1c1 .3 1.6 1.3 1.4 2.3l-1.7 7.6a1.8 1.8 0 0 1-2.1 1.4l-2-.4"/>'
|
||
};
|
||
|
||
var GRIP = '<circle cx="9" cy="5.5" r="1.15" fill="currentColor" stroke="none"/><circle cx="15" cy="5.5" r="1.15" fill="currentColor" stroke="none"/><circle cx="9" cy="12" r="1.15" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1.15" fill="currentColor" stroke="none"/><circle cx="9" cy="18.5" r="1.15" fill="currentColor" stroke="none"/><circle cx="15" cy="18.5" r="1.15" fill="currentColor" stroke="none"/>';
|
||
|
||
var ICONO_SUBIR = '<path d="M12 15.2V4.8M8.3 8.2 12 4.5l3.7 3.7"/><path d="M4.8 14.4v3a2 2 0 0 0 2 2h10.4a2 2 0 0 0 2-2v-3"/>';
|
||
|
||
// Formato WhatsApp de la barra (marcador, clase, descripción)
|
||
var FORMATOS = [
|
||
["*", "negrita", "Negrita · *texto*"],
|
||
["_", "cursiva", "Cursiva · _texto_"],
|
||
["~", "tachado", "Tachado · ~texto~"],
|
||
["`", "mono", "Monoespaciado · `texto`"]
|
||
];
|
||
|
||
/* ------------------------------------------------------------------
|
||
Estado interno de UI (qué tarjetas están abiertas, nodos cacheados)
|
||
------------------------------------------------------------------ */
|
||
var abiertas = Object.create(null);
|
||
var panel = null;
|
||
var avisoEl = null;
|
||
var avisoTimer = 0;
|
||
var panelEnlazado = false;
|
||
var obsTema = null;
|
||
|
||
// Drag & drop activo (reordenar mensajes y elementos internos)
|
||
var dragItem = null;
|
||
var dropObjetivo = null;
|
||
var dropTras = false;
|
||
var zonaArchivo = null;
|
||
|
||
/* ------------------------------------------------------------------
|
||
Utilidades
|
||
------------------------------------------------------------------ */
|
||
|
||
function predeterminado(v, d) { return (v === undefined || v === null) ? d : v; }
|
||
|
||
// Constructor de nodos: attrs especiales class/text/value/checked/disabled
|
||
function h(etiqueta, attrs, hijos) {
|
||
var el = document.createElement(etiqueta);
|
||
if (attrs) {
|
||
Object.keys(attrs).forEach(function (k) {
|
||
var v = attrs[k];
|
||
if (v === null || v === undefined || v === false) return;
|
||
if (k === "class") el.className = v;
|
||
else if (k === "text") el.textContent = v;
|
||
else if (k === "value") el.value = v;
|
||
else if (k === "checked") el.checked = !!v;
|
||
else if (k === "disabled") el.disabled = true;
|
||
else el.setAttribute(k, v === true ? "" : v);
|
||
});
|
||
}
|
||
// hijos: array de nodos, un solo nodo (incluido DocumentFragment) o nada
|
||
var listaHijos = (hijos === null || hijos === undefined) ? [] : (Array.isArray(hijos) ? hijos : [hijos]);
|
||
listaHijos.forEach(function (x) { if (x !== null && x !== undefined) el.appendChild(x); });
|
||
return el;
|
||
}
|
||
|
||
// Icono SVG inline (marcado propio y estático; nunca recibe input del usuario)
|
||
function svgIcono(marcado) {
|
||
var span = document.createElement("span");
|
||
span.className = "ed-ico";
|
||
span.setAttribute("aria-hidden", "true");
|
||
span.innerHTML =
|
||
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor"' +
|
||
' stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + (marcado || "") + "</svg>";
|
||
return span;
|
||
}
|
||
|
||
function generarIdLocal() {
|
||
if (typeof WAM.generarId === "function") { try { return WAM.generarId(); } catch (e) { /* sigue */ } }
|
||
return "m" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||
}
|
||
|
||
function placeholderSvg(texto) {
|
||
try {
|
||
if (typeof WAM.svgPlaceholder === "function") return WAM.svgPlaceholder(texto || "");
|
||
} catch (e) { /* sin placeholder */ }
|
||
return "";
|
||
}
|
||
|
||
function rutaGet(ruta) {
|
||
var partes = String(ruta).split(".");
|
||
return partes.reduce(function (o, k) { return (o === null || o === undefined) ? undefined : o[k]; }, WAM.estado);
|
||
}
|
||
|
||
function etiquetaTipo(tipo) {
|
||
for (var i = 0; i < TIPOS.length; i++) if (TIPOS[i][0] === tipo) return TIPOS[i][1];
|
||
return String(tipo || "?");
|
||
}
|
||
|
||
function esTipoValido(tipo) {
|
||
for (var i = 0; i < TIPOS.length; i++) if (TIPOS[i][0] === tipo) return true;
|
||
return false;
|
||
}
|
||
|
||
function aviso(texto, tipo) {
|
||
if (!panel) return;
|
||
if (!avisoEl || !avisoEl.isConnected) {
|
||
avisoEl = h("p", { id: "ed-aviso", class: "ed-aviso", role: "status" });
|
||
panel.insertBefore(avisoEl, panel.firstChild);
|
||
}
|
||
avisoEl.textContent = texto;
|
||
avisoEl.className = "ed-aviso ed-aviso-visible" + (tipo === "error" ? " ed-aviso-error" : "");
|
||
clearTimeout(avisoTimer);
|
||
avisoTimer = setTimeout(function () { avisoEl.className = "ed-aviso"; }, 3200);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Contadores de caracteres (límites oficiales, sin bloquear escritura)
|
||
------------------------------------------------------------------ */
|
||
|
||
function claseContador(n, max) {
|
||
if (n > max) return "ed-contador-over";
|
||
if (n >= Math.max(1, Math.round(max * 0.9))) return "ed-contador-alerta";
|
||
return "";
|
||
}
|
||
|
||
function edContador(valor, maximo) {
|
||
var n = String(predeterminado(valor, "")).length;
|
||
var clase = "ed-contador" + (claseContador(n, maximo) ? " " + claseContador(n, maximo) : "");
|
||
return h("span", { class: clase, text: n + " / " + maximo, "data-max": maximo });
|
||
}
|
||
|
||
// Refresca el contador del input/textarea que acaba de cambiar
|
||
function actualizarContador(input) {
|
||
if (!input || !input.closest) return;
|
||
var campo = input.closest(".ed-campo");
|
||
var cont = campo ? campo.querySelector(".ed-contador") : null;
|
||
if (!cont) return;
|
||
var max = Number(cont.getAttribute("data-max")) || 0;
|
||
if (!max) return;
|
||
var n = String(predeterminado(input.value, "")).length;
|
||
cont.textContent = n + " / " + max;
|
||
cont.classList.remove("ed-contador-alerta");
|
||
cont.classList.remove("ed-contador-over");
|
||
var extra = claseContador(n, max);
|
||
if (extra) cont.classList.add(extra);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Edición en cursor: barra de formato WhatsApp y variables {{n}}
|
||
------------------------------------------------------------------ */
|
||
|
||
// Cambios programáticos del valor no disparan "input": lo lanzamos burbujeando
|
||
// para reutilizar la delegación (setCampo + contador).
|
||
function dispararInput(el) {
|
||
if (!el) return;
|
||
if (typeof Event === "function") {
|
||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
} else {
|
||
var ruta = el.getAttribute ? el.getAttribute("data-ruta") : null;
|
||
if (ruta) WAM.setCampo(ruta, el.value);
|
||
actualizarContador(el);
|
||
}
|
||
}
|
||
|
||
function posSeleccion(input) {
|
||
var ini = (typeof input.selectionStart === "number") ? input.selectionStart : input.value.length;
|
||
var fin = (typeof input.selectionEnd === "number") ? input.selectionEnd : ini;
|
||
return [ini, fin];
|
||
}
|
||
|
||
// Envuelve la selección con el marcador (*, _, ~, `) y recoloca el cursor
|
||
function envolverSeleccion(input, marcador) {
|
||
if (!input || !marcador) return;
|
||
var pos = posSeleccion(input);
|
||
var ini = pos[0], fin = pos[1];
|
||
var sel = input.value.slice(ini, fin);
|
||
var nuevo = marcador + sel + marcador;
|
||
input.value = input.value.slice(0, ini) + nuevo + input.value.slice(fin);
|
||
input.focus();
|
||
var a = ini + marcador.length;
|
||
input.setSelectionRange(a, sel.length ? a + sel.length : a);
|
||
dispararInput(input);
|
||
}
|
||
|
||
// Inserta texto en la posición del cursor (variables {{n}})
|
||
function insertarTexto(input, texto) {
|
||
if (!input || !texto) return;
|
||
var pos = posSeleccion(input);
|
||
var ini = pos[0], fin = pos[1];
|
||
input.value = input.value.slice(0, ini) + texto + input.value.slice(fin);
|
||
input.focus();
|
||
var p = ini + texto.length;
|
||
input.setSelectionRange(p, p);
|
||
dispararInput(input);
|
||
}
|
||
|
||
// Siguiente variable libre: cuenta las {{n}} ya presentes y devuelve max + 1
|
||
function siguienteVariable(input) {
|
||
var re = /\{\{\s*(\d+)\s*\}\}/g;
|
||
var m, max = 0;
|
||
while ((m = re.exec(String(input.value || ""))) !== null) {
|
||
var n = parseInt(m[1], 10);
|
||
if (n > max) max = n;
|
||
}
|
||
return max + 1;
|
||
}
|
||
|
||
// El input/textarea asociado a un botón de la barra (mismo .ed-campo)
|
||
function inputDeCampo(el) {
|
||
var campo = el && el.closest ? el.closest(".ed-campo") : null;
|
||
return campo ? campo.querySelector("textarea, input.ed-input") : null;
|
||
}
|
||
|
||
function edBarra(opts) {
|
||
var bar = h("div", { class: "ed-fmtbar", role: "toolbar", "aria-label": "Formato de WhatsApp" });
|
||
if (opts.formato) {
|
||
FORMATOS.forEach(function (f) {
|
||
var txt = f[0] === "*" ? "B" : (f[0] === "_" ? "I" : (f[0] === "~" ? "S" : "</>"));
|
||
bar.appendChild(h("button", {
|
||
type: "button",
|
||
class: "ed-fmt-btn ed-fmt-" + f[1],
|
||
text: txt,
|
||
"data-acc": "fmt",
|
||
"data-fmt": f[0],
|
||
title: f[2],
|
||
"aria-label": f[2]
|
||
}));
|
||
});
|
||
}
|
||
if (opts.variables) {
|
||
bar.appendChild(h("button", {
|
||
type: "button",
|
||
class: "ed-fmt-btn ed-fmt-var",
|
||
text: "+ Añadir variable",
|
||
"data-acc": "var",
|
||
title: "Inserta {{1}}, {{2}}… en la posición del cursor (cuenta las existentes)",
|
||
"aria-label": "Añadir variable"
|
||
}));
|
||
}
|
||
return bar;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Contrato: WAM.setCampo(ruta, valor) — muta + guardar + render (SPEC §2)
|
||
ruta con puntos: "mensajes.3.body", "cuenta.nombre", "mensajes.0.botones"
|
||
------------------------------------------------------------------ */
|
||
WAM.setCampo = function (ruta, valor) {
|
||
if (!ruta || !WAM.estado) return;
|
||
var partes = String(ruta).split(".");
|
||
var obj = WAM.estado;
|
||
for (var i = 0; i < partes.length - 1; i++) {
|
||
var k = partes[i];
|
||
if (obj[k] === null || obj[k] === undefined) obj[k] = /^\d+$/.test(partes[i + 1]) ? [] : {};
|
||
obj = obj[k];
|
||
}
|
||
obj[partes[partes.length - 1]] = valor;
|
||
if (typeof WAM.guardar === "function") WAM.guardar();
|
||
if (typeof WAM.render === "function") WAM.render();
|
||
};
|
||
|
||
// Reemplaza un array completo vía setCampo (mutación contractual) y repinta tarjetas
|
||
function mutarArray(ruta, fn) {
|
||
var arr = rutaGet(ruta);
|
||
if (!Array.isArray(arr)) { aviso("Estructura no encontrada: " + ruta, "error"); return; }
|
||
var copia = fn(arr.slice());
|
||
if (!copia) return; // validación rechazada (fn ya avisó)
|
||
WAM.setCampo(ruta, copia);
|
||
WAM.renderTarjetas();
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Fábricas de datos (usadas solo al CREAR; no mutan lo importado)
|
||
------------------------------------------------------------------ */
|
||
|
||
function aplicarCamposTipo(m) {
|
||
switch (m.tipo) {
|
||
case "text": m.texto = predeterminado(m.texto, ""); break;
|
||
case "image":
|
||
case "video": m.src = predeterminado(m.src, ""); m.caption = predeterminado(m.caption, ""); break;
|
||
case "audio": m.src = predeterminado(m.src, ""); m.voz = !!m.voz; m.duracion = predeterminado(m.duracion, "0:12"); break;
|
||
case "document": m.src = predeterminado(m.src, ""); m.caption = predeterminado(m.caption, ""); m.filename = predeterminado(m.filename, "documento.pdf"); break;
|
||
case "sticker": m.src = predeterminado(m.src, ""); break;
|
||
case "location":
|
||
m.latitud = predeterminado(m.latitud, ""); m.longitud = predeterminado(m.longitud, "");
|
||
m.nombre = predeterminado(m.nombre, ""); m.direccion = predeterminado(m.direccion, ""); break;
|
||
case "contact":
|
||
m.nombre = predeterminado(m.nombre, ""); m.telefono = predeterminado(m.telefono, "");
|
||
m.email = predeterminado(m.email, ""); m.empresa = predeterminado(m.empresa, ""); break;
|
||
case "template":
|
||
m.headerTipo = predeterminado(m.headerTipo, ""); m.headerTexto = predeterminado(m.headerTexto, "");
|
||
m.headerMedia = predeterminado(m.headerMedia, ""); m.body = predeterminado(m.body, "");
|
||
m.footer = predeterminado(m.footer, "");
|
||
if (!Array.isArray(m.botones)) m.botones = [];
|
||
m.botones.forEach(function (b) {
|
||
b.texto = predeterminado(b.texto, "");
|
||
if (!Array.isArray(b.respuestas)) b.respuestas = [];
|
||
});
|
||
break;
|
||
case "buttons":
|
||
m.headerTipo = predeterminado(m.headerTipo, ""); m.headerTexto = predeterminado(m.headerTexto, "");
|
||
m.headerMedia = predeterminado(m.headerMedia, "");
|
||
m.body = predeterminado(m.body, ""); m.footer = predeterminado(m.footer, "");
|
||
if (!Array.isArray(m.botones)) m.botones = [];
|
||
m.botones.forEach(function (b) {
|
||
b.texto = predeterminado(b.texto, "");
|
||
if (!Array.isArray(b.respuestas)) b.respuestas = [];
|
||
});
|
||
break;
|
||
case "list":
|
||
m.header = predeterminado(m.header, ""); m.body = predeterminado(m.body, "");
|
||
m.footer = predeterminado(m.footer, ""); m.boton = predeterminado(m.boton, "Ver opciones");
|
||
if (!Array.isArray(m.secciones)) m.secciones = [];
|
||
m.secciones.forEach(function (s) {
|
||
s.titulo = predeterminado(s.titulo, "");
|
||
if (!Array.isArray(s.filas)) s.filas = [];
|
||
s.filas.forEach(function (f) {
|
||
f.titulo = predeterminado(f.titulo, ""); f.descripcion = predeterminado(f.descripcion, "");
|
||
if (!Array.isArray(f.respuestas)) f.respuestas = [];
|
||
});
|
||
});
|
||
break;
|
||
case "cta_url":
|
||
m.headerMedia = predeterminado(m.headerMedia, ""); m.body = predeterminado(m.body, "");
|
||
m.footer = predeterminado(m.footer, ""); m.textoBoton = predeterminado(m.textoBoton, "Abrir");
|
||
m.url = predeterminado(m.url, "");
|
||
break;
|
||
case "carousel":
|
||
m.body = predeterminado(m.body, "");
|
||
if (!Array.isArray(m.cards)) m.cards = [];
|
||
m.cards.forEach(function (c) {
|
||
c.media = predeterminado(c.media, ""); c.texto = predeterminado(c.texto, "");
|
||
c.textoBoton = predeterminado(c.textoBoton, "Abrir"); c.url = predeterminado(c.url, "");
|
||
if (!Array.isArray(c.respuestas)) c.respuestas = [];
|
||
});
|
||
break;
|
||
default: break;
|
||
}
|
||
return m;
|
||
}
|
||
|
||
function crearMensaje(tipo) {
|
||
var base = null;
|
||
if (typeof WAM.nuevoMensaje === "function") {
|
||
try { base = WAM.nuevoMensaje(tipo); } catch (e) { base = null; }
|
||
}
|
||
var m = (base && typeof base === "object") ? base : { id: "", dir: "in", tipo: tipo, hora: "10:24", estado: "sent" };
|
||
m.tipo = tipo;
|
||
if (!m.id) m.id = generarIdLocal();
|
||
if (m.dir !== "in" && m.dir !== "out") m.dir = "in";
|
||
if (!/^\d{1,2}:\d{2}$/.test(m.hora || "")) {
|
||
m.hora = (typeof WAM.horaAhora === "function") ? WAM.horaAhora() : "10:24";
|
||
}
|
||
if (ESTADOS.every(function (e) { return e[0] !== m.estado; })) m.estado = "sent";
|
||
aplicarCamposTipo(m);
|
||
// Arranques amables para tipos compuestos (solo mensajes nuevos)
|
||
if (m.tipo === "list" && m.secciones.length === 0) m.secciones.push(nuevaSeccion());
|
||
if (m.tipo === "carousel" && m.cards.length < LIMITES.tarjetasMin) {
|
||
while (m.cards.length < LIMITES.tarjetasMin) m.cards.push(nuevaCard());
|
||
}
|
||
if ((m.tipo === "buttons" || m.tipo === "template") && m.botones.length === 0) {
|
||
m.botones.push(nuevoBoton(m.tipo === "buttons" ? "Opción 1" : "Responder"));
|
||
}
|
||
return m;
|
||
}
|
||
|
||
function mensajeAnidado() {
|
||
var m = crearMensaje("text");
|
||
m.dir = "in"; // las respuestas SIEMPRE son entrantes (SPEC §1)
|
||
return m;
|
||
}
|
||
|
||
function nuevoBoton(texto, conUrl) {
|
||
var b = { texto: texto || "" };
|
||
if (conUrl) b.url = "";
|
||
b.respuestas = [];
|
||
return b;
|
||
}
|
||
|
||
function nuevaFila() { return { titulo: "", descripcion: "", respuestas: [] }; }
|
||
function nuevaSeccion() { return { titulo: "", filas: [nuevaFila()] }; }
|
||
function nuevaCard() { return { media: "", texto: "", textoBoton: "Abrir", url: "", respuestas: [] }; }
|
||
|
||
// Regenera ids del clon y de todas sus respuestas anidadas (solo nodos Mensaje)
|
||
function reasignarIds(m) {
|
||
var paseMensaje = function (x) {
|
||
if (!x || typeof x !== "object") return;
|
||
x.id = generarIdLocal();
|
||
if (Array.isArray(x.respuestas)) x.respuestas.forEach(paseMensaje);
|
||
if (Array.isArray(x.botones)) x.botones.forEach(function (b) {
|
||
if (Array.isArray(b.respuestas)) b.respuestas.forEach(paseMensaje);
|
||
});
|
||
if (Array.isArray(x.secciones)) x.secciones.forEach(function (s) {
|
||
if (!Array.isArray(s.filas)) return;
|
||
s.filas.forEach(function (f) { if (Array.isArray(f.respuestas)) f.respuestas.forEach(paseMensaje); });
|
||
});
|
||
if (Array.isArray(x.cards)) x.cards.forEach(function (c) {
|
||
if (Array.isArray(c.respuestas)) c.respuestas.forEach(paseMensaje);
|
||
});
|
||
};
|
||
paseMensaje(m);
|
||
return m;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Acciones de contrato sobre mensajes
|
||
------------------------------------------------------------------ */
|
||
|
||
function terminarCambios() {
|
||
if (typeof WAM.guardar === "function") WAM.guardar();
|
||
if (typeof WAM.render === "function") WAM.render();
|
||
WAM.renderTarjetas();
|
||
}
|
||
|
||
WAM.anadirMensaje = function (tipo) {
|
||
if (!WAM.estado) return;
|
||
if (!Array.isArray(WAM.estado.mensajes)) WAM.estado.mensajes = [];
|
||
var m = crearMensaje(tipo || "text");
|
||
// Alta contractual: setCampo sobre el array completo
|
||
WAM.setCampo("mensajes", WAM.estado.mensajes.concat([m]));
|
||
abiertas[m.id] = true;
|
||
WAM.renderTarjetas();
|
||
var nodo = listaNodo() && listaNodo().querySelector('[data-id="' + m.id + '"]');
|
||
if (nodo && nodo.scrollIntoView) nodo.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||
};
|
||
|
||
WAM.moverMensaje = function (i, delta) {
|
||
var menssajes = (WAM.estado && WAM.estado.mensajes) || [];
|
||
var j = Number(i) + Number(delta);
|
||
if (i < 0 || i >= menssajes.length || j < 0 || j >= menssajes.length) return;
|
||
var copia = menssajes.slice();
|
||
var tmp = copia[i]; copia[i] = copia[j]; copia[j] = tmp;
|
||
WAM.setCampo("mensajes", copia);
|
||
WAM.renderTarjetas();
|
||
};
|
||
|
||
WAM.duplicarMensaje = function (i) {
|
||
var mensajes = (WAM.estado && WAM.estado.mensajes) || [];
|
||
if (i < 0 || i >= mensajes.length) return;
|
||
var clon;
|
||
try { clon = JSON.parse(JSON.stringify(mensajes[i])); } catch (e) { return; }
|
||
reasignarIds(clon);
|
||
var copia = mensajes.slice();
|
||
copia.splice(Number(i) + 1, 0, clon);
|
||
WAM.setCampo("mensajes", copia);
|
||
abiertas[clon.id] = true;
|
||
WAM.renderTarjetas();
|
||
};
|
||
|
||
WAM.borrarMensaje = function (i) {
|
||
var mensajes = (WAM.estado && WAM.estado.mensajes) || [];
|
||
if (i < 0 || i >= mensajes.length) return;
|
||
if (!window.confirm("¿Borrar este mensaje (" + etiquetaTipo(mensajes[i].tipo) + ")?")) return;
|
||
var copia = mensajes.slice();
|
||
copia.splice(Number(i), 1);
|
||
WAM.setCampo("mensajes", copia);
|
||
WAM.renderTarjetas();
|
||
};
|
||
|
||
/* ------------------------------------------------------------------
|
||
Contrato: WAM.aDataUrl(archivo) → Promise<dataURL> (FileReader)
|
||
------------------------------------------------------------------ */
|
||
WAM.aDataUrl = function (archivo) {
|
||
return new Promise(function (resolve, reject) {
|
||
if (!archivo || typeof FileReader === "undefined") { reject(new Error("sin archivo o sin FileReader")); return; }
|
||
var lector = new FileReader();
|
||
lector.onload = function () { resolve(String(lector.result)); };
|
||
lector.onerror = function () { reject(lector.error || new Error("lectura fallida")); };
|
||
lector.readAsDataURL(archivo);
|
||
});
|
||
};
|
||
|
||
/* ------------------------------------------------------------------
|
||
Contrato: WAM.exportarJSON() — Blob + <a download="wam-demo.json">
|
||
------------------------------------------------------------------ */
|
||
WAM.exportarJSON = function () {
|
||
try {
|
||
var datos = JSON.stringify(WAM.estado, null, 2);
|
||
var blob = new Blob([datos], { type: "application/json" });
|
||
var url = URL.createObjectURL(blob);
|
||
var a = h("a", { href: url, download: "wam-demo.json" });
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
|
||
aviso("Demo exportada como wam-demo.json");
|
||
} catch (e) {
|
||
aviso("No se pudo exportar el JSON", "error");
|
||
}
|
||
};
|
||
|
||
// Acepta File, <input type="file"> o el evento change (robustez de integración con D)
|
||
function archivoDe(arg) {
|
||
if (!arg) return null;
|
||
if (arg instanceof File) return arg;
|
||
if (arg.files && arg.files[0]) return arg.files[0];
|
||
var t = arg.target || arg.currentTarget || arg.srcElement;
|
||
if (t && t.files && t.files[0]) return t.files[0];
|
||
return null;
|
||
}
|
||
|
||
// Convierte un archivo a dataURL (contrato WAM.aDataUrl) y lo guarda en ruta
|
||
function procesarArchivo(ruta, archivo, origenEl) {
|
||
if (!ruta || !archivo) return;
|
||
WAM.aDataUrl(archivo).then(function (dataURL) {
|
||
WAM.setCampo(ruta, dataURL);
|
||
if (ruta === "cuenta.avatar") refrescarAvatarVista();
|
||
else repintarDesde(origenEl);
|
||
aviso("Archivo cargado como dataURL");
|
||
}).catch(function () {
|
||
aviso("No se pudo leer el archivo", "error");
|
||
});
|
||
}
|
||
|
||
function gestionarArchivo(el) {
|
||
var ruta = el.getAttribute("data-media-ruta");
|
||
var f = el.files && el.files[0];
|
||
if (!ruta || !f) return;
|
||
procesarArchivo(ruta, f, el);
|
||
el.value = "";
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Contrato: WAM.importarJSON(archivo) → Promise<void>
|
||
parse + normalizar + guardar + re-render total.
|
||
Los errores se muestran en el aviso del panel y resuelven igualmente
|
||
(sin rechazos sin gestionar → consola limpia, criterio §5.1).
|
||
------------------------------------------------------------------ */
|
||
WAM.importarJSON = function (archivo) {
|
||
return new Promise(function (resolve) {
|
||
var f = archivoDe(archivo);
|
||
if (!f) { aviso("Selecciona un archivo JSON para importar", "error"); resolve(); return; }
|
||
var lector = new FileReader();
|
||
lector.onload = function () {
|
||
try {
|
||
var datos = JSON.parse(String(lector.result));
|
||
if (!datos || typeof datos !== "object" || Array.isArray(datos)) throw new Error("no es un objeto");
|
||
if (typeof WAM.normalizar === "function") datos = WAM.normalizar(datos);
|
||
WAM.estado = datos;
|
||
if (typeof WAM.guardar === "function") WAM.guardar();
|
||
if (typeof WAM.setTema === "function") WAM.setTema(WAM.estado.tema);
|
||
abiertas = Object.create(null);
|
||
if (typeof WAM.render === "function") WAM.render();
|
||
WAM.renderEditor();
|
||
aviso("Demo importada correctamente");
|
||
} catch (e) {
|
||
aviso("El archivo no contiene una demo válida", "error");
|
||
}
|
||
resolve();
|
||
};
|
||
lector.onerror = function () { aviso("No se pudo leer el archivo", "error"); resolve(); };
|
||
lector.readAsText(f);
|
||
});
|
||
};
|
||
|
||
/* ------------------------------------------------------------------
|
||
Resumen por tipo para la cabecera de cada tarjeta
|
||
------------------------------------------------------------------ */
|
||
function resumenDe(m) {
|
||
var t = "";
|
||
switch (m.tipo) {
|
||
case "text": t = m.texto; break;
|
||
case "image": t = "Imagen" + (m.caption ? " · " + m.caption : ""); break;
|
||
case "video": t = "Vídeo" + (m.caption ? " · " + m.caption : ""); break;
|
||
case "audio": t = (m.voz ? "Nota de voz" : "Audio") + (m.duracion ? " · " + m.duracion : ""); break;
|
||
case "document": t = m.filename || "Documento"; break;
|
||
case "sticker": t = "Sticker" + (m.src ? "" : " (placeholder)"); break;
|
||
case "location": t = m.nombre || m.direccion || "Ubicación"; break;
|
||
case "contact": t = m.nombre || "Contacto"; break;
|
||
case "template": t = "Plantilla" + (m.body ? " · " + m.body : ""); break;
|
||
case "buttons": t = m.body || "Botones rápidos"; break;
|
||
case "list": t = "Lista · " + (m.header || m.boton || "sin título"); break;
|
||
case "cta_url": t = (m.textoBoton || "CTA") + (m.url ? " · " + m.url : ""); break;
|
||
case "carousel": t = "Carrusel · " + ((m.cards || []).length) + " tarjetas"; break;
|
||
default: t = m.tipo || "";
|
||
}
|
||
t = String(t === undefined || t === null ? "" : t).replace(/\s+/g, " ").trim();
|
||
if (t.length > 44) t = t.slice(0, 43) + "…";
|
||
return t || "(vacío)";
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Constructores de formulario (nodos ed-*)
|
||
------------------------------------------------------------------ */
|
||
|
||
// opts.max = límite duro (maxlength) · opts.limite = límite oficial SOFT
|
||
// (contador ámbar/rojo, no bloquea la escritura) · opts.contar = contador
|
||
// también para límites duros.
|
||
function edCampo(etiqueta, ruta, valor, opts) {
|
||
opts = opts || {};
|
||
var maxContador = opts.limite || (opts.contar ? opts.max : 0);
|
||
var input = h("input", {
|
||
type: opts.tipo || "text",
|
||
class: "ed-input",
|
||
value: predeterminado(valor, ""),
|
||
id: opts.id || null,
|
||
"data-ruta": ruta,
|
||
placeholder: opts.placeholder || "",
|
||
maxlength: opts.max || null,
|
||
autocomplete: "off",
|
||
spellcheck: opts.spell === false ? "false" : null
|
||
});
|
||
var hijos = [h("span", { class: "ed-campo-e", text: etiqueta })];
|
||
if (opts.formato || opts.variables) hijos.push(edBarra(opts));
|
||
hijos.push(input);
|
||
if (maxContador) hijos.push(edContador(valor, maxContador));
|
||
return h("label", { class: "ed-campo" }, hijos);
|
||
}
|
||
|
||
// Nota: textarea no admite value como atributo; se asigna texto al construirlo:
|
||
function edTextarea(etiqueta, ruta, valor, opts) {
|
||
opts = opts || {};
|
||
var ta = h("textarea", {
|
||
class: "ed-input",
|
||
rows: opts.rows || 2,
|
||
maxlength: opts.max || null,
|
||
"data-ruta": ruta,
|
||
placeholder: opts.placeholder || ""
|
||
});
|
||
ta.value = predeterminado(valor, "");
|
||
var maxContador = opts.limite || (opts.contar ? opts.max : 0);
|
||
var hijos = [h("span", { class: "ed-campo-e", text: etiqueta })];
|
||
if (opts.formato || opts.variables) hijos.push(edBarra(opts));
|
||
hijos.push(ta);
|
||
if (maxContador) hijos.push(edContador(valor, maxContador));
|
||
return h("label", { class: "ed-campo" }, hijos);
|
||
}
|
||
|
||
function edCheck(etiqueta, ruta, marcado, id) {
|
||
return h("label", { class: "ed-check" }, [
|
||
h("input", { type: "checkbox", id: id || null, "data-ruta": ruta, checked: !!marcado }),
|
||
h("span", { text: etiqueta })
|
||
]);
|
||
}
|
||
|
||
function edSelect(etiqueta, ruta, valor, opciones, repintar) {
|
||
var sel = h("select", { class: "ed-input ed-select", "data-ruta": ruta, "data-repintar": repintar ? "1" : null });
|
||
opciones.forEach(function (op) {
|
||
sel.appendChild(h("option", { value: op[0], text: op[1], selected: op[0] === valor ? true : null }));
|
||
});
|
||
return h("label", { class: "ed-campo" }, [
|
||
h("span", { class: "ed-campo-e", text: etiqueta }),
|
||
sel
|
||
]);
|
||
}
|
||
|
||
function btnMini(texto, titulo, acc, extras, peligro) {
|
||
var attrs = {
|
||
type: "button",
|
||
class: "ed-btn ed-btn-mini" + (peligro ? " ed-btn-peligro" : ""),
|
||
text: texto,
|
||
title: titulo,
|
||
"aria-label": titulo,
|
||
"data-acc": acc
|
||
};
|
||
Object.keys(extras || {}).forEach(function (k) { attrs[k] = extras[k]; });
|
||
return h("button", attrs);
|
||
}
|
||
|
||
// Asa de arrastre para reordenar (mensajes, botones, filas, secciones, cards)
|
||
function edAsa(titulo) {
|
||
return h("span", {
|
||
class: "ed-asa",
|
||
title: titulo || "Arrastra para reordenar",
|
||
"aria-hidden": "true"
|
||
}, [svgIcono(GRIP)]);
|
||
}
|
||
|
||
/* --- Origen de media: radios URL / Subir archivo + dropzone --- */
|
||
|
||
function modoMediaDe(valor) {
|
||
var v = String(predeterminado(valor, ""));
|
||
if (!v) return "archivo";
|
||
return v.slice(0, 5) === "data:" ? "archivo" : "url";
|
||
}
|
||
|
||
function edRadiosMedia(ruta, modo) {
|
||
var nombre = "media-" + String(ruta).replace(/[^a-zA-Z0-9]+/g, "-");
|
||
return h("div", { class: "ed-media-modos", role: "radiogroup", "aria-label": "Origen del archivo" }, [
|
||
h("label", { class: "ed-radio" }, [
|
||
h("input", { type: "radio", name: nombre, value: "url", checked: modo === "url", "data-rol": "media-modo" }),
|
||
h("span", { text: "URL" })
|
||
]),
|
||
h("label", { class: "ed-radio" }, [
|
||
h("input", { type: "radio", name: nombre, value: "archivo", checked: modo === "archivo", "data-rol": "media-modo" }),
|
||
h("span", { text: "Subir archivo" })
|
||
])
|
||
]);
|
||
}
|
||
|
||
// Zona de soltado: clic abre el input de archivo y acepta arrastrar-soltar
|
||
function edDropzona(ruta, texto) {
|
||
return h("button", {
|
||
type: "button",
|
||
class: "ed-dropzona",
|
||
"data-acc": "drop-clic",
|
||
"data-media-ruta": ruta,
|
||
title: "Clic para elegir archivo, o arrastra y suelta aquí"
|
||
}, [
|
||
svgIcono(ICONO_SUBIR),
|
||
h("span", { class: "ed-dropzona-texto", text: texto || "Clic o arrastra un archivo aquí" })
|
||
]);
|
||
}
|
||
|
||
// Campo de media: radios de origen, dropzone (archivo) y entrada por URL
|
||
function edMedia(etiqueta, ruta, valor, opts) {
|
||
opts = opts || {};
|
||
var modo = modoMediaDe(valor);
|
||
var clasesMedia = "ed-media-vista" + (opts.redonda ? " ed-avatar-vista" : "");
|
||
var vista;
|
||
if (valor) {
|
||
if (opts.tipoMedia === "video") {
|
||
vista = h("video", { class: clasesMedia, src: valor, muted: true, preload: "metadata", playsinline: true });
|
||
} else if (opts.tipoMedia === "audio" || opts.tipoMedia === "doc") {
|
||
vista = h("span", { class: clasesMedia + " ed-media-icono", text: opts.tipoMedia === "audio" ? "🎧" : "📄" });
|
||
} else {
|
||
vista = h("img", { class: clasesMedia, src: valor, alt: "Vista previa" });
|
||
}
|
||
} else {
|
||
var ph = placeholderSvg(etiqueta);
|
||
vista = ph
|
||
? h("img", { class: clasesMedia + " ed-media-placeholder", src: ph, alt: "" })
|
||
: h("span", { class: clasesMedia + " ed-media-vacio", text: "—" });
|
||
}
|
||
if (opts.idVista) vista.setAttribute("id", opts.idVista);
|
||
var inputUrl = h("input", {
|
||
type: "text",
|
||
class: "ed-input",
|
||
value: predeterminado(valor, ""),
|
||
"data-ruta": ruta,
|
||
id: opts.idUrl || null,
|
||
placeholder: "Pega una URL / dataURL",
|
||
autocomplete: "off",
|
||
spellcheck: "false"
|
||
});
|
||
var inputArchivo = h("input", {
|
||
type: "file",
|
||
class: "ed-archivo",
|
||
accept: opts.aceptar || "",
|
||
"data-media-ruta": ruta,
|
||
id: opts.idArchivo || null,
|
||
"aria-label": "Subir " + etiqueta.toLowerCase()
|
||
});
|
||
return h("div", {
|
||
class: "ed-media " + (modo === "url" ? "ed-modo-url" : "ed-modo-archivo") + (opts.redonda ? " ed-avatar" : "")
|
||
}, [
|
||
vista,
|
||
h("div", { class: "ed-media-datos" }, [
|
||
h("span", { class: "ed-campo-e", text: etiqueta }),
|
||
edRadiosMedia(ruta, modo),
|
||
h("div", { class: "ed-media-panel ed-panel-url" }, [inputUrl]),
|
||
h("div", { class: "ed-media-panel ed-panel-archivo" }, [
|
||
inputArchivo,
|
||
edDropzona(ruta, opts.textoDrop)
|
||
])
|
||
])
|
||
]);
|
||
}
|
||
|
||
/* --- Sub-editor de respuestas encadenadas (array de Mensaje dir:"in") --- */
|
||
function edRespuestas(ruta, arr, raiz) {
|
||
var lista = Array.isArray(arr) ? arr : [];
|
||
var hijos = lista.map(function (m, j) {
|
||
return edTarjeta(m, { anidado: true, raiz: raiz, rutaMsg: ruta + "." + j, rutaArr: ruta, indice: j });
|
||
});
|
||
return h("div", { class: "ed-sub ed-resps" }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
h("span", { class: "ed-sub-titulo", text: "Respuestas (" + lista.length + ")" }),
|
||
h("button", {
|
||
type: "button", class: "ed-btn ed-btn-mini ed-add", text: "+ Respuesta",
|
||
"data-acc": "add-resp", "data-ruta": ruta,
|
||
title: "Mensaje entrante que llega al pulsar"
|
||
})
|
||
]),
|
||
hijos.length ? h("div", { class: "ed-anidadas" }, hijos)
|
||
: h("p", { class: "ed-nota", text: "Sin respuestas: al pulsarlo solo sale la burbuja del texto del botón." })
|
||
]);
|
||
}
|
||
|
||
/* --- Sub-editor de botones (template con URL / buttons sin URL) --- */
|
||
function edListaBotones(ruta, botones, o) {
|
||
var lista = Array.isArray(botones) ? botones : [];
|
||
var hijos = lista.map(function (b, i) {
|
||
return h("div", { class: "ed-item", "data-drag-ruta": ruta }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
edAsa("Arrastra para reordenar " + o.etiqueta.toLowerCase() + "s"),
|
||
h("span", { class: "ed-sub-titulo", text: o.etiqueta + " " + (i + 1) }),
|
||
btnMini("✕", "Quitar " + o.etiqueta.toLowerCase(), "del-boton", { "data-ruta": ruta, "data-idx": i }, true)
|
||
]),
|
||
edCampo("Texto", ruta + "." + i + ".texto", b.texto, { limite: o.maxTexto, placeholder: o.ejemplo || "" }),
|
||
o.conUrl ? edCampo("URL (si la pones, abre el enlace en vez de responder)", ruta + "." + i + ".url", b.url, { placeholder: "https://…" }) : null,
|
||
edRespuestas(ruta + "." + i + ".respuestas", b.respuestas, o.raiz)
|
||
]);
|
||
});
|
||
var lleno = lista.length >= o.max;
|
||
return h("div", { class: "ed-sub" }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
h("span", { class: "ed-sub-titulo", text: "Botones (" + lista.length + "/" + o.max + ")" })
|
||
]),
|
||
hijos.length ? h("div", { class: "ed-anidadas" }, hijos) : null,
|
||
h("button", {
|
||
type: "button", class: "ed-btn ed-btn-mini ed-add",
|
||
text: "+ Añadir " + o.etiqueta.toLowerCase(),
|
||
"data-acc": "add-boton", "data-ruta": ruta, "data-max": o.max, "data-conurl": o.conUrl ? "1" : "0",
|
||
disabled: lleno || null, title: lleno ? "Máximo " + o.max : null
|
||
})
|
||
]);
|
||
}
|
||
|
||
/* --- Sub-editor de secciones/filas de la lista --- */
|
||
function totalFilas(secciones) {
|
||
return (secciones || []).reduce(function (n, s) { return n + (Array.isArray(s.filas) ? s.filas.length : 0); }, 0);
|
||
}
|
||
|
||
function edSeccionesEd(ruta, secciones, raiz) {
|
||
var lista = Array.isArray(secciones) ? secciones : [];
|
||
var hijos = lista.map(function (s, si) {
|
||
var filas = Array.isArray(s.filas) ? s.filas : [];
|
||
var rutaFilas = ruta + "." + si + ".filas";
|
||
var filaEls = filas.map(function (f, fi) {
|
||
return h("div", { class: "ed-item", "data-drag-ruta": rutaFilas }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
edAsa("Arrastra para reordenar la fila"),
|
||
h("span", { class: "ed-sub-titulo", text: "Fila " + (fi + 1) }),
|
||
btnMini("✕", "Quitar fila", "del-fila", { "data-ruta": rutaFilas, "data-idx": fi, "data-min": 1 }, true)
|
||
]),
|
||
edCampo("Título", rutaFilas + "." + fi + ".titulo", f.titulo, { limite: 24 }),
|
||
edCampo("Descripción", rutaFilas + "." + fi + ".descripcion", f.descripcion, { limite: 72 }),
|
||
edRespuestas(rutaFilas + "." + fi + ".respuestas", f.respuestas, raiz)
|
||
]);
|
||
});
|
||
return h("div", { class: "ed-item", "data-drag-ruta": ruta }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
edAsa("Arrastra para reordenar la sección"),
|
||
h("span", { class: "ed-sub-titulo", text: "Sección " + (si + 1) }),
|
||
btnMini("✕", "Quitar sección", "del-sec", { "data-ruta": ruta, "data-idx": si, "data-min": 1 }, true)
|
||
]),
|
||
edCampo("Título de sección", ruta + "." + si + ".titulo", s.titulo, { limite: 24 }),
|
||
h("div", { class: "ed-sub" }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
h("span", { class: "ed-sub-titulo", text: "Filas (" + totalFilas(lista) + "/" + LIMITES.filas + ")" })
|
||
]),
|
||
filaEls.length ? h("div", { class: "ed-anidadas" }, filaEls) : h("p", { class: "ed-nota", text: "Sin filas." }),
|
||
h("button", {
|
||
type: "button", class: "ed-btn ed-btn-mini ed-add", text: "+ Fila",
|
||
"data-acc": "add-fila", "data-ruta": rutaFilas, "data-max": LIMITES.filas,
|
||
disabled: totalFilas(lista) >= LIMITES.filas || null,
|
||
title: totalFilas(lista) >= LIMITES.filas ? "Máximo " + LIMITES.filas + " filas en total" : null
|
||
})
|
||
])
|
||
]);
|
||
});
|
||
return h("div", { class: "ed-sub" }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
h("span", { class: "ed-sub-titulo", text: "Secciones (" + lista.length + "/" + LIMITES.secciones + ")" })
|
||
]),
|
||
hijos.length ? h("div", { class: "ed-anidadas" }, hijos) : h("p", { class: "ed-nota", text: "Sin secciones." }),
|
||
h("button", {
|
||
type: "button", class: "ed-btn ed-btn-mini ed-add", text: "+ Sección",
|
||
"data-acc": "add-sec", "data-ruta": ruta, "data-max": LIMITES.secciones,
|
||
disabled: lista.length >= LIMITES.secciones || null,
|
||
title: lista.length >= LIMITES.secciones ? "Máximo " + LIMITES.secciones : null
|
||
})
|
||
]);
|
||
}
|
||
|
||
/* --- Sub-editor de tarjetas del carrusel --- */
|
||
function edCardsEd(ruta, cards, raiz) {
|
||
var lista = Array.isArray(cards) ? cards : [];
|
||
var hijos = lista.map(function (c, ci) {
|
||
return h("div", { class: "ed-item", "data-drag-ruta": ruta }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
edAsa("Arrastra para reordenar la tarjeta"),
|
||
h("span", { class: "ed-sub-titulo", text: "Tarjeta " + (ci + 1) }),
|
||
btnMini("✕", "Quitar tarjeta", "del-card", { "data-ruta": ruta, "data-idx": ci, "data-min": LIMITES.tarjetasMin }, true)
|
||
]),
|
||
edMedia("Imagen", ruta + "." + ci + ".media", c.media, { aceptar: "image/*", textoDrop: "Clic o arrastra una imagen aquí" }),
|
||
edTextarea("Texto", ruta + "." + ci + ".texto", c.texto, { limite: 160, rows: 2 }),
|
||
h("div", { class: "ed-fila" }, [
|
||
edCampo("Texto del botón", ruta + "." + ci + ".textoBoton", c.textoBoton, { limite: 25 }),
|
||
edCampo("URL (opcional)", ruta + "." + ci + ".url", c.url, { placeholder: "https://…" })
|
||
]),
|
||
edRespuestas(ruta + "." + ci + ".respuestas", c.respuestas, raiz)
|
||
]);
|
||
});
|
||
return h("div", { class: "ed-sub" }, [
|
||
h("div", { class: "ed-sub-cab" }, [
|
||
h("span", { class: "ed-sub-titulo", text: "Tarjetas (" + lista.length + "/" + LIMITES.tarjetasMax + ", mín " + LIMITES.tarjetasMin + ")" })
|
||
]),
|
||
hijos.length ? h("div", { class: "ed-anidadas" }, hijos) : h("p", { class: "ed-nota", text: "Sin tarjetas." }),
|
||
h("button", {
|
||
type: "button", class: "ed-btn ed-btn-mini ed-add", text: "+ Tarjeta",
|
||
"data-acc": "add-card", "data-ruta": ruta, "data-max": LIMITES.tarjetasMax,
|
||
disabled: lista.length >= LIMITES.tarjetasMax || null,
|
||
title: lista.length >= LIMITES.tarjetasMax ? "Máximo " + LIMITES.tarjetasMax : null
|
||
})
|
||
]);
|
||
}
|
||
|
||
/* --- Cabecera común de template y buttons (research-api §6.1/§7.2) ---
|
||
Interruptor "Mostrar cabecera": headerTipo "" = oculta. Al activarla
|
||
aparecen las mini-tarjetas de tipo (Texto/Imagen/Vídeo/Documento) y
|
||
los campos que dependen de cada una (headerTexto o headerMedia). */
|
||
function edHeaderBloque(ruta, m, conVariables) {
|
||
var tipo = predeterminado(m.headerTipo, "");
|
||
var caja = h("div", { class: "ed-header-bloque" });
|
||
caja.appendChild(h("div", { class: "ed-sub-cab" }, [
|
||
h("span", { class: "ed-sub-titulo", text: "Cabecera" }),
|
||
h("label", { class: "ed-switch" }, [
|
||
h("input", { type: "checkbox", "data-rol": "header-toggle", "data-ruta": ruta, checked: tipo !== "" }),
|
||
h("span", { class: "ed-switch-pista" }, [h("span", { class: "ed-switch-nudo" })]),
|
||
h("span", { class: "ed-switch-texto", text: tipo === "" ? "Mostrar cabecera" : "Ocultar cabecera" })
|
||
])
|
||
]));
|
||
if (tipo !== "") {
|
||
var pal = h("div", { class: "ed-header-tipos", role: "group", "aria-label": "Tipo de cabecera" });
|
||
TIPOS_HEADER.forEach(function (t) {
|
||
pal.appendChild(h("button", {
|
||
type: "button",
|
||
class: "ed-tipo-card ed-tipo-card-mini" + (t[0] === tipo ? " ed-seleccionada" : ""),
|
||
"data-acc": "header-tipo",
|
||
"data-ruta": ruta,
|
||
"data-valor": t[0],
|
||
"aria-pressed": t[0] === tipo ? "true" : "false",
|
||
title: "Cabecera de tipo " + t[1]
|
||
}, [svgIcono(ICONOS_TIPO[t[0]] || ""), h("span", { class: "ed-tipo-card-e", text: t[1] })]));
|
||
});
|
||
caja.appendChild(pal);
|
||
if (tipo === "texto") {
|
||
caja.appendChild(edCampo("Texto de cabecera", ruta + ".headerTexto", m.headerTexto, {
|
||
limite: 60, variables: !!conVariables, placeholder: "Nueva promoción"
|
||
}));
|
||
} else {
|
||
caja.appendChild(edMedia("Media de cabecera", ruta + ".headerMedia", m.headerMedia, {
|
||
aceptar: tipo === "imagen" ? "image/*" : (tipo === "video" ? "video/*" : ""),
|
||
tipoMedia: tipo === "video" ? "video" : (tipo === "documento" ? "doc" : "img"),
|
||
textoDrop: tipo === "video" ? "Clic o arrastra un vídeo aquí"
|
||
: (tipo === "documento" ? "Clic o arrastra un documento aquí" : "Clic o arrastra una imagen aquí")
|
||
}));
|
||
}
|
||
} else {
|
||
caja.appendChild(h("p", { class: "ed-nota", text: "Sin cabecera. Actívala para añadir texto o media." }));
|
||
}
|
||
return caja;
|
||
}
|
||
|
||
/* --- Formulario completo por tipo (TODOS los campos del SPEC §1) --- */
|
||
function edFormTipo(m, ruta, raiz) {
|
||
var frag = document.createDocumentFragment();
|
||
var ap = function (n) { if (n) frag.appendChild(n); };
|
||
switch (m.tipo) {
|
||
case "text":
|
||
ap(edTextarea("Texto", ruta + ".texto", m.texto, { limite: 4096, rows: 3, formato: true, placeholder: "Hola 👋" }));
|
||
break;
|
||
case "image":
|
||
ap(edMedia("Imagen", ruta + ".src", m.src, { aceptar: "image/*", textoDrop: "Clic o arrastra una imagen aquí" }));
|
||
ap(edTextarea("Caption (opcional)", ruta + ".caption", m.caption, { limite: 1024 }));
|
||
break;
|
||
case "video":
|
||
ap(edMedia("Vídeo", ruta + ".src", m.src, { aceptar: "video/*", tipoMedia: "video", textoDrop: "Clic o arrastra un vídeo aquí" }));
|
||
ap(edTextarea("Caption (opcional)", ruta + ".caption", m.caption, { limite: 1024 }));
|
||
break;
|
||
case "audio":
|
||
ap(edMedia("Audio (opcional)", ruta + ".src", m.src, { aceptar: "audio/*", tipoMedia: "audio", textoDrop: "Clic o arrastra un audio aquí" }));
|
||
ap(edCheck("Es nota de voz (waveform)", ruta + ".voz", !!m.voz));
|
||
ap(edCampo("Duración", ruta + ".duracion", m.duracion, { max: 8, placeholder: "0:12" }));
|
||
break;
|
||
case "document":
|
||
ap(edCampo("Nombre de archivo (con extensión; define el icono)", ruta + ".filename", m.filename, { max: 80, contar: true, placeholder: "menu.pdf" }));
|
||
ap(edTextarea("Caption (opcional)", ruta + ".caption", m.caption, { limite: 1024 }));
|
||
ap(edMedia("Archivo (opcional, no se muestra)", ruta + ".src", m.src, { tipoMedia: "doc", textoDrop: "Clic o arrastra un documento aquí" }));
|
||
break;
|
||
case "sticker":
|
||
ap(edMedia("Sticker (WebP o imagen; sin burbuja de texto)", ruta + ".src", m.src, { aceptar: "image/webp,image/*", textoDrop: "Clic o arrastra una imagen o WebP aquí" }));
|
||
break;
|
||
case "location":
|
||
// Grid de 2 columnas con las 4 coordenadas del lugar
|
||
ap(h("div", { class: "ed-grid2" }, [
|
||
edCampo("Latitud", ruta + ".latitud", m.latitud, { placeholder: "37.44216" }),
|
||
edCampo("Longitud", ruta + ".longitud", m.longitud, { placeholder: "-122.16153" }),
|
||
edCampo("Nombre", ruta + ".nombre", m.nombre, { placeholder: "Café Aurora" }),
|
||
edCampo("Dirección", ruta + ".direccion", m.direccion, { placeholder: "C/ Mayor 1, Madrid" })
|
||
]));
|
||
break;
|
||
case "contact":
|
||
ap(h("div", { class: "ed-fila" }, [
|
||
edCampo("Nombre", ruta + ".nombre", m.nombre, { placeholder: "Ana García" }),
|
||
edCampo("Teléfono", ruta + ".telefono", m.telefono, { placeholder: "+34 600 123 456" })
|
||
]));
|
||
ap(h("div", { class: "ed-fila" }, [
|
||
edCampo("Email", ruta + ".email", m.email, { placeholder: "[email protected]" }),
|
||
edCampo("Empresa", ruta + ".empresa", m.empresa, { placeholder: "Café Aurora" })
|
||
]));
|
||
break;
|
||
case "template": {
|
||
ap(edHeaderBloque(ruta, m, true));
|
||
ap(edTextarea("Cuerpo", ruta + ".body", m.body, { limite: 1024, rows: 3, formato: true, variables: true }));
|
||
ap(edCampo("Pie (footer)", ruta + ".footer", m.footer, { limite: 60 }));
|
||
ap(edListaBotones(ruta + ".botones", m.botones, {
|
||
max: LIMITES.botonesPlantilla, maxTexto: 25, conUrl: true,
|
||
etiqueta: "Botón", ejemplo: "Confirmar", raiz: raiz
|
||
}));
|
||
break;
|
||
}
|
||
case "buttons":
|
||
ap(edHeaderBloque(ruta, m, false));
|
||
ap(edTextarea("Cuerpo", ruta + ".body", m.body, { limite: 1024, rows: 2 }));
|
||
ap(edCampo("Pie (footer)", ruta + ".footer", m.footer, { limite: 60 }));
|
||
ap(edListaBotones(ruta + ".botones", m.botones, {
|
||
max: LIMITES.botonesRapidos, maxTexto: 20, conUrl: false,
|
||
etiqueta: "Botón rápido", ejemplo: "Horarios", raiz: raiz
|
||
}));
|
||
break;
|
||
case "list":
|
||
ap(edCampo("Cabecera", ruta + ".header", m.header, { limite: 60 }));
|
||
ap(edTextarea("Cuerpo", ruta + ".body", m.body, { limite: 4096, rows: 2, formato: true }));
|
||
ap(edCampo("Pie (footer)", ruta + ".footer", m.footer, { limite: 60 }));
|
||
ap(edCampo("Etiqueta del botón", ruta + ".boton", m.boton, { limite: 20, placeholder: "Ver opciones" }));
|
||
ap(edSeccionesEd(ruta + ".secciones", m.secciones, raiz));
|
||
break;
|
||
case "cta_url":
|
||
ap(edMedia("Cabecera («» = sin cabecera)", ruta + ".headerMedia", m.headerMedia, { aceptar: "image/*", textoDrop: "Clic o arrastra una imagen aquí" }));
|
||
ap(edTextarea("Cuerpo", ruta + ".body", m.body, { limite: 1024, rows: 2 }));
|
||
ap(edCampo("Pie (footer)", ruta + ".footer", m.footer, { limite: 60 }));
|
||
ap(h("div", { class: "ed-fila" }, [
|
||
edCampo("Texto del botón", ruta + ".textoBoton", m.textoBoton, { limite: 25 }),
|
||
edCampo("URL", ruta + ".url", m.url, { placeholder: "https://…" })
|
||
]));
|
||
break;
|
||
case "carousel":
|
||
ap(edTextarea("Cuerpo (opcional)", ruta + ".body", m.body, { limite: 1024, rows: 2 }));
|
||
ap(edCardsEd(ruta + ".cards", m.cards, raiz));
|
||
break;
|
||
default:
|
||
ap(h("p", { class: "ed-nota", text: "Tipo desconocido: " + m.tipo }));
|
||
}
|
||
return frag;
|
||
}
|
||
|
||
/* --- Una tarjeta colapsable (mensaje top-level o respuesta anidada) --- */
|
||
function edTarjeta(m, o) {
|
||
o = o || {};
|
||
var abierta = !!abiertas[m.id];
|
||
var rutaMsg = o.rutaMsg || ("mensajes." + o.i);
|
||
var esOut = m.dir === "out";
|
||
var acciones = [];
|
||
if (!o.anidado) {
|
||
acciones.push(btnMini("↑", "Subir", "mover", { "data-i": o.i, "data-delta": -1 }));
|
||
acciones.push(btnMini("↓", "Bajar", "mover", { "data-i": o.i, "data-delta": 1 }));
|
||
acciones.push(btnMini("⧉", "Duplicar", "dup", { "data-i": o.i }));
|
||
}
|
||
acciones.push(o.anidado
|
||
? btnMini("✕", "Quitar respuesta", "del-resp", { "data-ruta": o.rutaArr, "data-idx": o.indice }, true)
|
||
: btnMini("✕", "Borrar mensaje", "borrar", { "data-i": o.i }, true));
|
||
|
||
var comunes = [];
|
||
if (!o.anidado) {
|
||
comunes.push(edSelect("Dirección", rutaMsg + ".dir", esOut ? "out" : "in", [
|
||
["in", "Entrada · recibido"],
|
||
["out", "Salida · enviado"]
|
||
], true));
|
||
comunes.push(edCampo("Hora", rutaMsg + ".hora", m.hora, { max: 5, placeholder: "10:24" }));
|
||
// El estado (ticks) solo aplica a los salientes (SPEC §1)
|
||
if (esOut) {
|
||
comunes.push(edSelect("Estado (ticks)", rutaMsg + ".estado",
|
||
ESTADOS.some(function (e) { return e[0] === m.estado; }) ? m.estado : "sent",
|
||
ESTADOS, true));
|
||
}
|
||
}
|
||
|
||
var cab = h("button", {
|
||
type: "button", class: "ed-cab-principal", "data-acc": "toggle",
|
||
"aria-expanded": abierta ? "true" : "false"
|
||
}, [
|
||
o.anidado ? null : h("span", { class: "ed-num", text: String(o.i + 1) }),
|
||
h("span", { class: "ed-dir ed-dir-" + (esOut ? "out" : "in"), text: esOut ? "Salida" : "Entrada" }),
|
||
h("span", { class: "ed-badge", text: etiquetaTipo(m.tipo) }),
|
||
h("span", { class: "ed-resumen", text: resumenDe(m) })
|
||
]);
|
||
|
||
return h(o.etiqueta || (o.anidado ? "div" : "li"), {
|
||
class: "ed-tarjeta" + (o.anidado ? " ed-anidada" : "") + (abierta ? " ed-abierta" : ""),
|
||
"data-id": m.id,
|
||
"data-raiz": o.raiz || m.id,
|
||
"data-drag-ruta": o.anidado ? null : "mensajes"
|
||
}, [
|
||
h("div", { class: "ed-tarjeta-cab" }, [
|
||
o.anidado ? null : edAsa("Arrastra para reordenar el mensaje"),
|
||
cab,
|
||
h("span", { class: "ed-acciones" }, acciones)
|
||
]),
|
||
h("div", { class: "ed-cuerpo" }, [
|
||
comunes.length ? h("div", { class: "ed-fila ed-comunes" }, comunes) : null,
|
||
h("div", { class: "ed-form" }, edFormTipo(m, rutaMsg, o.raiz || m.id))
|
||
])
|
||
]);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Contrato: WAM.renderTarjetas() — una tarjeta por mensaje en #lista-mensajes
|
||
------------------------------------------------------------------ */
|
||
function listaNodo() { return document.getElementById("lista-mensajes"); }
|
||
|
||
WAM.renderTarjetas = function () {
|
||
var lista = listaNodo();
|
||
if (!lista) return;
|
||
lista.classList.add("ed-lista");
|
||
var etiquetaItem = (lista.tagName === "OL" || lista.tagName === "UL") ? "li" : "div";
|
||
while (lista.firstChild) lista.removeChild(lista.firstChild);
|
||
var mensajes = (WAM.estado && Array.isArray(WAM.estado.mensajes)) ? WAM.estado.mensajes : [];
|
||
if (!mensajes.length) {
|
||
lista.appendChild(h(etiquetaItem, { class: "ed-vacio", text: "Sin mensajes todavía. Elige un tipo en la paleta y pulsa «Añadir»." }));
|
||
return;
|
||
}
|
||
mensajes.forEach(function (m, i) {
|
||
lista.appendChild(edTarjeta(m, { i: i, raiz: m.id, rutaMsg: "mensajes." + i, etiqueta: etiquetaItem }));
|
||
});
|
||
};
|
||
|
||
// Repinta solo la tarjeta top-level afectada (cambios estructurales locales)
|
||
function repintarTarjeta(raizId) {
|
||
var lista = listaNodo();
|
||
if (!lista) return;
|
||
var mensajes = (WAM.estado && WAM.estado.mensajes) || [];
|
||
var idx = -1;
|
||
for (var i = 0; i < mensajes.length; i++) if (mensajes[i].id === raizId) { idx = i; break; }
|
||
var viejo = lista.querySelector('[data-id="' + raizId + '"]');
|
||
if (idx < 0 || !viejo) { WAM.renderTarjetas(); return; }
|
||
var etiquetaItem = (lista.tagName === "OL" || lista.tagName === "UL") ? "li" : "div";
|
||
lista.replaceChild(edTarjeta(mensajes[idx], { i: idx, raiz: raizId, rutaMsg: "mensajes." + idx, etiqueta: etiquetaItem }), viejo);
|
||
}
|
||
|
||
function repintarDesde(el) {
|
||
var t = el && el.closest ? el.closest(".ed-tarjeta") : null;
|
||
var raiz = t && (t.getAttribute("data-raiz") || t.getAttribute("data-id"));
|
||
if (raiz) repintarTarjeta(raiz);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Sección identidad de la cuenta (#editor-cuenta)
|
||
------------------------------------------------------------------ */
|
||
function temaActual() {
|
||
return (WAM.estado && WAM.estado.tema) || document.body.getAttribute("data-tema") || "claro";
|
||
}
|
||
|
||
function aplicarTema(v) {
|
||
var tema = v === "oscuro" ? "oscuro" : "claro";
|
||
if (WAM.estado) {
|
||
WAM.estado.tema = tema;
|
||
if (typeof WAM.guardar === "function") WAM.guardar();
|
||
}
|
||
if (typeof WAM.setTema === "function") WAM.setTema(tema);
|
||
}
|
||
|
||
function sincronizarSelectTema() {
|
||
if (!panel) return;
|
||
var sel = panel.querySelector('[data-rol="tema"]');
|
||
if (sel) sel.value = temaActual();
|
||
}
|
||
|
||
function refrescarAvatarVista() {
|
||
var img = document.getElementById("cuenta-avatar-vista");
|
||
var url = document.getElementById("cuenta-avatar-url");
|
||
var avatar = WAM.estado && WAM.estado.cuenta ? predeterminado(WAM.estado.cuenta.avatar, "") : "";
|
||
if (img) {
|
||
var src = avatar || placeholderSvg("sin avatar");
|
||
if (src) { img.setAttribute("src", src); img.classList.remove("ed-media-vacio"); }
|
||
}
|
||
if (url && url.value !== avatar) url.value = avatar;
|
||
}
|
||
|
||
function renderCuenta(cont) {
|
||
// El cromado del bloque (padding, borde, título «Cuenta») lo aporta A
|
||
// (.wam-bloque); aquí solo la rejilla de campos.
|
||
cont.classList.add("ed-cuenta");
|
||
var c = (WAM.estado && WAM.estado.cuenta) || {};
|
||
while (cont.firstChild) cont.removeChild(cont.firstChild);
|
||
|
||
cont.appendChild(h("div", { class: "ed-fila" }, [
|
||
edCampo("Nombre", "cuenta.nombre", c.nombre, { id: "cuenta-nombre", max: 50, contar: true, placeholder: "Café Aurora" }),
|
||
edCampo("Subtítulo", "cuenta.subtitulo", c.subtitulo, { id: "cuenta-subtitulo", max: 60, contar: true, placeholder: "en línea" })
|
||
]));
|
||
|
||
var avatar = predeterminado(c.avatar, "");
|
||
var bloqueAvatar = edMedia("Avatar", "cuenta.avatar", avatar, {
|
||
aceptar: "image/*",
|
||
redonda: true,
|
||
idVista: "cuenta-avatar-vista",
|
||
idArchivo: "cuenta-avatar-archivo",
|
||
idUrl: "cuenta-avatar-url",
|
||
textoDrop: "Clic o arrastra una imagen aquí"
|
||
});
|
||
var datosAvatar = bloqueAvatar.querySelector(".ed-media-datos");
|
||
if (datosAvatar) {
|
||
datosAvatar.appendChild(h("button", {
|
||
type: "button", class: "ed-btn ed-btn-mini ed-btn-peligro",
|
||
text: "Quitar avatar", "data-acc": "avatar-quitar"
|
||
}));
|
||
}
|
||
cont.appendChild(bloqueAvatar);
|
||
|
||
cont.appendChild(edCheck("Cuenta verificada (badge)", "cuenta.verificado", !!c.verificado, "cuenta-verificado"));
|
||
|
||
var selTema = h("select", { class: "ed-input ed-select", "data-rol": "tema" }, [
|
||
h("option", { value: "claro", text: "Claro", selected: temaActual() !== "oscuro" ? true : null }),
|
||
h("option", { value: "oscuro", text: "Oscuro", selected: temaActual() === "oscuro" ? true : null })
|
||
]);
|
||
cont.appendChild(h("div", { class: "ed-fila ed-tema" }, [
|
||
h("label", { class: "ed-campo" }, [
|
||
h("span", { class: "ed-campo-e", text: "Tema" }),
|
||
selTema
|
||
])
|
||
]));
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Sección alta de mensajes: paleta de tarjetas de tipo (#nuevo-tipo)
|
||
+ #nuevo-tipo-valor (hidden) + #btn-anadir
|
||
------------------------------------------------------------------ */
|
||
|
||
function seleccionarTipo(tipo) {
|
||
if (!esTipoValido(tipo)) tipo = "text";
|
||
var oculto = document.getElementById("nuevo-tipo-valor");
|
||
if (oculto) oculto.value = tipo;
|
||
var pal = document.getElementById("nuevo-tipo");
|
||
if (pal && pal.tagName !== "SELECT") {
|
||
pal.setAttribute("data-valor", tipo);
|
||
var hijos = pal.querySelectorAll("[data-tipo]");
|
||
for (var i = 0; i < hijos.length; i++) {
|
||
var activo = hijos[i].getAttribute("data-tipo") === tipo;
|
||
hijos[i].classList.toggle("ed-seleccionada", activo);
|
||
hijos[i].setAttribute("aria-checked", activo ? "true" : "false");
|
||
}
|
||
}
|
||
}
|
||
|
||
function construirPaleta(cont, inicial) {
|
||
if (cont.getAttribute("data-wam-ed") !== "paleta" || !cont.firstElementChild) {
|
||
while (cont.firstChild) cont.removeChild(cont.firstChild);
|
||
cont.setAttribute("data-valor", inicial);
|
||
TIPOS.forEach(function (t) {
|
||
cont.appendChild(h("button", {
|
||
type: "button",
|
||
class: "ed-tipo-card",
|
||
role: "radio",
|
||
"aria-checked": "false",
|
||
"data-acc": "elegir-tipo",
|
||
"data-tipo": t[0],
|
||
title: "Añadir mensaje de tipo " + t[1]
|
||
}, [svgIcono(ICONOS_TIPO[t[0]] || ""), h("span", { class: "ed-tipo-card-e", text: t[1] })]));
|
||
});
|
||
cont.setAttribute("data-wam-ed", "paleta");
|
||
}
|
||
seleccionarTipo(inicial);
|
||
}
|
||
|
||
function tipoSeleccionadoAlta() {
|
||
var oculto = document.getElementById("nuevo-tipo-valor");
|
||
if (oculto && esTipoValido(oculto.value)) return oculto.value;
|
||
var pal = document.getElementById("nuevo-tipo");
|
||
if (pal) {
|
||
if (pal.tagName === "SELECT") return pal.value;
|
||
var v = pal.getAttribute("data-valor");
|
||
if (esTipoValido(v)) return v;
|
||
}
|
||
return "text";
|
||
}
|
||
|
||
function prepararAlta(lista) {
|
||
var pal = document.getElementById("nuevo-tipo");
|
||
var btn = document.getElementById("btn-anadir");
|
||
if (!pal || !btn) {
|
||
// Reserva: si A no puso el bloque de alta, lo levanta C
|
||
var sec = h("div", { class: "ed-seccion ed-alta" }, [h("h2", { class: "ed-titulo", text: "Añadir mensaje" })]);
|
||
if (!pal) {
|
||
pal = h("div", { id: "nuevo-tipo", class: "ed-paleta", role: "radiogroup", "aria-label": "Tipo de mensaje a añadir" });
|
||
}
|
||
sec.appendChild(pal);
|
||
sec.appendChild(h("input", { type: "hidden", id: "nuevo-tipo-valor", value: "text" }));
|
||
if (!btn) btn = h("button", { id: "btn-anadir", type: "button", class: "ed-btn ed-btn-primario", text: "Añadir" });
|
||
sec.appendChild(btn);
|
||
lista.parentNode.insertBefore(sec, lista);
|
||
}
|
||
if (pal.tagName === "SELECT") {
|
||
// Compatibilidad: si el HTML antiguo dejó el select, se rellena como antes
|
||
if (!pal.options.length) {
|
||
TIPOS.forEach(function (t) { pal.appendChild(h("option", { value: t[0], text: t[1] })); });
|
||
}
|
||
} else {
|
||
var oculto = document.getElementById("nuevo-tipo-valor");
|
||
var inicial = (oculto && oculto.value) || (pal.getAttribute && pal.getAttribute("data-valor")) || "text";
|
||
construirPaleta(pal, esTipoValido(inicial) ? inicial : "text");
|
||
}
|
||
// El estilo de #btn-anadir lo aporta A (.wam-btn-anadir);
|
||
// solo queda el cableado del clic, una única vez.
|
||
if (!btn.getAttribute("data-wam-ed")) {
|
||
btn.setAttribute("data-wam-ed", "1");
|
||
btn.addEventListener("click", function () {
|
||
WAM.anadirMensaje(tipoSeleccionadoAlta());
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Drag & drop nativo: reordenar mensajes (#lista-mensajes) y elementos
|
||
internos (botones, secciones, filas, cards). Todo delegado en #panel-
|
||
editor; el arrastre solo arranca desde un asa (.ed-asa). Indicador de
|
||
inserción: línea azul (ed-drop-antes / ed-drop-despues).
|
||
------------------------------------------------------------------ */
|
||
|
||
function limpiarIndicador() {
|
||
if (dropObjetivo) {
|
||
dropObjetivo.classList.remove("ed-drop-antes");
|
||
dropObjetivo.classList.remove("ed-drop-despues");
|
||
dropObjetivo = null;
|
||
}
|
||
}
|
||
|
||
function marcarObjetivo(objetivo, tras) {
|
||
if (dropObjetivo !== objetivo) limpiarIndicador();
|
||
dropObjetivo = objetivo;
|
||
dropTras = tras;
|
||
objetivo.classList.toggle("ed-drop-antes", !tras);
|
||
objetivo.classList.toggle("ed-drop-despues", tras);
|
||
}
|
||
|
||
function marcarZona(zona) {
|
||
if (zonaArchivo === zona) return;
|
||
if (zonaArchivo) zonaArchivo.classList.remove("ed-dropzona-sobre");
|
||
zonaArchivo = zona;
|
||
if (zonaArchivo) zonaArchivo.classList.add("ed-dropzona-sobre");
|
||
}
|
||
|
||
function esArrastreArchivos(e) {
|
||
var tipos = e.dataTransfer && e.dataTransfer.types;
|
||
if (!tipos) return false;
|
||
for (var i = 0; i < tipos.length; i++) if (tipos[i] === "Files") return true;
|
||
return false;
|
||
}
|
||
|
||
function objetivoDrop(e) {
|
||
if (!dragItem || !dragItem.isConnected) return null;
|
||
var t = (e.target && e.target.closest) ? e.target.closest("[data-drag-ruta]") : null;
|
||
if (!t || t === dragItem) return null;
|
||
if (t.getAttribute("data-drag-ruta") !== dragItem.getAttribute("data-drag-ruta")) return null;
|
||
if (t.parentNode !== dragItem.parentNode) return null;
|
||
return t;
|
||
}
|
||
|
||
function hermanosMismaRuta(item) {
|
||
var ruta = item.getAttribute("data-drag-ruta");
|
||
var out = [];
|
||
var padre = item.parentNode;
|
||
for (var n = padre ? padre.firstElementChild : null; n; n = n.nextElementSibling) {
|
||
if (n.getAttribute && n.getAttribute("data-drag-ruta") === ruta) out.push(n);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function ejecutarReorden(objetivo) {
|
||
var ruta = dragItem.getAttribute("data-drag-ruta");
|
||
var hermanos = hermanosMismaRuta(dragItem);
|
||
var desde = hermanos.indexOf(dragItem);
|
||
var hasta = hermanos.indexOf(objetivo) + (dropTras ? 1 : 0);
|
||
if (desde < 0 || hasta === desde || hasta === desde + 1) return;
|
||
mutarArray(ruta, function (arr) {
|
||
if (desde >= arr.length) return null;
|
||
var movido = arr.splice(desde, 1)[0];
|
||
var destino = hasta > desde ? hasta - 1 : hasta;
|
||
if (destino < 0) destino = 0;
|
||
if (destino > arr.length) destino = arr.length;
|
||
arr.splice(destino, 0, movido);
|
||
return arr;
|
||
});
|
||
}
|
||
|
||
function soltarDrag() {
|
||
if (dragItem) {
|
||
dragItem.classList.remove("ed-arrastrando");
|
||
dragItem.classList.remove("ed-listo-drag");
|
||
dragItem.draggable = false;
|
||
}
|
||
dragItem = null;
|
||
limpiarIndicador();
|
||
marcarZona(null);
|
||
}
|
||
|
||
// El arrastre solo se habilita al pulsar sobre un asa (evita secuestrar
|
||
// la selección de texto de los inputs).
|
||
function alMouseDownPanel(e) {
|
||
if (e.button !== 0) return;
|
||
var asa = (e.target && e.target.closest) ? e.target.closest(".ed-asa") : null;
|
||
if (!asa || !panel.contains(asa)) return;
|
||
var item = asa.closest("[data-drag-ruta]");
|
||
if (item) {
|
||
item.draggable = true;
|
||
item.classList.add("ed-listo-drag");
|
||
}
|
||
}
|
||
|
||
// Si se suelta el asa sin arrastrar, se deshabilita de nuevo
|
||
function alMouseUpPanel() {
|
||
if (!panel) return;
|
||
var listos = panel.querySelectorAll(".ed-listo-drag");
|
||
for (var i = 0; i < listos.length; i++) {
|
||
listos[i].classList.remove("ed-listo-drag");
|
||
listos[i].draggable = false;
|
||
}
|
||
}
|
||
|
||
function alDragStartPanel(e) {
|
||
soltarDrag(); // limpia restos de un arrastre anterior
|
||
var t = e.target;
|
||
if (!t || !t.getAttribute || !t.classList) return;
|
||
if (!t.classList.contains("ed-listo-drag") || !t.getAttribute("data-drag-ruta")) {
|
||
e.preventDefault(); // solo se arrastra desde el asa
|
||
return;
|
||
}
|
||
dragItem = t;
|
||
t.classList.add("ed-arrastrando");
|
||
if (e.dataTransfer) {
|
||
e.dataTransfer.effectAllowed = "move";
|
||
try { e.dataTransfer.setData("text/plain", t.getAttribute("data-drag-ruta")); } catch (err) { /* sin data */ }
|
||
}
|
||
}
|
||
|
||
function alDragOverPanel(e) {
|
||
// 1) Arrastre de archivos del sistema: solo las dropzones lo aceptan
|
||
var archivos = esArrastreArchivos(e);
|
||
var zona = (e.target && e.target.closest) ? e.target.closest(".ed-dropzona") : null;
|
||
if (archivos) {
|
||
if (zona) {
|
||
e.preventDefault();
|
||
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
||
marcarZona(zona);
|
||
} else {
|
||
// fuera de una dropzone: se anula para que el navegador no abra el archivo
|
||
e.preventDefault();
|
||
if (e.dataTransfer) e.dataTransfer.dropEffect = "none";
|
||
marcarZona(null);
|
||
}
|
||
return;
|
||
}
|
||
// 2) Reordenar elementos propios
|
||
var objetivo = objetivoDrop(e);
|
||
if (!objetivo) { limpiarIndicador(); return; }
|
||
e.preventDefault();
|
||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||
var rect = objetivo.getBoundingClientRect();
|
||
marcarObjetivo(objetivo, e.clientY > rect.top + rect.height / 2);
|
||
}
|
||
|
||
function alDragLeavePanel(e) {
|
||
var zona = (e.target && e.target.closest) ? e.target.closest(".ed-dropzona") : null;
|
||
if (zona && zona === zonaArchivo) {
|
||
var dentro = e.relatedTarget && zona.contains(e.relatedTarget);
|
||
if (!dentro) marcarZona(null);
|
||
}
|
||
}
|
||
|
||
function alDropPanel(e) {
|
||
// 1) Archivos soltados en una dropzone → dataURL vía WAM.aDataUrl
|
||
var zona = (e.target && e.target.closest) ? e.target.closest(".ed-dropzona") : null;
|
||
if (zona) {
|
||
marcarZona(null);
|
||
var f = (e.dataTransfer && e.dataTransfer.files) ? e.dataTransfer.files[0] : null;
|
||
if (f) {
|
||
e.preventDefault();
|
||
procesarArchivo(zona.getAttribute("data-media-ruta"), f, zona);
|
||
}
|
||
return;
|
||
}
|
||
// 2) Archivos fuera de una dropzone: anular (que el navegador no los abra)
|
||
if (esArrastreArchivos(e)) { e.preventDefault(); return; }
|
||
// 3) Reordenar
|
||
var objetivo = objetivoDrop(e);
|
||
if (!objetivo) return;
|
||
e.preventDefault();
|
||
ejecutarReorden(objetivo);
|
||
soltarDrag();
|
||
}
|
||
|
||
function alDragEndPanel() {
|
||
soltarDrag();
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Delegación de eventos: TODO dentro de #panel-editor
|
||
------------------------------------------------------------------ */
|
||
|
||
function opAgregar(el) {
|
||
var ruta = el.getAttribute("data-ruta");
|
||
var max = Number(el.getAttribute("data-max")) || 0;
|
||
mutarArray(ruta, function (arr) {
|
||
if (max && arr.length >= max) { aviso("Máximo " + max, "error"); return null; }
|
||
var acc = el.getAttribute("data-acc");
|
||
if (acc === "add-resp") arr.push(mensajeAnidado());
|
||
else if (acc === "add-boton") arr.push(nuevoBoton("Opción " + (arr.length + 1), el.getAttribute("data-conurl") === "1"));
|
||
else if (acc === "add-sec") arr.push(nuevaSeccion());
|
||
else if (acc === "add-fila") arr.push(nuevaFila());
|
||
else if (acc === "add-card") arr.push(nuevaCard());
|
||
return arr;
|
||
});
|
||
}
|
||
|
||
function opEliminar(el) {
|
||
var ruta = el.getAttribute("data-ruta");
|
||
var idx = Number(el.getAttribute("data-idx"));
|
||
var min = Number(el.getAttribute("data-min")) || 0;
|
||
mutarArray(ruta, function (arr) {
|
||
if (arr.length <= min) { aviso("Debe quedar al menos " + min, "error"); return null; }
|
||
if (idx < 0 || idx >= arr.length) return null;
|
||
arr.splice(idx, 1);
|
||
return arr;
|
||
});
|
||
}
|
||
|
||
function alternarModoMedia(el) {
|
||
var media = el.closest ? el.closest(".ed-media") : null;
|
||
if (!media) return;
|
||
var esUrl = el.value === "url";
|
||
media.classList.toggle("ed-modo-url", esUrl);
|
||
media.classList.toggle("ed-modo-archivo", !esUrl);
|
||
}
|
||
|
||
function alClic(e) {
|
||
var el = e.target && e.target.closest ? e.target.closest("[data-acc]") : null;
|
||
if (!el || !panel.contains(el)) return;
|
||
switch (el.getAttribute("data-acc")) {
|
||
case "toggle": {
|
||
var t = el.closest(".ed-tarjeta");
|
||
if (!t) return;
|
||
var id = t.getAttribute("data-id");
|
||
abiertas[id] = !abiertas[id];
|
||
t.classList.toggle("ed-abierta", abiertas[id]);
|
||
el.setAttribute("aria-expanded", abiertas[id] ? "true" : "false");
|
||
return;
|
||
}
|
||
case "mover": WAM.moverMensaje(Number(el.getAttribute("data-i")), Number(el.getAttribute("data-delta"))); return;
|
||
case "dup": WAM.duplicarMensaje(Number(el.getAttribute("data-i"))); return;
|
||
case "borrar": WAM.borrarMensaje(Number(el.getAttribute("data-i"))); return;
|
||
case "add-resp":
|
||
case "add-boton":
|
||
case "add-sec":
|
||
case "add-fila":
|
||
case "add-card":
|
||
opAgregar(el); return;
|
||
case "del-resp":
|
||
case "del-boton":
|
||
case "del-sec":
|
||
case "del-fila":
|
||
case "del-card":
|
||
opEliminar(el); return;
|
||
case "avatar-quitar":
|
||
WAM.setCampo("cuenta.avatar", "");
|
||
refrescarAvatarVista();
|
||
return;
|
||
case "elegir-tipo":
|
||
seleccionarTipo(el.getAttribute("data-tipo"));
|
||
return;
|
||
case "header-tipo": {
|
||
var rh = el.getAttribute("data-ruta");
|
||
if (rh) {
|
||
WAM.setCampo(rh + ".headerTipo", el.getAttribute("data-valor") || "");
|
||
repintarDesde(el);
|
||
}
|
||
return;
|
||
}
|
||
case "fmt": {
|
||
var inp = inputDeCampo(el);
|
||
if (inp) envolverSeleccion(inp, el.getAttribute("data-fmt") || "*");
|
||
return;
|
||
}
|
||
case "var": {
|
||
var inp2 = inputDeCampo(el);
|
||
if (inp2) insertarTexto(inp2, "{{" + siguienteVariable(inp2) + "}}");
|
||
return;
|
||
}
|
||
case "drop-clic": {
|
||
var media = el.closest(".ed-media");
|
||
var inp3 = media ? media.querySelector('input[type="file"]') : null;
|
||
if (inp3) inp3.click();
|
||
return;
|
||
}
|
||
default: return;
|
||
}
|
||
}
|
||
|
||
function alEscribir(e) {
|
||
var el = e.target;
|
||
if (!el || !el.matches) return;
|
||
if (el.tagName === "SELECT" || el.type === "checkbox" || el.type === "file" || el.type === "radio") return;
|
||
var ruta = el.getAttribute("data-ruta");
|
||
if (ruta) {
|
||
WAM.setCampo(ruta, el.value);
|
||
actualizarContador(el);
|
||
}
|
||
}
|
||
|
||
function alCambiar(e) {
|
||
var el = e.target;
|
||
if (!el || !el.matches) return;
|
||
if (el.type === "file") { gestionarArchivo(el); return; }
|
||
if (el.type === "radio") {
|
||
if (el.getAttribute("data-rol") === "media-modo") alternarModoMedia(el);
|
||
return;
|
||
}
|
||
if (el.tagName === "SELECT") {
|
||
if (el.getAttribute("data-rol") === "tema") { aplicarTema(el.value); sincronizarSelectTema(); return; }
|
||
var ruta = el.getAttribute("data-ruta");
|
||
if (ruta) WAM.setCampo(ruta, el.value);
|
||
if (el.hasAttribute("data-repintar")) repintarDesde(el);
|
||
return;
|
||
}
|
||
if (el.type === "checkbox") {
|
||
if (el.getAttribute("data-rol") === "header-toggle") {
|
||
var rt = el.getAttribute("data-ruta");
|
||
WAM.setCampo(rt + ".headerTipo", el.checked ? "texto" : "");
|
||
repintarDesde(el);
|
||
return;
|
||
}
|
||
var r = el.getAttribute("data-ruta");
|
||
if (r) WAM.setCampo(r, el.checked);
|
||
return;
|
||
}
|
||
// Al salir de un input de texto: si era la URL del avatar, refresca la vista
|
||
if (el.getAttribute("data-ruta") === "cuenta.avatar") refrescarAvatarVista();
|
||
}
|
||
|
||
function enlazarPanel() {
|
||
if (panelEnlazado || !panel) return;
|
||
panelEnlazado = true;
|
||
panel.addEventListener("click", alClic);
|
||
panel.addEventListener("input", alEscribir);
|
||
panel.addEventListener("change", alCambiar);
|
||
// Drag & drop (reordenar + dropzones de archivos)
|
||
panel.addEventListener("mousedown", alMouseDownPanel);
|
||
panel.addEventListener("mouseup", alMouseUpPanel);
|
||
panel.addEventListener("dragstart", alDragStartPanel);
|
||
panel.addEventListener("dragover", alDragOverPanel);
|
||
panel.addEventListener("dragleave", alDragLeavePanel);
|
||
panel.addEventListener("drop", alDropPanel);
|
||
panel.addEventListener("dragend", alDragEndPanel);
|
||
}
|
||
|
||
// Mantiene el selector de tema del panel sincronizado con #btn-tema (dueño D)
|
||
function observarTema() {
|
||
if (obsTema || typeof MutationObserver === "undefined") return;
|
||
obsTema = new MutationObserver(sincronizarSelectTema);
|
||
obsTema.observe(document.body, { attributes: true, attributeFilter: ["data-tema"] });
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
Contrato: WAM.renderEditor() — formulario de cuenta + tarjetas
|
||
------------------------------------------------------------------ */
|
||
function asegurarPanel() {
|
||
var p = document.getElementById("panel-editor");
|
||
if (!p) {
|
||
p = document.createElement("aside");
|
||
p.id = "panel-editor";
|
||
(document.getElementById("app") || document.body).appendChild(p);
|
||
}
|
||
return p;
|
||
}
|
||
|
||
WAM.renderEditor = function () {
|
||
panel = asegurarPanel();
|
||
enlazarPanel();
|
||
var cuenta = document.getElementById("editor-cuenta");
|
||
if (!cuenta) {
|
||
// Reserva: si A no está, el cromado lo pone .ed-seccion
|
||
cuenta = document.createElement("section");
|
||
cuenta.id = "editor-cuenta";
|
||
cuenta.className = "ed-seccion";
|
||
panel.insertBefore(cuenta, panel.firstChild);
|
||
}
|
||
var lista = listaNodo();
|
||
if (!lista) {
|
||
lista = document.createElement("ol");
|
||
lista.id = "lista-mensajes";
|
||
lista.className = "ed-seccion ed-lista";
|
||
panel.appendChild(lista);
|
||
}
|
||
renderCuenta(cuenta);
|
||
prepararAlta(lista);
|
||
observarTema();
|
||
WAM.renderTarjetas();
|
||
};
|
||
})();
|