Aplicar pendientes tras la integración
- render.js: botones y cards con urlBloqueada se pintan inertes. - interactivity.js: sinFormato con la misma regla de marcadores que render.js; cita de documento con icono y nombre. - Zoom manual (Ajustar / 100 % / 125 %) en #barra-movil, persistido en localStorage. - Contador «Paso N/M» con estilo en iphone.css en lugar de estilos en línea. - store/editor: campos opcionales tamano y headerTamano; editar la URL retira urlBloqueada; urlValida acepta URLs con forma de dominio; importar avisa de lo recortado. - README y SPEC actualizados. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a645056c00
commit
4fe8f73959
@@ -34,7 +34,8 @@ En el bloque **Cuenta** configuras lo que se ve en la cabecera del chat:
|
||||
- **Subtítulo** (p. ej. «en línea»); cuando el chat «está escribiendo» se cambia temporalmente por *escribiendo…* en verde.
|
||||
- **Avatar**: súbelo desde un archivo (se convierte a dataURL) o pega una URL/dataURL; también puedes quitarlo (se usa un placeholder SVG).
|
||||
- **Cuenta verificada**: activa o desactiva la insignia azul junto al nombre.
|
||||
- **Tema**: claro u oscuro (también con el botón *Tema* de la barra superior; se guarda con el resto del estado).
|
||||
|
||||
El **tema** (claro u oscuro) ya no está en Cuenta: se cambia con el botón *Tema* de la barra superior o con la tecla **T**, y se guarda con el resto del estado.
|
||||
|
||||
### Añadir, editar y reordenar mensajes
|
||||
|
||||
@@ -42,7 +43,8 @@ En el bloque **Cuenta** configuras lo que se ve en la cabecera del chat:
|
||||
- Cada mensaje es una **tarjeta colapsable** con un resumen; haz clic para desplegar sus campos.
|
||||
- En cada tarjeta puedes cambiar los campos comunes: **Dirección** (Entrada/Salida), **Hora** (texto literal `HH:MM`, se muestra tal cual) y, solo en los salientes, **Estado (ticks)**: Enviado ✓ / Entregado ✓✓ / Leído ✓✓ (celeste).
|
||||
- Acciones por tarjeta: **↑ / ↓** reordenar, **⧉** duplicar (con IDs regenerados) y **✕** borrar (pide confirmación).
|
||||
- Todo cambio se guarda al instante y se refleja en el teléfono. El botón **Restablecer** de la barra superior borra `localStorage` y restaura la demo «Café Aurora».
|
||||
- Todo cambio se guarda al instante y se refleja en el teléfono. En el menú **⋯** de la barra superior, **Volver al ejemplo Café Aurora** restaura la demo de ejemplo (pide confirmación y se puede deshacer).
|
||||
- Los botones, filas y tarjetas sin URL tienen la casilla **Se pulsa sola en la demo**: durante *Reproducir demo* se pulsa automáticamente (la cabecera plegada muestra el chip «▶ auto»).
|
||||
|
||||
### Campos por tipo de mensaje
|
||||
|
||||
@@ -51,7 +53,7 @@ En el bloque **Cuenta** configuras lo que se ve en la cabecera del chat:
|
||||
| Texto | texto (admite formato WhatsApp: `*negrita*`, `_cursiva_`, `~tachado~`, `` ` ```monoespaciado``` ` ` y URLs clicables) |
|
||||
| Imagen / Vídeo | archivo o URL + caption opcional |
|
||||
| Audio | archivo opcional, casilla **nota de voz** (waveform + micro) y duración (`0:12`) |
|
||||
| Documento | nombre de archivo con extensión (define el color del icono: pdf, docx, xlsx…), caption y archivo opcional |
|
||||
| Documento | nombre de archivo con extensión (define el color del icono: pdf, docx, xlsx…), tamaño opcional («243 KB • pdf»), caption y archivo opcional |
|
||||
| Sticker | imagen/WebP (se muestra sin burbuja ni hora) |
|
||||
| Ubicación | latitud, longitud, nombre y dirección (el mapa es un gráfico SVG con pin) |
|
||||
| Contacto | nombre, teléfono, email y empresa |
|
||||
@@ -91,11 +93,17 @@ Si un botón no tiene respuestas, al pulsarlo solo sale la burbuja con su texto.
|
||||
|
||||
## Modo demo (columna derecha)
|
||||
|
||||
- **Reproducir demo**: vacía el chat y revela los mensajes uno a uno (~600 ms por mensaje, con «escribiendo…» antes de cada mensaje entrante), como si la conversación ocurriera en directo. Pulsar de nuevo (**■ Detener**) lo cancela.
|
||||
- **Reproducir demo**: vacía el chat y revela los mensajes uno a uno (~600 ms por mensaje, con «escribiendo…» antes de cada mensaje entrante), como si la conversación ocurriera en directo. Mientras corre se ve el contador «Paso N/M»; pulsar de nuevo (**■ Detener**) lo cancela.
|
||||
- **Velocidad**: el selector junto al botón ajusta la reproducción (0,5x, 1x, 2x).
|
||||
- **Zoom** (solo escritorio): *Ajustar* calcula el tamaño del teléfono según el hueco; *100 %* y *125 %* lo fijan y el panel hace scroll. Se recuerda en este navegador.
|
||||
- **Repetir desde el inicio** (antes «Reiniciar chat»): descarta lo pulsado y escrito en el teléfono y vuelve al guion desde el primer mensaje.
|
||||
- **Nueva demo** (botón de la barra superior o tecla **N**): abre los escenarios de ejemplo y tus demos guardadas.
|
||||
- **Deshacer / Rehacer**: botones de la barra superior o **Ctrl/⌘ + Z** y **Ctrl/⌘ + Shift + Z**. Cargar un escenario, una demo o el ejemplo deja siempre un punto de deshacer.
|
||||
- **Mensajes libres**: escribe en el campo *Mensaje* del composer y envía (Enter o botón): sale una burbuja con la **hora actual** y ticks automáticos ✓ → ✓✓ → ✓✓ azul.
|
||||
- **Hoja de lista (sheet)**: al pulsar el botón de un mensaje de tipo *Lista* se abre la hoja inferior con las secciones y filas; toca una fila para disparar su flujo, o toca el fondo oscuro para cerrarla.
|
||||
- **Modo presentación**: oculta el editor y deja solo el iPhone centrado, perfecto para pantallazos o para proyectar. Se alterna con el botón *Presentación* de la barra superior.
|
||||
- **Tema**: alterna claro/oscuro en toda la app (editor y chat).
|
||||
- **Teclado**: el enlace **Saltar a la vista previa** (visible al tabular) y las teclas **G** / **F6** alternan el foco entre editor y teléfono. El diálogo *Atajos* lista el resto.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ Convenciones: `src`/`avatar`/`headerMedia`/`media` = dataURL o URL remota; `""`
|
||||
de `WAM.svgPlaceholder(texto)`. Botones con `url` no vacía se renderizan como `<a target="_blank">`
|
||||
(cta_url, template URL, card de carousel); sin `url` → flujo interactivo con `respuestas`.
|
||||
Botón de plantilla o card con URL no permitida: `normalizar` deja `url: ""` y añade `urlBloqueada: true`
|
||||
(sigue siendo un botón de enlace, inerte; nunca un quick reply con flujo; `resolverFlujo` lo ignora).
|
||||
(sigue siendo un botón de enlace, inerte `.qr-btn-inactiva` en render.js; nunca un quick reply con flujo;
|
||||
`resolverFlujo` lo ignora). Editar a mano el campo URL en el editor retira la marca.
|
||||
`WAM.normalizar(o)` rellena campos faltantes por tipo y devuelve el estado válido (import/upgrade).
|
||||
Campo opcional `autoPulsar` (entero ≥ 0) en `template`/`buttons`/`list`/`carousel` (también en respuestas
|
||||
anidadas): índice del botón, de la fila aplanada de la lista o de la card que «Reproducir demo» pulsa sola
|
||||
@@ -112,7 +113,9 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
|
||||
- Documento: campo opcional `tamano` (y `headerTamano` en la cabecera de plantilla) → subtítulo
|
||||
"243 KB • pdf"; sin él, solo la extensión en mayúsculas ("PDF").
|
||||
- Hooks de CSS (css/iphone.css): `body[data-wam-zoom="100"|"125"]` fija la escala del teléfono (sin
|
||||
atributo = ajuste automático); en ≤ 520 px el teléfono va sin marco salvo con `body.con-marco`.
|
||||
atributo = ajuste automático), solo ≥ 901 px y fuera de Presentación. Lo pone `#sel-zoom` (ux.js,
|
||||
`WAM.setZoom(v)`, persistido en `localStorage["wam-zoom"]`); en ≤ 520 px el teléfono va sin marco salvo
|
||||
con `body.con-marco`.
|
||||
- `WAM.tick(estado)` → string SVG inline (✓ / ✓✓ del color de la hora; ✓✓ leído azul iOS `#027bfc`)
|
||||
- Botones del teléfono: `<button type="button" class="qr-btn">` (Enter/Espacio nativos); URLs solo con
|
||||
esquema `http(s):`, `mailto:` o `tel:` (otro esquema → botón inerte `.qr-btn-inactiva`); `tel:` pinta
|
||||
@@ -192,7 +195,7 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
|
||||
|
||||
### js/main.js (dueño D) — bootstrap
|
||||
- `WAM.avisar(texto, {tipo:"info"|"error", duracion?})` → aviso flotante `#wam-aviso` (estilos por CSSOM)
|
||||
- Controles de `#barra-movil`: `#sel-velocidad`, `#btn-reiniciar`, `#btn-reproducir` (estilos en `css/interactividad.css`, D).
|
||||
- Controles de `#barra-movil`: `#sel-zoom` (ux.js), `#sel-velocidad`, `#btn-reiniciar`, `#btn-reproducir` (estilos en `css/interactividad.css`, D).
|
||||
Teclado en `#pantalla`: Enter/Espacio activan `[data-flujo]`/`[data-ver-todas]`/`.sheet-cerrar`; Escape cierra la hoja.
|
||||
`#btn-reset` pide confirmación. Enlaces del chat con esquema no permitido no se abren.
|
||||
- `WAM.init()` → void (se ejecuta al final del body): `estado = cargar()`; `setTema`; `render()`; `renderEditor()`;
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
margin-right: 4px;
|
||||
opacity: .8;
|
||||
}
|
||||
.wa .wa-cita-doc {
|
||||
display: inline-block;
|
||||
vertical-align: -2px;
|
||||
margin-right: 4px;
|
||||
color: var(--wa-text-meta);
|
||||
}
|
||||
.wa .wa-cita-mini {
|
||||
flex: none;
|
||||
width: 56px;
|
||||
@@ -100,6 +106,7 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
#btn-reiniciar,
|
||||
#sel-zoom,
|
||||
#sel-velocidad {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
@@ -112,8 +119,11 @@
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
#sel-zoom,
|
||||
#sel-velocidad { padding: 0 8px; }
|
||||
#btn-reiniciar:hover,
|
||||
#sel-zoom:hover,
|
||||
#sel-velocidad:hover { border-color: var(--wam-acento, #00a884); }
|
||||
#btn-reiniciar:focus-visible,
|
||||
#sel-zoom:focus-visible,
|
||||
#sel-velocidad:focus-visible { outline: 2px solid var(--wam-acento, #00a884); outline-offset: 2px; }
|
||||
|
||||
+23
-7
@@ -71,13 +71,16 @@ body.presentacion #barra-movil { display: none; }
|
||||
}
|
||||
|
||||
/* Zoom manual (lo fija js/ux.js con body[data-wam-zoom]; sin atributo =
|
||||
"Ajustar", el cálculo automático). Con zoom fijo el panel hace scroll. */
|
||||
html body[data-wam-zoom="100"] { --wam-escala: 1; }
|
||||
html body[data-wam-zoom="125"] { --wam-escala: 1.25; }
|
||||
body[data-wam-zoom="100"] #panel-movil,
|
||||
body[data-wam-zoom="125"] #panel-movil { overflow: auto; }
|
||||
body[data-wam-zoom="100"] .iphone-escena,
|
||||
body[data-wam-zoom="125"] .iphone-escena { margin: 0 auto; }
|
||||
"Ajustar", el cálculo automático). Con zoom fijo el panel hace scroll.
|
||||
Solo en escritorio y fuera de Presentación (ahí manda el ajuste). */
|
||||
@media (min-width: 901px) {
|
||||
html body[data-wam-zoom="100"]:not(.presentacion) { --wam-escala: 1; }
|
||||
html body[data-wam-zoom="125"]:not(.presentacion) { --wam-escala: 1.25; }
|
||||
body[data-wam-zoom="100"]:not(.presentacion) #panel-movil,
|
||||
body[data-wam-zoom="125"]:not(.presentacion) #panel-movil { overflow: auto; }
|
||||
body[data-wam-zoom="100"]:not(.presentacion) .iphone-escena,
|
||||
body[data-wam-zoom="125"]:not(.presentacion) .iphone-escena { margin: 0 auto; }
|
||||
}
|
||||
|
||||
/* --- Columna de vista previa (#panel-movil) ---------------------------- */
|
||||
#panel-movil {
|
||||
@@ -104,6 +107,19 @@ body[data-wam-zoom="125"] .iphone-escena { margin: 0 auto; }
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
/* Contador «Paso 3/12» de la demo (lo crea js/main.js junto al botón) */
|
||||
#barra-movil .demo-paso {
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--wam-texto-suave);
|
||||
white-space: nowrap;
|
||||
}
|
||||
#barra-movil .demo-paso[hidden] { display: none; }
|
||||
/* El zoom manual solo tiene sentido con marco y ventana de escritorio */
|
||||
@media (max-width: 900px) {
|
||||
#barra-movil .barra-zoom { display: none; }
|
||||
}
|
||||
|
||||
#barra-movil .barra-titulo {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -138,6 +138,15 @@
|
||||
<div id="barra-movil">
|
||||
<span class="barra-titulo">Vista previa</span>
|
||||
<div class="barra-acciones">
|
||||
<label class="barra-etq barra-zoom" for="sel-zoom" title="Tamaño del teléfono en pantalla">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.2-4.2M11 8.5v5M8.5 11h5"/></svg>
|
||||
<span class="barra-etq-txt">Zoom</span>
|
||||
</label>
|
||||
<select id="sel-zoom" class="barra-zoom" title="Tamaño del teléfono en pantalla">
|
||||
<option value="" selected>Ajustar</option>
|
||||
<option value="100">100 %</option>
|
||||
<option value="125">125 %</option>
|
||||
</select>
|
||||
<label class="barra-etq" for="sel-velocidad" title="Velocidad de la reproducción">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="13" r="8"/><path d="M12 13V9M10 2.8h4"/></svg>
|
||||
<span class="barra-etq-txt">Velocidad</span>
|
||||
|
||||
+18
-3
@@ -450,7 +450,10 @@
|
||||
if (obj[k] === null || obj[k] === undefined) obj[k] = /^\d+$/.test(partes[i + 1]) ? [] : {};
|
||||
obj = obj[k];
|
||||
}
|
||||
obj[partes[partes.length - 1]] = valor;
|
||||
var clave = partes[partes.length - 1];
|
||||
obj[clave] = valor;
|
||||
// Editar la URL a mano retira la marca de enlace bloqueado (store.js)
|
||||
if (clave === "url" && obj.urlBloqueada) delete obj.urlBloqueada;
|
||||
if (typeof WAM.guardar === "function") WAM.guardar();
|
||||
marcarEditado();
|
||||
if (opts && opts.diferido) { programarRender(String(ruta)); return; }
|
||||
@@ -978,6 +981,8 @@
|
||||
} finally {
|
||||
WAM.estado = previo;
|
||||
}
|
||||
// Se lee ya: cualquier normalización posterior sobrescribe el resumen
|
||||
var recortes = typeof WAM.describirRecortes === "function" && datos ? WAM.describirRecortes(datos.__recortes) : "";
|
||||
if (!datos || !Array.isArray(datos.mensajes)) throw new Error("sin mensajes");
|
||||
snap();
|
||||
WAM.estado = datos;
|
||||
@@ -992,7 +997,7 @@
|
||||
if (typeof WAM.render === "function") WAM.render();
|
||||
WAM.renderEditor();
|
||||
var n = WAM.estado.mensajes.length;
|
||||
WAM.toast("Demo importada: " + n + " mensaje" + (n === 1 ? "" : "s"),
|
||||
WAM.toast(recortes ? recortes + "." : "Demo importada: " + n + " mensaje" + (n === 1 ? "" : "s"),
|
||||
typeof WAM.deshacer === "function" ? { accion: { texto: "Deshacer", fn: WAM.deshacer }, ms: 8000 } : null);
|
||||
var primero = listaNodo() && listaNodo().querySelector(".ed-cab-principal");
|
||||
if (primero) primero.focus();
|
||||
@@ -1038,7 +1043,13 @@
|
||||
------------------------------------------------------------------ */
|
||||
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()); }
|
||||
// Con esquema http(s) o con forma de dominio («www.cafe.com», «cafe.com/menu»):
|
||||
// render.js abre estas últimas con https://, igual que WhatsApp.
|
||||
function urlValida(v) {
|
||||
var s = String(v || "").trim();
|
||||
return /^https?:\/\/[^\s/$.?#][^\s]*$/i.test(s) ||
|
||||
/^www\.[^\s]+$/i.test(s) || /^[\w-]+(\.[\w-]+)+(:\d+)?([\/?#][^\s]*)?$/.test(s);
|
||||
}
|
||||
// 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();
|
||||
@@ -1716,6 +1727,9 @@
|
||||
caja.appendChild(edCampo("Nombre del documento (con extensión; define el icono)", ruta + ".headerTexto", m.headerTexto, {
|
||||
max: 80, contar: true, placeholder: "catalogo.pdf", spell: false
|
||||
}));
|
||||
caja.appendChild(edCampo("Tamaño (opcional, p. ej. 243 KB)", ruta + ".headerTamano", m.headerTamano, {
|
||||
max: 20, placeholder: "243 KB", spell: false
|
||||
}));
|
||||
}
|
||||
caja.appendChild(edMedia("Media de cabecera", ruta + ".headerMedia", m.headerMedia, {
|
||||
aceptar: tipo === "imagen" ? "image/*" : (tipo === "video" ? "video/*" : ""),
|
||||
@@ -1801,6 +1815,7 @@
|
||||
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(edCampo("Tamaño (opcional, p. ej. 243 KB)", ruta + ".tamano", m.tamano, { max: 20, placeholder: "243 KB", spell: false }));
|
||||
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;
|
||||
|
||||
+26
-4
@@ -292,12 +292,23 @@ window.WAM = window.WAM || {};
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/```([\s\S]*?)```/g, "$1")
|
||||
.replace(/`([^`\n]+)`/g, "$1")
|
||||
.replace(/\*([^*\n]+)\*/g, "$1")
|
||||
.replace(/(^|[^\w])_([^_\n]+)_(?=[^\w]|$)/g, "$1$2")
|
||||
.replace(/(^|[^\w])~([^~\n]+)~(?=[^\w]|$)/g, "$1$2")
|
||||
.replace(reMarca("\*"), "$1$2")
|
||||
.replace(reMarca("_"), "$1$2")
|
||||
.replace(reMarca("~"), "$1$2")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* Misma regla que render.js: marca pegada a un carácter no blanco por dentro
|
||||
y sin letra, número ni _ por fuera («2 * 3 * 4» queda literal). */
|
||||
function reMarca(e) {
|
||||
try {
|
||||
return new RegExp("(^|[^\p{L}\p{N}_" + e + "])" + e + "(?=\S)([^" + e + "\n]*?\S)" + e +
|
||||
"(?=[^\p{L}\p{N}_" + e + "]|$)", "gu");
|
||||
} catch (err) {
|
||||
return new RegExp("(^|[^\w" + e + "])" + e + "(?=\S)([^" + e + "\n]*?\S)" + e + "(?=[^\w" + e + "]|$)", "g");
|
||||
}
|
||||
}
|
||||
|
||||
function miniaturaSegura(u) {
|
||||
if (vacio(u)) return "";
|
||||
var s = String(u);
|
||||
@@ -340,6 +351,9 @@ window.WAM = window.WAM || {};
|
||||
cita.foto = true;
|
||||
} else if (msg.headerTipo === "video") {
|
||||
cita.foto = true;
|
||||
} else if (msg.headerTipo === "documento") {
|
||||
cita.titulo = vacio(msg.headerTexto) ? "documento.pdf" : String(msg.headerTexto);
|
||||
cita.doc = true;
|
||||
}
|
||||
}
|
||||
cita.extracto = partes.filter(function (p) { return !vacio(p); }).map(sinFormato).join("\n");
|
||||
@@ -350,6 +364,9 @@ window.WAM = window.WAM || {};
|
||||
var SVG_CAMARA = '<svg class="wa-cita-cam" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">' +
|
||||
'<path fill="currentColor" d="M5.6 2.5h4.8l1 1.5H13a1.5 1.5 0 0 1 1.5 1.5v6A1.5 1.5 0 0 1 13 12.5H3A1.5 1.5 0 0 1 1.5 11V5.5A1.5 1.5 0 0 1 3 4h1.6l1-1.5Zm2.4 3a2.75 2.75 0 1 0 0 5.5 2.75 2.75 0 0 0 0-5.5Z"/></svg>';
|
||||
|
||||
var SVG_DOC = '<svg class="wa-cita-doc" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">' +
|
||||
'<path fill="currentColor" d="M4 1.5h5.3L13 5.2V13a1.5 1.5 0 0 1-1.5 1.5h-7A1.5 1.5 0 0 1 3 13V3a1.5 1.5 0 0 1 1-1.5Zm5 .9V5.5h3.1L9 2.4ZM5 8h6v1H5V8Zm0 2.2h6v1H5v-1Z"/></svg>';
|
||||
|
||||
/* Bloque de cita dentro de la burbuja OUT; todo por textContent (sin XSS). */
|
||||
function pintarCita(fila, cita) {
|
||||
var burbuja = fila.querySelector(".bubble");
|
||||
@@ -365,7 +382,12 @@ window.WAM = window.WAM || {};
|
||||
if (cita.titulo) {
|
||||
var tit = document.createElement("div");
|
||||
tit.className = "wa-cita-titulo";
|
||||
tit.textContent = cita.titulo;
|
||||
if (cita.doc) {
|
||||
var doc = document.createElement("span");
|
||||
doc.innerHTML = SVG_DOC; /* constante propia, sin datos del usuario */
|
||||
if (doc.firstChild) tit.appendChild(doc.firstChild);
|
||||
}
|
||||
tit.appendChild(document.createTextNode(cita.titulo));
|
||||
txt.appendChild(tit);
|
||||
}
|
||||
if (cita.extracto) {
|
||||
|
||||
@@ -86,11 +86,6 @@ window.WAM = window.WAM || {};
|
||||
c.setAttribute("aria-live", "polite");
|
||||
c.setAttribute("role", "status");
|
||||
c.hidden = true;
|
||||
/* estilo mínimo en línea (tabular) mientras no haya regla en iphone.css */
|
||||
c.style.fontSize = "12px";
|
||||
c.style.fontVariantNumeric = "tabular-nums";
|
||||
c.style.opacity = "0.75";
|
||||
c.style.whiteSpace = "nowrap";
|
||||
btn.parentNode.insertBefore(c, btn);
|
||||
return c;
|
||||
}
|
||||
|
||||
+2
-2
@@ -427,7 +427,7 @@
|
||||
return '<a class="qr-btn"' + h + ' href="' + esc(url) + '" target="_blank" rel="noopener noreferrer" data-btn-url="' + j + '">' +
|
||||
icono(tipoIconoUrl(url)) + lbl + "</a>";
|
||||
}
|
||||
if (!vacio(b.url)) {
|
||||
if (!vacio(b.url) || b.urlBloqueada) {
|
||||
return '<span class="qr-btn qr-btn-inactiva"' + h + ' aria-disabled="true">' + icono("url") + lbl + "</span>";
|
||||
}
|
||||
return '<button type="button" class="qr-btn"' + h + ' data-flujo="' + i + '" data-btn="' + j + '">' +
|
||||
@@ -540,7 +540,7 @@
|
||||
var url = urlSegura(c.url);
|
||||
var boton;
|
||||
if (url) boton = '<a class="qr-btn" href="' + esc(url) + '" target="_blank" rel="noopener noreferrer">' + icono(tipoIconoUrl(url)) + lbl + "</a>";
|
||||
else if (!vacio(c.url)) boton = '<span class="qr-btn qr-btn-inactiva" aria-disabled="true">' + icono("url") + lbl + "</span>";
|
||||
else if (!vacio(c.url) || c.urlBloqueada) boton = '<span class="qr-btn qr-btn-inactiva" aria-disabled="true">' + icono("url") + lbl + "</span>";
|
||||
else boton = '<button type="button" class="qr-btn" data-flujo="' + i + '" data-card="' + j + '">' + icono("responder") + lbl + "</button>";
|
||||
html += '<div class="card">' +
|
||||
'<img class="card-media" src="' + esc(media) + '" alt="" draggable="false">' +
|
||||
|
||||
@@ -359,6 +359,8 @@ window.WAM = window.WAM || {};
|
||||
msg.src = urlLimpia(m.src, "media");
|
||||
msg.caption = cad(m.caption);
|
||||
msg.filename = cad(m.filename, "documento.pdf") || "documento.pdf";
|
||||
/* opcional: «243 KB» para el subtítulo «243 KB • pdf» (render.js) */
|
||||
if (cad(m.tamano).trim()) msg.tamano = cad(m.tamano).trim().slice(0, 20);
|
||||
break;
|
||||
case "sticker":
|
||||
msg.src = urlLimpia(m.src, "media");
|
||||
@@ -380,6 +382,9 @@ window.WAM = window.WAM || {};
|
||||
msg.headerTipo = HEADER_TIPOS.indexOf(m.headerTipo) !== -1 ? m.headerTipo : "";
|
||||
msg.headerTexto = cad(m.headerTexto);
|
||||
msg.headerMedia = urlLimpia(m.headerMedia, "media");
|
||||
if (msg.headerTipo === "documento" && cad(m.headerTamano).trim()) {
|
||||
msg.headerTamano = cad(m.headerTamano).trim().slice(0, 20);
|
||||
}
|
||||
msg.body = cad(m.body);
|
||||
msg.footer = cad(m.footer);
|
||||
msg.botones = botones(m.botones);
|
||||
|
||||
@@ -955,6 +955,30 @@
|
||||
temaPintado = tema;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Zoom manual del teléfono (Ajustar / 100 % / 125 %). El CSS vive en
|
||||
iphone.css (body[data-wam-zoom]); sin atributo = ajustar al hueco.
|
||||
------------------------------------------------------------------ */
|
||||
var CLAVE_ZOOM = "wam-zoom";
|
||||
function aplicarZoom(v) {
|
||||
v = v === "100" || v === "125" ? v : "";
|
||||
if (v) document.body.setAttribute("data-wam-zoom", v);
|
||||
else document.body.removeAttribute("data-wam-zoom");
|
||||
var sel = porId("sel-zoom");
|
||||
if (sel && sel.value !== v) sel.value = v;
|
||||
return v;
|
||||
}
|
||||
WAM.setZoom = function (v) {
|
||||
v = aplicarZoom(String(v == null ? "" : v));
|
||||
if (v) lsSet(CLAVE_ZOOM, v); else lsDel(CLAVE_ZOOM);
|
||||
return v;
|
||||
};
|
||||
function enlazarZoom() {
|
||||
aplicarZoom(lsGet(CLAVE_ZOOM) || "");
|
||||
var sel = porId("sel-zoom");
|
||||
if (sel) sel.addEventListener("change", function () { WAM.setZoom(sel.value); });
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Inicialización (una sola vez)
|
||||
------------------------------------------------------------------ */
|
||||
@@ -978,6 +1002,7 @@
|
||||
observarToolbar();
|
||||
enlazarChat();
|
||||
enlazarSaltos();
|
||||
enlazarZoom();
|
||||
envolverAnuncios();
|
||||
|
||||
sincronizarTema();
|
||||
|
||||
Reference in New Issue
Block a user