/* =====================================================================
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: '',
image: '',
video: '',
audio: '',
document: '',
sticker: '',
location: '',
contact: '',
template: '',
buttons: '',
list: '',
cta_url: '',
carousel: ''
};
var GRIP = '';
var ICONO_SUBIR = '';
// 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 =
'";
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 (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 +
------------------------------------------------------------------ */
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, 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
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: "ana@cafeaurora.es" }),
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();
};
})();