Как создать интерактивную и динамичную таблицу содержания на JavaScript

Во время чтения технических статей на различных платформах, я постоянно замечал раздел Содержание в боковой панели. Некоторые из них были интерактивными, а некоторые просто содержали ссылки на эти разделы. Содержание обычно оказывается очень полезным для читателей. Оно позволяет легко пролистывать то, что...

При чтении некоторых технических статей на разных платформах, я постоянно замечал раздел “Содержание” в боковой панели. Некоторые из них были интерактивными, а некоторые просто являлись ссылками на эти разделы.

Содержание обычно очень полезно для читателей. Оно позволяет легко просмотреть, о чем будет статья, и найти интересующий вас раздел. Оно также позволяет узнать, содержит ли статья нужную вам информацию, и это большой плюс для доступности.

Инспирируясь всеми этими различными платформами, я попробовал создать свою собственную функциональность Содержания. Я хотел, чтобы она динамически перечисляла все заголовки H2 вместе с ссылками на них. Я также хотел, чтобы заголовки подсвечивались при прокрутке в области видимости. Я взволнован, давайте начнем.

Примечание: Я не мог использовать Codepen, так как он использует iframes для предварительного просмотра результатов – и на данный момент Intersection Observer в iframe работает довольно непредсказуемо. Вот gist для этого кода.

Intersection ObserverIntersection Observer. GitHub Gist: мгновенно обменивайтесь кодом, заметками и фрагментами.favicon262588213843476Gistgist-og-image-54fd7dc0713e

Необходимые навыки

Чтобы получить максимум от этого урока, вам следует быть знакомым с:

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

Хорошо, теперь давайте погрузимся.

Настройка проекта

Прежде всего, настроим структуру HTML для нашего Содержания. Ничего особенного – просто тег <article>, обертывающий весь контент, с боковым тегом <aside> как соседом, все это обернуто тегом <main>.

Вот как это будет выглядеть:

<main>    <article>        <h1>Главный заголовок</h1>        <h2>Первый заголовок</h2>        <p>Lorem ipsum dolor sit...</p>        <h2>Второй заголовок</h2>        <p>Lorem ipsum dolor sit...</p>        <h2>Третий заголовок</h2>        <p>Lorem ipsum dolor sit...</p>    </article>    <aside></aside></main>

Тег <aside> пустой, так как он будет заполняться в зависимости от содержимого в <article> с использованием JavaScript.

Мы закончили с частью структуры. Давайте добавим немного стилизации, чтобы можно было отличить неактивные и активные ссылки.

Как добавить стилизацию

Я импортировал шрифт Google под названием DM Sans для этого мини-проекта. В моем CSS я использую вложенность стилей.

@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;    }  }

Я использовал display: grid; чтобы собрать раскладку, где контент занимает три четверти контейнера (в данном случае, видимого окна), а оглавление занимает оставшуюся четверть пространства.

Я оставил <aside> закрепленным, чтобы он оставался в поле зрения при прокрутке контента. Ведь нам нужно испытать интерактивность и поведение этого “Оглавления”, верно?

Как построить логику

Итак, вот самое интересное – и, безусловно, самая важная часть. Давайте начнем с того, что легко можно достичь, и будем наращивать это.

Создание функции динамического оглавления

Сначала нам необходимо сохранить все элементы H2 в переменную, и именно это мы делаем в первой строке. Затем мы выбираем элементы aside, так как мы должны заполнить его чем-то. Затем мы создаем новый элемент ul и сохраняем его в переменной ul. После этого мы добавляем только что созданный элемент ul как дочерний элемент элемента aside.

Вот как это выглядит:

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);});

Теперь мы используем функцию map, чтобы перебрать и выполнить функцию для каждого элемента H2. Сначала мы создаем id для каждого элемента h2, преобразуя текстовое содержимое в нижний регистр и заменяя пробелы на знаки подчеркивания. Этот id используется для создания ссылок на соответствующий раздел.

Затем мы используем только что созданный id и устанавливаем его в качестве значения атрибута ‘id’. Затем мы создаем элемент якоря (<a>) с атрибутом href, указывающим на сгенерированный id. Текст якоря устанавливается в текстовое содержимое элемента h2.

Теперь мы можем создать элемент списка (<li>), содержащий ранее созданный элемент якоря, а затем этот элемент списка добавляется в HTML в конец неупорядоченного списка (ul).

Сделать оглавление интерактивным

Вот мы и почти пришли к цели! В данный момент у нас есть динамическое оглавление, которое автоматически перечисляет все элементы h2 с их ссылками на закладки.

Теперь у нас осталась только интерактивная часть. Мы хотим, чтобы наша ссылка выделялась, когда соответствующий раздел находится в пределах видимости страницы.

Так что, теперь, когда элемент aside заполнен и содержит теги якорей, мы сохраним все эти якори и назовем это tocAnchors.

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

Затем мы объявим стрелочную функцию с именем obFunc, которая будет использоваться позже в Intersection Observer. Intersection Observer – это, по сути, API, предоставляемый браузером. Он позволяет наблюдать за изменениями в пересечении элементов, которые нам нужны, с видимым окном документа или корневым элементом по вашему выбору.

const obFunc = (entries) => {}

Теперь мы определили функцию obFunc, которая принимает массив entries в качестве параметра. Эта функция будет выполняться каждый раз, когда наблюдаемые элементы (указанные позже) пересекаются с видимым окном.

В пределах цикла forEach для entries мы проверяем, пересекается ли наблюдаемый элемент с видимым окном. Если условие выполняется, мы находим индекс пересекающегося элемента (представленного entry.target) в массиве headings.

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

Используя новый цикл forEach, мы проходимся по всем элементам якорей (tocAnchors) и удаляем класс “active” у каждого из них, чтобы класс active не сохранялся на более чем одном элементе одновременно.

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

Теперь мы добавляем класс active к элементу якоря, который пересекается в данный момент. Кроме того, мы используем метод scrollIntoView, который прокручивает страницу, чтобы активный элемент якоря оказался в видимой области. Опция { block: "nearest" } гарантирует, что прокрутка будет выполняться до ближайшего положения как по вертикали, так и по горизонтали.

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

Теперь мы определяем объект obOption, который будет служить конфигурацией для Intersection Observer. rootMargin указывает отступы вокруг корня (в данном случае, области просмотра), а threshold устанавливает порог, при котором будет срабатывать функция обратного вызова.

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

Опция rootMargin очень важна здесь. Практически вы задаете псевдо-область просмотра, создавая смещение от оригинальной области просмотра. Это становится областью наблюдения (более или менее).

Эта опция принимает значения так же, как и отступы – за исключением того, что отрицательные значения здесь обрабатываются смещением к центру экрана. Вы можете использовать те же значения, что и у меня, и достичь идеальной области, или поиграться с ними, пока не получите желаемое поведение.

Наконец, все, что нам нужно сделать, это создать новый экземпляр Intersection Observer с ранее определенными функцией обратного вызова (obFunc) и параметрами (obOption). Затем мы используем цикл forEach, чтобы пройтись по всем элементам H2 и наблюдать за ними с помощью метода .observe().

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

Когда любой из этих элементов пересекается с областью просмотра, вызывается функция обратного вызова obFunc.

Screen-Recording-2023-11-13-at-3.22.45-PM--online-video-cutter.com-
Продемонстрированный проект с ToC справа при прокрутке текста

Итоги

Теперь у вас есть полностью интерактивная и динамичная таблица содержания. Надеюсь, этот урок вам помог. Дайте мне знать, если вы можете продолжить развивать этот проект или улучшить его дальше. Удачи!


Leave a Reply

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