Bottom dot-nav built from the slides (Danish hover labels, every slide reachable), IntersectionObserver scroll-spy + slide counter, keyboard nav, and reveal-on-enter that degrades gracefully without JS. README covers how to preview, the slide map, and the placeholdered logos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
4.2 KiB
JavaScript
113 lines
4.2 KiB
JavaScript
// ─────────────────────────────────────────────────────────────
|
|
// deck.js — Bifrost Advisory Board presentation runtime
|
|
//
|
|
// 1. Builds the bottom dot-nav from the slides in the DOM (one
|
|
// dot per slide, Danish label from data-label).
|
|
// 2. Scroll-snap is CSS-driven; here we keep the active dot +
|
|
// slide counter in sync via an IntersectionObserver, and
|
|
// smooth-scroll to a slide when its dot is clicked.
|
|
// 3. Adds .in-view to the active slide so its .reveal children
|
|
// animate in.
|
|
// 4. Keyboard: ↑/↓, PageUp/PageDown, Home/End jump between slides.
|
|
//
|
|
// No external libraries; no inline scripts (CSP-friendly).
|
|
// ─────────────────────────────────────────────────────────────
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Mark that JS is live — the .reveal hiding rule is gated on this, so a
|
|
// failed/blocked script leaves the deck fully visible rather than blank.
|
|
document.documentElement.classList.add('js');
|
|
|
|
const deck = document.getElementById('deck');
|
|
const nav = document.getElementById('dot-nav');
|
|
const counter = document.getElementById('counter-now');
|
|
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
if (!deck || !nav || !slides.length) return;
|
|
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
|
|
// ─── Build the dot-nav ───────────────────────────────────────
|
|
const dots = slides.map((slide, i) => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'dot-btn';
|
|
btn.type = 'button';
|
|
btn.dataset.index = String(i);
|
|
btn.setAttribute('aria-label', `${i + 1}. ${slide.dataset.label || ''}`.trim());
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'label';
|
|
label.textContent = slide.dataset.label || `Slide ${i + 1}`;
|
|
|
|
const dot = document.createElement('span');
|
|
dot.className = 'dot';
|
|
|
|
btn.appendChild(label);
|
|
btn.appendChild(dot);
|
|
btn.addEventListener('click', () => goTo(i));
|
|
nav.appendChild(btn);
|
|
return btn;
|
|
});
|
|
|
|
let activeIndex = 0;
|
|
|
|
function setActive(i) {
|
|
if (i === activeIndex) return;
|
|
activeIndex = i;
|
|
dots.forEach((d, j) => d.classList.toggle('is-active', j === i));
|
|
if (counter) counter.textContent = pad(i + 1);
|
|
}
|
|
|
|
function goTo(i) {
|
|
const target = slides[i];
|
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
// ─── Active-slide tracking + reveal ──────────────────────────
|
|
const io = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
const i = slides.indexOf(entry.target);
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('in-view');
|
|
// A slide counts as "active" once it owns most of the viewport.
|
|
if (entry.intersectionRatio >= 0.5) setActive(i);
|
|
}
|
|
});
|
|
}, { root: deck, threshold: [0, 0.5, 0.9] });
|
|
|
|
slides.forEach((s) => io.observe(s));
|
|
|
|
// First slide is visible immediately on load.
|
|
slides[0].classList.add('in-view');
|
|
setActive(0);
|
|
if (counter) counter.textContent = pad(1);
|
|
|
|
// ─── Keyboard navigation ─────────────────────────────────────
|
|
window.addEventListener('keydown', (e) => {
|
|
// Ignore when a control/input has focus (none here, but future-proof).
|
|
const tag = (e.target && e.target.tagName) || '';
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
case 'PageDown':
|
|
case ' ':
|
|
e.preventDefault();
|
|
goTo(Math.min(activeIndex + 1, slides.length - 1));
|
|
break;
|
|
case 'ArrowUp':
|
|
case 'PageUp':
|
|
e.preventDefault();
|
|
goTo(Math.max(activeIndex - 1, 0));
|
|
break;
|
|
case 'Home':
|
|
e.preventDefault();
|
|
goTo(0);
|
|
break;
|
|
case 'End':
|
|
e.preventDefault();
|
|
goTo(slides.length - 1);
|
|
break;
|
|
}
|
|
});
|
|
})();
|