1/* ---------------------------------------------------------------------------------------------------- 2 * wf_abrir_imagen 3 * ---------------------------------------------------------------------------------------------------- 4 * Visor de imágenes en JavaScript puro para Webforever. 5 * 6 * FUNCIONAMIENTO GENERAL 7 * ---------------------------------------------------------------------------------------------------- 8 * Esta utilidad permite abrir cualquier imagen que tenga la clase `.wf_abrir_imagen` 9 * en un visor a pantalla completa con fondo oscurecido. 10 * 11 * El visor: 12 * - abre la imagen al hacer clic 13 * - oscurece el fondo 14 * - permite ampliar y reducir con botones 15 * - permite hacer zoom con la rueda del ratón 16 * - permite arrastrar la imagen cuando está ampliada 17 * - permite cerrar con ESC, con el botón de cierre o pulsando fuera de la imagen 18 * 19 * PREPARACIÃN HTML 20 * ---------------------------------------------------------------------------------------------------- 21 * Basta con añadir la clase `wf_abrir_imagen` a una imagen: 22 * 23 * <img 24 * src='/ruta/imagen.jpg' 25 * alt='Descripción' 26 * class='img-fluid wf_abrir_imagen' 27 * > 28 * 29 * Si se quiere abrir una imagen grande distinta de la miniatura: 30 * 31 * <img 32 * src='/ruta/miniatura.jpg' 33 * data-wf-src='/ruta/imagen-grande.jpg' 34 * alt='Descripción' 35 * class='img-fluid wf_abrir_imagen' 36 * > 37 * 38 * En ese caso: 39 * - `src` muestra la miniatura en la página 40 * - `data-wf-src` indica la imagen real que se abrirá en el visor 41 * 42 * INICIALIZACIÃN 43 * ---------------------------------------------------------------------------------------------------- 44 * La clase debe inicializarse una vez, normalmente al cargar el documento: 45 * 46 * document.addEventListener(`DOMContentLoaded`, () => 47 * { 48 * new wf_abrir_imagen(); 49 * }); 50 * 51 * Si se desea cambiar el selector por defecto: 52 * 53 * new wf_abrir_imagen( 54 * { 55 * selector : `.mi_clase_personalizada` 56 * }); 57 * 58 * OPCIONES DISPONIBLES 59 * ---------------------------------------------------------------------------------------------------- 60 * selector 61 * Selector CSS que se usará para detectar las imágenes pulsables. 62 * Por defecto: `.wf_abrir_imagen` 63 * 64 * zoom_min 65 * Zoom mÃnimo permitido. 66 * Por defecto: 1 67 * 68 * zoom_max 69 * Zoom máximo permitido. 70 * Por defecto: 6 71 * 72 * zoom_paso 73 * Cantidad que se suma o se resta en cada ampliación o reducción. 74 * Por defecto: 0.2 75 * 76 * zoom_doble_click 77 * Zoom aplicado al hacer doble clic sobre la imagen cuando aún no está ampliada. 78 * Por defecto: 1.8 79 * 80 * EJEMPLO COMPLETO DE INICIALIZACIÃN 81 * ---------------------------------------------------------------------------------------------------- 82 * document.addEventListener(`DOMContentLoaded`, () => 83 * { 84 * new wf_abrir_imagen( 85 * { 86 * selector : `.wf_abrir_imagen`, 87 * zoom_min : 1, 88 * zoom_max : 8, 89 * zoom_paso : 0.25, 90 * zoom_doble_click : 2 91 * }); 92 * }); 93 * 94 * COMPORTAMIENTO DEL VISOR 95 * ---------------------------------------------------------------------------------------------------- 96 * 1. Al pulsar en una imagen válida: 97 * - se localiza la URL a mostrar 98 * - se abre el overlay 99 * - se bloquea el scroll del body 100 * - se reinicia el zoom y la posición 101 * 102 * 2. Al ampliar: 103 * - la imagen aumenta su escala 104 * - si se usa la rueda del ratón, el zoom intenta centrarse en la zona del cursor 105 * 106 * 3. Al arrastrar: 107 * - solo funciona si la imagen está ampliada 108 * - se actualiza la traslación X/Y 109 * 110 * 4. Al cerrar: 111 * - se oculta el overlay 112 * - se desbloquea el scroll del body 113 * - se limpia la imagen cargada 114 * - se restablecen zoom y desplazamientos 115 * 116 * ESTRUCTURA HTML INTERNA QUE CREA LA CLASE 117 * ---------------------------------------------------------------------------------------------------- 118 * La propia clase inserta automáticamente en el <body> una estructura similar a esta: 119 * 120 * <div id='wf_img_overlay'> 121 * <div class='wf_abrir_imagen_barra'> 122 * ... 123 * </div> 124 * <div class='wf_abrir_imagen_stage'> 125 * <img class='wf_abrir_imagen_visual'> 126 * </div> 127 * </div> 128 * 129 * No hace falta escribir ese HTML manualmente. 130 * 131 * REQUISITOS VISUALES 132 * ---------------------------------------------------------------------------------------------------- 133 * Esta utilidad está pensada para convivir con: 134 * - Bootstrap 4 135 * - Font Awesome 136 * 137 * Bootstrap 4 se utiliza para las clases visuales de botones y dis
137posición. 138 * Font Awesome se usa para los iconos de ampliar, reducir y cerrar. 139 * 140 * CSS NECESARIO 141 * ---------------------------------------------------------------------------------------------------- 142 * Además del JavaScript, se necesita el CSS del visor: 143 * 144 * #wf_img_overlay 145 * { 146 * position: fixed; 147 * inset: 0; 148 * z-index: 99999; 149 * background: rgba(0, 0, 0, 0.88); 150 * } 151 * 152 * .wf_abrir_imagen_no_scroll 153 * { 154 * overflow: hidden; 155 * } 156 * 157 * .wf_abrir_imagen_barra 158 * { 159 * position: absolute; 160 * top: 15px; 161 * left: 15px; 162 * right: 15px; 163 * z-index: 2; 164 * } 165 * 166 * .wf_abrir_imagen_stage 167 * { 168 * position: absolute; 169 * inset: 0; 170 * display: flex; 171 * align-items: center; 172 * justify-content: center; 173 * overflow: hidden; 174 * padding: 70px 20px 20px 20px; 175 * } 176 * 177 * .wf_abrir_imagen_visual 178 * { 179 * max-width: 95vw; 180 * max-height: 85vh; 181 * user-select: none; 182 * transform-origin: center center; 183 * transition: transform 0.08s linear; 184 * } 185 * 186 * CONTROLES DISPONIBLES PARA EL USUARIO 187 * ---------------------------------------------------------------------------------------------------- 188 * Ratón: 189 * - clic sobre imagen de la página: abrir 190 * - rueda arriba: ampliar 191 * - rueda abajo: reducir 192 * - arrastre con botón izquierdo: mover imagen ampliada 193 * - doble clic: ampliar rápido o volver al estado inicial 194 * 195 * Teclado: 196 * - ESC: cerrar 197 * - + o = : ampliar 198 * - - o _ : reducir 199 * 200 * Botones del visor: 201 * - botón + : ampliar 202 * - botón - : reducir 203 * - botón X : cerrar 204 * 205 * CONSEJOS DE USO 206 * ---------------------------------------------------------------------------------------------------- 207 * - Si las imágenes del contenido son miniaturas, conviene usar `data-wf-src` 208 * para abrir la versión grande. 209 * 210 * - Si el contenido se carga por AJAX, basta con que las imágenes nuevas tengan 211 * la clase `.wf_abrir_imagen`. Como el click se controla por delegación sobre 212 * `document`, seguirán funcionando sin reinicializar listeners por imagen. 213 * 214 * - Si se usa dentro de módulos o fichas dinámicas de WF, la clase puede cargarse 215 * una sola vez a nivel global. 216 * 217 * LIMITACIONES ACTUALES 218 * ---------------------------------------------------------------------------------------------------- 219 * Esta versión está pensada como visor individual. 220 * No incluye todavÃa: 221 * - navegación entre imágenes (anterior / siguiente) 222 * - swipe táctil avanzado 223 * - rotación 224 * - descarga 225 * - miniaturas internas 226 * 227 * POSIBLES MEJORAS FUTURAS 228 * ---------------------------------------------------------------------------------------------------- 229 * - galerÃa completa 230 * - swipe móvil 231 * - botones anterior / siguiente 232 * - contador de imágenes 233 * - animación de entrada y salida 234 * - zoom táctil con dos dedos real 235 * 236 * RESUMEN RÃPIDO 237 * ---------------------------------------------------------------------------------------------------- 238 * 1. Añadir clase `.wf_abrir_imagen` a las imágenes 239 * 2. Opcionalmente añadir `data-wf-src` con la imagen grande 240 * 3. Cargar el CSS del visor 241 * 4. Inicializar: 242 * 243 * document.addEventListener(`DOMContentLoaded`, () => 244 * { 245 * new wf_abrir_imagen(); 246 * }); 247 * 248 * Con eso ya queda operativo el sistema. 249 * ---------------------------------------------------------------------------------------------------- */ 250class wf_abrir_imagen 251{ 252 constructor(opciones = {}) 253 { 254 this.selector = opciones.selector || `.wf_abrir_imagen`; 255 this.zoom_min = opciones.zoom_min || 1; 256 this.zoom_max = opciones.zoom_max || 6; 257 this.zoom_paso = opciones.zoom_paso || 0.2; 258 this.zoom_doble_click = opciones.zoom_doble_click || 1.8; 259 260 this.overlay = null; 261 this.img = null; 262 this.btn_mas = null; 263 this.btn_menos = null; 264 this.btn_cerrar = null; 265 this.info = null; 266 267 this.scale = 1; 268 this.translateX = 0; 269 this.translateY = 0; 270 271 this.drag_activo = false; 272 this.drag_inicio_x = 0; 273 this.drag_inicio_y = 0; 274 this.drag_base_x = 0; 275 this.drag_base_y = 0; 276 277 this._crear_html(); 278 this._eventos_globales(); 279 } 280 281 _crear_html() 282 { 283 if (document.querySelector(`#wf_abrir_imagen_overlay`)) 284 { 285 this.overlay = document.querySelector(`#wf_abrir_imagen_overlay`); 286 this.img = this.overlay.querySelector(`.wf_abrir_imagen_visual`); 287 this.btn_mas = this.overlay.querySelector(`.wf_abrir_imagen_btn_mas`); 288 this.btn_menos = this.overlay.querySelector(`.wf_abrir_imagen_btn_menos`); 289 this.btn_cerrar = this.overlay.querySelector(`.wf_abrir_imagen_btn_cerrar`); 290 this.info = this.overlay.querySelector(`.wf_abrir_imagen_info`); 291 return; 292 } 293 294 const html = ` 295 <div id='wf_abrir_imagen_overlay' class='d-none'> 296 <div class='wf_abrir_imagen_barra d-flex align-items-center justify-content-between'> 297 <div class='wf_abrir_imagen_info small text-white'>100%</div> 298 299 <div class='btn-group btn-group-sm'> 300 <button type='button' class='btn btn-light wf_abrir_imagen_btn_menos' aria-label='Reducir'> 301 <i class='fa fa-search-minus'></i> 302 </button> 303 <button type='button' class='btn btn-light wf_abrir_imagen_btn_mas' aria-label='Ampliar'> 304 <i class='fa fa-search-plus'></i> 305 </button> 306 <button type='button' class='btn btn-danger wf_abrir_imagen_btn_cerrar' aria-label='Cerrar'> 307 <i class='fa fa-times'></i> 308 </button> 309 </div> 310 </div> 311 312 <div class='wf_abrir_imagen_stage'> 313 <img class='wf_abrir_imagen_visual' src='' alt=''> 314 </div> 315 </div> 316 `; 317 318 document.body.insertAdjacentHTML(`beforeend`, html); 319 320 this.overlay = document.querySelector(`#wf_abrir_imagen_overlay`); 321 this.img = this.overlay.querySelector(`.wf_abrir_imagen_visual`); 322 this.btn_mas = this.overlay.querySelector(`.wf_abrir_imagen_btn_mas`); 323 this.btn_menos = this.overlay.querySelector(`.wf_abrir_imagen_btn_menos`); 324 this.btn_cerrar = this.overlay.querySelector(`.wf_abrir_imagen_btn_cerrar`); 325 this.info = this.overlay.querySelector(`.wf_abrir_imagen_info`); 326 } 327 328 _eventos_globales() 329 { 330 document.addEventListener(`click`, (ev) => 331 { 332 const disparador = ev.target.closest(this.selector); 333 if (!disparador) return; 334 335 console.groupCollapsed(`wf_abrir_imagen > click`);
336 console.log(`target:`, ev.target); 337 console.log(`selector:`, this.selector); 338 console.log(`disparador encontrado:`, disparador); 339 340 if (disparador) 341 { 342 console.log(`tagName:`, disparador.tagName); 343 console.log(`className:`, disparador.className); 344 console.log(`src:`, disparador.getAttribute(`src`)); 345 console.log(`href:`, disparador.getAttribute(`href`)); 346 console.log(`data-wf-src:`, disparador.getAttribute(`data-wf-src`)); 347 console.log(`alt:`, disparador.getAttribute(`alt`)); 348 } 349 else 350 { 351 console.log(`No hay elemento coincidente con closest().`); 352 } 353 354 console.groupEnd(); 355 356 if (!disparador) return; 357 358 ev.preventDefault(); 359 this.abrir(disparador); 360 }); 361 362 this.btn_cerrar.addEventListener(`click`, () => this.cerrar()); 363 this.btn_mas.addEventListener(`click`, () => this.zoom_sumar()); 364 this.btn_menos.addEventListener(`click`, () => this.zoom_restar()); 365 366 this.overlay.addEventListener(`click`, (ev) => 367 { 368 if (ev.target === this.overlay || ev.target.classList.contains(`wf_abrir_imagen_stage`)) 369 this.cerrar(); 370 }); 371 372 document.addEventListener(`keydown`, (ev) => 373 { 374 if (this.overlay.classList.contains(`d-none`)) return; 375 376 if (ev.key === `Escape`) this.cerrar(); 377 if (ev.key === `+` || ev.key === `=`) this.zoom_sumar(); 378 if (ev.key === `-` || ev.key === `_`) this.zoom_restar(); 379 }); 380 381 this.img.addEventListener(`wheel`, (ev) => 382 { 383 ev.preventDefault(); 384 385 if (ev.deltaY < 0) this.zoom_sumar(ev.clientX, ev.clientY); 386 else this.zoom_restar(ev.clientX, ev.clientY); 387 }, { passive: false }); 388 389 this.img.addEventListener(`dblclick`, (ev) => 390 { 391 ev.preventDefault(); 392 393 if (this.scale <= 1.05) 394 this.zoom_a(this.zoom_doble_click, ev.clientX, ev.clientY); 395 else 396 this.reset(); 397 }); 398 399 this.img.addEventListener(`mousedown`, (ev) => 400 { 401 if (this.scale <= 1) return; 402 if (ev.button !== 0) return; 403 404 ev.preventDefault(); 405 406 this.drag_activo = true; 407 this.drag_inicio_x = ev.clientX; 408 this.drag_inicio_y = ev.clientY; 409 this.drag_base_x = this.translateX; 410 this.drag_base_y = this.translateY; 411 412 this.img.style.cursor = `grabbing`; 413 }); 414 415 document.addEventListener(`mousemove`, (ev) => 416 { 417 if (!this.drag_activo) return; 418 419 this.translateX = this.drag_base_x + (ev.clientX - this.drag_inicio_x); 420 this.translateY = this.drag_base_y + (ev.clientY - this.drag_inicio_y); 421 422 this._aplicar_transform(); 423 }); 424 425 document.addEventListener(`mouseup`, () => 426 { 427 this.drag_activo = false; 428 if (this.img) this.img.style.cursor = this.scale > 1 ? `grab` : `default`; 429 }); 430 } 431 432 abrir(imagen) 433 { 434 console.groupCollapsed(`wf_abrir_imagen > abrir`); 435 436 const data_src = imagen.getAttribute(`data-wf-src`); 437 const src_attr = imagen.getAttribute(`src`); 438 const href = imagen.getAttribute(`href`); 439 440 console.log(`elemento:`, imagen); 441 console.log(`data-wf-src:`, data_src); 442 console.log(`src:`, src_attr); 443 console.log(`href:`, href); 444 445 const src = data_src || src_attr || href; 446 const alt = imagen.getAttribute(`alt`) || ``; 447 448 console.log(`src final:`, src); 449 console.log(`alt final:`, alt); 450 451 console.groupEnd(); 452 453 if (!src) return; 454 455 this.img.setAttribute(`src`, src); 456 this.img.setAttribute(`alt`, alt); 457 458 this.reset(); 459 460 this.overlay.classList.remove(`d-none`); 461 document.body.classList.add(`wf_abrir_imagen_no_scroll`); 462 } 463 464 cerrar() 465 { 466 this.overlay.classList.add(`d-none`); 467 document.body.classList.remove(`wf_abrir_imagen_no_scroll`); 468 this.img.setAttribute(`src`, ``); 469 this.img.setAttribute(`alt`, ``); 470 this.reset(); 471 } 472 473 reset() 474 { 475 this.scale = 1; 476 this.translateX = 0; 477 this.translateY = 0; 478 this._aplicar_transform(); 479 this.img.style.cursor = `default`; 480 } 481 482 zoom_sumar(cx = null, cy = null) 483 { 484 this.zoom_a(this.scale + this.zoom_paso, cx, cy); 485 } 486 487 zoom_restar(cx = null, cy = null) 488 { 489 this.zoom_a(this.scale - this.zoom_paso, cx, cy); 490 } 491 492 zoom_a(nuevo_zoom, cx = null, cy = null) 493 { 494 nuevo_zoom = Math.max(this.zoom_min, Math.min(this.zoom_max, nuevo_zoom));
495 496 if (nuevo_zoom === this.scale) return; 497 498 if (cx !== null && cy !== null) 499 { 500 const rect = this.img.getBoundingClientRect(); 501 const ox = cx - rect.left - rect.width / 2; 502 const oy = cy - rect.top - rect.height / 2; 503 const ratio = nuevo_zoom / this.scale; 504 505 this.translateX -= ox * (ratio - 1); 506 this.translateY -= oy * (ratio - 1); 507 } 508 509 this.scale = nuevo_zoom; 510 511 if (this.scale <= 1) 512 { 513 this.translateX = 0; 514 this.translateY = 0; 515 } 516 517 this._aplicar_transform(); 518 this.img.style.cursor = this.scale > 1 ? `grab` : `default`; 519 } 520 521 _aplicar_transform() 522 { 523 this.img.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`; 524 this.info.textContent = `${Math.round(this.scale * 100)}%`; 525 } 526}
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.