astro_avtourist/frontend/src/components/blog/ArticleTableOfContents.astro

204 lines
No EOL
5.5 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
interface TOCItem {
id: string;
text: string;
level: number;
}
interface Props {
items: TOCItem[];
}
const { items } = Astro.props;
---
{items.length > 0 && (
<aside class="toc-container" id="toc-sidebar">
<nav class="toc-nav">
<h3 class="toc-title">Оглавление</h3>
<ul class="toc-list">
{items.map((item) => (
<li class={`toc-item level-${item.level}`} data-toc-target={item.id}>
<a href={`#${item.id}`} class="toc-link">
<span class="toc-text">{item.text}</span>
</a>
</li>
))}
</ul>
</nav>
</aside>
)}
<style is:global>
.toc-container {
width: 100%;
margin-top: 1.5rem;
transition: margin-top 0.3s ease;
}
.toc-container.no-top-margin {
margin-top: 0 !important;
}
.toc-nav {
background: #ffffff;
border-radius: 1.25rem;
padding: 1.5rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
}
.toc-title {
color: #1e3050;
font-size: 1.1rem;
font-weight: 800;
margin: 0 0 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid #eac26e;
text-align: center;
}
.toc-list {
list-style: none;
padding: 0;
margin: 0;
counter-reset: toc-h2;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
/* Номера только для H2 (level-1/2), H3 без номеров */
.toc-item.level-1, .toc-item.level-2 { counter-reset: toc-h3; counter-increment: toc-h2; }
.toc-item.level-3 { padding-left: 1.25rem; counter-increment: toc-h3; }
.toc-item.level-3 .toc-link::before { content: none; }
/* h2 = level 1 в HTML */
.toc-item.level-1, .toc-item.level-2 {
font-weight: 600;
}
.toc-item.level-1 .toc-link, .toc-item.level-2 .toc-link {
font-size: 0.95rem;
padding: 0.6rem 0.75rem;
}
.toc-link {
display: flex;
padding: 0.5rem 0.75rem;
color: #64748b;
text-decoration: none;
font-size: 0.9rem;
line-height: 1.4;
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.toc-link::before {
content: "";
display: inline-block;
width: 1.5rem;
color: #1e3050;
font-weight: 700;
margin-right: 0.6rem;
flex-shrink: 0;
text-align: center;
}
.toc-item.level-1 .toc-link::before, .toc-item.level-2 .toc-link::before { content: counter(toc-h2) "."; }
/* H3 без номеров */
.toc-link:hover { color: #1e3050; background: #f1f5f9; }
.toc-item.active .toc-link {
color: #eac26e;
font-weight: 700;
background: rgba(234, 194, 110, 0.1);
}
.toc-item.active .toc-link::before { color: #eac26e; }
</style>
<script>
function setupTOC() {
const toc = document.getElementById('toc-sidebar');
if (!toc) return;
let lastScrollY = window.scrollY;
let ticking = false;
const updateMargin = () => {
const currentScrollY = window.scrollY;
// При скролле ВНИЗ - убрать отступ
if (currentScrollY > lastScrollY) {
toc.classList.add('no-top-margin');
} else {
// При скролле ВВЕРХ или начальная позиция - вернуть отступ
toc.classList.remove('no-top-margin');
}
lastScrollY = currentScrollY;
ticking = false;
};
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => updateMargin());
ticking = true;
}
});
const items = toc.querySelectorAll('.toc-item');
const links = toc.querySelectorAll<HTMLAnchorElement>('.toc-link');
const HEADER_OFFSET = 120; // Высота хедера + запас
// --- 1. ПЛАВНЫЙ СКРОЛЛ ПРИ КЛИКЕ ---
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const id = link.getAttribute('href')?.substring(1);
const target = document.getElementById(id || '');
if (target) {
const elementPosition = target.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - HEADER_OFFSET;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
// Обновляем URL
history.pushState(null, '', `#${id}`);
}
});
});
// --- 2. ПОДСВЕТКА ПРИ СКРОЛЛЕ (SCROLL SPY) ---
const headingElements = Array.from(items)
.map(item => {
const id = item.getAttribute('data-toc-target');
return id ? document.getElementById(id) : null;
})
.filter((el): el is HTMLElement => el !== null);
const observerOptions = {
root: null,
rootMargin: '-120px 0px -70% 0px', // Зона срабатывания: верхняя часть экрана
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const activeId = entry.target.id;
items.forEach(item => {
if (item.getAttribute('data-toc-target') === activeId) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
});
}, observerOptions);
headingElements.forEach(heading => observer.observe(heading));
}
// Запуск для обычной загрузки и для Astro ViewTransitions
document.addEventListener('DOMContentLoaded', setupTOC);
document.addEventListener('astro:page-load', setupTOC);
</script>