Slideshow

Un slideshow muestra una imagen de un conjunto y permite cambiar a otra sin recargar la página. Las tres variantes de este documento son la misma estructura con tres formas distintas de disparar el cambio: una flecha, un reloj, y la combinación de ambas.

No hace falta nada nuevo. Las piezas son las de la clase anterior:

  1. Una colección con todas las imágenes, obtenida con getElementsByClassName.
  2. Un índice: una variable con el número de la que está a la vista.
  3. Una función que, dado el índice, deja visible solo esa.

Lo único que cambia de una variante a otra es quién modifica el índice.

1. El slideshow manual

Las flechas suman o restan uno al índice. El problema está en los extremos: pasada la última hay que volver a la primera, y antes de la primera hay que ir a la última. Con dos if queda resuelto.

el slideshow manual, en su versión mínima

<div class="slideshow" id="slideshow">
  <img class="slide" src="IMG/naturaleza-arroyo.jpg" alt="Arroyo de montaña en un bosque de Trentino">
  <img class="slide" src="IMG/naturaleza-abetos.jpg" alt="Arroyo entre abetos bajo un cielo nublado">
  <img class="slide" src="IMG/naturaleza-valle.jpg" alt="Valle con niebla al amanecer">
  <img class="slide" src="IMG/naturaleza-lago-bn.jpg" alt="Lago entre montañas en blanco y negro">
</div>

<div class="row">
  <button class="arrow" id="anterior" aria-label="Imagen anterior">&#10094;</button>
  <button class="arrow" id="siguiente" aria-label="Imagen siguiente">&#10095;</button>
  <div class="caja" id="contador">1 de 4</div>
</div>

Las dos flechas llaman a mover con 1 y con -1. Ese es todo el parecido y toda la diferencia: una función, un argumento.

fotos.length - 1 es el número de la última, no fotos.length. Con cuatro imágenes los números van del 0 al 3, y length vale 4. Pedir la 4 devuelve undefined, y el slideshow se queda en blanco sin dar ningún error.

Las flechas encima de la imagen, y el pie

Puestas dentro del contenedor y colocadas con position: absolute, las flechas quedan sobre la imagen. Exige que el contenedor tenga position: relative, para que el absolute se mida respecto a él y no respecto a la ventana.

El pie de foto no se escribe aparte: ya está en el alt de cada imagen, y de ahí se lee. Un solo texto por imagen, en un solo sitio.

flechas superpuestas, pie de foto y dots (puntos indicadores)

<div class="slideshow" id="slideshow">
  <div class="slideshow__track">
    <img class="slide" src="IMG/naturaleza-arroyo.jpg" alt="Arroyo de montaña en un bosque de Trentino">
    <img class="slide" src="IMG/naturaleza-abetos.jpg" alt="Arroyo entre abetos bajo un cielo nublado">
    <img class="slide" src="IMG/naturaleza-valle.jpg" alt="Valle con niebla al amanecer">
    <img class="slide" src="IMG/naturaleza-lago-bn.jpg" alt="Lago entre montañas en blanco y negro">
  </div>

  <button class="slideshow__arrow slideshow__arrow--left" data-paso="-1"
          aria-label="Imagen anterior">&#10094;</button>
  <button class="slideshow__arrow slideshow__arrow--right" data-paso="1"
          aria-label="Imagen siguiente">&#10095;</button>

  <p class="slideshow__caption" id="caption"></p>
</div>

<div class="dots" id="puntos">
  <button class="dot" data-ir="0" aria-label="Ir a la imagen 1"></button>
  <button class="dot" data-ir="1" aria-label="Ir a la imagen 2"></button>
  <button class="dot" data-ir="2" aria-label="Ir a la imagen 3"></button>
  <button class="dot" data-ir="3" aria-label="Ir a la imagen 4"></button>
</div>

Tres cosas nuevas, pequeñas.

classList.toggle("dot--active", i === n) con un segundo argumento no alterna: pone la clase si vale true y la quita si vale false. Se ahorra el if.

Number(...) es necesario. Lo que se lee de un atributo es siempre texto, y sumar texto concatena: 0 + "1" da "01", no 1. Number lo convierte a número.

La flecha es negra, pero con opacity: .55 deja ver la foto por detrás. Al pasar el ratón sube a .85 y se vuelve más sólida.

2. El slideshow automático

Aquí el índice no lo cambia un clic, sino el reloj del navegador. setInterval recibe una función y un número de milisegundos, y la ejecuta una y otra vez con esa separación.

