09. script.js — три модуля
script.js — глобальный файл скриптов. Он подключается на каждой странице сайта, в самом низу body. Содержит три независимых модуля, каждый решает свою задачу.
Структура файла
Три модуля script.jsscript.js
|
+-- МОДУЛЬ 1. Бургер-меню
| Открывает и закрывает навигацию на мобильных
|
+-- МОДУЛЬ 2. Автоопределение активной ссылки
| Подсвечивает текущий раздел в меню
|
+-- МОДУЛЬ 3. Кнопки «Скачать» / «Распечатать»
Обрабатывает кнопки в шаблоне статьи блога
Каждый модуль изолирован. Если убрать один — остальные продолжат работать. Это осознанное решение: модульность. Проще поддерживать, проще искать ошибки.
Могли бы сделать burger.js, active-link.js, tool-buttons.js — и подключать все три на каждой странице. Но это лишние HTTP-запросы, а модулей немного. Пока файл меньше 100 строк — дешевле держать всё вместе.
Общий каркас
Все три модуля построены по одному шаблону:
JavaScript — общий шаблонdocument.addEventListener('DOMContentLoaded', function () {
// ... код модуля ...
});
document — объект страницы. Через него получаем доступ ко всему: элементам, событиям, стилям.
addEventListener('DOMContentLoaded', …) — «подпишись на событие». Событие называется DOMContentLoaded — «DOM-дерево построено». Это значит: HTML-страница загружена, браузер разобрал все теги и создал объекты, к которым можно обращаться.
1. Начало загрузки HTML
2. Браузер читает теги и строит DOM
3. DOM готов --> срабатывает DOMContentLoaded
4. Браузер догружает картинки, шрифты, стили
5. Полная загрузка --> срабатывает window.load
Мы подписываемся именно на DOMContentLoaded, а не на window.load. Причина: DOM готов раньше, чем картинки и шрифты. Если подождать до window.load, пользователь увидит «задержку» перед тем, как заработает бургер.
function () { … } — анонимная функция-обработчик. Она выполнится, когда сработает событие. Внутри неё живёт весь код модуля.
Модуль 1: бургер-меню
Отвечает за открытие и закрытие навигации на мобильных. Пользователь тапает по бургеру — меню выезжает. Тапает ещё раз — меню прячется.
JavaScript — модуль бургераdocument.addEventListener('DOMContentLoaded', function () {
const burger = document.querySelector('.burger');
const nav = document.querySelector('.nav');
if (burger && nav) {
burger.addEventListener('click', function () {
burger.classList.toggle('active');
nav.classList.toggle('open');
});
}
});
Разберём построчно.
Поиск элементов
JavaScriptconst burger = document.querySelector('.burger');
const nav = document.querySelector('.nav');
document.querySelector('.burger') — «найди первый элемент с классом burger». Точка в начале строки означает класс. Если бы искали по ID — писали бы '#header'.
const — «константа». Значение переменной не изменится после присвоения. Мы сохранили ссылку на элемент — она же сама и не поменяется. const защищает от случайного переприсвоения.
Есть старые методы: getElementById, getElementsByClassName, getElementsByTagName. Работают и они. Но querySelector универсальнее: использует те же селекторы, что и CSS. Хочешь найти .header .icon? Просто пишешь querySelector('.header .icon'). Не надо запоминать другие правила.
Защита от отсутствия элементов
JavaScriptif (burger && nav) {
// ...
}
if (burger && nav) — «если и бургер, и nav существуют». Логический оператор && («и») требует, чтобы оба условия были истинными.
В чём смысл? Элемент .burger есть на всех страницах — он подключается через partials/header.html. Но что, если однажды кто-то создаст страницу без этого partial? Тогда querySelector вернёт null. А вызов null.addEventListener — ошибка в консоли, и все остальные скрипты упадут.
Проверка защищает: если элементов нет — тихо выходим из модуля, ничего не ломая.
Обработчик клика
JavaScriptburger.addEventListener('click', function () {
burger.classList.toggle('active');
nav.classList.toggle('open');
});
addEventListener('click', …) — подписаться на клик. Когда пользователь кликает по бургеру, сработает функция.
classList.toggle('active') — «переключи класс active». Если его не было — добавит. Если был — уберёт.
Одновременно переключаются два класса:
- У бургера — active. В CSS это превращает три полоски в «крестик».
- У меню — open. В CSS (в мобильном медиа-запросе) это правило
.nav.open { display: flex }показывает меню.
Клик по бургеру
|
+--> burger.classList.toggle('active')
| Если был "active" - убирает
| Если не было - добавляет
|
+--> nav.classList.toggle('open')
То же самое, но для меню
Состояние «открыто» Состояние «закрыто»
-------------------- ------------------
burger: class="burger active" burger: class="burger"
nav: class="nav open" nav: class="nav"
Модуль 2: активная ссылка в меню
Подсвечивает ту ссылку, которая соответствует текущей странице. Пользователь на «Услугах» — в меню подсвечено «Услуги» зелёной линией.
JavaScript — модуль активной ссылкиdocument.addEventListener('DOMContentLoaded', function () {
const links = document.querySelectorAll('.nav a');
if (!links.length) return;
let current = window.location.pathname
.replace(/\/index\.html$/, '/')
.replace(/\/$/, '');
if (current === '') current = '/';
links.forEach(function (link) {
let href = link.getAttribute('href')
.replace(/\/index\.html$/, '/')
.replace(/\/$/, '');
if (href === '') href = '/';
if (href === current) link.classList.add('active');
});
});
Находим все ссылки в меню
JavaScriptconst links = document.querySelectorAll('.nav a');
if (!links.length) return;
querySelectorAll (с «All» на конце) возвращает все элементы, подходящие под селектор. Здесь — все a внутри .nav.
Результат — не массив, а NodeList. Список узлов. У него есть свои особенности, но метод forEach работает, как у массива.
if (!links.length) return; — «если длина списка 0, выйти». Проверка на защиту от отсутствия меню на странице. Восклицательный знак ! — «не». Читается: «если НЕ длина — то есть если длина нулевая — выходим».
Определяем текущий путь
JavaScriptlet current = window.location.pathname
.replace(/\/index\.html$/, '/')
.replace(/\/$/, '');
if (current === '') current = '/';
Три строки, которые выглядят сложно, но делают простую вещь: приводят текущий URL к «нормальному виду».
window.location.pathname — часть URL после домена. Например:
Примеры pathnameURL pathname
--------------------------------- ------------------
https://secserpr.ru/ /
https://secserpr.ru/index.html /index.html
https://secserpr.ru/services.html /services.html
https://secserpr.ru/blog/ /blog/
Проблема: если пользователь открыл /index.html, а ссылка в меню указывает на / — они не совпадут. Но по сути это одна и та же страница. То же с /services.html и /services.html/.
Решение: убрать из URL то, что не влияет на смысл. Что делает код:
- .replace(/\/index\.html$/, '/') — «замени /index.html в конце строки на /».
- .replace(/\/$/, '') — «убери завершающий слэш, если он есть».
/…/ — это регулярное выражение. Способ искать и заменять по шаблону. \/ — слэш (экранирован), $ — конец строки.
После всех замен: и /index.html, и / приведутся к пустой строке. А if (current === '') current = '/'; вернёт туда слэш для единообразия.
URL: https://secserpr.ru/index.html → pathname /index.html → после замен / → пустая строка → /.
URL: https://secserpr.ru/ → pathname / → после замен пустая строка → /.
URL: https://secserpr.ru/services.html → pathname /services.html → не меняется.
Итог: обе формы главной приводятся к одному виду — /.
Перебор ссылок и сравнение
JavaScriptlinks.forEach(function (link) {
let href = link.getAttribute('href')
.replace(/\/index\.html$/, '/')
.replace(/\/$/, '');
if (href === '') href = '/';
if (href === current) link.classList.add('active');
});
links.forEach(…) — «пробеги по всем элементам списка и выполни для каждого функцию».
Для каждой ссылки повторяем ту же нормализацию, что и для URL. Приводим href к «чистому» виду: убираем /index.html и завершающий слэш.
link.getAttribute('href') — читает атрибут href из HTML. То же самое, что link.href, но без автоматической подстановки домена. Нам нужна именно «сырая» строка из кода.
И финальная проверка: if (href === current) link.classList.add('active'). Если нормализованные значения совпали — добавляем ссылке класс active. CSS тут же подсветит её зелёной линией.
Пример на странице /services.htmlcurrent = "/services.html"
Перебор ссылок:
"/index.html" --> "/" не совпало
"/services.html" --> "/services.html" СОВПАЛО - active
"/about.html" --> "/about.html" не совпало
...
Итог: подсветится только ссылка «Услуги»
Модуль 3: кнопки «Скачать» и «Распечатать»
Обрабатывает кнопки в шаблоне статьи блога. Пока блог не подключён, модуль не работает — но готов к использованию.
JavaScript — модуль 3document.addEventListener('click', function (e) {
const btn = e.target.closest('.tool-btn');
if (!btn) return;
const action = btn.dataset.action;
const postId = btn.dataset.post || 'unknown';
if (action === 'print') {
window.print();
return;
}
if (action === 'download') {
const article = document.querySelector('.article-full');
if (!article) {
alert('Не найден блок статьи для скачивания.');
return;
}
if (typeof html2pdf === 'undefined') {
alert('Библиотека для создания PDF не загружена.');
return;
}
const opt = {
margin: 1,
filename: postId + '.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, letterRendering: true, useCORS: true },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(article).save();
}
});
Модуль принципиально отличается от первых двух. Разбираем ключевые отличия.
Слушаем не кнопку, а документ
JavaScriptdocument.addEventListener('click', function (e) {
Мы подписываемся на клик по всему документу, а не по конкретной кнопке. Причина — делегирование событий.
В шаблоне статьи может быть сколько угодно статей. Нельзя заранее знать, сколько на странице кнопок .tool-btn. Делегирование решает эту проблему: слушаем «глобально», а внутри проверяем, попал ли клик в нужный элемент.
Клик по кнопке «всплывает» — сначала событие происходит на кнопке, потом на её родителе, потом на деде, и так до document. Мы ловим его на верхнем уровне и проверяем: «а клик был по нужной кнопке?». Если да — обрабатываем.
Параметр e — объект события. Внутри него лежит вся информация о клике: куда попал, какую кнопку нажали, координаты.
Поиск «целевой» кнопки
JavaScriptconst btn = e.target.closest('.tool-btn');
if (!btn) return;
e.target — элемент, по которому реально кликнули. Может быть самой кнопкой, а может — SVG-иконкой внутри неё или её строкой SVG.
.closest('.tool-btn') — «найди ближайшего предка с классом tool-btn, включая сам элемент». Если кликнули по иконке внутри кнопки — closest поднимется до кнопки. Если кликнули мимо — вернёт null.
Как работает .closest()Ситуация 1: клик по иконке внутри кнопки
button class="tool-btn" data-action="print"
svg ... <-- e.target (кликнули сюда)
/button <-- .closest('.tool-btn') найдёт это
Ситуация 2: клик по тексту абзаца
p Текст статьи ... <-- e.target
<-- .closest('.tool-btn') = null
выходим
Проверка if (!btn) return; — если кнопка не найдена, выходим. Не обрабатываем клики мимо.
Чтение data-атрибутов
JavaScriptconst action = btn.dataset.action;
const postId = btn.dataset.post || 'unknown';
btn.dataset — доступ ко всем data-* атрибутам элемента. Если в HTML написано <button data-action="print">, то через JS это btn.dataset.action === "print".
btn.dataset.post || 'unknown' — если у кнопки нет атрибута data-post, btn.dataset.post вернёт undefined. Логический оператор || («или») даст второе значение — строку 'unknown'. Это «запасное имя файла», если основной не задан.
Ветка «Печать»
JavaScriptif (action === 'print') {
window.print();
return;
}
Простейший случай. Если кнопка помечена как «печать» — вызываем встроенный метод браузера window.print(). Он открывает системный диалог печати — так же, как Ctrl+P.
return — «выйти из функции». После печати дальше идти не нужно.
Ветка «Скачать PDF»
JavaScriptconst article = document.querySelector('.article-full');
if (!article) {
alert('Не найден блок статьи для скачивания.');
return;
}
if (typeof html2pdf === 'undefined') {
alert('Библиотека для создания PDF не загружена.');
return;
}
Две последовательные проверки. Первая — есть ли блок статьи на странице. Вторая — подключена ли библиотека html2pdf.js.
typeof html2pdf === 'undefined' — «если переменная html2pdf не определена». Оператор typeof возвращает тип значения. Если библиотека не подключена — JS скажет 'undefined'.
Почему нельзя написать просто if (!html2pdf)? Потому что если переменная не определена — это уже ошибка ReferenceError, скрипт упадёт. А typeof безопасен: он не падает, а возвращает строку.
Настройки PDF
JavaScriptconst opt = {
margin: 1,
filename: postId + '.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, letterRendering: true, useCORS: true },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
};
Объект с настройками PDF. Библиотека html2pdf.js понимает такой формат. Разберём ключи.
- margin: 1 — отступ в 1 дюйм со всех сторон.
- filename: postId + '.pdf' — имя файла. Склеиваем значение из data-post с расширением .pdf. Если у кнопки
data-post="115-fz"— имя будет 115-fz.pdf. - image: { type: 'jpeg', quality: 0.98 } — настройки картинок внутри PDF. Формат jpeg, качество 98%. Почти максимальное.
- html2canvas: { scale: 2, … } — как рендерить HTML в картинку. scale: 2 — двойное разрешение. Иначе текст в PDF выглядел бы размытым.
- jsPDF: { … } — настройки итогового файла. Формат a4, ориентация portrait («книжная»), единицы измерения in (дюймы).
Число 1 здесь — один дюйм, потому что единицы заданы как unit: 'in' в настройках jsPDF. Если бы единицы были миллиметры — 1 мм был бы слишком маленьким отступом. Дюйм ≈ 2.54 см. Стандартные поля документа.
Запуск генерации
JavaScripthtml2pdf().set(opt).from(article).save();
Цепочка методов. Читается как английская фраза: «html2pdf установи настройки opt, от элемента article, сохрани».
- html2pdf() — создаём «задание» на генерацию PDF.
- .set(opt) — применяем настройки из объекта opt.
- .from(article) — указываем, какой HTML-блок превратить в PDF.
- .save() — запускаем процесс. Браузер скачает готовый PDF.
Такой стиль называется fluent interface — «плавный интерфейс». Каждый метод возвращает объект, к которому можно применить следующий метод. Одна строка вместо пяти. Читается как обычное предложение.
Итог
- script.js содержит три модуля: бургер, активная ссылка, кнопки PDF/печати.
- Каждый модуль изолирован — можно убрать один, не сломав другие.
- Все модули используют
DOMContentLoaded— работают, когда DOM готов. - Защита от отсутствия элементов —
if (element) { … }. - Модуль 2 (активная ссылка) нормализует URL — убирает /index.html и завершающий слэш.
- Модуль 3 использует делегирование событий — слушает клик на document, а не на каждой кнопке.
- Для PDF — библиотека html2pdf.js. Подключается только на странице статьи блога.
В следующем разделе — scroll-top.js. Там меньше кода, но есть интересные детали: как отслеживать прокрутку и почему кнопка «Наверх» вынесена в отдельный файл.