Как создать интерактивную и динамичную таблицу содержания на JavaScript
Во время чтения технических статей на различных платформах, я постоянно замечал раздел Содержание в боковой панели. Некоторые из них были интерактивными, а некоторые просто содержали ссылки на эти разделы. Содержание обычно оказывается очень полезным для читателей. Оно позволяет легко пролистывать то, что...
При чтении некоторых технических статей на разных платформах, я постоянно замечал раздел “Содержание” в боковой панели. Некоторые из них были интерактивными, а некоторые просто являлись ссылками на эти разделы.
Содержание обычно очень полезно для читателей. Оно позволяет легко просмотреть, о чем будет статья, и найти интересующий вас раздел. Оно также позволяет узнать, содержит ли статья нужную вам информацию, и это большой плюс для доступности.
Инспирируясь всеми этими различными платформами, я попробовал создать свою собственную функциональность Содержания. Я хотел, чтобы она динамически перечисляла все заголовки H2 вместе с ссылками на них. Я также хотел, чтобы заголовки подсвечивались при прокрутке в области видимости. Я взволнован, давайте начнем.
Примечание: Я не мог использовать Codepen, так как он использует iframes для предварительного просмотра результатов – и на данный момент Intersection Observer в iframe работает довольно непредсказуемо. Вот gist для этого кода.
Необходимые навыки
Чтобы получить максимум от этого урока, вам следует быть знакомым с:
- HTML5/CSS3/JavaScript
- 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
.
Итоги
Теперь у вас есть полностью интерактивная и динамичная таблица содержания. Надеюсь, этот урок вам помог. Дайте мне знать, если вы можете продолжить развивать этот проект или улучшить его дальше. Удачи!
Leave a Reply