PageSourceSearch

https://sportingclubhuelva.com/js/wf_abrir_imagen.js?v=1776796536

js sportingclubhuelva.com collected 2026-10-02 13:36:44 UTC 16,094 bytes, 526 lines download raw bytes

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.