feat: постраничная навигация по главам в teacher-guide (showChapter + hash-роутинг)

This commit is contained in:
Maxim Dolgolyov
2026-04-14 08:51:06 +03:00
parent 8317a991c4
commit dfb7c75fbf
+60 -81
View File
@@ -157,7 +157,12 @@
.tg-hero-chip svg { width: 13px; height: 13px; }
/* Chapter */
.tg-chapter { margin-bottom: 56px; }
.tg-chapter { margin-bottom: 0; display: none; }
.tg-chapter.tg-active { display: block; animation: tgFadeIn 0.28s ease; }
@keyframes tgFadeIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
/* Search mode: reveal all chapters so matches are visible */
.tg-content.tg-search-mode .tg-chapter { display: block; margin-bottom: 32px; }
.tg-content.tg-search-mode .tg-chapter.search-hidden { display: none; }
.tg-chapter-header {
display: flex; align-items: center; gap: 16px;
padding: 28px 0 18px;
@@ -1061,56 +1066,54 @@
});
lucide.createIcons();
function navChapterClick(chId, btn) {
const chapter = btn.closest('.tg-nav-chapter');
chapter.classList.toggle('open');
scrollToChapter(chId);
}
function scrollToChapter(chId) {
const el = document.getElementById(chId);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function scrollToSection(secId) {
const el = document.getElementById(secId);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/* ── Scroll progress ── */
/* ── Chapter switching ── */
const scrollEl = document.getElementById('tg-scroll');
const progBar = document.getElementById('tg-prog-bar');
const tgContent = document.querySelector('.tg-content');
let _activeChId = null;
scrollEl.addEventListener('scroll', () => {
const pct = scrollEl.scrollTop / (scrollEl.scrollHeight - scrollEl.clientHeight);
progBar.style.width = Math.round(Math.min(pct, 1) * 100) + '%';
});
/* ── Scroll spy (sections) ── */
const allSections = document.querySelectorAll('.tg-section');
const allNavSecs = document.querySelectorAll('.tg-nav-sec-link');
const sectionObs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (!e.isIntersecting) return;
const id = e.target.id;
allNavSecs.forEach(a => a.classList.toggle('active', a.dataset.sec === id));
// Also open+highlight parent chapter
const ch = CHAPTERS.find(c => c.sections.includes(id));
if (ch) {
document.querySelectorAll('.tg-nav-ch-btn').forEach(b => {
const parentDiv = b.closest('.tg-nav-chapter');
const isThis = parentDiv.dataset.ch === ch.id;
b.classList.toggle('active', isThis);
if (isThis && !parentDiv.classList.contains('open')) parentDiv.classList.add('open');
});
}
function showChapter(chId, sectionId) {
const newEl = document.getElementById(chId);
if (!newEl) return;
if (_activeChId) {
const prev = document.getElementById(_activeChId);
if (prev) prev.classList.remove('tg-active');
}
newEl.classList.add('tg-active');
_activeChId = chId;
history.replaceState(null, '', '#' + chId);
scrollEl.scrollTop = 0;
if (sectionId) {
requestAnimationFrame(() => {
const sec = document.getElementById(sectionId);
if (sec) {
const top = sec.offsetTop - 80;
scrollEl.scrollTo({ top, behavior: 'smooth' });
}
});
}
// Nav: highlight active chapter, open its sub-list
document.querySelectorAll('.tg-nav-chapter').forEach(div => {
const isThis = div.dataset.ch === chId;
div.querySelector('.tg-nav-ch-btn').classList.toggle('active', isThis);
if (isThis && !div.classList.contains('open')) div.classList.add('open');
});
}, { root: scrollEl, rootMargin: '-15% 0px -70% 0px' });
document.querySelectorAll('.tg-nav-sec-link').forEach(a => a.classList.remove('active'));
markRead(chId);
}
allSections.forEach(s => sectionObs.observe(s));
function scrollToChapter(chId) { showChapter(chId); }
function scrollToSection(secId) {
const ch = CHAPTERS.find(c => c.sections.includes(secId));
if (ch) showChapter(ch.id, secId);
}
/* ── Chapter read tracking ── */
function navChapterClick(chId, btn) {
btn.closest('.tg-nav-chapter').classList.toggle('open');
showChapter(chId);
}
/* ── Read tracking ── */
const READ_KEY = 'ls_tg_read';
let readChapters = JSON.parse(localStorage.getItem(READ_KEY) || '[]');
@@ -1118,28 +1121,20 @@
if (!readChapters.includes(chId)) {
readChapters.push(chId);
localStorage.setItem(READ_KEY, JSON.stringify(readChapters));
updateReadUI();
}
updateReadUI();
}
function updateReadUI() {
document.querySelectorAll('.tg-nav-chapter').forEach(div => {
const isRead = readChapters.includes(div.dataset.ch);
div.querySelector('.tg-nav-ch-btn').classList.toggle('read', isRead);
div.querySelector('.tg-nav-ch-btn').classList.toggle('read', readChapters.includes(div.dataset.ch));
});
const n = readChapters.length;
document.getElementById('tg-prog-text').textContent = `${n} из ${CHAPTERS.length} глав прочитано`;
progBar.style.width = Math.round(n / CHAPTERS.length * 100) + '%';
}
updateReadUI();
const chapterObs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting && e.intersectionRatio >= 0.15) markRead(e.target.id);
});
}, { root: scrollEl, threshold: 0.15 });
document.querySelectorAll('.tg-chapter').forEach(c => chapterObs.observe(c));
/* ── Checklist ── */
const CL_KEY = 'ls_tg_checklist';
const clState = JSON.parse(localStorage.getItem(CL_KEY) || '{}');
@@ -1154,7 +1149,6 @@
localStorage.setItem(CL_KEY, JSON.stringify(clState));
updateChecklist();
});
// prevent link from triggering checkbox
item.querySelector('.tg-cl-link')?.addEventListener('click', e => e.stopPropagation());
});
@@ -1168,44 +1162,29 @@
/* ── Accordion ── */
function toggleAcc(head) {
const item = head.closest('.tg-acc-item');
item.classList.toggle('open');
head.closest('.tg-acc-item').classList.toggle('open');
}
/* ── Nav search ── */
/* ── Search: show all chapters matching query ── */
document.getElementById('tg-search').addEventListener('input', function() {
const q = this.value.trim().toLowerCase();
if (!q) {
tgContent.classList.remove('tg-search-mode');
document.querySelectorAll('.tg-chapter, .tg-section').forEach(el => el.classList.remove('search-hidden'));
return;
}
tgContent.classList.add('tg-search-mode');
document.querySelectorAll('.tg-section').forEach(sec => {
const text = sec.textContent.toLowerCase();
sec.classList.toggle('search-hidden', !text.includes(q));
sec.classList.toggle('search-hidden', !sec.textContent.toLowerCase().includes(q));
});
document.querySelectorAll('.tg-chapter').forEach(ch => {
const visibleSecs = ch.querySelectorAll('.tg-section:not(.search-hidden)');
ch.classList.toggle('search-hidden', visibleSecs.length === 0);
ch.classList.toggle('search-hidden', ch.querySelectorAll('.tg-section:not(.search-hidden)').length === 0);
});
});
/* ── Card entrance animation ── */
const chapObs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.style.opacity = '1';
e.target.style.transform = 'translateY(0)';
chapObs.unobserve(e.target);
}
});
}, { root: scrollEl, threshold: 0.05 });
document.querySelectorAll('.tg-chapter').forEach((ch, i) => {
ch.style.opacity = '0';
ch.style.transform = 'translateY(20px)';
ch.style.transition = `opacity 0.4s ease ${i * 0.03}s, transform 0.4s ease ${i * 0.03}s`;
chapObs.observe(ch);
});
/* ── Init from hash or default ch-1 ── */
const initHash = location.hash.replace('#', '');
showChapter(CHAPTERS.find(c => c.id === initHash) ? initHash : 'ch-1');
</script>
</body>
</html>