Cómo hacer una tabla de contenido interactiva y dinámica en JavaScript

Mientras leía algunos artículos técnicos en varias plataformas, seguía notando la sección de Tabla de contenidos en la barra lateral. Algunas eran interactivas y otras eran simplemente enlaces a esas secciones. Una tabla de contenidos suele ser de gran ayuda para los lectores, ya que les permite hojear fácilmente de qué trata el artículo.

Mientras leía algunos artículos técnicos en varias plataformas, notaba constantemente la sección de Índice en la barra lateral. Algunos eran interactivos y otros solo eran enlaces a esas secciones.

Un Índice suele resultar muy útil para los lectores. Permite examinar rápidamente qué cubrirá un artículo y encontrar la sección que te interesa. También te informa si el artículo contiene la información que estás buscando, lo cual es un gran avance en términos de accesibilidad.

Tomando inspiración de todas estas varias plataformas, intenté construir mi propia funcionalidad de Índice. Quería que mostrara dinámicamente todas las cabeceras H2 junto con sus enlaces de marcadores. También quería que las cabeceras se resaltaran a medida que se desplazaban en la vista. Estoy entusiasmado, vamos a empezar.

Nota: No pude usar Codepen, ya que utiliza iframes para previsualizar los resultados, y en este momento, Intersection Observer actúa de manera bastante caprichosa en el iframe. Aquí tienes el gist para este código.

Intersection Observer. GitHub Gist: comparte instantáneamente código, notas y fragmentos.favicon262588213843476Gistgist-og-image-54fd7dc0713e

Requisitos previos

Para aprovechar al máximo este tutorial, debes estar familiarizado/a con:

  1. HTML5/CSS3/JavaScript
  2. API de Intersection Observer

Muy bien, ahora vamos a sumergirnos en el tema.

Configurar el proyecto

Lo primero es lo primero, vamos a configurar la estructura HTML para nuestro Índice. No será nada elegante, solo una etiqueta <article> que envuelve todo el contenido, con una etiqueta <aside> como hermano, todo envuelto por la etiqueta <main>.

Esto es cómo se verá:

<main>    <article>        <h1>Encabezado principal</h1>        <h2>Primer encabezado</h2>        <p>Lorem ipsum dolor sit...</p>        <h2>Segundo encabezado</h2>        <p>Lorem ipsum dolor sit...</p>        <h2>Tercer encabezado</h2>        <p>Lorem ipsum dolor sit...</p>    </article>    <aside></aside></main>

La etiqueta <aside> está vacía ya que se llenará dependiendo del contenido en <article> a través de JavaScript.

Hemos terminado con la parte de la estructura. Ahora hagamos algo de estilización, lo que nos ayudará a diferenciar entre los enlaces inactivos y activos.

Cómo agregar el estilo

He importado una fuente de Google llamada DM Sans para este mini-proyecto. En mi CSS, estoy utilizando anidación nativa.

@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600&display=swap');html {    scroll-padding: 3.125rem;     font-family: 'DM Sans', sans-serif;  }  main {    display: grid;    gap: 2rem;    grid-template-columns: 3fr 1fr;  }  aside {    align-self: start;    position: sticky;    top: 0.625rem;    ul {      li {        a {          transform-origin: left;          transition: transform 0.1s linear;          &.active {            font-weight: 600;            transform: scale(1.1);          }        }      }    }  }  @media (max-width: 767px) {    main {      grid-template-columns: 1fr;  }    aside {      display: none;    }  }

Utilicé display: grid; para crear un diseño en el que el contenido ocupa tres cuartos del espacio del contenedor (en este caso, el área de visualización) y la tabla de contenidos ocupa el cuarto restante del espacio.

Mantengo <aside> fijo para que se mantenga visible mientras se desplaza el contenido. ¿No es genial poder experimentar la interactividad y el comportamiento de la ‘Tabla de Contenidos’, verdad?

Cómo construir la lógica

Ahora viene la parte divertida, y definitivamente la más importante. Empecemos con lo que podemos lograr fácilmente y construyamos a partir de eso.

Crear la función de tabla de contenidos dinámica

Primero, necesitamos almacenar todos los elementos H2 en una variable, eso es lo que haremos en la primera línea. Luego, seleccionaremos los elementos aside, ya que tenemos que poblarlos con algo. Después, crearemos un nuevo elemento ul y lo almacenaremos en la variable ul. Después de eso, añadimos el nuevo elemento ul como hijo del elemento aside.

Así es como se ve:

const headings = Array.from(document.getElementsByTagName("h2"));const aside = document.querySelector("aside");const ul = document.createElement("ul");aside.appendChild(ul);headings.map((heading) => {    const id = heading.innerText.toLowerCase().replaceAll(" ", "_");    heading.setAttribute("id", id);    const anchorElement = `<a href="#${id}">${heading.textContent}</a>`;    const keyPointer = `<li>${anchorElement}</li>`;    ul.insertAdjacentHTML("beforeend", keyPointer);});

