1/* ============================================================================ 2 OFFCANVAS â abrir y cerrar el panel lateral derecho 3 ============================================================================ 4 El comportamiento del componente `<x-offcanvas>` 5 (`resources/views/components/offcanvas.blade.php`). Los estilos van aparte, 6 en la sección 51 de `public/css/app.theme.css`. 7 8 POR QUà ES UN ARCHIVO PROPIO Y NO UNA FUNCIÃN EN `app.custom.js` 9 --------------------------------------------------------------- 10 Porque `app.custom.js` y `app.theme.js` salen los dos de `partials/footer`, 11 que incluyen también las 13 landings públicas. Un offcanvas es del panel, y 12 no hay ninguna razón para que una landing de broker baje este código. Ãste se 13 enlaza sólo desde `layouts/app` y `layouts/master`. 14 15 CÃMO SE USA, desde el HTML 16 -------------------------- 17 Abrir: <button data-offcanvas-open="idDelPanel"> 18 Cerrar: <button data-offcanvas-close> (adentro del panel) 19 También cierran el fondo, la à del encabezado y la tecla Escape. 20 21 El botón que abre puede pedir dos cosas más: 22 23 data-offcanvas-title="Editar etiqueta" cambia el encabezado 24 data-offcanvas-load="/tags/7/form" trae ESE html y lo pone adentro 25 26 Con `load`, el contenido que vino con la página (`[data-offcanvas-slot]`) se esconde y en su 27 lugar va lo que devuelva la URL (`[data-offcanvas-remote]`); sin `load` se ve el slot. ESTE 28 ARCHIVO NO SABE QUà ESTà CARGANDO: recibe una dirección y pone el resultado. 29 30 EVENTOS, para lo que el formulario necesite y el panel no tiene por qué saber: 31 32 offcanvas:opened al abrir detail.container = el slot 33 offcanvas:loaded tras el AJAX detail.container = el contenedor remoto 34 35 En `/tags` y `/categories` es lo que engancha el `stringToSlug` del campo slug, que es del 36 formulario y no del panel. 37 38 FORMULARIOS QUE SE GUARDAN SIN SALIR DEL PANEL (2026-09-25) 39 ------------------------------------------------------------ 40 <form data-offcanvas-ajax ...> 41 42 Un formulario con ese atributo, adentro de un panel, se envÃa por AJAX y LO QUE DEVUELVA EL 43 SERVIDOR REEMPLAZA EL CONTENIDO DEL PANEL: el mismo formulario con los errores (422) o lo que 44 haya que mostrar después de guardar (200). Nació para "Registrar pago", que vive en el panel 45 de detalle de una suscripción y ese panel se abre desde varias pantallas: con un envÃo normal, 46 un error de validación recargarÃa la página y cerrarÃa el panel. Viaja con `FormData`, asà que 47 los archivos adjuntos también. 48 49 PESTAÃAS QUE TRAEN SU CONTENIDO AL ABRIRSE (2026-09-30) 50 -------------------------------------------------------- 51 <div class="tab-pane" id="x" data-cargar-al-abrir="/orders/30457/history"></div> 52 53 Una pestaña de Bootstrap con ese atributo nace vacÃa y pide su html la PRIMERA vez que se 54 abre. Nació para el "Historial" del detalle de un pedido, que sale de la tienda y no de 55 nuestra base: pedirlo junto con el detalle harÃa esperar a WooCommerce para dibujar la venta. 56 Vale adentro y afuera de un panel (la página de un pedido usa el mismo detalle). 57 58 LÃMITE CONOCIDO: lo que llega por `load` NO puede tener Livewire adentro. Un componente 59 inyectado con `innerHTML` no se inicializa y queda muerto. 60 61 TODO DELEGADO EN `document`, a propósito: los botones que abren pueden estar 62 en cualquier parte de la pantalla, y los de adentro del panel los puede haber 63 redibujado Livewire después de cargar la página. Con listeners atados uno por 64 uno al arrancar, ésos quedarÃan muertos y el botón no harÃa nada. 65 ========================================================================= */ 66(function () { 67 'use strict'; 68 69 function panelDe(id) { 70 return document.getElementById(id); 71 } 72 73 function fondoDe(id) { 74 return document.querySelector('[data-offcanvas-backdrop="' + id + '"]'); 75 } 76 77 function avisar(panel, evento, contenedor) { 78 panel.dispatchEvent(new CustomEvent(evento, { 79 bubbles: true, 80 detail: { container: contenedor } 81 })); 82 } 83 84 /** 85 * Trae el html de `url` y lo deja en el contenedor remoto del panel, escondiendo el 86 * contenido que vino con la página. El estado intermedio se dice: sin el "Cargandoâ¦", un 87 * servidor lento se ve igual que un panel roto. 88 */
89 function cargar(panel, url) { 90 var slot = panel.querySelector('[data-offcanvas-slot]'); 91 var remoto = panel.querySelector('[data-offcanvas-remote]'); 92 93 if (!remoto) return; 94 95 if (slot) slot.style.display = 'none'; 96 remoto.style.display = 'block'; 97 remoto.innerHTML = '<p class="text-muted">Cargandoâ¦</p>'; 98 99 var xhr = new XMLHttpRequest(); 100 xhr.open('GET', url, true); 101 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 102 xhr.setRequestHeader('Accept', 'text/html'); 103 104 xhr.onload = function () { 105 if (xhr.status >= 200 && xhr.status < 300) { 106 remoto.innerHTML = xhr.responseText; 107 avisar(panel, 'offcanvas:loaded', remoto); 108 } else { 109 remoto.innerHTML = '<p class="text-danger">No se pudo cargar el formulario.</p>'; 110 } 111 }; 112 113 xhr.onerror = function () { 114 remoto.innerHTML = '<p class="text-danger">Error de conexión.</p>'; 115 }; 116 117 xhr.send(); 118 } 119 120 /** Vuelve al contenido que vino con la página y descarta el remoto, si lo hubo. */ 121 function mostrarSlot(panel) { 122 var slot = panel.querySelector('[data-offcanvas-slot]'); 123 var remoto = panel.querySelector('[data-offcanvas-remote]'); 124 125 if (slot) slot.style.display = ''; 126 if (remoto) { 127 remoto.style.display = 'none'; 128 remoto.innerHTML = ''; 129 } 130 } 131 132 function abrir(id) { 133 var panel = panelDe(id); 134 var fondo = fondoDe(id); 135 136 if (!panel) return; 137 138 panel.classList.add('is-open'); 139 panel.setAttribute('aria-hidden', 'false'); 140 141 if (fondo) { 142 fondo.classList.add('is-open'); 143 fondo.setAttribute('aria-hidden', 'false'); 144 } 145 146 // El panel scrollea por dentro; sin esto, la rueda mueve la página de atrás. 147 document.body.style.overflow = 'hidden'; 148 } 149 150 function cerrar(panel) { 151 if (!panel) return; 152 153 var fondo = fondoDe(panel.id); 154 155 panel.classList.remove('is-open'); 156 panel.setAttribute('aria-hidden', 'true'); 157 158 if (fondo) { 159 fondo.classList.remove('is-open'); 160 fondo.setAttribute('aria-hidden', 'true'); 161 } 162 163 document.body.style.overflow = ''; 164 } 165 166 function cerrarTodos() { 167 Array.prototype.forEach.call( 168 document.querySelectorAll('.cb-offcanvas.is-open'), 169 cerrar 170 ); 171 } 172 173 document.addEventListener('click', function (e) { 174 var abridor = e.target.closest('[data-offcanvas-open]'); 175 if (abridor) { 176 e.preventDefault(); 177 178 var id = abridor.getAttribute('data-offcanvas-open'); 179 var panel = panelDe(id); 180 if (!panel) return; 181 182 var titulo = abridor.getAttribute('data-offcanvas-title'); 183 if (titulo) { 184 var tituloEl = panel.querySelector('.cb-offcanvas-title'); 185 if (tituloEl) tituloEl.textContent = titulo; 186 } 187 188 var url = abridor.getAttribute('data-offcanvas-load'); 189 if (url) { 190 cargar(panel, url); 191 } else { 192 mostrarSlot(panel); 193 } 194 195 abrir(id); 196 197 if (!url) avisar(panel, 'offcanvas:opened', panel.querySelector('[data-offcanvas-slot]')); 198 return; 199 } 200 201 var cerrador = e.target.closest('[data-offcanvas-close]'); 202 if (cerrador) { 203 e.preventDefault(); 204 cerrar(cerrador.closest('.cb-offcanvas')); 205 return; 206 } 207 208 var fondo = e.target.closest('.cb-offcanvas-backdrop'); 209 if (fondo) { 210 cerrar(panelDe(fondo.getAttribute('data-offcanvas-backdrop'))); 211 } 212 }); 213 214 document.addEventListener('submit', function (e) { 215 var form = e.target.closest('form[data-offcanvas-ajax]'); 216 if (!form) return; 217 218 var remoto = form.closest('[data-offcanvas-remote]'); 219 if (!remoto) return; 220 221 e.preventDefault(); 222 223 // Un doble clic no puede mandar el pago dos veces. 224 var boton = form.querySelector('[type="submit"]'); 225 if (boton) { 226 if (boton.disabled) return; 227 boton.disabled = true; 228 } 229 230 var xhr = new XMLHttpRequest(); 231 xhr.open((form.getAttribute('method') || 'POST').toUpperCase(), form.action, true); 232 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 233 xhr.setRequestHeader('Accept', 'text/html'); 234 235 xhr.onload = function () { 236 if (xhr.status === 200 || xhr.status === 422) { 237 remoto.innerHTML = xhr.responseText; 238 remoto.scrollTop = 0; 239 var panel = remoto.closest('.cb-offcanvas'); 240 if (panel) avisar(panel, 'offcanvas:loaded', remoto); 241 } else { 242 if (boton) boton.disabled = false;
243 alert('No se pudo guardar (error ' + xhr.status + '). No se registró nada; probá de nuevo.'); 244 } 245 }; 246 247 xhr.onerror = function () { 248 if (boton) boton.disabled = false; 249 alert('Error de conexión. No se registró nada; probá de nuevo.'); 250 }; 251 252 xhr.send(new FormData(form)); 253 }); 254 255 document.addEventListener('keydown', function (e) { 256 if (e.key === 'Escape') cerrarTodos(); 257 }); 258 259 // LA PESTAÃA QUE TRAE SU CONTENIDO AL ABRIRSE. Mostrarla es cosa de Bootstrap; acá sólo se 260 // mira si su panel pide contenido y todavÃa no lo tiene. Con el clic y no con 261 // `shown.bs.tab`: ese evento es de jQuery y este archivo no depende de él. 262 document.addEventListener('click', function (e) { 263 var pestania = e.target.closest('[data-toggle="tab"]'); 264 if (!pestania) return; 265 266 var destino = pestania.getAttribute('href') || ''; 267 if (destino.charAt(0) !== '#') return; 268 269 var panel = document.getElementById(destino.slice(1)); 270 if (!panel) return; 271 272 var url = panel.getAttribute('data-cargar-al-abrir'); 273 if (!url || panel.getAttribute('data-cargado')) return; 274 275 // Se marca antes de pedir: dos clics seguidos no piden dos veces. Si falla se desmarca, 276 // para que volver a la pestaña lo reintente. 277 panel.setAttribute('data-cargado', '1'); 278 panel.innerHTML = '<p class="text-muted">Cargandoâ¦</p>'; 279 280 var fallo = function (mensaje) { 281 panel.removeAttribute('data-cargado'); 282 panel.innerHTML = '<p class="text-danger">' + mensaje + '</p>'; 283 }; 284 285 var xhr = new XMLHttpRequest(); 286 xhr.open('GET', url, true); 287 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 288 xhr.setRequestHeader('Accept', 'text/html'); 289 290 xhr.onload = function () { 291 if (xhr.status >= 200 && xhr.status < 300) { 292 panel.innerHTML = xhr.responseText; 293 } else { 294 fallo('No se pudo cargar.'); 295 } 296 }; 297 298 xhr.onerror = function () { 299 fallo('Error de conexión.'); 300 }; 301 302 xhr.send(); 303 }); 304 305 // El panel puede venir ABIERTO desde el servidor: es lo que pasa cuando el 306 // formulario falló y hay que mostrar el error adentro. En ese caso la clase 307 // `is-open` ya está puesta en el HTML, pero el fondo y el bloqueo del scroll 308 // no, asà que se completan acá. 309 document.addEventListener('DOMContentLoaded', function () { 310 Array.prototype.forEach.call( 311 document.querySelectorAll('.cb-offcanvas.is-open'), 312 function (panel) { 313 abrir(panel.id); 314 315 // Y puede venir pidiendo que se rearme su contenido remoto: es lo que pasa 316 // cuando falló el formulario que se habÃa traÃdo por AJAX. La página se recargó 317 // y ese html ya no está, asà que se vuelve a pedir. 318 var url = panel.getAttribute('data-offcanvas-autoload'); 319 if (url) cargar(panel, url); 320 } 321 ); 322 }); 323})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.