PageSourceSearch

https://elperuano.pe/fenomeno-el-nino/components/archivo/archivo.j…FRUDPwj0jO6sVxZvN5ZuXTgLxJqktlh1lWBOPxtBLGQ

js elperuano.pe collected 2026-10-01 09:11:24 UTC 44,438 bytes, 978 lines download raw bytes

1// Componente: Archivo
2//
3// Un carrusel con las fotografias del Fenomeno que guarda el Archivo Historico
4// del diario. La seccion nace oculta y solo aparece si hay fotos: sin credencial
5// del archivo digital el API devuelve lista vacia, y una seccion vacia al final
6// de la portada se lee como algo roto.
7(function () {
8    'use strict';
9
10    var api = FELN.api;
11
12    FELN.componente('archivo', {
13        async init(root) {
14            var ref = function (n) { return root.querySelector('[data-ref="' + n + '"]'); };
15
16            var tira = ref('tira');
17            var antes = ref('antes');
18            var despues = ref('despues');
19
20            /* La seccion nace con esqueleto para no dar un salto al llegar las
21               fotos. Si no hay ninguna, se retira entera: una seccion de
22               fotografias sin fotografias no es una seccion, es un hueco. */
23            function retirar() {
24                root.remove();
25            }
26
27            var fotos;
28            try {
29                fotos = await api.getJson('/api/archivo/fotos');
30            } catch (e) {
31                console.warn('[archivo] no se pudo cargar:', e);
32                retirar();
33                return;
34            }
35
36            if (!Array.isArray(fotos) || fotos.length === 0) { retirar(); return; }
37
38            /* La tira lleva TRES copias del conjunto y se empieza en la de en
39               medio.
40               
41               Es lo que hace el giro infinito sin trucos de animacion: cuando el
42               desplazamiento se sale por un lado, se reposiciona de golpe --sin
43               animar-- una anchura de conjunto hacia el otro. Como las tres
44               copias son identicas, el salto cae sobre una imagen igual a la que
45               habia y no se percibe.
46               
47               Tres y no dos porque hace falta margen a los DOS lados: con dos
48               solo se puede girar hacia delante, y al retroceder desde el
49               principio te chocas con el borde.
50               
51               Las copias no las ve quien navega con lector de pantalla ni con
52               teclado: son la misma fotografia repetida, y anunciarla tres veces
53               convertiria la seccion en un galimatias. */
54            var unaVez = fotos.map(pintar).join('');
55            /* Se sustituye el esqueleto por las fotos. El atributo que lo
56               marcaba como "cargando" se quita aqui, en el mismo paso: si se
57               quitara antes, las tarjetas de espera se verian un instante con su
58               texto de mentira. */
59            tira.innerHTML = unaVez + copia(unaVez) + copia(unaVez);
60            root.removeAttribute('data-cargando');
61
62            /* El desplazamiento va por pixeles y no por indice.
63             *
64             * Cuantas fotos caben depende del ancho, y llevar la cuenta a mano
65             * obliga a recalcularla en cada giro de pantalla. Moviendo el ancho
66             * visible menos un poco, siempre queda una foto a la vista que sirve
67             * de ancla para saber por donde ibas. */
68            /* DE GOLPE quiere decir de golpe, y `behavior: 'auto'` no lo hace.
69             *
70             * ---- EL FALLO QUE ESTO ARREGLA ----
71             *
72             * Al pasar fotos con las flechas, cada cierto rato el carrusel «se iba
73             * al inicio» recorriendo TODAS las imagenes a la vista, en vez de
74             * girar sin que se notara.
75             *
76             * La causa: `.archivo-tira` lleva `scroll-behavior: smooth` en el CSS,
77             * y en `scrollTo` el valor `behavior: 'auto'` NO significa «al
78             * instante»: significa «usa lo que diga el CSS del elemento». Asi que
79             * lo que estaba escrito para ser un salto imperceptible de un conjunto
80             * entero era, en realidad, un desplazamiento ANIMADO de un conjunto
81             * entero. Eso es exactamente lo que se veia.
82             *
83             * El codigo decia «de golpe --sin animar--» en el comentario y hacia
84             * lo contrario. No habia manera de verlo leyendo la linea: hay que
85             * saber que 'auto' no es 'instant'.
86             *
87             * ---- Y por que no se usa `behavior: 'instant'` a secas ----
88             *
89             * Porque es mas nuevo que el resto de lo que soporta este portal, y si
90             * el navegador no lo entiende se cae otra vez al CSS y vuelve el
91             * fallo, sin ruido. Apagando el `scroll-behavior` en linea el
92             * navegador no tiene nada que interpretar. Se restaura de
92spues para
93             * que las flechas sigan yendo suaves. */
94            function alInstante(fn) {
95                var antes = tira.style.scrollBehavior;
96                tira.style.scrollBehavior = 'auto';
97                fn();
98
99                /* Se restaura en el siguiente cuadro, no en la linea siguiente.
100                 *
101                 * El navegador aplica el desplazamiento al recalcular el estilo,
102                 * y devolviendo `smooth` en la misma vuelta llega a tiempo de
103                 * animar el salto que se acaba de pedir. Es el mismo fallo, un
104                 * paso mas adelante. */
105                requestAnimationFrame(function () {
106                    tira.style.scrollBehavior = antes || '';
107                });
108            }
109
110            /* El ancho de UN conjunto, MEDIDO entre copias.
111             *
112             * Era `scrollWidth / 3`, y eso no es un tercio del ancho: la tira es
113             * un flex con `gap`, asi que el total son 3N tarjetas y 3N-1 huecos.
114             * Dividir por tres deja un error de dos tercios de hueco --unos 10 px--
115             * en CADA giro, y el error se acumula: despues de unas cuantas vueltas
116             * la tira ya no cae donde debia y el salto se empieza a notar.
117             *
118             * Medido entre el primer elemento de la copia 1 y el primero de la
119             * copia 2 es exacto por construccion, sea cual sea el hueco. */
120            function anchoConjunto() {
121                var hijos = tira.children;
122                var cuantas = fotos.length;
123
124                if (hijos.length >= cuantas * 2 && cuantas > 0)
125                    return hijos[cuantas].offsetLeft - hijos[0].offsetLeft;
126
127                // Respaldo: mejor un tercio aproximado que no girar.
128                return tira.scrollWidth / 3;
129            }
130
131            // Se arranca en la copia del medio para tener margen a los dos lados.
132            function alCentro() {
133                alInstante(function () { tira.scrollLeft = anchoConjunto(); });
134            }
135
136            /* Y aqui esta el giro: si el desplazamiento se sale de la copia
137               central, se corrige un conjunto entero, de golpe y sin animar. */
138            function recolocar() {
139                var uno = anchoConjunto();
140                if (uno <= 0) return;
141
142                if (tira.scrollLeft < uno * 0.5) {
143                    alInstante(function () { tira.scrollLeft += uno; });
144                } else if (tira.scrollLeft > uno * 1.5) {
145                    alInstante(function () { tira.scrollLeft -= uno; });
146                }
147            }
148
149            function mover(sentido) {
150                var paso = Math.max(240, tira.clientWidth - 80);
151                tira.scrollBy({ left: sentido * paso, behavior: 'smooth' });
152            }
153
154            antes.addEventListener('click', function () { mover(-1); });
155            despues.addEventListener('click', function () { mover(1); });
156
157            /* Las flechas ya no se apagan: con el giro infinito no hay
158               extremos a los que llegar. Antes se desactivaban al principio y al
159               final, que era lo correcto entonces y ahora seria mentira.
160               
161               El reposicionado va en el evento de desplazamiento, que es el
162               unico sitio donde se entera de todo: las flechas, el arrastre con
163               el dedo, la rueda del raton y el avance automatico. */
164            var reposo = null;
165
166            tira.addEventListener('scroll', function () {
167                /* Se recoloca cuando el desplazamiento SE PARA, no mientras
168                   corre: hacerlo a mitad de un movimiento suave lo cortaria en
169                   seco y se veria el tiron. Cada evento de scroll reinicia la
170                   cuenta, asi que solo entra al final.
171                   
172                   Con un temporizador y no con requestAnimationFrame: rAF no se
173                   ejecuta en pestañas de fondo, y ahi el guardia se quedaba
174                   encendido para siempre y el giro dejaba de funcionar hasta
175                   recargar. Los temporizadores se ralentizan en segundo plano,
176                   pero se ejecutan. */
177                clearTimeout(reposo);
178                reposo = setTimeout(recolocar, 120);
179            }, { passive: true });
180
181            window.addEventListener('resize', alCentro);
182            alCentro();
183
184            // ---- Que ande solo ----
185
186            /* Cada cinco segundos. Es un carrusel para mirar de reojo al final
187               de la pagina, no una presentacion: mas rapido no da tiempo a leer
188               el pie y mas lento parece parado. */
189            var PASO = 5000;
190
191            /* El primer paso, mucho antes que los demas.
192             *
193             * Con solo el intervalo, quien llega a la seccion se encuentra cinco
194             * segundos de quietud y da por hecho que el carrusel no se mueve.
195             * Un primer avance al segundo y pico deja claro que esto anda, y a
196             * partir de ahi el ritmo es el normal: mas rapido no daria tiempo a
197             * leer el pie de foto. */
198            var PRIMER_PASO = 1200;
199
200            var reloj = null;
201            var arranque = null;
202
203            /* Avanzar y ya esta: no hay final del que volver. El giro lo
204               resuelve recolocar() por debajo, asi que desde aqui la tira es
205               infinita hacia delante y hacia atras. */
206            function avanzar() { mover(1); }
207
208            function arrancarSolo() {
209                if (reloj || arranque) return;
210
211                arranque = setTimeout(function () {
212                    arranque = null;
213                    avanzar();
214                    reloj = setInterval(avanzar, PASO);
215                }, PRIMER_PASO);
216            }
217
218            function pararSolo() {
219                if (arranque) { clearTimeout(arranque); arranque = null; }
220                if (!reloj) return;
221                clearInterval(reloj);
222                reloj = null;
223            }
224
225            /* Se para cuando hay alguien mirando de cerca.
226             *
227             * Si se mueve mientras lees un pie de foto, lo pierdes; y si se
228             * mueve mientras arrastras con el dedo, pelea contigo. El foco
229             * cuenta tambien: quien navega con el teclado no puede perseguir un
230             * carrusel en marcha. */
231            ['mouseenter', 'focusin', 'touchstart', 'pointerdown'].forEach(function (ev) {
232                root.addEventListener(ev, pararSolo, { passive: true });
233            });
234
235            ['mouseleave', 'focusout'].forEach(function (ev) {
236                root.addEventListener(ev, arrancarSolo);
237            });
238
239            // Con la pestaña al fondo no tiene sentido seguir desplazando.
240            document.addEventListener('visibilitychange', function () {
241                if (document.hidden) pararSolo(); else arrancarSolo();
242            });
243
244            /* Y solo cuando la seccion esta a la vista.
245             *
246             * Vive al final de la portada: sin esto estaria pasando fotos todo
247             * el rato para nadie, gastando descargas y bateria en un movil. */
248            if (window.IntersectionObserver) {
249                new IntersectionObserver(function (entradas) {
250                    if (entradas[0].isIntersecting) arrancarSolo();
251                    else pararSolo();
252                }, { threshold: 0.25 }).observe(root);
253            } else {
254                arrancarSolo();
255            }
256
257            /* Quien pidio menos movimiento no lo tiene. Es una preferencia del
258               sistema y la ponen personas a las que la animacion les sienta
259               mal, no es un gusto. */
260            var quieto = window.matchMedia('(prefers-reduced-motion: reduce)');
261            if (quieto.matches) pararSolo();
262            quieto.addEventListener('change', function (e) {
263                if (e.matches) pararSolo(); else arrancarSolo();
264            });
265
266            // ---- Verlas todas, en cuadricula y a pantalla completa ----
267
268            var ampliar = ref('ampliar');
269            if (ampliar) {
270                /* Se enseña AHORA, con las fotos ya en la mano. Puesto visible en
271                   la plantilla, abriria una rejilla vacia mientras carga. */
272                ampliar.hidden = false;
273                ampliar.addEventListener('click', function () {
274                    abrirRejilla(root, fotos, ampliar, pararSolo, arrancarSolo);
275                });
276            }
277        }
278    });
279
280    /* ===================================================================
281       LA REJILLA A PANTALLA COMPLETA
282
283       Todas las fotografias de golpe, en cuadros, sin tener que pasarlas de
284       una en una.
285
286       ---- QUE TAPA Y QUE NO ----
287
288       Cubre la pantalla MENOS las dos cintas de arriba: la roja del menu y la
289       blanca de los filtros. Las dos son `position: sticky` y se quedan
290       visibles, asi que taparlas con un panel las dejaria debajo, visibles y
291       sin poder pulsarlas -- que es peor que taparlas del todo.
292
293       El techo se MIDE, no se calcula: la cinta de filtros no tiene altura
294       fija --su contenido se reparte en dos lineas en pantalla estrecha-- y
295       `--alto-menu` solo cubre la primera. Se mira donde acaba de verdad y se
296       vuelve a mirar al girar la pantalla.
297
298       ---- POR QUE PAGINADA Y NO CON SCROLL ----
299
300       Porque se pidio ver, no recorrer. La rejilla calcula cuantos cuadros
301       caben en el hueco que hay --columnas por filas-- y enseña exactamente
302       esos. Lo que no cabe pasa a la pagina siguiente.
303
304       El precio es que el numero de paginas cambia al girar el movil o al
305       cambiar el tamaño de la ventana. Se acepta: la alternativa es una
306       rejilla con scroll, y entonces no hace falta rejilla.
307
308       ---- POR QUE `contain` Y NO `cover` ----
309
310       La tira de la portada recorta las fotos a una proporcion fija, y ahi
311       esta bien: son miniaturas de paso. Aqui NO se recorta.
312
313       Son fotografias de un archivo historico, o sea documentos. Recortar un
314       documento para que quede cuadrado le quita informacion --lo que se ve en
315       los bordes de una foto de una inundacion es parte de lo que documenta--
316       y ademas se pidio explicitamente que no se deformen. Con `contain` se ve
317       la fotografia entera dentro de su cuadro, con su proporcion, y lo que
318       sobra del cuadro queda en gris.
319       =================================================================== */
320    /* Cuanto mide cada fotografia, de alto por ancho, recordado entre aperturas.
321     *
322     * Vive FUERA de abrirRejilla a proposito: medir diecisiete imagenes en cada
323     * apertura seria repetir un trabajo cuyo resultado no cambia nunca. Al abrir
324     * la segunda vez el collage sale ya bien repartido, sin el parpadeo del
325     * primer reparto con proporciones supuestas. */
326    var proporciones = {};
327
328    function abrirRejilla(root, fotos, quienAbrio, pararSolo, arrancarSolo) {
329        if (document.querySelector('.archivo-rejilla')) return;
330
331        pararSolo();
332
333        var pagina = 0;
334
335        var capa = document.createElement('div');
336        capa.className = 'archivo-rejilla';
337        capa.setAttribute('role', 'dialog');
338        capa.setAttribute('aria-modal', 'true');
339        capa.setAttribute('aria-label', 'Fotografías del Archivo Histórico');
340
341        var barra = document.createElement('div');
342        barra.className = 'archivo-rejilla-barra';
343
344        var titulo = document.createElement('p');
345        titulo.className = 'archivo-rejilla-titulo';
346        titulo.textContent = fotos.length + (fotos.length === 1
347            ? ' fotografía del Archivo Histórico'
348            : ' fotografías del Archivo Histórico');
349
350        var cerrar = document.createElement('button');
351        cerrar.type = 'button';
352        cerrar.className = 'archivo-rejilla-cerrar';
353        cerrar.title = 'Cerrar';
354        cerrar.setAttribute('aria-label', 'Cerrar');
355        cerrar.innerHTML = '<i class="fas fa-xmark" aria-hidden="true"></i>';
356
357        barra.appendChild(titulo);
358        barra.appendChild(cerrar);
359
360        var rejilla = document.createElement('div');
361        rejilla.className = 'archivo-cuadros';
362
363        var pie = document.createElement('div');
364        pie.className = 'archivo-rejilla-pie';
365
366        var atras = document.createElement('button');
367        atras.type = 'button';
368        atras.className = 'archivo-rejilla-paso';
369        atras.setAttribute('aria-label', 'Página anterior');
370        atras.innerHTML = '<i class="fas fa-chevron-left" aria-hidden="true"></i>';
371
372        var cuenta = document.createElement('span');
373        cuenta.className = 'archivo-rejilla-cuenta';
374
375        /* aria-live para que el cambio de pagina se anuncie: sin esto, quien usa
376           lector de pantalla pulsa «siguiente» y no se entera de nada, porque lo
377           que cambia son las imagenes y no el foco. */
378        cuenta.setAttribute('aria-live', 'polite');
379
380        var alante = document.createElement('button');
381        alante.type = 'button';
382        alante.className = 'archivo-rejilla-paso';
383        alante.setAttribute('aria-label', 'Página siguiente');
384        alante.innerHTML = '<i class="fas fa-chevron-right" aria-hidden="true"></i>';
385
386        pie.appendChild(atras);
387        pie.appendChild(cuenta);
388        pie.appendChild(alante);
389
390        /* Las salidas al archivo y el credito, TAMBIEN aqui.
391         *
392         * Se CLONAN de la seccion en vez de escribirse otra vez: son las mismas dos
393         * direcciones y la misma linea de propiedad, y copiadas a mano se quedarian
394         * atras el dia que alguien cambie el enlace o el texto de la portada. Ya
395         * paso con la promesa del aviso de notificaciones, que decia una cosa
396         * mientras el interruptor estaba en otro sitio.
397         *
398         * El credito no es adorno: son fotografias con derechos y la atribucion
399         * tiene que viajar con ellas. En la vista a pantalla completa, donde se ven
400         * todas juntas, con mas motivo. */
401        var salidas = root.querySelector('[data-ref="acciones"]');
402        var credito = root.querySelector('.archivo-pie');
403
404        if (salidas || credito) {
405            var extra = document.createElement('div');
406            extra.className = 'archivo-rejilla-extra';
407
408            if (salidas) {
409                var copia = salidas.cloneNode(true);
410                copia.removeAttribute('data-ref');
411
412                /* Lo clonado hereda la marca de analitica, y esta bien: interesa
413                   saber si se pulsa desde aqui o desde la portada. Se distingue por
414                   el destino, que se reetiqueta. */
415                copia.querySelectorAll('[data-ga-destino]').forEach(function (a) {
416                    a.dataset.gaDestino = a.dataset.gaDestino + '-rejilla';
417                });
418
419                extra.appendChild(copia);
420            }
421
422            if (credito) extra.appendChild(credito.cloneNode(true));
423
424            capa.appendChild(barra);
425            capa.appendChild(rejilla);
426            capa.appendChild(pie);
427            capa.appendChild(extra);
428        } else {
429            capa.appendChild(barra);
430            capa.appendChild(rejilla);
431            capa.appendChild(pie);
432        }
433
434        document.body.appendChild(capa);
435
436        /* El cuerpo no se desplaza mientras esto esta abierto: si no, al llegar
437           al borde de la rejilla el dedo arrastra la pagina de detras. */
438        var scrollAntes = document.body.style.overflow;
439        document.body.style.overflow = 'hidden';
440
441        /* Donde acaban las cintas de arriba, medido.
442         *
443         * La de filtros si esta; si no la hubiera --o estuviera escondida-- vale
444         * la del menu. Y si tampoco, cero: mejor a pantalla completa que con el
445         * panel descolocado. */
446        function techo() {
447            var candidatas = ['.sitio-filtros-barra', '.sitio-nav-barra'];
448
449            for (var i = 0; i < candidatas.length; i++) {
450                var n = document.querySelector(candidatas[i]);
451                if (!n) continue;
452
453                var r = n.getBoundingClientRect();
454                if (r.height > 0 && r.bottom > 0) return Math.round(r.bottom);
455            }
456
457            return 0;
458        }
459
460        /* ---- EL MOSAICO ENTRELAZADO ----
461         *
462         * Antes eran filas justificadas, y la queja era justa: una foto VERTICAL
463         * ocupaba el mismo alto que una apaisada, asi que salia estrechita y
464         * pequeña al lado de las demas. Una vertical tiene que valer lo que dos
465         * apaisadas puestas una encima de otra, y entonces las piezas se cruzan.
466         *
467         * Asi que esto es una rejilla de celdas con PROPORCION DE FOTO APAISADA
468         * (3:2), y cada fotografia ocupa las celdas que le corresponden:
469         *
470         *     apaisada normal   1 columna x 1 fila     -> 3:2
471         *     vertical          1 columna x 2 filas    -> ~3:4
472         *     panoramica        2 columnas x 1 fila    -> ~3:1
473         *
474         * La gracia de elegir 3:2 como celda es que las tres formas caen muy cerca
475         * de proporciones de foto reales, asi que al rellenar la celda no se
476         * recorta casi nada. Con celdas cuadradas, una apaisada perdia un tercio.
477         *
478         * ---- Y POR QUE SE COLOCA A MANO ----
479         *
480         * CSS sabe entrelazar esto solo, con `grid-auto-flow: dense`. No se usa: si
481         * el navegador decide las posiciones, desde aqui no se sabe cuantas fotos
482         * han entrado en la pagina, y la paginacion --que es el requisito: ver sin
483         * scrollear-- dejaria de cuadrar. Colocando aqui, lo que se pinta es
484         * exactamente lo que se ha contado.
485         *
486         * Se recorre en ORDEN y se para en la primera que no quepa. Podria saltarse
487         * y meter una mas pequeña detras, pero entonces las fotos saldrian
488         * desordenadas entre paginas y una que ya se vio podria reaparecer. */
489        var VERTICAL = 0.9;      // por debajo de esto, es una foto de pie
490        var PANORAMICA = 2.0;    // por encima, una tira muy apaisada
491
492        function proporcion(f) {
493            return proporciones[f.id] || 1.5;
494        }
495
496        /* Cuantas celdas ocupa una foto, segun su forma. */
497        function tramo(f, cols, filas) {
498            var p = proporcion(f);
499
500            var w = p >= PANORAMICA ? 2 : 1;
501            var h = p <= VERTICAL ? 2 : 1;
502
503            /* En una ventana de una sola fila --o de una sola columna-- el tramo no
504               cabe y la foto se pone en una celda. Se recorta mas de lo ideal, y es
505               mejor que no enseñarla. */
506            if (w > cols) w = 1;
507            if (h > filas) h = 1;
508
509            return { w: w, h: h };
510        }
511
512        /* El primer hueco libre donde cabe un tramo, recorriendo por filas.
513         *
514         * Es lo mismo que hace el relleno denso de CSS, y es lo que entrelaza las
515         * piezas: el hueco de una celda que deja una vertical lo aprovecha la
516         * apaisada siguiente. */
517        function hueco(ocupado, cols, filas, w, h) {
518            function cabe(r, c) {
519                if (c + w > cols || r + h > filas) return false;
520
521                for (var y = r; y < r + h; y++)
522                    for (var x = c; x < c + w; x++)
523                        if (ocupado[y][x]) return false;
524
525                return true;
526            }
527
528            /* Se busca POR COLUMNAS, no por filas. Y esto es lo que hace que el
529             * entrelazado funcione de verdad.
530             *
531             * ---- El fallo que se vio midiendo ----
532             *
533             * Con el recorrido por filas, en escritorio --donde la rejilla acaba
534             * siendo de DOS filas-- no entraba ni una sola foto vertical. Se
535             * llenaba la fila 0 de apaisadas, y entonces ninguna columna tenia ya
536             * sus dos celdas libres, que es lo que pide una vertical. Las
537             * verticales se iban aplazando pagina tras pagina.
538             *
539             * O sea: el entrelazado, que es la razon de ser de esta version, no
540             * ocurria en la pantalla mas comun.
541             *
542             * Por columnas se llena (0,0), (1,0), (0,1), (1,1)... asi que cuando
543             * toca una vertical hay columnas enteras libres y entra. Y de paso se
544             * lee como debe: pares apilados, y las altas ocupando su columna. */
545            for (var x2 = 0; x2 < cols; x2++)
546                for (var y2 = 0; y2 < filas; y2++)
547                    if (cabe(y2, x2)) return { r: y2, c: x2 };
548
549            return null;
550        }
551
552        /* CUANTO se mira hacia delante cuando la siguiente no cabe.
553         *
554         * ---- El hueco que esto arregla ----
555         *
556         * Antes se paraba en la primera foto que no cabia, para no desordenar. El
557         * efecto se vio en movil: quedaba una celda libre al final de la pagina, la
558         * siguiente foto era VERTICAL --y una vertical pide dos filas-- asi que no
559         * entraba y la pagina se cerraba con el hueco a la vista.
560         *
561         * Ahora, cuando la del frente no cabe, se mira si alguna de las ocho
562         * siguientes si. La que quepa se adelanta y la que no cabia se queda al
563         * frente de la cola: en la pagina siguiente entra primera, con la rejilla
564         * vacia, donde cabe seguro.
565         *
566         * ---- Por que OCHO y no todas ----
567         *
568         * Porque adelantar tiene un coste: el orden. Sin limite, para tapar un
569         * hueco se podria traer la foto numero cuarenta a la primera pagina y dejar
570         * la sexta para la tercera, y entonces el mosaico deja de seguir al
571         * carrusel. Ocho tapa los huecos reales --que son de una o dos celdas-- sin
572         * mover nada de sitio mas alla de una fila.
573         *
574         * Con 0 volveria el hueco. Con Infinity se llenaria siempre y el orden
575         * seria el que saliera. */
576        var MIRAR = 8;
577
578        /* Lo que cabe en UNA pagina, consumiendo la cola.
579         *
580         * Recibe y MUTA la cola a proposito: lo que se lleva esta pagina deja de
581         * estar pendiente, y lo que no cabe se queda para la siguiente sin tener
582         * que llevar indices por fuera. Es lo que permite que una pagina ya no sea
583         * un tramo contiguo de la lista.
584         */
585        function llenarPagina(cola, cols, filas) {
586            var ocupado = [];
587            for (var r = 0; r < filas; r++) {
588                var fila = [];
589                for (var c = 0; c < cols; c++) fila.push(false);
590                ocupado.push(fila);
591            }
592
593            var puestos = [];
594
595            while (cola.length > 0) {
596                var elegida = -1;
597                var donde = null;
598                var t = null;
599
600                var hasta = Math.min(MIRAR, cola.length);
601
602                for (var k = 0; k < hasta; k++) {
603                    var tk = tramo(cola[k], cols, filas);
604                    var dk = hueco(ocupado, cols, filas, tk.w, tk.h);
605
606                    if (dk) { elegida = k; donde = dk; t = tk; break; }
607                }
608
609                /* Ninguna de las que se miran cabe: la pagina esta llena. */
610                if (elegida < 0) break;
611
612                for (var y = donde.r; y < donde.r + t.h; y++)
613                    for (var x = donde.c; x < donde.c + t.w; x++) ocupado[y][x] = true;
614
615                puestos.push({ foto: cola[elegida], r: donde.r, c: donde.c,
616                               w: t.w, h: t.h });
617
618                cola.splice(elegida, 1);
619            }
620
621            return puestos;
622        }
623        /* LAS PAGINAS ENTERAS, no los cortes.
624         *
625         * Se guardaba el indice donde empezaba cada pagina, y con eso bastaba
626         * mientras una pagina fuera un tramo contiguo de la lista. Desde que se
627         * mira hacia delante para tapar huecos, ya no lo es: la pagina 1 puede
628         * llevarse la foto 7 y dejar la 6 para la 2, asi que un indice de inicio no
629         * la describe.
630         *
631         * Se guardan colocadas. Y hay una ventaja que no es menor: `pintarPagina`
632         * ya no recalcula nada, asi que lo que se pinta es exactamente lo que se
633         * conto. Antes eran dos llamadas al reparto --una para contar y otra para
634         * pintar-- y dos caminos que tienen que dar lo mismo acaban no dandolo.
635         *
636         * Se rehacen al cambiar el tamaño: con otro ancho caben otras columnas. */
637        var paginas = [[]];
638        var CELDA_PROP = 1.5;
639
640        /* La rejilla que MEJOR LLENA el hueco.
641         *
642         * ---- El espacio en blanco que esto arregla ----
643         *
644         * El numero de columnas salia de un ancho de celda objetivo, y el alto era
645         * lo que tocara. Medido en 1280x620: celdas de 314x209, dos filas, 426 px
646         * usados de 620. Ciento noventa y cuatro pixeles en blanco debajo del
647         * mosaico, en una vista cuyo objeto es precisamente ver muchas fotos.
648         *
649         * Asi que ya no se elige el ancho de celda: se PRUEBAN las reparticiones
650         * razonables y gana la que menos alto desperdicia. Si para eso hay que
651         * bajar de cuatro columnas a tres --con celdas mas grandes-- se baja, que
652         * es justo lo que se pidio.
653         *
654         * En 1280x620 la elegida pasa a ser 3 columnas de 421x281 en dos filas:
655         * 570 de 620, y las fotos un 34% mas anchas.
656         *
657         * ---- Por que hay una banda y no se busca el minimo a secas ----
658         *
659         * Porque el minimo absoluto lleva a celdas diminutas: con diez columnas de
660         * 120x80 el alto se llena casi al milimetro y las fotografias se ven como
661         * sellos. El objetivo de ancho sigue existiendo, pero como CENTRO de una
662         * banda --de 0,72 a 1,6 veces-- dentro de la que se busca. Fuera de la
663         * banda no se mira: son reparticiones que llenan mejor y enseñan peor.
664         *
665         * ---- Y el estirado final ----
666         *
667         * Con las columnas ya elegidas, la fila se estira para consumir el resto
668         * del alto. Eso aparta la celda de su 3:2 y recorta un poco mas, asi que se
669         * frena: la celda no baja de 1,15 ni pasa de 1,95 de proporcion. Con esos
670         * topes el recorte se queda en lo que ya se medía y el hueco desaparece o
671         * se queda en unos pocos pixeles. */
672        function mejorRejilla(ancho, alto, hueco2) {
673            var objetivo = ancho < 560 ? 158 : (ancho < 1100 ? 230 : 290);
674
675            /* La banda de anchos de celda que se consideran. */
676            var minAncho = objetivo * 0.72;
677            var maxAncho = objetivo * 1.6;
678
679            var mejor = null;
680
681            for (var cols = 2; cols <= 14; cols++) {
682                var celdaAncho = (ancho - hueco2 * (cols - 1)) / cols;
683                if (celdaAncho < minAncho || celdaAncho > maxAncho) continue;
684
685                var celdaAlto = celdaAncho / CELDA_PROP;
686
687                /* Si la celda no cabe de alto, esta reparticion no vale: forzarla
688                   recortaria el mosaico por abajo. */
689                if (celdaAlto > alto) continue;
690
691                var filas = Math.floor((alto + hueco2) / (celdaAlto + hueco2));
692                if (filas < 1) continue;
693
694                var sobra = alto - (filas * celdaAlto + hueco2 * (filas - 1));
695
696                /* Gana la que menos alto deja sin usar. A igualdad, la de celdas
697                   mas grandes: entre dos que llenan lo mismo, se ven mejor las
698                   fotos grandes. */
699                if (!mejor || sobra < mejor.sobra - 0.5 ||
700                    (Math.abs(sobra - mejor.sobra) <= 0.5 && celdaAncho > mejor.celdaAncho)) {
701                    mejor = { cols: cols, filas: filas, sobra: sobra,
702                              celdaAncho: celdaAncho, celdaAlto: celdaAlto };
703                }
704            }
705
706            /* Ninguna cabe: la ventana es mas baja que la celda mas pequeña de la
707               banda. Se cae a una sola fila con la celda recortada al alto, que es
708               lo que ya hacia antes. */
709            if (!mejor) {
710                var altoCelda = alto;
711                var anchoCelda = altoCelda * CELDA_PROP;
712                var cuantas = Math.max(2, Math.floor((ancho + hueco2) / (anchoCelda + hueco2)));
713
714                mejor = { cols: cuantas, filas: 1, sobra: 0,
715                          celdaAncho: (ancho - hueco2 * (cuantas - 1)) / cuantas,
716                          celdaAlto: altoCelda };
717            }
718
719            /* Y se estira la fila para consumir lo que sobre, con freno. */
720            if (mejor.sobra > 1) {
721                var estirada = (alto - hueco2 * (mejor.filas - 1)) / mejor.filas;
722                var propEstirada = mejor.celdaAncho / estirada;
723
724                if (propEstirada >= 1.15) mejor.celdaAlto = estirada;
725                else mejor.celdaAlto = mejor.celdaAncho / 1.15;
726            }
727
728            return mejor;
729        }
730
731        function repartir() {
732            var ancho = rejilla.clientWidth;
733            var alto = rejilla.clientHeight;
734            if (ancho <= 0 || alto <= 0) return;
735
736            var hueco2 = 8;
737            var g = mejorRejilla(ancho, alto, hueco2);
738
739            /* El ANCHO de celda va a una variable propia, y hace falta.
740             *
741             * Las columnas eran `1fr`, o sea que se estiraban para llenar el ancho.
742             * Con eso la rejilla siempre ocupa todo y centrar es imposible: no hay
743             * nada que centrar. Con el ancho fijo, una pagina de tres columnas mide
744             * tres columnas y `justify-content: center` la coloca en medio. */
745            rejilla.style.setProperty('--cols', g.cols);
746            rejilla.style.setProperty('--celda', g.celdaAlto.toFixed(2) + 'px');
747            rejilla.style.setProperty('--celda-ancho', g.celdaAncho.toFixed(2) + 'px');
748            rejilla.style.setProperty('--hueco', hueco2 + 'px');
749
750            var cola = fotos.slice();
751            paginas = [];
752            var vueltas = 0;
753
754            /* El tope de vueltas es un cinturon, no una regla: si un dia
755               `llenarPagina` devolviera vacio con cola pendiente, esto se comeria
756               la pestaña. */
757            while (cola.length > 0 && vueltas++ < 500) {
758                var ps = llenarPagina(cola, g.cols, g.filas);
759                if (ps.length === 0) break;
760                paginas.push(ps);
761            }
762
763            if (paginas.length === 0) paginas = [[]];
764        }
765
766        function pintarPagina() {
767            var total = Math.max(1, paginas.length);
768            if (pagina >= total) pagina = total - 1;
769            if (pagina < 0) pagina = 0;
770
771            var puestos = paginas[pagina] || [];
772
773            /* La rejilla se encoge a lo que esta OCUPADO en esta pagina.
774             *
775             * La ultima pagina casi nunca se llena --con 34 fotos y 6 por pagina
776             * sobran cuatro-- y con las columnas fijas de la rejilla completa, esas
777             * fotos quedaban arrimadas arriba a la izquierda, con el hueco a la
778             * derecha y debajo. Se pidio centrarlas, y para poder centrarlas la
779             * rejilla tiene que medir lo que ocupan: `justify-content: center` no
780             * hace nada si las pistas ya llenan el ancho.
781             *
782             * El reparto sigue haciendose con la rejilla ENTERA --es lo que decide
783             * cuantas caben-- y esto solo afecta a como se pinta. Y funciona porque
784             * las fotos se colocan por columnas: lo ocupado es siempre un bloque
785             * que empieza en la esquina, sin columnas vacias por medio. */
786            var cols = 0;
787            var filas = 0;
788
789            puestos.forEach(function (p) {
790                if (p.c + p.w > cols) cols = p.c + p.w;
791                if (p.r + p.h > filas) filas = p.r + p.h;
792            });
793
794            rejilla.style.setProperty('--cols', Math.max(1, cols));
795            rejilla.style.setProperty('--filas', Math.max(1, filas));
796
797            /* Cada foto lleva su sitio escrito: columna, fila y cuanto ocupa. Es lo
798               que hace que lo pintado sea exactamente lo contado. */
799            rejilla.innerHTML = puestos.map(function (p) {
800                return cuadro(p.foto,
801                              'grid-column:' + (p.c + 1) + ' / span ' + p.w + ';' +
802                              'grid-row:' + (p.r + 1) + ' / span ' + p.h + ';');
803            }).join('');
804
805            cuenta.textContent = total === 1
806                ? puestos.length + ' de ' + fotos.length
807                : 'Página ' + (pagina + 1) + ' de ' + total;
808
809            /* Los pasos se apagan en los extremos. Aqui SI hay extremos --es una
810               paginacion, no un giro infinito-- y fingir que no los hay seria el
811               error contrario al de la tira. */
812            atras.disabled = pagina === 0;
813            alante.disabled = pagina >= total - 1;
814
815            pie.hidden = total === 1;
816        }
817
818        function recolocarTodo() {
819            capa.style.setProperty('--techo', techo() + 'px');
820            repartir();
821            pintarPagina();
822        }
823
824        /* Las proporciones que falten, medidas en cuanto se pueda.
825         *
826         * No se espera a tenerlas: se pinta con la proporcion supuesta y se vuelve
827         * a repartir cuando llegan. Esperar significaria una pantalla en blanco
828         * mientras cargan diecisiete miniaturas, y casi todas estan ya en la cache
829         * del navegador porque acaban de verse en la tira.
830         *
831         * Una sola pasada por foto y solo las que falten: al reabrir la rejilla
832         * esto no hace nada. */
833        function medir() {
834            var faltan = fotos.filter(function (f) { return !proporciones[f.id]; });
835            if (faltan.length === 0) return;
836
837            var pendientes = faltan.length;
838
839            faltan.forEach(function (f) {
840                var img = new Image();
841
842                img.onload = function () {
843                    if (img.naturalWidth > 0 && img.naturalHeight > 0)
844                        proporciones[f.id] = img.naturalWidth / img.naturalHeight;
845
846                    if (--pendientes === 0 && document.body.contains(capa)) recolocarTodo();
847                };
848
849                /* Si una no carga, se queda con la proporcion supuesta y no
850                   bloquea a las demas. */
851                img.onerror = function () {
852                    if (--pendientes === 0 && document.body.contains(capa)) recolocarTodo();
853                };
854
855                img.src = FELN.apiBase + f.imagen;
856            });
857        }
858
859        function ir(cuanto) {
860            var total = Math.max(1, paginas.length);
861            var siguiente = pagina + cuanto;
862            if (siguiente < 0 || siguiente >= total) return;
863            pagina = siguiente;
864            pintarPagina();
865        }
866
867        atras.addEventListener('click', function () { ir(-1); });
868        alante.addEventListener('click', function () { ir(1); });
869
870        function alTeclado(e) {
871            if (e.key === 'Escape') { cerrarla(); return; }
872            if (e.key === 'ArrowLeft') { ir(-1); return; }
873            if (e.key === 'ArrowRight') { ir(1); }
874        }
875
876        /* Las cintas son sticky: al desplazar la pagina de detras cambiarian de
877           sitio y el techo dejaria de cuadrar. El cuerpo esta bloqueado, pero en
878           iOS el rebote sigue moviendo cosas, asi que se vuelve a medir. */
879        function alMoverse() { capa.style.setProperty('--techo', techo() + 'px'); }
880
881        window.addEventListener('resize', recolocarTodo);
882        window.addEventListener('scroll', alMoverse, { passive: true });
883        document.addEventListener('keydown', alTeclado);
884        cerrar.addEventListener('click', function () { cerrarla(); });
885
886        function cerrarla() {
887            window.removeEventListener('resize', recolocarTodo);
888            window.removeEventListener('scroll', alMoverse);
889            document.removeEventListener('keydown', alTeclado);
890
891            capa.remove();
892            document.body.style.overflow = scrollAntes;
893
894            /* El foco vuelve a donde estaba. Sin esto, quien navega con teclado
895               cierra la rejilla y aparece al principio de la pagina. */
896            if (quienAbrio && quienAbrio.focus) quienAbrio.focus();
897
898            arrancarSolo();
899        }
900
901        recolocarTodo();
902        medir();
903        cerrar.focus();
904
905        /* Y otra medida en el cuadro siguiente.
906         *
907         * La primera cae antes de que el navegador haya aplicad
907o el estilo de la
908         * capa, asi que `clientHeight` de la rejilla puede venir en cero y
909         * saldria una sola fila. Repetir cuando ya hay layout lo arregla, y no se
910         * ve: son dieciseis milisegundos. */
911        requestAnimationFrame(recolocarTodo);
912    }
913
914    /* Un cuadro de la rejilla: la fotografia y nada mas.
915     *
916     * Sin titulo ni pie, que es lo que se pidio: aqui se viene a VER, y veinte
917     * pies de foto a la vez convierten la rejilla en una lista.
918     *
919     * El enlace a la ficha se queda: es donde estan el titulo, la fecha y el
920     * credito, y quitarlo dejaria las fotografias sin procedencia a un clic. El
921     * titulo viaja en `alt` y en `title`, asi que se lee al pasar el raton y con
922     * lector de pantalla. */
923    function cuadro(f, sitio) {
924        return '' +
925            '<a class="archivo-cuadro" style="' + sitio + '" ' +
926               'href="' + api.escapeHtml(f.ficha) + '" ' +
927               'target="_blank" rel="noopener" ' +
928               'data-ga="archivo" data-ga-destino="foto-rejilla" ' +
929               'data-ga-titulo="' + api.escapeHtml(f.titulo) + '" ' +
930               'title="' + api.escapeHtml(f.titulo) + '">' +
931              '<img src="' + api.escapeHtml(FELN.apiBase + f.imagen) + '" ' +
932                   'alt="' + api.escapeHtml(f.titulo) + '" ' +
933                   'loading="lazy" decoding="async" />' +
934            '</a>';
935    }
936
937    /**
938     * Una copia del conjunto, invisible para lectores de pantalla y teclado.
939     *
940     * Son las mismas fotografias otra vez: quien navega con lector no debe
941     * oirlas tres veces, y quien tabula no debe recorrer cincuenta y un enlaces
942     * cuando solo hay diecisiete cosas distintas.
943     */
944    function copia(html) {
945        return html
946            .replace(/<figure /g, '<figure aria-hidden="true" ')
947            .replace(/<a /g, '<a tabindex="-1" ');
948    }
949
950    function pintar(f) {
951        var pie = f.descripcion || '';
952        var credito = f.autor ? 'Foto: ' + api.escapeHtml(f.autor) : '';
953
954        return '' +
955            '<figure class="archivo-foto" role="listitem">' +
956              '<a href="' + api.escapeHtml(f.ficha) + '" target="_blank" rel="noopener" ' +
957                 'data-ga="archivo" data-ga-destino="foto" ' +
958                 'data-ga-titulo="' + api.escapeHtml(f.titulo) + '" ' +
959                 'title="Ver la ficha en el Archivo Histórico">' +
960
961                /* loading=lazy porque esto vive al final de la portada: sin el,
962                   una veintena de miniaturas se descargan aunque nadie baje
963                   hasta aqui. decoding=async para que no bloqueen el pintado. */
964                /* La imagen la sirve el API, que en produccion cuelga del
965                   mismo dominio pero en local va en otro puerto. Se compone con
966                   apiBase por lo mismo que el resto de llamadas. */
967                '<img src="' + api.escapeHtml(FELN.apiBase + f.imagen) + '" ' +
968                     'alt="' + api.escapeHtml(f.titulo) + '" ' +
969                     'loading="lazy" decoding="async" />' +
970              '</a>' +
971              '<figcaption>' +
972                '<strong>' + api.escapeHtml(f.titulo) + '</strong>' +
973                (pie ? '<span class="archivo-pie-foto">' + api.escapeHtml(pie) + '</span>' : '') +
974                (credito ? '<span class="archivo-credito">' + credito + '</span>' : '') +
975              '</figcaption>' +
976            '</figure>';
977    }
978})();

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.