Ahora, usamos la función map para iterar y realizar una función para cada elemento H2. Primero, creamos un id para cada elemento h2 convirtiendo el contenido de texto en minúsculas y reemplazando los espacios por guiones bajos. Este id se utiliza para vincular con la sección correspondiente.

A continuación, usamos el id que acabamos de crear y lo establecemos como el valor del atributo ‘id’. Luego creamos un elemento de ancla (<a>) con un atributo href que apunta al id generado. El texto del ancla se establece en el contenido de texto del elemento h2.

Ahora, podemos crear un elemento de lista (<li>) que contiene el elemento de ancla creado anteriormente y luego ese elemento de lista se añade como HTML al final de la lista desordenada (ul).

Hacer que la tabla de contenidos sea interactiva

¡Muy bien, estamos a medio camino! En este momento, tenemos una tabla de contenidos dinámica que lista automáticamente todos los elementos h2 con enlaces a sus secciones correspondientes.

Ahora, solo nos queda la parte interactiva. Queremos que nuestro enlace se resalte cuando la sección correspondiente esté visible en la página.

Entonces, ahora que el elemento aside está poblado y contiene etiquetas de anclaje, almacenaremos todos esos anclajes en la variable tocAnchors.

const tocAnchors = aside.querySelectorAll("a");

A continuación, declararemos una función de flecha llamada obFunc que luego se usará en Intersection Observer. Intersection Observer es básicamente una API proporcionada por el navegador. Nos permite observar los cambios en la intersección de los elementos que deseamos con el área de visualización del documento o el elemento raíz que elijas.

const obFunc = (entries) => {}

Ahora, hemos definido una función obFunc que toma un arreglo de entries como su parámetro. La función se ejecutará cada vez que los elementos observados (especificados más adelante) se crucen con el área de visualización.

Dentro del bucle forEach para entries, comprobamos si un elemento observado se cruza con el área de visualización. Si se cumple la condición, encontramos el índice del elemento que se cruza (representado por entry.target) dentro del arreglo headings.

entries.forEach((entry) => {        if (entry.isIntersecting) {        	const index = headings.indexOf(entry.target);        }}

Usando un nuevo bucle forEach, iteramos a través de todos los elementos de anclaje (tocAnchors) y eliminamos la clase “active” de cada uno de ellos para que la clase active no persista en más de un elemento a la vez.

tocAnchors.forEach((tab) => {    tab.classList.remove("active");});

Y ahora agregamos la clase active al elemento de anclaje que se intersecta en ese momento. Además, utilizamos el método scrollIntoView que desplaza la página para llevar el elemento de anclaje activo a la vista. La opción { block: "nearest" } asegura que se desplace hasta la posición más cercana tanto vertical como horizontalmente.

tocAnchors[index].classList.add("active");    tocAnchors[index].scrollIntoView({        block: "nearest"});

Ahora, definimos un objeto, oboption, que actuará como una configuración para el Observador de Intersección. rootMargin especifica los márgenes alrededor del elemento raíz (en este caso, la ventana gráfica), y threshold establece el umbral en el que se activa la función de devolución de llamada.

const obOption = {    rootMargin: "-30px 0% -77%",    threshold: 1};

La opción rootMargin es muy importante aquí. Básicamente estás creando un pseudo-ventana gráfica al crear un desplazamiento desde la ventana gráfica original. Esto se convierte en el área de vigilancia (más o menos).

Esta opción toma valores de la misma manera que lo hace el margen, excepto que cuando usamos valores negativos aquí, lo compensa moviéndose hacia el centro de la pantalla. Definitivamente puedes usar los mismos valores que yo y lograr un área ideal, o puedes jugar con los valores hasta obtener el comportamiento ideal.

Por último, todo lo que tenemos que hacer es crear una nueva instancia del Observador de Intersección con la función previamente definida (obFunc) como devolución de llamada y las opciones (obOption). Y luego utilizaremos el bucle forEach para iterar sobre todos los elementos de H2 y ponerlos en observación utilizando el método .observe().

const observer = new IntersectionObserver(obFunc, obOption);headings.forEach((hTwo) => observer.observe(hTwo));

Cuando alguno de estos elementos se intersecta con la ventana gráfica, la función de devolución de llamada obFunc se ejecutará.

Screen-Recording-2023-11-13-at-3.22.45-PM--online-video-cutter.com-
Demostración del proyecto mostrando la tabla de contenidos a la derecha mientras desplazamos el texto

Conclusión

Ahora tienes una tabla de contenidos totalmente interactiva y dinámica. Espero que este tutorial te haya ayudado. Avísame si puedes construir sobre esto o mejorar aún más este proyecto. ¡Saludos!


Leave a Reply

Your email address will not be published. Required fields are marked *