setInterval(function () { ... }, 4000);   // cada cuatro segundos

El valor que devuelve es el identificador del temporizador, y hay que guardarlo: es lo único que permite detenerlo después con clearInterval.

Un slideshow que se mueve solo y no se puede parar incomoda a quien lee despacio y no llega a terminar. De ahí el botón de pausa del ejemplo.

el slideshow automático, con pausa

<div class="slideshow" id="slideshow">
  <img class="slide" src="IMG/naturaleza-arroyo.jpg" alt="Arroyo de montaña en un bosque de Trentino">
  <img class="slide" src="IMG/naturaleza-abetos.jpg" alt="Arroyo entre abetos bajo un cielo nublado">
  <img class="slide" src="IMG/naturaleza-valle.jpg" alt="Valle con niebla al amanecer">
  <img class="slide" src="IMG/naturaleza-lago-bn.jpg" alt="Lago entre montañas en blanco y negro">
</div>

<div class="row">
  <button id="pausa" aria-pressed="false">Pausa</button>
  <div class="caja" id="estado">En marcha</div>
</div>

La comprobación if (reloj !== null) return no es un adorno. Sin ella, dos pulsaciones seguidas en Continuar dejarían dos temporizadores corriendo a la vez, el slideshow iría al doble de velocidad y clearInterval solo pararía uno de los dos. El identificador del otro se habría perdido y no habría forma de detenerlo.

aria-pressed declara si un botón de dos estados está pulsado, igual que aria-expanded declaraba si un desplegable está abierto.

Pararlo al pasar el ratón

Lo habitual es que el slideshow se detenga mientras el ratón está encima, para que se pueda leer el pie, y siga al salir. Son dos eventos y las dos funciones que ya existen:

const slideshow = document.getElementById("slideshow");

slideshow.addEventListener("mouseenter", detener);
slideshow.addEventListener("mouseleave", arrancar);

Aquí se pasa detener, sin paréntesis. Con paréntesis se ejecutaría la función en ese mismo momento y se registraría su resultado, que no es nada. Sin paréntesis se entrega la función para que el navegador la invoque cuando el suceso ocurra.

Esto cubre el ratón, no el dedo. En una pantalla táctil no hay mouseenter, y por eso el botón de pausa sigue siendo necesario.

3. El slideshow de tarjetas

Lo que se pasa no tiene que ser una imagen suelta, ni tiene que verse de una en una. Un catálogo muestra varias tarjetas a la vez y la tira se desplaza lateralmente: es lo que hace cualquier tienda en su fila de más vendidos.

Eso obliga a cambiar el mecanismo. Ocultar con display: none servía mientras solo hubiera una a la vista; aquí hay que mover la tira entera. El planteamiento es otro:

  1. Una ventana con overflow: hidden, que recorta.
  2. Dentro, una tira en flex, más ancha que la ventana, con todas las tarjetas en fila y sin wrap.
  3. La tira se desplaza con transform: translateX(...), y la transition del CSS hace el movimiento.

Las tarjetas llevan flex: 0 0 13rem: ni crecen ni encogen, todas con el mismo ancho. Cuántas se ven no lo decide el código, sino el ancho disponible, y por eso tampoco hace falta ninguna media query.

El índice ya no es «qué tarjeta se ve», sino cuál es la primera de las que se ven. Y el tope no es la última tarjeta: es la última menos las que caben, porque pasado ese punto quedaría hueco a la derecha. Las flechas se apagan con disabled al llegar a cada extremo.

un catálogo de láminas, con varias tarjetas a la vista

