Integrar rama de render: chat pegado abajo, hojas sin desfase y fidelidad iOS
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -95,7 +95,16 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
|
|||||||
`#pantalla`: título = `boton`, filas `button.sheet-fila[data-sec][data-fila]`, botón `.sheet-cerrar`)
|
`#pantalla`: título = `boton`, filas `button.sheet-fila[data-sec][data-fila]`, botón `.sheet-cerrar`)
|
||||||
- `WAM.abrirOpciones(i | Mensaje, li?)` → void: hoja "Todas las opciones" de un mensaje con >3 botones
|
- `WAM.abrirOpciones(i | Mensaje, li?)` → void: hoja "Todas las opciones" de un mensaje con >3 botones
|
||||||
(la abre el propio B al pulsar `[data-ver-todas]`). Sus filas de respuesta llevan `data-opcion="<j>"`
|
(la abre el propio B al pulsar `[data-ver-todas]`). Sus filas de respuesta llevan `data-opcion="<j>"`
|
||||||
y B reenvía el clic al `[data-btn="<j>"]` oculto de la burbuja, que atiende la delegación normal de main.js.
|
y B reenvía el clic al `[data-btn="<j>"]` oculto de la burbuja, que atiende la delegación normal de main.js
|
||||||
|
(busca la fila viva en el momento del clic). `renderMensaje` de un mensaje cuya hoja está abierta la cierra
|
||||||
|
(re-render parcial del editor), así la hoja nunca queda desfasada.
|
||||||
|
- `scrollAbajo()` es instantáneo (ignora `scroll-behavior`) y deja el chat "pegado abajo": mientras el usuario
|
||||||
|
no suba (>60 px del fondo), un ResizeObserver sobre `#chat-cuerpo` y sus filas vuelve a bajar cuando algo
|
||||||
|
crece (imágenes que decodifican tarde, pestaña Vista previa que pasa a visible).
|
||||||
|
- 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`.
|
||||||
- `WAM.tick(estado)` → string SVG inline (✓ / ✓✓ del color de la hora; ✓✓ leído azul iOS `#027bfc`)
|
- `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
|
- 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
|
esquema `http(s):`, `mailto:` o `tel:` (otro esquema → botón inerte `.qr-btn-inactiva`); `tel:` pinta
|
||||||
|
|||||||
+69
-2
@@ -43,6 +43,42 @@
|
|||||||
que el teléfono crezca (en 1280×720 pasa de .67 a ~.73). */
|
que el teléfono crezca (en 1280×720 pasa de .67 a ~.73). */
|
||||||
body.presentacion #barra-movil { display: none; }
|
body.presentacion #barra-movil { display: none; }
|
||||||
|
|
||||||
|
/* Presentación en escritorio (ratón + teclado): captura limpia, sin toolbar.
|
||||||
|
Se sale con Esc o P (js/ux.js); una pista flotante lo recuerda. */
|
||||||
|
@media (min-width: 901px) and (hover: hover) and (pointer: fine) {
|
||||||
|
body.presentacion #toolbar { display: none; }
|
||||||
|
body.presentacion #panel-movil { position: relative; }
|
||||||
|
body.presentacion #panel-movil::before {
|
||||||
|
content: "Esc para salir";
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 16px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(11, 20, 26, .55);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: .75;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
@supports (scale: calc(1vh / 1px)) {
|
||||||
|
body.presentacion {
|
||||||
|
--wam-escala: max(.3, min(1, (100vh - 32px) / 852px, (100vw - 64px) / 393px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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; }
|
||||||
|
|
||||||
/* --- Columna de vista previa (#panel-movil) ---------------------------- */
|
/* --- Columna de vista previa (#panel-movil) ---------------------------- */
|
||||||
#panel-movil {
|
#panel-movil {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -185,7 +221,7 @@ body.presentacion #barra-movil { display: none; }
|
|||||||
height: 5px;
|
height: 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: #fff; /* en oscuro */
|
background: #fff; /* en oscuro */
|
||||||
z-index: 45;
|
z-index: 70; /* encima de la hoja inferior (.sheet z 60), como en iOS */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#pantalla:not(.oscuro) .home-indicator { background: #000; } /* en claro, negro */
|
#pantalla:not(.oscuro) .home-indicator { background: #000; } /* en claro, negro */
|
||||||
@@ -202,7 +238,7 @@ body.presentacion #barra-movil { display: none; }
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 8px 0 14px;
|
padding: 8px 0 14px;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: auto; /* el suave solo en enfocarEnChat (behavior explícito) */
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +277,37 @@ body.presentacion #barra-movil { display: none; }
|
|||||||
/* --- Modo presentación: el iPhone se centra y respira más --------------- */
|
/* --- Modo presentación: el iPhone se centra y respira más --------------- */
|
||||||
body.presentacion #panel-movil { padding: 14px 20px 22px; }
|
body.presentacion #panel-movil { padding: 14px 20px 22px; }
|
||||||
|
|
||||||
|
/* --- Móvil real (≤ 520 px): sin marco ----------------------------------
|
||||||
|
En el propio teléfono del consultor sobra el iPhone dibujado: la pantalla
|
||||||
|
ocupa el ancho y el alto disponibles a escala 1 (sin isla, status bar ni
|
||||||
|
indicador home duplicados). body.con-marco lo desactiva. */
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
html body:not(.con-marco) { --wam-escala: 1; }
|
||||||
|
body:not(.con-marco) #panel-movil { padding-left: 0; padding-right: 0; }
|
||||||
|
body:not(.con-marco) #barra-movil { padding: 0 16px; margin-bottom: 8px; }
|
||||||
|
body:not(.con-marco) .iphone-escena {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: 420px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
body:not(.con-marco) .iphone {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
body:not(.con-marco) .pantalla { border-radius: 0; padding-top: 0; }
|
||||||
|
body:not(.con-marco) .island,
|
||||||
|
body:not(.con-marco) .statusbar,
|
||||||
|
body:not(.con-marco) .home-indicator { display: none; }
|
||||||
|
body:not(.con-marco) .wa #composer { padding-bottom: 8px; }
|
||||||
|
body:not(.con-marco) .wa .sheet-panel { padding-bottom: 12px; }
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Accesibilidad: menos movimiento ------------------------------------- */
|
/* --- Accesibilidad: menos movimiento ------------------------------------- */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
#btn-reproducir { transition: none; }
|
#btn-reproducir { transition: none; }
|
||||||
|
|||||||
+27
-14
@@ -69,7 +69,7 @@
|
|||||||
--wa-chrome: #121313;
|
--wa-chrome: #121313;
|
||||||
--wa-chrome-text: #ffffff;
|
--wa-chrome-text: #ffffff;
|
||||||
--wa-header-icon: #ffffff;
|
--wa-header-icon: #ffffff;
|
||||||
--wa-compose-bar: #121313;
|
--wa-compose-bar: #232626; /* barra [medido]; el header sí es #121313 */
|
||||||
--wa-compose-field: #393c3c; /* [medido] */
|
--wa-compose-field: #393c3c; /* [medido] */
|
||||||
--wa-compose-border: transparent;
|
--wa-compose-border: transparent;
|
||||||
--wa-separator: rgba(255, 255, 255, .10);
|
--wa-separator: rgba(255, 255, 255, .10);
|
||||||
@@ -106,7 +106,6 @@
|
|||||||
|
|
||||||
/* Reset de <button> dentro del chat (botones de flujo accesibles) */
|
/* Reset de <button> dentro del chat (botones de flujo accesibles) */
|
||||||
.wa button.qr-btn,
|
.wa button.qr-btn,
|
||||||
.wa button.sheet-fila,
|
|
||||||
.wa button.sheet-cerrar,
|
.wa button.sheet-cerrar,
|
||||||
.wa button.contact-accion {
|
.wa button.contact-accion {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -116,6 +115,16 @@
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
/* Las filas de la hoja conservan su separador (lista-hoja-inferior.png) */
|
||||||
|
.wa button.sheet-fila {
|
||||||
|
font: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: .5px solid var(--wa-sheet-sep);
|
||||||
|
background: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 4. Header del chat (iOS: chevron negro, avatar 36, iconos de línea) ---------- */
|
/* ---------- 4. Header del chat (iOS: chevron negro, avatar 36, iconos de línea) ---------- */
|
||||||
.wa #chat-header {
|
.wa #chat-header {
|
||||||
@@ -127,7 +136,7 @@
|
|||||||
background: var(--wa-chrome);
|
background: var(--wa-chrome);
|
||||||
color: var(--wa-chrome-text);
|
color: var(--wa-chrome-text);
|
||||||
border-bottom: .5px solid var(--wa-separator);
|
border-bottom: .5px solid var(--wa-separator);
|
||||||
transition: background-color .2s ease;
|
transition: background-color .2s ease, color .2s ease;
|
||||||
}
|
}
|
||||||
.wa .wa-back { width: 22px; flex: none; display: flex; color: var(--wa-chrome-text); padding-left: 4px; }
|
.wa .wa-back { width: 22px; flex: none; display: flex; color: var(--wa-chrome-text); padding-left: 4px; }
|
||||||
.wa .wa-avatar,
|
.wa .wa-avatar,
|
||||||
@@ -139,6 +148,7 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
background: var(--wa-separator);
|
background: var(--wa-separator);
|
||||||
}
|
}
|
||||||
|
.wa #chat-avatar { position: relative; z-index: 1; }
|
||||||
.wa .wa-header-tit { flex: 1; min-width: 0; }
|
.wa .wa-header-tit { flex: 1; min-width: 0; }
|
||||||
.wa .header-title {
|
.wa .header-title {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
@@ -199,14 +209,14 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0; /* contexto: la cola queda bajo el contenido */
|
z-index: 0; /* contexto: la cola queda bajo el contenido */
|
||||||
max-width: 78%;
|
max-width: 78%;
|
||||||
padding: 6px 12px 7px;
|
padding: 7px 12px 6px; /* 7 arriba / 6 abajo [medido, README §2.2] */
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: var(--wa-bubble-in);
|
background: var(--wa-bubble-in);
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
transition: background-color .2s ease;
|
transition: background-color .2s ease, color .2s ease;
|
||||||
}
|
}
|
||||||
.wa .out .bubble { background: var(--wa-bubble-out); }
|
.wa .out .bubble { background: var(--wa-bubble-out); }
|
||||||
|
|
||||||
@@ -615,7 +625,7 @@
|
|||||||
background: var(--wa-bubble-in);
|
background: var(--wa-bubble-in);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
transition: background-color .2s ease;
|
transition: background-color .2s ease, color .2s ease;
|
||||||
}
|
}
|
||||||
.wa .card-media {
|
.wa .card-media {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -737,15 +747,17 @@
|
|||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
.wa .sheet-opciones > .sheet-seccion:first-child { padding-top: 12px; }
|
.wa .sheet-opciones > .sheet-seccion:first-child { padding-top: 30px; }
|
||||||
.wa .sheet-grupo { margin: 0; }
|
.wa .sheet-grupo { margin: 0; }
|
||||||
.wa .sheet-grupo + .sheet-grupo { margin-top: 18px; border-top: .5px solid var(--wa-sheet-sep); }
|
/* "Todas las opciones": un único separador (con sangría) entre grupos */
|
||||||
|
.wa .sheet-grupo + .sheet-grupo { margin: 0 0 0 16px; border-top: .5px solid var(--wa-sheet-sep); }
|
||||||
|
.wa .sheet-grupo + .sheet-grupo > .sheet-fila { margin-left: 0; width: 100%; }
|
||||||
.wa .sheet-fila {
|
.wa .sheet-fila {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
padding: 12px 16px 12px 0;
|
padding: 17px 16px 17px 0;
|
||||||
margin: 0 0 0 16px;
|
margin: 0 0 0 16px;
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 16px);
|
||||||
border-bottom: .5px solid var(--wa-sheet-sep);
|
border-bottom: .5px solid var(--wa-sheet-sep);
|
||||||
@@ -757,10 +769,11 @@
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.wa .sheet-fila:active { background: rgba(0, 0, 0, .05); }
|
.wa .sheet-fila:active { background: rgba(0, 0, 0, .05); }
|
||||||
.wa .sheet-fila-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
.wa .sheet-fila-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0; }
|
||||||
.wa .sheet-fila-titulo { font-size: 17px; line-height: 22px; }
|
.wa .sheet-fila-titulo { font-size: 17px; line-height: 22px; }
|
||||||
.wa .sheet-fila-desc { font-size: 15px; line-height: 20px; letter-spacing: 0; color: var(--wa-sheet-sub); }
|
.wa .sheet-fila-desc { font-size: 15px; line-height: 18px; letter-spacing: 0; color: var(--wa-sheet-sub); }
|
||||||
.wa .sheet-fila-btn { color: var(--wa-action); min-height: 48px; }
|
.wa .sheet-fila-btn,
|
||||||
|
.wa button.sheet-fila-btn { color: var(--wa-action); min-height: 48px; padding-top: 12px; padding-bottom: 12px; border-bottom: 0; }
|
||||||
.wa .sheet-fila-btn .sheet-fila-titulo { color: var(--wa-action); }
|
.wa .sheet-fila-btn .sheet-fila-titulo { color: var(--wa-action); }
|
||||||
.wa .sheet-fila .qr-ico { flex: none; display: flex; color: var(--wa-action); }
|
.wa .sheet-fila .qr-ico { flex: none; display: flex; color: var(--wa-action); }
|
||||||
.wa .sheet-pie {
|
.wa .sheet-pie {
|
||||||
@@ -780,8 +793,8 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 6px 12px 34px 10px; /* 34 pt de safe area bajo el home indicator */
|
padding: 6px 12px 34px 10px; /* 34 pt de safe area bajo el home indicator */
|
||||||
background: var(--wa-compose-bar);
|
background: var(--wa-compose-bar);
|
||||||
border-top: 0;
|
border-top: .5px solid var(--wa-separator); /* hairline como el del header */
|
||||||
transition: background-color .2s ease;
|
transition: background-color .2s ease, color .2s ease;
|
||||||
}
|
}
|
||||||
.wa #composer .attach,
|
.wa #composer .attach,
|
||||||
.wa #composer .cmp-icono {
|
.wa #composer .cmp-icono {
|
||||||
|
|||||||
+127
-24
@@ -29,11 +29,15 @@
|
|||||||
function estado() { return WAM.estado || { cuenta: {}, mensajes: [] }; }
|
function estado() { return WAM.estado || { cuenta: {}, mensajes: [] }; }
|
||||||
|
|
||||||
var ESCRIBIENDO = false;
|
var ESCRIBIENDO = false;
|
||||||
|
var HOJA_ID = ""; /* id del mensaje cuya hoja está abierta ("" = ninguna) */
|
||||||
|
|
||||||
/* Solo esquemas seguros en href (defensa en profundidad para JSON importados). */
|
/* Solo esquemas seguros en href (defensa en profundidad para JSON importados). */
|
||||||
function urlSegura(u) {
|
function urlSegura(u) {
|
||||||
var s = String(u == null ? "" : u).trim();
|
var s = String(u == null ? "" : u).trim();
|
||||||
return /^(https?:|mailto:|tel:)/i.test(s) ? s : "";
|
if (/^(https?:|mailto:|tel:)/i.test(s)) return s;
|
||||||
|
/* Sin esquema ("www.cafe.com", "cafe.com/menu"): WhatsApp lo abre como https */
|
||||||
|
if (/^www\.[^\s]+$/i.test(s) || /^[\w-]+(\.[\w-]+)+(:\d+)?([\/?#][^\s]*)?$/.test(s)) return "https://" + s;
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* src de <img>/<video>: bloquea javascript:/vbscript: y data: que no sea media. */
|
/* src de <img>/<video>: bloquea javascript:/vbscript: y data: que no sea media. */
|
||||||
@@ -202,27 +206,61 @@
|
|||||||
/* Formato WhatsApp: *negrita* _cursiva_ ~tachado~ ```bloque``` `en línea`
|
/* Formato WhatsApp: *negrita* _cursiva_ ~tachado~ ```bloque``` `en línea`
|
||||||
+ URLs hipervinculadas. Dentro del código no se aplica ningún otro formato. */
|
+ URLs hipervinculadas. Dentro del código no se aplica ningún otro formato. */
|
||||||
|
|
||||||
|
/* Marcadores de WhatsApp: el marcador va pegado a texto (sin espacio por
|
||||||
|
dentro) y por fuera no puede haber letra/número ("2 * 3 * 4" no es negrita). */
|
||||||
|
function reMarca(m) {
|
||||||
|
var e = m === "*" ? "\\*" : m; /* con flag u, "\_" y "\~" son escapes inválidos */
|
||||||
|
return new RegExp("(^|[^\\p{L}\\p{N}_" + e + "])" + e + "(?=\\S)([^" + e + "\\n]*?\\S)" + e +
|
||||||
|
"(?=[^\\p{L}\\p{N}_" + e + "]|$)", "gu");
|
||||||
|
}
|
||||||
|
var RE_NEGRITA = reMarca("*"), RE_CURSIVA = reMarca("_"), RE_TACHADO = reMarca("~");
|
||||||
|
|
||||||
|
/* Deja fuera del enlace la puntuación final y los ')' sin su '(' */
|
||||||
|
function separarUrl(u) {
|
||||||
|
var url = u, resto = "";
|
||||||
|
for (;;) {
|
||||||
|
var m = /[.,;:!?'"\]}]+$/.exec(url);
|
||||||
|
if (m) { resto = m[0] + resto; url = url.slice(0, -m[0].length); continue; }
|
||||||
|
if (/\)$/.test(url) && (url.split(")").length > url.split("(").length)) {
|
||||||
|
resto = ")" + resto; url = url.slice(0, -1); continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return [url, resto];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatoTexto(p) {
|
||||||
|
var codigos = [];
|
||||||
|
var guardar = function (_, c) { codigos.push(c); return "\u0000" + (codigos.length - 1) + "\u0000"; };
|
||||||
|
var t = esc(p.replace(/\u0000/g, ""));
|
||||||
|
t = t.replace(/```([\s\S]+?)```/g, guardar);
|
||||||
|
t = t.replace(/`([^`\n]+)`/g, guardar);
|
||||||
|
t = t.replace(RE_NEGRITA, "$1<strong>$2</strong>");
|
||||||
|
t = t.replace(RE_CURSIVA, "$1<em>$2</em>");
|
||||||
|
t = t.replace(RE_TACHADO, "$1<del>$2</del>");
|
||||||
|
return t.replace(/\u0000(\d+)\u0000/g, function (_, n) { return "<code>" + codigos[+n] + "</code>"; });
|
||||||
|
}
|
||||||
|
|
||||||
function formatear(txt) {
|
function formatear(txt) {
|
||||||
var partes = String(txt == null ? "" : txt).split(/(https?:\/\/[^\s<>"]+)/g);
|
var partes = String(txt == null ? "" : txt).split(/(https?:\/\/[^\s<>"]+)/g);
|
||||||
var html = "";
|
var html = "", pendiente = "";
|
||||||
for (var i = 0; i < partes.length; i++) {
|
for (var i = 0; i < partes.length; i++) {
|
||||||
var p = partes[i];
|
var p = partes[i];
|
||||||
if (p === "") continue;
|
if (p === "") continue;
|
||||||
if (/^https?:\/\//.test(p)) {
|
if (/^https?:\/\//.test(p)) {
|
||||||
html += '<a class="wa-link" href="' + esc(p) + '" target="_blank" rel="noopener noreferrer">' + esc(p) + "</a>";
|
var par = separarUrl(p);
|
||||||
|
if (pendiente) { html += formatoTexto(pendiente); pendiente = ""; }
|
||||||
|
if (par[0].length > p.indexOf("//") + 2) {
|
||||||
|
html += '<a class="wa-link" href="' + esc(par[0]) + '" target="_blank" rel="noopener noreferrer">' + esc(par[0]) + "</a>";
|
||||||
|
pendiente = par[1];
|
||||||
|
} else {
|
||||||
|
pendiente = p;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var codigos = [];
|
pendiente += p;
|
||||||
var guardar = function (_, c) { codigos.push(c); return "\u0000" + (codigos.length - 1) + "\u0000"; };
|
|
||||||
var t = esc(p.replace(/\u0000/g, ""));
|
|
||||||
t = t.replace(/```([\s\S]+?)```/g, guardar);
|
|
||||||
t = t.replace(/`([^`\n]+)`/g, guardar);
|
|
||||||
t = t.replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>");
|
|
||||||
t = t.replace(/(^|[^\w])_([^_\n]+)_(?=[^\w]|$)/g, "$1<em>$2</em>");
|
|
||||||
t = t.replace(/(^|[^\w])~([^~\n]+)~(?=[^\w]|$)/g, "$1<del>$2</del>");
|
|
||||||
t = t.replace(/\u0000(\d+)\u0000/g, function (_, n) { return "<code>" + codigos[+n] + "</code>"; });
|
|
||||||
html += t;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pendiente) html += formatoTexto(pendiente);
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,18 +360,20 @@
|
|||||||
derecha + "</div></div>";
|
derecha + "</div></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function panelDocumento(nombre) {
|
/* Subtítulo iOS "tamaño • extensión" (243 KB • pdf); sin tamaño, solo "PDF" */
|
||||||
|
function panelDocumento(nombre, tamano) {
|
||||||
var ext = extDe(nombre);
|
var ext = extDe(nombre);
|
||||||
|
var sub = vacio(tamano) ? ext.toUpperCase() : String(tamano) + " • " + ext;
|
||||||
return '<div class="doc-panel">' +
|
return '<div class="doc-panel">' +
|
||||||
'<span class="doc-icono">' + docIcon(ext) + "</span>" +
|
'<span class="doc-icono">' + docIcon(ext) + "</span>" +
|
||||||
'<span class="doc-datos"><span class="doc-nombre">' + esc(nombre) + "</span>" +
|
'<span class="doc-datos"><span class="doc-nombre">' + esc(nombre) + "</span>" +
|
||||||
'<span class="doc-sub">Documento • ' + esc(ext) + "</span></span>" +
|
'<span class="doc-sub">' + esc(sub) + "</span></span>" +
|
||||||
'<span class="doc-descarga">' + svgDescarga() + "</span></div>";
|
'<span class="doc-descarga">' + svgDescarga() + "</span></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function burbujaDocumento(m) {
|
function burbujaDocumento(m) {
|
||||||
var nombre = vacio(m.filename) ? "documento.pdf" : String(m.filename);
|
var nombre = vacio(m.filename) ? "documento.pdf" : String(m.filename);
|
||||||
var html = '<div class="bubble wa-doc">' + panelDocumento(nombre);
|
var html = '<div class="bubble wa-doc">' + panelDocumento(nombre, m.tamano);
|
||||||
if (!vacio(m.caption)) html += '<div class="wa-caption">' + formatear(m.caption) + metaHTML(m) + "</div>";
|
if (!vacio(m.caption)) html += '<div class="wa-caption">' + formatear(m.caption) + metaHTML(m) + "</div>";
|
||||||
else html += '<div class="wa-pie">' + metaHTML(m) + "</div>";
|
else html += '<div class="wa-pie">' + metaHTML(m) + "</div>";
|
||||||
return html + "</div>";
|
return html + "</div>";
|
||||||
@@ -436,7 +476,7 @@
|
|||||||
}
|
}
|
||||||
if (ht === "documento") {
|
if (ht === "documento") {
|
||||||
var nom = vacio(m.headerTexto) ? "documento.pdf" : String(m.headerTexto);
|
var nom = vacio(m.headerTexto) ? "documento.pdf" : String(m.headerTexto);
|
||||||
return '<div class="tpl-header-doc">' + panelDocumento(nom) + "</div>";
|
return '<div class="tpl-header-doc">' + panelDocumento(nom, m.headerTamano) + "</div>";
|
||||||
}
|
}
|
||||||
if (ht === "texto") {
|
if (ht === "texto") {
|
||||||
return vacio(m.headerTexto) ? "" : '<div class="tpl-header-txt">' + esc(m.headerTexto) + "</div>";
|
return vacio(m.headerTexto) ? "" : '<div class="tpl-header-txt">' + esc(m.headerTexto) + "</div>";
|
||||||
@@ -590,6 +630,10 @@
|
|||||||
if (!vacio(m.id)) li.setAttribute("data-id", String(m.id));
|
if (!vacio(m.id)) li.setAttribute("data-id", String(m.id));
|
||||||
li.innerHTML = cuerpo;
|
li.innerHTML = cuerpo;
|
||||||
activarPlegado(li, m);
|
activarPlegado(li, m);
|
||||||
|
/* Re-render parcial (editor) del mensaje cuya hoja está abierta: la hoja
|
||||||
|
quedaría desfasada (títulos viejos, fila destino fuera del DOM). */
|
||||||
|
if (HOJA_ID && !vacio(m.id) && String(m.id) === HOJA_ID) WAM.cerrarSheet();
|
||||||
|
observarAltura(li);
|
||||||
return li;
|
return li;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -608,8 +652,10 @@
|
|||||||
for (var s = 0; s < sis.length; s++) frag.appendChild(sis[s]);
|
for (var s = 0; s < sis.length; s++) frag.appendChild(sis[s]);
|
||||||
for (var i = 0; i < mensajes.length; i++) frag.appendChild(WAM.renderMensaje(mensajes[i], i));
|
for (var i = 0; i < mensajes.length; i++) frag.appendChild(WAM.renderMensaje(mensajes[i], i));
|
||||||
cuerpo.innerHTML = "";
|
cuerpo.innerHTML = "";
|
||||||
|
if (RO) { RO.disconnect(); RO.observe(cuerpo); }
|
||||||
if (indEnCuerpo && ind) frag.appendChild(ind); /* conserva el indicador */
|
if (indEnCuerpo && ind) frag.appendChild(ind); /* conserva el indicador */
|
||||||
cuerpo.appendChild(frag);
|
cuerpo.appendChild(frag);
|
||||||
|
if (RO) for (var k = 0; k < cuerpo.children.length; k++) RO.observe(cuerpo.children[k]);
|
||||||
WAM.scrollAbajo();
|
WAM.scrollAbajo();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -685,11 +731,58 @@
|
|||||||
if (ESCRIBIENDO) WAM.scrollAbajo();
|
if (ESCRIBIENDO) WAM.scrollAbajo();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Desplazamiento INSTANTÁNEO al final (un scroll suave se quedaría en el
|
||||||
|
máximo del momento si el contenido crece durante la animación). */
|
||||||
WAM.scrollAbajo = function () {
|
WAM.scrollAbajo = function () {
|
||||||
var cuerpo = byId("chat-cuerpo");
|
var cuerpo = byId("chat-cuerpo");
|
||||||
if (cuerpo) cuerpo.scrollTop = cuerpo.scrollHeight;
|
if (!cuerpo) return;
|
||||||
|
vigilarCuerpo(cuerpo);
|
||||||
|
var prev = cuerpo.style.scrollBehavior;
|
||||||
|
cuerpo.style.scrollBehavior = "auto";
|
||||||
|
cuerpo.scrollTop = cuerpo.scrollHeight;
|
||||||
|
cuerpo.style.scrollBehavior = prev;
|
||||||
|
PEGADO = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* "Pegado abajo" (como WhatsApp): mientras el usuario no suba, cualquier
|
||||||
|
crecimiento del chat (imágenes/SVG que decodifican tarde, vídeo, la
|
||||||
|
pestaña Vista previa que pasa de oculta a visible, una fila repintada)
|
||||||
|
vuelve a bajar. Si el usuario sube, se respeta su posición. */
|
||||||
|
var PEGADO = true;
|
||||||
|
var RO = null;
|
||||||
|
|
||||||
|
function distanciaFondo(c) { return c.scrollHeight - c.scrollTop - c.clientHeight; }
|
||||||
|
|
||||||
|
function reajustar() {
|
||||||
|
var c = byId("chat-cuerpo");
|
||||||
|
if (c && PEGADO && distanciaFondo(c) > 1) WAM.scrollAbajo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function vigilarCuerpo(cuerpo) {
|
||||||
|
if (cuerpo.__wamVigilado) return;
|
||||||
|
cuerpo.__wamVigilado = true;
|
||||||
|
cuerpo.addEventListener("scroll", function () {
|
||||||
|
if (cuerpo.clientHeight > 0) PEGADO = distanciaFondo(cuerpo) < 60;
|
||||||
|
}, { passive: true });
|
||||||
|
if (typeof ResizeObserver === "function") {
|
||||||
|
RO = new ResizeObserver(reajustar);
|
||||||
|
RO.observe(cuerpo);
|
||||||
|
for (var k = 0; k < cuerpo.children.length; k++) RO.observe(cuerpo.children[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Observa la altura de una fila (aunque aún no esté en el DOM). Sin
|
||||||
|
ResizeObserver, recurre al load de sus imágenes/vídeos. */
|
||||||
|
function observarAltura(li) {
|
||||||
|
if (RO) { RO.observe(li); return; }
|
||||||
|
if (typeof ResizeObserver === "function") return; /* aún sin vigilar: vigilarCuerpo lo hará */
|
||||||
|
var medios = li.querySelectorAll("img, video");
|
||||||
|
for (var k = 0; k < medios.length; k++) {
|
||||||
|
medios[k].addEventListener("load", reajustar, { once: true });
|
||||||
|
medios[k].addEventListener("loadedmetadata", reajustar, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Hoja inferior iOS genérica (B la crea/destruye) en #pantalla.
|
/* Hoja inferior iOS genérica (B la crea/destruye) en #pantalla.
|
||||||
alClic: manejador opcional de presentación para las filas. */
|
alClic: manejador opcional de presentación para las filas. */
|
||||||
function montarHoja(titulo, contenido, pie, alClic) {
|
function montarHoja(titulo, contenido, pie, alClic) {
|
||||||
@@ -741,20 +834,26 @@
|
|||||||
}
|
}
|
||||||
if (!total) html = '<div class="sheet-vacio">Sin opciones</div>';
|
if (!total) html = '<div class="sheet-vacio">Sin opciones</div>';
|
||||||
montarHoja(titulo, html, total ? "Toca un elemento para seleccionarlo" : "");
|
montarHoja(titulo, html, total ? "Toca un elemento para seleccionarlo" : "");
|
||||||
|
HOJA_ID = vacio(m.id) ? "" : String(m.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Hoja "Todas las opciones" de un mensaje con >3 botones (m = Mensaje o
|
/* Hoja "Todas las opciones" de un mensaje con >3 botones (m = Mensaje o
|
||||||
índice; fila = su li.wa-row). URL/llamada primero, separador, respuestas.
|
índice; fila = su li.wa-row). URL/llamada primero, separador, respuestas.
|
||||||
Pulsar una respuesta reenvía el clic al botón [data-btn] (oculto) de la
|
Pulsar una respuesta reenvía el clic al botón [data-btn] (oculto) de la
|
||||||
burbuja, así main.js/interactivity.js lo atienden igual que en línea. */
|
burbuja, así main.js/interactivity.js lo atienden igual que en línea. */
|
||||||
|
/* Última li.wa-row pintada de ese mensaje (o null) */
|
||||||
|
function filaDe(msg) {
|
||||||
|
if (!msg || vacio(msg.id)) return null;
|
||||||
|
var filas = document.querySelectorAll("#chat-cuerpo .wa-row");
|
||||||
|
for (var k = filas.length - 1; k >= 0; k--) if (filas[k].getAttribute("data-id") === String(msg.id)) return filas[k];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
WAM.abrirOpciones = function (m, fila) {
|
WAM.abrirOpciones = function (m, fila) {
|
||||||
var mensajes = estado().mensajes || [];
|
var mensajes = estado().mensajes || [];
|
||||||
var msg = (m && typeof m === "object" ? m : (typeof m === "number" ? mensajes[m] : null)) || {};
|
var msg = (m && typeof m === "object" ? m : (typeof m === "number" ? mensajes[m] : null)) || {};
|
||||||
var li = fila || null;
|
var li = fila || null;
|
||||||
if (!li && msg.id) {
|
if (!li) li = filaDe(msg);
|
||||||
var filas = document.querySelectorAll("#chat-cuerpo .wa-row");
|
|
||||||
for (var k = filas.length - 1; k >= 0 && !li; k--) if (filas[k].getAttribute("data-id") === String(msg.id)) li = filas[k];
|
|
||||||
}
|
|
||||||
var bs = Array.isArray(msg.botones) ? msg.botones.slice(0, 10) : [];
|
var bs = Array.isArray(msg.botones) ? msg.botones.slice(0, 10) : [];
|
||||||
var enlaces = "", respuestas = "";
|
var enlaces = "", respuestas = "";
|
||||||
for (var j = 0; j < bs.length; j++) {
|
for (var j = 0; j < bs.length; j++) {
|
||||||
@@ -773,20 +872,24 @@
|
|||||||
}
|
}
|
||||||
var html = (enlaces ? '<div class="sheet-grupo">' + enlaces + "</div>" : "") +
|
var html = (enlaces ? '<div class="sheet-grupo">' + enlaces + "</div>" : "") +
|
||||||
(respuestas ? '<div class="sheet-grupo">' + respuestas + "</div>" : "");
|
(respuestas ? '<div class="sheet-grupo">' + respuestas + "</div>" : "");
|
||||||
montarHoja("Todas las opciones", html || '<div class="sheet-vacio">Sin opciones</div>', "", function (e, t) {
|
var hojaOp = montarHoja("Todas las opciones", html || '<div class="sheet-vacio">Sin opciones</div>', "", function (e, t) {
|
||||||
if (t.closest("a.sheet-fila")) { WAM.cerrarSheet(); return; }
|
if (t.closest("a.sheet-fila")) { WAM.cerrarSheet(); return; }
|
||||||
var op = t.closest("[data-opcion]");
|
var op = t.closest("[data-opcion]");
|
||||||
if (!op) return;
|
if (!op) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (op.getAttribute("aria-disabled") === "true") return;
|
if (op.getAttribute("aria-disabled") === "true") return;
|
||||||
var destino = li ? li.querySelector('[data-btn="' + op.getAttribute("data-opcion") + '"]') : null;
|
/* la fila viva AHORA (el editor puede haberla repintado con la hoja abierta) */
|
||||||
|
var viva = li && li.isConnected ? li : filaDe(msg);
|
||||||
|
var destino = viva ? viva.querySelector('[data-btn="' + op.getAttribute("data-opcion") + '"]') : null;
|
||||||
WAM.cerrarSheet();
|
WAM.cerrarSheet();
|
||||||
if (destino) destino.click(); /* burbujea a #chat-cuerpo (delegación de main.js) */
|
if (destino) destino.click(); /* burbujea a #chat-cuerpo (delegación de main.js) */
|
||||||
});
|
});
|
||||||
|
if (hojaOp) HOJA_ID = vacio(msg.id) ? "" : String(msg.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
WAM.cerrarSheet = function () {
|
WAM.cerrarSheet = function () {
|
||||||
var hoja = byId("sheet");
|
var hoja = byId("sheet");
|
||||||
if (hoja && hoja.parentNode) hoja.parentNode.removeChild(hoja);
|
if (hoja && hoja.parentNode) hoja.parentNode.removeChild(hoja);
|
||||||
|
HOJA_ID = "";
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user