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.
Requisitos previos
Para aprovechar al máximo este tutorial, debes estar familiarizado/a con:
- HTML5/CSS3/JavaScript
- 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á.
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