Files
the-other-dude/docs/website/script.js
Jason Staack 7126621e83 fix(docs): make bullet throb repeat on every scroll into view
Remove unobserve so bullets reset when scrolled out and throb again
on re-entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:05:22 -05:00

275 lines
8.4 KiB
JavaScript

/* TOD Documentation Website — Shared JavaScript */
(function () {
'use strict';
/* -------------------------------------------------- */
/* 1. Scroll Spy (docs page) */
/* -------------------------------------------------- */
function initScrollSpy() {
const sidebar = document.querySelector('.sidebar-nav');
if (!sidebar) return;
const links = Array.from(document.querySelectorAll('.sidebar-link'));
const sections = links
.map(function (link) {
var id = link.getAttribute('data-section');
return id ? document.getElementById(id) : null;
})
.filter(Boolean);
if (!sections.length) return;
var current = null;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var id = entry.target.id;
if (id !== current) {
current = id;
links.forEach(function (l) {
l.classList.toggle(
'sidebar-link--active',
l.getAttribute('data-section') === id
);
});
/* keep active link visible in sidebar */
var active = sidebar.querySelector('.sidebar-link--active');
if (active) {
active.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}
}
});
},
{ rootMargin: '-80px 0px -60% 0px', threshold: 0 }
);
sections.forEach(function (s) {
observer.observe(s);
});
}
/* -------------------------------------------------- */
/* 2. Docs Search */
/* -------------------------------------------------- */
function initDocsSearch() {
var input = document.getElementById('docs-search-input');
if (!input) return;
var content = document.getElementById('docs-content');
if (!content) return;
var sections = Array.from(content.querySelectorAll('section[id]'));
var links = Array.from(document.querySelectorAll('.sidebar-link'));
input.addEventListener('input', function () {
var q = input.value.trim().toLowerCase();
if (!q) {
sections.forEach(function (s) { s.style.display = ''; });
links.forEach(function (l) { l.style.display = ''; });
return;
}
sections.forEach(function (s) {
var text = s.textContent.toLowerCase();
var match = text.indexOf(q) !== -1;
s.style.display = match ? '' : 'none';
});
links.forEach(function (l) {
var sectionId = l.getAttribute('data-section');
var section = sectionId ? document.getElementById(sectionId) : null;
if (section) {
l.style.display = section.style.display;
}
});
});
}
/* -------------------------------------------------- */
/* 3. Back to Top */
/* -------------------------------------------------- */
function initBackToTop() {
var btn = document.getElementById('back-to-top');
if (!btn) return;
window.addEventListener('scroll', function () {
btn.classList.toggle('back-to-top--visible', window.scrollY > 400);
}, { passive: true });
}
window.scrollToTop = function () {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
/* -------------------------------------------------- */
/* 4. Sidebar Toggle (mobile) */
/* -------------------------------------------------- */
window.toggleSidebar = function () {
var sidebar = document.getElementById('docs-sidebar');
if (!sidebar) return;
sidebar.classList.toggle('docs-sidebar--open');
};
function initSidebarClose() {
var sidebar = document.getElementById('docs-sidebar');
if (!sidebar) return;
/* close on outside click */
document.addEventListener('click', function (e) {
if (
sidebar.classList.contains('docs-sidebar--open') &&
!sidebar.contains(e.target) &&
!e.target.closest('.docs-hamburger')
) {
sidebar.classList.remove('docs-sidebar--open');
}
});
/* close on link click (mobile) */
sidebar.addEventListener('click', function (e) {
if (e.target.closest('.sidebar-link')) {
sidebar.classList.remove('docs-sidebar--open');
}
});
}
/* -------------------------------------------------- */
/* 5. Reveal Animation (landing page) */
/* -------------------------------------------------- */
function initReveal() {
var els = document.querySelectorAll('.reveal');
if (!els.length) return;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('reveal--visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
els.forEach(function (el) {
observer.observe(el);
});
}
/* -------------------------------------------------- */
/* 6. Smooth scroll for anchor links */
/* -------------------------------------------------- */
function initSmoothScroll() {
document.addEventListener('click', function (e) {
var link = e.target.closest('a[href^="#"]');
if (!link) return;
var id = link.getAttribute('href').slice(1);
var target = document.getElementById(id);
if (!target) return;
e.preventDefault();
var offset = 80;
var top = target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: top, behavior: 'smooth' });
/* update URL without jump */
history.pushState(null, '', '#' + id);
});
}
/* -------------------------------------------------- */
/* 7. Active nav link (landing page) */
/* -------------------------------------------------- */
function initActiveNav() {
var navLinks = document.querySelectorAll('.nav-link[href^="index.html#"]');
if (!navLinks.length) return;
/* only run on landing page */
if (document.body.classList.contains('docs-page')) return;
var sectionIds = [];
navLinks.forEach(function (l) {
var hash = l.getAttribute('href').split('#')[1];
if (hash) sectionIds.push({ id: hash, link: l });
});
if (!sectionIds.length) return;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
sectionIds.forEach(function (item) {
item.link.classList.toggle(
'nav-link--active',
item.id === entry.target.id
);
});
}
});
},
{ rootMargin: '-80px 0px -60% 0px', threshold: 0 }
);
sectionIds.forEach(function (item) {
var el = document.getElementById(item.id);
if (el) observer.observe(el);
});
}
/* -------------------------------------------------- */
/* 8. Bullet throb on scroll (landing page) */
/* -------------------------------------------------- */
function initBulletThrob() {
var items = document.querySelectorAll('.content-list li');
if (!items.length) return;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
var li = entry.target;
if (entry.isIntersecting) {
/* stagger each bullet by its index within the list */
var siblings = Array.from(li.parentElement.children);
var idx = siblings.indexOf(li);
setTimeout(function () {
li.classList.add('in-view');
}, idx * 120);
} else {
/* reset when scrolled out so it throbs again on re-entry */
li.classList.remove('in-view');
}
});
},
{ threshold: 0.3 }
);
items.forEach(function (item) {
observer.observe(item);
});
}
/* -------------------------------------------------- */
/* Init on DOMContentLoaded */
/* -------------------------------------------------- */
document.addEventListener('DOMContentLoaded', function () {
initScrollSpy();
initDocsSearch();
initBackToTop();
initSidebarClose();
initReveal();
initSmoothScroll();
initActiveNav();
initBulletThrob();
});
})();