slidewalk/web/assets/vendor/nav.js
2026-07-15 20:14:07 +02:00

181 lines
4.9 KiB
JavaScript

(function () {
"use strict";
var slides = Array.prototype.slice.call(document.querySelectorAll("section.slide"));
var total = slides.length;
var progressEl = document.querySelector(".progress");
var baseTitle = document.title;
var renderedMermaid = {};
var fragmentsCache = {};
var current = 1;
var fragmentIndex = 0;
var gridMode = false;
function clamp(n) {
if (n < 1) return 1;
if (n > total) return total;
return n;
}
// fragmentsFor returns the "fragment" elements of the given 1-based slide
// index, in document order, computed and cached once per slide. A slide
// marked with data-incremental="true" gets its top-level list items
// auto-tagged as fragments in addition to any explicitly marked via raw
// HTML (class="fragment").
function fragmentsFor(index) {
if (fragmentsCache[index]) return fragmentsCache[index];
var slide = slides[index - 1];
if (!slide) return [];
if (slide.dataset.incremental === "true") {
var autoItems = slide.querySelectorAll(":scope > ul > li, :scope > ol > li");
Array.prototype.forEach.call(autoItems, function (li) {
li.classList.add("fragment");
});
}
var frags = Array.prototype.slice.call(slide.querySelectorAll(".fragment"));
frags.forEach(function (el, i) {
el.setAttribute("data-fragment-index", String(i + 1));
});
fragmentsCache[index] = frags;
return frags;
}
function applyFragments(index, count) {
fragmentsFor(index).forEach(function (el, i) {
el.classList.toggle("fragment-visible", i < count);
});
}
function parseHash() {
var raw = location.hash.replace("#", "");
var parts = raw.split(".");
var slide = parseInt(parts[0], 10);
var fragment = parseInt(parts[1], 10);
return {
slide: isNaN(slide) ? 1 : clamp(slide),
fragment: isNaN(fragment) ? 0 : Math.max(0, fragment)
};
}
function renderMermaid(index) {
if (renderedMermaid[index]) return;
var slide = slides[index - 1];
if (!slide) return;
var nodes = slide.querySelectorAll("pre.mermaid");
if (nodes.length === 0) return;
renderedMermaid[index] = true;
mermaid.run({ nodes: Array.prototype.slice.call(nodes) });
}
function show(index, fragment, updateHash) {
current = clamp(index);
var frags = fragmentsFor(current);
fragmentIndex = typeof fragment === "number" ? Math.min(Math.max(fragment, 0), frags.length) : 0;
slides.forEach(function (slide, i) {
slide.classList.toggle("active", i === current - 1);
});
applyFragments(current, fragmentIndex);
if (progressEl) {
progressEl.textContent = current + " / " + total;
}
var slideTitle = slides[current - 1].dataset.title;
document.title = slideTitle ? slideTitle + " · " + baseTitle : baseTitle;
if (updateHash !== false) {
location.hash = fragmentIndex > 0 ? current + "." + fragmentIndex : String(current);
}
renderMermaid(current);
}
function next() {
var frags = fragmentsFor(current);
if (fragmentIndex < frags.length) {
show(current, fragmentIndex + 1);
return;
}
show(current + 1, 0);
}
function prev() {
if (fragmentIndex > 0) {
show(current, fragmentIndex - 1);
return;
}
var target = current - 1;
if (target < 1) return;
show(target, fragmentsFor(target).length);
}
function toggleGrid() {
gridMode = !gridMode;
document.body.classList.toggle("grid-mode", gridMode);
}
function exitGrid() {
if (!gridMode) return;
gridMode = false;
document.body.classList.remove("grid-mode");
}
window.addEventListener("hashchange", function () {
var h = parseHash();
show(h.slide, h.fragment, false);
});
document.addEventListener("keydown", function (e) {
if (e.key === "o" || e.key === "O") {
e.preventDefault();
toggleGrid();
return;
}
if (gridMode) {
if (e.key === "Escape") {
e.preventDefault();
exitGrid();
}
return;
}
switch (e.key) {
case "ArrowRight":
case " ":
case "PageDown":
e.preventDefault();
next();
break;
case "ArrowLeft":
case "PageUp":
e.preventDefault();
prev();
break;
case "Home":
e.preventDefault();
show(1, 0);
break;
case "End":
e.preventDefault();
show(total, 0);
break;
}
});
document.querySelectorAll(".nav-zone.prev, .nav-button.prev").forEach(function (el) {
el.addEventListener("click", prev);
});
document.querySelectorAll(".nav-zone.next, .nav-button.next").forEach(function (el) {
el.addEventListener("click", next);
});
slides.forEach(function (slide, i) {
slide.addEventListener("click", function () {
if (!gridMode) return;
exitGrid();
show(i + 1, 0);
});
});
var initial = parseHash();
show(initial.slide, initial.fragment, true);
})();