Editor y toolbar: jerarquía, tarjetas legibles, foco seguro y autoPulsar estable
- Toolbar: «Nueva demo» primaria junto a la marca (N); Tema como icono; «Volver al ejemplo Café Aurora» (peligro) solo dentro del menú «⋯»; etiquetas de Nueva demo y Presentación visibles entre 900 y 1100 px. - Tarjetas: flecha de dirección en el número (↙/↗), badge con icono (solo icono en editor estrecho), resumen en 2.ª línea al abrir, hueco de ↑/↓ reservado (sin saltos al pasar el ratón), container queries. - Borrar (mensaje, botón, fila, tarjeta, respuesta) nunca deja el foco en otro botón ✕. - autoPulsar sigue al elemento al mover/borrar/arrastrar; opción inexistente visible y aviso; casilla «Se pulsa sola» por elemento, chip «▶ auto» y select arriba del formulario. - Deshacer/rehacer conservan el tema; el tema marca «Guardando…». - Escenarios: siempre punto de deshacer, un único aviso con acción y foco al nombre de la cuenta; chips de tipos en el diálogo Demos, rejilla de 4 columnas en >= 1280 y sin nombre precargado. - Paleta sin estado seleccionado, «+» en hover/foco, chip de destino y «+ Otro» solo tras el primer alta. - Avisos abajo al centro sobre la vista previa; tel: válido en plantillas (máx. 1); interruptor «Mostrar cabecera» fijo (role=switch). - Teclado: enlace «Saltar a la vista previa» y G / F6; atajos con ⌘ en Mac; etiqueta «Velocidad»; «Repetir desde el inicio». - Demo vacía: herramientas ocultas y pista dentro del chat. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9a5f21badf
commit
3509a77d62
+209
-33
@@ -110,6 +110,7 @@
|
||||
var ultimaAbierta = null; // id top-level abierto más reciente (posición de alta)
|
||||
var insertarTras = null; // id tras el que insertar (botón «+» de una tarjeta)
|
||||
var ultimoTipo = "text";
|
||||
var yaAnadido = false; // ¿se añadió algo desde la paleta en esta sesión?
|
||||
var ultimoEnlazado = null; // id resaltado en la vista previa
|
||||
var tResalte = 0;
|
||||
var contadorIds = 0; // ids únicos para <label for>
|
||||
@@ -244,9 +245,9 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Aviso flotante (toast) abajo a la izquierda, fuera de la zona con
|
||||
scroll del panel. opts: { tipo: "error", ms, accion: { texto, fn } }.
|
||||
Devuelve una función que lo cierra. */
|
||||
/* Aviso flotante (toast) abajo, centrado sobre la vista previa (fuera de
|
||||
la lista de tarjetas). opts: { tipo: "error", ms, accion: { texto, fn } }.
|
||||
Solo hay un aviso con acción a la vez. Devuelve una función que lo cierra. */
|
||||
function contenedorToasts() {
|
||||
var c = document.getElementById("ed-toasts");
|
||||
if (!c) {
|
||||
@@ -280,14 +281,24 @@
|
||||
var x = h("button", { type: "button", class: "ed-toast-cerrar", "aria-label": "Cerrar aviso", text: "×" });
|
||||
x.addEventListener("click", cerrar);
|
||||
t.appendChild(x);
|
||||
// Como mucho 2 a la vista: primero se retiran los que no tienen acción
|
||||
var hijos = Array.prototype.slice.call(c.children);
|
||||
// Un aviso con acción nuevo invalida los anteriores con acción: su
|
||||
// «Deshacer» ya no desharía lo que dice (se deshace de uno en uno).
|
||||
if (opts.accion && typeof opts.accion.fn === "function") {
|
||||
hijos = hijos.filter(function (x) {
|
||||
if (!x.querySelector(".ed-toast-accion")) return true;
|
||||
if (typeof x.__cerrar === "function") x.__cerrar(); else if (x.parentNode) x.parentNode.removeChild(x);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
// Como mucho 2 a la vista: primero se retiran los que no tienen acción
|
||||
var sinAccion = hijos.filter(function (x) { return !x.querySelector(".ed-toast-accion"); });
|
||||
while (hijos.length >= 2) {
|
||||
var quitar = sinAccion.length ? sinAccion.shift() : hijos[0];
|
||||
hijos.splice(hijos.indexOf(quitar), 1);
|
||||
if (quitar.parentNode) quitar.parentNode.removeChild(quitar);
|
||||
}
|
||||
t.__cerrar = cerrar;
|
||||
c.appendChild(t);
|
||||
timer = setTimeout(cerrar, opts.ms || (esError ? 7000 : 3000));
|
||||
return cerrar;
|
||||
@@ -500,14 +511,51 @@
|
||||
if (car2) car2.scrollLeft = sl;
|
||||
}
|
||||
|
||||
// Reemplaza un array completo vía setCampo (mutación contractual) y repinta tarjetas
|
||||
/* --- autoPulsar (SPEC §1): índice del elemento que «Reproducir demo»
|
||||
pulsa solo. Botones (template/buttons) y tarjetas (carousel) usan su
|
||||
índice en el array; las listas, el índice aplanado de todas sus filas. */
|
||||
function pulsablesDe(m) {
|
||||
if (!m || typeof m !== "object") return [];
|
||||
if (m.tipo === "list") {
|
||||
var out = [];
|
||||
(m.secciones || []).forEach(function (s) { ((s && s.filas) || []).forEach(function (f) { out.push(f); }); });
|
||||
return out;
|
||||
}
|
||||
if (m.tipo === "carousel") return Array.isArray(m.cards) ? m.cards : [];
|
||||
if (m.tipo === "template" || m.tipo === "buttons") return Array.isArray(m.botones) ? m.botones : [];
|
||||
return [];
|
||||
}
|
||||
|
||||
function tieneAuto(m) { return !!m && typeof m.autoPulsar === "number" && isFinite(m.autoPulsar) && m.autoPulsar >= 0; }
|
||||
|
||||
// Ruta del mensaje dueño de una lista de botones / cards / secciones / filas
|
||||
function rutaMensajeDe(rutaArr) {
|
||||
var r = String(rutaArr || "");
|
||||
var m = /^(.*)\.secciones\.\d+\.filas$/.exec(r) || /^(.*)\.(?:botones|cards|secciones)$/.exec(r);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// Reemplaza un array completo vía setCampo (mutación contractual) y repinta tarjetas.
|
||||
// Si el array es de botones / cards / secciones / filas, autoPulsar sigue
|
||||
// al elemento pulsado (se mueve con él; se quita si se borró).
|
||||
function mutarArray(ruta, fn) {
|
||||
var arr = rutaGet(ruta);
|
||||
if (!Array.isArray(arr)) { aviso("Estructura no encontrada: " + ruta, "error"); return null; }
|
||||
var copia = fn(arr.slice());
|
||||
if (!copia) return null; // validación rechazada (fn ya avisó)
|
||||
snap();
|
||||
var rutaMsg = rutaMensajeDe(ruta);
|
||||
var dueno = rutaMsg ? rutaGet(rutaMsg) : null;
|
||||
var pulsado = tieneAuto(dueno) ? pulsablesDe(dueno)[dueno.autoPulsar] : undefined;
|
||||
WAM.setCampo(ruta, copia);
|
||||
if (pulsado !== undefined) {
|
||||
var nuevo = pulsablesDe(rutaGet(rutaMsg)).indexOf(pulsado);
|
||||
if (nuevo !== dueno.autoPulsar) {
|
||||
var padre = rutaGet(rutaMsg);
|
||||
if (nuevo < 0) delete padre.autoPulsar; else padre.autoPulsar = nuevo;
|
||||
WAM.setCampo(rutaMsg, padre);
|
||||
}
|
||||
}
|
||||
WAM.renderTarjetas();
|
||||
return copia;
|
||||
}
|
||||
@@ -816,7 +864,8 @@
|
||||
snap();
|
||||
WAM.setCampo("mensajes", copia);
|
||||
WAM.renderTarjetas();
|
||||
// Foco a la tarjeta vecina (la siguiente o, si era la última, la anterior)
|
||||
// Foco a la CABECERA de la tarjeta vecina (la siguiente o, si era la
|
||||
// última, la anterior), nunca a su botón borrar; sin tarjetas, la paleta.
|
||||
var vecino = copia[i] || copia[i - 1];
|
||||
if (vecino) enfocarTarjeta(vecino.id, false);
|
||||
else { var pal = paletaFoco(); if (pal) pal.focus(); }
|
||||
@@ -990,6 +1039,11 @@
|
||||
function largo(v) { return String(v === undefined || v === null ? "" : v).length; }
|
||||
function vacioTxt(v) { return !String(v === undefined || v === null ? "" : v).trim(); }
|
||||
function urlValida(v) { return /^https?:\/\/[^\s/$.?#][^\s]*$/i.test(String(v || "").trim()); }
|
||||
// Botón de llamada de plantilla: tel: con al menos 3 dígitos (espacios, guiones y paréntesis permitidos)
|
||||
function telValido(v) {
|
||||
var s = String(v || "").trim();
|
||||
return /^tel:\+?[\d\s().-]+$/i.test(s) && (s.replace(/\D/g, "").length >= 3);
|
||||
}
|
||||
|
||||
function validar(m, prefijo, profundidad) {
|
||||
var av = [];
|
||||
@@ -1032,12 +1086,16 @@
|
||||
lim(m.footer, 60, "El pie");
|
||||
var bt = m.botones || [];
|
||||
if (bt.length > LIMITES.botonesPlantilla) add("Máximo " + LIMITES.botonesPlantilla + " botones");
|
||||
var conUrl = bt.filter(function (b) { return b && !vacioTxt(b.url); }).length;
|
||||
// tel:+… es el botón de llamada (PHONE_NUMBER de la API): máximo 1 y
|
||||
// no cuenta entre los 2 de URL
|
||||
var conTel = bt.filter(function (b) { return b && telValido(b.url); }).length;
|
||||
var conUrl = bt.filter(function (b) { return b && !vacioTxt(b.url) && !telValido(b.url); }).length;
|
||||
if (conUrl > 2) add("Máximo 2 botones con URL (" + conUrl + ")");
|
||||
if (conTel > 1) add("Máximo 1 botón de llamada tel: (" + conTel + ")");
|
||||
bt.forEach(function (b, i) {
|
||||
if (vacioTxt(b.texto)) add("Botón " + (i + 1) + ": sin texto");
|
||||
else lim(b.texto, 25, "Botón " + (i + 1));
|
||||
if (!vacioTxt(b.url) && !urlValida(b.url)) add("Botón " + (i + 1) + ": URL no válida (usa https://…)");
|
||||
if (!vacioTxt(b.url) && !urlValida(b.url) && !telValido(b.url)) add("Botón " + (i + 1) + ": URL no válida (usa https://… o tel:+34…)");
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -1110,6 +1168,13 @@
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
// «Se pulsa sola» apunta a una opción que ya no existe o que abre una URL
|
||||
if (tieneAuto(m) && ["template", "buttons", "list", "carousel"].indexOf(m.tipo) >= 0) {
|
||||
var objAuto = pulsablesDe(m)[m.autoPulsar];
|
||||
var nAuto = "«Se pulsa sola en la demo»: la opción " + (m.autoPulsar + 1);
|
||||
if (!objAuto) add(nAuto + " ya no existe (la demo no pulsará nada)");
|
||||
else if (!vacioTxt(objAuto.url)) add(nAuto + " abre una URL y la demo no la pulsará");
|
||||
}
|
||||
if ((profundidad || 0) < 6) {
|
||||
origenesRespuesta(m).forEach(function (o) {
|
||||
o.respuestas.forEach(function (r) {
|
||||
@@ -1150,6 +1215,16 @@
|
||||
"aria-label": av.length + (av.length === 1 ? " aviso" : " avisos")
|
||||
}, [h("span", { "aria-hidden": "true", text: "⚠ " + av.length })]));
|
||||
}
|
||||
// ▶ auto: la demo pulsa sola una opción de este mensaje
|
||||
if (tieneAuto(m)) {
|
||||
var objA = pulsablesDe(m)[m.autoPulsar];
|
||||
if (objA && vacioTxt(objA.url)) {
|
||||
var etA = objA.texto || objA.titulo || objA.textoBoton || ("opción " + (m.autoPulsar + 1));
|
||||
var tA = "En «Reproducir demo» se pulsa sola: «" + etA + "»";
|
||||
frag.appendChild(h("span", { class: "ed-chip ed-chip-auto", title: tA, "aria-label": tA },
|
||||
[h("span", { "aria-hidden": "true", text: "▶ auto" })]));
|
||||
}
|
||||
}
|
||||
var f = contarFlujos(m);
|
||||
if (f.total) {
|
||||
var txt = "↳ " + f.con + (f.sin ? "/" + f.total : "");
|
||||
@@ -1188,6 +1263,8 @@
|
||||
if (!cab) return;
|
||||
var res = cab.querySelector(".ed-resumen");
|
||||
if (res) res.textContent = resumenDe(m);
|
||||
var cp = cab.querySelector(".ed-cab-principal");
|
||||
if (cp) cp.title = (t.classList.contains("ed-abierta") ? "Plegar" : "Desplegar") + " · " + resumenDe(m);
|
||||
var chips = cab.querySelector(".ed-chips");
|
||||
if (chips) {
|
||||
while (chips.firstChild) chips.removeChild(chips.firstChild);
|
||||
@@ -1477,7 +1554,8 @@
|
||||
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,
|
||||
o.conUrl ? edCampo("URL (si la pones, abre el enlace en vez de responder; tel:+34… llama)", ruta + "." + i + ".url", b.url, { placeholder: "https://…" }) : null,
|
||||
edCheckAuto(rutaMensajeDe(ruta), i, b),
|
||||
edRespuestas(ruta + "." + i + ".respuestas", b.respuestas, o.raiz)
|
||||
]);
|
||||
});
|
||||
@@ -1503,9 +1581,13 @@
|
||||
|
||||
function edSeccionesEd(ruta, secciones, raiz) {
|
||||
var lista = Array.isArray(secciones) ? secciones : [];
|
||||
var rutaMsgLista = rutaMensajeDe(ruta);
|
||||
var base = 0; // índice aplanado de la primera fila de la sección (autoPulsar)
|
||||
var hijos = lista.map(function (s, si) {
|
||||
var filas = Array.isArray(s.filas) ? s.filas : [];
|
||||
var rutaFilas = ruta + "." + si + ".filas";
|
||||
var offset = base;
|
||||
base += filas.length;
|
||||
var filaEls = filas.map(function (f, fi) {
|
||||
return h("div", { class: "ed-item", "data-drag-ruta": rutaFilas, "data-idx": fi }, [
|
||||
h("div", { class: "ed-sub-cab" }, [
|
||||
@@ -1516,6 +1598,7 @@
|
||||
]),
|
||||
edCampo("Título", rutaFilas + "." + fi + ".titulo", f.titulo, { limite: 24 }),
|
||||
edCampo("Descripción", rutaFilas + "." + fi + ".descripcion", f.descripcion, { limite: 72 }),
|
||||
edCheckAuto(rutaMsgLista, offset + fi, f),
|
||||
edRespuestas(rutaFilas + "." + fi + ".respuestas", f.respuestas, raiz)
|
||||
]);
|
||||
});
|
||||
@@ -1572,6 +1655,7 @@
|
||||
edCampo("Texto del botón", ruta + "." + ci + ".textoBoton", c.textoBoton, { limite: 20 }),
|
||||
edCampo("URL (opcional)", ruta + "." + ci + ".url", c.url, { placeholder: "https://…" })
|
||||
]),
|
||||
edCheckAuto(rutaMensajeDe(ruta), ci, c),
|
||||
edRespuestas(ruta + "." + ci + ".respuestas", c.respuestas, raiz)
|
||||
]);
|
||||
});
|
||||
@@ -1598,10 +1682,14 @@
|
||||
var caja = h("div", { class: "ed-header-bloque" });
|
||||
caja.appendChild(h("div", { class: "ed-sub-cab" }, [
|
||||
h("span", { class: "ed-sub-titulo", text: "Cabecera" }),
|
||||
// Etiqueta fija (no se invierte con el estado): el interruptor dice si se muestra
|
||||
h("label", { class: "ed-switch" }, [
|
||||
h("input", { type: "checkbox", "data-rol": "header-toggle", "data-ruta": ruta, checked: tipo !== "" }),
|
||||
h("input", {
|
||||
type: "checkbox", role: "switch", "aria-checked": tipo !== "" ? "true" : "false",
|
||||
"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" })
|
||||
h("span", { class: "ed-switch-texto", text: "Mostrar cabecera" })
|
||||
])
|
||||
]));
|
||||
if (tipo !== "") {
|
||||
@@ -1666,12 +1754,34 @@
|
||||
});
|
||||
}
|
||||
var v = (m.autoPulsar === undefined || m.autoPulsar === null) ? "" : String(m.autoPulsar);
|
||||
return edSelect("En «Reproducir demo», pulsar sola", ruta + ".autoPulsar", v, ops, false);
|
||||
// Valor guardado que ya no corresponde a ninguna opción (borrado, o con URL):
|
||||
// se muestra tal cual para que no parezca «No» en silencio (y validar() avisa)
|
||||
if (v !== "" && !ops.some(function (op) { return op[0] === v; })) {
|
||||
ops.push([v, "⚠ Opción inexistente (" + (Number(v) + 1) + ")"]);
|
||||
}
|
||||
var campo = edSelect("En «Reproducir demo», se pulsa sola", ruta + ".autoPulsar", v, ops, true);
|
||||
campo.classList.add("ed-campo-auto");
|
||||
return campo;
|
||||
}
|
||||
|
||||
// Casilla «Se pulsa sola en la demo» dentro de cada botón / fila / tarjeta
|
||||
// (exclusiva dentro del mensaje: marcar una desmarca la anterior)
|
||||
function edCheckAuto(rutaMsg, indice, elemento) {
|
||||
var m = rutaGet(rutaMsg);
|
||||
if (!m || !elemento || !vacioTxt(elemento.url)) return null; // con URL abre el enlace
|
||||
var marcado = tieneAuto(m) && m.autoPulsar === indice;
|
||||
return h("label", { class: "ed-check ed-check-auto", title: "En «Reproducir demo» se pulsa sola 1 s después de aparecer" }, [
|
||||
h("input", { type: "checkbox", "data-rol": "auto-item", "data-ruta": rutaMsg + ".autoPulsar", "data-idx": indice, checked: marcado }),
|
||||
h("span", { text: "Se pulsa sola en la demo" })
|
||||
]);
|
||||
}
|
||||
|
||||
function edFormTipo(m, ruta, raiz) {
|
||||
var frag = document.createDocumentFragment();
|
||||
var ap = function (n) { if (n) frag.appendChild(n); };
|
||||
// Clave para las demos: arriba del todo, justo bajo Dirección / Hora
|
||||
// (cada botón / fila / tarjeta tiene además su casilla «Se pulsa sola»)
|
||||
if (["template", "buttons", "list", "carousel"].indexOf(m.tipo) >= 0) ap(edAutoPulsar(ruta, m));
|
||||
switch (m.tipo) {
|
||||
case "text":
|
||||
ap(edTextarea("Texto", ruta + ".texto", m.texto, { limite: 4096, rows: 3, formato: true, placeholder: "Hola 👋" }));
|
||||
@@ -1724,7 +1834,6 @@
|
||||
max: LIMITES.botonesPlantilla, maxTexto: 25, conUrl: true,
|
||||
etiqueta: "Botón", ejemplo: "Confirmar", raiz: raiz
|
||||
}));
|
||||
ap(edAutoPulsar(ruta, m));
|
||||
break;
|
||||
}
|
||||
case "buttons":
|
||||
@@ -1735,7 +1844,6 @@
|
||||
max: LIMITES.botonesRapidos, maxTexto: 20, conUrl: false,
|
||||
etiqueta: "Botón rápido", ejemplo: "Horarios", raiz: raiz
|
||||
}));
|
||||
ap(edAutoPulsar(ruta, m));
|
||||
break;
|
||||
case "list":
|
||||
ap(edCampo("Cabecera", ruta + ".header", m.header, { limite: 60 }));
|
||||
@@ -1743,7 +1851,6 @@
|
||||
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));
|
||||
ap(edAutoPulsar(ruta, m));
|
||||
break;
|
||||
case "cta_url":
|
||||
ap(edMedia("Cabecera («» = sin cabecera)", ruta + ".headerMedia", m.headerMedia, { aceptar: "image/*", textoDrop: "Clic o arrastra una imagen aquí" }));
|
||||
@@ -1757,7 +1864,6 @@
|
||||
case "carousel":
|
||||
ap(edTextarea("Cuerpo (opcional)", ruta + ".body", m.body, { limite: 1024, rows: 2 }));
|
||||
ap(edCardsEd(ruta + ".cards", m.cards, raiz));
|
||||
ap(edAutoPulsar(ruta, m));
|
||||
break;
|
||||
default:
|
||||
ap(h("p", { class: "ed-nota", text: "Tipo desconocido: " + m.tipo }));
|
||||
@@ -1775,9 +1881,10 @@
|
||||
var total = o.anidado ? (o.total || 1) : ((WAM.estado && WAM.estado.mensajes) || []).length;
|
||||
var idx = o.anidado ? o.indice : o.i;
|
||||
if (!o.anidado) {
|
||||
acciones.push(btnMini("+", "Insertar un mensaje debajo", "insertar-debajo", { "data-i": o.i, "data-id": m.id }));
|
||||
// ↑ ↓ primero: su hueco reservado (ocultas en reposo) queda como margen
|
||||
acciones.push(btnMini("↑", "Subir", "mover", { "data-i": o.i, "data-delta": -1, disabled: idx <= 0 || null }, false, "ed-mov"));
|
||||
acciones.push(btnMini("↓", "Bajar", "mover", { "data-i": o.i, "data-delta": 1, disabled: idx >= total - 1 || null }, false, "ed-mov"));
|
||||
acciones.push(btnMini("+", "Insertar un mensaje debajo", "insertar-debajo", { "data-i": o.i, "data-id": m.id }));
|
||||
acciones.push(btnMini("⧉", "Duplicar", "dup", { "data-i": o.i }));
|
||||
acciones.push(btnMini("✕", "Borrar mensaje (se puede deshacer)", "borrar", { "data-i": o.i }, true));
|
||||
} else {
|
||||
@@ -1804,12 +1911,19 @@
|
||||
var cab = h("button", {
|
||||
type: "button", class: "ed-cab-principal", "data-acc": "toggle",
|
||||
"aria-expanded": abierta ? "true" : "false",
|
||||
title: abierta ? "Plegar" : "Desplegar"
|
||||
title: (abierta ? "Plegar" : "Desplegar") + " · " + resumenDe(m)
|
||||
}, [
|
||||
// Número + flecha de dirección (↙ entrada / ↗ salida): no depende solo del color
|
||||
o.anidado ? h("span", { class: "ed-num ed-num-resp", text: "↳" })
|
||||
: h("span", { class: "ed-num ed-num-" + (esOut ? "out" : "in"), title: esOut ? "Salida (enviado)" : "Entrada (recibido)", text: String(o.i + 1) }),
|
||||
o.anidado ? null : h("span", { class: "ed-dir ed-dir-" + (esOut ? "out" : "in"), text: esOut ? "Salida" : "Entrada" }),
|
||||
h("span", { class: "ed-badge", text: CORTO[m.tipo] || etiquetaTipo(m.tipo) }),
|
||||
: h("span", { class: "ed-num ed-num-" + (esOut ? "out" : "in"), title: esOut ? "Salida (enviado)" : "Entrada (recibido)" }, [
|
||||
h("span", { class: "ed-num-dir", "aria-hidden": "true", text: esOut ? "↗" : "↙" }),
|
||||
document.createTextNode(String(o.i + 1)),
|
||||
h("span", { class: "wam-sr", text: esOut ? " · salida" : " · entrada" })
|
||||
]),
|
||||
h("span", { class: "ed-badge", title: etiquetaTipo(m.tipo) }, [
|
||||
svgIcono(ICONOS_TIPO[m.tipo] || ""),
|
||||
h("span", { class: "ed-badge-txt", text: CORTO[m.tipo] || etiquetaTipo(m.tipo) })
|
||||
]),
|
||||
h("span", { class: "ed-resumen", text: resumenDe(m) }),
|
||||
h("span", { class: "ed-chips" }, o.anidado ? null : chipsDe(m))
|
||||
]);
|
||||
@@ -1853,6 +1967,11 @@
|
||||
pintarDestinoAlta();
|
||||
actualizarAvisosGlobal();
|
||||
actualizarResumenCuenta();
|
||||
// Demo vacía: sin herramientas de lista (no harían nada) y pista en el teléfono
|
||||
var herr = document.getElementById("ed-lista-herr") || document.querySelector(".ed-lista-herr");
|
||||
if (herr) herr.hidden = !mensajes.length;
|
||||
if (mensajes.length) document.body.removeAttribute("data-wam-vacia");
|
||||
else document.body.setAttribute("data-wam-vacia", "");
|
||||
if (!mensajes.length) {
|
||||
lista.appendChild(h(etiquetaItem, { class: "ed-vacio" }, [
|
||||
h("p", { class: "ed-vacio-titulo", text: "Sin mensajes todavía" }),
|
||||
@@ -1887,8 +2006,13 @@
|
||||
return f;
|
||||
}
|
||||
|
||||
// Botones que borran: tras usarlos el foco NUNCA vuelve a su gemelo (un
|
||||
// segundo Intro/Espacio borraría otro elemento); lo recoloca quien borra.
|
||||
function esAccionBorrar(acc) { return acc === "borrar" || /^del-/.test(String(acc || "")); }
|
||||
|
||||
function restaurarFoco(cont, f) {
|
||||
if (!f || !cont) return;
|
||||
if (esAccionBorrar(f.acc)) return;
|
||||
var sel = "";
|
||||
if (f.ruta) sel += '[data-ruta="' + cssEsc(f.ruta) + '"]';
|
||||
if (f.acc) sel += '[data-acc="' + cssEsc(f.acc) + '"]';
|
||||
@@ -2017,17 +2141,18 @@
|
||||
var pal = document.getElementById("nuevo-tipo");
|
||||
if (pal && pal.tagName !== "SELECT") {
|
||||
pal.setAttribute("data-valor", tipo);
|
||||
// Sin estado «seleccionado» visible: cada clic AÑADE (no es un selector).
|
||||
// Solo se conserva la parada de Tab (roving tabindex) en el último usado.
|
||||
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].tabIndex = activo ? 0 : -1;
|
||||
hijos[i].tabIndex = hijos[i].getAttribute("data-tipo") === tipo ? 0 : -1;
|
||||
}
|
||||
}
|
||||
var btn = document.getElementById("btn-anadir");
|
||||
if (btn) {
|
||||
btn.textContent = "+ Otro «" + etiquetaTipo(tipo) + "»";
|
||||
btn.title = "Añade otro mensaje de tipo " + etiquetaTipo(tipo);
|
||||
btn.title = "Repite el último tipo añadido: " + etiquetaTipo(tipo);
|
||||
btn.hidden = !yaAnadido;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2049,8 +2174,13 @@
|
||||
"data-acc": "anadir-tipo",
|
||||
"data-tipo": tipo,
|
||||
tabindex: "-1",
|
||||
title: "Añadir un mensaje de tipo " + etiquetaTipo(tipo)
|
||||
}, [svgIcono(ICONOS_TIPO[tipo] || ""), h("span", { class: "ed-tipo-card-e", text: CORTO[tipo] || etiquetaTipo(tipo) })]));
|
||||
title: "Añadir un mensaje de tipo " + etiquetaTipo(tipo),
|
||||
"aria-label": "Añadir " + etiquetaTipo(tipo)
|
||||
}, [
|
||||
svgIcono(ICONOS_TIPO[tipo] || ""),
|
||||
h("span", { class: "ed-tipo-card-e", text: CORTO[tipo] || etiquetaTipo(tipo) }),
|
||||
h("span", { class: "ed-tipo-mas", "aria-hidden": "true", text: "+" })
|
||||
]));
|
||||
});
|
||||
cont.appendChild(fila);
|
||||
});
|
||||
@@ -2102,15 +2232,31 @@
|
||||
while (el.firstChild) el.removeChild(el.firstChild);
|
||||
var ms = (WAM.estado && WAM.estado.mensajes) || [];
|
||||
var pos = posicionAlta();
|
||||
var previo = el.getAttribute("data-pos");
|
||||
var clave = ms.length ? String(pos >= ms.length ? "fin" : pos) : "vacio";
|
||||
el.setAttribute("data-pos", clave);
|
||||
// Chip visible del destino: «al final» o «debajo del n.º N ×» (× = al final)
|
||||
el.classList.toggle("ed-destino-chip", !!ms.length);
|
||||
el.classList.toggle("ed-destino-dentro", !!ms.length && pos < ms.length);
|
||||
if (pos >= ms.length) {
|
||||
el.appendChild(document.createTextNode(ms.length ? "Se añade al final" : "Elige un tipo para empezar"));
|
||||
return;
|
||||
} else {
|
||||
el.appendChild(document.createTextNode("Se inserta debajo del n.º " + pos));
|
||||
el.appendChild(h("button", {
|
||||
type: "button", class: "ed-destino-quitar", "data-acc": "destino-final",
|
||||
title: "Añadir al final", "aria-label": "Añadir al final", text: "×"
|
||||
}));
|
||||
}
|
||||
// Aviso visual breve cuando el destino cambia solo (al abrir otra tarjeta)
|
||||
if (previo !== null && previo !== clave && ms.length && !reducirMovimiento()) {
|
||||
el.classList.remove("ed-destino-cambio");
|
||||
void el.offsetWidth;
|
||||
el.classList.add("ed-destino-cambio");
|
||||
}
|
||||
el.appendChild(document.createTextNode("Se inserta debajo del n.º " + pos + " "));
|
||||
el.appendChild(h("button", { type: "button", class: "ed-enlace", "data-acc": "destino-final", text: "Al final" }));
|
||||
}
|
||||
|
||||
function anadirDesdePaleta(tipo) {
|
||||
yaAnadido = true; // desde ahora se ofrece «+ Otro «X»» (repetir el último)
|
||||
seleccionarTipo(tipo);
|
||||
var pos = posicionAlta();
|
||||
insertarTras = null;
|
||||
@@ -2239,7 +2385,7 @@
|
||||
snap();
|
||||
WAM.setCampo("mensajes", copia);
|
||||
WAM.renderTarjetas();
|
||||
WAM.toast("Horas repartidas desde las " + inicio + " (+1 min por mensaje)",
|
||||
WAM.toast("Horas repartidas desde " + inicio,
|
||||
typeof WAM.deshacer === "function" ? { accion: { texto: "Deshacer", fn: WAM.deshacer } } : null);
|
||||
}
|
||||
|
||||
@@ -2509,12 +2655,29 @@
|
||||
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) {
|
||||
var res = 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;
|
||||
});
|
||||
if (res) enfocarTrasBorrar(ruta, idx, res.length);
|
||||
}
|
||||
|
||||
/* Tras quitar un elemento: foco a la cabecera / primer campo del que
|
||||
ocupa su lugar (o del anterior); si la lista quedó vacía, a su botón
|
||||
«+ Añadir». Nunca a otro botón ✕. */
|
||||
function enfocarTrasBorrar(ruta, idx, n) {
|
||||
var lista = listaNodo();
|
||||
if (!lista) return;
|
||||
var j = idx < n ? idx : n - 1;
|
||||
var r = cssEsc(ruta);
|
||||
var it = j >= 0 ? lista.querySelector('[data-drag-ruta="' + r + '"][data-idx="' + j + '"], [data-mov-ruta="' + r + '"][data-idx="' + j + '"]') : null;
|
||||
var foco = it && (it.querySelector(".ed-cab-principal") || it.querySelector("input.ed-input, textarea"));
|
||||
if (!foco) foco = lista.querySelector('[data-acc^="add-"][data-ruta="' + r + '"]');
|
||||
if (!foco) return;
|
||||
try { foco.focus({ preventScroll: true }); } catch (e) { foco.focus(); }
|
||||
if (foco.scrollIntoView) foco.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
function alternarModoMedia(el) {
|
||||
@@ -2536,7 +2699,8 @@
|
||||
abiertas[id] = !abiertas[id];
|
||||
t.classList.toggle("ed-abierta", abiertas[id]);
|
||||
el.setAttribute("aria-expanded", abiertas[id] ? "true" : "false");
|
||||
el.title = abiertas[id] ? "Plegar" : "Desplegar";
|
||||
var resT = el.querySelector(".ed-resumen");
|
||||
el.title = (abiertas[id] ? "Plegar" : "Desplegar") + (resT ? " · " + resT.textContent : "");
|
||||
var raizT = t.getAttribute("data-raiz") || id;
|
||||
if (!t.classList.contains("ed-anidada")) {
|
||||
if (abiertas[id]) ultimaAbierta = id;
|
||||
@@ -2719,6 +2883,18 @@
|
||||
}
|
||||
if (el.type === "checkbox") {
|
||||
snap();
|
||||
if (el.getAttribute("data-rol") === "auto-item") {
|
||||
// Exclusiva dentro del mensaje: marcar fija autoPulsar; desmarcar lo quita
|
||||
var rutaA = String(el.getAttribute("data-ruta") || "").replace(/\.autoPulsar$/, "");
|
||||
var msgA = rutaGet(rutaA);
|
||||
if (msgA && typeof msgA === "object") {
|
||||
if (el.checked) msgA.autoPulsar = Number(el.getAttribute("data-idx")) || 0;
|
||||
else delete msgA.autoPulsar;
|
||||
WAM.setCampo(rutaA, msgA);
|
||||
}
|
||||
repintarDesde(el);
|
||||
return;
|
||||
}
|
||||
if (el.getAttribute("data-rol") === "header-toggle") {
|
||||
var rt = el.getAttribute("data-ruta");
|
||||
WAM.setCampo(rt + ".headerTipo", el.checked ? "texto" : "");
|
||||
|
||||
Reference in New Issue
Block a user