Files

123 lines
4.2 KiB
JavaScript

import { prefersReducedMotion } from "../shared/media.js";
export function initHeaderScroll() {
const header = document.querySelector(".header");
if (!header) return;
function updateHeaderScrollPadding() {
const inner = header.querySelector(".header__inner");
let h;
if (header.classList.contains("header--scrolled") && inner) {
const r = inner.getBoundingClientRect();
h = Math.ceil(r.bottom + 20);
} else {
h = Math.ceil(header.getBoundingClientRect().height);
}
document.documentElement.style.setProperty("--header-offset", `${Math.max(h, 52)}px`);
}
function getScrollPaddingTopPx() {
const raw = getComputedStyle(document.documentElement).scrollPaddingTop;
const px = Number.parseFloat(raw);
return Number.isFinite(px) ? px : 0;
}
/** Вертикальный скролл к якорю с учётом `scroll-padding-top` на `html` (запас под шапку). */
function scrollWindowToElementTop(el, behavior) {
updateHeaderScrollPadding();
const pad = getScrollPaddingTopPx();
const top = el.getBoundingClientRect().top + window.scrollY - pad;
window.scrollTo({ top: Math.max(0, top), behavior });
}
/** Smooth-скролл к якорю + одна подстройка после окончания (layout / смена режима шапки). */
function scrollToInPageAnchor(el) {
const reduced = prefersReducedMotion();
if (reduced) {
scrollWindowToElementTop(el, "auto");
return;
}
let settled = false;
const settle = () => {
if (settled) return;
updateHeaderScrollPadding();
const pad = getScrollPaddingTopPx();
const y = Math.max(0, el.getBoundingClientRect().top + window.scrollY - pad);
if (Math.abs(window.scrollY - y) <= 2) {
settled = true;
return;
}
settled = true;
window.scrollTo({ top: y, behavior: "auto" });
};
scrollWindowToElementTop(el, "smooth");
window.addEventListener("scrollend", settle, { passive: true, once: true });
window.setTimeout(settle, 550);
}
function onScroll() {
const y = window.scrollY;
const scrollDown = y > onScroll.lastY + 1;
const headerHeight = Math.max(52, Math.ceil(header.getBoundingClientRect().height || 0));
const floatStartY = Math.ceil(headerHeight * 1.35);
if (onScroll.isFloating) {
if (y <= Math.floor(headerHeight * 0.45)) onScroll.isFloating = false;
} else if (scrollDown && y > floatStartY) {
onScroll.isFloating = true;
}
const hadScrolled = header.classList.contains("header--scrolled");
header.classList.toggle("header--scrolled", onScroll.isFloating);
if (hadScrolled !== header.classList.contains("header--scrolled")) updateHeaderScrollPadding();
onScroll.lastY = y;
}
onScroll.lastY = window.scrollY;
onScroll.isFloating = false;
document.querySelectorAll('a[href^="#"]').forEach((link) => {
link.addEventListener("click", (e) => {
const id = link.getAttribute("href");
if (!id || id === "#") return;
const el = document.querySelector(id);
if (!el) return;
e.preventDefault();
scrollToInPageAnchor(el);
history.pushState(null, "", id);
});
});
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
if (typeof ResizeObserver !== "undefined") {
const roHeader = new ResizeObserver(() => updateHeaderScrollPadding());
roHeader.observe(header);
const headerInner = header.querySelector(".header__inner");
if (headerInner) roHeader.observe(headerInner);
}
window.addEventListener("resize", updateHeaderScrollPadding, { passive: true });
window.addEventListener(
"load",
() => {
updateHeaderScrollPadding();
const raw = location.hash;
if (!raw || raw.length < 2) return;
let id;
try {
id = decodeURIComponent(raw.slice(1));
} catch {
return;
}
if (!id) return;
const el = document.getElementById(id);
if (!el) return;
requestAnimationFrame(() => scrollWindowToElementTop(el, "auto"));
},
{ once: true },
);
updateHeaderScrollPadding();
}