<div class="carousel">
  <div class="carousel__head">
    <h3>Más vendidas</h3>
    <div class="carousel__arrows">
      <button class="carousel__arrow" data-paso="-1" aria-label="Láminas anteriores">&#10094;</button>
      <button class="carousel__arrow" data-paso="1" aria-label="Láminas siguientes">&#10095;</button>
    </div>
  </div>

  <div class="carousel__window" id="carousel-window">
    <div class="carousel__track" id="carousel-track">
      <article class="card">
        <p class="card__tag">Oferta</p>
        <img class="slide" src="IMG/naturaleza-valle.jpg" alt="Lámina del valle con niebla al amanecer">
        <h4 class="card__name">Valle con niebla</h4>
        <p class="card__tech">Papel de algodón, 40 &times; 30 cm</p>
        <p class="card__old">30,00 &euro;</p>
        <p class="card__price card__price--offer">24,00 &euro;</p>
        <p class="card__note">IGIC incl., envío no incl.</p>
      </article>

      <article class="card">
        <img class="slide" src="IMG/naturaleza-lago-bn.jpg" alt="Lámina del lago entre montañas en blanco y negro">
        <h4 class="card__name">Lago en blanco y negro</h4>
        <p class="card__tech">Papel barita, 50 &times; 70 cm</p>
        <p class="card__price">28,50 &euro;</p>
        <p class="card__note">IGIC incl., envío no incl.</p>
      </article>

      <article class="card">
        <p class="card__tag">Oferta</p>
        <img class="slide" src="IMG/ciudad-amanecer.jpg" alt="Lámina de los rascacielos al amanecer">
        <h4 class="card__name">Ciudad al amanecer</h4>
        <p class="card__tech">Giclée, 30 &times; 30 cm</p>
        <p class="card__old">25,00 &euro;</p>
        <p class="card__price card__price--offer">19,00 &euro;</p>
        <p class="card__note">IGIC incl., envío no incl.</p>
      </article>

      <article class="card">
        <img class="slide" src="IMG/naturaleza-arroyo.jpg" alt="Lámina del arroyo de montaña">
        <h4 class="card__name">Arroyo de montaña</h4>
        <p class="card__tech">Papel de algodón, 40 &times; 30 cm</p>
        <p class="card__price">22,00 &euro;</p>
        <p class="card__note">IGIC incl., envío no incl.</p>
      </article>

      <article class="card">
        <p class="card__tag">Oferta</p>
        <img class="slide" src="IMG/bn-caballo.jpg" alt="Lámina del caballo en blanco y negro">
        <h4 class="card__name">Caballo</h4>
        <p class="card__tech">Papel barita, 50 &times; 70 cm</p>
        <p class="card__old">26,00 &euro;</p>
        <p class="card__price card__price--offer">21,00 &euro;</p>
        <p class="card__note">IGIC incl., envío no incl.</p>
      </article>

      <article class="card">
        <img class="slide" src="IMG/bn-cupula.jpg" alt="Lámina de la cúpula del Palacio de Bellas Artes">
        <h4 class="card__name">Cúpula</h4>
        <p class="card__tech">Giclée, 30 &times; 30 cm</p>
        <p class="card__price">23,50 &euro;</p>
        <p class="card__note">IGIC incl., envío no incl.</p>
      </article>
    </div>
  </div>
</div>

Lo nuevo aquí es que JavaScript mide. Hasta ahora no había consultado ninguna dimensión: cambiaba clases y textos. getBoundingClientRect devuelve el tamaño real de un elemento en pantalla, ya resuelto el rem, el % y lo que haga falta.

Esos píxeles del translateX son los únicos de todo el curso, y no están escritos: son el resultado de medir lo que el CSS ha decidido. Si mañana la tarjeta pasa a 15rem, el JavaScript no se toca.

caben() se vuelve a calcular en cada llamada, y de ahí el listener de resize: al estrechar la ventana caben menos tarjetas, el tope baja, y una posición que antes era válida deja de serlo.

La variable se llama window_ con un guion bajo porque window ya existe: es el objeto que representa la ventana del navegador, el mismo del que cuelgan addEventListener y setInterval. Dos nombres iguales en el mismo ámbito y el segundo tapa al primero.

Resumen

  1. Las tres variantes comparten estructura: colección, índice y una función que muestra. Solo cambia quién modifica el índice.
  2. En los extremos, dos if: pasada la última se vuelve a la 0; antes de la primera, a length - 1.
  3. setInterval devuelve un identificador. Si no se guarda, el temporizador no se puede detener.
  4. Un movimiento automático necesita un botón de pausa.
  5. Un listener recibe la función sin paréntesis.
  6. Lo que se lee de un atributo data- es texto: Number para operar con él.
  7. El contenido (precios, descripciones, textos alternativos) va en el HTML.
  8. Con una sola a la vista se oculta con display: none; con varias se mueve la tira entera con transform dentro de una ventana que recorta.
  9. Cuando el JavaScript necesita una medida, la pide con getBoundingClientRect; no la lleva escrita.

Reconocimientos

Las fotos son de Pexels, con licencia libre: se pueden usar y modificar sin pagar y sin citar la fuente. Citarla no es obligatorio, pero es lo correcto, y aquí sirve además de ejemplo de cómo se hace.