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.